Merge pull request #887 from alphanury/feat/auto-detect-symlinks-default

Clean merge — fixes real silent data-loss bug in --update on symlinked repos, backward-compatible, inherits existing cycle protection. Follow-up: consider tightening _auto_follow_symlinks to is_symlink() and is_dir() to avoid tripping on incidental symlinked files at root.
This commit is contained in:
Safi
2026-05-16 14:12:54 +01:00
committed by GitHub
2 changed files with 66 additions and 3 deletions
+25 -3
View File
@@ -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: [],
@@ -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:
@@ -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)
+41
View File
@@ -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."""