Gerrit: Support REST query for HTTP servers

Change-Id: Icc164b9d84abe4efc34deaa5d19dca167fdb14e1
Reviewed-by: Tobias Hunger <tobias.hunger@qt.io>
Reviewed-by: André Hartmann <aha_1980@gmx.de>
This commit is contained in:
Orgad Shaneh
2017-02-23 23:03:30 +02:00
committed by Orgad Shaneh
parent be204a125e
commit f7a778690d
10 changed files with 662 additions and 35 deletions

View File

@@ -0,0 +1,142 @@
/****************************************************************************
**
** Copyright (C) 2017 Orgad Shaneh <orgads@gmail.com>.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
#include "authenticationdialog.h"
#include "ui_authenticationdialog.h"
#include "gerritparameters.h"
#include <utils/asconst.h>
#include <utils/fileutils.h>
#include <utils/hostosinfo.h>
#include <QDir>
#include <QFile>
#include <QPushButton>
#include <QRegularExpression>
#include <QTextStream>
namespace Gerrit {
namespace Internal {
static QString findEntry(const QString &line, const QString &type)
{
const QRegularExpression regexp("(?:^|\\s)" + type + "\\s(\\S+)");
const QRegularExpressionMatch match = regexp.match(line);
if (match.hasMatch())
return match.captured(1);
return QString();
}
static bool replaceEntry(QString &line, const QString &type, const QString &value)
{
const QRegularExpression regexp("(?:^|\\s)" + type + "\\s(\\S+)");
const QRegularExpressionMatch match = regexp.match(line);
if (!match.hasMatch())
return false;
line.replace(match.capturedStart(1), match.capturedLength(1), value);
return true;
}
AuthenticationDialog::AuthenticationDialog(GerritServer *server) :
ui(new Ui::AuthenticationDialog),
m_server(server)
{
ui->setupUi(this);
ui->descriptionLabel->setText(ui->descriptionLabel->text().replace(
"LINK_PLACEHOLDER", server->url() + "/#/settings/http-password"));
ui->descriptionLabel->setOpenExternalLinks(true);
ui->serverLineEdit->setText(server->host);
ui->userLineEdit->setText(server->user.userName);
m_netrcFileName = QLatin1String(Utils::HostOsInfo::isWindowsHost() ? "_netrc" : ".netrc");
readExistingConf();
QPushButton *anonymous = ui->buttonBox->addButton(tr("Anonymous"), QDialogButtonBox::AcceptRole);
connect(ui->buttonBox, &QDialogButtonBox::clicked,
this, [this, anonymous](QAbstractButton *button) {
if (button == anonymous)
m_authenticated = false;
});
QPushButton *okButton = ui->buttonBox->button(QDialogButtonBox::Ok);
okButton->setEnabled(false);
connect(ui->passwordLineEdit, &QLineEdit::editingFinished, this, [this, server, okButton] {
setupCredentials();
const int result = server->testConnection();
okButton->setEnabled(result == 200);
});
}
AuthenticationDialog::~AuthenticationDialog()
{
delete ui;
}
void AuthenticationDialog::readExistingConf()
{
QFile netrcFile(QDir::homePath() + '/' + m_netrcFileName);
if (!netrcFile.open(QFile::ReadOnly | QFile::Text))
return;
QTextStream stream(&netrcFile);
QString line;
while (stream.readLineInto(&line)) {
m_allMachines << line;
const QString machine = findEntry(line, "machine");
if (machine == m_server->host) {
const QString login = findEntry(line, "login");
const QString password = findEntry(line, "password");
if (!login.isEmpty())
ui->userLineEdit->setText(login);
if (!password.isEmpty())
ui->passwordLineEdit->setText(password);
}
}
netrcFile.close();
}
bool AuthenticationDialog::setupCredentials()
{
QString netrcContents;
QTextStream out(&netrcContents);
bool found = false;
const QString user = ui->userLineEdit->text().trimmed();
const QString password = ui->passwordLineEdit->text().trimmed();
for (QString &line : m_allMachines) {
const QString machine = findEntry(line, "machine");
if (machine == m_server->host) {
found = true;
replaceEntry(line, "login", user);
replaceEntry(line, "password", password);
}
out << line << endl;
}
if (!found)
out << "machine " << m_server->host << " login " << user << " password " << password;
Utils::FileSaver saver(m_netrcFileName, QFile::WriteOnly | QFile::Truncate | QFile::Text);
saver.write(netrcContents.toUtf8());
return saver.finalize();
}
} // Internal
} // Gerrit

View File

@@ -0,0 +1,61 @@
/****************************************************************************
**
** Copyright (C) 2017 Orgad Shaneh <orgads@gmail.com>.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
#ifndef AUTHENTICATIONDIALOG_H
#define AUTHENTICATIONDIALOG_H
#include <QCoreApplication>
#include <QDialog>
namespace Gerrit {
namespace Internal {
class GerritServer;
namespace Ui { class AuthenticationDialog; }
class AuthenticationDialog : public QDialog
{
Q_DECLARE_TR_FUNCTIONS(Gerrit::Internal::AuthenticationDialog)
public:
AuthenticationDialog(GerritServer *server);
~AuthenticationDialog();
bool isAuthenticated() const { return m_authenticated; }
private:
void readExistingConf();
bool setupCredentials();
Ui::AuthenticationDialog *ui;
GerritServer *m_server;
QString m_netrcFileName;
QStringList m_allMachines;
bool m_authenticated = true;
};
} // Internal
} // Gerrit
#endif // AUTHENTICATIONDIALOG_H

View File

@@ -0,0 +1,127 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Gerrit::Internal::AuthenticationDialog</class>
<widget class="QDialog" name="Gerrit::Internal::AuthenticationDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>334</height>
</rect>
</property>
<property name="windowTitle">
<string>Authentication</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="descriptionLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Gerrit server with HTTP was detected, but you need to setup credentials for it.&lt;/p&gt;&lt;p&gt;To get your password, &lt;a href=&quot;LINK_PLACEHOLDER&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#007af4;&quot;&gt;click here&lt;/span&gt;&lt;/a&gt; (sign in if needed). Click Generate Password if it is blank, and copy the user name and password to this form.&lt;/p&gt;&lt;p&gt;Choose Anonymous if you do not want authentication for this server. On this case, changes that require authentication (like draft changes, or private projects) will not be displayed.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QFormLayout" name="formLayout">
<item row="1" column="0">
<widget class="QLabel" name="userLabel">
<property name="text">
<string>&amp;User:</string>
</property>
<property name="buddy">
<cstring>userLineEdit</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="userLineEdit"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="passwordLabel">
<property name="text">
<string>&amp;Password:</string>
</property>
<property name="buddy">
<cstring>passwordLineEdit</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="passwordLineEdit"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="serverLabel">
<property name="text">
<string>Server:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="serverLineEdit">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Gerrit::Internal::AuthenticationDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Gerrit::Internal::AuthenticationDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -4,7 +4,8 @@ SOURCES += $$PWD/gerritdialog.cpp \
$$PWD/gerritplugin.cpp \
$$PWD/gerritoptionspage.cpp \
$$PWD/gerritpushdialog.cpp \
$$PWD/branchcombobox.cpp
$$PWD/branchcombobox.cpp \
$$PWD/authenticationdialog.cpp
HEADERS += $$PWD/gerritdialog.h \
$$PWD/gerritmodel.h \
@@ -12,8 +13,10 @@ HEADERS += $$PWD/gerritdialog.h \
$$PWD/gerritplugin.h \
$$PWD/gerritoptionspage.h \
$$PWD/gerritpushdialog.h \
$$PWD/branchcombobox.h
$$PWD/branchcombobox.h \
$$PWD/authenticationdialog.h
FORMS += $$PWD/gerritdialog.ui \
$$PWD/gerritpushdialog.ui
$$PWD/gerritpushdialog.ui \
$$PWD/authenticationdialog.ui

View File

@@ -240,12 +240,8 @@ void GerritDialog::updateRemotes()
while (mapIt.hasNext()) {
mapIt.next();
GerritServer server;
if (!server.fillFromRemote(mapIt.value(), m_parameters->server.user.userName))
if (!server.fillFromRemote(mapIt.value(), *m_parameters))
continue;
// Only Ssh is currently supported. In order to extend support for http[s],
// we need to move this logic to the model, and attempt connection to each
// remote (do it only on refresh, not on each path change)
if (server.type == GerritServer::Ssh)
addRemote(server, mapIt.key());
}
addRemote(m_parameters->server, tr("Fallback"));
@@ -257,7 +253,7 @@ void GerritDialog::addRemote(const GerritServer &server, const QString &name)
{
for (int i = 0, total = m_ui->remoteComboBox->count(); i < total; ++i) {
const GerritServer s = m_ui->remoteComboBox->itemData(i).value<GerritServer>();
if (s.host == server.host)
if (s == server)
return;
}
m_ui->remoteComboBox->addItem(server.host + QString(" (%1)").arg(name),

View File

@@ -50,6 +50,7 @@
#include <QTimer>
#include <QApplication>
#include <QFutureWatcher>
#include <QUrl>
enum { debug = 0 };
@@ -199,7 +200,9 @@ QString GerritChange::filterString() const
QStringList GerritChange::gitFetchArguments(const GerritServer &server) const
{
return {"fetch", server.url() + '/' + project, currentPatchSet.ref};
const QString url = currentPatchSet.url.isEmpty() ? server.url(true) + '/' + project
: currentPatchSet.url;
return {"fetch", url, currentPatchSet.ref};
}
QString GerritChange::fullTitle() const
@@ -258,13 +261,21 @@ QueryContext::QueryContext(const QString &query,
QObject *parent)
: QObject(parent)
{
if (server.type == GerritServer::Ssh) {
m_binary = p->ssh;
if (server.port)
m_arguments << p->portFlag << QString::number(server.port);
m_arguments << server.sshHostArgument() << "gerrit"
m_arguments << server.hostArgument() << "gerrit"
<< "query" << "--dependencies"
<< "--current-patch-set"
<< "--format=JSON" << query;
} else {
m_binary = p->curl;
const QString url = server.restUrl() + "/changes/?q="
+ QString::fromUtf8(QUrl::toPercentEncoding(query))
+ "&o=CURRENT_REVISION&o=DETAILED_LABELS&o=DETAILED_ACCOUNTS";
m_arguments = GerritServer::curlArguments() << url;
}
connect(&m_process, &QProcess::readyReadStandardError,
this, &QueryContext::readyReadStandardError);
connect(&m_process, &QProcess::readyReadStandardOutput,
@@ -615,18 +626,178 @@ static GerritChangePtr parseSshOutput(const QJsonObject &object)
return change;
}
/*
{
"kind": "gerritcodereview#change",
"id": "qt-creator%2Fqt-creator~master~Icc164b9d84abe4efc34deaa5d19dca167fdb14e1",
"project": "qt-creator/qt-creator",
"branch": "master",
"change_id": "Icc164b9d84abe4efc34deaa5d19dca167fdb14e1",
"subject": "WIP: Gerrit: Support REST query for HTTP servers",
"status": "NEW",
"created": "2017-02-22 21:23:39.403000000",
"updated": "2017-02-23 21:03:51.055000000",
"reviewed": true,
"mergeable": false,
"_sortkey": "004368cf0002d84f",
"_number": 186447,
"owner": {
"_account_id": 1000534,
"name": "Orgad Shaneh",
"email": "orgads@gmail.com"
},
"labels": {
"Code-Review": {
"all": [
{
"value": 0,
"_account_id": 1000009,
"name": "Tobias Hunger",
"email": "tobias.hunger@qt.io"
},
{
"value": 0,
"_account_id": 1000528,
"name": "André Hartmann",
"email": "aha_1980@gmx.de"
},
{
"value": 0,
"_account_id": 1000049,
"name": "Qt Sanity Bot",
"email": "qt_sanitybot@qt-project.org"
}
],
"values": {
"-2": "This shall not be merged",
"-1": "I would prefer this is not merged as is",
" 0": "No score",
"+1": "Looks good to me, but someone else must approve",
"+2": "Looks good to me, approved"
}
},
"Sanity-Review": {
"all": [
{
"value": 0,
"_account_id": 1000009,
"name": "Tobias Hunger",
"email": "tobias.hunger@qt.io"
},
{
"value": 0,
"_account_id": 1000528,
"name": "André Hartmann",
"email": "aha_1980@gmx.de"
},
{
"value": 1,
"_account_id": 1000049,
"name": "Qt Sanity Bot",
"email": "qt_sanitybot@qt-project.org"
}
],
"values": {
"-2": "Major sanity problems found",
"-1": "Sanity problems found",
" 0": "No sanity review",
"+1": "Sanity review passed"
}
}
},
"permitted_labels": {
"Code-Review": [
"-2",
"-1",
" 0",
"+1",
"+2"
],
"Sanity-Review": [
"-2",
"-1",
" 0",
"+1"
]
},
"current_revision": "87916545e2974913d56f56c9f06fc3822a876aca",
"revisions": {
"87916545e2974913d56f56c9f06fc3822a876aca": {
"draft": true,
"_number": 2,
"fetch": {
"http": {
"url": "https://codereview.qt-project.org/qt-creator/qt-creator",
"ref": "refs/changes/47/186447/2"
},
"ssh": {
"url": "ssh:// *:29418/qt-creator/qt-creator",
"ref": "refs/changes/47/186447/2"
}
}
}
}
}
*/
static GerritChangePtr parseRestOutput(const QJsonObject &object, const GerritServer &server)
{
GerritChangePtr change(new GerritChange);
change->number = object.value("_number").toInt();
change->url = QString("%1/%2").arg(server.url()).arg(change->number);
change->title = object.value("subject").toString();
change->owner = parseGerritUser(object.value("owner").toObject());
change->project = object.value("project").toString();
change->branch = object.value("branch").toString();
change->status = object.value("status").toString();
change->lastUpdated = QDateTime::fromString(object.value("updated").toString(),
Qt::DateFormat::ISODate);
// Read current patch set.
const QJsonObject patchSet = object.value("revisions").toObject().begin().value().toObject();
change->currentPatchSet.patchSetNumber = qMax(1, patchSet.value("number").toString().toInt());
change->currentPatchSet.ref = patchSet.value("ref").toString();
// Replace * in ssh://*:29418/qt-creator/qt-creator with the hostname
change->currentPatchSet.url = patchSet.value("url").toString().replace('*', server.host);
const QJsonObject labels = object.value("labels").toObject();
for (auto it = labels.constBegin(), end = labels.constEnd(); it != end; ++it) {
const QJsonArray all = it.value().toObject().value("all").toArray();
for (const QJsonValue &av : all) {
const QJsonObject ao = av.toObject();
const int value = ao.value("value").toInt();
if (!value)
continue;
GerritApproval approval;
approval.reviewer = parseGerritUser(ao);
approval.approval = value;
approval.type = it.key();
change->currentPatchSet.approvals.push_back(approval);
}
}
std::stable_sort(change->currentPatchSet.approvals.begin(),
change->currentPatchSet.approvals.end(),
gerritApprovalLessThan);
return change;
}
static bool parseOutput(const QSharedPointer<GerritParameters> &parameters,
const GerritServer &server,
const QByteArray &output,
QList<GerritChangePtr> &result)
{
QByteArray adaptedOutput;
if (server.type == GerritServer::Ssh) {
// The output consists of separate lines containing a document each
// Add a comma after each line (except the last), and enclose it as an array
QByteArray adaptedOutput = '[' + output + ']';
adaptedOutput = '[' + output + ']';
adaptedOutput.replace('\n', ',');
const int lastComma = adaptedOutput.lastIndexOf(',');
if (lastComma >= 0)
adaptedOutput[lastComma] = '\n';
} else {
adaptedOutput = output;
// Strip first line, which is )]}'
adaptedOutput.remove(0, adaptedOutput.indexOf("\n"));
}
bool res = true;
QJsonParseError error;
@@ -647,7 +818,9 @@ static bool parseOutput(const QSharedPointer<GerritParameters> &parameters,
// Skip stats line: {"type":"stats","rowCount":9,"runTimeMilliseconds":13}
if (object.contains("type"))
continue;
GerritChangePtr change = parseSshOutput(object);
GerritChangePtr change =
(server.type == GerritServer::Ssh ? parseSshOutput(object)
: parseRestOutput(object, server));
if (change->isValid()) {
if (change->url.isEmpty()) // No "canonicalWebUrl" is in gerrit.config.
change->url = defaultUrl(parameters, server, change->number);

View File

@@ -57,6 +57,7 @@ public:
bool hasApproval(const GerritUser &user) const;
int approvalLevel() const;
QString url;
QString ref;
int patchSetNumber;
QList<GerritApproval> approvals;

View File

@@ -25,6 +25,10 @@
#include "gerritparameters.h"
#include "gerritplugin.h"
#include "authenticationdialog.h"
#include "../gitplugin.h"
#include "../gitclient.h"
#include <coreplugin/shellcommand.h>
#include <utils/hostosinfo.h>
#include <utils/pathchooser.h>
@@ -32,6 +36,7 @@
#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QJsonDocument>
#include <QRegularExpression>
#include <QSettings>
#include <QStandardPaths>
@@ -51,6 +56,7 @@ static const char curlKeyC[] = "Curl";
static const char httpsKeyC[] = "Https";
static const char defaultHostC[] = "codereview.qt-project.org";
static const char savedQueriesKeyC[] = "SavedQueries";
static const char accountUrlC[] = "/accounts/self";
static const char defaultPortFlag[] = "-p";
@@ -141,23 +147,44 @@ GerritParameters::GerritParameters()
{
}
QString GerritServer::sshHostArgument() const
QString GerritServer::hostArgument() const
{
return user.userName.isEmpty() ? host : (user.userName + '@' + host);
}
QString GerritServer::url() const
QString GerritServer::url(bool withHttpUser) const
{
QString res = "ssh://" + sshHostArgument();
QString protocol;
switch (type) {
case Ssh: protocol = "ssh"; break;
case Http: protocol = "http"; break;
case Https: protocol = "https"; break;
}
QString res = protocol + "://";
if (type == Ssh || withHttpUser)
res += hostArgument();
else
res += host;
if (port)
res += ':' + QString::number(port);
if (type != Ssh)
res += rootPath;
return res;
}
bool GerritServer::fillFromRemote(const QString &remote, const QString &defaultUser)
QString GerritServer::restUrl() const
{
QString res = url(true);
if (type != Ssh && authenticated)
res += "/a";
return res;
}
bool GerritServer::fillFromRemote(const QString &remote, const GerritParameters &parameters)
{
static const QRegularExpression remotePattern(
"^(?:(?<protocol>[^:]+)://)?(?:(?<user>[^@]+)@)?(?<host>[^:/]+)(?::(?<port>\\d+))?");
"^(?:(?<protocol>[^:]+)://)?(?:(?<user>[^@]+)@)?(?<host>[^:/]+)"
"(?::(?<port>\\d+))?:?(?<path>/.*)$");
// Skip local remotes (refer to the root or relative path)
if (remote.isEmpty() || remote.startsWith('/') || remote.startsWith('.'))
@@ -178,14 +205,95 @@ bool GerritServer::fillFromRemote(const QString &remote, const QString &defaultU
else
return false;
const QString userName = match.captured("user");
user.userName = userName.isEmpty() ? defaultUser : userName;
user.userName = userName.isEmpty() ? parameters.server.user.userName : userName;
host = match.captured("host");
port = match.captured("port").toUShort();
if (host.contains("github.com")) // Clearly not gerrit
return false;
if (type != GerritServer::Ssh) {
curlBinary = parameters.curl;
if (curlBinary.isEmpty() || !QFile::exists(curlBinary))
return false;
rootPath = match.captured("path");
// Strip the last part of the path, which is always the repo name
// The rest of the path needs to be inspected to find the root path
// (can be http://example.net/review)
ascendPath();
if (!resolveRoot())
return false;
}
return true;
}
QStringList GerritServer::curlArguments()
{
// -k - insecure - do not validate certificate
// -f - fail silently on server error
// -n - use credentials from ~/.netrc (or ~/_netrc on Windows)
// -sS - silent, except server error (no progress)
// --basic, --digest - try both authentication types
return {"-kfnsS", "--basic", "--digest"};
}
int GerritServer::testConnection()
{
static Git::Internal::GitClient *const client = Git::Internal::GitPlugin::client();
const QStringList arguments = curlArguments() << (restUrl() + accountUrlC);
const SynchronousProcessResponse resp = client->vcsFullySynchronousExec(
QString(), FileName::fromString(curlBinary), arguments,
Core::ShellCommand::NoOutput);
if (resp.result == SynchronousProcessResponse::Finished) {
QString output = resp.stdOut();
output.remove(0, output.indexOf('\n')); // Strip first line
QJsonDocument doc = QJsonDocument::fromJson(output.toUtf8());
if (!doc.isNull())
user.fullName = doc.object().value("name").toString();
return 200;
}
const QRegularExpression errorRegexp("returned error: (\\d+)");
QRegularExpressionMatch match = errorRegexp.match(resp.stdErr());
if (match.hasMatch())
return match.captured(1).toInt();
return 400;
}
bool GerritServer::setupAuthentication()
{
AuthenticationDialog dialog(this);
if (!dialog.exec())
return false;
authenticated = dialog.isAuthenticated();
return true;
}
bool GerritServer::ascendPath()
{
const int lastSlash = rootPath.lastIndexOf('/');
if (lastSlash == -1)
return false;
rootPath = rootPath.left(lastSlash);
return true;
}
bool GerritServer::resolveRoot()
{
for (;;) {
switch (testConnection()) {
case 200:
return true;
case 401:
return setupAuthentication();
case 404:
if (!ascendPath())
return false;
break;
default: // unknown error - fail
return false;
}
}
return false;
}
bool GerritParameters::equals(const GerritParameters &rhs) const
{
return server == rhs.server && ssh == rhs.ssh && curl == rhs.curl && https == rhs.https;

View File

@@ -32,6 +32,8 @@ QT_FORWARD_DECLARE_CLASS(QSettings)
namespace Gerrit {
namespace Internal {
class GerritParameters;
class GerritUser
{
public:
@@ -55,14 +57,25 @@ public:
GerritServer();
GerritServer(const QString &host, unsigned short port, const QString &userName, HostType type);
bool operator==(const GerritServer &other) const;
QString sshHostArgument() const;
QString url() const;
bool fillFromRemote(const QString &remote, const QString &defaultUser);
QString hostArgument() const;
QString url(bool withHttpUser = false) const;
QString restUrl() const;
bool fillFromRemote(const QString &remote, const GerritParameters &parameters);
int testConnection();
static QStringList curlArguments();
QString host;
GerritUser user;
QString rootPath; // for http
unsigned short port = 0;
HostType type = Ssh;
bool authenticated = true;
private:
QString curlBinary;
bool setupAuthentication();
bool ascendPath();
bool resolveRoot();
};
class GerritParameters

View File

@@ -77,6 +77,9 @@ QtcPlugin {
name: "Gerrit"
prefix: "gerrit/"
files: [
"authenticationdialog.cpp",
"authenticationdialog.h",
"authenticationdialog.ui",
"branchcombobox.cpp",
"branchcombobox.h",
"gerritdialog.cpp",