有意提交:优秀提交的艺术

发布: (2026年3月15日 GMT+8 16:26)
12 分钟阅读
原文: Dev.to

Source: Dev.to

请提供您希望翻译的正文内容,我将为您翻译成简体中文并保留原有的格式、Markdown 语法以及技术术语。谢谢!

Git 精通系列 第 2 部分

第 1 部分: Git 实际是如何思考的 | 第 3 部分: 无惧分支 →

问题

项目进行到第六个月时,你在追踪一个 bug。运行 git log,看到的历史如下:

a3f8c9d fix
1b4e7a2 update
c5d2f8a wip
9d3e1f4 asdf
7a2b5c8 final
4f1e8d6 final2
2c9a3b7 ok now it works

这是谁写的?是你。现在你根本不知道每个提交到底包含了什么,除非逐个打开查看。

再看看这个:

a3f8c9d fix(auth): handle expired JWT tokens on refresh
1b4e7a2 feat(cart): add quantity update on product page
c5d2f8a refactor(api): extract payment service into dedicated class
9d3e1f4 fix(checkout): prevent duplicate order on double‑click submit

代码相同,历史相同。第二种写法是文档,第一种是噪音。

提交不仅仅是一个保存点。它还是给下一个阅读这段代码的人——几乎总是未来的你——的消息。

原子提交

“原子提交”是一个经常被提及却少有解释的术语。实际上它指的是 一次提交,只为一个理由

  • 不是针对单个文件。
  • 不是针对单个函数。
  • 只针对一个理由。

一次重构 一个 bug 修复是两个理由,即使它们触及了同一个文件。一次特性 其测试算一个理由——测试是特性的一部分。添加一个依赖 使用它也是一个理由——没有依赖就无法使用。

检验原子性

你能用一句话描述这个提交的作用吗,且不能使用 “and” 吗?

✅ 好❌ 差
“Fix the null pointer exception on empty cart checkout”“Fix cart bug and update user model and add some tests”
“Add email validation to the registration form”

第二种描述不仅更难说明——也更难 回滚挑拣(cherry‑pick)以及在代码审查时 理解。提交描述里出现的每一个 “and” 都是该提交应该被拆分的信号。

暂存区

大多数开发者对所有文件都使用 git add .,从不考虑暂存区的作用。这就像一次性写完整封邮件,而不是先草拟——虽然能完成,但并未按工具的设计初衷使用。

暂存区的存在是让你能够在下一次提交时精确地挑选想要的内容,而不受工作目录中其他改动的影响。比如你在五个文件中做了三类不同的改动,想要分别提交。暂存区正是为此而设。

暂存特定文件

git add src/auth/login.php
git add src/auth/logout.php
git commit -m "feat(auth): add login and logout handlers"

git add src/user/profile.php
git commit -m "feat(user): add profile page"

暂存文件中的特定行

git add -p src/user/model.php

这会打开交互式提示,逐块遍历文件中的每个改动并询问如何处理:

@@ -15,6 +15,10 @@ class User extends Model
     protected $fillable = ['name', 'email'];
+
+    public function orders() {
+        return $this->hasMany(Order::class);
+    }
+
     public function profile() {
Stage this hunk [y,n,q,a,d,/,e,?]?
  • y – 暂存该块
  • n – 跳过
  • e – 编辑想要暂存的具体行

一旦你经常使用 git add -p,就不会再出现把本该分开的三件事混在同一次提交里的情况。你会在编码时就开始思考 提交,而不是事后才去整理。

约定式提交

一种简洁的格式如下:

type(scope): description

Optional longer body explaining what and why,
not how (the code shows how).

Optional footer: Closes #123

常见类型: feat(特性)、fix(修复)、refactor(重构)、docs(文档)、test(测试)、chore(杂务)、perf(性能)。

你不需要工具强制执行,只需要养成习惯。它能为你带来:

  • 可快速浏览的历史featfixrefactor 在阅读 git log 时立即提供上下文。
  • 自动生成变更日志 – 像 sema 等工具可以直接依据这些信息生成 changelog。
  • 更好的回滚与挑拣 – 由于每次提交只做一件事,定位和恢复变得简单。
  • 一致的团队沟通 – 大家都遵循同一套规则,审查和协作更顺畅。

坚持使用约定式提交,你的 Git 历史将从噪音变成有价值的文档。

Source:

ntic‑release* 和 conventional‑changelog 会解析提交信息,自动生成发布说明。

  • 在代码审查中更清晰的意图 – 一个包含 feat(payment): add Razorpay integrationtest(payment): add unit tests for webhook handlerdocs(payment): add setup guide 等提交的 PR,能够让审查者准确了解每个提交的目的。

实际示例

git commit -m "feat(auth): add OTP login via Fast2SMS"
git commit -m "fix(cart): prevent negative quantity on decrement"
git commit -m "refactor(api): extract HTTP client into service layer"
git commit -m "chore(deps): upgrade Laravel from 10 to 11"
git commit -m "perf(images): lazy load product images on listing page"

这些提交并不因复杂而令人印象深刻,而是因清晰而出色。新加入团队的开发者可以阅读三个月的历史,快速了解产品的演进。

Subject vs. Body

Subject 行-m 部分)描述做了什么
Body 描述为什么

大多数提交不需要 Body。但当上下文重要——决策并不显而易见、修复了看似反直觉的问题、未来的你需要了解背景——这些信息就应写在 Body 中。

使用编辑器

git commit

这会打开你的编辑器。先写 Subject,留一个空行,再写 Body:

fix(payment): retry failed Razorpay webhooks on 5xx errors

Razorpay occasionally returns 503 on their webhook endpoint during
high load. Without retry logic, missed webhooks left orders stuck
in "pending" state with no automated resolution path.

Added exponential backoff (3 retries, 2s/4s/8s delays). If all
retries fail, the webhook is queued for manual review.

Closes #247

六个月后,调试这段代码的人阅读这段信息时,能够理解不仅是改了什么,更是为什么。那个人几乎肯定就是你自己。

经验法则: 如果你需要在代码审查中解释这次改动,就把解释写在提交的 Body 中。未来的开发者应当拥有与你审查者相同的上下文。

常见情形

你已经为一个功能工作了一天,提交记录如下:

wip: halfway through auth refactor
fix typo
add

请改为使用上述技巧,将它们拆分为原子且描述明确的提交。这样你的历史更整洁,审查更顺畅,未来的自己也会感激不已。

清理提交再合并

“提交不是备份。它是一条信息。”

下面是一份使用交互式 rebase 整理进行中历史的分步指南。保持结构和内容不变——只让它更易读。

1️⃣ 确定要清理的范围

# 交互式 rebase 最近 5 次提交(根据需要调整数字)
git rebase -i HEAD~5

这会打开编辑器,显示最近的五次提交:

pick a3f8c9d wip: halfway through auth refactor
pick 1b4e7a2 fix typo
pick c5d2f8a add missing return statement
pick 9d3e1f4 actually fix the auth refactor
pick 7a2b5c8 remove debug logs

2️⃣ 编辑 rebase 待办列表

将命令改为你想要的最终历史:

reword a3f8c9d wip: halfway through auth refactor
squash 1b4e7a2 fix typo
squash c5d2f8a add missing return statement
squash 9d3e1f4 actually fix the auth refactor
squash 7a2b5c8 remove debug logs
  • reword – 编辑提交信息。
  • squash(或 s – 将该提交合并到前一个提交。
  • drop(或 d – 完全删除该提交。

保存并关闭编辑器。Git 随后会引导你编辑最终的提交信息,最终得到 一个干净的提交,即可合并。

唯一规则:只对尚未推送到共享分支的提交进行重写。对已经被他人拉取的历史进行重写会导致冲突。

3️⃣ 实用 Git 命令

操作命令
暂存单个文件git add path/to/file
交互式暂存特定行git add -p path/to/file
查看未暂存的改动git diff
查看已暂存的改动git diff --staged
只写标题提交git commit -m "feat(scope): description"
标题 + 正文提交(打开编辑器)git commit
在推送前清理最近 N 次提交git rebase -i HEAD~N
修改最近一次提交(信息或内容)git commit --amend
撤销最近一次提交但保留已暂存的改动git reset --soft HEAD~1
撤销最近一次提交并取消暂存改动(保留文件)git reset HEAD~1

4️⃣ 思维方式的转变

当你执行 git commit -m "fix" 时,你不仅仅是在保存工作——你 在给所有以后会阅读这段代码的人写信,包括六个月后凌晨 2 点的 你自己。好提交信息现在只会多花几秒钟,却能在以后省下无数小时的时间。

5️⃣ 延伸阅读

第 1 部分: [How Git Actually Thinks]
第 3 部分: [Branching Without Fear]

如果你觉得这篇文章有帮助,我已将整套系列整理成 23 页 PDF 参考手册——包括清单、钩子模板、80+ 命令、reflog 与 bisect 深入解析,以及 12 种真实紧急情况的恢复方案。

Git Mastery Field Guide →

0 浏览
Back to Blog

相关文章

阅读更多 »

无惧分支

Git Mastery 系列第 3 部 ← 第 2 部:Committing with Intention https://dev.to/itxshakil/committing-with-intention-the-art-of-a-good-commit-p90 | 第 4 部:C…