Get-Branch.ps1
|
function Get-Branch { <# .SYNOPSIS List local and remote-tracking branches with tracking information. .DESCRIPTION Reads refs with git for-each-ref and returns GitBranch objects. By default, includes local branches and locally stored remote-tracking refs, including symbolic refs such as origin/HEAD. Never fetches or contacts a remote. Implicit partial-clone fetches are disabled; unavailable promised objects surface as git errors. Branch is the name below refs/heads/ or refs/remotes/. RefName, Upstream and SymbolicTarget retain full ref names to distinguish otherwise ambiguous local and remote names. Current identifies the branch named by HEAD in the selected repository or worktree, not other branches sharing its commit. AheadBy and BehindBy count commits relative to the configured upstream using the locally available history. Both are null without an upstream or when that upstream is gone; UpstreamGone distinguishes these cases. Empty string ref metadata is exposed as null. A detached HEAD marks no branch Current. An unborn branch has no ref yet and produces no row. .PARAMETER Path Literal directory inside the git working tree, or a bare repository. Defaults to the current location. Accepts pipeline paths or objects with a Path, RepositoryPath or RepoPath property. Does not change location. .PARAMETER Local Include local branches only, unless Remote is also specified. .PARAMETER Remote Include remote-tracking refs only, unless Local is also specified. These are cached refs in this repository, not a live query of any remote. .OUTPUTS GitBranch Branch, RefName, Current, Commit, Upstream, AheadBy, BehindBy, UpstreamGone, SymbolicTarget, IsRemote, Subject and RepositoryPath. .EXAMPLE Get-Branch Lists local and remote-tracking branches for the current repository. .EXAMPLE Get-Branch -Path ../project -Local Lists local branches for another repository without changing location. .EXAMPLE Get-Worktrees | Get-Branch -Local | Where-Object Current Gets each worktree's current branch with its repository context. #> [OutputType('GitBranch')] [CmdletBinding()] param( [Parameter(Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias('RepositoryPath', 'RepoPath')] [string]$Path, [switch]$Local, [switch]$Remote ) process { $gitEnvironment = @{ GIT_NO_LAZY_FETCH = '1' } $refPrefixes = @( if ($Local -or -not $Remote) { 'refs/heads/' } if ($Remote -or -not $Local) { 'refs/remotes/' } ) $format = '%(refname)%00%(HEAD)%00%(objectname)%00%(upstream)%00%(symref)%00%(subject)%00' $refs = Invoke-Git -Path $Path -AllowBare -Environment $gitEnvironment -Arguments ( @('for-each-ref', '--sort=refname', "--format=$format", '--') + $refPrefixes ) if (-not $refs -or -not $refs.StandardOutput) { return } $repoPath = $refs.RepositoryPath $entries = [System.Collections.Generic.List[object]]::new() # Each record has six NUL-terminated fields followed by git's newline. # Do not split records on newlines or trim user-controlled subjects. $fields = $refs.StandardOutput.Split([char]0) if ($fields.Count % 6 -ne 1 -or $fields[-1] -notin @("`n", "`r`n")) { $PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new( [System.FormatException]::new("Invalid branch ref output from git in '$repoPath'."), 'InvalidGitBranchOutput', [System.Management.Automation.ErrorCategory]::InvalidData, $refs)) return } for ($index = 0; $index -lt $fields.Count - 1; $index += 6) { $refName = if ($index -eq 0) { $fields[$index] } else { $fields[$index] -replace '\A\r?\n', '' } $isRemote = $refName.StartsWith('refs/remotes/', [System.StringComparison]::Ordinal) $prefix = if ($isRemote) { 'refs/remotes/' } else { 'refs/heads/' } if (-not $refName.StartsWith($prefix, [System.StringComparison]::Ordinal) -or $refName.Length -eq $prefix.Length -or $fields[$index + 1] -notin @(' ', '*') -or $fields[$index + 2] -cnotmatch '\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z') { $PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new( [System.FormatException]::new("Invalid branch ref record from git in '$repoPath'."), 'InvalidGitBranchOutput', [System.Management.Automation.ErrorCategory]::InvalidData, $refs)) return } $entries.Add([PSCustomObject]@{ PSTypeName = 'GitBranch' Branch = $refName.Substring($prefix.Length) RefName = $refName Current = $fields[$index + 1] -ceq '*' Commit = $fields[$index + 2] Upstream = if ($fields[$index + 3]) { $fields[$index + 3] } else { $null } AheadBy = $null BehindBy = $null UpstreamGone = $false SymbolicTarget = if ($fields[$index + 4]) { $fields[$index + 4] } else { $null } IsRemote = $isRemote Subject = $fields[$index + 5] RepositoryPath = $repoPath }) } $upstreamRefs = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) foreach ($entry in $entries) { if ($entry.Upstream) { $null = $upstreamRefs.Add($entry.Upstream) } } if ($upstreamRefs.Count -gt 0) { $upstreams = Invoke-Git -Path $repoPath -AllowBare -Environment $gitEnvironment -Arguments ( @('for-each-ref', '--format=%(refname)%00%(objectname)', '--') + @($upstreamRefs) ) if (-not $upstreams) { return } $upstreamCommits = [System.Collections.Generic.Dictionary[string, string]]::new([System.StringComparer]::Ordinal) foreach ($line in ($upstreams.StandardOutput -split '\r?\n')) { if (-not $line) { continue } $parts = $line.Split([char]0) if ($parts.Count -ne 2 -or $parts[1] -cnotmatch '\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z') { $PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new( [System.FormatException]::new("Invalid upstream ref output from git in '$repoPath'."), 'InvalidGitBranchOutput', [System.Management.Automation.ErrorCategory]::InvalidData, $upstreams)) return } # for-each-ref patterns also match descendants; require an exact, # case-sensitive ref match before treating an upstream as present. if ($upstreamRefs.Contains($parts[0])) { $upstreamCommits[$parts[0]] = $parts[1] } } foreach ($entry in $entries) { if (-not $entry.Upstream) { continue } if (-not $upstreamCommits.ContainsKey($entry.Upstream)) { $entry.UpstreamGone = $true continue } $counts = Invoke-Git -Path $repoPath -AllowBare -Environment $gitEnvironment -Arguments @( 'rev-list', '--left-right', '--count', "$($entry.Commit)...$($upstreamCommits[$entry.Upstream])", '--' ) if (-not $counts) { return } if ($counts.StandardOutput -cnotmatch '\A([0-9]+)\t([0-9]+)\r?\n?\z') { $PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new( [System.FormatException]::new("Invalid tracking counts from git in '$repoPath'."), 'InvalidGitBranchOutput', [System.Management.Automation.ErrorCategory]::InvalidData, $counts)) return } $entry.AheadBy = [long]$Matches[1] $entry.BehindBy = [long]$Matches[2] } } $entries } } |