capistrano 3.4.1 → 3.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +7 -5
- data/.rubocop.yml +49 -0
- data/.travis.yml +5 -4
- data/CHANGELOG.md +72 -9
- data/CONTRIBUTING.md +61 -93
- data/DEVELOPMENT.md +122 -0
- data/Gemfile +2 -2
- data/LICENSE.txt +1 -1
- data/README.md +121 -43
- data/RELEASING.md +16 -0
- data/Rakefile +4 -1
- data/bin/cap +1 -1
- data/capistrano.gemspec +16 -21
- data/features/doctor.feature +11 -0
- data/features/step_definitions/assertions.rb +17 -17
- data/features/step_definitions/cap_commands.rb +0 -1
- data/features/step_definitions/setup.rb +12 -8
- data/features/support/env.rb +5 -5
- data/features/support/remote_command_helpers.rb +8 -6
- data/features/support/vagrant_helpers.rb +5 -4
- data/issue_template.md +21 -0
- data/lib/Capfile +5 -1
- data/lib/capistrano/all.rb +9 -10
- data/lib/capistrano/application.rb +36 -26
- data/lib/capistrano/configuration.rb +56 -41
- data/lib/capistrano/configuration/empty_filter.rb +9 -0
- data/lib/capistrano/configuration/filter.rb +18 -47
- data/lib/capistrano/configuration/host_filter.rb +30 -0
- data/lib/capistrano/configuration/null_filter.rb +9 -0
- data/lib/capistrano/configuration/plugin_installer.rb +33 -0
- data/lib/capistrano/configuration/question.rb +10 -7
- data/lib/capistrano/configuration/role_filter.rb +30 -0
- data/lib/capistrano/configuration/server.rb +22 -23
- data/lib/capistrano/configuration/servers.rb +6 -7
- data/lib/capistrano/configuration/variables.rb +136 -0
- data/lib/capistrano/defaults.rb +13 -3
- data/lib/capistrano/deploy.rb +1 -1
- data/lib/capistrano/doctor.rb +5 -0
- data/lib/capistrano/doctor/environment_doctor.rb +19 -0
- data/lib/capistrano/doctor/gems_doctor.rb +45 -0
- data/lib/capistrano/doctor/output_helpers.rb +79 -0
- data/lib/capistrano/doctor/variables_doctor.rb +66 -0
- data/lib/capistrano/dotfile.rb +1 -2
- data/lib/capistrano/dsl.rb +12 -14
- data/lib/capistrano/dsl/env.rb +11 -42
- data/lib/capistrano/dsl/paths.rb +12 -13
- data/lib/capistrano/dsl/stages.rb +2 -4
- data/lib/capistrano/dsl/task_enhancements.rb +5 -7
- data/lib/capistrano/framework.rb +1 -1
- data/lib/capistrano/git.rb +17 -9
- data/lib/capistrano/hg.rb +4 -4
- data/lib/capistrano/i18n.rb +24 -24
- data/lib/capistrano/immutable_task.rb +29 -0
- data/lib/capistrano/install.rb +1 -1
- data/lib/capistrano/plugin.rb +95 -0
- data/lib/capistrano/scm.rb +7 -20
- data/lib/capistrano/setup.rb +19 -5
- data/lib/capistrano/svn.rb +9 -5
- data/lib/capistrano/tasks/console.rake +4 -8
- data/lib/capistrano/tasks/deploy.rake +75 -62
- data/lib/capistrano/tasks/doctor.rake +19 -0
- data/lib/capistrano/tasks/framework.rake +13 -14
- data/lib/capistrano/tasks/git.rake +10 -11
- data/lib/capistrano/tasks/hg.rake +7 -7
- data/lib/capistrano/tasks/install.rake +14 -15
- data/lib/capistrano/tasks/svn.rake +7 -7
- data/lib/capistrano/templates/Capfile +3 -3
- data/lib/capistrano/templates/deploy.rb.erb +6 -5
- data/lib/capistrano/upload_task.rb +1 -1
- data/lib/capistrano/version.rb +1 -1
- data/lib/capistrano/version_validator.rb +4 -6
- data/spec/integration/dsl_spec.rb +286 -239
- data/spec/integration_spec_helper.rb +3 -5
- data/spec/lib/capistrano/application_spec.rb +22 -14
- data/spec/lib/capistrano/configuration/empty_filter_spec.rb +17 -0
- data/spec/lib/capistrano/configuration/filter_spec.rb +82 -84
- data/spec/lib/capistrano/configuration/host_filter_spec.rb +61 -0
- data/spec/lib/capistrano/configuration/null_filter_spec.rb +17 -0
- data/spec/lib/capistrano/configuration/question_spec.rb +12 -16
- data/spec/lib/capistrano/configuration/role_filter_spec.rb +64 -0
- data/spec/lib/capistrano/configuration/server_spec.rb +102 -110
- data/spec/lib/capistrano/configuration/servers_spec.rb +124 -141
- data/spec/lib/capistrano/configuration_spec.rb +150 -61
- data/spec/lib/capistrano/doctor/environment_doctor_spec.rb +44 -0
- data/spec/lib/capistrano/doctor/gems_doctor_spec.rb +61 -0
- data/spec/lib/capistrano/doctor/output_helpers_spec.rb +47 -0
- data/spec/lib/capistrano/doctor/variables_doctor_spec.rb +79 -0
- data/spec/lib/capistrano/dsl/paths_spec.rb +58 -50
- data/spec/lib/capistrano/dsl/task_enhancements_spec.rb +62 -32
- data/spec/lib/capistrano/dsl_spec.rb +6 -8
- data/spec/lib/capistrano/git_spec.rb +35 -7
- data/spec/lib/capistrano/hg_spec.rb +14 -5
- data/spec/lib/capistrano/immutable_task_spec.rb +31 -0
- data/spec/lib/capistrano/plugin_spec.rb +84 -0
- data/spec/lib/capistrano/scm_spec.rb +6 -7
- data/spec/lib/capistrano/svn_spec.rb +40 -14
- data/spec/lib/capistrano/upload_task_spec.rb +7 -7
- data/spec/lib/capistrano/version_validator_spec.rb +37 -45
- data/spec/lib/capistrano_spec.rb +2 -3
- data/spec/spec_helper.rb +8 -8
- data/spec/support/Vagrantfile +9 -10
- data/spec/support/tasks/database.rake +3 -3
- data/spec/support/tasks/fail.rake +4 -3
- data/spec/support/tasks/failed.rake +2 -2
- data/spec/support/tasks/plugin.rake +6 -0
- data/spec/support/tasks/root.rake +4 -4
- data/spec/support/test_app.rb +31 -30
- metadata +93 -14
data/lib/capistrano/defaults.rb
CHANGED
@@ -1,14 +1,24 @@
|
|
1
|
+
validate :application do |_key, value|
|
2
|
+
changed_value = value.gsub(/[^A-Z0-9\.\-]/i, "_")
|
3
|
+
if value != changed_value
|
4
|
+
warn %Q(The :application value "#{value}" is invalid!)
|
5
|
+
warn "Use only letters, numbers, hyphens, dots, and underscores. For example:"
|
6
|
+
warn " set :application, '#{changed_value}'"
|
7
|
+
raise Capistrano::ValidationError
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
1
11
|
set_if_empty :scm, :git
|
2
|
-
set_if_empty :branch,
|
12
|
+
set_if_empty :branch, "master"
|
3
13
|
set_if_empty :deploy_to, -> { "/var/www/#{fetch(:application)}" }
|
4
14
|
set_if_empty :tmp_dir, "/tmp"
|
5
15
|
|
6
16
|
set_if_empty :default_env, {}
|
7
17
|
set_if_empty :keep_releases, 5
|
8
18
|
|
9
|
-
set_if_empty :format, :
|
19
|
+
set_if_empty :format, :airbrussh
|
10
20
|
set_if_empty :log_level, :debug
|
11
21
|
|
12
22
|
set_if_empty :pty, false
|
13
23
|
|
14
|
-
set_if_empty :local_user, -> {
|
24
|
+
set_if_empty :local_user, -> { ENV["USER"] || ENV["LOGNAME"] || ENV["USERNAME"] }
|
data/lib/capistrano/deploy.rb
CHANGED
@@ -0,0 +1,19 @@
|
|
1
|
+
require "capistrano/doctor/output_helpers"
|
2
|
+
|
3
|
+
module Capistrano
|
4
|
+
module Doctor
|
5
|
+
class EnvironmentDoctor
|
6
|
+
include Capistrano::Doctor::OutputHelpers
|
7
|
+
|
8
|
+
def call
|
9
|
+
title("Environment")
|
10
|
+
puts <<-OUT.gsub(/^\s+/, "")
|
11
|
+
Ruby #{RUBY_DESCRIPTION}
|
12
|
+
Rubygems #{Gem::VERSION}
|
13
|
+
Bundler #{defined?(Bundler::VERSION) ? Bundler::VERSION : 'N/A'}
|
14
|
+
Command #{$PROGRAM_NAME} #{ARGV.join(' ')}
|
15
|
+
OUT
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require "capistrano/doctor/output_helpers"
|
2
|
+
|
3
|
+
module Capistrano
|
4
|
+
module Doctor
|
5
|
+
# Prints table of all Capistrano-related gems and their version numbers. If
|
6
|
+
# there is a newer version of a gem available, call attention to it.
|
7
|
+
class GemsDoctor
|
8
|
+
include Capistrano::Doctor::OutputHelpers
|
9
|
+
|
10
|
+
def call
|
11
|
+
title("Gems")
|
12
|
+
table(all_gem_names) do |gem, row|
|
13
|
+
row.yellow if update_available?(gem)
|
14
|
+
row << gem
|
15
|
+
row << installed_gem_version(gem)
|
16
|
+
row << "(update available)" if update_available?(gem)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def installed_gem_version(gem_name)
|
23
|
+
Gem.loaded_specs[gem_name].version
|
24
|
+
end
|
25
|
+
|
26
|
+
def update_available?(gem_name)
|
27
|
+
latest = Gem.latest_version_for(gem_name)
|
28
|
+
return false if latest.nil?
|
29
|
+
latest > installed_gem_version(gem_name)
|
30
|
+
end
|
31
|
+
|
32
|
+
def all_gem_names
|
33
|
+
core_gem_names + plugin_gem_names
|
34
|
+
end
|
35
|
+
|
36
|
+
def core_gem_names
|
37
|
+
%w(capistrano airbrussh rake sshkit) & Gem.loaded_specs.keys
|
38
|
+
end
|
39
|
+
|
40
|
+
def plugin_gem_names
|
41
|
+
(Gem.loaded_specs.keys - ["capistrano"]).grep(/capistrano/).sort
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Capistrano
|
2
|
+
module Doctor
|
3
|
+
# Helper methods for pretty-printing doctor output to stdout. All output
|
4
|
+
# (other than `title`) is indented by four spaces to facilitate copying and
|
5
|
+
# pasting this output into e.g. GitHub or Stack Overflow to achieve code
|
6
|
+
# formatting.
|
7
|
+
module OutputHelpers
|
8
|
+
class Row
|
9
|
+
attr_reader :color
|
10
|
+
attr_reader :values
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@values = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def <<(value)
|
17
|
+
values << value
|
18
|
+
end
|
19
|
+
|
20
|
+
def yellow
|
21
|
+
@color = :yellow
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Prints a table for a given array of records. For each record, the block
|
26
|
+
# is yielded two arguments: the record and a Row object. To print values
|
27
|
+
# for that record, add values using `row << "some value"`. A row can
|
28
|
+
# optionally be highlighted in yellow using `row.yellow`.
|
29
|
+
def table(records, &block)
|
30
|
+
return if records.empty?
|
31
|
+
rows = collect_rows(records, &block)
|
32
|
+
col_widths = calculate_column_widths(rows)
|
33
|
+
|
34
|
+
rows.each do |row|
|
35
|
+
line = row.values.each_with_index.map do |value, col|
|
36
|
+
value.to_s.ljust(col_widths[col])
|
37
|
+
end.join(" ").rstrip
|
38
|
+
line = color.colorize(line, row.color) if row.color
|
39
|
+
puts line
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Prints a title in blue with surrounding newlines.
|
44
|
+
def title(text)
|
45
|
+
# Use $stdout directly to bypass the indentation that our `puts` does.
|
46
|
+
$stdout.puts(color.colorize("\n#{text}\n", :blue))
|
47
|
+
end
|
48
|
+
|
49
|
+
# Prints text in yellow.
|
50
|
+
def warning(text)
|
51
|
+
puts color.colorize(text, :yellow)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Override `Kernel#puts` to prepend four spaces to each line.
|
55
|
+
def puts(string=nil)
|
56
|
+
$stdout.puts(string.to_s.gsub(/^/, " "))
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def collect_rows(records)
|
62
|
+
records.map do |rec|
|
63
|
+
Row.new.tap { |row| yield(rec, row) }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def calculate_column_widths(rows)
|
68
|
+
num_columns = rows.map { |row| row.values.length }.max
|
69
|
+
Array.new(num_columns) do |col|
|
70
|
+
rows.map { |row| row.values[col].to_s.length }.max
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def color
|
75
|
+
@color ||= SSHKit::Color.new($stdout)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require "capistrano/doctor/output_helpers"
|
2
|
+
|
3
|
+
module Capistrano
|
4
|
+
module Doctor
|
5
|
+
# Prints a table of all Capistrano variables and their current values. If
|
6
|
+
# there are unrecognized variables, print warnings for them.
|
7
|
+
class VariablesDoctor
|
8
|
+
# These are keys that have no default values in Capistrano, but are
|
9
|
+
# nonetheless expected to be set.
|
10
|
+
WHITELIST = [:application, :repo_url].freeze
|
11
|
+
private_constant :WHITELIST
|
12
|
+
|
13
|
+
include Capistrano::Doctor::OutputHelpers
|
14
|
+
|
15
|
+
def initialize(env=Capistrano::Configuration.env)
|
16
|
+
@env = env
|
17
|
+
end
|
18
|
+
|
19
|
+
def call
|
20
|
+
title("Variables")
|
21
|
+
values = inspect_all_values
|
22
|
+
|
23
|
+
table(variables.keys.sort) do |key, row|
|
24
|
+
row.yellow if suspicious_keys.include?(key)
|
25
|
+
row << ":#{key}"
|
26
|
+
row << values[key]
|
27
|
+
end
|
28
|
+
|
29
|
+
puts if suspicious_keys.any?
|
30
|
+
|
31
|
+
suspicious_keys.sort.each do |key|
|
32
|
+
warning(
|
33
|
+
":#{key} is not a recognized Capistrano setting (#{location(key)})"
|
34
|
+
)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
attr_reader :env
|
41
|
+
|
42
|
+
def variables
|
43
|
+
env.variables
|
44
|
+
end
|
45
|
+
|
46
|
+
def inspect_all_values
|
47
|
+
variables.keys.each_with_object({}) do |key, inspected|
|
48
|
+
inspected[key] = if env.is_question?(key)
|
49
|
+
"<ask>"
|
50
|
+
else
|
51
|
+
variables.peek(key).inspect
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def suspicious_keys
|
57
|
+
(variables.untrusted_keys & variables.unused_keys) - WHITELIST
|
58
|
+
end
|
59
|
+
|
60
|
+
def location(key)
|
61
|
+
loc = variables.source_locations(key).first
|
62
|
+
loc && loc.sub(/^#{Regexp.quote(Dir.pwd)}/, "").sub(/:in.*/, "")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/capistrano/dotfile.rb
CHANGED
data/lib/capistrano/dsl.rb
CHANGED
@@ -1,9 +1,8 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require 'capistrano/configuration/filter'
|
1
|
+
require "capistrano/dsl/task_enhancements"
|
2
|
+
require "capistrano/dsl/paths"
|
3
|
+
require "capistrano/dsl/stages"
|
4
|
+
require "capistrano/dsl/env"
|
5
|
+
require "capistrano/configuration/filter"
|
7
6
|
|
8
7
|
module Capistrano
|
9
8
|
module DSL
|
@@ -30,12 +29,12 @@ module Capistrano
|
|
30
29
|
|
31
30
|
def revision_log_message
|
32
31
|
fetch(:revision_log_message,
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
32
|
+
t(:revision_log_message,
|
33
|
+
branch: fetch(:branch),
|
34
|
+
user: local_user,
|
35
|
+
sha: fetch(:current_revision),
|
36
|
+
release: fetch(:release_timestamp))
|
37
|
+
)
|
39
38
|
end
|
40
39
|
|
41
40
|
def rollback_log_message
|
@@ -58,7 +57,6 @@ module Capistrano
|
|
58
57
|
def run_locally(&block)
|
59
58
|
SSHKit::Backend::Local.new(&block).run
|
60
59
|
end
|
61
|
-
|
62
60
|
end
|
63
61
|
end
|
64
|
-
|
62
|
+
extend Capistrano::DSL
|
data/lib/capistrano/dsl/env.rb
CHANGED
@@ -1,46 +1,20 @@
|
|
1
|
+
require "forwardable"
|
2
|
+
|
1
3
|
module Capistrano
|
2
4
|
module DSL
|
3
5
|
module Env
|
6
|
+
extend Forwardable
|
7
|
+
def_delegators :env,
|
8
|
+
:configure_backend, :fetch, :set, :set_if_empty, :delete,
|
9
|
+
:ask, :role, :server, :primary, :validate, :append,
|
10
|
+
:remove, :dry_run?, :install_plugin
|
4
11
|
|
5
|
-
def
|
6
|
-
env.
|
7
|
-
end
|
8
|
-
|
9
|
-
def fetch(key, default=nil, &block)
|
10
|
-
env.fetch(key, default, &block)
|
12
|
+
def is_question?(key)
|
13
|
+
env.is_question?(key)
|
11
14
|
end
|
12
15
|
|
13
16
|
def any?(key)
|
14
|
-
|
15
|
-
if value && value.respond_to?(:any?)
|
16
|
-
value.any?
|
17
|
-
else
|
18
|
-
!fetch(key).nil?
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
def set(key, value)
|
23
|
-
env.set(key, value)
|
24
|
-
end
|
25
|
-
|
26
|
-
def set_if_empty(key, value)
|
27
|
-
env.set_if_empty(key, value)
|
28
|
-
end
|
29
|
-
|
30
|
-
def delete(key)
|
31
|
-
env.delete(key)
|
32
|
-
end
|
33
|
-
|
34
|
-
def ask(key, value, options={})
|
35
|
-
env.ask(key, value, options)
|
36
|
-
end
|
37
|
-
|
38
|
-
def role(name, servers, options={})
|
39
|
-
env.role(name, servers, options)
|
40
|
-
end
|
41
|
-
|
42
|
-
def server(name, properties={})
|
43
|
-
env.server(name, properties)
|
17
|
+
env.any?(key)
|
44
18
|
end
|
45
19
|
|
46
20
|
def roles(*names)
|
@@ -53,17 +27,13 @@ module Capistrano
|
|
53
27
|
|
54
28
|
def release_roles(*names)
|
55
29
|
if names.last.is_a? Hash
|
56
|
-
names.last
|
30
|
+
names.last[:exclude] = :no_release
|
57
31
|
else
|
58
32
|
names << { exclude: :no_release }
|
59
33
|
end
|
60
34
|
roles(*names)
|
61
35
|
end
|
62
36
|
|
63
|
-
def primary(role)
|
64
|
-
env.primary(role)
|
65
|
-
end
|
66
|
-
|
67
37
|
def env
|
68
38
|
Configuration.env
|
69
39
|
end
|
@@ -75,7 +45,6 @@ module Capistrano
|
|
75
45
|
def asset_timestamp
|
76
46
|
env.timestamp.strftime("%Y%m%d%H%M.%S")
|
77
47
|
end
|
78
|
-
|
79
48
|
end
|
80
49
|
end
|
81
50
|
end
|
data/lib/capistrano/dsl/paths.rb
CHANGED
@@ -1,8 +1,7 @@
|
|
1
|
-
require
|
1
|
+
require "pathname"
|
2
2
|
module Capistrano
|
3
3
|
module DSL
|
4
4
|
module Paths
|
5
|
-
|
6
5
|
def deploy_to
|
7
6
|
fetch(:deploy_to)
|
8
7
|
end
|
@@ -12,11 +11,11 @@ module Capistrano
|
|
12
11
|
end
|
13
12
|
|
14
13
|
def current_path
|
15
|
-
deploy_path.join(
|
14
|
+
deploy_path.join("current")
|
16
15
|
end
|
17
16
|
|
18
17
|
def releases_path
|
19
|
-
deploy_path.join(
|
18
|
+
deploy_path.join("releases")
|
20
19
|
end
|
21
20
|
|
22
21
|
def release_path
|
@@ -29,17 +28,17 @@ module Capistrano
|
|
29
28
|
end
|
30
29
|
|
31
30
|
def stage_config_path
|
32
|
-
Pathname.new fetch(:stage_config_path,
|
31
|
+
Pathname.new fetch(:stage_config_path, "config/deploy")
|
33
32
|
end
|
34
33
|
|
35
34
|
def deploy_config_path
|
36
|
-
Pathname.new fetch(:deploy_config_path,
|
35
|
+
Pathname.new fetch(:deploy_config_path, "config/deploy.rb")
|
37
36
|
end
|
38
37
|
|
39
38
|
def repo_url
|
40
|
-
require
|
41
|
-
require
|
42
|
-
if fetch(:git_http_username)
|
39
|
+
require "cgi"
|
40
|
+
require "uri"
|
41
|
+
if fetch(:git_http_username) && fetch(:git_http_password)
|
43
42
|
URI.parse(fetch(:repo_url)).tap do |repo_uri|
|
44
43
|
repo_uri.user = fetch(:git_http_username)
|
45
44
|
repo_uri.password = CGI.escape(fetch(:git_http_password))
|
@@ -54,15 +53,15 @@ module Capistrano
|
|
54
53
|
end
|
55
54
|
|
56
55
|
def repo_path
|
57
|
-
Pathname.new(fetch(:repo_path, ->(){deploy_path.join(
|
56
|
+
Pathname.new(fetch(:repo_path, ->() { deploy_path.join("repo") }))
|
58
57
|
end
|
59
58
|
|
60
59
|
def shared_path
|
61
|
-
deploy_path.join(
|
60
|
+
deploy_path.join("shared")
|
62
61
|
end
|
63
62
|
|
64
63
|
def revision_log
|
65
|
-
deploy_path.join(
|
64
|
+
deploy_path.join("revisions.log")
|
66
65
|
end
|
67
66
|
|
68
67
|
def now
|
@@ -96,7 +95,7 @@ module Capistrano
|
|
96
95
|
end
|
97
96
|
|
98
97
|
def map_dirnames(paths)
|
99
|
-
paths.map
|
98
|
+
paths.map(&:dirname).uniq
|
100
99
|
end
|
101
100
|
end
|
102
101
|
end
|