circleci-cli 1.0.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.all-contributorsrc +56 -0
- data/.circleci/config.yml +2 -2
- data/.github/workflows/test.yml +22 -0
- data/.rubocop.yml +9 -1
- data/.ruby-version +1 -0
- data/CHANGELOG.md +140 -0
- data/Gemfile.lock +93 -62
- data/README.md +74 -14
- data/Rakefile +8 -0
- data/circleci-cli.gemspec +6 -5
- data/lib/circleci/cli.rb +115 -24
- data/lib/circleci/cli/command/base_command.rb +6 -13
- data/lib/circleci/cli/command/build_command.rb +18 -2
- data/lib/circleci/cli/command/builds_command.rb +9 -9
- data/lib/circleci/cli/command/projects_command.rb +1 -1
- data/lib/circleci/cli/command/retry_command.rb +18 -2
- data/lib/circleci/cli/command/watch_command.rb +46 -43
- data/lib/circleci/cli/command/watch_command/build_repository.rb +44 -0
- data/lib/circleci/cli/command/watch_command/build_watcher.rb +85 -0
- data/lib/circleci/cli/networking/pusher_client.rb +15 -21
- data/lib/circleci/cli/printer.rb +25 -1
- data/lib/circleci/cli/printer/build_printer.rb +18 -6
- data/lib/circleci/cli/printer/project_printer.rb +2 -1
- data/lib/circleci/cli/printer/step_printer.rb +26 -16
- data/lib/circleci/cli/response/account.rb +6 -4
- data/lib/circleci/cli/response/action.rb +1 -0
- data/lib/circleci/cli/response/build.rb +35 -28
- data/lib/circleci/cli/response/step.rb +1 -2
- data/lib/circleci/cli/version.rb +1 -1
- data/movie/rec.gif +0 -0
- metadata +45 -27
data/README.md
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# circleci-cli
|
2
|
-
|
2
|
+
[![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)](#contributors-)
|
3
3
|
[![Gem Version](https://badge.fury.io/rb/circleci-cli.svg)](https://badge.fury.io/rb/circleci-cli)
|
4
4
|
[![Circle CI](https://circleci.com/gh/unhappychoice/circleci-cli.svg?style=shield)](https://circleci.com/gh/unhappychoice/circleci-cli)
|
5
5
|
[![Code Climate](https://codeclimate.com/github/unhappychoice/circleci-cli/badges/gpa.svg)](https://codeclimate.com/github/unhappychoice/circleci-cli)
|
@@ -12,8 +12,6 @@ circleci-cli is a CLI tool for [Circle CI](https://circleci.com).
|
|
12
12
|
|
13
13
|
![sample.gif](https://github.com/unhappychoice/circler/raw/master/movie/rec.gif)
|
14
14
|
|
15
|
-
Notice: This gem is renamed from `circler` to `circleci-cli` on 2019/09/22
|
16
|
-
|
17
15
|
## Installation
|
18
16
|
|
19
17
|
```sh
|
@@ -29,19 +27,59 @@ export CIRCLE_CI_TOKEN=your-circle-ci-token
|
|
29
27
|
## Usage
|
30
28
|
```
|
31
29
|
Commands:
|
32
|
-
circleci-cli browse
|
33
|
-
circleci-cli build
|
34
|
-
circleci-cli builds
|
35
|
-
circleci-cli
|
36
|
-
circleci-cli
|
37
|
-
circleci-cli
|
38
|
-
circleci-cli
|
39
|
-
circleci-cli
|
30
|
+
circleci-cli browse # Open CircleCI website
|
31
|
+
circleci-cli build # Show the build result
|
32
|
+
circleci-cli builds # List builds
|
33
|
+
circleci-cli cancel # Cancel a build
|
34
|
+
circleci-cli help [COMMAND] # Describe available commands or one specific command
|
35
|
+
circleci-cli projects # List projects
|
36
|
+
circleci-cli retry # Retry a build
|
37
|
+
circleci-cli version # Show gem version
|
38
|
+
circleci-cli watch # Watch builds in real time
|
40
39
|
|
41
40
|
Options:
|
42
|
-
-p user_name/project
|
43
|
-
|
44
|
-
|
41
|
+
-p user_name/project # Specify repository
|
42
|
+
# Default to the Git remote of current directory
|
43
|
+
|
44
|
+
-b branch # Specify branch name
|
45
|
+
# Default to the current Git branch
|
46
|
+
|
47
|
+
-a, --all, --no-all # Ignore the branch option and stop being filtered by the branch
|
48
|
+
# Default to false
|
49
|
+
|
50
|
+
-n build_number # Specify build number.
|
51
|
+
-l last # Get or retry last failed build.
|
52
|
+
-v verbose # Show all the logs if applied to watch command.
|
53
|
+
|
54
|
+
--pretty=true/false, --no-pretty # Make outputs pretty or not
|
55
|
+
# Default to true
|
56
|
+
```
|
57
|
+
|
58
|
+
### Examples
|
59
|
+
|
60
|
+
#### Watch your project
|
61
|
+
```
|
62
|
+
$ circleci-cli watch
|
63
|
+
```
|
64
|
+
|
65
|
+
#### Watch your project and show all of the build log
|
66
|
+
```
|
67
|
+
$ circleci-cli watch -v
|
68
|
+
```
|
69
|
+
|
70
|
+
#### Show last failed build
|
71
|
+
```
|
72
|
+
$ circleci-cli build --last
|
73
|
+
```
|
74
|
+
|
75
|
+
#### Retry last failed build
|
76
|
+
```
|
77
|
+
$ circleci-cli retry --last
|
78
|
+
```
|
79
|
+
|
80
|
+
#### Open CircleCI webpage for current project
|
81
|
+
```
|
82
|
+
$ circleci-cli browse
|
45
83
|
```
|
46
84
|
|
47
85
|
## Contributing
|
@@ -51,3 +89,25 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/unhapp
|
|
51
89
|
## License
|
52
90
|
|
53
91
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
92
|
+
|
93
|
+
## Contributors ✨
|
94
|
+
|
95
|
+
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
96
|
+
|
97
|
+
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
98
|
+
<!-- prettier-ignore-start -->
|
99
|
+
<!-- markdownlint-disable -->
|
100
|
+
<table>
|
101
|
+
<tr>
|
102
|
+
<td align="center"><a href="http://blog.unhappychoice.com"><img src="https://avatars3.githubusercontent.com/u/5608948?v=4" width="100px;" alt=""/><br /><sub><b>Yuji Ueki</b></sub></a><br /><a href="https://github.com/unhappychoice/circleci-cli/commits?author=unhappychoice" title="Code">💻</a></td>
|
103
|
+
<td align="center"><a href="https://mattbrictson.com/"><img src="https://avatars0.githubusercontent.com/u/189693?v=4" width="100px;" alt=""/><br /><sub><b>Matt Brictson</b></sub></a><br /><a href="https://github.com/unhappychoice/circleci-cli/commits?author=mattbrictson" title="Code">💻</a> <a href="https://github.com/unhappychoice/circleci-cli/commits?author=mattbrictson" title="Tests">⚠️</a></td>
|
104
|
+
<td align="center"><a href="http://fzf.me"><img src="https://avatars0.githubusercontent.com/u/1462357?v=4" width="100px;" alt=""/><br /><sub><b>Fletcher Fowler</b></sub></a><br /><a href="https://github.com/unhappychoice/circleci-cli/issues?q=author%3Afzf" title="Bug reports">🐛</a> <a href="#ideas-fzf" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/unhappychoice/circleci-cli/commits?author=fzf" title="Code">💻</a></td>
|
105
|
+
<td align="center"><a href="https://datadoghq.com"><img src="https://avatars3.githubusercontent.com/u/583503?v=4" width="100px;" alt=""/><br /><sub><b>Marco Costa</b></sub></a><br /><a href="#ideas-marcotc" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/unhappychoice/circleci-cli/commits?author=marcotc" title="Code">💻</a></td>
|
106
|
+
</tr>
|
107
|
+
</table>
|
108
|
+
|
109
|
+
<!-- markdownlint-enable -->
|
110
|
+
<!-- prettier-ignore-end -->
|
111
|
+
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
112
|
+
|
113
|
+
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
data/Rakefile
CHANGED
@@ -2,7 +2,15 @@
|
|
2
2
|
|
3
3
|
require 'bundler/gem_tasks'
|
4
4
|
require 'rspec/core/rake_task'
|
5
|
+
require 'github_changelog_generator/task'
|
5
6
|
|
6
7
|
RSpec::Core::RakeTask.new(:spec)
|
7
8
|
|
8
9
|
task default: :spec
|
10
|
+
|
11
|
+
GitHubChangelogGenerator::RakeTask.new :changelog do |config|
|
12
|
+
config.user = 'unhappychoice'
|
13
|
+
config.project = 'circleci-cli'
|
14
|
+
config.since_tag = '0.1.0'
|
15
|
+
# config.future_release = '2.2.0'
|
16
|
+
end
|
data/circleci-cli.gemspec
CHANGED
@@ -6,20 +6,20 @@ require 'circleci/cli/version'
|
|
6
6
|
|
7
7
|
def production_dependency(spec)
|
8
8
|
spec.add_dependency 'circleci', '~> 2.0.2'
|
9
|
-
spec.add_dependency '
|
10
|
-
spec.add_dependency 'faraday', '>= 0.14', '< 0.16'
|
9
|
+
spec.add_dependency 'faraday', '>= 0.14', '< 1.1'
|
11
10
|
spec.add_dependency 'highline', '>= 1.7.8', '< 2.1.0'
|
12
|
-
spec.add_dependency 'launchy', '
|
11
|
+
spec.add_dependency 'launchy', '>= 2.4.3', '< 2.6.0'
|
13
12
|
spec.add_dependency 'pusher-client', '~> 0.6.2'
|
14
|
-
spec.add_dependency 'rugged', '>= 0.26', '<
|
13
|
+
spec.add_dependency 'rugged', '>= 0.26', '< 1.1'
|
15
14
|
spec.add_dependency 'terminal-notifier', '~> 2.0.0'
|
16
15
|
spec.add_dependency 'terminal-table', '~> 1.8.0'
|
17
|
-
spec.add_dependency 'thor', '
|
16
|
+
spec.add_dependency 'thor', '>= 0.20', '< 1.1'
|
18
17
|
end
|
19
18
|
|
20
19
|
def development_dependency(spec)
|
21
20
|
spec.add_development_dependency 'bundler'
|
22
21
|
spec.add_development_dependency 'codecov'
|
22
|
+
spec.add_development_dependency 'github_changelog_generator'
|
23
23
|
spec.add_development_dependency 'guard'
|
24
24
|
spec.add_development_dependency 'guard-rspec'
|
25
25
|
spec.add_development_dependency 'guard-rubocop'
|
@@ -38,6 +38,7 @@ end
|
|
38
38
|
Gem::Specification.new do |spec|
|
39
39
|
spec.name = 'circleci-cli'
|
40
40
|
spec.version = CircleCI::CLI::VERSION
|
41
|
+
spec.required_ruby_version = '>= 2.5'
|
41
42
|
spec.authors = ['unhappychoice']
|
42
43
|
spec.email = ['unhappychoice@gmail.com']
|
43
44
|
|
data/lib/circleci/cli.rb
CHANGED
@@ -5,7 +5,6 @@ require 'faraday'
|
|
5
5
|
require 'launchy'
|
6
6
|
require 'terminal-table'
|
7
7
|
require 'highline/import'
|
8
|
-
require 'colorize'
|
9
8
|
require 'rugged'
|
10
9
|
require 'circleci'
|
11
10
|
require 'terminal-notifier'
|
@@ -18,57 +17,149 @@ require 'circleci/cli/networking'
|
|
18
17
|
|
19
18
|
module CircleCI
|
20
19
|
module CLI
|
21
|
-
class Runner < Thor
|
22
|
-
|
23
|
-
|
20
|
+
class Runner < Thor # rubocop:disable Metrics/ClassLength
|
21
|
+
package_name 'circleci-cli'
|
22
|
+
|
23
|
+
class << self
|
24
|
+
def project
|
25
|
+
repository = Rugged::Repository.new('.')
|
26
|
+
origin = repository.remotes.find { |r| r.name == 'origin' }
|
27
|
+
regexp = %r{(?:git@|https://)github.com(?::|/)([\w_-]+/[.\w_-]+?)(?:\.git)*$}
|
28
|
+
return Regexp.last_match(1) if origin.url =~ regexp
|
29
|
+
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def branch_name
|
34
|
+
repository = Rugged::Repository.new('.')
|
35
|
+
head = repository.head
|
36
|
+
|
37
|
+
return nil unless head.branch?
|
38
|
+
|
39
|
+
head.name.sub(%r{\Arefs/heads/}, '')
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
desc 'projects', 'List projects'
|
44
|
+
method_option :pretty, type: :boolean, default: true, desc: 'Make output pretty'
|
24
45
|
def projects
|
25
46
|
Command::ProjectsCommand.run(options)
|
26
47
|
end
|
27
48
|
|
28
|
-
desc 'builds', '
|
29
|
-
method_option :project,
|
30
|
-
|
31
|
-
|
49
|
+
desc 'builds', 'List builds'
|
50
|
+
method_option :project,
|
51
|
+
aliases: 'p',
|
52
|
+
type: :string,
|
53
|
+
banner: 'user/project',
|
54
|
+
default: project,
|
55
|
+
desc: 'A project you want to get.'
|
56
|
+
method_option :branch,
|
57
|
+
aliases: 'b',
|
58
|
+
type: :string,
|
59
|
+
banner: 'some-branch',
|
60
|
+
default: branch_name,
|
61
|
+
desc: 'A branch name you want to filter with.'
|
62
|
+
method_option :all,
|
63
|
+
aliases: 'a',
|
64
|
+
type: :boolean,
|
65
|
+
default: false,
|
66
|
+
desc: 'Target all the branches. This option overwrites branch option.'
|
67
|
+
method_option :pretty, type: :boolean, banner: 'true/false', default: true, desc: 'Make output pretty.'
|
32
68
|
def builds
|
33
69
|
Command::BuildsCommand.run(options)
|
34
70
|
end
|
35
71
|
|
36
|
-
desc 'build', '
|
37
|
-
method_option :project,
|
38
|
-
|
72
|
+
desc 'build', 'Show the build result'
|
73
|
+
method_option :project,
|
74
|
+
aliases: 'p',
|
75
|
+
type: :string,
|
76
|
+
banner: 'user/project',
|
77
|
+
default: project,
|
78
|
+
desc: 'A project you want to get.'
|
79
|
+
method_option :build, aliases: 'n', type: :numeric, banner: 'build-number', desc: 'Build number you want to get.'
|
80
|
+
method_option :last, aliases: 'l', type: :boolean, default: false, desc: 'Get last failed build.'
|
81
|
+
method_option :pretty, type: :boolean, banner: 'true/false', default: true, desc: 'Make output pretty.'
|
39
82
|
def build
|
40
83
|
Command::BuildCommand.run(options)
|
41
84
|
end
|
42
85
|
|
43
|
-
desc 'browse', '
|
44
|
-
method_option :project,
|
45
|
-
|
86
|
+
desc 'browse', 'Open CircleCI website'
|
87
|
+
method_option :project,
|
88
|
+
aliases: 'p',
|
89
|
+
type: :string,
|
90
|
+
banner: 'user/project',
|
91
|
+
default: project,
|
92
|
+
desc: 'A project you want to get.'
|
93
|
+
method_option :build,
|
94
|
+
aliases: 'n',
|
95
|
+
type: :numeric,
|
96
|
+
banner: 'build-number',
|
97
|
+
desc: 'Build number you want to browse.'
|
46
98
|
def browse
|
47
99
|
Command::BrowseCommand.run(options)
|
48
100
|
end
|
49
101
|
|
50
|
-
desc 'retry', '
|
51
|
-
method_option :project,
|
52
|
-
|
102
|
+
desc 'retry', 'Retry a build'
|
103
|
+
method_option :project,
|
104
|
+
aliases: 'p',
|
105
|
+
type: :string,
|
106
|
+
banner: 'user/project',
|
107
|
+
default: project,
|
108
|
+
desc: 'A project you want to get.'
|
109
|
+
method_option :build,
|
110
|
+
aliases: 'n',
|
111
|
+
type: :numeric,
|
112
|
+
banner: 'build-number',
|
113
|
+
desc: 'Build number you want to retry.'
|
114
|
+
method_option :last, aliases: 'l', type: :boolean, desc: 'Retry last failed build.'
|
53
115
|
def retry
|
54
116
|
Command::RetryCommand.run(options)
|
55
117
|
end
|
56
118
|
|
57
|
-
desc 'cancel', '
|
58
|
-
method_option :project,
|
59
|
-
|
119
|
+
desc 'cancel', 'Cancel a build'
|
120
|
+
method_option :project,
|
121
|
+
aliases: 'p',
|
122
|
+
type: :string,
|
123
|
+
default: project,
|
124
|
+
desc: 'A project you want to get.'
|
125
|
+
method_option :build,
|
126
|
+
aliases: 'n',
|
127
|
+
type: :numeric,
|
128
|
+
banner: 'build-number',
|
129
|
+
desc: 'Build number you want to cancel.'
|
60
130
|
def cancel
|
61
131
|
Command::CancelCommand.run(options)
|
62
132
|
end
|
63
133
|
|
64
|
-
desc 'watch', '
|
65
|
-
method_option :project,
|
66
|
-
|
134
|
+
desc 'watch', 'Watch builds in real time'
|
135
|
+
method_option :project,
|
136
|
+
aliases: 'p',
|
137
|
+
type: :string,
|
138
|
+
banner: 'user/project',
|
139
|
+
default: project,
|
140
|
+
desc: 'A project you want to get.'
|
141
|
+
method_option :branch,
|
142
|
+
aliases: 'b',
|
143
|
+
type: :string,
|
144
|
+
banner: 'some-branch',
|
145
|
+
default: branch_name,
|
146
|
+
desc: 'A branch name you want to filter with.'
|
147
|
+
method_option :all,
|
148
|
+
aliases: 'a',
|
149
|
+
type: :boolean,
|
150
|
+
default: false,
|
151
|
+
desc: 'Target all the branches. This option overwrites branch option.'
|
152
|
+
method_option :user, aliases: 'u', type: :string, banner: 'user'
|
153
|
+
method_option :verbose,
|
154
|
+
aliases: 'v',
|
155
|
+
type: :boolean,
|
156
|
+
default: false,
|
157
|
+
desc: 'Show all the build logs including successful build steps.'
|
67
158
|
def watch
|
68
159
|
Command::WatchCommand.run(options)
|
69
160
|
end
|
70
161
|
|
71
|
-
desc 'version', '
|
162
|
+
desc 'version', 'Show gem version'
|
72
163
|
def version
|
73
164
|
say CircleCI::CLI::VERSION
|
74
165
|
end
|
@@ -14,30 +14,23 @@ module CircleCI
|
|
14
14
|
def project_name(options)
|
15
15
|
if options.project
|
16
16
|
options.project
|
17
|
-
elsif reponame
|
18
|
-
reponame
|
19
17
|
else
|
20
18
|
say Printer::ProjectPrinter.new(Response::Project.all).to_s
|
21
19
|
ask('Input user-name/project-name :')
|
22
20
|
end
|
23
21
|
end
|
24
22
|
|
25
|
-
def
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
nil
|
23
|
+
def branch_name(options)
|
24
|
+
if options.all
|
25
|
+
nil
|
26
|
+
else
|
27
|
+
options.branch
|
28
|
+
end
|
32
29
|
end
|
33
30
|
|
34
31
|
def build_number(options)
|
35
32
|
options.build || ask('Input build number')
|
36
33
|
end
|
37
|
-
|
38
|
-
def should_be_pretty(options)
|
39
|
-
options['format'] != 'simple'
|
40
|
-
end
|
41
34
|
end
|
42
35
|
end
|
43
36
|
end
|
@@ -8,9 +8,25 @@ module CircleCI
|
|
8
8
|
def run(options)
|
9
9
|
setup_token
|
10
10
|
username, reponame = project_name(options).split('/')
|
11
|
+
build =
|
12
|
+
if options.last
|
13
|
+
get_last_build(username, reponame)
|
14
|
+
else
|
15
|
+
get_build(username, reponame, options)
|
16
|
+
end
|
17
|
+
say Printer::StepPrinter.new(build.steps, pretty: options.pretty).to_s
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def get_build(username, reponame, options)
|
11
23
|
number = build_number(options)
|
12
|
-
|
13
|
-
|
24
|
+
Response::Build.get(username, reponame, number)
|
25
|
+
end
|
26
|
+
|
27
|
+
def get_last_build(username, reponame)
|
28
|
+
builds = Response::Build.failed(username, reponame)
|
29
|
+
Response::Build.get(username, reponame, builds.map(&:build_number).max)
|
14
30
|
end
|
15
31
|
end
|
16
32
|
end
|
@@ -7,16 +7,16 @@ module CircleCI
|
|
7
7
|
class << self
|
8
8
|
def run(options)
|
9
9
|
setup_token
|
10
|
-
|
10
|
+
project_name = project_name(options)
|
11
|
+
username, reponame = project_name.split('/')
|
12
|
+
branch = branch_name(options)
|
13
|
+
builds = if branch
|
14
|
+
Response::Build.branch(username, reponame, branch)
|
15
|
+
else
|
16
|
+
Response::Build.all(username, reponame)
|
17
|
+
end
|
11
18
|
|
12
|
-
builds
|
13
|
-
if options.branch
|
14
|
-
Response::Build.branch(username, reponame, options.branch)
|
15
|
-
else
|
16
|
-
Response::Build.all(username, reponame)
|
17
|
-
end
|
18
|
-
|
19
|
-
say Printer::BuildPrinter.new(builds, pretty: should_be_pretty(options)).to_s
|
19
|
+
say Printer::BuildPrinter.new(builds, project_name, pretty: options.pretty).to_s
|
20
20
|
end
|
21
21
|
end
|
22
22
|
end
|
@@ -7,7 +7,7 @@ module CircleCI
|
|
7
7
|
class << self
|
8
8
|
def run(options)
|
9
9
|
setup_token
|
10
|
-
say Printer::ProjectPrinter.new(Response::Project.all, pretty:
|
10
|
+
say Printer::ProjectPrinter.new(Response::Project.all, pretty: options.pretty).to_s
|
11
11
|
end
|
12
12
|
end
|
13
13
|
end
|
@@ -8,14 +8,30 @@ module CircleCI
|
|
8
8
|
def run(options)
|
9
9
|
setup_token
|
10
10
|
username, reponame = project_name(options).split('/')
|
11
|
-
number =
|
11
|
+
number = build_number_for(username, reponame, options)
|
12
|
+
|
12
13
|
build = Response::Build.retry(username, reponame, number)
|
13
|
-
|
14
|
+
|
15
|
+
if build&.username
|
14
16
|
say "build #{username}/#{reponame} #{build.build_number} is triggered"
|
15
17
|
else
|
16
18
|
say "failed to trigger #{username}/#{reponame} #{number}"
|
17
19
|
end
|
18
20
|
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def build_number_for(username, reponame, options)
|
25
|
+
if options.last
|
26
|
+
get_last_build_number(username, reponame)
|
27
|
+
else
|
28
|
+
build_number(options)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def get_last_build_number(username, reponame)
|
33
|
+
Response::Build.failed(username, reponame).map(&:build_number).max
|
34
|
+
end
|
19
35
|
end
|
20
36
|
end
|
21
37
|
end
|