capistrano 2.3.0 → 2.4.0

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 (35) hide show
  1. data/CHANGELOG +50 -2
  2. data/lib/capistrano/cli/execute.rb +1 -0
  3. data/lib/capistrano/cli/options.rb +4 -0
  4. data/lib/capistrano/cli/ui.rb +13 -0
  5. data/lib/capistrano/command.rb +2 -2
  6. data/lib/capistrano/configuration.rb +2 -1
  7. data/lib/capistrano/configuration/actions/file_transfer.rb +5 -1
  8. data/lib/capistrano/configuration/actions/invocation.rb +45 -18
  9. data/lib/capistrano/configuration/callbacks.rb +3 -3
  10. data/lib/capistrano/configuration/execution.rb +8 -3
  11. data/lib/capistrano/configuration/loading.rb +20 -21
  12. data/lib/capistrano/recipes/deploy.rb +56 -27
  13. data/lib/capistrano/recipes/deploy/local_dependency.rb +6 -2
  14. data/lib/capistrano/recipes/deploy/remote_dependency.rb +2 -0
  15. data/lib/capistrano/recipes/deploy/scm/git.rb +21 -12
  16. data/lib/capistrano/recipes/deploy/scm/mercurial.rb +2 -1
  17. data/lib/capistrano/recipes/deploy/scm/subversion.rb +2 -1
  18. data/lib/capistrano/recipes/deploy/strategy/copy.rb +22 -34
  19. data/lib/capistrano/recipes/deploy/strategy/remote.rb +1 -1
  20. data/lib/capistrano/version.rb +1 -14
  21. data/test/cli/execute_test.rb +1 -1
  22. data/test/cli/options_test.rb +7 -1
  23. data/test/configuration/actions/file_transfer_test.rb +20 -1
  24. data/test/configuration/actions/invocation_test.rb +16 -10
  25. data/test/configuration/callbacks_test.rb +16 -2
  26. data/test/configuration/loading_test.rb +6 -1
  27. data/test/deploy/local_dependency_test.rb +73 -0
  28. data/test/deploy/remote_dependency_test.rb +114 -0
  29. data/test/deploy/scm/git_test.rb +22 -8
  30. data/test/deploy/scm/mercurial_test.rb +10 -4
  31. data/test/deploy/strategy/copy_test.rb +16 -11
  32. data/test/role_test.rb +11 -0
  33. data/test/server_definition_test.rb +14 -1
  34. metadata +5 -3
  35. data/test/version_test.rb +0 -24
@@ -30,7 +30,7 @@ module Capistrano
30
30
  # file is found that matches the parameter, this returns true.
31
31
  def find_in_path(utility)
32
32
  path = (ENV['PATH'] || "").split(File::PATH_SEPARATOR)
33
- suffixes = RUBY_PLATFORM =~ /mswin/ ? %w(.bat .exe .com .cmd) : [""]
33
+ suffixes = self.class.on_windows? ? %w(.bat .exe .com .cmd) : [""]
34
34
 
35
35
  path.each do |dir|
36
36
  suffixes.each do |sfx|
@@ -41,6 +41,10 @@ module Capistrano
41
41
 
42
42
  false
43
43
  end
44
+
45
+ def self.on_windows?
46
+ RUBY_PLATFORM =~ /mswin/
47
+ end
44
48
  end
45
49
  end
46
- end
50
+ end
@@ -1,3 +1,5 @@
1
+ require 'capistrano/errors'
2
+
1
3
  module Capistrano
2
4
  module Deploy
3
5
  class RemoteDependency
@@ -141,17 +141,17 @@ module Capistrano
141
141
 
142
142
  execute = []
143
143
  if args.empty?
144
- execute << "#{git} clone #{configuration[:repository]} #{destination}"
144
+ execute << "#{git} clone #{verbose} #{configuration[:repository]} #{destination}"
145
145
  else
146
- execute << "#{git} clone #{args.join(' ')} #{configuration[:repository]} #{destination}"
146
+ execute << "#{git} clone #{verbose} #{args.join(' ')} #{configuration[:repository]} #{destination}"
147
147
  end
