Files
gaokao-volunteer-system/scripts/gaokao-sync-remotes

147 lines
4.2 KiB
Plaintext
Raw Normal View History

2026-06-13 14:49:58 +08:00
#!/usr/bin/env python3
"""gaokao-sync-remotes — T10.3 三仓同步脚本。"""
from __future__ import annotations
import argparse
import subprocess
import sys
from dataclasses import dataclass
DEFAULT_REMOTES = ("gitea", "origin", "tksea")
@dataclass(frozen=True)
class PushResult:
remote: str
branch: str
local_head: str
class SyncError(RuntimeError):
"""Raised when sync preconditions or push verification fail."""
def _run_git(*args: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["git", *args],
capture_output=True,
text=True,
check=False,
)
def _git_stdout(*args: str) -> str:
result = _run_git(*args)
if result.returncode != 0:
raise SyncError(result.stderr.strip() or f"git {' '.join(args)} failed")
return result.stdout.strip()
def _resolve_branch(explicit_branch: str | None) -> str:
if explicit_branch:
return explicit_branch
branch = _git_stdout("branch", "--show-current")
if not branch:
raise SyncError("detached HEAD: 请显式传入 --branch")
return branch
def _ensure_required_remotes(remotes: tuple[str, ...]) -> None:
configured = {
line.strip()
for line in _git_stdout("remote").splitlines()
if line.strip()
}
missing = [remote for remote in remotes if remote not in configured]
if missing:
raise SyncError(f"missing remotes: {', '.join(missing)}")
def _current_head(branch: str) -> str:
return _git_stdout("rev-parse", branch)
def _remote_head(remote: str, branch: str) -> str:
result = _run_git("ls-remote", "--heads", remote, branch)
if result.returncode != 0:
raise SyncError(result.stderr.strip() or f"git ls-remote {remote} {branch} failed")
output = result.stdout.strip()
if not output:
raise SyncError(f"remote {remote} missing branch {branch} after push")
return output.split()[0]
def sync_remotes(remotes: tuple[str, ...], branch: str, dry_run: bool) -> list[PushResult]:
_ensure_required_remotes(remotes)
local_head = _current_head(branch)
results: list[PushResult] = []
for remote in remotes:
if dry_run:
print(f"[DRY-RUN] git push {remote} {branch}")
results.append(PushResult(remote=remote, branch=branch, local_head=local_head))
continue
push = _run_git("push", remote, branch)
if push.returncode != 0:
stderr = push.stderr.strip() or push.stdout.strip() or f"git push {remote} {branch} failed"
raise SyncError(f"push failed for {remote}: {stderr}")
remote_head = _remote_head(remote, branch)
if remote_head != local_head:
raise SyncError(
f"verify failed for {remote}: local {local_head} != remote {remote_head}"
)
results.append(PushResult(remote=remote, branch=branch, local_head=local_head))
return results
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="gaokao-sync-remotes",
description="Push 当前分支到 gitea / origin / tksea 并校验 remote HEAD。",
)
parser.add_argument(
"--branch",
help="要同步的本地分支,默认取当前分支",
)
parser.add_argument(
"--remote",
dest="remotes",
action="append",
help="指定要同步的 remote可重复传入默认 gitea/origin/tksea",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="只打印将执行的 git push不真正推送",
)
return parser
def main(argv: list[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
try:
branch = _resolve_branch(args.branch)
remotes = tuple(args.remotes or DEFAULT_REMOTES)
results = sync_remotes(remotes=remotes, branch=branch, dry_run=args.dry_run)
except SyncError as exc:
print(str(exc), file=sys.stderr)
return 2
if args.dry_run:
print(f"DRY-RUN OK: {branch} -> {', '.join(result.remote for result in results)}")
return 0
for result in results:
print(f"OK {result.remote}: {result.branch} @ {result.local_head}")
return 0
if __name__ == "__main__":
raise SystemExit(main())