fix(recovery): never write recovered logs into a git submodule
compute_output_path now parses .gitmodules and, for a project scope whose dir is a submodule (guru-rmm, guru-connect, youtube-sync-docker), falls back to the MAIN repo root session-logs/ per convention. Non-submodule projects (gururmm-agent, dataforth-dos) unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -132,6 +132,71 @@ def _identity() -> dict:
|
||||
return {}
|
||||
|
||||
|
||||
# Cache for parsed .gitmodules paths (keyed by repo root path string).
|
||||
_SUBMODULE_PATHS_CACHE: dict[str, frozenset[str]] = {}
|
||||
# Matches `path = projects/foo` lines in .gitmodules (leading whitespace, any
|
||||
# spacing around `=`). The captured value is the repo-relative submodule path.
|
||||
_GITMODULES_PATH_RE = re.compile(r"^\s*path\s*=\s*(.+?)\s*$", re.MULTILINE)
|
||||
|
||||
|
||||
def _submodule_paths() -> frozenset[str]:
|
||||
"""Return the set of repo-relative submodule paths from ``.gitmodules``.
|
||||
|
||||
Parses the ``path = ...`` lines via a simple line scan (no git calls).
|
||||
Paths are normalized to forward slashes with no trailing slash so they can
|
||||
be compared against forward-slashed, repo-relative work paths. Robust to a
|
||||
missing ``.gitmodules`` (returns an empty set -> no submodules). Cached per
|
||||
repo root for the life of the process.
|
||||
"""
|
||||
root = repo_root()
|
||||
key = str(root)
|
||||
cached = _SUBMODULE_PATHS_CACHE.get(key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
paths: set[str] = set()
|
||||
gitmodules = root / ".gitmodules"
|
||||
try:
|
||||
text = gitmodules.read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
text = ""
|
||||
for m in _GITMODULES_PATH_RE.finditer(text):
|
||||
rel = m.group(1).strip().replace("\\", "/").rstrip("/")
|
||||
if rel:
|
||||
paths.add(rel)
|
||||
|
||||
result = frozenset(paths)
|
||||
_SUBMODULE_PATHS_CACHE[key] = result
|
||||
return result
|
||||
|
||||
|
||||
def _is_inside_submodule(target_dir: Path) -> bool:
|
||||
"""True if ``target_dir`` is at or under any submodule path.
|
||||
|
||||
Comparison is done on repo-relative, forward-slashed path components so a
|
||||
submodule ``projects/msp-tools/guru-rmm`` matches that directory and
|
||||
anything beneath it (e.g. its ``session-logs/``), but does NOT match a
|
||||
sibling like ``projects/msp-tools/guru-rmm-extra``.
|
||||
"""
|
||||
subs = _submodule_paths()
|
||||
if not subs:
|
||||
return False
|
||||
root = repo_root()
|
||||
try:
|
||||
rel = target_dir.resolve().relative_to(root.resolve())
|
||||
except (OSError, ValueError):
|
||||
# target is not under the repo root (or cannot resolve) -> not a submodule.
|
||||
return False
|
||||
rel_parts = rel.parts
|
||||
for sub in subs:
|
||||
sub_parts = tuple(p for p in sub.split("/") if p)
|
||||
if not sub_parts:
|
||||
continue
|
||||
if rel_parts[: len(sub_parts)] == sub_parts:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def transcript_base_dir() -> Path:
|
||||
"""Compute ``~/.claude/projects/<slug>`` from identity's claudetools_root."""
|
||||
root = _identity().get("claudetools_root") or str(repo_root())
|
||||
@@ -923,7 +988,14 @@ def compute_output_path(parsed: ParsedTranscript, scope: dict, title: str) -> Pa
|
||||
msp_dir = root / "projects" / "msp-tools" / slug
|
||||
if msp_dir.exists():
|
||||
proj_dir = msp_dir
|
||||
base = proj_dir / "session-logs"
|
||||
# If the project dir is a git submodule (or inside one), its working
|
||||
# tree must NOT be written to -- repo convention keeps those session
|
||||
# logs in the MAIN repo root instead, and an unattended write would
|
||||
# dirty the submodule. Fall back to the main root session-logs dir.
|
||||
if _is_inside_submodule(proj_dir):
|
||||
base = root / "session-logs"
|
||||
else:
|
||||
base = proj_dir / "session-logs"
|
||||
else:
|
||||
base = root / "session-logs"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user