Complete/SubCommand/Config.ps1

# Copyright (C) 2024 kzrnm
# Based on git-completion.bash (https://github.com/git/git/blob/HEAD/contrib/completion/git-completion.bash).
# Distributed under the GNU General Public License, version 2.0.
using namespace System.Management.Automation;

function Complete-GitSubCommand-config {
    [CmdletBinding(PositionalBinding = $false)]
    [OutputType([CompletionResult[]])]
    param(
        [CommandLineContext]
        [Parameter(Position = 0, Mandatory)]$Context
    )

    $gitVersion = gitVersion
    if ($gitVersion -lt [version]::new(2, 46)) {
        Complete-GitSubCommand-config-Git2_45 $Context
        return
    }

    [string] $Prev = $Context.PreviousWord()
    [string] $Current = $Context.CurrentWord()

    $subcommands = gitResolveBuiltins $Context.Command
    [string] $subcommand = $Context.SubcommandWithoutGlobalOption()
    if ($subcommand -notin $subcommands) {
        if ($Context.HasDoubledash()) { return }
        if ($Current -eq '-') {
            $script:__helpCompletion
            return
        }
        else {
            $subcommands | gitcomp -Current $Current -DescriptionBuilder { 
                switch ($_) {
                    "list" { 'List all variables set in config file' }
                    "get" { 'Emits the value of the specified key' }
                    "set" { 'Set value for one or more config options' }
                    "unset" { 'Unset value for one or more config options' }
                    "rename-section" { 'Rename the given section to a new name' }
                    "remove-section" { 'Remove the given section from the configuration file' }
                    "edit" { 'Opens an editor to modify the specified config file' }
                }
            }
            return
        }
    }
    
    $shortOpts = Get-GitShortOptions $Context.Command -Subcommand $subcommand -Current $Current
    if ($shortOpts) { return $shortOpts }

    if ($Current.StartsWith('--')) {
        gitCompleteResolveBuiltins $Context.Command $subcommand -Current $Current
        return
    }

    if ($Prev.StartsWith('--')) {
        if ((gitResolveBuiltins $Context.Command $subcommand) -eq "$Prev=") {
            return @()
        }
    }

    switch ($subcommand) {
        'get' { completeGitConfigGetSetVariables $Context }
        'unset' { completeGitConfigGetSetVariables $Context }
        'set' {
            if ($Prev -like '*.*') {
                completeConfigVariableValue -VarName $Prev -Current $Current
            }
            else {
                completeConfigVariableName -Current $Current
            }
        }
    }

    return
}
function Complete-GitSubCommand-config-Git2_45 {
    [CmdletBinding(PositionalBinding = $false)]
    [OutputType([CompletionResult[]])]
    param(
        [CommandLineContext]
        [Parameter(Position = 0, Mandatory)]$Context
    )
    $Prev = $Context.PreviousWord()
    $Current = $Context.CurrentWord()
    if (!$Context.HasDoubledash()) {
        $shortOpts = Get-GitShortOptions $Context.Command -Subcommand $subcommand -Current $Current
        if ($shortOpts) { return $shortOpts }
        if ($Prev -in ('--get', '--get-all', '--unset', '--unset-all')) {
            completeGitConfigGetSetVariables $Context
            return
        }
        if ($Current.StartsWith('--')) {
            gitCompleteResolveBuiltins $Context.Command -Current $Current
            return
        }
    }
    if ($Prev -like '*.*') {
        completeConfigVariableValue -Current $Current -VarName $Prev
    }
    else {
        completeConfigVariableName -Current $Current
    }
}

function completeGitConfigGetSetVariables {
    [CmdletBinding(PositionalBinding = $false)]
    [OutputType([string[]])]
    param(
        [Parameter(Position = 0, Mandatory)][CommandLineContext]$Context
    )

    gitConfigGetSetVariables $Context | completeList -Current $Context.CurrentWord() -ResultType ParameterValue -DescriptionBuilder {
        Get-GitConfigVariableDescription $_
    }
}

# __git_config_get_set_variables
function gitConfigGetSetVariables {
    [CmdletBinding(PositionalBinding = $false)]
    [OutputType([string[]])]
    param(
        [Parameter(Position = 0, Mandatory)][CommandLineContext]$Context
    )
    $file = @()
    $prev = ''
    for ($i = $Context.DoubledashIndex - 1; $i -gt $Context.CommandIndex; $i--) {
        $word = $Context.Words[$i]
        if (($word -in @('--system', '--global', '--local')) -or ($word -like "--file=*")) {
            $file = @($word)
            break
        }
        elseif ($word -cmatch '^-([^-]*f|-file)$') {
            $file = @('--file', $prev)
        }
        $prev = $word
    }
    __git config @file --name-only --list
}


