mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
fix(core): Add .geminiignore support to SearchText tool (#13763)
Co-authored-by: Gaurav <39389231+gsquared94@users.noreply.github.com>
This commit is contained in:
@@ -27,6 +27,10 @@ class MockConfig {
|
||||
getDebugMode() {
|
||||
return true;
|
||||
}
|
||||
|
||||
getFileFilteringRespectGeminiIgnore() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
describe('ripgrep-real-direct', () => {
|
||||
|
||||
@@ -252,6 +252,7 @@ describe('RipGrepTool', () => {
|
||||
getTargetDir: () => tempRootDir,
|
||||
getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir),
|
||||
getDebugMode: () => false,
|
||||
getFileFilteringRespectGeminiIgnore: () => true,
|
||||
} as unknown as Config;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -735,6 +736,7 @@ describe('RipGrepTool', () => {
|
||||
getWorkspaceContext: () =>
|
||||
createMockWorkspaceContext(tempRootDir, [secondDir]),
|
||||
getDebugMode: () => false,
|
||||
getFileFilteringRespectGeminiIgnore: () => true,
|
||||
} as unknown as Config;
|
||||
|
||||
// Setup specific mock for this test - multi-directory search for 'world'
|
||||
@@ -876,6 +878,7 @@ describe('RipGrepTool', () => {
|
||||
getWorkspaceContext: () =>
|
||||
createMockWorkspaceContext(tempRootDir, [secondDir]),
|
||||
getDebugMode: () => false,
|
||||
getFileFilteringRespectGeminiIgnore: () => true,
|
||||
} as unknown as Config;
|
||||
|
||||
// Setup specific mock for this test - searching in 'sub' should only return matches from that directory
|
||||
@@ -1644,6 +1647,80 @@ describe('RipGrepTool', () => {
|
||||
expect(result.llmContent).toContain('L1: secret log entry');
|
||||
});
|
||||
|
||||
it('should add .geminiignore when enabled and patterns exist', async () => {
|
||||
const geminiIgnorePath = path.join(tempRootDir, '.geminiignore');
|
||||
await fs.writeFile(geminiIgnorePath, 'ignored.log');
|
||||
const configWithGeminiIgnore = {
|
||||
getTargetDir: () => tempRootDir,
|
||||
getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir),
|
||||
getDebugMode: () => false,
|
||||
getFileFilteringRespectGeminiIgnore: () => true,
|
||||
} as unknown as Config;
|
||||
const geminiIgnoreTool = new RipGrepTool(configWithGeminiIgnore);
|
||||
|
||||
mockSpawn.mockImplementationOnce(
|
||||
createMockSpawn({
|
||||
outputData:
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'ignored.log' },
|
||||
line_number: 1,
|
||||
lines: { text: 'secret log entry\n' },
|
||||
},
|
||||
}) + '\n',
|
||||
exitCode: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
const params: RipGrepToolParams = { pattern: 'secret' };
|
||||
const invocation = geminiIgnoreTool.build(params);
|
||||
await invocation.execute(abortSignal);
|
||||
|
||||
expect(mockSpawn).toHaveBeenLastCalledWith(
|
||||
expect.anything(),
|
||||
expect.arrayContaining(['--ignore-file', geminiIgnorePath]),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should skip .geminiignore when disabled', async () => {
|
||||
const geminiIgnorePath = path.join(tempRootDir, '.geminiignore');
|
||||
await fs.writeFile(geminiIgnorePath, 'ignored.log');
|
||||
const configWithoutGeminiIgnore = {
|
||||
getTargetDir: () => tempRootDir,
|
||||
getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir),
|
||||
getDebugMode: () => false,
|
||||
getFileFilteringRespectGeminiIgnore: () => false,
|
||||
} as unknown as Config;
|
||||
const geminiIgnoreTool = new RipGrepTool(configWithoutGeminiIgnore);
|
||||
|
||||
mockSpawn.mockImplementationOnce(
|
||||
createMockSpawn({
|
||||
outputData:
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'ignored.log' },
|
||||
line_number: 1,
|
||||
lines: { text: 'secret log entry\n' },
|
||||
},
|
||||
}) + '\n',
|
||||
exitCode: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
const params: RipGrepToolParams = { pattern: 'secret' };
|
||||
const invocation = geminiIgnoreTool.build(params);
|
||||
await invocation.execute(abortSignal);
|
||||
|
||||
expect(mockSpawn).toHaveBeenLastCalledWith(
|
||||
expect.anything(),
|
||||
expect.not.arrayContaining(['--ignore-file', geminiIgnorePath]),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle context parameters', async () => {
|
||||
mockSpawn.mockImplementationOnce(
|
||||
createMockSpawn({
|
||||
@@ -1761,6 +1838,7 @@ describe('RipGrepTool', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
storageSpy.mockRestore();
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
FileExclusions,
|
||||
COMMON_DIRECTORY_EXCLUDES,
|
||||
} from '../utils/ignorePatterns.js';
|
||||
import { GeminiIgnoreParser } from '../utils/geminiIgnoreParser.js';
|
||||
|
||||
const DEFAULT_TOTAL_MAX_MATCHES = 20000;
|
||||
|
||||
@@ -189,6 +190,7 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
> {
|
||||
constructor(
|
||||
private readonly config: Config,
|
||||
private readonly geminiIgnoreParser: GeminiIgnoreParser,
|
||||
params: RipGrepToolParams,
|
||||
messageBus?: MessageBus,
|
||||
_toolName?: string,
|
||||
@@ -387,6 +389,14 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
excludes.forEach((exclude) => {
|
||||
rgArgs.push('--glob', `!${exclude}`);
|
||||
});
|
||||
|
||||
if (this.config.getFileFilteringRespectGeminiIgnore()) {
|
||||
// Add .geminiignore support (ripgrep natively handles .gitignore)
|
||||
const geminiIgnorePath = this.geminiIgnoreParser.getIgnoreFilePath();
|
||||
if (geminiIgnorePath) {
|
||||
rgArgs.push('--ignore-file', geminiIgnorePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rgArgs.push('--threads', '4');
|
||||
@@ -479,6 +489,7 @@ export class RipGrepTool extends BaseDeclarativeTool<
|
||||
ToolResult
|
||||
> {
|
||||
static readonly Name = GREP_TOOL_NAME;
|
||||
private readonly geminiIgnoreParser: GeminiIgnoreParser;
|
||||
|
||||
constructor(
|
||||
private readonly config: Config,
|
||||
@@ -544,6 +555,7 @@ export class RipGrepTool extends BaseDeclarativeTool<
|
||||
false, // canUpdateOutput
|
||||
messageBus,
|
||||
);
|
||||
this.geminiIgnoreParser = new GeminiIgnoreParser(config.getTargetDir());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -580,6 +592,7 @@ export class RipGrepTool extends BaseDeclarativeTool<
|
||||
): ToolInvocation<RipGrepToolParams, ToolResult> {
|
||||
return new GrepToolInvocation(
|
||||
this.config,
|
||||
this.geminiIgnoreParser,
|
||||
params,
|
||||
messageBus,
|
||||
_toolName,
|
||||
|
||||
@@ -58,6 +58,25 @@ describe('GeminiIgnoreParser', () => {
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return ignore file path when patterns exist', () => {
|
||||
const parser = new GeminiIgnoreParser(projectRoot);
|
||||
expect(parser.getIgnoreFilePath()).toBe(
|
||||
path.join(projectRoot, '.geminiignore'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return true for hasPatterns when patterns exist', () => {
|
||||
const parser = new GeminiIgnoreParser(projectRoot);
|
||||
expect(parser.hasPatterns()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for hasPatterns when .geminiignore is deleted', async () => {
|
||||
const parser = new GeminiIgnoreParser(projectRoot);
|
||||
await fs.rm(path.join(projectRoot, '.geminiignore'));
|
||||
expect(parser.hasPatterns()).toBe(false);
|
||||
expect(parser.getIgnoreFilePath()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when .geminiignore does not exist', () => {
|
||||
@@ -66,5 +85,50 @@ describe('GeminiIgnoreParser', () => {
|
||||
expect(parser.getPatterns()).toEqual([]);
|
||||
expect(parser.isIgnored('any_file.txt')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return null for getIgnoreFilePath when no patterns exist', () => {
|
||||
const parser = new GeminiIgnoreParser(projectRoot);
|
||||
expect(parser.getIgnoreFilePath()).toBeNull();
|
||||
});
|
||||
|
||||
it('should return false for hasPatterns when no patterns exist', () => {
|
||||
const parser = new GeminiIgnoreParser(projectRoot);
|
||||
expect(parser.hasPatterns()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when .geminiignore is empty', () => {
|
||||
beforeEach(async () => {
|
||||
await createTestFile('.geminiignore', '');
|
||||
});
|
||||
|
||||
it('should return null for getIgnoreFilePath', () => {
|
||||
const parser = new GeminiIgnoreParser(projectRoot);
|
||||
expect(parser.getIgnoreFilePath()).toBeNull();
|
||||
});
|
||||
|
||||
it('should return false for hasPatterns', () => {
|
||||
const parser = new GeminiIgnoreParser(projectRoot);
|
||||
expect(parser.hasPatterns()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when .geminiignore only has comments', () => {
|
||||
beforeEach(async () => {
|
||||
await createTestFile(
|
||||
'.geminiignore',
|
||||
'# This is a comment\n# Another comment\n',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return null for getIgnoreFilePath', () => {
|
||||
const parser = new GeminiIgnoreParser(projectRoot);
|
||||
expect(parser.getIgnoreFilePath()).toBeNull();
|
||||
});
|
||||
|
||||
it('should return false for hasPatterns', () => {
|
||||
const parser = new GeminiIgnoreParser(projectRoot);
|
||||
expect(parser.hasPatterns()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,8 @@ import ignore from 'ignore';
|
||||
export interface GeminiIgnoreFilter {
|
||||
isIgnored(filePath: string): boolean;
|
||||
getPatterns(): string[];
|
||||
getIgnoreFilePath(): string | null;
|
||||
hasPatterns(): boolean;
|
||||
}
|
||||
|
||||
export class GeminiIgnoreParser implements GeminiIgnoreFilter {
|
||||
@@ -78,4 +80,26 @@ export class GeminiIgnoreParser implements GeminiIgnoreFilter {
|
||||
getPatterns(): string[] {
|
||||
return this.patterns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to .geminiignore file if it exists and has patterns.
|
||||
* Useful for tools like ripgrep that support --ignore-file flag.
|
||||
*/
|
||||
getIgnoreFilePath(): string | null {
|
||||
if (!this.hasPatterns()) {
|
||||
return null;
|
||||
}
|
||||
return path.join(this.projectRoot, '.geminiignore');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if .geminiignore exists and has patterns.
|
||||
*/
|
||||
hasPatterns(): boolean {
|
||||
if (this.patterns.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const ignoreFilePath = path.join(this.projectRoot, '.geminiignore');
|
||||
return fs.existsSync(ignoreFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user