用 husky 约束 Git 提交

1,249 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情

大家好我是乔柯力,今天为大家介绍如何从工程化的角度用 husky 来配置项目,让团队所有人都按照既定规范来写代码和提交代码。

什么是 git hooks?

所谓的 git hooks 就是在 Git 的命令之前或之后执行一些脚本而已,最常用的两个是:

  • precommit:定义在 commit 之前的脚本
  • prepush:定义 push 之前的脚本

这些脚本都被放在了 .git/hooks隐藏目录内,以可执行文件的形式存在:

$ ls .git/hooks
applypatch-msg.sample     pre-applypatch.sample     pre-rebase.sample         update.sample
commit-msg.sample         pre-commit.sample         pre-receive.sample
fsmonitor-watchman.sample pre-merge-commit.sample   prepare-commit-msg.sample
post-update.sample        pre-push.sample           push-to-checkout.sample

这些以 .sample 结尾的是示例文件,不会真正执行的。我们可以创建一个真实的 pre-commit 脚本,里面写上一行代码 date,然后在本地随便创建一个 README.md 文件,然后 commit,你会发现输出了当前的日期和时间,说明 date命令生效了。

$ git commit -am 'feat: 测试pre-commit hook'
2022年10月13日 星期三 23时44分57秒 CST
[master (root-commit) 1c47f08] feat: 测试pre-commit hook
 1 file changed, 1 insertion(+)
 create mode 100644 README.md

我们把刚才的 date 去掉,换成 abcd 不存在的命令,然后再随便修改一点 README.md 代码,重新提交一次:

$ git commit -am 'feat: 测试会执行失败的 pre-commit hook'
.git/hooks/pre-commit: line 1: abcd: command not found

你会发现,commit 行为被拦截了,因为前置的 pre-commit 脚本执行报错。所以我们可以利用 Git 的这个特性来做一些很有价值的事情。

什么是 husky?

husky 是基于 git hooks 实现的一套共享 hooks 脚本的解决方案,便于团队协作。我们首先在项目中安装 husky 依赖:

$ yarn add husky --dev
yarn add v1.22.17
info No lockfile found.
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
success Saved lockfile.
success Saved 1 new dependency.
info Direct dependencies
└─ husky@8.0.1
info All dependencies
└─ husky@8.0.1

然后创建 husky 配置:

$ npx husky install
husky - Git hooks installed

这个命令执行完毕之后,项目中会多出一个 .husky 的目录:

.husky
└── _
    ├── gitignore
    └── husky.sh

这个目录是干什么用的呢?上面说过,.git/hooks是默认的保存 hooks 脚本的目录,但是用户可以自己修改,Git 提供了 core.hooksPath选项让用户自定义脚本目录,husky 就是利用了这个原理,当你查看配置的时候,会发现配置文件中多了一行:

core.hookspath=.husky

所以 husky 把 hooks 脚本目录改成了 .husky 目录,好处就是可以跟随代码一起提交到远程,大家一起遵循 hooks 约束。

到这里还有一个问题没解决,就是虽然你在本地创建了 husky 配置,修改了 hooks 目录,那如何确保大家都做这个事情呢?难道一个个通知吗?这肯定不现实。这时,就用到了 npm 提供的 prepare hook,我们输入下面的命令在 package.json 中添加 prepare 脚本:

npm set-script prepare "husky install"

这里用到了 npm 提供的 set-script 命令,意思是设置脚本命令,这个时候再看 package.json 文件会发现自动添加了一行 prepare 脚本用于执行 husky install命令:

"scripts": {
  "prepare": "husky install"
}

当你的同事 pull 下来仓库代码 install 完依赖之后,就会自动执行 prepare 脚本,于是 husky 就会被自动启用了。

接下来我们增加一个 pre-commit 的 hook:

$ npx husky add .husky/pre-commit "date"
husky - created .husky/pre-commit

这个时候 .husky 目录里面就多了一个 pre-commit 的脚本:

.husky
├── _
│   └── husky.sh
└── pre-commit

里面的代码是:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

date

意思是在 commit 之前,先执行这里面的脚本,如果脚本返回 0 则允许提交,脚本返回 1 则不允许提交,我们可以手动执行一下这个脚本看看:

$ bash .husky/pre-commit
2022年10月13日 星期三 23时30分00秒 CST

可以看到顺利执行了,也没有出错,像这种代码就不会阻止 commit 行为。我们可以继续在这个脚本里面添加一些其他代码,例如:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

date
node xxx.js
npm run xxx

说了这么多,究竟能干些什么具体的事情呢?接下来通过案例向大家展示:

案例分析

限制提交时间

假如有这么一个需求:为了防止内卷,只允许在工作日的 9:00~18:00 之间提交代码。 我们就可以设计这么一个方案了,首先在 .husky/pre-commit 脚本里面添加一行代码:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

node scripts/check-commit-time.js

然后创建 scripts/check-commit-time.js 文件,写下时间检查逻辑:

const now = new Date()
const week = now.getDay()
const hour = now.getHours()
const validWeek = week >= 1 && week <= 5
const validHour = hour >= 9 && hour < 18
if (validHour && validWeek) return
throw new Error('不在可提交时间段之内,拒绝内卷,从我做起!')

