通过探索可以学得更多,而不是指令。
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)
安装
- 安装husky
npm install -D husky
# + husky@6.0.0
- 在项目中安装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
。
- 添加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’score.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
从文件中,我们可以得知如下信息:
- 设置
HUSKY_DEBUG=1
开启husky的debug
# 直接在终端设置变量值,也可以在~/.bash_profile中设置为全局环境变量
HUSKY_DEBUG=1
# 执行中指定变量值
HUSKY_DEBUG=1 git commit -m "this is msg"
- 设置
HUSKY=0
跳过hooks的执行,使用方法同HUSKY_DEBUG
。 可以通过
~/.huskyrc
在hook执行前执行一些命令,该文件可以自己创建。该文件也是通过
. ~/.huskyrc
执行的,如果在该文件中exit 0
或者exit 1
都会直接结束hook,前者表示正常执行,后者表示执行失败。
husky.sh
有如下的执行流程:
- 执行hook;
- 执行
husky.sh
,此时husky_skip_init=
,因此执行判断内部代码。 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),)
# 如果没有安装husky,安装husky
npx husky install
endif
ifeq ($(wildcard ~/.huskyrc),)
# 如果没有~/.huskyrc, 则创建为项目根目录的~/.huskyrc
ln -s $(PWD)/.huskyrc ~/.huskyrc
else
@echo "Fail: ~/.huskyrc already exist, please handle it manually."
endif
ifeq ($(wildcard .gitignore),)
# 如果没有.gitignore文件,则创建
touch .gitignore
endif
ifeq ($(wildcard .husky/customized),)
# 如果没有customized目录,则创建
mkdir -p .husky/customized
endif
ifeq ($(shell grep -c /customized .husky/.gitignore),0)
echo "\n# 忽略.husky/customized中开发人员自定义脚本\n/customized" >> .husky/.gitignore
endif
执行如下指令会自动完成husky的安装,并创建软连接到~/.huskyrc
。
cd <the-path-of-Makefile>
make install-husky-hooks
# 查看 ~/.huskyrc
创建自定义hooks
你可以选择通过如下指令将原本在.git/hooks/
中所有的hooks拷贝到.husky/customized
,也可以手动在.husky/customized
中创建自己的git hook。
cp -Rf .git/hooks/* .husky/customized/
之后就可以按照平常的方式使用啦。
在非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.json
和package-lock.json
总觉得有些奇怪。
vue项目中的 yokie
按照Vue Cli的网站说明。在安装之后,@vue/cli-service
也会安装 yorkie,它会让你在 package.json
的 gitHooks
字段中方便地指定 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。
相关文章
推荐
参考
- husky @typicode.github.io/husky
- husky @github.com
- Why husky has dropped conventional JS config 说明6.0.0版本修改的原因
- npm-scripts @docs.npmjs.com
- Husky原理解析及在代码Lint中的应用 讲解了husky 6.0.0 之前版本的配置和原理
- 记一次gitHook带来的思考🤔 同时用到husky和yorkie导致的问题,简单来说就是旧版本的husky和yorkie都会直接修改.git/hooks中脚本
- yorkie @github.com
- Vue Cli/cli-service/git-hook
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。