Protection from merge commits

Daniel P. Berrange berrange at redhat.com
Thu Sep 12 09:09:54 UTC 2013


On Wed, Sep 11, 2013 at 04:13:43PM -0600, Pete Zaitcev wrote:
> Dear All:
> 
> I found that I pushed a merge node into Fedora git (fortunately it was
> only on f19 branch, not in Rawhide):
> 
> commit 51eec48c20ba57054f17ba29f23e6a0aa36df9a4
> Merge: ac36771 f9213a5
> Author: Pete Zaitcev <zaitcev at kotori.zaitcev.us>
> Date:   Wed Aug 7 12:14:15 2013 -0600
> 
>     Merge branch 'f19' of ssh://pkgs.fedoraproject.org/openstack-swift into f19
> 
> Thought I was careful about them, but apparently not enough. So, does
> anyone have a script that can be attached to git hooks, reliably detects
> an attempt to push merge nodes, then bails the push?

The attached script is what we use in libvirt for preventing merge
commits being pushed to the main repo.  Copy that to your hooks
directory naming the file 'update'. Then edit your .git/config
file to set

  [hooks "denymerge"]
          master = true


It can also be used to prevent creation of new branches or to block
commits which have trailing whitespace at the end of line eg by
setting

  [hooks]
          allowbadwhitespace = false
          denycreatebranch = true

See the script for other possible config options

Regards,
Daniel
-- 
|: http://berrange.com      -o-    http://www.flickr.com/photos/dberrange/ :|
|: http://libvirt.org              -o-             http://virt-manager.org :|
|: http://autobuild.org       -o-         http://search.cpan.org/~danberr/ :|
|: http://entangle-photo.org       -o-       http://live.gnome.org/gtk-vnc :|
-------------- next part --------------
#!/bin/sh
#
# An example hook script to block unannotated tags from entering.
# Called by "git receive-pack" with arguments: refname sha1-old sha1-new
#
# To enable this hook, rename this file to "update".
#
# Config
# ------
# hooks.allowunannotated
#   This boolean sets whether unannotated tags will be allowed into the
#   repository.  By default they won't be.
# hooks.allowdeletetag
#   This boolean sets whether deleting tags will be allowed in the
#   repository.  By default they won't be.
# hooks.allowmodifytag
#   This boolean sets whether a tag may be modified after creation. By default
#   it won't be.
# hooks.allowdeletebranch
#   This boolean sets whether deleting branches will be allowed in the
#   repository.  By default they won't be.
# hooks.denycreatebranch
#   This boolean sets whether remotely creating branches will be denied
#   in the repository.  By default this is allowed.
#
# hooks.allowbadwhitespace
#   This boolean sets whether you may push a commit that adds bad whitespace.
#   By default, you may not.
#
# hooks.denypush.branch.BRANCH_NAME
#   If defined to a string that looks like an email address, this option
#   disables push access to the specified branch.  When a push fails as
#   a result of this option, the resulting diagnostic includes the specified
#   email address.  For example, run this on the server to deny all push
#   access to "master":
#
#   For example, enable it with this:
#   git config hooks.denypush.branch.master master-branch-owner at example.com
#
# hooks.denymerge.BRANCH_NAME
#   When this boolean is true, you may not push a merge commit to BRANCH_NAME.
#   By default, you may.
#
# ---------------------------------------------------------------------
# Allow people to change server-side git config in very specific ways.
# To enable this, on the server, you must do something like the following,
#
# git config hooks.server.config-changing.valid-commands \
# 'git config hooks.allowbadwhitespace true
# git config hooks.allowbadwhitespace false
# git config hooks.denypush.branch.master master-branch-owner at example.com
# git config --unset hooks.denypush.branch.master'
#
# where the git config variable, hooks.server.config-changing.valid-commands,
# contains the list of commands that are allowed, one per line.
#
# CAUTION: nothing about this hook code enforces the restriction that
# only "git config ..." commands be run automatically.
# That restriction comes solely from the list above.
#
# Then, when someone with a cloned repository wants to make the hook
# run one of those commands *on* *the* *server*, that user must push
# a tag whose name starts with "git-control-" and whose one-line message
# matches exactly one of the listed commands.
#
# For example, to temporarily allow someone to push a bad-whitespace
# commit, with the settings implied above, you might do this:
#
#   first_commit=$(git log --reverse --pretty=%H |head -1)
#   git tag -m 'git config hooks.allowbadwhitespace true' \
#     git-control-$(date +%F-%H-%M-%S.%N) $first_commit
#
# Note that we're not tagging HEAD, but rather the very first commit
# in the repository, in an attempt not to clutter up gitk/gitweb
# displays with these rarely-interesting tag names.
#
# Then, to reenable that hook, do this (nearly the same, except s/true/false/):
#
#   first_commit=$(git log --reverse --pretty=%H |head -1)
#   git tag -m 'git config hooks.allowbadwhitespace false' \
#     git-control-$(date +%F-%H-%M-%S.%N) $first_commit
# ---------------------------------------------------------------------

