reuse filtering service in getFolderStructure (#1016)

This commit is contained in:
Anas H. Sulaiman 2025-06-13 14:26:31 -04:00 committed by GitHub
parent 1f63f3331f
commit 084b58a50e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 24 additions and 33 deletions

View File

@ -19,6 +19,7 @@ import { Config } from '../config/config.js';
import { Turn } from './turn.js'; import { Turn } from './turn.js';
import { getCoreSystemPrompt } from './prompts.js'; import { getCoreSystemPrompt } from './prompts.js';
import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js'; import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js';
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
// --- Mocks --- // --- Mocks ---
const mockChatCreateFn = vi.fn(); const mockChatCreateFn = vi.fn();
@ -99,6 +100,7 @@ describe('Gemini Client (client.ts)', () => {
getFunctionDeclarations: vi.fn().mockReturnValue([]), getFunctionDeclarations: vi.fn().mockReturnValue([]),
getTool: vi.fn().mockReturnValue(null), getTool: vi.fn().mockReturnValue(null),
}; };
const fileService = new FileDiscoveryService('/test/dir');
const MockedConfig = vi.mocked(Config, true); const MockedConfig = vi.mocked(Config, true);
MockedConfig.mockImplementation(() => { MockedConfig.mockImplementation(() => {
const mock = { const mock = {
@ -118,6 +120,7 @@ describe('Gemini Client (client.ts)', () => {
getSessionId: vi.fn().mockReturnValue('test-session-id'), getSessionId: vi.fn().mockReturnValue('test-session-id'),
getProxy: vi.fn().mockReturnValue(undefined), getProxy: vi.fn().mockReturnValue(undefined),
getWorkingDir: vi.fn().mockReturnValue('/test/dir'), getWorkingDir: vi.fn().mockReturnValue('/test/dir'),
getFileService: vi.fn().mockReturnValue(fileService),
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
return mock as any; return mock as any;

View File

@ -85,7 +85,9 @@ export class GeminiClient {
day: 'numeric', day: 'numeric',
}); });
const platform = process.platform; const platform = process.platform;
const folderStructure = await getFolderStructure(cwd); const folderStructure = await getFolderStructure(cwd, {
fileService: await this.config.getFileService(),
});
const context = ` const context = `
Okay, just setting up the context for our chat. Okay, just setting up the context for our chat.
Today is ${today}. Today is ${today}.

View File

@ -11,6 +11,7 @@ import { Dirent as FSDirent } from 'fs';
import * as nodePath from 'path'; import * as nodePath from 'path';
import { getFolderStructure } from './getFolderStructure.js'; import { getFolderStructure } from './getFolderStructure.js';
import * as gitUtils from './gitUtils.js'; import * as gitUtils from './gitUtils.js';
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
vi.mock('path', async (importOriginal) => { vi.mock('path', async (importOriginal) => {
const original = (await importOriginal()) as typeof nodePath; const original = (await importOriginal()) as typeof nodePath;
@ -319,8 +320,10 @@ describe('getFolderStructure gitignore', () => {
}); });
it('should ignore files and folders specified in .gitignore', async () => { it('should ignore files and folders specified in .gitignore', async () => {
const fileService = new FileDiscoveryService('/test/project');
await fileService.initialize();
const structure = await getFolderStructure('/test/project', { const structure = await getFolderStructure('/test/project', {
projectRoot: '/test/project', fileService,
}); });
expect(structure).not.toContain('ignored.txt'); expect(structure).not.toContain('ignored.txt');
expect(structure).toContain('node_modules/...'); expect(structure).toContain('node_modules/...');
@ -328,9 +331,10 @@ describe('getFolderStructure gitignore', () => {
}); });
it('should not ignore files if respectGitIgnore is false', async () => { it('should not ignore files if respectGitIgnore is false', async () => {
const fileService = new FileDiscoveryService('/test/project');
await fileService.initialize({ respectGitIgnore: false });
const structure = await getFolderStructure('/test/project', { const structure = await getFolderStructure('/test/project', {
projectRoot: '/test/project', fileService,
respectGitIgnore: false,
}); });
expect(structure).toContain('ignored.txt'); expect(structure).toContain('ignored.txt');
// node_modules is still ignored by default // node_modules is still ignored by default

View File

@ -8,8 +8,7 @@ import * as fs from 'fs/promises';
import { Dirent } from 'fs'; import { Dirent } from 'fs';
import * as path from 'path'; import * as path from 'path';
import { getErrorMessage, isNodeError } from './errors.js'; import { getErrorMessage, isNodeError } from './errors.js';
import { GitIgnoreParser, GitIgnoreFilter } from './gitIgnoreParser.js'; import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
import { isGitRepository } from './gitUtils.js';
const MAX_ITEMS = 200; const MAX_ITEMS = 200;
const TRUNCATION_INDICATOR = '...'; const TRUNCATION_INDICATOR = '...';
@ -25,18 +24,16 @@ interface FolderStructureOptions {
ignoredFolders?: Set<string>; ignoredFolders?: Set<string>;
/** Optional regex to filter included files by name. */ /** Optional regex to filter included files by name. */
fileIncludePattern?: RegExp; fileIncludePattern?: RegExp;
/** Whether to respect .gitignore patterns. Defaults to true. */ /** For filtering files. */
respectGitIgnore?: boolean; fileService?: FileDiscoveryService;
/** The root of the project, used for gitignore resolution. */
projectRoot?: string;
} }
// Define a type for the merged options where fileIncludePattern remains optional // Define a type for the merged options where fileIncludePattern remains optional
type MergedFolderStructureOptions = Required< type MergedFolderStructureOptions = Required<
Omit<FolderStructureOptions, 'fileIncludePattern' | 'projectRoot'> Omit<FolderStructureOptions, 'fileIncludePattern' | 'fileService'>
> & { > & {
fileIncludePattern?: RegExp; fileIncludePattern?: RegExp;
projectRoot?: string; fileService?: FileDiscoveryService;
}; };
/** Represents the full, unfiltered information about a folder and its contents. */ /** Represents the full, unfiltered information about a folder and its contents. */
@ -59,7 +56,6 @@ interface FullFolderInfo {
async function readFullStructure( async function readFullStructure(
rootPath: string, rootPath: string,
options: MergedFolderStructureOptions, options: MergedFolderStructureOptions,
gitIgnoreFilter: GitIgnoreFilter | null,
): Promise<FullFolderInfo | null> { ): Promise<FullFolderInfo | null> {
const rootName = path.basename(rootPath); const rootName = path.basename(rootPath);
const rootNode: FullFolderInfo = { const rootNode: FullFolderInfo = {
@ -128,8 +124,8 @@ async function readFullStructure(
} }
const fileName = entry.name; const fileName = entry.name;
const filePath = path.join(currentPath, fileName); const filePath = path.join(currentPath, fileName);
if (gitIgnoreFilter) { if (options.fileService) {
if (gitIgnoreFilter.isIgnored(filePath)) { if (options.fileService.shouldIgnoreFile(filePath)) {
continue; continue;
} }
} }
@ -163,8 +159,8 @@ async function readFullStructure(
const subFolderPath = path.join(currentPath, subFolderName); const subFolderPath = path.join(currentPath, subFolderName);
let isIgnoredByGit = false; let isIgnoredByGit = false;
if (gitIgnoreFilter) { if (options?.fileService) {
if (gitIgnoreFilter.isIgnored(subFolderPath)) { if (options.fileService.shouldIgnoreFile(subFolderPath)) {
isIgnoredByGit = true; isIgnoredByGit = true;
} }
} }
@ -296,26 +292,12 @@ export async function getFolderStructure(
maxItems: options?.maxItems ?? MAX_ITEMS, maxItems: options?.maxItems ?? MAX_ITEMS,
ignoredFolders: options?.ignoredFolders ?? DEFAULT_IGNORED_FOLDERS, ignoredFolders: options?.ignoredFolders ?? DEFAULT_IGNORED_FOLDERS,
fileIncludePattern: options?.fileIncludePattern, fileIncludePattern: options?.fileIncludePattern,
respectGitIgnore: options?.respectGitIgnore ?? true, fileService: options?.fileService,
projectRoot: options?.projectRoot ?? resolvedPath,
}; };
let gitIgnoreFilter: GitIgnoreFilter | null = null;
if (mergedOptions.respectGitIgnore && mergedOptions.projectRoot) {
if (isGitRepository(mergedOptions.projectRoot)) {
const parser = new GitIgnoreParser(mergedOptions.projectRoot);
await parser.initialize();
gitIgnoreFilter = parser;
}
}
try { try {
// 1. Read the structure using BFS, respecting maxItems // 1. Read the structure using BFS, respecting maxItems
const structureRoot = await readFullStructure( const structureRoot = await readFullStructure(resolvedPath, mergedOptions);
resolvedPath,
mergedOptions,
gitIgnoreFilter,
);
if (!structureRoot) { if (!structureRoot) {
return `Error: Could not read directory "${resolvedPath}". Check path and permissions.`; return `Error: Could not read directory "${resolvedPath}". Check path and permissions.`;