然后 commit 的时候,会先执行这段 js 代码,如果不在可提交时间段内,就会报错:

$ git commit -m "feat: 测试检查提交时间"
/Users/keliq/awesome-app/scripts/check-commit-time.js:7
throw new Error('不在可提交时间段之内,拒绝内卷,从我做起!')
^

Error: 不在可提交时间段之内,拒绝内卷,从我做起!
    at Object.<anonymous> (/Users/keliq/awesome-app/scripts/check-commit-time.js:7:7)
    at Module._compile (node:internal/modules/cjs/loader:1101:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
    at Module.load (node:internal/modules/cjs/loader:981:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:79:12)
    at node:internal/main/run_main_module:17:47
husky - pre-commit hook exited with code 1 (error)

如果在指定时间段内,则可以顺利提交:

$ git commit -m "feat: 测试检查提交时间"
[master (root-commit) 4f8a4df] feat: 测试检查提交时间
 1 file changed, 13 insertions(+)
 create mode 100644 package.json

拒绝不规范的 commit message

为了避免大家在提交时随意写 commit message,可以使用 validate-commit-msg 来进行约束,强制要求符合以下提交规范:

<type>(<scope>): <subject>

首先我们用 husky 添加一个新的 commit-msg hook:

$ npx husky add .husky/commit-msg 'npm run validate-commit-msg'

然后在 package.json 中添加一行 script:

$ npm set-script "validate-commit-msg" "validate-commit-msg"

如果用不规范的提交消息进行测试,会发现报错了:

$ git commit -m "检查commit msg"

> awesome-app@1.0.0 validate-commit-msg
> validate-commit-msg

INVALID COMMIT MSG: does not match "<type>(<scope>): <subject>" !
检查commit msg
husky - commit-msg hook exited with code 1 (error)

当消息格式正确时可以顺利提交:

$ git commit -m "feat: 检查commit msg"

> awesome-app@1.0.0 validate-commit-msg
> validate-commit-msg

[master 09ee0aa] feat: 检查commit msg
 1 file changed, 7 insertions(+), 5 deletions(-)

提交之前自动格式化代码

这里需要引入两个工具:

因此,首先安装依赖:

$ yarn add prettier lint-staged --dev

我们在项目中新建 .prettierrc 文件,设置格式化的规则,例如:

{
  "singleQuote": true,
  "semi": false,
  "printWidth": 400,
  "arrowParens": "avoid",
  "trailingComma": "es5",
  "proseWrap": "never",
  "quoteProps": "consistent"
}

具体字段的含义这里不做详细解释,感兴趣的同学可以参阅官方文档。然后随便写点代码到 src/index.js 文件中,例如第一行代码故意多加一个分号:

const nickname = 'keliq';
console.log(nickname)

然后用下面的命令测试 prettier 的效果:

$ npx prettier --check src
Checking formatting...
[warn] src/index.js
[warn] Code style issues found in the above file. Forgot to run Prettier?

发现出现了 warn 提示,说明代码格式不正确,我们把第一行末尾的分号去掉之后再运行就 OK 了:

$ npx prettier --check src
Checking formatting...
All matched files use Prettier code style!

上面的代码只是检测,那能不能当检查到代码格式不正确时,自动进行格式化呢?当然是可以的,只需要运行下面的命令即可:

$ npx prettier --write src

你可能会问,既然 perttier 这么强大,那还要 lint-staged 干啥?其实 lint-staged 最重要的功能是:只处理暂存区的文件!这是 prettier 做不到的。假如一个大项目有成百上千个文件,每次提交之前全量检查一下格式,是很耗费时间的,聪明的做法是只检查暂存区的代码即可,我们在 package.json 中添加下面的代码:

{
  "lint-staged": {
    "*": "prettier --write src"
  }
}

然后运行:

$ npx lint-staged
✔ Preparing lint-staged...
✔ Hiding unstaged changes to partially staged files...
✔ Running tasks for staged files...
✔ Applying modifications from tasks...
✔ Restoring unstaged changes to partially staged files...
✔ Cleaning up temporary files...

你会发现暂存区的代码已经被格式化了,所以只需要 pre-commit 脚本中运行 lint-staged 即可:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

node scripts/check-commit-time.js
npx lint-staged

其他

如果用 sourcetree 或者 fork 之类的可视化工具,提交的时候可能会报错:

.husky/pre-commit: Line 4 npm: command not found

在命令行中是可以正确触发钩子的,这是因为 GUI 工具找不到 husky 钩子中需要使用的命令,解决办法就是找到命令所在的路径,然后添加到 ~/.huskyrc文件中,例如获取 npm 所在的目录:

$ where npm
/usr/local/bin/npm

我们可以将该目录添加到环境变量中:

$ echo 'export PATH="/usr/local/bin/:$PATH"' >> ~/.huskyrc

另外,git hooks 的约束是防君子不妨小人的,因为可以通过命令绕过:

$ git commit -m "yolo!" --no-verify

如果哪天觉得约束不爽了想卸载掉 husky 也很简单:

$ yarn remove husky # 移除依赖
$ rm -rf .husky && git config --unset core.hooksPath # 删除目录重置core.hooksPath