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:
+25
-3
@@ -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)
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user