capistrano 2.0.0 → 2.15.9

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 (124) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.travis.yml +9 -0
  4. data/CHANGELOG +755 -18
  5. data/Gemfile +13 -0
  6. data/README.md +94 -0
  7. data/Rakefile +11 -0
  8. data/bin/cap +0 -0
  9. data/bin/capify +37 -22
  10. data/capistrano.gemspec +40 -0
  11. data/lib/capistrano/callback.rb +5 -1
  12. data/lib/capistrano/cli/execute.rb +10 -7
  13. data/lib/capistrano/cli/help.rb +39 -16
  14. data/lib/capistrano/cli/help.txt +44 -16
  15. data/lib/capistrano/cli/options.rb +71 -11
  16. data/lib/capistrano/cli/ui.rb +13 -1
  17. data/lib/capistrano/cli.rb +5 -5
  18. data/lib/capistrano/command.rb +215 -58
  19. data/lib/capistrano/configuration/actions/file_transfer.rb +29 -14
  20. data/lib/capistrano/configuration/actions/inspect.rb +3 -3
  21. data/lib/capistrano/configuration/actions/invocation.rb +224 -22
  22. data/lib/capistrano/configuration/alias_task.rb +26 -0
  23. data/lib/capistrano/configuration/callbacks.rb +26 -27
  24. data/lib/capistrano/configuration/connections.rb +130 -52
  25. data/lib/capistrano/configuration/execution.rb +34 -18
  26. data/lib/capistrano/configuration/loading.rb +99 -6
  27. data/lib/capistrano/configuration/log_formatters.rb +75 -0
  28. data/lib/capistrano/configuration/namespaces.rb +45 -12
  29. data/lib/capistrano/configuration/roles.rb +34 -2
  30. data/lib/capistrano/configuration/servers.rb +51 -10
  31. data/lib/capistrano/configuration/variables.rb +3 -3
  32. data/lib/capistrano/configuration.rb +20 -4
  33. data/lib/capistrano/errors.rb +12 -8
  34. data/lib/capistrano/ext/multistage.rb +67 -0
  35. data/lib/capistrano/ext/string.rb +5 -0
  36. data/lib/capistrano/extensions.rb +1 -1
  37. data/lib/capistrano/fix_rake_deprecated_dsl.rb +8 -0
  38. data/lib/capistrano/logger.rb +112 -5
  39. data/lib/capistrano/processable.rb +55 -0
  40. data/lib/capistrano/recipes/compat.rb +2 -2
  41. data/lib/capistrano/recipes/deploy/assets.rb +202 -0
  42. data/lib/capistrano/recipes/deploy/dependencies.rb +2 -2
  43. data/lib/capistrano/recipes/deploy/local_dependency.rb +10 -2
  44. data/lib/capistrano/recipes/deploy/remote_dependency.rb +54 -2
  45. data/lib/capistrano/recipes/deploy/scm/accurev.rb +169 -0
  46. data/lib/capistrano/recipes/deploy/scm/base.rb +31 -11
  47. data/lib/capistrano/recipes/deploy/scm/bzr.rb +14 -14
  48. data/lib/capistrano/recipes/deploy/scm/cvs.rb +11 -9
  49. data/lib/capistrano/recipes/deploy/scm/darcs.rb +12 -1
  50. data/lib/capistrano/recipes/deploy/scm/git.rb +299 -0
  51. data/lib/capistrano/recipes/deploy/scm/mercurial.rb +23 -15
  52. data/lib/capistrano/recipes/deploy/scm/none.rb +55 -0
  53. data/lib/capistrano/recipes/deploy/scm/perforce.rb +54 -28
  54. data/lib/capistrano/recipes/deploy/scm/subversion.rb +36 -18
  55. data/lib/capistrano/recipes/deploy/scm.rb +1 -1
  56. data/lib/capistrano/recipes/deploy/strategy/base.rb +32 -4
  57. data/lib/capistrano/recipes/deploy/strategy/copy.rb +238 -43
  58. data/lib/capistrano/recipes/deploy/strategy/remote.rb +1 -1
  59. data/lib/capistrano/recipes/deploy/strategy/remote_cache.rb +11 -1
  60. data/lib/capistrano/recipes/deploy/strategy/unshared_remote_cache.rb +21 -0
  61. data/lib/capistrano/recipes/deploy/strategy.rb +4 -3
  62. data/lib/capistrano/recipes/deploy.rb +265 -123
  63. data/lib/capistrano/recipes/standard.rb +1 -1
  64. data/lib/capistrano/role.rb +102 -0
  65. data/lib/capistrano/server_definition.rb +6 -1
  66. data/lib/capistrano/shell.rb +30 -33
  67. data/lib/capistrano/ssh.rb +46 -60
  68. data/lib/capistrano/task_definition.rb +16 -8
  69. data/lib/capistrano/transfer.rb +218 -0
  70. data/lib/capistrano/version.rb +8 -17
  71. data/lib/capistrano.rb +4 -1
  72. data/test/cli/execute_test.rb +3 -3
  73. data/test/cli/help_test.rb +33 -7
  74. data/test/cli/options_test.rb +109 -6
  75. data/test/cli/ui_test.rb +2 -2
  76. data/test/cli_test.rb +3 -3
  77. data/test/command_test.rb +144 -124
  78. data/test/configuration/actions/file_transfer_test.rb +41 -20
  79. data/test/configuration/actions/inspect_test.rb +21 -7
  80. data/test/configuration/actions/invocation_test.rb +141 -30
  81. data/test/configuration/alias_task_test.rb +118 -0
  82. data/test/configuration/callbacks_test.rb +41 -46
  83. data/test/configuration/connections_test.rb +187 -36
  84. data/test/configuration/execution_test.rb +18 -2
  85. data/test/configuration/loading_test.rb +33 -4
  86. data/test/configuration/namespace_dsl_test.rb +54 -5
  87. data/test/configuration/roles_test.rb +114 -4
  88. data/test/configuration/servers_test.rb +97 -4
  89. data/test/configuration/variables_test.rb +12 -2
  90. data/test/configuration_test.rb +9 -13
  91. data/test/deploy/local_dependency_test.rb +76 -0
  92. data/test/deploy/remote_dependency_test.rb +146 -0
  93. data/test/deploy/scm/accurev_test.rb +23 -0
  94. data/test/deploy/scm/base_test.rb +1 -1
  95. data/test/deploy/scm/bzr_test.rb +51 -0
  96. data/test/deploy/scm/darcs_test.rb +37 -0
  97. data/test/deploy/scm/git_test.rb +274 -0
  98. data/test/deploy/scm/mercurial_test.rb +134 -0
  99. data/test/deploy/scm/none_test.rb +35 -0
  100. data/test/deploy/scm/perforce_test.rb +23 -0
  101. data/test/deploy/scm/subversion_test.rb +68 -0
  102. data/test/deploy/strategy/copy_test.rb +240 -26
  103. data/test/extensions_test.rb +2 -2
  104. data/test/logger_formatting_test.rb +149 -0
  105. data/test/logger_test.rb +13 -2
  106. data/test/recipes_test.rb +25 -0
  107. data/test/role_test.rb +11 -0
  108. data/test/server_definition_test.rb +15 -2
  109. data/test/shell_test.rb +33 -1
  110. data/test/ssh_test.rb +40 -24
  111. data/test/task_definition_test.rb +18 -2
  112. data/test/transfer_test.rb +168 -0
  113. data/test/utils.rb +28 -33
  114. data/test/version_test.rb +4 -17
  115. metadata +215 -100
  116. data/MIT-LICENSE +0 -20
  117. data/README +0 -43
  118. data/examples/sample.rb +0 -14
  119. data/lib/capistrano/gateway.rb +0 -131
  120. data/lib/capistrano/recipes/deploy/templates/maintenance.rhtml +0 -53
  121. data/lib/capistrano/recipes/upgrade.rb +0 -33
  122. data/lib/capistrano/upload.rb +0 -146
  123. data/test/gateway_test.rb +0 -167
  124. data/test/upload_test.rb +0 -131
@@ -1,5 +1,7 @@
1
- require 'capistrano/gateway'
1
+ require 'enumerator'
2
+ require 'net/ssh/gateway'
2
3
  require 'capistrano/ssh'
4
+ require 'capistrano/errors'
3
5
 
4
6
  module Capistrano
5
7
  class Configuration
@@ -9,8 +11,6 @@ module Capistrano
9
11
  base.send :alias_method, :initialize, :initialize_with_connections
10
12
  end
11
13
 
12
- # An adaptor for making the SSH interface look and act like that of the
13
- # Gateway class.
14
14
  class DefaultConnectionFactory #:nodoc:
15
15
  def initialize(options)
16
16
  @options = options
@@ -21,27 +21,76 @@ module Capistrano
21
21
  end
22
22
  end
23
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
+
24
71
  # A hash of the SSH sessions that are currently open and available.
25
72
  # Because sessions are constructed lazily, this will only contain
26
73
  # connections to those servers that have been the targets of one or more
27
- # executed tasks.
28
- attr_reader :sessions
74
+ # executed tasks. Stored on a per-thread basis to improve thread-safety.
75
+ def sessions
76
+ Thread.current[:sessions] ||= {}
77
+ end
29
78
 
30
79
  def initialize_with_connections(*args) #:nodoc:
31
80
  initialize_without_connections(*args)
32
- @sessions = {}
33
- @failed_sessions = []
81
+ Thread.current[:sessions] = {}
82
+ Thread.current[:failed_sessions] = []
34
83
  end
35
84
 
36
85
  # Indicate that the given server could not be connected to.
37
86
  def failed!(server)
38
- @failed_sessions << server
87
+ Thread.current[:failed_sessions] << server
39
88
  end
40
89
 
41
90
  # Query whether previous connection attempts to the given server have
42
91
  # failed.
43
92
  def has_failed?(server)
44
- @failed_sessions.include?(server)
93
+ Thread.current[:failed_sessions].include?(server)
45
94
  end
46
95
 
47
96
  # Used to force connections to be made to the current task's servers.
@@ -57,9 +106,9 @@ module Capistrano
57
106
  # establish connections to servers defined via ServerDefinition objects.
58
107
  def connection_factory
59
108
  @connection_factory ||= begin
60
- if exists?(:gateway)
61
- logger.debug "establishing connection to gateway `#{fetch(:gateway)}'"
62
- Gateway.new(ServerDefinition.new(fetch(:gateway)), self)
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)
63
112
  else
64
113
  DefaultConnectionFactory.new(self)
65
114
  end
@@ -70,22 +119,13 @@ module Capistrano
70
119
  def establish_connections_to(servers)
71
120
  failed_servers = []
72
121
 
73
- # This attemps to work around the problem where SFTP uploads hang
74
- # for some people. A bit of investigating seemed to reveal that the
75
- # hang only occurred when the SSH connections were established async,
76
- # so this setting allows people to at least work around the problem.
77
- if fetch(:synchronous_connect, false)
78
- logger.trace "synchronous_connect: true"
79
- Array(servers).each { |server| safely_establish_connection_to(server, failed_servers) }
80
- else
81
- # force the connection factory to be instantiated synchronously,
82
- # otherwise we wind up with multiple gateway instances, because
83
- # each connection is done in parallel.
84
- connection_factory
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
85
126
 
86
- threads = Array(servers).map { |server| establish_connection_to(server, failed_servers) }
87
- threads.each { |t| t.join }
88
- end
127
+ threads = Array(servers).map { |server| establish_connection_to(server, failed_servers) }
128
+ threads.each { |t| t.join }
89
129
 
90
130
  if failed_servers.any?
91
131
  errors = failed_servers.map { |h| "#{h[:server]} (#{h[:error].class}: #{h[:error].message})" }
@@ -95,47 +135,83 @@ module Capistrano
95
135
  end
96
136
  end
97
137
 
98
- # Determines the set of servers within the current task's scope and
99
- # establishes connections to them, and then yields that list of
100
- # servers.
101
- def execute_on_servers(options={})
102
- raise ArgumentError, "expected a block" unless block_given?
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
103
149
 
150
+ # Determines the set of servers within the current task's scope
151
+ def filter_servers(options={})
104
152
  if task = current_task
105
153
  servers = find_servers_for_task(task, options)
106
154
 
107
155
  if servers.empty?
108
- raise Capistrano::NoMatchingServersError, "`#{task.fully_qualified_name}' is only run for servers matching #{task.options.inspect}, but no servers matched"
156
+ if ENV['HOSTFILTER'] || ENV['HOSTROLEFILTER'] || 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
109
163
  end
110
164
 
111
165
  if task.continue_on_error?
112
166
  servers.delete_if { |s| has_failed?(s) }
113
- return if servers.empty?
114
167
  end
115
168
  else
116
169
  servers = find_servers(options)
117
- raise Capistrano::NoMatchingServersError, "no servers found to match #{options.inspect}" if servers.empty?
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
118
173
  end
119
174
 
120
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?
121
187
  logger.trace "servers: #{servers.map { |s| s.host }.inspect}"
122
188
 
123
- # establish connections to those servers, as necessary
124
- begin
125
- establish_connections_to(servers)
126
- rescue ConnectionError => error
127
- raise error unless task && task.continue_on_error?
128
- error.hosts.each do |h|
129
- servers.delete(h)
130
- failed!(h)
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) }
131
209
  end
132
- end
133
210
 
134
- begin
135
- yield servers
136
- rescue RemoteError => error
137
- raise error unless task && task.continue_on_error?
138
- error.hosts.each { |h| failed!(h) }
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
139
215
  end
140
216
  end
141
217
 
@@ -145,11 +221,13 @@ module Capistrano
145
221
  # prevents problems with the thread's scope seeing the wrong 'server'
146
222
  # variable if the thread just happens to take too long to start up.
147
223
  def establish_connection_to(server, failures=nil)
148
- Thread.new { safely_establish_connection_to(server, failures) }
224
+ current_thread = Thread.current
225
+ Thread.new { safely_establish_connection_to(server, current_thread, failures) }
149
226
  end
150
227
 
151
- def safely_establish_connection_to(server, failures=nil)
152
- sessions[server] ||= connection_factory.connect_to(server)
228
+ def safely_establish_connection_to(server, thread, failures=nil)
229
+ thread[:sessions] ||= {}
230
+ thread[:sessions][server] ||= connection_factory.connect_to(server)
153
231
  rescue Exception => err
154
232
  raise unless failures
155
233
  failures << { :server => server, :error => err }
@@ -8,22 +8,11 @@ module Capistrano
8
8
  base.send :alias_method, :initialize, :initialize_with_execution
9
9
  end
10
10
 
11
- # The call stack of the tasks. The currently executing task may inspect
12
- # this to see who its caller was. The current task is always the last
13
- # element of this stack.
14
- attr_reader :task_call_frames
15
-
16
- # The stack of tasks that have registered rollback handlers within the
17
- # current transaction. If this is nil, then there is no transaction
18
- # that is currently active.
19
- attr_reader :rollback_requests
20
-
21
11
  # A struct for representing a single instance of an invoked task.
22
12
  TaskCallFrame = Struct.new(:task, :rollback)
23
13
 
24
14
  def initialize_with_execution(*args) #:nodoc:
25
15
  initialize_without_execution(*args)
26
- @task_call_frames = []
27
16
  end
28
17
  private :initialize_with_execution
29
18
 
@@ -32,6 +21,25 @@ module Capistrano
32
21
  !rollback_requests.nil?
33
22
  end
34
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
+
35
43
  # Invoke a set of tasks in a transaction. If any task fails (raises an
36
44
  # exception), all tasks executed within the transaction are inspected to
37
45
  # see if they have an associated on_rollback hook, and if so, that hook
@@ -44,14 +52,14 @@ module Capistrano
44
52
 
45
53
  logger.info "transaction: start"
46
54
  begin
47
- @rollback_requests = []
55
+ self.rollback_requests = []
48
56
  yield
49
57
  logger.info "transaction: commit"
50
58
  rescue Object => e
51
59
  rollback!
52
60
  raise
53
61
  ensure
54
- @rollback_requests = nil
62
+ self.rollback_requests = nil
55
63
  end
56
64
  end
57
65
 
@@ -60,8 +68,9 @@ module Capistrano
60
68
  # hook will be executed.
61
69
  def on_rollback(&block)
62
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
63
73
  task_call_frames.last.rollback = block
64
- rollback_requests << task_call_frames.last
65
74
  end
66
75
  end
67
76
 
@@ -72,12 +81,12 @@ module Capistrano
72
81
  task_call_frames.last.task
73
82
  end
74
83
 
75
- # Executes the task with the given name, including the before and after
76
- # hooks.
84
+ # Executes the task with the given name, without invoking any associated
85
+ # callbacks.
77
86
  def execute_task(task)
78
87
  logger.debug "executing `#{task.fully_qualified_name}'"
79
88
  push_task_call_frame(task)
80
- task.namespace.instance_eval(&task.body)
89
+ invoke_task_directly(task)
81
90
  ensure
82
91
  pop_task_call_frame
83
92
  end
@@ -98,6 +107,8 @@ module Capistrano
98
107
  protected
99
108
 
100
109
  def rollback!
110
+ return if Thread.current[:rollback_requests].nil?
111
+
101
112
  # throw the task back on the stack so that roles are properly
102
113
  # interpreted in the scope of the task in question.
103
114
  rollback_requests.reverse.each do |frame|
@@ -121,6 +132,11 @@ module Capistrano
121
132
  def pop_task_call_frame
122
133
  task_call_frames.pop
123
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
124
140
  end
125
141
  end
126
- end
142
+ end
@@ -25,6 +25,28 @@ module Capistrano
25
25
  def instance=(config)
