On the server, you have two options, namely the pre-receive and update hooks. (For this particular case I'd probably use an update hook myself.)
The pre-receive hook is invoked once, with standard input connected to a pipe containing all the proposed reference updates. You should read all stdin lines, and use the old and new hash IDs and the names of all the references to decide whether the entire push is allowed to proceed, or the entire push—all name-updates—is to be rejected all at once. That is, given that some client has run:
git push origin hash1:name1 hash2:name2 ... hashN:nameN
so that there are N update requests on N lines of stdin, your pre-receive hook either accepts all, or rejects all. To accept all, exit with status zero; to forbid all, exit with any nonzero status. It's a good idea to print the reason for the rejection, if you exit nonzero, so that the client will see why you did this.
The update hook is invoked once per proposed update, after the pre-receive hook (if there is one) has allowed the entire process to enter the second phase. It has three positional parameters giving the same information that came in on one of the input lines to the pre-receive hook. You should examine the two hash IDs and the name, and decide whether this particular update is allowed.
That is, given the same client invocation, your update hook will be invoked N separate times. The second one will, for instance, have:
$1: refs/heads/name2
$2: the old hash ID (or the all-zeros "null hash")
$3: the new hash ID (or all-zeros)
If you're willing to have name2
set to point to the new hash ID, have your update hook exit with a zero (success) status. If not, have it exit with a nonzero status. Again, it's a good idea to print something if you are going to reject the update.
About server-side hooks in general
Your hooks receives, per reference, an old hash ID ($old
below), a new one ($new
), and the full name of the reference: refs/heads/name
if the reference is a branch name, refs/tags/name
if it's a tag name, refs/notes/name
if it's a notes reference, and so on. An update hook has finer granularity, but cannot see the proposed update as a whole.
At most one of $old
or $new
will be all-zeros. If so, the reference is to be created—e.g., a new branch or tag—or destroyed. Otherwise, it's an in-place update: the reference currently points to hash ID $old
and the person running git push
is proposing to change it to point to hash ID $new
.
These hooks are highly effective, but difficult to write. In particular, if a reference is being updated it's pretty clear what to do: the update will add commits in the $old..$new
range, so:
git rev-list $old..$new | while read rev; do
# examine the files in $rev
done
suffices to allow you to inspect the contents of each proposed new commit. (Some commits may be being deleted and those can be found by inspecting $new..$old
.)
However, if the reference is newly created, $old
will be all-zeros. It's impossible to tell for certain which references are newly introduced solely by this particular reference. You can use this trick:
git rev-list $new --not --all
to enumerate commits reachable from the proposed new reference, but not from any current reference. That could be misleading, though: perhaps the push is a request to create three new branch names:
...--o--o--o <-- master
\
A--B <-- newbranch1
\
C <-- newbranch2
\
D--E--F <-- newbranch3
Taken in isolation, the request to set newbranch3
to point to a commit whose hash ID is F
looks like a request to add all six commits (which it is!) but you might prefer to view it as a request to add just three commits after the otherwise-added branch newbranch2
, for instance. It's not possible to produce this view in an update hook. It is possible (but hard) to produce it in the pre-receive hook as it can tell that all three newbranch*
names are new.