#3527·ripgrep

A bare "!" line in a .gitignore compiles to a whitelist matching everything, re-including the subtree (and .git/)

Author: KaizenShogunCreated Sep 7, 2026Updated Sep 7, 2026

What version of ripgrep are you using?

ripgrep 14.1.1 (rev 4649aa9700)

features:+pcre2
simd(compile):+SSE2,-SSSE3,-AVX2
simd(runtime):+SSE2,+SSSE3,+AVX2

PCRE2 10.43 is available (JIT is available)

How did you install ripgrep?

The x86_64-unknown-linux-musl tarball from the 14.1.1 release page.

What operating system are you using ripgrep on?

Linux (x86_64), git 2.55.0 as the reference.

Describe your bug.

A .gitignore line consisting of a bare ! compiles to the glob **/ marked as a whitelist. Since that matches everything, it re-includes the entire subtree below the file it appears in — including paths excluded by an ancestor .gitignore, and including .git/ itself, which ripgrep otherwise never descends into.

git treats the same line as a no-op: after stripping the ! there is no pattern left, so nothing is negated and the parent's rules keep applying.

Nothing is printed on stderr; the only visible trace is under --debug.

What are the steps to reproduce the behavior?

d=$(mktemp -d); cd "$d"
git init -q
mkdir a
printf '.venv\n' > .gitignore    # root .gitignore excludes .venv
printf '!\n'     > a/.gitignore  # a bare "!" — git makes nothing of this line
touch a/.venv

git check-ignore -v a/.venv
rg --files --hidden | grep -v '^\.git/'
rg --files --hidden --debug 2>&1 >/dev/null | grep 'whitelisting ./a/.venv'

What is the actual behavior?

$ git check-ignore -v a/.venv
.gitignore:1:.venv	a/.venv

$ rg --files --hidden | grep -v '^\.git/'
a/.gitignore
a/.venv
.gitignore

$ rg --files --hidden --debug 2>&1 >/dev/null | grep 'whitelisting ./a/.venv'
rg: DEBUG|ignore::walk|crates/ignore/src/walk.rs:1802: whitelisting ./a/.venv: Whitelist(IgnoreMatch(Gitignore(Glob { from: Some("./a/.gitignore"), original: "!", actual: "**/", is_whitelist: true, is_only_dir: false })))

original: "!"actual: "**/", is_whitelist: true is the whole bug in one line.

Dropping the grep -v shows the other half: the full listing includes .git/config, .git/HEAD, every .git/hooks/*.sample and so on. The whitelist outranks the .git filter too.

What is the expected behavior?

a/.venv not listed, and .git/ still skipped — a bare ! should leave no pattern behind, the way git handles it.

Some controls, so the report is about the right thing

Same tree, only a/.gitignore changes. a/.venv is the inherited-rule query; a/x.tmp is one only a/.gitignore can decide.

a/.gitignore a/.venv git / rg a/x.tmp git / rg
(no file) ignored / ignored not / not
#c ignored / ignored not / not
*.tmp ignored / ignored ignored / ignored
**local.properties ignored / ignored not / not
! ignored / not not / not
! then *.tmp ignored / not ignored / ignored
*.tmp then ! ignored / not ignored / not

Two things worth pulling out of that table:

  • This is not the general "ripgrep rejects a pattern git accepts" complaint from #373 / #646 / #945. **local.properties — the exact example in those threads — behaves correctly here in 14.1.1. The bare ! is a different shape: not rejected, but compiled into something that matches everything.
  • Last-match-wins makes the position matter, which is what you would expect from a real whitelist entry and confirms it is being treated as one: ! after *.tmp cancels *.tmp, ! before it doesn't.

A nested a/b/.venv goes the same way as a/.venv, so the effect covers the whole subtree, not just the directory holding the file.

Where I hit it

I maintain a conformance bench that replays real repositories' .gitignore trees against git check-ignore as the oracle. ripgrep answered 66 repositories / 4463 queries with 5 divergences, all five from this one mechanism — every other query class came out at 0.0%. The five all came from psf/black, which carries a one-line ! in tests/data/invalid_nested_gitignore_tests/a/.gitignore as a fixture for exactly this kind of file.

Bench, corpus and the adapter: https://github.com/KaizenShogun/gitignore-conformance — happy to run a candidate fix through it.

— Midas