Skip to content

fix: Correct gitignore precedence in untracked walk and memoize matcher chains - #13221

Merged
anthonyshew merged 1 commit into
mainfrom
shew/untracked-walk-perf
Jul 3, 2026
Merged

anthonyshew merged 1 commit into
mainfrom
shew/untracked-walk-perf

Conversation

@anthonyshew

Copy link
Copy Markdown
Contributor

Why

The untracked-file walk in RepoGitIndex inverts git's gitignore precedence: any ancestor .gitignore ignore rule overrides a whitelist in a deeper .gitignore. Git gives deeper rules priority, so with root *.log and pkg/.gitignore containing !important.log, git reports pkg/important.log as untracked while turbo silently drops it — from both task hashing and the dirty hash. next.js hits this today (test/development/*/.gitignore whitelists node_modules back in). Additionally, tracked files merely ending in .gitignore (e.g. template.gitignore) were wrongly applied as ignore files.

Fixing precedence requires evaluating the matcher chain per entry anyway, which was also the walk's hotspot — so this restructures it for both.

What

  • Verdicts now come from the matcher chain ordered deepest-first: first Ignore/Whitelist match wins, mirroring git. Only files literally named .gitignore contribute rules.
  • Per-thread, generation-tagged memo of the ancestor matcher chain keyed by the entry's readdir parent, replacing per-entry ancestor walks over a HashMap<PathBuf>; matched replaces matched_path_or_any_parents (sound because the walker prunes ignored directories, so every visited entry's ancestors are already known non-ignored).
  • Nested matcher construction parallelized with rayon (~40ms → ~8ms serial cost on next.js).
  • Directories whose own .gitignore is a catch-all with no whitelists are pruned outright (provably equivalent to descending).
  • The untracked-.gitignore post-filter keeps the previous parent-walking semantics since no pruning protects it.

How

Verified against git ground truth on a next.js clone with untracked files planted in whitelisted node_modules/dist fixtures: the new walk's output exactly matches git ls-files --others --exclude-standard; main misses the whitelisted ones (covered by new unit tests that fail on main). Interleaved benchmarks on next.js (~44k entries, M-series, 10 runs): find_untracked_files 195ms → 168ms (−14%), end-to-end --dry 311ms → 286ms (−8%) — while visiting 3.7% more entries that main incorrectly skipped, so repos unaffected by the precedence bug see larger wins. turborepo-scm 182 tests and turborepo-lib 431 tests pass; clippy/fmt clean.

Note for reviewers: this changes hashes in repos where the precedence bug previously dropped whitelisted untracked files — that is the fix taking effect.

@anthonyshew
anthonyshew requested a review from a team as a code owner July 3, 2026 16:18
@anthonyshew
anthonyshew requested review from tknickman and removed request for a team July 3, 2026 16:18
@vercel

vercel Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
examples-basic-web Ready Ready Preview, Comment, Open in v0 Jul 3, 2026 4:19pm
examples-designsystem-docs Ready Ready Preview, Comment, Open in v0 Jul 3, 2026 4:19pm
examples-gatsby-web Ready Ready Preview, Comment, Open in v0 Jul 3, 2026 4:19pm
examples-kitchensink-blog Ready Ready Preview, Comment, Open in v0 Jul 3, 2026 4:19pm
examples-nonmonorepo Ready Ready Preview, Comment, Open in v0 Jul 3, 2026 4:19pm
examples-svelte-web Ready Ready Preview, Comment, Open in v0 Jul 3, 2026 4:19pm
examples-tailwind-web Ready Ready Preview, Comment, Open in v0 Jul 3, 2026 4:19pm
examples-vite-web Ready Ready Preview, Comment, Open in v0 Jul 3, 2026 4:19pm
turbo-site Ready Ready Preview, Comment, Open in v0 Jul 3, 2026 4:19pm
turborepo-eve-agent Error Error Open in v0 Jul 3, 2026 4:19pm

@anthonyshew
anthonyshew merged commit bc32fcc into main Jul 3, 2026
56 of 59 checks passed
@anthonyshew
anthonyshew deleted the shew/untracked-walk-perf branch July 3, 2026 16:30
anthonyshew pushed a commit that referenced this pull request Jul 3, 2026
## Release v2.10.3

