forked from qt-creator/qt-creator
GitLab: Allow fetching events
Projects that are linked to a GitLab instance will now fetch notifications for this project and print them to the vcs output pane. Change-Id: Ifb960e64b30a260327efb28a3dfd26f6457503a0 Reviewed-by: <github-actions-qt-creator@cristianadam.eu> Reviewed-by: David Schulz <david.schulz@qt.io>
This commit is contained in:
@@ -29,6 +29,8 @@
|
||||
#include "gitlaboptionspage.h"
|
||||
#include "gitlabparameters.h"
|
||||
#include "gitlabprojectsettings.h"
|
||||
#include "queryrunner.h"
|
||||
#include "resultparser.h"
|
||||
|
||||
#include <coreplugin/actionmanager/actioncontainer.h>
|
||||
#include <coreplugin/actionmanager/actionmanager.h>
|
||||
@@ -36,24 +38,39 @@
|
||||
#include <git/gitplugin.h>
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectpanelfactory.h>
|
||||
#include <projectexplorer/session.h>
|
||||
#include <utils/qtcassert.h>
|
||||
#include <vcsbase/vcsoutputwindow.h>
|
||||
|
||||
#include <QAction>
|
||||
#include <QMessageBox>
|
||||
#include <QPointer>
|
||||
#include <QTimer>
|
||||
|
||||
namespace GitLab {
|
||||
namespace Constants {
|
||||
const char GITLAB_OPEN_VIEW[] = "GitLab.OpenView";
|
||||
} // namespace Constants
|
||||
|
||||
class GitLabPluginPrivate
|
||||
class GitLabPluginPrivate : public QObject
|
||||
{
|
||||
public:
|
||||
GitLabParameters parameters;
|
||||
GitLabOptionsPage optionsPage{¶meters};
|
||||
QHash<ProjectExplorer::Project *, GitLabProjectSettings *> projectSettings;
|
||||
QPointer<GitLabDialog> dialog;
|
||||
|
||||
QTimer notificationTimer;
|
||||
QString projectName;
|
||||
Utils::Id serverId;
|
||||
bool runningQuery = false;
|
||||
|
||||
void setupNotificationTimer();
|
||||
void fetchEvents();
|
||||
void fetchUser();
|
||||
void createAndSendEventsRequest(const QDateTime timeStamp, int page = -1);
|
||||
void handleUser(const User &user);
|
||||
void handleEvents(const Events &events, const QDateTime &timeStamp);
|
||||
};
|
||||
|
||||
static GitLabPluginPrivate *dd = nullptr;
|
||||
@@ -93,6 +110,9 @@ bool GitLabPlugin::initialize(const QStringList & /*arguments*/, QString * /*err
|
||||
if (dd->dialog)
|
||||
dd->dialog->updateRemotes();
|
||||
});
|
||||
connect(ProjectExplorer::SessionManager::instance(),
|
||||
&ProjectExplorer::SessionManager::startupProjectChanged,
|
||||
this, &GitLabPlugin::onStartupProjectChanged);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -119,6 +139,141 @@ void GitLabPlugin::openView()
|
||||
dd->dialog->raise();
|
||||
}
|
||||
|
||||
void GitLabPlugin::onStartupProjectChanged()
|
||||
{
|
||||
QTC_ASSERT(dd, return);
|
||||
disconnect(&dd->notificationTimer);
|
||||
ProjectExplorer::Project *project = ProjectExplorer::SessionManager::startupProject();
|
||||
if (!project) {
|
||||
dd->notificationTimer.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
const GitLabProjectSettings *projSettings = projectSettings(project);
|
||||
if (!projSettings->isLinked()) {
|
||||
dd->notificationTimer.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
dd->fetchEvents();
|
||||
dd->setupNotificationTimer();
|
||||
}
|
||||
|
||||
void GitLabPluginPrivate::setupNotificationTimer()
|
||||
{
|
||||
// make interval configurable?
|
||||
notificationTimer.setInterval(15 * 60 * 1000);
|
||||
QObject::connect(¬ificationTimer, &QTimer::timeout, this, &GitLabPluginPrivate::fetchEvents);
|
||||
notificationTimer.start();
|
||||
}
|
||||
|
||||
void GitLabPluginPrivate::fetchEvents()
|
||||
{
|
||||
ProjectExplorer::Project *project = ProjectExplorer::SessionManager::startupProject();
|
||||
QTC_ASSERT(project, return);
|
||||
|
||||
if (runningQuery)
|
||||
return;
|
||||
|
||||
const GitLabProjectSettings *projSettings = GitLabPlugin::projectSettings(project);
|
||||
projectName = projSettings->currentProject();
|
||||
serverId = projSettings->currentServer();
|
||||
|
||||
const QDateTime lastRequest = projSettings->lastRequest();
|
||||
if (!lastRequest.isValid()) { // we haven't queried events for this project yet
|
||||
fetchUser();
|
||||
return;
|
||||
}
|
||||
createAndSendEventsRequest(lastRequest);
|
||||
}
|
||||
|
||||
void GitLabPluginPrivate::fetchUser()
|
||||
{
|
||||
if (runningQuery)
|
||||
return;
|
||||
|
||||
const Query query(Query::User);
|
||||
QueryRunner *runner = new QueryRunner(query, serverId, this);
|
||||
QObject::connect(runner, &QueryRunner::resultRetrieved, this, [this](const QByteArray &result) {
|
||||
handleUser(ResultParser::parseUser(result));
|
||||
});
|
||||
QObject::connect(runner, &QueryRunner::finished, [runner]() { runner->deleteLater(); });
|
||||
runningQuery = true;
|
||||
runner->start();
|
||||
}
|
||||
|
||||
void GitLabPluginPrivate::createAndSendEventsRequest(const QDateTime timeStamp, int page)
|
||||
{
|
||||
if (runningQuery)
|
||||
return;
|
||||
|
||||
Query query(Query::Events, {projectName});
|
||||
QStringList additional = {"sort=asc"};
|
||||
|
||||
QDateTime after = timeStamp.addDays(-1);
|
||||
additional.append(QLatin1String("after=%1").arg(after.toString("yyyy-MM-dd")));
|
||||
query.setAdditionalParameters(additional);
|
||||
|
||||
if (page > 1)
|
||||
query.setPageParameter(page);
|
||||
|
||||
QueryRunner *runner = new QueryRunner(query, serverId, this);
|
||||
QObject::connect(runner, &QueryRunner::resultRetrieved, this,
|
||||
[this, timeStamp](const QByteArray &result) {
|
||||
handleEvents(ResultParser::parseEvents(result), timeStamp);
|
||||
});
|
||||
QObject::connect(runner, &QueryRunner::finished, [runner]() { runner->deleteLater(); });
|
||||
runningQuery = true;
|
||||
runner->start();
|
||||
}
|
||||
|
||||
void GitLabPluginPrivate::handleUser(const User &user)
|
||||
{
|
||||
runningQuery = false;
|
||||
|
||||
QTC_ASSERT(user.error.message.isEmpty(), return);
|
||||
const QDateTime timeStamp = QDateTime::fromString(user.lastLogin, Qt::ISODateWithMs);
|
||||
createAndSendEventsRequest(timeStamp);
|
||||
}
|
||||
|
||||
void GitLabPluginPrivate::handleEvents(const Events &events, const QDateTime &timeStamp)
|
||||
{
|
||||
runningQuery = false;
|
||||
|
||||
ProjectExplorer::Project *project = ProjectExplorer::SessionManager::startupProject();
|
||||
QTC_ASSERT(project, return);
|
||||
|
||||
GitLabProjectSettings *projSettings = GitLabPlugin::projectSettings(project);
|
||||
QTC_ASSERT(projSettings->currentProject() == projectName, return);
|
||||
|
||||
if (!projSettings->isLinked()) // link state has changed meanwhile - ignore the request
|
||||
return;
|
||||
|
||||
if (!events.error.message.isEmpty()) {
|
||||
VcsBase::VcsOutputWindow::appendError("GitLab: Error while fetching events. "
|
||||
+ events.error.message + '\n');
|
||||
return;
|
||||
}
|
||||
|
||||
QDateTime lastTimeStamp;
|
||||
for (const Event &event : events.events) {
|
||||
const QDateTime eventTimeStamp = QDateTime::fromString(event.timeStamp, Qt::ISODateWithMs);
|
||||
if (!timeStamp.isValid() || timeStamp < eventTimeStamp) {
|
||||
VcsBase::VcsOutputWindow::appendMessage("GitLab: " + event.toMessage());
|
||||
if (!lastTimeStamp.isValid() || lastTimeStamp < eventTimeStamp)
|
||||
lastTimeStamp = eventTimeStamp;
|
||||
}
|
||||
}
|
||||
if (lastTimeStamp.isValid()) {
|
||||
if (auto outputWindow = VcsBase::VcsOutputWindow::instance())
|
||||
outputWindow->flash();
|
||||
projSettings->setLastRequest(lastTimeStamp);
|
||||
}
|
||||
|
||||
if (events.pageInfo.currentPage < events.pageInfo.totalPages)
|
||||
createAndSendEventsRequest(timeStamp, events.pageInfo.currentPage + 1);
|
||||
}
|
||||
|
||||
QList<GitLabServer> GitLabPlugin::allGitLabServers()
|
||||
{
|
||||
QTC_ASSERT(dd, return {});
|
||||
@@ -152,4 +307,28 @@ GitLabOptionsPage *GitLabPlugin::optionsPage()
|
||||
return &dd->optionsPage;
|
||||
}
|
||||
|
||||
void GitLabPlugin::linkedStateChanged(bool enabled)
|
||||
{
|
||||
QTC_ASSERT(dd, return);
|
||||
|
||||
ProjectExplorer::Project *project = ProjectExplorer::SessionManager::startupProject();
|
||||
if (project) {
|
||||
const GitLabProjectSettings *pSettings = projectSettings(project);
|
||||
dd->serverId = pSettings->currentServer();
|
||||
dd->projectName = pSettings->currentProject();
|
||||
} else {
|
||||
dd->serverId = Utils::Id();
|
||||
dd->projectName = QString();
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
dd->fetchEvents();
|
||||
dd->setupNotificationTimer();
|
||||
} else {
|
||||
QObject::disconnect(&dd->notificationTimer, &QTimer::timeout,
|
||||
dd, &GitLabPluginPrivate::fetchEvents);
|
||||
dd->notificationTimer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace GitLab
|
||||
|
||||
@@ -33,6 +33,7 @@ namespace ProjectExplorer { class Project; }
|
||||
|
||||
namespace GitLab {
|
||||
|
||||
class Events;
|
||||
class GitLabProjectSettings;
|
||||
class GitLabOptionsPage;
|
||||
|
||||
@@ -53,8 +54,10 @@ public:
|
||||
static GitLabProjectSettings *projectSettings(ProjectExplorer::Project *project);
|
||||
static GitLabOptionsPage *optionsPage();
|
||||
|
||||
static void linkedStateChanged(bool enabled);
|
||||
private:
|
||||
void openView();
|
||||
void onStartupProjectChanged();
|
||||
};
|
||||
|
||||
} // namespace GitLab
|
||||
|
||||
@@ -49,6 +49,7 @@ namespace GitLab {
|
||||
const char PSK_LINKED_ID[] = "GitLab.LinkedId";
|
||||
const char PSK_SERVER[] = "GitLab.Server";
|
||||
const char PSK_PROJECT[] = "GitLab.Project";
|
||||
const char PSK_LAST_REQ[] = "GitLab.LastRequest";
|
||||
|
||||
static QString accessLevelString(int accessLevel)
|
||||
{
|
||||
@@ -106,6 +107,7 @@ void GitLabProjectSettings::load()
|
||||
m_id = Utils::Id::fromSetting(m_project->namedSettings(PSK_LINKED_ID));
|
||||
m_host = m_project->namedSettings(PSK_SERVER).toString();
|
||||
m_currentProject = m_project->namedSettings(PSK_PROJECT).toString();
|
||||
m_lastRequest = m_project->namedSettings(PSK_LAST_REQ).toDateTime();
|
||||
|
||||
// may still be wrong, but we avoid an additional request by just doing sanity check here
|
||||
if (!m_id.isValid() || m_host.isEmpty())
|
||||
@@ -124,6 +126,7 @@ void GitLabProjectSettings::save()
|
||||
m_project->setNamedSettings(PSK_SERVER, QString());
|
||||
}
|
||||
m_project->setNamedSettings(PSK_PROJECT, m_currentProject);
|
||||
m_project->setNamedSettings(PSK_LAST_REQ, m_lastRequest);
|
||||
}
|
||||
|
||||
GitLabProjectSettingsWidget::GitLabProjectSettingsWidget(ProjectExplorer::Project *project,
|
||||
@@ -184,6 +187,7 @@ void GitLabProjectSettingsWidget::unlink()
|
||||
m_projectSettings->setLinked(false);
|
||||
m_projectSettings->setCurrentProject({});
|
||||
updateEnabledStates();
|
||||
GitLabPlugin::linkedStateChanged(false);
|
||||
}
|
||||
|
||||
void GitLabProjectSettingsWidget::checkConnection(CheckMode mode)
|
||||
@@ -245,6 +249,7 @@ void GitLabProjectSettingsWidget::onConnectionChecked(const Project &project,
|
||||
m_projectSettings->setCurrentServerHost(remote);
|
||||
m_projectSettings->setLinked(true);
|
||||
m_projectSettings->setCurrentProject(projectName);
|
||||
GitLabPlugin::linkedStateChanged(true);
|
||||
}
|
||||
updateEnabledStates();
|
||||
}
|
||||
@@ -282,8 +287,10 @@ void GitLabProjectSettingsWidget::updateUi()
|
||||
m_hostCB->setCurrentIndex(m_hostCB->findData(QVariant::fromValue(serverHost)));
|
||||
m_linkedGitLabServer->setCurrentIndex(
|
||||
m_linkedGitLabServer->findData(QVariant::fromValue(server)));
|
||||
GitLabPlugin::linkedStateChanged(true);
|
||||
} else {
|
||||
m_projectSettings->setLinked(false);
|
||||
GitLabPlugin::linkedStateChanged(false);
|
||||
}
|
||||
}
|
||||
updateEnabledStates();
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <projectexplorer/projectsettingswidget.h>
|
||||
#include <utils/id.h>
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QObject>
|
||||
#include <QWidget>
|
||||
|
||||
@@ -59,9 +60,12 @@ public:
|
||||
QString currentProject() const { return m_currentProject; }
|
||||
bool isLinked() const { return m_linked; }
|
||||
void setLinked(bool linked);
|
||||
QDateTime lastRequest() const { return m_lastRequest; }
|
||||
void setLastRequest(const QDateTime &lastRequest) { m_lastRequest = lastRequest; }
|
||||
ProjectExplorer::Project *project() const { return m_project; }
|
||||
|
||||
static std::tuple<QString, QString, int> remotePartsFromRemote(const QString &remote);
|
||||
|
||||
private:
|
||||
void load();
|
||||
void save();
|
||||
@@ -69,6 +73,7 @@ private:
|
||||
ProjectExplorer::Project *m_project = nullptr;
|
||||
QString m_host;
|
||||
Utils::Id m_id;
|
||||
QDateTime m_lastRequest;
|
||||
QString m_currentProject;
|
||||
bool m_linked = false;
|
||||
};
|
||||
|
||||
@@ -43,6 +43,7 @@ const char API_PREFIX[] = "/api/v4";
|
||||
const char QUERY_PROJECT[] = "/projects/%1";
|
||||
const char QUERY_PROJECTS[] = "/projects?simple=true";
|
||||
const char QUERY_USER[] = "/user";
|
||||
const char QUERY_EVENTS[] = "/projects/%1/events";
|
||||
|
||||
Query::Query(Type type, const QStringList ¶meter)
|
||||
: m_type(type)
|
||||
@@ -62,7 +63,7 @@ void Query::setAdditionalParameters(const QStringList &additional)
|
||||
|
||||
bool Query::hasPaginatedResults() const
|
||||
{
|
||||
return m_type == Query::Projects;
|
||||
return m_type == Query::Projects || m_type == Query::Events;
|
||||
}
|
||||
|
||||
QString Query::toString() const
|
||||
@@ -82,6 +83,11 @@ QString Query::toString() const
|
||||
case Query::User:
|
||||
query += QUERY_USER;
|
||||
break;
|
||||
case Query::Events:
|
||||
QTC_ASSERT(!m_parameter.isEmpty(), return {});
|
||||
query += QLatin1String(QUERY_EVENTS).arg(QLatin1String(
|
||||
QUrl::toPercentEncoding(m_parameter.at(0))));
|
||||
break;
|
||||
}
|
||||
if (m_pageParameter > 0) {
|
||||
query.append(m_type == Query::Projects ? '&' : '?');
|
||||
|
||||
@@ -40,7 +40,8 @@ public:
|
||||
NoQuery,
|
||||
User,
|
||||
Project,
|
||||
Projects
|
||||
Projects,
|
||||
Events
|
||||
};
|
||||
|
||||
explicit Query(Type type, const QStringList ¶meters = {});
|
||||
|
||||
@@ -32,6 +32,25 @@
|
||||
#include <utility>
|
||||
|
||||
namespace GitLab {
|
||||
|
||||
|
||||
QString Event::toMessage() const
|
||||
{
|
||||
QString message;
|
||||
if (author.realname.isEmpty())
|
||||
message.append(author.name);
|
||||
else
|
||||
message.append(author.realname + " (" + author.name + ')');
|
||||
message.append(' ');
|
||||
if (!pushData.isEmpty())
|
||||
message.append(pushData);
|
||||
else if (!targetTitle.isEmpty())
|
||||
message.append(action + ' ' + targetType + " '" + targetTitle +'\'');
|
||||
else
|
||||
message.append(action + ' ' + targetType);
|
||||
return message;
|
||||
}
|
||||
|
||||
namespace ResultParser {
|
||||
|
||||
static PageInformation paginationInformation(const QByteArray &header)
|
||||
@@ -133,6 +152,7 @@ static User userFromJson(const QJsonObject &jsonObj)
|
||||
user.realname = jsonObj.value("name").toString();
|
||||
user.id = jsonObj.value("id").toInt(-1);
|
||||
user.email = jsonObj.value("email").toString();
|
||||
user.lastLogin = jsonObj.value("last_sign_in_at").toString();
|
||||
user.bot = jsonObj.value("bot").toBool();
|
||||
return user;
|
||||
}
|
||||
@@ -162,6 +182,31 @@ static Project projectFromJson(const QJsonObject &jsonObj)
|
||||
return project;
|
||||
}
|
||||
|
||||
static Event eventFromJson(const QJsonObject &jsonObj)
|
||||
{
|
||||
Event event;
|
||||
event.action = jsonObj.value("action_name").toString();
|
||||
const QJsonValue value = jsonObj.value("target_type");
|
||||
event.targetType = value.isNull() ? "project" : jsonObj.value("target_type").toString();
|
||||
if (event.targetType == "DiffNote") {
|
||||
const QJsonObject noteObject = jsonObj.value("note").toObject();
|
||||
event.targetType = noteObject.value("noteable_type").toString();
|
||||
}
|
||||
event.targetTitle = jsonObj.value("target_title").toString();
|
||||
event.author = userFromJson(jsonObj.value("author").toObject());
|
||||
event.timeStamp = jsonObj.value("created_at").toString();
|
||||
if (jsonObj.contains("push_data")) {
|
||||
const QJsonObject pushDataObj = jsonObj.value("push_data").toObject();
|
||||
if (!pushDataObj.isEmpty()) {
|
||||
const QString action = pushDataObj.value("action").toString();
|
||||
const QString ref = pushDataObj.value("ref").toString();
|
||||
const QString refType = pushDataObj.value("ref_type").toString();
|
||||
event.pushData = action + ' ' + refType + " '" + ref + '\'';
|
||||
}
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
User parseUser(const QByteArray &input)
|
||||
{
|
||||
auto [error, userObj] = preHandleSingle(input);
|
||||
@@ -204,6 +249,27 @@ Projects parseProjects(const QByteArray &input)
|
||||
return result;
|
||||
}
|
||||
|
||||
Events parseEvents(const QByteArray &input)
|
||||
{
|
||||
auto [header, json] = splitHeaderAndBody(input);
|
||||
auto [error, jsonDoc] = preHandleHeaderAndBody(header, json);
|
||||
Events result;
|
||||
if (!error.message.isEmpty()) {
|
||||
result.error = error;
|
||||
return result;
|
||||
}
|
||||
result.pageInfo = paginationInformation(header);
|
||||
const QJsonArray eventsArray = jsonDoc.array();
|
||||
for (const QJsonValue &value : eventsArray) {
|
||||
if (!value.isObject())
|
||||
continue;
|
||||
const QJsonObject eventObj = value.toObject();
|
||||
result.events.append(eventFromJson(eventObj));
|
||||
}
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
Error parseErrorMessage(const QString &message)
|
||||
{
|
||||
Error error;
|
||||
|
||||
@@ -52,6 +52,7 @@ public:
|
||||
QString name;
|
||||
QString realname;
|
||||
QString email;
|
||||
QString lastLogin;
|
||||
Error error;
|
||||
int id = -1;
|
||||
bool bot = false;
|
||||
@@ -82,11 +83,34 @@ public:
|
||||
PageInformation pageInfo;
|
||||
};
|
||||
|
||||
class Event
|
||||
{
|
||||
public:
|
||||
QString action;
|
||||
QString targetType;
|
||||
QString targetTitle;
|
||||
QString timeStamp;
|
||||
QString pushData;
|
||||
User author;
|
||||
Error error;
|
||||
|
||||
QString toMessage() const;
|
||||
};
|
||||
|
||||
class Events
|
||||
{
|
||||
public:
|
||||
QList<Event> events;
|
||||
Error error;
|
||||
PageInformation pageInfo;
|
||||
};
|
||||
|
||||
namespace ResultParser {
|
||||
|
||||
User parseUser(const QByteArray &input);
|
||||
Project parseProject(const QByteArray &input);
|
||||
Projects parseProjects(const QByteArray &input);
|
||||
Events parseEvents(const QByteArray &input);
|
||||
Error parseErrorMessage(const QString &message);
|
||||
|
||||
} // namespace ResultParser
|
||||
|
||||
Reference in New Issue
Block a user