From 9ee0d1631635a3076c56018f06cf58f5d9e26bcc Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Fri, 30 Oct 2026 01:47:29 -0500 Subject: [PATCH] fix(gitignore): rules leave out the files git does; update ignore to 7.0.14 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 --- CHANGELOG.md | 1 + __tests__/gitignore-pattern-parity.test.ts | 68 ++++++++++++++++++++++ package-lock.json | 8 +-- package.json | 2 +- 4 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 __tests__/gitignore-pattern-parity.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ee561ad18..a0dfc9e2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -366,6 +366,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - `codegraph sync` and the file watcher now relink an import that reaches a module through a barrel file passing it on under another name, like `import { MatxLoading } from './components'` when `components/index.js` says `export { default as MatxLoading } from './MatxLoading'`, once an edit gives that module what is imported. Before, the import and every call through it stayed unlinked, or stayed on something else in the project with the same name, until the importing file itself changed or the project was indexed again. - A folder named `build` inside `src/` is now indexed for its TypeScript, JSX, Vue, Svelte, Astro, Go, C# and Rust files, so a feature or package kept there, like the `src/build/` pages of luci-go's milo, Go's own `src/go/build` package or the `src/build/` module of dioxus's CLI, shows up in `codegraph_explore`, and the routes that lazily load its pages link to them. Before, every folder named `build` was skipped as build output. The JavaScript, declaration and other files such a folder holds are still left out, since a build may have written them, and so is every `build` folder outside `src/`. - A folder git ignores at the top of your project no longer takes every folder of the same name further down with it: a multi-module Maven project whose root `src/` holds only a gitignored jib agent indexed no Java at all, and a root `lib/` or `static/` that a local build writes hid a package's own `lib/` or an app's `static/` from the index, `codegraph sync` and the file watcher. A gitignored folder whose name reads as a pattern, like `[draft]` or `scratch[1`, now stands for just that folder instead of matching others or making the file watcher fail, and the `includeIgnored` entry `codegraph init` suggests for a skipped nested repository now covers only that repository. Re-index affected projects after upgrading. Thanks @Victor-Rodriguez-Garcia for the report and the fix. (#2849) +- `.gitignore` rules now leave the same files out as git does in a few more cases: a backslash escape like `\d.ts`, a character class like `[[:digit:]]` or one that starts with `]` or `!`, and a run of three asterisks like `logs/***/debug.ts`. A line holding only `!` no longer brings back everything the `.gitignore` leaves out, `node_modules` included. Before, such rules could keep files git ignores in the index and leave out files git keeps, in git repositories and in projects without git alike. The `include`, `exclude` and `includeIgnored` patterns in `codegraph.json` follow the same rules. Re-index affected projects after upgrading. Thanks @johnhldiver-beep. - On Windows, a Swift call written after `try!`, like `try! Message2(data: data)` or `try! Settings.Defaults.load()`, now links to what it calls, as it already did on macOS and Linux. Before, the `!` was read as a logical not, so the call linked to nothing, and a call made through a type lost the type it was made on. Re-index Swift projects on Windows after upgrading. - In JavaScript and TypeScript, a barrel's `export { A, B as C } from './x'` now links each name it passes on to what `./x` exports, following barrels that pass it on in turn. Before, each name was looked up by itself and could link to a same-named symbol elsewhere in the project, such as another folder's `Provider`, and a name passed on from a package, like `export { debounce } from 'lodash'`, could link to a project function of that name. Re-index JavaScript and TypeScript projects after upgrading. - In Swift, a call written after `!` or another prefix operator, or chained after `try` or `await`, now links to what it calls. `if !isReady()` links to `isReady`, `!store.contains(id)` and `try! JSONDecoder().decode(…)` link to the method of the type they're called on rather than to whichever `contains` or `decode` came first, and `!Settings.Defaults.isOn()` lands on the type it names. Re-index Swift projects after upgrading. diff --git a/__tests__/gitignore-pattern-parity.test.ts b/__tests__/gitignore-pattern-parity.test.ts new file mode 100644 index 000000000..9d7e3932a --- /dev/null +++ b/__tests__/gitignore-pattern-parity.test.ts @@ -0,0 +1,68 @@ +/** + * 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']); + }); +}); diff --git a/package-lock.json b/package-lock.json index acc69b680..d414c1fbd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "commander": "^14.0.2", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", - "ignore": "^7.0.5", + "ignore": "^7.0.14", "jsonc-parser": "^3.3.1", "picomatch": "^4.0.3", "sisteransi": "^1.0.5", @@ -2160,9 +2160,9 @@ } }, "node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz", + "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==", "license": "MIT", "engines": { "node": ">= 4" diff --git a/package.json b/package.json index 336ef4ca8..40f33c798 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "commander": "^14.0.2", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", - "ignore": "^7.0.5", + "ignore": "^7.0.14", "jsonc-parser": "^3.3.1", "picomatch": "^4.0.3", "sisteransi": "^1.0.5",