1 | #include "GitLabRestApi.h" |
2 | #include <GitQlientSettings.h> |
3 | #include <Issue.h> |
4 | |
5 | #include <QNetworkAccessManager> |
6 | #include <QNetworkReply> |
7 | #include <QUrlQuery> |
8 | #include <QJsonDocument> |
9 | #include <QJsonArray> |
10 | #include <QJsonObject> |
11 | #include <QTimer> |
12 | |
13 | using namespace GitServer; |
14 | |
15 | GitLabRestApi::GitLabRestApi(const QString &userName, const QString &repoName, const QString &settingsKey, |
16 | const ServerAuthentication &auth, QObject *parent) |
17 | : IRestApi(auth, parent) |
18 | , mUserName(userName) |
19 | , mRepoName(repoName) |
20 | , mSettingsKey(settingsKey) |
21 | { |
22 | if (!userName.isEmpty() && !auth.userName.isEmpty() && !auth.userPass.isEmpty() && !auth.endpointUrl.isEmpty()) |
23 | { |
24 | mPreRequisites = 0; |
25 | GitQlientSettings settings("" ); |
26 | mUserId = settings.globalValue(QString("%1/%2-userId" ).arg(mSettingsKey, mRepoName), "" ).toString(); |
27 | mRepoId = settings.globalValue(QString("%1/%2-repoId" ).arg(mSettingsKey, mRepoName), "" ).toString(); |
28 | |
29 | if (mRepoId.isEmpty()) |
30 | { |
31 | ++mPreRequisites; |
32 | getProjects(); |
33 | } |
34 | |
35 | if (mUserId.isEmpty()) |
36 | { |
37 | ++mPreRequisites; |
38 | getUserInfo(); |
39 | } |
40 | } |
41 | } |
42 | |
43 | void GitLabRestApi::testConnection() |
44 | { |
45 | if (mPreRequisites == 0) |
46 | { |
47 | auto request = createRequest("/users" ); |
48 | auto url = request.url(); |
49 | |
50 | QUrlQuery query; |
51 | query.addQueryItem("username" , mUserName); |
52 | url.setQuery(query); |
53 | request.setUrl(url); |
54 | |
55 | const auto reply = mManager->get(request); |
56 | |
57 | connect(reply, &QNetworkReply::finished, this, [this]() { |
58 | const auto reply = qobject_cast<QNetworkReply *>(sender()); |
59 | QString errorStr; |
60 | const auto tmpDoc = validateData(reply, errorStr); |
61 | |
62 | if (!tmpDoc.isEmpty()) |
63 | emit connectionTested(); |
64 | else |
65 | emit errorOccurred(errorStr); |
66 | }); |
67 | } |
68 | else |
69 | mTestRequested = true; |
70 | } |
71 | |
72 | void GitLabRestApi::createIssue(const Issue &issue) |
73 | { |
74 | auto request = createRequest(QString("/projects/%1/issues" ).arg(mRepoId)); |
75 | auto url = request.url(); |
76 | |
77 | QUrlQuery query; |
78 | query.addQueryItem("title" , issue.title); |
79 | query.addQueryItem("description" , QString::fromUtf8(issue.body)); |
80 | |
81 | if (!issue.assignees.isEmpty()) |
82 | query.addQueryItem("assignee_ids" , mUserId); |
83 | |
84 | if (issue.milestone.id != -1) |
85 | query.addQueryItem("milestone_id" , QString::number(issue.milestone.id)); |
86 | |
87 | if (!issue.labels.isEmpty()) |
88 | { |
89 | QStringList labelsList; |
90 | |
91 | for (auto &label : issue.labels) |
92 | labelsList.append(label.name); |
93 | |
94 | query.addQueryItem("labels" , labelsList.join("," )); |
95 | } |
96 | |
97 | url.setQuery(query); |
98 | request.setUrl(url); |
99 | |
100 | const auto reply = mManager->post(request, "" ); |
101 | |
102 | connect(reply, &QNetworkReply::finished, this, &GitLabRestApi::onIssueCreated); |
103 | } |
104 | |
105 | void GitLabRestApi::updateIssue(int, const Issue &) { } |
106 | |
107 | void GitLabRestApi::createPullRequest(const PullRequest &pr) |
108 | { |
109 | auto request = createRequest(QString("/projects/%1/merge_requests" ).arg(mRepoId)); |
110 | auto url = request.url(); |
111 | |
112 | QUrlQuery query; |
113 | query.addQueryItem("title" , pr.title); |
114 | query.addQueryItem("description" , QString::fromUtf8(pr.body)); |
115 | query.addQueryItem("assignee_ids" , mUserId); |
116 | query.addQueryItem("target_branch" , pr.base); |
117 | query.addQueryItem("source_branch" , pr.head); |
118 | query.addQueryItem("allow_collaboration" , QVariant(pr.maintainerCanModify).toString()); |
119 | |
120 | if (pr.milestone.id != -1) |
121 | query.addQueryItem("milestone_id" , QString::number(pr.milestone.id)); |
122 | |
123 | if (!pr.labels.isEmpty()) |
124 | { |
125 | QStringList labelsList; |
126 | |
127 | for (auto &label : pr.labels) |
128 | labelsList.append(label.name); |
129 | |
130 | query.addQueryItem("labels" , labelsList.join("," )); |
131 | } |
132 | |
133 | url.setQuery(query); |
134 | request.setUrl(url); |
135 | |
136 | const auto reply = mManager->post(request, "" ); |
137 | |
138 | connect(reply, &QNetworkReply::finished, this, &GitLabRestApi::onMergeRequestCreated); |
139 | } |
140 | |
141 | void GitLabRestApi::requestLabels() |
142 | { |
143 | const auto reply = mManager->get(createRequest(QString("/projects/%1/labels" ).arg(mRepoId))); |
144 | |
145 | connect(reply, &QNetworkReply::finished, this, &GitLabRestApi::onLabelsReceived); |
146 | } |
147 | |
148 | void GitLabRestApi::requestMilestones() |
149 | { |
150 | const auto reply = mManager->get(createRequest(QString("/projects/%1/milestones" ).arg(mRepoId))); |
151 | |
152 | connect(reply, &QNetworkReply::finished, this, &GitLabRestApi::onMilestonesReceived); |
153 | } |
154 | |
155 | void GitLabRestApi::requestIssues(int) |
156 | { |
157 | auto request = createRequest(QString("/projects/%1/issues" ).arg(mRepoId)); |
158 | auto url = request.url(); |
159 | |
160 | QUrlQuery query; |
161 | query.addQueryItem("state" , "opened" ); |
162 | url.setQuery(query); |
163 | request.setUrl(url); |
164 | |
165 | const auto reply = mManager->get(request); |
166 | connect(reply, &QNetworkReply::finished, this, &GitLabRestApi::onIssueReceived); |
167 | } |
168 | |
169 | void GitLabRestApi::requestPullRequests(int) { } |
170 | |
171 | void GitLabRestApi::onIssueReceived() |
172 | { |
173 | const auto reply = qobject_cast<QNetworkReply *>(sender()); |
174 | QString errorStr; |
175 | const auto tmpDoc = validateData(reply, errorStr); |
176 | QVector<Issue> issues; |
177 | |
178 | if (!tmpDoc.isEmpty()) |
179 | { |
180 | const auto issuesArray = tmpDoc.array(); |
181 | |
182 | for (const auto &issueData : issuesArray) |
183 | { |
184 | if (const auto issueObj = issueData.toObject(); !issueObj.contains("pull_request" )) |
185 | issues.append(issueFromJson(issueObj)); |
186 | } |
187 | } |
188 | else |
189 | emit errorOccurred(errorStr); |
190 | |
191 | emit issuesReceived(issues); |
192 | } |
193 | |
194 | QNetworkRequest GitLabRestApi::createRequest(const QString &page) const |
195 | { |
196 | QNetworkRequest request; |
197 | request.setUrl(QString(mAuth.endpointUrl + page)); |
198 | request.setRawHeader("User-Agent" , "GitQlient" ); |
199 | request.setRawHeader("X-Custom-User-Agent" , "GitQlient" ); |
200 | request.setRawHeader("Content-Type" , "application/json" ); |
201 | request.setRawHeader(QByteArray("PRIVATE-TOKEN" ), |
202 | QByteArray(QString(QStringLiteral("%1" )).arg(mAuth.userPass).toLocal8Bit())); |
203 | |
204 | return request; |
205 | } |
206 | |
207 | void GitLabRestApi::getUserInfo() const |
208 | { |
209 | auto request = createRequest("/users" ); |
210 | auto url = request.url(); |
211 | |
212 | QUrlQuery query; |
213 | query.addQueryItem("username" , mUserName); |
214 | url.setQuery(query); |
215 | request.setUrl(url); |
216 | |
217 | const auto reply = mManager->get(request); |
218 | |
219 | connect(reply, &QNetworkReply::finished, this, &GitLabRestApi::onUserInfoReceived, Qt::DirectConnection); |
220 | } |
221 | |
222 | void GitLabRestApi::onUserInfoReceived() |
223 | { |
224 | const auto reply = qobject_cast<QNetworkReply *>(sender()); |
225 | QString errorStr; |
226 | const auto tmpDoc = validateData(reply, errorStr); |
227 | |
228 | if (!tmpDoc.isEmpty()) |
229 | { |
230 | const auto list = tmpDoc.toVariant().toList(); |
231 | |
232 | if (!list.isEmpty()) |
233 | { |
234 | const auto firstUser = list.first().toMap(); |
235 | |
236 | mUserId = firstUser.value("id" ).toString(); |
237 | |
238 | GitQlientSettings settings("" ); |
239 | settings.setGlobalValue(QString("%1/%2-userId" ).arg(mSettingsKey, mRepoName), mUserId); |
240 | |
241 | --mPreRequisites; |
242 | |
243 | if (mPreRequisites == 0 && mTestRequested) |
244 | testConnection(); |
245 | } |
246 | } |
247 | else |
248 | emit errorOccurred(errorStr); |
249 | } |
250 | |
251 | void GitLabRestApi::getProjects() |
252 | { |
253 | auto request = createRequest(QString("/users/%1/projects" ).arg(mUserName)); |
254 | const auto reply = mManager->get(request); |
255 | |
256 | connect(reply, &QNetworkReply::finished, this, &GitLabRestApi::onProjectsReceived, Qt::DirectConnection); |
257 | } |
258 | |
259 | void GitLabRestApi::onProjectsReceived() |
260 | { |
261 | const auto reply = qobject_cast<QNetworkReply *>(sender()); |
262 | QString errorStr; |
263 | const auto tmpDoc = validateData(reply, errorStr); |
264 | |
265 | if (!tmpDoc.isEmpty()) |
266 | { |
267 | const auto projectsObj = tmpDoc.toVariant().toList(); |
268 | |
269 | for (const auto &projObj : projectsObj) |
270 | { |
271 | const auto labelMap = projObj.toMap(); |
272 | |
273 | if (labelMap.value("path" ).toString() == mRepoName) |
274 | { |
275 | mRepoId = labelMap.value("id" ).toString(); |
276 | |
277 | GitQlientSettings settings("" ); |
278 | settings.setGlobalValue(QString("%1/%2-repoId" ).arg(mSettingsKey, mRepoName), mRepoId); |
279 | --mPreRequisites; |
280 | |
281 | if (mPreRequisites == 0 && mTestRequested) |
282 | testConnection(); |
283 | |
284 | break; |
285 | } |
286 | } |
287 | } |
288 | else |
289 | emit errorOccurred(errorStr); |
290 | } |
291 | |
292 | void GitLabRestApi::onLabelsReceived() |
293 | { |
294 | const auto reply = qobject_cast<QNetworkReply *>(sender()); |
295 | QString errorStr; |
296 | const auto tmpDoc = validateData(reply, errorStr); |
297 | QVector<Label> labels; |
298 | |
299 | if (!tmpDoc.isEmpty()) |
300 | { |
301 | const auto labelsObj = tmpDoc.toVariant().toList(); |
302 | |
303 | for (const auto &labelObj : labelsObj) |
304 | { |
305 | const auto labelMap = labelObj.toMap(); |
306 | Label sLabel { labelMap.value("id" ).toString().toInt(), |
307 | "" , |
308 | "" , |
309 | labelMap.value("name" ).toString(), |
310 | labelMap.value("description" ).toString(), |
311 | labelMap.value("color" ).toString(), |
312 | false }; |
313 | |
314 | labels.append(std::move(sLabel)); |
315 | } |
316 | } |
317 | else |
318 | emit errorOccurred(errorStr); |
319 | |
320 | emit labelsReceived(labels); |
321 | } |
322 | |
323 | void GitLabRestApi::onMilestonesReceived() |
324 | { |
325 | const auto reply = qobject_cast<QNetworkReply *>(sender()); |
326 | QString errorStr; |
327 | const auto tmpDoc = validateData(reply, errorStr); |
328 | |
329 | if (!tmpDoc.isEmpty()) |
330 | { |
331 | const auto milestonesObj = tmpDoc.toVariant().toList(); |
332 | |
333 | QVector<Milestone> milestones; |
334 | |
335 | for (const auto &milestoneObj : milestonesObj) |
336 | { |
337 | const auto labelMap = milestoneObj.toMap(); |
338 | Milestone sMilestone { |
339 | labelMap.value("id" ).toString().toInt(), labelMap.value("id" ).toString().toInt(), |
340 | labelMap.value("iid" ).toString(), labelMap.value("title" ).toString(), |
341 | labelMap.value("description" ).toString(), labelMap.value("state" ).toString() == "active" |
342 | }; |
343 | |
344 | milestones.append(std::move(sMilestone)); |
345 | } |
346 | |
347 | emit milestonesReceived(milestones); |
348 | } |
349 | else |
350 | emit errorOccurred(errorStr); |
351 | } |
352 | |
353 | void GitLabRestApi::onIssueCreated() |
354 | { |
355 | const auto reply = qobject_cast<QNetworkReply *>(sender()); |
356 | QString errorStr; |
357 | const auto tmpDoc = validateData(reply, errorStr); |
358 | |
359 | if (!tmpDoc.isEmpty()) |
360 | { |
361 | const auto issue = issueFromJson(tmpDoc.object()); |
362 | |
363 | emit issueUpdated(issue); |
364 | } |
365 | else |
366 | emit errorOccurred(errorStr); |
367 | } |
368 | |
369 | void GitLabRestApi::onMergeRequestCreated() |
370 | { |
371 | const auto reply = qobject_cast<QNetworkReply *>(sender()); |
372 | QString errorStr; |
373 | const auto tmpDoc = validateData(reply, errorStr); |
374 | |
375 | if (!tmpDoc.isEmpty()) |
376 | { |
377 | const auto issue = prFromJson(tmpDoc.object()); |
378 | emit pullRequestUpdated(issue); |
379 | } |
380 | else |
381 | emit errorOccurred(errorStr); |
382 | } |
383 | |
384 | Issue GitLabRestApi::issueFromJson(const QJsonObject &json) const |
385 | { |
386 | Issue issue; |
387 | issue.number = json["id" ].toInt(); |
388 | issue.title = json["title" ].toString(); |
389 | issue.body = json["description" ].toString().toUtf8(); |
390 | issue.url = json["web_url" ].toString(); |
391 | issue.creation = json["created_at" ].toVariant().toDateTime(); |
392 | issue.commentsCount = json["comments" ].toInt(); |
393 | |
394 | issue.creator |
395 | = { json["author" ].toObject()["id" ].toInt(), json["author" ].toObject()["username" ].toString(), |
396 | json["author" ].toObject()["avatar_url" ].toString(), json["author" ].toObject()["web_url" ].toString(), "" }; |
397 | |
398 | const auto labels = json["labels" ].toArray(); |
399 | |
400 | for (const auto &label : labels) |
401 | { |
402 | Label sLabel; |
403 | sLabel.name = label.toString(); |
404 | issue.labels.append(sLabel); |
405 | } |
406 | |
407 | const auto assignees = json["assignees" ].toArray(); |
408 | |
409 | for (const auto &assignee : assignees) |
410 | { |
411 | GitServer::User sAssignee; |
412 | sAssignee.id = assignee["id" ].toInt(); |
413 | sAssignee.url = assignee["web_url" ].toString(); |
414 | sAssignee.name = assignee["username" ].toString(); |
415 | sAssignee.avatar = assignee["avatar_url" ].toString(); |
416 | |
417 | issue.assignees.append(sAssignee); |
418 | } |
419 | |
420 | Milestone sMilestone { json["milestone" ].toObject()[QStringLiteral("id" )].toInt(), |
421 | json["milestone" ].toObject()[QStringLiteral("number" )].toInt(), |
422 | json["milestone" ].toObject()[QStringLiteral("iid" )].toString(), |
423 | json["milestone" ].toObject()[QStringLiteral("title" )].toString(), |
424 | json["milestone" ].toObject()[QStringLiteral("description" )].toString(), |
425 | json["milestone" ].toObject()[QStringLiteral("state" )].toString() == "open" }; |
426 | |
427 | issue.milestone = sMilestone; |
428 | |
429 | return issue; |
430 | } |
431 | |
432 | PullRequest GitLabRestApi::prFromJson(const QJsonObject &json) const |
433 | { |
434 | PullRequest pr; |
435 | pr.id = json["id" ].toInt(); |
436 | pr.number = json["id" ].toInt(); |
437 | pr.title = json["title" ].toString(); |
438 | pr.body = json["description" ].toString().toUtf8(); |
439 | pr.url = json["web_url" ].toString(); |
440 | pr.head = json["head" ].toObject()["ref" ].toString(); |
441 | pr.state.sha = json["head" ].toObject()["sha" ].toString(); |
442 | pr.base = json["base" ].toObject()["ref" ].toString(); |
443 | pr.isOpen = json["state" ].toString() == "open" ; |
444 | pr.draft = json["draft" ].toBool(); |
445 | pr.creation = json["created_at" ].toVariant().toDateTime(); |
446 | |
447 | pr.creator |
448 | = { json["author" ].toObject()["id" ].toInt(), json["author" ].toObject()["username" ].toString(), |
449 | json["author" ].toObject()["avatar_url" ].toString(), json["author" ].toObject()["web_url" ].toString(), "" }; |
450 | |
451 | const auto labels = json["labels" ].toArray(); |
452 | |
453 | for (const auto &label : labels) |
454 | { |
455 | Label sLabel; |
456 | sLabel.name = label.toString(); |
457 | pr.labels.append(sLabel); |
458 | } |
459 | |
460 | const auto assignee = json["assignee" ].toObject(); |
461 | GitServer::User sAssignee; |
462 | sAssignee.id = assignee["id" ].toInt(); |
463 | sAssignee.url = assignee["web_url" ].toString(); |
464 | sAssignee.name = assignee["username" ].toString(); |
465 | sAssignee.avatar = assignee["avatar_url" ].toString(); |
466 | |
467 | pr.assignees.append(sAssignee); |
468 | |
469 | Milestone sMilestone { json["milestone" ].toObject()[QStringLiteral("id" )].toInt(), |
470 | json["milestone" ].toObject()[QStringLiteral("number" )].toInt(), |
471 | json["milestone" ].toObject()[QStringLiteral("iid" )].toString(), |
472 | json["milestone" ].toObject()[QStringLiteral("title" )].toString(), |
473 | json["milestone" ].toObject()[QStringLiteral("description" )].toString(), |
474 | json["milestone" ].toObject()[QStringLiteral("state" )].toString() == "open" }; |
475 | |
476 | pr.milestone = sMilestone; |
477 | |
478 | return pr; |
479 | } |
480 | |