├── .gitignore ├── Gemfile ├── Gemfile.lock ├── README.rdoc ├── Rakefile ├── capistrano_rsync_with_remote_cache.gemspec ├── lib └── capistrano │ └── recipes │ └── deploy │ └── strategy │ └── rsync_with_remote_cache.rb └── spec └── capistrano_rsync_with_remote_cache_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /pkg/ 2 | /doc/ 3 | /coverage/ 4 | *.gem 5 | .yardoc 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in capistrano_rsync_with_remote_cache.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | capistrano_rsync_with_remote_cache (2.4.0) 5 | capistrano (~> 2.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | capistrano (2.15.5) 11 | highline 12 | net-scp (>= 1.0.0) 13 | net-sftp (>= 2.0.0) 14 | net-ssh (>= 2.0.14) 15 | net-ssh-gateway (>= 1.1.0) 16 | diff-lcs (1.2.5) 17 | highline (1.6.21) 18 | net-scp (1.2.1) 19 | net-ssh (>= 2.6.5) 20 | net-sftp (2.1.2) 21 | net-ssh (>= 2.6.5) 22 | net-ssh (2.9.1) 23 | net-ssh-gateway (1.2.0) 24 | net-ssh (>= 2.6.5) 25 | rake (10.3.2) 26 | rspec (3.1.0) 27 | rspec-core (~> 3.1.0) 28 | rspec-expectations (~> 3.1.0) 29 | rspec-mocks (~> 3.1.0) 30 | rspec-core (3.1.6) 31 | rspec-support (~> 3.1.0) 32 | rspec-expectations (3.1.2) 33 | diff-lcs (>= 1.2.0, < 2.0) 34 | rspec-support (~> 3.1.0) 35 | rspec-mocks (3.1.3) 36 | rspec-support (~> 3.1.0) 37 | rspec-support (3.1.2) 38 | 39 | PLATFORMS 40 | ruby 41 | 42 | DEPENDENCIES 43 | bundler (~> 1.6) 44 | capistrano_rsync_with_remote_cache! 45 | rake (~> 10.0) 46 | rspec (~> 3.1.0) 47 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 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 | rsync 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 rsync command-line utilities on the local and 16 | remote hosts. It also requires either svn, git, hg 17 | or bzr 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 deploy.rb 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 :local_cache variable in the 37 | configuration. If not specified, it will default to .rsync_cache 38 | in the same directory as the Capfile. 39 | 40 | * The remote cache directory is an rsync copy of the local cache directory. 41 | The remote cache directory is specified with the :repository_cache variable 42 | in the configuration (this name comes from the :remote_cache strategy that 43 | ships with Capistrano, and has been maintained for compatibility.) If not 44 | specified, it will default to shared/cached-copy (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 :repository variable, it is 53 | updated to the revision to be deployed. 54 | * If the local cache exists and does not match the
:repository
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, rsync runs on the local side to sync the remote cache to the local 60 | cache. When the rsync 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. 97 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec -------------------------------------------------------------------------------- /capistrano_rsync_with_remote_cache.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'capistrano_rsync_with_remote_cache' 7 | spec.version = '2.4.0' 8 | spec.authors = ['Patrick Reagan', 'Mark Cornick'] 9 | spec.email = ['patrick.reagan@viget.com'] 10 | 11 | spec.has_rdoc = true 12 | spec.extra_rdoc_files = %w(README.rdoc) 13 | spec.rdoc_options = %w(--main README.rdoc) 14 | spec.summary = "A deployment strategy for Capistrano 2.0 which combines rsync with a remote cache, allowing fast deployments from SCM servers behind firewalls." 15 | spec.description = spec.summary 16 | spec.homepage = 'https://github.com/vigetlabs/capistrano_rsync_with_remote_cache' 17 | spec.files = %w(README.rdoc Rakefile) + Dir.glob("{lib,spec}/**/*") 18 | 19 | spec.add_dependency 'capistrano', '~> 2.0' 20 | 21 | spec.add_development_dependency "bundler", "~> 1.6" 22 | spec.add_development_dependency "rspec", "~> 3.1.0" 23 | spec.add_development_dependency "rake", "~> 10.0" 24 | end 25 | -------------------------------------------------------------------------------- /lib/capistrano/recipes/deploy/strategy/rsync_with_remote_cache.rb: -------------------------------------------------------------------------------- 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 | CONFIG = { 12 | :subversion => {:url_command => "svn info . | sed -n \'s/URL: //p\'", :exclusions => '.svn*'}, 13 | :git => {:url_command => "git config remote.origin.url", :exclusions => '.git*'}, 14 | :mercurial => {:url_command => "hg showconfig paths.default", :exclusions => '.hg*'}, 15 | :bzr => {:url_command => "bzr info | grep parent | sed \'s/^.*parent branch: //\'", :exclusions => '.bzr*'} 16 | } 17 | 18 | def self.default_attribute(attribute, default_value) 19 | define_method(attribute) { configuration[attribute] || default_value } 20 | end 21 | 22 | default_attribute :rsync_options, '-az --delete-excluded' 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) 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| sync_source_to(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 sync_source_to(server) 47 | run_rsync(rsync_options, exclusion_options, "--rsh='#{ssh_command_for(server)}'", "'#{local_cache_path}/'", "#{rsync_host(server)}:#{repository_cache_path}/", :local => true) 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 default_exclusions 55 | Array(CONFIG[configuration[:scm]].fetch(:exclusions, [])) 56 | end 57 | 58 | def exclusion_options 59 | copy_exclude.map {|f| "--exclude='#{f}'" }.join(' ') 60 | end 61 | 62 | def ssh_port(server) 63 | server.port || ssh_options[:port] || configuration[:port] 64 | end 65 | 66 | def ssh_command_for(server) 67 | port = ssh_port(server) 68 | port.nil? ? "ssh" : "ssh -p #{port}" 69 | end 70 | 71 | def local_cache_path 72 | File.expand_path(local_cache) 73 | end 74 | 75 | def repository_cache_path 76 | File.join(shared_path, repository_cache) 77 | end 78 | 79 | def repository_url 80 | `cd #{local_cache_path} && #{CONFIG[configuration[:scm]][:url_command]}`.strip 81 | end 82 | 83 | def repository_url_changed? 84 | repository_url != configuration[:repository] 85 | end 86 | 87 | def remove_local_cache 88 | logger.trace "repository has changed; removing old local cache from #{local_cache_path}" 89 | FileUtils.rm_rf(local_cache_path) 90 | end 91 | 92 | def remove_cache_if_repository_url_changed 93 | remove_local_cache if repository_url_changed? 94 | end 95 | 96 | def rsync_host(server) 97 | configuration[:user] ? "#{configuration[:user]}@#{server.host}" : server.host 98 | end 99 | 100 | def local_cache_exists? 101 | File.exist?(local_cache_path) 102 | end 103 | 104 | def local_cache_valid? 105 | local_cache_exists? && File.directory?(local_cache_path) 106 | end 107 | 108 | # Defines commands that should be checked for by deploy:check. These include the SCM command 109 | # on the local end, and rsync on both ends. Note that the SCM command is not needed on the 110 | # remote end. 111 | def check! 112 | super.check do |check| 113 | check.local.command(source.command) 114 | check.local.command('rsync') 115 | check.remote.command('rsync') 116 | end 117 | end 118 | 119 | def command 120 | if local_cache_valid? 121 | source.sync(revision, local_cache_path) 122 | elsif !local_cache_exists? 123 | "mkdir -p #{local_cache_path} && #{source.checkout(revision, local_cache_path)}" 124 | else 125 | raise InvalidCacheError, "The local cache exists but is not valid (#{local_cache_path})" 126 | end 127 | end 128 | 129 | private 130 | 131 | def copy_exclude 132 | default_exclusions + Array(configuration.fetch(:copy_exclude, [])) 133 | end 134 | 135 | def run_rsync(*args) 136 | options = args.last.is_a?(Hash) ? args.pop : {} 137 | 138 | command_options = args.select {|a| a.strip.length > 0 }.join(' ') 139 | command = "rsync #{command_options}" 140 | 141 | options.fetch(:local, false) ? system(command) : run(command) 142 | end 143 | 144 | end 145 | end 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /spec/capistrano_rsync_with_remote_cache_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | 4 | require 'rspec' 5 | require 'tmpdir' 6 | 7 | require 'capistrano/recipes/deploy/strategy/rsync_with_remote_cache' 8 | 9 | RSpec.describe Capistrano::Deploy::Strategy::RsyncWithRemoteCache do 10 | 11 | describe "#rsync_options" do 12 | it "has a default value" do 13 | expect(subject.rsync_options).to eq('-az --delete-excluded') 14 | end 15 | 16 | it "allows a user-specified value" do 17 | expect(subject).to receive(:configuration).with(no_args).and_return(:rsync_options => 'new_opts') 18 | expect(subject.rsync_options).to eq('new_opts') 19 | end 20 | end 21 | 22 | describe "#default_exclusions" do 23 | it "knows the value when using Subversion" do 24 | allow(subject).to receive(:configuration).with(no_args).and_return(:scm => :subversion) 25 | expect(subject.default_exclusions).to eq(['.svn*']) 26 | end 27 | 28 | it "knows the value when using Git" do 29 | allow(subject).to receive(:configuration).with(no_args).and_return(:scm => :git) 30 | expect(subject.default_exclusions).to eq(['.git*']) 31 | end 32 | 33 | it "knows the value when using Mercurial" do 34 | allow(subject).to receive(:configuration).with(no_args).and_return(:scm => :mercurial) 35 | expect(subject.default_exclusions).to eq(['.hg*']) 36 | end 37 | 38 | it "knows the value when using Bazaar" do 39 | allow(subject).to receive(:configuration).with(no_args).and_return(:scm => :bzr) 40 | expect(subject.default_exclusions).to eq(['.bzr*']) 41 | end 42 | end 43 | 44 | describe "#exclusion_options" do 45 | before { allow(subject).to receive(:default_exclusions).with(no_args).and_return(['.git*']) } 46 | 47 | it "includes the SCM-specific list by default" do 48 | expect(subject.exclusion_options).to eq("--exclude='.git*'") 49 | end 50 | 51 | it "uses the value specified in the `:copy_exclude` configuration variable" do 52 | allow(subject).to receive(:configuration).with(no_args).and_return({:copy_exclude => '.jenkins'}) 53 | expect(subject.exclusion_options).to eq("--exclude='.git*' --exclude='.jenkins'") 54 | end 55 | 56 | it "allows multiple exclusions" do 57 | allow(subject).to receive(:configuration).with(no_args).and_return({:copy_exclude => ['.jenkins', 'test']}) 58 | expect(subject.exclusion_options).to eq("--exclude='.git*' --exclude='.jenkins' --exclude='test'") 59 | end 60 | end 61 | 62 | describe "#local_cache" do 63 | it "has a default value" do 64 | expect(subject.local_cache).to eq('.rsync_cache') 65 | end 66 | 67 | it "allows a user-specified value" do 68 | expect(subject).to receive(:configuration).with(no_args).and_return(:local_cache => 'cache') 69 | expect(subject.local_cache).to eq('cache') 70 | end 71 | end 72 | 73 | describe "#local_cache_path" do 74 | it "is generated from the full path to the cache" do 75 | expect(subject).to receive(:local_cache).with(no_args).and_return('cache_dir') 76 | expect(File).to receive(:expand_path).with('cache_dir').and_return('local_cache_path') 77 | 78 | expect(subject.local_cache_path).to eq('local_cache_path') 79 | end 80 | end 81 | 82 | describe "#repository_url" do 83 | before { expect(subject).to receive(:local_cache_path).with(no_args).and_return('cache_path') } 84 | 85 | it "knows the value for a Subversion repository" do 86 | expect(subject).to receive(:configuration).with(no_args).and_return(:scm => :subversion) 87 | expect(subject).to receive(:`).with("cd cache_path && svn info . | sed -n \'s/URL: //p\'").and_return("svn_url\n") 88 | expect(subject.repository_url).to eq('svn_url') 89 | end 90 | 91 | it "knows the value for a Git repository" do 92 | expect(subject).to receive(:configuration).with(no_args).and_return(:scm => :git) 93 | expect(subject).to receive(:`).with("cd cache_path && git config remote.origin.url").and_return("git_url\n") 94 | expect(subject.repository_url).to eq('git_url') 95 | end 96 | 97 | it "knows the value for a Mercurial repository" do 98 | expect(subject).to receive(:configuration).with(no_args).and_return(:scm => :mercurial) 99 | expect(subject).to receive(:`).with("cd cache_path && hg showconfig paths.default").and_return("hg_url\n") 100 | expect(subject.repository_url).to eq('hg_url') 101 | end 102 | 103 | it "knows the value for a bzr repository" do 104 | expect(subject).to receive(:configuration).with(no_args).and_return(:scm => :bzr) 105 | expect(subject).to receive(:`).with("cd cache_path && bzr info | grep parent | sed \'s/^.*parent branch: //\'").and_return("bzr_url\n") 106 | expect(subject.repository_url).to eq('bzr_url') 107 | end 108 | end 109 | 110 | describe "#url_changed?" do 111 | it "is false if it has not changed" do 112 | expect(subject).to receive(:repository_url).with(no_args).and_return('repo_url') 113 | expect(subject).to receive(:configuration).with(no_args).and_return(:repository => 'repo_url') 114 | 115 | expect(subject.repository_url_changed?).to be(false) 116 | end 117 | 118 | it "is true if it has changed" do 119 | expect(subject).to receive(:repository_url).with(no_args).and_return('new_repo_url') 120 | expect(subject).to receive(:configuration).with(no_args).and_return(:repository => 'old_repo_url') 121 | 122 | expect(subject.repository_url_changed?).to be(true) 123 | end 124 | end 125 | 126 | describe "#remove_local_cache" do 127 | it "removes the local directory" do 128 | expect(subject).to receive(:logger).with(no_args).and_return(double(:trace => nil)) 129 | expect(subject).to receive(:local_cache_path).at_least(:once).with(no_args).and_return('local_cache_path') 130 | expect(FileUtils).to receive(:rm_rf).with('local_cache_path') 131 | 132 | subject.remove_local_cache 133 | end 134 | end 135 | 136 | describe "#remove_cache_if_repository_url_changed" do 137 | it "removes the local cache if the repository URL has changed" do 138 | expect(subject).to receive(:repository_url_changed?).with(no_args).and_return(true) 139 | expect(subject).to receive(:remove_local_cache).with(no_args) 140 | 141 | subject.remove_cache_if_repository_url_changed 142 | end 143 | 144 | it "does not remove the local cache if the repository URL has not changed" do 145 | expect(subject).to receive(:repository_url_changed?).with(no_args).and_return(false) 146 | expect(subject).to receive(:remove_local_cache).never 147 | 148 | subject.remove_cache_if_repository_url_changed 149 | end 150 | end 151 | 152 | describe "#ssh_port" do 153 | let(:server) { double(:port => nil) } 154 | 155 | it "is nil by default" do 156 | allow(subject).to receive(:ssh_options).with(no_args).and_return({}) 157 | expect(subject.ssh_port(server)).to be_nil 158 | end 159 | 160 | it "uses the configured SSH port if specified" do 161 | server = double(:port => nil) 162 | 163 | allow(subject).to receive_messages({ 164 | :ssh_options => {:port => 95}, 165 | :configuration => {:port => 3000} 166 | }) 167 | 168 | expect(subject.ssh_port(server)).to eq(95) 169 | end 170 | 171 | it "uses the value for `:port` if available" do 172 | allow(subject).to receive_messages({ 173 | :ssh_options => {}, 174 | :configuration => {:port => 3000} 175 | }) 176 | 177 | expect(subject.ssh_port(server)).to eq(3000) 178 | end 179 | 180 | it "can be set on a per-server basis" do 181 | server = double(:port => 123) 182 | 183 | allow(subject).to receive_messages({ 184 | :configuration => {:port => 3000}, 185 | :ssh_options => {:port => 95} 186 | }) 187 | 188 | expect(subject.ssh_port(server)).to eq(123) 189 | end 190 | end 191 | 192 | describe "#ssh_command_for" do 193 | it "does not include the port if it isn't overridden" do 194 | allow(subject).to receive(:ssh_port).with('server').and_return(nil) 195 | 196 | expect(subject.ssh_command_for('server')).to eq('ssh') 197 | end 198 | 199 | it "includes the port if necessary" do 200 | allow(subject).to receive(:ssh_port).with('server').and_return(3000) 201 | expect(subject.ssh_command_for('server')).to eq('ssh -p 3000') 202 | end 203 | end 204 | 205 | describe "#repository_cache" do 206 | it "has a default value" do 207 | expect(subject.repository_cache).to eq('cached-copy') 208 | end 209 | 210 | it "can be overridden" do 211 | allow(subject).to receive(:configuration).with(no_args).and_return(:repository_cache => 'other_cache') 212 | expect(subject.repository_cache).to eq('other_cache') 213 | end 214 | end 215 | 216 | describe "#repository_cache_path" do 217 | it "is generated from the full path to the cache" do 218 | allow(subject).to receive(:shared_path).with(no_args).and_return('shared_path') 219 | allow(subject).to receive(:repository_cache).with(no_args).and_return('cache_path') 220 | 221 | allow(File).to receive(:join).with('shared_path', 'cache_path').and_return('path') 222 | 223 | expect(subject.repository_cache_path).to eq('path') 224 | end 225 | end 226 | 227 | describe "#rsync_host" do 228 | let(:server) { double(:host => 'host.com') } 229 | it "is taken from the server's host attribute by default" do 230 | expect(subject.rsync_host(server)).to eq('host.com') 231 | end 232 | 233 | it "can be overridden" do 234 | allow(subject).to receive(:configuration).with(no_args).and_return(:user => 'foobar') 235 | 236 | expect(subject.rsync_host(server)).to eq('foobar@host.com') 237 | end 238 | end 239 | 240 | describe "#local_cache_exists?" do 241 | it "returns true when the directory exists" do 242 | allow(subject).to receive(:local_cache_path).with(no_args).and_return('path') 243 | allow(File).to receive(:exist?).with('path').and_return(true) 244 | 245 | expect(subject.local_cache_exists?).to be(true) 246 | end 247 | 248 | it "returns false when the directory does not exist" do 249 | allow(subject).to receive(:local_cache_path).with(no_args).and_return('path') 250 | allow(File).to receive(:exist?).with('path').and_return(false) 251 | 252 | expect(subject.local_cache_exists?).to be(false) 253 | end 254 | end 255 | 256 | describe "#local_cache_valid?" do 257 | it "is false if the cache directory does not exist" do 258 | allow(subject).to receive(:local_cache_exists?).with(no_args).and_return(false) 259 | expect(subject.local_cache_valid?).to be(false) 260 | end 261 | 262 | it "is false if the cache path is not a directory" do 263 | allow(subject).to receive(:local_cache_path).with(no_args).and_return('path') 264 | allow(subject).to receive(:local_cache_exists?).with(no_args).and_return(true) 265 | 266 | allow(File).to receive(:directory?).with('path').and_return(false) 267 | 268 | expect(subject.local_cache_valid?).to be(false) 269 | end 270 | 271 | it "is true if the cache path exists and is a directory" do 272 | allow(subject).to receive(:local_cache_path).with(no_args).and_return('path') 273 | allow(subject).to receive(:local_cache_exists?).with(no_args).and_return(true) 274 | 275 | allow(File).to receive(:directory?).with('path').and_return(true) 276 | 277 | expect(subject.local_cache_valid?).to be(true) 278 | end 279 | end 280 | 281 | describe "#command" do 282 | let(:source) { double(:source) } 283 | 284 | before do 285 | allow(subject).to receive(:local_cache_path).with(no_args).and_return('path') 286 | allow(subject).to receive(:revision).with(no_args).and_return('revision') 287 | allow(subject).to receive(:source).with(no_args).and_return(source) 288 | 289 | end 290 | 291 | it "handles the case when the local cache exists" do 292 | allow(source).to receive(:sync).with('revision', 'path').and_return('scm_command') 293 | 294 | allow(subject).to receive(:local_cache_valid?).with(no_args).and_return(true) 295 | 296 | expect(subject.command).to eq('scm_command') 297 | end 298 | 299 | it "handles the case where the local cache does not exist" do 300 | allow(source).to receive(:checkout).with('revision', 'path').and_return('scm_command') 301 | 302 | allow(subject).to receive(:local_cache_valid?).with(no_args).and_return(false) 303 | allow(subject).to receive(:local_cache_exists?).with(no_args).and_return(false) 304 | 305 | expect(subject.command).to eq('mkdir -p path && scm_command') 306 | end 307 | 308 | it "raises an exception when the local cache is invalid" do 309 | allow(subject).to receive(:local_cache_valid?).with(no_args).and_return(false) 310 | allow(subject).to receive(:local_cache_exists?).with(no_args).and_return(true) 311 | 312 | expect { subject.command }.to raise_error(Capistrano::Deploy::Strategy::RsyncWithRemoteCache::InvalidCacheError) 313 | end 314 | end 315 | 316 | describe "#mark_local_cache" do 317 | let(:local_cache_path) { Dir.tmpdir } 318 | let(:revision_path) { File.join(local_cache_path, 'REVISION') } 319 | 320 | before do 321 | allow(subject).to receive(:local_cache_path).with(no_args).and_return(local_cache_path) 322 | end 323 | 324 | it "creates a file with the current revision" do 325 | allow(subject).to receive(:revision).with(no_args).and_return('1') 326 | 327 | subject.mark_local_cache 328 | 329 | expect(File.read(revision_path)).to eq('1') 330 | end 331 | 332 | it "updates the revision file with the new revision" do 333 | File.open(revision_path, 'w') {|f| f << '1' } 334 | 335 | allow(subject).to receive(:revision).with(no_args).and_return('2') 336 | 337 | expect { subject.mark_local_cache }.to change { File.read(revision_path) }.from('1').to('2') 338 | end 339 | end 340 | 341 | describe "#update_local_cache" do 342 | it "marks the local cache after fetching the source" do 343 | allow(subject).to receive(:command).with(no_args).and_return('scm_command') 344 | expect(subject).to receive(:system).with('scm_command') 345 | expect(subject).to receive(:mark_local_cache).with(no_args) 346 | 347 | subject.update_local_cache 348 | end 349 | end 350 | 351 | describe "#sync_source_to" do 352 | let(:server) { double(:server) } 353 | 354 | before do 355 | allow(subject).to receive(:rsync_host).with(server).and_return('rsync_host') 356 | 357 | allow(subject).to receive_messages({ 358 | :default_exclusions => [], 359 | :rsync_options => 'rsync_options', 360 | :ssh_port => 'ssh_port', 361 | :local_cache_path => 'local_cache_path', 362 | :repository_cache_path => 'repository_cache_path' 363 | }) 364 | end 365 | 366 | it "runs the rsync command based on the options" do 367 | expected_command = "rsync rsync_options --rsh='ssh -p ssh_port' 'local_cache_path/' rsync_host:repository_cache_path/" 368 | expect(subject).to receive(:system).with(expected_command) 369 | 370 | subject.sync_source_to(server) 371 | end 372 | 373 | it "excludes any configured files" do 374 | allow(subject).to receive(:configuration).with(no_args).and_return({:copy_exclude => '.git'}) 375 | 376 | expected_command = "rsync rsync_options --exclude='.git' --rsh='ssh -p ssh_port' 'local_cache_path/' rsync_host:repository_cache_path/" 377 | expect(subject).to receive(:system).with(expected_command) 378 | 379 | subject.sync_source_to(server) 380 | end 381 | end 382 | 383 | describe "#update_remote_cache" do 384 | it "updates the cache on all applicable servers" do 385 | server_1, server_2 = [double(:server), double(:server)] 386 | 387 | allow(subject).to receive(:find_servers).with(:except => {:no_release => true}).and_return([server_1, server_2]) 388 | 389 | expect(subject).to receive(:sync_source_to).with(server_1).and_return('server_1_rsync_command') 390 | expect(subject).to receive(:sync_source_to).with(server_2).and_return('server_2_rsync_command') 391 | 392 | subject.update_remote_cache 393 | end 394 | end 395 | 396 | describe "#copy_remote_cache" do 397 | before do 398 | allow(subject).to receive_messages({ 399 | :default_exclusions => [], 400 | :repository_cache_path => 'repository_cache_path' 401 | }) 402 | end 403 | 404 | it "runs the appropriate rsync command" do 405 | allow(subject).to receive(:configuration).with(no_args).and_return(:release_path => 'release_path') 406 | 407 | expect(subject).to receive(:run).with("rsync -a --delete repository_cache_path/ release_path/") 408 | subject.copy_remote_cache 409 | end 410 | 411 | it "excludes any configured files" do 412 | allow(subject).to receive(:configuration).with(no_args).and_return({ 413 | :release_path => 'release_path', 414 | :copy_exclude => '.git' 415 | }) 416 | 417 | expect(subject).to receive(:run).with("rsync -a --delete repository_cache_path/ release_path/") 418 | subject.copy_remote_cache 419 | end 420 | end 421 | 422 | describe "#deploy!" do 423 | it "deploys the code" do 424 | expect(subject).to receive(:update_local_cache).with(no_args) 425 | expect(subject).to receive(:update_remote_cache).with(no_args) 426 | expect(subject).to receive(:copy_remote_cache).with(no_args) 427 | 428 | subject.deploy! 429 | end 430 | 431 | end 432 | 433 | end --------------------------------------------------------------------------------