ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

commitlint与Husky实践:用Git钩子强制规范提交信息

commitlint与Husky实践:用Git钩子强制规范提交信息 如果你在一个多人协作的项目里待过半年以上大概率见过这样的提交信息fix、111、update、改bug、还有一版。刚写的时候没什么感觉等版本上线出了问题要回滚或者月底想整理一份变更日志你会发现根本不敢打开git log。这几乎是每个团队从“能跑就行”走向工程化时都会撞上的一堵墙。commitlint 就是用来拆这堵墙的工具。它的核心职责是在你执行git commit的时候按照项目里约定好的规则检查提交信息不符合规则就直接拦住。配合 husky 把这条检查挂到 Git 钩子上之后团队里不管老手还是实习生提交信息都会被统一约束成一种清晰、可读、有结构的格式。这篇文章我会从实际项目接入的角度把 commitlint 的安装、配置、与 husky 的集成、规则原理、常见坑一次性讲清楚。完全没有接触过 Git 钩子的同学跟着本文操作也能跑通已经在用但遇到疑难杂症的可以直接跳到第 6 章对照排查。1. 为什么要统一提交信息commitlint 到底解决了什么问题很多刚接触工程化的人会觉得提交信息而已随便写写不就行了工作流也没断代码也没丢。但真到用的时候才明白提交信息是项目历史最重要的“人肉索引”。今天你要查“登录页的 bug 是哪次提交改出来的”一条规范的提交信息能让你十秒钟定位一条fix只能让你把整个分支都翻一遍。到了发版阶段很多团队还要手写 CHANGELOG如果提交信息乱七八糟这项工作基本等于考古。commitlint 解决的就是这个“提交信息治理”问题。它不关心你写得好不好只关心你写的是否符合规则然后把不合格的提交挡在门外。也就是说它强制团队把“写清楚提交信息”变成一种纪律。1.1 Conventional Commits 规范速览commitlint 本身只是一个检查引擎真正管规矩的是你选择的规则集。目前社区用得最广的规则集就是commitlint/config-conventional它遵循的是 Conventional Commits 规范。这套规范的核心格式很好记完整提交信息长这样type[optional scope]: subject [optional body] [optional footer(s)]日常使用中我们只需要写第一行就够也就是 header 部分。常见的 type 如下表所示type说明feat新增功能fix修复Bugdocs仅文档变更style不影响代码运行逻辑的格式调整refactor重构不是新功能也不是修Bugperf性能优化test增加或调整测试build构建系统或外部依赖变更ciCI配置或脚本变更chore其他日常杂项revert回滚某次提交scope 是可选的作用域一般写模块名比如feat(user-center): add login record。scope 后面必须跟一个冒号和空格然后再写主题。1.2 commitlint 和 husky 在流程中扮演的角色这里有一个关键点commitlint 本身不会主动拦截你的提交它只是一个命令行工具。要让它在git commit时自动运行需要 Git 钩子来触发。Git 提供了很多钩子其中commit-msg钩子会在用户输入提交信息之后、提交成功之前触发。我们需要在这个钩子里调用 commitlint把本次提交的信息交给它校验如果校验失败整个git commit就会被中断。管理 Git 钩子最流行的工具是 husky。husky 做的事情是帮你把钩子脚本安装到.husky/目录下并在依赖安装时自动注册让钩子可以随项目一起被团队成员共享。简单总结commitlint 负责“判断提交信息合不合格”。husky 负责“在合适的时机调用 commitlint”。两者配合才能形成完整的提交检查链路。2. 安装 commitlint 前的准备环境与思路在动手跑命令之前我建议先把几个前置条件确认好。很多新手装不上或者装完没反应十有八九是环境这里出了问题。2.1 前置环境Node.js 版本要求commitlint 是用 Node.js 写的命令行工具所以你的机器上必须要有 Node.js 环境。这里要特别提醒一下千万不要拿一个很老的 Node 版本去装最新版 commitlint。当前主流的 commitlint v19 要求 Node.js 18 及以上的版本建议直接用 18 或 20 LTS。版本太低会出现安装警告甚至运行时直接报语法错误。先检查一下你当前的环境node -v npm -v如果你用的是 nvm想要切换 Node 版本会非常方便建议长期做前端工程化开发的同学都装一个。2.2 项目初始化与包管理器选择如果项目里还没有package.json先在项目根目录执行npm init -y这会生成一个基本的package.json文件。如果项目是已经存在的老项目跳过这步即可直接基于原有依赖安装。包管理器方面npm、pnpm、yarn 都支持 commitlint 和 husky但不同 PM 在 husky 的初始化命令上稍有差异。我下面的示例统一以 npm 为准用 pnpm 或 yarn 的同学把命令里的npx换成pnpm dlx或yarn dlx就好。这里有一个工程化建议统一团队使用的包管理器并把 lockfile 提交到仓库。混合使用包管理器会导致依赖树不一致有时候钩子不生效就是依赖版本对不上引起的。2.3 commitlint 要装的两个包分别是什么执行安装命令时我们通常要装两个包npm install --save-dev commitlint/cli commitlint/config-conventional很多新手第一次看到会懵为什么要装两个commitlint/cli是核心命令行工具负责解析提交信息、执行规则、输出报告。commitlint/config-conventional是一套现成的规则配置。commitlint 把“引擎”和“规则”拆开了你不用从零写规则引用官方预设即可。装完可以用下面命令确认是否安装成功npx commitlint --version如果能看到版本号说明核心工具没问题接下来进入配置环节。3. 从零开始安装配置 commitlint命令行实操这一章我们只做一件事让 commitlint 能在命令行里跑起来并且能用“合法/非法”两组提交信息验证它的判断。这一步如果能通过后面的 husky 集成就是水到渠成的事。3.1 安装命令行工具和官方配置包按照上一章的说明执行安装命令一并安装两个开发依赖npm install --save-dev commitlint/cli commitlint/config-conventional安装结束后看一下package.jsondevDependencies 里应该出现这两个包。版本号前面是^或~都正常不要锁死到精确版本避免后续升级麻烦。3.2 生成 commitlint.config.cjs 配置文件commitlint 需要一个配置文件。最简单的方式是手动创建echo module.exports { extends: [commitlint/config-conventional] }; commitlint.config.cjs这里我特意用了.cjs扩展名。因为新版项目常常在package.json里设置了type: module如果配置文件用.jsNode 会把它当 ES Module 解析而module.exports是 CommonJS 语法两者一起用大概率会报错。用.cjs可以直接避开这个坑。如果你是那种不喜欢手写配置的人也可以直接跑初始化命令它会用交互问答的方式帮你生成配置npx commitlint --init它会问你一些问题比如预设选哪套、配置文件生成什么格式回答完就能得到一份可用的配置。我第一次用--init的时候有点意外这工具居然还带交互式向导对新手确实友好。3.3 用命令行直接验证配置是否生效配置文件创建好后先把 commitlint 单独跑一遍确认规则真的能判对错。先测一条符合规范的提交信息echo feat: add login page | npx commitlint如果没有任何输出说明校验通过退出码是 0。再测一条不合规的echo add login page | npx commitlint这时应该会看到类似type must not be empty或subject may not be empty的错误提示退出码非 0。想看退出码可以在命令后面加一句echo $?返回 0 是通过返回 1 是失败。这一步能直接验证 commitlint 是否正常工作所以千万别跳过。命令行都跑不通的话挂了钩子也一定白搭。4. 配合 Husky 实现提交前自动校验现在 commitlint 已经能手动跑了但还没接入git commit。接下来就是关键一步装 husky挂 commit-msg 钩子。这是整个流程里最容易踩坑的部分我把每一步都拆开讲。4.1 安装并初始化 Husky首先安装 huskynpm install --save-dev husky然后初始化npx husky init初始化脚本会帮你在项目根目录创建.husky/文件夹里面默认生成一个pre-commit钩子示例文件同时在package.json的scripts里加上一行prepare: husky这个prepare脚本非常关键。npm install执行完毕之后npm 会自动运行prepare脚本husky 就是靠这脚本把 Git 钩子指向.husky/目录的。默认生成的pre-commit钩子内容通常是npm test如果你暂时不需要在 pre-commit 阶段跑测试可以直接删掉这个文件不影响后续操作。钩子文件本身要用 Unix 换行符文件开头要带 shebang 行husky 默认生成的文件都已经处理好了一般不用手动改。4.2 创建 commit-msg 钩子husky 初始化只会给你一个pre-commit示例commitlint 需要的是commit-msg钩子所以我们要手动创建npx husky add .husky/commit-msg npx --no -- commitlint --edit \$1\如果你用新版 husky也可以直接创建.husky/commit-msg文件写入以下内容#!/usr/bin/env sh . $(dirname -- $0)/_/husky.sh npx --no -- commitlint --edit $1我解释一下这里几个细节$1是 Git 传给 commit-msg 钩子的参数对应当前提交信息的临时文件路径commitlint 会读取该文件内容。npx --no表示不联网安装缺失的包直接使用本地 node_modules 里的 commitlint。--用来分隔 npx 自身的参数和要运行命令的参数能把--edit正确传给 commitlint。创建完成后检查.husky/目录结构确保commit-msg文件存在且有可执行权限。不同系统权限显示不一样在 Linux/macOS 下可以用ls -l .husky/commit-msg查看有-rwxr-xr-x类似输出就正常。4.3 实测一次合法和不合法提交钩子建好了立刻用真实验证。先故意写一条不规范的提交信息git add . git commit -m test husky这次提交应该会被拦截终端会打印 commitlint 的错误提示比如“subject may not be empty”。这意味着钩子生效了。再写一条规范的git commit -m chore: test husky可以看到提交成功。从这一步开始你项目的提交信息就算正式上了规矩。4.4 新成员克隆项目后钩子怎么生效这里有一个团队协作中经常会遇到的疑问.husky/目录要不要提交到 Git 仓库答案是要。husky 的钩子脚本必须随着仓库一起分发否则新成员克隆项目后没有钩子可用。而且.husky/目录不应该写进.gitignore。新成员克隆项目后只要执行过npm installnpm 就会自动运行prepare脚本husky 会自动把钩子激活。也就是说新成员什么都不用做环境装完钩子就绪。如果你遇到克隆项目后钩子没生效的情况大概率是装了依赖后prepare脚本没跑成功或者团队成员手动删过.husky目录。这时候手动执行一次npx husky就能重新挂上。5. commitlint 规则体系配置详解走到这一步基础流程已经通了。不过 commitlint 真正的威力在于规则可配置。团队越大越需要根据自身情况定制规则。这一章我把规则体系讲透。5.1 规则定义的格式和取值commitlint 每条规则基本都可以表示为三要素rule-name: [severity, condition, value]severity 表示级别有三个取值值效果0 或 off关闭不检查1 或 warn警告输出提示但不阻断提交2 或 error错误输出提示并阻断提交condition 只有两个取值always表示必须满足该规则never表示必须不满足该规则。value 根据规则不同可能是字符串、数组或对象。举个例子type-empty: [2, never]这行规则的意思是type 不能为空如果 type 为空报 error 并拒绝提交。再看一个带 value 的header-max-length: [2, always, 100]这条表示 header 第一行最大长度不能超过 100 个字符。5.2 常用规则清单与配置建议下面是我觉得所有团队都值得配置的规则清单整理成表方便查阅规则名含义推荐配置type-enum限定 type 可选值[2, always, [...]]type-case限定 type 大小写[2, always, lower-case]type-emptytype 是否允许为空[2, never]subject-emptysubject 是否允许为空[2, never]subject-full-stopsubject 结尾是否允许句号[2, never, .]header-max-lengthheader 最大长度[2, always, 100]scope-casescope 大小写[2, always, lower-case]body-leading-blankbody 前是否必须空行[2, always]footer-leading-blankfooter 前是否必须空行[2, always]你可以把上面这些规则组合起来写出一份比官方预设更贴合自己团队的配置。比如module.exports { extends: [commitlint/config-conventional], rules: { type-enum: [2, always, [feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert]], type-case: [2, always, lower-case], type-empty: [2, never], subject-empty: [2, never], subject-full-stop: [2, never, .], header-max-length: [2, always, 100], body-leading-blank: [2, always], footer-leading-blank: [2, always], }, };注意rules里的配置会覆盖extends引入的预设规则所以你可以只写需要调整的项不调整的项继承预设即可。5.3 使用社区配置包快速起步commitlint/config-conventional是目前使用最广的预设但社区的配置包远不止它一个。如果你使用的是 Angular 风格、Conventional 风格或其他风格可以按需引入对应的 config 包。引用方式很简单extends数组里可以写多个配置commitlint 会按顺序合并module.exports { extends: [commitlint/config-conventional, some-other-config], rules: { // 覆盖项 }, };我个人的建议是没有特殊理由不要轻易换预设直接使用commitlint/config-conventional理由很简单——社区配套工具最多、示例最多、团队新人理解成本最低。等你真的需要某一项特殊规则时再用rules覆盖。5.4 自定义规则的典型场景规则引擎最爽的一点是可以针对团队自己的流程做限制。比如你们团队规定只能在frontend和backend这两个模块上做改动那 scope 就可以做枚举校验scope-enum: [2, always, [frontend, backend]]这样如果有人写了feat(api-server): add endpoint只要 api-server 不在允许列表里就会被拦下。再比如你们希望自动版本的提交必须走特殊前缀可以用ignores字段放行某些提交信息module.exports { extends: [commitlint/config-conventional], ignores: [ (message) /^chore\(release\):/.test(message), ], };上面配置的意思是只要提交信息以chore(release):开头就跳过校验。这种能力在做自动化发版、版本发布机器人提交时很实用。6. 常见问题与排查技巧实录commitlint 本身安装不复杂但落地过程中会有各种奇怪问题。我把这几年帮团队排查时遇到的高频问题整理出来按“症状-原因-解决”的思路讲。6.1 钩子没有触发症状git commit一切正常提交信息写得再烂也没有任何检查提示。这种情况大概率是 husky 没有正确接管 Git 钩子。首先检查.husky/目录是否存在。如果不存在说明 husky 初始化没完成重新执行npx husky init。然后检查 Git 的 hooksPath 配置git config core.hooksPathhusky 9 会把这个值设置为.husky输出应该是.husky。如果输出为空或为.git/hooks说明钩子没有指向正确位置手动执行npx husky重新挂载即可。还有一种情况项目是从老版本 husky 升级上来的.husky/目录里残留了旧格式的脚本。建议删掉.husky目录重新执行npx husky init然后按本章流程重建钩子。6.2 commitlint 命令找不到症状钩子执行时报错提示commitlint: command not found或npx: command not found。这个问题的根源在于钩子运行时 PATH 环境变量和终端里不一样。在.husky/commit-msg脚本里使用本地依赖时最稳妥的写法是npx --no -- commitlint --edit $1不要直接写成commitlint --edit $1。如果 npx 本身都找不到说明 Node.js 的 bin 目录没有进 PATH。这是环境配置问题和 husky 无关。检查 Node.js 安装路径把node安装目录配到 PATH 里或者重新安装 Node.js。还有一种特殊情况本地node_modules里有 commitlint 但 npx 提示找不到可以试试npx --no-install commitlint --edit $1--no-install的意思是只允许使用本地已安装的包禁止联网下载。如果本地包确实存在而 npx 没找到往往是因为 npx 缓存或权限问题清掉 npm 缓存再试。6.3 配置文件不生效或加载报错症状commitlint 能跑但提示找不到规则或者感觉提示的内容和你的配置不一致。先确认你确实创建了配置文件。很多人装完两个包就测试结果 commitlint 默认没有加载任何规则自然会报错。最少也要有extends那一行module.exports { extends: [commitlint/config-conventional] };如果你想知道最终生效的配置长什么样可以用内置命令打印npx commitlint --print-config这个命令会把合并后的完整配置输出到终端排查规则覆盖问题时特别有用。加载报错最常见的原因是配置文件格式与项目模块类型冲突。如果你的package.json里设置了type: module而配置文件用的是module.exportsNode 就会报错。解决办法前面已经说了把配置文件改成.cjs。还有一个容易忽略的点配置文件修改后终端里如果挂着旧的进程或者编辑器缓存可能导致加载的还是旧配置。重启终端或编辑器再跑一次验证。6.4 如何临时跳过校验在某些特殊场景下你可能确实需要跳过校验。比如紧急修复线上的安全漏洞提交信息还没来得及按规范写完整需要先提交再说。Git 提供了计划内的后门git commit --no-verify -m hotfix: force commit--no-verify会跳过当前提交的所有 Git 钩子。注意这不只是跳过 commitlint也会跳过 pre-commit 等其它钩子所以不要滥用。在我的团队里有一个不成文约定紧急提交只是“特批”事后必须补一条规范的 commit 记录或者在下一次 commit 里修正历史。如果没有这样的约束--no-verify很快会变成偷懒借口。6.5 在 CI 中增加一道校验前面所有校验都依赖本地的 Git 钩子但有一个漏洞任何开发者都可以通过--no-verify绕过校验。如果你的团队规模大或者有外包/兼职成员建议在 CI 里加一道强制校验作为“最后防线”。在本地环境可以先校验某个范围内的提交npx commitlint --from HEAD~1 --to HEAD这会检查最近 1 个提交是否符合规范。如果你的 CI 是基于 MR/PR 的可以在 pipeline 里计算改动范围内的所有提交并逐一检查。比如 GitLab CI 中npx commitlint --from $CI_MERGE_REQUEST_TARGET_BRANCH_NAME --to $CI_COMMIT_SHAGitHub Actions 则可以在 workflow 里增加一个 jobcheckout 完整历史后跑同样的命令。这里有一个重要提示CI 里检查 commit 范围时要确保 checkout 时拉取到完整历史否则--from指定提交不存在命令会报错。在 GitHub Actions 中给 checkout 步骤加fetch-depth: 0即可。7. 几个我长期使用的工程化组合commitlint 只是提交信息治理的“检查端”真正让团队顺利运转还需要配套工具和合理的规则尺度。这一章分享一些我自己的实践希望能帮你少走弯路。7.1 结合 commitizen 让新手也提合格信息commitlint 解决的是“怎么拦”但没有解决“怎么写”。新人进入团队第一次接触 Conventional Commits记不住那么多 type 和格式全靠背是不现实的。我的做法是在项目里引入 commitizen提供一个交互式的提交命令。安装npm install --save-dev commitizen cz-conventional-changelog然后在package.json里配置{ scripts: { commit: cz }, config: { commitizen: { path: cz-conventional-changelog } } }以后团队成员执行npm run commit就会出现一个交互面板依次让你选择 type、填写 scope、输入 subject最后自动拼接成符合规范的提交信息。对新人来说这个体验比强制背诵规则人性化得多。7.2 我目前的推荐配置最后放一份我的常用配置直接复制就能用module.exports { extends: [commitlint/config-conventional], rules: { type-enum: [2, always, [feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert]], type-case: [2, always, lower-case], type-empty: [2, never], subject-empty: [2, never], subject-full-stop: [2, never, .], header-max-length: [2, always, 100], body-leading-blank: [2, always], footer-leading-blank: [2, always], }, };这套配置不算苛刻但足以保证提交信息可读、可追溯。它的价值不在“难”而在“一致”。7.3 最后提醒我见过一些团队第一次上 commitlint 时把规则写得很严格比如 scope 必须枚举、subject 禁止中文、长度限制 50结果大家怨声载道甚至有人开始用--no-verify逃避检查规则形同虚设。我的经验是规则落地要分步走。第一周只检查 type 非空、subject 非空、header 长度让全队适应格式两周后再逐渐放开更细的规则。刚开始的“松”是为了后面的“紧”能执行得下去。另外公司内部的规范文档应该和规则配置保持同步光有工具没有文档新人很难理解为什么提交信息必须这样写。把“为什么”讲清楚配合 commitizen 降低输入成本commitlint 才能真正发挥价值。就我个人而言这套东西接入之后最大的感受是git log 真的可以当 CHANGELOG 用了。排查问题、生成发布记录、回溯需求变更效率提升非常明显。只要熬过前两周的适应期你会发现团队成员反而会感谢这条“硬规矩”。
返回列表