Compare commits

..

94 Commits

Author SHA1 Message Date
John Bintz a0c9fbbde9 don't re-bundle after committing, so you can safely pull 2013-08-02 11:25:48 -04:00
John Bintz de06f3e3e3 add bundler encoding fix, bump version 2013-07-25 15:26:00 -04:00
John Bintz eeb743467d handle modern bundler :github syntax for gem property with default penchant files 2013-01-24 10:06:49 -05:00
John Bintz 2250d629be tiny refactor and version bump 2012-10-09 15:22:02 -04:00
John Bintz fd619a5e6f a little more refactoring 2012-10-09 14:52:14 -04:00
John Bintz cd072c1d4c more refactoring 2012-10-09 14:45:27 -04:00
John Bintz 4f88e3977f more cleanup 2012-10-09 14:42:07 -04:00
John Bintz 9e9ce113cd refactor the most complex thing in the app, in preparation of more cleaning 2012-10-09 14:33:12 -04:00
John Bintz 0ba33a008d remove last vestiges of erb support 2012-10-09 11:29:17 -04:00
John Bintz a3fa16b6a7 missed a refactor 2012-10-09 11:26:22 -04:00
John Bintz 152067f925 factor out some defaults handling 2012-10-09 11:24:12 -04:00
John Bintz 7af1e933b2 more refactoring 2012-10-09 11:18:56 -04:00
John Bintz 003c7ab2cb refactor a little 2012-10-09 11:12:59 -04:00
John Bintz f7b1cc1b9c Merge branch 'master' of github.com:johnbintz/penchant
Merge another commit i made.
2012-10-09 11:09:01 -04:00
John Bintz 50e8222254 rip out erb processing and unnecessary specs 2012-10-09 11:08:18 -04:00
John Bintz 17ea7c0464 readme update 2012-10-06 17:07:08 -04:00
John Bintz 439218ebd3 bump version 2012-10-06 16:44:08 -04:00
John Bintz a0f610602b support the ruby version option 2012-10-06 16:42:51 -04:00
John Bintz dacc9d9334 bump version 2012-10-04 10:45:29 -04:00
John Bintz 55e31dd47f fix a bug with command order 2012-10-04 10:43:38 -04:00
John Bintz a1e9874b30 add opposites feature to cut down on repetition 2012-09-14 10:44:44 -04:00
John Bintz 832ec8f30c support for local bundling only 2012-09-04 14:38:19 -04:00
John Bintz 635e07a112 add opposite env feature for less repetition 2012-08-29 11:31:13 -04:00
John Bintz d5403f1139 make hooks a little smarter and forgiving 2012-08-27 09:40:41 -04:00
John Bintz 5cdbdd4ffc make install also install a gemfile if you don't have one 2012-08-21 10:43:01 -04:00
John Bintz b7626a1107 add the preflight_check rake task check/usage to pre-commit 2012-08-15 16:17:21 -04:00
John Bintz 3ef5122246 make converted gemfiles more useful out of the box 2012-08-13 16:20:50 -04:00
John Bintz df0154a225 run bundle update if it's needed, with the option to turn that off 2012-08-07 10:55:55 -04:00
John Bintz b29794152f shut up when no script/hooks dir, but there is a .git dir 2012-08-02 12:18:33 -04:00
John Bintz 3761c79d25 force install of git hooks in gemfile.penchant if so desired 2012-07-25 08:50:36 -04:00
John Bintz 01ac9bf14e more workarounds for apple's stupid default git client 2012-07-24 11:07:42 -04:00
John Bintz 10be919b0a detect git hook installation status 2012-07-24 10:48:11 -04:00
John Bintz 423fb66175 update docs a bit 2012-07-23 10:34:03 -04:00
John Bintz 64d4b5a323 ensure git hooks not installed if no git repo found 2012-07-23 10:25:42 -04:00
John Bintz 6679a65cdf support properties as hashes, too 2012-07-10 06:38:02 -04:00
John Bintz 1ac9b52ae2 bump version 2012-07-05 18:33:08 -04:00
John Bintz 9713be8dfd add property support, too 2012-07-05 18:27:59 -04:00
John Bintz dbc98424d8 docs for new features 2012-07-05 09:52:02 -04:00
John Bintz 4f15f45492 fix defaults precedence for gems 2012-07-03 11:14:43 -04:00
John Bintz fbbf14d639 make sure dependencies get installed and RVM installation instructions 2012-06-21 16:18:48 -04:00
John Bintz 387495873e bump version 2012-06-21 15:26:46 -04:00
John Bintz 3fc02e9180 make sure git repo discovery digs into any environment [ice cream] 2012-06-21 15:25:46 -04:00
John Bintz dfbe5a7ccf update some docs 2012-06-21 15:15:19 -04:00
John Bintz 3a2bde9a7b bump version 2012-06-20 13:26:36 -04:00
John Bintz 98239ee141 defaults for things in environments 2012-06-20 13:25:43 -04:00
John Bintz 08803b76ce defaults_for 2012-06-20 09:34:47 -04:00
John Bintz bc09df6bce gem by itself handles version now 2012-06-11 16:46:34 -04:00
John Bintz 1b97fa6078 fix erb processing for good 2012-06-08 15:25:04 -04:00
John Bintz b3274c7e34 fix bug in erb processing for env blocks 2012-06-08 15:14:59 -04:00
John Bintz f4cc4ec0a7 wow, even cooler\! 2012-06-07 10:48:27 -04:00
John Bintz 7f2e0d6301 ensure gemspec works 2012-06-06 13:22:28 -04:00
John Bintz 66d8886205 another readme tweak 2012-06-06 11:28:55 -04:00
John Bintz b60e60598c another readme tweak 2012-06-06 11:26:26 -04:00
John Bintz 6c35253266 update version and readme 2012-06-06 11:25:15 -04:00
John Bintz 34a7e04ed4 basic os support 2012-06-06 11:20:02 -04:00
John Bintz 011c53b5eb rubygems blew up 2012-06-05 15:39:17 -04:00
John Bintz 1a5f0142df yeah it's all consolidated now 2012-06-05 15:29:27 -04:00
John Bintz 033ba8a54c fix tests 2012-06-05 08:20:48 -04:00
John Bintz 47d8d3c52c support pure ruby gemfile.penchant 2012-06-05 07:50:33 -04:00
John Bintz c858dcdd69 a new way to manage lists of gems with commoon things 2012-06-04 15:39:22 -04:00
John Bintz 9e0bfa722e oops 2012-04-20 14:52:57 -04:00
John Bintz 9c3a0dfa8f bump version 2012-04-20 11:34:45 -04:00
John Bintz cabaea5ac3 update post-commit, fixed the git issue 2012-04-20 11:34:05 -04:00
John Bintz 57fc9a0e99 bump version 2012-04-20 11:26:45 -04:00
John Bintz 95a450cce2 oops 2012-04-20 11:26:27 -04:00
John Bintz e23aae423d merge 2012-04-20 11:13:43 -04:00
John Bintz 36cc4d6504 move things around 2012-04-20 11:12:56 -04:00
John Bintz 2112153b85 fix spec 2012-04-18 15:49:08 -04:00
John Bintz e465b3bbe4 bump version 2012-04-18 15:46:47 -04:00
John Bintz ad0b2499ef Merge branch 'master' of github.com:johnbintz/penchant 2012-04-18 13:23:16 -04:00
John Bintz b8e03e9acd only do deployment gemfile generation after successful remote-only generation. good for ripping out testing libraries from deploys, but still keeping them around for running the default rake task 2012-04-18 13:22:38 -04:00
John Bintz b30d56819e implement switching back to prior env on post-commit, should fix #2 2012-01-09 10:48:47 -05:00
John Bintz 9354fed2dc add post-commit script 2011-10-13 14:27:02 -04:00
John Bintz 25f9f7e3fb also redo the pre-commit hook 2011-10-13 10:30:33 -04:00
John Bintz bf69d6ee6d update things and docs and stuff 2011-10-13 10:01:18 -04:00
John Bintz 5fe4778e6b add no_deployment option 2011-10-13 09:45:37 -04:00
John Bintz f4486a9fd2 bump version 2011-09-14 14:45:30 -04:00
John Bintz af74214e34 update docs and fix a regex 2011-09-09 14:41:16 -04:00
John Bintz f7738f3d42 Merge branch 'master' of github.com:johnbintz/penchant 2011-09-09 14:15:59 -04:00
John Bintz 041fe931e2 duh, fix stupid bug with convert 2011-09-09 14:15:38 -04:00
John Bintz c5b6480996 fix a thing 2011-09-06 15:36:20 -04:00
John Bintz 459a391260 fix 2011-08-31 21:40:51 -04:00
John Bintz 3801299847 change how dotfiles are handled 2011-08-31 21:39:50 -04:00
John Bintz f8e5c9fcf7 update docs for dotfile 2011-08-31 14:43:43 -04:00
John Bintz b4e79b5daa rake support 2011-08-31 14:11:05 -04:00
John Bintz c4407f1690 start work on dotfile hook suttport 2011-08-31 14:03:48 -04:00
John Bintz 508597a9be haha i need to write tests for this 2011-08-19 14:04:13 -04:00
John Bintz 2a29b24477 make penchant gemfile pass through to bundler when possible, for drop-in replacement 2011-08-19 14:01:34 -04:00
John Bintz 7f97c10cd3 nicer behavior 2011-08-19 13:57:11 -04:00
John Bintz 6ec550ead7 bump version, that was quick 2011-08-18 17:46:38 -04:00
John Bintz f59463d31a bump version, eat own dog food, and fix a problem 2011-08-18 17:45:59 -04:00
John Bintz 7d90d5fd47 readme update 2011-08-18 17:39:59 -04:00
John Bintz 892ad17e4e make things betterer 2011-08-18 17:37:54 -04:00
John Bintz 7c0f49e931 update git hook 2011-08-18 14:53:55 -04:00
56 changed files with 1687 additions and 200 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
.bundle
Gemfile.lock
pkg/*
.DS_Store

View File

@ -5,6 +5,8 @@ gemspec
gem 'guard'
gem 'guard-rspec'
gem 'mocha'
gem 'fakefs'
gem 'rspec', '~> 2.6.0'
gem 'guard-cucumber'
# see, *this* is why you need penchant
#gem 'cuke-pack', :path => '../cuke-pack'
gem 'cuke-pack', :git => 'git://github.com/johnbintz/cuke-pack.git'

View File

@ -1,9 +1,21 @@
# A sample Guardfile
# More info at https://github.com/guard/guard#readme
guard 'rspec', :cli => '-c', :version => 2 do
watch(%r{^spec/.+_spec\.rb$})
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
watch('spec/spec_helper.rb') { "spec" }
group :rspec do
guard 'rspec', :cli => '-c', :version => 2 do
watch(%r{^spec/.+_spec\.rb$})
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
watch('spec/spec_helper.rb') { "spec" }
end
end
# added by cuke-pack
group :wip do
guard 'cucumber', :env => :cucumber, :cli => '-p wip' do
watch(%r{^features/.+.feature$})
watch(%r{^(app|lib).*}) { 'features' }
watch(%r{^features/support/.+$}) { 'features' }
watch(%r{^features/step_definitions/(.+).rb$}) { 'features' }
end
end

241
README.md
View File

@ -2,9 +2,11 @@
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.
* If I'm developing gems alongside this project, I use a `Gemfile.erb` to get around the "one gem, one source" issue in
* 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.
@ -14,39 +16,238 @@ This gem makes that easier!
Installs a bunch of scripts into the `scripts` directory of your project:
* `gemfile` which switches between `Gemfile.erb` environments
* `gemfile` which switches between `Gemfile.penchant` environments
* `install-git-hooks` which will do just what it says
* `hooks/pre-commit`, one of the hooks the prior script installs
* `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
## Gemfile.penchant?!
Yeah, it's a `Gemfile` with some extras:
``` ruby
# Gemfile.penchant
source :rubygems
# ensure git hooks are installed when a gemfile is processed, see below
ensure_git_hooks!
# need the bundler UTF-8 fix? ask for it by name!
bundler_encoding_fix!
# 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://github.com/$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
end
env :remote do
# expands to:
#
# gem 'flowerbox', :git => 'git://github.com/johnbintz/flowerbox.git', :require => nil
# gem 'guard-flowerbox', :git => 'git://github.com/johnbintz/guard-flowerbox.git', :require => nil
gems dev_gems, :github => 'johnbintz'
end
# an even shorter way to specify environments!
# in remote env, expands to:
# gem 'bullseye', :git => 'git://github.com/johnbintz/bullseye.git'
# in local env, expands to:
# gem 'bullseye', :path => '../bullseye'
env :remote, :opposite => :local do
gem 'bullseye', :github => 'johnbintz'
end
# only expanded on Mac OS X
os :darwin do
gem 'rb-fsevent'
end
# only expanded on Linux
os :linux do
gems 'rb-inotify', 'ffi'
end
end
end
```
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:
``` ruby
defaults_for env(:local), :path => '../%s'
opposites :local, :remote
env :remote do
gem 'my-gem', :git => 'git://github.com/johnbintz/my-gem.git'
end
```
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:
``` ruby
gem 'rails'
no_deployment do
os :darwin do
gems 'growl_notify', 'growl', 'rb-fsevent'
end
os :linux do
gem 'libnotify', :require => nil
end
group :test do
# ... all your testing libraries you won't need on the deployed end ...
end
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 rubygems.org 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`:
``` ruby
env :local do
gem 'my-gem', :path => '../%s'
end
env :remote do
gem 'my-gem', :git => 'git://github.com/johnbintz/%s.git'
end
```
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.
## initialize-environment
It will also try to run `rake bootstrap`, so add a `:bootstrap` task for things that should happen when you start going
(make databases, other stuff, etc, whatever).
Get new developers up to speed fast! `script/initialize-environment` does the following when run:
## Gemfile.erb?!
* 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.
Yeah, it's a `Gemfile` with ERB in it:
### After-`gemfile` hooks?
``` erb
<% if env == "local" %>
gem 'guard', :path => '../guard'
<% else %>
gem 'guard', :git => 'git://github.com/johnbintz/guard.git'
<% end %>
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:
``` ruby
# rake knows if you need "bundle exec" or not.
rake "hydra:sync hydra:remote:bundle"
```
Use `script/gemfile local` to get at the local ones, and `script/gemfile remote` (or anything, really) to get at the remote ones.
It then runs `bundle install`.
### What environment are you currently using in that Gemfile?
`head -n 1` that puppy, or `penchant gemfile-env`.
## git hook?!
It runs `script/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.
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.
## How?!
* `gem install penchant`
* No RVM? `gem install penchant`
* RVM? `rvm gemset use global && gem install penchant && rvm gemset use default`
* `cd` to your project directory
* `penchant install` (can do `--dir=WHEREVER`, too)
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)

View File

@ -1,2 +1,16 @@
require 'bundler'
Bundler::GemHelper.install_tasks
begin
require 'cucumber'
require 'cucumber/rake/task'
Cucumber::Rake::Task.new(:cucumber) do |t|
t.cucumber_opts = "features --format pretty"
end
rescue LoadError
"#$! - no cucumber"
end
task :default => [ :cucumber ]

View File

@ -3,28 +3,157 @@
require 'rubygems'
require 'thor'
require 'penchant'
require 'fileutils'
class PenchantCLI < Thor
include Thor::Actions
source_root File.expand_path('../..', __FILE__)
SCRIPT_DIR = 'script'
CLONE_DIR = '..'
desc "install", "Copy the common scripts to the project"
method_options :dir => 'script'
method_options :dir => SCRIPT_DIR
def install
directory 'template/script', options[:dir]
Dir[File.join(options[:dir], '**/*')].each { |file| File.chmod(0755, file) }
if File.directory?('.git')
Penchant::Hooks.install!
else
puts "No git repository detected here. Skipping git hook installation..."
end
if !File.file?('Gemfile') && !File.file?('Gemfile.penchant')
FileUtils.touch('Gemfile.penchant')
prepend_to_file 'Gemfile.penchant', <<-RB
source :rubygems
RB
install_gemfile_penchant
end
end
desc "gemfile ENV", "Switch the gemfile environment"
def gemfile(env)
Penchant::Gemfile.do_full_env_switch!(env)
desc "update", "Update the installed scripts"
method_options :dir => SCRIPT_DIR
def update
install
end
desc "convert", "Make an existing project Penchant-isized"
method_options :dir => SCRIPT_DIR
def convert
install
FileUtils.mv 'Gemfile', 'Gemfile.penchant'
install_gemfile_penchant
end
method_options :deployment => false
method_options :switch_back => false
method_options :no_auto_update => false
method_options :local => false
desc "gemfile ENV", "Switch the gemfile environment, or rebuild the current environment if not given"
def gemfile(env = get_current_env)
check_git_hooks!
if env
if options[:switch_back]
puts "[penchant] Switching back, fallback: #{env}..."
Penchant::Gemfile.switch_back!(env)
else
puts "[penchant] Rebunding for #{env} environment#{options[:deployment] ? ", deployment mode" : ''}..."
Penchant::Gemfile.do_full_env_switch!(env, options[:deployment])
end
end
gemfile = Penchant::Gemfile.new
if !gemfile.has_gemfile?
puts "No Gemfile or Gemfile.penchant, exiting."
exit 1
end
command = %{bundle}
command << " --local" if options[:local]
system command
# it's asking for bundle update, we know what we're doing
if $?.exitstatus == 6 and !options[:no_auto_update]
command = %{bundle update}
command << " --local" if options[:local]
system command
end
end
desc "gemfile-env", "Get the gemfile environment"
def gemfile_env
gemfile = Penchant::Gemfile.new
puts gemfile.environment
puts get_current_env
end
desc "bootstrap [DIR = #{CLONE_DIR}]", "Download all referred-to git repos to the specified directory"
def bootstrap(dir = CLONE_DIR)
Penchant::Gemfile.defined_git_repos.each do |repo|
puts "Cloning #{repo} to #{dir}"
repo.clone_to(dir)
end
end
def method_missing(method, *args)
if Penchant::Gemfile.available_environments.include?(method)
gemfile(method, *args)
else
super(method, *args)
end
end
no_tasks do
def get_current_env
gemfile = Penchant::Gemfile.new
out = [ gemfile.environment ]
out << "deployment" if gemfile.deployment?
out.join(' ')
end
def check_git_hooks!
if !Penchant::Hooks.installed?
puts "[penchant] git hooks not installed. Run script/install-git-hooks."
puts
end
end
def install_gemfile_penchant
prepend_to_file 'Gemfile.penchant', <<-RB
# ensure git hooks are always installed
ensure_git_hooks!
# everything in the :local env is assumed to be a sibling directory of this one
defaults_for env(:local), :path => '../%s'
# reference a github repository with gem 'my-gem', :github => 'username'
# also supports modern bundler user/repo syntax
property(:github) { |name|
parts = name.split('/')
url = case parts.length
when 1
"git://github.com/\#{name}/%s.git"
when 2
"git://github.com/\#{parts.first}/\#{parts.last}.git"
end
{ :git => url }
}
RB
gemfile(:remote)
end
end
default_task :gemfile
end
PenchantCLI.start

8
config/cucumber.yml Normal file
View File

@ -0,0 +1,8 @@
<%
std_opts = "-r features --format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} -f Cucumber::StepWriter --out features/step_definitions --strict"
%>
default: <%= std_opts %> features
wip: <%= std_opts %> --tags @wip features
precommit: FAILFAST=true <%= std_opts %> --tags ~@wip:0 features
cleanup: <%= std_opts %> -f Cucumber::CleanupFormatter --out unused.txt features

77
features/cli.feature Normal file
View File

@ -0,0 +1,77 @@
Feature: CLI
Scenario: Switch back to the original pre-deployment environment
Given I have the file "tmp/Gemfile.penchant" with the content:
"""
gem 'rake'
"""
And I have the file "tmp/Gemfile" with the content:
"""
# generated by penchant, environment: production, deployment mode (was local)
"""
When I run "bin/penchant gemfile other --switch-back" in the "tmp" directory
Then the file "tmp/Gemfile" should have the following content:
"""
# generated by penchant, environment: local
gem "rake"
"""
And the output should include "fallback: other"
Scenario: Try to convert a project, ignoring git hooks
Given I have the file "tmp/Gemfile" with the content:
"""
source :rubygems
"""
When I run "bin/penchant convert" in the "tmp" directory
Then the file "tmp/Gemfile.penchant" should include the following content:
"""
source :rubygems
"""
And the output should include "No git"
Scenario: Run in a project where the git hooks are not set up
Given I have the file "tmp/Gemfile.penchant" with the content:
"""
gem 'rake'
"""
Given I have the file "tmp/script/hooks/pre-commit" with the content:
"""
a penchant hook
"""
When I run "bin/penchant gemfile remote" in the "tmp" directory
Then the output should include "git hooks not installed"
Scenario: Run in a project where there are no git hooks, but there is a git repo
Given I have the file "tmp/Gemfile.penchant" with the content:
"""
gem 'rake'
"""
Given I have the directory "tmp/.git"
When I run "bin/penchant gemfile remote" in the "tmp" directory
Then the output should not include "git hooks not installed"
Scenario: Run in a project where git hooks are set up
Given I have the file "tmp/Gemfile.penchant" with the content:
"""
gem 'rake'
"""
Given I have the file "tmp/script/hooks/pre-commit" with the content:
"""
a penchant hook
"""
Given I have the symlink "tmp/.git/hooks/pre-commit" which points to "tmp/script/hooks/pre-commit"
When I run "bin/penchant gemfile remote" in the "tmp" directory
Then the output should not include "git hooks not installed"
Scenario: Install Penchant into a directory with no Gemfile
Given I have the directory "tmp"
When I run "bin/penchant install" in the "tmp" directory
Then the file "tmp/Gemfile.penchant" should include the following content:
"""
source :rubygems
"""
Then the file "tmp/Gemfile" should include the following content:
"""
source :rubygems
"""
And the output should include "No git"

