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