scripts/win/git/status.ps1
<#
.SYNOPSIS Simple, read-only Git status for Borg. .DESCRIPTION Prints a friendly snapshot of the current repository: - Repo root, branch, upstream, ahead/behind (parsed from porcelain v2 -b, same as `git status`) - Staged / Unstaged / Untracked groups - Conflict detection + guidance No interactivity, no actions. #> function W-Info($m){ Write-Host "🧭 $m" } function W-Ok($m){ Write-Host "✅ $m" } function W-Warn($m){ Write-Host "⚠️ $m" } function W-Err($m){ Write-Host "❌ $m" } if (-not (Get-Command git -ErrorAction SilentlyContinue)) { W-Err "git not found in PATH."; exit 1 } $top = git rev-parse --show-toplevel 2>$null if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($top)) { W-Err "Not inside a git repository."; exit 2 } Set-Location $top | Out-Null # --- Parse branch/upstream/ahead/behind from porcelain v2 (-b) --- $porcelainB = git status --porcelain=v2 -b $branch = ""; $upstream = ""; $ahead = 0; $behind = 0 foreach ($l in $porcelainB) { if ($l -match '^\#\sbranch\.head\s+(.*)$') { $branch = $Matches[1].Trim() ; continue } if ($l -match '^\#\sbranch\.upstream\s+(.*)$') { $upstream = $Matches[1].Trim() ; continue } if ($l -match '^\#\sbranch\.ab\s+\+(\d+)\s\-(\d+)$') { $ahead = [int]$Matches[1]; $behind = [int]$Matches[2]; continue } } # Fallbacks if porcelain didn't yield if (-not $branch) { $branch = (git rev-parse --abbrev-ref HEAD).Trim() } # Note: upstream may be empty if none is set # --- Parse file changes from porcelain v2 (no -b to get entries) --- $status = git status --porcelain=v2 $staged = New-Object System.Collections.Generic.List[string] $unstaged = New-Object System.Collections.Generic.List[string] $untracked = New-Object System.Collections.Generic.List[string] $conflicts = New-Object System.Collections.Generic.HashSet[string] foreach ($line in $status) { if ($line -match '^\?{2}\s+(.+)$') { [void]$untracked.Add($Matches[1]); continue } if ($line -match '^u\s+.+\s(.+)$') { $p = $Matches[1]; [void]$unstaged.Add($p); [void]$conflicts.Add($p); continue } if ($line -match '^\d+\s+([MADRCU\.])([MADRCU\.])\s+.+\s+(.+)$') { $x = $Matches[1]; $y = $Matches[2]; $path = $Matches[3] if ($x -ne '.') { [void]$staged.Add($path) } if ($y -ne '.') { [void]$unstaged.Add($path) } continue } } $staged = $staged | Sort-Object -Unique $unstaged = $unstaged | Sort-Object -Unique $untracked = $untracked | Sort-Object -Unique Write-Host "" W-Info ("Repo : {0}" -f $top) W-Info ("Branch : {0}{1}" -f $branch, ($(if ($upstream) { " (upstream: $upstream, ↑$ahead ↓$behind)" } else { "" }))) Write-Host "" function Show-Group([string]$title, $items, [string]$prefix, [bool]$markConflicts = $false) { if ($items -and $items.Count -gt 0) { Write-Host "• $title ($($items.Count))" foreach ($i in $items) { if ($markConflicts -and $conflicts.Contains($i)) { Write-Host (" {0} <<CONFLICT>> {1}" -f $prefix, $i) } else { Write-Host (" {0} {1}" -f $prefix, $i) } } Write-Host "" } } Show-Group "Staged changes" $staged "+" $false Show-Group "Unstaged changes" $unstaged "~" $true Show-Group "Untracked files" $untracked "?" $false if ($behind -gt 0 -and $upstream) { W-Warn ("Branch is behind upstream by {0} commit(s). Consider: git pull --rebase" -f $behind) } if ($conflicts.Count -gt 0) { W-Warn "Unmerged changes detected. Resolve conflicts, then:" Write-Host " git add <files> && git rebase --continue (or) git commit" } if ($staged.Count -eq 0 -and $unstaged.Count -eq 0 -and $untracked.Count -eq 0) { W-Ok "Working tree clean." } |