写点什么

在 Git 项目中使用 husky 统一管理 hooks

用户头像
DoneSpeak
关注
发布于: 3 小时前

通过探索可以学得更多,而不是指令。

We learn best by discovery, not instruction.

-- 《程序员的思维修炼 | 开发认知潜能的九堂课》

写在前面

最近总想尽快调研完 husky 的项目,然后尽快确定项目中可以集成的 git-hook 管理工具。之前已经探究了pre-commit,再看完这个项目,就可以确定方案了。

安装 & 卸载

执行环境

node -v# v16.4.0npm -v# 7.18.1git --version# git version 2.24.3 (Apple Git-128)
复制代码

安装

  1. 安装 husky


npm install -D husky# + husky@6.0.0
复制代码


  1. 在项目中安装 husky


npx husky install
复制代码


执行之后可以再 package.json 的 devDependencies 看到 husky 的配置。并在根目录增加一个.husky/的目录。(之后会讲解.husky/_/husky.sh


.husky├── .gitignore # 忽略 _ 目录└── _    └── husky.sh
复制代码


查看.git/config,可以看到配置中修改了core.hooksPath指向为.husky。这就是husky 6.0.0的实现原理:替换.git/hooks的目录为自定义目录,且该目录会提交到远程仓库。


$ cat .git/config[core]        ...        hooksPath = .husky
复制代码


在知道可以修改core.hooksPath之前,我都是直接创建换一个软连接来实现该效果的rm .git/hooks && ln -s .husky .git/hooks


  1. 添加 husky install 到 package.json scripts 中


#(npm 7.x中才有效)npm set-script prepare "husky install"
复制代码


执行之后会在 package.json 的 script 中会增加一个 prepare。

卸载

npm uninstall husky
复制代码


6.0.0 版本做了很大的修改,且和之前的版本不再兼容。主要使用到了 2016 年 git 提供的新特性 core.hooksPath,允许用户指定自己的 githooks 目录。官方在 Why husky has dropped conventional JS config 给出了如下的说明。表示虽然可以直接修改core.hooksPath的指定路径将 hooks 放到仓库中,但 husky 提供了更多便捷的功能。


“Why still use husky if there’s core.hooksPath?"

Husky provides some safe guards based on previous versions feedbacks, user-friendly error messages and some additional features. It’s a balance between going full native and a bit of user-friendliness.

添加 husky hook

npx husky add .husky/pre-commit "npm test"
复制代码


执行之后会增加文件.husky/pre-commit(其中的注释是我另外添加的)。


#!/bin/sh# . 指令为source,表示不产生新的shell,在当前shell下执行命令,共享上下文,类似将两个文件拼接到一起# 执行 .husky/_/husky.sh. "$(dirname "$0")/_/husky.sh"
npm test
复制代码


当然你也可以直接在.husky/创建特定的 git hook,毕竟husky只是帮你重新指定了 hooksPath。

.husky/_/husky.sh

下面将讲解一下.husky/_/husky.sh以学习 husky 的设计原理。


.husky/_/husky.sh


#!/bin/shif [ -z "$husky_skip_init" ]; then  debug () {    # 当 HUSKY_DEBUG 存在值为1时,开启debug    [ "$HUSKY_DEBUG" = "1" ] && echo "husky (debug) - $1"  }
readonly hook_name="$(basename "$0")" debug "starting $hook_name..." # 如果 HUSKY=0,则忽略hook if [ "$HUSKY" = "0" ]; then debug "HUSKY env variable is set to 0, skipping hook" exit 0 fi # 如果存在~/.huskyrc,则执行 if [ -f ~/.huskyrc ]; then debug "sourcing ~/.huskyrc" . ~/.huskyrc fi # 设置husky_skip_init=1,再次执行hook时不再进入该代码块 export readonly husky_skip_init=1 # 再次执行当前hook,其中的$0为hook路径,$@为所有参数 sh -e "$0" "$@" exitCode="$?"
if [ $exitCode != 0 ]; then # 打印hook执行失败信息 echo "husky - $hook_name hook exited with code $exitCode (error)" exit $exitCode fi
exit 0fi
复制代码


从前文可以知道,通过husky生成的 hook 都会在开始都会通过. "$(dirname "$0")/_/husky.sh"执行一遍该文件,因此该文件会影响这些 hook 的行为。


#!/bin/sh# . 指令为source,表示不产生新的shell,在当前shell下执行命令,共享上下文,类似将两个文件拼接到一起# 执行 .husky/_/husky.sh. "$(dirname "$0")/_/husky.sh"
npm test
复制代码


从文件中,我们可以得知如下信息:


  1. 设置HUSKY_DEBUG=1开启 husky 的 debug


# 直接在终端设置变量值,也可以在~/.bash_profile中设置为全局环境变量HUSKY_DEBUG=1# 执行中指定变量值HUSKY_DEBUG=1 git commit -m "this is msg"
复制代码


  1. 设置HUSKY=0跳过 hooks 的执行,使用方法同HUSKY_DEBUG

  2. 可以通过~/.huskyrc在 hook 执行前执行一些命令,该文件可以自己创建。

  3. 该文件也是通过. ~/.huskyrc执行的,如果在该文件中exit 0或者exit 1都会直接结束 hook,前者表示正常执行,后者表示执行失败。


husky.sh有如下的执行流程:


  1. 执行 hook;

  2. 执行husky.sh,此时husky_skip_init=,因此执行判断内部代码。

  3. husky.sh中设置husky_skip_init=1并通过sh -e "$0" "$@"再一次执行当前 hook,此时因为husky_skip_init=1跳过判断内部代码,执行 hooks 剩余代码。


从代码上来看,这个有点绕的流程是为了获取到 hook 执行的结果,并在 debug 模式下输出。

自定义本地脚本

还是和之前的 pre-commit 一样,这里需要为开发者提供一个自定义本地 hooks 的功能。利用husky/_/.husky.sh会执行~/.huskyrc脚本的特性,可以在~/.huskyrc中进行功能拓展。


.huskyrc


#!/bin/bashreadonly CUSTOMIZED_HOOK="$(dirname "$0")/customized/$(basename "$0")"if [ -f "$CUSTOMIZED_HOOK" ];then    debug "husky - run customized hook - $CUSTOMIZED_HOOK"  sh -e $CUSTOMIZED_HOOK $@    resultCode="$?"    if [ $resultCode != 0 ]; then        echo "husky - $CUSTOMIZED_HOOK hook exited with code $resultCode (error)"        exit $resultCode    fifi
复制代码


Makefile


install-husky-hooks: ifeq ($(wildcard .husky/_/husky.sh),)  npx husky installendififeq ($(wildcard ~/.huskyrc),)  ln -s $(PWD)/.huskyrc ~/.huskyrcelse  @echo "Fail: ~/.huskyrc already exist, please handle it manually."endififeq ($(wildcard .gitignore),)  touch .gitignoreendififeq ($(shell grep -c .husky/customized .husky/.gitignore),0)  echo "\n# 忽略.husky/customized中开发人员自定义脚本\n.husky/customized" >> .husky/.gitignoreendif
复制代码

在非 node 项目中执行 husky

mkdir new-projectcd new-projectgit initnpm initnpm install -D huskynpx husky install
复制代码


这样做虽然也没有问题,可以正常的使用 husky 定义的 hook,毕竟 hooks 也仅仅重新指定了core.hooksPathd.husky/。但一个非 node 项目中包含了一个node_module,package.jsonpackage-lock.json总觉得有些奇怪。

vue 项目中的 yokie

按照 Vue Cli 的网站说明。在安装之后,@vue/cli-service 也会安装 yorkie,它会让你在 package.jsongitHooks 字段中方便地指定 Git hook:


{  "gitHooks": {    "pre-commit": "lint-staged"  },   "lint-staged": {    "*.{js,vue}": [      "vue-cli-service lint",      "git add"    ]  }}
复制代码


yorkie fork 自 husky并且与后者不兼容。


yokie 显示的语法为 husky 6.0.0 之前的版本的写法,husky 6.0.0 之后就没有支持了。

总结

在了解到husky时,我觉得很厉害,可以简单地通过一个配置文件添加 git hooks。但因为我本身是做后端开发的,所以多少又有点失望,毕竟没法很好集成到后端项目中。在这次进行学习了解之后,又觉得 husky 好像也没有做什么,和我之前写的gromithook/git-hooks的思路也基本相同。当然,他确实有解决了提交 hook 到仓库统一团队开发的 hook,也可以取巧地实现开发人员本地自定义 hook 的功能。但还是没有之前的pre-commit那样给人带来惊喜。


无中央 hooks 仓库,复用靠拷贝: 之前开发的时候,因为团队中有多个 repo,但都需要集成相同的 hook,这时候如果使用 husky,那么就需要将这些 hook 拷贝到每个项目 中。如果使用 pre-commit,则没有这个烦恼,因为 pre-commit 是一个远程 hook 仓库+config 文件的配置方式,只需要修改配置文件即可实现多个项目使用相同的 hooks。


每个 hook 只能定义一个: 还有一处是 husky 令人感到失望的,每个 hook 只能定义一个文件,如果要在一个阶段做多种任务,那么久必须将这些任务都写到一个 hook 中。


综上,如果不需要在多个 repo 中共享 hook,且 hook 任务比较简单,那么可以考虑选择 husky,否则就是用 pre-commit。这里我首推 pre-commit。

相关文章

推荐

参考

发布于: 3 小时前阅读数: 3
用户头像

DoneSpeak

关注

Let the Work That I've Done Speak for Me 2018.05.10 加入

Java后端开发

评论

发布
暂无评论
在Git项目中使用husky统一管理hooks