Skip to content

Instantly share code, notes, and snippets.

@kfkonrad
Last active July 28, 2023 07:37
Show Gist options
  • Save kfkonrad/6e83a08bed4528ace98255f448ce77d2 to your computer and use it in GitHub Desktop.
Save kfkonrad/6e83a08bed4528ace98255f448ce77d2 to your computer and use it in GitHub Desktop.
How to configure global git hooks (my handcrafted silver bullet)

How to configure global git hooks (my handcrafted silver bullet)

Background

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.

Setup

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.

Explanation of the silver bullet

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.

Example for a ~/.githooks/<hook>.d/ script

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: exiting with anything other than 0 is required to fail the hook.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment