如何防止 'master' 分支中的狐步舞合并?

How can I prevent foxtrot merges in my 'master' branch?

狐步舞合并是 'origin/master' 作为第二个(或更高版本的)父级合并的合并,如下所示:

提交 'D' 是一个狐步舞合并,因为 'origin/master' 是它的第二个父级。请注意 'origin/master' 中的第一父历史记录此时如何包含提交 'B'。

但是在我的 git 存储库中,我需要涉及 'origin/master' 的所有合并,以保持 'origin/master' 作为第一个父级。不幸的是 git 在评估提交是否符合快进条件时并不关心父顺序。这导致我的 master 分支上的第一个父历史有时会丢失曾经存在的提交(例如 "git log --first-parent" 的输出)。

这是推送前面图表中的提交 'D' 时发生的情况:

如何阻止此推送? 'origin/master' 的第一父历史在推送 foxtrot 合并后不再包含提交 'B'!

显然没有任何提交或工作实际上丢失,只是在我的环境中我真的需要 "git log --first-parent" 来作为提交的稳定累积记录 - 如果你愿意,一种 "Write-Once Read-Many"(蠕虫)数据库。我的脚本和流程使用 "git log --first-parent" 来生成变更日志和发行说明,以及在我的票务系统 (JIRA) 中管理票证转换。 Foxtrot 合并破坏了我的脚本!

是否可以在我的 git 存储库中安装某种预接收挂钩以防止推送 foxtrot 合并?

p.s。这个 Whosebug 问题中的提交图是使用 http://bit-booster.com/graph.html.

生成的

以下预接收挂钩将阻止那些:

#/bin/bash

# Copyright (c) 2016 G. Sylvie Davies. http://bit-booster.com/
# Copyright (c) 2016 torek. http://whosebug.com/users/1256452/torek
# License: MIT license. https://opensource.org/licenses/MIT
while read oldrev newrev refname
do
if [ "$refname" = "refs/heads/master" ]; then
   MATCH=`git log --first-parent --pretty='%H %P' $oldrev..$newrev |
     grep $oldrev |
     awk '{ print $2 }'`

   if [ "$oldrev" = "$MATCH" ]; then
     exit 0
   else
     echo "*** PUSH REJECTED! FOXTROT MERGE BLOCKED!!! ***"
     exit 1
   fi
fi
done

