We Matched ripgrep’s Ignore Rules — Wide Field Part 7: 43 Checks Agree on Three OSes, and the One Rule We Refused to Copy

Technical Guide

The counts did not match.

The same directory, handed to rg --files and to my own CLI, produced two different file lists. The culprit was .gitignore — or rather, the fact that we were not reading it at all.

Which raises the question: when you say “the same rules as ripgrep,” how far does the same go?

The short version: we now follow .ignore and .gitignore under ripgrep 15’s rules, and the rg --files comparison agrees on 43 checks across Mac, Linux and Windows — zero mismatches. The one thing we did not copy is .rgignore

When a glob expands the targets — uvf 'logs/**' ERROR — files matched by .gitignore or .ignore are not searched. Which files we read, the precedence between them, ! negation, how far up the tree we look: all matched to ripgrep 15, and verified by comparing file lists with rg --files. The single deliberate difference is .rgignore, ripgrep’s own filename. Mac (Apple M4, 10 cores), 32 GB RAM, external USB SSD.

uvf and uvp are the search commands of UwView v1.7.3.6 “Wide Field” (released 28 September 2026), which I make. uvf ships in the free edition; uvp is part of the paid UwView Pro. The ignore rules landed in v1.7.2.5, were corrected in v1.7.2.6, and the three-OS results here are from the v1.7.3.6 integration run of 29 September 2026.


What we did not match, first

  • We do not read .rgignore. It is ripgrep’s own filename, and reading it would mean a ripgrep-specific config silently steering a different tool. If you want the same content, put it in .ignore. This incompatibility is deliberate.
  • The fine-grained switches are missing. No --no-ignore-vcs, no --no-ignore-parent. They go in if someone asks.
  • We got --no-ignore wrong once. More on that below: the specification that said “same as ripgrep” was itself wrong, and an outside review caught it.
  • We never timed the ignore matching on its own. Directories with no ignore files short-circuit the check by design, but there is no isolated measurement proving that costs nothing.

Exclusion is not a way to search less. It is a way to agree

I first filed this under performance. Skip node_modules and you go faster, surely.

Building it showed something else. Exclusion is what makes the same command return the same answer as ripgrep.

The reason is blunt: different exclusion means different match counts. And once counts differ, neither the reader nor I can say which side is right. The whole basis for a speed comparison collapses. Before you can compare how fast, you have to guarantee you are searching the same thing.

So the completion criterion was never a timing. It was rg --files and uvf --files '**' producing identical lists across every pattern in the harness.

Which files we read, and which one wins

ripgrep 15 reads .gitignore, .ignore, .rgignore, .git/info/exclude, and git’s global config (core.excludesFile). We read the same set minus .rgignore.

Precedence runs later wins, and within one kind, deeper directories beat shallower ones. From weakest to strongest:

Rank Source Notes
Weakest --ignore-file <path> Repeatable. Lowest precedence (same as ripgrep)
↓ git global config (core.excludesFile in ~/.gitconfig) Falls back to ~/.config/git/ignore
↓ .git/info/exclude
↓ .gitignore Only inside a git repository
Strongest .ignore Works outside a repository too

That .gitignore clause is ripgrep’s behaviour, and it bit us in testing: a copy of a tree without its .git directory is not a repository, so its .gitignore does nothing. Same as ripgrep.

The syntax is gitignore’s, unchanged: *.log at any depth, /build for the top level only, build/ for directories, **/x, !pattern to negate, trailing-whitespace handling, \# escaping. Including git’s quirk that a negation cannot resurrect a file whose parent directory is excluded. The matcher is written by hand — no git subprocess, no extra library, because it has to run under NativeAOT.

A file you name is never excluded

This is the part people trip over.

# Searched, even if build/ is in .gitignore
uvf 'build/app.log' ERROR

# A glob, so the ignore rules apply
uvf 'build/*.log' ERROR

Exclusion applies only to what a glob expanded. Explicit filenames survive it. ripgrep behaves the same way.

