mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-09 18:28:15 +01:00
* fix(stamp): name branch lines by position; carry and finish accept a kept VERSION claim (GST-979, GST-980) BranchSide carries the positions of the branch-added lines and stampOnlyLine takes the removed line position, so a base line whose text duplicates a branch line keeps the old-version floor. At equal versions only the branch own lines may move. stampOnlyText and stampedFiles accept a kept claim for those lines, and the carry leaves a stamp file out of the product comparison when its only change since the reviewed head is a stamp on the branch own rows and the base did not touch it. Failing-first: all five new tests fail on mainfc292e22f. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (GST-980 carry reads the stamp file line by line at the branch's positions, and keeps its mode) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(changelog): GST-979/980 entry counts the seven tests Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: the receipt carry follows a stamp on the branch's own rows (GST-980) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: keep docs/ship-policy.md at the reviewed text; its GST-980 sentence already states the carry rule Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Revert "docs: keep docs/ship-policy.md at the reviewed text; its GST-980 sentence already states the carry rule" This reverts commitf926ccb0c2. * fix(stamp): a declared placeholder restamped on the branch's own row is a stamp; the carry names HEAD's version (GST-1026, GST-1030) stamp_placeholders (default []) lets a repo declare literals such as TBD that its restamp replaces with the new version inside a stamp_patterns template. stampOnlyLine accepts that move only on a branch line named by position; finish's stampedFiles, the receipt carry and the tree receipt share it. The carried receipt's version is HEAD's, read the way write reads it. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (carry keeps a write --version receipt's version without a version source; a placeholder fill composes with a stamp move; a bare {version} template and a blank placeholder take no placeholder) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(changelog): GST-1026/1030 fragment names the review fixes Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(ship-receipt): carry masks only package.json version on a pin-bump PR (GST-1030, GBEV-3) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * refactor(ship-policy): stamp key validation moves to lib/ship-policy-stamps.ts for the module-size ratchet Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(ship-policy): stamp_placeholders key names the next minor 1.95.0.0 1.94.0.0 shipped as #3901 without the key, so the released-key test reds once main's CHANGELOG is in the tree. The slot stamp moves the tentative value through stamp_paths. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(restamp-mentions): set the stamp file's exec bit through the index so the mode-change carry case holds on Windows Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * unstamp: return VERSION, package.json, CHANGELOG and the digest to main f6d6215's values and restore the changelog fragment after #3892 left train 100 The tentative STAMP_PLACEHOLDERS_KEY_VERSION goes back to 1.95.0.0 for the next slot stamp to move. 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>
102 lines
5.9 KiB
TypeScript
102 lines
5.9 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, escapeRegExp, forkVersion, rewriteStamps, stampMentions } from './restamp';
|
|
import { VERSION_SLOT } from './ship-policy';
|
|
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, named by its 1-based
|
|
* position `at` in the side's file (GST-979: a base line with the same text
|
|
* is not the branch's), 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. When the
|
|
* version does not move (a slot that keeps its claimed VERSION), only such a
|
|
* branch line may change: the stage moves nothing else then. On such a branch
|
|
* line a declared placeholder (`stamp_placeholders`, GST-1026: gbrain's
|
|
* `since: 'TBD'`) may also become the new version, but only inside a
|
|
* `stamp_patterns` template with literal text around its slot, never a built-in
|
|
* form: with at least one such rewrite, the line must then be the added line,
|
|
* or one whose remaining stamp-form moves pass the fork-version bound.
|
|
*/
|
|
export function stampOnlyLine(removed: string, added: string, oldV: string, newV: string, templates: readonly string[] = [], side?: BranchSide, at?: number, placeholders: readonly string[] = []): boolean {
|
|
if (removed === added) return false;
|
|
if (oldV !== newV && stampOnlyWithin(removed, added, oldV, newV, templates)) return true;
|
|
if (!side || at === undefined || !side.at.has(at) || !side.added.has(removed)) return false;
|
|
if (stampOnlyWithin(removed, added, side.fork, newV, templates)) return true;
|
|
const filled = fillPlaceholders(removed, newV, templates, placeholders);
|
|
return filled.count > 0 && (filled.text === added || stampOnlyWithin(filled.text, 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): their
|
|
* text and their 1-based positions in the side's file, and its fork point's version.
|
|
*/
|
|
export interface BranchSide { fork: string; added: ReadonlySet<string>; at: ReadonlySet<number> }
|
|
|
|
/**
|
|
* 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 own = branchAddedLines(repoRoot, since, [rel], to, branchAddedLines(repoRoot, since, [rel], baseRef)).get(rel);
|
|
if (own?.text.size) side = { fork: (fork ??= forkVersion(repoRoot, baseRef, fallback, head)), added: own.text, at: own.at };
|
|
} catch {}
|
|
}
|
|
sides.set(rel, side);
|
|
return side;
|
|
};
|
|
}
|
|
|
|
/** Each declared placeholder rewritten to `newV` inside each template with literal text around its slot (a bare edge at a word boundary). */
|
|
function fillPlaceholders(removed: string, newV: string, templates: readonly string[], placeholders: readonly string[]): { text: string; count: number } {
|
|
let text = removed;
|
|
let count = 0;
|
|
for (const t of templates) {
|
|
const [pre, post] = t.split(VERSION_SLOT) as [string, string];
|
|
if (!pre && !post) continue;
|
|
for (const p of placeholders) {
|
|
const re = new RegExp(`${escapeRegExp(pre)}${pre ? '' : '(?<![A-Za-z0-9_.])'}${escapeRegExp(p)}${post ? '' : '(?![A-Za-z0-9_.])'}${escapeRegExp(post)}`, 'g');
|
|
text = text.replace(re, () => { count++; return pre + newV + post; });
|
|
}
|
|
}
|
|
return { text, count };
|
|
}
|
|
|
|
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;
|
|
}
|