mirror of
https://github.com/safishamsi/graphify.git
synced 2026-07-23 07:41:46 +00:00
fix(detect): keep ignore globs within path segments
This commit is contained in:
committed by
safishamsi
parent
d56b70a451
commit
aad595571d
+37
-5
@@ -7,6 +7,7 @@ import re
|
||||
import shlex
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from enum import Enum
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
from graphify.google_workspace import (
|
||||
@@ -934,6 +935,34 @@ def _load_graphifyignore(root: Path) -> list[tuple[Path, str]]:
|
||||
return patterns
|
||||
|
||||
|
||||
def _match_anchored_ignore_pattern(path: str, pattern: str) -> bool:
|
||||
"""Match an anchored gitignore pattern without letting ``*`` cross ``/``."""
|
||||
path_parts = tuple(path.split("/"))
|
||||
pattern_parts = tuple(pattern.split("/"))
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def _matches(path_idx: int, pattern_idx: int) -> bool:
|
||||
if pattern_idx == len(pattern_parts):
|
||||
return path_idx == len(path_parts)
|
||||
|
||||
part = pattern_parts[pattern_idx]
|
||||
if part == "**":
|
||||
if pattern_idx == len(pattern_parts) - 1:
|
||||
return path_idx < len(path_parts)
|
||||
return _matches(path_idx, pattern_idx + 1) or (
|
||||
path_idx < len(path_parts)
|
||||
and _matches(path_idx + 1, pattern_idx)
|
||||
)
|
||||
|
||||
return (
|
||||
path_idx < len(path_parts)
|
||||
and fnmatch.fnmatchcase(path_parts[path_idx], part)
|
||||
and _matches(path_idx + 1, pattern_idx + 1)
|
||||
)
|
||||
|
||||
return _matches(0, 0)
|
||||
|
||||
|
||||
def _is_ignored(
|
||||
path: Path,
|
||||
root: Path,
|
||||
@@ -961,9 +990,9 @@ def _is_ignored(
|
||||
"""Apply last-match-wins to a single target path."""
|
||||
if _cache is not None and target in _cache:
|
||||
return _cache[target]
|
||||
def _matches(rel: str, p: str, anchored: bool) -> bool:
|
||||
if anchored:
|
||||
return fnmatch.fnmatch(rel, p)
|
||||
def _matches(rel: str, p: str, path_relative: bool) -> bool:
|
||||
if path_relative:
|
||||
return _match_anchored_ignore_pattern(rel, p)
|
||||
parts = rel.split("/")
|
||||
if fnmatch.fnmatch(rel, p):
|
||||
return True
|
||||
@@ -980,7 +1009,8 @@ def _is_ignored(
|
||||
for anchor, pattern in patterns:
|
||||
negated = pattern.startswith("!")
|
||||
raw = pattern[1:] if negated else pattern
|
||||
anchored = raw.startswith("/")
|
||||
directory_only = raw.endswith("/")
|
||||
path_relative = "/" in raw.rstrip("/")
|
||||
p = raw.strip("/")
|
||||
if not p:
|
||||
continue
|
||||
@@ -996,7 +1026,9 @@ def _is_ignored(
|
||||
except ValueError:
|
||||
continue # target outside this pattern's anchor: cannot match
|
||||
if rel_anchor != ".":
|
||||
matched = _matches(rel_anchor, p, anchored=anchored)
|
||||
matched = _matches(rel_anchor, p, path_relative=path_relative)
|
||||
if matched and directory_only and not target.is_dir():
|
||||
matched = False
|
||||
|
||||
if matched:
|
||||
result = not negated # last match wins; ! flips to un-ignore
|
||||
|
||||
@@ -738,6 +738,69 @@ def test_detect_skips_graphify_own_cache(tmp_path):
|
||||
|
||||
# --- #882: gitignore parent-exclusion rule for ! re-includes ---
|
||||
|
||||
def test_anchored_root_wildcard_negation_reincludes_subtree(tmp_path):
|
||||
"""`/*` stays at the root, so `!/src/` makes the subtree walkable (#1975)."""
|
||||
for rel in ("src/app/main.py", "src/lib/util.py", "docs/guide.md", "README.md"):
|
||||
path = tmp_path / rel
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("x\n")
|
||||
(tmp_path / ".graphifyignore").write_text("/*\n!/src/\n")
|
||||
|
||||
result = detect(tmp_path)
|
||||
|
||||
files = {
|
||||
Path(path).relative_to(tmp_path).as_posix()
|
||||
for paths in result["files"].values()
|
||||
for path in paths
|
||||
}
|
||||
assert files == {"src/app/main.py", "src/lib/util.py"}
|
||||
|
||||
|
||||
def test_anchored_negation_cannot_skip_excluded_parent(tmp_path):
|
||||
"""Re-including a child cannot rescue it while its parent stays excluded."""
|
||||
victim = tmp_path / "src" / "app" / "main.py"
|
||||
victim.parent.mkdir(parents=True)
|
||||
victim.write_text("x\n")
|
||||
(tmp_path / ".graphifyignore").write_text("/*\n!/src/app/\n")
|
||||
|
||||
assert detect(tmp_path)["total_files"] == 0
|
||||
|
||||
|
||||
def test_path_pattern_single_star_does_not_cross_segment(tmp_path):
|
||||
"""A regular `*` matches one component; recursive matching requires `**`."""
|
||||
direct = tmp_path / "src" / "main.py"
|
||||
nested = tmp_path / "src" / "app" / "main.py"
|
||||
nested.parent.mkdir(parents=True)
|
||||
direct.write_text("x\n")
|
||||
nested.write_text("x\n")
|
||||
for pattern in ("/src/*.py", "src/*.py"):
|
||||
(tmp_path / ".graphifyignore").write_text(f"{pattern}\n")
|
||||
result = detect(tmp_path)
|
||||
files = [path for paths in result["files"].values() for path in paths]
|
||||
assert not any(path.endswith("src/main.py") for path in files)
|
||||
assert any(path.endswith("src/app/main.py") for path in files)
|
||||
|
||||
|
||||
def test_directory_only_negation_does_not_reinclude_file(tmp_path):
|
||||
"""A trailing slash restricts a pattern to directories, as in gitignore."""
|
||||
readme = tmp_path / "README.md"
|
||||
readme.write_text("# docs\n")
|
||||
(tmp_path / ".graphifyignore").write_text("/*\n!/README.md/\n")
|
||||
|
||||
assert detect(tmp_path)["total_files"] == 0
|
||||
|
||||
|
||||
def test_anchored_double_star_crosses_path_segments(tmp_path):
|
||||
"""`**` retains recursive gitignore matching at zero or more depths."""
|
||||
direct = tmp_path / "src" / "generated.py"
|
||||
nested = tmp_path / "src" / "app" / "deep" / "generated.py"
|
||||
nested.parent.mkdir(parents=True)
|
||||
direct.write_text("x\n")
|
||||
nested.write_text("x\n")
|
||||
(tmp_path / ".graphifyignore").write_text("/src/**/generated.py\n")
|
||||
|
||||
assert detect(tmp_path)["total_files"] == 0
|
||||
|
||||
def test_negation_cannot_rescue_file_under_excluded_dir(tmp_path):
|
||||
"""A ! re-include cannot un-ignore a file whose parent dir is excluded (#882)."""
|
||||
from graphify.detect import _is_ignored, _load_graphifyignore
|
||||
|
||||
Reference in New Issue
Block a user