View File

@ -0,0 +1,441 @@
@fakefs
Feature: Gemfiles
Scenario: Process a pure Ruby gemfile
Given I have the file "Gemfile.penchant" with the content:
"""
source :rubygems
gemspec
group :cats, :dogs do
case environment
when :local
gem 'test', :path => '../test'
when :remote
gem 'test', :git => 'git://github.com/johnbintz/test.git'
end
end
"""
When I rebuild the Gemfile for "local" mode
Then the file "Gemfile" should have the following content:
"""
# generated by penchant, environment: local
source :rubygems
gemspec
group :cats, :dogs do
gem "test", {:path=>"../test"}
end
"""
When I rebuild the Gemfile for "remote" mode
Then the file "Gemfile" should have the following content:
"""
# generated by penchant, environment: remote
source :rubygems
gemspec
group :cats, :dogs do
gem "test", {:git=>"git://github.com/johnbintz/test.git"}
end
"""
Scenario: Use a gemlist
Given I have the file "Gemfile.penchant" with the content:
"""
gems 'one', 'two', 'three', :path => '../%s'
"""
When I rebuild the Gemfile for "local" mode
Then the file "Gemfile" should have the following content:
"""
# generated by penchant, environment: local
gem "one", {:path=>"../one"}
gem "two", {:path=>"../two"}
gem "three", {:path=>"../three"}
"""
Scenario: Use an env block
Given I have the file "Gemfile.penchant" with the content:
"""
env :local do
gems 'one', 'two', 'three', :path => '../%s'
end
"""
When I rebuild the Gemfile for "local" mode
Then the file "Gemfile" should have the following content:
"""
# generated by penchant, environment: local
gem "one", {:path=>"../one"}
gem "two", {:path=>"../two"}
gem "three", {:path=>"../three"}
"""
When I rebuild the Gemfile for "remote" mode
Then the file "Gemfile" should have the following content:
"""
# generated by penchant, environment: remote
"""
Scenario: Skip deployment blocks
Given I have the file "Gemfile.penchant" with the content:
"""
no_deployment do
gem 'one'
end
"""
When I rebuild the Gemfile for "local" mode
Then the file "Gemfile" should have the following content:
"""
# generated by penchant, environment: local
gem "one"
"""
When I rebuild the Gemfile for "local" mode with deployment
Then the file "Gemfile" should have the following content:
"""
# generated by penchant, environment: local, deployment mode (was local)
"""
Scenario: Peel multiple hashes off a gemlist
Given I have the file "Gemfile.penchant" with the content:
"""
gems 'one', { :path => '../%s' }, { :require => nil }
"""
When I rebuild the Gemfile for "local" mode
Then the file "Gemfile" should have the following content:
"""
# generated by penchant, environment: local
gem "one", {:path=>"../one", :require=>nil}
"""
Scenario: Don't add an empty hash
Given I have the file "Gemfile.penchant" with the content:
"""
gems 'one'
"""
When I rebuild the Gemfile for "local" mode
Then the file "Gemfile" should have the following content:
"""
# generated by penchant, environment: local
gem "one"
"""
Scenario: Single gem gets processed like a gems list
Given I have the file "Gemfile.penchant" with the content:
"""
gem 'one', '1.2.3', :path => '../%s'
"""
When I rebuild the Gemfile for "local" mode
Then the file "Gemfile" should have the following content:
"""
# generated by penchant, environment: local
gem "one", "1.2.3", {:path=>"../one"}
"""
@mocha
Scenario: OS-specific blocks
Given I have the file "Gemfile.penchant" with the content:
"""
os :darwin do
gem 'one', :path => '../%s'
end
"""
And I am on the "darwin" platform
When I rebuild the Gemfile for "local" mode
Then the file "Gemfile" should have the following content:
"""
# generated by penchant, environment: local
gem "one", {:path=>"../one"}
"""
Given I am on the "linux" platform
When I rebuild the Gemfile for "local" mode
Then the file "Gemfile" should have the following content:
"""
# generated by penchant, environment: local
"""
Scenario: Get the list of environments defined
Given I have the file "Gemfile.penchant" with the content:
"""
env :cat do
gem 'one', :path => '../%s'
end
env :dog do
gem 'two', :path => '../%s'
end
"""
When I request the list of environments available
Then I should get the following environments:
| cat |
| dog |
Scenario: Get the list of git repos defined
Given I have the file "Gemfile.penchant" with the content:
"""
gem 'one', :path => '../%s'
gem 'two', :git => 'git://github.cats/%s.git'
"""
When I request the list of git repositories
Then I should get the following repositories:
| git://github.cats/two.git |
Scenario: Get the list of git repos defined, regardless of environment
Given I have the file "Gemfile.penchant" with the content:
"""
gem 'one', :path => '../%s'
env :remote do
gem 'two', :git => 'git://github.cats/%s.git'
end
"""
When I request the list of git repositories
Then I should get the following repositories:
| git://github.cats/two.git |
Scenario: Propose defaults for a gem
Given I have the file "Gemfile.penchant" with the content:
"""
defaults_for 'one', :path => '../%s'
gem 'one', '1.2.3'
"""
When I rebuild the Gemfile for "local" mode
Then the file "Gemfile" should have the following content:
"""
# generated by penchant, environment: local
gem "one", "1.2.3", {:path=>"../one"}
"""
Scenario: Propose defaults for an array of gems
Given I have the file "Gemfile.penchant" with the content:
"""
defaults_for ['one'], :path => '../%s'
gem 'one', '1.2.3'
"""
When I rebuild the Gemfile for "local" mode
Then the file "Gemfile" should have the following content:
"""
# generated by penchant, environment: local
gem "one", "1.2.3", {:path=>"../one"}
"""
Scenario: Propose defaults for an environment
Given I have the file "Gemfile.penchant" with the content:
"""
defaults_for env(:local), :path => '../%s'
env :local do
gem 'one', '1.2.3'
end
"""
When I rebuild the Gemfile for "local" mode
Then the file "Gemfile" should have the following content:
"""
# generated by penchant, environment: local
gem "one", "1.2.3", {:path=>"../one"}
"""
Scenario: Create opposite environment gem assumptions to cut down on repetition
Given I have the file "Gemfile.penchant" with the content:
"""
defaults_for env(:local), :path => '../%s'
property(:github) { |name| { :git => "git://github.com/#{name}/%s.git" } }
env :remote, :opposite => :local do
gem 'one', :github => 'johnbintz', :require => nil
end
env :local, :opposite => :remote do
gem 'two', :path => '../%s', :require => nil
end
"""
When I rebuild the Gemfile for "local" mode
Then the file "Gemfile" should have the following content:
"""
# generated by penchant, environment: local
gem "one", {:path=>"../one", :require=>nil}
gem "two", {:path=>"../two", :require=>nil}
"""
When I rebuild the Gemfile for "remote" mode
Then the file "Gemfile" should have the following content:
"""
# generated by penchant, environment: remote
gem "one", {:git=>"git://github.com/johnbintz/one.git", :require=>nil}
gem "two", {:require=>nil}
"""
Scenario: Set the opposite environment in the environment defaults
Given I have the file "Gemfile.penchant" with the content:
"""
defaults_for env(:local), :path => '../%s', :opposite => :remote
defaults_for env(:remote), :opposite => :local
property(:github) { |name| { :git => "git://github.com/#{name}/%s.git" } }
env :remote do
gem 'one', :github => 'johnbintz', :require => nil
end
env :local do
gem 'two', :path => '../%s', :require => nil
end
"""
When I rebuild the Gemfile for "local" mode
Then the file "Gemfile" should have the following content:
"""
# generated by penchant, environment: local
gem "one", {:path=>"../one", :require=>nil}
gem "two", {:path=>"../two", :require=>nil}
"""
When I rebuild the Gemfile for "remote" mode
Then the file "Gemfile" should have the following content:
"""
# generated by penchant, environment: remote
gem "one", {:git=>"git://github.com/johnbintz/one.git", :require=>nil}
gem "two", {:require=>nil}
"""
Scenario: Define a pair of opposites
Given I have the file "Gemfile.penchant" with the content:
"""
defaults_for env(:local), :path => '../%s'
opposites :local, :remote
property(:github) { |name| { :git => "git://github.com/#{name}/%s.git" } }
env :remote do
gem 'one', :github => 'johnbintz', :require => nil
end
env :local do
gem 'two', :path => '../%s', :require => nil
end
"""
When I rebuild the Gemfile for "local" mode
Then the file "Gemfile" should have the following content:
"""
# generated by penchant, environment: local
gem "one", {:path=>"../one", :require=>nil}
gem "two", {:path=>"../two", :require=>nil}
"""
When I rebuild the Gemfile for "remote" mode
Then the file "Gemfile" should have the following content:
"""
# generated by penchant, environment: remote
gem "one", {:git=>"git://github.com/johnbintz/one.git", :require=>nil}
gem "two", {:require=>nil}
"""
Scenario: Define a pair of opposites in the other order
Given I have the file "Gemfile.penchant" with the content:
"""
opposites :local, :remote
defaults_for env(:local), :path => '../%s'
property(:github) { |name| { :git => "git://github.com/#{name}/%s.git" } }
env :remote do
gem 'one', :github => 'johnbintz', :require => nil
end
env :local do
gem 'two', :path => '../%s', :require => nil
end
"""
When I rebuild the Gemfile for "local" mode
Then the file "Gemfile" should have the following content:
"""
# generated by penchant, environment: local
gem "one", {:path=>"../one", :require=>nil}
gem "two", {:path=>"../two", :require=>nil}
"""
When I rebuild the Gemfile for "remote" mode
Then the file "Gemfile" should have the following content:
"""
# generated by penchant, environment: remote
gem "one", {:git=>"git://github.com/johnbintz/one.git", :require=>nil}
gem "two", {:require=>nil}
"""
Scenario: Override defaults for an environment
Given I have the file "Gemfile.penchant" with the content:
"""
defaults_for env(:local), :path => '../%s'
env :local do
gem 'one', '1.2.3', :path => '../cats'
end
"""
When I rebuild the Gemfile for "local" mode
Then the file "Gemfile" should have the following content:
"""
# generated by penchant, environment: local
gem "one", "1.2.3", {:path=>"../cats"}
"""
Scenario: Define special expansions for properties
Given I have the file "Gemfile.penchant" with the content:
"""
defaults_for env(:local), :path => '../%s'
property(:github) { |name| { :git => "git://github.com/#{name}/%s.git" } }
gem 'one', :github => 'john'
"""
When I rebuild the Gemfile for "local" mode
Then the file "Gemfile" should have the following content:
"""
# generated by penchant, environment: local
gem "one", {:git=>"git://github.com/john/one.git"}
"""
Scenario: Define special expansions for properties as a hash
Given I have the file "Gemfile.penchant" with the content:
"""
defaults_for env(:local), :path => '../%s'
property :github, :git => "git://github.com/$1/%s.git"
gem 'one', :github => 'john'
"""
When I rebuild the Gemfile for "local" mode
Then the file "Gemfile" should have the following content:
"""
# generated by penchant, environment: local
gem "one", {:git=>"git://github.com/john/one.git"}
"""
@mocha
Scenario: Force installing of git hooks if the gemfile asks for it
Given I expect git hooks to be installed
Given I have the file "Gemfile.penchant" with the content:
"""
ensure_git_hooks!
gem 'one'
"""
When I rebuild the Gemfile for "local" mode
Then the file "Gemfile" should have the following content:
"""
# generated by penchant, environment: local
gem "one"
"""
Scenario: Pass through the Ruby version
Given I have the file "Gemfile.penchant" with the content:
"""
ruby '1.9.3'
"""
When I rebuild the Gemfile for "local" mode
Then the file "Gemfile" should have the following content:
"""
# generated by penchant, environment: local
ruby "1.9.3"
"""
Scenario: Ask for the Gemfile encoding fix
Given I have the file "Gemfile.penchant" with the content:
"""
bundler_encoding_fix!
"""
When I rebuild the Gemfile for "local" mode
Then the file "Gemfile" should have the following content:
"""
# generated by penchant, environment: local
if RUBY_VERSION =~ /1.9/
Encoding.default_external = Encoding::UTF_8
Encoding.default_internal = Encoding::UTF_8
end
"""

