用工具思路来规范化 git commit message

2,786 阅读7分钟
原文链接: github.com

在团队协作中我们经常碰到的问题是每个人都有自己的开发习惯,这个习惯包含但不限于编码风格,工具使用等,所以往往协作中就会出现各种各样的问题。这篇文章将会从很小的切入点开始讲,即如标题所诉 Git Commit Message

但在讲之前大家最好对如下的分支管理有一定的了解,原因在于,好的分支管理模型和好的 Git Commit Message 是规范化开发必不可少的内容。

衍生阅读 Git 分支管理模型gitflow

为什么要规范 Git Commit Message

在项目开发开发中或许我们能经常看到

  • 说不出所以然的一连串的 commit 提交日志
  • commit 信息写的很简单,根本没有办法从 commit 信息中获知该 commit 用意的
  • commit 信息写的很随意,commit 信息和变更代码之间不能建立联系的
  • commit 信息写的过于冗余的

相信或多或少大家都曾碰到过。一旦涉及代码回滚,issue 回溯,changelog,语义化版本发布等操作时,作为 PM 肯定一脸懵逼 即使 PM 参与了全程的 CR 环节。

那理想中的 Git Commit Message 应该是要能较好的解决如上问题

  • 发生问题时快速让 PM 识别问题代码并回滚
  • commit 和 代码之间能建立起联系,并和相关的 issue 予以关联,做到任何代码都能区域性的解决问题(当然这也需要好的分支模型来支撑)

而 changelog,语义化版本发布这更像是合理化 commit 后水到渠成之事。

如何算比较好的 Git Commit Message

以个人来看,好的 commit 需要有以下特征

  • 有节制性的
  • 简明扼要的
  • 和代码,issue 强关联,利于 CR 的

如何写出规范化的 Git Commit Message

当前业界应用的比较广泛的是 Angular Git Commit Guidelines

具体格式为:

<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>

type: 本次 commit 的类型,诸如 bugfix docs style 等
scope: 本次 commit 波及的范围
subject: 简明扼要的阐述下本次 commit 的主旨,在原文中特意强调了几点 1. 使用祈使句,是不是很熟悉又陌生的一个词,来传送门在此 祈使句 2. 首字母不要大写 3. 结尾无需添加标点
body: 同样使用祈使句,在主体内容中我们需要把本次 commit 详细的描述一下,比如此次变更的动机,如需换行,则使用 |
footer: 描述下与之关联的 issue 或 break change,详见案例

一方面我们可以通过 commit 模板,但是这对于整体管控而言比较难以把握。所以如标题所诉,我采取了工具化的方式。

问题将会被拆分成

如何利用工具协助 生成 commit

commitizen 来格式化 git commit message 的工具,它提供了一种问询式的方式去获取所需信息,而在一个大框架下,我们肯定有自己想要遵循的范式(即个性化内容,比如 types 的类型),此时就由 commitizen 中的 adapter 来承载,例如如上提到的 angular 规范则是由 cz-conventional-changelog 来实现。

如何利用工具协助 校验 commit

commitlint 来校验 git commit message 的工具,而所需要校验的内容是否符合规范则和 commitizen 一样需要一个 adapter,例如校验 angular 规范的则由 @commitlint/config-conventional 来呈现。

何时校验才算合理

这就需要 husky 了。附所有可用的 hooks

在老版本中在 package.json

"scripts": {
  "commitmsg": "commitlint -e $GIT_PARAMS"
}

在新版本中

  "husky": {
    "hooks": {
      "commit-msg": "commitlint -e $GIT_PARAMS"
    }
  }

水到渠成的 changelog

依托于 commitizen 对 Git Commit Message 的规范化,我们非常容易依托 commit 信息来自动化生成 changelog。

正常情况下,我们可以使用 standard-versionsemantic-release 来生成 changelog。

standard-version 与 semantic-release 的区别,总结来说就是 standard-version 只针对 local git repo 而 semantic-release 则会牵扯到代码 push 亦或 npm publish。

另外如果你的项目是 mono repo 的,即通过 lerna 来管理的,然后代码又托管在 github 上,那么 lerna 也给了一套自己的解决方案,一种基于 github tag 给 pr 和 issue 打标的方式。这一块可以见我之前的文章 monorepo 新浪潮 | introduce lerna

项目实战

在实际我们的业务项目中当前有两种场景,一种是普通的 repo,还有一种是 mono repo,如果还不知道 mono repo 是什么的,可以参考下我之前写的这篇文章 monorepo 新浪潮 | introduce lerna,这篇文章也在文章上面有所提到。

