Files
codegraph/__tests__/gitignore-pattern-parity.test.ts
T
Colby MchenryandClaude Opus 5.5 19b74d10f3 fix(deps): read .gitignore rules the way git does, with ignore 7.0.14 (#3050)
codegraph applies .gitignore rules itself, through the ignore package:
on the filesystem walk for every .gitignore, and in git repositories on
top of git ls-files for its built-in defaults merged with the root
.gitignore and git's exclude files. codegraph.json's include, exclude
and includeIgnored patterns go through it too. ignore 7.0.5 read several
forms differently from git: a backslash escape (\d was a regex digit),
POSIX classes and a ] or ! first in a bracket, and a run of three
asterisks. A lone ! line matched every path, so it re-included
everything the rules and the built-in defaults leave out, node_modules
included. 2,031 of 200,000 random patterns also threw at match time,
which readGitignorePatterns has to guard against (issue 682).

ignore 7.0.14, with the range raised to ^7.0.14, reads all of these as
git does, and none of the 200,000 patterns throws. Its code diff from
7.0.5 was read in full: pattern translation only, with no I/O, no
dependencies and no install scripts. The lockfile entry matches the npm
registry's own metadata on registry.npmjs.org. The package has no
provenance, like every release its maintainer has published.

Validation:
- New __tests__/gitignore-pattern-parity.test.ts checks scanDirectory
  against git's own answer for these patterns, on the filesystem walk
  and in a git repository: its 4 tests fail with 7.0.5 and pass with
  7.0.14.
- scanDirectory on react, astro and django, in git mode and in walk
  mode on a copy without .git, returns the same 4,631, 4,946 and 3,027
  files with either version.
- On a fixture of the changed forms, 7.0.14 leaves out exactly what git
  leaves out in both modes, node_modules aside, which codegraph's
  defaults always exclude. With a lone ! line, 7.0.5 kept 16 of the 18
  files on the walk, node_modules among them, where git keeps 6. Without
  that line it differed from git on 8 files on the walk and on 2 in git
  mode.
- The gitignore and scope test files (extraction,
  git-ignored-dirs-scope, the include, exclude and includeIgnored
  configs, src-build-folders and others): 853 passed.

Raised by PR 2885 (@johnhldiver-beep).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 07:49:46 +00:00

69 lines
2.9 KiB
TypeScript

/**
* codegraph applies .gitignore rules itself in two places: on the filesystem
* walk (a project without git), for every .gitignore it meets, and in a git
* project on top of `git ls-files`, for its built-in defaults merged with the
* root .gitignore. Both go through the `ignore` package, which before 7.0.14
* read several pattern forms differently from git:
* - a backslash escape: `\d` was a regex digit, not a literal `d`;
* - a POSIX class such as `[[:digit:]]`, and a `]` or `!` first in a bracket;
* - a run of three asterisks, which git reads as a globstar;
* - a lone `!` line, which matched every path, so it re-included everything
* the .gitignore and codegraph's own defaults (node_modules) leave out.
* Each fixture file is kept or dropped the way git decides, except
* node_modules, which codegraph's defaults always leave out.
*/
import { afterEach, describe, expect, it } from 'vitest';
import { execFileSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { scanDirectory } from '../src/extraction';
const roots: string[] = [];
afterEach(() => {
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
});
function project(mode: 'walk' | 'git', gitignore: string, files: string[]): string {
const root = fs.mkdtempSync(path.join(os.tmpdir(), `cg-gitignore-parity-${mode}-`));
roots.push(root);
for (const file of files) {
fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
fs.writeFileSync(path.join(root, file), 'export const value = 1;\n');
}
fs.writeFileSync(path.join(root, '.gitignore'), gitignore);
if (mode === 'git') execFileSync('git', ['init', '-q'], { cwd: root, stdio: 'ignore' });
return root;
}
function scanned(root: string): string[] {
return scanDirectory(root).map((f) => f.replace(/\\/g, '/')).sort();
}
describe.each(['walk', 'git'] as const)('.gitignore patterns follow git (%s)', (mode) => {
it('reads escapes, POSIX classes, brackets, *** and trailing spaces as git does', () => {
const root = project(
mode,
[
'\\d.ts', // a literal "d"
'[[:digit:]]x.ts', // a POSIX class
'[!a]neg.ts', // a negated bracket
'[]]bracket.ts', // "]" first in a bracket is a member
'stars/***/z.ts', // three asterisks are a globstar
'trail.ts ', // trailing spaces are trimmed
].join('\n') + '\n',
[
'1.ts', 'd.ts', '1x.ts', 'ax.ts', 'aneg.ts', 'bneg.ts', ']bracket.ts',
'stars/z.ts', 'stars/b/z.ts', 'stars/b/c/z.ts', 'trail.ts', 'other.ts',
],
);
expect(scanned(root)).toEqual(['1.ts', 'aneg.ts', 'ax.ts', 'other.ts']);
});
it('treats a lone "!" line as no pattern, so ignored files and node_modules stay out', () => {
const root = project(mode, 'skipped.ts\n!\n', ['kept.ts', 'skipped.ts', 'node_modules/pkg/index.ts']);
expect(scanned(root)).toEqual(['kept.ts']);
});
});