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.
- 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
The minimal set-up
- .pre-commit-config.yaml inside the repo
pip install pre-commit, or equivalent
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.
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
- 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:
- 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
- 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
- All the pre-commit benefits could be of course achieved in other ways as well. However, I argue that pre-commit beats its competitors in developer experience thanks to it seamless way of execution - no need for manual triggers.
- It's easy to write own custom hooks and let the whole open source community enjoy them.
- If you practice heavy TDD and prefer committing failing tests on purpose, then having tests part of pre-commit / pre-push is naturally unacceptable. However, a smoke suite or similar could be still ran as part of commit/push.
- If you need to cheat a bit, there's always --no-verify.
- As the ancient proverb says, commit often, push at least once a day, and make sure the quality is high when it hits remote.