尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

用Commitizen和commitlint建立可追溯的git提交规范

用Commitizen和commitlint建立可追溯的git提交规范 你接手过别人的项目打开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新增功能升 minorfix修复缺陷升 patchdocs仅修改文档不升版本style格式调整空格、分号等不升版本refactor重构不改功能不升版本perf性能优化不升版本视团队约定test增改测试不升版本build构建系统、依赖变更不升版本ciCI 配置变更不升版本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 statefix(api): handle timeout retry。scope 还承担着一个隐藏功能帮助评审者快速判断改动范围。一个提交如果 scope 是user却动了支付相关的文件评审时一眼就能发现异常。这比单纯靠人肉比对 diff 高效得多。2.4 subject 的黄金标准完成句子测试subject 是整个消息里最容易被写废的部分。很多人写fix user bug等于没写。Git 官方文档里给出过一个非常实用的检查方法把 subject 前面加上一句If applied, this commit will...如果应用了这个提交它将……读起来是否通顺。按这个测试来验证提交信息完整句子是否合格fix user bugIf applied, this commit will fix user bug不合格user bug指代不明fix login button not respondingIf applied, this commit will fix login button not responding合格清晰描述问题update docsIf applied, this commit will update docs不合格太笼统docs: clarify installation steps for WindowsIf 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: #88GitHub 等平台检测到后会在这个提交合并时自动关闭对应 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.jsmodule.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 } }它的运行逻辑完全依赖提交信息里的 typefix提交会提升 patch 版本号。feat提交会提升 minor 版本号。BREAKING CHANGE:标记会提升 major 版本号。假设当前版本是 1.3.0git 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 --grepfix(login) # 查某个需求相关的所有提交假设 scope 是 order git log --oneline --grep(order) # 查两个版本之间所有功能新增 git log --oneline v1.4.0..v1.5.0 --grepfeat # 查引入某个文件的变更记录 git log --oneline --follow -- src/utils/format.ts这些命令在排线上问题时非常管用。我之前接到一个线上 bug用户说某个数据导出功能坏了。我先git log --grepfix(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.jsoncommitizen 的 config 字段可能出现在子包而不是根包里。用指导.czrc文件放在仓库根目录让它对各子包统一生效。6.2 commitlint 报了错误但提示信息看不懂commitlint 的报错类型里我遇到最高频的是这几种报错内容原因解决方案subject may not be emptysubject 为空或者在冒号后没有加空格确保格式为type(scope): subject冒号后必须有一个空格type must be lower-casetype 写成了大写开头统一用小写Feat:是错的feat:才对header must not be longer than 100 charactersheader 超长精简 subject或者调整 commitlint 配置里的header-max-lengthfound 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 写成revertsubject 带上被回滚的那条原始提交的 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 这条链路串起来。配置就是那几个文件真正需要花心思的是让每个提交都逻辑独立、描述清晰——这跟写代码本身一样是一门值得下功夫的手艺。
返回列表