/** * The daemon's own git and `gh` operations. * * Everything here runs as the daemon, with the operator's credentials, and it is * the only place this package **constructs** credential material for a git or * `gh` call of its own. That is the organising idea: "what does the conductor * itself do with the operator's credential" is answered by this file's call sites * rather than by auditing every `Bun.spawn` in the tree. * * It is emphatically **not** a claim that nothing else can reach that credential. * Sessions are child processes of the daemon and inherit its environment, so a * session can reach whatever the daemon can — `omp.ts` spawns them and does not * scrub anything. What keeps a run's publication accountable is that the * dispatcher performs it: a push or a pull request the daemon did not make is a * run it cannot account for, and is absent from the verb ledger. The mediated * verbs route a worker's intent through here so the settlement record and the * branch cannot disagree. */ import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { homedir, tmpdir } from "node:os"; import { join } from "node:path"; import { parseChainSource, type ChainEntry } from "./chain-check.ts"; import type { ProjectConfig, RepoTarget } from "./types.ts"; import { ensureMirror } from "./worktree.ts"; /** * One process run, captured. Injected everywhere in this module so the * daemon-side push is testable without a network or a credential. */ export type Exec = ( argv: readonly string[], opts?: { cwd?: string; env?: Record; stdin?: string }, ) => Promise<{ code: number; stdout: string; stderr: string }>; /** The real one. Never inherits stdin: an unattended daemon must not block on a prompt. */ export const spawnCaptured: Exec = async (argv, opts = {}) => { const [command, ...rest] = argv; if (command === undefined) return { code: 127, stdout: "", stderr: "empty argv" }; const proc = Bun.spawn([command, ...rest], { ...(opts.cwd === undefined ? {} : { cwd: opts.cwd }), ...(opts.env === undefined ? {} : { env: opts.env }), stdin: new Blob([opts.stdin ?? ""]), stdout: "pipe", stderr: "pipe", }); const [stdout, stderr, code] = await Promise.all([ new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited, ]); return { code, stdout, stderr }; }; /** * The daemon's own environment, credentials intact. **The only place this * package constructs credential material.** * * There is nothing clever in the body, and that is deliberate: the value is * that it is one named function, so a privileged spawn that does not come * through here is a bug, whatever it happens to do. * * `GIT_TERMINAL_PROMPT=0` rides along because every caller is unattended: a * credential prompt nobody can answer is a hang, not a failure. */ export function credentialedEnv( extra: Readonly> = {}, base: Readonly> = process.env, ): Record { const env: Record = {}; for (const [key, value] of Object.entries(base)) { if (value !== undefined) env[key] = value; } env["GIT_TERMINAL_PROMPT"] = "0"; return { ...env, ...extra }; } /** * The exact per-run dubious-ownership exemption for a daemon git call against * a worker-owned run repository (#816). * * Every daemon-side invocation whose cwd (or opening) is the run repository * carries the run's own path as a command-line `-c safe.directory=` — * never a wildcard, and never an entry in the daemon's global config, which * would grow one line per run forever. Git 2.35+ refuses to open a repository * owned by another uid ("dubious ownership"); the run repository was handed * to the worker identity at dispatch, so the daemon's own `rev-parse`, * salvage, lane probe and cleanup calls are exactly that shape. */ export function runRepoSafeDirectoryExemption(runRepoPath: string): [string, string] { return ["-c", `safe.directory=${runRepoPath}`]; } /** * A daemon-owned, per-call global git config granting the *source-side* * ownership exemption for a local-path fetch out of one worker-owned run * repository. * * `git fetch ` from the mirror is the one daemon git call whose * *source* is worker-owned, and the one shape command-line config cannot * reach: git spawns `upload-pack` against the source — `.git` dubious-ownership * check included — and that subprocess sees none of the destination process's * `-c` options. The only real config file the subprocess reads that this side * controls is the global one, so the run's exemption is materialised there, * for exactly this call: a temp file (0700, daemon's own) naming precisely the * run's repo path and its gitdir, never a wildcard, with the daemon's existing * global config replayed through `include.path` so nothing else about the * caller's git behavior changes. The file is removed when the call is done. */ export interface ScopedSafeDirectory { /** The env additions for one git call; the caller's own use of it is what * scopes the exemption to that call. */ env: Record; /** Remove the daemon-owned temp file. The call must not outlive it. */ close: () => void; } /** A config value: git's quoting accepts C escapes in double quotes. */ function quoteGitConfigValue(value: string): string { return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`; } export function scopedSafeDirectoryEnv(...runRepos: readonly string[]): ScopedSafeDirectory { const dir = mkdtempSync(join(tmpdir(), "omp-conductor-safe-")); try { // The global-config chain the daemon normally reads, replayed: setting // GIT_CONFIG_GLOBAL would otherwise replace those files rather than // augmenting them, dropping e.g. credential helpers and `insteadOf` rules // for the duration of this one call. const globals: string[] = []; const globalOverride = process.env["GIT_CONFIG_GLOBAL"]; if (globalOverride !== undefined && globalOverride !== "") { globals.push(globalOverride); } else { const xdgHome = process.env["XDG_CONFIG_HOME"]; const xdgConfig = xdgHome === undefined || xdgHome === "" ? join(homedir(), ".config", "git", "config") : join(xdgHome, "git", "config"); if (existsSync(xdgConfig)) globals.push(xdgConfig); const home = homedir(); if (home !== "") { const gitconfig = join(home, ".gitconfig"); if (existsSync(gitconfig)) globals.push(gitconfig); } } const lines = [ ...globals.flatMap((file) => ["[include]", `\tpath = ${quoteGitConfigValue(file)}`]), "[safe]", // Both spellings git actually checks (2.43 and later use the gitdir // path for a local-path source; a direct open names the worktree path). // Both are exact, never a wildcard. ...runRepos.flatMap((repo) => [ `\tdirectory = ${quoteGitConfigValue(repo)}`, `\tdirectory = ${quoteGitConfigValue(join(repo, ".git"))}`, ]), "", ]; const file = join(dir, "gitconfig"); writeFileSync(file, lines.join("\n")); return { env: { ...credentialedEnv(), GIT_CONFIG_GLOBAL: file }, close: () => rmSync(dir, { recursive: true, force: true }), }; } catch (err) { rmSync(dir, { recursive: true, force: true }); throw err; } } // ------------------------------------------------- the privileged publish path /** One run's own repository, as the daemon addresses it. */ export interface RunRepoRef { repo: RepoTarget; /** The run's own git repository — not a linked worktree of the mirror. */ runRepoPath: string; branch: string; } export type PushOutcome = { ok: true; sha: string } | { ok: false; stderr: string }; export type PrOutcome = { ok: true; url: string } | { ok: false; stderr: string }; /** A clone URL can carry a token, and it lands in git's own error text. */ const URL_USERINFO = /([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)[^\s/@]+@/g; /** Blank the `user:token@` part of any URL before a string reaches a log or a human. */ export function scrubUserinfo(text: string): string { return text.replace(URL_USERINFO, "$1***@"); } export function mirrorPath(project: Pick, repo: RepoTarget): string { return join(project.mirrorRoot, `${repo.name}.git`); } /** * Everything the file-lane interlock (#555) needs to read one active run's lane. * * The dispatcher may read worktrees — this is squarely its job, and the issue * is explicit that the orchestrator must not be the one guessing file lanes * from another run's tree. A live run advertises its lane from its own worktree * (`git status --porcelain` plus `git diff --name-only ...HEAD`, exactly * the two commands the continuation brief already hands every worker), which is * the truth rather than the run's issue body — the incident this interlock * exists for was an issue *saying* one set of files while its tree wrote * another. A worker-finished run (`pushed-pending` / `pushed-green`) no longer * has a worktree, but its open PR still reaches for base's files, so its lane * is read from the retained branch in the mirror instead. */ export interface RunLaneInput { /** Conductor-created worktree path; `""` when the run has no live checkout. */ worktree: string; /** Tracking ref of the base branch to diff against, e.g. `refs/remotes/origin/main`. */ baseRef: string; /** The run's branch ref in the mirror (e.g. `refs/heads/feat-x`), for a * worker-finished run whose worktree has been removed. */ branchRef?: string; /** The repo's bare mirror directory, for the branch-diff read. */ mirror?: string; } /** * One file a run's lane covers, tagged with the probe read that produced it. * The source is what lets a `file-lane` hold say *which* occupancy it is — the * run's live worktree (`"worktree"`: uncommitted authored changes, from the * porcelain read) or its committed work (`"branch"`: the base-relative diff, * worktree or mirror) — so "held by run #N" can be told apart from the * base-reconciliation noise #684 filters out entirely. */ export interface LaneFile { file: string; source: LaneSource; } /** Which of the probe's reads produced one occupied file. */ export type LaneSource = "worktree" | "branch"; /** * The set of files one active run has touched relative to base, deduped and * sorted, each tagged with the read that produced it. Persistable but * advisory: the admitting gate treats an unreadable or absent lane as "no * overlap" (fail open), so no probe bug can refuse a well-formed issue. */ export type RunLaneProbe = (input: RunLaneInput) => Promise; /** The real one, reading the run's own worktree and/or the mirror branch. */ export async function probeRunLane( input: RunLaneInput, exec: Exec = spawnCaptured, ): Promise { const files = new Map(); const add = (path: string, source: LaneSource): void => { if (path !== "" && !files.has(path)) files.set(path, source); }; const addDiff = (stdout: string, source: LaneSource): void => { for (const line of stdout.split("\n")) add(line.trim(), source); }; if (input.worktree !== "") { // The live run's worktree is owned by the worker identity (#798), so // every daemon-side read of it carries the exact per-run exemption — no // wildcard, no global safe.directory (#816). const exemption = runRepoSafeDirectoryExemption(input.worktree); const status = await exec(["git", ...exemption, "-C", input.worktree, "status", "--porcelain"], {}); if (status.code === 0) { const { untracked, tracked } = parsePorcelain(status.stdout); // Untracked files are authored by construction: a merge stages the files // it brings in, it never leaves them untracked. for (const path of untracked) add(path, "worktree"); if (tracked.length > 0) { // While a merge of the base is in progress — the reconciliation every // continuation brief requires — the porcelain read reports every file // the merge staged, which is every file the base changed since the // branch's merge-base, none of them the run's own work (#684). The // three-dot diff is immune (merge-base relative), so the porcelain // half is gated on a live MERGE_HEAD and filtered to files whose // worktree content has actually diverged from base: reconciliation // brings base content forward (worktree == base), authored work does // not. Unresolved conflicts were already excluded by // {@link parsePorcelain}: a conflicted file is not the run's // resolution. An unreadable divergence read fails open — no tracked // occupancy is claimed while the merge makes the read ambiguous. const merge = await exec( ["git", ...exemption, "-C", input.worktree, "rev-parse", "-q", "--verify", "MERGE_HEAD"], {}, ); if (merge.code === 0) { const diverged = await exec( ["git", ...exemption, "-C", input.worktree, "diff", "--name-only", input.baseRef], {}, ); if (diverged.code === 0) { const set = new Set( diverged.stdout.split("\n").map((line) => line.trim()).filter((line) => line !== ""), ); for (const path of tracked) if (set.has(path)) add(path, "worktree"); } } else { for (const path of tracked) add(path, "worktree"); } } } const diff = await exec( ["git", ...exemption, "-C", input.worktree, "diff", "--name-only", `${input.baseRef}...HEAD`], {}, ); if (diff.code === 0) addDiff(diff.stdout, "branch"); } if (input.branchRef !== undefined && input.mirror !== undefined && input.mirror !== "") { const diff = await exec( ["git", "--git-dir", input.mirror, "diff", "--name-only", `${input.baseRef}...${input.branchRef}`], {}, ); if (diff.code === 0) addDiff(diff.stdout, "branch"); } return [...files.entries()] .map(([file, source]) => ({ file, source })) .sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0)); } /** * The porcelain codes `git status --porcelain` emits for an unmerged index * entry — a merge/rebase/cherry-pick conflict. The X/Y letters are then the * stage-2/stage-3 states and only ever appear in this exact set (git-status(1) * lists them all), so membership is unambiguous. */ const UNMERGED_PORCELAIN: Record = { UU: true, AA: true, DD: true, AU: true, UA: true, DU: true, UD: true, }; /** * Parse `git status --porcelain` output into the paths it names, split by how * they may occupy a lane. A rename or copy is reported as `XY old -> new`, and * the *new* path is the one that occupies the lane (it is the path the other * run's future writes would collide with), so the arrow form keeps only its * target. Untracked (`??`) paths are always the run's own work. Unmerged * (`UU`-class) paths are dropped: a conflicted file is not the run's * resolution, so it neither occupies a lane nor proves authorship. */ function parsePorcelain(stdout: string): { untracked: string[]; tracked: string[] } { const untracked: string[] = []; const tracked: string[] = []; for (const raw of stdout.split("\n")) { if (raw.length < 3) continue; const code = raw.slice(0, 2); if (code === "??") { untracked.push(raw.slice(2).trim()); continue; } if (UNMERGED_PORCELAIN[code] === true) continue; let rest = raw.slice(2).trim(); const arrow = rest.indexOf(" -> "); if (arrow !== -1) rest = rest.slice(arrow + 4).trim(); if (rest !== "") tracked.push(rest); } return { untracked, tracked }; } /** GitHub slug parsed off the clone URL, falling back to the routing name. */ export function repoSlugFor(repo: RepoTarget): string { const m = /(?:[:/])([^/:]+\/[^/]+?)(?:\.git)?$/.exec(repo.cloneUrl); return m?.[1] ?? repo.name; } /** * Publish a run's branch: run repo → mirror → GitHub, **fast-forward only**. * * The worker commits into its own repository, which is durable and local; every * hop that touches the network happens here, so the dispatcher observes the * exact sha it recorded. * * The run-repo hop carries **no leading `+`** — that single character is the * difference between "advance the mirror's copy of this branch" and "make the * mirror's copy whatever the run says it is", and a run repo is the one place * in this system that model-executed code fully controls. The push carries no * `--force` and no `--force-with-lease`: a lease is still a force, and a * rejected push is a decision for a human, not a retry. * * The one exception is narrower than it looks. The mirror's copy of a run * branch can be stale in a way that is not the run's fault: * `conductor_pr_update_branch` moves — and on a rewritten history, replaces — * the remote branch server-side after a run's last push, so a continuation * that incorporated the reviewed baseline can propose a head the *live remote* * is an ancestor of while the mirror's old copy is not. That is a legitimate * fast-forward, not a divergence, so when the plain copy is refused this push * reconciles the mirror's remote-tracking ref of the run branch with the live * remote and asks git whether the live head is an ancestor of the proposed * head. Only that proof lets the mirror's copy be refreshed, and the push hop * still re-checks the ancestor relation against the live remote itself. * Everything else is refused carrying the exact proposed and live SHAs so a * real divergence is diagnosable. * * A rejection — non-fast-forward, protected ref, unauthorised — comes back as a * value carrying the reason, because the caller settles the run `failed` on it * and a paraphrase is worthless in that report. */ export async function pushRunBranch( project: Pick, run: RunRepoRef, exec: Exec = spawnCaptured, ): Promise { const mirror = mirrorPath(project, run.repo); const ref = `refs/heads/${run.branch}`; const tracked = `refs/remotes/origin/${run.branch}`; const env = credentialedEnv(); // The run repository is worker-owned (#798): the direct read below carries // the exact-path exemption on the command line, and the local-path fetch's // source (upload-pack) needs it as a scoped global config for the one call // (#816). Both are exact per-run entries, never a wildcard, and the scoped // file dies with the call. const scoped = scopedSafeDirectoryEnv(run.runRepoPath); try { // Copy the run's branch into the mirror, fast-forward only. const fetched = await exec(["git", ...runRepoSafeDirectoryExemption(run.runRepoPath), "-C", mirror, "fetch", "--no-tags", run.runRepoPath, `${ref}:${ref}`], { env: scoped.env }); // The proposed head is read from the run repo itself: when the copy above // was refused, the mirror's copy of the branch is exactly the stale one. const proposedRun = await exec(["git", ...runRepoSafeDirectoryExemption(run.runRepoPath), "-C", run.runRepoPath, "rev-parse", ref], { env: scoped.env }); if (proposedRun.code !== 0) { return { ok: false, stderr: scrubUserinfo(proposedRun.stderr.trim() || `git rev-parse ${ref} exited ${String(proposedRun.code)}`) }; } const proposed = proposedRun.stdout.trim(); // Reconcile with the live remote before enforcing anything: the ancestry // that decides a fast-forward is the live remote's, not the mirror's copy's // (see the docstring above). ls-remote answers "is the branch published at // all" and "what does GitHub hold" in one call. const liveListed = await exec(["git", "-C", mirror, "ls-remote", "origin", ref], { env }); if (liveListed.code !== 0) { return { ok: false, stderr: scrubUserinfo(liveListed.stderr.trim() || `git ls-remote origin ${ref} exited ${String(liveListed.code)}`) }; } const live = liveListed.stdout .split("\n") .map((line) => line.trimEnd()) .find((line) => line.endsWith(`\t${ref}`)) ?.split(/\s+/, 1)[0]; if (live !== undefined && live !== proposed) { // The branch is published and the run proposes something different. Pull // the live branch's history into the mirror once (which also keeps the // tracked ref a reattach reads fresh), then prove the fast-forward. const reconciled = await exec(["git", "-C", mirror, "fetch", "--no-tags", "origin", `+${ref}:${tracked}`], { env }); if (reconciled.code !== 0) { return { ok: false, stderr: scrubUserinfo(reconciled.stderr.trim() || reconciled.stdout.trim() || `git fetch origin exited ${String(reconciled.code)}`) }; } const isAncestor = await exec(["git", "-C", mirror, "merge-base", "--is-ancestor", live, proposed], { env }); if (isAncestor.code === 128) { // git could not perform the check at all (an object it was asked to // resolve is missing, not merely unrelated). That is a failed // verification, not a divergence verdict: refuse with git's own words, // still naming both SHAs so the report carries the mismatch. return { ok: false, stderr: scrubUserinfo( isAncestor.stderr.trim() || isAncestor.stdout.trim() || `git merge-base --is-ancestor ${live} ${proposed} exited ${String(isAncestor.code)}`, ), }; } if (isAncestor.code !== 0) { // A real divergence: no fast-forward exists, and the mirror is left // exactly where it was. Both SHAs are named so the mismatch is // diagnosable instead of a bare "non-fast-forward". return { ok: false, stderr: `refusing non-fast-forward push of ${run.branch}: the live remote tip ${live} is not an ancestor of ` + `the proposed head ${proposed}. Fetch the live branch and rebase or merge it before pushing again.`, }; } } if (fetched.code !== 0 && live === undefined) { // The mirror refused the plain copy and there is no published branch to // validate the run's head against: a rewritten branch that was never // published must not rewrite the mirror either. Refuse with git's words. return { ok: false, stderr: scrubUserinfo(fetched.stderr.trim() || fetched.stdout.trim() || `git fetch exited ${String(fetched.code)}`) }; } if (fetched.code !== 0) { // The proposed head is a legitimate fast-forward over the live remote (it // equals the live head or the ancestor test above passed), so the only // thing the plain copy refused on was the mirror's own stale copy. // Refresh it; the push below still re-checks against the live remote. const refreshed = await exec(["git", ...runRepoSafeDirectoryExemption(run.runRepoPath), "-C", mirror, "fetch", "--no-tags", run.runRepoPath, `+${ref}:${ref}`], { env: scoped.env }); if (refreshed.code !== 0) { return { ok: false, stderr: scrubUserinfo(refreshed.stderr.trim() || refreshed.stdout.trim() || `git fetch exited ${String(refreshed.code)}`) }; } } const pushed = await exec(["git", "-C", mirror, "push", "origin", `${ref}:${ref}`], { env }); if (pushed.code !== 0) { return { ok: false, stderr: scrubUserinfo(pushed.stderr.trim() || pushed.stdout.trim() || `git push exited ${String(pushed.code)}`) }; } return { ok: true, sha: proposed }; } finally { scoped.close(); } } /** * Open the run's pull request from the daemon's side. * * The body goes over stdin, never argv: a body with newlines, backticks or a * leading `-` is ordinary, and interpolating it into a command line is how a * worker's report would become an argument. */ export async function openRunPr( project: Pick, run: RunRepoRef, opts: { title: string; body: string; base: string }, exec: Exec = spawnCaptured, ): Promise { void project; const out = await exec( [ "gh", "pr", "create", "--repo", repoSlugFor(run.repo), "--head", run.branch, "--base", opts.base, "--title", opts.title, "--body-file", "-", ], { env: credentialedEnv(), stdin: opts.body }, ); if (out.code !== 0) { return { ok: false, stderr: scrubUserinfo(out.stderr.trim() || out.stdout.trim() || `gh pr create exited ${String(out.code)}`) }; } const url = /https:\/\/github\.com\/\S+\/pull\/\d+/.exec(out.stdout)?.[0]; if (url === undefined) { return { ok: false, stderr: `gh pr create succeeded but printed no PR URL: ${out.stdout.trim()}` }; } return { ok: true, url }; } // ------------------------------------------------- the migration-chain guard /** * The base branch's ordered migration chain, read at the tip — not from a * checkout that might be stale, but from the live mirror `ensureMirror` * refreshes every call, which is the same freshness contract the dispatch path * relies on. Alembic fields only; files without a `revision` line are skipped. */ export async function readBaseChain( project: Pick, repo: RepoTarget, dir: string, exec: Exec = spawnCaptured, ): Promise<{ ok: true; entries: ChainEntry[] } | { ok: false; stderr: string }> { try { const mirror = await ensureMirror(repo, project.mirrorRoot); const env = credentialedEnv(); const listed = await exec( ["git", "--git-dir", mirror, "ls-tree", "-r", "--name-only", repo.defaultBranch, "--", dir], { env }, ); if (listed.code !== 0) { return { ok: false, stderr: scrubUserinfo(listed.stderr.trim() || listed.stdout.trim() || `git ls-tree exited ${String(listed.code)}`), }; } const entries: ChainEntry[] = []; for (const rel of listed.stdout.split("\n")) { const path = rel.trim(); if (path === "" || !path.endsWith(".py")) continue; const shown = await exec(["git", "--git-dir", mirror, "show", `${repo.defaultBranch}:${path}`], { env }); if (shown.code !== 0) { return { ok: false, stderr: scrubUserinfo(shown.stderr.trim() || `git show ${path} exited ${String(shown.code)}`), }; } const parsed = parseChainSource(path, shown.stdout); if (parsed !== undefined) entries.push(parsed); } return { ok: true, entries }; } catch (err) { return { ok: false, stderr: err instanceof Error ? err.message : String(err) }; } } // ------------------------------------------- the shared-host critical-base guard /** * The stale-base admission verdict (#428): whether a preserved continuation * branch may be reattached at all, once a project marks a base commit as * critical. Fail closed — a branch that cannot be *proven* to contain every * marker is refused. */ export type CriticalBaseVerdict = /** No preserved branch for this issue in the mirror — a clean first attempt. */ | { state: "no-branch" } /** The reattach source contains every configured marker. */ | { state: "fresh" } /** The reattach source predates a marker and cannot be safely advanced to it, * so the dispatcher must not reattach it. `range` names the base commits * the branch has not absorbed, for the hold's recovery wording. */ | { state: "stale"; marker: string; range: string[] } /** The marker could not be verified at all (unresolvable, fetch failed). */ | { state: "unknown"; error: string }; /** The stubborn-session view of the probe, injectable for deterministic tests. */ export type CriticalBaseProbe = ( repo: RepoTarget, markers: readonly string[], branch: string, ) => Promise; /** The bounded sample of base commits a held branch is missing. */ const CRITICAL_BASE_RANGE_MAX = 8; /** * Answer whether the preserved branch {@link addRunRepo} would reattach * contains every configured critical-base marker (#428). A base safety fix * protects only branches forked after it landed; a continuation forked before * it still carries the dangerous code, and re-running the lifecycle suite it * retains on a shared host is what SIGTERMed the production daemon. * * The reattach source is the mirror's `refs/heads/`. When it lacks a * marker but the live remote branch now carries it and the reattach source is * that live head's ancestor, the operator advanced the branch on GitHub (e.g. * merged base into the PR branch): the fast-forward is folded into the mirror * — safe and work-preserving, the same reconcile `pushRunBranch` performs — * and the marker is accepted. Judged against the reattach source, never the * live head alone, so a stale local copy cannot smuggle pre-fix code back in. */ export async function probeCriticalBase( project: Pick, repo: RepoTarget, branch: string, markers: readonly string[], exec: Exec = spawnCaptured, ): Promise { const mirror = mirrorPath(project, repo); // A mirror is where preserved branches live; without one there is nothing to // reattach. Judged "no branch" rather than "unknown" so a fleet that has not // yet created a mirror is not deadlocked at admission. if (!existsSync(mirror)) return { state: "no-branch" }; const branchRef = `refs/heads/${branch}`; const env = credentialedEnv(); const present = await exec( ["git", "--git-dir", mirror, "show-ref", "--verify", "--quiet", branchRef], { env }, ); if (present.code !== 0) return { state: "no-branch" }; // The configured marker is a commit that landed on base, so base has to be // resolvable to prove anything about the branch against it. Fetch today's // base tip into the mirror's tracking refs first, so the judgement is against // the current base rather than whatever a run last left behind. const baseRef = `refs/remotes/origin/${repo.defaultBranch}`; const fetched = await exec( ["git", "--git-dir", mirror, "fetch", "--no-tags", "origin", `+refs/heads/${repo.defaultBranch}:${baseRef}`], { env }, ); if (fetched.code !== 0) { return { state: "unknown", error: scrubUserinfo(fetched.stderr.trim() || fetched.stdout.trim() || `git fetch exited ${String(fetched.code)}`), }; } for (const marker of markers) { const resolved = await exec( ["git", "--git-dir", mirror, "rev-parse", "--verify", `${marker}^{commit}`], { env }, ); if (resolved.code !== 0) { return { state: "unknown", error: `critical-base marker "${marker}" is not resolvable in the ${repo.name} mirror`, }; } const markerSha = resolved.stdout.trim(); if (markerSha === "") { return { state: "unknown", error: `critical-base marker "${marker}" resolved to no commit` }; } const localHas = await exec( ["git", "--git-dir", mirror, "merge-base", "--is-ancestor", markerSha, branchRef], { env }, ); if (localHas.code === 0) continue; // The reattach source lacks the marker. If the live branch now carries it // and the reattach source is its ancestor, the operator advanced the branch // on GitHub (e.g. merged base into the PR branch): fold that fast-forward // into the reattach source — safe, work-preserving — and accept the marker. const tracked = `refs/remotes/origin/${branch}`; const liveFetched = await exec( ["git", "--git-dir", mirror, "fetch", "--no-tags", "origin", `+refs/heads/${branch}:${tracked}`], { env }, ); if (liveFetched.code !== 0) { return { state: "unknown", error: scrubUserinfo(liveFetched.stderr.trim() || liveFetched.stdout.trim() || `git fetch ${branch} exited ${String(liveFetched.code)}`), }; } const liveHas = await exec( ["git", "--git-dir", mirror, "merge-base", "--is-ancestor", markerSha, tracked], { env }, ); if (liveHas.code === 0) { const localIsAncestor = await exec( ["git", "--git-dir", mirror, "merge-base", "--is-ancestor", branchRef, tracked], { env }, ); if (localIsAncestor.code === 0) { await exec(["git", "--git-dir", mirror, "fetch", "--no-tags", "origin", `+${branchRef}:${branchRef}`], { env }); continue; } } // Predates the marker and cannot be safely advanced to it. Name the base // commits the branch has not absorbed so the hold's recovery is concrete. const range: string[] = []; const missing = await exec( ["git", "--git-dir", mirror, "log", "--oneline", "--format=%h", `${branchRef}..${baseRef}`], { env }, ); if (missing.code === 0) { range.push( ...missing.stdout .trim() .split("\n") .filter((line) => line.length > 0) .slice(0, CRITICAL_BASE_RANGE_MAX), ); } return { state: "stale", marker: markerSha, range }; } return { state: "fresh" }; }