一个 git 仓库下拥有多个项目的 git hooks 配置方案
前言
通常情况下,一个 git 仓库就是一个项目,只需要配置一套 git hooks 脚本就可以执行各种校验任务。对于 monorepo 项目也是如此,monorepo 项目下的多个 packages 之间,它们是有关联的,可以互相引用,所以当成一个项目也没问题。
但是也有一种情况,一个 git 仓库下的多个项目之间是彼此独立的,比如 git 仓库下存在前端项目、后端项目、文档项目等等。这时候就需要为每个项目配置不同的 git hooks 脚本了,因为不同的项目有可能校验规则不一样。
本文主要探讨一下如何为不同的项目配置 git hooks 脚本。
PS:配置 git hooks 脚本使用 huksy。
方案一:每个项目下都配置一套 git hooks 脚本
假设仓库拥前后端两个项目:
frontend
backend
那么我们需要在每个项目下安装 husky
,同时要在 package.json
中配置一下 prepare
脚本(这里以前端项目为示例):
# package.json
{
"scripts" {
"prepare": "cd .. && husky install frontend/.husky"
}
}
然后按照 husky
文档创建 pre-commit
和 commit-msg
钩子文件:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# pre-commit
cd frontend
npx lint-staged
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# commit-msg
cd frontend
FORCE_COLOR=1 node scripts/verifyCommitMsg.mjs $1
上面展示的是前端项目的 git hooks 创建过程,后端项目按照同样的过程创建即可。目前仓库的目录结构如下:
frontend
.husky
- pre-commit
- commit-msg
backend
.husky
- pre-commit
- commit-msg
运行一段时间后,发现这个方案有问题,那就是每次触发的 git hooks 脚本都是前端项目的,后端项目提交代码根本不触发 git hooks。排查问题后发现是 git 仓库的配置引起的,打开 .git/config
文件:
[core]
hooksPath = frontend/.husky
上面 hooksPath
路径对应的就是 git hooks 的目录位置,目前 git 只支持指定一个目录作为 git hooks 的位置。所以第一个方案不靠谱,达不到我们想要的效果。
方案二:只在根目录下配置一套 git hooks 脚本
第二个方案是将 git hooks 放在项目根目录下,统一在根目录里执行各个子项目的校验脚本。这个方案有以下几个步骤:
修改 husky 安装位置
在每个项目下安装 husky 时,要把 git hooks 钩子目录设置在根目录:
# package.json
{
"scripts" {
"prepare": "cd .. && husky install .husky" # 放到根目录
}
}
同时 .git/config
文件也要修改一下:
[core]
hooksPath = .husky # 改为根目录
在 git hooks 中进行各个子项目的校验操作
这里以 commit-msg
作为示例编写一个脚本:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# 拿到所有改动的文件名
changedFiles=$(git diff --cached --name-only --diff-filter=ACM)
# 判断目录是否改动
isBackendChanged=false
isFrontendChanged=false
for file in $changedFiles
do
if [[ $file == frontend/* ]]
then
isFrontendChanged=true
elif [[ $file == backend/* ]]
then
isBackendChanged=true
fi
done
# 改动的目录需要执行校验命令
# $1 $2 代表传给函数的第一个、第二个参数
execTask() {
echo "root $1 commit-msg"
cd $1
FORCE_COLOR=1 node scripts/verifyCommitMsg.mjs $2
}
if $isFrontendChanged
then
execTask "frontend" $1 & # 使用 & 让任务在后台执行
task1=$! # 保存任务 id
fi
if $isBackendChanged
then
execTask "backend" $1 &
task2=$!
fi
if [[ -n $task1 ]]; then
wait $task1
fi
if [[ -n $task2 ]]; then
wait $task2
fi
echo "All tasks finished."
上面脚本的逻辑是这样的:
- 每次 git 提交代码时,判断一下当前所有改动的文件是属于哪个项目
- 文件发生改动的项目需要执行校验任务
- 每个校验任务都使用子进程去执行
- 等待所有校验任务执行结束后,输出
All tasks finished.
pre-push
脚本编写
与 pre-commit
和 commit-msg
不同,在 pre-push
钩子中需要通过其他方式来拿到发生改动的文件,大家直接看代码:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# 判断目录是否改动
isFrontendChanged=false
isComponentTemplateChanged=false
isComponentAttrPanelChanged=false
# 获取远程仓库的名字和 URL
remote="$1"
url="$2"
# 定义一个空的 git 哈希值
z40=0000000000000000000000000000000000000000
# 这个循环从 stdin 读取数据,这些数据是 git 在调用 pre-push 钩子时传递的。
# 每一行数据包括 4 个字段:本地引用名,本地最新的提交哈希值,远程引用名,远程最新的提交哈希值。
while read local_ref local_sha remote_ref remote_sha
do
# 这段代码检查是否正在删除一个引用(例如,删除一个分支)。如果是,那么本地的 sha 值将被设置为一个空哈希值。
if [ "$local_sha" = $z40 ]
then
# Handle delete
:
else
# 这段代码确定要检查哪些提交。如果远程的 sha 值是一个空哈希值,那么我们正在创建一个新的引用,所以我们需要检查所有的提交。
# 否则,我们正在更新一个已经存在的引用,所以我们只需要检查新的提交。
if [ "$remote_sha" = $z40 ]
then
# New branch, examine all commits
range="$local_sha"
else
# Update to existing branch, examine new commits
range="$remote_sha..$local_sha"
fi
# 这个循环对每一个包含在 range 变量中的提交执行 git rev-list 命令,这个命令会返回一系列的提交哈希值。
# 然后,对每个提交,我们使用 git diff-tree 命令来找到在那个提交中修改的文件。这些文件的名字被存储在 files 变量中。
for commit in $(git rev-list "$range"); do
# 拿到所有改动的文件名
files=$(git diff-tree --no-commit-id --name-only -r $commit)
for file in $files
do
if [[ $file == frontend/* ]]
then
isFrontendChanged=true
elif [[ $file == component-attr-panel/* ]]
then
isComponentAttrPanelChanged=true
elif [[ $file == component-template/* ]]
then
isComponentTemplateChanged=true
fi
done
done
fi
done
# 改动的目录需要执行校验命令
execTask() {
echo "root $1 pre-push"
cd $1
npm run type-check
}
if $isFrontendChanged
then
execTask "frontend" & # 使用 & 让任务在后台执行
task1=$! # 保存任务 id
fi
if $isComponentTemplateChanged
then
execTask "component-template" &
task2=$!
fi
if $isComponentAttrPanelChanged
then
execTask "component-attr-panel" &
task3=$!
fi
if [[ -n $task1 ]]; then
wait $task1
fi
if [[ -n $task2 ]]; then
wait $task2
fi
if [[ -n $task3 ]]; then
wait $task3
fi
echo "All tasks finished."
测试一段时间后,发现第二个方案没发生什么问题,完全满足需求。
PS:在写脚本的时候要注意各个任务是否能并发执行,比如 lint-staged
这个任务就不能并发执行,所以在编写 pre-commit
脚本执行代码校验的时候,得改为串行。