gitplugin.cpp 51.1 KB
Newer Older
1 2
/***************************************************************************
 *   Copyright 2008 Evgeniy Ivanov <powerfox@kde.ru>                       *
3
 *   Copyright 2009 Hugo Parente Lima <hugo.pl@gmail.com>                  *
4
 *   Copyright 2010 Aleix Pol Gonzalez <aleixpol@kde.org>                  *
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
 *                                                                         *
 *   This program is free software; you can redistribute it and/or         *
 *   modify it under the terms of the GNU General Public License as        *
 *   published by the Free Software Foundation; either version 2 of        *
 *   the License or (at your option) version 3 or any later version        *
 *   accepted by the membership of KDE e.V. (or its successor approved     *
 *   by the membership of KDE e.V.), which shall act as a proxy            *
 *   defined in Section 14 of version 3 of the license.                    *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
 ***************************************************************************/

#include "gitplugin.h"

25
#include <QDateTime>
Dāvis Mosāns's avatar
Dāvis Mosāns committed
26
#include <QDebug>
27 28 29 30
#include <QDir>
#include <QFileInfo>
#include <QMenu>
#include <QTimer>
31

32 33
#include <interfaces/icore.h>
#include <interfaces/iprojectcontroller.h>
34
#include <interfaces/iproject.h>
35

Milian Wolff's avatar
Milian Wolff committed
36 37
#include <util/path.h>

38 39
#include <vcs/vcsjob.h>
#include <vcs/vcsrevision.h>
40
#include <vcs/vcsevent.h>
41
#include <vcs/dvcs/dvcsjob.h>
42
#include <vcs/vcsannotation.h>
43
#include <vcs/widgets/standardvcslocationwidget.h>
44
#include "gitclonejob.h"
45 46 47
#include <interfaces/contextmenuextension.h>
#include <interfaces/iruncontroller.h>
#include "stashmanagerdialog.h"
48 49 50 51

#include <KDirWatch>
#include <KIO/CopyJob>
#include <KLocalizedString>
52
#include <KMessageBox>
53
#include <KTextEdit>
54
#include <KTextEditor/Document>
55

56
#include "gitjob.h"
57
#include "gitmessagehighlighter.h"
58
#include "gitplugincheckinrepositoryjob.h"
Dāvis Mosāns's avatar
Dāvis Mosāns committed
59 60 61
#include "debug.h"

Q_LOGGING_CATEGORY(PLUGIN_GIT, "kdevplatform.plugins.git")
62

63 64
using namespace KDevelop;

65 66 67 68 69 70 71 72 73 74
QVariant runSynchronously(KDevelop::VcsJob* job)
{
    QVariant ret;
    if(job->exec() && job->status()==KDevelop::VcsJob::JobSucceeded) {
        ret = job->fetchResults();
    }
    delete job;
    return ret;
}

75 76
namespace
{
Milian Wolff's avatar
Milian Wolff committed
77 78

QDir dotGitDirectory(const QUrl& dirPath)
79
{
80
    const QFileInfo finfo(dirPath.toLocalFile());
81
    QDir dir = finfo.isDir() ? QDir(finfo.filePath()): finfo.absoluteDir();
Dāvis Mosāns's avatar
Dāvis Mosāns committed
82

83
    static const QString gitDir = QStringLiteral(".git");
84
    while (!dir.exists(gitDir) && dir.cdUp()) {} // cdUp, until there is a sub-directory called .git
85

86
    if (dir.isRoot()) {
Kevin Funk's avatar
Kevin Funk committed
87
        qCWarning(PLUGIN_GIT) << "couldn't find the git root for" << dirPath;
88 89
    }

90 91 92
    return dir;
}

93 94 95 96
/**
 * Whenever a directory is provided, change it for all the files in it but not inner directories,
 * that way we make sure we won't get into recursion,
 */
Milian Wolff's avatar
Milian Wolff committed
97
static QList<QUrl> preventRecursion(const QList<QUrl>& urls)
98
{
Milian Wolff's avatar
Milian Wolff committed
99 100
    QList<QUrl> ret;
    foreach(const QUrl& url, urls) {
101 102 103 104
        QDir d(url.toLocalFile());
        if(d.exists()) {
            QStringList entries = d.entryList(QDir::Files | QDir::NoDotAndDotDot);
            foreach(const QString& entry, entries) {
Milian Wolff's avatar
Milian Wolff committed
105
                QUrl entryUrl = QUrl::fromLocalFile(d.absoluteFilePath(entry));
106 107 108 109 110 111 112 113
                ret += entryUrl;
            }
        } else
            ret += url;
    }
    return ret;
}

114 115 116 117 118 119 120 121
QString toRevisionName(const KDevelop::VcsRevision& rev, QString currentRevision=QString())
{
    switch(rev.revisionType()) {
        case VcsRevision::Special:
            switch(rev.revisionValue().value<VcsRevision::RevisionSpecialType>()) {
                case VcsRevision::Head:
                    return "^HEAD";
                case VcsRevision::Base:
122
                    return "";
123 124 125 126 127 128
                case VcsRevision::Working:
                    return "";
                case VcsRevision::Previous:
                    Q_ASSERT(!currentRevision.isEmpty());
                    return currentRevision + "^1";
                case VcsRevision::Start:
129 130
                    return "";
                case VcsRevision::UserSpecialType: //Not used
131 132 133 134 135 136 137 138 139 140 141 142 143 144
                    Q_ASSERT(false && "i don't know how to do that");
            }
            break;
        case VcsRevision::GlobalNumber:
            return rev.revisionValue().toString();
        case VcsRevision::Date:
        case VcsRevision::FileNumber:
        case VcsRevision::Invalid:
        case VcsRevision::UserSpecialType:
            Q_ASSERT(false);
    }
    return QString();
}

145
QString revisionInterval(const KDevelop::VcsRevision& rev, const KDevelop::VcsRevision& limit)
146 147
{
    QString ret;
148
    if(rev.revisionType()==VcsRevision::Special &&
Yuri Chornoivan's avatar
Yuri Chornoivan committed
149
                rev.revisionValue().value<VcsRevision::RevisionSpecialType>()==VcsRevision::Start) //if we want it to the beginning just put the revisionInterval
150 151
        ret = toRevisionName(limit, QString());
    else {
152
        QString dst = toRevisionName(limit);
153 154 155 156 157 158 159 160 161
        if(dst.isEmpty())
            ret = dst;
        else {
            QString src = toRevisionName(rev, dst);
            if(src.isEmpty())
                ret = src;
            else
                ret = src+".."+dst;
        }
162
    }
163 164 165
    return ret;
}

Milian Wolff's avatar
Milian Wolff committed
166
QDir urlDir(const QUrl& url)
167 168 169 170 171 172 173
{
    QFileInfo f(url.toLocalFile());
    if(f.isDir())
        return QDir(url.toLocalFile());
    else
        return f.absoluteDir();
}
Milian Wolff's avatar
Milian Wolff committed
174
QDir urlDir(const QList<QUrl>& urls) { return urlDir(urls.first()); } //TODO: could be improved
175

176 177
}

178
GitPlugin::GitPlugin( QObject *parent, const QVariantList & )
179
    : DistributedVersionControlPlugin(parent, "kdevgit"), m_oldVersion(false), m_usePrefix(true)
180
{
181
    if (QStandardPaths::findExecutable("git").isEmpty()) {
182
        m_hasError = true;
Laurent Montel's avatar
Laurent Montel committed
183
        m_errorDescription = i18n("git is not installed");
184 185 186
        return;
    }

187 188
    KDEV_USE_EXTENSION_INTERFACE( KDevelop::IBasicVersionControl )
    KDEV_USE_EXTENSION_INTERFACE( KDevelop::IDistributedVersionControl )
189
    KDEV_USE_EXTENSION_INTERFACE( KDevelop::IBranchingVersionControl )
190

191
    m_hasError = false;
192
    setObjectName("Git");
Dāvis Mosāns's avatar
Dāvis Mosāns committed
193

194 195
    DVcsJob* versionJob = new DVcsJob(QDir::tempPath(), this, KDevelop::OutputJob::Silent);
    *versionJob << "git" << "--version";
196
    connect(versionJob, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitVersionOutput);
197
    ICore::self()->runController()->registerJob(versionJob);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
198

199
    m_watcher = new KDirWatch(this);
200 201
    connect(m_watcher, &KDirWatch::dirty, this, &GitPlugin::fileChanged);
    connect(m_watcher, &KDirWatch::created, this, &GitPlugin::fileChanged);
202 203 204
}

GitPlugin::~GitPlugin()
205
{}
206

207 208 209 210 211
bool emptyOutput(DVcsJob* job)
{
    QScopedPointer<DVcsJob> _job(job);
    if(job->exec() && job->status()==VcsJob::JobSucceeded)
        return job->rawOutput().trimmed().isEmpty();
Dāvis Mosāns's avatar
Dāvis Mosāns committed
212

213 214 215 216 217 218 219 220
    return false;
}

bool GitPlugin::hasStashes(const QDir& repository)
{
    return !emptyOutput(gitStash(repository, QStringList("list"), KDevelop::OutputJob::Silent));
}

221
bool GitPlugin::hasModifications(const QDir& d)
222
{
223
    return !emptyOutput(lsFiles(d, QStringList("-m"), OutputJob::Silent));
224 225
}

Milian Wolff's avatar
Milian Wolff committed
226
bool GitPlugin::hasModifications(const QDir& repo, const QUrl& file)
227 228 229 230
{
    return !emptyOutput(lsFiles(repo, QStringList() << "-m" << file.path(), OutputJob::Silent));
}

Milian Wolff's avatar
Milian Wolff committed
231
void GitPlugin::additionalMenuEntries(QMenu* menu, const QList<QUrl>& urls)
232 233
{
    m_urls = urls;
Dāvis Mosāns's avatar
Dāvis Mosāns committed
234

235
    QDir dir=urlDir(urls);
236
    bool hasSt = hasStashes(dir);
237
    menu->addSeparator()->setText(i18n("Git Stashes"));
238 239 240
    menu->addAction(i18n("Stash Manager"), this, SLOT(ctxStashManager()))->setEnabled(hasSt);
    menu->addAction(i18n("Push Stash"), this, SLOT(ctxPushStash()));
    menu->addAction(i18n("Pop Stash"), this, SLOT(ctxPopStash()))->setEnabled(hasSt);
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258
}

void GitPlugin::ctxPushStash()
{
    VcsJob* job = gitStash(urlDir(m_urls), QStringList(), KDevelop::OutputJob::Verbose);
    ICore::self()->runController()->registerJob(job);
}

void GitPlugin::ctxPopStash()
{
    VcsJob* job = gitStash(urlDir(m_urls), QStringList("pop"), KDevelop::OutputJob::Verbose);
    ICore::self()->runController()->registerJob(job);
}

void GitPlugin::ctxStashManager()
{
    QPointer<StashManagerDialog> d = new StashManagerDialog(urlDir(m_urls), this, 0);
    d->exec();
Dāvis Mosāns's avatar
Dāvis Mosāns committed
259

260 261 262
    delete d;
}

263
DVcsJob* GitPlugin::errorsFound(const QString& error, KDevelop::OutputJob::OutputJobVerbosity verbosity=OutputJob::Verbose)
264
{
265
    DVcsJob* j = new DVcsJob(QDir::temp(), this, verbosity);
266 267 268 269
    *j << "echo" << i18n("error: %1", error) << "-n";
    return j;
}

270
QString GitPlugin::name() const
271
{
272 273 274
    return QLatin1String("Git");
}

Milian Wolff's avatar
Milian Wolff committed
275
QUrl GitPlugin::repositoryRoot(const QUrl& path)
276
{
Milian Wolff's avatar
Milian Wolff committed
277
    return QUrl::fromLocalFile(dotGitDirectory(path).absolutePath());
278 279
}

Milian Wolff's avatar
Milian Wolff committed
280
bool GitPlugin::isValidDirectory(const QUrl & dirPath)
281
{
282
    QDir dir=dotGitDirectory(dirPath);
283

284
    return dir.cd(".git") && dir.exists("HEAD");
285 286
}

Milian Wolff's avatar
Milian Wolff committed
287
bool GitPlugin::isVersionControlled(const QUrl &path)
288
{
289
    QFileInfo fsObject(path.toLocalFile());
290 291 292
    if (!fsObject.exists()) {
        return false;
    }
293
    if (fsObject.isDir()) {
294
        return isValidDirectory(path);
295
    }
296

297 298
    QString filename = fsObject.fileName();

299
    QStringList otherFiles = getLsFiles(fsObject.dir(), QStringList("--") << filename, KDevelop::OutputJob::Silent);
300
    return !otherFiles.empty();
301 302
}

Milian Wolff's avatar
Milian Wolff committed
303
VcsJob* GitPlugin::init(const QUrl &directory)
304
{
305
    DVcsJob* job = new DVcsJob(urlDir(directory), this);
306
    job->setType(VcsJob::Import);
307 308
    *job << "git" << "init";
    return job;
309 310
}

Milian Wolff's avatar
Milian Wolff committed
311
VcsJob* GitPlugin::createWorkingCopy(const KDevelop::VcsLocation & source, const QUrl& dest, KDevelop::IBasicVersionControl::RecursionMode)
312
{
313
    DVcsJob* job = new GitCloneJob(urlDir(dest), this);
314
    job->setType(VcsJob::Import);
315 316
    *job << "git" << "clone" << "--progress" << "--" << source.localUrl().url() << dest;
    return job;
317 318
}

