Bundle While You Git

07 Feb 2011

Update: hookup does all this for you, plus migrates your database!

I recently discovered RVM’s ability to create a project .rvmrc when running rvm use --rvmrc. Commented out by default is a section for automatically running Bundler when cding to your project:

# Bundle while reducing excess noise.
printf "Bundling your gems. This may take a few minutes on a fresh clone.\n"
bundle | grep -v '^Using ' | grep -v ' is complete' | sed '/^$/d'

I excitedly enabled this, as the less I have to run the bundle command manually, the better. But it quickly became apparent that this is the wrong time to bundle. Nothing changes when I cd into a project, so why would the bundle be out of date? Except after the initial clone, this trick did nothing but eat valuable seconds (sometimes minutes). What I really needed was a way to bundle right after a git checkout or git pull --rebase (or throughout a git bisect, if we want to get really ambitious).

As luck would have it, Git provides a post-checkout hook that runs in all those cases. Let’s take a stab at it, shall we? Create .git/hooks/post-checkout with the following content and chmod +x it:

#!/bin/sh
if [ -f Gemfile ] && command -v bundle >/dev/null; then
  # $GIT_DIR can cause chaos if Bundler in turn invokes Git.
  # Unset it in a subshell so it remains set later in the hook.
  (unset GIT_DIR; exec bundle)
  # Even if bundling fails, exit from `git checkout` with a zero status.
  true
fi

Awesome? You bet, but it runs on every HEAD switch, which means we still lose valuable seconds even when the Gemfile isn’t updated. Luckily, the post-checkout hook receives the old and new HEAD as arguments, so it’s easy to check for changes to the Gemfile, Gemfile.lock, or the project’s gem spec:

#!/bin/sh
if [ $1 = 0000000000000000000000000000000000000000 ]; then
  # Special case for initial clone: compare to empty directory.
  old=4b825dc642cb6eb9a060e54bf8d69288fbee4904
else
  old=$1
fi
if [ -f Gemfile ] && command -v bundle >/dev/null &&
  git diff --name-only $old $2 | egrep -q '^Gemfile|\.gemspec$'
then
  (unset GIT_DIR; exec bundle)
  true
fi

What else? Well, we could silence some of the noise like RVM does:

#!/bin/sh
if [ $1 = 0000000000000000000000000000000000000000 ]; then
  old=4b825dc642cb6eb9a060e54bf8d69288fbee4904
else
  old=$1
fi
if [ -f Gemfile ] && command -v bundle >/dev/null &&
  git diff --name-only $old $2 | egrep -q '^Gemfile|\.gemspec$'
then
  (unset GIT_DIR; exec bundle) | grep -v '^Using ' | grep -v ' is complete'
  true
fi

Left as an exercise to the reader: installing Bundler if it’s not already installed (personally, I still do that from the .rvmrc), and configuring Git to install that hook into new and freshly cloned repositories.

Last but not least, here’s a complimentary, complementary pre-commit hook:

#!/bin/sh
git diff --exit-code --cached -- Gemfile Gemfile.lock >/dev/null || bundle check