GitCommands.ps1

function __git {
    [CmdletBinding(PositionalBinding = $false)]
    param(
        [Parameter()]$GitDirOverride = $null,
        [Parameter(ValueFromRemainingArguments)]$OrdinaryArgs
    )

    if ($GitDirOverride) {
        $gitDirOption = "--git-dir=$GitDirOverride"
    }
    elseif ($gitDir) {
        $gitDirOption = "--git-dir=$gitDir"
    }
    else {
        $gitDirOption = $null
    }

    git $gitDirOption @gitCArgs @OrdinaryArgs
}


$script:__git_merge_strategies = $null
function gitListMergeStrategies {
    [OutputType([string[]])]
    param()

    if ($script:__git_merge_strategies -is [string[]]) {
        return $script:__git_merge_strategies 
    }

    function listMergeStrategies {
        param ()
        $ErrorActionPreference = 'Continue'
        (git merge -s help 2>&1 | Where-Object { $_ -match "[Aa]vailable strategies are: " }) -match ".*:\s*(.*)\s*\." | Out-Null
        return ($Matches[1] -split " ")
    }

    try {
        $LANG = $env:LANG
        $LC_ALL = $env:LC_ALL
        $env:LANG = "C"
        $env:LC_ALL = "C"

        return $script:__git_merge_strategies = (listMergeStrategies)
    }
    finally {
        $env:LANG = $LANG
        $env:LC_ALL = $LC_ALL
    }
}

# -- compute config --
$script:__git_config_vars = $null
function gitConfigVars {
    [OutputType([string[]])] param()
    if ($script:__git_config_vars) {
        return $script:__git_config_vars
    }
    return $script:__git_config_vars = (git help --config-for-completion)
}

$script:__git_config_vars_all = $null
function gitConfigVarsAll {
    [OutputType([string[]])] param()
    if ($script:__git_config_vars_all) {
        return $script:__git_config_vars_all
    }
    return $script:__git_config_vars_all = (git --no-pager help --config)
}

$script:__git_config_sections = $null
function gitConfigSections {
    [OutputType([string[]])] param()
    if ($script:__git_config_sections) {
        return $script:__git_config_sections
    }
    return $script:__git_config_sections = (git help --config-sections-for-completion)
}

$script:__git_first_level_config_vars_for_section = $null
function gitFirstLevelConfigVarsForSection {
    [OutputType([string[]])]
    param (
        [Parameter(Position = 0, Mandatory)][string] $section
    )
    if (-not $script:__git_first_level_config_vars_for_section) {
        $script:__git_first_level_config_vars_for_section = @{}
    }

    if ($script:__git_first_level_config_vars_for_section[$section]) {
        return $script:__git_first_level_config_vars_for_section[$section]
    }

    return $script:__git_first_level_config_vars_for_section[$section] = (
        gitConfigVars | ForEach-Object {
            $s = $_.Split(".")
            if (($section -eq $s[0]) -and $s[1]) {
                return $s[1] 
            }
        }
    )
}

$script:__git_second_level_config_vars_for_section = $null
function gitSecondLevelConfigVarsForSection {
    [OutputType([string[]])]
    param (
        [Parameter(Position = 0, Mandatory)][string] $section
    )
    if (-not $script:__git_second_level_config_vars_for_section) {
        $script:__git_second_level_config_vars_for_section = @{}
    }

    if ($script:__git_second_level_config_vars_for_section[$section]) {
        return $script:__git_second_level_config_vars_for_section[$section]
    }

    return $script:__git_second_level_config_vars_for_section[$section] = (
        gitConfigVarsAll | ForEach-Object {
            $s = $_.Split(".")
            if (($section -eq $s[0]) -and $s[2]) {
                return $s[2] 
            }
        }
    )
}

function gitAllCommands {
    [CmdletBinding()]
    [OutputType([string[]])]
    param(
        [Parameter(Mandatory, ValueFromRemainingArguments)]
        [string[]]
        $Categories
    )
    $list = ($Categories -join ",")
    return (__git "--list-cmds=$list" | Sort-Object -CaseSensitive)
}

# Discovers the path to the git repository taking any '--git-dir=<path>' and
# '-C <path>' options into account and stores it in the $__git_repo_path
# variable.
function gitRepoPath {
    [OutputType([string])]
    param()
    if ($gitCArgs) {
        return (__git rev-parse --absolute-git-dir 2>$null)
    }
    elseif ($gitDir) {
        return "$gitDir"
    }
    elseif ($env:GIT_DIR) {
        return $env:GIT_DIR
    }
    elseif (Test-Path -Path ".git" -PathType Container) {
        return ".git"
    }
    else {
        return (git rev-parse --git-dir 2>$null)
    }
}

