diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index cb69e2ab..6d408834 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -3130,6 +3130,37 @@ describe('Git Submodules', () => { expect(files).toContain('app.ts'); expect(files).toContain('libs/lib/lib.ts'); }); + + it('should index files inside independent nested git repositories (issue #193)', async () => { + const { execFileSync } = await import('child_process'); + const git = (cwd: string, ...args: string[]) => + execFileSync('git', args, { cwd, stdio: 'pipe' }); + + const mainDir = path.join(tempDir, 'main'); + fs.mkdirSync(mainDir, { recursive: true }); + git(mainDir, 'init', '-q'); + git(mainDir, 'config', 'user.email', 'test@test.com'); + git(mainDir, 'config', 'user.name', 'Test'); + fs.writeFileSync(path.join(mainDir, 'CMakeLists.txt'), 'add_subdirectory(sub_repo1)\n'); + git(mainDir, 'add', 'CMakeLists.txt'); + git(mainDir, 'commit', '-q', '-m', 'root init'); + + const nestedRepo = path.join(mainDir, 'sub_repo1'); + fs.mkdirSync(path.join(nestedRepo, 'src'), { recursive: true }); + git(nestedRepo, 'init', '-q'); + git(nestedRepo, 'config', 'user.email', 'test@test.com'); + git(nestedRepo, 'config', 'user.name', 'Test'); + fs.writeFileSync(path.join(nestedRepo, 'CMakeLists.txt'), 'project(sub_repo1)\n'); + fs.writeFileSync(path.join(nestedRepo, 'src', 'main.cpp'), 'int main() { return 0; }\n'); + git(nestedRepo, 'add', '-A'); + git(nestedRepo, 'commit', '-q', '-m', 'nested init'); + + const config = { ...DEFAULT_CONFIG, rootDir: mainDir }; + const files = scanDirectory(mainDir, config); + + expect(files).toContain('sub_repo1/src/main.cpp'); + expect(files.every((file) => !file.includes('.git/'))).toBe(true); + }); }); // ============================================================================= diff --git a/src/extraction/index.ts b/src/extraction/index.ts index bf1e6319..5301b11e 100644 --- a/src/extraction/index.ts +++ b/src/extraction/index.ts @@ -101,6 +101,14 @@ function matchesGlob(filePath: string, pattern: string): boolean { return picomatch.isMatch(filePath, pattern, { dot: true }); } +function isSameDirectory(left: string, right: string): boolean { + try { + return fs.realpathSync(left) === fs.realpathSync(right); + } catch { + return path.resolve(left) === path.resolve(right); + } +} + /** * Check if a file should be included based on config */ @@ -141,7 +149,7 @@ function getGitVisibleFiles(rootDir: string): Set | null { { cwd: rootDir, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] } ).trim(); - if (path.resolve(gitRoot) !== path.resolve(rootDir)) { + if (!isSameDirectory(gitRoot, rootDir)) { try { // git check-ignore exits 0 if the path IS ignored, 1 if not execFileSync( @@ -177,7 +185,20 @@ function getGitVisibleFiles(rootDir: string): Set | null { for (const line of untracked.split('\n')) { const trimmed = line.trim(); if (trimmed) { - files.add(normalizePath(trimmed)); + const normalized = normalizePath(trimmed); + // Git reports unregistered nested repos as "dir/" entries; expand + // them so top-level workspace indexes can see the repo's files. + if (normalized.endsWith('/')) { + const nestedFiles = getNestedGitRepoFiles(rootDir, normalized); + if (nestedFiles.length > 0) { + for (const nestedFile of nestedFiles) { + files.add(nestedFile); + } + continue; + } + } + + files.add(normalized); } } @@ -244,6 +265,38 @@ function getGitChangedFiles(rootDir: string, config: CodeGraphConfig): GitChange */ const CODEGRAPH_IGNORE_MARKER = '.codegraphignore'; +function isGitRepositoryRoot(dir: string): boolean { + try { + const gitRoot = execFileSync( + 'git', + ['rev-parse', '--show-toplevel'], + { cwd: dir, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] } + ).trim(); + return isSameDirectory(gitRoot, dir); + } catch { + return false; + } +} + +function getNestedGitRepoFiles(rootDir: string, nestedRelativePath: string): string[] { + const nestedPrefix = normalizePath(nestedRelativePath).replace(/\/+$/, ''); + if (!nestedPrefix) return []; + + const nestedRoot = path.join(rootDir, nestedPrefix); + try { + if (!fs.statSync(nestedRoot).isDirectory()) return []; + } catch { + return []; + } + + if (!isGitRepositoryRoot(nestedRoot)) return []; + + const nestedFiles = getGitVisibleFiles(nestedRoot); + if (!nestedFiles) return []; + + return [...nestedFiles].map((filePath) => normalizePath(path.join(nestedPrefix, filePath))); +} + /** * Recursively scan directory for source files. *