Skip to content

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

Description

@KaizenShogun

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:

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions