A Behind-the-Scenes Look at How Pre-Commit Works
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:
- Clone the repository (the
repo
key) at the provided revision (therev
key) - Install the hook(s) one-by-one (each of the
id
keys underneath thehooks
key in the.pre-commit-config.yaml
file):- Look up the
id
in the cloned repository's.pre-commit-hooks.yaml
file and grab the language the hook is written (thelanguage
key in the.pre-commit-hooks.yaml
file) - Use the specified language to run the appropriate installation commands (supported languages only)
- Look up the
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 Pre-Commit Hook Creation Guide 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.