mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 07:18:03 +01:00
* fix(restamp): a branch-added stamp left at the fork version is stale (GST-963) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(stamp): one side-aware stamp bound for the tree receipt, carry and finish (GST-963) A stamp row the branch added at its fork version now passes every stamp check, not only the restamp writer: stampOnlyLine takes the branch side (its own lines and fork version), and stampedFiles, the tree receipt and the receipt carry all pass it. The stamp-only checks move to lib/stamp-only.ts to keep lib/restamp.ts under the module-size ratchet. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(train): the train stamp and verify checks read the slot predecessor as the base (GST-963) The train restamp moves a branch-added row left at the predecessor version, so stampMismatch and the three train-verify receipts pass baseRef (the gated commit first parent), and the tree receipt applies the branch side even when the slot keeps its claimed VERSION. Failing-first: test/ship-queue-train-stamp.test.ts refused with QUEUE_TRAIN_STAMP_MISMATCH before this commit; on main the pin stamps but the row stays stale. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(changelog): GST-963 fragment names the carry and duplicate-text gaps (GST-979, GST-980) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(ship-policy): stamp_paths names the carry and duplicate-text gaps (GST-979, GST-980) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
71 lines
4.0 KiB
TypeScript
71 lines
4.0 KiB
TypeScript
/**
|
|
* Stamp-only checks: whether a removed/added line pair is a restamp alone, and
|
|
* the branch side (its own lines and fork version) that bounds that check for
|
|
* lines the branch wrote itself. Split from restamp.ts (module-size ratchet).
|
|
*/
|
|
import { spawnSync } from 'node:child_process';
|
|
import { branchAddedLines, forkVersion, rewriteStamps, stampMentions } from './restamp';
|
|
import { cmpVersion, parseVersion, versionWidth } from './version-source';
|
|
|
|
/**
|
|
* A removed/added line pair that a restamp alone explains: every version the
|
|
* removed line mentions in a stamp form and the added line no longer does
|
|
* moves to `newV`, and the rewritten line is the added line byte for byte.
|
|
* The moved versions are the ones the stamp_paths stage moves, not only the
|
|
* old version `oldV` (the gated head's): a tentative `*_KEY_VERSION = '<next
|
|
* minor>'` never equals the base version at the slot and is a stamp all the
|
|
* same (#3512). The stage's own bound applies: a tentative mention is newer
|
|
* than the version the branch was written against, so a moved version older
|
|
* than `oldV` (a released key moved by hand) is not a stamp, and neither is
|
|
* one of another width (`tentativeMentions`: a 3-digit dependency pin in a
|
|
* 4-digit repo is never a candidate, so moving it is a hand edit). A line that
|
|
* moves nothing, or changes anything beyond those mentions, is not one either.
|
|
* With `side`, a removed line the branch added itself is bounded by its fork
|
|
* point's version instead, the stage's bound for its own lines (GST-895,
|
|
* GST-963): finish's stampedFiles, the tree receipt and the receipt carry all
|
|
* pass the same side.
|
|
*/
|
|
export function stampOnlyLine(removed: string, added: string, oldV: string, newV: string, templates: readonly string[] = [], side?: BranchSide): boolean {
|
|
if (removed === added) return false;
|
|
return stampOnlyWithin(removed, added, oldV, newV, templates) || (!!side?.added.has(removed) && stampOnlyWithin(removed, added, side.fork, newV, templates));
|
|
}
|
|
|
|
/** Lines the branch added to one file (since its merge base with the base, less the lines the base added since) and its fork point's version. */
|
|
export interface BranchSide { fork: string; added: ReadonlySet<string> }
|
|
|
|
/**
|
|
* Each file's BranchSide for `head` against `baseRef`, read lazily and cached: the lines come from merge-base..`to`
|
|
* (`head` unless a reviewed tree stands in for it), the fork version from `head`'s first-parent history
|
|
* (`fallback` when it has no own commit). A file git cannot diff has no side, so its lines keep the `oldV` bound.
|
|
*/
|
|
export function branchSides(repoRoot: string, baseRef: string, head: string, fallback: string, to = head): (rel: string) => BranchSide | undefined {
|
|
const sides = new Map<string, BranchSide | undefined>();
|
|
let since: string | undefined;
|
|
let fork: string | undefined;
|
|
return rel => {
|
|
if (sides.has(rel)) return sides.get(rel);
|
|
since ??= (spawnSync('git', ['merge-base', baseRef, head], { cwd: repoRoot, encoding: 'utf8', timeout: 60_000 }).stdout ?? '').trim();
|
|
let side: BranchSide | undefined;
|
|
if (since) {
|
|
try {
|
|
const added = branchAddedLines(repoRoot, since, [rel], to, branchAddedLines(repoRoot, since, [rel], baseRef)).get(rel)?.text;
|
|
if (added?.size) side = { fork: (fork ??= forkVersion(repoRoot, baseRef, fallback, head)), added };
|
|
} catch {}
|
|
}
|
|
sides.set(rel, side);
|
|
return side;
|
|
};
|
|
}
|
|
|
|
function stampOnlyWithin(removed: string, added: string, oldV: string, newV: string, templates: readonly string[]): boolean {
|
|
const floor = parseVersion(oldV);
|
|
const width = versionWidth(oldV);
|
|
const after = stampMentions(added, templates);
|
|
const moved = [...stampMentions(removed, templates)].filter(v => v !== newV && !after.has(v));
|
|
if (!moved.length || !floor) return false;
|
|
if (moved.some(v => { const p = parseVersion(v); return !p || versionWidth(v) !== width || cmpVersion(p, floor) < 0; })) return false;
|
|
let text = removed;
|
|
for (const v of moved) text = rewriteStamps(text, v, newV, templates).text;
|
|
return text === added;
|
|
}
|