capistrano 2.3.0 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
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