| | 1 | require 'capistrano/recipes/deploy/scm/base' |
|---|
| | 2 | require 'rexml/xpath' |
|---|
| | 3 | require 'rexml/document' |
|---|
| | 4 | |
|---|
| | 5 | module Capistrano |
|---|
| | 6 | module Deploy |
|---|
| | 7 | module SCM |
|---|
| | 8 | # Accurev bridge for use by Capistrano. This implementation does not |
|---|
| | 9 | # implement all features of a Capistrano SCM module. The ones that are |
|---|
| | 10 | # left out are either exceedingly difficult to implement with Accurev |
|---|
| | 11 | # or are considered bad form. |
|---|
| | 12 | # |
|---|
| | 13 | # When using this module in a project, the following variables are used: |
|---|
| | 14 | # * :repository - This should match the depot that code lives in. If your code |
|---|
| | 15 | # exists in a subdirectory, you can append the path depot. |
|---|
| | 16 | # eg. foo-depot/bar_dir |
|---|
| | 17 | # * :stream - The stream in the depot that code should be pulled from. If |
|---|
| | 18 | # left blank, the depot stream will be used |
|---|
| | 19 | # * :revision - Should be in the form 'stream/transaction'. |
|---|
| | 20 | class Accurev < Base |
|---|
| | 21 | include REXML |
|---|
| | 22 | default_command 'accurev' |
|---|
| | 23 | |
|---|
| | 24 | # Defines pseudo-revision value for the most recent changes to be deployed. |
|---|
| | 25 | def head |
|---|
| | 26 | "#{stream}/highest" |
|---|
| | 27 | end |
|---|
| | 28 | |
|---|
| | 29 | # Given an Accurev revision identifier, this method returns an identifier that |
|---|
| | 30 | # can be used for later SCM calls. This returned identifier will not |
|---|
| | 31 | # change as a result of further SCM activity. |
|---|
| | 32 | def query_revision(revision) |
|---|
| | 33 | internal_revision = InternalRevision.parse(revision) |
|---|
| | 34 | return revision unless internal_revision.psuedo_revision? |
|---|
| | 35 | |
|---|
| | 36 | logger.debug("Querying for real revision for #{internal_revision}") |
|---|
| | 37 | rev_stream = internal_revision.stream |
|---|
| | 38 | |
|---|
| | 39 | logger.debug("Determining what type of stream #{rev_stream} is...") |
|---|
| | 40 | stream_xml = yield show_streams_for(rev_stream) |
|---|
| | 41 | stream_doc = Document.new(stream_xml) |
|---|
| | 42 | type = XPath.first(stream_doc, '//streams/stream/@type').value |
|---|
| | 43 | |
|---|
| | 44 | case type |
|---|
| | 45 | when 'snapshot' |
|---|
| | 46 | InternalRevision.new(rev_stream, 'highest').to_s |
|---|
| | 47 | else |
|---|
| | 48 | logger.debug("Getting latest transaction id in #{rev_stream}") |
|---|
| | 49 | # Doing another yield for a second Accurev call. Hopefully this is ok. |
|---|
| | 50 | hist_xml = yield scm(:hist, '-ftx', '-s', rev_stream, '-t', 'now.1') |
|---|
| | 51 | hist_doc = Document.new(hist_xml) |
|---|
| | 52 | transaction_id = XPath.first(hist_doc, '//AcResponse/transaction/@id').value |
|---|
| | 53 | InternalRevision.new(stream, transaction_id).to_s |
|---|
| | 54 | end |
|---|
| | 55 | end |
|---|
| | 56 | |
|---|
| | 57 | # Pops a copy of the code for the specified Accurev revision identifier. |
|---|
| | 58 | # The revision identifier is represented as a stream & transaction ID combo. |
|---|
| | 59 | # Accurev can only pop a particular transaction if a stream is created on the server |
|---|
| | 60 | # with a time basis of that transaction id. Therefore, we will create a stream with |
|---|
| | 61 | # the required criteria and pop that. |
|---|
| | 62 | def export(revision_id, destination) |
|---|
| | 63 | revision = InternalRevision.parse(revision_id) |
|---|
| | 64 | logger.debug("Exporting #{revision.stream}/#{revision.transaction_id} to #{destination}") |
|---|
| | 65 | |
|---|
| | 66 | commands = [ |
|---|
| | 67 | change_or_create_stream("#{revision.stream}-capistrano-deploy", revision), |
|---|
| | 68 | "mkdir -p #{destination}", |
|---|
| | 69 | scm_quiet(:pop, "-Rv #{stream}", "-L #{destination}", "'/./#{subdir}'") |
|---|
| | 70 | ] |
|---|
| | 71 | if subdir |
|---|
| | 72 | commands.push( |
|---|
| | 73 | "mv #{destination}/#{subdir}/* #{destination}", |
|---|
| | 74 | "rm -rf #{File.join(destination, subdir)}" |
|---|
| | 75 | ) |
|---|
| | 76 | end |
|---|
| | 77 | commands.join(' && ') |
|---|
| | 78 | end |
|---|
| | 79 | |
|---|
| | 80 | # Returns the command needed to show the changes that exist between the two revisions. |
|---|
| | 81 | def log(from, to=head) |
|---|
| | 82 | logger.info("Getting transactions between #{from} and #{to}") |
|---|
| | 83 | from_rev = InternalRevision.parse(from) |
|---|
| | 84 | to_rev = InternalRevision.parse(to) |
|---|
| | 85 | |
|---|
| | 86 | [ |
|---|
| | 87 | scm(:hist, '-s', from_rev.stream, '-t', "#{to_rev.transaction_id}-#{from_rev.transaction_id}"), |
|---|
| | 88 | "sed -e '/transaction #{from_rev.transaction_id}/ { Q }'" |
|---|
| | 89 | ].join(' | ') |
|---|
| | 90 | end |
|---|
| | 91 | |
|---|
| | 92 | # Returns the command needed to show the diff between what is deployed and what is |
|---|
| | 93 | # pending. Because Accurev can not do this task without creating some streams, |
|---|
| | 94 | # two time basis streams will be created for the purposes of doing the diff. |
|---|
| | 95 | def diff(from, to=head) |
|---|
| | 96 | from = InternalRevision.parse(from) |
|---|
| | 97 | to = InternalRevision.parse(to) |
|---|
| | 98 | |
|---|
| | 99 | from_stream = "#{from.stream}-capistrano-diff-from" |
|---|
| | 100 | to_stream = "#{to.stream}-capistrano-diff-to" |
|---|
| | 101 | |
|---|
| | 102 | [ |
|---|
| | 103 | change_or_create_stream(from_stream, from), |
|---|
| | 104 | change_or_create_stream(to_stream, to), |
|---|
| | 105 | scm(:diff, '-v', from_stream, '-V', to_stream, '-a') |
|---|
| | 106 | ].join(' && ') |
|---|
| | 107 | end |
|---|
| | 108 | |
|---|
| | 109 | private |
|---|
| | 110 | def depot |
|---|
| | 111 | repository.split('/')[0] |
|---|
| | 112 | end |
|---|
| | 113 | |
|---|
| | 114 | def stream |
|---|
| | 115 | variable(:stream) || depot |
|---|
| | 116 | end |
|---|
| | 117 | |
|---|
| | 118 | def subdir |
|---|
| | 119 | repository.split('/')[1..-1].join('/') unless repository.index('/').nil? |
|---|
| | 120 | end |
|---|
| | 121 | |
|---|
| | 122 | def change_or_create_stream(name, revision) |
|---|
| | 123 | [ |
|---|
| | 124 | scm_quiet(:mkstream, '-b', revision.stream, '-s', name, '-t', revision.transaction_id), |
|---|
| | 125 | scm_quiet(:chstream, '-b', revision.stream, '-s', name, '-t', revision.transaction_id) |
|---|
| | 126 | ].join('; ') |
|---|
| | 127 | end |
|---|
| | 128 | |
|---|
| | 129 | def show_streams_for(stream) |
|---|
| | 130 | scm :show, '-fx', '-s', stream, :streams |
|---|
| | 131 | end |
|---|
| | 132 | |
|---|
| | 133 | def scm_quiet(*args) |
|---|
| | 134 | scm(*args) + (variable(:scm_verbose) ? '' : '&> /dev/null') |
|---|
| | 135 | end |
|---|
| | 136 | |
|---|
| | 137 | class InternalRevision |
|---|
| | 138 | attr_reader :stream, :transaction_id |
|---|
| | 139 | |
|---|
| | 140 | def self.parse(string) |
|---|
| | 141 | match = /([^\/]+)(\/(.+)){0,1}/.match(string) |
|---|
| | 142 | raise "Unrecognized revision identifier: #{string}" unless match |
|---|
| | 143 | |
|---|
| | 144 | stream = match[1] |
|---|
| | 145 | transaction_id = match[3] || 'highest' |
|---|
| | 146 | InternalRevision.new(stream, transaction_id) |
|---|
| | 147 | end |
|---|
| | 148 | |
|---|
| | 149 | def initialize(stream, transaction_id) |
|---|
| | 150 | @stream = stream |
|---|
| | 151 | @transaction_id = transaction_id |
|---|
| | 152 | end |
|---|
| | 153 | |
|---|
| | 154 | def psuedo_revision? |
|---|
| | 155 | @transaction_id == 'highest' |
|---|
| | 156 | end |
|---|
| | 157 | |
|---|
| | 158 | def to_s |
|---|
| | 159 | "#{stream}/#{transaction_id}" |
|---|
| | 160 | end |
|---|
| | 161 | |
|---|
| | 162 | def ==(other) |
|---|
| | 163 | (stream == other.stream) && (transaction_id == other.transaction_id) |
|---|
| | 164 | end |
|---|
| | 165 | end |
|---|
| | 166 | end |
|---|
| | 167 | end |
|---|
| | 168 | end |
|---|
| | 169 | end |