How can I prevent foxtrot merges in my 'master' branch?
Solution 1:
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.
Solution 2:
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;
Solution 3:
I wrote this to provide feedback early (pre-receive
hook is also needed). I use post-merge
and pre-push
hooks for this. It's not possible to prevent a foxtrot merge in commit-msg hook as the information to detect one is available only after. In post-merge
hook, I simply warn. In pre-push
hook, I throw and block the push. See 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
# https://stackoverflow.com/questions/35962754/git-how-can-i-prevent-foxtrot-merges-in-my-master-branch
remoteBranch=$(git rev-parse --abbrev-ref "$1"@{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
And then use it as
pre-push hook:
#!/bin/sh
remote="$1"
url="$2"
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-merge:
#!/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