minmb-capistrano 2.15.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (119) hide show
  1. data/.gitignore +10 -0
  2. data/.travis.yml +7 -0
  3. data/CHANGELOG +1170 -0
  4. data/Gemfile +13 -0
  5. data/README.md +94 -0
  6. data/Rakefile +11 -0
  7. data/bin/cap +4 -0
  8. data/bin/capify +92 -0
  9. data/capistrano.gemspec +40 -0
  10. data/lib/capistrano.rb +5 -0
  11. data/lib/capistrano/callback.rb +45 -0
  12. data/lib/capistrano/cli.rb +47 -0
  13. data/lib/capistrano/cli/execute.rb +85 -0
  14. data/lib/capistrano/cli/help.rb +125 -0
  15. data/lib/capistrano/cli/help.txt +81 -0
  16. data/lib/capistrano/cli/options.rb +243 -0
  17. data/lib/capistrano/cli/ui.rb +40 -0
  18. data/lib/capistrano/command.rb +303 -0
  19. data/lib/capistrano/configuration.rb +57 -0
  20. data/lib/capistrano/configuration/actions/file_transfer.rb +50 -0
  21. data/lib/capistrano/configuration/actions/inspect.rb +46 -0
  22. data/lib/capistrano/configuration/actions/invocation.rb +329 -0
  23. data/lib/capistrano/configuration/alias_task.rb +26 -0
  24. data/lib/capistrano/configuration/callbacks.rb +147 -0
  25. data/lib/capistrano/configuration/connections.rb +237 -0
  26. data/lib/capistrano/configuration/execution.rb +142 -0
  27. data/lib/capistrano/configuration/loading.rb +205 -0
  28. data/lib/capistrano/configuration/log_formatters.rb +75 -0
  29. data/lib/capistrano/configuration/namespaces.rb +223 -0
  30. data/lib/capistrano/configuration/roles.rb +77 -0
  31. data/lib/capistrano/configuration/servers.rb +116 -0
  32. data/lib/capistrano/configuration/variables.rb +127 -0
  33. data/lib/capistrano/errors.rb +19 -0
  34. data/lib/capistrano/ext/multistage.rb +64 -0
  35. data/lib/capistrano/ext/string.rb +5 -0
  36. data/lib/capistrano/extensions.rb +57 -0
  37. data/lib/capistrano/fix_rake_deprecated_dsl.rb +8 -0
  38. data/lib/capistrano/logger.rb +166 -0
  39. data/lib/capistrano/processable.rb +57 -0
  40. data/lib/capistrano/recipes/compat.rb +32 -0
  41. data/lib/capistrano/recipes/deploy.rb +625 -0
  42. data/lib/capistrano/recipes/deploy/assets.rb +201 -0
  43. data/lib/capistrano/recipes/deploy/dependencies.rb +44 -0
  44. data/lib/capistrano/recipes/deploy/local_dependency.rb +54 -0
  45. data/lib/capistrano/recipes/deploy/remote_dependency.rb +117 -0
  46. data/lib/capistrano/recipes/deploy/scm.rb +19 -0
  47. data/lib/capistrano/recipes/deploy/scm/accurev.rb +169 -0
  48. data/lib/capistrano/recipes/deploy/scm/base.rb +200 -0
  49. data/lib/capistrano/recipes/deploy/scm/bzr.rb +86 -0
  50. data/lib/capistrano/recipes/deploy/scm/cvs.rb +153 -0
  51. data/lib/capistrano/recipes/deploy/scm/darcs.rb +96 -0
  52. data/lib/capistrano/recipes/deploy/scm/git.rb +293 -0
  53. data/lib/capistrano/recipes/deploy/scm/mercurial.rb +137 -0
  54. data/lib/capistrano/recipes/deploy/scm/none.rb +55 -0
  55. data/lib/capistrano/recipes/deploy/scm/perforce.rb +152 -0
  56. data/lib/capistrano/recipes/deploy/scm/subversion.rb +121 -0
  57. data/lib/capistrano/recipes/deploy/strategy.rb +19 -0
  58. data/lib/capistrano/recipes/deploy/strategy/base.rb +92 -0
  59. data/lib/capistrano/recipes/deploy/strategy/checkout.rb +20 -0
  60. data/lib/capistrano/recipes/deploy/strategy/copy.rb +338 -0
  61. data/lib/capistrano/recipes/deploy/strategy/export.rb +20 -0
  62. data/lib/capistrano/recipes/deploy/strategy/remote.rb +52 -0
  63. data/lib/capistrano/recipes/deploy/strategy/remote_cache.rb +57 -0
  64. data/lib/capistrano/recipes/deploy/strategy/unshared_remote_cache.rb +21 -0
  65. data/lib/capistrano/recipes/standard.rb +37 -0
  66. data/lib/capistrano/recipes/templates/maintenance.rhtml +53 -0
  67. data/lib/capistrano/role.rb +102 -0
  68. data/lib/capistrano/server_definition.rb +56 -0
  69. data/lib/capistrano/shell.rb +265 -0
  70. data/lib/capistrano/ssh.rb +95 -0
  71. data/lib/capistrano/task_definition.rb +77 -0
  72. data/lib/capistrano/transfer.rb +218 -0
  73. data/lib/capistrano/version.rb +11 -0
  74. data/test/cli/execute_test.rb +132 -0
  75. data/test/cli/help_test.rb +165 -0
  76. data/test/cli/options_test.rb +329 -0
  77. data/test/cli/ui_test.rb +28 -0
  78. data/test/cli_test.rb +17 -0
  79. data/test/command_test.rb +322 -0
  80. data/test/configuration/actions/file_transfer_test.rb +61 -0
  81. data/test/configuration/actions/inspect_test.rb +76 -0
  82. data/test/configuration/actions/invocation_test.rb +288 -0
  83. data/test/configuration/alias_task_test.rb +118 -0
  84. data/test/configuration/callbacks_test.rb +201 -0
  85. data/test/configuration/connections_test.rb +439 -0
  86. data/test/configuration/execution_test.rb +175 -0
  87. data/test/configuration/loading_test.rb +148 -0
  88. data/test/configuration/namespace_dsl_test.rb +332 -0
  89. data/test/configuration/roles_test.rb +157 -0
  90. data/test/configuration/servers_test.rb +183 -0
  91. data/test/configuration/variables_test.rb +190 -0
  92. data/test/configuration_test.rb +77 -0
  93. data/test/deploy/local_dependency_test.rb +76 -0
  94. data/test/deploy/remote_dependency_test.rb +146 -0
  95. data/test/deploy/scm/accurev_test.rb +23 -0
  96. data/test/deploy/scm/base_test.rb +55 -0
  97. data/test/deploy/scm/bzr_test.rb +51 -0
  98. data/test/deploy/scm/darcs_test.rb +37 -0
  99. data/test/deploy/scm/git_test.rb +221 -0
  100. data/test/deploy/scm/mercurial_test.rb +134 -0
  101. data/test/deploy/scm/none_test.rb +35 -0
  102. data/test/deploy/scm/perforce_test.rb +23 -0
  103. data/test/deploy/scm/subversion_test.rb +40 -0
  104. data/test/deploy/strategy/copy_test.rb +360 -0
  105. data/test/extensions_test.rb +69 -0
  106. data/test/fixtures/cli_integration.rb +5 -0
  107. data/test/fixtures/config.rb +5 -0
  108. data/test/fixtures/custom.rb +3 -0
  109. data/test/logger_formatting_test.rb +149 -0
  110. data/test/logger_test.rb +134 -0
  111. data/test/recipes_test.rb +25 -0
  112. data/test/role_test.rb +11 -0
  113. data/test/server_definition_test.rb +121 -0
  114. data/test/shell_test.rb +96 -0
  115. data/test/ssh_test.rb +113 -0
  116. data/test/task_definition_test.rb +117 -0
  117. data/test/transfer_test.rb +168 -0
  118. data/test/utils.rb +37 -0
  119. metadata +316 -0