# __git_complete_config_variable_name_and_value
function completeConfigOptionVariableNameAndValue {
    [OutputType([CompletionResult[]])]
    param(
        [Parameter(Mandatory)][AllowEmptyString()][string] $Current,
        [string] $Prefix = ''
    )

    if ($Current -match '(.*)=(.*)') {
        $VarName = $Matches[1]
        return (completeConfigVariableValue -VarName $VarName -Prefix "$Prefix$VarName=" -Current $Matches[2])
    }
    else {
        return (completeConfigVariableName -Prefix $Prefix -Suffix "=" -Current $Current)
    }
}


# __git_complete_config_variable_value
function completeConfigVariableValue {
    [OutputType([CompletionResult[]])]
    param(
        [Parameter(Mandatory)][AllowEmptyString()][string] $Current,
        [Parameter(Mandatory)][AllowEmptyString()][string] $VarName,
        [string] $Prefix = ''
    )

    function remote {
        $remotes = [string[]](__git remote)
        $remotes | completeList -Current $Current -Prefix $Prefix -ResultType ParameterValue
    }

    $v = $VarName.ToLowerInvariant()
    $candidates = switch -Wildcard ($v) {
        'branch.*.remote' {
            gitRemote
            continue
        }
        'branch.*.pushremote' {
            gitRemote
            continue
        }
        'branch.*.pushdefault' {
            gitRemote
            continue
        }
        'remote.pushdefault' {
            gitRemote
            continue
        }
        'branch.*.merge' {
            gitCompleteRefs -Current $Current -Prefix $Prefix
            return
        }
        'branch.*.rebase' {
            $gitPullRebaseConfig
            continue
        }
        'remote.*.fetch' {
            if ($Current -eq '') {
                $result = 'refs/heads'
                return [CompletionResult]::new(
                    "${Prefix}$result",
                    $result,
                    'ParameterValue',
                    $result
                )
            }
            try {
                $remote = ($v -replace '^remote\.' -replace '\.fetch$')
                (__git ls-remote $remote 'refs/heads/*') -match "(\S+)\s+(\S+)" > $null
                # $hash = $Matches[1]
                $ref = $Matches[2]
                $result = "${ref}:refs/remotes/$remote/$($ref -replace '^refs/heads/')"
                return [CompletionResult]::new(
                    "${Prefix}$result",
                    $result,
                    'ParameterValue',
                    $result
                )
            }
            catch {
                # not found
            }
            return
        }
        'remote.*.push' {
            __git for-each-ref --format='%(refname):%(refname)' refs/heads
            continue
        }
        'pull.twohead' {
            gitListMergeStrategies
            continue
        }
        'pull.octopus' {
            gitListMergeStrategies
            continue
        }
        'color.pager' {
            'false', 'true'
            continue
        }
        'color.*.*' {
            'normal', 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', 'bold', 'dim', 'ul', 'blink', 'reverse'
            continue
        }
        'color.*' {
            'false', 'true', 'always', 'never', 'auto'
            continue
        }
        'diff.submodule' {
            $script:gitDiffSubmoduleFormats
            continue
        }
        'diff.algorithm' {
            $script:gitDiffAlgorithms
            continue
        }
        'http.proxyAuthMethod' {
            $gitHttpProxyAuthMethod
            continue
        }
        'help.format' {
            'man', 'info', 'web', 'html'
            continue
        }
        'log.date' {
            $script:gitLogDateFormats
            continue
        }
        'sendemail.aliasfiletype' {
            'mutt', 'mailrc', 'pine', 'elm', 'gnus'
            continue
        }
        'sendemail.confirm' {
            $script:gitSendEmailConfirmOptions
            continue
        }
        'sendemail.suppresscc' {
            $script:gitSendEmailSuppressccOptions
            continue
        }
        'sendemail.transferencoding' {
            '7bit', '8bit', 'quoted-printable', 'base64'
            continue
        }
        # git-completion.bash does not complete below cases.
        'merge.conflictStyle' {
            $script:gitConflictSolver
            continue
        }
        'push.recurseSubmodules' {
            $script:gitPushRecurseSubmodules
            continue
        }
        'fetch.recurseSubmodules' {
            $script:gitFetchRecurseSubmodules
            continue
        }
        'diff.colorMoved' {
            $script:gitColorMovedOpts
            continue
        }
        'diff.colorMovedWS' {
            $script:gitColorMovedWsOpts
            continue
        }
        'diff.wsErrorHighlight' {
            $script:gitWsErrorHighlightOpts
            continue
        }
        'column.*' {
            if ($v.Substring(7) -cnotin @('ui', 'branch', 'clean', 'status', 'tag')) {
                return
            }
            $script:gitColumnUiPatterns
            continue
        }
        'pull.rebase' {
            $gitPullRebaseConfig
            continue
        }
    }

    $candidates | completeList -Current $Current -Prefix $Prefix -ResultType ParameterValue
}

