Hello Bob and all,

Here's my attempt at a git update hook which conditionally allows 
non-fast-forward commits on branches (and even 'master', based on 
configuration).

It works like this:

1.
By default, non-fast-forwards are not allowed.

Trying to push non-fast-forward to 'master' branch gives:
====
$ git push origin +master
Counting objects: 1, done.
Writing objects: 100% (1/1), 208 bytes | 0 bytes/s, done.
Total 1 (delta 0), reused 0 (delta 0)
remote: *** Rejected: non-fast-forward on the master branch are disabled in 
Savannah.
remote:     See http://savannah.gnu.org/maintenance/XXXXXXX for details.
remote: error: hook declined to update refs/heads/master
To /home/gordon/projects/git-hook-test/orig-bare/
 ! [remote rejected] master -> master (hook declined)
error: failed to push some refs to 
'/home/gordon/projects/git-hook-test/orig-bare/'
====

Trying to push non-fast-forward to non-master branch gives:
====
$ git push origin +dev/test1
Counting objects: 18, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (8/8), done.
Writing objects: 100% (12/12), 968 bytes | 0 bytes/s, done.
Total 12 (delta 4), reused 0 (delta 0)
remote: *** Rejected: non-fast-forward on branches are disabled for this 
repository.
remote:     See http://savannah.gnu.org/maintenance/XXXXXXX for details.
remote: error: hook declined to update refs/heads/dev/test1
To /home/gordon/projects/git-hook-test/orig-bare/
 ! [remote rejected] dev/test1 -> dev/test1 (hook declined)
error: failed to push some refs to 
'/home/gordon/projects/git-hook-test/orig-bare/'
====

2.
Two configuration options control whether non-fast-forwards commits are allowed:
Setting this:
    git config hooks.allowbranchnonfastforward true
Will enable non-fast-forward commits to all non-master branch.

Setting this:
    git config hooks.allowbranchnonfastforward true
Will enable non-fast-forward commits to the master branch.
This will also send a notification email to a pre-configured email (e.g. 
sv-hk-private), to ensure such non-common events don't go unnoticed.

The email looks like this (not pretty, but informative):
===
This is an automated message.

The master branch of '/home/gordon/projects/git-hook-test/orig-bare' is being 
modified with a
non-fast-forward commit.
time: Tue, 20 Jan 2015 20:39:41 +0000
old-revision: 9f329a0388358af91ae194a6f4d4fcd640a1b117
new-revision: 31f645f6582e9763f5c9537568122cf4791b415a
===


Comments and improvements are welcomed,
 - Assaf

P.S.
Note that deleting branches is allowed without checking, as it is today.
Controlling branch deletion is also possible.



#!/bin/sh
#
# --- Command line
refname="$1"
oldrev="$2"
newrev="$3"

# --- 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

# Get the full path of the repository
repository=$(readlink -f "$GIT_DIR")

##
## Custom Configuration Options
##

## Allow non-fast-forward commits on branches (except 'master')
## To enable:
##   git config hooks.allowbranchnonfastforward true
allow_branch_nonfastforward=$(git config --bool hooks.allowbranchnonfastforward)

## Allow non-fast-forward commits on the master branch
## This should be 'false' by default, and only enabled under special
## circumstances.
## To enable:
##  git config hooks.allowmasternonfastforward true
allow_master_nonfastforward=$(git config --bool hooks.allowmasternonfastforward)

## When a master-branch is updated with non-fast-forward commit,
## notify this email address (e.g [email protected])
[email protected]


# --- 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

case "$refname","$newrev_type" in
    refs/tags/*,commit)
        # un-annotated tag
        ;;
    refs/tags/*,delete)
        # delete tag
        ;;
    refs/tags/*,tag)
        # annotated tag
        ;;
    refs/heads/*,commit)
        # commit to a branch
        #  'revlist' should be empty on a fast-forward commit
        revlist=$(git rev-list "$newrev".."$oldrev")
        branch=${refname##refs/heads/}

        # Non-fast-forard commit?
        if test -n "$revlist" ; then

            # non-fast-forward to the master branch?
            if test "x$branch" = "xmaster" ; then

                if test "x$allow_master_nonfastforward" != "xtrue" ; then
                    echo \
"*** Rejected: non-fast-forward on the master branch are disabled in Savannah.
    See http://savannah.gnu.org/maintenance/XXXXXXX for details."
                    exit 1
                fi

                # A non-fast-forward commit is allowed for the master
                # branch of this repository.
                # To regulate funkiness, send an email notification.
                echo "This is an automated message.

The master branch of '$repository' is being modified with a
non-fast-forward commit.
time: $(date -uR)
old-revision: $oldrev
new-revision: $newrev" \
                    | mail -s 'Master branch update: $repository' \
                            "$master_update_notification"
            else
                # non-fast-foward to a non-master branch?

                if test "x$allow_branch_nonfastforward" != "xtrue" ; then
                    echo \
"*** Rejected: non-fast-forward commits on non-master branches are disabled
    for this repository.
    See http://savannah.gnu.org/maintenance/XXXXXXX for details."
                    exit 1
                fi
            fi
        fi
        ;;
    refs/heads/*,delete)
        # delete branch
        ;;
    refs/remotes/*,commit)
        # tracking branch
        ;;
    refs/remotes/*,delete)
        # delete tracking branch
        ;;
    *)
        # Anything else (is there anything else?)
        echo "*** Update hook: unknown type of update to ref $refname of type 
$newrev_type" >&2
        exit 1
        ;;
esac

# --- Finished
exit 0

Reply via email to