g-registry.ps1
|
$GapRequirements = @{ B_CLASS = @('BRANCH_CREATE') W_CLASS = @('BRANCH_RENAME') BEHIND = @('REBASE', 'PULL') CHECKS = @('PR_CHECKS') NO_PUSH = @('PUSH') NO_PR = @('PR_CREATE') } # [ordered] so specific patterns match before generic subsets (e.g. BRANCH_CREATE before CHECKOUT) $CapabilityPatterns = [ordered]@{ BRANCH_CREATE = 'git\b.+checkout\b.+-b\b' PUSH_DELETE = 'git\b.+push\b.+--delete\b' BRANCH_RENAME = 'git\b.+branch\b.+-m\b' BRANCH_DELETE = 'git\b.+branch\b.+(-d|-D)\b' STAGE = 'git\b.+add\b' COMMIT = 'git\b.+commit\b' PUSH = 'git\b.+push\b' PULL = 'git\b.+pull\b' REBASE = 'git\b.+rebase\b' CHECKOUT = 'git\b.+checkout\b' MERGE = 'git\b.+merge\b' REVERT = 'git\b.+revert\b' TAG = 'git\b.+tag\b' PR_CREATE = 'gh\b.+pr\s+create\b' PR_MERGE = 'gh\b.+pr\s+merge\b' PR_READY = 'gh\b.+pr\s+ready\b' PR_CHECKS = 'gh\b.+pr\s+checks\b' PR_LIST = 'gh\b.+pr\s+list\b' } function Get-ScriptCapabilities { param( [string]$Path, [System.Collections.Generic.HashSet[string]]$Visited = $null ) if (-not $Visited) { $Visited = [System.Collections.Generic.HashSet[string]]::new() } if (-not $Visited.Add($Path)) { return [string[]]@() } $seen = [System.Collections.Generic.HashSet[string]]::new() $result = [System.Collections.Generic.List[string]]::new() foreach ($line in (Get-Content $Path)) { $t = $line.Trim() if (-not $t -or $t -match '^#' -or $t -match '^\$\w+\s*[+]?=\s*".*\b(git|gh)\b') { continue } # Inherit caps from scripts called with & via Join-Path; excludes dot-source (.) infrastructure loads if ($t -match '&\s+.*\bJoin-Path\b' -and $t -match "'(g-[^']+\.ps1)'") { $refPath = Join-Path (Split-Path $Path) $Matches[1] if (Test-Path $refPath) { foreach ($cap in (Get-ScriptCapabilities -Path $refPath -Visited $Visited)) { if ($seen.Add($cap)) { $result.Add($cap) } } } } foreach ($cap in $CapabilityPatterns.Keys) { if ($t -match $CapabilityPatterns[$cap]) { if ($seen.Add($cap)) { $result.Add($cap) } break } } } return [string[]]$result } $FlagScripts = @{ b = 'g-branch-create.ps1' r = 'g-branch-rename.ps1' s = 'g-branch-sync.ps1' c = 'g-commit-push.ps1' u = 'g-push.ps1' o = 'g-open-pr.ps1' x = 'g-pr-checks.ps1' m = 'g-merge-rotate.ps1' g = 'g-branch-base.ps1' k = 'g-branch-checkout.ps1' n = 'g-unstack.ps1' z = 'g-release.ps1' } $FlagCapabilities = @{} foreach ($flag in $FlagScripts.Keys) { $sp = Join-Path $PSScriptRoot $FlagScripts[$flag] if (Test-Path $sp) { $FlagCapabilities[$flag] = Get-ScriptCapabilities -Path $sp } } $AllCapabilities = [System.Collections.Generic.HashSet[string]]::new() foreach ($caps in $FlagCapabilities.Values) { foreach ($cap in $caps) { [void]$AllCapabilities.Add($cap) } } # Named flag sequences for the gitbox orchestrator $WorkflowRegistry = [ordered]@{ start = 'b' rename = 'r' sync = 's' commit = 'c' push = 'u' pr = 'o' checks = 'x' merge = 'm' revert = 'v' base = 'g' checkout = 'k' unstack = 'n' stack = 'T' promote = 'rcuo' submit = 'cuo' land = 'cxm' ship = 'xm' full = 'cuoxm' release = 'z' health = 'H' } function Get-GitboxConfig { param([string]$RepoPath = (Get-Location)) $cfgPath = Join-Path $RepoPath '.gitbox.json' $base = $null; $default = $null; $mergeStrategy = $null; $editor = $null; $postMerge = $null if (Test-Path $cfgPath) { $cfg = Get-Content $cfgPath -Raw | ConvertFrom-Json $base = $cfg.BaseBranch $default = $cfg.DefaultBranch if ($cfg.MergeStrategy) { $mergeStrategy = $cfg.MergeStrategy.ToLower() } if ($null -ne $cfg.Editor) { $editor = [bool]$cfg.Editor } if ($cfg.PostMerge) { $postMerge = $cfg.PostMerge.ToLower() } } else { $default = gh repo view --json defaultBranchRef -q .defaultBranchRef.name 2>$null if (-not $default) { $default = 'main' } $base = $default $mergeStrategy = 'merge' $editor = $false } # Partial-config fallbacks: fill any keys the config file omitted if (-not $default) { $default = gh repo view --json defaultBranchRef -q .defaultBranchRef.name 2>$null; if (-not $default) { $default = 'main' } } if (-not $base) { $base = $default } if ($null -eq $mergeStrategy) { $mergeStrategy = 'merge' } if ($null -eq $editor) { $editor = $false } if ($null -eq $postMerge) { $postMerge = 'wip' } return @{ BaseBranch = $base; DefaultBranch = $default; MergeStrategy = $mergeStrategy; Editor = $editor; PostMerge = $postMerge } } function Invoke-GitboxEditor { param([string]$Template = '') $editorCmd = git var GIT_EDITOR 2>$null if (-not $editorCmd) { $editorCmd = $env:EDITOR } if (-not $editorCmd) { Write-Host "no editor configured; set core.editor in git config"; return $null } $tmp = [System.IO.Path]::GetTempFileName() + ".txt" if ($Template) { Set-Content $tmp $Template -Encoding UTF8 } & $editorCmd $tmp if ($LASTEXITCODE -ne 0) { Remove-Item $tmp -ErrorAction SilentlyContinue; return $null } $content = ((Get-Content $tmp -Encoding UTF8 | Where-Object { $_ -notmatch '^\s*#' }) -join "`n").Trim() Remove-Item $tmp -ErrorAction SilentlyContinue return if ($content) { $content } else { $null } } function Get-PRRollup { param($CheckRollup) if (-not $CheckRollup) { return $null } if ($CheckRollup -is [string]) { return $CheckRollup.ToUpper() } if ($CheckRollup.Count -eq 0) { return $null } $fail = @($CheckRollup | Where-Object { $_.conclusion -in @('FAILURE','ERROR') -or $_.state -eq 'FAILURE' }) $pending = @($CheckRollup | Where-Object { $_.status -in @('QUEUED','IN_PROGRESS') -or $_.state -eq 'PENDING' }) $pass = @($CheckRollup | Where-Object { $_.conclusion -eq 'SUCCESS' -or $_.state -eq 'SUCCESS' }) if ($fail.Count -gt 0) { return 'FAILURE' } if ($pending.Count -gt 0) { return 'PENDING' } if ($pass.Count -gt 0) { return 'SUCCESS' } return $null } function Get-GitRepoState { param( [string]$RepoPath = (Get-Location), [int]$RunLimit = 0, [switch]$GitOnly ) $branch = git -C $RepoPath branch --show-current 2>$null if (-not $branch) { return $null } $baseBranch = (Get-GitboxConfig -RepoPath $RepoPath).BaseBranch $repoName = if (-not $GitOnly) { gh repo view --json nameWithOwner -q .nameWithOwner 2>$null } else { $null } $ahead = 0; $behind = 0 if (git -C $RepoPath rev-parse --verify "origin/$baseBranch" 2>$null) { $ahead = (git -C $RepoPath rev-list "origin/${baseBranch}..HEAD" 2>$null | Measure-Object -Line).Lines $behind = (git -C $RepoPath rev-list "HEAD..origin/${baseBranch}" 2>$null | Measure-Object -Line).Lines } $dirtyFiles = @(git -C $RepoPath status --porcelain 2>$null | Where-Object { $_ }) $remoteBranch = git -C $RepoPath rev-parse --verify "origin/$branch" 2>$null $unpushed = if ($remoteBranch) { (git -C $RepoPath rev-list "origin/${branch}..HEAD" 2>$null | Measure-Object -Line).Lines } else { -1 } $pr = $null if (-not $GitOnly -and $repoName) { $prJson = gh pr list --repo $repoName --head $branch --json number,state,title,reviewDecision,statusCheckRollup 2>$null | ConvertFrom-Json if ($prJson -and $prJson.Count -gt 0) { $pr = $prJson[0] } } $runs = $null if ($RunLimit -gt 0 -and $repoName) { $runsJson = gh run list --repo $repoName --branch $branch --limit $RunLimit --json databaseId,name,status,conclusion,createdAt 2>$null if ($runsJson) { $runs = $runsJson | ConvertFrom-Json } } return [pscustomobject]@{ Branch = $branch BaseBranch = $baseBranch RepoName = $repoName Ahead = $ahead Behind = $behind DirtyFiles = $dirtyFiles RemoteBranch = $remoteBranch Unpushed = $unpushed PR = $pr Runs = $runs } } # Populate $GapRequirements for any dim Resolve-MatrixAction uses that isn't in the static map. # Runs once at load time; pure in-memory — no I/O. Eliminates the manual step when adding a new dim. foreach ($cl_ in 'B','F','W') { foreach ($di_ in 'c','d1','s1') { foreach ($ah_ in 'a0','a1') { foreach ($be_ in 'b0','b1') { foreach ($pu_ in 'P','U') { foreach ($pr_ in 'PR-','PRD','PRO','PRX','PRA') { $ar_ = Resolve-MatrixAction -Hash "$cl_|$di_|$ah_|$be_|$pu_|$pr_" if ($ar_ -and $ar_.Dim -and -not $GapRequirements.ContainsKey($ar_.Dim)) { if ($ar_.Action -match 'gitbox\s+([a-z])') { $af_ = $Matches[1] if ($FlagCapabilities.ContainsKey($af_)) { $GapRequirements[$ar_.Dim] = [string[]]$FlagCapabilities[$af_] } } } } } } } } } |