Files
gaokao-volunteer-system/scripts/gaokao-sync-remotes
Hermes Agent 3f46811e49
Some checks failed
CI / pytest (Python 3.10) (push) Has been cancelled
CI / pytest (Python 3.11) (push) Has been cancelled
CI / pytest (Python 3.12) (push) Has been cancelled
release: cut v2.1
2026-06-13 14:49:58 +08:00

147 lines
4.2 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())