src/GitCompletion.ps1
|
#requires -Version 5.1 # GitCompletion.ps1 - Comprehensive tab completion for power-git # # Improvements over posh-git: # - Uses Register-ArgumentCompleter (PS 5.0+) properly # - Handles git aliases transparently # - Completes --flag=value patterns # - Smarter context detection (e.g. only suggest untracked files for 'git add') # - Handles git worktrees # - Subcommand delegation pattern for cleaner code # ────────────────────────────────────────────────────────────────────────────── # Helpers # ────────────────────────────────────────────────────────────────────────────── function New-CompletionResult { param( [string]$CompletionText, [string]$ListItemText = $CompletionText, [string]$ToolTip = $CompletionText, [System.Management.Automation.CompletionResultType]$Type = [System.Management.Automation.CompletionResultType]::ParameterValue ) [System.Management.Automation.CompletionResult]::new($CompletionText, $ListItemText, $Type, $ToolTip) } function Get-CompletionPrefix { param([string]$Word) # Return the word as-is; used for filtering lists return $Word } function Complete-List { param( [string]$Prefix, [string[]]$Items, [System.Management.Automation.CompletionResultType]$Type = [System.Management.Automation.CompletionResultType]::ParameterValue ) foreach ($item in $Items) { if ($item.StartsWith($Prefix, [System.StringComparison]::OrdinalIgnoreCase)) { New-CompletionResult -CompletionText $item -Type $Type } } } # ────────────────────────────────────────────────────────────────────────────── # Git ref completions (branches, tags, HEAD~n, etc.) # ────────────────────────────────────────────────────────────────────────────── function Complete-GitRef { param([string]$Prefix, [switch]$LocalOnly, [switch]$TagsOnly, [switch]$IncludeTags) if (-not $TagsOnly) { $local = Get-GitLocalBranches Complete-List -Prefix $Prefix -Items $local if (-not $LocalOnly) { $remote = Get-GitRemoteBranches Complete-List -Prefix $Prefix -Items $remote } } if ($TagsOnly -or $IncludeTags) { $tags = Get-GitTags Complete-List -Prefix $Prefix -Items $tags } # Special refs Complete-List -Prefix $Prefix -Items @('HEAD', 'ORIG_HEAD', 'FETCH_HEAD', 'MERGE_HEAD') } function Complete-GitRemote { param([string]$Prefix) $remotes = Get-GitRemotes Complete-List -Prefix $Prefix -Items $remotes } function Complete-GitStash { param([string]$Prefix) $stashes = Invoke-Git 'stash' 'list' '--format=%gd' Complete-List -Prefix $Prefix -Items @($stashes) } function Complete-GitFile { param([string]$Prefix, [switch]$Modified, [switch]$Staged, [switch]$Untracked) if ($Modified) { $files = Invoke-Git 'diff' '--name-only' Complete-List -Prefix $Prefix -Items @($files) return } if ($Staged) { $files = Invoke-Git 'diff' '--staged' '--name-only' Complete-List -Prefix $Prefix -Items @($files) return } if ($Untracked) { $files = Invoke-Git 'ls-files' '--others' '--exclude-standard' Complete-List -Prefix $Prefix -Items @($files) return } # Default: tracked + untracked $files = Invoke-Git 'ls-files' '--cached' '--others' '--exclude-standard' Complete-List -Prefix $Prefix -Items @($files) } function Complete-GitWorktree { param([string]$Prefix) $lines = Invoke-Git 'worktree' 'list' '--porcelain' $paths = $lines | Where-Object { $_ -match '^worktree ' } | ForEach-Object { ($_ -split ' ',2)[1] } Complete-List -Prefix $Prefix -Items @($paths) } # ────────────────────────────────────────────────────────────────────────────── # Top-level git subcommands # ────────────────────────────────────────────────────────────────────────────── $script:GitCommands = @( # Porcelain commands 'add', 'am', 'archive', 'bisect', 'branch', 'bundle', 'checkout', 'cherry-pick', 'citool', 'clean', 'clone', 'commit', 'describe', 'diff', 'fetch', 'format-patch', 'gc', 'grep', 'gui', 'init', 'log', 'merge', 'mv', 'notes', 'pull', 'push', 'range-diff', 'rebase', 'reset', 'restore', 'revert', 'rm', 'shortlog', 'show', 'sparse-checkout', 'stash', 'status', 'submodule', 'switch', 'tag', 'worktree', # Plumbing (commonly typed) 'apply', 'cat-file', 'check-ref-format', 'checkout-index', 'commit-graph', 'commit-tree', 'diff-index', 'diff-tree', 'for-each-ref', 'hash-object', 'ls-files', 'ls-remote', 'ls-tree', 'merge-base', 'merge-tree', 'pack-objects', 'read-tree', 'rev-list', 'rev-parse', 'show-ref', 'symbolic-ref', 'unpack-objects', 'update-index', 'update-ref', 'write-tree', # Extra 'bisect', 'blame', 'cherry', 'config', 'help', 'reflog', 'remote', 'rerere', 'send-email', 'svn', 'whatchanged' ) # ────────────────────────────────────────────────────────────────────────────── # Per-subcommand completion delegates # ────────────────────────────────────────────────────────────────────────────── $script:SubcommandCompleters = @{} # git add $script:SubcommandCompleters['add'] = { param([string]$Prefix, [string[]]$PrevWords) $flags = @('-A','--all','-u','--update','-n','--dry-run','-p','--patch', '-e','--edit','-f','--force','-i','--interactive', '--chmod=+x','--chmod=-x','--intent-to-add','-N') if ($Prefix.StartsWith('-')) { Complete-List -Prefix $Prefix -Items $flags } else { Complete-GitFile -Prefix $Prefix -Untracked } } # git branch $script:SubcommandCompleters['branch'] = { param([string]$Prefix, [string[]]$PrevWords) $flags = @('-a','--all','-r','--remotes','-v','--verbose','--merged', '--no-merged','-d','--delete','-D','--force-delete', '-m','--move','-c','--copy','-l','--list', '--set-upstream-to','--unset-upstream','--track','--no-track', '--contains','--no-contains','--points-at','--format') if ($Prefix.StartsWith('-')) { Complete-List -Prefix $Prefix -Items $flags } else { Complete-GitRef -Prefix $Prefix -LocalOnly } } # git checkout / git switch $script:SubcommandCompleters['checkout'] = { param([string]$Prefix, [string[]]$PrevWords) $flags = @('-b','-B','-t','--track','--no-track','-l','-d','--detach', '-f','--force','-m','--merge','-p','--patch','--ours','--theirs', '--orphan','--conflict','--ignore-skip-worktree-bits','--recurse-submodules') if ($Prefix.StartsWith('-')) { Complete-List -Prefix $Prefix -Items $flags } else { Complete-GitRef -Prefix $Prefix -IncludeTags } } $script:SubcommandCompleters['switch'] = $script:SubcommandCompleters['checkout'] # git restore $script:SubcommandCompleters['restore'] = { param([string]$Prefix, [string[]]$PrevWords) $flags = @('-s','--source','-p','--patch','-W','--worktree', '-S','--staged','--ours','--theirs','--merge','--conflict', '--ignore-unmerged','--ignore-skip-worktree-bits','--recurse-submodules', '--overlay','--no-overlay','--pathspec-from-file','--pathspec-file-nul') if ($Prefix.StartsWith('-')) { Complete-List -Prefix $Prefix -Items $flags } else { Complete-GitFile -Prefix $Prefix -Modified } } # git commit $script:SubcommandCompleters['commit'] = { param([string]$Prefix, [string[]]$PrevWords) $flags = @('-a','--all','-p','--patch','-C','--reuse-message', '-c','--reedit-message','-F','--file','-m','--message', '-t','--template','--signoff','-s','--squash','--fixup', '--reset-author','--allow-empty','--allow-empty-message', '--no-edit','--amend','--no-post-rewrite','-n','--no-verify', '-v','--verbose','-q','--quiet','--dry-run','--status','--no-status', '--gpg-sign','-S','--no-gpg-sign','--trailer','--cleanup') Complete-List -Prefix $Prefix -Items $flags } # git diff $script:SubcommandCompleters['diff'] = { param([string]$Prefix, [string[]]$PrevWords) $flags = @('--cached','--staged','--stat','--name-only','--name-status', '--check','--diff-filter','--no-color','--color', '-U','--unified','-w','--ignore-all-space','-b','--ignore-space-change', '-p','--patch','--raw','--numstat','--shortstat','--summary', '--word-diff','--word-diff-regex','--histogram','--patience', '--diff-algorithm','--output','--relative','--no-relative', '--exit-code','--quiet','--ext-diff','--no-ext-diff', '--ignore-cr-at-eol','--ignore-space-at-eol') if ($Prefix.StartsWith('-')) { Complete-List -Prefix $Prefix -Items $flags } else { Complete-GitRef -Prefix $Prefix -IncludeTags } } # git fetch $script:SubcommandCompleters['fetch'] = { param([string]$Prefix, [string[]]$PrevWords) $flags = @('--all','--append','--depth','--deepen','--shallow-since', '--shallow-exclude','--unshallow','--update-shallow', '--dry-run','--force','-f','--keep','-k','--multiple', '--prune','-p','--prune-tags','--no-tags','-t','--tags', '--recurse-submodules','--jobs','-j','--set-upstream', '--no-recurse-submodules','--submodule-prefix','--update-head-ok', '--upload-pack','--quiet','-q','--verbose','-v','--progress', '--server-option','--show-forced-updates','--no-show-forced-updates', '--negotiate-only','--filter','--auto-maintenance','--no-auto-maintenance', '--auto-gc','--no-auto-gc','--write-fetch-head','--no-write-fetch-head', '--stdin') if ($Prefix.StartsWith('-')) { Complete-List -Prefix $Prefix -Items $flags } elseif ($PrevWords.Count -eq 0) { Complete-GitRemote -Prefix $Prefix } else { Complete-GitRef -Prefix $Prefix } } # git log $script:SubcommandCompleters['log'] = { param([string]$Prefix, [string[]]$PrevWords) $flags = @('--follow','-p','--patch','--stat','--shortstat','--name-only', '--name-status','--abbrev-commit','--no-abbrev-commit','--oneline', '--format','--pretty','--date','--parents','--children', '--left-right','--cherry-pick','--cherry-mark','--cherry', '--walk-reflogs','-g','--merge','--boundary','--simplify-by-decoration', '--full-history','--dense','--sparse','--simplify-merges', '--ancestry-path','--all','--branches','--tags','--remotes', '--glob','--exclude','--ignore-missing','--bisect', '--stdin','--first-parent','--not','--no-walk','--do-walk', '--author','--committer','--grep','--grep-reflog', '--all-match','--invert-grep','-i','--regexp-ignore-case', '-E','--extended-regexp','-F','--fixed-strings','-P','--perl-regexp', '--max-count','-n','--skip','--since','--until','--after','--before', '--graph','--decorate','--no-decorate','--decorate-refs', '--decorate-refs-exclude','--source','--full-diff','--log-size', '--use-mailmap','--no-mailmap','--no-notes','--show-notes', '--standard-notes','--no-standard-notes','--show-signature', '--relative-date','--date=','--color','--no-color','--color-words', '--no-patch','-s','--reverse','--topo-order','--date-order', '--author-date-order') if ($Prefix.StartsWith('-')) { Complete-List -Prefix $Prefix -Items $flags } else { Complete-GitRef -Prefix $Prefix -IncludeTags } } # git merge $script:SubcommandCompleters['merge'] = { param([string]$Prefix, [string[]]$PrevWords) $flags = @('--commit','--no-commit','--ff','--no-ff','--ff-only', '-n','--stat','--no-stat','--squash','--no-squash', '-s','--strategy','--strategy-option','-X', '--verify-signatures','--no-verify-signatures', '-q','--quiet','-v','--verbose','--progress','--no-progress', '-S','--gpg-sign','--no-gpg-sign','--overwrite-ignore', '--signoff','--no-signoff','--log','--no-log', '-m','--message','--file','-F','--into-name', '--allow-unrelated-histories','--abort','--continue','--quit', '--autostash','--no-autostash') if ($Prefix.StartsWith('-')) { Complete-List -Prefix $Prefix -Items $flags } else { Complete-GitRef -Prefix $Prefix } } # git pull $script:SubcommandCompleters['pull'] = { param([string]$Prefix, [string[]]$PrevWords) $flags = @('--rebase','--no-rebase','--ff','--no-ff','--ff-only', '-q','--quiet','-v','--verbose','--progress','--no-progress', '-n','--no-stat','--stat','--squash','--no-squash', '--force','-f','--autostash','--no-autostash', '--all','--append','-a','--depth','--deepen','--update-shallow', '--allow-unrelated-histories','--log','--no-log', '-s','--strategy','-X','--strategy-option','--verify-signatures', '--gpg-sign','-S','--no-gpg-sign','--recurse-submodules', '--no-recurse-submodules','--jobs','-j','--set-upstream', '--upload-pack','--prune','--dry-run','--tags','--no-tags') if ($Prefix.StartsWith('-')) { Complete-List -Prefix $Prefix -Items $flags } elseif ($PrevWords.Count -eq 0) { Complete-GitRemote -Prefix $Prefix } else { Complete-GitRef -Prefix $Prefix } } # git push $script:SubcommandCompleters['push'] = { param([string]$Prefix, [string[]]$PrevWords) $flags = @('--all','--mirror','--tags','--follow-tags','--atomic', '--no-atomic','--force','-f','--force-with-lease', '--force-if-includes','--dry-run','-n','--porcelain', '--delete','-d','--prune','-u','--set-upstream', '--push-option','-o','--receive-pack','--exec','--no-verify', '--progress','--no-progress','--verbose','-v','--quiet','-q', '--recurse-submodules','--verify','--ipv4','-4','--ipv6','-6', '--signed','--no-signed','--gpg-sign','-S','--no-gpg-sign', '--thin','--no-thin','--repo') if ($Prefix.StartsWith('-')) { Complete-List -Prefix $Prefix -Items $flags } elseif ($PrevWords.Count -eq 0) { Complete-GitRemote -Prefix $Prefix } else { Complete-GitRef -Prefix $Prefix -LocalOnly } } # git rebase $script:SubcommandCompleters['rebase'] = { param([string]$Prefix, [string[]]$PrevWords) $flags = @('--onto','--keep-base','--no-verify','--quiet','-q', '--verbose','-v','--stat','--no-stat','-n','--no-ff','-f', '--force-rebase','--fork-point','--no-fork-point', '--ignore-whitespace','--whitespace','--committer-date-is-author-date', '--ignore-date','--reset-author-date','--signoff', '-i','--interactive','-r','--rebase-merges','--no-rebase-merges', '-x','--exec','--root','--autosquash','--no-autosquash', '--autostash','--no-autostash','--reschedule-failed-exec', '--no-reschedule-failed-exec','--skip','--continue','--abort', '--quit','--edit-todo','--show-current-patch', '-m','--merge','-s','--strategy','-X','--strategy-option', '-S','--gpg-sign','--no-gpg-sign','--apply','--no-apply', '--empty','--keep-empty','--no-keep-empty','--rerere-autoupdate', '--no-rerere-autoupdate','--update-refs','--no-update-refs') if ($Prefix.StartsWith('-')) { Complete-List -Prefix $Prefix -Items $flags } else { Complete-GitRef -Prefix $Prefix } } # git remote $script:SubcommandCompleters['remote'] = { param([string]$Prefix, [string[]]$PrevWords) $subCmds = @('add','rename','remove','rm','set-head','set-branches', 'get-url','set-url','show','prune','update') if ($PrevWords.Count -eq 0) { Complete-List -Prefix $Prefix -Items $subCmds } else { $sub = $PrevWords[0] switch ($sub) { { $_ -in 'rename','remove','rm','set-head','set-branches', 'get-url','set-url','show','prune' } { Complete-GitRemote -Prefix $Prefix } } } } # git reset $script:SubcommandCompleters['reset'] = { param([string]$Prefix, [string[]]$PrevWords) $flags = @('--soft','--mixed','--hard','--merge','--keep', '-p','--patch','-q','--quiet','--no-quiet', '--pathspec-from-file','--pathspec-file-nul','--recurse-submodules', '--no-recurse-submodules') if ($Prefix.StartsWith('-')) { Complete-List -Prefix $Prefix -Items $flags } else { Complete-GitRef -Prefix $Prefix -IncludeTags } } # git revert $script:SubcommandCompleters['revert'] = { param([string]$Prefix, [string[]]$PrevWords) $flags = @('--edit','--no-edit','-n','--no-commit','-s','--signoff', '-m','--mainline','-S','--gpg-sign','--no-gpg-sign', '--strategy','--strategy-option','-X','--rerere-autoupdate', '--no-rerere-autoupdate','--continue','--skip','--abort','--quit', '--cleanup','--reference','--no-reference') if ($Prefix.StartsWith('-')) { Complete-List -Prefix $Prefix -Items $flags } else { Complete-GitRef -Prefix $Prefix -IncludeTags } } # git rm $script:SubcommandCompleters['rm'] = { param([string]$Prefix, [string[]]$PrevWords) $flags = @('-f','--force','-n','--dry-run','-r','--cached', '--ignore-unmatch','--sparse','-q','--quiet', '--pathspec-from-file','--pathspec-file-nul') if ($Prefix.StartsWith('-')) { Complete-List -Prefix $Prefix -Items $flags } else { Complete-GitFile -Prefix $Prefix -Staged } } # git show $script:SubcommandCompleters['show'] = { param([string]$Prefix, [string[]]$PrevWords) $flags = @('--stat','--name-only','--name-status','--format','--pretty', '--oneline','--abbrev-commit','--no-abbrev-commit','--notes', '--no-notes','--show-notes','--no-patch','-s','--patch','-p') if ($Prefix.StartsWith('-')) { Complete-List -Prefix $Prefix -Items $flags } else { Complete-GitRef -Prefix $Prefix -IncludeTags } } # git stash $script:SubcommandCompleters['stash'] = { param([string]$Prefix, [string[]]$PrevWords) $subCmds = @('list','show','drop','pop','apply','branch','clear','push','save','create','store') if ($PrevWords.Count -eq 0) { Complete-List -Prefix $Prefix -Items $subCmds } else { $sub = $PrevWords[0] switch ($sub) { { $_ -in 'show','drop','pop','apply','branch' } { Complete-GitStash -Prefix $Prefix } 'push' { $flags = @('-p','--patch','-k','--keep-index','--no-keep-index', '-u','--include-untracked','--all','-a','-q','--quiet', '-m','--message','--pathspec-from-file','--pathspec-file-nul') Complete-List -Prefix $Prefix -Items $flags } } } } # git status $script:SubcommandCompleters['status'] = { param([string]$Prefix, [string[]]$PrevWords) $flags = @('-s','--short','-b','--branch','--porcelain','--long', '-v','--verbose','-u','--untracked-files', '--ignore-submodules','--ignored','--column','--no-column', '--ahead-behind','--no-ahead-behind','--renames','--no-renames', '--find-renames','--pathspec-from-file','--pathspec-file-nul') Complete-List -Prefix $Prefix -Items $flags } # git submodule $script:SubcommandCompleters['submodule'] = { param([string]$Prefix, [string[]]$PrevWords) $subCmds = @('add','status','init','deinit','update','set-branch', 'set-url','summary','foreach','sync','absorbgitdirs') if ($PrevWords.Count -eq 0) { Complete-List -Prefix $Prefix -Items $subCmds } } # git tag $script:SubcommandCompleters['tag'] = { param([string]$Prefix, [string[]]$PrevWords) $flags = @('-a','--annotate','-s','--sign','-u','--local-user', '-f','--force','-d','--delete','-v','--verify', '-l','--list','--sort','--color','--no-color', '-m','--message','-F','--file','-e','--edit', '--contains','--no-contains','--merged','--no-merged', '--points-at','--format','--create-reflog', '--cleanup','--column','--no-column','--ignore-case','-i') if ($Prefix.StartsWith('-')) { Complete-List -Prefix $Prefix -Items $flags } else { Complete-GitRef -Prefix $Prefix -TagsOnly } } # git config $script:SubcommandCompleters['config'] = { param([string]$Prefix, [string[]]$PrevWords) $flags = @('--global','--system','--local','--worktree','--file','-f', '--blob','--get','--get-all','--get-regexp','--get-urlmatch', '--replace-all','--add','--unset','--unset-all','--rename-section', '--remove-section','-l','--list','--fixed-value','--type', '--bool','--int','--float','--bool-or-int','--bool-or-str','--path','--expiry-date', '--null','-z','--name-only','--show-origin','--show-scope', '-e','--edit','--no-includes','--includes', '--default','--comment') Complete-List -Prefix $Prefix -Items $flags } # git worktree $script:SubcommandCompleters['worktree'] = { param([string]$Prefix, [string[]]$PrevWords) $subCmds = @('add','list','lock','move','prune','remove','repair','unlock') if ($PrevWords.Count -eq 0) { Complete-List -Prefix $Prefix -Items $subCmds } elseif ($PrevWords[0] -in @('remove','lock','unlock','move')) { Complete-GitWorktree -Prefix $Prefix } } # git cherry-pick $script:SubcommandCompleters['cherry-pick'] = { param([string]$Prefix, [string[]]$PrevWords) $flags = @('-e','--edit','-x','-r','-m','--mainline','-n','--no-commit', '-s','--signoff','-S','--gpg-sign','--no-gpg-sign', '--ff','--allow-empty','--allow-empty-message','--keep-redundant-commits', '--strategy','--strategy-option','-X','--rerere-autoupdate', '--no-rerere-autoupdate','--continue','--skip','--quit','--abort', '--cleanup','--no-gpg-sign') if ($Prefix.StartsWith('-')) { Complete-List -Prefix $Prefix -Items $flags } else { Complete-GitRef -Prefix $Prefix -IncludeTags } } # git bisect $script:SubcommandCompleters['bisect'] = { param([string]$Prefix, [string[]]$PrevWords) $subCmds = @('start','bad','new','good','old','terms','skip','reset','visualize','replay','log','run') Complete-List -Prefix $Prefix -Items $subCmds } # git describe $script:SubcommandCompleters['describe'] = { param([string]$Prefix, [string[]]$PrevWords) $flags = @('--dirty','--broken','--all','--tags','--contains', '--abbrev','--candidates','--exact-match','--long', '--match','--exclude','--always','--first-parent', '--debug') if ($Prefix.StartsWith('-')) { Complete-List -Prefix $Prefix -Items $flags } else { Complete-GitRef -Prefix $Prefix -IncludeTags } } # git clone $script:SubcommandCompleters['clone'] = { param([string]$Prefix, [string[]]$PrevWords) $flags = @('--local','-l','--no-hardlinks','--shared','-s','--reference', '--reference-if-able','--dissociate','--quiet','-q','--verbose','-v', '--progress','--no-checkout','-n','--bare','--mirror','--origin','-o', '--branch','-b','--upload-pack','-u','--template','--config','-c', '--depth','--shallow-since','--shallow-exclude','--single-branch', '--no-single-branch','--no-tags','--recurse-submodules', '--shallow-submodules','--no-shallow-submodules','--remote-submodules', '--no-remote-submodules','--separate-git-dir','--jobs','-j','--bundle-uri', '--filter','--also-filter-submodules','--reject-shallow','--no-reject-shallow', '--sparse','--bundle-uri','--server-option') Complete-List -Prefix $Prefix -Items $flags } # git init $script:SubcommandCompleters['init'] = { param([string]$Prefix, [string[]]$PrevWords) $flags = @('-q','--quiet','-b','--initial-branch','--bare', '--template','--separate-git-dir','--object-format', '--shared') Complete-List -Prefix $Prefix -Items $flags } # git notes $script:SubcommandCompleters['notes'] = { param([string]$Prefix, [string[]]$PrevWords) $subCmds = @('list','add','copy','append','edit','show','merge','remove','prune','get-ref') if ($PrevWords.Count -eq 0) { Complete-List -Prefix $Prefix -Items $subCmds } elseif ($PrevWords[0] -in @('show','edit','copy','append','remove','add')) { Complete-GitRef -Prefix $Prefix -IncludeTags } } # git blame $script:SubcommandCompleters['blame'] = { param([string]$Prefix, [string[]]$PrevWords) $flags = @('-b','-p','--porcelain','--line-porcelain','--incremental','--root', '--show-stats','--progress','--score-debug','-f','--show-name', '-n','--show-number','-s','-e','--show-email','-w', '--ignore-rev','--ignore-revs-file','--color-lines','--color-by-age', '--minimal','-S','--reverse','--first-parent','-L', '-M','-C','-c','--textconv','--no-textconv','--abbrev','--date', '--since','--until') if ($Prefix.StartsWith('-')) { Complete-List -Prefix $Prefix -Items $flags } else { Complete-GitFile -Prefix $Prefix } } # ────────────────────────────────────────────────────────────────────────────── # Main argument completer # ────────────────────────────────────────────────────────────────────────────── function Complete-GitArgument { [CmdletBinding()] param( [string]$WordToComplete, [System.Management.Automation.Language.CommandAst]$CommandAst, [int]$CursorPosition ) # Not in a git repo? Still complete subcommands and global flags $allWordsArr = @($CommandAst.CommandElements | Select-Object -Skip 1 | ForEach-Object { $_.ToString() }) $prefix = $WordToComplete # Select-Object -SkipLast is PS 6+ only; use array slice for PS 5.1 compatibility $priorWords = if ($allWordsArr.Count -gt 1) { $allWordsArr[0..($allWordsArr.Count - 2)] } else { @() } # Parse global flags and find the subcommand position $subCmd = $null $subArgs = [System.Collections.Generic.List[string]]::new() $seenSub = $false foreach ($word in $priorWords) { if (-not $seenSub -and -not $word.StartsWith('-')) { $subCmd = $word $seenSub = $true } elseif ($seenSub) { $subArgs.Add($word) } } # No subcommand yet: complete subcommands + global flags + aliases if (-not $seenSub -and -not $prefix.StartsWith('-')) { Complete-List -Prefix $prefix -Items $script:GitCommands ` -Type ([System.Management.Automation.CompletionResultType]::ParameterValue) # Add user-defined aliases $aliases = Get-GitAliases foreach ($alias in $aliases.Keys) { if ($alias.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) { $tooltip = "alias: $($aliases[$alias])" [System.Management.Automation.CompletionResult]::new( $alias, $alias, [System.Management.Automation.CompletionResultType]::ParameterValue, $tooltip) } } return } if (-not $seenSub -and $prefix.StartsWith('-')) { $globalFlags = @('--version','--help','-C','--exec-path','--html-path', '--man-path','--info-path','-p','--paginate','--no-pager', '--git-dir','--work-tree','--namespace','--super-prefix', '--bare','--no-replace-objects','--literal-pathspecs', '--glob-pathspecs','--noglob-pathspecs','--icase-pathspecs', '--no-optional-locks','--list-cmds','--attr-source') Complete-List -Prefix $prefix -Items $globalFlags return } # Resolve alias to real command for completion purposes $resolvedCmd = $subCmd $aliases = Get-GitAliases if ($aliases.ContainsKey($subCmd)) { $aliasVal = $aliases[$subCmd] $resolvedCmd = ($aliasVal -split '\s+')[0] } # Dispatch to per-subcommand completer if ($script:SubcommandCompleters.ContainsKey($resolvedCmd)) { & $script:SubcommandCompleters[$resolvedCmd] $prefix $subArgs.ToArray() } } # ────────────────────────────────────────────────────────────────────────────── # Registration # ────────────────────────────────────────────────────────────────────────────── function Register-GitCompletion { [CmdletBinding()] param() Register-ArgumentCompleter -Native -CommandName git -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) Complete-GitArgument -WordToComplete $wordToComplete ` -CommandAst $commandAst ` -CursorPosition $cursorPosition } } function Unregister-GitCompletion { # PS 5.1 doesn't support unregistering native completers directly; # re-importing the module restores the default. Write-Warning 'power-git: Re-import the module (Remove-Module power-git; Import-Module power-git) to reset completion.' } |