dynport-capistrano_rsync_with_remote_cache 2.4.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,96 @@
1
+ = Capistrano rsync_with_remote_cache Deployment Strategy
2
+
3
+ == Description
4
+
5
+ This gem provides a deployment strategy for Capistrano which combines the
6
+ <tt>rsync</tt> command with a remote cache, allowing fast deployments from SCM
7
+ repositories behind firewalls.
8
+
9
+ == Requirements
10
+
11
+ This gem supports Subversion, Git, Mercurial and Bazaar. Only Subversion and
12
+ Git have been extensively tested. This gem is unlikely to be supported for
13
+ other SCM systems.
14
+
15
+ This gem requires the <tt>rsync</tt> command-line utilities on the local and
16
+ remote hosts. It also requires either <tt>svn</tt>, <tt>git</tt>, <tt>hg</tt>
17
+ or <tt>bzr</tt> on the local host, but not the remote host.
18
+
19
+ This gem is tested on Mac OS X and Linux. Windows is neither tested nor supported.
20
+
21
+ == Installation
22
+
23
+ gem install capistrano_rsync_with_remote_cache
24
+
25
+ == Usage
26
+
27
+ To use this deployment strategy, add this line to your <tt>deploy.rb</tt> file:
28
+
29
+ set :deploy_via, :rsync_with_remote_cache
30
+
31
+ == Under the Hood
32
+
33
+ This strategy maintains two cache directories:
34
+
35
+ * The local cache directory is a checkout from the SCM repository. The local
36
+ cache directory is specified with the <tt>:local_cache</tt> variable in the
37
+ configuration. If not specified, it will default to <tt>.rsync_cache</tt>
38
+ in the same directory as the Capfile.
39
+
40
+ * The remote cache directory is an <tt>rsync</tt> copy of the local cache directory.
41
+ The remote cache directory is specified with the <tt>:repository_cache</tt> variable
42
+ in the configuration (this name comes from the <tt>:remote_cache</tt> strategy that
43
+ ships with Capistrano, and has been maintained for compatibility.) If not
44
+ specified, it will default to <tt>shared/cached-copy</tt> (again, for compatibility
45
+ with remote_cache.)
46
+
47
+ Deployment happens in three major steps. First, the local cache directory is
48
+ processed. There are three possibilities:
49
+
50
+ * If the local cache does not exist, it is created with a checkout of the
51
+ revision to be deployed.
52
+ * If the local cache exists and matches the <tt>:repository</tt> variable, it is
53
+ updated to the revision to be deployed.
54
+ * If the local cache exists and does not match the <p>:repository</p> variable,
55
+ the local cache is purged and recreated with a checkout of the revision
56
+ to be deployed.
57
+ * If the local cache exists but is not a directory, an exception is raised
58
+
59
+ Second, <tt>rsync</tt> runs on the local side to sync the remote cache to the local
60
+ cache. When the <tt>rsync</tt> is complete, the remote cache should be an exact
61
+ replica of the local cache.
62
+
63
+ Finally, a copy of the remote cache is made in the appropriate release
64
+ directory. The end result is the same as if the code had been checked out
65
+ directly on the remote server, as in the default strategy.
66
+
67
+ == Contributors
68
+
69
+ Thanks to the people who submitted patches:
70
+
71
+ * {S. Brent Faulkner}[http://github.com/sbfaulkner]
72
+
73
+ == License
74
+
75
+ Copyright (c) 2007 - 2010 Patrick Reagan (patrick.reagan@viget.com) & Mark Cornick
76
+
77
+ Permission is hereby granted, free of charge, to any person
78
+ obtaining a copy of this software and associated documentation
79
+ files (the "Software"), to deal in the Software without
80
+ restriction, including without limitation the rights to use,
81
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
82
+ copies of the Software, and to permit persons to whom the
83
+ Software is furnished to do so, subject to the following
84
+ conditions:
85
+
86
+ The above copyright notice and this permission notice shall be
87
+ included in all copies or substantial portions of the Software.
88
+
89
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
90
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
91
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
92
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
93
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
94
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
95
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
96
+ OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ require 'rubygems'
2
+ require 'rake/gempackagetask'
3
+ require 'rake/testtask'
4
+
5
+ spec = eval(File.read(File.expand_path("../capistrano_rsync_with_remote_cache.gemspec", __FILE__)))
6
+
7
+ Rake::GemPackageTask.new(spec) do |pkg|
8
+ pkg.gem_spec = spec
9
+ end
10
+
11
+ Rake::TestTask.new do |t|
12
+ t.libs << 'test'
13
+ t.test_files = FileList["test/**/*_test.rb"]
14
+ t.verbose = true
15
+ end
16
+
17
+ task :default => :test
@@ -0,0 +1,121 @@
1
+ require 'capistrano/recipes/deploy/strategy/remote'
2
+ require 'fileutils'
3
+
4
+ module Capistrano
5
+ module Deploy
6
+ module Strategy
7
+ class RsyncWithRemoteCache < Remote
8
+
9
+ class InvalidCacheError < Exception; end
10
+
11
+ def self.default_attribute(attribute, default_value)
12
+ define_method(attribute) { configuration[attribute] || default_value }
13
+ end
14
+
15
+ INFO_COMMANDS = {
16
+ :subversion => "svn info . | sed -n \'s/URL: //p\'",
17
+ :git => "git config remote.origin.url",
18
+ :mercurial => "hg showconfig paths.default",
19
+ :bzr => "bzr info | grep parent | sed \'s/^.*parent branch: //\'"
20
+ }
21
+
22
+ default_attribute :rsync_options, '-az --delete'
23
+ default_attribute :local_cache, '.rsync_cache'
24
+ default_attribute :repository_cache, 'cached-copy'
25
+
26
+ def deploy!
27
+ update_local_cache
28
+ update_remote_cache
29
+ copy_remote_cache
30
+ end
31
+
32
+ def update_local_cache
33
+ system(command) or raise "Dynport fixed it!"
34
+ mark_local_cache
35
+ end
36
+
37
+ def update_remote_cache
38
+ finder_options = {:except => { :no_release => true }}
39
+ find_servers(finder_options).each {|s| system(rsync_command_for(s)) }
40
+ end
41
+
42
+ def copy_remote_cache
43
+ run("rsync -a --delete #{repository_cache_path}/ #{configuration[:release_path]}/")
44
+ end
45
+
46
+ def rsync_command_for(server)
47
+ "rsync #{rsync_options} --rsh='ssh -p #{ssh_port(server)}' #{local_cache_path}/ #{rsync_host(server)}:#{repository_cache_path}/"
48
+ end
49
+
50
+ def mark_local_cache
51
+ File.open(File.join(local_cache_path, 'REVISION'), 'w') {|f| f << revision }
52
+ end
53
+
54
+ def ssh_port(server)
55
+ server.port || ssh_options[:port] || 22
56
+ end
57
+
58
+ def local_cache_path
59
+ File.expand_path(local_cache)
60
+ end
61
+
62
+ def repository_cache_path
63
+ File.join(shared_path, repository_cache)
64
+ end
65
+
66
+ def repository_url
67
+ `cd #{local_cache_path} && #{INFO_COMMANDS[configuration[:scm]]}`.strip
68
+ end
69
+
70
+ def repository_url_changed?
71
+ repository_url != configuration[:repository]
72
+ end
73
+
74
+ def remove_local_cache
75
+ logger.trace "repository has changed; removing old local cache from #{local_cache_path}"
76
+ FileUtils.rm_rf(local_cache_path)
77
+ end
78
+
79
+ def remove_cache_if_repository_url_changed
80
+ remove_local_cache if repository_url_changed?
81
+ end
82
+
83
+ def rsync_host(server)
84
+ configuration[:user] ? "#{configuration[:user]}@#{server.host}" : server.host
85
+ end
86
+
87
+ def local_cache_exists?
88
+ File.exist?(local_cache_path)
89
+ end
90
+
91
+ def local_cache_valid?
92
+ local_cache_exists? && File.directory?(local_cache_path)
93
+ end
94
+
95
+ # Defines commands that should be checked for by deploy:check. These include the SCM command
96
+ # on the local end, and rsync on both ends. Note that the SCM command is not needed on the
97
+ # remote end.
98
+ def check!
99
+ super.check do |check|
100
+ check.local.command(source.command)
101
+ check.local.command('rsync')
102
+ check.remote.command('rsync')
103
+ end
104
+ end
105
+
106
+ private
107
+
108
+ def command
109
+ if local_cache_valid?
110
+ source.sync(revision, local_cache_path)
111
+ elsif !local_cache_exists?
112
+ "mkdir -p #{local_cache_path} && #{source.checkout(revision, local_cache_path)}"
113
+ else
114
+ raise InvalidCacheError, "The local cache exists but is not valid (#{local_cache_path})"
115
+ end
116
+ end
117
+ end
118
+
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,299 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'mocha'
4
+ require 'shoulda'
5
+ require 'matchy'
6
+ require 'tmpdir'
7
+
8
+ require 'capistrano/recipes/deploy/strategy/rsync_with_remote_cache'
9
+
10
+ class CapistranoRsyncWithRemoteCacheTest < Test::Unit::TestCase
11
+
12
+ context "An instance of the CapistranoRsyncWithRemoteCache class" do
13
+ setup { @strategy = Capistrano::Deploy::Strategy::RsyncWithRemoteCache.new }
14
+
15
+ should "know the default rsync options" do
16
+ @strategy.rsync_options.should == '-az --delete'
17
+ end
18
+
19
+ should "allow overriding of the rsync options" do
20
+ @strategy.stubs(:configuration).with().returns(:rsync_options => 'new_opts')
21
+ @strategy.rsync_options.should == 'new_opts'
22
+ end
23
+
24
+ should "know the default local cache name" do
25
+ @strategy.local_cache.should == '.rsync_cache'
26
+ end
27
+
28
+ should "know the local cache name if it has been configured" do
29
+ @strategy.stubs(:configuration).with().returns(:local_cache => 'cache')
30
+ @strategy.local_cache.should == 'cache'
31
+ end
32
+
33
+ should "know the cache path" do
34
+ @strategy.stubs(:local_cache).with().returns('cache_dir')
35
+ File.expects(:expand_path).with('cache_dir').returns('local_cache_path')
36
+
37
+ @strategy.local_cache_path.should == 'local_cache_path'
38
+ end
39
+
40
+ should "know the repository URL for a subversion repository" do
41
+ @strategy.stubs(:local_cache_path).with().returns('cache_path')
42
+ @strategy.stubs(:configuration).with().returns(:scm => :subversion)
43
+ @strategy.expects(:`).with("cd cache_path && svn info . | sed -n \'s/URL: //p\'").returns("svn_url\n")
44
+ @strategy.repository_url.should == 'svn_url'
45
+ end
46
+
47
+ should "know the repository URL for a git repository" do
48
+ @strategy.stubs(:local_cache_path).with().returns('cache_path')
49
+ @strategy.stubs(:configuration).with().returns(:scm => :git)
50
+ @strategy.expects(:`).with("cd cache_path && git config remote.origin.url").returns("git_url\n")
51
+ @strategy.repository_url.should == 'git_url'
52
+ end
53
+
54
+ should "know the repository URL for a mercurial repository" do
55
+ @strategy.stubs(:local_cache_path).with().returns('cache_path')
56
+ @strategy.stubs(:configuration).with().returns(:scm => :mercurial)
57
+ @strategy.expects(:`).with("cd cache_path && hg showconfig paths.default").returns("hg_url\n")
58
+ @strategy.repository_url.should == 'hg_url'
59
+ end
60
+
61
+ should "know the repository URL for a bzr repository" do
62
+ @strategy.stubs(:local_cache_path).with().returns('cache_path')
63
+ @strategy.stubs(:configuration).with().returns(:scm => :bzr)
64
+ @strategy.expects(:`).with("cd cache_path && bzr info | grep parent | sed \'s/^.*parent branch: //\'").returns("bzr_url\n")
65
+ @strategy.repository_url.should == 'bzr_url'
66
+ end
67
+
68
+ should "know that the repository URL has not changed" do
69
+ @strategy.stubs(:repository_url).with().returns('repo_url')
70
+ @strategy.stubs(:configuration).with().returns(:repository => 'repo_url')
71
+
72
+ @strategy.repository_url_changed?.should be(false)
73
+ end
74
+
75
+ should "know that the repository URL has changed" do
76
+ @strategy.stubs(:repository_url).with().returns('new_repo_url')
77
+ @strategy.stubs(:configuration).with().returns(:repository => 'old_repo_url')
78
+
79
+ @strategy.repository_url_changed?.should be(true)
80
+ end
81
+
82
+ should "be able to remove the local cache" do
83
+ @strategy.stubs(:logger).with().returns(stub(:trace))
84
+ @strategy.stubs(:local_cache_path).with().returns('local_cache_path')
85
+ FileUtils.expects(:rm_rf).with('local_cache_path')
86
+
87
+ @strategy.remove_local_cache
88
+ end
89
+
90
+ should "remove the local cache if the repository URL has changed" do
91
+ @strategy.stubs(:repository_url_changed?).with().returns(true)
92
+ @strategy.expects(:remove_local_cache).with()
93
+
94
+ @strategy.remove_cache_if_repository_url_changed
95
+ end
96
+
97
+ should "not remove the local cache if the repository URL has not changed" do
98
+ @strategy.stubs(:repository_url_changed?).with().returns(false)
99
+ @strategy.expects(:remove_local_cache).with().never
100
+
101
+ @strategy.remove_cache_if_repository_url_changed
102
+ end
103
+
104
+ should "know the default SSH port" do
105
+ @strategy.stubs(:ssh_options).with().returns({})
106
+ server = stub(:port => nil)
107
+ @strategy.ssh_port(server).should == 22
108
+ end
109
+
110
+ should "be able to override the default SSH port" do
111
+ @strategy.stubs(:ssh_options).with().returns({:port => 95})
112
+ server = stub(:port => nil)
113
+ @strategy.ssh_port(server).should == 95
114
+ end
115
+
116
+ should "be able to override the default SSH port for each server" do
117
+ @strategy.stubs(:ssh_options).with().returns({:port => 95})
118
+ server = stub(:port => 123)
119
+ @strategy.ssh_port(server).should == 123
120
+ end
121
+
122
+ should "know the default repository cache" do
123
+ @strategy.repository_cache.should == 'cached-copy'
124
+ end
125
+
126
+ should "be able to override the default repository cache" do
127
+ @strategy.stubs(:configuration).with().returns(:repository_cache => 'other_cache')
128
+ @strategy.repository_cache.should == 'other_cache'
129
+ end
130
+
131
+ should "know the repository cache path" do
132
+ @strategy.stubs(:shared_path).with().returns('shared_path')
133
+ @strategy.stubs(:repository_cache).with().returns('cache_path')
134
+
135
+ File.expects(:join).with('shared_path', 'cache_path').returns('path')
136
+ @strategy.repository_cache_path.should == 'path'
137
+ end
138
+
139
+ should "be able to determine the hostname for the rsync command" do
140
+ server = stub(:host => 'host.com')
141
+ @strategy.rsync_host(server).should == 'host.com'
142
+ end
143
+
144
+ should "be able to determine the hostname for the rsync command when a user is configured" do
145
+ @strategy.stubs(:configuration).with().returns(:user => 'foobar')
146
+ server = stub(:host => 'host.com')
147
+
148
+ @strategy.rsync_host(server).should == 'foobar@host.com'
149
+ end
150
+
151
+ should "know that the local cache exists" do
152
+ @strategy.stubs(:local_cache_path).with().returns('path')
153
+ File.stubs(:exist?).with('path').returns(true)
154
+
155
+ @strategy.local_cache_exists?.should be(true)
156
+ end
157
+
158
+ should "know that the local cache does not exist" do
159
+ @strategy.stubs(:local_cache_path).with().returns('path')
160
+ File.stubs(:exist?).with('path').returns(false)
161
+
162
+ @strategy.local_cache_exists?.should be(false)
163
+ end
164
+
165
+ should "know that the local cache is not valid if it does not exist" do
166
+ @strategy.stubs(:local_cache_exists?).with().returns(false)
167
+ @strategy.local_cache_valid?.should be(false)
168
+ end
169
+
170
+ should "know that the local cache is not valid if it's not a directory" do
171
+ @strategy.stubs(:local_cache_path).with().returns('path')
172
+ @strategy.stubs(:local_cache_exists?).with().returns(true)
173
+
174
+ File.stubs(:directory?).with('path').returns(false)
175
+ @strategy.local_cache_valid?.should be(false)
176
+ end
177
+
178
+ should "know that the local cache is valid" do
179
+ @strategy.stubs(:local_cache_path).with().returns('path')
180
+ @strategy.stubs(:local_cache_exists?).with().returns(true)
181
+
182
+ File.stubs(:directory?).with('path').returns(true)
183
+ @strategy.local_cache_valid?.should be(true)
184
+ end
185
+
186
+ should "know the SCM command when the local cache is valid" do
187
+ source = mock() {|s| s.expects(:sync).with('revision', 'path').returns('scm_command') }
188
+
189
+ @strategy.stubs(:local_cache_valid?).with().returns(true)
190
+ @strategy.stubs(:local_cache_path).with().returns('path')
191
+ @strategy.stubs(:revision).with().returns('revision')
192
+ @strategy.stubs(:source).with().returns(source)
193
+
194
+ @strategy.send(:command).should == 'scm_command'
195
+ end
196
+
197
+ should "know the SCM command when the local cache does not exist" do
198
+ source = mock() {|s| s.expects(:checkout).with('revision', 'path').returns('scm_command') }
199
+
200
+ @strategy.stubs(:local_cache_valid?).with().returns(false)
201
+ @strategy.stubs(:local_cache_exists?).with().returns(false)
202
+ @strategy.stubs(:local_cache_path).with().returns('path')
203
+ @strategy.stubs(:revision).with().returns('revision')
204
+ @strategy.stubs(:source).with().returns(source)
205
+
206
+ @strategy.send(:command).should == 'mkdir -p path && scm_command'
207
+ end
208
+
209
+ should "raise an exception when the local cache is invalid" do
210
+ @strategy.stubs(:local_cache_valid?).with().returns(false)
211
+ @strategy.stubs(:local_cache_exists?).with().returns(true)
212
+
213
+ lambda {
214
+ @strategy.send(:command)
215
+ }.should raise_error(Capistrano::Deploy::Strategy::RsyncWithRemoteCache::InvalidCacheError)
216
+ end
217
+
218
+ should "be able to tag the local cache" do
219
+ local_cache_path = Dir.tmpdir
220
+ @strategy.stubs(:revision).with().returns('1')
221
+ @strategy.stubs(:local_cache_path).with().returns(local_cache_path)
222
+
223
+ @strategy.mark_local_cache
224
+
225
+ File.read(File.join(local_cache_path, 'REVISION')).should == '1'
226
+ end
227
+
228
+ should "be able to update the local cache" do
229
+ @strategy.stubs(:command).with().returns('scm_command')
230
+ @strategy.expects(:system).with('scm_command').returns true
231
+ @strategy.expects(:mark_local_cache).with()
232
+
233
+ @strategy.update_local_cache
234
+ end
235
+
236
+ [nil, false].each do |value|
237
+ should "raise an error when system returns nil" do
238
+ @strategy.stubs(:command).returns('failing_command')
239
+ @strategy.stubs(:system).with("failing_command").returns value
240
+ @strategy.stubs(:mark_local_cache).returns nil
241
+ lambda {
242
+ @strategy.update_local_cache
243
+ }.should raise_error
244
+ end
245
+ end
246
+
247
+ should "be able to run the rsync command on a server" do
248
+ server = stub()
249
+
250
+ @strategy.stubs(:rsync_host).with(server).returns('rsync_host')
251
+
252
+ @strategy.stubs(
253
+ :rsync_options => 'rsync_options',
254
+ :ssh_port => 'ssh_port',
255
+ :local_cache_path => 'local_cache_path',
256
+ :repository_cache_path => 'repository_cache_path'
257
+ )
258
+
259
+ expected = "rsync rsync_options --rsh='ssh -p ssh_port' local_cache_path/ rsync_host:repository_cache_path/"
260
+
261
+ @strategy.rsync_command_for(server).should == expected
262
+ end
263
+
264
+ should "be able to update the remote cache" do
265
+ server_1, server_2 = [stub(), stub()]
266
+ @strategy.stubs(:find_servers).with(:except => {:no_release => true}).returns([server_1, server_2])
267
+
268
+ @strategy.stubs(:rsync_command_for).with(server_1).returns('server_1_rsync_command')
269
+ @strategy.stubs(:rsync_command_for).with(server_2).returns('server_2_rsync_command')
270
+
271
+ @strategy.expects(:system).with('server_1_rsync_command')
272
+ @strategy.expects(:system).with('server_2_rsync_command')
273
+
274
+ @strategy.update_remote_cache
275
+ end
276
+
277
+ should "be able copy the remote cache into place" do
278
+ @strategy.stubs(
279
+ :repository_cache_path => 'repository_cache_path',
280
+ :configuration => {:release_path => 'release_path'}
281
+ )
282
+
283
+ command = "rsync -a --delete repository_cache_path/ release_path/"
284
+ @strategy.expects(:run).with(command)
285
+
286
+ @strategy.copy_remote_cache
287
+ end
288
+
289
+ should "be able to deploy" do
290
+ @strategy.expects(:update_local_cache).with()
291
+ @strategy.expects(:update_remote_cache).with()
292
+ @strategy.expects(:copy_remote_cache).with()
293
+
294
+ @strategy.deploy!
295
+ end
296
+
297
+ end
298
+
299
+ end
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dynport-capistrano_rsync_with_remote_cache
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 2.4.1
6
+ platform: ruby
7
+ authors:
8
+ - Patrick Reagan
9
+ - Mark Cornick
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+
14
+ date: 2011-07-15 00:00:00 Z
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: capistrano
18
+ prerelease: false
19
+ requirement: &id001 !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: 2.1.0
25
+ type: :runtime
26
+ version_requirements: *id001
27
+ description:
28
+ email: patrick.reagan@viget.com
29
+ executables: []
30
+
31
+ extensions: []
32
+
33
+ extra_rdoc_files:
34
+ - README.rdoc
35
+ files:
36
+ - README.rdoc
37
+ - Rakefile
38
+ - lib/capistrano/recipes/deploy/strategy/rsync_with_remote_cache.rb
39
+ - test/capistrano_rsync_with_remote_cache_test.rb
40
+ homepage: http://www.viget.com/extend/
41
+ licenses: []
42
+
43
+ post_install_message:
44
+ rdoc_options:
45
+ - --main
46
+ - README.rdoc
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: "0"
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: "0"
61
+ requirements: []
62
+
63
+ rubyforge_project:
64
+ rubygems_version: 1.7.2
65
+ signing_key:
66
+ specification_version: 3
67
+ summary: A deployment strategy for Capistrano 2.0 which combines rsync with a remote cache, allowing fast deployments from SCM servers behind firewalls.
68
+ test_files: []
69
+