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