Files
agent-radar/scripts/mirror_sync.sh
T
gitea-publish 56e2c8a403 fix: 排除规则漏了根目录的 EE 许可证引用文件;脚本补 public 方向修正
修一个会误导读者的排除漏洞:
只排除 ee/ 时,LICENSES/LicenseRef-OpenWork-EE.txt 仍会被推进镜像。
它是 REUSE 规范的引用文件,存在的唯一意义是给 ee/ 下的文件做 SPDX 引用
(ee/ 里写着 "SPDX-License-Identifier: LicenseRef-OpenWork-EE")。
ee/ 一删,它就成了孤儿。许可证文本公開本身不侵权(上游自己就公开),
但一个不含 EE 代码的仓库里躺着 EE 许可证,读者只会以为我们漏排了。

许可证块补 redistributable_after_excludes: true。
依据:根 LICENSE 原文(已读远端全文)——
  "All content that resides under the /ee directory ... OpenWork EE License"
  "Content outside ... is available under the 'MIT' license"
排除 ee/ 后为纯 MIT。

mirror_sync.sh 补 private → public 方向修正,但默认不自动翻:
原先脚本只有「收紧」方向(应为私有就 PATCH private:true),
没有放宽方向——openwork 判定为 public 但仓库停在 private,
重跑多少次都不会翻过来,档案与事实不一致却看不出原因。
现在会报告「应为 public 但当前 private」,要求显式加 --set-public。
公开不可逆,放松方向永远不该自动做。

一句话结论去掉样本计数:
maka「四个里唯一」、QwenPaw/Tutti「七个里…」——每加一批样本就过时一次。
改成不带计数的表述,「已收录样本中」本就是隐含的。

总纲 id=98 已重发。
2026-09-02 00:26:49 +08:00

427 lines
21 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 # 超大仓只取近期历史
#
# 可见性由档案的 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"
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;;
*) 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)必须显式授权 ----
# 公开是不可逆动作:一旦有人 fork/克隆,就再也收不回来。
# 所以这里**不自动翻**——只报告,要求显式加 --set-public。
# 2026-09-02 补:原先脚本只有"收紧"方向的修正,openwork 判定为 public 但仓库
# 停在 private,重跑多少次都不会翻过来,档案与事实不一致却看不出原因。
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)"
if [[ "${CUR_PRIVATE}" == "true" ]]; then
if [[ "${SET_PUBLIC}" == "1" ]]; then
echo " ⚠ 按 --set-public 将 ${ORG}/${REPO_NAME} 从 private 翻为 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=${VISIBILITY}"
fi
fi
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 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}")
if [[ -n "${DEPTH}" ]]; then
echo " 首次镜像(--depth ${DEPTH},只取近期历史)..."
else
echo " 首次全量镜像(大仓请耐心)..."
fi
for attempt in 1 2 3; do
if git "${CLONE_ARGS[@]}" "${UPSTREAM}" "${MIRROR_DIR}"; then
break
fi
echo " ⚠ 第 ${attempt} 次克隆失败,8s 后重试 ..."
rm -rf "${MIRROR_DIR}"
sleep 8
[[ ${attempt} -eq 3 ]] && { 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 ))
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
git -C "${PUSH_DIR}" push "${PROTO}://${U_ENC}:${P_ENC}@${HOST}/${ORG}/${REPO_NAME}.git" HEAD:refs/heads/main
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})"