2

I wanted to set up a pre-receive hook that does a logic such that

  • Checks if there are any merge commits
  • Checks if a new branch is pushed
  • Checks if there are multiple commits.

If any of the above is present, then interact with the user to check with him if he would like to push the change and if yes, then push the change and else drop it.

Is there a possibility to achieve the above in pre-receive hook in git?

Michael Wild
  • 24,977
  • 3
  • 43
  • 43
Shar
  • 55
  • 1
  • 8

1 Answers1

3

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 0s; it's being deleted if <new-value> is 40 0s; 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).

torek
  • 448,244
  • 59
  • 642
  • 775
  • Torek thanks. I will try this. Also one way to make hooks interactive is by using 'exec < /dev/tty' similar to the one noted in http://stackoverflow.com/questions/3417896/how-do-i-prompt-the-user-from-within-a-commit-msg-hook/10015707#10015707 – Shar May 01 '12 at 06:43
  • The `exec` trick is fine in a locally-run hook, but pre-receive is typically run on a remote machine with a non-interactive ssh: `git push` ... `remote: hooks/pre-receive: line 30: /dev/tty: Device not configured` – torek May 01 '12 at 06:59