gerritplugin.cpp 18.4 KB
Newer Older
hjk's avatar
hjk committed
1
/****************************************************************************
2
**
3 4
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
5
**
hjk's avatar
hjk committed
6
** This file is part of Qt Creator.
7
**
hjk's avatar
hjk committed
8 9 10 11
** 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
12 13 14
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
15
**
16 17 18 19 20 21 22
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23
**
hjk's avatar
hjk committed
24
****************************************************************************/
25 26 27 28 29 30

#include "gerritplugin.h"
#include "gerritparameters.h"
#include "gerritdialog.h"
#include "gerritmodel.h"
#include "gerritoptionspage.h"
31
#include "gerritpushdialog.h"
32

33 34 35 36
#include "../gitplugin.h"
#include "../gitclient.h"
#include "../gitversioncontrol.h"
#include "../gitconstants.h"
37
#include <vcsbase/vcsbaseconstants.h>
38
#include <vcsbase/vcsbaseeditor.h>
39 40 41 42 43 44 45 46 47 48 49

#include <coreplugin/icore.h>
#include <coreplugin/coreconstants.h>
#include <coreplugin/vcsmanager.h>
#include <coreplugin/progressmanager/progressmanager.h>
#include <coreplugin/progressmanager/futureprogress.h>
#include <coreplugin/documentmanager.h>
#include <coreplugin/actionmanager/actionmanager.h>
#include <coreplugin/actionmanager/actioncontainer.h>
#include <coreplugin/actionmanager/command.h>
#include <coreplugin/editormanager/editormanager.h>
50
#include <coreplugin/locator/commandlocator.h>
51

52
#include <vcsbase/vcsoutputwindow.h>
53 54

#include <utils/synchronousprocess.h>
55
#include <coreplugin/messagebox.h>
56 57 58 59 60 61

#include <QDebug>
#include <QProcess>
#include <QRegExp>
#include <QAction>
#include <QFileDialog>
62
#include <QMessageBox>
63 64
#include <QTemporaryFile>
#include <QDir>
65
#include <QMap>
66
#include <QFutureWatcher>
67

68
using namespace Core;
Orgad Shaneh's avatar
Orgad Shaneh committed
69
using namespace Git::Internal;
70

71 72 73 74 75
enum { debug = 0 };

namespace Gerrit {
namespace Constants {
const char GERRIT_OPEN_VIEW[] = "Gerrit.OpenView";
76
const char GERRIT_PUSH[] = "Gerrit.Push";
77 78 79 80 81 82
}
namespace Internal {

enum FetchMode
{
    FetchDisplay,
83
    FetchCherryPick,
84 85 86 87 88 89 90 91 92 93 94
    FetchCheckout
};

/* FetchContext: Retrieves the patch and displays
 * or applies it as desired. Does deleteLater() once it is done. */

class FetchContext : public QObject
{
     Q_OBJECT
public:
    FetchContext(const QSharedPointer<GerritChange> &change,
95
                 const QString &repository, const Utils::FileName &git,
96 97 98 99 100 101 102 103 104 105
                 const QSharedPointer<GerritParameters> &p,
                 FetchMode fm, QObject *parent = 0);
    ~FetchContext();

public slots:
    void start();

private:
    enum State
    {
106
        FetchState,
107 108 109 110
        DoneState,
        ErrorState
    };

Orgad Shaneh's avatar
Orgad Shaneh committed
111 112 113 114 115
    void processError(QProcess::ProcessError);
    void processFinished(int exitCode, QProcess::ExitStatus);
    void processReadyReadStandardError();
    void processReadyReadStandardOutput();

116
    void handleError(const QString &message);
117
    void show();
118
    void cherryPick();
119
    void checkout();
120
    void terminate();
121 122 123 124

