仓库镜像运维规范 (repo-mirror-ops)
本技能用于指导全舰队智能体与工程师进行仓库镜像同步(source of truth → public mirror)的治理,核心场景为 gitea.capitaltrain.cn(源)→ cnb.cool(镜像)。
关键词:mirror、镜像、refspec、--all、non-fast-forward、force、ls-remote。
一、核心模型:单向镜像,不是双向同步
| 位置 | 角色 | 语义 |
|---|---|---|
| 上游(gitea) | 源(source of truth) | 唯一的开发真相源,所有提交必须先落这里 |
| 下游(CNB) | 镜像(mirror) | 对外公开,只读式复制 |
这是单向不可逆的复制关系,不是"两边互相同步"。镜像侧产生的一切变更都是分叉,必须回流到源。
> 命名约定:叫 "mirror" 而不是 "sync"。sync 暗示双向,是错的。
二、三条铁律(不可协商)
铁律 1 · 永不--force
包括 --force 与 --force-with-lease,任何情况下都不许用。
理由:镜像一旦被强推覆盖,等于把源的历史截断。而源才是真相源 —— 覆盖镜像毫无收益、只有风险。
铁律 2 · 推送前必须体检,不许盲推
原因是一个不对称性:
| 异常 | git push 的反应 |
|---|---|
| 分支漏推 | 静默成功,exit 0 + Everything up-to-date |
| 非快进冲突 | 报错,exit 1 |
push 对"漏分支"完全沉默。 所以"跑完 push 看退出码"不是有效验证,必须先跑 scripts/mirror-audit.sh 做前置体检。
铁律 3 · 禁止在镜像侧直接提交
镜像侧产生的提交会让两端发散,制造 C 类状态。正确做法:先在源提交,镜像由推送生成。
三、⛔ 禁用 push --all
这是本技能最重要的一条,也是历史上最容易踩的坑。
3.1 缺陷
git push <remote> --all 的语义是"把本地分支逐个展开成 refspec 推送"。它有两个致命缺陷:
| 缺陷 | 后果 |
|---|---|
| 只扫本地分支 | 源上存在、但本地只有 remote-tracking 引用(无本地分支)的 ref 永远推不上去 |
| 不报错 | 退出码 0、输出 Everything up-to-date,看起来完全正常 |
3.2 实测证据(2026-10-09 · key-agent)
gitea 有 6 个分支,CNB 镜像上只有 3 个。缺失的 4 个在本地连本地分支都不存在:
auth/userinfo-cache-spof 8f1f455 ← 从未同步
feature/retire-picooraclaw-config d2aeb53 ← 从未同步
ferry-status c2c4e81 ← 从未同步
roadmap/guest-host-split ef6e095 ← 从未同步
自动化按 push --all 连跑 12 次,每次都报"成功",漏分支数始终为 4。
3.3 术语澄清
--all 的左端需要能解析为本地已存在的 ref。origin/foo 是 remote-tracking 引用,不是本地分支,--all 不认它。
3.4 正确写法:显式通配 refspec
git push <remote> 'refs/heads/*:refs/heads/*' # 所有分支一对一推送
git push <remote> 'refs/tags/*:refs/tags/*' # 所有标签
- 幂等:已同步的返回
Everything up-to-date,无副作用,可放心挂定时任务 - 覆盖完整:通配符作用于远端 ref 空间,不受本地分支状态限制
四、ABCD 四类状态判定
每个 ref 只可能落在 4 类。只有 B 类允许自动处置。
| 类 | 判定条件 | 处置 | 能否自动 |
|---|---|---|---|
| A | 源与镜像指向同一提交 | 无需动作 | — |
| B | 源是镜像的祖先(镜像落后) | 直接推,快进 | ✅ 可 |
| C | 镜像是源的祖先(镜像领先) | 回流到源,勿 force | ⚠️ 需人工确认 |
| D | 两端互不为祖先(发散) | 人工合并决策 | 🛑 必须人工 |
判定命令:
git merge-base --is-ancestor origin/<branch> <remote>/<branch> # 真 → B 类
git merge-base --is-ancestor <remote>/<branch> origin/<branch> # 真 → C 类
# 两者皆假 → D 类
4.1 C 类标准解法(全程无 force)
核心洞察:只要源仍是镜像的祖先,回流就永远是 fast-forward。
# 前提:已确认 origin/main 是 cnb/main 的祖先
git push origin <cnb侧领先提交SHA>:main # 回流,快进
git push cnb 'refs/heads/*:refs/heads/*' # 此时源已最新,重推即成功
refspec 左端可以接受任意"可解析为提交的对象"(commit SHA、HEAD、remote ref),不必须是本地分支 —— 这正是回流能work 的原理。
> 先查祖先关系,再决定动作。不要默认 force。 历史上 12 次因main 推送被拒而卡死,全部是 C 类,本可免force 解决。
五、⚠️ 查ref 的正确口径
必须用 git ls-remote,禁用 git rev-parse <remote>/<ref>。
后者因refspec 解析方式给出误导性结果。实测案例:曾据此判定"cnb 上缺少 tag codewiki-bootstrap",而 git ls-remote 显示该 tag 一直在 cnb 上。连续 12 次错误诊断的根源。
# ✅ 权威:直接问远端
git ls-remote --heads <remote>
git ls-remote --tags <remote>
# ⚠️ 误导:本地 refspec 解析
git rev-parse <remote>/<branch> # 可能为 nil,但远端其实存在
六、源侧 URL 漂移检查
裸配置的 remote URL 可能已失效(仓库迁移后gitea 返回 301 重定向),会导致后续所有报错被误判为权限/网络问题。
git remote get-url origin # 本地配置值
git ls-remote --get-url origin # 跟随重定向后的真实 URL
两者不一致 → 先更新 remote URL,再谈同步。
> 实测:kunpengzhi-podcast 的 origin 已 301 重定向到 media/studio-podcast-script(仓库迁移过),本地配置是旧路径。
七、覆盖度校验(防假成功)
同步后必须核对,不能只看退出码。输出必须为空:
comm -23 \
<(git ls-remote --heads origin | awk '{print $2}' | sed 's#refs/heads/##' | sort) \
<(git ls-remote --heads <remote> | awk '{print $2}' | sed 's#refs/heads/##' | sort)
# ↑ 源有、镜像没有的分支。非空 = 漏了。
八、标准 workflow
# 0. 前置体检(铁律 2)—— 唯一有效的验证手段
./scripts/mirror-audit.sh <repo-dir>
# 1. 双向 fetch --prune
git fetch origin --prune
git fetch <remote> --prune
# 2. 推送(显式 refspec,非--all)
git push <remote> 'refs/heads/*:refs/heads/*'
git push <remote> 'refs/tags/*:refs/tags/*'
# 3. 覆盖度校验(§7),输出必须为空
体检脚本退出码:0= 可自动同步(B/A 类)|2 = 存在 C 类需回流 |3 = 存在 D 类必须人工合并。
九、新仓库接入检查表
- [ ]
originURL 与源实际路径一致(无 301 漂移,用ls-remote --get-url核验) - [ ] 镜像remote 已配置,凭据内嵌于
.git/config - [ ] 跑过体检,ABCD 四类状态已分类
- [ ] D 类(发散)已人工处置完毕
- [ ] 推送命令为显式 refspec,非
--all - [ ] 定时任务已接入前置体检
- [ ] 同步后覆盖度校验输出为空
十、汇报口径
| 字段 | 内容 |
|---|---|
| 体检结果 | A/B/C/D 各类 ref 数量 |
| 结果 | 成功(exit 0)/ 失败(附逐字原始报错) |
| 覆盖度校验 | 必须明确"输出为空"或列出漏掉的 ref |
| force 使用 | 恒为"未使用" |
失败时逐字粘贴原始报错(含被拒绝的 ref、hint),不要转述、不要自动解决冲突。
相关
- 完整推导与实测记录 → gitea
WikiFlow/docs/07-镜像同步范式.md§6「镜像同步管理基线」 - Gitea 日常操作 →
internal/gitea-tea - Gitea 项目看板 →
internal/gitea-project-ops