前置条件

环境版本

  • 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信息。
  1. 安装依赖

    npm install --save-dev commitizen
    npx commitizen init cz-conventional-changelog --save-dev --save-exact

    以上命令会自动执行以下任务:

    1. 安装cz-conventional-changelog适配器;
    2. package.json中添加config.commitizen配置,指定适配器:

      "config": {
          "commitizen": {
              "path": "./node_modules/cz-conventional-changelog"
          }
      }

      (可选项)手动将迁移到单独文件.czrc中:

      {
        "path": "./node_modules/cz-conventional-changelog"
      }
  2. 通过npx cz可以触发git commit的提示
  3. 将npx cz整合到git工作流中,让项目维护者不必不熟悉Commitizen的使用

    # 创建.husky/prepare-commit-msg
    exec < /dev/tty && node_modules/.bin/cz --hook || true

    即可通过git commit触发git commit提示。

  4. 优化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 提交信息的工具有:commitlintcommitizen

两者各有专攻,commitlint 校验提交信息,commitizen 辅助填写提交信息;

Git 提交工作流程中,commitlint 作用于 commit-msg 阶段,commitizen作用于 prepare-commit-msg

commitlint和配置文件.commitlintrc.json直接交流,将配置文件解析成后续能使用的规则集;

commitizen本身与提交信息规则并无关系。commitizen只提供与 Git 交互的框架,它传递 inquirer 对象给适配器(Adapter),由适配器负责描述命令行填写流程并返回用户填写的信息。借助cz-customizable配置文件.cz-config.js可实现自定义适配器。

整体定制化交互方案

了解 commitlintcommitizen 的机制之后,我们来考虑核心问题:怎么使两者共用同一份规则配置。

有两种思路:

  1. commitlint 配置出发,读取 commitlint 配置并生成对应的命令行提交流程,即创造一个 commitizen 适配器,@commitlint/cz-commitlint 已实现。
  2. cz-customizable 配置出发,将 cz-customizable 配置翻译为 commitlint 规则,即创造一个 commitlint 配置,commitlint-config-cz 已实现。

基于@commitlint/cli的解决方案适用于对已有规范(e.g. @commitlint/config-conventional)进行调整/扩展。

基于commitizen的解决方案适用于完全定制化。

基于@commitlint/cli

  1. 安装依赖

    npm install --save-dev husky @commitlint/cli @commitlint/config-conventional @commitlint/cz-commitlint commitizen
    npx husky init
  2. 自定义commitlint规范

    创建commitlint.config.js:

    export default { 
      extends: [
        '@commitlint/config-conventional'
      ],
      rules: {
        'type-enum': [2, 'always', ['foo']],
      },
    }
  3. 配置commitizen适配器

    创建.czrc文件,支持cz自定义适配器(Adapter),内容如下:

    {
      "path": "@commitlint/cz-commitlint"
    }
  4. 配置husky hooks

    1. 创建.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
      1. 创建.husky/commit-msg,支持commitlint

        npx --no -- commitlint --edit $1

基于commitizen

  1. 安装依赖

    npm install --save-dev husky commitizen cz-customizable commitlint-config-cz @commitlint/cli
    npx husky init
  2. 自定义commitizen配置

    1. 创建.czrc文件,支持commitizen自定义适配器(Adapter),内容如下:

      {
        "path": "./node_modules/cz-customizable"
      }
    2. 创建.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;
      1. 创建.commitlintrc.json补充commitlint规范

        若仅以.cz-config.js定义的适配器执行husky hooks,会发现commitlint不起作用:

        commitlint-config-cz生成的commitlint 配置仅添加了上述的 type-enumscope-enum 规则(这些规则从您的 cz-customizable config 中读取)。

      这意味着 commitlnt 默认允许以空类型、空主题提交,因此还需要在.commitlintrc.json中添加类型为空、主题为空的规则。

      {
       "extends": [
      "cz"
       ],
       "rules": {
      "type-empty": [2, "never"],
      "subject-empty": [2, "never"]
       }
      }
  3. 配置husky hooks

    1. 创建.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
    2. 创建.husky/commit-msg,支持commitlint

      npx --no -- commitlint --edit $1

特殊操作

忽略lint

git commit --amend

参考文档


米花儿团儿
1.3k 声望75 粉丝