diff --git a/src/core/file/fileSearch.ts b/src/core/file/fileSearch.ts index 8bf343c63..8e3e885e4 100644 --- a/src/core/file/fileSearch.ts +++ b/src/core/file/fileSearch.ts @@ -42,6 +42,21 @@ const findEmptyDirectories = async ( return emptyDirs; }; +// Check if a path is a git worktree reference file +const isGitWorktreeRef = async (gitPath: string): Promise => { + try { + const stats = await fs.stat(gitPath); + if (!stats.isFile()) { + return false; + } + + const content = await fs.readFile(gitPath, 'utf8'); + return content.startsWith('gitdir:'); + } catch { + return false; + } +}; + // Get all file paths considering the config export const searchFiles = async (rootDir: string, config: RepomixConfigMerged): Promise => { // First check directory permissions @@ -66,9 +81,24 @@ export const searchFiles = async (rootDir: string, config: RepomixConfigMerged): logger.trace('Ignore patterns:', ignorePatterns); logger.trace('Ignore file patterns:', ignoreFilePatterns); + // Check if .git is a worktree reference + const gitPath = path.join(rootDir, '.git'); + const isWorktree = await isGitWorktreeRef(gitPath); + + // Modify ignore patterns for git worktree + const adjustedIgnorePatterns = [...ignorePatterns]; + if (isWorktree) { + // Remove '.git/**' pattern and add '.git' to ignore the reference file + const gitIndex = adjustedIgnorePatterns.indexOf('.git/**'); + if (gitIndex !== -1) { + adjustedIgnorePatterns.splice(gitIndex, 1); + adjustedIgnorePatterns.push('.git'); + } + } + const filePaths = await globby(includePatterns, { cwd: rootDir, - ignore: [...ignorePatterns], + ignore: [...adjustedIgnorePatterns], ignoreFiles: [...ignoreFilePatterns], onlyFiles: true, absolute: false, @@ -89,7 +119,7 @@ export const searchFiles = async (rootDir: string, config: RepomixConfigMerged): if (config.output.includeEmptyDirectories) { const directories = await globby(includePatterns, { cwd: rootDir, - ignore: [...ignorePatterns], + ignore: [...adjustedIgnorePatterns], ignoreFiles: [...ignoreFilePatterns], onlyDirectories: true, absolute: false, @@ -97,7 +127,7 @@ export const searchFiles = async (rootDir: string, config: RepomixConfigMerged): followSymbolicLinks: false, }); - emptyDirPaths = await findEmptyDirectories(rootDir, directories, ignorePatterns); + emptyDirPaths = await findEmptyDirectories(rootDir, directories, adjustedIgnorePatterns); } logger.trace(`Filtered ${filePaths.length} files`); diff --git a/tests/core/file/fileSearch.test.ts b/tests/core/file/fileSearch.test.ts index 65b9e227d..ab89b7142 100644 --- a/tests/core/file/fileSearch.test.ts +++ b/tests/core/file/fileSearch.test.ts @@ -1,3 +1,4 @@ +import type { Stats } from 'node:fs'; import * as fs from 'node:fs/promises'; import path from 'node:path'; import process from 'node:process'; @@ -272,5 +273,73 @@ node_modules expect(result.filePaths).toContain('root/subdir/ignored.js'); expect(result.emptyDirPaths).toEqual([]); }); + + test('should handle git worktree correctly', async () => { + // Mock .git file content for worktree + const gitWorktreeContent = 'gitdir: /path/to/main/repo/.git/worktrees/feature-branch'; + + // Mock fs.stat and fs.readFile for .git file + vi.mocked(fs.stat).mockResolvedValue({ + isFile: () => true, + } as Stats); + vi.mocked(fs.readFile).mockResolvedValue(gitWorktreeContent); + + // Mock globby to return some test files + vi.mocked(globby).mockResolvedValue(['file1.js', 'file2.js']); + + const mockConfig = createMockConfig({ + ignore: { + useGitignore: true, + useDefaultPatterns: true, + customPatterns: [], + }, + }); + + const result = await searchFiles('/test/dir', mockConfig); + + // Check that globby was called with correct ignore patterns + const globbyCall = vi.mocked(globby).mock.calls[0]; + const ignorePatterns = globbyCall[1]?.ignore as string[]; + + // Verify .git file (not directory) is in ignore patterns + expect(ignorePatterns).toContain('.git'); + // Verify .git/** is not in ignore patterns + expect(ignorePatterns).not.toContain('.git/**'); + + // Verify the files were returned correctly + expect(result.filePaths).toEqual(['file1.js', 'file2.js']); + }); + + test('should handle regular git repository correctly', async () => { + // Mock .git as a directory + vi.mocked(fs.stat).mockResolvedValue({ + isFile: () => false, + } as Stats); + + // Mock globby to return some test files + vi.mocked(globby).mockResolvedValue(['file1.js', 'file2.js']); + + const mockConfig = createMockConfig({ + ignore: { + useGitignore: true, + useDefaultPatterns: true, + customPatterns: [], + }, + }); + + const result = await searchFiles('/test/dir', mockConfig); + + // Check that globby was called with correct ignore patterns + const globbyCall = vi.mocked(globby).mock.calls[0]; + const ignorePatterns = globbyCall[1]?.ignore as string[]; + + // Verify .git/** is in ignore patterns for regular git repos + expect(ignorePatterns).toContain('.git/**'); + // Verify just .git is not in ignore patterns + expect(ignorePatterns).not.toContain('.git'); + + // Verify the files were returned correctly + expect(result.filePaths).toEqual(['file1.js', 'file2.js']); + }); }); });