Common/GitCommands.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.Collections.Generic; function __git { [CmdletBinding(PositionalBinding = $false)] param( [Parameter()][string]$GitDirOverride = $null, [Parameter(ValueFromRemainingArguments)]$OrdinaryArgs ) $Context = Get-Variable 'Context' -ValueOnly -Scope 'Script' -ErrorAction Ignore $gitDir = $Context.gitDir $gitArgs = $Context.gitCArgs if ($GitDirOverride) { $gitArgs = @("--git-dir=$GitDirOverride") + $Context.gitCArgs } elseif ($gitDir) { $gitArgs = @("--git-dir=$gitDir") + $Context.gitCArgs } else { $gitArgs = $Context.gitCArgs } $OutputEncodingBak = [Console]::OutputEncoding try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 git @gitArgs @OrdinaryArgs } finally { [Console]::OutputEncoding = $OutputEncodingBak } } $script:__gitVersion = $null function gitVersion { [OutputType([version])] param () if ($script:__gitVersion) { return $script:__gitVersion } (git --version) -match 'version\s*(\d+\.\d+\.\d+)' [version]::TryParse($Matches[1], [ref]$script:__gitVersion) | Out-Null return $script:__gitVersion } # Returns true if $1 matches the name of a configured remote, false otherwise. function gitIsConfiguredRemote { [OutputType([bool])] param([Parameter(Mandatory, Position = 0)][string]$Remote) return @(gitRemote | Where-Object { $_ -eq $Remote }) } function gitRemote { [OutputType([string[]])] param () return @(__git remote) } function gitListAliases { [OutputType([PSCustomObject[]])] param() __git config --get-regexp "^alias\." | ForEach-Object { if ($_ -match "^alias\.([^ ]+) (.*)") { [PSCustomObject]@{ Name = $Matches[1]; Value = $Matches[2]; } } } } function gitParseShellArgs { [OutputType([string[]])] param ( [Parameter(Mandatory, Position = 0)] [string] $Line ) $cmd = "!printf '%s\n' $($Line.Replace("`n", ' '))" return @(git -c alias.cmp-shell-args=$cmd cmp-shell-args) } function gitGetAlias { [OutputType([string])] param( [Parameter(Mandatory, Position = 0)][string] $Alias ) $ErrorActionPreference = 'SilentlyContinue' try { __git config --get "alias.$Alias" 2>$null } catch { $null } } function gitLsTreeFile { [OutputType([string[]])] param ([Parameter(Mandatory, Position = 0)][string]$treeIsh) $lsTree = @(__git ls-tree "$treeIsh" -z) -split "`0" foreach ($line in $lsTree) { if ($line -cmatch '(?<mode>\S+) (?<type>\S+) (?<name>\S+)\t(?<path>.+)') { $path = $Matches['path'] if ($Matches['type'] -eq 'tree') { $path += '/' } $path } } } $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 () git merge -s help 2>&1 | Where-Object { $_ -like "*Available strategies are: *" } | ForEach-Object { if ($_ -match ".*:\s*(.*)\s*\.") { $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 | Sort-Object) } $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 (!$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 (!$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") } # __git_find_repo_path, gitFindRepoPath # 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() $Context = Get-Variable 'Context' -ValueOnly -Scope 'Script' -ErrorAction Ignore $gitDir = $Context.gitDir $gitCArgs = $Context.gitCArgs if ($gitCArgs) { return [string](__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 [string](git rev-parse --git-dir 2>$null) } } # 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 ) $ignoreCase = $null if ($script:GitCompletionSettings.IgnoreCase) { $ignoreCase = '--ignore-case' } @(__git for-each-ref --format="%(refname:strip=2)" ` $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 ) $ignoreCase = $null if ($script:GitCompletionSettings.IgnoreCase) { $ignoreCase = '--ignore-case' } @(__git for-each-ref --format="%(refname:strip=2)" ` $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, [string] $Prefix = '', [string] $Suffix = '' ) $ForeachPrefix = "$Prefix".Replace('%', '%%') $ignoreCase = $null if ($script:GitCompletionSettings.IgnoreCase) { $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, [string]$Prefix = '' ) $ignoreCase = $null if ($script:GitCompletionSettings.IgnoreCase) { $ignoreCase = '--ignore-case' } @(__git for-each-ref --format="${Prefix}%(refname:strip=3)" ` --sort="refname:strip=3" ` $ignoreCase ` "refs/remotes/*/$Current*" "refs/remotes/*/$Current*/**") | Group-Object | Where-Object Count -gt 0 | Select-Object -ExpandProperty Name | Where-Object { $_ } } # __git_refs # 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, [string] $Track = "", [string] $Prefix = '' ) $listRefsFrom = "path" $match = $Current $umatch = $Current $ignoreCase = $null $dir = (gitRepoPath) if (!$Remote) { if (!$dir) { return @() } } else { if (gitIsConfiguredRemote $Remote) { # 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 ($script:GitCompletionSettings.IgnoreCase) { $ignoreCase = '--ignore-case' $umatch = $Current.ToUpperInvariant() } if ($listRefsFrom -eq "path") { if ($Current.StartsWith("^")) { $Current = $Current.Substring(1) $match = $match.Substring(1) $umatch = $umatch.Substring(1) $Prefix = '^' } 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.StartsWith($match)) -or ($i.StartsWith($umatch))) { if (Test-Path "$dir/$i" -PathType Leaf) { "$Prefix$i" } } } $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=$Prefix%($format)" $ignoreCase @refs if ($Track) { gitDwimRemoteHeads -Current $match -Prefix $Prefix } return } if ($Current -match "refs(/.*)?") { __git ls-remote "$Remote" "$Match*" | ForEach-Object { $_ -match "(\S+)\s+(\S+)" | Out-Null $i = $Matches[2] if ($i -notlike "*^{}") { "$i" } } } elseif ($listRefsFrom -eq "remote") { if ("HEAD".StartsWith($match)) { "HEAD" } __git for-each-ref --format="%(refname:strip=3)" ` $ignoreCase ` "refs/remotes/$remote/$match*" "refs/remotes/$remote/$match*/**" } else { $querySymref = $null if ("HEAD".StartsWith($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/*')) { $i.Substring('refs/*'.Length) } else { "$i" } } } } } # __git_resolve_builtins function gitResolveBuiltins { [OutputType([string[]])] [CmdletBinding(DefaultParameterSetName = 'Default')] param( [Parameter(Mandatory, ValueFromRemainingArguments)] [string[]] $Command, [Parameter(ParameterSetName = 'All')] [switch] $All, [switch] $Check ) if ($PSCmdlet.ParameterSetName -eq 'Default') { $All = [bool]$script:GitCompletionSettings.ShowAllOptions } if (!$Check -or (gitSupportParseoptHelper $Command[0])) { return @(gitResolveBuiltinsImpl @Command -All:([bool]$All) | ForEach-Object { $_ -split "\s+" } | Where-Object { $_ } ) } } function gitResolveBuiltinsImpl { [OutputType([string[]])] param( [Parameter(Mandatory, ValueFromRemainingArguments)][string[]] $Command, [switch] $All ) if ($All) { $completionHelper = '--git-completion-helper-all' } else { $completionHelper = '--git-completion-helper' } return (git @Command $completionHelper 2>$null) } $script:__git_support_parseopt_helper = $null # __git_support_parseopt_helper function gitSupportParseoptHelper { [OutputType([bool])] param([Parameter(Mandatory, Position = 0)][string]$Command) if (!$script:__git_support_parseopt_helper) { $script:__git_support_parseopt_helper = [HashSet[string]]::new( -split ([string](git --list-cmds=parseopt))) } return $script:__git_support_parseopt_helper.Contains($Command) } # __git_get_config_variables # Lists all set config variables starting with the given section prefix, # with the prefix removed. function gitGetConfigVariables () { [CmdletBinding()] [OutputType([string[]])] param( [Parameter(Mandatory, Position = 0)][string]$Section ) __git config --name-only --get-regexp "^$Section\..*" | ForEach-Object { $_.Substring($Section.Length + 1) } } # __git_pseudoref_exists # Runs git in $__git_repo_path to determine whether a pseudoref exists. # 1: The pseudo-ref to search function gitPseudorefExists { param([Parameter(Mandatory, Position = 0)][string]$ref) $repoPath = (gitRepoPath) [string]$head = (Get-Content "$repoPath/HEAD" -ErrorAction Ignore | Select-Object -First 1) # If the reftable is in use, we have to shell out to 'git rev-parse' # to determine whether the ref exists instead of looking directly in # the filesystem to determine whether the ref exists. Otherwise, use # Bash builtins since executing Git commands are expensive on some # platforms. if ($head -eq 'ref: refs/heads/.invalid') { __git show-ref --exists "$ref" 1>$null 2>$null return $LASTEXITCODE } return ((Get-Item "$repoPath/$ref" -ErrorAction Ignore) -is [System.IO.FileInfo]) } # __git_pretty_aliases function gitPrettyAliases() { [CmdletBinding()] [OutputType([string[]])] param() gitGetConfigVariables pretty } function gitArchiveList { [CmdletBinding()] [OutputType([string[]])] param () __git archive --list } function gitStashList { [CmdletBinding()] [OutputType([PSCustomObject[]])] param () foreach ($line in ((git stash list -z) -split '\0')) { if ($line -match '^([^:]+): ?(.*?)$') { [PSCustomObject]@{ Name = $Matches[1] Message = $Matches[2] } } } } function gitCommitMessage() { [CmdletBinding()] [OutputType([string])] param( [Parameter(Position = 0)] [string] $ref ) return [string](__git show -s "$ref" --oneline 2>$null) } function gitRecentLog() { [CmdletBinding()] [OutputType([string[]])] param( [Parameter(Position = 0)] $ref = $null, $MaxCount = 5, $Skip = 0 ) $line = [string](git log $ref --oneline -z "--max-count=$MaxCount" "--skip=$Skip" 2>$null) if ($line) { return $line.Split([char[]]@([char]0), [StringSplitOptions]::RemoveEmptyEntries) } return @() } |