@@ -125,10 +125,61 @@ export function shouldIncludeFile(
125125 return false ;
126126}
127127
128+ /**
129+ * Collect git-visible files (tracked + untracked, .gitignore-respected) from the
130+ * git repository rooted at `repoDir`, adding each to `files` with `prefix`
131+ * prepended so paths stay relative to the original scan root.
132+ *
133+ * Recurses into embedded git repositories — nested repos that are NOT submodules
134+ * (independent clones living inside the workspace, common in CMake "super-repo"
135+ * layouts). The parent repo's `git ls-files` cannot see into them: tracked output
136+ * skips them entirely, and untracked output reports them only as an opaque
137+ * "subdir/" entry (trailing slash) rather than expanding their files. Each
138+ * embedded repo is its own git boundary, so we re-run `git ls-files` inside it.
139+ * (See issue #193.)
140+ */
141+ function collectGitFiles ( repoDir : string , prefix : string , files : Set < string > ) : void {
142+ const gitOpts = { cwd : repoDir , encoding : 'utf-8' as const , timeout : 30000 , maxBuffer : 50 * 1024 * 1024 , stdio : [ 'pipe' , 'pipe' , 'pipe' ] as [ 'pipe' , 'pipe' , 'pipe' ] } ;
143+
144+ // Tracked files. --recurse-submodules pulls in files from active submodules,
145+ // which the index would otherwise represent only as a commit pointer.
146+ // Without this, monorepos using submodules index 0 files. (See issue #147.)
147+ // Note: --recurse-submodules only supports -c/--cached and --stage modes — it
148+ // can't be combined with -o, so untracked files are gathered separately below.
149+ const tracked = execFileSync ( 'git' , [ 'ls-files' , '-c' , '--recurse-submodules' ] , gitOpts ) ;
150+ for ( const line of tracked . split ( '\n' ) ) {
151+ const trimmed = line . trim ( ) ;
152+ if ( trimmed ) {
153+ files . add ( normalizePath ( prefix + trimmed ) ) ;
154+ }
155+ }
156+
157+ // Untracked files (submodules manage their own untracked state). Embedded git
158+ // repos surface here as a single "subdir/" entry that git refuses to descend
159+ // into — recurse into those as their own repos so their source gets indexed.
160+ const untracked = execFileSync ( 'git' , [ 'ls-files' , '-o' , '--exclude-standard' ] , gitOpts ) ;
161+ for ( const line of untracked . split ( '\n' ) ) {
162+ const trimmed = line . trim ( ) ;
163+ if ( ! trimmed ) continue ;
164+ if ( trimmed . endsWith ( '/' ) ) {
165+ // git only emits a trailing-slash directory entry for an embedded repo.
166+ // Guard with a .git check anyway, and skip anything else exactly as git
167+ // itself skips it (we never descend into a non-repo opaque dir).
168+ const childDir = path . join ( repoDir , trimmed ) ;
169+ if ( fs . existsSync ( path . join ( childDir , '.git' ) ) ) {
170+ collectGitFiles ( childDir , prefix + trimmed , files ) ;
171+ }
172+ continue ;
173+ }
174+ files . add ( normalizePath ( prefix + trimmed ) ) ;
175+ }
176+ }
177+
128178/**
129179 * Get all files visible to git (tracked + untracked but not ignored).
130- * Respects .gitignore at all levels (root, subdirectories).
131- * Returns null on failure (non-git project) so callers can fall back.
180+ * Respects .gitignore at all levels (root, subdirectories) and descends into
181+ * embedded (nested, non-submodule) git repos. Returns null on failure
182+ * (non-git project) so callers can fall back to a filesystem walk.
132183 */
133184function getGitVisibleFiles ( rootDir : string ) : Set < string > | null {
134185 try {
@@ -157,30 +208,7 @@ function getGitVisibleFiles(rootDir: string): Set<string> | null {
157208 }
158209
159210 const files = new Set < string > ( ) ;
160- const gitOpts = { cwd : rootDir , encoding : 'utf-8' as const , timeout : 30000 , maxBuffer : 50 * 1024 * 1024 , stdio : [ 'pipe' , 'pipe' , 'pipe' ] as [ 'pipe' , 'pipe' , 'pipe' ] } ;
161-
162- // Tracked files. --recurse-submodules pulls in files from active submodules,
163- // which the main repo's index would otherwise represent only as a commit pointer.
164- // Without this, monorepos using submodules index 0 files. (See issue #147.)
165- // Note: --recurse-submodules only supports -c/--cached and --stage modes — it
166- // can't be combined with -o, so untracked files are gathered separately below.
167- const tracked = execFileSync ( 'git' , [ 'ls-files' , '-c' , '--recurse-submodules' ] , gitOpts ) ;
168- for ( const line of tracked . split ( '\n' ) ) {
169- const trimmed = line . trim ( ) ;
170- if ( trimmed ) {
171- files . add ( normalizePath ( trimmed ) ) ;
172- }
173- }
174-
175- // Untracked files in the main repo (submodules manage their own untracked state).
176- const untracked = execFileSync ( 'git' , [ 'ls-files' , '-o' , '--exclude-standard' ] , gitOpts ) ;
177- for ( const line of untracked . split ( '\n' ) ) {
178- const trimmed = line . trim ( ) ;
179- if ( trimmed ) {
180- files . add ( normalizePath ( trimmed ) ) ;
181- }
182- }
183-
211+ collectGitFiles ( rootDir , '' , files ) ;
184212 return files ;
185213 } catch {
186214 return null ;
0 commit comments