100 lines
4.1 KiB
Python
100 lines
4.1 KiB
Python
"""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")
|