capistrano 2.0.0 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +54 -0
- data/bin/capify +1 -0
- data/lib/capistrano/callback.rb +4 -0
- data/lib/capistrano/cli/options.rb +2 -2
- data/lib/capistrano/command.rb +26 -11
- data/lib/capistrano/configuration/actions/invocation.rb +11 -4
- data/lib/capistrano/configuration/loading.rb +91 -5
- data/lib/capistrano/configuration/namespaces.rb +6 -0
- data/lib/capistrano/recipes/deploy.rb +22 -11
- data/lib/capistrano/recipes/deploy/remote_dependency.rb +31 -0
- data/lib/capistrano/recipes/deploy/scm/accurev.rb +169 -0
- data/lib/capistrano/recipes/deploy/scm/base.rb +12 -0
- data/lib/capistrano/recipes/deploy/scm/git.rb +191 -0
- data/lib/capistrano/recipes/deploy/scm/subversion.rb +20 -9
- data/lib/capistrano/recipes/deploy/strategy/copy.rb +2 -1
- data/lib/capistrano/recipes/upgrade.rb +1 -1
- data/lib/capistrano/shell.rb +5 -17
- data/lib/capistrano/upload.rb +1 -1
- data/lib/capistrano/version.rb +1 -1
- data/test/command_test.rb +36 -29
- data/test/configuration/actions/invocation_test.rb +15 -8
- data/test/configuration/connections_test.rb +3 -3
- data/test/configuration/loading_test.rb +8 -0
- data/test/configuration/namespace_dsl_test.rb +14 -0
- data/test/deploy/scm/accurev_test.rb +23 -0
- data/test/deploy/scm/git_test.rb +112 -0
- data/test/deploy/strategy/copy_test.rb +7 -6
- data/test/upload_test.rb +4 -4
- metadata +6 -2
data/CHANGELOG
CHANGED
@@ -1,3 +1,57 @@
|
|
1
|
+
*2.1.0* October 14, 2007
|
2
|
+
|
3
|
+
* Default to 0664 instead of 0660 on upload [Jamis Buck]
|
4
|
+
|
5
|
+
* Fix deploy:pending to query SCM for the subsequent revision so that it does not include the last deployed change [Jamis Buck]
|
6
|
+
|
7
|
+
* Prefer 'Last Changed Rev' over 'Revision' when querying latest revision via Subversion [Jamis Buck]
|
8
|
+
|
9
|
+
* Explicitly require 'stringio' in copy_test [mislav]
|
10
|
+
|
11
|
+
* When Subversion#query_revision fails, give a more sane error [Jamis Buck]
|
12
|
+
|
13
|
+
* Don't run the upgrade:revisions task on non-release servers [Jamis Buck]
|
14
|
+
|
15
|
+
* Fix cap shell to properly recognize sudo prompt [Mark Imbriaco, barnaby, Jamis Buck]
|
16
|
+
|
17
|
+
* Git SCM module [Garry Dolley, Geoffrey Grosenbach, Scott Chacon]
|
18
|
+
|
19
|
+
* Use the --password switch for subversion by default, but add :scm_prefer_prompt variable (defaults to false) [Jamis Buck]
|
20
|
+
|
21
|
+
|
22
|
+
*2.0.100 (2.1 Preview 1)* September 1, 2007
|
23
|
+
|
24
|
+
* capify-generated Capfile will autoload all recipes from vendor/plugins/*/recipes/*.rb [Graeme Mathieson]
|
25
|
+
|
26
|
+
* Use sudo -p switch to set sudo password prompt to something predictable [Mike Bailey]
|
27
|
+
|
28
|
+
* Allow independent configurations to require the same recipe file [Jamis Buck]
|
29
|
+
|
30
|
+
* Set :shell to false to run a command without wrapping it in "sh -c" [Jamis Buck]
|
31
|
+
|
32
|
+
* Don't request a pty by default [Jamis Buck]
|
33
|
+
|
34
|
+
* Add a "match" remote dependency method [Adam Greene]
|
35
|
+
|
36
|
+
* Allow auth-caching of subversion credentials to be enabled via :scm_auth_cache [tsmith]
|
37
|
+
|
38
|
+
* Don't let a task trigger itself when used as the source for an "on" hook [Jamis Buck]
|
39
|
+
|
40
|
+
* Avoid using the --password switch with subversion for security purposes [sentinel]
|
41
|
+
|
42
|
+
* Add version_dir, current_dir, and shared_dir variables for naming the directories used in deployment [drinkingbird]
|
43
|
+
|
44
|
+
* Use Windows-safe binary reads for reading file contents [Ladislav Martincik]
|
45
|
+
|
46
|
+
* Add Accurev SCM support [Doug Barth]
|
47
|
+
|
48
|
+
* Use the :runner variable to determine who to sudo as for deploy:restart [Graham Ashton]
|
49
|
+
|
50
|
+
* Add Namespaces#top to always return a reference to the topmost namespace [Jamis Buck]
|
51
|
+
|
52
|
+
* Change the "-h" output so that it does not say that "-q" is the default [Jamis Buck]
|
53
|
+
|
54
|
+
|
1
55
|
*2.0.0* July 21, 2007
|
2
56
|
|
3
57
|
* Make the "no matching servers" error more sane [halorgium]
|
data/bin/capify
CHANGED
data/lib/capistrano/callback.rb
CHANGED
@@ -54,7 +54,7 @@ module Capistrano
|
|
54
54
|
) { options[:password] = nil }
|
55
55
|
|
56
56
|
opts.on("-q", "--quiet",
|
57
|
-
"Make the output as quiet as possible
|
57
|
+
"Make the output as quiet as possible."
|
58
58
|
) { options[:verbose] = 0 }
|
59
59
|
|
60
60
|
opts.on("-S", "--set-before NAME=VALUE",
|
@@ -180,4 +180,4 @@ module Capistrano
|
|
180
180
|
|
181
181
|
end
|
182
182
|
end
|
183
|
-
end
|
183
|
+
end
|
data/lib/capistrano/command.rb
CHANGED
@@ -83,24 +83,39 @@ module Capistrano
|
|
83
83
|
channel[:server] = server
|
84
84
|
channel[:host] = server.host
|
85
85
|
channel[:options] = options
|
86
|
-
channel.request_pty :want_reply => true
|
87
86
|
|
88
|
-
|
87
|
+
execute_command = Proc.new do |ch|
|
89
88
|
logger.trace "executing command", ch[:server] if logger
|
90
|
-
|
91
|
-
|
89
|
+
cmd = replace_placeholders(command, ch)
|
90
|
+
|
91
|
+
if options[:shell] == false
|
92
|
+
shell = nil
|
93
|
+
else
|
94
|
+
shell = "#{options[:shell] || "sh"} -c"
|
95
|
+
cmd = cmd.gsub(/[$\\`"]/) { |m| "\\#{m}" }
|
96
|
+
cmd = "\"#{cmd}\""
|
97
|
+
end
|
98
|
+
|
99
|
+
command_line = [environment, shell, cmd].compact.join(" ")
|
100
|
+
|
92
101
|
ch.exec(command_line)
|
93
102
|
ch.send_data(options[:data]) if options[:data]
|
94
103
|
end
|
95
104
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
105
|
+
if options[:pty]
|
106
|
+
channel.request_pty(:want_reply => true)
|
107
|
+
channel.on_success(&execute_command)
|
108
|
+
channel.on_failure do |ch|
|
109
|
+
# just log it, don't actually raise an exception, since the
|
110
|
+
# process method will see that the status is not zero and will
|
111
|
+
# raise an exception then.
|
112
|
+
logger.important "could not open channel", ch[:server] if logger
|
113
|
+
ch.close
|
114
|
+
end
|
115
|
+
else
|
116
|
+
execute_command.call(channel)
|
102
117
|
end
|
103
|
-
|
118
|
+
|
104
119
|
channel.on_data do |ch, data|
|
105
120
|
@callback[ch, :out, data] if @callback
|
106
121
|
end
|
@@ -23,6 +23,7 @@ module Capistrano
|
|
23
23
|
def initialize_with_invocation(*args) #:nodoc:
|
24
24
|
initialize_without_invocation(*args)
|
25
25
|
set :default_environment, {}
|
26
|
+
set :default_run_options, {}
|
26
27
|
end
|
27
28
|
|
28
29
|
# Invokes the given command. If a +via+ key is given, it will be used
|
@@ -69,7 +70,7 @@ module Capistrano
|
|
69
70
|
as = options.delete(:as)
|
70
71
|
|
71
72
|
user = as && "-u #{as}"
|
72
|
-
command = [fetch(:sudo, "sudo"), user, command].compact.join(" ")
|
73
|
+
command = [fetch(:sudo, "sudo"), "-p '#{sudo_prompt}'", user, command].compact.join(" ")
|
73
74
|
|
74
75
|
run(command, options, &sudo_behavior_callback(block))
|
75
76
|
end
|
@@ -83,9 +84,9 @@ module Capistrano
|
|
83
84
|
# was wrong, let's track which host prompted first and only allow
|
84
85
|
# subsequent prompts from that host.
|
85
86
|
prompt_host = nil
|
86
|
-
|
87
|
+
|
87
88
|
Proc.new do |ch, stream, out|
|
88
|
-
if out =~ /
|
89
|
+
if out =~ /^#{Regexp.escape(sudo_prompt)}/
|
89
90
|
ch.send_data "#{self[:password]}\n"
|
90
91
|
elsif out =~ /try again/
|
91
92
|
if prompt_host.nil? || prompt_host == ch[:server]
|
@@ -110,7 +111,8 @@ module Capistrano
|
|
110
111
|
# Otherwise, if the :default_shell key exists in the configuration,
|
111
112
|
# it will be used. Otherwise, no :shell key is added.
|
112
113
|
def add_default_command_options(options)
|
113
|
-
|
114
|
+
defaults = self[:default_run_options]
|
115
|
+
options = defaults.merge(options)
|
114
116
|
|
115
117
|
env = self[:default_environment]
|
116
118
|
env = env.merge(options[:env]) if options[:env]
|
@@ -121,6 +123,11 @@ module Capistrano
|
|
121
123
|
|
122
124
|
options
|
123
125
|
end
|
126
|
+
|
127
|
+
# Returns the prompt text to use with sudo
|
128
|
+
def sudo_prompt
|
129
|
+
fetch(:sudo_prompt, "sudo password: ")
|
130
|
+
end
|
124
131
|
end
|
125
132
|
end
|
126
133
|
end
|
@@ -25,6 +25,28 @@ module Capistrano
|
|
25
25
|
def instance=(config)
|
26
26
|
Thread.current[:capistrano_configuration] = config
|
27
27
|
end
|
28
|
+
|
29
|
+
# Used internally by Capistrano to track which recipes have been loaded
|
30
|
+
# via require, so that they may be successfully reloaded when require
|
31
|
+
# is called again.
|
32
|
+
def recipes_per_feature
|
33
|
+
@recipes_per_feature ||= {}
|
34
|
+
end
|
35
|
+
|
36
|
+
# Used internally to determine what the current "feature" being
|
37
|
+
# required is. This is used to track which files load which recipes
|
38
|
+
# via require.
|
39
|
+
def current_feature
|
40
|
+
Thread.current[:capistrano_current_feature]
|
41
|
+
end
|
42
|
+
|
43
|
+
# Used internally to specify the current file being required, so that
|
44
|
+
# any recipes loaded by that file can be remembered. This allows
|
45
|
+
# recipes loaded via require to be correctly reloaded in different
|
46
|
+
# Configuration instances in the same Ruby instance.
|
47
|
+
def current_feature=(feature)
|
48
|
+
Thread.current[:capistrano_current_feature] = feature
|
49
|
+
end
|
28
50
|
end
|
29
51
|
|
30
52
|
# The load paths used for locating recipe files.
|
@@ -33,6 +55,7 @@ module Capistrano
|
|
33
55
|
def initialize_with_loading(*args) #:nodoc:
|
34
56
|
initialize_without_loading(*args)
|
35
57
|
@load_paths = [".", File.expand_path(File.join(File.dirname(__FILE__), "../recipes"))]
|
58
|
+
@loaded_features = []
|
36
59
|
end
|
37
60
|
private :initialize_with_loading
|
38
61
|
|
@@ -66,9 +89,11 @@ module Capistrano
|
|
66
89
|
load_from_file(options[:file], options[:name])
|
67
90
|
|
68
91
|
elsif options[:string]
|
92
|
+
remember_load(options) unless options[:reloading]
|
69
93
|
instance_eval(options[:string], options[:name] || "<eval>")
|
70
94
|
|
71
95
|
elsif options[:proc]
|
96
|
+
remember_load(options) unless options[:reloading]
|
72
97
|
instance_eval(&options[:proc])
|
73
98
|
|
74
99
|
else
|
@@ -80,12 +105,63 @@ module Capistrano
|
|
80
105
|
# with the exception that it sets the receiver as the "current" configuration
|
81
106
|
# so that third-party task bundles can include themselves relative to
|
82
107
|
# that configuration.
|
108
|
+
#
|
109
|
+
# This is a bit more complicated than an initial review would seem to
|
110
|
+
# necessitate, but the use case that complicates things is this: An
|
111
|
+
# advanced user wants to embed capistrano, and needs to instantiate
|
112
|
+
# more than one capistrano configuration at a time. They also want each
|
113
|
+
# configuration to require a third-party capistrano extension. Using a
|
114
|
+
# naive require implementation, this would allow the first configuration
|
115
|
+
# to successfully load the third-party extension, but the require would
|
116
|
+
# fail for the second configuration because the extension has already
|
117
|
+
# been loaded.
|
118
|
+
#
|
119
|
+
# To work around this, we do a few things:
|
120
|
+
#
|
121
|
+
# 1. Each time a 'require' is invoked inside of a capistrano recipe,
|
122
|
+
# we remember the arguments (see "current_feature").
|
123
|
+
# 2. Each time a 'load' is invoked inside of a capistrano recipe, and
|
124
|
+
# "current_feature" is not nil (meaning we are inside of a pending
|
125
|
+
# require) we remember the options (see "remember_load" and
|
126
|
+
# "recipes_per_feature").
|
127
|
+
# 3. Each time a 'require' is invoked inside of a capistrano recipe,
|
128
|
+
# we check to see if this particular configuration has ever seen these
|
129
|
+
# arguments to require (see @loaded_features), and if not, we proceed
|
130
|
+
# as if the file had never been required. If the superclass' require
|
131
|
+
# returns false (meaning, potentially, that the file has already been
|
132
|
+
# required), then we look in the recipes_per_feature collection and
|
133
|
+
# load any remembered recipes from there.
|
134
|
+
#
|
135
|
+
# It's kind of a bear, but it works, and works transparently. Note that
|
136
|
+
# a simpler implementation would just muck with $", allowing files to be
|
137
|
+
# required multiple times, but that will cause warnings (and possibly
|
138
|
+
# errors) if the file to be required contains constant definitions and
|
139
|
+
# such, alongside (or instead of) capistrano recipe definitions.
|
83
140
|
def require(*args) #:nodoc:
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
141
|
+
# look to see if this specific configuration instance has ever seen
|
142
|
+
# these arguments to require before
|
143
|
+
if !@loaded_features.include?(args)
|
144
|
+
@loaded_features << args
|
145
|
+
|
146
|
+
begin
|
147
|
+
original_instance, self.class.instance = self.class.instance, self
|
148
|
+
original_feature, self.class.current_feature = self.class.current_feature, args
|
149
|
+
|
150
|
+
result = super
|
151
|
+
if !result # file has been required previously, load up the remembered recipes
|
152
|
+
list = self.class.recipes_per_feature[args] || []
|
153
|
+
list.each { |options| load(options.merge(:reloading => true)) }
|
154
|
+
end
|
155
|
+
|
156
|
+
return result
|
157
|
+
ensure
|
158
|
+
# restore the original, so that require's can be nested
|
159
|
+
self.class.instance = original_instance
|
160
|
+
self.class.current_feature = original_feature
|
161
|
+
end
|
162
|
+
else
|
163
|
+
return false
|
164
|
+
end
|
89
165
|
end
|
90
166
|
|
91
167
|
private
|
@@ -107,6 +183,16 @@ module Capistrano
|
|
107
183
|
|
108
184
|
raise LoadError, "no such file to load -- #{file}"
|
109
185
|
end
|
186
|
+
|
187
|
+
# If a file is being required, the options associated with loading a
|
188
|
+
# recipe are remembered in the recipes_per_feature archive under the
|
189
|
+
# name of the file currently being required.
|
190
|
+
def remember_load(options)
|
191
|
+
if self.class.current_feature
|
192
|
+
list = (self.class.recipes_per_feature[self.class.current_feature] ||= [])
|
193
|
+
list << options
|
194
|
+
end
|
195
|
+
end
|
110
196
|
end
|
111
197
|
end
|
112
198
|
end
|
@@ -32,6 +32,12 @@ module Capistrano
|
|
32
32
|
end
|
33
33
|
private :initialize_with_namespaces
|
34
34
|
|
35
|
+
# Returns the top-level namespace (the one with no parent).
|
36
|
+
def top
|
37
|
+
return parent.top if parent
|
38
|
+
return self
|
39
|
+
end
|
40
|
+
|
35
41
|
# Returns the fully-qualified name of this namespace, or nil if the
|
36
42
|
# namespace is at the top-level.
|
37
43
|
def fully_qualified_name
|
@@ -39,9 +39,14 @@ _cset(:real_revision) { source.local.query_revision(revision) { |cmd| with_e
|
|
39
39
|
_cset(:strategy) { Capistrano::Deploy::Strategy.new(deploy_via, self) }
|
40
40
|
|
41
41
|
_cset(:release_name) { set :deploy_timestamped, true; Time.now.utc.strftime("%Y%m%d%H%M%S") }
|
42
|
-
|
43
|
-
_cset
|
44
|
-
_cset
|
42
|
+
|
43
|
+
_cset :version_dir, "releases"
|
44
|
+
_cset :shared_dir, "shared"
|
45
|
+
_cset :current_dir, "current"
|
46
|
+
|
47
|
+
_cset(:releases_path) { File.join(deploy_to, version_dir) }
|
48
|
+
_cset(:shared_path) { File.join(deploy_to, shared_dir) }
|
49
|
+
_cset(:current_path) { File.join(deploy_to, current_dir) }
|
45
50
|
_cset(:release_path) { File.join(releases_path, release_name) }
|
46
51
|
|
47
52
|
_cset(:releases) { capture("ls -x #{releases_path}").split.sort }
|
@@ -224,21 +229,26 @@ namespace :deploy do
|
|
224
229
|
abort "Please specify at least one file to update (via the FILES environment variable)" if files.empty?
|
225
230
|
|
226
231
|
files.each do |file|
|
227
|
-
|
232
|
+
content = File.open(file, "rb") { |f| f.read }
|
233
|
+
put content, File.join(current_path, file)
|
228
234
|
end
|
229
235
|
end
|
230
236
|
|
231
237
|
desc <<-DESC
|
232
238
|
Restarts your application. This works by calling the script/process/reaper \
|
233
|
-
script under the current path.
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
239
|
+
script under the current path.
|
240
|
+
|
241
|
+
By default, this will be invoked via sudo as the `app' user. If \
|
242
|
+
you wish to run it as a different user, set the :runner variable to \
|
243
|
+
that user. If you are in an environment where you can't use sudo, set \
|
244
|
+
the :use_sudo variable to false:
|
245
|
+
|
238
246
|
set :use_sudo, false
|
239
247
|
DESC
|
240
248
|
task :restart, :roles => :app, :except => { :no_release => true } do
|
241
|
-
|
249
|
+
as = fetch(:runner, "app")
|
250
|
+
via = fetch(:run_method, :sudo)
|
251
|
+
invoke_command "#{current_path}/script/process/reaper", :via => via, :as => as
|
242
252
|
end
|
243
253
|
|
244
254
|
desc <<-DESC
|
@@ -436,7 +446,8 @@ namespace :deploy do
|
|
436
446
|
might not be supported on all SCM's.
|
437
447
|
DESC
|
438
448
|
task :default, :except => { :no_release => true } do
|
439
|
-
|
449
|
+
from = source.next_revision(current_revision)
|
450
|
+
system(source.local.log(from))
|
440
451
|
end
|
441
452
|
end
|
442
453
|
|
@@ -34,6 +34,36 @@ module Capistrano
|
|
34
34
|
self
|
35
35
|
end
|
36
36
|
|
37
|
+
def match(command, expect, options={})
|
38
|
+
expect = Regexp.new(Regexp.escape(expect.to_s)) unless expect.is_a?(Regexp)
|
39
|
+
|
40
|
+
output_per_server = {}
|
41
|
+
try("#{command} ", options) do |ch, stream, out|
|
42
|
+
output_per_server[ch[:server]] ||= ''
|
43
|
+
output_per_server[ch[:server]] += out
|
44
|
+
end
|
45
|
+
|
46
|
+
# It is possible for some of these commands to return a status != 0
|
47
|
+
# (for example, rake --version exits with a 1). For this check we
|
48
|
+
# just care if the output matches, so we reset the success flag.
|
49
|
+
@success = true
|
50
|
+
|
51
|
+
errored_hosts = []
|
52
|
+
output_per_server.each_pair do |server, output|
|
53
|
+
next if output =~ expect
|
54
|
+
errored_hosts << server
|
55
|
+
end
|
56
|
+
|
57
|
+
if errored_hosts.any?
|
58
|
+
@hosts = errored_hosts.join(', ')
|
59
|
+
output = output_per_server[errored_hosts.first]
|
60
|
+
@message = "the output #{output.inspect} from #{command.inspect} did not match #{expect.inspect}"
|
61
|
+
@success = false
|
62
|
+
end
|
63
|
+
|
64
|
+
self
|
65
|
+
end
|
66
|
+
|
37
67
|
def or(message)
|
38
68
|
@message = message
|
39
69
|
self
|
@@ -55,6 +85,7 @@ module Capistrano
|
|
55
85
|
return unless @success # short-circuit evaluation
|
56
86
|
configuration.run(command, options) do |ch,stream,out|
|
57
87
|
warn "#{ch[:server]}: #{out}" if stream == :err
|
88
|
+
yield ch, stream, out if block_given?
|
58
89
|
end
|
59
90
|
rescue Capistrano::CommandError => e
|
60
91
|
@success = false
|
@@ -0,0 +1,169 @@
|
|
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
|