背景
最近公司一声令下,开始搞"标准化开发"大行动,意思是:代码可以不惊艳,但提交必须整整齐齐、仪式感拉满。于是,一份"神圣不可更改"的规范文档横空出世,赫然写着:
- Git 分支名必须像早操队伍一样整齐划一;
- 提交信息不能胡写,必须有理有据、感情真挚;
- 提交信息必须绑定 Issue ID,不然你都不知道自己为哪桩需求流的汗;
- 提交代码前,还得通过 ESLint 的"质检",不然就别想进主仓。
规矩定得挺好,但问题来了------谁来确保这套规则不是贴在墙上的"企业文化标语"?
这个"项目督查"的荣誉任务,砸到了我的头上。
好在我和 Husky 有点过节------啊不,是革命友谊。当年用它给项目提交代码时"上纲上线",快,准,稳。现在领导提的这些功能,怎么看都像是 husky 的拿手好戏,妥妥的熟面孔,不慌!
但,故事总得有点挑战才精彩------这次不是给一个项目搞定就完事,而是......四十多个项目!
一个个手动配置?那得配到退休。
幸好我懂点 shell 脚本,husky 配 shell,那简直是"黑科技联姻",兵器谱前十都有它们的位置。就这样,我撸起袖子,开干。
整体思路
想象你是一个偏爱效率(也稍微有点懒)的开发者,眼前有 40 多个项目等着你去"上锁" ------ 锁什么?当然是锁代码提交规范、代码质量底线,还有开发者随心所欲的"放飞自我"!
一个个手动配置?那怕是要从青春配到秃顶。你需要帮手,于是灵机一动,写下了一段全自动驯狗大师脚本(别慌,说的是那只叫 Husky 🐶 的"钩子狗"),让它替你在每个项目里立下规矩、震慑八方。
🔒 脚本的整体思路是这样的:
-
问你用的什么狗粮(包管理工具)
脚本先抬头看你一眼:你到底是
npm
、yarn
还是pnpm
派的?不给狗粮不干活! -
喂它 git-hooks-config(远程配置)
它拿着你选的工具,去远程仓库拉取"训狗秘籍"(git-hooks-config),也就是一堆标准化配置,省得你一个项目一个项目配得吐血。
-
检查秘籍里缺不缺"内功心法"
它偷偷翻了下
package.json
,看看有没有把秘籍中定义的必备依赖(devDependencies
)都装上------这些可是 Husky 能否顺利开口"咬人"的关键。缺了?那赶紧装上,不然就是没有牙齿的看家犬! -
开始训狗(安装 husky)
它比较"喜新厌旧"------一看你家角落还蹲着只旧 Husky,立刻开门送走(
rm -rf .husky
),转身就牵来一只全新的,重新从零训练,绝不恋旧,只认新欢! -
复制招式(配置文件和 hooks)
把那些"拷打开发者提交"的钩子(hook)及其附属配置文件
commitlint.config.js
,header-pattern
通通拷进项目里,放好位置,安排妥当。 -
狗训完了,把秘籍销毁
一切搞定后, 它最后很讲究,把那本"git-hooks-config"秘籍原地销毁(卸载),只留下精华在项目里,一点也不拖泥带水, 优雅退场。
bash
#!/bin/bash
set -e
export PUPPETEER_SKIP_DOWNLOAD=true
TOOL=$1
if [ -z "$TOOL" ]; then
echo "❌ 请输入要使用的包管理工具,例如:bash init-git-hooks.sh pnpm"
exit 1
fi
INSTALL_CMD=""
ADD_CMD=""
case $TOOL in
npm)
INSTALL_CMD="npm install git+ssh://git@gitee.com:getbetter/git-hooks-config.git -D"
ADD_CMD="npm install -D"
;;
yarn)
INSTALL_CMD="yarn add git+ssh://git@gitee.com:getbetter/git-hooks-config.git -D"
ADD_CMD="yarn add -D"
;;
pnpm)
INSTALL_CMD="pnpm add git+ssh://git@gitee.com:getbetter/git-hooks-config.git -D"
ADD_CMD="pnpm add -D"
;;
*)
echo "❌ 不支持的包管理工具: $TOOL"
exit 1
;;
esac
echo "🔧 使用 $TOOL 安装 git-hooks-config 包..."
eval "$INSTALL_CMD"
echo "📦 动态解析 git-hooks-config 中的 devDependencies 并安装到主项目中..."
PKG_PATH="node_modules/git-hooks-config/package.json"
if [ ! -f "$PKG_PATH" ]; then
echo "❌ 未找到 package.json: $PKG_PATH"
exit 1
fi
DEPS=$(node -e "const deps=require('./$PKG_PATH').devDependencies; console.log(Object.entries(deps).map(([k,v])=>k+'@'+v).join(' '))")
if [ -z "$DEPS" ]; then
echo "⚠️ 未发现依赖需要安装"
else
echo "➡️ 安装依赖: $DEPS"
eval "$ADD_CMD $DEPS"
fi
echo "✅ 安装 husky..."
if [ -d ".husky" ]; then
echo "🔄 检测到已有 .husky 目录,正在删除..."
rm -rf .husky
fi
npx husky init
echo "📂 拷贝 hooks 和配置文件到当前项目..."
cp -r node_modules/git-hooks-config/.husky/* .husky/
cp node_modules/git-hooks-config/commitlint.config.js commitlint.config.js
cp node_modules/git-hooks-config/header-pattern.cjs header-pattern.cjs
case $TOOL in
npm)
REMOVE_CMD="npm uninstall git-hooks-config"
;;
yarn)
REMOVE_CMD="yarn remove git-hooks-config"
;;
pnpm)
REMOVE_CMD="pnpm remove git-hooks-config"
;;
*)
echo "❌ 不支持的包管理工具: $TOOL"
exit 1
;;
esac
eval "$REMOVE_CMD"
echo "🎉 Git Hooks 配置完成"
训练大纲
别以为把 Husky 装上就万事大吉,它只是个忠诚的执行者,关键还是得有人告诉它该怎么"管人"------是时候祭出我们为它量身定制的《训练大纲》了!
这套大纲一共三式,招招致命、环环相扣,像极了那种武侠小说里的"入门-进阶-终极"三重修炼法。
👮 第一式:.husky/pre-commit · 分支取名不能乱,代码质量不能差
这招的核心思想是:"人可以不帅,分支名必须规范;你可以摸鱼,但代码得过质检。"
脚本一出,先抓住你的分支名瞪一眼:
"你这叫啥?
fix-final-final-last-v2
??赶紧改成bugfix/123-修复登录bug
!"
接着再盯着你最新提交的 .ts
、.tsx
、.vue
文件扫一圈,只要 ESLint 一皱眉,它立马拉你回炉重做:
"风格不对别想走,代码不齐别想提!"
可以说是从分支命名到代码质量,里内管制。
bash
#!/bin/bash
# set -e
# set -x
#trap 'echo "Error at line $LINENO: $BASH_COMMAND"; exit 1' ERR
# Branch命名检查
# 获取当前Branch的名称
JenkinsBranchName=$1
if [ -n "$JenkinsBranchName" ]; then
CURRENT_BRANCH="${JenkinsBranchName}" # Jenkins获取分支名
else
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) # 本地获取分支名
fi
echo "当前分支名称: $CURRENT_BRANCH"
# 检查是否成功获取Branch名称
if [ -z "$CURRENT_BRANCH" ]; then
echo "错误:无法获取当前Branch名称。请确保在Git仓库中运行此脚本。"
exit 1
fi
# 定义Branch命名规范的正则表达式
# 获取公共配置PATTERN
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/config.sh"
# 校验分支名称
if ! echo "$CURRENT_BRANCH" | grep -Pq "$PATTERN"; then
arr=(
"(1)feat/215-用户登录界面;"
"(2)hotfix/284-修复用户名重复问题; "
"(3)bugfix/233-修复404问题; "
"(4)release/v1.0.0;|release/20250516; "
"(5)master; "
"(5)dev")
# 使用分号连接
joined=$(IFS=';' echo "${arr[*]}")
echo "分支名称 '$CURRENT_BRANCH' 不符合命名规范。命名规则:[issue_type]/[issue_id]-description, 请使用以下命名规范之一:$joined"
exit 1
fi
# 注意这里加了 || true
files=$(git diff --cached --name-only --diff-filter=AM | grep -E '\.(ts|tsx|vue)$' || true)
if [ -z "$files" ]; then
exit 0
fi
set +e
output=$(echo "$files" | xargs -r npx eslint 2>&1)
exit_code=$?
if [ $exit_code -ne 0 ]; then
echo "ESLint 检查未通过,请修复后再提交!"
echo "$output"
exit $exit_code
fi
👨⚖️ 第二式:.husky/commit-msg · 文案不走心,脚本揪着你重写
你以为名字过关就能安心 commit?太天真。
这个阶段,Husky 摇身一变,成了一个吹毛求疵的主编,一边看着你提交信息一边念叨:
"不加类型、不带议题 ID?感情不真挚,态度不严谨,退回!"
它连 Git 的"自动提交"都替你考虑周全了:像 Merge branch
、squash
这种系统自动生成的提交信息,它都会识趣地选择跳过检查,毕竟这些不是你亲手写的,其余情况嘛------一律按模板来,不讲情面。
而你要是实在记不住格式,别怕,它还贴心地提供了一整套例子模板让你对照参考(如下所示),温柔又坚定:
yaml
(1) feat: 新增用户登录功能 #123
(2) fix: 修复登录跳转错误 #456
...
bash
#!/bin/bash
commit_msg=$(cat "$1")
# 如果提交信息包含 'Merge branch' 或 'Merge pull request',则跳过 commitlint 检查
if echo "$commit_msg" | grep -qE '^Merge (branch|remote|pull request)'; then
exit 0
fi
if [ "$2" = "merge" ] || [ "$2" = "squash" ]; then
exit 0
fi
commit_msg=$(cat "$1")
npx --no -- commitlint --edit "$1" >/dev/null || {
arr=(
"(1)feat: 新增用户登录功能 #议题Id1#议题Id2; "
"(2)fix: 修复登录跳转错误 #议题Id1#议题Id2; "
"(3)docs: 更新项目文档 #议题Id1#议题Id2; "
"(4)chore: 升级npm依赖库 #议题Id1#议题Id2; "
"(5)perf: 优化页面加载速度 #议题Id1#议题Id2; "
"(6)refactor: 用户登录重构 #议题Id1#议题Id2;"
"(7)style: 用户登录样式调整 #议题Id1#议题Id2; "
"(8)test: 用户登录测试 #议题Id1#议题Id2; "
"(9)revert: 撤销用户登录提交 #议题Id1#议题Id2;"
)
joined=$(IFS=';' echo "${arr[*]}")
echo "提交信息 '$commit_msg' 不符合规范,请使用规范的格式提交!正确示例:${joined}"
exit 1
}
issues=$(echo "$commit_msg" | grep -oE '#[0-9]+' | sort -u | tr -d '\n')
chmod +x "$(dirname "$0")/check-issue"
echo "commit_msg: $issues"
"$(dirname "$0")/check-issue" "$issues"
exit $?
🕵️ 第三式: .husky/check-issue · 瞎写 Issue-ID?它可不吃这套!
到了终极验收阶段------你以为在提交信息里图省事,随便捏造几个不存在的 Issue ID 就能糊弄过去?不行!脚本表示:没有调查就没有发言权,它得亲自去 GitLab 核实每一个 ID 的真伪。
它会穿越你配置的多个项目路径,只要 Issue ID 在任意一个项目里存在,就算你过关 ;但如果有哪个 ID 在所有项目里都查无此号,那它立马当场发难:
❌ "#274?你这不是糊弄我吗?我把所有项目都翻遍了,连根毛都没找到!"
只有当所有 Issue ID 至少各自能在一个项目里落脚,脚本才会欣然点头、拍案通过:
🎉 "完美!所有议题均已实名验证,提交合法,驯狗三连通关成功!"
bash
#!/bin/bash
set -e
# 读取公共常量配置参数 GITLAB_URL + ACCESS_TOKEN + PROJECT_PATHS
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/config.sh"
# Parameters
ISSUE_IDS="$1" # 格式: #274#275
# Validate input
if [ ${#PROJECT_PATHS[@]} -eq 0 ] || [ -z "$ISSUE_IDS" ] || [ -z "$ACCESS_TOKEN" ]; then
echo "Usage: $0 <issue-ids: #1#2#3>"
exit 1
fi
command -v curl >/dev/null 2>&1 || {
echo >&2 "Error: curl required but not installed."
exit 1
}
# 解析 issue id:去掉前后 #,按 # 分割
IFS="#" read -ra ISSUE_ARRAY <<<"$ISSUE_IDS"
# 遍历所有 issue id
for iid in "${ISSUE_ARRAY[@]}"; do
echo "Checking issue #$iid..."
if [ -z "$iid" ]; then
continue
fi
issue_found=false
for project_path in "${PROJECT_PATHS[@]}"; do
encoded_project_path=$(jq -rn --arg x "$project_path" '$x|@uri')
response_file=$(mktemp)
status_code=$(curl -s -w "%{http_code}" -o "$response_file" \
-H "PRIVATE-TOKEN: $ACCESS_TOKEN" \
"${GITLAB_URL}/api/v4/projects/${encoded_project_path}/issues/${iid}")
if [ "$status_code" -eq 200 ]; then
echo "✅ Issue #$iid exists in project $project_path."
issue_found=true
rm -f "$response_file"
break
fi
rm -f "$response_file"
done
if [ "$issue_found" = false ]; then
echo "❌ Issue #$iid not found in any configured project path."
exit 1
fi
done
echo "🎉 All issues exist in at least one project path."
🐶 从此以后,Husky 不再只是条"会叫的狗",它是你开发流程里的:
- 👮 分支命名警察
- 👨⚖️ 提交信息法官
- 🕵️♀️ 议题真伪鉴定师
- 🧹 代码风格质检员
规范就像隐形的电围栏,让你不知不觉中告别"野生开发习惯",走向"高质量工程师"之路------别说你不配,Husky 配得上每一位想偷懒又被迫优秀的你。
最后
提交规范靠吼没用,只需一条命令:
bash
curl -sSL https://gitee.com/getbetter/git-hooks-config/raw/master/init-git-hooks.sh | bash -s pnpm
用我这把 Shell + Husky 螺丝刀,一键就能给四十多个项目上锁,自此开发规范不用"靠人盯",自动校验、自动上锁、自动守规矩, 不怕你手抖乱提交,就怕你不用这条命令。本文的所有脚本已上传到码云仓库,欢迎讨论交流。