mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-18 18:11:02 -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() {
|
getDebugMode() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFileFilteringRespectGeminiIgnore() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('ripgrep-real-direct', () => {
|
describe('ripgrep-real-direct', () => {
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ describe('RipGrepTool', () => {
|
|||||||
getTargetDir: () => tempRootDir,
|
getTargetDir: () => tempRootDir,
|
||||||
getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir),
|
getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir),
|
||||||
getDebugMode: () => false,
|
getDebugMode: () => false,
|
||||||
|
getFileFilteringRespectGeminiIgnore: () => true,
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -735,6 +736,7 @@ describe('RipGrepTool', () => {
|
|||||||
getWorkspaceContext: () =>
|
getWorkspaceContext: () =>
|
||||||
createMockWorkspaceContext(tempRootDir, [secondDir]),
|
createMockWorkspaceContext(tempRootDir, [secondDir]),
|
||||||
getDebugMode: () => false,
|
getDebugMode: () => false,
|
||||||
|
getFileFilteringRespectGeminiIgnore: () => true,
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
|
|
||||||
// Setup specific mock for this test - multi-directory search for 'world'
|
// Setup specific mock for this test - multi-directory search for 'world'
|
||||||
@@ -876,6 +878,7 @@ describe('RipGrepTool', () => {
|
|||||||
getWorkspaceContext: () =>
|
getWorkspaceContext: () =>
|
||||||
createMockWorkspaceContext(tempRootDir, [secondDir]),
|
createMockWorkspaceContext(tempRootDir, [secondDir]),
|
||||||
getDebugMode: () => false,
|
getDebugMode: () => false,
|
||||||
|
getFileFilteringRespectGeminiIgnore: () => true,
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
|
|
||||||
// Setup specific mock for this test - searching in 'sub' should only return matches from that directory
|
// 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');
|
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 () => {
|
it('should handle context parameters', async () => {
|
||||||
mockSpawn.mockImplementationOnce(
|
mockSpawn.mockImplementationOnce(
|
||||||
createMockSpawn({
|
createMockSpawn({
|
||||||
@@ -1761,6 +1838,7 @@ describe('RipGrepTool', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
storageSpy.mockRestore();
|
storageSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
FileExclusions,
|
FileExclusions,
|
||||||
COMMON_DIRECTORY_EXCLUDES,
|
COMMON_DIRECTORY_EXCLUDES,
|
||||||
} from '../utils/ignorePatterns.js';
|
} from '../utils/ignorePatterns.js';
|
||||||
|
import { GeminiIgnoreParser } from '../utils/geminiIgnoreParser.js';
|
||||||
|
|
||||||
const DEFAULT_TOTAL_MAX_MATCHES = 20000;
|
const DEFAULT_TOTAL_MAX_MATCHES = 20000;
|
||||||
|
|
||||||
@@ -189,6 +190,7 @@ class GrepToolInvocation extends BaseToolInvocation<
|
|||||||
> {
|
> {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly config: Config,
|
private readonly config: Config,
|
||||||
|
private readonly geminiIgnoreParser: GeminiIgnoreParser,
|
||||||
params: RipGrepToolParams,
|
params: RipGrepToolParams,
|
||||||
messageBus?: MessageBus,
|
messageBus?: MessageBus,
|
||||||
_toolName?: string,
|
_toolName?: string,
|
||||||
@@ -387,6 +389,14 @@ class GrepToolInvocation extends BaseToolInvocation<
|
|||||||
excludes.forEach((exclude) => {
|
excludes.forEach((exclude) => {
|
||||||
rgArgs.push('--glob', `!${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');
|
rgArgs.push('--threads', '4');
|
||||||
@@ -479,6 +489,7 @@ export class RipGrepTool extends BaseDeclarativeTool<
|
|||||||
ToolResult
|
ToolResult
|
||||||
> {
|
> {
|
||||||
static readonly Name = GREP_TOOL_NAME;
|
static readonly Name = GREP_TOOL_NAME;
|
||||||
|
private readonly geminiIgnoreParser: GeminiIgnoreParser;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly config: Config,
|
private readonly config: Config,
|
||||||
@@ -544,6 +555,7 @@ export class RipGrepTool extends BaseDeclarativeTool<
|
|||||||
false, // canUpdateOutput
|
false, // canUpdateOutput
|
||||||
messageBus,
|
messageBus,
|
||||||
);
|
);
|
||||||
|
this.geminiIgnoreParser = new GeminiIgnoreParser(config.getTargetDir());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -580,6 +592,7 @@ export class RipGrepTool extends BaseDeclarativeTool<
|
|||||||
): ToolInvocation<RipGrepToolParams, ToolResult> {
|
): ToolInvocation<RipGrepToolParams, ToolResult> {
|
||||||
return new GrepToolInvocation(
|
return new GrepToolInvocation(
|
||||||
this.config,
|
this.config,
|
||||||
|
this.geminiIgnoreParser,
|
||||||
params,
|
params,
|
||||||
messageBus,
|
messageBus,
|
||||||
_toolName,
|
_toolName,
|
||||||
|
|||||||
@@ -58,6 +58,25 @@ describe('GeminiIgnoreParser', () => {
|
|||||||
false,
|
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', () => {
|
describe('when .geminiignore does not exist', () => {
|
||||||
@@ -66,5 +85,50 @@ describe('GeminiIgnoreParser', () => {
|
|||||||
expect(parser.getPatterns()).toEqual([]);
|
expect(parser.getPatterns()).toEqual([]);
|
||||||
expect(parser.isIgnored('any_file.txt')).toBe(false);
|
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 {
|
export interface GeminiIgnoreFilter {
|
||||||
isIgnored(filePath: string): boolean;
|
isIgnored(filePath: string): boolean;
|
||||||
getPatterns(): string[];
|
getPatterns(): string[];
|
||||||
|
getIgnoreFilePath(): string | null;
|
||||||
|
hasPatterns(): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GeminiIgnoreParser implements GeminiIgnoreFilter {
|
export class GeminiIgnoreParser implements GeminiIgnoreFilter {
|
||||||
@@ -78,4 +80,26 @@ export class GeminiIgnoreParser implements GeminiIgnoreFilter {
|
|||||||
getPatterns(): string[] {
|
getPatterns(): string[] {
|
||||||
return this.patterns;
|
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