Andrew Nesbitt's journey to fix phantom diffs in git-pkgs reveals the hidden complexity behind .gitignore implementations and how competing standards fracture the 'gitignore syntax' ecosystem.

A phantom diff in git-pkgs led developer Andrew Nesbitt down a rabbit hole that exposed fundamental fractures in how tools implement ignore files. What began as a bug report—where go-git's ignore implementation mishandled unanchored patterns in nested directories—revealed a broader ecosystem problem: the phrase "gitignore syntax" means wildly different things across development tools.
The Hidden Depths of .gitignore
Most developers interact with .gitignore through simple patterns like *.log or node_modules/, but Git's actual implementation involves surprising complexity:
- Four-layer pattern hierarchy: Global excludes (~/.config/git/ignore), repo-local (.git/info/exclude), root .gitignore, and per-directory .gitignore files cascade with increasing priority
- Anchoring semantics: Patterns without slashes (
debug.log) match anywhere (**/debug.logimplied), while patterns containing slashes (/debug.log) anchor to specific directories - Wildcard nuances:
*matches within single path segments only, while**must be standalone between slashes (foo/**/bar) - Bracket edge cases:
[B-a]spans byte values 66-97 (B-Z, symbols, a), and]as first character is literal - Negation limitations:
!can't re-include files in excluded parent directories without re-including each intermediate path
These behaviors are codified in Git's wildmatch.c and dir.c, with test suites in t0008-ignores.sh and t3070-wildmatch.sh. Yet most reimplementations—like go-git—fail to replicate these edge cases faithfully.
The Ecosystem of Ignorance
Nearly 30 tools have adopted ignore files claiming "gitignore syntax," including:
- Deployment:
.dockerignore,.gcloudignore,.vercelignore,.slugignore - Linters:
.prettierignore,.eslintignore,.stylelintignore - Package managers:
.npmignore(which inverts to allow-listing via package.jsonfiles) - Search tools:
.ignore(shared by ripgrep/silver searcher),.rgignore,.agignore
Each makes divergent implementation choices:
- Docker: Uses Go's filepath.Match without Git's implicit
**/prefixing for unanchored patterns (@balena/dockerignore documents differences) - npm: Falls back to .gitignore if no .npmignore exists—causing accidental exclusion of build artifacts
- Mercurial: Supports regex patterns (
syntax: regexp) alongside globs in .hgignore - ripgrep: Deprecated .rgignore in favor of shared .ignore files using BurntSushi's ignore crate
The "Gitignore Syntax" Myth
The term "gitignore syntax" typically implies:
- Line-delimited patterns
#comments!negation- Basic wildcards (*, ?)
But critical variations persist:
| Feature | Git | Docker | npm | Mercurial | ripgrep |
|---|---|---|---|---|---|
| Doublestar ** | ✅ | ✅ | ✅ | ✅ | ✅ |
| Anchored paths | ✅ | ❌ | ❌ | ✅ | ✅ |
| Negation | ✅ | ❌ | ✅ | ✅ | ✅ |
| Regex support | ❌ | ❌ | ❌ | ✅ | ❌ |
| Cascading ignores | ✅ | ❌ | ❌ | ✅ | ✅ |
This fragmentation forces developers to memorize tool-specific quirks. Nesbitt notes: "You can't assume trailing / directory matching works the same everywhere—Docker treats build/ as directory-only while Git also ignores contents."
Toward a Common Ignore Standard
The ecosystem shows early signs of consolidation. ripgrep and silver searcher converged on .ignore, while BurntSushi's ignore crate (91M+ downloads) powers multiple tools. This parallels Markdown's journey: pre-CommonMark fragmentation was resolved through formal specifications with test suites.
A potential path forward:
- Codify Git's behavior in a formal spec
- Create compliance levels (Level 1: basic globs; Level 2: negation/doublestar)
- Build shared test harnesses using Git's wildmatch tests as foundation
Yet challenges remain. Mercurial's regex support offers functionality Git lacks, while Docker's context-limited ignores need different optimization constraints. As tools evolve beyond Git's original use cases, strict compatibility may hinder innovation.
The ignore file ecosystem reveals a fundamental tension: developer convenience demands interoperability, but specialized tools require domain-specific behaviors. Until standardized compliance levels emerge, "gitignore syntax" will remain a convenient fiction—one that masks profound implementation differences beneath a veneer of compatibility.

Comments
Please log in or register to join the discussion