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
- It exits shell if there is an error and writes error messages to standard error if any variable wasn’t set.
- 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!