function gitCompleteRefs {
    [OutputType([string[]])]
    param(
        [Parameter(Mandatory)][AllowEmptyString()][string] $Current,
        [string] $Remote = "",
        [string] $Prefix = "",
        [string] $Suffix = "",
        [ValidateSet('refs', 'heads', 'remote-heads')][string] $Mode = "refs",
        [switch] $dwim
    )


    switch ($Mode) {
        'refs' { 
            $result = (gitRefs -Current $Current -Prefix $Prefix -Suffix $Suffix -Remote $Remote)
        }
        'heads' { 
            $result = (gitHeads -Current $Current -Prefix $Prefix -Suffix $Suffix)
        }
        'remote-heads' { 
            $result = (gitRemoteHeads -Current $Current -Prefix $Prefix -Suffix $Suffix)
        }
    }
    
    if ($dwim) {
        $result += (gitDwimRemoteHeads -Current $Current -Prefix $Prefix -Suffix $Suffix)
    }

    return [string[]]$result
}


# Lists branches from the local repository.
# 1: A prefix to be added to each listed branch (optional).
# 2: List only branches matching this word (optional; list all branches if
# unset or empty).
# 3: A suffix to be appended to each listed branch (optional).
function gitHeads {
    [OutputType([string[]])]
    param(
        [Parameter(Mandatory)][AllowEmptyString()][string] $Current,
        [Parameter(Mandatory)][AllowEmptyString()][string] $Prefix,
        [Parameter(Mandatory)][AllowEmptyString()][string] $Suffix
    )

    $ForeachPrefix = "$Prefix".Replace('%', '%%')
    $ignoreCase = $null
    if ($env:GIT_COMPLETION_IGNORE_CASE) {
        $ignoreCase = '--ignore-case'
    }

    __git for-each-ref --format="$ForeachPrefix%(refname:strip=2)$Suffix" `
        $ignoreCase `
        "refs/heads/$Current*" "refs/heads/$Current*/**"
}

# Lists branches from remote repositories.
# 1: A prefix to be added to each listed branch (optional).
# 2: List only branches matching this word (optional; list all branches if
# unset or empty).
# 3: A suffix to be appended to each listed branch (optional).
function gitRemoteHeads {
    [OutputType([string[]])]
    param(
        [Parameter(Mandatory)][AllowEmptyString()][string] $Current,
        [Parameter(Mandatory)][AllowEmptyString()][string] $Prefix,
        [Parameter(Mandatory)][AllowEmptyString()][string] $Suffix
    )

    $ForeachPrefix = "$Prefix".Replace('%', '%%')
    $ignoreCase = $null
    if ($env:GIT_COMPLETION_IGNORE_CASE) {
        $ignoreCase = '--ignore-case'
    }

    __git for-each-ref --format="$ForeachPrefix%(refname:strip=2)$Suffix" `
        $ignoreCase `
        "refs/remotes/$Current*" "refs/remotes/$Current*/**"
}

# Lists tags from the local repository.
# Accepts the same positional parameters as gitHeads() above.
function gitTags {
    [OutputType([string[]])]
    param(
        [Parameter(Mandatory)][AllowEmptyString()][string] $Current,
        [Parameter(Mandatory)][AllowEmptyString()][string] $Prefix,
        [Parameter(Mandatory)][AllowEmptyString()][string] $Suffix
    )

    $ForeachPrefix = "$Prefix".Replace('%', '%%')
    $ignoreCase = $null
    if ($env:GIT_COMPLETION_IGNORE_CASE) {
        $ignoreCase = '--ignore-case'
    }

    __git for-each-ref --format="$ForeachPrefix%(refname:strip=2)$Suffix" `
        $ignoreCase `
        "refs/tags/$Current*" "refs/tags/$Current*/**"
}

