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
@@ -0,0 +1,102 @@
1
+ module Capistrano
2
+ class Role
3
+ include Enumerable
4
+
5
+ def initialize(*list)
6
+ @static_servers = []
7
+ @dynamic_servers = []
8
+ push(*list)
9
+ end
10
+
11
+ def each(&block)
12
+ servers.each &block
13
+ end
14
+
15
+ def push(*list)
16
+ options = list.last.is_a?(Hash) ? list.pop : {}
17
+ list.each do |item|
18
+ if item.respond_to?(:call)
19
+ @dynamic_servers << DynamicServerList.new(item, options)
20
+ else
21
+ @static_servers << self.class.wrap_server(item, options)
22
+ end
23
+ end
24
+ end
25
+ alias_method :<<, :push
26
+
27
+ def servers
28
+ @static_servers + dynamic_servers
29
+ end
30
+ alias_method :to_ary, :servers
31
+
32
+ def empty?
33
+ servers.empty?
34
+ end
35
+
36
+ def clear
37
+ @dynamic_servers.clear
38
+ @static_servers.clear
39
+ end
40
+
41
+ def include?(server)
42
+ servers.include?(server)
43
+ end
44
+
45
+ protected
46
+
47
+ # This is the combination of a block, a hash of options, and a cached value.
48
+ class DynamicServerList
49
+ def initialize (block, options)
50
+ @block = block
51
+ @options = options
52
+ @cached = []
53
+ @is_cached = false
54
+ end
55
+
56
+ # Convert to a list of ServerDefinitions
57
+ def to_ary
58
+ unless @is_cached
59
+ @cached = Role::wrap_list(@block.call(@options), @options)
60
+ @is_cached = true
61
+ end
62
+ @cached
63
+ end
64
+
65
+ # Clear the cached value
66
+ def reset!
67
+ @cached.clear
68
+ @is_cached = false
69
+ end
70
+ end
71
+
72
+ # Attribute reader for the cached results of executing the blocks in turn
73
+ def dynamic_servers
74
+ @dynamic_servers.inject([]) { |list, item| list.concat item }
75
+ end
76
+
77
+ # Wraps a string in a ServerDefinition, if it isn't already.
78
+ # This and wrap_list should probably go in ServerDefinition in some form.
79
+ def self.wrap_server (item, options)
80
+ item.is_a?(ServerDefinition) ? item : ServerDefinition.new(item, options)
81
+ end
82
+
83
+ # Turns a list, or something resembling a list, into a properly-formatted
84
+ # ServerDefinition list. Keep an eye on this one -- it's entirely too
85
+ # magical for its own good. In particular, if ServerDefinition ever inherits
86
+ # from Array, this will break.
87
+ def self.wrap_list (*list)
88
+ options = list.last.is_a?(Hash) ? list.pop : {}
89
+ if list.length == 1
90
+ if list.first.nil?
91
+ return []
92
+ elsif list.first.is_a?(Array)
93
+ list = list.first
94
+ end
95
+ end
96
+ options.merge! list.pop if list.last.is_a?(Hash)
97
+ list.map do |item|
98
+ self.wrap_server item, options
99
+ end
100
+ end
101
+ end
102
+ end
@@ -7,6 +7,11 @@ module Capistrano
7
7
  attr_reader :port
8
8
  attr_reader :options
9
9
 
10
+ # The default user name to use when a user name is not explicitly provided
11
+ def self.default_user
12
+ ENV['USER'] || ENV['USERNAME'] || "not-specified"
13
+ end
14
+
10
15
  def initialize(string, options={})
11
16
  @user, @host, @port = string.match(/^(?:([^;,:=]+)@|)(.*?)(?::(\d+)|)$/)[1,3]
12
17
 
@@ -48,4 +53,4 @@ module Capistrano
48
53
  end
49
54
  end
50
55
  end
51
- end
56
+ end
@@ -1,4 +1,5 @@
1
1
  require 'thread'
2
+ require 'capistrano/processable'
2
3
 
3
4
  module Capistrano
4
5
  # The Capistrano::Shell class is the guts of the "shell" task. It implements
@@ -6,6 +7,8 @@ module Capistrano
6
7
  # commands. It makes for a GREAT way to monitor systems, and perform quick
7
8
  # maintenance on one or more machines.
8
9
  class Shell
