mirror of
https://github.com/Murmele/Gittyup.git
synced 2024-10-05 14:37:31 +03:00
Add Gitea Support
This is a rough, initial attempt at adding Gitea support. Some features have been tested similarly to GitHub. Signed-off-by: Odin Vex <44311901+OdinVex@users.noreply.github.com>
This commit is contained in:
parent
ff418c49eb
commit
8fd8644bd3
BIN
rsrc/gitea.png
Normal file
BIN
rsrc/gitea.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
@ -29,6 +29,7 @@
|
||||
<file>general@2x.png</file>
|
||||
<file>github.png</file>
|
||||
<file>github_dark.png</file>
|
||||
<file>gitea.png</file>
|
||||
<file>gitlab.png</file>
|
||||
<file>hotkeys.png</file>
|
||||
<file>hotkeys@2x.png</file>
|
||||
|
@ -24,6 +24,7 @@ AccountDialog::AccountDialog(Account *account, QWidget *parent)
|
||||
mHost = new QComboBox(this);
|
||||
mHost->setMinimumWidth(mHost->sizeHint().width() * 2);
|
||||
mHost->addItem("GitHub", Account::GitHub);
|
||||
mHost->addItem("Gitea", Account::Gitea);
|
||||
mHost->addItem("Bitbucket", Account::Bitbucket);
|
||||
mHost->addItem("Beanstalk", Account::Beanstalk);
|
||||
mHost->addItem("GitLab", Account::GitLab);
|
||||
|
@ -11,6 +11,7 @@
|
||||
#include "Beanstalk.h"
|
||||
#include "Bitbucket.h"
|
||||
#include "GitHub.h"
|
||||
#include "Gitea.h"
|
||||
#include "GitLab.h"
|
||||
#include "cred/CredentialHelper.h"
|
||||
#include <QFileInfo>
|
||||
@ -136,6 +137,9 @@ QIcon Account::icon(Kind kind) {
|
||||
case Account::GitHub:
|
||||
name = "github";
|
||||
break;
|
||||
case Account::Gitea:
|
||||
name = "gitea";
|
||||
break;
|
||||
case Account::Bitbucket:
|
||||
name = "bitbucket";
|
||||
break;
|
||||
@ -184,6 +188,10 @@ QString Account::helpText(Kind kind) {
|
||||
"command-line/'>personal access token</a> in the password field "
|
||||
"instead.");
|
||||
|
||||
case Gitea:
|
||||
return tr(
|
||||
"<b>Note:</b> Only Basic authentication is currently supported ");
|
||||
|
||||
case GitLab:
|
||||
return tr("<b>Note:</b> Basic authentication is not supported. Use a "
|
||||
"<a href='https://docs.gitlab.com/ee/user/profile/personal_"
|
||||
@ -200,6 +208,8 @@ QString Account::defaultUrl(Kind kind) {
|
||||
switch (kind) {
|
||||
case GitHub:
|
||||
return GitHub::defaultUrl();
|
||||
case Gitea:
|
||||
return Gitea::defaultUrl();
|
||||
case Bitbucket:
|
||||
return Bitbucket::defaultUrl();
|
||||
case Beanstalk:
|
||||
|
@ -25,7 +25,7 @@ class Account : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum Kind { GitHub, Bitbucket, Beanstalk, GitLab };
|
||||
enum Kind { GitHub, Gitea, Bitbucket, Beanstalk, GitLab };
|
||||
|
||||
struct Comment {
|
||||
QString body;
|
||||
|
@ -11,6 +11,7 @@
|
||||
#include "Beanstalk.h"
|
||||
#include "Bitbucket.h"
|
||||
#include "GitHub.h"
|
||||
#include "Gitea.h"
|
||||
#include "GitLab.h"
|
||||
#include <QCoreApplication>
|
||||
#include <QSettings>
|
||||
@ -60,6 +61,9 @@ Account *Accounts::createAccount(Account::Kind kind, const QString &username,
|
||||
case Account::GitHub:
|
||||
account = new GitHub(username);
|
||||
break;
|
||||
case Account::Gitea:
|
||||
account = new Gitea(username);
|
||||
break;
|
||||
case Account::Bitbucket:
|
||||
account = new Bitbucket(username);
|
||||
break;
|
||||
|
@ -5,6 +5,7 @@ add_library(
|
||||
Beanstalk.cpp
|
||||
Bitbucket.cpp
|
||||
GitHub.cpp
|
||||
Gitea.cpp
|
||||
GitLab.cpp
|
||||
Repository.cpp)
|
||||
|
||||
@ -12,6 +13,8 @@ target_link_libraries(host conf cred Qt5::Core Qt5::Gui Qt5::Network)
|
||||
|
||||
target_compile_definitions(
|
||||
host PRIVATE GITHUB_CLIENT_ID="${GITHUB_CLIENT_ID}"
|
||||
GITHUB_CLIENT_SECRET="${GITHUB_CLIENT_SECRET}")
|
||||
GITHUB_CLIENT_SECRET="${GITHUB_CLIENT_SECRET}"
|
||||
GITEA_CLIENT_ID="${GITEA_CLIENT_ID}"
|
||||
GITEA_CLIENT_SECRET="${GITEA_CLIENT_SECRET}")
|
||||
|
||||
set_target_properties(host PROPERTIES AUTOMOC ON)
|
||||
|
321
src/host/Gitea.cpp
Normal file
321
src/host/Gitea.cpp
Normal file
@ -0,0 +1,321 @@
|
||||
//
|
||||
// Copyright (c) 2016, Scientific Toolworks, Inc.
|
||||
//
|
||||
// This software is licensed under the MIT License. The LICENSE.md file
|
||||
// describes the conditions under which this software may be distributed.
|
||||
//
|
||||
// Author: Jason Haslam
|
||||
//
|
||||
|
||||
#include "Gitea.h"
|
||||
#include "Repository.h"
|
||||
#include <QCoreApplication>
|
||||
#include <QDesktopServices>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QRandomGenerator>
|
||||
#include <QRegularExpression>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
|
||||
namespace {
|
||||
|
||||
const char *kPasswordProperty = "password";
|
||||
|
||||
const QString kScope = "repo";
|
||||
|
||||
const QString kAuthUrl =
|
||||
QStringLiteral("https://try.gitea.io/login/oauth/authorize");
|
||||
const QString kAccessUrl =
|
||||
QStringLiteral("https://try.gitea.io/login/oauth/access_token");
|
||||
const QString kGraphQlUrl = QStringLiteral("https://try.gitea.io/graphql");
|
||||
|
||||
} // namespace
|
||||
|
||||
Gitea::Gitea(const QString &username) : Account(username) {
|
||||
QObject::connect(
|
||||
mMgr, &QNetworkAccessManager::finished, this,
|
||||
[this](QNetworkReply *reply) {
|
||||
QString password = reply->property(kPasswordProperty).toString();
|
||||
if (password.isEmpty())
|
||||
return;
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
setErrorReply(*reply);
|
||||
mProgress->finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle repositories.
|
||||
QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
|
||||
QJsonArray array = doc.array();
|
||||
for (int i = 0; i < array.size(); ++i) {
|
||||
QJsonObject obj = array.at(i).toObject();
|
||||
|
||||
// Add username to HTTPS URL.
|
||||
QUrl httpsUrl(obj.value("clone_url").toString());
|
||||
httpsUrl.setUserName(this->username());
|
||||
|
||||
QString name = obj.value("name").toString();
|
||||
QString fullName = obj.value("full_name").toString();
|
||||
Repository *repo = addRepository(name, fullName);
|
||||
repo->setUrl(Repository::Https, httpsUrl.toString());
|
||||
repo->setUrl(Repository::Ssh, obj.value("ssh_url").toString());
|
||||
}
|
||||
|
||||
// Check for additional pages.
|
||||
QString link = reply->rawHeader("Link");
|
||||
if (link.isEmpty()) {
|
||||
mProgress->finish();
|
||||
return;
|
||||
}
|
||||
|
||||
QMap<QString, QString> map;
|
||||
QRegularExpression re("<(.*)>; rel=\"(\\w+)\"");
|
||||
foreach (const QString &record, link.split(", ")) {
|
||||
QRegularExpressionMatch match = re.match(record);
|
||||
if (match.isValid() && match.hasMatch())
|
||||
map.insert(match.captured(2), match.captured(1));
|
||||
}
|
||||
|
||||
QString next = map.value("next");
|
||||
if (next.isEmpty()) {
|
||||
mProgress->finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Request next page.
|
||||
QNetworkRequest request(next);
|
||||
if (setHeaders(request, password)) {
|
||||
QNetworkReply *reply = mMgr->get(request);
|
||||
reply->setProperty(kPasswordProperty, password);
|
||||
startProgress();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Account::Kind Gitea::kind() const { return Account::Gitea; }
|
||||
|
||||
QString Gitea::name() const { return QStringLiteral("Gitea"); }
|
||||
|
||||
QString Gitea::host() const { return QStringLiteral("try.gitea.io"); }
|
||||
|
||||
void Gitea::connect(const QString &password) {
|
||||
clearRepos();
|
||||
|
||||
QString suffix = hasCustomUrl() ? "/api/v1" : QString();
|
||||
QNetworkRequest request(url() + suffix + "/user/repos");
|
||||
if (setHeaders(request, password)) {
|
||||
QNetworkReply *reply = mMgr->get(request);
|
||||
reply->setProperty(kPasswordProperty,
|
||||
!password.isEmpty() ? password : this->password());
|
||||
startProgress();
|
||||
}
|
||||
}
|
||||
|
||||
void Gitea::requestForkParents(Repository *repo) {
|
||||
QString query = QString("query {"
|
||||
" repository(owner:\"%1\", name:\"%2\") {"
|
||||
" isFork"
|
||||
" parent {"
|
||||
" isFork"
|
||||
" nameWithOwner"
|
||||
" defaultBranchRef {"
|
||||
" name"
|
||||
" }"
|
||||
" parent {"
|
||||
" isFork"
|
||||
" nameWithOwner"
|
||||
" defaultBranchRef {"
|
||||
" name"
|
||||
" }"
|
||||
" parent{"
|
||||
" isFork"
|
||||
" nameWithOwner"
|
||||
" defaultBranchRef {"
|
||||
" name"
|
||||
" }"
|
||||
" }"
|
||||
" }"
|
||||
" }"
|
||||
" }"
|
||||
"}")
|
||||
.arg(repo->owner(), repo->name());
|
||||
|
||||
graphql(query, [this](const QJsonObject &data) {
|
||||
QMap<QString, QString> map;
|
||||
QJsonObject repository = data.value("repository").toObject();
|
||||
while (repository.value("isFork").toBool()) {
|
||||
repository = repository.value("parent").toObject();
|
||||
|
||||
QString nameWithOwner = repository.value("nameWithOwner").toString();
|
||||
QString branch = repository.value("defaultBranchRef")
|
||||
.toObject()
|
||||
.value("name")
|
||||
.toString();
|
||||
|
||||
map.insert(nameWithOwner, branch);
|
||||
}
|
||||
|
||||
emit forkParentsReady(map);
|
||||
});
|
||||
}
|
||||
|
||||
void Gitea::createPullRequest(Repository *repo, const QString &ownerRepo,
|
||||
const QString &title, const QString &body,
|
||||
const QString &head, const QString &base,
|
||||
bool canModify) {
|
||||
QJsonDocument doc;
|
||||
doc.setObject({{"title", title},
|
||||
{"body", body},
|
||||
{"head", QString("%1:%2").arg(repo->owner(), head)},
|
||||
{"base", base},
|
||||
{"maintainer_can_modify", canModify}});
|
||||
|
||||
QUrl url(QString("https://try.gitea.io/repos/%1/pulls").arg(ownerRepo));
|
||||
rest(url, doc, [this, title](const QJsonObject &obj) {
|
||||
foreach (const QJsonValue &error, obj.value("errors").toArray())
|
||||
emit pullRequestError(title,
|
||||
error.toObject().value("message").toString());
|
||||
});
|
||||
}
|
||||
|
||||
void Gitea::requestComments(Repository *repo, const QString &oid) {
|
||||
QString query = QString("query {"
|
||||
" repository(owner: \"%1\", name: \"%2\") {"
|
||||
" object(oid: \"%3\") {"
|
||||
" ... on Commit {"
|
||||
" comments(first: 50) {"
|
||||
" nodes {"
|
||||
" path"
|
||||
" position"
|
||||
" publishedAt"
|
||||
" body"
|
||||
" author {"
|
||||
" login"
|
||||
" }"
|
||||
" }"
|
||||
" }"
|
||||
" }"
|
||||
" }"
|
||||
" }"
|
||||
"}")
|
||||
.arg(repo->owner(), repo->name(), oid);
|
||||
|
||||
graphql(query, [this, repo, oid](const QJsonObject &data) {
|
||||
QJsonArray nodes = data.value("repository")
|
||||
.toObject()
|
||||
.value("object")
|
||||
.toObject()
|
||||
.value("comments")
|
||||
.toObject()
|
||||
.value("nodes")
|
||||
.toArray();
|
||||
|
||||
if (nodes.isEmpty())
|
||||
return;
|
||||
|
||||
CommitComments comments;
|
||||
foreach (const QJsonValue &value, nodes) {
|
||||
QJsonObject obj = value.toObject();
|
||||
QString path = obj.value("path").toString();
|
||||
int position = obj.value("position").toInt() - 1;
|
||||
|
||||
QString raw = obj.value("body").toString();
|
||||
QString body = raw.trimmed().replace("\r\n", "\n");
|
||||
|
||||
QJsonObject author = obj.value("author").toObject();
|
||||
QString login = author.value("login").toString();
|
||||
|
||||
QString published = obj["publishedAt"].toString();
|
||||
QDateTime date = QDateTime::fromString(published, Qt::ISODate);
|
||||
|
||||
Comments &map =
|
||||
path.isEmpty() ? comments.comments : comments.files[path][position];
|
||||
map.insert(date, {body, login});
|
||||
}
|
||||
|
||||
emit commentsReady(repo, oid, comments);
|
||||
});
|
||||
}
|
||||
|
||||
void Gitea::authorize() {
|
||||
mState = QString();
|
||||
for (int i = 0; i < 32; i++) {
|
||||
int value = QRandomGenerator::global()->bounded('a', 'z' + 1);
|
||||
mState.append(QChar::fromLatin1(value));
|
||||
}
|
||||
|
||||
QUrlQuery query;
|
||||
query.addQueryItem("client_id", GITEA_CLIENT_ID);
|
||||
query.addQueryItem("scope", kScope);
|
||||
query.addQueryItem("state", mState);
|
||||
|
||||
QUrl url(kAuthUrl);
|
||||
url.setQuery(query);
|
||||
|
||||
// Open in default browser.
|
||||
QDesktopServices::openUrl(url);
|
||||
}
|
||||
|
||||
bool Gitea::isAuthorizeSupported() {
|
||||
QByteArray id(GITEA_CLIENT_ID);
|
||||
QByteArray secret(GITEA_CLIENT_SECRET);
|
||||
QByteArray env = qgetenv("GITTYUP_OAUTH");
|
||||
return (!id.isEmpty() && !secret.isEmpty() && !env.isEmpty());
|
||||
}
|
||||
|
||||
QString Gitea::defaultUrl() {
|
||||
return QStringLiteral("https://try.gitea.io");
|
||||
}
|
||||
|
||||
void Gitea::graphql(const QString &query, const Callback &callback) {
|
||||
if (mAccessToken.isEmpty())
|
||||
return;
|
||||
|
||||
QJsonDocument doc;
|
||||
doc.setObject({{"query", query}});
|
||||
|
||||
QNetworkRequest request(kGraphQlUrl);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setRawHeader("Authorization",
|
||||
QString("bearer %1").arg(mAccessToken).toUtf8());
|
||||
|
||||
QNetworkReply *reply = mMgr->post(request, doc.toJson());
|
||||
QObject::connect(reply, &QNetworkReply::finished, [reply, callback] {
|
||||
QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
|
||||
callback(doc.object().value("data").toObject());
|
||||
reply->deleteLater();
|
||||
});
|
||||
}
|
||||
|
||||
void Gitea::rest(const QUrl &url, const QJsonDocument &doc,
|
||||
const Callback &callback) {
|
||||
if (mAccessToken.isEmpty())
|
||||
return;
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setRawHeader("Authorization",
|
||||
QString("token %1").arg(mAccessToken).toUtf8());
|
||||
|
||||
QNetworkReply *reply;
|
||||
if (doc.isEmpty()) {
|
||||
reply = mMgr->get(request);
|
||||
} else {
|
||||
reply = mMgr->post(request, doc.toJson());
|
||||
}
|
||||
|
||||
QObject::connect(reply, &QNetworkReply::finished, [reply, callback] {
|
||||
QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
|
||||
callback(doc.object());
|
||||
reply->deleteLater();
|
||||
});
|
||||
}
|
51
src/host/Gitea.h
Normal file
51
src/host/Gitea.h
Normal file
@ -0,0 +1,51 @@
|
||||
//
|
||||
// Copyright (c) 2016, Scientific Toolworks, Inc.
|
||||
//
|
||||
// This software is licensed under the MIT License. The LICENSE.md file
|
||||
// describes the conditions under which this software may be distributed.
|
||||
//
|
||||
// Author: Jason Haslam
|
||||
//
|
||||
|
||||
#ifndef GITEA_H
|
||||
#define GITEA_H
|
||||
|
||||
#include "Account.h"
|
||||
#include <QJsonDocument>
|
||||
|
||||
class Gitea : public Account {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Gitea(const QString &username);
|
||||
|
||||
Kind kind() const override;
|
||||
QString name() const override;
|
||||
QString host() const override;
|
||||
void connect(const QString &password = QString()) override;
|
||||
|
||||
void requestForkParents(Repository *repo) override;
|
||||
virtual void createPullRequest(Repository *repo, const QString &ownerRepo,
|
||||
const QString &title, const QString &body,
|
||||
const QString &head, const QString &base,
|
||||
bool canModify) override;
|
||||
|
||||
void requestComments(Repository *repo, const QString &oid) override;
|
||||
|
||||
void authorize() override;
|
||||
bool isAuthorizeSupported() override;
|
||||
|
||||
static QString defaultUrl();
|
||||
|
||||
private:
|
||||
using Callback = std::function<void(const QJsonObject &)>;
|
||||
|
||||
void graphql(const QString &query, const Callback &callback);
|
||||
|
||||
void rest(const QUrl &url, const QJsonDocument &doc = QJsonDocument(),
|
||||
const Callback &callback = Callback());
|
||||
|
||||
QString mState;
|
||||
};
|
||||
|
||||
#endif
|
Loading…
Reference in New Issue
Block a user