refactor(core): improve ignore resolution and fix directory-matching bug (#23816)

This commit is contained in:
Emily Hedlund
2026-03-27 13:12:26 -04:00
committed by GitHub
parent f3977392e6
commit 29031ea7cf
9 changed files with 557 additions and 503 deletions
@@ -221,7 +221,7 @@ describe('FileDiscoveryService', () => {
});
});
describe('shouldGitIgnoreFile & shouldGeminiIgnoreFile', () => {
describe('shouldIgnoreFile & shouldIgnoreDirectory', () => {
beforeEach(async () => {
await fs.mkdir(path.join(projectRoot, '.git'));
await createTestFile('.gitignore', 'node_modules/');
@@ -238,6 +238,13 @@ describe('FileDiscoveryService', () => {
).toBe(true);
});
it('should return true for git-ignored directories', () => {
const service = new FileDiscoveryService(projectRoot);
expect(
service.shouldIgnoreDirectory(path.join(projectRoot, 'node_modules')),
).toBe(true);
});
it('should return false for non-git-ignored files', () => {
const service = new FileDiscoveryService(projectRoot);
@@ -293,6 +300,7 @@ describe('FileDiscoveryService', () => {
]);
});
});
describe('precedence (.geminiignore over .gitignore)', () => {
beforeEach(async () => {
await fs.mkdir(path.join(projectRoot, '.git'));
@@ -495,4 +503,99 @@ describe('FileDiscoveryService', () => {
expect(paths[0]).toBe(path.join(projectRoot, '.gitignore'));
});
});
describe('getIgnoredPaths', () => {
beforeEach(async () => {
await fs.mkdir(path.join(projectRoot, '.git'));
});
it('should return all ignored paths that exist on disk', async () => {
await createTestFile(
'.gitignore',
'ignored-dir/\nignored-file.txt\n*.log',
);
await createTestFile('ignored-dir/inside.txt');
await createTestFile('ignored-file.txt');
await createTestFile('keep.log');
await createTestFile('src/index.ts');
await createTestFile(GEMINI_IGNORE_FILE_NAME, 'secrets/');
await createTestFile('secrets/passwords.txt');
const service = new FileDiscoveryService(projectRoot);
const ignoredPaths = await service.getIgnoredPaths();
const expectedPaths = [
path.join(projectRoot, '.git'),
path.join(projectRoot, 'ignored-dir'),
path.join(projectRoot, 'ignored-file.txt'),
path.join(projectRoot, 'keep.log'),
path.join(projectRoot, 'secrets'),
].sort();
expect(ignoredPaths.sort()).toEqual(expectedPaths);
});
it('should optimize by not traversing into ignored directories', async () => {
await createTestFile('.gitignore', 'ignored-dir/');
const ignoredDir = path.join(projectRoot, 'ignored-dir');
await fs.mkdir(ignoredDir);
await createTestFile('ignored-dir/large-file-1.txt');
const service = new FileDiscoveryService(projectRoot);
const ignoredPaths = await service.getIgnoredPaths();
expect(ignoredPaths.sort()).toEqual(
[path.join(projectRoot, '.git'), ignoredDir].sort(),
);
});
it('should handle un-ignore patterns correctly', async () => {
await createTestFile(
'.gitignore',
'ignored-dir/*\n!ignored-dir/keep.txt',
);
await createTestFile('ignored-dir/ignored.txt');
await createTestFile('ignored-dir/keep.txt');
const service = new FileDiscoveryService(projectRoot);
const ignoredPaths = await service.getIgnoredPaths();
expect(ignoredPaths).toContain(
path.join(projectRoot, 'ignored-dir/ignored.txt'),
);
expect(ignoredPaths).not.toContain(
path.join(projectRoot, 'ignored-dir/keep.txt'),
);
expect(ignoredPaths).not.toContain(path.join(projectRoot, 'ignored-dir'));
});
it('should respect FilterFilesOptions when provided', async () => {
await createTestFile('.gitignore', 'ignored-by-git.txt');
await createTestFile(GEMINI_IGNORE_FILE_NAME, 'ignored-by-gemini.txt');
await createTestFile('ignored-by-git.txt');
await createTestFile('ignored-by-gemini.txt');
const service = new FileDiscoveryService(projectRoot);
const onlyGemini = await service.getIgnoredPaths({
respectGitIgnore: false,
respectGeminiIgnore: true,
});
expect(onlyGemini).toContain(
path.join(projectRoot, 'ignored-by-gemini.txt'),
);
expect(onlyGemini).not.toContain(
path.join(projectRoot, 'ignored-by-git.txt'),
);
const onlyGit = await service.getIgnoredPaths({
respectGitIgnore: true,
respectGeminiIgnore: false,
});
expect(onlyGit).toContain(path.join(projectRoot, 'ignored-by-git.txt'));
expect(onlyGit).not.toContain(
path.join(projectRoot, 'ignored-by-gemini.txt'),
);
});
});
});