148
148
 
149
149
  # checkout into a local branch rather than a detached HEAD
150
- execute << "cd #{destination} && #{git} checkout -b deploy #{revision}"
150
+ execute << "cd #{destination} && #{git} checkout #{verbose} -b deploy #{revision}"
151
151
 
152
152
  if configuration[:git_enable_submodules]
153
- execute << "#{git} submodule init"
154
- execute << "#{git} submodule update"
153
+ execute << "#{git} submodule #{verbose} init"
154
+ execute << "#{git} submodule #{verbose} update"
155
155
  end
156
156
 
157
157
  execute.join(" && ")
@@ -184,11 +184,11 @@ module Capistrano
184
184
  end
185
185
 
186
186
  # since we're in a local branch already, just reset to specified revision rather than merge
187
- execute << "#{git} fetch --tags #{remote} && #{git} reset --hard #{revision}"
187
+ execute << "#{git} fetch #{verbose} #{remote} && #{git} reset #{verbose} --hard #{revision}"
188
188
 
189
189
  if configuration[:git_enable_submodules]
190
- execute << "#{git} submodule init"
191
- execute << "#{git} submodule update"
190
+ execute << "#{git} submodule #{verbose} init"
191
+ execute << "#{git} submodule #{verbose} update"
192
192
  end
193
193
 
194
194
  execute.join(" && ")
@@ -202,19 +202,19 @@ module Capistrano
202
202
 
203
203
  # Returns a log of changes between the two revisions (inclusive).
204
204
  def log(from, to=nil)
205
- from << "..#{to}" if to
206
- scm :log, from
205
+ scm :log, "#{from}..#{to}"
207
206
  end
208
207
 
209
208
  # Getting the actual commit id, in case we were passed a tag
210
209
  # or partial sha or something - it will return the sha if you pass a sha, too
211
210
  def query_revision(revision)
211
+ raise ArgumentError, "Deploying remote branches has been deprecated. Specify the remote branch as a local branch for the git repository you're deploying from (ie: '#{revision.gsub('origin/', '')}' rather than '#{revision}')." if revision =~ /^origin\//
212
212
  return revision if revision =~ /^[0-9a-f]{40}$/
213
213
  command = scm('ls-remote', repository, revision)
214
214
  result = yield(command)
215
215
  revdata = result.split("\t")
216
216
  newrev = revdata[0]
217
- raise "Unable to resolve revision for #{revision}" unless newrev =~ /^[0-9a-f]{40}$/
217
+ raise "Unable to resolve revision for '#{revision}' on repository '#{repository}'." unless newrev =~ /^[0-9a-f]{40}$/
218
218
  return newrev
219
219
  end
220
220
 
@@ -227,7 +227,8 @@ module Capistrano
227
227
  # from the SCM. Password prompts, connection requests, passphrases,
228
228
  # etc. are handled here.
229
229
  def handle_data(state, stream, text)
230
- logger.info "[#{stream}] #{text}"
230
+ host = state[:channel][:host]
231
+ logger.info "[#{host} :: #{stream}] #{text}"
231
232
  case text
232
233
  when /\bpassword.*:/i
233
234
  # git is prompting for a password
@@ -249,6 +250,14 @@ module Capistrano
249
250
  "t\n"
250
251
  end
251
252
  end
253
+
254
+ private
255
+
256
+ # If verbose output is requested, return nil, otherwise return the
257
+ # command-line switch for "quiet" ("-q").
258
+ def verbose
259
+ variable(:scm_verbose) ? nil : "-q"
260
+ end
252
261
  end
253
262
  end
254
263
  end
@@ -67,7 +67,8 @@ module Capistrano
67
67
  # user/pass can come from ssh and http distribution methods
68
68
  # yes/no is for when ssh asks you about fingerprints
69
69
  def handle_data(state, stream, text)
70
- logger.info "[#{stream}] #{text}"
70
+ host = state[:channel][:host]
71
+ logger.info "[#{host} :: #{stream}] #{text}"
71
72
  case text
