mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 08:47:44 +00:00
feat: full implementation for .geminiignore in settings and respective tool calls (#3727)
This commit is contained in:
@@ -145,4 +145,43 @@ describe('bfsFileSearch', () => {
|
||||
});
|
||||
expect(result).toEqual(['/test/subdir1/file1.txt']);
|
||||
});
|
||||
|
||||
it('should respect .geminiignore files', async () => {
|
||||
const mockFs = vi.mocked(fsPromises);
|
||||
const mockGitUtils = vi.mocked(gitUtils);
|
||||
|
||||
mockGitUtils.isGitRepository.mockReturnValue(false);
|
||||
|
||||
const mockReaddir = mockFs.readdir as unknown as ReaddirWithFileTypes;
|
||||
vi.mocked(mockReaddir).mockImplementation(async (dir) => {
|
||||
if (dir === '/test') {
|
||||
return [
|
||||
createMockDirent('.geminiignore', true),
|
||||
createMockDirent('subdir1', false),
|
||||
createMockDirent('subdir2', false),
|
||||
];
|
||||
}
|
||||
if (dir === '/test/subdir1') {
|
||||
return [createMockDirent('file1.txt', true)];
|
||||
}
|
||||
if (dir === '/test/subdir2') {
|
||||
return [createMockDirent('file1.txt', true)];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
vi.mocked(fs).readFileSync.mockReturnValue('subdir2');
|
||||
|
||||
const fileService = new FileDiscoveryService('/test');
|
||||
const result = await bfsFileSearch('/test', {
|
||||
fileName: 'file1.txt',
|
||||
fileService,
|
||||
fileFilteringOptions: {
|
||||
respectGitIgnore: true,
|
||||
respectGeminiIgnore: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual(['/test/subdir1/file1.txt']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { Dirent } from 'fs';
|
||||
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
|
||||
|
||||
import { FileFilteringOptions } from '../config/config.js';
|
||||
// Simple console logger for now.
|
||||
// TODO: Integrate with a more robust server-side logger.
|
||||
const logger = {
|
||||
@@ -22,6 +22,7 @@ interface BfsFileSearchOptions {
|
||||
maxDirs?: number;
|
||||
debug?: boolean;
|
||||
fileService?: FileDiscoveryService;
|
||||
fileFilteringOptions?: FileFilteringOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,7 +70,13 @@ export async function bfsFileSearch(
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
if (fileService?.shouldGitIgnoreFile(fullPath)) {
|
||||
if (
|
||||
fileService?.shouldIgnoreFile(fullPath, {
|
||||
respectGitIgnore: options.fileFilteringOptions?.respectGitIgnore,
|
||||
respectGeminiIgnore:
|
||||
options.fileFilteringOptions?.respectGeminiIgnore,
|
||||
})
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -307,6 +307,7 @@ describe('getFolderStructure gitignore', () => {
|
||||
createDirent('file1.txt', 'file'),
|
||||
createDirent('node_modules', 'dir'),
|
||||
createDirent('ignored.txt', 'file'),
|
||||
createDirent('gem_ignored.txt', 'file'),
|
||||
createDirent('.gemini', 'dir'),
|
||||
] as any;
|
||||
}
|
||||
@@ -327,6 +328,9 @@ describe('getFolderStructure gitignore', () => {
|
||||
if (path === '/test/project/.gitignore') {
|
||||
return 'ignored.txt\nnode_modules/\n.gemini/\n!/.gemini/config.yaml';
|
||||
}
|
||||
if (path === '/test/project/.geminiignore') {
|
||||
return 'gem_ignored.txt\nnode_modules/\n.gemini/\n!/.gemini/config.yaml';
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
@@ -347,10 +351,37 @@ describe('getFolderStructure gitignore', () => {
|
||||
const fileService = new FileDiscoveryService('/test/project');
|
||||
const structure = await getFolderStructure('/test/project', {
|
||||
fileService,
|
||||
respectGitIgnore: false,
|
||||
fileFilteringOptions: {
|
||||
respectGeminiIgnore: false,
|
||||
respectGitIgnore: false,
|
||||
},
|
||||
});
|
||||
expect(structure).toContain('ignored.txt');
|
||||
// node_modules is still ignored by default
|
||||
expect(structure).toContain('node_modules/...');
|
||||
});
|
||||
|
||||
it('should ignore files and folders specified in .geminiignore', async () => {
|
||||
const fileService = new FileDiscoveryService('/test/project');
|
||||
const structure = await getFolderStructure('/test/project', {
|
||||
fileService,
|
||||
});
|
||||
expect(structure).not.toContain('gem_ignored.txt');
|
||||
expect(structure).toContain('node_modules/...');
|
||||
expect(structure).not.toContain('logs.json');
|
||||
});
|
||||
|
||||
it('should not ignore files if respectGeminiIgnore is false', async () => {
|
||||
const fileService = new FileDiscoveryService('/test/project');
|
||||
const structure = await getFolderStructure('/test/project', {
|
||||
fileService,
|
||||
fileFilteringOptions: {
|
||||
respectGeminiIgnore: false,
|
||||
respectGitIgnore: true, // Explicitly disable gemini ignore only
|
||||
},
|
||||
});
|
||||
expect(structure).toContain('gem_ignored.txt');
|
||||
// node_modules is still ignored by default
|
||||
expect(structure).toContain('node_modules/...');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,8 @@ import { Dirent } from 'fs';
|
||||
import * as path from 'path';
|
||||
import { getErrorMessage, isNodeError } from './errors.js';
|
||||
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
|
||||
import { FileFilteringOptions } from '../config/config.js';
|
||||
import { DEFAULT_FILE_FILTERING_OPTIONS } from '../config/config.js';
|
||||
|
||||
const MAX_ITEMS = 200;
|
||||
const TRUNCATION_INDICATOR = '...';
|
||||
@@ -26,16 +28,16 @@ interface FolderStructureOptions {
|
||||
fileIncludePattern?: RegExp;
|
||||
/** For filtering files. */
|
||||
fileService?: FileDiscoveryService;
|
||||
/** Whether to use .gitignore patterns. */
|
||||
respectGitIgnore?: boolean;
|
||||
/** File filtering ignore options. */
|
||||
fileFilteringOptions?: FileFilteringOptions;
|
||||
}
|
||||
|
||||
// Define a type for the merged options where fileIncludePattern remains optional
|
||||
type MergedFolderStructureOptions = Required<
|
||||
Omit<FolderStructureOptions, 'fileIncludePattern' | 'fileService'>
|
||||
> & {
|
||||
fileIncludePattern?: RegExp;
|
||||
fileService?: FileDiscoveryService;
|
||||
fileFilteringOptions?: FileFilteringOptions;
|
||||
};
|
||||
|
||||
/** Represents the full, unfiltered information about a folder and its contents. */
|
||||
@@ -126,8 +128,13 @@ async function readFullStructure(
|
||||
}
|
||||
const fileName = entry.name;
|
||||
const filePath = path.join(currentPath, fileName);
|
||||
if (options.respectGitIgnore && options.fileService) {
|
||||
if (options.fileService.shouldGitIgnoreFile(filePath)) {
|
||||
if (options.fileService) {
|
||||
const shouldIgnore =
|
||||
(options.fileFilteringOptions.respectGitIgnore &&
|
||||
options.fileService.shouldGitIgnoreFile(filePath)) ||
|
||||
(options.fileFilteringOptions.respectGeminiIgnore &&
|
||||
options.fileService.shouldGeminiIgnoreFile(filePath));
|
||||
if (shouldIgnore) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -160,14 +167,16 @@ async function readFullStructure(
|
||||
const subFolderName = entry.name;
|
||||
const subFolderPath = path.join(currentPath, subFolderName);
|
||||
|
||||
let isIgnoredByGit = false;
|
||||
if (options.respectGitIgnore && options.fileService) {
|
||||
if (options.fileService.shouldGitIgnoreFile(subFolderPath)) {
|
||||
isIgnoredByGit = true;
|
||||
}
|
||||
let isIgnored = false;
|
||||
if (options.fileService) {
|
||||
isIgnored =
|
||||
(options.fileFilteringOptions.respectGitIgnore &&
|
||||
options.fileService.shouldGitIgnoreFile(subFolderPath)) ||
|
||||
(options.fileFilteringOptions.respectGeminiIgnore &&
|
||||
options.fileService.shouldGeminiIgnoreFile(subFolderPath));
|
||||
}
|
||||
|
||||
if (options.ignoredFolders.has(subFolderName) || isIgnoredByGit) {
|
||||
if (options.ignoredFolders.has(subFolderName) || isIgnored) {
|
||||
const ignoredSubFolder: FullFolderInfo = {
|
||||
name: subFolderName,
|
||||
path: subFolderPath,
|
||||
@@ -295,7 +304,8 @@ export async function getFolderStructure(
|
||||
ignoredFolders: options?.ignoredFolders ?? DEFAULT_IGNORED_FOLDERS,
|
||||
fileIncludePattern: options?.fileIncludePattern,
|
||||
fileService: options?.fileService,
|
||||
respectGitIgnore: options?.respectGitIgnore ?? true,
|
||||
fileFilteringOptions:
|
||||
options?.fileFilteringOptions ?? DEFAULT_FILE_FILTERING_OPTIONS,
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
@@ -15,6 +15,10 @@ import {
|
||||
} from '../tools/memoryTool.js';
|
||||
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
|
||||
import { processImports } from './memoryImportProcessor.js';
|
||||
import {
|
||||
DEFAULT_MEMORY_FILE_FILTERING_OPTIONS,
|
||||
FileFilteringOptions,
|
||||
} from '../config/config.js';
|
||||
|
||||
// Simple console logger, similar to the one previously in CLI's config.ts
|
||||
// TODO: Integrate with a more robust server-side logger if available/appropriate.
|
||||
@@ -85,6 +89,7 @@ async function getGeminiMdFilePathsInternal(
|
||||
debugMode: boolean,
|
||||
fileService: FileDiscoveryService,
|
||||
extensionContextFilePaths: string[] = [],
|
||||
fileFilteringOptions: FileFilteringOptions,
|
||||
): Promise<string[]> {
|
||||
const allPaths = new Set<string>();
|
||||
const geminiMdFilenames = getAllGeminiMdFilenames();
|
||||
@@ -181,11 +186,18 @@ async function getGeminiMdFilePathsInternal(
|
||||
}
|
||||
upwardPaths.forEach((p) => allPaths.add(p));
|
||||
|
||||
// Merge options with memory defaults, with options taking precedence
|
||||
const mergedOptions = {
|
||||
...DEFAULT_MEMORY_FILE_FILTERING_OPTIONS,
|
||||
...fileFilteringOptions,
|
||||
};
|
||||
|
||||
const downwardPaths = await bfsFileSearch(resolvedCwd, {
|
||||
fileName: geminiMdFilename,
|
||||
maxDirs: MAX_DIRECTORIES_TO_SCAN_FOR_MEMORY,
|
||||
debug: debugMode,
|
||||
fileService,
|
||||
fileFilteringOptions: mergedOptions, // Pass merged options as fileFilter
|
||||
});
|
||||
downwardPaths.sort(); // Sort for consistent ordering, though hierarchy might be more complex
|
||||
if (debugMode && downwardPaths.length > 0)
|
||||
@@ -282,11 +294,13 @@ export async function loadServerHierarchicalMemory(
|
||||
debugMode: boolean,
|
||||
fileService: FileDiscoveryService,
|
||||
extensionContextFilePaths: string[] = [],
|
||||
fileFilteringOptions?: FileFilteringOptions,
|
||||
): Promise<{ memoryContent: string; fileCount: number }> {
|
||||
if (debugMode)
|
||||
logger.debug(
|
||||
`Loading server hierarchical memory for CWD: ${currentWorkingDirectory}`,
|
||||
);
|
||||
|
||||
// For the server, homedir() refers to the server process's home.
|
||||
// This is consistent with how MemoryTool already finds the global path.
|
||||
const userHomePath = homedir();
|
||||
@@ -296,6 +310,7 @@ export async function loadServerHierarchicalMemory(
|
||||
debugMode,
|
||||
fileService,
|
||||
extensionContextFilePaths,
|
||||
fileFilteringOptions || DEFAULT_MEMORY_FILE_FILTERING_OPTIONS,
|
||||
);
|
||||
if (filePaths.length === 0) {
|
||||
if (debugMode) logger.debug('No GEMINI.md files found in hierarchy.');
|
||||
|
||||
Reference in New Issue
Block a user