require 'capistrano/recipes/deploy/scm/base' require 'rexml/xpath' require 'rexml/document' module Capistrano module Deploy module SCM # Accurev bridge for use by Capistrano. This implementation does not # implement all features of a Capistrano SCM module. The ones that are # left out are either exceedingly difficult to implement with Accurev # or are considered bad form. # # When using this module in a project, the following variables are used: # * :repository - This should match the depot that code lives in. If your code # exists in a subdirectory, you can append the path depot. # eg. foo-depot/bar_dir # * :stream - The stream in the depot that code should be pulled from. If # left blank, the depot stream will be used # * :revision - Should be in the form 'stream/transaction'. class Accurev < Base include REXML default_command 'accurev' # Defines pseudo-revision value for the most recent changes to be deployed. def head "#{stream}/highest" end # Given an Accurev revision identifier, this method returns an identifier that # can be used for later SCM calls. This returned identifier will not # change as a result of further SCM activity. def query_revision(revision) internal_revision = InternalRevision.parse(revision) return revision unless internal_revision.psuedo_revision? logger.debug("Querying for real revision for #{internal_revision}") rev_stream = internal_revision.stream logger.debug("Determining what type of stream #{rev_stream} is...") stream_xml = yield show_streams_for(rev_stream) stream_doc = Document.new(stream_xml) type = XPath.first(stream_doc, '//streams/stream/@type').value case type when 'snapshot' InternalRevision.new(rev_stream, 'highest').to_s else logger.debug("Getting latest transaction id in #{rev_stream}") # Doing another yield for a second Accurev call. Hopefully this is ok. hist_xml = yield scm(:hist, '-ftx', '-s', rev_stream, '-t', 'now.1') hist_doc = Document.new(hist_xml) transaction_id = XPath.first(hist_doc, '//AcResponse/transaction/@id').value InternalRevision.new(stream, transaction_id).to_s end end # Pops a copy of the code for the specified Accurev revision identifier. # The revision identifier is represented as a stream & transaction ID combo. # Accurev can only pop a particular transaction if a stream is created on the server # with a time basis of that transaction id. Therefore, we will create a stream with # the required criteria and pop that. def export(revision_id, destination) revision = InternalRevision.parse(revision_id) logger.debug("Exporting #{revision.stream}/#{revision.transaction_id} to #{destination}") commands = [ change_or_create_stream("#{revision.stream}-capistrano-deploy", revision), "mkdir -p #{destination}", scm_quiet(:pop, "-Rv #{stream}", "-L #{destination}", "'/./#{subdir}'") ] if subdir commands.push( "mv #{destination}/#{subdir}/* #{destination}", "rm -rf #{File.join(destination, subdir)}" ) end commands.join(' && ') end # Returns the command needed to show the changes that exist between the two revisions. def log(from, to=head) logger.info("Getting transactions between #{from} and #{to}") from_rev = InternalRevision.parse(from) to_rev = InternalRevision.parse(to) [ scm(:hist, '-s', from_rev.stream, '-t', "#{to_rev.transaction_id}-#{from_rev.transaction_id}"), "sed -e '/transaction #{from_rev.transaction_id}/ { Q }'" ].join(' | ') end # Returns the command needed to show the diff between what is deployed and what is # pending. Because Accurev can not do this task without creating some streams, # two time basis streams will be created for the purposes of doing the diff. def diff(from, to=head) from = InternalRevision.parse(from) to = InternalRevision.parse(to) from_stream = "#{from.stream}-capistrano-diff-from" to_stream = "#{to.stream}-capistrano-diff-to" [ change_or_create_stream(from_stream, from), change_or_create_stream(to_stream, to), scm(:diff, '-v', from_stream, '-V', to_stream, '-a') ].join(' && ') end private def depot repository.split('/')[0] end def stream variable(:stream) || depot end def subdir repository.split('/')[1..-1].join('/') unless repository.index('/').nil? end def change_or_create_stream(name, revision) [ scm_quiet(:mkstream, '-b', revision.stream, '-s', name, '-t', revision.transaction_id), scm_quiet(:chstream, '-b', revision.stream, '-s', name, '-t', revision.transaction_id) ].join('; ') end def show_streams_for(stream) scm :show, '-fx', '-s', stream, :streams end def scm_quiet(*args) scm(*args) + (variable(:scm_verbose) ? '' : '&> /dev/null') end class InternalRevision attr_reader :stream, :transaction_id def self.parse(string) match = /([^\/]+)(\/(.+)){0,1}/.match(string) raise "Unrecognized revision identifier: #{string}" unless match stream = match[1] transaction_id = match[3] || 'highest' InternalRevision.new(stream, transaction_id) end def initialize(stream, transaction_id) @stream = stream @transaction_id = transaction_id end def psuedo_revision? @transaction_id == 'highest' end def to_s "#{stream}/#{transaction_id}" end def ==(other) (stream == other.stream) && (transaction_id == other.transaction_id) end end end end end end