你接手过别人的项目,打开git log --oneline一看,满屏都是fix、update、bug fix,甚至还有asdf、111这种随手敲的提交。你根本不知道哪个提交对应哪个需求,也不知道哪个改动引入了回归。我经历过太多次这种"提交考古"现场,后来才明白,Commitizen这类工具加上一套明确的git提交规范,治的不是"提交信息不好看"这种表面问题,治的是"团队协作效率低下、回溯成本高"这种内伤。
这篇文章我会从 Commitizen 的实际定位讲起,把 Conventional Commits 规范里的字段逐一拆开,再讲清楚 commitlint、husky、changelog 生成这些配套链路怎么接。不管你是刚入行的新人,还是要带团队的同学,按着文章里的思路和配置文件走一遍,基本就能在项目里把提交规范立起来。
1. Commitizen 不是格式检查器:先把工具的边界搞清楚
很多团队一聊 git 规范,第一反应就是"装个 Commitizen 就行了"。这句话只说对了一半。我见过好几个项目装了 Commitizen,提交信息依然五花八门,原因就是大家把它当成一个"自动纠错器"来用了。实际上,Commitizen 的本职工作是交互式生成提交信息,它不负责校验,更不负责纠错。
1.1 它真正做的事:用提问代替手打
Commitizen 的核心机制很简单:它把git commit这个命令包装成git cz,执行后不会直接打开编辑器让你写提交信息,而是一个问题一个问题的问你——这次提交属于哪个类型?影响范围是哪个模块?简短描述写什么?详细说明写不写?有没有破坏性变更?
你回答完这些问题,它会按照你在适配器里定义的格式,把答案拼装成一条规范的提交信息,然后替你交给 git 完成提交。关键在于"替你拼装"这一步,它保证生成的提交信息在结构上是统一的。
这套设计的聪明之处在于,它把"规范"从"每个人都得背诵格式"变成"工具引导填写"。新人入职第一天不用背 type 列表,跟着提示走就不会错。但这同时也意味着,如果你用git commit而不是git cz提交,Commitizen 完全管不到你——它只是一个壳,不是一个钩子。
1.2 规范链路的完整拼图
要说清楚 Commitizen 的位置,得先给整条链路分个角色。我画一个职责清单,你一看就明白:
| 组件 | 职责 | 触发时机 |
|---|---|---|
| Commitizen | 交互式生成提交信息 | 用户主动执行git cz时 |
| 适配器(adapter) | 定义提问内容与输出格式 | Commitizen 加载时 |
| commitlint | 对提交信息做机器校验 | git 提交时(commit-msg 钩子) |
| husky | 挂载 git 钩子 | git 事件触发时 |
| conventional-changelog | 解析提交信息生成变更日志 | 发布时 |
这里有个极易混淆的认知:刚才表格里的适配器,才是真正决定"提交格式长什么样"的东西,Commitizen 本身不内置任何格式。最常见的适配器是cz-conventional-changelog,它把问题映射到 Conventional Commits 规范的字段上。还有很多团队用cz-git,这个适配器交互体验更好,支持搜索和历史选择,我在后面落地部分会细说。
还有个点必须强调:校验这条线是靠 commitlint 完成的,不是 commitizen。Commitizen 只保证"用我生成的提交是规范的",不保证"你绕过我用原始命令提交也规范"。所以真正落地的时候,必须把 husky 和 commitlint 接上,在 commit-msg 钩子阶段做硬性拦截,双管齐下才能堵住漏网之鱼。
1.3 到底什么样的团队需要这套东西
先泼一盆冷水:个人玩的开源项目、就一个人维护的内部小工具,不一定需要 Commitizen。提交信息写得再乱,你自己半年内凭git diff也能找回上下文。但当项目出现下面几种信号,就该把规范提上议程了:
- 团队成员超过 5 人,提交历史开始变得混乱,没人能说清某个版本到底改了什么。
- 项目需要对外发布,要生成 CHANGELOG,或者要基于提交信息做自动化版本号管理。
- 代码评审(Code Review)已经常态化,PR 描述和 commit message 经常对不上。
- 你发现自己频繁用
git log --grep搜提交,却搜不到想要的结果。
满足其中任何一条,Commitizen + 规范提交就不是"锦上添花",而是"基础设施"了。
2. Angular 提交规范逐字段拆解:一条提交信息其实是一个结构体
Conventional Commits 规范最早从 Angular 团队的提交约定里提炼而来,现在已经成为社区里事实上的提交标准。它把原本自由发挥的一段纯文本,拆解成header / body / footer三个区域,其中 header 又由type(scope): subject三部分组成。理解这套结构,是后面所有自动化的前提。
2.1 一次规范提交的完整长相
先看一条符合规范的提交信息长什么样:
feat(login): add phone number login support The login page now accepts phone number and sends a one-time password. Existing password login remains unchanged. Closes: #88 BREAKING CHANGE: `loginWithPassword` now requires a `secondFactor` param一行一行的解释:
feat是 type,表示"新增功能"。login是 scope,表示影响范围是登录模块。add phone number login support是 subject,是对本次改动的一句话描述。- 空行后面的段落是 body,记录更详细的背景和说明。
- 最后的
Closes: #88和BREAKING CHANGE:属于 footer,前者关联 issue,后者标记破坏性变更。
Header 部分用单行写完,推荐不超过 50 个字符;body 区每行不超过 72 个字符。这个长度约定不是因为强迫症,而是为了在git log --oneline紧凑视图里不折行,在终端和网页端都能完整可读。
2.2 type 为什么是名词,而不是动词
规范里 type 必须是feat、fix、docs这种名词形式,很多人没想过为什么。其实这是为了让提交信息可以像"事件记录"一样被机器消费。如果 type 是随意动词,下游的 changelog 分组、语义化版本号计算、自动化发布规则全都没法稳定匹配。
我整理了一份常用 type 清单,连"是否影响版本号"也标了出来:
| type | 含义 | 对语义化版本的影响 |
|---|---|---|
| feat | 新增功能 | 升 minor |
| fix | 修复缺陷 | 升 patch |
| docs | 仅修改文档 | 不升版本 |
| style | 格式调整,空格、分号等 | 不升版本 |
| refactor | 重构,不改功能 | 不升版本 |
| perf | 性能优化 | 不升版本(视团队约定) |
| test | 增改测试 | 不升版本 |
| build | 构建系统、依赖变更 | 不升版本 |
| ci | CI 配置变更 | 不升版本 |
| chore | 杂务,不修改 src 和 test | 不升版本 |
| revert | 回滚某个提交 | 不升版本 |
这里最容易出问题的是style和refactor的边界。style只管格式层面——加个空格、去掉多余分号、调整缩进。一旦你改了变量名、抽了函数、拆了模块,哪怕功能完全没变,也该用refactor。反过来,chore是"什么都不是但又得提交"的兜底类型,改.gitignore、更新依赖锁定文件、调编辑器配置,都可以扔进去。判断标准就一条:常人看到这个提交,会不会想"这跟业务代码有什么关系",会,就多半是 chore。
2.3 scope 怎么定才不变成摆设
scope 是圆括号里那个字段,用来标注影响范围。它的威力在大型项目里才能显示出来。我见过最好的实践,是把 scope 当成模块词典来维护,而不是随手乱写。
举个例子,一个电商项目可以把 scope 定为user、order、cart、payment、admin这几个稳定模块。提交时严格从里面选,不要今天写user,明天写user-center,后天写usercenter。这样git log --grep "fix(payment)"才能一次把支付相关的修复全捞出来。如果你们是 monorepo 结构,scope 直接填包名效果最好,比如feat(ui): add button loading state,fix(api): handle timeout retry。
scope 还承担着一个隐藏功能:帮助评审者快速判断改动范围。一个提交如果 scope 是user,却动了支付相关的文件,评审时一眼就能发现异常。这比单纯靠人肉比对 diff 高效得多。
2.4 subject 的黄金标准:完成句子测试
subject 是整个消息里最容易被写废的部分。很多人写fix user bug,等于没写。Git 官方文档里给出过一个非常实用的检查方法:把 subject 前面加上一句"If applied, this commit will..."(如果应用了这个提交,它将……),读起来是否通顺。
按这个测试来验证:
| 提交信息 | 完整句子 | 是否合格 |
|---|---|---|
fix user bug | If applied, this commit will fix user bug | 不合格,"user bug"指代不明 |
fix login button not responding | If applied, this commit will fix login button not responding | 合格,清晰描述问题 |
update docs | If applied, this commit will update docs | 不合格,太笼统 |
docs: clarify installation steps for Windows | If applied, this commit will clarify installation steps for Windows | 合格 |
用祈使句、现在时态开头(add、fix、update、remove),句末不要加句号。这套写作原则跟中文技术文档写作规范里强调的"操作指令要用动词开头"是一个道理。写 subject 的时候,把未来的自己当成读者,那个三个月后要回查历史的你,会感谢现在认真写提交信息的你。
2.5 body 和 footer:决定提交信息能不能驱动自动化
body 不是必填项,小修小改可以不写。但涉及复杂改动时,body 里至少要交代两件事:为什么这么改,以及改动的关键取舍。不要复述代码本身——代码 diff 已经说明"改了什么",body 该回答的是"为什么这样改"。
footer 有两个场景特别值得注意。第一个是 issue 关联,在消息里写Closes: #88,GitHub 等平台检测到后会在这个提交合并时自动关闭对应 issue,省掉手动关闭的动作。第二个是BREAKING CHANGE:,它放在 footer 区域顶格写,后面跟上破坏性变更的说明。这个标记不是写给人看的——下游的语义化版本工具检测到它,会自动把下一个版本判定为 major 版本。
3. 从 git cz 到 commitlint:交互式生成与机器校验如何衔接
前面讲清楚了规范本身,这一步落到实操。我按"生成端 + 校验端"两条线讲,你照着配就能跑通。
3.1 第一步:把 Commitizen 和适配器装好
最直接的安装方式,是把 commitizen 作为项目开发依赖装进去,再配上适配器。我推荐cz-git,它的交互体验比传统的cz-conventional-changelog好不少,支持中文提示、上下键选择、输入关键字过滤,还能记住你上次选的类型。
先在项目里安装:
npm install --save-dev commitizen cz-git然后在package.json里配置 commitizen 的适配器路径:
{ "config": { "commitizen": { "path": "cz-git" } }, "scripts": { "commit": "cz" } }接着在项目根目录创建.czrc,或直接在package.json里配置 cz-git 的个性化选项。一个最简配置长这样:
{ "types": ["feat", "fix", "docs", "style", "refactor", "perf", "test", "build", "ci", "chore", "revert"], "scopes": ["user", "order", "payment", "common"], "maxHeaderLength": 100, "scopeOverrides": { "fix": ["urgent-fix"] } }配置完成后,用npm run commit或npx cz就能启动交互式提问。你每答一个问题,它都会实时显示拼好的提交信息预览,确认后直接提交。这一步走通,你已经拥有了"生成端"的能力。
3.2 第二步:用 commitlint 收紧提交入口
生成端解决"怎么写规范",校验端解决"不规范的怎么拦下来"。commitlint 就是干这个的。先装依赖:
npm install --save-dev @commitlint/cli @commitlint/config-conventional在项目根目录创建commitlint.config.js:
module.exports = { extends: ['@commitlint/config-conventional'], rules: { 'header-max-length': [2, 'always', 100], 'subject-case': [0], 'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore', 'revert']] } };这里我关掉了subject-case,因为中文提交信息经常带大写字母或专有名词,默认规则会严格要求小写开头,容易误伤。type-enum和 cz-git 配置里的类型清单要保持一致,两边不应出现互相矛盾的情况。
但光有 commitlint 配置还不够,它默认不会自动运行。需要一个 git 钩子在 commit-msg 阶段叫醒它。这一步我用 husky 来实现。
3.3 第三步:husky v9 挂载钩子
husky 在 v9 之后的配置方式跟早期版本差异很大,新项目直接按新方式走。先初始化:
npx husky-init npm install然后添加 commit-msg 钩子:
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'执行后,.husky/commit-msg文件内容大概长这样:
npx --no -- commitlint --edit "$1"从这之后,不管你是用git commit还是git cz提交,commit-msg 钩子都会触发,校验失败 git 会直接拒绝本次提交。很多团队还会配合 lint-staged 在 pre-commit 钩子里先跑一遍代码检查和格式化,串起来的流程是:暂存文件先过 lint,提交信息再过 commitlint,两道关卡都通过,提交才算成功。
3.4 绕过钩子的几种情况,提前想好对策
第一,有人会用git commit --no-verify绕过钩子。这是 git 的合法逃生门,拦不住也没必要拦。但 CI 服务器上可以再加一道服务端校验——在 CI 跑一个commitlint检查当前分支里所有新增提交,不合格就打回。这样即使本地绕过了钩子,合并主分支时也会被拦下来。
第二,IDE 内置的 Git 面板一般不会启动交互式git cz,用户直接从面板提交的话,会走到 commit-msg 钩子校验,但生成的提交信息完全靠手写。解决办法是把 commitlint 规则写好,让手写的人也能被引导。你可以在项目 README 里贴一条"标准提交示例",让习惯用 IDE 面板的人直接照着抄。
第三,Windows 环境要注意 npm 脚本的路径分隔符问题,husky在跨平台上兼容性没问题,但不要用绝对路径去引用 npx 工具,统一用npx --no --前缀最稳妥。
4. changelog 与版本号跟着提交信息走:规范在项目里的复利
提交信息一旦结构化,它能驱动的东西远超你的想象。最直观的收益就是 changelog 自动生成和版本号自动提升。这一步我会用一个实际的例子讲清楚"提交信息如何变成发布原料"。
4.1 从提交历史到 CHANGELOG 的生成原理
conventional-changelog 生态里,standard-version是最容易上手的工具。它做三件事:根据提交历史自动升版本号、生成 CHANGELOG.md、打 tag。先装:
npm install --save-dev standard-version在package.json里配置发布脚本:
{ "scripts": { "release": "standard-version", "release:minor": "standard-version --release-as minor", "release:major": "standard-version --release-as major" } }它的运行逻辑完全依赖提交信息里的 type:
fix提交会提升 patch 版本号。feat提交会提升 minor 版本号。BREAKING CHANGE:标记会提升 major 版本号。
假设当前版本是 1.3.0,git log里新合入了一条feat(user): add avatar upload,你执行npm run release,工具扫到这条 feat 提交,会直接把版本号推到 1.4.0,并在 CHANGELOG.md 里新增一节:
## [1.4.0] - 2025-06-20 ### Features - **user:** add avatar upload全程不用人肉回忆"这版本改了什么",提交信息就是发布说明的雏形。我第一次在一个有三个多月历史的项目上跑通 standard-version,生成的 changelog 里包含几十条提交记录,每一条都能对应到具体需求,那一刻才真正理解了"提交规范是自动化的前提"这句话。
4.2 用提交信息做问题回溯:git log 的高级用法
规范提交的另一个隐藏收益,是git log的检索效率大幅提升。以前搜提交靠肉眼翻屏,现在可以直接用表达式精准定位:
# 查所有登录模块的修复 git log --oneline --grep="fix(login)" # 查某个需求相关的所有提交(假设 scope 是 order) git log --oneline --grep="(order)" # 查两个版本之间所有功能新增 git log --oneline v1.4.0..v1.5.0 --grep="feat" # 查引入某个文件的变更记录 git log --oneline --follow -- src/utils/format.ts这些命令在排线上问题时非常管用。我之前接到一个线上 bug,用户说某个数据导出功能坏了。我先git log --grep="fix(export)"拉出相关提交,再用git blame定位到具体代码行,最后通过git show <commit>查看那次改动的完整 diff 和提交说明,整个过程不到十分钟。要是提交信息全是update、fix bug,这一步基本无能为力。
4.3 在测试联调规范里的位置
再往大了说,提交规范还能跟测试联调流程串成一条线。比如团队规定:fix类型必须附带对应的回归测试,feat类型必须更新接口文档或 mock 数据。这些规定写成规范文档是没人看的,但结合 commitlint 规则可以实现一部分自动化拦截。比如用 commitlint 的body-empty规则,强制要求带rename类提交必须写 body;更进一步,可以在 CI 里解析提交信息,遇到feat就触发测试环境自动部署,遇到fix就自动跑单测。这些做法没有标准答案,但底层依赖都一样——提交信息必须机器可读。这也是为什么我一直强调,规范提交不是给 git log 好看的,是给整个研发流程当数据源用的。
5. 团队落地时文档上不会写的三个细节
工具链配齐只是第一步,真正的难点是让团队每一个人都接受并坚持。这部分我分享三个在项目里实测下来的经验,全是踩过坑换来的。
5.1 先解决"不想写"的心态问题
我见过不少开发者的真实想法:提交信息就是个备注,写那么详细干嘛,反正代码能跑就行。这种心态靠培训是扭转不了的,要靠"痛感"扭转。我一般会在团队里做一次演示:挑一条很早以前的提交,让当事人说说当时为什么这么改。十有八九当事人都答不上来,围观的人也笑不出来——因为自己的提交也可能同样不可追溯。那次演示之后,团队对提交规范的态度明显认真了很多。
防御性的做法是把规范落到工具上而不是念叨上。人都会偷懒,工具不会。只要 commitlint 在 commit-msg 阶段做硬校验,写得再烂的提交也会被弹回去,多弹几次,团队自然就记住格式了。所以我对团队的要求从来只有一条:不要用 --no-verify 跳过钩子。其他的一切,交给工具和模板去引导。
5.2 scope 词典要有人维护
scope 写多了会失控。今天有人写user,明天有人写user-info,后天有人写账号,检索能力直接归零。我建议团队维护一个docs/commit-scope.md文件,或者直接放进代码仓库的 CONTRIBUTING.md 里,列出当前稳定的 scope 集合:
user:账号、登录、注册、个人资料。order:订单列表、详情、状态流转。payment:支付、退款、对账。common:跨模块公共组件和工具函数。
与此同时,commitlint 的 type-enum 规则只能约束 type,约束不了 scope。如果你们想硬性限制 scope 取值,可以在 commitlint 配置里加scope-enum规则:
'scope-enum': [2, 'always', ['user', 'order', 'payment', 'common']]这样乱写 scope 的提交会直接被拒绝。团队大了之后,这个约束能省掉很多沟通成本。
5.3 一个提交只干一件事
这条算是我个人最看重的规范,比 type 和 scope 都重要。刚写代码那几年,我经常一个提交里塞了三四个改动:修了一个 bug、重构了一个函数、顺手改了个样式。结果是以后想单独回滚某个改动,根本无从下手。正确做法是遵循单一职责原则,一个提交解决一个逻辑问题:
- 修复 bug 的改动单独提交。
- 重构代码单独提交。
- 调整样式单独提交。
用交互式 git add 可以很方便地实现分开暂存:
git add -p src/components/UserCard.tsx然后把同一主题的改动分批次提交,每个提交对应一条清晰的信息。这样做还有个额外好处:如果某个提交引入回归,你可以直接git revert <commit>精准回滚,不会连带影响其他正在开发的特性。
6. 容易踩的坑与排查思路
最后这部分写给那些"按教程配完还是有问题"的同学。以下坑我都踩过,按排查思路写,方便你对照。
6.1 git cz 不生效,输入后直接进入默认编辑器
最常见的原因是 commitizen 没被安装为全局命令,或者 npx 找不到本地依赖。分两种情况处理:
- 项目级安装:确保
node_modules/.bin/cz存在,执行npx cz而不是git cz。如果npx cz报找不到,回查npm install是否正常完成。 - 全局安装:
npm install -g commitizen,然后确认node全局 bin 目录在 PATH 里。全局安装能让你在任何仓库的终端下用git cz,但适配器必须按项目配置走,全局模式下要手动指定适配器路径,我这里还是建议一律用项目级安装,避免团队里每个人环境不一致。
还有一个隐蔽原因:如果你们项目用 monorepo 并且有多个 package.json,commitizen 的 config 字段可能出现在子包而不是根包里。用指导.czrc文件放在仓库根目录,让它对各子包统一生效。
6.2 commitlint 报了错误,但提示信息看不懂
commitlint 的报错类型里,我遇到最高频的是这几种:
| 报错内容 | 原因 | 解决方案 |
|---|---|---|
subject may not be empty | subject 为空或者在冒号后没有加空格 | 确保格式为type(scope): subject,冒号后必须有一个空格 |
type must be lower-case | type 写成了大写开头 | 统一用小写,Feat:是错的,feat:才对 |
header must not be longer than 100 characters | header 超长 | 精简 subject,或者调整 commitlint 配置里的header-max-length |
found 2 problems, 0 warnings | 多条规则同时不通过 | 逐条看输出的行号,通常第一个问题解决后后面的也会消失 |
我之前遇到一个团队,全员提交被拒,原因就是 IDE 自动把首字母变成大写,subject 全是Add xxx开头。这正是我在 3.2 节关掉subject-case规则的原因。不同团队有不同习惯,规则不要照抄,要根据实际报错微调。
6.3 "Commitizen 规范"其实是 Conventional Commits 规范
一个容易在搜索资料时踩的坑:很多人把"Commitizen 规范"理解为 Commitizen 这个工具自带的格式,其实它背后是独立的 Conventional Commits 规范。工具可以换,适配器可以换,但规范本身是稳定的。所以你搜资料的时候,搜Commitizen更多是工具配置问题,搜Conventional Commits才是规范本身的定义和讨论。
这还带出另一个时常见到的需求:团队已经用了某个严格的提交规范,但希望交互式工具也能按这套规范提问。此时不需要换掉 Commitizen,换一个适配器即可。比如有人在用@commitlint/cz-commitlint适配器,它能让交互式提问和 commitlint 规则共用同一份配置,两侧永远保持一致,我特别推荐给需要长期维护规范的老项目。
6.4 revert 提交别把它写坏
回滚提交的格式有讲究。用git revert <commit>自动生成的提交信息默认是一条Revert "xxx"开头的内容,但其实 Conventional Commits 规范里 revert 类型的推荐姿势是这样的:
revert: feat(user): add avatar upload This reverts commit xxxxxx.关键点是 type 写成revert,subject 带上被回滚的那条原始提交的 type 和 scope。这样做的好处是 changelog 工具能正确识别这是一个回滚,会在发布说明里单独列出。另外,如果被回滚的提交原本带了BREAKING CHANGE标记,回滚提交本身不需要重复标记,否则工具会在回滚时又触发一次 major 版本提升,造成版本号虚高。
还有一个 merge 提交的问题:merge类型并不在常规 type 列表里,很多团队直接在配置里忽略 merge 提交。代码合并产生的 merge commit 由工具自行生成,不经 commit-msg 钩子校验,这是正常现象,不用特殊处理。如果你们用 rebase 方式合入 PR,那 merge commit 不存在,commit-msg 钩子校验的是原始提交本身,这点在配置 CI 流程时要留意。
跟各种项目打交道这些年,我个人最大的体会是:提交规范的收益不在当下,而在三个月之后。当下你只是多花一分钟回答问题,三个月后回查历史、定位回归、生成发布说明时,它帮你省下的时间是以小时计的。如果你现在正被乱七八糟的 git log 折磨,别犹豫,照着这篇文章把 Commitizen、cz-git、commitlint、husky 这条链路串起来。配置就是那几个文件,真正需要花心思的,是让每个提交都"逻辑独立、描述清晰"——这跟写代码本身一样,是一门值得下功夫的手艺。