diff --git a/src/core/file/fileSearch.ts b/src/core/file/fileSearch.ts index 8e3e885e4..e632d8a4a 100644 --- a/src/core/file/fileSearch.ts +++ b/src/core/file/fileSearch.ts @@ -57,6 +57,17 @@ const isGitWorktreeRef = async (gitPath: string): Promise => { } }; +/** + * Escapes special characters in glob patterns to handle paths with parentheses. + * Example: "src/(categories)" -> "src/\\(categories\\)" + */ +export const escapeGlobPattern = (pattern: string): string => { + // First escape backslashes + const escapedBackslashes = pattern.replace(/\\/g, '\\\\'); + // Then escape special characters + return escapedBackslashes.replace(/[()[\]{}]/g, '\\$&'); +}; + // Get all file paths considering the config export const searchFiles = async (rootDir: string, config: RepomixConfigMerged): Promise => { // First check directory permissions @@ -69,7 +80,8 @@ export const searchFiles = async (rootDir: string, config: RepomixConfigMerged): throw new Error(`Cannot access directory ${rootDir}: ${permissionCheck.error?.message}`); } - const includePatterns = config.include.length > 0 ? config.include : ['**/*']; + const includePatterns = + config.include.length > 0 ? config.include.map((pattern) => escapeGlobPattern(pattern)) : ['**/*']; try { const [ignorePatterns, ignoreFilePatterns] = await Promise.all([ diff --git a/tests/core/file/fileSearch.test.ts b/tests/core/file/fileSearch.test.ts index ab89b7142..35ff061b2 100644 --- a/tests/core/file/fileSearch.test.ts +++ b/tests/core/file/fileSearch.test.ts @@ -6,6 +6,7 @@ import { globby } from 'globby'; import { minimatch } from 'minimatch'; import { beforeEach, describe, expect, test, vi } from 'vitest'; import { + escapeGlobPattern, getIgnoreFilePatterns, getIgnorePatterns, parseIgnoreContent, @@ -342,4 +343,50 @@ node_modules expect(result.filePaths).toEqual(['file1.js', 'file2.js']); }); }); + + describe('escapeGlobPattern', () => { + test('should escape parentheses in pattern', () => { + const pattern = 'src/(categories)/**/*.ts'; + expect(escapeGlobPattern(pattern)).toBe('src/\\(categories\\)/**/*.ts'); + }); + + test('should escape multiple types of brackets', () => { + const pattern = 'src/(auth)/[id]/{slug}/**/*.ts'; + expect(escapeGlobPattern(pattern)).toBe('src/\\(auth\\)/\\[id\\]/\\{slug\\}/**/*.ts'); + }); + + test('should handle nested brackets', () => { + const pattern = 'src/(auth)/([id])/**/*.ts'; + expect(escapeGlobPattern(pattern)).toBe('src/\\(auth\\)/\\(\\[id\\]\\)/**/*.ts'); + }); + + test('should handle empty string', () => { + expect(escapeGlobPattern('')).toBe(''); + }); + + test('should not modify patterns without special characters', () => { + const pattern = 'src/components/**/*.ts'; + expect(escapeGlobPattern(pattern)).toBe(pattern); + }); + + test('should handle multiple occurrences of the same bracket type', () => { + const pattern = 'src/(auth)/(settings)/**/*.ts'; + expect(escapeGlobPattern(pattern)).toBe('src/\\(auth\\)/\\(settings\\)/**/*.ts'); + }); + }); + + test('should escape backslashes in pattern', () => { + const pattern = 'src\\temp\\(categories)'; + expect(escapeGlobPattern(pattern)).toBe('src\\\\temp\\\\\\(categories\\)'); + }); + + test('should handle patterns with already escaped special characters', () => { + const pattern = 'src\\\\(categories)'; + expect(escapeGlobPattern(pattern)).toBe('src\\\\\\\\\\(categories\\)'); + }); + + test('should handle patterns with mixed backslashes and special characters', () => { + const pattern = 'src\\temp\\[id]\\{slug}'; + expect(escapeGlobPattern(pattern)).toBe('src\\\\temp\\\\\\[id\\]\\\\\\{slug\\}'); + }); });