"""Fetch remote refs for selected repositories without merging.""" from __future__ import annotations from pathlib import Path import subprocess from .git_state import collect_repository_snapshot from .repository_push import command_text, compact_output from .workspace import load_repository_specs, resolve_repo_path, resolve_workspace_root def sync_repositories( *, repos: tuple[str, ...], workspace_root: Path | str | None = None, remote: str = "origin", apply: bool = False, # noqa: A002 - mirrors API field. ) -> dict[str, object]: workspace = resolve_workspace_root(workspace_root) remote = remote.strip() or "origin" specs = {spec.name: spec for spec in load_repository_specs(include_website=False)} selected = tuple(dict.fromkeys(repos)) results: list[dict[str, object]] = [] command = ("git", "fetch", "--prune", "--tags", remote) for repo in selected: spec = specs.get(repo) if spec is None: results.append({"repo": repo, "status": "blocked", "detail": "repository is not listed in repositories.json"}) continue before = collect_repository_snapshot(spec, workspace_root=workspace, target_tag=None, online=False) row: dict[str, object] = { "repo": repo, "cwd": before.absolute_path, "branch": before.branch, "upstream": before.upstream, "head": before.head, "ahead": int(before.ahead or 0), "behind": int(before.behind or 0), "dirty_count": len(before.dirty_entries), "remote": remote, "command": command_text(command), } if not before.exists or not before.is_git: results.append({**row, "status": "blocked", "detail": "repository is missing or is not a Git repository"}) continue if before.safe_directory_required: results.append({**row, "status": "blocked", "detail": "Git safe.directory approval is required"}) continue if not apply: results.append({**row, "status": "planned", "detail": "fetch remote refs and tags; no merge or rebase will be run"}) continue process = run(command, cwd=resolve_repo_path(spec, workspace)) if process.returncode != 0: results.append( { **row, "status": "failed", "returncode": process.returncode, "detail": "git fetch failed", "stdout": compact_output(process.stdout), "stderr": compact_output(process.stderr), } ) continue after = collect_repository_snapshot(spec, workspace_root=workspace, target_tag=None, online=False) results.append( { **row, "status": "synced", "returncode": process.returncode, "detail": "remote refs and tags fetched; local branch was not merged or rebased", "after_ahead": int(after.ahead or 0), "after_behind": int(after.behind or 0), "after_dirty_count": len(after.dirty_entries), "stdout": compact_output(process.stdout), "stderr": compact_output(process.stderr), } ) if any(item["status"] in {"blocked", "failed"} for item in results): status = "blocked" if not apply else "partial" elif any(item["status"] == "synced" for item in results): status = "synced" elif any(item["status"] == "planned" for item in results): status = "planned" else: status = "noop" return {"status": status, "apply": apply, "remote": remote, "repositories": results} def run(command: tuple[str, ...], *, cwd: Path) -> subprocess.CompletedProcess[str]: try: return subprocess.run(command, cwd=cwd, check=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=120) except subprocess.TimeoutExpired as exc: return subprocess.CompletedProcess(command, 124, exc.stdout or "", exc.stderr or "git fetch timed out")