如何解决如果尚未在开发分支中,则 Git 预接收挂钩可防止合并到 master
我们的 git 流程是这样的。主分支是 master,我们从中创建功能分支。当我们认为我们完成了一个功能时,我们将它合并到开发分支中,并将其构建到集成环境中进行测试。测试完成后,我们从功能分支向 master 提交 MR。
我正在尝试制作一个 pre-receive git hook,它会阻止 MR 被接受,除非分支已合并到 develop 中并因此进行了集成测试。但是我正在努力弄清楚如何使用钩子中可用的信息来做到这一点。
我当前的脚本如下
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
这有时有效,但有时会失败,因为 newrev
和 prevrev
都是与合并请求相关的随机散列,并且它们都没有出现在任何历史记录中.我想不出一种方法来区分这些我不关心的 MR 哈希值和尚未合并的代码提交的实际哈希值。
是否有其他方法可以做到这一点,或者这是不可能的?
注意:我也希望脚本拒绝直接提交给 master。
解决方法
是的,你的钩子代码肯定有点错误。 :-)
一个完美的解决方案非常困难,原因有一个:git push
可以在一次推送中接收多个更新请求。正如你所说:
我正在尝试制作一个 pre-receive git hook,以防止 MR 被接受,除非分支已合并到 develop 中并因此进行了集成测试。
我们先说最一般的情况,然后再说更具体的情况。假设一个推送请求说:
- 将
develop
更改为指向提交a123456
——当前为a654321
; - 将 HEAD-ref 分支(假设是
master
)更改为指向提交b789abc
;和 - 更新 MR1234(可能是
refs/merges/1234
)以指向提交c765432
。
现在进一步假设 a654321
(develop
的 当前 值)包含 c765432
。也就是说,MR1234 是 之前的develop
。因此,它已经通过了某种集成测试。显然这失败了,因为 develop
正在被撤回到 a123456
,它是“before”a654321
以便 after 这个推送完成,a654321
不会在develop
了。
与此同时,在 b789abc
的祖先中的某处,存在提交 a123456
。那是旧的 MR1234——在这次推送中被替换的那个。
就我个人而言,我不知道该怎么处理这个烂摊子。幸运的是,pre-receive 钩子只有两个选项:全部接受,或者全部拒绝。 update 钩子——如果 pre-receive 钩子允许的话,存储库中的钩子将用于这三个更新中的每一个——可以选择一次接受或拒绝一个请求,但不会无法获取此类全局信息。
鉴于您正在使用一个系统,其中一个人发出合并请求,等待某种 CI/CD 系统来测试它们,然后才尝试将它们提交以进行最终处理,这与 有关这种推送——试图同时更新master
、develop
和一些合并请求——只是拒绝它,并带有以下形式的消息:don不要一次做所有的事情,我需要一次处理一个请求。
这需要将一些逻辑混合在一起或进行多次传递。您可以使用您喜欢的任何方法,但在 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,非零 = 已发送错误消息)。让我们先看看请求是否是branch 名称:
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
或任何头部引用。我们现在将要求 (1) 这是一个合并提交,并且 (2) 合并提交的 second 父项位于 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 并通过它在其标准输入上运行一些示例输入。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。