mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-05 16:34:00 +01:00
forgetNotes removed the note locally before the leased push, so a rejected lease left a local notes ref without the note that the next plain persist pushed without a lease. It now snapshots the ref and restores it on rejection, reporting failed removals separately. forget refused only merging/gate-pending/free-pending; built, pushed, free-green, dispatch-unknown and dispatch-absent are in flight too and are refused the same way. CHANGELOG and ship-policy say so. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
57 lines
4.2 KiB
TypeScript
57 lines
4.2 KiB
TypeScript
/**
|
|
* `train forget --train <nonce> | --receipt <pr>@<head> [--dry-run]`: remove a
|
|
* train record (its attempts and bisect ledger) or a READY receipt from
|
|
* refs/notes/gstack-trains under an exact lease, then delete the local files
|
|
* (until now a stale record needed a hand-run
|
|
* `git notes remove` plus a file hunt on every machine that ran the train).
|
|
* A rejected lease removes nothing (the local notes ref is restored too, so a
|
|
* later write cannot carry the removal without a lease). Other machines that
|
|
* still hold the local files can push the record back with their next write;
|
|
* the output says so.
|
|
*/
|
|
import * as fs from 'node:fs';
|
|
import type { QueueContext } from './ship-queue';
|
|
import { outOk, outRefused, trainEnv, type TrainOut } from './ship-queue-train';
|
|
import { attemptsOf, forgetNotes, loadBisect, loadReady, loadTrain, noteName, readyPath, rebuildFromNotes, trainDir, type AnyRecord, type TrainState } from './ship-queue-train-record';
|
|
|
|
const Q = 'gstack-ship-queue';
|
|
/** States with work still in flight (a branch being built or pushed, free tests or a gate running, a merge under way): forget never settles them. */
|
|
const LIVE_STATES: readonly TrainState[] = ['built', 'pushed', 'free-pending', 'free-green', 'dispatch-unknown', 'dispatch-absent', 'gate-pending', 'merging'];
|
|
|
|
export function trainForget(ctx: QueueContext, o: { train?: string; receipt?: string; dryRun: boolean }): TrainOut {
|
|
const env = trainEnv(ctx);
|
|
if ('code' in env) return outRefused(env, [], 'none', 'none');
|
|
rebuildFromNotes(ctx.repoRoot, env.remote);
|
|
const records: AnyRecord[] = [];
|
|
const files: string[] = [];
|
|
let what = '';
|
|
if (o.train) {
|
|
const t = loadTrain(o.train);
|
|
if (!t) return outRefused({ code: 'QUEUE_PROOF_MISSING', message: `no train record ${o.train} here or in refs/notes/gstack-trains` }, [], `${Q} train status`, 'none');
|
|
if (LIVE_STATES.includes(t.state)) {
|
|
return outRefused({ code: 'QUEUE_PROOF_MISSING', message: `train ${t.nonce} is ${t.state}; forget settles nothing that is still building, running or merging (train prune --stale for a train whose PRs merged elsewhere)` }, [], `${Q} train status --train ${t.nonce}`, trainDir(t.nonce));
|
|
}
|
|
const bisect = loadBisect(t.nonce);
|
|
records.push(t, ...attemptsOf(t), ...(bisect ? [bisect] : []));
|
|
files.push(trainDir(t.nonce));
|
|
what = `train ${t.nonce} (${t.state}; ${t.attempts.length} attempt(s)${bisect ? ', a bisect ledger' : ''})`;
|
|
} else {
|
|
const m = /^#?(\d+)@([0-9a-f]{40,64})$/.exec(o.receipt ?? '');
|
|
if (!m) return outRefused({ code: 'QUEUE_PROOF_MISSING', message: '--receipt takes <pr>@<full head sha>' }, [], `${Q} train forget --receipt <pr>@<sha>`, 'none');
|
|
const r = loadReady(env.repo.keys, m[1]!, m[2]!);
|
|
if (!r) return outRefused({ code: 'QUEUE_PROOF_MISSING', message: `no READY receipt for #${m[1]} at ${m[2]!.slice(0, 12)} here or in refs/notes/gstack-trains` }, [], `${Q} train status`, 'none');
|
|
records.push(r);
|
|
files.push(readyPath(r.repo_key, r.pr, r.head));
|
|
what = `READY receipt #${r.pr} at ${r.head.slice(0, 12)} (screen ${r.screen.choice}${r.screen.run ? `, run ${r.screen.run}` : ''})`;
|
|
}
|
|
const names = records.map(noteName);
|
|
const lines = [`FORGET: ${what}`, ...names.map(n => ` note ${n}`), ...files.map(f => ` local ${f}`)];
|
|
if (o.dryRun) return outOk([...lines, 'DRY-RUN: nothing removed'], `${Q} train forget ${o.train ? `--train ${o.train}` : `--receipt ${o.receipt}`}`, 'none');
|
|
const f = forgetNotes(ctx.repoRoot, names, env.remote);
|
|
if (!f.pushed) return outRefused({ code: 'QUEUE_PROOF_MISSING', message: `the notes ref was not updated (${f.detail}); nothing deleted locally` }, lines, `${Q} train forget ${o.train ? `--train ${o.train}` : `--receipt ${o.receipt}`} (the lease is re-read each run)`, 'none');
|
|
for (const file of files) fs.rmSync(file, { recursive: true, force: true });
|
|
lines.push(`FORGOTTEN: ${f.removed.length} note(s) removed${f.missing.length ? `, ${f.missing.length} already absent` : ''}; ${f.detail}; ${files.length} local path(s) deleted`,
|
|
'note: a machine that still holds these files locally can push the record back with its next write; run train forget there too, or clear its $GSTACK_STATE_ROOT/ship-queue copy');
|
|
return outOk(lines, `${Q} train status`, 'none');
|
|
}
|