72
73
  when /^user:/mi
73
74
  # support :scm_user for backwards compatibility of this module
@@ -68,7 +68,8 @@ module Capistrano
68
68
  # from the SCM. Password prompts, connection requests, passphrases,
69
69
  # etc. are handled here.
70
70
  def handle_data(state, stream, text)
71
- logger.info "[#{stream}] #{text}"
71
+ host = state[:channel][:host]
72
+ logger.info "[#{host} :: #{stream}] #{text}"
72
73
  case text
73
74
  when /\bpassword.*:/i
74
75
  # subversion is prompting for a password
@@ -64,7 +64,9 @@ module Capistrano
64
64
  next if name == "." || name == ".."
65
65
  next if copy_exclude.any? { |pattern| File.fnmatch(pattern, item) }
66
66
 
67
- if File.directory?(item)
67
+ if File.symlink?(item)
68
+ FileUtils.ln_s(File.readlink(File.join(copy_cache, item)), File.join(destination, item))
69
+ elsif File.directory?(item)
68
70
  queue += Dir.glob("#{item}/*", File::FNM_DOTMATCH)
69
71
  FileUtils.mkdir(File.join(destination, item))
70
72
  else
@@ -87,8 +89,7 @@ module Capistrano
87
89
  logger.trace "compressing #{destination} to #{filename}"
88
90
  Dir.chdir(tmpdir) { system(compress(File.basename(destination), File.basename(filename)).join(" ")) }
89
91
 
90
- content = File.open(filename, "rb") { |f| f.read }
91
- put content, remote_filename
92
+ upload(filename, remote_filename)
92
93
  run "cd #{configuration[:releases_path]} && #{decompress(remote_filename).join(" ")} && rm #{remote_filename}"
93
94
  ensure
94
95
  FileUtils.rm filename rescue nil
@@ -97,7 +98,7 @@ module Capistrano
97
98
 
98
99
  def check!
99
100
  super.check do |d|
100
- d.local.command(source.local.command)
101
+ d.local.command(source.local.command) if source.local.command
101
102
  d.local.command(compress(nil, nil).first)
102
103
  d.remote.command(decompress(nil).first)
103
104
  end
@@ -147,7 +148,7 @@ module Capistrano
147
148
  # Returns the name of the file that the source code will be
148
149
  # compressed to.
149
150
  def filename
150
- @filename ||= File.join(tmpdir, "#{File.basename(destination)}.#{compression_extension}")
151
+ @filename ||= File.join(tmpdir, "#{File.basename(destination)}.#{compression.extension}")
151
152
  end
152
153
 
153
154
  # The directory to which the copy should be checked out
@@ -167,46 +168,33 @@ module Capistrano
167
168
  @remote_filename ||= File.join(remote_dir, File.basename(filename))
168
169
  end
169
170
 
171
+ # A struct for representing the specifics of a compression type.
172
+ # Commands are arrays, where the first element is the utility to be
173
+ # used to perform the compression or decompression.
174
+ Compression = Struct.new(:extension, :compress_command, :decompress_command)
175
+
170
176
  # The compression method to use, defaults to :gzip.
171
177
  def compression
172
- configuration[:copy_compression] || :gzip
173
- end
174
-
175
- # Returns the file extension used for the compression method in
176
- # question.
177
- def compression_extension
178
- case compression
179
- when :gzip, :gz then "tar.gz"
180
- when :bzip2, :bz2 then "tar.bz2"
181
- when :zip then "zip"
182
- else raise ArgumentError, "invalid compression type #{compression.inspect}"
178
+ type = configuration[:copy_compression] || :gzip
179
+ case type
180
+ when :gzip, :gz then Compression.new("tar.gz", %w(tar czf), %w(tar xzf))
181
+ when :bzip2, :bz2 then Compression.new("tar.bz2", %w(tar cjf), %w(tar xjf))
182
+ when :zip then Compression.new("zip", %w(zip -qr), %w(unzip -q))
183
+ else raise ArgumentError, "invalid compression type #{type.inspect}"
183
184
  end
