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
+17 -2
View File
@@ -256,6 +256,21 @@ function defaultsOnlyIgnore(): Ignore {
return ignore().add(DEFAULT_IGNORE_PATTERNS);
}
/**
* `git ls-files --directory` collapses a wholly-untracked/ignored directory into
* one entry — and when the command's own cwd is such a directory (the indexed
* root is itself a git-ignored subdir of an enclosing repo), git emits the
* literal `./` meaning "this entire directory". That sentinel is not a real
* nested path: feeding it to the `ignore` matcher throws ("path should be a
* `path.relative()`d string, but got "./""), which used to abort `buildScopeIgnore`
* and so break the MCP daemon's watcher/auto-sync on connect; and joining it back
* onto `repoDir` would just re-point at the cwd. Drop it wherever we consume
* `--directory` output. (#936)
*/
function isWholeCwdEntry(entry: string): boolean {
return entry === './' || entry === '.' || entry === '';
}
/**
* List the gitignored DIRECTORIES of a repo (collapsed, trailing-slash form),
* relative to `repoDir`. These are invisible to every other `git ls-files` /
@@ -270,7 +285,7 @@ function listIgnoredDirs(repoDir: string): string[] {
['ls-files', '-z', '-o', '-i', '--exclude-standard', '--directory'],
{ cwd: repoDir, encoding: 'utf-8' as const, timeout: 30000, maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'] as ['pipe', 'pipe', 'pipe'], windowsHide: true }
);
return out.split('\0').filter((e) => e.endsWith('/'));
return out.split('\0').filter((e) => e.endsWith('/') && !isWholeCwdEntry(e));
} catch {
return [];
}
@@ -434,7 +449,7 @@ export function discoverEmbeddedRepoRoots(rootDir: string): string[] {
{ cwd: repoAbs, encoding: 'utf-8', timeout: 30000, maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }
);
for (const e of o.split('\0')) {
if (e.endsWith('/') && !defaults.ignores(e)) {
if (e.endsWith('/') && !isWholeCwdEntry(e) && !defaults.ignores(e)) {
candidates.push(...findNestedGitRepos(path.join(repoAbs, e), e));
}
}