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

2,572 阅读4分钟

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

We learn best by discovery, not instruction.

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

写在前面

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

安装 & 卸载

执行环境

node -v
# v16.4.0
npm -v
# 7.18.1
git --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/sh
if [ -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 0
fi

从前文可以知道,通过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执行前执行一些命令,该文件可以自己创建。

    该文件也是通过. ~/.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/bash

readonly 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
    fi
fi

Makefile

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

在非node项目中执行husky

mkdir new-project
cd new-project
git init
npm init
npm install -D husky
npx 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。

相关文章

推荐

参考