如何使 commitlint 和 commitizen 共用规则配置

6,543 阅读6分钟

规范 Git 提交信息的工具有:commitlint 和 commitizen。

两者各有专攻,commitlint 校验提交信息,commitizen 辅助填写提交信息;在 Git 提交工作流程中,commitlint 作用于 commit-msg 阶段,commitizen作用于 pre-commit。互不干扰,各司其职,相辅相成。

看似天下太平,然而,两者遵循各自的规则配置,没个统一接口各干各的。在同一流程中协作,用两套规则配置,总有些闹心。

为什么不能统一呢?

好好的为什么就不能用同一份规则配置呢?因为这俩工具的机制是在是太不一样了。

commitlint

先来说说 commitlint,听这名字就知道是 lint 一脉的,容易想到 eslint、stylelint,机制上也有些相似。

我们知道目前认同范围最广的基础规范是约定式提交 (conventionalcommits)

<type>[scope]: <subject>

[body]

[footer]

它由 typescopesubjectbodyfooter 几部分组成,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,配置文件里有 extendsrulesplugins 等字段。用过 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 的机制之后,我们来考虑核心问题:怎么使两者共用同一份规则配置。

有两种思路:

  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
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 的前端开发者来说或许不是一个问题。