Imagine you've just put the finishing touches on a feature you've been hard at work on, you do one final
git push, open up a pull request, and take a well-deserved break while you wait for all of the reviews to come in.
When they finally arrive, you find that the reviewer has spent most of their time commenting about minor style and syntactic issues rather than saying anything about the implementation itself.
Seeing as code reviews are meant to focus on the architectural implications and limitations of an approach, discussing minor syntactic issues at this point isn't a good use of anyone's time.
Wouldn't it be great if we were able to catch these minor issues before the code review process began? What if we could catch all of these issues before we even committed?
While static analysis tools like SwiftLint can help us detect some of these issues, let's see how Git hooks can be used to address the rest.
Static program analysis is the analysis of computer software performed by examining code without executing it.
Git hooks Explained
Git hooks are scripts that run automatically every time a particular event occurs in a Git repository. They let you customize Git's internal behavior and trigger customizable actions at key points in the development life cycle.
Hooks can be further divided into client-side hooks and server-side hooks, where client-side hooks will be triggered by operations like committing and merging, whereas server-side hooks are triggered by events like receiving a pushed commit.
Whenever we initialize a new Git repository, Git will provide some default hooks for us to use at the
These hooks are currently inactive due to the
.sample file extension.
To activate them, we can simply remove this suffix and add our custom logic directly to the script or we can create a new script by ensuring that the script's filename matches one of the supported Git hook types.
We'll see how to do this in the next section.
Since Git hooks are completely language-independent, they can be written in whichever language you and your team prefer. This means you can write your Git hooks in Swift!
Diving Into Pre-Commit
For now, we'll focus on the
As the name suggests, this hook runs every time you attempt to make a commit. It effectively serves as the gatekeeper between your local changes and the rest of the codebase.
For example, the following script shows how we can use a
pre-commit hook to ensure that all committed files contain only ASCII characters in their filenames.
You don't need to focus too much on understanding the code here - it's just to demonstrate how easy it is to create custom
pre-commit hooks to monitor the topics relevant to your project and team.
As another example, this script will help us determine whether we are introducing any ambiguous constraints into any of our
.storyboard files, and if so, will prevent the commit from happening.
By leveraging these hooks, we can verify that all of our staged files abide by whatever custom rules we want to enforce. It's also worth mentioning that all of these checks happen before Git prompts you for a commit message or even creates a commit object.
Taking this one step further, our hypothetical
pre-commit script could be extended to check for the following conditions:
- Ensure we are not committing any large files.
- Verify that any test
.jsonfile in our project has the correct syntax.
- Check that there are no outstanding merge conflicts.
- Ensure we are not committing any private keys into the repository.
We'll see shortly how we can add support for all of these checks without writing any additional code.
As these checks are likely to run regularly, it's important that they're all meaningful, deterministic, and execute quickly - I'll share some thoughts on best practices in a moment.
Although this system of checks and balances is great, there is one limitation we still need to address - making this work with a larger development team.
Git Hooks & Development Teams
Any file that we save to
./git/hooks is not checked in with our Git repository which means it won't be available to those that clone the repo. Obviously, this isn't an ideal solution for teams, as other team members won't have the same set of checks you do and there would be no way of enforcing their usage.
One solution to this problem would be to copy and paste scripts from one machine to the next, but this is clearly inelegant and complicates managing different versions of these hooks.
Fortunately, we can address all of these limitations through the use of the Python pre-commit utility.
By using this tool, you can establish standards across the team and ensure that all code checked into the codebase is validated against a standardized set of tests.
To install pre-commit, simply run:
brew install pre-commit
Next, create a a file named
.pre-commit-config.yaml in your project's root directory.
Here, we configure the different checks that we want to perform before allowing a successful commit to take place.
Let's start with a simple implementation:
The full set of configuration options are available here.
As you can see in this configuration, all we've done is point to a repository that contains the
trailing-whitespace checks we want to use.
We get all of this functionality for free without having to write any code ourselves!
One of the major advantages of this tool is the enormous collection of open-source
pre-commit checks that we can now easily incorporate into our project.
The final step is to run
pre-commit install which will complete the setup and installation process.
Now, all of the checks defined in our
.yaml will run on every
You may find the following checks useful for your project:
check-json(attempts to load all json files to verify syntax)
check-merge-conflict(check for files that contain merge conflict strings.)
detect-private-key(checks for the existence of private keys)
swiftformat(check Swift files for formatting issues with SwiftFormat)
swiftlint(check Swift files for issues with SwiftLint)
More information available here.
On a more specific iOS note, I've used
pre-commit to check for the following situations:
#includeare in alphabetical order.
- All localized string keys match the same naming style (i.e.
- No duplicated localized strings keys (a common occurrence when resolving merge conflicts)
I've been working on a Git hook that will check for dangling
IBOutletreferences. In other words, it ensures that every
IBOutletdeclared in code is attached to a view in Interface Builder and vice-versa.
It's easy to accidentally remove
IBActionreferences in your project and introduce potential runtime crashes this way.
I'll open-source this utility soon after a bit more testing.
Whenever you add new checks to your
pre-commit configuration, be sure to run them against all of the files in your project, not just those staged with changes:
pre-commit run --all-files
This ensures that you are resolving existing issues before enforcing a new rule.
Hopefully this post has given you some ideas about how you can leverage
pre-commit to improve your team's development workflow and code review process.
Once set up, not only does
pre-commit help establish a common set of checks and balances for your team, but it makes it easy to easily integrate checks made by other developers. This will allow you to easily extend your configuration's functionality without having to write any code yourself. 🚀 Moreover, using
pre-commit means you won't have to worry about dependencies, testing, different hook versions, etc. - it's all taken care of for you.
The next time you create a pull request, the comments you receive should now focus on the implications of your code rather than on formatting errors, trailing whitespace issues, or an endless list of nit-picky suggestions.
Join the mailing list below to be notified when I release new posts.
Do you have an iOS Interview coming up?
Check out my book Ace The iOS Interview!