Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/core/file/fileSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ const isGitWorktreeRef = async (gitPath: string): Promise<boolean> => {
}
};

/**
* 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<FileSearchResult> => {
// First check directory permissions
Expand All @@ -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([
Expand Down
47 changes: 47 additions & 0 deletions tests/core/file/fileSearch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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\\}');
});
});
Loading