@@ -0,0 +1,201 @@
1
+ require 'json'
2
+
3
+ load 'deploy' unless defined?(_cset)
4
+
5
+ _cset :asset_env, "RAILS_GROUPS=assets"
6
+ _cset :assets_prefix, "assets"
7
+ _cset :shared_assets_prefix, "assets"
8
+ _cset :assets_role, [:web]
9
+ _cset :expire_assets_after, (3600 * 24 * 7)
10
+
11
+ _cset :normalize_asset_timestamps, false
12
+
13
+ before 'deploy:finalize_update', 'deploy:assets:symlink'
14
+ after 'deploy:update_code', 'deploy:assets:precompile'
15
+ before 'deploy:assets:precompile', 'deploy:assets:update_asset_mtimes'
16
+ after 'deploy:cleanup', 'deploy:assets:clean_expired'
17
+ after 'deploy:rollback:revision', 'deploy:assets:rollback'
18
+
19
+ def shared_manifest_path
20
+ @shared_manifest_path ||= capture("ls #{shared_path.shellescape}/#{shared_assets_prefix}/manifest*").strip
21
+ end
22
+
23
+ # Parses manifest and returns array of uncompressed and compressed asset filenames with and without digests
24
+ # "Intelligently" determines format of string - supports YAML and JSON
25
+ def parse_manifest(str)
26
+ assets_hash = str[0,1] == '{' ? JSON.parse(str)['assets'] : YAML.load(str)
27
+
28
+ assets_hash.to_a.flatten.map {|a| [a, "#{a}.gz"] }.flatten
29
+ end
30
+
31
+ namespace :deploy do
32
+ namespace :assets do
33
+ desc <<-DESC
34
+ [internal] This task will set up a symlink to the shared directory \
35
+ for the assets directory. Assets are shared across deploys to avoid \
36
+ mid-deploy mismatches between old application html asking for assets \
37
+ and getting a 404 file not found error. The assets cache is shared \
38
+ for efficiency. If you customize the assets path prefix, override the \
39
+ :assets_prefix variable to match. If you customize shared assets path \
40
+ prefix, override :shared_assets_prefix variable to match.
41
+ DESC
42
+ task :symlink, :roles => lambda { assets_role }, :except => { :no_release => true } do
43
+ run <<-CMD.compact
44
+ rm -rf #{latest_release}/public/#{assets_prefix} &&
45
+ mkdir -p #{latest_release}/public &&
46
+ mkdir -p #{shared_path}/#{shared_assets_prefix} &&
47
+ ln -s #{shared_path}/#{shared_assets_prefix} #{latest_release}/public/#{assets_prefix}
48
+ CMD
49
+ end
50
+
51
+ desc <<-DESC
52
+ Run the asset precompilation rake task. You can specify the full path \
53
+ to the rake executable by setting the rake variable. You can also \
54
+ specify additional environment variables to pass to rake via the \
55
+ asset_env variable. The defaults are:
56
+
57
+ set :rake, "rake"
58
+ set :rails_env, "production"
59
+ set :asset_env, "RAILS_GROUPS=assets"
60
+ DESC
61
+ task :precompile, :roles => lambda { assets_role }, :except => { :no_release => true } do
62
+ run <<-CMD.compact
63
+ cd -- #{latest_release} &&
64
+ RAILS_ENV=#{rails_env.to_s.shellescape} #{asset_env} #{rake} assets:precompile
65
+ CMD
66
+
67
+ if capture("ls -1 #{shared_path.shellescape}/#{shared_assets_prefix}/manifest* | wc -l").to_i > 1
68
+ raise "More than one asset manifest file was found in '#{shared_path.shellescape}/#{shared_assets_prefix}'. If you are upgrading a Rails 3 application to Rails 4, follow these instructions: http://github.com/capistrano/capistrano/wiki/Upgrading-to-Rails-4#asset-pipeline"
69
+ end
70
+
71
+ # Sync manifest filenames across servers if our manifest has a random filename
72
+ if shared_manifest_path =~ /manifest-.+\./
73
+ run <<-CMD.compact
74
+ [ -e #{shared_manifest_path.shellescape} ] || mv -- #{shared_path.shellescape}/#{shared_assets_prefix}/manifest* #{shared_manifest_path.shellescape}
75
+ CMD
76
+ end
77
+
78
+ # Copy manifest to release root (for clean_expired task)
79
+ run <<-CMD.compact
80
+ cp -- #{shared_manifest_path.shellescape} #{current_release.to_s.shellescape}/assets_manifest#{File.extname(shared_manifest_path)}
81
+ CMD
82
+ end
83
+
84
+ desc <<-DESC
85
+ [internal] Updates the mtimes for assets that are required by the current release.
86
+ This task runs before assets:precompile.
87
+ DESC
88
+ task :update_asset_mtimes, :roles => lambda { assets_role }, :except => { :no_release => true } do
89
+ # Fetch assets/manifest contents.
90
+ manifest_content = capture("[ -e #{shared_path.shellescape}/#{shared_assets_prefix}/manifest* ] && cat #{shared_path.shellescape}/#{shared_assets_prefix}/manifest* || echo").strip
91
+
92
+ if manifest_content != ""
93
+ current_assets = parse_manifest(manifest_content)
94
+ logger.info "Updating mtimes for ~#{current_assets.count} assets..."
95
+ put current_assets.map{|a| "#{shared_path}/#{shared_assets_prefix}/#{a}" }.join("\n"), "#{deploy_to}/TOUCH_ASSETS", :via => :scp
96
+ run <<-CMD.compact
97
+ cat #{deploy_to.shellescape}/TOUCH_ASSETS | while read asset; do
98
+ touch -c -- "$asset";
99
+ done &&
100
+ rm -f -- #{deploy_to.shellescape}/TOUCH_ASSETS
101
+ CMD
102
+ end
103
+ end
104
+
105
+ desc <<-DESC
106
+ Run the asset clean rake task. Use with caution, this will delete \
107
+ all of your compiled assets. You can specify the full path \
108
+ to the rake executable by setting the rake variable. You can also \
109
+ specify additional environment variables to pass to rake via the \
110
+ asset_env variable. The defaults are:
111
+
112
+ set :rake, "rake"
113
+ set :rails_env, "production"
114
+ set :asset_env, "RAILS_GROUPS=assets"
115
+ DESC
116
+ task :clean, :roles => lambda { assets_role }, :except => { :no_release => true } do
117
+ run "cd #{latest_release} && #{rake} RAILS_ENV=#{rails_env} #{asset_env} assets:clean"
118
+ end
119
+
120
+ desc <<-DESC
121
+ Clean up any assets that haven't been deployed for more than :expire_assets_after seconds.
122
+ Default time to keep old assets is one week. Set the :expire_assets_after variable
123
+ to change the assets expiry time. Assets will only be deleted if they are not required by
124
+ an existing release.
125
+ DESC
126
+ task :clean_expired, :roles => lambda { assets_role }, :except => { :no_release => true } do
127
+ # Fetch all assets_manifest contents.
128
+ manifests_output = capture <<-CMD.compact
129
+ for manifest in #{releases_path.shellescape}/*/assets_manifest.*; do
130
+ cat -- "$manifest" 2> /dev/null && printf ':::' || true;
131
+ done
132
+ CMD
133
+ manifests = manifests_output.split(':::')
134
+
135
+ if manifests.empty?
136
+ logger.info "No manifests in #{releases_path}/*/assets_manifest.*"
137
+ else
138
+ logger.info "Fetched #{manifests.count} manifests from #{releases_path}/*/assets_manifest.*"
139
+ current_assets = Set.new
140
+ manifests.each do |content|
141
+ current_assets += parse_manifest(content)
142
+ end
143
+ current_assets += [File.basename(shared_manifest_path), "sources_manifest.yml"]
144
+
145
+ # Write the list of required assets to server.
146
+ logger.info "Writing required assets to #{deploy_to}/REQUIRED_ASSETS..."
147
+ escaped_assets = current_assets.sort.join("\n").gsub("\"", "\\\"") << "\n"
148
+ put escaped_assets, "#{deploy_to}/REQUIRED_ASSETS", :via => :scp
149
+
150
+ # Finds all files older than X minutes, then removes them if they are not referenced
151
+ # in REQUIRED_ASSETS.
152
+ expire_after_mins = (expire_assets_after.to_f / 60.0).to_i
153
+ logger.info "Removing assets that haven't been deployed for #{expire_after_mins} minutes..."
154
+ # LC_COLLATE=C tells the `sort` and `comm` commands to sort files in byte order.
155
+ run <<-CMD.compact
156
+ cd -- #{deploy_to.shellescape}/ &&
157
+ LC_COLLATE=C sort REQUIRED_ASSETS -o REQUIRED_ASSETS &&
158
+ cd -- #{shared_path.shellescape}/#{shared_assets_prefix}/ &&
159
+ for f in $(
160
+ find * -mmin +#{expire_after_mins.to_s.shellescape} -type f | LC_COLLATE=C sort |
161
+ LC_COLLATE=C comm -23 -- - #{deploy_to.shellescape}/REQUIRED_ASSETS
162
+ ); do
163
+ echo "Removing unneeded asset: $f";
164
+ rm -f -- "$f";
165
+ done;
166
+ rm -f -- #{deploy_to.shellescape}/REQUIRED_ASSETS
167
+ CMD
168
+ end
169
+ end
170
+
171
+ desc <<-DESC
172
+ Rolls back assets to the previous release by symlinking the release's manifest
173
+ to shared/assets/manifest, and finally recompiling or regenerating nondigest assets.
174
+ DESC
175
+ task :rollback, :roles => lambda { assets_role }, :except => { :no_release => true } do
176
+ previous_manifest = capture("ls #{previous_release.shellescape}/assets_manifest.*").strip
177
+ if capture("[ -e #{previous_manifest.shellescape} ] && echo true || echo false").strip != 'true'
178
+ puts "#{previous_manifest} is missing! Cannot roll back assets. " <<
179
+ "Please run deploy:assets:precompile to update your assets when the rollback is finished."
180
+ else
181
+ # If the user is rolling back a Rails 4 app to Rails 3
182
+ if File.extname(previous_manifest) == '.yml' && File.extname(shared_manifest_path) == '.json'
183
+ # Remove the existing JSON manifest
184
+ run "rm -f -- #{shared_manifest_path.shellescape}"
185
+
186
+ # Restore the manifest to the Rails 3 path
187
+ restored_manifest_path = "#{shared_path.shellescape}/#{shared_assets_prefix}/manifest.yml"
188
+ else
189
+ # If the user is not rolling back from Rails 4 to 3, we just want to replace the current manifest
190
+ restored_manifest_path = shared_manifest_path
191
+ end
192
+
193
+ run <<-CMD.compact
194
+ cd -- #{previous_release.shellescape} &&
195
+ cp -f -- #{previous_manifest.shellescape} #{restored_manifest_path.shellescape} &&
196
+ [ -z "$(#{rake} -P | grep assets:precompile:nondigest)" ] || #{rake} RAILS_ENV=#{rails_env.to_s.shellescape} #{asset_env} assets:precompile:nondigest
197
+ CMD
198
+ end
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,44 @@
1
+ require 'capistrano/recipes/deploy/local_dependency'
2
+ require 'capistrano/recipes/deploy/remote_dependency'
3
+
4
+ module Capistrano
5
+ module Deploy
6
+ class Dependencies
7
+ include Enumerable
8
+
9
+ attr_reader :configuration
10
+
11
+ def initialize(configuration)
12
+ @configuration = configuration
13
+ @dependencies = []
14
+ yield self if block_given?
15
+ end
16
+
17
+ def check
18
+ yield self
19
+ self
20
+ end
21
+
22
+ def remote
23
+ dep = RemoteDependency.new(configuration)
24
+ @dependencies << dep
25
+ dep
26
+ end
27
+
28
+ def local
29
+ dep = LocalDependency.new(configuration)
30
+ @dependencies << dep
31
+ dep
32
+ end
33
+
34
+ def each
35
+ @dependencies.each { |d| yield d }
36
+ self
37
+ end
38
+
39
+ def pass?
40
+ all? { |d| d.pass? }
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,54 @@
1
+ module Capistrano
2
+ module Deploy
3
+ class LocalDependency
4
+ attr_reader :configuration
5
+ attr_reader :message
6
+
7
+ def initialize(configuration)
8
+ @configuration = configuration
9
+ @success = true
10
+ end
11
+
12
+ def command(command)
13
+ @message ||= "`#{command}' could not be found in the path on the local host"
14
+ @success = find_in_path(command)
15
+ self
16
+ end
17
+
18
+ def or(message)
19
+ @message = message
20
+ self
21
+ end
22
+
23
+ def pass?
24
+ @success
25
+ end
26
+
27
+ private
28
+
29
+ # Searches the path, looking for the given utility. If an executable
30
+ # file is found that matches the parameter, this returns true.
31
+ def find_in_path(utility)
32
+ path = (ENV['PATH'] || "").split(File::PATH_SEPARATOR)
33
+ suffixes = self.class.on_windows? ? self.class.windows_executable_extensions : [""]
34
+
35
+ path.each do |dir|
36
+ suffixes.each do |sfx|
37
+ file = File.join(dir, utility + sfx)
38
+ return true if File.executable?(file)
39
+ end
40
+ end
41
+
42
+ false
43
+ end
44
+
45
+ def self.on_windows?
46
+ RUBY_PLATFORM =~ /mswin|mingw/
47
+ end
48
+
49
+ def self.windows_executable_extensions
50
+ %w(.exe .bat .com .cmd)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,117 @@
1
+ require 'capistrano/errors'
2
+
3
+ module Capistrano
4
+ module Deploy
5
+ class RemoteDependency
6
+ attr_reader :configuration
7
+ attr_reader :hosts
8
+
9
+ def initialize(configuration)
10
+ @configuration = configuration
11
+ @success = true
12
+ @hosts = nil
13
+ end
14
+
15
+ def directory(path, options={})
16
+ @message ||= "`#{path}' is not a directory"
17
+ try("test -d #{path}", options)
18
+ self
19
+ end
20
+
21
+ def file(path, options={})
22
+ @message ||= "`#{path}' is not a file"
23
+ try("test -f #{path}", options)
24
+ self
25
+ end
26
+
27
+ def writable(path, options={})
28
+ @message ||= "`#{path}' is not writable"
29
+ try("test -w #{path}", options)
30
+ self
31
+ end
32
+
33
+ def command(command, options={})
34
+ @message ||= "`#{command}' could not be found in the path"
35
+ try("which #{command}", options)
36
+ self
37
+ end
38
+
39
+ def gem(name, version, options={})
40
+ @message ||= "gem `#{name}' #{version} could not be found"
41
+ gem_cmd = configuration.fetch(:gem_command, "gem")
42
+ try("#{gem_cmd} specification --version '#{version}' #{name} 2>&1 | awk 'BEGIN { s = 0 } /^name:/ { s = 1; exit }; END { if(s == 0) exit 1 }'", options)
43
+ self
44
+ end
45
+
46
+ def deb(name, version, options={})
47
+ @message ||= "package `#{name}' #{version} could not be found"
48
+ try("dpkg -s #{name} | grep '^Version: #{version}'", options)
49
+ self
50
+ end
51
+
52
+ def rpm(name, version, options={})
53
+ @message ||= "package `#{name}' #{version} could not be found"
54
+ try("rpm -q #{name} | grep '#{version}'", options)
55
+ self
56
+ end
57
+
58
+ def match(command, expect, options={})
59
+ expect = Regexp.new(Regexp.escape(expect.to_s)) unless expect.is_a?(Regexp)
60
+
61
+ output_per_server = {}
62
+ try("#{command} ", options) do |ch, stream, out|
63
+ output_per_server[ch[:server]] ||= ''
64
+ output_per_server[ch[:server]] += out
65
+ end
66
+
67
+ # It is possible for some of these commands to return a status != 0
68
+ # (for example, rake --version exits with a 1). For this check we
69
+ # just care if the output matches, so we reset the success flag.
70
+ @success = true
71
+
72
+ errored_hosts = []
73
+ output_per_server.each_pair do |server, output|
74
+ next if output =~ expect
75
+ errored_hosts << server
76
+ end
77
+
78
+ if errored_hosts.any?
79
+ @hosts = errored_hosts.join(', ')
80
+ output = output_per_server[errored_hosts.first]
81
+ @message = "the output #{output.inspect} from #{command.inspect} did not match #{expect.inspect}"
82
+ @success = false
83
+ end
84
+
85
+ self
86
+ end
87
+
88
+ def or(message)
89
+ @message = message
90
+ self
91
+ end
92
+
93
+ def pass?
94
+ @success
95
+ end
96
+
97
+ def message
98
+ s = @message.dup
99
+ s << " (#{@hosts})" if @hosts
100
+ s
101
+ end
102
+
103
+ private
104
+
105
+ def try(command, options)
106
+ return unless @success # short-circuit evaluation
107
+ configuration.invoke_command(command, options) do |ch,stream,out|
108
+ warn "#{ch[:server]}: #{out}" if stream == :err
109
+ yield ch, stream, out if block_given?
110
+ end
111
+ rescue Capistrano::CommandError => e
112
+ @success = false
113
+ @hosts = e.hosts.join(', ')
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,19 @@
1
+ module Capistrano
2
+ module Deploy
3
+ module SCM
4
+ def self.new(scm, config={})
5
+ scm_file = "capistrano/recipes/deploy/scm/#{scm}"
6
+ require(scm_file)
7
+
8
+ scm_const = scm.to_s.capitalize.gsub(/_(.)/) { $1.upcase }
9
+ if const_defined?(scm_const)
10
+ const_get(scm_const).new(config)
11
+ else
12
+ raise Capistrano::Error, "could not find `#{name}::#{scm_const}' in `#{scm_file}'"
13
+ end
14
+ rescue LoadError
15
+ raise Capistrano::Error, "could not find any SCM named `#{scm}'"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -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