# FF-only 政策启用首日：staging/production 强制对齐到共同祖先

## 场景

把 `develop → staging → production` 升级合并策略改为 fast-forward only 后，开第一个 develop→staging PR 时，Gitea 下拉里**没有出现** "Fast-forward only" 选项。

## 根因

FF 的几何前提是 base 分支 HEAD 必须是 head 分支的祖先。但历史上 staging/production 通过 plain merge 累积了 develop 上没有的 commit（虽然全是 merge commit、零实质代码改动），导致：

- staging 不是 develop 的祖先 → develop→staging FF 不可达
- production 不是 staging 的祖先 → staging→production 同理

Gitea 在几何不可达时**直接隐藏** Fast-forward only 选项（不会让你点了再报错）。

## 诊断步骤

```bash
# 关键指标：non-merge commit 数（实质内容漂移）
git log --oneline --no-merges develop..staging      # 0 → 无内容漂移
git log --oneline --no-merges develop..production   # 0 → 无内容漂移

# 决定性证据：工作树是否完全一致
git diff --stat develop~N staging       # empty → 树相同
git diff --stat develop~N production    # empty → 树相同
```

如果 `--no-merges` 列表为空 + tree diff 为空，说明 staging/production "多出来" 的 commit 都是 merge commit 包装，**对齐到 develop 历史上某个等价 commit 是安全的**。

## 关键设计决策：对齐到"上次发布的那个 commit"，不是 develop 当前 HEAD

最初想法是把 staging/production reset 到 develop@HEAD。**错的**——这等于把还没走完发布流程的最新 PR 偷塞进 staging/production，跳过 UAT 验收。

正确做法：对齐到 develop 历史上**等价于当前 staging/production 工作树**的那个 commit（用 `git diff --stat develop~N staging` 找到 N），然后让最新 PR 通过正常的 FF 升级链路推上去。

本次：staging/production 实际部署内容 = develop~1 = `d5eddeae`（PR #348 sync-back），所以对齐目标就是 `d5eddeae`。

## 操作步骤（可复用）

```bash
GITEA_TOKEN=$GITEA_API_TOKEN
TARGET=d5eddeae...   # 上次发布的等价 commit

# 1. 临时开 staging 推送权限（push + force-push 都要开，缺一不可）
curl -X PATCH -H "Authorization: token $GITEA_TOKEN" \
  http://43.130.59.228/api/v1/repos/FFAIWorkspace/workspace/branch_protections/staging \
  -d '{
    "enable_push": true,
    "enable_push_whitelist": true,
    "push_whitelist_usernames": ["chentao.jia"],
    "enable_force_push": true,
    "enable_force_push_allowlist": true,
    "force_push_allowlist_usernames": ["chentao.jia"]
  }'

# 2. force-push
git push --force-with-lease=refs/heads/staging:$(git rev-parse origin/staging) \
  origin $TARGET:refs/heads/staging

# 3. 立刻关
curl -X PATCH ... -d '{
  "enable_push": false,
  "enable_push_whitelist": false,
  "push_whitelist_usernames": [],
  "enable_force_push": false,
  "enable_force_push_allowlist": false,
  "force_push_allowlist_usernames": []
}'

# production 同理
```

## 坑：Gitea 分支保护下 force-push 需要"双开"

**`enable_force_push: true` 单独不够**。Gitea 的 pre-receive hook 会先检查 `enable_push`——如果 push 被全局禁了，force-push 也连带被拒（"Not allowed to force-push to protected branch"）。

必须同时设置：
- `enable_push: true` + `enable_push_whitelist: true` + `push_whitelist_usernames: [user]`
- `enable_force_push: true` + `enable_force_push_allowlist: true` + `force_push_allowlist_usernames: [user]`

文档里没写得很清楚，第一次操作容易踩。

## 坑：`--force-with-lease=<refname>:<expect>` 的 expect 必须是远端真实 SHA

`refs/heads/staging` 的远端 SHA 用 `git ls-remote origin staging` 拿（或本地的 `git rev-parse origin/staging`，前提是刚 fetch 过）。手输 hash 容易漏字符变成 "stale info" 报错。

## Takeaways

- FF-only 政策上线前，先用 `git log --no-merges base..head` 体检三分支漂移情况
- 对齐目标永远是"当前实际部署的那个 commit"，不是 develop@HEAD——保留发布流程的完整性
- Gitea force-push 要双开 push + force-push 权限，操作完立刻关
- 这种"一次性强制对齐"是从老工作流切到 FF-only 的必经一步，每个项目都只会做一次，**留 learning 比留 skill 更合适**
