前言
通常情况下,一个 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
脚本执行代码校验的时候,得改为串行。