pf-git.ps1


function Initialize-Git_CLI {
    # Git return messages in sdterr by default, which can be interpreted as
    # an execution error even when the call succeded.
    # The following ensures output is redirected to stdout.
    # Execution error should be checked using $LASTEXITCODE
    $env:GIT_REDIRECT_STDERR = '2>&1'
}

function Get-Git_BranchName {
    git symbolic-ref HEAD | ForEach-Object { $_.TrimStart("refs/heads/") }
}

function Get-Git_RootFolder {
    $result = Invoke-Exe git rev-parse --show-toplevel
    $result = $result -replace '/', '\'
    return $result
}

function Get-Git_RepositoryName($remote = 'origin') {
    $remoteUrl = git remote get-url --push $remote
    if ($LASTEXITCODE -eq 0) {
        $result = Split-Path -Path $remoteUrl -Leaf
        return $result
    }
}

function Get-Git_Info($path) {
    $result = @{}

    try {
        if ($path) {
            Push-Location $path
        }
        Initialize-Git_CLI
        $result.Folder = Get-Git_RootFolder
        $result.Name = Get-Git_RepositoryName
        if (-not $result.Name) {
            $result.Name = Split-Path $result.Folder -Leaf
        }
        $result.Name = $result.Name.Replace('.','-') 
        $result.Branch = Get-Git_BranchName
        $result.LastAuthor = git log -1 --pretty=format:'%an'
        $result.CommitCount = git rev-list HEAD --count
        return $result
    }
    finally {
        if ($path) {
            Pop-Location
        }
    }
}

function Remove-Git_Branch {
    param (
        [Parameter(ValueFromPipeline=$true)]
        $pattern
    )
    begin {
        git config --global fetch.prune true
        git fetch
        git prune
        $b = git branch -r
    }
    process {
        $todelete = $b | Where-Object { $_ -like $pattern} | Update-Prefix -prefix ' origin/'
        foreach ($branch in $todelete ) {
            git push origin :$branch
        }
    }
}
function Remove-Git_Branch:::Example {
    @('*-JL-*', '*-JL') | Remove-Git_Branch
}

function Set-Git_Proxy {
    Param (
        [Parameter(ValueFromPipeline = $true)]
        $url = '',
        $proxy = '""'
    )
    process {
        $configEntry = if (-not $url) {
            "http.proxy"
        }
        else {
            "http." + '"' + $url + '"' + ".proxy"
        }
        
        git config --unset-all $configEntry
        git config $configEntry $proxy
    }
}
function Set-Git_Proxy:::Example {
     'http://proxyserver.gb.CORP.com', 'proxyserver' | Set-Git_Proxy 
}

function Get-Git_Author_Last {
    if (Test-GIT) {
        $lastCommit = Get-Git_Log 1
        $lastAuthor = $lastCommit.author
        $AuthorName = "$lastAuthor-$env:USERNAME" 
        return $AuthorName 
    }
}

function Set-Git_Author_Default {
    $AuthorName = Get-Git_Author_Last ?? "$env:USERNAME" 

    git config --global user.name $AuthorName
    git config --global user.email "$AuthorName@$env:USERDNSDOMAIN"
}

function Reset-Git_Configuration {
    $gitconfigPath = "$env:USERPROFILE\.gitconfig" 
    if ( test-path $gitconfigPath ) {
        Remove-Item $gitconfigPath
    }

    if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
        return
    }

    git config --global credential.helper wincred
    git config --global --unset-all user.name
    git config --global --unset-all user.email
    git config --global push.default simple

    if ( $src ) {
        Invoke-InLocation -path $src -script {
            Set-Git_Author_Default
            if (Test-GIT) {
                $branchname = ( Get-Git_Branch ).BranchName
                git config branch.$branchname.remote origin
                git config branch.$branchname.merge refs/heads/$branchname
            }
        }
    }
}

function Get-Git_HeadHash {
    return  git rev-parse --verify HEAD
}

function New-Git_Clone ($gitSource,  $target) {
    if (-not $gitSource ) {
        $gitSource = Get-PS_WorkingFolder
    }
    Invoke-Exe git clone --single-branch $gitSource  $target -q -l --no-hardlinks --recursive | Out-Null
}

