mbailey-capistrano 2.5.5

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 (105) hide show
  1. data/CHANGELOG.rdoc +761 -0
  2. data/Manifest +104 -0
  3. data/README.rdoc +66 -0
  4. data/Rakefile +34 -0
  5. data/bin/cap +4 -0
  6. data/bin/capify +78 -0
  7. data/examples/sample.rb +14 -0
  8. data/lib/capistrano/callback.rb +45 -0
  9. data/lib/capistrano/cli/execute.rb +84 -0
  10. data/lib/capistrano/cli/help.rb +125 -0
  11. data/lib/capistrano/cli/help.txt +75 -0
  12. data/lib/capistrano/cli/options.rb +224 -0
  13. data/lib/capistrano/cli/ui.rb +40 -0
  14. data/lib/capistrano/cli.rb +47 -0
  15. data/lib/capistrano/command.rb +283 -0
  16. data/lib/capistrano/configuration/actions/file_transfer.rb +47 -0
  17. data/lib/capistrano/configuration/actions/inspect.rb +46 -0
  18. data/lib/capistrano/configuration/actions/invocation.rb +293 -0
  19. data/lib/capistrano/configuration/callbacks.rb +148 -0
  20. data/lib/capistrano/configuration/connections.rb +200 -0
  21. data/lib/capistrano/configuration/execution.rb +132 -0
  22. data/lib/capistrano/configuration/loading.rb +197 -0
  23. data/lib/capistrano/configuration/namespaces.rb +197 -0
  24. data/lib/capistrano/configuration/roles.rb +73 -0
  25. data/lib/capistrano/configuration/servers.rb +85 -0
  26. data/lib/capistrano/configuration/variables.rb +127 -0
  27. data/lib/capistrano/configuration.rb +43 -0
  28. data/lib/capistrano/errors.rb +15 -0
  29. data/lib/capistrano/extensions.rb +57 -0
  30. data/lib/capistrano/logger.rb +59 -0
  31. data/lib/capistrano/processable.rb +53 -0
  32. data/lib/capistrano/recipes/compat.rb +32 -0
  33. data/lib/capistrano/recipes/deploy/dependencies.rb +44 -0
  34. data/lib/capistrano/recipes/deploy/local_dependency.rb +54 -0
  35. data/lib/capistrano/recipes/deploy/remote_dependency.rb +105 -0
  36. data/lib/capistrano/recipes/deploy/scm/accurev.rb +169 -0
  37. data/lib/capistrano/recipes/deploy/scm/base.rb +196 -0
  38. data/lib/capistrano/recipes/deploy/scm/bzr.rb +83 -0
  39. data/lib/capistrano/recipes/deploy/scm/cvs.rb +152 -0
  40. data/lib/capistrano/recipes/deploy/scm/darcs.rb +85 -0
  41. data/lib/capistrano/recipes/deploy/scm/git.rb +271 -0
  42. data/lib/capistrano/recipes/deploy/scm/mercurial.rb +137 -0
  43. data/lib/capistrano/recipes/deploy/scm/none.rb +44 -0
  44. data/lib/capistrano/recipes/deploy/scm/perforce.rb +133 -0
  45. data/lib/capistrano/recipes/deploy/scm/subversion.rb +121 -0
  46. data/lib/capistrano/recipes/deploy/scm.rb +19 -0
  47. data/lib/capistrano/recipes/deploy/strategy/base.rb +79 -0
  48. data/lib/capistrano/recipes/deploy/strategy/checkout.rb +20 -0
  49. data/lib/capistrano/recipes/deploy/strategy/copy.rb +210 -0
  50. data/lib/capistrano/recipes/deploy/strategy/export.rb +20 -0
  51. data/lib/capistrano/recipes/deploy/strategy/remote.rb +52 -0
  52. data/lib/capistrano/recipes/deploy/strategy/remote_cache.rb +56 -0
  53. data/lib/capistrano/recipes/deploy/strategy.rb +19 -0
  54. data/lib/capistrano/recipes/deploy/templates/maintenance.rhtml +53 -0
  55. data/lib/capistrano/recipes/deploy.rb +562 -0
  56. data/lib/capistrano/recipes/standard.rb +37 -0
  57. data/lib/capistrano/recipes/templates/maintenance.rhtml +53 -0
  58. data/lib/capistrano/recipes/upgrade.rb +33 -0
  59. data/lib/capistrano/role.rb +102 -0
  60. data/lib/capistrano/server_definition.rb +56 -0
  61. data/lib/capistrano/shell.rb +260 -0
  62. data/lib/capistrano/ssh.rb +99 -0
  63. data/lib/capistrano/task_definition.rb +70 -0
  64. data/lib/capistrano/transfer.rb +216 -0
  65. data/lib/capistrano/version.rb +18 -0
  66. data/lib/capistrano.rb +2 -0
  67. data/setup.rb +1346 -0
  68. data/test/cli/execute_test.rb +132 -0
  69. data/test/cli/help_test.rb +165 -0
  70. data/test/cli/options_test.rb +317 -0
  71. data/test/cli/ui_test.rb +28 -0
  72. data/test/cli_test.rb +17 -0
  73. data/test/command_test.rb +286 -0
  74. data/test/configuration/actions/file_transfer_test.rb +61 -0
  75. data/test/configuration/actions/inspect_test.rb +65 -0
  76. data/test/configuration/actions/invocation_test.rb +224 -0
  77. data/test/configuration/callbacks_test.rb +220 -0
  78. data/test/configuration/connections_test.rb +349 -0
  79. data/test/configuration/execution_test.rb +175 -0
  80. data/test/configuration/loading_test.rb +132 -0
  81. data/test/configuration/namespace_dsl_test.rb +311 -0
  82. data/test/configuration/roles_test.rb +144 -0
  83. data/test/configuration/servers_test.rb +121 -0
  84. data/test/configuration/variables_test.rb +184 -0
  85. data/test/configuration_test.rb +88 -0
  86. data/test/deploy/local_dependency_test.rb +76 -0
  87. data/test/deploy/remote_dependency_test.rb +114 -0
  88. data/test/deploy/scm/accurev_test.rb +23 -0
  89. data/test/deploy/scm/base_test.rb +55 -0
  90. data/test/deploy/scm/git_test.rb +167 -0
  91. data/test/deploy/scm/mercurial_test.rb +129 -0
  92. data/test/deploy/strategy/copy_test.rb +258 -0
  93. data/test/extensions_test.rb +69 -0
  94. data/test/fixtures/cli_integration.rb +5 -0
  95. data/test/fixtures/config.rb +5 -0
  96. data/test/fixtures/custom.rb +3 -0
  97. data/test/logger_test.rb +123 -0
  98. data/test/role_test.rb +11 -0
  99. data/test/server_definition_test.rb +121 -0
  100. data/test/shell_test.rb +90 -0
  101. data/test/ssh_test.rb +104 -0
  102. data/test/task_definition_test.rb +101 -0
  103. data/test/transfer_test.rb +160 -0
  104. data/test/utils.rb +38 -0
  105. metadata +205 -0
