Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

A foxtrot merge is a merge where 'origin/master' merges in as a 2nd (or later) parent, like so:

Commit 'D' is a foxtrot merge because 'origin/master' is its 2nd parent.

Commit 'D' is a foxtrot merge because 'origin/master' is its 2nd parent. Notice how first-parent history from 'origin/master' contains commit 'B' at this moment.

But in my git repo I need all merges involving 'origin/master' to keep 'origin/master' as the 1st parent. Unfortunately git doesn't care about parent-order when it evaluates whether a commit is eligible for fast-forward. This causes the first parent history on my master branch to sometimes lose commits that used to be there (e.g., output of "git log --first-parent").

Here's what happens when commit 'D' from the earlier diagram is pushed:

How can I prevent this push? First-parent history of 'origin/master' no longer contains commit 'B' after the foxtrot merge is pushed!

How can I prevent this push? First-parent history of 'origin/master' no longer contains commit 'B' after the foxtrot merge is pushed!

Obviously no commits or work are actually lost, it's just that in my environment I really need "git log --first-parent" to be a stable accumulative record of commits - if you like, a kind of "Write-Once Read-Many" (WORM) database. I have scripts and processes that use "git log --first-parent" to generate changelogs and release notes, as well as to manage ticket transitions in my ticketing system (JIRA). Foxtrot merges are breaking my scripts!

Is there some kind of pre-receive hook I could install in my git repositories to prevent foxtrot merges from getting pushed?

p.s. The commit graphs in this stackoverflow question were generated using http://bit-booster.com/graph.html.

like image 679
G. Sylvie Davies Avatar asked Mar 12 '16 20:03

G. Sylvie Davies


People also ask

How do I stop a branch from merging?

Use git-reset or git merge --abort to cancel a merge that had conflicts. Please note that all the changes will be reset, and this operation cannot be reverted, so make sure to commit or git-stash all your changes before you start a merge.

How do you fix commit rejected Foxtrot merges not allowed?

Typically only default "git pull" causes foxtrot merges, and "git pull -r" is a great remedy. The "git merge" command can also cause it, but it's rare for people to accidentally create a foxtrot merge when using "git merge" in typical scenarios that call for "git merge".

Does merging affect both branches?

No, merging does only affect one branch.


2 Answers

The following pre-receive hook will block those:

#/bin/bash  # Copyright (c) 2016 G. Sylvie Davies. http://bit-booster.com/ # Copyright (c) 2016 torek. http://stackoverflow.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 

If you're using Github / Gitlab / Bitbucket Cloud, you may need to look into creating some kind of call into their commit status apis (here's api docs for: bitbucket, github; not sure if gitlab has one), because you don't have access to the pre-receive hooks, and even if you did, you'd still have to deal with people clicking the "merge" button directly in the web ui of those products (in which case there is no "push").

With Bitbucket Server you can install the add-on I created.

Once it's installed you click "enable" on the "Protect First Parent Hook" in a given repository's "hook" settings:

enter image description here

It will block foxtrot merges via push and via the "merge" button in the Bitbucket Server UI. It does this even if its license is expired, making the "Protect First-Parent Hook" a free component of the larger add-on.

Here's an example of my Bit-Booster "Protect First Parent" pre-receive hook in action:

$ ​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:  

For more background on foxtrot merges I wrote a blog post.

like image 171
G. Sylvie Davies Avatar answered Oct 07 '22 02:10

G. Sylvie Davies


Here is a hook code which will do what you are asking for:

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='\033[0;31m';     green='\033[0;32m';     yellow='\033[0;33m';     default='\033[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; 
like image 40
CodeWizard Avatar answered Oct 07 '22 02:10

CodeWizard