fix(rollup): discover git worktrees - .git tested for existence, not directory-ness
The catalog half of a shared gap that was previously PINNED rather than fixed. This reader of ~/repos tested `.git` with a directory check, so a plain `git worktree add ~/repos/feature-x` produced a depth-1 sibling that can carry its own STATE.md and was dropped SILENTLY - no warning, no count, indistinguishable from a repo with nothing to say. A submodule fails the same way for the same reason. It was pinned, not fixed, because diverging would break the very same-name-from-both-readers invariant discoverRepos exists to hold. ~/repos answered YES to the gated question and reported their half landed. Verified here rather than taken on report: board.sh's Discovery block tests `-e`, and has done since board.sh was introduced (repo-mailbox 61e224c, first released v0.9.0) - so the released reader has no `-d` era at all, and only this side was ever the outlier. The two move together from here - change one, change the other. THE ACCEPTANCE TRAP, MEASURED RATHER THAN ASSUMED. This changes NO number against real ~/repos. Old and new builders were run back to back against the live tree and diffed: stdout and stderr byte-identical, both exit 0. Directly measured why: 0 directories at depth 1 or 2 currently carry .git as a file. A number standing still is not ambiguous here, it is the ONLY possible outcome, and it is why a fixture is not the best way to test this but the only way. FIXTURE GATED BEFORE BEHAVIOUR. The worktree fixture uses a real `git worktree add`, not a hand-written `.git` file - faking it would assert against our guess at git's on-disk format instead of against git. A separate test asserts the fixture itself: .git exists, is a file, is not a directory. Without it, the day git stops writing worktree .git as a file the behaviour tests would go green while measuring nothing, and a green run cannot distinguish that from success. The gate says which one it was, and says the premise of the -e rule is gone rather than inviting a test tweak. Two behaviour tests: a worktree at depth 1, and one under a polyrepo container at depth 2 - the same rule at both depths board.sh walks. Suite 90/90 across the six files (rollup 31 -> 34). check-versions 11 OK, 0 WARN, 0 ERROR. Real run unchanged at 8 repos, 17 markers, Output B 0 lines, 1 V6 warning, exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KSX2v1m7Fz22BKrGJZuUpQ
This commit is contained in:
parent
e57d2e349f
commit
2efe98ba02
2 changed files with 91 additions and 6 deletions
|
|
@ -207,13 +207,20 @@ export function buildRegister({ repos }) {
|
|||
//
|
||||
// Not descending into a git repo also makes the known nested-name trap
|
||||
// (claude-code-100x/claude-code-100x, both git repos) structurally unreachable rather than
|
||||
// merely absent. SHARED, DELIBERATE GAP: like board.sh, "is a repo" means `.git` is a
|
||||
// DIRECTORY, so a git worktree or submodule (where `.git` is a file) is not seen by either
|
||||
// reader. Pinned here rather than fixed one-sidedly — diverging would break the very
|
||||
// same-name-from-both-readers invariant this function exists to hold.
|
||||
// merely absent.
|
||||
//
|
||||
// "Is a repo" tests `.git` for EXISTENCE, not directory-ness: a worktree or submodule has
|
||||
// `.git` as a FILE. A plain `git worktree add ~/repos/feature-x` lands a depth-1 sibling
|
||||
// that can carry its own STATE.md, and a directory test dropped it SILENTLY — no warning,
|
||||
// no count, indistinguishable from a repo with nothing to say. This was pinned rather than
|
||||
// fixed one-sidedly because diverging would break the very same-name-from-both-readers
|
||||
// invariant this function exists to hold. board.sh's side is in place — MEASURED, not
|
||||
// reported: its Discovery block tests `-e`, and has since board.sh was introduced
|
||||
// (repo-mailbox 61e224c, first released v0.9.0). This is the other half. The two move
|
||||
// together or not at all — change one, change the other.
|
||||
function isGitRepo(dir) {
|
||||
const dotGit = join(dir, '.git');
|
||||
return existsSync(dotGit) && statSync(dotGit).isDirectory();
|
||||
return existsSync(dotGit);
|
||||
}
|
||||
|
||||
function subdirs(dir) {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@
|
|||
// and to make the V4 "no em-dash in carrier" axis unambiguous.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, statSync, existsSync } from 'node:fs';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import {
|
||||
|
|
@ -350,3 +351,80 @@ test('D1: a basename collision fails LOUDLY instead of silently overwriting', ()
|
|||
'two STATE.md files resolving to one name must stop the build',
|
||||
);
|
||||
});
|
||||
|
||||
// --- D1 discovery: git worktrees, where .git is a FILE and not a directory -------------
|
||||
// Closes a gap that was previously PINNED rather than fixed: both readers tested `.git`
|
||||
// with a directory check, so `git worktree add ~/repos/feature-x` produced a depth-1
|
||||
// sibling that can carry its own STATE.md and was dropped SILENTLY here. board.sh's
|
||||
// Discovery block tests `-e` (measured: repo-mailbox 61e224c, first released v0.9.0); this
|
||||
// is the catalog half. Diverging would break the one invariant the mirroring exists to
|
||||
// hold, so the two move together or not at all.
|
||||
//
|
||||
// This cannot be faked with mkdirSync: a fixture that writes `.git` as a file by hand
|
||||
// would assert against our own guess at git's on-disk format rather than against git.
|
||||
// Hence a REAL `git worktree add` -- and hence the fixture gate below.
|
||||
function gitWorktreeTree() {
|
||||
const root = mkdtempSync(join(tmpdir(), 'rollup-worktree-'));
|
||||
const main = join(root, 'main');
|
||||
mkdirSync(main, { recursive: true });
|
||||
const git = (args, cwd) => execFileSync('git', args, { cwd, stdio: 'pipe' });
|
||||
git(['init', '-q', '-b', 'main'], main);
|
||||
git(['config', 'user.email', 'test@example.invalid'], main);
|
||||
git(['config', 'user.name', 'test'], main);
|
||||
writeFileSync(join(main, 'seed.txt'), 'seed\n');
|
||||
git(['add', 'seed.txt'], main);
|
||||
git(['commit', '-q', '-m', 'seed'], main);
|
||||
// A worktree needs a commit to attach to, which is why the seed above exists.
|
||||
const worktree = join(root, 'feature-x');
|
||||
git(['worktree', 'add', '-q', '-b', 'feature-x', worktree], main);
|
||||
writeFileSync(join(main, 'STATE.md'), MARKER);
|
||||
writeFileSync(join(worktree, 'STATE.md'), MARKER);
|
||||
return { root, main, worktree };
|
||||
}
|
||||
|
||||
test('D1 fixture gate: git really does write a worktree .git as a FILE', () => {
|
||||
// Gate the fixture BEFORE gating the behaviour. Without this, the day git starts
|
||||
// writing worktree `.git` as a directory, the test below would go green while
|
||||
// measuring nothing -- passing for the wrong reason is the failure mode that a
|
||||
// green run cannot distinguish from success. This assertion says which one it was.
|
||||
const { worktree } = gitWorktreeTree();
|
||||
const dotGit = join(worktree, '.git');
|
||||
assert.ok(existsSync(dotGit), 'a worktree must carry a .git entry at all');
|
||||
assert.ok(
|
||||
statSync(dotGit).isFile(),
|
||||
'git no longer writes a worktree .git as a file -- the premise of the -e rule is gone, '
|
||||
+ 'and both this builder and board.sh need re-deciding, not a test tweak',
|
||||
);
|
||||
assert.ok(!statSync(dotGit).isDirectory(), 'a file and a directory must not both be true');
|
||||
});
|
||||
|
||||
test('D1: a git worktree at depth 1 is discovered (.git is a file, not a directory)', () => {
|
||||
const { root } = gitWorktreeTree();
|
||||
const repos = discoverRepos(root);
|
||||
assert.deepEqual(
|
||||
repos.map((r) => r.name).sort(),
|
||||
['feature-x', 'main'],
|
||||
'the worktree carries its own STATE.md and must not be dropped for having a .git FILE',
|
||||
);
|
||||
});
|
||||
|
||||
test('D1: a git worktree under a polyrepo container (depth 2) is discovered', () => {
|
||||
// Same rule at the depth board.sh also walks: a container is not itself a repo, so a
|
||||
// worktree parked inside one is reached by the same child pass as an ordinary repo.
|
||||
const { root, main, worktree } = gitWorktreeTree();
|
||||
const outer = mkdtempSync(join(tmpdir(), 'rollup-worktree-outer-'));
|
||||
const container = join(outer, 'container');
|
||||
mkdirSync(container, { recursive: true });
|
||||
execFileSync('git', ['worktree', 'add', '-q', '-b', 'nested-x', join(container, 'nested-x')], {
|
||||
cwd: main,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
writeFileSync(join(container, 'nested-x', 'STATE.md'), MARKER);
|
||||
assert.ok(statSync(join(container, 'nested-x', '.git')).isFile(), 'fixture gate: .git is a file');
|
||||
assert.deepEqual(
|
||||
discoverRepos(outer).map((r) => r.name),
|
||||
['nested-x'],
|
||||
'a container holding only a worktree still contributes that worktree',
|
||||
);
|
||||
assert.ok(existsSync(join(root, 'main')) && existsSync(worktree), 'fixture roots intact');
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue