写好 Git commit 信息
1. 概述
commit 信息是你在调试、代码审查或编写更新日志时最经常重新阅读的内容——比你刚写的代码还要看得多。本文帮你掌握标准的 commit 信息结构,应用 Conventional Commits 规范,以及用 commitlint + husky 自动检查信息。读完本文后,你能写出清晰一致的 commit,team 还能从 git 历史自动生成更新日志。
2. 为什么 commit 信息很重要
一条好的 commit 信息能回答三个问题:改了什么、为什么改、影响是什么。没有它,你必须用 git show 逐一查看每个 commit 来猜测作者的意图——浪费的时间是从一开始就写好信息的好几倍。
比较两条针对同一改动的信息:
# 差的
fix bug
# 好的
fix(payment): 处理当 PaymentService.charge() 收到没有 Customer 的 Order 时的空指针异常
差的信息没有说明是什么 bug,在哪里。好的信息明确了模块(payment)、操作(fix)和具体原因——读 5 秒就明白了。
3. 标准 commit 信息结构
Git 建议分为 3 部分结构:subject(主题)、body(正文)、footer(页脚)。
<type>(<scope>): <subject>
<body>
<footer>
subject:简短总结,≤ 50 个字符,用现在时(“add” 而不是 “added”)。body:解释为什么改变,而不是改了什么(diff 已经说明了)。footer:元数据——breaking change、相关 issue、共同作者。
完整例子:
fix(order): 避免重试失败的付款时重复扣费
PaymentService.charge() 没有 idempotency key,所以网络超时
后客户端重试时,系统会对同一个 Order 调用 API 网关两次。
在请求头中添加 idempotencyKey = order.getId() + "-" + attemptCount
让网关自动去重。
Closes #482
3.1. subject 行
subject 是最重要的一行——它会出现在 git log --oneline、PR 标题、更新日志上。规则如下:
# 错的——首字母大写,句末有句号,用过去时
Fixed the bug in order service.
# 对的——小写开头,无句号,现在时动词
fix: 通过电子钱包付款时没有扣减库存
注意: 50 个字符的限制不是硬性规定,但超过 72 个字符会在许多终端和 GitHub UI 上被截断。尽量简洁。
3.2. body 部分
body 用来解释上下文——代码 diff 无法传达的内容:为什么选择这个方案,其他方案为什么被排除,有没有副作用。
refactor(cart): 将 CartService 从同步改为异步
之前 CartService.checkout() 按顺序调用 InventoryService 和
ShippingService,总延迟约 800ms。切换到 CompletableFuture
并行执行这两个调用,延迟降至约 450ms。
不改变 public API,只改内部实现。
用一个空行分隔 subject 和 body——git 和各种 commit 解析工具都依赖这个约定。
4. Conventional Commits 规范
Conventional Commits 是目前最流行的标准,定义固定的 type 让工具能自动分类和生成更新日志。
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
常见的 type:
| Type | 含义 |
|---|---|
feat |
添加新功能 |
fix |
修复 bug |
docs |
仅改动文档 |
style |
格式化代码,不改逻辑 |
refactor |
改动代码结构,不改行为 |
test |
添加/修改测试 |
chore |
杂务(构建、依赖、配置) |
perf |
改善性能 |
以 scope 为模块名的例子:
feat(auth): 支持 OAuth2 Google 登录
添加 GoogleOAuth2Provider 实现 OAuth2LoginProvider。
在 application.properties 中新增 client-id、client-secret 配置。
4.1. Breaking change
当改动破坏向后兼容性时,用 ! 标记在 type/scope 之后或用 BREAKING CHANGE: 页脚。
feat(api)!: 将 GET /orders/{id} 的响应改为 OrderDto
BREAKING CHANGE: 字段 "customerName" 改为对象
"customer",包含 id、name、email。客户端需要更新解析逻辑。
semantic-release 和各种 CI/CD 工具根据 BREAKING CHANGE: 行来自动提升主版本号。
5. 自动检查 commit 信息
好的规范只有在 team 一致遵守时才有效。用 commitlint 在本地阻止格式错误的 commit,防止 push。
<!-- 不适用于 Node 项目,仅作范例,如果用 Maven 搭配 husky-git-hooks -->
实际上 commitlint 运行在 Node 上,通过 npm 安装:
npm install --save-dev @commitlint/cli @commitlint/config-conventional husky
创建配置文件:
// commitlint.config.js
module.exports = { extends: ['@commitlint/config-conventional'] };
通过 husky 绑定 commit-msg hook:
npx husky init
echo 'npx --no -- commitlint --edit "$1"' > .husky/commit-msg
现在试试提交格式错误的 commit:
git commit -m "fixed bug"
⧗ input: fixed bug
✖ subject may not be empty [subject-empty]
✖ type must be one of [feat, fix, docs, style, refactor, test, chore, perf]
✖ found 2 problems, 0 warnings
husky - commit-msg script failed (code 1)
commit 立即被阻止,强制你在创建 commit 前修正格式。
5.1. 用于 Java/Spring Boot 项目
如果项目是 Maven,你仍可并行使用 husky + commitlint(它们只需要 Node 运行时,不影响 Java 构建)。或者如果想要纯 Java 方案,可以写一个简单的检查脚本并通过 core.hooksPath 调用:
#!/bin/bash
# .githooks/commit-msg
PATTERN="^(feat|fix|docs|style|refactor|test|chore|perf)(\([a-z-]+\))?: .{1,50}"
MSG=$(cat "$1")
if ! [[ "$MSG" =~ $PATTERN ]]; then
echo "Commit 信息不符合 Conventional Commits 格式" >&2
exit 1
fi
git config core.hooksPath .githooks
chmod +x .githooks/commit-msg
5.2. 为格式检查脚本编写测试
如果你维护自己的检查脚本(不用 commitlint),应该为正则表达式编写测试。假设你把解析逻辑封装在 Java 类中以便在 CI 中复用:
public class CommitMessageValidator {
private static final Pattern PATTERN =
Pattern.compile("^(feat|fix|docs|style|refactor|test|chore|perf)" +
"(\\([a-z0-9-]+\\))?: .{1,50}");
public boolean isValid(String message) {
String subject = message.lines().findFirst().orElse("");
return PATTERN.matcher(subject).find();
}
}
class CommitMessageValidatorTest {
private final CommitMessageValidator validator = new CommitMessageValidator();
@Test
void shouldAcceptConventionalFormat() {
boolean result = validator.isValid("fix(order): 避免重试时重复扣费");
assertThat(result).isTrue();
}
@Test
void shouldRejectMessageWithoutType() {
boolean result = validator.isValid("fixed the bug");
assertThat(result).isFalse();
}
@Test
void shouldRejectUnknownType() {
boolean result = validator.isValid("update: 添加了一些内容");
assertThat(result).isFalse();
}
}
这些测试确保当你之后修改正则表达式时验证逻辑不会退化——比如添加新 type 但忘了更新测试。
6. 常见错误
commit 太大,信息笼统。 一个 commit 修改 15 个不同文件却用 “update code” 作信息,说明你应该拆成多个 commit,每个 commit 一个明确的目标(atomic commit)。
在一个 commit 中混合多种类型的改动。 同时 fix bug 和 refactor 无关代码会让 git revert 和 git bisect 难以使用——revert 一个 fix 可能连带丢失 refactor 部分。
body 重复 subject。 body 应该补充上下文,而不是用更长的句子重新解释第一行。
# 重复,无意义
fix(order): 修复计算总金额的错误
修复 OrderService 中计算总金额的错误。
用 emoji/装饰符替代实际内容。 emoji 本身没错,但如果用它替代真实描述,自动生成的更新日志就会信息贫乏。
注意: Conventional Commits 适合有自动生成更新日志或语义版本管理的 team CI/CD。如果是小 team、commit 少,过于严格的规范可能造成不必要的摩擦——优先考虑清晰度而不是机械地遵守格式。
7. 总结
好的 commit 信息不需要长,只需结构正确:简洁的现在时 subject、解释原因的 body、记录必要元数据的 footer。应用 Conventional Commits 能帮 team 更快读懂 git 历史,并让工具自动生成更新日志和版本管理。设置 commitlint 或自定义 git hook 把规范变成自动约束,避免依赖团队成员的自觉。