项目治理实践之代码仓库 commit 管理

892 阅读10分钟

我们可能不会记录每天自己干了什么, 但是 commit 会。

commit 信息管理是我们日常开发种很容易忽略的一个环节, 但当我们遇到以下情况时, 我们就会意识到有意义的 commit 信息对于项目的管理至关重要。

  • 在发起 PR 时, commit 的信息会加快 Code Review 流程
  • 在项目 Release 时, 可以根据 commit 信息生成 Changelog
  • 当项目出现 Bug 时, commit 信息可以帮助我们定位 Bug 的位置
  • 多人协作时, commit 信息可以让我们了解他人每次提交代码的原因

目前项目 commit 信息存在的问题

问题1:commit 信息随意填写

以我们的一个前端项目为例, 目前它的 commit 信息如下:

image.png

从上图红框中的 commit 信息我们可以看到, 每次代码提交的信息是由开发者随意填写的, 这会导致项目的维护成本增加, 我们甚至不知道自己提交了什么内容。

解决方案

规范 commit 的思路是: 在开发人员提交 commit message 时, 对 commit message 进行检查, 如果不符合规范则终止本次提交。

如下图所示, 在项目根目录的 .git/hooks 中我们可以看到一些预置 Git Hooks, Git Hooks 是在 Git 执行特定事件(如 commit、push 等)后触发运行的脚本, 可以帮我们自定义一些处理逻辑。

image.png

因为这是一个前端项目, 所以选择了 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 信息时会提示如下错误并无法提交代码。

image.png

当按照规范书写 commit message 后, 我们就可以正常提交代码了。

image.png

Tips: 在 git commit 时, 添加 --no-verify 可以跳过规范检查, 应对一些紧急情况。但希望大家永远不会用到。

问题2:因为代码格式不同, 导致 commit 记录混乱

在检查某一次 commit 时, 发现 diff 的内容如下:

image.png

可以看到左边和右边的代码由于格式不同产生了一次 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 来格式化代码后再提交。

image.png

问题3:在 commit 记录中会有无意义的 Merge 记录

继续检查项目的 commit 信息之后, 我们发现了大量的 Merge 信息如下:

image.png

多人协作时, 我们在本地提交代码, 并且想向远程服务器推送代码之前会先运行 git pull 命令来保证本地的代码和远程服务器的代码是同步的。 git pull 本质上是 git fetchgit merge 的组合, git merge 会产生这条 Merge 记录并随着我们这次的 commit 一起提交并推送至服务器。

解决方案

通过对 git pull 命令的分析, 我们可以得到两条规则:

  1. 如果远程分支超前于本地分支(即远程仓库有更新), 并且 本地没有 commit 操作, 此时 git pull 会采用 fast-forward 模式, 该模式不会产生合并节点, 不产生 Merge branch xxx of … 信息。

  2. 如果远程分支超前于本地分支(即远程仓库有更新), 并且 本地有 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 信息的记录这样:

image.png

或者这样:

image.png

导致这样提交的原因可能有很多, 比如:

  • 反复润色文档
  • 日常提交记录
  • 多次修改同一个 Bug

这样的多次提交依然会导致 commit 信息混乱, 也会导致 Code Review 非常困难。

首先要说明的是, 频繁在开发者自己分支上 commit 是被鼓励的。 并且开发者应该尽量保证每一次 commit “单一”, 这样当需求变化我们可以更好的应对。

这里有 2 个关于 commit 的最佳实践:

  1. One Thing,One Commit

在提交 commit 的时候尽量保证这个 commit 只做一件事情,比如实现某个功能或者修改了配置文件。

  1. 不要 commit 一半的工作

当开发任务没有完整的完成的时候,不要 commit。这并不是说每次 commit 都需要开发完成一个非常完整的功能,而是当把功能切分成许多小的但仍然具备完整性的功能点。

比如我们实际遇到的一个场景是: 产品经理说 A 功能这个版本不需要, 但下一个版本需要。面对这种情况时, 我们通常做法就是将 A 功能有关的代码全部找出来然后注释掉 , 会造成代码混乱, 难追踪, 难管理。如果我们一开始就养成将每个feature 单一 commit 的习惯, 我们可以通过 cherry pick 指定我们需要的 commit 合入主分支即可。

解决方案

目前问题的出现是因为 开发者自己的分支在合入主分支时, 没有将 commit 合并, 导致commit 信息混乱

因此在向多人协作分支合并代码之前我们要 整理 一下 commit, 将某些 commit 合并为一个 commit。

举一个合并 commit 的例子:

通过 git log 查看发现最近 3 次提交都是关于文档内容的修改

image.png

下面输入 git rebase -i HEAD~3 命令来合并这3次 commit, 输入命令后编辑器会弹出一个文件让我们整理 commit:

image.png

这时我们将前两次提交由 pick 改为 squash (或者缩写 s) 并关闭文件。

image.png

随后会再次弹出一个文件让我们编辑这3次提交合并的 commit 信息, 填写 commit 信息后即可完成合并。

image.png

再次 git log 检查提交记录, 我们发现 commit 已经被合并了。

image.png

问题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 工具提交

image.png

从上面的图中我们可以看到 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'],
  }

我们定义了几种常用的提交类型比如: featfixrefactor 等等, 并且将提交流程简化为 3 步如下:

image.png

总结

经过这次改造, 我们的 commit 信息和之前对比有了巨大的改变

Before:

image.png

After:

image.png

除此之外, 我们可以在 release 时通过 Changelog 工具轻松的生成 Changelog, 让我们对项目的改动一目了然。

package.jsonscripts 中增加一条 changelog 命令:

 "scripts": {
     "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s"
     ...
  }

之后, 通过 npm run changelog 我们即可得到如下图一样的 Changelog。

image.png

限制开发人员的 commit 信息, 本质上是提高我们软件开发人员的专业素养, 让我们能够更有信心应对大型项目中的维护和协作。