fix(detect): scope nested ignore patterns to their own subtree (#1873)
Non-anchored patterns from a nested .gitignore/.graphifyignore were
matched against root-relative paths first, so a nested ignore file's
patterns leaked outside its directory. In the wild, .hypothesis/.gitignore
(a bare "*" auto-written by the hypothesis library) ignored the entire
repository and detect() returned 0 files.
Per gitignore semantics, patterns from A/.gitignore apply only to paths
under A. Match every pattern against the path relative to its own anchor
(the anchor dir itself exempt — an ignore file governs its directory's
contents, not the directory), and skip patterns whose anchor does not
contain the target.
Regression introduced with nested-ignore support in 8a5287a (#1206).
tests/test_detect.py: 143 passed, including the existing nested-ignore
and nested-negation tests plus two new regressions.
Fixes #1873
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
committed by
safishamsi
co-authored by
Claude Fable 5
parent
368407874f
commit
fb4d45226e
+11
-18
@@ -955,25 +955,18 @@ def _is_ignored(
|
|||||||
if not p:
|
if not p:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# gitignore semantics: patterns from A/.gitignore apply ONLY to paths
|
||||||
|
# under A. Matching non-anchored patterns against root-relative paths
|
||||||
|
# let e.g. .hypothesis/.gitignore's bare "*" ignore the ENTIRE repo
|
||||||
|
# (detect() returned 0 files). The anchor dir itself is exempt — an
|
||||||
|
# ignore file governs its directory's contents, not the directory.
|
||||||
matched = False
|
matched = False
|
||||||
if anchored:
|
try:
|
||||||
try:
|
rel_anchor = str(target.relative_to(anchor)).replace(os.sep, "/")
|
||||||
rel_anchor = str(target.relative_to(anchor)).replace(os.sep, "/")
|
except ValueError:
|
||||||
matched = _matches(rel_anchor, p, anchored=True)
|
continue # target outside this pattern's anchor: cannot match
|
||||||
except ValueError:
|
if rel_anchor != ".":
|
||||||
pass
|
matched = _matches(rel_anchor, p, anchored=anchored)
|
||||||
else:
|
|
||||||
try:
|
|
||||||
rel = str(target.relative_to(root)).replace(os.sep, "/")
|
|
||||||
matched = _matches(rel, p, anchored=False)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
if not matched and anchor != root:
|
|
||||||
try:
|
|
||||||
rel_anchor = str(target.relative_to(anchor)).replace(os.sep, "/")
|
|
||||||
matched = _matches(rel_anchor, p, anchored=False)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if matched:
|
if matched:
|
||||||
result = not negated # last match wins; ! flips to un-ignore
|
result = not negated # last match wins; ! flips to un-ignore
|
||||||
|
|||||||
@@ -1815,3 +1815,35 @@ def test_detect_surfaces_unreadable_dir_instead_of_silent_skip(tmp_path, capsys)
|
|||||||
assert any(f.endswith("a.py") for f in code) # rest of tree still enumerated
|
assert any(f.endswith("a.py") for f in code) # rest of tree still enumerated
|
||||||
assert len(res["walk_errors"]) >= 1
|
assert len(res["walk_errors"]) >= 1
|
||||||
assert "could not scan" in capsys.readouterr().err
|
assert "could not scan" in capsys.readouterr().err
|
||||||
|
|
||||||
|
|
||||||
|
def test_nested_gitignore_star_does_not_ignore_outside_its_dir(tmp_path):
|
||||||
|
"""A nested .gitignore containing a bare `*` (auto-written by e.g. the
|
||||||
|
hypothesis library into .hypothesis/) must ignore ONLY that directory's
|
||||||
|
contents — matching it against root-relative paths ignored the entire
|
||||||
|
corpus (detect() returned 0 files on a real repo). Regression for #1873."""
|
||||||
|
(tmp_path / "README.md").write_text("# hello")
|
||||||
|
(tmp_path / "main.py").write_text("x = 1")
|
||||||
|
hyp = tmp_path / ".hypothesis"
|
||||||
|
hyp.mkdir()
|
||||||
|
(hyp / ".gitignore").write_text("*\n")
|
||||||
|
(hyp / "cached.py").write_text("y = 2")
|
||||||
|
|
||||||
|
result = detect(tmp_path)
|
||||||
|
|
||||||
|
assert result["total_files"] == 2 # README.md + main.py survive; .hypothesis/* ignored
|
||||||
|
|
||||||
|
|
||||||
|
def test_nested_gitignore_patterns_still_apply_inside_their_dir(tmp_path):
|
||||||
|
"""Counterpart guard: the anchor-scoped fix must not stop nested ignore
|
||||||
|
files from working WITHIN their own subtree."""
|
||||||
|
(tmp_path / "main.py").write_text("x = 1")
|
||||||
|
sub = tmp_path / "sub"
|
||||||
|
sub.mkdir()
|
||||||
|
(sub / ".gitignore").write_text("*.log\n")
|
||||||
|
(sub / "keep.py").write_text("y = 2")
|
||||||
|
(sub / "noise.log").write_text("z")
|
||||||
|
|
||||||
|
result = detect(tmp_path)
|
||||||
|
|
||||||
|
assert result["total_files"] == 2 # main.py + sub/keep.py; sub/noise.log ignored
|
||||||
|
|||||||
Reference in New Issue
Block a user