/**************************************************************************** ** ** 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 "gerritplugin.h" #include "gerritparameters.h" #include "gerritdialog.h" #include "gerritmodel.h" #include "gerritoptionspage.h" #include "gerritpushdialog.h" #include "../gitplugin.h" #include "../gitclient.h" #include "../gitversioncontrol.h" #include "../gitconstants.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Core; using namespace Git::Internal; enum { debug = 0 }; namespace Gerrit { namespace Constants { const char GERRIT_OPEN_VIEW[] = "Gerrit.OpenView"; const char GERRIT_PUSH[] = "Gerrit.Push"; } namespace Internal { enum FetchMode { FetchDisplay, FetchCherryPick, FetchCheckout }; static inline GitClient *gitClient() { return GitPlugin::instance()->gitClient(); } /* 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 &change, const QString &repository, const QString &git, const QSharedPointer &p, FetchMode fm, QObject *parent = 0); ~FetchContext(); public slots: void start(); private slots: void processError(QProcess::ProcessError); void processFinished(int exitCode, QProcess::ExitStatus); void processReadyReadStandardError(); void processReadyReadStandardOutput(); private: enum State { FetchState, DoneState, ErrorState }; void handleError(const QString &message); void show(); void cherryPick(); void checkout(); const QSharedPointer m_change; const QString m_repository; const FetchMode m_fetchMode; const QString m_git; const QSharedPointer m_parameters; State m_state; QProcess m_process; QFutureInterface m_progress; }; FetchContext::FetchContext(const QSharedPointer &change, const QString &repository, const QString &git, const QSharedPointer &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) { connect(&m_process, SIGNAL(error(QProcess::ProcessError)), this, SLOT(processError(QProcess::ProcessError))); connect(&m_process, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(processFinished(int,QProcess::ExitStatus))); connect(&m_process, SIGNAL(readyReadStandardError()), this, SLOT(processReadyReadStandardError())); connect(&m_process, SIGNAL(readyReadStandardOutput()), this, SLOT(processReadyReadStandardOutput())); m_process.setWorkingDirectory(repository); m_process.setProcessEnvironment(gitClient()->processEnvironment()); m_process.closeWriteChannel(); } FetchContext::~FetchContext() { if (m_progress.isRunning()) m_progress.reportFinished(); m_process.disconnect(this); Utils::SynchronousProcess::stopProcess(m_process); } void FetchContext::start() { m_progress.setProgressRange(0, 2); FutureProgress *fp = ProgressManager::addTask(m_progress.future(), tr("Fetching from Gerrit"), "gerrit-fetch"); fp->setKeepOnFinish(FutureProgress::HideOnFinish); m_progress.reportStarted(); // Order: initialize future before starting the process in case error handling is invoked. const QStringList args = m_change->gitFetchArguments(m_parameters); VcsBase::VcsBaseOutputWindow::instance()->appendCommand(m_repository, m_git, args); m_process.start(m_git, args); m_process.closeWriteChannel(); } void FetchContext::processFinished(int exitCode, QProcess::ExitStatus es) { if (es != QProcess::NormalExit) { handleError(tr("%1 crashed.").arg(m_git)); return; } if (exitCode) { handleError(tr("%1 returned %2.").arg(m_git).arg(exitCode)); return; } if (m_state == FetchState) { m_progress.setProgressValue(m_progress.progressValue() + 1); if (m_fetchMode == FetchDisplay) show(); else if (m_fetchMode == FetchCherryPick) cherryPick(); else if (m_fetchMode == FetchCheckout) checkout(); m_progress.reportFinished(); m_state = DoneState; deleteLater(); } } void FetchContext::processReadyReadStandardError() { // Note: fetch displays progress on stderr. const QString errorOutput = QString::fromLocal8Bit(m_process.readAllStandardError()); if (m_state == FetchState) VcsBase::VcsBaseOutputWindow::instance()->append(errorOutput); else VcsBase::VcsBaseOutputWindow::instance()->appendError(errorOutput); } void FetchContext::processReadyReadStandardOutput() { const QByteArray output = m_process.readAllStandardOutput(); VcsBase::VcsBaseOutputWindow::instance()->append(QString::fromLocal8Bit(output)); } void FetchContext::handleError(const QString &e) { m_state = ErrorState; VcsBase::VcsBaseOutputWindow::instance()->appendError(e); m_progress.reportCanceled(); m_progress.reportFinished(); deleteLater(); } void FetchContext::processError(QProcess::ProcessError e) { const QString msg = tr("Error running %1: %2").arg(m_git, m_process.errorString()); if (e == QProcess::FailedToStart) handleError(msg); else VcsBase::VcsBaseOutputWindow::instance()->appendError(msg); } void FetchContext::show() { const QString title = QString::number(m_change->number) + QLatin1Char('/') + QString::number(m_change->currentPatchSet.patchSetNumber); gitClient()->show(m_repository, QLatin1String("FETCH_HEAD"), title); } void FetchContext::cherryPick() { // Point user to errors. VcsBase::VcsBaseOutputWindow::instance()->popup(IOutputPane::ModeSwitch | IOutputPane::WithFocus); gitClient()->synchronousCherryPick(m_repository, QLatin1String("FETCH_HEAD")); } void FetchContext::checkout() { gitClient()->stashAndCheckout(m_repository, QLatin1String("FETCH_HEAD")); } GerritPlugin::GerritPlugin(QObject *parent) : QObject(parent) , m_parameters(new GerritParameters) { } GerritPlugin::~GerritPlugin() { } bool GerritPlugin::initialize(ActionContainer *ac) { m_parameters->fromSettings(ICore::settings()); QAction *openViewAction = new QAction(tr("Gerrit..."), this); m_gerritCommand = ActionManager::registerAction(openViewAction, Constants::GERRIT_OPEN_VIEW, Context(Core::Constants::C_GLOBAL)); connect(openViewAction, SIGNAL(triggered()), this, SLOT(openView())); ac->addAction(m_gerritCommand); QAction *pushAction = new QAction(tr("Push to Gerrit..."), this); m_pushToGerritCommand = ActionManager::registerAction(pushAction, Constants::GERRIT_PUSH, Context(Core::Constants::C_GLOBAL)); connect(pushAction, SIGNAL(triggered()), this, SLOT(push())); ac->addAction(m_pushToGerritCommand); GitPlugin::instance()->addAutoReleasedObject(new GerritOptionsPage(m_parameters)); return true; } void GerritPlugin::updateActions(bool hasTopLevel) { m_gerritCommand->action()->setEnabled(hasTopLevel); m_pushToGerritCommand->action()->setEnabled(hasTopLevel); } void GerritPlugin::addToLocator(Core::CommandLocator *locator) { locator->appendCommand(m_gerritCommand); locator->appendCommand(m_pushToGerritCommand); } void GerritPlugin::push(const QString &topLevel) { // QScopedPointer is required to delete the dialog when leaving the function GerritPushDialog dialog(topLevel, m_reviewers, ICore::mainWindow()); if (!dialog.valid()) { QMessageBox::warning(ICore::mainWindow(), tr("Initialization Failed"), tr("Failed to initialize dialog. Aborting.")); return; } if (dialog.exec() == QDialog::Rejected) return; QStringList args; m_reviewers = dialog.reviewers(); args << dialog.selectedRemoteName(); QString target = dialog.selectedCommit(); if (target.isEmpty()) target = QLatin1String("HEAD"); target += QLatin1String(":refs/") + dialog.selectedPushType() + QLatin1Char('/') + dialog.selectedRemoteBranchName(); const QString topic = dialog.selectedTopic(); if (!topic.isEmpty()) target += QLatin1Char('/') + topic; QStringList options; const QStringList reviewers = m_reviewers.split(QLatin1Char(','), QString::SkipEmptyParts); foreach (const QString &reviewer, reviewers) options << QLatin1String("r=") + reviewer; if (!options.isEmpty()) target += QLatin1Char('%') + options.join(QLatin1String(",")); args << target; gitClient()->push(topLevel, args); } // Open or raise the Gerrit dialog window. void GerritPlugin::openView() { if (m_dialog.isNull()) { while (!m_parameters->isValid()) { QMessageBox::warning(ICore::dialogParent(), tr("Error"), tr("Invalid Gerrit configuration. Host, user and ssh binary are mandatory.")); const Id group = VcsBase::Constants::VCS_SETTINGS_CATEGORY; if (!ICore::showOptionsDialog(group, "Gerrit")) return; } GerritDialog *gd = new GerritDialog(m_parameters, ICore::mainWindow()); gd->setModal(false); connect(gd, SIGNAL(fetchDisplay(QSharedPointer)), this, SLOT(fetchDisplay(QSharedPointer))); connect(gd, SIGNAL(fetchCherryPick(QSharedPointer)), this, SLOT(fetchCherryPick(QSharedPointer))); connect(gd, SIGNAL(fetchCheckout(QSharedPointer)), this, SLOT(fetchCheckout(QSharedPointer))); connect(this, SIGNAL(fetchStarted(QSharedPointer)), gd, SLOT(fetchStarted(QSharedPointer))); connect(this, SIGNAL(fetchFinished()), gd, SLOT(fetchFinished())); m_dialog = gd; } if (!m_dialog->isVisible()) m_dialog->setCurrentPath(GitPlugin::instance()->currentState().topLevel()); const Qt::WindowStates state = m_dialog->windowState(); if (state & Qt::WindowMinimized) m_dialog->setWindowState(state & ~Qt::WindowMinimized); m_dialog->show(); m_dialog->raise(); } void GerritPlugin::push() { push(GitPlugin::instance()->currentState().topLevel()); } QString GerritPlugin::gitBinary() { bool ok; const QString git = gitClient()->gitExecutable(&ok); if (!ok) { VcsBase::VcsBaseOutputWindow::instance()->appendError(tr("Git is not available.")); return QString(); } return git; } Utils::FileName GerritPlugin::gitBinDirectory() { return gitClient()->gitBinDirectory(); } // Find the branch of a repository. QString GerritPlugin::branch(const QString &repository) { return gitClient()->synchronousCurrentLocalBranch(repository); } void GerritPlugin::fetchDisplay(const QSharedPointer &change) { fetch(change, FetchDisplay); } void GerritPlugin::fetchCherryPick(const QSharedPointer &change) { fetch(change, FetchCherryPick); } void GerritPlugin::fetchCheckout(const QSharedPointer &change) { fetch(change, FetchCheckout); } void GerritPlugin::fetch(const QSharedPointer &change, int mode) { // Locate git. const QString git = gitBinary(); if (git.isEmpty()) return; GitClient *client = gitClient(); QString repository; bool verifiedRepository = false; if (!m_dialog.isNull() && !m_parameters.isNull() && QFile::exists(m_dialog->repositoryPath())) repository = Core::VcsManager::findTopLevelForDirectory(m_dialog->repositoryPath()); if (!repository.isEmpty()) { // Check if remote from a working dir is the same as remote from patch QMap remotesList = client->synchronousRemotesList(repository); if (!remotesList.isEmpty()) { QStringList remotes = remotesList.values(); foreach (QString remote, remotes) { if (remote.endsWith(QLatin1String(".git"))) remote.chop(4); if (remote.contains(m_parameters->host) && remote.endsWith(change->project)) { verifiedRepository = true; break; } } if (!verifiedRepository) { SubmoduleDataMap submodules = client->submoduleList(repository); foreach (const SubmoduleData &submoduleData, submodules) { QString remote = submoduleData.url; if (remote.endsWith(QLatin1String(".git"))) remote.chop(4); if (remote.contains(m_parameters->host) && remote.endsWith(change->project) && QFile::exists(repository + QLatin1Char('/') + submoduleData.dir)) { repository = QDir::cleanPath(repository + QLatin1Char('/') + submoduleData.dir); verifiedRepository = true; break; } } } if (!verifiedRepository) { QMessageBox::StandardButton answer = QMessageBox::question( ICore::mainWindow(), tr("Remote Not Verified"), tr("Change host %1\nand project %2\n\nwere not verified among remotes" " 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 = tr("Enter Local Repository for \"%1\" (%2)").arg(change->project, change->branch); const QString suggestedRespository = findLocalRepository(change->project, change->branch); repository = QFileDialog::getExistingDirectory(m_dialog.data(), title, suggestedRespository); } if (repository.isEmpty()) return; FetchContext *fc = new FetchContext(change, repository, git, m_parameters, FetchMode(mode), this); connect(fc, SIGNAL(destroyed(QObject*)), this, SIGNAL(fetchFinished())); emit fetchStarted(change); fc->start(); } // Try to find a matching repository for a project by asking the VcsManager. QString GerritPlugin::findLocalRepository(QString project, const QString &branch) const { const QStringList gitRepositories = VcsManager::repositories(GitPlugin::instance()->gitVersionControl()); // Determine key (file name) to look for (qt/qtbase->'qtbase'). const int slashPos = project.lastIndexOf(QLatin1Char('/')); 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 branchRegexp; if (!branch.isEmpty() && branch != QLatin1String("master")) { QString branchPattern = branch; branchPattern.replace(QLatin1String("."), QLatin1String("[\\.-_]?")); const QString pattern = QLatin1Char('^') + project + QLatin1String("[-_]?") + branchPattern + QLatin1Char('$'); branchRegexp.reset(new QRegExp(pattern)); if (!branchRegexp->isValid()) branchRegexp.reset(); // Oops. } foreach (const QString &repository, gitRepositories) { const QString fileName = QFileInfo(repository).fileName(); 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? if (Core::DocumentManager::useProjectsDirectory()) return DocumentManager::projectsDirectory(); return QDir::currentPath(); } } // namespace Internal } // namespace Gerrit #include "gerritplugin.moc"