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,132 @@
1
+ require 'capistrano/errors'
2
+
3
+ module Capistrano
4
+ class Configuration
5
+ module Execution
6
+ def self.included(base) #:nodoc:
7
+ base.send :alias_method, :initialize_without_execution, :initialize
8
+ base.send :alias_method, :initialize, :initialize_with_execution
9
+ end
10
+
11
+ # The call stack of the tasks. The currently executing task may inspect
12
+ # this to see who its caller was. The current task is always the last
13
+ # element of this stack.
14
+ attr_reader :task_call_frames
15
+
16
+ # The stack of tasks that have registered rollback handlers within the
17
+ # current transaction. If this is nil, then there is no transaction
18
+ # that is currently active.
19
+ attr_reader :rollback_requests
20
+
21
+ # A struct for representing a single instance of an invoked task.
22
+ TaskCallFrame = Struct.new(:task, :rollback)
23
+
24
+ def initialize_with_execution(*args) #:nodoc:
25
+ initialize_without_execution(*args)
26
+ @task_call_frames = []
27
+ end
28
+ private :initialize_with_execution
29
+
30
+ # Returns true if there is a transaction currently active.
31
+ def transaction?
32
+ !rollback_requests.nil?
33
+ end
34
+
35
+ # Invoke a set of tasks in a transaction. If any task fails (raises an
36
+ # exception), all tasks executed within the transaction are inspected to
37
+ # see if they have an associated on_rollback hook, and if so, that hook
38
+ # is called.
39
+ def transaction
40
+ raise ArgumentError, "expected a block" unless block_given?
41
+ raise ScriptError, "transaction must be called from within a task" if task_call_frames.empty?
42
+
43
+ return yield if transaction?
44
+
45
+ logger.info "transaction: start"
46
+ begin
47
+ @rollback_requests = []
48
+ yield
49
+ logger.info "transaction: commit"
50
+ rescue Object => e
51
+ rollback!
52
+ raise
53
+ ensure
54
+ @rollback_requests = nil
55
+ end
56
+ end
57
+
58
+ # Specifies an on_rollback hook for the currently executing task. If this
59
+ # or any subsequent task then fails, and a transaction is active, this
60
+ # hook will be executed.
61
+ def on_rollback(&block)
62
+ if transaction?
63
+ # don't note a new rollback request if one has already been set
64
+ rollback_requests << task_call_frames.last unless task_call_frames.last.rollback
65
+ task_call_frames.last.rollback = block
66
+ end
67
+ end
68
+
69
+ # Returns the TaskDefinition object for the currently executing task.
70
+ # It returns nil if there is no task being executed.
71
+ def current_task
72
+ return nil if task_call_frames.empty?
73
+ task_call_frames.last.task
74
+ end
75
+
76
+ # Executes the task with the given name, without invoking any associated
77
+ # callbacks.
78
+ def execute_task(task)
79
+ logger.debug "executing `#{task.fully_qualified_name}'"
80
+ push_task_call_frame(task)
81
+ invoke_task_directly(task)
82
+ ensure
83
+ pop_task_call_frame
84
+ end
85
+
86
+ # Attempts to locate the task at the given fully-qualified path, and
87
+ # execute it. If no such task exists, a Capistrano::NoSuchTaskError will
88
+ # be raised.
89
+ def find_and_execute_task(path, hooks={})
90
+ task = find_task(path) or raise NoSuchTaskError, "the task `#{path}' does not exist"
91
+
92
+ trigger(hooks[:before], task) if hooks[:before]
93
+ result = execute_task(task)
94
+ trigger(hooks[:after], task) if hooks[:after]
95
+
96
+ result
97
+ end
98
+
99
+ protected
100
+
101
+ def rollback!
102
+ # throw the task back on the stack so that roles are properly
103
+ # interpreted in the scope of the task in question.
104
+ rollback_requests.reverse.each do |frame|
105
+ begin
106
+ push_task_call_frame(frame.task)
107
+ logger.important "rolling back", frame.task.fully_qualified_name
108
+ frame.rollback.call
109
+ rescue Object => e
110
+ logger.info "exception while rolling back: #{e.class}, #{e.message}", frame.task.fully_qualified_name
111
+ ensure
112
+ pop_task_call_frame
113
+ end
114
+ end
115
+ end
116
+
117
+ def push_task_call_frame(task)
118
+ frame = TaskCallFrame.new(task)
119
+ task_call_frames.push frame
120
+ end
121
+
122
+ def pop_task_call_frame
123
+ task_call_frames.pop
124
+ end
125
+
126
+ # Invokes the task's body directly, without setting up the call frame.
127
+ def invoke_task_directly(task)
128
+ task.namespace.instance_eval(&task.body)
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,197 @@
1
+ module Capistrano
2
+ class Configuration
3
+ module Loading
4
+ def self.included(base) #:nodoc:
5
+ base.send :alias_method, :initialize_without_loading, :initialize
6
+ base.send :alias_method, :initialize, :initialize_with_loading
7
+ base.extend ClassMethods
8
+ end
9
+
10
+ module ClassMethods
11
+ # Used by third-party task bundles to identify the capistrano
12
+ # configuration that is loading them. Its return value is not reliable
13
+ # in other contexts. If +require_config+ is not false, an exception
14
+ # will be raised if the current configuration is not set.
15
+ def instance(require_config=false)
16
+ config = Thread.current[:capistrano_configuration]
17
+ if require_config && config.nil?
18
+ raise LoadError, "Please require this file from within a Capistrano recipe"
19
+ end
20
+ config
21
+ end
22
+
23
+ # Used internally by Capistrano to specify the current configuration
24
+ # before loading a third-party task bundle.
25
+ def instance=(config)
26
+ Thread.current[:capistrano_configuration] = config
27
+ end
28
+
29
+ # Used internally by Capistrano to track which recipes have been loaded
30
+ # via require, so that they may be successfully reloaded when require
31
+ # is called again.
32
+ def recipes_per_feature
33
+ @recipes_per_feature ||= {}
34
+ end
35
+
36
+ # Used internally to determine what the current "feature" being
37
+ # required is. This is used to track which files load which recipes
38
+ # via require.
39
+ def current_feature
40
+ Thread.current[:capistrano_current_feature]
41
+ end
42
+
43
+ # Used internally to specify the current file being required, so that
44
+ # any recipes loaded by that file can be remembered. This allows
45
+ # recipes loaded via require to be correctly reloaded in different
46
+ # Configuration instances in the same Ruby instance.
47
+ def current_feature=(feature)
48
+ Thread.current[:capistrano_current_feature] = feature
49
+ end
50
+ end
51
+
52
+ # The load paths used for locating recipe files.
53
+ attr_reader :load_paths
54
+
55
+ def initialize_with_loading(*args) #:nodoc:
56
+ initialize_without_loading(*args)
57
+ @load_paths = [".", File.expand_path(File.join(File.dirname(__FILE__), "../recipes"))]
58
+ @loaded_features = []
59
+ end
60
+ private :initialize_with_loading
61
+
62
+ # Load a configuration file or string into this configuration.
63
+ #
64
+ # Usage:
65
+ #
66
+ # load("recipe"):
67
+ # Look for and load the contents of 'recipe.rb' into this
68
+ # configuration.
69
+ #
70
+ # load(:file => "recipe"):
71
+ # same as above
72
+ #
73
+ # load(:string => "set :scm, :subversion"):
74
+ # Load the given string as a configuration specification.
75
+ #
76
+ # load { ... }
77
+ # Load the block in the context of the configuration.
78
+ def load(*args, &block)
79
+ options = args.last.is_a?(Hash) ? args.pop : {}
80
+
81
+ if block
82
+ raise ArgumentError, "loading a block requires 0 arguments" unless options.empty? && args.empty?
83
+ load(:proc => block)
84
+
85
+ elsif args.any?
86
+ args.each { |arg| load options.merge(:file => arg) }
87
+
88
+ elsif options[:file]
89
+ load_from_file(options[:file], options[:name])
90
+
91
+ elsif options[:string]
92
+ remember_load(options) unless options[:reloading]
93
+ instance_eval(options[:string], options[:name] || "<eval>")
94
+
95
+ elsif options[:proc]
96
+ remember_load(options) unless options[:reloading]
97
+ instance_eval(&options[:proc])
98
+
99
+ else
100
+ raise ArgumentError, "don't know how to load #{options.inspect}"
101
+ end
102
+ end
103
+
104
+ # Require another file. This is identical to the standard require method,
105
+ # with the exception that it sets the receiver as the "current" configuration
106
+ # so that third-party task bundles can include themselves relative to
107
+ # that configuration.
108
+ #
109
+ # This is a bit more complicated than an initial review would seem to
110
+ # necessitate, but the use case that complicates things is this: An
111
+ # advanced user wants to embed capistrano, and needs to instantiate
112
+ # more than one capistrano configuration at a time. They also want each
113
+ # configuration to require a third-party capistrano extension. Using a
114
+ # naive require implementation, this would allow the first configuration
115
+ # to successfully load the third-party extension, but the require would
116
+ # fail for the second configuration because the extension has already
117
+ # been loaded.
118
+ #
119
+ # To work around this, we do a few things:
120
+ #
121
+ # 1. Each time a 'require' is invoked inside of a capistrano recipe,
122
+ # we remember the arguments (see "current_feature").
123
+ # 2. Each time a 'load' is invoked inside of a capistrano recipe, and
124
+ # "current_feature" is not nil (meaning we are inside of a pending
125
+ # require) we remember the options (see "remember_load" and
126
+ # "recipes_per_feature").
127
+ # 3. Each time a 'require' is invoked inside of a capistrano recipe,
128
+ # we check to see if this particular configuration has ever seen these
129
+ # arguments to require (see @loaded_features), and if not, we proceed
130
+ # as if the file had never been required. If the superclass' require
131
+ # returns false (meaning, potentially, that the file has already been
132
+ # required), then we look in the recipes_per_feature collection and
133
+ # load any remembered recipes from there.
134
+ #
135
+ # It's kind of a bear, but it works, and works transparently. Note that
136
+ # a simpler implementation would just muck with $", allowing files to be
137
+ # required multiple times, but that will cause warnings (and possibly
138
+ # errors) if the file to be required contains constant definitions and
139
+ # such, alongside (or instead of) capistrano recipe definitions.
140
+ def require(*args) #:nodoc:
141
+ # look to see if this specific configuration instance has ever seen
142
+ # these arguments to require before
143
+ if @loaded_features.include?(args)
144
+ return false
145
+ end
146
+
147
+ @loaded_features << args
148
+ begin
149
+ original_instance, self.class.instance = self.class.instance, self
150
+ original_feature, self.class.current_feature = self.class.current_feature, args
151
+
152
+ result = super
153
+ if !result # file has been required previously, load up the remembered recipes
154
+ list = self.class.recipes_per_feature[args] || []
155
+ list.each { |options| load(options.merge(:reloading => true)) }
156
+ end
157
+
158
+ return result
159
+ ensure
160
+ # restore the original, so that require's can be nested
161
+ self.class.instance = original_instance
162
+ self.class.current_feature = original_feature
163
+ end
164
+ end
165
+
166
+ private
167
+
168
+ # Load a recipe from the named file. If +name+ is given, the file will
169
+ # be reported using that name.
170
+ def load_from_file(file, name=nil)
171
+ file = find_file_in_load_path(file) unless File.file?(file)
172
+ load :string => File.read(file), :name => name || file
173
+ end
174
+
175
+ def find_file_in_load_path(file)
176
+ load_paths.each do |path|
177
+ ["", ".rb"].each do |ext|
178
+ name = File.join(path, "#{file}#{ext}")
179
+ return name if File.file?(name)
180
+ end
181
+ end
182
+
183
+ raise LoadError, "no such file to load -- #{file}"
184
+ end
185
+
186
+ # If a file is being required, the options associated with loading a
187
+ # recipe are remembered in the recipes_per_feature archive under the
188
+ # name of the file currently being required.
189
+ def remember_load(options)
190
+ if self.class.current_feature
191
+ list = (self.class.recipes_per_feature[self.class.current_feature] ||= [])
192
+ list << options
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,197 @@
1
+ require 'capistrano/task_definition'
2
+
3
+ module Capistrano
4
+ class Configuration
5
+ module Namespaces
6
+ DEFAULT_TASK = :default
7
+
8
+ def self.included(base) #:nodoc:
9
+ base.send :alias_method, :initialize_without_namespaces, :initialize
10
+ base.send :alias_method, :initialize, :initialize_with_namespaces
11
+ end
12
+
13
+ # The name of this namespace. Defaults to +nil+ for the top-level
14
+ # namespace.
15
+ attr_reader :name
16
+
17
+ # The parent namespace of this namespace. Returns +nil+ for the top-level
18
+ # namespace.
19
+ attr_reader :parent
20
+
21
+ # The hash of tasks defined for this namespace.
22
+ attr_reader :tasks
23
+
24
+ # The hash of namespaces defined for this namespace.
25
+ attr_reader :namespaces
26
+
27
+ def initialize_with_namespaces(*args) #:nodoc:
28
+ @name = @parent = nil
29
+ initialize_without_namespaces(*args)
30
+ @tasks = {}
31
+ @namespaces = {}
32
+ end
33
+ private :initialize_with_namespaces
34
+
35
+ # Returns the top-level namespace (the one with no parent).
36
+ def top
37
+ return parent.top if parent
38
+ return self
39
+ end
40
+
41
+ # Returns the fully-qualified name of this namespace, or nil if the
42
+ # namespace is at the top-level.
43
+ def fully_qualified_name
44
+ return nil if name.nil?
45
+ [parent.fully_qualified_name, name].compact.join(":")
46
+ end
47
+
48
+ # Describe the next task to be defined. The given text will be attached to
49
+ # the next task that is defined and used as its description.
50
+ def desc(text)
51
+ @next_description = text
52
+ end
53
+
54
+ # Returns the value set by the last, pending "desc" call. If +reset+ is
55
+ # not false, the value will be reset immediately afterwards.
56
+ def next_description(reset=false)
57
+ @next_description
58
+ ensure
59
+ @next_description = nil if reset
60
+ end
61
+
62
+ # Open a namespace in which to define new tasks. If the namespace was
63
+ # defined previously, it will be reopened, otherwise a new namespace
64
+ # will be created for the given name.
65
+ def namespace(name, &block)
66
+ name = name.to_sym
67
+ raise ArgumentError, "expected a block" unless block_given?
68
+
69
+ namespace_already_defined = namespaces.key?(name)
70
+ if all_methods.any? { |m| m.to_sym == name } && !namespace_already_defined
71
+ thing = tasks.key?(name) ? "task" : "method"
72
+ raise ArgumentError, "defining a namespace named `#{name}' would shadow an existing #{thing} with that name"
73
+ end
74
+
75
+ namespaces[name] ||= Namespace.new(name, self)
76
+ namespaces[name].instance_eval(&block)
77
+
78
+ # make sure any open description gets terminated
79
+ namespaces[name].desc(nil)
80
+
81
+ if !namespace_already_defined
82
+ metaclass = class << self; self; end
83
+ metaclass.send(:define_method, name) { namespaces[name] }
84
+ end
85
+ end
86
+
87
+ # Describe a new task. If a description is active (see #desc), it is added
88
+ # to the options under the <tt>:desc</tt> key. The new task is added to
89
+ # the namespace.
90
+ def task(name, options={}, &block)
91
+ name = name.to_sym
92
+ raise ArgumentError, "expected a block" unless block_given?
93
+
94
+ task_already_defined = tasks.key?(name)
95
+ if all_methods.any? { |m| m.to_sym == name } && !task_already_defined
96
+ thing = namespaces.key?(name) ? "namespace" : "method"
97
+ raise ArgumentError, "defining a task named `#{name}' would shadow an existing #{thing} with that name"
98
+ end
99
+
100
+ tasks[name] = TaskDefinition.new(name, self, {:desc => next_description(:reset)}.merge(options), &block)
101
+
102
+ if !task_already_defined
103
+ metaclass = class << self; self; end
104
+ metaclass.send(:define_method, name) { execute_task(tasks[name]) }
105
+ end
106
+ end
107
+
108
+ # Find the task with the given name, where name is the fully-qualified
109
+ # name of the task. This will search into the namespaces and return
110
+ # the referenced task, or nil if no such task can be found. If the name
111
+ # refers to a namespace, the task in that namespace named "default"
112
+ # will be returned instead, if one exists.
113
+ def find_task(name)
114
+ parts = name.to_s.split(/:/)
115
+ tail = parts.pop.to_sym
116
+
117
+ ns = self
118
+ until parts.empty?
119
+ next_part = parts.shift
120
+ ns = next_part.empty? ? nil : ns.namespaces[next_part.to_sym]
121
+ return nil if ns.nil?
122
+ end
123
+
124
+ if ns.namespaces.key?(tail)
125
+ ns = ns.namespaces[tail]
126
+ tail = DEFAULT_TASK
127
+ end
128
+
129
+ ns.tasks[tail]
130
+ end
131
+
132
+ # Given a task name, this will search the current namespace, and all
133
+ # parent namespaces, looking for a task that matches the name, exactly.
134
+ # It returns the task, if found, or nil, if not.
135
+ def search_task(name)
136
+ name = name.to_sym
137
+ ns = self
138
+
139
+ until ns.nil?
140
+ return ns.tasks[name] if ns.tasks.key?(name)
141
+ ns = ns.parent
142
+ end
143
+
144
+ return nil
145
+ end
146
+
147
+ # Returns the default task for this namespace. This will be +nil+ if
148
+ # the namespace is at the top-level, and will otherwise return the
149
+ # task named "default". If no such task exists, +nil+ will be returned.
150
+ def default_task
151
+ return nil if parent.nil?
152
+ return tasks[DEFAULT_TASK]
153
+ end
154
+
155
+ # Returns the tasks in this namespace as an array of TaskDefinition
156
+ # objects. If a non-false parameter is given, all tasks in all
157
+ # namespaces under this namespace will be returned as well.
158
+ def task_list(all=false)
159
+ list = tasks.values
160
+ namespaces.each { |name,space| list.concat(space.task_list(:all)) } if all
161
+ list
162
+ end
163
+
164
+ private
165
+
166
+ def all_methods
167
+ public_methods.concat(protected_methods).concat(private_methods)
168
+ end
169
+
170
+ class Namespace
171
+ def initialize(name, parent)
172
+ @parent = parent
173
+ @name = name
174
+ end
175
+
176
+ def role(*args)
177
+ raise NotImplementedError, "roles cannot be defined in a namespace"
178
+ end
179
+
180
+ def respond_to?(sym, include_priv=false)
181
+ super || parent.respond_to?(sym, include_priv)
182
+ end
183
+
184
+ def method_missing(sym, *args, &block)
185
+ if parent.respond_to?(sym)
186
+ parent.send(sym, *args, &block)
187
+ else
188
+ super
189
+ end
190
+ end
191
+
192
+ include Capistrano::Configuration::Namespaces
193
+ undef :desc, :next_description
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,73 @@
1
+ require 'capistrano/server_definition'
2
+ require 'capistrano/role'
3
+
4
+ module Capistrano
5
+ class Configuration
6
+ module Roles
7
+ def self.included(base) #:nodoc:
8
+ base.send :alias_method, :initialize_without_roles, :initialize
9
+ base.send :alias_method, :initialize, :initialize_with_roles
10
+ end
11
+
12
+ # The hash of roles defined for this configuration. Each entry in the
13
+ # hash points to an array of server definitions that belong in that
14
+ # role.
15
+ attr_reader :roles
16
+
17
+ def initialize_with_roles(*args) #:nodoc:
18
+ initialize_without_roles(*args)
19
+ @roles = Hash.new { |h,k| h[k] = Role.new }
20
+ end
21
+
22
+ # Define a new role and its associated servers. You must specify at least
23
+ # one host for each role. Also, you can specify additional information
24
+ # (in the form of a Hash) which can be used to more uniquely specify the
25
+ # subset of servers specified by this specific role definition.
26
+ #
27
+ # Usage:
28
+ #
29
+ # role :db, "db1.example.com", "db2.example.com"
30
+ # role :db, "master.example.com", :primary => true
31
+ # role :app, "app1.example.com", "app2.example.com"
32
+ #
33
+ # You can also encode the username and port number for each host in the
34
+ # server string, if needed:
35
+ #
36
+ # role :web, "www@web1.example.com"
37
+ # role :file, "files.example.com:4144"
38
+ # role :db, "admin@db3.example.com:1234"
39
+ #
40
+ # Lastly, username and port number may be passed as options, if that is
41
+ # preferred; note that the options apply to all servers defined in
42
+ # that call to "role":
43
+ #
44
+ # role :web, "web2", "web3", :user => "www", :port => 2345
45
+ def role(which, *args, &block)
46
+ options = args.last.is_a?(Hash) ? args.pop : {}
47
+ which = which.to_sym
48
+
49
+ # The roles Hash is defined so that unrecognized keys always auto-initialize
50
+ # to a new Role instance (see the assignment in the initialize_with_roles method,
51
+ # above). However, we explicitly assign here so that role declarations will
52
+ # vivify the role object even if there are no server arguments. (Otherwise,
53
+ # role(:app) won't actually instantiate a Role object for :app.)
54
+ roles[which] ||= Role.new
55
+
56
+ roles[which].push(block, options) if block_given?
57
+ args.each { |host| roles[which] << ServerDefinition.new(host, options) }
58
+ end
59
+
60
+ # An alternative way to associate servers with roles. If you have a server
61
+ # that participates in multiple roles, this can be a DRYer way to describe
62
+ # the relationships. Pass the host definition as the first parameter, and
63
+ # the roles as the remaining parameters:
64
+ #
65
+ # server "master.example.com", :web, :app
66
+ def server(host, *roles)
67
+ options = roles.last.is_a?(Hash) ? roles.pop : {}
68
+ raise ArgumentError, "you must associate a server with at least one role" if roles.empty?
69
+ roles.each { |name| role(name, host, options) }
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,85 @@
1
+ module Capistrano
2
+ class Configuration
3
+ module Servers
4
+ # Identifies all servers that the given task should be executed on.
5
+ # The options hash accepts the same arguments as #find_servers, and any
6
+ # preexisting options there will take precedence over the options in
7
+ # the task.
8
+ def find_servers_for_task(task, options={})
9
+ find_servers(task.options.merge(options))
10
+ end
11
+
12
+ # Attempts to find all defined servers that match the given criteria.
13
+ # The options hash may include a :hosts option (which should specify
14
+ # an array of host names or ServerDefinition instances), a :roles
15
+ # option (specifying an array of roles), an :only option (specifying
16
+ # a hash of key/value pairs that any matching server must match), and
17
+ # an :exception option (like :only, but the inverse).
18
+ #
19
+ # Additionally, if the HOSTS environment variable is set, it will take
20
+ # precedence over any other options. Similarly, the ROLES environment
21
+ # variable will take precedence over other options. If both HOSTS and
22
+ # ROLES are given, HOSTS wins.
23
+ #
24
+ # Yet additionally, if the HOSTFILTER environment variable is set, it
25
+ # will limit the result to hosts found in that (comma-separated) list.
26
+ #
27
+ # Usage:
28
+ #
29
+ # # return all known servers
30
+ # servers = find_servers
31
+ #
32
+ # # find all servers in the app role that are not exempted from
33
+ # # deployment
34
+ # servers = find_servers :roles => :app,
35
+ # :except => { :no_release => true }
36
+ #
37
+ # # returns the given hosts, translated to ServerDefinition objects
38
+ # servers = find_servers :hosts => "jamis@example.host.com"
39
+ def find_servers(options={})
40
+ hosts = server_list_from(ENV['HOSTS'] || options[:hosts])
41
+
42
+ if hosts.any?
43
+ filter_server_list(hosts.uniq)
44
+ else
45
+ roles = role_list_from(ENV['ROLES'] || options[:roles] || self.roles.keys)
46
+ only = options[:only] || {}
47
+ except = options[:except] || {}
48
+
49
+ servers = roles.inject([]) { |list, role| list.concat(self.roles[role]) }
50
+ servers = servers.select { |server| only.all? { |key,value| server.options[key] == value } }
51
+ servers = servers.reject { |server| except.any? { |key,value| server.options[key] == value } }
52
+ filter_server_list(servers.uniq)
53
+ end
54
+ end
55
+
56
+ protected
57
+
58
+ def filter_server_list(servers)
59
+ return servers unless ENV['HOSTFILTER']
60
+ filters = ENV['HOSTFILTER'].split(/,/)
61
+ servers.select { |server| filters.include?(server.host) }
62
+ end
63
+
64
+ def server_list_from(hosts)
65
+ hosts = hosts.split(/,/) if String === hosts
66
+ hosts = build_list(hosts)
67
+ hosts.map { |s| String === s ? ServerDefinition.new(s.strip) : s }
68
+ end
69
+
70
+ def role_list_from(roles)
71
+ roles = roles.split(/,/) if String === roles
72
+ roles = build_list(roles)
73
+ roles.map do |role|
74
+ role = String === role ? role.strip.to_sym : role
75
+ raise ArgumentError, "unknown role `#{role}'" unless self.roles.key?(role)
76
+ role
77
+ end
78
+ end
79
+
80
+ def build_list(list)
81
+ Array(list).map { |item| item.respond_to?(:call) ? item.call : item }.flatten
82
+ end
83
+ end
84
+ end
85
+ end