Git提交工作流提交信息校验

前置条件

环境版本

  • 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
      
    2. 创建.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;
      
    3. 创建.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

参考文档

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容