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:
Colby Mchenry
2026-07-16 16:01:08 -05:00
committed by GitHub
co-authored by Claude Fable 5
parent c472cfb52e
commit a5a8942d1c
3 changed files with 59 additions and 2 deletions
+19 -2
View File
@@ -839,6 +839,10 @@ export function findUnindexedIgnoredRepos(rootDir: string): string[] {
if (defaults.ignores(dir)) continue; // node_modules etc. — never project code
if (includeIgnored?.ignores(normalizePath(dir))) continue; // already opted in — nothing to nag about
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);
if (repos.length >= UNINDEXED_IGNORED_REPO_HINT_CAP) return repos;
}
@@ -867,8 +871,21 @@ function findIgnoredEmbeddedRepos(repoDir: string, includeIgnored: Ignore | null
const repos: string[] = [];
for (const dir of listIgnoredDirs(repoDir)) {
if (defaults.ignores(dir)) continue;
if (!includeIgnored.ignores(normalizePath(prefix + dir))) continue;
repos.push(...findNestedGitRepos(path.join(repoDir, dir), dir));
const nested = 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;
}