Increase the quality of your commits with pre-commit

pre-commit [1] is a framework for managing and maintaining pre-commit hooks for git. By running hooks before any commit, many small pitfalls could be avoided before being pushed and will spare reviewers time and energy.

Such hooks could for example check that commit messages follow a specific format or that the code pass a lint test for a specific type of file.

The hooks you use is specified in a .pre-commit-config.yaml file that you could include into your git repository. This makes it easy to bind the hooks to the project and share between all the developers.

Available hooks

pre-commit have a range of supported hooks, here is a list of those hooks that is currently part of the repository:

  • check-added-large-files - prevents giant files from being committed.
  • check-ast - simply checks whether the files parse as valid python.
  • check-byte-order-marker - forbids files which have a utf-8 byte-order marker.
  • check-builtin-literals - requires literal syntax when initializing empty or zero python builtin types.
  • check-case-conflict - checks for files that would conflict in case-insensitive filesystems.
  • check-docstring-first - checks a common error of defining a docstring after code.
  • check-executables-have-shebangs - ensures that (non-binary) executables have a shebang.
  • check-json - checks json files for parseable syntax.
  • check-shebang-scripts-are-executable - ensures that (non-binary) files with a shebang are executable.
  • pretty-format-json - sets a standard for formatting json files.
  • check-merge-conflict - checks for files that contain merge conflict strings.
  • check-symlinks - checks for symlinks which do not point to anything.
  • check-toml - checks toml files for parseable syntax.
  • check-vcs-permalinks - ensures that links to vcs websites are permalinks.
  • check-xml - checks xml files for parseable syntax.
  • check-yaml - checks yaml files for parseable syntax.
  • debug-statements - checks for debugger imports and py37+ breakpoint() calls in python source.
  • destroyed-symlinks - detects symlinks which are changed to regular files with a content of a path which that symlink was pointing to.
  • detect-aws-credentials - detects your aws credentials from the aws cli credentials file.
  • detect-private-key - detects the presence of private keys.
  • double-quote-string-fixer - replaces double quoted strings with single quoted strings.
  • end-of-file-fixer - ensures that a file is either empty, or ends with one newline.
  • file-contents-sorter - sorts the lines in specified files (defaults to alphabetical). you must provide list of target files as input in your .pre-commit-config.yaml file.
  • fix-byte-order-marker - removes utf-8 byte order marker.
  • fix-encoding-pragma - adds # -- coding: utf-8 -- to the top of python files.
  • forbid-new-submodules - prevents addition of new git submodules.
  • forbid-submodules - forbids any submodules in the repository
  • mixed-line-ending - replaces or checks mixed line ending.
  • name-tests-test - verifies that test files are named correctly.
  • no-commit-to-branch - don't commit to branch
  • requirements-txt-fixer - sorts entries in requirements.txt.
  • sort-simple-yaml - sorts simple yaml files which consist only of top-level keys, preserving comments and blocks.
  • trailing-whitespace - trims trailing whitespace.

It also has tons of supported hooks from third-parties. See [2] for a full list.

Quick start

The webpage [1] does a terrific job to describe this, so I will only briefly go through the steps.

Installation

pre-commit is available in most of the package managers out there. I use pacman as I'm hooked (phun intended) to Archlinux.

pacman install pre-commit

pre-commit configuration

pre-commit use a configuration file in order to know wich hooks to use. In your repository, generate a basic configuration file using pre-commit sample-config:

pre-commit sample-config > .pre-commit-config.yaml

The default configuration file looks as follow:

	repos:
	-   repo: https://github.com/pre-commit/pre-commit-hooks
	    rev: v3.2.0
	    hooks:
	    -   id: trailing-whitespace
	    -   id: end-of-file-fixer
	    -   id: check-yaml
	    -   id: check-added-large-files

Install hooks

When we've the configuration file in place, install the hooks and we're ready to go:

pre-commit install

Test it

Once the hooks are installed, we can make a test-commit:

/media/pre-commit.png

As we can see, several tests are performed before the commit are made. It won't allow you to commit if any of the pass does not pass.

Git template directory

This is great, we now have a way to specify which hooks we want to run for a certain repository! But what if we want to use these hooks for all of our repositores?

Git supports a template directory [3], which basically is a directory with files that will be copied to $GIT_DIR after a repository is created (which includes both new and cloned repositories).

You can do this in several ways, even per repo by the --template option, but the global init.templateDir configuration variable is how I prefer to do it.

First, create a home for your git-template and tell git to use it:

mkdir $HOME/.git-template
git config --global init.templatedir '~/.git-template'

Then create a pre-commit configuration file and let pre-commit init-templatedir populate the directory as a templatedir:

pre-commit sample-config > $HOME/.git-template/.pre-commit-config.yaml
pre-commit init-templatedir -c $HOME/.git-template/.pre-commit-config.yaml $HOME/.git-template

Now these hooks will be applied to all repositories you will clone or create. Test it out:

$ git init
$ echo 'echo $PATH' > test.sh
$ git add test.sh 
$ git commit -m "add test.sh"
Trim Trailing Whitespace.................................Passed
Fix End of Files.........................................Passed
Check Yaml...........................(no files to check)Skipped
Check for added large files..............................Passed
shellcheck...............................................Failed
- hook id: shellcheck
- exit code: 1

In test.sh line 1:
echo $PATH
^-- SC2148 (error): Tips depend on target shell and yours is unknown. Add a shebang or a 'shell' directive.
     ^---^ SC2086 (info): Double quote to prevent globbing and word splitting.

     Did you mean: 
     echo "$PATH"

     For more information:
       https://www.shellcheck.net/wiki/SC2148 -- Tips depend on target shell and y...
         https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ...

As you can see, I have a few things to fix :-)

My pre-commit config file

My configuration file does not differ much from the default one.

In additional to the default config file, I've added shellcheck to check my shell-scripts: :

	repos:
	-   repo: https://github.com/pre-commit/pre-commit-hooks
	    rev: v3.2.0
	    hooks:
	    -   id: trailing-whitespace
	    -   id: end-of-file-fixer
	    -   id: check-yaml
	    -   id: check-added-large-files
	-   repo: https://github.com/shellcheck-py/shellcheck-py
	    rev: v0.10.0.1
	    hooks:
	    -   id: shellcheck