Git 自动化实践:hooks 与发布脚本

系统讲解 Git hooks 的机制与编写、commit-msg 提交规范校验、批量仓库操作,以及版本号管理、changelog 生成与一键发布流水线的完整实践。

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-commitcommit 前格式检查、lint、禁止大文件
commit-msgcommit 信息提交前校验提交信息规范
pre-pushpush 前跑测试、构建检查
post-commitcommit 后通知、更新其他系统
post-checkoutcheckout 后更新依赖、切换环境

一句话总结: 记住三个主力钩子: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 主次补丁,脚本按类型递增
changeloggit log --grep 聚合提交,规范兑现收益
发布编排干净检查 → 递增 → 构建 → 打标签 → 推送
CI 集成环境变量控制门禁,干跑模式验证脚本

Git 自动化把版本与发布从「手工记忆」变成「脚本纪律」:hooks 在入口拦截错误,批量命令统一管理多仓库,语义化版本让发布可预期。这套思路与部署发布脚本一脉相承——下一步进入终端 UI 与进度条,让脚本从「默默运行」变得可见可控。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「shell」更多文章

  1. 任务编排与 Makefile 实战
  2. 文件监控与事件驱动流水线实战
  3. 结构化数据清洗与报表生成实战