/**************************************************************************** ** ** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). ** Contact: http://www.qt-project.org/legal ** ** 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 Digia. For licensing terms and ** conditions see http://www.qt.io/licensing. For further information ** use the contact form at http://www.qt.io/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 or version 3 as published by the Free ** Software Foundation and appearing in the file LICENSE.LGPLv21 and ** LICENSE.LGPLv3 included in the packaging of this file. Please review the ** following information to ensure the GNU Lesser General Public License ** requirements will be met: https://www.gnu.org/licenses/lgpl.html and ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Digia gives you certain additional ** rights. These rights are described in the Digia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ****************************************************************************/ #include "gerritmodel.h" #include "gerritparameters.h" #include "../gitplugin.h" #include "../gitclient.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if QT_VERSION >= 0x050000 # include # include # include # include #else # include # include #endif enum { debug = 0 }; namespace Gerrit { namespace Internal { QDebug operator<<(QDebug d, const GerritApproval &a) { d.nospace() << a.reviewer << " :" << a.approval << " (" << a.type << ", " << a.description << ')'; return d; } // Sort approvals by type and reviewer bool gerritApprovalLessThan(const GerritApproval &a1, const GerritApproval &a2) { return a1.type.compare(a2.type) < 0 || a1.reviewer.compare(a2.reviewer) < 0; } QDebug operator<<(QDebug d, const GerritPatchSet &p) { d.nospace() << " Patch set: " << p.ref << ' ' << p.patchSetNumber << ' ' << p.approvals; return d; } QDebug operator<<(QDebug d, const GerritChange &c) { d.nospace() << c.title << " by " << c.email << ' ' << c.lastUpdated << ' ' << c.currentPatchSet; return d; } // Format default Url for a change static inline QString defaultUrl(const QSharedPointer &p, int gerritNumber) { QString result = p->https ? QLatin1String("https://") : QLatin1String("http://"); result += p->host; result += QLatin1Char('/'); result += QString::number(gerritNumber); return result; } // Format (sorted) approvals as separate HTML table // lines by type listing the revievers: // "Code ReviewJohn Doe: -1, ......Sanity Review: ...". QString GerritPatchSet::approvalsToHtml() const { if (approvals.isEmpty()) return QString(); QString result; QTextStream str(&result); QString lastType; foreach (const GerritApproval &a, approvals) { if (a.type != lastType) { if (!lastType.isEmpty()) str << "\n"; str << "" << (a.description.isEmpty() ? a.type : a.description) << ""; lastType = a.type; } else { str << ", "; } str << a.reviewer; if (!a.email.isEmpty()) str << " " << a.email << ""; str << ": " << forcesign << a.approval << noforcesign; } str << "\n"; return result; } // Determine total approval level. Negative values take preference // and stay. static inline void applyApproval(int approval, int *total) { if (approval < *total || (*total >= 0 && approval > *total)) *total = approval; } // Format the approvals similar to the columns in the Web view // by a type character followed by the approval level: "C: -2, S: 1" QString GerritPatchSet::approvalsColumn() const { typedef QMap TypeReviewMap; typedef TypeReviewMap::iterator TypeReviewMapIterator; typedef TypeReviewMap::const_iterator TypeReviewMapConstIterator; QString result; if (approvals.isEmpty()) return result; TypeReviewMap reviews; // Sort approvals into a map by type character foreach (const GerritApproval &a, approvals) { if (a.type != QLatin1String("STGN")) { // Qt-Project specific: Ignore "STGN" (Staged) const QChar typeChar = a.type.at(0); TypeReviewMapIterator it = reviews.find(typeChar); if (it == reviews.end()) it = reviews.insert(typeChar, 0); applyApproval(a.approval, &it.value()); } } QTextStream str(&result); const TypeReviewMapConstIterator cend = reviews.constEnd(); for (TypeReviewMapConstIterator it = reviews.constBegin(); it != cend; ++it) { if (!result.isEmpty()) str << ' '; str << it.key() << ": " << forcesign << it.value() << noforcesign; } return result; } bool GerritPatchSet::hasApproval(const QString &userName) const { foreach (const GerritApproval &a, approvals) if (a.reviewer == userName) return true; return false; } int GerritPatchSet::approvalLevel() const { int value = 0; foreach (const GerritApproval &a, approvals) applyApproval(a.approval, &value); return value; } QString GerritChange::filterString() const { const QChar blank = QLatin1Char(' '); QString result = QString::number(number) + blank + title + blank + owner + blank + project + blank + branch + blank + status; foreach (const GerritApproval &a, currentPatchSet.approvals) { result += blank; result += a.reviewer; } return result; } QStringList GerritChange::gitFetchArguments(const QSharedPointer &p) const { QStringList arguments; const QString url = QLatin1String("ssh://") + p->sshHostArgument() + QLatin1Char(':') + QString::number(p->port) + QLatin1Char('/') + project; arguments << QLatin1String("fetch") << url << currentPatchSet.ref; return arguments; } // Helper class that runs ssh gerrit queries from a list of query argument // string lists, // see http://gerrit.googlecode.com/svn/documentation/2.1.5/cmd-query.html // In theory, querying uses a continuation/limit protocol, but we assume // we will never reach a limit with those queries. class QueryContext : public QObject { Q_OBJECT public: QueryContext(const QStringList &queries, const QSharedPointer &p, QObject *parent = 0); ~QueryContext(); int currentQuery() const { return m_currentQuery; } public slots: void start(); signals: void queryFinished(const QByteArray &); void finished(); private slots: void processError(QProcess::ProcessError); void processFinished(int exitCode, QProcess::ExitStatus); void readyReadStandardError(); void readyReadStandardOutput(); void timeout(); private: void startQuery(const QString &query); void errorTermination(const QString &msg); const QSharedPointer m_parameters; const QStringList m_queries; QProcess m_process; QTimer m_timer; QString m_binary; QByteArray m_output; int m_currentQuery; QFutureInterface m_progress; QStringList m_baseArguments; }; enum { timeOutMS = 30000 }; QueryContext::QueryContext(const QStringList &queries, const QSharedPointer &p, QObject *parent) : QObject(parent) , m_parameters(p) , m_queries(queries) , m_currentQuery(0) , m_baseArguments(p->baseCommandArguments()) { connect(&m_process, SIGNAL(readyReadStandardError()), this, SLOT(readyReadStandardError())); connect(&m_process, SIGNAL(readyReadStandardOutput()), this, SLOT(readyReadStandardOutput())); connect(&m_process, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(processFinished(int,QProcess::ExitStatus))); connect(&m_process, SIGNAL(error(QProcess::ProcessError)), this, SLOT(processError(QProcess::ProcessError))); m_process.setProcessEnvironment(Git::Internal::GitPlugin::instance()-> gitClient()->processEnvironment()); m_progress.setProgressRange(0, m_queries.size()); // Determine binary and common command line arguments. m_baseArguments << QLatin1String("query") << QLatin1String("--dependencies") << QLatin1String("--current-patch-set") << QLatin1String("--format=JSON"); m_binary = m_baseArguments.front(); m_baseArguments.pop_front(); m_timer.setInterval(timeOutMS); m_timer.setSingleShot(true); connect(&m_timer, SIGNAL(timeout()), this, SLOT(timeout())); } QueryContext::~QueryContext() { if (m_progress.isRunning()) m_progress.reportFinished(); if (m_timer.isActive()) m_timer.stop(); m_process.disconnect(this); Utils::SynchronousProcess::stopProcess(m_process); } void QueryContext::start() { Core::FutureProgress *fp = Core::ProgressManager::addTask(m_progress.future(), tr("Querying Gerrit"), "gerrit-query"); fp->setKeepOnFinish(Core::FutureProgress::HideOnFinish); m_progress.reportStarted(); startQuery(m_queries.front()); // Order: synchronous call to error handling if something goes wrong. } void QueryContext::startQuery(const QString &query) { QStringList arguments = m_baseArguments; arguments.push_back(query); VcsBase::VcsBaseOutputWindow::instance() ->appendCommand(m_process.workingDirectory(), m_binary, arguments); m_timer.start(); m_process.start(m_binary, arguments); m_process.closeWriteChannel(); } void QueryContext::errorTermination(const QString &msg) { m_progress.reportCanceled(); m_progress.reportFinished(); VcsBase::VcsBaseOutputWindow::instance()->appendError(msg); emit finished(); } void QueryContext::processError(QProcess::ProcessError e) { const QString msg = tr("Error running %1: %2").arg(m_binary, m_process.errorString()); if (e == QProcess::FailedToStart) errorTermination(msg); else VcsBase::VcsBaseOutputWindow::instance()->appendError(msg); } void QueryContext::processFinished(int exitCode, QProcess::ExitStatus es) { if (m_timer.isActive()) m_timer.stop(); if (es != QProcess::NormalExit) { errorTermination(tr("%1 crashed.").arg(m_binary)); return; } else if (exitCode) { errorTermination(tr("%1 returned %2.").arg(m_binary).arg(exitCode)); return; } emit queryFinished(m_output); m_output.clear(); if (++m_currentQuery >= m_queries.size()) { m_progress.reportFinished(); emit finished(); } else { m_progress.setProgressValue(m_currentQuery); startQuery(m_queries.at(m_currentQuery)); } } void QueryContext::readyReadStandardError() { VcsBase::VcsBaseOutputWindow::instance()->appendError(QString::fromLocal8Bit(m_process.readAllStandardError())); } void QueryContext::readyReadStandardOutput() { m_output.append(m_process.readAllStandardOutput()); } void QueryContext::timeout() { if (m_process.state() != QProcess::Running) return; QWidget *parent = QApplication::activeModalWidget(); if (!parent) parent = QApplication::activeWindow(); QMessageBox box(QMessageBox::Question, tr("Timeout"), tr("The gerrit process has not responded within %1s.\n" "Most likely this is caused by problems with SSH authentication.\n" "Would you like to terminate it?"). arg(timeOutMS / 1000), QMessageBox::NoButton, parent); QPushButton *terminateButton = box.addButton(tr("Terminate"), QMessageBox::YesRole); box.addButton(tr("Keep Running"), QMessageBox::NoRole); connect(&m_process, SIGNAL(finished(int)), &box, SLOT(reject())); box.exec(); if (m_process.state() != QProcess::Running) return; if (box.clickedButton() == terminateButton) Utils::SynchronousProcess::stopProcess(m_process); else m_timer.start(); } GerritModel::GerritModel(const QSharedPointer &p, QObject *parent) : QStandardItemModel(0, ColumnCount, parent) , m_parameters(p) , m_query(0) { QStringList headers; // Keep in sync with GerritChange::toHtml() headers << QLatin1String("#") << tr("Subject") << tr("Owner") << tr("Updated") << tr("Project") << tr("Approvals") << tr("Status"); setHorizontalHeaderLabels(headers); } GerritModel::~GerritModel() { } static inline GerritChangePtr changeFromItem(const QStandardItem *item) { return qvariant_cast(item->data(GerritModel::GerritChangeRole)); } GerritChangePtr GerritModel::change(const QModelIndex &index) const { if (index.isValid()) return changeFromItem(itemFromIndex(index)); return GerritChangePtr(new GerritChange); } QString GerritModel::dependencyHtml(const QString &header, const QString &changeId, const QString &serverPrefix) const { QString res; if (changeId.isEmpty()) return res; QTextStream str(&res); str << "" << header << "' << changeId << ""; if (const QStandardItem *item = itemForId(changeId)) str << " (" << changeFromItem(item)->title << ')'; str << ""; return res; } QString GerritModel::toHtml(const QModelIndex& index) const { static const QString subjectHeader = GerritModel::tr("Subject"); static const QString numberHeader = GerritModel::tr("Number"); static const QString ownerHeader = GerritModel::tr("Owner"); static const QString projectHeader = GerritModel::tr("Project"); static const QString statusHeader = GerritModel::tr("Status"); static const QString patchSetHeader = GerritModel::tr("Patch set"); static const QString urlHeader = GerritModel::tr("URL"); static const QString dependsOnHeader = GerritModel::tr("Depends on"); static const QString neededByHeader = GerritModel::tr("Needed by"); if (!index.isValid()) return QString(); const GerritChangePtr c = change(index); const QString serverPrefix = c->url.left(c->url.lastIndexOf(QLatin1Char('/')) + 1); QString result; QTextStream str(&result); str << "" << "" << "" << "" << "" << dependencyHtml(dependsOnHeader, c->dependsOnId, serverPrefix) << dependencyHtml(neededByHeader, c->neededById, serverPrefix) << "" << "" << c->currentPatchSet.patchSetNumber << "" << c->currentPatchSet.approvalsToHtml() << "" << "
" << subjectHeader << "" << c->title << "
" << numberHeader << "url << "\">" << c->number << "
" << ownerHeader << "" << c->owner << ' ' << "email << "\">" << c->email << "
" << projectHeader << "" << c->project << " (" << c->branch << ")
" << statusHeader << "" << c->status << ", " << c->lastUpdated.toString(Qt::DefaultLocaleShortDate) << "
" << patchSetHeader << "" << "
" << urlHeader << "url << "\">" << c->url << "
"; return result; } static QStandardItem *idSearchRecursion(QStandardItem *item, const QString &id) { if (changeFromItem(item)->id == id) return item; const int rowCount = item->rowCount(); for (int r = 0; r < rowCount; ++r) { if (QStandardItem *i = idSearchRecursion(item->child(r, 0), id)) return i; } return 0; } QStandardItem *GerritModel::itemForId(const QString &id) const { if (id.isEmpty()) return 0; const int numRows = rowCount(); for (int r = 0; r < numRows; ++r) { if (QStandardItem *i = idSearchRecursion(item(r, 0), id)) return i; } return 0; } void GerritModel::refresh(const QString &query) { if (m_query) { qWarning("%s: Another query is still running", Q_FUNC_INFO); return; } clearData(); // Assemble list of queries QStringList queries; if (!query.trimmed().isEmpty()) queries.push_back(query); else { const QString statusOpenQuery = QLatin1String("status:open"); if (m_parameters->user.isEmpty()) { queries.push_back(statusOpenQuery); } else { // Owned by: queries.push_back(statusOpenQuery + QLatin1String(" owner:") + m_parameters->user); // For Review by: queries.push_back(statusOpenQuery + QLatin1String(" reviewer:") + m_parameters->user); } } m_query = new QueryContext(queries, m_parameters, this); connect(m_query, SIGNAL(queryFinished(QByteArray)), this, SLOT(queryFinished(QByteArray))); connect(m_query, SIGNAL(finished()), this, SLOT(queriesFinished())); emit refreshStateChanged(true); m_query->start(); } void GerritModel::clearData() { if (const int rows = rowCount()) removeRows(0, rows); } /* Parse gerrit query Json output. * See http://gerrit.googlecode.com/svn/documentation/2.1.5/cmd-query.html * Note: The url will be present only if "canonicalWebUrl" is configured * in gerrit.config. \code {"project":"qt/qtbase","branch":"master","id":"I6601ca68c427b909680423ae81802f1ed5cd178a", "number":"24143","subject":"bla","owner":{"name":"Hans Mustermann","email":"hm@acme.com"}, "url":"https://...","lastUpdated":1335127888,"sortKey":"001c8fc300005e4f", "open":true,"status":"NEW","currentPatchSet": {"number":"1","revision":"0a1e40c78ef16f7652472f4b4bb4c0addeafbf82", "ref":"refs/changes/43/24143/1", "uploader":{"name":"Hans Mustermann","email":"hm@acme.com"}, "approvals":[{"type":"SRVW","description":"Sanity Review","value":"1", "grantedOn":1335127888,"by":{ "name":"Qt Sanity Bot","email":"qt_sanity_bot@ovi.com"}}]}} \endcode */ #if QT_VERSION >= 0x050000 // Qt 5 using the QJsonDocument classes. static bool parseOutput(const QSharedPointer ¶meters, const QByteArray &output, QList &result) { // The output consists of separate lines containing a document each const QString typeKey = QLatin1String("type"); const QString idKey = QLatin1String("id"); const QString dependsOnKey = QLatin1String("dependsOn"); const QString neededByKey = QLatin1String("neededBy"); const QString branchKey = QLatin1String("branch"); const QString numberKey = QLatin1String("number"); const QString ownerKey = QLatin1String("owner"); const QString ownerNameKey = QLatin1String("name"); const QString ownerEmailKey = QLatin1String("email"); const QString statusKey = QLatin1String("status"); const QString projectKey = QLatin1String("project"); const QString titleKey = QLatin1String("subject"); const QString urlKey = QLatin1String("url"); const QString patchSetKey = QLatin1String("currentPatchSet"); const QString refKey = QLatin1String("ref"); const QString approvalsKey = QLatin1String("approvals"); const QString approvalsValueKey = QLatin1String("value"); const QString approvalsByKey = QLatin1String("by"); const QString lastUpdatedKey = QLatin1String("lastUpdated"); const QList lines = output.split('\n'); const QString approvalsTypeKey = QLatin1String("type"); const QString approvalsDescriptionKey = QLatin1String("description"); bool res = true; result.clear(); result.reserve(lines.size()); foreach (const QByteArray &line, lines) { if (line.isEmpty()) continue; QJsonParseError error; const QJsonDocument doc = QJsonDocument::fromJson(line, &error); if (doc.isNull()) { QString errorMessage = GerritModel::tr("Parse error: \"%1\" -> %2") .arg(QString::fromLocal8Bit(line)) .arg(error.errorString()); qWarning() << errorMessage; VcsBase::VcsBaseOutputWindow::instance()->appendError(errorMessage); res = false; continue; } GerritChangePtr change(new GerritChange); const QJsonObject object = doc.object(); // Skip stats line: {"type":"stats","rowCount":9,"runTimeMilliseconds":13} if (!object.value(typeKey).toString().isEmpty()) continue; // Read current patch set. const QJsonObject patchSet = object.value(patchSetKey).toObject(); change->currentPatchSet.patchSetNumber = qMax(1, patchSet.value(numberKey).toString().toInt()); change->currentPatchSet.ref = patchSet.value(refKey).toString(); const QJsonArray approvalsJ = patchSet.value(approvalsKey).toArray(); const int ac = approvalsJ.size(); for (int a = 0; a < ac; ++a) { const QJsonObject ao = approvalsJ.at(a).toObject(); GerritApproval approval; const QJsonObject approverO = ao.value(approvalsByKey).toObject(); approval.reviewer = approverO.value(ownerNameKey).toString(); approval.email = approverO.value(ownerEmailKey).toString(); approval.approval = ao.value(approvalsValueKey).toString().toInt(); approval.type = ao.value(approvalsTypeKey).toString(); approval.description = ao.value(approvalsDescriptionKey).toString(); change->currentPatchSet.approvals.push_back(approval); } qStableSort(change->currentPatchSet.approvals.begin(), change->currentPatchSet.approvals.end(), gerritApprovalLessThan); // Remaining change->number = object.value(numberKey).toString().toInt(); change->url = object.value(urlKey).toString(); if (change->url.isEmpty()) // No "canonicalWebUrl" is in gerrit.config. change->url = defaultUrl(parameters, change->number); change->id = object.value(idKey).toString(); change->title = object.value(titleKey).toString(); const QJsonObject ownerJ = object.value(ownerKey).toObject(); change->owner = ownerJ.value(ownerNameKey).toString(); change->email = ownerJ.value(ownerEmailKey).toString(); change->project = object.value(projectKey).toString(); change->branch = object.value(branchKey).toString(); change->status = object.value(statusKey).toString(); if (const int timeT = qRound(object.value(lastUpdatedKey).toDouble())) change->lastUpdated = QDateTime::fromTime_t(timeT); if (change->isValid()) { result.push_back(change); } else { qWarning("%s: Parse error: '%s'.", Q_FUNC_INFO, line.constData()); VcsBase::VcsBaseOutputWindow::instance() ->appendError(GerritModel::tr("Parse error: \"%1\"") .arg(QString::fromLocal8Bit(line))); res = false; } // Read out dependencies const QJsonValue dependsOnValue = object.value(dependsOnKey); if (dependsOnValue.isArray()) { const QJsonArray dependsOnArray = dependsOnValue.toArray(); if (!dependsOnArray.isEmpty()) { const QJsonValue first = dependsOnArray.at(0); if (first.isObject()) change->dependsOnId = first.toObject()[idKey].toString(); } } // Read out needed by const QJsonValue neededByValue = object.value(neededByKey); if (neededByValue.isArray()) { const QJsonArray neededByArray = neededByValue.toArray(); if (!neededByArray.isEmpty()) { const QJsonValue first = neededByArray.at(0); if (first.isObject()) change->neededById = first.toObject()[idKey].toString(); } } } return res; } #else // QT_VERSION >= 0x050000 // Qt 4.8 using the Json classes from utils. static inline int jsonIntMember(Utils::JsonObjectValue *object, const QString &key, int defaultValue = 0) { if (Utils::JsonValue *v = object->member(key)) { if (Utils::JsonIntValue *iv = v->toInt()) return iv->value(); if (Utils::JsonStringValue *sv = v->toString()) { bool ok; const int result = sv->value().toInt(&ok); if (ok) return result; } } return defaultValue; } static inline QString jsonStringMember(Utils::JsonObjectValue *object, const QString &key) { if (Utils::JsonValue *v = object->member(key)) if (Utils::JsonStringValue *sv = v->toString()) return sv->value(); return QString(); } static bool parseOutput(const QSharedPointer ¶meters, const QByteArray &output, QList &result) { // The output consists of separate lines containing a document each const QList lines = output.split('\n'); const QString typeKey = QLatin1String("type"); const QString idKey = QLatin1String("id"); const QString dependsOnKey = QLatin1String("dependsOn"); const QString neededByKey = QLatin1String("neededBy"); const QString branchKey = QLatin1String("branch"); const QString numberKey = QLatin1String("number"); const QString ownerKey = QLatin1String("owner"); const QString ownerNameKey = QLatin1String("name"); const QString ownerEmailKey = QLatin1String("email"); const QString statusKey = QLatin1String("status"); const QString projectKey = QLatin1String("project"); const QString titleKey = QLatin1String("subject"); const QString urlKey = QLatin1String("url"); const QString patchSetKey = QLatin1String("currentPatchSet"); const QString refKey = QLatin1String("ref"); const QString approvalsKey = QLatin1String("approvals"); const QString approvalsValueKey = QLatin1String("value"); const QString approvalsByKey = QLatin1String("by"); const QString approvalsTypeKey = QLatin1String("type"); const QString approvalsDescriptionKey = QLatin1String("description"); const QString lastUpdatedKey = QLatin1String("lastUpdated"); bool res = true; result.clear(); result.reserve(lines.size()); Utils::JsonMemoryPool pool; foreach (const QByteArray &line, lines) { if (line.isEmpty()) continue; Utils::JsonValue *objectValue = Utils::JsonValue::create(QString::fromUtf8(line), &pool); if (!objectValue) { QString errorMessage = GerritModel::tr("Parse error: \"%1\"") .arg(QString::fromLocal8Bit(line)); qWarning() << errorMessage; VcsBase::VcsBaseOutputWindow::instance()->appendError(errorMessage); res = false; continue; } Utils::JsonObjectValue *object = objectValue->toObject(); QTC_ASSERT(object, continue ); GerritChangePtr change(new GerritChange); // Skip stats line: {"type":"stats","rowCount":9,"runTimeMilliseconds":13} if (jsonStringMember(object, typeKey) == QLatin1String("stats")) continue; // Read current patch set. if (Utils::JsonValue *patchSetValue = object->member(patchSetKey)) { Utils::JsonObjectValue *patchSet = patchSetValue->toObject(); QTC_ASSERT(patchSet, continue ); const int n = jsonIntMember(patchSet, numberKey); change->currentPatchSet.patchSetNumber = qMax(1, n); change->currentPatchSet.ref = jsonStringMember(patchSet, refKey); if (Utils::JsonValue *approvalsV = patchSet->member(approvalsKey)) { Utils::JsonArrayValue *approvals = approvalsV->toArray(); QTC_ASSERT(approvals, continue ); foreach (Utils::JsonValue *c, approvals->elements()) { Utils::JsonObjectValue *oc = c->toObject(); QTC_ASSERT(oc, break ); const int value = jsonIntMember(oc, approvalsValueKey); QString by; QString byMail; if (Utils::JsonValue *byV = oc->member(approvalsByKey)) { Utils::JsonObjectValue *byO = byV->toObject(); QTC_ASSERT(byO, break ); by = jsonStringMember(byO, ownerNameKey); byMail = jsonStringMember(byO, ownerEmailKey); } GerritApproval a; a.reviewer = by; a.email = byMail; a.approval = value; a.type = jsonStringMember(oc, approvalsTypeKey); a.description = jsonStringMember(oc, approvalsDescriptionKey); change->currentPatchSet.approvals.push_back(a); } qStableSort(change->currentPatchSet.approvals.begin(), change->currentPatchSet.approvals.end(), gerritApprovalLessThan); } } // patch set // Remaining change->number = jsonIntMember(object, numberKey); change->url = jsonStringMember(object, urlKey); if (change->url.isEmpty()) // No "canonicalWebUrl" is in gerrit.config. change->url = defaultUrl(parameters, change->number); change->id = jsonStringMember(object, idKey); change->title = jsonStringMember(object, titleKey); if (Utils::JsonValue *ownerV = object->member(ownerKey)) { Utils::JsonObjectValue *ownerO = ownerV->toObject(); QTC_ASSERT(ownerO, continue ); change->owner = jsonStringMember(ownerO, ownerNameKey); change->email = jsonStringMember(ownerO, ownerEmailKey); } change->project = jsonStringMember(object, projectKey); change->branch = jsonStringMember(object, branchKey); change->status = jsonStringMember(object, statusKey); if (const int timeT = jsonIntMember(object, lastUpdatedKey)) change->lastUpdated = QDateTime::fromTime_t(timeT); if (change->isValid()) { result.push_back(change); } else { QString errorMessage = GerritModel::tr("Parse error in line \"%1\"") .arg(QString::fromLocal8Bit(line)); VcsBase::VcsBaseOutputWindow::instance()->appendError(errorMessage); qWarning("%s: Parse error in line '%s'.", Q_FUNC_INFO, line.constData()); res = false; } // Read out dependencies if (Utils::JsonValue *dependsOnValue = object->member(dependsOnKey)) { if (Utils::JsonArrayValue *dependsOnArray = dependsOnValue->toArray()) { if (dependsOnArray->size()) { if (Utils::JsonObjectValue *dependsOnObject = dependsOnArray->elements().first()->toObject()) change->dependsOnId = jsonStringMember(dependsOnObject, idKey); } } } // Read out needed by if (Utils::JsonValue *neededByValue = object->member(neededByKey)) { if (Utils::JsonArrayValue *neededByArray = neededByValue->toArray()) { if (neededByArray->size()) { if (Utils::JsonObjectValue *neededByObject = neededByArray->elements().first()->toObject()) change->neededById = jsonStringMember(neededByObject, idKey); } } } } if (debug) { qDebug() << __FUNCTION__; foreach (const GerritChangePtr &p, result) qDebug() << *p; } return res; } #endif // QT_VERSION < 0x050000 QList GerritModel::changeToRow(const GerritChangePtr &c) const { QList row; const QVariant filterV = QVariant(c->filterString()); const QVariant changeV = qVariantFromValue(c); for (int i = 0; i < GerritModel::ColumnCount; ++i) { QStandardItem *item = new QStandardItem; item->setData(changeV, GerritModel::GerritChangeRole); item->setData(filterV, GerritModel::FilterRole); item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); row.append(item); } row[NumberColumn]->setText(QString::number(c->number)); row[TitleColumn]->setText(c->title); row[OwnerColumn]->setText(c->owner); // Shorten columns: Display time if it is today, else date const QString dateString = c->lastUpdated.date() == QDate::currentDate() ? c->lastUpdated.time().toString(Qt::SystemLocaleShortDate) : c->lastUpdated.date().toString(Qt::SystemLocaleShortDate); row[DateColumn]->setText(dateString); QString project = c->project; if (c->branch != QLatin1String("master")) project += QLatin1String(" (") + c->branch + QLatin1Char(')'); row[ProjectColumn]->setText(project); row[StatusColumn]->setText(c->status); row[ApprovalsColumn]->setText(c->currentPatchSet.approvalsColumn()); // Mark changes awaiting action using a bold font. bool bold = false; if (c->owner == m_userName) { // Owned changes: Review != 0,1. Submit or amend. const int level = c->currentPatchSet.approvalLevel(); bold = level != 0 && level != 1; } else if (m_query->currentQuery() == 1) { // Changes pending for review: No review yet. bold = !m_userName.isEmpty() && !c->currentPatchSet.hasApproval(m_userName); } if (bold) { QFont font = row.first()->font(); font.setBold(true); for (int i = 0; i < GerritModel::ColumnCount; ++i) row[i]->setFont(font); } return row; } bool gerritChangeLessThan(const GerritChangePtr &c1, const GerritChangePtr &c2) { if (c1->depth != c2->depth) return c1->depth < c2->depth; return c1->lastUpdated < c2->lastUpdated; } void GerritModel::queryFinished(const QByteArray &output) { QList changes; if (!parseOutput(m_parameters, output, changes)) emit queryError(); // Populate a hash with indices for faster access. QHash idIndexHash; const int count = changes.size(); for (int i = 0; i < count; ++i) idIndexHash.insert(changes.at(i)->id, i); // Mark root nodes: Changes that do not have a dependency, depend on a change // not in the list or on a change that is not "NEW". for (int i = 0; i < count; ++i) { if (changes.at(i)->dependsOnId.isEmpty()) { changes.at(i)->depth = 0; } else { const int dependsOnIndex = idIndexHash.value(changes.at(i)->dependsOnId, -1); if (dependsOnIndex < 0 || changes.at(dependsOnIndex)->status != QLatin1String("NEW")) changes.at(i)->depth = 0; } } // Indicate depth of dependent changes by using that of the parent + 1 until no more // changes occur. for (bool changed = true; changed; ) { changed = false; for (int i = 0; i < count; ++i) { if (changes.at(i)->depth < 0) { const int dependsIndex = idIndexHash.value(changes.at(i)->dependsOnId); const int dependsOnDepth = changes.at(dependsIndex)->depth; if (dependsOnDepth >= 0) { changes.at(i)->depth = dependsOnDepth + 1; changed = true; } } } } // Sort by depth (root nodes first) and by date. qStableSort(changes.begin(), changes.end(), gerritChangeLessThan); idIndexHash.clear(); foreach (const GerritChangePtr &c, changes) { // Avoid duplicate entries for example in the (unlikely) // case people do self-reviews. if (!itemForId(c->id)) { // Determine the verbose user name from the owner of the first query. // It used for marking the changes pending for review in bold. if (m_userName.isEmpty() && !m_query->currentQuery()) m_userName = c->owner; const QList newRow = changeToRow(c); if (c->depth) { QStandardItem *parent = itemForId(c->dependsOnId); // Append changes with depth > 1 to the parent with depth=1 to avoid // too-deeply nested items. for (; changeFromItem(parent)->depth >= 1; parent = parent->parent()) {} parent->appendRow(newRow); QString parentFilterString = parent->data(FilterRole).toString(); parentFilterString += QLatin1Char(' '); parentFilterString += newRow.first()->data(FilterRole).toString(); parent->setData(QVariant(parentFilterString), FilterRole); } else { appendRow(newRow); } } } } void GerritModel::queriesFinished() { m_query->deleteLater(); m_query = 0; emit refreshStateChanged(false); } } // namespace Internal } // namespace Gerrit #include "gerritmodel.moc"