Milian Wolff's avatar
Milian Wolff committed
319
VcsJob* GitPlugin::add(const QList<QUrl>& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion)
320 321
{
    if (localLocations.empty())
322
        return errorsFound(i18n("Did not specify the list of files"), OutputJob::Verbose);
323

324
    DVcsJob* job = new GitJob(dotGitDirectory(localLocations.front()), this);
325
    job->setType(VcsJob::Add);
326
    *job << "git" << "add" << "--" << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations));
327
    return job;
328 329
}

Milian Wolff's avatar
Milian Wolff committed
330
KDevelop::VcsJob* GitPlugin::status(const QList<QUrl>& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion)
331
{
332 333 334
    if (localLocations.empty())
        return errorsFound(i18n("Did not specify the list of files"), OutputJob::Verbose);

335
    DVcsJob* job = new GitJob(urlDir(localLocations), this, OutputJob::Silent);
336
    job->setType(VcsJob::Status);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
337

338 339
    if(m_oldVersion) {
        *job << "git" << "ls-files" << "-t" << "-m" << "-c" << "-o" << "-d" << "-k" << "--directory";
340
        connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitStatusOutput_old);
341 342
    } else {
        *job << "git" << "status" << "--porcelain";
343
        job->setIgnoreError(true);
344
        connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitStatusOutput);
345 346
    }
    *job << "--" << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations));
347

348
    return job;
349 350
}

Milian Wolff's avatar
Milian Wolff committed
351
VcsJob* GitPlugin::diff(const QUrl& fileOrDirectory, const KDevelop::VcsRevision& srcRevision, const KDevelop::VcsRevision& dstRevision,
352
                        VcsDiff::Type /*type*/, IBasicVersionControl::RecursionMode recursion)
353
{
354
    //TODO: control different types
Milian Wolff's avatar
Milian Wolff committed
355

356
    DVcsJob* job = new GitJob(dotGitDirectory(fileOrDirectory), this, KDevelop::OutputJob::Silent);
357
    job->setType(VcsJob::Diff);
358
    *job << "git" << "diff" << "--no-color" << "--no-ext-diff";
359 360 361 362 363
    if (!usePrefix()) {
        // KDE's ReviewBoard now requires p1 patchfiles, so `git diff --no-prefix` to generate p0 patches
        // has become optional.
        *job << "--no-prefix";
    }
364 365 366 367 368 369 370 371 372 373
    if(srcRevision.revisionType()==VcsRevision::Special
        && dstRevision.revisionType()==VcsRevision::Special
        && srcRevision.specialType()==VcsRevision::Base
        && dstRevision.specialType()==VcsRevision::Working)
        *job << "HEAD";
    else {
        QString revstr = revisionInterval(srcRevision, dstRevision);
        if(!revstr.isEmpty())
            *job << revstr;
    }
Milian Wolff's avatar
Milian Wolff committed
374 375 376 377 378 379 380 381

    *job << "--";
    if (recursion == IBasicVersionControl::Recursive) {
        *job << fileOrDirectory;
    } else {
        *job << preventRecursion(QList<QUrl>() << fileOrDirectory);
    }

382
    connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitDiffOutput);
383
    return job;
384 385
}

Milian Wolff's avatar
Milian Wolff committed
386
VcsJob* GitPlugin::revert(const QList<QUrl>& localLocations, IBasicVersionControl::RecursionMode recursion)
387
{
388 389
    if(localLocations.isEmpty() )
        return errorsFound(i18n("Could not revert changes"), OutputJob::Verbose);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
390

391 392 393 394
    QDir repo = urlDir(repositoryRoot(localLocations.first()));
    QString modified;
    for (const auto& file: localLocations) {
        if (hasModifications(repo, file)) {
Milian Wolff's avatar
Milian Wolff committed
395
            modified.append(file.toDisplayString(QUrl::PreferLocalFile) + "<br/>");
396 397 398 399 400 401 402 403 404 405
        }
    }
    if (!modified.isEmpty()) {
        auto res = KMessageBox::questionYesNo(nullptr, i18n("The following files have uncommited changes, "
                                              "which will be lost. Continue?") + "<br/><br/>" + modified);
        if (res != KMessageBox::Yes) {
            return errorsFound(QString(), OutputJob::Silent);
        }
    }

406
    DVcsJob* job = new GitJob(dotGitDirectory(localLocations.front()), this);
407
    job->setType(VcsJob::Revert);
408 409
    *job << "git" << "checkout" << "--";
    *job << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations));
Dāvis Mosāns's avatar
Dāvis Mosāns committed
410

411
    return job;
412 413 414
}


415 416 417
//TODO: git doesn't like empty messages, but "KDevelop didn't provide any message, it may be a bug" looks ugly...
//If no files specified then commit already added files
VcsJob* GitPlugin::commit(const QString& message,
Milian Wolff's avatar
Milian Wolff committed
418
                             const QList<QUrl>& localLocations,
419 420
                             KDevelop::IBasicVersionControl::RecursionMode recursion)
{
421
    if (localLocations.empty() || message.isEmpty())
422
        return errorsFound(i18n("No files or message specified"));
423

424 425
    QDir dir = dotGitDirectory(localLocations.front());
    DVcsJob* job = new DVcsJob(dir, this);
426
    job->setType(VcsJob::Commit);
Milian Wolff's avatar
Milian Wolff committed
427
    QList<QUrl> files = (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations));
428
    addNotVersionedFiles(dir, files);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
429

430
    *job << "git" << "commit" << "-m" << message;
431
    *job << "--" << files;
432
    return job;
433
}
434

Milian Wolff's avatar
Milian Wolff committed
435
void GitPlugin::addNotVersionedFiles(const QDir& dir, const QList<QUrl>& files)
436 437
{
    QStringList otherStr = getLsFiles(dir, QStringList() << "--others", KDevelop::OutputJob::Silent);
Milian Wolff's avatar
Milian Wolff committed
438
    QList<QUrl> toadd, otherFiles;
Dāvis Mosāns's avatar
Dāvis Mosāns committed
439

440
    foreach(const QString& file, otherStr) {
Milian Wolff's avatar
Milian Wolff committed
441
        QUrl v = QUrl::fromLocalFile(dir.absoluteFilePath(file));
Dāvis Mosāns's avatar
Dāvis Mosāns committed
442

443 444
        otherFiles += v;
    }
Dāvis Mosāns's avatar
Dāvis Mosāns committed
445

446
    //We add the files that are not versioned
Milian Wolff's avatar
Milian Wolff committed
447
    foreach(const QUrl& file, files) {
448 449 450
        if(otherFiles.contains(file) && QFileInfo(file.toLocalFile()).isFile())
            toadd += file;
    }
Dāvis Mosāns's avatar
Dāvis Mosāns committed
451

452 453 454 455
    if(!toadd.isEmpty()) {
        VcsJob* job = add(toadd);
        job->exec();
    }
456 457
}

458 459
bool isEmptyDirStructure(const QDir &dir)
{
460
    foreach (const QFileInfo &i, dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot)) {
461 462 463 464 465 466 467 468 469
        if (i.isDir()) {
            if (!isEmptyDirStructure(QDir(i.filePath()))) return false;
        } else if (i.isFile()) {
            return false;
        }
    }
    return true;
}

Milian Wolff's avatar
Milian Wolff committed
470
VcsJob* GitPlugin::remove(const QList<QUrl>& files)
471
{
472 473
    if (files.isEmpty())
        return errorsFound(i18n("No files to remove"));
474 475 476
    QDir dotGitDir = dotGitDirectory(files.front());


Milian Wolff's avatar
Milian Wolff committed
477
    QList<QUrl> files_(files);
478

Milian Wolff's avatar
Milian Wolff committed
479
    QMutableListIterator<QUrl> i(files_);
480
    while (i.hasNext()) {
Milian Wolff's avatar
Milian Wolff committed
481
        QUrl file = i.next();
482
        QFileInfo fileInfo(file.toLocalFile());
483 484 485 486

        QStringList otherStr = getLsFiles(dotGitDir, QStringList() << "--others" << "--" << file.toLocalFile(), KDevelop::OutputJob::Silent);
        if(!otherStr.isEmpty()) {
            //remove files not under version control
Milian Wolff's avatar
Milian Wolff committed
487
            QList<QUrl> otherFiles;
488
            foreach(const QString &f, otherStr) {
Milian Wolff's avatar
Milian Wolff committed
489
                otherFiles << QUrl::fromLocalFile(dotGitDir.path()+'/'+f);
490 491 492 493 494
            }
            if (fileInfo.isFile()) {
                //if it's an unversioned file we are done, don't use git rm on it
                i.remove();
            }
495 496 497

            auto trashJob = KIO::trash(otherFiles);
            trashJob->exec();
Dāvis Mosāns's avatar
Dāvis Mosāns committed
498
            qCDebug(PLUGIN_GIT) << "other files" << otherFiles;
499 500
        }

501
        if (fileInfo.isDir()) {
502 503
            if (isEmptyDirStructure(QDir(file.toLocalFile()))) {
                //remove empty folders, git doesn't do that
504 505
                auto trashJob = KIO::trash(file);
                trashJob->exec();
Dāvis Mosāns's avatar
Dāvis Mosāns committed
506
                qCDebug(PLUGIN_GIT) << "empty folder, removing" << file;
507
                //we already deleted it, don't use git rm on it
508 509 510 511 512 513
                i.remove();
            }
        }
    }

    if (files_.isEmpty()) return 0;
514

515 516
    DVcsJob* job = new GitJob(dotGitDir, this);
    job->setType(VcsJob::Remove);
Kevin Funk's avatar
Kevin Funk committed
517 518 519
    // git refuses to delete files with local modifications
    // use --force to overcome this
    *job << "git" << "rm" << "-r" << "--force";
520 521
    *job << "--" << files_;
    return job;
522 523
}