10
+ include Processable
11
+
9
12
  # A Readline replacement for platforms where readline is either
10
13
  # unavailable, or has not been installed.
11
14
  class ReadlineFallback #:nodoc:
@@ -61,6 +64,9 @@ INTRO
61
64
  return false
62
65
  when /^set -(\w)\s*(\S+)/
63
66
  set_option($1, $2)
67
+ when /^set :(.*)\s+(.*)/
68
+ configuration.set($1.to_sym, $2)
69
+ puts "updated :#{$1} to #{$2}"
64
70
  when /^(?:(with|on)\s*(\S+))?\s*(\S.*)?/i
65
71
  process_command($1, $2, $3)
66
72
  else
@@ -142,11 +148,13 @@ HELP
142
148
  # be invoked. Otherwise, it is executed as a command on all associated
143
149
  # servers.
144
150
  def exec(command)
145
- if command[0] == ?!
146
- exec_tasks(command[1..-1].split)
147
- else
148
- servers = connect(configuration.current_task)
149
- exec_command(command, servers)
151
+ @mutex.synchronize do
152
+ if command[0] == ?!
153
+ exec_tasks(command[1..-1].split)
154
+ else
155
+ servers = connect(configuration.current_task)
156
+ exec_command(command, servers)
157
+ end
150
158
  end
151
159
  ensure
152
160
  STDOUT.flush
@@ -166,24 +174,13 @@ HELP
166
174
 
167
175
  # Execute a command on the given list of servers.
168
176
  def exec_command(command, servers)
169
- processor = Proc.new do |ch, stream, out|
170
- # TODO: more robust prompt detection
171
- out.each do |line|
172
- if stream == :out
173
- if out =~ /Password:\s*/i
174
- ch.send_data "#{configuration[:password]}\n"
175
- else
176
- puts "[#{ch[:server]}] #{line.chomp}"
177
- end
178
- elsif stream == :err
179
- puts "[#{ch[:server]} ERR] #{line.chomp}"
180
- end
181
- end
182
- end
183
-
184
- previous = trap("INT") { cmd.stop! }
177
+ command = command.gsub(/^(\s*)sudo\b|([|;&])\s*sudo\b/, "\\0 -p '#{configuration.sudo_prompt}'")
178
+ processor = configuration.sudo_behavior_callback(Configuration.default_io_proc)
185
179
  sessions = servers.map { |server| configuration.sessions[server] }
186
- Command.process(command, sessions, :logger => configuration.logger, &Capistrano::Configuration.default_io_proc)
180
+ options = configuration.add_default_command_options({})
181
+ cmd = Command.new(command, sessions, options.merge(:logger => configuration.logger), &processor)
182
+ previous = trap("INT") { cmd.stop! }
183
+ cmd.process!
187
184
  rescue Capistrano::Error => error
188
185
  warn "error: #{error.message}"
189
186
  ensure
@@ -205,20 +202,15 @@ HELP
205
202
  # thread and generally gets things ready for the REPL.
206
203
  def setup
207
204
  configuration.logger.level = Capistrano::Logger::INFO
205
+ wait_for = 0.1
208
206
 
209
207
  @mutex = Mutex.new
210
208
  @bgthread = Thread.new do
211
- loop do
212
- ready = configuration.sessions.values.select { |sess| sess.connection.reader_ready? }
213
- if ready.empty?
214
- sleep 0.1
215
- else
216
- @mutex.synchronize do
217
- ready.each { |session| session.connection.process(true) }
218
- end
219
- end
220
- end
209
+ loop do
210
+ ret = @mutex.synchronize { process_iteration(wait_for) }
211
+ sleep wait_for if !ret
221
212
  end
213
+ end
222
214
  end
223
215
 
224
216
  # Set the given option to +value+.
@@ -256,7 +248,7 @@ HELP
256
248
  old_var, ENV[env_var] = ENV[env_var], (scope_value == "all" ? nil : scope_value) if env_var
257
249
  if command
258
250
  begin
259
- @mutex.synchronize { exec(command) }
251
+ exec(command)
260
252
  ensure
261
253
  ENV[env_var] = old_var if env_var
262
254
  end
@@ -264,5 +256,10 @@ HELP
264
256
  puts "scoping #{scope_type} #{scope_value}"
265
257
  end
266
258
  end
