Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 11 additions & 18 deletions graphify/detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -955,25 +955,18 @@ def _matches(rel: str, p: str, anchored: bool) -> bool:
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
Expand Down
32 changes: 32 additions & 0 deletions tests/test_detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -1754,3 +1754,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