function Get-Git_Remote ($name, [switch]$all) {
    $remotesRaw = Invoke-Exe git remote '-v'
    $fetchMark = '(fetch)'
    $pushMark = '(push)'

    function GetRemotes() {
        foreach ($remoteLine in $remotesRaw) {
            if (-not $remoteLine) {
                continue
            }
            $p = $remoteLine.split("`t")
            $url = $p[1].Trim()
            $fetch = $url.EndsWith($fetchMark)
            $push = $url.EndsWith($pushMark)
            if ($fetch) {
                $url = $url.SubString( 0, $url.length - $fetchMark.length).TrimEnd()
            }
            if ($push) {
                $url = $url.SubString( 0, $url.length - $pushMark.length).TrimEnd()
            }

            New-object PSObject -Property @{ 'name'= $p[0]; 'url' = $url; 'fetch' = $fetch; 'push' = $push }
        }
    }

    function GroupGitRemotesByName($remotes) {
        foreach ($group in ( $remotes | Group-Object -Property Name ) )
        {
            $result = $group.Group[0];
            foreach ($item in $group.Group  ) {
                $result.fetch = $result.fetch -or $item.fetch
                $result.push = $result.push -or $item.push
            }
            $result
        }
    }

    $result = GetRemotes
    if ($name) {
        $result = $result | Where-Object { $_.name -like $name }
    }

    elseif (-not $all) {
        $result = $result | Where-Object { $_.fetch -or $_.push } | Select-Object -First 1
    }
    $result = GroupGitRemotesByName $result
    $result
}

function Get-Git_Branch
{ 
    [CmdletBinding()]     
    Param 
    (
        # Param1 help description
        [Parameter()] 
        [switch]$Current, 
        [switch]$All, 
        [switch]$Raw 
    ) 

    function BranchDesc([string] $line){ 
        if ([String]::IsNullOrWhiteSpace($line)) {
            return
        }

        $outputColumns = [Ordered]@{ current = 2; BranchName = 50 }
        $branchLineInfo = $line | Split-StringInColumns -columns $outputColumns -Trim

        $branchName = $branchLineInfo.BranchName 
        
        $remotePrefix = 'remotes/'
        $isRemote = $branchName.StartsWith($remotePrefix)
        if ($isRemote){
            $branchName = $branchName.Substring($remotePrefix.Length)
        } 
        $obj = [pscustomobject] @{ 
            BranchName = $branchName 
            Current = $branchLineInfo.Current -eq '*' 
            Remote = $isRemote             
        } 
        $obj 
    } 

    if ($all){ 
        $gitArgs = "-a" 
    } 
    $branchLineList = Invoke-Exe git branch $gitArgs
    foreach($branchLine in $branchLineList ){ 
        if ($Raw){ 
            $branchLine
        } 
        else { 
            $branchInfo = BranchDesc $branchLine
            if ($Current) {
                if ( $branchInfo.Current) {
                    return $branchInfo
                }
            }
            else {
               $branchInfo
            }
        } 
    }
}
function Get-Git_Branch:::Example {
    Get-Git_Branch
}

function Compress-Git ([string]$baseCommit = "WIP") {
    $commits = Get-Git_Log 20
    $commit = $commits | Where-Object { $_.subject.Trim().StartsWith($baseCommit) } | 
        Select-Object -Last 1
    if ( -not $commit ) {
        throw "No Commit starts with '$baseCommit' "
    }

    $ref = $commit.HashFull + "~1"
    Invoke-Exe git reset $ref | Out-Null
    Add-GIT_Changes
    Invoke-Exe git commit -a -m $commit.subject | Out-Null
}

function Get-Git_Ref ([switch]$all) {
    $output = Invoke-Exe git show-ref
    $result = $output | 
        ForEach-Object { 
            $p = $_ -split ' '
            [PSCustomObject]@{'ref' = $p[1]; 'hash' = $p[0]; 'refhash' = ''; name = '' } 
          } | 
        ForEach-Object { 
            $_.refhash = "LG:$($_.ref):$($_.hash)";  
            $_.name = $_.ref.split('/')[-1]
            $_ 
          }
    $gitHead = Get-Git_HeadHash
    if (-not $all) {
        $result = $result | Where-Object { $_.hash -eq $gitHead } | Select-Object -First 1
    }
    $result
}

function Unlock-GIT_RepositoryFolder {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline=$true)]
        $path
    )
    process {
        if (Test-Path "$path\index.lock") {
            $ignoredOutput = Get-Process | Where-Object Name -like '*git*' | Stop-Process -Force 
            Write-Verbose $ignoredOutput
            Remove-Item "$path\index.lock" -Force
        }
    }
}

function Test-GIT {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline=$true)]
        $path
    )
    process {
        $path = Get-Path -path $path
        if ( -not $path ) {
            $path = Get-Location | Get-Path
        }

        $isGit = Invoke-InLocation -path $path { 
            Invoke-Exe git rev-parse -OkExitCode 0,128 | Out-Null #nooutput
            return $LASTEXITCODE -eq 0
        }
        if ($isGit) {
            return $path    
        }
    }
}
function Test-GIT:::Example {
    Test-GIT -path 'C:\code\PowerFrame'
}

