Easier Code Review With Pre-Commit Hooks

In this article, we'll see how we can use Git hooks to establish a common set of checks and balances for our team and simplify the code review process.

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 .git/hooks location:

Sample Hooks

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 pre-commit hook.

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.

Sample pre-commit Hook

As another example, this script will help us determine whether we are introducing any ambiguous constraints into any of our .xib / .storyboard files, and if so, will prevent the commit from happening.

See source code

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:

  1. Ensure we are not committing any large files.
  2. Verify that any test .json file in our project has the correct syntax.
  3. Check that there are no outstanding merge conflicts.
  4. 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.

touch .pre-commit-config.yaml

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:

-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v2.3.0
    -   id: end-of-file-fixer
    -   id: trailing-whitespace
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 end-of-file-fixer and 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 git commit.

You may find the following checks useful for your project:

  • check-added-large-files
  • 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:

  • All import , #import, and #include are in alphabetical order.
  • All localized string keys match the same naming style (i.e. confirm_logout_action vs. confirm-logout-action)
  • 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 IBOutlet references. In other words, it ensures that every IBOutlet declared in code is attached to a view in Interface Builder and vice-versa.

It's easy to accidentally remove IBOutlet and IBAction references 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.

Wrapping Up

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.

If you're interested in more articles about iOS Development & Swift, check out my YouTube channel or follow me on Twitter.

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!

Subscribe to Digital Bunker

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
[email protected]