# pre-commit hook 和 CI workflow 是独立两套校验，修了一个不代表另一个过

**日期**：2026-05-18
**场景**：PR #426 merge develop 后本地 pre-commit hook 跑过（修了 merge commit 跳过 migration count 的 worktree-aware 边界 bug），push 成功。但 Gitea CI 上 `migration-file-count` job 仍 FAILURE —— **两套校验完全独立**。

## 不直观点

很多人（包括我）以为：
- pre-commit hook 是 CI 的"本地左移版本"
- 改了 hook = 改了 CI

**错**。实际上两套各有逻辑：

| 维度 | pre-commit hook | CI workflow |
|---|---|---|
| 位置 | `.githooks/pre-commit` | `.gitea/workflows/quality-gates.yml` |
| 触发点 | `git commit` 本地 | `pull_request` 事件远端 |
| 视角 | **单 commit** staged 内容 | **PR 累积 diff**（base..HEAD） |
| 数据源 | `git diff --cached` | `git diff origin/develop...HEAD` 三点 |
| 边界 case | merge commit / cherry-pick 中间态 | epic PR 累积 / merge PR 多 source |

这次 PR #426 修 hook 让 merge commit 跳过 migration count 检查（local 视角 OK），但 CI 用 PR-total diff 检查（epic PR 累积 7 个 migration 全在 diff 里），完全不同的失败模式。

## 同样的"≤ 1 migration"规则需要修两处

CLAUDE.md「数据库」段规定 `每次提交最多一个迁移文件`。

| 错误实现 | 正确实现 |
|---|---|
| **hook**: `git diff --cached` 检查 staged 数量 | 加 worktree-aware merge commit 跳过：`GIT_DIR_PATH="$(git rev-parse --git-dir)"; [ -f "$GIT_DIR_PATH/MERGE_HEAD" ] && skip` |
| **CI**: `git diff origin/develop...HEAD` 检查 PR-total | 改 per-commit 遍历 `git rev-list base..HEAD` 各自校验，merge commit (>1 parent) 跳过 |

## 通用模式：本地 vs 远端校验差异

**用 git rev-list 逐 commit 校验** 而不是 PR-total diff 校验，避免：
- epic PR 累积多 commit 误判
- merge PR 带 source branch 多 commit 内容误判
- rebase / cherry-pick 重新打包 commits 误判

```bash
# ❌ 错（PR-total 视角）
git diff --name-only --diff-filter=A "origin/${BASE_REF}...HEAD" | grep MIGRATION_PATTERN

# ✅ 对（per-commit 视角）
for sha in $(git rev-list "origin/${BASE_REF}..HEAD"); do
  # merge commit 跳过（其 source commit 各自已经过校验）
  parents=$(git rev-list --parents -n 1 "$sha" | awk '{print NF-1}')
  [ "$parents" -gt 1 ] && continue
  count=$(git show --name-only --pretty=format: "$sha" | grep -c MIGRATION_PATTERN)
  [ "$count" -gt 1 ] && { echo "violate"; exit 1; }
done
```

## 何时遇到

- 写新 quality gate（"每 commit X ≤ N"类规则）
- epic PR 累积多个 commit 触发 CI 失败
- merge PR 带 source 多 source commit 内容触发 CI 失败
- worktree 场景下本地 hook 失效

## 防御措施

1. **凡是 "≤ N per commit" 规则同步修两处**：`.githooks/` 本地 + `.gitea/workflows/` 远端
2. **改 hook 时同步检查 CI 同名 job 有没有同 bug**
3. **逐 commit 视角优先于 PR-total 视角**：CLAUDE.md 规则原意是 per-commit，不是 per-PR

## 配套 learning

详见 [2026-05-18-worktree-merge-head-detection.md](2026-05-18-worktree-merge-head-detection.md) 关于 worktree 下 `.git` 是 file 不是 directory 的 hook 修复要点。

## 参考

- PR #426: `.githooks/pre-commit` (merge commit skip + worktree-aware) + `.gitea/workflows/quality-gates.yml` (per-commit 遍历)
- CLAUDE.md「数据库」段 — `每次提交最多一个迁移文件`