@@ -0,0 +1,127 @@
1
+ require 'thread'
2
+
3
+ module Capistrano
4
+ class Configuration
5
+ module Variables
6
+ def self.included(base) #:nodoc:
7
+ %w(initialize respond_to? method_missing).each do |m|
8
+ base_name = m[/^\w+/]
9
+ punct = m[/\W+$/]
10
+ base.send :alias_method, "#{base_name}_without_variables#{punct}", m
11
+ base.send :alias_method, m, "#{base_name}_with_variables#{punct}"
12
+ end
13
+ end
14
+
15
+ # The hash of variables that have been defined in this configuration
16
+ # instance.
17
+ attr_reader :variables
18
+
19
+ # Set a variable to the given value.
20
+ def set(variable, *args, &block)
21
+ if variable.to_s !~ /^[_a-z]/
22
+ raise ArgumentError, "invalid variable `#{variable}' (variables must begin with an underscore, or a lower-case letter)"
23
+ end
24
+
25
+ if !block_given? && args.empty? || block_given? && !args.empty?
26
+ raise ArgumentError, "you must specify exactly one of either a value or a block"
27
+ end
28
+
29
+ if args.length > 1
30
+ raise ArgumentError, "wrong number of arguments (#{args.length} for 1)"
31
+ end
32
+
33
+ value = args.empty? ? block : args.first
34
+ sym = variable.to_sym
35
+ protect(sym) { @variables[sym] = value }
36
+ end
37
+
38
+ alias :[]= :set
39
+
40
+ # Removes any trace of the given variable.
41
+ def unset(variable)
42
+ sym = variable.to_sym
43
+ protect(sym) do
44
+ @original_procs.delete(sym)
45
+ @variables.delete(sym)
46
+ end
47
+ end
48
+
49
+ # Returns true if the variable has been defined, and false otherwise.
50
+ def exists?(variable)
51
+ @variables.key?(variable.to_sym)
52
+ end
53
+
54
+ # If the variable was originally a proc value, it will be reset to it's
55
+ # original proc value. Otherwise, this method does nothing. It returns
56
+ # true if the variable was actually reset.
57
+ def reset!(variable)
58
+ sym = variable.to_sym
59
+ protect(sym) do
60
+ if @original_procs.key?(sym)
61
+ @variables[sym] = @original_procs.delete(sym)
62
+ true
63
+ else
64
+ false
65
+ end
66
+ end
67
+ end
68
+
69
+ # Access a named variable. If the value of the variable responds_to? :call,
70
+ # #call will be invoked (without parameters) and the return value cached
71
+ # and returned.
72
+ def fetch(variable, *args)
73
+ if !args.empty? && block_given?
74
+ raise ArgumentError, "you must specify either a default value or a block, but not both"
75
+ end
76
+
77
+ sym = variable.to_sym
78
+ protect(sym) do
79
+ if !@variables.key?(sym)
80
+ return args.first unless args.empty?
81
+ return yield(variable) if block_given?
82
+ raise IndexError, "`#{variable}' not found"
83
+ end
84
+
85
+ if @variables[sym].respond_to?(:call)
86
+ @original_procs[sym] = @variables[sym]
87
+ @variables[sym] = @variables[sym].call
88
+ end
89
+ end
90
+
91
+ @variables[sym]
92
+ end
93
+
94
+ def [](variable)
95
+ fetch(variable, nil)
96
+ end
97
+
98
+ def initialize_with_variables(*args) #:nodoc:
99
+ initialize_without_variables(*args)
100
+ @variables = {}
101
+ @original_procs = {}
102
+ @variable_locks = Hash.new { |h,k| h[k] = Mutex.new }
103
+
104
+ set :ssh_options, {}
105
+ set :logger, logger
106
+ end
107
+ private :initialize_with_variables
108
+
109
+ def protect(variable)
110
+ @variable_locks[variable.to_sym].synchronize { yield }
111
+ end
112
+ private :protect
113
+
114
+ def respond_to_with_variables?(sym, include_priv=false) #:nodoc:
115
+ @variables.has_key?(sym) || respond_to_without_variables?(sym, include_priv)
116
+ end
117
+
118
+ def method_missing_with_variables(sym, *args, &block) #:nodoc:
119
+ if args.length == 0 && block.nil? && @variables.has_key?(sym)
120
+ self[sym]
121
+ else
122
+ method_missing_without_variables(sym, *args, &block)
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,43 @@
1
+ require 'capistrano/logger'
2
+
3
+ require 'capistrano/configuration/callbacks'
4
+ require 'capistrano/configuration/connections'
5
+ require 'capistrano/configuration/execution'
6
+ require 'capistrano/configuration/loading'
7
+ require 'capistrano/configuration/namespaces'
8
+ require 'capistrano/configuration/roles'
9
+ require 'capistrano/configuration/servers'
10
+ require 'capistrano/configuration/variables'
11
+
12
+ require 'capistrano/configuration/actions/file_transfer'
13
+ require 'capistrano/configuration/actions/inspect'
14
+ require 'capistrano/configuration/actions/invocation'
15
+
16
+ module Capistrano
17
+ # Represents a specific Capistrano configuration. A Configuration instance
18
+ # may be used to load multiple recipe files, define and describe tasks,
19
+ # define roles, and set configuration variables.
20
+ class Configuration
21
+ # The logger instance defined for this configuration.
22
+ attr_accessor :debug, :logger, :dry_run
23
+
24
+ def initialize #:nodoc:
25
+ @debug = false
26
+ @dry_run = false
27
+ @logger = Logger.new
28
+ end
29
+
30
+ # make the DSL easier to read when using lazy evaluation via lambdas
31
+ alias defer lambda
32
+
33
+ # The includes must come at the bottom, since they may redefine methods
34
+ # defined in the base class.
35
+ include Connections, Execution, Loading, Namespaces, Roles, Servers, Variables
36
+
37
+ # Mix in the actions
38
+ include Actions::FileTransfer, Actions::Inspect, Actions::Invocation
39
+
40
+ # Must mix last, because it hooks into previously defined methods
41
+ include Callbacks
42
+ end
43
+ end
@@ -0,0 +1,15 @@
1
+ module Capistrano
2
+ class Error < RuntimeError; end
3
+
4
+ class CaptureError < Error; end
5
+ class NoSuchTaskError < Error; end
6
+ class NoMatchingServersError < Error; end
7
+
8
+ class RemoteError < Error
9
+ attr_accessor :hosts
10
+ end
11
+
12
+ class ConnectionError < RemoteError; end
13
+ class TransferError < RemoteError; end
14
+ class CommandError < RemoteError; end
15
+ end
@@ -0,0 +1,57 @@
1
+ module Capistrano
2
+ class ExtensionProxy #:nodoc:
3
+ def initialize(config, mod)
4
+ @config = config
5
+ extend(mod)
6
+ end
7
+
8
+ def method_missing(sym, *args, &block)
9
+ @config.send(sym, *args, &block)
10
+ end
11
+ end
12
+
13
+ # Holds the set of registered plugins, keyed by name (where the name is a
14
+ # symbol).
15
+ EXTENSIONS = {}
16
+
17
+ # Register the given module as a plugin with the given name. It will henceforth
18
+ # be available via a proxy object on Configuration instances, accessible by
19
+ # a method with the given name.
20
+ def self.plugin(name, mod)
21
+ name = name.to_sym
22
+ return false if EXTENSIONS.has_key?(name)
23
+
24
+ methods = Capistrano::Configuration.public_instance_methods +
25
+ Capistrano::Configuration.protected_instance_methods +
26
+ Capistrano::Configuration.private_instance_methods
27
+
28
+ if methods.any? { |m| m.to_sym == name }
29
+ raise Capistrano::Error, "registering a plugin named `#{name}' would shadow a method on Capistrano::Configuration with the same name"
30
+ end
31
+
32
+ Capistrano::Configuration.class_eval <<-STR, __FILE__, __LINE__+1
33
+ def #{name}
34
+ @__#{name}_proxy ||= Capistrano::ExtensionProxy.new(self, Capistrano::EXTENSIONS[#{name.inspect}])
35
+ end
36
+ STR
37
+
38
+ EXTENSIONS[name] = mod
39
+ return true
40
+ end
41
+
42
+ # Unregister the plugin with the given name.
43
+ def self.remove_plugin(name)
44
+ name = name.to_sym
45
+ if EXTENSIONS.delete(name)
46
+ Capistrano::Configuration.send(:remove_method, name)
47
+ return true
48
+ end
49
+
50
+ return false
51
+ end
52
+
53
+ def self.configuration(*args) #:nodoc:
54
+ warn "[DEPRECATION] Capistrano.configuration is deprecated. Use Capistrano::Configuration.instance instead"
55
+ Capistrano::Configuration.instance(*args)
56
+ end
57
+ end
@@ -0,0 +1,59 @@
1
+ module Capistrano
2
+ class Logger #:nodoc:
3
+ attr_accessor :level
4
+ attr_reader :device
5
+
6
+ IMPORTANT = 0
7
+ INFO = 1
8
+ DEBUG = 2
9
+ TRACE = 3
10
+
11
+ MAX_LEVEL = 3
12
+
13
+ def initialize(options={})
14
+ output = options[:output] || $stderr
15
+ if output.respond_to?(:puts)
16
+ @device = output
17
+ else
18
+ @device = File.open(output.to_str, "a")
19
+ @needs_close = true
20
+ end
21
+
22
+ @options = options
23
+ @level = 0
24
+ end
25
+
26
+ def close
27
+ device.close if @needs_close
28
+ end
29
+
30
+ def log(level, message, line_prefix=nil)
31
+ if level <= self.level
32
+ indent = "%*s" % [MAX_LEVEL, "*" * (MAX_LEVEL - level)]
33
+ (RUBY_VERSION >= "1.9" ? message.lines : message).each do |line|
34
+ if line_prefix
35
+ device.puts "#{indent} [#{line_prefix}] #{line.strip}\n"
36
+ else
37
+ device.puts "#{indent} #{line.strip}\n"
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ def important(message, line_prefix=nil)
44
+ log(IMPORTANT, message, line_prefix)
45
+ end
46
+
47
+ def info(message, line_prefix=nil)
48
+ log(INFO, message, line_prefix)
49
+ end
50
+
51
+ def debug(message, line_prefix=nil)
52
+ log(DEBUG, message, line_prefix)
53
+ end
54
+
55
+ def trace(message, line_prefix=nil)
56
+ log(TRACE, message, line_prefix)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,53 @@
1
+ module Capistrano
2
+ module Processable
3
+ module SessionAssociation
4
+ def self.on(exception, session)
5
+ unless exception.respond_to?(:session)
6
+ exception.extend(self)
7
+ exception.session = session
8
+ end
9
+
10
+ return exception
11
+ end
12
+
13
+ attr_accessor :session
14
+ end
15
+
16
+ def process_iteration(wait=nil, &block)
17
+ ensure_each_session { |session| session.preprocess }
18
+
19
+ return false if block && !block.call(self)
20
+
21
+ readers = sessions.map { |session| session.listeners.keys }.flatten.reject { |io| io.closed? }
22
+ writers = readers.select { |io| io.respond_to?(:pending_write?) && io.pending_write? }
23
+
24
+ if readers.any? || writers.any?
25
+ readers, writers, = IO.select(readers, writers, nil, wait)
26
+ end
27
+
28
+ if readers
29
+ ensure_each_session do |session|
30
+ ios = session.listeners.keys
31
+ session.postprocess(ios & readers, ios & writers)
32
+ end
33
+ end
34
+
35
+ true
36
+ end
37
+
38
+ def ensure_each_session
39
+ errors = []
40
+
41
+ sessions.each do |session|
42
+ begin
43
+ yield session
44
+ rescue Exception => error
45
+ errors << SessionAssociation.on(error, session)
46
+ end
47
+ end
48
+
49
+ raise errors.first if errors.any?
50
+ sessions
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,32 @@
1
+ # A collection of compatibility scripts, to ease the transition between
2
+ # Capistrano 1.x and Capistrano 2.x.
3
+
4
+ # Depends on the deployment system
5
+ load 'deploy'
6
+
7
+ map = { "diff_from_last_deploy" => "deploy:pending:diff",
8
+ "update" => "deploy:update",
9
+ "update_code" => "deploy:update_code",
10
+ "symlink" => "deploy:symlink",
11
+ "restart" => "deploy:restart",
12
+ "rollback" => "deploy:rollback",
13
+ "cleanup" => "deploy:cleanup",
14
+ "disable_web" => "deploy:web:disable",
15
+ "enable_web" => "deploy:web:enable",
16
+ "cold_deploy" => "deploy:cold",
17
+ "deploy_with_migrations" => "deploy:migrations" }
18
+
19
+ map.each do |old, new|
20
+ desc "DEPRECATED: See #{new}."
21
+ eval "task(#{old.inspect}) do
22
+ warn \"[DEPRECATED] `#{old}' is deprecated. Use `#{new}' instead.\"
23
+ find_and_execute_task(#{new.inspect})
24
+ end"
25
+ end
26
+
27
+ desc "DEPRECATED: See deploy:start."
28
+ task :spinner do
29
+ warn "[DEPRECATED] `spinner' is deprecated. Use `deploy:start' instead."
30
+ set :runner, fetch(:spinner_user, "app")
31
+ deploy.start
32
+ end
@@ -0,0 +1,44 @@
1
+ require 'capistrano/recipes/deploy/local_dependency'
2
+ require 'capistrano/recipes/deploy/remote_dependency'
3
+
4
+ module Capistrano
5
+ module Deploy
6
+ class Dependencies
7
+ include Enumerable
8
+
9
+ attr_reader :configuration
10
+
11
+ def initialize(configuration)
12
+ @configuration = configuration
13
+ @dependencies = []
14
+ yield self if block_given?
15
+ end
16
+
17
+ def check
18
+ yield self
19
+ self
20
+ end
21
+
22
+ def remote
23
+ dep = RemoteDependency.new(configuration)
24
+ @dependencies << dep
25
+ dep
26
+ end
27
+
28
+ def local
29
+ dep = LocalDependency.new(configuration)
30
+ @dependencies << dep
31
+ dep
32
+ end
33
+
34
+ def each
35
+ @dependencies.each { |d| yield d }
36
+ self
37
+ end
38
+
39
+ def pass?
40
+ all? { |d| d.pass? }
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,54 @@
1
+ module Capistrano
2
+ module Deploy
3
+ class LocalDependency
4
+ attr_reader :configuration
5
+ attr_reader :message
6
+
7
+ def initialize(configuration)
8
+ @configuration = configuration
9
+ @success = true
10
+ end
11
+
12
+ def command(command)
13
+ @message ||= "`#{command}' could not be found in the path on the local host"
14
+ @success = find_in_path(command)
15
+ self
16
+ end
17
+
18
+ def or(message)
19
+ @message = message
20
+ self
21
+ end
22
+
23
+ def pass?
24
+ @success
25
+ end
26
+
27
+ private
28
+
29
+ # Searches the path, looking for the given utility. If an executable
30
+ # file is found that matches the parameter, this returns true.
31
+ def find_in_path(utility)
32
+ path = (ENV['PATH'] || "").split(File::PATH_SEPARATOR)
33
+ suffixes = self.class.on_windows? ? self.class.windows_executable_extensions : [""]
34
+
35
+ path.each do |dir|
36
+ suffixes.each do |sfx|
37
+ file = File.join(dir, utility + sfx)
38
+ return true if File.executable?(file)
39
+ end
40
+ end
41
+
42
+ false
43
+ end
44
+
45
+ def self.on_windows?
46
+ RUBY_PLATFORM =~ /mswin|mingw/
47
+ end
48
+
49
+ def self.windows_executable_extensions
50
+ %w(.exe .bat .com .cmd)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,105 @@
1
+ require 'capistrano/errors'
2
+
3
+ module Capistrano
4
+ module Deploy
5
+ class RemoteDependency
6
+ attr_reader :configuration
7
+ attr_reader :hosts
8
+
9
+ def initialize(configuration)
10
+ @configuration = configuration
11
+ @success = true
12
+ @hosts = nil
13
+ end
14
+
15
+ def directory(path, options={})
16
+ @message ||= "`#{path}' is not a directory"
17
+ try("test -d #{path}", options)
18
+ self
19
+ end
20
+
21
+ def file(path, options={})
22
+ @message ||= "`#{path}' is not a file"
23
+ try("test -f #{path}", options)
24
+ self
25
+ end
26
+
27
+ def writable(path, options={})
28
+ @message ||= "`#{path}' is not writable"
29
+ try("test -w #{path}", options)
30
+ self
31
+ end
32
+
33
+ def command(command, options={})
34
+ @message ||= "`#{command}' could not be found in the path"
35
+ try("which #{command}", options)
36
+ self
37
+ end
38
+
39
+ def gem(name, version, options={})
40
+ @message ||= "gem `#{name}' #{version} could not be found"
41
+ gem_cmd = configuration.fetch(:gem_command, "gem")
42
+ try("#{gem_cmd} specification --version '#{version}' #{name} 2>&1 | awk 'BEGIN { s = 0 } /^name:/ { s = 1; exit }; END { if(s == 0) exit 1 }'", options)
43
+ self
44
+ end
45
+
46
+ def match(command, expect, options={})
47
+ expect = Regexp.new(Regexp.escape(expect.to_s)) unless expect.is_a?(Regexp)
48
+
49
+ output_per_server = {}
50
+ try("#{command} ", options) do |ch, stream, out|
51
+ output_per_server[ch[:server]] ||= ''
52
+ output_per_server[ch[:server]] += out
53
+ end
54
+
55
+ # It is possible for some of these commands to return a status != 0
56
+ # (for example, rake --version exits with a 1). For this check we
57
+ # just care if the output matches, so we reset the success flag.
58
+ @success = true
59
+
60
+ errored_hosts = []
61
+ output_per_server.each_pair do |server, output|
62
+ next if output =~ expect
63
+ errored_hosts << server
64
+ end
65
+
66
+ if errored_hosts.any?
67
+ @hosts = errored_hosts.join(', ')
68
+ output = output_per_server[errored_hosts.first]
69
+ @message = "the output #{output.inspect} from #{command.inspect} did not match #{expect.inspect}"
70
+ @success = false
71
+ end
72
+
73
+ self
74
+ end
75
+
76
+ def or(message)
77
+ @message = message
78
+ self
79
+ end
80
+
81
+ def pass?
82
+ @success
83
+ end
84
+
85
+ def message
86
+ s = @message.dup
87
+ s << " (#{@hosts})" if @hosts
88
+ s
89
+ end
90
+
91
+ private
92
+
93
+ def try(command, options)
94
+ return unless @success # short-circuit evaluation
95
+ configuration.invoke_command(command, options) do |ch,stream,out|
96
+ warn "#{ch[:server]}: #{out}" if stream == :err
97
+ yield ch, stream, out if block_given?
98
+ end
99
+ rescue Capistrano::CommandError => e
100
+ @success = false
101
+ @hosts = e.hosts.join(', ')
102
+ end
103
+ end
104
+ end
105
+ end