> [!CAUTION]
> Versioned docs aliasing FAILED. [View
logs](https://github.com/vercel/turborepo/actions/runs/28672736782)

### Changes

- feat: Add nub to create-turbo options (#13173) (`82b1cf1`)
- release(turborepo): 2.10.2 (#13174) (`ec30b18`)
- release(turborepo): 2.10.3-canary.1 (#13176) (`47e2b22`)
- fix: Normalize package manager versions (#13177) (`658f607`)
- fix: Use singular workspace in package resolution error (#13178)
(`d199c53`)
- fix: Support nub native lockfiles (#13179) (`50546e6`)
- perf(build): Enable thin LTO + codegen-units=1 for release builds
(#13160) (`628aea9`)
- release(turborepo): 2.10.3-canary.2 (#13181) (`16aed08`)
- feat: Add aube package manager support (#13183) (`0f78006`)
- release(turborepo): 2.10.3-canary.3 (#13184) (`fec7cee`)
- fix: Normalize package manager version output (#13185) (`f469ed8`)
- release(turborepo): 2.10.3-canary.4 (#13186) (`406fe59`)
- refactor: Richer internal dependency data (#13188) (`909c696`)
- fix: Honor package manager version ranges (`136273e`)
- fix: Skip node_modules in boundaries checks (#13191) (`93741a0`)
- fix: Add token exchange recovery guidance (#13192) (`bef588b`)
- feat: Add --production flag to turbo prune (#13190) (`3777442`)
- release(turborepo): 2.10.3-canary.5 (#13193) (`ca20ca2`)
- feat: Accept Bun lockfile version 2 (#13119) (`69e2085`)
- fix: Handle task executor join errors as internal errors (#13148)
(`c4017e9`)
- fix:  Tweaks for nub support (#13187) (`ca029ee`)
- release(turborepo): 2.10.3-canary.6 (#13194) (`b79f767`)
- release(turborepo): 2.10.3-canary.7 (#13195) (`b426736`)
- chore: Upgrade TypeScript to 7.0.1-rc (#13144) (`cf07baa`)
- docs: Add filtered installs to Vercel guide (#13196) (`7e70b52`)
- examples: add with-mcp-servers (#12997) (`8eb4d9d`)
- fix: Report malformed JSON parse errors instead of panicking (#13198)
(`a69df7d`)
- fix: Improve error messaging when platform binary is missing (#13199)
(`8e145f0`)
- release(turborepo): 2.10.3-canary.8 (#13200) (`5987ed1`)
- fix(watch): recover from slow initial hash instead of timing out
(#13159) (`05e26cb`)
- chore: Update vitest example to vitest 4 and use native test merging
(#13202) (`e2b61dc`)
- feat: Toggle between the TUI and streamed logs (#13203) (`1325455`)
- fix: Resolve Bun scoped package deps that share a name with a
dependency (#13207) (`e4773ab`)
- feat: Select tasks by clicking rows in the TUI task list (#13206)
(`c568f67`)
- perf: Avoid deep-cloning dependency maps in lockfile closure cache
(#13209) (`abd84cf`)
- perf: Cache root internal dependencies in package graph (#13211)
(`7ec8c6d`)
- feat: Automatically copy TUI selection to clipboard on mouse release
(#13208) (`61d6013`)
- perf: Cache env wildcard matches across tasks during hashing (#13210)
(`148b1dd`)
- perf: Replace per-package graph traversals in scope filtering (#13212)
(`28d1871`)
- perf: Derive dirty hash from repo index (#13213) (`b521d32`)
- fix: Include untracked symlinks in repo-index dirty hash (#13218)
(`c3c91ab`)
- refactor: Use upstream libghostty-vt crates instead of vendored
bindings (#13205) (`c2115dc`)
- fix: Remove devtools feature flag (#13219) (`65efe27`)
- fix: Harden TUI terminal restore during shutdown (#13220) (`dccab93`)
- fix: Correct gitignore precedence in untracked walk and memoize
matcher chains (#13221) (`bc32fcc`)

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
anthonyshew pushed a commit that referenced this pull request Jul 4, 2026
## Release v2.10.4-canary.1

> [!CAUTION]
> Versioned docs aliasing FAILED. [View
logs](https://github.com/vercel/turborepo/actions/runs/28719728846)

### Changes

- release(turborepo): 2.10.3-canary.8 (#13200) (`5987ed1`)
- fix(watch): recover from slow initial hash instead of timing out
(#13159) (`05e26cb`)
- chore: Update vitest example to vitest 4 and use native test merging
(#13202) (`e2b61dc`)
- feat: Toggle between the TUI and streamed logs (#13203) (`1325455`)
- fix: Resolve Bun scoped package deps that share a name with a
dependency (#13207) (`e4773ab`)
- feat: Select tasks by clicking rows in the TUI task list (#13206)
(`c568f67`)
- perf: Avoid deep-cloning dependency maps in lockfile closure cache
(#13209) (`abd84cf`)
- perf: Cache root internal dependencies in package graph (#13211)
(`7ec8c6d`)
- feat: Automatically copy TUI selection to clipboard on mouse release
(#13208) (`61d6013`)
- perf: Cache env wildcard matches across tasks during hashing (#13210)
(`148b1dd`)
- perf: Replace per-package graph traversals in scope filtering (#13212)
(`28d1871`)
- perf: Derive dirty hash from repo index (#13213) (`b521d32`)
- fix: Include untracked symlinks in repo-index dirty hash (#13218)
(`c3c91ab`)
- refactor: Use upstream libghostty-vt crates instead of vendored
bindings (#13205) (`c2115dc`)
- fix: Remove devtools feature flag (#13219) (`65efe27`)
- fix: Harden TUI terminal restore during shutdown (#13220) (`dccab93`)
- fix: Correct gitignore precedence in untracked walk and memoize
matcher chains (#13221) (`bc32fcc`)
- release(turborepo): 2.10.3 (#13222) (`02f4ed0`)
- ci: Retry Windows nextest aborts from transient 0xc0000142 spawn
failures (#13240) (`222ffff`)
- perf: Use mimalloc as the global allocator (#13237) (`c42a8a9`)
- perf: Parse yarn v1 lockfiles in a single pass (#13241) (`6207099`)
- ci: Fix cache outputs for Eve app (#13243) (`d8b556d`)
- perf: Stop materializing spans for the disabled daemon log layer
(#13244) (`623aadc`)
- fix: Keep ancestor-scoped bun dependencies resolvable after prune
renames (#13236) (`8658b66`)
- perf: Overlap external dependency hashing with package file hashing
(#13234) (`445b0c7`)
- perf: Build tracked repo index concurrently with package graph
(#13232) (`f4699c6`)
- perf: Release tokio runtime at exit so background DNS lookups never
stall the user (#13231) (`ae635bd`)
- perf: Overhaul pnpm lockfile parsing and dependency closure
computation (#13228) (`669676d`)
- feat: Add futureFlags.experimentalCargoWorkspaces flag (no-op)
(#13227) (`f546930`)
- feat: Introduce toolchain provider abstraction (#13235) (`f03addb`)
- perf: Parse Berry lockfiles in a single pass (#13242) (`6d36125`)
- fix: Apply input exclusion globs to the filesystem walk (#13224)
(`d3f9dd3`)
- fix: Stop root-directory packages from claiming every file in change
mapping (#13225) (`f80203a`)
- chore: Rename turborepo-repository napi package to @turbo/repository
(#13226) (`c1972b3`)
- perf: Hash git blobs with hardware-accelerated SHA-1 (#13245)
(`17ffa0c`)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
anthonyshew pushed a commit that referenced this pull request Sep 2, 2026
…he untracked walk (#13916)

### Description

#13221 established that only files literally named `.gitignore` carry
ignore rules, and fixed the tracked and dirty case. its commit message
put it plainly:

> Additionally, tracked files merely *ending* in `.gitignore` (e.g.
`template.gitignore`) were wrongly applied as ignore files.
> Only files literally named `.gitignore` contribute rules.

the untracked walk was left on the old loose test, so the rule is only
half applied. `find_untracked_files` still does

```rust
.filter(|p| p.as_str().ends_with(".gitignore"))
```

which treats a vendored `Node.gitignore`, a `template.gitignore`, or any
stray `old.gitignore` as a real ignore file. patterns are read out of it
and the files it appears to ignore are dropped from `untracked.paths`
before they ever reach `to_hash`.

that lands in the cache key. those files are genuinely untracked as far
as git is concerned, `git check-ignore` does not ignore them, but turbo
never hashes them. editing one does not change the task hash, so `turbo
run` serves a cache entry that does not match the inputs on disk. it is
the default path, `populate_repo_index_untracked` is reached from
`run/builder.rs` on every run.

the same file already had the correct predicate 230 lines above, so this
fixes the drift by giving both sites one helper rather than a second
copy of the string test. the self-exemption inside `not_ignored` had the
same loose test and is covered too.

### Testing Instructions

added
`test_find_untracked_files_ignores_untracked_suffix_named_gitignore_files`,
the untracked twin of the existing
`test_find_untracked_files_ignores_suffix_named_gitignore_files`. same
fixture shape, except the template is untracked rather than tracked.

`cargo test -p turborepo-scm gitignore` passes, 14 tests.

i also checked the test actually catches this rather than just passing.
reverting the predicate and rerunning gives

```
assertion `left == right` failed
  left: [RelativeUnixPathBuf("pkg/Node.gitignore")]
 right: [RelativeUnixPathBuf("pkg/Node.gitignore"), RelativeUnixPathBuf("pkg/debug.log")]
```

`pkg/debug.log` disappearing there is the bug: it is untracked, it
should be hashed, and on main it silently is not.

Co-authored-by: nityam <185968020+Nixxx19@users.noreply.github.com>

This branch had an error being deployed

1 failed deployment
Preview – turborepo-eve-agent 22cd91f6 Deployed Jul 3, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants