mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 08:47:44 +00:00
feat: full implementation for .geminiignore in settings and respective tool calls (#3727)
This commit is contained in:
@@ -21,6 +21,11 @@ const mockConfig = {
|
||||
isSandboxed: vi.fn(() => false),
|
||||
getFileService: vi.fn(),
|
||||
getFileFilteringRespectGitIgnore: vi.fn(() => true),
|
||||
getFileFilteringRespectGeminiIgnore: vi.fn(() => true),
|
||||
getFileFilteringOptions: vi.fn(() => ({
|
||||
respectGitIgnore: true,
|
||||
respectGeminiIgnore: true,
|
||||
})),
|
||||
getEnableRecursiveFileSearch: vi.fn(() => true),
|
||||
} as unknown as Config;
|
||||
|
||||
@@ -171,7 +176,13 @@ describe('handleAtCommand', () => {
|
||||
125,
|
||||
);
|
||||
expect(mockReadManyFilesExecute).toHaveBeenCalledWith(
|
||||
{ paths: [filePath], respect_git_ignore: true },
|
||||
{
|
||||
paths: [filePath],
|
||||
file_filtering_options: {
|
||||
respect_git_ignore: true,
|
||||
respect_gemini_ignore: true,
|
||||
},
|
||||
},
|
||||
abortController.signal,
|
||||
);
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
@@ -217,7 +228,13 @@ describe('handleAtCommand', () => {
|
||||
126,
|
||||
);
|
||||
expect(mockReadManyFilesExecute).toHaveBeenCalledWith(
|
||||
{ paths: [resolvedGlob], respect_git_ignore: true },
|
||||
{
|
||||
paths: [resolvedGlob],
|
||||
file_filtering_options: {
|
||||
respect_git_ignore: true,
|
||||
respect_gemini_ignore: true,
|
||||
},
|
||||
},
|
||||
abortController.signal,
|
||||
);
|
||||
expect(mockOnDebugMessage).toHaveBeenCalledWith(
|
||||
@@ -318,7 +335,13 @@ describe('handleAtCommand', () => {
|
||||
signal: abortController.signal,
|
||||
});
|
||||
expect(mockReadManyFilesExecute).toHaveBeenCalledWith(
|
||||
{ paths: [unescapedPath], respect_git_ignore: true },
|
||||
{
|
||||
paths: [unescapedPath],
|
||||
file_filtering_options: {
|
||||
respect_git_ignore: true,
|
||||
respect_gemini_ignore: true,
|
||||
},
|
||||
},
|
||||
abortController.signal,
|
||||
);
|
||||
});
|
||||
@@ -347,7 +370,13 @@ describe('handleAtCommand', () => {
|
||||
signal: abortController.signal,
|
||||
});
|
||||
expect(mockReadManyFilesExecute).toHaveBeenCalledWith(
|
||||
{ paths: [file1, file2], respect_git_ignore: true },
|
||||
{
|
||||
paths: [file1, file2],
|
||||
file_filtering_options: {
|
||||
respect_git_ignore: true,
|
||||
respect_gemini_ignore: true,
|
||||
},
|
||||
},
|
||||
abortController.signal,
|
||||
);
|
||||
expect(result.processedQuery).toEqual([
|
||||
@@ -389,7 +418,13 @@ describe('handleAtCommand', () => {
|
||||
signal: abortController.signal,
|
||||
});
|
||||
expect(mockReadManyFilesExecute).toHaveBeenCalledWith(
|
||||
{ paths: [file1, file2], respect_git_ignore: true },
|
||||
{
|
||||
paths: [file1, file2],
|
||||
file_filtering_options: {
|
||||
respect_git_ignore: true,
|
||||
respect_gemini_ignore: true,
|
||||
},
|
||||
},
|
||||
abortController.signal,
|
||||
);
|
||||
expect(result.processedQuery).toEqual([
|
||||
@@ -454,7 +489,13 @@ describe('handleAtCommand', () => {
|
||||
});
|
||||
|
||||
expect(mockReadManyFilesExecute).toHaveBeenCalledWith(
|
||||
{ paths: [file1, resolvedFile2], respect_git_ignore: true },
|
||||
{
|
||||
paths: [file1, resolvedFile2],
|
||||
file_filtering_options: {
|
||||
respect_git_ignore: true,
|
||||
respect_gemini_ignore: true,
|
||||
},
|
||||
},
|
||||
abortController.signal,
|
||||
);
|
||||
expect(result.processedQuery).toEqual([
|
||||
@@ -556,7 +597,13 @@ describe('handleAtCommand', () => {
|
||||
// If the mock is simpler, it might use queryPath if stat(queryPath) succeeds.
|
||||
// The most important part is that *some* version of the path that leads to the content is used.
|
||||
// Let's assume it uses the path from the query if stat confirms it exists (even if different case on disk)
|
||||
{ paths: [queryPath], respect_git_ignore: true },
|
||||
{
|
||||
paths: [queryPath],
|
||||
file_filtering_options: {
|
||||
respect_git_ignore: true,
|
||||
respect_gemini_ignore: true,
|
||||
},
|
||||
},
|
||||
abortController.signal,
|
||||
);
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
@@ -583,8 +630,18 @@ describe('handleAtCommand', () => {
|
||||
|
||||
// Mock the file discovery service to report this file as git-ignored
|
||||
mockFileDiscoveryService.shouldIgnoreFile.mockImplementation(
|
||||
(path: string, options?: { respectGitIgnore?: boolean }) =>
|
||||
path === gitIgnoredFile && options?.respectGitIgnore !== false,
|
||||
(
|
||||
path: string,
|
||||
options?: {
|
||||
respectGitIgnore?: boolean;
|
||||
respectGeminiIgnore?: boolean;
|
||||
},
|
||||
) => {
|
||||
if (path !== gitIgnoredFile) return false;
|
||||
if (options?.respectGitIgnore) return true;
|
||||
if (options?.respectGeminiIgnore) return false;
|
||||
return false;
|
||||
},
|
||||
);
|
||||
|
||||
const result = await handleAtCommand({
|
||||
@@ -596,15 +653,24 @@ describe('handleAtCommand', () => {
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
// Should be called twice - once for git ignore check and once for gemini ignore check
|
||||
expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledTimes(
|
||||
2,
|
||||
);
|
||||
expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
|
||||
gitIgnoredFile,
|
||||
{ respectGitIgnore: true },
|
||||
{ respectGitIgnore: true, respectGeminiIgnore: false },
|
||||
);
|
||||
expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
|
||||
gitIgnoredFile,
|
||||
{ respectGitIgnore: false, respectGeminiIgnore: true },
|
||||
);
|
||||
|
||||
expect(mockOnDebugMessage).toHaveBeenCalledWith(
|
||||
`Path ${gitIgnoredFile} is git-ignored and will be skipped.`,
|
||||
);
|
||||
expect(mockOnDebugMessage).toHaveBeenCalledWith(
|
||||
'Ignored 1 git-ignored files: node_modules/package.json',
|
||||
'Ignored 1 files:\nGit-ignored: node_modules/package.json',
|
||||
);
|
||||
expect(mockReadManyFilesExecute).not.toHaveBeenCalled();
|
||||
expect(result.processedQuery).toEqual([{ text: query }]);
|
||||
@@ -616,7 +682,15 @@ describe('handleAtCommand', () => {
|
||||
const query = `@${validFile}`;
|
||||
const fileContent = 'console.log("Hello world");';
|
||||
|
||||
mockFileDiscoveryService.shouldIgnoreFile.mockReturnValue(false);
|
||||
mockFileDiscoveryService.shouldIgnoreFile.mockImplementation(
|
||||
(
|
||||
_path: string,
|
||||
_options?: {
|
||||
respectGitIgnore?: boolean;
|
||||
respectGeminiIgnore?: boolean;
|
||||
},
|
||||
) => false,
|
||||
);
|
||||
mockReadManyFilesExecute.mockResolvedValue({
|
||||
llmContent: [`--- ${validFile} ---\n\n${fileContent}\n\n`],
|
||||
returnDisplay: 'Read 1 file.',
|
||||
@@ -631,12 +705,26 @@ describe('handleAtCommand', () => {
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
// Should be called twice - once for git ignore check and once for gemini ignore check
|
||||
expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledTimes(
|
||||
2,
|
||||
);
|
||||
expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
|
||||
validFile,
|
||||
{ respectGitIgnore: true },
|
||||
{ respectGitIgnore: true, respectGeminiIgnore: false },
|
||||
);
|
||||
expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
|
||||
validFile,
|
||||
{ respectGitIgnore: false, respectGeminiIgnore: true },
|
||||
);
|
||||
expect(mockReadManyFilesExecute).toHaveBeenCalledWith(
|
||||
{ paths: [validFile], respect_git_ignore: true },
|
||||
{
|
||||
paths: [validFile],
|
||||
file_filtering_options: {
|
||||
respect_git_ignore: true,
|
||||
respect_gemini_ignore: true,
|
||||
},
|
||||
},
|
||||
abortController.signal,
|
||||
);
|
||||
expect(result.processedQuery).toEqual([
|
||||
@@ -656,8 +744,21 @@ describe('handleAtCommand', () => {
|
||||
const fileContent = '# Project README';
|
||||
|
||||
mockFileDiscoveryService.shouldIgnoreFile.mockImplementation(
|
||||
(path: string, options?: { respectGitIgnore?: boolean }) =>
|
||||
path === gitIgnoredFile && options?.respectGitIgnore !== false,
|
||||
(
|
||||
path: string,
|
||||
options?: {
|
||||
respectGitIgnore?: boolean;
|
||||
respectGeminiIgnore?: boolean;
|
||||
},
|
||||
) => {
|
||||
if (path === gitIgnoredFile && options?.respectGitIgnore) {
|
||||
return true;
|
||||
}
|
||||
if (options?.respectGeminiIgnore) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
);
|
||||
mockReadManyFilesExecute.mockResolvedValue({
|
||||
llmContent: [`--- ${validFile} ---\n\n${fileContent}\n\n`],
|
||||
@@ -673,22 +774,40 @@ describe('handleAtCommand', () => {
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
// Should be called twice for each file - once for git ignore check and once for gemini ignore check
|
||||
expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledTimes(
|
||||
4,
|
||||
);
|
||||
expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
|
||||
validFile,
|
||||
{ respectGitIgnore: true },
|
||||
{ respectGitIgnore: true, respectGeminiIgnore: false },
|
||||
);
|
||||
expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
|
||||
validFile,
|
||||
{ respectGitIgnore: false, respectGeminiIgnore: true },
|
||||
);
|
||||
expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
|
||||
gitIgnoredFile,
|
||||
{ respectGitIgnore: true },
|
||||
{ respectGitIgnore: true, respectGeminiIgnore: false },
|
||||
);
|
||||
expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
|
||||
gitIgnoredFile,
|
||||
{ respectGitIgnore: false, respectGeminiIgnore: true },
|
||||
);
|
||||
expect(mockOnDebugMessage).toHaveBeenCalledWith(
|
||||
`Path ${gitIgnoredFile} is git-ignored and will be skipped.`,
|
||||
);
|
||||
expect(mockOnDebugMessage).toHaveBeenCalledWith(
|
||||
'Ignored 1 git-ignored files: .env',
|
||||
'Ignored 1 files:\nGit-ignored: .env',
|
||||
);
|
||||
expect(mockReadManyFilesExecute).toHaveBeenCalledWith(
|
||||
{ paths: [validFile], respect_git_ignore: true },
|
||||
{
|
||||
paths: [validFile],
|
||||
file_filtering_options: {
|
||||
respect_git_ignore: true,
|
||||
respect_gemini_ignore: true,
|
||||
},
|
||||
},
|
||||
abortController.signal,
|
||||
);
|
||||
expect(result.processedQuery).toEqual([
|
||||
@@ -705,7 +824,16 @@ describe('handleAtCommand', () => {
|
||||
const gitFile = '.git/config';
|
||||
const query = `@${gitFile}`;
|
||||
|
||||
mockFileDiscoveryService.shouldIgnoreFile.mockReturnValue(true);
|
||||
// Mock to return true for git ignore check, false for gemini ignore check
|
||||
mockFileDiscoveryService.shouldIgnoreFile.mockImplementation(
|
||||
(
|
||||
_path: string,
|
||||
options?: {
|
||||
respectGitIgnore?: boolean;
|
||||
respectGeminiIgnore?: boolean;
|
||||
},
|
||||
) => options?.respectGitIgnore === true,
|
||||
);
|
||||
|
||||
const result = await handleAtCommand({
|
||||
query,
|
||||
@@ -716,13 +844,24 @@ describe('handleAtCommand', () => {
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
// Should be called twice - once for git ignore check and once for gemini ignore check
|
||||
expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledTimes(
|
||||
2,
|
||||
);
|
||||
expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
|
||||
gitFile,
|
||||
{ respectGitIgnore: true },
|
||||
{ respectGitIgnore: true, respectGeminiIgnore: false },
|
||||
);
|
||||
expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
|
||||
gitFile,
|
||||
{ respectGitIgnore: false, respectGeminiIgnore: true },
|
||||
);
|
||||
expect(mockOnDebugMessage).toHaveBeenCalledWith(
|
||||
`Path ${gitFile} is git-ignored and will be skipped.`,
|
||||
);
|
||||
expect(mockOnDebugMessage).toHaveBeenCalledWith(
|
||||
'Ignored 1 files:\nGit-ignored: .git/config',
|
||||
);
|
||||
expect(mockReadManyFilesExecute).not.toHaveBeenCalled();
|
||||
expect(result.processedQuery).toEqual([{ text: query }]);
|
||||
expect(result.shouldProceed).toBe(true);
|
||||
@@ -759,4 +898,208 @@ describe('handleAtCommand', () => {
|
||||
expect(result.shouldProceed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('gemini-ignore filtering', () => {
|
||||
it('should skip gemini-ignored files in @ commands', async () => {
|
||||
const geminiIgnoredFile = 'build/output.js';
|
||||
const query = `@${geminiIgnoredFile}`;
|
||||
|
||||
// Mock the file discovery service to report this file as gemini-ignored
|
||||
mockFileDiscoveryService.shouldIgnoreFile.mockImplementation(
|
||||
(
|
||||
path: string,
|
||||
options?: {
|
||||
respectGitIgnore?: boolean;
|
||||
respectGeminiIgnore?: boolean;
|
||||
},
|
||||
) => {
|
||||
if (path !== geminiIgnoredFile) return false;
|
||||
if (options?.respectGeminiIgnore) return true;
|
||||
if (options?.respectGitIgnore) return false;
|
||||
return false;
|
||||
},
|
||||
);
|
||||
|
||||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 204,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
// Should be called twice - once for git ignore check and once for gemini ignore check
|
||||
expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledTimes(
|
||||
2,
|
||||
);
|
||||
expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
|
||||
geminiIgnoredFile,
|
||||
{ respectGitIgnore: true, respectGeminiIgnore: false },
|
||||
);
|
||||
expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
|
||||
geminiIgnoredFile,
|
||||
{ respectGitIgnore: false, respectGeminiIgnore: true },
|
||||
);
|
||||
|
||||
expect(mockOnDebugMessage).toHaveBeenCalledWith(
|
||||
`Path ${geminiIgnoredFile} is gemini-ignored and will be skipped.`,
|
||||
);
|
||||
expect(mockOnDebugMessage).toHaveBeenCalledWith(
|
||||
'No valid file paths found in @ commands to read.',
|
||||
);
|
||||
expect(mockOnDebugMessage).toHaveBeenCalledWith(
|
||||
'Ignored 1 files:\nGemini-ignored: build/output.js',
|
||||
);
|
||||
expect(mockReadManyFilesExecute).not.toHaveBeenCalled();
|
||||
expect(result.processedQuery).toEqual([{ text: query }]);
|
||||
expect(result.shouldProceed).toBe(true);
|
||||
});
|
||||
|
||||
it('should process non-ignored files when .geminiignore is present', async () => {
|
||||
const validFile = 'src/index.ts';
|
||||
const query = `@${validFile}`;
|
||||
const fileContent = 'console.log("Hello world")';
|
||||
|
||||
mockFileDiscoveryService.shouldIgnoreFile.mockImplementation(
|
||||
(
|
||||
_path: string,
|
||||
_options?: {
|
||||
respectGitIgnore?: boolean;
|
||||
respectGeminiIgnore?: boolean;
|
||||
},
|
||||
) => false,
|
||||
);
|
||||
mockReadManyFilesExecute.mockResolvedValue({
|
||||
llmContent: [`--- ${validFile} ---\n\n${fileContent}\n\n`],
|
||||
returnDisplay: 'Read 1 file.',
|
||||
});
|
||||
|
||||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 205,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
// Should be called twice - once for git ignore check and once for gemini ignore check
|
||||
expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledTimes(
|
||||
2,
|
||||
);
|
||||
expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
|
||||
validFile,
|
||||
{ respectGitIgnore: true, respectGeminiIgnore: false },
|
||||
);
|
||||
expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
|
||||
validFile,
|
||||
{ respectGitIgnore: false, respectGeminiIgnore: true },
|
||||
);
|
||||
|
||||
expect(mockReadManyFilesExecute).toHaveBeenCalledWith(
|
||||
{
|
||||
paths: [validFile],
|
||||
file_filtering_options: {
|
||||
respect_git_ignore: true,
|
||||
respect_gemini_ignore: true,
|
||||
},
|
||||
},
|
||||
abortController.signal,
|
||||
);
|
||||
expect(result.processedQuery).toEqual([
|
||||
{ text: `@${validFile}` },
|
||||
{ text: '\n--- Content from referenced files ---' },
|
||||
{ text: `\nContent from @${validFile}:\n` },
|
||||
{ text: fileContent },
|
||||
{ text: '\n--- End of content ---' },
|
||||
]);
|
||||
expect(result.shouldProceed).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle mixed gemini-ignored and valid files', async () => {
|
||||
const validFile = 'src/main.ts';
|
||||
const geminiIgnoredFile = 'dist/bundle.js';
|
||||
const query = `@${validFile} @${geminiIgnoredFile}`;
|
||||
const fileContent = '// Main application entry';
|
||||
|
||||
mockFileDiscoveryService.shouldIgnoreFile.mockImplementation(
|
||||
(
|
||||
path: string,
|
||||
options?: {
|
||||
respectGitIgnore?: boolean;
|
||||
respectGeminiIgnore?: boolean;
|
||||
},
|
||||
) => {
|
||||
if (path === geminiIgnoredFile && options?.respectGeminiIgnore) {
|
||||
return true;
|
||||
}
|
||||
if (options?.respectGitIgnore) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
);
|
||||
mockReadManyFilesExecute.mockResolvedValue({
|
||||
llmContent: [`--- ${validFile} ---\n\n${fileContent}\n\n`],
|
||||
returnDisplay: 'Read 1 file.',
|
||||
});
|
||||
|
||||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 206,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
// Should be called twice for each file - once for git ignore check and once for gemini ignore check
|
||||
expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledTimes(
|
||||
4,
|
||||
);
|
||||
|
||||
// Verify both files were checked against both ignore types
|
||||
[validFile, geminiIgnoredFile].forEach((file) => {
|
||||
expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
|
||||
file,
|
||||
{ respectGitIgnore: true, respectGeminiIgnore: false },
|
||||
);
|
||||
expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
|
||||
file,
|
||||
{ respectGitIgnore: false, respectGeminiIgnore: true },
|
||||
);
|
||||
});
|
||||
|
||||
expect(mockOnDebugMessage).toHaveBeenCalledWith(
|
||||
`Path ${validFile} resolved to file: ${validFile}`,
|
||||
);
|
||||
expect(mockOnDebugMessage).toHaveBeenCalledWith(
|
||||
`Path ${geminiIgnoredFile} is gemini-ignored and will be skipped.`,
|
||||
);
|
||||
expect(mockOnDebugMessage).toHaveBeenCalledWith(
|
||||
'Ignored 1 files:\nGemini-ignored: dist/bundle.js',
|
||||
);
|
||||
|
||||
expect(mockReadManyFilesExecute).toHaveBeenCalledWith(
|
||||
{
|
||||
paths: [validFile],
|
||||
file_filtering_options: {
|
||||
respect_git_ignore: true,
|
||||
respect_gemini_ignore: true,
|
||||
},
|
||||
},
|
||||
abortController.signal,
|
||||
);
|
||||
|
||||
expect(result.processedQuery).toEqual([
|
||||
{ text: `@${validFile} @${geminiIgnoredFile}` },
|
||||
{ text: '\n--- Content from referenced files ---' },
|
||||
{ text: `\nContent from @${validFile}:\n` },
|
||||
{ text: fileContent },
|
||||
{ text: '\n--- End of content ---' },
|
||||
]);
|
||||
expect(result.shouldProceed).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -136,12 +136,17 @@ export async function handleAtCommand({
|
||||
|
||||
// Get centralized file discovery service
|
||||
const fileDiscovery = config.getFileService();
|
||||
const respectGitIgnore = config.getFileFilteringRespectGitIgnore();
|
||||
|
||||
const respectFileIgnore = config.getFileFilteringOptions();
|
||||
|
||||
const pathSpecsToRead: string[] = [];
|
||||
const atPathToResolvedSpecMap = new Map<string, string>();
|
||||
const contentLabelsForDisplay: string[] = [];
|
||||
const ignoredPaths: string[] = [];
|
||||
const ignoredByReason: Record<string, string[]> = {
|
||||
git: [],
|
||||
gemini: [],
|
||||
both: [],
|
||||
};
|
||||
|
||||
const toolRegistry = await config.getToolRegistry();
|
||||
const readManyFilesTool = toolRegistry.getTool('read_many_files');
|
||||
@@ -182,10 +187,31 @@ export async function handleAtCommand({
|
||||
}
|
||||
|
||||
// Check if path should be ignored based on filtering options
|
||||
if (fileDiscovery.shouldIgnoreFile(pathName, { respectGitIgnore })) {
|
||||
const reason = respectGitIgnore ? 'git-ignored' : 'custom-ignored';
|
||||
onDebugMessage(`Path ${pathName} is ${reason} and will be skipped.`);
|
||||
ignoredPaths.push(pathName);
|
||||
|
||||
const gitIgnored =
|
||||
respectFileIgnore.respectGitIgnore &&
|
||||
fileDiscovery.shouldIgnoreFile(pathName, {
|
||||
respectGitIgnore: true,
|
||||
respectGeminiIgnore: false,
|
||||
});
|
||||
const geminiIgnored =
|
||||
respectFileIgnore.respectGeminiIgnore &&
|
||||
fileDiscovery.shouldIgnoreFile(pathName, {
|
||||
respectGitIgnore: false,
|
||||
respectGeminiIgnore: true,
|
||||
});
|
||||
|
||||
if (gitIgnored || geminiIgnored) {
|
||||
const reason =
|
||||
gitIgnored && geminiIgnored ? 'both' : gitIgnored ? 'git' : 'gemini';
|
||||
ignoredByReason[reason].push(pathName);
|
||||
const reasonText =
|
||||
reason === 'both'
|
||||
? 'ignored by both git and gemini'
|
||||
: reason === 'git'
|
||||
? 'git-ignored'
|
||||
: 'gemini-ignored';
|
||||
onDebugMessage(`Path ${pathName} is ${reasonText} and will be skipped.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -319,11 +345,26 @@ export async function handleAtCommand({
|
||||
initialQueryText = initialQueryText.trim();
|
||||
|
||||
// Inform user about ignored paths
|
||||
if (ignoredPaths.length > 0) {
|
||||
const ignoreType = respectGitIgnore ? 'git-ignored' : 'custom-ignored';
|
||||
onDebugMessage(
|
||||
`Ignored ${ignoredPaths.length} ${ignoreType} files: ${ignoredPaths.join(', ')}`,
|
||||
);
|
||||
const totalIgnored =
|
||||
ignoredByReason.git.length +
|
||||
ignoredByReason.gemini.length +
|
||||
ignoredByReason.both.length;
|
||||
|
||||
if (totalIgnored > 0) {
|
||||
const messages = [];
|
||||
if (ignoredByReason.git.length) {
|
||||
messages.push(`Git-ignored: ${ignoredByReason.git.join(', ')}`);
|
||||
}
|
||||
if (ignoredByReason.gemini.length) {
|
||||
messages.push(`Gemini-ignored: ${ignoredByReason.gemini.join(', ')}`);
|
||||
}
|
||||
if (ignoredByReason.both.length) {
|
||||
messages.push(`Ignored by both: ${ignoredByReason.both.join(', ')}`);
|
||||
}
|
||||
|
||||
const message = `Ignored ${totalIgnored} files:\n${messages.join('\n')}`;
|
||||
console.log(message);
|
||||
onDebugMessage(message);
|
||||
}
|
||||
|
||||
// Fallback for lone "@" or completely invalid @-commands resulting in empty initialQueryText
|
||||
@@ -347,7 +388,11 @@ export async function handleAtCommand({
|
||||
|
||||
const toolArgs = {
|
||||
paths: pathSpecsToRead,
|
||||
respect_git_ignore: respectGitIgnore, // Use configuration setting
|
||||
file_filtering_options: {
|
||||
respect_git_ignore: respectFileIgnore.respectGitIgnore,
|
||||
respect_gemini_ignore: respectFileIgnore.respectGeminiIgnore,
|
||||
},
|
||||
// Use configuration setting
|
||||
};
|
||||
let toolCallDisplay: IndividualToolCallDisplay;
|
||||
|
||||
|
||||
@@ -14,7 +14,10 @@ import { CommandContext, SlashCommand } from '../commands/types.js';
|
||||
import { Config, FileDiscoveryService } from '@google/gemini-cli-core';
|
||||
|
||||
interface MockConfig {
|
||||
getFileFilteringRespectGitIgnore: () => boolean;
|
||||
getFileFilteringOptions: () => {
|
||||
respectGitIgnore: boolean;
|
||||
respectGeminiIgnore: boolean;
|
||||
};
|
||||
getEnableRecursiveFileSearch: () => boolean;
|
||||
getFileService: () => FileDiscoveryService | null;
|
||||
}
|
||||
@@ -118,12 +121,16 @@ describe('useCompletion git-aware filtering integration', () => {
|
||||
projectRoot: '',
|
||||
gitIgnoreFilter: null,
|
||||
geminiIgnoreFilter: null,
|
||||
isFileIgnored: vi.fn(),
|
||||
} as unknown as Mocked<FileDiscoveryService>;
|
||||
|
||||
mockConfig = {
|
||||
getFileFilteringRespectGitIgnore: vi.fn(() => true),
|
||||
getFileService: vi.fn().mockReturnValue(mockFileDiscoveryService),
|
||||
getFileFilteringOptions: vi.fn(() => ({
|
||||
respectGitIgnore: true,
|
||||
respectGeminiIgnore: true,
|
||||
})),
|
||||
getEnableRecursiveFileSearch: vi.fn(() => true),
|
||||
getFileService: vi.fn(() => mockFileDiscoveryService),
|
||||
};
|
||||
|
||||
vi.mocked(FileDiscoveryService).mockImplementation(
|
||||
@@ -186,7 +193,7 @@ describe('useCompletion git-aware filtering integration', () => {
|
||||
{ name: '.env', isDirectory: () => false },
|
||||
] as unknown as Awaited<ReturnType<typeof fs.readdir>>);
|
||||
|
||||
// Mock git ignore service to ignore certain files
|
||||
// Mock ignore service to ignore certain files
|
||||
mockFileDiscoveryService.shouldGitIgnoreFile.mockImplementation(
|
||||
(path: string) =>
|
||||
path.includes('node_modules') ||
|
||||
@@ -195,8 +202,17 @@ describe('useCompletion git-aware filtering integration', () => {
|
||||
);
|
||||
mockFileDiscoveryService.shouldIgnoreFile.mockImplementation(
|
||||
(path: string, options) => {
|
||||
if (options?.respectGitIgnore !== false) {
|
||||
return mockFileDiscoveryService.shouldGitIgnoreFile(path);
|
||||
if (
|
||||
options?.respectGitIgnore &&
|
||||
mockFileDiscoveryService.shouldGitIgnoreFile(path)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
options?.respectGeminiIgnore &&
|
||||
mockFileDiscoveryService.shouldGeminiIgnoreFile
|
||||
) {
|
||||
return mockFileDiscoveryService.shouldGeminiIgnoreFile(path);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
@@ -231,38 +247,54 @@ describe('useCompletion git-aware filtering integration', () => {
|
||||
it('should handle recursive search with git-aware filtering', async () => {
|
||||
// Mock the recursive file search scenario
|
||||
vi.mocked(fs.readdir).mockImplementation(
|
||||
async (dirPath: string | Buffer | URL) => {
|
||||
if (dirPath === testCwd) {
|
||||
return [
|
||||
{ name: 'src', isDirectory: () => true },
|
||||
{ name: 'node_modules', isDirectory: () => true },
|
||||
{ name: 'temp', isDirectory: () => true },
|
||||
] as Array<{ name: string; isDirectory: () => boolean }>;
|
||||
async (
|
||||
dirPath: string | Buffer | URL,
|
||||
options?: { withFileTypes?: boolean },
|
||||
) => {
|
||||
const path = dirPath.toString();
|
||||
if (options?.withFileTypes) {
|
||||
if (path === testCwd) {
|
||||
return [
|
||||
{ name: 'data', isDirectory: () => true },
|
||||
{ name: 'dist', isDirectory: () => true },
|
||||
{ name: 'node_modules', isDirectory: () => true },
|
||||
{ name: 'README.md', isDirectory: () => false },
|
||||
{ name: '.env', isDirectory: () => false },
|
||||
] as unknown as Awaited<ReturnType<typeof fs.readdir>>;
|
||||
}
|
||||
if (path.endsWith('/src')) {
|
||||
return [
|
||||
{ name: 'index.ts', isDirectory: () => false },
|
||||
{ name: 'components', isDirectory: () => true },
|
||||
] as unknown as Awaited<ReturnType<typeof fs.readdir>>;
|
||||
}
|
||||
if (path.endsWith('/temp')) {
|
||||
return [
|
||||
{ name: 'temp.log', isDirectory: () => false },
|
||||
] as unknown as Awaited<ReturnType<typeof fs.readdir>>;
|
||||
}
|
||||
}
|
||||
if (dirPath.endsWith('/src')) {
|
||||
return [
|
||||
{ name: 'index.ts', isDirectory: () => false },
|
||||
{ name: 'components', isDirectory: () => true },
|
||||
] as Array<{ name: string; isDirectory: () => boolean }>;
|
||||
}
|
||||
if (dirPath.endsWith('/temp')) {
|
||||
return [{ name: 'temp.log', isDirectory: () => false }] as Array<{
|
||||
name: string;
|
||||
isDirectory: () => boolean;
|
||||
}>;
|
||||
}
|
||||
return [] as Array<{ name: string; isDirectory: () => boolean }>;
|
||||
return [];
|
||||
},
|
||||
);
|
||||
|
||||
// Mock git ignore service
|
||||
// Mock ignore service
|
||||
mockFileDiscoveryService.shouldGitIgnoreFile.mockImplementation(
|
||||
(path: string) => path.includes('node_modules') || path.includes('temp'),
|
||||
);
|
||||
mockFileDiscoveryService.shouldIgnoreFile.mockImplementation(
|
||||
(path: string, options) => {
|
||||
if (options?.respectGitIgnore !== false) {
|
||||
return mockFileDiscoveryService.shouldGitIgnoreFile(path);
|
||||
if (
|
||||
options?.respectGitIgnore &&
|
||||
mockFileDiscoveryService.shouldGitIgnoreFile(path)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
options?.respectGeminiIgnore &&
|
||||
mockFileDiscoveryService.shouldGeminiIgnoreFile
|
||||
) {
|
||||
return mockFileDiscoveryService.shouldGeminiIgnoreFile(path);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
@@ -405,9 +437,12 @@ describe('useCompletion git-aware filtering integration', () => {
|
||||
);
|
||||
mockFileDiscoveryService.shouldIgnoreFile.mockImplementation(
|
||||
(path: string, options) => {
|
||||
if (options?.respectGitIgnore !== false) {
|
||||
if (options?.respectGitIgnore) {
|
||||
return mockFileDiscoveryService.shouldGitIgnoreFile(path);
|
||||
}
|
||||
if (options?.respectGeminiIgnore) {
|
||||
return mockFileDiscoveryService.shouldGeminiIgnoreFile(path);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -55,6 +55,10 @@ describe('useCompletion', () => {
|
||||
getFileFilteringRespectGitIgnore: vi.fn(() => true),
|
||||
getFileService: vi.fn().mockReturnValue(mockFileDiscoveryService),
|
||||
getEnableRecursiveFileSearch: vi.fn(() => true),
|
||||
getFileFilteringOptions: vi.fn(() => ({
|
||||
respectGitIgnore: true,
|
||||
respectGeminiIgnore: true,
|
||||
})),
|
||||
} as unknown as Mocked<Config>;
|
||||
|
||||
mockCommandContext = {} as CommandContext;
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
getErrorMessage,
|
||||
Config,
|
||||
FileDiscoveryService,
|
||||
DEFAULT_FILE_FILTERING_OPTIONS,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
MAX_SUGGESTIONS_TO_SHOW,
|
||||
@@ -415,10 +416,8 @@ export function useCompletion(
|
||||
const fileDiscoveryService = config ? config.getFileService() : null;
|
||||
const enableRecursiveSearch =
|
||||
config?.getEnableRecursiveFileSearch() ?? true;
|
||||
const filterOptions = {
|
||||
respectGitIgnore: config?.getFileFilteringRespectGitIgnore() ?? true,
|
||||
respectGeminiIgnore: true,
|
||||
};
|
||||
const filterOptions =
|
||||
config?.getFileFilteringOptions() ?? DEFAULT_FILE_FILTERING_OPTIONS;
|
||||
|
||||
try {
|
||||
// If there's no slash, or it's the root, do a recursive search from cwd
|
||||
|
||||
Reference in New Issue
Block a user