259
+
260
+ # All open sessions, needed to satisfy the Command::Processable include
261
+ def sessions
262
+ configuration.sessions.values
263
+ end
267
264
  end
268
265
  end
@@ -1,55 +1,6 @@
1
1
  require 'net/ssh'
2
2
 
3
3
  module Capistrano
4
- unless ENV['SKIP_VERSION_CHECK']
5
- require 'capistrano/version'
6
- require 'net/ssh/version'
7
- ssh_version = [Net::SSH::Version::MAJOR, Net::SSH::Version::MINOR, Net::SSH::Version::TINY]
8
- if !Version.check(Version::SSH_REQUIRED, ssh_version)
9
- raise "You have Net::SSH #{ssh_version.join(".")}, but you need at least #{Version::SSH_REQUIRED.join(".")}"
10
- end
11
- end
12
-
13
- # Now, Net::SSH is kind of silly, and tries to lazy-load everything. This
14
- # wreaks havoc with the parallel connection trick that Capistrano wants to
15
- # use, so we're going to do something hideously ugly here and force all the
16
- # files that Net::SSH uses to load RIGHT NOW, rather than lazily.
17
-
18
- net_ssh_dependencies = %w(connection/services connection/channel connection/driver
19
- service/agentforward/services service/agentforward/driver
20
- service/process/driver util/prompter
21
- service/forward/services service/forward/driver service/forward/local-network-handler service/forward/remote-network-handler
22
- service/shell/services service/shell/driver
23
- lenient-host-key-verifier
24
- transport/compress/services transport/compress/zlib-compressor transport/compress/none-compressor transport/compress/zlib-decompressor transport/compress/none-decompressor
25
- transport/kex/services transport/kex/dh transport/kex/dh-gex
26
- transport/ossl/services
27
- transport/ossl/hmac/services transport/ossl/hmac/sha1 transport/ossl/hmac/sha1-96 transport/ossl/hmac/md5 transport/ossl/hmac/md5-96 transport/ossl/hmac/none
28
- transport/ossl/cipher-factory transport/ossl/hmac-factory transport/ossl/buffer-factory transport/ossl/key-factory transport/ossl/digest-factory
29
- transport/identity-cipher transport/packet-stream transport/version-negotiator transport/algorithm-negotiator transport/session
30
- userauth/methods/services userauth/methods/password userauth/methods/keyboard-interactive userauth/methods/publickey userauth/methods/hostbased
31
- userauth/services userauth/agent userauth/userkeys userauth/driver
32
- transport/services service/services
33
- )
34
-
35
- net_ssh_dependencies << "userauth/pageant" if File::ALT_SEPARATOR
36
- net_ssh_dependencies.each do |path|
37
- begin
38
- require "net/ssh/#{path}"
39
- rescue LoadError
40
- # Ignore load errors from this, since some files are in the list which
41
- # do not exist in different (supported) versions of Net::SSH. We know
42
- # (by this point) that Net::SSH is installed, though, since we do a
43
- # require 'net/ssh' at the very top of this file, and we know the
44
- # installed version meets the minimum version requirements because of
45
- # the version check, also at the top of this file. So, if we get a
46
- # LoadError, it's simply because the file in question does not exist in
47
- # the version of Net::SSH that is installed.
48
- #
49
- # Whew!
50
- end
51
- end
52
-
53
4
  # A helper class for dealing with SSH connections.
54
5
  class SSH
55
6
  # Patch an accessor onto an SSH connection so that we can record the server
@@ -66,9 +17,6 @@ module Capistrano
66
17
  attr_accessor :xserver
67
18
  end
68
19
 
69
- # The default port for SSH.
70
- DEFAULT_PORT = 22
71
-
72
20
  # An abstraction to make it possible to connect to the server via public key
73
21
  # without prompting for the password. If the public key authentication fails
74
22
  # this will fall back to password authentication.
@@ -82,13 +30,53 @@ module Capistrano
82
30
  # constructor. Values in +options+ are then merged into it, and any
83
31
  # connection information in +server+ is added last, so that +server+ info
84
32
  # takes precedence over +options+, which takes precendence over ssh_options.
