capistrano 2.2.0 → 2.3.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 (59) hide show
  1. data/CHANGELOG +29 -0
  2. data/README +4 -7
  3. data/bin/cap +0 -0
  4. data/bin/capify +0 -0
  5. data/lib/capistrano/command.rb +32 -38
  6. data/lib/capistrano/configuration/actions/file_transfer.rb +21 -13
  7. data/lib/capistrano/configuration/actions/invocation.rb +1 -1
  8. data/lib/capistrano/configuration/connections.rb +30 -20
  9. data/lib/capistrano/errors.rb +1 -1
  10. data/lib/capistrano/processable.rb +53 -0
  11. data/lib/capistrano/recipes/deploy/remote_dependency.rb +6 -0
  12. data/lib/capistrano/recipes/deploy/scm/git.rb +26 -11
  13. data/lib/capistrano/recipes/deploy/scm/none.rb +44 -0
  14. data/lib/capistrano/recipes/deploy/strategy/base.rb +6 -0
  15. data/lib/capistrano/recipes/deploy/strategy/copy.rb +74 -3
  16. data/lib/capistrano/recipes/deploy.rb +15 -13
  17. data/lib/capistrano/role.rb +0 -14
  18. data/lib/capistrano/server_definition.rb +5 -0
  19. data/lib/capistrano/shell.rb +21 -17
  20. data/lib/capistrano/ssh.rb +24 -58
  21. data/lib/capistrano/transfer.rb +216 -0
  22. data/lib/capistrano/version.rb +1 -1
  23. data/test/cli/execute_test.rb +1 -1
  24. data/test/cli/help_test.rb +1 -1
  25. data/test/cli/options_test.rb +1 -1
  26. data/test/cli/ui_test.rb +1 -1
  27. data/test/cli_test.rb +1 -1
  28. data/test/command_test.rb +31 -51
  29. data/test/configuration/actions/file_transfer_test.rb +21 -19
  30. data/test/configuration/actions/inspect_test.rb +1 -1
  31. data/test/configuration/actions/invocation_test.rb +6 -6
  32. data/test/configuration/callbacks_test.rb +1 -1
  33. data/test/configuration/connections_test.rb +11 -12
  34. data/test/configuration/execution_test.rb +1 -1
  35. data/test/configuration/loading_test.rb +1 -1
  36. data/test/configuration/namespace_dsl_test.rb +1 -1
  37. data/test/configuration/roles_test.rb +1 -1
  38. data/test/configuration/servers_test.rb +1 -1
  39. data/test/configuration/variables_test.rb +1 -1
  40. data/test/configuration_test.rb +1 -1
  41. data/test/deploy/scm/accurev_test.rb +1 -1
  42. data/test/deploy/scm/base_test.rb +1 -1
  43. data/test/deploy/scm/git_test.rb +10 -6
  44. data/test/deploy/scm/mercurial_test.rb +1 -1
  45. data/test/deploy/strategy/copy_test.rb +120 -27
  46. data/test/extensions_test.rb +1 -1
  47. data/test/logger_test.rb +1 -1
  48. data/test/server_definition_test.rb +1 -1
  49. data/test/shell_test.rb +27 -1
  50. data/test/ssh_test.rb +27 -21
  51. data/test/task_definition_test.rb +1 -1
  52. data/test/transfer_test.rb +160 -0
  53. data/test/utils.rb +30 -34
  54. data/test/version_test.rb +1 -1
  55. metadata +26 -14
  56. data/lib/capistrano/gateway.rb +0 -131
  57. data/lib/capistrano/upload.rb +0 -152
  58. data/test/gateway_test.rb +0 -167
  59. data/test/upload_test.rb +0 -131
