我们可能不会记录每天自己干了什么, 但是 commit 会。
commit 信息管理是我们日常开发种很容易忽略的一个环节, 但当我们遇到以下情况时, 我们就会意识到有意义的 commit 信息对于项目的管理至关重要。
- 在发起 PR 时, commit 的信息会加快 Code Review 流程
- 在项目 Release 时, 可以根据 commit 信息生成 Changelog
- 当项目出现 Bug 时, commit 信息可以帮助我们定位 Bug 的位置
- 多人协作时, commit 信息可以让我们了解他人每次提交代码的原因
目前项目 commit 信息存在的问题
问题1:commit 信息随意填写
以我们的一个前端项目为例, 目前它的 commit 信息如下:
从上图红框中的 commit 信息我们可以看到, 每次代码提交的信息是由开发者随意填写的, 这会导致项目的维护成本增加, 我们甚至不知道自己提交了什么内容。
解决方案
规范 commit 的思路是: 在开发人员提交 commit message 时, 对 commit message 进行检查, 如果不符合规范则终止本次提交。
如下图所示, 在项目根目录的 .git/hooks 中我们可以看到一些预置 Git Hooks, Git Hooks 是在 Git 执行特定事件(如 commit、push 等)后触发运行的脚本, 可以帮我们自定义一些处理逻辑。
因为这是一个前端项目, 所以选择了 Node.js 生态中的 husky 帮我们更方便的使用 Git Hooks。
首先安装 husky
npm i -D husky
然后在 package.json 添加以下代码安装 husky。
{
"scripts": {
"prepare": "husky install"
}
}
prepare 是 npm 的生命周期 Hook, 会在 npm install 之后被 npm 调用。这样保证了每个开发者在运行 npm install 安装前端项目依赖时会在本地完成 husky 的初始化工作。
husky 初始化之后我们可以在项目根目录看到 ./husky 目录。接下来我们通过添加 Husky Hook 就可以在开发者提交时检查 commit 信息。
commit 信息规范我们使用 Google 的 Angular 项目规范, 首先安装规范校验工具:
npm i -D @commitlint/config-conventional @commitlint/cli
Angular 这种大型项目对于 commit 信息规范是很严格的, 有些不太适用于我们团队, 因此我们需要定制一套自己的检查规则。
在项目根目录创建 .commitlintrc.js 制定校验规则:
module.exports = {
extents:[
"@commitlint/config-conventional"
],
rules:{
'body-leading-blank': [1, 'always'],
'footer-leading-blank': [1, 'always'],
'header-max-length': [2, 'always', 72],
'scope-case': [2, 'always', 'lower-case'],
'subject-case': [
2,
'never',
['sentence-case', 'start-case', 'pascal-case', 'upper-case']
],
'subject-empty': [2, 'never'],
'subject-full-stop': [2, 'never', '.'],
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],
'type-enum': [
2,
'always',
[
'chore',
'docs',
'feat',
'fix',
'refactor',
'revert',
'test'
]
]
}
}
接下来我们就要添加一个 huksy Hook, 当用户输入 commit 信息后触发规范检查工具, 在项目根目录的 .husky 目录添加 commit-msg 文件并输入以下内容:
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no-install commitlint --edit $1
这里有一点要注意的是: 上面脚本我们通过 npx 代替 npm 是因为 commit 校验工具我们仅安装到当前项目中, npm 运行项目内的库时只有 2 种方法:
- 通过 npm run-scripts 形式
- 通过指定安装路径比如
./node_modules/commitlint/.bin
这导致我们脚本编写复杂, 需要导入环境变量, 因此这里我们通过 npx 配合 --no-install 参数会自动找到项目内的库然后运行。
一切就绪后, 当我们再次随意填写 commit 信息时会提示如下错误并无法提交代码。
当按照规范书写 commit message 后, 我们就可以正常提交代码了。
Tips: 在 git commit 时, 添加 --no-verify 可以跳过规范检查, 应对一些紧急情况。但希望大家永远不会用到。
问题2:因为代码格式不同, 导致 commit 记录混乱
在检查某一次 commit 时, 发现 diff 的内容如下:
可以看到左边和右边的代码由于格式不同产生了一次 commit 记录, 这是由于开发者之间的代码格式没有统一的规范造成的, 严重影响 Code Review 和 协作开发。
解决方案
思路: 在开发者提交代码前, 运行 linter 工具对代码进行格式化, 确保提交的代码格式统一。
我们的前端项目是基于 Vue Cli 3 脚手架的, 自带了一个 linter 工具, 下面我们把它添加到 npm script。
"scripts": {
"lint": "vue-cli-service lint --fix",
...
接下来要做的事情就是添加一个 husky Hook 在用户提交 commit 信息之前运行刚才添加的 lint 命令。
husky add .husky/pre-commit "npm run lint"
之后我们再次 commit 时会先运行 linter 来格式化代码后再提交。
问题3:在 commit 记录中会有无意义的 Merge 记录
继续检查项目的 commit 信息之后, 我们发现了大量的 Merge 信息如下:
多人协作时, 我们在本地提交代码, 并且想向远程服务器推送代码之前会先运行 git pull 命令来保证本地的代码和远程服务器的代码是同步的。 git pull 本质上是 git fetch 与 git merge 的组合, git merge 会产生这条 Merge 记录并随着我们这次的 commit 一起提交并推送至服务器。
解决方案
通过对 git pull 命令的分析, 我们可以得到两条规则:
-
如果远程分支超前于本地分支(即远程仓库有更新), 并且 本地没有 commit 操作, 此时
git pull会采用fast-forward模式, 该模式不会产生合并节点, 不产生 Merge branch xxx of … 信息。 -
如果远程分支超前于本地分支(即远程仓库有更新), 并且 本地有 commit 操作, 此时若存在冲突,
git pull拉取代码时会要求分支合并 (即解决并合并冲突),会产生 Merge branch xxx of … 信息。
那么针对情况 2, 我们可以将 git pull 替换为 git pull --rebase 命令, 在rebase 模式下, 合并分支时不会生成一个新的 commit 节点,也就不会有 Merge 记录。
Tips: 通过如下 git 全局设置, 让 git pull 默认使用 rebase 模式:
git config --global pull.rebase true
问题4:对于同一个 feature 或者 bug 有多次 commit 记录
此外在查看 commit 信息的过程中, 发现存在很多相同 commit 信息的记录这样:
或者这样:
导致这样提交的原因可能有很多, 比如:
- 反复润色文档
- 日常提交记录
- 多次修改同一个 Bug
这样的多次提交依然会导致 commit 信息混乱, 也会导致 Code Review 非常困难。
首先要说明的是, 频繁在开发者自己分支上 commit 是被鼓励的。 并且开发者应该尽量保证每一次 commit “单一”, 这样当需求变化我们可以更好的应对。
这里有 2 个关于 commit 的最佳实践:
- One Thing,One Commit
在提交 commit 的时候尽量保证这个 commit 只做一件事情,比如实现某个功能或者修改了配置文件。
- 不要 commit 一半的工作
当开发任务没有完整的完成的时候,不要 commit。这并不是说每次 commit 都需要开发完成一个非常完整的功能,而是当把功能切分成许多小的但仍然具备完整性的功能点。
比如我们实际遇到的一个场景是: 产品经理说 A 功能这个版本不需要, 但下一个版本需要。面对这种情况时, 我们通常做法就是将 A 功能有关的代码全部找出来然后注释掉 , 会造成代码混乱, 难追踪, 难管理。如果我们一开始就养成将每个feature 单一 commit 的习惯, 我们可以通过 cherry pick 指定我们需要的 commit 合入主分支即可。
解决方案
目前问题的出现是因为 开发者自己的分支在合入主分支时, 没有将 commit 合并, 导致commit 信息混乱。
因此在向多人协作分支合并代码之前我们要 整理 一下 commit, 将某些 commit 合并为一个 commit。
举一个合并 commit 的例子:
通过 git log 查看发现最近 3 次提交都是关于文档内容的修改
下面输入 git rebase -i HEAD~3 命令来合并这3次 commit, 输入命令后编辑器会弹出一个文件让我们整理 commit:
这时我们将前两次提交由 pick 改为 squash (或者缩写 s) 并关闭文件。
随后会再次弹出一个文件让我们编辑这3次提交合并的 commit 信息, 填写 commit 信息后即可完成合并。
再次 git log 检查提交记录, 我们发现 commit 已经被合并了。
问题5:人为的约定大家遵守 commit 规则是不可靠的
在软件设计领域我们会经常听到 convention over configuration , 也就是约定优于配置, 这种设计范式会减少开发人员的心智负担, 因为 约定即是标准 。按照标准做, 我们就会减少出错的概率。
但在项目管理上 人为的约定 却是不可靠的, 所以我们需要借助工具来约束开发人员或者说是推进标准。
解决方案
通过 commitizen 工具, 我们可以简化开发人员的 commit message 书写, 更低门槛的推进项目标准。
安装工具:
npm i -D commitizen cz-conventional-changelog
然后在 package.json 中添加 npm run-scripts
"scripts": {
"commit": "git-cz",
...
之后我们提交代码时只需要运行 npm run commit, 即可使用 commitizen 工具提交
从上面的图中我们可以看到 Angular 提交工具对于我们还是有点严格的, 需要很多交互式问答。我们的前端项目复杂度不高, 因此需要自定义 commitizen 提交工具来简化使用。
安装 cz-customizable:
npm i -D cz-customizable
并且在 package.json 中添加如下设置:
"config": {
"commitizen": {
"path": "node_modules/cz-customizable"
}
}
最后按照我们的团队情况对提交工具进行定制:
在项目根目录新建 .cz-config.js
module.exports = {
types: [
{ value: 'feat', name: 'feat: A new feature' },
{ value: 'fix', name: 'fix: A bug fix' },
{ value: 'refactor', name: 'refactor: A code change that neither fixes a bug nor adds a feature' },
{ value: 'docs', name: 'docs: Documentation only changes' },
{ value: 'test', name: 'test: Adding missing tests or correcting existing tests' },
{ value: 'chore', name: 'chore: Other changes that do not modify src or test files' },
{ value: 'revert', name: 'revert: Reverts a previous commit' },
],
messages: {
type: 'Select the type of change that you are committing:\n',
subject: 'Provide a description of the change:\n',
footer: 'Does this change affect any open issues? E.g.: #32, #34 (optional):\n',
},
skipQuestions:['scope', 'breaking', 'body'],
}
我们定义了几种常用的提交类型比如: feat、fix、refactor 等等, 并且将提交流程简化为 3 步如下:
总结
经过这次改造, 我们的 commit 信息和之前对比有了巨大的改变
Before:
After:
除此之外, 我们可以在 release 时通过 Changelog 工具轻松的生成 Changelog, 让我们对项目的改动一目了然。
在 package.json 的 scripts 中增加一条 changelog 命令:
"scripts": {
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s"
...
}
之后, 通过 npm run changelog 我们即可得到如下图一样的 Changelog。
限制开发人员的 commit 信息, 本质上是提高我们软件开发人员的专业素养, 让我们能够更有信心应对大型项目中的维护和协作。