soda-x/blog

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

soda-x opened this issue · 12 comments

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

如果你把 commitizen 安装在全局,即 -g

那么也可以使用

$ git 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: https://github.com/angular/angular.js/blob/master/DEVELOPERS.md

前几周给项目了加个这个,leader 说没必要。。。团队总共就4个人,加了还麻烦。。

@huyansheng3 应该坚持一下 XD

一方面可以让团队协作更规范和顺畅些,另外一方面也是培养下良好的开发习惯,这是让人受益匪浅的地方。

代码实现 架构 可以张扬个性,但最基本的诸如代码规范,提交日志还是需要统一化。

其实我是更看中一旦规范化,对于 issue 的溯源,CR,和后续自动化 changelog 都有着比较重要的意义。

done


action

  • 夯实两个 starterkit

同感。在没有使用 commitizen 之前,commit message 都是写得很随意。即不利于团队开发也不方便查看,很容易出现一些无意义的提交信息。
后面公司项目和个人项目都用上了,整个 commit message 看起来就规范且清晰多了。

体验上可以参考 https://github.com/tj/git-extras 的做法。

git cmtnpm run cmt 这种 体感会好很多

@coolme200 嗯,其实把 commitizen 装在全局,全局就可以使用 git ct

我把这个部分同步到文章内。

@pigcan 嗯,分支还保留着。。虽然没有加上去,但自己每次提交的时候都会按上面的规范来。如果改变不了别人,就从改变自己开始吧。

为啥用git ct,空注释也能提交,好像没经过校验

我是 commitizen 装在里项目里,然后 yarn commit 来提交 commit message。
然后利用 hook 在 commit 后执行 lint 和 test 。如果不通过则自动取消 commit。

standard-version可以尝试,但是的确是不太好用。

  1. 像一般项目,可能要几年时间才能发3个/4个大版本,使用这个工具的频率不高。

  2. 像是次版本的频率,再加上这个工具不是太智能,也不太可能会用这个工具。

  3. standard-version所做的工作不多,也可以被其他工具(生成changelog之类的工具)代替;其余部分还是由人工来完成关键的步骤比较踏实一点(人工取代standard-version的工作量也不麻烦啊)

学习了

为啥我使用standard-version -r 1.0.0 -p beta来指定版本号会不生效呢?