data/CHANGELOG CHANGED
@@ -1,3 +1,32 @@
1
+ *2.3.0* May 2, 2008
2
+
3
+ * Make sure git fetches include tags [Alex Arnell]
4
+
5
+ * Make deploy:setup obey the :use_sudo and :runner directives, and generalize the :use_sudo and :runner options into a try_sudo() helper method [Jamis Buck]
6
+
7
+ * Make sudo helper play nicely with complex command chains [Jamis Buck]
8
+
9
+ * Expand file-transfer options with new upload() and download() helpers. [Jamis Buck]
10
+
11
+ * Allow SCP transfers in addition to SFTP. [Jamis Buck]
12
+
13
+ * Use Net::SSH v2 and Net::SSH::Gateway. [Jamis Buck]
14
+
15
+ * Added #export method for git SCM [Phillip Goldenburg]
16
+
17
+ * For query_revision, git SCM used git-rev-parse on the repo hosting the Capfile, which may NOT be the same tree as the actual source reposistory. Use git-ls-remote instead to resolve the revision for checkout. [Robin H. Johnson]
18
+
19
+ * Allow :ssh_options hash to be specified per server [Jesse Newland]
20
+
21
+ * Added support for depend :remote, :file to test for existence of a specific file [Andrew Carter]
22
+
23
+ * Ensure that the default run options are mixed into the command options when executing a command from the cap shell [Ken Collins]
24
+
25
+ * Added :none SCM module for deploying a specific directory's contents [Jamis Buck]
26
+
27
+ * Improved "copy" strategy supports local caching and pattern exclusion (via :copy_cache and :copy_exclude variables) [Jamis Buck]
28
+
29
+
1
30
  *2.2.0* February 27, 2008
2
31
 
3
32
  * Fix git submodule support to init on sync [halorgium]
data/README CHANGED
@@ -7,8 +7,10 @@ Capistrano was originally designed to simplify and automate deployment of web ap
7
7
 
8
8
  == Dependencies
9
9
 
