mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 08:47:44 +00:00
Add support for .geminiignore file (#757)
This commit is contained in:
18
package-lock.json
generated
18
package-lock.json
generated
@@ -14,6 +14,7 @@
|
|||||||
"gemini": "bundle/gemini.js"
|
"gemini": "bundle/gemini.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/micromatch": "^4.0.9",
|
||||||
"@types/mime-types": "^2.1.4",
|
"@types/mime-types": "^2.1.4",
|
||||||
"@types/minimatch": "^5.1.2",
|
"@types/minimatch": "^5.1.2",
|
||||||
"@vitest/coverage-v8": "^3.1.1",
|
"@vitest/coverage-v8": "^3.1.1",
|
||||||
@@ -1542,6 +1543,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/chai": {
|
||||||
"version": "5.2.2",
|
"version": "5.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz",
|
||||||
@@ -1615,6 +1623,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/mime-types": {
|
||||||
"version": "2.1.4",
|
"version": "2.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz",
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
"LICENSE"
|
"LICENSE"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/micromatch": "^4.0.9",
|
||||||
"@types/mime-types": "^2.1.4",
|
"@types/mime-types": "^2.1.4",
|
||||||
"@types/minimatch": "^5.1.2",
|
"@types/minimatch": "^5.1.2",
|
||||||
"@vitest/coverage-v8": "^3.1.1",
|
"@vitest/coverage-v8": "^3.1.1",
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ export interface LoadCliConfigResult {
|
|||||||
|
|
||||||
export async function loadCliConfig(
|
export async function loadCliConfig(
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
|
geminiIgnorePatterns: string[],
|
||||||
): Promise<LoadCliConfigResult> {
|
): Promise<LoadCliConfigResult> {
|
||||||
loadEnvironment();
|
loadEnvironment();
|
||||||
|
|
||||||
@@ -211,6 +212,7 @@ export async function loadCliConfig(
|
|||||||
vertexai: useVertexAI,
|
vertexai: useVertexAI,
|
||||||
showMemoryUsage:
|
showMemoryUsage:
|
||||||
argv.show_memory_usage || settings.showMemoryUsage || false,
|
argv.show_memory_usage || settings.showMemoryUsage || false,
|
||||||
|
geminiIgnorePatterns,
|
||||||
accessibility: settings.accessibility,
|
accessibility: settings.accessibility,
|
||||||
// Git-aware file filtering settings
|
// Git-aware file filtering settings
|
||||||
fileFilteringRespectGitIgnore: settings.fileFiltering?.respectGitIgnore,
|
fileFilteringRespectGitIgnore: settings.fileFiltering?.respectGitIgnore,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { LoadedSettings, loadSettings } from './config/settings.js';
|
|||||||
import { themeManager } from './ui/themes/theme-manager.js';
|
import { themeManager } from './ui/themes/theme-manager.js';
|
||||||
import { getStartupWarnings } from './utils/startupWarnings.js';
|
import { getStartupWarnings } from './utils/startupWarnings.js';
|
||||||
import { runNonInteractive } from './nonInteractiveCli.js';
|
import { runNonInteractive } from './nonInteractiveCli.js';
|
||||||
|
import { loadGeminiIgnorePatterns } from './utils/loadIgnorePatterns.js';
|
||||||
import {
|
import {
|
||||||
ApprovalMode,
|
ApprovalMode,
|
||||||
Config,
|
Config,
|
||||||
@@ -56,9 +57,12 @@ async function main() {
|
|||||||
process.env.GEMINI_CODE_SANDBOX_IMAGE; // Corrected to GEMINI_SANDBOX_IMAGE_NAME
|
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 } =
|
const { config, modelWasSwitched, originalModelBeforeSwitch, finalModel } =
|
||||||
await loadCliConfig(settings.merged);
|
await loadCliConfig(settings.merged, geminiIgnorePatterns);
|
||||||
|
|
||||||
// Initialize centralized FileDiscoveryService
|
// Initialize centralized FileDiscoveryService
|
||||||
await config.getFileService();
|
await config.getFileService();
|
||||||
@@ -150,6 +154,7 @@ main().catch((error) => {
|
|||||||
}
|
}
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadNonInteractiveConfig(
|
async function loadNonInteractiveConfig(
|
||||||
config: Config,
|
config: Config,
|
||||||
settings: LoadedSettings,
|
settings: LoadedSettings,
|
||||||
@@ -185,6 +190,7 @@ async function loadNonInteractiveConfig(
|
|||||||
};
|
};
|
||||||
const nonInteractiveConfigResult = await loadCliConfig(
|
const nonInteractiveConfigResult = await loadCliConfig(
|
||||||
nonInteractiveSettings,
|
nonInteractiveSettings,
|
||||||
|
config.getGeminiIgnorePatterns(),
|
||||||
);
|
);
|
||||||
return nonInteractiveConfigResult.config;
|
return nonInteractiveConfigResult.config;
|
||||||
}
|
}
|
||||||
|
|||||||
226
packages/cli/src/utils/loadIgnorePatterns.test.ts
Normal file
226
packages/cli/src/utils/loadIgnorePatterns.test.ts
Normal file
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
71
packages/cli/src/utils/loadIgnorePatterns.ts
Normal file
71
packages/cli/src/utils/loadIgnorePatterns.ts
Normal file
@@ -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;
|
vertexai?: boolean;
|
||||||
showMemoryUsage?: boolean;
|
showMemoryUsage?: boolean;
|
||||||
contextFileName?: string;
|
contextFileName?: string;
|
||||||
|
geminiIgnorePatterns?: string[];
|
||||||
accessibility?: AccessibilitySettings;
|
accessibility?: AccessibilitySettings;
|
||||||
fileFilteringRespectGitIgnore?: boolean;
|
fileFilteringRespectGitIgnore?: boolean;
|
||||||
fileFilteringAllowBuildArtifacts?: boolean;
|
fileFilteringAllowBuildArtifacts?: boolean;
|
||||||
@@ -97,6 +98,7 @@ export class Config {
|
|||||||
private readonly showMemoryUsage: boolean;
|
private readonly showMemoryUsage: boolean;
|
||||||
private readonly accessibility: AccessibilitySettings;
|
private readonly accessibility: AccessibilitySettings;
|
||||||
private readonly geminiClient: GeminiClient;
|
private readonly geminiClient: GeminiClient;
|
||||||
|
private readonly geminiIgnorePatterns: string[] = [];
|
||||||
private readonly fileFilteringRespectGitIgnore: boolean;
|
private readonly fileFilteringRespectGitIgnore: boolean;
|
||||||
private readonly fileFilteringAllowBuildArtifacts: boolean;
|
private readonly fileFilteringAllowBuildArtifacts: boolean;
|
||||||
private fileDiscoveryService: FileDiscoveryService | null = null;
|
private fileDiscoveryService: FileDiscoveryService | null = null;
|
||||||
@@ -129,6 +131,9 @@ export class Config {
|
|||||||
if (params.contextFileName) {
|
if (params.contextFileName) {
|
||||||
setGeminiMdFilename(params.contextFileName);
|
setGeminiMdFilename(params.contextFileName);
|
||||||
}
|
}
|
||||||
|
if (params.geminiIgnorePatterns) {
|
||||||
|
this.geminiIgnorePatterns = params.geminiIgnorePatterns;
|
||||||
|
}
|
||||||
|
|
||||||
this.toolRegistry = createToolRegistry(this);
|
this.toolRegistry = createToolRegistry(this);
|
||||||
this.geminiClient = new GeminiClient(this);
|
this.geminiClient = new GeminiClient(this);
|
||||||
@@ -229,6 +234,10 @@ export class Config {
|
|||||||
return this.geminiClient;
|
return this.geminiClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getGeminiIgnorePatterns(): string[] {
|
||||||
|
return this.geminiIgnorePatterns;
|
||||||
|
}
|
||||||
|
|
||||||
getFileFilteringRespectGitIgnore(): boolean {
|
getFileFilteringRespectGitIgnore(): boolean {
|
||||||
return this.fileFilteringRespectGitIgnore;
|
return this.fileFilteringRespectGitIgnore;
|
||||||
}
|
}
|
||||||
@@ -311,7 +320,7 @@ export function createToolRegistry(config: Config): Promise<ToolRegistry> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
registerCoreTool(LSTool, targetDir, config);
|
registerCoreTool(LSTool, targetDir, config);
|
||||||
registerCoreTool(ReadFileTool, targetDir);
|
registerCoreTool(ReadFileTool, targetDir, config);
|
||||||
registerCoreTool(GrepTool, targetDir);
|
registerCoreTool(GrepTool, targetDir);
|
||||||
registerCoreTool(GlobTool, targetDir, config);
|
registerCoreTool(GlobTool, targetDir, config);
|
||||||
registerCoreTool(EditTool, config);
|
registerCoreTool(EditTool, config);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import * as fileUtils from '../utils/fileUtils.js';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import fs from 'fs'; // For actual fs operations in setup
|
import fs from 'fs'; // For actual fs operations in setup
|
||||||
|
import { Config } from '../config/config.js';
|
||||||
|
|
||||||
// Mock fileUtils.processSingleFileContent
|
// Mock fileUtils.processSingleFileContent
|
||||||
vi.mock('../utils/fileUtils', async () => {
|
vi.mock('../utils/fileUtils', async () => {
|
||||||
@@ -33,7 +34,10 @@ describe('ReadFileTool', () => {
|
|||||||
tempRootDir = fs.mkdtempSync(
|
tempRootDir = fs.mkdtempSync(
|
||||||
path.join(os.tmpdir(), 'read-file-tool-root-'),
|
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();
|
mockProcessSingleFileContent.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -224,5 +228,15 @@ describe('ReadFileTool', () => {
|
|||||||
5,
|
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 path from 'path';
|
||||||
|
import micromatch from 'micromatch';
|
||||||
import { SchemaValidator } from '../utils/schemaValidator.js';
|
import { SchemaValidator } from '../utils/schemaValidator.js';
|
||||||
import { makeRelative, shortenPath } from '../utils/paths.js';
|
import { makeRelative, shortenPath } from '../utils/paths.js';
|
||||||
import { BaseTool, ToolResult } from './tools.js';
|
import { BaseTool, ToolResult } from './tools.js';
|
||||||
import { isWithinRoot, processSingleFileContent } from '../utils/fileUtils.js';
|
import { isWithinRoot, processSingleFileContent } from '../utils/fileUtils.js';
|
||||||
|
import { Config } from '../config/config.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parameters for the ReadFile tool
|
* Parameters for the ReadFile tool
|
||||||
@@ -35,8 +37,12 @@ export interface ReadFileToolParams {
|
|||||||
*/
|
*/
|
||||||
export class ReadFileTool extends BaseTool<ReadFileToolParams, ToolResult> {
|
export class ReadFileTool extends BaseTool<ReadFileToolParams, ToolResult> {
|
||||||
static readonly Name: string = 'read_file';
|
static readonly Name: string = 'read_file';
|
||||||
|
private readonly geminiIgnorePatterns: string[];
|
||||||
|
|
||||||
constructor(private rootDirectory: string) {
|
constructor(
|
||||||
|
private rootDirectory: string,
|
||||||
|
config: Config,
|
||||||
|
) {
|
||||||
super(
|
super(
|
||||||
ReadFileTool.Name,
|
ReadFileTool.Name,
|
||||||
'ReadFile',
|
'ReadFile',
|
||||||
@@ -64,6 +70,7 @@ export class ReadFileTool extends BaseTool<ReadFileToolParams, ToolResult> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
this.rootDirectory = path.resolve(rootDirectory);
|
this.rootDirectory = path.resolve(rootDirectory);
|
||||||
|
this.geminiIgnorePatterns = config.getGeminiIgnorePatterns() || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
validateToolParams(params: ReadFileToolParams): string | null {
|
validateToolParams(params: ReadFileToolParams): string | null {
|
||||||
@@ -89,6 +96,19 @@ export class ReadFileTool extends BaseTool<ReadFileToolParams, ToolResult> {
|
|||||||
if (params.limit !== undefined && params.limit <= 0) {
|
if (params.limit !== undefined && params.limit <= 0) {
|
||||||
return 'Limit must be a positive number';
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|||||||
import { mockControl } from '../__mocks__/fs/promises.js';
|
import { mockControl } from '../__mocks__/fs/promises.js';
|
||||||
import { ReadManyFilesTool } from './read-many-files.js';
|
import { ReadManyFilesTool } from './read-many-files.js';
|
||||||
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
|
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
|
||||||
import { Config } from '../config/config.js';
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs'; // Actual fs for setup
|
import fs from 'fs'; // Actual fs for setup
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
|
import { Config } from '../config/config.js';
|
||||||
|
|
||||||
describe('ReadManyFilesTool', () => {
|
describe('ReadManyFilesTool', () => {
|
||||||
let tool: ReadManyFilesTool;
|
let tool: ReadManyFilesTool;
|
||||||
@@ -31,6 +31,7 @@ describe('ReadManyFilesTool', () => {
|
|||||||
getFileFilteringRespectGitIgnore: () => true,
|
getFileFilteringRespectGitIgnore: () => true,
|
||||||
getFileFilteringCustomIgnorePatterns: () => [],
|
getFileFilteringCustomIgnorePatterns: () => [],
|
||||||
getFileFilteringAllowBuildArtifacts: () => false,
|
getFileFilteringAllowBuildArtifacts: () => false,
|
||||||
|
getGeminiIgnorePatterns: () => ['**/foo.bar', 'foo.baz', 'foo.*'],
|
||||||
} as Partial<Config> as Config;
|
} as Partial<Config> as Config;
|
||||||
|
|
||||||
beforeEach(async () => {
|
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
|
ToolResult
|
||||||
> {
|
> {
|
||||||
static readonly Name: string = 'read_many_files';
|
static readonly Name: string = 'read_many_files';
|
||||||
|
private readonly geminiIgnorePatterns: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance of ReadManyFilesTool.
|
* 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,
|
parameterSchema,
|
||||||
);
|
);
|
||||||
this.targetDir = path.resolve(targetDir);
|
this.targetDir = path.resolve(targetDir);
|
||||||
|
this.geminiIgnorePatterns = config.getGeminiIgnorePatterns() || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
validateParams(params: ReadManyFilesParams): string | null {
|
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 allPatterns = [...params.paths, ...(params.include || [])];
|
||||||
const pathDesc = `using patterns: \`${allPatterns.join('`, `')}\` (within target directory: \`${this.targetDir}\`)`;
|
const pathDesc = `using patterns: \`${allPatterns.join('`, `')}\` (within target directory: \`${this.targetDir}\`)`;
|
||||||
|
|
||||||
let effectiveExcludes =
|
// Determine the final list of exclusion patterns exactly as in execute method
|
||||||
params.useDefaultExcludes !== false ? [...DEFAULT_EXCLUDES] : [];
|
const paramExcludes = params.exclude || [];
|
||||||
if (params.exclude && params.exclude.length > 0) {
|
const paramUseDefaultExcludes = params.useDefaultExcludes !== false;
|
||||||
effectiveExcludes = [...effectiveExcludes, ...params.exclude];
|
|
||||||
|
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')}".`;
|
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 contentParts: PartListUnion = [];
|
||||||
|
|
||||||
const effectiveExcludes = useDefaultExcludes
|
const effectiveExcludes = useDefaultExcludes
|
||||||
? [...DEFAULT_EXCLUDES, ...exclude]
|
? [...DEFAULT_EXCLUDES, ...exclude, ...this.geminiIgnorePatterns]
|
||||||
: [...exclude];
|
: [...exclude, ...this.geminiIgnorePatterns];
|
||||||
|
|
||||||
const searchPatterns = [...inputPatterns, ...include];
|
const searchPatterns = [...inputPatterns, ...include];
|
||||||
if (searchPatterns.length === 0) {
|
if (searchPatterns.length === 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user