fix(core): combine .gitignore and .geminiignore logic for correct precedence (#11587)

Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
Eric Rahm
2025-11-01 10:06:34 -07:00
committed by GitHub
parent caf2ca1438
commit e3262f8766
4 changed files with 169 additions and 4 deletions
@@ -245,4 +245,87 @@ describe('FileDiscoveryService', () => {
]);
});
});
describe('precedence (.geminiignore over .gitignore)', () => {
beforeEach(async () => {
await fs.mkdir(path.join(projectRoot, '.git'));
});
it('should un-ignore a file in .geminiignore that is ignored in .gitignore', async () => {
await createTestFile('.gitignore', '*.txt');
await createTestFile('.geminiignore', '!important.txt');
const service = new FileDiscoveryService(projectRoot);
const files = ['file.txt', 'important.txt'].map((f) =>
path.join(projectRoot, f),
);
const filtered = service.filterFiles(files);
expect(filtered).toEqual([path.join(projectRoot, 'important.txt')]);
});
it('should un-ignore a directory in .geminiignore that is ignored in .gitignore', async () => {
await createTestFile('.gitignore', 'logs/');
await createTestFile('.geminiignore', '!logs/');
const service = new FileDiscoveryService(projectRoot);
const files = ['logs/app.log', 'other/app.log'].map((f) =>
path.join(projectRoot, f),
);
const filtered = service.filterFiles(files);
expect(filtered).toEqual(files);
});
it('should extend ignore rules in .geminiignore', async () => {
await createTestFile('.gitignore', '*.log');
await createTestFile('.geminiignore', 'temp/');
const service = new FileDiscoveryService(projectRoot);
const files = ['app.log', 'temp/file.txt'].map((f) =>
path.join(projectRoot, f),
);
const filtered = service.filterFiles(files);
expect(filtered).toEqual([]);
});
it('should use .gitignore rules if respectGeminiIgnore is false', async () => {
await createTestFile('.gitignore', '*.txt');
await createTestFile('.geminiignore', '!important.txt');
const service = new FileDiscoveryService(projectRoot);
const files = ['file.txt', 'important.txt'].map((f) =>
path.join(projectRoot, f),
);
const filtered = service.filterFiles(files, {
respectGitIgnore: true,
respectGeminiIgnore: false,
});
expect(filtered).toEqual([]);
});
it('should use .geminiignore rules if respectGitIgnore is false', async () => {
await createTestFile('.gitignore', '*.txt');
await createTestFile('.geminiignore', '!important.txt\ntemp/');
const service = new FileDiscoveryService(projectRoot);
const files = ['file.txt', 'important.txt', 'temp/file.js'].map((f) =>
path.join(projectRoot, f),
);
const filtered = service.filterFiles(files, {
respectGitIgnore: false,
respectGeminiIgnore: true,
});
// .gitignore is ignored, so *.txt is not applied.
// .geminiignore un-ignores important.txt (which wasn't ignored anyway)
// and ignores temp/
expect(filtered).toEqual(
['file.txt', 'important.txt'].map((f) => path.join(projectRoot, f)),
);
});
});
});
@@ -24,6 +24,7 @@ export interface FilterReport {
export class FileDiscoveryService {
private gitIgnoreFilter: GitIgnoreFilter | null = null;
private geminiIgnoreFilter: GeminiIgnoreFilter | null = null;
private combinedIgnoreFilter: GitIgnoreFilter | null = null;
private projectRoot: string;
constructor(projectRoot: string) {
@@ -32,6 +33,15 @@ export class FileDiscoveryService {
this.gitIgnoreFilter = new GitIgnoreParser(this.projectRoot);
}
this.geminiIgnoreFilter = new GeminiIgnoreParser(this.projectRoot);
if (this.gitIgnoreFilter) {
const geminiPatterns = this.geminiIgnoreFilter.getPatterns();
// Create combined parser: .gitignore + .geminiignore
this.combinedIgnoreFilter = new GitIgnoreParser(
this.projectRoot,
geminiPatterns,
);
}
}
/**
@@ -40,6 +50,14 @@ export class FileDiscoveryService {
filterFiles(filePaths: string[], options: FilterFilesOptions = {}): string[] {
const { respectGitIgnore = true, respectGeminiIgnore = true } = options;
return filePaths.filter((filePath) => {
if (
respectGitIgnore &&
respectGeminiIgnore &&
this.combinedIgnoreFilter
) {
return !this.combinedIgnoreFilter.isIgnored(filePath);
}
if (respectGitIgnore && this.gitIgnoreFilter?.isIgnored(filePath)) {
return false;
}