Git钩子脚本如何保护敏感分支

wen 实用脚本 1

本文目录导读:

Git钩子脚本如何保护敏感分支

  1. 使用 Pre-receive 钩子(最推荐,远程仓库侧)
  2. 使用 Pre-commit 钩子(本地仓库侧,辅助作用)
  3. 使用 Pre-push 钩子(本地仓库侧,较实用)
  4. 使用 Update 钩子(远程仓库侧,按分支单独控制)
  5. 总结与建议

Git 钩子脚本是保护敏感分支(如 mastermainproduction 等)的有效手段,通过在本地或远程仓库的特定钩子中编写脚本,可以强制实施规则,防止意外提交、强制推送或删除这些分支。

以下是使用 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

部署方法:

  1. 将上述脚本保存到远程仓库的 .git/hooks/pre-receive
  2. 赋予执行权限:chmod +x .git/hooks/pre-receive
  3. 注意: 在 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) 防止在本地错误分支提交

最佳实践:

  1. 首选远程钩子(pre-receive/update),它们无法被普通用户绕过。
  2. 对于 GitHub/GitLab 用户,请使用平台内置的“分支保护规则”(Branch Protection Rules),它们提供了更强大、易用的功能(如要求 Pull Request 审查、要求状态检查通过等),比手写钩子更稳定。
  3. 手写钩子通常用于自建 Git 服务器,或者需要实现平台规则无法覆盖的定制逻辑(如检查提交信息的特殊格式)。
  4. 将钩子脚本纳入版本管理,并让团队通过 make install-hooksgit config core.hooksPath 统一管理,避免每个成员手动复制。

抱歉,评论功能暂时关闭!