vershunt 2.0.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.
- checksums.yaml +7 -0
- data/bin/vershunt +19 -0
- data/lib/msp_release.rb +153 -0
- data/lib/msp_release/build.rb +64 -0
- data/lib/msp_release/cli.rb +90 -0
- data/lib/msp_release/cli/branch.rb +97 -0
- data/lib/msp_release/cli/build.rb +99 -0
- data/lib/msp_release/cli/bump.rb +49 -0
- data/lib/msp_release/cli/checkout.rb +196 -0
- data/lib/msp_release/cli/distrib.rb +7 -0
- data/lib/msp_release/cli/help.rb +14 -0
- data/lib/msp_release/cli/new.rb +62 -0
- data/lib/msp_release/cli/push.rb +29 -0
- data/lib/msp_release/cli/reset.rb +17 -0
- data/lib/msp_release/cli/status.rb +46 -0
- data/lib/msp_release/debian.rb +229 -0
- data/lib/msp_release/exec.rb +120 -0
- data/lib/msp_release/git.rb +145 -0
- data/lib/msp_release/log.rb +48 -0
- data/lib/msp_release/make_branch.rb +29 -0
- data/lib/msp_release/options.rb +35 -0
- data/lib/msp_release/project.rb +41 -0
- data/lib/msp_release/project/base.rb +63 -0
- data/lib/msp_release/project/debian.rb +168 -0
- data/lib/msp_release/project/gem.rb +74 -0
- data/lib/msp_release/project/git.rb +76 -0
- data/lib/msp_release/project/ruby.rb +40 -0
- data/lib/msp_release/version.rb +3 -0
- metadata +116 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA512:
|
3
|
+
data.tar.gz: 489aaa7a9417ff0c038fc4bc149ecfc1dd465cc18ea3ecc99384f4e63fe51b70ee7bf918937cd7a569e1715edfc000d4ee88392e67d37bd772b4e255291348b8
|
4
|
+
metadata.gz: 41ad772239c464ba17b89dd070a1b8df7801edcd93511ac3db554f6a5b84ac08b13e57082542f8624041e4c4951da6c33f944ca992a2c270c9c80763108fa2eb
|
5
|
+
SHA1:
|
6
|
+
data.tar.gz: d86f32da8656675a8ad473d55466d4ef551ad276
|
7
|
+
metadata.gz: a60005aa2a2a9bab6c5c31801ce8379ce1a43aac
|
data/bin/vershunt
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
INSTALL_DIR='/usr/lib/vershunt'
|
4
|
+
INSTALL_BIN='/usr/bin/vershunt'
|
5
|
+
|
6
|
+
lib_dir =
|
7
|
+
if __FILE__ == INSTALL_BIN
|
8
|
+
# smells like debian, rely on fixed installation locations
|
9
|
+
INSTALL_DIR + '/lib'
|
10
|
+
else
|
11
|
+
# local dev copy, or installed via rubygems
|
12
|
+
require 'rubygems'
|
13
|
+
File.expand_path(File.join(File.dirname(__FILE__), "../lib"))
|
14
|
+
end
|
15
|
+
|
16
|
+
$LOAD_PATH.unshift(lib_dir)
|
17
|
+
|
18
|
+
require 'msp_release'
|
19
|
+
MSPRelease::CLI.run(ARGV)
|
data/lib/msp_release.rb
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module MSPRelease
|
4
|
+
|
5
|
+
require 'climate'
|
6
|
+
require 'msp_release/exec'
|
7
|
+
|
8
|
+
include Exec::Helpers
|
9
|
+
|
10
|
+
module Helpers
|
11
|
+
|
12
|
+
PROJECT_FILE = ".msp_project"
|
13
|
+
|
14
|
+
# Slush bucket for stuff :)
|
15
|
+
|
16
|
+
def msp_version
|
17
|
+
project.version
|
18
|
+
end
|
19
|
+
|
20
|
+
def git_version
|
21
|
+
(m = /release-(.+)/.match(git.cur_branch)) && m[1]
|
22
|
+
end
|
23
|
+
|
24
|
+
def time; @time ||= Time.now; end
|
25
|
+
|
26
|
+
def timestamp
|
27
|
+
@timestamp ||= time.strftime("%Y%m%d%H%M%S")
|
28
|
+
end
|
29
|
+
|
30
|
+
def time_rfc
|
31
|
+
offset = time.gmt_offset / (60 * 60)
|
32
|
+
gmt_offset = "#{offset < 0 ? '-' : '+'}#{offset.abs.to_s.rjust(2, "0")}00"
|
33
|
+
time.strftime("%a, %d %b %Y %H:%M:%S #{gmt_offset}")
|
34
|
+
end
|
35
|
+
|
36
|
+
def author
|
37
|
+
@author ||=
|
38
|
+
begin
|
39
|
+
name, email = ['name', 'email'].map {|f| `git config --get user.#{f}`.strip}
|
40
|
+
Author.new(name, email)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def changelog
|
45
|
+
@changelog ||= begin
|
46
|
+
project.respond_to?(:changelog) && project.changelog
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def on_release_branch?
|
51
|
+
!!/release/.match(git.cur_branch)
|
52
|
+
end
|
53
|
+
|
54
|
+
def data
|
55
|
+
@data ||= {}
|
56
|
+
end
|
57
|
+
|
58
|
+
def data=(data_hash)
|
59
|
+
@data = data_hash
|
60
|
+
end
|
61
|
+
|
62
|
+
def data_exists?
|
63
|
+
File.exists?(DATAFILE)
|
64
|
+
end
|
65
|
+
|
66
|
+
def load_data
|
67
|
+
@data = File.open(DATAFILE, 'r') {|f| YAML.load(f) }
|
68
|
+
end
|
69
|
+
|
70
|
+
def save_data
|
71
|
+
File.open(DATAFILE, 'w') {|f| YAML.dump(@data, f) }
|
72
|
+
end
|
73
|
+
|
74
|
+
def remove_data
|
75
|
+
File.delete(DATAFILE) if File.exists?(DATAFILE)
|
76
|
+
end
|
77
|
+
|
78
|
+
def fail_if_modified_wc
|
79
|
+
annoying_files = git.modified_files + git.added_files
|
80
|
+
if annoying_files.length > 0
|
81
|
+
$stderr.puts("You have modified files in your working copy, and that just won't do")
|
82
|
+
annoying_files.each {|f|$stderr.puts(" " + f)}
|
83
|
+
exit 1
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def fail_if_push_pending
|
88
|
+
if data_exists?
|
89
|
+
$stderr.puts("You have a release commit pending to be pushed")
|
90
|
+
$stderr.puts("Please push, or reset the operation")
|
91
|
+
exit 1
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
include Helpers
|
97
|
+
|
98
|
+
require 'msp_release/log'
|
99
|
+
require 'msp_release/debian'
|
100
|
+
require 'msp_release/git'
|
101
|
+
require 'msp_release/options'
|
102
|
+
require 'msp_release/project'
|
103
|
+
require 'msp_release/build'
|
104
|
+
require 'msp_release/make_branch'
|
105
|
+
require 'msp_release/cli'
|
106
|
+
|
107
|
+
MSP_VERSION_FILE = "lib/msp/version.rb"
|
108
|
+
DATAFILE = ".msp_release"
|
109
|
+
|
110
|
+
LOG = Log.new
|
111
|
+
|
112
|
+
VERSION_SEGMENTS = [:major, :minor, :bugfix]
|
113
|
+
Version = Struct.new(*VERSION_SEGMENTS)
|
114
|
+
Version.module_eval do
|
115
|
+
|
116
|
+
def format(opts={})
|
117
|
+
opts[:without_bugfix] ? "#{major}.#{minor}" : "#{major}.#{minor}.#{bugfix}"
|
118
|
+
end
|
119
|
+
|
120
|
+
alias :to_s :format
|
121
|
+
|
122
|
+
def self.from_string(str)
|
123
|
+
match = /([0-9]+)\.([0-9]+)\.([0-9]+)/.match(str)
|
124
|
+
match && new(*(1..3).map{|i|match[i]})
|
125
|
+
end
|
126
|
+
|
127
|
+
def bump(segment)
|
128
|
+
|
129
|
+
raise ArgumentError, "no such segment: #{segment}" unless VERSION_SEGMENTS.include?(segment)
|
130
|
+
|
131
|
+
reset = false
|
132
|
+
new_segments = VERSION_SEGMENTS.map do |cur_seg|
|
133
|
+
part = self.send(cur_seg)
|
134
|
+
if cur_seg == segment
|
135
|
+
reset = true
|
136
|
+
part.to_i + 1
|
137
|
+
else
|
138
|
+
reset ? 0 : part
|
139
|
+
end.to_s
|
140
|
+
end
|
141
|
+
|
142
|
+
"new_segments: #{new_segments}"
|
143
|
+
|
144
|
+
self.class.new(*new_segments)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
Author = Struct.new(:name, :email)
|
149
|
+
|
150
|
+
extend self
|
151
|
+
|
152
|
+
end
|
153
|
+
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module MSPRelease
|
2
|
+
class Build
|
3
|
+
|
4
|
+
class NoChangesFileError < StandardError ; end
|
5
|
+
|
6
|
+
include Exec::Helpers
|
7
|
+
|
8
|
+
def initialize(basedir, project, options={})
|
9
|
+
@basedir = basedir
|
10
|
+
@project = project
|
11
|
+
@options = options
|
12
|
+
|
13
|
+
@sign = options.fetch(:sign, true)
|
14
|
+
end
|
15
|
+
|
16
|
+
def perform_from_cli!
|
17
|
+
LOG.debug("Building package...")
|
18
|
+
|
19
|
+
result =
|
20
|
+
begin
|
21
|
+
self.perform!
|
22
|
+
rescue Exec::UnexpectedExitStatus => e
|
23
|
+
raise CLI::Exit, "build failed:\n#{e.stderr}"
|
24
|
+
rescue Build::NoChangesFileError => e
|
25
|
+
raise CLI::Exit, "Unable to find changes file with version: " +
|
26
|
+
"#{e.message}\nAvailable: \n" +
|
27
|
+
self.available_changes_files.map { |f| " #{f}" }.join("\n")
|
28
|
+
end
|
29
|
+
|
30
|
+
result.tap do
|
31
|
+
LOG.debug("Package built: #{result.package}")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def perform!
|
36
|
+
dir = File.expand_path(@basedir)
|
37
|
+
raise "directory does not exist: #{dir}" unless
|
38
|
+
File.directory?(dir)
|
39
|
+
|
40
|
+
e = Exec.new(:name => 'build', :quiet => false, :status => :any)
|
41
|
+
Dir.chdir(@project.dir) do
|
42
|
+
e.exec(build_command)
|
43
|
+
end
|
44
|
+
|
45
|
+
if e.last_exitstatus != 0
|
46
|
+
LOG.warn("Warning: #{build_command} exited with #{e.last_exitstatus}")
|
47
|
+
end
|
48
|
+
|
49
|
+
@project.build_result(output_directory)
|
50
|
+
end
|
51
|
+
|
52
|
+
def output_directory
|
53
|
+
File.expand_path(@project.config[:deb_output_directory] ||
|
54
|
+
File.join(@basedir, '..'))
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def build_command
|
60
|
+
@project.build_command
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module MSPRelease
|
2
|
+
module CLI
|
3
|
+
|
4
|
+
# Base class for command line operations
|
5
|
+
class Command < Climate::Command
|
6
|
+
include Exec::Helpers
|
7
|
+
end
|
8
|
+
|
9
|
+
# alias this class for shorter lines
|
10
|
+
Exit = Climate::ExitException
|
11
|
+
|
12
|
+
# root of the command hierarchy
|
13
|
+
class Root < Climate::Command('vershunt')
|
14
|
+
|
15
|
+
description """
|
16
|
+
Manipulate your git repository by creating commits and performing
|
17
|
+
branch management to create a consistent log of commits to be used
|
18
|
+
as part of a repeatable build system, as well as encouraging
|
19
|
+
semantic versioning (see http://semver.org).
|
20
|
+
|
21
|
+
Projects must include a .msp_project, which is a yaml file that must at least
|
22
|
+
not be empty. If the project file has a ruby_version_file key, then this
|
23
|
+
file will be considered as well as the debian changelog when updating version
|
24
|
+
information.
|
25
|
+
"""
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
# Commands that require a git working copy can include this module
|
30
|
+
module WorkingCopyCommand
|
31
|
+
|
32
|
+
include Helpers
|
33
|
+
|
34
|
+
attr_accessor :project, :git
|
35
|
+
|
36
|
+
def initialize(options, leftovers)
|
37
|
+
super
|
38
|
+
|
39
|
+
if File.exists?(PROJECT_FILE)
|
40
|
+
@project = MSPRelease::Project.new_from_project_file(PROJECT_FILE)
|
41
|
+
else
|
42
|
+
raise Climate::ExitException.
|
43
|
+
new("No #{PROJECT_FILE} present in current directory")
|
44
|
+
end
|
45
|
+
|
46
|
+
@git = Git.new(@project, @options)
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
# hardcoded list of commands
|
52
|
+
COMMANDS = ['help', 'new', 'push', 'branch', 'status', 'reset', 'bump', 'checkout', 'build']
|
53
|
+
|
54
|
+
# These are available on the CLI module
|
55
|
+
module ClassMethods
|
56
|
+
|
57
|
+
attr_reader :commands
|
58
|
+
|
59
|
+
def run(args)
|
60
|
+
init_commands
|
61
|
+
|
62
|
+
Climate.with_standard_exception_handling do
|
63
|
+
begin
|
64
|
+
Root.run(args)
|
65
|
+
rescue Exec::UnexpectedExitStatus => e
|
66
|
+
$stderr.puts(e.message)
|
67
|
+
$stderr.puts(e.stderr)
|
68
|
+
exit 1
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def init_commands
|
74
|
+
@commands = {}
|
75
|
+
COMMANDS.each do |name|
|
76
|
+
require "msp_release/cli/#{name}"
|
77
|
+
camel_name =
|
78
|
+
name.split(/[^a-z0-9]/i).map{|w| w.capitalize}.join
|
79
|
+
|
80
|
+
command = MSPRelease::CLI.const_get(camel_name)
|
81
|
+
@commands[name] = command
|
82
|
+
command.set_name(name)
|
83
|
+
command.subcommand_of(Root)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
self.extend(ClassMethods)
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module MSPRelease
|
2
|
+
class CLI::Branch < CLI::Command
|
3
|
+
include CLI::WorkingCopyCommand
|
4
|
+
|
5
|
+
description """
|
6
|
+
Create and switch to a release branch for the version on HEAD
|
7
|
+
|
8
|
+
The release branch will be named after the project version returned by
|
9
|
+
`vershunt status`, excluding the bugfix version. If the version is
|
10
|
+
1.2.3, a branch of release-1.2 will be created.
|
11
|
+
|
12
|
+
The minor version on master is bumped after the branch is created, although this can be disabled.
|
13
|
+
"""
|
14
|
+
|
15
|
+
opt :allow_non_master_branch, "Allow release branch to be created " +
|
16
|
+
"even if you are not on master. Normally you would not want to do" +
|
17
|
+
" this, so this is here to prevent branches from mistakenly being " +
|
18
|
+
"created from branches other than master",
|
19
|
+
{
|
20
|
+
:short => 'a',
|
21
|
+
:default => false
|
22
|
+
}
|
23
|
+
|
24
|
+
|
25
|
+
opt :no_bump_master, "Do not bump the minor version of master " +
|
26
|
+
"as part of creating the release branch. Typically after creating a " +
|
27
|
+
"release branch, the minor version being stabilised now lives on the " +
|
28
|
+
"branch and master is now a new version",
|
29
|
+
{
|
30
|
+
:short => 'n',
|
31
|
+
:default => false
|
32
|
+
}
|
33
|
+
|
34
|
+
def run
|
35
|
+
fail_if_push_pending
|
36
|
+
|
37
|
+
# take the version before we do any bumping
|
38
|
+
version = project.version
|
39
|
+
|
40
|
+
branch_from =
|
41
|
+
if git.on_master?
|
42
|
+
bump_and_push_master
|
43
|
+
else
|
44
|
+
check_branching_ok!
|
45
|
+
end
|
46
|
+
|
47
|
+
branch_name = project.branch_name(version)
|
48
|
+
|
49
|
+
begin
|
50
|
+
MSPRelease::MakeBranch.new(git, branch_name, :start_point => branch_from).
|
51
|
+
perform!
|
52
|
+
rescue MSPRelease::MakeBranch::BranchExistsError => e
|
53
|
+
raise CLI::Exit, "A branch already exists for #{version}"
|
54
|
+
end
|
55
|
+
|
56
|
+
$stdout.puts("Switched to release branch '#{branch_name}'")
|
57
|
+
end
|
58
|
+
|
59
|
+
def check_branching_ok!
|
60
|
+
if options[:allow_non_master_branch]
|
61
|
+
$stderr.puts("Creating a non-master release branch, --allow-non-master-branch supplied")
|
62
|
+
"HEAD@{0}"
|
63
|
+
else
|
64
|
+
raise CLI::Exit, "You must be on master to create " +
|
65
|
+
"release branches, or pass --allow-non-master-branch"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def bump_and_push_master
|
70
|
+
|
71
|
+
return "HEAD@{0}" if options[:no_bump_master]
|
72
|
+
|
73
|
+
# don't let this happen with a dirty working copy, because we reset the
|
74
|
+
# master branch, which will kill all your changes
|
75
|
+
fail_if_modified_wc
|
76
|
+
|
77
|
+
new_version, *changed_files = project.bump_version('minor')
|
78
|
+
exec "git add -- #{changed_files.join(' ')}"
|
79
|
+
|
80
|
+
# FIXME dry this part up, perhaps using a bump operation class
|
81
|
+
exec "git commit -m 'BUMPED VERSION TO #{new_version}'"
|
82
|
+
|
83
|
+
begin
|
84
|
+
$stdout.puts "Bumping master to #{new_version}, pushing to origin..."
|
85
|
+
exec "git push origin master"
|
86
|
+
rescue
|
87
|
+
$stderr.puts "error pushing bump commit to master, undoing bump..."
|
88
|
+
exec "git reset --hard HEAD@{1}"
|
89
|
+
raise CLI::Exit, 'could not push bump commit to master, if you do ' +
|
90
|
+
'not want to bump the minor version of master, try again with ' +
|
91
|
+
'--no-bump-master'
|
92
|
+
end
|
93
|
+
|
94
|
+
"HEAD@{1}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module MSPRelease
|
2
|
+
class CLI::Build < CLI::Command
|
3
|
+
include Debian::Versions
|
4
|
+
|
5
|
+
# When cloning repositories, limit to this many commits from each head
|
6
|
+
CLONE_DEPTH = 5
|
7
|
+
|
8
|
+
description """Build debian packages suitable for deployment fresh from
|
9
|
+
source control using a build commit or a development version.
|
10
|
+
|
11
|
+
When no BRANCH_NAME is given, or that branch is not a release branch, the
|
12
|
+
latest commit from that branch is checked out and the changelog version is
|
13
|
+
adjusted to show this is a development build.
|
14
|
+
|
15
|
+
If BRANCH_NAME denotes a release branch (i.e release-1.1) then the latest
|
16
|
+
/release/ commit is checked out, even if there are commits after it.
|
17
|
+
The changelog remains unaltered in this case - the source tree should have
|
18
|
+
the correct version information in it.
|
19
|
+
"""
|
20
|
+
|
21
|
+
arg :git_url, "URL used to clone the git repository"
|
22
|
+
|
23
|
+
arg :branch_name, "Name of a branch to build from, defaults to ${default}",
|
24
|
+
:default => "master"
|
25
|
+
|
26
|
+
opt :sign, "Pass options to dpkg-buildpackage to tell it whether or not to sign the build products",
|
27
|
+
{
|
28
|
+
:short => 'S',
|
29
|
+
:default => false
|
30
|
+
}
|
31
|
+
|
32
|
+
opt :shallow, "Only perform a shallow checkout to a depth of five" +
|
33
|
+
"commits from each head. See git documentation for more details",
|
34
|
+
{
|
35
|
+
:short => 's',
|
36
|
+
:default => true
|
37
|
+
}
|
38
|
+
|
39
|
+
opt :distribution, "Specify the debian distribution to put in the " +
|
40
|
+
"changelog when checking out a development version",
|
41
|
+
{
|
42
|
+
:short => 'd',
|
43
|
+
:long => 'debian-distribution',
|
44
|
+
:type => :string
|
45
|
+
}
|
46
|
+
|
47
|
+
opt :verbose, "Print some useful debugging output to stdout",
|
48
|
+
{
|
49
|
+
:long => 'verbose',
|
50
|
+
:short => 'v', :default => false
|
51
|
+
}
|
52
|
+
|
53
|
+
opt :noisy, "Output dpkg-buildpackage output to stderr",
|
54
|
+
{
|
55
|
+
:short => 'n', :default => false
|
56
|
+
}
|
57
|
+
|
58
|
+
opt :silent, "Do not print out build products to stdout", {
|
59
|
+
:default => false
|
60
|
+
}
|
61
|
+
|
62
|
+
def run
|
63
|
+
git_url = arguments[:git_url]
|
64
|
+
release_spec_arg = arguments[:branch_name]
|
65
|
+
|
66
|
+
do_build = options[:build]
|
67
|
+
tar_it = options[:tar]
|
68
|
+
clone_depth = options[:shallow] ? CLONE_DEPTH : nil
|
69
|
+
|
70
|
+
LOG.silent if options[:silent]
|
71
|
+
LOG.verbose if options[:verbose]
|
72
|
+
LOG.noisy if options[:noisy]
|
73
|
+
|
74
|
+
branch_name = release_spec_arg || 'master'
|
75
|
+
|
76
|
+
shallow_output = clone_depth.nil?? '' : ' (shallow)'
|
77
|
+
|
78
|
+
options[:shallow_output] = shallow_output
|
79
|
+
|
80
|
+
# checkout project
|
81
|
+
tmp_dir = "vershunt-#{Time.now.to_i}.tmp"
|
82
|
+
Git.clone(git_url, {:depth => clone_depth, :out_to => tmp_dir,
|
83
|
+
:branch => branch_name, :no_single_branch => true,
|
84
|
+
:exec => {:quiet => true}})
|
85
|
+
|
86
|
+
project = Project.new_from_project_file(tmp_dir + "/" + Helpers::PROJECT_FILE)
|
87
|
+
|
88
|
+
project.prepare_for_build(branch_name, options)
|
89
|
+
|
90
|
+
if project.respond_to?(:build)
|
91
|
+
project.build(options)
|
92
|
+
else
|
93
|
+
raise "I don't know how to build this project"
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|