From e7a95e5872f00475fe87533edd510b3652e39c9d Mon Sep 17 00:00:00 2001 From: "Hartog C. de Mik" Date: Tue, 10 Dec 2013 07:58:35 +0100 Subject: [PATCH] Use an SCM object with a pluggable strategy I've taken the responsibility for executing scm commands away from the rake tasks and have placed them in an SCM object. This object can be loaded with any given strategy. A default strategy is supplied (the commands as they where in the rake tasks) Moved Hg to the new strategy pattern --- CHANGELOG.md | 1 + lib/capistrano/git.rb | 35 ++++++++++ lib/capistrano/hg.rb | 32 +++++++++ lib/capistrano/scm.rb | 116 ++++++++++++++++++++++++++++++++ lib/capistrano/tasks/git.rake | 14 ++-- lib/capistrano/tasks/hg.rake | 14 ++-- spec/lib/capistrano/git_spec.rb | 70 +++++++++++++++++++ spec/lib/capistrano/hg_spec.rb | 70 +++++++++++++++++++ spec/lib/capistrano/scm_spec.rb | 104 ++++++++++++++++++++++++++++ 9 files changed, 446 insertions(+), 10 deletions(-) create mode 100644 lib/capistrano/scm.rb create mode 100644 spec/lib/capistrano/git_spec.rb create mode 100644 spec/lib/capistrano/hg_spec.rb create mode 100644 spec/lib/capistrano/scm_spec.rb 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 +