Milian Wolff's avatar
Milian Wolff committed
524
VcsJob* GitPlugin::log(const QUrl& localLocation,
525
                const KDevelop::VcsRevision& src, const KDevelop::VcsRevision& dst)
526
{
527
    DVcsJob* job = new GitJob(dotGitDirectory(localLocation), this, KDevelop::OutputJob::Silent);
528
    job->setType(VcsJob::Log);
529
    *job << "git" << "log" << "--date=raw" << "--name-status" << "-M80%" << "--follow";
530 531 532
    QString rev = revisionInterval(dst, src);
    if(!rev.isEmpty())
        *job << rev;
533
    *job << "--" << localLocation;
534
    connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitLogOutput);
535
    return job;
536 537
}

538

Milian Wolff's avatar
Milian Wolff committed
539
VcsJob* GitPlugin::log(const QUrl& localLocation, const KDevelop::VcsRevision& rev, unsigned long int limit)
540
{
541
    DVcsJob* job = new GitJob(dotGitDirectory(localLocation), this, KDevelop::OutputJob::Silent);
542
    job->setType(VcsJob::Log);
543
    *job << "git" << "log" << "--date=raw" << "--name-status" << "-M80%" << "--follow";
544 545 546
    QString revStr = toRevisionName(rev, QString());
    if(!revStr.isEmpty())
        *job << revStr;
547
    if(limit>0)
548
        *job << QStringLiteral("-%1").arg(limit);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
549

550
    *job << "--" << localLocation;
551
    connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitLogOutput);
552
    return job;
553 554
}

Milian Wolff's avatar
Milian Wolff committed
555
KDevelop::VcsJob* GitPlugin::annotate(const QUrl &localLocation, const KDevelop::VcsRevision&)
556
{
557
    DVcsJob* job = new GitJob(dotGitDirectory(localLocation), this, KDevelop::OutputJob::Silent);
558
    job->setType(VcsJob::Annotate);
559
    *job << "git" << "blame" << "--porcelain" << "-w";
560
    *job << "--" << localLocation;
561
    connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitBlameOutput);
562
    return job;
563 564
}

565 566
void GitPlugin::parseGitBlameOutput(DVcsJob *job)
{
567
    QVariantList results;
Milian Wolff's avatar
Milian Wolff committed
568
    VcsAnnotationLine* annotation = 0;
569
    QStringList lines = job->output().split('\n');
Dāvis Mosāns's avatar
Dāvis Mosāns committed
570

571 572 573 574 575 576 577 578
    bool skipNext=false;
    QMap<QString, VcsAnnotationLine> definedRevisions;
    for(QStringList::const_iterator it=lines.constBegin(), itEnd=lines.constEnd();
        it!=itEnd; ++it)
    {
        if(skipNext) {
            skipNext=false;
            results += qVariantFromValue(*annotation);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
579

580 581
            continue;
        }
Dāvis Mosāns's avatar
Dāvis Mosāns committed
582

583 584
        if(it->isEmpty())
            continue;
Dāvis Mosāns's avatar
Dāvis Mosāns committed
585

586 587
        QString name = it->left(it->indexOf(' '));
        QString value = it->right(it->size()-name.size()-1);
588

589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605
        if(name=="author")
            annotation->setAuthor(value);
        else if(name=="author-mail") {} //TODO: do smth with the e-mail?
        else if(name=="author-tz") {} //TODO: does it really matter?
        else if(name=="author-time")
            annotation->setDate(QDateTime::fromTime_t(value.toUInt()));
        else if(name=="summary")
            annotation->setCommitMessage(value);
        else if(name.startsWith("committer")) {} //We will just store the authors
        else if(name=="previous") {} //We don't need that either
        else if(name=="filename") { skipNext=true; }
        else if(name=="boundary") {
            definedRevisions.insert("boundary", VcsAnnotationLine());
        }
        else
        {
            QStringList values = value.split(' ');
Dāvis Mosāns's avatar
Dāvis Mosāns committed
606

607
            VcsRevision rev;
Milian Wolff's avatar
Milian Wolff committed
608
            rev.setRevisionValue(name.left(8), KDevelop::VcsRevision::GlobalNumber);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
609

610
            skipNext = definedRevisions.contains(name);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
611

612 613
            if(!skipNext)
                definedRevisions.insert(name, VcsAnnotationLine());
Dāvis Mosāns's avatar
Dāvis Mosāns committed
614

615
            annotation = &definedRevisions[name];
616
            annotation->setLineNumber(values[1].toInt() - 1);
617
            annotation->setRevision(rev);
618 619 620 621
        }
    }
    job->setResults(results);
}
622

