From f7a778690d085cf1c93d1a060379aa6f40136062 Mon Sep 17 00:00:00 2001 From: Orgad Shaneh Date: Thu, 23 Feb 2017 23:03:30 +0200 Subject: [PATCH] Gerrit: Support REST query for HTTP servers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change-Id: Icc164b9d84abe4efc34deaa5d19dca167fdb14e1 Reviewed-by: Tobias Hunger Reviewed-by: André Hartmann --- .../git/gerrit/authenticationdialog.cpp | 142 ++++++++++++ src/plugins/git/gerrit/authenticationdialog.h | 61 ++++++ .../git/gerrit/authenticationdialog.ui | 127 +++++++++++ src/plugins/git/gerrit/gerrit.pri | 9 +- src/plugins/git/gerrit/gerritdialog.cpp | 10 +- src/plugins/git/gerrit/gerritmodel.cpp | 205 ++++++++++++++++-- src/plugins/git/gerrit/gerritmodel.h | 1 + src/plugins/git/gerrit/gerritparameters.cpp | 120 +++++++++- src/plugins/git/gerrit/gerritparameters.h | 19 +- src/plugins/git/git.qbs | 3 + 10 files changed, 662 insertions(+), 35 deletions(-) create mode 100644 src/plugins/git/gerrit/authenticationdialog.cpp create mode 100644 src/plugins/git/gerrit/authenticationdialog.h create mode 100644 src/plugins/git/gerrit/authenticationdialog.ui diff --git a/src/plugins/git/gerrit/authenticationdialog.cpp b/src/plugins/git/gerrit/authenticationdialog.cpp new file mode 100644 index 00000000000..c51b47bbc06 --- /dev/null +++ b/src/plugins/git/gerrit/authenticationdialog.cpp @@ -0,0 +1,142 @@ +/**************************************************************************** +** +** Copyright (C) 2017 Orgad Shaneh . +** 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 +#include +#include + +#include +#include +#include +#include +#include + +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 diff --git a/src/plugins/git/gerrit/authenticationdialog.h b/src/plugins/git/gerrit/authenticationdialog.h new file mode 100644 index 00000000000..86ca1595627 --- /dev/null +++ b/src/plugins/git/gerrit/authenticationdialog.h @@ -0,0 +1,61 @@ +/**************************************************************************** +** +** Copyright (C) 2017 Orgad Shaneh . +** 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 +#include + +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 diff --git a/src/plugins/git/gerrit/authenticationdialog.ui b/src/plugins/git/gerrit/authenticationdialog.ui new file mode 100644 index 00000000000..7aabac76e0b --- /dev/null +++ b/src/plugins/git/gerrit/authenticationdialog.ui @@ -0,0 +1,127 @@ + + + Gerrit::Internal::AuthenticationDialog + + + + 0 + 0 + 400 + 334 + + + + Authentication + + + + + + + 0 + 0 + + + + <html><head/><body><p>Gerrit server with HTTP was detected, but you need to setup credentials for it.</p><p>To get your password, <a href="LINK_PLACEHOLDER"><span style=" text-decoration: underline; color:#007af4;">click here</span></a> (sign in if needed). Click Generate Password if it is blank, and copy the user name and password to this form.</p><p>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.</p></body></html> + + + Qt::RichText + + + true + + + + + + + + + &User: + + + userLineEdit + + + + + + + + + + &Password: + + + passwordLineEdit + + + + + + + + + + Server: + + + + + + + false + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + Gerrit::Internal::AuthenticationDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Gerrit::Internal::AuthenticationDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/plugins/git/gerrit/gerrit.pri b/src/plugins/git/gerrit/gerrit.pri index 01d71f89864..04f23aa6c05 100644 --- a/src/plugins/git/gerrit/gerrit.pri +++ b/src/plugins/git/gerrit/gerrit.pri @@ -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 diff --git a/src/plugins/git/gerrit/gerritdialog.cpp b/src/plugins/git/gerrit/gerritdialog.cpp index ae19da0ebbb..87ff53730bb 100644 --- a/src/plugins/git/gerrit/gerritdialog.cpp +++ b/src/plugins/git/gerrit/gerritdialog.cpp @@ -240,13 +240,9 @@ 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(server, mapIt.key()); } addRemote(m_parameters->server, tr("Fallback")); m_updatingRemotes = false; @@ -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(); - if (s.host == server.host) + if (s == server) return; } m_ui->remoteComboBox->addItem(server.host + QString(" (%1)").arg(name), diff --git a/src/plugins/git/gerrit/gerritmodel.cpp b/src/plugins/git/gerrit/gerritmodel.cpp index 599d592cc41..a79567375e7 100644 --- a/src/plugins/git/gerrit/gerritmodel.cpp +++ b/src/plugins/git/gerrit/gerritmodel.cpp @@ -50,6 +50,7 @@ #include #include #include +#include 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) { - m_binary = p->ssh; - if (server.port) - m_arguments << p->portFlag << QString::number(server.port); - m_arguments << server.sshHostArgument() << "gerrit" - << "query" << "--dependencies" - << "--current-patch-set" - << "--format=JSON" << query; + if (server.type == GerritServer::Ssh) { + m_binary = p->ssh; + if (server.port) + m_arguments << p->portFlag << QString::number(server.port); + 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 ¶meters, const GerritServer &server, const QByteArray &output, QList &result) { - // 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.replace('\n', ','); - const int lastComma = adaptedOutput.lastIndexOf(','); - if (lastComma >= 0) - adaptedOutput[lastComma] = '\n'; + 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 + 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 ¶meters, // 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); diff --git a/src/plugins/git/gerrit/gerritmodel.h b/src/plugins/git/gerrit/gerritmodel.h index f284c677ccc..fb124417e62 100644 --- a/src/plugins/git/gerrit/gerritmodel.h +++ b/src/plugins/git/gerrit/gerritmodel.h @@ -57,6 +57,7 @@ public: bool hasApproval(const GerritUser &user) const; int approvalLevel() const; + QString url; QString ref; int patchSetNumber; QList approvals; diff --git a/src/plugins/git/gerrit/gerritparameters.cpp b/src/plugins/git/gerrit/gerritparameters.cpp index 3b70ab4a87e..42466554275 100644 --- a/src/plugins/git/gerrit/gerritparameters.cpp +++ b/src/plugins/git/gerrit/gerritparameters.cpp @@ -25,6 +25,10 @@ #include "gerritparameters.h" #include "gerritplugin.h" +#include "authenticationdialog.h" +#include "../gitplugin.h" +#include "../gitclient.h" +#include #include #include @@ -32,6 +36,7 @@ #include #include #include +#include #include #include #include @@ -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 ¶meters) { static const QRegularExpression remotePattern( - "^(?:(?[^:]+)://)?(?:(?[^@]+)@)?(?[^:/]+)(?::(?\\d+))?"); + "^(?:(?[^:]+)://)?(?:(?[^@]+)@)?(?[^:/]+)" + "(?::(?\\d+))?:?(?/.*)$"); // 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; diff --git a/src/plugins/git/gerrit/gerritparameters.h b/src/plugins/git/gerrit/gerritparameters.h index acf2a230b18..69e9b5812b7 100644 --- a/src/plugins/git/gerrit/gerritparameters.h +++ b/src/plugins/git/gerrit/gerritparameters.h @@ -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 ¶meters); + 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 diff --git a/src/plugins/git/git.qbs b/src/plugins/git/git.qbs index e3fe4158c0c..a8c32d387a5 100644 --- a/src/plugins/git/git.qbs +++ b/src/plugins/git/git.qbs @@ -77,6 +77,9 @@ QtcPlugin { name: "Gerrit" prefix: "gerrit/" files: [ + "authenticationdialog.cpp", + "authenticationdialog.h", + "authenticationdialog.ui", "branchcombobox.cpp", "branchcombobox.h", "gerritdialog.cpp",