07 · Commit / PR 工作流

07 · Commit / PR 工作流

写给未来的自己,不是写给现在。

开场比喻

commit 历史不是"完成的记录",是未来 debug 时的档案

半年后某个功能炸了,你 git bisect 找凶手——届时帮你的不是代码本身,而是 commit 粒度和 message 质量:

  • 每个 commit 只做一件事 → bisect 能精准定位到一行
  • Message 写清楚 why → 你一眼看懂当时为什么这么改

每次写 commit 都在给未来的自己留证据。Claude 帮你写得差,未来的你就要付利息。

第 1 条铁律:Claude 不该主动提交

除非你明说"提交一下",否则 Claude 不应该 git commit

这是 Claude Code 的默认规则,也是你该严格守的规则:

  • 主动提交会把不完整的改动封进历史(没人想要半成品 commit)
  • 打乱你的节奏(本来想先 review 再提)
  • 生成草率的 commit message(没和你对齐 why)

判断信号

  • ✅ 你说"commit 一下"、"提交" → 明确指令
  • ❌ 你说"改完了" / "完成了" → 不是提交指令,别脑补

第 2 条铁律:一个 commit 一件事

这些都该

混合内容拆法
修 bug + 顺手重构一个无关文件两个 commit:fix: + refactor:
加功能 + 改代码风格feat: + style:
功能 A + 功能 B两个 feat:
加依赖 + 用了新依赖实现功能chore: + feat:

为什么拆不是洁癖

拆 commit 是为了保护未来的 revert 能力

  • 发现 feat: 引入 bug → git revert <那个 commit> 就能单独撤掉
  • 混在一起 → revert 会把重构也撤掉 → 你得手动挑改

判断标准:问自己"这个 commit 单独 revert 会不会出问题?"

  • 是 → 必须拆
  • 否 → 可以合

Commit Message 模板

最小化模板(Conventional Commits):

<type>(<scope>): <what changed in one line, imperative>

<optional body: why, not what>

<optional footer: BREAKING CHANGE, refs>

Type 速查

type用途
feat新功能
fixBug 修复
refactor重构(不改行为)
style代码格式(空格、分号)
docs文档
test测试
chore构建、依赖、CI
perf性能优化

好 vs 坏 对比

update code
fix bug
wip
改一下

fix(checkout): prevent duplicate submissions on fast double-click

debounce was missing on handleSubmit; reproduced by QA in #234.

要点

  • 标题用祈使语气add,不是 added / adding
  • 标题小写开头、不加句号
  • Body 写 why,不写 what —— 代码已经告诉你 what 了

Why 才是价值

一年后翻这条 commit:

  • "what":看 diff 就知道了
  • "why":只有 commit message 能告诉你
fix(pricing): fallback to USD when exchange rate API returns 502

日区 API 偶发 502,之前 service 抛异常整页挂。
改成返回默认价 + 标记数据陈旧,比崩页好。
相关告警:Sentry AP-4271

这种 message 是未来的救命稻草。

PR 描述三段式

## Why
<动机 / 来源:解决什么问题、对应哪个 issue / 需求>
 
## What changed
<核心改动 1-3 条,不要罗列文件>
 
## How to test
<reviewer 能按步骤验证的 checklist>

写作要点

  • Why 放最前面——reviewer 最想知道的是动机
  • What 不罗列文件——reviewer 会自己看 diff;你要说的是"改动的意图分组"
  • How to test 可执行——"点 X 按钮,看 Y 应该发生"而不是"测一下功能"

真实对比

改了一些东西,修了 bug。请 review。

## Why
QA 报告双击 Pay 按钮会重复扣款(#234)。根因:handleSubmit 没做 
debounce,且 server action 没幂等检查。
 
## What changed
- `<CheckoutForm>` 在 submit 期间锁定按钮(loading 状态 + disabled)
- `submitPayment` action 增加 idempotency key 校验
 
## How to test
1. `pnpm dev`,打开 /checkout
2. 快速双击 Pay 按钮
3. 确认只产生一笔订单(检查 /api/orders 日志)
4. 用相同 idempotency key 再次调用 action,应返回原订单

让 Claude 帮你写的正确姿势

Pattern A:让 Claude 先打 message,你审过再提交

不要这么说:

"提交一下"

→ Claude 自己决定分几个 commit、怎么写 message → 你可能要返工

该这么说

"先看一下 git status 和 diff,打印你建议的 commit 拆分和 message,
我审过再提。"

→ Claude 交方案 → 你修改 → 批准 → 提交。

Pattern B:多 commit 的 PR

"把 <base>..HEAD 的所有 commit 合起来总结 PR 描述,不要只看最后一个。"

Claude 默认可能只看 HEAD,漏掉前面的改动。显式让它 git log <base>..HEAD

Pattern C:别跳过 hook

pre-commit hook 报错时——不要 git commit --no-verify

  • Hook 存在是有原因的(lint / typecheck / format)
  • 绕过只是把问题推到下一个阶段(CI 报错 / 线上出问题)

正确做法:修 hook 报的问题 → 重新 stage → 提交。

危险操作的红线

这些操作你不该轻易让 Claude 做,Claude 也应该先问再做

操作风险
git push --force(到共享分支)覆盖别人的提交
git push --force 到 main / master公司事故级操作
git reset --hard抹掉本地未提交工作
git clean -fd删除所有未跟踪文件
git branch -D <branch>强删分支(未合并的也不警告)
git commit --amend(已推送的 commit)改历史,强推会影响协作者
rm -rf 在 repo 里字面意思

双重保险

  1. CLAUDE.md 里明确:"这些操作前必须先问"
  2. 关键项加 hook(PreToolUse + Bash matcher,检测危险命令)

反模式五连

① "提交吧"然后不看

Claude 自己决定拆分、写 message、提交——你省了 30 秒,未来 30 分钟还回来。

② Commit message 写 what

update user.ts → 代码已经说了。写 why。

③ 标题超过 72 字符

git log --oneline 会截断。标题精炼,细节放 body。

④ 混 commit 不拆

一个 commit 里既修 bug 又重构又改格式——未来 revert / bisect 全废。

⑤ Force push 到 main

整个团队的修罗场。Claude Code 默认拒绝,不要强行让它做

一句话总结

Git 历史是写给未来的,不是写给自己的。

三条底线:Claude 不主动提交、一个 commit 一件事、message 写 why。

做到这三条,半年后 debug 时未来的你会感谢现在的你。

判断速查

准备提交了

改动是一件事吗?          ── 否 → 先拆 stage
  ↓ 是
让 Claude 打 message 草稿  ← 不要直接 "commit"

你审过 message ok?       ── 否 → 改
  ↓ 是
提交(不要 --no-verify)

Hook 报错?               ── 是 → 修问题,不要绕
  ↓ 否

← 06 · Hook 与自动化 | 目录 | 08 · 常见反模式与翻车现场 →