How Pre-Commit Works

4 min read
Featured

Featured Article

This article was featured in PyCoder's Weekly Issue #644.


Mission control stations from the Apollo moon program at Cape Canaveral, FL, USA.

Photo by Sieuwert Otterloo on Unsplash

So, you've just set up pre-commit hooks on your repository using pre-commit, but do you know what actually happened when you ran pre-commit install or why you had to run it in the first place? How does pre-commit actually work with Git? In this article, I will take you behind the scenes of how your pre-commit setup works.


A brief overview of the Git hooks system

Git supports hooks, which are executables that it can run on your behalf when certain actions are taken on a repository. Take a look at the .git/hooks/ directory of any of your projects, and you will see something like this:

.git/hooks
├── commit-msg.sample
├── pre-commit.sample
├── pre-merge-commit.sample
├── pre-push.sample
├── pre-rebase.sample
├── prepare-commit-msg.sample
├── push-to-checkout.sample
└── [...]

By default, Git provides a bunch examples of how you could plug into the hook system. The .sample suffix prevents them from being run, but remove that, and you have an executable that Git will trigger automatically on your behalf:

cat .git/hooks/pre-commit.sample
#!/bin/sh
#
# An example hook script to verify what is about to be committed.
# Called by "git commit" with no arguments.  The hook should
# exit with non-zero status after issuing an appropriate message if
# it wants to stop the commit.
#
# To enable this hook, rename this file to "pre-commit".

if git rev-parse --verify HEAD >/dev/null 2>&1
then
	against=HEAD
else
	# Initial commit: diff against an empty tree object
	against=$(git hash-object -t tree /dev/null)
fi
...

One of the most useful Git hooks is the pre-commit hook, which, as the name implies, is run right before the commit actually happens, making it possible to automatically run checks on the proposed code (and potentially, reject the changes). Regardless of how many checks you want to run, there can only be one .git/hooks/pre-commit executable per repository, which can be a bit cumbersome, especially when you have multiple checks. This is where tools like pre-commit really shine.

Using pre-commit as a Git pre-commit hook

The pre-commit tool allows for modular, composable, and reusable hooks, which can come from a variety of providers and even be written in different languages. For this flexibility to be possible, pre-commit has to stand in as your Git pre-commit hook. This is why we run pre-commit install – it installs the pre-commit tool's own executable as the repository's Git pre-commit hook (take a look at the output of pre-commit install):

pre-commit install
pre-commit installed at .git/hooks/pre-commit

When you run git commit, Git invokes the .git/hooks/pre-commit executable on the files you staged, which in turn calls pre-commit to run the hooks specified in the .pre-commit-config.yaml file:

cat .git/hooks/pre-commit
#!/usr/bin/env bash
# File generated by pre-commit: https://pre-commit.com
# ID: 138fd403232d2ddd5efb44317e38bf03

# start templated
INSTALL_PYTHON=/path/to/bin/python
ARGS=(hook-impl --config=.pre-commit-config.yaml --hook-type=pre-commit)
# end templated

HERE="$(cd "$(dirname "$0")" && pwd)"
ARGS+=(--hook-dir "$HERE" -- "$@")

if [ -x "$INSTALL_PYTHON" ]; then
  exec "$INSTALL_PYTHON" -mpre_commit "${ARGS[@]}"
elif command -v pre-commit > /dev/null; then
  exec pre-commit "${ARGS[@]}"
else
  echo '`pre-commit` not found. Did you [...] activate your virtualenv?' 1>&2
  exit 1
fi

This is why every collaborator on your project must run pre-commit install locally: pre-commit needs to install its executable at .git/hooks/pre-commit, but that file (and everything in the .git/hooks/ directory) is not part of Git's version control, and therefore, only exists locally on each collaborator's machine. The .pre-commit-config.yaml configuration, on the other hand, is tracked in version control, so collaborators can easily use the same configuration.


Finding and installing pre-commit hooks

When you first commit after installing pre-commit as your Git pre-commit hook, pre-commit will need to install each of your hooks in order to use them. The hooks will be processed in the order they are defined in your .pre-commit-config.yaml file as follows:

  1. Clone the repository (the repo key) at the provided revision (the rev key)
  2. Install the hook(s) one-by-one (each of the id keys underneath the hooks key in the .pre-commit-config.yaml file):
    1. Look up the id in the cloned repository's .pre-commit-hooks.yaml file and grab the language the hook is written (the language key in the .pre-commit-hooks.yaml file)
    2. Use the specified language to run the appropriate installation commands (supported languages only)

As you can see below, pre-commit puts each hook in a separate environment and will reuse the environment until you change the configuration:

git commit -m "Add numpydoc-validation hook"
[INFO] Initializing environment for [...]github.com/numpy/numpydoc.
[INFO] Installing environment for [...]github.com/numpy/numpydoc.
[INFO] Once installed this environment will be reused.
[INFO] This may take a few minutes...

Note that the .pre-commit-hooks.yaml file must be present in a repository for it to be used in your .pre-commit-config.yaml file – without it, pre-commit doesn't have the information it needs to install or even find the hooks.

Running pre-commit hooks

Once pre-commit has installed each of the hooks, it uses the entry value in the .pre-commit-hooks.yaml file to invoke the hook. Upon completion, the hook will return an exit code, which pre-commit uses to determine whether the checks passed or not. By default, pre-commit will reproduce any output generated by the hook upon failure; however, running in verbose mode (--verbose) will always show any hook output.

Looking to make your own hook? Check out my How to Create a Pre-Commit Hook article.


In this article, we took a peek behind the curtain to understand how pre-commit interfaces with the Git hooks system to provide an easily-configurable hook setup. We also discussed how pre-commit finds, installs, and uses the hooks you specify in your .pre-commit-config.yaml file at a high level. Hopefully, you have a greater appreciation for why pre-commit is such a popular tool.

Never miss a post: sign up for my newsletter.



You may also like

Cover image for How to Create a Pre-Commit Hook

Pre-commit hooks are a great way to help maintain code quality. However, some of your code quality standards may be specific to your project, and therefore, not covered by existing code linting and formatting tools. In this article, I will show you how to incorporate custom checks into your pre-commit setup.

Cover image for Common Pre-Commit Errors and How to Solve Them

Having issues with your pre-commit setup? In this troubleshooting guide, I've collected the most common errors pre-commit users face and provided explanations and guidance for fixing them.