forked from qt-creator/qt-creator
GitLab: Allow browsing and cloning projects
Change-Id: I1cc877ea6b5a55ae7bdb8e7a529afeb08d09e0c0 Reviewed-by: <github-actions-qt-creator@cristianadam.eu> Reviewed-by: David Schulz <david.schulz@qt.io>
This commit is contained in:
@@ -3,6 +3,8 @@ add_qtc_plugin(GitLab
|
||||
PLUGIN_DEPENDS Core ProjectExplorer Git VcsBase
|
||||
DEPENDS Utils
|
||||
SOURCES
|
||||
gitlabclonedialog.cpp gitlabclonedialog.h
|
||||
gitlabdialog.cpp gitlabdialog.h gitlabdialog.ui
|
||||
gitlaboptionspage.cpp gitlaboptionspage.h
|
||||
gitlabparameters.cpp gitlabparameters.h
|
||||
gitlabplugin.cpp gitlabplugin.h
|
||||
|
||||
@@ -10,6 +10,11 @@ QtcPlugin {
|
||||
Depends { name: "Utils" }
|
||||
|
||||
files: [
|
||||
"gitlabclonedialog.cpp",
|
||||
"gitlabclonedialog.h",
|
||||
"gitlabdialog.cpp",
|
||||
"gitlabdialog.h",
|
||||
"gitlabdialog.ui",
|
||||
"gitlaboptionspage.cpp",
|
||||
"gitlaboptionspage.h",
|
||||
"gitlabparameters.cpp",
|
||||
|
||||
263
src/plugins/gitlab/gitlabclonedialog.cpp
Normal file
263
src/plugins/gitlab/gitlabclonedialog.cpp
Normal file
@@ -0,0 +1,263 @@
|
||||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2022 The Qt Company Ltd.
|
||||
** 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 "gitlabclonedialog.h"
|
||||
|
||||
#include "gitlabprojectsettings.h"
|
||||
#include "resultparser.h"
|
||||
|
||||
#include <coreplugin/documentmanager.h>
|
||||
#include <coreplugin/shellcommand.h>
|
||||
#include <coreplugin/vcsmanager.h>
|
||||
#include <git/gitclient.h>
|
||||
#include <projectexplorer/projectexplorer.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
#include <utils/algorithm.h>
|
||||
#include <utils/commandline.h>
|
||||
#include <utils/environment.h>
|
||||
#include <utils/fancylineedit.h>
|
||||
#include <utils/filepath.h>
|
||||
#include <utils/infolabel.h>
|
||||
#include <utils/mimeutils.h>
|
||||
#include <utils/pathchooser.h>
|
||||
#include <utils/qtcassert.h>
|
||||
#include <utils/qtcprocess.h>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QInputDialog>
|
||||
#include <QMessageBox>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace GitLab {
|
||||
|
||||
GitLabCloneDialog::GitLabCloneDialog(const Project &project, QWidget *parent)
|
||||
: QDialog(parent)
|
||||
{
|
||||
setWindowTitle(tr("Clone Repository"));
|
||||
QVBoxLayout *layout = new QVBoxLayout(this);
|
||||
layout->addWidget(new QLabel(tr("Specify repository URL, checkout path and directory.")));
|
||||
QHBoxLayout *centerLayout = new QHBoxLayout;
|
||||
QFormLayout *form = new QFormLayout;
|
||||
m_repositoryCB = new QComboBox(this);
|
||||
m_repositoryCB->addItems({project.sshUrl, project.httpUrl});
|
||||
form->addRow(tr("Repository"), m_repositoryCB);
|
||||
m_pathChooser = new Utils::PathChooser(this);
|
||||
m_pathChooser->setExpectedKind(Utils::PathChooser::ExistingDirectory);
|
||||
form->addRow(tr("Path"), m_pathChooser);
|
||||
m_directoryLE = new Utils::FancyLineEdit(this);
|
||||
m_directoryLE->setValidationFunction([this](Utils::FancyLineEdit *e, QString *msg) {
|
||||
const Utils::FilePath fullPath = m_pathChooser->filePath().pathAppended(e->text());
|
||||
bool alreadyExists = fullPath.exists();
|
||||
if (alreadyExists && msg)
|
||||
*msg = tr("Path \"%1\" already exists.").arg(fullPath.toUserOutput());
|
||||
return !alreadyExists;
|
||||
});
|
||||
form->addRow(tr("Directory"), m_directoryLE);
|
||||
m_submodulesCB = new QCheckBox(this);
|
||||
form->addRow(tr("Recursive"), m_submodulesCB);
|
||||
centerLayout->addLayout(form);
|
||||
m_cloneOutput = new QPlainTextEdit(this);
|
||||
m_cloneOutput->setReadOnly(true);
|
||||
centerLayout->addWidget(m_cloneOutput);
|
||||
layout->addLayout(centerLayout);
|
||||
m_infoLabel = new Utils::InfoLabel(this);
|
||||
layout->addWidget(m_infoLabel);
|
||||
layout->addStretch(1);
|
||||
auto buttons = new QDialogButtonBox(QDialogButtonBox::Cancel, this);
|
||||
m_cloneButton = new QPushButton(tr("Clone"), this);
|
||||
buttons->addButton(m_cloneButton, QDialogButtonBox::ActionRole);
|
||||
m_cancelButton = buttons->button(QDialogButtonBox::Cancel);
|
||||
layout->addWidget(buttons);
|
||||
setLayout(layout);
|
||||
|
||||
m_pathChooser->setFilePath(Core::DocumentManager::projectsDirectory());
|
||||
auto [host, path, port]
|
||||
= GitLabProjectSettings::remotePartsFromRemote(m_repositoryCB->currentText());
|
||||
int slashIndex = path.indexOf('/');
|
||||
QTC_ASSERT(slashIndex > 0, return);
|
||||
m_directoryLE->setText(path.mid(slashIndex + 1));
|
||||
|
||||
connect(m_pathChooser, &Utils::PathChooser::pathChanged, this, [this]() {
|
||||
m_directoryLE->validate();
|
||||
GitLabCloneDialog::updateUi();
|
||||
});
|
||||
connect(m_directoryLE, &Utils::FancyLineEdit::textChanged, this, &GitLabCloneDialog::updateUi);
|
||||
connect(m_cloneButton, &QPushButton::clicked, this, &GitLabCloneDialog::cloneProject);
|
||||
connect(m_cancelButton, &QPushButton::clicked,
|
||||
this, &GitLabCloneDialog::cancel);
|
||||
connect(this, &QDialog::rejected, this, [this]() {
|
||||
if (m_commandRunning) {
|
||||
cancel();
|
||||
QApplication::restoreOverrideCursor();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
updateUi();
|
||||
resize(575, 265);
|
||||
}
|
||||
|
||||
void GitLabCloneDialog::updateUi()
|
||||
{
|
||||
bool pathValid = m_pathChooser->isValid();
|
||||
bool directoryValid = m_directoryLE->isValid();
|
||||
m_cloneButton->setEnabled(pathValid && directoryValid);
|
||||
if (!pathValid) {
|
||||
m_infoLabel->setText(m_pathChooser->errorMessage());
|
||||
m_infoLabel->setType(Utils::InfoLabel::Error);
|
||||
} else if (!directoryValid) {
|
||||
m_infoLabel->setText(m_directoryLE->errorMessage());
|
||||
m_infoLabel->setType(Utils::InfoLabel::Error);
|
||||
}
|
||||
m_infoLabel->setVisible(!pathValid || !directoryValid);
|
||||
}
|
||||
|
||||
void GitLabCloneDialog::cloneProject()
|
||||
{
|
||||
Core::IVersionControl *vc = Core::VcsManager::versionControl(Utils::Id::fromString("G.Git"));
|
||||
QTC_ASSERT(vc, return);
|
||||
const QStringList extraArgs = m_submodulesCB->isChecked() ? QStringList{ "--recursive" }
|
||||
: QStringList{};
|
||||
m_command = vc->createInitialCheckoutCommand(m_repositoryCB->currentText(),
|
||||
m_pathChooser->absoluteFilePath(),
|
||||
m_directoryLE->text(), extraArgs);
|
||||
const Utils::FilePath workingDirectory = m_pathChooser->absoluteFilePath();
|
||||
m_command->setProgressiveOutput(true);
|
||||
connect(m_command, &Utils::ShellCommand::stdOutText, this, [this](const QString &text) {
|
||||
m_cloneOutput->appendPlainText(text);
|
||||
});
|
||||
connect(m_command, &Utils::ShellCommand::stdErrText, this, [this](const QString &text) {
|
||||
m_cloneOutput->appendPlainText(text);
|
||||
});
|
||||
connect(m_command, &Utils::ShellCommand::finished, this, &GitLabCloneDialog::cloneFinished);
|
||||
QApplication::setOverrideCursor(Qt::WaitCursor);
|
||||
|
||||
m_cloneOutput->clear();
|
||||
m_cloneButton->setEnabled(false);
|
||||
m_pathChooser->setReadOnly(true);
|
||||
m_directoryLE->setReadOnly(true);
|
||||
m_commandRunning = true;
|
||||
m_command->execute();
|
||||
}
|
||||
|
||||
void GitLabCloneDialog::cancel()
|
||||
{
|
||||
if (m_commandRunning) {
|
||||
m_cloneOutput->appendPlainText(tr("User canceled process."));
|
||||
m_cancelButton->setEnabled(false);
|
||||
m_command->cancel(); // FIXME does not cancel the git processes... QTCREATORBUG-27567
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
}
|
||||
|
||||
static Utils::FilePaths scanDirectoryForFiles(const Utils::FilePath &directory)
|
||||
{
|
||||
Utils::FilePaths result;
|
||||
const Utils::FilePaths entries = directory.dirEntries(QDir::AllEntries | QDir::NoDotAndDotDot);
|
||||
|
||||
for (const Utils::FilePath &entry : entries) {
|
||||
if (entry.isDir())
|
||||
result.append(scanDirectoryForFiles(entry));
|
||||
else
|
||||
result.append(entry);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void GitLabCloneDialog::cloneFinished(bool ok, int exitCode)
|
||||
{
|
||||
const bool success = (ok && exitCode == 0);
|
||||
m_commandRunning = false;
|
||||
delete m_command;
|
||||
m_command = nullptr;
|
||||
|
||||
const QString emptyLine("\n\n");
|
||||
m_cloneOutput->appendPlainText(emptyLine);
|
||||
QApplication::restoreOverrideCursor();
|
||||
|
||||
if (success) {
|
||||
m_cloneOutput->appendPlainText(tr("Cloning succeeded.") + emptyLine);
|
||||
m_cloneButton->setEnabled(false);
|
||||
|
||||
const Utils::FilePath base = m_pathChooser->filePath().pathAppended(m_directoryLE->text());
|
||||
Utils::FilePaths filesWeMayOpen
|
||||
= Utils::filtered(scanDirectoryForFiles(base), [](const Utils::FilePath &f) {
|
||||
return ProjectExplorer::ProjectManager::canOpenProjectForMimeType(
|
||||
Utils::mimeTypeForFile(f));
|
||||
});
|
||||
|
||||
// limit the files to the most top-level item(s)
|
||||
int minimum = std::numeric_limits<int>::max();
|
||||
for (const Utils::FilePath &f : filesWeMayOpen) {
|
||||
int parentCount = f.toString().count('/');
|
||||
if (parentCount < minimum)
|
||||
minimum = parentCount;
|
||||
}
|
||||
filesWeMayOpen = Utils::filtered(filesWeMayOpen, [minimum](const Utils::FilePath &f) {
|
||||
return f.toString().count('/') == minimum;
|
||||
});
|
||||
|
||||
hide(); // avoid to many dialogs.. FIXME: maybe change to some wizard approach?
|
||||
if (filesWeMayOpen.isEmpty()) {
|
||||
QMessageBox::warning(this, tr("Warning"),
|
||||
tr("Cloned project does not have a project file that can be "
|
||||
"opened. Try importing the project as a generic project."));
|
||||
accept();
|
||||
} else {
|
||||
const QStringList pFiles = Utils::transform(filesWeMayOpen,
|
||||
[base](const Utils::FilePath &f) {
|
||||
return f.relativePath(base).toUserOutput();
|
||||
});
|
||||
bool ok = false;
|
||||
const QString fileToOpen
|
||||
= QInputDialog::getItem(this, tr("Open Project"),
|
||||
tr("Choose the project file to be opened."),
|
||||
pFiles, 0, false, &ok);
|
||||
accept();
|
||||
if (ok && !fileToOpen.isEmpty())
|
||||
ProjectExplorer::ProjectExplorerPlugin::openProject(base.pathAppended(fileToOpen));
|
||||
}
|
||||
} else {
|
||||
m_cloneOutput->appendPlainText(tr("Cloning failed.") + emptyLine);
|
||||
const Utils::FilePath fullPath = m_pathChooser->filePath()
|
||||
.pathAppended(m_directoryLE->text());
|
||||
fullPath.removeRecursively();
|
||||
m_cloneButton->setEnabled(true);
|
||||
m_cancelButton->setEnabled(true);
|
||||
m_pathChooser->setReadOnly(false);
|
||||
m_directoryLE->setReadOnly(false);
|
||||
m_directoryLE->validate();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace GitLab
|
||||
74
src/plugins/gitlab/gitlabclonedialog.h
Normal file
74
src/plugins/gitlab/gitlabclonedialog.h
Normal file
@@ -0,0 +1,74 @@
|
||||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2022 The Qt Company Ltd.
|
||||
** 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.
|
||||
**
|
||||
****************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDialog>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QCheckBox;
|
||||
class QComboBox;
|
||||
class QPlainTextEdit;
|
||||
class QPushButton;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
namespace Core { class ShellCommand; }
|
||||
|
||||
namespace Utils {
|
||||
class FancyLineEdit;
|
||||
class InfoLabel;
|
||||
class PathChooser;
|
||||
}
|
||||
|
||||
namespace GitLab {
|
||||
|
||||
class Project;
|
||||
|
||||
class GitLabCloneDialog : public QDialog
|
||||
{
|
||||
Q_DECLARE_TR_FUNCTIONS(GitLab::GitLabCloneDialog)
|
||||
public:
|
||||
explicit GitLabCloneDialog(const Project &project, QWidget *parent = nullptr);
|
||||
|
||||
private:
|
||||
void updateUi();
|
||||
void cloneProject();
|
||||
void cancel();
|
||||
void cloneFinished(bool ok, int exitCode);
|
||||
|
||||
QComboBox * m_repositoryCB = nullptr;
|
||||
QCheckBox *m_submodulesCB = nullptr;
|
||||
QPushButton *m_cloneButton = nullptr;
|
||||
QPushButton *m_cancelButton = nullptr;
|
||||
QPlainTextEdit *m_cloneOutput = nullptr;
|
||||
Utils::PathChooser *m_pathChooser = nullptr;
|
||||
Utils::FancyLineEdit *m_directoryLE = nullptr;
|
||||
Utils::InfoLabel *m_infoLabel = nullptr;
|
||||
Core::ShellCommand *m_command = nullptr;
|
||||
bool m_commandRunning = false;
|
||||
};
|
||||
|
||||
} // namespace GitLab
|
||||
294
src/plugins/gitlab/gitlabdialog.cpp
Normal file
294
src/plugins/gitlab/gitlabdialog.cpp
Normal file
@@ -0,0 +1,294 @@
|
||||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2022 The Qt Company Ltd.
|
||||
** 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 "gitlabdialog.h"
|
||||
|
||||
#include "gitlabclonedialog.h"
|
||||
#include "gitlabparameters.h"
|
||||
#include "gitlabplugin.h"
|
||||
#include "gitlabprojectsettings.h"
|
||||
|
||||
#include <projectexplorer/session.h>
|
||||
#include <texteditor/fontsettings.h>
|
||||
#include <texteditor/texteditorsettings.h>
|
||||
#include <utils/listmodel.h>
|
||||
#include <utils/qtcassert.h>
|
||||
#include <utils/utilsicons.h>
|
||||
|
||||
#include <QKeyEvent>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QRegularExpression>
|
||||
#include <QSyntaxHighlighter>
|
||||
|
||||
namespace GitLab {
|
||||
|
||||
GitLabDialog::GitLabDialog(QWidget *parent)
|
||||
: QDialog(parent)
|
||||
, m_lastTreeViewQuery(Query::NoQuery)
|
||||
{
|
||||
m_ui.setupUi(this);
|
||||
m_clonePB = new QPushButton(Utils::Icons::DOWNLOAD.icon(), tr("Clone..."), this);
|
||||
m_ui.buttonBox->addButton(m_clonePB, QDialogButtonBox::ActionRole);
|
||||
m_clonePB->setEnabled(false);
|
||||
|
||||
updateRemotes();
|
||||
|
||||
connect(m_ui.remoteCB, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, &GitLabDialog::requestMainViewUpdate);
|
||||
connect(m_ui.searchLE, &QLineEdit::returnPressed, this, &GitLabDialog::querySearch);
|
||||
connect(m_ui.searchPB, &QPushButton::clicked, this, &GitLabDialog::querySearch);
|
||||
connect(m_clonePB, &QPushButton::clicked, this, &GitLabDialog::cloneSelected);
|
||||
connect(m_ui.firstTB, &QToolButton::clicked, this, &GitLabDialog::queryFirstPage);
|
||||
connect(m_ui.previousTB, &QToolButton::clicked, this, &GitLabDialog::queryPreviousPage);
|
||||
connect(m_ui.nextTB, &QToolButton::clicked, this, &GitLabDialog::queryNextPage);
|
||||
connect(m_ui.lastTB, &QToolButton::clicked, this, &GitLabDialog::queryLastPage);
|
||||
requestMainViewUpdate();
|
||||
}
|
||||
|
||||
void GitLabDialog::resetTreeView(QTreeView *treeView, QAbstractItemModel *model)
|
||||
{
|
||||
auto oldModel = treeView->model();
|
||||
treeView->setModel(model);
|
||||
delete oldModel;
|
||||
if (model) {
|
||||
connect(treeView->selectionModel(), &QItemSelectionModel::selectionChanged,
|
||||
this, [this](const QItemSelection &selected, const QItemSelection &) {
|
||||
m_clonePB->setEnabled(!selected.isEmpty());
|
||||
});
|
||||
m_clonePB->setEnabled(!treeView->selectionModel()->selectedIndexes().isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
void GitLabDialog::updateRemotes()
|
||||
{
|
||||
m_ui.remoteCB->clear();
|
||||
const GitLabParameters *global = GitLabPlugin::globalParameters();
|
||||
for (const GitLabServer &server : qAsConst(global->gitLabServers))
|
||||
m_ui.remoteCB->addItem(server.displayString(), QVariant::fromValue(server));
|
||||
|
||||
m_ui.remoteCB->setCurrentIndex(m_ui.remoteCB->findData(
|
||||
QVariant::fromValue(global->currentDefaultServer())));
|
||||
}
|
||||
|
||||
void GitLabDialog::keyPressEvent(QKeyEvent *event)
|
||||
{
|
||||
if (event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return)
|
||||
return;
|
||||
QDialog::keyPressEvent(event);
|
||||
}
|
||||
|
||||
void GitLabDialog::requestMainViewUpdate()
|
||||
{
|
||||
m_lastPageInformation = PageInformation();
|
||||
m_lastTreeViewQuery = Query(Query::NoQuery);
|
||||
|
||||
m_ui.mainLabel->setText({});
|
||||
m_ui.detailsLabel->setText({});
|
||||
m_ui.treeViewTitle->setText({});
|
||||
m_ui.searchLE->setText({});
|
||||
resetTreeView(m_ui.treeView, nullptr);
|
||||
updatePageButtons();
|
||||
|
||||
bool linked = false;
|
||||
m_currentServerId = Utils::Id();
|
||||
if (auto project = ProjectExplorer::SessionManager::startupProject()) {
|
||||
GitLabProjectSettings *projSettings = GitLabPlugin::projectSettings(project);
|
||||
if (projSettings->isLinked()) {
|
||||
m_currentServerId = projSettings->currentServer();
|
||||
linked = true;
|
||||
}
|
||||
}
|
||||
if (!m_currentServerId.isValid())
|
||||
m_currentServerId = m_ui.remoteCB->currentData().value<GitLabServer>().id;
|
||||
if (m_currentServerId.isValid()) {
|
||||
const GitLabParameters *global = GitLabPlugin::globalParameters();
|
||||
const GitLabServer server = global->serverForId(m_currentServerId);
|
||||
m_ui.remoteCB->setCurrentIndex(m_ui.remoteCB->findData(QVariant::fromValue(server)));
|
||||
}
|
||||
m_ui.remoteCB->setEnabled(!linked);
|
||||
|
||||
const Query query(Query::User);
|
||||
QueryRunner *runner = new QueryRunner(query, m_currentServerId, this);
|
||||
connect(runner, &QueryRunner::resultRetrieved, this, [this](const QByteArray &result) {
|
||||
handleUser(ResultParser::parseUser(result));
|
||||
});
|
||||
connect(runner, &QueryRunner::finished, [runner]() { runner->deleteLater(); });
|
||||
runner->start();
|
||||
}
|
||||
|
||||
void GitLabDialog::updatePageButtons()
|
||||
{
|
||||
if (m_lastPageInformation.currentPage == -1) {
|
||||
m_ui.currentPage->setVisible(false);
|
||||
m_ui.firstTB->setVisible(false);
|
||||
m_ui.lastTB->setVisible(false);
|
||||
m_ui.previousTB->setVisible(false);
|
||||
m_ui.nextTB->setVisible(false);
|
||||
} else {
|
||||
m_ui.currentPage->setText(QString::number(m_lastPageInformation.currentPage));
|
||||
m_ui.currentPage->setVisible(true);
|
||||
m_ui.firstTB->setVisible(true);
|
||||
m_ui.lastTB->setVisible(true);
|
||||
}
|
||||
if (m_lastPageInformation.currentPage > 1) {
|
||||
m_ui.firstTB->setEnabled(true);
|
||||
m_ui.previousTB->setText(QString::number(m_lastPageInformation.currentPage - 1));
|
||||
m_ui.previousTB->setVisible(true);
|
||||
} else {
|
||||
m_ui.firstTB->setEnabled(false);
|
||||
m_ui.previousTB->setVisible(false);
|
||||
}
|
||||
if (m_lastPageInformation.currentPage < m_lastPageInformation.totalPages) {
|
||||
m_ui.lastTB->setEnabled(true);
|
||||
m_ui.nextTB->setText(QString::number(m_lastPageInformation.currentPage + 1));
|
||||
m_ui.nextTB->setVisible(true);
|
||||
} else {
|
||||
m_ui.lastTB->setEnabled(false);
|
||||
m_ui.nextTB->setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
void GitLabDialog::queryFirstPage()
|
||||
{
|
||||
QTC_ASSERT(m_lastTreeViewQuery.type() != Query::NoQuery, return);
|
||||
QTC_ASSERT(m_lastPageInformation.currentPage != -1, return);
|
||||
m_lastTreeViewQuery.setPageParameter(1);
|
||||
fetchProjects();
|
||||
}
|
||||
|
||||
void GitLabDialog::queryPreviousPage()
|
||||
{
|
||||
QTC_ASSERT(m_lastTreeViewQuery.type() != Query::NoQuery, return);
|
||||
QTC_ASSERT(m_lastPageInformation.currentPage != -1, return);
|
||||
m_lastTreeViewQuery.setPageParameter(m_lastPageInformation.currentPage - 1);
|
||||
fetchProjects();
|
||||
}
|
||||
|
||||
void GitLabDialog::queryNextPage()
|
||||
{
|
||||
QTC_ASSERT(m_lastTreeViewQuery.type() != Query::NoQuery, return);
|
||||
QTC_ASSERT(m_lastPageInformation.currentPage != -1, return);
|
||||
m_lastTreeViewQuery.setPageParameter(m_lastPageInformation.currentPage + 1);
|
||||
fetchProjects();
|
||||
}
|
||||
|
||||
void GitLabDialog::queryLastPage()
|
||||
{
|
||||
QTC_ASSERT(m_lastTreeViewQuery.type() != Query::NoQuery, return);
|
||||
QTC_ASSERT(m_lastPageInformation.currentPage != -1, return);
|
||||
m_lastTreeViewQuery.setPageParameter(m_lastPageInformation.totalPages);
|
||||
fetchProjects();
|
||||
}
|
||||
|
||||
void GitLabDialog::querySearch()
|
||||
{
|
||||
QTC_ASSERT(m_lastTreeViewQuery.type() != Query::NoQuery, return);
|
||||
m_lastTreeViewQuery.setPageParameter(-1);
|
||||
m_lastTreeViewQuery.setAdditionalParameters({"search=" + m_ui.searchLE->text()});
|
||||
fetchProjects();
|
||||
}
|
||||
|
||||
void GitLabDialog::handleUser(const User &user)
|
||||
{
|
||||
m_lastPageInformation = {};
|
||||
m_currentUserId = user.id;
|
||||
|
||||
if (!user.error.message.isEmpty()) {
|
||||
// TODO
|
||||
if (user.error.code == 1) {
|
||||
m_ui.mainLabel->setText(tr("Not logged in."));
|
||||
m_ui.detailsLabel->setText(tr("Insufficient access token."));
|
||||
m_ui.detailsLabel->setToolTip(user.error.message + QLatin1Char('\n')
|
||||
+ tr("Permission scope read_api or api needed."));
|
||||
updatePageButtons();
|
||||
m_ui.treeViewTitle->setText(tr("Projects (%1)").arg(0));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (user.id != -1) {
|
||||
if (user.bot) {
|
||||
m_ui.mainLabel->setText(tr("Using project access token."));
|
||||
m_ui.detailsLabel->setText({});
|
||||
} else {
|
||||
m_ui.mainLabel->setText(tr("Logged in as %1").arg(user.name));
|
||||
m_ui.detailsLabel->setText(tr("Id: %1 (%2)").arg(user.id).arg(user.email));
|
||||
}
|
||||
m_ui.detailsLabel->setToolTip({});
|
||||
} else {
|
||||
m_ui.mainLabel->setText(tr("Not logged in."));
|
||||
m_ui.detailsLabel->setText({});
|
||||
m_ui.detailsLabel->setToolTip({});
|
||||
}
|
||||
m_lastTreeViewQuery = Query(Query::Projects);
|
||||
fetchProjects();
|
||||
}
|
||||
|
||||
void GitLabDialog::handleProjects(const Projects &projects)
|
||||
{
|
||||
Utils::ListModel<Project *> *listModel = new Utils::ListModel<Project *>(this);
|
||||
for (const Project &project : projects.projects)
|
||||
listModel->appendItem(new Project(project));
|
||||
|
||||
// TODO use a real model / delegate..?
|
||||
listModel->setDataAccessor([](Project *data, int /*column*/, int role) -> QVariant {
|
||||
if (role == Qt::DisplayRole)
|
||||
return QString(data->displayName + " (" + data->visibility + ')');
|
||||
if (role == Qt::UserRole)
|
||||
return QVariant::fromValue(*data);
|
||||
return QVariant();
|
||||
});
|
||||
resetTreeView(m_ui.treeView, listModel);
|
||||
int count = projects.error.message.isEmpty() ? projects.pageInfo.total : 0;
|
||||
m_ui.treeViewTitle->setText(tr("Projects (%1)").arg(count));
|
||||
|
||||
m_lastPageInformation = projects.pageInfo;
|
||||
updatePageButtons();
|
||||
}
|
||||
|
||||
void GitLabDialog::fetchProjects()
|
||||
{
|
||||
QueryRunner *runner = new QueryRunner(m_lastTreeViewQuery, m_currentServerId, this);
|
||||
connect(runner, &QueryRunner::resultRetrieved, this, [this](const QByteArray &result) {
|
||||
handleProjects(ResultParser::parseProjects(result));
|
||||
});
|
||||
connect(runner, &QueryRunner::finished, [runner]() { runner->deleteLater(); });
|
||||
runner->start();
|
||||
}
|
||||
|
||||
void GitLabDialog::cloneSelected()
|
||||
{
|
||||
const QModelIndexList indexes = m_ui.treeView->selectionModel()->selectedIndexes();
|
||||
QTC_ASSERT(indexes.size() == 1, return);
|
||||
const Project project = indexes.first().data(Qt::UserRole).value<Project>();
|
||||
QTC_ASSERT(!project.sshUrl.isEmpty() && !project.httpUrl.isEmpty(), return);
|
||||
GitLabCloneDialog dialog(project, this);
|
||||
if (dialog.exec() == QDialog::Accepted)
|
||||
reject();
|
||||
}
|
||||
|
||||
} // namespace GitLab
|
||||
81
src/plugins/gitlab/gitlabdialog.h
Normal file
81
src/plugins/gitlab/gitlabdialog.h
Normal file
@@ -0,0 +1,81 @@
|
||||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2022 The Qt Company Ltd.
|
||||
** 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.
|
||||
**
|
||||
****************************************************************************/
|
||||
#pragma once
|
||||
|
||||
#include "ui_gitlabdialog.h"
|
||||
|
||||
#include "queryrunner.h"
|
||||
#include "resultparser.h"
|
||||
|
||||
#include <utils/filepath.h>
|
||||
#include <utils/id.h>
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QPushButton;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
namespace GitLab {
|
||||
|
||||
class GitLabParameters;
|
||||
class GitLabDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit GitLabDialog(QWidget *parent = nullptr);
|
||||
|
||||
void updateRemotes();
|
||||
|
||||
protected:
|
||||
void keyPressEvent(QKeyEvent *event) override;
|
||||
|
||||
private:
|
||||
void resetTreeView(QTreeView *treeView, QAbstractItemModel *model);
|
||||
void requestMainViewUpdate();
|
||||
void updatePageButtons();
|
||||
|
||||
void queryFirstPage();
|
||||
void queryPreviousPage();
|
||||
void queryNextPage();
|
||||
void queryLastPage();
|
||||
void querySearch();
|
||||
void continuePageUpdate();
|
||||
|
||||
void handleUser(const User &user);
|
||||
void handleProjects(const Projects &projects);
|
||||
void fetchProjects();
|
||||
|
||||
void cloneSelected();
|
||||
|
||||
Ui::GitLabDialog m_ui;
|
||||
QPushButton *m_clonePB = nullptr;
|
||||
Utils::Id m_currentServerId;
|
||||
Query m_lastTreeViewQuery;
|
||||
PageInformation m_lastPageInformation;
|
||||
int m_currentUserId = -1;
|
||||
};
|
||||
|
||||
} // namespace GitLab
|
||||
268
src/plugins/gitlab/gitlabdialog.ui
Normal file
268
src/plugins/gitlab/gitlabdialog.ui
Normal file
@@ -0,0 +1,268 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>GitLab::GitLabDialog</class>
|
||||
<widget class="QDialog" name="GitLab::GitLabDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>665</width>
|
||||
<height>530</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>GitLab</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_0">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_1">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_0">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="mainLabel">
|
||||
<property name="text">
|
||||
<string>Login</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="detailsLabel">
|
||||
<property name="text">
|
||||
<string>Details</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_0">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Remote:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="remoteCB">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QLabel" name="treeViewTitle">
|
||||
<property name="text">
|
||||
<string>Projects</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_1">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="searchLE">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>Search</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="searchPB">
|
||||
<property name="text">
|
||||
<string>Search</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_6">
|
||||
<item>
|
||||
<widget class="QTreeView" name="treeView">
|
||||
<property name="rootIsDecorated">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="uniformRowHeights">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="itemsExpandable">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="expandsOnDoubleClick">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<attribute name="headerVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<item>
|
||||
<widget class="QToolButton" name="firstTB">
|
||||
<property name="text">
|
||||
<string notr="true">|<</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="previousTB">
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="currentPage">
|
||||
<property name="text">
|
||||
<string>0</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="nextTB">
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="lastTB">
|
||||
<property name="text">
|
||||
<string notr="true">>|</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Close</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>GitLab::GitLabDialog</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>
|
||||
@@ -25,16 +25,27 @@
|
||||
|
||||
#include "gitlabplugin.h"
|
||||
|
||||
#include "gitlabdialog.h"
|
||||
#include "gitlaboptionspage.h"
|
||||
#include "gitlabparameters.h"
|
||||
#include "gitlabprojectsettings.h"
|
||||
|
||||
#include <coreplugin/actionmanager/actioncontainer.h>
|
||||
#include <coreplugin/actionmanager/actionmanager.h>
|
||||
#include <coreplugin/icore.h>
|
||||
#include <git/gitplugin.h>
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectpanelfactory.h>
|
||||
#include <utils/qtcassert.h>
|
||||
|
||||
#include <QAction>
|
||||
#include <QMessageBox>
|
||||
#include <QPointer>
|
||||
|
||||
namespace GitLab {
|
||||
namespace Constants {
|
||||
const char GITLAB_OPEN_VIEW[] = "GitLab.OpenView";
|
||||
} // namespace Constants
|
||||
|
||||
class GitLabPluginPrivate
|
||||
{
|
||||
@@ -42,6 +53,7 @@ public:
|
||||
GitLabParameters parameters;
|
||||
GitLabOptionsPage optionsPage{¶meters};
|
||||
QHash<ProjectExplorer::Project *, GitLabProjectSettings *> projectSettings;
|
||||
QPointer<GitLabDialog> dialog;
|
||||
};
|
||||
|
||||
static GitLabPluginPrivate *dd = nullptr;
|
||||
@@ -71,9 +83,42 @@ bool GitLabPlugin::initialize(const QStringList & /*arguments*/, QString * /*err
|
||||
return new GitLabProjectSettingsWidget(project);
|
||||
});
|
||||
ProjectExplorer::ProjectPanelFactory::registerFactory(panelFactory);
|
||||
QAction *openViewAction = new QAction(tr("GitLab..."), this);
|
||||
auto gitlabCommand = Core::ActionManager::registerAction(openViewAction,
|
||||
Constants::GITLAB_OPEN_VIEW);
|
||||
connect(openViewAction, &QAction::triggered, this, &GitLabPlugin::openView);
|
||||
Core::ActionContainer *ac = Core::ActionManager::actionContainer(Core::Constants::M_TOOLS);
|
||||
ac->addAction(gitlabCommand);
|
||||
connect(&dd->optionsPage, &GitLabOptionsPage::settingsChanged, this, [this] {
|
||||
if (dd->dialog)
|
||||
dd->dialog->updateRemotes();
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
void GitLabPlugin::openView()
|
||||
{
|
||||
if (dd->dialog.isNull()) {
|
||||
while (!dd->parameters.isValid()) {
|
||||
QMessageBox::warning(Core::ICore::dialogParent(), tr("Error"),
|
||||
tr("Invalid GitLab configuration. For a fully functional "
|
||||
"configuration, you need to set up host name or address and "
|
||||
"an access token. Providing the path to curl is mandatory."));
|
||||
if (!Core::ICore::showOptionsDialog("GitLab"))
|
||||
return;
|
||||
}
|
||||
GitLabDialog *gitlabD = new GitLabDialog(Core::ICore::dialogParent());
|
||||
gitlabD->setModal(true);
|
||||
Core::ICore::registerWindow(gitlabD, Core::Context("Git.GitLab"));
|
||||
dd->dialog = gitlabD;
|
||||
}
|
||||
const Qt::WindowStates state = dd->dialog->windowState();
|
||||
if (state & Qt::WindowMinimized)
|
||||
dd->dialog->setWindowState(state & ~Qt::WindowMinimized);
|
||||
dd->dialog->show();
|
||||
dd->dialog->raise();
|
||||
}
|
||||
|
||||
QList<GitLabServer> GitLabPlugin::allGitLabServers()
|
||||
{
|
||||
QTC_ASSERT(dd, return {});
|
||||
|
||||
@@ -52,6 +52,9 @@ public:
|
||||
static GitLabParameters *globalParameters();
|
||||
static GitLabProjectSettings *projectSettings(ProjectExplorer::Project *project);
|
||||
static GitLabOptionsPage *optionsPage();
|
||||
|
||||
private:
|
||||
void openView();
|
||||
};
|
||||
|
||||
} // namespace GitLab
|
||||
|
||||
@@ -294,6 +294,7 @@ void GitLabProjectSettingsWidget::updateEnabledStates()
|
||||
const bool isGitRepository = m_hostCB->count() > 0;
|
||||
const bool hasGitLabServers = m_linkedGitLabServer->count();
|
||||
const bool linked = m_projectSettings->isLinked();
|
||||
|
||||
m_linkedGitLabServer->setEnabled(isGitRepository && !linked);
|
||||
m_hostCB->setEnabled(isGitRepository && !linked);
|
||||
m_linkWithGitLab->setEnabled(isGitRepository && !linked && hasGitLabServers);
|
||||
|
||||
@@ -41,6 +41,8 @@ namespace GitLab {
|
||||
|
||||
const char API_PREFIX[] = "/api/v4";
|
||||
const char QUERY_PROJECT[] = "/projects/%1";
|
||||
const char QUERY_PROJECTS[] = "/projects?simple=true";
|
||||
const char QUERY_USER[] = "/user";
|
||||
|
||||
Query::Query(Type type, const QStringList ¶meter)
|
||||
: m_type(type)
|
||||
@@ -48,6 +50,21 @@ Query::Query(Type type, const QStringList ¶meter)
|
||||
{
|
||||
}
|
||||
|
||||
void Query::setPageParameter(int page)
|
||||
{
|
||||
m_pageParameter = page;
|
||||
}
|
||||
|
||||
void Query::setAdditionalParameters(const QStringList &additional)
|
||||
{
|
||||
m_additionalParameters = additional;
|
||||
}
|
||||
|
||||
bool Query::hasPaginatedResults() const
|
||||
{
|
||||
return m_type == Query::Projects;
|
||||
}
|
||||
|
||||
QString Query::toString() const
|
||||
{
|
||||
QString query = API_PREFIX;
|
||||
@@ -59,6 +76,20 @@ QString Query::toString() const
|
||||
query += QLatin1String(QUERY_PROJECT).arg(QLatin1String(
|
||||
QUrl::toPercentEncoding(m_parameter.at(0))));
|
||||
break;
|
||||
case Query::Projects:
|
||||
query += QLatin1String(QUERY_PROJECTS);
|
||||
break;
|
||||
case Query::User:
|
||||
query += QUERY_USER;
|
||||
break;
|
||||
}
|
||||
if (m_pageParameter > 0) {
|
||||
query.append(m_type == Query::Projects ? '&' : '?');
|
||||
query.append("page=").append(QString::number(m_pageParameter));
|
||||
}
|
||||
if (!m_additionalParameters.isEmpty()) {
|
||||
query.append((m_type == Query::Projects || m_pageParameter > 0) ? '&' : '?');
|
||||
query.append(m_additionalParameters.join('&'));
|
||||
}
|
||||
return query;
|
||||
}
|
||||
@@ -69,6 +100,9 @@ QueryRunner::QueryRunner(const Query &query, const Utils::Id &id, QObject *paren
|
||||
const GitLabParameters *p = GitLabPlugin::globalParameters();
|
||||
const auto server = p->serverForId(id);
|
||||
QStringList args = server.curlArguments();
|
||||
m_paginated = query.hasPaginatedResults();
|
||||
if (m_paginated)
|
||||
args << "-i";
|
||||
if (!server.token.isEmpty())
|
||||
args << "--header" << "PRIVATE-TOKEN: " + server.token;
|
||||
QString url = "https://" + server.host;
|
||||
|
||||
@@ -38,15 +38,23 @@ class Query
|
||||
public:
|
||||
enum Type {
|
||||
NoQuery,
|
||||
Project
|
||||
User,
|
||||
Project,
|
||||
Projects
|
||||
};
|
||||
|
||||
explicit Query(Type type, const QStringList ¶meters = {});
|
||||
void setPageParameter(int page);
|
||||
void setAdditionalParameters(const QStringList &additional);
|
||||
bool hasPaginatedResults() const;
|
||||
Type type() const { return m_type; }
|
||||
QString toString() const;
|
||||
|
||||
private:
|
||||
Type m_type = NoQuery;
|
||||
QStringList m_parameter;
|
||||
QStringList m_additionalParameters;
|
||||
int m_pageParameter = -1;
|
||||
};
|
||||
|
||||
class QueryRunner : public QObject
|
||||
@@ -70,6 +78,7 @@ private:
|
||||
|
||||
Utils::QtcProcess m_process;
|
||||
bool m_running = false;
|
||||
bool m_paginated = false;
|
||||
};
|
||||
|
||||
} // namespace GitLab
|
||||
|
||||
@@ -34,6 +34,38 @@
|
||||
namespace GitLab {
|
||||
namespace ResultParser {
|
||||
|
||||
static PageInformation paginationInformation(const QByteArray &header)
|
||||
{
|
||||
PageInformation result;
|
||||
const QByteArrayList lines = header.split('\n');
|
||||
for (const QByteArray &line : lines) {
|
||||
const QByteArray lower = line.toLower(); // depending on OS this may be capitalized
|
||||
if (lower.startsWith("x-page: "))
|
||||
result.currentPage = line.mid(8).toInt();
|
||||
else if (lower.startsWith("x-per-page: "))
|
||||
result.perPage = line.mid(12).toInt();
|
||||
else if (lower.startsWith("x-total: "))
|
||||
result.total = line.mid(9).toInt();
|
||||
else if (lower.startsWith("x-total-pages: "))
|
||||
result.totalPages = line.mid(15).toInt();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::pair<QByteArray, QByteArray> splitHeaderAndBody(const QByteArray &input)
|
||||
{
|
||||
QByteArray header;
|
||||
QByteArray json;
|
||||
int emptyLine = input.indexOf("\r\n\r\n"); // we always get \r\n as line separator?
|
||||
if (emptyLine != -1) {
|
||||
header = input.left(emptyLine);
|
||||
json = input.mid(emptyLine + 4);
|
||||
} else {
|
||||
json = input;
|
||||
}
|
||||
return std::make_pair(header, json);
|
||||
}
|
||||
|
||||
static std::pair<Error, QJsonObject> preHandleSingle(const QByteArray &json)
|
||||
{
|
||||
Error result;
|
||||
@@ -59,6 +91,52 @@ static std::pair<Error, QJsonObject> preHandleSingle(const QByteArray &json)
|
||||
return std::make_pair(result, object);
|
||||
}
|
||||
|
||||
static std::pair<Error, QJsonDocument> preHandleHeaderAndBody(const QByteArray &header,
|
||||
const QByteArray &json)
|
||||
{
|
||||
Error result;
|
||||
if (header.isEmpty()) {
|
||||
result.message = "Missing Expected Header";
|
||||
return std::make_pair(result, QJsonDocument());
|
||||
}
|
||||
|
||||
QJsonParseError error;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(json, &error);
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
result.message = error.errorString();
|
||||
return std::make_pair(result, doc);
|
||||
}
|
||||
|
||||
if (doc.isObject()) {
|
||||
const QJsonObject obj = doc.object();
|
||||
if (obj.contains("message")) {
|
||||
result = parseErrorMessage(obj.value("message").toString());
|
||||
return std::make_pair(result, doc);
|
||||
} else if (obj.contains("error")) {
|
||||
if (obj.value("error").toString() == "insufficient_scope")
|
||||
result.code = 1;
|
||||
result.message = obj.value("error_description").toString();
|
||||
return std::make_pair(result, doc);
|
||||
}
|
||||
}
|
||||
|
||||
if (!doc.isArray())
|
||||
result.message = "Not an Array";
|
||||
|
||||
return std::make_pair(result, doc);
|
||||
}
|
||||
|
||||
static User userFromJson(const QJsonObject &jsonObj)
|
||||
{
|
||||
User user;
|
||||
user.name = jsonObj.value("username").toString();
|
||||
user.realname = jsonObj.value("name").toString();
|
||||
user.id = jsonObj.value("id").toInt(-1);
|
||||
user.email = jsonObj.value("email").toString();
|
||||
user.bot = jsonObj.value("bot").toBool();
|
||||
return user;
|
||||
}
|
||||
|
||||
static Project projectFromJson(const QJsonObject &jsonObj)
|
||||
{
|
||||
Project project;
|
||||
@@ -67,6 +145,8 @@ static Project projectFromJson(const QJsonObject &jsonObj)
|
||||
project.pathName = jsonObj.value("path_with_namespace").toString();
|
||||
project.id = jsonObj.value("id").toInt(-1);
|
||||
project.visibility = jsonObj.value("visibility").toString("public");
|
||||
project.httpUrl = jsonObj.value("http_url_to_repo").toString();
|
||||
project.sshUrl = jsonObj.value("ssh_url_to_repo").toString();
|
||||
if (jsonObj.contains("forks_count"))
|
||||
project.forkCount = jsonObj.value("forks_count").toInt();
|
||||
if (jsonObj.contains("star_count"))
|
||||
@@ -82,6 +162,17 @@ static Project projectFromJson(const QJsonObject &jsonObj)
|
||||
return project;
|
||||
}
|
||||
|
||||
User parseUser(const QByteArray &input)
|
||||
{
|
||||
auto [error, userObj] = preHandleSingle(input);
|
||||
if (!error.message.isEmpty()) {
|
||||
User result;
|
||||
result.error = error;
|
||||
return result;
|
||||
}
|
||||
return userFromJson(userObj);
|
||||
}
|
||||
|
||||
Project parseProject(const QByteArray &input)
|
||||
{
|
||||
auto [error, projectObj] = preHandleSingle(input);
|
||||
@@ -93,6 +184,26 @@ Project parseProject(const QByteArray &input)
|
||||
return projectFromJson(projectObj);
|
||||
}
|
||||
|
||||
Projects parseProjects(const QByteArray &input)
|
||||
{
|
||||
auto [header, json] = splitHeaderAndBody(input);
|
||||
auto [error, jsonDoc] = preHandleHeaderAndBody(header, json);
|
||||
Projects result;
|
||||
if (!error.message.isEmpty()) {
|
||||
result.error = error;
|
||||
return result;
|
||||
}
|
||||
result.pageInfo = paginationInformation(header);
|
||||
const QJsonArray projectsArray = jsonDoc.array();
|
||||
for (const QJsonValue &value : projectsArray) {
|
||||
if (!value.isObject())
|
||||
continue;
|
||||
const QJsonObject projectObj = value.toObject();
|
||||
result.projects.append(projectFromJson(projectObj));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Error parseErrorMessage(const QString &message)
|
||||
{
|
||||
Error error;
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QList>
|
||||
#include <QMetaType>
|
||||
#include <QString>
|
||||
|
||||
namespace GitLab {
|
||||
@@ -35,6 +37,26 @@ struct Error
|
||||
QString message;
|
||||
};
|
||||
|
||||
class PageInformation
|
||||
{
|
||||
public:
|
||||
int currentPage = -1;
|
||||
int totalPages = -1;
|
||||
int perPage = -1;
|
||||
int total = -1;
|
||||
};
|
||||
|
||||
class User
|
||||
{
|
||||
public:
|
||||
QString name;
|
||||
QString realname;
|
||||
QString email;
|
||||
Error error;
|
||||
int id = -1;
|
||||
bool bot = false;
|
||||
};
|
||||
|
||||
class Project
|
||||
{
|
||||
public:
|
||||
@@ -42,18 +64,32 @@ public:
|
||||
QString displayName;
|
||||
QString pathName;
|
||||
QString visibility;
|
||||
QString httpUrl;
|
||||
QString sshUrl;
|
||||
Error error;
|
||||
int id = -1;
|
||||
int starCount = -1;
|
||||
int forkCount = -1;
|
||||
int issuesCount = -1;
|
||||
int accessLevel = -1; // 40 maintainer, 30 developer, 20 reporter, 10 guest
|
||||
int accessLevel = -1; // 50 owner, 40 maintainer, 30 developer, 20 reporter, 10 guest
|
||||
};
|
||||
|
||||
class Projects
|
||||
{
|
||||
public:
|
||||
QList<Project> projects;
|
||||
Error error;
|
||||
PageInformation pageInfo;
|
||||
};
|
||||
|
||||
namespace ResultParser {
|
||||
|
||||
User parseUser(const QByteArray &input);
|
||||
Project parseProject(const QByteArray &input);
|
||||
Projects parseProjects(const QByteArray &input);
|
||||
Error parseErrorMessage(const QString &message);
|
||||
|
||||
} // namespace ResultParser
|
||||
} // namespace GitLab
|
||||
|
||||
Q_DECLARE_METATYPE(GitLab::Project)
|
||||
|
||||
Reference in New Issue
Block a user