pindo 4.8.7 → 4.8.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/lib/pindo/base/githelper.rb +21 -0
  3. data/lib/pindo/client/pgyerclient.rb +34 -1
  4. data/lib/pindo/client/unityhelper.rb +179 -0
  5. data/lib/pindo/command/android/debug.rb +73 -0
  6. data/lib/pindo/command/android.rb +14 -0
  7. data/lib/pindo/command/deploy/build.rb +2 -3
  8. data/lib/pindo/command/dev/autobuild.rb +2 -0
  9. data/lib/pindo/command/dev/build.rb +4 -1
  10. data/lib/pindo/command/dev/debug.rb +25 -15
  11. data/lib/pindo/command/gplay/iap.rb +43 -0
  12. data/lib/pindo/command/gplay/itcapp.rb +41 -0
  13. data/lib/pindo/command/gplay/metadata.rb +43 -0
  14. data/lib/pindo/command/gplay/screenshots.rb +43 -0
  15. data/lib/pindo/command/gplay/upload.rb +40 -0
  16. data/lib/pindo/command/gplay.rb +17 -0
  17. data/lib/pindo/command/ios/adhoc.rb +190 -0
  18. data/lib/pindo/command/ios/applovin.rb +241 -0
  19. data/lib/pindo/command/ios/autoresign.rb +164 -0
  20. data/lib/pindo/command/ios/build.rb +115 -0
  21. data/lib/pindo/command/ios/cert.rb +164 -0
  22. data/lib/pindo/command/ios/debug.rb +195 -0
  23. data/lib/pindo/command/ios.rb +18 -0
  24. data/lib/pindo/command/pgyer/apptest.rb +5 -1
  25. data/lib/pindo/command/pgyer/upload.rb +4 -4
  26. data/lib/pindo/command/unity/apk.rb +78 -0
  27. data/lib/pindo/command/unity/ipa.rb +152 -0
  28. data/lib/pindo/command/unity.rb +16 -0
  29. data/lib/pindo/command.rb +3 -1
  30. data/lib/pindo/module/appselect.rb +4 -0
  31. data/lib/pindo/module/cert/xcodecerthelper.rb +2 -2
  32. data/lib/pindo/module/pgyer/pgyerhelper.rb +47 -7
  33. data/lib/pindo/module/xcode/xcodehelper.rb +1 -1
  34. data/lib/pindo/options/deployoptions.rb +4 -0
  35. data/lib/pindo/version.rb +1 -1
  36. data/lib/pindo.rb +1 -0
  37. metadata +22 -6
