File indexing completed on 2024-04-14 04:29:53

0001 /***************************************************************************
0002  *   This file was taken from KDevelop's git plugin                        *
0003  *   Copyright 2008 Evgeniy Ivanov <powerfox@kde.ru>                       *
0004  *                                                                         *
0005  *   Adapted for Mercurial                                                 *
0006  *   Copyright 2009 Fabian Wiesel <fabian.wiesel@fu-berlin.de>             *
0007  *   Copyright 2011 Andrey Batyiev <batyiev@gmail.com>                     *
0008  *                                                                         *
0009  *   This program is free software; you can redistribute it and/or         *
0010  *   modify it under the terms of the GNU General Public License as        *
0011  *   published by the Free Software Foundation; either version 2 of        *
0012  *   the License or (at your option) version 3 or any later version        *
0013  *   accepted by the membership of KDE e.V. (or its successor approved     *
0014  *   by the membership of KDE e.V.), which shall act as a proxy            *
0015  *   defined in Section 14 of version 3 of the license.                    *
0016  *                                                                         *
0017  *   This program is distributed in the hope that it will be useful,       *
0018  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0019  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0020  *   GNU General Public License for more details.                          *
0021  *                                                                         *
0022  *   You should have received a copy of the GNU General Public License     *
0023  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
0024  ***************************************************************************/
0025 
0026 #include "mercurialplugin.h"
0027 
0028 #include <algorithm>
0029 #include <memory>
0030 
0031 #include <QDir>
0032 #include <QDateTime>
0033 #include <QFileInfo>
0034 #include <QAction>
0035 #include <QMenu>
0036 #include <QDateTime>
0037 #include <QStandardPaths>
0038 #include <QTimer>
0039 
0040 #include <KDirWatch>
0041 
0042 #include <interfaces/icore.h>
0043 
0044 #include <vcs/vcsjob.h>
0045 #include <vcs/vcsevent.h>
0046 #include <vcs/vcsrevision.h>
0047 #include <vcs/vcsannotation.h>
0048 #include <vcs/vcslocation.h>
0049 #include <vcs/dvcs/dvcsjob.h>
0050 
0051 #include <util/path.h>
0052 
0053 #include "mercurialjob.h"
0054 #include "mercurialvcslocationwidget.h"
0055 #include "mercurialannotatejob.h"
0056 #include "mercurialpushjob.h"
0057 #include "ui/mercurialheadswidget.h"
0058 #include "ui/mercurialqueuesmanager.h"
0059 
0060 #include "debug.h"
0061 
0062 using namespace KDevelop;
0063 
0064 namespace
0065 {
0066     QString logTemplate()
0067     {
0068         return QStringLiteral("{desc}\\_%{date|rfc3339date}\\_%{author}\\_%{parents}\\_%{node}\\_%{rev}\\_%{file_dels}\\_%{file_adds}\\_%{file_mods}\\_%{file_copies}\\_%");
0069     }
0070 }
0071 
0072 MercurialPlugin::MercurialPlugin(QObject *parent, const QVariantList &)
0073     : DistributedVersionControlPlugin(parent, QStringLiteral("kdevmercurial"))
0074 {
0075     if (QStandardPaths::findExecutable(QStringLiteral("hg")).isEmpty()) {
0076         setErrorDescription(i18n("Unable to find hg executable. Is it installed on the system?"));
0077         return;
0078     }
0079 
0080     m_headsAction = new QAction(i18n("Heads..."), this);
0081     m_mqNew = new QAction(i18nc("mercurial queues submenu", "New..."), this);
0082     m_mqPushAction = new QAction(i18nc("mercurial queues submenu", "Push"), this);
0083     m_mqPushAllAction = new QAction(i18nc("mercurial queues submenu", "Push All"), this);
0084     m_mqPopAction = new QAction(i18nc("mercurial queues submenu", "Pop"), this);
0085     m_mqPopAllAction = new QAction(i18nc("mercurial queues submenu", "Pop All"), this);
0086     m_mqManagerAction = new QAction(i18nc("mercurial queues submenu", "Manager..."), this);
0087 
0088     connect(m_headsAction, &QAction::triggered, this, &MercurialPlugin::showHeads);
0089     connect(m_mqManagerAction, &QAction::triggered, this, &MercurialPlugin::showMercurialQueuesManager);
0090 
0091     m_watcher = new KDirWatch(this);
0092     connect(m_watcher, &KDirWatch::dirty, this, &MercurialPlugin::fileChanged);
0093     connect(m_watcher, &KDirWatch::created, this, &MercurialPlugin::fileChanged);
0094 }
0095 
0096 MercurialPlugin::~MercurialPlugin()
0097 {
0098 }
0099 
0100 QString MercurialPlugin::name() const
0101 {
0102     return QLatin1String("Mercurial");
0103 }
0104 
0105 void MercurialPlugin::fileChanged(const QString& file)
0106 {
0107     Q_ASSERT(file.endsWith(QStringLiteral("branch")));
0108     const QUrl repoUrl = Path(file).parent().parent().toUrl();
0109 
0110     m_branchesChange.append(repoUrl);
0111     QTimer::singleShot(1000, this, [this](){emit repositoryBranchChanged(m_branchesChange.takeFirst());});
0112 }
0113 
0114 bool MercurialPlugin::isValidDirectory(const QUrl &directory)
0115 {
0116     // Mercurial uses the same test, so we don't lose any functionality
0117     static const QString hgDir(".hg");
0118 
0119     if (m_lastRepoRoot.isParentOf(directory))
0120         return true;
0121 
0122     const QString initialPath(directory.adjusted(QUrl::StripTrailingSlash).toLocalFile());
0123     const QFileInfo finfo(initialPath);
0124     QDir dir;
0125     if (finfo.isFile()) {
0126         dir = finfo.absoluteDir();
0127     } else {
0128         dir = QDir(initialPath);
0129         dir.makeAbsolute();
0130     }
0131 
0132     while (!dir.cd(hgDir) && dir.cdUp())
0133     {} // cdUp, until there is a sub-directory called .hg
0134 
0135     if (hgDir != dir.dirName())
0136         return false;
0137 
0138     dir.cdUp(); // Leave .hg
0139     m_lastRepoRoot.setPath(dir.absolutePath());
0140     return true;
0141 }
0142 
0143 bool MercurialPlugin::isValidRemoteRepositoryUrl(const QUrl& remoteLocation)
0144 {
0145     Q_UNUSED(remoteLocation);
0146     // TODO
0147     return false;
0148 }
0149 
0150 bool MercurialPlugin::isVersionControlled(const QUrl &url)
0151 {
0152     const QFileInfo fsObject(url.toLocalFile());
0153 
0154     if (!fsObject.isFile()) {
0155         return isValidDirectory(url);
0156     }
0157 
0158     DVcsJob *job = static_cast<DVcsJob *>(status({url}, Recursive));
0159     if (!job->exec()) {
0160         return false;
0161     }
0162 
0163     QList<QVariant> statuses = qvariant_cast<QList<QVariant> >(job->fetchResults());
0164     VcsStatusInfo info = qvariant_cast< VcsStatusInfo >(statuses.first());
0165     if (info.state() == VcsStatusInfo::ItemAdded ||
0166             info.state() == VcsStatusInfo::ItemModified ||
0167             info.state() == VcsStatusInfo::ItemUpToDate) {
0168         return true;
0169     }
0170 
0171     return false;
0172 }
0173 
0174 VcsJob *MercurialPlugin::init(const QUrl &directory)
0175 {
0176     DVcsJob *job = new DVcsJob(directory.path(), this);
0177 
0178     *job << "hg" << "init";
0179 
0180     return job;
0181 }
0182 
0183 VcsJob *MercurialPlugin::repositoryLocation(const QUrl &/*directory*/)
0184 {
0185     return nullptr;
0186 }
0187 
0188 VcsJob *MercurialPlugin::createWorkingCopy(const VcsLocation &localOrRepoLocationSrc, const QUrl &destinationDirectory, IBasicVersionControl::RecursionMode)
0189 {
0190     DVcsJob *job = new DVcsJob(QDir::home(), this, KDevelop::OutputJob::Silent);
0191 
0192     *job << "hg" << "clone" << "--" << localOrRepoLocationSrc.localUrl().toLocalFile() << destinationDirectory.toLocalFile();
0193 
0194     return job;
0195 }
0196 
0197 VcsJob *MercurialPlugin::pull(const VcsLocation &otherRepository, const QUrl &workingRepository)
0198 {
0199     DVcsJob *job = new DVcsJob(findWorkingDir(workingRepository), this);
0200 
0201     *job << "hg" << "pull" << "--";
0202 
0203     QString pathOrUrl = otherRepository.localUrl().toLocalFile();
0204 
0205     if (!pathOrUrl.isEmpty())
0206         *job << pathOrUrl;
0207 
0208     return job;
0209 }
0210 
0211 VcsJob *MercurialPlugin::push(const QUrl &workingRepository, const VcsLocation &otherRepository)
0212 {
0213     return new MercurialPushJob(findWorkingDir(workingRepository), otherRepository.localUrl(), this);
0214 }
0215 
0216 VcsJob *MercurialPlugin::add(const QList<QUrl> &localLocations, IBasicVersionControl::RecursionMode recursion)
0217 {
0218     QList<QUrl> locations = localLocations;
0219 
0220     if (recursion == NonRecursive) {
0221         filterOutDirectories(locations);
0222     }
0223 
0224     if (locations.empty()) {
0225         // nothing left after filtering
0226         return nullptr;
0227     }
0228 
0229     DVcsJob *job = new DVcsJob(findWorkingDir(locations.first()), this);
0230     *job << "hg" << "add" << "--" << locations;
0231     return job;
0232 }
0233 
0234 VcsJob *MercurialPlugin::copy(const QUrl &localLocationSrc, const QUrl &localLocationDst)
0235 {
0236     DVcsJob *job = new DVcsJob(findWorkingDir(localLocationSrc), this, KDevelop::OutputJob::Silent);
0237     *job << "hg" << "cp" << "--" << localLocationSrc.toLocalFile() << localLocationDst.path();
0238     return job;
0239 }
0240 
0241 VcsJob *MercurialPlugin::move(const QUrl &localLocationSrc,
0242                               const QUrl &localLocationDst)
0243 {
0244     DVcsJob *job = new DVcsJob(findWorkingDir(localLocationSrc), this, KDevelop::OutputJob::Silent);
0245     *job << "hg" << "mv" << "--" << localLocationSrc.toLocalFile() << localLocationDst.path();
0246     return job;
0247 }
0248 
0249 //If no files specified then commit already added files
0250 VcsJob *MercurialPlugin::commit(const QString &message,
0251                                 const QList<QUrl> &localLocations,
0252                                 IBasicVersionControl::RecursionMode recursion)
0253 {
0254     QList<QUrl> locations = localLocations;
0255 
0256     if (recursion == NonRecursive) {
0257         filterOutDirectories(locations);
0258     }
0259 
0260     if (locations.empty() || message.isEmpty()) {
0261         return nullptr;
0262     }
0263 
0264     DVcsJob *job = new DVcsJob(findWorkingDir(locations.first()), this, KDevelop::OutputJob::Silent);
0265     *job << "hg" << "commit" << "-m" << message << "--" << locations;
0266     return job;
0267 }
0268 
0269 VcsJob *MercurialPlugin::update(const QList<QUrl> &localLocations,
0270                                 const VcsRevision &rev,
0271                                 IBasicVersionControl::RecursionMode recursion)
0272 {
0273     if (rev.revisionType() == VcsRevision::Special &&
0274             rev.revisionValue().value<VcsRevision::RevisionSpecialType>() == VcsRevision::Head) {
0275         return pull(VcsLocation(), QUrl::fromLocalFile(findWorkingDir(localLocations.first()).path()));
0276     }
0277 
0278     QList<QUrl> locations = localLocations;
0279 
0280     if (recursion == NonRecursive) {
0281         filterOutDirectories(locations);
0282     }
0283 
0284     if (locations.empty()) {
0285         return nullptr;
0286     }
0287 
0288     DVcsJob *job = new DVcsJob(findWorkingDir(locations.first()), this);
0289 
0290 
0291     *job << "hg" << "revert" << "-r" << toMercurialRevision(rev) << "--" << locations;
0292     return job;
0293 }
0294 
0295 VcsJob *MercurialPlugin::resolve(const QList<QUrl>   &files, KDevelop::IBasicVersionControl::RecursionMode recursion)
0296 {
0297     QList<QUrl> fileList = files;
0298     if (recursion == NonRecursive) {
0299         filterOutDirectories(fileList);
0300     }
0301 
0302     if (fileList.empty()) {
0303         return nullptr;
0304     }
0305 
0306     DVcsJob *job = new DVcsJob(findWorkingDir(fileList.first()), this);
0307 
0308     //Note: the message is quoted somewhere else, so if we quote here then we have quotes in the commit log
0309     *job << "hg" << "resolve" << "--" << fileList;
0310 
0311     return job;
0312 }
0313 
0314 VcsJob *MercurialPlugin::diff(const QUrl &fileOrDirectory,
0315                               const VcsRevision &srcRevision,
0316                               const VcsRevision &dstRevision,
0317                               IBasicVersionControl::RecursionMode recursionMode)
0318 {
0319     if (!fileOrDirectory.isLocalFile()) {
0320         return nullptr;
0321     }
0322 
0323     // non-standard searching for working directory
0324     QString workingDir;
0325     QFileInfo fileInfo(fileOrDirectory.toLocalFile());
0326 
0327     // It's impossible to non-recursively diff directories
0328     if (recursionMode == NonRecursive && fileInfo.isDir()) {
0329         return nullptr;
0330     }
0331 
0332     // find out correct working directory
0333     if (fileInfo.isFile()) {
0334         workingDir = fileInfo.absolutePath();
0335     } else {
0336         workingDir = fileInfo.absoluteFilePath();
0337     }
0338 
0339     QString srcRev = toMercurialRevision(srcRevision);
0340     QString dstRev = toMercurialRevision(dstRevision);
0341 
0342     qCDebug(PLUGIN_MERCURIAL) << "Diff between" << srcRevision.prettyValue() << '(' << srcRev << ')' << "and" << dstRevision.prettyValue() << '(' << dstRev << ')' << " requested";
0343 
0344     if (
0345         (srcRev.isNull() && dstRev.isNull()) ||
0346         (srcRev.isEmpty() && dstRev.isEmpty())
0347     ) {
0348         qWarning() << "Diff between" << srcRevision.prettyValue() << '(' << srcRev << ')' << "and" << dstRevision.prettyValue() << '(' << dstRev << ')' << " not possible";
0349         return nullptr;
0350     }
0351 
0352     const QString srcPath = fileOrDirectory.toLocalFile();
0353 
0354     DVcsJob *job = new DVcsJob(workingDir, this, KDevelop::OutputJob::Silent);
0355 
0356     *job << "hg" << "diff" << "-g";
0357 
0358     // Hg cannot do a diff from
0359     //   SomeRevision to Previous, or
0360     //   Working to SomeRevsion directly, but the reverse
0361     if (dstRev.isNull() // Destination is "Previous"
0362             || (!srcRev.isNull() && srcRev.isEmpty()) // Source is "Working"
0363        ) {
0364         std::swap(srcRev, dstRev);
0365         *job << "--reverse";
0366     }
0367 
0368     // unified diff
0369     *job << "-U" << "3";    // Default from GNU diff
0370 
0371     if (srcRev.isNull() /* from "Previous" */ && dstRev.isEmpty() /* to "Working" */) {
0372         // Do nothing, that is the default
0373     } else if (srcRev.isNull()) { // Changes made in one arbitrary revision
0374         *job << "-c" << dstRev;
0375     } else {
0376         *job << "-r" << srcRev;
0377         if (!dstRev.isEmpty()) {
0378             *job << "-r" << dstRev;
0379         }
0380     }
0381 
0382     *job << "--" << srcPath;
0383 
0384     connect(job, &DVcsJob::readyForParsing, this, &MercurialPlugin::parseDiff);
0385 
0386     return job;
0387 }
0388 
0389 VcsJob *MercurialPlugin::remove(const QList<QUrl> &files)
0390 {
0391     if (files.empty()) {
0392         return nullptr;
0393     }
0394 
0395     DVcsJob *job = new DVcsJob(findWorkingDir(files.first()), this);
0396     *job << "hg" << "rm" << "--" << files;
0397     return job;
0398 }
0399 
0400 VcsJob *MercurialPlugin::status(const QList<QUrl> &localLocations, IBasicVersionControl::RecursionMode recursion)
0401 {
0402     QList<QUrl> locations = localLocations;
0403     if (recursion == NonRecursive) {
0404         filterOutDirectories(locations);
0405     }
0406 
0407     if (locations.empty()) {
0408         return nullptr;
0409     }
0410 
0411     DVcsJob *job = new DVcsJob(findWorkingDir(locations.first()), this, KDevelop::OutputJob::Silent);
0412     *job << "hg" << "status" << "-A" << "--" << locations;
0413 
0414     connect(job, &DVcsJob::readyForParsing, this, &MercurialPlugin::parseStatus);
0415 
0416     return job;
0417 }
0418 
0419 bool MercurialPlugin::parseStatus(DVcsJob *job) const
0420 {
0421     if (job->status() != VcsJob::JobSucceeded) {
0422         qCDebug(PLUGIN_MERCURIAL) << "Job failed: " << job->output();
0423         return false;
0424     }
0425 
0426     const QString dir = job->directory().absolutePath().append(QDir::separator());
0427     qCDebug(PLUGIN_MERCURIAL) << "Job succeeded for " << dir;
0428     const QStringList output = job->output().split('\n', QString::SkipEmptyParts);
0429     QList<QVariant> filestatus;
0430 
0431     QSet<QUrl> conflictedFiles;
0432 
0433     QStringList::const_iterator it = output.constBegin();
0434 
0435     // FIXME: Revive this functionality and add tests for it!
0436     // conflicts first
0437 //     for (; it != output.constEnd(); it++) {
0438 //         QChar stCh = it->at(0);
0439 //         if (stCh == '%') {
0440 //             it++;
0441 //             break;
0442 //         }
0443 // 
0444 //         QUrl file = QUrl::fromLocalFile(it->mid(2).prepend(dir));
0445 // 
0446 //         VcsStatusInfo status;
0447 //         status.setUrl(file);
0448 //         // FIXME: conflicts resolved
0449 //         status.setState(VcsStatusInfo::ItemHasConflicts);
0450 // 
0451 //         conflictedFiles.insert(file);
0452 //         filestatus.append(qVariantFromValue(status));
0453 //     }
0454 
0455     // standard statuses next
0456     for (; it != output.constEnd(); it++) {
0457         QChar stCh = it->at(0);
0458 
0459         QUrl file = QUrl::fromLocalFile(it->mid(2).prepend(dir));
0460 
0461         if (!conflictedFiles.contains(file)) {
0462             VcsStatusInfo status;
0463             status.setUrl(file);
0464             status.setState(charToState(stCh.toLatin1()));
0465             filestatus.append(qVariantFromValue(status));
0466         }
0467     }
0468 
0469     job->setResults(qVariantFromValue(filestatus));
0470     return true;
0471 }
0472 
0473 VcsJob *MercurialPlugin::revert(const QList<QUrl> &localLocations,
0474                                 IBasicVersionControl::RecursionMode recursion)
0475 {
0476     QList<QUrl> locations = localLocations;
0477     if (recursion == NonRecursive) {
0478         filterOutDirectories(locations);
0479     }
0480 
0481     if (locations.empty()) {
0482         return nullptr;
0483     }
0484 
0485     DVcsJob *job = new DVcsJob(findWorkingDir(locations.first()), this);
0486     *job << "hg" << "revert" << "--" << locations;
0487     return job;
0488 }
0489 
0490 VcsJob *MercurialPlugin::log(const QUrl &localLocation,
0491                              const VcsRevision &rev,
0492                              unsigned long limit)
0493 {
0494     return log(localLocation, VcsRevision::createSpecialRevision(VcsRevision::Start), rev, limit);
0495 }
0496 
0497 VcsJob *MercurialPlugin::log(const QUrl &localLocation,
0498                              const VcsRevision &rev,
0499                              const VcsRevision &limit)
0500 {
0501     return log(localLocation, rev, limit, 0);
0502 }
0503 
0504 
0505 VcsJob *MercurialPlugin::log(const QUrl &localLocation,
0506                              const VcsRevision &to,
0507                              const VcsRevision &from,
0508                              unsigned long limit)
0509 {
0510     DVcsJob *job = new DVcsJob(findWorkingDir(localLocation), this, KDevelop::OutputJob::Silent);
0511 
0512     *job << "hg" << "log" << "-r" << toMercurialRevision(from) + ':' + toMercurialRevision(to);
0513 
0514     if (limit > 0)
0515         *job << "-l" << QString::number(limit);
0516 
0517     *job << "--template"
0518          << logTemplate() << "--" << localLocation;
0519 
0520     connect(job, &DVcsJob::readyForParsing, this,
0521             &MercurialPlugin::parseLogOutputBasicVersionControl);
0522     return job;
0523 }
0524 
0525 VcsJob *MercurialPlugin::annotate(const QUrl &localLocation,
0526                                   const VcsRevision &rev)
0527 {
0528     return new MercurialAnnotateJob(findWorkingDir(localLocation), rev, localLocation, this);
0529 }
0530 
0531 class MergeBranchJob : public MercurialJob
0532 {
0533 public:
0534     MergeBranchJob(const QDir &workingDir, const QString& branchName, MercurialPlugin* parent)
0535     : MercurialJob(workingDir, parent, JobType::Merge),
0536     m_branchName(branchName)
0537     {}
0538 
0539     void start() override
0540     {
0541         m_status = JobRunning;
0542 
0543         auto job = new DVcsJob(m_workingDir, vcsPlugin(), KDevelop::OutputJob::Silent);
0544         *job << "hg" << "log" << "-b" << m_branchName << "-l" << "1" << "--template" << "{rev}";
0545 
0546         connect(job, &DVcsJob::resultsReady, this, [this](VcsJob* j) {
0547             auto job = static_cast<DVcsJob *>(j);
0548             if (job->status() != VcsJob::JobSucceeded || job->output().isEmpty())
0549                 return setFail();
0550 
0551             auto mergeJob = new DVcsJob(m_workingDir, vcsPlugin());
0552 
0553             *mergeJob << "hg" << "merge" << "-r" << job->output().trimmed();
0554 
0555             connect(mergeJob, &KJob::finished, this, [this](KJob *job) {
0556                 if (job->error()) {
0557                     setFail();
0558                 }
0559             });
0560             connect(mergeJob, &DVcsJob::resultsReady, this, [this](VcsJob* j) {
0561                 auto job = static_cast<DVcsJob *>(j);
0562                 if (job->status() != VcsJob::JobSucceeded || job->output().isEmpty())
0563                     return setFail();
0564                 setSuccess();
0565             });
0566 
0567             m_job = mergeJob;
0568             mergeJob->start();
0569         });
0570         connect(job, &KJob::finished, this, [this](KJob *job) {
0571             if (job->error()) {
0572                 setFail();
0573             }
0574         });
0575         m_job = job;
0576         job->start();
0577     }
0578 
0579 private:
0580     QString m_branchName;
0581 };
0582 
0583 KDevelop::VcsJob* MercurialPlugin::mergeBranch(const QUrl& repository, const QString& branchName)
0584 {
0585     Q_ASSERT(!branchName.isEmpty());
0586 
0587     return new MergeBranchJob(findWorkingDir(repository), branchName, this);
0588 }
0589 
0590 VcsJob *MercurialPlugin::heads(const QUrl &localLocation)
0591 {
0592     DVcsJob *job = new DVcsJob(findWorkingDir(localLocation), this, KDevelop::OutputJob::Silent);
0593 
0594     *job << "hg" << "heads" << "--template" << logTemplate();
0595 
0596     connect(job, &DVcsJob::readyForParsing, this,
0597             &MercurialPlugin::parseLogOutputBasicVersionControl);
0598     return job;
0599 }
0600 
0601 VcsJob *MercurialPlugin::identify(const QUrl &localLocation)
0602 {
0603     DVcsJob *job = new DVcsJob(findWorkingDir(localLocation), this, KDevelop::OutputJob::Silent);
0604 
0605     *job << "hg" << "identify" << "-n";
0606 
0607     connect(job, &DVcsJob::readyForParsing, this,
0608             &MercurialPlugin::parseIdentify);
0609     return job;
0610 }
0611 
0612 VcsJob *MercurialPlugin::checkoutHead(const QUrl &localLocation, const KDevelop::VcsRevision &rev)
0613 {
0614     DVcsJob *job = new DVcsJob(findWorkingDir(localLocation), this);
0615 
0616     *job << "hg" << "update" << "-C" << toMercurialRevision(rev);
0617 
0618     return job;
0619 }
0620 
0621 VcsJob *MercurialPlugin::mergeWith(const QUrl &localLocation, const KDevelop::VcsRevision &rev)
0622 {
0623     DVcsJob *job = new DVcsJob(findWorkingDir(localLocation), this);
0624 
0625     *job << "hg" << "merge" << "-r" << toMercurialRevision(rev);
0626 
0627     return job;
0628 }
0629 
0630 VcsJob *MercurialPlugin::branch(const QUrl &repository, const VcsRevision &/*rev*/, const QString &branchName)
0631 {
0632     DVcsJob *job = new DVcsJob(findWorkingDir(repository), this, OutputJob::Silent);
0633     *job << "hg" << "branch" << "--" << branchName;
0634     return job;
0635 }
0636 
0637 VcsJob *MercurialPlugin::branches(const QUrl &repository)
0638 {
0639     DVcsJob *job = new DVcsJob(findWorkingDir(repository), this, OutputJob::Silent);
0640     *job << "hg" << "branches";
0641     connect(job, &DVcsJob::readyForParsing, this, &MercurialPlugin::parseMultiLineOutput);
0642     return job;
0643 }
0644 
0645 VcsJob *MercurialPlugin::currentBranch(const QUrl &repository)
0646 {
0647     DVcsJob *job = new DVcsJob(findWorkingDir(repository), this, OutputJob::Silent);
0648     *job << "hg" << "branch";
0649     connect(job, &DVcsJob::readyForParsing, this, &MercurialPlugin::parseMultiLineOutput);
0650     return job;
0651 }
0652 
0653 VcsJob *MercurialPlugin::deleteBranch(const QUrl &/*repository*/, const QString &/*branchName*/)
0654 {
0655     // Not supported
0656     return nullptr;
0657 }
0658 
0659 VcsJob *MercurialPlugin::renameBranch(const QUrl &/*repository*/, const QString &/*oldBranchName*/, const QString &/*newBranchName*/)
0660 {
0661     // Not supported
0662     return nullptr;
0663 }
0664 
0665 VcsJob *MercurialPlugin::switchBranch(const QUrl &repository, const QString &branchName)
0666 {
0667     DVcsJob *job = new DVcsJob(findWorkingDir(repository), this);
0668     *job << "hg" << "update" << "--" << branchName;
0669     return job;
0670 }
0671 
0672 VcsJob *MercurialPlugin::tag(const QUrl &repository, const QString &commitMessage, const VcsRevision &rev, const QString &tagName)
0673 {
0674     DVcsJob *job = new DVcsJob(findWorkingDir(repository), this);
0675     *job << "hg" << "tag" << "-m" << commitMessage << "-r" << toMercurialRevision(rev) << "--" << tagName;
0676     return job;
0677 }
0678 
0679 QVector<DVcsEvent> MercurialPlugin::allCommits(const QString& repo)
0680 {
0681     DVcsJob *job = new DVcsJob(findWorkingDir(QUrl::fromLocalFile(repo)), this, OutputJob::Silent);
0682 
0683     *job << "hg" << "log" << "--template" << "{desc}\\_%{date|isodate}\\_%{author}\\_%{parents}\\_%{node}\\_%{rev}\\_%";
0684 
0685     if (!job->exec() || job->status() != VcsJob::JobSucceeded)
0686         return QVector<DVcsEvent>();
0687 
0688     QVector<DVcsEvent> commits;
0689 
0690     parseLogOutput(job, commits);
0691     return commits;
0692 }
0693 
0694 void MercurialPlugin::parseMultiLineOutput(DVcsJob *job) const
0695 {
0696     qCDebug(PLUGIN_MERCURIAL) << job->output();
0697     job->setResults(job->output().split('\n', QString::SkipEmptyParts));
0698 }
0699 
0700 void MercurialPlugin::parseDiff(DVcsJob *job)
0701 {
0702     if (job->status() != VcsJob::JobSucceeded) {
0703         qCDebug(PLUGIN_MERCURIAL) << "Parse-job failed: " << job->output();
0704         return;
0705     }
0706 
0707     // Diffs are generated relativly to the repository root,
0708     // so we have recover it from the job.
0709     // Not quite clean m_lastRepoRoot holds the root, after querying isValidDirectory()
0710     QString workingDir(job->directory().absolutePath());
0711     isValidDirectory(QUrl::fromLocalFile(workingDir));
0712     QString repoRoot = m_lastRepoRoot.adjusted(QUrl::StripTrailingSlash).path();
0713 
0714     VcsDiff diff;
0715 
0716     // We have to recover the type of the diff somehow.
0717     QString output = job->output();
0718 
0719     // hg diff adds a prefix to the paths, which we will now strip
0720     QRegExp reg("(diff [^\n]+\n--- )a(/[^\n]+\n\\+\\+\\+ )b(/[^\n]+\n)");
0721     QString replacement("\\1" + repoRoot + "\\2" + repoRoot + "\\3");
0722     output.replace(reg, replacement);
0723 
0724     diff.setDiff(output);
0725     diff.setBaseDiff(QUrl::fromLocalFile(repoRoot));
0726     diff.setDepth(0);
0727 
0728     job->setResults(qVariantFromValue(diff));
0729 }
0730 
0731 void MercurialPlugin::parseLogOutputBasicVersionControl(DVcsJob *job) const
0732 {
0733     QList<QVariant> events;
0734     static unsigned int entriesPerCommit = 10;
0735     auto items = job->output().split("\\_%");
0736 
0737     qCDebug(PLUGIN_MERCURIAL) << items;
0738     /* remove trailing \\_% */
0739     items.removeLast();
0740 
0741     if ((items.count() % entriesPerCommit) != 0) {
0742         qCDebug(PLUGIN_MERCURIAL) << "Cannot parse commit log: unexpected number of entries";
0743         return;
0744     }
0745 
0746     /* get revision data from items.
0747      * because there is continuous stream of \\_% separated strings,
0748      * incrementation will occur inside loop body
0749      *
0750      * "{desc}\\_%{date|rfc3339date}\\_%{author}\\_%{parents}\\_%{node}\\_%{rev}\\_%"
0751      * "{file_dels}\\_%{file_adds}\\_%{file_mods}\\_%{file_copies}\\_%'
0752      */
0753     for (auto it = items.constBegin(); it != items.constEnd();) {
0754         QString desc = *it++;
0755         qCDebug(PLUGIN_MERCURIAL) << desc;
0756         Q_ASSERT(!desc.isEmpty());
0757         QString date = *it++;
0758         qCDebug(PLUGIN_MERCURIAL) << date;
0759         Q_ASSERT(!date.isEmpty());
0760         QString author = *it++;
0761         qCDebug(PLUGIN_MERCURIAL) << author;
0762         Q_ASSERT(!author.isEmpty());
0763         QString parents = *it++;
0764         QString node = *it++;
0765         QString rev = *it++;
0766         qCDebug(PLUGIN_MERCURIAL) << rev;
0767         Q_ASSERT(!rev.isEmpty());
0768 
0769         VcsEvent event;
0770         event.setMessage(desc);
0771         event.setDate(QDateTime::fromString(date, Qt::ISODate));
0772         event.setAuthor(author);
0773 
0774         VcsRevision revision;
0775         revision.setRevisionValue(rev.toLongLong(), KDevelop::VcsRevision::GlobalNumber);
0776         event.setRevision(revision);
0777 
0778         QList<VcsItemEvent> items;
0779 
0780         const VcsItemEvent::Action actions[3] = {VcsItemEvent::Deleted, VcsItemEvent::Added, VcsItemEvent::ContentsModified};
0781         for (int i = 0; i < 3; i++) {
0782             const auto &files = *it++;
0783             if (files.isEmpty()) {
0784                 continue;
0785             }
0786 
0787             foreach (const auto& file, files.split(' ')) {
0788                 VcsItemEvent item;
0789                 item.setActions(actions[i]);
0790                 item.setRepositoryLocation(QUrl::fromPercentEncoding(file.toLocal8Bit()));
0791                 items.push_back(item);
0792             }
0793         }
0794 
0795         const auto &copies = *it++;
0796         if (!copies.isEmpty()) {
0797             foreach (const auto & copy, copies.split(' ')) {
0798                 auto files = copy.split('~');
0799 
0800                 // TODO: What is it? Why it doesn't work
0801                 if (files.size() >= 2) {
0802                     VcsItemEvent item;
0803                     item.setActions(VcsItemEvent::Copied);
0804                     item.setRepositoryCopySourceLocation(QUrl::fromPercentEncoding(files[0].toLocal8Bit()));
0805                     item.setRepositoryLocation(QUrl::fromPercentEncoding(files[1].toLocal8Bit()));
0806                     items.push_back(item);
0807                 }
0808             }
0809         }
0810 
0811         event.setItems(items);
0812         events.push_back(QVariant::fromValue(event));
0813     }
0814 
0815     job->setResults(QVariant(events));
0816 }
0817 
0818 void MercurialPlugin::parseIdentify(DVcsJob *job) const
0819 {
0820     QString value = job->output();
0821     QList<QVariant> result;
0822 
0823     qCDebug(PLUGIN_MERCURIAL) << value;
0824     // remove last '\n'
0825     value.chop(1);
0826 
0827     // remove last '+' if necessary
0828     if (value.endsWith('+'))
0829         value.chop(1);
0830 
0831     foreach (const QString & rev, value.split('+')) {
0832         VcsRevision revision;
0833         revision.setRevisionValue(rev.toLongLong(), VcsRevision::GlobalNumber);
0834         result << qVariantFromValue<VcsRevision>(revision);
0835     }
0836     job->setResults(QVariant(result));
0837 }
0838 
0839 
0840 void MercurialPlugin::parseLogOutput(const DVcsJob* job, QVector<DVcsEvent>& commits) const
0841 {
0842     qCDebug(PLUGIN_MERCURIAL) << "parseLogOutput";
0843 
0844     static unsigned int entriesPerCommit = 6;
0845     auto items = job->output().split("\\_%");
0846 
0847     if (uint(items.size()) < entriesPerCommit || 1 != (items.size() % entriesPerCommit)) {
0848         qCDebug(PLUGIN_MERCURIAL) << "Cannot parse commit log: unexpected number of entries";
0849         return;
0850     }
0851 
0852     bool success = false;
0853 
0854     QString const &lastRev = items.at(entriesPerCommit - 1);
0855     unsigned int id = lastRev.toUInt(&success);
0856 
0857     if (!success) {
0858         qCDebug(PLUGIN_MERCURIAL) << "Could not parse last revision \"" << lastRev << '"';
0859         id = 1024;
0860     }
0861 
0862     QVector<QString> fullIds(id + 1);
0863 
0864     typedef std::reverse_iterator<QStringList::const_iterator> QStringListReverseIterator;
0865     QStringListReverseIterator rbegin(items.constEnd() - 1), rend(items.constBegin());  // Skip the final 0
0866     unsigned int lastId;
0867 
0868     while (rbegin != rend) {
0869         QString rev = *rbegin++;
0870         QString node = *rbegin++;
0871         QString parents = *rbegin++;
0872         QString author = *rbegin++;
0873         QString date = *rbegin++;
0874         QString desc = *rbegin++;
0875         lastId = id;
0876         id = rev.toUInt(&success);
0877 
0878         if (!success) {
0879             qCDebug(PLUGIN_MERCURIAL) << "Could not parse revision \"" << rev << '"';
0880             return;
0881         }
0882 
0883         if (uint(fullIds.size()) <= id) {
0884             fullIds.resize(id * 2);
0885         }
0886 
0887         fullIds[id] = node;
0888 
0889         DVcsEvent commit;
0890         commit.setCommit(node);
0891         commit.setAuthor(author);
0892         commit.setDate(date);
0893         commit.setLog(desc);
0894 
0895         if (id == 0) {
0896             commit.setType(DVcsEvent::INITIAL);
0897         } else {
0898             if (parents.isEmpty() && id != 0) {
0899                 commit.setParents(QStringList(fullIds[lastId]));
0900             } else {
0901                 QStringList parentList;
0902                 QStringList unparsedParentList = parents.split(QChar(' '), QString::SkipEmptyParts);
0903                 // id:Short-node
0904                 static const unsigned int shortNodeSuffixLen = 13;
0905                 foreach (const QString & p, unparsedParentList) {
0906                     QString ids = p.left(p.size() - shortNodeSuffixLen);
0907                     id = ids.toUInt(&success);
0908 
0909                     if (!success) {
0910                         qCDebug(PLUGIN_MERCURIAL) << "Could not parse parent-revision \"" << ids << "\" of revision " << rev;
0911                         return;
0912                     }
0913 
0914                     parentList.push_back(fullIds[id]);
0915                 }
0916 
0917                 commit.setParents(parentList);
0918             }
0919         }
0920 
0921         commits.push_front(commit);
0922     }
0923 }
0924 
0925 VcsLocationWidget *MercurialPlugin::vcsLocation(QWidget *parent) const
0926 {
0927     return new MercurialVcsLocationWidget(parent);
0928 }
0929 
0930 void MercurialPlugin::filterOutDirectories(QList<QUrl> &locations)
0931 {
0932     QList<QUrl> fileLocations;
0933     foreach (const QUrl & location, locations) {
0934         if (!QFileInfo(location.toLocalFile()).isDir()) {
0935             fileLocations << location;
0936         }
0937     }
0938     locations = fileLocations;
0939 }
0940 
0941 VcsStatusInfo::State MercurialPlugin::charToState(const char ch)
0942 {
0943     switch (ch) {
0944     case 'M':
0945         return VcsStatusInfo::ItemModified;
0946     case 'A':
0947         return VcsStatusInfo::ItemAdded;
0948     case 'R':
0949         return VcsStatusInfo::ItemDeleted;
0950     case 'C':
0951         return VcsStatusInfo::ItemUpToDate;
0952     case '!':   // Missing
0953         return VcsStatusInfo::ItemUserState;
0954     default:
0955         return VcsStatusInfo::ItemUnknown;
0956     }
0957 }
0958 
0959 QString MercurialPlugin::toMercurialRevision(const VcsRevision &vcsrev)
0960 {
0961     switch (vcsrev.revisionType()) {
0962     case VcsRevision::Special:
0963         switch (vcsrev.revisionValue().value<KDevelop::VcsRevision::RevisionSpecialType>()) {
0964         case VcsRevision::Head:
0965             return QString("tip");
0966         case VcsRevision::Base:
0967             return QString(".");
0968         case VcsRevision::Working:
0969             return QString("");
0970         case VcsRevision::Previous: // We can't determine this from one revision alone
0971             return QString();
0972         case VcsRevision::Start:
0973             return QString("0");
0974         default:
0975             return QString();
0976         }
0977     case VcsRevision::GlobalNumber:
0978         return QString::number(vcsrev.revisionValue().toLongLong());
0979     case VcsRevision::Date:         // TODO
0980     case VcsRevision::FileNumber:   // No file number for mercurial
0981     default:
0982         return QString();
0983     }
0984 }
0985 
0986 QDir MercurialPlugin::findWorkingDir(const QUrl &location)
0987 {
0988     QFileInfo fileInfo(location.toLocalFile());
0989 
0990     // find out correct working directory
0991     if (fileInfo.isFile()) {
0992         return fileInfo.absolutePath();
0993     } else {
0994         return fileInfo.absoluteFilePath();
0995     }
0996 }
0997 
0998 QUrl MercurialPlugin::remotePushRepositoryLocation(QDir &directory)
0999 {
1000     // check default-push first
1001     DVcsJob *job = new DVcsJob(directory, this);
1002     *job << "hg" << "paths" << "default-push";
1003     if (!job->exec() || job->status() != VcsJob::JobSucceeded) {
1004         qCDebug(PLUGIN_MERCURIAL) << "no default-push, hold on";
1005 
1006         // or try default
1007         job = new DVcsJob(directory, this);
1008         *job << "hg" << "paths" << "default";
1009 
1010         if (!job->exec() || job->status() != VcsJob::JobSucceeded) {
1011             qCDebug(PLUGIN_MERCURIAL) << "nowhere to push!";
1012 
1013             // fail is everywhere
1014             return QUrl();
1015         }
1016     }
1017 
1018     // don't forget to strip last '\n'
1019     return QUrl::fromLocalFile(job->output().trimmed());
1020 }
1021 
1022 void MercurialPlugin::registerRepositoryForCurrentBranchChanges(const QUrl& repository)
1023 {
1024     if (isValidDirectory(repository)) {
1025         QString headFile = m_lastRepoRoot.path() + QStringLiteral("/.hg/branch");
1026         m_watcher->addFile(headFile);
1027     }
1028 }
1029 
1030 void MercurialPlugin::additionalMenuEntries(QMenu *menu, const QList<QUrl> &urls)
1031 {
1032     m_urls = urls;
1033 
1034     menu->addAction(m_headsAction);
1035     menu->addSeparator()->setText(i18n("Mercurial Queues"));
1036     menu->addAction(m_mqNew);
1037     menu->addAction(m_mqPushAction);
1038     menu->addAction(m_mqPushAllAction);
1039     menu->addAction(m_mqPopAction);
1040     menu->addAction(m_mqPopAllAction);
1041     menu->addAction(m_mqManagerAction);
1042 
1043     m_headsAction->setEnabled(m_urls.count() == 1);
1044 
1045     //FIXME:not supported yet, so disable
1046     m_mqNew->setEnabled(false);
1047     m_mqPushAction->setEnabled(false);
1048     m_mqPushAllAction->setEnabled(false);
1049     m_mqPopAction->setEnabled(false);
1050     m_mqPopAllAction->setEnabled(false);
1051 }
1052 
1053 void MercurialPlugin::showHeads()
1054 {
1055     const QUrl &current = m_urls.first();
1056     MercurialHeadsWidget *headsWidget = new MercurialHeadsWidget(this, current);
1057     headsWidget->show();
1058 }
1059 
1060 void MercurialPlugin::showMercurialQueuesManager()
1061 {
1062     const QUrl &current = m_urls.first();
1063     MercurialQueuesManager *managerWidget = new MercurialQueuesManager(this, current);
1064     managerWidget->show();
1065 }
1066 
1067 /*
1068  * Mercurial Queues
1069  */
1070 
1071 VcsJob *MercurialPlugin::mqNew(const QUrl &/*localLocation*/, const QString &/*name*/, const QString &/*message*/)
1072 {
1073     return nullptr;
1074 }
1075 
1076 VcsJob *MercurialPlugin::mqPush(const QUrl &localLocation)
1077 {
1078     DVcsJob *job = new DVcsJob(findWorkingDir(localLocation), this);
1079     *job << "hg" << "qpush";
1080     return job;
1081 }
1082 
1083 VcsJob *MercurialPlugin::mqPushAll(const QUrl &localLocation)
1084 {
1085     DVcsJob *job = new DVcsJob(findWorkingDir(localLocation), this);
1086     *job << "hg" << "qpush" << "-a";
1087     return job;
1088 }
1089 
1090 VcsJob *MercurialPlugin::mqPop(const QUrl &localLocation)
1091 {
1092     DVcsJob *job = new DVcsJob(findWorkingDir(localLocation), this);
1093     *job << "hg" << "qpop";
1094     return job;
1095 }
1096 
1097 VcsJob *MercurialPlugin::mqPopAll(const QUrl &localLocation)
1098 {
1099     DVcsJob *job = new DVcsJob(findWorkingDir(localLocation), this);
1100     *job << "hg" << "qpop" << "-a";
1101     return job;
1102 }
1103 
1104 VcsJob *MercurialPlugin::mqApplied(const QUrl &localLocation)
1105 {
1106     DVcsJob *job = new DVcsJob(findWorkingDir(localLocation), this);
1107     *job << "hg" << "qapplied";
1108     connect(job, &DVcsJob::readyForParsing, this, &MercurialPlugin::parseMultiLineOutput);
1109     return job;
1110 }
1111 
1112 VcsJob *MercurialPlugin::mqUnapplied(const QUrl &localLocation)
1113 {
1114     DVcsJob *job = new DVcsJob(findWorkingDir(localLocation), this);
1115     *job << "hg" << "qunapplied";
1116     connect(job, &DVcsJob::readyForParsing, this, &MercurialPlugin::parseMultiLineOutput);
1117     return job;
1118 }
1119 
1120 // #include "mercurialplugin.moc"