geet 0.26.0 → 0.27.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 +4 -4
- data/Gemfile +1 -0
- data/geet.gemspec +1 -1
- data/lib/geet/git/repository.rb +2 -2
- data/lib/geet/github/abstract_issue.rb +38 -12
- data/lib/geet/github/api_interface.rb +86 -16
- data/lib/geet/github/branch.rb +5 -1
- data/lib/geet/github/gist.rb +26 -2
- data/lib/geet/github/issue.rb +31 -5
- data/lib/geet/github/label.rb +32 -8
- data/lib/geet/github/milestone.rb +56 -11
- data/lib/geet/github/pr.rb +105 -21
- data/lib/geet/github/remote_repository.rb +19 -3
- data/lib/geet/github/user.rb +47 -11
- data/lib/geet/gitlab/api_interface.rb +85 -22
- data/lib/geet/gitlab/issue.rb +32 -5
- data/lib/geet/gitlab/label.rb +37 -8
- data/lib/geet/gitlab/milestone.rb +41 -6
- data/lib/geet/gitlab/pr.rb +60 -8
- data/lib/geet/gitlab/user.rb +27 -5
- data/lib/geet/helpers/services_workflow_helper.rb +1 -1
- data/lib/geet/services/comment_pr.rb +1 -1
- data/lib/geet/services/create_gist.rb +1 -1
- data/lib/geet/services/create_issue.rb +70 -8
- data/lib/geet/services/create_pr.rb +14 -5
- data/lib/geet/services/list_issues.rb +20 -1
- data/lib/geet/services/merge_pr.rb +1 -1
- data/lib/geet/services/open_pr.rb +16 -1
- data/lib/geet/version.rb +1 -1
- data/spec/integration/create_issue_spec.rb +1 -2
- data/spec/integration/create_pr_spec.rb +10 -10
- data/spec/spec_helper.rb +3 -0
- data/spec/unit/github/pr_spec.rb +165 -0
- metadata +3 -2
data/lib/geet/github/pr.rb
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
2
3
|
|
|
3
4
|
module Geet
|
|
4
5
|
module Github
|
|
5
6
|
class PR < AbstractIssue
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
sig { returns(T.nilable(String)) }
|
|
6
10
|
attr_reader :node_id
|
|
7
11
|
|
|
12
|
+
sig {
|
|
13
|
+
override.params(
|
|
14
|
+
number: Integer,
|
|
15
|
+
api_interface: Geet::Github::ApiInterface,
|
|
16
|
+
title: String,
|
|
17
|
+
link: String,
|
|
18
|
+
node_id: T.nilable(String)
|
|
19
|
+
).void
|
|
20
|
+
}
|
|
8
21
|
def initialize(number, api_interface, title, link, node_id: nil)
|
|
9
22
|
super(number, api_interface, title, link)
|
|
10
23
|
@node_id = node_id
|
|
@@ -12,6 +25,16 @@ module Geet
|
|
|
12
25
|
|
|
13
26
|
# See https://developer.github.com/v3/pulls/#create-a-pull-request
|
|
14
27
|
#
|
|
28
|
+
sig {
|
|
29
|
+
params(
|
|
30
|
+
title: String,
|
|
31
|
+
description: String,
|
|
32
|
+
head: String,
|
|
33
|
+
api_interface: Geet::Github::ApiInterface,
|
|
34
|
+
base: String,
|
|
35
|
+
draft: T::Boolean
|
|
36
|
+
).returns(Geet::Github::PR)
|
|
37
|
+
}
|
|
15
38
|
def self.create(title, description, head, api_interface, base, draft: false)
|
|
16
39
|
api_path = 'pulls'
|
|
17
40
|
|
|
@@ -20,19 +43,34 @@ module Geet
|
|
|
20
43
|
head = "#{authenticated_user}:#{head}"
|
|
21
44
|
end
|
|
22
45
|
|
|
23
|
-
request_data = { title
|
|
46
|
+
request_data = { title:, body: description, head:, base:, draft: }
|
|
24
47
|
|
|
25
|
-
response = api_interface.send_request(api_path, data: request_data)
|
|
48
|
+
response = T.cast(api_interface.send_request(api_path, data: request_data), T::Hash[String, T.untyped])
|
|
26
49
|
|
|
27
|
-
number
|
|
28
|
-
|
|
50
|
+
number = T.cast(response.fetch('number'), Integer)
|
|
51
|
+
title = T.cast(response.fetch('title'), String)
|
|
52
|
+
link = T.cast(response.fetch('html_url'), String)
|
|
53
|
+
node_id = T.cast(response['node_id'], T.nilable(String))
|
|
29
54
|
|
|
30
55
|
new(number, api_interface, title, link, node_id:)
|
|
31
56
|
end
|
|
32
57
|
|
|
33
58
|
# See https://developer.github.com/v3/pulls/#list-pull-requests
|
|
34
59
|
#
|
|
35
|
-
|
|
60
|
+
sig {
|
|
61
|
+
override.params(
|
|
62
|
+
api_interface: Geet::Github::ApiInterface,
|
|
63
|
+
milestone: T.nilable(Geet::Github::Milestone),
|
|
64
|
+
assignee: T.nilable(Geet::Github::User),
|
|
65
|
+
owner: T.nilable(String),
|
|
66
|
+
head: T.nilable(String),
|
|
67
|
+
type_filter:
|
|
68
|
+
T.nilable(
|
|
69
|
+
T.proc.params(issue_data: T::Hash[String, T.untyped]).returns(T::Boolean)
|
|
70
|
+
)
|
|
71
|
+
).returns(T::Array[Geet::Github::PR])
|
|
72
|
+
}
|
|
73
|
+
def self.list(api_interface, milestone: nil, assignee: nil, owner: nil, head: nil, &type_filter)
|
|
36
74
|
check_list_params!(milestone, assignee, head)
|
|
37
75
|
|
|
38
76
|
if head
|
|
@@ -44,7 +82,10 @@ module Geet
|
|
|
44
82
|
# For upstream pulls, the owner is the authenticated user, otherwise, the repository owner.
|
|
45
83
|
#
|
|
46
84
|
response = if api_interface.upstream?
|
|
47
|
-
unfiltered_response =
|
|
85
|
+
unfiltered_response = T.cast(
|
|
86
|
+
api_interface.send_request(api_path, multipage: true),
|
|
87
|
+
T::Array[T::Hash[String, T.untyped]]
|
|
88
|
+
)
|
|
48
89
|
|
|
49
90
|
# VERY weird. From the docs, it's not clear if the user/org is required in the `head` parameter,
|
|
50
91
|
# but:
|
|
@@ -54,29 +95,44 @@ module Geet
|
|
|
54
95
|
#
|
|
55
96
|
# For this reason, we can't use that param, and have to filter manually.
|
|
56
97
|
#
|
|
57
|
-
unfiltered_response.select
|
|
98
|
+
unfiltered_response.select do |pr_data|
|
|
99
|
+
pr_head = T.cast(pr_data.fetch('head'), T::Hash[String, T.untyped])
|
|
100
|
+
label = T.cast(pr_head.fetch('label'), String)
|
|
101
|
+
|
|
102
|
+
label == "#{owner}:#{head}"
|
|
103
|
+
end
|
|
58
104
|
else
|
|
59
105
|
request_params = { head: "#{owner}:#{head}" }
|
|
60
106
|
|
|
61
|
-
|
|
107
|
+
T.cast(
|
|
108
|
+
api_interface.send_request(api_path, params: request_params, multipage: true),
|
|
109
|
+
T::Array[T::Hash[String, T.untyped]]
|
|
110
|
+
)
|
|
62
111
|
end
|
|
63
112
|
|
|
64
113
|
response.map do |pr_data|
|
|
65
|
-
number = pr_data.fetch('number')
|
|
66
|
-
title = pr_data.fetch('title')
|
|
67
|
-
link = pr_data.fetch('html_url')
|
|
114
|
+
number = T.cast(pr_data.fetch('number'), Integer)
|
|
115
|
+
title = T.cast(pr_data.fetch('title'), String)
|
|
116
|
+
link = T.cast(pr_data.fetch('html_url'), String)
|
|
68
117
|
|
|
69
118
|
new(number, api_interface, title, link)
|
|
70
119
|
end
|
|
71
120
|
else
|
|
72
|
-
super(api_interface, milestone
|
|
121
|
+
result = super(api_interface, milestone:, assignee:) do |issue_data|
|
|
73
122
|
issue_data.key?('pull_request')
|
|
74
123
|
end
|
|
124
|
+
|
|
125
|
+
T.cast(result, T::Array[Geet::Github::PR])
|
|
75
126
|
end
|
|
76
127
|
end
|
|
77
128
|
|
|
78
129
|
# See https://developer.github.com/v3/pulls/#merge-a-pull-request-merge-button
|
|
79
130
|
#
|
|
131
|
+
sig {
|
|
132
|
+
params(
|
|
133
|
+
merge_method: T.nilable(String)
|
|
134
|
+
).void
|
|
135
|
+
}
|
|
80
136
|
def merge(merge_method: nil)
|
|
81
137
|
api_path = "pulls/#{number}/merge"
|
|
82
138
|
request_data = { merge_method: } if merge_method
|
|
@@ -84,9 +140,14 @@ module Geet
|
|
|
84
140
|
@api_interface.send_request(api_path, http_method: :put, data: request_data)
|
|
85
141
|
end
|
|
86
142
|
|
|
143
|
+
sig {
|
|
144
|
+
params(
|
|
145
|
+
reviewers: T::Array[String]
|
|
146
|
+
).void
|
|
147
|
+
}
|
|
87
148
|
def request_review(reviewers)
|
|
88
149
|
api_path = "pulls/#{number}/requested_reviewers"
|
|
89
|
-
request_data = { reviewers:
|
|
150
|
+
request_data = { reviewers: }
|
|
90
151
|
|
|
91
152
|
@api_interface.send_request(api_path, data: request_data)
|
|
92
153
|
end
|
|
@@ -96,6 +157,7 @@ module Geet
|
|
|
96
157
|
# (see method comment below for the priority).
|
|
97
158
|
# See https://docs.github.com/en/graphql/reference/mutations#enablepullrequestautomerge
|
|
98
159
|
#
|
|
160
|
+
sig { returns(String) }
|
|
99
161
|
def enable_automerge
|
|
100
162
|
merge_method = fetch_available_merge_method
|
|
101
163
|
|
|
@@ -116,35 +178,48 @@ module Geet
|
|
|
116
178
|
variables = { pullRequestId: @node_id, mergeMethod: merge_method }
|
|
117
179
|
|
|
118
180
|
@api_interface.send_graphql_request(query, variables:)
|
|
181
|
+
|
|
182
|
+
merge_method
|
|
119
183
|
end
|
|
120
184
|
|
|
121
185
|
private
|
|
122
186
|
|
|
123
187
|
# Query the repository to find the first available merge method.
|
|
124
|
-
# Priority:
|
|
188
|
+
# Priority:
|
|
189
|
+
# - If there is one commit and squash merge is supported: SQUASH
|
|
190
|
+
# - Otherwise, if merge commit is supported: MERGE
|
|
191
|
+
# - Otherwise: SQUASH > REBASE
|
|
125
192
|
#
|
|
193
|
+
sig { returns(String) }
|
|
126
194
|
def fetch_available_merge_method
|
|
127
195
|
query = <<~GRAPHQL
|
|
128
|
-
query($owner: String!, $name: String!) {
|
|
196
|
+
query($owner: String!, $name: String!, $number: Int!) {
|
|
129
197
|
repository(owner: $owner, name: $name) {
|
|
130
198
|
mergeCommitAllowed
|
|
131
199
|
squashMergeAllowed
|
|
132
200
|
rebaseMergeAllowed
|
|
201
|
+
pullRequest(number: $number) {
|
|
202
|
+
commits {
|
|
203
|
+
totalCount
|
|
204
|
+
}
|
|
205
|
+
}
|
|
133
206
|
}
|
|
134
207
|
}
|
|
135
208
|
GRAPHQL
|
|
136
209
|
|
|
137
|
-
owner, name = @api_interface.repository_path.split('/')
|
|
210
|
+
owner, name = T.must(@api_interface.repository_path).split('/')
|
|
138
211
|
|
|
139
|
-
response = @api_interface.send_graphql_request(query, variables: {owner:, name:})
|
|
212
|
+
response = @api_interface.send_graphql_request(query, variables: {owner:, name:, number:})
|
|
140
213
|
repo_data = response['repository'].transform_keys(&:to_sym)
|
|
214
|
+
commit_count = repo_data[:pullRequest]['commits']['totalCount']
|
|
141
215
|
|
|
142
|
-
|
|
143
|
-
|
|
216
|
+
if commit_count == 1 && repo_data[:squashMergeAllowed]
|
|
217
|
+
'SQUASH'
|
|
218
|
+
elsif repo_data[:mergeCommitAllowed]
|
|
144
219
|
'MERGE'
|
|
145
|
-
|
|
220
|
+
elsif repo_data[:squashMergeAllowed]
|
|
146
221
|
'SQUASH'
|
|
147
|
-
|
|
222
|
+
elsif repo_data[:rebaseMergeAllowed]
|
|
148
223
|
'REBASE'
|
|
149
224
|
else
|
|
150
225
|
raise 'No merge methods are allowed on this repository'
|
|
@@ -152,8 +227,17 @@ module Geet
|
|
|
152
227
|
end
|
|
153
228
|
|
|
154
229
|
class << self
|
|
230
|
+
extend T::Sig
|
|
231
|
+
|
|
155
232
|
private
|
|
156
233
|
|
|
234
|
+
sig {
|
|
235
|
+
params(
|
|
236
|
+
milestone: T.nilable(Geet::Github::Milestone),
|
|
237
|
+
assignee: T.nilable(Geet::Github::User),
|
|
238
|
+
head: T.nilable(String)
|
|
239
|
+
).void
|
|
240
|
+
}
|
|
157
241
|
def check_list_params!(milestone, assignee, head)
|
|
158
242
|
if (milestone || assignee) && head
|
|
159
243
|
raise "Head can't be specified with milestone or assignee!"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
2
3
|
|
|
3
4
|
module Geet
|
|
4
5
|
module Github
|
|
@@ -10,10 +11,19 @@ module Geet
|
|
|
10
11
|
# duplication. All in all, for simplicity, the latter design is chosen, but is subject to redesign.
|
|
11
12
|
#
|
|
12
13
|
class RemoteRepository
|
|
14
|
+
extend T::Sig
|
|
15
|
+
|
|
13
16
|
# Nil if the repository is not a fork.
|
|
14
17
|
#
|
|
18
|
+
sig { returns(T.nilable(String)) }
|
|
15
19
|
attr_reader :parent_path
|
|
16
20
|
|
|
21
|
+
sig {
|
|
22
|
+
params(
|
|
23
|
+
api_interface: Geet::Github::ApiInterface,
|
|
24
|
+
parent_path: T.nilable(String)
|
|
25
|
+
).void
|
|
26
|
+
}
|
|
17
27
|
def initialize(api_interface, parent_path: nil)
|
|
18
28
|
@api_interface = api_interface
|
|
19
29
|
@parent_path = parent_path
|
|
@@ -23,14 +33,20 @@ module Geet
|
|
|
23
33
|
#
|
|
24
34
|
# https://docs.github.com/en/rest/reference/repos#get-a-repository
|
|
25
35
|
#
|
|
36
|
+
sig {
|
|
37
|
+
params(
|
|
38
|
+
api_interface: Geet::Github::ApiInterface
|
|
39
|
+
).returns(Geet::Github::RemoteRepository)
|
|
40
|
+
}
|
|
26
41
|
def self.find(api_interface)
|
|
27
42
|
api_path = "/repos/#{api_interface.repository_path}"
|
|
28
43
|
|
|
29
|
-
response = api_interface.send_request(api_path)
|
|
44
|
+
response = T.cast(api_interface.send_request(api_path), T::Hash[String, T.untyped])
|
|
30
45
|
|
|
31
|
-
|
|
46
|
+
parent_hash = T.cast(response['parent'], T.nilable(T::Hash[String, T.untyped]))
|
|
47
|
+
parent_path = T.cast(parent_hash&.fetch("full_name)"), T.nilable(String))
|
|
32
48
|
|
|
33
|
-
|
|
49
|
+
new(api_interface, parent_path:)
|
|
34
50
|
end
|
|
35
51
|
end # module RemoteRepository
|
|
36
52
|
end # module GitHub
|
data/lib/geet/github/user.rb
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
2
3
|
|
|
3
4
|
module Geet
|
|
4
5
|
module Github
|
|
5
6
|
class User
|
|
7
|
+
extend T::Sig
|
|
6
8
|
include Geet::Shared::RepoPermissions
|
|
7
9
|
|
|
10
|
+
sig { returns(String) }
|
|
8
11
|
attr_reader :username
|
|
9
12
|
|
|
13
|
+
sig {
|
|
14
|
+
params(
|
|
15
|
+
username: String,
|
|
16
|
+
api_interface: Geet::Github::ApiInterface
|
|
17
|
+
).void
|
|
18
|
+
}
|
|
10
19
|
def initialize(username, api_interface)
|
|
11
20
|
@username = username
|
|
12
21
|
@api_interface = api_interface
|
|
@@ -14,6 +23,11 @@ module Geet
|
|
|
14
23
|
|
|
15
24
|
# See #repo_permission.
|
|
16
25
|
#
|
|
26
|
+
sig {
|
|
27
|
+
params(
|
|
28
|
+
permission: String
|
|
29
|
+
).returns(T::Boolean)
|
|
30
|
+
}
|
|
17
31
|
def has_permission?(permission)
|
|
18
32
|
user_permission = self.class.repo_permission(@api_interface)
|
|
19
33
|
|
|
@@ -22,6 +36,7 @@ module Geet
|
|
|
22
36
|
|
|
23
37
|
# See https://developer.github.com/v3/repos/collaborators/#check-if-a-user-is-a-collaborator
|
|
24
38
|
#
|
|
39
|
+
sig { returns(T::Boolean) }
|
|
25
40
|
def is_collaborator?
|
|
26
41
|
api_path = "collaborators/#{@username}"
|
|
27
42
|
|
|
@@ -42,32 +57,50 @@ module Geet
|
|
|
42
57
|
|
|
43
58
|
# See https://developer.github.com/v3/users/#get-the-authenticated-user
|
|
44
59
|
#
|
|
45
|
-
|
|
60
|
+
sig {
|
|
61
|
+
params(
|
|
62
|
+
api_interface: Geet::Github::ApiInterface
|
|
63
|
+
).returns(Geet::Github::User)
|
|
64
|
+
}
|
|
65
|
+
def self.authenticated(api_interface)
|
|
46
66
|
api_path = '/user'
|
|
47
67
|
|
|
48
|
-
response = api_interface.send_request(api_path)
|
|
68
|
+
response = T.cast(api_interface.send_request(api_path), T::Hash[String, T.untyped])
|
|
49
69
|
|
|
50
|
-
|
|
70
|
+
login = T.cast(response.fetch('login'), String)
|
|
71
|
+
|
|
72
|
+
new(login, api_interface)
|
|
51
73
|
end
|
|
52
74
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
75
|
+
sig {
|
|
76
|
+
params(
|
|
77
|
+
api_interface: Geet::Github::ApiInterface
|
|
78
|
+
).returns(T::Array[Geet::Github::User])
|
|
79
|
+
}
|
|
80
|
+
def self.list_collaborators(api_interface)
|
|
56
81
|
api_path = 'collaborators'
|
|
57
|
-
response = api_interface.send_request(api_path, multipage: true)
|
|
82
|
+
response = T.cast(api_interface.send_request(api_path, multipage: true), T::Array[T::Hash[String, T.untyped]])
|
|
58
83
|
|
|
59
|
-
response.map
|
|
84
|
+
response.map do |user_entry|
|
|
85
|
+
login = T.cast(user_entry.fetch('login'), String)
|
|
86
|
+
new(login, api_interface)
|
|
87
|
+
end
|
|
60
88
|
end
|
|
61
89
|
|
|
62
90
|
# See https://developer.github.com/v3/repos/collaborators/#review-a-users-permission-level
|
|
63
91
|
#
|
|
92
|
+
sig {
|
|
93
|
+
params(
|
|
94
|
+
api_interface: Geet::Github::ApiInterface
|
|
95
|
+
).returns(String)
|
|
96
|
+
}
|
|
64
97
|
def self.repo_permission(api_interface)
|
|
65
98
|
username = authenticated(api_interface).username
|
|
66
99
|
api_path = "collaborators/#{username}/permission"
|
|
67
100
|
|
|
68
|
-
response = api_interface.send_request(api_path)
|
|
101
|
+
response = T.cast(api_interface.send_request(api_path), T::Hash[String, T.untyped])
|
|
69
102
|
|
|
70
|
-
permission = response.fetch('permission')
|
|
103
|
+
permission = T.cast(response.fetch('permission'), String)
|
|
71
104
|
|
|
72
105
|
check_permission!(permission)
|
|
73
106
|
|
|
@@ -75,12 +108,15 @@ module Geet
|
|
|
75
108
|
end
|
|
76
109
|
|
|
77
110
|
class << self
|
|
111
|
+
extend T::Sig
|
|
112
|
+
|
|
78
113
|
private
|
|
79
114
|
|
|
80
115
|
# Future-proofing.
|
|
81
116
|
#
|
|
117
|
+
sig { params(permission: String).void }
|
|
82
118
|
def check_permission!(permission)
|
|
83
|
-
raise "Unexpected permission #{permission.inspect}!" if !
|
|
119
|
+
raise "Unexpected permission #{permission.inspect}!" if !Geet::Shared::RepoPermissions::ALL_PERMISSIONS.include?(permission)
|
|
84
120
|
end
|
|
85
121
|
end
|
|
86
122
|
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
2
3
|
|
|
3
4
|
require 'cgi'
|
|
4
5
|
require 'uri'
|
|
@@ -8,64 +9,86 @@ require 'json'
|
|
|
8
9
|
module Geet
|
|
9
10
|
module Gitlab
|
|
10
11
|
class ApiInterface
|
|
12
|
+
extend T::Sig
|
|
13
|
+
|
|
11
14
|
API_BASE_URL = 'https://gitlab.com/api/v4'
|
|
12
15
|
|
|
16
|
+
sig { returns(T.nilable(String)) }
|
|
13
17
|
attr_reader :repository_path
|
|
14
18
|
|
|
15
19
|
# repo_path: "path/namespace"; required for the current GitLab operations.
|
|
16
20
|
# upstream: boolean; required for the current GitLab operations.
|
|
17
21
|
#
|
|
22
|
+
sig {
|
|
23
|
+
params(
|
|
24
|
+
api_token: String,
|
|
25
|
+
repo_path: String,
|
|
26
|
+
upstream: T::Boolean
|
|
27
|
+
).void
|
|
28
|
+
}
|
|
18
29
|
def initialize(api_token, repo_path:, upstream:)
|
|
19
30
|
@api_token = api_token
|
|
20
31
|
@path_with_namespace = repo_path
|
|
21
32
|
@upstream = upstream
|
|
33
|
+
@repository_path = T.let(nil, T.nilable(String))
|
|
22
34
|
end
|
|
23
35
|
|
|
36
|
+
sig { returns(T::Boolean) }
|
|
24
37
|
def upstream?
|
|
25
38
|
@upstream
|
|
26
39
|
end
|
|
27
40
|
|
|
41
|
+
sig {
|
|
42
|
+
params(
|
|
43
|
+
encoded: T::Boolean
|
|
44
|
+
).returns(String)
|
|
45
|
+
}
|
|
28
46
|
def path_with_namespace(encoded: false)
|
|
29
47
|
encoded ? CGI.escape(@path_with_namespace) : @path_with_namespace
|
|
30
48
|
end
|
|
31
49
|
|
|
32
50
|
# Send a request.
|
|
33
51
|
#
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
sig {
|
|
53
|
+
params(
|
|
54
|
+
# Appended to the API URL.
|
|
55
|
+
# for root path, prepend a `/`:
|
|
56
|
+
# - use `/gists` for `https://api.github.com/gists`
|
|
57
|
+
# when owner/project/repos is included, don't prepend `/`:
|
|
58
|
+
# - use `issues` for `https://api.github.com/myowner/myproject/repos/issues`
|
|
59
|
+
api_path: String,
|
|
60
|
+
params: T.nilable(T::Hash[Symbol, T.untyped]),
|
|
61
|
+
# If present, will generate a POST request, otherwise, a GET
|
|
62
|
+
data: T.nilable(T.any(T::Hash[Symbol, T.untyped], T::Array[T.untyped])),
|
|
63
|
+
# Set true for paged Github responses (eg. issues); it will make the method
|
|
64
|
+
multipage: T::Boolean,
|
|
65
|
+
# Method (:get, :patch, :post, :put and :delete)
|
|
66
|
+
# :get and :post are automatically inferred by the presence of :data; the other cases must be specified.
|
|
67
|
+
http_method: T.nilable(Symbol)
|
|
68
|
+
# Returns the parsed response, or an Array, in case of multipage.
|
|
69
|
+
# Where no body is present in the response, nil is returned.
|
|
70
|
+
).returns(T.nilable(T.any(T::Hash[String, T.untyped], T::Array[T::Hash[String, T.untyped]])))
|
|
71
|
+
}
|
|
51
72
|
def send_request(api_path, params: nil, data: nil, multipage: false, http_method: nil)
|
|
52
|
-
address = api_url(api_path)
|
|
73
|
+
address = T.let(api_url(api_path), T.nilable(String))
|
|
53
74
|
# filled only on :multipage
|
|
54
75
|
parsed_responses = []
|
|
55
76
|
|
|
56
77
|
loop do
|
|
57
|
-
response = send_http_request(address, params
|
|
78
|
+
response = send_http_request(T.must(address), params:, data:, http_method:)
|
|
58
79
|
|
|
59
|
-
|
|
80
|
+
if response_body = response.body
|
|
81
|
+
parsed_response = JSON.parse(response_body)
|
|
82
|
+
end
|
|
60
83
|
|
|
61
84
|
if error?(response)
|
|
62
|
-
formatted_error = decode_and_format_error(parsed_response)
|
|
85
|
+
formatted_error = decode_and_format_error(T.cast(parsed_response, T::Hash[String, T.untyped]))
|
|
63
86
|
raise(formatted_error)
|
|
64
87
|
end
|
|
65
88
|
|
|
66
89
|
return parsed_response if !multipage
|
|
67
90
|
|
|
68
|
-
parsed_responses.concat(parsed_response)
|
|
91
|
+
parsed_responses.concat(T.cast(parsed_response, T::Array[T::Hash[String, T.untyped]]))
|
|
69
92
|
|
|
70
93
|
address = link_next_page(response.to_hash)
|
|
71
94
|
|
|
@@ -79,10 +102,23 @@ module Geet
|
|
|
79
102
|
|
|
80
103
|
private
|
|
81
104
|
|
|
105
|
+
sig {
|
|
106
|
+
params(
|
|
107
|
+
api_path: String
|
|
108
|
+
).returns(String)
|
|
109
|
+
}
|
|
82
110
|
def api_url(api_path)
|
|
83
111
|
"#{API_BASE_URL}/#{api_path}"
|
|
84
112
|
end
|
|
85
113
|
|
|
114
|
+
sig {
|
|
115
|
+
params(
|
|
116
|
+
address: String,
|
|
117
|
+
params: T.nilable(T::Hash[Symbol, T.untyped]),
|
|
118
|
+
data: T.nilable(T.any(T::Hash[Symbol, T.untyped], T::Array[T.untyped])),
|
|
119
|
+
http_method: T.nilable(Symbol)
|
|
120
|
+
).returns(Net::HTTPResponse)
|
|
121
|
+
}
|
|
86
122
|
def send_http_request(address, params: nil, data: nil, http_method: nil)
|
|
87
123
|
uri = encode_uri(address, params)
|
|
88
124
|
http_class = find_http_class(http_method, data)
|
|
@@ -97,16 +133,32 @@ module Geet
|
|
|
97
133
|
end
|
|
98
134
|
end
|
|
99
135
|
|
|
136
|
+
sig {
|
|
137
|
+
params(
|
|
138
|
+
address: String,
|
|
139
|
+
params: T.nilable(T::Hash[Symbol, T.untyped])
|
|
140
|
+
).returns(URI::Generic)
|
|
141
|
+
}
|
|
100
142
|
def encode_uri(address, params)
|
|
101
143
|
address += '?' + URI.encode_www_form(params) if params
|
|
102
144
|
|
|
103
145
|
URI(address)
|
|
104
146
|
end
|
|
105
147
|
|
|
148
|
+
sig {
|
|
149
|
+
params(
|
|
150
|
+
response: Net::HTTPResponse
|
|
151
|
+
).returns(T::Boolean)
|
|
152
|
+
}
|
|
106
153
|
def error?(response)
|
|
107
154
|
!response.code.start_with?('2')
|
|
108
155
|
end
|
|
109
156
|
|
|
157
|
+
sig {
|
|
158
|
+
params(
|
|
159
|
+
parsed_response: T::Hash[String, T.untyped]
|
|
160
|
+
).returns(String)
|
|
161
|
+
}
|
|
110
162
|
def decode_and_format_error(parsed_response)
|
|
111
163
|
if parsed_response.key?('error')
|
|
112
164
|
parsed_response.fetch('error')
|
|
@@ -117,6 +169,11 @@ module Geet
|
|
|
117
169
|
end
|
|
118
170
|
end
|
|
119
171
|
|
|
172
|
+
sig {
|
|
173
|
+
params(
|
|
174
|
+
response_headers: T::Hash[String, T.untyped]
|
|
175
|
+
).returns(T.nilable(String))
|
|
176
|
+
}
|
|
120
177
|
def link_next_page(response_headers)
|
|
121
178
|
# An array (or nil) is returned.
|
|
122
179
|
link_header = Array(response_headers['link'])
|
|
@@ -126,6 +183,12 @@ module Geet
|
|
|
126
183
|
link_header[0][/<(\S+)>; rel="next"/, 1]
|
|
127
184
|
end
|
|
128
185
|
|
|
186
|
+
sig {
|
|
187
|
+
params(
|
|
188
|
+
http_method: T.nilable(Symbol),
|
|
189
|
+
data: T.nilable(Object)
|
|
190
|
+
).returns(T.class_of(Net::HTTPRequest))
|
|
191
|
+
}
|
|
129
192
|
def find_http_class(http_method, data)
|
|
130
193
|
http_method ||= data ? :post : :get
|
|
131
194
|
|
data/lib/geet/gitlab/issue.rb
CHANGED
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
2
3
|
|
|
3
4
|
module Geet
|
|
4
5
|
module Gitlab
|
|
5
6
|
class Issue
|
|
6
|
-
|
|
7
|
+
extend T::Sig
|
|
7
8
|
|
|
9
|
+
sig { returns(Integer) }
|
|
10
|
+
attr_reader :number
|
|
11
|
+
|
|
12
|
+
sig { returns(String) }
|
|
13
|
+
attr_reader :title
|
|
14
|
+
|
|
15
|
+
sig { returns(String) }
|
|
16
|
+
attr_reader :link
|
|
17
|
+
|
|
18
|
+
sig {
|
|
19
|
+
params(
|
|
20
|
+
number: Integer,
|
|
21
|
+
title: String,
|
|
22
|
+
link: String
|
|
23
|
+
).void
|
|
24
|
+
}
|
|
8
25
|
def initialize(number, title, link)
|
|
9
26
|
@number = number
|
|
10
27
|
@title = title
|
|
@@ -13,6 +30,13 @@ module Geet
|
|
|
13
30
|
|
|
14
31
|
# See https://docs.gitlab.com/ee/api/issues.html#list-issues
|
|
15
32
|
#
|
|
33
|
+
sig {
|
|
34
|
+
params(
|
|
35
|
+
api_interface: ApiInterface,
|
|
36
|
+
assignee: T.nilable(User),
|
|
37
|
+
milestone: T.nilable(Milestone)
|
|
38
|
+
).returns(T::Array[Issue])
|
|
39
|
+
}
|
|
16
40
|
def self.list(api_interface, assignee: nil, milestone: nil)
|
|
17
41
|
api_path = "projects/#{api_interface.path_with_namespace(encoded: true)}/issues"
|
|
18
42
|
|
|
@@ -20,12 +44,15 @@ module Geet
|
|
|
20
44
|
request_params[:assignee_id] = assignee.id if assignee
|
|
21
45
|
request_params[:milestone] = milestone.title if milestone
|
|
22
46
|
|
|
23
|
-
response =
|
|
47
|
+
response = T.cast(
|
|
48
|
+
api_interface.send_request(api_path, params: request_params, multipage: true),
|
|
49
|
+
T::Array[T::Hash[String, T.untyped]]
|
|
50
|
+
)
|
|
24
51
|
|
|
25
52
|
response.map do |issue_data, result|
|
|
26
|
-
number = issue_data.fetch('iid')
|
|
27
|
-
title = issue_data.fetch('title')
|
|
28
|
-
link = issue_data.fetch('web_url')
|
|
53
|
+
number = T.cast(issue_data.fetch('iid'), Integer)
|
|
54
|
+
title = T.cast(issue_data.fetch('title'), String)
|
|
55
|
+
link = T.cast(issue_data.fetch('web_url'), String)
|
|
29
56
|
|
|
30
57
|
new(number, title, link)
|
|
31
58
|
end
|