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,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
@@ -0,0 +1,56 @@
1
+ module Capistrano
2
+ class ServerDefinition
3
+ include Comparable
4
+
5
+ attr_reader :host
6
+ attr_reader :user
7
+ attr_reader :port
8
+ attr_reader :options
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
+
15
+ def initialize(string, options={})
16
+ @user, @host, @port = string.match(/^(?:([^;,:=]+)@|)(.*?)(?::(\d+)|)$/)[1,3]
17
+
18
+ @options = options.dup
19
+ user_opt, port_opt = @options.delete(:user), @options.delete(:port)
20
+
21
+ @user ||= user_opt
22
+ @port ||= port_opt
23
+
24
+ @port = @port.to_i if @port
25
+ end
26
+
27
+ def <=>(server)
28
+ [host, port, user] <=> [server.host, server.port, server.user]
29
+ end
30
+
31
+ # Redefined, so that Array#uniq will work to remove duplicate server
32
+ # definitions, based solely on their host names.
33
+ def eql?(server)
34
+ host == server.host &&
35
+ user == server.user &&
36
+ port == server.port
37
+ end
38
+
39
+ alias :== :eql?
40
+
41
+ # Redefined, so that Array#uniq will work to remove duplicate server
42
+ # definitions, based on their connection information.
43
+ def hash
44
+ @hash ||= [host, user, port].hash
45
+ end
46
+
47
+ def to_s
48
+ @to_s ||= begin
49
+ s = host
50
+ s = "#{user}@#{s}" if user
51
+ s = "#{s}:#{port}" if port && port != 22
52
+ s
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,260 @@
1
+ require 'thread'
2
+ require 'capistrano/processable'
3
+
4
+ module Capistrano
5
+ # The Capistrano::Shell class is the guts of the "shell" task. It implements
6
+ # an interactive REPL interface that users can employ to execute tasks and
7
+ # commands. It makes for a GREAT way to monitor systems, and perform quick
8
+ # maintenance on one or more machines.
9
+ class Shell
10
+ include Processable
11
+
12
+ # A Readline replacement for platforms where readline is either
13
+ # unavailable, or has not been installed.
14
+ class ReadlineFallback #:nodoc:
15
+ HISTORY = []
16
+
17
+ def self.readline(prompt)
18
+ STDOUT.print(prompt)
19
+ STDOUT.flush
20
+ STDIN.gets
21
+ end
22
+ end
23
+
24
+ # The configuration instance employed by this shell
25
+ attr_reader :configuration
26
+
27
+ # Instantiate a new shell and begin executing it immediately.
28
+ def self.run(config)
29
+ new(config).run!
30
+ end
31
+
32
+ # Instantiate a new shell
33
+ def initialize(config)
34
+ @configuration = config
35
+ end
36
+
37
+ # Start the shell running. This method will block until the shell
38
+ # terminates.
39
+ def run!
40
+ setup
41
+
42
+ puts <<-INTRO
43
+ ====================================================================
44
+ Welcome to the interactive Capistrano shell! This is an experimental
45
+ feature, and is liable to change in future releases. Type 'help' for
46
+ a summary of how to use the shell.
47
+ --------------------------------------------------------------------
48
+ INTRO
49
+
50
+ loop do
51
+ break if !read_and_execute
52
+ end
53
+
54
+ @bgthread.kill
55
+ end
56
+
57
+ def read_and_execute
58
+ command = read_line
59
+
60
+ case command
61
+ when "?", "help" then help
62
+ when "quit", "exit" then
63
+ puts "exiting"
64
+ return false
65
+ when /^set -(\w)\s*(\S+)/
66
+ set_option($1, $2)
67
+ when /^(?:(with|on)\s*(\S+))?\s*(\S.*)?/i
68
+ process_command($1, $2, $3)
69
+ else
70
+ raise "eh?"
71
+ end
72
+
73
+ return true
74
+ end
75
+
76
+ private
77
+
78
+ # Present the prompt and read a single line from the console. It also
79
+ # detects ^D and returns "exit" in that case. Adds the input to the
80
+ # history, unless the input is empty. Loops repeatedly until a non-empty
81
+ # line is input.
82
+ def read_line
83
+ loop do
84
+ command = reader.readline("cap> ")
85
+
86
+ if command.nil?
87
+ command = "exit"
88
+ puts(command)
89
+ else
90
+ command.strip!
91
+ end
92
+
93
+ unless command.empty?
94
+ reader::HISTORY << command
95
+ return command
96
+ end
97
+ end
98
+ end
99
+
100
+ # Display a verbose help message.
101
+ def help
102
+ puts <<-HELP
103
+ --- HELP! ---------------------------------------------------
104
+ "Get me out of this thing. I just want to quit."
105
+ -> Easy enough. Just type "exit", or "quit". Or press ctrl-D.
106
+
107
+ "I want to execute a command on all servers."
108
+ -> Just type the command, and press enter. It will be passed,
109
+ verbatim, to all defined servers.
110
+
111
+ "What if I only want it to execute on a subset of them?"
112
+ -> No problem, just specify the list of servers, separated by
113
+ commas, before the command, with the `on' keyword:
114
+
115
+ cap> on app1.foo.com,app2.foo.com echo ping
116
+
117
+ "Nice, but can I specify the servers by role?"
118
+ -> You sure can. Just use the `with' keyword, followed by the
119
+ comma-delimited list of role names:
120
+
121
+ cap> with app,db echo ping
122
+
123
+ "Can I execute a Capistrano task from within this shell?"
124
+ -> Yup. Just prefix the task with an exclamation mark:
125
+
126
+ cap> !deploy
127
+ HELP
128
+ end
129
+
130
+ # Determine which servers the given task requires a connection to, and
131
+ # establish connections to them if necessary. Return the list of
132
+ # servers (names).
133
+ def connect(task)
134
+ servers = configuration.find_servers_for_task(task)
135
+ needing_connections = servers - configuration.sessions.keys
136
+ unless needing_connections.empty?
137
+ puts "[establishing connection(s) to #{needing_connections.join(', ')}]"
138
+ configuration.establish_connections_to(needing_connections)
139
+ end
140
+ servers
141
+ end
142
+
143
+ # Execute the given command. If the command is prefixed by an exclamation
144
+ # mark, it is assumed to refer to another capistrano task, which will
145
+ # be invoked. Otherwise, it is executed as a command on all associated
146
+ # servers.
147
+ def exec(command)
148
+ @mutex.synchronize do
149
+ if command[0] == ?!
150
+ exec_tasks(command[1..-1].split)
151
+ else
152
+ servers = connect(configuration.current_task)
153
+ exec_command(command, servers)
154
+ end
155
+ end
156
+ ensure
157
+ STDOUT.flush
158
+ end
159
+
160
+ # Given an array of task names, invoke them in sequence.
161
+ def exec_tasks(list)
162
+ list.each do |task_name|
163
+ task = configuration.find_task(task_name)
164
+ raise Capistrano::NoSuchTaskError, "no such task `#{task_name}'" unless task
165
+ connect(task)
166
+ configuration.execute_task(task)
167
+ end
168
+ rescue Capistrano::NoMatchingServersError, Capistrano::NoSuchTaskError => error
169
+ warn "error: #{error.message}"
170
+ end
171
+
172
+ # Execute a command on the given list of servers.
173
+ def exec_command(command, servers)
174
+ command = command.gsub(/\bsudo\b/, "sudo -p '#{configuration.sudo_prompt}'")
175
+ processor = configuration.sudo_behavior_callback(Configuration.default_io_proc)
176
+ sessions = servers.map { |server| configuration.sessions[server] }
177
+ options = configuration.add_default_command_options({})
178
+ cmd = Command.new(command, sessions, options.merge(:logger => configuration.logger), &processor)
179
+ previous = trap("INT") { cmd.stop! }
180
+ cmd.process!
181
+ rescue Capistrano::Error => error
182
+ warn "error: #{error.message}"
183
+ ensure
184
+ trap("INT", previous)
185
+ end
186
+
187
+ # Return the object that will be used to query input from the console.
188
+ # The returned object will quack (more or less) like Readline.
189
+ def reader
190
+ @reader ||= begin
191
+ require 'readline'
192
+ Readline
193
+ rescue LoadError
194
+ ReadlineFallback
195
+ end
196
+ end
197
+
198
+ # Prepare every little thing for the shell. Starts the background
199
+ # thread and generally gets things ready for the REPL.
200
+ def setup
201
+ configuration.logger.level = Capistrano::Logger::INFO
202
+
203
+ @mutex = Mutex.new
204
+ @bgthread = Thread.new do
205
+ loop do
206
+ @mutex.synchronize { process_iteration(0.1) }
207
+ end
208
+ end
209
+ end
210
+
211
+ # Set the given option to +value+.
212
+ def set_option(opt, value)
213
+ case opt
214
+ when "v" then
215
+ puts "setting log verbosity to #{value.to_i}"
216
+ configuration.logger.level = value.to_i
217
+ when "o" then
218
+ case value
219
+ when "vi" then
220
+ puts "using vi edit mode"
221
+ reader.vi_editing_mode
222
+ when "emacs" then
223
+ puts "using emacs edit mode"
224
+ reader.emacs_editing_mode
225
+ else
226
+ puts "unknown -o option #{value.inspect}"
227
+ end
228
+ else
229
+ puts "unknown setting #{opt.inspect}"
230
+ end
231
+ end
232
+
233
+ # Process a command. Interprets the scope_type (must be nil, "with", or
234
+ # "on") and the command. If no command is given, then the scope is made
235
+ # effective for all subsequent commands. If the scope value is "all",
236
+ # then the scope is unrestricted.
237
+ def process_command(scope_type, scope_value, command)
238
+ env_var = case scope_type
239
+ when "with" then "ROLES"
240
+ when "on" then "HOSTS"
241
+ end
242
+
243
+ old_var, ENV[env_var] = ENV[env_var], (scope_value == "all" ? nil : scope_value) if env_var
244
+ if command
245
+ begin
246
+ exec(command)
247
+ ensure
248
+ ENV[env_var] = old_var if env_var
249
+ end
250
+ else
251
+ puts "scoping #{scope_type} #{scope_value}"
252
+ end
253
+ end
254
+ end
255
+
256
+ # All open sessions, needed to satisfy the Command::Processable include
257
+ def sessions
258
+ configuration.sessions.values
259
+ end
260
+ end
@@ -0,0 +1,99 @@
1
+ begin
2
+ require 'rubygems'
3
+ gem 'net-ssh', ">= 2.0.10"
4
+ rescue LoadError, NameError
5
+ end
6
+
7
+ require 'net/ssh'
8
+
9
+ module Capistrano
10
+ # A helper class for dealing with SSH connections.
11
+ class SSH
12
+ # Patch an accessor onto an SSH connection so that we can record the server
13
+ # definition object that defines the connection. This is useful because
14
+ # the gateway returns connections whose "host" is 127.0.0.1, instead of
15
+ # the host on the other side of the tunnel.
16
+ module Server #:nodoc:
17
+ def self.apply_to(connection, server)
18
+ connection.extend(Server)
19
+ connection.xserver = server
20
+ connection
21
+ end
22
+
23
+ attr_accessor :xserver
24
+ end
25
+
26
+ # An abstraction to make it possible to connect to the server via public key
27
+ # without prompting for the password. If the public key authentication fails
28
+ # this will fall back to password authentication.
29
+ #
30
+ # +server+ must be an instance of ServerDefinition.
31
+ #
32
+ # If a block is given, the new session is yielded to it, otherwise the new
33
+ # session is returned.
34
+ #
35
+ # If an :ssh_options key exists in +options+, it is passed to the Net::SSH
36
+ # constructor. Values in +options+ are then merged into it, and any
37
+ # connection information in +server+ is added last, so that +server+ info
38
+ # takes precedence over +options+, which takes precendence over ssh_options.
39
+ def self.connect(server, options={})
40
+ connection_strategy(server, options) do |host, user, connection_options|
41
+ connection = Net::SSH.start(host, user, connection_options)
42
+ Server.apply_to(connection, server)
43
+ end
44
+ end
45
+
46
+ # Abstracts the logic for establishing an SSH connection (which includes
47
+ # testing for connection failures and retrying with a password, and so forth,
48
+ # mostly made complicated because of the fact that some of these variables
49
+ # might be lazily evaluated and try to do something like prompt the user,
50
+ # which should only happen when absolutely necessary.
51
+ #
52
+ # This will yield the hostname, username, and a hash of connection options
53
+ # to the given block, which should return a new connection.
54
+ def self.connection_strategy(server, options={}, &block)
55
+ methods = [ %w(publickey hostbased), %w(password keyboard-interactive) ]
56
+ password_value = nil
57
+
58
+ # construct the hash of ssh options that should be passed more-or-less
59
+ # directly to Net::SSH. This will be the general ssh options, merged with
60
+ # the server-specific ssh-options.
61
+ ssh_options = (options[:ssh_options] || {}).merge(server.options[:ssh_options] || {})
62
+
63
+ # load any SSH configuration files that were specified in the SSH options. This
64
+ # will load from ~/.ssh/config and /etc/ssh_config by default (see Net::SSH
65
+ # for details). Merge the explicitly given ssh_options over the top of the info
66
+ # from the config file.
67
+ ssh_options = Net::SSH.configuration_for(server.host, ssh_options.fetch(:config, true)).merge(ssh_options)
68
+
69
+ # Once we've loaded the config, we don't need Net::SSH to do it again.
70
+ ssh_options[:config] = false
71
+
72
+ user = server.user || options[:user] || ssh_options[:username] ||
73
+ ssh_options[:user] || ServerDefinition.default_user
74
+ port = server.port || options[:port] || ssh_options[:port]
75
+
76
+ # the .ssh/config file might have changed the host-name on us
77
+ host = ssh_options.fetch(:host_name, server.host)
78
+
79
+ ssh_options[:port] = port if port
80
+
81
+ # delete these, since we've determined which username to use by this point
82
+ ssh_options.delete(:username)
83
+ ssh_options.delete(:user)
84
+
85
+ begin
86
+ connection_options = ssh_options.merge(
87
+ :password => password_value,
88
+ :auth_methods => ssh_options[:auth_methods] || methods.shift
89
+ )
90
+
91
+ yield host, user, connection_options
92
+ rescue Net::SSH::AuthenticationFailed
93
+ raise if methods.empty? || ssh_options[:auth_methods]
94
+ password_value = options[:password]
95
+ retry
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,70 @@
1
+ require 'capistrano/server_definition'
2
+
3
+ module Capistrano
4
+ # Represents the definition of a single task.
5
+ class TaskDefinition
6
+ attr_reader :name, :namespace, :options, :body, :desc, :on_error, :max_hosts
7
+
8
+ def initialize(name, namespace, options={}, &block)
9
+ @name, @namespace, @options = name, namespace, options
10
+ @desc = @options.delete(:desc)
11
+ @on_error = options.delete(:on_error)
12
+ @max_hosts = options[:max_hosts] && options[:max_hosts].to_i
13
+ @body = block or raise ArgumentError, "a task requires a block"
14
+ @servers = nil
15
+ end
16
+
17
+ # Returns the task's fully-qualified name, including the namespace
18
+ def fully_qualified_name
19
+ @fully_qualified_name ||= begin
20
+ if namespace.default_task == self
21
+ namespace.fully_qualified_name
22
+ else
23
+ [namespace.fully_qualified_name, name].compact.join(":")
24
+ end
25
+ end
26
+ end
27
+
28
+ # Returns the description for this task, with newlines collapsed and
29
+ # whitespace stripped. Returns the empty string if there is no
30
+ # description for this task.
31
+ def description(rebuild=false)
32
+ @description = nil if rebuild
33
+ @description ||= begin
34
+ description = @desc || ""
35
+
36
+ indentation = description[/\A\s+/]
37
+ if indentation
38
+ reformatted_description = ""
39
+ description.strip.each_line do |line|
40
+ line = line.chomp.sub(/^#{indentation}/, "")
41
+ line = line.gsub(/#{indentation}\s*/, " ") if line[/^\S/]
42
+ reformatted_description << line << "\n"
43
+ end
44
+ description = reformatted_description
45
+ end
46
+
47
+ description.strip.gsub(/\r\n/, "\n")
48
+ end
49
+ end
50
+
51
+ # Returns the first sentence of the full description. If +max_length+ is
52
+ # given, the result will be truncated if it is longer than +max_length+,
53
+ # and an ellipsis appended.
54
+ def brief_description(max_length=nil)
55
+ brief = description[/^.*?\.(?=\s|$)/] || description
56
+
57
+ if max_length && brief.length > max_length
58
+ brief = brief[0,max_length-3] + "..."
59
+ end
60
+
61
+ brief
62
+ end
63
+
64
+ # Indicates whether the task wants to continue, even if a server has failed
65
+ # previously
66
+ def continue_on_error?
67
+ @on_error == :continue
68
+ end
69
+ end
70
+ end