mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-10 15:09:00 +02:00
fix(evidence): carry the real index mtime onto gstack-wtree's temp copy
The stat-cache seed (cp of the real index) stamped the temp index "now", which defeats git's racy-git protection: an entry is only re-hashed when its cached mtime is not older than the index file itself, so a same-size rewrite landing in the same second as the last real index write looked non-racy, kept its stale stat-cache entry, and vanished from the fingerprint — evidence stayed FRESH after a source change. This is the CI flake in test/evidence.test.ts "allow-paths carve-out" (sub-second alignment on fast runners: expected STALE exit 1, got FRESH exit 0). touch -r restores the original index timestamp, reinstating the exact racy window git itself uses. Deterministic regression pin in test/review-log.test.ts reproduces the miss with pinned zero-nsec timestamps (fails on the old script, passes now); receipts: manual probe shows the fresh-stamped copy returning the clean tree for a same-size 'hello'→'howdy' rewrite while the mtime-carried copy detects it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+7
-1
@@ -42,7 +42,13 @@ case "$REAL_INDEX" in
|
||||
*) REAL_INDEX="$TOP/$REAL_INDEX" ;;
|
||||
esac
|
||||
if [ -n "$REAL_INDEX" ] && [ -f "$REAL_INDEX" ] && cp "$REAL_INDEX" "$TMPIDX" 2>/dev/null; then
|
||||
: # stat-cache-preserving seed
|
||||
# Carry the real index's mtime onto the copy. Git's racy-git protection
|
||||
# re-hashes any entry whose cached mtime is not older than the index file
|
||||
# itself; `cp` stamps the copy "now", which silently marks every entry
|
||||
# non-racy and lets a same-size rewrite in the same second as the original
|
||||
# `git add` keep its stale stat-cache entry — the content change vanishes
|
||||
# from the fingerprint. touch -r restores the original racy window.
|
||||
touch -r "$REAL_INDEX" "$TMPIDX" 2>/dev/null || true
|
||||
else
|
||||
git -C "$TOP" read-tree HEAD 2>/dev/null || exit 1
|
||||
fi
|
||||
|
||||
@@ -182,6 +182,35 @@ describe('gstack-wtree', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('racy-git window: a same-size rewrite pinned to the index timestamp changes the fingerprint', () => {
|
||||
withScratchRepo((repoDir, wtree) => {
|
||||
const file = path.join(repoDir, 'a.txt');
|
||||
const indexPath = path.join(repoDir, '.git', 'index');
|
||||
// ctime can't be restored after a rewrite; production hits this window
|
||||
// when everything lands in the same second (ctime SECONDS match).
|
||||
// trustctime=false isolates the racy mechanism deterministically
|
||||
// instead of racing a second boundary.
|
||||
gitIn(repoDir, 'config core.trustctime false');
|
||||
// Pin the cached entry's mtime to a fixed timestamp (zero nsec, so the
|
||||
// restore below is exact even on USE_NSEC git builds).
|
||||
const pinned = new Date('2026-01-01T12:00:00Z');
|
||||
fs.utimesSync(file, pinned, pinned);
|
||||
gitIn(repoDir, 'add a.txt');
|
||||
const clean = wtree();
|
||||
// Same-size rewrite restored to the pinned stat, with the index file
|
||||
// itself pinned to the SAME timestamp: the entry is stat-identical to
|
||||
// its stale cache and sits exactly on git's racy-git boundary.
|
||||
// gstack-wtree must carry the real index's mtime onto its temp copy —
|
||||
// a fresh-stamped copy marks the entry non-racy, trusts the stale stat
|
||||
// cache, and the edit vanishes from the fingerprint (evidence would
|
||||
// stay FRESH after a source change).
|
||||
fs.writeFileSync(file, 'howdy\n'); // same byte length as 'hello\n'
|
||||
fs.utimesSync(file, pinned, pinned);
|
||||
fs.utimesSync(indexPath, pinned, pinned);
|
||||
expect(wtree()).not.toBe(clean);
|
||||
});
|
||||
});
|
||||
|
||||
test('exits non-zero outside a git repo', () => {
|
||||
const nonGit = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-wtree-nongit-'));
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user