Merge PR #736: fix(detect): forward follow_symlinks from detect_incremental to detect

This commit is contained in:
Safi
2026-05-06 11:58:59 +01:00
3 changed files with 41 additions and 3 deletions
+1
View File
@@ -9,6 +9,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu
- Feat: `graphify extract --global --as <tag>` flag -- after building a project graph, auto-registers it into the global graph in one step (#729)
- Feat: `merge-graphs` now prefix-relabels each input graph before composing, preventing silent node ID collisions when two projects share entity names (#729)
- Fix: `deduplicate_entities` raises `ValueError` if called with nodes spanning multiple repos (cross-project dedup disabled by design -- per-project graphs are deduplicated in isolation) (#729)
- Fix: `detect_incremental()` now accepts and forwards `follow_symlinks` to `detect()`. Without this, `--update` runs silently miss any files reached through a symlinked sub-tree (e.g. `state_of_truth/` symlinking to a directory outside the corpus root), even when the original full run had detected them. Previously the flag was on `detect()` and `collect_files()` only. (#736)
## 0.7.6 (2026-05-05)
+12 -2
View File
@@ -771,7 +771,12 @@ def save_manifest(files: dict[str, list[str]], manifest_path: str = _MANIFEST_PA
Path(manifest_path).write_text(json.dumps(manifest, indent=2), encoding="utf-8")
def detect_incremental(root: Path, manifest_path: str = _MANIFEST_PATH) -> dict:
def detect_incremental(
root: Path,
manifest_path: str = _MANIFEST_PATH,
*,
follow_symlinks: bool = False,
) -> dict:
"""Like detect(), but returns only new or modified files since the last run.
Fast path: mtime unchanged unchanged (free, no hash).
@@ -779,8 +784,13 @@ def detect_incremental(root: Path, manifest_path: str = _MANIFEST_PATH) -> dict:
treat as unchanged. Different hash = actually changed, re-extract.
Backwards compatible with legacy manifests storing plain float mtime values.
The ``follow_symlinks`` flag is forwarded to :func:`detect` so corpora that
rely on symlinked sub-trees (e.g. a ``state_of_truth/`` symlink pointing to a
directory outside the scan root) are scanned consistently between full and
incremental runs.
"""
full = detect(root)
full = detect(root, follow_symlinks=follow_symlinks)
manifest = load_manifest(manifest_path)
if not manifest:
+28 -1
View File
@@ -1,5 +1,5 @@
from pathlib import Path
from graphify.detect import classify_file, count_words, detect, FileType, _looks_like_paper, _is_ignored, _load_graphifyignore
from graphify.detect import classify_file, count_words, detect, detect_incremental, save_manifest, FileType, _looks_like_paper, _is_ignored, _load_graphifyignore
FIXTURES = Path(__file__).parent / "fixtures"
@@ -220,6 +220,33 @@ def test_detect_handles_circular_symlinks(tmp_path):
assert any("main.py" in f for f in result["files"]["code"])
def test_detect_incremental_propagates_follow_symlinks(tmp_path, monkeypatch):
"""detect_incremental must forward follow_symlinks so symlinked sub-trees
appear in incremental scans the same way they appear in full scans."""
monkeypatch.chdir(tmp_path)
real_dir = tmp_path / "real_corpus"
real_dir.mkdir()
(real_dir / "note.md").write_text("# real note\n\nsome content")
(tmp_path / "linked_corpus").symlink_to(real_dir)
manifest_path = str(tmp_path / "manifest.json")
# Without following symlinks, the symlinked dir contents are invisible.
no_link = detect_incremental(tmp_path, manifest_path, follow_symlinks=False)
assert not any("linked_corpus" in f for f in no_link["files"]["document"])
# With follow_symlinks=True, the symlinked dir contents appear and are new.
yes_link = detect_incremental(tmp_path, manifest_path, follow_symlinks=True)
assert any("linked_corpus" in f for f in yes_link["files"]["document"])
assert yes_link["new_total"] >= 2 # real + linked
# After saving manifest, a second incremental scan should see no changes.
save_manifest(yes_link["files"], manifest_path)
second = detect_incremental(tmp_path, manifest_path, follow_symlinks=True)
assert second["new_total"] == 0
def test_classify_video_extensions():
"""Video and audio file extensions should classify as VIDEO."""
from graphify.detect import FileType