Ian Fisher ― June 21, 2020 ― history
I use git pre-commit hooks in all my projects to ensure that my code meets some basic standards before I commit it. A pre-commit hook is a script at a special location (
.git/hooks/pre-commit) that git will run whenever you try to make a commit. If the script exits with an error code, then git will abort the commit.
#!/bin/bash set -e black --check . flake8 . python3 tests.py
Simple as it is, this script has a couple of problems:
It checks every Python file in the directory, including files that aren't tracked by git at all (e.g., files inside a virtual environment), files that haven't been modified since the last commit, and files that have been modified but have not been staged. This is highly inefficient, and doesn't allow for any files to be exempt from any of the checks.
It exits as soon as any of the checks fail. You may want to run all the checks to see all the errors.
We can at least exclude non-Python files and files in the virtual environment (assuming that it is at
#!/bin/bash set -e PY=$(find . -path ./.venv -prune -o -name "*.py" -print) black --check $PY flake8 $PY pytest
But we haven't fully solved the first problem, nor addressed the second problem at all, and our solution would quickly become unwieldy if we wanted checks for multiple languages.1
Because these issues plagued me whenever I started a new project and discouraged me from writing more comprehensive pre-commit checks, I created my own tool,
precommit, to manage my pre-commit hooks.
Above all, precommit is designed to be lightweight. To set up a pre-commit hook, you run
precommit init anywhere in your git repository, which creates a configuration file called
precommit.py in the repository's root and symlinks it to git's hooks directory. Then, the check will be triggered automatically whenever you run
git commit. You can also trigger it manually with
Here's what it looks like:2
The default configuration comes with a number of useful checks out of the box:3
Most of the checks are enabled by default. There is no overhead for enabling checks in a language your project does not use, because checks are only run when a matching file is staged for the commit.
Many checks can fix problems as well as identify them. For example,
PythonFormat can use
black to automatically re-format your code, and
PythonImportOrder can use
isort to sort imports. To apply all available fixes, run
precommit fix. You can pass
precommit fix to operate on both unstaged and staged changes.
If the built-in checks aren't enough, you can easily write your own checks with the
from precommitlib import checks def init(precommit): # ... precommit.check(checks.Command("UnitTests", ["./test"])) precommit.check(checks.Command("GoFormat", ["gofmt"], pass_files=True, include=["*.go"]))
The first check,
UnitTests runs a shell command (
./test) with no arguments. The second check,
gofmt on all staged files (
pass_files=True) that match the given list of patterns (
include=["*.go"]). Naturally, there is also an
exclude parameter to exempt files from a check.
If this post has piqued your interest, you are welcome to fork your own version of precommit on GitHub. I encourage you to fork it rather than download it so that you can tailor it to your personal workflow and preferences. I've intentionally kept the tool small and only implemented the features that I need, which are likely not exactly the features that you need.
If you do just want to download it, you can do so with pip:
pip3 install git+https://github.com/iafisher/precommit.git
Note that depending on your system's configuration, pip may not place the
precommit script on your
Enjoy this post? Sign up for my mailing list!
We'd also need to adjust the
find command to something like
find . -path ./.venv -prune -o -name "*.py" -print0 | xargs -0 black --check in order to correctly handle file names that contain whitespace.↩
Note that many of these checks require an external program (
flake8, etc.) to be installed. precommit does not install them for you.↩