Use Git Hooks to Automate Necessary but Annoying Tasks

Certain tasks like updating dependencies or migrating a database must be done after pulling code or checking out a branch. Other tasks such as re-indexing our ctags improve our development experience. Both kinds of tasks are easy to forget to do and are therefore error-prone. To address the problem, we've recently added a standard, extensible set of git hooks to our dotfiles in order to automate necessary, but annoying tasks.

Git Hooks

Git has a commonly under-utilized feature: hooks. You can think of a hook as an event that gets triggered before and after various stages of revision control process. Some hooks of note are:

  • prepare-commit-msg - Fires before the commit message prompt.
  • pre-commit - Fires before a git commit.
  • post-commit - Fires after a git commit.
  • post-checkout - Fires after changing branches.
  • post-merge - Fires after merging branches.
  • pre-push - Fires before code is pushed to a remote.

Extending our Hooks

Our dotfiles' convention for extension is to place our custom hooks in {pre,post}-$EVENT files within our ~/.git_template.local/hooks directory. Now, anything we add to those hook files will be automatically executed, running tasks that we normally would forget.

What tasks do you commonly forget

I forget to re-index my ctags!

Lucky for you, we've set up git to re-index your ctags after each git command.

I always forget to run bundle install after switching branches!

Automatically install new gems:

# ~/.git_template.local/hooks/post-checkout

# use `hookup` gem if it's installed
if command -v hookup > /dev/null; then
  hookup post-checkout "$@"
else
  # otherwise, do it yourself
  [ -f Gemfile ] && bundle install > /dev/null &
fi

I never remember to run pending migrations!

Automatically run your migrations:

# ~/.git_template.local/hooks/post-checkout

# use `hookup` gem if it's installed
if command -v hookup > /dev/null; then
  hookup post-checkout "$@"
else
  # otherwise, do it yourself
  [ -f db/schema.rb ] && bin/rake db:migrate > /dev/null &
fi

I document my API with fdoc, but I forget to generate the pages!

Automatically generate the HTML docs:

# ~/.git_template.local/hooks/post-checkout

bin/fdoc convert ./spec/fixtures --output=./html > /dev/null &

I really like Go's commitment to a standard code format, but I constantly forget to format my files!

Run go fmt before you commit:

# ~/.git_template.local/hooks/pre-commit

gofiles=$(git diff --cached --name-only --diff-filter=ACM | grep '.go$')
[ -z "$gofiles" ] && exit 0

function checkfmt() {
  unformatted=$(gofmt -l $gofiles)
  [ -z "$unformatted" ] && return 0

  echo >&2 "Go files must be formatted with gofmt. Please run:"
  for fn in $unformatted; do
    echo >&2 "  gofmt -w $PWD/$fn"
  done

  return 1
}

checkfmt || fail=yes

[ -z "$fail" ] || exit 1

exit 0

I want my extensive network of friends to know when I'm merging code!

Send out a Yo every time you merge a branch:

# ~/.git_template.local/hooks/post-merge

curl --data "api_token=$YO_API_TOKEN" https://api.justyo.co/yoall/ > /dev/null &

When we aggressively simplify and automate the tedious parts of the development process, we can focus on what's important: getting things done.

What's next

If you found this useful, you might also enjoy: