You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A ! line stops re-including when an ancestor directory is matched by a dir/** pattern. git
re-includes in that situation, because X/** excludes the contents of X and not X itself, so
the "you cannot re-include under an excluded directory" rule never fires.
dvc 3.67.1, pathspec 1.1.1, git 2.55.0, Python 3.14, Linux. The same verdict comes out of the
library (DvcIgnoreFilter.is_ignored_file), so it is not a check-ignore-only discrepancy like
the one discussed in #10122.
Drop the ** and dvc is right. With volumes/functions/ + !volumes/functions/deno.json,
git also refuses to re-include (the directory itself is excluded), and dvc says the same. So
the variable is the **, not the negation.
Where it happens
DvcIgnorePatterns._ignore walks the ancestor prefixes of a path and breaks on the first one that
matches, which is the right shape for git's rule. The ancestor is tested through _find_matching_pattern, which — for a directory — also tries path + "/":
pathspec compiles volumes/functions/** to ^volumes/functions/, and that regex matches the
probe string volumes/functions/. So the ancestor is judged excluded, the loop breaks at i = 2,
and the basename where !volumes/functions/deno.json would have won is never reached.
The trailing-slash probe cannot simply be dropped: a genuine directory pattern X/ compiles to ^X(?P<ps_d>/) and needs it. What is missing is the distinction between a pattern that excludes X and one that only excludes what is under it.
Worth knowing before choosing a fix: on the same two lines, pathspec.GitIgnoreSpec.from_lines(...).match_file("volumes/functions/deno.json") returns False
— the correct answer. The dependency you already depend on resolves this case; it is dvc's own
ancestor walk that diverges from it.
How I found it, and how rare it is
I maintain a conformance bench that compares tools against git check-ignore --no-index on rule
files harvested from real repositories, and dvc is in it because it is the one project I could find
that promises the per-directory layer rather than improvising it. Over 4441 queries from 64
repositories, this is the only divergence dvc produces. The failing case is supabase/supabase's docker/.gitignore, which has exactly this shape:
Bug Report
Description
A
!line stops re-including when an ancestor directory is matched by adir/**pattern. gitre-includes in that situation, because
X/**excludes the contents ofXand notXitself, sothe "you cannot re-include under an excluded directory" rule never fires.
Reproduce
dvc3.67.1,pathspec1.1.1,git2.55.0, Python 3.14, Linux. The same verdict comes out of thelibrary (
DvcIgnoreFilter.is_ignored_file), so it is not acheck-ignore-only discrepancy likethe one discussed in #10122.
Two controls, so the variable is pinned
ignore.txt+!no-ignore.txtat the root: git and dvc agree,both APIs. Whatever Negation "!" in .dvcignore doesn't unignore #10122 was about, this is not a repeat of it.
**and dvc is right. Withvolumes/functions/+!volumes/functions/deno.json,git also refuses to re-include (the directory itself is excluded), and dvc says the same. So
the variable is the
**, not the negation.Where it happens
DvcIgnorePatterns._ignorewalks the ancestor prefixes of a path and breaks on the first one thatmatches, which is the right shape for git's rule. The ancestor is tested through
_find_matching_pattern, which — for a directory — also triespath + "/":pathspeccompilesvolumes/functions/**to^volumes/functions/, and that regex matches theprobe string
volumes/functions/. So the ancestor is judged excluded, the loop breaks ati = 2,and the basename where
!volumes/functions/deno.jsonwould have won is never reached.The trailing-slash probe cannot simply be dropped: a genuine directory pattern
X/compiles to^X(?P<ps_d>/)and needs it. What is missing is the distinction between a pattern that excludesXand one that only excludes what is under it.Worth knowing before choosing a fix: on the same two lines,
pathspec.GitIgnoreSpec.from_lines(...).match_file("volumes/functions/deno.json")returnsFalse— the correct answer. The dependency you already depend on resolves this case; it is dvc's own
ancestor walk that diverges from it.
How I found it, and how rare it is
I maintain a conformance bench that compares tools against
git check-ignore --no-indexon rulefiles harvested from real repositories, and dvc is in it because it is the one project I could find
that promises the per-directory layer rather than improvising it. Over 4441 queries from 64
repositories, this is the only divergence dvc produces. The failing case is
supabase/supabase'sdocker/.gitignore, which has exactly this shape:Bench, corpus and the reduction scripts: https://github.com/KaizenShogun/gitignore-conformance —
happy to run any candidate fix through it.
— Midas