You can't interact with the user in the pre-receive hook, at least not in general, since the user might be on some other machine doing an "ssh", and standard input is not going to the user. (You might be able to cobble something up by inspecting uids and/or user names and such, and escaping out to a script that "calls back" the user. I leave experiments in that direction for you.) You can do all those checks, though.
The pre-receive hook gets (on stdin) a series of lines:
<old-value> SP <new-value> SP <ref-name> LF
(quoted from githooks(5)
). If the ref-name has the form:
refs/heads/<branchname>
then the given branch is being created, deleted, or updated. The branch is being created if <old-value>
is 40 0
s; it's being deleted if <new-value>
is 40 0
s; and it's being updated otherwise. (There are things other than refs/heads/*; see the git-send-email hook for a complete list.)
If the branch is being updated, <old-value>
is the commit ID to which it used to point, and <new-value>
is the commit ID to which it will point if the update is allowed (this depends on both the pre-receive hook and the update hook).
Here's a hook that simply detects your second and third cases (plus deletion). To find out if there are merges, you'll have to go through each rev in $between
and see whether any is a merge (i.e., has more than one parent). To stop the commit, exit nonzero instead of returning 0.
#! /bin/sh
check()
{
local old=$1 new=$2 longref=$3
local between rev
if expr $old : '^00*$' >/dev/null; then
echo creating new branch ${longref#refs/heads/}
return 0
fi
if expr $new : '^00*$' >/dev/null; then
echo removing branch ${longref#refs/heads/}
return 0
fi
between=$(git rev-list $old..$new)
case "$between" in
*$'\n'*)
echo at least two revs
for rev in $between; do git log -1 --oneline $rev; done
return 0
esac
echo only one rev
return 0
}
while read old new longref; do
case $longref in
refs/heads/*) check $old $new $longref;;
esac
done
(you can inspect all the revs in $between
as they have already been sent to the remote repo, even if you're going to reject them).
Update: I figured out a way to make this work even when using the ssh transport, which does not let you smuggle in any extra data.
I modified (and renamed) the check
function. Under the new name, get_confirmation
, it's meant to figure out (and return 0 for) cases where you don't want to allow the push by default (and return 1 for cases where it's to be allowed).
Then, in the main loop, you can do this:
case $longref in
refs/heads/*)
if get_confirmation $old $new $longref; then
case $PWD in
*.allow.git)
echo 'allowed via alternate path'
;;
*.git)
echo "denied ... push to ${PWD%.git}.allow.git to allow"
exit 1
;;
*)
echo "denied, don't know where I am"
exit 1
;;
esac
fi
;;
# add more cases here if desired
esac
This assumes that you push to a --bare
clone that lives in /some/path/to/repo.git
.
To make an equivalent repo that allows pushes, you have to create an actual parallel directory whose name ends in repo.allow.git
. This directory should contain one actual file, HEAD
, copied from the plain repo; and everything else in the directory can be a symbolic link to ../repo.git/<same>
:
cd /some/path/to
mkdir repo.allow.git
cd repo.allow.git
ln -s ../repo.git/* .
rm HEAD; cp ../repo.git/HEAD .
The reason that HEAD
has to be an ordinary file is that otherwise git push
won't treat this as a valid repo (it's only allowed to be a symlink if it's a link to a branch, and that's the "old way" of doing a symbolic ref).
A regular git push
fails, and you can instead git push remotehost:/some/path/to/repo.allow.git
to make it go through. Of course your users might get in the habit of doing that all the time, obviating the entire purpose of this hack, but maybe they won't.
This all does assume that your remote repo is a Unix-like host, of course (i.e., supports symlinks).