使用 husky 实现基础代码审查
在日常提交 PR 的过程中,我们提交的文件不应该有例如 console、debugger、test.only 等调试语句,这会影响到线上代码。那每次提交之前都检查似乎又像是一个繁琐的工作,如果有个工作能代替我们检查我们提交的代码,让不能提交到线上的代码在 commit 阶段停止下来,对 code reviewer 的工作会减少不少。 这里就来跟大家探讨一下我的一个实现方式。
前言
在提交代码的时候不知道大家有没有注意命令行中会打印一些日志:
像这里的
husky > pre-commit
🔍 Finding changed files since ...
🎯 Found 3 changed files.
✅ Everything is awesome!
husky > commit-msg
这个出处大家应该都知道,来自 pretty-quick ,然后通过 packge.json 中的:
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
"pre-commit": "pretty-quick --staged"
}
}
这段代码在我们 commit 的时候对代码进行相关处理。
这里是 husky 调用了相关的 Git hooks ,在合适的时机处理我们提交的代码文件,从而使我们的代码达到提交的要求(如上面的是格式化相关代码)。
看到这里肯定大家就会想到,那这个是不是可以做的还有更多?
没错,下面就直接上配置,来实现我们不想让某些代码提交到线上这样的需求。
第一版
知道这个原理,那就很简单了,我们在 pre-commit
这个事件触发的时候对我们要提交代码检查一下,看其中有没有那几个关键字就可以了。
那就直接动手:
我们在项目根目录找到 .git/hooks
:
可以看到,这里提供了各种触发时机,我们找到我们想要的 pre-commit
(如果后缀有 .sample,需要去除掉才能让此文件生效)。打开此文件,前端的话应该看到的是(不需要阅读):
#!/bin/sh
# husky
# Hook created by Husky
# Version: 2.7.0
# At: 2023/2/2 13:14:26
# See: https://github.com/typicode/husky#readme
# From
# Directory: /Users/frank/Documents/work/worktile/wt-cronus/projects/pc-flow-sky/node_modules/husky
# Homepage: undefined
scriptPath="node_modules/husky/run.js"
hookName=`basename "$0"`
gitParams="$*"
debug() {
if [ "${HUSKY_DEBUG}" = "true" ] || [ "${HUSKY_DEBUG}" = "1" ]; then
echo "husky:debug $1"
fi
}
debug "$hookName hook started"
if [ "${HUSKY_SKIP_HOOKS}" = "true" ] || [ "${HUSKY_SKIP_HOOKS}" = "1" ]; then
debug "HUSKY_SKIP_HOOKS is set to ${HUSKY_SKIP_HOOKS}, skipping hook"
exit 0
fi
if ! command -v node >/dev/null 2>&1; then
echo "Info: can't find node in PATH, trying to find a node binary on your system"
fi
if [ -f "$scriptPath" ]; then
# if [ -t 1 ]; then
# exec < /dev/tty
# fi
if [ -f ~/.huskyrc ]; then
debug "source ~/.huskyrc"
. ~/.huskyrc
fi
node_modules/run-node/run-node "$scriptPath" $hookName "$gitParams"
else
echo "Can't find Husky, skipping $hookName hook"
echo "You can reinstall it using 'npm install husky --save-dev' or delete this hook"
fi
看了下,感觉没啥用,就是一个检测 husky 有没有安装的脚本。我们这里直接使用下面的替换掉:
#!/bin/sh
errorForOnly() {
result=""
for FILE in `git diff --name-only --cached`; do
# 忽略检查的文件
if [[ $FILE == *".html"* ]] ; then
continue
fi
# 匹配不能上传的关键字
grep 'serial.only\|console.log(\|alert(' $FILE 2>&1 >/dev/null
if [ $? -eq 0 ]; then
# 将错误输出
echo '❌' $FILE '此文件中包含 [only]、[console]、[alert] 中的关键字, 删除后再次提交'
# exit 1
result=0
else
result=1
fi
done
if [[ ${result} == 0 ]];then
exit 1
fi
echo "✅ All files is OK!"
}
errorForOnly
然后我们在一些文件中添加我们不想要的关键字,然后 git commit
:
可以看到错误日志以及文件已经在命令行中打印出来了,同时文件也没有进入本地仓库(Repository),让然在我们的暂存区(Index)。
使用这个方式,在一定程度上我们避免了提交一些不想要的代码到线上这种情况了发生。同时也是在本地提交代码之前就做了这个事情,也避免了使用服务端 hooks 造成提交历史混乱的问题。
这时候你肯定会产生这样的疑问:
【欸,欸,欸,不对啊】
问题
这种方式有个显而易见的问题,那就是不同团队协同方面。由于 hooks 本身不跟随克隆的项目副本分发,所以必须通过其他途径把这些 hooks 分发到团队其他成员的 .git/hooks
目录并设为可执行文件。
另外一个问题是我们使用了 husky,在每次 npm i
之后都会重置 hooks 文件。也就是 .git/hooks/pre-commit
文件恢复到了最初(只有 husky 检测的代码)的样子,没有了我们写的逻辑。
这种情况是不能允许的。那就寻找解决途径。查了下相关文档,发现可以使用新版的 husky 来解决这个问题。(其他相关的工具应该也可以,这里使用 husky 来进行展示)。
最新实现
husky 在 v4 版本之后进行了大的重构,一些配置方式不一样了,至于为什么重构,大家可以去
安装
安装 npm install husky --save-dev
安装最新版本为 8.0.3
,安装完成后启用 Git hooks: npx husky install
在团队协作的情景下,得让本团队的其他人也能自动的启用相关 hooks ,所以添加下面这个命令,在每次 npm install 之后执行:
npm pkg set scripts.prepare="husky install"
我们就在 package.json 得到了这样的命令:
yarn2+ 不支持 prepare 生命周期脚本命令, 安装方式在此处
使用
先按照官方文档测试一下,执行 npx husky add .husky/pre-commit "npm test"
在相关目录我们就看到:
相应的文件以及内容已经准备就绪。这里就不运行了。
那如何将之前的流程使用新的版本配置好呢?
这里直接提供相关文件内容:
# .husky/commit-msg
# 用于 commit 信息的验证
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no -- commitlint --edit $1
使用下面的语句生成;
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit `echo "\$1"`'
另外还有 pre-commit:
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# pretty-quick 相关
npx pretty-quick --staged
# 验证是否有调试关键字的脚本:
node bin/debugger-keywords.js
脚本内容:
// bin/debugger-keywords.js
const { execSync } = require('child_process');
const fs = require('fs');
const gitLog = execSync('git diff --name-only --cached').toString();
const fileList = gitLog.split('\n').slice(1);
const keyWordsRegex = /(console\.(log|error|info|warn))|(serial\.only)|(debugger)/g;
const result = [];
for (let i = 0; i < fileList.length; i++) {
const filePath = fileList[i].trim();
if (filePath.length === 0 || filePath.includes('.husky') || filePath.includes('bin/')) {
continue;
}
const fileContent = fs.readFileSync(`${filePath}`, 'utf8');
const containerKeyWords = Array.from(new Set(Array.from(fileContent.matchAll(keyWordsRegex), m => m[0])));
if (containerKeyWords.length > 0) {
const log = `❌ ${filePath} 中包含 \x1B[31m${containerKeyWords.join('、')}\x1B[0m 关键字`;
result.push(log);
console.log(log);
}
}
if (result.length >= 1) {
console.log(`💡 修改以上问题后再次提交 💡`);
process.exit(1);
} else {
console.log('✅ All files is OK! \n');
process.exit(0);
}
为几个文件添加 console
、测试 only
等,提交 commit 后效果展示:
会提示文件中不合规的关键字是哪些。
更多
有了以上的使用示例,我们可以在随意添加脚本,比如,为 19:00 之后或周末提交代码的你来上一杯奶茶🧋和一个甜甜圈🍩:
// bin/check-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 < 19
if (validHour && validWeek) return
console.log(`🌃 来点 🧋 🍩`);
这次为了方便也在 pre-commit hook 中执行:
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# pretty-quick 相关
npx pretty-quick --staged
# 验证是否有调试关键字的脚本:
node bin/debugger-keywords.js
# 来杯奶茶
node bin/check-time.js
来看下结果:
结论
目前单纯使用 pre-commit hook 针对常见的 console、debugger 还有测试 only 这种。但是我们也可以看到只要我们写不同的脚本,可以实现不同的需求。
之后如果有更多的需求也能继续添加脚本,也可以产生我们 PingCode 自己的 lint 插件。
ps. 如果使用可视化工具提交可能会报错,大家可以自行查阅解决。
作者:阿朋
来源:juejin.cn/post/7202392792726011959