如果您使用的是 Github / Gitlab / Bitbucket Cloud,您可能需要考虑创建某种对他们 commit status apis (here's api docs for: bitbucket, github 的调用;不确定 gitlab 是否有),因为您无权访问预接收挂钩,即使您访问了,您仍然需要处理直接在网络中单击 "merge" 按钮的人 ui 的那些产品(在这种情况下没有 "push")。

使用 Bitbucket Server,您可以安装 add-on I created

安装后,在给定存储库的 "hook" 设置中的 "Protect First Parent Hook" 上单击 "enable":

它将通过推送和 Bitbucket 服务器 UI 中的 "merge" 按钮阻止狐步舞合并。即使其许可证已过期,它也会这样做,从而使 "Protect First-Parent Hook" 成为更大附加组件的免费组件。

这是我的 Bit-Booster "Protect First Parent" 预接收挂钩的一个例子:

$ ​git pull
$ git push

remote: *** PUSH REJECTED BY Protect-First-Parent HOOK ***
remote: 
remote: Merge [1f70043b34d3] is not allowed. *Current* master must appear
remote: in the 'first-parent' position of the subsequent commit. To see how
remote: master is merging into the wrong side (not as 1st parent), try this:
remote: 
remote:   git show --graph -s --pretty='%h %d%n' \
remote:      1f70043b34d3 1f70043b34d3~1 origin/master
remote: 
remote: To fix, there are two traditional solutions:
remote: 
remote:   1. (Preferred) rebase your branch:
remote: 
remote:       git rebase origin/master
remote:       git push origin master
remote: 
remote:   2. Redo the merge in the correct direction:
remote: 
remote:       git checkout master 
remote:       git reset --hard origin/master 
remote:       git merge --no-ff 1f70043b34d3eaedb750~1
remote:       git push origin master
remote: 

有关狐步舞合并的更多背景信息 I wrote a blog post

这是一个钩子代码,可以满足您的要求:

pre-receive hook

#!/bin/sh

# Check to see if this is the first commit in the repository or not
if git rev-parse --verify HEAD >/dev/null 2>&1
then
    # We compare our changes against the previous commit
    against=HEAD^
else
    # Initial commit: diff against an empty tree object
    against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi

# Redirect output to screen.
exec 1>&2

# Check to see if we have updated the master branch
if [ "$refname" eq "refs/heads/master" ];
then

    # Output colors
    red='3[0;31m';
    green='3[0;32m';
    yellow='3[0;33m';
    default='3[0;m';
    
    # personal touch :-)
    echo "${red}"
    echo "                                         "
    echo "                   |ZZzzz                "
    echo "                   |                     "
    echo "                   |                     "
    echo "      |ZZzzz      /^\            |ZZzzz  "
    echo "      |          |~~~|           |       "
    echo "      |        |-     -|        / \      "
    echo "     /^\       |[]+    |       |^^^|     "
    echo "  |^^^^^^^|    |    +[]|       |   |     "
    echo "  |    +[]|/\/\/\/\^/\/\/\/\/|^^^^^^^|   "
    echo "  |+[]+   |~~~~~~~~~~~~~~~~~~|    +[]|   "
    echo "  |       |  []   /^\   []   |+[]+   |   "
    echo "  |   +[]+|  []  || ||  []   |   +[]+|   "
    echo "  |[]+    |      || ||       |[]+    |   "
    echo "  |_______|------------------|_______|   "
    echo "                                         "
    echo "                                         "
    echo "      ${green}You have just committed code ${red}  " 
    echo "      Your code ${yellow}is bad.!!!      "
    echo "      ${red} Do not ever commit again    "
    echo "                                         "
    echo "${default}"
fi;

# set the exit code to 0 or 1 based upon your needs
# 0 = good to push
# 1 = exit without pushing.
exit 0;

我写这个是为了提早提供反馈(pre-receive 钩子也是需要的)。我为此使用 post-mergepre-push 钩子。无法阻止 commit-msg 钩子中的狐步舞合并,因为检测一个的信息仅在之后可用。在post-merge 钩子中,我只是警告。在 pre-push 钩子中,我抛出并阻止推动。参见 d3f1821("foxtrot: Add sub-hooks to detect foxtrot merges",2017-08-05)。

~/.git-hooks/helpers/foxtrot-merge-detector:

#!/bin/sh

#usage:
#   foxtrot-merge-detector [<branch>]
#
# If foxtrot merge detected for branch (current branch if no branch),
# exit with 1.

# foxtrot merges:
# See http://bit-booster.blogspot.cz/2016/02/no-foxtrots-allowed.html
# 

remoteBranch=$(git rev-parse --abbrev-ref ""@{u} 2>/dev/null)
# no remote tracking branch, exit
if [[ -z "$remoteBranch" ]]; then
    exit 0
fi
branch=$(git rev-parse --abbrev-ref "${1-@}" 2>/dev/null)
# branch commit does not cover remote branch commit, exit
if ! $(git merge-base --is-ancestor $remoteBranch $branch); then
    exit 0
fi
remoteBranchCommit=$(git rev-parse $remoteBranch)
# branch commit is same as remote branch commit, exit
if [[ $(git rev-parse $branch) == $remoteBranchCommit ]]; then
    exit 0
fi
# remote branch commit is first-parent of branch, exit
if [[ $(git log --first-parent --pretty='%P' $remoteBranchCommit..$branch | \
    cut -d' ' -f1 | \
    grep $remoteBranchCommit | wc -l) -eq 1 ]]; then
    exit 0
fi
# foxtrot merge detected if here
exit 1

然后用作

预推钩:

#!/bin/sh

remote=""
url=""
z40=0000000000000000000000000000000000000000
while read local_ref local_sha remote_ref remote_sha
do
    if [ "$local_sha" = $z40 ]; then
        # handle delete, do nothing
        :
    else
        # ex $local_ref as "refs/heads/dev"
        branch=$(git rev-parse --abbrev-ref "$local_ref")
        ~/.git-hooks/helpers/foxtrot-merge-detector "$branch"
        # check exit code and exit if needed
        exitcode=$?
        if [ $exitcode -ne 0 ]; then
            echo 1>&2 "fatal: foxtrot merge detected, aborting push"
            echo 1>&2 "fatal: branch $branch"
            exit $exitcode
        fi
    fi
done

post-合并:

#!/bin/sh

~/.git-hooks/helpers/foxtrot-merge-detector
# check exit code and exit if needed
exitcode=$?
if [ $exitcode -ne 0 ]; then
    echo -e "  ${Yellow}WARNING:${None} foxtrot merge detected"
    # swallow exit code
fi