scripts/internal/continuous-co-review/co-review-trunk-resolver.ps1
|
# Shared TRUNK RESOLUTION - the one repository capability that decides which branch the co-review gate treats as # the shipped trunk (the merge-base anchor / diff baseline). Replaces the duplicated 'main' defaults + ad-hoc # candidate loops across the CLI, navigator, signoff gate, worktree baseline resolver, and lineage resolver. # # PRECEDENCE (maintainer 2026-07-13): # 1. Explicit co_review_trunk (a -Trunk override, else .specrew/config.yml co_review_trunk). # 2. refs/remotes/origin/HEAD (the remote's advertised default branch). # 3. The current branch's tracking REMOTE's default branch (branch.<name>.remote -> refs/remotes/<remote>/HEAD; # NEVER the @{upstream} ref itself, so a branch tracking origin/<self> does not become its own trunk). # 4. Existing CONVENTIONAL refs (main, master, develop, dev), local or origin/, in that priority order. # 5. A LOCAL-ONLY repository with exactly ONE pre-feature branch -> that branch. # 6. AMBIGUOUS -> fail with a clear configuration instruction. # # It NEVER creates, renames, or moves a branch to satisfy the gate. A repo whose ONLY branch is the feature # branch (no trunk) resolves to `greenfield` (ok, trunk_ref = $null) so the baseline resolver uses the empty tree # - that is a legitimate state, not an ambiguity. # # Returns [pscustomobject]@{ ok; trunk_ref; source; message }: # ok=$true, trunk_ref=<ref> -> a trunk resolved (source names the precedence level). # ok=$true, trunk_ref=$null, source='greenfield' -> no trunk (single feature branch); caller uses the empty tree. # ok=$false, trunk_ref=$null -> failure (source: no-commit-repo | explicit-trunk-unresolvable | ambiguous), # message carries the human-facing configuration instruction. function Invoke-ContinuousCoReviewTrunkGit { # Prefer the encoding-immune resolver-git wrapper (hook-provider context); fall back to a direct call. param([Parameter(Mandatory)][string]$RepoRoot, [Parameter(Mandatory)][string[]]$Arguments) if (Get-Command -Name 'Invoke-ContinuousCoReviewResolverGit' -ErrorAction SilentlyContinue) { $r = Invoke-ContinuousCoReviewResolverGit -RepoRoot $RepoRoot -Arguments $Arguments return [pscustomobject]@{ ok = ($r.ExitCode -eq 0); lines = @($r.Output | ForEach-Object { ([string]$_).Trim() } | Where-Object { $_ -ne '' }) } } $out = @(& git -C $RepoRoot @Arguments 2>$null) return [pscustomobject]@{ ok = ($LASTEXITCODE -eq 0); lines = @($out | ForEach-Object { ([string]$_).Trim() } | Where-Object { $_ -ne '' }) } } function Test-ContinuousCoReviewGitRefExists { param([Parameter(Mandatory)][string]$RepoRoot, [Parameter(Mandatory)][AllowEmptyString()][string]$Ref) if ([string]::IsNullOrWhiteSpace($Ref)) { return $false } return (Invoke-ContinuousCoReviewTrunkGit -RepoRoot $RepoRoot -Arguments @('rev-parse', '--verify', '--quiet', ("{0}^{{commit}}" -f $Ref))).ok } # The explicit co_review_trunk from .specrew/config.yml (quote-strip + inline-comment tolerant). '' when unset. function Get-ContinuousCoReviewConfiguredTrunk { param([Parameter(Mandatory)][string]$RepoRoot) $configPath = Join-Path $RepoRoot '.specrew/config.yml' if (-not (Test-Path -LiteralPath $configPath -PathType Leaf)) { return '' } foreach ($line in Get-Content -LiteralPath $configPath -Encoding UTF8) { if ($line -match '^\s*co_review_trunk:\s*[''"]?(?<value>[^''"#]+?)[''"]?\s*(?:#.*)?$') { $value = $Matches['value'].Trim() if (-not [string]::IsNullOrWhiteSpace($value)) { return $value } } } return '' } function Resolve-ContinuousCoReviewTrunkRef { param( [Parameter(Mandatory)][string]$RepoRoot, [AllowEmptyString()][string]$Trunk = '' # an explicit override (e.g. the CLI --trunk); wins over config ) $mk = { param([bool]$ok, $ref, [string]$source, [string]$message) [pscustomobject]@{ ok = $ok; trunk_ref = $ref; source = $source; message = $message } } # 0. The repo must have commits (else HEAD - and every merge-base - is unresolvable). if (-not (Test-ContinuousCoReviewGitRefExists -RepoRoot $RepoRoot -Ref 'HEAD')) { return & $mk $false $null 'no-commit-repo' "The repository at '$RepoRoot' has no commits, so a trunk cannot be resolved. Commit a base branch, or set 'co_review_trunk: <branch>' in .specrew/config.yml." } $currentBranch = '' $cb = Invoke-ContinuousCoReviewTrunkGit -RepoRoot $RepoRoot -Arguments @('rev-parse', '--abbrev-ref', 'HEAD') if ($cb.ok -and $cb.lines.Count -gt 0) { $currentBranch = $cb.lines[0] } # 1. EXPLICIT co_review_trunk (override wins over config). Try the bare name, then origin/<name> # (a fresh checkout may hold the trunk only as a remote-tracking ref - the F5/145 case). $explicit = if (-not [string]::IsNullOrWhiteSpace($Trunk)) { $Trunk.Trim() } else { Get-ContinuousCoReviewConfiguredTrunk -RepoRoot $RepoRoot } if (-not [string]::IsNullOrWhiteSpace($explicit)) { $explicitCandidates = @($explicit) if ($explicit -notmatch '/') { $explicitCandidates += "origin/$explicit" } foreach ($cand in $explicitCandidates) { if (Test-ContinuousCoReviewGitRefExists -RepoRoot $RepoRoot -Ref $cand) { return & $mk $true $cand 'explicit-co_review_trunk' '' } } $tried = if ($explicit -notmatch '/') { "'$explicit' and 'origin/$explicit'" } else { "'$explicit'" } return & $mk $false $null 'explicit-trunk-unresolvable' "The configured co_review_trunk '$explicit' does not resolve to a ref (tried $tried). Set a valid branch in .specrew/config.yml (co_review_trunk), or remove it to auto-detect." } # 2. refs/remotes/origin/HEAD (the remote's advertised default). $oh = Invoke-ContinuousCoReviewTrunkGit -RepoRoot $RepoRoot -Arguments @('symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD') if ($oh.ok -and $oh.lines.Count -gt 0 -and (Test-ContinuousCoReviewGitRefExists -RepoRoot $RepoRoot -Ref $oh.lines[0])) { return & $mk $true $oh.lines[0] 'origin-head' '' } # 3. If the current branch tracks a REMOTE, resolve THAT remote's symbolic default branch # (refs/remotes/<remote>/HEAD) - NEVER the @{upstream} ref itself. A branch that tracks origin/<self> # (e.g. feature -> origin/feature) must not treat origin/<self> as trunk: the merge-base with itself is # empty, hiding the entire feature diff. A local-tracking upstream ('.') or no upstream falls through to # conventional/local-only resolution. (This generalizes level 2's hardcoded origin to the branch's actual # tracking remote, e.g. an 'upstream' fork remote whose HEAD is the canonical trunk.) if (-not [string]::IsNullOrWhiteSpace($currentBranch)) { $trackingRemote = @((Invoke-ContinuousCoReviewTrunkGit -RepoRoot $RepoRoot -Arguments @('config', '--get', "branch.$currentBranch.remote")).lines) if ($trackingRemote.Count -gt 0 -and $trackingRemote[0] -ne '' -and $trackingRemote[0] -ne '.') { $remoteHead = @((Invoke-ContinuousCoReviewTrunkGit -RepoRoot $RepoRoot -Arguments @('symbolic-ref', '--quiet', '--short', "refs/remotes/$($trackingRemote[0])/HEAD")).lines) if ($remoteHead.Count -gt 0 -and (Test-ContinuousCoReviewGitRefExists -RepoRoot $RepoRoot -Ref $remoteHead[0])) { return & $mk $true $remoteHead[0] 'tracking-remote-head' '' } } } # 4. Existing CONVENTIONAL refs (priority order), local then origin/, excluding the current branch. foreach ($c in @('main', 'master', 'develop', 'dev')) { if ($c -eq $currentBranch) { continue } if (Test-ContinuousCoReviewGitRefExists -RepoRoot $RepoRoot -Ref $c) { return & $mk $true $c 'conventional-ref' '' } if (Test-ContinuousCoReviewGitRefExists -RepoRoot $RepoRoot -Ref "origin/$c") { return & $mk $true "origin/$c" 'conventional-ref' '' } } # 5. LOCAL-ONLY repo with exactly ONE pre-feature branch. $remotes = @((Invoke-ContinuousCoReviewTrunkGit -RepoRoot $RepoRoot -Arguments @('remote')).lines) if ($remotes.Count -eq 0) { $branches = @((Invoke-ContinuousCoReviewTrunkGit -RepoRoot $RepoRoot -Arguments @('branch', '--format=%(refname:short)')).lines) $others = @($branches | Where-Object { $_ -ne $currentBranch }) if ($others.Count -eq 1) { return & $mk $true $others[0] 'single-pre-feature-branch' '' } if ($others.Count -eq 0) { return & $mk $true $null 'greenfield' 'The repository has only the feature branch (no trunk); the co-review baseline is the empty tree.' } # multiple pre-feature branches, none conventional -> ambiguous (fall through). } # 6. AMBIGUOUS -> fail with a clear configuration instruction (never guess, never move a branch). return & $mk $false $null 'ambiguous' "Could not unambiguously determine the trunk branch for '$RepoRoot'. Set 'co_review_trunk: <branch>' in .specrew/config.yml, or configure origin/HEAD or a branch upstream. Specrew never creates, renames, or moves a branch to resolve this." } |