function Clear-GIT {
    Invoke-Exe git clean -d -f -X | Out-Null
}

function Export-Git {
   param(
       [Parameter(ValueFromPipeLine=$true)]
       $gitFolder,
       $destination
   )
   process {
       if (-not (test-path $gitFolder)) {
           return
       }
       $name = Split-Path $gitFolder -Leaf
       New-Folder_EnsureExists $destination
       $outputFile = "$destination\$name.zip"
       Invoke-InLocation -path $gitFolder -script {
           Invoke-Exe git archive -o $outputFile --prefix=$name/ HEAD
       }
   }
}

function Add-GIT_Changes {
    Invoke-Exe git add -A | Out-Null
}

function push-GIT {
    $branch = Get-Git_Branch -Current
    Invoke-Exe git push origin ( $branch.BranchName ) --force | Out-Null
}

function Get-Git_Log ([int]$Count = 10) {
    $data = @{"Hash" = "h"; "HashFull" = "H"; "author" = "an"; "date" = "cd"; "subject" = "s"; "email" = "ae" }
    $psFormat = '[PSCustomObject]@{'
    foreach ($d in $data.GetEnumerator() ) {
        $tag = $d.Key
        $logToken = $d.Value
        $psFormat += if ( $tag -eq 'subject' ) { " $tag = @'`n%$logToken`n'@; " }
                        else { " $tag = '%$logToken'; " }
    }
    $endtoken = '#EXPEND'
    $psFormat += '} ' + $endtoken
    $psFormat = $psFormat | Update-String_Enclose '"'

    $exeArgs = Join-ExeArguments $MyInvocation.UnboundArguments
    $results = Invoke-Exe git log -$Count --date=iso8601 --pretty=$psFormat $exeArgs
    $results = $results -join "`n"
    $results = $results -split $endtoken
    function dateConv ($date) {
        $r = $date.Substring(0,10) + "T" + $date.Substring(11,8) `
            + $date.Substring(20,3) + ":" + $date.Substring(23,2)
        [DateTime]$r
    }

    foreach ($result in $results) {
        if (-not $result) { continue }
        $psResult = Invoke-Expression -Command $result
        $psResult.date = dateConv $psResult.date
        $psResult.subject = $psResult.subject.Trim()
        $psResult
    } 
}

function New-GIT_Commit {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline=$true)]
        $path,
        $message
    )
    process {
        if (-not $path)    {
            $path = Get-Location | Get-Path
        }
        if ( -not ( Test-Path $path ) ) {
            return
        }
        if (-not $message) {
            $date = Get-Date -Format "yyyyMMdd HHmmss"
            $message = "AutoCommit $date - $ENV:USERNAME" 
        }

        Initialize-Git_EnsureRepository -path $path
        Unlock-GIT_RepositoryFolder -path $path

# $pathQuoted = $path | Update-String_Enclose '"' -conditional
        $message = $message | Update-String_Enclose '"' -conditional

        New-Git_Ignore $path

        Invoke-InLocation $path -script {
        try {
            Invoke-Exe git config core.autocrlf false | out-null
            Invoke-Exe git config user.email "$env:username@$env:USERDNSDOMAIN" | out-null
            Invoke-Exe git config user.name "$env:username" | out-null
            Invoke-Exe git add -A | out-null
            Invoke-Exe git commit -a -m $message -OkExitCode 0, 1 | out-null
        }
        catch { }
        }
    }
}

function New-Git_Tag {
    param ( 
        # Other examples 'DEV', 'LIVE'
        $EnvName = 'UAT',
        $deployDate = ( get-date -format 'yyyyMMdd' )
    )

    $tag = "$EnvName-$deployDate"
    Invoke-Exe git tag -a $tag -m "$tag $env:userdomain\$env:username"
    Invoke-Exe git push origin $tag
}

function Initialize-Git_EnsureRepository {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline=$true)]
        $path
    )
    process {
        if (-not ( Test-GIT $path ) ) {
            $pathQuoted = $path | Update-String_Enclose '"' -conditional
            $dotGitPath = "$path\.git"
            if ( Test-Path $dotGitPath ) {
                Remove-Item -Path $dotGitPath -Recurse -Force
            }
            Invoke-Exe git init $pathQuoted | Out-Null #nooutput
        }
    }
}

function Copy-Git_Source ($src, $target, $branch) {
    if ( -not ( Test-GIT ) ) {
        throw " '$src' is not a git repository "
    }

    New-Folder_EnsureExists -folder $target

    Invoke-Exe git init $target -q | Out-Null #nooutput

    Invoke-InLocation -Path $target -script {
        Invoke-Exe git config receive.denyCurrentBranch updateInstead
        if ( Get-Git_Changed ) {
            $tempBranch = "WIP-" + ( Get-Date -Format 'yyyyDDmmhhmmss' )
            Invoke-Exe git clean -dfx | Out-Null #nooutput
            Invoke-Exe git reset --hard | Out-Null #nooutput
            Invoke-Exe git checkout -b $tempBranch | Out-Null #nooutput
        }
        $currentBranch = Get-Git_Branch -Current
        if ($branch -and ($currentBranch.BranchName -eq $branch)) {
            $tempBranch = "SYNC-$branch-" + ( Get-Date -Format 'yyyyDDmmhhmmss' )
            Invoke-Exe git clean -dfx | Out-Null #nooutput
            Invoke-Exe git reset --hard | Out-Null #nooutput
            Invoke-Exe git checkout -b $tempBranch | Out-Null #nooutput
        }
    }

    $gitSrcInfo = Invoke-InLocation -path $src -script {
        # Ensure the right branch name is displayed
        if ($branch) {
            #Invoke-Exe git add -A
            #Invoke-Exe git stash
            Invoke-Exe git checkout $branch -q | Out-Null #nooutput
        }

        Invoke-Exe git push "file://$target" -q --force | Out-Null #nooutput

        $gitInfo = [PSCustomObject]@{ 
            remotes = Get-Git_Remote -All 
            log = Get-Git_Log -Count 1
        }
        return $gitInfo
    }

    #$gitUserName = $gitSrcInfo.log.author
    #$gitUseremail = $gitSrcInfo.log.email

    $originUrl = ( $gitSrcInfo.remotes | Where-Object name -eq origin ).url
    Invoke-InLocation -Path $target -script {

        $currentRemotes = Get-Git_Remote -all
        $currentOriginUrl = ( $currentRemotes | Where-Object name -eq origin ).url
        if ( $currentOriginUrl -ne $originUrl ) {
            if ( $currentOriginUrl ) {
                Invoke-Exe git remote remove origin | Out-Null #nooutput
            }
            Invoke-Exe git remote add origin $originUrl | Out-Null #nooutput
        }
        
        if ($branch) {
            Invoke-Exe git checkout $branch -q --force -OkExitCode 0, 1 | Out-Null #nooutput

            Invoke-Exe git config "branch.$branch.remote" origin | Out-Null #nooutput
            Invoke-Exe git config "branch.$branch.merge" "refs/heads/$branch" | Out-Null #nooutput
        }

        Invoke-Exe git submodule update --init --recursive --remote -OkExitCode 0, 1 | Out-Null #nooutput
    }
}
function Copy-Git_Source:::Example {
  Copy-Git_Source -src c:\code\TBC\SRC -target '//192.168.150.205/c$/AppSource' -branch QA-FULL
}

function Get-GitCommitCount {
    $result = Invoke-Exe git rev-list --count HEAD
    return [int]$result
}

function Get-Git_Changed {
    param (
        [Parameter(ValueFromPipeline=$true)]
        $path,
        [int]$LastDaysAgo = [int]::MaxValue,
        [switch]$File,
        [switch]$folder,
        [switch]$NoFilter
    )
    begin {
        $currentFolder = Get-Location | Get-Path
        if ($LastDaysAgo -ge [int]::MaxValue) {
            $output = Invoke-Exe git status -s --ignore-submodules| Where-Object { $_ } 
            $output = $output | ForEach-Object { $_.substring(2).Trim() } #Remove file Status
        }
        else {
            $since = "$LastDaysAgo days ago" | Update-String_Enclose '"'
            $output = Invoke-Exe git log "--pretty=format: --name-only" --since=$since | Where-Object { $_ }
        }

        $files = $output | Update-Prefix -prefix '"' | Update-Suffix -Suffix '"'
        $files = $files | ForEach-Object { Join-Path $currentFolder $_ } | Select-Object -Unique
        $result = @()
        $result += if ($folder) {
                        $folders = $files | Get-Path_Ancestors | Select-Object -unique
                        $folders
                    }
        $result += if ($File) {
            $files
        }
        $result = $result | Sort-Object
    }
    process {
        $stringPath = Get-Path -path $path
        if ( $stringPath -iin $result ) {
             return $path
        }
    }
    end {
        if ($NoFilter) {
            $result
        }
    }
}
function Get-Git_Changed:::Example {
    Get-Git_Changed -NoFilter
    Get-Git_Changed -Folder -NoFilter
    Get-ChildItem -Filter *git.ps1 -Recurse | Get-Git_Changed -File
}