(rootDir: string)
| 555 | * there already. |
| 556 | */ |
| 557 | export function discoverEmbeddedRepoRoots(rootDir: string): string[] { |
| 558 | try { |
| 559 | execFileSync('git', ['rev-parse', '--git-dir'], { cwd: rootDir, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }); |
| 560 | } catch { |
| 561 | return []; |
| 562 | } |
| 563 | const out: string[] = []; |
| 564 | const defaults = defaultsOnlyIgnore(); |
| 565 | const includeIgnored = loadIncludeIgnoredMatcher(rootDir); |
| 566 | const visit = (repoAbs: string, prefix: string): void => { |
| 567 | const candidates: string[] = []; |
| 568 | try { |
| 569 | const o = execFileSync( |
| 570 | 'git', |
| 571 | ['ls-files', '-z', '-o', '--exclude-standard', '--directory'], |
| 572 | { cwd: repoAbs, encoding: 'utf-8', timeout: 30000, maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true } |
| 573 | ); |
| 574 | for (const e of o.split('\0')) { |
| 575 | if (e.endsWith('/') && !isWholeCwdEntry(e) && !defaults.ignores(e)) { |
| 576 | candidates.push(...findNestedGitRepos(path.join(repoAbs, e), e)); |
| 577 | } |
| 578 | } |
| 579 | } catch { /* untracked listing failed — ignored-side discovery still runs */ } |
| 580 | // Unexpanded gitlinks (mode 160000) with a real checkout on disk — embedded |
| 581 | // repos `git add`ed without `.gitmodules`, or submodules not active here. The |
| 582 | // untracked listing above can't see them (they're tracked), so find them the |
| 583 | // same way collectGitFiles does, keeping watcher scope == indexer scope. |
| 584 | // (#1031, #1033) |
| 585 | try { |
| 586 | const staged = execFileSync( |
| 587 | 'git', |
| 588 | ['ls-files', '-z', '-s', '--recurse-submodules'], |
| 589 | { cwd: repoAbs, encoding: 'utf-8', timeout: 30000, maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true } |
| 590 | ); |
| 591 | const repoIgnore = buildDefaultIgnore(repoAbs); |
| 592 | for (const entry of staged.split('\0')) { |
| 593 | if (!entry || entry.slice(0, 6) !== '160000') continue; |
| 594 | const tab = entry.indexOf('\t'); |
| 595 | if (tab === -1) continue; |
| 596 | const rel = entry.slice(tab + 1); |
| 597 | const relDir = rel.endsWith('/') ? rel : rel + '/'; |
| 598 | // A gitlink under a gitignored path is respected (not indexed) unless the |
| 599 | // project opted it in — same rule as the untracked-ignored kind (#1065). |
| 600 | if (gitlinkEmbeddedRepoSkipped(relDir, prefix, defaults, repoIgnore, includeIgnored)) continue; |
| 601 | if (classifyGitDir(path.join(repoAbs, rel)) === 'embedded') candidates.push(relDir); |
| 602 | } |
| 603 | } catch { /* staged listing failed — other discovery still runs */ } |
| 604 | candidates.push(...findIgnoredEmbeddedRepos(repoAbs, includeIgnored, prefix)); |
| 605 | for (const rel of candidates) { |
| 606 | const full = normalizePath(prefix + rel); |
| 607 | out.push(full); |
| 608 | visit(path.join(repoAbs, rel), full); |
| 609 | } |
| 610 | }; |
| 611 | visit(rootDir, ''); |
| 612 | return out; |
| 613 | } |
| 614 |
no test coverage detected