fix anchored gitignore pattern leaking basename match into subtree (closes #1087)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
a9d6be6537
commit
cca13aa8fd
+12
-8
@@ -671,7 +671,9 @@ def _is_ignored(path: Path, root: Path, patterns: list[tuple[Path, str]]) -> boo
|
||||
|
||||
def _eval(target: Path) -> bool:
|
||||
"""Apply last-match-wins to a single target path."""
|
||||
def _matches(rel: str, p: str) -> bool:
|
||||
def _matches(rel: str, p: str, anchored: bool) -> bool:
|
||||
if anchored:
|
||||
return fnmatch.fnmatch(rel, p)
|
||||
parts = rel.split("/")
|
||||
if fnmatch.fnmatch(rel, p):
|
||||
return True
|
||||
@@ -697,19 +699,19 @@ def _is_ignored(path: Path, root: Path, patterns: list[tuple[Path, str]]) -> boo
|
||||
if anchored:
|
||||
try:
|
||||
rel_anchor = str(target.relative_to(anchor)).replace(os.sep, "/")
|
||||
matched = _matches(rel_anchor, p)
|
||||
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)
|
||||
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)
|
||||
matched = _matches(rel_anchor, p, anchored=False)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
@@ -769,7 +771,9 @@ def _is_included(path: Path, root: Path, patterns: list[tuple[Path, str]]) -> bo
|
||||
if not patterns:
|
||||
return False
|
||||
|
||||
def _matches(rel: str, p: str) -> bool:
|
||||
def _matches(rel: str, p: str, anchored: bool) -> bool:
|
||||
if anchored:
|
||||
return fnmatch.fnmatch(rel, p)
|
||||
parts = rel.split("/")
|
||||
if fnmatch.fnmatch(rel, p):
|
||||
return True
|
||||
@@ -790,21 +794,21 @@ def _is_included(path: Path, root: Path, patterns: list[tuple[Path, str]]) -> bo
|
||||
if anchored:
|
||||
try:
|
||||
rel_anchor = str(path.relative_to(anchor)).replace(os.sep, "/")
|
||||
if _matches(rel_anchor, p):
|
||||
if _matches(rel_anchor, p, anchored=True):
|
||||
return True
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
rel = str(path.relative_to(root)).replace(os.sep, "/")
|
||||
if _matches(rel, p):
|
||||
if _matches(rel, p, anchored=False):
|
||||
return True
|
||||
except ValueError:
|
||||
pass
|
||||
if anchor != root:
|
||||
try:
|
||||
rel_anchor = str(path.relative_to(anchor)).replace(os.sep, "/")
|
||||
if _matches(rel_anchor, p):
|
||||
if _matches(rel_anchor, p, anchored=False):
|
||||
return True
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
@@ -503,6 +503,87 @@ def test_negation_ancestor_itself_reincluded(tmp_path):
|
||||
assert not _is_ignored(f, tmp_path, patterns)
|
||||
|
||||
|
||||
# Regression tests for #1087 - anchored patterns must not match basename deep in tree
|
||||
|
||||
def test_anchored_dir_not_matched_at_depth(tmp_path):
|
||||
"""/inbox/ must not match src/inbox/ — only inbox/ at the anchor root."""
|
||||
from graphify.detect import _is_ignored, _load_graphifyignore
|
||||
src_inbox = tmp_path / "src" / "inbox"
|
||||
src_inbox.mkdir(parents=True)
|
||||
f = src_inbox / "main.rs"
|
||||
f.write_text("fn main() {}")
|
||||
(tmp_path / ".graphifyignore").write_text("/inbox/\n")
|
||||
patterns = _load_graphifyignore(tmp_path)
|
||||
assert not _is_ignored(f, tmp_path, patterns), (
|
||||
"src/inbox/main.rs must NOT be ignored by /inbox/ — the pattern is anchored to root"
|
||||
)
|
||||
assert not _is_ignored(src_inbox, tmp_path, patterns), (
|
||||
"src/inbox/ must NOT be ignored by /inbox/ — the pattern is anchored to root"
|
||||
)
|
||||
|
||||
|
||||
def test_anchored_dir_matches_at_root(tmp_path):
|
||||
"""/inbox/ must still match inbox/ at the anchor root (positive case)."""
|
||||
from graphify.detect import _is_ignored, _load_graphifyignore
|
||||
inbox = tmp_path / "inbox"
|
||||
inbox.mkdir()
|
||||
f = inbox / "data.json"
|
||||
f.write_text("{}")
|
||||
(tmp_path / ".graphifyignore").write_text("/inbox/\n")
|
||||
patterns = _load_graphifyignore(tmp_path)
|
||||
assert _is_ignored(f, tmp_path, patterns), (
|
||||
"inbox/data.json must be ignored by /inbox/"
|
||||
)
|
||||
assert _is_ignored(inbox, tmp_path, patterns), (
|
||||
"inbox/ must be ignored by /inbox/"
|
||||
)
|
||||
|
||||
|
||||
def test_anchored_file_not_matched_at_depth(tmp_path):
|
||||
"""/build must not match src/build."""
|
||||
from graphify.detect import _is_ignored, _load_graphifyignore
|
||||
src_build = tmp_path / "src" / "build"
|
||||
src_build.mkdir(parents=True)
|
||||
(tmp_path / ".graphifyignore").write_text("/build\n")
|
||||
patterns = _load_graphifyignore(tmp_path)
|
||||
assert not _is_ignored(src_build, tmp_path, patterns), (
|
||||
"src/build must NOT be ignored by /build"
|
||||
)
|
||||
|
||||
|
||||
def test_unanchored_dir_still_matches_at_depth(tmp_path):
|
||||
"""inbox/ (no leading /) must still match src/inbox/ anywhere in the tree."""
|
||||
from graphify.detect import _is_ignored, _load_graphifyignore
|
||||
src_inbox = tmp_path / "src" / "inbox"
|
||||
src_inbox.mkdir(parents=True)
|
||||
f = src_inbox / "main.rs"
|
||||
f.write_text("fn main() {}")
|
||||
(tmp_path / ".graphifyignore").write_text("inbox/\n")
|
||||
patterns = _load_graphifyignore(tmp_path)
|
||||
assert _is_ignored(f, tmp_path, patterns), (
|
||||
"src/inbox/main.rs must be ignored by unanchored inbox/"
|
||||
)
|
||||
|
||||
|
||||
def test_anchored_multi_segment_pattern(tmp_path):
|
||||
"""/src/inbox/ must match src/inbox/ but not x/src/inbox/."""
|
||||
from graphify.detect import _is_ignored, _load_graphifyignore
|
||||
(tmp_path / "src" / "inbox").mkdir(parents=True)
|
||||
(tmp_path / "x" / "src" / "inbox").mkdir(parents=True)
|
||||
target_ok = tmp_path / "src" / "inbox" / "a.py"
|
||||
target_ok.write_text("x=1")
|
||||
target_bad = tmp_path / "x" / "src" / "inbox" / "b.py"
|
||||
target_bad.write_text("x=1")
|
||||
(tmp_path / ".graphifyignore").write_text("/src/inbox/\n")
|
||||
patterns = _load_graphifyignore(tmp_path)
|
||||
assert _is_ignored(target_ok, tmp_path, patterns), (
|
||||
"src/inbox/a.py must be ignored by /src/inbox/"
|
||||
)
|
||||
assert not _is_ignored(target_bad, tmp_path, patterns), (
|
||||
"x/src/inbox/b.py must NOT be ignored by /src/inbox/"
|
||||
)
|
||||
|
||||
|
||||
# Regression tests for #920 - sensitive pattern misses underscore-prefixed names
|
||||
def test_sensitive_flags_api_token_txt():
|
||||
assert _is_sensitive(Path("api_token.txt"))
|
||||
|
||||
Reference in New Issue
Block a user