Pre-commit Hook

The project on which I’m working has acquired some young people recently.

For a couple of them it’s even their first working experience so I’m really trying to transmit them the importance of conventions and an uniform coding style.

It’s usual (and perfectly normal) to find some coding style mistakes in their commits. So, to help us happily code all together, I created a small pre-commit hook to put in our repositories.

Right now it’s just a simple no-tabs check, but with some small changes it could check for use of deprecated functions or some forgotten import pdb; pdb.set_trace() :)

# Files extensions in hooks.notabs are separated by "|"
# Eg:
#   git config hooks.notabs py|js

exec 1>&2
notabs=$(git config hooks.notabs)
toplevel=$(echo "`git rev-parse --show-toplevel`/" | sed -e 's/[\/&]/\\&/g')
if [ -n "$notabs" ] &&
    git diff --cached -- not_a_file `git diff --cached --name-only |
      egrep '\.('$notabs')$' |
      sed 's/^/'$toplevel'/g' | xargs` |
      egrep -qn $'^\+\.*\t'
then
    echo "Error: Attempt to add TABS"
    echo
    echo "Do you really want to break our beautiful coding conventions? :("
    echo
    echo "Remove all tabs characters and stage again your changes."
    echo
    echo "To override this hook use:"
    echo
    echo "  git commit --no-verify"
    echo
    exit 1
fi

To have it working:

copy this script into repository_dir/.git/hooks/pre-commit. Add some file extensions to hooks.notabs variable (these must be separated by |). Done!

Let’s look at each parts in rigorous order of execution

  • notabs=$(git config hooks.notabs): retrieves from local config the files extensions to check

  • git diff --cached --name-only: lists names of files that have staged changes

  • egrep '\.('$notabs')$': removes files having different extensions from the ones in notabs

  • sed 's/^/'$toplevel'/g': prepend the absoulute path to each filename

  • xargs: creates a list of arguments using previous file names list

  • git diff --cached -- not_a_file: returns staged changes for files coming from “xargs”. Note that “not_a_file” is an unexistant file, specified in case that the output of xargs is empty. In fact if no files are specified the “git diff —cached” command would return staged changes for every file.

  • egrep -qn '^\+\.*\t': searches for all the lines starting with ”+” and containing a tab

Note that this pre-commit hook can be easily bypassed using.

git commit --no-verify

This reflects exactly the same thought I want communicate to my collegues. Everyone should struggle to pass the pre-commit hook checks, but no one should be forced to.

References:

  1. Customizing-Git-Git-Hooks

— 13/08/2013