首页 > 解决方案 > 如果尚未在开发分支中,Git 预接收钩子可防止合并到主控

问题描述

我们的 git 进程是这样​​的。主分支是主分支,我们从中创建特征分支。当我们认为我们已经完成了一个特性时,我们将它合并到开发分支中,并将它构建到一个集成环境中进行测试。测试完成后,我们从特性分支提交一个 MR 到 master。

我正在尝试制作一个预接收 git 钩子,以防止 MR 被接受,除非该分支已合并到开发中并因此进行了集成测试。但是我正在努力弄清楚如何使用我在钩子中提供的信息来做到这一点。

我拥有的当前脚本如下

while read oldrev newrev refname; do
    # Get the name of the branch that HEAD is pointing to
    headbranch=$(git symbolic-ref --short HEAD)
    # Get the current branch name
    branch=$(echo $refname | sed 's/refs\/heads\///g')
    # During a merge request both the branch and head branch will be master
    if [[ "$branch" == "master" && "$headbranch" == "master" ]]; then
        # Grab the PREVIOUS revision of the current revision as this is the last commit before the current merge commit
        prevrev=$(git rev-list -2 $newrev | sed -n '2 p')
        # Check that revision is in the develop branch already, otherwise reject the commit
        $(git merge-base --is-ancestor $prevrev develop)
        if [ $? -eq 1 ]; then
            echo "The branch has not been merged with \"develop\" or you have not pushed \"develop\" to the remote server." && exit 1
        fi
    fi
done

有时有效,但有时它会失败,因为newrevprevrev都是与合并请求相关的一些随机哈希,并且它们都不在任何历史记录中。我想不出一种方法来区分这些我不关心的 MR 哈希和带有尚未合并的代码提交的实际哈希。

有没有其他方法可以做到这一点,或者这是不可能的?

注意:我还希望脚本拒绝直接提交给 master。

标签: bashgitgitlabgithooks

解决方案


是的,你的钩子代码肯定有点错误。:-)

一个完美的解决方案非常困难,原因之一是:可以在一次推送中git push接收多个更新请求。如你所说:

我正在尝试制作一个预接收 git 钩子,以防止 MR 被接受,除非该分支已合并到开发中并因此进行了集成测试。

让我们先谈谈最一般的情况,然后再谈谈更具体的情况。假设一个推送请求说:

  • 更改develop为指向提交a123456——目前是a654321;
  • 将 HEAD-ref 分支(假设是master)更改为指向 commit b789abc;和
  • 更新 MR1234(refs/merges/1234也许)指向 commit c765432

现在进一步假设a654321(的当前develop)包含c765432。也就是说,MR1234在之前develop。因此,它经过了某种集成测试。显然这失败了,因为develop撤回a123456“之前” a654321,所以推送完成后,a654321 将不再存在develop

同时,在 的祖先的某个地方b789abc,commita123456存在。那是旧的 MR1234——在这次推动中被替换的那个。

就个人而言,我不知道该怎么处理这个烂摊子。幸运的是,预接收挂钩只有两种选择:全部接受,或全部拒绝。更新钩子——存储库中的钩子将用于这三个更新中的每一个,如果 pre-receive 钩子允许它们——可以选择一次接受或拒绝一个请求,但不会获得这种全局信息.

鉴于您使用的系统发出合并请求,等待某种 CI/CD 系统对其进行测试,然后才尝试将它们提交以进行最终处理,与这种推送有关的事情就是尝试同时更新master,develop和一些合并请求 - 只是拒绝它,并带有以下形式的消息:不要一次做所有事情,我需要一次处理每个请求

这需要将一些逻辑混合在一起或进行多次传递。您可以使用您喜欢的任何方法,但是在 Git 挂钩中执行多次传递有点棘手,因为 Git 将这些数据发送到管道中。1 这意味着您只能阅读一次。您可以读取并保存它,然后对数据进行多次传递。在 shell 中,您通常会通过制作自己的临时文件来做到这一点:

TF=$(mktemp)
trap "rm -f $TF" 0 1 2 3 15
cat > $TF

现在您可以随意阅读$TF。多次阅读效率较低,但更容易正确。


1我认为这是 Git 的一个错误,但这完全是另一个问题。


