Long story short, pre-commit is great! Although this is a Python oriented blog and pre-commit happens to be written in Python, pre-commit is basically language independent. If you use git as VCS (who doesn't these days?), keep reading. I'll go though part of its awesomeness here with a Pythonic use case. If you are hacking around some other language, don't worry, I'm sure you'll get the gist and plenty of ideas about how to utilize it in your Java/C#/JS/whatever masterpieces.

Why

Best practices gently forced among all team members or contributors

No need to fight about formatting, pre-commit can enforce it

Better quality when code hits CI, more green pipelines

Better quality when it's time to review, human time is expensive

How

The minimal set-up

.pre-commit-config.yaml inside the repo pip install pre-commit , or equivalent pre-commit install 🍻

It might be a good idea to list those steps in the readme of the project and make it clear that everyone should follow them. Once done, the hooks configured in the pre-commit config file will run on each commit and check the files which are in the changeset. If any of the hooks fails during the commit, the commit itself will fail as well.

Might be also worth to run those steps as part of CI pipeline/build as there will be always that one rebel in the team who won't run them locally. On CI, replace pre-commit install by pre-commit run -a . We don't need to install it on CI as it'll be a one-shot operation. run -a will run the hooks over all files (taking into account excludes if configured).

What kind of hooks to configure

Linting, automatic formatting, safety checks, custom scripts, you name it. The specific needs are unique for each project. Perhaps you'll find some inspiration from the official hooks.

It's always a tradeoff between the benefits gained vs time. In general, the more hooks there are, the longer it'll take. However, pre-commit also provides an option to run the hooks during push instead of commit. It's a considerably better option considering the time usage unless you are one of the mad lads who have one-to-one commit-push-ratio. Instead of pre-commit install , type pre-commit install -t pre-push and you are good to go, they'll run only during push.

pre-commit install is actually a shorthand for pre-commit install -t pre-commit . Uninstalling is done by pre-commit uninstall and the same -t flags can be used there.

Tuning

Perhaps you prefer to run some of the hooks during commit and the rest during push, see top-level default_stages and repo specific stages variables. For example, maybe you want to run the whole test suite or some fast part of it (e.g. smoke tests) during push. If you are battling with a large existing Python test suite and use pytest, markers could be a shortcut to building a fast/smoke suite without a need to modify existing directory structure.

Maybe you'd like to have some hooks which should be ran only when explicitly asked to run, e.g. due to their time consuming nature. There's manual stage option for this, which might be handy in run-only-on-CI cases.

Alright, show me some example

.pre-commit-config.yaml

- repo: https://github.com/pre-commit/pre-commit-hooks sha: v1.2.3 hooks: - id: check-merge-conflict - id: debug-statements - id: flake8 args: [--max-line-length=100] - repo: https://github.com/ambv/black rev: stable hooks: - id: black language_version: python3.6 - repo: https://github.com/Lucas-C/pre-commit-hooks-bandit sha: v1.0.3 hooks: - id: python-bandit-vulnerability-check args: [-l, --recursive, -x, tests] files: .py$ - repo: local hooks: - id: tests name: run tests entry: pytest -v language: system types: [python] stages: [push]

Assuming both pre-commit and pre-push are installed ( pre-commit install && pre-commit install -t pre-push ), it will:

During commit

Check if there are merge conflicts

Check if there are debug statements, we don't want those in checked in code

Lint with flake8

black the code, modifies files in-place if the code in the changeset is not already black compliant and fails the hook

Run security checks with bandit, except for files in tests dir

During push

All the above

Runs also pytest with verbose flag

You'd see something like this in the command line while committing/pushing:

$ git push Check for merge conflicts..........Passed Debug Statements (Python)..........Passed Flake8.............................Passed black..............................Passed bandit.............................Passed run tests..........................Passed

Closing remarks