diff options
Diffstat (limited to 'packages/cli/src')
| -rw-r--r-- | packages/cli/src/config/config.test.ts | 12 | ||||
| -rw-r--r-- | packages/cli/src/config/config.ts | 13 | ||||
| -rw-r--r-- | packages/cli/src/config/settings.ts | 1 | ||||
| -rw-r--r-- | packages/cli/src/ui/App.tsx | 2 | ||||
| -rw-r--r-- | packages/cli/src/ui/hooks/atCommandProcessor.test.ts | 387 | ||||
| -rw-r--r-- | packages/cli/src/ui/hooks/atCommandProcessor.ts | 69 | ||||
| -rw-r--r-- | packages/cli/src/ui/hooks/useCompletion.integration.test.ts | 95 | ||||
| -rw-r--r-- | packages/cli/src/ui/hooks/useCompletion.test.ts | 4 | ||||
| -rw-r--r-- | packages/cli/src/ui/hooks/useCompletion.ts | 7 |
9 files changed, 522 insertions, 68 deletions
diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index cc0f112a..0c0761cc 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -43,6 +43,14 @@ vi.mock('@google/gemini-cli-core', async () => { fileCount: extensionPaths?.length || 0, }), ), + DEFAULT_MEMORY_FILE_FILTERING_OPTIONS: { + respectGitIgnore: false, + respectGeminiIgnore: true, + }, + DEFAULT_FILE_FILTERING_OPTIONS: { + respectGitIgnore: true, + respectGeminiIgnore: true, + }, }; }); @@ -479,6 +487,10 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => { '/path/to/ext3/context1.md', '/path/to/ext3/context2.md', ], + { + respectGitIgnore: false, + respectGeminiIgnore: true, + }, ); }); diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 2d33daa3..fd4907d0 100644 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -15,8 +15,10 @@ import { ApprovalMode, DEFAULT_GEMINI_MODEL, DEFAULT_GEMINI_EMBEDDING_MODEL, + DEFAULT_MEMORY_FILE_FILTERING_OPTIONS, FileDiscoveryService, TelemetryTarget, + FileFilteringOptions, MCPServerConfig, IDE_SERVER_NAME, } from '@google/gemini-cli-core'; @@ -219,12 +221,14 @@ export async function loadHierarchicalGeminiMemory( debugMode: boolean, fileService: FileDiscoveryService, extensionContextFilePaths: string[] = [], + fileFilteringOptions?: FileFilteringOptions, ): Promise<{ memoryContent: string; fileCount: number }> { if (debugMode) { logger.debug( `CLI: Delegating hierarchical memory load to server for CWD: ${currentWorkingDirectory}`, ); } + // Directly call the server function. // The server function will use its own homedir() for the global path. return loadServerHierarchicalMemory( @@ -232,6 +236,7 @@ export async function loadHierarchicalGeminiMemory( debugMode, fileService, extensionContextFilePaths, + fileFilteringOptions, ); } @@ -277,12 +282,19 @@ export async function loadCliConfig( ); const fileService = new FileDiscoveryService(process.cwd()); + + const fileFiltering = { + ...DEFAULT_MEMORY_FILE_FILTERING_OPTIONS, + ...settings.fileFiltering, + }; + // Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version const { memoryContent, fileCount } = await loadHierarchicalGeminiMemory( process.cwd(), debugMode, fileService, extensionContextFilePaths, + fileFiltering, ); let mcpServers = mergeMcpServers(settings, activeExtensions); @@ -405,6 +417,7 @@ export async function loadCliConfig( // Git-aware file filtering settings fileFiltering: { respectGitIgnore: settings.fileFiltering?.respectGitIgnore, + respectGeminiIgnore: settings.fileFiltering?.respectGeminiIgnore, enableRecursiveFileSearch: settings.fileFiltering?.enableRecursiveFileSearch, }, diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index 24b9e9e6..3cbfe22d 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -81,6 +81,7 @@ export interface Settings { // Git-aware file filtering settings fileFiltering?: { respectGitIgnore?: boolean; + respectGeminiIgnore?: boolean; enableRecursiveFileSearch?: boolean; }; diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx index 39a1f14c..027665f1 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -252,7 +252,9 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { config.getDebugMode(), config.getFileService(), config.getExtensionContextFilePaths(), + config.getFileFilteringOptions(), ); + config.setUserMemory(memoryContent); config.setGeminiMdFileCount(fileCount); setGeminiMdFileCount(fileCount); diff --git a/packages/cli/src/ui/hooks/atCommandProcessor.test.ts b/packages/cli/src/ui/hooks/atCommandProcessor.test.ts index efe15c64..6e272b24 100644 --- a/packages/cli/src/ui/hooks/atCommandProcessor.test.ts +++ b/packages/cli/src/ui/hooks/atCommandProcessor.test.ts @@ -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, respectGeminiIgnore: false }, + ); expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith( validFile, - { respectGitIgnore: true }, + { 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, respectGeminiIgnore: false }, + ); expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith( validFile, - { respectGitIgnore: true }, + { 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, respectGeminiIgnore: false }, + ); expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith( gitFile, - { respectGitIgnore: true }, + { 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); + }); + }); }); diff --git a/packages/cli/src/ui/hooks/atCommandProcessor.ts b/packages/cli/src/ui/hooks/atCommandProcessor.ts index 7fe68f10..983abc62 100644 --- a/packages/cli/src/ui/hooks/atCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/atCommandProcessor.ts @@ -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; diff --git a/packages/cli/src/ui/hooks/useCompletion.integration.test.ts b/packages/cli/src/ui/hooks/useCompletion.integration.test.ts index 37075e3c..f6f0944b 100644 --- a/packages/cli/src/ui/hooks/useCompletion.integration.test.ts +++ b/packages/cli/src/ui/hooks/useCompletion.integration.test.ts @@ -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 }>; - } - if (dirPath.endsWith('/src')) { - return [ - { name: 'index.ts', isDirectory: () => false }, - { name: 'components', 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('/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; }, ); diff --git a/packages/cli/src/ui/hooks/useCompletion.test.ts b/packages/cli/src/ui/hooks/useCompletion.test.ts index 267bce13..f4227c1a 100644 --- a/packages/cli/src/ui/hooks/useCompletion.test.ts +++ b/packages/cli/src/ui/hooks/useCompletion.test.ts @@ -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; diff --git a/packages/cli/src/ui/hooks/useCompletion.ts b/packages/cli/src/ui/hooks/useCompletion.ts index 81acc992..69d8bfb9 100644 --- a/packages/cli/src/ui/hooks/useCompletion.ts +++ b/packages/cli/src/ui/hooks/useCompletion.ts @@ -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 |
