From 6aff1c57d06c1ec0dcf8e00491bdad23f285c086 Mon Sep 17 00:00:00 2001 From: Viacheslav Shvets Date: Fri, 10 Jan 2025 11:25:24 +0200 Subject: [PATCH 1/2] fix: handle git worktree repositories correctly Previously repomix failed when processing git worktree directories due to .git being a file rather than directory. Now it correctly adjusts ignore patterns for worktree references. --- src/core/file/fileSearch.ts | 40 ++++++++++++++++-- tests/core/file/fileSearch.test.ts | 68 ++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 3 deletions(-) diff --git a/src/core/file/fileSearch.ts b/src/core/file/fileSearch.ts index 8bf343c6..8e5cf374 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,11 @@ 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 65b9e227..c09a325e 100644 --- a/tests/core/file/fileSearch.test.ts +++ b/tests/core/file/fileSearch.test.ts @@ -272,5 +272,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 fs.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 fs.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']); + }); }); }); From c7061a46f873ad6c3870f9fc3ea07c4863658436 Mon Sep 17 00:00:00 2001 From: Kazuki Yamada Date: Sat, 11 Jan 2025 18:33:47 +0900 Subject: [PATCH 2/2] refact(file): lint --- src/core/file/fileSearch.ts | 16 ++++++---------- tests/core/file/fileSearch.test.ts | 5 +++-- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/core/file/fileSearch.ts b/src/core/file/fileSearch.ts index 8e5cf374..8e3e885e 100644 --- a/src/core/file/fileSearch.ts +++ b/src/core/file/fileSearch.ts @@ -50,8 +50,8 @@ const isGitWorktreeRef = async (gitPath: string): Promise => { return false; } - const content = await fs.readFile(gitPath, "utf8"); - return content.startsWith("gitdir:"); + const content = await fs.readFile(gitPath, 'utf8'); + return content.startsWith('gitdir:'); } catch { return false; } @@ -82,17 +82,17 @@ export const searchFiles = async (rootDir: string, config: RepomixConfigMerged): logger.trace('Ignore file patterns:', ignoreFilePatterns); // Check if .git is a worktree reference - const gitPath = path.join(rootDir, ".git"); + 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/**"); + const gitIndex = adjustedIgnorePatterns.indexOf('.git/**'); if (gitIndex !== -1) { adjustedIgnorePatterns.splice(gitIndex, 1); - adjustedIgnorePatterns.push(".git"); + adjustedIgnorePatterns.push('.git'); } } @@ -127,11 +127,7 @@ export const searchFiles = async (rootDir: string, config: RepomixConfigMerged): followSymbolicLinks: false, }); - emptyDirPaths = await findEmptyDirectories( - rootDir, - directories, - adjustedIgnorePatterns - ); + 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 c09a325e..ab89b714 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'; @@ -280,7 +281,7 @@ node_modules // Mock fs.stat and fs.readFile for .git file vi.mocked(fs.stat).mockResolvedValue({ isFile: () => true, - } as fs.Stats); + } as Stats); vi.mocked(fs.readFile).mockResolvedValue(gitWorktreeContent); // Mock globby to return some test files @@ -313,7 +314,7 @@ node_modules // Mock .git as a directory vi.mocked(fs.stat).mockResolvedValue({ isFile: () => false, - } as fs.Stats); + } as Stats); // Mock globby to return some test files vi.mocked(globby).mockResolvedValue(['file1.js', 'file2.js']);