Use Git Hooks to Automate Necessary but Annoying Tasks

Sean Doyle

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:

About thoughtbot

We've been helping engineering teams deliver exceptional products for over 20 years. Our designers, developers, and product managers work closely with teams to solve your toughest software challenges through collaborative design and development. Learn more about us.