623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638

DVcsJob* GitPlugin::lsFiles(const QDir &repository, const QStringList &args,
                            OutputJob::OutputJobVerbosity verbosity)
{
    DVcsJob* job = new DVcsJob(repository, this, verbosity);
    *job << "git" << "ls-files" << args;
    return job;
}

DVcsJob* GitPlugin::gitStash(const QDir& repository, const QStringList& args, OutputJob::OutputJobVerbosity verbosity)
{
    DVcsJob* job = new DVcsJob(repository, this, verbosity);
    *job << "git" << "stash" << args;
    return job;
}

Milian Wolff's avatar
Milian Wolff committed
639
VcsJob* GitPlugin::tag(const QUrl& repository, const QString& commitMessage, const VcsRevision& rev, const QString& tagName)
640 641 642 643 644 645 646 647
{
    DVcsJob* job = new DVcsJob(urlDir(repository), this);
    *job << "git" << "tag" << "-m" << commitMessage << tagName;
    if(rev.revisionValue().isValid())
        *job << rev.revisionValue().toString();
    return job;
}

Milian Wolff's avatar
Milian Wolff committed
648
VcsJob* GitPlugin::switchBranch(const QUrl &repository, const QString &branch)
649
{
650
    QDir d=urlDir(repository);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
651

652
    if(hasModifications(d) && KMessageBox::questionYesNo(0, i18n("There are pending changes, do you want to stash them first?"))==KMessageBox::Yes) {
653
        QScopedPointer<DVcsJob> stash(gitStash(d, QStringList(), KDevelop::OutputJob::Verbose));
654 655
        stash->exec();
    }
Dāvis Mosāns's avatar
Dāvis Mosāns committed
656

657
    DVcsJob* job = new DVcsJob(d, this);
658 659
    *job << "git" << "checkout" << branch;
    return job;
660 661
}

Milian Wolff's avatar
Milian Wolff committed
662
VcsJob* GitPlugin::branch(const QUrl& repository, const KDevelop::VcsRevision& rev, const QString& branchName)
663
{
664
    Q_ASSERT(!branchName.isEmpty());
Dāvis Mosāns's avatar
Dāvis Mosāns committed
665

666 667
    DVcsJob* job = new DVcsJob(urlDir(repository), this);
    *job << "git" << "branch" << "--" << branchName;
Dāvis Mosāns's avatar
Dāvis Mosāns committed
668

669 670
    if(!rev.prettyValue().isEmpty())
        *job << rev.revisionValue().toString();
671
    return job;
672 673
}

Milian Wolff's avatar
Milian Wolff committed
674
VcsJob* GitPlugin::deleteBranch(const QUrl& repository, const QString& branchName)
675
{
676 677
    DVcsJob* job = new DVcsJob(urlDir(repository), this, OutputJob::Silent);
    *job << "git" << "branch" << "-D" << branchName;
678
    connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitCurrentBranch);
679
    return job;
680 681
}

Milian Wolff's avatar
Milian Wolff committed
682
VcsJob* GitPlugin::renameBranch(const QUrl& repository, const QString& oldBranchName, const QString& newBranchName)
683
{
684 685
    DVcsJob* job = new DVcsJob(urlDir(repository), this, OutputJob::Silent);
    *job << "git" << "branch" << "-m" << newBranchName << oldBranchName;
686
    connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitCurrentBranch);
