Blog Logo

04-Jun-2023 ~ 6 min read

Git pre-commit hooks


git

pre-commit hooks you must know

pre-commit hooks are a mechanism of the version control system git. They let you execute code right before the commit. Confusingly, there is also a Python package called pre-commit which allows you to create and use pre-commit hooks with a way simpler interface. The Python package has a plugin-system to create git pre-commit hooks automatically. It’s not only for Python projects but for any project.

After reading this article, you will know the popular plugins for professional software development. Let’s get started!

Table of Contents

pre-commit basics

Install pre-commit via

pip install pre-commit

Create a .pre-commit-config.yaml file within your project. This file contains the pre-commit hooks you want to run every time before you commit. It looks like this:

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v3.2.0
    hooks:
      - id: trailing-whitespace
      - id: mixed-line-ending
  - repo: https://github.com/psf/black
    rev: 20.8b1
    hooks:
      - id: black

pre-commit will look in those two repositories with the specified git tags for a file called .pre-commit-hooks.yaml. Within that file can be arbitrary many hooks defined. They all need an id so that you can choose which ones you want to use. The above git-commit config would use 3 hooks.

Finally, you need to run pre-commit install to tell pre-commit to always run for this repository.

Before I used it, I was worried about losing control. I wanted to know exactly which changes I commit. pre-commit will abort the commit if it changes anything. So you can still have a look at the code and check if the changes are reasonable. You can also choose not to run pre-commit by

git commit --no-verify

File formatting

Formatting files in a similar way helps readability by improving consistency and keeps git commits clean. For example, you usually don’t want trailing spaces. You want the text files to end with exactly one newline character so that some of the Linux command-line tools behave well. You want consistent newline characters between Linux ( \n ), Mac ( \rMac changed to \n 🎉) and windows ( \r\n ). My configuration for that is

# pre-commit run --all-files
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v3.2.0
    hooks:
      - id: check-byte-order-marker # Forbid UTF-8 byte-order markers
      # Check for files with names that would conflict on a case-insensitive
      # filesystem like MacOS HFS+ or Windows FAT.
      - id: check-case-conflict
      - id: check-json
      - id: check-yaml
      - id: end-of-file-fixer
      - id: trailing-whitespace
      - id: mixed-line-ending

Code style

We can write code in a lot of different ways. Many of them show almost no difference in runtime, but there are differences in readability.

Code Autoformatter

When your code looks ugly, don’t waste your time with learning style guides and applying it by hand. Run a code formatter.

Image by Randall Munroe(XKCD)

Image by Randall Munroe (XKCD)

Automatic code formatting has the same advantages as the file formatting. Additionally, it prevents meaningless discussions. Thus it lets you and your team focus on the important and complicated parts.

I love Pythons autoformatter black and mentioned it already in the article about static code analysis:

- repo: https://github.com/psf/black
  rev: 20.8b1
  hooks:
    - id: black
- repo: https://github.com/asottile/blacken-docs
  rev: v1.8.0
  hooks:
    - id: blacken-docs
      additional_dependencies: [black==20.8b1]

The first one is black itself, the second one is a project which applies the black formatting to code-strings within docstrings.

Additionally, I want my imports to be sorted:

- repo: https://github.com/asottile/seed-isort-config
  rev: v2.2.0
  hooks:
    - id: seed-isort-config
- repo: https://github.com/pre-commit/mirrors-isort
  rev: v5.4.2
  hooks:
    - id: isort

There are autoformatters with pre-commit hooks for many languages:

  • Prettier: HTML, CSS, JavaScript, GraphQL, and many more.
  • Clang-format: C, C++, Java, JavaScript, Objective-C, Protobuf, C#
  • Rustfmt: Rust

Modern Python

pyupgrade runs over your Python code and automatically changes old-style syntax to new-style syntax. Just have a look at some examples:

dict([(a, b) for a, b in y])  # -> {a: b for a, b in y}
class C(object): pass         # -> class C: pass
from mock import patch        # -> from unittest.mock import patch

Do you want it? Here you are:

- repo: https://github.com/asottile/pyupgrade
  rev: v2.7.2
  hooks:
    - id: pyupgrade
      args: [--py36-plus]

Testing your Code

I thought about running the unit tests automatically by pre-commit. I decided not to do that as this might take quite a while. However, there are some quick tests which are good to run automatically and every time:

- repo: https://github.com/pre-commit/pre-commit-hooks
  rev: v3.2.0
  hooks:
    - id: check-ast # Is it valid Python?
    # Check for debugger imports and py37+ breakpoint() calls
    # in python source.
    - id: debug-statements
- repo: https://github.com/pre-commit/mirrors-mypy
  rev: v0.782
  hooks:
    - id: mypy
      args: [--ignore-missing-imports]
- repo: https://gitlab.com/pycqa/flake8
  rev: '3.8.3'
  hooks:
    - id: flake8

Security

Checking in credentials is a pretty common mistake. Here is how you prevent it:

- repo: https://github.com/pre-commit/pre-commit-hooks
  rev: v3.2.0
  hooks:
    - id: detect-aws-credentials
    - id: detect-private-key

Miscellaneous pre-commit hooks

Some hooks don’t fit in the above categories but are still useful. For example, this one prevents big files from being committed:

- repo: https://github.com/pre-commit/pre-commit-hooks
  rev: v3.2.0
  hooks:
    - id: check-added-large-files

Working in a Team

The pre-commit hooks are installed locally and thus every developer could decide on their own if they want pre-commit hooks and which ones. However, I think it is helpful to provide a .pre-commit-config.yaml with plugins you recommend to execute.

All the hooks!

If you’re looking for a complete .pre-commit-config.yaml ready to use, here it is:

# Apply to all files without committing:
#   pre-commit run --all-files
# Update this file:
#   pre-commit autoupdate
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v3.2.0
    hooks:
      - id: check-ast
      - id: check-byte-order-marker
      - id: check-case-conflict
      - id: check-docstring-first
      - id: check-executables-have-shebangs
      - id: check-json
      - id: check-yaml
      - id: debug-statements
      - id: detect-aws-credentials
      - id: detect-private-key
      - id: end-of-file-fixer
      - id: trailing-whitespace
      - id: mixed-line-ending
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v0.782
    hooks:
      - id: mypy
        args: [--ignore-missing-imports]
  - repo: https://github.com/asottile/seed-isort-config
    rev: v2.2.0
    hooks:
      - id: seed-isort-config
  - repo: https://github.com/pre-commit/mirrors-isort
    rev: v5.4.2
    hooks:
      - id: isort
  - repo: https://github.com/psf/black
    rev: 20.8b1
    hooks:
      - id: black
  - repo: https://github.com/asottile/pyupgrade
    rev: v2.7.2
    hooks:
      - id: pyupgrade
        args: [--py36-plus]
  - repo: https://github.com/asottile/blacken-docs
    rev: v1.8.0
    hooks:
      - id: blacken-docs
        additional_dependencies: [black==20.8b1]

Summary

I love pre-commit as it fits so well in my workflow. I just commit as usual and pre-commit does all the checks which I sometimes forget. It speeds up development because the CI/CD pipeline is just way slower than executing the same steps locally. Especially for linting, it’s an enormous time-saver to quickly run black over the code instead of committing, waiting for the CI/CD pipeline, finding an error, fixing that error locally, pushing, and waiting again for the CI/CD pipeline.

Please let me know as a comment or email (info@martin-thoma.de) if there are other pre-commit hooks you like!