civicrm/gitignore
A Laravel/PHP package for managing .gitignore rules, helping keep generated and environment-specific files out of version control. It provides a simple way to maintain consistent ignore patterns across CiviCRM projects.
Why reach for this. You need to answer, inside PHP, "would git ignore this path?" — or you need to read/write a pattern list that happens to share gitignore syntax. That second part is the real leverage: .gitignore, .dockerignore, .npmignore, .prettierignore, ripgrep ignore files, PHP-CS-Fixer finder excludes, and git sparse-checkout all use the same grammar. One parser covers all of them, which makes this a small but broadly applicable utility rather than a git-specific one.
Typical day-to-day triggers in a Laravel codebase:
Finder traversal that should skip ignored trees instead of descending into node_modules and storage/framework..gitignore.sparse-checkout without hand-rolling a second pattern dialect.Install and orient.
composer require civicrm/gitignore
Then read, in this order:
composer.json — check the PHP constraint (CiviCRM-adjacent packages often target older runtimes) and the license field. It reports NOASSERTION on GitHub, so confirm the actual LICENSE file before shipping it in anything distributed.src/ — the package is deliberately small, likely one parser plus a rule value object. Skim the whole thing; it will take minutes and removes all guesswork.tests/ — this is the de facto spec. Whatever the README omits about **, negation, anchoring, and escaped characters will be pinned down here.First use case: don't clobber ignored files.
The shape of the API is "feed it pattern lines (or a file's contents), get back something you can match paths against." Confirm the exact class and method names in src/ — the sketch below shows the flow, not a guaranteed signature:
// Adjust names to the real entry point found in src/.
$rules = $parser->parse(file_get_contents(base_path('.gitignore')));
if ($rules->matches('storage/app/generated/thumb.png')) {
// leave the user's ignored file alone
return;
}
If that's all you need, stop there. Don't build a service layer around a thirty-line call site.
Wrap it once, cache the compiled rules. Parsing is cheap but per-request parsing is pointless. Bind a matcher as a singleton keyed on the file's hash, so edits invalidate naturally:
$this->app->singleton(IgnoreMatcher::class, fn () => IgnoreMatcher::fromFiles([
base_path('.gitignore'),
base_path('.git/info/exclude'),
]));
In a queued worker or long-running Octane process, key the cache on md5_file() rather than registering once at boot.
Honour the layering, not just one file. Git resolves rules in layers, with deeper files winning:
| Layer | Path | Notes |
|---|---|---|
| Global | core.excludesFile |
Not discoverable from the repo; read git config or skip |
| Repo-local | .git/info/exclude |
Same syntax, never committed |
| Root | .gitignore |
The one everyone thinks of |
| Nested | path/to/.gitignore |
Patterns are relative to their directory |
A parser returns patterns; you own the directory-relative resolution. Pass paths relative to each file's base directory and evaluate from shallow to deep, letting later rules override earlier ones.
Use it as a generator, not only a matcher. Because the grammar is shared, the same code writes sparse-checkout sets and rsync --exclude-from inputs. Keep a small emit helper alongside the parse helper — that symmetry is where the package pays for itself twice.
Integrate with Finder instead of walking manually. Convert rules to a filter callback and let Symfony do the traversal:
$finder->filter(fn (SplFileInfo $f) => !$matcher->matches(
str_replace(base_path().'/', '', $f->getPathname())
));
Note Finder's own ->ignoreVCSIgnored(true) already shells out to git — prefer that when a git binary is present and use this package only where it isn't.
Add a CI guard command. Parse git ls-files, test each path against the matcher, and fail on any tracked file that is ignored. That single check catches the most common cause of "works on my machine" deploys — a .env or compiled asset committed by accident.
Test the patterns as data. A PHPUnit data provider over [pattern, path, expected] triples documents your team's assumptions and gives you a regression net if you ever swap implementations.
Keep an escape hatch. Define your own tiny IgnoreMatcher interface with matches(string $relativePath): bool, backed by this package. If you later need authoritative semantics, you can swap in git check-ignore or a fork without touching call sites.
A parser is not git. The hard parts of gitignore are not lexing patterns — they're path resolution semantics. Verify these against git check-ignore -v in a throwaway repo before trusting the library:
foo/ plus !foo/keep.txt still ignores keep.txt, because git never descends into foo. Naive matchers get this wrong and it's the #1 source of surprise./ (other than a trailing one) is anchored to the .gitignore's directory; one without is matched at any depth.** placement matters. **/logs differs from logs/**, and a/**/b matches zero or more intermediate directories.# is a comment; \! and \# are literal. CRLF line endings bite Windows contributors.core.ignorecase=true is common on Windows/macOS and changes the answer. Decide explicitly and document it.Subdirectory pruning differs. Git skips reading nested .gitignore files inside already-excluded directories for performance. A recursive parser that reads everything will produce answers git wouldn't. Usually harmless; occasionally not.
Global and info excludes are out of scope. No parser can find core.excludesFile for you — it lives in git config. If your matcher only reads .gitignore, it will disagree with git status in repos with global excludes. Either read git config --get core.excludesFile once or accept the divergence and say so in the class docblock.
Zero stars, zero dependents, no release safety net. That's not disqualifying for a package this small, but it changes how you consume it:
"civicrm/gitignore": "1.0.0"), not a caret range.License is NOASSERTION. GitHub couldn't classify it. CiviCRM projects are typically AGPL-3.0-or-later, which has real obligations if you distribute the application. Check LICENSE and get a yes/no from whoever owns that question before the code lands in a shipped product.
Check the PHP constraint early. Older CiviCRM tooling often supports conservative PHP ranges and may lack typed properties or return types. If you're on PHP 8.3 with strict tooling, verify it installs and passes your static analysis before designing around it.
Debugging divergences. Keep a fixture repo with the awkward cases (nested ignores, negation inside excluded dirs, ** variants) and diff the library's verdicts against git check-ignore -v --no-index output. Do this once, in an afternoon; it turns every later "why wasn't that file skipped?" into a one-line lookup.
How can I help you explore Laravel packages today?