capistrano 1.4.2 → 2.0.0

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 (113) hide show
  1. data/CHANGELOG +140 -4
  2. data/MIT-LICENSE +1 -1
  3. data/README +22 -14
  4. data/bin/cap +1 -8
  5. data/bin/capify +77 -0
  6. data/examples/sample.rb +10 -109
  7. data/lib/capistrano.rb +1 -0
  8. data/lib/capistrano/callback.rb +41 -0
  9. data/lib/capistrano/cli.rb +17 -317
  10. data/lib/capistrano/cli/execute.rb +82 -0
  11. data/lib/capistrano/cli/help.rb +102 -0
  12. data/lib/capistrano/cli/help.txt +53 -0
  13. data/lib/capistrano/cli/options.rb +183 -0
  14. data/lib/capistrano/cli/ui.rb +28 -0
  15. data/lib/capistrano/command.rb +62 -29
  16. data/lib/capistrano/configuration.rb +25 -226
  17. data/lib/capistrano/configuration/actions/file_transfer.rb +35 -0
  18. data/lib/capistrano/configuration/actions/inspect.rb +46 -0
  19. data/lib/capistrano/configuration/actions/invocation.rb +127 -0
  20. data/lib/capistrano/configuration/callbacks.rb +148 -0
  21. data/lib/capistrano/configuration/connections.rb +159 -0
  22. data/lib/capistrano/configuration/execution.rb +126 -0
  23. data/lib/capistrano/configuration/loading.rb +112 -0
  24. data/lib/capistrano/configuration/namespaces.rb +190 -0
  25. data/lib/capistrano/configuration/roles.rb +51 -0
  26. data/lib/capistrano/configuration/servers.rb +75 -0
  27. data/lib/capistrano/configuration/variables.rb +127 -0
  28. data/lib/capistrano/errors.rb +15 -0
  29. data/lib/capistrano/extensions.rb +27 -8
  30. data/lib/capistrano/gateway.rb +54 -29
  31. data/lib/capistrano/logger.rb +11 -11
  32. data/lib/capistrano/recipes/compat.rb +32 -0
  33. data/lib/capistrano/recipes/deploy.rb +483 -0
  34. data/lib/capistrano/recipes/deploy/dependencies.rb +44 -0
  35. data/lib/capistrano/recipes/deploy/local_dependency.rb +46 -0
  36. data/lib/capistrano/recipes/deploy/remote_dependency.rb +65 -0
  37. data/lib/capistrano/recipes/deploy/scm.rb +19 -0
  38. data/lib/capistrano/recipes/deploy/scm/base.rb +180 -0
  39. data/lib/capistrano/recipes/deploy/scm/bzr.rb +86 -0
  40. data/lib/capistrano/recipes/deploy/scm/cvs.rb +151 -0
  41. data/lib/capistrano/recipes/deploy/scm/darcs.rb +85 -0
  42. data/lib/capistrano/recipes/deploy/scm/mercurial.rb +129 -0
  43. data/lib/capistrano/recipes/deploy/scm/perforce.rb +126 -0
  44. data/lib/capistrano/recipes/deploy/scm/subversion.rb +103 -0
  45. data/lib/capistrano/recipes/deploy/strategy.rb +19 -0
  46. data/lib/capistrano/recipes/deploy/strategy/base.rb +64 -0
  47. data/lib/capistrano/recipes/deploy/strategy/checkout.rb +20 -0
  48. data/lib/capistrano/recipes/deploy/strategy/copy.rb +143 -0
  49. data/lib/capistrano/recipes/deploy/strategy/export.rb +20 -0
  50. data/lib/capistrano/recipes/deploy/strategy/remote.rb +52 -0
  51. data/lib/capistrano/recipes/deploy/strategy/remote_cache.rb +47 -0
  52. data/lib/capistrano/recipes/deploy/templates/maintenance.rhtml +53 -0
  53. data/lib/capistrano/recipes/standard.rb +26 -276
  54. data/lib/capistrano/recipes/templates/maintenance.rhtml +1 -1
  55. data/lib/capistrano/recipes/upgrade.rb +33 -0
  56. data/lib/capistrano/server_definition.rb +51 -0
  57. data/lib/capistrano/shell.rb +125 -81
  58. data/lib/capistrano/ssh.rb +80 -36
  59. data/lib/capistrano/task_definition.rb +69 -0
  60. data/lib/capistrano/upload.rb +146 -0
  61. data/lib/capistrano/version.rb +13 -17
  62. data/test/cli/execute_test.rb +132 -0
  63. data/test/cli/help_test.rb +139 -0
  64. data/test/cli/options_test.rb +226 -0
  65. data/test/cli/ui_test.rb +28 -0
  66. data/test/cli_test.rb +17 -0
  67. data/test/command_test.rb +284 -25
  68. data/test/configuration/actions/file_transfer_test.rb +40 -0
  69. data/test/configuration/actions/inspect_test.rb +62 -0
  70. data/test/configuration/actions/invocation_test.rb +195 -0
  71. data/test/configuration/callbacks_test.rb +206 -0
  72. data/test/configuration/connections_test.rb +288 -0
  73. data/test/configuration/execution_test.rb +159 -0
  74. data/test/configuration/loading_test.rb +119 -0
  75. data/test/configuration/namespace_dsl_test.rb +283 -0
  76. data/test/configuration/roles_test.rb +47 -0
  77. data/test/configuration/servers_test.rb +90 -0
  78. data/test/configuration/variables_test.rb +180 -0
  79. data/test/configuration_test.rb +60 -212
  80. data/test/deploy/scm/base_test.rb +55 -0
  81. data/test/deploy/strategy/copy_test.rb +146 -0
  82. data/test/extensions_test.rb +69 -0
  83. data/test/fixtures/cli_integration.rb +5 -0
  84. data/test/fixtures/custom.rb +2 -2
  85. data/test/gateway_test.rb +167 -0
  86. data/test/logger_test.rb +123 -0
  87. data/test/server_definition_test.rb +108 -0
  88. data/test/shell_test.rb +64 -0
  89. data/test/ssh_test.rb +67 -154
  90. data/test/task_definition_test.rb +101 -0
  91. data/test/upload_test.rb +131 -0
  92. data/test/utils.rb +31 -39
  93. data/test/version_test.rb +24 -0
  94. metadata +145 -98
  95. data/THANKS +0 -4
  96. data/lib/capistrano/actor.rb +0 -567
  97. data/lib/capistrano/generators/rails/deployment/deployment_generator.rb +0 -25
  98. data/lib/capistrano/generators/rails/deployment/templates/capistrano.rake +0 -49
  99. data/lib/capistrano/generators/rails/deployment/templates/deploy.rb +0 -122
  100. data/lib/capistrano/generators/rails/loader.rb +0 -20
  101. data/lib/capistrano/scm/base.rb +0 -61
  102. data/lib/capistrano/scm/baz.rb +0 -118
  103. data/lib/capistrano/scm/bzr.rb +0 -70
  104. data/lib/capistrano/scm/cvs.rb +0 -129
  105. data/lib/capistrano/scm/darcs.rb +0 -27
  106. data/lib/capistrano/scm/mercurial.rb +0 -83
  107. data/lib/capistrano/scm/perforce.rb +0 -139
  108. data/lib/capistrano/scm/subversion.rb +0 -128
  109. data/lib/capistrano/transfer.rb +0 -97
  110. data/lib/capistrano/utils.rb +0 -26
  111. data/test/actor_test.rb +0 -402
  112. data/test/scm/cvs_test.rb +0 -196
  113. data/test/scm/subversion_test.rb +0 -145
