规范 Git 提交信息的工具有:commitlint 和 commitizen。
两者各有专攻,commitlint 校验提交信息,commitizen 辅助填写提交信息;在 Git 提交工作流程中,commitlint 作用于 commit-msg
阶段,commitizen作用于 pre-commit
。互不干扰,各司其职,相辅相成。
看似天下太平,然而,两者遵循各自的规则配置,没个统一接口各干各的。在同一流程中协作,用两套规则配置,总有些闹心。
为什么不能统一呢?
好好的为什么就不能用同一份规则配置呢?因为这俩工具的机制是在是太不一样了。
commitlint
先来说说 commitlint,听这名字就知道是 lint 一脉的,容易想到 eslint、stylelint,机制上也有些相似。
我们知道目前认同范围最广的基础规范是约定式提交 (conventionalcommits)
<type>[scope]: <subject>
[body]
[footer]
它由 type
、scope
、subject
、body
、footer
几部分组成,commitlint 成立于这套基础规范之上,也就是说,它相信一条提交信息字符串是可以被解析成这几部分的,然后再检查各部分是否符合规定。
了解到 commitlint 的思路后,它的运行机制也好理解了。简单来说,它的运行过程是:解析提交信息 → 确定规则集 → 执行规则检查
解析提交信息
首先它通过 parserPreset
解析器将提交信息字符串解析为结构对象,例如:
例如:如下提交信息
fix(package1): update title
The old title is overdated
Issues: https://github.com/conventional-changelog/commitlint/issues/2507
被解析为
{
"header": "fix(package1): update title",
"type": "fix",
"scope": "package1",
"subject": "update title",
"body": "The old title is overdated",
"footer": "Issues: https://github.com/conventional-changelog/commitlint/issues/2507",
"references": [
"https://github.com/conventional-changelog/commitlint/issues/2507"
]
}
确定规则集
接着,它根据规则配置文件生成规则集。commitlint 的配置文件是 commitlint.config.js
,配置文件里有 extends
,rules
,plugins
等字段。用过 eslint 的朋友可能会觉得熟悉,和 eslint 相似配置中的 extends
字段继承已有配置,rules
定义具体规则。
一言概之,在这一步它把配置文件解析成后续能使用的规则集。我们截一小段看看规则集大概的样子:
{
'subject-full-stop': [2, 'never', '.'],
'subject-case': [
2,
'never',
['sentence-case', 'start-case', 'pascal-case', 'upper-case'],
],
'subject-empty': [2, 'never'],
}
键是规则名,值是规则参数。一个规则实际上对应一个逻辑函数, commitlint 内置的规则逻辑由 一个键值对列表 维护。我们取 subject-full-stop
的逻辑函数看看。
// subject-full-stop.ts
import message from '@commitlint/message';
import {SyncRule} from '@commitlint/types';
export const subjectFullStop: SyncRule<string> = (
parsed,
when = 'always',
value = '.'
) => {
const input = parsed.subject;
if (!input) {
return [true];
}
const negated = when === 'never';
const hasStop = input[input.length - 1] === value;
return [
negated ? !hasStop : hasStop,
message(['subject', negated ? 'may not' : 'must', 'end with full stop']),
];
};
subject-full-stop
函数接受的第一个参数 parsed
是第一步产生的结构对象;第二个参数 when
是规则值中第二个元素,代表条件前缀是或者非;第三个参数 value
是规则值中的第三个元素,代表具体值。
subject-full-stop
函数返回校验结果和错误提示信息。
现在再来看看规则的含义,比如
'subject-full-stop': [2, 'never', '.']
:表示标题不能以.
结束'subject-full-stop: [2, 'always', '!']'
:表示标题必须以!
结束
回看上面 subject-full-stop
函数的逻辑的确可以表达这个意思。
执行规则检查
拥有「规则集」和「提交信息解析对象」后,这一阶段 commitlint 遍历「规则集」并执行相对应逻辑函数,最终格式化校验结果并输出报告。
commitizen
commitlint 和配置文件直接交流,不同的是,commitizen 本身与提交信息规则并无关系。commitizen 只提供与 Git 交互的框架,它传递 inquirer
对象给适配器(Adapter),由适配器负责描述命令行填写流程并返回用户填写的信息。因此真正起核心作用,与规则相关的是适配器。
inquirer 是封装了常见命令行交互界面的一个工具集,它支持快速使用 input、confirm、list、checkbox、editor 等常用命令行界面。
大多数 commitizen 适配器都是按特定的规范来描述命令行交互流程,比如基于 conventional-changelog 规范的 cz-conventional-changelog。这样的机制下,如果开发者需要自定规则,得学习使用 inquirer 从头创造一个新的适配器,这实在是不太易用。那么有没有支持自定义的适配器存在呢?有的,cz-customizable 就是这特殊的存在。
cz-customizable 对外设定了一份配置文件 .cz-config.js
,设计了一些可配置字段,cz-customizable 读入配置并将其转换为基于 inquirer 的命令行交互流程。
cz-customizable 的配置示例如下:
module.exports = {
// type 可选列表
types: [
{ value: 'feat', name: 'feat: A new feature' },
{ value: 'fix', name: 'fix: A bug fix' },
],
// scope 可选列表
scopes: [{ name: 'accounts' }, { name: 'admin' }, { name: 'exampleScope' }, { name: 'changeMe' }],
// 覆盖交互提示信息
messages: {
type: "请选择你的提交类型",
scope: '请选择 SCOPE',
subject: '简短描述本次修改:\n',
body: '提供关于本次修改更具体的信息(可选),使用 "|" 换行:\n',
confirmCommit: '确定提交信息?',
},
// skip any questions you want
skipQuestions: ['body'],
// limit subject length
subjectLimit: 100,
};
怎么实现共用?
了解 commitlint 和 commitizen 的机制之后,我们来考虑核心问题:怎么使两者共用同一份规则配置。
有两种思路:
- 从 commitlint 配置出发,读取 commitlint 配置并生成对应的命令行提交流程,即创造一个 commitizen 适配器,@commitlint/cz-commitlint 已实现
- 从 cz-customizable 配置出发,将 cz-customizable 配置翻译为 commitlint 规则,即创造一个 commitlint 配置,commitlint-config-cz 已实现
cz-commitlint
@commitlint/cz-commitlint 配置过程很简单
# 安装 commitizen 和 commitlint 和 cz-commitlint
npm install --save-dev @commitlint/cz-commitlint commitizen @commitlint/cli
# or yarn
yarn add -D @commitlint/cz-commitlint commitizen @commitlint/cli
在 package.json 中配置 commitizen 适配器
{
"scripts": {
"commit": "git-cz"
},
"config": {
"commitizen": {
"path": "@commitlint/cz-commitlint"
}
}
}
安装并配置所需的规范,比如 conventional-changelog 规范
npm install --save-dev @commitlint/config-conventional
# or yarn
yarn add -D @commitlint/config-conventional
# 配置 commitlint.config.js
echo "module.exports = {extends: ['@commitlint/config-conventional']};" > commitlint.config.js
commitlint-config-cz
commitlint-config-cz 的配置过程也不复杂
# 安装 commitizen 和 commitlint 和 cz-customizable 和 commitlint-config-cz
npm install --save-dev commitlint-config-cz commitizen cz-customizable @commitlint/cli
# or yarn
yarn add -D commitlint-config-cz commitizen cz-customizable @commitlint/cli
在 package.json 中配置 commitizen 适配器
{
"scripts": {
"commit": "git-cz"
},
"config": {
"commitizen": {
"path": "node_modules/cz-customizable"
}
}
}
在 commitlint.config.js 中配置
module.exports = {
extends: [
'cz'
]
};
配置 .cz-config.js
,可以选择以 官方模板 为基础。
如何选择
两者各有适用场景
@commitlint/cz-commitlint 适合使用已有规范,或基于已有规范进行自定义扩展的情况。
commitlint-config-cz 适合不依赖已有规范完全自定义的场景。
cz-commitlint 也能实现完全自定义,但需要了解 commitlint 的配置和 规则,相较于 .cz-config.js
简单直观的配置会增加一点门槛,不过这对于适应 eslint 的前端开发者来说或许不是一个问题。