fix(index): skip nested git worktrees instead of indexing them as duplicate repos (#848) (#868)

A git worktree nested in a project (e.g. Claude Code's gitignored
`.claude/worktrees/<name>/`) was swept into the index as an embedded repo: its
`.git` is a FILE pointing into the host repo's `.git/worktrees/`, and embedded-
repo discovery treated any `.git` (file or directory) as a distinct repo to
index. Each worktree then duplicated the entire graph — one report went from
~1,850 files to 24,533, with search/explore flooded by stale copies.

classifyGitDir() now distinguishes:
- `.git` directory       -> embedded clone, index (#193/#514/#622, unchanged)
- `.git` file → worktrees/ -> worktree, skip (#848)
- `.git` file → modules/   -> submodule, index (unchanged)

Applied at both embedded-repo entry points: findNestedGitRepos discovery (which
also covers the sync/change-detection path) and the untracked-subdir recursion
in collectGitFiles.

Verified: the reproduction drops from 6 files / betaHelper×3 to 3 files / ×1,
with a genuine embedded clone and submodules still indexed. Regression test added.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-13 16:01:00 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 64ff7597d0
commit 5cc155ddc4
3 changed files with 65 additions and 2 deletions
+23
View File
@@ -108,6 +108,29 @@ describe('multi-repo workspaces (#514)', () => {
expect(files).toContain('main.ts');
});
it('skips nested git worktrees instead of indexing them as duplicate embedded repos (#848)', () => {
// Claude Code (and others) create worktrees under a gitignored path like
// `.claude/worktrees/<name>/`. A worktree's `.git` is a FILE pointing into
// the host repo's own `.git/worktrees/`, so it is the SAME repo already
// indexed — sweeping it in as an embedded repo multiplies the whole graph.
// A genuine embedded clone (a `.git` *directory*) must still be indexed.
write(path.join(ws, 'src/app.ts'), 'export function app() { return 1; }\n');
write(path.join(ws, '.gitignore'), '.claude/\nvendored/\n');
makeRepo(ws);
// A real linked worktree under the gitignored .claude/worktrees/.
git(ws, 'worktree', 'add', '-q', '.claude/worktrees/feature', '-b', 'feature');
// A genuine embedded clone, also gitignored — must STAY indexed (#514).
write(path.join(ws, 'vendored/lib.ts'), 'export function vendoredFn() { return 9; }\n');
makeRepo(path.join(ws, 'vendored'));
const files = scanDirectory(ws);
expect(files).toContain('src/app.ts');
// The worktree is a duplicate working view — never indexed.
expect(files.some((f) => f.includes('.claude/worktrees'))).toBe(false);
// The genuine embedded clone is still indexed (#514/#622 preserved).
expect(files).toContain('vendored/lib.ts');
});
it('non-git workspace: walks children and respects each child own .gitignore', () => {
write(path.join(ws, 'proj-a/src/auth.ts'), 'export function login() {}\n');
write(path.join(ws, 'proj-a/build/out.ts'), 'export function generated() {}\n');