git-process-lib 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.
- data/CHANGELOG.md +123 -0
- data/Gemfile +21 -0
- data/Gemfile.lock +57 -0
- data/LICENSE +193 -0
- data/README.md +342 -0
- data/Rakefile +32 -0
- data/bin/git-new-fb +39 -0
- data/bin/git-pull-request +63 -0
- data/bin/git-sync +38 -0
- data/bin/git-to-master +44 -0
- data/docs/git-new-fb.1.adoc +83 -0
- data/docs/git-process.1.adoc +227 -0
- data/docs/git-pull-request.1.adoc +166 -0
- data/docs/git-sync.1.adoc +120 -0
- data/docs/git-to-master.1.adoc +172 -0
- data/git-new-fb.gemspec +20 -0
- data/git-process-lib.gemspec +25 -0
- data/git-process.gemspec +22 -0
- data/git-pull-request.gemspec +20 -0
- data/git-sync.gemspec +20 -0
- data/git-to-master.gemspec +20 -0
- data/lib/git-process/abstract_error_builder.rb +53 -0
- data/lib/git-process/changed_file_helper.rb +115 -0
- data/lib/git-process/git_abstract_merge_error_builder.rb +130 -0
- data/lib/git-process/git_branch.rb +105 -0
- data/lib/git-process/git_branches.rb +81 -0
- data/lib/git-process/git_config.rb +135 -0
- data/lib/git-process/git_lib.rb +646 -0
- data/lib/git-process/git_logger.rb +84 -0
- data/lib/git-process/git_merge_error.rb +28 -0
- data/lib/git-process/git_process.rb +159 -0
- data/lib/git-process/git_process_error.rb +18 -0
- data/lib/git-process/git_process_options.rb +101 -0
- data/lib/git-process/git_rebase_error.rb +30 -0
- data/lib/git-process/git_remote.rb +222 -0
- data/lib/git-process/git_status.rb +108 -0
- data/lib/git-process/github_configuration.rb +298 -0
- data/lib/git-process/github_pull_request.rb +165 -0
- data/lib/git-process/new_fb.rb +49 -0
- data/lib/git-process/parked_changes_error.rb +41 -0
- data/lib/git-process/pull_request.rb +136 -0
- data/lib/git-process/pull_request_error.rb +25 -0
- data/lib/git-process/rebase_to_master.rb +148 -0
- data/lib/git-process/sync_process.rb +55 -0
- data/lib/git-process/syncer.rb +157 -0
- data/lib/git-process/uncommitted_changes_error.rb +23 -0
- data/lib/git-process/version.rb +22 -0
- data/local-build.rb +24 -0
- data/spec/FileHelpers.rb +19 -0
- data/spec/GitRepoHelper.rb +123 -0
- data/spec/changed_file_helper_spec.rb +127 -0
- data/spec/git_abstract_merge_error_builder_spec.rb +64 -0
- data/spec/git_branch_spec.rb +123 -0
- data/spec/git_config_spec.rb +45 -0
- data/spec/git_lib_spec.rb +176 -0
- data/spec/git_logger_spec.rb +66 -0
- data/spec/git_process_spec.rb +208 -0
- data/spec/git_remote_spec.rb +227 -0
- data/spec/git_status_spec.rb +122 -0
- data/spec/github_configuration_spec.rb +152 -0
- data/spec/github_pull_request_spec.rb +117 -0
- data/spec/github_test_helper.rb +49 -0
- data/spec/new_fb_spec.rb +126 -0
- data/spec/pull_request_helper.rb +94 -0
- data/spec/pull_request_spec.rb +137 -0
- data/spec/rebase_to_master_spec.rb +362 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/sync_spec.rb +1474 -0
- metadata +249 -0
@@ -0,0 +1,49 @@
|
|
1
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
2
|
+
# you may not use this file except in compliance with the License.
|
3
|
+
# You may obtain a copy of the License at
|
4
|
+
#
|
5
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
6
|
+
#
|
7
|
+
# Unless required by applicable law or agreed to in writing, software
|
8
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
9
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
10
|
+
# See the License for the specific language governing permissions and
|
11
|
+
# limitations under the License.
|
12
|
+
|
13
|
+
require 'git-process/git_process'
|
14
|
+
|
15
|
+
module GitProc
|
16
|
+
|
17
|
+
class NewFeatureBranch < Process
|
18
|
+
|
19
|
+
def initialize(dir, opts)
|
20
|
+
@branch_name = opts[:branch_name]
|
21
|
+
@local_only = opts[:local]
|
22
|
+
super
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
def runner
|
27
|
+
mybranches = gitlib.branches()
|
28
|
+
on_parking = (mybranches.parking == mybranches.current)
|
29
|
+
|
30
|
+
base_branch = if on_parking and not mybranches[config.integration_branch].contains_all_of(mybranches.parking.name)
|
31
|
+
'_parking_'
|
32
|
+
else
|
33
|
+
config.integration_branch
|
34
|
+
end
|
35
|
+
|
36
|
+
gitlib.fetch if gitlib.has_a_remote? and not @local_only
|
37
|
+
|
38
|
+
logger.info { "Creating #{@branch_name} off of #{base_branch}" }
|
39
|
+
new_branch = gitlib.checkout(@branch_name, :new_branch => base_branch)
|
40
|
+
|
41
|
+
branches = gitlib.branches()
|
42
|
+
branches[@branch_name].upstream(config.integration_branch)
|
43
|
+
branches.parking.delete! if on_parking
|
44
|
+
new_branch
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
2
|
+
# you may not use this file except in compliance with the License.
|
3
|
+
# You may obtain a copy of the License at
|
4
|
+
#
|
5
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
6
|
+
#
|
7
|
+
# Unless required by applicable law or agreed to in writing, software
|
8
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
9
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
10
|
+
# See the License for the specific language governing permissions and
|
11
|
+
# limitations under the License.
|
12
|
+
|
13
|
+
require 'git-process/git_process_error'
|
14
|
+
|
15
|
+
module GitProc
|
16
|
+
|
17
|
+
class ParkedChangesError < GitProcessError
|
18
|
+
include GitProc::AbstractErrorBuilder
|
19
|
+
|
20
|
+
attr_reader :error_message, :lib
|
21
|
+
|
22
|
+
|
23
|
+
def initialize(lib)
|
24
|
+
@lib = lib
|
25
|
+
msg = build_message
|
26
|
+
super(msg)
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
def human_message
|
31
|
+
"You made your changes on the the '_parking_' branch instead of a feature branch.\n"+'Please rename the branch to be a feature branch.'
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
def build_commands
|
36
|
+
['git branch -m _parking_ my_feature_branch']
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
2
|
+
# you may not use this file except in compliance with the License.
|
3
|
+
# You may obtain a copy of the License at
|
4
|
+
#
|
5
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
6
|
+
#
|
7
|
+
# Unless required by applicable law or agreed to in writing, software
|
8
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
9
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
10
|
+
# See the License for the specific language governing permissions and
|
11
|
+
# limitations under the License.
|
12
|
+
|
13
|
+
require 'git-process/git_process'
|
14
|
+
require 'git-process/github_pull_request'
|
15
|
+
require 'git-process/pull_request_error'
|
16
|
+
require 'git-process/syncer'
|
17
|
+
require 'highline/import'
|
18
|
+
|
19
|
+
|
20
|
+
module GitProc
|
21
|
+
|
22
|
+
class PullRequest < Process
|
23
|
+
|
24
|
+
def initialize(dir, opts)
|
25
|
+
super
|
26
|
+
@base_branch = opts[:base_branch]
|
27
|
+
@head_branch = opts[:head_branch]
|
28
|
+
@_repo_name = opts[:repo_name]
|
29
|
+
@_remote_name = opts[:server]
|
30
|
+
@pr_number = opts[:prNumber]
|
31
|
+
@title = opts[:title]
|
32
|
+
@description = opts[:description]
|
33
|
+
@user = opts[:user]
|
34
|
+
@password = opts[:password]
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
def runner
|
39
|
+
if @pr_number.nil? or @pr_number.empty?
|
40
|
+
pr = create_pull_request
|
41
|
+
logger.info { "Created pull request at #{pr.html_url}" }
|
42
|
+
else
|
43
|
+
checkout_pull_request
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
def create_pull_request_client(remote_name, repo_name)
|
49
|
+
PullRequest.create_pull_request_client(self, remote_name, repo_name, @user, @password)
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
def create_pull_request
|
54
|
+
current_branch = gitlib.branches.current.name
|
55
|
+
base_branch = @base_branch || config.master_branch
|
56
|
+
head_branch = @head_branch || current_branch
|
57
|
+
title = @title || current_branch
|
58
|
+
description = @description || ''
|
59
|
+
|
60
|
+
PullRequest.create_pull_request(gitlib, remote.name, remote.name, remote.repo_name, current_branch, base_branch, head_branch, title, description, logger, @user, @password)
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
def checkout_pull_request
|
65
|
+
PullRequest.checkout_pull_request(gitlib, @pr_number, remote.name, remote.repo_name, @user, @password, logger)
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
class << self
|
70
|
+
|
71
|
+
def create_pull_request_client(lib, remote_name, repo_name, username, password)
|
72
|
+
GitHub::PullRequest.new(lib, remote_name, repo_name, {:user => username, :password => password})
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
def create_pull_request(lib, server_name, remote_name, repo_name, current_branch, base_branch, head_branch, title, description, logger, username, password)
|
77
|
+
if base_branch == head_branch
|
78
|
+
raise PullRequestError.new("Can not create a pull request where the base branch and head branch are the same: #{base_branch}")
|
79
|
+
end
|
80
|
+
|
81
|
+
lib.push(server_name, current_branch, current_branch, :force => false)
|
82
|
+
pr = create_pull_request_client(lib, remote_name, repo_name, username, password)
|
83
|
+
pr.create(base_branch, head_branch, title, description)
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
def checkout_pull_request(lib, pr_number, remote_name, repo_name, username, password, logger)
|
88
|
+
logger.info { 'Getting #pr_number' }
|
89
|
+
|
90
|
+
lib.fetch(remote_name)
|
91
|
+
|
92
|
+
pr = create_pull_request_client(lib, remote_name, repo_name, username, password)
|
93
|
+
json = pr.pull_request(pr_number)
|
94
|
+
head_branch_name = json.head.ref
|
95
|
+
base_branch_name = json.base.ref
|
96
|
+
|
97
|
+
remote_head_server_name = match_remote_to_pr_remote(lib, json.head.repo.ssh_url)
|
98
|
+
remote_base_server_name = match_remote_to_pr_remote(lib, json.base.repo.ssh_url)
|
99
|
+
lib.checkout(head_branch_name, :new_branch => "#{remote_head_server_name}/#{head_branch_name}")
|
100
|
+
lib.branch(head_branch_name, :upstream => "#{remote_base_server_name}/#{base_branch_name}")
|
101
|
+
#logger.info(json.to_hash)
|
102
|
+
|
103
|
+
lib.fetch(remote_base_server_name) if remote_head_server_name != remote_base_server_name
|
104
|
+
Syncer.rebase_sync(lib, true)
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
def match_remote_to_pr_remote(lib, pr_remote)
|
109
|
+
pr_url = lib.remote.expanded_url(nil, pr_remote)
|
110
|
+
servers = lib.remote.remote_names
|
111
|
+
server_urls = servers.collect { |s| {:server_name => s, :url => lib.remote.expanded_url(s)} }
|
112
|
+
|
113
|
+
pair = server_urls.find do |su|
|
114
|
+
url = su[:url]
|
115
|
+
uri = URI.parse(url)
|
116
|
+
host = uri.host
|
117
|
+
path = uri.path
|
118
|
+
|
119
|
+
pr_uri = URI.parse(lib.remote.expanded_url(nil, pr_url))
|
120
|
+
pr_host = pr_uri.host
|
121
|
+
pr_path = pr_uri.path
|
122
|
+
|
123
|
+
pr_host == host and pr_path == path
|
124
|
+
end
|
125
|
+
|
126
|
+
if pair.nil?
|
127
|
+
raise GitHubService::NoRemoteRepository.new("Could not match pull request url (#{pr_url}) to any of the registered remote urls: #{server_urls.map {|s| '{' + s[:server_name] + ': ' + s[:url] + '}'}.join(', ')}")
|
128
|
+
end
|
129
|
+
|
130
|
+
pair[:server_name]
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
2
|
+
# you may not use this file except in compliance with the License.
|
3
|
+
# You may obtain a copy of the License at
|
4
|
+
#
|
5
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
6
|
+
#
|
7
|
+
# Unless required by applicable law or agreed to in writing, software
|
8
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
9
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
10
|
+
# See the License for the specific language governing permissions and
|
11
|
+
# limitations under the License.
|
12
|
+
|
13
|
+
require 'git-process/git_process_error'
|
14
|
+
|
15
|
+
module GitProc
|
16
|
+
|
17
|
+
class PullRequestError < GitProcessError
|
18
|
+
|
19
|
+
def initialize(msg)
|
20
|
+
super(msg)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
2
|
+
# you may not use this file except in compliance with the License.
|
3
|
+
# You may obtain a copy of the License at
|
4
|
+
#
|
5
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
6
|
+
#
|
7
|
+
# Unless required by applicable law or agreed to in writing, software
|
8
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
9
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
10
|
+
# See the License for the specific language governing permissions and
|
11
|
+
# limitations under the License.
|
12
|
+
|
13
|
+
require 'git-process/git_process'
|
14
|
+
require 'git-process/git_rebase_error'
|
15
|
+
require 'git-process/git_process_error'
|
16
|
+
require 'git-process/parked_changes_error'
|
17
|
+
require 'git-process/uncommitted_changes_error'
|
18
|
+
require 'git-process/github_pull_request'
|
19
|
+
require 'git-process/pull_request'
|
20
|
+
require 'git-process/syncer'
|
21
|
+
|
22
|
+
|
23
|
+
module GitProc
|
24
|
+
|
25
|
+
class RebaseToMaster < Process
|
26
|
+
|
27
|
+
def initialize(dir, opts)
|
28
|
+
@keep = opts[:keep]
|
29
|
+
@pr_number = opts[:prNumber]
|
30
|
+
@user = opts[:user]
|
31
|
+
@password = opts[:password]
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
def verify_preconditions
|
37
|
+
super
|
38
|
+
|
39
|
+
raise UncommittedChangesError.new unless gitlib.status.clean?
|
40
|
+
raise ParkedChangesError.new(gitlib) if is_parked?
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
def runner
|
45
|
+
if remote.exists?
|
46
|
+
gitlib.fetch(remote.name)
|
47
|
+
|
48
|
+
unless @pr_number.nil? or @pr_number.empty?
|
49
|
+
checkout_pull_request
|
50
|
+
end
|
51
|
+
|
52
|
+
Syncer.rebase_sync(gitlib, true)
|
53
|
+
current = gitlib.branches.current.name
|
54
|
+
gitlib.push(remote.name, current, config.master_branch)
|
55
|
+
|
56
|
+
unless @keep
|
57
|
+
close_pull_request
|
58
|
+
remove_feature_branch
|
59
|
+
gitlib.delete_sync_control_file!(current) if gitlib.sync_control_file_exists?(current)
|
60
|
+
end
|
61
|
+
else
|
62
|
+
Syncer.rebase_sync(gitlib, true)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
def checkout_pull_request
|
68
|
+
PullRequest.checkout_pull_request(gitlib, @pr_number, remote.name, remote.repo_name, @user, @password, logger)
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
def remove_feature_branch
|
73
|
+
mybranches = gitlib.branches
|
74
|
+
|
75
|
+
remote_master = mybranches[remote.master_branch_name]
|
76
|
+
current_branch = mybranches.current
|
77
|
+
logger.debug { "Removing feature branch (#{current_branch})" }
|
78
|
+
|
79
|
+
unless remote_master.contains_all_of(current_branch.name)
|
80
|
+
raise GitProcessError.new("Branch '#{current_branch.name}' has not been merged into '#{remote.master_branch_name}'")
|
81
|
+
end
|
82
|
+
|
83
|
+
parking_branch = mybranches['_parking_']
|
84
|
+
if parking_branch
|
85
|
+
if parking_branch.is_ahead_of(remote_master.name) and
|
86
|
+
!current_branch.contains_all_of(parking_branch.name)
|
87
|
+
|
88
|
+
parking_branch.rename('_parking_OLD_')
|
89
|
+
|
90
|
+
logger.warn { bad_parking_branch_msg }
|
91
|
+
else
|
92
|
+
parking_branch.delete!
|
93
|
+
end
|
94
|
+
end
|
95
|
+
remote_master.checkout_to_new('_parking_', :no_track => true)
|
96
|
+
|
97
|
+
current_branch.delete!(true)
|
98
|
+
if mybranches["#{remote.name}/#{current_branch.name}"]
|
99
|
+
gitlib.push(remote.name, nil, nil, :delete => current_branch.name)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
def close_pull_request
|
105
|
+
pr = GitHub::PullRequest.new(gitlib, remote.name, remote.repo_name)
|
106
|
+
|
107
|
+
# Assume that if we haven't done something that would create the
|
108
|
+
# GitHub auth token, then this likely isn't a GitHub-based repo.
|
109
|
+
# (Or at least the user isn't using pull requests)
|
110
|
+
if pr.configuration.get_config_auth_token
|
111
|
+
begin
|
112
|
+
if @pr_number
|
113
|
+
pr.close(@pr_number)
|
114
|
+
else
|
115
|
+
mybranches = gitlib.branches()
|
116
|
+
pull = pr.find_pull_request(config.master_branch, mybranches.current.name)
|
117
|
+
if pull
|
118
|
+
pr.close(pull[:number])
|
119
|
+
else
|
120
|
+
logger.debug { "There is no pull request for #{mybranches.current.name} against #{config.master_branch}" }
|
121
|
+
end
|
122
|
+
end
|
123
|
+
rescue GitHubService::NoRemoteRepository => exp
|
124
|
+
logger.debug exp.to_s
|
125
|
+
end
|
126
|
+
else
|
127
|
+
logger.debug 'There is no GitHub auth token defined, so not trying to close a pull request.'
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
private
|
133
|
+
|
134
|
+
|
135
|
+
def bad_parking_branch_msg
|
136
|
+
hl = HighLine.new
|
137
|
+
hl.color(
|
138
|
+
"\n***********************************************************************************************\n\n"+
|
139
|
+
"There is an old '_parking_' branch with unacounted changes in it.\n"+
|
140
|
+
"It has been renamed to '_parking_OLD_'.\n"+
|
141
|
+
"Please rename the branch to what the changes are about (`git branch -m _parking_OLD_ my_fb_name`),\n"+
|
142
|
+
" or remove it altogher (`git branch -D _parking_OLD_`).\n\n"+
|
143
|
+
"***********************************************************************************************\n", :red, :bold)
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
2
|
+
# you may not use this file except in compliance with the License.
|
3
|
+
# You may obtain a copy of the License at
|
4
|
+
#
|
5
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
6
|
+
#
|
7
|
+
# Unless required by applicable law or agreed to in writing, software
|
8
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
9
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
10
|
+
# See the License for the specific language governing permissions and
|
11
|
+
# limitations under the License.
|
12
|
+
|
13
|
+
require 'git-process/git_process'
|
14
|
+
require 'git-process/parked_changes_error'
|
15
|
+
require 'git-process/syncer'
|
16
|
+
require 'git-process/changed_file_helper'
|
17
|
+
|
18
|
+
|
19
|
+
module GitProc
|
20
|
+
|
21
|
+
class Sync < Process
|
22
|
+
|
23
|
+
def initialize(base, opts)
|
24
|
+
super
|
25
|
+
|
26
|
+
@opts = opts
|
27
|
+
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
#noinspection RubyControlFlowConversionInspection
|
33
|
+
def verify_preconditions
|
34
|
+
super
|
35
|
+
|
36
|
+
if not gitlib.status.clean?
|
37
|
+
change_file_helper.offer_to_help_uncommitted_changes
|
38
|
+
end
|
39
|
+
|
40
|
+
raise ParkedChangesError.new(self) if gitlib.is_parked?
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
def cleanup
|
45
|
+
gitlib.stash_pop if @stash_pushed
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
def runner
|
50
|
+
GitProc::Syncer.do_sync(gitlib, @opts)
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|