capistrano 2.0.0 → 2.15.2

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 (125) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.travis.yml +7 -0
  4. data/CHANGELOG +715 -18
  5. data/Gemfile +12 -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 +212 -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 +91 -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 +28 -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 +62 -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 +185 -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 +10 -8
  49. data/lib/capistrano/recipes/deploy/scm/darcs.rb +12 -1
  50. data/lib/capistrano/recipes/deploy/scm/git.rb +293 -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 +35 -17
  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 +1 -1
  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 +6 -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 +91 -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 +17 -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 +221 -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 +40 -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 +27 -33
  114. metadata +215 -102
  115. data/MIT-LICENSE +0 -20
  116. data/README +0 -43
  117. data/examples/sample.rb +0 -14
  118. data/lib/capistrano/gateway.rb +0 -131
  119. data/lib/capistrano/recipes/deploy/templates/maintenance.rhtml +0 -53
  120. data/lib/capistrano/recipes/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
  125. data/test/version_test.rb +0 -24
@@ -22,7 +22,19 @@ module Capistrano
22
22
  def password_prompt(prompt="Password: ")
23
23
  ui.ask(prompt) { |q| q.echo = false }
24
24
  end
25
+
26
+ # Debug mode prompt
27
+ def debug_prompt(cmd)
28
+ ui.say("Preparing to execute command: #{cmd}")
29
+ prompt = "Execute ([Yes], No, Abort) "
30
+ ui.ask("#{prompt}? ") do |q|
31
+ q.overwrite = false
32
+ q.default = 'y'
33
+ q.validate = /(y(es)?)|(no?)|(a(bort)?|\n)/i
34
+ q.responses[:not_valid] = prompt
35
+ end
36
+ end
25
37
  end
26
38
  end
27
39
  end
28
- end
40
+ end
@@ -17,17 +17,16 @@ module Capistrano
17
17
  # different set of parameters (such as when embedded cap in a program):
18
18
  #
19
19
  # require 'capistrano/cli'
20
- # Capistrano::CLI.parse(%w(-vvvv -r config/deploy update_code)).execute!
20
+ # Capistrano::CLI.parse(%W(-vvvv -f config/deploy update_code)).execute!
21
21
  #
22
22
  # Note that you can also embed cap directly by creating a new Configuration
23
- # instance and setting it up, but you'll often wind up duplicating logic
24
- # defined in the CLI class. The above snippet, redone using the Configuration
25
- # class directly, would look like:
23
+ # instance and setting it up, The above snippet, redone using the
24
+ # Configuration class directly, would look like:
26
25
  #
27
26
  # require 'capistrano'
28
27
  # require 'capistrano/cli'
29
28
  # config = Capistrano::Configuration.new
30
- # config.logger_level = Capistrano::Logger::TRACE
29
+ # config.logger.level = Capistrano::Logger::TRACE
31
30
  # config.set(:password) { Capistrano::CLI.password_prompt }
32
31
  # config.load "config/deploy"
33
32
  # config.update_code
@@ -43,5 +42,6 @@ module Capistrano
43
42
  # Mix-in the actual behavior
44
43
  include Execute, Options, UI
45
44
  include Help # needs to be included last, because it overrides some methods
45
+
46
46
  end
47
47
  end
@@ -1,14 +1,143 @@
1
+ require 'benchmark'
1
2
  require 'capistrano/errors'
3
+ require 'capistrano/processable'
2
4
 
3
5
  module Capistrano
4
6
 
5
7
  # This class encapsulates a single command to be executed on a set of remote
6
8
  # machines, in parallel.
7
9
  class Command
8
- attr_reader :command, :sessions, :options
10
+ include Processable
9
11
 
