git-lint 1.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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/LICENSE.adoc +162 -0
- data/README.adoc +1068 -0
- data/bin/git-lint +9 -0
- data/lib/git/kit/repo.rb +30 -0
- data/lib/git/lint.rb +51 -0
- data/lib/git/lint/analyzers/abstract.rb +108 -0
- data/lib/git/lint/analyzers/commit_author_capitalization.rb +35 -0
- data/lib/git/lint/analyzers/commit_author_email.rb +35 -0
- data/lib/git/lint/analyzers/commit_author_name.rb +40 -0
- data/lib/git/lint/analyzers/commit_body_bullet.rb +43 -0
- data/lib/git/lint/analyzers/commit_body_bullet_capitalization.rb +46 -0
- data/lib/git/lint/analyzers/commit_body_bullet_delimiter.rb +40 -0
- data/lib/git/lint/analyzers/commit_body_issue_tracker_link.rb +45 -0
- data/lib/git/lint/analyzers/commit_body_leading_line.rb +30 -0
- data/lib/git/lint/analyzers/commit_body_line_length.rb +42 -0
- data/lib/git/lint/analyzers/commit_body_paragraph_capitalization.rb +47 -0
- data/lib/git/lint/analyzers/commit_body_phrase.rb +72 -0
- data/lib/git/lint/analyzers/commit_body_presence.rb +36 -0
- data/lib/git/lint/analyzers/commit_body_single_bullet.rb +40 -0
- data/lib/git/lint/analyzers/commit_subject_length.rb +33 -0
- data/lib/git/lint/analyzers/commit_subject_prefix.rb +42 -0
- data/lib/git/lint/analyzers/commit_subject_suffix.rb +39 -0
- data/lib/git/lint/analyzers/commit_trailer_collaborator_capitalization.rb +51 -0
- data/lib/git/lint/analyzers/commit_trailer_collaborator_duplication.rb +56 -0
- data/lib/git/lint/analyzers/commit_trailer_collaborator_email.rb +52 -0
- data/lib/git/lint/analyzers/commit_trailer_collaborator_key.rb +56 -0
- data/lib/git/lint/analyzers/commit_trailer_collaborator_name.rb +57 -0
- data/lib/git/lint/branches/environments/circle_ci.rb +28 -0
- data/lib/git/lint/branches/environments/local.rb +28 -0
- data/lib/git/lint/branches/environments/netlify_ci.rb +34 -0
- data/lib/git/lint/branches/environments/travis_ci.rb +57 -0
- data/lib/git/lint/branches/feature.rb +44 -0
- data/lib/git/lint/cli.rb +122 -0
- data/lib/git/lint/collector.rb +64 -0
- data/lib/git/lint/commits/saved.rb +104 -0
- data/lib/git/lint/commits/unsaved.rb +120 -0
- data/lib/git/lint/errors/base.rb +14 -0
- data/lib/git/lint/errors/severity.rb +13 -0
- data/lib/git/lint/errors/sha.rb +13 -0
- data/lib/git/lint/identity.rb +13 -0
- data/lib/git/lint/kit/filter_list.rb +30 -0
- data/lib/git/lint/parsers/trailers/collaborator.rb +57 -0
- data/lib/git/lint/rake/setup.rb +4 -0
- data/lib/git/lint/rake/tasks.rb +33 -0
- data/lib/git/lint/refinements/strings.rb +25 -0
- data/lib/git/lint/reporters/branch.rb +67 -0
- data/lib/git/lint/reporters/commit.rb +30 -0
- data/lib/git/lint/reporters/line.rb +32 -0
- data/lib/git/lint/reporters/lines/paragraph.rb +49 -0
- data/lib/git/lint/reporters/lines/sentence.rb +31 -0
- data/lib/git/lint/reporters/style.rb +52 -0
- data/lib/git/lint/runner.rb +34 -0
- data/lib/git/lint/validators/capitalization.rb +29 -0
- data/lib/git/lint/validators/email.rb +24 -0
- data/lib/git/lint/validators/name.rb +30 -0
- metadata +363 -0
- metadata.gz.sig +3 -0
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Git
|
4
|
+
module Lint
|
5
|
+
module Analyzers
|
6
|
+
class CommitTrailerCollaboratorDuplication < Abstract
|
7
|
+
def self.defaults
|
8
|
+
{
|
9
|
+
enabled: true,
|
10
|
+
severity: :error
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize commit:,
|
15
|
+
settings: self.class.defaults,
|
16
|
+
parser: Parsers::Trailers::Collaborator
|
17
|
+
super commit: commit, settings: settings
|
18
|
+
@parser = parser
|
19
|
+
@tally = build_tally
|
20
|
+
end
|
21
|
+
|
22
|
+
def valid?
|
23
|
+
affected_commit_trailer_lines.empty?
|
24
|
+
end
|
25
|
+
|
26
|
+
def issue
|
27
|
+
return {} if valid?
|
28
|
+
|
29
|
+
{
|
30
|
+
hint: "Avoid duplication.",
|
31
|
+
lines: affected_commit_trailer_lines
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
def invalid_line? line
|
38
|
+
collaborator = parser.new line
|
39
|
+
collaborator.match? && tally[line] != 1
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
attr_reader :parser, :tally
|
45
|
+
|
46
|
+
def build_tally
|
47
|
+
zeros = Hash.new { |new_hash, missing_key| new_hash[missing_key] = 0 }
|
48
|
+
|
49
|
+
zeros.tap do |collection|
|
50
|
+
commit.trailer_lines.each { |line| collection[line] += 1 if parser.new(line).match? }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Git
|
4
|
+
module Lint
|
5
|
+
module Analyzers
|
6
|
+
class CommitTrailerCollaboratorEmail < Abstract
|
7
|
+
def self.defaults
|
8
|
+
{
|
9
|
+
enabled: true,
|
10
|
+
severity: :error
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
# rubocop:disable Metrics/ParameterLists
|
15
|
+
def initialize commit:,
|
16
|
+
settings: self.class.defaults,
|
17
|
+
parser: Parsers::Trailers::Collaborator,
|
18
|
+
validator: Validators::Email
|
19
|
+
|
20
|
+
super commit: commit, settings: settings
|
21
|
+
@parser = parser
|
22
|
+
@validator = validator
|
23
|
+
end
|
24
|
+
# rubocop:enable Metrics/ParameterLists
|
25
|
+
|
26
|
+
def valid?
|
27
|
+
affected_commit_trailer_lines.empty?
|
28
|
+
end
|
29
|
+
|
30
|
+
def issue
|
31
|
+
return {} if valid?
|
32
|
+
|
33
|
+
{
|
34
|
+
hint: %(Email must follow name and use format: "<name@server.domain>".),
|
35
|
+
lines: affected_commit_trailer_lines
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
protected
|
40
|
+
|
41
|
+
def invalid_line? line
|
42
|
+
collaborator = parser.new line
|
43
|
+
collaborator.match? && !validator.new(collaborator.email).valid?
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
attr_reader :parser, :validator
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Git
|
4
|
+
module Lint
|
5
|
+
module Analyzers
|
6
|
+
class CommitTrailerCollaboratorKey < Abstract
|
7
|
+
def self.defaults
|
8
|
+
{
|
9
|
+
enabled: true,
|
10
|
+
severity: :error,
|
11
|
+
includes: ["Co-Authored-By"]
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize commit:,
|
16
|
+
settings: self.class.defaults,
|
17
|
+
parser: Parsers::Trailers::Collaborator
|
18
|
+
super commit: commit, settings: settings
|
19
|
+
@parser = parser
|
20
|
+
end
|
21
|
+
|
22
|
+
def valid?
|
23
|
+
affected_commit_trailer_lines.empty?
|
24
|
+
end
|
25
|
+
|
26
|
+
def issue
|
27
|
+
return {} if valid?
|
28
|
+
|
29
|
+
{
|
30
|
+
hint: "Use format: #{filter_list.to_hint}.",
|
31
|
+
lines: affected_commit_trailer_lines
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
def load_filter_list
|
38
|
+
Kit::FilterList.new settings.fetch :includes
|
39
|
+
end
|
40
|
+
|
41
|
+
def invalid_line? line
|
42
|
+
collaborator = parser.new line
|
43
|
+
key = collaborator.key
|
44
|
+
|
45
|
+
collaborator.match? && !key.empty? && !key.match?(
|
46
|
+
/\A#{Regexp.union filter_list.to_regexp}\Z/
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
attr_reader :parser
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Git
|
4
|
+
module Lint
|
5
|
+
module Analyzers
|
6
|
+
class CommitTrailerCollaboratorName < Abstract
|
7
|
+
def self.defaults
|
8
|
+
{
|
9
|
+
enabled: true,
|
10
|
+
severity: :error,
|
11
|
+
minimum: 2
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
# rubocop:disable Metrics/ParameterLists
|
16
|
+
def initialize commit:,
|
17
|
+
settings: self.class.defaults,
|
18
|
+
parser: Parsers::Trailers::Collaborator,
|
19
|
+
validator: Validators::Name
|
20
|
+
|
21
|
+
super commit: commit, settings: settings
|
22
|
+
@parser = parser
|
23
|
+
@validator = validator
|
24
|
+
end
|
25
|
+
# rubocop:enable Metrics/ParameterLists
|
26
|
+
|
27
|
+
def valid?
|
28
|
+
affected_commit_trailer_lines.empty?
|
29
|
+
end
|
30
|
+
|
31
|
+
def issue
|
32
|
+
return {} if valid?
|
33
|
+
|
34
|
+
{
|
35
|
+
hint: "Name must follow key and consist of #{minimum} parts (minimum).",
|
36
|
+
lines: affected_commit_trailer_lines
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
protected
|
41
|
+
|
42
|
+
def invalid_line? line
|
43
|
+
collaborator = parser.new line
|
44
|
+
collaborator.match? && !validator.new(collaborator.name.strip, minimum: minimum).valid?
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
attr_reader :parser, :validator
|
50
|
+
|
51
|
+
def minimum
|
52
|
+
settings.fetch :minimum
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Git
|
4
|
+
module Lint
|
5
|
+
module Branches
|
6
|
+
module Environments
|
7
|
+
# Provides Circle CI build environment feature branch information.
|
8
|
+
class CircleCI
|
9
|
+
def initialize repo: Git::Kit::Repo.new
|
10
|
+
@repo = repo
|
11
|
+
end
|
12
|
+
|
13
|
+
def name
|
14
|
+
"origin/#{repo.branch_name}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def shas
|
18
|
+
repo.shas start: "origin/master", finish: name
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :repo
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Git
|
4
|
+
module Lint
|
5
|
+
module Branches
|
6
|
+
module Environments
|
7
|
+
# Provides local build environment feature branch information.
|
8
|
+
class Local
|
9
|
+
def initialize repo: Git::Kit::Repo.new
|
10
|
+
@repo = repo
|
11
|
+
end
|
12
|
+
|
13
|
+
def name
|
14
|
+
repo.branch_name
|
15
|
+
end
|
16
|
+
|
17
|
+
def shas
|
18
|
+
repo.shas start: "master", finish: name
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :repo
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "open3"
|
4
|
+
|
5
|
+
module Git
|
6
|
+
module Lint
|
7
|
+
module Branches
|
8
|
+
module Environments
|
9
|
+
# Provides Netlify CI build environment feature branch information.
|
10
|
+
class NetlifyCI
|
11
|
+
def initialize environment: ENV, repo: Git::Kit::Repo.new, shell: Open3
|
12
|
+
@environment = environment
|
13
|
+
@repo = repo
|
14
|
+
@shell = shell
|
15
|
+
end
|
16
|
+
|
17
|
+
def name
|
18
|
+
environment["HEAD"]
|
19
|
+
end
|
20
|
+
|
21
|
+
def shas
|
22
|
+
shell.capture2e "git remote add -f origin #{environment["REPOSITORY_URL"]}"
|
23
|
+
shell.capture2e "git fetch origin #{name}:#{name}"
|
24
|
+
repo.shas start: "origin/master", finish: "origin/#{name}"
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :environment, :repo, :shell
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "open3"
|
4
|
+
|
5
|
+
module Git
|
6
|
+
module Lint
|
7
|
+
module Branches
|
8
|
+
module Environments
|
9
|
+
# Provides Travis CI build environment feature branch information.
|
10
|
+
class TravisCI
|
11
|
+
def initialize environment: ENV, repo: Git::Kit::Repo.new, shell: Open3
|
12
|
+
@environment = environment
|
13
|
+
@repo = repo
|
14
|
+
@shell = shell
|
15
|
+
end
|
16
|
+
|
17
|
+
def name
|
18
|
+
pull_request_branch.empty? ? ci_branch : pull_request_branch
|
19
|
+
end
|
20
|
+
|
21
|
+
def shas
|
22
|
+
prepare_project
|
23
|
+
repo.shas start: "origin/master", finish: name
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :environment, :repo, :shell
|
29
|
+
|
30
|
+
def prepare_project
|
31
|
+
slug = pull_request_slug
|
32
|
+
|
33
|
+
unless slug.empty?
|
34
|
+
shell.capture2e "git remote add -f original_branch https://github.com/#{slug}.git"
|
35
|
+
shell.capture2e "git fetch original_branch #{name}:#{name}"
|
36
|
+
end
|
37
|
+
|
38
|
+
shell.capture2e "git remote set-branches --add origin master"
|
39
|
+
shell.capture2e "git fetch"
|
40
|
+
end
|
41
|
+
|
42
|
+
def ci_branch
|
43
|
+
environment["TRAVIS_BRANCH"]
|
44
|
+
end
|
45
|
+
|
46
|
+
def pull_request_branch
|
47
|
+
environment["TRAVIS_PULL_REQUEST_BRANCH"]
|
48
|
+
end
|
49
|
+
|
50
|
+
def pull_request_slug
|
51
|
+
environment["TRAVIS_PULL_REQUEST_SLUG"]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
module Git
|
6
|
+
module Lint
|
7
|
+
module Branches
|
8
|
+
# Represents a feature branch.
|
9
|
+
class Feature
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
def_delegators :selected_environment, :name, :shas
|
13
|
+
|
14
|
+
def initialize environment: ENV, git_repo: Git::Kit::Repo.new
|
15
|
+
message = "Invalid repository. Are you within a Git-enabled project?"
|
16
|
+
fail Errors::Base, message unless git_repo.exist?
|
17
|
+
|
18
|
+
@current_environment = environment
|
19
|
+
@selected_environment = load_environment
|
20
|
+
end
|
21
|
+
|
22
|
+
def commits
|
23
|
+
shas.map { |sha| Commits::Saved.new sha: sha }
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :current_environment, :selected_environment
|
29
|
+
|
30
|
+
def load_environment
|
31
|
+
if key? "CIRCLECI" then Environments::CircleCI.new
|
32
|
+
elsif key? "NETLIFY" then Environments::NetlifyCI.new environment: current_environment
|
33
|
+
elsif key? "TRAVIS" then Environments::TravisCI.new environment: current_environment
|
34
|
+
else Environments::Local.new
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def key? key
|
39
|
+
current_environment[key] == "true"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/git/lint/cli.rb
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "thor"
|
4
|
+
require "thor/actions"
|
5
|
+
require "runcom"
|
6
|
+
require "pastel"
|
7
|
+
|
8
|
+
module Git
|
9
|
+
module Lint
|
10
|
+
# The Command Line Interface (CLI) for the gem.
|
11
|
+
class CLI < Thor
|
12
|
+
include Thor::Actions
|
13
|
+
|
14
|
+
package_name Identity::VERSION_LABEL
|
15
|
+
|
16
|
+
def self.configuration
|
17
|
+
defaults = Analyzers::Abstract.descendants.reduce({}) do |settings, analyzer|
|
18
|
+
settings.merge analyzer.id => analyzer.defaults
|
19
|
+
end
|
20
|
+
|
21
|
+
Runcom::Config.new "#{Identity::NAME}/configuration.yml", defaults: defaults
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize args = [], options = {}, config = {}
|
25
|
+
super args, options, config
|
26
|
+
@configuration = self.class.configuration
|
27
|
+
@runner = Runner.new configuration: @configuration.to_h
|
28
|
+
@colorizer = Pastel.new
|
29
|
+
rescue Runcom::Errors::Base => error
|
30
|
+
abort error.message
|
31
|
+
end
|
32
|
+
|
33
|
+
desc "-c, [--config]", "Manage gem configuration."
|
34
|
+
map %w[-c --config] => :config
|
35
|
+
method_option :edit,
|
36
|
+
aliases: "-e",
|
37
|
+
desc: "Edit gem configuration.",
|
38
|
+
type: :boolean,
|
39
|
+
default: false
|
40
|
+
method_option :info,
|
41
|
+
aliases: "-i",
|
42
|
+
desc: "Print gem configuration.",
|
43
|
+
type: :boolean,
|
44
|
+
default: false
|
45
|
+
def config
|
46
|
+
path = configuration.current
|
47
|
+
|
48
|
+
if options.edit? then `#{ENV["EDITOR"]} #{path}`
|
49
|
+
elsif options.info?
|
50
|
+
path ? say(path) : say("Configuration doesn't exist.")
|
51
|
+
else help :config
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
desc "-a, [--analyze]", "Analyze feature branch for issues."
|
56
|
+
map %w[-a --analyze] => :analyze
|
57
|
+
method_option :commits,
|
58
|
+
aliases: "-c",
|
59
|
+
desc: "Analyze specific commit SHA(s).",
|
60
|
+
type: :array,
|
61
|
+
default: []
|
62
|
+
def analyze
|
63
|
+
collector = analyze_commits options.commits
|
64
|
+
abort if collector.errors?
|
65
|
+
rescue Errors::Base => error
|
66
|
+
abort colorizer.red("#{Identity::LABEL}: #{error.message}")
|
67
|
+
end
|
68
|
+
|
69
|
+
desc "--hook", "Add Git Hook support."
|
70
|
+
map "--hook" => :hook
|
71
|
+
method_option :commit_message,
|
72
|
+
desc: "Analyze commit message.",
|
73
|
+
banner: "PATH",
|
74
|
+
type: :string
|
75
|
+
def hook
|
76
|
+
if options.commit_message?
|
77
|
+
check_commit_message options.commit_message
|
78
|
+
else
|
79
|
+
help "--hook"
|
80
|
+
end
|
81
|
+
rescue Errors::Base => error
|
82
|
+
abort colorizer.red("#{Identity::LABEL}: #{error.message}")
|
83
|
+
end
|
84
|
+
|
85
|
+
desc "-v, [--version]", "Show gem version."
|
86
|
+
map %w[-v --version] => :version
|
87
|
+
def version
|
88
|
+
say Identity::VERSION_LABEL
|
89
|
+
end
|
90
|
+
|
91
|
+
desc "-h, [--help=COMMAND]", "Show this message or get help for a command."
|
92
|
+
map %w[-h --help] => :help
|
93
|
+
def help task = nil
|
94
|
+
say and super
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
attr_reader :configuration, :runner, :colorizer
|
100
|
+
|
101
|
+
def load_collector shas
|
102
|
+
commits = shas.map { |sha| Commits::Saved.new sha: sha }
|
103
|
+
commits.empty? ? runner.call : runner.call(commits: commits)
|
104
|
+
end
|
105
|
+
|
106
|
+
def analyze_commits shas
|
107
|
+
load_collector(shas).tap do |collector|
|
108
|
+
reporter = Reporters::Branch.new collector: collector
|
109
|
+
say reporter.to_s
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def check_commit_message path
|
114
|
+
commit = Commits::Unsaved.new path: path
|
115
|
+
collector = runner.call commits: commit
|
116
|
+
reporter = Reporters::Branch.new collector: collector
|
117
|
+
say reporter.to_s
|
118
|
+
abort if collector.errors?
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|