capistrano 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|