10
- * Net::SSH and Net::SFTP (http://net-ssh.rubyforge.org)
11
- * Needle (via Net::SSH)
10
+ * Net::SSH v2 (http://net-ssh.rubyforge.org)
11
+ * Net::SFTP v2 (http://net-ssh.rubyforge.org)
12
+ * Net::SCP v1 (http://net-ssh.rubyforge.org)
13
+ * Net::SSH::Gateway v1 (http://net-ssh.rubyforge.org)
12
14
  * HighLine (http://highline.rubyforge.org)
13
15
 
14
16
  If you want to run the tests, you'll also need to have the following dependencies installed:
@@ -36,8 +38,3 @@ Use the +cap+ script as follows:
36
38
  cap sometask
37
39
 
38
40
  By default, the script will look for a file called one of +capfile+ or +Capfile+. The +someaction+ text indicates which task to execute. You can do "cap -h" to see all the available options and "cap -T" to see all the available tasks.
39
-
40
- == KNOWN ISSUES
41
-
42
- * Using "put" to upload a file to two or more hosts when a gateway is in effect has a good chance of crashing with a "corrupt mac detected" error. This is due to a bug in Net::SSH.
43
- * Running commands may rarely hang inexplicably. This appears to be specific only to certain platforms. Most people will never see this behavior.
data/bin/cap CHANGED
File without changes
data/bin/capify CHANGED
File without changes
@@ -1,10 +1,13 @@
1
1
  require 'capistrano/errors'
2
+ require 'capistrano/processable'
2
3
 
3
4
  module Capistrano
4
5
 
5
6
  # This class encapsulates a single command to be executed on a set of remote
6
7
  # machines, in parallel.
7
8
  class Command
9
+ include Processable
10
+
8
11
  attr_reader :command, :sessions, :options
9
12
 
10
13
  def self.process(command, sessions, options={}, &block)
@@ -32,21 +35,8 @@ module Capistrano
32
35
  # fails (non-zero return code) on any of the hosts, this will raise a
33
36
  # Capistrano::CommandError.
34
37
  def process!
35
- since = Time.now
36
38
  loop do
37
- active = 0
38
- @channels.each do |ch|
39
- next if ch[:closed]
40
- active += 1
41
- ch.connection.process(true)
42
- end
43
-
44
- break if active == 0
45
- if Time.now - since >= 1
46
- since = Time.now
47
- @channels.each { |ch| ch.connection.ping! }
48
- end
49
- sleep 0.01 # a brief respite, to keep the CPU from going crazy
39
+ break unless process_iteration { @channels.any? { |ch| !ch[:closed] } }
50
40
  end
51
41
 
52
42
  logger.trace "command finished" if logger
@@ -84,38 +74,32 @@ module Capistrano
84
74
  channel[:host] = server.host
85
75
  channel[:options] = options
86
76
 
87
- execute_command = Proc.new do |ch|
88
- logger.trace "executing command", ch[:server] if logger
89
- cmd = replace_placeholders(command, ch)
90
-
91
- if options[:shell] == false
92
- shell = nil
93
- else
94
- shell = "#{options[:shell] || "sh"} -c"
95
- cmd = cmd.gsub(/[$\\`"]/) { |m| "\\#{m}" }
96
- cmd = "\"#{cmd}\""
97
- end
77
+ request_pty_if_necessary(channel) do |ch, success|
78
+ if success
79
+ logger.trace "executing command", ch[:server] if logger
80
+ cmd = replace_placeholders(command, ch)
98
81
 
99
- command_line = [environment, shell, cmd].compact.join(" ")
82
+ if options[:shell] == false
83
+ shell = nil
84
+ else
85
+ shell = [options.fetch(:shell, "sh"), "-c"].join(" ")
86
+ cmd = cmd.gsub(/[$\\`"]/) { |m| "\\#{m}" }
87
+ cmd = "\"#{cmd}\""
88
+ end
100
89
 
101
- ch.exec(command_line)
102
- ch.send_data(options[:data]) if options[:data]
103
- end
90
+ command_line = [environment, options[:command_prefix], shell, cmd].compact.join(" ")
104
91
 
105
- if options[:pty]
106
- channel.request_pty(:want_reply => true)
107
- channel.on_success(&execute_command)
108
- channel.on_failure do |ch|
92
+ ch.exec(command_line)
93
+ ch.send_data(options[:data]) if options[:data]
94
+ else
109
95
  # just log it, don't actually raise an exception, since the
110
96
  # process method will see that the status is not zero and will
111
97
  # raise an exception then.
112
98
  logger.important "could not open channel", ch[:server] if logger
113
99
  ch.close
114
100
  end
115
- else
116
- execute_command.call(channel)
117
101
  end
118
-
102
+
119
103
  channel.on_data do |ch, data|
120
104
  @callback[ch, :out, data] if @callback
121
105
  end
@@ -124,8 +108,8 @@ module Capistrano
124
108
  @callback[ch, :err, data] if @callback
125
109
  end
126
110
 
127
- channel.on_request do |ch, request, reply, data|
128
- ch[:status] = data.read_long if request == "exit-status"
111
+ channel.on_request("exit-status") do |ch, data|
112
+ ch[:status] = data.read_long
129
113
  end
130
114
 
131
115
  channel.on_close do |ch|
@@ -135,6 +119,16 @@ module Capistrano
135
119
  end
136
120
  end
137
121
 
122
+ def request_pty_if_necessary(channel)
123
+ if options[:pty]
124
+ channel.request_pty do |ch, success|
125
+ yield ch, success
126
+ end
127
+ else
128
+ yield channel, true
129
+ end
130
+ end
131
+
138
132
  def replace_placeholders(command, channel)
139
133
  command.gsub(/\$CAPISTRANO:HOST\$/, channel[:host])
140
134
  end
@@ -1,4 +1,4 @@
1
- require 'capistrano/upload'
1
+ require 'capistrano/transfer'
2
2
 
3
3
  module Capistrano
4
4
  class Configuration
@@ -9,23 +9,31 @@ module Capistrano
9
9
  # by the current task. If <tt>:mode</tt> is specified it is used to
10
10
  # set the mode on the file.
11
11
  def put(data, path, options={})
12
- execute_on_servers(options) do |servers|
13
- targets = servers.map { |s| sessions[s] }
14
- Upload.process(targets, path, :data => data, :mode => options[:mode], :logger => logger)
15
- end
12
+ opts = options.dup
13
+ opts[:permissions] = opts.delete(:mode)
14
+ upload(StringIO.new(data), path, opts)
16
15
  end
17
16
 
18
- # Get file remote_path from FIRST server targetted by
17
+ # Get file remote_path from FIRST server targeted by
19
18
  # the current task and transfer it to local machine as path.
20
19
  #
21
20
  # get "#{deploy_to}/current/log/production.log", "log/production.log.web"
22
- def get(remote_path, path, options = {})
23
- execute_on_servers(options.merge(:once => true)) do |servers|
24
- logger.info "downloading `#{servers.first.host}:#{remote_path}' to `#{path}'"
25
- sftp = sessions[servers.first].sftp
26
- sftp.connect unless sftp.state == :open
27
- sftp.get_file remote_path, path
28
- logger.debug "download finished"
21
+ def get(remote_path, path, options={}, &block)
22
+ download(remote_path, path, options.merge(:once => true), &block)
23
+ end
24
+
25
+ def upload(from, to, options={}, &block)
26
+ transfer(:up, from, to, options, &block)
27
+ end
28
+
29
+ def download(from, to, options={}, &block)
30
+ transfer(:down, from, to, options, &block)
31
+ end
32
+
33
+ def transfer(direction, from, to, options={}, &block)
34
+ execute_on_servers(options) do |servers|
35
+ targets = servers.map { |s| sessions[s] }
36
+ Transfer.process(direction, from, to, targets, options.merge(:logger => logger), &block)
29
37
  end
30
38
  end
31
39
 
@@ -70,7 +70,7 @@ module Capistrano
70
70
  as = options.delete(:as)
71
71
 
72
72
  user = as && "-u #{as}"
73
- command = [fetch(:sudo, "sudo"), "-p '#{sudo_prompt}'", user, command].compact.join(" ")
73
+ options[:command_prefix] = [fetch(:sudo, "sudo"), "-p '#{sudo_prompt}'", user].compact.join(" ")
74
74
 
75
75
  run(command, options, &sudo_behavior_callback(block))
76
76
  end
@@ -1,7 +1,7 @@
1
-
2
1
  require 'enumerator'
3
- require 'capistrano/gateway'
2
+ require 'net/ssh/gateway'
4
3
  require 'capistrano/ssh'
4
+ require 'capistrano/errors'
5
5
 
6
6
  module Capistrano
7
7
  class Configuration
@@ -11,8 +11,6 @@ module Capistrano
11
11
  base.send :alias_method, :initialize, :initialize_with_connections
12
12
  end
13
13
 
14
- # An adaptor for making the SSH interface look and act like that of the
15
- # Gateway class.
16
14
  class DefaultConnectionFactory #:nodoc:
17
15
  def initialize(options)
18
16
  @options = options
@@ -23,6 +21,27 @@ module Capistrano
23
21
  end
24
22
  end
25
23
 
24
+ class GatewayConnectionFactory #:nodoc:
25
+ def initialize(gateway, options)
26
+ Thread.abort_on_exception = true
27
+ server = ServerDefinition.new(gateway)
28
+
29
+ @options = options
30
+ @gateway = SSH.connection_strategy(server, options) do |host, user, connect_options|
31
+ Net::SSH::Gateway.new(host, user, connect_options)
32
+ end
33
+ end
34
+
35
+ def connect_to(server)
36
+ @options[:logger].debug "establishing connection to `#{server}' via gateway" if @options[:logger]
37
+ local_host = ServerDefinition.new("127.0.0.1", :user => server.user, :port => @gateway.open(server.host, server.port || 22))
38
+ session = SSH.connect(local_host, @options)
39
+ session.xserver = server
40
+ @options[:logger].trace "connected: `#{server}' (via gateway)" if @options[:logger]
41
+ session
42
+ end
43
+ end
44
+
26
45
  # A hash of the SSH sessions that are currently open and available.
27
46
  # Because sessions are constructed lazily, this will only contain
28
47
  # connections to those servers that have been the targets of one or more
@@ -61,7 +80,7 @@ module Capistrano
61
80
  @connection_factory ||= begin
62
81
  if exists?(:gateway)
63
82
  logger.debug "establishing connection to gateway `#{fetch(:gateway)}'"
64
- Gateway.new(ServerDefinition.new(fetch(:gateway)), self)
83
+ GatewayConnectionFactory.new(fetch(:gateway), self)
65
84
  else
66
85
  DefaultConnectionFactory.new(self)
67
86
  end
@@ -72,22 +91,13 @@ module Capistrano
72
91
  def establish_connections_to(servers)
73
92
  failed_servers = []
74
93
 
75
- # This attemps to work around the problem where SFTP uploads hang
76
- # for some people. A bit of investigating seemed to reveal that the
77
- # hang only occurred when the SSH connections were established async,
78
- # so this setting allows people to at least work around the problem.
79
- if fetch(:synchronous_connect, false)
80
- logger.trace "synchronous_connect: true"
81
- Array(servers).each { |server| safely_establish_connection_to(server, failed_servers) }
82
- else
83
- # force the connection factory to be instantiated synchronously,
84
- # otherwise we wind up with multiple gateway instances, because
85
- # each connection is done in parallel.
86
- connection_factory
94
+ # force the connection factory to be instantiated synchronously,
95
+ # otherwise we wind up with multiple gateway instances, because
96
+ # each connection is done in parallel.
97
+ connection_factory
87
98
 
88
- threads = Array(servers).map { |server| establish_connection_to(server, failed_servers) }
89
- threads.each { |t| t.join }
90
- end
99
+ threads = Array(servers).map { |server| establish_connection_to(server, failed_servers) }
100
+ threads.each { |t| t.join }
91
101
 
92
102
  if failed_servers.any?
93
103
  errors = failed_servers.map { |h| "#{h[:server]} (#{h[:error].class}: #{h[:error].message})" }
@@ -10,6 +10,6 @@ module Capistrano
10
10
  end
11
11
 
12
12
  class ConnectionError < RemoteError; end
13
- class UploadError < RemoteError; end
13
+ class TransferError < RemoteError; end
14
14
  class CommandError < RemoteError; end
15
15
  end
@@ -0,0 +1,53 @@
1
+ module Capistrano
2
+ module Processable
3
+ module SessionAssociation
4
+ def self.on(exception, session)
5
+ unless exception.respond_to?(:session)
6
+ exception.extend(self)
7
+ exception.session = session
8
+ end
9
+
10
+ return exception
11
+ end
12
+
13
+ attr_accessor :session
14
+ end
15
+
16
+ def process_iteration(wait=nil, &block)
17
+ ensure_each_session { |session| session.preprocess }
18
+
19
+ return false if block && !block.call(self)
20
+
21
+ readers = sessions.map { |session| session.listeners.keys }.flatten.reject { |io| io.closed? }
22
+ writers = readers.select { |io| io.respond_to?(:pending_write?) && io.pending_write? }
23
+
24
+ if readers.any? || writers.any?
25
+ readers, writers, = IO.select(readers, writers, nil, wait)
26
+ end
27
+
28
+ if readers
29
+ ensure_each_session do |session|
30
+ ios = session.listeners.keys
31
+ session.postprocess(ios & readers, ios & writers)
32
+ end
33
+ end
34
+
35
+ true
36
+ end
37
+
38
+ def ensure_each_session
39
+ errors = []
40
+
41
+ sessions.each do |session|
42
+ begin
43
+ yield session
44
+ rescue Exception => error
45
+ errors << SessionAssociation.on(error, session)
46
+ end
47
+ end
48
+
49
+ raise errors.first if errors.any?
50
+ sessions
51
+ end
52
+ end
53
+ end
@@ -15,6 +15,12 @@ module Capistrano
15
15
  self
16
16
  end
17
17
 
18
+ def file(path, options={})
19
+ @message ||= "`#{path}' is not a file"
20
+ try("test -f #{path}", options)
21
+ self
22
+ end
23
+
18
24
  def writable(path, options={})
19
25
  @message ||= "`#{path}' is not writable"
20
26
  try("test -w #{path}", options)
@@ -24,14 +24,6 @@ module Capistrano
24
24
  # * Supports :scm_command, :scm_password, :scm_passphrase Capistrano
25
25
  # directives.
26
26
  #
27
- # REQUIREMENTS
28
- # ------------
29
- #
30
- # Git is required to be installed on your remote machine(s), because a
31
- # clone and checkout is done to get the code up there. This is the way
32
- # I prefer to deploy; there is no alternative to this, so :deploy_via
33
- # is ignored.
34
- #
35
27
  # CONFIGURATION
36
28
  # -------------
37
29
  #
@@ -100,13 +92,24 @@ module Capistrano
100
92
  #
101
93
  # set :git_shallow_clone, 1
102
94
  #
95
+ # For those that don't like to leave your entire repository on
96
+ # your production server you can:
97
+ #
98
+ # set :deploy_via, :export
99
+ #
100
+ # To deploy from a local repository:
101
+ #
102
+ # set :repository, "file://."
103
+ # set :deploy_via, :copy
104
+ #
103
105
  # AUTHORS
104
106
  # -------
105
107
  #
106
108
  # Garry Dolley http://scie.nti.st
107
109
  # Contributions by Geoffrey Grosenbach http://topfunky.com
108
110
  # Scott Chacon http://jointheconversation.org
109
- # and Alex Arnell http://twologic.com
111
+ # Alex Arnell http://twologic.com
112
+ # and Phillip Goldenburg
110
113
 
111
114
  class Git < Base
112
115
  # Sets the default command name for this SCM on your *local* machine.
@@ -153,6 +156,12 @@ module Capistrano
153
156
 
154
157
  execute.join(" && ")
155
158
  end
159
+
160
+ # An expensive export. Performs a checkout as above, then
161
+ # removes the repo.
162
+ def export(revision, destination)
163
+ checkout(revision, destination) << " && rm -Rf #{destination}/.git"
164
+ end
156
165
 
157
166
  # Merges the changes to 'head' since the last fetch, for remote_cache
158
167
  # deployment strategy
@@ -175,7 +184,7 @@ module Capistrano
175
184
  end
176
185
 
177
186
  # since we're in a local branch already, just reset to specified revision rather than merge
178
- execute << "#{git} fetch #{remote} && #{git} reset --hard #{revision}"
187
+ execute << "#{git} fetch --tags #{remote} && #{git} reset --hard #{revision}"
179
188
 
180
189
  if configuration[:git_enable_submodules]
181
190
  execute << "#{git} submodule init"
@@ -200,7 +209,13 @@ module Capistrano
200
209
  # Getting the actual commit id, in case we were passed a tag
201
210
  # or partial sha or something - it will return the sha if you pass a sha, too
202
211
  def query_revision(revision)
203
- yield(scm('rev-parse', revision)).chomp
212
+ return revision if revision =~ /^[0-9a-f]{40}$/
213
+ command = scm('ls-remote', repository, revision)
214
+ result = yield(command)
215
+ revdata = result.split("\t")
216
+ newrev = revdata[0]
217
+ raise "Unable to resolve revision for #{revision}" unless newrev =~ /^[0-9a-f]{40}$/
218
+ return newrev
204
219
  end
205
220
 
206
221
  def command
@@ -0,0 +1,44 @@
1
+ require 'capistrano/recipes/deploy/scm/base'
2
+
3
+ module Capistrano
4
+ module Deploy
5
+ module SCM
6
+
7
+ # A trivial SCM wrapper for representing the current working directory
8
+ # as a repository. Obviously, not all operations are available for this
9
+ # SCM, but it works sufficiently for use with the "copy" deployment
10
+ # strategy.
11
+ #
12
+ # Use of this module is _not_ recommended; in general, it is good
13
+ # practice to use some kind of source code management even for anything
14
+ # you are wanting to deploy. However, this module is provided in
15
+ # acknowledgement of the cases where trivial deployment of your current
16
+ # working directory is desired.
17
+ #
18
+ # set :repository, "."
19
+ # set :scm, :none
20
+ # set :deploy_via, :copy
21
+ class None < Base
22
+ # No versioning, thus, no head. Returns the empty string.
23
+ def head
24
+ ""
25
+ end
26
+
27
+ # Simply does a copy from the :repository directory to the
28
+ # :destination directory.
29
+ def checkout(revision, destination)
30
+ "cp -R #{repository} #{destination}"
31
+ end
32
+
33
+ alias_method :export, :checkout
34
+
35
+ # No versioning, so this just returns the argument, with no
36
+ # modification.
37
+ def query_revision(revision)
38
+ revision
39
+ end
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -46,6 +46,12 @@ module Capistrano
46
46
  end
47
47
  end
48
48
 
49
+ # A wrapper for Kernel#system that logs the command being executed.
50
+ def system(*args)
51
+ logger.trace "executing locally: #{args.join(' ')}"
52
+ super
53
+ end
54
+
49
55
  private
50
56
 
51
57
  def logger
@@ -15,7 +15,26 @@ module Capistrano
15
15
  # of the source code. If you would rather use the export operation,
16
16
  # you can set the :copy_strategy variable to :export.
17
17
  #
18
- # This deployment strategy supports a special variable,
18
+ # set :copy_strategy, :export
19
+ #
20
+ # For even faster deployments, you can set the :copy_cache variable to
21
+ # true. This will cause deployments to do a new checkout of your
22
+ # repository to a new directory, and then copy that checkout. Subsequent
23
+ # deploys will just resync that copy, rather than doing an entirely new
24
+ # checkout. Additionally, you can specify file patterns to exclude from
25
+ # the copy when using :copy_cache; just set the :copy_exclude variable
26
+ # to a file glob (or an array of globs).
27
+ #
28
+ # set :copy_cache, true
29
+ # set :copy_exclude, ".git/*"
30
+ #
31
+ # Note that :copy_strategy is ignored when :copy_cache is set. Also, if
32
+ # you want the copy cache put somewhere specific, you can set the variable
33
+ # to the path you want, instead of merely 'true':
34
+ #
35
+ # set :copy_cache, "/tmp/caches/myapp"
36
+ #
37
+ # This deployment strategy also supports a special variable,
19
38
  # :copy_compression, which must be one of :gzip, :bz2, or
20
39
  # :zip, and which specifies how the source should be compressed for
21
40
  # transmission to each host.
@@ -25,8 +44,44 @@ module Capistrano
25
44
  # servers, and uncompresses it on each of them into the deployment
26
45
  # directory.
27
46
  def deploy!
28
- logger.debug "getting (via #{copy_strategy}) revision #{revision} to #{destination}"
29
- system(command)
47
+ if copy_cache
48
+ if File.exists?(copy_cache)
49
+ logger.debug "refreshing local cache to revision #{revision} at #{copy_cache}"
50
+ system(source.sync(revision, copy_cache))
51
+ else
52
+ logger.debug "preparing local cache at #{copy_cache}"
53
+ system(source.checkout(revision, copy_cache))
54
+ end
55
+
56
+ logger.debug "copying cache to deployment staging area #{destination}"
57
+ Dir.chdir(copy_cache) do
58
+ FileUtils.mkdir_p(destination)
59
+ queue = Dir.glob("*", File::FNM_DOTMATCH)
60
+ while queue.any?
61
+ item = queue.shift
62
+ name = File.basename(item)
63
+
64
+ next if name == "." || name == ".."
65
+ next if copy_exclude.any? { |pattern| File.fnmatch(pattern, item) }
66
+
67
+ if File.directory?(item)
68
+ queue += Dir.glob("#{item}/*", File::FNM_DOTMATCH)
69
+ FileUtils.mkdir(File.join(destination, item))
70
+ else
71
+ FileUtils.ln(File.join(copy_cache, item), File.join(destination, item))
72
+ end
73
+ end
74
+ end
75
+ else
76
+ logger.debug "getting (via #{copy_strategy}) revision #{revision} to #{destination}"
77
+ system(command)
78
+
79
+ if copy_exclude.any?
80
+ logger.debug "processing exclusions..."
81
+ copy_exclude.each { |pattern| FileUtils.rm_rf(File.join(destination, pattern)) }
82
+ end
83
+ end
84
+
30
85
  File.open(File.join(destination, "REVISION"), "w") { |f| f.puts(revision) }
31
86
 
32
87
  logger.trace "compressing #{destination} to #{filename}"
@@ -48,8 +103,24 @@ module Capistrano
48
103
  end
49
104
  end
50
105
 
106
+ # Returns the location of the local copy cache, if the strategy should
107
+ # use a local cache + copy instead of a new checkout/export every
108
+ # time. Returns +nil+ unless :copy_cache has been set. If :copy_cache
109
+ # is +true+, a default cache location will be returned.
110
+ def copy_cache
111
+ @copy_cache ||= configuration[:copy_cache] == true ?
112
+ File.join(Dir.tmpdir, configuration[:application]) :
113
+ configuration[:copy_cache]
114
+ end
115
+
51
116
  private
52
117
 
118
+ # Specify patterns to exclude from the copy. This is only valid
119
+ # when using a local cache.
120
+ def copy_exclude
121
+ @copy_exclude ||= Array(configuration.fetch(:copy_exclude, []))
122
+ end
123
+
53
124
  # Returns the basename of the release_path, which will be used to
54
125
  # name the local copy and archive file.
55
126
  def destination