File indexing completed on 2024-04-28 04:38:50

0001 /*
0002     SPDX-FileCopyrightText: 2008 Evgeniy Ivanov <powerfox@kde.ru>
0003     SPDX-FileCopyrightText: 2009 Hugo Parente Lima <hugo.pl@gmail.com>
0004     SPDX-FileCopyrightText: 2010 Aleix Pol Gonzalez <aleixpol@kde.org>
0005 
0006     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0007 */
0008 
0009 #include "gitplugin.h"
0010 
0011 #include "repostatusmodel.h"
0012 #include "committoolview.h"
0013 
0014 #include <QDateTime>
0015 #include <QProcess>
0016 #include <QDir>
0017 #include <QFileInfo>
0018 #include <QMenu>
0019 #include <QTimer>
0020 #include <QRegularExpression>
0021 #include <QPointer>
0022 #include <QTemporaryFile>
0023 #include <QVersionNumber>
0024 
0025 #include <interfaces/icore.h>
0026 #include <interfaces/iproject.h>
0027 #include <interfaces/iruncontroller.h>
0028 #include <interfaces/iuicontroller.h>
0029 
0030 #include <util/path.h>
0031 
0032 #include <vcs/vcsjob.h>
0033 #include <vcs/vcsrevision.h>
0034 #include <vcs/vcsevent.h>
0035 #include <vcs/vcslocation.h>
0036 #include <vcs/dvcs/dvcsjob.h>
0037 #include <vcs/vcsannotation.h>
0038 #include <vcs/widgets/standardvcslocationwidget.h>
0039 #include "gitclonejob.h"
0040 #include "rebasedialog.h"
0041 #include "stashmanagerdialog.h"
0042 
0043 #include <KDirWatch>
0044 #include <KIO/CopyJob>
0045 #include <KIO/DeleteJob>
0046 #include <KLocalizedString>
0047 #include <KMessageBox>
0048 #include <KMessageBox_KDevCompat>
0049 #include <KTextEdit>
0050 #include <KTextEditor/Document>
0051 
0052 #include "gitjob.h"
0053 #include "gitmessagehighlighter.h"
0054 #include "gitplugincheckinrepositoryjob.h"
0055 #include "gitnameemaildialog.h"
0056 #include "debug.h"
0057 
0058 #include <array>
0059 
0060 using namespace KDevelop;
0061 
0062 QVariant runSynchronously(KDevelop::VcsJob* job)
0063 {
0064     QVariant ret;
0065     if(job->exec() && job->status()==KDevelop::VcsJob::JobSucceeded) {
0066         ret = job->fetchResults();
0067     }
0068     delete job;
0069     return ret;
0070 }
0071 
0072 namespace
0073 {
0074 
0075 QDir dotGitDirectory(const QUrl& dirPath, bool silent = false)
0076 {
0077     const QFileInfo finfo(dirPath.toLocalFile());
0078     QDir dir = finfo.isDir() ? QDir(finfo.filePath()): finfo.absoluteDir();
0079 
0080     const QString gitDir = QStringLiteral(".git");
0081     while (!dir.exists(gitDir) && dir.cdUp()) {} // cdUp, until there is a sub-directory called .git
0082 
0083     if (!silent && dir.isRoot()) {
0084         qCWarning(PLUGIN_GIT) << "couldn't find the git root for" << dirPath;
0085     }
0086 
0087     return dir;
0088 }
0089 
0090 /**
0091  * Whenever a directory is provided, change it for all the files in it but not inner directories,
0092  * that way we make sure we won't get into recursion,
0093  */
0094 static QList<QUrl> preventRecursion(const QList<QUrl>& urls)
0095 {
0096     QList<QUrl> ret;
0097     for (const QUrl& url : urls) {
0098         QDir d(url.toLocalFile());
0099         if(d.exists()) {
0100             const QStringList entries = d.entryList(QDir::Files | QDir::NoDotAndDotDot);
0101             ret.reserve(ret.size() + entries.size());
0102             for (const QString& entry : entries) {
0103                 QUrl entryUrl = QUrl::fromLocalFile(d.absoluteFilePath(entry));
0104                 ret += entryUrl;
0105             }
0106         } else
0107             ret += url;
0108     }
0109     return ret;
0110 }
0111 
0112 QString toRevisionName(const KDevelop::VcsRevision& rev, const QString& currentRevision=QString())
0113 {
0114     switch(rev.revisionType()) {
0115         case VcsRevision::Special:
0116             switch(rev.revisionValue().value<VcsRevision::RevisionSpecialType>()) {
0117                 case VcsRevision::Head:
0118                     return QStringLiteral("^HEAD");
0119                 case VcsRevision::Base:
0120                     return QString();
0121                 case VcsRevision::Working:
0122                     return QString();
0123                 case VcsRevision::Previous:
0124                     Q_ASSERT(!currentRevision.isEmpty());
0125                     return currentRevision + QLatin1String("^1");
0126                 case VcsRevision::Start:
0127                     return QString();
0128                 case VcsRevision::UserSpecialType: //Not used
0129                     Q_ASSERT(false && "i don't know how to do that");
0130             }
0131             break;
0132         case VcsRevision::GlobalNumber:
0133             return rev.revisionValue().toString();
0134         case VcsRevision::Date:
0135         case VcsRevision::FileNumber:
0136         case VcsRevision::Invalid:
0137         case VcsRevision::UserType:
0138             Q_ASSERT(false);
0139     }
0140     return QString();
0141 }
0142 
0143 QString revisionInterval(const KDevelop::VcsRevision& rev, const KDevelop::VcsRevision& limit)
0144 {
0145     QString ret;
0146     if(rev.revisionType()==VcsRevision::Special &&
0147                 rev.revisionValue().value<VcsRevision::RevisionSpecialType>()==VcsRevision::Start) //if we want it to the beginning just put the revisionInterval
0148         ret = toRevisionName(limit, QString());
0149     else {
0150         QString dst = toRevisionName(limit);
0151         if(dst.isEmpty())
0152             ret = dst;
0153         else {
0154             QString src = toRevisionName(rev, dst);
0155             if(src.isEmpty())
0156                 ret = src;
0157             else
0158                 ret = src + QLatin1String("..") + dst;
0159         }
0160     }
0161     return ret;
0162 }
0163 
0164 QDir urlDir(const QUrl& url)
0165 {
0166     QFileInfo f(url.toLocalFile());
0167     if(f.isDir())
0168         return QDir(url.toLocalFile());
0169     else
0170         return f.absoluteDir();
0171 }
0172 QDir urlDir(const QList<QUrl>& urls) { return urlDir(urls.first()); } //TODO: could be improved
0173 
0174 }
0175 
0176 GitPlugin::GitPlugin(QObject* parent, const QVariantList&)
0177     : DistributedVersionControlPlugin(parent, QStringLiteral("kdevgit"))
0178     , m_repoStatusModel(new RepoStatusModel(this))
0179     , m_commitToolViewFactory(new CommitToolViewFactory(m_repoStatusModel))
0180 {
0181     if (QStandardPaths::findExecutable(QStringLiteral("git")).isEmpty()) {
0182         setErrorDescription(i18n("Unable to find git executable. Is it installed on the system?"));
0183         return;
0184     }
0185 
0186     // FIXME: Is this needed (I don't quite understand the comment
0187     // in vcsstatusinfo.h which says we need to do this if we want to
0188     // use VcsStatusInfo in queued signals/slots)
0189     qRegisterMetaType<VcsStatusInfo>();
0190 
0191     ICore::self()->uiController()->addToolView(i18n("Git Commit"), m_commitToolViewFactory);
0192 
0193     setObjectName(QStringLiteral("Git"));
0194 
0195     auto* versionJob = new GitJob(QDir::tempPath(), this, KDevelop::OutputJob::Silent);
0196     *versionJob << "git" << "--version";
0197     connect(versionJob, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitVersionOutput);
0198     ICore::self()->runController()->registerJob(versionJob);
0199 
0200     m_watcher = new KDirWatch(this);
0201     connect(m_watcher, &KDirWatch::dirty, this, &GitPlugin::fileChanged);
0202     connect(m_watcher, &KDirWatch::created, this, &GitPlugin::fileChanged);
0203 }
0204 
0205 GitPlugin::~GitPlugin()
0206 {}
0207 
0208 bool emptyOutput(DVcsJob* job)
0209 {
0210     QScopedPointer<DVcsJob> _job(job);
0211     if(job->exec() && job->status()==VcsJob::JobSucceeded)
0212         return job->rawOutput().trimmed().isEmpty();
0213 
0214     return false;
0215 }
0216 
0217 bool GitPlugin::hasStashes(const QDir& repository)
0218 {
0219     if (auto *job = qobject_cast<DVcsJob*>(gitStash(repository, QStringList(QStringLiteral("list")), KDevelop::OutputJob::Silent))) {
0220         return !emptyOutput(job);
0221     }
0222     Q_ASSERT(false); // gitStash should always return a DVcsJob !
0223     return false;
0224 }
0225 
0226 bool GitPlugin::hasModifications(const QDir& d)
0227 {
0228     return !emptyOutput(lsFiles(d, QStringList(QStringLiteral("-m")), OutputJob::Silent));
0229 }
0230 
0231 bool GitPlugin::hasModifications(const QDir& repo, const QUrl& file)
0232 {
0233     return !emptyOutput(lsFiles(repo, QStringList{QStringLiteral("-m"), file.path()}, OutputJob::Silent));
0234 }
0235 
0236 void GitPlugin::additionalMenuEntries(QMenu* menu, const QList<QUrl>& urls)
0237 {
0238     m_urls = urls;
0239 
0240     QDir dir=urlDir(urls);
0241     bool hasSt = hasStashes(dir);
0242 
0243     menu->addAction(i18nc("@action:inmenu", "Rebase"), this, SLOT(ctxRebase()));
0244     menu->addSeparator()->setText(i18nc("@title:menu", "Git Stashes"));
0245     menu->addAction(i18nc("@action:inmenu", "Stash Manager"), this, SLOT(ctxStashManager()))->setEnabled(hasSt);
0246     menu->addAction(QIcon::fromTheme(QStringLiteral("vcs-stash")), i18nc("@action:inmenu", "Push Stash"), this, SLOT(ctxPushStash()));
0247     menu->addAction(QIcon::fromTheme(QStringLiteral("vcs-stash-pop")), i18nc("@action:inmenu", "Pop Stash"), this, SLOT(ctxPopStash()))->setEnabled(hasSt);
0248 }
0249 
0250 void GitPlugin::ctxRebase()
0251 {
0252     auto* dialog = new RebaseDialog(this, m_urls.first(), nullptr);
0253     dialog->setAttribute(Qt::WA_DeleteOnClose);
0254     dialog->open();
0255 }
0256 
0257 void GitPlugin::ctxPushStash()
0258 {
0259     VcsJob* job = gitStash(urlDir(m_urls), QStringList(), KDevelop::OutputJob::Verbose);
0260     ICore::self()->runController()->registerJob(job);
0261 }
0262 
0263 void GitPlugin::ctxPopStash()
0264 {
0265     VcsJob* job = gitStash(urlDir(m_urls), QStringList(QStringLiteral("pop")), KDevelop::OutputJob::Verbose);
0266     ICore::self()->runController()->registerJob(job);
0267 }
0268 
0269 void GitPlugin::ctxStashManager()
0270 {
0271     QPointer<StashManagerDialog> d = new StashManagerDialog(urlDir(m_urls), this, nullptr);
0272     d->exec();
0273 
0274     delete d;
0275 }
0276 
0277 DVcsJob* GitPlugin::errorsFound(const QString& error, KDevelop::OutputJob::OutputJobVerbosity verbosity=OutputJob::Verbose)
0278 {
0279     auto* j = new GitJob(QDir::temp(), this, verbosity);
0280     *j << "echo" << i18n("error: %1", error) << "-n";
0281     return j;
0282 }
0283 
0284 QString GitPlugin::name() const
0285 {
0286     return QStringLiteral("Git");
0287 }
0288 
0289 QUrl GitPlugin::repositoryRoot(const QUrl& path)
0290 {
0291     return QUrl::fromLocalFile(dotGitDirectory(path).absolutePath());
0292 }
0293 
0294 bool GitPlugin::isValidDirectory(const QUrl & dirPath)
0295 {
0296     QDir dir = dotGitDirectory(dirPath, true);
0297     QFile dotGitPotentialFile(dir.filePath(QStringLiteral(".git")));
0298     // if .git is a file, we may be in a git worktree
0299     QFileInfo dotGitPotentialFileInfo(dotGitPotentialFile);
0300     if (!dotGitPotentialFileInfo.isDir() && dotGitPotentialFile.exists()) {
0301         QString gitWorktreeFileContent;
0302         if (dotGitPotentialFile.open(QFile::ReadOnly)) {
0303             // the content should be gitdir: /path/to/the/.git/worktree
0304             gitWorktreeFileContent = QString::fromUtf8(dotGitPotentialFile.readAll());
0305             dotGitPotentialFile.close();
0306         } else {
0307             return false;
0308         }
0309         const auto items = gitWorktreeFileContent.split(QLatin1Char(' '));
0310         if (items.size() == 2 && items.at(0) == QLatin1String("gitdir:")) {
0311             qCDebug(PLUGIN_GIT) << "we are in a git worktree" << items.at(1);
0312             return true;
0313         }
0314     }
0315     return dir.exists(QStringLiteral(".git/HEAD"));
0316 }
0317 
0318 bool GitPlugin::isValidRemoteRepositoryUrl(const QUrl& remoteLocation)
0319 {
0320     if (remoteLocation.isLocalFile()) {
0321         QFileInfo fileInfo(remoteLocation.toLocalFile());
0322         if (fileInfo.isDir()) {
0323             QDir dir(fileInfo.filePath());
0324             if (dir.exists(QStringLiteral(".git/HEAD"))) {
0325                 return true;
0326             }
0327             // TODO: check also for bare repo
0328         }
0329     } else {
0330         const QString scheme = remoteLocation.scheme();
0331         if (scheme == QLatin1String("git") || scheme == QLatin1String("git+ssh")) {
0332             return true;
0333         }
0334         // heuristic check, anything better we can do here without talking to server?
0335         if ((scheme == QLatin1String("http") ||
0336              scheme == QLatin1String("https")) &&
0337             remoteLocation.path().endsWith(QLatin1String(".git"))) {
0338             return true;
0339         }
0340     }
0341     return false;
0342 }
0343 
0344 bool GitPlugin::isVersionControlled(const QUrl &path)
0345 {
0346     QFileInfo fsObject(path.toLocalFile());
0347     if (!fsObject.exists()) {
0348         return false;
0349     }
0350     if (fsObject.isDir()) {
0351         return isValidDirectory(path);
0352     }
0353 
0354     QString filename = fsObject.fileName();
0355 
0356     QStringList otherFiles = getLsFiles(fsObject.dir(), QStringList(QStringLiteral("--")) << filename, KDevelop::OutputJob::Silent);
0357     return !otherFiles.empty();
0358 }
0359 
0360 VcsJob* GitPlugin::init(const QUrl &directory)
0361 {
0362     auto* job = new GitJob(urlDir(directory), this);
0363     job->setType(VcsJob::Import);
0364     *job << "git" << "init";
0365     return job;
0366 }
0367 
0368 VcsJob* GitPlugin::createWorkingCopy(const KDevelop::VcsLocation & source, const QUrl& dest, KDevelop::IBasicVersionControl::RecursionMode)
0369 {
0370     DVcsJob* job = new GitCloneJob(urlDir(dest), this);
0371     job->setType(VcsJob::Import);
0372     *job << "git" << "clone" << "--progress" << "--" << source.localUrl().url() << dest;
0373     return job;
0374 }
0375 
0376 VcsJob* GitPlugin::add(const QList<QUrl>& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion)
0377 {
0378     if (localLocations.empty())
0379         return errorsFound(i18n("Did not specify the list of files"), OutputJob::Verbose);
0380 
0381     DVcsJob* job = new GitJob(dotGitDirectory(localLocations.front()), this);
0382     job->setType(VcsJob::Add);
0383     *job << "git" << "add" << "--" << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations));
0384     return job;
0385 }
0386 
0387 KDevelop::VcsJob* GitPlugin::status(const QList<QUrl>& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion)
0388 {
0389     if (localLocations.empty())
0390         return errorsFound(i18n("Did not specify the list of files"), OutputJob::Verbose);
0391 
0392     DVcsJob* job = new GitJob(urlDir(localLocations), this, OutputJob::Silent);
0393     job->setType(VcsJob::Status);
0394 
0395     if(m_oldVersion) {
0396         *job << "git" << "ls-files" << "-t" << "-m" << "-c" << "-o" << "-d" << "-k" << "--directory";
0397         connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitStatusOutput_old);
0398     } else {
0399         *job << "git" << "status" << "--porcelain";
0400         job->setIgnoreError(true);
0401         connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitStatusOutput);
0402     }
0403     *job << "--" << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations));
0404 
0405     return job;
0406 }
0407 
0408 VcsJob* GitPlugin::diff(const QUrl& fileOrDirectory, const KDevelop::VcsRevision& srcRevision, const KDevelop::VcsRevision& dstRevision,
0409                         IBasicVersionControl::RecursionMode recursion)
0410 {
0411     DVcsJob* job = static_cast<DVcsJob*>(diff(fileOrDirectory, srcRevision, dstRevision));
0412     *job << "--";
0413     if (recursion == IBasicVersionControl::Recursive) {
0414         *job << fileOrDirectory;
0415     } else {
0416         *job << preventRecursion(QList<QUrl>() << fileOrDirectory);
0417     }
0418     return job;
0419 }
0420 
0421 KDevelop::VcsJob * GitPlugin::diff(const QUrl& repoPath, const KDevelop::VcsRevision& srcRevision, const KDevelop::VcsRevision& dstRevision)
0422 {
0423     DVcsJob* job = new GitJob(dotGitDirectory(repoPath), this, KDevelop::OutputJob::Silent);
0424     job->setType(VcsJob::Diff);
0425     *job << "git" << "diff" << "--no-color" << "--no-ext-diff";
0426     if (!usePrefix()) {
0427         // KDE's ReviewBoard now requires p1 patchfiles, so `git diff --no-prefix` to generate p0 patches
0428         // has become optional.
0429         *job << "--no-prefix";
0430     }
0431     if (dstRevision.revisionType() == VcsRevision::Special &&
0432          dstRevision.specialType() == VcsRevision::Working) {
0433         if (srcRevision.revisionType() == VcsRevision::Special &&
0434              srcRevision.specialType() == VcsRevision::Base) {
0435             *job << "HEAD";
0436         } else {
0437             *job << "--cached" << srcRevision.revisionValue().toString();
0438         }
0439     } else {
0440         QString revstr = revisionInterval(srcRevision, dstRevision);
0441         if(!revstr.isEmpty())
0442             *job << revstr;
0443     }
0444     connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitDiffOutput);
0445     return job;
0446 }
0447 
0448 
0449 KDevelop::VcsJob * GitPlugin::reset ( const QList<QUrl>& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion )
0450 {
0451     if(localLocations.isEmpty() )
0452         return errorsFound(i18n("Could not reset changes (empty list of paths)"), OutputJob::Verbose);
0453 
0454     DVcsJob* job = new GitJob(dotGitDirectory(localLocations.front()), this);
0455     job->setType(VcsJob::Reset);
0456     *job << "git" << "reset" << "--";
0457     *job << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations));
0458     return job;
0459 }
0460 
0461 KDevelop::VcsJob * GitPlugin::apply(const KDevelop::VcsDiff& diff, const ApplyParams applyTo)
0462 {
0463     DVcsJob* job = new GitJob(dotGitDirectory(diff.baseDiff()), this);
0464     job->setType(VcsJob::Apply);
0465     *job << "git" << "apply";
0466     if (applyTo == Index) {
0467         *job << "--index";   // Applies the diff also to the index
0468         *job << "--cached";  // Does not touch the work tree
0469     }
0470     auto* diffFile = new QTemporaryFile(this);
0471     if (diffFile->open()) {
0472         *job << diffFile->fileName();
0473         diffFile->write(diff.diff().toUtf8());
0474         diffFile->close();
0475         connect(job, &KDevelop::VcsJob::resultsReady, [=](){delete diffFile;});
0476     } else {
0477         job->cancel();
0478         delete diffFile;
0479     }
0480     return job;
0481 }
0482 
0483 
0484 VcsJob* GitPlugin::revert(const QList<QUrl>& localLocations, IBasicVersionControl::RecursionMode recursion)
0485 {
0486     if(localLocations.isEmpty() )
0487         return errorsFound(i18n("Could not revert changes"), OutputJob::Verbose);
0488 
0489     QDir repo = urlDir(repositoryRoot(localLocations.first()));
0490     QString modified;
0491     for (const auto& file: localLocations) {
0492         if (hasModifications(repo, file)) {
0493             modified.append(file.toDisplayString(QUrl::PreferLocalFile) + QLatin1String("<br/>"));
0494         }
0495     }
0496     if (!modified.isEmpty()) {
0497         auto res = KMessageBox::questionTwoActions(nullptr,
0498                                                    i18n("The following files have uncommitted changes, "
0499                                                         "which will be lost. Continue?")
0500                                                        + QLatin1String("<br/><br/>") + modified,
0501                                                    {}, KStandardGuiItem::discard(), KStandardGuiItem::cancel());
0502         if (res != KMessageBox::PrimaryAction) {
0503             return errorsFound(QString(), OutputJob::Silent);
0504         }
0505     }
0506 
0507     DVcsJob* job = new GitJob(dotGitDirectory(localLocations.front()), this);
0508     job->setType(VcsJob::Revert);
0509     *job << "git" << "checkout" << "--";
0510     *job << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations));
0511 
0512     return job;
0513 }
0514 
0515 
0516 //TODO: git doesn't like empty messages, but "KDevelop didn't provide any message, it may be a bug" looks ugly...
0517 //If no files specified then commit already added files
0518 VcsJob* GitPlugin::commit(const QString& message,
0519                              const QList<QUrl>& localLocations,
0520                              KDevelop::IBasicVersionControl::RecursionMode recursion)
0521 {
0522     if (localLocations.empty() || message.isEmpty())
0523         return errorsFound(i18n("No files or message specified"));
0524 
0525     const QDir dir = dotGitDirectory(localLocations.front());
0526     if (!ensureValidGitIdentity(dir)) {
0527         return errorsFound(i18n("Email or name for Git not specified"));
0528     }
0529 
0530     auto* job = new GitJob(dir, this);
0531     job->setType(VcsJob::Commit);
0532     QList<QUrl> files = (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations));
0533     addNotVersionedFiles(dir, files);
0534 
0535     *job << "git" << "commit" << "-m" << message;
0536     *job << "--" << files;
0537     return job;
0538 }
0539 
0540 KDevelop::VcsJob * GitPlugin::commitStaged(const QString& message, const QUrl& repoUrl)
0541 {
0542     if (message.isEmpty())
0543         return errorsFound(i18n("No message specified"));
0544     const QDir dir = dotGitDirectory(repoUrl);
0545     if (!ensureValidGitIdentity(dir)) {
0546         return errorsFound(i18n("Email or name for Git not specified"));
0547     }
0548     auto* job = new GitJob(dir, this);
0549     job->setType(VcsJob::Commit);
0550     *job << "git" << "commit" << "-m" << message;
0551     return job;
0552 }
0553 
0554 
0555 bool GitPlugin::ensureValidGitIdentity(const QDir& dir)
0556 {
0557     const QUrl url = QUrl::fromLocalFile(dir.absolutePath());
0558 
0559     const QString name = readConfigOption(url, QStringLiteral("user.name"));
0560     const QString email = readConfigOption(url, QStringLiteral("user.email"));
0561     if (!email.isEmpty() && !name.isEmpty()) {
0562         return true; // already okay
0563     }
0564 
0565     GitNameEmailDialog dialog;
0566     dialog.setName(name);
0567     dialog.setEmail(email);
0568     if (!dialog.exec()) {
0569         return false;
0570     }
0571 
0572     runSynchronously(setConfigOption(url, QStringLiteral("user.name"), dialog.name(), dialog.isGlobal()));
0573     runSynchronously(setConfigOption(url, QStringLiteral("user.email"), dialog.email(), dialog.isGlobal()));
0574     return true;
0575 }
0576 
0577 void GitPlugin::addNotVersionedFiles(const QDir& dir, const QList<QUrl>& files)
0578 {
0579     const QStringList otherStr = getLsFiles(dir, QStringList() << QStringLiteral("--others"), KDevelop::OutputJob::Silent);
0580     QList<QUrl> toadd, otherFiles;
0581 
0582     otherFiles.reserve(otherStr.size());
0583     for (const QString& file : otherStr) {
0584         QUrl v = QUrl::fromLocalFile(dir.absoluteFilePath(file));
0585 
0586         otherFiles += v;
0587     }
0588 
0589     //We add the files that are not versioned
0590     for (const QUrl& file : files) {
0591         if(otherFiles.contains(file) && QFileInfo(file.toLocalFile()).isFile())
0592             toadd += file;
0593     }
0594 
0595     if(!toadd.isEmpty()) {
0596         VcsJob* job = add(toadd);
0597         job->exec(); // krazy:exclude=crashy
0598     }
0599 }
0600 
0601 bool isEmptyDirStructure(const QDir &dir)
0602 {
0603     const auto infos = dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot);
0604     for (const QFileInfo& i : infos) {
0605         if (i.isDir()) {
0606             if (!isEmptyDirStructure(QDir(i.filePath()))) return false;
0607         } else if (i.isFile()) {
0608             return false;
0609         }
0610     }
0611     return true;
0612 }
0613 
0614 VcsJob* GitPlugin::remove(const QList<QUrl>& files)
0615 {
0616     if (files.isEmpty())
0617         return errorsFound(i18n("No files to remove"));
0618     QDir dotGitDir = dotGitDirectory(files.front());
0619 
0620 
0621     QList<QUrl> files_(files);
0622 
0623     QMutableListIterator<QUrl> i(files_);
0624     while (i.hasNext()) {
0625         QUrl file = i.next();
0626         QFileInfo fileInfo(file.toLocalFile());
0627 
0628         const QStringList otherStr = getLsFiles(dotGitDir, QStringList{QStringLiteral("--others"), QStringLiteral("--"), file.toLocalFile()}, KDevelop::OutputJob::Silent);
0629         if(!otherStr.isEmpty()) {
0630             //remove files not under version control
0631             QList<QUrl> otherFiles;
0632             otherFiles.reserve(otherStr.size());
0633             for (const QString& f : otherStr) {
0634                 otherFiles << QUrl::fromLocalFile(dotGitDir.path() + QLatin1Char('/') + f);
0635             }
0636             if (fileInfo.isFile()) {
0637                 //if it's an unversioned file we are done, don't use git rm on it
0638                 i.remove();
0639             }
0640 
0641             auto deleteJob = KIO::del(otherFiles);
0642             deleteJob->exec();
0643             qCDebug(PLUGIN_GIT) << "other files" << otherFiles;
0644         }
0645 
0646         if (fileInfo.isDir()) {
0647             if (isEmptyDirStructure(QDir(file.toLocalFile()))) {
0648                 //remove empty folders, git doesn't do that
0649                 auto deleteJob = KIO::del(file);
0650                 deleteJob->exec();
0651                 qCDebug(PLUGIN_GIT) << "empty folder, removing" << file;
0652                 //we already deleted it, don't use git rm on it
0653                 i.remove();
0654             }
0655         }
0656     }
0657 
0658     if (files_.isEmpty()) return nullptr;
0659 
0660     DVcsJob* job = new GitJob(dotGitDir, this);
0661     job->setType(VcsJob::Remove);
0662     // git refuses to delete files with local modifications
0663     // use --force to overcome this
0664     *job << "git" << "rm" << "-r" << "--force";
0665     *job << "--" << files_;
0666     return job;
0667 }
0668 
0669 VcsJob* GitPlugin::log(const QUrl& localLocation,
0670                 const KDevelop::VcsRevision& src, const KDevelop::VcsRevision& dst)
0671 {
0672     DVcsJob* job = new GitJob(dotGitDirectory(localLocation), this, KDevelop::OutputJob::Silent);
0673     job->setType(VcsJob::Log);
0674     *job << "git" << "log" << "--date=raw" << "--name-status" << "-M80%" << "--follow";
0675     QString rev = revisionInterval(dst, src);
0676     if(!rev.isEmpty())
0677         *job << rev;
0678     *job << "--" << localLocation;
0679     connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitLogOutput);
0680     return job;
0681 }
0682 
0683 
0684 VcsJob* GitPlugin::log(const QUrl& localLocation, const KDevelop::VcsRevision& rev, unsigned long int limit)
0685 {
0686     DVcsJob* job = new GitJob(dotGitDirectory(localLocation), this, KDevelop::OutputJob::Silent);
0687     job->setType(VcsJob::Log);
0688     *job << "git" << "log" << "--date=raw" << "--name-status" << "-M80%" << "--follow";
0689     QString revStr = toRevisionName(rev, QString());
0690     if(!revStr.isEmpty())
0691         *job << revStr;
0692     if(limit>0)
0693         *job << QStringLiteral("-%1").arg(limit);
0694 
0695     *job << "--" << localLocation;
0696     connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitLogOutput);
0697     return job;
0698 }
0699 
0700 KDevelop::VcsJob* GitPlugin::annotate(const QUrl &localLocation, const KDevelop::VcsRevision&)
0701 {
0702     DVcsJob* job = new GitJob(dotGitDirectory(localLocation), this, KDevelop::OutputJob::Silent);
0703     job->setType(VcsJob::Annotate);
0704     *job << "git" << "blame" << "--porcelain" << "-w";
0705     *job << "--" << localLocation;
0706     connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitBlameOutput);
0707     return job;
0708 }
0709 
0710 void GitPlugin::parseGitBlameOutput(DVcsJob *job)
0711 {
0712     QVariantList results;
0713     VcsAnnotationLine* annotation = nullptr;
0714     const auto output = job->output();
0715     const auto lines = output.splitRef(QLatin1Char('\n'));
0716 
0717     bool skipNext=false;
0718     QMap<QString, VcsAnnotationLine> definedRevisions;
0719     for (auto& line : lines) {
0720         if(skipNext) {
0721             skipNext=false;
0722             results += QVariant::fromValue(*annotation);
0723 
0724             continue;
0725         }
0726 
0727         if (line.isEmpty())
0728             continue;
0729 
0730         QStringRef name = line.left(line.indexOf(QLatin1Char(' ')));
0731         QStringRef value = line.mid(name.size()+1);
0732 
0733         if(name==QLatin1String("author"))
0734             annotation->setAuthor(value.toString());
0735         else if(name==QLatin1String("author-mail")) {} //TODO: do smth with the e-mail?
0736         else if(name==QLatin1String("author-tz")) {} //TODO: does it really matter?
0737         else if(name==QLatin1String("author-time"))
0738             annotation->setDate(QDateTime::fromSecsSinceEpoch(value.toUInt(), Qt::LocalTime));
0739         else if(name==QLatin1String("summary"))
0740             annotation->setCommitMessage(value.toString());
0741         else if(name.startsWith(QLatin1String("committer"))) {} //We will just store the authors
0742         else if(name==QLatin1String("previous")) {} //We don't need that either
0743         else if(name==QLatin1String("filename")) { skipNext=true; }
0744         else if(name==QLatin1String("boundary")) {
0745             definedRevisions.insert(QStringLiteral("boundary"), VcsAnnotationLine());
0746         }
0747         else
0748         {
0749             const auto values = value.split(QLatin1Char(' '));
0750 
0751             VcsRevision rev;
0752             rev.setRevisionValue(name.left(8).toString(), KDevelop::VcsRevision::GlobalNumber);
0753 
0754             skipNext = definedRevisions.contains(name.toString());
0755 
0756             if(!skipNext)
0757                 definedRevisions.insert(name.toString(), VcsAnnotationLine());
0758 
0759             annotation = &definedRevisions[name.toString()];
0760             annotation->setLineNumber(values[1].toInt() - 1);
0761             annotation->setRevision(rev);
0762         }
0763     }
0764     job->setResults(results);
0765 }
0766 
0767 
0768 DVcsJob* GitPlugin::lsFiles(const QDir &repository, const QStringList &args,
0769                             OutputJob::OutputJobVerbosity verbosity)
0770 {
0771     auto* job = new GitJob(repository, this, verbosity);
0772     *job << "git" << "ls-files" << args;
0773     return job;
0774 }
0775 
0776 VcsJob* GitPlugin::gitStash(const QDir& repository, const QStringList& args, OutputJob::OutputJobVerbosity verbosity)
0777 {
0778     auto* job = new GitJob(repository, this, verbosity);
0779     *job << "git" << "stash" << args;
0780     return job;
0781 }
0782 
0783 VcsJob* GitPlugin::stashList(const QDir& repository,
0784                                        KDevelop::OutputJob::OutputJobVerbosity verbosity)
0785 {
0786     /* The format returns 4 fields separated by a 0-byte character (%x00):
0787      *
0788      *   %gd ... shortened reflog selector
0789      *   %p  ... abbreviated parent hashes (separated by a space, the first is the commit
0790      *                                      on which the stash was made)
0791      *   %s  ... subject (the stash message)
0792      *   %ct ... committer timestamp
0793      *
0794      * see man git-log, PRETTY FORMATS section and man git-stash for details.
0795      */
0796     auto* job=qobject_cast<DVcsJob*>(gitStash(repository, QStringList({
0797         QStringLiteral("list"),
0798         QStringLiteral("--format=format:%gd%x00%P%x00%s%x00%ct"),
0799     }), verbosity));
0800     connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitStashList);
0801     return job;
0802 }
0803 
0804 void GitPlugin::parseGitStashList(KDevelop::VcsJob* _job)
0805 {
0806     auto* job = qobject_cast<DVcsJob*>(_job);
0807     const QList<QByteArray> output = job->rawOutput().split('\n');
0808     QList<StashItem> results;
0809 
0810     for (const QByteArray& line : output) {
0811         if (line.isEmpty()) continue;
0812 
0813         const QList<QByteArray> fields = line.split('\x00');
0814 
0815         /* Extract the fields */
0816         Q_ASSERT(fields.length() >= 4);
0817         const auto message = QString::fromUtf8(fields[2]);
0818         const auto parentHash = QString::fromUtf8(fields[1].split(' ')[0]);
0819         const auto creationTime = QDateTime::fromSecsSinceEpoch(fields[3].toInt());
0820         const auto shortRef = QString::fromUtf8(fields[0]);
0821         const auto stackDepth = fields[0].mid(7, fields[0].indexOf('}')-7).toInt();
0822         QStringRef branch {};
0823         QStringRef parentCommitDesc {};
0824         if (message.startsWith(QStringLiteral("WIP on "))) {
0825             const int colPos = message.indexOf(QLatin1Char(':'), 7);
0826             branch = message.midRef(7, colPos-7);
0827             parentCommitDesc = message.midRef(colPos+2);
0828         }
0829 
0830         results << StashItem {
0831             stackDepth,
0832             shortRef,
0833             parentHash,
0834             parentCommitDesc.toString(),
0835             branch.toString(),
0836             message,
0837             creationTime,
0838         };
0839     }
0840     job->setResults(QVariant::fromValue(results));
0841 }
0842 
0843 VcsJob* GitPlugin::tag(const QUrl& repository, const QString& commitMessage, const VcsRevision& rev, const QString& tagName)
0844 {
0845     auto* job = new GitJob(urlDir(repository), this);
0846     *job << "git" << "tag" << "-m" << commitMessage << tagName;
0847     if(rev.revisionValue().isValid())
0848         *job << rev.revisionValue().toString();
0849     return job;
0850 }
0851 
0852 VcsJob* GitPlugin::switchBranch(const QUrl &repository, const QString &branch)
0853 {
0854     QDir d=urlDir(repository);
0855 
0856     if(hasModifications(d)) {
0857         auto answer = KMessageBox::questionTwoActionsCancel(
0858             nullptr, i18n("There are pending changes, do you want to stash them first?"), {},
0859             KGuiItem(i18nc("@action:button", "Stash"), QStringLiteral("vcs-stash")),
0860             KGuiItem(i18nc("@action:button", "Keep"), QStringLiteral("dialog-cancel")));
0861         if (answer == KMessageBox::PrimaryAction) {
0862             QScopedPointer<VcsJob> stash(gitStash(d, QStringList(), KDevelop::OutputJob::Verbose));
0863             stash->exec();
0864         } else if (answer == KMessageBox::Cancel) {
0865             return nullptr;
0866         }
0867     }
0868 
0869     auto* job = new GitJob(d, this);
0870     *job << "git" << "checkout" << branch;
0871     return job;
0872 }
0873 
0874 VcsJob* GitPlugin::branch(const QUrl& repository, const KDevelop::VcsRevision& rev, const QString& branchName)
0875 {
0876     Q_ASSERT(!branchName.isEmpty());
0877 
0878     auto* job = new GitJob(urlDir(repository), this);
0879     *job << "git" << "branch" << "--" << branchName;
0880 
0881     if(rev.revisionType() == VcsRevision::Special && rev.specialType() == VcsRevision::Head) {
0882         *job << "HEAD";
0883     } else if(!rev.prettyValue().isEmpty()) {
0884         *job << rev.revisionValue().toString();
0885     }
0886     return job;
0887 }
0888 
0889 VcsJob* GitPlugin::deleteBranch(const QUrl& repository, const QString& branchName)
0890 {
0891     auto* job = new GitJob(urlDir(repository), this, OutputJob::Silent);
0892     *job << "git" << "branch" << "-D" << branchName;
0893     connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitCurrentBranch);
0894     return job;
0895 }
0896 
0897 VcsJob* GitPlugin::renameBranch(const QUrl& repository, const QString& oldBranchName, const QString& newBranchName)
0898 {
0899     auto* job = new GitJob(urlDir(repository), this, OutputJob::Silent);
0900     *job << "git" << "branch" << "-m" << newBranchName << oldBranchName;
0901     connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitCurrentBranch);
0902     return job;
0903 }
0904 
0905 VcsJob* GitPlugin::mergeBranch(const QUrl& repository, const QString& branchName)
0906 {
0907     Q_ASSERT(!branchName.isEmpty());
0908 
0909     auto* job = new GitJob(urlDir(repository), this);
0910     *job << "git" << "merge" << branchName;
0911 
0912     return job;
0913 }
0914 
0915 VcsJob* GitPlugin::rebase(const QUrl& repository, const QString& branchName)
0916 {
0917     auto* job = new GitJob(urlDir(repository), this);
0918     *job << "git" << "rebase" << branchName;
0919 
0920     return job;
0921 }
0922 
0923 VcsJob* GitPlugin::currentBranch(const QUrl& repository)
0924 {
0925     auto* job = new GitJob(urlDir(repository), this, OutputJob::Silent);
0926     job->setIgnoreError(true);
0927     *job << "git" << "symbolic-ref" << "-q" << "--short" << "HEAD";
0928     connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitCurrentBranch);
0929     return job;
0930 }
0931 
0932 void GitPlugin::parseGitCurrentBranch(DVcsJob* job)
0933 {
0934     QString out = job->output().trimmed();
0935 
0936     job->setResults(out);
0937 }
0938 
0939 VcsJob* GitPlugin::branches(const QUrl &repository)
0940 {
0941     auto* job = new GitJob(urlDir(repository));
0942     *job << "git" << "branch" << "-a";
0943     connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitBranchOutput);
0944     return job;
0945 }
0946 
0947 void GitPlugin::parseGitBranchOutput(DVcsJob* job)
0948 {
0949     const auto output = job->output();
0950     const auto branchListDirty = output.splitRef(QLatin1Char('\n'), Qt::SkipEmptyParts);
0951 
0952     QStringList branchList;
0953     for (const auto& branch : branchListDirty) {
0954         // Skip pointers to another branches (one example of this is "origin/HEAD -> origin/master");
0955         // "git rev-list" chokes on these entries and we do not need duplicate branches altogether.
0956         if (branch.contains(QLatin1String("->")))
0957             continue;
0958 
0959         // Skip entries such as '(no branch)'
0960         if (branch.contains(QLatin1String("(no branch)")))
0961             continue;
0962 
0963         QStringRef name = branch;
0964         if (name.startsWith(QLatin1Char('*')))
0965             name = branch.mid(2);
0966 
0967         branchList << name.trimmed().toString();
0968     }
0969 
0970     job->setResults(branchList);
0971 }
0972 
0973 /* Few words about how this hardcore works:
0974 1. get all commits (with --parents)
0975 2. select master (root) branch and get all unique commits for branches (git-rev-list br2 ^master ^br3)
0976 3. parse allCommits. While parsing set mask (columns state for every row) for BRANCH, INITIAL, CROSS,
0977    MERGE and INITIAL are also set in DVCScommit::setParents (depending on parents count)
0978    another setType(INITIAL) is used for "bottom/root/first" commits of branches
0979 4. find and set merges, HEADS. It's an iteration through all commits.
0980     - first we check if parent is from the same branch, if no then we go through all commits searching parent's index
0981       and set CROSS/HCROSS for rows (in 3 rows are set EMPTY after commit with parent from another tree met)
0982     - then we check branchesShas[i][0] to mark heads
0983 
0984 4 can be a separate function. TODO: All this porn require refactoring (rewriting is better)!
0985 
0986 It's a very dirty implementation.
0987 FIXME:
0988 1. HEAD which is head has extra line to connect it with further commit
0989 2. If you merge branch2 to master, only new commits of branch2 will be visible (it's fine, but there will be
0990 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).
0991 3. Commits that have additional commit-data (not only history merging, but changes to fix conflicts) are shown incorrectly
0992 */
0993 
0994 QVector<DVcsEvent> GitPlugin::allCommits(const QString& repo)
0995 {
0996     initBranchHash(repo);
0997 
0998     const QStringList args{
0999         QStringLiteral("--all"),
1000         QStringLiteral("--pretty"),
1001         QStringLiteral("--parents"),
1002     };
1003     QScopedPointer<DVcsJob> job(gitRevList(repo, args));
1004     bool ret = job->exec();
1005     Q_ASSERT(ret && job->status()==VcsJob::JobSucceeded && "TODO: provide a fall back in case of failing");
1006     Q_UNUSED(ret);
1007     const QStringList commits = job->output().split(QLatin1Char('\n'), Qt::SkipEmptyParts);
1008 
1009     static QRegExp rx_com(QStringLiteral("commit \\w{40,40}"));
1010 
1011     QVector<DVcsEvent> commitList;
1012     DVcsEvent item;
1013 
1014     //used to keep where we have empty/cross/branch entry
1015     //true if it's an active branch (then cross or branch) and false if not
1016     QVector<bool> additionalFlags(branchesShas.count());
1017     additionalFlags.fill(false);
1018 
1019     //parse output
1020     for(int i = 0; i < commits.count(); ++i)
1021     {
1022         if (commits[i].contains(rx_com))
1023         {
1024             qCDebug(PLUGIN_GIT) << "commit found in " << commits[i];
1025             item.setCommit(commits[i].section(QLatin1Char(' '), 1, 1).trimmed());
1026 //             qCDebug(PLUGIN_GIT) << "commit is: " << commits[i].section(' ', 1);
1027 
1028             QStringList parents;
1029             QString parent = commits[i].section(QLatin1Char(' '), 2);
1030             int section = 2;
1031             while (!parent.isEmpty())
1032             {
1033                 /*                qCDebug(PLUGIN_GIT) << "Parent is: " << parent;*/
1034                 parents.append(parent.trimmed());
1035                 section++;
1036                 parent = commits[i].section(QLatin1Char(' '), section);
1037             }
1038             item.setParents(parents);
1039 
1040             //Avoid Merge string
1041             while (!commits[i].contains(QLatin1String("Author: ")))
1042                     ++i;
1043 
1044             item.setAuthor(commits[i].section(QStringLiteral("Author: "), 1).trimmed());
1045 //             qCDebug(PLUGIN_GIT) << "author is: " << commits[i].section("Author: ", 1);
1046 
1047             item.setDate(commits[++i].section(QStringLiteral("Date:   "), 1).trimmed());
1048 //             qCDebug(PLUGIN_GIT) << "date is: " << commits[i].section("Date:   ", 1);
1049 
1050             QString log;
1051             i++; //next line!
1052             while (i < commits.count() && !commits[i].contains(rx_com))
1053                 log += commits[i++];
1054             --i; //while took commit line
1055             item.setLog(log.trimmed());
1056 //             qCDebug(PLUGIN_GIT) << "log is: " << log;
1057 
1058             //mask is used in CommitViewDelegate to understand what we should draw for each branch
1059             QList<int> mask;
1060             mask.reserve(branchesShas.count());
1061 
1062             //set mask (properties for each graph column in row)
1063             for(int i = 0; i < branchesShas.count(); ++i)
1064             {
1065                 qCDebug(PLUGIN_GIT)<<"commit: " << item.commit();
1066                 if (branchesShas[i].contains(item.commit()))
1067                 {
1068                     mask.append(item.type()); //we set type in setParents
1069 
1070                     //check if parent from the same branch, if not then we have found a root of the branch
1071                     //and will use empty column for all further (from top to bottom) revisions
1072                     //FIXME: we should set CROSS between parent and child (and do it when find merge point)
1073                     additionalFlags[i] = false;
1074                     const auto parentShas = item.parents();
1075                     for (const QString& sha : parentShas) {
1076                         if (branchesShas[i].contains(sha))
1077                             additionalFlags[i] = true;
1078                     }
1079                     if (additionalFlags[i] == false)
1080                        item.setType(DVcsEvent::INITIAL); //hasn't parents from the same branch, used in drawing
1081                 }
1082                 else
1083                 {
1084                     if (additionalFlags[i] == false)
1085                         mask.append(DVcsEvent::EMPTY);
1086                     else
1087                         mask.append(DVcsEvent::CROSS);
1088                 }
1089                 qCDebug(PLUGIN_GIT) << "mask " << i << "is " << mask[i];
1090             }
1091             item.setProperties(mask);
1092             commitList.append(item);
1093         }
1094     }
1095 
1096     //find and set merges, HEADS, require refactoring!
1097     for (auto iter = commitList.begin();
1098         iter != commitList.end(); ++iter)
1099     {
1100         QStringList parents = iter->parents();
1101         //we need only only child branches
1102         if (parents.count() != 1)
1103             break;
1104 
1105         QString parent = parents[0];
1106         const QString commit = iter->commit();
1107         bool parent_checked = false;
1108         int heads_checked = 0;
1109 
1110         for(int i = 0; i < branchesShas.count(); ++i)
1111         {
1112             //check parent
1113             if (branchesShas[i].contains(commit))
1114             {
1115                 if (!branchesShas[i].contains(parent))
1116                 {
1117                     //parent and child are not in same branch
1118                     //since it is list, than parent has i+1 index
1119                     //set CROSS and HCROSS
1120                     for (auto f_iter = iter;
1121                         f_iter != commitList.end(); ++f_iter)
1122                     {
1123                         if (parent == f_iter->commit())
1124                         {
1125                             for(int j = 0; j < i; ++j)
1126                             {
1127                                 if(branchesShas[j].contains(parent))
1128                                     f_iter->setProperty(j, DVcsEvent::MERGE);
1129                                 else
1130                                     f_iter->setProperty(j, DVcsEvent::HCROSS);
1131                             }
1132                             f_iter->setType(DVcsEvent::MERGE);
1133                             f_iter->setProperty(i, DVcsEvent::MERGE_RIGHT);
1134                             qCDebug(PLUGIN_GIT) << parent << " is parent of " << commit;
1135                             qCDebug(PLUGIN_GIT) << f_iter->commit() << " is merge";
1136                             parent_checked = true;
1137                             break;
1138                         }
1139                         else
1140                             f_iter->setProperty(i, DVcsEvent::CROSS);
1141                     }
1142                 }
1143             }
1144             //mark HEADs
1145 
1146             if (!branchesShas[i].empty() && commit == branchesShas[i][0])
1147             {
1148                 iter->setType(DVcsEvent::HEAD);
1149                 iter->setProperty(i, DVcsEvent::HEAD);
1150                 heads_checked++;
1151                 qCDebug(PLUGIN_GIT) << "HEAD found";
1152             }
1153             //some optimization
1154             if (heads_checked == branchesShas.count() && parent_checked)
1155                 break;
1156         }
1157     }
1158 
1159     return commitList;
1160 }
1161 
1162 void GitPlugin::initBranchHash(const QString &repo)
1163 {
1164     const QUrl repoUrl = QUrl::fromLocalFile(repo);
1165     const QStringList gitBranches = runSynchronously(branches(repoUrl)).toStringList();
1166     qCDebug(PLUGIN_GIT) << "BRANCHES: " << gitBranches;
1167     //Now root branch is the current branch. In future it should be the longest branch
1168     //other commitLists are got with git-rev-lits branch ^br1 ^ br2
1169     QString root = runSynchronously(currentBranch(repoUrl)).toString();
1170     QScopedPointer<DVcsJob> job(gitRevList(repo, QStringList(root)));
1171     bool ret = job->exec();
1172     Q_ASSERT(ret && job->status()==VcsJob::JobSucceeded && "TODO: provide a fall back in case of failing");
1173     Q_UNUSED(ret);
1174     const QStringList commits = job->output().split(QLatin1Char('\n'), Qt::SkipEmptyParts);
1175     //     qCDebug(PLUGIN_GIT) << "\n\n\n commits" << commits << "\n\n\n";
1176     branchesShas.append(commits);
1177     for (const QString& branch : gitBranches) {
1178         if (branch == root)
1179             continue;
1180         QStringList args(branch);
1181         for (const QString& branch_arg : gitBranches) {
1182             if (branch_arg != branch)
1183                 //man gitRevList for '^'
1184                 args << QLatin1Char('^') + branch_arg;
1185         }
1186         QScopedPointer<DVcsJob> job(gitRevList(repo, args));
1187         bool ret = job->exec();
1188         Q_ASSERT(ret && job->status()==VcsJob::JobSucceeded && "TODO: provide a fall back in case of failing");
1189         Q_UNUSED(ret);
1190         const QStringList commits = job->output().split(QLatin1Char('\n'), Qt::SkipEmptyParts);
1191         //         qCDebug(PLUGIN_GIT) << "\n\n\n commits" << commits << "\n\n\n";
1192         branchesShas.append(commits);
1193     }
1194 }
1195 
1196 //Actually we can just copy the output without parsing. So it's a kind of draft for future
1197 void GitPlugin::parseLogOutput(const DVcsJob* job, QVector<DVcsEvent>& commits) const
1198 {
1199 //     static QRegExp rx_sep( "[-=]+" );
1200 //     static QRegExp rx_date( "date:\\s+([^;]*);\\s+author:\\s+([^;]*).*" );
1201 
1202     static QRegularExpression rx_com( QStringLiteral("commit \\w{1,40}") );
1203 
1204     const auto output = job->output();
1205     const auto lines = output.splitRef(QLatin1Char('\n'), Qt::SkipEmptyParts);
1206 
1207     DVcsEvent item;
1208     QString commitLog;
1209 
1210     for (int i=0; i<lines.count(); ++i) {
1211 //         qCDebug(PLUGIN_GIT) << "line:" << s;
1212         if (rx_com.match(lines[i]).hasMatch()) {
1213 //             qCDebug(PLUGIN_GIT) << "MATCH COMMIT";
1214             item.setCommit(lines[++i].toString());
1215             item.setAuthor(lines[++i].toString());
1216             item.setDate(lines[++i].toString());
1217             item.setLog(commitLog);
1218             commits.append(item);
1219         }
1220         else
1221         {
1222             //FIXME: add this in a loop to the if, like in getAllCommits()
1223             commitLog += lines[i].toString() + QLatin1Char('\n');
1224         }
1225     }
1226 }
1227 
1228 VcsItemEvent::Actions actionsFromString(char c)
1229 {
1230     switch(c) {
1231         case 'A': return VcsItemEvent::Added;
1232         case 'D': return VcsItemEvent::Deleted;
1233         case 'R': return VcsItemEvent::Replaced;
1234         case 'M': return VcsItemEvent::Modified;
1235     }
1236     return VcsItemEvent::Modified;
1237 }
1238 
1239 void GitPlugin::parseGitLogOutput(DVcsJob * job)
1240 {
1241     static QRegExp commitRegex(QStringLiteral("^commit (\\w{8})\\w{32}"));
1242     static QRegExp infoRegex(QStringLiteral("^(\\w+):(.*)"));
1243     static QRegExp modificationsRegex(QStringLiteral("^([A-Z])[0-9]*\t([^\t]+)\t?(.*)"), Qt::CaseSensitive, QRegExp::RegExp2);
1244     //R099    plugins/git/kdevgit.desktop     plugins/git/kdevgit.desktop.cmake
1245     //M       plugins/grepview/CMakeLists.txt
1246 
1247     QList<QVariant> commits;
1248 
1249     QString contents = job->output();
1250     // check if git-log returned anything
1251     if (contents.isEmpty()) {
1252         job->setResults(commits); // empty list
1253         return;
1254     }
1255 
1256     // start parsing the output
1257     QTextStream s(&contents);
1258 
1259     VcsEvent item;
1260     QString message;
1261     bool pushCommit = false;
1262 
1263     while (!s.atEnd()) {
1264         QString line = s.readLine();
1265 
1266         if (commitRegex.exactMatch(line)) {
1267             if (pushCommit) {
1268                 item.setMessage(message.trimmed());
1269                 commits.append(QVariant::fromValue(item));
1270                 item.setItems(QList<VcsItemEvent>());
1271             } else {
1272                 pushCommit = true;
1273             }
1274             VcsRevision rev;
1275             rev.setRevisionValue(commitRegex.cap(1), KDevelop::VcsRevision::GlobalNumber);
1276             item.setRevision(rev);
1277             message.clear();
1278         } else if (infoRegex.exactMatch(line)) {
1279             QString cap1 = infoRegex.cap(1);
1280             if (cap1 == QLatin1String("Author")) {
1281                 item.setAuthor(infoRegex.cap(2).trimmed());
1282             } else if (cap1 == QLatin1String("Date")) {
1283                 item.setDate(QDateTime::fromSecsSinceEpoch(infoRegex.cap(2).trimmed().split(QLatin1Char(' '))[0].toUInt(), Qt::LocalTime));
1284             }
1285         } else if (modificationsRegex.exactMatch(line)) {
1286             VcsItemEvent::Actions a = actionsFromString(modificationsRegex.cap(1).at(0).toLatin1());
1287             QString filenameA = modificationsRegex.cap(2);
1288 
1289             VcsItemEvent itemEvent;
1290             itemEvent.setActions(a);
1291             itemEvent.setRepositoryLocation(filenameA);
1292             if(a==VcsItemEvent::Replaced) {
1293                 QString filenameB = modificationsRegex.cap(3);
1294                 itemEvent.setRepositoryCopySourceLocation(filenameB);
1295             }
1296 
1297             item.addItem(itemEvent);
1298         } else if (line.startsWith(QLatin1String("    "))) {
1299             message += line.midRef(4) + QLatin1Char('\n');
1300         }
1301     }
1302 
1303     item.setMessage(message.trimmed());
1304     commits.append(QVariant::fromValue(item));
1305     job->setResults(commits);
1306 }
1307 
1308 void GitPlugin::parseGitDiffOutput(DVcsJob* job)
1309 {
1310     VcsDiff diff;
1311     diff.setDiff(job->output());
1312     diff.setBaseDiff(repositoryRoot(QUrl::fromLocalFile(job->directory().absolutePath())));
1313     diff.setDepth(usePrefix()? 1 : 0);
1314 
1315     job->setResults(QVariant::fromValue(diff));
1316 }
1317 
1318 static VcsStatusInfo::State lsfilesToState(char id)
1319 {
1320     switch(id) {
1321         case 'H': return VcsStatusInfo::ItemUpToDate; //Cached
1322         case 'S': return VcsStatusInfo::ItemUpToDate; //Skip work tree
1323         case 'M': return VcsStatusInfo::ItemHasConflicts; //unmerged
1324         case 'R': return VcsStatusInfo::ItemDeleted; //removed/deleted
1325         case 'C': return VcsStatusInfo::ItemModified; //modified/changed
1326         case 'K': return VcsStatusInfo::ItemDeleted; //to be killed
1327         case '?': return VcsStatusInfo::ItemUnknown; //other
1328     }
1329     Q_ASSERT(false);
1330     return VcsStatusInfo::ItemUnknown;
1331 }
1332 
1333 void GitPlugin::parseGitStatusOutput_old(DVcsJob* job)
1334 {
1335     const QString output = job->output();
1336     const auto outputLines = output.splitRef(QLatin1Char('\n'), Qt::SkipEmptyParts);
1337 
1338     QDir dir = job->directory();
1339     QMap<QUrl, VcsStatusInfo::State> allStatus;
1340     for (const auto& line : outputLines) {
1341         VcsStatusInfo::State status = lsfilesToState(line[0].toLatin1());
1342 
1343         QUrl url = QUrl::fromLocalFile(dir.absoluteFilePath(line.mid(2).toString()));
1344 
1345         allStatus[url] = status;
1346     }
1347 
1348     QVariantList statuses;
1349     statuses.reserve(allStatus.size());
1350     QMap< QUrl, VcsStatusInfo::State >::const_iterator it = allStatus.constBegin(), itEnd=allStatus.constEnd();
1351     for(; it!=itEnd; ++it) {
1352 
1353         VcsStatusInfo status;
1354         status.setUrl(it.key());
1355         status.setState(it.value());
1356 
1357         statuses.append(QVariant::fromValue<VcsStatusInfo>(status));
1358     }
1359 
1360     job->setResults(statuses);
1361 }
1362 
1363 void GitPlugin::parseGitStatusOutput(DVcsJob* job)
1364 {
1365     const auto output = job->output();
1366     const auto outputLines = output.splitRef(QLatin1Char('\n'), Qt::SkipEmptyParts);
1367     QDir workingDir = job->directory();
1368     QDir dotGit = dotGitDirectory(QUrl::fromLocalFile(workingDir.absolutePath()));
1369 
1370     QVariantList statuses;
1371     QList<QUrl> processedFiles;
1372 
1373     for (const QStringRef& line : outputLines) {
1374         //every line is 2 chars for the status, 1 space then the file desc
1375         QStringRef curr=line.mid(3);
1376         QStringRef state = line.left(2);
1377 
1378         int arrow = curr.indexOf(QLatin1String(" -> "));
1379         if(arrow>=0) {
1380             VcsStatusInfo status;
1381             status.setUrl(QUrl::fromLocalFile(dotGit.absoluteFilePath(curr.toString().left(arrow))));
1382             status.setState(VcsStatusInfo::ItemDeleted);
1383             statuses.append(QVariant::fromValue<VcsStatusInfo>(status));
1384             processedFiles += status.url();
1385 
1386             curr = curr.mid(arrow+4);
1387         }
1388 
1389         if (curr.startsWith(QLatin1Char('\"')) && curr.endsWith(QLatin1Char('\"'))) { //if the path is quoted, unquote
1390             curr = curr.mid(1, curr.size()-2);
1391         }
1392 
1393         VcsStatusInfo status;
1394         ExtendedState ex_state = parseGitState(state);
1395         status.setUrl(QUrl::fromLocalFile(dotGit.absoluteFilePath(curr.toString())));
1396         status.setExtendedState(ex_state);
1397         status.setState(extendedStateToBasic(ex_state));
1398         processedFiles.append(status.url());
1399 
1400         qCDebug(PLUGIN_GIT) << "Checking git status for " << line << curr << status.state();
1401 
1402         statuses.append(QVariant::fromValue<VcsStatusInfo>(status));
1403     }
1404     QStringList paths;
1405     QStringList oldcmd=job->dvcsCommand();
1406     QStringList::const_iterator it=oldcmd.constBegin()+oldcmd.indexOf(QStringLiteral("--"))+1, itEnd=oldcmd.constEnd();
1407     paths.reserve(oldcmd.size());
1408     for(; it!=itEnd; ++it)
1409         paths += *it;
1410 
1411     //here we add the already up to date files
1412     const QStringList files = getLsFiles(job->directory(), QStringList{QStringLiteral("-c"), QStringLiteral("--")} << paths, OutputJob::Silent);
1413     for (const QString& file : files) {
1414         QUrl fileUrl = QUrl::fromLocalFile(workingDir.absoluteFilePath(file));
1415 
1416         if(!processedFiles.contains(fileUrl)) {
1417             VcsStatusInfo status;
1418             status.setUrl(fileUrl);
1419             status.setState(VcsStatusInfo::ItemUpToDate);
1420 
1421             statuses.append(QVariant::fromValue<VcsStatusInfo>(status));
1422         }
1423     }
1424     job->setResults(statuses);
1425 }
1426 
1427 void GitPlugin::parseGitVersionOutput(DVcsJob* job)
1428 {
1429     const auto output = job->output().trimmed();
1430     auto versionString = output.midRef(output.lastIndexOf(QLatin1Char(' ')));
1431     const auto minimumVersion = QVersionNumber(1, 7);
1432     const auto actualVersion = QVersionNumber::fromString(versionString);
1433     m_oldVersion = actualVersion < minimumVersion;
1434     qCDebug(PLUGIN_GIT) << "checking git version" << versionString << actualVersion << "against" << minimumVersion
1435                         << m_oldVersion;
1436 }
1437 
1438 QStringList GitPlugin::getLsFiles(const QDir &directory, const QStringList &args,
1439     KDevelop::OutputJob::OutputJobVerbosity verbosity)
1440 {
1441     QScopedPointer<DVcsJob> job(lsFiles(directory, args, verbosity));
1442     if (job->exec() && job->status() == KDevelop::VcsJob::JobSucceeded)
1443         return job->output().split(QLatin1Char('\n'), Qt::SkipEmptyParts);
1444 
1445     return QStringList();
1446 }
1447 
1448 DVcsJob* GitPlugin::gitRevParse(const QString &repository, const QStringList &args,
1449     KDevelop::OutputJob::OutputJobVerbosity verbosity)
1450 {
1451     auto* job = new GitJob(QDir(repository), this, verbosity);
1452     *job << "git" << "rev-parse" << args;
1453 
1454     return job;
1455 }
1456 
1457 DVcsJob* GitPlugin::gitRevList(const QString& directory, const QStringList& args)
1458 {
1459     auto* job = new GitJob(urlDir(QUrl::fromLocalFile(directory)), this, KDevelop::OutputJob::Silent);
1460     {
1461         *job << "git" << "rev-list" << args;
1462         return job;
1463     }
1464 }
1465 
1466 constexpr int _pair(char a, char b) { return a*256 + b;}
1467 
1468 GitPlugin::ExtendedState GitPlugin::parseGitState(const QStringRef& msg)
1469 {
1470     Q_ASSERT(msg.size()==1 || msg.size()==2);
1471     ExtendedState ret = GitInvalid;
1472 
1473     if(msg.contains(QLatin1Char('U')) || msg == QLatin1String("AA") || msg == QLatin1String("DD"))
1474         ret = GitConflicts;
1475     else switch(_pair(msg.at(0).toLatin1(), msg.at(1).toLatin1()))
1476     {
1477         case _pair(' ', ' '):
1478             ret = GitXX;
1479             break;
1480         case _pair(' ','T'): // Typechange
1481         case _pair(' ','M'):
1482             ret = GitXM;
1483             break;
1484         case _pair ( ' ','D' ) :
1485             ret = GitXD;
1486             break;
1487         case _pair ( ' ','R' ) :
1488             ret = GitXR;
1489             break;
1490         case _pair ( ' ','C' ) :
1491             ret = GitXC;
1492             break;
1493         case _pair ( 'T',' ' ) : // Typechange
1494         case _pair ( 'M',' ' ) :
1495             ret = GitMX;
1496             break;
1497         case _pair ( 'M','M' ) :
1498             ret = GitMM;
1499             break;
1500         case _pair ( 'M','D' ) :
1501             ret = GitMD;
1502             break;
1503         case _pair ( 'A',' ' ) :
1504             ret = GitAX;
1505             break;
1506         case _pair ( 'A','M' ) :
1507             ret = GitAM;
1508             break;
1509         case _pair ( 'A','D' ) :
1510             ret = GitAD;
1511             break;
1512         case _pair ( 'D',' ' ) :
1513             ret = GitDX;
1514             break;
1515         case _pair ( 'D','R' ) :
1516             ret = GitDR;
1517             break;
1518         case _pair ( 'D','C' ) :
1519             ret = GitDC;
1520             break;
1521         case _pair ( 'R',' ' ) :
1522             ret = GitRX;
1523             break;
1524         case _pair ( 'R','M' ) :
1525             ret = GitRM;
1526             break;
1527         case _pair ( 'R','D' ) :
1528             ret = GitRD;
1529             break;
1530         case _pair ( 'C',' ' ) :
1531             ret = GitCX;
1532             break;
1533         case _pair ( 'C','M' ) :
1534             ret = GitCM;
1535             break;
1536         case _pair ( 'C','D' ) :
1537             ret = GitCD;
1538             break;
1539         case _pair ( '?','?' ) :
1540             ret = GitUntracked;
1541             break;
1542         default:
1543             qCDebug(PLUGIN_GIT) << "Git status not identified:" << msg;
1544             ret = GitInvalid;
1545             break;
1546     }
1547 
1548     return ret;
1549 }
1550 
1551 KDevelop::VcsStatusInfo::State GitPlugin::extendedStateToBasic(const GitPlugin::ExtendedState state)
1552 {
1553     switch(state) {
1554         case GitXX: return VcsStatusInfo::ItemUpToDate;
1555         case GitXM: return VcsStatusInfo::ItemModified;
1556         case GitXD: return VcsStatusInfo::ItemDeleted;
1557         case GitXR: return VcsStatusInfo::ItemModified;
1558         case GitXC: return VcsStatusInfo::ItemModified;
1559         case GitMX: return VcsStatusInfo::ItemModified;
1560         case GitMM: return VcsStatusInfo::ItemModified;
1561         case GitMD: return VcsStatusInfo::ItemDeleted;
1562         case GitAX: return VcsStatusInfo::ItemAdded;
1563         case GitAM: return VcsStatusInfo::ItemAdded;
1564         case GitAD: return VcsStatusInfo::ItemAdded;
1565         case GitDX: return VcsStatusInfo::ItemDeleted;
1566         case GitDR: return VcsStatusInfo::ItemDeleted;
1567         case GitDC: return VcsStatusInfo::ItemDeleted;
1568         case GitRX: return VcsStatusInfo::ItemModified;
1569         case GitRM: return VcsStatusInfo::ItemModified;
1570         case GitRD: return VcsStatusInfo::ItemDeleted;
1571         case GitCX: return VcsStatusInfo::ItemModified;
1572         case GitCM: return VcsStatusInfo::ItemModified;
1573         case GitCD: return VcsStatusInfo::ItemDeleted;
1574         case GitUntracked: return VcsStatusInfo::ItemUnknown;
1575         case GitConflicts: return VcsStatusInfo::ItemHasConflicts;
1576         case GitInvalid: return VcsStatusInfo::ItemUnknown;
1577     }
1578     return VcsStatusInfo::ItemUnknown;
1579 }
1580 
1581 
1582 StandardJob::StandardJob(IPlugin* parent, KJob* job,
1583                                  OutputJob::OutputJobVerbosity verbosity)
1584     : VcsJob(parent, verbosity)
1585     , m_job(job)
1586     , m_plugin(parent)
1587     , m_status(JobNotStarted)
1588 {}
1589 
1590 void StandardJob::start()
1591 {
1592     connect(m_job, &KJob::result, this, &StandardJob::result);
1593     m_job->start();
1594     m_status=JobRunning;
1595 }
1596 
1597 void StandardJob::result(KJob* job)
1598 {
1599     if (job->error() == 0) {
1600         m_status = JobSucceeded;
1601         setError(NoError);
1602     } else {
1603         m_status = JobFailed;
1604         setError(UserDefinedError);
1605     }
1606     emitResult();
1607 }
1608 
1609 VcsJob* GitPlugin::copy(const QUrl& localLocationSrc, const QUrl& localLocationDstn)
1610 {
1611     //TODO: Probably we should "git add" after
1612     return new StandardJob(this, KIO::copy(localLocationSrc, localLocationDstn), KDevelop::OutputJob::Silent);
1613 }
1614 
1615 VcsJob* GitPlugin::move(const QUrl& source, const QUrl& destination)
1616 {
1617     QDir dir = urlDir(source);
1618 
1619     QFileInfo fileInfo(source.toLocalFile());
1620     if (fileInfo.isDir()) {
1621         if (isEmptyDirStructure(QDir(source.toLocalFile()))) {
1622             //move empty folder, git doesn't do that
1623             qCDebug(PLUGIN_GIT) << "empty folder" << source;
1624             return new StandardJob(this, KIO::move(source, destination), KDevelop::OutputJob::Silent);
1625         }
1626     }
1627 
1628     const QStringList otherStr = getLsFiles(dir, QStringList{QStringLiteral("--others"), QStringLiteral("--"), source.toLocalFile()}, KDevelop::OutputJob::Silent);
1629     if(otherStr.isEmpty()) {
1630         auto* job = new GitJob(dir, this, KDevelop::OutputJob::Verbose);
1631         *job << "git" << "mv" << source.toLocalFile() << destination.toLocalFile();
1632         return job;
1633     } else {
1634         return new StandardJob(this, KIO::move(source, destination), KDevelop::OutputJob::Silent);
1635     }
1636 }
1637 
1638 void GitPlugin::parseGitRepoLocationOutput(DVcsJob* job)
1639 {
1640     job->setResults(QVariant::fromValue(QUrl::fromLocalFile(job->output())));
1641 }
1642 
1643 VcsJob* GitPlugin::repositoryLocation(const QUrl& localLocation)
1644 {
1645     auto* job = new GitJob(urlDir(localLocation), this);
1646     //Probably we should check first if origin is the proper remote we have to use but as a first attempt it works
1647     *job << "git" << "config" << "remote.origin.url";
1648     connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitRepoLocationOutput);
1649     return job;
1650 }
1651 
1652 VcsJob* GitPlugin::pull(const KDevelop::VcsLocation& localOrRepoLocationSrc, const QUrl& localRepositoryLocation)
1653 {
1654     auto* job = new GitJob(urlDir(localRepositoryLocation), this);
1655     job->setCommunicationMode(KProcess::MergedChannels);
1656     *job << "git" << "pull";
1657     if(!localOrRepoLocationSrc.localUrl().isEmpty())
1658         *job << localOrRepoLocationSrc.localUrl().url();
1659     return job;
1660 }
1661 
1662 VcsJob* GitPlugin::push(const QUrl& localRepositoryLocation, const KDevelop::VcsLocation& localOrRepoLocationDst)
1663 {
1664     auto* job = new GitJob(urlDir(localRepositoryLocation), this);
1665     job->setCommunicationMode(KProcess::MergedChannels);
1666     *job << "git" << "push";
1667     if(!localOrRepoLocationDst.localUrl().isEmpty())
1668         *job << localOrRepoLocationDst.localUrl().url();
1669     return job;
1670 }
1671 
1672 VcsJob* GitPlugin::resolve(const QList<QUrl>& localLocations, IBasicVersionControl::RecursionMode recursion)
1673 {
1674     return add(localLocations, recursion);
1675 }
1676 
1677 VcsJob* GitPlugin::update(const QList<QUrl>& localLocations, const KDevelop::VcsRevision& rev, IBasicVersionControl::RecursionMode recursion)
1678 {
1679     if(rev.revisionType()==VcsRevision::Special && rev.revisionValue().value<VcsRevision::RevisionSpecialType>()==VcsRevision::Head) {
1680         return pull(VcsLocation(), localLocations.first());
1681     } else {
1682         auto* job = new GitJob(urlDir(localLocations.first()), this);
1683         {
1684             //Probably we should check first if origin is the proper remote we have to use but as a first attempt it works
1685             *job << "git" << "checkout" << rev.revisionValue().toString() << "--";
1686             *job << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations));
1687             return job;
1688         }
1689     }
1690 }
1691 
1692 void GitPlugin::setupCommitMessageEditor(const QUrl& localLocation, KTextEdit* editor) const
1693 {
1694     new GitMessageHighlighter(editor);
1695     QFile mergeMsgFile(dotGitDirectory(localLocation).filePath(QStringLiteral(".git/MERGE_MSG")));
1696     // Some limit on the file size should be set since whole content is going to be read into
1697     // the memory. 1Mb seems to be good value since it's rather strange to have so huge commit
1698     // message.
1699     static const qint64 maxMergeMsgFileSize = 1024*1024;
1700     if (mergeMsgFile.size() > maxMergeMsgFileSize || !mergeMsgFile.open(QIODevice::ReadOnly))
1701         return;
1702 
1703     QString mergeMsg = QString::fromLocal8Bit(mergeMsgFile.read(maxMergeMsgFileSize));
1704     editor->setPlainText(mergeMsg);
1705 }
1706 
1707 class GitVcsLocationWidget : public KDevelop::StandardVcsLocationWidget
1708 {
1709     Q_OBJECT
1710     public:
1711         explicit GitVcsLocationWidget(QWidget* parent = nullptr)
1712             : StandardVcsLocationWidget(parent)
1713         {}
1714 
1715         bool isCorrect() const override
1716         {
1717             return !url().isEmpty();
1718         }
1719 };
1720 
1721 KDevelop::VcsLocationWidget* GitPlugin::vcsLocation(QWidget* parent) const
1722 {
1723     return new GitVcsLocationWidget(parent);
1724 }
1725 
1726 void GitPlugin::registerRepositoryForCurrentBranchChanges(const QUrl& repository)
1727 {
1728     QDir dir = dotGitDirectory(repository);
1729     QString headFile = dir.absoluteFilePath(QStringLiteral(".git/HEAD"));
1730     m_watcher->addFile(headFile);
1731 }
1732 
1733 void GitPlugin::fileChanged(const QString& file)
1734 {
1735     Q_ASSERT(file.endsWith(QLatin1String("HEAD")));
1736     //SMTH/.git/HEAD -> SMTH/
1737     const QUrl fileUrl = Path(file).parent().parent().toUrl();
1738 
1739     //We need to delay the emitted signal, otherwise the branch hasn't change yet
1740     //and the repository is not functional
1741     m_branchesChange.append(fileUrl);
1742     QTimer::singleShot(1000, this, &GitPlugin::delayedBranchChanged);
1743 }
1744 
1745 void GitPlugin::delayedBranchChanged()
1746 {
1747     emit repositoryBranchChanged(m_branchesChange.takeFirst());
1748 }
1749 
1750 CheckInRepositoryJob* GitPlugin::isInRepository(KTextEditor::Document* document)
1751 {
1752     CheckInRepositoryJob* job = new GitPluginCheckInRepositoryJob(document, repositoryRoot(document->url()).path());
1753     job->start();
1754     return job;
1755 }
1756 
1757 DVcsJob* GitPlugin::setConfigOption(const QUrl& repository, const QString& key, const QString& value, bool global)
1758 {
1759     auto job = new GitJob(urlDir(repository), this);
1760     QStringList args;
1761     args << QStringLiteral("git") << QStringLiteral("config");
1762     if(global)
1763         args << QStringLiteral("--global");
1764     args << key << value;
1765     *job << args;
1766     return job;
1767 }
1768 
1769 QString GitPlugin::readConfigOption(const QUrl& repository, const QString& key)
1770 {
1771     QProcess exec;
1772     exec.setWorkingDirectory(urlDir(repository).absolutePath());
1773     exec.start(QStringLiteral("git"), QStringList{QStringLiteral("config"), QStringLiteral("--get"), key});
1774     exec.waitForFinished();
1775     return QString::fromUtf8(exec.readAllStandardOutput().trimmed());
1776 }
1777 
1778 #include "gitplugin.moc"
1779 #include "moc_gitplugin.cpp"