26
26
  Thread.current[:capistrano_configuration] = config
27
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
28
50
  end
29
51
 
30
52
  # The load paths used for locating recipe files.
@@ -33,6 +55,7 @@ module Capistrano
33
55
  def initialize_with_loading(*args) #:nodoc:
34
56
  initialize_without_loading(*args)
35
57
  @load_paths = [".", File.expand_path(File.join(File.dirname(__FILE__), "../recipes"))]
58
+ @loaded_features = []
36
59
  end
37
60
  private :initialize_with_loading
38
61
 
@@ -66,9 +89,11 @@ module Capistrano
66
89
  load_from_file(options[:file], options[:name])
67
90
 
68
91
  elsif options[:string]
92
+ remember_load(options) unless options[:reloading]
69
93
  instance_eval(options[:string], options[:name] || "<eval>")
70
94
 
71
95
  elsif options[:proc]
96
+ remember_load(options) unless options[:reloading]
72
97
  instance_eval(&options[:proc])
73
98
 
74
99
  else
@@ -80,12 +105,70 @@ module Capistrano
80
105
  # with the exception that it sets the receiver as the "current" configuration
81
106
  # so that third-party task bundles can include themselves relative to
82
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.
83
140
  def require(*args) #:nodoc:
84
- original, self.class.instance = self.class.instance, self
85
- super
86
- ensure
87
- # restore the original, so that require's can be nested
88
- self.class.instance = original
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
89
172
  end
90
173
 
91
174
  private
@@ -93,7 +176,7 @@ module Capistrano
93
176
  # Load a recipe from the named file. If +name+ is given, the file will
94
177
  # be reported using that name.
95
178
  def load_from_file(file, name=nil)
96
- file = find_file_in_load_path(file) unless file[0] == ?/
179
+ file = find_file_in_load_path(file) unless File.file?(file)
97
180
  load :string => File.read(file), :name => name || file
98
181
  end
99
182
 
@@ -107,6 +190,16 @@ module Capistrano
107
190
 
108
191
  raise LoadError, "no such file to load -- #{file}"
109
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
110
203
  end
111
204
  end
112
205
  end
@@ -0,0 +1,75 @@
1
+ # Add custom log formatters
2
+ #
3
+ # Passing a hash or a array of hashes with custom log formatters.
4
+ #
5
+ # Add the following to your deploy.rb or in your ~/.caprc
6
+ #
7
+ # == Example:
8
+ #
9
+ # capistrano_log_formatters = [
10
+ # { :match => /command finished/, :color => :hide, :priority => 10, :prepend => "$$$" },
11
+ # { :match => /executing command/, :color => :blue, :priority => 10, :style => :underscore, :timestamp => true },
12
+ # { :match => /^transaction: commit$/, :color => :magenta, :priority => 10, :style => :blink },
13
+ # { :match => /git/, :color => :white, :priority => 20, :style => :reverse }
14
+ # ]
15
+ #
16
+ # log_formatter capistrano_log_formatters
17
+ #
18
+ # You can call log_formatter multiple times, with either a hash or an array of hashes.
19
+ #
20
+ # == Colors:
21
+ #
22
+ # :color can have the following values:
23
+ #
24
+ # * :hide (hides the row completely)
25
+ # * :none
26
+ # * :black
27
+ # * :red
28
+ # * :green
29
+ # * :yellow
30
+ # * :blue
31
+ # * :magenta
32
+ # * :cyan
33
+ # * :white
34
+ #
35
+ # == Styles:
36
+ #
37
+ # :style can have the following values:
38
+ #
39
+ # * :bright
40
+ # * :dim
41
+ # * :underscore
42
+ # * :blink
43
+ # * :reverse
44
+ # * :hidden
45
+ #
46
+ #
47
+ # == Text alterations
48
+ #
49
+ # :prepend gives static text to be prepended to the output
50
+ # :replace replaces the matched text in the output
51
+ # :timestamp adds the current time before the output
52
+
53
+ module Capistrano
54
+ class Configuration
55
+ module LogFormatters
56
+ def log_formatter(options)
57
+ if options.class == Array
58
+ options.each do |option|
59
+ Capistrano::Logger.add_formatter(option)
60
+ end
61
+ else
62
+ Capistrano::Logger.add_formatter(options)
63
+ end
64
+ end
65
+
66
+ def default_log_formatters(formatters)
67
+ default_formatters = [*formatters]
68
+ end
69
+
70
+ def disable_log_formatters
71
+ @logger.disable_formatters = true
72
+ end
73
+ end
74
+ end
75
+ end