le1t0-capistrano 2.5.18.001

Sign up to get free protection for your applications and to get access to all the features.
Files changed (107) hide show
  1. data/.gitignore +9 -0
  2. data/CHANGELOG +843 -0
  3. data/README +102 -0
  4. data/Rakefile +36 -0
  5. data/VERSION +1 -0
  6. data/bin/cap +4 -0
  7. data/bin/capify +86 -0
  8. data/lib/capistrano.rb +2 -0
  9. data/lib/capistrano/callback.rb +45 -0
  10. data/lib/capistrano/cli.rb +47 -0
  11. data/lib/capistrano/cli/execute.rb +85 -0
  12. data/lib/capistrano/cli/help.rb +125 -0
  13. data/lib/capistrano/cli/help.txt +78 -0
  14. data/lib/capistrano/cli/options.rb +243 -0
  15. data/lib/capistrano/cli/ui.rb +40 -0
  16. data/lib/capistrano/command.rb +283 -0
  17. data/lib/capistrano/configuration.rb +44 -0
  18. data/lib/capistrano/configuration/actions/file_transfer.rb +52 -0
  19. data/lib/capistrano/configuration/actions/inspect.rb +46 -0
  20. data/lib/capistrano/configuration/actions/invocation.rb +295 -0
  21. data/lib/capistrano/configuration/callbacks.rb +148 -0
  22. data/lib/capistrano/configuration/connections.rb +204 -0
  23. data/lib/capistrano/configuration/execution.rb +143 -0
  24. data/lib/capistrano/configuration/loading.rb +197 -0
  25. data/lib/capistrano/configuration/namespaces.rb +197 -0
  26. data/lib/capistrano/configuration/roles.rb +73 -0
  27. data/lib/capistrano/configuration/servers.rb +98 -0
  28. data/lib/capistrano/configuration/variables.rb +127 -0
  29. data/lib/capistrano/errors.rb +19 -0
  30. data/lib/capistrano/extensions.rb +57 -0
  31. data/lib/capistrano/logger.rb +59 -0
  32. data/lib/capistrano/processable.rb +53 -0
  33. data/lib/capistrano/recipes/compat.rb +32 -0
  34. data/lib/capistrano/recipes/deploy.rb +589 -0
  35. data/lib/capistrano/recipes/deploy/dependencies.rb +44 -0
  36. data/lib/capistrano/recipes/deploy/local_dependency.rb +54 -0
  37. data/lib/capistrano/recipes/deploy/remote_dependency.rb +105 -0
  38. data/lib/capistrano/recipes/deploy/scm.rb +19 -0
  39. data/lib/capistrano/recipes/deploy/scm/accurev.rb +169 -0
  40. data/lib/capistrano/recipes/deploy/scm/base.rb +196 -0
  41. data/lib/capistrano/recipes/deploy/scm/bzr.rb +86 -0
  42. data/lib/capistrano/recipes/deploy/scm/cvs.rb +152 -0
  43. data/lib/capistrano/recipes/deploy/scm/darcs.rb +96 -0
  44. data/lib/capistrano/recipes/deploy/scm/git.rb +278 -0
  45. data/lib/capistrano/recipes/deploy/scm/mercurial.rb +137 -0
  46. data/lib/capistrano/recipes/deploy/scm/none.rb +44 -0
  47. data/lib/capistrano/recipes/deploy/scm/perforce.rb +138 -0
  48. data/lib/capistrano/recipes/deploy/scm/subversion.rb +121 -0
  49. data/lib/capistrano/recipes/deploy/strategy.rb +19 -0
  50. data/lib/capistrano/recipes/deploy/strategy/base.rb +79 -0
  51. data/lib/capistrano/recipes/deploy/strategy/checkout.rb +20 -0
  52. data/lib/capistrano/recipes/deploy/strategy/copy.rb +218 -0
  53. data/lib/capistrano/recipes/deploy/strategy/export.rb +20 -0
  54. data/lib/capistrano/recipes/deploy/strategy/remote.rb +52 -0
  55. data/lib/capistrano/recipes/deploy/strategy/remote_cache.rb +56 -0
  56. data/lib/capistrano/recipes/deploy/templates/maintenance.rhtml +53 -0
  57. data/lib/capistrano/recipes/standard.rb +37 -0
  58. data/lib/capistrano/recipes/templates/maintenance.rhtml +53 -0
  59. data/lib/capistrano/role.rb +102 -0
  60. data/lib/capistrano/server_definition.rb +56 -0
  61. data/lib/capistrano/shell.rb +260 -0
  62. data/lib/capistrano/ssh.rb +99 -0
  63. data/lib/capistrano/task_definition.rb +75 -0
  64. data/lib/capistrano/transfer.rb +216 -0
  65. data/lib/capistrano/version.rb +18 -0
  66. data/test/cli/execute_test.rb +132 -0
  67. data/test/cli/help_test.rb +165 -0
  68. data/test/cli/options_test.rb +329 -0
  69. data/test/cli/ui_test.rb +28 -0
  70. data/test/cli_test.rb +17 -0
  71. data/test/command_test.rb +286 -0
  72. data/test/configuration/actions/file_transfer_test.rb +61 -0
  73. data/test/configuration/actions/inspect_test.rb +65 -0
  74. data/test/configuration/actions/invocation_test.rb +225 -0
  75. data/test/configuration/callbacks_test.rb +220 -0
  76. data/test/configuration/connections_test.rb +349 -0
  77. data/test/configuration/execution_test.rb +175 -0
  78. data/test/configuration/loading_test.rb +132 -0
  79. data/test/configuration/namespace_dsl_test.rb +311 -0
  80. data/test/configuration/roles_test.rb +144 -0
  81. data/test/configuration/servers_test.rb +158 -0
  82. data/test/configuration/variables_test.rb +184 -0
  83. data/test/configuration_test.rb +88 -0
  84. data/test/deploy/local_dependency_test.rb +76 -0
  85. data/test/deploy/remote_dependency_test.rb +114 -0
  86. data/test/deploy/scm/accurev_test.rb +23 -0
  87. data/test/deploy/scm/base_test.rb +55 -0
  88. data/test/deploy/scm/bzr_test.rb +51 -0
  89. data/test/deploy/scm/darcs_test.rb +37 -0
  90. data/test/deploy/scm/git_test.rb +184 -0
  91. data/test/deploy/scm/mercurial_test.rb +134 -0
  92. data/test/deploy/scm/none_test.rb +35 -0
  93. data/test/deploy/scm/subversion_test.rb +32 -0
  94. data/test/deploy/strategy/copy_test.rb +302 -0
  95. data/test/extensions_test.rb +69 -0
  96. data/test/fixtures/cli_integration.rb +5 -0
  97. data/test/fixtures/config.rb +5 -0
  98. data/test/fixtures/custom.rb +3 -0
  99. data/test/logger_test.rb +123 -0
  100. data/test/role_test.rb +11 -0
  101. data/test/server_definition_test.rb +121 -0
  102. data/test/shell_test.rb +90 -0
  103. data/test/ssh_test.rb +104 -0
  104. data/test/task_definition_test.rb +116 -0
  105. data/test/transfer_test.rb +160 -0
  106. data/test/utils.rb +39 -0
  107. metadata +289 -0
@@ -0,0 +1,204 @@
1
+ require 'enumerator'
2
+ require 'net/ssh/gateway'
3
+ require 'capistrano/ssh'
4
+ require 'capistrano/errors'
5
+
6
+ module Capistrano
7
+ class Configuration
8
+ module Connections
9
+ def self.included(base) #:nodoc:
10
+ base.send :alias_method, :initialize_without_connections, :initialize
11
+ base.send :alias_method, :initialize, :initialize_with_connections
12
+ end
13
+
14
+ class DefaultConnectionFactory #:nodoc:
15
+ def initialize(options)
16
+ @options = options
17
+ end
18
+
19
+ def connect_to(server)
20
+ SSH.connect(server, @options)
21
+ end
22
+ end
23
+
24
+ class GatewayConnectionFactory #:nodoc:
25
+ def initialize(gateway, options)
26
+ @options = options
27
+ @options[:logger].debug "Creating gateway using #{[*gateway].join(', ')}" if @options[:logger]
28
+ Thread.abort_on_exception = true
29
+ @gateways = [*gateway].collect { |g| ServerDefinition.new(g) }
30
+ tunnel = SSH.connection_strategy(@gateways[0], @options) do |host, user, connect_options|
31
+ Net::SSH::Gateway.new(host, user, connect_options)
32
+ end
33
+ @gateway = (@gateways[1..-1]).inject(tunnel) do |tunnel, destination|
34
+ @options[:logger].debug "Creating tunnel to #{destination}" if @options[:logger]
35
+ local_host = ServerDefinition.new("127.0.0.1", :user => destination.user, :port => tunnel.open(destination.host, (destination.port || 22)))
36
+ SSH.connection_strategy(local_host, @options) do |host, user, connect_options|
37
+ Net::SSH::Gateway.new(host, user, connect_options)
38
+ end
39
+ end
40
+ end
41
+
42
+ def connect_to(server)
43
+ @options[:logger].debug "establishing connection to `#{server}' via gateway" if @options[:logger]
44
+ local_host = ServerDefinition.new("127.0.0.1", :user => server.user, :port => @gateway.open(server.host, server.port || 22))
45
+ session = SSH.connect(local_host, @options)
46
+ session.xserver = server
47
+ session
48
+ end
49
+ end
50
+
51
+ # A hash of the SSH sessions that are currently open and available.
52
+ # Because sessions are constructed lazily, this will only contain
53
+ # connections to those servers that have been the targets of one or more
54
+ # executed tasks. Stored on a per-thread basis to improve thread-safety.
55
+ def sessions
56
+ Thread.current[:sessions] ||= {}
57
+ end
58
+
59
+ def initialize_with_connections(*args) #:nodoc:
60
+ initialize_without_connections(*args)
61
+ Thread.current[:sessions] = {}
62
+ Thread.current[:failed_sessions] = []
63
+ end
64
+
65
+ # Indicate that the given server could not be connected to.
66
+ def failed!(server)
67
+ Thread.current[:failed_sessions] << server
68
+ end
69
+
70
+ # Query whether previous connection attempts to the given server have
71
+ # failed.
72
+ def has_failed?(server)
73
+ Thread.current[:failed_sessions].include?(server)
74
+ end
75
+
76
+ # Used to force connections to be made to the current task's servers.
77
+ # Connections are normally made lazily in Capistrano--you can use this
78
+ # to force them open before performing some operation that might be
79
+ # time-sensitive.
80
+ def connect!(options={})
81
+ execute_on_servers(options) { }
82
+ end
83
+
84
+ # Returns the object responsible for establishing new SSH connections.
85
+ # The factory will respond to #connect_to, which can be used to
86
+ # establish connections to servers defined via ServerDefinition objects.
87
+ def connection_factory
88
+ @connection_factory ||= begin
89
+ if exists?(:gateway)
90
+ logger.debug "establishing connection to gateway `#{fetch(:gateway)}'"
91
+ GatewayConnectionFactory.new(fetch(:gateway), self)
92
+ else
93
+ DefaultConnectionFactory.new(self)
94
+ end
95
+ end
96
+ end
97
+
98
+ # Ensures that there are active sessions for each server in the list.
99
+ def establish_connections_to(servers)
100
+ failed_servers = []
101
+
102
+ # force the connection factory to be instantiated synchronously,
103
+ # otherwise we wind up with multiple gateway instances, because
104
+ # each connection is done in parallel.
105
+ connection_factory
106
+
107
+ threads = Array(servers).map { |server| establish_connection_to(server, failed_servers) }
108
+ threads.each { |t| t.join }
109
+
110
+ if failed_servers.any?
111
+ errors = failed_servers.map { |h| "#{h[:server]} (#{h[:error].class}: #{h[:error].message})" }
112
+ error = ConnectionError.new("connection failed for: #{errors.join(', ')}")
113
+ error.hosts = failed_servers.map { |h| h[:server] }
114
+ raise error
115
+ end
116
+ end
117
+
118
+ # Destroys sessions for each server in the list.
119
+ def teardown_connections_to(servers)
120
+ servers.each do |server|
121
+ sessions[server].close
122
+ sessions.delete(server)
123
+ end
124
+ end
125
+
126
+ # Determines the set of servers within the current task's scope and
127
+ # establishes connections to them, and then yields that list of
128
+ # servers.
129
+ def execute_on_servers(options={})
130
+ raise ArgumentError, "expected a block" unless block_given?
131
+
132
+ if task = current_task
133
+ servers = find_servers_for_task(task, options)
134
+
135
+ if servers.empty?
136
+ if ENV['HOSTFILTER']
137
+ logger.info "skipping `#{task.fully_qualified_name}' because no servers matched"
138
+ return
139
+ else
140
+ raise Capistrano::NoMatchingServersError, "`#{task.fully_qualified_name}' is only run for servers matching #{task.options.inspect}, but no servers matched"
141
+ end
142
+ end
143
+
144
+ if task.continue_on_error?
145
+ servers.delete_if { |s| has_failed?(s) }
146
+ return if servers.empty?
147
+ end
148
+ else
149
+ servers = find_servers(options)
150
+ raise Capistrano::NoMatchingServersError, "no servers found to match #{options.inspect}" if servers.empty?
151
+ end
152
+
153
+ servers = [servers.first] if options[:once]
154
+ logger.trace "servers: #{servers.map { |s| s.host }.inspect}"
155
+
156
+ max_hosts = (options[:max_hosts] || (task && task.max_hosts) || servers.size).to_i
157
+ is_subset = max_hosts < servers.size
158
+
159
+ # establish connections to those servers in groups of max_hosts, as necessary
160
+ servers.each_slice(max_hosts) do |servers_slice|
161
+ begin
162
+ establish_connections_to(servers_slice)
163
+ rescue ConnectionError => error
164
+ raise error unless task && task.continue_on_error?
165
+ error.hosts.each do |h|
166
+ servers_slice.delete(h)
167
+ failed!(h)
168
+ end
169
+ end
170
+
171
+ begin
172
+ yield servers_slice
173
+ rescue RemoteError => error
174
+ raise error unless task && task.continue_on_error?
175
+ error.hosts.each { |h| failed!(h) }
176
+ end
177
+
178
+ # if dealing with a subset (e.g., :max_hosts is less than the
179
+ # number of servers available) teardown the subset of connections
180
+ # that were just made, so that we can make room for the next subset.
181
+ teardown_connections_to(servers_slice) if is_subset
182
+ end
183
+ end
184
+
185
+ private
186
+
187
+ # We establish the connection by creating a thread in a new method--this
188
+ # prevents problems with the thread's scope seeing the wrong 'server'
189
+ # variable if the thread just happens to take too long to start up.
190
+ def establish_connection_to(server, failures=nil)
191
+ current_thread = Thread.current
192
+ Thread.new { safely_establish_connection_to(server, current_thread, failures) }
193
+ end
194
+
195
+ def safely_establish_connection_to(server, thread, failures=nil)
196
+ thread[:sessions] ||= {}
197
+ thread[:sessions][server] ||= connection_factory.connect_to(server)
198
+ rescue Exception => err
199
+ raise unless failures
200
+ failures << { :server => server, :error => err }
201
+ end
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,143 @@
1
+ require 'capistrano/errors'
2
+
3
+ module Capistrano
4
+ class Configuration
5
+ module Execution
6
+ def self.included(base) #:nodoc:
7
+ base.send :alias_method, :initialize_without_execution, :initialize
8
+ base.send :alias_method, :initialize, :initialize_with_execution
9
+ end
10
+
11
+ # A struct for representing a single instance of an invoked task.
12
+ TaskCallFrame = Struct.new(:task, :rollback)
13
+
14
+ def initialize_with_execution(*args) #:nodoc:
15
+ initialize_without_execution(*args)
16
+ end
17
+ private :initialize_with_execution
18
+
19
+ # Returns true if there is a transaction currently active.
20
+ def transaction?
21
+ !rollback_requests.nil?
22
+ end
23
+
24
+ # The call stack of the tasks. The currently executing task may inspect
25
+ # this to see who its caller was. The current task is always the last
26
+ # element of this stack.
27
+ def task_call_frames
28
+ Thread.current[:task_call_frames] ||= []
29
+ end
30
+
31
+
32
+ # The stack of tasks that have registered rollback handlers within the
33
+ # current transaction. If this is nil, then there is no transaction
34
+ # that is currently active.
35
+ def rollback_requests
36
+ Thread.current[:rollback_requests]
37
+ end
38
+
39
+ def rollback_requests=(rollback_requests)
40
+ Thread.current[:rollback_requests] = rollback_requests
41
+ end
42
+
43
+ # Invoke a set of tasks in a transaction. If any task fails (raises an
44
+ # exception), all tasks executed within the transaction are inspected to
45
+ # see if they have an associated on_rollback hook, and if so, that hook
46
+ # is called.
47
+ def transaction
48
+ raise ArgumentError, "expected a block" unless block_given?
49
+ raise ScriptError, "transaction must be called from within a task" if task_call_frames.empty?
50
+
51
+ return yield if transaction?
52
+
53
+ logger.info "transaction: start"
54
+ begin
55
+ self.rollback_requests = []
56
+ yield
57
+ logger.info "transaction: commit"
58
+ rescue Object => e
59
+ rollback!
60
+ raise
61
+ ensure
62
+ self.rollback_requests = nil if Thread.main == Thread.current
63
+ end
64
+ end
65
+
66
+ # Specifies an on_rollback hook for the currently executing task. If this
67
+ # or any subsequent task then fails, and a transaction is active, this
68
+ # hook will be executed.
69
+ def on_rollback(&block)
70
+ if transaction?
71
+ # don't note a new rollback request if one has already been set
72
+ rollback_requests << task_call_frames.last unless task_call_frames.last.rollback
73
+ task_call_frames.last.rollback = block
74
+ end
75
+ end
76
+
77
+ # Returns the TaskDefinition object for the currently executing task.
78
+ # It returns nil if there is no task being executed.
79
+ def current_task
80
+ return nil if task_call_frames.empty?
81
+ task_call_frames.last.task
82
+ end
83
+
84
+ # Executes the task with the given name, without invoking any associated
85
+ # callbacks.
86
+ def execute_task(task)
87
+ logger.debug "executing `#{task.fully_qualified_name}'"
88
+ push_task_call_frame(task)
89
+ invoke_task_directly(task)
90
+ ensure
91
+ pop_task_call_frame
92
+ end
93
+
94
+ # Attempts to locate the task at the given fully-qualified path, and
95
+ # execute it. If no such task exists, a Capistrano::NoSuchTaskError will
96
+ # be raised.
97
+ def find_and_execute_task(path, hooks={})
98
+ task = find_task(path) or raise NoSuchTaskError, "the task `#{path}' does not exist"
99
+
100
+ trigger(hooks[:before], task) if hooks[:before]
101
+ result = execute_task(task)
102
+ trigger(hooks[:after], task) if hooks[:after]
103
+
104
+ result
105
+ end
106
+
107
+ protected
108
+
109
+ def rollback!
110
+ return if Thread.current[:rollback_requests].nil?
111
+ Thread.current[:rolled_back] = true
112
+
113
+ # throw the task back on the stack so that roles are properly
114
+ # interpreted in the scope of the task in question.
115
+ rollback_requests.reverse.each do |frame|
116
+ begin
117
+ push_task_call_frame(frame.task)
118
+ logger.important "rolling back", frame.task.fully_qualified_name
119
+ frame.rollback.call
120
+ rescue Object => e
121
+ logger.info "exception while rolling back: #{e.class}, #{e.message}", frame.task.fully_qualified_name
122
+ ensure
123
+ pop_task_call_frame
124
+ end
125
+ end
126
+ end
127
+
128
+ def push_task_call_frame(task)
129
+ frame = TaskCallFrame.new(task)
130
+ task_call_frames.push frame
131
+ end
132
+
133
+ def pop_task_call_frame
134
+ task_call_frames.pop
135
+ end
136
+
137
+ # Invokes the task's body directly, without setting up the call frame.
138
+ def invoke_task_directly(task)
139
+ task.namespace.instance_eval(&task.body)
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,197 @@
1
+ module Capistrano
2
+ class Configuration
3
+ module Loading
4
+ def self.included(base) #:nodoc:
5
+ base.send :alias_method, :initialize_without_loading, :initialize
6
+ base.send :alias_method, :initialize, :initialize_with_loading
7
+ base.extend ClassMethods
8
+ end
9
+
10
+ module ClassMethods
11
+ # Used by third-party task bundles to identify the capistrano
12
+ # configuration that is loading them. Its return value is not reliable
13
+ # in other contexts. If +require_config+ is not false, an exception
14
+ # will be raised if the current configuration is not set.
15
+ def instance(require_config=false)
16
+ config = Thread.current[:capistrano_configuration]
17
+ if require_config && config.nil?
18
+ raise LoadError, "Please require this file from within a Capistrano recipe"
19
+ end
20
+ config
21
+ end
22
+
23
+ # Used internally by Capistrano to specify the current configuration
24
+ # before loading a third-party task bundle.
25
+ def instance=(config)
26
+ Thread.current[:capistrano_configuration] = config
27
+ end
28
+
29
+ # Used internally by Capistrano to track which recipes have been loaded
30
+ # via require, so that they may be successfully reloaded when require
31
+ # is called again.
32
+ def recipes_per_feature
33
+ @recipes_per_feature ||= {}
34
+ end
35
+
36
+ # Used internally to determine what the current "feature" being
37
+ # required is. This is used to track which files load which recipes
38
+ # via require.
39
+ def current_feature
40
+ Thread.current[:capistrano_current_feature]
41
+ end
42
+
43
+ # Used internally to specify the current file being required, so that
44
+ # any recipes loaded by that file can be remembered. This allows
45
+ # recipes loaded via require to be correctly reloaded in different
46
+ # Configuration instances in the same Ruby instance.
47
+ def current_feature=(feature)
48
+ Thread.current[:capistrano_current_feature] = feature
49
+ end
50
+ end
51
+
52
+ # The load paths used for locating recipe files.
53
+ attr_reader :load_paths
54
+
55
+ def initialize_with_loading(*args) #:nodoc:
56
+ initialize_without_loading(*args)
57
+ @load_paths = [".", File.expand_path(File.join(File.dirname(__FILE__), "../recipes"))]
58
+ @loaded_features = []
59
+ end
60
+ private :initialize_with_loading
61
+
62
+ # Load a configuration file or string into this configuration.
63
+ #
64
+ # Usage:
65
+ #
66
+ # load("recipe"):
67
+ # Look for and load the contents of 'recipe.rb' into this
68
+ # configuration.
69
+ #
70
+ # load(:file => "recipe"):
71
+ # same as above
72
+ #
73
+ # load(:string => "set :scm, :subversion"):
74
+ # Load the given string as a configuration specification.
75
+ #
76
+ # load { ... }
77
+ # Load the block in the context of the configuration.
78
+ def load(*args, &block)
79
+ options = args.last.is_a?(Hash) ? args.pop : {}
80
+
81
+ if block
82
+ raise ArgumentError, "loading a block requires 0 arguments" unless options.empty? && args.empty?
83
+ load(:proc => block)
84
+
85
+ elsif args.any?
86
+ args.each { |arg| load options.merge(:file => arg) }
87
+
88
+ elsif options[:file]
89
+ load_from_file(options[:file], options[:name])
90
+
91
+ elsif options[:string]
92
+ remember_load(options) unless options[:reloading]
93
+ instance_eval(options[:string], options[:name] || "<eval>")
94
+
95
+ elsif options[:proc]
96
+ remember_load(options) unless options[:reloading]
97
+ instance_eval(&options[:proc])
98
+
99
+ else
100
+ raise ArgumentError, "don't know how to load #{options.inspect}"
101
+ end
102
+ end
103
+
104
+ # Require another file. This is identical to the standard require method,
105
+ # with the exception that it sets the receiver as the "current" configuration
106
+ # so that third-party task bundles can include themselves relative to
107
+ # that configuration.
108
+ #
109
+ # This is a bit more complicated than an initial review would seem to
110
+ # necessitate, but the use case that complicates things is this: An
111
+ # advanced user wants to embed capistrano, and needs to instantiate
112
+ # more than one capistrano configuration at a time. They also want each
113
+ # configuration to require a third-party capistrano extension. Using a
114
+ # naive require implementation, this would allow the first configuration
115
+ # to successfully load the third-party extension, but the require would
116
+ # fail for the second configuration because the extension has already
117
+ # been loaded.
118
+ #
119
+ # To work around this, we do a few things:
120
+ #
121
+ # 1. Each time a 'require' is invoked inside of a capistrano recipe,
122
+ # we remember the arguments (see "current_feature").
123
+ # 2. Each time a 'load' is invoked inside of a capistrano recipe, and
124
+ # "current_feature" is not nil (meaning we are inside of a pending
125
+ # require) we remember the options (see "remember_load" and
126
+ # "recipes_per_feature").
127
+ # 3. Each time a 'require' is invoked inside of a capistrano recipe,
128
+ # we check to see if this particular configuration has ever seen these
129
+ # arguments to require (see @loaded_features), and if not, we proceed
130
+ # as if the file had never been required. If the superclass' require
131
+ # returns false (meaning, potentially, that the file has already been
132
+ # required), then we look in the recipes_per_feature collection and
133
+ # load any remembered recipes from there.
134
+ #
135
+ # It's kind of a bear, but it works, and works transparently. Note that
136
+ # a simpler implementation would just muck with $", allowing files to be
137
+ # required multiple times, but that will cause warnings (and possibly
138
+ # errors) if the file to be required contains constant definitions and
139
+ # such, alongside (or instead of) capistrano recipe definitions.
140
+ def require(*args) #:nodoc:
141
+ # look to see if this specific configuration instance has ever seen
142
+ # these arguments to require before
143
+ if @loaded_features.include?(args)
144
+ return false
145
+ end
146
+
147
+ @loaded_features << args
148
+ begin
149
+ original_instance, self.class.instance = self.class.instance, self
150
+ original_feature, self.class.current_feature = self.class.current_feature, args
151
+
152
+ result = super
153
+ if !result # file has been required previously, load up the remembered recipes
154
+ list = self.class.recipes_per_feature[args] || []
155
+ list.each { |options| load(options.merge(:reloading => true)) }
156
+ end
157
+
158
+ return result
159
+ ensure
160
+ # restore the original, so that require's can be nested
161
+ self.class.instance = original_instance
162
+ self.class.current_feature = original_feature
163
+ end
164
+ end
165
+
166
+ private
167
+
168
+ # Load a recipe from the named file. If +name+ is given, the file will
169
+ # be reported using that name.
170
+ def load_from_file(file, name=nil)
171
+ file = find_file_in_load_path(file) unless File.file?(file)
172
+ load :string => File.read(file), :name => name || file
173
+ end
174
+
175
+ def find_file_in_load_path(file)
176
+ load_paths.each do |path|
177
+ ["", ".rb"].each do |ext|
178
+ name = File.join(path, "#{file}#{ext}")
179
+ return name if File.file?(name)
180
+ end
181
+ end
182
+
183
+ raise LoadError, "no such file to load -- #{file}"
184
+ end
185
+
186
+ # If a file is being required, the options associated with loading a
187
+ # recipe are remembered in the recipes_per_feature archive under the
188
+ # name of the file currently being required.
189
+ def remember_load(options)
190
+ if self.class.current_feature
191
+ list = (self.class.recipes_per_feature[self.class.current_feature] ||= [])
192
+ list << options
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end