Common methods for bootstrapping, pre-commit testing, and Gemfile management? Sign me up!
Go to file
John Bintz a0c9fbbde9 don't re-bundle after committing, so you can safely pull 2013-08-02 11:25:48 -04:00
bin handle modern bundler :github syntax for gem property with default penchant files 2013-01-24 10:06:49 -05:00
config a new way to manage lists of gems with commoon things 2012-06-04 15:39:22 -04:00
features add bundler encoding fix, bump version 2013-07-25 15:26:00 -04:00
lib don't re-bundle after committing, so you can safely pull 2013-08-02 11:25:48 -04:00
script remove last vestiges of erb support 2012-10-09 11:29:17 -04:00
template/script don't re-bundle after committing, so you can safely pull 2013-08-02 11:25:48 -04:00
.gitignore a new way to manage lists of gems with commoon things 2012-06-04 15:39:22 -04:00
Gemfile make sure dependencies get installed and RVM installation instructions 2012-06-21 16:18:48 -04:00
Guardfile a new way to manage lists of gems with commoon things 2012-06-04 15:39:22 -04:00 add bundler encoding fix, bump version 2013-07-25 15:26:00 -04:00
Rakefile rip out erb processing and unnecessary specs 2012-10-09 11:08:18 -04:00
penchant.gemspec make sure dependencies get installed and RVM installation instructions 2012-06-21 16:18:48 -04:00

I have a penchant for setting up all my projects so they work the same.

I like to do these things in all my projects:

  • Have all my tests run before committing. I don't like buying ice cream for the team on test failures, and setting up internal CI for smaller projects is a pain.
  • If I'm developing gems alongside this project, I use a Gemfile.penchant to get around the "one gem, one source" issue in current versions of Bundler.
  • I can also factor out and simplify a lot of my Gemfile settings.
  • If I'm moving to different machines or (heaven forbid!) having other developers work on the project, I want to make getting all those local gems as easy as possible.

This gem makes that easier!

What's it do?

Installs a bunch of scripts into the scripts directory of your project:

  • gemfile which switches between Gemfile.penchant environments
  • install-git-hooks which will do just what it says
  • hooks, several git hooks that the prior script symlinks into .git/hooks for you
  • initialize-environment, which bootstraps your local environment so you can get up and running


Yeah, it's a Gemfile with some extras:

# Gemfile.penchant
source :rubygems

# ensure git hooks are installed when a gemfile is processed, see below

# need the bundler UTF-8 fix? ask for it by name!

# deploying to heroku and want 1.9.3 goodness?
ruby '1.9.3'

gem 'rails', '3.2.3'
# expands to:
# gem 'rake'
# gem 'nokogiri'
# gem 'rack-rewrite'
gems 'rake', 'nokogiri', 'rack-rewrite'

# define custom gem properties that get expanded to ones bundler understands
property :github, :git => 'git://$1/%s.git'
  # values to the key are [ value ].flatten-ed and the $s are replaced on the fly,
  # with $1 being the first parameter given

# set up defaults for all gems in a particular environment
defaults_for env(:local), :path => '../%s' # the %s is the name of the gem

no_deployment do
  group :development, :test do
    gem 'rspec', '~> 2.6.0'

    dev_gems = %w{flowerbox guard-flowerbox}

    # set up defaults for certain gems that are probably being used in envs
    defaults_for dev_gems, :require => nil

    env :local do
      # expands to:
      # gem 'flowerbox', :path => '../flowerbox', :require => nil
      # gem 'guard-flowerbox', :path => '../guard-flowerbox', :require => nil
      gems dev_gems

    env :remote do
      # expands to:
      # gem 'flowerbox', :git => 'git://', :require => nil
      # gem 'guard-flowerbox', :git => 'git://', :require => nil
      gems dev_gems, :github => 'johnbintz'

    # an even shorter way to specify environments!
    # in remote env, expands to:
    #     gem 'bullseye', :git => 'git://'
    # in local env, expands to:
    #     gem 'bullseye', :path => '../bullseye'
    env :remote, :opposite => :local do
      gem 'bullseye', :github => 'johnbintz'

    # only expanded on Mac OS X
    os :darwin do
      gem 'rb-fsevent'

    # only expanded on Linux
    os :linux do
      gems 'rb-inotify', 'ffi'

Use script/gemfile local to get at the local ones, and script/gemfile remote to get at the remote ones. It then runs bundle install.

You can also run penchant gemfile ENV. Just straight penchant gemfile will rebuild the Gemfile from Gemfile.penchant for whatever environment the Gemfile is currently using.

