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
13using namespace GitServer;
14
15GitLabRestApi::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
43void 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
72void 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
105void GitLabRestApi::updateIssue(int, const Issue &) { }
106
107void 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
141void 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
148void 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
155void 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
169void GitLabRestApi::requestPullRequests(int) { }
170
171void 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
194QNetworkRequest 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
207void 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
222void 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
251void 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
259void 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
292void 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
323void 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
353void 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
369void 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
384Issue 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
432PullRequest 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