Files
agent-radar/scripts/mirror_sync.sh
T
gitea-publish 87dc8f78fa fix: 总纲去除过时的「四个里」计数(YAML+文章源),mirror_sync 增 --blobless 与克隆重试加固
- data/projects/*.yaml:maka/openclaw/openwork 的 positioning 文案去掉样本计数比较级
  (扩到 7 样本后「四个里唯一…」已不成立或表述失真),改事实陈述
- articles/00-overview.md(id=98 源):同步 4 处,已重发线上覆盖
- scripts/mirror_sync.sh:新增 --blobless(--filter=blob:none 懒拉 blob,大仓可省 10x 传输);
  克隆重试 3→8 次且退避 10→80s,抗沙箱代理 502 抖动
2026-09-02 01:44:08 +08:00

466 lines
24 KiB
Bash
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 bash
# 把被追踪项目的源码镜像到 gitinbox 组织,供代码级追踪。
#
# 用法:
# bash scripts/mirror_sync.sh --id ipollowork
# bash scripts/mirror_sync.sh --id ipollowork --depth 200 # 超大仓只取近期历史
# bash scripts/mirror_sync.sh --id orca --blobless # 大仓懒拉 blob,只走源码快照
#
# 可见性由档案的 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=""
# auto = 体积超限时自动降级为源码快照;no = 始终尝试完整镜像(超限会 413 失败)。
# 这两个变量必须在 set -u 之前就有值:下面体积闸门的条件里会引用它们,
# 而这个分支只在特定体积下才走到——不初始化就成了「偶尔才炸」的雷。
SOURCE_ONLY="auto"
SOURCE_ONLY_FORCE="no"
# private → public 的显式授权开关。默认 0:公开不可逆,绝不自动放宽。
SET_PUBLIC="0"
EXCLUDE_COUNT="0"
# 跳过上游更新(本地裸仓已完整,只为重做快照时用)
NO_FETCH="0"
# blobless 克隆:--filter=blob:none,初始只拉 commits+trees,checkout 时按需懒拉 blob。
# 大仓(尤其 blob 占比高)可把传输量从整包几百 MB 砍到几十 MB,且具备"克隆成功即落盘、
# 之后懒拉 blob 失败可重跑续传"的容错;代价是只能走源码快照(partial clone 不能按 mirror 推送)。
BLOBLESS="0"
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;;
--no-source-only) SOURCE_ONLY="no"; shift;;
--set-public) SET_PUBLIC="1"; shift;;
--no-fetch) NO_FETCH="1"; shift;;
--blobless) BLOBLESS="1"; shift;;
*) 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
exc = m.get("excludes") or []
if exc:
# 目录切分许可:被排除的目录不进镜像,所以决定可见性的是**排除后**的内容。
# 这里必须显式读 redistributable_after_excludes,缺了就报错——
# 不能拿整体许可证去猜:openwork 整体是 L3(含 ee/),排除后是纯 MIT,
# 两种情况结论相反,猜错就是把受限内容公开出去,或者把能公开的白白锁起来。
ra = lic.get("redistributable_after_excludes")
if ra is None:
sys.stderr.write(
"档案声明了 mirror.excludes,但 business.license 里没有 "
"redistributable_after_excludes,无法判定排除后的可见性。\n"
"请在许可证块补上该字段(true=排除后可公开,false=仍须私有)。\n")
sys.exit(2)
vis = "public" if ra else "private"
else:
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 ''))}")
# 目录切分许可的排除项(如 OpenWork 的 ee/)。
# 必须读档案 mirror.excludes,不能读 registry.yaml 的 mirror_excludes:
# 后者只是批量抓取的提示,脚本从来没读过它,写在那里等于没写——
# openwork 的 visibility: public 曾因此建立在一条从未生效的规则上。
print("EXCLUDES=" + shlex.quote("\n".join(str(x) for x in (m.get("excludes") or []))))
PY
# shellcheck disable=SC1090
source "${CONF_FILE}"
# 换行分隔 → 数组。空输入要能正确得到零长数组,否则下面 ${#EXCLUDE_PATHS[@]}
# 在 set -u 下会因未绑定而炸(这类"偶尔才走到"的分支最容易埋雷)。
EXCLUDE_PATHS=()
if [[ -n "${EXCLUDES}" ]]; then
while IFS= read -r _line; do
[[ -n "${_line}" ]] && EXCLUDE_PATHS+=("${_line}")
done <<< "${EXCLUDES}"
fi
EXCLUDE_COUNT=${#EXCLUDE_PATHS[@]}
[[ -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
if [[ ${#EXCLUDE_PATHS[@]} -gt 0 ]]; then
echo " 排除路径 : ${EXCLUDE_PATHS[*]}"
echo " ⚠ 本项目的 public 可见性建立在「上述路径不进镜像」之上,推送前会二次校验。"
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
# ---- 放宽方向(private → public)推迟到推送成功之后做 ----
# 2026-09-02 的教训:这段原先放在同步之前,结果 openwork 翻成了 public
# 但推送失败(source-only 快照历史不连续被拒),
# 于是出现「仓库已公开 + 内容仍是旧快照」的窗口期——
# 虽然旧内容也不含 ee/(合法),但公开了不该公开的版本,正是最该避免的局面。
# 正确顺序:先推成功、内容对了,再翻可见性。任一步失败都停在 private,无害。
# 这里只做一次探测,真正的翻转在文件末尾 apply_visibility()。
if [[ "${VISIBILITY}" == "public" ]]; then
CUR_PRIVATE="$(curl -sS -u "${GITEA_USER}:${GITEA_TOKEN}" "${API}/repos/${ORG}/${REPO_NAME}" \
| /Users/figmarlite/.workbuddy/binaries/python/envs/default/bin/python -c 'import sys,json;print("true" if json.load(sys.stdin).get("private") else "false")' 2>/dev/null || echo unknown)"
fi
# ---- 同步 ----
MIRROR_DIR="${ROOT}/.mirrors/${REPO_NAME}.git"
mkdir -p "${ROOT}/.mirrors"
DEEPEN_STEP="${DEEPEN_STEP:-100}"
# 注意:不能用 `rev-parse --git-dir` 判断仓库是否有效。git clone 会先建好
# 目录骨架再下载 pack,被中断的半成品照样能通过 rev-parse,于是被当成
# "已有仓库"走 fetch 分支,报出 "CONNECT tunnel failed" 这类误导性错误。
# 真正的判据是"里面有没有提交"。
repo_ok() {
[[ -d "${MIRROR_DIR}" ]] || return 1
git -C "${MIRROR_DIR}" rev-parse --git-dir >/dev/null 2>&1 || return 1
local n
n="$(git -C "${MIRROR_DIR}" rev-list --count --all 2>/dev/null || echo 0)"
[[ "${n}" -gt 0 ]]
}
is_shallow() { [[ "$(git -C "${MIRROR_DIR}" rev-parse --is-shallow-repository 2>/dev/null || echo false)" == "true" ]]; }
# clone/fetch 失败时清掉半成品,避免下次误判(脚本有 set -e,失败会直接退出)
cleanup_broken() {
if [[ -d "${MIRROR_DIR}" ]] && ! repo_ok; then
echo " 清理未完成的半成品裸仓 ..."
rm -rf "${MIRROR_DIR}"
fi
}
trap cleanup_broken EXIT
if [[ -d "${MIRROR_DIR}" ]] && ! repo_ok; then
echo " 清理上次中断的半成品裸仓 ..."
rm -rf "${MIRROR_DIR}"
fi
if [[ "${NO_FETCH}" == "1" ]]; then
# 跳过上游更新,直接用本地裸仓生成快照。
# 2026-09-02 需要这个:本地裸仓已是完整的,只想重跑一次快照(改了排除项),
# 但 GitHub 恰好连不上("Empty reply from server"),脚本在 fetch 阶段就退出,
# 明明有数据却什么都做不了——上游不可用时,不该阻止我们用已有内容重做快照。
if repo_ok; then
echo " 跳过上游更新(--no-fetch),直接用本地裸仓生成快照"
else
echo "✗ --no-fetch 但本地裸仓不可用:${MIRROR_DIR}" >&2; exit 1
fi
elif repo_ok; then
if is_shallow && [[ -z "${DEPTH}" ]]; then
# 慢链路续传:分批 --deepen。每批成功即落盘,中断后重跑本脚本可继续,
# 不像 `git clone` 那样失败一次全部作废。
echo " 浅仓分批续传(每批 --deepen=${DEEPEN_STEP},中断可重跑)..."
while is_shallow; do
PRE="$(git -C "${MIRROR_DIR}" rev-list --count --all 2>/dev/null || echo 0)"
echo " 已有 ${PRE} 条历史,再加深 ${DEEPEN_STEP} ..."
if ! git -C "${MIRROR_DIR}" fetch --deepen="${DEEPEN_STEP}" origin; then
echo " ⚠ 本批中断;已落盘的 ${PRE} 条历史保留,重跑本脚本继续。"
break
fi
POST="$(git -C "${MIRROR_DIR}" rev-list --count --all 2>/dev/null || echo 0)"
[[ "${POST}" == "${PRE}" ]] && { echo " 已补齐完整历史"; break; }
done
elif is_shallow && [[ -n "${DEPTH}" ]]; then
echo " 浅仓加深到 depth=${DEPTH} ..."
git -C "${MIRROR_DIR}" fetch --depth="${DEPTH}" origin || \
echo " ⚠ 加深中断,已保留现有深度,重跑可继续"
else
echo " 更新已有裸仓 ..."
git -C "${MIRROR_DIR}" remote update --prune
fi
else
# 走沙箱代理时偶发 `CONNECT tunnel failed, response 502`,且 git clone
# 失败不保留已下载内容,所以首次克隆必须带重试。
CLONE_ARGS=(clone --mirror)
[[ -n "${DEPTH}" ]] && CLONE_ARGS+=(--depth "${DEPTH}")
[[ "${BLOBLESS}" == "1" ]] && CLONE_ARGS+=(--filter=blob:none)
if [[ -n "${DEPTH}" ]]; then
echo " 首次镜像(--depth ${DEPTH},只取近期历史)..."
else
echo " 首次全量镜像(大仓请耐心)..."
fi
# 沙箱代理对 GitHub 偶发 502(CONNECT tunnel failed),且大仓传输动辄几小时,
# 一次 502 就可能断在半途。所以克隆重试要够多、退避够长:8 次、10→80s 递增。
# blobless 模式下"克隆成功即落盘"还能在 checkout 阶段继续容错(见下)。
for attempt in $(seq 1 8); do
if git "${CLONE_ARGS[@]}" "${UPSTREAM}" "${MIRROR_DIR}"; then
break
fi
echo " ⚠ 第 ${attempt} 次克隆失败,$(( attempt * 10 ))s 后重试 ..."
rm -rf "${MIRROR_DIR}"
sleep $(( attempt * 10 ))
[[ ${attempt} -eq 8 ]] && { echo "克隆八次均失败,请检查网络/代理" >&2; exit 1; }
done
fi
# ---- 浅仓处理:服务端拒绝 shallow update,必须先补齐历史 ----
# Git 服务端默认 `receive.shallowUpdate = deny`,推送浅仓会被拒:
# ! [remote rejected] main -> main (shallow update not allowed)
# 而 --depth 是本脚本自己提供的选项,推不上去等于自相矛盾,所以在这里兜住:
# 先 --unshallow 补齐(增量下载,通常只比浅仓多几十 MB),失败再降级为快照。
UNSHALLOWED="no"
if is_shallow; then
echo " 浅仓无法直接推送(服务端拒绝 shallow update),先补齐完整历史 ..."
for attempt in 1 2 3; do
if git -C "${MIRROR_DIR}" fetch --unshallow origin 2>/dev/null; then
UNSHALLOWED="yes"; break
fi
echo " ⚠ 第 ${attempt} 次 unshallow 失败,10s 后重试 ..."
sleep 10
done
if [[ "${UNSHALLOWED}" == "yes" ]]; then
echo " 已补齐完整历史"
else
# 网络或体积原因补不齐 —— 退回快照模式,丢历史但至少把代码留下来。
echo " ⚠ 补齐失败,将降级为【源码快照】(会丢失提交历史)"
SOURCE_ONLY_FORCE="yes"
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}")"
# ---- 体积闸门:Gitea 前置 nginx 有 client_max_body_size 上限 ----
# 实测 gitea.apescale.com:48 MB 放行、64 MB 拒绝(推算为 50m,宝塔常见默认值)。
# 超限推送会在传完全部对象后才收到 HTTP 413,白白浪费几十分钟下载。
# 所以推送前先量体积,超限则自动降级为「源码快照」:剔除图片/音视频/字体等
# 非源码素材——这些内容通常占一半以上体积,却不是"跟踪源码"要看的东西。
MAX_PUSH_MB="${MAX_PUSH_MB:-45}"
PACK_KB="$(git -C "${MIRROR_DIR}" count-objects -v | awk '/^size-pack:/{print $2}')"
PACK_MB=$(( PACK_KB / 1024 ))
# blobless 镜像只能走源码快照:partial clone 不能按 mirror 推送。
[[ "${BLOBLESS}" == "1" ]] && SOURCE_ONLY_FORCE="yes"
MIRROR_MODE="full"
echo " 本地裸仓打包体积: ${PACK_MB} MB(推送上限 ${MAX_PUSH_MB} MB)"
if [[ ( "${PACK_MB}" -gt "${MAX_PUSH_MB}" || "${SOURCE_ONLY_FORCE}" == "yes" ) \
&& "${SOURCE_ONLY}" != "no" ]]; then
MIRROR_MODE="source-only"
if [[ "${SOURCE_ONLY_FORCE}" == "yes" ]]; then
echo " ⚠ 降级为【源码快照】:提交历史不完整,按快照提交(仍会剔除素材)"
else
echo " ⚠ 超出上限,降级为【源码快照】:剔除非源码素材后推送"
fi
SNAP="$(mktemp -d)"
# shellcheck disable=SC2064
trap "cleanup_broken; rm -rf '${SNAP}'" EXIT
git init -q "${SNAP}/repo"
git --git-dir="${MIRROR_DIR}" --work-tree="${SNAP}/repo" checkout HEAD -- . 2>/dev/null
# 目录切分许可的排除在这里落地:工作树 checkout 完之后整棵删掉。
# 注意必须在资产剔除**之前**做——ee/ 这类目录里也可能有图片/字体,
# 顺序反了会留下空目录和残留文件。
if [[ ${#EXCLUDE_PATHS[@]} -gt 0 ]]; then
for _exc in "${EXCLUDE_PATHS[@]}"; do
if [[ -e "${SNAP}/repo/${_exc}" ]]; then
rm -rf "${SNAP}/repo/${_exc}"
echo " 已排除 ${_exc}"
else
# 排除了个不存在的路径,多半是上游改了目录结构。
# 不静默放过——否则哪天 ee/ 挪了窝,我们还在按 public 推。
echo " ⚠ 声明要排除的 ${_exc} 在上游不存在,请核对档案的 excludes 是否过期"
fi
done
fi
find "${SNAP}/repo" -type f -not -path '*/.git/*' \( \
-iname '*.png' -o -iname '*.gif' -o -iname '*.jpg' -o -iname '*.jpeg' \
-o -iname '*.webp' -o -iname '*.ico' -o -iname '*.mp4' -o -iname '*.mov' \
-o -iname '*.webm' -o -iname '*.wav' -o -iname '*.mp3' -o -iname '*.glb' \
-o -iname '*.ttf' -o -iname '*.woff' -o -iname '*.woff2' -o -iname '*.eot' \
-o -iname '*.pdf' -o -iname '*.zip' \) -delete
cat > "${SNAP}/repo/MIRROR-NOTE.md" <<NOTE
# 镜像说明
- 上游:${UPSTREAM}
- 许可证:${LIC_NAME}(${LIC_LEVEL})
- 抓取日期:$(date +%F)
## 这不是完整保真镜像
本仓库只含**源码与文档**,已剔除图片、音视频、字体等非源码素材。
$(if [[ ${#EXCLUDE_PATHS[@]} -gt 0 ]]; then
printf '此外按许可证要求排除了以下目录:%s。\n' "${EXCLUDE_PATHS[*]}"
printf '本仓库的公开可见性以该排除为前提;完整上游含受限许可内容。\n'
fi)
原因:Gitea 前置 nginx 的 \`client_max_body_size\` 上限约 ${MAX_PUSH_MB} MB,
完整裸仓打包 ${PACK_MB} MB 会被 HTTP 413 拒绝。
完整保真快照保留在追踪体系的本地裸仓 \`.mirrors/${REPO_NAME}.git\` 中。
把服务器 nginx 放开到 \`client_max_body_size 512m;\` 后即可推送完整版。
NOTE
PUSH_DIR="${SNAP}/repo"
( cd "${PUSH_DIR}" && git add -A \
&& git -c user.name='agent-radar' -c user.email='radar@local' \
commit -q -m "源码快照 @ $(date +%F) (source-only, 剔除非源码素材)" )
# 必须先 repack 再量:新 commit 出来的都是 loose object,
# size-pack 为 0,会打印出「源码快照体积: 0 MB」这种误导性输出。
# repack 顺带也把推送体积压下去(实测 iPolloWork:散装 57 MB → 打包 14 MB)。
git -C "${PUSH_DIR}" repack -a -d -q --window=50 --depth=50 2>/dev/null || true
SNAP_MB=$(( $(git -C "${PUSH_DIR}" count-objects -v | awk '/^size-pack:/{print $2}') / 1024 ))
echo " 源码快照体积: ${SNAP_MB} MB"
else
PUSH_DIR="${MIRROR_DIR}"
fi
# ---- 推送前硬校验:排除项绝不能进公开镜像 ----
# 这是整套许可证分级的最后一道闸。前面所有环节(声明、checkout 后删除)都可能有
# 遗漏或顺序问题,而"public"这个动作的代价是不可逆的——东西一旦公开就收不回来。
# 所以这里不看过程只看结果:直接检查待推送目录里还有没有这些路径。
if [[ "${VISIBILITY}" == "public" && ${#EXCLUDE_PATHS[@]} -gt 0 ]]; then
if [[ "${MIRROR_MODE}" == "full" ]]; then
# full 模式直接拒:push --mirror 推的是完整裸仓,剔除目录需要重写历史
# (git filter-repo),本脚本不做。更关键的是——裸仓没有工作树,
# 文件系统层面的检查会**假通过**,而 ee/ 的内容在历史里照样推得出去。
# 这种"检查了但没真检查到"比不检查更危险,所以这里不看结果直接拦。
echo "" >&2
echo "✗ 拒绝以 full 模式推送到公开仓库:${EXCLUDE_PATHS[*]}" >&2
echo " full 模式用 push --mirror 推完整裸仓,无法在此层面剔除受限目录," >&2
echo " 且历史提交里仍含该内容。请二选一:" >&2
echo " a) 把档案 mirror.visibility 改为 private;" >&2
echo " b) 让体积超过闸门(MAX_PUSH_MB=${MAX_PUSH_MB})走 source-only 路径,排除在该路径生效。" >&2
exit 1
fi
# source-only:有真实工作树,可以做结果校验。
# 不看"我们有没有执行删除",只看"待推送内容里到底还有没有"——
# 过程可能被改坏,结果不会说谎。
LEAK=()
for _exc in "${EXCLUDE_PATHS[@]}"; do
_base="${_exc%/}"
[[ -e "${PUSH_DIR}/${_base}" ]] && LEAK+=("${_base}")
done
if [[ ${#LEAK[@]} -gt 0 ]]; then
echo "" >&2
echo "✗ 拒绝推送:以下受限目录仍在待推送内容中:${LEAK[*]}" >&2
echo " 目标仓库是 **public**,而其中含受限许可内容。" >&2
exit 1
fi
echo " 排除校验通过:${EXCLUDE_PATHS[*]} 均不存在于待推送内容"
fi
echo " 推送到 ${ORG}/${REPO_NAME} ..."
if [[ "${MIRROR_MODE}" == "full" ]]; then
git -C "${PUSH_DIR}" push --mirror "${PROTO}://${U_ENC}:${P_ENC}@${HOST}/${ORG}/${REPO_NAME}.git"
else
# source-only 必须强推:每次快照都是「checkout → git init → 单次 commit」,
# 与远端上一次的快照 commit 毫无血缘关系,永远不可能 fast-forward。
# 2026-09-02 踩到:改了排除项后重跑,推送被拒
# "! [rejected] HEAD -> main (fetch first)"
# 但排除校验明明通过了——不是排除没生效,是历史不连续。
# 强推在这里语义正确:source-only 镜像保的是**当前源码状态**,不是历史链。
# 若哪天需要保留历史,该做的是增量快照而非回滚这个 force。
echo " (source-only 快照,强推覆盖:历史不连续是设计使然)"
git -C "${PUSH_DIR}" push --force "${PROTO}://${U_ENC}:${P_ENC}@${HOST}/${ORG}/${REPO_NAME}.git" HEAD:refs/heads/main
fi
# ---- 可见性放宽:走到这里说明内容已经推对了,才允许公开 ----
# 见上方 2026-09-02 的说明:翻转必须发生在成功推送之后。
# 收紧方向(private)在同步前就做了,因为锁起来永远安全、早做没代价。
if [[ "${VISIBILITY}" == "public" && "${CUR_PRIVATE}" == "true" ]]; then
if [[ "${SET_PUBLIC}" == "1" ]]; then
echo " ⚠ 内容已推送且排除校验通过 → 将 ${ORG}/${REPO_NAME} 翻为 public(不可逆)"
curl -sS -o /dev/null -u "${GITEA_USER}:${GITEA_TOKEN}" -X PATCH \
-H 'Content-Type: application/json' -d '{"private":false}' "${API}/repos/${ORG}/${REPO_NAME}" \
&& echo " 已翻为 public"
else
echo ""
echo " ⚠ ${ORG}/${REPO_NAME} 判定应为 public,但远端当前是 private。"
echo " 内容已推送且排除校验通过;公开不可逆,确认后重跑并加 --set-public。"
echo " 判定依据:许可证 ${LIC_LEVEL},排除 ${EXCLUDE_COUNT} 项后 redistributable=public"
fi
fi
# ---- 回写档案 ----
/Users/figmarlite/.workbuddy/binaries/python/envs/default/bin/python - "${YAML}" "${VISIBILITY}" "${REPO_NAME}" "${MIRROR_MODE}" <<'PY'
import sys
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(),
"mode": sys.argv[4],
})
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} (模式 ${MIRROR_MODE},本地裸仓 ${SIZE})"