The reason is intent. Someone who typed a full path is saying “search this file.” Dropping it silently because of a config file leaves them with zero results and no explanation.

By the same logic, when the ignore rules are what reduced a pattern to zero files, we say so: “(excluded by .gitignore and similar; use --no-ignore to include)”. A small thing, but without it the user keeps re-reading their own glob looking for a typo that is not there.

Search output itself stays clean — excluded files are dropped silently, as in grep and ripgrep. The notices appear only under --files and when a pattern lost everything.

Saying “same as ripgrep” about --no-ignore was our mistake

This piece would otherwise be all good news, so here is the part we got wrong.

The spec said: “--no-ignore also disables rules added with --ignore-file. Same as ripgrep.” That was false. ripgrep 15.2.0 keeps --ignore-file rules in effect under --no-ignore; the flag that turns those off is --no-ignore-files.

An outside review caught it, and we moved to ripgrep’s behaviour: --no-ignore now disables only .ignore / .gitignore and friends, and a new --no-ignore-files was added. Three combinations went into the comparison harness, all agreeing with ripgrep.

Once you declare compatibility, the other implementation is the specification — not your reading of its manual.

How this meets the bundled index

uvp bundles several files into one .uwvz (UwView Pro’s compression format plus index). Add exclusion and the same command can now cover a different set of files. Does the index need rebuilding?

It does not. The rules:

Situation Behaviour
Newly excluded (still on disk) The part stays in the index but is not searched
Newly included again Appended (only the new part is built)
Deleted from disk Still searched from the index (Keep)
Run with --no-ignore The excluded parts get appended. A later default run treats them as “kept but not searched”

So what gets searched is “today’s expanded list plus the parts that vanished from disk.” The index only grows; flipping an ignore rule never triggers a rebuild.

The decision not to rebuild when sources disappear came first; exclusion is a layer sitting on top of it.

The numbers: 43 checks, three operating systems

These are correctness numbers. There are no timings in this article.

Stage Scope Mac Linux Windows
itest .ignore / .gitignore comparison PASS 43 / FAIL 0 PASS 43 / FAIL 0 PASS 43 / FAIL 0
  • The harness builds a small tree — with and without .git, nested .gitignore, .ignore, ! negation, a ! inside an excluded directory, /-anchored patterns, directory-only patterns, **, [a-z], \#, trailing whitespace, .git/info/exclude, CRLF and BOM, an .ignore above the starting directory — and compares its file list with rg --files. Three ways: default, --no-ignore, --ignore-file.
  • Separate unit tests cover the syntax line by line: 44 cases in IgnoreRulesTests, plus 6 for the index interaction.
  • The count went 36 → 42 → 43. The first 36 passed on a development build; the rest came from adding the --no-ignore combinations and re-running against the actual shipped binaries.

The time Windows failed 15 of them

At one point Windows alone failed 15 checks. The bug was in the test script.

ripgrep on Windows returns paths with mixed separators — C:/W11Work/…\1net10-code\…. The comparison script stripped the base directory prefix before normalising the separators, so nothing was stripped and every file looked like it appeared “only in ripgrep.”

The match counts in those 15 checks agreed with ripgrep the whole time. Normalising first, then stripping, put all three operating systems at 43.

The older Windows test machine had no such directory and skipped this section entirely, which is why the bug only surfaced now. A skipped test is not a passing test.

How it was checked

  • Reference: ripgrep 15.2.0 (Mac, Windows), 15.1.0 (Linux)
  • Comparison: rg --files against uvf --files '**' and uvp --files '**' (default, --no-ignore, --ignore-file)
  • Build: uvf and uvp at v1.7.3.6 “Wide Field”, three-OS integration run of 29 September 2026
  • Machines: Mac (Apple M4, 10 cores, 32 GB RAM) with an external USB SSD; Linux on an aarch64 VM; Windows on separate hardware
  • No seconds are compared across operating systems. This article contains no timings

