mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
Secondary filter.
This commit is contained in:
@@ -330,9 +330,34 @@ describe('GrepTool', () => {
|
|||||||
|
|
||||||
// Count occurrences of match lines in the output
|
// Count occurrences of match lines in the output
|
||||||
// Matches lines start with L<number>:
|
// Matches lines start with L<number>:
|
||||||
const matches = result.llmContent.match(/^L\d+:.*world/gm);
|
const content =
|
||||||
|
typeof result.llmContent === 'string' ? result.llmContent : '';
|
||||||
|
const matches = content.match(/^L\d+:.*world/gm);
|
||||||
expect(matches?.length).toBe(2);
|
expect(matches?.length).toBe(2);
|
||||||
}, 30000);
|
}, 30000);
|
||||||
|
|
||||||
|
it('should filter files based on the filter parameter', async () => {
|
||||||
|
// fileA.txt contains "hello" and "world"
|
||||||
|
// fileB.js contains "hello" but NOT "world"
|
||||||
|
// sub/fileC.txt contains "world" but NOT "hello"
|
||||||
|
|
||||||
|
const params: GrepToolParams = {
|
||||||
|
pattern: 'hello',
|
||||||
|
filter: 'world',
|
||||||
|
};
|
||||||
|
const invocation = grepTool.build(params);
|
||||||
|
const result = await invocation.execute(abortSignal);
|
||||||
|
|
||||||
|
// Should find matches in fileA.txt because it has both
|
||||||
|
// Should NOT find matches in fileB.js (has hello, missing world)
|
||||||
|
// Should NOT find matches in sub/fileC.txt (missing hello, has world)
|
||||||
|
|
||||||
|
const content =
|
||||||
|
typeof result.llmContent === 'string' ? result.llmContent : '';
|
||||||
|
expect(content).toContain('File: fileA.txt');
|
||||||
|
expect(content).not.toContain('File: fileB.js');
|
||||||
|
expect(content).not.toContain('File: fileC.txt');
|
||||||
|
}, 30000);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('multi-directory workspace', () => {
|
describe('multi-directory workspace', () => {
|
||||||
|
|||||||
+219
-53
@@ -47,6 +47,11 @@ export interface GrepToolParams {
|
|||||||
*/
|
*/
|
||||||
include?: string;
|
include?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional: A string that must be present in the file for it to be included (logical AND).
|
||||||
|
*/
|
||||||
|
filter?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional: Maximum number of matches to return per file.
|
* Optional: Maximum number of matches to return per file.
|
||||||
*/
|
*/
|
||||||
@@ -214,6 +219,7 @@ class GrepToolInvocation extends BaseToolInvocation<
|
|||||||
pattern: this.params.pattern,
|
pattern: this.params.pattern,
|
||||||
path: searchDir,
|
path: searchDir,
|
||||||
include: this.params.include,
|
include: this.params.include,
|
||||||
|
filter: this.params.filter,
|
||||||
maxMatches: remainingLimit,
|
maxMatches: remainingLimit,
|
||||||
maxMatchesPerFile: this.params.max_matches_per_file,
|
maxMatchesPerFile: this.params.max_matches_per_file,
|
||||||
signal: timeoutController.signal,
|
signal: timeoutController.signal,
|
||||||
@@ -343,6 +349,7 @@ class GrepToolInvocation extends BaseToolInvocation<
|
|||||||
pattern: string;
|
pattern: string;
|
||||||
path: string; // Expects absolute path
|
path: string; // Expects absolute path
|
||||||
include?: string;
|
include?: string;
|
||||||
|
filter?: string;
|
||||||
maxMatches: number;
|
maxMatches: number;
|
||||||
maxMatchesPerFile?: number;
|
maxMatchesPerFile?: number;
|
||||||
signal: AbortSignal;
|
signal: AbortSignal;
|
||||||
@@ -351,6 +358,7 @@ class GrepToolInvocation extends BaseToolInvocation<
|
|||||||
pattern,
|
pattern,
|
||||||
path: absolutePath,
|
path: absolutePath,
|
||||||
include,
|
include,
|
||||||
|
filter,
|
||||||
maxMatches,
|
maxMatches,
|
||||||
maxMatchesPerFile,
|
maxMatchesPerFile,
|
||||||
} = options;
|
} = options;
|
||||||
@@ -376,33 +384,116 @@ class GrepToolInvocation extends BaseToolInvocation<
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const generator = execStreaming('git', gitArgs, {
|
// If a filter is provided, first find files matching the filter.
|
||||||
cwd: absolutePath,
|
// This simulates "grep -l 'filter' | xargs grep 'pattern'".
|
||||||
signal: options.signal,
|
if (filter) {
|
||||||
allowedExitCodes: [0, 1],
|
const filterArgs = [
|
||||||
});
|
'grep',
|
||||||
|
'--untracked',
|
||||||
|
'-l', // List filenames only
|
||||||
|
'--ignore-case',
|
||||||
|
'-E', // Extended regex (optional, but consistent)
|
||||||
|
filter,
|
||||||
|
];
|
||||||
|
if (include) {
|
||||||
|
filterArgs.push('--', include);
|
||||||
|
}
|
||||||
|
|
||||||
const results: GrepMatch[] = [];
|
const filesGenerator = execStreaming('git', filterArgs, {
|
||||||
const matchesPerFile = new Map<string, number>();
|
cwd: absolutePath,
|
||||||
|
signal: options.signal,
|
||||||
|
allowedExitCodes: [0, 1],
|
||||||
|
});
|
||||||
|
|
||||||
for await (const line of generator) {
|
const matchingFiles: string[] = [];
|
||||||
const match = this.parseGrepLine(line, absolutePath);
|
for await (const fileLine of filesGenerator) {
|
||||||
if (match) {
|
if (fileLine.trim()) {
|
||||||
if (maxMatchesPerFile) {
|
matchingFiles.push(fileLine.trim());
|
||||||
const count = matchesPerFile.get(match.filePath) || 0;
|
|
||||||
if (count >= maxMatchesPerFile) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
matchesPerFile.set(match.filePath, count + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
results.push(match);
|
|
||||||
if (results.length >= maxMatches) {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (matchingFiles.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have files, search ONLY within those files.
|
||||||
|
// We append the list of files to the main search command.
|
||||||
|
// Note: This could hit command line length limits if matchingFiles is huge.
|
||||||
|
// For now, we assume the filter reduces the set significantly.
|
||||||
|
// git grep expects paths after '--' or just as arguments.
|
||||||
|
// We need to be careful with '--' if include was used.
|
||||||
|
// Actually, if we provide explicit file paths, include glob is irrelevant for the second grep.
|
||||||
|
// So we can drop the include glob and just pass the files.
|
||||||
|
|
||||||
|
// Reset args for the second command
|
||||||
|
const filteredGitArgs = [
|
||||||
|
'grep',
|
||||||
|
'--untracked',
|
||||||
|
'-n',
|
||||||
|
'-E',
|
||||||
|
'--ignore-case',
|
||||||
|
pattern,
|
||||||
|
'--',
|
||||||
|
...matchingFiles,
|
||||||
|
];
|
||||||
|
|
||||||
|
const generator = execStreaming('git', filteredGitArgs, {
|
||||||
|
cwd: absolutePath,
|
||||||
|
signal: options.signal,
|
||||||
|
allowedExitCodes: [0, 1],
|
||||||
|
});
|
||||||
|
|
||||||
|
const results: GrepMatch[] = [];
|
||||||
|
const matchesPerFile = new Map<string, number>();
|
||||||
|
|
||||||
|
for await (const line of generator) {
|
||||||
|
const match = this.parseGrepLine(line, absolutePath);
|
||||||
|
if (match) {
|
||||||
|
if (maxMatchesPerFile) {
|
||||||
|
const count = matchesPerFile.get(match.filePath) || 0;
|
||||||
|
if (count >= maxMatchesPerFile) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
matchesPerFile.set(match.filePath, count + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push(match);
|
||||||
|
if (results.length >= maxMatches) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
} else {
|
||||||
|
// Normal execution without filter
|
||||||
|
const generator = execStreaming('git', gitArgs, {
|
||||||
|
cwd: absolutePath,
|
||||||
|
signal: options.signal,
|
||||||
|
allowedExitCodes: [0, 1],
|
||||||
|
});
|
||||||
|
|
||||||
|
const results: GrepMatch[] = [];
|
||||||
|
const matchesPerFile = new Map<string, number>();
|
||||||
|
|
||||||
|
for await (const line of generator) {
|
||||||
|
const match = this.parseGrepLine(line, absolutePath);
|
||||||
|
if (match) {
|
||||||
|
if (maxMatchesPerFile) {
|
||||||
|
const count = matchesPerFile.get(match.filePath) || 0;
|
||||||
|
if (count >= maxMatchesPerFile) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
matchesPerFile.set(match.filePath, count + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push(match);
|
||||||
|
if (results.length >= maxMatches) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
return results;
|
|
||||||
} catch (gitError: unknown) {
|
} catch (gitError: unknown) {
|
||||||
debugLogger.debug(
|
debugLogger.debug(
|
||||||
`GrepLogic: git grep failed: ${getErrorMessage(
|
`GrepLogic: git grep failed: ${getErrorMessage(
|
||||||
@@ -443,47 +534,115 @@ class GrepToolInvocation extends BaseToolInvocation<
|
|||||||
})
|
})
|
||||||
.filter((dir): dir is string => !!dir);
|
.filter((dir): dir is string => !!dir);
|
||||||
commonExcludes.forEach((dir) => grepArgs.push(`--exclude-dir=${dir}`));
|
commonExcludes.forEach((dir) => grepArgs.push(`--exclude-dir=${dir}`));
|
||||||
if (include) {
|
|
||||||
grepArgs.push(`--include=${include}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maxMatchesPerFile) {
|
// If filter is present, we first find matching files using grep -l
|
||||||
grepArgs.push(`-m`, maxMatchesPerFile.toString());
|
if (filter) {
|
||||||
}
|
const filterArgs = ['-r', '-l', '-E', '-I'];
|
||||||
|
commonExcludes.forEach((dir) =>
|
||||||
|
filterArgs.push(`--exclude-dir=${dir}`),
|
||||||
|
);
|
||||||
|
if (include) {
|
||||||
|
filterArgs.push(`--include=${include}`);
|
||||||
|
}
|
||||||
|
filterArgs.push(filter);
|
||||||
|
filterArgs.push('.');
|
||||||
|
|
||||||
grepArgs.push(pattern);
|
const filesGenerator = execStreaming('grep', filterArgs, {
|
||||||
grepArgs.push('.');
|
|
||||||
|
|
||||||
const results: GrepMatch[] = [];
|
|
||||||
try {
|
|
||||||
const generator = execStreaming('grep', grepArgs, {
|
|
||||||
cwd: absolutePath,
|
cwd: absolutePath,
|
||||||
signal: options.signal,
|
signal: options.signal,
|
||||||
allowedExitCodes: [0, 1],
|
allowedExitCodes: [0, 1],
|
||||||
});
|
});
|
||||||
|
|
||||||
for await (const line of generator) {
|
const matchingFiles: string[] = [];
|
||||||
const match = this.parseGrepLine(line, absolutePath);
|
for await (const fileLine of filesGenerator) {
|
||||||
if (match) {
|
if (fileLine.trim()) {
|
||||||
results.push(match);
|
matchingFiles.push(fileLine.trim());
|
||||||
if (results.length >= maxMatches) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return results;
|
|
||||||
} catch (grepError: unknown) {
|
if (matchingFiles.length === 0) {
|
||||||
if (
|
return [];
|
||||||
grepError instanceof Error &&
|
}
|
||||||
/Permission denied|Is a directory/i.test(grepError.message)
|
|
||||||
) {
|
// Search within the filtered files
|
||||||
return results;
|
const filteredGrepArgs = ['-n', '-H', '-E', '-I'];
|
||||||
|
if (maxMatchesPerFile) {
|
||||||
|
filteredGrepArgs.push(`-m`, maxMatchesPerFile.toString());
|
||||||
|
}
|
||||||
|
filteredGrepArgs.push(pattern);
|
||||||
|
filteredGrepArgs.push(...matchingFiles);
|
||||||
|
|
||||||
|
const results: GrepMatch[] = [];
|
||||||
|
// execStreaming will handle potentially large arg list by failing if too large,
|
||||||
|
// but for now we assume it fits or rely on fallback.
|
||||||
|
// Ideally we'd chunk it.
|
||||||
|
try {
|
||||||
|
const generator = execStreaming('grep', filteredGrepArgs, {
|
||||||
|
cwd: absolutePath,
|
||||||
|
signal: options.signal,
|
||||||
|
allowedExitCodes: [0, 1],
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const line of generator) {
|
||||||
|
const match = this.parseGrepLine(line, absolutePath);
|
||||||
|
if (match) {
|
||||||
|
results.push(match);
|
||||||
|
if (results.length >= maxMatches) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
} catch (e) {
|
||||||
|
debugLogger.debug(
|
||||||
|
`GrepLogic: System grep (second pass) failed: ${getErrorMessage(e)}`,
|
||||||
|
);
|
||||||
|
// Fallback to JS if second pass fails (e.g. arg list too long)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Normal system grep execution
|
||||||
|
if (include) {
|
||||||
|
grepArgs.push(`--include=${include}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxMatchesPerFile) {
|
||||||
|
grepArgs.push(`-m`, maxMatchesPerFile.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
grepArgs.push(pattern);
|
||||||
|
grepArgs.push('.');
|
||||||
|
|
||||||
|
const results: GrepMatch[] = [];
|
||||||
|
try {
|
||||||
|
const generator = execStreaming('grep', grepArgs, {
|
||||||
|
cwd: absolutePath,
|
||||||
|
signal: options.signal,
|
||||||
|
allowedExitCodes: [0, 1],
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const line of generator) {
|
||||||
|
const match = this.parseGrepLine(line, absolutePath);
|
||||||
|
if (match) {
|
||||||
|
results.push(match);
|
||||||
|
if (results.length >= maxMatches) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
} catch (grepError: unknown) {
|
||||||
|
if (
|
||||||
|
grepError instanceof Error &&
|
||||||
|
/Permission denied|Is a directory/i.test(grepError.message)
|
||||||
|
) {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
debugLogger.debug(
|
||||||
|
`GrepLogic: System grep failed: ${getErrorMessage(
|
||||||
|
grepError,
|
||||||
|
)}. Falling back...`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
debugLogger.debug(
|
|
||||||
`GrepLogic: System grep failed: ${getErrorMessage(
|
|
||||||
grepError,
|
|
||||||
)}. Falling back...`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,6 +664,7 @@ class GrepToolInvocation extends BaseToolInvocation<
|
|||||||
});
|
});
|
||||||
|
|
||||||
const regex = new RegExp(pattern, 'i');
|
const regex = new RegExp(pattern, 'i');
|
||||||
|
const filterRegex = filter ? new RegExp(filter, 'i') : null;
|
||||||
const allMatches: GrepMatch[] = [];
|
const allMatches: GrepMatch[] = [];
|
||||||
|
|
||||||
for await (const filePath of filesStream) {
|
for await (const filePath of filesStream) {
|
||||||
@@ -521,6 +681,12 @@ class GrepToolInvocation extends BaseToolInvocation<
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const content = await fsPromises.readFile(fileAbsolutePath, 'utf8');
|
const content = await fsPromises.readFile(fileAbsolutePath, 'utf8');
|
||||||
|
|
||||||
|
// Check filter first if present
|
||||||
|
if (filterRegex && !filterRegex.test(content)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const lines = content.split(/\r?\n/);
|
const lines = content.split(/\r?\n/);
|
||||||
let fileMatchCount = 0;
|
let fileMatchCount = 0;
|
||||||
for (let index = 0; index < lines.length; index++) {
|
for (let index = 0; index < lines.length; index++) {
|
||||||
|
|||||||
Reference in New Issue
Block a user