diff --git a/src/core/file/fileSearch.ts b/src/core/file/fileSearch.ts index 492af837e..7073e5337 100644 --- a/src/core/file/fileSearch.ts +++ b/src/core/file/fileSearch.ts @@ -330,6 +330,9 @@ export const getIgnoreFilePatterns = async (config: RepomixConfigMerged): Promis // // .gitignore files are handled by globby's gitignore option (not ignoreFiles) // to properly respect parent directory .gitignore files, matching Git's behavior. + // + // These ignore file patterns are always applied regardless of whether customPatterns + // is defined in the config. Both sources are merged by globby at the file search level. if (config.ignore.useDotIgnore) { ignoreFilePatterns.push('**/.ignore'); diff --git a/tests/config/configLoad.test.ts b/tests/config/configLoad.test.ts index aa221e7b4..7e92e86ae 100644 --- a/tests/config/configLoad.test.ts +++ b/tests/config/configLoad.test.ts @@ -351,5 +351,54 @@ describe('configLoad', () => { const merged = mergeConfigs(process.cwd(), {}, { skillGenerate: 'from-cli' }); expect(merged.skillGenerate).toBe('from-cli'); }); + + // Regression tests for #959: .ignore file should work when customPatterns is defined + test('should preserve useDotIgnore default when only customPatterns is defined in fileConfig', () => { + const fileConfig: RepomixConfigFile = { + ignore: { + customPatterns: ['bin/'], + }, + }; + const cliConfig: RepomixConfigCli = {}; + const merged = mergeConfigs(process.cwd(), fileConfig, cliConfig); + + expect(merged.ignore.useDotIgnore).toBe(true); + expect(merged.ignore.useGitignore).toBe(true); + expect(merged.ignore.useDefaultPatterns).toBe(true); + expect(merged.ignore.customPatterns).toContain('bin/'); + }); + + test('should preserve all ignore boolean flags when customPatterns is defined in both sources', () => { + const fileConfig: RepomixConfigFile = { + ignore: { + customPatterns: ['from-file/'], + }, + }; + const cliConfig: RepomixConfigCli = { + ignore: { + customPatterns: ['from-cli/'], + }, + }; + const merged = mergeConfigs(process.cwd(), fileConfig, cliConfig); + + expect(merged.ignore.useDotIgnore).toBe(true); + expect(merged.ignore.useGitignore).toBe(true); + expect(merged.ignore.useDefaultPatterns).toBe(true); + expect(merged.ignore.customPatterns).toEqual(['from-file/', 'from-cli/']); + }); + + test('should allow explicitly disabling useDotIgnore alongside customPatterns', () => { + const fileConfig: RepomixConfigFile = { + ignore: { + useDotIgnore: false, + customPatterns: ['bin/'], + }, + }; + const cliConfig: RepomixConfigCli = {}; + const merged = mergeConfigs(process.cwd(), fileConfig, cliConfig); + + expect(merged.ignore.useDotIgnore).toBe(false); + expect(merged.ignore.customPatterns).toContain('bin/'); + }); }); }); diff --git a/tests/core/file/fileSearch.ignore.integration.test.ts b/tests/core/file/fileSearch.ignore.integration.test.ts new file mode 100644 index 000000000..1f6280229 --- /dev/null +++ b/tests/core/file/fileSearch.ignore.integration.test.ts @@ -0,0 +1,157 @@ +import * as fs from 'node:fs/promises'; +import os from 'node:os'; +import * as path from 'node:path'; +import { afterEach, beforeEach, describe, expect, test } from 'vitest'; +import { mergeConfigs } from '../../../src/config/configLoad.js'; +import type { RepomixConfigFile } from '../../../src/config/configSchema.js'; +import { searchFiles } from '../../../src/core/file/fileSearch.js'; +import { createMockConfig } from '../../testing/testUtils.js'; + +/** + * Integration tests for .ignore file behavior when customPatterns is defined. + * Regression tests for issue #959. + * + * These tests use real file system operations to verify that all ignore sources + * (.ignore, .repomixignore, customPatterns) are properly merged and applied + * together, regardless of which sources are configured. + */ +describe('fileSearch - .ignore integration with customPatterns (#959)', () => { + let tempDir: string; + + beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'repomix-959-')); + }); + + afterEach(async () => { + await fs.rm(tempDir, { recursive: true, force: true }); + }); + + test('should respect .ignore file when customPatterns is defined in config', async () => { + // Setup: exact scenario from issue #959 + await fs.mkdir(path.join(tempDir, 'bin'), { recursive: true }); + await fs.mkdir(path.join(tempDir, 'spec', 'data'), { recursive: true }); + await fs.mkdir(path.join(tempDir, 'src'), { recursive: true }); + await fs.writeFile(path.join(tempDir, 'bin', 'test.sh'), 'bin content'); + await fs.writeFile(path.join(tempDir, 'spec', 'data', 'test.txt'), 'test content'); + await fs.writeFile(path.join(tempDir, 'src', 'main.js'), 'main content'); + await fs.writeFile(path.join(tempDir, '.ignore'), 'spec/data/\n'); + + // Simulate: config file has { ignore: { customPatterns: ["bin/"] } } + const fileConfig: RepomixConfigFile = { + ignore: { customPatterns: ['bin/'] }, + }; + const config = mergeConfigs(tempDir, fileConfig, {}); + const testConfig = { ...config, ignore: { ...config.ignore, useGitignore: false, useDefaultPatterns: false } }; + + const result = await searchFiles(tempDir, testConfig); + + // Both bin/ (from customPatterns) and spec/data/ (from .ignore) should be excluded + expect(result.filePaths).not.toContain('bin/test.sh'); + expect(result.filePaths).not.toContain('spec/data/test.txt'); + expect(result.filePaths).toContain('src/main.js'); + }); + + test('should respect .ignore file when customPatterns is empty', async () => { + await fs.mkdir(path.join(tempDir, 'spec', 'data'), { recursive: true }); + await fs.mkdir(path.join(tempDir, 'src'), { recursive: true }); + await fs.writeFile(path.join(tempDir, 'spec', 'data', 'test.txt'), 'test content'); + await fs.writeFile(path.join(tempDir, 'src', 'main.js'), 'main content'); + await fs.writeFile(path.join(tempDir, '.ignore'), 'spec/data/\n'); + + const config = createMockConfig({ + cwd: tempDir, + ignore: { + useGitignore: false, + useDotIgnore: true, + useDefaultPatterns: false, + customPatterns: [], + }, + }); + + const result = await searchFiles(tempDir, config); + + expect(result.filePaths).not.toContain('spec/data/test.txt'); + expect(result.filePaths).toContain('src/main.js'); + }); + + test('should not use .ignore when useDotIgnore is false', async () => { + await fs.mkdir(path.join(tempDir, 'spec', 'data'), { recursive: true }); + await fs.mkdir(path.join(tempDir, 'src'), { recursive: true }); + await fs.writeFile(path.join(tempDir, 'spec', 'data', 'test.txt'), 'test content'); + await fs.writeFile(path.join(tempDir, 'src', 'main.js'), 'main content'); + await fs.writeFile(path.join(tempDir, '.ignore'), 'spec/data/\n'); + + const config = createMockConfig({ + cwd: tempDir, + ignore: { + useGitignore: false, + useDotIgnore: false, + useDefaultPatterns: false, + customPatterns: [], + }, + }); + + const result = await searchFiles(tempDir, config); + + // .ignore should NOT be respected when useDotIgnore is false + expect(result.filePaths).toContain('spec/data/test.txt'); + expect(result.filePaths).toContain('src/main.js'); + }); + + test('should merge all ignore sources together', async () => { + await fs.mkdir(path.join(tempDir, 'dist'), { recursive: true }); + await fs.mkdir(path.join(tempDir, 'tmp'), { recursive: true }); + await fs.mkdir(path.join(tempDir, 'vendor'), { recursive: true }); + await fs.mkdir(path.join(tempDir, 'src'), { recursive: true }); + await fs.writeFile(path.join(tempDir, 'dist', 'bundle.js'), 'dist'); + await fs.writeFile(path.join(tempDir, 'tmp', 'cache.dat'), 'tmp'); + await fs.writeFile(path.join(tempDir, 'vendor', 'lib.js'), 'vendor'); + await fs.writeFile(path.join(tempDir, 'src', 'index.js'), 'src'); + await fs.writeFile(path.join(tempDir, '.ignore'), 'dist/\n'); + await fs.writeFile(path.join(tempDir, '.repomixignore'), 'tmp/\n'); + + const config = createMockConfig({ + cwd: tempDir, + ignore: { + useGitignore: false, + useDotIgnore: true, + useDefaultPatterns: false, + customPatterns: ['vendor/'], + }, + }); + + const result = await searchFiles(tempDir, config); + + // All three ignore sources should be merged + expect(result.filePaths).not.toContain('dist/bundle.js'); + expect(result.filePaths).not.toContain('tmp/cache.dat'); + expect(result.filePaths).not.toContain('vendor/lib.js'); + expect(result.filePaths).toContain('src/index.js'); + }); + + test('should respect .ignore in subdirectory when customPatterns is defined', async () => { + await fs.mkdir(path.join(tempDir, 'src', 'generated'), { recursive: true }); + await fs.mkdir(path.join(tempDir, 'bin'), { recursive: true }); + await fs.writeFile(path.join(tempDir, 'src', 'app.js'), 'normal'); + await fs.writeFile(path.join(tempDir, 'src', 'generated', 'output.js'), 'generated'); + await fs.writeFile(path.join(tempDir, 'bin', 'run.sh'), 'bin'); + // Subdirectory .ignore file + await fs.writeFile(path.join(tempDir, 'src', '.ignore'), 'generated/\n'); + + const config = createMockConfig({ + cwd: tempDir, + ignore: { + useGitignore: false, + useDotIgnore: true, + useDefaultPatterns: false, + customPatterns: ['bin/'], + }, + }); + + const result = await searchFiles(tempDir, config); + + expect(result.filePaths).not.toContain('bin/run.sh'); + expect(result.filePaths).not.toContain('src/generated/output.js'); + expect(result.filePaths).toContain('src/app.js'); + }); +});