Search code examples
node.jsgitshellgithookspost-checkout-hook

optional githook behaving as non-optional


I am attempting to make use of this gist in my workflow as post-merge and post-checkout git hooks.

#!/usr/bin/env bash
# MIT © Sindre Sorhus - sindresorhus.com

# git hook to run a command after `git pull` if a specified file was changed
# Run `chmod +x post-merge` to make it executable then put it into `.git/hooks/`.

changed_files="$(git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD)"

check_run() {
    echo "$changed_files" | grep --quiet "$1" && eval "$2"
}

# Example usage
# In this example it's used to run `npm install` if package.json changed
check_run package.json "npm install"

This claims to only run npm install if the package.json file is changed.

However on all the machines I have tried this on. The npm install command runs regardless of whether package.json has been changed or not.

To test this I have been creating a new branch at my current commit and then checking it out, thus triggering the post-checkout git hook. I would not expect npm install to run because the package.json is unchanged.

Visual Proof (note the npm warning text):

enter image description here


Solution

  • TL;DR

    Use a different post-checkout hook, that uses $1 instead of ORIG_HEAD. (Or, check the number of arguments to decide whether you are being invoked as the post-checkout or post-merge hook, to get the same effect. Or, if you know that reflogs are always enabled, use HEAD@{1} to get the previous value of HEAD.)

    Discussion

    Using ORIG_HEAD in a post-merge hook makes sense, because git merge sets ORIG_HEAD to the commit that was current before the merge. (If the merge was a true merge, rather than a fast-forward, the commit identified by MERGE_HEAD and the commit identified by HEAD^1 are necessarily identical. If the merge was a fast-forward, however, only MERGE_HEAD and the reflog will be able to locate the previous commit hash that was stored in HEAD before the merge.)

    Using ORIG_HEAD in a post-checkout hook, however, is blatantly wrong, because git checkout does not set ORIG_HEAD. This means that if ORIG_HEAD even exists at all, it effectively points to some random commit. (Of course, it actually resolves to whatever commit was left in it by whatever command last updated it: git merge, git rebase, or any other command that writes to ORIG_HEAD. But the point here is that it does not have any relationship to the commit that was current before the checkout.) A post-checkout hook:

    is given three parameters: the ref of the previous HEAD, the ref of the new HEAD (which may or may not have changed), and a flag indicating whether the checkout was a branch checkout (changing branches, flag=1) or a file checkout (retrieving a file from the index, flag=0). This hook cannot affect the outcome of git checkout.

    (That last sentence is not quite right. Although the post-checkout hook cannot stop checkout from having updated the index and work-tree, it can overwrite various work-tree or index contents, and if it produces a failure exit status, it causes git checkout itself to also produce a failure exit status.)

    What this all means is that you need to take a different action in a post-checkout hook: use $1, the first parameter, to get the hash ID of the previous HEAD. Note that in exotic cases,1 the post-checkout hook is run on the initial git clone, so $1 can be the null-ref. (I'm now curious as to what it is when you use git checkout --orphan and then don't create the new branch, as well. It seems likely that $1 will be the null-ref here too.)


    1The only way to get a post-checkout hook to run on git clone is to have git clone install the post-checkout hook. This is normally impossible, but can be done by pointing your Git to your own template directories that have actual hooks instead of just sample hooks.