`git diff` does not work when run from a git pre-commit hook

I have a git pre-commit hook that does some style checking on any modified files before committing.

The implementation is irrelevant, but it starts by calling git diff. Here's what i have in (repo)/.git/hooks/pre-commit.


echo "=== Running script..."
git diff
echo "=== Done running script..."

# Other stuf
# ....

# Always exit with 1 so pre-commit hook always fails.
# Useful for testing
exit 1

When I actually try committing something, the pre-commit hook correctly fires, but the git diff command doesn't output anything (there are definitely modified files)

> git commit --all -m "foo"
=== Running script...
=== Done running script...

However if I run the pre-commit hook script directly/manually, it does work

> ./.git/hooks/pre-commit
=== Running script...
(... outputs git diff ...)
=== Done running script...

What's different about git calling the hook versus me manually calling it? It runs as the same user either way (my username)

I've also tried suggestions from this thread, but unset GIT_DIR, --git-dir=, and work-tree= didn't fix anything.


2 answers

  • answered 2017-08-16 19:34 ishegg

    You need to use git diff --cached because the changes are already staged.

  • answered 2017-08-16 19:34 torek

    As ishegg said, you will need git diff --cached here, but that's not necessarily the whole story.

    There are two traps to be wary of here. The first is what git diff does: it compares two trees (or tree-like things). The second has to do with the very phrase the index.

    The index, and selecting trees for git diff

    Quite often, the two trees you diff are the trees associated with two specific commits:

    git diff <hash1> <hash2>

    (or the same with <hash1>..<hash2>, which—unlike most Git commands—doesn't treat the two hashes the way the two-dot .. operation is described in the SPECIFYING RANGES section of the gitrevisions documentation).

    It's also pretty common to have one of the two trees be your work-tree, which is what happens when you run:

    git diff HEAD

    for instance: this compares the commit named by HEAD—i.e., the current branch-tip commit—to the current work-tree.


    git diff --cached

    tells Git to compare the HEAD commit to the tree represented by your index. Git's index, also called the staging area or the cache, is where Git builds up the next commit to make. It's why you must run git add before committing: the git add command copies files from the work-tree to the index.


    git diff

    with no arguments at all chooses the index as the first tree, and the work-tree as the second tree. Once everything has been copied from the work-tree into the index, this particular diff will be empty, which is what you are seeing here.

    The index vs an index

    The second trap lies in the fact that you are using git commit --all.

    While Git talks about the index, and there is in fact one particular, distinguished index to go with each work-tree, Git actually allows for more than one index, using exactly one at a time.

    When you use git commit --all or pass file names to git commit, Git makes a temporary index that it uses during the pre-committing process. To indicate that there is a temporary index in play, Git sets the environment variable GIT_INDEX_FILE. This temporary index gets used instead of the ordinary index, up until either the commit is allowed and committed, or until the commit is rejected.

    If the commit is rejected, Git just removes the temporary index, and everything goes back to the way it was before. If things were not git add-ed, they are still not git add-ed.

    If the commit is accepted, the temporary index becomes the "real" or "main" index, more or less. Here things can get complicated, because you can stage some items in the real index, run git commit --all or git commit --include <paths> or git commit --only <paths>, and if the commit succeeds, the difference between the initial (main or real) index and the temporary index matters.

    If you plan to do special tricks with files in the work-tree and/or their copies in the index, you may want to simply reject any attempt to work with a temporary index, so as to avoid these complications. However, that will preclude the use of --all.