写好 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 把规范变成自动约束,避免依赖团队成员的自觉。

gitconventional-commitsbest-practicescommitlintdevops