minmb-capistrano 2.15.4

Sign up to get free protection for your applications and to get access to all the features.
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