Skip to content
Closed
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,7 @@ Instruction
#### File Selection Options
- `--include <patterns>`: List of include patterns (comma-separated)
- `-i, --ignore <patterns>`: Additional ignore patterns (comma-separated)
- `--ignore-content <patterns>`: Patterns to include files but skip their content (comma-separated; prefix with `!` to keep specific paths)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The description for --ignore-content could be clearer. "Patterns to include files but skip their content" might be confusing, as this option doesn't handle file inclusion but rather content exclusion for already included files. A more accurate description would focus on skipping content.

Suggested change
- `--ignore-content <patterns>`: Patterns to include files but skip their content (comma-separated; prefix with `!` to keep specific paths)
- `--ignore-content <patterns>`: Patterns for files whose content should be skipped (comma-separated; prefix with `!` to keep specific paths)

- `--no-gitignore`: Disable .gitignore file usage
- `--no-default-patterns`: Disable default patterns

Expand Down Expand Up @@ -607,6 +608,9 @@ repomix --compress
# Process specific files
repomix --include "src/**/*.ts" --ignore "**/*.test.ts"

# Ignore file contents for matching patterns
repomix --ignore-content "components/**,!components/slider/**"

# Remote repository with branch
repomix --remote https://github.com/user/repo/tree/main

Expand Down Expand Up @@ -959,6 +963,7 @@ Here's an explanation of the configuration options:
| `output.git.includeLogs` | Whether to include git logs in the output (includes commit history with dates, messages, and file paths) | `false` |
| `output.git.includeLogsCount` | Number of git log commits to include | `50` |
| `include` | Patterns of files to include (using [glob patterns](https://github.com/mrmlnc/fast-glob?tab=readme-ov-file#pattern-syntax)) | `[]` |
| `ignoreContent` | Patterns of files whose content should be ignored (using [glob patterns](https://github.com/mrmlnc/fast-glob?tab=readme-ov-file#pattern-syntax)); prefix with `!` to keep specific paths | `[]` |
| `ignore.useGitignore` | Whether to use patterns from the project's `.gitignore` file | `true` |
| `ignore.useDefaultPatterns` | Whether to use default ignore patterns | `true` |
| `ignore.customPatterns` | Additional patterns to ignore (using [glob patterns](https://github.com/mrmlnc/fast-glob?tab=readme-ov-file#pattern-syntax)) | `[]` |
Expand Down Expand Up @@ -1004,6 +1009,7 @@ Example configuration:
}
},
"include": ["**/*"],
"ignoreContent": ["components/**", "!components/slider/**"],
"ignore": {
"useGitignore": true,
"useDefaultPatterns": true,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
},
"bin": "./bin/repomix.cjs",
"scripts": {
"prepare": "npm run build",
"build": "rimraf lib && tsc -p tsconfig.build.json --sourceMap --declaration",
"build-bun": "bun run build",
"lint": "node --run lint-biome && node --run lint-oxlint && node --run lint-ts && node --run lint-secretlint",
Expand Down
3 changes: 3 additions & 0 deletions src/cli/actions/defaultAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,9 @@ export const buildCliConfig = (options: CliOptions): RepomixConfigCli => {
if (options.ignore) {
cliConfig.ignore = { customPatterns: splitPatterns(options.ignore) };
}
if (options.ignoreContent) {
cliConfig.ignoreContent = splitPatterns(options.ignoreContent);
}
// Only apply gitignore setting if explicitly set to false
if (options.gitignore === false) {
cliConfig.ignore = { ...cliConfig.ignore, useGitignore: options.gitignore };
Expand Down
4 changes: 4 additions & 0 deletions src/cli/cliRun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ export const run = async () => {
.optionsGroup('File Selection Options')
.option('--include <patterns>', 'list of include patterns (comma-separated)')
.option('-i, --ignore <patterns>', 'additional ignore patterns (comma-separated)')
.option(
'--ignore-content <patterns>',
'patterns to skip file content (comma-separated; prefix with ! to keep paths)',
)
.option('--no-gitignore', 'disable .gitignore file usage')
.option('--no-default-patterns', 'disable default patterns')
// Remote Repository Options
Expand Down
1 change: 1 addition & 0 deletions src/cli/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface CliOptions extends OptionValues {
// Filter Options
include?: string;
ignore?: string;
ignoreContent?: string;
gitignore?: boolean;
defaultPatterns?: boolean;
stdin?: boolean;
Expand Down
5 changes: 5 additions & 0 deletions src/config/configLoad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ export const mergeConfigs = (
...cliConfig.output,
},
include: [...(baseConfig.include || []), ...(fileConfig.include || []), ...(cliConfig.include || [])],
ignoreContent: [
...(baseConfig.ignoreContent || []),
...(fileConfig.ignoreContent || []),
...(cliConfig.ignoreContent || []),
],
ignore: {
...baseConfig.ignore,
...fileConfig.ignore,
Expand Down
2 changes: 2 additions & 0 deletions src/config/configSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const repomixConfigBaseSchema = z.object({
})
.optional(),
include: z.array(z.string()).optional(),
ignoreContent: z.array(z.string()).optional(),
ignore: z
.object({
useGitignore: z.boolean().optional(),
Expand Down Expand Up @@ -112,6 +113,7 @@ export const repomixConfigDefaultSchema = z.object({
})
.default({}),
include: z.array(z.string()).default([]),
ignoreContent: z.array(z.string()).default([]),
ignore: z
.object({
useGitignore: z.boolean().default(true),
Expand Down
19 changes: 19 additions & 0 deletions src/core/file/fileCollect.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { minimatch } from 'minimatch';
import pc from 'picocolors';
import type { RepomixConfigMerged } from '../../config/configSchema.js';
import { logger } from '../../shared/logger.js';
import { initTaskRunner } from '../../shared/processConcurrency.js';
import type { RepomixProgressCallback } from '../../shared/types.js';
import { normalizeGlobPattern } from './fileSearch.js';
import type { RawFile } from './fileTypes.js';
import type { FileCollectResult, FileCollectTask, SkippedFileInfo } from './workers/fileCollectWorker.js';

Expand All @@ -28,12 +30,29 @@ export const collectFiles = async (
workerPath: new URL('./workers/fileCollectWorker.js', import.meta.url).href,
runtime: 'worker_threads',
});

const shouldSkipContent = (filePath: string, patterns: string[]): boolean => {
let skip = false;
for (const pattern of patterns) {
const normalizedPattern = normalizeGlobPattern(pattern.startsWith('!') ? pattern.slice(1) : pattern);
if (pattern.startsWith('!')) {
if (minimatch(filePath, normalizedPattern, { dot: true })) {
skip = false;
}
} else if (minimatch(filePath, normalizedPattern, { dot: true })) {
skip = true;
}
}
return skip;
};
Comment on lines +34 to +47
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation of shouldSkipContent is dependent on the order of patterns in the configuration. For example, with ['!foo', 'foo'], a file named foo will be skipped, but with ['foo', '!foo'], it will not. This can be confusing and lead to unexpected behavior. A more robust approach is to give negated patterns (!) precedence regardless of their order.

  const shouldSkipContent = (filePath: string, patterns: string[]): boolean => {
    // Negated patterns ("!...") should always take precedence to ensure content is included.
    const isExplicitlyIncluded = patterns
      .filter((pattern) => pattern.startsWith('!'))
      .some((pattern) => minimatch(filePath, normalizeGlobPattern(pattern.slice(1)), { dot: true }));

    if (isExplicitlyIncluded) {
      return false; // Do not skip content.
    }

    // If not explicitly included, check if it matches any ignore pattern.
    const isIgnored = patterns
      .filter((pattern) => !pattern.startsWith('!'))
      .some((pattern) => minimatch(filePath, normalizeGlobPattern(pattern), { dot: true }));

    return isIgnored;
  };


const tasks = filePaths.map(
(filePath) =>
({
filePath,
rootDir,
maxFileSize: config.input.maxFileSize,
skipContent: shouldSkipContent(filePath, config.ignoreContent),
}) satisfies FileCollectTask,
);

Expand Down
7 changes: 4 additions & 3 deletions src/core/file/fileProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@ export const processFiles = async (
getFileManipulator,
},
): Promise<ProcessedFile[]> => {
const filesToProcess = rawFiles.filter((file) => file.content !== undefined);
const taskRunner = deps.initTaskRunner<FileProcessTask, ProcessedFile>({
numOfTasks: rawFiles.length,
numOfTasks: filesToProcess.length,
workerPath: new URL('./workers/fileProcessWorker.js', import.meta.url).href,
// High memory usage and leak risk
runtime: 'child_process',
});
const tasks = rawFiles.map(
const tasks = filesToProcess.map(
(rawFile, _index) =>
({
rawFile,
Expand All @@ -37,7 +38,7 @@ export const processFiles = async (

try {
const startTime = process.hrtime.bigint();
logger.trace(`Starting file processing for ${rawFiles.length} files using worker pool`);
logger.trace(`Starting file processing for ${filesToProcess.length} files using worker pool`);

let completedTasks = 0;
const totalTasks = tasks.length;
Expand Down
3 changes: 3 additions & 0 deletions src/core/file/fileProcessContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import { truncateBase64Content } from './truncateBase64.js';
*/
export const processContent = async (rawFile: RawFile, config: RepomixConfigMerged): Promise<string> => {
const processStartAt = process.hrtime.bigint();
if (rawFile.content === undefined) {
throw new Error(`No content to process for ${rawFile.path}`);
}
let processedContent = rawFile.content;
const manipulator = getFileManipulator(rawFile.path);

Expand Down
2 changes: 1 addition & 1 deletion src/core/file/fileTypes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export interface RawFile {
path: string;
content: string;
content?: string;
}

export interface ProcessedFile {
Expand Down
16 changes: 15 additions & 1 deletion src/core/file/workers/fileCollectWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface FileCollectTask {
filePath: string;
rootDir: string;
maxFileSize: number;
skipContent?: boolean;
}

export interface SkippedFileInfo {
Expand All @@ -23,8 +24,21 @@ export interface FileCollectResult {
skippedFile?: SkippedFileInfo;
}

export default async ({ filePath, rootDir, maxFileSize }: FileCollectTask): Promise<FileCollectResult> => {
export default async ({
filePath,
rootDir,
maxFileSize,
skipContent = false,
}: FileCollectTask): Promise<FileCollectResult> => {
const fullPath = path.resolve(rootDir, filePath);
if (skipContent) {
return {
rawFile: {
path: filePath,
},
};
}

const result = await readRawFile(fullPath, maxFileSize);

if (result.content !== null) {
Expand Down
3 changes: 2 additions & 1 deletion src/core/packager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,10 @@ export const pack = async (
);

// Process files (remove comments, etc.)
const rawFilesForProcessing = safeRawFiles.filter((file) => file.content !== undefined);
progressCallback('Processing files...');
const processedFiles = await withMemoryLogging('Process Files', () =>
deps.processFiles(safeRawFiles, config, progressCallback),
deps.processFiles(rawFilesForProcessing, config, progressCallback),
Comment on lines +114 to +117
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The filtering of safeRawFiles to exclude files without content is redundant here, as the same filtering is already performed inside the processFiles function. You can simplify the code by removing the rawFilesForProcessing variable and passing safeRawFiles directly to deps.processFiles.

  progressCallback('Processing files...');
  const processedFiles = await withMemoryLogging('Process Files', () =>
    deps.processFiles(safeRawFiles, config, progressCallback),
  );

);

progressCallback('Generating output...');
Expand Down
2 changes: 1 addition & 1 deletion src/core/security/securityCheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface SuspiciousFileResult {
}

export const runSecurityCheck = async (
rawFiles: RawFile[],
rawFiles: Array<RawFile & { content: string }>,
progressCallback: RepomixProgressCallback = () => {},
gitDiffResult?: GitDiffResult,
gitLogResult?: GitLogResult,
Expand Down
5 changes: 4 additions & 1 deletion src/core/security/validateFileSafety.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ export const validateFileSafety = async (

if (config.security.enableSecurityCheck) {
progressCallback('Running security check...');
const allResults = await deps.runSecurityCheck(rawFiles, progressCallback, gitDiffResult, gitLogResult);
const filesWithContent = rawFiles.filter(
(file): file is RawFile & { content: string } => file.content !== undefined,
);
const allResults = await deps.runSecurityCheck(filesWithContent, progressCallback, gitDiffResult, gitLogResult);

// Separate Git diff and Git log results from regular file results
suspiciousFilesResults = allResults.filter((result) => result.type === 'file');
Expand Down
16 changes: 16 additions & 0 deletions tests/cli/actions/defaultAction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,22 @@ describe('defaultAction', () => {
);
});

it('should handle ignore-content patterns', async () => {
const options: CliOptions = {
ignoreContent: 'components/**,!components/slider/**',
};

await runDefaultAction(['.'], process.cwd(), options);

expect(configLoader.mergeConfigs).toHaveBeenCalledWith(
process.cwd(),
expect.anything(),
expect.objectContaining({
ignoreContent: ['components/**', '!components/slider/**'],
}),
);
});

it('should handle custom output style', async () => {
const options: CliOptions = {
style: 'xml',
Expand Down
2 changes: 2 additions & 0 deletions tests/config/configSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ describe('configSchema', () => {
useDefaultPatterns: true,
customPatterns: [],
},
ignoreContent: [],
security: {
enableSecurityCheck: true,
},
Expand Down Expand Up @@ -226,6 +227,7 @@ describe('configSchema', () => {
useDefaultPatterns: true,
customPatterns: ['*.log'],
},
ignoreContent: [],
security: {
enableSecurityCheck: true,
},
Expand Down
63 changes: 63 additions & 0 deletions tests/core/file/fileCollect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,69 @@ describe('fileCollect', () => {
});
});

it('should skip file content when matching ignoreContent patterns', async () => {
const mockFilePaths = ['docs/readme.md', 'src/index.ts'];
const mockRootDir = '/root';
const mockConfig = createMockConfig({ ignoreContent: ['docs/**'] });

vi.mocked(isBinary).mockReturnValue(false);
vi.mocked(fs.readFile).mockResolvedValue(Buffer.from('file content'));
vi.mocked(jschardet.detect).mockReturnValue({ encoding: 'utf-8', confidence: 0.99 });
vi.mocked(iconv.decode).mockReturnValue('decoded content');

const result = await collectFiles(mockFilePaths, mockRootDir, mockConfig, () => {}, {
initTaskRunner: mockInitTaskRunner,
});

expect(result).toEqual({
rawFiles: [{ path: 'docs/readme.md' }, { path: 'src/index.ts', content: 'decoded content' }],
skippedFiles: [],
});
expect(fs.readFile).toHaveBeenCalledTimes(1);
});

it('should allow negated patterns to override ignoreContent', async () => {
const mockFilePaths = ['components/button.ts', 'components/slider/index.ts'];
const mockRootDir = '/root';
const mockConfig = createMockConfig({ ignoreContent: ['components/**', '!components/slider/**'] });

vi.mocked(isBinary).mockReturnValue(false);
vi.mocked(fs.readFile).mockResolvedValue(Buffer.from('file content'));
vi.mocked(jschardet.detect).mockReturnValue({ encoding: 'utf-8', confidence: 0.99 });
vi.mocked(iconv.decode).mockReturnValue('decoded content');

const result = await collectFiles(mockFilePaths, mockRootDir, mockConfig, () => {}, {
initTaskRunner: mockInitTaskRunner,
});

expect(result).toEqual({
rawFiles: [{ path: 'components/button.ts' }, { path: 'components/slider/index.ts', content: 'decoded content' }],
skippedFiles: [],
});
expect(fs.readFile).toHaveBeenCalledTimes(1);
});

it('should match dotfiles when using ignoreContent patterns', async () => {
const mockFilePaths = ['docs/.env', 'docs/readme.md'];
const mockRootDir = '/root';
const mockConfig = createMockConfig({ ignoreContent: ['docs/**'] });

vi.mocked(isBinary).mockReturnValue(false);
vi.mocked(fs.readFile).mockResolvedValue(Buffer.from('file content'));
vi.mocked(jschardet.detect).mockReturnValue({ encoding: 'utf-8', confidence: 0.99 });
vi.mocked(iconv.decode).mockReturnValue('decoded content');

const result = await collectFiles(mockFilePaths, mockRootDir, mockConfig, () => {}, {
initTaskRunner: mockInitTaskRunner,
});

expect(result).toEqual({
rawFiles: [{ path: 'docs/.env' }, { path: 'docs/readme.md' }],
skippedFiles: [],
});
expect(fs.readFile).not.toHaveBeenCalled();
});

it('should skip binary files', async () => {
const mockFilePaths = ['binary.bin', 'text.txt'];
const mockRootDir = '/root';
Expand Down
2 changes: 1 addition & 1 deletion tests/core/security/securityCheck.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ vi.mock('../../../src/shared/processConcurrency', () => ({
})),
}));

const mockFiles: RawFile[] = [
const mockFiles: Array<RawFile & { content: string }> = [
{
path: 'test1.js',
// secretlint-disable
Expand Down
1 change: 1 addition & 0 deletions tests/testing/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const createMockConfig = (config: DeepPartial<RepomixConfigMerged> = {}):
...config.ignore,
customPatterns: [...(defaultConfig.ignore.customPatterns || []), ...(config.ignore?.customPatterns || [])],
},
ignoreContent: [...(defaultConfig.ignoreContent || []), ...(config.ignoreContent || [])],
include: [...(defaultConfig.include || []), ...(config.include || [])],
security: {
...defaultConfig.security,
Expand Down
4 changes: 4 additions & 0 deletions website/client/src/de/guide/command-line-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
## Dateiauswahloptionen
- `--include <patterns>`: Liste der Einschlussmuster (kommagetrennt)
- `-i, --ignore <patterns>`: Zusätzliche Ignoriermuster (kommagetrennt)
- `--ignore-content <patterns>`: Skip file content for matched patterns (comma-separated; prefix with `!` to keep specific paths)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The description for --ignore-content is in English. It should be translated into German for consistency with the rest of the document.

Suggested change
- `--ignore-content <patterns>`: Skip file content for matched patterns (comma-separated; prefix with `!` to keep specific paths)
- `--ignore-content <patterns>`: Muster zum Überspringen von Dateiinhalten für übereinstimmende Muster (kommagetrennt; mit `!` voranstellen, um bestimmte Pfade beizubehalten)

- `--no-gitignore`: .gitignore-Datei-Nutzung deaktivieren
- `--no-default-patterns`: Standardmuster deaktivieren

Expand Down Expand Up @@ -75,6 +76,9 @@ repomix --compress
# Spezifische Dateien verarbeiten
repomix --include "src/**/*.ts" --ignore "**/*.test.ts"

# Ignore file contents for matching patterns
repomix --ignore-content "components/**,!components/slider/**"

# Remote-Repository mit Branch
repomix --remote https://github.com/user/repo/tree/main

Expand Down
Loading