1

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?

eran meiri
  • 1,322
  • 3
  • 12
  • 29
  • You may want `post-receive` or `post-update`. – ElpieKay Oct 21 '18 at 13:36
  • I tried post-receive but I can't tell if I am in a merge or just a regular commit – eran meiri Oct 21 '18 at 13:57
  • 1
    `post-commit` can't work if the repository is used as a remote repository (a server side repository). In `post-receive` you can get the old value and the new value of all pushed refs from the stdandard input. Check all the commits between the old commit and the new commit, by `git log --pretty=%H $oldvalue..$newvalue` for example. If a commit has more than 1 parent, it's a merge commit. `git log -1 $commit --pretty=%P` returns all the parents of the commit. – ElpieKay Oct 21 '18 at 14:03

1 Answers1

2

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.

torek
  • 448,244
  • 59
  • 642
  • 775
  • Hi , first of all thanks for the answer. I tried running it but my reftype is not branch nor tag for some reason ( in the script default is other ). the shortref is the commit hash. I tested it in Fast-Forward if it matters – eran meiri Oct 22 '18 at 11:27
  • @eranmeiri: Ah, I managed to scramble the order of the items read. Let me fix the sample code (note - it's a one line change, the `read` arguments should be `oldhash newhash ref`, not `ref oldhash newhash`). – torek Oct 22 '18 at 14:23
  • HI Again , I have an question again since it suddenly stopped working. lets say that my merged branch only includes 1 commit. won't the parents just count 1 commit and hence goes to the ordinary commits section in the case part instead of * section? – eran meiri Nov 08 '18 at 12:44
  • I understood that if the merged master did not have any commits to it then the parent is only one since it is a fast forward commit and then it goes to 1 section. 2 parents will only happen if two branches have been changed. how can I resolve this ? – eran meiri Nov 08 '18 at 12:58
  • That's correct: the post-receive hook (or a pre-receiver or update hook) is told "this reference (branch name, tag name, whatever) moved, it used to identify commit , now it identifies or will identify ". You can only *infer* what someone wants or did from this, and allow or reject it (pre-receive and update hooks) or report on it (post-update hook). What to do, and how to do it, is up to you. If you got a fast-forward action, rather than a merge action, you can detect that (the old hash is an ancestor of the new hash *and* the hashes changed *and* there's no merge)... – torek Nov 08 '18 at 18:06
  • ... and then you choose something to do about it. Or, if it's a merge action, the old hash is an ancestor of the new hash *and* the hashes change *and* the sequence ends in a merge, and then you choose to do something about *that*. Or, you can detect a forced-push rollback (the new hash is an ancestor of the old hash, no commits were added, some were removed), and do something there, and so on. But you have to write all of this yourself. – torek Nov 08 '18 at 18:07