CB-634: write the overlay exclude to the common git dir, not the per-worktree gitdir
git reads info/exclude from the common dir for a linked worktree (only info/sparse-checkout is per-worktree), so the entry written into <common>/worktrees/<name>/info/exclude was never honoured and CLAUDE.local.md showed as untracked -- at risk of being swept into a worker's PR. Derive the common dir (<common>/worktrees/<name> -> <common>) and write there. Found by dogfooding a real spawn on fleet01; the test now uses the real worktree layout and asserts the entry lands in the common dir, not the per-worktree gitdir.
This commit is contained in:
@@ -356,7 +356,8 @@ public final class ClaudeCodeLauncher extends HerdrPeerLauncher {
|
||||
/**
|
||||
* Deliver the shared IDE guidance ({@link PeerLauncher#ideOverlayText}) as an on-disk
|
||||
* {@code CLAUDE.local.md} overlay beside the project's own {@code CLAUDE.md} (CB-634), and
|
||||
* register the overlay in the worktree's {@code info/exclude} so it never shows as untracked.
|
||||
* register the overlay in the repository's common {@code info/exclude} so it never shows as
|
||||
* untracked (git reads a worktree's excludes from the common dir, not the per-worktree gitdir).
|
||||
*
|
||||
* <p><strong>Safety gate:</strong> the overlay is written ONLY when {@code cwd/.git} is a
|
||||
* <em>regular file</em> — a provisioned worktree keeps a {@code .git} FILE holding a
|
||||
@@ -382,7 +383,16 @@ public final class ClaudeCodeLauncher extends HerdrPeerLauncher {
|
||||
if (!gitDir.isAbsolute()) {
|
||||
gitDir = Path.of(cwd).resolve(gitDir).normalize();
|
||||
}
|
||||
Path exclude = gitDir.resolve("info").resolve("exclude");
|
||||
// git reads info/exclude from the COMMON dir, never the per-worktree gitdir (only
|
||||
// info/sparse-checkout is per-worktree). A provisioned worktree's gitdir is
|
||||
// <common>/worktrees/<name>, so the common dir is two levels up; writing the entry into
|
||||
// the per-worktree gitdir leaves it un-honoured and the overlay shows as untracked.
|
||||
Path commonDir = gitDir;
|
||||
if (gitDir.getParent() != null && gitDir.getParent().getFileName() != null
|
||||
&& "worktrees".equals(gitDir.getParent().getFileName().toString())) {
|
||||
commonDir = gitDir.getParent().getParent();
|
||||
}
|
||||
Path exclude = commonDir.resolve("info").resolve("exclude");
|
||||
Files.createDirectories(exclude.getParent());
|
||||
String overlayLine = "CLAUDE.local.md";
|
||||
if (!Files.exists(exclude) || Files.readAllLines(exclude).stream().noneMatch(overlayLine::equals)) {
|
||||
|
||||
@@ -138,14 +138,17 @@ class ClaudeCodeLauncherTest {
|
||||
}
|
||||
|
||||
// CB-634: the IDE guidance is delivered as a CLAUDE.local.md overlay (written only into a
|
||||
// provisioned worktree — cwd with a `.git` FILE) and registered in the worktree's info/exclude.
|
||||
// provisioned worktree — cwd with a `.git` FILE) and registered in the repository's COMMON
|
||||
// info/exclude. git reads a worktree's excludes from the common dir, not the per-worktree
|
||||
// gitdir (only info/sparse-checkout is per-worktree), so a real provisioned layout
|
||||
// <common>/worktrees/<name> must land the entry in <common>/info/exclude.
|
||||
|
||||
@Test
|
||||
void writeIdeOverlayWritesClaudeLocalAndAddsItToInfoExclude(@TempDir Path root) throws Exception {
|
||||
void writeIdeOverlayWritesClaudeLocalAndAddsItToCommonInfoExclude(@TempDir Path root) throws Exception {
|
||||
Path worktree = Files.createDirectory(root.resolve("worktree"));
|
||||
Path gitDir = Files.createDirectory(root.resolve("gitdir"));
|
||||
Files.createDirectories(gitDir.resolve("info"));
|
||||
// A provisioned worktree keeps its gitdir as a `.git` FILE holding a gitdir: <path> pointer.
|
||||
// Real worktree layout: the .git FILE points at <common>/worktrees/<name>.
|
||||
Path commonDir = Files.createDirectory(root.resolve("dotgit"));
|
||||
Path gitDir = Files.createDirectories(commonDir.resolve("worktrees").resolve("wt1"));
|
||||
Files.writeString(worktree.resolve(".git"), "gitdir: " + gitDir);
|
||||
|
||||
FakeHerdr herdr = new FakeHerdr();
|
||||
@@ -156,10 +159,12 @@ class ClaudeCodeLauncherTest {
|
||||
assertTrue(Files.exists(overlay), "the overlay is written beside the project's CLAUDE.md");
|
||||
assertTrue(Files.readString(overlay).contains("project_path: \"" + worktree + "\""),
|
||||
"the overlay pins every ide_* call to the member's own worktree");
|
||||
Path exclude = gitDir.resolve("info").resolve("exclude");
|
||||
assertTrue(Files.exists(exclude), "info/exclude is created from the gitdir pointer");
|
||||
assertTrue(Files.readAllLines(exclude).contains("CLAUDE.local.md"),
|
||||
"the overlay is registered so it never shows as untracked");
|
||||
Path commonExclude = commonDir.resolve("info").resolve("exclude");
|
||||
assertTrue(Files.exists(commonExclude), "info/exclude is created in the COMMON git dir");
|
||||
assertTrue(Files.readAllLines(commonExclude).contains("CLAUDE.local.md"),
|
||||
"the overlay is registered in the common exclude git actually honours for a worktree");
|
||||
assertFalse(Files.exists(gitDir.resolve("info").resolve("exclude")),
|
||||
"the entry must NOT go to the per-worktree gitdir, which git ignores for excludes");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user