Skip to content
Merged
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
28 changes: 25 additions & 3 deletions graphify/detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -662,8 +662,29 @@ def _could_contain_included_path(path: Path, root: Path, patterns: list[tuple[Pa
return False


def detect(root: Path, *, follow_symlinks: bool = False, google_workspace: bool | None = None) -> dict:
def _auto_follow_symlinks(root: Path) -> bool:
"""Auto-detect: ``True`` if ``root`` has any direct symlinked child.

Allows "fake working dir" patterns (e.g. a folder full of symlinks pointing
at scattered source dirs across the user's machine) to work transparently
without the caller having to know to pass ``follow_symlinks=True``.

Override is always possible by passing an explicit ``follow_symlinks=True``
or ``follow_symlinks=False`` to :func:`detect` / :func:`detect_incremental`.
"""
try:
for p in root.iterdir():
if p.is_symlink():
return True
except (OSError, PermissionError):
pass
return False


def detect(root: Path, *, follow_symlinks: bool | None = None, google_workspace: bool | None = None) -> dict:
root = root.resolve()
if follow_symlinks is None:
follow_symlinks = _auto_follow_symlinks(root)
google_workspace = google_workspace_enabled() if google_workspace is None else google_workspace
files: dict[FileType, list[str]] = {
FileType.CODE: [],
Expand Down Expand Up @@ -869,7 +890,7 @@ def detect_incremental(
root: Path,
manifest_path: str = _MANIFEST_PATH,
*,
follow_symlinks: bool = False,
follow_symlinks: bool | None = None,
google_workspace: bool | None = None,
kind: str = "semantic",
) -> dict:
Expand All @@ -893,7 +914,8 @@ def detect_incremental(
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.
incremental runs. ``None`` (default) means auto-detect: ``True`` when ``root``
contains at least one direct symlinked child, ``False`` otherwise.
"""
full = detect(root, follow_symlinks=follow_symlinks, google_workspace=google_workspace)
manifest = load_manifest(manifest_path)
Expand Down
41 changes: 41 additions & 0 deletions tests/test_detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,47 @@ def test_detect_handles_circular_symlinks(tmp_path):
assert any("main.py" in f for f in result["files"]["code"])


def test_detect_auto_detects_direct_symlink_child(tmp_path):
"""When ``root`` has a direct symlinked child, default (None) follows symlinks
so the user does not have to know to pass follow_symlinks=True for "fake
working dir" patterns (folder of symlinks pointing at scattered sources)."""
real_dir = tmp_path / "real_lib"
real_dir.mkdir()
(real_dir / "util.py").write_text("x = 1")
(tmp_path / "linked_lib").symlink_to(real_dir)

# Default (no kwarg): auto-detect → follows because of linked_lib symlink
result = detect(tmp_path)
assert any("linked_lib" in f for f in result["files"]["code"])


def test_detect_default_does_not_follow_when_no_symlinks(tmp_path):
"""When ``root`` has no direct symlinks, the auto-detect default stays False
(legacy behaviour preserved for ordinary scans)."""
(tmp_path / "main.py").write_text("x = 1")
sub = tmp_path / "sub"
sub.mkdir()
(sub / "other.py").write_text("y = 2")

# Smoke: no symlinks anywhere → auto-detect returns False, scan succeeds
result = detect(tmp_path)
assert any("main.py" in f for f in result["files"]["code"])
assert any("other.py" in f for f in result["files"]["code"])


def test_detect_explicit_false_overrides_auto_detect(tmp_path):
"""An explicit follow_symlinks=False overrides the auto-detect, even when
root contains symlinks. Lets callers opt out of the new behaviour."""
real_dir = tmp_path / "real_lib"
real_dir.mkdir()
(real_dir / "util.py").write_text("x = 1")
(tmp_path / "linked_lib").symlink_to(real_dir)

# Explicit False overrides auto-detect; symlink contents must NOT appear.
result = detect(tmp_path, follow_symlinks=False)
assert not any("linked_lib" 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."""
Expand Down
Loading