184
185
  end
185
-
186
+
186
187
  # Returns the command necessary to compress the given directory
187
- # into the given file. The command is returned as an array, where
188
- # the first element is the utility to be used to perform the compression.
188
+ # into the given file.
189
189
  def compress(directory, file)
190
- case compression
191
- when :gzip, :gz then ["tar", "czf", file, directory]
192
- when :bzip2, :bz2 then ["tar", "cjf", file, directory]
193
- when :zip then ["zip", "-qr", file, directory]
194
- else raise ArgumentError, "invalid compression type #{compression.inspect}"
195
- end
190
+ compression.compress_command + [file, directory]
196
191
  end
197
192
 
198
193
  # Returns the command necessary to decompress the given file,
199
194
  # relative to the current working directory. It must also
200
- # preserve the directory structure in the file. The command is returned
201
- # as an array, where the first element is the utility to be used to
202
- # perform the decompression.
195
+ # preserve the directory structure in the file.
203
196
  def decompress(file)
204
- case compression
205
- when :gzip, :gz then ["tar", "xzf", file]
206
- when :bzip2, :bz2 then ["tar", "xjf", file]
207
- when :zip then ["unzip", "-q", file]
208
- else raise ArgumentError, "invalid compression type #{compression.inspect}"
209
- end
197
+ compression.decompress_command + [file]
210
198
  end
211
199
  end
212
200
 
@@ -27,7 +27,7 @@ module Capistrano
27
27
  # #handle_data filter of the SCM implementation.
28
28
  def scm_run(command)
29
29
  run(command) do |ch,stream,text|
30
- ch[:state] ||= {}
30
+ ch[:state] ||= { :channel => ch }
31
31
  output = source.handle_data(ch[:state], stream, text)
32
32
  ch.send_data(output) if output
33
33
  end
@@ -1,22 +1,9 @@
1
1
  module Capistrano
2
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
-
13
3
  MAJOR = 2
14
- MINOR = 3
4
+ MINOR = 4
15
5
  TINY = 0
16
6
 
17
7
  STRING = [MAJOR, MINOR, TINY].join(".")
18
-
19
- SSH_REQUIRED = [1,0,10]
20
- SFTP_REQUIRED = [1,1,0]
21
8
  end
22
9
  end
@@ -15,7 +15,7 @@ class CLIExecuteTest < Test::Unit::TestCase
15
15
  def setup
16
16
  @cli = MockCLI.new
17
17
  @logger = stub_everything
18
- @config = stub(:logger => @logger)
18
+ @config = stub(:logger => @logger, :debug= => nil)
19
19
  @config.stubs(:set)
20
20
  @config.stubs(:load)
21
21
  @config.stubs(:trigger)
@@ -24,6 +24,12 @@ class CLIOptionsTest < Test::Unit::TestCase
24
24
  assert_raises(ExitException) { @cli.parse_options! }
25
25
  end
26
26
 
27
+ def test_parse_options_with_d_should_set_debug_option
28
+ @cli.args << "-d"
29
+ @cli.parse_options!
30
+ assert @cli.options[:debug]
31
+ end
32
+
27
33
  def test_parse_options_with_e_should_set_explain_option
28
34
  @cli.args << "-e" << "sample"
29
35
  @cli.parse_options!
@@ -223,4 +229,4 @@ class CLIOptionsTest < Test::Unit::TestCase
223
229
  MockCLI.expects(:new).with(%w(a b c)).returns(cli)
224
230
  assert_equal cli, MockCLI.parse(%w(a b c))
225
231
  end
226
- end
232
+ end
@@ -14,7 +14,8 @@ class ConfigurationActionsFileTransferTest < Test::Unit::TestCase
14
14
 
15
15
  def test_put_should_delegate_to_upload
16
16
  @config.expects(:upload).with { |from, to, opts|
17
- from.string == "some data" && to == "test.txt" && opts == { :permissions => 0777 } }
17
+ from.string == "some data" && to == "test.txt" && opts == { :mode => 0777 } }
18
+ @config.expects(:run).never
18
19
  @config.put("some data", "test.txt", :mode => 0777)
