At work, I push commits to different repositories, which have evolved over a few years to have slightly different code style requirements. At some point, for example, the decision was made to enforce a maximal line length in new repositories, but not to apply the requirement retroactively.

If I run a linter in a new repository, it will tell me which lines I need to shorten so that the linting stage in our automated build pipeline doesn’t fail. This saves me time and I want it to happen before every commit – this is what pre-commit hooks are for. If I run the same linter in an ‘old’ repository, it will generate thousands of warnings, most of which are irrelevant for me. Hence the need for having different pre-commit hooks in different repos.

This post describes the iterations I went through in coming up with a maintainable Git hook system. For basic background see the Git book or the documentation at man githooks(5).

Separate pre-commit files per repository

Self-explanatory, with the obvious drawback that I have to maintain one separate file per repo. Instead of installing the hook each time a repo is cloned or initialized, I can set up a custom template directory ~/dev/git/templates, which will be used by git clone and git init, and which will contain the hooks I want.

Symlinking / global

If the linting requirements of all your repos are the same, you can use the core.hooksPath config variable to make all your repos use hooks from some shared directory ~/dev/git/hooks. If you want a repo to use a different script, you can simply override core.hooksPath in the repo-specific configuration. Alternatively you could create a symlink at .git/hooks/pre-commit pointing to ~/dev/hooks/pre-commit and overwrite the symlink with a custom script if you need different functionality for a single repo.

Parametric hooks

If your needs are to complex for the previous solution, the following architecture might work. Make ~/dev/hooks/pre-commit parametric on environment variables. In python land you might be using flake8:

#!/usr/bin/bash

flake8_ignore="${pch_flake8_ignore:-E123,W503}"

flake8 --extend-ignore "$flake8_ignore" ./src/

You can make ~/dev/git/templates/hooks/pre-commit a symlink to ~/dev/hooks/pre-commit, which will execute the default version before every commit. If you need to make modifications, you can replace it with an actual script which runs the first one:

#!/usr/bin/bash

export pch_flake8_ignore=W504
$HOME/dev/hooks/pre-commit

# repo specific
./run_quick_unit_tests.sh

This setup is versatile enough that I don’t expect to outgrow it for a long time. One limitation is that doesn’t support versioning of the different variations of hooks. The central ~/dev/hooks/pre-commit directiry can be versioned of course. The individual per-project hooks can not, unless your org would allow to include them with the repository code.

It’s funny how people who go down the bash route to software engineering seem to reinvent solutions to the same problems over and over again.