diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b7efda0..97f34930 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Reverse Chronological Order: ## master * Add a command line option to control role filter (`--roles`) (@andytinycat) + * Use an SCM object with a pluggable strategy ## `3.1.0` (not released) diff --git a/lib/capistrano/git.rb b/lib/capistrano/git.rb index 8d17e6c4..1a3cdb85 100644 --- a/lib/capistrano/git.rb +++ b/lib/capistrano/git.rb @@ -1 +1,36 @@ load File.expand_path("../tasks/git.rake", __FILE__) + +require 'capistrano/scm' + +class Capistrano::Git < Capistrano::SCM + + # execute git with argument in the context + # + def git(*args) + args.unshift :git + context.execute *args + end + + # The Capistrano default strategy for git. You should want to use this. + module DefaultStrategy + def test + test! " [ -f #{repo_path}/HEAD ] " + end + + def check + test! :git, :'ls-remote', repo_url + end + + def clone + git :clone, '--mirror', repo_url, repo_path + end + + def update + git :remote, :update + end + + def release + git :archive, fetch(:branch), '| tar -x -C', release_path + end + end +end diff --git a/lib/capistrano/hg.rb b/lib/capistrano/hg.rb index 7747cf1b..8ed9fc33 100644 --- a/lib/capistrano/hg.rb +++ b/lib/capistrano/hg.rb @@ -1 +1,33 @@ load File.expand_path("../tasks/hg.rake", __FILE__) + +require 'capistrano/scm' + +class Capistrano::Hg < Capistrano::SCM + # execute hg in context with arguments + def hg(*args) + args.unshift(:hg) + context.execute *args + end + + module DefaultStrategy + def test + test! " [ -d #{repo_path}/.hg ] " + end + + def check + hg "id", repo_url + end + + def clone + hg "clone", "--noupdate", repo_url, repo_path + end + + def update + hg "pull" + end + + def release + hg "archive", release_path, "--rev", fetch(:branch) + end + end +end diff --git a/lib/capistrano/scm.rb b/lib/capistrano/scm.rb new file mode 100644 index 00000000..f0728647 --- /dev/null +++ b/lib/capistrano/scm.rb @@ -0,0 +1,116 @@ +module Capistrano + + # Base class for SCM strategy providers. + # + # @abstract + # + # @attr_reader [Rake] context + # + # @author Hartog de Mik + # + class SCM + attr_reader :context + + # Provide a wrapper for the SCM that loads a strategy for the user. + # + # @param [Rake] context The context in which the strategy should run + # @param [Module] strategy A module to include into the SCM instance. The + # module should provide the abstract methods of Capistrano::SCM + # + def initialize(context, strategy) + @context = context + singleton = class << self; self; end + singleton.send(:include, strategy) + end + + # Call test in context + def test!(*args) + context.test *args + end + + # The repository URL accoriding to the context + def repo_url + context.repo_url + end + + # The repository path accoriding to the context + def repo_path + context.repo_path + end + + # The release path accoriding to the context + def release_path + context.release_path + end + + # Fetch a var from the context + # @param [Symbol] variable The variable to fetch + # @param [Object] default The default value if not found + # + def fetch(*args) + context.fetch(*args) + end + + # @abstract + # + # Your implementation should check the existance of a cache repository on + # the deployment target + # + # @return [Boolean] + # + def test + raise NotImplementedError.new( + "Your SCM strategy module should provide a #test method" + ) + end + + # @abstract + # + # Your implementation should check if the specified remote-repository is + # available. + # + # @return [Boolean] + # + def check + raise NotImplementedError.new( + "Your SCM strategy module should provide a #check method" + ) + end + + # @abstract + # + # Create a (new) clone of the remote-repository on the deployment target + # + # @return void + # + def clone + raise NotImplementedError.new( + "Your SCM strategy module should provide a #clone method" + ) + end + + # @abstract + # + # Update the clone on the deployment target + # + # @return void + # + def update + raise NotImplementedError.new( + "Your SCM strategy module should provide a #update method" + ) + end + + # @abstract + # + # Copy the contents of the cache-repository onto the release path + # + # @return void + # + def release + raise NotImplementedError.new( + "Your SCM strategy module should provide a #release method" + ) + end + end +end diff --git a/lib/capistrano/tasks/git.rake b/lib/capistrano/tasks/git.rake index 978d8794..684556a5 100644 --- a/lib/capistrano/tasks/git.rake +++ b/lib/capistrano/tasks/git.rake @@ -1,5 +1,9 @@ namespace :git do + def strategy + @strategy ||= Capistrano::Git.new(self, fetch(:git_strategy, Capistrano::Git::DefaultStrategy)) + end + set :git_environmental_variables, ->() { { git_askpass: "/bin/echo", @@ -21,7 +25,7 @@ namespace :git do fetch(:branch) on release_roles :all do with fetch(:git_environmental_variables) do - exit 1 unless test :git, :'ls-remote', repo_url + exit 1 unless strategy.check end end end @@ -29,12 +33,12 @@ namespace :git do desc 'Clone the repo to the cache' task clone: :'git:wrapper' do on release_roles :all do - if test " [ -f #{repo_path}/HEAD ] " + if strategy.test info t(:mirror_exists, at: repo_path) else within deploy_path do with fetch(:git_environmental_variables) do - execute :git, :clone, '--mirror', repo_url, repo_path + strategy.clone end end end @@ -46,7 +50,7 @@ namespace :git do on release_roles :all do within repo_path do capturing_revisions do - execute :git, :remote, :update + strategy.update end end end @@ -58,7 +62,7 @@ namespace :git do with fetch(:git_environmental_variables) do within repo_path do execute :mkdir, '-p', release_path - execute :git, :archive, fetch(:branch), '| tar -x -C', release_path + strategy.release end end end diff --git a/lib/capistrano/tasks/hg.rake b/lib/capistrano/tasks/hg.rake index 138a75c7..2021cce4 100644 --- a/lib/capistrano/tasks/hg.rake +++ b/lib/capistrano/tasks/hg.rake @@ -1,19 +1,23 @@ namespace :hg do + def strategy + @strategy ||= Capistrano::Hg.new(self, fetch(:hg_strategy, Capistrano::Hg::DefaultStrategy)) + end + desc 'Check that the repo is reachable' task :check do on release_roles :all do - execute "hg", "id", repo_url + strategy.check end end desc 'Clone the repo to the cache' task :clone do on release_roles :all do - if test " [ -d #{repo_path}/.hg ] " + if strategy.test info t(:mirror_exists, at: repo_path) else within deploy_path do - execute "hg", "clone", "--noupdate", repo_url, repo_path + strategy.clone end end end @@ -23,7 +27,7 @@ namespace :hg do task :update => :'hg:clone' do on release_roles :all do within repo_path do - execute "hg", "pull" + strategy.update end end end @@ -32,7 +36,7 @@ namespace :hg do task :create_release => :'hg:update' do on release_roles :all do within repo_path do - execute "hg", "archive", release_path, "--rev", fetch(:branch) + strategy.release end end end diff --git a/spec/lib/capistrano/git_spec.rb b/spec/lib/capistrano/git_spec.rb new file mode 100644 index 00000000..1ad9b502 --- /dev/null +++ b/spec/lib/capistrano/git_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' + +require 'capistrano/git' + +module Capistrano + describe Git do + let(:context) { Class.new.new } + subject { Capistrano::Git.new(context, Capistrano::Git::DefaultStrategy) } + + describe "#git" do + it "should call execute git in the context, with arguments" do + context.expects(:execute).with(:git, :init) + subject.git(:init) + end + end + end + + describe Git::DefaultStrategy do + let(:context) { Class.new.new } + subject { Capistrano::Git.new(context, Capistrano::Git::DefaultStrategy) } + + describe "#test" do + it "should call test for repo HEAD" do + context.expects(:repo_path).returns("/path/to/repo") + context.expects(:test).with " [ -f /path/to/repo/HEAD ] " + + subject.test + end + end + + describe "#check" do + it "should test the repo url" do + context.expects(:repo_url).returns(:url) + context.expects(:test).with(:git, :'ls-remote', :url).returns(true) + + subject.check + end + end + + describe "#clone" do + it "should run git clone" do + context.expects(:repo_url).returns(:url) + context.expects(:repo_path).returns(:path) + + context.expects(:execute).with(:git, :clone, '--mirror', :url, :path) + + subject.clone + end + end + + describe "#update" do + it "should run git update" do + context.expects(:execute).with(:git, :remote, :update) + + subject.update + end + end + + describe "#release" do + it "should run git archive" do + context.expects(:fetch).returns(:branch) + context.expects(:release_path).returns(:path) + + context.expects(:execute).with(:git, :archive, :branch, '| tar -x -C', :path) + + subject.release + end + end + end +end diff --git a/spec/lib/capistrano/hg_spec.rb b/spec/lib/capistrano/hg_spec.rb new file mode 100644 index 00000000..7519cffc --- /dev/null +++ b/spec/lib/capistrano/hg_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' + +require 'capistrano/hg' + +module Capistrano + describe Hg do + let(:context) { Class.new.new } + subject { Capistrano::Hg.new(context, Capistrano::Hg::DefaultStrategy) } + + describe "#hg" do + it "should call execute hg in the context, with arguments" do + context.expects(:execute).with(:hg, :init) + subject.hg(:init) + end + end + end + + describe Hg::DefaultStrategy do + let(:context) { Class.new.new } + subject { Capistrano::Hg.new(context, Capistrano::Hg::DefaultStrategy) } + + describe "#test" do + it "should call test for repo HEAD" do + context.expects(:repo_path).returns("/path/to/repo") + context.expects(:test).with " [ -d /path/to/repo/.hg ] " + + subject.test + end + end + + describe "#check" do + it "should test the repo url" do + context.expects(:repo_url).returns(:url) + context.expects(:execute).with(:hg, "id", :url) + + subject.check + end + end + + describe "#clone" do + it "should run hg clone" do + context.expects(:repo_url).returns(:url) + context.expects(:repo_path).returns(:path) + + context.expects(:execute).with(:hg, "clone", '--noupdate', :url, :path) + + subject.clone + end + end + + describe "#update" do + it "should run hg update" do + context.expects(:execute).with(:hg, "pull") + + subject.update + end + end + + describe "#release" do + it "should run hg archive" do + context.expects(:fetch).returns(:branch) + context.expects(:release_path).returns(:path) + + context.expects(:execute).with(:hg, "archive", :path, "--rev", :branch) + + subject.release + end + end + end +end diff --git a/spec/lib/capistrano/scm_spec.rb b/spec/lib/capistrano/scm_spec.rb new file mode 100644 index 00000000..3e736bed --- /dev/null +++ b/spec/lib/capistrano/scm_spec.rb @@ -0,0 +1,104 @@ +require 'spec_helper' + +require 'capistrano/scm' + +module RaiseNotImplementedMacro + def raise_not_implemented_on(method) + it "should raise NotImplemented on #{method}" do + expect { + subject.send(method) + }.to raise_error(NotImplementedError) + end + end +end + +RSpec.configure do + include RaiseNotImplementedMacro +end + +module DummyStrategy + def test + test!("you dummy!") + end +end + +module BlindStrategy; end + +module Capistrano + describe SCM do + let(:context) { Class.new.new } + + describe "#initialize" do + subject { Capistrano::SCM.new(context, DummyStrategy) } + + it "should load the provided strategy" do + context.expects(:test).with("you dummy!") + subject.test + end + end + + describe "Convenience methods" do + subject { Capistrano::SCM.new(context, BlindStrategy) } + + describe "#test!" do + it "should return call test on the context" do + context.expects(:test).with(:x) + subject.test!(:x) + end + end + + describe "#repo_url" do + it "should return the repo url according to the context" do + context.expects(:repo_url).returns(:url) + subject.repo_url.should == :url + end + end + + describe "#repo_path" do + it "should return the repo path according to the context" do + context.expects(:repo_path).returns(:path) + subject.repo_path.should == :path + end + end + + describe "#release_path" do + it "should return the release path according to the context" do + context.expects(:release_path).returns('/path/to/nowhere') + subject.release_path.should == '/path/to/nowhere' + end + end + + describe "#fetch" do + it "should call fetch on the context" do + context.expects(:fetch) + subject.fetch(:branch) + end + end + end + + describe "With a 'blind' strategy" do + subject { Capistrano::SCM.new(context, BlindStrategy) } + + describe "#test" do + raise_not_implemented_on(:test) + end + + describe "#check" do + raise_not_implemented_on(:check) + end + + describe "#clone" do + raise_not_implemented_on(:clone) + end + + describe "#update" do + raise_not_implemented_on(:update) + end + + describe "#release" do + raise_not_implemented_on(:release) + end + end + end +end +