forked from qt-creator/qt-creator
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:
committed by
Orgad Shaneh
parent
be204a125e
commit
f7a778690d
142
src/plugins/git/gerrit/authenticationdialog.cpp
Normal file
142
src/plugins/git/gerrit/authenticationdialog.cpp
Normal 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
|
61
src/plugins/git/gerrit/authenticationdialog.h
Normal file
61
src/plugins/git/gerrit/authenticationdialog.h
Normal 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
|
127
src/plugins/git/gerrit/authenticationdialog.ui
Normal file
127
src/plugins/git/gerrit/authenticationdialog.ui
Normal 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><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></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>&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>&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>
|
@@ -4,7 +4,8 @@ SOURCES += $$PWD/gerritdialog.cpp \
|
|||||||
$$PWD/gerritplugin.cpp \
|
$$PWD/gerritplugin.cpp \
|
||||||
$$PWD/gerritoptionspage.cpp \
|
$$PWD/gerritoptionspage.cpp \
|
||||||
$$PWD/gerritpushdialog.cpp \
|
$$PWD/gerritpushdialog.cpp \
|
||||||
$$PWD/branchcombobox.cpp
|
$$PWD/branchcombobox.cpp \
|
||||||
|
$$PWD/authenticationdialog.cpp
|
||||||
|
|
||||||
HEADERS += $$PWD/gerritdialog.h \
|
HEADERS += $$PWD/gerritdialog.h \
|
||||||
$$PWD/gerritmodel.h \
|
$$PWD/gerritmodel.h \
|
||||||
@@ -12,8 +13,10 @@ HEADERS += $$PWD/gerritdialog.h \
|
|||||||
$$PWD/gerritplugin.h \
|
$$PWD/gerritplugin.h \
|
||||||
$$PWD/gerritoptionspage.h \
|
$$PWD/gerritoptionspage.h \
|
||||||
$$PWD/gerritpushdialog.h \
|
$$PWD/gerritpushdialog.h \
|
||||||
$$PWD/branchcombobox.h
|
$$PWD/branchcombobox.h \
|
||||||
|
$$PWD/authenticationdialog.h
|
||||||
|
|
||||||
FORMS += $$PWD/gerritdialog.ui \
|
FORMS += $$PWD/gerritdialog.ui \
|
||||||
$$PWD/gerritpushdialog.ui
|
$$PWD/gerritpushdialog.ui \
|
||||||
|
$$PWD/authenticationdialog.ui
|
||||||
|
|
||||||
|
@@ -240,12 +240,8 @@ void GerritDialog::updateRemotes()
|
|||||||
while (mapIt.hasNext()) {
|
while (mapIt.hasNext()) {
|
||||||
mapIt.next();
|
mapIt.next();
|
||||||
GerritServer server;
|
GerritServer server;
|
||||||
if (!server.fillFromRemote(mapIt.value(), m_parameters->server.user.userName))
|
if (!server.fillFromRemote(mapIt.value(), *m_parameters))
|
||||||
continue;
|
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"));
|
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) {
|
for (int i = 0, total = m_ui->remoteComboBox->count(); i < total; ++i) {
|
||||||
const GerritServer s = m_ui->remoteComboBox->itemData(i).value<GerritServer>();
|
const GerritServer s = m_ui->remoteComboBox->itemData(i).value<GerritServer>();
|
||||||
if (s.host == server.host)
|
if (s == server)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
m_ui->remoteComboBox->addItem(server.host + QString(" (%1)").arg(name),
|
m_ui->remoteComboBox->addItem(server.host + QString(" (%1)").arg(name),
|
||||||
|
@@ -50,6 +50,7 @@
|
|||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QFutureWatcher>
|
#include <QFutureWatcher>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
enum { debug = 0 };
|
enum { debug = 0 };
|
||||||
|
|
||||||
@@ -199,7 +200,9 @@ QString GerritChange::filterString() const
|
|||||||
|
|
||||||
QStringList GerritChange::gitFetchArguments(const GerritServer &server) 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
|
QString GerritChange::fullTitle() const
|
||||||
@@ -258,13 +261,21 @@ QueryContext::QueryContext(const QString &query,
|
|||||||
QObject *parent)
|
QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
{
|
{
|
||||||
|
if (server.type == GerritServer::Ssh) {
|
||||||
m_binary = p->ssh;
|
m_binary = p->ssh;
|
||||||
if (server.port)
|
if (server.port)
|
||||||
m_arguments << p->portFlag << QString::number(server.port);
|
m_arguments << p->portFlag << QString::number(server.port);
|
||||||
m_arguments << server.sshHostArgument() << "gerrit"
|
m_arguments << server.hostArgument() << "gerrit"
|
||||||
<< "query" << "--dependencies"
|
<< "query" << "--dependencies"
|
||||||
<< "--current-patch-set"
|
<< "--current-patch-set"
|
||||||
<< "--format=JSON" << query;
|
<< "--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,
|
connect(&m_process, &QProcess::readyReadStandardError,
|
||||||
this, &QueryContext::readyReadStandardError);
|
this, &QueryContext::readyReadStandardError);
|
||||||
connect(&m_process, &QProcess::readyReadStandardOutput,
|
connect(&m_process, &QProcess::readyReadStandardOutput,
|
||||||
@@ -615,18 +626,178 @@ static GerritChangePtr parseSshOutput(const QJsonObject &object)
|
|||||||
return change;
|
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> ¶meters,
|
static bool parseOutput(const QSharedPointer<GerritParameters> ¶meters,
|
||||||
const GerritServer &server,
|
const GerritServer &server,
|
||||||
const QByteArray &output,
|
const QByteArray &output,
|
||||||
QList<GerritChangePtr> &result)
|
QList<GerritChangePtr> &result)
|
||||||
{
|
{
|
||||||
|
QByteArray adaptedOutput;
|
||||||
|
if (server.type == GerritServer::Ssh) {
|
||||||
// The output consists of separate lines containing a document each
|
// 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
|
// Add a comma after each line (except the last), and enclose it as an array
|
||||||
QByteArray adaptedOutput = '[' + output + ']';
|
adaptedOutput = '[' + output + ']';
|
||||||
adaptedOutput.replace('\n', ',');
|
adaptedOutput.replace('\n', ',');
|
||||||
const int lastComma = adaptedOutput.lastIndexOf(',');
|
const int lastComma = adaptedOutput.lastIndexOf(',');
|
||||||
if (lastComma >= 0)
|
if (lastComma >= 0)
|
||||||
adaptedOutput[lastComma] = '\n';
|
adaptedOutput[lastComma] = '\n';
|
||||||
|
} else {
|
||||||
|
adaptedOutput = output;
|
||||||
|
// Strip first line, which is )]}'
|
||||||
|
adaptedOutput.remove(0, adaptedOutput.indexOf("\n"));
|
||||||
|
}
|
||||||
bool res = true;
|
bool res = true;
|
||||||
|
|
||||||
QJsonParseError error;
|
QJsonParseError error;
|
||||||
@@ -647,7 +818,9 @@ static bool parseOutput(const QSharedPointer<GerritParameters> ¶meters,
|
|||||||
// Skip stats line: {"type":"stats","rowCount":9,"runTimeMilliseconds":13}
|
// Skip stats line: {"type":"stats","rowCount":9,"runTimeMilliseconds":13}
|
||||||
if (object.contains("type"))
|
if (object.contains("type"))
|
||||||
continue;
|
continue;
|
||||||
GerritChangePtr change = parseSshOutput(object);
|
GerritChangePtr change =
|
||||||
|
(server.type == GerritServer::Ssh ? parseSshOutput(object)
|
||||||
|
: parseRestOutput(object, server));
|
||||||
if (change->isValid()) {
|
if (change->isValid()) {
|
||||||
if (change->url.isEmpty()) // No "canonicalWebUrl" is in gerrit.config.
|
if (change->url.isEmpty()) // No "canonicalWebUrl" is in gerrit.config.
|
||||||
change->url = defaultUrl(parameters, server, change->number);
|
change->url = defaultUrl(parameters, server, change->number);
|
||||||
|
@@ -57,6 +57,7 @@ public:
|
|||||||
bool hasApproval(const GerritUser &user) const;
|
bool hasApproval(const GerritUser &user) const;
|
||||||
int approvalLevel() const;
|
int approvalLevel() const;
|
||||||
|
|
||||||
|
QString url;
|
||||||
QString ref;
|
QString ref;
|
||||||
int patchSetNumber;
|
int patchSetNumber;
|
||||||
QList<GerritApproval> approvals;
|
QList<GerritApproval> approvals;
|
||||||
|
@@ -25,6 +25,10 @@
|
|||||||
|
|
||||||
#include "gerritparameters.h"
|
#include "gerritparameters.h"
|
||||||
#include "gerritplugin.h"
|
#include "gerritplugin.h"
|
||||||
|
#include "authenticationdialog.h"
|
||||||
|
#include "../gitplugin.h"
|
||||||
|
#include "../gitclient.h"
|
||||||
|
#include <coreplugin/shellcommand.h>
|
||||||
|
|
||||||
#include <utils/hostosinfo.h>
|
#include <utils/hostosinfo.h>
|
||||||
#include <utils/pathchooser.h>
|
#include <utils/pathchooser.h>
|
||||||
@@ -32,6 +36,7 @@
|
|||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
|
#include <QJsonDocument>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
@@ -51,6 +56,7 @@ static const char curlKeyC[] = "Curl";
|
|||||||
static const char httpsKeyC[] = "Https";
|
static const char httpsKeyC[] = "Https";
|
||||||
static const char defaultHostC[] = "codereview.qt-project.org";
|
static const char defaultHostC[] = "codereview.qt-project.org";
|
||||||
static const char savedQueriesKeyC[] = "SavedQueries";
|
static const char savedQueriesKeyC[] = "SavedQueries";
|
||||||
|
static const char accountUrlC[] = "/accounts/self";
|
||||||
|
|
||||||
static const char defaultPortFlag[] = "-p";
|
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);
|
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)
|
if (port)
|
||||||
res += ':' + QString::number(port);
|
res += ':' + QString::number(port);
|
||||||
|
if (type != Ssh)
|
||||||
|
res += rootPath;
|
||||||
return res;
|
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(
|
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)
|
// Skip local remotes (refer to the root or relative path)
|
||||||
if (remote.isEmpty() || remote.startsWith('/') || remote.startsWith('.'))
|
if (remote.isEmpty() || remote.startsWith('/') || remote.startsWith('.'))
|
||||||
@@ -178,14 +205,95 @@ bool GerritServer::fillFromRemote(const QString &remote, const QString &defaultU
|
|||||||
else
|
else
|
||||||
return false;
|
return false;
|
||||||
const QString userName = match.captured("user");
|
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");
|
host = match.captured("host");
|
||||||
port = match.captured("port").toUShort();
|
port = match.captured("port").toUShort();
|
||||||
if (host.contains("github.com")) // Clearly not gerrit
|
if (host.contains("github.com")) // Clearly not gerrit
|
||||||
return false;
|
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;
|
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
|
bool GerritParameters::equals(const GerritParameters &rhs) const
|
||||||
{
|
{
|
||||||
return server == rhs.server && ssh == rhs.ssh && curl == rhs.curl && https == rhs.https;
|
return server == rhs.server && ssh == rhs.ssh && curl == rhs.curl && https == rhs.https;
|
||||||
|
@@ -32,6 +32,8 @@ QT_FORWARD_DECLARE_CLASS(QSettings)
|
|||||||
namespace Gerrit {
|
namespace Gerrit {
|
||||||
namespace Internal {
|
namespace Internal {
|
||||||
|
|
||||||
|
class GerritParameters;
|
||||||
|
|
||||||
class GerritUser
|
class GerritUser
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -55,14 +57,25 @@ public:
|
|||||||
GerritServer();
|
GerritServer();
|
||||||
GerritServer(const QString &host, unsigned short port, const QString &userName, HostType type);
|
GerritServer(const QString &host, unsigned short port, const QString &userName, HostType type);
|
||||||
bool operator==(const GerritServer &other) const;
|
bool operator==(const GerritServer &other) const;
|
||||||
QString sshHostArgument() const;
|
QString hostArgument() const;
|
||||||
QString url() const;
|
QString url(bool withHttpUser = false) const;
|
||||||
bool fillFromRemote(const QString &remote, const QString &defaultUser);
|
QString restUrl() const;
|
||||||
|
bool fillFromRemote(const QString &remote, const GerritParameters ¶meters);
|
||||||
|
int testConnection();
|
||||||
|
static QStringList curlArguments();
|
||||||
|
|
||||||
QString host;
|
QString host;
|
||||||
GerritUser user;
|
GerritUser user;
|
||||||
|
QString rootPath; // for http
|
||||||
unsigned short port = 0;
|
unsigned short port = 0;
|
||||||
HostType type = Ssh;
|
HostType type = Ssh;
|
||||||
|
bool authenticated = true;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString curlBinary;
|
||||||
|
bool setupAuthentication();
|
||||||
|
bool ascendPath();
|
||||||
|
bool resolveRoot();
|
||||||
};
|
};
|
||||||
|
|
||||||
class GerritParameters
|
class GerritParameters
|
||||||
|
@@ -77,6 +77,9 @@ QtcPlugin {
|
|||||||
name: "Gerrit"
|
name: "Gerrit"
|
||||||
prefix: "gerrit/"
|
prefix: "gerrit/"
|
||||||
files: [
|
files: [
|
||||||
|
"authenticationdialog.cpp",
|
||||||
|
"authenticationdialog.h",
|
||||||
|
"authenticationdialog.ui",
|
||||||
"branchcombobox.cpp",
|
"branchcombobox.cpp",
|
||||||
"branchcombobox.h",
|
"branchcombobox.h",
|
||||||
"gerritdialog.cpp",
|
"gerritdialog.cpp",
|
||||||
|
Reference in New Issue
Block a user