Check parent directories in Match and add git check-ignore conformance tests - #27
Conversation
…e tests Match now tests each parent directory of a path before the path itself, so a file under an excluded directory is reported as ignored and a negation cannot re-include it, matching git's per-directory traversal. The implicit trailing ** and the /** rewrite are removed; matchSegments takes a flag so a trailing ** must consume at least one segment. A whole-segment run of two or more asterisks now compiles as **. Four existing tests that asserted the opposite of git are corrected. Fixes #25. Fixes #26.
b504362 to
66f08d2
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
Guard stderr access in conformance_test.go so command startup failures report test failures instead of panicking.
Pull request overview
Aligns ignore matching with Git’s parent-directory traversal semantics and adds broad git check-ignore conformance coverage.
Changes:
- Checks excluded parent directories before matching descendants.
- Corrects trailing
**, multi-star, and bracket-suffix behavior. - Adds fixture-based, self-checking, and fuzz conformance tests.
File summaries
| File | Description |
|---|---|
wildmatch.go |
Enforces required consumption for trailing **. |
testdata/conformance/vscode-star-idiom/paths |
Adds VS Code idiom query paths. |
testdata/conformance/vscode-star-idiom/gitignore |
Adds VS Code idiom patterns. |
testdata/conformance/real-world-vscode/paths |
Adds real-world VS Code paths. |
testdata/conformance/real-world-vscode/gitignore |
Adds real-world VS Code patterns. |
testdata/conformance/real-world-node/paths |
Adds Node.js query paths. |
testdata/conformance/real-world-node/gitignore |
Adds Node.js ignore patterns. |
testdata/conformance/real-world-generated/paths |
Adds generated-tree paths. |
testdata/conformance/real-world-generated/gitignore |
Adds generated-file patterns. |
testdata/conformance/nested-doublestar-both-ends/paths |
Adds nested ** paths. |
testdata/conformance/nested-doublestar-both-ends/gitignore |
Adds nested ** patterns. |
testdata/conformance/negation-under-excluded-dir/paths |
Adds excluded-parent cases. |
testdata/conformance/negation-under-excluded-dir/gitignore |
Adds negation-under-exclusion patterns. |
testdata/conformance/negation-under-excluded-dir-slash/paths |
Adds slash-exclusion cases. |
testdata/conformance/negation-under-excluded-dir-slash/gitignore |
Adds slash-exclusion patterns. |
testdata/conformance/negation-reincluded-dir-contents/paths |
Adds re-inclusion cases. |
testdata/conformance/negation-reincluded-dir-contents/gitignore |
Adds re-inclusion patterns. |
testdata/conformance/negation-not-inherited/paths |
Adds negation inheritance cases. |
testdata/conformance/negation-not-inherited/gitignore |
Adds negation inheritance patterns. |
testdata/conformance/negation-not-inherited-dironly/paths |
Adds directory-only inheritance cases. |
testdata/conformance/negation-not-inherited-dironly/gitignore |
Adds directory-only negation patterns. |
testdata/conformance/multi-star-segment/paths |
Adds multi-star query paths. |
testdata/conformance/multi-star-segment/gitignore |
Adds multi-star patterns. |
testdata/conformance/exclusion-then-negation-same-dir/paths |
Adds ordering cases. |
testdata/conformance/exclusion-then-negation-same-dir/gitignore |
Adds exclusion/negation patterns. |
testdata/conformance/doublestar-slash-only/paths |
Adds **/ cases. |
testdata/conformance/doublestar-slash-only/gitignore |
Adds slash-only ** patterns. |
testdata/conformance/doublestar-not-parent/paths |
Adds trailing ** cases. |
testdata/conformance/doublestar-not-parent/gitignore |
Adds non-directory trailing ** patterns. |
testdata/conformance/doublestar-not-parent-dironly/paths |
Adds directory-only trailing ** cases. |
testdata/conformance/doublestar-not-parent-dironly/gitignore |
Adds directory-only trailing ** patterns. |
testdata/conformance/dir-only-basics/paths |
Adds directory-only query paths. |
testdata/conformance/dir-only-basics/gitignore |
Adds directory-only patterns. |
testdata/conformance/bracket-with-star/paths |
Adds bracket-expression paths. |
testdata/conformance/bracket-with-star/gitignore |
Adds bracket-star patterns. |
testdata/conformance/anchored-and-wildcard/paths |
Adds anchored and wildcard paths. |
testdata/conformance/anchored-and-wildcard/gitignore |
Adds anchored and wildcard patterns. |
gitignore.go |
Implements parent checks and revised pattern compilation. |
gitignore_test.go |
Updates tests for Git-compatible semantics. |
conformance_test.go |
Adds Git conformance, self-check, and fuzz tests. |
Review details
Suppressed comments (1)
conformance_test.go:284
- If
exec.Commandfails before starting a process, the error is not an*exec.ExitError, sookis false but this branch still dereferencesee.Stderrand panics while reporting the failure. Guard the stderr access (or useCombinedOutput) so startup errors produce a test failure instead of a nil-pointer panic.
if ee, ok := err.(*exec.ExitError); !ok || ee.ExitCode() != 1 {
t.Fatalf("git check-ignore: %v\n%s", err, ee.Stderr)
- Files reviewed: 40/40 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Ran it. Compared as sets rather than counts, against
The middle three are patches I built while measuring the two issues, so they're only useful as a Control, because a zero on its own doesn't prove the bench still detects anything: I re-measured Two things the zero does not cover, both from your PR description:
For the record the only other subject at 0 on this corpus is libgit2 with Bench and corpus: https://github.com/KaizenShogun/gitignore-conformance |
|
Added the variant I said I'd add: the directories a path is reached through, which none of the The zero isn't the interesting part. Same harness, same day, every git-pkgs build I have on disk:
Same row all six times: supabase's Hand-checked the shape I'd have bet on breaking under that flag: git says The oracle, since a directory question needs one stated: Size, honestly: one finding in 1,010 questions, and 40 of those questions are ignored paths at all.
|
|
Ran it again on what you tagged rather than on the branch: v1.3.0 (
Answer for answer identical to the branch I measured here, so the three merges that went in behind this one moved the tree without moving a single verdict — including #30, which is the one I'd have bet on: a fast-reject that skips whole groups is the same shape as the Control, same harness today, because a zero proves nothing on its own: the pre-#27 build still answers 12 and 1, same rows as before. What the zero doesn't reach, so it isn't read as wider than it is: no pattern line in this corpus contains a |
Matchscanned patterns against the full path only. Git checks ignore rules per directory during tree traversal and skips an excluded directory entirely, so any path under it is ignored and a later!line cannot re-include it.Matchnow checks each parent directory of the queried path first, which fixes #25 and #26.With the parent check in place the implicit trailing
**thatcompilePatternappended to every non-dir-only pattern is redundant, and it was the direct cause of #26 (!foocompiled as!foo/**and matched descendants). It is removed, along with the/**-> dir-only rewrite and the dir-only descendant loop inmatchPattern.matchSegmentsgains a flag requiring a trailing**to consume at least one path segment, sofoo/**no longer matchesfooitself.Two more differences from git turned up while building the tests:
foo/**andfoo/**/matched the directoryfooitself; in git they match only its contents. Handled by the trailing-**flag above.***,****) was compiled as a single-segment glob; in git it is equivalent to**. Any all-*segment of length two or more is now compiled as**.extractLiteralSuffixtreated a*inside a bracket expression as the glob-star boundary, so[a*]bproduced a suffix of]band the fast-reject skippedab. That was masked onmainby the implicit trailing**; the extractor now bails when the suffix contains].Four existing tests asserted the opposite of
git check-ignore2.55.0 and are corrected:TestMatchDoubleStarSlash,TestMatchCannotReincludeUnderExcludedParent,TestMatchDoubleStarTrailingDir,TestMatchNegationSubdirectoryFilter.TestMatchAgainstGitCheckIgnorenever invoked git and is renamedTestMatchTable.New tests in
conformance_test.go:TestConformancebuilds a temp repo per case undertestdata/conformance/, materialises the listed paths, runsgit check-ignore --no-index -z -v -n --stdinonce over all of them, and compares the results with the library. Eighteen cases covering the two issues, the four corrected tests, the findings above, and a few real-world pattern shapes.TestConformanceHarnessSelfCheckrunsgit check-ignore -qper path and compares against the batched parse, so a parsing bug in the harness surfaces separately from a matcher bug.TestConformanceFuzz(skipped under-short) generates random pattern sets and paths and compares against git.GITIGNORE_FUZZ_ROUNDSandGITIGNORE_FUZZ_SEEDcontrol the run size.HOME,XDG_CONFIG_HOME,GIT_CONFIG_GLOBALandGIT_CONFIG_SYSTEMare pointed at an empty temp dir for these tests so neither the git subprocess norNewreads user-level excludes.Benchmarks: shallow-path matches are roughly twice as fast because the literal-suffix fast-reject now applies to most patterns instead of almost none;
MatchDeepPath(depth 8) is about 44% slower from the parent loop;Walkis unchanged. Allocations unchanged.