687 688 689
    return job;
}

Milian Wolff's avatar
Milian Wolff committed
690
VcsJob* GitPlugin::currentBranch(const QUrl& repository)
691
{
692
    DVcsJob* job = new DVcsJob(urlDir(repository), this, OutputJob::Silent);
693
    job->setIgnoreError(true);
694
    *job << "git" << "symbolic-ref" << "-q" << "--short" << "HEAD";
695
    connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitCurrentBranch);
696
    return job;
697 698
}

699
void GitPlugin::parseGitCurrentBranch(DVcsJob* job)
700
{
701
    QString out = job->output().trimmed();
702

703
    job->setResults(out);
704 705
}

Milian Wolff's avatar
Milian Wolff committed
706
VcsJob* GitPlugin::branches(const QUrl &repository)
707 708 709
{
    DVcsJob* job=new DVcsJob(urlDir(repository));
    *job << "git" << "branch" << "-a";
710
    connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitBranchOutput);
711 712 713 714 715 716
    return job;
}

void GitPlugin::parseGitBranchOutput(DVcsJob* job)
{
    QStringList branchListDirty = job->output().split('\n', QString::SkipEmptyParts);
717 718 719 720

    QStringList branchList;
    foreach(QString branch, branchListDirty)
    {
721 722 723 724
        // Skip pointers to another branches (one example of this is "origin/HEAD -> origin/master");
        // "git rev-list" chokes on these entries and we do not need duplicate branches altogether.
        if (branch.contains("->"))
            continue;
725 726 727 728 729

        // Skip entries such as '(no branch)'
        if (branch.contains("(no branch)"))
            continue;

730 731
        if (branch.startsWith('*'))
            branch = branch.right(branch.size()-2);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
732

733
        branchList<<branch.trimmed();
734
    }
Dāvis Mosāns's avatar
Dāvis Mosāns committed
735

736
    job->setResults(branchList);
737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761
}

/* Few words about how this hardcore works:
1. get all commits (with --paretns)
2. select master (root) branch and get all unicial commits for branches (git-rev-list br2 ^master ^br3)
3. parse allCommits. While parsing set mask (columns state for every row) for BRANCH, INITIAL, CROSS,
   MERGE and INITIAL are also set in DVCScommit::setParents (depending on parents count)
   another setType(INITIAL) is used for "bottom/root/first" commits of branches
4. find and set merges, HEADS. It's an ittaration through all commits.
    - first we check if parent is from the same branch, if no then we go through all commits searching parent's index
      and set CROSS/HCROSS for rows (in 3 rows are set EMPTY after commit with parent from another tree met)
    - then we check branchesShas[i][0] to mark heads

4 can be a seporate function. TODO: All this porn require refactoring (rewriting is better)!

It's a very dirty implementation.
FIXME:
1. HEAD which is head has extra line to connect it with further commit
2. If you menrge branch2 to master, only new commits of branch2 will be visible (it's fine, but there will be
extra merge rectangle in master. If there are no extra commits in branch2, but there are another branches, then the place for branch2 will be empty (instead of be used for branch3).
3. Commits that have additional commit-data (not only history merging, but changes to fix conflicts) are shown incorrectly
*/

QList<DVcsEvent> GitPlugin::getAllCommits(const QString &repo)
{
762
    initBranchHash(repo);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
763

764 765
    QStringList args;
    args << "--all" << "--pretty" << "--parents";
766 767 768
    QScopedPointer<DVcsJob> job(gitRevList(repo, args));
    bool ret = job->exec();
    Q_ASSERT(ret && job->status()==VcsJob::JobSucceeded && "TODO: provide a fall back in case of failing");
Milian Wolff's avatar
Milian Wolff committed
769
    Q_UNUSED(ret);
770 771 772 773 774 775 776 777 778 779
    QStringList commits = job->output().split('\n', QString::SkipEmptyParts);

    static QRegExp rx_com("commit \\w{40,40}");

    QList<DVcsEvent>commitList;
    DVcsEvent item;

    //used to keep where we have empty/cross/branch entry
    //true if it's an active branch (then cross or branch) and false if not
    QVector<bool> additionalFlags(branchesShas.count());
780
    additionalFlags.fill(false);
781 782 783 784 785 786

    //parse output
    for(int i = 0; i < commits.count(); ++i)
    {
        if (commits[i].contains(rx_com))
        {
Dāvis Mosāns's avatar
Dāvis Mosāns committed
787
            qCDebug(PLUGIN_GIT) << "commit found in " << commits[i];
Andreas Pakulat's avatar