<commit_before>/****************************************************************************
**
** 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 <coreplugin/progressmanager/progressmanager.h>
#include <coreplugin/progressmanager/futureprogress.h>
#include <vcsbase/vcsoutputwindow.h>
#include <utils/synchronousprocess.h>

#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QStringList>
#include <QProcess>
#include <QVariant>
#include <QTextStream>
#include <QMessageBox>
#include <QPushButton>
#include <QDebug>
#include <QScopedPointer>
#include <QTimer>
#include <QApplication>

enum { debug = 0 };

using namespace VcsBase;

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<GerritParameters> &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:
// "<tr><td>Code Review</td><td><NAME>: -1, ...</tr><tr>...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 << "</tr>\n";
            str << "<tr><td>"
                << (a.description.isEmpty() ? a.type : a.description)
                << "</td><td>";
            lastType = a.type;
        } else {
            str << ", ";
        }
        str << a.reviewer;
        if (!a.email.isEmpty())
            str << " <a href=\"mailto:" << a.email << "\">" << a.email << "</a>";
        str << ": " << forcesign << a.approval << noforcesign;
    }
    str << "</tr>\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<QChar, int> 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<GerritParameters> &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<GerritParameters> &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<GerritParameters> m_parameters;
    const QStringList m_queries;
    QProcess m_process;
    QTimer m_timer;
    QString m_binary;
    QByteArray m_output;
    int m_currentQuery;
    QFutureInterface<void> m_progress;
    QStringList m_baseArguments;
};

enum { timeOutMS = 30000 };

QueryContext::QueryContext(const QStringList &queries,
                           const QSharedPointer<GerritParameters> &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);
    VcsOutputWindow::appendCommand(
                m_process.workingDirectory(), Utils::FileName::fromString(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();
    VcsOutputWindow::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
        VcsOutputWindow::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()
{
    VcsOutputWindow::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<GerritParameters> &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<GerritChangePtr>(item->data(GerritModel::GerritChangeRole));
}

GerritChangePtr GerritModel::change(const QModelIndex &index) const
{
    if (index.isValid())
        return changeFromItem(itemFromIndex(index));
    return GerritChangePtr(new GerritChange);<commit_msg>Gerrit: Support cancel in query context<commit_after>/****************************************************************************
**
** 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 <coreplugin/progressmanager/progressmanager.h>
#include <coreplugin/progressmanager/futureprogress.h>
#include <vcsbase/vcsoutputwindow.h>
#include <utils/synchronousprocess.h>

#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QStringList>
#include <QProcess>
#include <QVariant>
#include <QTextStream>
#include <QMessageBox>
#include <QPushButton>
#include <QDebug>
#include <QScopedPointer>
#include <QTimer>
#include <QApplication>
#include <QFutureWatcher>

enum { debug = 0 };

using namespace VcsBase;

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<GerritParameters> &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:
// "<tr><td>Code Review</td><td><NAME>: -1, ...</tr><tr>...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 << "</tr>\n";
            str << "<tr><td>"
                << (a.description.isEmpty() ? a.type : a.description)
                << "</td><td>";
            lastType = a.type;
        } else {
            str << ", ";
        }
        str << a.reviewer;
        if (!a.email.isEmpty())
            str << " <a href=\"mailto:" << a.email << "\">" << a.email << "</a>";
        str << ": " << forcesign << a.approval << noforcesign;
    }
    str << "</tr>\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<QChar, int> 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<GerritParameters> &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<GerritParameters> &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);
    void terminate();

    const QSharedPointer<GerritParameters> m_parameters;
    const QStringList m_queries;
    QProcess m_process;
    QTimer m_timer;
    QString m_binary;
    QByteArray m_output;
    int m_currentQuery;
    QFutureInterface<void> m_progress;
    QFutureWatcher<void> m_watcher;
    QStringList m_baseArguments;
};

enum { timeOutMS = 30000 };

QueryContext::QueryContext(const QStringList &queries,
                           const QSharedPointer<GerritParameters> &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)));
    connect(&m_watcher, &QFutureWatcherBase::canceled,
            this, &QueryContext::terminate);
    m_watcher.setFuture(m_progress.future());
    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);
    terminate();
}

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);
    VcsOutputWindow::appendCommand(
                m_process.workingDirectory(), Utils::FileName::fromString(m_binary), arguments);
    m_timer.start();
    m_process.start(m_binary, arguments);
    m_process.closeWriteChannel();
}

void QueryContext::errorTermination(const QString &msg)
{
    if (!m_progress.isCanceled())
        VcsOutputWindow::appendError(msg);
    m_progress.reportCanceled();
    m_progress.reportFinished();
    emit finished();
}

void QueryContext::terminate()
{
    Utils::SynchronousProcess::stopProcess(m_process);
}

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
        VcsOutputWindow::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()
{
    VcsOutputWindow::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)
        terminate();
    else
        m_timer.start();
}

GerritModel::GerritModel(const QSharedPointer<GerritParameters> &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<GerritChangePtr>(item->data(GerritModel::GerritChangeRole));
}

GerritChangePtr GerritModel::change(const QModelIndex &index) const
{
    if (index.isValid())
        return changeFromItem(itemFromIndex(index));
    return GerritChangePtr(new GerritChange);
{'fraction_non_alphanumeric': 0.14034652588314495, 'fraction_numerical': 0.004818764837189527, 'mean_word_length': 3.4321608040201004, 'pattern_counts': {'":': 4, '<': 335, '<?xml version=': 0, '>': 113, 'https://': 4, 'lorem ipsum': 0, 'www.': 10, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4654598', 'n_tokens_mistral': 8502, 'n_tokens_neox': 8208, 'n_words': 2004}