If you have an existing project, penchant convert will convert the Gemfile into a Gemfile.penchant and add some bonuses, like defining that anything in env :local blocks automatically reference .., ensuring that hooks are always installed when penchant gemfile is executed, and adding the :github gem property that lets you pass in the username of the repo to reference that repo: gem 'penchant', :github => 'johnbintz'.

Stupid-simple local/remote setup

Use opposites :local, :remote and environment settings for local/remote gems will be set accordingly depending on environment:

defaults_for env(:local), :path => '../%s'
opposites :local, :remote

env :remote do
  gem 'my-gem', :git => 'git://'

In remote, the Git repo version is used. In local, the path is used. Only one gem definition needed!

Deployment mode

Use no_deployment blocks to indicate gems that shouldn't even appear in Gemfiles destined for remote servers. Very helpful when you have OS-specific gems and are developing on one platform and deploying on another, or if you don't want to deal with the dependencies for your testing frameworks:

gem 'rails'

no_deployment do
  os :darwin do
    gems 'growl_notify', 'growl', 'rb-fsevent'

  os :linux do
    gem 'libnotify', :require => nil

  group :test do
    # ... all your testing libraries you won't need on the deployed end ...

Run penchant gemfile ENV --deployment to get this behavior. This is run by default when the pre-commit git hook runs, but only after the default Rake task passes.

If you just want any locally installed gems, add the --local switch. Great if is down!

Won't this change the project dependencies?!

Probably not. You probably have the "main" gems in your project locked to a version of Rails or Sinatra or something else, and all of the other gems for authentication, queue processing, etc. are dependent on that framework. Ripping out your testing framework and deployment helpers really shouldn't be changing the main versions of your application gems. It WORKSFORME and YMMV.

Getting local gems all set up

penchant bootstrap will go through and find all git repo references in your Gemfile.penchant and will download them to the specified directory (by default, ..). This means blocks like this will work as expected when you penchant bootstrap and then penchant gemfile local:

env :local do
  gem 'my-gem', :path => '../%s'

env :remote do
  gem 'my-gem', :git => 'git://'

Note that this just does a quick git clone, so if your project is already in there in a different state, nothing "happens" except that git fails.


Get new developers up to speed fast! script/initialize-environment does the following when run:

  • Check out any remote repos found in Gemfile.penchant to the same directory where your current project lives. That way, you can have your Gemfile.penchant set up as above and everything works cleanly.
  • Runs script/gemfile remote to set your project to using remote repositories.
  • Runs rake bootstrap for the project if it exists.

After-gemfile hooks?

Drop a file called .penchant in your project directory. It'll get executed every time you switch environments using Penchant. I use it to tell my Hydra clients to sync and update their Gemfiles, too:

# rake knows if you need "bundle exec" or not.

rake "hydra:sync hydra:remote:bundle"

What environment are you currently using in that Gemfile?

head -n 1 that puppy, or penchant gemfile-env.

git hook?!

It runs penchant gemfile remote then runs bundle exec rake. Make sure your default Rake task for the project runs your tests and performs any other magic necessary before each commit. Your re-environmented Gemfile and Gemfile.lock will be added to your commit if they've changed.

Ensuring git hooks get installed

I find that when I pull down new projects I never remember to install the git hooks, which involves an awkward running of bundle exec rake after I've already committed code. Since we have computers now, and they can be told to do things, you can add ensure_git_hooks! anywhere in your Gemfile.penchant to make sure the git hooks are symlinked to the ones in the script/hooks directory with every processing of Gemfile.penchant.

Performing pre-bundle exec rake tasks.

Example: I use a style of Cucumber testing where I co-opt the @wip tag and then tell Guard to only run scenarios with @wip tags. I don't want @wip tasks to be committed to the repo, since committing a half-completed scenario seems silly. So I use bundle exec rake preflight_check to check all feature files for @wip tasks, and to fail if I hit one. Yes, Cucumber already does this, but in order to get to bundle exec rake, I need to go through two Gemfile creations, one for remote --deployment and one for remote to make sure my tests work on remote gems only.

If bundle exec rake -T preflight_check returns a task, that task will be run before all the Gemfile switcheroo. Don't use it as a place to run your tests!

Skipping all that Rake falderal?

Do it Travis CI style: stick [ci skip] in your commit message. That's why the meat of the git hooks resides in commit-msg and not pre-commit: you need the commit message before you can determine if the tests should be run based on the commit message. Weird, I know.


  • No RVM? gem install penchant
  • RVM? rvm gemset use global && gem install penchant && rvm gemset use default
  • cd to your project directory

And then one of the following:

  • penchant install for a new project (--dir=WHEREVER will install the scripts to a directory other than $PWD/scripts)
  • penchant update to update the installation (--dir=WHEVEVER works here, too)
  • penchant convert for an existing project (--dir=WHEVEVER works here, too)