mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-24 18:27:01 -07:00
Avoid broad at-command recursive searches
This commit is contained in:
@@ -551,6 +551,65 @@ describe('handleAtCommand', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip recursive glob fallback for bare @ names', async () => {
|
||||
const buildAndExecute = vi.fn().mockResolvedValue({
|
||||
llmContent: 'No files found',
|
||||
returnDisplay: 'No files found',
|
||||
});
|
||||
vi.mocked(mockConfig.getToolRegistry).mockReturnValue({
|
||||
getTool: vi.fn((name: string) =>
|
||||
name === 'glob' ? { buildAndExecute } : undefined,
|
||||
),
|
||||
} as unknown as ToolRegistry);
|
||||
|
||||
const query = 'Ask @teammate for review';
|
||||
|
||||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 134,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(buildAndExecute).not.toHaveBeenCalled();
|
||||
expect(mockOnDebugMessage).toHaveBeenCalledWith(
|
||||
'Path teammate not found directly and does not look like a path. Skipping recursive glob search.',
|
||||
);
|
||||
expect(result).toEqual({
|
||||
processedQuery: [{ text: query }],
|
||||
});
|
||||
});
|
||||
|
||||
it('should not treat @ in the middle of a word as an @ command', async () => {
|
||||
const buildAndExecute = vi.fn().mockResolvedValue({
|
||||
llmContent: 'No files found',
|
||||
returnDisplay: 'No files found',
|
||||
});
|
||||
vi.mocked(mockConfig.getToolRegistry).mockReturnValue({
|
||||
getTool: vi.fn((name: string) =>
|
||||
name === 'glob' ? { buildAndExecute } : undefined,
|
||||
),
|
||||
} as unknown as ToolRegistry);
|
||||
|
||||
const query = 'Connect to user@host before continuing.';
|
||||
|
||||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 135,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(buildAndExecute).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({
|
||||
processedQuery: [{ text: query }],
|
||||
});
|
||||
});
|
||||
|
||||
describe('git-aware filtering', () => {
|
||||
beforeEach(async () => {
|
||||
await fsPromises.mkdir(path.join(testRootDir, '.git'), {
|
||||
|
||||
@@ -61,6 +61,12 @@ export function unescapeLiteralAt(text: string): string {
|
||||
export const AT_COMMAND_PATH_REGEX_SOURCE =
|
||||
'(?:(?:"(?:[^"]*)")|(?:\\\\.|[^ \\t\\n\\r,;!?()\\[\\]{}.]|\\.(?!$|[ \\t\\n\\r])))+';
|
||||
|
||||
/**
|
||||
* An @ reference can start at the beginning of input or after punctuation/space,
|
||||
* but not in the middle of a word such as user@host or hello@file.
|
||||
*/
|
||||
export const AT_COMMAND_START_REGEX_SOURCE = '(?<![\\\\A-Za-z0-9_])@';
|
||||
|
||||
interface HandleAtCommandParams {
|
||||
query: string;
|
||||
config: Config;
|
||||
@@ -94,7 +100,7 @@ function parseAllAtCommands(
|
||||
|
||||
// Create a new RegExp instance for each call to avoid shared state/lastIndex issues.
|
||||
const atCommandRegex = new RegExp(
|
||||
`(?<!\\\\)@${AT_COMMAND_PATH_REGEX_SOURCE}`,
|
||||
`${AT_COMMAND_START_REGEX_SOURCE}${AT_COMMAND_PATH_REGEX_SOURCE}`,
|
||||
'g',
|
||||
);
|
||||
|
||||
@@ -218,6 +224,19 @@ interface IgnoredFile {
|
||||
reason: 'git' | 'gemini' | 'both';
|
||||
}
|
||||
|
||||
function shouldAttemptGlobFallback(pathName: string): boolean {
|
||||
return (
|
||||
pathName.includes('/') ||
|
||||
pathName.includes('\\') ||
|
||||
pathName.includes('.') ||
|
||||
pathName.includes('*') ||
|
||||
pathName.includes('?') ||
|
||||
pathName.includes('[') ||
|
||||
pathName.includes(']') ||
|
||||
pathName.startsWith('~')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves file paths from @ commands, handling globs, recursion, and ignores.
|
||||
*/
|
||||
@@ -304,7 +323,11 @@ async function resolveFilePaths(
|
||||
// We also allow glob fallback for "unauthorized" results from resolveAtCommandPath,
|
||||
// as they might represent a relative path that matched an unauthorized file in one directory
|
||||
// but might have a valid match (via glob) in another.
|
||||
if (config.getEnableRecursiveFileSearch() && globTool) {
|
||||
if (!shouldAttemptGlobFallback(pathName)) {
|
||||
onDebugMessage(
|
||||
`Path ${pathName} not found directly and does not look like a path. Skipping recursive glob search.`,
|
||||
);
|
||||
} else if (config.getEnableRecursiveFileSearch() && globTool) {
|
||||
onDebugMessage(
|
||||
`Path ${pathName} not found directly, attempting glob search.`,
|
||||
);
|
||||
|
||||
@@ -177,9 +177,12 @@ describe('commandUtils', () => {
|
||||
// referenced files are pre-loaded before the query is sent to the model.
|
||||
expect(isAtCommand('check:@file.py')).toBe(true);
|
||||
expect(isAtCommand('analyze(@file.py)')).toBe(true);
|
||||
expect(isAtCommand('hello@file')).toBe(true);
|
||||
expect(isAtCommand('text@path/to/file')).toBe(true);
|
||||
expect(isAtCommand('user@host')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when @ is in the middle of a word', () => {
|
||||
expect(isAtCommand('hello@file')).toBe(false);
|
||||
expect(isAtCommand('text@path/to/file')).toBe(false);
|
||||
expect(isAtCommand('user@host')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when query does not contain any @<path> pattern', () => {
|
||||
|
||||
@@ -10,13 +10,16 @@ import type { SlashCommand } from '../commands/types.js';
|
||||
import fs from 'node:fs';
|
||||
import type { Writable } from 'node:stream';
|
||||
import type { Settings } from '../../config/settingsSchema.js';
|
||||
import { AT_COMMAND_PATH_REGEX_SOURCE } from '../hooks/atCommandProcessor.js';
|
||||
import {
|
||||
AT_COMMAND_PATH_REGEX_SOURCE,
|
||||
AT_COMMAND_START_REGEX_SOURCE,
|
||||
} from '../hooks/atCommandProcessor.js';
|
||||
|
||||
// Pre-compiled regex for detecting @<path> patterns consistent with parseAllAtCommands.
|
||||
// Uses the same AT_COMMAND_PATH_REGEX_SOURCE so that isAtCommand is true whenever
|
||||
// parseAllAtCommands would find at least one atPath part.
|
||||
const AT_COMMAND_DETECT_REGEX = new RegExp(
|
||||
`(?<!\\\\)@${AT_COMMAND_PATH_REGEX_SOURCE}`,
|
||||
`${AT_COMMAND_START_REGEX_SOURCE}${AT_COMMAND_PATH_REGEX_SOURCE}`,
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user