mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-17 02:15:31 +02:00
fix(wtree): a failed touch falls through to the HEAD seed instead of reopening the racy window (#2687)
The v1.74 racy-git fix carries the real index's mtime onto the temp copy —
but its 'touch -r … || true' meant a FAILED touch silently kept the copy's
fresh stamp, marking every entry non-racy and reopening the exact same-size-
rewrite hole. A failed touch now discards the copy and seeds from read-tree
HEAD (slower; every entry re-hashed; fingerprint stays honest).
Verification for #2687 itself: the reporter's same-size-rewrite repro run 20
iterations against this tree — 0 misses (the underlying race was fixed by
v1.74's b1485d88 with its own regression test; this wave verifies and closes,
it does not claim that fix). Receipt: the stubbed-touch test fails on a
v1.77.0.0 scratch worktree.
Fixes #2687
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
78cd06e157
commit
d4b0131636
+7
-1
@@ -48,7 +48,13 @@ if [ -n "$REAL_INDEX" ] && [ -f "$REAL_INDEX" ] && cp "$REAL_INDEX" "$TMPIDX" 2>
|
|||||||
# non-racy and lets a same-size rewrite in the same second as the original
|
# 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
|
# `git add` keep its stale stat-cache entry — the content change vanishes
|
||||||
# from the fingerprint. touch -r restores the original racy window.
|
# from the fingerprint. touch -r restores the original racy window.
|
||||||
touch -r "$REAL_INDEX" "$TMPIDX" 2>/dev/null || true
|
# #2687 hardening: a FAILED touch silently reopened that exact hole (the
|
||||||
|
# copy keeps its "now" stamp). Fall through to the HEAD seed instead —
|
||||||
|
# slower, but every entry gets re-hashed, so the fingerprint stays honest.
|
||||||
|
if ! touch -r "$REAL_INDEX" "$TMPIDX" 2>/dev/null; then
|
||||||
|
rm -f "$TMPIDX" 2>/dev/null || true
|
||||||
|
git -C "$TOP" read-tree HEAD 2>/dev/null || exit 1
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
git -C "$TOP" read-tree HEAD 2>/dev/null || exit 1
|
git -C "$TOP" read-tree HEAD 2>/dev/null || exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -211,6 +211,40 @@ describe('gstack-wtree', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// #2687 hardening: `touch -r ... || true` meant a FAILED touch silently
|
||||||
|
// reopened the racy-window hole (the temp index copy keeps its "now" stamp
|
||||||
|
// and every entry reads non-racy). A failed touch must fall through to the
|
||||||
|
// read-tree HEAD seed, which re-hashes everything.
|
||||||
|
test('racy-git window stays closed even when touch fails (stubbed-touch fallback)', () => {
|
||||||
|
withScratchRepo((repoDir, _wtree) => {
|
||||||
|
const file = path.join(repoDir, 'a.txt');
|
||||||
|
const indexPath = path.join(repoDir, '.git', 'index');
|
||||||
|
gitIn(repoDir, 'config core.trustctime false');
|
||||||
|
const pinned = new Date('2026-01-01T12:00:00Z');
|
||||||
|
fs.utimesSync(file, pinned, pinned);
|
||||||
|
gitIn(repoDir, 'add a.txt');
|
||||||
|
// PATH-stubbed `touch` that always fails.
|
||||||
|
const stubDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-touch-stub-'));
|
||||||
|
fs.writeFileSync(path.join(stubDir, 'touch'), '#!/bin/sh\nexit 1\n', { mode: 0o755 });
|
||||||
|
const wtreeStubbed = () =>
|
||||||
|
execSync(`${BIN}/gstack-wtree`, {
|
||||||
|
cwd: repoDir,
|
||||||
|
encoding: 'utf-8',
|
||||||
|
timeout: 10000,
|
||||||
|
env: { ...process.env, PATH: `${stubDir}:${process.env.PATH ?? ''}` },
|
||||||
|
}).trim();
|
||||||
|
try {
|
||||||
|
const clean = wtreeStubbed();
|
||||||
|
fs.writeFileSync(file, 'howdy\n'); // same byte length as 'hello\n'
|
||||||
|
fs.utimesSync(file, pinned, pinned);
|
||||||
|
fs.utimesSync(indexPath, pinned, pinned);
|
||||||
|
expect(wtreeStubbed()).not.toBe(clean);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(stubDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('exits non-zero outside a git repo', () => {
|
test('exits non-zero outside a git repo', () => {
|
||||||
const nonGit = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-wtree-nongit-'));
|
const nonGit = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-wtree-nongit-'));
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user