By Thomas Aglassinger, software developer (Web | LinkedIn)
Ensuring a consistently high quality for source code is a challenge every software development team has to face one way or another. Human code reviews are one tool that can help with that. However, there always is a danger of them being bogged down by trivial issues like formatting mistakes. This wastes time for that human reviewer that would prefer to focus on real issues like hard to understand design decisions, questionable scalability or lack of robustness.
The source control system git supports so-called "hooks" that can act on certain git events. One of them are pre-commit hooks, which can run little scripts or programs before changes are committed. These particular hooks are useful to perform small checks and clean up the source code to be committed. The intention is to find and fix minor issues in the code before they pop up in a human code review. Consequently, they save time for human reviewers.
For example, pre-commit hocks can remove trailing white space or ensure that your code starts with a license comment.
The command line tool and framework pre-commit allows to manage and implement pre-commit hooks using a configuration file. Many standard hooks for multiple programming languages already exist that can be added to your project by adding a few lines to your .pre-commit-config.yaml
.
In this open discussion I share hooks that I found helpful to improve the code quality of my projects. Additionally, we collect hooks that people from the audience found useful.
If you are looking for more hooks, a good starting point is the collection of popular hooks by the pre-commit project.
The pre-commit tool can be downloaded and installed as described on its website.
Next, add a file .pre-commit-config.yaml
to your project. A very simple one could look like this:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.3.0
hooks:
- id: check-yaml
- id: trailing-whitespace
This checks if all YAML files in your project are syntactically correct, and removed trailing white space from text files.
To check if the files on your current git change list conform, run:
pre-commit
To check if all files in your repository pass, run:
pre-commit --all-files
As the authors of pre-commit hooks might update them from time to time, you also want to ensure your project uses the most current version. For that, run:
pre-commit autoupdate
A collection of general hooks that can be helpful for many projects:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: fix-byte-order-marker
# Helpful for teams where some members use Windows and Microsoft editors,
# and others Unix or macOS
- id: trailing-whitespace
# Ensure that each line in a text files does not have white space at the end,
# which would only waste storage.
- id: end-of-file-fixer
# Ensure that each text file ends with a new line. Otherwise, some badly
# coded unix tools might get confused when processing them.
- id: check-added-large-files
# Prevents you from accidentally adding large files, for example a ZIP you
# dropped and extracted in the project folder.
# Check various file formats for syntactical correctness.
- id: check-json
- id: check-toml
- id: check-xml
- id: check-yaml
- id: check-merge-conflict
# Check that the source code does not contain any markers indicating that a
# merge conflict has yet to be resolved.
- id: mixed-line-ending
# Check that the end of lines in a text file are consistent
# (Windows uses CR+LF, Unix LF and macOS classic CR).
#
# Note that you can also set ".gitattributes" to let git ensure that, see
# <https://www.git-scm.com/docs/gitattributes>.
In case you have a policy to only use blanks and avoid tabs:
- repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.5.4
hooks:
- id: remove-tabs
args: [--whitespaces-count, '2'] # defaults to: 4
exclude: Makefile
Note that we are excluding Makefile
because it demands tabs as part of its syntax. 😑
If your project uses symbolic links:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: destroyed-symlinks
# Detect symbolic links which are changed to regular files
# with a content of a path which that symlink was pointing to.
To protect e.g. main from direct commits without feature branch.
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: no-commit-to-branch
args: ["--branch", "main", "--branch", "production"]
- repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.5.4
hooks:
- id: insert-license
files: \.(po|py)$
args:
- --license-filepath=LICENSE
- --comment-style=#
- --detect-license-in-X-top-lines=2
- --no-extra-eol
- --allow-past-years # Using these arguments allows for a date range
- --use-current-year # (original_date - current_date) to be generated.
- Prettier: Pretty prints CSS, HTML, JavaScript, Markdown and more.
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.0.3
hooks:
- id: prettier
- black: Pretty prints Python
- repo: https://github.com/ambv/black
rev: 23.9.0
hooks:
- id: black
- isort: Sorts Python
import
consistently
- repo: https://github.com/pre-commit/mirrors-isort
rev: v5.10.1
hooks:
- id: isort
In general husky.net can help with managing git hooks.
To format code:
- repo: https://github.com/dotnet/format
rev: ""
hooks:
- id: dotnet-format
args: [--folder, --include]
For many programming languages, additional pre-commit hooks exist. Here are some examples.
The audience recommendation was to include formatting and linting in the build process. In order to do that, many plugins for Maven already exist.
You can run eslint via pre-commit. The actual configuration depends on the plugins your project uses, so refer to the documentation for details.
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: check-executables-have-shebangs
# Check that shell scripts start with something like "#!/bin/sh"
- id: check-shebang-scripts-are-executable
# Check that on Unix shell scripts also have the "chmod x" set and
# can does be run directly without calling "sh" (or something similar).
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: check-ast
# Check that Python source code is syntactically correct by
# parsing its abstract syntax tree (AST).
- id: debug-statements
# Check for debug statements that would stop the application when
# encountered (pdb, ipdb, rdb).
- repo: https://github.com/asottile/pyupgrade
rev: v3.10.1
hooks:
- id: pyupgrade
args: ["--py310-plus"]
# Ensure Python source code effectively uses language features that have been added at some point.
For linting there are multiple solutions:
- flake8: Limited and simple
- repo: https://github.com/PyCQA/flake8
rev: 6.1.0
hooks:
- id: flake8
additional_dependencies: ["pep8-naming==0.13.3"]
- pylint: Flexible and complex, requires a lot of configuration
If you are using the Django web framework:
- repo: https://github.com/ecugol/pre-commit-hooks-django
rev: v0.4.0
hooks:
- id: check-untracked-migrations
- id: check-unapplied-migrations
- id: check-absent-migrations
# Various checks to perform on database migrations.
- repo: https://github.com/rtts/djhtml
rev: 3.0.6
hooks:
- id: djhtml
# Indent only HTML files in template directories.
files: .*/templates/.*\.html$
# Indent HTML files that include Django's macro language,
# which can not be formatted using Prettier.
Gitleaks detects attempts to commit secret keys as part of the source code.
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
In addition to using hooks from git repositories, you can also add your own local hooks. As example consider a hook that prevents you from committing files with the suffix ".rej":
- repo: local
hooks:
- id: forbidden-files
name: forbidden files
entry: found Copier update rejection files; review them and remove them
language: fail
files: "\\.rej$"
For testing, create a script to runs the tests with all options needed to do so. For example, scripts/run_tests.sh
, in a Python project using poetry for package management and pytest for testing, you could use:
#!/bin/sh
poetry run pytest
Then add a local hook to run this upon commit:
- repo: local
hooks:
- id: run_tests
language: script
name: Run tests
entry: ./scripts/run_test.sh
For teams with multiple similar projects and can save effort by having a common template for the .pre-commit-congig.yaml
that is shared across projects.
If you want to do that, take a look at tools like copier or cookiecutter.