85
- def self.connect(server, options={}, &block)
33
+ def self.connect(server, options={})
34
+ connection_strategy(server, options) do |host, user, connection_options|
35
+ connection = Net::SSH.start(host, user, connection_options)
36
+ Server.apply_to(connection, server)
37
+ end
38
+ end
39
+
40
+ # Abstracts the logic for establishing an SSH connection (which includes
41
+ # testing for connection failures and retrying with a password, and so forth,
42
+ # mostly made complicated because of the fact that some of these variables
43
+ # might be lazily evaluated and try to do something like prompt the user,
44
+ # which should only happen when absolutely necessary.
45
+ #
46
+ # This will yield the hostname, username, and a hash of connection options
47
+ # to the given block, which should return a new connection.
48
+ def self.connection_strategy(server, options={}, &block)
86
49
  methods = [ %w(publickey hostbased), %w(password keyboard-interactive) ]
87
50
  password_value = nil
88
-
89
- ssh_options = (options[:ssh_options] || {}).dup
90
- ssh_options[:username] = server.user || options[:user] || ssh_options[:username]
91
- ssh_options[:port] = server.port || options[:port] || ssh_options[:port] || DEFAULT_PORT
51
+
52
+ # construct the hash of ssh options that should be passed more-or-less
53
+ # directly to Net::SSH. This will be the general ssh options, merged with
54
+ # the server-specific ssh-options.
55
+ ssh_options = (options[:ssh_options] || {}).merge(server.options[:ssh_options] || {})
56
+
57
+ # load any SSH configuration files that were specified in the SSH options. This
58
+ # will load from ~/.ssh/config and /etc/ssh_config by default (see Net::SSH
59
+ # for details). Merge the explicitly given ssh_options over the top of the info
60
+ # from the config file.
61
+ ssh_options = Net::SSH.configuration_for(server.host, ssh_options.fetch(:config, true)).merge(ssh_options)
62
+
63
+ # Once we've loaded the config, we don't need Net::SSH to do it again.
64
+ ssh_options[:config] = false
65
+
66
+ ssh_options[:verbose] = :debug if options[:verbose] && options[:verbose] > 0
67
+
68
+ user = server.user || options[:user] || ssh_options[:username] ||
69
+ ssh_options[:user] || ServerDefinition.default_user
70
+ port = server.port || options[:port] || ssh_options[:port]
71
+
72
+ # the .ssh/config file might have changed the host-name on us
73
+ host = ssh_options.fetch(:host_name, server.host)
74
+
75
+ ssh_options[:port] = port if port
76
+
77
+ # delete these, since we've determined which username to use by this point
78
+ ssh_options.delete(:username)
79
+ ssh_options.delete(:user)
92
80
 
93
81
  begin
94
82
  connection_options = ssh_options.merge(
@@ -96,9 +84,7 @@ module Capistrano
96
84
  :auth_methods => ssh_options[:auth_methods] || methods.shift
97
85
  )
98
86
 
99
- connection = Net::SSH.start(server.host, connection_options, &block)
100
- Server.apply_to(connection, server)
101
-
87
+ yield host, user, connection_options
102
88
  rescue Net::SSH::AuthenticationFailed
103
89
  raise if methods.empty? || ssh_options[:auth_methods]
104
90
  password_value = options[:password]
@@ -1,18 +1,20 @@
1
1
  require 'capistrano/server_definition'
2
2
 
3
3
  module Capistrano
4
- # Represents the definition of a single task.
4
+
5
5
  class TaskDefinition
6
- attr_reader :name, :namespace, :options, :body, :desc, :on_error
6
+
7
+ attr_reader :name, :namespace, :options, :body, :desc, :on_error, :max_hosts
7
8
 
8
9
  def initialize(name, namespace, options={}, &block)
9
10
  @name, @namespace, @options = name, namespace, options
10
- @desc = @options.delete(:desc)
11
- @on_error = options.delete(:on_error)
12
- @body = block or raise ArgumentError, "a task requires a block"
13
- @servers = nil
11
+ @desc = @options.delete(:desc)
12
+ @on_error = options.delete(:on_error)
13
+ @max_hosts = options[:max_hosts] && options[:max_hosts].to_i
14
+ @body = block or raise ArgumentError, "a task requires a block"
15
+ @servers = nil
14
16
  end
15
-
17
+
16
18
  # Returns the task's fully-qualified name, including the namespace
17
19
  def fully_qualified_name
18
20
  @fully_qualified_name ||= begin