接下来,推送请求还可以创建新分支或删除现有分支。它可以创建或删除一个标签(或被要求更新一个标签,但除非在旧版本的 Git 中意外使用标签名称上的分支规则,只有在强制的情况下)。我认为它也可以用于既不是分支也不是标签名称的名称(例如,refs/notes/*名称)。我们可以将它们分开并允许推送,例如,既创建一个标签更新一个分支。但目前让我们从“仅一次操作”开始:

count=0
while read line; do : $((count+=1)); done < $TF
if [ $count -gt 1 ]; then
    echo "Only one operation per push, please!" 1>&2
    exit 1
fi

接下来,让我们做同样的事情:弄清楚HEAD接收裸存储库中的分支是什么。这是主线分支的名称(main,master等):

headbranch=$(git symbolic-ref --short HEAD)

如果已分离或引用尚不存在的分支,此命令可能会失败。HEAD那不应该发生,但是如果我们想变得非常聪明,我们可能应该对此进行测试并打印一些东西并退出:

headbranch=$(git symbolic-ref --short HEAD) || {
    echo "detached HEAD - please get an admin to fix me" 1>&2
    exit 1
}

让我们再添加一件事:零哈希。为了对 sha256 进行未来验证,我们将读取 head ref 的哈希并将所有十六进制值变为零:

zerohash=$(git rev-parse HEAD) || {
    echo "missing HEAD - please get an admin to fix me" 1>&2
    exit 1
}
zerohash=$(echo $zerohash | sed 's/[1-9a-z]/0/g')

现在我们可以阅读更新请求并审查它们。为了编码的理智,我们可能应该使用 shell 函数。我们将在文件的前面定义 shell 函数(以便我们可以使用它),但现在让我们编写使用它的代码。我们知道这里只有一个操作,因为我们之前计算过它们,但是为了清洁起见,我将再次循环阅读它们:

while read old new hash; do check $old $new $hash; done < $TF

我们将check返回一个状态,所以让我们在这个 while 循环之前设置它:

exitcode=0
while read old new hash; do
    check $old $new $hash
    status=$?
    if [ $status -ne 0 ]; then exitcode=$status; fi
done < $TF
exit $exitcode

现在我们需要我们的check函数。它将继承$headbranch. 它接受三个参数并返回一个状态(0 = OK,非零 = 发送错误消息)。让我们先看看请求是否针对分支名称:

check() {
    local old=$1 new=$2 ref=$3
    local shortref

    case $ref in
    refs/heads/*) shortref=${ref#refs/heads/};;
    *) return 0;;
    esac

case构造允许使用 glob 表达式进行大量测试,这在这里很方便。(在非常旧的 shell 版本中,它也比 快if,但如果您使用的是 bash 或现代sh的,则没有速度差异。)

现在我们需要查看分支是否是感兴趣的分支(master或者主分支是什么)。如果没有,我们就允许这样做。

    if [ "$shortref" != "$headbranch" ]; then return 0; fi

在这一点上,我们正在更新master或任何 head ref 是什么。我们现在将要求 (1) 这是一个合并提交,并且 (2) 合并提交的第二个父级在develop. 除非,也许推送是由一些适当的管理员完成的(我会把测试留给你,特别是因为“谁在做这个推送”可能很难测试——它完全取决于你的服务器框架的其余部分) . 所以:

    # if person doing push is admin: return 0
    # Make sure this is neither creation nor deletion
    local op
    case $old,$new in
    $zerohash,*) op=create;;
    *,$zerohash) op=delete;;
    *) op=update;;
    esac
    if [ $op != update ]; then
        echo "not allowed to create or delete $headbranch" 1>&2
        return 1
    fi

    # require that this add exactly 1 2-parent merge commit: find
    # all parents with ^@ suffix and set them as our $1, $2, ..., $n
    set -- $(git rev-parse $new^@)
    case $# in
    0) echo "root commit: forbidden on $headbranch" 1>&2; return 1;;
    1) echo "not a merge commit: forbidden on $headbranch" 1>&2; return 1;;
    2) ;;
    *) echo "$#-parent merge: forbidden on $headbranch" 1>&2; return 1;;

    # Now $1 is parent #1, which had better be the OLD commit;
    # $2 is the commit being merged, which needs to be in the
    # history of `develop`.
    if [ $1 != $old ]; then
        echo "this push adds more than one commit to $headbranch," 1>&2
        echo "which is not allowed: one commit at a time only please" 1>&2
        return 1
    fi

    if ! git merge-base --is-ancestor $2 develop; then
        echo "this push tries to merge $2," 1>&2
        echo "which is not present in branch `develop` on the server;" 1>&2
        echo "this is not allowed"
        return 1
    fi

    # all tests passed
    return 0
}

注意:以上都没有经过测试。但是,这些位中的许多位都是来自标准钩子的片段,因此它们至少都应该是“关闭的”。要对其进行测试,请克隆一些 repo 并在其标准输入上通过它运行一些示例输入。


推荐阅读