@@ -0,0 +1,85 @@
1
+ require 'capistrano/recipes/deploy/scm/base'
2
+
3
+ module Capistrano
4
+ module Deploy
5
+ module SCM
6
+
7
+ # Implements the Capistrano SCM interface for the darcs revision
8
+ # control system (http://www.abridgegame.org/darcs/).
9
+ class Darcs < Base
10
+ # Sets the default command name for this SCM. Users may override this
11
+ # by setting the :scm_command variable.
12
+ default_command "darcs"
13
+
14
+ # Because darcs does not have any support for pseudo-ids, we'll just
15
+ # return something here that we can use in the helpers below for
16
+ # determining whether we need to look up the latest revision.
17
+ def head
18
+ :head
19
+ end
20
+
21
+ # Returns the command that will check out the given revision to the
22
+ # given destination. The 'revision' parameter must be the 'hash' value
23
+ # for the revision in question, as given by 'darcs changes --xml-output'.
24
+ def checkout(revision, destination)
25
+ scm :get, verbose, "--repo-name=#{destination}", "--to-match='hash #{revision}'", repository
26
+ end
27
+
28
+ # Tries to update the destination repository in-place, to bring it up
29
+ # to the given revision. Note that because darcs' "pull" operation
30
+ # does not support a "to-match" argument (or similar), this basically
31
+ # nukes the destination directory and re-gets it.
32
+ def sync(revision, destination)
33
+ ["rm -rf #{destination}", checkout(revision, destination)].join(" && ")
34
+ end
35
+
36
+ # Darcs does not have a real 'export' option; there is 'darcs dist',
37
+ # but that presupposes a utility that can untar and ungzip the dist
38
+ # file. We'll cheat and just do a get, followed by a deletion of the
39
+ # _darcs metadata directory.
40
+ def export(revision, destination)
41
+ [checkout(revision, destination), "rm -rf #{destination}/_darcs"].join(" && ")
42
+ end
43
+
44
+ # Returns the command that will do a "darcs diff" for the two revisions.
45
+ # Each revision must be the 'hash' identifier of a darcs revision.
46
+ def diff(from, to=nil)
47
+ scm :diff, "--from-match 'hash #{from}'", to && "--to-match 'hash #{to}'"
48
+ end
49
+
50
+ # Returns the log of changes between the two revisions. Each revision
51
+ # must be the 'hash' identifier of a darcs revision.
52
+ def log(from, to=nil)
53
+ scm :changes, "--from-match 'hash #{from}'", to && "--to-match 'hash #{to}'", "--repo=#{repository}"
54
+ end
55
+
56
+ # Attempts to translate the given revision identifier to a "real"
57
+ # revision. If the identifier is a symbol, it is assumed to be a
58
+ # pseudo-id. Otherwise, it will be immediately returned. If it is a
59
+ # pseudo-id, a set of commands to execute will be yielded, and the
60
+ # result of executing those commands must be returned by the block.
61
+ # This method will then extract the actual revision hash from the
62
+ # returned data.
63
+ def query_revision(revision)
64
+ case revision
65
+ when :head
66
+ xml = yield(scm(:changes, "--last 1", "--xml-output", "--repo=#{repository}"))
67
+ return xml[/hash='(.*?)'/, 1]
68
+ else return revision
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def verbose
75
+ case variable(:scm_verbose)
76
+ when nil then "-q"
77
+ when false then nil
78
+ else "-v"
79
+ end
80
+ end
81
+ end
82
+
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,129 @@
1
+ # Copyright 2007 Matthew Elder <sseses@gmail.com>
2
+ # based on work by Tobias Luetke
3
+
4
+ require 'capistrano/recipes/deploy/scm/base'
5
+
6
+ module Capistrano
7
+ module Deploy
8
+ module SCM
9
+
10
+ # Implements the Capistrano SCM interface for the Mercurial revision
11
+ # control system (http://www.selenic.com/mercurial/).
12
+ # Latest updates at http://tackletechnology.org/oss/cap2-mercurial
13
+ class Mercurial < Base
14
+ # Sets the default command name for this SCM. Users may override this
15
+ # by setting the :scm_command variable.
16
+ default_command "hg"
17
+
18
+ # For mercurial HEAD == tip except that it bases this assumption on what
19
+ # tip is in the current repository (so push before you deploy)
20
+ def head
21
+ "tip"
22
+ end
23
+
24
+ # Clone the repository and update to the specified changeset.
25
+ def checkout(changeset, destination)
26
+ clone(destination) + " && " + update(changeset, destination)
27
+ end
28
+
29
+ # Pull from the repository and update to the specified changeset.
30
+ def sync(changeset, destination)
31
+ pull(destination) + " && " + update(changeset, destination)
32
+ end
33
+
34
+ # One day we will have hg archive, although i think its not needed
35
+ def export(revision, destination)
36
+ raise NotImplementedError, "`diff' is not implemented by #{self.class.name}" +
37
+ "use checkout strategy"
38
+ end
39
+
40
+ # Compute the difference between the two changesets +from+ and +to+
41
+ # as a unified diff.
42
+ def diff(from, to=nil)
43
+ scm :diff,
44
+ "--rev #{from}",
45
+ (to ? "--rev #{to}" : nil)
46
+ end
47
+
48
+ # Return a log of all changes between the two specified changesets,
49
+ # +from+ and +to+, inclusive or the log for +from+ if +to+ is omitted.
50
+ def log(from, to=nil)
51
+ scm :log,
52
+ verbose,
53
+ "--rev #{from}" +
54
+ (to ? ":#{to}" : "")
55
+ end
56
+
57
+ # Translates a tag to a changeset if needed or just returns changeset.
58
+ def query_revision(changeset)
59
+ cmd = scm :log,
60
+ verbose,
61
+ "-r #{changeset}",
62
+ "--template '{node|short}'"
63
+ yield cmd
64
+ end
65
+
66
+ # Determine response for SCM prompts
67
+ # user/pass can come from ssh and http distribution methods
68
+ # yes/no is for when ssh asks you about fingerprints
69
+ def handle_data(state, stream, text)
70
+ logger.info "[#{stream}] #{text}"
71
+ case text
72
+ when /^user:/mi
73
+ if variable(:scm_user)
74
+ "#{variable(:scm_user)}\n"
75
+ else
76
+ raise "No variable :scm_user specified and Mercurial asked!\n" +
77
+ "Prompt was: #{text}"
78
+ end
79
+ when /^password:/mi
80
+ if variable(:scm_password)
81
+ "#{variable(:scm_password)}\n"
82
+ else
83
+ raise "No variable :scm_password specified and Mercurial asked!\n" +
84
+ "Prompt was: #{text}"
85
+ end
86
+ when /yes\/no/i
87
+ "yes\n"
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ # Fine grained mercurial commands
94
+ def clone(destination)
95
+ scm :clone,
96
+ verbose,
97
+ "--noupdate", # do not update to tip when cloning is done
98
+ repository, # clone which repository?
99
+ destination # and put the clone where?
100
+ end
101
+
102
+ def pull(destination)
103
+ scm :pull,
104
+ verbose,
105
+ "--repository #{destination}", # pull changes into what?
106
+ repository # and pull the changes from?
107
+ end
108
+
109
+ def update(changeset, destination)
110
+ scm :update,
111
+ verbose,
112
+ "--repository #{destination}", # update what?
113
+ "--clean", # ignore untracked changes
114
+ changeset # update to this changeset
115
+ end
116
+
117
+ # verbosity configuration grokking :)
118
+ def verbose
119
+ case variable(:scm_verbose)
120
+ when nil: nil
121
+ when false: "--quiet"
122
+ else "--verbose"
123
+ end
124
+ end
125
+
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,126 @@
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, "-f")
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, "-f")
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, "-f")
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
+ # Determines what the response should be for a particular bit of text
62
+ # from the SCM. Password prompts, connection requests, passphrases,
63
+ # etc. are handled here.
64
+ def handle_data(state, stream, text)
65
+ case text
66
+ when /\(P4PASSWD\) invalid or unset\./i
67
+ raise Capistrano::Error, "scm_password (or p4passwd) is incorrect or unset"
68
+ when /Can.t create a new user.*/i
69
+ raise Capistrano::Error, "scm_username (or p4user) is incorrect or unset"
70
+ when /Perforce client error\:/i
71
+ raise Capistrano::Error, "p4port is incorrect or unset"
72
+ when /Client \'[\w\-\_\.]+\' unknown.*/i
73
+ raise Capistrano::Error, "p4client is incorrect or unset"
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ # Builds the set of authentication switches that perforce understands.
80
+ def authentication
81
+ [ p4port && "-p #{p4port}",
82
+ p4user && "-u #{p4user}",
83
+ p4passwd && "-P #{p4passwd}",
84
+ p4client && "-c #{p4client}" ].compact.join(" ")
85
+ end
86
+
87
+ # Returns the command that will sync the given revision to the given
88
+ # destination directory with specific options. The perforce client has
89
+ # a fixed destination so the files must be copied from there to their
90
+ # intended resting place.
91
+ def p4_sync(revision, destination, options="")
92
+ p4client_root = "`#{command} #{authentication} client -o | grep ^Root | cut -f2`"
93
+ scm authentication, :sync, options, "#{rev_no(revision)}", "&& cp -rf #{p4client_root} #{destination}"
94
+ end
95
+
96
+ def p4client
97
+ variable(:p4client)
98
+ end
99
+
100
+ def p4port
101
+ variable(:p4port)
102
+ end
103
+
104
+ def p4user
105
+ variable(:p4user) || variable(:scm_username)
106
+ end
107
+
108
+ def p4passwd
109
+ variable(:p4passwd) || variable(:scm_password)
110
+ end
111
+
112
+ def rev_no(revision)
113
+ case revision.to_s
114
+ when "head"
115
+ "#head"
116
+ when /^\d+/
117
+ "@#{revision}"
118
+ else
119
+ revision
120
+ end
121
+ end
122
+ end
123
+
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,103 @@
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, 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 :update, verbose, authentication, "-r#{revision}", 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, 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, 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, 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
+ result = yield(scm(:info, repository, authentication, "-r#{revision}"))
56
+ YAML.load(result)['Revision']
57
+ end
58
+
59
+ # Determines what the response should be for a particular bit of text
60
+ # from the SCM. Password prompts, connection requests, passphrases,
61
+ # etc. are handled here.
62
+ def handle_data(state, stream, text)
63
+ logger.info "[#{stream}] #{text}"
64
+ case text
65
+ when /\bpassword.*:/i
66
+ # subversion is prompting for a password
67
+ "#{variable(:scm_password) || variable(:password)}\n"
68
+ when %r{\(yes/no\)}
69
+ # subversion is asking whether or not to connect
70
+ "yes\n"
71
+ when /passphrase/i
72
+ # subversion is asking for the passphrase for the user's key
73
+ "#{variable(:scm_passphrase)}\n"
74
+ when /The entry \'(.+?)\' is no longer a directory/
75
+ raise Capisrano::Error, "subversion can't update because directory '#{$1}' was replaced. Please add it to svn:ignore."
76
+ when /accept \(t\)emporarily/
77
+ # subversion is asking whether to accept the certificate
78
+ "t\n"
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ # If a username or password is configured for the SCM, return the
85
+ # command-line switches for those values.
86
+ def authentication
87
+ auth = ""
88
+ auth << "--username #{variable(:scm_username)} " if variable(:scm_username)
89
+ auth << "--password #{variable(:scm_password)} " if variable(:scm_password)
90
+ auth << "--no-auth-cache" if !auth.empty?
91
+ auth
92
+ end
93
+
94
+ # If verbose output is requested, return nil, otherwise return the
95
+ # command-line switch for "quiet" ("-q").
96
+ def verbose
97
+ variable(:scm_verbose) ? nil : "-q"
98
+ end
99
+ end
100
+
101
+ end
102
+ end
103
+ 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