# List unique branches from refs/remotes used for 'git checkout' and 'git
# switch' tracking DWIMery.
# 1: A prefix to be added to each listed branch (optional)
# 2: List only branches matching this word (optional; list all branches if
# unset or empty).
# 3: A suffix to be appended to each listed branch (optional).
function gitDwimRemoteHeads {
    [OutputType([string[]])]
    param(
        [Parameter(Mandatory)][AllowEmptyString()][string] $Current,
        [Parameter(Mandatory)][AllowEmptyString()][string] $Prefix,
        [Parameter(Mandatory)][AllowEmptyString()][string] $Suffix
    )

    $ForeachPrefix = "$Prefix".Replace('%', '%%')
    $ignoreCase = $null
    if ($env:GIT_COMPLETION_IGNORE_CASE) {
        $ignoreCase = '--ignore-case'
    }

    __git for-each-ref --format="$ForeachPrefix%(refname:strip=3)$Suffix" `
        --sort="refname:strip=3" `
        $ignoreCase `
        "refs/remotes/*/$Current*" "refs/remotes/*/$Current*/**" | 
    Group-Object |
    Where-Object Count -EQ 1 |
    Select-Object -ExpandProperty Name
}


# Lists refs from the local (by default) or from a remote repository.
# It accepts 0, 1 or 2 arguments:
# 1: The remote to list refs from (optional; ignored, if set but empty).
# Can be the name of a configured remote, a path, or a URL.
# 2: In addition to local refs, list unique branches from refs/remotes/ for
# 'git checkout's tracking DWIMery (optional; ignored, if set but empty).
# 3: A prefix to be added to each listed ref (optional).
# 4: List only refs matching this word (optional; list all refs if unset or
# empty).
# 5: A suffix to be appended to each listed ref (optional; ignored, if set
# but empty).
#
# Use gitCompleteRefs() instead.
function gitRefs {
    [OutputType([string[]])]
    param(
        [Parameter(Mandatory)][AllowEmptyString()][string] $Remote,
        [Parameter(Mandatory)][AllowEmptyString()][string] $Current,
        [Parameter(Mandatory)][AllowEmptyString()][string] $Prefix,
        [Parameter(Mandatory)][AllowEmptyString()][string] $Suffix,
        [string] $Track = ""
    )
    
    $listRefsFrom = "path"
    $match = $Current
    $umatch = $Current
    $ForeachPrefix = "$Prefix".Replace('%', '%%')
    $ignoreCase = $null

    $dir = (gitRepoPath)
    if (-not $Remote) {
        if (-not $dir) {
            return @()
        }
    }
    else {
        if (gitIsConfiguredRemote) {
            # configured remote takes precedence over a
            # local directory with the same name
            $listRefsFrom = "remote"
        }
        elseif (Test-Path -Path "$Remote/.git" -PathType Container) {
            $dir = "$Remote/.git"
        }
        elseif (Test-Path -Path "$Remote" -PathType Container) {
            $dir = "$Remote"
        }
        else {
            $listRefsFrom = "url"
        }
    }

    if ($env:GIT_COMPLETION_IGNORE_CASE -and ($null -ne $env:GIT_COMPLETION_IGNORE_CASE)) {
        $ignoreCase = '--ignore-case'
        $umatch = $Current.ToUpperInvariant()
    }

    if ($listRefsFrom -eq "path") {
        if ($Current.StartsWith("^")) {
            $Prefix = "$Prefix^"
            $ForeachPrefix = "$ForeachPrefix^"
            $Current = $Current.Substring(1)
            $match = $match.Substring(1)
            $umatch = $umatch.Substring(1)
        }

        if ($Current -match "refs(/.*)?") {
            $format = "refname"
            $refs = @("$match*", "$match*/**")
            $Track = ""
        }
        else {
            foreach ($i in ("HEAD", "FETCH_HEAD", "ORIG_HEAD", "MERGE_HEAD", "REBASE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD", "BISECT_HEAD", "AUTO_MERGE")) {
                if (($i -clike "$match*") -or ($i -clike "$umatch*")) {
                    if (Test-Path "$dir/$i" -PathType Leaf) {
                        "$Prefix$i$Suffix"
                    }
                }
            }

            $format = "refname:strip=2"
            $refs = @("refs/tags/$match*",
                "refs/tags/$match*/**",
                "refs/heads/$match*",
                "refs/heads/$match*/**",
                "refs/remotes/$match*", 
                "refs/remotes/$match*/**")
        }
        __git -GitDirOverride $dir for-each-ref "--format=$ForeachPrefix%($format)$Suffix" $ignoreCase @refs
        if ($Track) {
            gitDwimRemoteHeads -Prefix $Prefix -Current $match -Suffix $Suffix
        }
        return
    }
    
    if ($Current -match "refs(/.*)?") {
        __git ls-remote "$Remote" "$Match*" | 
        ForEach-Object {
            $_ -match "(\S+)\s+(\S+)" | Out-Null
            $i = $Matches[2]
            if ($i -notlike "*^{}") {
                "$Prefix$i$Suffix"
            }
        }
    }
    elseif ($listRefsFrom -eq "remote") {
        if ("HEAD" -match "$match*") {
            "${Prefix}HEAD$Suffix"
        }
        __git for-each-ref --format="$ForeachPrefix%(refname:strip=3)$Suffix" `
            $ignoreCase `
            "refs/remotes/$remote/$match*" "refs/remotes/$remote/$match*/**"
    }
    else {
        $querySymref = $null
        if ("HEAD" -match "$match*") {
            $querySymref = 'HEAD'
        }
        __git ls-remote "$Remote" $querySymref "refs/tags/$match*" "refs/heads/$match*" "refs/remotes/$match*" |
        ForEach-Object {
            $_ -match "(\S+)\s+(\S+)" | Out-Null
            $i = $Matches[2]
            if ($i -notlike "*^{}") {
                if ($i.StartsWith('refs/*')) {
                    $j = $i.Substring('refs/*'.Length)
                    "$Prefix$j$Suffix"
                }
                else {
                    "$Prefix$i$Suffix"
                }
            }
        }
    }
}


# Returns true if $1 matches the name of a configured remote, false otherwise.
function gitIsConfiguredRemote {
    [OutputType([bool])]
    param([Parameter(Mandatory)][string]$Remote)
    return (__git remote | Where-Object { $_ -eq $Remote })
}

function gitListAliases {
    [OutputType([PSCustomObject[]])]
    param()

    __git config -l | ForEach-Object {
        if ($_ -match "^alias\.([^=]+)=(.*)") {
            [PSCustomObject]@{
                Name  = $Matches[1];
                Value = $Matches[2];
            }
        }
    }
}