1. 为什么提交信息值得你花心思
你有没有过这种经历:线上出了个bug,需要定位是哪次改动引入的,结果翻看提交历史,满屏都是“fix bug”、“update”、“修改一下”、“提交”这类信息。你只能一个个点进去看diff,运气不好翻了几十页才找到目标,半小时就这么没了。更糟的是,如果这个项目是半年前你自己写的,你连当时为什么这么改都想不起来了。
Git commit message(提交信息)这件事,看起来是小事,实际上直接影响三件事:代码审查的效率、问题回溯的速度、以及团队协作的沟通成本。我见过太多项目,代码写得挺漂亮,但提交历史一塌糊涂,导致后来接手的人怨声载道。反过来,一个提交信息规范清晰的项目,哪怕代码本身一般,维护起来也顺手得多。
这篇文章面向所有用Git做版本管理的开发者,不管你是刚入行的新手,还是写了几年代码的老手,只要你还在往仓库里提交代码,这套实践就跟你有关。我会从提交信息的结构设计、撰写规范、工具链配置、团队落地几个层面,把这件事讲透,并且给出可以直接抄作业的配置和模板。全文基于我这些年带团队、做代码审查、维护开源项目的实际经验,不是从文档里抄来的理论。
先抛一个核心观点:提交信息不是写给自己看的备忘录,而是写给未来的自己和同事的一封短信。你写的时候多花三十秒,别人读的时候可能省下三十分钟。这笔账怎么算都划算。
2. 提交信息的结构设计与核心思路拆解
2.1 一条合格的提交信息长什么样
先看一个反面例子,这种提交信息你在很多仓库里都能见到:
update再看一个正面例子:
feat(auth): 新增手机号验证码登录方式 由于部分用户反馈忘记密码后找回流程太长,增加了手机号+验证码的 直接登录入口。验证码有效期为5分钟,同一手机号60秒内只能请求一次。 Closes #482两者的差距一目了然。一条合格的提交信息,应该包含三个层次的信息:
- 标题行(Subject):一句话说清楚这次提交做了什么,控制在50个字符以内(中文的话大约25个字),让人一眼扫过去就知道大概。
- 正文(Body):解释为什么这么改,而不是重复标题说改了什么。正文可以分多段,每段说明一个点。
- 脚注(Footer):关联的issue编号、破坏性变更说明、共同作者等元信息。
这个结构不是我拍脑袋定的,它来自Git社区的长期实践沉淀。Linux内核、Angular、Vue这些大型项目的提交历史,基本都是这个套路。为什么是三层而不是一层?因为不同的人读提交信息的场景不一样:快速浏览的人只看标题,做代码审查的人看正文,做发布管理的人看脚注。三层结构让每类人都能快速找到自己需要的信息。
2.2 为什么标题行要限制长度
标题行限制在50个字符左右,这个数字有讲究。Git命令行默认的显示宽度、GitHub的提交列表、各种Git客户端的日志视图,基本都在50到72个字符之间截断。超过这个长度,标题就会被省略号截掉,读者反而看不到关键信息。
我个人的习惯是:标题行尽量控制在40个中文字符以内,因为中文一个字符占两个英文字符的宽度。如果你用英文写提交信息,那就控制在50个英文字符。写完之后自己数一下,超了就精简。
这里有个实操技巧:标题行不要以句号结尾。这是英文提交信息的惯例,中文也适用。因为标题本身就是一个短语或短句,不是完整段落,加句号显得多余。另外,标题行的首字母大小写、动词形式,团队内部要统一,不要一会儿“新增”一会儿“Added”一会儿“add”。
2.3 正文为什么强调“为什么”而不是“做了什么”
这是很多人容易搞混的地方。标题已经说了“做了什么”,正文如果还在重复“做了什么”,那就是浪费空间。正文的价值在于回答:为什么要做这个改动?当时的背景是什么?有没有考虑过其他方案?
举个例子。假设你把某个接口的超时时间从3秒改成了10秒。标题写“调整接口超时时间”就够了。正文应该写:“近期监控发现该接口在高峰期响应时间经常超过3秒,导致大量请求失败。经过排查,是下游服务在批量处理时耗时较长。暂时将超时调整为10秒作为过渡方案,后续计划推动下游做异步化改造。” 这段正文里包含了问题现象、排查结论、临时方案、后续计划,这些信息在diff里是看不出来的,但对后来的人极其有价值。
我踩过的一个坑:早期写提交信息只写“修复登录问题”,结果三个月后同样的登录问题又出现了,我翻到那次提交,完全想不起来当时修的是什么、怎么修的,只能重新排查一遍。从那以后,我就养成了在正文里写清楚背景和思路的习惯。
2.4 约定式提交:让机器也能读懂你的提交
约定式提交(Conventional Commits)是目前最流行的一套提交信息规范。它的核心是在标题行加一个类型前缀,格式是:
<类型>(<范围>): <描述>常见的类型有这些:
| 类型 | 含义 | 使用场景 |
|---|---|---|
| feat | 新功能 | 增加了用户可见的新能力 |
| fix | 修复缺陷 | 修了一个bug |
| docs | 文档变更 | 只改了文档,没动代码 |
| style | 格式调整 | 空格、分号、缩进,不影响逻辑 |
| refactor | 重构 | 既不是新功能也不是修bug的代码调整 |
| perf | 性能优化 | 提升了运行效率 |
| test | 测试相关 | 增加或修改测试用例 |
| chore | 杂项 | 构建流程、依赖更新等 |
范围(scope)是可选的,用来标明改动影响的范围,比如feat(auth)、fix(api)、docs(readme)。
这套规范最大的好处是可自动化。有了类型前缀,工具可以自动生成变更日志(changelog)、自动判断版本号该升major还是minor还是patch。比如用了standard-version这类工具,你提交的时候按规范写,发版的时候一条命令就能生成完整的CHANGELOG.md,省去手工整理的麻烦。
提示:约定式提交不是强制标准,小团队或者个人项目不一定要全套照搬。但类型前缀这个习惯,我强烈建议养成,哪怕你只用一个简单的“feat/fix/docs”三分法,也比什么都不加强。
3. 核心细节解析与实操要点
3.1 标题行的动词选择与语态
标题行用什么动词,直接决定了提交信息的可读性。我总结了一个简单的原则:用祈使句,不用过去式,不用进行时。
对比一下:
- 差:
Added user login feature(过去式) - 差:
Adding user login feature(进行时) - 好:
add user login feature(祈使句) - 好:
新增用户登录功能(中文祈使句)
为什么用祈使句?因为Git本身生成的信息就是祈使句,比如Merge branch 'xxx'、Revert "xxx"。你的提交信息和Git自动生成的信息风格一致,整个历史读起来才顺畅。想象一下你在给代码库下命令:“添加这个功能”、“修复那个bug”,读起来干脆利落。
中文提交信息在这方面反而有优势,因为中文动词没有时态变化,“新增”、“修复”、“调整”这些词天然就是祈使语气,不用纠结时态问题。
3.2 正文的换行与格式
正文的换行有个硬性要求:每行不要超过72个字符。这是因为Git在终端里显示提交信息时,超过72个字符会自动换行,如果你的原文没有在合适的位置断行,显示出来就会乱七八糟。
用命令行提交的时候,可以这样操作:
git commit不带-m参数,Git会打开默认编辑器,你就可以写多行提交信息了。第一行写标题,空一行,然后写正文。编辑器里通常会有注释提示你每行不要超过72字符。
如果你习惯用-m参数,也可以写多行:
git commit -m "feat(auth): 新增手机号验证码登录" -m "由于部分用户反馈忘记密码后找回流程太长,增加了手机号+验证码的直接登录入口。"两个-m参数之间会自动加一个空行,第一个是标题,第二个是正文。但正文如果有多段,这种方式就不太方便了,还是推荐用编辑器。
3.3 脚注的写法与关联issue
脚注部分用来放元信息,最常见的几种:
Closes #123或Fixes #123:表示这次提交关闭了某个issue。GitHub、GitLab这些平台会自动识别这个关键词,合并后自动关闭对应issue。BREAKING CHANGE: xxx:表示这次提交包含破坏性变更,升级时需要注意。Co-authored-by: 某某 <email>:表示共同作者。
这里有个细节:Closes和Fixes后面跟的issue编号,必须是同一仓库的issue。如果是跨仓库关联,需要写完整路径,比如Closes owner/repo#123。
注意:
BREAKING CHANGE必须大写,而且后面要跟冒号。这是约定式提交规范里明确定义的,工具靠这个关键词来识别破坏性变更。写成breaking change或者Breaking Change都可能识别不到。
3.4 什么算“一次提交”,粒度怎么把握
提交粒度是另一个容易被忽视的点。我见过有人一天的工作攒成一个提交,标题写“今日工作”,也见过有人改一行就提交一次,历史里几百个提交全是“修改”。
合理的粒度原则是:一个提交只做一件事,且这件事是完整的。具体来说:
- 一个提交应该能独立通过代码审查,不依赖后续提交才能理解。
- 一个提交不应该混合多个不相关的改动。比如你修了一个bug,顺手格式化了整个文件,这两件事应该分成两个提交。
- 一个提交应该是可回滚的。如果这个提交出了问题需要撤销,撤销它不应该影响其他功能。
为什么粒度这么重要?因为代码审查和问题定位都是以提交为单位的。如果一个提交里混了五件事,审查的人很难逐项确认;如果出了问题需要回滚,你没法只回滚其中一件事。
实操中,我推荐用git add -p来分块暂存。这个命令会让你逐块确认要暂存哪些改动,这样你就可以把一次编辑中的多个改动拆成多个提交。刚开始可能觉得麻烦,用习惯了会发现这是保持提交历史干净的最有效手段。
3.5 提交信息里的语言选择
团队用中文还是英文写提交信息,这个没有绝对的对错,但必须统一。我见过最糟糕的情况是同一个仓库里,有人写中文,有人写英文,有人中英混杂,读起来非常割裂。
如果团队都是中文母语者,且项目不涉及外部协作,用中文完全没问题。如果项目是开源的,或者团队有非中文母语的成员,那建议用英文。混合团队的情况下,英文是更安全的选择。
不管选哪种语言,专有名词、技术术语保持原文。比如“API”、“token”、“cache”这些词,不要硬翻译成“接口”、“令牌”、“缓存”,因为大家在日常沟通中本来就说英文,翻译过来反而增加理解成本。
4. 实操过程与核心环节实现
4.1 配置提交模板,让规范落地
光靠自觉是不够的,人总有偷懒的时候。最有效的办法是配置一个提交模板,每次git commit的时候自动带出格式提示。
在项目根目录创建一个.gitmessage文件:
# <类型>(<范围>): <标题> # 类型可选:feat / fix / docs / style / refactor / perf / test / chore # 标题不超过50个字符,不以句号结尾 # 空一行后写正文,说明为什么这么改,每行不超过72字符 # 空一行后写脚注,如 Closes #123然后配置Git使用这个模板:
git config commit.template .gitmessage如果想让这个配置对团队所有人生效,可以把.gitmessage提交到仓库里,然后在项目的README或者贡献指南里说明需要执行上面这条命令。更彻底的做法是用core.hooksPath配置一个commit-msg钩子,在提交时自动校验格式,不符合规范就拒绝提交。
4.2 用commitlint做自动校验
commitlint是一个专门用来校验提交信息格式的工具。配合husky(Git钩子管理工具),可以在每次提交时自动检查。
安装步骤:
npm install --save-dev @commitlint/cli @commitlint/config-conventional husky创建commitlint.config.js:
module.exports = { extends: ['@commitlint/config-conventional'], rules: { 'type-enum': [2, 'always', [ 'feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'chore' ]], 'subject-max-length': [2, 'always', 50], 'body-max-line-length': [2, 'always', 72] } };初始化husky并添加钩子:
npx husky install npx husky add .husky/commit-msg 'npx --no -- commitlint --edit $1'配置好之后,如果你提交的信息不符合规范,会被直接拒绝,并提示具体哪里有问题。这个机制对团队新人特别有用,能帮他们快速养成习惯。
提示:commitlint的规则可以按团队情况调整。比如你们不用约定式提交,可以把
type-enum规则关掉,只保留长度校验。工具是为人服务的,不要被工具绑架。
4.3 用交互式工具辅助撰写
如果你觉得手写提交信息太麻烦,可以试试commitizen。它提供一个交互式命令行界面,一步步问你这次改动的类型、范围、描述、关联issue,最后自动拼出规范的提交信息。
安装:
npm install --save-dev commitizen cz-conventional-changelog npx commitizen init cz-conventional-changelog --save-dev --save-exact之后用npx cz代替git commit,就会进入交互式问答。这种方式对新手特别友好,不需要记格式,跟着提示走就行。
不过我个人在熟练之后就不太用commitizen了,因为交互式问答有时候比直接写还慢。我的习惯是配好模板和校验,然后直接git commit手写。工具的选择看个人习惯,没有标准答案。
4.4 从提交历史自动生成变更日志
规范提交信息的一个直接回报,就是可以自动生成CHANGELOG。用standard-version这个工具:
npm install --save-dev standard-version在package.json里加一个脚本:
{ "scripts": { "release": "standard-version" } }执行npm run release,它会自动分析自上次发版以来的所有提交,根据类型判断版本号该升多少(feat升minor,fix升patch,BREAKING CHANGE升major),然后生成CHANGELOG.md,打上tag。
这个流程的前提就是你的提交信息必须规范。如果提交信息乱七八糟,生成的CHANGELOG也没法看。所以你看,规范提交信息不只是为了好看,它直接支撑了自动化发版流程。
4.5 修改最近一次提交信息
有时候提交完才发现信息写错了,或者漏了关联issue。用这个命令修改最近一次提交:
git commit --amend这会打开编辑器,让你修改最近一次提交的信息。如果只是想改信息,不想改内容,直接保存退出就行。注意:如果这次提交已经推送到远程仓库,amend之后需要强制推送,这在多人协作的分支上要谨慎操作。
如果要修改更早的提交信息,就需要用交互式rebase:
git rebase -i HEAD~3把要修改的那次提交前面的pick改成reword,保存后会依次让你修改这些提交的信息。这个操作同样会改写历史,只建议在未推送的本地提交上使用。
5. 常见问题与排查技巧实录
5.1 提交信息写错了怎么办
这是最高频的问题。分几种情况:
| 场景 | 处理方法 | 注意事项 |
|---|---|---|
| 刚提交,未推送 | git commit --amend | 直接改,无风险 |
| 已推送,但只有自己用这个分支 | git commit --amend后强制推送 | 确认没有其他人基于这个分支工作 |
| 已推送,多人共用分支 | 不要amend,追加一次新提交说明 | 改写公共历史会造成其他人冲突 |
| 历史中较早的提交 | git rebase -i用 reword | 仅限未推送的本地提交 |
我踩过的一个坑:有一次在团队共享的分支上amend了已推送的提交,然后强制推送,结果同事本地拉取时产生了大量冲突,花了半天才理顺。从那以后我就定了个规矩:公共分支上的提交信息写错了,宁可追加一条“修正上次提交信息”的提交,也不改写历史。
5.2 提交信息太长被截断
前面说过标题行控制在50字符以内。如果你发现标题被截断了,说明写太长了。解决办法是把细节移到正文里,标题只保留最核心的信息。
比如“修复了用户在使用手机号登录时因为验证码过期导致无法登录的问题”,这个标题太长了。可以改成“fix(auth): 修复验证码过期导致的登录失败”,具体细节放正文。
5.3 团队新人不按规范提交
这是团队落地时最常见的问题。我的经验是三道防线:
- 模板提示:配置
.gitmessage,让新人每次提交都能看到格式要求。 - 自动校验:配置commitlint钩子,不符合规范直接拒绝,并给出明确的错误提示。
- 代码审查:在review时把提交信息也纳入审查范围,信息写得不清楚就要求修改。
三道防线里,自动校验是最有效的。人都有惰性,靠自觉不如靠工具。但工具也不能太严格,否则会让人产生抵触情绪。我的建议是先严后松:项目初期严格执行,等大家养成习惯后,可以适当放宽一些非关键规则。
5.4 中文提交信息在工具里显示乱码
这个问题通常出现在Windows环境下。原因是Git的默认编码和终端编码不一致。解决办法是设置:
git config --global i18n.commitEncoding utf-8 git config --global i18n.logOutputEncoding utf-8然后在终端里把编码也设为UTF-8。如果用的是某些老旧的Git客户端,可能还需要在客户端设置里手动指定编码。
5.5 如何关联多个issue
一次提交关联多个issue,可以在脚注里写多行:
Closes #123 Closes #456或者用逗号分隔:
Closes #123, #456但要注意,不同平台对关键词的识别规则略有差异。GitHub支持Closes、Fixes、Resolves这几种写法,后面跟#编号。GitLab也类似。如果你的项目同时托管在多个平台,建议查一下各平台的文档确认。
5.6 提交信息里能不能放表情符号
技术上可以,Git不限制提交信息的内容。但我的建议是谨慎使用。表情符号在不同终端、不同操作系统上的显示效果不一致,有些环境可能显示成方块。而且表情符号会增加标题的字符宽度,容易导致截断。
如果你确实想用,建议只在标题的类型前缀位置用,比如用特定符号代替feat、fix这些文字。但这样会牺牲可读性,不熟悉的人看不懂。我的做法是干脆不用,保持纯文本,兼容性最好。
5.7 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 提交被拒绝,提示格式错误 | commitlint校验不通过 | 按提示修改提交信息格式 |
| 标题显示不全 | 超过50字符 | 精简标题,细节移到正文 |
| 正文显示混乱 | 单行超过72字符 | 手动在合适位置换行 |
| 关联issue没自动关闭 | 关键词拼写错误或格式不对 | 检查是否用了Closes/Fixes + #编号 |
| 中文显示乱码 | 编码设置不一致 | 设置i18n.commitEncoding为utf-8 |
| amend后推送被拒绝 | 远程有新的提交 | 先pull --rebase再推送 |
| 钩子不生效 | husky未正确初始化 | 重新执行husky install |
6. 团队落地与长期维护的经验
6.1 规范要写进贡献指南
提交信息规范不能只停留在口头,必须写进项目的CONTRIBUTING.md里。内容要包括:格式要求、类型列表、标题长度限制、正文写法、脚注关键词。最好再配几个正例和反例,让人一看就懂。
贡献指南里的规范要简洁,不要写成一篇论文。新人没耐心读长文,把最核心的规则用列表列出来就行。详细的解释可以放在单独的文档里,需要的时候再查。
6.2 代码审查时把提交信息纳入检查项
代码审查不只是看代码,提交信息也是审查对象。我在review的时候,如果看到提交信息写得不清楚,会直接留言要求修改。这不是吹毛求疵,而是因为提交信息是代码库的长期资产,它的质量直接影响项目的可维护性。
审查提交信息主要看几点:标题是否清晰、正文是否解释了原因、是否关联了issue、粒度是否合理。如果一次提交里混了多个不相关的改动,我会要求拆分。
6.3 定期整理提交历史
项目运行一段时间后,提交历史可能会变得杂乱。如果项目还没发布正式版本,可以考虑用rebase整理历史,把零散的提交合并成有意义的单元。但如果已经发布过版本,就不要动历史了,因为已经有人可能基于这些提交做了工作。
整理历史的原则是:只整理未推送的本地提交,已推送的公共历史不动。这是铁律,违反它会给团队带来麻烦。
6.4 工具链的维护
commitlint、husky这些工具会随着版本更新而变化,配置方式也可能调整。建议在项目里锁定版本号,避免因为工具升级导致钩子失效。同时,在CI流程里也加一道提交信息校验,作为本地钩子的补充。因为本地钩子可以被绕过(git commit --no-verify),CI上的校验才是最后一道防线。
CI里校验提交信息的思路是:在流水线里加一个步骤,检查最近一次提交的信息是否符合规范。如果不符合,直接让流水线失败。这样即使有人绕过了本地钩子,也过不了CI这一关。
6.5 我个人的一些习惯
最后分享几个我自己的习惯,不一定适合所有人,但可以参考:
- 提交前先
git diff --staged看一眼,确认暂存的内容和提交信息描述的一致。 - 标题写完后默读一遍,如果读起来别扭,说明没写清楚。
- 正文里多写“因为”、“所以”、“但是”,这些连接词能帮你把逻辑讲清楚。
- 关联issue用
Closes而不是Fixed,因为Closes更通用,GitHub和GitLab都认。 - 破坏性变更一定要在脚注里用
BREAKING CHANGE:标明,这关系到版本号自动升级的正确性。
这套实践我用了好几年,带过几个团队落地,效果是实打实的。代码审查时间缩短了,问题回溯速度快了,发版流程也自动化了。投入的成本就是每次提交多花三十秒,回报却是长期的。这笔账,怎么算都值。