10
- def self.process(command, sessions, options={}, &block)
11
- new(command, sessions, options, &block).process!
12
+ class Tree
13
+ attr_reader :configuration
14
+ attr_reader :branches
15
+ attr_reader :fallback
16
+
17
+ include Enumerable
18
+
19
+ class Branch
20
+ attr_accessor :command, :callback, :condition
21
+ attr_reader :options
22
+
23
+ def initialize(command, options, callback)
24
+ @command = command.strip.gsub(/\r?\n/, "\\\n")
25
+ @callback = callback || Capistrano::Configuration.default_io_proc
26
+ @options = options
27
+ @skip = false
28
+ end
29
+
30
+ def last?
31
+ options[:last]
32
+ end
33
+
34
+ def skip?
35
+ @skip
36
+ end
37
+
38
+ def skip!
39
+ @skip = true
40
+ end
41
+
42
+ def match(server)
43
+ true
44
+ end
45
+
46
+ def to_s(parallel=false)
47
+ if parallel && @condition
48
+ "#{condition.inspect} :: #{command.inspect}"
49
+ else
50
+ command.inspect
51
+ end
52
+ end
53
+ end
54
+
55
+ class ConditionBranch < Branch
56
+ attr_accessor :configuration
57
+
58
+ class Evaluator
59
+ attr_reader :configuration, :condition, :server
60
+
61
+ def initialize(config, condition, server)
62
+ @configuration = config
63
+ @condition = condition
64
+ @server = server
65
+ end
66
+
67
+ def in?(role)
68
+ configuration.roles[role].include?(server)
69
+ end
70
+
71
+ def result
72
+ eval(condition, binding)
73
+ end
74
+
75
+ def method_missing(sym, *args, &block)
76
+ if server.respond_to?(sym)
77
+ server.send(sym, *args, &block)
78
+ elsif configuration.respond_to?(sym)
79
+ configuration.send(sym, *args, &block)
80
+ else
81
+ super
82
+ end
83
+ end
84
+ end
85
+
86
+ def initialize(configuration, condition, command, options, callback)
87
+ @configuration = configuration
88
+ @condition = condition
89
+ super(command, options, callback)
90
+ end
91
+
92
+ def match(server)
93
+ Evaluator.new(configuration, condition, server).result
94
+ end
95
+ end
96
+
97
+ class ElseBranch < Branch
98
+ def initialize(command, options, callback)
99
+ @condition = "else"
100
+ super(command, options, callback)
101
+ end
102
+ end
103
+
104
+ def initialize(config)
105
+ @configuration = config
106
+ @branches = []
107
+ yield self if block_given?
108
+ end
109
+
110
+ def when(condition, command, options={}, &block)
111
+ branches << ConditionBranch.new(configuration, condition, command, options, block)
112
+ end
113
+
114
+ def else(command, &block)
115
+ @fallback = ElseBranch.new(command, {}, block)
116
+ end
117
+
118
+ def branches_for(server)
119
+ seen_last = false
120
+ matches = branches.select do |branch|
121
+ success = !seen_last && !branch.skip? && branch.match(server)
122
+ seen_last = success && branch.last?
123
+ success
124
+ end
125
+
126
+ matches << fallback if matches.empty? && fallback
127
+ return matches
128
+ end
129
+
130
+ def each
131
+ branches.each { |branch| yield branch }
132
+ yield fallback if fallback
133
+ return self
134
+ end
135
+ end
136
+
137
+ attr_reader :tree, :sessions, :options
138
+
139
+ def self.process(tree, sessions, options={})
140
+ new(tree, sessions, options).process!
12
141
  end
13
142
 
14
143
  # Instantiates a new command object. The +command+ must be a string
@@ -20,11 +149,16 @@ module Capistrano
20
149
  # * +data+: (optional), a string to be sent to the command via it's stdin
21
150
  # * +env+: (optional), a string or hash to be interpreted as environment
22
151
  # variables that should be defined for this command invocation.
23
- def initialize(command, sessions, options={}, &block)
24
- @command = command.strip.gsub(/\r?\n/, "\\\n")
152
+ def initialize(tree, sessions, options={}, &block)
153
+ if String === tree
154
+ tree = Tree.new(nil) { |t| t.else(tree, &block) }
155
+ elsif block
156
+ raise ArgumentError, "block given with tree argument"
157
+ end
158
+
159
+ @tree = tree
25
160
  @sessions = sessions
26
161
  @options = options
27
- @callback = block
28
162
  @channels = open_channels
29
163
  end
30
164
 
@@ -32,29 +166,19 @@ module Capistrano
32
166
  # fails (non-zero return code) on any of the hosts, this will raise a
33
167
  # Capistrano::CommandError.
34
168
  def process!
35
- since = Time.now
36
- loop do
37
- active = 0
38
- @channels.each do |ch|
39
- next if ch[:closed]
40
- active += 1
41
- ch.connection.process(true)
42
- end
43
-
44
- break if active == 0
45
- if Time.now - since >= 1
46
- since = Time.now
47
- @channels.each { |ch| ch.connection.ping! }
169
+ elapsed = Benchmark.realtime do
170
+ loop do
171
+ break unless process_iteration { @channels.any? { |ch| !ch[:closed] } }
48
172
  end
49
- sleep 0.01 # a brief respite, to keep the CPU from going crazy
50
173
  end