普通 repo

为了让读者可以快速上手,我已经把相关内容整理到一个示例 repo - normal repo,对它的解释是 Starter kit with zero-config for building a library in ES6, featuring Prettier, Semantic Release, and more! 这是一个还在进行中的 repo,当前还确少 babel 那个部分,如果是 ts 用户的话 还需要 ts 那个部分,这些都是需要后续补上的部分。

接下来说下关键部分,先上 package.json

 "scripts": {
  "ct": "git-cz",
  "precommit": "lint-staged",
  "commitmsg": "commitlint -e $GIT_PARAMS",
  "release": "standard-version"
},
"config": {
  "commitizen": {
    "path": "./node_modules/cz-conventional-changelog"
  }
},
"standard-version": {
  "skip": {
    "commit": true,
    "tag": true
  }
},
"lint-staged": {
  "*.js": [
    "prettier --trailing-comma es5 --single-quote --write",
    "git add"
  ]
},

在常规开发中,我们的操作方式会变更为如下:

第一步:使用 commitizen 替代 git commit

使用

$ npm run ct

来替代原有的 git commit

如果你是 sourceTree 用户,其实也不用担心,你完全可以可视化操作完后,再在命令行里面执行 ct 命令,这一部分确实破坏了整体的体验,当前并没有找到更好的方式来解决。

第二步:格式化代码

这一步,并不需要人为干预,因为 precommit 中的 lint-staged 会自动化格式,以保证代码风格尽量一致

第三步:commit message 校验

这一步,同样也不需要人为介入,因为 commitmsg 中的 commitlint 会自动校验 msg 的规范

第四步:当有发布需求时

使用

$ npm run release

在这一步中,我们依托 standard-version 的能力,输出 changelog,细心的同学可以看到在配置 standard-version 时,我们忽略了相关的打标操作。 原因在于,我们会介入修改 changelog,因为依托 commit msg 的 changelog 对用户而言或许并不直观。 如果没有这种特殊需求的,可以选择打标。

"standard-version": {
  "skip": {
    "commit": true,
    "tag": true
  }
},

第五步:发布

$ npm publish

mono repo

同上,这是一个使用 mono repo 的快速上手示例。 对它的解释是 Starter kit with lerna and zero-config for building a library in ES6, featuring Prettier, Semantic Release, and more!

mono repo 最大的差异是,需要用不同的 commiizen adapter 来适配 mono repo 这种特殊的项目结构,所以在这边我们也选用了 cz-lerna-changelog,最大原因在于我们想要根据 commit 生成 changelog 时 commit 能落实到对应的 package,以及有一份归总的 changelog,这份 changelog 能说明所有的子 packages 的 changelog。

同样说下关键部分,先上 package.json

"scripts": {
  "ct": "git-cz",
  "precommit": "lint-staged",
  "commitmsg": "commitlint -e $GIT_PARAMS",
  "release": "lerna publish --conventional-commits --skip-git --skip-npm",
  "publish": "./tasks/publish.js"
},
"config": {
  "commitizen": {
    "path": "./node_modules/cz-lerna-changelog"
  }
},
"lint-staged": {
  "*.js": [
    "prettier --trailing-comma es5 --single-quote --write",
    "git add"
  ]
},

这边我只说下差异部分

第四步:当有发布需求时

使用

$ npm run release

在这一步中我们借助了 lerna 自身的能力来根据 commit msg 来生成了 changelog,同样我们忽略了打标,以及发布流程,原因依旧是我们需要修改自动化生成的 changelog。但这个操作带来的问题是后续需要手动进行 publish 的操作。在实际业务项目里面,我们的选择是在项目根目录中新建一个 tasks 目录,该目录内放一些自动化脚本,比如这里有 publish.js

所以这边变成利用 release 来生成 changelog,继而我们修改,然后再到根目录中执行 npm run publish 来执行 tasks/publish.js。这部分当前我还没有同步到示例中,后续会添加。

第五步:发布

$ npm run publish

缘由如上诉。

总结

前几天听 UCAN 分享,温伯华提到一点特别印象深刻,大意就是成长必定是上坡路,必定是艰辛的,其实没有如上工具照样可以开发,然而它的存在是让人养成良好的协作习惯,而好的习惯是可以让人受益终身的,所以希望作为读者的你,能踏上这个上坡路。

Ref: github.com/angular/ang…