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:
Christian Stenger
2022-05-12 13:54:00 +02:00
parent cd1af2864b
commit dd27901759
8 changed files with 294 additions and 3 deletions

View File

@@ -29,6 +29,8 @@
#include "gitlaboptionspage.h" #include "gitlaboptionspage.h"
#include "gitlabparameters.h" #include "gitlabparameters.h"
#include "gitlabprojectsettings.h" #include "gitlabprojectsettings.h"
#include "queryrunner.h"
#include "resultparser.h"
#include <coreplugin/actionmanager/actioncontainer.h> #include <coreplugin/actionmanager/actioncontainer.h>
#include <coreplugin/actionmanager/actionmanager.h> #include <coreplugin/actionmanager/actionmanager.h>
@@ -36,24 +38,39 @@
#include <git/gitplugin.h> #include <git/gitplugin.h>
#include <projectexplorer/project.h> #include <projectexplorer/project.h>
#include <projectexplorer/projectpanelfactory.h> #include <projectexplorer/projectpanelfactory.h>
#include <projectexplorer/session.h>
#include <utils/qtcassert.h> #include <utils/qtcassert.h>
#include <vcsbase/vcsoutputwindow.h>
#include <QAction> #include <QAction>
#include <QMessageBox> #include <QMessageBox>
#include <QPointer> #include <QPointer>
#include <QTimer>
namespace GitLab { namespace GitLab {
namespace Constants { namespace Constants {
const char GITLAB_OPEN_VIEW[] = "GitLab.OpenView"; const char GITLAB_OPEN_VIEW[] = "GitLab.OpenView";
} // namespace Constants } // namespace Constants
class GitLabPluginPrivate class GitLabPluginPrivate : public QObject
{ {
public: public:
GitLabParameters parameters; GitLabParameters parameters;
GitLabOptionsPage optionsPage{&parameters}; GitLabOptionsPage optionsPage{&parameters};
QHash<ProjectExplorer::Project *, GitLabProjectSettings *> projectSettings; QHash<ProjectExplorer::Project *, GitLabProjectSettings *> projectSettings;
QPointer<GitLabDialog> dialog; 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; static GitLabPluginPrivate *dd = nullptr;
@@ -93,6 +110,9 @@ bool GitLabPlugin::initialize(const QStringList & /*arguments*/, QString * /*err
if (dd->dialog) if (dd->dialog)
dd->dialog->updateRemotes(); dd->dialog->updateRemotes();
}); });
connect(ProjectExplorer::SessionManager::instance(),
&ProjectExplorer::SessionManager::startupProjectChanged,
this, &GitLabPlugin::onStartupProjectChanged);
return true; return true;
} }
@@ -119,6 +139,141 @@ void GitLabPlugin::openView()
dd->dialog->raise(); 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(&notificationTimer, &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() QList<GitLabServer> GitLabPlugin::allGitLabServers()
{ {
QTC_ASSERT(dd, return {}); QTC_ASSERT(dd, return {});
@@ -152,4 +307,28 @@ GitLabOptionsPage *GitLabPlugin::optionsPage()
return &dd->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 } // namespace GitLab

View File

@@ -33,6 +33,7 @@ namespace ProjectExplorer { class Project; }
namespace GitLab { namespace GitLab {
class Events;
class GitLabProjectSettings; class GitLabProjectSettings;
class GitLabOptionsPage; class GitLabOptionsPage;
@@ -53,8 +54,10 @@ public:
static GitLabProjectSettings *projectSettings(ProjectExplorer::Project *project); static GitLabProjectSettings *projectSettings(ProjectExplorer::Project *project);
static GitLabOptionsPage *optionsPage(); static GitLabOptionsPage *optionsPage();
static void linkedStateChanged(bool enabled);
private: private:
void openView(); void openView();
void onStartupProjectChanged();
}; };
} // namespace GitLab } // namespace GitLab

View File

@@ -49,6 +49,7 @@ namespace GitLab {
const char PSK_LINKED_ID[] = "GitLab.LinkedId"; const char PSK_LINKED_ID[] = "GitLab.LinkedId";
const char PSK_SERVER[] = "GitLab.Server"; const char PSK_SERVER[] = "GitLab.Server";
const char PSK_PROJECT[] = "GitLab.Project"; const char PSK_PROJECT[] = "GitLab.Project";
const char PSK_LAST_REQ[] = "GitLab.LastRequest";
static QString accessLevelString(int accessLevel) static QString accessLevelString(int accessLevel)
{ {
@@ -106,6 +107,7 @@ void GitLabProjectSettings::load()
m_id = Utils::Id::fromSetting(m_project->namedSettings(PSK_LINKED_ID)); m_id = Utils::Id::fromSetting(m_project->namedSettings(PSK_LINKED_ID));
m_host = m_project->namedSettings(PSK_SERVER).toString(); m_host = m_project->namedSettings(PSK_SERVER).toString();
m_currentProject = m_project->namedSettings(PSK_PROJECT).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 // may still be wrong, but we avoid an additional request by just doing sanity check here
if (!m_id.isValid() || m_host.isEmpty()) if (!m_id.isValid() || m_host.isEmpty())
@@ -124,6 +126,7 @@ void GitLabProjectSettings::save()
m_project->setNamedSettings(PSK_SERVER, QString()); m_project->setNamedSettings(PSK_SERVER, QString());
} }
m_project->setNamedSettings(PSK_PROJECT, m_currentProject); m_project->setNamedSettings(PSK_PROJECT, m_currentProject);
m_project->setNamedSettings(PSK_LAST_REQ, m_lastRequest);
} }
GitLabProjectSettingsWidget::GitLabProjectSettingsWidget(ProjectExplorer::Project *project, GitLabProjectSettingsWidget::GitLabProjectSettingsWidget(ProjectExplorer::Project *project,
@@ -184,6 +187,7 @@ void GitLabProjectSettingsWidget::unlink()
m_projectSettings->setLinked(false); m_projectSettings->setLinked(false);
m_projectSettings->setCurrentProject({}); m_projectSettings->setCurrentProject({});
updateEnabledStates(); updateEnabledStates();
GitLabPlugin::linkedStateChanged(false);
} }
void GitLabProjectSettingsWidget::checkConnection(CheckMode mode) void GitLabProjectSettingsWidget::checkConnection(CheckMode mode)
@@ -245,6 +249,7 @@ void GitLabProjectSettingsWidget::onConnectionChecked(const Project &project,
m_projectSettings->setCurrentServerHost(remote); m_projectSettings->setCurrentServerHost(remote);
m_projectSettings->setLinked(true); m_projectSettings->setLinked(true);
m_projectSettings->setCurrentProject(projectName); m_projectSettings->setCurrentProject(projectName);
GitLabPlugin::linkedStateChanged(true);
} }
updateEnabledStates(); updateEnabledStates();
} }
@@ -282,8 +287,10 @@ void GitLabProjectSettingsWidget::updateUi()
m_hostCB->setCurrentIndex(m_hostCB->findData(QVariant::fromValue(serverHost))); m_hostCB->setCurrentIndex(m_hostCB->findData(QVariant::fromValue(serverHost)));
m_linkedGitLabServer->setCurrentIndex( m_linkedGitLabServer->setCurrentIndex(
m_linkedGitLabServer->findData(QVariant::fromValue(server))); m_linkedGitLabServer->findData(QVariant::fromValue(server)));
GitLabPlugin::linkedStateChanged(true);
} else { } else {
m_projectSettings->setLinked(false); m_projectSettings->setLinked(false);
GitLabPlugin::linkedStateChanged(false);
} }
} }
updateEnabledStates(); updateEnabledStates();

View File

@@ -28,6 +28,7 @@
#include <projectexplorer/projectsettingswidget.h> #include <projectexplorer/projectsettingswidget.h>
#include <utils/id.h> #include <utils/id.h>
#include <QDateTime>
#include <QObject> #include <QObject>
#include <QWidget> #include <QWidget>
@@ -59,9 +60,12 @@ public:
QString currentProject() const { return m_currentProject; } QString currentProject() const { return m_currentProject; }
bool isLinked() const { return m_linked; } bool isLinked() const { return m_linked; }
void setLinked(bool 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; } ProjectExplorer::Project *project() const { return m_project; }
static std::tuple<QString, QString, int> remotePartsFromRemote(const QString &remote); static std::tuple<QString, QString, int> remotePartsFromRemote(const QString &remote);
private: private:
void load(); void load();
void save(); void save();
@@ -69,6 +73,7 @@ private:
ProjectExplorer::Project *m_project = nullptr; ProjectExplorer::Project *m_project = nullptr;
QString m_host; QString m_host;
Utils::Id m_id; Utils::Id m_id;
QDateTime m_lastRequest;
QString m_currentProject; QString m_currentProject;
bool m_linked = false; bool m_linked = false;
}; };

View File

@@ -43,6 +43,7 @@ const char API_PREFIX[] = "/api/v4";
const char QUERY_PROJECT[] = "/projects/%1"; const char QUERY_PROJECT[] = "/projects/%1";
const char QUERY_PROJECTS[] = "/projects?simple=true"; const char QUERY_PROJECTS[] = "/projects?simple=true";
const char QUERY_USER[] = "/user"; const char QUERY_USER[] = "/user";
const char QUERY_EVENTS[] = "/projects/%1/events";
Query::Query(Type type, const QStringList &parameter) Query::Query(Type type, const QStringList &parameter)
: m_type(type) : m_type(type)
@@ -62,7 +63,7 @@ void Query::setAdditionalParameters(const QStringList &additional)
bool Query::hasPaginatedResults() const bool Query::hasPaginatedResults() const
{ {
return m_type == Query::Projects; return m_type == Query::Projects || m_type == Query::Events;
} }
QString Query::toString() const QString Query::toString() const
@@ -82,6 +83,11 @@ QString Query::toString() const
case Query::User: case Query::User:
query += QUERY_USER; query += QUERY_USER;
break; 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) { if (m_pageParameter > 0) {
query.append(m_type == Query::Projects ? '&' : '?'); query.append(m_type == Query::Projects ? '&' : '?');

View File

@@ -40,7 +40,8 @@ public:
NoQuery, NoQuery,
User, User,
Project, Project,
Projects Projects,
Events
}; };
explicit Query(Type type, const QStringList &parameters = {}); explicit Query(Type type, const QStringList &parameters = {});

View File

@@ -32,6 +32,25 @@
#include <utility> #include <utility>
namespace GitLab { 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 { namespace ResultParser {
static PageInformation paginationInformation(const QByteArray &header) static PageInformation paginationInformation(const QByteArray &header)
@@ -133,6 +152,7 @@ static User userFromJson(const QJsonObject &jsonObj)
user.realname = jsonObj.value("name").toString(); user.realname = jsonObj.value("name").toString();
user.id = jsonObj.value("id").toInt(-1); user.id = jsonObj.value("id").toInt(-1);
user.email = jsonObj.value("email").toString(); user.email = jsonObj.value("email").toString();
user.lastLogin = jsonObj.value("last_sign_in_at").toString();
user.bot = jsonObj.value("bot").toBool(); user.bot = jsonObj.value("bot").toBool();
return user; return user;
} }
@@ -162,6 +182,31 @@ static Project projectFromJson(const QJsonObject &jsonObj)
return project; 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) User parseUser(const QByteArray &input)
{ {
auto [error, userObj] = preHandleSingle(input); auto [error, userObj] = preHandleSingle(input);
@@ -204,6 +249,27 @@ Projects parseProjects(const QByteArray &input)
return result; 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 parseErrorMessage(const QString &message)
{ {
Error error; Error error;

View File

@@ -52,6 +52,7 @@ public:
QString name; QString name;
QString realname; QString realname;
QString email; QString email;
QString lastLogin;
Error error; Error error;
int id = -1; int id = -1;
bool bot = false; bool bot = false;
@@ -82,11 +83,34 @@ public:
PageInformation pageInfo; 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 { namespace ResultParser {
User parseUser(const QByteArray &input); User parseUser(const QByteArray &input);
Project parseProject(const QByteArray &input); Project parseProject(const QByteArray &input);
Projects parseProjects(const QByteArray &input); Projects parseProjects(const QByteArray &input);
Events parseEvents(const QByteArray &input);
Error parseErrorMessage(const QString &message); Error parseErrorMessage(const QString &message);
} // namespace ResultParser } // namespace ResultParser