diff --git a/plugins/bazaar/bazaarplugin.cpp b/plugins/bazaar/bazaarplugin.cpp index 31853d928a..e4463a5ff7 100644 --- a/plugins/bazaar/bazaarplugin.cpp +++ b/plugins/bazaar/bazaarplugin.cpp @@ -1,336 +1,346 @@ /*************************************************************************** * Copyright 2013-2014 Maciej Poleski * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "bazaarplugin.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "bazaarutils.h" #include "bzrannotatejob.h" #include "copyjob.h" #include "diffjob.h" using namespace KDevelop; BazaarPlugin::BazaarPlugin(QObject* parent, const QVariantList& args) : IPlugin(QStringLiteral("kdevbazaar"), parent), m_vcsPluginHelper(new KDevelop::VcsPluginHelper(this, this)) { Q_UNUSED(args); // What is this? if (QStandardPaths::findExecutable(QStringLiteral("bzr")).isEmpty()) { setErrorDescription(i18n("Unable to find Bazaar (bzr) executable Is it installed on the system?")); return; } setObjectName(QStringLiteral("Bazaar")); } BazaarPlugin::~BazaarPlugin() { } QString BazaarPlugin::name() const { return QStringLiteral("Bazaar"); } +bool BazaarPlugin::isValidRemoteRepositoryUrl(const QUrl& remoteLocation) +{ + const QString scheme = remoteLocation.scheme(); + if (scheme == QLatin1String("bzr") || + scheme == QLatin1String("bzr+ssh")) { + return true; + } + return false; +} + VcsJob* BazaarPlugin::add(const QList& localLocations, IBasicVersionControl::RecursionMode recursion) { DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localLocations[0]), this); job->setType(VcsJob::Add); *job << "bzr" << "add"; if(recursion == NonRecursive) *job << "--no-recurse"; *job << localLocations; return job; } VcsJob* BazaarPlugin::annotate(const QUrl& localLocation, const VcsRevision& rev) { VcsJob* job = new BzrAnnotateJob(BazaarUtils::workingCopy(localLocation), BazaarUtils::getRevisionSpec(rev), localLocation, this, KDevelop::OutputJob::Silent); return job; } VcsJob* BazaarPlugin::commit(const QString& message, const QList& localLocations, IBasicVersionControl::RecursionMode recursion) { QDir dir = BazaarUtils::workingCopy(localLocations[0]); DVcsJob* job = new DVcsJob(dir, this); job->setType(VcsJob::Commit); *job << "bzr" << "commit" << BazaarUtils::handleRecursion(localLocations, recursion) << "-m" << message; return job; } VcsJob* BazaarPlugin::copy(const QUrl& localLocationSrc, const QUrl& localLocationDstn) { return new CopyJob(localLocationSrc, localLocationDstn, this); } VcsImportMetadataWidget* BazaarPlugin::createImportMetadataWidget(QWidget* parent) { return new DvcsImportMetadataWidget(parent); } VcsJob* BazaarPlugin::createWorkingCopy(const VcsLocation& sourceRepository, const QUrl& destinationDirectory, IBasicVersionControl::RecursionMode recursion) { Q_UNUSED(recursion); // What is the purpose of recursion parameter? DVcsJob* job = new DVcsJob(BazaarUtils::toQDir(sourceRepository.localUrl()), this); job->setType(VcsJob::Import); *job << "bzr" << "branch" << sourceRepository.localUrl().url() << destinationDirectory; return job; } VcsJob* BazaarPlugin::diff(const QUrl& fileOrDirectory, const VcsRevision& srcRevision, const VcsRevision& dstRevision, VcsDiff::Type, IBasicVersionControl::RecursionMode recursion) { Q_UNUSED(recursion); VcsJob* job = new DiffJob(BazaarUtils::workingCopy(fileOrDirectory), BazaarUtils::getRevisionSpecRange(srcRevision, dstRevision), fileOrDirectory, this); return job; } VcsJob* BazaarPlugin::init(const QUrl& localRepositoryRoot) { DVcsJob* job = new DVcsJob(BazaarUtils::toQDir(localRepositoryRoot), this); job->setType(VcsJob::Import); *job << "bzr" << "init"; return job; } bool BazaarPlugin::isVersionControlled(const QUrl& localLocation) { QDir workCopy = BazaarUtils::workingCopy(localLocation); DVcsJob* job = new DVcsJob(workCopy, this, OutputJob::Silent); job->setType(VcsJob::Unknown); job->setIgnoreError(true); *job << "bzr" << "ls" << "--from-root" << "-R" << "-V"; job->exec(); if (job->status() == VcsJob::JobSucceeded) { QList filesAndDirectoriesList; foreach (const QString& fod, job->output().split('\n')) { filesAndDirectoriesList.append(QFileInfo(workCopy.absolutePath() + QDir::separator() + fod)); } QFileInfo fi(localLocation.toLocalFile()); if (fi.isDir() || fi.isFile()) { QFileInfo file(localLocation.toLocalFile()); return filesAndDirectoriesList.contains(file); } } return false; } VcsJob* BazaarPlugin::log(const QUrl& localLocation, const VcsRevision& rev, long unsigned int limit) { DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localLocation), this); job->setType(VcsJob::Log); *job << "bzr" << "log" << "--long" << "-v" << localLocation << BazaarUtils::getRevisionSpecRange(rev) << "-l" << QString::number(limit); connect(job, &DVcsJob::readyForParsing, this, &BazaarPlugin::parseBzrLog); return job; } VcsJob* BazaarPlugin::log(const QUrl& localLocation, const VcsRevision& rev, const VcsRevision& limit) { DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localLocation), this); job->setType(VcsJob::Log); *job << "bzr" << "log" << "--long" << "-v" << localLocation << BazaarUtils::getRevisionSpecRange(limit, rev); connect(job, &DVcsJob::readyForParsing, this, &BazaarPlugin::parseBzrLog); return job; } void BazaarPlugin::parseBzrLog(DVcsJob* job) { QVariantList result; auto parts = job->output().split(QStringLiteral("------------------------------------------------------------"), QString::SkipEmptyParts); foreach (const QString& part, parts) { auto event = BazaarUtils::parseBzrLogPart(part); if (event.revision().revisionType() != VcsRevision::Invalid) result.append(QVariant::fromValue(event)); } job->setResults(result); } VcsJob* BazaarPlugin::move(const QUrl& localLocationSrc, const QUrl& localLocationDst) { DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localLocationSrc), this); job->setType(VcsJob::JobType::Move); *job << "bzr" << "move" << localLocationSrc << localLocationDst; return job; } VcsJob* BazaarPlugin::pull(const VcsLocation& localOrRepoLocationSrc, const QUrl& localRepositoryLocation) { // API describes hg pull which is git fetch equivalent // bzr has pull, but it succeeds only if fast-forward is possible // in other cases bzr merge should be used instead (bzr pull would fail) // Information about repository must be provided at least once. DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localRepositoryLocation), this); job->setType(VcsJob::JobType::Pull); *job << "bzr" << "pull"; if (!localOrRepoLocationSrc.localUrl().isEmpty()) { *job << localOrRepoLocationSrc.localUrl(); } // localUrl always makes sense. Even on remote repositores which are handled // transparently. return job; } VcsJob* BazaarPlugin::push(const QUrl& localRepositoryLocation, const VcsLocation& localOrRepoLocationDst) { DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localRepositoryLocation), this); job->setType(VcsJob::JobType::Push); *job << "bzr" << "push" << localOrRepoLocationDst.localUrl(); // localUrl always makes sense. Even on remote repositores which are handled // transparently. return job; } VcsJob* BazaarPlugin::remove(const QList& localLocations) { DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localLocations[0]), this); job->setType(VcsJob::JobType::Remove); *job << "bzr" << "remove" << localLocations; return job; } VcsJob* BazaarPlugin::repositoryLocation(const QUrl& localLocation) { DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localLocation), this); job->setType(VcsJob::JobType::Unknown); *job << "bzr" << "root" << localLocation; // It is only to make sure connect(job, &DVcsJob::readyForParsing, this, &BazaarPlugin::parseBzrRoot); return job; } void BazaarPlugin::parseBzrRoot(DVcsJob* job) { QString filename = job->dvcsCommand().at(2); QString rootDirectory = job->output(); QString localFilename = QFileInfo(QUrl::fromLocalFile(filename).toLocalFile()).absoluteFilePath(); QString result = localFilename.mid(localFilename.indexOf(rootDirectory) + rootDirectory.length()); job->setResults(QVariant::fromValue(result)); } VcsJob* BazaarPlugin::resolve(const QList& localLocations, IBasicVersionControl::RecursionMode recursion) { return add(localLocations, recursion); // How to provide "a conflict solving dialog to the user"? // In any case this plugin is unable to make any conflict. } VcsJob* BazaarPlugin::revert(const QList& localLocations, IBasicVersionControl::RecursionMode recursion) { DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localLocations[0]), this); job->setType(VcsJob::JobType::Revert); *job << "bzr" << "revert" << BazaarUtils::handleRecursion(localLocations, recursion); return job; } VcsJob* BazaarPlugin::status(const QList& localLocations, IBasicVersionControl::RecursionMode recursion) { Q_UNUSED(recursion); DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localLocations[0]), this); job->setType(VcsJob::Status); *job << "bzr" << "status" << "--short" << "--no-pending" << "--no-classify" << localLocations; connect(job, &DVcsJob::readyForParsing, this, &BazaarPlugin::parseBzrStatus); return job; } void BazaarPlugin::parseBzrStatus(DVcsJob* job) { QVariantList result; QSet filesWithStatus; QDir workingCopy = job->directory(); foreach (const QString& line, job->output().split('\n')) { auto status = BazaarUtils::parseVcsStatusInfoLine(line); result.append(QVariant::fromValue(status)); filesWithStatus.insert(BazaarUtils::concatenatePath(workingCopy, status.url())); } QStringList command = job->dvcsCommand(); for (auto it = command.constBegin() + command.indexOf(QStringLiteral("--no-classify")) + 1, itEnd = command.constEnd(); it != itEnd; ++it) { QString path = QFileInfo(*it).absoluteFilePath(); if (!filesWithStatus.contains(path)) { filesWithStatus.insert(path); KDevelop::VcsStatusInfo status; status.setState(VcsStatusInfo::ItemUpToDate); status.setUrl(QUrl::fromLocalFile(*it)); result.append(QVariant::fromValue(status)); } } job->setResults(result); } VcsJob* BazaarPlugin::update(const QList& localLocations, const VcsRevision& rev, IBasicVersionControl::RecursionMode recursion) { // bzr update is stronger than API (it's effectively merge) // the best approximation is bzr pull DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localLocations[0]), this); Q_UNUSED(recursion); // recursion and file locations are ignored - we can update only whole // working copy job->setType(VcsJob::JobType::Update); *job << "bzr" << "pull" << BazaarUtils::getRevisionSpec(rev); return job; } VcsLocationWidget* BazaarPlugin::vcsLocation(QWidget* parent) const { return new KDevelop::StandardVcsLocationWidget(parent); } ContextMenuExtension BazaarPlugin::contextMenuExtension(Context* context) { m_vcsPluginHelper->setupFromContext(context); QList const& ctxUrlList = m_vcsPluginHelper->contextUrlList(); bool isWorkingDirectory = false; for (const QUrl & url : ctxUrlList) { if (BazaarUtils::isValidDirectory(url)) { isWorkingDirectory = true; break; } } if (!isWorkingDirectory) { // Not part of a repository return ContextMenuExtension(); } QMenu* menu = m_vcsPluginHelper->commonActions(); ContextMenuExtension menuExt; menuExt.addAction(ContextMenuExtension::VcsGroup, menu->menuAction()); return menuExt; } diff --git a/plugins/bazaar/bazaarplugin.h b/plugins/bazaar/bazaarplugin.h index e538f2f48d..c5fee4615e 100644 --- a/plugins/bazaar/bazaarplugin.h +++ b/plugins/bazaar/bazaarplugin.h @@ -1,77 +1,79 @@ /*************************************************************************** * Copyright 2013-2014 Maciej Poleski * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #ifndef BAZAAR_BAZAARPLUGIN_H #define BAZAAR_BAZAARPLUGIN_H #include #include #include #include namespace KDevelop { class DVcsJob; } class BazaarPlugin : public KDevelop::IPlugin, public KDevelop::IDistributedVersionControl { Q_OBJECT Q_INTERFACES(KDevelop::IBasicVersionControl KDevelop::IDistributedVersionControl) public: explicit BazaarPlugin(QObject* parent, const QVariantList& args = QVariantList()); ~BazaarPlugin() override; QString name() const override; + bool isValidRemoteRepositoryUrl(const QUrl& remoteLocation) override; + KDevelop::VcsJob* add(const QList& localLocations, RecursionMode recursion=Recursive) override; KDevelop::VcsJob* annotate(const QUrl& localLocation, const KDevelop::VcsRevision& rev) override; KDevelop::VcsJob* commit(const QString& message, const QList& localLocations, RecursionMode recursion=Recursive) override; KDevelop::VcsJob* copy(const QUrl& localLocationSrc, const QUrl& localLocationDstn) override; KDevelop::VcsImportMetadataWidget* createImportMetadataWidget(QWidget* parent) override; KDevelop::VcsJob* createWorkingCopy(const KDevelop::VcsLocation& sourceRepository, const QUrl& destinationDirectory, RecursionMode recursion=Recursive) override; KDevelop::VcsJob* diff(const QUrl& fileOrDirectory, const KDevelop::VcsRevision& srcRevision, const KDevelop::VcsRevision& dstRevision, KDevelop::VcsDiff::Type, RecursionMode recursion=Recursive) override; KDevelop::VcsJob* init(const QUrl& localRepositoryRoot) override; bool isVersionControlled(const QUrl& localLocation) override; KDevelop::VcsJob* log(const QUrl& localLocation, const KDevelop::VcsRevision& rev, long unsigned int limit) override; KDevelop::VcsJob* log(const QUrl& localLocation, const KDevelop::VcsRevision& rev, const KDevelop::VcsRevision& limit) override; KDevelop::VcsJob* move(const QUrl& localLocationSrc, const QUrl& localLocationDst) override; KDevelop::VcsJob* pull(const KDevelop::VcsLocation& localOrRepoLocationSrc, const QUrl& localRepositoryLocation) override; KDevelop::VcsJob* push(const QUrl& localRepositoryLocation, const KDevelop::VcsLocation& localOrRepoLocationDst) override; KDevelop::VcsJob* remove(const QList& localLocations) override; KDevelop::VcsJob* repositoryLocation(const QUrl& localLocation) override; KDevelop::VcsJob* resolve(const QList& localLocations, RecursionMode recursio=Recursive) override; KDevelop::VcsJob* revert(const QList& localLocations, RecursionMode recursion=Recursive) override; KDevelop::VcsJob* status(const QList& localLocations, RecursionMode recursion=Recursive) override; KDevelop::VcsJob* update(const QList& localLocations, const KDevelop::VcsRevision& rev, RecursionMode recursion=Recursive) override; KDevelop::VcsLocationWidget* vcsLocation(QWidget* parent) const override; KDevelop::ContextMenuExtension contextMenuExtension(KDevelop::Context* context) override; private slots: void parseBzrStatus(KDevelop::DVcsJob* job); void parseBzrLog(KDevelop::DVcsJob* job); void parseBzrRoot(KDevelop::DVcsJob* job); private: KDevelop::VcsPluginHelper* m_vcsPluginHelper; }; #endif // BAZAAR_BAZAARPLUGIN_H diff --git a/plugins/cvs/cvsplugin.cpp b/plugins/cvs/cvsplugin.cpp index 0f99da4ac0..b37980b204 100644 --- a/plugins/cvs/cvsplugin.cpp +++ b/plugins/cvs/cvsplugin.cpp @@ -1,486 +1,493 @@ /*************************************************************************** * Copyright 2007 Robert Gruber * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "cvsplugin.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "cvsmainview.h" #include "cvsproxy.h" #include "cvsjob.h" #include "editorsview.h" #include "commitdialog.h" #include "cvsgenericoutputview.h" #include "checkoutdialog.h" #include "importdialog.h" #include "importmetadatawidget.h" #include "debug.h" #include #include #include Q_LOGGING_CATEGORY(PLUGIN_CVS, "kdevplatform.plugins.cvs") K_PLUGIN_FACTORY(KDevCvsFactory, registerPlugin();) // K_EXPORT_PLUGIN(KDevCvsFactory(KAboutData("kdevcvs", "kdevcvs", ki18n("CVS"), "0.1", ki18n("Support for CVS version control system"), KAboutData::License_GPL))) class KDevCvsViewFactory: public KDevelop::IToolViewFactory { public: explicit KDevCvsViewFactory(CvsPlugin *plugin): m_plugin(plugin) {} QWidget* create(QWidget *parent = nullptr) override { return new CvsMainView(m_plugin, parent); } Qt::DockWidgetArea defaultPosition() override { return Qt::BottomDockWidgetArea; } QString id() const override { return QStringLiteral("org.kdevelop.CVSView"); } private: CvsPlugin *m_plugin; }; class CvsPluginPrivate { public: explicit CvsPluginPrivate(CvsPlugin *pThis) : m_factory(new KDevCvsViewFactory(pThis)) , m_proxy(new CvsProxy(pThis)) , m_common(new KDevelop::VcsPluginHelper(pThis, pThis)) {} KDevCvsViewFactory* m_factory; QPointer m_proxy; QScopedPointer m_common; }; CvsPlugin::CvsPlugin(QObject *parent, const QVariantList &) : KDevelop::IPlugin(QStringLiteral("kdevcvs"), parent) , d(new CvsPluginPrivate(this)) { core()->uiController()->addToolView(i18n("CVS"), d->m_factory); setXMLFile(QStringLiteral("kdevcvs.rc")); setupActions(); } CvsPlugin::~CvsPlugin() { } void CvsPlugin::unload() { core()->uiController()->removeToolView( d->m_factory ); } CvsProxy* CvsPlugin::proxy() { return d->m_proxy; } void CvsPlugin::setupActions() { QAction *action; action = actionCollection()->addAction(QStringLiteral("cvs_import")); action->setText(i18n("Import Directory...")); connect(action, &QAction::triggered, this, &CvsPlugin::slotImport); action = actionCollection()->addAction(QStringLiteral("cvs_checkout")); action->setText(i18n("Checkout...")); connect(action, &QAction::triggered, this, &CvsPlugin::slotCheckout); action = actionCollection()->addAction(QStringLiteral("cvs_status")); action->setText(i18n("Status...")); connect(action, &QAction::triggered, this, &CvsPlugin::slotStatus); } const QUrl CvsPlugin::urlFocusedDocument() const { KParts::ReadOnlyPart *plugin = dynamic_cast(core()->partController()->activePart()); if (plugin) { if (plugin->url().isLocalFile()) { return plugin->url(); } } return QUrl(); } void CvsPlugin::slotImport() { QUrl url = urlFocusedDocument(); ImportDialog dlg(this, url); dlg.exec(); } void CvsPlugin::slotCheckout() { ///@todo don't use proxy directly; use interface instead CheckoutDialog dlg(this); dlg.exec(); } void CvsPlugin::slotStatus() { QUrl url = urlFocusedDocument(); QList urls; urls << url; KDevelop::VcsJob* j = status(urls, KDevelop::IBasicVersionControl::Recursive); CvsJob* job = dynamic_cast(j); if (job) { CvsGenericOutputView* view = new CvsGenericOutputView(job); emit addNewTabToMainView(view, i18n("Status")); KDevelop::ICore::self()->runController()->registerJob(job); } } KDevelop::ContextMenuExtension CvsPlugin::contextMenuExtension(KDevelop::Context* context) { d->m_common->setupFromContext(context); QList const & ctxUrlList = d->m_common->contextUrlList(); bool hasVersionControlledEntries = false; foreach(const QUrl &url, ctxUrlList) { if (d->m_proxy->isValidDirectory(url)) { hasVersionControlledEntries = true; break; } } qCDebug(PLUGIN_CVS) << "version controlled?" << hasVersionControlledEntries; if (!hasVersionControlledEntries) return IPlugin::contextMenuExtension(context); QMenu* menu = d->m_common->commonActions(); menu->addSeparator(); QAction *action; // Just add actions which are not covered by the cvscommon plugin action = new QAction(i18n("Edit"), this); connect(action, &QAction::triggered, this, &CvsPlugin::ctxEdit); menu->addAction(action); action = new QAction(i18n("Unedit"), this); connect(action, &QAction::triggered, this, &CvsPlugin::ctxUnEdit); menu->addAction(action); action = new QAction(i18n("Show Editors"), this); connect(action, &QAction::triggered, this, &CvsPlugin::ctxEditors); menu->addAction(action); KDevelop::ContextMenuExtension menuExt; menuExt.addAction(KDevelop::ContextMenuExtension::VcsGroup, menu->menuAction()); return menuExt; } void CvsPlugin::ctxEdit() { QList const & urls = d->m_common->contextUrlList(); Q_ASSERT(!urls.empty()); KDevelop::VcsJob* j = edit(urls.front()); CvsJob* job = dynamic_cast(j); if (job) { connect(job, &CvsJob::result, this, &CvsPlugin::jobFinished); KDevelop::ICore::self()->runController()->registerJob(job); } } void CvsPlugin::ctxUnEdit() { QList const & urls = d->m_common->contextUrlList(); Q_ASSERT(!urls.empty()); KDevelop::VcsJob* j = unedit(urls.front()); CvsJob* job = dynamic_cast(j); if (job) { connect(job, &CvsJob::result, this, &CvsPlugin::jobFinished); KDevelop::ICore::self()->runController()->registerJob(job); } } void CvsPlugin::ctxEditors() { QList const & urls = d->m_common->contextUrlList(); Q_ASSERT(!urls.empty()); CvsJob* job = d->m_proxy->editors(findWorkingDir(urls.front()), urls); if (job) { KDevelop::ICore::self()->runController()->registerJob(job); EditorsView* view = new EditorsView(job); emit addNewTabToMainView(view, i18n("Editors")); } } QString CvsPlugin::findWorkingDir(const QUrl& location) { QFileInfo fileInfo(location.toLocalFile()); // find out correct working directory if (fileInfo.isFile()) { return fileInfo.absolutePath(); } else { return fileInfo.absoluteFilePath(); } } // Begin: KDevelop::IBasicVersionControl +bool CvsPlugin::isValidRemoteRepositoryUrl(const QUrl& remoteLocation) +{ + Q_UNUSED(remoteLocation); + // TODO + return false; +} + bool CvsPlugin::isVersionControlled(const QUrl & localLocation) { return d->m_proxy->isVersionControlled(localLocation); } KDevelop::VcsJob * CvsPlugin::repositoryLocation(const QUrl & localLocation) { Q_UNUSED(localLocation); return nullptr; } KDevelop::VcsJob * CvsPlugin::add(const QList & localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) { CvsJob* job = d->m_proxy->add(findWorkingDir(localLocations[0]), localLocations, (recursion == KDevelop::IBasicVersionControl::Recursive) ? true : false); return job; } KDevelop::VcsJob * CvsPlugin::remove(const QList & localLocations) { CvsJob* job = d->m_proxy->remove(findWorkingDir(localLocations[0]), localLocations); return job; } KDevelop::VcsJob * CvsPlugin::localRevision(const QUrl & localLocation, KDevelop::VcsRevision::RevisionType) { Q_UNUSED(localLocation) return nullptr; } KDevelop::VcsJob * CvsPlugin::status(const QList & localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) { CvsJob* job = d->m_proxy->status(findWorkingDir(localLocations[0]), localLocations, (recursion == KDevelop::IBasicVersionControl::Recursive) ? true : false); return job; } KDevelop::VcsJob * CvsPlugin::unedit(const QUrl& localLocation) { CvsJob* job = d->m_proxy->unedit(findWorkingDir(localLocation), QList() << localLocation); return job; } KDevelop::VcsJob * CvsPlugin::edit(const QUrl& localLocation) { CvsJob* job = d->m_proxy->edit(findWorkingDir(localLocation), QList() << localLocation); return job; } KDevelop::VcsJob * CvsPlugin::copy(const QUrl & localLocationSrc, const QUrl & localLocationDstn) { bool ok = QFile::copy(localLocationSrc.toLocalFile(), localLocationDstn.path()); if (!ok) { return nullptr; } QList listDstn; listDstn << localLocationDstn; CvsJob* job = d->m_proxy->add(findWorkingDir(localLocationDstn), listDstn, true); return job; } KDevelop::VcsJob * CvsPlugin::move(const QUrl &, const QUrl &) { return nullptr; } KDevelop::VcsJob * CvsPlugin::revert(const QList & localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) { KDevelop::VcsRevision rev; CvsJob* job = d->m_proxy->update(findWorkingDir(localLocations[0]), localLocations, rev, QStringLiteral("-C"), (recursion == KDevelop::IBasicVersionControl::Recursive) ? true : false, false, false); return job; } KDevelop::VcsJob * CvsPlugin::update(const QList & localLocations, const KDevelop::VcsRevision & rev, KDevelop::IBasicVersionControl::RecursionMode recursion) { CvsJob* job = d->m_proxy->update(findWorkingDir(localLocations[0]), localLocations, rev, QString(), (recursion == KDevelop::IBasicVersionControl::Recursive) ? true : false, false, false); return job; } KDevelop::VcsJob * CvsPlugin::commit(const QString & message, const QList & localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) { Q_UNUSED(recursion); QString msg = message; if (msg.isEmpty()) { CommitDialog dlg; if (dlg.exec() == QDialog::Accepted) { msg = dlg.message(); } } CvsJob* job = d->m_proxy->commit(findWorkingDir(localLocations[0]), localLocations, msg); return job; } KDevelop::VcsJob * CvsPlugin::diff(const QUrl & fileOrDirectory, const KDevelop::VcsRevision & srcRevision, const KDevelop::VcsRevision & dstRevision, KDevelop::VcsDiff::Type, KDevelop::IBasicVersionControl::RecursionMode) { CvsJob* job = d->m_proxy->diff(fileOrDirectory, srcRevision, dstRevision, QStringLiteral("-uN")/*always unified*/); return job; } KDevelop::VcsJob * CvsPlugin::log(const QUrl & localLocation, const KDevelop::VcsRevision & rev, unsigned long limit) { Q_UNUSED(limit) CvsJob* job = d->m_proxy->log(localLocation, rev); return job; } KDevelop::VcsJob * CvsPlugin::log(const QUrl & localLocation, const KDevelop::VcsRevision & rev, const KDevelop::VcsRevision & limit) { Q_UNUSED(limit) return log(localLocation, rev, 0); } KDevelop::VcsJob * CvsPlugin::annotate(const QUrl & localLocation, const KDevelop::VcsRevision & rev) { CvsJob* job = d->m_proxy->annotate(localLocation, rev); return job; } KDevelop::VcsJob * CvsPlugin::resolve(const QList & localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) { Q_UNUSED(localLocations); Q_UNUSED(recursion); return nullptr; } KDevelop::VcsJob * CvsPlugin::import(const QString& commitMessage, const QUrl& sourceDirectory, const KDevelop::VcsLocation& destinationRepository) { if (commitMessage.isEmpty() || !sourceDirectory.isLocalFile() || !destinationRepository.isValid() || destinationRepository.type() != KDevelop::VcsLocation::RepositoryLocation) { return nullptr; } qCDebug(PLUGIN_CVS) << "CVS Import requested " << "src:" << sourceDirectory.toLocalFile() << "srv:" << destinationRepository.repositoryServer() << "module:" << destinationRepository.repositoryModule(); CvsJob* job = d->m_proxy->import(sourceDirectory, destinationRepository.repositoryServer(), destinationRepository.repositoryModule(), destinationRepository.userData().toString(), destinationRepository.repositoryTag(), commitMessage); return job; } KDevelop::VcsJob * CvsPlugin::createWorkingCopy(const KDevelop::VcsLocation & sourceRepository, const QUrl & destinationDirectory, KDevelop::IBasicVersionControl::RecursionMode recursion) { Q_UNUSED(recursion); if (!destinationDirectory.isLocalFile() || !sourceRepository.isValid() || sourceRepository.type() != KDevelop::VcsLocation::RepositoryLocation) { return nullptr; } qCDebug(PLUGIN_CVS) << "CVS Checkout requested " << "dest:" << destinationDirectory.toLocalFile() << "srv:" << sourceRepository.repositoryServer() << "module:" << sourceRepository.repositoryModule() << "branch:" << sourceRepository.repositoryBranch() << endl; CvsJob* job = d->m_proxy->checkout(destinationDirectory, sourceRepository.repositoryServer(), sourceRepository.repositoryModule(), QString(), sourceRepository.repositoryBranch(), true, true); return job; } QString CvsPlugin::name() const { return i18n("CVS"); } KDevelop::VcsImportMetadataWidget* CvsPlugin::createImportMetadataWidget(QWidget* parent) { return new ImportMetadataWidget(parent); } KDevelop::VcsLocationWidget* CvsPlugin::vcsLocation(QWidget* parent) const { return new KDevelop::StandardVcsLocationWidget(parent); } // End: KDevelop::IBasicVersionControl #include "cvsplugin.moc" diff --git a/plugins/cvs/cvsplugin.h b/plugins/cvs/cvsplugin.h index fc97ac5287..fa5efe6f9f 100644 --- a/plugins/cvs/cvsplugin.h +++ b/plugins/cvs/cvsplugin.h @@ -1,139 +1,140 @@ /*************************************************************************** * Copyright 2007 Robert Gruber * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef KDEVPLATFORM_PLUGIN_CVSPLUGIN_H #define KDEVPLATFORM_PLUGIN_CVSPLUGIN_H #include #include #include #include #include #include class CvsProxy; namespace KDevelop { class ContextMenuExtension; } class CvsPluginPrivate; /** * This is the main class of KDevelop's CVS plugin. * * It implements the IVersionControl interface. * * @author Robert Gruber */ class CvsPlugin : public KDevelop::IPlugin, public KDevelop::ICentralizedVersionControl { Q_OBJECT Q_INTERFACES(KDevelop::IBasicVersionControl KDevelop::ICentralizedVersionControl) friend class CvsProxy; public: explicit CvsPlugin(QObject *parent, const QVariantList & args = QVariantList()); ~CvsPlugin() override; void unload() override; QString name() const override; KDevelop::VcsImportMetadataWidget* createImportMetadataWidget(QWidget* parent) override; // From KDevelop::IPlugin KDevelop::ContextMenuExtension contextMenuExtension(KDevelop::Context*) override; // Begin: KDevelop::IBasicVersionControl + bool isValidRemoteRepositoryUrl(const QUrl& remoteLocation) override; bool isVersionControlled(const QUrl& localLocation) override; KDevelop::VcsJob* repositoryLocation(const QUrl& localLocation) override; KDevelop::VcsJob* add(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) override; KDevelop::VcsJob* remove(const QList& localLocations) override; KDevelop::VcsJob* copy(const QUrl& localLocationSrc, const QUrl& localLocationDstn) override; KDevelop::VcsJob* move(const QUrl& localLocationSrc, const QUrl& localLocationDst) override; KDevelop::VcsJob* status(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) override; KDevelop::VcsJob* revert(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) override; KDevelop::VcsJob* update(const QList& localLocations, const KDevelop::VcsRevision& rev, KDevelop::IBasicVersionControl::RecursionMode recursion) override; KDevelop::VcsJob* commit(const QString& message, const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) override; KDevelop::VcsJob* diff(const QUrl& fileOrDirectory, const KDevelop::VcsRevision& srcRevision, const KDevelop::VcsRevision& dstRevision, KDevelop::VcsDiff::Type, KDevelop::IBasicVersionControl::RecursionMode = KDevelop::IBasicVersionControl::Recursive) override; KDevelop::VcsJob* log(const QUrl& localLocation, const KDevelop::VcsRevision& rev, unsigned long limit) override; KDevelop::VcsJob* log(const QUrl& localLocation, const KDevelop::VcsRevision& rev, const KDevelop::VcsRevision& limit) override; KDevelop::VcsJob* annotate(const QUrl& localLocation, const KDevelop::VcsRevision& rev) override; KDevelop::VcsJob* resolve(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) override; KDevelop::VcsJob* createWorkingCopy(const KDevelop::VcsLocation & sourceRepository, const QUrl & destinationDirectory, KDevelop::IBasicVersionControl::RecursionMode recursion = KDevelop::IBasicVersionControl::Recursive) override; // End: KDevelop::IBasicVersionControl // Begin: KDevelop::ICentralizedVersionControl KDevelop::VcsJob* edit(const QUrl& localLocation) override; KDevelop::VcsJob* unedit(const QUrl& localLocation) override; KDevelop::VcsJob* localRevision(const QUrl& localLocation, KDevelop::VcsRevision::RevisionType) override; KDevelop::VcsJob* import(const QString& commitMessage, const QUrl& sourceDirectory, const KDevelop::VcsLocation& destinationRepository) override; // End: KDevelop::ICentralizedVersionControl CvsProxy* proxy(); const QUrl urlFocusedDocument() const; KDevelop::VcsLocationWidget* vcsLocation(QWidget* parent) const override; public slots: // slots for context menu void ctxEdit(); void ctxUnEdit(); void ctxEditors(); // slots for menu void slotImport(); void slotCheckout(); void slotStatus(); signals: /** * Some actions like commit, add, remove... will connect the job's * result() signal to this signal. Anybody, like for instance the * CvsMainView class, that is interested in getting notified about * jobs that finished can connect to this signal. * @see class CvsMainView */ void jobFinished(KJob* job); /** * Gets emmited when a job like log, editors... was created. * CvsPlugin will connect the newly created view to the result() signal * of a job. So the new view will show the output of that job as * soon as it has finished. */ void addNewTabToMainView(QWidget* tab, QString label); private: void setupActions(); QScopedPointer d; QString findWorkingDir(const QUrl& location); }; #endif diff --git a/plugins/git/gitplugin.cpp b/plugins/git/gitplugin.cpp index c5c7f8f5a5..b56d53df38 100644 --- a/plugins/git/gitplugin.cpp +++ b/plugins/git/gitplugin.cpp @@ -1,1510 +1,1536 @@ /*************************************************************************** * Copyright 2008 Evgeniy Ivanov * * Copyright 2009 Hugo Parente Lima * * Copyright 2010 Aleix Pol Gonzalez * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "gitplugin.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "gitclonejob.h" #include #include #include "stashmanagerdialog.h" #include #include #include #include #include #include #include "gitjob.h" #include "gitmessagehighlighter.h" #include "gitplugincheckinrepositoryjob.h" #include "gitnameemaildialog.h" #include "debug.h" Q_LOGGING_CATEGORY(PLUGIN_GIT, "kdevplatform.plugins.git") using namespace KDevelop; QVariant runSynchronously(KDevelop::VcsJob* job) { QVariant ret; if(job->exec() && job->status()==KDevelop::VcsJob::JobSucceeded) { ret = job->fetchResults(); } delete job; return ret; } namespace { QDir dotGitDirectory(const QUrl& dirPath) { const QFileInfo finfo(dirPath.toLocalFile()); QDir dir = finfo.isDir() ? QDir(finfo.filePath()): finfo.absoluteDir(); static const QString gitDir = QStringLiteral(".git"); while (!dir.exists(gitDir) && dir.cdUp()) {} // cdUp, until there is a sub-directory called .git if (dir.isRoot()) { qCWarning(PLUGIN_GIT) << "couldn't find the git root for" << dirPath; } return dir; } /** * Whenever a directory is provided, change it for all the files in it but not inner directories, * that way we make sure we won't get into recursion, */ static QList preventRecursion(const QList& urls) { QList ret; foreach(const QUrl& url, urls) { QDir d(url.toLocalFile()); if(d.exists()) { QStringList entries = d.entryList(QDir::Files | QDir::NoDotAndDotDot); foreach(const QString& entry, entries) { QUrl entryUrl = QUrl::fromLocalFile(d.absoluteFilePath(entry)); ret += entryUrl; } } else ret += url; } return ret; } QString toRevisionName(const KDevelop::VcsRevision& rev, QString currentRevision=QString()) { switch(rev.revisionType()) { case VcsRevision::Special: switch(rev.revisionValue().value()) { case VcsRevision::Head: return QStringLiteral("^HEAD"); case VcsRevision::Base: return QString(); case VcsRevision::Working: return QString(); case VcsRevision::Previous: Q_ASSERT(!currentRevision.isEmpty()); return currentRevision + "^1"; case VcsRevision::Start: return QString(); case VcsRevision::UserSpecialType: //Not used Q_ASSERT(false && "i don't know how to do that"); } break; case VcsRevision::GlobalNumber: return rev.revisionValue().toString(); case VcsRevision::Date: case VcsRevision::FileNumber: case VcsRevision::Invalid: case VcsRevision::UserSpecialType: Q_ASSERT(false); } return QString(); } QString revisionInterval(const KDevelop::VcsRevision& rev, const KDevelop::VcsRevision& limit) { QString ret; if(rev.revisionType()==VcsRevision::Special && rev.revisionValue().value()==VcsRevision::Start) //if we want it to the beginning just put the revisionInterval ret = toRevisionName(limit, QString()); else { QString dst = toRevisionName(limit); if(dst.isEmpty()) ret = dst; else { QString src = toRevisionName(rev, dst); if(src.isEmpty()) ret = src; else ret = src+".."+dst; } } return ret; } QDir urlDir(const QUrl& url) { QFileInfo f(url.toLocalFile()); if(f.isDir()) return QDir(url.toLocalFile()); else return f.absoluteDir(); } QDir urlDir(const QList& urls) { return urlDir(urls.first()); } //TODO: could be improved } GitPlugin::GitPlugin( QObject *parent, const QVariantList & ) : DistributedVersionControlPlugin(parent, QStringLiteral("kdevgit")), m_oldVersion(false), m_usePrefix(true) { if (QStandardPaths::findExecutable(QStringLiteral("git")).isEmpty()) { setErrorDescription(i18n("Unable to find git executable. Is it installed on the system?")); return; } setObjectName(QStringLiteral("Git")); DVcsJob* versionJob = new DVcsJob(QDir::tempPath(), this, KDevelop::OutputJob::Silent); *versionJob << "git" << "--version"; connect(versionJob, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitVersionOutput); ICore::self()->runController()->registerJob(versionJob); m_watcher = new KDirWatch(this); connect(m_watcher, &KDirWatch::dirty, this, &GitPlugin::fileChanged); connect(m_watcher, &KDirWatch::created, this, &GitPlugin::fileChanged); } GitPlugin::~GitPlugin() {} bool emptyOutput(DVcsJob* job) { QScopedPointer _job(job); if(job->exec() && job->status()==VcsJob::JobSucceeded) return job->rawOutput().trimmed().isEmpty(); return false; } bool GitPlugin::hasStashes(const QDir& repository) { return !emptyOutput(gitStash(repository, QStringList(QStringLiteral("list")), KDevelop::OutputJob::Silent)); } bool GitPlugin::hasModifications(const QDir& d) { return !emptyOutput(lsFiles(d, QStringList(QStringLiteral("-m")), OutputJob::Silent)); } bool GitPlugin::hasModifications(const QDir& repo, const QUrl& file) { return !emptyOutput(lsFiles(repo, QStringList() << QStringLiteral("-m") << file.path(), OutputJob::Silent)); } void GitPlugin::additionalMenuEntries(QMenu* menu, const QList& urls) { m_urls = urls; QDir dir=urlDir(urls); bool hasSt = hasStashes(dir); menu->addSeparator()->setText(i18n("Git Stashes")); menu->addAction(i18n("Stash Manager"), this, SLOT(ctxStashManager()))->setEnabled(hasSt); menu->addAction(i18n("Push Stash"), this, SLOT(ctxPushStash())); menu->addAction(i18n("Pop Stash"), this, SLOT(ctxPopStash()))->setEnabled(hasSt); } void GitPlugin::ctxPushStash() { VcsJob* job = gitStash(urlDir(m_urls), QStringList(), KDevelop::OutputJob::Verbose); ICore::self()->runController()->registerJob(job); } void GitPlugin::ctxPopStash() { VcsJob* job = gitStash(urlDir(m_urls), QStringList(QStringLiteral("pop")), KDevelop::OutputJob::Verbose); ICore::self()->runController()->registerJob(job); } void GitPlugin::ctxStashManager() { QPointer d = new StashManagerDialog(urlDir(m_urls), this, nullptr); d->exec(); delete d; } DVcsJob* GitPlugin::errorsFound(const QString& error, KDevelop::OutputJob::OutputJobVerbosity verbosity=OutputJob::Verbose) { DVcsJob* j = new DVcsJob(QDir::temp(), this, verbosity); *j << "echo" << i18n("error: %1", error) << "-n"; return j; } QString GitPlugin::name() const { return QStringLiteral("Git"); } QUrl GitPlugin::repositoryRoot(const QUrl& path) { return QUrl::fromLocalFile(dotGitDirectory(path).absolutePath()); } bool GitPlugin::isValidDirectory(const QUrl & dirPath) { QDir dir=dotGitDirectory(dirPath); return dir.exists(QStringLiteral(".git/HEAD")); } +bool GitPlugin::isValidRemoteRepositoryUrl(const QUrl& remoteLocation) +{ + if (remoteLocation.isLocalFile()) { + QFileInfo fileInfo(remoteLocation.toLocalFile()); + if (fileInfo.isDir()) { + QDir dir(fileInfo.filePath()); + if (dir.exists(QStringLiteral(".git/HEAD"))) { + return true; + } + // TODO: check also for bare repo + } + } else { + const QString scheme = remoteLocation.scheme(); + if (scheme == QLatin1String("git")) { + return true; + } + // heuristic check, anything better we can do here without talking to server? + if ((scheme == QLatin1String("http") || + scheme == QLatin1String("https")) && + remoteLocation.path().endsWith(QLatin1String(".git"))) { + return true; + } + } + return false; +} + bool GitPlugin::isVersionControlled(const QUrl &path) { QFileInfo fsObject(path.toLocalFile()); if (!fsObject.exists()) { return false; } if (fsObject.isDir()) { return isValidDirectory(path); } QString filename = fsObject.fileName(); QStringList otherFiles = getLsFiles(fsObject.dir(), QStringList(QStringLiteral("--")) << filename, KDevelop::OutputJob::Silent); return !otherFiles.empty(); } VcsJob* GitPlugin::init(const QUrl &directory) { DVcsJob* job = new DVcsJob(urlDir(directory), this); job->setType(VcsJob::Import); *job << "git" << "init"; return job; } VcsJob* GitPlugin::createWorkingCopy(const KDevelop::VcsLocation & source, const QUrl& dest, KDevelop::IBasicVersionControl::RecursionMode) { DVcsJob* job = new GitCloneJob(urlDir(dest), this); job->setType(VcsJob::Import); *job << "git" << "clone" << "--progress" << "--" << source.localUrl().url() << dest; return job; } VcsJob* GitPlugin::add(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) { if (localLocations.empty()) return errorsFound(i18n("Did not specify the list of files"), OutputJob::Verbose); DVcsJob* job = new GitJob(dotGitDirectory(localLocations.front()), this); job->setType(VcsJob::Add); *job << "git" << "add" << "--" << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations)); return job; } KDevelop::VcsJob* GitPlugin::status(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) { if (localLocations.empty()) return errorsFound(i18n("Did not specify the list of files"), OutputJob::Verbose); DVcsJob* job = new GitJob(urlDir(localLocations), this, OutputJob::Silent); job->setType(VcsJob::Status); if(m_oldVersion) { *job << "git" << "ls-files" << "-t" << "-m" << "-c" << "-o" << "-d" << "-k" << "--directory"; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitStatusOutput_old); } else { *job << "git" << "status" << "--porcelain"; job->setIgnoreError(true); connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitStatusOutput); } *job << "--" << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations)); return job; } VcsJob* GitPlugin::diff(const QUrl& fileOrDirectory, const KDevelop::VcsRevision& srcRevision, const KDevelop::VcsRevision& dstRevision, VcsDiff::Type /*type*/, IBasicVersionControl::RecursionMode recursion) { //TODO: control different types DVcsJob* job = new GitJob(dotGitDirectory(fileOrDirectory), this, KDevelop::OutputJob::Silent); job->setType(VcsJob::Diff); *job << "git" << "diff" << "--no-color" << "--no-ext-diff"; if (!usePrefix()) { // KDE's ReviewBoard now requires p1 patchfiles, so `git diff --no-prefix` to generate p0 patches // has become optional. *job << "--no-prefix"; } if (dstRevision.revisionType() == VcsRevision::Special && dstRevision.specialType() == VcsRevision::Working) { if (srcRevision.revisionType() == VcsRevision::Special && srcRevision.specialType() == VcsRevision::Base) { *job << "HEAD"; } else { *job << "--cached" << srcRevision.revisionValue().toString(); } } else { QString revstr = revisionInterval(srcRevision, dstRevision); if(!revstr.isEmpty()) *job << revstr; } *job << "--"; if (recursion == IBasicVersionControl::Recursive) { *job << fileOrDirectory; } else { *job << preventRecursion(QList() << fileOrDirectory); } connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitDiffOutput); return job; } VcsJob* GitPlugin::revert(const QList& localLocations, IBasicVersionControl::RecursionMode recursion) { if(localLocations.isEmpty() ) return errorsFound(i18n("Could not revert changes"), OutputJob::Verbose); QDir repo = urlDir(repositoryRoot(localLocations.first())); QString modified; for (const auto& file: localLocations) { if (hasModifications(repo, file)) { modified.append(file.toDisplayString(QUrl::PreferLocalFile) + "
"); } } if (!modified.isEmpty()) { auto res = KMessageBox::questionYesNo(nullptr, i18n("The following files have uncommited changes, " "which will be lost. Continue?") + "

" + modified); if (res != KMessageBox::Yes) { return errorsFound(QString(), OutputJob::Silent); } } DVcsJob* job = new GitJob(dotGitDirectory(localLocations.front()), this); job->setType(VcsJob::Revert); *job << "git" << "checkout" << "--"; *job << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations)); return job; } //TODO: git doesn't like empty messages, but "KDevelop didn't provide any message, it may be a bug" looks ugly... //If no files specified then commit already added files VcsJob* GitPlugin::commit(const QString& message, const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) { if (localLocations.empty() || message.isEmpty()) return errorsFound(i18n("No files or message specified")); const QDir dir = dotGitDirectory(localLocations.front()); if (!ensureValidGitIdentity(dir)) { return errorsFound(i18n("Email or name for Git not specified")); } DVcsJob* job = new DVcsJob(dir, this); job->setType(VcsJob::Commit); QList files = (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations)); addNotVersionedFiles(dir, files); *job << "git" << "commit" << "-m" << message; *job << "--" << files; return job; } bool GitPlugin::ensureValidGitIdentity(const QDir& dir) { const QUrl url = QUrl::fromLocalFile(dir.absolutePath()); const QString name = readConfigOption(url, QStringLiteral("user.name")); const QString email = readConfigOption(url, QStringLiteral("user.email")); if (!email.isEmpty() && !name.isEmpty()) { return true; // already okay } GitNameEmailDialog dialog; dialog.setName(name); dialog.setEmail(email); if (!dialog.exec()) { return false; } runSynchronously(setConfigOption(url, QStringLiteral("user.name"), dialog.name(), dialog.isGlobal())); runSynchronously(setConfigOption(url, QStringLiteral("user.email"), dialog.email(), dialog.isGlobal())); return true; } void GitPlugin::addNotVersionedFiles(const QDir& dir, const QList& files) { QStringList otherStr = getLsFiles(dir, QStringList() << QStringLiteral("--others"), KDevelop::OutputJob::Silent); QList toadd, otherFiles; foreach(const QString& file, otherStr) { QUrl v = QUrl::fromLocalFile(dir.absoluteFilePath(file)); otherFiles += v; } //We add the files that are not versioned foreach(const QUrl& file, files) { if(otherFiles.contains(file) && QFileInfo(file.toLocalFile()).isFile()) toadd += file; } if(!toadd.isEmpty()) { VcsJob* job = add(toadd); job->exec(); } } bool isEmptyDirStructure(const QDir &dir) { foreach (const QFileInfo &i, dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot)) { if (i.isDir()) { if (!isEmptyDirStructure(QDir(i.filePath()))) return false; } else if (i.isFile()) { return false; } } return true; } VcsJob* GitPlugin::remove(const QList& files) { if (files.isEmpty()) return errorsFound(i18n("No files to remove")); QDir dotGitDir = dotGitDirectory(files.front()); QList files_(files); QMutableListIterator i(files_); while (i.hasNext()) { QUrl file = i.next(); QFileInfo fileInfo(file.toLocalFile()); QStringList otherStr = getLsFiles(dotGitDir, QStringList() << QStringLiteral("--others") << QStringLiteral("--") << file.toLocalFile(), KDevelop::OutputJob::Silent); if(!otherStr.isEmpty()) { //remove files not under version control QList otherFiles; foreach(const QString &f, otherStr) { otherFiles << QUrl::fromLocalFile(dotGitDir.path()+'/'+f); } if (fileInfo.isFile()) { //if it's an unversioned file we are done, don't use git rm on it i.remove(); } auto trashJob = KIO::trash(otherFiles); trashJob->exec(); qCDebug(PLUGIN_GIT) << "other files" << otherFiles; } if (fileInfo.isDir()) { if (isEmptyDirStructure(QDir(file.toLocalFile()))) { //remove empty folders, git doesn't do that auto trashJob = KIO::trash(file); trashJob->exec(); qCDebug(PLUGIN_GIT) << "empty folder, removing" << file; //we already deleted it, don't use git rm on it i.remove(); } } } if (files_.isEmpty()) return nullptr; DVcsJob* job = new GitJob(dotGitDir, this); job->setType(VcsJob::Remove); // git refuses to delete files with local modifications // use --force to overcome this *job << "git" << "rm" << "-r" << "--force"; *job << "--" << files_; return job; } VcsJob* GitPlugin::log(const QUrl& localLocation, const KDevelop::VcsRevision& src, const KDevelop::VcsRevision& dst) { DVcsJob* job = new GitJob(dotGitDirectory(localLocation), this, KDevelop::OutputJob::Silent); job->setType(VcsJob::Log); *job << "git" << "log" << "--date=raw" << "--name-status" << "-M80%" << "--follow"; QString rev = revisionInterval(dst, src); if(!rev.isEmpty()) *job << rev; *job << "--" << localLocation; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitLogOutput); return job; } VcsJob* GitPlugin::log(const QUrl& localLocation, const KDevelop::VcsRevision& rev, unsigned long int limit) { DVcsJob* job = new GitJob(dotGitDirectory(localLocation), this, KDevelop::OutputJob::Silent); job->setType(VcsJob::Log); *job << "git" << "log" << "--date=raw" << "--name-status" << "-M80%" << "--follow"; QString revStr = toRevisionName(rev, QString()); if(!revStr.isEmpty()) *job << revStr; if(limit>0) *job << QStringLiteral("-%1").arg(limit); *job << "--" << localLocation; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitLogOutput); return job; } KDevelop::VcsJob* GitPlugin::annotate(const QUrl &localLocation, const KDevelop::VcsRevision&) { DVcsJob* job = new GitJob(dotGitDirectory(localLocation), this, KDevelop::OutputJob::Silent); job->setType(VcsJob::Annotate); *job << "git" << "blame" << "--porcelain" << "-w"; *job << "--" << localLocation; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitBlameOutput); return job; } void GitPlugin::parseGitBlameOutput(DVcsJob *job) { QVariantList results; VcsAnnotationLine* annotation = nullptr; const auto output = job->output(); const auto lines = output.splitRef('\n'); bool skipNext=false; QMap definedRevisions; for(QVector::const_iterator it=lines.constBegin(), itEnd=lines.constEnd(); it!=itEnd; ++it) { if(skipNext) { skipNext=false; results += qVariantFromValue(*annotation); continue; } if(it->isEmpty()) continue; QStringRef name = it->left(it->indexOf(' ')); QStringRef value = it->right(it->size()-name.size()-1); if(name==QLatin1String("author")) annotation->setAuthor(value.toString()); else if(name==QLatin1String("author-mail")) {} //TODO: do smth with the e-mail? else if(name==QLatin1String("author-tz")) {} //TODO: does it really matter? else if(name==QLatin1String("author-time")) annotation->setDate(QDateTime::fromTime_t(value.toUInt())); else if(name==QLatin1String("summary")) annotation->setCommitMessage(value.toString()); else if(name.startsWith(QStringLiteral("committer"))) {} //We will just store the authors else if(name==QLatin1String("previous")) {} //We don't need that either else if(name==QLatin1String("filename")) { skipNext=true; } else if(name==QLatin1String("boundary")) { definedRevisions.insert(QStringLiteral("boundary"), VcsAnnotationLine()); } else { const auto values = value.split(' '); VcsRevision rev; rev.setRevisionValue(name.left(8).toString(), KDevelop::VcsRevision::GlobalNumber); skipNext = definedRevisions.contains(name.toString()); if(!skipNext) definedRevisions.insert(name.toString(), VcsAnnotationLine()); annotation = &definedRevisions[name.toString()]; annotation->setLineNumber(values[1].toInt() - 1); annotation->setRevision(rev); } } job->setResults(results); } DVcsJob* GitPlugin::lsFiles(const QDir &repository, const QStringList &args, OutputJob::OutputJobVerbosity verbosity) { DVcsJob* job = new DVcsJob(repository, this, verbosity); *job << "git" << "ls-files" << args; return job; } DVcsJob* GitPlugin::gitStash(const QDir& repository, const QStringList& args, OutputJob::OutputJobVerbosity verbosity) { DVcsJob* job = new DVcsJob(repository, this, verbosity); *job << "git" << "stash" << args; return job; } VcsJob* GitPlugin::tag(const QUrl& repository, const QString& commitMessage, const VcsRevision& rev, const QString& tagName) { DVcsJob* job = new DVcsJob(urlDir(repository), this); *job << "git" << "tag" << "-m" << commitMessage << tagName; if(rev.revisionValue().isValid()) *job << rev.revisionValue().toString(); return job; } VcsJob* GitPlugin::switchBranch(const QUrl &repository, const QString &branch) { QDir d=urlDir(repository); if(hasModifications(d) && KMessageBox::questionYesNo(nullptr, i18n("There are pending changes, do you want to stash them first?"))==KMessageBox::Yes) { QScopedPointer stash(gitStash(d, QStringList(), KDevelop::OutputJob::Verbose)); stash->exec(); } DVcsJob* job = new DVcsJob(d, this); *job << "git" << "checkout" << branch; return job; } VcsJob* GitPlugin::branch(const QUrl& repository, const KDevelop::VcsRevision& rev, const QString& branchName) { Q_ASSERT(!branchName.isEmpty()); DVcsJob* job = new DVcsJob(urlDir(repository), this); *job << "git" << "branch" << "--" << branchName; if(!rev.prettyValue().isEmpty()) *job << rev.revisionValue().toString(); return job; } VcsJob* GitPlugin::deleteBranch(const QUrl& repository, const QString& branchName) { DVcsJob* job = new DVcsJob(urlDir(repository), this, OutputJob::Silent); *job << "git" << "branch" << "-D" << branchName; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitCurrentBranch); return job; } VcsJob* GitPlugin::renameBranch(const QUrl& repository, const QString& oldBranchName, const QString& newBranchName) { DVcsJob* job = new DVcsJob(urlDir(repository), this, OutputJob::Silent); *job << "git" << "branch" << "-m" << newBranchName << oldBranchName; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitCurrentBranch); return job; } VcsJob* GitPlugin::mergeBranch(const QUrl& repository, const QString& branchName) { Q_ASSERT(!branchName.isEmpty()); DVcsJob* job = new DVcsJob(urlDir(repository), this); *job << "git" << "merge" << branchName; return job; } VcsJob* GitPlugin::currentBranch(const QUrl& repository) { DVcsJob* job = new DVcsJob(urlDir(repository), this, OutputJob::Silent); job->setIgnoreError(true); *job << "git" << "symbolic-ref" << "-q" << "--short" << "HEAD"; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitCurrentBranch); return job; } void GitPlugin::parseGitCurrentBranch(DVcsJob* job) { QString out = job->output().trimmed(); job->setResults(out); } VcsJob* GitPlugin::branches(const QUrl &repository) { DVcsJob* job=new DVcsJob(urlDir(repository)); *job << "git" << "branch" << "-a"; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitBranchOutput); return job; } void GitPlugin::parseGitBranchOutput(DVcsJob* job) { const auto output = job->output(); const auto branchListDirty = output.splitRef('\n', QString::SkipEmptyParts); QStringList branchList; foreach(const auto & branch, branchListDirty) { // Skip pointers to another branches (one example of this is "origin/HEAD -> origin/master"); // "git rev-list" chokes on these entries and we do not need duplicate branches altogether. if (branch.contains(QStringLiteral("->"))) continue; // Skip entries such as '(no branch)' if (branch.contains(QStringLiteral("(no branch)"))) continue; QStringRef name = branch; if (name.startsWith('*')) name = branch.right(branch.size()-2); branchList << name.trimmed().toString(); } job->setResults(branchList); } /* Few words about how this hardcore works: 1. get all commits (with --paretns) 2. select master (root) branch and get all unicial commits for branches (git-rev-list br2 ^master ^br3) 3. parse allCommits. While parsing set mask (columns state for every row) for BRANCH, INITIAL, CROSS, MERGE and INITIAL are also set in DVCScommit::setParents (depending on parents count) another setType(INITIAL) is used for "bottom/root/first" commits of branches 4. find and set merges, HEADS. It's an ittaration through all commits. - first we check if parent is from the same branch, if no then we go through all commits searching parent's index and set CROSS/HCROSS for rows (in 3 rows are set EMPTY after commit with parent from another tree met) - then we check branchesShas[i][0] to mark heads 4 can be a seporate function. TODO: All this porn require refactoring (rewriting is better)! It's a very dirty implementation. FIXME: 1. HEAD which is head has extra line to connect it with further commit 2. If you menrge branch2 to master, only new commits of branch2 will be visible (it's fine, but there will be extra merge rectangle in master. If there are no extra commits in branch2, but there are another branches, then the place for branch2 will be empty (instead of be used for branch3). 3. Commits that have additional commit-data (not only history merging, but changes to fix conflicts) are shown incorrectly */ QList GitPlugin::getAllCommits(const QString &repo) { initBranchHash(repo); QStringList args; args << QStringLiteral("--all") << QStringLiteral("--pretty") << QStringLiteral("--parents"); QScopedPointer job(gitRevList(repo, args)); bool ret = job->exec(); Q_ASSERT(ret && job->status()==VcsJob::JobSucceeded && "TODO: provide a fall back in case of failing"); Q_UNUSED(ret); QStringList commits = job->output().split('\n', QString::SkipEmptyParts); static QRegExp rx_com("commit \\w{40,40}"); QListcommitList; DVcsEvent item; //used to keep where we have empty/cross/branch entry //true if it's an active branch (then cross or branch) and false if not QVector additionalFlags(branchesShas.count()); additionalFlags.fill(false); //parse output for(int i = 0; i < commits.count(); ++i) { if (commits[i].contains(rx_com)) { qCDebug(PLUGIN_GIT) << "commit found in " << commits[i]; item.setCommit(commits[i].section(' ', 1, 1).trimmed()); // qCDebug(PLUGIN_GIT) << "commit is: " << commits[i].section(' ', 1); QStringList parents; QString parent = commits[i].section(' ', 2); int section = 2; while (!parent.isEmpty()) { /* qCDebug(PLUGIN_GIT) << "Parent is: " << parent;*/ parents.append(parent.trimmed()); section++; parent = commits[i].section(' ', section); } item.setParents(parents); //Avoid Merge string while (!commits[i].contains(QStringLiteral("Author: "))) ++i; item.setAuthor(commits[i].section(QStringLiteral("Author: "), 1).trimmed()); // qCDebug(PLUGIN_GIT) << "author is: " << commits[i].section("Author: ", 1); item.setDate(commits[++i].section(QStringLiteral("Date: "), 1).trimmed()); // qCDebug(PLUGIN_GIT) << "date is: " << commits[i].section("Date: ", 1); QString log; i++; //next line! while (i < commits.count() && !commits[i].contains(rx_com)) log += commits[i++]; --i; //while took commit line item.setLog(log.trimmed()); // qCDebug(PLUGIN_GIT) << "log is: " << log; //mask is used in CommitViewDelegate to understand what we should draw for each branch QList mask; //set mask (properties for each graph column in row) for(int i = 0; i < branchesShas.count(); ++i) { qCDebug(PLUGIN_GIT)<<"commit: " << item.getCommit(); if (branchesShas[i].contains(item.getCommit())) { mask.append(item.getType()); //we set type in setParents //check if parent from the same branch, if not then we have found a root of the branch //and will use empty column for all futher (from top to bottom) revisions //FIXME: we should set CROSS between parent and child (and do it when find merge point) additionalFlags[i] = false; foreach(const QString &sha, item.getParents()) { if (branchesShas[i].contains(sha)) additionalFlags[i] = true; } if (additionalFlags[i] == false) item.setType(DVcsEvent::INITIAL); //hasn't parents from the same branch, used in drawing } else { if (additionalFlags[i] == false) mask.append(DVcsEvent::EMPTY); else mask.append(DVcsEvent::CROSS); } qCDebug(PLUGIN_GIT) << "mask " << i << "is " << mask[i]; } item.setProperties(mask); commitList.append(item); } } //find and set merges, HEADS, require refactoring! for(QList::iterator iter = commitList.begin(); iter != commitList.end(); ++iter) { QStringList parents = iter->getParents(); //we need only only child branches if (parents.count() != 1) break; QString parent = parents[0]; QString commit = iter->getCommit(); bool parent_checked = false; int heads_checked = 0; for(int i = 0; i < branchesShas.count(); ++i) { //check parent if (branchesShas[i].contains(commit)) { if (!branchesShas[i].contains(parent)) { //parent and child are not in same branch //since it is list, than parent has i+1 index //set CROSS and HCROSS for(QList::iterator f_iter = iter; f_iter != commitList.end(); ++f_iter) { if (parent == f_iter->getCommit()) { for(int j = 0; j < i; ++j) { if(branchesShas[j].contains(parent)) f_iter->setPropetry(j, DVcsEvent::MERGE); else f_iter->setPropetry(j, DVcsEvent::HCROSS); } f_iter->setType(DVcsEvent::MERGE); f_iter->setPropetry(i, DVcsEvent::MERGE_RIGHT); qCDebug(PLUGIN_GIT) << parent << " is parent of " << commit; qCDebug(PLUGIN_GIT) << f_iter->getCommit() << " is merge"; parent_checked = true; break; } else f_iter->setPropetry(i, DVcsEvent::CROSS); } } } //mark HEADs if (!branchesShas[i].empty() && commit == branchesShas[i][0]) { iter->setType(DVcsEvent::HEAD); iter->setPropetry(i, DVcsEvent::HEAD); heads_checked++; qCDebug(PLUGIN_GIT) << "HEAD found"; } //some optimization if (heads_checked == branchesShas.count() && parent_checked) break; } } return commitList; } void GitPlugin::initBranchHash(const QString &repo) { const QUrl repoUrl = QUrl::fromLocalFile(repo); QStringList gitBranches = runSynchronously(branches(repoUrl)).toStringList(); qCDebug(PLUGIN_GIT) << "BRANCHES: " << gitBranches; //Now root branch is the current branch. In future it should be the longest branch //other commitLists are got with git-rev-lits branch ^br1 ^ br2 QString root = runSynchronously(currentBranch(repoUrl)).toString(); QScopedPointer job(gitRevList(repo, QStringList(root))); bool ret = job->exec(); Q_ASSERT(ret && job->status()==VcsJob::JobSucceeded && "TODO: provide a fall back in case of failing"); Q_UNUSED(ret); QStringList commits = job->output().split('\n', QString::SkipEmptyParts); // qCDebug(PLUGIN_GIT) << "\n\n\n commits" << commits << "\n\n\n"; branchesShas.append(commits); foreach(const QString &branch, gitBranches) { if (branch == root) continue; QStringList args(branch); foreach(const QString &branch_arg, gitBranches) { if (branch_arg != branch) //man gitRevList for '^' args<<'^' + branch_arg; } QScopedPointer job(gitRevList(repo, args)); bool ret = job->exec(); Q_ASSERT(ret && job->status()==VcsJob::JobSucceeded && "TODO: provide a fall back in case of failing"); Q_UNUSED(ret); QStringList commits = job->output().split('\n', QString::SkipEmptyParts); // qCDebug(PLUGIN_GIT) << "\n\n\n commits" << commits << "\n\n\n"; branchesShas.append(commits); } } //Actually we can just copy the output without parsing. So it's a kind of draft for future void GitPlugin::parseLogOutput(const DVcsJob * job, QList& commits) const { // static QRegExp rx_sep( "[-=]+" ); // static QRegExp rx_date( "date:\\s+([^;]*);\\s+author:\\s+([^;]*).*" ); static QRegularExpression rx_com( "commit \\w{1,40}" ); const auto output = job->output(); const auto lines = output.splitRef('\n', QString::SkipEmptyParts); DVcsEvent item; QString commitLog; for (int i=0; i= 0x050500 if (rx_com.match(lines[i]).hasMatch()) { #else if (rx_com.match(lines[i].toString()).hasMatch()) { #endif // qCDebug(PLUGIN_GIT) << "MATCH COMMIT"; item.setCommit(lines[++i].toString()); item.setAuthor(lines[++i].toString()); item.setDate(lines[++i].toString()); item.setLog(commitLog); commits.append(item); } else { //FIXME: add this in a loop to the if, like in getAllCommits() commitLog += lines[i].toString() +'\n'; } } } VcsItemEvent::Actions actionsFromString(char c) { switch(c) { case 'A': return VcsItemEvent::Added; case 'D': return VcsItemEvent::Deleted; case 'R': return VcsItemEvent::Replaced; case 'M': return VcsItemEvent::Modified; } return VcsItemEvent::Modified; } void GitPlugin::parseGitLogOutput(DVcsJob * job) { static QRegExp commitRegex( "^commit (\\w{8})\\w{32}" ); static QRegExp infoRegex( "^(\\w+):(.*)" ); static QRegExp modificationsRegex("^([A-Z])[0-9]*\t([^\t]+)\t?(.*)", Qt::CaseSensitive, QRegExp::RegExp2); //R099 plugins/git/kdevgit.desktop plugins/git/kdevgit.desktop.cmake //M plugins/grepview/CMakeLists.txt QList commits; QString contents = job->output(); // check if git-log returned anything if (contents.isEmpty()) { job->setResults(commits); // empty list return; } // start parsing the output QTextStream s(&contents); VcsEvent item; QString message; bool pushCommit = false; while (!s.atEnd()) { QString line = s.readLine(); if (commitRegex.exactMatch(line)) { if (pushCommit) { item.setMessage(message.trimmed()); commits.append(QVariant::fromValue(item)); item.setItems(QList()); } else { pushCommit = true; } VcsRevision rev; rev.setRevisionValue(commitRegex.cap(1), KDevelop::VcsRevision::GlobalNumber); item.setRevision(rev); message.clear(); } else if (infoRegex.exactMatch(line)) { QString cap1 = infoRegex.cap(1); if (cap1 == QLatin1String("Author")) { item.setAuthor(infoRegex.cap(2).trimmed()); } else if (cap1 == QLatin1String("Date")) { item.setDate(QDateTime::fromTime_t(infoRegex.cap(2).trimmed().split(' ')[0].toUInt())); } } else if (modificationsRegex.exactMatch(line)) { VcsItemEvent::Actions a = actionsFromString(modificationsRegex.cap(1).at(0).toLatin1()); QString filenameA = modificationsRegex.cap(2); VcsItemEvent itemEvent; itemEvent.setActions(a); itemEvent.setRepositoryLocation(filenameA); if(a==VcsItemEvent::Replaced) { QString filenameB = modificationsRegex.cap(3); itemEvent.setRepositoryCopySourceLocation(filenameB); } item.addItem(itemEvent); } else if (line.startsWith(QLatin1String(" "))) { message += line.remove(0, 4); message += '\n'; } } item.setMessage(message.trimmed()); commits.append(QVariant::fromValue(item)); job->setResults(commits); } void GitPlugin::parseGitDiffOutput(DVcsJob* job) { VcsDiff diff; diff.setDiff(job->output()); diff.setBaseDiff(repositoryRoot(QUrl::fromLocalFile(job->directory().absolutePath()))); diff.setDepth(usePrefix()? 1 : 0); job->setResults(qVariantFromValue(diff)); } static VcsStatusInfo::State lsfilesToState(char id) { switch(id) { case 'H': return VcsStatusInfo::ItemUpToDate; //Cached case 'S': return VcsStatusInfo::ItemUpToDate; //Skip work tree case 'M': return VcsStatusInfo::ItemHasConflicts; //unmerged case 'R': return VcsStatusInfo::ItemDeleted; //removed/deleted case 'C': return VcsStatusInfo::ItemModified; //modified/changed case 'K': return VcsStatusInfo::ItemDeleted; //to be killed case '?': return VcsStatusInfo::ItemUnknown; //other } Q_ASSERT(false); return VcsStatusInfo::ItemUnknown; } void GitPlugin::parseGitStatusOutput_old(DVcsJob* job) { QStringList outputLines = job->output().split('\n', QString::SkipEmptyParts); QDir dir = job->directory(); QMap allStatus; foreach(const QString& line, outputLines) { VcsStatusInfo::State status = lsfilesToState(line[0].toLatin1()); QUrl url = QUrl::fromLocalFile(dir.absoluteFilePath(line.right(line.size()-2))); allStatus[url] = status; } QVariantList statuses; QMap< QUrl, VcsStatusInfo::State >::const_iterator it = allStatus.constBegin(), itEnd=allStatus.constEnd(); for(; it!=itEnd; ++it) { VcsStatusInfo status; status.setUrl(it.key()); status.setState(it.value()); statuses.append(qVariantFromValue(status)); } job->setResults(statuses); } void GitPlugin::parseGitStatusOutput(DVcsJob* job) { const auto output = job->output(); const auto outputLines = output.splitRef('\n', QString::SkipEmptyParts); QDir workingDir = job->directory(); QDir dotGit = dotGitDirectory(QUrl::fromLocalFile(workingDir.absolutePath())); QVariantList statuses; QList processedFiles; foreach(const QStringRef& line, outputLines) { //every line is 2 chars for the status, 1 space then the file desc QStringRef curr=line.right(line.size()-3); QStringRef state = line.left(2); int arrow = curr.indexOf(QStringLiteral(" -> ")); if(arrow>=0) { VcsStatusInfo status; status.setUrl(QUrl::fromLocalFile(dotGit.absoluteFilePath(curr.toString().left(arrow)))); status.setState(VcsStatusInfo::ItemDeleted); statuses.append(qVariantFromValue(status)); processedFiles += status.url(); curr = curr.mid(arrow+4); } if(curr.startsWith('\"') && curr.endsWith('\"')) { //if the path is quoted, unquote curr = curr.mid(1, curr.size()-2); } VcsStatusInfo status; status.setUrl(QUrl::fromLocalFile(dotGit.absoluteFilePath(curr.toString()))); status.setState(messageToState(state)); processedFiles.append(status.url()); qCDebug(PLUGIN_GIT) << "Checking git status for " << line << curr << status.state(); statuses.append(qVariantFromValue(status)); } QStringList paths; QStringList oldcmd=job->dvcsCommand(); QStringList::const_iterator it=oldcmd.constBegin()+oldcmd.indexOf(QStringLiteral("--"))+1, itEnd=oldcmd.constEnd(); for(; it!=itEnd; ++it) paths += *it; //here we add the already up to date files QStringList files = getLsFiles(job->directory(), QStringList() << QStringLiteral("-c") << QStringLiteral("--") << paths, OutputJob::Silent); foreach(const QString& file, files) { QUrl fileUrl = QUrl::fromLocalFile(workingDir.absoluteFilePath(file)); if(!processedFiles.contains(fileUrl)) { VcsStatusInfo status; status.setUrl(fileUrl); status.setState(VcsStatusInfo::ItemUpToDate); statuses.append(qVariantFromValue(status)); } } job->setResults(statuses); } void GitPlugin::parseGitVersionOutput(DVcsJob* job) { const auto output = job->output().trimmed(); auto versionString = output.midRef(output.lastIndexOf(' ')).split('.'); static const QList minimumVersion = QList() << 1 << 7; qCDebug(PLUGIN_GIT) << "checking git version" << versionString << "against" << minimumVersion; m_oldVersion = false; if (versionString.size() < minimumVersion.size()) { m_oldVersion = true; qCWarning(PLUGIN_GIT) << "invalid git version string:" << job->output().trimmed(); return; } foreach(int num, minimumVersion) { QStringRef curr = versionString.takeFirst(); int valcurr = curr.toInt(); if (valcurr < num) { m_oldVersion = true; break; } if (valcurr > num) { m_oldVersion = false; break; } } qCDebug(PLUGIN_GIT) << "the current git version is old: " << m_oldVersion; } QStringList GitPlugin::getLsFiles(const QDir &directory, const QStringList &args, KDevelop::OutputJob::OutputJobVerbosity verbosity) { QScopedPointer job(lsFiles(directory, args, verbosity)); if (job->exec() && job->status() == KDevelop::VcsJob::JobSucceeded) return job->output().split('\n', QString::SkipEmptyParts); return QStringList(); } DVcsJob* GitPlugin::gitRevParse(const QString &repository, const QStringList &args, KDevelop::OutputJob::OutputJobVerbosity verbosity) { DVcsJob* job = new DVcsJob(QDir(repository), this, verbosity); *job << "git" << "rev-parse" << args; return job; } DVcsJob* GitPlugin::gitRevList(const QString& directory, const QStringList& args) { DVcsJob* job = new DVcsJob(urlDir(QUrl::fromLocalFile(directory)), this, KDevelop::OutputJob::Silent); { *job << "git" << "rev-list" << args; return job; } } VcsStatusInfo::State GitPlugin::messageToState(const QStringRef& msg) { Q_ASSERT(msg.size()==1 || msg.size()==2); VcsStatusInfo::State ret = VcsStatusInfo::ItemUnknown; if(msg.contains('U') || msg == QLatin1String("AA") || msg == QLatin1String("DD")) ret = VcsStatusInfo::ItemHasConflicts; else switch(msg.at(0).toLatin1()) { case 'M': ret = VcsStatusInfo::ItemModified; break; case 'A': ret = VcsStatusInfo::ItemAdded; break; case 'R': ret = VcsStatusInfo::ItemModified; break; case 'C': ret = VcsStatusInfo::ItemHasConflicts; break; case ' ': ret = msg.at(1) == 'M' ? VcsStatusInfo::ItemModified : VcsStatusInfo::ItemDeleted; break; case 'D': ret = VcsStatusInfo::ItemDeleted; break; case '?': ret = VcsStatusInfo::ItemUnknown; break; default: qCDebug(PLUGIN_GIT) << "Git status not identified:" << msg; break; } return ret; } StandardJob::StandardJob(IPlugin* parent, KJob* job, OutputJob::OutputJobVerbosity verbosity) : VcsJob(parent, verbosity) , m_job(job) , m_plugin(parent) , m_status(JobNotStarted) {} void StandardJob::start() { connect(m_job, &KJob::result, this, &StandardJob::result); m_job->start(); m_status=JobRunning; } void StandardJob::result(KJob* job) { m_status=job->error() == 0? JobSucceeded : JobFailed; emitResult(); } VcsJob* GitPlugin::copy(const QUrl& localLocationSrc, const QUrl& localLocationDstn) { //TODO: Probably we should "git add" after return new StandardJob(this, KIO::copy(localLocationSrc, localLocationDstn), KDevelop::OutputJob::Silent); } VcsJob* GitPlugin::move(const QUrl& source, const QUrl& destination) { QDir dir = urlDir(source); QFileInfo fileInfo(source.toLocalFile()); if (fileInfo.isDir()) { if (isEmptyDirStructure(QDir(source.toLocalFile()))) { //move empty folder, git doesn't do that qCDebug(PLUGIN_GIT) << "empty folder" << source; return new StandardJob(this, KIO::move(source, destination), KDevelop::OutputJob::Silent); } } QStringList otherStr = getLsFiles(dir, QStringList() << QStringLiteral("--others") << QStringLiteral("--") << source.toLocalFile(), KDevelop::OutputJob::Silent); if(otherStr.isEmpty()) { DVcsJob* job = new DVcsJob(dir, this, KDevelop::OutputJob::Verbose); *job << "git" << "mv" << source.toLocalFile() << destination.toLocalFile(); return job; } else { return new StandardJob(this, KIO::move(source, destination), KDevelop::OutputJob::Silent); } } void GitPlugin::parseGitRepoLocationOutput(DVcsJob* job) { job->setResults(QVariant::fromValue(QUrl::fromLocalFile(job->output()))); } VcsJob* GitPlugin::repositoryLocation(const QUrl& localLocation) { DVcsJob* job = new DVcsJob(urlDir(localLocation), this); //Probably we should check first if origin is the proper remote we have to use but as a first attempt it works *job << "git" << "config" << "remote.origin.url"; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitRepoLocationOutput); return job; } VcsJob* GitPlugin::pull(const KDevelop::VcsLocation& localOrRepoLocationSrc, const QUrl& localRepositoryLocation) { DVcsJob* job = new DVcsJob(urlDir(localRepositoryLocation), this); job->setCommunicationMode(KProcess::MergedChannels); *job << "git" << "pull"; if(!localOrRepoLocationSrc.localUrl().isEmpty()) *job << localOrRepoLocationSrc.localUrl().url(); return job; } VcsJob* GitPlugin::push(const QUrl& localRepositoryLocation, const KDevelop::VcsLocation& localOrRepoLocationDst) { DVcsJob* job = new DVcsJob(urlDir(localRepositoryLocation), this); job->setCommunicationMode(KProcess::MergedChannels); *job << "git" << "push"; if(!localOrRepoLocationDst.localUrl().isEmpty()) *job << localOrRepoLocationDst.localUrl().url(); return job; } VcsJob* GitPlugin::resolve(const QList& localLocations, IBasicVersionControl::RecursionMode recursion) { return add(localLocations, recursion); } VcsJob* GitPlugin::update(const QList& localLocations, const KDevelop::VcsRevision& rev, IBasicVersionControl::RecursionMode recursion) { if(rev.revisionType()==VcsRevision::Special && rev.revisionValue().value()==VcsRevision::Head) { return pull(VcsLocation(), localLocations.first()); } else { DVcsJob* job = new DVcsJob(urlDir(localLocations.first()), this); { //Probably we should check first if origin is the proper remote we have to use but as a first attempt it works *job << "git" << "checkout" << rev.revisionValue().toString() << "--"; *job << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations)); return job; } } } void GitPlugin::setupCommitMessageEditor(const QUrl& localLocation, KTextEdit* editor) const { new GitMessageHighlighter(editor); QFile mergeMsgFile(dotGitDirectory(localLocation).filePath(QStringLiteral(".git/MERGE_MSG"))); // Some limit on the file size should be set since whole content is going to be read into // the memory. 1Mb seems to be good value since it's rather strange to have so huge commit // message. static const qint64 maxMergeMsgFileSize = 1024*1024; if (mergeMsgFile.size() > maxMergeMsgFileSize || !mergeMsgFile.open(QIODevice::ReadOnly)) return; QString mergeMsg = QString::fromLocal8Bit(mergeMsgFile.read(maxMergeMsgFileSize)); editor->setPlainText(mergeMsg); } class GitVcsLocationWidget : public KDevelop::StandardVcsLocationWidget { Q_OBJECT public: explicit GitVcsLocationWidget(QWidget* parent = nullptr, Qt::WindowFlags f = nullptr) : StandardVcsLocationWidget(parent, f) {} bool isCorrect() const override { return !url().isEmpty(); } }; KDevelop::VcsLocationWidget* GitPlugin::vcsLocation(QWidget* parent) const { return new GitVcsLocationWidget(parent); } void GitPlugin::registerRepositoryForCurrentBranchChanges(const QUrl& repository) { QDir dir = dotGitDirectory(repository); QString headFile = dir.absoluteFilePath(QStringLiteral(".git/HEAD")); m_watcher->addFile(headFile); } void GitPlugin::fileChanged(const QString& file) { Q_ASSERT(file.endsWith(QStringLiteral("HEAD"))); //SMTH/.git/HEAD -> SMTH/ const QUrl fileUrl = Path(file).parent().parent().toUrl(); //We need to delay the emitted signal, otherwise the branch hasn't change yet //and the repository is not functional m_branchesChange.append(fileUrl); QTimer::singleShot(1000, this, SLOT(delayedBranchChanged())); } void GitPlugin::delayedBranchChanged() { emit repositoryBranchChanged(m_branchesChange.takeFirst()); } CheckInRepositoryJob* GitPlugin::isInRepository(KTextEditor::Document* document) { CheckInRepositoryJob* job = new GitPluginCheckInRepositoryJob(document, repositoryRoot(document->url()).path()); job->start(); return job; } DVcsJob* GitPlugin::setConfigOption(const QUrl& repository, const QString& key, const QString& value, bool global) { auto job = new DVcsJob(urlDir(repository), this); QStringList args; args << "git" << "config"; if(global) args << "--global"; args << key << value; *job << args; return job; } QString GitPlugin::readConfigOption(const QUrl& repository, const QString& key) { QProcess exec; exec.setWorkingDirectory(urlDir(repository).absolutePath()); exec.start("git", QStringList() << "config" << "--get" << key); exec.waitForFinished(); return exec.readAllStandardOutput().trimmed(); } #include "gitplugin.moc" diff --git a/plugins/git/gitplugin.h b/plugins/git/gitplugin.h index bbf4ed055f..e8b3191a77 100644 --- a/plugins/git/gitplugin.h +++ b/plugins/git/gitplugin.h @@ -1,224 +1,225 @@ /*************************************************************************** * Copyright 2008 Evgeniy Ivanov * * Copyright 2009 Hugo Parente Lima * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #ifndef KDEVPLATFORM_PLUGIN_GIT_PLUGIN_H #define KDEVPLATFORM_PLUGIN_GIT_PLUGIN_H #include #include #include #include #include #include #include #include class KDirWatch; class QDir; namespace KDevelop { class VcsJob; class VcsRevision; } class StandardJob : public KDevelop::VcsJob { Q_OBJECT public: StandardJob(KDevelop::IPlugin* parent, KJob* job, OutputJobVerbosity verbosity); QVariant fetchResults() override { return QVariant(); } void start() override; JobStatus status() const override { return m_status; } KDevelop::IPlugin* vcsPlugin() const override { return m_plugin; } public slots: void result(KJob*); private: KJob* m_job; KDevelop::IPlugin* m_plugin; JobStatus m_status; }; /** * This is the main class of KDevelop's Git plugin. * * It implements the DVCS dependent things not implemented in KDevelop::DistributedVersionControlPlugin * @author Evgeniy Ivanov */ class GitPlugin: public KDevelop::DistributedVersionControlPlugin, public KDevelop::IContentAwareVersionControl { Q_OBJECT Q_INTERFACES(KDevelop::IBasicVersionControl KDevelop::IDistributedVersionControl KDevelop::IContentAwareVersionControl) friend class GitInitTest; public: explicit GitPlugin(QObject *parent, const QVariantList & args = QVariantList() ); ~GitPlugin() override; QString name() const override; + bool isValidRemoteRepositoryUrl(const QUrl& remoteLocation) override; bool isVersionControlled(const QUrl &path) override; KDevelop::VcsJob* copy(const QUrl& localLocationSrc, const QUrl& localLocationDstn) override; KDevelop::VcsJob* move(const QUrl& localLocationSrc, const QUrl& localLocationDst) override; //TODO KDevelop::VcsJob* pull(const KDevelop::VcsLocation& localOrRepoLocationSrc, const QUrl& localRepositoryLocation) override; KDevelop::VcsJob* push(const QUrl& localRepositoryLocation, const KDevelop::VcsLocation& localOrRepoLocationDst) override; KDevelop::VcsJob* repositoryLocation(const QUrl& localLocation) override; KDevelop::VcsJob* resolve(const QList& localLocations, RecursionMode recursion) override; KDevelop::VcsJob* update(const QList& localLocations, const KDevelop::VcsRevision& rev, RecursionMode recursion) override; KDevelop::VcsLocationWidget* vcsLocation(QWidget* parent) const override; void setupCommitMessageEditor(const QUrl& localLocation, KTextEdit* editor) const override; //End of KDevelop::VcsJob* add(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion = KDevelop::IBasicVersionControl::Recursive) override; KDevelop::VcsJob* createWorkingCopy(const KDevelop::VcsLocation & localOrRepoLocationSrc, const QUrl& localRepositoryRoot, KDevelop::IBasicVersionControl::RecursionMode) override; KDevelop::VcsJob* remove(const QList& files) override; KDevelop::VcsJob* status(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion = KDevelop::IBasicVersionControl::Recursive) override; KDevelop::VcsJob* commit(const QString& message, const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion = KDevelop::IBasicVersionControl::Recursive) override; KDevelop::VcsJob* diff(const QUrl& fileOrDirectory, const KDevelop::VcsRevision& srcRevision, const KDevelop::VcsRevision& dstRevision, KDevelop::VcsDiff::Type, RecursionMode recursion) override; KDevelop::VcsJob* log( const QUrl& localLocation, const KDevelop::VcsRevision& rev, unsigned long limit) override; KDevelop::VcsJob* log(const QUrl& localLocation, const KDevelop::VcsRevision& rev, const KDevelop::VcsRevision& limit) override; KDevelop::VcsJob* annotate(const QUrl &localLocation, const KDevelop::VcsRevision &rev) override; KDevelop::VcsJob* revert(const QList& localLocations, RecursionMode recursion) override; // Begin: KDevelop::IDistributedVersionControl KDevelop::VcsJob* init(const QUrl & directory) override; // Branch management KDevelop::VcsJob* tag(const QUrl& repository, const QString& commitMessage, const KDevelop::VcsRevision& rev, const QString& tagName) override; KDevelop::VcsJob* branch(const QUrl& repository, const KDevelop::VcsRevision& rev, const QString& branchName) override; KDevelop::VcsJob* branches(const QUrl& repository) override; KDevelop::VcsJob* currentBranch(const QUrl& repository) override; KDevelop::VcsJob* deleteBranch(const QUrl& repository, const QString& branchName) override; KDevelop::VcsJob* switchBranch(const QUrl& repository, const QString& branchName) override; KDevelop::VcsJob* renameBranch(const QUrl& repository, const QString& oldBranchName, const QString& newBranchName) override; KDevelop::VcsJob* mergeBranch(const QUrl& repository, const QString& branchName) override; //graph helpers QList getAllCommits(const QString &repo) override; //used in log void parseLogOutput(const KDevelop::DVcsJob * job, QList& commits) const override; void additionalMenuEntries(QMenu* menu, const QList& urls) override; KDevelop::DVcsJob* gitStash(const QDir& repository, const QStringList& args, KDevelop::OutputJob::OutputJobVerbosity verbosity); bool hasStashes(const QDir& repository); bool hasModifications(const QDir& repository); bool hasModifications(const QDir& repo, const QUrl& file); void registerRepositoryForCurrentBranchChanges(const QUrl& repository) override; KDevelop::CheckInRepositoryJob* isInRepository(KTextEditor::Document* document) override; KDevelop::DVcsJob* setConfigOption(const QUrl& repository, const QString& key, const QString& value, bool global = false); QString readConfigOption(const QUrl& repository, const QString& key); // this indicates whether the diff() function will generate a diff (patch) which // includes the working copy directory name or not (in which case git diff is called // with --no-prefix). bool usePrefix() const { return m_usePrefix; } void setUsePrefix(bool p) { m_usePrefix = p; } protected: QUrl repositoryRoot(const QUrl& path); bool isValidDirectory(const QUrl &dirPath) override; KDevelop::DVcsJob* lsFiles(const QDir &repository, const QStringList &args, KDevelop::OutputJob::OutputJobVerbosity verbosity = KDevelop::OutputJob::Verbose); KDevelop::DVcsJob* gitRevList(const QString &directory, const QStringList &args); KDevelop::DVcsJob* gitRevParse(const QString &repository, const QStringList &args, KDevelop::OutputJob::OutputJobVerbosity verbosity = KDevelop::OutputJob::Silent); private slots: void parseGitBlameOutput(KDevelop::DVcsJob *job); void parseGitLogOutput(KDevelop::DVcsJob *job); void parseGitDiffOutput(KDevelop::DVcsJob* job); void parseGitRepoLocationOutput(KDevelop::DVcsJob* job); void parseGitStatusOutput(KDevelop::DVcsJob* job); void parseGitStatusOutput_old(KDevelop::DVcsJob* job); void parseGitVersionOutput(KDevelop::DVcsJob* job); void parseGitBranchOutput(KDevelop::DVcsJob* job); void parseGitCurrentBranch(KDevelop::DVcsJob* job); void ctxPushStash(); void ctxPopStash(); void ctxStashManager(); void fileChanged(const QString& file); void delayedBranchChanged(); signals: void repositoryBranchChanged(const QUrl& repository); private: bool ensureValidGitIdentity(const QDir& dir); void addNotVersionedFiles(const QDir& dir, const QList& files); //commit dialog "main" helper QStringList getLsFiles(const QDir &directory, const QStringList &args, KDevelop::OutputJob::OutputJobVerbosity verbosity); KDevelop::DVcsJob* errorsFound(const QString& error, KDevelop::OutputJob::OutputJobVerbosity verbosity); void initBranchHash(const QString &repo); static KDevelop::VcsStatusInfo::State messageToState(const QStringRef& ch); QList branchesShas; QList m_urls; /** Tells if it's older than 1.7.0 or not */ bool m_oldVersion; KDirWatch* m_watcher; QList m_branchesChange; bool m_usePrefix; }; QVariant runSynchronously(KDevelop::VcsJob* job); #endif diff --git a/plugins/perforce/perforceplugin.cpp b/plugins/perforce/perforceplugin.cpp index 2b96de43ae..fb16f9daa6 100644 --- a/plugins/perforce/perforceplugin.cpp +++ b/plugins/perforce/perforceplugin.cpp @@ -1,692 +1,699 @@ /*************************************************************************** * Copyright 2010 Morten Danielsen Volden * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "perforceplugin.h" #include "debug.h" #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 KDevelop; namespace { QString toRevisionName(const KDevelop::VcsRevision& rev, QString currentRevision=QString()) { bool ok; int previous = currentRevision.toInt(&ok); previous--; QString tmp; switch(rev.revisionType()) { case VcsRevision::Special: switch(rev.revisionValue().value()) { case VcsRevision::Head: return QStringLiteral("#head"); case VcsRevision::Base: return QStringLiteral("#have"); case VcsRevision::Working: return QStringLiteral("#have"); case VcsRevision::Previous: Q_ASSERT(!currentRevision.isEmpty()); tmp.setNum(previous); tmp.prepend("#"); return tmp; case VcsRevision::Start: return QString(); case VcsRevision::UserSpecialType: //Not used Q_ASSERT(false && "i don't know how to do that"); } break; case VcsRevision::GlobalNumber: tmp.append("#"); tmp.append(rev.revisionValue().toString()); return tmp; case VcsRevision::Date: case VcsRevision::FileNumber: case VcsRevision::Invalid: case VcsRevision::UserSpecialType: Q_ASSERT(false); } return QString(); } VcsItemEvent::Actions actionsFromString(QString const& changeDescription) { if(changeDescription == "add") return VcsItemEvent::Added; if(changeDescription == "delete") return VcsItemEvent::Deleted; return VcsItemEvent::Modified; } QDir urlDir(const QUrl& url) { QFileInfo f(url.toLocalFile()); if(f.isDir()) return QDir(url.toLocalFile()); else return f.absoluteDir(); } } Q_LOGGING_CATEGORY(PLUGIN_PERFORCE, "kdevplatform.plugins.perforce") PerforcePlugin::PerforcePlugin(QObject* parent, const QVariantList&): KDevelop::IPlugin("kdevperforce", parent) , m_common(new KDevelop::VcsPluginHelper(this, this)) , m_perforcemenu(nullptr) , m_perforceConfigName("p4config.txt") , m_perforceExecutable("p4") , m_edit_action(nullptr) { QProcessEnvironment currentEviron(QProcessEnvironment::systemEnvironment()); QString tmp(currentEviron.value("P4CONFIG")); if (tmp.isEmpty()) { // We require the P4CONFIG variable to be set because the perforce command line client will need it setErrorDescription(i18n("The variable P4CONFIG is not set. Is perforce installed on the system?")); return; } else { m_perforceConfigName = tmp; } qCDebug(PLUGIN_PERFORCE) << "The value of P4CONFIG is : " << tmp; } PerforcePlugin::~PerforcePlugin() { } QString PerforcePlugin::name() const { return i18n("Perforce"); } KDevelop::VcsImportMetadataWidget* PerforcePlugin::createImportMetadataWidget(QWidget* /*parent*/) { return nullptr; } +bool PerforcePlugin::isValidRemoteRepositoryUrl(const QUrl& remoteLocation) +{ + Q_UNUSED(remoteLocation); + // TODO + return false; +} + bool PerforcePlugin::isValidDirectory(const QUrl & dirPath) { const QFileInfo finfo(dirPath.toLocalFile()); QDir dir = finfo.isDir() ? QDir(dirPath.toLocalFile()) : finfo.absoluteDir(); do { if (dir.exists(m_perforceConfigName)) { return true; } } while (dir.cdUp()); return false; } bool PerforcePlugin::isVersionControlled(const QUrl& localLocation) { QFileInfo fsObject(localLocation.toLocalFile()); if (fsObject.isDir()) { return isValidDirectory(localLocation); } return parseP4fstat(fsObject, KDevelop::OutputJob::Silent); } DVcsJob* PerforcePlugin::p4fstatJob(const QFileInfo& curFile, OutputJob::OutputJobVerbosity verbosity) { DVcsJob* job = new DVcsJob(curFile.absolutePath(), this, verbosity); setEnvironmentForJob(job, curFile); *job << m_perforceExecutable << "fstat" << curFile.fileName(); return job; } bool PerforcePlugin::parseP4fstat(const QFileInfo& curFile, OutputJob::OutputJobVerbosity verbosity) { QScopedPointer job(p4fstatJob(curFile, verbosity)); if (job->exec() && job->status() == KDevelop::VcsJob::JobSucceeded) { qCDebug(PLUGIN_PERFORCE) << "Perforce returned: " << job->output(); if (!job->output().isEmpty()) return true; } return false; } QString PerforcePlugin::getRepositoryName(const QFileInfo& curFile) { static const QString DEPOT_FILE_STR("... depotFile "); QString ret; QScopedPointer job(p4fstatJob(curFile, KDevelop::OutputJob::Silent)); if (job->exec() && job->status() == KDevelop::VcsJob::JobSucceeded) { if (!job->output().isEmpty()) { QStringList outputLines = job->output().split('\n', QString::SkipEmptyParts); foreach(const QString & line, outputLines) { int idx(line.indexOf(DEPOT_FILE_STR)); if (idx != -1) { ret = line.right(line.size() - DEPOT_FILE_STR.size()); return ret; } } } } return ret; } KDevelop::VcsJob* PerforcePlugin::repositoryLocation(const QUrl& /*localLocation*/) { return nullptr; } KDevelop::VcsJob* PerforcePlugin::add(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/) { QFileInfo curFile(localLocations.front().toLocalFile()); DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose); setEnvironmentForJob(job, curFile); *job << m_perforceExecutable << "add" << localLocations; return job; } KDevelop::VcsJob* PerforcePlugin::remove(const QList& /*localLocations*/) { return nullptr; } KDevelop::VcsJob* PerforcePlugin::copy(const QUrl& /*localLocationSrc*/, const QUrl& /*localLocationDstn*/) { return nullptr; } KDevelop::VcsJob* PerforcePlugin::move(const QUrl& /*localLocationSrc*/, const QUrl& /*localLocationDst*/) { return nullptr; } KDevelop::VcsJob* PerforcePlugin::status(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/) { if (localLocations.count() != 1) { KMessageBox::error(nullptr, i18n("Please select only one item for this operation")); return nullptr; } QFileInfo curFile(localLocations.front().toLocalFile()); DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose); setEnvironmentForJob(job, curFile); *job << m_perforceExecutable << "fstat" << curFile.fileName(); connect(job, &DVcsJob::readyForParsing, this, &PerforcePlugin::parseP4StatusOutput); return job; } KDevelop::VcsJob* PerforcePlugin::revert(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/) { if (localLocations.count() != 1) { KMessageBox::error(nullptr, i18n("Please select only one item for this operation")); return nullptr; } QFileInfo curFile(localLocations.front().toLocalFile()); DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose); setEnvironmentForJob(job, curFile); *job << m_perforceExecutable << "revert" << curFile.fileName(); return job; } KDevelop::VcsJob* PerforcePlugin::update(const QList& localLocations, const KDevelop::VcsRevision& /*rev*/, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/) { QFileInfo curFile(localLocations.front().toLocalFile()); DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose); setEnvironmentForJob(job, curFile); //*job << m_perforceExecutable << "-p" << "127.0.0.1:1666" << "info"; - Let's keep this for now it's very handy for debugging QString fileOrDirectory; if (curFile.isDir()) fileOrDirectory = curFile.absolutePath() + "/..."; else fileOrDirectory = curFile.fileName(); *job << m_perforceExecutable << "sync" << fileOrDirectory; return job; } KDevelop::VcsJob* PerforcePlugin::commit(const QString& message, const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/) { if (localLocations.empty() || message.isEmpty()) return errorsFound(i18n("No files or message specified")); QFileInfo curFile(localLocations.front().toLocalFile()); DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose); setEnvironmentForJob(job, curFile); *job << m_perforceExecutable << "submit" << "-d" << message << localLocations; return job; } KDevelop::VcsJob* PerforcePlugin::diff(const QUrl& fileOrDirectory, const KDevelop::VcsRevision& srcRevision, const KDevelop::VcsRevision& dstRevision, KDevelop::VcsDiff::Type , KDevelop::IBasicVersionControl::RecursionMode /*recursion*/) { QFileInfo curFile(fileOrDirectory.toLocalFile()); QString depotSrcFileName = getRepositoryName(curFile); QString depotDstFileName = depotSrcFileName; depotSrcFileName.append(toRevisionName(srcRevision, dstRevision.prettyValue())); // dstRevision acutally contains the number that we want to take previous of DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose); setEnvironmentForJob(job, curFile); switch (dstRevision.revisionType()) { case VcsRevision::FileNumber: case VcsRevision::GlobalNumber: depotDstFileName.append("#"); depotDstFileName.append(dstRevision.prettyValue()); *job << m_perforceExecutable << "diff2" << "-u" << depotSrcFileName << depotDstFileName; break; case VcsRevision::Special: switch (dstRevision.revisionValue().value()) { case VcsRevision::Working: *job << m_perforceExecutable << "diff" << "-du" << depotSrcFileName; break; case VcsRevision::Start: case VcsRevision::UserSpecialType: default: break; } default: break; } connect(job, &DVcsJob::readyForParsing, this, &PerforcePlugin::parseP4DiffOutput); return job; } KDevelop::VcsJob* PerforcePlugin::log(const QUrl& localLocation, const KDevelop::VcsRevision& rev, long unsigned int limit) { static QString lastSeenChangeList; QFileInfo curFile(localLocation.toLocalFile()); QString localLocationAndRevStr = localLocation.toLocalFile(); DVcsJob* job = new DVcsJob(urlDir(localLocation), this, KDevelop::OutputJob::Verbose); setEnvironmentForJob(job, curFile); *job << m_perforceExecutable << "filelog" << "-lit"; if(limit > 0) *job << QStringLiteral("-m %1").arg(limit); if (curFile.isDir()) { localLocationAndRevStr.append("/..."); } QString revStr = toRevisionName(rev, QString()); if(!revStr.isEmpty()) { // This is not too nice, but perforce argument for restricting output from filelog does not Work :-( // So putting this in so we do not end up in infinite loop calling log, if(revStr == lastSeenChangeList) { localLocationAndRevStr.append("#none"); lastSeenChangeList.clear(); } else { localLocationAndRevStr.append(revStr); lastSeenChangeList = revStr; } } *job << localLocationAndRevStr; qCDebug(PLUGIN_PERFORCE) << "Issuing the following command to p4: " << job->dvcsCommand(); connect(job, &DVcsJob::readyForParsing, this, &PerforcePlugin::parseP4LogOutput); return job; } KDevelop::VcsJob* PerforcePlugin::log(const QUrl& localLocation, const KDevelop::VcsRevision& /*rev*/, const KDevelop::VcsRevision& /*limit*/) { QFileInfo curFile(localLocation.toLocalFile()); if (curFile.isDir()) { KMessageBox::error(nullptr, i18n("Please select a file for this operation")); return errorsFound(i18n("Directory not supported for this operation")); } DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose); setEnvironmentForJob(job, curFile); *job << m_perforceExecutable << "filelog" << "-lit" << localLocation; connect(job, &DVcsJob::readyForParsing, this , &PerforcePlugin::parseP4LogOutput); return job; } KDevelop::VcsJob* PerforcePlugin::annotate(const QUrl& localLocation, const KDevelop::VcsRevision& /*rev*/) { QFileInfo curFile(localLocation.toLocalFile()); if (curFile.isDir()) { KMessageBox::error(nullptr, i18n("Please select a file for this operation")); return errorsFound(i18n("Directory not supported for this operation")); } DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose); setEnvironmentForJob(job, curFile); *job << m_perforceExecutable << "annotate" << "-qi" << localLocation; connect(job, &DVcsJob::readyForParsing, this , &PerforcePlugin::parseP4AnnotateOutput); return job; } KDevelop::VcsJob* PerforcePlugin::resolve(const QList& /*localLocations*/, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/) { return nullptr; } KDevelop::VcsJob* PerforcePlugin::createWorkingCopy(const KDevelop::VcsLocation& /*sourceRepository*/, const QUrl& /*destinationDirectory*/, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/) { return nullptr; } KDevelop::VcsLocationWidget* PerforcePlugin::vcsLocation(QWidget* parent) const { return new StandardVcsLocationWidget(parent); } KDevelop::VcsJob* PerforcePlugin::edit(const QList& localLocations) { QFileInfo curFile(localLocations.front().toLocalFile()); DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose); setEnvironmentForJob(job, curFile); *job << m_perforceExecutable << "edit" << localLocations; return job; } KDevelop::VcsJob* PerforcePlugin::edit(const QUrl& /*localLocation*/) { return nullptr; } KDevelop::VcsJob* PerforcePlugin::unedit(const QUrl& /*localLocation*/) { return nullptr; } KDevelop::VcsJob* PerforcePlugin::localRevision(const QUrl& /*localLocation*/, KDevelop::VcsRevision::RevisionType) { return nullptr; } KDevelop::VcsJob* PerforcePlugin::import(const QString& /*commitMessage*/, const QUrl& /*sourceDirectory*/, const KDevelop::VcsLocation& /*destinationRepository*/) { return nullptr; } KDevelop::ContextMenuExtension PerforcePlugin::contextMenuExtension(KDevelop::Context* context) { m_common->setupFromContext(context); const QList & ctxUrlList = m_common->contextUrlList(); bool hasVersionControlledEntries = false; for( const QUrl& url : ctxUrlList) { if (isValidDirectory(url)) { hasVersionControlledEntries = true; break; } } if (!hasVersionControlledEntries) return IPlugin::contextMenuExtension(context); QMenu * perforceMenu = m_common->commonActions(); perforceMenu->addSeparator(); perforceMenu->addSeparator(); if (!m_edit_action) { m_edit_action = new QAction(i18n("Edit"), this); connect(m_edit_action, &QAction::triggered, this, & PerforcePlugin::ctxEdit); } perforceMenu->addAction(m_edit_action); ContextMenuExtension menuExt; menuExt.addAction(ContextMenuExtension::VcsGroup, perforceMenu->menuAction()); return menuExt; } void PerforcePlugin::ctxEdit() { QList const & ctxUrlList = m_common->contextUrlList(); KDevelop::ICore::self()->runController()->registerJob(edit(ctxUrlList)); } void PerforcePlugin::setEnvironmentForJob(DVcsJob* job, const QFileInfo& curFile) { KProcess* jobproc = job->process(); jobproc->setEnv("P4CONFIG", m_perforceConfigName); if (curFile.isDir()) { jobproc->setEnv("PWD", curFile.filePath()); } else { jobproc->setEnv("PWD", curFile.absolutePath()); } } QList PerforcePlugin::getQvariantFromLogOutput(QStringList const& outputLines) { static const QString LOGENTRY_START("... #"); static const QString DEPOTMESSAGE_START("... ."); QMap changes; QList commits; QString currentFileName; QString changeNumberStr, author,changeDescription, commitMessage; VcsEvent currentVcsEvent; VcsItemEvent currentRepoFile; VcsRevision rev; int indexofAt; int changeNumber = 0; foreach(const QString & line, outputLines) { if (!line.startsWith(LOGENTRY_START) && !line.startsWith(DEPOTMESSAGE_START) && !line.startsWith('\t')) { currentFileName = line; } if(line.indexOf(LOGENTRY_START) != -1) { // expecting the Logentry line to be of the form: //... #5 change 10 edit on 2010/12/06 12:07:31 by mvo@testbed (text) changeNumberStr = line.section(' ', 3, 3 ); // We use global change number changeNumber = changeNumberStr.toInt(); author = line.section(' ', 9, 9); changeDescription = line.section(' ' , 4, 4 ); indexofAt = author.indexOf('@'); author.remove(indexofAt, author.size()); // Only keep the username itself rev.setRevisionValue(changeNumberStr, KDevelop::VcsRevision::GlobalNumber); changes[changeNumber].setRevision(rev); changes[changeNumber].setAuthor(author); changes[changeNumber].setDate(QDateTime::fromString(line.section(' ', 6, 7), "yyyy/MM/dd hh:mm:ss")); currentRepoFile.setRepositoryLocation(currentFileName); currentRepoFile.setActions( actionsFromString(changeDescription) ); changes[changeNumber].addItem(currentRepoFile); commitMessage.clear(); // We have a new entry, clear message } if (line.startsWith('\t') || line.startsWith(DEPOTMESSAGE_START)) { commitMessage += line.trimmed() + '\n'; changes[changeNumber].setMessage(commitMessage); } } for(auto item : changes) { commits.prepend(QVariant::fromValue(item)); } return commits; } void PerforcePlugin::parseP4StatusOutput(DVcsJob* job) { QStringList outputLines = job->output().split('\n', QString::SkipEmptyParts); QVariantList statuses; QList processedFiles; static const QString ACTION_STR("... action "); static const QString CLIENT_FILE_STR("... clientFile "); VcsStatusInfo status; status.setState(VcsStatusInfo::ItemUserState); foreach(const QString & line, outputLines) { int idx(line.indexOf(ACTION_STR)); if (idx != -1) { QString curr = line.right(line.size() - ACTION_STR.size()); if (curr == "edit") { status.setState(VcsStatusInfo::ItemModified); } else if (curr == "add") { status.setState(VcsStatusInfo::ItemAdded); } else { status.setState(VcsStatusInfo::ItemUserState); } continue; } idx = line.indexOf(CLIENT_FILE_STR); if (idx != -1) { QUrl fileUrl = QUrl::fromLocalFile(line.right(line.size() - CLIENT_FILE_STR.size())); status.setUrl(fileUrl); } } statuses.append(qVariantFromValue(status)); job->setResults(statuses); } void PerforcePlugin::parseP4LogOutput(KDevelop::DVcsJob* job) { QList commits(getQvariantFromLogOutput(job->output().split('\n', QString::SkipEmptyParts))); job->setResults(commits); } void PerforcePlugin::parseP4DiffOutput(DVcsJob* job) { VcsDiff diff; diff.setDiff(job->output()); QDir dir(job->directory()); do { if (dir.exists(m_perforceConfigName)) { break; } } while (dir.cdUp()); diff.setBaseDiff(QUrl::fromLocalFile(dir.absolutePath())); job->setResults(qVariantFromValue(diff)); } void PerforcePlugin::parseP4AnnotateOutput(DVcsJob *job) { QVariantList results; /// First get the changelists for this file QStringList strList(job->dvcsCommand()); QString localLocation(strList.last()); /// ASSUMPTION WARNING - localLocation is the last in the annotate command KDevelop::VcsRevision dummyRev; QScopedPointer logJob(new DVcsJob(job->directory(), this, OutputJob::Silent)); QFileInfo curFile(localLocation); setEnvironmentForJob(logJob.data(), curFile); *logJob << m_perforceExecutable << "filelog" << "-lit" << localLocation; QList commits; if (logJob->exec() && logJob->status() == KDevelop::VcsJob::JobSucceeded) { if (!job->output().isEmpty()) { commits = getQvariantFromLogOutput(logJob->output().split('\n', QString::SkipEmptyParts)); } } VcsEvent item; QMap globalCommits; /// Move the VcsEvents to a more suitable data strucure for (QList::const_iterator commitsIt = commits.constBegin(), commitsEnd = commits.constEnd(); commitsIt != commitsEnd; ++commitsIt) { if(commitsIt->canConvert()) { item = commitsIt->value(); } globalCommits.insert(item.revision().revisionValue().toLongLong(), item); } VcsAnnotationLine* annotation; QStringList lines = job->output().split('\n'); size_t lineNumber(0); QMap definedRevisions; QMap::iterator currentEvent; bool convertToIntOk(false); int globalRevisionInt(0); QString globalRevision; for (QStringList::const_iterator it = lines.constBegin(), itEnd = lines.constEnd(); it != itEnd; ++it) { if (it->isEmpty()) { continue; } globalRevision = it->left(it->indexOf(':')); annotation = new VcsAnnotationLine; annotation->setLineNumber(lineNumber); VcsRevision rev; rev.setRevisionValue(globalRevision, KDevelop::VcsRevision::GlobalNumber); annotation->setRevision(rev); // Find the other info in the commits list globalRevisionInt = globalRevision.toLongLong(&convertToIntOk); if(convertToIntOk) { currentEvent = globalCommits.find(globalRevisionInt); annotation->setAuthor(currentEvent->author()); annotation->setCommitMessage(currentEvent->message()); annotation->setDate(currentEvent->date()); } results += qVariantFromValue(*annotation); ++lineNumber; } job->setResults(results); } KDevelop::VcsJob* PerforcePlugin::errorsFound(const QString& error, KDevelop::OutputJob::OutputJobVerbosity verbosity) { DVcsJob* j = new DVcsJob(QDir::temp(), this, verbosity); *j << "echo" << i18n("error: %1", error) << "-n"; return j; } diff --git a/plugins/perforce/perforceplugin.h b/plugins/perforce/perforceplugin.h index ef9bb7b837..dcb2560ec7 100644 --- a/plugins/perforce/perforceplugin.h +++ b/plugins/perforce/perforceplugin.h @@ -1,176 +1,177 @@ /*************************************************************************** * Copyright 2010 Morten Danielsen Volden * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #ifndef KDEVPERFORCEPLUGIN_H #define KDEVPERFORCEPLUGIN_H #include #include #include #include #include #include #include class QMenu; class QFileInfo; class QDir; namespace KDevelop { class ContextMenuExtension; class VcsPluginHelper; class DVcsJob; } class PerforcePlugin : public KDevelop::IPlugin, public KDevelop::ICentralizedVersionControl { Q_OBJECT Q_INTERFACES(KDevelop::IBasicVersionControl KDevelop::ICentralizedVersionControl) friend class PerforcePluginTest; public: explicit PerforcePlugin(QObject* parent, const QVariantList & = QVariantList()); ~PerforcePlugin() override; //@{ /** Methods inherited from KDevelop::IBasicVersionControl */ QString name() const override; KDevelop::VcsImportMetadataWidget* createImportMetadataWidget(QWidget* parent) override; + bool isValidRemoteRepositoryUrl(const QUrl& remoteLocation) override; bool isVersionControlled(const QUrl& localLocation) override; KDevelop::VcsJob* repositoryLocation(const QUrl& localLocation) override; KDevelop::VcsJob* add(const QList& localLocations, RecursionMode recursion = IBasicVersionControl::Recursive) override; KDevelop::VcsJob* remove(const QList& localLocations) override; KDevelop::VcsJob* copy(const QUrl& localLocationSrc, const QUrl& localLocationDstn) override; KDevelop::VcsJob* move(const QUrl& localLocationSrc, const QUrl& localLocationDst) override; KDevelop::VcsJob* status(const QList& localLocations, RecursionMode recursion = IBasicVersionControl::Recursive) override; KDevelop::VcsJob* revert(const QList& localLocations, RecursionMode recursion = IBasicVersionControl::Recursive) override; KDevelop::VcsJob* update(const QList& localLocations, const KDevelop::VcsRevision& rev = KDevelop::VcsRevision::createSpecialRevision(KDevelop::VcsRevision::Head), KDevelop::IBasicVersionControl::RecursionMode recursion = KDevelop::IBasicVersionControl::Recursive) override; KDevelop::VcsJob* commit(const QString& message, const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion = KDevelop::IBasicVersionControl::Recursive) override; KDevelop::VcsJob* diff(const QUrl& fileOrDirectory, const KDevelop::VcsRevision& srcRevision, const KDevelop::VcsRevision& dstRevision, KDevelop::VcsDiff::Type = KDevelop::VcsDiff::DiffUnified, KDevelop::IBasicVersionControl::RecursionMode recursion = KDevelop::IBasicVersionControl::Recursive) override; KDevelop::VcsJob* log(const QUrl& localLocation, const KDevelop::VcsRevision& rev, unsigned long limit = 0) override; KDevelop::VcsJob* log(const QUrl& localLocation, const KDevelop::VcsRevision& rev = KDevelop::VcsRevision::createSpecialRevision(KDevelop::VcsRevision::Base), const KDevelop::VcsRevision& limit = KDevelop::VcsRevision::createSpecialRevision(KDevelop::VcsRevision::Start)) override; KDevelop::VcsJob* annotate(const QUrl& localLocation, const KDevelop::VcsRevision& rev = KDevelop::VcsRevision::createSpecialRevision(KDevelop::VcsRevision::Head)) override; KDevelop::VcsJob* resolve(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) override; KDevelop::VcsJob* createWorkingCopy(const KDevelop::VcsLocation & sourceRepository, const QUrl & destinationDirectory, KDevelop::IBasicVersionControl::RecursionMode recursion = IBasicVersionControl::Recursive) override; KDevelop::VcsLocationWidget* vcsLocation(QWidget* parent) const override; //@} //@{ /** Methods inherited from KDevelop::ICentralizedVersionControl */ KDevelop::VcsJob* edit(const QUrl& localLocation) override; KDevelop::VcsJob* unedit(const QUrl& localLocation) override; KDevelop::VcsJob* localRevision(const QUrl& localLocation, KDevelop::VcsRevision::RevisionType) override; KDevelop::VcsJob* import(const QString & commitMessage, const QUrl & sourceDirectory, const KDevelop::VcsLocation & destinationRepository) override; //@} /// This plugin implements its own edit method KDevelop::VcsJob* edit(const QList& localLocations); KDevelop::ContextMenuExtension contextMenuExtension(KDevelop::Context* context) override; public Q_SLOTS: /// invoked by context-menu void ctxEdit(); // void ctxUnedit(); // void ctxLocalRevision(); // void ctxImport(); private slots: void parseP4StatusOutput(KDevelop::DVcsJob* job); void parseP4DiffOutput(KDevelop::DVcsJob* job); void parseP4LogOutput(KDevelop::DVcsJob* job); void parseP4AnnotateOutput(KDevelop::DVcsJob* job); private: bool isValidDirectory(const QUrl & dirPath); KDevelop::DVcsJob* p4fstatJob(const QFileInfo& curFile, KDevelop::OutputJob::OutputJobVerbosity verbosity = KDevelop::OutputJob::Verbose); bool parseP4fstat(const QFileInfo& curFile, KDevelop::OutputJob::OutputJobVerbosity verbosity = KDevelop::OutputJob::Verbose); KDevelop::VcsJob* errorsFound(const QString& error, KDevelop::OutputJob::OutputJobVerbosity verbosity = KDevelop::OutputJob::Verbose); QString getRepositoryName(const QFileInfo& curFile); void setEnvironmentForJob(KDevelop::DVcsJob* job, QFileInfo const& fsObject); QList getQvariantFromLogOutput(QStringList const& outputLines); std::unique_ptr m_common; QMenu* m_perforcemenu; QString m_perforceConfigName; QString m_perforceExecutable; QAction* m_edit_action; }; #endif // PERFORCEPLUGIN_H diff --git a/plugins/subversion/kdevsvnplugin.cpp b/plugins/subversion/kdevsvnplugin.cpp index 71fb3fef44..cb70196f6e 100644 --- a/plugins/subversion/kdevsvnplugin.cpp +++ b/plugins/subversion/kdevsvnplugin.cpp @@ -1,528 +1,539 @@ /*************************************************************************** * Copyright 2007 Dukju Ahn * * Copyright 2008 Andreas Pakulat * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "kdevsvnplugin.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 #include #include #include #include #include #include #include "kdevsvncpp/apr.hpp" #include "svncommitjob.h" #include "svnstatusjob.h" #include "svnaddjob.h" #include "svnrevertjob.h" #include "svnremovejob.h" #include "svnupdatejob.h" #include "svninfojob.h" #include "svndiffjob.h" #include "svncopyjob.h" #include "svnmovejob.h" #include "svnlogjob.h" #include "svnblamejob.h" #include "svnimportjob.h" #include "svncheckoutjob.h" #include "svnimportmetadatawidget.h" #include "svncheckoutmetadatawidget.h" #include #include #include "svnlocationwidget.h" #include "debug.h" Q_LOGGING_CATEGORY(PLUGIN_SVN, "kdevplatform.plugins.svn") K_PLUGIN_FACTORY_WITH_JSON(KDevSvnFactory, "kdevsubversion.json", registerPlugin();) KDevSvnPlugin::KDevSvnPlugin(QObject *parent, const QVariantList &) : KDevelop::IPlugin(QStringLiteral("kdevsubversion"), parent) , m_common(new KDevelop::VcsPluginHelper(this, this)) , copy_action( nullptr ) , move_action( nullptr ) , m_jobQueue(new ThreadWeaver::Queue(this)) { qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); } KDevSvnPlugin::~KDevSvnPlugin() { } +bool KDevSvnPlugin::isValidRemoteRepositoryUrl(const QUrl& remoteLocation) +{ + const QString scheme = remoteLocation.scheme(); + if (scheme == QLatin1String("svn") || + scheme == QLatin1String("svn+ssh")) { + return true; + } + return false; +} + + bool KDevSvnPlugin::isVersionControlled(const QUrl &localLocation) { ///TODO: also check this in the other functions? if (!localLocation.isValid()) { return false; } SvnInfoJob* job = new SvnInfoJob(this); job->setLocation(localLocation); if (job->exec()) { QVariant result = job->fetchResults(); if (result.isValid()) { SvnInfoHolder h = result.value(); return !h.name.isEmpty(); } } else { qCDebug(PLUGIN_SVN) << "Couldn't execute job"; } return false; } KDevelop::VcsJob* KDevSvnPlugin::repositoryLocation(const QUrl &localLocation) { SvnInfoJob* job = new SvnInfoJob(this); job->setLocation(localLocation); job->setProvideInformation(SvnInfoJob::RepoUrlOnly); return job; } KDevelop::VcsJob* KDevSvnPlugin::status(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode mode) { SvnStatusJob* job = new SvnStatusJob(this); job->setLocations(localLocations); job->setRecursive((mode == KDevelop::IBasicVersionControl::Recursive)); return job; } KDevelop::VcsJob* KDevSvnPlugin::add(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) { SvnAddJob* job = new SvnAddJob(this); job->setLocations(localLocations); job->setRecursive((recursion == KDevelop::IBasicVersionControl::Recursive)); return job; } KDevelop::VcsJob* KDevSvnPlugin::remove(const QList& localLocations) { SvnRemoveJob* job = new SvnRemoveJob(this); job->setLocations(localLocations); return job; } KDevelop::VcsJob* KDevSvnPlugin::edit(const QUrl& /*localLocation*/) { return nullptr; } KDevelop::VcsJob* KDevSvnPlugin::unedit(const QUrl& /*localLocation*/) { return nullptr; } KDevelop::VcsJob* KDevSvnPlugin::localRevision(const QUrl &localLocation, KDevelop::VcsRevision::RevisionType type) { SvnInfoJob* job = new SvnInfoJob(this); job->setLocation(localLocation); job->setProvideInformation(SvnInfoJob::RevisionOnly); job->setProvideRevisionType(type); return job; } KDevelop::VcsJob* KDevSvnPlugin::copy(const QUrl &localLocationSrc, const QUrl& localLocationDstn) { SvnCopyJob* job = new SvnCopyJob(this); job->setSourceLocation(localLocationSrc); job->setDestinationLocation(localLocationDstn); return job; } KDevelop::VcsJob* KDevSvnPlugin::move(const QUrl &localLocationSrc, const QUrl& localLocationDst) { SvnMoveJob* job = new SvnMoveJob(this); job->setSourceLocation(localLocationSrc); job->setDestinationLocation(localLocationDst); return job; } KDevelop::VcsJob* KDevSvnPlugin::revert(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) { SvnRevertJob* job = new SvnRevertJob(this); job->setLocations(localLocations); job->setRecursive((recursion == KDevelop::IBasicVersionControl::Recursive)); return job; } KDevelop::VcsJob* KDevSvnPlugin::update(const QList& localLocations, const KDevelop::VcsRevision& rev, KDevelop::IBasicVersionControl::RecursionMode recursion) { SvnUpdateJob* job = new SvnUpdateJob(this); job->setLocations(localLocations); job->setRevision(rev); job->setRecursive((recursion == KDevelop::IBasicVersionControl::Recursive)); return job; } KDevelop::VcsJob* KDevSvnPlugin::commit(const QString& message, const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) { SvnCommitJob* job = new SvnCommitJob(this); qCDebug(PLUGIN_SVN) << "Committing locations:" << localLocations << endl; job->setUrls(localLocations); job->setCommitMessage(message) ; job->setRecursive((recursion == KDevelop::IBasicVersionControl::Recursive)); return job; } KDevelop::VcsJob* KDevSvnPlugin::diff(const QUrl &fileOrDirectory, const KDevelop::VcsRevision& srcRevision, const KDevelop::VcsRevision& dstRevision, KDevelop::VcsDiff::Type diffType, KDevelop::IBasicVersionControl::RecursionMode recurse) { KDevelop::VcsLocation loc(fileOrDirectory); return diff2(loc, loc, srcRevision, dstRevision, diffType, recurse); } KDevelop::VcsJob* KDevSvnPlugin::diff2(const KDevelop::VcsLocation& src, const KDevelop::VcsLocation& dst, const KDevelop::VcsRevision& srcRevision, const KDevelop::VcsRevision& dstRevision, KDevelop::VcsDiff::Type diffType, KDevelop::IBasicVersionControl::RecursionMode recurse) { SvnDiffJob* job = new SvnDiffJob(this); job->setSource(src); job->setDestination(dst); job->setSrcRevision(srcRevision); job->setDstRevision(dstRevision); job->setDiffType(diffType); job->setRecursive((recurse == KDevelop::IBasicVersionControl::Recursive)); return job; } KDevelop::VcsJob* KDevSvnPlugin::log(const QUrl &localLocation, const KDevelop::VcsRevision& rev, unsigned long limit) { SvnLogJob* job = new SvnLogJob(this); job->setLocation(localLocation); job->setStartRevision(rev); job->setLimit(limit); return job; } KDevelop::VcsJob* KDevSvnPlugin::log(const QUrl &localLocation, const KDevelop::VcsRevision& startRev, const KDevelop::VcsRevision& endRev) { SvnLogJob* job = new SvnLogJob(this); job->setLocation(localLocation); job->setStartRevision(startRev); job->setEndRevision(endRev); return job; } KDevelop::VcsJob* KDevSvnPlugin::annotate(const QUrl &localLocation, const KDevelop::VcsRevision& rev) { SvnBlameJob* job = new SvnBlameJob(this); job->setLocation(localLocation); job->setEndRevision(rev); return job; } KDevelop::VcsJob* KDevSvnPlugin::merge(const KDevelop::VcsLocation& localOrRepoLocationSrc, const KDevelop::VcsLocation& localOrRepoLocationDst, const KDevelop::VcsRevision& srcRevision, const KDevelop::VcsRevision& dstRevision, const QUrl &localLocation) { // TODO implement merge Q_UNUSED(localOrRepoLocationSrc) Q_UNUSED(localOrRepoLocationDst) Q_UNUSED(srcRevision) Q_UNUSED(dstRevision) Q_UNUSED(localLocation) return nullptr; } KDevelop::VcsJob* KDevSvnPlugin::resolve(const QList& /*localLocations*/, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/) { return nullptr; } KDevelop::VcsJob* KDevSvnPlugin::import(const QString & commitMessage, const QUrl &sourceDirectory, const KDevelop::VcsLocation & destinationRepository) { SvnImportJob* job = new SvnImportJob(this); job->setMapping(sourceDirectory, destinationRepository); job->setMessage(commitMessage); return job; } KDevelop::VcsJob* KDevSvnPlugin::createWorkingCopy(const KDevelop::VcsLocation & sourceRepository, const QUrl &destinationDirectory, KDevelop::IBasicVersionControl::RecursionMode recursion) { SvnCheckoutJob* job = new SvnCheckoutJob(this); job->setMapping(sourceRepository, destinationDirectory, recursion); return job; } KDevelop::ContextMenuExtension KDevSvnPlugin::contextMenuExtension(KDevelop::Context* context) { m_common->setupFromContext(context); const QList & ctxUrlList = m_common->contextUrlList(); bool hasVersionControlledEntries = false; foreach(const QUrl &url, ctxUrlList) { if (isVersionControlled(url) || isVersionControlled(KIO::upUrl(url))) { hasVersionControlledEntries = true; break; } } qCDebug(PLUGIN_SVN) << "version controlled?" << hasVersionControlledEntries; if (!hasVersionControlledEntries) return IPlugin::contextMenuExtension(context); QMenu* svnmenu= m_common->commonActions(); svnmenu->addSeparator(); if( !copy_action ) { copy_action = new QAction(i18n("Copy..."), this); connect(copy_action, SIGNAL(triggered()), this, SLOT(ctxCopy())); } svnmenu->addAction(copy_action); if( !move_action ) { move_action = new QAction(i18n("Move..."), this); connect(move_action, SIGNAL(triggered()), this, SLOT(ctxMove())); } svnmenu->addAction(move_action); KDevelop::ContextMenuExtension menuExt; menuExt.addAction(KDevelop::ContextMenuExtension::VcsGroup, svnmenu->menuAction()); return menuExt; } void KDevSvnPlugin::ctxInfo() { QList const & ctxUrlList = m_common->contextUrlList(); if (ctxUrlList.count() != 1) { KMessageBox::error(nullptr, i18n("Please select only one item for this operation")); return; } } void KDevSvnPlugin::ctxStatus() { QList const & ctxUrlList = m_common->contextUrlList(); if (ctxUrlList.count() > 1) { KMessageBox::error(nullptr, i18n("Please select only one item for this operation")); return; } } void KDevSvnPlugin::ctxCopy() { QList const & ctxUrlList = m_common->contextUrlList(); if (ctxUrlList.count() > 1) { KMessageBox::error(nullptr, i18n("Please select only one item for this operation")); return; } QUrl source = ctxUrlList.first(); if (source.isLocalFile()) { QUrl dir = source; bool isFile = QFileInfo(source.toLocalFile()).isFile(); if (isFile) { dir = dir.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash); } KUrlRequesterDialog dlg(dir, i18n("Destination file/directory"), nullptr); if (isFile) { dlg.urlRequester()->setMode(KFile::File | KFile::Directory | KFile::LocalOnly); } else { dlg.urlRequester()->setMode(KFile::Directory | KFile::LocalOnly); } if (dlg.exec() == QDialog::Accepted) { KDevelop::ICore::self()->runController()->registerJob(copy(source, dlg.selectedUrl())); } } else { KMessageBox::error(nullptr, i18n("Copying only works on local files")); return; } } void KDevSvnPlugin::ctxMove() { QList const & ctxUrlList = m_common->contextUrlList(); if (ctxUrlList.count() != 1) { KMessageBox::error(nullptr, i18n("Please select only one item for this operation")); return; } QUrl source = ctxUrlList.first(); if (source.isLocalFile()) { QUrl dir = source; bool isFile = QFileInfo(source.toLocalFile()).isFile(); if (isFile) { dir = source.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash); } KUrlRequesterDialog dlg(dir, i18n("Destination file/directory"), nullptr); if (isFile) { dlg.urlRequester()->setMode(KFile::File | KFile::Directory | KFile::LocalOnly); } else { dlg.urlRequester()->setMode(KFile::Directory | KFile::LocalOnly); } if (dlg.exec() == QDialog::Accepted) { KDevelop::ICore::self()->runController()->registerJob(move(source, dlg.selectedUrl())); } } else { KMessageBox::error(nullptr, i18n("Moving only works on local files/dirs")); return; } } void KDevSvnPlugin::ctxCat() { QList const & ctxUrlList = m_common->contextUrlList(); if (ctxUrlList.count() != 1) { KMessageBox::error(nullptr, i18n("Please select only one item for this operation")); return; } } QString KDevSvnPlugin::name() const { return i18n("Subversion"); } KDevelop::VcsImportMetadataWidget* KDevSvnPlugin::createImportMetadataWidget(QWidget* parent) { return new SvnImportMetadataWidget(parent); } void KDevSvnPlugin::ctxImport() { QList const & ctxUrlList = m_common->contextUrlList(); if (ctxUrlList.count() != 1) { KMessageBox::error(nullptr, i18n("Please select only one item for this operation")); return; } QDialog dlg; dlg.setWindowTitle(i18n("Import into Subversion repository")); SvnImportMetadataWidget* widget = new SvnImportMetadataWidget(&dlg); widget->setSourceLocation(KDevelop::VcsLocation(ctxUrlList.first())); widget->setSourceLocationEditable(false); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); auto layout = new QVBoxLayout(); dlg.setLayout(layout); layout->addWidget(widget); layout->addWidget(buttonBox); connect(buttonBox, &QDialogButtonBox::accepted, &dlg, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, &dlg, &QDialog::reject); if (dlg.exec() == QDialog::Accepted) { KDevelop::ICore::self()->runController()->registerJob(import(widget->message(), widget->source(), widget->destination())); } } void KDevSvnPlugin::ctxCheckout() { QList const & ctxUrlList = m_common->contextUrlList(); if (ctxUrlList.count() != 1) { KMessageBox::error(nullptr, i18n("Please select only one item for this operation")); return; } QDialog dlg; dlg.setWindowTitle(i18n("Checkout from Subversion repository")); SvnCheckoutMetadataWidget* widget = new SvnCheckoutMetadataWidget(&dlg); QUrl tmp = KIO::upUrl(ctxUrlList.first()); widget->setDestinationLocation(tmp); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); auto layout = new QVBoxLayout(); dlg.setLayout(layout); layout->addWidget(widget); layout->addWidget(buttonBox); connect(buttonBox, &QDialogButtonBox::accepted, &dlg, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, &dlg, &QDialog::reject); if (dlg.exec() == QDialog::Accepted) { KDevelop::ICore::self()->runController()->registerJob(createWorkingCopy(widget->source(), widget->destination(), widget->recursionMode())); } } KDevelop::VcsLocationWidget* KDevSvnPlugin::vcsLocation(QWidget* parent) const { return new SvnLocationWidget(parent); } ThreadWeaver::Queue* KDevSvnPlugin::jobQueue() const { return m_jobQueue; } #include "kdevsvnplugin.moc" diff --git a/plugins/subversion/kdevsvnplugin.h b/plugins/subversion/kdevsvnplugin.h index 6f92acea9b..77e3f2ecff 100644 --- a/plugins/subversion/kdevsvnplugin.h +++ b/plugins/subversion/kdevsvnplugin.h @@ -1,159 +1,160 @@ /*************************************************************************** * Copyright 2007 Dukju Ahn * * Copyright 2008 Andreas Pakulat * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef KDEVPLATFORM_PLUGIN_KDEVSVNPLUGIN_H #define KDEVPLATFORM_PLUGIN_KDEVSVNPLUGIN_H #include #include #include #include class QMenu; class QUrl; class SvnStatusHolder; class QTemporaryDir; class SvnCommitDialog; namespace ThreadWeaver { class Queue; } namespace KDevelop { class VcsCommitDialog; class ContextMenuExtension; class VcsPluginHelper; } class KDevSvnPlugin: public KDevelop::IPlugin, public KDevelop::ICentralizedVersionControl { Q_OBJECT Q_INTERFACES(KDevelop::IBasicVersionControl KDevelop::ICentralizedVersionControl) public: explicit KDevSvnPlugin(QObject *parent, const QVariantList & = QVariantList()); virtual ~KDevSvnPlugin(); QString name() const override; KDevelop::VcsImportMetadataWidget* createImportMetadataWidget(QWidget* parent) override; // Begin: KDevelop::IBasicVersionControl + bool isValidRemoteRepositoryUrl(const QUrl& remoteLocation) override; bool isVersionControlled(const QUrl &localLocation) override; KDevelop::VcsJob* repositoryLocation(const QUrl &localLocation) override; KDevelop::VcsJob* status(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion = KDevelop::IBasicVersionControl::Recursive) override; KDevelop::VcsJob* add(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion = KDevelop::IBasicVersionControl::Recursive) override; KDevelop::VcsJob* remove(const QList& localLocations) override; KDevelop::VcsJob* copy(const QUrl &localLocationSrc, const QUrl &localLocationDstn) override; KDevelop::VcsJob* move(const QUrl &localLocationSrc, const QUrl &localLocationDst) override; KDevelop::VcsJob* revert(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion = KDevelop::IBasicVersionControl::Recursive) override; KDevelop::VcsJob* update(const QList& localLocations, const KDevelop::VcsRevision& rev, KDevelop::IBasicVersionControl::RecursionMode recursion = KDevelop::IBasicVersionControl::Recursive) override; KDevelop::VcsJob* commit(const QString& message, const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion = KDevelop::IBasicVersionControl::Recursive) override; KDevelop::VcsJob* diff(const QUrl &fileOrDirectory, const KDevelop::VcsRevision& srcRevision, const KDevelop::VcsRevision& dstRevision, KDevelop::VcsDiff::Type = KDevelop::VcsDiff::DiffUnified, KDevelop::IBasicVersionControl::RecursionMode recursion = KDevelop::IBasicVersionControl::Recursive) override; /** * Retrieves a diff between the two locations at the given revisions * * The diff is in unified diff format for text files by default */ KDevelop::VcsJob* diff2(const KDevelop::VcsLocation& localOrRepoLocationSrc, const KDevelop::VcsLocation& localOrRepoLocationDst, const KDevelop::VcsRevision& srcRevision, const KDevelop::VcsRevision& dstRevision, KDevelop::VcsDiff::Type = KDevelop::VcsDiff::DiffDontCare, KDevelop::IBasicVersionControl::RecursionMode = KDevelop::IBasicVersionControl::Recursive); KDevelop::VcsJob* log(const QUrl &localLocation, const KDevelop::VcsRevision& rev, unsigned long limit) override; KDevelop::VcsJob* log(const QUrl &localLocation, const KDevelop::VcsRevision& rev, const KDevelop::VcsRevision& limit) override; KDevelop::VcsJob* annotate(const QUrl &localLocation, const KDevelop::VcsRevision& rev) override; KDevelop::VcsJob* merge(const KDevelop::VcsLocation& localOrRepoLocationSrc, const KDevelop::VcsLocation& localOrRepoLocationDst, const KDevelop::VcsRevision& srcRevision, const KDevelop::VcsRevision& dstRevision, const QUrl &localLocation); KDevelop::VcsJob* resolve(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) override; KDevelop::VcsLocationWidget* vcsLocation(QWidget* parent) const override; // End: KDevelop::IBasicVersionControl // Begin: KDevelop::ICentralizedVersionControl KDevelop::VcsJob* import(const QString & commitMessage, const QUrl &sourceDirectory, const KDevelop::VcsLocation & destinationRepository) override; KDevelop::VcsJob* createWorkingCopy(const KDevelop::VcsLocation & sourceRepository, const QUrl &destinationDirectory, KDevelop::IBasicVersionControl::RecursionMode recursion = KDevelop::IBasicVersionControl::Recursive) override; KDevelop::VcsJob* edit(const QUrl &localLocation) override; KDevelop::VcsJob* unedit(const QUrl &localLocation) override; KDevelop::VcsJob* localRevision(const QUrl &localLocation, KDevelop::VcsRevision::RevisionType) override; // End: KDevelop::ICentralizedVersionControl KDevelop::ContextMenuExtension contextMenuExtension(KDevelop::Context*) override; ThreadWeaver::Queue* jobQueue() const; public Q_SLOTS: // invoked by context-menu void ctxInfo(); void ctxStatus(); void ctxCopy(); void ctxMove(); void ctxCat(); void ctxImport(); void ctxCheckout(); private: QScopedPointer m_common; QAction* copy_action; QAction* move_action; ThreadWeaver::Queue* m_jobQueue; }; #endif diff --git a/shell/mainwindow.cpp b/shell/mainwindow.cpp index 016fc573d0..95d7ff7f9f 100644 --- a/shell/mainwindow.cpp +++ b/shell/mainwindow.cpp @@ -1,473 +1,513 @@ /* This file is part of the KDevelop project Copyright 2002 Falk Brettschneider Copyright 2003 John Firebaugh Copyright 2006 Adam Treat Copyright 2006, 2007 Alexander Dymo This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "mainwindow.h" #include "mainwindow_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "shellextension.h" #include "partcontroller.h" #include "plugincontroller.h" #include "projectcontroller.h" #include "uicontroller.h" #include "documentcontroller.h" #include "debugcontroller.h" #include "workingsetcontroller.h" #include "sessioncontroller.h" #include "sourceformattercontroller.h" #include "areadisplay.h" #include "project.h" #include "debug.h" #include "uiconfig.h" #include #include +#include #include #include #include #include #include #include using namespace KDevelop; namespace { QColor defaultColor(const QPalette& palette) { return palette.foreground().color(); } QColor colorForDocument(const QUrl& url, const QPalette& palette, const QColor& defaultColor) { auto project = Core::self()->projectController()->findProjectForUrl(url); if (!project) return defaultColor; return WidgetColorizer::colorForId(qHash(project->path()), palette); } } void MainWindow::applyMainWindowSettings(const KConfigGroup& config) { if(!d->changingActiveView()) KXmlGuiWindow::applyMainWindowSettings(config); } void MainWindow::createGUI(KParts::Part* part) { //TODO remove if-clause once KF5 >= 5.24 is required #if KPARTS_VERSION_MINOR >= 24 Sublime::MainWindow::setWindowTitleHandling(false); Sublime::MainWindow::createGUI(part); #else Sublime::MainWindow::createGUI(part); if (part) { // Don't let the Part control the main window caption -- we take care of that disconnect(part, SIGNAL(setWindowCaption(QString)), this, SLOT(setCaption(QString))); } #endif } void MainWindow::initializeCorners() { const KConfigGroup cg = KSharedConfig::openConfig()->group( "UiSettings" ); const int bottomleft = cg.readEntry( "BottomLeftCornerOwner", 0 ); const int bottomright = cg.readEntry( "BottomRightCornerOwner", 0 ); qCDebug(SHELL) << "Bottom Left:" << bottomleft; qCDebug(SHELL) << "Bottom Right:" << bottomright; // 0 means vertical dock (left, right), 1 means horizontal dock( top, bottom ) if( bottomleft == 0 ) setCorner( Qt::BottomLeftCorner, Qt::LeftDockWidgetArea ); else if( bottomleft == 1 ) setCorner( Qt::BottomLeftCorner, Qt::BottomDockWidgetArea ); if( bottomright == 0 ) setCorner( Qt::BottomRightCorner, Qt::RightDockWidgetArea ); else if( bottomright == 1 ) setCorner( Qt::BottomRightCorner, Qt::BottomDockWidgetArea ); } MainWindow::MainWindow( Sublime::Controller *parent, Qt::WindowFlags flags ) : Sublime::MainWindow( parent, flags ) { QDBusConnection::sessionBus().registerObject( QStringLiteral("/kdevelop/MainWindow"), this, QDBusConnection::ExportScriptableSlots ); setAcceptDrops( true ); initializeCorners(); setObjectName( QStringLiteral("MainWindow") ); d = new MainWindowPrivate(this); setStandardToolBarMenuEnabled( true ); d->setupActions(); if( !ShellExtension::getInstance()->xmlFile().isEmpty() ) { setXMLFile( ShellExtension::getInstance() ->xmlFile() ); } menuBar()->setCornerWidget(new AreaDisplay(this), Qt::TopRightCorner); } MainWindow::~ MainWindow() { if (memberList().count() == 1) { // We're closing down... Core::self()->shutdown(); } delete d; } KTextEditorIntegration::MainWindow *MainWindow::kateWrapper() const { return d->kateWrapper(); } void MainWindow::ensureVisible() { if (isMinimized()) { if (isMaximized()) { showMaximized(); } else { showNormal(); } } KWindowSystem::forceActiveWindow(winId()); } QAction* MainWindow::createCustomElement(QWidget* parent, int index, const QDomElement& element) { QAction* before = nullptr; if (index > 0 && index < parent->actions().count()) before = parent->actions().at(index); //KDevelop needs to ensure that separators defined as //are always shown in the menubar. For those, we create special disabled actions //instead of calling QMenuBar::addSeparator() because menubar separators are ignored if (element.tagName().toLower() == QLatin1String("separator") && element.attribute(QStringLiteral("style")) == QLatin1String("visible")) { if ( QMenuBar* bar = qobject_cast( parent ) ) { QAction *separatorAction = new QAction(QStringLiteral("|"), this); bar->insertAction( before, separatorAction ); separatorAction->setDisabled(true); return separatorAction; } } return KXMLGUIBuilder::createCustomElement(parent, index, element); } void MainWindow::dragEnterEvent( QDragEnterEvent* ev ) { - if( ev->mimeData()->hasFormat( QStringLiteral("text/uri-list") ) && ev->mimeData()->hasUrls() ) - { + const QMimeData* mimeData = ev->mimeData(); + if (mimeData->hasUrls()) { ev->acceptProposedAction(); + } else if (mimeData->hasText()) { + // also take text which contains a URL + const QUrl url = QUrl::fromUserInput(mimeData->text()); + if (url.isValid()) { + ev->acceptProposedAction(); + } } } void MainWindow::dropEvent( QDropEvent* ev ) { Sublime::View* dropToView = viewForPosition(mapToGlobal(ev->pos())); if(dropToView) activateView(dropToView); - foreach( const QUrl& u, ev->mimeData()->urls() ) - { - Core::self()->documentController()->openDocument( u ); + QList urls; + + const QMimeData* mimeData = ev->mimeData(); + if (mimeData->hasUrls()) { + urls = mimeData->urls(); + } else if (mimeData->hasText()) { + const QUrl url = QUrl::fromUserInput(mimeData->text()); + if (url.isValid()) { + urls << url; + } } + + bool eventUsed = false; + if (urls.size() == 1) { + const QUrl& url = urls.at(0); + // TODO: query also projectprovider plugins, and that before plain vcs plugins + // e.g. KDE provider plugin could catch URLs from mirror or pickup kde:repo things + + auto* pluginController = Core::self()->pluginController(); + const auto& plugins = pluginController->allPluginsForExtension(QStringLiteral("org.kdevelop.IBasicVersionControl")); + + for (auto* plugin : plugins) { + auto* iface = plugin->extension(); + if (iface->isValidRemoteRepositoryUrl(url)) { + Core::self()->projectControllerInternal()->fetchProjectFromUrl(url, plugin); + eventUsed = true; + break; + } + } + } + + if (!eventUsed) { + for(const auto& url : urls) { + Core::self()->documentController()->openDocument(url); + } + } + ev->acceptProposedAction(); } void MainWindow::loadSettings() { qCDebug(SHELL) << "Loading Settings"; initializeCorners(); updateAllTabColors(); Sublime::MainWindow::loadSettings(); } void MainWindow::configureShortcuts() { ///Workaround for a problem with the actions: Always start the shortcut-configuration in the first mainwindow, then propagate the updated ///settings into the other windows // We need to bring up the shortcut dialog ourself instead of // Core::self()->uiControllerInternal()->mainWindows()[0]->guiFactory()->configureShortcuts(); // so we can connect to the saved() signal to propagate changes in the editor shortcuts KShortcutsDialog dlg(KShortcutsEditor::AllActions, KShortcutsEditor::LetterShortcutsAllowed, this); foreach (KXMLGUIClient *client, Core::self()->uiControllerInternal()->mainWindows()[0]->guiFactory()->clients()) { if(client && !client->xmlFile().isEmpty()) dlg.addCollection( client->actionCollection() ); } connect(&dlg, &KShortcutsDialog::saved, this, &MainWindow::shortcutsChanged); dlg.configure(true); QMap shortcuts; foreach(KXMLGUIClient* client, Core::self()->uiControllerInternal()->mainWindows()[0]->guiFactory()->clients()) { foreach(QAction* action, client->actionCollection()->actions()) { if(!action->objectName().isEmpty()) { shortcuts[action->objectName()] = action->shortcut(); } } } for(int a = 1; a < Core::self()->uiControllerInternal()->mainWindows().size(); ++a) { foreach(KXMLGUIClient* client, Core::self()->uiControllerInternal()->mainWindows()[a]->guiFactory()->clients()) { foreach(QAction* action, client->actionCollection()->actions()) { qCDebug(SHELL) << "transferring setting shortcut for" << action->objectName(); if(shortcuts.contains(action->objectName())) { action->setShortcut(shortcuts[action->objectName()]); } } } } } void MainWindow::shortcutsChanged() { KTextEditor::View *activeClient = Core::self()->documentController()->activeTextDocumentView(); if(!activeClient) return; foreach(IDocument * doc, Core::self()->documentController()->openDocuments()) { KTextEditor::Document *textDocument = doc->textDocument(); if (textDocument) { foreach(KTextEditor::View *client, textDocument->views()) { if (client != activeClient) { client->reloadXML(); } } } } } void MainWindow::initialize() { KStandardAction::keyBindings(this, SLOT(configureShortcuts()), actionCollection()); setupGUI( KXmlGuiWindow::ToolBar | KXmlGuiWindow::Create | KXmlGuiWindow::Save ); Core::self()->partController()->addManagedTopLevelWidget(this); qCDebug(SHELL) << "Adding plugin-added connection"; connect( Core::self()->pluginController(), &IPluginController::pluginLoaded, d, &MainWindowPrivate::addPlugin); connect( Core::self()->pluginController(), &IPluginController::pluginUnloaded, d, &MainWindowPrivate::removePlugin); connect( Core::self()->partController(), &IPartController::activePartChanged, d, &MainWindowPrivate::activePartChanged); connect( this, &MainWindow::activeViewChanged, d, &MainWindowPrivate::changeActiveView); foreach(IPlugin* plugin, Core::self()->pluginController()->loadedPlugins()) d->addPlugin(plugin); guiFactory()->addClient(Core::self()->sessionController()); guiFactory()->addClient(Core::self()->sourceFormatterControllerInternal()); // Needed to re-plug the actions from the sessioncontroller as xmlguiclients don't // seem to remember which actions where plugged in. Core::self()->sessionController()->updateXmlGuiActionList(); d->setupGui(); //Queued so we process it with some delay, to make sure the rest of the UI has already adapted connect(Core::self()->documentController(), &IDocumentController::documentActivated, this, &MainWindow::updateCaption, Qt::QueuedConnection); connect(Core::self()->documentController(), &IDocumentController::documentActivated, this, &MainWindow::updateActiveDocumentConnection, Qt::QueuedConnection); connect(Core::self()->documentController(), &IDocumentController::documentClosed, this, &MainWindow::updateCaption, Qt::QueuedConnection); connect(Core::self()->documentController(), &IDocumentController::documentUrlChanged, this, &MainWindow::updateCaption, Qt::QueuedConnection); connect(Core::self()->sessionController()->activeSession(), &ISession::sessionUpdated, this, &MainWindow::updateCaption); connect(Core::self()->documentController(), &IDocumentController::documentOpened, this, &MainWindow::updateTabColor); connect(Core::self()->documentController(), &IDocumentController::documentUrlChanged, this, &MainWindow::updateTabColor); connect(this, &Sublime::MainWindow::viewAdded, this, &MainWindow::updateAllTabColors); connect(Core::self()->projectController(), &ProjectController::projectOpened, this, &MainWindow::updateAllTabColors, Qt::QueuedConnection); updateCaption(); } void MainWindow::cleanup() { } void MainWindow::setVisible( bool visible ) { KXmlGuiWindow::setVisible( visible ); emit finishedLoading(); } bool MainWindow::queryClose() { if (!Core::self()->documentControllerInternal()->saveAllDocumentsForWindow(this, IDocument::Default)) return false; return Sublime::MainWindow::queryClose(); } void MainWindow::updateActiveDocumentConnection(IDocument* document) { disconnect(d->activeDocumentReadWriteConnection); if (auto textDocument = document->textDocument()) { d->activeDocumentReadWriteConnection = connect(textDocument, &KTextEditor::Document::readWriteChanged, this, &MainWindow::updateCaption); } } void MainWindow::updateCaption() { const auto activeSession = Core::self()->sessionController()->activeSession(); QString title = activeSession ? activeSession->description() : QString(); if(area()->activeView()) { if(!title.isEmpty()) title += QLatin1String(" - [ "); Sublime::Document* doc = area()->activeView()->document(); Sublime::UrlDocument* urlDoc = dynamic_cast(doc); if(urlDoc) title += Core::self()->projectController()->prettyFileName(urlDoc->url(), KDevelop::IProjectController::FormatPlain); else title += doc->title(); auto activeDocument = Core::self()->documentController()->activeDocument(); if (activeDocument && activeDocument->textDocument() && !activeDocument->textDocument()->isReadWrite()) title += i18n(" (read only)"); title += QLatin1String(" ]"); } setCaption(title); } void MainWindow::updateAllTabColors() { auto documentController = Core::self()->documentController(); if (!documentController) return; const auto defaultColor = ::defaultColor(palette()); if (UiConfig::colorizeByProject()) { QHash viewColors; foreach (auto container, containers()) { auto views = container->views(); viewColors.reserve(views.size()); viewColors.clear(); foreach (auto view, views) { const auto urlDoc = qobject_cast(view->document()); if (urlDoc) { viewColors[view] = colorForDocument(urlDoc->url(), palette(), defaultColor); } } container->setTabColors(viewColors); } } else { foreach (auto container, containers()) { container->resetTabColors(defaultColor); } } } void MainWindow::updateTabColor(IDocument* doc) { if (!UiConfig::self()->colorizeByProject()) return; const auto color = colorForDocument(doc->url(), palette(), defaultColor(palette())); foreach (auto container, containers()) { foreach (auto view, container->views()) { const auto urlDoc = qobject_cast(view->document()); if (urlDoc && urlDoc->url() == doc->url()) { container->setTabColor(view, color); } } } } void MainWindow::registerStatus(QObject* status) { d->registerStatus(status); } void MainWindow::initializeStatusBar() { d->setupStatusBar(); } void MainWindow::showErrorMessage(const QString& message, int timeout) { d->showErrorMessage(message, timeout); } void MainWindow::tabContextMenuRequested(Sublime::View* view, QMenu* menu) { Sublime::MainWindow::tabContextMenuRequested(view, menu); d->tabContextMenuRequested(view, menu); } void MainWindow::tabToolTipRequested(Sublime::View* view, Sublime::Container* container, int tab) { d->tabToolTipRequested(view, container, tab); } void MainWindow::dockBarContextMenuRequested(Qt::DockWidgetArea area, const QPoint& position) { d->dockBarContextMenuRequested(area, position); } void MainWindow::newTabRequested() { Sublime::MainWindow::newTabRequested(); d->fileNew(); } diff --git a/shell/openprojectdialog.cpp b/shell/openprojectdialog.cpp index cd5c5c70e3..431631f25c 100644 --- a/shell/openprojectdialog.cpp +++ b/shell/openprojectdialog.cpp @@ -1,360 +1,362 @@ /*************************************************************************** * Copyright (C) 2008 by Andreas Pakulat #include #include #include #include #include #include #include #include #include "core.h" #include "uicontroller.h" #include "plugincontroller.h" #include "mainwindow.h" #include "shellextension.h" #include "projectsourcepage.h" #include namespace { struct URLInfo { bool isValid; bool isDir; QString extension; }; URLInfo getUrlInfo(const QUrl& url) { URLInfo ret; ret.isValid = false; if (url.isLocalFile()) { QFileInfo info(url.toLocalFile()); ret.isValid = info.exists(); if (ret.isValid) { ret.isDir = info.isDir(); ret.extension = info.suffix(); } } else if (url.isValid()) { KIO::StatJob* statJob = KIO::stat(url, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, KDevelop::Core::self()->uiControllerInternal()->defaultMainWindow()); ret.isValid = statJob->exec(); // TODO: do this asynchronously so that the user isn't blocked while typing every letter of the hostname in sftp://hostname if (ret.isValid) { KIO::UDSEntry entry = statJob->statResult(); ret.isDir = entry.isDir(); ret.extension = QFileInfo(entry.stringValue(KIO::UDSEntry::UDS_NAME)).suffix(); } } return ret; } } namespace KDevelop { -OpenProjectDialog::OpenProjectDialog( bool fetch, const QUrl& startUrl, QWidget* parent ) +OpenProjectDialog::OpenProjectDialog(bool fetch, const QUrl& startUrl, + const QUrl& repoUrl, IPlugin* vcsOrProviderPlugin, + QWidget* parent) : KAssistantDialog( parent ) , m_urlIsDirectory(false) , sourcePage(nullptr) , openPage(nullptr) , projectInfoPage(nullptr) { resize(QSize(700, 500)); // KAssistantDialog creates a help button by default, no option to prevent that auto helpButton = button(QDialogButtonBox::Help); if (helpButton) { buttonBox()->removeButton(helpButton); delete helpButton; } const bool useKdeFileDialog = qEnvironmentVariableIsSet("KDE_FULL_SESSION"); QStringList filters, allEntry; QString filterFormat = useKdeFileDialog ? QStringLiteral("%1|%2 (%1)") : QStringLiteral("%2 (%1)"); allEntry << "*." + ShellExtension::getInstance()->projectFileExtension(); filters << filterFormat.arg("*." + ShellExtension::getInstance()->projectFileExtension(), ShellExtension::getInstance()->projectFileDescription()); QVector plugins = ICore::self()->pluginController()->queryExtensionPlugins(QStringLiteral("org.kdevelop.IProjectFileManager")); foreach(const KPluginMetaData& info, plugins) { QStringList filter = KPluginMetaData::readStringList(info.rawData(), QStringLiteral("X-KDevelop-ProjectFilesFilter")); QString desc = info.value(QStringLiteral("X-KDevelop-ProjectFilesFilterDescription")); m_projectFilters.insert(info.name(), filter); m_projectPlugins.insert(info.name(), info); allEntry += filter; filters << filterFormat.arg(filter.join(QStringLiteral(" ")), desc); } if (useKdeFileDialog) filters.prepend(i18n("%1|All Project Files (%1)", allEntry.join(QStringLiteral(" ")))); QUrl start = startUrl.isValid() ? startUrl : Core::self()->projectController()->projectsBaseDirectory(); start = start.adjusted(QUrl::NormalizePathSegments); KPageWidgetItem* currentPage = nullptr; if( fetch ) { - sourcePageWidget = new ProjectSourcePage( start, this ); + sourcePageWidget = new ProjectSourcePage(start, repoUrl, vcsOrProviderPlugin, this); connect( sourcePageWidget, &ProjectSourcePage::isCorrect, this, &OpenProjectDialog::validateSourcePage ); sourcePage = addPage( sourcePageWidget, i18n("Select Source") ); currentPage = sourcePage; } if (useKdeFileDialog) { openPageWidget = new OpenProjectPage( start, filters, this ); connect( openPageWidget, &OpenProjectPage::urlSelected, this, &OpenProjectDialog::validateOpenUrl ); connect( openPageWidget, &OpenProjectPage::accepted, this, &OpenProjectDialog::openPageAccepted ); openPage = addPage( openPageWidget, i18n("Select a build system setup file, existing KDevelop project, " "or any folder to open as a project") ); if (!currentPage) { currentPage = openPage; } } else { nativeDialog = new QFileDialog(this, i18n("Open Project")); nativeDialog->setDirectoryUrl(start); nativeDialog->setFileMode(QFileDialog::Directory); } ProjectInfoPage* page = new ProjectInfoPage( this ); connect( page, &ProjectInfoPage::projectNameChanged, this, &OpenProjectDialog::validateProjectName ); connect( page, &ProjectInfoPage::projectManagerChanged, this, &OpenProjectDialog::validateProjectManager ); projectInfoPage = addPage( page, i18n("Project Information") ); if (!currentPage) { currentPage = projectInfoPage; } setValid( sourcePage, false ); setValid( openPage, false ); setValid( projectInfoPage, false); setAppropriate( projectInfoPage, false ); setCurrentPage( currentPage ); setWindowTitle(i18n("Open Project")); } bool OpenProjectDialog::execNativeDialog() { while (true) { if (nativeDialog->exec()) { QUrl selectedUrl = nativeDialog->selectedUrls().at(0); if (getUrlInfo(selectedUrl).isValid) { // validate directory first to populate m_projectName and m_projectManager validateOpenUrl(selectedUrl.adjusted(QUrl::RemoveFilename)); validateOpenUrl(selectedUrl); return true; } } else { return false; } } } int OpenProjectDialog::exec() { if (nativeDialog && !execNativeDialog()) { reject(); return QDialog::Rejected; } return KAssistantDialog::exec(); } void OpenProjectDialog::validateSourcePage(bool valid) { setValid(sourcePage, valid); if (!nativeDialog) { openPageWidget->setUrl(sourcePageWidget->workingDir()); } } void OpenProjectDialog::validateOpenUrl( const QUrl& url_ ) { URLInfo urlInfo = getUrlInfo(url_); const QUrl url = url_.adjusted(QUrl::StripTrailingSlash); // openPage is used only in KDE if (openPage) { if ( urlInfo.isValid ) { // reset header openPage->setHeader(i18n("Open \"%1\" as project", url.fileName())); } else { // report error KColorScheme scheme(palette().currentColorGroup()); const QString errorMsg = i18n("Selected URL is invalid"); openPage->setHeader(QStringLiteral("%2") .arg(scheme.foreground(KColorScheme::NegativeText).color().name(), errorMsg) ); setAppropriate( projectInfoPage, false ); setAppropriate( openPage, true ); setValid( openPage, false ); return; } } m_selected = url; if( urlInfo.isDir || urlInfo.extension != ShellExtension::getInstance()->projectFileExtension() ) { m_urlIsDirectory = urlInfo.isDir; setAppropriate( projectInfoPage, true ); m_url = url; if( !urlInfo.isDir ) { m_url = m_url.adjusted(QUrl::StripTrailingSlash | QUrl::RemoveFilename); } ProjectInfoPage* page = qobject_cast( projectInfoPage->widget() ); if( page ) { page->setProjectName( m_url.fileName() ); // clear the filelist m_fileList.clear(); if( urlInfo.isDir ) { // If a dir was selected fetch all files in it KIO::ListJob* job = KIO::listDir( m_url ); connect( job, &KIO::ListJob::entries, this, &OpenProjectDialog::storeFileList); KJobWidgets::setWindow(job, Core::self()->uiController()->activeMainWindow()); job->exec(); } else { // Else we'll just take the given file m_fileList << url.fileName(); } // Now find a manager for the file(s) in our filelist. QVector choices; Q_FOREACH ( const auto& file, m_fileList ) { auto plugins = projectManagerForFile(file); if ( plugins.contains("") ) { plugins.removeAll(""); choices.append({i18n("Open existing file \"%1\"", file), "", QString()}); } Q_FOREACH ( const auto& plugin, plugins ) { auto meta = m_projectPlugins.value(plugin); choices.append({file + QString(" (%1)").arg(plugin), meta.pluginId(), meta.iconName(), file}); } } Q_FOREACH ( const auto& plugin, m_projectFilters.keys() ) { qDebug() << plugin << m_projectFilters.value(plugin); if ( m_projectFilters.value(plugin).isEmpty() ) { // that works in any case auto meta = m_projectPlugins.value(plugin); choices.append({plugin, meta.pluginId(), meta.iconName()}); } } page->populateProjectFileCombo(choices); } m_url.setPath( m_url.path() + '/' + m_url.fileName() + '.' + ShellExtension::getInstance()->projectFileExtension() ); } else { setAppropriate( projectInfoPage, false ); m_url = url; m_urlIsDirectory = false; } validateProjectInfo(); setValid( openPage, true ); } QStringList OpenProjectDialog::projectManagerForFile(const QString& file) const { QStringList ret; foreach( const QString& manager, m_projectFilters.keys() ) { foreach( const QString& filterexp, m_projectFilters.value(manager) ) { QRegExp exp( filterexp, Qt::CaseSensitive, QRegExp::Wildcard ); if ( exp.exactMatch(file) ) { ret.append(manager); } } } if ( file.endsWith(ShellExtension::getInstance()->projectFileExtension()) ) { ret.append(""); } return ret; } void OpenProjectDialog::openPageAccepted() { if ( isValid( openPage ) ) { next(); } } void OpenProjectDialog::validateProjectName( const QString& name ) { m_projectName = name; validateProjectInfo(); } void OpenProjectDialog::validateProjectInfo() { setValid( projectInfoPage, (!projectName().isEmpty() && !projectManager().isEmpty()) ); } void OpenProjectDialog::validateProjectManager( const QString& manager, const QString & fileName ) { m_projectManager = manager; if ( m_urlIsDirectory ) { m_selected = m_url.resolved( QUrl("./" + fileName) ); } validateProjectInfo(); } QUrl OpenProjectDialog::projectFileUrl() const { return m_url; } QUrl OpenProjectDialog::selectedUrl() const { return m_selected; } QString OpenProjectDialog::projectName() const { return m_projectName; } QString OpenProjectDialog::projectManager() const { return m_projectManager; } void OpenProjectDialog::storeFileList(KIO::Job*, const KIO::UDSEntryList& list) { foreach( const KIO::UDSEntry& entry, list ) { QString name = entry.stringValue( KIO::UDSEntry::UDS_NAME ); if( name != QLatin1String(".") && name != QLatin1String("..") && !entry.isDir() ) { m_fileList << name; } } } } diff --git a/shell/openprojectdialog.h b/shell/openprojectdialog.h index 4feb69d27a..ffca733e9b 100644 --- a/shell/openprojectdialog.h +++ b/shell/openprojectdialog.h @@ -1,87 +1,90 @@ /*************************************************************************** * Copyright (C) 2008 by Andreas Pakulat #include #include #include class KPageWidgetItem; class QFileDialog; namespace KIO { class Job; } namespace KDevelop { class ProjectSourcePage; class OpenProjectPage; +class IPlugin; class OpenProjectDialog : public KAssistantDialog { Q_OBJECT public: - OpenProjectDialog( bool fetch, const QUrl& startUrl, QWidget* parent = nullptr ); + OpenProjectDialog(bool fetch, const QUrl& startUrl, + const QUrl& repoUrl = QUrl(), IPlugin* vcsOrProviderPlugin = nullptr, + QWidget* parent = nullptr); /** * Return a QUrl pointing to the project's .kdev file. */ QUrl projectFileUrl() const; /** * Return a QUrl pointing to the file, that was selected by the user. * Unlike projectFileUrl(), this can be a .kdev file, as well * as build system file (e.g. CMakeLists.txt). */ QUrl selectedUrl() const; QString projectName() const; QString projectManager() const; int exec() override; QStringList projectManagerForFile(const QString& file) const; private slots: void validateSourcePage( bool ); void validateOpenUrl( const QUrl& ); void validateProjectName( const QString& ); void validateProjectManager( const QString&, const QString & ); void storeFileList(KIO::Job*, const KIO::UDSEntryList&); void openPageAccepted(); private: bool execNativeDialog(); void validateProjectInfo(); QUrl m_url; QUrl m_selected; QString m_projectName; QString m_projectManager; bool m_urlIsDirectory; /// Used to select files when we aren't in KDE QFileDialog* nativeDialog = nullptr; KPageWidgetItem* sourcePage; KPageWidgetItem* openPage; KPageWidgetItem* projectInfoPage; QStringList m_fileList; QMap m_projectFilters; QMap m_projectPlugins; KDevelop::OpenProjectPage* openPageWidget = nullptr; KDevelop::ProjectSourcePage* sourcePageWidget = nullptr; }; } #endif diff --git a/shell/projectcontroller.cpp b/shell/projectcontroller.cpp index 4f3182b58f..e3e63929b4 100644 --- a/shell/projectcontroller.cpp +++ b/shell/projectcontroller.cpp @@ -1,1222 +1,1232 @@ /* This file is part of KDevelop Copyright 2006 Adam Treat Copyright 2007 Andreas Pakulat This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "projectcontroller.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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "core.h" // TODO: Should get rid off this include (should depend on IProject only) #include "project.h" #include "mainwindow.h" #include "shellextension.h" #include "plugincontroller.h" #include "configdialog.h" #include "uicontroller.h" #include "documentcontroller.h" #include "openprojectdialog.h" #include "sessioncontroller.h" #include "session.h" #include "debug.h" namespace KDevelop { class ProjectControllerPrivate { public: QList m_projects; QMap< IProject*, QList > m_projectPlugins; QPointer m_recentAction; Core* m_core; // IProject* m_currentProject; ProjectModel* model; QPointer m_openProject; QPointer m_fetchProject; QPointer m_closeProject; QPointer m_openConfig; IProjectDialogProvider* dialog; QList m_currentlyOpening; // project-file urls that are being opened ProjectController* q; ProjectBuildSetModel* buildset; bool m_foundProjectFile; //Temporary flag used while searching the hierarchy for a project file bool m_cleaningUp; //Temporary flag enabled while destroying the project-controller QPointer m_changesModel; QHash< IProject*, QPointer > m_parseJobs; // parse jobs that add files from the project to the background parser. explicit ProjectControllerPrivate( ProjectController* p ) : m_core(nullptr), model(nullptr), dialog(nullptr), q(p), buildset(nullptr), m_foundProjectFile(false), m_cleaningUp(false) { } void unloadAllProjectPlugins() { if( m_projects.isEmpty() ) m_core->pluginControllerInternal()->unloadProjectPlugins(); } void projectConfig( QObject * obj ) { if( !obj ) return; Project* proj = qobject_cast(obj); if( !proj ) return; QVector configPages; auto mainWindow = m_core->uiController()->activeMainWindow(); ProjectConfigOptions options; options.developerFile = proj->developerFile(); options.developerTempFile = proj->developerTempFile(); options.projectTempFile = proj->projectTempFile(); options.project = proj; foreach (IPlugin* plugin, findPluginsForProject(proj)) { for (int i = 0; i < plugin->perProjectConfigPages(); ++i) { configPages.append(plugin->perProjectConfigPage(i, options, mainWindow)); } } std::sort(configPages.begin(), configPages.end(), [](const ConfigPage* a, const ConfigPage* b) { return a->name() < b->name(); }); auto cfgDlg = new KDevelop::ConfigDialog(configPages, mainWindow); cfgDlg->setAttribute(Qt::WA_DeleteOnClose); cfgDlg->setModal(true); QObject::connect(cfgDlg, &ConfigDialog::configSaved, cfgDlg, [this, proj](ConfigPage* page) { Q_UNUSED(page) Q_ASSERT_X(proj, Q_FUNC_INFO, "ConfigDialog signalled project config change, but no project set for configuring!"); emit q->projectConfigurationChanged(proj); }); cfgDlg->setWindowTitle(i18n("Configure Project %1", proj->name())); QObject::connect(cfgDlg, &KDevelop::ConfigDialog::finished, [this, proj]() { proj->projectConfiguration()->sync(); }); cfgDlg->show(); } void saveListOfOpenedProjects() { auto activeSession = Core::self()->activeSession(); if (!activeSession) { return; } QList openProjects; openProjects.reserve( m_projects.size() ); foreach( IProject* project, m_projects ) { openProjects.append(project->projectFile().toUrl()); } activeSession->setContainedProjects( openProjects ); } // Recursively collects builder dependencies for a project. static void collectBuilders( QList< IProjectBuilder* >& destination, IProjectBuilder* topBuilder, IProject* project ) { QList< IProjectBuilder* > auxBuilders = topBuilder->additionalBuilderPlugins( project ); destination.append( auxBuilders ); foreach( IProjectBuilder* auxBuilder, auxBuilders ) { collectBuilders( destination, auxBuilder, project ); } } QVector findPluginsForProject( IProject* project ) { QList plugins = m_core->pluginController()->loadedPlugins(); QVector projectPlugins; QList buildersForKcm; // Important to also include the "top" builder for the project, so // projects with only one such builder are kept working. Otherwise the project config // dialog is empty for such cases. if( IBuildSystemManager* buildSystemManager = project->buildSystemManager() ) { buildersForKcm << buildSystemManager->builder(); collectBuilders( buildersForKcm, buildSystemManager->builder(), project ); } foreach(auto plugin, plugins) { auto info = m_core->pluginController()->pluginInfo(plugin); IProjectFileManager* manager = plugin->extension(); if( manager && manager != project->projectFileManager() ) { // current plugin is a manager but does not apply to given project, skip continue; } IProjectBuilder* builder = plugin->extension(); if ( builder && !buildersForKcm.contains( builder ) ) { continue; } qCDebug(SHELL) << "Using plugin" << info.pluginId() << "for project" << project->name(); projectPlugins << plugin; } return projectPlugins; } void updateActionStates() { // if only one project loaded, this is always our target int itemCount = (m_projects.size() == 1) ? 1 : 0; if (itemCount == 0) { // otherwise base on selection ProjectItemContext* itemContext = dynamic_cast(ICore::self()->selectionController()->currentSelection()); if (itemContext) { itemCount = itemContext->items().count(); } } m_openConfig->setEnabled(itemCount == 1); m_closeProject->setEnabled(itemCount > 0); } void openProjectConfig() { // if only one project loaded, this is our target IProject *project = (m_projects.count() == 1) ? m_projects.at(0) : nullptr; // otherwise base on selection if (!project) { ProjectItemContext* ctx = dynamic_cast(ICore::self()->selectionController()->currentSelection()); if (ctx && ctx->items().count() == 1) { project = ctx->items().at(0)->project(); } } if (project) { q->configureProject(project); } } void closeSelectedProjects() { QSet projects; // if only one project loaded, this is our target if (m_projects.count() == 1) { projects.insert(m_projects.at(0)); } else { // otherwise base on selection ProjectItemContext* ctx = dynamic_cast(ICore::self()->selectionController()->currentSelection()); if (ctx) { foreach (ProjectBaseItem* item, ctx->items()) { projects.insert(item->project()); } } } foreach (IProject* project, projects) { q->closeProject(project); } } void importProject(const QUrl& url_) { QUrl url(url_); if (url.isLocalFile()) { const QString path = QFileInfo(url.toLocalFile()).canonicalFilePath(); if (!path.isEmpty()) { url = QUrl::fromLocalFile(path); } } if ( !url.isValid() ) { KMessageBox::error(Core::self()->uiControllerInternal()->activeMainWindow(), i18n("Invalid Location: %1", url.toDisplayString(QUrl::PreferLocalFile))); return; } if ( m_currentlyOpening.contains(url)) { qCDebug(SHELL) << "Already opening " << url << ". Aborting."; KPassivePopup::message( i18n( "Project already being opened"), i18n( "Already opening %1, not opening again", url.toDisplayString(QUrl::PreferLocalFile) ), m_core->uiController()->activeMainWindow() ); return; } foreach( IProject* project, m_projects ) { if ( url == project->projectFile().toUrl() ) { if ( dialog->userWantsReopen() ) { // close first, then open again by falling through q->closeProject(project); } else { // abort return; } } } m_currentlyOpening += url; m_core->pluginControllerInternal()->loadProjectPlugins(); Project* project = new Project(); QObject::connect(project, &Project::aboutToOpen, q, &ProjectController::projectAboutToBeOpened); if ( !project->open( Path(url) ) ) { m_currentlyOpening.removeAll(url); q->abortOpeningProject(project); project->deleteLater(); } } void areaChanged(Sublime::Area* area) { KActionCollection* ac = m_core->uiControllerInternal()->defaultMainWindow()->actionCollection(); ac->action(QStringLiteral("commit_current_project"))->setEnabled(area->objectName() == QLatin1String("code")); ac->action(QStringLiteral("commit_current_project"))->setVisible(area->objectName() == QLatin1String("code")); } }; IProjectDialogProvider::IProjectDialogProvider() {} IProjectDialogProvider::~IProjectDialogProvider() {} ProjectDialogProvider::ProjectDialogProvider(ProjectControllerPrivate* const p) : d(p) {} ProjectDialogProvider::~ProjectDialogProvider() {} bool writeNewProjectFile( const QString& localConfigFile, const QString& name, const QString& createdFrom, const QString& manager ) { KSharedConfigPtr cfg = KSharedConfig::openConfig( localConfigFile, KConfig::SimpleConfig ); if (!cfg->isConfigWritable(true)) { qCDebug(SHELL) << "can't write to configfile"; return false; } KConfigGroup grp = cfg->group( "Project" ); grp.writeEntry( "Name", name ); grp.writeEntry( "CreatedFrom", createdFrom ); grp.writeEntry( "Manager", manager ); cfg->sync(); return true; } bool writeProjectSettingsToConfigFile(const QUrl& projectFileUrl, OpenProjectDialog* dlg) { if ( !projectFileUrl.isLocalFile() ) { QTemporaryFile tmp; if ( !tmp.open() ) { return false; } if ( !writeNewProjectFile( tmp.fileName(), dlg->projectName(), dlg->selectedUrl().fileName(), dlg->projectManager() ) ) { return false; } // explicitly close file before uploading it, see also: https://bugs.kde.org/show_bug.cgi?id=254519 tmp.close(); auto uploadJob = KIO::file_copy(QUrl::fromLocalFile(tmp.fileName()), projectFileUrl); KJobWidgets::setWindow(uploadJob, Core::self()->uiControllerInternal()->defaultMainWindow()); return uploadJob->exec(); } // Here and above we take .filename() part of the selectedUrl() to make it relative to the project root, // thus keeping .kdev file relocatable return writeNewProjectFile( projectFileUrl.toLocalFile(), dlg->projectName(), dlg->selectedUrl().fileName(), dlg->projectManager() ); } bool projectFileExists( const QUrl& u ) { if( u.isLocalFile() ) { return QFileInfo::exists( u.toLocalFile() ); } else { auto statJob = KIO::stat(u, KIO::StatJob::DestinationSide, 0); KJobWidgets::setWindow(statJob, Core::self()->uiControllerInternal()->activeMainWindow()); return statJob->exec(); } } bool equalProjectFile( const QString& configPath, OpenProjectDialog* dlg ) { KSharedConfigPtr cfg = KSharedConfig::openConfig( configPath, KConfig::SimpleConfig ); KConfigGroup grp = cfg->group( "Project" ); QString defaultName = dlg->projectFileUrl().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).fileName(); return (grp.readEntry( "Name", QString() ) == dlg->projectName() || dlg->projectName() == defaultName) && grp.readEntry( "Manager", QString() ) == dlg->projectManager(); } -QUrl ProjectDialogProvider::askProjectConfigLocation(bool fetch, const QUrl& startUrl) +QUrl ProjectDialogProvider::askProjectConfigLocation(bool fetch, const QUrl& startUrl, + const QUrl& repoUrl, IPlugin* vcsOrProviderPlugin) { Q_ASSERT(d); - OpenProjectDialog dlg( fetch, startUrl, Core::self()->uiController()->activeMainWindow() ); + OpenProjectDialog dlg(fetch, startUrl, repoUrl, vcsOrProviderPlugin, Core::self()->uiController()->activeMainWindow()); if(dlg.exec() == QDialog::Rejected) return QUrl(); QUrl projectFileUrl = dlg.projectFileUrl(); qCDebug(SHELL) << "selected project:" << projectFileUrl << dlg.projectName() << dlg.projectManager(); if ( dlg.projectManager() == "" ) { return projectFileUrl; } // controls if existing project file should be saved bool writeProjectConfigToFile = true; if( projectFileExists( projectFileUrl ) ) { // check whether config is equal bool shouldAsk = true; if( projectFileUrl == dlg.selectedUrl() ) { if( projectFileUrl.isLocalFile() ) { shouldAsk = !equalProjectFile( projectFileUrl.toLocalFile(), &dlg ); } else { shouldAsk = false; QTemporaryFile tmpFile; if (tmpFile.open()) { auto downloadJob = KIO::file_copy(projectFileUrl, QUrl::fromLocalFile(tmpFile.fileName())); KJobWidgets::setWindow(downloadJob, qApp->activeWindow()); if (downloadJob->exec()) { shouldAsk = !equalProjectFile(tmpFile.fileName(), &dlg); } } } } if ( shouldAsk ) { KGuiItem yes = KStandardGuiItem::yes(); yes.setText(i18n("Override")); yes.setToolTip(i18nc("@info:tooltip", "Continue to open the project and use the just provided project configuration.")); yes.setIcon(QIcon()); KGuiItem no = KStandardGuiItem::no(); no.setText(i18n("Open Existing File")); no.setToolTip(i18nc("@info:tooltip", "Continue to open the project but use the existing project configuration.")); no.setIcon(QIcon()); KGuiItem cancel = KStandardGuiItem::cancel(); cancel.setToolTip(i18nc("@info:tooltip", "Cancel and do not open the project.")); int ret = KMessageBox::questionYesNoCancel(qApp->activeWindow(), i18n("There already exists a project configuration file at %1.\n" "Do you want to override it or open the existing file?", projectFileUrl.toDisplayString(QUrl::PreferLocalFile)), i18n("Override existing project configuration"), yes, no, cancel ); if ( ret == KMessageBox::No ) { writeProjectConfigToFile = false; } else if ( ret == KMessageBox::Cancel ) { return QUrl(); } // else fall through and write new file } else { writeProjectConfigToFile = false; } } if (writeProjectConfigToFile) { if (!writeProjectSettingsToConfigFile(projectFileUrl, &dlg)) { KMessageBox::error(d->m_core->uiControllerInternal()->defaultMainWindow(), i18n("Unable to create configuration file %1", projectFileUrl.url())); return QUrl(); } } return projectFileUrl; } bool ProjectDialogProvider::userWantsReopen() { Q_ASSERT(d); return (KMessageBox::questionYesNo( d->m_core->uiControllerInternal()->defaultMainWindow(), i18n( "Reopen the current project?" ) ) == KMessageBox::No) ? false : true; } void ProjectController::setDialogProvider(IProjectDialogProvider* dialog) { Q_ASSERT(d->dialog); delete d->dialog; d->dialog = dialog; } ProjectController::ProjectController( Core* core ) : IProjectController( core ), d( new ProjectControllerPrivate( this ) ) { qRegisterMetaType>(); setObjectName(QStringLiteral("ProjectController")); d->m_core = core; d->model = new ProjectModel(); //NOTE: this is required to be called here, such that the // actions are available when the UI controller gets // initialized *before* the project controller if (Core::self()->setupFlags() != Core::NoUi) { setupActions(); } } void ProjectController::setupActions() { KActionCollection * ac = d->m_core->uiControllerInternal()->defaultMainWindow()->actionCollection(); QAction*action; d->m_openProject = action = ac->addAction( QStringLiteral("project_open") ); action->setText(i18nc( "@action", "Open / Import Project..." ) ); action->setToolTip( i18nc( "@info:tooltip", "Open or import project" ) ); action->setWhatsThis( i18nc( "@info:whatsthis", "Open an existing KDevelop 4 project or import " "an existing Project into KDevelop 4. This entry " "allows one to select a KDevelop4 project file " "or an existing directory to open it in KDevelop. " "When opening an existing directory that does " "not yet have a KDevelop4 project file, the file " "will be created." ) ); action->setIcon(QIcon::fromTheme(QStringLiteral("project-open"))); connect(action, &QAction::triggered, this, [&] { openProject(); }); d->m_fetchProject = action = ac->addAction( QStringLiteral("project_fetch") ); action->setText(i18nc( "@action", "Fetch Project..." ) ); action->setIcon( QIcon::fromTheme( QStringLiteral("edit-download") ) ); action->setToolTip( i18nc( "@info:tooltip", "Fetch project" ) ); action->setWhatsThis( i18nc( "@info:whatsthis", "Guides the user through the project fetch " "and then imports it into KDevelop 4." ) ); // action->setIcon(QIcon::fromTheme("project-open")); connect( action, &QAction::triggered, this, &ProjectController::fetchProject ); // action = ac->addAction( "project_close" ); // action->setText( i18n( "C&lose Project" ) ); // connect( action, SIGNAL(triggered(bool)), SLOT(closeProject()) ); // action->setToolTip( i18n( "Close project" ) ); // action->setWhatsThis( i18n( "Closes the current project." ) ); // action->setEnabled( false ); d->m_closeProject = action = ac->addAction( QStringLiteral("project_close") ); connect( action, &QAction::triggered, this, [&] { d->closeSelectedProjects(); } ); action->setText( i18nc( "@action", "Close Project(s)" ) ); action->setIcon( QIcon::fromTheme( QStringLiteral("project-development-close") ) ); action->setToolTip( i18nc( "@info:tooltip", "Closes all currently selected projects" ) ); action->setEnabled( false ); d->m_openConfig = action = ac->addAction( QStringLiteral("project_open_config") ); connect( action, &QAction::triggered, this, [&] { d->openProjectConfig(); } ); action->setText( i18n( "Open Configuration..." ) ); action->setIcon( QIcon::fromTheme(QStringLiteral("configure")) ); action->setEnabled( false ); action = ac->addAction( QStringLiteral("commit_current_project") ); connect( action, &QAction::triggered, this, &ProjectController::commitCurrentProject ); action->setText( i18n( "Commit Current Project..." ) ); action->setIconText( i18n( "Commit..." ) ); action->setIcon( QIcon::fromTheme(QStringLiteral("svn-commit")) ); connect(d->m_core->uiControllerInternal()->defaultMainWindow(), &MainWindow::areaChanged, this, [&] (Sublime::Area* area) { d->areaChanged(area); }); d->m_core->uiControllerInternal()->area(0, QStringLiteral("code"))->addAction(action); KSharedConfig * config = KSharedConfig::openConfig().data(); // KConfigGroup group = config->group( "General Options" ); d->m_recentAction = KStandardAction::openRecent(this, SLOT(openProject(QUrl)), this); ac->addAction( QStringLiteral("project_open_recent"), d->m_recentAction ); d->m_recentAction->setText( i18n( "Open Recent Project" ) ); d->m_recentAction->setWhatsThis( i18nc( "@info:whatsthis", "Opens recently opened project." ) ); d->m_recentAction->loadEntries( KConfigGroup(config, "RecentProjects") ); QAction* openProjectForFileAction = new QAction( this ); ac->addAction(QStringLiteral("project_open_for_file"), openProjectForFileAction); openProjectForFileAction->setText(i18n("Open Project for Current File")); connect( openProjectForFileAction, &QAction::triggered, this, &ProjectController::openProjectForUrlSlot); } ProjectController::~ProjectController() { delete d->model; delete d->dialog; delete d; } void ProjectController::cleanup() { if ( d->m_currentlyOpening.isEmpty() ) { d->saveListOfOpenedProjects(); } d->m_cleaningUp = true; if( buildSetModel() ) { buildSetModel()->storeToSession( Core::self()->activeSession() ); } closeAllProjects(); } void ProjectController::initialize() { d->buildset = new ProjectBuildSetModel( this ); buildSetModel()->loadFromSession( Core::self()->activeSession() ); connect( this, &ProjectController::projectOpened, d->buildset, &ProjectBuildSetModel::loadFromProject ); connect( this, &ProjectController::projectClosing, d->buildset, &ProjectBuildSetModel::saveToProject ); connect( this, &ProjectController::projectClosed, d->buildset, &ProjectBuildSetModel::projectClosed ); loadSettings(false); d->dialog = new ProjectDialogProvider(d); QDBusConnection::sessionBus().registerObject( QStringLiteral("/org/kdevelop/ProjectController"), this, QDBusConnection::ExportScriptableSlots ); KSharedConfigPtr config = Core::self()->activeSession()->config(); KConfigGroup group = config->group( "General Options" ); QList openProjects = group.readEntry( "Open Projects", QList() ); QMetaObject::invokeMethod(this, "openProjects", Qt::QueuedConnection, Q_ARG(QList, openProjects)); connect( Core::self()->selectionController(), &ISelectionController::selectionChanged, this, [&] () { d->updateActionStates(); } ); connect(this, &ProjectController::projectOpened, this, [&] () { d->updateActionStates(); }); connect(this, &ProjectController::projectClosing, this, [&] () { d->updateActionStates(); }); } void ProjectController::openProjects(const QList& projects) { foreach (const QUrl& url, projects) openProject(url); } void ProjectController::loadSettings( bool projectIsLoaded ) { Q_UNUSED(projectIsLoaded) } void ProjectController::saveSettings( bool projectIsLoaded ) { Q_UNUSED( projectIsLoaded ); } int ProjectController::projectCount() const { return d->m_projects.count(); } IProject* ProjectController::projectAt( int num ) const { if( !d->m_projects.isEmpty() && num >= 0 && num < d->m_projects.count() ) return d->m_projects.at( num ); return nullptr; } QList ProjectController::projects() const { return d->m_projects; } void ProjectController::eventuallyOpenProjectFile(KIO::Job* _job, KIO::UDSEntryList entries ) { KIO::SimpleJob* job(dynamic_cast(_job)); Q_ASSERT(job); foreach(const KIO::UDSEntry& entry, entries) { if(d->m_foundProjectFile) break; if(!entry.isDir()) { QString name = entry.stringValue( KIO::UDSEntry::UDS_NAME ); if(name.endsWith(QLatin1String(".kdev4"))) { //We have found a project-file, open it openProject(Path(Path(job->url()), name).toUrl()); d->m_foundProjectFile = true; } } } } void ProjectController::openProjectForUrlSlot(bool) { if(ICore::self()->documentController()->activeDocument()) { QUrl url = ICore::self()->documentController()->activeDocument()->url(); IProject* project = ICore::self()->projectController()->findProjectForUrl(url); if(!project) { openProjectForUrl(url); }else{ KMessageBox::error(Core::self()->uiController()->activeMainWindow(), i18n("Project already open: %1", project->name())); } }else{ KMessageBox::error(Core::self()->uiController()->activeMainWindow(), i18n("No active document")); } } void ProjectController::openProjectForUrl(const QUrl& sourceUrl) { Q_ASSERT(!sourceUrl.isRelative()); QUrl dirUrl = sourceUrl.adjusted(QUrl::RemoveFilename); QUrl testAt = dirUrl; d->m_foundProjectFile = false; while(!testAt.path().isEmpty()) { KIO::ListJob* job = KIO::listDir(testAt); connect(job, &KIO::ListJob::entries, this, &ProjectController::eventuallyOpenProjectFile); KJobWidgets::setWindow(job, ICore::self()->uiController()->activeMainWindow()); job->exec(); if(d->m_foundProjectFile) { //Fine! We have directly opened the project d->m_foundProjectFile = false; return; } QUrl oldTest = testAt.adjusted(QUrl::RemoveFilename); if(oldTest == testAt) break; } QUrl askForOpen = d->dialog->askProjectConfigLocation(false, dirUrl); if(askForOpen.isValid()) openProject(askForOpen); } void ProjectController::openProject( const QUrl &projectFile ) { QUrl url = projectFile; if ( url.isEmpty() ) { url = d->dialog->askProjectConfigLocation(false); if ( url.isEmpty() ) { return; } } Q_ASSERT(!url.isRelative()); QList existingSessions; if(!Core::self()->sessionController()->activeSession()->containedProjects().contains(url)) { foreach( const Session* session, Core::self()->sessionController()->sessions()) { if(session->containedProjects().contains(url)) { existingSessions << session; #if 0 ///@todo Think about this! Problem: The session might already contain files, the debugger might be active, etc. //If this session is empty, close it if(Core::self()->sessionController()->activeSession()->description().isEmpty()) { //Terminate this instance of kdevelop if the user agrees foreach(Sublime::MainWindow* window, Core::self()->uiController()->controller()->mainWindows()) window->close(); } #endif } } } if ( ! existingSessions.isEmpty() ) { QDialog dialog(Core::self()->uiControllerInternal()->activeMainWindow()); dialog.setWindowTitle(i18n("Project Already Open")); auto mainLayout = new QVBoxLayout(&dialog); mainLayout->addWidget(new QLabel(i18n("The project you're trying to open is already open in at least one " "other session.
What do you want to do?"))); QGroupBox sessions; sessions.setLayout(new QVBoxLayout); QRadioButton* newSession = new QRadioButton(i18n("Add project to current session")); sessions.layout()->addWidget(newSession); newSession->setChecked(true); foreach ( const Session* session, existingSessions ) { QRadioButton* button = new QRadioButton(i18n("Open session %1", session->description())); button->setProperty("sessionid", session->id().toString()); sessions.layout()->addWidget(button); } sessions.layout()->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Expanding)); mainLayout->addWidget(&sessions); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Abort); auto okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); mainLayout->addWidget(buttonBox); bool success = dialog.exec(); if (!success) return; foreach ( const QObject* obj, sessions.children() ) { if ( const QRadioButton* button = qobject_cast(obj) ) { QString sessionid = button->property("sessionid").toString(); if ( button->isChecked() && ! sessionid.isEmpty() ) { Core::self()->sessionController()->loadSession(sessionid); return; } } } } if ( url.isEmpty() ) { url = d->dialog->askProjectConfigLocation(false); } if ( !url.isEmpty() ) { d->importProject(url); } } +void ProjectController::fetchProjectFromUrl(const QUrl& repoUrl, IPlugin* vcsOrProviderPlugin) +{ + const QUrl url = d->dialog->askProjectConfigLocation(true, QUrl(), repoUrl, vcsOrProviderPlugin); + + if (!url.isEmpty()) { + d->importProject(url); + } +} + void ProjectController::fetchProject() { QUrl url = d->dialog->askProjectConfigLocation(true); if ( !url.isEmpty() ) { d->importProject(url); } } void ProjectController::projectImportingFinished( IProject* project ) { if( !project ) { qWarning() << "OOOPS: 0-pointer project"; return; } IPlugin *managerPlugin = project->managerPlugin(); QList pluglist; pluglist.append( managerPlugin ); d->m_projectPlugins.insert( project, pluglist ); d->m_projects.append( project ); if ( d->m_currentlyOpening.isEmpty() ) { d->saveListOfOpenedProjects(); } if (Core::self()->setupFlags() != Core::NoUi) { d->m_recentAction->addUrl( project->projectFile().toUrl() ); KSharedConfig * config = KSharedConfig::openConfig().data(); KConfigGroup recentGroup = config->group("RecentProjects"); d->m_recentAction->saveEntries( recentGroup ); config->sync(); } Q_ASSERT(d->m_currentlyOpening.contains(project->projectFile().toUrl())); d->m_currentlyOpening.removeAll(project->projectFile().toUrl()); emit projectOpened( project ); reparseProject(project); } // helper method for closeProject() void ProjectController::unloadUnusedProjectPlugins(IProject* proj) { QList pluginsForProj = d->m_projectPlugins.value( proj ); d->m_projectPlugins.remove( proj ); QList otherProjectPlugins; Q_FOREACH( const QList& _list, d->m_projectPlugins ) { otherProjectPlugins << _list; } QSet pluginsForProjSet = QSet::fromList( pluginsForProj ); QSet otherPrjPluginsSet = QSet::fromList( otherProjectPlugins ); // loaded - target = tobe unloaded. QSet tobeRemoved = pluginsForProjSet.subtract( otherPrjPluginsSet ); Q_FOREACH( IPlugin* _plugin, tobeRemoved ) { KPluginMetaData _plugInfo = Core::self()->pluginController()->pluginInfo( _plugin ); if( _plugInfo.isValid() ) { QString _plugName = _plugInfo.pluginId(); qCDebug(SHELL) << "about to unloading :" << _plugName; Core::self()->pluginController()->unloadPlugin( _plugName ); } } } // helper method for closeProject() void ProjectController::closeAllOpenedFiles(IProject* proj) { foreach(IDocument* doc, Core::self()->documentController()->openDocuments()) { if (proj->inProject(IndexedString(doc->url()))) { doc->close(); } } } // helper method for closeProject() void ProjectController::initializePluginCleanup(IProject* proj) { // Unloading (and thus deleting) these plugins is not a good idea just yet // as we're being called by the view part and it gets deleted when we unload the plugin(s) // TODO: find a better place to unload connect(proj, &IProject::destroyed, this, [&] { d->unloadAllProjectPlugins(); }); } void ProjectController::takeProject(IProject* proj) { if (!proj) { return; } // loading might have failed d->m_currentlyOpening.removeAll(proj->projectFile().toUrl()); d->m_projects.removeAll(proj); emit projectClosing(proj); //Core::self()->saveSettings(); // The project file is being closed. // Now we can save settings for all of the Core // objects including this one!! unloadUnusedProjectPlugins(proj); closeAllOpenedFiles(proj); proj->close(); if (d->m_projects.isEmpty()) { initializePluginCleanup(proj); } if(!d->m_cleaningUp) d->saveListOfOpenedProjects(); emit projectClosed(proj); } void ProjectController::closeProject(IProject* proj) { takeProject(proj); proj->deleteLater(); // be safe when deleting } void ProjectController::closeAllProjects() { foreach (auto project, d->m_projects) { closeProject(project); } } void ProjectController::abortOpeningProject(IProject* proj) { d->m_currentlyOpening.removeAll(proj->projectFile().toUrl()); emit projectOpeningAborted(proj); } ProjectModel* ProjectController::projectModel() { return d->model; } IProject* ProjectController::findProjectForUrl( const QUrl& url ) const { if (d->m_projects.isEmpty()) { return nullptr; } ProjectBaseItem* item = d->model->itemForPath(IndexedString(url)); if (item) { return item->project(); } return nullptr; } IProject* ProjectController::findProjectByName( const QString& name ) { Q_FOREACH( IProject* proj, d->m_projects ) { if( proj->name() == name ) { return proj; } } return nullptr; } void ProjectController::configureProject( IProject* project ) { d->projectConfig( project ); } void ProjectController::addProject(IProject* project) { Q_ASSERT(project); if (d->m_projects.contains(project)) { qWarning() << "Project already tracked by this project controller:" << project; return; } // fake-emit signals so listeners are aware of a new project being added emit projectAboutToBeOpened(project); project->setParent(this); d->m_projects.append(project); emit projectOpened(project); } bool ProjectController::isProjectNameUsed( const QString& name ) const { foreach( IProject* p, projects() ) { if( p->name() == name ) { return true; } } return false; } QUrl ProjectController::projectsBaseDirectory() const { KConfigGroup group = ICore::self()->activeSession()->config()->group( "Project Manager" ); return group.readEntry( "Projects Base Directory", QUrl::fromLocalFile( QDir::homePath() + "/projects" ) ); } QString ProjectController::prettyFilePath(const QUrl& url, FormattingOptions format) const { IProject* project = Core::self()->projectController()->findProjectForUrl(url); if(!project) { // Find a project with the correct base directory at least foreach(IProject* candidateProject, Core::self()->projectController()->projects()) { if(candidateProject->path().toUrl().isParentOf(url)) { project = candidateProject; break; } } } Path parent = Path(url).parent(); QString prefixText; if (project) { if (format == FormatHtml) { prefixText = "" + project->name() + "/"; } else { prefixText = project->name() + ':'; } QString relativePath = project->path().relativePath(parent); if(relativePath.startsWith(QLatin1String("./"))) { relativePath = relativePath.mid(2); } if (!relativePath.isEmpty()) { prefixText += relativePath + '/'; } } else { prefixText = parent.pathOrUrl() + '/'; } return prefixText; } QString ProjectController::prettyFileName(const QUrl& url, FormattingOptions format) const { IProject* project = Core::self()->projectController()->findProjectForUrl(url); if(project && project->path() == Path(url)) { if (format == FormatHtml) { return "" + project->name() + ""; } else { return project->name(); } } QString prefixText = prettyFilePath( url, format ); if (format == FormatHtml) { return prefixText + "" + url.fileName() + ""; } else { return prefixText + url.fileName(); } } ContextMenuExtension ProjectController::contextMenuExtension ( Context* ctx ) { ContextMenuExtension ext; if ( ctx->type() != Context::ProjectItemContext || !static_cast(ctx)->items().isEmpty() ) { return ext; } ext.addAction(ContextMenuExtension::ProjectGroup, d->m_openProject); ext.addAction(ContextMenuExtension::ProjectGroup, d->m_fetchProject); ext.addAction(ContextMenuExtension::ProjectGroup, d->m_recentAction); return ext; } ProjectBuildSetModel* ProjectController::buildSetModel() { return d->buildset; } ProjectChangesModel* ProjectController::changesModel() { if(!d->m_changesModel) d->m_changesModel=new ProjectChangesModel(this); return d->m_changesModel; } void ProjectController::commitCurrentProject() { IDocument* doc=ICore::self()->documentController()->activeDocument(); if(!doc) return; QUrl url=doc->url(); IProject* project = ICore::self()->projectController()->findProjectForUrl(url); if(project && project->versionControlPlugin()) { IPlugin* plugin = project->versionControlPlugin(); IBasicVersionControl* vcs=plugin->extension(); if(vcs) { ICore::self()->documentController()->saveAllDocuments(KDevelop::IDocument::Silent); const Path basePath = project->path(); VCSCommitDiffPatchSource* patchSource = new VCSCommitDiffPatchSource(new VCSStandardDiffUpdater(vcs, basePath.toUrl())); bool ret = showVcsDiff(patchSource); if(!ret) { VcsCommitDialog *commitDialog = new VcsCommitDialog(patchSource); commitDialog->setCommitCandidates(patchSource->infos()); commitDialog->exec(); } } } } QString ProjectController::mapSourceBuild( const QString& path_, bool reverse, bool fallbackRoot ) const { Path path(path_); IProject* sourceDirProject = nullptr, *buildDirProject = nullptr; Q_FOREACH(IProject* proj, d->m_projects) { if(proj->path().isParentOf(path) || proj->path() == path) sourceDirProject = proj; if(proj->buildSystemManager()) { Path buildDir = proj->buildSystemManager()->buildDirectory(proj->projectItem()); if(buildDir.isValid() && (buildDir.isParentOf(path) || buildDir == path)) buildDirProject = proj; } } if(!reverse) { // Map-target is the build directory if(sourceDirProject && sourceDirProject->buildSystemManager()) { // We're in the source, map into the build directory QString relativePath = sourceDirProject->path().relativePath(path); Path build = sourceDirProject->buildSystemManager()->buildDirectory(sourceDirProject->projectItem()); build.addPath(relativePath); while(!QFile::exists(build.path())) build = build.parent(); return build.pathOrUrl(); }else if(buildDirProject && fallbackRoot) { // We're in the build directory, map to the build directory root return buildDirProject->buildSystemManager()->buildDirectory(buildDirProject->projectItem()).pathOrUrl(); } }else{ // Map-target is the source directory if(buildDirProject) { Path build = buildDirProject->buildSystemManager()->buildDirectory(buildDirProject->projectItem()); // We're in the source, map into the build directory QString relativePath = build.relativePath(path); Path source = buildDirProject->path(); source.addPath(relativePath); while(!QFile::exists(source.path())) source = source.parent(); return source.pathOrUrl(); }else if(sourceDirProject && fallbackRoot) { // We're in the source directory, map to the root return sourceDirProject->path().pathOrUrl(); } } return QString(); } void ProjectController::reparseProject( IProject* project, bool forceUpdate ) { if (auto job = d->m_parseJobs.value(project)) { job->kill(); } d->m_parseJobs[project] = new KDevelop::ParseProjectJob(project, forceUpdate); ICore::self()->runController()->registerJob(d->m_parseJobs[project]); } } #include "moc_projectcontroller.cpp" diff --git a/shell/projectcontroller.h b/shell/projectcontroller.h index 98635261f5..22a711d1cb 100644 --- a/shell/projectcontroller.h +++ b/shell/projectcontroller.h @@ -1,178 +1,182 @@ /* This file is part of KDevelop Copyright 2006 Adam Treat Copyright 2007 Andreas Pakulat This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KDEVPLATFORM_PROJECTCONTROLLER_H #define KDEVPLATFORM_PROJECTCONTROLLER_H #include #include #include #include "shellexport.h" namespace Sublime { class Area; } namespace KIO { class Job; } namespace KDevelop { class IProject; class Core; class Context; class ContextMenuExtension; class IPlugin; class KDEVPLATFORMSHELL_EXPORT IProjectDialogProvider : public QObject { Q_OBJECT public: IProjectDialogProvider(); ~IProjectDialogProvider() override; public Q_SLOTS: /** * Displays some UI to ask the user for the project location. * * @param fetch will tell the UI that the user might want to fetch the project first * @param startUrl tells where to look first */ - virtual QUrl askProjectConfigLocation(bool fetch, const QUrl& startUrl = QUrl()) = 0; + virtual QUrl askProjectConfigLocation(bool fetch, const QUrl& startUrl = QUrl(), + const QUrl& repoUrl = QUrl(), IPlugin* plugin = nullptr) = 0; virtual bool userWantsReopen() = 0; }; class KDEVPLATFORMSHELL_EXPORT ProjectController : public IProjectController { Q_OBJECT Q_CLASSINFO( "D-Bus Interface", "org.kdevelop.ProjectController" ) friend class Core; friend class CorePrivate; friend class ProjectPreferences; public: explicit ProjectController( Core* core ); ~ProjectController() override; IProject* projectAt( int ) const override; int projectCount() const override; QList projects() const override; ProjectBuildSetModel* buildSetModel() override; ProjectModel* projectModel() override; ProjectChangesModel* changesModel() override; IProject* findProjectByName( const QString& name ) override; IProject* findProjectForUrl( const QUrl& ) const override; bool isProjectNameUsed( const QString& name ) const override; void setDialogProvider(IProjectDialogProvider*); QUrl projectsBaseDirectory() const override; QString prettyFileName(const QUrl& url, FormattingOptions format = FormatHtml) const override; QString prettyFilePath(const QUrl& url, FormattingOptions format = FormatHtml) const override; ContextMenuExtension contextMenuExtension( KDevelop::Context* ctx ); + void fetchProjectFromUrl(const QUrl& repoUrl, IPlugin* vcsOrProviderPlugin); + public Q_SLOTS: void openProjectForUrl( const QUrl &sourceUrl ) override; void fetchProject(); void openProject( const QUrl &KDev4ProjectFile = QUrl() ) override; void abortOpeningProject( IProject* ); void projectImportingFinished( IProject* ); void closeProject( IProject* ) override; void closeAllProjects() override; void configureProject( IProject* ) override; void reparseProject( IProject* project, bool forceUpdate = false ) override; void eventuallyOpenProjectFile(KIO::Job*,KIO::UDSEntryList); void openProjectForUrlSlot(bool); // void changeCurrentProject( ProjectBaseItem* ); void openProjects(const QList& projects); void commitCurrentProject(); // Maps the given path from the source to the equivalent path within the build directory // of the corresponding project. If the path is already in the build directory and fallbackRoot is true, // then it is mapped to the root of the build directory. // This is used through DBus from within kdevplatform_shell_environment.sh // If reverse is true, maps the opposite direction, from build to source. [ Used in kdevplatform_shell_environment.sh ] Q_SCRIPTABLE QString mapSourceBuild( const QString& path, bool reverse = false, bool fallbackRoot = true ) const; protected: /** * Add the existing project @p project to the controller * * @note Method is used for testing objectives, consider using openProject() instead! * @note takes ownership over the project * * @sa openProject() */ void addProject(IProject* proj); /** * Remove the project @p project from the controller * * @note Ownership is passed on to the caller */ void takeProject(IProject* proj); virtual void loadSettings( bool projectIsLoaded ); virtual void saveSettings( bool projectIsLoaded ); virtual void initialize(); private: //FIXME Do not load all of this just for the project being opened... //void legacyLoading(); void setupActions(); void cleanup(); // helper methods for closeProject() void unloadUnusedProjectPlugins(IProject* proj); void disableProjectCloseAction(); void closeAllOpenedFiles(IProject* proj); void initializePluginCleanup(IProject* proj); private: class ProjectControllerPrivate* const d; friend class ProjectControllerPrivate; }; class ProjectDialogProvider : public IProjectDialogProvider { Q_OBJECT public: explicit ProjectDialogProvider(ProjectControllerPrivate* const p); ~ProjectDialogProvider() override; ProjectControllerPrivate* const d; public Q_SLOTS: - QUrl askProjectConfigLocation(bool fetch, const QUrl& sta) override; + QUrl askProjectConfigLocation(bool fetch, const QUrl& startUrl, + const QUrl& repoUrl, IPlugin* plugin) override; bool userWantsReopen() override; }; } #endif diff --git a/shell/projectsourcepage.cpp b/shell/projectsourcepage.cpp index 348defc3f0..d700de9d20 100644 --- a/shell/projectsourcepage.cpp +++ b/shell/projectsourcepage.cpp @@ -1,280 +1,309 @@ /*************************************************************************** * Copyright (C) 2010 by Aleix Pol Gonzalez * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "projectsourcepage.h" #include "ui_projectsourcepage.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; static const int FROM_FILESYSTEM_SOURCE_INDEX = 0; -ProjectSourcePage::ProjectSourcePage(const QUrl& initial, QWidget* parent) +ProjectSourcePage::ProjectSourcePage(const QUrl& initial, const QUrl& repoUrl, IPlugin* preSelectPlugin, + QWidget* parent) : QWidget(parent) { m_ui = new Ui::ProjectSourcePage; m_ui->setupUi(this); m_ui->status->setCloseButtonVisible(false); m_ui->status->setMessageType(KMessageWidget::Error); m_ui->workingDir->setUrl(initial); m_ui->workingDir->setMode(KFile::Directory); m_ui->remoteWidget->setLayout(new QVBoxLayout(m_ui->remoteWidget)); m_ui->sources->addItem(QIcon::fromTheme(QStringLiteral("folder")), i18n("From File System")); m_plugins.append(nullptr); + int preselectIndex = -1; IPluginController* pluginManager = ICore::self()->pluginController(); QList plugins = pluginManager->allPluginsForExtension( QStringLiteral("org.kdevelop.IBasicVersionControl") ); foreach( IPlugin* p, plugins ) { + if (p == preSelectPlugin) { + preselectIndex = m_plugins.count(); + } m_plugins.append(p); m_ui->sources->addItem(QIcon::fromTheme(pluginManager->pluginInfo(p).iconName()), p->extension()->name()); } plugins = pluginManager->allPluginsForExtension( QStringLiteral("org.kdevelop.IProjectProvider") ); foreach( IPlugin* p, plugins ) { + if (p == preSelectPlugin) { + preselectIndex = m_plugins.count(); + } m_plugins.append(p); m_ui->sources->addItem(QIcon::fromTheme(pluginManager->pluginInfo(p).iconName()), p->extension()->name()); } + if (preselectIndex == -1) { + // "From File System" is quite unlikely to be what the user wants, so default to first real plugin... + const int defaultIndex = (m_plugins.count() > 1) ? 1 : 0; + KConfigGroup configGroup = KSharedConfig::openConfig()->group("Providers"); + preselectIndex = configGroup.readEntry("LastProviderIndex", defaultIndex); + } + preselectIndex = qBound(0, preselectIndex, m_ui->sources->count() - 1); + m_ui->sources->setCurrentIndex(preselectIndex); + setSourceWidget(preselectIndex, repoUrl); + + // connect as last step, otherwise KMessageWidget could get both animatedHide() and animatedShow() + // during setup and due to a bug will ignore any but the first call + // Patch proposed at https://phabricator.kde.org/D4329 connect(m_ui->workingDir, &KUrlRequester::textChanged, this, &ProjectSourcePage::reevaluateCorrection); connect(m_ui->sources, static_cast(&QComboBox::currentIndexChanged), this, &ProjectSourcePage::setSourceIndex); connect(m_ui->get, &QPushButton::clicked, this, &ProjectSourcePage::checkoutVcsProject); - - emit isCorrect(false); - - setSourceIndex(FROM_FILESYSTEM_SOURCE_INDEX); - - const int defaultIndex = m_plugins.isEmpty() ? 0 : 1; // "From File System" is quite unlikely to what you want... - KConfigGroup configGroup = KSharedConfig::openConfig()->group("Providers"); - const int lastCurrentIndex = configGroup.readEntry("LastProviderIndex", defaultIndex); - m_ui->sources->setCurrentIndex(qBound(0, lastCurrentIndex, m_ui->sources->count() - 1)); } ProjectSourcePage::~ProjectSourcePage() { KConfigGroup configGroup = KSharedConfig::openConfig()->group("Providers"); configGroup.writeEntry("LastProviderIndex", m_ui->sources->currentIndex()); delete m_ui; } void ProjectSourcePage::setSourceIndex(int index) +{ + setSourceWidget(index, QUrl()); +} + +void ProjectSourcePage::setSourceWidget(int index, const QUrl& repoUrl) { m_locationWidget = nullptr; m_providerWidget = nullptr; QLayout* remoteWidgetLayout = m_ui->remoteWidget->layout(); QLayoutItem *child; while ((child = remoteWidgetLayout->takeAt(0)) != nullptr) { delete child->widget(); delete child; } IBasicVersionControl* vcIface = vcsPerIndex(index); IProjectProvider* providerIface; bool found=false; if(vcIface) { found=true; m_locationWidget=vcIface->vcsLocation(m_ui->sourceBox); connect(m_locationWidget, &VcsLocationWidget::changed, this, &ProjectSourcePage::locationChanged); + // set after connect, to trigger handler + if (!repoUrl.isEmpty()) { + m_locationWidget->setLocation(repoUrl); + } remoteWidgetLayout->addWidget(m_locationWidget); } else { providerIface = providerPerIndex(index); if(providerIface) { found=true; m_providerWidget=providerIface->providerWidget(m_ui->sourceBox); connect(m_providerWidget, &IProjectProviderWidget::changed, this, &ProjectSourcePage::projectChanged); remoteWidgetLayout->addWidget(m_providerWidget); } } reevaluateCorrection(); m_ui->sourceBox->setVisible(found); } IBasicVersionControl* ProjectSourcePage::vcsPerIndex(int index) { IPlugin* p = m_plugins.value(index); if(!p) return nullptr; else return p->extension(); } IProjectProvider* ProjectSourcePage::providerPerIndex(int index) { IPlugin* p = m_plugins.value(index); if(!p) return nullptr; else return p->extension(); } VcsJob* ProjectSourcePage::jobPerCurrent() { QUrl url=m_ui->workingDir->url(); IPlugin* p=m_plugins[m_ui->sources->currentIndex()]; VcsJob* job=nullptr; if(IBasicVersionControl* iface=p->extension()) { Q_ASSERT(iface && m_locationWidget); job=iface->createWorkingCopy(m_locationWidget->location(), url); } else if(m_providerWidget) { job=m_providerWidget->createWorkingCopy(url); } return job; } void ProjectSourcePage::checkoutVcsProject() { QUrl url=m_ui->workingDir->url(); QDir d(url.toLocalFile()); if(!url.isLocalFile() && !d.exists()) { bool corr = d.mkpath(d.path()); if(!corr) { KMessageBox::error(nullptr, i18n("Could not create the directory: %1", d.path())); return; } } VcsJob* job=jobPerCurrent(); if (!job) { return; } m_ui->sources->setEnabled(false); m_ui->sourceBox->setEnabled(false); m_ui->workingDir->setEnabled(false); m_ui->get->setEnabled(false); m_ui->creationProgress->setValue(m_ui->creationProgress->minimum()); connect(job, &VcsJob::result, this, &ProjectSourcePage::projectReceived); // Can't use new signal-slot syntax, KJob::percent is private :/ connect(job, SIGNAL(percent(KJob*,ulong)), SLOT(progressChanged(KJob*,ulong))); connect(job, &VcsJob::infoMessage, this, &ProjectSourcePage::infoMessage); ICore::self()->runController()->registerJob(job); } void ProjectSourcePage::progressChanged(KJob*, unsigned long value) { m_ui->creationProgress->setValue(value); } void ProjectSourcePage::infoMessage(KJob* , const QString& text, const QString& /*rich*/) { m_ui->creationProgress->setFormat(i18nc("Format of the progress bar text. progress and info", "%1 : %p%", text)); } void ProjectSourcePage::projectReceived(KJob* job) { if (job->error()) { m_ui->creationProgress->setValue(0); } else { m_ui->creationProgress->setValue(m_ui->creationProgress->maximum()); } reevaluateCorrection(); m_ui->creationProgress->setFormat(QStringLiteral("%p%")); } void ProjectSourcePage::reevaluateCorrection() { //TODO: Probably we should just ignore remote URL's, I don't think we're ever going //to support checking out to remote directories const QUrl cwd = m_ui->workingDir->url(); const QDir dir = cwd.toLocalFile(); // case where we import a project from local file system if (m_ui->sources->currentIndex() == FROM_FILESYSTEM_SOURCE_INDEX) { emit isCorrect(dir.exists()); return; } // all other cases where remote locations need to be specified bool correct=!cwd.isRelative() && (!cwd.isLocalFile() || QDir(cwd.adjusted(QUrl::RemoveFilename).toLocalFile()).exists()); emit isCorrect(correct && m_ui->creationProgress->value() == m_ui->creationProgress->maximum()); const bool validWidget = ((m_locationWidget && m_locationWidget->isCorrect()) || (m_providerWidget && m_providerWidget->isCorrect())); const bool isFolderEmpty = (correct && cwd.isLocalFile() && dir.exists() && dir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot).isEmpty()); const bool validToCheckout = correct && validWidget && (!dir.exists() || isFolderEmpty); m_ui->get->setEnabled(validToCheckout); m_ui->creationProgress->setEnabled(validToCheckout); if(!correct) setStatus(i18n("You need to specify a valid or nonexistent directory to check out a project")); else if(!m_ui->get->isEnabled() && m_ui->workingDir->isEnabled() && !validWidget) setStatus(i18n("You need to specify the source for your remote project")); else if(!m_ui->get->isEnabled() && m_ui->workingDir->isEnabled() && !isFolderEmpty) setStatus(i18n("You need to specify an empty folder as your project destination")); else clearStatus(); } void ProjectSourcePage::locationChanged() { Q_ASSERT(m_locationWidget); if(m_locationWidget->isCorrect()) { QString currentUrl = m_ui->workingDir->text(); currentUrl = currentUrl.left(currentUrl.lastIndexOf('/')+1); QUrl current = QUrl::fromUserInput(currentUrl + m_locationWidget->projectName()); m_ui->workingDir->setUrl(current); } else reevaluateCorrection(); } void ProjectSourcePage::projectChanged(const QString& name) { Q_ASSERT(m_providerWidget); QString currentUrl = m_ui->workingDir->text(); currentUrl = currentUrl.left(currentUrl.lastIndexOf('/')+1); QUrl current = QUrl::fromUserInput(currentUrl + name); m_ui->workingDir->setUrl(current); } void ProjectSourcePage::setStatus(const QString& message) { m_ui->status->setText(message); m_ui->status->animatedShow(); } void ProjectSourcePage::clearStatus() { - m_ui->status->animatedHide(); + // workaround for KMessageWidget bug: + // animatedHide will not explicitely hide the widget if it is not yet shown, + // so if show() is called on the parent later, the KMessageWidget + // Patch proposed at https://phabricator.kde.org/D4329 + if (!m_ui->status->isVisible()) { + m_ui->status->hide(); + } else { + m_ui->status->animatedHide(); + } } QUrl ProjectSourcePage::workingDir() const { return m_ui->workingDir->url(); } diff --git a/shell/projectsourcepage.h b/shell/projectsourcepage.h index a45ee19e68..37da36bedd 100644 --- a/shell/projectsourcepage.h +++ b/shell/projectsourcepage.h @@ -1,65 +1,67 @@ /*************************************************************************** * Copyright (C) 2010 by Aleix Pol Gonzalez * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef KDEVPLATFORM_PROJECTSOURCEPAGE_H #define KDEVPLATFORM_PROJECTSOURCEPAGE_H #include class KJob; class QUrl; namespace Ui { class ProjectSourcePage; } namespace KDevelop { class VcsJob; class IProjectProvider; class IProjectProviderWidget; class IPlugin; class IBasicVersionControl; class VcsLocationWidget; class ProjectSourcePage : public QWidget { Q_OBJECT public: - explicit ProjectSourcePage(const QUrl& initial, QWidget* parent = nullptr); + explicit ProjectSourcePage(const QUrl& initial, const QUrl& repoUrl, IPlugin* preSelectPlugin, + QWidget* parent = nullptr); ~ProjectSourcePage() override; QUrl workingDir() const; private slots: void setSourceIndex(int index); void checkoutVcsProject(); void projectReceived(KJob* job); void reevaluateCorrection(); void progressChanged(KJob*, unsigned long); void infoMessage(KJob*, const QString& text, const QString& rich); void locationChanged(); void projectChanged(const QString& name); signals: void isCorrect(bool); private: + void setSourceWidget(int index, const QUrl& repoUrl); void setStatus(const QString& message); void clearStatus(); KDevelop::IBasicVersionControl* vcsPerIndex(int index); KDevelop::IProjectProvider* providerPerIndex(int index); KDevelop::VcsJob* jobPerCurrent(); Ui::ProjectSourcePage* m_ui; QList m_plugins; KDevelop::VcsLocationWidget* m_locationWidget; KDevelop::IProjectProviderWidget* m_providerWidget; }; } #endif // KDEVPLATFORM_PROJECTSOURCEPAGE_H diff --git a/shell/tests/test_projectcontroller.cpp b/shell/tests/test_projectcontroller.cpp index 3eac7cadcf..4189065fef 100644 --- a/shell/tests/test_projectcontroller.cpp +++ b/shell/tests/test_projectcontroller.cpp @@ -1,574 +1,576 @@ /*************************************************************************** * Copyright 2008 Manuel Breugelmans * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Library General Public License as * * published by the Free Software Foundation; either version 2 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "test_projectcontroller.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; Q_DECLARE_METATYPE(KDevelop::IProject*) namespace { class DialogProviderFake : public IProjectDialogProvider { Q_OBJECT public: DialogProviderFake() : m_reopen(true) {} ~DialogProviderFake() override {} bool m_reopen; public slots: - QUrl askProjectConfigLocation(bool /*fetch*/, const QUrl& /*startUrl*/ = QUrl()) override { return QUrl(); } + QUrl askProjectConfigLocation(bool /*fetch*/, const QUrl& /*startUrl*/, + const QUrl& /*repoUrl*/, IPlugin* /*plugin*/) override + { return QUrl(); } bool userWantsReopen() override { return m_reopen; } }; } /*! A Filemanager plugin that allows you to setup a file & directory structure */ class FakeFileManager : public IPlugin, public IProjectFileManager { Q_OBJECT Q_INTERFACES(KDevelop::IProjectFileManager) public: FakeFileManager(QObject*, const QVariantList&) : IPlugin(ICore::self()->aboutData().componentName(), Core::self()) { } FakeFileManager() : IPlugin(ICore::self()->aboutData().componentName(), Core::self()) { } ~FakeFileManager() override {} Features features() const override { return IProjectFileManager::Files | IProjectFileManager::Folders; } QMap m_filesInFolder; // initialize QMap m_subFoldersInFolder; /*! Setup this manager such that @p folder contains @p file */ void addFileToFolder(const Path& folder, const Path& file) { if (!m_filesInFolder.contains(folder)) { m_filesInFolder[folder] = Path::List(); } m_filesInFolder[folder] << file; } /*! Setup this manager such that @p folder has @p subFolder */ void addSubFolderTo(const Path& folder, Path subFolder) { if (!m_subFoldersInFolder.contains(folder)) { m_subFoldersInFolder[folder] = Path::List(); } m_subFoldersInFolder[folder] << subFolder; } QList parse(ProjectFolderItem *dom) override { Path::List files = m_filesInFolder[dom->path()]; foreach (const Path& file, files) { new ProjectFileItem(dom->project(), file, dom); } Path::List folderPaths = m_subFoldersInFolder[dom->path()]; QList folders; foreach (const Path& folderPath, folderPaths) { folders << new ProjectFolderItem(dom->project(), folderPath, dom); } return folders; } ProjectFolderItem *import(IProject *project) override { ProjectFolderItem* it = new ProjectFolderItem(project, project->path()); return it; } ProjectFolderItem* addFolder(const Path& /*folder*/, ProjectFolderItem */*parent*/) override { return nullptr; } ProjectFileItem* addFile(const Path& /*file*/, ProjectFolderItem */*parent*/) override { return nullptr; } bool removeFilesAndFolders(const QList &/*items*/) override { return false; } bool moveFilesAndFolders(const QList< KDevelop::ProjectBaseItem* > &/*items*/, KDevelop::ProjectFolderItem* /*newParent*/) override { return false; } bool copyFilesAndFolders(const Path::List &/*items*/, KDevelop::ProjectFolderItem* /*newParent*/) override { return false; } bool renameFile(ProjectFileItem* /*file*/, const Path& /*newPath*/) override { return false; } bool renameFolder(ProjectFolderItem* /*oldFolder*/, const Path& /*newPath*/ ) override { return false; } bool reload(ProjectFolderItem* /*item*/) override { return false; } }; class FakePluginController : public PluginController { Q_OBJECT public: explicit FakePluginController(Core* core) : PluginController(core) , m_fakeFileManager(new FakeFileManager) { } IPlugin* pluginForExtension(const QString& extension, const QString& pluginName = {}, const QVariantMap& constraints = QVariantMap()) override { if (extension == qobject_interface_iid()) { return m_fakeFileManager; } return PluginController::pluginForExtension(extension, pluginName, constraints); } private: FakeFileManager* m_fakeFileManager; }; ////////////////////// Fixture /////////////////////////////////////////////// void TestProjectController::initTestCase() { AutoTestShell::init({{}}); TestCore* testCore = new TestCore; testCore->setPluginController( new FakePluginController(testCore) ); testCore->initialize(); qRegisterMetaType(); m_core = Core::self(); m_scratchDir = QDir(QDir::tempPath()); m_scratchDir.mkdir(QStringLiteral("prjctrltest")); m_scratchDir.cd(QStringLiteral("prjctrltest")); } void TestProjectController::cleanupTestCase() { TestCore::shutdown(); } void TestProjectController::init() { m_projName = QStringLiteral("foo"); m_projFilePath = writeProjectConfig(m_projName); m_projCtrl = m_core->projectControllerInternal(); m_tmpConfigs << m_projFilePath; m_projFolder = Path(m_scratchDir.absolutePath() + '/'); } void TestProjectController::cleanup() { // also close any opened projects as we do not get a clean fixture, // following tests should start off clean. foreach(IProject* p, m_projCtrl->projects()) { m_projCtrl->closeProject(p); } foreach(const Path &cfg, m_tmpConfigs) { QFile::remove(cfg.pathOrUrl()); } qDeleteAll(m_fileManagerGarbage); m_fileManagerGarbage.clear(); } ////////////////////// Commands ////////////////////////////////////////////// #define WAIT_FOR_OPEN_SIGNAL \ {\ QSignalSpy signal(m_projCtrl, SIGNAL(projectOpened(KDevelop::IProject*)));\ QVERIFY2(signal.wait(30000), "Timeout while waiting for opened signal");\ } void(0) void TestProjectController::openProject() { QSignalSpy* spy = createOpenedSpy(); QVERIFY(!m_projCtrl->isProjectNameUsed(m_projName)); m_projCtrl->openProject(m_projFilePath.toUrl()); WAIT_FOR_OPEN_SIGNAL; QCOMPARE(m_projCtrl->projectCount(), 1); IProject* proj; assertProjectOpened(m_projName, proj);QVERIFY(proj); assertSpyCaughtProject(spy, proj); QCOMPARE(proj->projectFile(), m_projFilePath); QCOMPARE(proj->path(), Path(m_scratchDir.absolutePath()+'/')); QVERIFY(m_projCtrl->isProjectNameUsed(m_projName)); } void TestProjectController::closeProject() { m_projCtrl->openProject(m_projFilePath.toUrl()); WAIT_FOR_OPEN_SIGNAL; IProject* proj = m_projCtrl->findProjectByName(m_projName); Q_ASSERT(proj); QSignalSpy* spy1 = createClosedSpy(); QSignalSpy* spy2 = createClosingSpy(); m_projCtrl->closeProject(proj); QVERIFY(!m_projCtrl->isProjectNameUsed(m_projName)); QCOMPARE(m_projCtrl->projectCount(), 0); assertProjectClosed(proj); assertSpyCaughtProject(spy1, proj); assertSpyCaughtProject(spy2, proj); } void TestProjectController::openCloseOpen() { m_projCtrl->openProject(m_projFilePath.toUrl()); WAIT_FOR_OPEN_SIGNAL; IProject* proj; assertProjectOpened(m_projName, proj); m_projCtrl->closeProject(proj); QSignalSpy* spy = createOpenedSpy(); m_projCtrl->openProject(m_projFilePath.toUrl()); WAIT_FOR_OPEN_SIGNAL; QVERIFY(m_projCtrl->isProjectNameUsed(m_projName)); QCOMPARE(m_projCtrl->projectCount(), 1); assertProjectOpened(m_projName, proj); assertSpyCaughtProject(spy, proj); } void TestProjectController::reopen() { m_projCtrl->setDialogProvider(new DialogProviderFake); m_projCtrl->openProject(m_projFilePath.toUrl()); WAIT_FOR_OPEN_SIGNAL; QSignalSpy* spy = createOpenedSpy(); m_projCtrl->openProject(m_projFilePath.toUrl()); WAIT_FOR_OPEN_SIGNAL; QCOMPARE(m_projCtrl->projectCount(), 1); QVERIFY(m_projCtrl->isProjectNameUsed(m_projName)); IProject* proj; assertProjectOpened(m_projName, proj); assertSpyCaughtProject(spy, proj); } void TestProjectController::reopenWhileLoading() { // Open the same project again while the first is still // loading. The second open request should be blocked. m_projCtrl->setDialogProvider(new DialogProviderFake); QSignalSpy* spy = createOpenedSpy(); m_projCtrl->openProject(m_projFilePath.toUrl()); //m_projCtrl->openProject(m_projFilePath.toUrl()); WAIT_FOR_OPEN_SIGNAL; // wait a bit for a second signal, this should timeout QSignalSpy signal(m_projCtrl, SIGNAL(projectOpened(KDevelop::IProject*))); QVERIFY2(!signal.wait(100), "Received 2 projectOpened signals."); QCOMPARE(m_projCtrl->projectCount(), 1); IProject* proj; assertProjectOpened(m_projName, proj); assertSpyCaughtProject(spy, proj); } void TestProjectController::openMultiple() { QString secondProj(QStringLiteral("bar")); Path secondCfgUrl = writeProjectConfig(secondProj); QSignalSpy* spy = createOpenedSpy(); m_projCtrl->openProject(m_projFilePath.toUrl()); WAIT_FOR_OPEN_SIGNAL; m_projCtrl->openProject(secondCfgUrl.toUrl()); WAIT_FOR_OPEN_SIGNAL; QCOMPARE(m_projCtrl->projectCount(), 2); IProject *proj1, *proj2; assertProjectOpened(m_projName, proj1); assertProjectOpened(secondProj, proj2); QVERIFY(m_projCtrl->isProjectNameUsed(m_projName)); QVERIFY(m_projCtrl->isProjectNameUsed(QStringLiteral("bar"))); QCOMPARE(spy->size(), 2); IProject* emittedProj1 = (*spy)[0][0].value(); IProject* emittedProj2 = (*spy)[1][0].value(); QCOMPARE(emittedProj1, proj1); QCOMPARE(emittedProj2, proj2); m_tmpConfigs << secondCfgUrl; } /*! Verify that the projectmodel contains a single project. Put this project's * ProjectFolderItem in the output parameter @p RootItem */ #define ASSERT_SINGLE_PROJECT_IN_MODEL(rootItem) \ {\ QCOMPARE(m_projCtrl->projectModel()->rowCount(), 1); \ QModelIndex projIndex = m_projCtrl->projectModel()->index(0,0); \ QVERIFY(projIndex.isValid()); \ ProjectBaseItem* i = m_projCtrl->projectModel()->itemFromIndex( projIndex ); \ QVERIFY(i); \ QVERIFY(i->folder()); \ rootItem = i->folder();\ } void(0) /*! Verify that the projectitem @p item has a single child item * named @p name with url @p url. @p subFolder is an output parameter * that contains the sub-folder projectitem. */ #define ASSERT_SINGLE_SUBFOLDER_IN(item, name, path__, subFolder) \ {\ QCOMPARE(item->rowCount(), 1);\ QCOMPARE(item->folderList().size(), 1);\ ProjectFolderItem* fo = item->folderList().at(0);\ QVERIFY(fo);\ QCOMPARE(fo->path(), path__);\ QCOMPARE(fo->folderName(), QStringLiteral(name));\ subFolder = fo;\ } void(0) #define ASSERT_SINGLE_FILE_IN(rootFolder, name, path__, fileItem)\ {\ QCOMPARE(rootFolder->rowCount(), 1);\ QCOMPARE(rootFolder->fileList().size(), 1);\ fileItem = rootFolder->fileList().at(0);\ QVERIFY(fileItem);\ QCOMPARE(fileItem->path(), path__);\ QCOMPARE(fileItem->fileName(), QStringLiteral(name));\ } void(0) // command void TestProjectController::emptyProject() { // verify that the project model contains a single top-level folder after loading // an empty project assertEmptyProjectModel(); m_projCtrl->openProject(m_projFilePath.toUrl()); WAIT_FOR_OPEN_SIGNAL; Project* proj; assertProjectOpened(m_projName, (KDevelop::IProject*&)proj); FakeFileManager* fileMng = createFileManager(); Q_ASSERT(fileMng); proj->setManagerPlugin(fileMng); proj->reloadModel(); QTest::qWait(100); ProjectFolderItem* rootFolder; ASSERT_SINGLE_PROJECT_IN_MODEL(rootFolder); // check that the project is empty QCOMPARE(rootFolder->rowCount(), 0); QCOMPARE(rootFolder->project()->name(), m_projName); QCOMPARE(rootFolder->path(), m_projFolder); } // command void TestProjectController::singleFile() { // verify that the project model contains a single file in the // top folder. First setup a FakeFileManager with this file m_projCtrl->openProject(m_projFilePath.toUrl()); WAIT_FOR_OPEN_SIGNAL; Project* proj; assertProjectOpened(m_projName, (KDevelop::IProject*&)proj); FakeFileManager* fileMng = createFileManager(); proj->setManagerPlugin(fileMng); Path filePath = Path(m_projFolder, QStringLiteral("foobar")); fileMng->addFileToFolder(m_projFolder, filePath); proj->reloadModel(); QTest::qWait(100); // NO signals for reload ... ProjectFolderItem* rootFolder; ASSERT_SINGLE_PROJECT_IN_MODEL(rootFolder); ProjectFileItem* fi; ASSERT_SINGLE_FILE_IN(rootFolder, "foobar", filePath, fi); QCOMPARE(fi->rowCount(), 0); ASSERT_SINGLE_PROJECT_IN_MODEL(rootFolder); ASSERT_SINGLE_FILE_IN(rootFolder, "foobar", filePath, fi); } // command void TestProjectController::singleDirectory() { // verify that the project model contains a single folder in the // top folder. First setup a FakeFileManager with this folder m_projCtrl->openProject(m_projFilePath.toUrl()); WAIT_FOR_OPEN_SIGNAL; Project* proj; assertProjectOpened(m_projName, (KDevelop::IProject*&)proj); Path folderPath = Path(m_projFolder, QStringLiteral("foobar/")); FakeFileManager* fileMng = createFileManager(); fileMng->addSubFolderTo(m_projFolder, folderPath); proj->setManagerPlugin(fileMng); proj->reloadModel(); QTest::qWait(100); ProjectFolderItem* rootFolder; ASSERT_SINGLE_PROJECT_IN_MODEL(rootFolder); // check that the project contains a single subfolder ProjectFolderItem* sub; ASSERT_SINGLE_SUBFOLDER_IN(rootFolder, "foobar", folderPath, sub); QCOMPARE(sub->rowCount(), 0); } // command void TestProjectController::fileInSubdirectory() { // verify that the project model contains a single file in a subfolder // First setup a FakeFileManager with this folder + file m_projCtrl->openProject(m_projFilePath.toUrl()); WAIT_FOR_OPEN_SIGNAL; Project* proj; assertProjectOpened(m_projName, (KDevelop::IProject*&)proj); Path folderPath = Path(m_projFolder, QStringLiteral("foobar/")); FakeFileManager* fileMng = createFileManager(); fileMng->addSubFolderTo(m_projFolder, folderPath); Path filePath = Path(folderPath, QStringLiteral("zoo")); fileMng->addFileToFolder(folderPath, filePath); proj->setManagerPlugin(fileMng); ProjectFolderItem* rootFolder = nullptr; ProjectFolderItem* sub = nullptr; ProjectFileItem* file = nullptr; proj->reloadModel(); QTest::qWait(100); ASSERT_SINGLE_PROJECT_IN_MODEL(rootFolder); ASSERT_SINGLE_SUBFOLDER_IN(rootFolder, "foobar", folderPath, sub); ASSERT_SINGLE_FILE_IN(sub,"zoo",filePath,file); ASSERT_SINGLE_PROJECT_IN_MODEL(rootFolder); ASSERT_SINGLE_SUBFOLDER_IN(rootFolder, "foobar", folderPath, sub); ASSERT_SINGLE_FILE_IN(sub,"zoo",filePath,file); } void TestProjectController::prettyFileName_data() { QTest::addColumn("relativeFilePath"); QTest::newRow("basic") << "foobar.txt"; QTest::newRow("subfolder") << "sub/foobar.txt"; } void TestProjectController::prettyFileName() { QFETCH(QString, relativeFilePath); m_projCtrl->openProject(m_projFilePath.toUrl()); WAIT_FOR_OPEN_SIGNAL; Project* proj; assertProjectOpened(m_projName, (KDevelop::IProject*&)proj); FakeFileManager* fileMng = createFileManager(); proj->setManagerPlugin(fileMng); Path filePath = Path(m_projFolder, relativeFilePath); fileMng->addFileToFolder(m_projFolder, filePath); QCOMPARE(m_projCtrl->prettyFileName(filePath.toUrl(), ProjectController::FormattingOptions::FormatPlain), QString(m_projName + ':' + relativeFilePath)); } ////////////////////// Helpers /////////////////////////////////////////////// Path TestProjectController::writeProjectConfig(const QString& name) { Path configPath = Path(m_scratchDir.absolutePath() + '/' + name + ".kdev4"); QFile f(configPath.pathOrUrl()); f.open(QIODevice::WriteOnly); QTextStream str(&f); str << "[Project]\n" << "Name=" << name << "\n"; f.close(); return configPath; } ////////////////// Custom assertions ///////////////////////////////////////// void TestProjectController::assertProjectOpened(const QString& name, IProject*& proj) { QVERIFY(proj = m_projCtrl->findProjectByName(name)); QVERIFY(m_projCtrl->projects().contains(proj)); } void TestProjectController::assertSpyCaughtProject(QSignalSpy* spy, IProject* proj) { QCOMPARE(spy->size(), 1); IProject* emittedProj = (*spy)[0][0].value(); QCOMPARE(proj, emittedProj); } void TestProjectController::assertProjectClosed(IProject* proj) { IProject* p = m_projCtrl->findProjectByName(proj->name()); QVERIFY(p == nullptr); QVERIFY(!m_projCtrl->projects().contains(proj)); } void TestProjectController::assertEmptyProjectModel() { ProjectModel* m = m_projCtrl->projectModel(); Q_ASSERT(m); QCOMPARE(m->rowCount(), 0); } ///////////////////// Creation stuff ///////////////////////////////////////// QSignalSpy* TestProjectController::createOpenedSpy() { return new QSignalSpy(m_projCtrl, SIGNAL(projectOpened(KDevelop::IProject*))); } QSignalSpy* TestProjectController::createClosedSpy() { return new QSignalSpy(m_projCtrl, SIGNAL(projectClosed(KDevelop::IProject*))); } QSignalSpy* TestProjectController::createClosingSpy() { return new QSignalSpy(m_projCtrl, SIGNAL(projectClosing(KDevelop::IProject*))); } FakeFileManager* TestProjectController::createFileManager() { FakeFileManager* fileMng = new FakeFileManager; m_fileManagerGarbage << fileMng; return fileMng; } QTEST_MAIN(TestProjectController) #include "moc_test_projectcontroller.cpp" #include "test_projectcontroller.moc" diff --git a/vcs/interfaces/ibasicversioncontrol.h b/vcs/interfaces/ibasicversioncontrol.h index dfb0433ebf..f438c54bf0 100644 --- a/vcs/interfaces/ibasicversioncontrol.h +++ b/vcs/interfaces/ibasicversioncontrol.h @@ -1,268 +1,281 @@ /* This file is part of KDevelop * * Copyright 2007 Andreas Pakulat * Copyright 2007 Matthew Woehlke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #ifndef KDEVPLATFORM_IBASICVERSIONCONTROL_H #define KDEVPLATFORM_IBASICVERSIONCONTROL_H #include #include #include "../vcsrevision.h" #include "../vcsdiff.h" class QString; class KTextEdit; namespace KDevelop { class VcsJob; class VcsLocation; class VcsImportMetadataWidget; class VcsLocationWidget; /** * This is the basic interface that all Version Control or Source Code Management * plugins need to implement. None of the methods in this interface are optional. * * This only works on a local checkout from the repository, if your plugin should * offer functionality that works solely on the server see the * IRepositoryVersionControl interface * */ class KDEVPLATFORMVCS_EXPORT IBasicVersionControl { public: enum RecursionMode { Recursive /**< run recursively through subdirectories */, NonRecursive /**< don't run recursively through subdirectories */ }; virtual ~IBasicVersionControl(){} /** * return a user-visible string for the version control plugin * Can be used for example when importing a project into a * version control system to choose the appropriate system * * @returns a translated user-visible name for this version control plugin */ virtual QString name() const = 0; /** * provides a widget that fetches the needed input data from the user * to import a project into a version control system. * * If this returns 0 the plugin will not be available as an option for import * when creating a new project * * @param parent the parent widget for the newly created widget * @returns a widget to fetch metadata needed to import a project */ virtual VcsImportMetadataWidget* createImportMetadataWidget( QWidget* parent ) = 0; + /** + * Checks whether the given @p remoteLocation is a valid remote repository URL. + * + * If the URL is a local filesystem path, the folder will be checked + * if it contains proper repository content. + * For non-local filesystem URLs only the URL properties will be checked, + * no communication to any server is done. + * + * @param remoteLocation the URL used to access a remote repository + * @returns true if the the given @p remoteLocation seems valid for this version control system + */ + virtual bool isValidRemoteRepositoryUrl(const QUrl& remoteLocation) = 0; + /** * These methods rely on a valid vcs-directory with vcs-metadata in it. * * revisions can contain a date in format parseable by QDate, a number, * or the special words HEAD and BASE (whose exact meaning depends on the * used VCS system) */ /** * Is the given @p localLocation under version control? This checks whether * the @p localLocation is under control of the versioning system or not. * It does not only check whether the @p localLocation lies in a version * controlled directory * * @returns true if the version control system knows the given @p localLocation */ virtual bool isVersionControlled( const QUrl& localLocation ) = 0; /** * Get the repository location of a local file */ virtual VcsJob* repositoryLocation( const QUrl& localLocation ) = 0; /** * adds a local unversioned file or directory to the list of versioned files. * @param localLocations a list of files or directories that should be put under version control * @param recursion whether to add directories and their children or only directories themselves * @returns a job that executes the addition */ virtual VcsJob* add( const QList& localLocations, RecursionMode recursion = IBasicVersionControl::Recursive ) = 0; /** * removes a local versioned file or directory from the version control system * @param localLocations the list of files/directories to remove from the VCS * @returns a job that executes the removal */ virtual VcsJob* remove( const QList& localLocations ) = 0; /** * executes a copy of a file/dir, preserving history if the VCS system * allows that, may be implemented by filesystem copy+add */ virtual VcsJob* copy( const QUrl& localLocationSrc, const QUrl& localLocationDstn ) = 0; /** * moves src to dst, preserving history if the VCS system allows that, may * be implemented by copy+remove */ virtual VcsJob* move( const QUrl& localLocationSrc, const QUrl& localLocationDst ) = 0; /** * Fetches the status of the given local vcs locations. * * The returned VcsJob populates a QList that can be fetched * using fetchResults(). The QVariant inside the list wraps a * KDevelo::VcsStatusInfo object which contains all the relevant * information about the status of a specific file or folder */ virtual VcsJob* status( const QList& localLocations, RecursionMode recursion = IBasicVersionControl::Recursive ) = 0; /** * revert all local changes on the given file, making its content equal * to the version in the repository * unedit() (if not a no-op) is implied. */ virtual VcsJob* revert( const QList& localLocations, RecursionMode recursion = IBasicVersionControl::Recursive ) = 0; /** * fetches the latest changes from the repository, if there are * conflicts a merge needs to be executed separately * * @param localLocations the local files/dirs that should be updated * @param rev Update to this revision. The operation will fail if @p rev is * a range. * @param recursion defines whether the directories should be updated * recursively */ virtual VcsJob* update( const QList& localLocations, const VcsRevision& rev = VcsRevision::createSpecialRevision( VcsRevision::Head ), RecursionMode recursion = IBasicVersionControl::Recursive ) = 0; /** * Checks in the changes of the given file(s)/dir(s) into the repository */ virtual VcsJob* commit( const QString& message, const QList& localLocations, RecursionMode recursion = IBasicVersionControl::Recursive ) = 0; /** * Retrieves a diff between two revisions of a file * * The diff is in unified diff format for text files by default */ virtual VcsJob* diff( const QUrl& fileOrDirectory, const VcsRevision& srcRevision, const VcsRevision& dstRevision, VcsDiff::Type = VcsDiff::DiffUnified, IBasicVersionControl::RecursionMode recursion = IBasicVersionControl::Recursive ) = 0; /** * Retrieve the history of a given local url * * The resulting VcsJob will emit the resultsReady signal every time new * log events are available. The fetchResults method will return a QList * where the QVariant is a KDevelop::VcsEvent. * * @param rev List @p rev and earlier. * @param limit Restrict to the most recent @p limit entries. Note that the * limit is @e advisory and may be ignored. */ virtual VcsJob* log( const QUrl& localLocation, const VcsRevision& rev, unsigned long limit = 0 ) = 0; /** * Retrieve the history of a given local url * * The resulting VcsJob will emit the resultsReady signal every time new * log events are available. The fetchResults method will return a QList * where the QVariant is a KDevelop::VcsEvent. * * @param rev List @p rev and earlier. The default is BASE. * @param limit Do not show entries earlier than @p limit. Note that the * limit is @e advisory and may be ignored. The default is the first revision. */ virtual VcsJob* log( const QUrl& localLocation, const VcsRevision& rev = VcsRevision::createSpecialRevision( VcsRevision::Base ), const VcsRevision& limit= VcsRevision::createSpecialRevision( VcsRevision::Start ) ) = 0; /** * Annotate each line of the given local url at the given revision * with information about who changed it and when. * * The job should return a QList where the QVariant wraps * a KDevelop::VcsAnnotationLine. The instance contains all information * needed for the caller to construct a KDevelop::VcsAnnotation * * @see KDevelop::VcsAnnotation * @see KDevelop::VcsAnnotationLine * * @param localLocation local file that should be annotated. * @param rev Revision that should be annotated. */ virtual VcsJob* annotate( const QUrl& localLocation, const VcsRevision& rev = VcsRevision::createSpecialRevision( VcsRevision::Head ) ) = 0; /** * check for conflicts in the given file and eventually present a * conflict solving dialog to the user */ virtual VcsJob* resolve( const QList& localLocations, RecursionMode recursion ) = 0; /** * Creates a working copy of the latest revision of the repository sourceRepository in * the newly created directory destinationDirectory. * In the case of a decentralized VCSs, there is no working copy without repository, * therefor checkout() includes the creation of a local copy of the repository. * * @param sourceRepository source of the checkout * @param destinationDirectory location of the created working copy (local repository) */ virtual VcsJob* createWorkingCopy(const VcsLocation & sourceRepository, const QUrl & destinationDirectory, RecursionMode recursion = IBasicVersionControl::Recursive) = 0; virtual VcsLocationWidget* vcsLocation(QWidget* parent) const=0; /** * Optionally apply VCS specific settings to the commit message editor. * Overwrite this to set e.g. a default commit message or to setup highlighting or validation. * The default implementation does nothing. */ virtual void setupCommitMessageEditor(const QUrl&, KTextEdit* edit) const; }; } Q_DECLARE_INTERFACE( KDevelop::IBasicVersionControl, "org.kdevelop.IBasicVersionControl" ) #endif diff --git a/vcs/widgets/standardvcslocationwidget.cpp b/vcs/widgets/standardvcslocationwidget.cpp index 881be34705..a751c2e4ca 100644 --- a/vcs/widgets/standardvcslocationwidget.cpp +++ b/vcs/widgets/standardvcslocationwidget.cpp @@ -1,64 +1,74 @@ /* KDevPlatform Vcs Support * * Copyright 2010 Aleix Pol * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #include "standardvcslocationwidget.h" #include #include #include #include using namespace KDevelop; StandardVcsLocationWidget::StandardVcsLocationWidget(QWidget* parent, Qt::WindowFlags f) : VcsLocationWidget(parent, f) { setLayout(new QVBoxLayout(this)); m_urlWidget = new KUrlRequester(this); m_urlWidget->setPlaceholderText(i18n("Enter the repository URL...")); layout()->addWidget(m_urlWidget); connect(m_urlWidget, &KUrlRequester::textChanged, this, &StandardVcsLocationWidget::textChanged); } +void StandardVcsLocationWidget::setLocation(const QUrl& remoteLocation) +{ + setUrl(remoteLocation); +} + +void StandardVcsLocationWidget::setUrl(const QUrl& url) +{ + m_urlWidget->setUrl(url); +} + QUrl StandardVcsLocationWidget::url() const { return m_urlWidget->url(); } VcsLocation StandardVcsLocationWidget::location() const { VcsLocation v(m_urlWidget->url()); return v; } bool StandardVcsLocationWidget::isCorrect() const { return !m_urlWidget->url().isEmpty(); } void StandardVcsLocationWidget::textChanged(const QString& /*str*/) { emit changed(); } QString KDevelop::StandardVcsLocationWidget::projectName() const { return m_urlWidget->url().fileName(); } diff --git a/vcs/widgets/standardvcslocationwidget.h b/vcs/widgets/standardvcslocationwidget.h index 71c2d32c4f..40b2039e31 100644 --- a/vcs/widgets/standardvcslocationwidget.h +++ b/vcs/widgets/standardvcslocationwidget.h @@ -1,50 +1,52 @@ /* KDevPlatform Vcs Support * * Copyright 2010 Aleix Pol * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #ifndef KDEVPLATFORM_STANDARDVCSLOCATIONWIDGET_H #define KDEVPLATFORM_STANDARDVCSLOCATIONWIDGET_H #include #include class QUrl; class KUrlRequester; namespace KDevelop { class KDEVPLATFORMVCS_EXPORT StandardVcsLocationWidget : public VcsLocationWidget { Q_OBJECT public: explicit StandardVcsLocationWidget(QWidget* parent = nullptr, Qt::WindowFlags f = nullptr); VcsLocation location() const override; bool isCorrect() const override; QUrl url() const; QString projectName() const override; - + void setLocation(const QUrl& remoteLocation) override; + void setUrl(const QUrl& url); + public slots: void textChanged(const QString& str); private: KUrlRequester* m_urlWidget; }; } #endif // KDEVPLATFORM_STANDARDVCSLOCATIONWIDGET_H diff --git a/vcs/widgets/vcslocationwidget.h b/vcs/widgets/vcslocationwidget.h index 62de53c962..aeb2de85c8 100644 --- a/vcs/widgets/vcslocationwidget.h +++ b/vcs/widgets/vcslocationwidget.h @@ -1,61 +1,67 @@ /* This file is part of KDevelop * * Copyright 2010 Aleix Pol Gonzalez * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #ifndef KDEVPLATFORM_VCSLOCATIONWIDGET_H #define KDEVPLATFORM_VCSLOCATIONWIDGET_H #include #include namespace KDevelop { class VcsLocation; /** * Provides a widget to be used to ask the user for a VersionControl location. * * Every VCS plugin will provide their own to be able to construct VcsLocations * from the UI in a VCS-dependent fashion. */ class KDEVPLATFORMVCS_EXPORT VcsLocationWidget : public QWidget { Q_OBJECT public: explicit VcsLocationWidget(QWidget* parent = nullptr, Qt::WindowFlags f = nullptr); /** @returns the VcsLocation specified in the widget. */ virtual VcsLocation location() const=0; /** @returns whether we have a correct location in the widget. */ virtual bool isCorrect() const=0; /** @returns a proposed project name to be used as a hint for an identifier * for the VcsLocation. */ virtual QString projectName() const=0; - + + /** Sets the location by a respective URL + * @param remoteLocation the URL used to access a remote repository + * @see IBasicVersionControl::isValidRemoteRepositoryUrl + */ + virtual void setLocation(const QUrl& remoteLocation) = 0; + signals: void changed(); }; } #endif