Public/Get-GitStatusSummary.ps1
|
function Get-GitStatusSummary { <# .SYNOPSIS Get a summary of the current git repository status. .DESCRIPTION Parses 'git status --porcelain=v1 --branch' directly to produce a typed GitStatusSummary object with branch, upstream, ahead/behind counts, index/working file change counts, conflicts, untracked count, stash count, any in-progress operation (rebase/merge/cherry-pick/revert/bisect), repo name, relative path, and a formatted status string. Self-contained — no posh-git or external module dependency. Only requires the git CLI. .PARAMETER Path The directory to check. Defaults to the current location. .EXAMPLE Get-GitStatusSummary Returns git status summary for the current directory. .EXAMPLE (Get-GitStatusSummary).StatusString Returns just the formatted status string like [main ≡ +1 ~2 -0 | +0 ~1 -0]. #> [OutputType('GitStatusSummary')] [CmdletBinding()] param( [Parameter(Position = 0)] [string]$Path ) if ($Path) { Push-Location $Path } try { # Quick check: are we in a git repo? $null = git rev-parse --is-inside-work-tree 2>$null if ($LASTEXITCODE -ne 0) { return [PSCustomObject]@{ PSTypeName = 'GitStatusSummary' IsGitRepo = $false RepoName = $null Branch = $null Upstream = $null WorktreePath = $null RelativePath = $null AheadBy = 0 BehindBy = 0 UpstreamGone = $false Operation = $null IndexAdded = 0 IndexModified = 0 IndexDeleted = 0 WorkingAdded = 0 WorkingModified = 0 WorkingDeleted = 0 Conflicts = 0 Untracked = 0 StashCount = 0 HasChanges = $false StatusString = $null } } # Parse git status $lines = git status --porcelain=v1 --branch 2>$null $branch = $null; $upstream = $null; $ahead = 0; $behind = 0; $upstreamGone = $false $idxA = 0; $idxM = 0; $idxD = 0 $wrkA = 0; $wrkM = 0; $wrkD = 0 $untracked = 0; $conflicts = 0 foreach ($line in $lines) { if ($line.StartsWith('## ')) { $branchLine = $line.Substring(3) if ($branchLine -match '^(.+?)\.\.\.(.+?)(?:\s+\[(.+)\])?$') { $branch = $Matches[1] $upstream = $Matches[2] $trackInfo = $Matches[3] if ($trackInfo) { if ($trackInfo -match 'ahead (\d+)') { $ahead = [int]$Matches[1] } if ($trackInfo -match 'behind (\d+)') { $behind = [int]$Matches[1] } if ($trackInfo -match 'gone') { $upstreamGone = $true } } } elseif ($branchLine -match '^(.+?)$') { $branch = $branchLine.Trim() if ($branch -eq 'HEAD (no branch)') { $branch = 'HEAD' } } continue } if ($line.Length -lt 2) { continue } $x = $line[0] $y = $line[1] # Untracked if ($x -eq '?' -and $y -eq '?') { $untracked++; continue } # Unmerged/conflicted (UU, AA, DD, AU, UA, DU, UD) if ($x -eq 'U' -or $y -eq 'U' -or ($x -eq 'A' -and $y -eq 'A') -or ($x -eq 'D' -and $y -eq 'D')) { $conflicts++; continue } # Index (staged) changes switch ($x) { 'A' { $idxA++ } 'M' { $idxM++ } 'R' { $idxM++ } 'C' { $idxA++ } 'D' { $idxD++ } } # Working tree changes switch ($y) { 'M' { $wrkM++ } 'D' { $wrkD++ } 'A' { $wrkA++ } } } # Worktree path $toplevel = (git rev-parse --show-toplevel 2>$null) -replace '/', '\' # Detect in-progress git operations via .git/ sentinel files $gitDir = (git rev-parse --git-dir 2>$null) -replace '/', '\' $operation = if ($gitDir) { if (Test-Path "$gitDir\rebase-merge") { $step = if (Test-Path "$gitDir\rebase-merge\msgnum") { (Get-Content "$gitDir\rebase-merge\msgnum" -Raw).Trim() } $total = if (Test-Path "$gitDir\rebase-merge\end") { (Get-Content "$gitDir\rebase-merge\end" -Raw).Trim() } $type = if (Test-Path "$gitDir\rebase-merge\interactive") { 'REBASE-i' } else { 'REBASE-m' } if ($step -and $total) { "$type $step/$total" } else { $type } } elseif (Test-Path "$gitDir\rebase-apply") { $step = if (Test-Path "$gitDir\rebase-apply\next") { (Get-Content "$gitDir\rebase-apply\next" -Raw).Trim() } $total = if (Test-Path "$gitDir\rebase-apply\last") { (Get-Content "$gitDir\rebase-apply\last" -Raw).Trim() } $type = if (Test-Path "$gitDir\rebase-apply\rebasing") { 'REBASE' } elseif (Test-Path "$gitDir\rebase-apply\applying") { 'AM' } else { 'AM/REBASE' } if ($step -and $total) { "$type $step/$total" } else { $type } } elseif (Test-Path "$gitDir\MERGE_HEAD") { 'MERGING' } elseif (Test-Path "$gitDir\REVERT_HEAD") { 'REVERTING' } elseif (Test-Path "$gitDir\CHERRY_PICK_HEAD") { 'CHERRY-PICKING' } elseif (Test-Path "$gitDir\BISECT_LOG") { 'BISECTING' } } # Repo name from remote URL or directory name $remoteUrl = git remote get-url origin 2>$null $repoName = if ($remoteUrl) { ($remoteUrl.Substring($remoteUrl.LastIndexOf('/') + 1)) -replace '\.git$', '' } elseif ($toplevel) { Split-Path $toplevel -Leaf } else { '' } # Stash count $stashCount = 0 $stashOutput = git rev-list --walk-reflogs --count refs/stash 2>$null if ($LASTEXITCODE -eq 0 -and $stashOutput) { $stashCount = [int]$stashOutput } # Relative path from worktree root $currentPath = (Get-Location).Path $relativePath = '' if ($toplevel -and $currentPath.StartsWith($toplevel, [System.StringComparison]::OrdinalIgnoreCase)) { $relativePath = $currentPath.Substring($toplevel.Length) } # Relation indicator $relation = if ($ahead -gt 0 -and $behind -gt 0) { "↓${behind} ↑${ahead}" } elseif ($ahead -gt 0) { "↑$ahead" } elseif ($behind -gt 0) { "↓$behind" } elseif ($upstreamGone) { '×' } elseif ($upstream) { '≡' } else { '' } # Build status string: [branch|OP ≡ +A ~M -D | +A ~M -D !C ?U] (S) $hasIndex = ($idxA + $idxM + $idxD) -gt 0 $hasWorking = ($wrkA + $wrkM + $wrkD) -gt 0 $hasUntracked = $untracked -gt 0 $hasConflicts = $conflicts -gt 0 $sb = [System.Text.StringBuilder]::new('[') [void]$sb.Append($branch) if ($operation) { [void]$sb.Append("|$operation") } if ($relation) { [void]$sb.Append(" $relation") } if ($hasIndex) { [void]$sb.Append(" +$idxA ~$idxM -$idxD") } if ($hasIndex -or $hasWorking) { [void]$sb.Append(' |') } if ($hasWorking) { [void]$sb.Append(" +$wrkA ~$wrkM -$wrkD") } if ($hasConflicts) { [void]$sb.Append(" !$conflicts") } if ($hasUntracked) { [void]$sb.Append(' ?') } [void]$sb.Append(']') [PSCustomObject]@{ PSTypeName = 'GitStatusSummary' IsGitRepo = $true RepoName = $repoName Branch = $branch Upstream = $upstream WorktreePath = $toplevel RelativePath = $relativePath AheadBy = $ahead BehindBy = $behind UpstreamGone = $upstreamGone Operation = $operation IndexAdded = $idxA IndexModified = $idxM IndexDeleted = $idxD WorkingAdded = $wrkA WorkingModified = $wrkM WorkingDeleted = $wrkD Conflicts = $conflicts Untracked = $untracked StashCount = $stashCount HasChanges = ($hasIndex -or $hasWorking -or $hasUntracked -or $hasConflicts) StatusString = $sb.ToString() } } finally { if ($Path) { Pop-Location } } } |