fix(park): read HEAD membership once and move nothing when it cannot be read
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5ddbcd3de6
commit
bf4736a254
4 changed files with 190 additions and 15 deletions
|
|
@ -6,10 +6,11 @@
|
|||
import { after, test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { mkdtempSync, writeFileSync, readFileSync, existsSync, mkdirSync, rmSync, symlinkSync, cpSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { join, dirname, delimiter } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { childEnv } from '../helpers/child-env.mjs';
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||
const CLI = join(ROOT, 'lib/util/park-attempt-files.mjs');
|
||||
|
|
@ -369,3 +370,121 @@ test('[F4a] one index entry that cannot be removed exits 2 with every entry stil
|
|||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// --- fix round 4-5b: M7 ----------------------------------------------------------
|
||||
// [M7] HEAD membership was one `git cat-file -e HEAD:<path>` per path, and any
|
||||
// failure of it read as "not in HEAD": a git that could not read HEAD unindexed
|
||||
// and moved the attempt's files. A path in HEAD but gone from the index was
|
||||
// moved like an untracked file. Both now exit 2 before anything changes.
|
||||
|
||||
test('[M7] a failing HEAD read exits 2 and moves nothing', async () => {
|
||||
const { pathWith } = await import('../helpers/bin-path.mjs');
|
||||
const real = pathWith(['git']);
|
||||
const fake = mkdtempSync(join(tmpdir(), 'voyage-fake-git-'));
|
||||
const dir = repo();
|
||||
try {
|
||||
const realGit = join(real.path, 'git');
|
||||
assert.ok(!realGit.includes("'"), 'control: the real git path carries no quote');
|
||||
writeFileSync(join(fake, 'git'), [
|
||||
'#!/bin/sh',
|
||||
'for a in "$@"; do',
|
||||
' if [ "$a" = ls-tree ]; then echo "fatal: ls-tree failed (fake git)" >&2; exit 128; fi',
|
||||
'done',
|
||||
`exec '${realGit}' "$@"`,
|
||||
'',
|
||||
].join('\n'), { mode: 0o755 });
|
||||
attemptFiles(dir);
|
||||
git(dir, 'add', '--intent-to-add', '--', 'tests/batch.sh', 'tests/cli-stdin.test.mjs');
|
||||
const env = childEnv({ PATH: `${fake}${delimiter}${process.env.PATH ?? ''}` });
|
||||
const ctl = spawnSync('git', ['ls-tree', 'HEAD'], { cwd: dir, encoding: 'utf8', env });
|
||||
assert.equal(ctl.status, 128, `control: the fake git must fail ls-tree: ${ctl.stderr}`);
|
||||
assert.equal(spawnSync('git', ['rev-parse', 'HEAD'], { cwd: dir, encoding: 'utf8', env }).status, 0, 'control: the fake git runs every other call');
|
||||
writeFileSync(join(dir, '.git', 'upward.diff'), DIFF);
|
||||
const r = spawnSync(process.execPath, [CLI, '--diff', join(dir, '.git', 'upward.diff'), '--step', '1', '--attempt', '1', '--cwd', dir], { encoding: 'utf8', env });
|
||||
assert.equal(r.status, 2, `a HEAD read that failed must be exit 2: ${r.stdout}${r.stderr}`);
|
||||
assert.match(r.stderr, /ls-tree/, 'the message does not name the git call that failed');
|
||||
const listed = git(dir, 'ls-files').split('\n');
|
||||
assert.ok(listed.includes('tests/batch.sh'), 'the intent-to-add entry was taken out of the index although HEAD could not be read');
|
||||
assert.ok(listed.includes('tests/cli-stdin.test.mjs'), 'the second intent-to-add entry was taken out of the index');
|
||||
assert.equal(readFileSync(join(dir, 'tests/batch.sh'), 'utf8'), 'stale | cat\n', 'the file was moved although HEAD could not be read');
|
||||
assert.equal(existsSync(join(dir, 'tests/cli-stdin.test.mjs')), true, 'the second file was moved');
|
||||
assert.equal(existsSync(parkedPath(dir, 'tests/batch.sh')), false, 'a file was parked although HEAD could not be read');
|
||||
} finally {
|
||||
real.cleanup();
|
||||
rmSync(fake, { recursive: true, force: true });
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('[M7] a HEAD path missing from the index exits 2 and moves nothing', () => {
|
||||
const dir = repo();
|
||||
try {
|
||||
attemptFiles(dir);
|
||||
git(dir, 'add', '--', 'tests/batch.sh');
|
||||
git(dir, 'commit', '-q', '-m', 'operator');
|
||||
git(dir, 'rm', '--cached', '-q', '--', 'tests/batch.sh');
|
||||
assert.doesNotMatch(git(dir, 'ls-files'), /tests\/batch\.sh/, 'control: the committed file is out of the index');
|
||||
assert.match(git(dir, 'ls-tree', '-r', '--name-only', 'HEAD'), /tests\/batch\.sh/, 'control: the file is in HEAD');
|
||||
const r = run(dir);
|
||||
assert.equal(r.status, 2, `a HEAD path missing from the index must be exit 2: ${r.stdout}${r.stderr}`);
|
||||
assert.match(r.stderr, /tests\/batch\.sh/, 'the message does not name the path');
|
||||
assert.equal(readFileSync(join(dir, 'tests/batch.sh'), 'utf8'), 'stale | cat\n', 'the committed file was moved');
|
||||
assert.equal(existsSync(join(dir, 'tests/cli-stdin.test.mjs')), true, 'the untracked file was moved although the run could not finish');
|
||||
assert.equal(existsSync(parkedPath(dir, 'tests/batch.sh')), false, 'the committed file was parked');
|
||||
assert.equal(existsSync(parkedPath(dir, 'tests/cli-stdin.test.mjs')), false, 'a file was parked although the run could not finish');
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
/** The CLI run on `diff` written into the repo's git dir, its child given childEnv(). */
|
||||
function runIsolated(dir, diff = DIFF) {
|
||||
writeFileSync(join(dir, '.git', 'upward.diff'), diff);
|
||||
return spawnSync(process.execPath, [CLI, '--diff', join(dir, '.git', 'upward.diff'), '--step', '1', '--attempt', '1', '--cwd', dir], { encoding: 'utf8', env: childEnv() });
|
||||
}
|
||||
|
||||
test('[M7] a corrupt branch ref is a failed HEAD read: exit 2, nothing unindexed or moved', () => {
|
||||
const dir = repo();
|
||||
try {
|
||||
attemptFiles(dir);
|
||||
git(dir, 'add', '--', 'tests/batch.sh');
|
||||
git(dir, 'commit', '-q', '-m', 'operator');
|
||||
const ref = git(dir, 'symbolic-ref', '-q', 'HEAD').trim();
|
||||
writeFileSync(join(dir, '.git', ref), 'garbage\n');
|
||||
const ctl = spawnSync('git', ['rev-parse', '--verify', '--quiet', 'HEAD'], { cwd: dir, encoding: 'utf8', env: childEnv() });
|
||||
assert.equal(ctl.status, 1, `control: the corrupt ref reads like an unborn HEAD (exit 1): ${ctl.stderr}`);
|
||||
assert.equal(ctl.stdout.trim(), '', 'control: and prints nothing');
|
||||
const r = runIsolated(dir);
|
||||
assert.equal(r.status, 2, `a corrupt branch ref must be exit 2: ${r.stdout}${r.stderr}`);
|
||||
const listed = git(dir, 'ls-files').split('\n');
|
||||
assert.ok(listed.includes('tests/batch.sh'), 'the committed file was taken out of the index although HEAD could not be read');
|
||||
assert.equal(readFileSync(join(dir, 'tests/batch.sh'), 'utf8'), 'stale | cat\n', 'the committed file was moved');
|
||||
assert.equal(existsSync(join(dir, 'tests/cli-stdin.test.mjs')), true, 'the untracked file was moved although HEAD could not be read');
|
||||
assert.equal(existsSync(parkedPath(dir, 'tests/batch.sh')), false, 'the committed file was parked');
|
||||
assert.equal(existsSync(parkedPath(dir, 'tests/cli-stdin.test.mjs')), false, 'a file was parked although HEAD could not be read');
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('[M7] an unborn HEAD parks the attempt\'s files', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'voyage-park-unborn-'));
|
||||
try {
|
||||
git(dir, 'init', '-q');
|
||||
attemptFiles(dir);
|
||||
git(dir, 'add', '--intent-to-add', '--', 'tests/batch.sh');
|
||||
assert.match(git(dir, 'ls-files'), /tests\/batch\.sh/, 'control: the intent-to-add file is in the index');
|
||||
const ctl = spawnSync('git', ['rev-parse', '--verify', '--quiet', 'HEAD'], { cwd: dir, encoding: 'utf8', env: childEnv() });
|
||||
assert.equal(ctl.status, 1, 'control: HEAD is unborn');
|
||||
const r = runIsolated(dir);
|
||||
assert.equal(r.status, 0, `an unborn HEAD must park: ${r.stdout}${r.stderr}`);
|
||||
assert.deepEqual(JSON.parse(r.stdout).parked, ['tests/batch.sh', 'tests/cli-stdin.test.mjs']);
|
||||
assert.doesNotMatch(git(dir, 'ls-files'), /tests\/batch\.sh/, 'the intent-to-add entry is still in the index');
|
||||
assert.equal(existsSync(join(dir, 'tests/batch.sh')), false, 'the intent-to-add file is still in the work tree');
|
||||
assert.equal(existsSync(join(dir, 'tests/cli-stdin.test.mjs')), false, 'the untracked file is still in the work tree');
|
||||
assert.equal(readFileSync(parkedPath(dir, 'tests/batch.sh'), 'utf8'), 'stale | cat\n', 'the file is not under the parked directory');
|
||||
assert.equal(readFileSync(parkedPath(dir, 'tests/cli-stdin.test.mjs'), 'utf8'), 'stale test\n');
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue