capistrano 2.0.0 → 2.15.11
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.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.travis.yml +9 -0
- data/CHANGELOG +763 -18
- data/Gemfile +15 -0
- data/README.md +94 -0
- data/Rakefile +11 -0
- data/bin/cap +0 -0
- data/bin/capify +40 -25
- data/capistrano.gemspec +40 -0
- data/lib/capistrano/callback.rb +5 -1
- data/lib/capistrano/cli/execute.rb +10 -7
- data/lib/capistrano/cli/help.rb +39 -16
- data/lib/capistrano/cli/help.txt +44 -16
- data/lib/capistrano/cli/options.rb +71 -11
- data/lib/capistrano/cli/ui.rb +13 -1
- data/lib/capistrano/cli.rb +5 -5
- data/lib/capistrano/command.rb +215 -58
- data/lib/capistrano/configuration/actions/file_transfer.rb +29 -14
- data/lib/capistrano/configuration/actions/inspect.rb +3 -3
- data/lib/capistrano/configuration/actions/invocation.rb +224 -22
- data/lib/capistrano/configuration/alias_task.rb +26 -0
- data/lib/capistrano/configuration/callbacks.rb +26 -27
- data/lib/capistrano/configuration/connections.rb +130 -52
- data/lib/capistrano/configuration/execution.rb +34 -18
- data/lib/capistrano/configuration/loading.rb +99 -6
- data/lib/capistrano/configuration/log_formatters.rb +75 -0
- data/lib/capistrano/configuration/namespaces.rb +45 -12
- data/lib/capistrano/configuration/roles.rb +34 -2
- data/lib/capistrano/configuration/servers.rb +51 -10
- data/lib/capistrano/configuration/variables.rb +3 -3
- data/lib/capistrano/configuration.rb +20 -4
- data/lib/capistrano/errors.rb +12 -8
- data/lib/capistrano/ext/multistage.rb +67 -0
- data/lib/capistrano/ext/string.rb +5 -0
- data/lib/capistrano/extensions.rb +1 -1
- data/lib/capistrano/fix_rake_deprecated_dsl.rb +8 -0
- data/lib/capistrano/logger.rb +112 -5
- data/lib/capistrano/processable.rb +55 -0
- data/lib/capistrano/recipes/compat.rb +2 -2
- data/lib/capistrano/recipes/deploy/assets.rb +202 -0
- data/lib/capistrano/recipes/deploy/dependencies.rb +2 -2
- data/lib/capistrano/recipes/deploy/local_dependency.rb +10 -2
- data/lib/capistrano/recipes/deploy/remote_dependency.rb +54 -2
- data/lib/capistrano/recipes/deploy/scm/accurev.rb +169 -0
- data/lib/capistrano/recipes/deploy/scm/base.rb +31 -11
- data/lib/capistrano/recipes/deploy/scm/bzr.rb +14 -14
- data/lib/capistrano/recipes/deploy/scm/cvs.rb +11 -9
- data/lib/capistrano/recipes/deploy/scm/darcs.rb +12 -1
- data/lib/capistrano/recipes/deploy/scm/git.rb +299 -0
- data/lib/capistrano/recipes/deploy/scm/mercurial.rb +23 -15
- data/lib/capistrano/recipes/deploy/scm/none.rb +55 -0
- data/lib/capistrano/recipes/deploy/scm/perforce.rb +54 -28
- data/lib/capistrano/recipes/deploy/scm/subversion.rb +36 -18
- data/lib/capistrano/recipes/deploy/scm.rb +1 -1
- data/lib/capistrano/recipes/deploy/strategy/base.rb +32 -4
- data/lib/capistrano/recipes/deploy/strategy/copy.rb +238 -43
- data/lib/capistrano/recipes/deploy/strategy/remote.rb +1 -1
- data/lib/capistrano/recipes/deploy/strategy/remote_cache.rb +11 -1
- data/lib/capistrano/recipes/deploy/strategy/unshared_remote_cache.rb +21 -0
- data/lib/capistrano/recipes/deploy/strategy.rb +4 -3
- data/lib/capistrano/recipes/deploy.rb +265 -123
- data/lib/capistrano/recipes/standard.rb +1 -1
- data/lib/capistrano/role.rb +102 -0
- data/lib/capistrano/server_definition.rb +6 -1
- data/lib/capistrano/shell.rb +30 -33
- data/lib/capistrano/ssh.rb +46 -60
- data/lib/capistrano/task_definition.rb +16 -8
- data/lib/capistrano/transfer.rb +218 -0
- data/lib/capistrano/version.rb +8 -17
- data/lib/capistrano.rb +4 -1
- data/test/cli/execute_test.rb +3 -3
- data/test/cli/help_test.rb +33 -7
- data/test/cli/options_test.rb +109 -6
- data/test/cli/ui_test.rb +2 -2
- data/test/cli_test.rb +3 -3
- data/test/command_test.rb +144 -124
- data/test/configuration/actions/file_transfer_test.rb +41 -20
- data/test/configuration/actions/inspect_test.rb +21 -7
- data/test/configuration/actions/invocation_test.rb +141 -30
- data/test/configuration/alias_task_test.rb +118 -0
- data/test/configuration/callbacks_test.rb +41 -46
- data/test/configuration/connections_test.rb +187 -36
- data/test/configuration/execution_test.rb +18 -2
- data/test/configuration/loading_test.rb +33 -4
- data/test/configuration/namespace_dsl_test.rb +54 -5
- data/test/configuration/roles_test.rb +114 -4
- data/test/configuration/servers_test.rb +97 -4
- data/test/configuration/variables_test.rb +12 -2
- data/test/configuration_test.rb +9 -13
- data/test/deploy/local_dependency_test.rb +76 -0
- data/test/deploy/remote_dependency_test.rb +146 -0
- data/test/deploy/scm/accurev_test.rb +23 -0
- data/test/deploy/scm/base_test.rb +1 -1
- data/test/deploy/scm/bzr_test.rb +51 -0
- data/test/deploy/scm/darcs_test.rb +37 -0
- data/test/deploy/scm/git_test.rb +274 -0
- data/test/deploy/scm/mercurial_test.rb +134 -0
- data/test/deploy/scm/none_test.rb +35 -0
- data/test/deploy/scm/perforce_test.rb +23 -0
- data/test/deploy/scm/subversion_test.rb +68 -0
- data/test/deploy/strategy/copy_test.rb +240 -26
- data/test/extensions_test.rb +2 -2
- data/test/logger_formatting_test.rb +149 -0
- data/test/logger_test.rb +13 -2
- data/test/recipes_test.rb +25 -0
- data/test/role_test.rb +11 -0
- data/test/server_definition_test.rb +15 -2
- data/test/shell_test.rb +33 -1
- data/test/ssh_test.rb +40 -24
- data/test/task_definition_test.rb +18 -2
- data/test/transfer_test.rb +168 -0
- data/test/utils.rb +28 -33
- data/test/version_test.rb +4 -17
- metadata +214 -100
- data/MIT-LICENSE +0 -20
- data/README +0 -43
- data/examples/sample.rb +0 -14
- data/lib/capistrano/gateway.rb +0 -131
- data/lib/capistrano/recipes/deploy/templates/maintenance.rhtml +0 -53
- data/lib/capistrano/recipes/upgrade.rb +0 -33
- data/lib/capistrano/upload.rb +0 -146
- data/test/gateway_test.rb +0 -167
- data/test/upload_test.rb +0 -131
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
require '
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
#
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
76
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
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
|