What generalises

(1) Define compatibility by output equality, not by feature lists. “Supports .gitignore” guarantees nothing. “Produces the same list as rg --files” does. When a reference implementation exists, make its output your completion criterion.

(2) The other implementation outranks your spec. The --no-ignore error came from writing down my understanding of someone else’s manual. Every line that claims “same as X” is a hypothesis until a harness compares them.

(3) Make deliberate incompatibilities explicit, and keep them countable. Not reading .rgignore is a decision. But “mostly the same” leaves users unable to find the edges. Being able to enumerate the differences earns more trust than claiming there are none.

(4) Do not count skipped tests as passing tests. Those 15 Windows checks lived in a section the old machine silently skipped. A green table only speaks about the checks that actually ran.

Being straight about it

  • There are no timings in this article. Exclusion is a correctness feature; PASS 43 / FAIL 0 is the only number I can stand behind here
  • The cost of ignore matching was never measured on its own. The design short-circuits when no ignore files exist, but that has not been isolated in a benchmark
  • We do not read .rgignore, and --no-ignore-vcs / --no-ignore-parent do not exist
  • The global git config is read as one line — [core] excludesFile in ~/.gitconfig. include directives are not followed
  • The --no-ignore specification was wrong once, and was corrected after an outside review
  • Linux and Windows appear here only as correctness evidence. Seconds are never compared across operating systems
  • I am one side of this comparison. Discount accordingly

The tools behind this

UwView is a CLI and GUI for inspecting very large text files and logs. In v1.7.3.6 “Wide Field” it searches multiple files and seven compression formats in one pass, with no external commands.

In uvp, you would write

# Exclusion on by default (.ignore and .gitignore are honoured)
uvf 'logs/**' ERROR

# See what the pattern covers first (prints how many were excluded; free)
uvf 'logs/**' --files

# Turn exclusion off
uvf 'logs/**' ERROR --no-ignore

# Add more rules (repeatable; lowest precedence)
uvf 'logs/**' ERROR --ignore-file ./my-ignore

# Also disable the rules added by --ignore-file
uvf 'logs/**' ERROR --no-ignore --no-ignore-files

# A named file is never excluded (searched even with build/ in .gitignore)
uvf 'build/app.log' ERROR

# Bundle and search; excluded parts stay in the index but are not searched (UwView Pro)
uvp 'logs/**' ERROR

Exit codes follow grep: 0 = found, 1 = not found, 2 = error or incomplete result.

The free edition is on GitHub Releases. On Mac, brew install --cask amru195704/uwview/uwview; on Windows, Scoop (scoop bucket add uwview https://github.com/amru195704/scoop-uwview, then scoop install uwview).

  • Seven formats without calling an external command: https://uvp.y42u.net/en/blog/uwview-wf02-seven-formats-no-external-en/
  • Searching compressed logs compared with ugrep, ripgrep and zgrep: https://uvp.y42u.net/en/blog/uwview-ugrep-ripgrep-zgrep-compressed-logs-en/
  • Building a .uwvz, searching inside it, extracting from it: https://uvp.y42u.net/en/blog/uwvz-compressed-archive-search-extract-en/
  • Compressed logs are faster to search (choosing a format): https://uvp.y42u.net/en/blog/log-compression-format-choice-en/
  • Thirteen rounds of reading the file fewer times: https://uvp.y42u.net/en/blog/uwview-fl13-first-light-recap-en/
  • Every measurement we publish: https://uvp.y42u.net/en/benchmarks-en/
  • Source code (GitHub): https://github.com/amru195704/UwView

And if huge logs are eating your storage and you want them compressed and fast to search, take a look at UwView Pro: a persistent index, search inside the compressed cache, roughly 1/9 the storage, and a faster reopen every time (all operating systems; one-time purchase or monthly plan).


From the developer: my apps, Kindle books and public projects are listed at GitHub: amru195704.


Copied title and URL