Git Pre-Commit Hooks and Specta’s Focused Examples

On my current iOS project, we’re using Specta as the testing framework. One of the really nice features that Specta provides is the ability to run specific specs/tests by prepending an f to an it or describe (or the other spec aliases). According to the Specta docs, “When specs are focused, all unfocused specs are skipped.”

When you’re trying to figure out why a test is failing, the ability to run a specific spec is invaluable. I love this feature, and I use it all the time.

The Problem

The only downside to the focused spec is that Xcode and AppCode both show all of the un-focused specs as having passed (as opposed to showing them disabled in some way). So if you have a focused spec, run all of the tests, and switch over to check your email—you’ll come back to see a green test suite that looks like it’s ready to be committed.

And if you do commit the suite with a focused spec, the CI server will pick it up and so will anyone else on the team when they pull your changes. Not good.

To prevent this from happening, my teammate John Ruble and I put together a Git pre-commit hook that would check for these focused specs and prevent them from being committed.

Git Hooks

The Git documentation states that

Like many other Version Control Systems, Git has a way to fire off custom scripts when certain important actions occur.

We wanted something that would prevent a commit from happening at all. Again, from the git documentation:

The pre-commit hook is run first, before you even type in a commit message. It’s used to inspect the snapshot that’s about to be committed, to see if you’ve forgotten something, to make sure tests run, or to examine whatever you need to inspect in the code.

When a new git repository is created, git init populates the .git/hooks directory with a bunch of sample scripts. We used the pre-commit.sample as a starting point, renaming it to pre-commit so that git would automatically run it (the file needs to be executable, so double check if yours doesn’t seem to be running).

The Script

The following shell script will look for fit and fdescribe in any files that have been staged and reside in one of our *Tests directories.

#!/bin/sh

if git rev-parse --verify HEAD > /dev/null 2>&1
then
  against=HEAD
else
  # Initial commit: diff against an empty tree object
  against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi

alias testfiles='find . -depth 1 -type d -name "*Tests" | xargs git diff --cached --name-only $against'

if [[ -n $(testfiles) ]]; then
  if testfiles | xargs grep --with-filename -n -E --regexp="fit\(" --regexp="fdescribe\("; then
    echo "You forgot to un-F an it!"
    exit 1
  fi
fi

The first section of the script comes from the git generated boilerplate. I think it’s just to handle the case when you’ve got a detached HEAD.

The rest of the script uses find to only look in directories that end with “Tests”, uses git diff to include only files that have been staged for commit (--cached), and uses grep to look for files that include “fit” or “fdescribe”.

If there are any matches, they will be printed out in the terminal and the git commit will abort.

End result—no more accidental commits with focused specs!

Conclusion

Adding a pre-commit hook to your local git repository is quick and easy. It doesn’t have to be a sh/bash script either. From the git documentation:

… but any properly named executable scripts will work fine – you can write them in Ruby or Python or what have you.

I plan to start making heavier use of the hooks Git provides. And the next time I’m on a web project I’m not going to be the one accidentally checking in the debugger statements in the JavaScript code.