minmb-capistrano 2.15.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. data/.gitignore +10 -0
  2. data/.travis.yml +7 -0
  3. data/CHANGELOG +1170 -0
  4. data/Gemfile +13 -0
  5. data/README.md +94 -0
  6. data/Rakefile +11 -0
  7. data/bin/cap +4 -0
  8. data/bin/capify +92 -0
  9. data/capistrano.gemspec +40 -0
  10. data/lib/capistrano.rb +5 -0
  11. data/lib/capistrano/callback.rb +45 -0
  12. data/lib/capistrano/cli.rb +47 -0
  13. data/lib/capistrano/cli/execute.rb +85 -0
  14. data/lib/capistrano/cli/help.rb +125 -0
  15. data/lib/capistrano/cli/help.txt +81 -0
  16. data/lib/capistrano/cli/options.rb +243 -0
  17. data/lib/capistrano/cli/ui.rb +40 -0
  18. data/lib/capistrano/command.rb +303 -0
  19. data/lib/capistrano/configuration.rb +57 -0
  20. data/lib/capistrano/configuration/actions/file_transfer.rb +50 -0
  21. data/lib/capistrano/configuration/actions/inspect.rb +46 -0
  22. data/lib/capistrano/configuration/actions/invocation.rb +329 -0
  23. data/lib/capistrano/configuration/alias_task.rb +26 -0
  24. data/lib/capistrano/configuration/callbacks.rb +147 -0
  25. data/lib/capistrano/configuration/connections.rb +237 -0
  26. data/lib/capistrano/configuration/execution.rb +142 -0
  27. data/lib/capistrano/configuration/loading.rb +205 -0
  28. data/lib/capistrano/configuration/log_formatters.rb +75 -0
  29. data/lib/capistrano/configuration/namespaces.rb +223 -0
  30. data/lib/capistrano/configuration/roles.rb +77 -0
  31. data/lib/capistrano/configuration/servers.rb +116 -0
  32. data/lib/capistrano/configuration/variables.rb +127 -0
  33. data/lib/capistrano/errors.rb +19 -0
  34. data/lib/capistrano/ext/multistage.rb +64 -0
  35. data/lib/capistrano/ext/string.rb +5 -0
  36. data/lib/capistrano/extensions.rb +57 -0
  37. data/lib/capistrano/fix_rake_deprecated_dsl.rb +8 -0
  38. data/lib/capistrano/logger.rb +166 -0
  39. data/lib/capistrano/processable.rb +57 -0
  40. data/lib/capistrano/recipes/compat.rb +32 -0
  41. data/lib/capistrano/recipes/deploy.rb +625 -0
  42. data/lib/capistrano/recipes/deploy/assets.rb +201 -0
  43. data/lib/capistrano/recipes/deploy/dependencies.rb +44 -0
  44. data/lib/capistrano/recipes/deploy/local_dependency.rb +54 -0
  45. data/lib/capistrano/recipes/deploy/remote_dependency.rb +117 -0
  46. data/lib/capistrano/recipes/deploy/scm.rb +19 -0
  47. data/lib/capistrano/recipes/deploy/scm/accurev.rb +169 -0
  48. data/lib/capistrano/recipes/deploy/scm/base.rb +200 -0
  49. data/lib/capistrano/recipes/deploy/scm/bzr.rb +86 -0
  50. data/lib/capistrano/recipes/deploy/scm/cvs.rb +153 -0
  51. data/lib/capistrano/recipes/deploy/scm/darcs.rb +96 -0
  52. data/lib/capistrano/recipes/deploy/scm/git.rb +293 -0
  53. data/lib/capistrano/recipes/deploy/scm/mercurial.rb +137 -0
  54. data/lib/capistrano/recipes/deploy/scm/none.rb +55 -0
  55. data/lib/capistrano/recipes/deploy/scm/perforce.rb +152 -0
  56. data/lib/capistrano/recipes/deploy/scm/subversion.rb +121 -0
  57. data/lib/capistrano/recipes/deploy/strategy.rb +19 -0
  58. data/lib/capistrano/recipes/deploy/strategy/base.rb +92 -0
  59. data/lib/capistrano/recipes/deploy/strategy/checkout.rb +20 -0
  60. data/lib/capistrano/recipes/deploy/strategy/copy.rb +338 -0
  61. data/lib/capistrano/recipes/deploy/strategy/export.rb +20 -0
  62. data/lib/capistrano/recipes/deploy/strategy/remote.rb +52 -0
  63. data/lib/capistrano/recipes/deploy/strategy/remote_cache.rb +57 -0
  64. data/lib/capistrano/recipes/deploy/strategy/unshared_remote_cache.rb +21 -0
  65. data/lib/capistrano/recipes/standard.rb +37 -0
  66. data/lib/capistrano/recipes/templates/maintenance.rhtml +53 -0
  67. data/lib/capistrano/role.rb +102 -0
  68. data/lib/capistrano/server_definition.rb +56 -0
  69. data/lib/capistrano/shell.rb +265 -0
  70. data/lib/capistrano/ssh.rb +95 -0
  71. data/lib/capistrano/task_definition.rb +77 -0
  72. data/lib/capistrano/transfer.rb +218 -0
  73. data/lib/capistrano/version.rb +11 -0
  74. data/test/cli/execute_test.rb +132 -0
  75. data/test/cli/help_test.rb +165 -0
  76. data/test/cli/options_test.rb +329 -0
  77. data/test/cli/ui_test.rb +28 -0
  78. data/test/cli_test.rb +17 -0
  79. data/test/command_test.rb +322 -0
  80. data/test/configuration/actions/file_transfer_test.rb +61 -0
  81. data/test/configuration/actions/inspect_test.rb +76 -0
  82. data/test/configuration/actions/invocation_test.rb +288 -0
  83. data/test/configuration/alias_task_test.rb +118 -0
  84. data/test/configuration/callbacks_test.rb +201 -0
  85. data/test/configuration/connections_test.rb +439 -0
  86. data/test/configuration/execution_test.rb +175 -0
  87. data/test/configuration/loading_test.rb +148 -0
  88. data/test/configuration/namespace_dsl_test.rb +332 -0
  89. data/test/configuration/roles_test.rb +157 -0
  90. data/test/configuration/servers_test.rb +183 -0
  91. data/test/configuration/variables_test.rb +190 -0
  92. data/test/configuration_test.rb +77 -0
  93. data/test/deploy/local_dependency_test.rb +76 -0
  94. data/test/deploy/remote_dependency_test.rb +146 -0
  95. data/test/deploy/scm/accurev_test.rb +23 -0
  96. data/test/deploy/scm/base_test.rb +55 -0
  97. data/test/deploy/scm/bzr_test.rb +51 -0
  98. data/test/deploy/scm/darcs_test.rb +37 -0
  99. data/test/deploy/scm/git_test.rb +221 -0
  100. data/test/deploy/scm/mercurial_test.rb +134 -0
  101. data/test/deploy/scm/none_test.rb +35 -0
  102. data/test/deploy/scm/perforce_test.rb +23 -0
  103. data/test/deploy/scm/subversion_test.rb +40 -0
  104. data/test/deploy/strategy/copy_test.rb +360 -0
  105. data/test/extensions_test.rb +69 -0
  106. data/test/fixtures/cli_integration.rb +5 -0
  107. data/test/fixtures/config.rb +5 -0
  108. data/test/fixtures/custom.rb +3 -0
  109. data/test/logger_formatting_test.rb +149 -0
  110. data/test/logger_test.rb +134 -0
  111. data/test/recipes_test.rb +25 -0
  112. data/test/role_test.rb +11 -0
  113. data/test/server_definition_test.rb +121 -0
  114. data/test/shell_test.rb +96 -0
  115. data/test/ssh_test.rb +113 -0
  116. data/test/task_definition_test.rb +117 -0
  117. data/test/transfer_test.rb +168 -0
  118. data/test/utils.rb +37 -0
  119. metadata +316 -0
@@ -0,0 +1,237 @@
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
+ Thread.abort_on_exception = true
28
+ @gateways = {}
29
+ if gateway.is_a?(Hash)
30
+ @options[:logger].debug "Creating multiple gateways using #{gateway.inspect}" if @options[:logger]
31
+ gateway.each do |gw, hosts|
32
+ gateway_connection = add_gateway(gw)
33
+ [*hosts].each do |host|
34
+ @gateways[:default] ||= gateway_connection
35
+ @gateways[host] = gateway_connection
36
+ end
37
+ end
38
+ else
39
+ @options[:logger].debug "Creating gateway using #{[*gateway].join(', ')}" if @options[:logger]
40
+ @gateways[:default] = add_gateway(gateway)
41
+ end
42
+ end
43
+
44
+ def add_gateway(gateway)
45
+ gateways = [*gateway].collect { |g| ServerDefinition.new(g) }
46
+ tunnel = SSH.connection_strategy(gateways[0], @options) do |host, user, connect_options|
47
+ Net::SSH::Gateway.new(host, user, connect_options)
48
+ end
49
+ (gateways[1..-1]).inject(tunnel) do |tunnel, destination|
50
+ @options[:logger].debug "Creating tunnel to #{destination}" if @options[:logger]
51
+ local_host = ServerDefinition.new("127.0.0.1", :user => destination.user, :port => tunnel.open(destination.host, (destination.port || 22)))
52
+ SSH.connection_strategy(local_host, @options) do |host, user, connect_options|
53
+ Net::SSH::Gateway.new(host, user, connect_options)
54
+ end
55
+ end
56
+ end
57
+
58
+ def connect_to(server)
59
+ @options[:logger].debug "establishing connection to `#{server}' via gateway" if @options[:logger]
60
+ local_host = ServerDefinition.new("127.0.0.1", :user => server.user, :port => gateway_for(server).open(server.host, server.port || 22))
61
+ session = SSH.connect(local_host, @options)
62
+ session.xserver = server
63
+ session
64
+ end
65
+
66
+ def gateway_for(server)
67
+ @gateways[server.host] || @gateways[:default]
68
+ end
69
+ end
70
+
71
+ # A hash of the SSH sessions that are currently open and available.
72
+ # Because sessions are constructed lazily, this will only contain
73
+ # connections to those servers that have been the targets of one or more
74
+ # executed tasks. Stored on a per-thread basis to improve thread-safety.
75
+ def sessions
76
+ Thread.current[:sessions] ||= {}
77
+ end
78
+
79
+ def initialize_with_connections(*args) #:nodoc:
80
+ initialize_without_connections(*args)
81
+ Thread.current[:sessions] = {}
82
+ Thread.current[:failed_sessions] = []
83
+ end
84
+
85
+ # Indicate that the given server could not be connected to.
86
+ def failed!(server)
87
+ Thread.current[:failed_sessions] << server
88
+ end
89
+
90
+ # Query whether previous connection attempts to the given server have
91
+ # failed.
92
+ def has_failed?(server)
93
+ Thread.current[:failed_sessions].include?(server)
94
+ end
95
+
96
+ # Used to force connections to be made to the current task's servers.
97
+ # Connections are normally made lazily in Capistrano--you can use this
98
+ # to force them open before performing some operation that might be
99
+ # time-sensitive.
100
+ def connect!(options={})
101
+ execute_on_servers(options) { }
102
+ end
103
+
104
+ # Returns the object responsible for establishing new SSH connections.
105
+ # The factory will respond to #connect_to, which can be used to
106
+ # establish connections to servers defined via ServerDefinition objects.
107
+ def connection_factory
108
+ @connection_factory ||= begin
109
+ if exists?(:gateway) && !fetch(:gateway).nil? && !fetch(:gateway).empty?
110
+ logger.debug "establishing connection to gateway `#{fetch(:gateway).inspect}'"
111
+ GatewayConnectionFactory.new(fetch(:gateway), self)
112
+ else
113
+ DefaultConnectionFactory.new(self)
114
+ end
115
+ end
116
+ end
117
+
118
+ # Ensures that there are active sessions for each server in the list.
119
+ def establish_connections_to(servers)
120
+ failed_servers = []
121
+
122
+ # force the connection factory to be instantiated synchronously,
123
+ # otherwise we wind up with multiple gateway instances, because
124
+ # each connection is done in parallel.
125
+ connection_factory
126
+
127
+ threads = Array(servers).map { |server| establish_connection_to(server, failed_servers) }
128
+ threads.each { |t| t.join }
129
+
130
+ if failed_servers.any?
131
+ errors = failed_servers.map { |h| "#{h[:server]} (#{h[:error].class}: #{h[:error].message})" }
132
+ error = ConnectionError.new("connection failed for: #{errors.join(', ')}")
133
+ error.hosts = failed_servers.map { |h| h[:server] }
134
+ raise error
135
+ end
136
+ end
137
+
138
+ # Destroys sessions for each server in the list.
139
+ def teardown_connections_to(servers)
140
+ servers.each do |server|
141
+ begin
142
+ session = sessions.delete(server)
143
+ session.close if session
144
+ rescue IOError, Net::SSH::Disconnect
145
+ # the TCP connection is already dead
146
+ end
147
+ end
148
+ end
149
+
150
+ # Determines the set of servers within the current task's scope
151
+ def filter_servers(options={})
152
+ if task = current_task
153
+ servers = find_servers_for_task(task, options)
154
+
155
+ if servers.empty?
156
+ if ENV['HOSTFILTER'] || task.options.merge(options)[:on_no_matching_servers] == :continue
157
+ logger.info "skipping `#{task.fully_qualified_name}' because no servers matched"
158
+ else
159
+ unless dry_run
160
+ raise Capistrano::NoMatchingServersError, "`#{task.fully_qualified_name}' is only run for servers matching #{task.options.inspect}, but no servers matched"
161
+ end
162
+ end
163
+ end
164
+
165
+ if task.continue_on_error?
166
+ servers.delete_if { |s| has_failed?(s) }
167
+ end
168
+ else
169
+ servers = find_servers(options)
170
+ if servers.empty? && !dry_run
171
+ raise Capistrano::NoMatchingServersError, "no servers found to match #{options.inspect}" if options[:on_no_matching_servers] != :continue
172
+ end
173
+ end
174
+
175
+ servers = [servers.first] if options[:once]
176
+ [task, servers.compact]
177
+ end
178
+
179
+ # Determines the set of servers within the current task's scope and
180
+ # establishes connections to them, and then yields that list of
181
+ # servers.
182
+ def execute_on_servers(options={})
183
+ raise ArgumentError, "expected a block" unless block_given?
184
+
185
+ task, servers = filter_servers(options)
186
+ return if servers.empty?
187
+ logger.trace "servers: #{servers.map { |s| s.host }.inspect}"
188
+
189
+ max_hosts = (options[:max_hosts] || (task && task.max_hosts) || servers.size).to_i
190
+ is_subset = max_hosts < servers.size
191
+
192
+ # establish connections to those servers in groups of max_hosts, as necessary
193
+ servers.each_slice(max_hosts) do |servers_slice|
194
+ begin
195
+ establish_connections_to(servers_slice)
196
+ rescue ConnectionError => error
197
+ raise error unless task && task.continue_on_error?
198
+ error.hosts.each do |h|
199
+ servers_slice.delete(h)
200
+ failed!(h)
201
+ end
202
+ end
203
+
204
+ begin
205
+ yield servers_slice
206
+ rescue RemoteError => error
207
+ raise error unless task && task.continue_on_error?
208
+ error.hosts.each { |h| failed!(h) }
209
+ end
210
+
211
+ # if dealing with a subset (e.g., :max_hosts is less than the
212
+ # number of servers available) teardown the subset of connections
213
+ # that were just made, so that we can make room for the next subset.
214
+ teardown_connections_to(servers_slice) if is_subset
215
+ end
216
+ end
217
+
218
+ private
219
+
220
+ # We establish the connection by creating a thread in a new method--this
221
+ # prevents problems with the thread's scope seeing the wrong 'server'
222
+ # variable if the thread just happens to take too long to start up.
223
+ def establish_connection_to(server, failures=nil)
224
+ current_thread = Thread.current
225
+ Thread.new { safely_establish_connection_to(server, current_thread, failures) }
226
+ end
227
+
228
+ def safely_establish_connection_to(server, thread, failures=nil)
229
+ thread[:sessions] ||= {}
230
+ thread[:sessions][server] ||= connection_factory.connect_to(server)
231
+ rescue Exception => err
232
+ raise unless failures
233
+ failures << { :server => server, :error => err }
234
+ end
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,142 @@
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
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
+
112
+ # throw the task back on the stack so that roles are properly
113
+ # interpreted in the scope of the task in question.
114
+ rollback_requests.reverse.each do |frame|
115
+ begin
116
+ push_task_call_frame(frame.task)
117
+ logger.important "rolling back", frame.task.fully_qualified_name
118
+ frame.rollback.call
119
+ rescue Object => e
120
+ logger.info "exception while rolling back: #{e.class}, #{e.message}", frame.task.fully_qualified_name
121
+ ensure
122
+ pop_task_call_frame
123
+ end
124
+ end
125
+ end
126
+
127
+ def push_task_call_frame(task)
128
+ frame = TaskCallFrame.new(task)
129
+ task_call_frames.push frame
130
+ end
131
+
132
+ def pop_task_call_frame
133
+ task_call_frames.pop
134
+ end
135
+
136
+ # Invokes the task's body directly, without setting up the call frame.
137
+ def invoke_task_directly(task)
138
+ task.namespace.instance_eval(&task.body)
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,205 @@
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
+ def file_in_load_path?(file)
167
+ begin
168
+ !!find_file_in_load_path(file)
169
+ rescue LoadError
170
+ false
171
+ end
172
+ end
173
+
174
+ private
175
+
176
+ # Load a recipe from the named file. If +name+ is given, the file will
177
+ # be reported using that name.
178
+ def load_from_file(file, name=nil)
179
+ file = find_file_in_load_path(file) unless File.file?(file)
180
+ load :string => File.read(file), :name => name || file
181
+ end
182
+
183
+ def find_file_in_load_path(file)
184
+ load_paths.each do |path|
185
+ ["", ".rb"].each do |ext|
186
+ name = File.join(path, "#{file}#{ext}")
187
+ return name if File.file?(name)
188
+ end
189
+ end
190
+
191
+ raise LoadError, "no such file to load -- #{file}"
192
+ end
193
+
194
+ # If a file is being required, the options associated with loading a
195
+ # recipe are remembered in the recipes_per_feature archive under the
196
+ # name of the file currently being required.
197
+ def remember_load(options)
198
+ if self.class.current_feature
199
+ list = (self.class.recipes_per_feature[self.class.current_feature] ||= [])
200
+ list << options
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end