eran meiri
eran meiri

Reputation: 1508

git post commit hook - server side

I need to run a post-commit git server side hook that si supposed to check if there has been a merge and do some automation if there was a merge.

I tried using git reflog -1 and it works great but only on the client side.

I also tried implementing a post-merge hook but that did not work.

when I run git reflog -1 on the server side I get no output.

any ideas how I can accomplish this?

Upvotes: 1

Views: 473

Answers (1)

torek
torek

Reputation: 487755

ElpieKay's comment has the key to the answer, but it's worth a bit more detail. Since the server did not actually run git commit, you must use a post-receive hook to watch for reference updates. Unfortunately, post-receive hooks are not simple. Here is a simple framework that is generally useful, expressed in POSIX shell:

#! /bin/sh
# sample post-receive hook

# return true if the argument ($1) is the null hash (all-0s)
is_nullhash() {
    expr "$1" : '0*$' >/dev/null
}

while read oldhash newhash ref; do
    # If the old hash is all 0s, the reference was just created.
    # If the new hash is all 0s, the reference was just deleted.
    # Otherwise, the reference was updated, from $oldhash to $newhash.
    if is_nullhash $oldhash; then
        op=create
    elif is_nullhash $newhash; then
        op=delete
    else
        op=update
    fi

    # If the reference begins with refs/heads/, the rest of it is
    # a branch name.  If it starts with refs/tags, the rest is a tag.
    # Otherwise it's some other type of reference (not decoded here).
    case $ref in
    refs/heads/*) reftype=branch; shortref=${ref#refs/heads/};;
    refs/tags/*) reftype=tag; shortref=${ref#refs/tags/};;
    *) reftype=other; shortref=$ref;;  # NB: not shortened!
    esac

     ... insert code here ...
done

The code that goes in the "insert code here" section must:

  • determine whether this is an operation that you care about
  • if so, decide what to do based on the type of operation

For instance, if you want to watch specifically for a push to master only:

if [ $reftype = branch -a $shortref = master ]; then
    if [ $op = update ]; then
        handle_master_update $oldhash $newhash
    else
        ... do something different if the op is create or delete ...
    fi
fi

where handle_master_update is your function to deal with an update to master. If you don't want to handle creation and deletion of the master branch, you can simplify this further to:

case $ref,$op in
refs/heads/master,update) handle_master_update $oldhash $newhash;;
esac

in which case you can delete the boilerplate section that decodes the reference type.

Now we get to the heart of the update-handling:

# Do something with update to master branch.
# The old hash is $1 and the new hash is $2,
# so commits in $1..$2 now appear on `master` but
# were not part of `master` before (they may have
# been on some other branch, and may still be).
# Meanwhile, commits in $2..$1 used to be on `master`
# but have just been removed via force-push.  If this
# list is empty, the push did not have to be forced,
# and maybe was not.
handle_master_update() {
    local rev parents
    git rev-list $2..$1 | while read rev; do ...; done  # deal with removed commits

    git rev-list --parents $1..$2 | while read rev parents; do
        ...
    done
}

The first section (dealing with removed commits) is of course optional. The second section is the one where you wanted to detect merges, so we use git rev-list (not git log—rev-list is the more useful workhorse here) to enumerate all the added commits along with their parent hash IDs. The read puts all the parent hashes into the variable parents, so we can easily count them in the ... section. For instance:

        set -- $parents
        case $# in
        0) ...;; # $rev is a root commit
        1) ...;; # $rev is an ordinary commit
        *) ...;; # $rev is a merge, its parents are $1, $2, ... through $#
        esac

The zero case is highly unusual—for it to happen as an update, we must have added a root commit to the branch, e.g., going from:

A--B--C   <-- master

to:

A--B--C--D--H   <-- master
           /
F---------G

The git rev-list, given C..H (well, their hash IDs), will list H, D, G, and F in some order. The hash for H will come first, but everything after that depends on any sorting options you supply to git rev-list. Use --topo-order to guarantee that you receive a topological sort, if that's important to how your code works (e.g., you might want --reverse --topo-sort so as to always be working forwards, getting one of D, F, G, H or F, G, D, H as your inputs).

Note that if you do use a POSIX-compatible sh to implement your hook, there's an annoyance with git rev-list ... | while read ...: the while loop runs in a subshell which means any variables set here are lost when the loop exits. You can work around this several ways; see While-loop subshell dilemma in Bash.

Upvotes: 2

Related Questions