mirror of
https://github.com/safishamsi/graphify.git
synced 2026-07-17 21:06:59 +00:00
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:
+11
-18
@@ -955,25 +955,18 @@ def _is_ignored(
|
||||
if not p:
|
||||
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
|
||||
if anchored:
|
||||
try:
|
||||
rel_anchor = str(target.relative_to(anchor)).replace(os.sep, "/")
|
||||
matched = _matches(rel_anchor, p, anchored=True)
|
||||
except ValueError:
|
||||
pass
|
||||
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
|
||||
try:
|
||||
rel_anchor = str(target.relative_to(anchor)).replace(os.sep, "/")
|
||||
except ValueError:
|
||||
continue # target outside this pattern's anchor: cannot match
|
||||
if rel_anchor != ".":
|
||||
matched = _matches(rel_anchor, p, anchored=anchored)
|
||||
|
||||
if matched:
|
||||
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 len(res["walk_errors"]) >= 1
|
||||
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