aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--switchtower/CHANGELOG2
-rw-r--r--switchtower/lib/switchtower/scm/cvs.rb82
-rw-r--r--switchtower/test/scm/cvs_test.rb159
3 files changed, 243 insertions, 0 deletions
diff --git a/switchtower/CHANGELOG b/switchtower/CHANGELOG
index f40d897780..7b3a24a42d 100644
--- a/switchtower/CHANGELOG
+++ b/switchtower/CHANGELOG
@@ -1,5 +1,7 @@
*SVN*
+* Added CVS module (very very experimental!)
+
* Works with public keys now, for passwordless deployment
* Subversion module recognizes the password prompt for HTTP authentication
diff --git a/switchtower/lib/switchtower/scm/cvs.rb b/switchtower/lib/switchtower/scm/cvs.rb
new file mode 100644
index 0000000000..a4c588e611
--- /dev/null
+++ b/switchtower/lib/switchtower/scm/cvs.rb
@@ -0,0 +1,82 @@
+require 'time'
+
+module SwitchTower
+ module SCM
+
+ # An SCM module for using CVS as your source control tool. You can
+ # specify it by placing the following line in your configuration:
+ #
+ # set :scm, :cvs
+ #
+ # Also, this module accepts a <tt>:cvs</tt> configuration variable,
+ # which (if specified) will be used as the full path to the cvs
+ # executable on the remote machine:
+ #
+ # set :cvs, "/opt/local/bin/cvs"
+ #
+ # You can specify the location of your local copy (used to query
+ # the revisions, etc.) via the <tt>:local</tt> variable, which defaults to
+ # ".".
+ #
+ # Also, you can specify the CVS_RSH variable to use on the remote machine(s)
+ # via the <tt>:cvs_rsh</tt> variable. This defaults to the value of the
+ # CVS_RSH environment variable locally, or if it is not set, to "ssh".
+ class Cvs
+ attr_reader :configuration
+
+ def initialize(configuration) #:nodoc:
+ @configuration = configuration
+ end
+
+ # Return a string representing the date of the last revision (CVS is
+ # seriously retarded, in that it does not give you a way to query when
+ # the last revision was made to the repository, so this is a fairly
+ # expensive operation...)
+ def latest_revision
+ return @latest_revision if @latest_revision
+ configuration.logger.debug "querying latest revision..."
+ @latest_revision = cvs_log(configuration.local).
+ split(/\r?\n/).
+ grep(/^date: (.*?);/) { Time.parse($1).strftime("%FT%T") }.
+ sort.
+ last
+ end
+
+ # Check out (on all servers associated with the current task) the latest
+ # revision. Uses the given actor instance to execute the command.
+ def checkout(actor)
+ cvs = configuration[:cvs] || "cvs"
+ cvs_rsh = configuration[:cvs_rsh] || ENV['CVS_RSH'] || "ssh"
+
+ command = <<-CMD
+ if [[ -d #{actor.release_path} ]]; then
+ cd #{actor.release_path};
+ CVS_RSH="#{cvs_rsh}" #{cvs} -q up -d#{latest_revision};
+ else
+ cd #{configuration.releases_path};
+ CVS_RSH="#{cvs_rsh}" #{cvs} -d #{configuration.repository} -q co -D #{latest_revision} -d #{latest_revision} #{actor.application};
+ fi
+ CMD
+ actor.run(command) do |ch, stream, out|
+ prefix = "#{stream} :: #{ch[:host]}"
+ actor.logger.info out, prefix
+ if out =~ %r{password:}
+ actor.logger.info "CVS is asking for a password", prefix
+ ch.send_data "#{actor.password}\n"
+ elsif out =~ %r{^Enter passphrase}
+ message = "CVS needs your key's passphrase and cannot proceed"
+ actor.logger.info message, prefix
+ raise message
+ end
+ end
+ end
+
+ private
+
+ def cvs_log(path)
+ `cd #{path || "."} && cvs -q log -N -rHEAD`
+ end
+ end
+
+ end
+end
diff --git a/switchtower/test/scm/cvs_test.rb b/switchtower/test/scm/cvs_test.rb
new file mode 100644
index 0000000000..3df64cce0e
--- /dev/null
+++ b/switchtower/test/scm/cvs_test.rb
@@ -0,0 +1,159 @@
+$:.unshift File.dirname(__FILE__) + "/../../lib"
+
+require File.dirname(__FILE__) + "/../utils"
+require 'test/unit'
+require 'switchtower/scm/cvs'
+
+class ScmCvsTest < Test::Unit::TestCase
+ class CvsTest < SwitchTower::SCM::Cvs
+ attr_accessor :story
+ attr_reader :last_path
+
+ def cvs_log(path)
+ @last_path = path
+ story.shift
+ end
+ end
+
+ class MockChannel
+ attr_reader :sent_data
+
+ def send_data(data)
+ @sent_data ||= []
+ @sent_data << data
+ end
+
+ def [](name)
+ "value"
+ end
+ end
+
+ class MockActor
+ attr_reader :command
+ attr_reader :channels
+ attr_accessor :story
+
+ def initialize(config)
+ @config = config
+ end
+
+ def run(command)
+ @command = command
+ @channels ||= []
+ @channels << MockChannel.new
+ story.each { |stream, line| yield @channels.last, stream, line }
+ end
+
+ def method_missing(sym, *args)
+ @config.send(sym, *args)
+ end
+ end
+
+ def setup
+ @config = MockConfiguration.new
+ @config[:repository] = ":ext:joetester@rubyforge.org:/hello/world"
+ @config[:local] = "/hello/world"
+ @config[:cvs] = "/path/to/cvs"
+ @config[:password] = "chocolatebrownies"
+ @scm = CvsTest.new(@config)
+ @actor = MockActor.new(@config)
+ @log_msg = <<MSG.strip
+RCS file: /var/cvs/copland/copland/LICENSE,v
+Working file: LICENSE
+head: 1.1
+branch:
+locks: strict
+access list:
+keyword substitution: kv
+total revisions: 1; selected revisions: 1
+description:
+----------------------------
+revision 1.1
+date: 2004/08/29 04:23:36; author: minam; state: Exp;
+New implementation.
+=============================================================================
+
+RCS file: /var/cvs/copland/copland/Rakefile,v
+Working file: Rakefile
+head: 1.7
+branch:
+locks: strict
+access list:
+keyword substitution: kv
+total revisions: 7; selected revisions: 1
+description:
+----------------------------
+revision 1.7
+date: 2004/09/15 16:35:01; author: minam; state: Exp; lines: +2 -1
+Rakefile now publishes package documentation from doc/packages instead of
+doc/packrat. Updated "latest updates" in manual.
+=============================================================================
+
+RCS file: /var/cvs/copland/copland/TODO,v
+Working file: TODO
+head: 1.18
+branch:
+locks: strict
+access list:
+keyword substitution: kv
+total revisions: 18; selected revisions: 1
+description:
+----------------------------
+revision 1.18
+date: 2004/10/12 02:21:02; author: minam; state: Exp; lines: +4 -1
+Added RubyConf 2004 presentation.
+=============================================================================
+
+RCS file: /var/cvs/copland/copland/Attic/build-gemspec.rb,v
+Working file: build-gemspec.rb
+head: 1.5
+branch:
+locks: strict
+access list:
+keyword substitution: kv
+total revisions: 5; selected revisions: 1
+description:
+----------------------------
+revision 1.5
+date: 2004/08/29 04:10:17; author: minam; state: dead; lines: +0 -0
+Here we go -- point of no return. Deleting existing implementation to make
+way for new implementation.
+=============================================================================
+
+RCS file: /var/cvs/copland/copland/copland.gemspec,v
+Working file: copland.gemspec
+head: 1.12
+branch:
+locks: strict
+access list:
+keyword substitution: kv
+total revisions: 13; selected revisions: 1
+description:
+----------------------------
+revision 1.12
+date: 2004/09/11 21:45:58; author: minam; state: Exp; lines: +4 -4
+Minor change in how version is communicated to gemspec.
+=============================================================================
+MSG
+ @scm.story = [ @log_msg ]
+ end
+
+ def test_latest_revision
+ @scm.story = [ @log_msg ]
+ assert_equal "2004-10-12T02:21:02", @scm.latest_revision
+ assert_equal "/hello/world", @scm.last_path
+ end
+
+ def test_checkout
+ @actor.story = []
+ assert_nothing_raised { @scm.checkout(@actor) }
+ assert_nil @actor.channels.last.sent_data
+ assert_match %r{/path/to/cvs}, @actor.command
+ end
+
+ def test_checkout_needs_ssh_password
+ @actor.story = [[:out, "joetester@rubyforge.org's password: "]]
+ assert_nothing_raised { @scm.checkout(@actor) }
+ assert_equal ["chocolatebrownies\n"], @actor.channels.last.sent_data
+ end
+end