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):
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
.)
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.