前置条件
环境版本
- Node.js12+
- npm 6+
熟知规范
每条提交信息都由页眉、正文和页脚组成。页眉有一种特殊格式,包括类型、范围和主题:
<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>
规范详见Angular提交规范
快速上手
安装依赖
npm install --save-dev husky
npx husky init
npm install --save-dev @commitlint/config-conventional @commitlint/cli
配置规范
在项目根目录下创建.commitlintrc.json
:
{
"extends": [
"@commitlint/config-conventional"
]
}
选择合适的git
钩子
删除.husky/*
内的其他hooks
文件,更新.husky/commit-msg
文件(若没有,则新建):
npx --no -- commitlint --edit $1
exit 1
测试
通过exit 1
可以测试git commit
命令,触发commit-msg
钩子,而不会产生真实git
提交。
定制化
@commitlint/cli
从以下文件获取配置:
- .commitlintrc
- .commitlintrc.json
- .commitlintrc.yaml
- .commitlintrc.yml
- .commitlintrc.js
- .commitlintrc.cjs
- .commitlintrc.mjs
- .commitlintrc.ts
- .commitlintrc.cts
- commitlint.config.js
- commitlint.config.cjs
- commitlint.config.mjs
- commitlint.config.ts
- commitlint.config.cts
规则配置项由名称和配置规则组成。
配置规则可以是数组,也可以是返回数组的(异步)函数。
配置规则的数组包含:
-
Level
[0..2]:- 0:忽略规则,不使用该条规则;
- 1:触犯该规则视为警告,命令行有日志警告,命令照常执行;
- 2:触犯该规则视为错误,命令行有日志报错,命令直接停止;
-
always|never
:-
always
:正向规则,该 -
never
:逆向规则,
-
-
value
:该规则使用的值。
规则解读:
- 从规则列表获取名称定义的具体规则;
- 通过
always|never
判断规则的正逆; - 通过
Level
判断规则级别;
{
"extends": [
"@commitlint/config-conventional"
],
"rules": {
/**
* 条件:type为空;
* always|never: 逆向规则;
* level:错误级别
* 解读:type不能为空,违反该规则,报错,停止执行任务;
*/
"type-empty": [2, "never"],
/**
* 条件:subject为空;
* always|never: 正向规则;
* level:错误级别
* 解读:subject为空,违反该规则,报错,停止执行任务;
*/
"subject-empty": [2, "always"],
/**
* 条件:type类型的值最长为72个字符;
* always|never: 正向规则;
* level:忽略规则
* 解读:type类型的值最长为72个字符,违反该规则,没反应,相当于该规则无效;
*/
'type-max-length': [0, 'always', 72],
'header-max-length': async () => [0, 'always', 72],
}
}
规则配置示例见:https://commitlint.js.org/reference/rules-configuration.html
规则列表见:https://commitlint.js.org/reference/rules.html
CLI交互
通过上述配置,会在git commit提交stash的时候,校验提供的message信息是否符合规范。
该部分增加cli交互,提交git commit命令时,命令行会给出提示,可通过交互提交符合规范的message信息。
-
安装依赖
npm install --save-dev commitizen npx commitizen init cz-conventional-changelog --save-dev --save-exact
以上命令会自动执行以下任务:
安装
cz-conventional-changelog
适配器;-
在
package.json
中添加config.commitizen
配置,指定适配器:"config": { "commitizen": { "path": "./node_modules/cz-conventional-changelog" } }
(可选项)手动将迁移到单独文件
.czrc
中:{ "path": "./node_modules/cz-conventional-changelog" }
通过npx cz可以触发git commit的提示
-
将npx cz整合到git工作流中,让项目维护者不必不熟悉Commitizen的使用
# 创建.husky/prepare-commit-msg exec < /dev/tty && node_modules/.bin/cz --hook || true
即可通过git commit触发git commit提示。
-
优化
prepare-commit-msg
目前
git commit -m "feat: 新功能"
也会触发cz提示,这里做一下优化,符合规范的不做cz提示:commit_msg=`cat $1` msg_re="^(feat|fix|docs|style|refactor|perf|test|workflow|build|ci|chore|release|workflow)(\(.+\))?: .{1,100}" if [[ $commit_msg =~ $msg_re ]] then exit 0 else echo "\n不合法的 commit 消息提交格式,请使用正确的格式\n" exec < /dev/tty && node_modules/.bin/cz --hook || true fi
比较
规范 Git
提交信息的工具有:commitlint
和 commitizen
。
两者各有专攻,commitlint
校验提交信息,commitizen
辅助填写提交信息;
在 Git
提交工作流程中,commitlint
作用于 commit-msg
阶段,commitizen
作用于 prepare-commit-msg
。
commitlint
和配置文件.commitlintrc.json
直接交流,将配置文件解析成后续能使用的规则集;
commitizen
本身与提交信息规则并无关系。commitizen
只提供与 Git
交互的框架,它传递 inquirer
对象给适配器(Adapter),由适配器负责描述命令行填写流程并返回用户填写的信息。借助cz-customizable
配置文件.cz-config.js
可实现自定义适配器。
整体定制化交互方案
了解 commitlint
和 commitizen
的机制之后,我们来考虑核心问题:怎么使两者共用同一份规则配置。
有两种思路:
- 从
commitlint
配置出发,读取commitlint
配置并生成对应的命令行提交流程,即创造一个commitizen
适配器,@commitlint/cz-commitlint
已实现。 - 从
cz-customizable
配置出发,将cz-customizable
配置翻译为commitlint
规则,即创造一个commitlint
配置,commitlint-config-cz
已实现。
基于@commitlint/cli
的解决方案适用于对已有规范(e.g. @commitlint/config-conventional
)进行调整/扩展。
基于commitizen
的解决方案适用于完全定制化。
基于@commitlint/cli
-
安装依赖
npm install --save-dev husky @commitlint/cli @commitlint/config-conventional @commitlint/cz-commitlint commitizen npx husky init
-
自定义commitlint规范
创建
commitlint.config.js
:export default { extends: [ '@commitlint/config-conventional' ], rules: { 'type-enum': [2, 'always', ['foo']], }, }
-
配置
commitizen
适配器创建
.czrc
文件,支持cz
自定义适配器(Adapter),内容如下:{ "path": "@commitlint/cz-commitlint" }
-
配置
husky hooks
-
创建
.husky/prepare-commit-msg
,支持commitizen
适配器echo $0 # 打印当前执行的husky hook名称 echo $1 # 打印当前执行的git hook名称 echo `cat $1` # 打印当前执行的git hook参数值 commit_msg=`cat $1` msg_re="^(feat|fix|docs|style|refactor|perf|test|workflow|build|ci|chore|release|workflow)(\(.+\))?: .{1,100}" if [[ $commit_msg =~ $msg_re ]] then exit 0 else echo "\n不合法的 commit 消息提交格式,请使用正确的格式\n" exec < /dev/tty && node_modules/.bin/cz --hook || true fi
-
创建
.husky/commit-msg
,支持commitlint
:npx --no -- commitlint --edit $1
-
基于commitizen
-
安装依赖
npm install --save-dev husky commitizen cz-customizable commitlint-config-cz @commitlint/cli npx husky init
-
自定义
commitizen
配置- 创建
.czrc
文件,支持commitizen
自定义适配器(Adapter),内容如下:{ "path": "./node_modules/cz-customizable" }
- 创建
.cz-config.js
,自定义适配器(Adapter):const path = require('path'); const fs = require('fs'); const appRootDir = 'apps' const projectJSONFile = 'project.json'; const workspace = __dirname const appRootAbsoluteDir = path.resolve(workspace, appRootDir); const scopes = [] try { const apps = fs.readdirSync(appRootAbsoluteDir) apps.forEach((appName) => { const filePath = path.resolve(appRootAbsoluteDir, appName, projectJSONFile) const file = fs.readFileSync(filePath, 'utf-8') const projectJson = JSON.parse(file) scopes.push(projectJson.name) }) } catch { } const czConfig = { types: [ { value: 'feat', name: 'feat: A new feature' }, { value: 'fix', name: 'fix: A bug fix' }, { value: 'docs', name: 'docs: Documentation only changes' }, { value: 'style', name: 'style: Changes that do not affect the meaning of the code\n (white-space, formatting, missing semi-colons, etc)', }, { value: 'refactor', name: 'refactor: A code change that neither fixes a bug nor adds a feature', }, { value: 'perf', name: 'perf: A code change that improves performance', }, { value: 'test', name: 'test: Adding missing tests' }, { value: 'chore', name: 'chore: Changes to the build process or auxiliary tools\n and libraries such as documentation generation', }, { value: 'revert', name: 'revert: Revert to a commit' }, { value: 'WIP', name: 'WIP: Work in progress' }, ], scopes: [ { value: '', name: 'empty' }, ...scopes.map((name) => ({ name })) ], usePreparedCommit: false, // to re-use commit from ./.git/COMMIT_EDITMSG allowTicketNumber: false, isTicketNumberRequired: false, ticketNumberPrefix: 'TICKET-', ticketNumberRegExp: '\\d{1,5}', messages: { type: "Select the type of change that you're committing:", scope: '\nDenote the SCOPE of this change:', subject: 'Write a SHORT, IMPERATIVE tense description of the change:\n', body: 'Provide a LONGER description of the change (optional). Use "|" to break new line:\n', breaking: 'List any BREAKING CHANGES (optional):\n', footer: 'List any ISSUES CLOSED by this change (optional). E.g.: #31, #34:\n', confirmCommit: 'Are you sure you want to proceed with the commit above?', }, allowCustomScopes: false, allowBreakingChanges: ['feat', 'fix'], skipQuestions: ['body'], subjectLimit: 100 }; module.exports = czConfig;
- 创建
.commitlintrc.json
补充commitlint
规范
若仅以
.cz-config.js
定义的适配器执行husky hooks
,会发现commitlint
不起作用:commitlint-config-cz
生成的commitlint
配置仅添加了上述的type-enum
和scope-enum
规则(这些规则从您的cz-customizable
config 中读取)。
这意味着commitlnt
默认允许以空类型、空主题提交,因此还需要在.commitlintrc.json
中添加类型为空、主题为空的规则。{ "extends": [ "cz" ], "rules": { "type-empty": [2, "never"], "subject-empty": [2, "never"] } }
- 创建
-
配置
husky hooks
- 创建
.husky/prepare-commit-msg
,支持commitizen
自定义适配器(Adapter)echo $0 // 打印当前执行的husky hook名称 echo $1 // 打印当前执行的git hook名称 echo `cat $1` // 打印当前执行的git hook参数值 commit_msg=`cat $1` msg_re="^(feat|fix|docs|style|refactor|perf|test|workflow|build|ci|chore|release|workflow)(\(.+\))?: .{1,100}" if [[ $commit_msg =~ $msg_re ]] then exit 0 else echo "\n不合法的 commit 消息提交格式,请使用正确的格式\n" exec < /dev/tty && node_modules/.bin/cz --hook || true fi
- 创建
.husky/commit-msg
,支持commitlint
:npx --no -- commitlint --edit $1
- 创建
特殊操作
忽略lint
git commit --amend