fix(scan): includeIgnored child patterns revive repos under a gitignored parent (#1318)
`.gitignore: /repos/` lists `repos/` as ONE ignored entry, while the CLI hint (#1156) suggests `includeIgnored: ["repos/a/", "repos/b/"]` — the child spelling. findIgnoredEmbeddedRepos tested the opt-in matcher against the PARENT path only, which a child pattern never matches, so the documented opt-in silently indexed nothing and init looped the byte-identical suggestion back at the user (#1295). Ignored dirs that don't match as a whole are now descended (the walk was already bounded: depth 4 / 2000 entries, and only runs when includeIgnored is configured) and each nested repo root is matched individually — parent spelling opts in everything under the dir, child spelling exactly the named repos. findUnindexedIgnoredRepos gets the same per-repo check so the hint stops nagging about repos that are already configured while still naming unopted siblings. Fixes #1295 Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
c472cfb52e
commit
a5a8942d1c
@@ -18,6 +18,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
|
- `codegraph.json`'s `includeIgnored` works again for the "folder of repos" layout: when one `.gitignore` rule covers a parent directory (`/repos/`) holding several embedded git repositories, opting in the individual repos (`"includeIgnored": ["repos/a/"]` — the exact spelling `codegraph init`'s own hint suggests) previously matched nothing and indexed zero files, looping the same suggestion back at you. Both spellings now work — name the parent directory to opt in everything under it, or name individual repos to opt in just those — and the hint no longer re-suggests repos that are already configured. (#1295)
|
||||||
- Method calls on literals (`", ".join(...)` in Python, `"x".split(...)` in JavaScript, and the like) no longer produce call edges to unrelated project functions that happen to share the builtin's name — a codebase with a function called `join`, `get`, or `update` could show phantom callers from every string-builtin use. Additionally, a function nested inside another function is now only matched as a call target from inside its container, since it isn't reachable from anywhere else. Blast-radius and affected-test results get cleaner on Python and JavaScript codebases especially. (#1230)
|
- Method calls on literals (`", ".join(...)` in Python, `"x".split(...)` in JavaScript, and the like) no longer produce call edges to unrelated project functions that happen to share the builtin's name — a codebase with a function called `join`, `get`, or `update` could show phantom callers from every string-builtin use. Additionally, a function nested inside another function is now only matched as a call target from inside its container, since it isn't reachable from anywhere else. Blast-radius and affected-test results get cleaner on Python and JavaScript codebases especially. (#1230)
|
||||||
- Go method calls through a struct field (`target.conn.Exec(...)`) no longer bind to unrelated same-named local methods when the field's type is external — `conn *sql.DB` calls were being attributed to a local interface that happened to declare `Exec`, fabricating internal dependencies. Chained field calls now resolve by inferring the field's declared type from the struct definition: in-project types (including unexported ones like chi's `tree *node`) gain correct, validated call edges that never existed before, and external types (standard library, third-party modules) are left unlinked instead of guessed. (#1276)
|
- Go method calls through a struct field (`target.conn.Exec(...)`) no longer bind to unrelated same-named local methods when the field's type is external — `conn *sql.DB` calls were being attributed to a local interface that happened to declare `Exec`, fabricating internal dependencies. Chained field calls now resolve by inferring the field's declared type from the struct definition: in-project types (including unexported ones like chi's `tree *node`) gain correct, validated call edges that never existed before, and external types (standard library, third-party modules) are left unlinked instead of guessed. (#1276)
|
||||||
- TypeScript/JavaScript method calls through an imported singleton (`import { store } from './store'; store.notify()`) now resolve to the class method instead of the exported constant, so `codegraph callers` sees cross-file callers of the method — previously only same-file calls were attributed and a method used everywhere could look unused. The same declaration-based type inference applies across the languages that share it (Python, Java, Kotlin, Go, and more), and a failed inference keeps the old edge rather than guessing. (#1292)
|
- TypeScript/JavaScript method calls through an imported singleton (`import { store } from './store'; store.notify()`) now resolve to the class method instead of the exported constant, so `codegraph callers` sees cross-file callers of the method — previously only same-file calls were attributed and a method used everywhere could look unused. The same declaration-based type inference applies across the languages that share it (Python, Java, Kotlin, Go, and more), and a failed inference keeps the old edge rather than guessing. (#1292)
|
||||||
|
|||||||
@@ -120,6 +120,45 @@ describe('multi-repo workspaces (#514) + .gitignore-respect default (#970, #976)
|
|||||||
expect(files).toContain('tools.ts'); // the parent's own tracked code still indexes
|
expect(files).toContain('tools.ts'); // the parent's own tracked code still indexes
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('child-pattern spelling revives repos whose PARENT dir carries the gitignore rule (#1295)', () => {
|
||||||
|
// `.gitignore: /repos/` lists `repos/` as ONE ignored entry, while the
|
||||||
|
// CLI hint suggests `includeIgnored: ["repos/a/", "repos/b/"]` — the
|
||||||
|
// child spelling. That never matched the parent path, so the documented
|
||||||
|
// opt-in silently indexed nothing.
|
||||||
|
write(path.join(ws, 'repos/a/a.ts'), 'export const a = 1;\n');
|
||||||
|
write(path.join(ws, 'repos/b/b.ts'), 'export const b = 2;\n');
|
||||||
|
makeRepo(path.join(ws, 'repos/a'));
|
||||||
|
makeRepo(path.join(ws, 'repos/b'));
|
||||||
|
write(path.join(ws, '.gitignore'), '/repos/\n');
|
||||||
|
writeConfig({ includeIgnored: ['repos/a/', 'repos/b/'] });
|
||||||
|
makeRepo(ws);
|
||||||
|
|
||||||
|
const files = scanDirectory(ws);
|
||||||
|
expect(files).toContain('repos/a/a.ts');
|
||||||
|
expect(files).toContain('repos/b/b.ts');
|
||||||
|
// Discovery (the watcher path) agrees with the scanner.
|
||||||
|
expect(discoverEmbeddedRepoRoots(ws).sort()).toEqual(['repos/a/', 'repos/b/']);
|
||||||
|
// And the CLI hint has nothing left to nag about.
|
||||||
|
expect(findUnindexedIgnoredRepos(ws)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('child-pattern spelling opts in ONLY the named repo; siblings stay out and stay hinted (#1295)', () => {
|
||||||
|
write(path.join(ws, 'repos/a/a.ts'), 'export const a = 1;\n');
|
||||||
|
write(path.join(ws, 'repos/b/b.ts'), 'export const b = 2;\n');
|
||||||
|
makeRepo(path.join(ws, 'repos/a'));
|
||||||
|
makeRepo(path.join(ws, 'repos/b'));
|
||||||
|
write(path.join(ws, '.gitignore'), '/repos/\n');
|
||||||
|
writeConfig({ includeIgnored: ['repos/a/'] });
|
||||||
|
makeRepo(ws);
|
||||||
|
|
||||||
|
const files = scanDirectory(ws);
|
||||||
|
expect(files).toContain('repos/a/a.ts');
|
||||||
|
expect(files.some((f) => f.startsWith('repos/b/'))).toBe(false);
|
||||||
|
expect(discoverEmbeddedRepoRoots(ws)).toEqual(['repos/a/']);
|
||||||
|
// The unopted sibling is still worth hinting about.
|
||||||
|
expect(findUnindexedIgnoredRepos(ws)).toEqual(['repos/b/']);
|
||||||
|
});
|
||||||
|
|
||||||
it('only re-includes the opted-in dir, not every gitignored dir', () => {
|
it('only re-includes the opted-in dir, not every gitignored dir', () => {
|
||||||
// `packages/` is opted in; `scratch/` (also holding a repo) is NOT.
|
// `packages/` is opted in; `scratch/` (also holding a repo) is NOT.
|
||||||
write(path.join(ws, 'packages/proj-a/src/auth.ts'), 'export function login() {}\n');
|
write(path.join(ws, 'packages/proj-a/src/auth.ts'), 'export function login() {}\n');
|
||||||
|
|||||||
+19
-2
@@ -839,6 +839,10 @@ export function findUnindexedIgnoredRepos(rootDir: string): string[] {
|
|||||||
if (defaults.ignores(dir)) continue; // node_modules etc. — never project code
|
if (defaults.ignores(dir)) continue; // node_modules etc. — never project code
|
||||||
if (includeIgnored?.ignores(normalizePath(dir))) continue; // already opted in — nothing to nag about
|
if (includeIgnored?.ignores(normalizePath(dir))) continue; // already opted in — nothing to nag about
|
||||||
for (const repo of findNestedGitRepos(path.join(rootDir, dir), dir)) {
|
for (const repo of findNestedGitRepos(path.join(rootDir, dir), dir)) {
|
||||||
|
// Per-repo opt-in check, mirroring findIgnoredEmbeddedRepos: a child
|
||||||
|
// pattern (`repos/a/`) doesn't match the parent dir above but DOES
|
||||||
|
// cover this repo — it's indexed, so don't nag about it (#1295).
|
||||||
|
if (includeIgnored?.ignores(normalizePath(repo))) continue;
|
||||||
repos.push(repo);
|
repos.push(repo);
|
||||||
if (repos.length >= UNINDEXED_IGNORED_REPO_HINT_CAP) return repos;
|
if (repos.length >= UNINDEXED_IGNORED_REPO_HINT_CAP) return repos;
|
||||||
}
|
}
|
||||||
@@ -867,8 +871,21 @@ function findIgnoredEmbeddedRepos(repoDir: string, includeIgnored: Ignore | null
|
|||||||
const repos: string[] = [];
|
const repos: string[] = [];
|
||||||
for (const dir of listIgnoredDirs(repoDir)) {
|
for (const dir of listIgnoredDirs(repoDir)) {
|
||||||
if (defaults.ignores(dir)) continue;
|
if (defaults.ignores(dir)) continue;
|
||||||
if (!includeIgnored.ignores(normalizePath(prefix + dir))) continue;
|
const nested = findNestedGitRepos(path.join(repoDir, dir), dir);
|
||||||
repos.push(...findNestedGitRepos(path.join(repoDir, dir), dir));
|
if (includeIgnored.ignores(normalizePath(prefix + dir))) {
|
||||||
|
// The whole ignored dir is opted in — every nested repo under it counts.
|
||||||
|
repos.push(...nested);
|
||||||
|
} else {
|
||||||
|
// A single gitignore rule often covers the PARENT of the opted-in
|
||||||
|
// repos: `.gitignore: /repos/` lists `repos/` as ONE ignored entry,
|
||||||
|
// while `includeIgnored: ["repos/a/"]` (the CLI hint's own suggested
|
||||||
|
// spelling) names the child — which never matches the parent path, so
|
||||||
|
// the opt-in silently did nothing (#1295). Match each nested repo
|
||||||
|
// root individually so both spellings work. The walk is bounded
|
||||||
|
// (depth/entry caps in findNestedGitRepos) and only runs when
|
||||||
|
// includeIgnored is configured at all.
|
||||||
|
repos.push(...nested.filter((r) => includeIgnored.ignores(normalizePath(prefix + r))));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return repos;
|
return repos;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user