Git 提交信息鉴赏:从 fix 到 fix again
如果把一个仓库的提交历史打印出来钉在墙上,它大概会像一面潦草的留言板:
1fix
2update
3修改
4再修一下
5这次真的修好了
6最终版
7最终版2Git 其实不关心这些字。它只需要一个哈希,就能把代码管得明明白白。提交信息是写给人的,更准确地说,是写给三个月后打开 git blame、对着屏幕皱眉的那个人的。
而那个人,通常就是你自己。
一、那些让人沉默的提交
先来一场小型鉴赏。以下提交信息均来自真实生活,如有雷同,说明大家的仓库都差不多。
| 提交信息 | 想表达的意思 | 读者看到后 |
|---|---|---|
fix | 修了个东西 | 修了啥? |
update | 更新了一下 | 更新了啥? |
修改 | 同上,但更真诚 | 同上 |
fix bug | 修了一个 bug | 哪个 bug? |
再提交一次 | 上次没提交全 | 为什么没提交全? |
最终版 | 当时的最终版 | 现在第几版了? |
临时提交,别删 | 先存个档 | 三年后还在 |
Merge branch 'master' of ... | Git 自己写的 | 至少它诚实 |
这些信息不能算错,它们只是没有信息量。哈希告诉你“哪一次”,提交信息本该告诉你“为什么”。
一个残酷的事实是:代码只能说明“现在是什么样”,提交信息才能说明“当时为什么变成这样”。前者可以读,后者只能猜。而猜历史的人,最后往往会写出更奇怪的代码。
二、一条提交信息该回答什么
不用把它写成小作文,一条够用的提交信息只需要回答三个问题:
- 为什么改:原来的行为哪里不对,或者新需求是什么。
- 改了什么:范围有多大,动了哪个模块。
- 影响谁:有没有接口变更、数据迁移、配置调整。
反面例子:
1fix: 修复问题正面例子:
1fix(auth): 避免 Safari 下重复提交登录表单
2
3Safari 在表单提交后不会立即禁用按钮,用户双击会发出两次请求。
4本次在 submit 事件中加锁,并在 fetch 的 finally 中释放。
5
6影响:仅登录页;无接口变更。
7Closes #128第一行是标题,空一行之后是正文。标题控制在 50 个字符左右,正文每行别太长,方便在终端里读。这个格式不复杂,难的是在赶进度的时候还愿意多写三行。
三、为什么我们总是写成 fix
把锅全甩给“懒”并不公平。
提交的那一刻,你脑子里装着完整的上下文:刚改的那个判断、刚踩的那个坑、刚和产品确认的那个边界条件。此时写 fix,你自己完全看得懂,因为上下文还在内存里。
问题是,内存不持久化。
三个月后,上下文早已被新需求覆盖。你打开 git log,看到的是一排 fix,像一排没有标签的罐头。你只能打开 diff,一行行反推,最后得出一个熟悉的结论:“这行代码是谁写的?”
还有一个现实原因:git commit -m "fix" 太方便了。一个 -m 把编辑器都省了,人自然会倾向于把话压缩到一行。想写正文,反而不那么顺手。
其实不加 -m 就会打开编辑器:
1git commit编辑器里会有一行注释提示你写标题和正文。如果默认编辑器不顺手,可以换一个:
1git config --global core.editor "vim"再加一个 -v,编辑器里还会附上本次 diff,边看改动边写说明:
1git commit -v四、一个够用的最小规范
如果团队还没约定,可以从 Conventional Commits 的常用类型开始。不必一次上全套,先把类型和标题统一,收益就很明显。
| 类型 | 用途 |
|---|---|
feat | 新功能 |
fix | 修复缺陷 |
docs | 文档 |
refactor | 重构,不改变外部行为 |
perf | 性能优化 |
test | 测试 |
chore | 构建、依赖、杂项 |
revert | 回滚某次提交 |
格式是:
1<type>(<scope>): <subject>scope 可以省略。比如:
1feat(search): 支持按标签筛选结果
2fix(api): 处理空数组导致的 500
3docs(readme): 补充本地启动步骤
4perf(search): 缓存重复的关键词解析结果再配几条简单规则,基本就够用了:
- 一个提交只做一件事,别把格式化、重构和功能修复混在一起。
- 标题写“做了什么”,正文写“为什么”和“影响”。
- 不写“修复 bug”,写“什么条件下出现的什么现象”。
- 关联 issue,例如
Closes #128,让未来的人能顺藤摸瓜。 - 提交前先看一遍
git diff --staged,确认没有把调试代码带进去。
好的提交信息不一定长,但一定具体。具体到别人不用问你就知道发生了什么。
五、已经写烂了,还能抢救吗
能,但要看有没有推送。
最近一条,尚未推送:
1git commit --amend这会打开编辑器,让你修改最后一条提交的信息。只改信息、不改内容时,直接保存退出即可。
最近几条,尚未推送:
1git rebase -i HEAD~5把想改的那几行前面的 pick 改成 reword,保存退出后,Git 会逐条打开编辑器让你重写提交信息。HEAD~5 表示最近五条,按需调整。
已经推送到共享分支:
最好不要改写历史。rebase 和 amend 都会产生新的哈希,别人的本地分支会和你分叉。此时更稳妥的做法是:
- 在 PR 描述里补充背景;
- 需要撤销时用
git revert,保留完整历史; - 如果确实要强制推送,先和协作者打招呼,并使用:
1git push --force-with-lease它比 --force 安全一些:如果远端出现了你本地没见过的提交,它会拒绝覆盖。但“安全一些”不等于“随便用”,共享分支上的历史仍然属于大家。
另外,git log --oneline --graph --decorate --all 很适合做一次历史体检。看着那一排 fix、update、final,你会更愿意从下一条开始好好写。
六、把提交信息当成一次微型评审
提交之前,花十秒钟读一遍自己的信息,问三个问题:
- 一个不熟悉这个模块的人,能看懂为什么改吗?
- 标题能不能再具体一点?
- 正文里有没有写清楚影响范围?
如果答案是否定的,就再改一句。成本很低,收益会持续很久。
代码是写给机器执行的,提交信息是写给人类考古的。机器不需要你解释,但人类需要,尤其是那个在深夜打开 git blame、试图弄明白“这行为什么要加个 if”的人类。
所以,下次手指准备敲下 git commit -m "fix" 的时候,不妨多写五个字。
毕竟,三个月后的你,记性真的不太好。