From 48a8da896be0158fa89f5e4314b950e0d8f85822 Mon Sep 17 00:00:00 2001 From: abhirupone Date: Fri, 25 Sep 2026 15:27:43 +0530 Subject: [PATCH] fix(markdown): respect ignore rules when building wikilink index (#3822) --- graphify/extractors/markdown.py | 19 +++++++++++----- tests/test_languages.py | 40 +++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/graphify/extractors/markdown.py b/graphify/extractors/markdown.py index 4dc2b8feb..d2d28f30d 100644 --- a/graphify/extractors/markdown.py +++ b/graphify/extractors/markdown.py @@ -147,20 +147,27 @@ def _build_link_index(root: Path) -> "dict[str, list[tuple[int, str, Path]]]": """Index every linkable document under *root* by NFC-normalized basename. Each entry maps basename -> [(depth, root-relative posix path, absolute - path)]. Directories in detect._SKIP_DIRS and dot-directories are pruned — - the same corpus boundary the scanner draws, and Obsidian itself does not - index dot-folders. + path)]. Directories in detect._SKIP_DIRS, dot-directories, and paths + excluded by .graphifyignore/.gitignore are pruned — the same corpus + boundary the scanner draws, and Obsidian itself does not index dot-folders + or ignored paths (#3822). """ - from graphify.detect import _SKIP_DIRS + from graphify.detect import _SKIP_DIRS, ignored_predicate + root = Path(root) + ignored = ignored_predicate(root) index: dict[str, list[tuple[int, str, Path]]] = {} for dirpath, dirnames, filenames in os.walk(root): + dp = Path(dirpath) dirnames[:] = sorted( - d for d in dirnames if not d.startswith(".") and d not in _SKIP_DIRS + d for d in dirnames + if not d.startswith(".") and d not in _SKIP_DIRS and not ignored(dp / d) ) for fname in filenames: if Path(fname).suffix.lower() not in _MD_LINKABLE_EXTS: continue - abs_path = Path(dirpath) / fname + abs_path = dp / fname + if ignored(abs_path): + continue rel = os.path.relpath(str(abs_path), str(root)).replace("\\", "/") index.setdefault(_nfc(fname), []).append( (rel.count("/"), _nfc(rel), abs_path) diff --git a/tests/test_languages.py b/tests/test_languages.py index 4508bbfdd..7416425bf 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -3119,6 +3119,46 @@ def test_markdown_wikilink_fallback_unicode_normalization(tmp_path): for e in refs), f"NFD wikilink missed the NFC file: {refs}" +def test_markdown_wikilink_index_prunes_ignored_directories(tmp_path, monkeypatch): + """#3822: _build_link_index must prune directories and files matched by + .graphifyignore/.gitignore, preventing both 50+ min directory walks on + large ignored trees and wikilinks erroneously resolving into ignored files.""" + vault = tmp_path / "vault" + vault.mkdir() + (vault / ".graphifyignore").write_text("bigdata/\nsecret.md\n", encoding="utf-8") + + (vault / "notes").mkdir() + (vault / "notes" / "hub.md").write_text("# Hub\nSee [[target]] and [[secret]].\n", encoding="utf-8") + (vault / "notes" / "target.md").write_text("# Target\n", encoding="utf-8") + + (vault / "bigdata").mkdir() + (vault / "bigdata" / "sub").mkdir() + (vault / "bigdata" / "sub" / "secret.md").write_text("# Secret in bigdata\n", encoding="utf-8") + + import os + visited = [] + real_walk = os.walk + + def spy_walk(top, *args, **kwargs): + for dirpath, dirnames, filenames in real_walk(top, *args, **kwargs): + visited.append(os.path.relpath(dirpath, str(vault))) + yield dirpath, dirnames, filenames + + monkeypatch.setattr(os, "walk", spy_walk) + + node_ids, refs, page_id = _vault_extract( + vault, [vault / "notes" / "hub.md", vault / "notes" / "target.md"] + ) + + bigdata_walked = [v for v in visited if v.startswith("bigdata")] + assert not bigdata_walked, f"ignored directory descended during index walk: {bigdata_walked}" + + hub_id = page_id(vault / "notes" / "hub.md") + target_id = page_id(vault / "notes" / "target.md") + assert any(e["source"] == hub_id and e["target"] == target_id for e in refs), f"valid target link lost: {refs}" + assert not any("bigdata" in e["target"] for e in refs), f"wikilink resolved into ignored path: {refs}" + + # ── Groovy ───────────────────────────────────────────────────────────────────