Add support for `.geminiignore` file (#757)
This commit is contained in:
parent
1d20cedf03
commit
422c763a55
|
@ -14,6 +14,7 @@
|
|||
"gemini": "bundle/gemini.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/micromatch": "^4.0.9",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/minimatch": "^5.1.2",
|
||||
"@vitest/coverage-v8": "^3.1.1",
|
||||
|
@ -1542,6 +1543,13 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/braces": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/braces/-/braces-3.0.5.tgz",
|
||||
"integrity": "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/chai": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz",
|
||||
|
@ -1615,6 +1623,16 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/micromatch": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-4.0.9.tgz",
|
||||
"integrity": "sha512-7V+8ncr22h4UoYRLnLXSpTxjQrNUXtWHGeMPRJt1nULXI57G9bIcpyrHlmrQ7QK24EyyuXvYcSSWAM8GA9nqCg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/braces": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mime-types": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz",
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
"LICENSE"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/micromatch": "^4.0.9",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/minimatch": "^5.1.2",
|
||||
"@vitest/coverage-v8": "^3.1.1",
|
||||
|
|
|
@ -123,6 +123,7 @@ export interface LoadCliConfigResult {
|
|||
|
||||
export async function loadCliConfig(
|
||||
settings: Settings,
|
||||
geminiIgnorePatterns: string[],
|
||||
): Promise<LoadCliConfigResult> {
|
||||
loadEnvironment();
|
||||
|
||||
|
@ -211,6 +212,7 @@ export async function loadCliConfig(
|
|||
vertexai: useVertexAI,
|
||||
showMemoryUsage:
|
||||
argv.show_memory_usage || settings.showMemoryUsage || false,
|
||||
geminiIgnorePatterns,
|
||||
accessibility: settings.accessibility,
|
||||
// Git-aware file filtering settings
|
||||
fileFilteringRespectGitIgnore: settings.fileFiltering?.respectGitIgnore,
|
||||
|
|
|
@ -17,6 +17,7 @@ import { LoadedSettings, loadSettings } from './config/settings.js';
|
|||
import { themeManager } from './ui/themes/theme-manager.js';
|
||||
import { getStartupWarnings } from './utils/startupWarnings.js';
|
||||
import { runNonInteractive } from './nonInteractiveCli.js';
|
||||
import { loadGeminiIgnorePatterns } from './utils/loadIgnorePatterns.js';
|
||||
import {
|
||||
ApprovalMode,
|
||||
Config,
|
||||
|
@ -56,9 +57,12 @@ async function main() {
|
|||
process.env.GEMINI_CODE_SANDBOX_IMAGE; // Corrected to GEMINI_SANDBOX_IMAGE_NAME
|
||||
}
|
||||
|
||||
const settings = loadSettings(process.cwd());
|
||||
const workspaceRoot = process.cwd();
|
||||
const settings = loadSettings(workspaceRoot);
|
||||
const geminiIgnorePatterns = loadGeminiIgnorePatterns(workspaceRoot);
|
||||
|
||||
const { config, modelWasSwitched, originalModelBeforeSwitch, finalModel } =
|
||||
await loadCliConfig(settings.merged);
|
||||
await loadCliConfig(settings.merged, geminiIgnorePatterns);
|
||||
|
||||
// Initialize centralized FileDiscoveryService
|
||||
await config.getFileService();
|
||||
|
@ -150,6 +154,7 @@ main().catch((error) => {
|
|||
}
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
async function loadNonInteractiveConfig(
|
||||
config: Config,
|
||||
settings: LoadedSettings,
|
||||
|
@ -185,6 +190,7 @@ async function loadNonInteractiveConfig(
|
|||
};
|
||||
const nonInteractiveConfigResult = await loadCliConfig(
|
||||
nonInteractiveSettings,
|
||||
config.getGeminiIgnorePatterns(),
|
||||
);
|
||||
return nonInteractiveConfigResult.config;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,226 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
vi,
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
Mock,
|
||||
beforeAll,
|
||||
} from 'vitest';
|
||||
import * as path from 'node:path';
|
||||
import { loadGeminiIgnorePatterns } from './loadIgnorePatterns.js';
|
||||
import os from 'node:os';
|
||||
|
||||
// Define the type for our mock function explicitly.
|
||||
type ReadFileSyncMockType = Mock<
|
||||
(path: string, encoding: string) => string | Buffer
|
||||
>;
|
||||
|
||||
// Declare a variable to hold our mock function instance.
|
||||
let mockedFsReadFileSync: ReadFileSyncMockType;
|
||||
|
||||
vi.mock('node:fs', async () => {
|
||||
const actualFsModule =
|
||||
await vi.importActual<typeof import('node:fs')>('node:fs');
|
||||
return {
|
||||
...actualFsModule,
|
||||
readFileSync: vi.fn(), // The factory creates and returns the vi.fn() instance.
|
||||
};
|
||||
});
|
||||
|
||||
let actualFs: typeof import('node:fs');
|
||||
|
||||
describe('loadGeminiIgnorePatterns', () => {
|
||||
let tempDir: string;
|
||||
let consoleLogSpy: Mock<
|
||||
(message?: unknown, ...optionalParams: unknown[]) => void
|
||||
>;
|
||||
let consoleWarnSpy: Mock<
|
||||
(message?: unknown, ...optionalParams: unknown[]) => void
|
||||
>;
|
||||
|
||||
beforeAll(async () => {
|
||||
actualFs = await vi.importActual<typeof import('node:fs')>('node:fs');
|
||||
const mockedFsModule = await import('node:fs');
|
||||
mockedFsReadFileSync =
|
||||
mockedFsModule.readFileSync as unknown as ReadFileSyncMockType;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = actualFs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'gemini-ignore-test-'),
|
||||
);
|
||||
consoleLogSpy = vi
|
||||
.spyOn(console, 'log')
|
||||
.mockImplementation(() => {}) as Mock<
|
||||
(message?: unknown, ...optionalParams: unknown[]) => void
|
||||
>;
|
||||
consoleWarnSpy = vi
|
||||
.spyOn(console, 'warn')
|
||||
.mockImplementation(() => {}) as Mock<
|
||||
(message?: unknown, ...optionalParams: unknown[]) => void
|
||||
>;
|
||||
mockedFsReadFileSync.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (actualFs.existsSync(tempDir)) {
|
||||
actualFs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
consoleLogSpy.mockRestore();
|
||||
consoleWarnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should load and parse patterns from .geminiignore, ignoring comments and empty lines', () => {
|
||||
const ignoreContent = [
|
||||
'# This is a comment',
|
||||
'pattern1',
|
||||
' pattern2 ', // Should be trimmed
|
||||
'', // Empty line
|
||||
'pattern3 # Inline comment', // Handled by trim
|
||||
'*.log',
|
||||
'!important.file',
|
||||
].join('\n');
|
||||
const ignoreFilePath = path.join(tempDir, '.geminiignore');
|
||||
actualFs.writeFileSync(ignoreFilePath, ignoreContent);
|
||||
|
||||
mockedFsReadFileSync.mockImplementation((p: string, encoding: string) => {
|
||||
if (p === ignoreFilePath && encoding === 'utf-8') return ignoreContent;
|
||||
throw new Error(
|
||||
`Mock fs.readFileSync: Unexpected call with path: ${p}, encoding: ${encoding}`,
|
||||
);
|
||||
});
|
||||
|
||||
const patterns = loadGeminiIgnorePatterns(tempDir);
|
||||
|
||||
expect(patterns).toEqual([
|
||||
'pattern1',
|
||||
'pattern2',
|
||||
'pattern3 # Inline comment',
|
||||
'*.log',
|
||||
'!important.file',
|
||||
]);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Loaded 5 patterns from .geminiignore'),
|
||||
);
|
||||
expect(mockedFsReadFileSync).toHaveBeenCalledWith(ignoreFilePath, 'utf-8');
|
||||
});
|
||||
|
||||
it('should return an empty array and log info if .geminiignore is not found', () => {
|
||||
const ignoreFilePath = path.join(tempDir, '.geminiignore');
|
||||
mockedFsReadFileSync.mockImplementation((p: string, encoding: string) => {
|
||||
if (p === ignoreFilePath && encoding === 'utf-8') {
|
||||
const error = new Error('File not found') as NodeJS.ErrnoException;
|
||||
error.code = 'ENOENT';
|
||||
throw error;
|
||||
}
|
||||
throw new Error(
|
||||
`Mock fs.readFileSync: Unexpected call with path: ${p}, encoding: ${encoding}`,
|
||||
);
|
||||
});
|
||||
|
||||
const patterns = loadGeminiIgnorePatterns(tempDir);
|
||||
expect(patterns).toEqual([]);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'[INFO] No .geminiignore file found. Proceeding without custom ignore patterns.',
|
||||
);
|
||||
expect(mockedFsReadFileSync).toHaveBeenCalledWith(ignoreFilePath, 'utf-8');
|
||||
});
|
||||
|
||||
it('should return an empty array if .geminiignore is empty', () => {
|
||||
const ignoreFilePath = path.join(tempDir, '.geminiignore');
|
||||
actualFs.writeFileSync(ignoreFilePath, '');
|
||||
mockedFsReadFileSync.mockImplementation((p: string, encoding: string) => {
|
||||
if (p === ignoreFilePath && encoding === 'utf-8') return ''; // Return string for empty file
|
||||
throw new Error(
|
||||
`Mock fs.readFileSync: Unexpected call with path: ${p}, encoding: ${encoding}`,
|
||||
);
|
||||
});
|
||||
|
||||
const patterns = loadGeminiIgnorePatterns(tempDir);
|
||||
expect(patterns).toEqual([]);
|
||||
expect(consoleLogSpy).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('Loaded 0 patterns from .geminiignore'),
|
||||
);
|
||||
expect(consoleLogSpy).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('No .geminiignore file found'),
|
||||
);
|
||||
expect(mockedFsReadFileSync).toHaveBeenCalledWith(ignoreFilePath, 'utf-8');
|
||||
});
|
||||
|
||||
it('should return an empty array if .geminiignore contains only comments and empty lines', () => {
|
||||
const ignoreContent = [
|
||||
'# Comment 1',
|
||||
' # Comment 2 with leading spaces',
|
||||
'',
|
||||
' ', // Whitespace only line
|
||||
].join('\n');
|
||||
const ignoreFilePath = path.join(tempDir, '.geminiignore');
|
||||
actualFs.writeFileSync(ignoreFilePath, ignoreContent);
|
||||
mockedFsReadFileSync.mockImplementation((p: string, encoding: string) => {
|
||||
if (p === ignoreFilePath && encoding === 'utf-8') return ignoreContent;
|
||||
throw new Error(
|
||||
`Mock fs.readFileSync: Unexpected call with path: ${p}, encoding: ${encoding}`,
|
||||
);
|
||||
});
|
||||
|
||||
const patterns = loadGeminiIgnorePatterns(tempDir);
|
||||
expect(patterns).toEqual([]);
|
||||
expect(consoleLogSpy).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('Loaded 0 patterns from .geminiignore'),
|
||||
);
|
||||
expect(consoleLogSpy).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('No .geminiignore file found'),
|
||||
);
|
||||
expect(mockedFsReadFileSync).toHaveBeenCalledWith(ignoreFilePath, 'utf-8');
|
||||
});
|
||||
|
||||
it('should handle read errors (other than ENOENT) and log a warning', () => {
|
||||
const ignoreFilePath = path.join(tempDir, '.geminiignore');
|
||||
mockedFsReadFileSync.mockImplementation((p: string, encoding: string) => {
|
||||
if (p === ignoreFilePath && encoding === 'utf-8') {
|
||||
const error = new Error('Test read error') as NodeJS.ErrnoException;
|
||||
error.code = 'EACCES';
|
||||
throw error;
|
||||
}
|
||||
throw new Error(
|
||||
`Mock fs.readFileSync: Unexpected call with path: ${p}, encoding: ${encoding}`,
|
||||
);
|
||||
});
|
||||
|
||||
const patterns = loadGeminiIgnorePatterns(tempDir);
|
||||
expect(patterns).toEqual([]);
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
`[WARN] Could not read .geminiignore file at ${ignoreFilePath}: Test read error`,
|
||||
),
|
||||
);
|
||||
expect(mockedFsReadFileSync).toHaveBeenCalledWith(ignoreFilePath, 'utf-8');
|
||||
});
|
||||
|
||||
it('should correctly handle patterns with inline comments if not starting with #', () => {
|
||||
const ignoreContent = 'src/important # but not this part';
|
||||
const ignoreFilePath = path.join(tempDir, '.geminiignore');
|
||||
actualFs.writeFileSync(ignoreFilePath, ignoreContent);
|
||||
mockedFsReadFileSync.mockImplementation((p: string, encoding: string) => {
|
||||
if (p === ignoreFilePath && encoding === 'utf-8') return ignoreContent;
|
||||
throw new Error(
|
||||
`Mock fs.readFileSync: Unexpected call with path: ${p}, encoding: ${encoding}`,
|
||||
);
|
||||
});
|
||||
|
||||
const patterns = loadGeminiIgnorePatterns(tempDir);
|
||||
expect(patterns).toEqual(['src/important # but not this part']);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Loaded 1 patterns from .geminiignore'),
|
||||
);
|
||||
expect(mockedFsReadFileSync).toHaveBeenCalledWith(ignoreFilePath, 'utf-8');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
const GEMINI_IGNORE_FILE_NAME = '.geminiignore';
|
||||
|
||||
/**
|
||||
* Loads and parses a .geminiignore file from the given workspace root.
|
||||
* The .geminiignore file follows a format similar to .gitignore:
|
||||
* - Each line specifies a glob pattern.
|
||||
* - Lines are trimmed of leading and trailing whitespace.
|
||||
* - Blank lines (after trimming) are ignored.
|
||||
* - Lines starting with a pound sign (#) (after trimming) are treated as comments and ignored.
|
||||
* - Patterns are case-sensitive and follow standard glob syntax.
|
||||
* - If a # character appears elsewhere in a line (not at the start after trimming),
|
||||
* it is considered part of the glob pattern.
|
||||
*
|
||||
* @param workspaceRoot The absolute path to the workspace root where the .geminiignore file is expected.
|
||||
* @returns An array of glob patterns extracted from the .geminiignore file. Returns an empty array
|
||||
* if the file does not exist or contains no valid patterns.
|
||||
*/
|
||||
export function loadGeminiIgnorePatterns(workspaceRoot: string): string[] {
|
||||
const ignoreFilePath = path.join(workspaceRoot, GEMINI_IGNORE_FILE_NAME);
|
||||
const patterns: string[] = [];
|
||||
|
||||
try {
|
||||
const fileContent = fs.readFileSync(ignoreFilePath, 'utf-8');
|
||||
const lines = fileContent.split(/\r?\n/);
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim();
|
||||
if (trimmedLine && !trimmedLine.startsWith('#')) {
|
||||
patterns.push(trimmedLine);
|
||||
}
|
||||
}
|
||||
if (patterns.length > 0) {
|
||||
console.log(
|
||||
`[INFO] Loaded ${patterns.length} patterns from .geminiignore`,
|
||||
);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
'code' in error &&
|
||||
typeof error.code === 'string'
|
||||
) {
|
||||
if (error.code === 'ENOENT') {
|
||||
// .geminiignore not found, which is fine.
|
||||
console.log(
|
||||
'[INFO] No .geminiignore file found. Proceeding without custom ignore patterns.',
|
||||
);
|
||||
} else {
|
||||
// Other error reading the file (e.g., permissions)
|
||||
console.warn(
|
||||
`[WARN] Could not read .geminiignore file at ${ignoreFilePath}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// For other types of errors, or if code is not available
|
||||
console.warn(
|
||||
`[WARN] An unexpected error occurred while trying to read ${ignoreFilePath}: ${String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return patterns;
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue