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. 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!
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.
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.
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".
No, merging does only affect one branch.
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:
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.
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;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With