From e83e742ff9269a08174d35b38aba48de44be3f50 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 14 Jun 2026 21:10:59 +0800 Subject: [PATCH] chore: add gitea backup hook --- app/Makefile | 5 +- app/scripts/install_gitea_backup_hook.py | 101 +++++++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 app/scripts/install_gitea_backup_hook.py diff --git a/app/Makefile b/app/Makefile index f1c989f..76035a0 100644 --- a/app/Makefile +++ b/app/Makefile @@ -3,7 +3,7 @@ SHELL := /bin/bash APP_PORT ?= 18080 DATA_FILES := ../data/classnotes.txt ../data/学生课时账户.md -.PHONY: check smoke data-hash build up ps health deploy logs +.PHONY: check smoke data-hash build up ps health deploy logs install-gitea-backup check: node --check app/static/app.js @@ -38,3 +38,6 @@ deploy: check smoke logs: docker compose logs --tail=120 xsk-records-web + +install-gitea-backup: + python3 scripts/install_gitea_backup_hook.py diff --git a/app/scripts/install_gitea_backup_hook.py b/app/scripts/install_gitea_backup_hook.py new file mode 100644 index 0000000..6441d47 --- /dev/null +++ b/app/scripts/install_gitea_backup_hook.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +from pathlib import Path +import stat +import subprocess +import sys + + +PROJECT_DIR = Path(__file__).resolve().parents[1] + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="安装提交后自动推送到 Gitea 的 Git hook") + parser.add_argument("--remote", default="origin", help="Gitea remote 名称,默认 origin") + parser.add_argument("--force", action="store_true", help="覆盖已有 post-commit hook") + parser.add_argument("--dry-run", action="store_true", help="只打印将要写入的 hook 内容") + return parser.parse_args() + + +def run_git(args: list[str]) -> str: + result = subprocess.run( + ["git", *args], + cwd=PROJECT_DIR, + text=True, + capture_output=True, + check=False, + ) + if result.returncode != 0: + raise RuntimeError(result.stderr.strip() or result.stdout.strip()) + return result.stdout.strip() + + +def hook_text(remote: str) -> str: + return f"""#!/bin/sh +set -eu + +remote={remote!r} +branch=$(git symbolic-ref --quiet --short HEAD || true) + +if [ -z "$branch" ]; then + echo "[gitea-backup] detached HEAD,跳过自动推送" >&2 + exit 0 +fi + +if ! git remote get-url "$remote" >/dev/null 2>&1; then + echo "[gitea-backup] remote '$remote' 不存在,跳过自动推送" >&2 + exit 0 +fi + +echo "[gitea-backup] pushing $branch to $remote" +if ! git push "$remote" "HEAD:$branch"; then + echo "[gitea-backup] push 失败;本地提交已保留,请检查 SSH key 或远端权限" >&2 + exit 0 +fi +""" + + +def main() -> int: + args = parse_args() + git_dir = Path(run_git(["rev-parse", "--git-dir"])) + if not git_dir.is_absolute(): + git_dir = PROJECT_DIR / git_dir + hook_path = git_dir / "hooks" / "post-commit" + + try: + remote_url = run_git(["remote", "get-url", args.remote]) + except RuntimeError: + remote_url = "" + + content = hook_text(args.remote) + if args.dry_run: + print(content) + if remote_url: + print(f"# remote {args.remote}: {remote_url}") + else: + print(f"# remote {args.remote}: 未配置") + return 0 + + if hook_path.exists() and not args.force: + raise RuntimeError(f"{hook_path} 已存在;如需覆盖,请加 --force") + + hook_path.parent.mkdir(parents=True, exist_ok=True) + hook_path.write_text(content, encoding="utf-8") + mode = hook_path.stat().st_mode + hook_path.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + print(f"已安装 {hook_path}") + if remote_url: + print(f"提交后将自动推送到 {args.remote}: {remote_url}") + else: + print(f"注意:当前尚未配置 remote '{args.remote}',请先设置 Gitea SSH 地址") + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except Exception as exc: + print(f"安装失败: {exc}", file=sys.stderr) + raise SystemExit(1)