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 | |
15 | using namespace QLogger; |
16 | using namespace GitServer; |
17 | |
18 | GitHubRestApi::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 | |
36 | void 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 | |
54 | void 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 | |
66 | void 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 | |
91 | void 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 | |
115 | void 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 | |
139 | void GitHubRestApi::requestLabels() |
140 | { |
141 | const auto reply = mManager->get(createRequest(mRepoEndpoint + "/labels" )); |
142 | |
143 | connect(reply, &QNetworkReply::finished, this, &GitHubRestApi::onLabelsReceived); |
144 | } |
145 | |
146 | void GitHubRestApi::requestMilestones() |
147 | { |
148 | const auto reply = mManager->get(createRequest(mRepoEndpoint + "/milestones" )); |
149 | |
150 | connect(reply, &QNetworkReply::finished, this, &GitHubRestApi::onMilestonesReceived); |
151 | } |
152 | |
153 | void 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 | |
175 | void 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 | |
197 | void 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 | |
204 | void GitHubRestApi::(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 | |
211 | void 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 | |
218 | void 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 | |
226 | void GitHubRestApi::(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 = 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 | |
272 | void 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 = 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 | |
317 | void 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 = 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 | |
383 | void GitHubRestApi::replyCodeReview(int prNumber, int , 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 = 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 | |
448 | QNetworkRequest 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 | |
461 | void 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 | |
492 | void 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 | |
522 | void 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 | |
537 | void 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 | |
565 | void 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 | |
577 | void GitHubRestApi::onPullRequestReceived() |
578 | { |
579 | const auto reply = qobject_cast<QNetworkReply *>(sender()); |
580 | |
581 | if (const auto = 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 | |
640 | void 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 | |
679 | void GitHubRestApi::onIssuesReceived() |
680 | { |
681 | const auto reply = qobject_cast<QNetworkReply *>(sender()); |
682 | |
683 | if (const auto = 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 | |
731 | void GitHubRestApi::(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> ; |
740 | const auto = tmpDoc.array(); |
741 | |
742 | for (const auto & : 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 | |
765 | void 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 | |
790 | void 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 = tmpDoc.array(); |
800 | |
801 | for (const auto & : 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 | |
829 | void GitHubRestApi::(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 | |
836 | void GitHubRestApi::(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> ; |
845 | const auto = tmpDoc.array(); |
846 | |
847 | for (const auto & : 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 | |
891 | void 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 | |
980 | Issue 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 | |
1032 | PullRequest 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 | |