diff options
| author | Eddie Santos <[email protected]> | 2025-06-05 10:15:27 -0700 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-06-05 10:15:27 -0700 |
| commit | 422c763a55c28394df359bd9b31388c8d9765fc8 (patch) | |
| tree | fefda70212927409936ad326de5ec50ad515bfea /packages/core/src | |
| parent | 1d20cedf033f9c9a8f27812020fead584510bf84 (diff) | |
Add support for `.geminiignore` file (#757)
Diffstat (limited to 'packages/core/src')
| -rw-r--r-- | packages/core/src/config/config.ts | 11 | ||||
| -rw-r--r-- | packages/core/src/tools/read-file.test.ts | 16 | ||||
| -rw-r--r-- | packages/core/src/tools/read-file.ts | 22 | ||||
| -rw-r--r-- | packages/core/src/tools/read-many-files.test.ts | 14 | ||||
| -rw-r--r-- | packages/core/src/tools/read-many-files.ts | 30 |
5 files changed, 82 insertions, 11 deletions
diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index deb8b62b..92a929cc 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -70,6 +70,7 @@ export interface ConfigParameters { vertexai?: boolean; showMemoryUsage?: boolean; contextFileName?: string; + geminiIgnorePatterns?: string[]; accessibility?: AccessibilitySettings; fileFilteringRespectGitIgnore?: boolean; fileFilteringAllowBuildArtifacts?: boolean; @@ -97,6 +98,7 @@ export class Config { private readonly showMemoryUsage: boolean; private readonly accessibility: AccessibilitySettings; private readonly geminiClient: GeminiClient; + private readonly geminiIgnorePatterns: string[] = []; private readonly fileFilteringRespectGitIgnore: boolean; private readonly fileFilteringAllowBuildArtifacts: boolean; private fileDiscoveryService: FileDiscoveryService | null = null; @@ -129,6 +131,9 @@ export class Config { if (params.contextFileName) { setGeminiMdFilename(params.contextFileName); } + if (params.geminiIgnorePatterns) { + this.geminiIgnorePatterns = params.geminiIgnorePatterns; + } this.toolRegistry = createToolRegistry(this); this.geminiClient = new GeminiClient(this); @@ -229,6 +234,10 @@ export class Config { return this.geminiClient; } + getGeminiIgnorePatterns(): string[] { + return this.geminiIgnorePatterns; + } + getFileFilteringRespectGitIgnore(): boolean { return this.fileFilteringRespectGitIgnore; } @@ -311,7 +320,7 @@ export function createToolRegistry(config: Config): Promise<ToolRegistry> { }; registerCoreTool(LSTool, targetDir, config); - registerCoreTool(ReadFileTool, targetDir); + registerCoreTool(ReadFileTool, targetDir, config); registerCoreTool(GrepTool, targetDir); registerCoreTool(GlobTool, targetDir, config); registerCoreTool(EditTool, config); diff --git a/packages/core/src/tools/read-file.test.ts b/packages/core/src/tools/read-file.test.ts index 8ea42134..39c22d06 100644 --- a/packages/core/src/tools/read-file.test.ts +++ b/packages/core/src/tools/read-file.test.ts @@ -10,6 +10,7 @@ import * as fileUtils from '../utils/fileUtils.js'; import path from 'path'; import os from 'os'; import fs from 'fs'; // For actual fs operations in setup +import { Config } from '../config/config.js'; // Mock fileUtils.processSingleFileContent vi.mock('../utils/fileUtils', async () => { @@ -33,7 +34,10 @@ describe('ReadFileTool', () => { tempRootDir = fs.mkdtempSync( path.join(os.tmpdir(), 'read-file-tool-root-'), ); - tool = new ReadFileTool(tempRootDir); + const mockConfigInstance = { + getGeminiIgnorePatterns: () => ['**/foo.bar', 'foo.baz', 'foo.*'], + } as Config; + tool = new ReadFileTool(tempRootDir, mockConfigInstance); mockProcessSingleFileContent.mockReset(); }); @@ -224,5 +228,15 @@ describe('ReadFileTool', () => { 5, ); }); + + it('should return error if path is ignored by a .geminiignore pattern', async () => { + const params: ReadFileToolParams = { + path: path.join(tempRootDir, 'foo.bar'), + }; + const result = await tool.execute(params, abortSignal); + expect(result.returnDisplay).toContain('foo.bar'); + expect(result.returnDisplay).toContain('foo.*'); + expect(result.returnDisplay).not.toContain('foo.baz'); + }); }); }); diff --git a/packages/core/src/tools/read-file.ts b/packages/core/src/tools/read-file.ts index f92885ea..5c5994d7 100644 --- a/packages/core/src/tools/read-file.ts +++ b/packages/core/src/tools/read-file.ts @@ -5,10 +5,12 @@ */ import path from 'path'; +import micromatch from 'micromatch'; import { SchemaValidator } from '../utils/schemaValidator.js'; import { makeRelative, shortenPath } from '../utils/paths.js'; import { BaseTool, ToolResult } from './tools.js'; import { isWithinRoot, processSingleFileContent } from '../utils/fileUtils.js'; +import { Config } from '../config/config.js'; /** * Parameters for the ReadFile tool @@ -35,8 +37,12 @@ export interface ReadFileToolParams { */ export class ReadFileTool extends BaseTool<ReadFileToolParams, ToolResult> { static readonly Name: string = 'read_file'; + private readonly geminiIgnorePatterns: string[]; - constructor(private rootDirectory: string) { + constructor( + private rootDirectory: string, + config: Config, + ) { super( ReadFileTool.Name, 'ReadFile', @@ -64,6 +70,7 @@ export class ReadFileTool extends BaseTool<ReadFileToolParams, ToolResult> { }, ); this.rootDirectory = path.resolve(rootDirectory); + this.geminiIgnorePatterns = config.getGeminiIgnorePatterns() || []; } validateToolParams(params: ReadFileToolParams): string | null { @@ -89,6 +96,19 @@ export class ReadFileTool extends BaseTool<ReadFileToolParams, ToolResult> { if (params.limit !== undefined && params.limit <= 0) { return 'Limit must be a positive number'; } + + // Check against .geminiignore patterns + if (this.geminiIgnorePatterns.length > 0) { + const relativePath = makeRelative(params.path, this.rootDirectory); + if (micromatch.isMatch(relativePath, this.geminiIgnorePatterns)) { + // Get patterns that matched to show in the error message + const matchingPatterns = this.geminiIgnorePatterns.filter((p) => + micromatch.isMatch(relativePath, p), + ); + return `File path '${shortenPath(relativePath)}' is ignored by the following .geminiignore pattern(s):\n\n${matchingPatterns.join('\n')}`; + } + } + return null; } diff --git a/packages/core/src/tools/read-many-files.test.ts b/packages/core/src/tools/read-many-files.test.ts index f4ecc9d0..eb647c18 100644 --- a/packages/core/src/tools/read-many-files.test.ts +++ b/packages/core/src/tools/read-many-files.test.ts @@ -10,10 +10,10 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { mockControl } from '../__mocks__/fs/promises.js'; import { ReadManyFilesTool } from './read-many-files.js'; import { FileDiscoveryService } from '../services/fileDiscoveryService.js'; -import { Config } from '../config/config.js'; import path from 'path'; import fs from 'fs'; // Actual fs for setup import os from 'os'; +import { Config } from '../config/config.js'; describe('ReadManyFilesTool', () => { let tool: ReadManyFilesTool; @@ -31,6 +31,7 @@ describe('ReadManyFilesTool', () => { getFileFilteringRespectGitIgnore: () => true, getFileFilteringCustomIgnorePatterns: () => [], getFileFilteringAllowBuildArtifacts: () => false, + getGeminiIgnorePatterns: () => ['**/foo.bar', 'foo.baz', 'foo.*'], } as Partial<Config> as Config; beforeEach(async () => { @@ -367,5 +368,16 @@ describe('ReadManyFilesTool', () => { }, ]); }); + + it('should return error if path is ignored by a .geminiignore pattern', async () => { + createFile('foo.bar', ''); + createFile('qux/foo.baz', ''); + createFile('foo.quux', ''); + const params = { paths: ['foo.bar', 'qux/foo.baz', 'foo.quux'] }; + const result = await tool.execute(params, new AbortController().signal); + expect(result.returnDisplay).not.toContain('foo.bar'); + expect(result.returnDisplay).toContain('qux/foo.baz'); + expect(result.returnDisplay).not.toContain('foo.quux'); + }); }); }); diff --git a/packages/core/src/tools/read-many-files.ts b/packages/core/src/tools/read-many-files.ts index 30f70c91..718ce510 100644 --- a/packages/core/src/tools/read-many-files.ts +++ b/packages/core/src/tools/read-many-files.ts @@ -119,6 +119,7 @@ export class ReadManyFilesTool extends BaseTool< ToolResult > { static readonly Name: string = 'read_many_files'; + private readonly geminiIgnorePatterns: string[]; /** * Creates an instance of ReadManyFilesTool. @@ -190,6 +191,7 @@ Use this tool when the user's query implies needing the content of several files parameterSchema, ); this.targetDir = path.resolve(targetDir); + this.geminiIgnorePatterns = config.getGeminiIgnorePatterns() || []; } validateParams(params: ReadManyFilesParams): string | null { @@ -242,12 +244,26 @@ Use this tool when the user's query implies needing the content of several files const allPatterns = [...params.paths, ...(params.include || [])]; const pathDesc = `using patterns: \`${allPatterns.join('`, `')}\` (within target directory: \`${this.targetDir}\`)`; - let effectiveExcludes = - params.useDefaultExcludes !== false ? [...DEFAULT_EXCLUDES] : []; - if (params.exclude && params.exclude.length > 0) { - effectiveExcludes = [...effectiveExcludes, ...params.exclude]; + // Determine the final list of exclusion patterns exactly as in execute method + const paramExcludes = params.exclude || []; + const paramUseDefaultExcludes = params.useDefaultExcludes !== false; + + const finalExclusionPatternsForDescription: string[] = + paramUseDefaultExcludes + ? [...DEFAULT_EXCLUDES, ...paramExcludes, ...this.geminiIgnorePatterns] + : [...paramExcludes, ...this.geminiIgnorePatterns]; + + let excludeDesc = `Excluding: ${finalExclusionPatternsForDescription.length > 0 ? `patterns like \`${finalExclusionPatternsForDescription.slice(0, 2).join('`, `')}${finalExclusionPatternsForDescription.length > 2 ? '...`' : '`'}` : 'none specified'}`; + + // Add a note if .geminiignore patterns contributed to the final list of exclusions + if (this.geminiIgnorePatterns.length > 0) { + const geminiPatternsInEffect = this.geminiIgnorePatterns.filter((p) => + finalExclusionPatternsForDescription.includes(p), + ).length; + if (geminiPatternsInEffect > 0) { + excludeDesc += ` (includes ${geminiPatternsInEffect} from .geminiignore)`; + } } - const excludeDesc = `Excluding: ${effectiveExcludes.length > 0 ? `patterns like \`${effectiveExcludes.slice(0, 2).join('`, `')}${effectiveExcludes.length > 2 ? '...`' : '`'}` : 'none explicitly (beyond default non-text file avoidance).'}`; return `Will attempt to read and concatenate files ${pathDesc}. ${excludeDesc}. File encoding: ${DEFAULT_ENCODING}. Separator: "${DEFAULT_OUTPUT_SEPARATOR_FORMAT.replace('{filePath}', 'path/to/file.ext')}".`; } @@ -285,8 +301,8 @@ Use this tool when the user's query implies needing the content of several files const contentParts: PartListUnion = []; const effectiveExcludes = useDefaultExcludes - ? [...DEFAULT_EXCLUDES, ...exclude] - : [...exclude]; + ? [...DEFAULT_EXCLUDES, ...exclude, ...this.geminiIgnorePatterns] + : [...exclude, ...this.geminiIgnorePatterns]; const searchPatterns = [...inputPatterns, ...include]; if (searchPatterns.length === 0) { |
