Files
yamadashy--repomix/tests/core/file/fileSearch.gitignoreSpec.test.ts
wehub-resource-sync 719032b19f
Update Schema / Update configuration json schema (push) Has been cancelled
Memory Benchmark / Memory Test (Full Analysis) (push) Has been cancelled
CI Quality / Lint GitHub Actions (actionlint) (push) Has been cancelled
CI Quality / Lint GitHub Actions (zizmor) (push) Has been cancelled
CI Quality / Check typos (push) Has been cancelled
CI / Test coverage (push) Has been cancelled
CI / Test (22.x, windows-latest) (push) Has been cancelled
CI / Test (24.x, macos-latest) (push) Has been cancelled
CI / Test (24.x, ubuntu-latest) (push) Has been cancelled
CI / Test (24.x, windows-latest) (push) Has been cancelled
CI / Test (26.x, macos-latest) (push) Has been cancelled
CI / Test (26.x, ubuntu-latest) (push) Has been cancelled
CI / Test (26.x, windows-latest) (push) Has been cancelled
CI / Test with Bun (latest, macos-latest) (push) Has been cancelled
CI / Test with Bun (latest, ubuntu-latest) (push) Has been cancelled
CI / Test with Bun (latest, windows-latest) (push) Has been cancelled
CI / Build and run (22.x, macos-latest) (push) Has been cancelled
CI / Build and run (22.x, ubuntu-latest) (push) Has been cancelled
CI / Build and run (22.x, windows-latest) (push) Has been cancelled
autofix.ci / autofix (push) Has been cancelled
CI Browser Extension / Lint Browser Extension (push) Has been cancelled
CI Browser Extension / Test Browser Extension (push) Has been cancelled
Memory Benchmark / Memory Test (push) Has been cancelled
CI Website / Lint Website Client (push) Has been cancelled
CI Website / Lint Website Server (push) Has been cancelled
CI Website / Test Website Server (push) Has been cancelled
CI Website / Bundle Website Server (push) Has been cancelled
CI / Lint Biome (push) Has been cancelled
CI / Lint oxlint (push) Has been cancelled
CI / Lint TypeScript (push) Has been cancelled
CI / Lint Secretlint (push) Has been cancelled
CI / Test (22.x, macos-latest) (push) Has been cancelled
CI / Test (22.x, ubuntu-latest) (push) Has been cancelled
CI / Build and run (24.x, macos-latest) (push) Has been cancelled
CI / Build and run (24.x, ubuntu-latest) (push) Has been cancelled
CI / Build and run (24.x, windows-latest) (push) Has been cancelled
CI / Build and run (26.x, macos-latest) (push) Has been cancelled
CI / Build and run (26.x, ubuntu-latest) (push) Has been cancelled
CI / Build and run (26.x, windows-latest) (push) Has been cancelled
CI / Build and run with Bun (latest, macos-latest) (push) Has been cancelled
CI / Build and run with Bun (latest, ubuntu-latest) (push) Has been cancelled
CI / Build and run with Bun (latest, windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Docker / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Docker / build (linux/arm/v7, ubuntu-24.04-arm) (push) Has been cancelled
Docker / build (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Docker / merge (push) Has been cancelled
Pack repository with Repomix / pack-repo (push) Has been cancelled
Performance Benchmark History / Benchmark (macos-latest) (push) Has been cancelled
Performance Benchmark History / Benchmark (ubuntu-latest) (push) Has been cancelled
Performance Benchmark History / Benchmark (windows-latest) (push) Has been cancelled
Performance Benchmark History / Store Results (push) Has been cancelled
Test Repomix Action / Test Node.js 22 (push) Has been cancelled
Test Repomix Action / Test Node.js 24 (push) Has been cancelled
Test Repomix Action / Test Node.js 26 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:37 +08:00

200 lines
7.7 KiB
TypeScript

import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { searchFiles } from '../../../src/core/file/fileSearch.js';
import { createMockConfig, writeFixture } from '../../testing/testUtils.js';
// Behavior-level regression tests for gitignore handling.
//
// These tests run against a real temp directory (no module mocks) so they
// assert the contract `searchFiles` exposes to users — which files end up in
// the pack, given a particular tree of .gitignore files. Any optimization that
// swaps out the underlying ignore-file engine must keep these passing.
//
// The auto-perf-tuning agent has historically proposed prescan replacements
// that silently broke gitignore semantics (negation, slash-less recursion,
// parent .gitignore, dot-dir traversal, monorepo packages/). These cases all
// originate from that incident — they are the spec, not coincidental coverage.
//
// Note on fixture choices: every filename and extension below intentionally
// avoids the project's `defaultIgnoreList` so that any filtering observed must
// originate from the user-provided .gitignore, not from baseline defaults.
// Picking patterns like `*.log` or `dist/` would produce false positives
// because those are filtered regardless of gitignore behavior.
describe('fileSearch gitignore spec', () => {
let tmpDir: string;
beforeEach(async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'repomix-gitignore-spec-'));
});
afterEach(async () => {
await fs.rm(tmpDir, { recursive: true, force: true });
});
// Mirrors git's own negation semantics: a file may be re-included if its
// ancestor directories are not themselves excluded. (Git docs:
// "It is not possible to re-include a file if a parent directory of that
// file is excluded.") A prescan implementation that drops `!` lines wholesale
// — as a previous auto-perf-tuning iteration did — would fail this case.
it('honors negation patterns: `!keep.draft` re-includes a file that a broader rule would otherwise ignore', async () => {
await writeFixture(tmpDir, {
'.gitignore': '*.draft\n!keep.draft\n',
'src/index.ts': 'export {};\n',
'noisy.draft': 'noisy\n',
'keep.draft': 'this one matters\n',
});
const { filePaths } = await searchFiles(tmpDir, createMockConfig());
expect(filePaths).toContain('src/index.ts');
expect(filePaths).toContain('keep.draft');
expect(filePaths).not.toContain('noisy.draft');
});
it.each([
'browser/.gitignore',
'**/.gitignore',
// A trailing slash must behave identically: the pattern is still deferred so
// globby loads the rules, and the post-filter (sharing the same normalization)
// still removes the file rather than leaking it.
'**/.gitignore/',
])('still applies nested .gitignore rules when the nested .gitignore file itself is ignored by `%s`', async (ignorePattern) => {
await writeFixture(tmpDir, {
'browser/.gitignore': '*.draft\n',
'browser/src/index.ts': 'export {};\n',
'browser/noisy.draft': 'noisy\n',
});
const { filePaths } = await searchFiles(
tmpDir,
createMockConfig({
include: ['browser/**'],
ignore: {
useDefaultPatterns: false,
customPatterns: [ignorePattern],
},
}),
);
expect(filePaths).toContain('browser/src/index.ts');
expect(filePaths).not.toContain('browser/.gitignore');
expect(filePaths).not.toContain('browser/noisy.draft');
});
it('filters root and nested .gitignore files matched by `**/.gitignore` while still applying their rules', async () => {
await writeFixture(tmpDir, {
'.gitignore': 'root-noisy.draft\n',
'keep.ts': 'export {};\n',
'root-noisy.draft': 'noisy\n',
'src/.gitignore': 'generated.ts\n',
'src/keep.ts': 'export {};\n',
'src/generated.ts': 'generated\n',
});
await fs.mkdir(path.join(tmpDir, 'empty'), { recursive: true });
const { filePaths, emptyDirPaths } = await searchFiles(
tmpDir,
createMockConfig({
output: { includeEmptyDirectories: true },
ignore: {
useDefaultPatterns: false,
customPatterns: ['**/.gitignore'],
},
}),
);
expect(filePaths).toContain('keep.ts');
expect(filePaths).toContain('src/keep.ts');
expect(filePaths).not.toContain('.gitignore');
expect(filePaths).not.toContain('src/.gitignore');
expect(filePaths).not.toContain('root-noisy.draft');
expect(filePaths).not.toContain('src/generated.ts');
expect(emptyDirPaths).toContain('empty');
});
it('excludes the contents of a directory literally named `.gitignore` when ignored by `**/.gitignore`', async () => {
// Pathological but valid: `.gitignore` as a directory name. The old globby
// behavior (where `**/.gitignore` normalized to `**/.gitignore/**`) excluded
// its contents, so the post-filter must drop descendants too, not just a
// file named `.gitignore`.
await writeFixture(tmpDir, {
'proj/.gitignore/inside.txt': 'x\n',
'proj/keep.txt': 'x\n',
});
const { filePaths } = await searchFiles(
tmpDir,
createMockConfig({
include: ['proj/**'],
ignore: {
useDefaultPatterns: false,
customPatterns: ['**/.gitignore'],
},
}),
);
expect(filePaths).toContain('proj/keep.txt');
expect(filePaths).not.toContain('proj/.gitignore/inside.txt');
});
it('applies slash-less patterns recursively to all subdirectories', async () => {
await writeFixture(tmpDir, {
'.gitignore': '*.draft\nsecret.data\n',
'a.draft': 'top\n',
'secret.data': 'top\n',
'keep.ts': 'export {};\n',
'sub/nested/b.draft': 'nested\n',
'sub/secret.data': 'nested\n',
'sub/keep.ts': 'export {};\n',
});
const { filePaths } = await searchFiles(tmpDir, createMockConfig());
// Slash-less gitignore entries must match at every depth, matching git's
// own behavior. A prescan that flattens entries to `${relPath}/${pattern}`
// would break this.
expect(filePaths).not.toContain('a.draft');
expect(filePaths).not.toContain('secret.data');
expect(filePaths).not.toContain('sub/nested/b.draft');
expect(filePaths).not.toContain('sub/secret.data');
expect(filePaths).toContain('keep.ts');
expect(filePaths).toContain('sub/keep.ts');
});
// Note: parent-directory .gitignore handling when `searchFiles` is invoked
// against a subdirectory is intentionally NOT covered here. globby reads
// .gitignore files within `cwd` only, so this is a pre-existing gap on main
// rather than a regression target. Capture it as a real spec the day the
// codebase commits to that behavior.
it('reads .gitignore inside monorepo `packages/*` (the classic Lerna/pnpm layout)', async () => {
await writeFixture(tmpDir, {
'package.json': '{"name":"root","private":true}\n',
'packages/ui/.gitignore': 'generated/\n',
'packages/ui/src.ts': 'export {};\n',
'packages/ui/generated/bundle.data': '// generated\n',
});
const { filePaths } = await searchFiles(tmpDir, createMockConfig());
expect(filePaths).toContain('packages/ui/src.ts');
expect(filePaths).not.toContain('packages/ui/generated/bundle.data');
});
it('reads .gitignore inside dot-prefixed directories like `.config/`', async () => {
await writeFixture(tmpDir, {
'.config/.gitignore': 'secret.data\n',
'.config/public.data': '{"ok":true}\n',
'.config/secret.data': '{"shh":true}\n',
});
const { filePaths } = await searchFiles(tmpDir, createMockConfig());
expect(filePaths).toContain('.config/public.data');
expect(filePaths).not.toContain('.config/secret.data');
});
});