* Filesystem walk fallback for non-git projects.
( rootDir: string, onProgress?: (current: number, file: string) => void )
| 1246 | * Filesystem walk fallback for non-git projects. |
| 1247 | */ |
| 1248 | function scanDirectoryWalk( |
| 1249 | rootDir: string, |
| 1250 | onProgress?: (current: number, file: string) => void |
| 1251 | ): string[] { |
| 1252 | const files: string[] = []; |
| 1253 | let count = 0; |
| 1254 | const visitedDirs = new Set<string>(); |
| 1255 | // Custom extension → language overrides from the project's codegraph.json. |
| 1256 | const overrides = loadExtensionOverrides(rootDir); |
| 1257 | |
| 1258 | // A .gitignore matcher scoped to the directory that declared it. Patterns in |
| 1259 | // a nested .gitignore are relative to that directory, so we keep the dir |
| 1260 | // alongside the matcher and test paths relative to it — mirroring how git |
| 1261 | // applies .gitignore files at every level. |
| 1262 | interface ScopedIgnore { |
| 1263 | dir: string; |
| 1264 | ig: Ignore; |
| 1265 | } |
| 1266 | |
| 1267 | const loadIgnore = (dir: string): ScopedIgnore | null => { |
| 1268 | const giPath = path.join(dir, '.gitignore'); |
| 1269 | if (!fs.existsSync(giPath)) return null; |
| 1270 | // readGitignorePatterns is defensive: a non-UTF-8 (DLP-encrypted) or |
| 1271 | // uncompilable .gitignore is skipped/filtered with a warning, never thrown |
| 1272 | // (issue #682) — so the per-file `.ignores()` calls below can't crash. |
| 1273 | const patterns = readGitignorePatterns(giPath); |
| 1274 | return patterns ? { dir, ig: ignore().add(patterns) } : null; |
| 1275 | }; |
| 1276 | |
| 1277 | const isIgnored = (fullPath: string, isDir: boolean, matchers: ScopedIgnore[]): boolean => { |
| 1278 | for (const { dir, ig } of matchers) { |
| 1279 | let rel = normalizePath(path.relative(dir, fullPath)); |
| 1280 | if (!rel || rel.startsWith('..')) continue; // not under this matcher's dir |
| 1281 | if (isDir) rel += '/'; // dir-only rules (e.g. `build/`) only match with the slash |
| 1282 | if (ig.ignores(rel)) return true; |
| 1283 | } |
| 1284 | return false; |
| 1285 | }; |
| 1286 | |
| 1287 | function walk(dir: string, matchers: ScopedIgnore[]): void { |
| 1288 | let realDir: string; |
| 1289 | try { |
| 1290 | realDir = fs.realpathSync(dir); |
| 1291 | } catch { |
| 1292 | logDebug('Skipping unresolvable directory', { dir }); |
| 1293 | return; |
| 1294 | } |
| 1295 | |
| 1296 | if (visitedDirs.has(realDir)) { |
| 1297 | logDebug('Skipping already-visited directory (symlink cycle)', { dir, realDir }); |
| 1298 | return; |
| 1299 | } |
| 1300 | visitedDirs.add(realDir); |
| 1301 | |
| 1302 | // This directory's own .gitignore (if present) applies to everything below it. |
| 1303 | // The root's .gitignore is already merged into the seeded base matcher (so a |
| 1304 | // negation there can override a built-in default), so skip it here. |
| 1305 | const own = dir === rootDir ? null : loadIgnore(dir); |
no test coverage detected