1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
|
# frozen_string_literal: true
# These endpoints partially mimic Github API behavior in order to successfully
# integrate with Jira Development Panel.
# Endpoints returning an empty list were temporarily added to avoid 404's
# during Jira's DVCS integration.
#
module API
module V3
class Github < ::API::Base
NO_SLASH_URL_PART_REGEX = %r{[^/]+}.freeze
ENDPOINT_REQUIREMENTS = {
namespace: NO_SLASH_URL_PART_REGEX,
project: NO_SLASH_URL_PART_REGEX,
username: NO_SLASH_URL_PART_REGEX
}.freeze
# Used to differentiate Jira Cloud requests from Jira Server requests
# Jira Cloud user agent format: Jira DVCS Connector Vertigo/version
# Jira Server user agent format: Jira DVCS Connector/version
JIRA_DVCS_CLOUD_USER_AGENT = 'Jira DVCS Connector Vertigo'
GITALY_TIMEOUT_CACHE_KEY = 'api:v3:Gitaly-timeout-cache-key'
GITALY_TIMEOUT_CACHE_EXPIRY = 1.day
include PaginationParams
feature_category :integrations
before do
authorize_jira_user_agent!(request)
authenticate!
end
helpers do
params :project_full_path do
requires :namespace, type: String
requires :project, type: String
end
def authorize_jira_user_agent!(request)
not_found! unless Gitlab::Jira::Middleware.jira_dvcs_connector?(request.env)
end
def update_project_feature_usage_for(project)
# Prevent errors on GitLab Geo not allowing
# UPDATE statements to happen in GET requests.
return if Gitlab::Database.read_only?
project.log_jira_dvcs_integration_usage(cloud: jira_cloud?)
end
def jira_cloud?
request.env['HTTP_USER_AGENT'].include?(JIRA_DVCS_CLOUD_USER_AGENT)
end
def find_project_with_access(params)
project = find_project!(
::Gitlab::Jira::Dvcs.restore_full_path(**params.slice(:namespace, :project).symbolize_keys)
)
not_found! unless can?(current_user, :download_code, project)
project
end
# rubocop: disable CodeReuse/ActiveRecord
def find_merge_requests
merge_requests = authorized_merge_requests.reorder(updated_at: :desc)
paginate(merge_requests)
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def find_merge_request_with_access(id, access_level = :read_merge_request)
merge_request = authorized_merge_requests.find_by(id: id)
not_found! unless can?(current_user, access_level, merge_request)
merge_request
end
# rubocop: enable CodeReuse/ActiveRecord
def authorized_merge_requests
MergeRequestsFinder.new(current_user, authorized_only: !current_user.admin?)
.execute.with_jira_integration_associations
end
def authorized_merge_requests_for_project(project)
MergeRequestsFinder
.new(current_user, authorized_only: !current_user.admin?, project_id: project.id)
.execute.with_jira_integration_associations
end
# rubocop: disable CodeReuse/ActiveRecord
def find_notes(noteable)
# They're not presented on Jira Dev Panel ATM. A comments count with a
# redirect link is presented.
notes = paginate(noteable.notes.user.reorder(nil))
notes.select { |n| n.readable_by?(current_user) }
end
# rubocop: enable CodeReuse/ActiveRecord
# Returns an empty Array instead of the Commit diff files for a period
# of time after a Gitaly timeout, to mitigate frequent Gitaly timeouts
# for some Commit diffs.
def diff_files(commit)
cache_key = [
GITALY_TIMEOUT_CACHE_KEY,
commit.project.id,
commit.cache_key
].join(':')
return [] if Rails.cache.read(cache_key).present?
begin
commit.diffs.diff_files
rescue GRPC::DeadlineExceeded => error
# Gitaly fails to load diffs consistently for some commits. The other information
# is still valuable for Jira. So we skip the loading and respond with a 200 excluding diffs
# Remove this when https://gitlab.com/gitlab-org/gitaly/-/issues/3741 is fixed.
Rails.cache.write(cache_key, 1, expires_in: GITALY_TIMEOUT_CACHE_EXPIRY)
Gitlab::ErrorTracking.track_exception(error)
[]
end
end
end
resource :orgs do
get ':namespace/repos' do
present []
end
end
resource :user do
get :repos do
present []
end
end
resource :users do
params do
use :pagination
end
get ':namespace/repos' do
namespace = Namespace.find_by_full_path(params[:namespace])
not_found!('Namespace') unless namespace
projects = current_user.can_read_all_resources? ? Project.all : current_user.authorized_projects
projects = projects.in_namespace(namespace.self_and_descendants)
projects_cte = Project.wrap_with_cte(projects)
.eager_load_namespace_and_owner
.with_route
present paginate(projects_cte),
with: ::API::Github::Entities::Repository,
root_namespace: namespace.root_ancestor
end
get ':username' do
forbidden! unless can?(current_user, :read_users_list)
user = UsersFinder.new(current_user, { username: params[:username] }).execute.first
not_found! unless user
present user, with: ::API::Github::Entities::User
end
end
# Jira dev panel integration weirdly requests for "/-/jira/pulls" instead
# "/api/v3/repos/<namespace>/<project>/pulls". This forces us into
# returning _all_ Merge Requests from authorized projects (user is a member),
# instead just the authorized MRs from a project.
# Jira handles the filtering, presenting just MRs mentioning the Jira
# issue ID on the MR title / description.
resource :repos do
# Keeping for backwards compatibility with old Jira integration instructions
# so that users that do not change it will not suddenly have a broken integration
get '/-/jira/pulls' do
present find_merge_requests, with: ::API::Github::Entities::PullRequest
end
get '/-/jira/events' do
present []
end
params do
use :project_full_path
end
# TODO Remove the custom Apdex SLO target `urgency: :low` when this endpoint has been optimised.
# https://gitlab.com/gitlab-org/gitlab/-/issues/337269
get ':namespace/:project/pulls', urgency: :low do
user_project = find_project_with_access(params)
merge_requests = authorized_merge_requests_for_project(user_project)
present paginate(merge_requests), with: ::API::Github::Entities::PullRequest
end
params do
use :project_full_path
end
get ':namespace/:project/pulls/:id' do
merge_request = find_merge_request_with_access(params[:id])
present merge_request, with: ::API::Github::Entities::PullRequest
end
# In Github, each Merge Request is automatically also an issue.
# Therefore we return its comments here.
# It'll present _just_ the comments counting with a link to GitLab on
# Jira dev panel, not the actual note content.
get ':namespace/:project/issues/:id/comments' do
merge_request = find_merge_request_with_access(params[:id])
present find_notes(merge_request), with: ::API::Github::Entities::NoteableComment
end
# This refer to "review" comments but Jira dev panel doesn't seem to
# present it accordingly.
get ':namespace/:project/pulls/:id/comments' do
present []
end
# Commits are not presented within "Pull Requests" modal on Jira dev
# panel.
get ':namespace/:project/pulls/:id/commits' do
present []
end
# Self-hosted Jira (tested on 7.11.1) requests this endpoint right
# after fetching branches.
get ':namespace/:project/events' do
user_project = find_project_with_access(params)
merge_requests = authorized_merge_requests_for_project(user_project)
present paginate(merge_requests), with: ::API::Github::Entities::PullRequestEvent
end
params do
use :project_full_path
use :pagination
end
# TODO Remove the custom Apdex SLO target `urgency: :low` when this endpoint has been optimised.
# https://gitlab.com/gitlab-org/gitlab/-/issues/337268
get ':namespace/:project/branches', urgency: :low do
user_project = find_project_with_access(params)
update_project_feature_usage_for(user_project)
next [] unless user_project.repo_exists?
branches = ::Kaminari.paginate_array(user_project.repository.branches.sort_by(&:name))
present paginate(branches), with: ::API::Github::Entities::Branch, project: user_project
end
params do
use :project_full_path
end
get ':namespace/:project/commits/:sha' do
user_project = find_project_with_access(params)
commit = user_project.commit(params[:sha])
not_found! 'Commit' unless commit
present commit, with: ::API::Github::Entities::RepoCommit, diff_files: diff_files(commit)
end
end
end
end
end
|