minmb-capistrano 2.15.4

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.
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