diff options
Diffstat (limited to 'contrib/hooks/repo-specific/save-push-signatures')
-rwxr-xr-x | contrib/hooks/repo-specific/save-push-signatures | 188 |
1 files changed, 188 insertions, 0 deletions
diff --git a/contrib/hooks/repo-specific/save-push-signatures b/contrib/hooks/repo-specific/save-push-signatures new file mode 100755 index 0000000..2470491 --- /dev/null +++ b/contrib/hooks/repo-specific/save-push-signatures @@ -0,0 +1,188 @@ +#!/bin/sh + +# ---------------------------------------------------------------------- +# post-receive hook to adopt push certs into 'refs/push-certs' + +# Collects the cert blob on push and saves it, then, if a certain number of +# signed pushes have been seen, processes all the "saved" blobs in one go, +# adding them to the special ref 'refs/push-certs'. This is done in a way +# that allows searching for all the certs pertaining to one specific branch +# (thanks to Junio Hamano for this idea plus general brainstorming). + +# The "collection" happens only if $GIT_PUSH_CERT_NONCE_STATUS = OK; again, +# thanks to Junio for pointing this out; see [1] +# +# [1]: https://groups.google.com/forum/#!topic/gitolite/7cSrU6JorEY + +# WARNINGS: +# Does not check that GIT_PUSH_CERT_STATUS = "G". If you want to check that +# and FAIL the push, you'll have to write a simple pre-receive hook +# (post-receive is not the place for that; see 'man githooks'). +# +# Gitolite users: failing the hook cannot be done as a VREF because git does +# not set those environment variables in the update hook. You'll have to +# write a trivial pre-receive hook and add that in. + +# Relevant gitolite doc links: +# repo-specific environment variables +# http://gitolite.com/gitolite/dev-notes.html#rsev +# repo-specific hooks +# http://gitolite.com/gitolite/non-core.html#rsh +# http://gitolite.com/gitolite/cookbook.html#v3.6-variation-repo-specific-hooks + +# Environment: +# GIT_PUSH_CERT_NONCE_STATUS should be "OK" (as mentioned above) +# +# GL_OPTIONS_GPC_PENDING (optional; defaults to 1). This is the number of +# git push certs that should be waiting in order to trigger the post +# processing. You can set it within gitolite like so: +# +# repo foo bar # or maybe just 'repo @all' +# option ENV.GPC_PENDING = 5 + +# Setup: +# Set up this code as a post-receive hook for whatever repos you need to. +# Then arrange to have the environment variable GL_OPTION_GPC_PENDING set to +# some number, as shown above. (This is only required if you need it to be +# greater than 1.) It could of course be different for different repos. +# Also see "Invocation" section below. + +# Invocation: +# Normally via git (see 'man githooks'), once it is setup as a post-receive +# hook. +# +# However, if you set the "pending" limit high, and want to periodically +# "clean up" pending certs without necessarily waiting for the counter to +# trip, do the following (untested): +# +# RB=$(gitolite query-rc GL_REPO_BASE) +# for r in $(gitolite list-phy-repos) +# do +# cd $RB/$repo.git +# unset GL_OPTIONS_GPC_PENDING # if it is set higher up +# hooks/post-receive post_process +# done +# +# That will take care of it. + +# Using without gitolite: +# Just set GL_OPTIONS_GPC_PENDING within the script (maybe read it from git +# config). Everything else is independent of gitolite. + +# ---------------------------------------------------------------------- +# make it work on BSD also (but NOT YET TESTED on FreeBSD!) +uname_s=`uname -s` +if [ "$uname_s" = "Linux" ] +then + _lock() { flock "$@"; } +else + _lock() { lockf -k "$@"; } + # I'm assuming other BSDs also have this; I only have FreeBSD. +fi + +# ---------------------------------------------------------------------- +# standard stuff +die() { echo "$@" >&2; exit 1; } +warn() { echo "$@" >&2; } + +# ---------------------------------------------------------------------- +# if there are no arguments, we're running as a "post-receive" hook +if [ -z "$1" ] +then + # ignore if it may be a replay attack + [ "$GIT_PUSH_CERT_NONCE_STATUS" = "OK" ] || exit 1 + # I don't think "exit 1" does anything in a post-receive anyway, so that's + # just a symbolic gesture! + + # note the lock file used + _lock .gpc.lock $0 cat_blob + + # if you want to initiate the post-processing ONLY from outside (for + # example via cron), comment out the next line. + exec $0 post_process +fi + +# ---------------------------------------------------------------------- +# the 'post_process' part; see "Invocation" section in the doc at the top +if [ "$1" = "post_process" ] +then + # this is the same lock file as above + _lock .gpc.lock $0 count_and_rotate $$ + + [ -d git-push-certs.$$ ] || exit 0 + + # but this is a different one + _lock .gpc.ref.lock $0 update_ref $$ + + exit 0 +fi + +# ---------------------------------------------------------------------- +# other values for "$1" are internal use only + +if [ "$1" = "cat_blob" ] +then + mkdir -p git-push-certs + git cat-file blob $GIT_PUSH_CERT > git-push-certs/$GIT_PUSH_CERT + echo $GIT_PUSH_CERT >> git-push-certs/.blob.list +fi + +if [ "$1" = "count_and_rotate" ] +then + count=$(ls git-push-certs | wc -l) + if test $count -ge ${GL_OPTIONS_GPC_PENDING:-1} + then + # rotate the directory + mv git-push-certs git-push-certs.$2 + fi +fi + +if [ "$1" = "update_ref" ] +then + # use a different index file for all this + GIT_INDEX_FILE=push_certs_index; export GIT_INDEX_FILE + + # prepare the special ref to receive commits + PUSH_CERTS=refs/push-certs + if git rev-parse -q --verify $PUSH_CERTS >/dev/null + then + git read-tree $PUSH_CERTS + else + git read-tree --empty + T=$(git write-tree) + C=$(echo 'start' | git commit-tree $T) + git update-ref $PUSH_CERTS $C + fi + + # for each cert blob... + for b in `cat git-push-certs.$2/.blob.list` + do + cf=git-push-certs.$2/$b + + # it's highly unlikely that the blob got GC-ed already but write it + # back anyway, just in case + B=$(git hash-object -w $cf) + + # bit of a sanity check + [ "$B" = "$b" ] || warn "this should not happen: $B is not equal to $b" + + # for each ref described within the cert, update the index + for ref in `cat $cf | egrep '^[a-f0-9]+ [a-f0-9]+ refs/' | cut -f3 -d' '` + do + git update-index --add --cacheinfo 100644,$b,$ref + # we're using the ref name as a "fake" filename, so people can, + # for example, 'git log refs/push-certs -- refs/heads/master', to + # see all the push certs pertaining to the master branch. This + # idea came from Junio Hamano, the git maintainer (I certainly + # don't deal with git plumbing enough to have thought of it!) + done + + T=$(git write-tree) + C=$( git commit-tree -p $PUSH_CERTS $T < $cf ) + git update-ref $PUSH_CERTS $C + + rm -f $cf + done + rm -f git-push-certs.$2/.blob.list + rmdir git-push-certs.$2 +fi |