前端项目做了两年多,我对"Git 提交信息"这件事的态度,经历了从"随便写写"到"认真较劲"的转变。转折点是有一次上线前要回滚某个功能,我盯着git log里满屏的fix bug、update、change,完全分不清哪个提交对应哪个改动,只能一个文件一个文件地去翻 diff,那天加班到深夜。后来我花了点时间把提交信息的规范和简写格式研究透,才发现这件事根本不玄乎——它就是一套固定的"填空模板",加上几条写 comment 的纪律。这篇文章就把这套东西完整地梳理一遍,从格式拆解到落地配置,再到团队协作时的实操技巧,希望能帮你少走我走过的弯路。
1. 满屏"fix bug"的提交历史,回滚时是真的会骂人
先说一个比较扎心的事实:绝大部分人讨厌 Git 提交信息,并不是因为"不会写",而是因为"没吃过亏"。刚学 Git 的时候,老师只教了git add、git commit -m "xxx"、git push,没人告诉我们-m后面那句话到底该怎么写才算合格。于是"xxx"就变成了随心所欲的表达——提交时有多痛快,日后排查时就有多痛苦。
我在项目里见过很多典型的提交信息,这里列几类:
update、update file、update again——这类是最多的,信息量几乎为零,只能看出"改过了",看不出改了什么、为什么改。fix bug、fix a bug、fix the bug——像是给完全陌生的人留言,这个 bug 是哪里的 bug?怎么修的?影响哪个模块?全都没有。搞定、改好了、test、1、s——属于情绪化表达,只有当事人当下明白,过两周自己都未必还记得。merge branch xxx、Merge pull request #123 from xxx——这种是工具自动生成的,虽然信息明确,但缺失了类型语义,后续筛选用处有限。
这些提交信息叠加在一起,会产生三个非常现实的连锁问题。
第一是回滚困难。假设你的项目上线后出问题了,需要回滚某个模块的改动。你把提交历史翻出来,看到的全是update和fix,你根本不知道哪一次提交引入的功能对应现在线上要回滚的东西。只能挨个git show commit-id看 diff,代码量一大,这个排查过程会持续几小时,效率极低。
第二是版本变更记录缺失。正规一点的团队做版本发布,往往需要一份 Changelog(变更记录),告诉使用者"这个版本新增了什么、修复了什么、哪些改动不兼容旧版本"。如果提交信息都是自由发挥,Changelog 就只能靠人工从一堆 commit 里"考古"。但如果提交信息遵循统一格式,Changelog 完全可以由工具自动生成。
第三是Code Review 效率下降。代码评审的时候,评审人第一眼看的往往不是 diff,而是 commit message。一条好的提交信息能帮评审者快速建立上下文:这次改动属于"新增功能"还是"修 bug"?设计上有没有重大调整?如果提交信息含糊,评审人就会被逼着去读完整段代码,才能猜出你的意图,沟通成本立刻上来了。
所以,规范化提交信息本质上不是为了"好看",而是为了给未来的自己和其他协作者降低信息检索成本。它跟写代码注释是一个道理——注释是写给下一个维护者看的,提交信息则是写给"下周的自己和三个月后的同事"看的。
2. 一次规范提交信息到底长什么样:拆开 Conventional Commits 看
这里说的"规范化简写格式",其实就是开源社区已经总结好的Conventional Commits(约定式提交)规范。它不是什么高深的发明,而是一套被大量项目验证过的提交信息写法。这套规范在 Angular 项目的提交风格基础上提炼而来,现在很多现代工程(包括一些知名框架和工具链)都在用。
先看一个标准样例:
feat(login): add phone number login support Implement the phone number login flow, including the SMS code validation and the fallback to email login when the phone is not registered. Closes #123拆开来看,它由四部分构成:
- type(类型):说明这次提交的性质,比如 feat 表示新功能,fix 表示修 bug。
- scope(可选,作用范围):放在括号里,表示这次改动影响的模块、目录或领域,比如 login、cart、router。
- subject(主题描述):冒号加空格后的一句话,简洁说明做了什么。
- body(正文,可选):空一行后开始写,解释"为什么这样做",而不是重复"做了什么"。
- footer(脚注,可选):放置不兼容变更说明(BREAKING CHANGE)或关联的 issue 编号。
对于日常提交,绝大多数人只需要用到"类型 + 范围 + 主题"这三部分,也就是标题里说的"简写格式"。一行就能写完,例如fix(cart): correct total price calculation with coupon。
这里有一个关键细节:冒号后面必须有一个空格。这个空格看起来微不足道,但如果团队后续接入了自动检查工具(比如 commitlint),少了这个空格就会被判定为格式不合法。
再来说说 subject(主题)的写法。这部分的黄金法则是:用祈使句,简洁到本人一眼能看懂。中文团队可以写中文,英文团队建议统一用英文,但无论哪种语言,都要遵守"对象 + 动作 + 关键对象"的结构。
我给自己定了几条"土规矩":
- 不超过 50 个字符(英文),中文建议不超过 20 个字。这跟写邮件标题的心理一样,长了没人看。
- 动词开头,用"add""fix""update""remove""refactor"这类具体动词,避免 "update" 这种万能词。比如要表达"增加了登录功能",用
feat: add login feature就比update login清楚得多。 - 不要写"和""并"这种连接词。如果一条提交里同时干了三件事,说明你应该拆成三条提交。一次提交只解决一个问题,这是规范化的底层原则。
- 不需要句号。提交信息的标题不是完整句子,加上句号反而显得累赘。
body(正文)什么时候写?我的经验是:凡是改动的背景信息无法从代码本身看出来的,就必须写。比如你改了一个算法,代码里的函数名能告诉别人"做了什么",但不会告诉别人"为什么放弃原来的方案",这时候 body 就显得格外重要。body 不要求长,两三句话把背景讲清楚就行。有些项目还会要求在 body 里写测试描述或者回滚注意事项,这些都是团队的约定,可以不那么死板。
footer(脚注)最常见的用法是关联 issue 编号。比如Closes #123配合 GitHub/GitLab 的规则,能实现"合并提交后自动关闭对应 issue"的效果,这对项目跟踪帮助很大。另一个重要用法是标记"不兼容变更"(BREAKING CHANGE),一旦出现这种标记,工具就可以据此生成 major 版本的变更记录。
3. 九种 type 怎么选:别把 refactor 和 fix 混成一锅粥
Conventional Commits 规范里定义了若干 type,但实际工作中真正高频用到的也就十来个。我根据自己的真实使用体验,按"必须会用""偶尔使用""尽量避免使用"三个层级做个梳理。
先说必须会用的:
feat:新增功能。比如新的接口、新的页面、新的交互逻辑。凡是用户或外部系统可感知的能力增强,都用 feat。fix:修复缺陷。比如某个接口返回错误、页面样式错乱、计算逻辑有误。核心是"bug 消失了"。docs:文档变更。README、API 文档、项目 wiki、代码注释的调整,统一走 docs。refactor:重构。不改变外部行为,只改变内部实现。比如把一段函数拆成多个小函数、把类改成函数式写法、优化模块之间的依赖。注意:如果重构顺带修了一个 bug,请拆成两条提交,因为 refactor 和 fix 混在一起,后续回溯时很难判断这个改动到底有没有行为变化。test:测试相关。新增测试、修改测试、修复测试用例,都归这里。很多团队对测试单独提交特别有好感,因为测试相关的提交合并/回滚时可以整体处理。chore:杂务。构建流程调整、依赖包升级、配置文件修改,这些不直接影响业务代码的改动都算 chore。比如把构建工具从 gulp 换成 vite,就是chore(build): migrate from gulp to vite。
再说偶尔会用但容易用错的:
style:这是最容易误解的一个 type。它不指 UI 样式的调整,而是指代码格式层面的改动,比如加不加分号、缩进用空格还是 Tab、调整行尾空白。也就是说,style对应的是eslint --fix或格式化工具的处理结果。改按钮颜色、调布局这种 UI 变更,本质上是功能或修复,应该用feat或fix。perf:性能优化。比如接口响应从 2 秒降到 200 毫秒,属于用户体验改进,但因为它的专项性很强,单独立个 type 方便日后统计性能专项的投入。ci:持续集成相关。改动 .github/workflows、Jenkinsfile、GitLab CI 配置这类内容时使用。在 CI 配置改动和实施代码改动分开提交的团队里,这个 type 很常见。
还有一类叫revert(回滚提交),它通常不是手写的,而是执行git revert命令时由 Git 自动生成。规范的提交信息列举里会包含 revert,因为它本身是特殊的一类操作。
这里说一个我自己的判断标准:当你不确定该用哪个 type 时,先想"这个提交进入 Changelog 后,会出现在哪个标题下"。Changelog 通常按 Features、Bug Fixes、Documentation、Performance Improvements 等分节,你用的 type 直接决定了改动出现在哪个 section。有了这层逻辑,选择就不难了。比如"把登录按钮从蓝色改成绿色"这种几乎称不上功能的东西,我会归入style(如果纯粹是视觉层面微调)或干脆归入chore,避免污染 Features 列表。
给一份我项目里实际使用的 type 速查表,方便你贴到团队文档里:
| type | 含义 | 典型场景 | 是否进入 changelog |
|---|---|---|---|
| feat | 新功能 | 新增接口、页面、能力 | 是 |
| fix | 修复 bug | 修正错误行为 | 是 |
| docs | 文档改动 | README、注释、API 文档 | 是 |
| style | 格式调整 | 缩进、空格、分号 | 否 |
| refactor | 重构 | 内部实现调整,行为不变 | 否 |
| perf | 性能优化 | 提速、降内存、降带宽 | 是 |
| test | 测试相关 | 新增/修改/修复测试 | 否 |
| build | 构建系统 | 打包配置、依赖工具 | 否 |
| ci | CI 配置 | 流水线脚本、CI 平台配置 | 否 |
| chore | 杂务 | 依赖升级、配置文件 | 否 |
4. 简写不是偷懒:commit 命令、alias、模板三件套
讲完格式,回到标题里的重点——简写格式。这里的"简写"有两层含义:一层是信息内容的精简表达,另一层是操作层面的快捷输入。前者靠写 comment 的纪律,后者靠工具配置来保障。
很多人在git commit上浪费了大量时间,主要原因是把命令写得太繁琐、太随意。有一次我看到同事提交的 message 是git commit -m "fix data processing bug in report module, the issue is cause by the timezone conversion, we need to unify the timezone before calculation, also update the test case"。这其实是一条含金量很高的提交信息——类型、范围、原因都有了——但用-m写这么长,回车之后换行、格式都不好控制,阅读体验很差。
更合理的"简写"姿势是分场景使用:
- 极简场景:一条小改动,一句话能说清楚,用
git commit -m "fix(login): correct redirect after login"。 - 一般场景:需要写 body 解释背景,我会直接用
git commit(不带 -m),让编辑器(通常是 vim)打开一个空白文件,按规范格式填写。这里有一个技巧:git commit时会自动把注释模板放进来(比如# Please enter the commit message for your changes.),但这些注释会干扰 vim 编辑,可以在~/.gitconfig里关掉:
然后在[commit] template = ~/.gitmessage.txt~/.gitmessage.txt里预填一个提交模板,比如:
这样每次<type>(<scope>): <subject> <body> <footer>git commit自动打开这个模板,你只需要填空。这是我认为最优雅的"简写":不用记任何格式,按行填内容就行。 - 带注释多行提交:如果你习惯了 shell,也可以用
git commit -m "type: subject" -m "body content",多个-m会拼接成多个段落,但用这种方式无法方便地写 footer 和妥善处理空行,所以我不太推荐用在规范要求的场景。
再来说 alias(命令别名)。简约不能牺牲效率,所以我会把高频命令缩短成自己的"快捷键",比如:
git config --global alias.cm "commit -m" git config --global alias.ca "commit --amend" git config --global alias.lg "log --oneline --graph --decorate -20"配好之后,日常提交只需要敲git cm "fix(xxx): xxx",补提交用git ca,查看历史用git lg。这个操作本质上是把"简写"落实到命令层面。
这里插一个与"简写"密切相关的小习惯:给提交信息打上类型标签后再写主题。我见过最快也最实用的提交写法是:先想这个改动算什么"类型"(feat? fix? docs?),然后写一个 5 到 10 个词的主题,再用 1 到 3 句话写背景 body。整个过程不超过 1 分钟。比如你要修一个"购物车在优惠券叠加时总价算错"的问题,提交信息可能是:
fix(cart): correct total price when applying multiple coupons The previous logic used the discounted price as the base for each coupon applied in sequence, which changed the final result depending on the order. Now all coupons are applied to the original price.这样的提交信息,在git log --oneline下看是fix(cart): correct total price when applying multiple coupons,一目了然。点进详情看 body,又知道"为什么之前是错的",排查效率直接翻倍。
所谓的"简写格式",我的理解就是:格式上简化为三段式(type + scope + subject),正文按需补充,绝不为了简而丢掉关键信息。
5. 团队落地:commitlint + husky 让工具替你盯格式
个人用了规范,团队怎么落地?这是最现实的问题。你要是只发一篇文章让大家"写规范点",大概率两周后就回归原样了。真正的解法是:把格式检查嵌入到提交流程里,不合格的提交信息直接在源头拦下。
我在前端项目里推荐一套轻量组合:commitlint + husky。commitlint 负责检查提交信息是否符合 Conventional Commits 规范,husky 负责在 Git 钩子(commit-msg)里触发检查。
先说环境准备。如果你的电脑还没装 Git,那第一步是去官网下载对应系统的安装包,或者用包管理器安装:macOS 可以用brew install git,Ubuntu/Debian 可以用apt install git,Windows 推荐 Git for Windows。装完之后一定要先做两件事:设置user.name和user.email,否则提交信息里显示的用户名可能是一串乱码,或者干脆提交失败。
git config --global user.name "Your Name" git config --global user.email "you@example.com"这两行配置和提交信息规范直接相关,因为规范化的提交信息除了格式,还需要"谁提交的"这个信息是完整准确的。很多新人忽略这个步骤,导致项目里出现大量"未知作者"的提交,后面做责任追溯极其麻烦。
然后在新项目里初始化工具链:
# 在仓库根目录 npm init -y npm install --save-dev @commitlint/cli @commitlint/config-conventional husky npx husky install npx husky add .husky/commit-msg "npx --no -- commitlint --edit $1"接着创建commitlint.config.js:
module.exports = { extends: ['@commitlint/config-conventional'], };config-conventional是 commitlint 官方提供的预设规则,足够覆盖大部分团队的默认需求。如果你想自定义 type 列表、scope 范围或 subject 长度限制,可以这样扩展:
module.exports = { extends: ['@commitlint/config-conventional'], rules: { 'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore', 'revert']], 'subject-max-length': [2, 'always', 50], 'scope-enum': [0, 'always', []], // 关闭 scope 枚举限制 }, };配置好之后,团队里任何人只要提交信息不符合规范,git commit就会被阻断,并提示错误。这就把"靠自觉"变成了"靠机制"。大家写坏 commit 的概率会大幅下降,因为第一关就过不去。
如果说上面是"防守",那"进攻"层面还有一招:用 standard-version 自动生成 Changelog。当你把 commit 都按规范书写后,执行npx standard-version,它会读取提交历史,自动生成一份按版本号归档的 CHANGELOG.md。这正是前面说的"规范带来的红利"。版本发布前,你只需要看一下 Features 和 Bug Fixes 两个 section 的条目,就能快速决定当前版本是 minor 还是 patch,要不要发 major(因为有 BREAKING CHANGE)。
工具链落地的关键,是要让团队成员尝到甜头。我自己的经验是:先把规范带来的收益讲清楚(回滚更快、Changelog 不用手写),再辅以工具检查,推行阻力会小很多。如果一上来就强推,很多人只会觉得"又多了一条规矩",效果反而不好。
6. 分支合并、SSH 认证与提交信息纠缠不清的那些小事
规范提交信息写好了,后续的 Git 操作如果不了解几个常见细节,还是会踩坑。这里把几个高关联的场景串一遍,都是团队协作时的高频问题。
第一个场景:分支合并时的提交信息污染。
git merge默认会把两个分支的分叉点生成一条Merge branch 'xxx' into main的合并提交。这类提交本身语义上没有大问题,但如果团队用 rebase 合并到主干,情况就不同了:rebase 会把分支上的 commits 逐个重放到主干之上,期间如果你有连续的几个杂乱提交,它们不会自动合并。于是有人会先在分支上git rebase -i,把多个小提交squash成一条规范的 feat 提交,再合入主干。我的建议是:任何分支合并进主干之前,用git rebase -i把自己的提交整理一遍,确认每条提交都符合"一次提交一个意图"的规范,再执行老板式的git merge --no-ff。--no-ff的好处是保留合并现场,让提交历史上能看到"这条分支合进来"的痕迹。
第二个场景:改写提交信息时别影响合作者。
git commit --amend很好用,但它只适合"这个提交还没推给别人的时候"。如果你已经git push了,再 amend 并且 force push,会重写远端历史,别人的本地分支就会错乱。所以我的纪律是:推送过的提交不轻易改。如果实在要改,提前和所有人打招呼,并明确告知git pull --rebase是他们的恢复手段。规范化是一个长期过程,中途有几条烂提交,完全可以用后续的提交来"弥补式"解释,不必为了完美重写历史。
第三个场景:SSH 认证失败,push 不上去,提交信息白写了。
这个场景现实中很常见:提交信息写好了,推不上远端,提示Permission denied (publickey)或ssh: connect to host github.com port 22: Connection timed out。首次配置 SSH key 的完整步骤是这样:
# 1. 生成密钥对(用你的邮箱替换) ssh-keygen -t ed25519 -C "you@example.com" # 2. 查看公钥内容并复制 cat ~/.ssh/id_ed25519.pub # 3. 到代码托管平台(GitHub/GitLab/Gitee)的 SSH keys 页面粘贴 # 4. 验证连接 ssh -T git@github.com验证通过后,把本地仓库的 remote 地址改成 SSH 格式(git@github.com:user/repo.git),推送就不会再遇到认证问题。配置 SSH 本身不是提交信息规范的一部分,但它们共同决定了"你最终能否顺利把规范提交发布出去",所以放在一起提一嘴。
还有一个容易被忽略的点:提交模板配合分支命名。如果团队里分支叫feature/login或fix/cart-price,那么提交信息里的 scope 就可以直接对齐分支名,比如feat(login): ...、fix(cart): ...。这样每次开分支时,就已经想好了 scope,写提交信息只是顺水推舟。
最后再说一个小技巧:如果你在写提交信息时频繁卡壳,说明改动本身就太大了。一条规范提交写不出来,往往不是"不会写",而是"改动太杂"。这时候最优解是拆提交:git add -p按 hunk 暂存不同的改动,分开提交,分别赋予不同类型。这个习惯坚持久了,提交信息会越来越简洁,代码评审也会越来越轻松。
这条规范说到底,真正解决的是"记忆容量"问题。代码仓库承载了几百上千次改动,人的大脑记不住每一次的前因后果,那就把前因后果写进提交信息里,让 Git 帮你记住。短期的确会多花十几秒,长期看,省下来的回滚、排查、发版时间,是那十几秒的百倍千倍。我自己实践了半年,最大的变化是:再也不用靠git log -p大海捞针了,打开git log --oneline就能准确找到想要的那一次改动。这份安心感,值得每个 Git 用户拥有。