Taking corrective action with git’s commit hooks

March 08, 2011

Like many teams we use a Subversion pre-commit hook to perform simple validation on code we’re about to commit. When we commit to our front-end repository, for example, the hook verifies that lines don’t have trailing whitespace and that all text files end with a newline character. Unfortunately, when a SVN pre-commit hook fails, the commit fails. SVN hooks run on the server and are forbidden from altering commit transactions. As a result they are powerless to correct any errors they find.

In contrast git’s pre-commit hooks run locally. They can actually take corrective action on files in your index, modifying them before the final commit. Many of us here at Wealthfront use git locally and rely on its SVN integration to interact with Subversion. Tired of failed commits, we adapted our SVN pre-commit hook to fix errors instead of of just reporting them and installed it as a local git pre-commit hook.

Below you’ll find the script in its entirety. It removes trailing whitespace and appends newline characters when necessary, correcting these errors silently and not troubling us with pesky warnings. You’ll also see we do more than correct whitespace. As a guideline we require all PNG images to be pngcrushed before they are committed. Our hook script looks for any new or modified pngs, runs pngcrush against them, and re-adds them to the index before commit. We’ll examine a few key lines from the script below.

#Files (not deleted) in the index
files=$(git diff-index --name-status --cached HEAD | grep -v ^D | cut -c3-)
if [ "$files" != "" ]
then
  for f in $files
  do
    if [[ "$f" =~ [.]png$ ]]
    then
      if which pngcrush.sh &> /dev/null
      then
        pngcrush.sh $f
        git add $f
      else
        echo "**"
        echo "*  Warning: Can't find pngcrush.sh, is it installed and on your path?"
        echo "*  Remember, all PNGs must be crushed before committing!"
        echo "**"
      fi
    fi
    # Only examine known text files
    if [[ "$f" =~ [.](conf|css|erb|html|js|json|log|properties|rb|ru|txt|xml|yml)$ ]]
    then
      # Add a linebreak to the file if it doesn't have one
      if [ "$(tail -c1 $f)" != '' ]
      then
        echo >> $f
        git add $f
      fi

      # Remove trailing whitespace if it exists
      if grep -q "[[:blank:]]$" $f
      then
        sed -i "" -e $'s/[ t]*$//g' $f
        git add $f
      fi
    fi
  done
fi

git diff-index --cached HEAD will provide you with a list of files currently in the index (staged for commit) that differ from HEAD. We add the --name-status flag to get the output in a format that’s easier to work with. We grep out lines that start with D since these are deleted files, and cut to just the file path. The end result is a list of files in the index that are being added or have been modified:

files=$(git diff-index --name-status --cached HEAD | grep -v ^D | cut -c3-)

For each file ending with a .png extension we run pngcrush if the executable is present and echo a warning if it is not. “pngcrush.sh” is a short shell script that wraps up some standard pngcrush configuration options.

Next we examine known text files for the whitespace errors discussed above. The following will check if the last character of a file is a newline character and add one if necessary. Command line substitution actually strips newlines, which is why we have to check against an empty string instead of n.

if [ "$(tail -c1 $f)" != '' ]thenecho >> $fgit add $ffi

Finally grep -q '[[:blank:]]$ will test for lines that end in a whitespace character. If any are found we remove the whitespace with sed and re-add the file to the index.

if grep -q "[[:blank:]]$" $fthensed -i "" -e $'s/[ t]*$//g' $fgit add $ffi

Note that we always re-add the file to the index after modifying it, otherwise the script’s changes won’t be part of the commit!

There’s a lot of power in git’s commit hooks. Today our SVN-using colleagues still face whitespace warnings, while the git users of Wealthfront march forward in automated glory.