1. Git hooks 机制与生命周期
一句话总结: Git hooks 是 Git 在特定事件前后调用的脚本钩子,放在
.git/hooks/下,实现提交前检查、提交信息校验、推送前测试等自动化。
Git 在 commit、push、merge 等动作的关键节点会执行对应钩子脚本,返回非零即中断操作。
# 查看自带示例钩子
ls -la .git/hooks/ | grep sample
# 钩子命名:钩子名.sample 为示例,去掉后缀即启用
mv .git/hooks/pre-commit.sample .git/hooks/pre-commit
# 编写自己的 pre-commit
cat > .git/hooks/pre-commit <<'EOF'
#!/usr/bin/env bash
echo "提交前检查..."
EOF
chmod +x .git/hooks/pre-commit
1.1 常用钩子一览
| 钩子 | 触发时机 | 典型用途 |
|---|---|---|
| pre-commit | commit 前 | 格式检查、lint、禁止大文件 |
| commit-msg | commit 信息提交前 | 校验提交信息规范 |
| pre-push | push 前 | 跑测试、构建检查 |
| post-commit | commit 后 | 通知、更新其他系统 |
| post-checkout | checkout 后 | 更新依赖、切换环境 |
一句话总结: 记住三个主力钩子:pre-commit 做代码检查、commit-msg 校验提交信息、pre-push 在推送前跑完整验证。
1.2 钩子共享与工具
.git/hooks 不入版本库,团队共享钩子需用 husky、pre-commit 框架或把钩子文件放仓库内软链。
# 方案一:仓库内维护钩子,安装时软链
mkdir -p .githooks
cat > .githooks/pre-commit <<'EOF'
#!/usr/bin/env bash
echo "团队共享的 pre-commit"
EOF
chmod +x .githooks/pre-commit
# 每个成员执行一次
git config core.hooksPath .githooks
2. commit-msg:提交规范校验
一句话总结: commit-msg 钩子拿到提交信息文件路径,用正则校验格式(如 Conventional Commits),不符合就拒绝提交。
提交规范让历史清晰、便于自动生成 changelog。下面实现一个 conventional 风格校验。
cat > .githooks/commit-msg <<'EOF'
#!/usr/bin/env bash
# 读取提交信息
msg=$(cat "$1")
# 正则:feat/fix/docs/refactor + 冒号 + 描述
pattern='^(feat|fix|docs|refactor|test|chore|revert)(\([a-z]+\))?: .+'
if ! [[ "$msg" =~ $pattern ]]; then
echo "提交信息不符合规范,示例: feat(core): 新增用户缓存" >&2
exit 1
fi
exit 0
EOF
chmod +x .githooks/commit-msg
2.1 校验多行提交信息
提交信息可能含 body,校验时只看第一行标题。
cat > .githooks/commit-msg <<'EOF'
#!/usr/bin/env bash
msg_file="$1"
title=$(head -n 1 "$msg_file")
pattern='^(feat|fix|docs|refactor|test|chore)(\([^)]+\))?: [a-z].*'
if ! [[ "$title" =~ $pattern ]]; then
cat >&2 <<'MSG'
提交标题格式错误:
允许类型: feat fix docs refactor test chore
示例: feat(core): 新增用户缓存功能
MSG
exit 1
fi
exit 0
EOF
一句话总结:
head -n 1取标题、bash =~正则匹配、>&2把错误写到 stderr,是 commit-msg 钩子的标准骨架。
3. pre-commit 与 pre-push
一句话总结: pre-commit 在提交前跑轻量检查,pre-push 在推送前跑重型验证(测试、构建),把错误拦截在远端之前。
cat > .githooks/pre-commit <<'EOF'
#!/usr/bin/env bash
# 禁止把密钥提交进仓库
set -e
if git diff --cached | grep -E 'BEGIN (RSA|OPENSSH) PRIVATE KEY'; then
echo "错误: 检测到私钥被暂存" >&2
exit 1
fi
# 禁止提交大文件
if git diff --cached --name-only -z \
| xargs -0 -I {} sh -c 'test -f "{}" && test "$(wc -c < "{}")" -gt 500000' 2>/dev/null; then
echo "错误: 存在超过 500KB 的文件" >&2
exit 1
fi
exit 0
EOF
chmod +x .githooks/pre-commit
3.1 pre-push 跑测试
cat > .githooks/pre-push <<'EOF'
#!/usr/bin/env bash
# 推送前跑测试
echo "运行测试套件..."
if ! make test; then
echo "测试失败,禁止推送" >&2
exit 1
fi
echo "测试通过,允许推送"
EOF
chmod +x .githooks/pre-push
一句话总结: pre-push 可以访问
$1(远端)、$2(远端地址),适合按分支条件跳过某些验证。
4. 批量仓库操作
一句话总结:
git -C指定仓库目录执行命令,配合循环能对一批仓库做统一操作(拉取、切分支、打标签)。
#!/usr/bin/env bash
set -euo pipefail
# 对多个仓库执行 git pull
repos=(~/code/app ~/code/lib ~/code/tools)
for repo in "${repos[@]}"; do
echo "== $repo =="
git -C "$repo" pull --ff-only || echo " $repo 拉取失败"
done
4.1 批量打标签与统计
#!/usr/bin/env bash
set -euo pipefail
# 为所有仓库打相同标签
tag="v1.2.0"
for repo in $(cat repos.txt); do
git -C "$repo" tag "$tag" || true
git -C "$repo" push origin "$tag" || true
done
# 统计每个仓库的提交数
for repo in $(cat repos.txt); do
count=$(git -C "$repo" rev-list --count HEAD)
echo "$repo: $count commits"
done
一句话总结:
-C让 git 不必 cd 也能操作任意目录,是批量脚本的基石;|| true防止单个失败中断整批。
5. 版本号与语义化
一句话总结: 语义化版本
主.次.补丁有明确递增规则,脚本可以解析当前版本并按类型自动递增、打标签。
#!/usr/bin/env bash
set -euo pipefail
# 从 git 最近标签取版本
version=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
echo "当前版本: $version"
# 去掉 v 前缀拆分
v=${version#v}
IFS='.' read -r major minor patch <<< "$v"
echo "major=$major minor=$minor patch=$patch"
5.1 按类型递增
#!/usr/bin/env bash
set -euo pipefail
# 用法: bump.sh <major|minor|patch>
bump_type="${1:-patch}"
version=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
v=${version#v}
IFS='.' read -r major minor patch <<< "$v"
case "$bump_type" in
major) major=$((major+1)); minor=0; patch=0 ;;
minor) minor=$((minor+1)); patch=0 ;;
patch) patch=$((patch+1)) ;;
*) echo "未知类型: $bump_type"; exit 1 ;;
esac
new="v${major}.${minor}.${patch}"
echo "$new"
一句话总结: semver 的递增强行规律完全可脚本化,major/minor/patch 三档对应破坏性/功能/修复三种发布。
5.2 结合 commit-msg 自动判类型
#!/usr/bin/env bash
set -euo pipefail
# 依据最近提交信息推断 bump 类型
last_msg=$(git log -1 --format=%s)
case "$last_msg" in
feat*) bump_type=minor ;;
fix*) bump_type=patch ;;
*) bump_type=patch ;;
esac
echo "推断类型: $bump_type"
6. 发布与 changelog 生成
一句话总结: 依据 tag 区间聚合提交信息,自动生成 changelog;
git log是 changelog 的原料库。
#!/usr/bin/env bash
set -euo pipefail
prev=$(git describe --tags --abbrev=0 HEAD~ 2>/dev/null || echo "")
since="${prev:-$(git rev-list --max-parents=0 HEAD)}"
# 聚合上一版本以来的提交
git log --oneline --no-merges "$since..HEAD" \
| grep -E '^(feat|fix|docs|refactor)' \
| sed 's/^\([a-z]*\)/: \1/' \
> /tmp/changelog.txt
cat /tmp/changelog.txt
6.1 按类型分组生成 changelog
#!/usr/bin/env bash
set -euo pipefail
since=$(git describe --tags --abbrev=0 HEAD~ 2>/dev/null || git rev-list --max-parents=0 HEAD)
{
echo "## 变更日志"
echo ""
echo "### 新功能"
git log --oneline "$since..HEAD" | grep '^feat'
echo ""
echo "### Bug 修复"
git log --oneline "$since..HEAD" | grep '^fix'
} > CHANGELOG.md
一句话总结:
git log --grep='^feat'按类型筛提交,commit-msg 规范的收益在此兑现——changelog 零人工生成。
6.2 打标签与推送发布
#!/usr/bin/env bash
set -euo pipefail
new=$(./bump.sh minor)
echo "发布版本: $new"
git tag -a "$new" -m "release $new"
git push origin "$new"
# 触发 CI 或部署 webhook
curl -s -X POST "https://ci.example.com/build/$new"
7. 实战:一键发布流水线
一句话总结: 把「版本递增 → 构建 → changelog → 打标签 → 推送」串成一条命令,发布从手工操作变为可重复的确定性流程。
7.1 完整发布脚本
#!/usr/bin/env bash
set -euo pipefail
# 前置检查:工作区干净
if ! git diff --quiet || ! git diff --cached --quiet; then
echo "工作区有未提交变更,先提交再发布" >&2
exit 1
fi
bump_type="${1:-patch}"
new_version=$(./bump.sh "$bump_type")
# 1. 更新版本文件
echo "$new_version" > VERSION
git add VERSION
git commit -m "chore(release): $new_version"
# 2. 构建
make build || { echo "构建失败"; exit 1; }
# 3. 生成 changelog
./gen-changelog.sh "$new_version"
# 4. 打标签并推送
git tag -a "$new_version" -m "release $new_version"
git push origin main --tags
# 5. 触发部署
./deploy.sh "$new_version"
一句话总结: 发布的每一步都必须是「幂等、可重复、失败即停」的命令,一键脚本就是把它们按依赖顺序编排。
7.2 环境隔离与干跑
#!/usr/bin/env bash
set -euo pipefail
DRY_RUN="${DRY_RUN:-false}"
run() {
if [ "$DRY_RUN" = "true" ]; then
echo "[干跑] $*"
else
"$@"
fi
}
run git push origin main --tags
run ./deploy.sh v1.2.0
7.3 在 CI 中接入
#!/usr/bin/env bash
set -euo pipefail
# CI 里自动打版本(仅标签触发部署)
if [ "${GITHUB_REF}" = "refs/heads/main" ]; then
new=$(./bump.sh patch)
echo "tag=$new" >> "$GITHUB_OUTPUT"
fi
8. 总结
| 环节 | 要点 |
|---|---|
| hooks 机制 | .git/hooks/ 下按事件命名,非零退出即中断 |
| 常用钩子 | pre-commit / commit-msg / pre-push 三大主力 |
| 提交规范 | commit-msg 正则校验,conventional 风格 |
| 批量操作 | git -C 循环多仓库,` |
| 版本号 | semver 主次补丁,脚本按类型递增 |
| changelog | git log --grep 聚合提交,规范兑现收益 |
| 发布编排 | 干净检查 → 递增 → 构建 → 打标签 → 推送 |
| CI 集成 | 环境变量控制门禁,干跑模式验证脚本 |
Git 自动化把版本与发布从「手工记忆」变成「脚本纪律」:hooks 在入口拦截错误,批量命令统一管理多仓库,语义化版本让发布可预期。这套思路与部署发布脚本一脉相承——下一步进入终端 UI 与进度条,让脚本从「默默运行」变得可见可控。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。