diff --git a/src/plugins/phabricator/CMakeLists.txt b/src/plugins/phabricator/CMakeLists.txt index 184bbc2..5319adc 100644 --- a/src/plugins/phabricator/CMakeLists.txt +++ b/src/plugins/phabricator/CMakeLists.txt @@ -1,37 +1,37 @@ find_program(ARCANIST arc) if(NOT ARCANIST) message(WARNING "The phabricator plugin depends on having the 'arc' script available in the PATH") else() message(STATUS "The 'arc' script was found as ${ARCANIST}") endif() add_definitions(-DTRANSLATION_DOMAIN=\"purpose_phabricator\") add_subdirectory(icons) set(PhabricatorHelper_SRCS phabricatorjobs.cpp) ecm_qt_declare_logging_category(PhabricatorHelper_SRCS HEADER debug.h IDENTIFIER PLUGIN_PHABRICATOR CATEGORY_NAME kdevplatform.plugins.phabricator DEFAULT_SEVERITY Debug) add_library(PhabricatorHelpers ${PhabricatorHelper_SRCS}) generate_export_header(PhabricatorHelpers EXPORT_FILE_NAME phabricatorhelpers_export.h) target_link_libraries(PhabricatorHelpers KF5::CoreAddons KF5::I18n) add_executable(testphabricator tests/testphabricator.cpp) ecm_mark_nongui_executable(testphabricator) target_link_libraries(testphabricator PhabricatorHelpers Qt5::Core) add_share_plugin(phabricatorplugin phabricatorplugin.cpp) target_link_libraries(phabricatorplugin Qt5::Widgets PhabricatorHelpers) set_target_properties(PhabricatorHelpers PROPERTIES VERSION ${PURPOSE_VERSION_STRING} SOVERSION ${PURPOSE_SOVERSION}) install(TARGETS PhabricatorHelpers ${KF5_INSTALL_TARGETS_DEFAULT_ARGS} LIBRARY NAMELINK_SKIP) add_library(phabricatorquickplugin quick/phabricatorquickplugin.cpp quick/difflistmodel.cpp quick/phabricatorrc.cpp) -target_link_libraries(phabricatorquickplugin Qt5::Qml PhabricatorHelpers) +target_link_libraries(phabricatorquickplugin Qt5::Qml Qt5::Gui PhabricatorHelpers) install(TARGETS phabricatorquickplugin DESTINATION ${QML_INSTALL_DIR}/org/kde/purpose/phabricator) install(FILES quick/qmldir DESTINATION ${QML_INSTALL_DIR}/org/kde/purpose/phabricator) diff --git a/src/plugins/phabricator/phabricatorjobs.cpp b/src/plugins/phabricator/phabricatorjobs.cpp index 8657ace..fc1dcb1 100644 --- a/src/plugins/phabricator/phabricatorjobs.cpp +++ b/src/plugins/phabricator/phabricatorjobs.cpp @@ -1,265 +1,269 @@ /* * This file is part of KDevelop * Copyright 2017 René J.V. Bertin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "phabricatorjobs.h" #include "debug.h" #include #include #include #include // #include // #include // #include // #include // #include // #include // #include #include #define COLOURCODES "\u001B\[[0-9]*m" using namespace Phabricator; bool DifferentialRevision::buildArcCommand(const QString& workDir, const QString& patchFile, bool doBrowse) { bool ret; QString arc = QStandardPaths::findExecutable(QStringLiteral("arc")); if (!arc.isEmpty()) { QStringList args; args << QStringLiteral("diff"); if (m_id.isEmpty()) { // creating a new differential revision (review request) // the fact we skip "--create" means we'll be creating a new "differential diff" // which obliges the user to fill in the details we cannot provide through the plugin ATM. // TODO: grab the TARGET_GROUPS from .reviewboardrc and pass that via --reviewers } else { // updating an existing differential revision (review request) args << QStringLiteral("--update") << m_id; } args << QStringLiteral("--excuse") << QStringLiteral("patch submitted with the purpose/phabricator plugin"); if (m_commit.isEmpty()) { args << QStringLiteral("--raw"); } else { args << QStringLiteral("--allow-untracked") << QStringLiteral("--ignore-unsound-tests") << QStringLiteral("--nolint") << QStringLiteral("-nounit") << QStringLiteral("--verbatim") << m_commit; } if (doBrowse) { args << QStringLiteral("--browse"); } m_arcCmd.setProgram(arc); m_arcCmd.setArguments(args); if (!patchFile.isEmpty()) { m_arcCmd.setStandardInputFile(patchFile); m_arcInput = patchFile; } m_arcCmd.setWorkingDirectory(workDir); connect(&m_arcCmd, static_cast(&QProcess::finished), this, &DifferentialRevision::done); setPercent(33); ret = true; } else { qCWarning(PLUGIN_PHABRICATOR) << "Could not find 'arc' in the path"; setError(KJob::UserDefinedError + 3); setErrorText(i18n("Could not find the 'arc' command")); setErrorString(errorText()); ret = false; } return ret; } void DifferentialRevision::start() { if (!m_arcCmd.program().isEmpty()) { qCDebug(PLUGIN_PHABRICATOR) << "starting" << m_arcCmd.program() << m_arcCmd.arguments(); qCDebug(PLUGIN_PHABRICATOR) << "\twordDir=" << m_arcCmd.workingDirectory() << "stdin=" << m_arcInput; m_arcCmd.start(); if (m_arcCmd.waitForStarted(5000)) { setPercent(66); } } } void DifferentialRevision::setErrorString(const QString& msg) { QRegExp unwanted(QString::fromUtf8(COLOURCODES)); m_errorString = msg; m_errorString.replace(unwanted, QString()); } QString DifferentialRevision::scrubbedResult() { QString result = QString::fromUtf8(m_arcCmd.readAllStandardOutput()); // the return string can contain terminal text colour codes: remove them. QRegExp unwanted(QString::fromUtf8(COLOURCODES)); result.replace(unwanted, QString()); return result; } QStringList DifferentialRevision::scrubbedResultList() { QStringList result = QString::fromUtf8(m_arcCmd.readAllStandardOutput()).split(QChar::LineFeed); // the return string can contain terminal text colour codes: remove them. QRegExp unwanted(QString::fromUtf8(COLOURCODES)); result.replaceInStrings(unwanted, QString()); // remove all (now) empty strings result.removeAll(QString()); return result; } NewDiffRev::NewDiffRev(const QUrl& patch, const QString& projectPath, bool doBrowse, QObject* parent) : DifferentialRevision(QString(), parent) , m_patch(patch) , m_project(projectPath) { buildArcCommand(projectPath, patch.toLocalFile(), doBrowse); } void NewDiffRev::done(int exitCode, QProcess::ExitStatus exitStatus) { if (exitStatus != QProcess::NormalExit || exitCode) { setError(KJob::UserDefinedError + exitCode); setErrorText(i18n("Could not create the new \"differential diff\"")); setErrorString(QString::fromUtf8(m_arcCmd.readAllStandardError())); qCWarning(PLUGIN_PHABRICATOR) << "Could not create the new \"differential diff\":" << m_arcCmd.error() << ";" << errorString(); } else { setPercent(99); const QString arcOutput = scrubbedResult(); const char *diffOpCode = "Diff URI: "; int diffOffset = arcOutput.indexOf(QLatin1String(diffOpCode)); if (diffOffset >= 0) { m_diffURI = arcOutput.mid(diffOffset + strlen(diffOpCode)).split(QChar::LineFeed).at(0); } else { m_diffURI = arcOutput; } } emitResult(); } UpdateDiffRev::UpdateDiffRev(const QUrl& patch, const QString& basedir, const QString& id, const QString& updateComment, bool doBrowse, QObject* parent) : DifferentialRevision(id, parent) , m_patch(patch) , m_basedir(basedir) { buildArcCommand(m_basedir, m_patch.toLocalFile(), doBrowse); QStringList args = m_arcCmd.arguments(); if (updateComment.isEmpty()) { args << QStringLiteral("--message") << QStringLiteral(""); } else { args << QStringLiteral("--message") << updateComment; } m_arcCmd.setArguments(args); } void UpdateDiffRev::done(int exitCode, QProcess::ExitStatus exitStatus) { if (exitStatus != QProcess::NormalExit || exitCode) { setError(KJob::UserDefinedError + exitCode); setErrorText(i18n("Patch upload to Phabricator failed")); setErrorString(QString::fromUtf8(m_arcCmd.readAllStandardError())); qCWarning(PLUGIN_PHABRICATOR) << "Patch upload to Phabricator failed with exit code" << exitCode << ", error" << m_arcCmd.error() << ";" << errorString(); } else { const QString arcOutput = scrubbedResult(); const char *diffOpCode = "Revision URI: "; int diffOffset = arcOutput.indexOf(QLatin1String(diffOpCode)); if (diffOffset >= 0) { m_diffURI = arcOutput.mid(diffOffset + strlen(diffOpCode)).split(QChar::LineFeed).at(0); } else { m_diffURI = arcOutput; } } emitResult(); } DiffRevList::DiffRevList(const QString& projectDir, QObject* parent) : DifferentialRevision(QString(), parent) , m_projectDir(projectDir) { buildArcCommand(m_projectDir); } bool Phabricator::DiffRevList::buildArcCommand(const QString& workDir, const QString& unused, bool) { Q_UNUSED(unused) bool ret; QString arc = QStandardPaths::findExecutable(QStringLiteral("arc")); if (!arc.isEmpty()) { QStringList args; args << QStringLiteral("list"); m_arcCmd.setProgram(arc); m_arcCmd.setArguments(args); m_arcCmd.setWorkingDirectory(workDir); connect(&m_arcCmd, static_cast(&QProcess::finished), this, &DiffRevList::done); setPercent(33); ret = true; } else { qCWarning(PLUGIN_PHABRICATOR) << "Could not find 'arc' in the path"; setError(KJob::UserDefinedError + 3); setErrorText(i18n("Could not find the 'arc' command")); setErrorString(errorText()); ret = false; } return ret; } void DiffRevList::done(int exitCode, QProcess::ExitStatus exitStatus) { if (exitStatus != QProcess::NormalExit || exitCode) { setError(KJob::UserDefinedError + exitCode); setErrorText(i18n("Could not get list of differential revisions in %1", QDir::currentPath())); setErrorString(QString::fromUtf8(m_arcCmd.readAllStandardError())); qCWarning(PLUGIN_PHABRICATOR) << "Could not get list of differential revisions" << m_arcCmd.error() << ";" << errorString(); } else { setPercent(99); QStringList reviews = scrubbedResultList(); qCDebug(PLUGIN_PHABRICATOR) << "arc list returned:" << reviews; foreach (auto rev, reviews) { QRegExp revIDExpr(QString::fromUtf8(" D[0-9][0-9]*: ")); int idStart = rev.indexOf(revIDExpr); if (idStart >= 0) { QString revID = rev.mid(idStart+1).split(QString::fromUtf8(": ")).at(0); QString revTitle = rev.section(revIDExpr, 1); if (rev.startsWith(QStringLiteral("* Accepted "))) { // append a Unicode "Heavy Check Mark" to signal accepted revisions revTitle += QStringLiteral(" ") + QString(QChar(0x2714)); + m_statusMap[revTitle] = Accepted; } else if (rev.startsWith(QStringLiteral("* Needs Revision "))) { // append a Unicode "Heavy Ballot X" for lack of a Unicode glyph // resembling the icon used on the Phab site. revTitle += QStringLiteral(" ") + QString(QChar(0x2718)); + m_statusMap[revTitle] = NeedsRevision; + } else if (rev.startsWith(QStringLiteral("* Needs Review "))) { + m_statusMap[revTitle] = NeedsReview; } m_reviews << qMakePair(revID, revTitle); m_revMap[revTitle] = revID; } } } emitResult(); } diff --git a/src/plugins/phabricator/phabricatorjobs.h b/src/plugins/phabricator/phabricatorjobs.h index 00ce63d..6e05c90 100644 --- a/src/plugins/phabricator/phabricatorjobs.h +++ b/src/plugins/phabricator/phabricatorjobs.h @@ -1,140 +1,149 @@ /* * This file is part of KDevelop * Copyright 2017 René J.V. Bertin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KDEVPLATFORM_PLUGIN_PHABRICATORJOBS_H #define KDEVPLATFORM_PLUGIN_PHABRICATORJOBS_H #include "phabricatorhelpers_export.h" #include #include #include #include #include #include class QNetworkReply; namespace Phabricator { class PHABRICATORHELPERS_EXPORT DifferentialRevision : public KJob { Q_OBJECT public: DifferentialRevision(const QString& id, QObject* parent) : KJob(parent), m_id(id), m_commit(QString()) { setPercent(0); } QString requestId() const { return m_id; } void setRequestId(const QString& id) { m_id = id; } QString commitRef() const { return m_commit; } void setCommitRef(const QString& commit) { m_commit = commit; } void start() override; QString errorString() const override { return m_errorString; } void setErrorString(const QString& msg); QString scrubbedResult(); QStringList scrubbedResultList(); private Q_SLOTS: virtual void done(int exitCode, QProcess::ExitStatus exitStatus) = 0; protected: virtual bool buildArcCommand(const QString& workDir, const QString& patchFile=QString(), bool doBrowse=false); QProcess m_arcCmd; private: QString m_id; QString m_commit; QString m_errorString; QString m_arcInput; }; class PHABRICATORHELPERS_EXPORT NewDiffRev : public DifferentialRevision { Q_OBJECT public: NewDiffRev(const QUrl& patch, const QString& project, bool doBrowse = false, QObject* parent = nullptr); QString diffURI() const { return m_diffURI; } private Q_SLOTS: void done(int exitCode, QProcess::ExitStatus exitStatus) override; private: QUrl m_patch; QString m_project; QString m_diffURI; }; class PHABRICATORHELPERS_EXPORT UpdateDiffRev : public DifferentialRevision { Q_OBJECT public: UpdateDiffRev(const QUrl& patch, const QString& basedir, const QString& id, const QString& updateComment = QString(), bool doBrowse = false, QObject* parent = nullptr); QString diffURI() const { return m_diffURI; } private Q_SLOTS: void done(int exitCode, QProcess::ExitStatus exitStatus) override; private: QUrl m_patch; QString m_basedir; QString m_diffURI; }; class PHABRICATORHELPERS_EXPORT DiffRevList : public DifferentialRevision { Q_OBJECT public: + enum Status { Accepted, NeedsReview, NeedsRevision }; + Q_ENUM(Status) + DiffRevList(const QString& projectDir, QObject* parent = nullptr); // return the open diff. revisions as a list of pairs QList > reviews() const { return m_reviews; } // return the open diff. revisions as a map of diffDescription->diffID entries QHash reviewMap() const { return m_revMap; } + // return the open diff. revision statuses as a map of diffDescription->Status entries + QHash statusMap() const + { + return m_statusMap; + } private Q_SLOTS: void done(int exitCode, QProcess::ExitStatus exitStatus) override; protected: bool buildArcCommand(const QString& workDir, const QString& unused=QString(), bool ignored=false) override; private: QList > m_reviews; QHash m_revMap; + QHash m_statusMap; QString m_projectDir; }; } #endif diff --git a/src/plugins/phabricator/quick/difflistmodel.cpp b/src/plugins/phabricator/quick/difflistmodel.cpp index 866b29a..0e8faca 100644 --- a/src/plugins/phabricator/quick/difflistmodel.cpp +++ b/src/plugins/phabricator/quick/difflistmodel.cpp @@ -1,150 +1,177 @@ /* * Copyright 2017 René J.V. Bertin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "difflistmodel.h" #include "phabricatorjobs.h" #include +#include #include #include DiffListModel::DiffListModel(QObject* parent) : QAbstractListModel(parent) , m_initialDir(QDir::currentPath()) , m_tempDir(nullptr) { refresh(); } void DiffListModel::refresh() { beginResetModel(); m_values.clear(); endResetModel(); if (m_tempDir) { qCritical() << "DiffListModel::refresh() called while still active!"; return; } // our CWD should be the directory from which the application was launched, which // may or may not be a git, mercurial or svn working copy, so we create a temporary // directory in which we initialise a git repository. This may be an empty repo. m_initialDir = QDir::currentPath(); m_tempDir = new QTemporaryDir; if (!m_tempDir->isValid()) { qCritical() << "DiffListModel::refresh() failed to create temporary directory" << m_tempDir->path() << ":" << m_tempDir->errorString(); } else { if (QDir::setCurrent(m_tempDir->path())) { // the directory will be removed in receivedDiffRevs() m_tempDir->setAutoRemove(false); QProcess initGit; bool ok = false; // create the virgin git repo. This is a very cheap operation that should // never fail in a fresh temporary directory we ourselves created, so it // should be OK to do this with a synchronous call. initGit.start(QLatin1String("git init")); if (initGit.waitForStarted(1000)) { ok = initGit.waitForFinished(500); } if (!ok) { qCritical() << "DiffListModel::refresh() : couldn't create temp. git repo:" << initGit.errorString(); } } else { qCritical() << "DiffListModel::refresh() failed to chdir to" << m_tempDir->path(); } } // create a list request with the current (= temp.) directory as the project directory. // This request is executed asynchronously, which is why we cannot restore the initial // working directory just yet, nor remove the temporary directory. Phabricator::DiffRevList* repo = new Phabricator::DiffRevList(QDir::currentPath(), this); connect(repo, &Phabricator::DiffRevList::finished, this, &DiffListModel::receivedDiffRevs); repo->start(); } void DiffListModel::receivedDiffRevs(KJob* job) { if (job->error() != 0) { qWarning() << "error getting differential revision list" << job->errorString(); beginResetModel(); m_values.clear(); endResetModel(); return; } - const auto revs = dynamic_cast(job)->reviews(); + const auto diffRevList = dynamic_cast(job); + const auto revs = diffRevList->reviews(); QVector tmpValues; foreach (const auto review, revs) { - tmpValues += Value { review.second, review.first }; + auto status = diffRevList->statusMap()[review.second]; + tmpValues += Value { review.second, review.first, status }; } qSort(tmpValues.begin(), tmpValues.end()); beginResetModel(); m_values.clear(); foreach (const auto value, tmpValues) { m_values += value; } endResetModel(); // now we can restore the initial working directory and remove the temp directory // (in that order!). if (!QDir::setCurrent(m_initialDir)) { qCritical() << "DiffListModel::receivedDiffRevs() failed to restore initial directory" << m_initialDir; } if (m_tempDir) { m_tempDir->remove(); delete m_tempDir; m_tempDir = nullptr; } } +QHash DiffListModel::roleNames() const +{ + const QHash roles = { + {Qt::DisplayRole, QByteArrayLiteral("display")}, + {Qt::ToolTipRole, QByteArrayLiteral("toolTip")}, + {Qt::TextColorRole, QByteArrayLiteral("textColor")} }; + return roles; +} + QVariant DiffListModel::data(const QModelIndex &idx, int role) const { if (!idx.isValid() || idx.column() != 0 || idx.row() >= m_values.size()) { return QVariant(); } switch (role) { case Qt::DisplayRole: return m_values[idx.row()].summary; case Qt::ToolTipRole: return m_values[idx.row()].id; + case Qt::TextColorRole: + // Use the colours arc also uses + QVariant ret; + switch (m_values[idx.row()].status.value()) { + case Phabricator::DiffRevList::Accepted: + // alternative: KColorScheme::ForegroundRole::PositiveText + ret = QBrush(Qt::green); + case Phabricator::DiffRevList::NeedsReview: + // alternative: KColorScheme::ForegroundRole::NeutralText + ret = QBrush(Qt::magenta); + case Phabricator::DiffRevList::NeedsRevision: + // alternative: KColorScheme::ForegroundRole::NegativeText + ret = QBrush(Qt::red); + } + return ret; } return QVariant(); } int DiffListModel::rowCount(const QModelIndex & parent) const { return parent.isValid() ? 0 : m_values.count(); } QVariant DiffListModel::get(int row, const QByteArray &role) { return index(row, 0).data(roleNames().key(role)); } void DiffListModel::setStatus(const QString &status) { if (m_status != status) { m_status = status; refresh(); } } diff --git a/src/plugins/phabricator/quick/difflistmodel.h b/src/plugins/phabricator/quick/difflistmodel.h index 3b4af14..751059f 100644 --- a/src/plugins/phabricator/quick/difflistmodel.h +++ b/src/plugins/phabricator/quick/difflistmodel.h @@ -1,73 +1,76 @@ /* * Copyright 2017 René J.V. Bertin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef DIFFLISTMODEL_H #define DIFFLISTMODEL_H #include #include #include +#include #include class KJob; class QTemporaryDir; class DiffListModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(QString status READ status WRITE setStatus) public: DiffListModel(QObject* parent = nullptr); void refresh(); + QHash roleNames() const override; QVariant data(const QModelIndex &idx, int role) const override; int rowCount(const QModelIndex & parent) const override; QString status() const { return m_status; } void setStatus(const QString &status); void receivedDiffRevs(KJob* job); Q_SCRIPTABLE QVariant get(int row, const QByteArray &role); private: struct Value { QVariant summary; QVariant id; + QVariant status; inline bool operator<(const DiffListModel::Value &b) const { return summary.toString().localeAwareCompare(b.summary.toString()); } #ifndef QT_NO_DEBUG_STREAM operator QString() const { - QString ret = QStringLiteral("DiffListModel::Value{summary=\"%1\" id=\"%2\"}"); - return ret.arg(this->summary.toString()).arg(this->id.toString()); + QString ret = QStringLiteral("DiffListModel::Value{summary=\"%1\" id=\"%2\" status=\"%3\"}"); + return ret.arg(this->summary.toString()).arg(this->id.toString()).arg(this->status.toInt()); } #endif }; QVector m_values; QString m_status; QString m_initialDir; QTemporaryDir *m_tempDir; }; #endif