mirror of
https://github.com/capistrano/capistrano
synced 2023-03-27 23:21:18 -04:00
Allow independent configurations to require the same recipe file (closes #9367)
git-svn-id: http://svn.rubyonrails.org/rails/tools/capistrano@7389 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
parent
ee78acbca6
commit
8bfb81eac9
3 changed files with 101 additions and 5 deletions
|
@ -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]
|
||||
|
|
|
@ -25,6 +25,28 @@ module Capistrano
|
|||
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.
|
||||
|
@ -33,6 +55,7 @@ module Capistrano
|
|||
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
|
||||
|
||||
|
@ -66,9 +89,11 @@ module Capistrano
|
|||
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
|
||||
|
@ -80,12 +105,63 @@ module Capistrano
|
|||
# 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
|
||||
|
@ -107,6 +183,16 @@ module Capistrano
|
|||
|
||||
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
|
||||
|
|
|
@ -116,4 +116,12 @@ class ConfigurationLoadingTest < Test::Unit::TestCase
|
|||
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
|
Loading…
Add table
Add a link
Reference in a new issue