geet 0.25.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 +139 -17
- 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,38 +140,104 @@ 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
|
|
93
154
|
|
|
94
|
-
# Enable auto-merge for this PR using
|
|
155
|
+
# Enable auto-merge for this PR using an available merge method.
|
|
156
|
+
# Queries the repository to find allowed merge methods and uses the first available one
|
|
157
|
+
# (see method comment below for the priority).
|
|
95
158
|
# See https://docs.github.com/en/graphql/reference/mutations#enablepullrequestautomerge
|
|
96
159
|
#
|
|
160
|
+
sig { returns(String) }
|
|
97
161
|
def enable_automerge
|
|
162
|
+
merge_method = fetch_available_merge_method
|
|
163
|
+
|
|
98
164
|
query = <<~GRAPHQL
|
|
99
|
-
mutation($pullRequestId: ID!) {
|
|
100
|
-
enablePullRequestAutoMerge(input: {pullRequestId: $pullRequestId}) {
|
|
165
|
+
mutation($pullRequestId: ID!, $mergeMethod: PullRequestMergeMethod!) {
|
|
166
|
+
enablePullRequestAutoMerge(input: {pullRequestId: $pullRequestId, mergeMethod: $mergeMethod}) {
|
|
101
167
|
pullRequest {
|
|
102
168
|
id
|
|
103
169
|
autoMergeRequest {
|
|
104
170
|
enabledAt
|
|
171
|
+
mergeMethod
|
|
105
172
|
}
|
|
106
173
|
}
|
|
107
174
|
}
|
|
108
175
|
}
|
|
109
176
|
GRAPHQL
|
|
110
177
|
|
|
111
|
-
variables = { pullRequestId: @node_id }
|
|
178
|
+
variables = { pullRequestId: @node_id, mergeMethod: merge_method }
|
|
112
179
|
|
|
113
180
|
@api_interface.send_graphql_request(query, variables:)
|
|
181
|
+
|
|
182
|
+
merge_method
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
private
|
|
186
|
+
|
|
187
|
+
# Query the repository to find the first available merge method.
|
|
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
|
|
192
|
+
#
|
|
193
|
+
sig { returns(String) }
|
|
194
|
+
def fetch_available_merge_method
|
|
195
|
+
query = <<~GRAPHQL
|
|
196
|
+
query($owner: String!, $name: String!, $number: Int!) {
|
|
197
|
+
repository(owner: $owner, name: $name) {
|
|
198
|
+
mergeCommitAllowed
|
|
199
|
+
squashMergeAllowed
|
|
200
|
+
rebaseMergeAllowed
|
|
201
|
+
pullRequest(number: $number) {
|
|
202
|
+
commits {
|
|
203
|
+
totalCount
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
GRAPHQL
|
|
209
|
+
|
|
210
|
+
owner, name = T.must(@api_interface.repository_path).split('/')
|
|
211
|
+
|
|
212
|
+
response = @api_interface.send_graphql_request(query, variables: {owner:, name:, number:})
|
|
213
|
+
repo_data = response['repository'].transform_keys(&:to_sym)
|
|
214
|
+
commit_count = repo_data[:pullRequest]['commits']['totalCount']
|
|
215
|
+
|
|
216
|
+
if commit_count == 1 && repo_data[:squashMergeAllowed]
|
|
217
|
+
'SQUASH'
|
|
218
|
+
elsif repo_data[:mergeCommitAllowed]
|
|
219
|
+
'MERGE'
|
|
220
|
+
elsif repo_data[:squashMergeAllowed]
|
|
221
|
+
'SQUASH'
|
|
222
|
+
elsif repo_data[:rebaseMergeAllowed]
|
|
223
|
+
'REBASE'
|
|
224
|
+
else
|
|
225
|
+
raise 'No merge methods are allowed on this repository'
|
|
226
|
+
end
|
|
114
227
|
end
|
|
115
228
|
|
|
116
229
|
class << self
|
|
230
|
+
extend T::Sig
|
|
231
|
+
|
|
117
232
|
private
|
|
118
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
|
+
}
|
|
119
241
|
def check_list_params!(milestone, assignee, head)
|
|
120
242
|
if (milestone || assignee) && head
|
|
121
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
|