#!/usr/bin/env bash # 把被追踪项目的源码镜像到 gitinbox 组织,供代码级追踪。 # # 用法: # bash scripts/mirror_sync.sh --id ipollowork # bash scripts/mirror_sync.sh --id ipollowork --depth 200 # 超大仓只取近期历史 # # 可见性由档案的 business.license.redistributable 决定(L1/L2 → public,L3 → private)。 # 这条规则不能手工绕过:把 source-available 项目公开镜像,等于替对方做了一次 # 未经授权的公开分发。 set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" GITEA_URL="${GITEA_URL:-https://gitea.apescale.com}" DISPLAY_URL="${GITEA_DISPLAY_URL:-https://gitea.apescale.com}" ORG="${GITEA_ORG:-gitinbox}" ID="" DEPTH="" while [[ $# -gt 0 ]]; do case "$1" in --id) ID="$2"; shift 2;; --org) ORG="$2"; shift 2;; --url) GITEA_URL="$2"; shift 2;; --depth) DEPTH="$2"; shift 2;; *) echo "未知参数: $1" >&2; exit 1;; esac done [[ -z "$ID" ]] && { echo "缺少 --id" >&2; exit 1; } YAML="$ROOT/data/projects/$ID.yaml" [[ -f "$YAML" ]] || { echo "档案不存在: $YAML" >&2; exit 1; } # ---- 从档案读取镜像配置(bash 不适合解析 YAML,借用 python 输出 shell 赋值) ---- # 用"写临时文件再 source"而不是 eval "$(...)": # eval 会二次解析内容,YAML 里的括号/引号一旦漏转义就会变成难定位的 # syntax error,而且报错行号会偏移到完全无关的位置。 CONF_FILE="$(mktemp)" trap 'rm -f "$CONF_FILE"' EXIT /Users/figmarlite/.workbuddy/binaries/python/envs/default/bin/python - "$YAML" > "$CONF_FILE" <<'PY' import sys, shlex from pathlib import Path sys.path.insert(0, str(Path(sys.argv[1]).resolve().parent.parent.parent / "scripts")) from _common import load_yaml d = load_yaml(Path(sys.argv[1])) m = d.get("mirror") or {} lic = (d.get("business") or {}).get("license") or {} print(f"UPSTREAM={shlex.quote(str(m.get('upstream') or ''))}") print(f"REPO_NAME={shlex.quote(str(m.get('gitea_repo') or '').split('/')[-1] or d.get('id',''))}") # 可见性以许可证判定为准,不信任档案里手写的 visibility vis = "public" if lic.get("redistributable") else "private" print(f"VISIBILITY={shlex.quote(vis)}") print(f"LIC_LEVEL={shlex.quote(str(lic.get('openness') or '?'))}") print(f"LIC_NAME={shlex.quote(str(lic.get('name') or ''))}") PY # shellcheck disable=SC1090 source "$CONF_FILE" [[ -z "$UPSTREAM" ]] && { echo "档案缺少 mirror.upstream" >&2; exit 1; } echo "==> 镜像 $ID" echo " 上游 : $UPSTREAM" echo " 目标 : $ORG/$REPO_NAME" echo " 许可证 : $LIC_LEVEL ($LIC_NAME)" echo " 可见性 : $VISIBILITY" if [[ "$VISIBILITY" == "private" ]]; then echo " ⚠ L3 级 source-available,已强制私有。公开镜像可能构成未经授权的分发。" fi # ---- 凭据:优先环境变量,回退到 macOS 钥匙串 ---- if [[ -z "${GITEA_TOKEN:-}" ]]; then CRED="$(printf "host=%s\nprotocol=https\n" "${DISPLAY_URL#*://}" | git credential-osxkeychain get 2>/dev/null || true)" GITEA_USER="${GITEA_USER:-$(printf '%s' "$CRED" | sed -n 's/^username=//p')}" GITEA_TOKEN="$(printf '%s' "$CRED" | sed -n 's/^password=//p')" fi [[ -z "${GITEA_TOKEN:-}" || -z "${GITEA_USER:-}" ]] && { echo "未拿到 Gitea 凭据(设 GITEA_TOKEN/GITEA_USER,或存入钥匙串)" >&2; exit 1; } # ---- 确保目标仓库存在(组织端点,不是 /user/repos) ---- API="$GITEA_URL/api/v1" HTTP="$(curl -sS -o /dev/null -w '%{http_code}' -u "$GITEA_USER:$GITEA_TOKEN" "$API/repos/$ORG/$REPO_NAME" || echo 000)" if [[ "$HTTP" == "200" ]]; then echo " 仓库已存在" elif [[ "$HTTP" == "404" ]]; then echo " 创建仓库 $ORG/$REPO_NAME (private=$([[ $VISIBILITY == private ]] && echo true || echo false)) ..." CREATE="$(curl -sS -o /tmp/gitea_mirror_create.json -w '%{http_code}' -u "$GITEA_USER:$GITEA_TOKEN" \ -H 'Content-Type: application/json' -X POST "$API/org/$ORG/repos" \ -d "{\"name\":\"$REPO_NAME\",\"private\":$([[ $VISIBILITY == private ]] && echo true || echo false),\"auto_init\":false,\"description\":\"Mirror of $UPSTREAM (license: $LIC_NAME)\"}")" [[ "$CREATE" =~ ^(200|201)$ ]] || { echo "创建失败 (HTTP $CREATE):" >&2; cat /tmp/gitea_mirror_create.json >&2; exit 1; } echo " 已创建" else echo "查询仓库失败 (HTTP $HTTP)" >&2; exit 1 fi # 已存在的仓库若应为私有,显式修正(创建时的 private 对已存在仓库不生效) if [[ "$VISIBILITY" == "private" ]]; then curl -sS -o /dev/null -u "$GITEA_USER:$GITEA_TOKEN" -X PATCH \ -H 'Content-Type: application/json' -d '{"private":true}' "$API/repos/$ORG/$REPO_NAME" || true fi # ---- 同步 ---- MIRROR_DIR="$ROOT/.mirrors/$REPO_NAME.git" mkdir -p "$ROOT/.mirrors" if [[ -d "$MIRROR_DIR" ]]; then echo " 更新已有裸仓 ..." git -C "$MIRROR_DIR" remote update --prune else echo " 首次全量镜像(大仓请耐心)..." if [[ -n "$DEPTH" ]]; then git clone --mirror --depth "$DEPTH" "$UPSTREAM" "$MIRROR_DIR" else git clone --mirror "$UPSTREAM" "$MIRROR_DIR" fi fi PROTO="${GITEA_URL%%://*}" HOST="${GITEA_URL#*//}" U_ENC="$(/Users/figmarlite/.workbuddy/binaries/python/envs/default/bin/python -c \ 'import sys,urllib.parse;print(urllib.parse.quote(sys.argv[1],safe=""))' "$GITEA_USER")" P_ENC="$(/Users/figmarlite/.workbuddy/binaries/python/envs/default/bin/python -c \ 'import sys,urllib.parse;print(urllib.parse.quote(sys.argv[1],safe=""))' "$GITEA_TOKEN")" echo " 推送到 $ORG/$REPO_NAME ..." git -C "$MIRROR_DIR" push --mirror "$PROTO://$U_ENC:$P_ENC@$HOST/$ORG/$REPO_NAME.git" # ---- 回写档案 ---- /Users/figmarlite/.workbuddy/binaries/python/envs/default/bin/python - "$YAML" "$VISIBILITY" "$REPO_NAME" <<'PY' import sys from datetime import datetime from pathlib import Path sys.path.insert(0, str(Path(sys.argv[1]).resolve().parent.parent.parent / "scripts")) from _common import today, update_block p = Path(sys.argv[1]) text = p.read_text(encoding="utf-8") text = update_block(text, "mirror", { "visibility": sys.argv[2], "gitea_repo": f"{'gitinbox'}/{sys.argv[3]}", "last_sync": today(), }) p.write_text(text, encoding="utf-8") PY SIZE="$(du -sh "$MIRROR_DIR" 2>/dev/null | cut -f1 || echo '?')" echo "==> 完成。$DISPLAY_URL/$ORG/$REPO_NAME (本地裸仓 $SIZE)"