Use git stash in your pre-commit hook

Using Git pre-commit hook it helps you to test your code, but be aware that it may be wrong if it doesn't stash your changes.

A pre-commit hook is a script that runs before every commit, even before you type in a commit message, and determines whether or not the commit should be accepted. It is used to check the changes that you're committing, to make sure tests runs, to remove pdb code, or whatever you need to inspect in the code. A non-zero exit status of pre-commit script will abort the commit.

You can find a lot of pre-commit scripts on the web but be aware that they may be wrong. The problem is that they are testing the files in the working tree instead of the ones that are in the staging area, which means that you may committing files that actually don't pass the checks.

Let see an example, first create a test repository and do the first commit:

$ cd /tmp
$ git init testrepo
$ cd testrepo/
$ touch program.py
$ git add program.py
$ git commit -m "Initial commit"

It is usually a good idea to have a README.txt file, so we can use a simple pre-commit hook that abort the commit if the README.txt file is missing or empty.

Copy the following script in the ~/.git_template/hooks directory.

#!/bin/sh

# Abort commit if README.txt is missing or empty
# Broken version, it check the working tree instead of the staging area

[ -s README.txt ]
RESULT=$?
if [ $RESULT ]; then
echo "Error: README.txt file is missing or empty."
fi
exit $RESULT

Add a README.txt to your repository and try to commit your changes:

$ touch README.txt 
$ git add README.txt
$ git commit
Error: README.txt file is missing or empty.
$

As you can see the pre-commit script abort the commit because the file is empty, it doesn’t even open your favourite editor to type in the message, so add some content and commit again.

$ echo "This is a repository for testing thing out." >> README.txt 
$ git commit
... your editor is open now, waiting for the commit message...
... But.. Wait a moment. This is wrong !

We forgot to do git add README.txt which add the file to the staging area, but the pre-commit script didn't stop us, so we were going to leave an empty file in the repository.

Abort the commit with an empty message and check the status of the repository.

$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: README.txt <--- THIS FILE IS EMPTY !
#
# Changes not staged for commit:
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
# modified: README.txt <--- THIS FILE WAS TESTED BY pre-commit SCRIPT,
# IT PASS THE CHECKS BUT IT'S NOT STAGED FOR COMMIT !
#
$

As you can see, the problem is that pre-commit script was checking the README.txt file in our working tree but those changes are not in the staging area yet.

Solution to this problem is to stash all changes that are not part of the staging area before running our checks and then popping them back.

This is the correct pre-commit script:

#!/bin/sh

# Abort commit if README.txt is missing or empty

# Stash any changes to the working tree that are not going to be committed
git stash -q --keep-index
[ -s README.txt ]
RESULT=$?
if [ $RESULT ]; then
    echo "Error: README.txt file is missing or empty."
fi
# Unstash changes to the working tree that we had stashed
git stash pop -q
exit $RESULT

Now it's working has expected.

$ git commit
Error: README.txt file is missing or empty.
$

 

Danilo