51
174
 
52
- logger.trace "command finished" if logger
175
+ logger.trace "command finished in #{(elapsed * 1000).round}ms" if logger
53
176
 
54
177
  if (failed = @channels.select { |ch| ch[:status] != 0 }).any?
55
- hosts = failed.map { |ch| ch[:server] }
56
- error = CommandError.new("command #{command.inspect} failed on #{hosts.join(',')}")
57
- error.hosts = hosts
178
+ commands = failed.inject({}) { |map, ch| (map[ch[:command]] ||= []) << ch[:server]; map }
179
+ message = commands.map { |command, list| "#{command.inspect} on #{list.join(',')}" }.join("; ")
180
+ error = CommandError.new("failed: #{message}")
181
+ error.hosts = commands.values.flatten
58
182
  raise error
59
183
  end
60
184
 
@@ -77,51 +201,84 @@ module Capistrano
77
201
 
78
202
  def open_channels
79
203
  sessions.map do |session|
80
- session.open_channel do |channel|
81
- server = session.xserver
82
-
83
- channel[:server] = server
84
- channel[:host] = server.host
85
- channel[:options] = options
86
- channel.request_pty :want_reply => true
87
-
88
- channel.on_success do |ch|
89
- logger.trace "executing command", ch[:server] if logger
90
- escaped = replace_placeholders(command, ch).gsub(/[$\\`"]/) { |m| "\\#{m}" }
91
- command_line = [environment, options[:shell] || "sh", "-c", "\"#{escaped}\""].compact.join(" ")
92
- ch.exec(command_line)
93
- ch.send_data(options[:data]) if options[:data]
94
- end
204
+ server = session.xserver
205
+ tree.branches_for(server).map do |branch|
206
+ session.open_channel do |channel|
207
+ channel[:server] = server
208
+ channel[:host] = server.host
209
+ channel[:options] = options
210
+ channel[:branch] = branch
95
211
 
96
- channel.on_failure do |ch|
97
- # just log it, don't actually raise an exception, since the
98
- # process method will see that the status is not zero and will
99
- # raise an exception then.
100
- logger.important "could not open channel", ch[:server] if logger
101
- ch.close
102
- end
212
+ request_pty_if_necessary(channel) do |ch, success|
213
+ if success
214
+ logger.trace "executing command", ch[:server] if logger
215
+ cmd = replace_placeholders(channel[:branch].command, ch)
103
216
 
104
- channel.on_data do |ch, data|
105
- @callback[ch, :out, data] if @callback
106
- end
217
+ if options[:shell] == false
218
+ shell = nil
219
+ else
220
+ shell = "#{options[:shell] || "sh"} -c"
221
+ cmd = cmd.gsub(/'/) { |m| "'\\''" }
222
+ cmd = "'#{cmd}'"
223
+ end
107
224
 
108
- channel.on_extended_data do |ch, type, data|
109
- @callback[ch, :err, data] if @callback
110
- end
225
+ command_line = [environment, shell, cmd].compact.join(" ")
226
+ ch[:command] = command_line
111
227
 
112
- channel.on_request do |ch, request, reply, data|
113
- ch[:status] = data.read_long if request == "exit-status"
114
- end
228
+ ch.exec(command_line)
229
+ ch.send_data(options[:data]) if options[:data]
230
+ ch.eof! if options[:eof]
231
+ else
232
+ # just log it, don't actually raise an exception, since the
233
+ # process method will see that the status is not zero and will
234
+ # raise an exception then.
235
+ logger.important "could not open channel", ch[:server] if logger
236
+ ch.close
237
+ end
238
+ end
239
+
240
+ channel.on_data do |ch, data|
241
+ ch[:branch].callback[ch, :out, data]
242
+ end
243
+
244
+ channel.on_extended_data do |ch, type, data|
245
+ ch[:branch].callback[ch, :err, data]
246
+ end
247
+
248
+ channel.on_request("exit-status") do |ch, data|
249
+ ch[:status] = data.read_long
250
+ end
115
251
 
116
- channel.on_close do |ch|
117
- ch[:closed] = true
252
+ channel.on_request("exit-signal") do |ch, data|
253
+ if logger
254
+ exit_signal = data.read_string
255
+ logger.important "command received signal #{exit_signal}", ch[:server]
256
+ end
257
+ end
258
+
259
+ channel.on_close do |ch|
260
+ ch[:closed] = true
261
+ end
118
262
  end
119
263
  end
264
+ end.flatten
265
+ end
266
+
267
+ def request_pty_if_necessary(channel)
268
+ if options[:pty]
269
+ channel.request_pty do |ch, success|
270
+ yield ch, success
271
+ end
272
+ else
273
+ yield channel, true
120
274
  end
121
275
  end
122
276
 
123
277
  def replace_placeholders(command, channel)
124
- command.gsub(/\$CAPISTRANO:HOST\$/, channel[:host])
278
+ roles = @tree.configuration && @tree.configuration.role_names_for_host(channel[:server])
279
+ command = command.gsub(/\$CAPISTRANO:HOST\$/, channel[:host])
280
+ command.gsub!(/\$CAPISTRANO:HOSTROLES\$/, roles.join(',')) if roles
281
+ command
125
282
  end
126
283
 
127
284
  # prepare a space-separated sequence of variables assignments
@@ -1,4 +1,4 @@
1
- require 'capistrano/upload'
1
+ require 'capistrano/transfer'
2
2
 
3
3
  module Capistrano
4
4
  class Configuration
@@ -9,23 +9,38 @@ module Capistrano
9
9
  # by the current task. If <tt>:mode</tt> is specified it is used to
10
10
  # set the mode on the file.
11
11
  def put(data, path, options={})
12
- execute_on_servers(options) do |servers|
13
- targets = servers.map { |s| sessions[s] }
14
- Upload.process(targets, path, :data => data, :mode => options[:mode], :logger => logger)
15
- end
12
+ opts = options.dup
13
+ upload(StringIO.new(data), path, opts)
16
14
  end
17
-
18
- # Get file remote_path from FIRST server targetted by
15
+
16
+ # Get file remote_path from FIRST server targeted by
19
17
  # the current task and transfer it to local machine as path.
20
18
  #
21
19
  # get "#{deploy_to}/current/log/production.log", "log/production.log.web"
22
- def get(remote_path, path, options = {})
23
- execute_on_servers(options.merge(:once => true)) do |servers|
24
- logger.info "downloading `#{servers.first.host}:#{remote_path}' to `#{path}'"
25
- sftp = sessions[servers.first].sftp
26
- sftp.connect unless sftp.state == :open
27
- sftp.get_file remote_path, path
28
- logger.debug "download finished"
20
+ def get(remote_path, path, options={}, &block)
21
+ download(remote_path, path, options.merge(:once => true), &block)
22
+ end
23
+
24
+ def upload(from, to, options={}, &block)
25
+ mode = options.delete(:mode)
26
+ transfer(:up, from, to, options, &block)
27
+ if mode
28
+ mode = mode.is_a?(Numeric) ? mode.to_s(8) : mode.to_s
29
+ run "chmod #{mode} #{to}", options
30
+ end
31
+ end
32
+
33
+ def download(from, to, options={}, &block)
34
+ transfer(:down, from, to, options, &block)
35
+ end
36
+
37
+ def transfer(direction, from, to, options={}, &block)
38
+ if dry_run
39
+ return logger.debug "transfering: #{[direction, from, to] * ', '}"
40
+ end
41
+ execute_on_servers(options) do |servers|
42
+ targets = servers.map { |s| sessions[s] }
43
+ Transfer.process(direction, from, to, targets, options.merge(:logger => logger), &block)
29
44
  end
30
45
  end
31
46
 
@@ -20,7 +20,7 @@ module Capistrano
20
20
  # stream "tail -f #{shared_path}/log/fastcgi.crash.log"
21
21
  # end
22
22
  def stream(command, options={})
23
- invoke_command(command, options) do |ch, stream, out|
23
+ invoke_command(command, options.merge(:eof => !command.include?(sudo))) do |ch, stream, out|
24
24
  puts out if stream == :out
25
25
  warn "[err :: #{ch[:server]}] #{out}" if stream == :err
26
26
  end
@@ -31,10 +31,10 @@ module Capistrano
31
31
  # string. The command is invoked via #invoke_command.
32
32
  def capture(command, options={})
33
33
  output = ""
34
- invoke_command(command, options.merge(:once => true)) do |ch, stream, data|
34
+ invoke_command(command, options.merge(:once => true, :eof => !command.include?(sudo))) do |ch, stream, data|
35
35
  case stream
36
36
  when :out then output << data
37
- when :err then raise CaptureError, "error processing #{command.inspect}: #{data.inspect}"
37
+ when :err then warn "[err :: #{ch[:server]}] #{data}"
38
38
  end
39
39
  end
40
40
  output