gtlab 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/Gitl.yml +42 -0
- data/LICENSE.txt +21 -0
- data/README.md +43 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/gitl +8 -0
- data/gitl.gemspec +39 -0
- data/lib/Command.rb +70 -0
- data/lib/commands/branch.rb +2 -0
- data/lib/commands/create.rb +67 -0
- data/lib/commands/create_tag.rb +104 -0
- data/lib/commands/delete_tag.rb +98 -0
- data/lib/commands/diff.rb +2 -0
- data/lib/commands/forall.rb +2 -0
- data/lib/commands/init.rb +36 -0
- data/lib/commands/push.rb +2 -0
- data/lib/commands/review.rb +262 -0
- data/lib/commands/start.rb +116 -0
- data/lib/commands/status.rb +2 -0
- data/lib/commands/sync.rb +45 -0
- data/lib/config/gitl_config.rb +71 -0
- data/lib/config/work_space_config.rb +25 -0
- data/lib/git_ext.rb +120 -0
- data/lib/gitl.rb +8 -0
- data/lib/gitl/version.rb +3 -0
- data/lib/gitlab_ext.rb +26 -0
- data/lib/sub_command.rb +139 -0
- metadata +175 -0
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'sub_command'
|
2
|
+
|
3
|
+
module Gitl
|
4
|
+
class Start < SubCommand
|
5
|
+
|
6
|
+
self.summary = '创建对应工作分支,并同步到gitlab.'
|
7
|
+
|
8
|
+
self.description = <<-DESC
|
9
|
+
创建对应工作分支,并同步到gitlab.
|
10
|
+
DESC
|
11
|
+
|
12
|
+
self.arguments = [
|
13
|
+
CLAide::Argument.new('working_branch', true, false),
|
14
|
+
CLAide::Argument.new('remote_branch', true, false),
|
15
|
+
]
|
16
|
+
|
17
|
+
def self.options
|
18
|
+
[
|
19
|
+
["--force", "忽略工作分支是否存在,强制执行"],
|
20
|
+
].concat(super)
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(argv)
|
24
|
+
@working_branch = argv.shift_argument
|
25
|
+
@remote_branch = argv.shift_argument
|
26
|
+
@force = argv.flag?('force')
|
27
|
+
super
|
28
|
+
end
|
29
|
+
|
30
|
+
def validate!
|
31
|
+
super
|
32
|
+
if @working_branch.nil?
|
33
|
+
help! 'working_branch is required.'
|
34
|
+
end
|
35
|
+
if @remote_branch.nil?
|
36
|
+
help! 'remote_branch is required.'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def run
|
41
|
+
remote = 'origin'
|
42
|
+
workspace_config = WorkSpaceConfig.new(@remote_branch, @working_branch)
|
43
|
+
|
44
|
+
self.gitl_config.projects.each do |project|
|
45
|
+
project_path = File.expand_path(project.name, './')
|
46
|
+
if File.exist?(project_path)
|
47
|
+
g = Git.open(project_path)
|
48
|
+
else
|
49
|
+
g = Git.clone(project.git, project.name, :path => './')
|
50
|
+
end
|
51
|
+
|
52
|
+
if self.verbose?
|
53
|
+
# g.setLogger(Logger.new(STDOUT))
|
54
|
+
end
|
55
|
+
|
56
|
+
check_uncommit(g, project.name)
|
57
|
+
|
58
|
+
# 更新本地代码
|
59
|
+
g.fetch(remote, :p => true, :t => true)
|
60
|
+
# g.pull_opts(remote, g.current_branch, :p => true)
|
61
|
+
|
62
|
+
if !g.is_remote_branch?(@remote_branch)
|
63
|
+
raise Error.new("remote branch '#{@remote_branch}' does not exist for project '#{project.name}'.")
|
64
|
+
end
|
65
|
+
|
66
|
+
if g.is_remote_branch?(@working_branch) && !@force
|
67
|
+
raise Error.new("branch '#{@working_branch}' exist in remote '#{remote}' for project '#{project.name}'.")
|
68
|
+
end
|
69
|
+
|
70
|
+
if g.is_local_branch?(@working_branch) && !@force
|
71
|
+
raise Error.new("branch '#{@working_branch}' exist in local for project '#{project.name}'.")
|
72
|
+
end
|
73
|
+
|
74
|
+
# g.remote(remote).branch(@remote_branch).checkout()
|
75
|
+
# g.branch(@remote_branch).checkout
|
76
|
+
|
77
|
+
# git_cmd = "git remote set-branches #{remote} '#{@remote_branch}'"
|
78
|
+
# puts `#{git_cmd}`.chomp
|
79
|
+
#
|
80
|
+
# git_cmd = "git fetch --depth 1 #{remote} '#{@remote_branch}'"
|
81
|
+
# puts `#{git_cmd}`.chomp
|
82
|
+
#
|
83
|
+
# $ git remote set-branches origin 'remote_branch_name'
|
84
|
+
# $ git fetch --depth 1 origin remote_branch_name
|
85
|
+
# $ git checkout remote_branch_name
|
86
|
+
|
87
|
+
g.checkout(@remote_branch)
|
88
|
+
|
89
|
+
g.pull(remote, @remote_branch)
|
90
|
+
|
91
|
+
if g.is_local_branch?(@working_branch)
|
92
|
+
g.checkout(@working_branch)
|
93
|
+
g.pull(remote, @working_branch)
|
94
|
+
else
|
95
|
+
puts "create branch '#{@working_branch}' for project '#{project.name}'.".green
|
96
|
+
# 创建本地工作分支
|
97
|
+
g.checkout(@working_branch, :new_branch => true)
|
98
|
+
end
|
99
|
+
|
100
|
+
# 跟踪远程分支
|
101
|
+
g.track(remote, @remote_branch)
|
102
|
+
|
103
|
+
puts "push branch '#{@working_branch}' to remote for project '#{project.name}'.".green
|
104
|
+
# push到origin
|
105
|
+
g.push(remote, @working_branch)
|
106
|
+
|
107
|
+
puts
|
108
|
+
end
|
109
|
+
|
110
|
+
#保存新的workspace配置
|
111
|
+
self.save_workspace_config(workspace_config)
|
112
|
+
|
113
|
+
puts "create work branch '#{@working_branch}' from #{@remote_branch} and push to '#{remote}' success.".green
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'sub_command'
|
2
|
+
|
3
|
+
|
4
|
+
module Gitl
|
5
|
+
|
6
|
+
class Sync < SubCommand
|
7
|
+
|
8
|
+
self.summary = '更新工作分支代码'
|
9
|
+
|
10
|
+
self.description = <<-DESC
|
11
|
+
根据yml配置,更新代码.
|
12
|
+
DESC
|
13
|
+
|
14
|
+
def run_in_workspace
|
15
|
+
|
16
|
+
remote = 'origin'
|
17
|
+
workspace_config = self.workspace_config
|
18
|
+
|
19
|
+
info "current work branch '#{workspace_config.workspace_branch}', remote branch '#{workspace_config.remote_branch}'."
|
20
|
+
|
21
|
+
self.gitl_config.projects.each do |project|
|
22
|
+
project_path = File.expand_path(project.name, './')
|
23
|
+
|
24
|
+
if File.exist?(project_path)
|
25
|
+
info "sync project '#{project.name}'..."
|
26
|
+
g = Git.open(project_path)
|
27
|
+
if workspace_config.workspace_branch != g.current_branch
|
28
|
+
error "current branch is not work branch(#{workspace_config.workspace_branch})."
|
29
|
+
exit(1)
|
30
|
+
end
|
31
|
+
g.fetch(remote, :p => true, :t => true)
|
32
|
+
g.pull("origin", workspace_config.workspace_branch)
|
33
|
+
g.pull("origin", workspace_config.remote_branch)
|
34
|
+
puts
|
35
|
+
|
36
|
+
else
|
37
|
+
error "please run 'gitl init' first."
|
38
|
+
break
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Gitl
|
2
|
+
class GitlConfig
|
3
|
+
|
4
|
+
attr_reader :projects
|
5
|
+
attr_reader :gitlab
|
6
|
+
attr_reader :config_path
|
7
|
+
|
8
|
+
def initialize(config_path, node)
|
9
|
+
@config_path = config_path
|
10
|
+
|
11
|
+
gitlab = node['gitlab']
|
12
|
+
@gitlab = GitlabConfig.new(gitlab)
|
13
|
+
|
14
|
+
@projects = []
|
15
|
+
projects = node['projects']
|
16
|
+
projects.each do |project|
|
17
|
+
projectConfig = ProjectConfig.new(project)
|
18
|
+
@projects << projectConfig
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.load_file(config_path)
|
23
|
+
node = YAML.load_file(config_path)
|
24
|
+
GitlConfig.new(config_path, node)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.load_yml(yml)
|
28
|
+
node = YAML.load(yml)
|
29
|
+
GitlConfig.new(nil, node)
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_dictionary
|
33
|
+
projects = self.projects.map do |project|
|
34
|
+
project.to_dictionary
|
35
|
+
end
|
36
|
+
{"projects"=>projects, "gitlab"=>self.gitlab.to_dictionary}
|
37
|
+
end
|
38
|
+
|
39
|
+
class ProjectConfig
|
40
|
+
attr_reader :name
|
41
|
+
attr_reader :git
|
42
|
+
|
43
|
+
def initialize(node)
|
44
|
+
@name = node['name']
|
45
|
+
@git = node['git']
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_dictionary
|
49
|
+
{"name"=>self.name, "git"=>self.git}
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
class GitlabConfig
|
55
|
+
attr_reader :endpoint
|
56
|
+
attr_accessor :private_token
|
57
|
+
|
58
|
+
def initialize(node)
|
59
|
+
@endpoint = node['endpoint']
|
60
|
+
@private_token = node['private_token']
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_dictionary
|
64
|
+
{"endpoint"=>self.endpoint, "private_token"=>private_token}
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
|
2
|
+
module Gitl
|
3
|
+
class WorkSpaceConfig
|
4
|
+
attr_reader :remote_branch
|
5
|
+
attr_reader :workspace_branch
|
6
|
+
|
7
|
+
def initialize(remote_branch, workspace_branch)
|
8
|
+
@remote_branch = remote_branch
|
9
|
+
@workspace_branch = workspace_branch
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.load_file(yaml_filename)
|
13
|
+
node = YAML.load_file(yaml_filename)
|
14
|
+
remote_branch = node['remote_branch']
|
15
|
+
workspace_branch = node['workspace_branch']
|
16
|
+
return WorkSpaceConfig.new(remote_branch, workspace_branch)
|
17
|
+
end
|
18
|
+
|
19
|
+
def save(path)
|
20
|
+
File.open(path, 'w') do |file|
|
21
|
+
Psych.dump({'remote_branch' => @remote_branch, 'workspace_branch' => @workspace_branch}, file)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/git_ext.rb
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'git'
|
2
|
+
|
3
|
+
|
4
|
+
module Git
|
5
|
+
def self.clone_without_env(repository, name, opts = {})
|
6
|
+
opts = Git::Lib.new.clone_without_env(repository, name, opts)
|
7
|
+
Base.new(opts)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module Patches
|
12
|
+
module Git
|
13
|
+
|
14
|
+
module Base
|
15
|
+
|
16
|
+
def track(remote, branch)
|
17
|
+
self.lib.track(remote, branch)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
module Lib
|
22
|
+
def initialize(*args)
|
23
|
+
super
|
24
|
+
# @logger = Logger.new(STDOUT)
|
25
|
+
end
|
26
|
+
|
27
|
+
def run_command(git_cmd, &block)
|
28
|
+
git_cmd = git_cmd.gsub(/2>&1$/, '')
|
29
|
+
return IO.popen(git_cmd, &block) if block_given?
|
30
|
+
|
31
|
+
`#{git_cmd}`.chomp
|
32
|
+
end
|
33
|
+
|
34
|
+
def track(remote, branch)
|
35
|
+
arr_opts = []
|
36
|
+
arr_opts << '-u'
|
37
|
+
arr_opts << "#{remote}/#{branch}"
|
38
|
+
command('branch', arr_opts)
|
39
|
+
end
|
40
|
+
|
41
|
+
def clone_without_env(repository, name, opts = {})
|
42
|
+
@path = opts[:path] || '.'
|
43
|
+
clone_dir = opts[:path] ? File.join(@path, name) : name
|
44
|
+
|
45
|
+
arr_opts = []
|
46
|
+
arr_opts << '--bare' if opts[:bare]
|
47
|
+
arr_opts << '--branch' << opts[:branch] if opts[:branch]
|
48
|
+
arr_opts << '--depth' << opts[:depth].to_i if opts[:depth] && opts[:depth].to_i > 0
|
49
|
+
arr_opts << '--config' << opts[:gitl_config] if opts[:gitl_config]
|
50
|
+
arr_opts << '--origin' << opts[:remote] || opts[:origin] if opts[:remote] || opts[:origin]
|
51
|
+
arr_opts << '--recursive' if opts[:recursive]
|
52
|
+
arr_opts << "--mirror" if opts[:mirror]
|
53
|
+
|
54
|
+
arr_opts << '--'
|
55
|
+
|
56
|
+
arr_opts << repository
|
57
|
+
arr_opts << clone_dir
|
58
|
+
|
59
|
+
command_without_env('clone', arr_opts)
|
60
|
+
|
61
|
+
(opts[:bare] or opts[:mirror]) ? {:repository => clone_dir} : {:working_directory => clone_dir}
|
62
|
+
end
|
63
|
+
|
64
|
+
def command_without_env(cmd, opts = [], chdir = true, redirect = '', &block)
|
65
|
+
global_opts = []
|
66
|
+
global_opts << "--git-dir=#{@git_dir}" if !@git_dir.nil?
|
67
|
+
global_opts << "--work-tree=#{@git_work_dir}" if !@git_work_dir.nil?
|
68
|
+
|
69
|
+
opts = [opts].flatten.map {|s| escape(s) }.join(' ')
|
70
|
+
|
71
|
+
global_opts = global_opts.flatten.map {|s| escape(s) }.join(' ')
|
72
|
+
|
73
|
+
git_cmd = "#{::Git::Base.config.binary_path} #{global_opts} #{cmd} #{opts} #{redirect} 2>&1"
|
74
|
+
|
75
|
+
output = nil
|
76
|
+
|
77
|
+
command_thread = nil;
|
78
|
+
|
79
|
+
exitstatus = nil
|
80
|
+
|
81
|
+
command_thread = Thread.new do
|
82
|
+
output = run_command(git_cmd, &block)
|
83
|
+
exitstatus = $?.exitstatus
|
84
|
+
end
|
85
|
+
command_thread.join
|
86
|
+
|
87
|
+
if @logger
|
88
|
+
@logger.info(git_cmd)
|
89
|
+
@logger.debug(output)
|
90
|
+
end
|
91
|
+
|
92
|
+
if exitstatus > 1 || (exitstatus == 1 && output != '')
|
93
|
+
raise Git::GitExecuteError.new(git_cmd + ':' + output.to_s)
|
94
|
+
end
|
95
|
+
|
96
|
+
return output
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
module Status
|
102
|
+
def fetch_untracked
|
103
|
+
ignore = @base.lib.ignored_files
|
104
|
+
|
105
|
+
Dir.chdir(@base.dir.path) do
|
106
|
+
Dir.glob('**/*', File::FNM_DOTMATCH) do |file|
|
107
|
+
next if @files[file] || File.directory?(file) ||
|
108
|
+
ignore.include?(file) || file =~ %r{^.git\/.+} || file =~ %r{^(.*\/)?.gitkeep$}
|
109
|
+
|
110
|
+
@files[file] = { path: file, untracked: true }
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
Git::Status.prepend(Patches::Git::Status)
|
119
|
+
Git::Lib.prepend(Patches::Git::Lib)
|
120
|
+
Git::Base.prepend(Patches::Git::Base)
|
data/lib/gitl.rb
ADDED
data/lib/gitl/version.rb
ADDED
data/lib/gitlab_ext.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
module Patches
|
4
|
+
# Defines methods related to projects.
|
5
|
+
# @see https://docs.gitlab.com/ce/api/projects.html
|
6
|
+
module Projects
|
7
|
+
|
8
|
+
# Gets a list of project users.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# Gitlab.project_usesrs(42)
|
12
|
+
# Gitlab.project_usesrs('gitlab')
|
13
|
+
#
|
14
|
+
# @param [Integer, String] project The ID or path of a project.
|
15
|
+
# @param [Hash] options A customizable set of options.
|
16
|
+
# @option options [Integer] :page The page number.
|
17
|
+
# @option options [Integer] :per_page The number of results per page.
|
18
|
+
# @return [Array<Gitlab::ObjectifiedHash>]
|
19
|
+
def project_usesrs(project, options = {})
|
20
|
+
get("/projects/#{url_encode project}/users", query: options)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
Gitlab::Client.prepend(Patches::Projects)
|
data/lib/sub_command.rb
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'command'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'config/gitl_config'
|
4
|
+
require 'colored2'
|
5
|
+
require 'gitlab'
|
6
|
+
require 'git_ext'
|
7
|
+
require 'yaml'
|
8
|
+
|
9
|
+
module Gitl
|
10
|
+
class SubCommand < Command
|
11
|
+
|
12
|
+
self.ignore_in_command_lookup = true
|
13
|
+
attr_reader :gitl_config
|
14
|
+
|
15
|
+
def self.options
|
16
|
+
[
|
17
|
+
['--config=[Gitl.yml]', 'gitl配置, 默认为Gitl.yml'],
|
18
|
+
].concat(super)
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(argv)
|
22
|
+
@yml = argv.option('config')
|
23
|
+
if @yml.nil?
|
24
|
+
@yml = 'Gitl.yml'
|
25
|
+
end
|
26
|
+
super
|
27
|
+
end
|
28
|
+
|
29
|
+
def validate!
|
30
|
+
super
|
31
|
+
end
|
32
|
+
|
33
|
+
def run
|
34
|
+
workspace_path = "./"
|
35
|
+
find_workspace = false;
|
36
|
+
begin
|
37
|
+
result = nil
|
38
|
+
Dir.chdir(workspace_path) do
|
39
|
+
result = Dir.glob('.gitl', File::FNM_DOTMATCH)
|
40
|
+
end
|
41
|
+
if result.length > 0
|
42
|
+
find_workspace = true
|
43
|
+
break
|
44
|
+
else
|
45
|
+
workspace_path = File.expand_path("../", workspace_path)
|
46
|
+
end
|
47
|
+
end while workspace_path.length > 0 && workspace_path != "/"
|
48
|
+
|
49
|
+
if find_workspace
|
50
|
+
Dir.chdir(workspace_path) do
|
51
|
+
self.run_in_workspace()
|
52
|
+
end
|
53
|
+
else
|
54
|
+
raise Error.new("Current path is not gitl workspace.")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def run_in_workspace
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
def gitl_config
|
63
|
+
if File.exist?(@yml)
|
64
|
+
@gitl_config = GitlConfig.load_file(@yml)
|
65
|
+
else
|
66
|
+
help! 'config is required.'
|
67
|
+
end
|
68
|
+
@gitl_config
|
69
|
+
end
|
70
|
+
|
71
|
+
def workspace_config
|
72
|
+
if @workspace_config.nil?
|
73
|
+
filename = '.gitl'
|
74
|
+
# workspace_config_path = File.expand_path(filename, File.dirname(self.gitl_config.config_path))
|
75
|
+
workspace_config_path = filename
|
76
|
+
if !File.exist?(workspace_config_path)
|
77
|
+
help! "workspace config not found. please run 'gitl start' first."
|
78
|
+
end
|
79
|
+
@workspace_config = WorkSpaceConfig.load_file(workspace_config_path)
|
80
|
+
end
|
81
|
+
@workspace_config
|
82
|
+
end
|
83
|
+
|
84
|
+
def save_workspace_config(workspace_config)
|
85
|
+
filename = '.gitl'
|
86
|
+
# workspace_config_path = File.expand_path(filename, File.dirname(self.gitl_config.config_path))
|
87
|
+
workspace_config_path = filename
|
88
|
+
workspace_config.save(workspace_config_path)
|
89
|
+
@workspace_config = workspace_config
|
90
|
+
end
|
91
|
+
|
92
|
+
def check_uncommit(g, project_name)
|
93
|
+
changed = g.status.changed
|
94
|
+
added = g.status.added
|
95
|
+
deleted = g.status.deleted
|
96
|
+
untracked = g.status.untracked
|
97
|
+
|
98
|
+
if !changed.empty?
|
99
|
+
alert = true
|
100
|
+
puts "modified files:".red
|
101
|
+
changed.each do |file, status|
|
102
|
+
puts (" M: " << file).red
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
if !added.empty?
|
107
|
+
alert = true
|
108
|
+
puts "added files:".red
|
109
|
+
added.each do |file, status|
|
110
|
+
puts (" A: " << file).red
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
if !deleted.empty?
|
115
|
+
alert = true
|
116
|
+
puts "deleted files:".red
|
117
|
+
deleted.each do |file, status|
|
118
|
+
puts (" D: " << file).red
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
if !untracked.empty?
|
123
|
+
alert = true
|
124
|
+
puts "untracked files:".red
|
125
|
+
untracked.each do |file, status|
|
126
|
+
puts (" " << file).red
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
if alert
|
131
|
+
puts "exist uncommit files in current branch '#{g.current_branch}' for '#{project_name}'. ignore it? y/n "
|
132
|
+
flag = STDIN.gets.chomp
|
133
|
+
unless flag.downcase == "y"
|
134
|
+
exit
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|