@@ -24,6 +26,11 @@ module Capistrano
24
26
  end
25
27
  end
26
28
 
29
+ def name=(value)
30
+ raise ArgumentError, "expected a valid task name" if !value.respond_to?(:to_sym)
31
+ @name = value.to_sym
32
+ end
33
+
27
34
  # Returns the description for this task, with newlines collapsed and
28
35
  # whitespace stripped. Returns the empty string if there is no
29
36
  # description for this task.
@@ -65,5 +72,6 @@ module Capistrano
65
72
  def continue_on_error?
66
73
  @on_error == :continue
67
74
  end
75
+
68
76
  end
69
- end
77
+ end
@@ -0,0 +1,218 @@
1
+ require 'net/scp'
2
+ require 'net/sftp'
3
+
4
+ require 'capistrano/processable'
5
+
6
+ module Capistrano
7
+ class Transfer
8
+ include Processable
9
+
10
+ def self.process(direction, from, to, sessions, options={}, &block)
11
+ new(direction, from, to, sessions, options, &block).process!
12
+ end
13
+
14
+ attr_reader :sessions
15
+ attr_reader :options
16
+ attr_reader :callback
17
+
18
+ attr_reader :transport
19
+ attr_reader :direction
20
+ attr_reader :from
21
+ attr_reader :to
22
+
23
+ attr_reader :logger
24
+ attr_reader :transfers
25
+
26
+ def initialize(direction, from, to, sessions, options={}, &block)
27
+ @direction = direction
28
+ @from = from
29
+ @to = to
30
+ @sessions = sessions
31
+ @options = options
32
+ @callback = block
33
+
34
+ @transport = options.fetch(:via, :sftp)
35
+ @logger = options.delete(:logger)
36
+
37
+ @session_map = {}
38
+
39
+ prepare_transfers
40
+ end
41
+
42
+ def process!
43
+ loop do
44
+ begin
45
+ break unless process_iteration { active? }
46
+ rescue Exception => error
47
+ if error.respond_to?(:session)
48
+ handle_error(error)
49
+ else
50
+ raise
51
+ end
52
+ end
53
+ end
54
+
55
+ failed = transfers.select { |txfr| txfr[:failed] }
56
+ if failed.any?
57
+ hosts = failed.map { |txfr| txfr[:server] }
58
+ errors = failed.map { |txfr| "#{txfr[:error]} (#{txfr[:error].message})" }.uniq.join(", ")
59
+ error = TransferError.new("#{operation} via #{transport} failed on #{hosts.join(',')}: #{errors}")
60
+ error.hosts = hosts
61
+
62
+ logger.important(error.message) if logger
63
+ raise error
64
+ end
65
+
66
+ logger.debug "#{transport} #{operation} complete" if logger
67
+ self
68
+ end
69
+
70
+ def active?
71
+ transfers.any? { |transfer| transfer.active? }
72
+ end
73
+
74
+ def operation
75
+ "#{direction}load"
76
+ end
77
+
78
+ def sanitized_from
79
+ if from.responds_to?(:read)
80
+ "#<#{from.class}>"
81
+ else
82
+ from
83
+ end
84
+ end
85
+
86
+ def sanitized_to
87
+ if to.responds_to?(:read)
88
+ "#<#{to.class}>"
89
+ else
90
+ to
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ def session_map
97
+ @session_map
98
+ end
99
+
100
+ def prepare_transfers
101
+ logger.info "#{transport} #{operation} #{from} -> #{to}" if logger
102
+
103
+ @transfers = sessions.map do |session|
104
+ session_from = normalize(from, session)
105
+ session_to = normalize(to, session)
106
+
107
+ session_map[session] = case transport
108
+ when :sftp
109
+ prepare_sftp_transfer(session_from, session_to, session)
110
+ when :scp
111
+ prepare_scp_transfer(session_from, session_to, session)
112
+ else
113
+ raise ArgumentError, "unsupported transport type: #{transport.inspect}"
114
+ end
115
+ end
116
+ end
117
+
118
+ def prepare_scp_transfer(from, to, session)
119
+ real_callback = callback || Proc.new do |channel, name, sent, total|
120
+ logger.trace "[#{channel[:host]}] #{name}" if logger && sent == 0
121
+ end
122
+
123
+ channel = case direction
124
+ when :up
125
+ session.scp.upload(from, to, options, &real_callback)
126
+ when :down
127
+ session.scp.download(from, to, options, &real_callback)
128
+ else
129
+ raise ArgumentError, "unsupported transfer direction: #{direction.inspect}"
130
+ end
131
+
132
+ channel[:server] = session.xserver
133
+ channel[:host] = session.xserver.host
134
+
135
+ return channel
136
+ end
137
+
138
+ class SFTPTransferWrapper
139
+ attr_reader :operation
140
+
141
+ def initialize(session, &callback)
142
+ session.sftp(false).connect do |sftp|
143
+ @operation = callback.call(sftp)
144
+ end
145
+ end
146
+
147
+ def active?
148
+ @operation.nil? || @operation.active?
149
+ end
150
+
151
+ def [](key)
152
+ @operation[key]
153
+ end
154
+
155
+ def []=(key, value)
156
+ @operation[key] = value
157
+ end
158
+
159
+ def abort!
160
+ @operation.abort!
161
+ end
162
+ end
163
+
164
+ def prepare_sftp_transfer(from, to, session)
165
+ SFTPTransferWrapper.new(session) do |sftp|
166
+ real_callback = Proc.new do |event, op, *args|
167
+ if callback
168
+ callback.call(event, op, *args)
169
+ elsif event == :open
170
+ logger.trace "[#{op[:host]}] #{args[0].remote}"
171
+ elsif event == :finish
172
+ logger.trace "[#{op[:host]}] done"
173
+ end
174
+ end
175
+
176
+ opts = options.dup
177
+ opts[:properties] = (opts[:properties] || {}).merge(
178
+ :server => session.xserver,
179
+ :host => session.xserver.host)
180
+
181
+ case direction
182
+ when :up
183
+ sftp.upload(from, to, opts, &real_callback)
184
+ when :down
185
+ sftp.download(from, to, opts, &real_callback)
186
+ else
187
+ raise ArgumentError, "unsupported transfer direction: #{direction.inspect}"
188
+ end
189
+ end
190
+ end
191
+
192
+ def normalize(argument, session)
193
+ if argument.is_a?(String)
194
+ argument.gsub(/\$CAPISTRANO:HOST\$/, session.xserver.host)
195
+ elsif argument.respond_to?(:read)
196
+ pos = argument.pos
197
+ clone = StringIO.new(argument.read)
198
+ clone.pos = argument.pos = pos
199
+ clone
200
+ else
201
+ argument
202
+ end
203
+ end
204
+
205
+ def handle_error(error)
206
+ raise error if error.message.include?('expected a file to upload')
207
+
208
+ transfer = session_map[error.session]
209
+ transfer[:error] = error
210
+ transfer[:failed] = true
211
+
212
+ case transport
213
+ when :sftp then transfer.abort!
214
+ when :scp then transfer.close
215
+ end
216
+ end
217
+ end
218
+ end
@@ -1,22 +1,11 @@
1
1
  module Capistrano