    const QSharedPointer<GerritChange> m_change;
    const QString m_repository;
    const FetchMode m_fetchMode;
125
    const Utils::FileName m_git;
126 127 128 129
    const QSharedPointer<GerritParameters> m_parameters;
    State m_state;
    QProcess m_process;
    QFutureInterface<void> m_progress;
130
    QFutureWatcher<void> m_watcher;
131 132 133
};

FetchContext::FetchContext(const QSharedPointer<GerritChange> &change,
134
                           const QString &repository, const Utils::FileName &git,
135 136 137 138 139 140 141 142 143 144
                           const QSharedPointer<GerritParameters> &p,
                           FetchMode fm, QObject *parent)
    : QObject(parent)
    , m_change(change)
    , m_repository(repository)
    , m_fetchMode(fm)
    , m_git(git)
    , m_parameters(p)
    , m_state(FetchState)
{
145
    connect(&m_process, &QProcess::errorOccurred, this, &FetchContext::processError);
Tobias Hunger's avatar
Tobias Hunger committed
146 147 148 149 150 151 152
    connect(&m_process, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
            this, &FetchContext::processFinished);
    connect(&m_process, &QProcess::readyReadStandardError,
            this, &FetchContext::processReadyReadStandardError);
    connect(&m_process, &QProcess::readyReadStandardOutput,
            this, &FetchContext::processReadyReadStandardOutput);
    connect(&m_watcher, &QFutureWatcher<void>::canceled, this, &FetchContext::terminate);
153
    m_watcher.setFuture(m_progress.future());
154
    m_process.setWorkingDirectory(repository);
Tobias Hunger's avatar
Tobias Hunger committed
155
    m_process.setProcessEnvironment(GitPlugin::client()->processEnvironment());
156
    m_process.closeWriteChannel();
157 158 159 160 161 162 163
}

FetchContext::~FetchContext()
{
    if (m_progress.isRunning())
        m_progress.reportFinished();
    m_process.disconnect(this);
164
    terminate();
165 166 167 168 169
}

void FetchContext::start()
{
    m_progress.setProgressRange(0, 2);
170
    FutureProgress *fp = ProgressManager::addTask(m_progress.future(), tr("Fetching from Gerrit"),
171
                                           "gerrit-fetch");
172
    fp->setKeepOnFinish(FutureProgress::HideOnFinish);
173 174 175
    m_progress.reportStarted();
    // Order: initialize future before starting the process in case error handling is invoked.
    const QStringList args = m_change->gitFetchArguments(m_parameters);
176
    VcsBase::VcsOutputWindow::appendCommand(m_repository, m_git, args);
177
    m_process.start(m_git.toString(), args);
178 179 180 181 182 183
    m_process.closeWriteChannel();
}

void FetchContext::processFinished(int exitCode, QProcess::ExitStatus es)
{
    if (es != QProcess::NormalExit) {
184
        handleError(tr("%1 crashed.").arg(m_git.toUserOutput()));
185 186 187
        return;
    }
    if (exitCode) {
188
        handleError(tr("%1 returned %2.").arg(m_git.toUserOutput()).arg(exitCode));
189 190
        return;
    }
191
    if (m_state == FetchState) {
192
        m_progress.setProgressValue(m_progress.progressValue() + 1);
193 194 195 196 197 198 199 200 201 202
        if (m_fetchMode == FetchDisplay)
            show();
        else if (m_fetchMode == FetchCherryPick)
            cherryPick();
        else if (m_fetchMode == FetchCheckout)
            checkout();

        m_progress.reportFinished();
        m_state = DoneState;
        deleteLater();
203 204 205 206 207 208 209
    }
}

void FetchContext::processReadyReadStandardError()
{
    // Note: fetch displays progress on stderr.
    const QString errorOutput = QString::fromLocal8Bit(m_process.readAllStandardError());
210
    if (m_state == FetchState)
211
        VcsBase::VcsOutputWindow::append(errorOutput);
212
    else
213
        VcsBase::VcsOutputWindow::appendError(errorOutput);
214 215 216 217 218
}

void FetchContext::processReadyReadStandardOutput()
{
    const QByteArray output = m_process.readAllStandardOutput();
219
    VcsBase::VcsOutputWindow::append(QString::fromLocal8Bit(output));
220 221 222 223 224
}

void FetchContext::handleError(const QString &e)
{
    m_state = ErrorState;
225 226
    if (!m_progress.isCanceled())
        VcsBase::VcsOutputWindow::appendError(e);
227 228 229 230 231 232 233
    m_progress.reportCanceled();
    m_progress.reportFinished();
    deleteLater();
}

void FetchContext::processError(QProcess::ProcessError e)
{
234 235
    if (m_progress.isCanceled())
        return;
236
    const QString msg = tr("Error running %1: %2").arg(m_git.toUserOutput(), m_process.errorString());
237
    if (e == QProcess::FailedToStart)
238
        handleError(msg);
239
    else
240
        VcsBase::VcsOutputWindow::appendError(msg);
241 242
}

243
void FetchContext::show()
244
{
Orgad Shaneh's avatar
Orgad Shaneh committed
245
    const QString title = QString::number(m_change->number) + '/'
246
            + QString::number(m_change->currentPatchSet.patchSetNumber);
Orgad Shaneh's avatar
Orgad Shaneh committed
247
    GitPlugin::client()->show(m_repository, "FETCH_HEAD", title);
248 249
}

250
void FetchContext::cherryPick()
251
{
252
    // Point user to errors.
253
    VcsBase::VcsOutputWindow::instance()->popup(IOutputPane::ModeSwitch
254
                                                  | IOutputPane::WithFocus);
Orgad Shaneh's avatar
Orgad Shaneh committed
255
    GitPlugin::client()->synchronousCherryPick(m_repository, "FETCH_HEAD");
256 257
}

258 259
void FetchContext::checkout()
{
Orgad Shaneh's avatar
Orgad Shaneh committed
260
    GitPlugin::client()->stashAndCheckout(m_repository, "FETCH_HEAD");
261 262
}

263 264 265 266 267
void FetchContext::terminate()
{
    Utils::SynchronousProcess::stopProcess(m_process);
}

268

269 270 271
GerritPlugin::GerritPlugin(QObject *parent)
    : QObject(parent)
    , m_parameters(new GerritParameters)
272
    , m_gerritCommand(0), m_pushToGerritCommand(0)
273 274 275 276 277 278 279
{
}

GerritPlugin::~GerritPlugin()
{
}

280
bool GerritPlugin::initialize(ActionContainer *ac)
281
{
282
    m_parameters->fromSettings(ICore::settings());
283 284 285

    QAction *openViewAction = new QAction(tr("Gerrit..."), this);

Andre Hartmann's avatar
Andre Hartmann committed
286
    m_gerritCommand =
287
        ActionManager::registerAction(openViewAction, Constants::GERRIT_OPEN_VIEW);
Tobias Hunger's avatar
Tobias Hunger committed
288
    connect(openViewAction, &QAction::triggered, this, &GerritPlugin::openView);
Andre Hartmann's avatar
Andre Hartmann committed
289
    ac->addAction(m_gerritCommand);
290

291 292
    QAction *pushAction = new QAction(tr("Push to Gerrit..."), this);

Orgad Shaneh's avatar
Orgad Shaneh committed
293
    m_pushToGerritCommand =
294
        ActionManager::registerAction(pushAction, Constants::GERRIT_PUSH);
Tobias Hunger's avatar
Tobias Hunger committed
295
    connect(pushAction, &QAction::triggered, this, [this]() { push(); });
Orgad Shaneh's avatar
Orgad Shaneh committed
296
    ac->addAction(m_pushToGerritCommand);
297

Orgad Shaneh's avatar
Orgad Shaneh committed
298
    GitPlugin::instance()->addAutoReleasedObject(new GerritOptionsPage(m_parameters));
299 300 301
    return true;
}

302 303
void GerritPlugin::updateActions(bool hasTopLevel)
{
304
    m_gerritCommand->action()->setEnabled(hasTopLevel);
Orgad Shaneh's avatar
Orgad Shaneh committed
305
    m_pushToGerritCommand->action()->setEnabled(hasTopLevel);
306 307
}

308
void GerritPlugin::addToLocator(CommandLocator *locator)
309
{
Andre Hartmann's avatar
Andre Hartmann committed
310
    locator->appendCommand(m_gerritCommand);
Orgad Shaneh's avatar
Orgad Shaneh committed
311
    locator->appendCommand(m_pushToGerritCommand);
312 313
}

314
void GerritPlugin::push(const QString &topLevel)
315
{
316
    // QScopedPointer is required to delete the dialog when leaving the function
317
    GerritPushDialog dialog(topLevel, m_reviewers, ICore::mainWindow());
318

Tobias Hunger's avatar
Tobias Hunger committed
319
    if (!dialog.isValid()) {
320
        QMessageBox::warning(ICore::mainWindow(), tr("Initialization Failed"),
321 322 323 324
                              tr("Failed to initialize dialog. Aborting."));
        return;
    }

Orgad Shaneh's avatar
Orgad Shaneh committed
325
    if (dialog.exec() == QDialog::Rejected)
326 327
        return;

Orgad Shaneh's avatar
Orgad Shaneh committed
328
    m_reviewers = dialog.reviewers();
329

Orgad Shaneh's avatar
Orgad Shaneh committed
330
    QString target = dialog.selectedCommit();
331
    if (target.isEmpty())
Orgad Shaneh's avatar
Orgad Shaneh committed
332 333 334
        target = "HEAD";
    target += ":refs/" + dialog.selectedPushType() +
            '/' + dialog.selectedRemoteBranchName();
Orgad Shaneh's avatar
Orgad Shaneh committed
335
    const QString topic = dialog.selectedTopic();
336
    if (!topic.isEmpty())
Orgad Shaneh's avatar
Orgad Shaneh committed
337
        target += '/' + topic;
338 339

    QStringList options;
Orgad Shaneh's avatar
Orgad Shaneh committed
340
    const QStringList reviewers = m_reviewers.split(',', QString::SkipEmptyParts);
341
    foreach (const QString &reviewer, reviewers)
Orgad Shaneh's avatar
Orgad Shaneh committed
342
        options << "r=" + reviewer;
343 344

    if (!options.isEmpty())
Orgad Shaneh's avatar
Orgad Shaneh committed
345
        target += '%' + options.join(',');
346

Orgad Shaneh's avatar
Orgad Shaneh committed
347
    GitPlugin::client()->push(topLevel, { dialog.selectedRemoteName(), target });
348 349
}

350 351 352 353 354
// Open or raise the Gerrit dialog window.
void GerritPlugin::openView()
{
    if (m_dialog.isNull()) {
        while (!m_parameters->isValid()) {
355
            Core::AsynchronousMessageBox::warning(tr("Error"),
Tobias Hunger's avatar
Tobias Hunger committed
356
                                                  tr("Invalid Gerrit configuration. Host, user and ssh binary are mandatory."));
357
            if (!ICore::showOptionsDialog("Gerrit"))
358 359
                return;
        }
360
        GerritDialog *gd = new GerritDialog(m_parameters, ICore::mainWindow());
361
        gd->setModal(false);
Tobias Hunger's avatar
Tobias Hunger committed
362 363 364 365 366
        connect(gd, &GerritDialog::fetchDisplay, this, &GerritPlugin::fetchDisplay);
        connect(gd, &GerritDialog::fetchCherryPick, this, &GerritPlugin::fetchCherryPick);
        connect(gd, &GerritDialog::fetchCheckout, this, &GerritPlugin::fetchCheckout);
        connect(this, &GerritPlugin::fetchStarted, gd, &GerritDialog::fetchStarted);
        connect(this, &GerritPlugin::fetchFinished, gd, &GerritDialog::fetchFinished);
367 368
        m_dialog = gd;
    }
369 370
    if (!m_dialog->isVisible())
        m_dialog->setCurrentPath(GitPlugin::instance()->currentState().topLevel());
Orgad Shaneh's avatar
Orgad Shaneh committed
371
    const Qt::WindowStates state = m_dialog->windowState();
372
    if (state & Qt::WindowMinimized)
Orgad Shaneh's avatar
Orgad Shaneh committed
373 374 375
        m_dialog->setWindowState(state & ~Qt::WindowMinimized);
    m_dialog->show();
    m_dialog->raise();
376 377
}

378 379
void GerritPlugin::push()
{
Orgad Shaneh's avatar
Orgad Shaneh committed
380
    push(GitPlugin::instance()->currentState().topLevel());
381 382
}

383 384
Utils::FileName GerritPlugin::gitBinDirectory()
{
Tobias Hunger's avatar
Tobias Hunger committed
385
    return GitPlugin::client()->gitBinDirectory();
386 387
}

388 389 390
// Find the branch of a repository.
QString GerritPlugin::branch(const QString &repository)
{
Tobias Hunger's avatar
Tobias Hunger committed
391
    return GitPlugin::client()->synchronousCurrentLocalBranch(repository);
392 393
}

Orgad Shaneh's avatar
Orgad Shaneh committed
394
void GerritPlugin::fetchDisplay(const QSharedPointer<GerritChange> &change)
395 396 397 398
{
    fetch(change, FetchDisplay);
}

Orgad Shaneh's avatar
Orgad Shaneh committed
399
void GerritPlugin::fetchCherryPick(const QSharedPointer<GerritChange> &change)
400
{
401
    fetch(change, FetchCherryPick);
402 403
}

Orgad Shaneh's avatar
Orgad Shaneh committed
404
void GerritPlugin::fetchCheckout(const QSharedPointer<GerritChange> &change)
405 406 407 408
{
    fetch(change, FetchCheckout);
}

Orgad Shaneh's avatar
Orgad Shaneh committed
409
void GerritPlugin::fetch(const QSharedPointer<GerritChange> &change, int mode)
410 411
{
    // Locate git.
Tobias Hunger's avatar
Tobias Hunger committed
412
    const Utils::FileName git = GitPlugin::client()->vcsBinary();
413 414
    if (git.isEmpty()) {
        VcsBase::VcsOutputWindow::appendError(tr("Git is not available."));
415
        return;
416
    }
417

418 419
    QString repository;
    bool verifiedRepository = false;
420
    if (!m_dialog.isNull() && !m_parameters.isNull() && QFile::exists(m_dialog->repositoryPath()))
421
        repository = VcsManager::findTopLevelForDirectory(m_dialog->repositoryPath());
Petar Perisin's avatar
Petar Perisin committed
422

423 424
    if (!repository.isEmpty()) {
        // Check if remote from a working dir is the same as remote from patch
Tobias Hunger's avatar
Tobias Hunger committed
425
        QMap<QString, QString> remotesList = GitPlugin::client()->synchronousRemotesList(repository);
426 427 428
        if (!remotesList.isEmpty()) {
            QStringList remotes = remotesList.values();
            foreach (QString remote, remotes) {
Orgad Shaneh's avatar
Orgad Shaneh committed
429
                if (remote.endsWith(".git"))
430 431 432 433 434 435 436
                    remote.chop(4);
                if (remote.contains(m_parameters->host) && remote.endsWith(change->project)) {
                    verifiedRepository = true;
                    break;
                }
            }

437
            if (!verifiedRepository) {
Tobias Hunger's avatar
Tobias Hunger committed
438
                SubmoduleDataMap submodules = GitPlugin::client()->submoduleList(repository);
Orgad Shaneh's avatar
Orgad Shaneh committed
439
                foreach (const SubmoduleData &submoduleData, submodules) {
440
                    QString remote = submoduleData.url;
Orgad Shaneh's avatar
Orgad Shaneh committed
441
                    if (remote.endsWith(".git"))
442 443
                        remote.chop(4);
                    if (remote.contains(m_parameters->host) && remote.endsWith(change->project)
Orgad Shaneh's avatar
Orgad Shaneh committed
444 445
                            && QFile::exists(repository + '/' + submoduleData.dir)) {
                        repository = QDir::cleanPath(repository + '/'
446
                                                     + submoduleData.dir);
447 448 449 450 451 452
                        verifiedRepository = true;
                        break;
                    }
                }
            }

Petar Perisin's avatar
Petar Perisin committed
453
            if (!verifiedRepository) {
454
                QMessageBox::StandardButton answer = QMessageBox::question(
455
                            ICore::mainWindow(), tr("Remote Not Verified"),
456
                            tr("Change host %1\nand project %2\n\nwere not verified among remotes"
457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478
                               " in %3. Select different folder?")
                            .arg(m_parameters->host,
                                 change->project,
                                 QDir::toNativeSeparators(repository)),
                            QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel,
                            QMessageBox::Yes);
                switch (answer) {
                case QMessageBox::Cancel:
                    return;
                case QMessageBox::No:
                    verifiedRepository = true;
                    break;
                default:
                    break;
                }
            }
        }
    }

    if (!verifiedRepository) {
        // Ask the user for a repository to retrieve the change.
        const QString title =
479
                tr("Enter Local Repository for \"%1\" (%2)").arg(change->project, change->branch);
480 481 482 483 484 485
        const QString suggestedRespository =
                findLocalRepository(change->project, change->branch);
        repository = QFileDialog::getExistingDirectory(m_dialog.data(),
                                                       title, suggestedRespository);
    }

486 487 488 489 490
    if (repository.isEmpty())
        return;

    FetchContext *fc = new FetchContext(change, repository, git,
                                        m_parameters, FetchMode(mode), this);
Tobias Hunger's avatar
Tobias Hunger committed
491
    connect(fc, &QObject::destroyed, this, &GerritPlugin::fetchFinished);
492
    emit fetchStarted(change);
493 494 495 496 497 498
    fc->start();
}

// Try to find a matching repository for a project by asking the VcsManager.
QString GerritPlugin::findLocalRepository(QString project, const QString &branch) const
{
Orgad Shaneh's avatar
Orgad Shaneh committed
499
    const QStringList gitRepositories = VcsManager::repositories(GitPlugin::instance()->gitVersionControl());
500
    // Determine key (file name) to look for (qt/qtbase->'qtbase').
Orgad Shaneh's avatar
Orgad Shaneh committed
501
    const int slashPos = project.lastIndexOf('/');
502 503 504 505 506
    if (slashPos != -1)
        project.remove(0, slashPos + 1);
    // When looking at branch 1.7, try to check folders
    // "qtbase_17", 'qtbase1.7' with a semi-smart regular expression.
    QScopedPointer<QRegExp> branchRegexp;
Orgad Shaneh's avatar
Orgad Shaneh committed
507
    if (!branch.isEmpty() && branch != "master") {
508
        QString branchPattern = branch;
Orgad Shaneh's avatar
Orgad Shaneh committed
509 510 511 512
        branchPattern.replace('.', "[\\.-_]?");
        const QString pattern = '^' + project
                                + "[-_]?"
                                + branchPattern + '$';
513 514 515 516 517
        branchRegexp.reset(new QRegExp(pattern));
        if (!branchRegexp->isValid())
            branchRegexp.reset(); // Oops.
    }
    foreach (const QString &repository, gitRepositories) {
518
        const QString fileName = Utils::FileName::fromString(repository).fileName();
519 520 521 522 523 524 525 526 527 528 529 530 531
        if ((!branchRegexp.isNull() && branchRegexp->exactMatch(fileName))
            || fileName == project) {
            // Perform a check on the branch.
            if (branch.isEmpty())  {
                return repository;
            } else {
                const QString repositoryBranch = GerritPlugin::branch(repository);
                if (repositoryBranch.isEmpty() || repositoryBranch == branch)
                    return repository;
            } // !branch.isEmpty()
        } // branchRegexp or file name match
    } // for repositories
    // No match, do we have  a projects folder?
532
    if (DocumentManager::useProjectsDirectory())
533
        return DocumentManager::projectsDirectory();
534 535 536 537 538 539 540 541

    return QDir::currentPath();
}

} // namespace Internal
} // namespace Gerrit

#include "gerritplugin.moc"