本文目录导读:

- 使用 Pre-receive 钩子(最推荐,远程仓库侧)
- 使用 Pre-commit 钩子(本地仓库侧,辅助作用)
- 使用 Pre-push 钩子(本地仓库侧,较实用)
- 使用 Update 钩子(远程仓库侧,按分支单独控制)
- 总结与建议
Git 钩子脚本是保护敏感分支(如 master、main、production 等)的有效手段,通过在本地或远程仓库的特定钩子中编写脚本,可以强制实施规则,防止意外提交、强制推送或删除这些分支。
以下是使用 Git 钩子保护敏感分支的几种常见方法及脚本示例:
使用 Pre-receive 钩子(最推荐,远程仓库侧)
这是最安全的方式,因为它在服务器端执行,用户无法绕过,即使开发者没有配置本地钩子,服务器也会拒绝违规操作。
工作原理: 当用户执行 git push 时,远程仓库会触发 pre-receive 钩子,脚本会检查推送的分支是否是被保护的分支,以及操作是否被允许(如禁止强制推送、禁止删除)。
脚本示例(pre-receive):
#!/bin/bash
# 放在远程仓库的 .git/hooks/pre-receive 文件中,并赋予执行权限 (chmod +x)
#
# 这个脚本会拒绝任何对敏感分支的强制推送或删除操作。
# 定义敏感分支列表(用空格分隔)
PROTECTED_BRANCHES="master main production release-*"
while read oldrev newrev refname; do
# 提取分支名 (refs/heads/master -> master)
branch_name=$(echo "$refname" | sed 's/refs\/heads\///')
# 检查分支是否在保护列表中
is_protected=false
for pattern in $PROTECTED_BRANCHES; do
if [[ "$branch_name" == $pattern ]]; then
is_protected=true
break
fi
done
# 如果是保护分支,进行检查
if $is_protected; then
# 1. 禁止删除分支 (oldrev 为 0000... 表示创建,newrev 为 0000... 表示删除)
if [ "$newrev" = "0000000000000000000000000000000000000000" ]; then
echo "错误: 禁止删除保护分支 '$branch_name'"
exit 1
fi
# 2. 禁止强制推送 (通过检查是否有 --force 标志,或 oldrev 被重写)
# 检测方式:如果旧对象(oldrev)不是一个有效的祖先节点,则视为强制推送。
# 注意:这只是一种常用检测方式,不是完美的方式。
zero="0000000000000000000000000000000000000000"
if [ "$oldrev" != "$zero" ]; then
# 检查 newrev 是否基于 oldrev (即 oldrev 是 newrev 的祖先)
# 使用 `git merge-base --is-ancestor` 命令
if ! git merge-base --is-ancestor "$oldrev" "$newrev"; then
echo "错误: 禁止对保护分支 '$branch_name' 进行强制推送 (non-fast-forward)"
exit 1
fi
fi
# 3. (可选) 禁止直接推送,要求通过 Pull Request
# 这个钩子无法完美判断是否来自PR,通常由 Git 服务提供商(如GitHub/GitLab)实现。
# 如果你有严格的审核要求,可以考虑使用 `git rev-list` 检查提交信息或作者。
# 4. (可选) 检查提交信息是否包含特定关键字或格式
# while IFS= read -r commit_hash; do
# commit_msg=$(git log --format=%B -n1 "$commit_hash")
# if ! echo "$commit_msg" | grep -qE "^\[PRODUCTION\]|^\[RELEASE\]"; then
# echo "错误: 提交到 $branch_name 必须包含 [PRODUCTION] 或 [RELEASE] 前缀"
# exit 1
# fi
# done < <(git rev-list "$oldrev".."$newrev")
fi
done
exit 0
部署方法:
- 将上述脚本保存到远程仓库的
.git/hooks/pre-receive。 - 赋予执行权限:
chmod +x .git/hooks/pre-receive。 - 注意: 在 GitHub/GitLab 等平台上,你通常无法直接编辑服务端的钩子,在这些平台上,应使用平台内置的“分支保护规则”。
使用 Pre-commit 钩子(本地仓库侧,辅助作用)
pre-commit 钩子在每次 git commit 之前执行,它可以防止在敏感分支上直接进行提交,从而避免将未审核的代码提交到本地仓库的敏感分支。
工作原理: 检查当前分支是否是被保护的分支,如果是,则拒绝提交。
脚本示例(pre-commit):
#!/bin/bash
# 放在本地仓库的 .git/hooks/pre-commit 文件中
# 定义敏感分支
PROTECTED_BRANCHES="master main production"
# 获取当前分支名
current_branch=$(git symbolic-ref --short HEAD 2>/dev/null)
# 检查是否在保护分支上
for branch in $PROTECTED_BRANCHES; do
if [ "$current_branch" = "$branch" ]; then
echo "错误: 当前在 '$current_branch' 分支上,禁止直接提交。"
echo "请切换到其他开发分支,完成开发后再通过 Pull Request 合并。"
exit 1
fi
done
exit 0
局限性:
- 用户可以通过
git commit --no-verify跳过此钩子。 - 仅适用于本地提交,无法控制推送。
使用 Pre-push 钩子(本地仓库侧,较实用)
pre-push 钩子在执行 git push 之前触发,它可以检查即将推送的分支是否是敏感分支,并阻止推送。
工作原理: 分析 git push 的远程引用(通常是 refs/heads/branch-name),如果目标分支是敏感分支,则拒绝。
脚本示例(pre-push):
#!/bin/bash
# 放在本地仓库的 .git/hooks/pre-push 文件中
# 定义敏感分支
PROTECTED_BRANCHES="master main production"
# 标准输入会提供如下格式的信息:
# <本地引用> <本地SHA> <远程引用> <远程SHA>
while read local_ref local_sha remote_ref remote_sha; do
# 提取远程分支名 (refs/heads/master -> master)
branch_name=$(echo "$remote_ref" | sed 's/refs\/heads\///')
# 检查是否是敏感分支
for pattern in $PROTECTED_BRANCHES; do
if [ "$branch_name" = "$pattern" ]; then
echo "错误: 禁止推送到保护分支 '$branch_name'"
echo "请先切换到其他分支,然后创建 Pull Request 或 Merge Request。"
exit 1
fi
done
done
exit 0
优点: 在推送前拦截,给予用户即时反馈。
缺点: 用户可以通过 git push --no-verify 跳过。
使用 Update 钩子(远程仓库侧,按分支单独控制)
update 钩子与 pre-receive 类似,但它为每个即将被更新的引用单独执行一次,这使得我们可以针对不同分支设置不同的规则。
脚本示例(update):
#!/bin/bash
# 放在远程仓库的 .git/hooks/update 文件中
# 参数:$1 是 refname (如 refs/heads/master), $2 是旧 SHA, $3 是新 SHA
refname="$1"
oldrev="$2"
newrev="$3"
# 只处理分支
if [[ "$refname" =~ ^refs/heads/ ]]; then
branch_name="${refname#refs/heads/}"
# 保护分支列表
case "$branch_name" in
master|main|production)
# 禁止删除
if [ "$newrev" = "0000000000000000000000000000000000000000" ]; then
echo "错误: 禁止删除保护分支 '$branch_name'"
exit 1
fi
# 禁止强制推送
if [ "$oldrev" != "0000000000000000000000000000000000000000" ] && \
! git merge-base --is-ancestor "$oldrev" "$newrev"; then
echo "错误: 禁止对保护分支 '$branch_name' 进行强制推送"
exit 1
fi
echo "允许推送到保护分支 $branch_name (fast-forward)"
;;
*)
# 非保护分支,不做限制
;;
esac
fi
exit 0
总结与建议
| 钩子类型 | 位置 | 能否跳过 | 推荐使用场景 |
|---|---|---|---|
| pre-receive | 远程服务器 | 否 (管理员可改) | 生产环境最推荐,强制全局策略 |
| update | 远程服务器 | 否 (管理员可改) | 需要对不同分支设置不同规则时 |
| pre-push | 本地仓库 | 是 (--no-verify) | 作为辅助提醒,增加一层保障 |
| pre-commit | 本地仓库 | 是 (--no-verify) | 防止在本地错误分支提交 |
最佳实践:
- 首选远程钩子(pre-receive/update),它们无法被普通用户绕过。
- 对于 GitHub/GitLab 用户,请使用平台内置的“分支保护规则”(Branch Protection Rules),它们提供了更强大、易用的功能(如要求 Pull Request 审查、要求状态检查通过等),比手写钩子更稳定。
- 手写钩子通常用于自建 Git 服务器,或者需要实现平台规则无法覆盖的定制逻辑(如检查提交信息的特殊格式)。
- 将钩子脚本纳入版本管理,并让团队通过
make install-hooks或git config core.hooksPath统一管理,避免每个成员手动复制。