1#include "GitHubRestApi.h"
2#include <Issue.h>
3
4#include <QNetworkAccessManager>
5#include <QNetworkRequest>
6#include <QNetworkReply>
7#include <QJsonDocument>
8#include <QJsonObject>
9#include <QJsonArray>
10#include <QTimer>
11#include <QUrlQuery>
12
13#include <QLogger.h>
14
15using namespace QLogger;
16using namespace GitServer;
17
18GitHubRestApi::GitHubRestApi(QString repoOwner, QString repoName, const ServerAuthentication &auth, QObject *parent)
19 : IRestApi(auth, parent)
20{
21 if (!repoOwner.endsWith("/"))
22 repoOwner.append("/");
23
24 if (!repoOwner.startsWith("/"))
25 repoOwner.prepend("/");
26
27 if (repoName.endsWith("/"))
28 repoName = repoName.left(repoName.size() - 1);
29
30 mRepoEndpoint = QString("/repos") + repoOwner + repoName;
31
32 mAuthString = "Basic "
33 + QByteArray(QString(QStringLiteral("%1:%2")).arg(mAuth.userName, mAuth.userPass).toLocal8Bit()).toBase64();
34}
35
36void GitHubRestApi::testConnection()
37{
38 auto request = createRequest("/user/repos");
39
40 const auto reply = mManager->get(request);
41
42 connect(reply, &QNetworkReply::finished, this, [this]() {
43 const auto reply = qobject_cast<QNetworkReply *>(sender());
44 QString errorStr;
45 const auto tmpDoc = validateData(reply, errorStr);
46
47 if (!tmpDoc.isEmpty())
48 emit connectionTested();
49 else
50 emit errorOccurred(errorStr);
51 });
52}
53
54void GitHubRestApi::createIssue(const Issue &issue)
55{
56 QJsonDocument doc(issue.toJson());
57 const auto data = doc.toJson(QJsonDocument::Compact);
58
59 auto request = createRequest(mRepoEndpoint + "/issues");
60 request.setRawHeader("Content-Length", QByteArray::number(data.size()));
61 const auto reply = mManager->post(request, data);
62
63 connect(reply, &QNetworkReply::finished, this, &GitHubRestApi::onIssueCreated);
64}
65
66void GitHubRestApi::updateIssue(int issueNumber, const Issue &issue)
67{
68 QJsonDocument doc(issue.toJson());
69 const auto data = doc.toJson(QJsonDocument::Compact);
70
71 auto request = createRequest(QString(mRepoEndpoint + "/issues/%1").arg(issueNumber));
72 request.setRawHeader("Content-Length", QByteArray::number(data.size()));
73 request.setRawHeader("Accept", "application/vnd.github.v3+json");
74 const auto reply = mManager->post(request, data);
75
76 connect(reply, &QNetworkReply::finished, this, [this]() {
77 const auto reply = qobject_cast<QNetworkReply *>(sender());
78 QString errorStr;
79 const auto tmpDoc = validateData(reply, errorStr);
80
81 if (const auto issueObj = tmpDoc.object(); !issueObj.contains("pull_request"))
82 {
83 const auto issue = issueFromJson(issueObj);
84 emit issueUpdated(issue);
85 }
86 else
87 emit errorOccurred(errorStr);
88 });
89}
90
91void GitHubRestApi::updatePullRequest(int number, const PullRequest &pr)
92{
93 QJsonDocument doc(Issue(pr).toJson());
94 const auto data = doc.toJson(QJsonDocument::Compact);
95
96 auto request = createRequest(QString(mRepoEndpoint + "/issues/%1").arg(number));
97 request.setRawHeader("Content-Length", QByteArray::number(data.size()));
98 const auto reply = mManager->post(request, data);
99
100 connect(reply, &QNetworkReply::finished, this, [this]() {
101 const auto reply = qobject_cast<QNetworkReply *>(sender());
102 QString errorStr;
103 const auto tmpDoc = validateData(reply, errorStr);
104
105 if (const auto issueObj = tmpDoc.object(); issueObj.contains("pull_request"))
106 {
107 const auto pr = prFromJson(issueObj);
108 emit pullRequestUpdated(pr);
109 }
110 else
111 emit errorOccurred(errorStr);
112 });
113}
114
115void GitHubRestApi::createPullRequest(const PullRequest &pullRequest)
116{
117 QJsonDocument doc(pullRequest.toJson());
118 const auto data = doc.toJson(QJsonDocument::Compact);
119
120 auto request = createRequest(mRepoEndpoint + "/pulls");
121 request.setRawHeader("Content-Length", QByteArray::number(data.size()));
122
123 const auto reply = mManager->post(request, data);
124 connect(reply, &QNetworkReply::finished, this, [this]() {
125 const auto reply = qobject_cast<QNetworkReply *>(sender());
126 QString errorStr;
127 const auto tmpDoc = validateData(reply, errorStr);
128
129 if (!tmpDoc.isEmpty())
130 {
131 const auto pr = prFromJson(tmpDoc.object());
132 emit pullRequestUpdated(pr);
133
134 updatePullRequest(pr.number, pr);
135 }
136 });
137}
138
139void GitHubRestApi::requestLabels()
140{
141 const auto reply = mManager->get(createRequest(mRepoEndpoint + "/labels"));
142
143 connect(reply, &QNetworkReply::finished, this, &GitHubRestApi::onLabelsReceived);
144}
145
146void GitHubRestApi::requestMilestones()
147{
148 const auto reply = mManager->get(createRequest(mRepoEndpoint + "/milestones"));
149
150 connect(reply, &QNetworkReply::finished, this, &GitHubRestApi::onMilestonesReceived);
151}
152
153void GitHubRestApi::requestIssues(int page)
154{
155 auto request = createRequest(mRepoEndpoint + "/issues");
156 auto url = request.url();
157 QUrlQuery query;
158
159 if (page != -1)
160 {
161 query.addQueryItem("page", QString::number(page));
162 url.setQuery(query);
163 }
164
165 query.addQueryItem("per_page", QString::number(100));
166 url.setQuery(query);
167
168 request.setUrl(url);
169
170 const auto reply = mManager->get(request);
171
172 connect(reply, &QNetworkReply::finished, this, &GitHubRestApi::onIssuesReceived);
173}
174
175void GitHubRestApi::requestPullRequests(int page)
176{
177 auto request = createRequest(mRepoEndpoint + "/pulls");
178 auto url = request.url();
179 QUrlQuery query;
180
181 if (page != -1)
182 {
183 query.addQueryItem("page", QString::number(page));
184 url.setQuery(query);
185 }
186
187 query.addQueryItem("per_page", QString::number(100));
188 url.setQuery(query);
189
190 request.setUrl(url);
191
192 const auto reply = mManager->get(request);
193
194 connect(reply, &QNetworkReply::finished, this, &GitHubRestApi::onPullRequestReceived);
195}
196
197void GitHubRestApi::mergePullRequest(int number, const QByteArray &data)
198{
199 const auto reply = mManager->put(createRequest(mRepoEndpoint + QString("/pulls/%1/merge").arg(number)), data);
200
201 connect(reply, &QNetworkReply::finished, this, &GitHubRestApi::onPullRequestMerged);
202}
203
204void GitHubRestApi::requestComments(int issueNumber)
205{
206 const auto reply = mManager->get(createRequest(mRepoEndpoint + QString("/issues/%1/comments").arg(issueNumber)));
207
208 connect(reply, &QNetworkReply::finished, this, [this, issueNumber]() { onCommentsReceived(issueNumber); });
209}
210
211void GitHubRestApi::requestReviews(int prNumber)
212{
213 const auto reply = mManager->get(createRequest(mRepoEndpoint + QString("/pulls/%1/reviews").arg(prNumber)));
214
215 connect(reply, &QNetworkReply::finished, this, [this, prNumber]() { onReviewsReceived(prNumber); });
216}
217
218void GitHubRestApi::requestCommitsFromPR(int prNumber)
219{
220 auto request = createRequest(mRepoEndpoint + QString("/pulls/%1/commits").arg(prNumber));
221 const auto reply = mManager->get(request);
222
223 connect(reply, &QNetworkReply::finished, this, [this, prNumber]() { onCommitsReceived(prNumber); });
224}
225
226void GitHubRestApi::addIssueComment(const Issue &issue, const QString &text)
227{
228 QJsonObject object;
229 object.insert("body", text);
230
231 QJsonDocument doc(object);
232 const auto data = doc.toJson(QJsonDocument::Compact);
233
234 auto request = createRequest(QString(mRepoEndpoint + "/issues/%1/comments").arg(issue.number));
235 request.setRawHeader("Content-Length", QByteArray::number(data.size()));
236 request.setRawHeader("Accept", "application/vnd.github.v3+json");
237 const auto reply = mManager->post(request, data);
238
239 connect(reply, &QNetworkReply::finished, this, [this, issue]() {
240 const auto reply = qobject_cast<QNetworkReply *>(sender());
241 const auto statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
242 QString errorStr;
243 const auto tmpDoc = validateData(reply, errorStr);
244
245 if (statusCode.isValid() && statusCode.toInt() == 201 && !tmpDoc.isEmpty())
246 {
247 const auto commentData = tmpDoc.object();
248 auto newIssue = issue;
249 newIssue.commentsCount += 1;
250
251 Comment c;
252 c.id = commentData["id"].toInt();
253 c.body = commentData["body"].toString();
254 c.creation = commentData["created_at"].toVariant().toDateTime();
255 c.association = commentData["author_association"].toString();
256
257 GitServer::User sAssignee;
258 sAssignee.id = commentData["user"].toObject()["id"].toInt();
259 sAssignee.url = commentData["user"].toObject()["html_url"].toString();
260 sAssignee.name = commentData["user"].toObject()["login"].toString();
261 sAssignee.avatar = commentData["user"].toObject()["avatar_url"].toString();
262 sAssignee.type = commentData["user"].toObject()["type"].toString();
263
264 c.creator = std::move(sAssignee);
265 newIssue.comments.append(std::move(c));
266
267 emit issueUpdated(newIssue);
268 }
269 });
270}
271
272void GitHubRestApi::addPrReview(int prNumber, const QString &body, const QString &event)
273{
274 QJsonObject object;
275 object.insert("body", body);
276 object.insert("pull_number", prNumber);
277 object.insert("event", event);
278
279 QJsonDocument doc(object);
280 const auto data = doc.toJson(QJsonDocument::Compact);
281
282 auto request = createRequest(QString(mRepoEndpoint + "/pulls/%1/reviews").arg(prNumber));
283 request.setRawHeader("Content-Length", QByteArray::number(data.size()));
284 request.setRawHeader("Accept", "application/vnd.github.v3+json");
285 const auto reply = mManager->post(request, data);
286
287 connect(reply, &QNetworkReply::finished, this, [this, prNumber]() {
288 const auto reply = qobject_cast<QNetworkReply *>(sender());
289 const auto statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
290 QString errorStr;
291 const auto tmpDoc = validateData(reply, errorStr);
292
293 if (statusCode.isValid() && statusCode.toInt() == 201 && !tmpDoc.isEmpty())
294 {
295 const auto commentData = tmpDoc.object();
296 Review r;
297 r.id = commentData["id"].toInt();
298 r.body = commentData["body"].toString();
299 r.creation = commentData["submitted_at"].toVariant().toDateTime();
300 r.state = commentData["state"].toString();
301 r.association = commentData["author_association"].toString();
302
303 GitServer::User sAssignee;
304 sAssignee.id = commentData["user"].toObject()["id"].toInt();
305 sAssignee.url = commentData["user"].toObject()["html_url"].toString();
306 sAssignee.name = commentData["user"].toObject()["login"].toString();
307 sAssignee.avatar = commentData["user"].toObject()["avatar_url"].toString();
308 sAssignee.type = commentData["user"].toObject()["type"].toString();
309
310 r.creator = std::move(sAssignee);
311
312 emit commentReviewsReceived(prNumber, { { r.id, r } });
313 }
314 });
315}
316
317void GitHubRestApi::addPrCodeReview(int prNumber, const QString &body, const QString &path, int pos, const QString &sha)
318{
319 QJsonObject object;
320 object.insert("body", body);
321 object.insert("path", path);
322 object.insert("line", pos);
323 object.insert("commit_id", sha);
324
325 QJsonDocument doc(object);
326 const auto data = doc.toJson(QJsonDocument::Compact);
327
328 auto request = createRequest(QString(mRepoEndpoint + "/pulls/%1/comments").arg(prNumber));
329 request.setRawHeader("Content-Length", QByteArray::number(data.size()));
330 request.setRawHeader("Accept", "application/vnd.github.v3+json");
331 const auto reply = mManager->post(request, data);
332
333 connect(reply, &QNetworkReply::finished, this, [this, prNumber]() {
334 const auto reply = qobject_cast<QNetworkReply *>(sender());
335 const auto statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
336 QString errorStr;
337 const auto tmpDoc = validateData(reply, errorStr);
338
339 if (statusCode.isValid() && statusCode.toInt() == 201 && !tmpDoc.isEmpty())
340 {
341 const auto commentData = tmpDoc.object();
342 CodeReview c;
343 c.outdated = false;
344 c.id = commentData["id"].toInt();
345 c.body = commentData["body"].toString();
346 c.creation = commentData["created_at"].toVariant().toDateTime();
347 c.association = commentData["author_association"].toString();
348 c.diff.diff = commentData["diff_hunk"].toString();
349 c.diff.file = commentData["path"].toString();
350
351 if (commentData.contains("line"))
352 c.diff.line = commentData["line"].toInt();
353 else
354 {
355 if (commentData["position"].toInt() != 0)
356 c.diff.line = commentData["position"].toInt();
357 else
358 c.outdated = true;
359 }
360
361 if (commentData.contains("original_line"))
362 c.diff.originalLine = commentData["original_line"].toInt();
363 else
364 c.diff.originalLine = commentData["original_position"].toInt();
365
366 c.reviewId = commentData["pull_request_review_id"].toInt();
367 c.replyToId = commentData["in_reply_to_id"].toInt();
368
369 GitServer::User sAssignee;
370 sAssignee.id = commentData["user"].toObject()["id"].toInt();
371 sAssignee.url = commentData["user"].toObject()["html_url"].toString();
372 sAssignee.name = commentData["user"].toObject()["login"].toString();
373 sAssignee.avatar = commentData["user"].toObject()["avatar_url"].toString();
374 sAssignee.type = commentData["user"].toObject()["type"].toString();
375
376 c.creator = std::move(sAssignee);
377
378 emit codeReviewsReceived(prNumber, { c });
379 }
380 });
381}
382
383void GitHubRestApi::replyCodeReview(int prNumber, int commentId, const QString &msgBody)
384{
385 QJsonObject object;
386 object.insert("body", msgBody);
387
388 QJsonDocument doc(object);
389 const auto data = doc.toJson(QJsonDocument::Compact);
390
391 const auto url = QString("%1/pulls/%2/comments/%3/replies")
392 .arg(mRepoEndpoint, QString::number(prNumber), QString::number(commentId));
393 auto request = createRequest(url);
394 request.setRawHeader("Content-Length", QByteArray::number(data.size()));
395 request.setRawHeader("Accept", "application/vnd.github.v3+json");
396 const auto reply = mManager->post(request, data);
397
398 connect(reply, &QNetworkReply::finished, this, [this, prNumber]() {
399 const auto reply = qobject_cast<QNetworkReply *>(sender());
400 const auto statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
401 QString errorStr;
402 const auto tmpDoc = validateData(reply, errorStr);
403
404 if (statusCode.isValid() && statusCode.toInt() == 201 && !tmpDoc.isEmpty())
405 {
406 const auto commentData = tmpDoc.object();
407 CodeReview c;
408 c.outdated = false;
409 c.id = commentData["id"].toInt();
410 c.body = commentData["body"].toString();
411 c.creation = commentData["created_at"].toVariant().toDateTime();
412 c.association = commentData["author_association"].toString();
413 c.diff.diff = commentData["diff_hunk"].toString();
414 c.diff.file = commentData["path"].toString();
415
416 if (commentData.contains("line"))
417 c.diff.line = commentData["line"].toInt();
418 else
419 {
420 if (commentData["position"].toInt() != 0)
421 c.diff.line = commentData["position"].toInt();
422 else
423 c.outdated = true;
424 }
425
426 if (commentData.contains("original_line"))
427 c.diff.originalLine = commentData["original_line"].toInt();
428 else
429 c.diff.originalLine = commentData["original_position"].toInt();
430
431 c.reviewId = commentData["pull_request_review_id"].toInt();
432 c.replyToId = commentData["in_reply_to_id"].toInt();
433
434 GitServer::User sAssignee;
435 sAssignee.id = commentData["user"].toObject()["id"].toInt();
436 sAssignee.url = commentData["user"].toObject()["html_url"].toString();
437 sAssignee.name = commentData["user"].toObject()["login"].toString();
438 sAssignee.avatar = commentData["user"].toObject()["avatar_url"].toString();
439 sAssignee.type = commentData["user"].toObject()["type"].toString();
440
441 c.creator = std::move(sAssignee);
442
443 emit codeReviewsReceived(prNumber, { c });
444 }
445 });
446}
447
448QNetworkRequest GitHubRestApi::createRequest(const QString &page) const
449{
450 QNetworkRequest request;
451 request.setUrl(QUrl(QString("%1%2").arg(mAuth.endpointUrl, page)));
452 request.setRawHeader("User-Agent", "GitQlient");
453 request.setRawHeader("X-Custom-User-Agent", "GitQlient");
454 request.setRawHeader("Content-Type", "application/json");
455 request.setRawHeader("Accept", "application/vnd.github.v3+json");
456 request.setRawHeader("Authorization", mAuthString);
457
458 return request;
459}
460
461void GitHubRestApi::onLabelsReceived()
462{
463 const auto reply = qobject_cast<QNetworkReply *>(sender());
464 QString errorStr;
465 const auto tmpDoc = validateData(reply, errorStr);
466 QVector<Label> labels;
467
468 if (!tmpDoc.isEmpty())
469 {
470 const auto labelsArray = tmpDoc.array();
471
472 for (const auto &label : labelsArray)
473 {
474 const auto jobObject = label.toObject();
475 Label sLabel { jobObject[QStringLiteral("id")].toInt(),
476 jobObject[QStringLiteral("node_id")].toString(),
477 jobObject[QStringLiteral("url")].toString(),
478 jobObject[QStringLiteral("name")].toString(),
479 jobObject[QStringLiteral("description")].toString(),
480 jobObject[QStringLiteral("color")].toString(),
481 jobObject[QStringLiteral("default")].toBool() };
482
483 labels.append(std::move(sLabel));
484 }
485 }
486 else
487 emit errorOccurred(errorStr);
488
489 emit labelsReceived(labels);
490}
491
492void GitHubRestApi::onMilestonesReceived()
493{
494 const auto reply = qobject_cast<QNetworkReply *>(sender());
495 QString errorStr;
496 const auto tmpDoc = validateData(reply, errorStr);
497 QVector<Milestone> milestones;
498
499 if (!tmpDoc.isEmpty())
500 {
501 const auto labelsArray = tmpDoc.array();
502
503 for (const auto &label : labelsArray)
504 {
505 const auto jobObject = label.toObject();
506 Milestone sMilestone { jobObject[QStringLiteral("id")].toInt(),
507 jobObject[QStringLiteral("number")].toInt(),
508 jobObject[QStringLiteral("node_id")].toString(),
509 jobObject[QStringLiteral("title")].toString(),
510 jobObject[QStringLiteral("description")].toString(),
511 jobObject[QStringLiteral("state")].toString() == "open" };
512
513 milestones.append(std::move(sMilestone));
514 }
515 }
516 else
517 emit errorOccurred(errorStr);
518
519 emit milestonesReceived(milestones);
520}
521
522void GitHubRestApi::onIssueCreated()
523{
524 const auto reply = qobject_cast<QNetworkReply *>(sender());
525 QString errorStr;
526 const auto tmpDoc = validateData(reply, errorStr);
527
528 if (!tmpDoc.isEmpty())
529 {
530 const auto issue = issueFromJson(tmpDoc.object());
531 emit issueUpdated(issue);
532 }
533 else
534 emit errorOccurred(errorStr);
535}
536
537void GitHubRestApi::onPullRequestCreated()
538{
539 const auto reply = qobject_cast<QNetworkReply *>(sender());
540 QString errorStr;
541 const auto tmpDoc = validateData(reply, errorStr);
542
543 if (!tmpDoc.isEmpty())
544 {
545 const auto pr = prFromJson(tmpDoc.object());
546
547 /*
548 QTimer::singleShot(200, [this, number = pr.number]() {
549 const auto reply = mManager->get(createRequest(mRepoEndpoint + QString("/pulls/%1").arg(number)));
550 connect(reply, &QNetworkReply::finished, this, [this, pr]() { onPullRequestDetailesReceived(pr); });
551 });
552 */
553 QTimer::singleShot(200, this, [this, pr]() {
554 auto request = createRequest(mRepoEndpoint + QString("/commits/%1/status").arg(pr.state.sha));
555 const auto reply = mManager->get(request);
556 connect(reply, &QNetworkReply::finished, this, [this, pr] { onPullRequestStatusReceived(pr); });
557 });
558
559 emit pullRequestUpdated(pr);
560 }
561 else
562 emit errorOccurred(errorStr);
563}
564
565void GitHubRestApi::onPullRequestMerged()
566{
567 const auto reply = qobject_cast<QNetworkReply *>(sender());
568 QString errorStr;
569 const auto tmpDoc = validateData(reply, errorStr);
570
571 if (!tmpDoc.isEmpty())
572 emit pullRequestMerged();
573 else
574 emit errorOccurred(errorStr);
575}
576
577void GitHubRestApi::onPullRequestReceived()
578{
579 const auto reply = qobject_cast<QNetworkReply *>(sender());
580
581 if (const auto pagination = QString::fromUtf8(reply->rawHeader("Link")); !pagination.isEmpty())
582 {
583 QStringList pages = pagination.split(",");
584 auto current = 0;
585 auto next = 0;
586 auto total = 0;
587
588 for (const auto &page : pages)
589 {
590 const auto values = page.trimmed().remove("<").remove(">").split(";");
591
592 if (values.last().contains("next"))
593 {
594 next = values.first().split("page=").last().toInt();
595 current = next - 1;
596 }
597 else if (values.last().contains("last"))
598 total = values.first().split("page=").last().toInt();
599 }
600
601 emit paginationPresent(current, next, total);
602 }
603 else
604 emit paginationPresent(0, 0, 0);
605
606 QString errorStr;
607 const auto tmpDoc = validateData(reply, errorStr);
608 QVector<PullRequest> pullRequests;
609
610 if (!tmpDoc.isEmpty())
611 {
612 const auto issuesArray = tmpDoc.array();
613 for (const auto &issueData : issuesArray)
614 {
615 const auto pr = prFromJson(issueData.toObject());
616 pullRequests.append(pr);
617
618 /*
619 QTimer::singleShot(200, [this, number = pr.number]() {
620 const auto reply = mManager->get(createRequest(mRepoEndpoint + QString("/pulls/%1").arg(number)));
621 connect(reply, &QNetworkReply::finished, this, [this, pr]() { onPullRequestDetailsReceived(pr); });
622 });
623 */
624 QTimer::singleShot(200, this, [this, pr]() {
625 auto request = createRequest(mRepoEndpoint + QString("/commits/%1/status").arg(pr.state.sha));
626 const auto reply = mManager->get(request);
627 connect(reply, &QNetworkReply::finished, this, [this, pr] { onPullRequestStatusReceived(pr); });
628 });
629 }
630 }
631 else
632 emit errorOccurred(errorStr);
633
634 std::sort(pullRequests.begin(), pullRequests.end(),
635 [](const PullRequest &p1, const PullRequest &p2) { return p1.creation > p2.creation; });
636
637 emit pullRequestsReceived(pullRequests);
638}
639
640void GitHubRestApi::onPullRequestStatusReceived(PullRequest pr)
641{
642 const auto reply = qobject_cast<QNetworkReply *>(sender());
643 QString errorStr;
644 const auto tmpDoc = validateData(reply, errorStr);
645
646 if (!tmpDoc.isEmpty())
647 {
648 const auto obj = tmpDoc.object();
649
650 pr.state.state = obj["state"].toString();
651
652 pr.state.eState = pr.state.state == "success" ? PullRequest::HeadState::State::Success
653 : pr.state.state == "failure" ? PullRequest::HeadState::State::Failure
654 : PullRequest::HeadState::State::Pending;
655
656 const auto statuses = obj["statuses"].toArray();
657
658 for (const auto &status : statuses)
659 {
660 auto statusStr = status["state"].toString();
661
662 if (statusStr == "ok")
663 statusStr = "success";
664 else if (statusStr == "error")
665 statusStr = "failure";
666
667 PullRequest::HeadState::Check check { status["description"].toString(), statusStr,
668 status["target_url"].toString(), status["context"].toString() };
669
670 pr.state.checks.append(std::move(check));
671 }
672
673 emit pullRequestUpdated(pr);
674 }
675 else
676 emit errorOccurred(errorStr);
677}
678
679void GitHubRestApi::onIssuesReceived()
680{
681 const auto reply = qobject_cast<QNetworkReply *>(sender());
682
683 if (const auto pagination = QString::fromUtf8(reply->rawHeader("Link")); !pagination.isEmpty())
684 {
685 QStringList pages = pagination.split(",");
686 auto current = 0;
687 auto next = 0;
688 auto total = 0;
689
690 for (const auto &page : pages)
691 {
692 const auto values = page.trimmed().remove("<").remove(">").split(";");
693
694 if (values.last().contains("next"))
695 {
696 next = values.first().split("page=").last().toInt();
697 current = next - 1;
698 }
699 else if (values.last().contains("last"))
700 total = values.first().split("page=").last().toInt();
701 }
702
703 emit paginationPresent(current, next, total);
704 }
705 else
706 emit paginationPresent(0, 0, 0);
707
708 QString errorStr;
709 const auto tmpDoc = validateData(reply, errorStr);
710 QVector<Issue> issues;
711
712 if (!tmpDoc.isEmpty())
713 {
714 const auto issuesArray = tmpDoc.array();
715
716 for (const auto &issueData : issuesArray)
717 {
718 if (const auto issueObj = issueData.toObject(); !issueObj.contains("pull_request"))
719 issues.append(issueFromJson(issueObj));
720 }
721 }
722 else
723 emit errorOccurred(errorStr);
724
725 emit issuesReceived(issues);
726
727 for (auto &issue : issues)
728 QTimer::singleShot(200, this, [this, num = issue.number]() { requestComments(num); });
729}
730
731void GitHubRestApi::onCommentsReceived(int issueNumber)
732{
733 const auto reply = qobject_cast<QNetworkReply *>(sender());
734 QString errorStr;
735 const auto tmpDoc = validateData(reply, errorStr);
736
737 if (!tmpDoc.isEmpty())
738 {
739 QVector<Comment> comments;
740 const auto commentsArray = tmpDoc.array();
741
742 for (const auto &commentData : commentsArray)
743 {
744 Comment c;
745 c.id = commentData["id"].toInt();
746 c.body = commentData["body"].toString();
747 c.creation = commentData["created_at"].toVariant().toDateTime();
748 c.association = commentData["author_association"].toString();
749
750 GitServer::User sAssignee;
751 sAssignee.id = commentData["user"].toObject()["id"].toInt();
752 sAssignee.url = commentData["user"].toObject()["html_url"].toString();
753 sAssignee.name = commentData["user"].toObject()["login"].toString();
754 sAssignee.avatar = commentData["user"].toObject()["avatar_url"].toString();
755 sAssignee.type = commentData["user"].toObject()["type"].toString();
756
757 c.creator = std::move(sAssignee);
758 comments.append(std::move(c));
759 }
760
761 emit commentsReceived(issueNumber, comments);
762 }
763}
764
765void GitHubRestApi::onPullRequestDetailsReceived(PullRequest pr)
766{
767 const auto reply = qobject_cast<QNetworkReply *>(sender());
768 QString errorStr;
769 const auto tmpDoc = validateData(reply, errorStr);
770
771 if (!tmpDoc.isEmpty())
772 {
773 const auto prInfo = tmpDoc.object();
774
775 pr.commentsCount = prInfo["comments"].toInt();
776 pr.reviewCommentsCount = prInfo["review_comments"].toInt();
777 pr.commitCount = prInfo["commits"].toInt();
778 pr.additions = prInfo["aditions"].toInt();
779 pr.deletions = prInfo["deletions"].toInt();
780 pr.changedFiles = prInfo["changed_files"].toInt();
781 pr.merged = prInfo["merged"].toBool();
782 pr.mergeable = prInfo["mergeable"].toBool();
783 pr.rebaseable = prInfo["rebaseable"].toBool();
784 pr.mergeableState = prInfo["mergeable_state"].toString();
785
786 emit pullRequestUpdated(pr);
787 }
788}
789
790void GitHubRestApi::onReviewsReceived(int prNumber)
791{
792 const auto reply = qobject_cast<QNetworkReply *>(sender());
793 QString errorStr;
794 const auto tmpDoc = validateData(reply, errorStr);
795
796 if (!tmpDoc.isEmpty())
797 {
798 QMap<int, Review> reviews;
799 const auto commentsArray = tmpDoc.array();
800
801 for (const auto &commentData : commentsArray)
802 {
803 auto id = commentData["id"].toInt();
804
805 Review r;
806 r.id = id;
807 r.body = commentData["body"].toString();
808 r.creation = commentData["submitted_at"].toVariant().toDateTime();
809 r.state = commentData["state"].toString();
810 r.association = commentData["author_association"].toString();
811
812 GitServer::User sAssignee;
813 sAssignee.id = commentData["user"].toObject()["id"].toInt();
814 sAssignee.url = commentData["user"].toObject()["html_url"].toString();
815 sAssignee.name = commentData["user"].toObject()["login"].toString();
816 sAssignee.avatar = commentData["user"].toObject()["avatar_url"].toString();
817 sAssignee.type = commentData["user"].toObject()["type"].toString();
818
819 r.creator = std::move(sAssignee);
820 reviews.insert(id, std::move(r));
821 }
822
823 emit commentReviewsReceived(prNumber, reviews);
824
825 QTimer::singleShot(200, this, [this, prNumber]() { requestReviewComments(prNumber); });
826 }
827}
828
829void GitHubRestApi::requestReviewComments(int prNumber)
830{
831 const auto reply = mManager->get(createRequest(mRepoEndpoint + QString("/pulls/%1/comments").arg(prNumber)));
832
833 connect(reply, &QNetworkReply::finished, this, [this, prNumber]() { onReviewCommentsReceived(prNumber); });
834}
835
836void GitHubRestApi::onReviewCommentsReceived(int prNumber)
837{
838 const auto reply = qobject_cast<QNetworkReply *>(sender());
839 QString errorStr;
840 const auto tmpDoc = validateData(reply, errorStr);
841
842 if (!tmpDoc.isEmpty())
843 {
844 QVector<CodeReview> comments;
845 const auto commentsArray = tmpDoc.array();
846
847 for (const auto &commentData : commentsArray)
848 {
849 CodeReview c;
850 c.outdated = false;
851 c.id = commentData["id"].toInt();
852 c.body = commentData["body"].toString();
853 c.creation = commentData["created_at"].toVariant().toDateTime();
854 c.association = commentData["author_association"].toString();
855 c.diff.diff = commentData["diff_hunk"].toString();
856 c.diff.file = commentData["path"].toString();
857
858 if (commentData.toObject().contains("line"))
859 c.diff.line = commentData["line"].toInt();
860 else
861 {
862 if (commentData["position"].toInt() != 0)
863 c.diff.line = commentData["position"].toInt();
864 else
865 c.outdated = true;
866 }
867
868 if (commentData.toObject().contains("original_line"))
869 c.diff.originalLine = commentData["original_line"].toInt();
870 else
871 c.diff.originalLine = commentData["original_position"].toInt();
872
873 c.reviewId = commentData["pull_request_review_id"].toInt();
874 c.replyToId = commentData["in_reply_to_id"].toInt();
875
876 GitServer::User sAssignee;
877 sAssignee.id = commentData["user"].toObject()["id"].toInt();
878 sAssignee.url = commentData["user"].toObject()["html_url"].toString();
879 sAssignee.name = commentData["user"].toObject()["login"].toString();
880 sAssignee.avatar = commentData["user"].toObject()["avatar_url"].toString();
881 sAssignee.type = commentData["user"].toObject()["type"].toString();
882
883 c.creator = std::move(sAssignee);
884 comments.append(std::move(c));
885 }
886
887 emit codeReviewsReceived(prNumber, comments);
888 }
889}
890
891void GitHubRestApi::onCommitsReceived(int prNumber)
892{
893 const auto reply = qobject_cast<QNetworkReply *>(sender());
894 QString errorStr;
895 const auto tmpDoc = validateData(reply, errorStr);
896
897 if (!tmpDoc.isEmpty())
898 {
899 QVector<Commit> commits;
900 const auto commitsArray = tmpDoc.array();
901
902 for (const auto &commitData : commitsArray)
903 {
904 Commit c;
905 c.url = commitData["html_url"].toString();
906 c.sha = commitData["sha"].toString();
907
908 GitServer::User sAuthor;
909 sAuthor.id = commitData["author"].toObject()["id"].toInt();
910 sAuthor.url = commitData["author"].toObject()["html_url"].toString();
911 sAuthor.name = commitData["author"].toObject()["login"].toString();
912 sAuthor.avatar = commitData["author"].toObject()["avatar_url"].toString();
913 sAuthor.type = commitData["author"].toObject()["type"].toString();
914
915 c.author = std::move(sAuthor);
916
917 GitServer::User sCommitter;
918 sCommitter.id = commitData["committer"].toObject()["id"].toInt();
919 sCommitter.url = commitData["committer"].toObject()["html_url"].toString();
920 sCommitter.name = commitData["committer"].toObject()["login"].toString();
921 sCommitter.avatar = commitData["committer"].toObject()["avatar_url"].toString();
922 sCommitter.type = commitData["committer"].toObject()["type"].toString();
923
924 c.commiter = std::move(sCommitter);
925
926 c.message = commitData["commit"].toObject()["message"].toString();
927 c.authorCommittedTimestamp
928 = commitData["commit"].toObject()["author"].toObject()["date"].toVariant().toDateTime();
929
930 commits.append(std::move(c));
931 }
932
933 const auto link = reply->rawHeader("Link").split(',');
934
935 QString nextUrl;
936 QString lastUrl;
937 auto currentPage = 0;
938 auto lastPage = 0;
939
940 for (auto &pair : link)
941 {
942 const auto page = pair.split(';');
943 const auto rel = page.last().trimmed();
944
945 if (rel.contains("next"))
946 {
947 nextUrl = QString::fromUtf8(page.first().trimmed());
948 nextUrl.remove(0, 1);
949 nextUrl.remove(nextUrl.size() - 1, 1);
950
951 currentPage = nextUrl.split("page=").last().toInt();
952 }
953 else if (rel.contains("last"))
954 {
955 lastUrl = QString::fromUtf8(page.first().trimmed());
956 lastUrl.remove(0, 1);
957 lastUrl.remove(lastUrl.size() - 1, 1);
958
959 lastPage = lastUrl.split("page=").last().toInt();
960 }
961 }
962
963 if (currentPage <= lastPage)
964 {
965 auto request = createRequest(mRepoEndpoint + QString("/pulls/%1/commits").arg(prNumber));
966 request.setUrl(nextUrl);
967 const auto reply = mManager->get(request);
968
969 connect(reply, &QNetworkReply::finished, this, [this, prNumber]() { onCommitsReceived(prNumber); });
970 }
971
972 std::sort(commits.begin(), commits.end(), [](const Commit &c1, const Commit &c2) {
973 return c1.authorCommittedTimestamp < c2.authorCommittedTimestamp;
974 });
975
976 emit commitsReceived(prNumber, commits, currentPage, lastPage);
977 }
978}
979
980Issue GitHubRestApi::issueFromJson(const QJsonObject &json) const
981{
982 Issue issue;
983 issue.number = json["number"].toInt();
984 issue.title = json["title"].toString();
985 issue.body = json["body"].toString().toUtf8();
986 issue.url = json["html_url"].toString();
987 issue.creation = json["created_at"].toVariant().toDateTime();
988 issue.commentsCount = json["comments"].toInt();
989 issue.isOpen = json["state"].toString() == "open";
990
991 issue.creator = { json["user"].toObject()["id"].toInt(), json["user"].toObject()["login"].toString(),
992 json["user"].toObject()["avatar_url"].toString(), json["user"].toObject()["html_url"].toString(),
993 json["user"].toObject()["type"].toString() };
994
995 const auto labels = json["labels"].toArray();
996
997 for (const auto &label : labels)
998 {
999 issue.labels.append({ label["id"].toInt(), label["node_id"].toString(), label["url"].toString(),
1000 label["name"].toString(), label["description"].toString(), label["color"].toString(),
1001 label["default"].toBool() });
1002 }
1003
1004 const auto assignees = json["assignees"].toArray();
1005
1006 for (const auto &assignee : assignees)
1007 {
1008 GitServer::User sAssignee;
1009 sAssignee.id = assignee["id"].toInt();
1010 sAssignee.url = assignee["html_url"].toString();
1011 sAssignee.name = assignee["login"].toString();
1012 sAssignee.avatar = assignee["avatar_url"].toString();
1013
1014 issue.assignees.append(sAssignee);
1015 }
1016
1017 if (const auto value = json["milestone"].toString(); !json["milestone"].toObject().isEmpty() && value != "NULL")
1018 {
1019 Milestone sMilestone { json["milestone"].toObject()[QStringLiteral("id")].toInt(),
1020 json["milestone"].toObject()[QStringLiteral("number")].toInt(),
1021 json["milestone"].toObject()[QStringLiteral("node_id")].toString(),
1022 json["milestone"].toObject()[QStringLiteral("title")].toString(),
1023 json["milestone"].toObject()[QStringLiteral("description")].toString(),
1024 json["milestone"].toObject()[QStringLiteral("state")].toString() == "open" };
1025
1026 issue.milestone = sMilestone;
1027 }
1028
1029 return issue;
1030}
1031
1032PullRequest GitHubRestApi::prFromJson(const QJsonObject &json) const
1033{
1034 PullRequest pr;
1035 pr.number = json["number"].toInt();
1036 pr.title = json["title"].toString();
1037 pr.body = json["body"].toString().toUtf8();
1038 pr.url = json["html_url"].toString();
1039 pr.head = json["head"].toObject()["ref"].toString();
1040 pr.headRepo = json["head"].toObject()["repo"].toObject()["full_name"].toString();
1041 pr.headUrl = json["head"].toObject()["repo"].toObject()["clone_url"].toString();
1042 pr.state.sha = json["head"].toObject()["sha"].toString();
1043 pr.base = json["base"].toObject()["ref"].toString();
1044 pr.baseRepo = json["base"].toObject()["repo"].toObject()["full_name"].toString();
1045 pr.isOpen = json["state"].toString() == "open";
1046 pr.draft = json["draft"].toBool();
1047 pr.creation = json["created_at"].toVariant().toDateTime();
1048
1049 pr.creator = { json["user"].toObject()["id"].toInt(), json["user"].toObject()["login"].toString(),
1050 json["user"].toObject()["avatar_url"].toString(), json["user"].toObject()["html_url"].toString(),
1051 json["user"].toObject()["type"].toString() };
1052
1053 const auto labels = json["labels"].toArray();
1054
1055 for (const auto &label : labels)
1056 {
1057 pr.labels.append({ label["id"].toInt(), label["node_id"].toString(), label["url"].toString(),
1058 label["name"].toString(), label["description"].toString(), label["color"].toString(),
1059 label["default"].toBool() });
1060 }
1061
1062 const auto assignees = json["assignees"].toArray();
1063
1064 for (const auto &assignee : assignees)
1065 {
1066 GitServer::User sAssignee;
1067 sAssignee.id = assignee["id"].toInt();
1068 sAssignee.url = assignee["html_url"].toString();
1069 sAssignee.name = assignee["login"].toString();
1070 sAssignee.avatar = assignee["avatar_url"].toString();
1071
1072 pr.assignees.append(sAssignee);
1073 }
1074
1075 Milestone sMilestone { json["milestone"].toObject()[QStringLiteral("id")].toInt(),
1076 json["milestone"].toObject()[QStringLiteral("number")].toInt(),
1077 json["milestone"].toObject()[QStringLiteral("node_id")].toString(),
1078 json["milestone"].toObject()[QStringLiteral("title")].toString(),
1079 json["milestone"].toObject()[QStringLiteral("description")].toString(),
1080 json["milestone"].toObject()[QStringLiteral("state")].toString() == "open" };
1081
1082 pr.milestone = sMilestone;
1083
1084 return pr;
1085}
1086