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
28 changes: 18 additions & 10 deletions src/core/output/outputGenerate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ export const generateOutput = async (
gitDiffResult: GitDiffResult | undefined = undefined,
gitLogResult: GitLogResult | undefined = undefined,
filePathsByRoot?: FilesByRoot[],
emptyDirPaths?: string[],
deps = {
buildOutputGeneratorContext,
generateHandlebarOutput,
Expand All @@ -277,6 +278,7 @@ export const generateOutput = async (
gitDiffResult,
gitLogResult,
filePathsByRoot,
emptyDirPaths,
);
const renderContext = createRenderContext(outputGeneratorContext);

Expand All @@ -303,6 +305,7 @@ export const buildOutputGeneratorContext = async (
gitDiffResult: GitDiffResult | undefined = undefined,
gitLogResult: GitLogResult | undefined = undefined,
filePathsByRoot?: FilesByRoot[],
emptyDirPaths?: string[],
deps = {
listDirectories,
listFiles,
Expand Down Expand Up @@ -356,16 +359,21 @@ export const buildOutputGeneratorContext = async (
);
}
} else if (config.output.directoryStructure && config.output.includeEmptyDirectories) {
// Default behavior: include empty directories only
try {
const results = await Promise.all(rootDirs.map((rootDir) => deps.searchFiles(rootDir, config)));
const merged = results.flatMap((r) => r.emptyDirPaths);
directoryPathsForTree = [...new Set(merged)].sort();
} catch (error) {
throw new RepomixError(
`Failed to search for empty directories: ${error instanceof Error ? error.message : String(error)}`,
error instanceof Error ? { cause: error } : undefined,
);
// Reuse pre-computed emptyDirPaths from the initial searchFiles call when available,
// avoiding a redundant full directory scan.
if (emptyDirPaths) {
directoryPathsForTree = emptyDirPaths;
} else {
try {
const results = await Promise.all(rootDirs.map((rootDir) => deps.searchFiles(rootDir, config)));
const merged = results.flatMap((r) => r.emptyDirPaths);
directoryPathsForTree = [...new Set(merged)].sort();
} catch (error) {
throw new RepomixError(
`Failed to search for empty directories: ${error instanceof Error ? error.message : String(error)}`,
error instanceof Error ? { cause: error } : undefined,
);
}
}
}

Expand Down
6 changes: 6 additions & 0 deletions src/core/output/outputSplit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ const renderGroups = async (
gitDiffResult: GitDiffResult | undefined,
gitLogResult: GitLogResult | undefined,
filePathsByRoot: FilesByRoot[] | undefined,
emptyDirPaths: string[] | undefined,
generateOutput: GenerateOutputFn,
): Promise<string> => {
const chunkProcessedFiles = groupsToRender.flatMap((g) => g.processedFiles);
Expand All @@ -115,6 +116,7 @@ const renderGroups = async (
partIndex === 1 ? gitDiffResult : undefined,
partIndex === 1 ? gitLogResult : undefined,
filePathsByRoot,
emptyDirPaths,
);
};

Expand All @@ -128,6 +130,7 @@ export const generateSplitOutputParts = async ({
gitLogResult,
progressCallback,
filePathsByRoot,
emptyDirPaths,
deps,
}: {
rootDirs: string[];
Expand All @@ -139,6 +142,7 @@ export const generateSplitOutputParts = async ({
gitLogResult: GitLogResult | undefined;
progressCallback: RepomixProgressCallback;
filePathsByRoot?: FilesByRoot[];
emptyDirPaths?: string[];
deps: {
generateOutput: GenerateOutputFn;
};
Expand Down Expand Up @@ -178,6 +182,7 @@ export const generateSplitOutputParts = async ({
gitDiffResult,
gitLogResult,
filePathsByRoot,
emptyDirPaths,
deps.generateOutput,
);
const nextBytes = getUtf8ByteLength(nextContent);
Expand Down Expand Up @@ -215,6 +220,7 @@ export const generateSplitOutputParts = async ({
gitDiffResult,
gitLogResult,
filePathsByRoot,
emptyDirPaths,
deps.generateOutput,
);
const singleGroupBytes = getUtf8ByteLength(singleGroupContent);
Expand Down
21 changes: 14 additions & 7 deletions src/core/packager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,22 +70,28 @@ export const pack = async (
logMemoryUsage('Pack - Start');

progressCallback('Searching for files...');
const filePathsByDir = await withMemoryLogging('Search Files', async () =>
const searchResultsByDir = await withMemoryLogging('Search Files', async () =>
Promise.all(
rootDirs.map(async (rootDir) => ({
rootDir,
filePaths: (await deps.searchFiles(rootDir, config, explicitFiles)).filePaths,
})),
rootDirs.map(async (rootDir) => {
const result = await deps.searchFiles(rootDir, config, explicitFiles);
return { rootDir, filePaths: result.filePaths, emptyDirPaths: result.emptyDirPaths };
}),
),
);

// Deduplicate and sort empty directory paths for reuse during output generation,
// avoiding a redundant searchFiles call in buildOutputGeneratorContext.
const emptyDirPaths = config.output.includeEmptyDirectories
? [...new Set(searchResultsByDir.flatMap((r) => r.emptyDirPaths))].sort()
: undefined;

// Sort file paths
progressCallback('Sorting files...');
const allFilePaths = filePathsByDir.flatMap(({ filePaths }) => filePaths);
const allFilePaths = searchResultsByDir.flatMap(({ filePaths }) => filePaths);
const sortedFilePaths = deps.sortPaths(allFilePaths);

// Regroup sorted file paths by rootDir using Set for O(1) membership checks
const filePathSetByDir = new Map(filePathsByDir.map(({ rootDir, filePaths }) => [rootDir, new Set(filePaths)]));
const filePathSetByDir = new Map(searchResultsByDir.map(({ rootDir, filePaths }) => [rootDir, new Set(filePaths)]));
const sortedFilePathsByDir = rootDirs.map((rootDir) => ({
rootDir,
filePaths: sortedFilePaths.filter((filePath) => filePathSetByDir.get(rootDir)?.has(filePath) ?? false),
Expand Down Expand Up @@ -194,6 +200,7 @@ export const pack = async (
gitLogResult,
progressCallback,
filePathsByRoot,
emptyDirPaths,
);

const outputForMetricsPromise = outputPromise.then((r) => r.outputForMetrics);
Expand Down
17 changes: 16 additions & 1 deletion src/core/packager/produceOutput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const produceOutput = async (
gitLogResult: GitLogResult | undefined,
progressCallback: RepomixProgressCallback,
filePathsByRoot?: FilesByRoot[],
emptyDirPaths?: string[],
overrideDeps: Partial<typeof defaultDeps> = {},
): Promise<ProduceOutputResult> => {
const deps = { ...defaultDeps, ...overrideDeps };
Expand All @@ -47,6 +48,7 @@ export const produceOutput = async (
gitLogResult,
progressCallback,
filePathsByRoot,
emptyDirPaths,
deps,
);
}
Expand All @@ -60,6 +62,7 @@ export const produceOutput = async (
gitLogResult,
progressCallback,
filePathsByRoot,
emptyDirPaths,
deps,
);
};
Expand All @@ -74,6 +77,7 @@ const generateAndWriteSplitOutput = async (
gitLogResult: GitLogResult | undefined,
progressCallback: RepomixProgressCallback,
filePathsByRoot: FilesByRoot[] | undefined,
emptyDirPaths: string[] | undefined,
deps: typeof defaultDeps,
): Promise<ProduceOutputResult> => {
const parts = await withMemoryLogging('Generate Split Output', async () => {
Expand All @@ -87,6 +91,7 @@ const generateAndWriteSplitOutput = async (
gitLogResult,
progressCallback,
filePathsByRoot,
emptyDirPaths,
deps: {
generateOutput: deps.generateOutput,
},
Expand Down Expand Up @@ -125,10 +130,20 @@ const generateAndWriteSingleOutput = async (
gitLogResult: GitLogResult | undefined,
progressCallback: RepomixProgressCallback,
filePathsByRoot: FilesByRoot[] | undefined,
emptyDirPaths: string[] | undefined,
deps: typeof defaultDeps,
): Promise<ProduceOutputResult> => {
const output = await withMemoryLogging('Generate Output', () =>
deps.generateOutput(rootDirs, config, processedFiles, allFilePaths, gitDiffResult, gitLogResult, filePathsByRoot),
deps.generateOutput(
rootDirs,
config,
processedFiles,
allFilePaths,
gitDiffResult,
gitLogResult,
filePathsByRoot,
emptyDirPaths,
),
);

progressCallback('Writing output file...');
Expand Down
2 changes: 2 additions & 0 deletions tests/core/output/diffsInOutput.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ index 123..456 100644
gitDiffResult,
undefined,
undefined,
undefined,
{
buildOutputGeneratorContext: mockBuildOutputGeneratorContext,
generateHandlebarOutput: mockGenerateHandlebarOutput,
Expand Down Expand Up @@ -204,6 +205,7 @@ index 123..456 100644
gitDiffResult,
undefined,
undefined,
undefined,
{
buildOutputGeneratorContext: mockBuildOutputGeneratorContext,
generateHandlebarOutput: mockGenerateHandlebarOutput,
Expand Down
107 changes: 106 additions & 1 deletion tests/core/output/flagFullDirectoryStructure.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, expect, test } from 'vitest';
import { describe, expect, test, vi } from 'vitest';
import type { RepomixConfigMerged } from '../../../src/config/configSchema.js';
import type { ProcessedFile } from '../../../src/core/file/fileTypes.js';
import { buildOutputGeneratorContext } from '../../../src/core/output/outputGenerate.js';
Expand Down Expand Up @@ -72,6 +72,7 @@ describe('includeFullDirectoryStructure flag', () => {
undefined,
undefined,
undefined,
undefined,
deps,
);

Expand Down Expand Up @@ -105,6 +106,7 @@ describe('includeFullDirectoryStructure flag', () => {
undefined,
undefined,
undefined,
undefined,
deps,
);

Expand All @@ -117,3 +119,106 @@ describe('includeFullDirectoryStructure flag', () => {
expect(ctx.treeString).toContain('index.ts');
});
});

describe('includeEmptyDirectories with pre-computed emptyDirPaths', () => {
const createEmptyDirConfig = (overrides: Partial<RepomixConfigMerged> = {}): RepomixConfigMerged => ({
cwd: '/repo',
input: { maxFileSize: 1024 * 1024 },
output: {
filePath: 'repomix-output.json',
style: 'json',
parsableStyle: false,
headerText: undefined,
instructionFilePath: undefined,
fileSummary: true,
directoryStructure: true,
files: true,
removeComments: false,
removeEmptyLines: false,
compress: false,
topFilesLength: 5,
showLineNumbers: false,
truncateBase64: false,
copyToClipboard: false,
includeEmptyDirectories: true,
includeFullDirectoryStructure: false,
tokenCountTree: false,
git: {
sortByChanges: false,
sortByChangesMaxCommits: 10,
includeDiffs: false,
includeLogs: false,
includeLogsCount: 5,
},
},
include: [],
ignore: {
useGitignore: true,
useDotIgnore: true,
useDefaultPatterns: true,
customPatterns: [],
},
security: { enableSecurityCheck: true },
tokenCount: { encoding: 'cl100k_base' },
...overrides,
});

test('uses pre-computed emptyDirPaths and skips searchFiles call', async () => {
const config = createEmptyDirConfig();
const processedFiles: ProcessedFile[] = [{ path: 'src/index.ts', content: 'export const a = 1;\n' }];
const allFilePaths = processedFiles.map((f) => f.path);
const preComputedEmptyDirs = ['empty-dir'];

const deps = {
listDirectories: vi.fn(),
listFiles: vi.fn(),
searchFiles: vi.fn().mockResolvedValue({ filePaths: allFilePaths, emptyDirPaths: ['should-not-use'] }),
};

const ctx = await buildOutputGeneratorContext(
['/repo'],
config,
allFilePaths,
processedFiles,
undefined,
undefined,
undefined,
preComputedEmptyDirs,
deps,
);

// searchFiles should NOT be called when emptyDirPaths is provided
expect(deps.searchFiles).not.toHaveBeenCalled();
// The pre-computed empty dir should appear in the tree
expect(ctx.treeString).toContain('empty-dir');
});

test('falls back to searchFiles when emptyDirPaths is not provided', async () => {
const config = createEmptyDirConfig();
const processedFiles: ProcessedFile[] = [{ path: 'src/index.ts', content: 'export const a = 1;\n' }];
const allFilePaths = processedFiles.map((f) => f.path);

const deps = {
listDirectories: vi.fn(),
listFiles: vi.fn(),
searchFiles: vi.fn().mockResolvedValue({ filePaths: allFilePaths, emptyDirPaths: ['fallback-empty-dir'] }),
};

const ctx = await buildOutputGeneratorContext(
['/repo'],
config,
allFilePaths,
processedFiles,
undefined,
undefined,
undefined,
undefined,
deps,
);

// searchFiles SHOULD be called as fallback
expect(deps.searchFiles).toHaveBeenCalledWith('/repo', config);
// The fallback empty dir should appear in the tree
expect(ctx.treeString).toContain('fallback-empty-dir');
});
});
2 changes: 2 additions & 0 deletions tests/core/output/outputGenerate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ describe('outputGenerate', () => {
undefined,
undefined,
undefined,
undefined,
mockDeps,
);

Expand All @@ -70,6 +71,7 @@ describe('outputGenerate', () => {
undefined,
undefined,
undefined,
undefined,
);
expect(output).toBe('mock output');
});
Expand Down
Loading
Loading