Setting up pre-commit hook for iOS


Many of us already write unit tests and run continuous integration servers, we can also leverage great tools like Danger to easily add some checks to the pull requests.

If we’d like to prevent some common mistakes from appearing in the repo in the first place, we can use pre-commit hook.

Edit: My friend Sami Samhuri has improved my shell script, the article has been updated to reflect that.

Keeping hook in sync across team

Since most apps are made in teams, we’d like to have the git hook in the repository, which isn’t how git works, how we can make it happen? symlinks

Most projects I work on have some kind of bootstrap script, either for loading Carthage or doing some other preparation like bundling ruby gems.

Here is a simple example of the bootstrap script that will allow you to have git hook inside your repository and make it easy for the whole team to be in sync.

#!/usr/bin/env bash
# Usage: scripts/bootstrap

set -eu

ln -s ../../scripts/pre-commit.sh .git/hooks/pre-commit
  1. It exits shell if there is an error and writes error messages to standard error if any variable wasn’t set.
  2. Creates a symlink between internal git pre-commit hook file and our repository.

Assumes that both pre-commit.sh and bootstrap files under Scripts folder in your repo.

Things we want to prevent

Misplaced Views

Have you ever committed misplaced views to your repository, just to fix it in some later commits?

It’s very easy with the overly eager Xcode and multiple monitors (retina vs non-retina issues…) to suddenly get things misplaced.

I surely did.

Finding misplaced views is a simple grep scan on the content of interface builder files:

  • pattern: misplaced="YES"
  • files: *Specs.swift *.storyboard

Focused tests

When using libraries like Kiwi or Quick we have access to focused tests, which are great for development as they increase speed.

They should never be committed or we risk sneaking in a change that will disable all other tests and can hide some serious issues.

We need to find occurences of fdescribe / fit / fcontext and similar, but only under our Test files:

  • pattern: (fdescribe|fit|fcontext|xdescribe|xit|xcontext)
  • files: *Specs.swift

Putting it together

We only want to verify whether the staged changes contain any of the above, and not just run it on all files as this get’s really annoying while developing.

Fortunately, we can use git diff-index -p -M --cached HEAD along with matching additions using grep '^+'.

Final pre-commit.sh file:

#!/usr/bin/env bash
set -eu

failed=0

test_pattern='\b(fdescribe|fit|fcontext|xdescribe|xit|xcontext)\b'
if git diff-index -p -M --cached HEAD -- '*Tests.swift' '*Specs.swift' | grep '^+' | egrep "$test_pattern" >/dev/null 2>&1
then
  echo "COMMIT REJECTED for fdescribe/fit/fcontext/xdescribe/xit/xcontext." >&2
  echo "Remove focused and disabled tests before committing." >&2
  echo '----' >&2
  git grep -E "$test_pattern" '*Tests.swift' '*Specs.swift'  >&2
  echo '----' >&2
  failed=1
fi

misplaced_pattern='misplaced="YES"'

if git diff-index -p -M --cached HEAD -- '*.xib' '*.storyboard' | grep '^+' | egrep "$misplaced_pattern" >/dev/null 2>&1
then
  echo "COMMIT REJECTED for misplaced views. Correct them before committing." >&2
  echo '----' >&2
  git grep -E "$misplaced_pattern" '*.xib' '*.storyboard' >&2
  echo '----' >&2
  failed=1
fi

exit $failed

This works work in both console and in macOS git clients.

Conclusion

Pre-commit hooks offer us a very simple way to prevent common mistakes, and with the setup I described:

  • One liner setup for anyone on the team
  • Synced across the whole team
  • Changes are tracked by the git and visible in PR’s
  • The way the script is written, you can run it as build phase if you’d like (although I don’t)

We use this approach at The New York Times and it has been really helpful.

If you have other uses for the pre-commit hook, I’d love to hear your ideas!

Support my writing: PayPal Tip Jar or become a Patron