# --- Command line
refname="$1"
oldrev="$2"
newrev="$3"

is_merge_commit()
{
	git rev-parse --verify --quiet $1^2 > /dev/null
}

# --- Safety check
if [ -z "$GIT_DIR" ]; then
	echo "Don't run this script from the command line." >&2
	echo " (if you want, you could supply GIT_DIR then run" >&2
	echo "  $0 <ref> <oldrev> <newrev>)" >&2
	exit 1
fi

if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
	echo "Usage: $0 <ref> <oldrev> <newrev>" >&2
	exit 1
fi

# --- Config
allowunannotated=$(git config --bool hooks.allowunannotated)
allowdeletebranch=$(git config --bool hooks.allowdeletebranch)
denycreatebranch=$(git config --bool hooks.denycreatebranch)
allowdeletetag=$(git config --bool hooks.allowdeletetag)
allowmodifytag=$(git config --bool hooks.allowmodifytag)

# check for no description
projectdesc=$(sed -e '1q' "$GIT_DIR/description")
case "$projectdesc" in
"Unnamed repository"* | "")
	echo "*** Project description file hasn't been set" >&2
	exit 1
	;;
esac

# --- Check types
# if $newrev is 0000...0000, it's a commit to delete a ref.
zero="0000000000000000000000000000000000000000"
if [ "$newrev" = "$zero" ]; then
	newrev_type=delete
else
	newrev_type=$(git cat-file -t $newrev)
fi

check_diff=no
case "$refname","$newrev_type" in
	refs/tags/*,commit)
		# un-annotated tag
		short_refname=${refname##refs/tags/}
		if [ "$allowunannotated" != "true" ]; then
			echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2
			echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
			exit 1
		fi
		;;
	refs/tags/*,delete)
		# delete tag
		if [ "$allowdeletetag" != "true" ]; then
			echo "*** Deleting a tag is not allowed in this repository" >&2
			exit 1
		fi
		;;
	refs/tags/*,tag)
		case $refname in
		  refs/tags/show)
		    echo "*** a tag named 'show' is not permitted" >&2
		    exit 1
		    ;;
		  refs/tags/git-control-*)
		    cmd=$(git cat-file -p $newrev|tail -n+6)
		    git config hooks.server.config-changing.valid-commands |
		    (while read v_cmd; do
		      if test "x$cmd" = "x$v_cmd"; then
			echo Running cmd: "$v_cmd"
			eval "$v_cmd"
			match=1
			exit 0 # found a match
		      fi
		    done
		    exit 1 # signal failure to match
		    )
		    if test $? = 1; then
		      echo "*** unrecognized directive: $cmd" >&2
		      exit 1
		    fi
		    ;;
		esac
		# annotated tag
		if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1
		then
			echo "*** Tag '$refname' already exists." >&2
			echo "*** Modifying a tag is not allowed in this repository." >&2
			exit 1
		fi
		;;
	refs/heads/*,commit)
		# branch
		if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then
			echo "*** Creating a branch is not allowed in this repository" >&2
			exit 1
		fi

		check_diff=yes
		branch=${1##refs/heads/}
		deny_push_email=$(git config "hooks.denypush.branch.$branch")
		case $deny_push_email in
			'') ;;
			*) printf "error: *** %s\n" \
			      "commit on branch '$branch'" \
			      "locked by $deny_push_email" >&2
			 exit 1;;
		esac

		# When enabled, this prohibits pushing a merge commit.
		# Enable this hook for branch "next" with e.g.,
		# git config --bool hooks.denymerge.next true
		deny_merge=$(git config --bool "hooks.denymerge.$branch")
		case $deny_merge in
		    true)
			for r in $(test "$oldrev" = $zero \
				     && git rev-list $newrev \
				     || git rev-list $newrev ^$oldrev); do
			  is_merge_commit $r && {
			    printf "error: *** %s\n" \
			  "You may not push merge commits to branch $branch." \
			  "Did you forget to rebase? ($r)" >&2
			  exit 1
			  }
			done
			;;
		esac

		;;
	refs/heads/*,delete)
		# delete branch
		if [ "$allowdeletebranch" != "true" ]; then
			echo "*** Deleting a branch is not allowed in this repository" >&2
			exit 1
		fi
		;;
	refs/remotes/*,commit)
		# tracking branch
		check_diff=yes
		;;
	refs/remotes/*,delete)
		# delete tracking branch
		if [ "$allowdeletebranch" != "true" ]; then
			echo "*** Deleting a tracking branch is not allowed in this repository" >&2
			exit 1
		fi
		;;
	*)
		# Anything else (is there anything else?)
		echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
		exit 1
		;;
esac

if [ $check_diff = yes ]; then
	allow_bad_whitespace=$(git config --bool hooks.allowbadwhitespace)
	if [ "$allow_bad_whitespace" != "true" ]; then
		test "$oldrev" = $zero \
			&& exit 0
		exec git diff --check $oldrev $newrev --
	fi
fi

# --- Finished
exit 0


More information about the devel mailing list