@@ -0,0 +1,115 @@
1
+ require 'highline/import'
2
+ require 'fileutils'
3
+ require 'json'
4
+ require 'xcodeproj'
5
+ require 'gym'
6
+
7
+ module Pindo
8
+ class Command
9
+ class Ios < Command
10
+ class Build < Ios
11
+
12
+ # 命令的简要说明 - 编译iOS工程并可选择上传到测试平台
13
+ self.summary = '编译iOS工程,并支持上传ipa'
14
+
15
+ # 命令的详细说明,包含用法示例
16
+ self.description = <<-DESC
17
+ 编译iOS工程并生成ipa文件。
18
+
19
+ 支持功能:
20
+
21
+ * 编译工程
22
+
23
+ * 生成IPA文件
24
+
25
+ * 支持上传分发
26
+
27
+ 使用示例:
28
+
29
+ $ pindo ios build # 仅编译
30
+
31
+ $ pindo ios build --upload # 编译并上传
32
+
33
+ $ pindo ios build --send # 编译并发送通知
34
+
35
+ $ pindo ios build --proj=myapp # 指定项目名称
36
+ DESC
37
+
38
+ self.arguments = [
39
+
40
+ ]
41
+
42
+ def self.options
43
+ [
44
+ ['--proj', '指定上传到测试平台的项目名称'],
45
+ ['--upload', '上传编译后的ipa到测试平台'],
46
+ ['--send', '上传成功后发送测试通知'],
47
+ ].concat(super)
48
+ end
49
+
50
+ def initialize(argv)
51
+
52
+ @args_upload_flag = argv.flag?('upload', false)
53
+ @args_send_flag = argv.flag?('send', false)
54
+ @args_proj_name = argv.option('proj')
55
+
56
+ if @args_send_flag
57
+ @args_upload_flag = true
58
+ end
59
+
60
+
61
+ super
62
+ @additional_args = argv.remainder!
63
+ end
64
+
65
+ def run
66
+
67
+
68
+ app_info_obj = nil
69
+ if @args_upload_flag
70
+ proj_name = @args_proj_name
71
+ app_info_obj = PgyerHelper.share_instace.prepare_upload(working_directory:Dir.pwd, proj_name:proj_name)
72
+ end
73
+
74
+
75
+ args_temp = []
76
+ Pindo::Command::Deploy::Build::run(args_temp)
77
+
78
+
79
+ pindo_new_project_dir = Dir.pwd
80
+ build_path = File.join(pindo_new_project_dir, "build", "*.{ipa,app,apk}")
81
+ ipa_file_upload = Dir.glob(build_path).max_by {|f| File.mtime(f)}
82
+
83
+ if !ipa_file_upload.nil? && !app_info_obj.nil?
84
+
85
+ description = nil
86
+ if File.exist?(File.join(pindo_new_project_dir, ".release_info"))
87
+ description = File.read(File.join(pindo_new_project_dir, ".release_info"))
88
+ else
89
+ if File.exist?(File.join(pindo_new_project_dir, ".git"))
90
+ description = git!(%W(-C #{pindo_new_project_dir} show -s --format=%h::%s)).strip
91
+ unless !description.nil? && description.length > 10
92
+ description = nil
93
+ end
94
+ end
95
+ end
96
+
97
+ result_data = PgyerHelper.share_instace.start_upload(app_info_obj:app_info_obj, ipa_file_upload:ipa_file_upload, description:description)
98
+ if !result_data.nil? && !result_data["data"].nil? && !result_data["data"]["id"].nil?
99
+ msg_data = PgyerHelper.share_instace.make_msg_data(app_info_obj:app_info_obj, app_version_info_obj:result_data["data"])
100
+
101
+ PgyerHelper.share_instace.print_app_version_info(msg_data:msg_data)
102
+ if @args_send_flag
103
+ PgyerHelper.share_instace.send_apptest_wechat_msg(msg_data:msg_data)
104
+ end
105
+ end
106
+ end
107
+
108
+
109
+ end
110
+
111
+
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,164 @@
1
+ require 'fileutils'
2
+
3
+ module Pindo
4
+ class Command
5
+ class Ios < Command
6
+ class Cert < Ios
7
+
8
+ include Appselect
9
+
10
+ include XcodeCertHelper
11
+ self.summary = '更新iOS证书并使用新证书设置Xcode工程'
12
+ self.description = <<-DESC
13
+ 更新iOS证书并使用新证书设置Xcode工程。
14
+
15
+ 支持功能:
16
+
17
+ * 更新iOS证书
18
+
19
+ * 设置Xcode工程证书
20
+
21
+ * 支持开发和发布证书
22
+
23
+ 使用示例:
24
+
25
+ $ pindo ios cert # 更新开发证书
26
+
27
+ $ pindo ios cert --deploy # 更新发布bundle id
28
+
29
+ $ pindo ios cert --adhoc # 更新adhoc证书
30
+
31
+ $ pindo ios cert --macos # 更新macos平台证书
32
+
33
+ DESC
34
+
35
+ self.arguments = [
36
+
37
+ ]
38
+
39
+ # 命令选项
40
+ def self.options
41
+ [
42
+ ['--deploy', '默认使用开发环境的bundle id,使用此选项切换为发布环境的bundle id'],
43
+ ['--adhoc', '默认使用开发证书,使用此选项切换为adhoc证书'],
44
+ ['--macos', '指定为macOS平台的证书'],
45
+ ['--upload', '生成用于上传到蒲公英平台的证书'],
46
+ ].concat(super)
47
+ end
48
+
49
+ def initialize(argv)
50
+ @args_adhoc_flag = argv.flag?('adhoc', false)
51
+ @args_deploy_flag = argv.flag?('deploy', false)
52
+ @args_macos_flag = argv.flag?('macos', false)
53
+ @upload_flag = argv.flag?('upload', false)
54
+ super
55
+ end
56
+
57
+
58
+ def run
59
+
60
+ mainapp_bundleid= nil
61
+ if @args_deploy_flag
62
+ mainapp_bundleid = get_selected_deploy_bundleid()
63
+ else
64
+ mainapp_bundleid = get_selected_dev_bundleid()
65
+ end
66
+
67
+ args_temp = []
68
+ args_temp << mainapp_bundleid
69
+ Pindo::Command::Deploy::Pullconfig::run(args_temp)
70
+
71
+
72
+ project_dir = Dir.pwd
73
+ Dir.chdir(project_dir)
74
+ config_json_file = File.join(project_dir,"config.json")
75
+ Cert::modify_cert_with_project(project_dir:project_dir, config_file:config_json_file)
76
+
77
+ args_temp = []
78
+ if @args_adhoc_flag
79
+ args_temp << "--adhoc"
80
+ else
81
+ args_temp << "--dev"
82
+ end
83
+
84
+
85
+
86
+ project_fullname = Dir.glob(File.join(project_dir, "/*.xcodeproj")).max_by {|f| File.mtime(f)}
87
+ if !project_fullname.nil?
88
+ project_obj = Xcodeproj::Project.open(project_fullname)
89
+ project_build_platform = project_obj.root_object.build_configuration_list.get_setting("SDKROOT")["Release"]
90
+ if !project_build_platform.nil? && project_build_platform.eql?("macosx")
91
+ @args_macos_flag = true
92
+ end
93
+ end
94
+
95
+ if @args_macos_flag
96
+ args_temp << "--macos"
97
+ end
98
+
99
+ if @upload_flag
100
+ args_temp << "--upload"
101
+ end
102
+
103
+ Pindo::Command::Deploy::Cert::run(args_temp)
104
+
105
+ end
106
+
107
+ def self.modify_cert_with_project(project_dir:nil, config_file:nil)
108
+
109
+ project_fullname = Dir.glob(File.join(project_dir, "/*.xcodeproj")).max_by {|f| File.mtime(f)}
110
+ if !project_fullname.nil?
111
+
112
+ entitlements_plist_path = nil
113
+ project_obj = Xcodeproj::Project.open(project_fullname)
114
+ project_obj.targets.each do |target|
115
+ if target.product_type.to_s.eql?("com.apple.product-type.application") then
116
+ temp_entitlements_file = target.build_configurations.first.build_settings['CODE_SIGN_ENTITLEMENTS']
117
+ if !temp_entitlements_file.nil? && !temp_entitlements_file.empty?
118
+ entitlements_plist_path = File.join(project_dir, temp_entitlements_file)
119
+ end
120
+ end
121
+ end
122
+
123
+ # puts entitlements_plist_path
124
+ if !entitlements_plist_path.nil? && File.exist?(entitlements_plist_path)
125
+ config_json = nil
126
+ if File.exist?(config_file)
127
+ config_json = JSON.parse(File.read(config_file))
128
+ end
129
+
130
+ entitlements_plist_dict = Xcodeproj::Plist.read_from_path(entitlements_plist_path)
131
+
132
+ if entitlements_plist_dict["com.apple.developer.icloud-container-identifiers"].nil?
133
+ if !config_json.nil? && !config_json["app_info"]['app_icloud_id'].nil?
134
+ config_json["app_info"].delete('app_icloud_id')
135
+ end
136
+ end
137
+
138
+ if entitlements_plist_dict["com.apple.security.application-groups"].nil?
139
+ if !config_json.nil? && !config_json["app_info"]['app_group_id'].nil?
140
+ config_json["app_info"].delete('app_group_id')
141
+ end
142
+ end
143
+
144
+ # puts JSON.pretty_generate(config_json)
145
+ if !config_json.nil?
146
+ File.open(config_file, "w") do |f|
147
+ f.write(JSON.pretty_generate(config_json))
148
+ end
149
+
150
+ end
151
+
152
+ end
153
+
154
+ end
155
+
156
+
157
+ end
158
+
159
+
160
+ end
161
+ end
162
+ end
163
+ end
164
+
@@ -0,0 +1,195 @@
1
+ require 'highline/import'
2
+ require 'xcodeproj'
3
+ require 'find'
4
+ require 'fileutils'
5
+ require 'pindo/base/executable'
6
+
7
+ module Pindo
8
+ class Command
9
+ class Ios < Command
10
+ class Debug < Ios
11
+
12
+ include Appselect
13
+ # 命令的简要说明 - 打包iOS工程并发布到蒲公英
14
+ self.summary = '打包iOS工程并发布到蒲公英'
15
+
16
+ # 命令的详细说明,包含用法示例
17
+ self.description = <<-DESC
18
+ 编译iOS Debug包并支持上传到测试平台。
19
+
20
+ 支持功能:
21
+
22
+ * 编译Debug包
23
+
24
+ * 上传到测试平台
25
+
26
+ * 发送测试通知
27
+
28
+ 使用示例:
29
+
30
+ $ pindo ios debug # 编译Debug包
31
+
32
+ $ pindo ios debug --upload # 编译并上传
33
+
34
+ $ pindo ios debug --send # 编译上传并发送通知
35
+
36
+ $ pindo ios debug --proj=myapp # 指定项目名称
37
+ DESC
38
+
39
+ # 命令的参数列表
40
+ self.arguments = [
41
+ # 暂无参数
42
+ ]
43
+
44
+ # 命令的选项列表
45
+ def self.options
46
+ [
47
+ ['--bundleid', '指定打包的bundleID'],
48
+ # 指定上传到蒲公英的项目
49
+ ['--proj', '指定上传到测试平台的项目名称'],
50
+ # 上传编译包
51
+ ['--upload', '上传编译后的ipa到测试平台'],
52
+ # 发送通知
53
+ ['--send', '上传成功后发送测试通知']
54
+ ].concat(super)
55
+ end
56
+
57
+ def initialize(argv)
58
+
59
+ @args_deploy_flag = argv.flag?('deploy', false)
60
+ @args_adhoc_flag = argv.flag?('adhoc', false)
61
+ @args_upload_flag = argv.flag?('upload', false)
62
+ @args_send_flag = argv.flag?('send', false)
63
+ @args_proj_name = argv.option('proj')
64
+ @args_bundle_id = argv.option('bundleid')
65
+
66
+ if @args_send_flag
67
+ @args_upload_flag = true
68
+ end
69
+
70
+ super
71
+ @additional_args = argv.remainder!
72
+ end
73
+
74
+ def validate!
75
+
76
+ super
77
+ end
78
+
79
+ def run
80
+
81
+ mainapp_bundleid= nil
82
+ if @args_bundle_id
83
+ mainapp_bundleid = @args_bundle_id
84
+ else
85
+ if @args_deploy_flag
86
+ mainapp_bundleid = get_selected_deploy_bundleid()
87
+ else
88
+ mainapp_bundleid = get_selected_dev_bundleid()
89
+ end
90
+ end
91
+
92
+ app_info_obj = nil
93
+ if @args_upload_flag
94
+ proj_name = @args_proj_name
95
+ app_info_obj = PgyerHelper.share_instace.prepare_upload(working_directory:Dir.pwd, proj_name:proj_name)
96
+ end
97
+
98
+ args_temp = []
99
+ args_temp << mainapp_bundleid
100
+ Pindo::Command::Deploy::Pullconfig::run(args_temp)
101
+
102
+ project_dir = Dir.pwd
103
+ Dir.chdir(project_dir)
104
+ config_json_file = File.join(project_dir,"config.json")
105
+ Cert::modify_cert_with_project(project_dir:project_dir, config_file:config_json_file)
106
+
107
+ if File.exist?(File.join(project_dir, "Podfile"))
108
+
109
+ args_temp = []
110
+ args_temp << config_json_file
111
+ Pindo::Command::Lib::Update::run([])
112
+
113
+ begin
114
+ if File.exist?(File.join(project_dir, "Podfile.lock"))
115
+ FileUtils.rm_rf(File.join(project_dir, "Podfile.lock"))
116
+ end
117
+ if File.exist?(File.join(project_dir,"Pods"))
118
+ FileUtils.rm_rf(File.join(project_dir, "Pods"))
119
+ end
120
+ puts "正在执行pod deintegrate..."
121
+ system 'pod deintegrate'
122
+ puts "正在执行pod install..."
123
+ system 'pod install'
124
+ rescue => error
125
+ puts(error.to_s)
126
+ raise Informative, "pod install失败!!先pod install 完成后成编译 !"
127
+ end
128
+
129
+ Dir.chdir(project_dir)
130
+ pod_lock_file = File.join(project_dir, "Podfile.lock")
131
+ if !File.exist?(pod_lock_file)
132
+ raise Informative, "pod install失败!!先pod install 完成后成编译 !"
133
+ end
134
+
135
+ end
136
+
137
+
138
+ args_temp = []
139
+ if @args_adhoc_flag
140
+ args_temp << "--adhoc"
141
+ else
142
+ args_temp << "--dev"
143
+ end
144
+
145
+ project_fullname = Dir.glob(File.join(project_dir, "/*.xcodeproj")).max_by {|f| File.mtime(f)}
146
+ if !project_fullname.nil?
147
+ project_obj = Xcodeproj::Project.open(project_fullname)
148
+ project_build_platform = project_obj.root_object.build_configuration_list.get_setting("SDKROOT")["Release"]
149
+ if !project_build_platform.nil? && project_build_platform.eql?("macosx")
150
+ @args_macos_flag = true
151
+ end
152
+ end
153
+
154
+ if @args_macos_flag
155
+ args_temp << "--macos"
156
+ end
157
+
158
+ Pindo::Command::Deploy::Cert::run(args_temp)
159
+
160
+ Dir.chdir(project_dir)
161
+ Pindo::Command::Deploy::Build::run(args_temp)
162
+
163
+
164
+ pindo_new_project_dir = Dir.pwd
165
+ build_path = File.join(pindo_new_project_dir, "build", "*.{ipa,app}")
166
+ ipa_file_upload = Dir.glob(build_path).max_by {|f| File.mtime(f)}
167
+
168
+ if !ipa_file_upload.nil? && !app_info_obj.nil?
169
+ description = nil
170
+ if File.exist?(File.join(pindo_new_project_dir, ".release_info"))
171
+ description = File.read(File.join(pindo_new_project_dir, ".release_info"))
172
+ else
173
+ if File.exist?(File.join(pindo_new_project_dir, ".git"))
174
+ description = git!(%W(-C #{pindo_new_project_dir} show -s --format=%h::%s)).strip
175
+ end
176
+ end
177
+ result_data = PgyerHelper.share_instace.start_upload(app_info_obj:app_info_obj, ipa_file_upload:ipa_file_upload, description:description)
178
+ if !result_data.nil? && !result_data["data"].nil? && !result_data["data"]["id"].nil?
179
+ msg_data = PgyerHelper.share_instace.make_msg_data(app_info_obj:app_info_obj, app_version_info_obj:result_data["data"])
180
+ PgyerHelper.share_instace.print_app_version_info(msg_data:msg_data)
181
+ if @args_send_flag
182
+ PgyerHelper.share_instace.send_apptest_wechat_msg(msg_data:msg_data)
183
+ end
184
+ end
185
+ end
186
+
187
+ system "open #{project_dir}"
188
+
189
+ end
190
+
191
+ end
192
+ end
193
+ end
194
+ end
195
+
@@ -0,0 +1,18 @@
1
+
2
+ require 'pindo/command/ios/cert'
3
+ require 'pindo/command/ios/build'
4
+ require 'pindo/command/ios/debug'
5
+ require 'pindo/command/ios/adhoc'
6
+ require 'pindo/command/ios/autoresign'
7
+ require 'pindo/command/ios/applovin'
8
+
9
+ module Pindo
10
+ class Command
11
+
12
+ class Ios < Command
13
+ self.abstract_command = true
14
+ self.summary = 'iOS相关命令'
15
+ end
16
+
17
+ end
18
+ end
@@ -67,8 +67,12 @@ module Pindo
67
67
  msg_data = PgyerHelper.share_instace.make_msg_data(app_info_obj:app_info_obj, app_version_info_obj:version_item_obj)
68
68
 
69
69
  PgyerHelper.share_instace.print_app_version_info(msg_data:msg_data)
70
+
70
71
  if @args_send_flag
71
- PgyerHelper.share_instace.send_apptest_wechat_msg(msg_data:msg_data)
72
+ # PgyerHelper.share_instace.send_apptest_msg(appId:app_info_obj["appId"], appVersionId:version_item_obj["id"], chatEnv: "PreFavTest", receiveType:"chat")
73
+ # PgyerHelper.share_instace.send_apptest_msg(appId:app_info_obj["appId"], appVersionId:version_item_obj["id"], chatEnv: "DevTest", receiveType:"chat")
74
+ else
75
+ PgyerHelper.share_instace.send_apptest_msg(appId:app_info_obj["appId"], appVersionId:version_item_obj["id"], chatEnv: "", receiveType:"self")
72
76
  end
73
77
 
74
78
  end
@@ -86,15 +86,15 @@ module Pindo
86
86
 
87
87
  current_project_dir = Dir.pwd
88
88
  if @args_ipa_file.nil? || !File.exist?(@args_ipa_file)
89
- build_path = File.join(current_project_dir, "build", "*.{ipa,app}")
89
+ build_path = File.join(current_project_dir, "build", "*.{ipa,app,apk}")
90
90
  @args_ipa_file = Dir.glob(build_path).max_by {|f| File.mtime(f)}
91
91
  if @args_ipa_file.nil?
92
- build_path = File.join(current_project_dir, "*.ipa")
92
+ build_path = File.join(current_project_dir, "*.{ipa,app,apk}")
93
93
  @args_ipa_file = Dir.glob(build_path).max_by {|f| File.mtime(f)}
94
94
  end
95
95
 
96
96
  if !@args_ipa_file.nil?
97
- answer = agree("需要上传的ipa文件是: #{@args_ipa_file} ?(Y/n)")
97
+ answer = agree("需要上传的文件是: #{@args_ipa_file} ?(Y/n)")
98
98
  if !answer
99
99
  @args_ipa_file = nil
100
100
  end
@@ -102,7 +102,7 @@ module Pindo
102
102
 
103
103
  if !@args_ipa_file.nil? && File.exist?(@args_ipa_file)
104
104
  else
105
- @args_ipa_file = ask('需要上传的IPA文件:') || nil
105
+ @args_ipa_file = ask('需要上传的文件:') || nil
106
106
  @args_ipa_file = @args_ipa_file.strip.gsub(/\\ /, ' ')
107
107
  end
108
108
  end
@@ -0,0 +1,78 @@
1
+ require 'highline/import'
2
+ require 'xcodeproj'
3
+ require 'find'
4
+ require 'fileutils'
5
+ require 'pindo/base/executable'
6
+
7
+
8
+ module Pindo
9
+ class Command
10
+ class Unity < Command
11
+ class Apk < Unity
12
+
13
+ # 命令的简要说明 - 编译Unity工程生成Android APK
14
+ self.summary = '编译Unity工程生成Android APK'
15
+
16
+ # 命令的详细说明,包含用法示例
17
+ self.description = <<-DESC
18
+ 编译Unity工程生成Android APK。
19
+
20
+ 支持功能:
21
+
22
+ * 编译生成APK
23
+
24
+ * 上传到测试平台
25
+
26
+ * 发送测试通知
27
+
28
+ 使用示例:
29
+
30
+ $ pindo unity apk # 编译APK
31
+
32
+ $ pindo unity apk --upload # 编译并上传
33
+
34
+ $ pindo unity apk --send # 编译并上传并发送通知
35
+
36
+ $ pindo unity apk --proj=myapp # 指定项目名称
37
+ DESC
38
+
39
+ # 命令的参数列表
40
+ self.arguments = [
41
+
42
+ ]
43
+
44
+
45
+ # 命令的选项列表
46
+ def self.options
47
+ [
48
+ # 指定上传到蒲公英的项目
49
+ ['--proj', '指定上传到测试平台的项目名称'],
50
+ # 上传编译包
51
+ ['--upload', '上传编译后的APK到测试平台'],
52
+ # 发送通知
53
+ ['--send', '上传成功后发送测试通知']
54
+
55
+ ].concat(super)
56
+ end
57
+
58
+ def initialize(argv)
59
+
60
+ @args_proj_name = argv.option('proj')
61
+ @args_upload_flag = argv.flag?('upload', false)
62
+ @args_send_flag = argv.flag?('send', false)
63
+
64
+ if @args_send_flag
65
+ @args_upload_flag = true
66
+ end
67
+
68
+ super
69
+ end
70
+
71
+ def run
72
+ puts "apk"
73
+ end
74
+
75
+ end
76
+ end
77
+ end
78
+ end