为什么要规范commit message?
我们先来看一个不太规范的 commit 记录
写得全是update,这种 commit 信息对于想要从中获取有效信息的人来说无疑是一种致命的打击。
接着我们来看一个社区里面比较流行的Angular规范的 commit 记录:
首先,提交的信息一目了然,上图中这种规范的 commit 信息首先提供了更多的历史信息,方便快速浏览。
其次,可以过滤某些 commit(比如文档改动),便于快速查找信息。
如何规范代码提交信息?
规范 Git 提交信息的工具有:commitlint 和 commitizen。
两者各有专攻,commitlint 校验提交信息,commitizen 辅助填写提交信息;在 Git 提交工作流程中,commitlint 作用于 commit-msg 阶段,commitizen作用于 pre-commit。
两者遵循各自的规则配置,没有统一接口,在同一流程中协作,用两套规则配置,总有些闹心。
为什么没有统一?
因为两个工具的机制不一样。
commitlint
看名字就知道是属于 lint 一脉的,容易联想到 eslint、stylelint,机制上也有些相似。
我们知道目前认同范围最广的基础规范是约定式提交 (conventionalcommits)
<type>[scope]: <subject>
[body]
[footer]
-
Header 信息头 【必须】
-
type commit类型【必须】
-
scope commit 作用范围
-
subject commit 表述【必须】
-
-
body commit 详细信息
-
footer 辅助信息: 1. 不兼容变动 2. 关闭 Issue
它由 type、scope、subject、body、footer 几部分组成,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 = {
types: [
{ value: 'feat', name: 'feat: 新功能' },
{ value: 'fix', name: 'fix: 修复bug' },
{ value: 'docs', name: 'docs: 文档变更' },
{ value: 'style', name: 'style: 代码格式(不影响功能,例如空格、分号等格式修正)' },
{ value: 'refactor', name: 'refactor: 代码重构(不包括 bug 修复、功能新增)' },
{ value: 'perf', name: 'perf: 性能优化' },
{ value: 'test', name: 'test: 增加、修改测试用例' },
{ value: 'build', name: 'build: 构建系统、外部依赖变更(如升级 npm 包、修改 webpack 配置等)' },
{ value: 'ci', name: 'ci: 修改 CI 配置、脚本' },
{ value: 'chore', name: 'chore: 对构建过程或辅助工具和库(如文档生成)的更改' },
{ value: 'revert', name: 'revert: 代码回退' }
],
scopes: [
{ name: 'app' },
{ name: 'eslint' }
],
// 覆盖交互提示信息
messages: {
type: '请选择你的提交类型',
scope: '请选择 SCOPE',
subject: '简短描述本次修改:\n',
body: '提供关于本次修改更具体的信息(可选),使用 "|" 换行:\n',
breaking: '列举非兼容性重大的变更(可选):\n',
footer: '列举出所有变更的 ISSUES CLOSED(可选),例如:#31, #34:\n',
confirmCommit: '确定提交信息?'
},
// 跳过的问题列表
skipQuestions: ['footer'],
// 允许自定义范围
allowCustomScopes: true,
// 允许更改提交类型
allowBreakingChanges: ['feat', 'fix'],
// 设置提交信息的长度限制
subjectLimit: 100
}
怎么实现共用?
了解 commitlint 和 commitizen 的机制之后,我们来考虑核心问题:怎么使两者共用同一份规则配置。
有两种思路:
-
从 commitlint 配置出发,读取 commitlint 配置并生成对应的命令行提交流程,即创造一个 commitizen 适配器,@commitlint/cz-commitlint 可以实现;
-
从 cz-customizable 配置出发,将 cz-customizable 配置翻译为 commitlint 规则,即创造一个 commitlint 配置,commitlint-config-cz 可以实现;
cz-commitlint
# 安装 commitizen 和 commitlint 和 cz-commitlint
pnpm install --save-dev @commitlint/cz-commitlint commitizen @commitlint/cli -w
在 package.json 中配置 commitizen 适配器
{
"scripts": {
"commit": "git-cz"
},
"config": {
"commitizen": {
"path": "@commitlint/cz-commitlint"
}
}
}
安装并配置所需的规范,比如 conventional-changelog 规范
pnpm install --save-dev @commitlint/config-conventional -w
# 配置 commitlint.config.js
echo "module.exports = {extends: ['@commitlint/config-conventional']};" > commitlint.config.js
commitlint-config-cz
# 安装 commitizen 和 commitlint 和 cz-customizable 和 commitlint-config-cz
pnpm install --save-dev 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,可参考 官方模板 。
module.exports = {
types: [
{ value: 'feat', name: 'feat: 新功能' },
{ value: 'fix', name: 'fix: 修复bug' },
{ value: 'docs', name: 'docs: 文档变更' },
{ value: 'style', name: 'style: 代码格式(不影响功能,例如空格、分号等格式修正)' },
{ value: 'refactor', name: 'refactor: 代码重构(不包括 bug 修复、功能新增)' },
{ value: 'perf', name: 'perf: 性能优化' },
{ value: 'test', name: 'test: 增加、修改测试用例' },
{ value: 'build', name: 'build: 构建系统、外部依赖变更(如升级 npm 包、修改 webpack 配置等)' },
{ value: 'ci', name: 'ci: 修改 CI 配置、脚本' },
{ value: 'chore', name: 'chore: 对构建过程或辅助工具和库(如文档生成)的更改' },
{ value: 'revert', name: 'revert: 代码回退' }
],
scopes: [
{ name: 'app' },
{ name: 'packages' }
],
// 覆盖交互提示信息
messages: {
type: '请选择你的提交类型',
scope: '请选择 SCOPE',
customScope: '请输入自定义的 SCOPE:\n',
subject: '简短描述本次修改:\n',
body: '提供关于本次修改更具体的信息(可选),使用 "|" 换行:\n',
breaking: '列举非兼容性重大的变更(可选):\n',
footer: '列举出所有变更的 ISSUES CLOSED(可选),例如:#31, #34:\n',
confirmCommit: '确定提交信息?'
},
// 跳过的问题列表
skipQuestions: ['footer'],
// 允许自定义范围
allowCustomScopes: true,
// 允许更改提交类型
allowBreakingChanges: ['feat', 'fix'],
// 允许在提交信息中包含工单(ticket)编号
allowTicketNumber: true,
// 是否必须填写编号
isTicketNumberRequired: false,
// 工单编号的前缀
ticketNumberPrefix: 'TICKET-',
// 单编号的正则表达式
ticketNumberRegExp: '\\d{1,5}',
// 设置提交信息的长度限制
subjectLimit: 100
}
如何选择?
两者各有适用场景
@commitlint/cz-commitlint 适合使用已有规范,或基于已有规范进行自定义扩展的情况。
commitlint-config-cz 适合不依赖已有规范完全自定义的场景,直接在 .cz-config.js文件中配置,操作简单清晰。
规范 commit message 的好处
-
首行就是简洁实用的关键信息,方便在 git history 中快速浏览
-
具有详实的 body 和 footer ,可以清晰的看出某次提交的目的和影响
-
可以通过 type 过滤出想要查找的信息,也可以通过关键字快速查找相关提交
-
可以直接从 commit 生成 change log
// 列举几个常用的 log 参数
// 输出 log 的首行 git log --pretty=oneline
// 只输出首行的 commit 信息。不包含 hash 和 合并信息等 git log --pretty=format:%s
// 查找有关“cz-config”的提交 git log --grep="cz-config"
// 打印出 zhangjunnan 的提交 git log --author=zhangjunnan
// 红色的短 hash,黄色的 ref , 绿色的相对时间 git log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr)%Creset'
用工具实现规范提交的好处
上面介绍了规范提交的格式,如果让大家在 git commit 的时候严格按照上面的规范来写,
首先心智是有负担的,得记住不同的类型到底是用来定义什么的,subject 怎么写,body 怎么写,footer 要不要写。
其次,对人的规范大部分都是反人性的,所以很可能在过不了多久,就会有同学渐渐的不按照规范来写。
靠意志力来控制自己严格按照规范来写是需要额外耗费一些精力的,把精力耗费在这种事情上面实在有些浪费。
上面使用工具实现规范提交的方案,即提示了必填字段,又规范了提交信息的格式。
如何使用?
// 执行 commit 命令 替换 git commit
npm run commit
// or 执行git cz 替换 git commit
git cz
彩蛋
1、若执行git cz报错,可先执行 npm install -g commitizen 再执行 git cz