2
- module Version #:nodoc:
3
- # A method for comparing versions of required modules. It expects two
4
- # arrays of integers as parameters, the first being the minimum version
5
- # required, and the second being the actual version available. It returns
6
- # true if the actual version is at least equal to the required version.
7
- def self.check(required, actual) #:nodoc:
8
- required = required.map { |v| "%06d" % v }.join(".")
9
- actual = actual.map { |v| "%06d" % v }.join(".")
10
- return actual >= required
11
- end
12
-
2
+ class Version
13
3
  MAJOR = 2
14
- MINOR = 0
15
- TINY = 0
4
+ MINOR = 15
5
+ PATCH = 2
16
6
 
17
- STRING = [MAJOR, MINOR, TINY].join(".")
18
-
19
- SSH_REQUIRED = [1,0,10]
20
- SFTP_REQUIRED = [1,1,0]
7
+ def self.to_s
8
+ "#{MAJOR}.#{MINOR}.#{PATCH}"
9
+ end
21
10
  end
22
11
  end
data/lib/capistrano.rb CHANGED
@@ -1,2 +1,5 @@
1
+ require 'capistrano/fix_rake_deprecated_dsl'
2
+
1
3
  require 'capistrano/configuration'
2
- require 'capistrano/extensions'
4
+ require 'capistrano/extensions'
5
+ require 'capistrano/ext/string'