View File

@ -0,0 +1,3 @@
Given /^I am on the "([^"]*)" platform$/ do |os|
Penchant::FileProcessor.any_instance.stubs(:current_os).returns(os.to_sym)
end

View File

@ -0,0 +1,3 @@
Given /^I expect git hooks to be installed$/ do
Penchant::Hooks.expects(:install!)
end

View File

@ -0,0 +1,3 @@
Given /^I have the directory "(.*?)"$/ do |dir|
FileUtils.mkdir_p dir
end

View File

@ -0,0 +1,5 @@
Given /^I have the file "([^"]*)" with the content:$/ do |file, string|
FileUtils.mkdir_p File.dirname(file)
File.open(file, 'wb') { |fh| fh.print string }
end

View File

@ -0,0 +1,4 @@
Given /^I have the symlink "(.*?)" which points to "(.*?)"$/ do |source, target|
FileUtils.mkdir_p(File.dirname(source))
File.symlink(target, source)
end

View File

@ -0,0 +1,3 @@
Then /^I should get the following environments:$/ do |table|
@environments.collect(&:to_s).sort.should == table.raw.flatten.sort
end

View File

@ -0,0 +1,4 @@
Then /^I should get the following repositories:$/ do |table|
@repos.collect(&:to_s).sort.should == table.raw.flatten.sort
end

View File

@ -0,0 +1,9 @@
Then /^the file "(.*?)" should have the following stripped content:$/ do |file, string|
test_lines = string.lines.to_a
File.read(file).lines.collect(&:strip).reject(&:empty?).to_a.each do |line|
raise StandardError.new if test_lines.empty?
line.strip.should == test_lines.shift.strip
end
end

View File

@ -0,0 +1,3 @@
Then /^the file "([^"]*)" should have the following content:$/ do |file, string|
File.read(file).should == string
end

View File

@ -0,0 +1,3 @@
Then /^the file "(.*?)" should include the following content:$/ do |file, string|
File.read(file).should include(string)
end

View File

@ -0,0 +1,3 @@
Then /^the output should include "([^"]*)"$/ do |text|
@output.should include(text)
end

View File

@ -0,0 +1,3 @@
Then /^the output should not include "(.*?)"$/ do |text|
@output.should_not include(text)
end

View File

@ -0,0 +1,4 @@
When /^I rebuild the Gemfile for "(.*?)" mode$/ do |env|
Penchant::Gemfile.do_full_env_switch!(env)
end

View File

@ -0,0 +1,3 @@
When /^I rebuild the Gemfile asking to switch back to the previous state$/ do
Penchant::Gemfile.switch_back!("remote")
end

View File

@ -0,0 +1,3 @@
When /^I rebuild the Gemfile for "([^"]*)" mode with deployment$/ do |env|
Penchant::Gemfile.do_full_env_switch!(env, true)
end

View File

@ -0,0 +1,3 @@
When /^I request the list of environments available$/ do
@environments = Penchant::Gemfile.available_environments
end

View File

@ -0,0 +1,3 @@
When /^I request the list of git repositories$/ do
@repos = Penchant::Gemfile.defined_git_repos
end

View File

@ -0,0 +1,3 @@
When /^I run "([^"]*)" in the "([^"]*)" directory$/ do |command, dir|
@output = %x{bash -c 'opwd=$PWD; cd #{dir} && $opwd/#{command}'}
end

View File

@ -0,0 +1,20 @@
require 'cuke-pack/support/pause'
require 'cuke-pack/support/pending'
Before do
# if you want pending steps to pause before marking the step as pending,
# set @pause_ok to true
@pause_ok = false
end
require 'cuke-pack/support/step_writer'
require 'cuke-pack/support/wait_for'
require 'cuke-pack/support/failfast'
# set the level of flaying on the step definitions
# set it to false to skip flaying
flay_level = 32
require 'cuke-pack/support/flay'

27
features/support/env.rb Normal file
View File

@ -0,0 +1,27 @@
require 'fakefs/safe'
require 'penchant'
require 'mocha'
World(Mocha::API)
Before('@fakefs') do
FakeFS.activate!
end
Before('@mocha') do
mocha_setup
end
After do
FakeFS::FileSystem.clear
FakeFS.deactivate!
begin
mocha_verify
ensure
mocha_teardown
end
FileUtils.rm_rf 'tmp'
end

View File

@ -1,3 +1,13 @@
module Penchant
autoload :Gemfile, 'penchant/gemfile'
autoload :Repo, 'penchant/repo'
autoload :DotPenchant, 'penchant/dot_penchant'
autoload :Hooks, 'penchant/hooks'
autoload :Env, 'penchant/env'
autoload :FileProcessor, 'penchant/file_processor'
autoload :Defaults, 'penchant/defaults'
autoload :CustomProperty, 'penchant/custom_property'
autoload :PropertyStack, 'penchant/property_stack'
autoload :PropertyStackBuilder, 'penchant/property_stack_builder'
autoload :PropertyStackProcessor, 'penchant/property_stack_processor'
end

View File

@ -0,0 +1,20 @@
module Penchant
class CustomProperty
def initialize(value)
@value = value
end
def process(values)
if @value.respond_to?(:call)
@value.call(*values).to_a
else
@value.collect do |k, v|
v = v.dup.gsub(%r{\$(\d+)}) { |m| values[m.to_i - 1 ] }
[ k, v ]
end
end
end
end
end

11
lib/penchant/defaults.rb Normal file
View File

@ -0,0 +1,11 @@
module Penchant
class Defaults
def initialize
@defaults = {}
end
def [](key)
@defaults[key.to_s] ||= {}
end
end
end

View File

@ -0,0 +1,27 @@
module Penchant
class DotPenchant
class << self
def run(env = nil, deployment = false)
dot_penchant = new
dot_penchant.run(env)
dot_penchant
end
end
def run(env = nil, deployment = false)
instance_eval(File.read('.penchant'))
end
def rake(*tasks)
command = [ "rake", *tasks ]
command.unshift("bundle exec") if gemfile?
Kernel.system command.join(' ')
end
private
def gemfile?
File.file?('Gemfile')
end
end
end

18
lib/penchant/env.rb Normal file
View File

@ -0,0 +1,18 @@
module Penchant
class Env
attr_accessor :name
def initialize(name)
@name = name.to_s
end
def ==(other)
@name == other.name
end
def to_s
"@#{name}"
end
end
end

View File

@ -0,0 +1,210 @@
module Penchant
class FileProcessor
attr_reader :environment, :is_deployment, :available_environments, :defined_git_repos
ANY_ENVIRONMENT = :any_environment
def self.result(data, *args)
new(data).result(*args)
end
def initialize(data)
@data = data
@available_environments = []
@defined_git_repos = []
@defaults = Defaults.new
@properties = PropertyStackBuilder.new(@defaults)
@_current_env_defaults = {}
end
def result(_env, _is_deployment)
@environment = _env.to_s.to_sym
@is_deployment = _is_deployment
@output = []
instance_eval(@data)
@output.join("\n")
end
def <<(string)
@output << string
end
def env(*args)
options = {}
options = args.pop if args.last.kind_of?(::Hash)
@available_environments += args
requested_env_defaults = _defaults_for(Env.new(environment))
if block_given?
if for_environment?(args)
@_current_env_defaults = requested_env_defaults
yield
@_current_env_defaults = {}
else
if opposite_environment = (options[:opposite] or requested_env_defaults[:opposite])
if for_environment?([ environment, args, opposite_environment ].flatten.uniq)
@_current_env_defaults = requested_env_defaults
@_strip_pathing_options = true
yield
@_strip_pathing_options = false
@_current_env_defaults = {}
end
end
end
else
Env.new(args.shift)
end
end
def property(name, hash = nil, &block)
@properties[name] = hash || block
end
def opposites(left, right)
@defaults[Env.new(left)][:opposite] = right
@defaults[Env.new(right)][:opposite] = left
end
def for_environment?(envs)
envs.include?(environment) || environment == ANY_ENVIRONMENT
end
def no_deployment
yield if !is_deployment
end
def ensure_git_hooks!
Penchant::Hooks.install!
end
def os(*args)
yield if args.include?(current_os)
end
def defaults_for(*args)
defaults = args.pop
args.flatten.each do |gem|
@defaults[gem].merge!(defaults)
end
end
protected
def args_to_string(args)
args.inspect[1..-2]
end
def split_args(args)
template = {}
while args.last.instance_of?(Hash)
template.merge!(args.pop)
end
[ args, template ]
end
def call_and_indent_output(block = nil, &given_block)
index = @output.length
(block || given_block).call
index.upto(@output.length - 1) do |i|
@output[i] = " " + @output[i]
end
end
def _defaults_for(gem_name)
result = @_current_env_defaults
result.merge(@defaults[gem_name] || {})
end
def current_os
require 'rbconfig'
case host_os = RbConfig::CONFIG['host_os']
when /darwin/
:darwin
when /linux/
:linux
else
host_os[%r{^[a-z]+}, 1].to_sym
end
end
def gem(*args)
gem_name = [ args.shift ]
template = {}
if args.last.kind_of?(::Hash)
template = args.pop
end
version = args.first
options = @properties.create_stack_for(template, @_strip_pathing_options).process_for_gem(gem_name.first, @_current_env_defaults)
args = [ gem_name.first ]
args << version if version
if options[:git]
@defined_git_repos << Penchant::Repo.new(options[:git])
end
args << options if !options.empty?
self << %{gem #{args_to_string(args)}}
end
def gems(*args)
gems, template = split_args(args)
gems.flatten.each do |gem_name|
options = @properties.create_stack_for(template, @_strip_pathing_options).process_for_gem(gem_name)
args = [ gem_name ]
args << options if !options.empty?
gem *args
end
end
def group(*args, &block)
self << ""
self << %{group #{args_to_string(args)} do}
call_and_indent_output(block)
self << %{end}
end
def ruby(*args)
passthrough :ruby, *args
end
def bundler_encoding_fix!
self << (<<-RB)
if RUBY_VERSION =~ /1.9/
Encoding.default_external = Encoding::UTF_8
Encoding.default_internal = Encoding::UTF_8
end
RB
end
def gemspec
passthrough :gemspec
end
def source(*args)
passthrough :source, *args
end
def passthrough(method, *args)
self << %{#{method} #{args_to_string(args)}}.strip
end
end
end

View File

@ -1,24 +1,40 @@
require 'erb'
module Penchant
class Gemfile
attr_reader :path
attr_reader :path, :is_deployment
class << self
def do_full_env_switch!(env)
gemfile = Penchant::Gemfile.new
if !gemfile.has_gemfile_erb?
puts "Not using Gemfile.erb, exiting."
return false
end
def self.do_full_env_switch!(env, deployment = false)
return false if !(gemfile = pre_switch(env, deployment))
gemfile.switch_to!(env)
system %{bundle}
end
gemfile.switch_to!(env, deployment)
end
def self.switch_back!(fallback_env)
return false if !(gemfile = pre_switch(fallback_env))
gemfile.switch_back!(fallback_env)
end
def self.pre_switch(env, deployment = false)
gemfile = new
return false if !gemfile.has_processable_gemfile?
gemfile.run_dot_penchant!(env, deployment)
gemfile
end
def self.available_environments
new.available_environments
end
def self.defined_git_repos
new.defined_git_repos
end
def current_env ; @env ; end
def initialize(path = Dir.pwd)
@path = path
@env = environment
end
def gemfile_path
@ -26,30 +42,75 @@ module Penchant
end
def has_gemfile?
File.file?('Gemfile')
File.file?(gemfile_path)
end
def gemfile_erb_path
file_in_path('Gemfile.erb')
def has_dot_penchant?
File.file?('.penchant')
end
def has_gemfile_erb?
File.file?(gemfile_erb_path)
def gemfile_penchant_path
file_in_path('Gemfile.penchant')
end
def has_gemfile_penchant?
File.file?(gemfile_penchant_path)
end
def has_processable_gemfile?
has_gemfile_penchant?
end
def processable_gemfile_path
gemfile_penchant_path
end
def environment
File.readlines(gemfile_path).first.strip[%r{environment: (.*)}, 1]
gemfile_header.strip[%r{environment: ([^, ]*)}, 1]
end
def switch_to!(gemfile_env = nil)
@env = gemfile_env
template = File.read(gemfile_erb_path)
def deployment?
gemfile_header['deployment mode'] != nil
end
File.open(gemfile_path, 'wb') do |fh|
fh.puts "# generated by penchant, environment: #{@env}"
def available_environments
process
builder.available_environments
end
fh.print ERB.new(template).result(binding)
def defined_git_repos
process(FileProcessor::ANY_ENVIRONMENT)
builder.defined_git_repos
end
def switch_to!(gemfile_env = nil, deployment = false)
@env, @is_deployment = gemfile_env, deployment
output = [ header, process ]
File.open(gemfile_path, 'wb') { |fh| fh.print output.join("\n") }
end
def run_dot_penchant!(env, deployment)
DotPenchant.run(env || environment, deployment) if has_dot_penchant?
end
def header
header = [ "# generated by penchant, environment: #{current_env}" ]
if is_deployment
header << ", deployment mode (was #{environment})"
end
header.join
end
def prior_environment
gemfile_header[%r{\(was (.+)\)}, 1]
end
def switch_back!(fallback_env)
switch_to!(prior_environment || fallback_env)
end
private
@ -57,8 +118,20 @@ module Penchant
File.join(@path, file)
end
def env(check, &block)
instance_eval(&block) if check.to_s == @env.to_s
def process(env = @env)
builder.result(env, @is_deployment)
end
def builder
@builder ||= FileProcessor.new(template)
end
def template
File.read(processable_gemfile_path)
end
def gemfile_header
(has_gemfile? and File.readlines(gemfile_path).first) or ""
end
end
end

35
lib/penchant/hooks.rb Normal file
View File

@ -0,0 +1,35 @@
require 'pathname'
module Penchant
class Hooks
HOOKS_DIR = 'script/hooks'
GIT_HOOKS_DIR = '.git/hooks'
def self.installed?
if File.directory?(HOOKS_DIR)
Dir[File.join(HOOKS_DIR, '*')].each do |file|
target = File.join(GIT_HOOKS_DIR, File.basename(file))
return false if !File.symlink?(target)
return false if !File.expand_path(File.readlink(target)) == File.expand_path(file)
end
end
true
end
def self.install!
if git?
puts "[penchant] installing git hooks"
Dir['script/hooks/*'].each do |hook|
FileUtils.ln_sf File.join(Dir.pwd, hook), "#{GIT_HOOKS_DIR}/#{File.split(hook).last}"
end
end
end
def self.git?
File.directory?(GIT_HOOKS_DIR)
end
end
end

View File

@ -0,0 +1,28 @@
module Penchant
class PropertyStack
PATHING_OPTIONS = [ :git, :branch, :path ].freeze
def initialize(builder, property_stack, strip_pathing_options)
@builder, @property_stack, @strip_pathing_options = builder, property_stack.dup, strip_pathing_options
end
def processor
@processor ||= PropertyStackProcessor.new(@builder)
end
def process_for_gem(gem_name, additional_env = {})
properties = processor.process(gem_name, @property_stack)
if @strip_pathing_options
PATHING_OPTIONS.each { |key| properties.delete(key) }
end
properties = processor.process(gem_name, @builder.defaults[gem_name].merge(additional_env)).merge(properties)
properties.delete(:opposite)
Hash[properties.sort]
end
end
end

View File

@ -0,0 +1,24 @@
module Penchant
class PropertyStackBuilder
attr_reader :defaults
def initialize(defaults)
@defaults = defaults
@custom_properties = {}
end
def []=(key, value)
@custom_properties[key] = CustomProperty.new(value)
end
def [](key)
@custom_properties[key]
end
def create_stack_for(stack, strip_pathing_options = false)
PropertyStack.new(self, stack, strip_pathing_options)
end
end
end

View File

@ -0,0 +1,26 @@
module Penchant
class PropertyStackProcessor
def initialize(builder)
@builder = builder
end
def process(gem_name, stack)
properties = {}
property_stack = stack.dup.to_a
while !property_stack.empty?
key, value = property_stack.shift
if property = @builder[key]
property_stack += property.process([ value ].flatten)
else
value = value % gem_name if value.respond_to?(:%)
properties[key] = value
end
end
properties
end
end
end

16
lib/penchant/repo.rb Normal file
View File

@ -0,0 +1,16 @@
module Penchant
class Repo
def initialize(url)
@url = url
end
def clone_to(dir)
Dir.chdir(dir) do
system %{git clone #{@url}}
end
end
def to_s ; @url ; end
end
end

View File

@ -1,3 +1,3 @@
module Penchant
VERSION = "0.0.1"
VERSION = "0.2.29"
end

View File

@ -18,4 +18,13 @@ Gem::Specification.new do |s|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.require_paths = ["lib"]
s.add_dependency 'bundler'
s.add_dependency 'thor'
s.add_development_dependency 'cucumber'
s.add_development_dependency 'mocha'
s.add_development_dependency 'fakefs'
s.add_development_dependency 'rspec', '~> 2.6.0'
s.add_development_dependency 'rake'
end

11
script/gemfile Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env ruby
require 'rubygems'
require 'penchant'
if Penchant::Gemfile.do_full_env_switch!(ARGV[0])
puts "Gemfile switched to #{ARGV[0]}"
else
exit 0
end

27
script/hooks/commit-msg Executable file
View File

@ -0,0 +1,27 @@
#!/bin/bash
msg=$(cat $1)
# wtf mac os x lion
if [ ! -z "$MY_RUBY_HOME" ]; then
PATH="$MY_RUBY_HOME/bin:$PATH"
fi
if [ ! -z "$GEM_PATH" ]; then
oifs="$IFS"
while IFS=":" read -ra GEM_PATHS; do
FIXED_GEM_PATH=""
for i in "${GEM_PATHS[@]}"; do
FIXED_GEM_PATH="$FIXED_GEM_PATH:${i}/bin"
done
done <<< "$GEM_PATH"
IFS="$oifs"
PATH="$FIXED_GEM_PATH:$PATH"
fi
if [[ "${msg}" != *"[ci skip]"* ]]; then
bundle exec rake --trace
R=$?
if [ $R -ne 0 ]; then exit $R; fi
fi

2
script/hooks/post-commit Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash

2
script/hooks/pre-commit Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash

19
script/initialize-environment Executable file
View File

@ -0,0 +1,19 @@
#!/usr/bin/env ruby
puts "Bundling..."
system %{bundle}
puts "Installing git hooks"
system %{script/install-git-hooks}
bundle = File.file?('Gemfile') ? 'bundle exec' : ''
command = [ bundle, 'rake', '-s', '-T', 'bootstrap' ]
if !(%x{#{command.join(' ')}}).empty?
puts "Trying to run rake bootstrap..."
system %{#{bundle} rake bootstrap}
end
puts "Done!"

6
script/install-git-hooks Executable file
View File

@ -0,0 +1,6 @@
#!/bin/bash
for hook in script/hooks/* ; do
ln -sf $PWD/$hook .git/hooks/${hook##*/}
done

View File

@ -1,109 +0,0 @@
require 'spec_helper'
describe Penchant::Gemfile do
include FakeFS::SpecHelpers
let(:dir) { File.expand_path(Dir.pwd) }
let(:gemfile) { described_class.new(dir) }
let(:gemfile_path) { File.join(dir, 'Gemfile') }
let(:gemfile_erb_path) { File.join(dir, 'Gemfile.erb') }
def write_file(path, content = nil)
File.open(path, 'wb') do |fh|
content = yield if block_given?
fh.print content
end
end
subject { gemfile }
context 'with no gemfile' do
it { should_not have_gemfile }
it { should_not have_gemfile_erb }
end
context 'with gemfile' do
let(:data) { "whatever" }
before do
write_file(gemfile_path) { data }
end
describe 'existence' do
it { should have_gemfile }
it { should_not have_gemfile_erb }
end
describe '#environment' do
context 'not defined' do
its(:environment) { should be_nil }
end
context 'defined' do
let(:environment) { 'test' }
let(:data) { <<-GEMFILE }
# generated by penchant, environment: #{environment}
GEMFILE
its(:environment) { should == environment }
end
end
describe '#switch_to!' do
it 'should raise an exception' do
expect { subject.switch_to!(:whatever) }.to raise_error(Errno::ENOENT)
end
end
end
context 'with gemfile.erb' do
let(:erb_data) { 'whatever' }
before do
write_file(gemfile_erb_path) { erb_data }
end
it { should_not have_gemfile }
it { should have_gemfile_erb }
describe '#switch_to!' do
let(:erb_data) { <<-ERB }
<% env :test do %>
test
<% end %>
<% env :not do %>
not
<% end %>
all
ERB
it 'should render test data' do
subject.switch_to!(:test)
File.read('Gemfile').should include('test')
File.read('Gemfile').should_not include('not')
File.read('Gemfile').should include('all')
end
it 'should not render test data' do
subject.switch_to!(:not)
File.read('Gemfile').should_not include('test')
File.read('Gemfile').should include('not')
File.read('Gemfile').should include('all')
end
it 'should not render either' do
subject.switch_to!
File.read('Gemfile').should_not include('test')
File.read('Gemfile').should_not include('not')
File.read('Gemfile').should include('all')
end
end
end
end

View File

@ -1,6 +0,0 @@
require 'fakefs/spec_helpers'
require 'penchant'
RSpec.configure do |c|
c.mock_with :mocha
end

View File

@ -0,0 +1,40 @@
#!/bin/bash
msg=$(cat $1)
OLD_GIT_DIR=$GIT_DIR
# lion appears to insert git paths before everything else. ensure rvm can
# bust through, at the very least.
if [ ! -z "$MY_RUBY_HOME" ]; then
PATH="$MY_RUBY_HOME/bin:$PATH"
fi
if [ ! -z "$GEM_PATH" ]; then
oifs="$IFS"
while IFS=":" read -ra GEM_PATHS; do
FIXED_GEM_PATH=""
for i in "${GEM_PATHS[@]}"; do
FIXED_GEM_PATH="$FIXED_GEM_PATH:${i}/bin"
done
done <<< "$GEM_PATH"
IFS="$oifs"
PATH="$FIXED_GEM_PATH:$PATH"
fi
if [[ "${msg}" != *"[ci skip]"* ]]; then
if [ "$(penchant gemfile-env)" != "remote" ]; then
unset GIT_DIR
penchant gemfile remote
GIT_DIR=$OLD_GIT_DIR
fi
if [ $(bundle exec rake -P | grep default | wc -l) -ne 0 ]; then
bundle exec rake
R=$?
if [ $R -ne 0 ]; then exit $R; fi
else
echo "[penchant] No default Rake task found! Add one if you want to run tests before committing."
fi
fi

View File

@ -0,0 +1,5 @@
#!/bin/bash
unset GIT_DIR
echo "Run penchant gemfile remote to get back to work!"

View File

@ -1,12 +1,14 @@
#!/bin/bash
OLD_GIT_DIR=$GIT_DIR
if [ $(bundle exec rake -P | grep preflight_check | wc -l) -ne 0 ]; then
bundle exec rake preflight_check
R=$?
if [ $R -ne 0 ]; then exit $R; fi
fi
unset GIT_DIR
penchant gemfile remote
penchant gemfile remote --deployment
GIT_DIR=$OLD_GIT_DIR
git add Gemfile*
bundle exec rake
R=$?
if [ $R -ne 0 ]; then exit $R; fi
exit 0

View File

@ -1,23 +1,7 @@
#!/usr/bin/env ruby
if File.file?('Gemfile.erb')
pwd = Dir.pwd
Dir.chdir '..' do
File.readlines(File.join(pwd, 'Gemfile.erb')).find_all { |line| line[':git'] }.each do |line|
repo = line[%r{:git => (['"])(.*)\1}, 2]
puts "Installing #{repo}"
system %{git clone #{repo}}
end
end
puts "Bundling for local environment"
system %{script/gemfile local}
else
puts "Bundling..."
system %{bundle}
end
puts "Bundling..."
system %{bundle}
puts "Installing git hooks"
system %{script/install-git-hooks}