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,152 @@
1
+ require 'capistrano/recipes/deploy/scm/base'
2
+
3
+ # Notes:
4
+ # no global verbose flag for scm_verbose
5
+ # sync, checkout and export are just sync in p4
6
+ #
7
+ module Capistrano
8
+ module Deploy
9
+ module SCM
10
+
11
+ # Implements the Capistrano SCM interface for the Perforce revision
12
+ # control system (http://www.perforce.com).
13
+ class Perforce < Base
14
+ # Sets the default command name for this SCM. Users may override this
15
+ # by setting the :scm_command variable.
16
+ default_command "p4"
17
+
18
+ # Perforce understands '#head' to refer to the latest revision in the
19
+ # depot.
20
+ def head
21
+ 'head'
22
+ end
23
+
24
+ # Returns the command that will sync the given revision to the given
25
+ # destination directory. The perforce client has a fixed destination so
26
+ # the files must be copied from there to their intended resting place.
27
+ def checkout(revision, destination)
28
+ p4_sync(revision, destination, p4sync_flags)
29
+ end
30
+
31
+ # Returns the command that will sync the given revision to the given
32
+ # destination directory. The perforce client has a fixed destination so
33
+ # the files must be copied from there to their intended resting place.
34
+ def sync(revision, destination)
35
+ p4_sync(revision, destination, p4sync_flags)
36
+ end
37
+
38
+ # Returns the command that will sync the given revision to the given
39
+ # destination directory. The perforce client has a fixed destination so
40
+ # the files must be copied from there to their intended resting place.
41
+ def export(revision, destination)
42
+ p4_sync(revision, destination, p4sync_flags)
43
+ end
44
+
45
+ # Returns the command that will do an "p4 diff2" for the two revisions.
46
+ def diff(from, to=head)
47
+ scm authentication, :diff2, "-u -db", "//#{p4client}/...#{rev_no(from)}", "//#{p4client}/...#{rev_no(to)}"
48
+ end
49
+
50
+ # Returns a "p4 changes" command for the two revisions.
51
+ def log(from=1, to=head)
52
+ scm authentication, :changes, "-s submitted", "//#{p4client}/...#{rev_no(from)},#{rev_no(to)}"
53
+ end
54
+
55
+ def query_revision(revision)
56
+ return revision if revision.to_s =~ /^\d+$/
57
+ command = scm(authentication, :changes, "-s submitted", "-m 1", "//#{p4client}/...#{rev_no(revision)}")
58
+ yield(command)[/Change (\d+) on/, 1]
59
+ end
60
+
61
+ # Increments the given revision number and returns it.
62
+ def next_revision(revision)
63
+ revision.to_i + 1
64
+ end
65
+
66
+ # Determines what the response should be for a particular bit of text
67
+ # from the SCM. Password prompts, connection requests, passphrases,
68
+ # etc. are handled here.
69
+ def handle_data(state, stream, text)
70
+ case text
71
+ when /\(P4PASSWD\) invalid or unset\./i
72
+ raise Capistrano::Error, "scm_password (or p4passwd) is incorrect or unset"
73
+ when /Can.t create a new user.*/i
74
+ raise Capistrano::Error, "scm_username (or p4user) is incorrect or unset"
75
+ when /Perforce client error\:/i
76
+ raise Capistrano::Error, "p4port is incorrect or unset"
77
+ when /Client \'[\w\-\_\.]+\' unknown.*/i
78
+ raise Capistrano::Error, "p4client is incorrect or unset"
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ # Builds the set of authentication switches that perforce understands.
85
+ def authentication
86
+ [ p4port && "-p #{p4port}",
87
+ p4user && "-u #{p4user}",
88
+ p4passwd && "-P #{p4passwd}",
89
+ p4client && "-c #{p4client}",
90
+ p4charset && "-C #{p4charset}" ].compact.join(" ")
91
+ end
92
+
93
+ # Returns the command that will sync the given revision to the given
94
+ # destination directory with specific options. The perforce client has
95
+ # a fixed destination so the files must be copied from there to their
96
+ # intended resting place.
97
+ def p4_sync(revision, destination, options="")
98
+ scm authentication, :sync, options, "#{rev_no(revision)}", "&& cp -rf #{p4client_root} #{destination}"
99
+ end
100
+
101
+ def p4client
102
+ variable(:p4client)
103
+ end
104
+
105
+ def p4port
106
+ variable(:p4port)
107
+ end
108
+
109
+ def p4user
110
+ variable(:p4user) || variable(:scm_username)
111
+ end
112
+
113
+ def p4passwd
114
+ variable(:p4passwd) || variable(:scm_password)
115
+ end
116
+
117
+ def p4charset
118
+ variable(:p4charset)
119
+ end
120
+
121
+ def p4sync_flags
122
+ variable(:p4sync_flags) || "-f"
123
+ end
124
+
125
+ def p4client_root
126
+ variable(:p4client_root) || "`#{command} #{authentication} client -o | grep ^Root | cut -f2`"
127
+ end
128
+
129
+ def rev_no(revision)
130
+ if variable(:p4_label)
131
+ p4_label = if variable(:p4_label) =~ /\A@/
132
+ variable(:p4_label)
133
+ else
134
+ "@#{variable(:p4_label)}"
135
+ end
136
+ return p4_label
137
+ end
138
+
139
+ case revision.to_s
140
+ when "head"
141
+ "#head"
142
+ when /^\d+/
143
+ "@#{revision}"
144
+ else
145
+ revision
146
+ end
147
+ end
148
+ end
149
+
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,121 @@
1
+ require 'capistrano/recipes/deploy/scm/base'
2
+ require 'yaml'
3
+
4
+ module Capistrano
5
+ module Deploy
6
+ module SCM
7
+
8
+ # Implements the Capistrano SCM interface for the Subversion revision
9
+ # control system (http://subversion.tigris.org).
10
+ class Subversion < Base
11
+ # Sets the default command name for this SCM. Users may override this
12
+ # by setting the :scm_command variable.
13
+ default_command "svn"
14
+
15
+ # Subversion understands 'HEAD' to refer to the latest revision in the
16
+ # repository.
17
+ def head
18
+ "HEAD"
19
+ end
20
+
21
+ # Returns the command that will check out the given revision to the
22
+ # given destination.
23
+ def checkout(revision, destination)
24
+ scm :checkout, arguments, arguments(:checkout), verbose, authentication, "-r#{revision}", repository, destination
25
+ end
26
+
27
+ # Returns the command that will do an "svn update" to the given
28
+ # revision, for the working copy at the given destination.
29
+ def sync(revision, destination)
30
+ scm :switch, arguments, verbose, authentication, "-r#{revision}", repository, destination
31
+ end
32
+
33
+ # Returns the command that will do an "svn export" of the given revision
34
+ # to the given destination.
35
+ def export(revision, destination)
36
+ scm :export, arguments, arguments(:export), verbose, authentication, "-r#{revision}", repository, destination
37
+ end
38
+
39
+ # Returns the command that will do an "svn diff" for the two revisions.
40
+ def diff(from, to=nil)
41
+ scm :diff, repository, arguments(:diff), authentication, "-r#{from}:#{to || head}"
42
+ end
43
+
44
+ # Returns an "svn log" command for the two revisions.
45
+ def log(from, to=nil)
46
+ scm :log, repository, arguments(:log), authentication, "-r#{from}:#{to || head}"
47
+ end
48
+
49
+ # Attempts to translate the given revision identifier to a "real"
50
+ # revision. If the identifier is an integer, it will simply be returned.
51
+ # Otherwise, this will yield a string of the commands it needs to be
52
+ # executed (svn info), and will extract the revision from the response.
53
+ def query_revision(revision)
54
+ return revision if revision =~ /^\d+$/
55
+ command = scm(:info, arguments, arguments(:info), repository, authentication, "-r#{revision}")
56
+ result = yield(command)
57
+ yaml = YAML.load(result)
58
+ raise "tried to run `#{command}' and got unexpected result #{result.inspect}" unless Hash === yaml
59
+ [ (yaml['Last Changed Rev'] || 0).to_i, (yaml['Revision'] || 0).to_i ].max
60
+ end
61
+
62
+ # Increments the given revision number and returns it.
63
+ def next_revision(revision)
64
+ revision.to_i + 1
65
+ end
66
+
67
+ # Determines what the response should be for a particular bit of text
68
+ # from the SCM. Password prompts, connection requests, passphrases,
69
+ # etc. are handled here.
70
+ def handle_data(state, stream, text)
71
+ host = state[:channel][:host]
72
+ logger.info "[#{host} :: #{stream}] #{text}"
73
+ case text
74
+ when /\bpassword.*:/i
75
+ # subversion is prompting for a password
76
+ "#{scm_password_prompt}\n"
77
+ when %r{\(yes/no\)}
78
+ # subversion is asking whether or not to connect
79
+ "yes\n"
80
+ when /passphrase/i
81
+ # subversion is asking for the passphrase for the user's key
82
+ "#{variable(:scm_passphrase)}\n"
83
+ when /The entry \'(.+?)\' is no longer a directory/
84
+ raise Capistrano::Error, "subversion can't update because directory '#{$1}' was replaced. Please add it to svn:ignore."
85
+ when /accept \(t\)emporarily/
86
+ # subversion is asking whether to accept the certificate
87
+ "t\n"
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ # If a username is configured for the SCM, return the command-line
94
+ # switches for that. Note that we don't need to return the password
95
+ # switch, since Capistrano will check for that prompt in the output
96
+ # and will respond appropriately.
97
+ def authentication
98
+ username = variable(:scm_username)
99
+ return "" unless username
100
+ result = "--username #{variable(:scm_username)} "
101
+ result << "--password #{variable(:scm_password)} " unless variable(:scm_auth_cache) || variable(:scm_prefer_prompt)
102
+ result << "--no-auth-cache " unless variable(:scm_auth_cache)
103
+ result
104
+ end
105
+
106
+ # If verbose output is requested, return nil, otherwise return the
107
+ # command-line switch for "quiet" ("-q").
108
+ def verbose
109
+ variable(:scm_verbose) ? nil : "-q"
110
+ end
111
+
112
+ def scm_password_prompt
113
+ @scm_password_prompt ||= variable(:scm_password) ||
114
+ variable(:password) ||
115
+ Capistrano::CLI.password_prompt("Subversion password: ")
116
+ end
117
+ end
118
+
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,19 @@
1
+ module Capistrano
2
+ module Deploy
3
+ module Strategy
4
+ def self.new(strategy, config={})
5
+ strategy_file = "capistrano/recipes/deploy/strategy/#{strategy}"
6
+ require(strategy_file)
7
+
8
+ strategy_const = strategy.to_s.capitalize.gsub(/_(.)/) { $1.upcase }
9
+ if const_defined?(strategy_const)
10
+ const_get(strategy_const).new(config)
11
+ else
12
+ raise Capistrano::Error, "could not find `#{name}::#{strategy_const}' in `#{strategy_file}'"
13
+ end
14
+ rescue LoadError
15
+ raise Capistrano::Error, "could not find any strategy named `#{strategy}'"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,92 @@
1
+ require 'benchmark'
2
+ require 'capistrano/recipes/deploy/dependencies'
3
+
4
+ module Capistrano
5
+ module Deploy
6
+ module Strategy
7
+
8
+ # This class defines the abstract interface for all Capistrano
9
+ # deployment strategies. Subclasses must implement at least the
10
+ # #deploy! method.
11
+ class Base
12
+ attr_reader :configuration
13
+
14
+ # Instantiates a strategy with a reference to the given configuration.
15
+ def initialize(config={})
16
+ @configuration = config
17
+ end
18
+
19
+ # Executes the necessary commands to deploy the revision of the source
20
+ # code identified by the +revision+ variable. Additionally, this
21
+ # should write the value of the +revision+ variable to a file called
22
+ # REVISION, in the base of the deployed revision. This file is used by
23
+ # other tasks, to perform diffs and such.
24
+ def deploy!
25
+ raise NotImplementedError, "`deploy!' is not implemented by #{self.class.name}"
26
+ end
27
+
28
+ # Performs a check on the remote hosts to determine whether everything
29
+ # is setup such that a deploy could succeed.
30
+ def check!
31
+ Dependencies.new(configuration) do |d|
32
+ if exists?(:stage)
33
+ d.remote.directory(configuration[:releases_path]).or("`#{configuration[:releases_path]}' does not exist. Please run `cap #{configuration[:stage]} deploy:setup'.")
34
+ else
35
+ d.remote.directory(configuration[:releases_path]).or("`#{configuration[:releases_path]}' does not exist. Please run `cap deploy:setup'.")
36
+ end
37
+ d.remote.writable(configuration[:deploy_to]).or("You do not have permissions to write to `#{configuration[:deploy_to]}'.")
38
+ d.remote.writable(configuration[:releases_path]).or("You do not have permissions to write to `#{configuration[:releases_path]}'.")
39
+ end
40
+ end
41
+
42
+ protected
43
+
44
+ # This is to allow helper methods like "run" and "put" to be more
45
+ # easily accessible to strategy implementations.
46
+ def method_missing(sym, *args, &block)
47
+ if configuration.respond_to?(sym)
48
+ configuration.send(sym, *args, &block)
49
+ else
50
+ super
51
+ end
52
+ end
53
+
54
+ # A wrapper for Kernel#system that logs the command being executed.
55
+ def system(*args)
56
+ cmd = args.join(' ')
57
+ result = nil
58
+ if RUBY_PLATFORM =~ /win32/
59
+ cmd = cmd.split(/\s+/).collect {|w| w.match(/^[\w+]+:\/\//) ? w : w.gsub('/', '\\') }.join(' ') # Split command by spaces, change / by \\ unless element is a some+thing://
60
+ cmd.gsub!(/^cd /,'cd /D ') # Replace cd with cd /D
61
+ cmd.gsub!(/&& cd /,'&& cd /D ') # Replace cd with cd /D
62
+ logger.trace "executing locally: #{cmd}"
63
+ elapsed = Benchmark.realtime do
64
+ result = super(cmd)
65
+ end
66
+ else
67
+ logger.trace "executing locally: #{cmd}"
68
+ elapsed = Benchmark.realtime do
69
+ result = super
70
+ end
71
+ end
72
+
73
+ logger.trace "command finished in #{(elapsed * 1000).round}ms"
74
+ result
75
+ end
76
+
77
+ private
78
+
79
+ def logger
80
+ @logger ||= configuration.logger || Capistrano::Logger.new(:output => STDOUT)
81
+ end
82
+
83
+ # The revision to deploy. Must return a real revision identifier,
84
+ # and not a pseudo-id.
85
+ def revision
86
+ configuration[:real_revision]
87
+ end
88
+ end
89
+
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,20 @@
1
+ require 'capistrano/recipes/deploy/strategy/remote'
2
+
3
+ module Capistrano
4
+ module Deploy
5
+ module Strategy
6
+
7
+ # Implements the deployment strategy which does an SCM checkout on each
8
+ # target host. This is the default deployment strategy for Capistrano.
9
+ class Checkout < Remote
10
+ protected
11
+
12
+ # Returns the SCM's checkout command for the revision to deploy.
13
+ def command
14
+ @command ||= source.checkout(revision, configuration[:release_path])
15
+ end
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,338 @@
1
+ require 'capistrano/recipes/deploy/strategy/base'
2
+ require 'fileutils'
3
+ require 'tempfile' # Dir.tmpdir
4
+
5
+ module Capistrano
6
+ module Deploy
7
+ module Strategy
8
+
9
+ # This class implements the strategy for deployments which work
10
+ # by preparing the source code locally, compressing it, copying the
11
+ # file to each target host, and uncompressing it to the deployment
12
+ # directory.
13
+ #
14
+ # By default, the SCM checkout command is used to obtain the local copy
15
+ # of the source code. If you would rather use the export operation,
16
+ # you can set the :copy_strategy variable to :export.
17
+ #
18
+ # set :copy_strategy, :export
19
+ #
20
+ # For even faster deployments, you can set the :copy_cache variable to
21
+ # true. This will cause deployments to do a new checkout of your
22
+ # repository to a new directory, and then copy that checkout. Subsequent
23
+ # deploys will just resync that copy, rather than doing an entirely new
24
+ # checkout. Additionally, you can specify file patterns to exclude from
25
+ # the copy when using :copy_cache; just set the :copy_exclude variable
26
+ # to a file glob (or an array of globs).
27
+ #
28
+ # set :copy_cache, true
29
+ # set :copy_exclude, ".git/*"
30
+ #
31
+ # Note that :copy_strategy is ignored when :copy_cache is set. Also, if
32
+ # you want the copy cache put somewhere specific, you can set the variable
33
+ # to the path you want, instead of merely 'true':
34
+ #
35
+ # set :copy_cache, "/tmp/caches/myapp"
36
+ #
37
+ # This deployment strategy also supports a special variable,
38
+ # :copy_compression, which must be one of :gzip, :bz2, or
39
+ # :zip, and which specifies how the source should be compressed for
40
+ # transmission to each host.
41
+ #
42
+ # By default, files will be transferred across to the remote machines via 'sftp'. If you prefer
43
+ # to use 'scp' you can set the :copy_via variable to :scp.
44
+ #
45
+ # set :copy_via, :scp
46
+ #
47
+ # There is a possibility to pass a build command that will get
48
+ # executed if your code needs to be compiled or something needs to be
49
+ # done before the code is ready to run.
50
+ #
51
+ # set :build_script, "make all"
52
+ #
53
+ # Note that if you use :copy_cache, the :build_script is used on the
54
+ # cache and thus you get faster compilation if your script does not
55
+ # recompile everything.
56
+ class Copy < Base
57
+ # Obtains a copy of the source code locally (via the #command method),
58
+ # compresses it to a single file, copies that file to all target
59
+ # servers, and uncompresses it on each of them into the deployment
60
+ # directory.
61
+ def deploy!
62
+ copy_cache ? run_copy_cache_strategy : run_copy_strategy
63
+
64
+ create_revision_file
65
+ compress_repository
66
+ distribute!
67
+ ensure
68
+ rollback_changes
69
+ end
70
+
71
+ def build directory
72
+ execute "running build script on #{directory}" do
73
+ Dir.chdir(directory) { system(build_script) }
74
+ end if build_script
75
+ end
76
+
77
+ def check!
78
+ super.check do |d|
79
+ d.local.command(source.local.command) if source.local.command
80
+ d.local.command(compress(nil, nil).first)
81
+ d.remote.command(decompress(nil).first)
82
+ end
83
+ end
84
+
85
+ # Returns the location of the local copy cache, if the strategy should
86
+ # use a local cache + copy instead of a new checkout/export every
87
+ # time. Returns +nil+ unless :copy_cache has been set. If :copy_cache
88
+ # is +true+, a default cache location will be returned.
89
+ def copy_cache
90
+ @copy_cache ||= configuration[:copy_cache] == true ?
91
+ File.expand_path(configuration[:application], Dir.tmpdir) :
92
+ File.expand_path(configuration[:copy_cache], Dir.pwd) rescue nil
93
+ end
94
+
95
+ private
96
+
97
+ def run_copy_cache_strategy
98
+ copy_repository_to_local_cache
99
+ build copy_cache
100
+ copy_cache_to_staging_area
101
+ end
102
+
103
+ def run_copy_strategy
104
+ copy_repository_to_server
105
+ build destination
106
+ remove_excluded_files if copy_exclude.any?
107
+ end
108
+
109
+ def execute description, &block
110
+ logger.debug description
111
+ handle_system_errors &block
112
+ end
113
+
114
+ def handle_system_errors &block
115
+ block.call
116
+ raise_command_failed if last_command_failed?
117
+ end
118
+
119
+ def refresh_local_cache
120
+ execute "refreshing local cache to revision #{revision} at #{copy_cache}" do
121
+ system(source.sync(revision, copy_cache))
122
+ end
123
+ end
124
+
125
+ def create_local_cache
126
+ execute "preparing local cache at #{copy_cache}" do
127
+ system(source.checkout(revision, copy_cache))
128
+ end
129
+ end
130
+
131
+ def raise_command_failed
132
+ raise Capistrano::Error, "shell command failed with return code #{$?}"
133
+ end
134
+
135
+ def last_command_failed?
136
+ $? != 0
137
+ end
138
+
139
+ def copy_cache_to_staging_area
140
+ execute "copying cache to deployment staging area #{destination}" do
141
+ create_destination
142
+ Dir.chdir(copy_cache) { copy_files(queue_files) }
143
+ end
144
+ end
145
+
146
+ def create_destination
147
+ FileUtils.mkdir_p(destination)
148
+ end
149
+
150
+ def copy_files files
151
+ files.each { |name| process_file(name) }
152
+ end
153
+
154
+ def process_file name
155
+ send "copy_#{filetype(name)}", name
156
+ end
157
+
158
+ def filetype name
159
+ filetype = File.ftype name
160
+ filetype = "file" unless ["link", "directory"].include? filetype
161
+ filetype
162
+ end
163
+
164
+ def copy_link name
165
+ FileUtils.ln_s(File.readlink(name), File.join(destination, name))
166
+ end
167
+
168
+ def copy_directory name
169
+ FileUtils.mkdir(File.join(destination, name))
170
+ copy_files(queue_files(name))
171
+ end
172
+
173
+ def copy_file name
174
+ FileUtils.ln(name, File.join(destination, name))
175
+ end
176
+
177
+ def queue_files directory=nil
178
+ Dir.glob(pattern_for(directory), File::FNM_DOTMATCH).reject! { |file| excluded_files_contain? file }
179
+ end
180
+
181
+ def pattern_for directory
182
+ !directory.nil? ? "#{escape_globs(directory)}/*" : "*"
183
+ end
184
+
185
+ def escape_globs path
186
+ path.gsub(/[*?{}\[\]]/, '\\\\\\&')
187
+ end
188
+
189
+ def excluded_files_contain? file
190
+ copy_exclude.any? { |p| File.fnmatch(p, file) } or [ ".", ".."].include? File.basename(file)
191
+ end
192
+
193
+ def copy_repository_to_server
194
+ execute "getting (via #{copy_strategy}) revision #{revision} to #{destination}" do
195
+ copy_repository_via_strategy
196
+ end
197
+ end
198
+
199
+ def copy_repository_via_strategy
200
+ system(command)
201
+ end
202
+
203
+ def remove_excluded_files
204
+ logger.debug "processing exclusions..."
205
+
206
+ copy_exclude.each do |pattern|
207
+ delete_list = Dir.glob(File.join(destination, pattern), File::FNM_DOTMATCH)
208
+ # avoid the /.. trap that deletes the parent directories
209
+ delete_list.delete_if { |dir| dir =~ /\/\.\.$/ }
210
+ FileUtils.rm_rf(delete_list.compact)
211
+ end
212
+ end
213
+
214
+ def create_revision_file
215
+ File.open(File.join(destination, "REVISION"), "w") { |f| f.puts(revision) }
216
+ end
217
+
218
+ def compress_repository
219
+ execute "Compressing #{destination} to #{filename}" do
220
+ Dir.chdir(copy_dir) { system(compress(File.basename(destination), File.basename(filename)).join(" ")) }
221
+ end
222
+ end
223
+
224
+ def rollback_changes
225
+ FileUtils.rm filename rescue nil
226
+ FileUtils.rm_rf destination rescue nil
227
+ end
228
+
229
+ def copy_repository_to_local_cache
230
+ return refresh_local_cache if File.exists?(copy_cache)
231
+ create_local_cache
232
+ end
233
+
234
+ def build_script
235
+ configuration[:build_script]
236
+ end
237
+
238
+ # Specify patterns to exclude from the copy. This is only valid
239
+ # when using a local cache.
240
+ def copy_exclude
241
+ @copy_exclude ||= Array(configuration.fetch(:copy_exclude, []))
242
+ end
243
+
244
+ # Returns the basename of the release_path, which will be used to
245
+ # name the local copy and archive file.
246
+ def destination
247
+ @destination ||= File.join(copy_dir, File.basename(configuration[:release_path]))
248
+ end
249
+
250
+ # Returns the value of the :copy_strategy variable, defaulting to
251
+ # :checkout if it has not been set.
252
+ def copy_strategy
253
+ @copy_strategy ||= configuration.fetch(:copy_strategy, :checkout)
254
+ end
255
+
256
+ # Should return the command(s) necessary to obtain the source code
257
+ # locally.
258
+ def command
259
+ @command ||= case copy_strategy
260
+ when :checkout
261
+ source.checkout(revision, destination)
262
+ when :export
263
+ source.export(revision, destination)
264
+ end
265
+ end
266
+
267
+ # Returns the name of the file that the source code will be
268
+ # compressed to.
269
+ def filename
270
+ @filename ||= File.join(copy_dir, "#{File.basename(destination)}.#{compression.extension}")
271
+ end
272
+
273
+ # The directory to which the copy should be checked out
274
+ def copy_dir
275
+ @copy_dir ||= File.expand_path(configuration[:copy_dir] || Dir.tmpdir, Dir.pwd)
276
+ end
277
+
278
+ # The directory on the remote server to which the archive should be
279
+ # copied
280
+ def remote_dir
281
+ @remote_dir ||= configuration[:copy_remote_dir] || "/tmp"
282
+ end
283
+
284
+ # The location on the remote server where the file should be
285
+ # temporarily stored.
286
+ def remote_filename
287
+ @remote_filename ||= File.join(remote_dir, File.basename(filename))
288
+ end
289
+
290
+ # A struct for representing the specifics of a compression type.
291
+ # Commands are arrays, where the first element is the utility to be
292
+ # used to perform the compression or decompression.
293
+ Compression = Struct.new(:extension, :compress_command, :decompress_command)
294
+
295
+ # The compression method to use, defaults to :gzip.
296
+ def compression
297
+ remote_tar = configuration[:copy_remote_tar] || 'tar'
298
+ local_tar = configuration[:copy_local_tar] || 'tar'
299
+
300
+ type = configuration[:copy_compression] || :gzip
301
+ case type
302
+ when :gzip, :gz then Compression.new("tar.gz", [local_tar, 'czf'], [remote_tar, 'xzf'])
303
+ when :bzip2, :bz2 then Compression.new("tar.bz2", [local_tar, 'cjf'], [remote_tar, 'xjf'])
304
+ when :zip then Compression.new("zip", %w(zip -qyr), %w(unzip -q))
305
+ else raise ArgumentError, "invalid compression type #{type.inspect}"
306
+ end
307
+ end
308
+
309
+ # Returns the command necessary to compress the given directory
310
+ # into the given file.
311
+ def compress(directory, file)
312
+ compression.compress_command + [file, directory]
313
+ end
314
+
315
+ # Returns the command necessary to decompress the given file,
316
+ # relative to the current working directory. It must also
317
+ # preserve the directory structure in the file.
318
+ def decompress(file)
319
+ compression.decompress_command + [file]
320
+ end
321
+
322
+ def decompress_remote_file
323
+ run "cd #{configuration[:releases_path]} && #{decompress(remote_filename).join(" ")} && rm #{remote_filename}"
324
+ end
325
+
326
+ # Distributes the file to the remote servers
327
+ def distribute!
328
+ args = [filename, remote_filename]
329
+ args << { :via => configuration[:copy_via] } if configuration[:copy_via]
330
+
331
+ upload(*args)
332
+ decompress_remote_file
333
+ end
334
+ end
335
+
336
+ end
337
+ end
338
+ end