capistrano 2.0.0 → 2.15.9

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 (124) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.travis.yml +9 -0
  4. data/CHANGELOG +755 -18
  5. data/Gemfile +13 -0
  6. data/README.md +94 -0
  7. data/Rakefile +11 -0
  8. data/bin/cap +0 -0
  9. data/bin/capify +37 -22
  10. data/capistrano.gemspec +40 -0
  11. data/lib/capistrano/callback.rb +5 -1
  12. data/lib/capistrano/cli/execute.rb +10 -7
  13. data/lib/capistrano/cli/help.rb +39 -16
  14. data/lib/capistrano/cli/help.txt +44 -16
  15. data/lib/capistrano/cli/options.rb +71 -11
  16. data/lib/capistrano/cli/ui.rb +13 -1
  17. data/lib/capistrano/cli.rb +5 -5
  18. data/lib/capistrano/command.rb +215 -58
  19. data/lib/capistrano/configuration/actions/file_transfer.rb +29 -14
  20. data/lib/capistrano/configuration/actions/inspect.rb +3 -3
  21. data/lib/capistrano/configuration/actions/invocation.rb +224 -22
  22. data/lib/capistrano/configuration/alias_task.rb +26 -0
  23. data/lib/capistrano/configuration/callbacks.rb +26 -27
  24. data/lib/capistrano/configuration/connections.rb +130 -52
  25. data/lib/capistrano/configuration/execution.rb +34 -18
  26. data/lib/capistrano/configuration/loading.rb +99 -6
  27. data/lib/capistrano/configuration/log_formatters.rb +75 -0
  28. data/lib/capistrano/configuration/namespaces.rb +45 -12
  29. data/lib/capistrano/configuration/roles.rb +34 -2
  30. data/lib/capistrano/configuration/servers.rb +51 -10
  31. data/lib/capistrano/configuration/variables.rb +3 -3
  32. data/lib/capistrano/configuration.rb +20 -4
  33. data/lib/capistrano/errors.rb +12 -8
  34. data/lib/capistrano/ext/multistage.rb +67 -0
  35. data/lib/capistrano/ext/string.rb +5 -0
  36. data/lib/capistrano/extensions.rb +1 -1
  37. data/lib/capistrano/fix_rake_deprecated_dsl.rb +8 -0
  38. data/lib/capistrano/logger.rb +112 -5
  39. data/lib/capistrano/processable.rb +55 -0
  40. data/lib/capistrano/recipes/compat.rb +2 -2
  41. data/lib/capistrano/recipes/deploy/assets.rb +202 -0
  42. data/lib/capistrano/recipes/deploy/dependencies.rb +2 -2
  43. data/lib/capistrano/recipes/deploy/local_dependency.rb +10 -2
  44. data/lib/capistrano/recipes/deploy/remote_dependency.rb +54 -2
  45. data/lib/capistrano/recipes/deploy/scm/accurev.rb +169 -0
  46. data/lib/capistrano/recipes/deploy/scm/base.rb +31 -11
  47. data/lib/capistrano/recipes/deploy/scm/bzr.rb +14 -14
  48. data/lib/capistrano/recipes/deploy/scm/cvs.rb +11 -9
  49. data/lib/capistrano/recipes/deploy/scm/darcs.rb +12 -1
  50. data/lib/capistrano/recipes/deploy/scm/git.rb +299 -0
  51. data/lib/capistrano/recipes/deploy/scm/mercurial.rb +23 -15
  52. data/lib/capistrano/recipes/deploy/scm/none.rb +55 -0
  53. data/lib/capistrano/recipes/deploy/scm/perforce.rb +54 -28
  54. data/lib/capistrano/recipes/deploy/scm/subversion.rb +36 -18
  55. data/lib/capistrano/recipes/deploy/scm.rb +1 -1
  56. data/lib/capistrano/recipes/deploy/strategy/base.rb +32 -4
  57. data/lib/capistrano/recipes/deploy/strategy/copy.rb +238 -43
  58. data/lib/capistrano/recipes/deploy/strategy/remote.rb +1 -1
  59. data/lib/capistrano/recipes/deploy/strategy/remote_cache.rb +11 -1
  60. data/lib/capistrano/recipes/deploy/strategy/unshared_remote_cache.rb +21 -0
  61. data/lib/capistrano/recipes/deploy/strategy.rb +4 -3
  62. data/lib/capistrano/recipes/deploy.rb +265 -123
  63. data/lib/capistrano/recipes/standard.rb +1 -1
  64. data/lib/capistrano/role.rb +102 -0
  65. data/lib/capistrano/server_definition.rb +6 -1
  66. data/lib/capistrano/shell.rb +30 -33
  67. data/lib/capistrano/ssh.rb +46 -60
  68. data/lib/capistrano/task_definition.rb +16 -8
  69. data/lib/capistrano/transfer.rb +218 -0
  70. data/lib/capistrano/version.rb +8 -17
  71. data/lib/capistrano.rb +4 -1
  72. data/test/cli/execute_test.rb +3 -3
  73. data/test/cli/help_test.rb +33 -7
  74. data/test/cli/options_test.rb +109 -6
  75. data/test/cli/ui_test.rb +2 -2
  76. data/test/cli_test.rb +3 -3
  77. data/test/command_test.rb +144 -124
  78. data/test/configuration/actions/file_transfer_test.rb +41 -20
  79. data/test/configuration/actions/inspect_test.rb +21 -7
  80. data/test/configuration/actions/invocation_test.rb +141 -30
  81. data/test/configuration/alias_task_test.rb +118 -0
  82. data/test/configuration/callbacks_test.rb +41 -46
  83. data/test/configuration/connections_test.rb +187 -36
  84. data/test/configuration/execution_test.rb +18 -2
  85. data/test/configuration/loading_test.rb +33 -4
  86. data/test/configuration/namespace_dsl_test.rb +54 -5
  87. data/test/configuration/roles_test.rb +114 -4
  88. data/test/configuration/servers_test.rb +97 -4
  89. data/test/configuration/variables_test.rb +12 -2
  90. data/test/configuration_test.rb +9 -13
  91. data/test/deploy/local_dependency_test.rb +76 -0
  92. data/test/deploy/remote_dependency_test.rb +146 -0
  93. data/test/deploy/scm/accurev_test.rb +23 -0
  94. data/test/deploy/scm/base_test.rb +1 -1
  95. data/test/deploy/scm/bzr_test.rb +51 -0
  96. data/test/deploy/scm/darcs_test.rb +37 -0
  97. data/test/deploy/scm/git_test.rb +274 -0
  98. data/test/deploy/scm/mercurial_test.rb +134 -0
  99. data/test/deploy/scm/none_test.rb +35 -0
  100. data/test/deploy/scm/perforce_test.rb +23 -0
  101. data/test/deploy/scm/subversion_test.rb +68 -0
  102. data/test/deploy/strategy/copy_test.rb +240 -26
  103. data/test/extensions_test.rb +2 -2
  104. data/test/logger_formatting_test.rb +149 -0
  105. data/test/logger_test.rb +13 -2
  106. data/test/recipes_test.rb +25 -0
  107. data/test/role_test.rb +11 -0
  108. data/test/server_definition_test.rb +15 -2
  109. data/test/shell_test.rb +33 -1
  110. data/test/ssh_test.rb +40 -24
  111. data/test/task_definition_test.rb +18 -2
  112. data/test/transfer_test.rb +168 -0
  113. data/test/utils.rb +28 -33
  114. data/test/version_test.rb +4 -17
  115. metadata +215 -100
  116. data/MIT-LICENSE +0 -20
  117. data/README +0 -43
  118. data/examples/sample.rb +0 -14
  119. data/lib/capistrano/gateway.rb +0 -131
  120. data/lib/capistrano/recipes/deploy/templates/maintenance.rhtml +0 -53
  121. data/lib/capistrano/recipes/upgrade.rb +0 -33
  122. data/lib/capistrano/upload.rb +0 -146
  123. data/test/gateway_test.rb +0 -167
  124. data/test/upload_test.rb +0 -131
