Skip to content

Commit

Permalink
Allow independent configurations to require the same recipe file (clo…
Browse files Browse the repository at this point in the history
…ses #9367)

git-svn-id: http://svn.rubyonrails.org/rails/tools/capistrano@7389 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information
jamis committed Sep 1, 2007
1 parent c0b12a2 commit cce9364
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 5 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
*SVN*

* Allow independent configurations to require the same recipe file [Jamis Buck]

* Set :shell to false to run a command without wrapping it in "sh -c" [Jamis Buck]

* Don't request a pty by default [Jamis Buck]
Expand Down
96 changes: 91 additions & 5 deletions lib/capistrano/configuration/loading.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,28 @@ def instance(require_config=false)
def instance=(config)
Thread.current[:capistrano_configuration] = config
end

# Used internally by Capistrano to track which recipes have been loaded
# via require, so that they may be successfully reloaded when require
# is called again.
def recipes_per_feature
@recipes_per_feature ||= {}
end

# Used internally to determine what the current "feature" being
# required is. This is used to track which files load which recipes
# via require.
def current_feature
Thread.current[:capistrano_current_feature]
end

# Used internally to specify the current file being required, so that
# any recipes loaded by that file can be remembered. This allows
# recipes loaded via require to be correctly reloaded in different
# Configuration instances in the same Ruby instance.
def current_feature=(feature)
Thread.current[:capistrano_current_feature] = feature
end
end

# The load paths used for locating recipe files.
Expand All @@ -33,6 +55,7 @@ def instance=(config)
def initialize_with_loading(*args) #:nodoc:
initialize_without_loading(*args)
@load_paths = [".", File.expand_path(File.join(File.dirname(__FILE__), "../recipes"))]
@loaded_features = []
end
private :initialize_with_loading

Expand Down Expand Up @@ -66,9 +89,11 @@ def load(*args, &block)
load_from_file(options[:file], options[:name])

elsif options[:string]
remember_load(options) unless options[:reloading]
instance_eval(options[:string], options[:name] || "<eval>")

elsif options[:proc]
remember_load(options) unless options[:reloading]
instance_eval(&options[:proc])

else
Expand All @@ -80,12 +105,63 @@ def load(*args, &block)
# with the exception that it sets the receiver as the "current" configuration
# so that third-party task bundles can include themselves relative to
# that configuration.
#
# This is a bit more complicated than an initial review would seem to
# necessitate, but the use case that complicates things is this: An
# advanced user wants to embed capistrano, and needs to instantiate
# more than one capistrano configuration at a time. They also want each
# configuration to require a third-party capistrano extension. Using a
# naive require implementation, this would allow the first configuration
# to successfully load the third-party extension, but the require would
# fail for the second configuration because the extension has already
# been loaded.
#
# To work around this, we do a few things:
#
# 1. Each time a 'require' is invoked inside of a capistrano recipe,
# we remember the arguments (see "current_feature").
# 2. Each time a 'load' is invoked inside of a capistrano recipe, and
# "current_feature" is not nil (meaning we are inside of a pending
# require) we remember the options (see "remember_load" and
# "recipes_per_feature").
# 3. Each time a 'require' is invoked inside of a capistrano recipe,
# we check to see if this particular configuration has ever seen these
# arguments to require (see @loaded_features), and if not, we proceed
# as if the file had never been required. If the superclass' require
# returns false (meaning, potentially, that the file has already been
# required), then we look in the recipes_per_feature collection and
# load any remembered recipes from there.
#
# It's kind of a bear, but it works, and works transparently. Note that
# a simpler implementation would just muck with $", allowing files to be
# required multiple times, but that will cause warnings (and possibly
# errors) if the file to be required contains constant definitions and
# such, alongside (or instead of) capistrano recipe definitions.
def require(*args) #:nodoc:
original, self.class.instance = self.class.instance, self
super
ensure
# restore the original, so that require's can be nested
self.class.instance = original
# look to see if this specific configuration instance has ever seen
# these arguments to require before
if !@loaded_features.include?(args)
@loaded_features << args

begin
original_instance, self.class.instance = self.class.instance, self
original_feature, self.class.current_feature = self.class.current_feature, args

result = super
if !result # file has been required previously, load up the remembered recipes
list = self.class.recipes_per_feature[args] || []
list.each { |options| load(options.merge(:reloading => true)) }
end

return result
ensure
# restore the original, so that require's can be nested
self.class.instance = original_instance
self.class.current_feature = original_feature
end
else
return false
end
end

private
Expand All @@ -107,6 +183,16 @@ def find_file_in_load_path(file)

raise LoadError, "no such file to load -- #{file}"
end

# If a file is being required, the options associated with loading a
# recipe are remembered in the recipes_per_feature archive under the
# name of the file currently being required.
def remember_load(options)
if self.class.current_feature
list = (self.class.recipes_per_feature[self.class.current_feature] ||= [])
list << options
end
end
end
end
end
8 changes: 8 additions & 0 deletions test/configuration/loading_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,12 @@ def test_require_without_config_should_raise_load_error
require "#{File.dirname(__FILE__)}/../fixtures/custom"
end
end

def test_require_in_multiple_instances_should_load_recipes_in_each_instance
config2 = MockConfig.new
@config.require "#{File.dirname(__FILE__)}/../fixtures/custom"
config2.require "#{File.dirname(__FILE__)}/../fixtures/custom"
assert_equal :custom, @config.ping
assert_equal :custom, config2.ping
end
end

0 comments on commit cce9364

Please sign in to comment.