Keeping Git Hooks in Sync

Git hooks let developers automate expectations and workflows–terrific news for supporting ease-of-use and consistency across a development team. Unfortunately, git doesn’t automatically synchronize hooks between project contributors. I can set up git hooks for my own workflow, but I need some extra help to share them with the rest of my team.

Fortunately, git hook synchronization is an easy (and in later versions of git, almost trivial) problem to solve.

Bootstrapping

First, we’ll need an easy way for other developers to configure their checked-out copy of the project. I keep project-specific tooling in a ./dev directory as a matter of convention, and one of the first files to go in itis a ./dev/bootstrap.sh script. This script ensures that the remotes, secrets, and services the project depends on are all set up correctly. It’s also a good place to set up our hooks:

#!/bin/bash

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

cd "$DIR/.."

log() {
  echo "$(basename ${BASH_SOURCE[0]}): $@"
}

install_hooks() {
  # See alternative installs below
}

log 're/configuring hooks...'
install_hooks

Fair enough: a bit of bash boilerplate and an install_hooks stub for us to start filling in.

The Easy Way

If you can count on all of the project’s contributors to use git >= 2.9, git itself includes a built-in solution to our problem.

install_hooks() {
  git config core.hooksPath \
    || git config core.hooksPath ./dev/hooks
}

In other words, point core.hooksPath at ./dev/hooks and skip the rest of this article. If there’s the tiniest chance that someone might have an older version of git, however, read on.

The Slightly Less Easy Way

Our next option is to symlink the ./dev/hooks directory into .git/hooks and let the operating system keep them in sync. At this point the repository should follow:

$ tree dev
dev
├── bootstrap.sh
└── hooks
    └── ...

We can install ./dev/hooks by clobbering .git/hooks and linking our version in.

install_hooks() {
  rm -rf ./.git/hooks
  ln -s ./hooks ./.git/hooks
}

The catch is that symlink behavior–less than obvious at the best of times–may end tragically for development teams working across platforms. Tread carefully.

Brute Force

The final option is to copy around the hooks ourselves. This time, we’ll warm up ./dev/hooks with a script that re-applies ./dev/bootstrap.sh when the git index changes. This can happen at various stages of the workflow, but I typically use post-checkout:

#!/bin/bash
#
# ./dev/hooks/post-checkout

$(git rev-parse --show-toplevel)/dev/bootstrap.sh $@

The final variation on install_hooks() takes a hammer to the entire works by copying changes to our custom hooks over the top of .git/hooks:

install_hooks() {
  git diff --quiet $1 $2 './dev/hooks' 2> /dev/null || {
    rm -rf ./.git/hooks
    cp -r ./dev/hooks ./.git/hooks
  }
}

All that’s left is to install the scripts for the first time:

$ ./dev/bootstrap.sh
bootstrap.sh: re/configuring hooks...

Now we’re cooking with gas! Better yet, other contributors will now get to join in the fun. Add new hooks or edit the ones you’ve got, commit them, and watch them propagate out to the rest of the team.

Hey, I'm RJ! For more learnings about software and management, find me @rjzaworski or sign up for my semi-regular newsletter.

Let’s keep in touch

Send me timely updates on software, product, and process.