Files
gstack/lib/history-read.ts
Garry Tanandcapy-ai[bot] c03f185d15 v1.92.21.0 ci: blobless clones on the PR and train path, fail-loud history reads (GSTA-203 PR 5) (#3713)
* ci: blobless clones on the PR and train path, fail-loud history reads (GSTA-203 PR 5)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* changelog: GSTA-203 PR 5 fragment

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: tree-only merge-base diff in the Version Gate; ls-tree --full-tree in both history readers

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* ci: keep the route and backstop policy reads inline (git and jq only); docs follow

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: base CHANGELOG read in readFragments fails loudly; history readers try git show first

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: HISTORY_READ_FAILED from gstack-restamp and ship-queue land

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>
2026-11-01 09:19:25 +00:00

44 lines
2.5 KiB
TypeScript

/**
* Read one file at a commit when the checkout may be a blobless clone
* (`filter: blob:none`), where git fetches an old file's contents on demand
* and that fetch can fail. The TypeScript twin of
* .github/scripts/history-read.sh. A read that `git show` answers is the
* file. When it fails, existence comes from the ref's tree (`git ls-tree
* --full-tree`, local data only): a path the tree does not list is absent, and
* a listed one is retried, 3 tries in all in a partial clone (only a fetch can
* recover) and none otherwise, waiting HISTORY_READ_BACKOFF_SECONDS (default 2)
* times the attempt between tries. Every other failure throws
* HISTORY_READ_FAILED instead of reading as absent
* (docs/troubleshooting.md#history-read-failed).
*/
import { spawnSync } from 'node:child_process';
export const HISTORY_READ_TRIES = 3;
export class HistoryReadError extends Error {
readonly code = 'HISTORY_READ_FAILED';
}
/** The file's text at `ref`, or null when the ref's tree does not list it. */
export function readHistoryFile(repoRoot: string, ref: string, file: string): string | null {
const git = (args: string[]) => spawnSync('git', args, { cwd: repoRoot, encoding: 'utf8', timeout: 60_000, maxBuffer: 64 * 1024 * 1024 });
const fail = (cause: string) => {
const version = git(['--version']).stdout?.trim() || 'git version unknown';
return new HistoryReadError(`cannot read ${file} at ${ref}. ${cause} (${version}). Fix: see docs/troubleshooting.md#history-read-failed (HISTORY_READ_FAILED)`);
};
const firstLine = (r: ReturnType<typeof git>) => (r.stderr ?? '').split('\n')[0] || r.error?.message || `exit ${r.status}`;
let shown = git(['show', `${ref}:${file}`]);
if (shown.status === 0) return shown.stdout;
const listed = git(['ls-tree', '--full-tree', ref, '--', file]);
if (listed.status !== 0) throw fail(`git ls-tree failed: ${firstLine(listed)}`);
if (!listed.stdout.trim()) return null;
const promisors = (git(['config', '--get-regexp', '^(remote\\..+\\.promisor|extensions\\.partialclone)$']).stdout ?? '').split('\n');
const tries = promisors.some(line => line && !/ false$/i.test(line)) ? HISTORY_READ_TRIES : 1;
for (let attempt = 1; attempt < tries; attempt++) {
Bun.sleepSync(attempt * 1000 * Number(process.env.HISTORY_READ_BACKOFF_SECONDS ?? 2));
shown = git(['show', `${ref}:${file}`]);
if (shown.status === 0) return shown.stdout;
}
throw fail(`git show failed ${tries === 1 ? 'once (not a partial clone, so no retry)' : `${tries} times`}: ${firstLine(shown)}`);
}