19
20
  end
20
21
 
@@ -28,6 +29,24 @@ class ConfigurationActionsFileTransferTest < Test::Unit::TestCase
28
29
  @config.upload("testl.txt", "testr.txt", :foo => "bar")
29
30
  end
30
31
 
32
+ def test_upload_without_mode_should_not_try_to_chmod
33
+ @config.expects(:transfer).with(:up, "testl.txt", "testr.txt", :foo => "bar")
34
+ @config.expects(:run).never
35
+ @config.upload("testl.txt", "testr.txt", :foo => "bar")
36
+ end
37
+
38
+ def test_upload_with_mode_should_try_to_chmod
39
+ @config.expects(:transfer).with(:up, "testl.txt", "testr.txt", :foo => "bar")
40
+ @config.expects(:run).with("chmod 775 testr.txt")
41
+ @config.upload("testl.txt", "testr.txt", :mode => 0775, :foo => "bar")
42
+ end
43
+
44
+ def test_upload_with_symbolic_mode_should_try_to_chmod
45
+ @config.expects(:transfer).with(:up, "testl.txt", "testr.txt", :foo => "bar")
46
+ @config.expects(:run).with("chmod g+w testr.txt")
47
+ @config.upload("testl.txt", "testr.txt", :mode => "g+w", :foo => "bar")
48
+ end
49
+
31
50
  def test_download_should_delegate_to_transfer
32
51
  @config.expects(:transfer).with(:down, "testr.txt", "testl.txt", :foo => "bar")
33
52
  @config.download("testr.txt", "testl.txt", :foo => "bar")
@@ -4,6 +4,7 @@ require 'capistrano/configuration/actions/invocation'
4
4
  class ConfigurationActionsInvocationTest < Test::Unit::TestCase
5
5
  class MockConfig
6
6
  attr_reader :options
7
+ attr_accessor :debug
7
8
 
8
9
  def initialize
9
10
  @options = {}
@@ -75,6 +76,11 @@ class ConfigurationActionsInvocationTest < Test::Unit::TestCase
75
76
  assert_equal({:foo => "bar", :shell => "/bin/bash"}, @config.add_default_command_options(:foo => "bar"))
76
77
  end
77
78
 
79
+ def test_add_default_command_options_should_use_default_shell_of_false_if_present
80
+ @config.set :default_shell, false
81
+ assert_equal({:foo => "bar", :shell => false}, @config.add_default_command_options(:foo => "bar"))
82
+ end
83
+
78
84
  def test_add_default_command_options_should_use_shell_in_preference_of_default_shell
79
85
  @config.set :default_shell, "/bin/bash"
80
86
  assert_equal({:foo => "bar", :shell => "/bin/sh"}, @config.add_default_command_options(:foo => "bar", :shell => "/bin/sh"))
@@ -97,29 +103,29 @@ class ConfigurationActionsInvocationTest < Test::Unit::TestCase
97
103
  end
98
104
 
99
105
  def test_sudo_should_default_to_sudo
100
- @config.expects(:run).with("ls", :command_prefix => "sudo -p 'sudo password: '")
106
+ @config.expects(:run).with("sudo -p 'sudo password: ' ls", {})
101
107
  @config.sudo "ls"
102
108
  end
103
109
 
104
110
  def test_sudo_should_use_sudo_variable_definition
105
- @config.expects(:run).with("ls", :command_prefix => "/opt/local/bin/sudo -p 'sudo password: '")
111
+ @config.expects(:run).with("/opt/local/bin/sudo -p 'sudo password: ' ls", {})
106
112
  @config.options[:sudo] = "/opt/local/bin/sudo"
107
113
  @config.sudo "ls"
108
114
  end
109
115
 
110
116
  def test_sudo_should_interpret_as_option_as_user
111
- @config.expects(:run).with("ls", :command_prefix => "sudo -p 'sudo password: ' -u app")
117
+ @config.expects(:run).with("sudo -p 'sudo password: ' -u app ls", {})
112
118
  @config.sudo "ls", :as => "app"