@@ -7,7 +7,7 @@ load 'deploy'
7
7
  map = { "diff_from_last_deploy" => "deploy:pending:diff",
8
8
  "update" => "deploy:update",
9
9
  "update_code" => "deploy:update_code",
10
- "symlink" => "deploy:symlink",
10
+ "symlink" => "deploy:create_symlink",
11
11
  "restart" => "deploy:restart",
12
12
  "rollback" => "deploy:rollback",
13
13
  "cleanup" => "deploy:cleanup",
@@ -29,4 +29,4 @@ task :spinner do
29
29
  warn "[DEPRECATED] `spinner' is deprecated. Use `deploy:start' instead."
30
30
  set :runner, fetch(:spinner_user, "app")
31
31
  deploy.start
32
- end
32
+ end
@@ -0,0 +1,202 @@
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
+ _cset(:asset_manifest_prefix) { (`sprockets -v`.chomp < "3.0" ? "manifest" : ".sprockets-manifest") rescue "manifest" }
11
+
12
+ _cset :normalize_asset_timestamps, false
13
+
14
+ before 'deploy:finalize_update', 'deploy:assets:symlink'
15
+ after 'deploy:update_code', 'deploy:assets:precompile'
16
+ before 'deploy:assets:precompile', 'deploy:assets:update_asset_mtimes'
17
+ after 'deploy:cleanup', 'deploy:assets:clean_expired'
18
+ after 'deploy:rollback:revision', 'deploy:assets:rollback'
19
+
20
+ def shared_manifest_path
21
+ @shared_manifest_path ||= capture("ls #{shared_path.shellescape}/#{shared_assets_prefix}/#{asset_manifest_prefix}*").strip
22
+ end
23
+
24
+ # Parses manifest and returns array of uncompressed and compressed asset filenames with and without digests
25
+ # "Intelligently" determines format of string - supports YAML and JSON
26
+ def parse_manifest(str)
27
+ assets_hash = str[0,1] == '{' ? JSON.parse(str)['assets'] : YAML.load(str)
28
+
29
+ assets_hash.to_a.flatten.map {|a| [a, "#{a}.gz"] }.flatten
30
+ end
31
+
32
+ namespace :deploy do
33
+ namespace :assets do
34
+ desc <<-DESC
35
+ [internal] This task will set up a symlink to the shared directory \
36
+ for the assets directory. Assets are shared across deploys to avoid \
37
+ mid-deploy mismatches between old application html asking for assets \
38
+ and getting a 404 file not found error. The assets cache is shared \
39
+ for efficiency. If you customize the assets path prefix, override the \
40
+ :assets_prefix variable to match. If you customize shared assets path \
41
+ prefix, override :shared_assets_prefix variable to match.
42
+ DESC
43
+ task :symlink, :roles => lambda { assets_role }, :except => { :no_release => true } do
44
+ run <<-CMD.compact
45
+ rm -rf #{latest_release}/public/#{assets_prefix} &&
46
+ mkdir -p #{latest_release}/public &&
47
+ mkdir -p #{shared_path}/#{shared_assets_prefix} &&
48
+ ln -s #{shared_path}/#{shared_assets_prefix} #{latest_release}/public/#{assets_prefix}
49
+ CMD
50
+ end
51
+
52
+ desc <<-DESC
53
+ Run the asset precompilation rake task. You can specify the full path \
54
+ to the rake executable by setting the rake variable. You can also \
55
+ specify additional environment variables to pass to rake via the \
56
+ asset_env variable. The defaults are:
57
+
58
+ set :rake, "rake"
59
+ set :rails_env, "production"
60
+ set :asset_env, "RAILS_GROUPS=assets"
61
+ DESC
62
+ task :precompile, :roles => lambda { assets_role }, :except => { :no_release => true } do
63
+ run <<-CMD.compact
64
+ cd -- #{latest_release} &&
65
+ RAILS_ENV=#{rails_env.to_s.shellescape} #{asset_env} #{rake} assets:precompile
66
+ CMD
67
+
68
+ if capture("ls -1 #{shared_path.shellescape}/#{shared_assets_prefix}/#{asset_manifest_prefix}* | wc -l").to_i > 1
69
+ 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"
70
+ end
71
+
72
+ # Sync manifest filenames across servers if our manifest has a random filename
73
+ if shared_manifest_path =~ /#{asset_manifest_prefix}-.+\./
74
+ run <<-CMD.compact
75
+ [ -e #{shared_manifest_path.shellescape} ] || mv -- #{shared_path.shellescape}/#{shared_assets_prefix}/#{asset_manifest_prefix}* #{shared_manifest_path.shellescape}
76
+ CMD
77
+ end
78
+
79
+ # Copy manifest to release root (for clean_expired task)
80
+ run <<-CMD.compact
81
+ cp -- #{shared_manifest_path.shellescape} #{current_release.to_s.shellescape}/assets_manifest#{File.extname(shared_manifest_path)}
82
+ CMD
83
+ end
84
+
85
+ desc <<-DESC
86
+ [internal] Updates the mtimes for assets that are required by the current release.
87
+ This task runs before assets:precompile.
88
+ DESC
89
+ task :update_asset_mtimes, :roles => lambda { assets_role }, :except => { :no_release => true } do
90
+ # Fetch assets/manifest contents.
91
+ manifest_content = capture("[ -e '#{shared_path.shellescape}/#{shared_assets_prefix}/#{asset_manifest_prefix}*' ] && cat #{shared_path.shellescape}/#{shared_assets_prefix}/#{asset_manifest_prefix}* || echo").strip
92
+
93
+ if manifest_content != ""
94
+ current_assets = parse_manifest(manifest_content)
95
+ logger.info "Updating mtimes for ~#{current_assets.count} assets..."
96
+ put current_assets.map{|a| "#{shared_path}/#{shared_assets_prefix}/#{a}" }.join("\n"), "#{deploy_to}/TOUCH_ASSETS", :via => :scp
97
+ run <<-CMD.compact
98
+ cat #{deploy_to.shellescape}/TOUCH_ASSETS | while read asset; do
99
+ touch -c -- "$asset";
100
+ done &&
101
+ rm -f -- #{deploy_to.shellescape}/TOUCH_ASSETS
102
+ CMD
103
+ end
104
+ end
105
+
106
+ desc <<-DESC
107
+ Run the asset clean rake task. Use with caution, this will delete \
108
+ all of your compiled assets. You can specify the full path \
109
+ to the rake executable by setting the rake variable. You can also \
110
+ specify additional environment variables to pass to rake via the \
111
+ asset_env variable. The defaults are:
112
+
113
+ set :rake, "rake"
114
+ set :rails_env, "production"
115
+ set :asset_env, "RAILS_GROUPS=assets"
116
+ DESC
117
+ task :clean, :roles => lambda { assets_role }, :except => { :no_release => true } do
118
+ run "cd #{latest_release} && #{rake} RAILS_ENV=#{rails_env} #{asset_env} assets:clean"
119
+ end
120
+
121
+ desc <<-DESC
122
+ Clean up any assets that haven't been deployed for more than :expire_assets_after seconds.
123
+ Default time to keep old assets is one week. Set the :expire_assets_after variable
124
+ to change the assets expiry time. Assets will only be deleted if they are not required by
125
+ an existing release.
126
+ DESC
127
+ task :clean_expired, :roles => lambda { assets_role }, :except => { :no_release => true } do
128
+ # Fetch all assets_manifest contents.
129
+ manifests_output = capture <<-CMD.compact
130
+ for manifest in #{releases_path.shellescape}/*/assets_manifest.*; do
131
+ cat -- "$manifest" 2> /dev/null && printf ':::' || true;
132
+ done
133
+ CMD
134
+ manifests = manifests_output.split(':::')
135
+
136
+ if manifests.empty?
137
+ logger.info "No manifests in #{releases_path}/*/assets_manifest.*"
138
+ else
139
+ logger.info "Fetched #{manifests.count} manifests from #{releases_path}/*/assets_manifest.*"
140
+ current_assets = Set.new
141
+ manifests.each do |content|
142
+ current_assets += parse_manifest(content)
143
+ end
144
+ current_assets += [File.basename(shared_manifest_path), "sources_manifest.yml"]
145
+
146
+ # Write the list of required assets to server.
147
+ logger.info "Writing required assets to #{deploy_to}/REQUIRED_ASSETS..."
148
+ escaped_assets = current_assets.sort.join("\n").gsub("\"", "\\\"") << "\n"
149
+ put escaped_assets, "#{deploy_to}/REQUIRED_ASSETS", :via => :scp
150
+
151
+ # Finds all files older than X minutes, then removes them if they are not referenced
152
+ # in REQUIRED_ASSETS.
153
+ expire_after_mins = (expire_assets_after.to_f / 60.0).to_i
154
+ logger.info "Removing assets that haven't been deployed for #{expire_after_mins} minutes..."
155
+ # LC_COLLATE=C tells the `sort` and `comm` commands to sort files in byte order.
156
+ run <<-CMD.compact
157
+ cd -- #{deploy_to.shellescape}/ &&
158
+ LC_COLLATE=C sort REQUIRED_ASSETS -o REQUIRED_ASSETS &&
159
+ cd -- #{shared_path.shellescape}/#{shared_assets_prefix}/ &&
160
+ for f in $(
161
+ find * -mmin +#{expire_after_mins.to_s.shellescape} -type f | LC_COLLATE=C sort |
162
+ LC_COLLATE=C comm -23 -- - #{deploy_to.shellescape}/REQUIRED_ASSETS
163
+ ); do
164
+ echo "Removing unneeded asset: $f";
165
+ rm -f -- "$f";
166
+ done;
167
+ rm -f -- #{deploy_to.shellescape}/REQUIRED_ASSETS
168
+ CMD
169
+ end
170
+ end
171
+
172
+ desc <<-DESC
173
+ Rolls back assets to the previous release by symlinking the release's manifest
174
+ to shared/assets/manifest, and finally recompiling or regenerating nondigest assets.
175
+ DESC
176
+ task :rollback, :roles => lambda { assets_role }, :except => { :no_release => true } do
177
+ previous_manifest = capture("ls #{previous_release.shellescape}/assets_manifest.*").strip
178
+ if capture("[ -e #{previous_manifest.shellescape} ] && echo true || echo false").strip != 'true'
179
+ puts "#{previous_manifest} is missing! Cannot roll back assets. " <<
180
+ "Please run deploy:assets:precompile to update your assets when the rollback is finished."
181
+ else
182
+ # If the user is rolling back a Rails 4 app to Rails 3
183
+ if File.extname(previous_manifest) == '.yml' && File.extname(shared_manifest_path) == '.json'
184
+ # Remove the existing JSON manifest
185
+ run "rm -f -- #{shared_manifest_path.shellescape}"
186
+
187
+ # Restore the manifest to the Rails 3 path
188
+ restored_manifest_path = "#{shared_path.shellescape}/#{shared_assets_prefix}/manifest.yml"
189
+ else
190
+ # If the user is not rolling back from Rails 4 to 3, we just want to replace the current manifest
191
+ restored_manifest_path = shared_manifest_path
192
+ end
193
+
194
+ run <<-CMD.compact
195
+ cd -- #{previous_release.shellescape} &&
196
+ cp -f -- #{previous_manifest.shellescape} #{restored_manifest_path.shellescape} &&
197
+ [ -z "$(#{rake} -P | grep assets:precompile:nondigest)" ] || #{rake} RAILS_ENV=#{rails_env.to_s.shellescape} #{asset_env} assets:precompile:nondigest
198
+ CMD
199
+ end
200
+ end
201
+ end
202
+ end
@@ -18,7 +18,7 @@ module Capistrano
18
18
  yield self
19
19
  self
20
20
  end
21
-
21
+
22
22
  def remote
23
23
  dep = RemoteDependency.new(configuration)
24
24
  @dependencies << dep
@@ -41,4 +41,4 @@ module Capistrano
41
41
  end
42
42
  end
43
43
  end
44
- end
44
+ end
@@ -30,7 +30,7 @@ module Capistrano
30
30
  # file is found that matches the parameter, this returns true.
31
31
  def find_in_path(utility)
32
32
  path = (ENV['PATH'] || "").split(File::PATH_SEPARATOR)
33
- suffixes = RUBY_PLATFORM =~ /mswin/ ? %w(.bat .exe .com .cmd) : [""]
33
+ suffixes = self.class.on_windows? ? self.class.windows_executable_extensions : [""]
34
34
 
35
35
  path.each do |dir|
36
36
  suffixes.each do |sfx|
@@ -41,6 +41,14 @@ module Capistrano
41
41
 
42
42
  false
43
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
44
52
  end
45
53
  end
46
- end
54
+ end
@@ -1,3 +1,5 @@
1
+ require 'capistrano/errors'
2
+
1
3
  module Capistrano
2
4
  module Deploy
3
5
  class RemoteDependency
@@ -7,6 +9,7 @@ module Capistrano
7
9
  def initialize(configuration)
8
10
  @configuration = configuration
9
11
  @success = true
12
+ @hosts = nil
10
13
  end
11
14
 
12
15
  def directory(path, options={})
@@ -15,6 +18,12 @@ module Capistrano
15
18
  self
16
19
  end
17
20
 
21
+ def file(path, options={})
22
+ @message ||= "`#{path}' is not a file"
23
+ try("test -f #{path}", options)
24
+ self
25
+ end
26
+
18
27
  def writable(path, options={})
19
28
  @message ||= "`#{path}' is not writable"
20
29
  try("test -w #{path}", options)
@@ -34,6 +43,48 @@ module Capistrano
34
43
  self
35
44
  end
36
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
+
37
88
  def or(message)
38
89
  @message = message
39
90
  self
@@ -45,7 +96,7 @@ module Capistrano
45
96
 
46
97
  def message
47
98
  s = @message.dup
48
- s << " (#{@hosts})" if @hosts && @hosts.any?
99
+ s << " (#{@hosts})" if @hosts
49
100
  s
50
101
  end
51
102
 
@@ -53,8 +104,9 @@ module Capistrano
53
104
 
54
105
  def try(command, options)
55
106
  return unless @success # short-circuit evaluation
56
- configuration.run(command, options) do |ch,stream,out|
107
+ configuration.invoke_command(command, options) do |ch,stream,out|
57
108
  warn "#{ch[:server]}: #{out}" if stream == :err
109
+ yield ch, stream, out if block_given?
58
110
  end
59
111
  rescue Capistrano::CommandError => e
60
112
  @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
@@ -54,7 +54,7 @@ module Capistrano
54
54
  # and "local_scm_command" to be set, if the two differ.
55
55
  #
56
56
  # Alternatively, it may be called with a block, and for the duration of
57
- # the block, all requests on this configuration object will be
57
+ # the block, all requests on this configuration object will be
58
58
  # considered local.
59
59
  def local
60
60
  if block_given?
@@ -114,6 +114,18 @@ module Capistrano
114
114
  raise NotImplementedError, "`query_revision' is not implemented by #{self.class.name}"
115
115
  end
116
116
 
117
+ # Returns the revision number immediately following revision, if at
118
+ # all possible. A block should always be passed to this method, which
119
+ # accepts a command to invoke and returns the result, although a
120
+ # particular SCM's implementation is not required to invoke the block.
121
+ #
122
+ # By default, this method simply returns the revision itself. If a
123
+ # particular SCM is able to determine a subsequent revision given a
124
+ # revision identifier, it should override this method.
125
+ def next_revision(revision)
126
+ revision
127
+ end
128
+
117
129
  # Should analyze the given text and determine whether or not a
118
130
  # response is expected, and if so, return the appropriate response.
119
131
  # If no response is expected, return nil. The +state+ parameter is a
@@ -138,15 +150,22 @@ module Capistrano
138
150
  command || default_command
139
151
  end
140
152
 
153
+ # A helper method that can be used to define SCM commands naturally.
154
+ # It returns a single string with all arguments joined by spaces,
155
+ # with the scm command prefixed onto it.
156
+ def scm(*args)
157
+ [command, *args].compact.join(" ")
158
+ end
159
+
141
160
  private
142
161
 
143
162
  # A helper for accessing variable values, which takes into
144
163
  # consideration the current mode ("normal" vs. "local").
145
- def variable(name)
164
+ def variable(name, default = nil)
146
165
  if local? && configuration.exists?("local_#{name}".to_sym)
147
- return configuration["local_#{name}".to_sym]
166
+ return configuration["local_#{name}".to_sym].nil? ? default : configuration["local_#{name}".to_sym]
148
167
  else
149
- configuration[name]
168
+ configuration[name].nil? ? default : configuration[name]
150
169
  end
151
170
  end
152
171
 
@@ -162,17 +181,18 @@ module Capistrano
162
181
  self.class.default_command
163
182
  end
164
183
 
165
- # A helper method that can be used to define SCM commands naturally.
166
- # It returns a single string with all arguments joined by spaces,
167
- # with the scm command prefixed onto it.
168
- def scm(*args)
169
- [command, *args].compact.join(" ")
170
- end
171
-
172
184
  # A convenience method for accessing the declared repository value.
173
185
  def repository
174
186
  variable(:repository)
175
187
  end
188
+
189
+ def arguments(command = :all)
190
+ value = variable(:scm_arguments)
191
+ if value.is_a?(Hash)
192
+ value = value[command]
193
+ end
194
+ value
195
+ end
176
196
  end
177
197
 
178
198
  end
@@ -21,7 +21,7 @@ module Capistrano
21
21
  # Returns the command that will check out the given revision to the
22
22
  # given destination.
23
23
  def checkout(revision, destination)
24
- scm :branch, revswitch(revision), repository, destination
24
+ scm :checkout, "--lightweight", revswitch(revision), repository, destination
25
25
  end
26
26
 
27
27
  # The bzr 'update' command does not support updating to a specific
@@ -33,13 +33,9 @@ module Capistrano
33
33
  commands.join(" && ")
34
34
  end
35
35
 
36
- # The bzr 'export' command would work fine, except it doesn't let you
37
- # specify the repository itself, so it only works if there is a working
38
- # tree handy, locally. Since this needs to work even on a remote host
39
- # where there is no working tree, we'll just do a checkout, followed
40
- # by a deletion of the ".bzr" metadata directory.
36
+ # The bzr 'export' does an export similar to other SCM systems
41
37
  def export(revision, destination)
42
- [checkout(revision, destination) && "rm -rf #{destination}/.bzr"].join(" && ")
38
+ scm :export, revswitch(revision), destination, repository
43
39
  end
44
40
 
45
41
  # The bzr "diff" command doesn't accept a repository argument, so it
@@ -63,20 +59,24 @@ module Capistrano
63
59
  # If the 'revision' argument, on the other hand, is not :head, it is
64
60
  # simply returned.
65
61
  def query_revision(revision)
66
- if revision == head
67
- yield(scm(:revno, repository)).chomp
68
- else
69
- revision
70
- end
62
+ return revision unless :head == revision
63
+
64
+ command = scm('revno', repository)
65
+ result = yield(command)
66
+ end
67
+
68
+ # Increments the given revision number and returns it.
69
+ def next_revision(revision)
70
+ revision.to_i + 1
71
71
  end
72
72
 
73
73
  private
74
74
 
75
75
  def revswitch(revision)
76
- if revision == :head
76
+ if revision == :head || revision.nil?
77
77
  nil
78
78
  else
79
- "-r #{revision}"
79
+ "-r #{revision}".chomp
80
80
  end
81
81
  end
82
82
  end