使用commitlint+ commitizen规范 commit message

675 阅读9分钟

为什么要规范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 的机制之后,我们来考虑核心问题:怎么使两者共用同一份规则配置。

有两种思路:

  1. 从 commitlint 配置出发,读取 commitlint 配置并生成对应的命令行提交流程,即创造一个 commitizen 适配器,@commitlint/cz-commitlint 可以实现;

  2. 从 cz-customizable 配置出发,将 cz-customizable 配置翻译为 commitlint 规则,即创造一个 commitlint 配置,commitlint-config-cz 可以实现;

cz-commitlint

@commitlint/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

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 适合使用已有规范,或基于已有规范进行自定义扩展的情况。

github.com/curly210102…

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