fix(extraction): drop the ./ self-entry from git ls-files --directory (#936) (#941)

When the indexed root is a directory an enclosing git repo ignores,
`git ls-files --directory` collapses the whole cwd to a single literal
`./` entry. That sentinel reached the `ignore` matcher, which rejects it
("path should be a `path.relative()`d string, but got "./""), aborting
buildScopeIgnore — the one ignore-building call in FileWatcher.start().
So the MCP daemon's startWatching() threw, was caught as "Failed to open
project", and auto-sync never started: the index silently went stale
until a manual `codegraph sync` (CODEGRAPH_NO_DAEMON=1 was the only
workaround).

Filter the `./`/`.` self-entry wherever we consume `--directory` output
(listIgnoredDirs + the untracked-dir loop in discoverEmbeddedRepoRoots).
Semantically correct, not just a crash guard: `./` means "the whole cwd",
never a nested repo to recurse into.

Not platform-specific (reported on Codex/Windows, reproduced on macOS):
the trigger is git state, not the OS.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-22 08:13:23 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent e43ac82cdf
commit 03666584ed
3 changed files with 39 additions and 2 deletions
+21
View File
@@ -203,6 +203,27 @@ describe('multi-repo workspaces (#514)', () => {
expect(scope.ignores('src/app.ts')).toBe(false);
});
it('buildScopeIgnore: indexed root is itself a gitignored subdir of an enclosing repo (#936)', () => {
// `child/` is NOT its own repo, so `git` resolves the ENCLOSING repo from
// inside it — and `git ls-files --directory`, whose cwd is then a wholly
// ignored directory, emits the literal `./` ("this entire directory").
// That sentinel used to reach the `ignore` matcher and throw
// ("path should be a `path.relative()`d string, but got "./""), aborting
// buildScopeIgnore → the MCP daemon's watcher never started and auto-sync
// silently stalled until a manual `codegraph sync`.
write(path.join(ws, 'child/src/a.ts'), 'export const x = 1;\n');
write(path.join(ws, '.gitignore'), '/child/\n');
makeRepo(ws);
const child = path.join(ws, 'child');
// The crux: building scope for the ignored subdir must not throw.
const scope = buildScopeIgnore(child);
// The subdir's own source is watchable/indexable, not ignored.
expect(scope.ignores('src/a.ts')).toBe(false);
// And the `./` self entry must not be mistaken for a nested embedded repo.
expect(discoverEmbeddedRepoRoots(child)).toEqual([]);
});
it('sync picks up a change inside a gitignored embedded repo', async () => {
write(path.join(ws, 'packages/proj-a/src/auth.ts'), 'export function login() { return 1; }\n');
makeRepo(path.join(ws, 'packages/proj-a'));