capistrano 2.2.0 → 2.3.0

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