Could not reproduce the reported nested-gitignore node_modules blowup on Linux against main (or with the real Boba-Base ignore files): both the git ls-files path and scanDirectoryWalk already exclude via DEFAULT_IGNORE and per-directory .gitignore. Add CODEGRAPH_DEBUG logging when the git listing falls back, plus regression tests for the reporter's layout on both scan paths so a future regression fails loudly. Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
This commit is contained in:
co-authored by
Colby McHenry
parent
a6f52d737a
commit
bb204f8855
@@ -7350,6 +7350,105 @@ describe('Directory Exclusion', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('Nested .gitignore node_modules exclusion (#1567)', () => {
|
||||||
|
let tempDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tempDir = createTempDir();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanupTempDir(tempDir);
|
||||||
|
});
|
||||||
|
|
||||||
|
function plantNodeModules(subproject: string, packages = 80): void {
|
||||||
|
const base = path.join(tempDir, subproject, 'node_modules');
|
||||||
|
for (let i = 0; i < packages; i++) {
|
||||||
|
const pkg = path.join(base, `pkg${i}`);
|
||||||
|
fs.mkdirSync(pkg, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(pkg, 'index.js'), `module.exports = ${i};`);
|
||||||
|
fs.writeFileSync(path.join(pkg, 'index.d.ts'), 'export const n: number;');
|
||||||
|
if (i % 4 === 0) fs.writeFileSync(path.join(pkg, '.gitignore'), '*.map\n');
|
||||||
|
const nested = path.join(pkg, 'node_modules', `nested${i}`);
|
||||||
|
fs.mkdirSync(nested, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(nested, 'lib.ts'), 'export const x = 1;');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initGitRepo(): void {
|
||||||
|
const { execFileSync } = require('child_process') as typeof import('child_process');
|
||||||
|
execFileSync('git', ['init'], { cwd: tempDir, stdio: 'ignore' });
|
||||||
|
execFileSync('git', ['add', '-A'], { cwd: tempDir, stdio: 'ignore' });
|
||||||
|
execFileSync(
|
||||||
|
'git',
|
||||||
|
['-c', 'user.email=test@example.com', '-c', 'user.name=Test', 'commit', '-m', 'init'],
|
||||||
|
{ cwd: tempDir, stdio: 'ignore' },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('excludes node_modules ignored only by a nested .gitignore (git path)', () => {
|
||||||
|
fs.mkdirSync(path.join(tempDir, 'frontend', 'src'), { recursive: true });
|
||||||
|
fs.mkdirSync(path.join(tempDir, 'extension', 'src'), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(tempDir, 'frontend', 'src', 'app.ts'), 'export const a = 1;');
|
||||||
|
fs.writeFileSync(path.join(tempDir, 'extension', 'src', 'ext.ts'), 'export const b = 1;');
|
||||||
|
fs.writeFileSync(path.join(tempDir, 'root.ts'), 'export const r = 1;');
|
||||||
|
fs.writeFileSync(path.join(tempDir, '.gitignore'), '*.log\n');
|
||||||
|
fs.writeFileSync(path.join(tempDir, 'frontend', '.gitignore'), '/node_modules\n');
|
||||||
|
fs.writeFileSync(path.join(tempDir, 'extension', '.gitignore'), 'node_modules/\n');
|
||||||
|
plantNodeModules('frontend');
|
||||||
|
plantNodeModules('extension');
|
||||||
|
initGitRepo();
|
||||||
|
|
||||||
|
const files = scanDirectory(tempDir);
|
||||||
|
expect(files.sort()).toEqual(['extension/src/ext.ts', 'frontend/src/app.ts', 'root.ts']);
|
||||||
|
expect(files.every((f) => !f.includes('node_modules'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('excludes nested-gitignore node_modules on the filesystem-walk fallback too', () => {
|
||||||
|
fs.mkdirSync(path.join(tempDir, 'frontend', 'src'), { recursive: true });
|
||||||
|
fs.mkdirSync(path.join(tempDir, 'extension', 'src'), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(tempDir, 'frontend', 'src', 'app.ts'), 'export const a = 1;');
|
||||||
|
fs.writeFileSync(path.join(tempDir, 'extension', 'src', 'ext.ts'), 'export const b = 1;');
|
||||||
|
fs.writeFileSync(path.join(tempDir, 'root.ts'), 'export const r = 1;');
|
||||||
|
fs.writeFileSync(path.join(tempDir, '.gitignore'), '*.log\n');
|
||||||
|
fs.writeFileSync(path.join(tempDir, 'frontend', '.gitignore'), '/node_modules\n');
|
||||||
|
fs.writeFileSync(path.join(tempDir, 'extension', '.gitignore'), 'node_modules/\n');
|
||||||
|
plantNodeModules('frontend', 60);
|
||||||
|
plantNodeModules('extension', 60);
|
||||||
|
|
||||||
|
const files = scanDirectory(tempDir);
|
||||||
|
expect(files.sort()).toEqual(['extension/src/ext.ts', 'frontend/src/app.ts', 'root.ts']);
|
||||||
|
expect(files.every((f) => !f.includes('node_modules'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('still excludes when root only lists one subproject node_modules (Boba-like)', () => {
|
||||||
|
fs.mkdirSync(path.join(tempDir, 'frontend', 'src'), { recursive: true });
|
||||||
|
fs.mkdirSync(path.join(tempDir, 'extension', 'src'), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(tempDir, 'frontend', 'src', 'app.ts'), 'export const a = 1;');
|
||||||
|
fs.writeFileSync(path.join(tempDir, 'extension', 'src', 'ext.ts'), 'export const b = 1;');
|
||||||
|
fs.writeFileSync(path.join(tempDir, 'root.ts'), 'export const r = 1;');
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tempDir, '.gitignore'),
|
||||||
|
['*.log', 'frontend/node_modules/', 'frontend/.angular/', ''].join('\n'),
|
||||||
|
);
|
||||||
|
fs.writeFileSync(path.join(tempDir, 'frontend', '.gitignore'), '/node_modules\n');
|
||||||
|
fs.writeFileSync(path.join(tempDir, 'extension', '.gitignore'), 'node_modules/\n');
|
||||||
|
plantNodeModules('frontend', 40);
|
||||||
|
plantNodeModules('extension', 40);
|
||||||
|
|
||||||
|
const fsFiles = scanDirectory(tempDir);
|
||||||
|
expect(fsFiles.every((f) => !f.includes('node_modules'))).toBe(true);
|
||||||
|
expect(fsFiles.sort()).toEqual(['extension/src/ext.ts', 'frontend/src/app.ts', 'root.ts']);
|
||||||
|
|
||||||
|
initGitRepo();
|
||||||
|
const gitFiles = scanDirectory(tempDir);
|
||||||
|
expect(gitFiles.every((f) => !f.includes('node_modules'))).toBe(true);
|
||||||
|
expect(gitFiles.sort()).toEqual(['extension/src/ext.ts', 'frontend/src/app.ts', 'root.ts']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('Git Submodules', () => {
|
describe('Git Submodules', () => {
|
||||||
let tempDir: string;
|
let tempDir: string;
|
||||||
|
|
||||||
|
|||||||
+18
-1
@@ -1050,6 +1050,10 @@ function getGitVisibleFiles(rootDir: string): Set<string> | null {
|
|||||||
{ cwd: rootDir, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }
|
{ cwd: rootDir, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }
|
||||||
);
|
);
|
||||||
// Directory is gitignored by parent repo — fall back to filesystem walk
|
// Directory is gitignored by parent repo — fall back to filesystem walk
|
||||||
|
logDebug('project root is gitignored by a parent repo — falling back to filesystem walk', {
|
||||||
|
rootDir,
|
||||||
|
gitRoot,
|
||||||
|
});
|
||||||
return null;
|
return null;
|
||||||
} catch {
|
} catch {
|
||||||
// Not ignored — safe to use git ls-files
|
// Not ignored — safe to use git ls-files
|
||||||
@@ -1074,7 +1078,20 @@ function getGitVisibleFiles(rootDir: string): Set<string> | null {
|
|||||||
// Git, but still wanted in the graph.)
|
// Git, but still wanted in the graph.)
|
||||||
for (const f of collectIncludedFilesForRoot(rootDir)) visible.add(f);
|
for (const f of collectIncludedFilesForRoot(rootDir)) visible.add(f);
|
||||||
return visible;
|
return visible;
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
// Any failure here (git missing, a `git rev-parse`/`ls-files` timeout or
|
||||||
|
// buffer overrun under load, an unreadable repo, unsupported flag combo on
|
||||||
|
// older git, etc.) silently sent every caller to `scanDirectoryWalk` with
|
||||||
|
// zero signal that the fast git-delegated path was skipped — making reports
|
||||||
|
// like #1567 (nested-`.gitignore`-excluded `node_modules` walked into)
|
||||||
|
// hard to triage, since both ignore implementations look correct in
|
||||||
|
// isolation but there was no way to tell which one ran. Log it under the
|
||||||
|
// existing CODEGRAPH_DEBUG gate so a future report can confirm or rule out
|
||||||
|
// the fallback in one step.
|
||||||
|
logDebug('git-based file listing unavailable — falling back to filesystem walk', {
|
||||||
|
rootDir,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user