# ERR-20260512-002: Force-push 共享分支后，所有 mirror checkout（UAT/PROD 服务器）部署时 git pull 报 divergent branches

## 症状

FF-only 政策启用日（2026-05-12）force-push staging/production 到共同祖先后，下一次 `push staging` 触发的 `deploy-uat.yml` 在 UAT 服务器上 `git pull` 失败：

```
[FFAI]  * branch              staging    -> FETCH_HEAD
[FFAI]    115f4398..d95fcfd7  staging    -> origin/staging   ← FETCH 范围看起来是 FF，没问题
[FFAI] hint: You have divergent branches and need to specify how to reconcile them.
[FFAI] fatal: Need to specify how to reconcile divergent branches.
##[error]Process completed with exit code 128.
```

`deploy.sh` 没传 `--ff-only` / `--rebase` / `--no-ff`，git 2.27+ 默认拒绝 divergent pull，exit 128。

## 根因

Force-push 改写了 staging 的历史 SHA（`0d9b66c8` → `d5eddeae` → ... → `d95fcfd7`）。

UAT 服务器上 `/srv/apps/ffworkspace-test` 本地 staging HEAD 仍指向旧 SHA `0d9b66c8`（force-push 前的最后一次正常 deploy 拉到的）。新旧两条历史**工作树等价但 commit DAG 上没有 ancestor 关系**——`0d9b66c8` 不是 `d95fcfd7` 的祖先，反过来也不是。git pull 看到本地 ref 跟远端 ref 既不能 FF 也不在同一线上，直接报 divergent。

注意 `git fetch` 那一行看起来是 FF（`115f4398..d95fcfd7`）——那是 **`origin/staging` tracking ref 的更新范围**（远端跟踪 ref 从旧远端值跟到新远端值），跟"本地 HEAD vs 新远端 HEAD 能不能 FF"是两回事。容易被这个 FETCH 行误导。

## 关键发现

**任何 force-push 一条共享分支后，所有持续 mirror 那条分支的 checkout 都会撞同一个雷**：

- UAT 部署服务器（每次 deploy 跑 git pull）
- Production 部署服务器（同上）
- CI runner 上的 cache checkout（看具体 actions/checkout 配置，通常 fresh clone 不受影响）
- 任何团队成员本地未 fetch 过的 working copy

force-push 当时只对操作者本地干净，**镜像方都是潜在受害者**。

## 修复（每个 mirror 跑一次）

```bash
ssh <user>@<host> 'cd /srv/apps/<repo> && \
  git fetch origin <branch> && \
  git reset --hard origin/<branch>'
```

`reset --hard` 不动 untracked 文件（`.env.<env>.bak.*` / 本地 build 产物如 `.next/`），安全。本地 tracked 文件的"deploy 期间产生的脏改动"（如 `frontend/tsconfig.json` 被 deploy 脚本临时改）会被丢弃——这是预期的，mirror 部署机不应该有"需要保留的本地改动"。

## 本次修复记录（2026-05-12）

| 服务器 | 路径 | 修复前 HEAD | 修复后 HEAD |
|--------|------|------------|------------|
| FFAI UAT (43.153.69.73) | /srv/apps/ffworkspace-test | 0d9b66c8 | d95fcfd7 ✓ |
| AIxC UAT (52.234.29.56) | /srv/apps/aixcworkspace | 0d9b66c8 | d95fcfd7 ✓ |
| FFAI PROD (43.130.6.44) | /srv/apps/ffworkspace | 77d1efb4 | d95fcfd7 ✓ |
| AIxC PROD (23.101.202.65) | /srv/apps/aixcworkspace | 77d1efb4 | d95fcfd7 ✓ |

production 走 `git fetch + git reset --hard origin/production`，不触发 pm2 reload / docker 重启（只动 git 指针和工作树），生产业务零中断。`reset --hard` 后下次正常 push production 才会跑完整 deploy。

## 防御措施

**已做**：

1. `scripts/deploy/deploy.sh` 的 `pull_code()` 函数已把 `git pull origin <branch>` 改成 `git fetch origin <branch> && git reset --hard origin/<branch>`——deploy 机本来就是 mirror 模式，硬同步远端永远是正确语义；现在的 `git pull` 默认行为反而会被 divergent 卡住。本次同 PR 一起改。

**待做**：

2. 在 FF-only 政策上线 SOP 里加一条"force-push 共享分支后**必须立刻**在所有部署服务器上跑 `git fetch + git reset --hard origin/<branch>`"（虽然 deploy.sh 改完后下次 deploy 自动对齐，但首次 force-push 后到下次 deploy 之间，本地 ref 仍是漂移状态，可观测性差）。
3. 部署脚本里加一个 pre-check：本地 HEAD 不在 `origin/<branch>` 历史线上时输出明确的"上游可能被 force-push 过"提示，比甩 `git pull` 的 hint 更友好（reset --hard 改完后这种提示也没必要再有，可选）。

## Takeaways

- Force-push 共享分支是**全分布式**操作的破坏者：发起方本地零感，所有镜像方在下一次 sync 时炸。SOP 必须包括"所有镜像的强制对齐"作为收尾。
- `git fetch` 输出里那行 `old..new` 是 tracking ref 的更新范围，**不代表本地 HEAD 跟新远端 ref 能 FF**。日志误导性强。
- 部署机 `git pull` 是错的语义——应该是 `git fetch + git reset --hard`。日常运行没事，遇到 force-push 立刻暴露。