# __git_complete_config_variable_name
function completeConfigVariableName {
    [OutputType([CompletionResult[]])]
    param(
        [Parameter(Mandatory)][AllowEmptyString()][string] $Current,
        [string] $Prefix = '',
        [string] $Suffix = ''
    )

    $DescriptionBuilder = [scriptblock] {
        Get-GitConfigVariableDescription $_
    }

    if ($Current -match "(branch|guitool|difftool|man|mergetool|remote|submodule|url)\.(.*)\.([^\.]*)") {
        $section = $Matches[1]
        $second = $Matches[2]
        gitSecondLevelConfigVarsForSection $section | ForEach-Object {
            "$section.$second.$_"
        } | completeList -Current $Current -Prefix $Prefix -DescriptionBuilder $DescriptionBuilder -Suffix $Suffix
        return
    }
    if ($Current -match "branch\.(.*)") {
        $section = 'branch'
        $second = $Matches[1]
        gitHeads -Current $second | ForEach-Object { "$section.$_." } | completeList -DescriptionBuilder $DescriptionBuilder
        gitFirstLevelConfigVarsForSection $section | ForEach-Object {
            "$section.$_"
        } | completeList -Current $Current -Prefix $Prefix -DescriptionBuilder $DescriptionBuilder -Suffix $Suffix
        return
    }
    if ($Current -match "pager\.(.*)") {
        $section = 'pager'
        $second = $Matches[1]
        gitAllCommands main others alias nohelpers | ForEach-Object {
            "$section.$_"
        } | completeList -Current $Current -Prefix $Prefix -DescriptionBuilder $DescriptionBuilder -Suffix $Suffix
        return
    }
    if ($Current -match "remote\.(.*)") {
        $section = 'remote'
        $second = $Matches[1]
        
        __git remote | Where-Object {
            $_.StartsWith($second) 
        } | ForEach-Object {
            "$section.$_."
        } | completeList -Current $Current -Prefix $Prefix -DescriptionBuilder $DescriptionBuilder

        gitFirstLevelConfigVarsForSection $section | ForEach-Object {
            "$section.$_"
        } | completeList -Current $Current -Prefix $Prefix -DescriptionBuilder $DescriptionBuilder -Suffix $Suffix
        return
    }
    if ($Current -match "submodule\.(.*)") {
        $section = 'submodule'
        $second = $Matches[1]
        $gitTopPath = (__git rev-parse --show-toplevel)

        __git config -f "$gitTopPath/.gitmodules" --name-only --list 2>$null |
        ForEach-Object {
            if ($_ -match 'submodule\.(.*)\.path') {
                $sub = $Matches[1]
                "$section.$sub."
            }
        } | completeList -Current $Current -Prefix $Prefix -DescriptionBuilder $DescriptionBuilder

        gitFirstLevelConfigVarsForSection $section | ForEach-Object {
            "$section.$_"
        } | completeList -Current $Current -Prefix $Prefix -DescriptionBuilder $DescriptionBuilder -Suffix $Suffix
        return
    }
    if ($Current -match "([^\.]*)\.(.*)") {
        $section = $Matches[1]
        gitConfigVars | Where-Object { !$_.EndsWith('.') } | completeList -Current $Current -Prefix $Prefix -DescriptionBuilder $DescriptionBuilder -Suffix $Suffix
        return
    }
    else {
        gitConfigSections | ForEach-Object { "$_." } | completeList -Current $Current -Prefix $Prefix -DescriptionBuilder $DescriptionBuilder
    }
    return
}