pod-binary-pack 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
- data/CHANGELOG.md +15 -0
- data/LICENSE.txt +21 -0
- data/README.md +147 -0
- data/exe/pod-binary-pack +6 -0
- data/lib/core/auto_build.rb +299 -0
- data/lib/core/build_item.rb +69 -0
- data/lib/core/cli.rb +442 -0
- data/lib/core/commit.rb +19 -0
- data/lib/core/config.rb +56 -0
- data/lib/core/cost.rb +68 -0
- data/lib/core/git_helper.rb +222 -0
- data/lib/core/github_fork.rb +425 -0
- data/lib/core/github_helper.rb +334 -0
- data/lib/core/hook_manager.rb +32 -0
- data/lib/core/hosting/gitee_client.rb +114 -0
- data/lib/core/hosting/gitee_provider.rb +312 -0
- data/lib/core/hosting/gitlab_provider.rb +342 -0
- data/lib/core/hosting/provider.rb +96 -0
- data/lib/core/hosting.rb +43 -0
- data/lib/core/logger.rb +66 -0
- data/lib/core/podspec.rb +126 -0
- data/lib/core/push.rb +80 -0
- data/lib/core/repo.rb +284 -0
- data/lib/core/repo_cocoapods.rb +64 -0
- data/lib/core/repo_module.rb +281 -0
- data/lib/core/repo_module_binary.rb +439 -0
- data/lib/core/repo_module_source.rb +506 -0
- data/lib/core/repo_xcode.rb +41 -0
- data/lib/core/repo_xcode_package.rb +713 -0
- data/lib/core/repo_xcode_shell.rb +170 -0
- data/lib/core/util/file_helper.rb +100 -0
- data/lib/core/util/string.rb +35 -0
- data/lib/pod/binary_pack/version.rb +7 -0
- data/lib/pod/binary_pack.rb +9 -0
- metadata +144 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'net/http'
|
|
6
|
+
require 'uri'
|
|
7
|
+
require_relative 'push'
|
|
8
|
+
require 'singleton'
|
|
9
|
+
|
|
10
|
+
module Build
|
|
11
|
+
class GithubHelper
|
|
12
|
+
include Singleton
|
|
13
|
+
|
|
14
|
+
attr_accessor :path
|
|
15
|
+
attr_accessor :pod_name
|
|
16
|
+
attr_accessor :pod_version
|
|
17
|
+
attr_accessor :third
|
|
18
|
+
attr_accessor :pusher
|
|
19
|
+
attr_accessor :github_tag
|
|
20
|
+
attr_accessor :github_url
|
|
21
|
+
attr_accessor :github_ssh_url
|
|
22
|
+
attr_accessor :podspec_content
|
|
23
|
+
|
|
24
|
+
# 是否为http归档组件(podspec source为{http: 归档地址}形态,无git源码仓)
|
|
25
|
+
attr_accessor :http_source
|
|
26
|
+
# http归档下载地址(source.http声明的原始地址)
|
|
27
|
+
attr_accessor :archive_url
|
|
28
|
+
# 归档类型(zip/tar.gz/tgz/tar.bz2/tar.xz)
|
|
29
|
+
attr_accessor :archive_type
|
|
30
|
+
# source.http中声明的校验摘要(未声明时为nil)
|
|
31
|
+
attr_accessor :archive_sha256_declared
|
|
32
|
+
attr_accessor :archive_sha1_declared
|
|
33
|
+
# 下载后实际计算的sha256(下载阶段写入,供发布podspec的build_metadata溯源)
|
|
34
|
+
attr_accessor :archive_sha_actual
|
|
35
|
+
# 是否解压后上移顶层目录(source.http.flatten=true)
|
|
36
|
+
attr_accessor :archive_flatten
|
|
37
|
+
# 下载到本地的podspec.json绝对路径(播种到归档目录时使用)
|
|
38
|
+
attr_accessor :podspec_json_path
|
|
39
|
+
|
|
40
|
+
def initialize
|
|
41
|
+
@http_source = false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def request_podspec(path, pod_name, pod_version)
|
|
45
|
+
@path = path
|
|
46
|
+
@pod_name = pod_name
|
|
47
|
+
@pod_version = pod_version
|
|
48
|
+
@pusher = Push.instance
|
|
49
|
+
|
|
50
|
+
download_podspec
|
|
51
|
+
analyzer_tag
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# http归档信息汇总(http归档git化流程使用);非http组件返回nil
|
|
55
|
+
# @return [Hash, nil] {url:, type:, sha256:, sha1:, flatten:, version:}
|
|
56
|
+
def archive_info
|
|
57
|
+
return nil unless @http_source
|
|
58
|
+
|
|
59
|
+
{
|
|
60
|
+
url: @archive_url,
|
|
61
|
+
type: @archive_type,
|
|
62
|
+
sha256: @archive_sha256_declared,
|
|
63
|
+
sha1: @archive_sha1_declared,
|
|
64
|
+
flatten: @archive_flatten,
|
|
65
|
+
version: @github_tag
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
def download_podspec
|
|
71
|
+
Build.log.step("(#{File.basename(self.path)})从github上下载podspec文件")
|
|
72
|
+
|
|
73
|
+
unless Dir.exist?(self.path)
|
|
74
|
+
Dir.mkdir(self.path)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
podspec_json_path = "#{self.path}/#{pod_name}.podspec.json"
|
|
78
|
+
|
|
79
|
+
pod_name_md5 = Digest::MD5.hexdigest(pod_name)
|
|
80
|
+
pod_name_md5_0 = pod_name_md5[0..0]
|
|
81
|
+
pod_name_md5_1 = pod_name_md5[1..1]
|
|
82
|
+
pod_name_md5_2 = pod_name_md5[2..2]
|
|
83
|
+
# 存为实例变量:analyzer_tag 方法也要用该路径打印所有版本 podspec 地址,
|
|
84
|
+
# 不能用局部变量(跨方法不可见会被解析为 self.split_path 方法调用并报 NameError)
|
|
85
|
+
@split_path = "#{pod_name_md5_0}/#{pod_name_md5_1}/#{pod_name_md5_2}"
|
|
86
|
+
# podspec 下载源候选地址,按可用性优先级排列,请求失败自动切换下一个
|
|
87
|
+
raw_json_url = "https://raw.githubusercontent.com/CocoaPods/Specs/master/Specs/#{@split_path}/#{pod_name}/#{pod_version}/#{pod_name}.podspec.json"
|
|
88
|
+
podspec_json_urls = [
|
|
89
|
+
# jsDelivr Fastly 线路(CDN,命中的文件速度快;源上不存在时可能 301 回源 raw)
|
|
90
|
+
"https://fastly.jsdelivr.net/cocoa/Specs/#{@split_path}/#{pod_name}/#{pod_version}/#{pod_name}.podspec.json",
|
|
91
|
+
# jsDelivr GCore 线路
|
|
92
|
+
"https://gcore.jsdelivr.net/cocoa/Specs/#{@split_path}/#{pod_name}/#{pod_version}/#{pod_name}.podspec.json",
|
|
93
|
+
# ghproxy.net 代理(国内可达,如实透传 GitHub raw 的 200/404,版本不存在时可据此准确判定)
|
|
94
|
+
"https://ghproxy.net/#{raw_json_url}",
|
|
95
|
+
# gh-proxy.com 代理(同上,备用)
|
|
96
|
+
"https://gh-proxy.com/#{raw_json_url}"
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
Build.log.detail("组件md5裁切:#{@split_path}")
|
|
100
|
+
|
|
101
|
+
# GitHub 上该组件的 podspec 目录(人工核对组件名大小写/版本是否存在时用)
|
|
102
|
+
specs_page_url = "https://github.com/CocoaPods/Specs/tree/master/Specs/#{@split_path}/#{pod_name}"
|
|
103
|
+
|
|
104
|
+
# 依次尝试候选下载源,任一成功即停止,全部失败才终止打包
|
|
105
|
+
podspec_content = nil
|
|
106
|
+
podspec_json_url = nil
|
|
107
|
+
not_found_count = 0
|
|
108
|
+
podspec_json_urls.each_with_index do |candidate_url, index|
|
|
109
|
+
begin
|
|
110
|
+
# 跟随 301/302:jsDelivr 对源上不存在的文件会 301 回源 raw.githubusercontent,
|
|
111
|
+
# 必须跟随到最终地址,以最终状态码判断是否真的 404
|
|
112
|
+
response = fetch_with_redirect(candidate_url)
|
|
113
|
+
# 非 2xx 状态码视为该下载源不可用,切换下一个下载源
|
|
114
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
115
|
+
not_found_count += 1 if response.code == '404'
|
|
116
|
+
Build.log.detail("下载源(#{index + 1}/#{podspec_json_urls.size})返回状态码 #{response.code}:#{candidate_url}")
|
|
117
|
+
next
|
|
118
|
+
end
|
|
119
|
+
podspec_content = JSON.parse(response.body)
|
|
120
|
+
podspec_json_url = candidate_url
|
|
121
|
+
break
|
|
122
|
+
rescue => exception
|
|
123
|
+
Build.log.detail("下载源(#{index + 1}/#{podspec_json_urls.size})请求失败:#{exception.message}")
|
|
124
|
+
Build.log.detail(" 地址:#{candidate_url}")
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
unless podspec_content
|
|
129
|
+
# 各源尝试时已逐个打印过具体 URL,失败块不再重复罗列,只给结论和权威核对地址
|
|
130
|
+
if not_found_count > 0
|
|
131
|
+
Build.log.error("有下载源返回 404,GitHub Specs 上大概率不存在组件 '#{pod_name}' 的 #{pod_version} 版本,请核对组件名大小写(如 fmdb 应为 FMDB)与版本号")
|
|
132
|
+
else
|
|
133
|
+
Build.log.error("所有podspec下载源均未成功(可能是网络不可达或镜像回源失败),请检查网络后重试;若网络正常仍失败,通常是组件名/版本不存在")
|
|
134
|
+
end
|
|
135
|
+
Build.log.error("请打开以下 GitHub 地址确认组件/版本是否存在:#{specs_page_url}")
|
|
136
|
+
Process.exit(-1)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
Build.log.detail("当前版本podspec地址: #{podspec_json_url}")
|
|
140
|
+
|
|
141
|
+
res_pod_name = podspec_content["name"]
|
|
142
|
+
unless res_pod_name == pod_name
|
|
143
|
+
error = "获取到的podspec的名字#{res_pod_name}!= #{pod_name}"
|
|
144
|
+
pusher.push("❌ 打包失败", error)
|
|
145
|
+
Process.exit(-1)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
podspec_content.delete("license")
|
|
149
|
+
podspec_content.delete("module_map")
|
|
150
|
+
|
|
151
|
+
# swift使用静态库
|
|
152
|
+
unless podspec_content.has_key?("swift_version")
|
|
153
|
+
podspec_content.delete("static_framework")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# 记录变量
|
|
157
|
+
@podspec_content = podspec_content
|
|
158
|
+
|
|
159
|
+
## 写文件
|
|
160
|
+
new_file = File.new(podspec_json_path, "w+")
|
|
161
|
+
if new_file
|
|
162
|
+
new_file.syswrite(JSON.pretty_generate(podspec_content))
|
|
163
|
+
end
|
|
164
|
+
# 记录下载产物路径(http归档组件播种podspec到归档目录时使用)
|
|
165
|
+
@podspec_json_path = File.expand_path(podspec_json_path)
|
|
166
|
+
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# 发起 GET 并自动跟随 301/302 重定向(最多 5 次),返回最终响应。
|
|
170
|
+
# jsDelivr 对源上不存在的文件会 301 回源 raw.githubusercontent,
|
|
171
|
+
# 只有跟随到终点拿到的状态码才能准确反映文件是否存在(404)。
|
|
172
|
+
def fetch_with_redirect(url, limit = 5)
|
|
173
|
+
raise '重定向次数超过上限' if limit <= 0
|
|
174
|
+
|
|
175
|
+
uri = URI(url)
|
|
176
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
177
|
+
http.use_ssl = (uri.scheme == 'https')
|
|
178
|
+
# 设置10秒连接/读取超时,避免慢速镜像/被墙地址长时间卡住打包流程
|
|
179
|
+
http.open_timeout = 10
|
|
180
|
+
http.read_timeout = 10
|
|
181
|
+
response = http.request_get(uri.request_uri)
|
|
182
|
+
|
|
183
|
+
if response.is_a?(Net::HTTPRedirection) && response['location']
|
|
184
|
+
next_url = URI.join(uri, response['location']).to_s
|
|
185
|
+
Build.log.detail(" 跟随重定向(#{response.code}):#{next_url}")
|
|
186
|
+
return fetch_with_redirect(next_url, limit - 1)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
response
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def analyzer_tag
|
|
193
|
+
|
|
194
|
+
source = podspec_content["source"]
|
|
195
|
+
unless source
|
|
196
|
+
error = "podspec指定的 source字段为空"
|
|
197
|
+
pusher.push("❌ 解析失败", error)
|
|
198
|
+
Process.exit(-1)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# 复位http归档状态,避免同进程重复解析时残留上一次的归档信息
|
|
202
|
+
@http_source = false
|
|
203
|
+
@archive_url = nil
|
|
204
|
+
@archive_type = nil
|
|
205
|
+
@archive_sha256_declared = nil
|
|
206
|
+
@archive_sha1_declared = nil
|
|
207
|
+
@archive_flatten = false
|
|
208
|
+
|
|
209
|
+
# source.http 归档形态走归档git化流程,git 形态走原fork流程
|
|
210
|
+
if source["http"]
|
|
211
|
+
analyzer_http_archive(source)
|
|
212
|
+
else
|
|
213
|
+
analyzer_git_source(source)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# git源组件解析(source.git + tag/branch/commit),即原analyzer_tag逻辑
|
|
218
|
+
def analyzer_git_source(source)
|
|
219
|
+
@github_url = source["git"]
|
|
220
|
+
@github_tag = if source["tag"]
|
|
221
|
+
source["tag"]
|
|
222
|
+
else
|
|
223
|
+
source["branch"] ? source["branch"] : source["commit"]
|
|
224
|
+
end
|
|
225
|
+
unless @github_url
|
|
226
|
+
error = "podspec文件未使用source[git],暂未支持"
|
|
227
|
+
pusher.push("❌ 失败", error)
|
|
228
|
+
Process.exit(-1)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
unless @github_tag
|
|
232
|
+
error = "podspec未使用source[tag] source[branch] source[commit], 暂未支持"
|
|
233
|
+
pusher.push("❌ 解析失败", error)
|
|
234
|
+
Process.exit(-1)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# 转成ssh地址
|
|
238
|
+
@github_ssh_url = @github_url.http_to_ssh
|
|
239
|
+
|
|
240
|
+
Build.log.detail("所有版本podspec地址:https://github.com/CocoaPods/Specs/tree/master/Specs/#{@split_path}/#{pod_name}")
|
|
241
|
+
Build.log.detail("当前版本源码地址: #{@github_url}")
|
|
242
|
+
Build.log.detail("当前版本源码tag: #{@github_tag}")
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# http归档组件解析(source.http):
|
|
246
|
+
# 归档没有上游git仓,fork阶段会把下载解压后的归档就地git化并推送镜像仓,
|
|
247
|
+
# 因此这里不解析git地址,镜像仓克隆引用统一取版本号(与git化时打的tag一致)
|
|
248
|
+
# 支持两种source形态:
|
|
249
|
+
# 字符串: "http": "https://xxx.zip"
|
|
250
|
+
# 对象: "http": {"url": "...", "type": "zip", "sha256": "...", "flatten": true}
|
|
251
|
+
def analyzer_http_archive(source)
|
|
252
|
+
@http_source = true
|
|
253
|
+
|
|
254
|
+
entry = source["http"]
|
|
255
|
+
url = nil
|
|
256
|
+
declared_type = ''
|
|
257
|
+
sha256 = nil
|
|
258
|
+
sha1 = nil
|
|
259
|
+
flatten = false
|
|
260
|
+
case entry
|
|
261
|
+
when String
|
|
262
|
+
url = entry
|
|
263
|
+
when Hash
|
|
264
|
+
url = entry["url"].to_s
|
|
265
|
+
declared_type = entry["type"].to_s.strip
|
|
266
|
+
sha256 = entry["sha256"].to_s.strip
|
|
267
|
+
sha256 = nil if sha256.empty?
|
|
268
|
+
sha1 = entry["sha1"].to_s.strip
|
|
269
|
+
sha1 = nil if sha1.empty?
|
|
270
|
+
flatten = entry["flatten"] == true
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
if url.to_s.strip.empty?
|
|
274
|
+
error = "source[http]归档地址为空,无法解析下载地址"
|
|
275
|
+
pusher.push("❌ 解析失败", error)
|
|
276
|
+
Process.exit(-1)
|
|
277
|
+
end
|
|
278
|
+
url = url.strip
|
|
279
|
+
|
|
280
|
+
# 已知不支持的归档附加配置(如headers需携带鉴权下载、subdir需子目录裁切),直接明确报错
|
|
281
|
+
extra_keys = entry.is_a?(Hash) ? entry.keys - %w[url type sha256 sha1 flatten] : []
|
|
282
|
+
unless extra_keys.empty?
|
|
283
|
+
error = "source[http]归档使用了暂不支持的配置项: #{extra_keys.join('、')}(目前支持url/type/sha256/sha1/flatten)"
|
|
284
|
+
pusher.push("❌ 解析失败", error)
|
|
285
|
+
Process.exit(-1)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# 归档类型:显式声明优先,否则按URL扩展名推断
|
|
289
|
+
@archive_type = declared_type.strip.downcase
|
|
290
|
+
@archive_type = infer_archive_type(url) if @archive_type.empty?
|
|
291
|
+
unless %w[zip tar.gz tgz tar.bz2 tbz tbz2 tar.xz txz].include?(@archive_type)
|
|
292
|
+
error = "不支持的归档类型: #{@archive_type},目前支持 zip/tar.gz/tgz/tar.bz2/tar.xz,可使用source.http.type显式声明"
|
|
293
|
+
pusher.push("❌ 解析失败", error)
|
|
294
|
+
Process.exit(-1)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
@archive_url = url
|
|
298
|
+
@archive_sha256_declared = sha256
|
|
299
|
+
@archive_sha1_declared = sha1
|
|
300
|
+
@archive_flatten = flatten
|
|
301
|
+
|
|
302
|
+
# 镜像仓克隆引用取版本号:git化时在镜像仓打的tag即版本号,后续git_clone/podspec改写均复用该引用
|
|
303
|
+
@github_tag = pod_version
|
|
304
|
+
|
|
305
|
+
Build.log.detail("所有版本podspec地址:https://github.com/CocoaPods/Specs/tree/master/Specs/#{@split_path}/#{pod_name}")
|
|
306
|
+
Build.log.detail("当前版本为http归档组件(无git源码仓),将下载归档后git化到镜像仓")
|
|
307
|
+
Build.log.detail("归档地址: #{@archive_url}")
|
|
308
|
+
Build.log.detail("归档类型: #{@archive_type} flatten: #{@archive_flatten}")
|
|
309
|
+
Build.log.detail("归档摘要声明: sha256=#{sha256} sha1=#{sha1}")
|
|
310
|
+
Build.log.detail("镜像仓tag(取版本号): #{@github_tag}")
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# 按URL路径扩展名推断归档类型(忽略query串)
|
|
314
|
+
# @return [String] 识别不出返回空串,由调用方报错
|
|
315
|
+
def infer_archive_type(url)
|
|
316
|
+
path = begin
|
|
317
|
+
URI.parse(url).path
|
|
318
|
+
rescue StandardError
|
|
319
|
+
url
|
|
320
|
+
end
|
|
321
|
+
lower = path.to_s.downcase
|
|
322
|
+
if lower.end_with?('.zip') then 'zip'
|
|
323
|
+
elsif lower.end_with?('.tar.gz') then 'tar.gz'
|
|
324
|
+
elsif lower.end_with?('.tgz') then 'tgz'
|
|
325
|
+
elsif lower.end_with?('.tar.bz2') then 'tar.bz2'
|
|
326
|
+
elsif lower.end_with?('.tbz') || lower.end_with?('.tbz2') then 'tar.bz2'
|
|
327
|
+
elsif lower.end_with?('.tar.xz') then 'tar.xz'
|
|
328
|
+
elsif lower.end_with?('.txz') then 'tar.xz'
|
|
329
|
+
else ''
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
end
|
|
334
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'singleton'
|
|
4
|
+
|
|
5
|
+
module Status
|
|
6
|
+
# 打包工程拷贝yaml文件后
|
|
7
|
+
XCODE_PACKAGE_COPY_YAML = "XCODE_PACKAGE_COPY_YAML".downcase
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class HookManager
|
|
11
|
+
|
|
12
|
+
include Singleton
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@hooks = {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# 提供注册钩子的方法
|
|
19
|
+
def register_hook(status, &block)
|
|
20
|
+
@hooks[status] ||= []
|
|
21
|
+
@hooks[status] << block
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# 触发钩子的方法
|
|
25
|
+
def trigger_hook(status, *args)
|
|
26
|
+
if @hooks[status]
|
|
27
|
+
Build.log.debug("执行hook阶段#{status}逻辑")
|
|
28
|
+
@hooks[status].each { |hook| hook.call(*args) }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'openssl'
|
|
6
|
+
require 'cgi'
|
|
7
|
+
|
|
8
|
+
module Build
|
|
9
|
+
# Gitee OpenAPI v5 的极简HTTP客户端(基于Net::HTTP,无额外gem依赖)
|
|
10
|
+
# 接口文档: https://gitee.com/api/v5/swagger
|
|
11
|
+
class GiteeClient
|
|
12
|
+
class Error < StandardError; end
|
|
13
|
+
|
|
14
|
+
# 资源不存在(404)
|
|
15
|
+
class NotFound < Error; end
|
|
16
|
+
|
|
17
|
+
# 令牌无效/权限不足(401/403)
|
|
18
|
+
class Unauthorized < Error; end
|
|
19
|
+
|
|
20
|
+
# @param endpoint [String] 如 https://gitee.com/api/v5
|
|
21
|
+
# @param token [String] 私人令牌(access_token)
|
|
22
|
+
def initialize(endpoint, token)
|
|
23
|
+
@endpoint = endpoint.to_s.sub(/\/\z/, '')
|
|
24
|
+
@token = token.to_s
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def get(path, params = {})
|
|
28
|
+
request('GET', path, params)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def post(path, params = {})
|
|
32
|
+
request('POST', path, params)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def patch(path, params = {})
|
|
36
|
+
request('PATCH', path, params)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def put(path, params = {})
|
|
40
|
+
request('PUT', path, params)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def delete(path, params = {})
|
|
44
|
+
request('DELETE', path, params)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# 对URL path段进行转义
|
|
48
|
+
def self.escape_segment(value)
|
|
49
|
+
CGI.escape(value.to_s)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def request(method, path, params)
|
|
55
|
+
params = (params || {}).reject { |_k, v| v.nil? }
|
|
56
|
+
|
|
57
|
+
uri = URI("#{@endpoint}#{path}")
|
|
58
|
+
request_class = Net::HTTP.const_get(method.capitalize)
|
|
59
|
+
|
|
60
|
+
if %w[GET DELETE].include?(method)
|
|
61
|
+
uri.query = URI.encode_www_form(params.merge(access_token: @token))
|
|
62
|
+
req = request_class.new(uri.request_uri)
|
|
63
|
+
else
|
|
64
|
+
# Gitee v5 同时接受 query 中的 access_token 和表单 body
|
|
65
|
+
uri.query = URI.encode_www_form(access_token: @token)
|
|
66
|
+
req = request_class.new(uri.request_uri)
|
|
67
|
+
req.set_form_data(params) unless params.empty?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
71
|
+
if uri.scheme == 'https'
|
|
72
|
+
http.use_ssl = true
|
|
73
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
74
|
+
end
|
|
75
|
+
http.open_timeout = 15
|
|
76
|
+
http.read_timeout = 30
|
|
77
|
+
|
|
78
|
+
res = http.request(req)
|
|
79
|
+
parse_response(res)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def parse_response(res)
|
|
83
|
+
code = res.code.to_i
|
|
84
|
+
data = parse_body(res.body)
|
|
85
|
+
|
|
86
|
+
return data if (200..299).cover?(code)
|
|
87
|
+
|
|
88
|
+
message = extract_message(data)
|
|
89
|
+
exception =
|
|
90
|
+
case code
|
|
91
|
+
when 404 then NotFound
|
|
92
|
+
when 401, 403 then Unauthorized
|
|
93
|
+
else Error
|
|
94
|
+
end
|
|
95
|
+
raise exception, "Gitee API请求失败(#{code}): #{message}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def parse_body(body)
|
|
99
|
+
return {} if body.nil? || body.to_s.strip.empty?
|
|
100
|
+
|
|
101
|
+
JSON.parse(body)
|
|
102
|
+
rescue JSON::ParserError
|
|
103
|
+
{}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def extract_message(data)
|
|
107
|
+
if data.is_a?(Hash)
|
|
108
|
+
data['message'] || data['error'] || data.to_s
|
|
109
|
+
else
|
|
110
|
+
data.to_s
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|