Git 钩子

基本使用

Git 钩子(hook)本质上是在特定的 Git 操作发生时执行的脚本(可以是 shell 脚本,也可以是其他的可执行脚本,如 JavaScript、Python、Ruby 脚本等,需要在首行指定该脚本使用的语言)。例如,如果我们希望能够每次在执行 git commit 后、新的 commit 记录创建前(甚至在输入 commit message 之前),统计某个目录下的 markdown 文件内的文字个数,可以在 .git/hooks/pre-commit 文件里添加这样的内容:

#!/bin/sh
num=$(find ./src -type f -name "*.md"  | xargs wc -m | grep total)
echo ""
echo "\033[36m-------------------------\033[0m"
echo "\033[36m 总字数 =$num\033[0m"
echo "\033[36m-------------------------\033[0m"
echo ""

注:在实践之前,可以先检查一下 .git/hooks/pre-commit 文件的权限,确保当前用户具有执行权限:

$ chmod a+x .git/hooks/pre-commit

这样一来,在执行 git commit 操作后,就会先触发该脚本的执行:

$ git add .
$ git ci --amend

# 填写完 commit message 之后,打印如下内容:

-------------------------
总字数 =  115534 total
-------------------------

[master 60f6b23] add about git hooks
 Date: Wed Aug 1 00:56:02 2018 +0800
 1 file changed, 14 insertions(+)

阻断事件的默认行为

大部分钩子都约定了零值作为成功执行的标识。如果希望在某个钩子退出时停止后续的 Git 行为,那么该钩子应该以非零值退出。例如,在执行了 ESLint 后发现语法问题,希望开发者先修复问题后再提交,那么 .git/hooks/pre-commit 文件需要以非零状态退出程序:

#!/bin/sh
echo "总是阻止提交"
exit 1

此时如果对代码做出改动,然后运行:

$ git add --all
$ git commit

此时,即使填写了提交信息,刚才的变动也依然在暂存区,并没有进入代码仓库。

其他 git 操作事件及各自常见的应用场景

在每个 git 仓库的 .git/hooks 目录下,都可以看到若干个 *.sample 文件,它们就是 git 给出的各个钩子的示例。

$ cd .git/hooks
$ tree
.
├── applypatch-msg.sample
├── commit-msg.sample
├── post-update.sample
├── pre-applypatch.sample
├── pre-commit.sample
├── pre-push.sample
├── pre-rebase.sample
├── prepare-commit-msg.sample
└── update.sample

注:上面使用了 tree 这个程序,用来展示某个目录下的文件/目录结构。它并非系统自带,Mac 下可以通过 brew install tree 来安装。

Git 钩子可以分为两大类:客户端钩子(client side hooks),服务器端钩子(Server side hooks)。Git 钩子的数量可能在不断增加,在这里,我们只重点介绍几个常用的。其他的钩子可以参考 Git 的专业书籍。

客户端钩子

pre-commit

pre-commit 是最先执行的一个钩子,在敲入提交信息之前被执行。使用 git commit --no-verify 则可以跳过这个钩子。pre-commit 钩子经常被用于执行单元测试或者运行代码检查,下面分别看一个例子。

首先,我们在项目的 package.json 里声明 testlint 两个指令:

{
  "scripts": {
    "test": "mocha test/index.js",
    "lint": "eslint ./src -c eslintrc.js --ext .js"
  }
}

然后,在 .git/hooks/pre-commit.sh 文件里定义钩子内容:

#!/bin/bash
echo "执行单元测试..."
npm run test

echo "执行代码检查..."
npm run lint

那么,以后每次在运行 git commit 后,会先运行上面的 .git/hooks/pre-commit.sh,如果正常结束(exit status0),则继续提交;如果运行出错,即单元测试或者代码检查发现了问题而以非零值退出,则会中断提交过程。

pre-rebase

pre-rebase 钩子会在我们通过 git rebase [-i] [<branch-name> | <commit-id>] 进行变基或交互式变基操作时执行。它接收一个或者两个参数:

  • $1,上游仓库
  • $2,要作为 rebase 的基准的分支,可能为空

变基操作不宜在公共分支上面进行,因此这个钩子很有用,可以防止团队中的任何成员不小心在某些公共分支上面变基。例如,常见的公共主分支 master,我们可以通过下面这一个钩子,避免开发人员在 master 分支上面执行 git rebase

#!/bin/bash
# TODO

共享钩子

写在 .git/hooks 目录下的钩子有个问题,就是并不能自动跨计算机共享。这样一来,如果多人在一个项目中协作开发,就需要一个机制来确保所有人的钩子都有效。

一个简单的办法是将钩子放到项目中,与源码一同接收版本控制,然后提供一些命令方便每个成员初始化钩子。

使用自动化脚本

可以将写好的钩子统一放到一个目录中,例如 scripts,然后在项目的 package.json 里添加一个脚本:

"scripts": {
  "init-hooks": "cp ./scripts/pre-commit.sh ./.git/hooks/pre-commit && chmod a+x .git/hooks/pre-commit"
}

其他项目成员在获取到项目代码后,就可以通过 npm run init-hooks 来初始化所有的钩子了。

npm 钩子工具

除了借助 shell 脚本,还可以使用 npm 生态下的工具。例如 husky

husky 在下载完成后,会执行其 package.json 里用 scripts.install 指定的命令(所以说,本质上是利用 npm 的钩子来创建 git 的钩子):

{
  "scripts": {
    "install": "node ./bin/install.js"
  }
}

该脚本会自动在 .git/hooks 目录下添加所有的钩子文件,并使用一个模板进行初始化(如果已有某个钩子,则跳过)。

然后,git 操作触发某个钩子执行时,该钩子都会读取项目的 package.json 里面对应的脚本(例如 pre-commit 钩子会去寻找 precommit),并执行之。

参考资料

  1. husky