I realized that a lot of my repos shared code for git hooks, especially since I use hooks for commom tasks such as linting, formatting and project cleanups. I wanted to have a central place for all hooks and their scripts so I could update them in one place.
Since some projects provide their own set of git hooks, I also wanted to be able to use those instead of my global hooks (if present).
So I started looking for a tool to solve the problem and found nothing. Nothing that was still maintained or easy to use anyway.
I'm calling it a silver bullet because one simple shell script without dependencies solves the problem and it's easy to adapt should you need to.
Use git config --global core.hooksPath '~/.githooks'
to configure the directory ~/.githooks
as the central directory for shared git hooks on all repos.
(Alternatively: Set on a per repo-basis without the --global
flag to opt-in to your central git hooks)
You will have to create the directory with mkdir -p ~/.githooks
first. Each hook you use needs its own subdirectory ~/.githooks/<hook>.d/
which contains the script(s) that will be run for the respective hook.
The currently available git hooks are as follows:
- applypatch-msg
- commit-msg
- fsmonitor-watchman
- post-update
- pre-applypatch
- pre-commit
- pre-merge-commit
- pre-push
- pre-rebase
- pre-receive
- prepare-commit-msg
- push-to-checkout
- update
My silver bullet-suggestion is to use this exact script for each and every git hook you intend to use:
#!/usr/bin/env sh
hook=$(echo $0 | sed 's,.*/,,')
if test -e $PWD/.githooks/$hook
then
exec $PWD/.githooks/$hook
fi
if test -e $PWD/githooks/$hook
then
exec $PWD/githooks/$hook
fi
for file in $(find $0.d -type f | sort)
do
$file
done
If a hook is undefined nothing will be executed, this is expected and normal git behavior. Note: All scripts involved in this solution must be marked as executable (i.e. chmod +x ~/.githooks/<hook>.d/*
)!
This executes a hook in .githhooks/<hook>
or githooks/<hook>
in your repo if present and runs scripts in ~/.githooks/<hook>.d/
in alphabetical order otherwise.
The line hook=$(echo $0 | sed 's,.*/,,')
extracts the name of the hook from the parameters passed to the script by git. This make the script usable for all hooks.
The script first looks for a .githooks/<hook>
file in the root of the current git repo. If this file exists, it is executed.
If .githooks/<hook>
is not found, githooks/<hook>
(without the dot) is checked and executed if it exists.
If neither .githooks/<hook>
nor githooks/<hook>
are found the script will search recursively for all files in the .githooks/<hook>.d/
directory and execute them in alphabetical order.
All files under .githooks/<hook>.d/
will have to be executable as well.
This mechanism allows for an override if a repo prefers to provide its own hooks and executes predefined (optionally hierarchically organized) scripts otherwise.
This is the script I use as my ~/.githooks/pre-commit.d/terraform-fmt
to check terraform formatting:
#!/bin/sh
which -s terraform
if test $? -eq 0
then
output=$(terraform fmt -recursive -check)
if test $? -ne 0
then
echo Malformatted files found, aborting. Run to format:
echo terraform fmt -recursive -write
echo
echo $output
exit 1
fi
fi
I've written it so that the script is portable and will only run the check if terraform
is installed. If I had many more checks,
I would've implemented further preconditions (like the repo containing any .tf
files) for performance.
This however is good enough for me (for now).
Note: exit
ing with anything other than 0
is required to fail the hook.