113
119
  end
114
120
 
115
121
  def test_sudo_should_pass_options_through_to_run
116
- @config.expects(:run).with("ls", :command_prefix => "sudo -p 'sudo password: '", :foo => "bar")
122
+ @config.expects(:run).with("sudo -p 'sudo password: ' ls", :foo => "bar")
117
123
  @config.sudo "ls", :foo => "bar"
118
124
  end
119
125
 
120
126
  def test_sudo_should_interpret_sudo_prompt_variable_as_custom_prompt
121
127
  @config.set :sudo_prompt, "give it to me: "
122
- @config.expects(:run).with("ls", :command_prefix => "sudo -p 'give it to me: '")
128
+ @config.expects(:run).with("sudo -p 'give it to me: ' ls", {})
123
129
  @config.sudo "ls"
124
130
  end
125
131
 
@@ -143,7 +149,7 @@ class ConfigurationActionsInvocationTest < Test::Unit::TestCase
143
149
  ch.stubs(:[]).with(:host).returns("capistrano")
144
150
  ch.stubs(:[]).with(:server).returns(server("capistrano"))
145
151
  @config.expects(:reset!).with(:password)
146
- @config.sudo_behavior_callback(nil)[ch, nil, "blah blah try again blah blah"]
152
+ @config.sudo_behavior_callback(nil)[ch, nil, "Sorry, try again."]
147
153
  end
148
154
 
149
155
  def test_sudo_behavior_callback_with_incorrect_password_on_subsequent_prompts
@@ -158,9 +164,9 @@ class ConfigurationActionsInvocationTest < Test::Unit::TestCase
158
164
 
159
165
  @config.expects(:reset!).with(:password).times(2)
160
166
 
161
- callback[ch, nil, "blah blah try again blah blah"]
162
- callback[ch2, nil, "blah blah try again blah blah"] # shouldn't call reset!
163
- callback[ch, nil, "blah blah try again blah blah"]
167
+ callback[ch, nil, "Sorry, try again."]
168
+ callback[ch2, nil, "Sorry, try again."] # shouldn't call reset!
169
+ callback[ch, nil, "Sorry, try again."]
164
170
  end
165
171
 
166
172
  def test_sudo_behavior_callback_should_defer_to_fallback_for_other_output
@@ -199,4 +205,4 @@ class ConfigurationActionsInvocationTest < Test::Unit::TestCase
199
205
  c = mock("data", :called => true)
200
206
  Capistrano::Command.expects(:process).with(command, sessions, options).yields(a, b, c)
201
207
  end
202
- end
208
+ end
@@ -12,9 +12,15 @@ class ConfigurationCallbacksTest < Test::Unit::TestCase
12
12
  end
13
13
 
14
14
  def execute_task(task)
15
- @called << task
15
+ invoke_task_directly(task)
16
16
  end
17
17
 
18
+ protected
19
+
20
+ def invoke_task_directly(task)
21
+ @called << task
22
+ end
23
+
18
24
  include Capistrano::Configuration::Callbacks
19
25
  end
20
26
 
@@ -96,6 +102,14 @@ class ConfigurationCallbacksTest < Test::Unit::TestCase
96
102
  assert_equal 1, @config.callbacks[:before].length
97
103
  assert_equal %w(primary), @config.callbacks[:before].first.except
98
104
  end
105
+
106
+ def test_on_without_tasks_or_block_should_raise_error
107
+ assert_raises(ArgumentError) { @config.on(:before) }
108
+ end
109
+
110
+ def test_on_with_both_tasks_and_block_should_raise_error
111
+ assert_raises(ArgumentError) { @config.on(:before, :first) { blah } }
112
+ end
99
113
 
100
114
  def test_trigger_without_constraints_should_invoke_all_callbacks
101
115
  task = stub(:fully_qualified_name => "any:old:thing")
@@ -203,4 +217,4 @@ class ConfigurationCallbacksTest < Test::Unit::TestCase
203
217
 
204
218
  @config.execute_task(task)
205
219
  end
206
- end
220
+ end