gitplugin.cpp 51 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 25 26
 *                                                                         *
 *   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"

#include <KPluginFactory>
#include <KPluginLoader>
27 28 29 30
#include <KLocalizedString>
#include <KAboutData>
#include <QFileInfo>
#include <QDir>
31
#include <QDateTime>
32
#include <QFileSystemWatcher>
33
#include <QTimer>
34
#include <QTextCodec>
Dāvis Mosāns's avatar
Dāvis Mosāns committed
35
#include <QDebug>
36

37 38
#include <interfaces/icore.h>
#include <interfaces/iprojectcontroller.h>
39
#include <interfaces/iproject.h>
40

Milian Wolff's avatar
Milian Wolff committed
41 42
#include <util/path.h>

43 44
#include <vcs/vcsjob.h>
#include <vcs/vcsrevision.h>
45
#include <vcs/vcsevent.h>
46
#include <vcs/dvcs/dvcsjob.h>
47
#include <vcs/vcsannotation.h>
48
#include <vcs/widgets/standardvcslocationwidget.h>
49
#include "gitclonejob.h"
50 51 52 53
#include <interfaces/contextmenuextension.h>
#include <QMenu>
#include <interfaces/iruncontroller.h>
#include "stashmanagerdialog.h"
54
#include <KMessageBox>
55
#include <KTextEdit>
56
#include <KDirWatch>
57
#include <KTextEditor/Document>
58
#include <kio/copyjob.h>
59
#include "gitjob.h"
60
#include "gitmessagehighlighter.h"
61
#include "gitplugincheckinrepositoryjob.h"
Dāvis Mosāns's avatar
Dāvis Mosāns committed
62 63 64
#include "debug.h"

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

66 67
using namespace KDevelop;

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

78 79
namespace
{
Milian Wolff's avatar
Milian Wolff committed
80 81

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

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

89 90 91 92
    if (dir.isRoot()) {
        qWarning() << "couldn't find the git root for" << dirPath;
    }

93 94 95
    return dir;
}

96 97 98 99
/**
 * 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
100
static QList<QUrl> preventRecursion(const QList<QUrl>& urls)
101
{
Milian Wolff's avatar
Milian Wolff committed
102 103
    QList<QUrl> ret;
    foreach(const QUrl& url, urls) {
104 105 106 107
        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
108
                QUrl entryUrl = QUrl::fromLocalFile(d.absoluteFilePath(entry));
109 110 111 112 113 114 115 116
                ret += entryUrl;
            }
        } else
            ret += url;
    }
    return ret;
}

117 118 119 120 121 122 123 124
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:
125
                    return "";
126 127 128 129 130 131
                case VcsRevision::Working:
                    return "";
                case VcsRevision::Previous:
                    Q_ASSERT(!currentRevision.isEmpty());
                    return currentRevision + "^1";
                case VcsRevision::Start:
132 133
                    return "";
                case VcsRevision::UserSpecialType: //Not used
134 135 136 137 138 139 140 141 142 143 144 145 146 147
                    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();
}

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

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

179 180
}

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

190 191
    KDEV_USE_EXTENSION_INTERFACE( KDevelop::IBasicVersionControl )
    KDEV_USE_EXTENSION_INTERFACE( KDevelop::IDistributedVersionControl )
192
    KDEV_USE_EXTENSION_INTERFACE( KDevelop::IBranchingVersionControl )
193

194
    m_hasError = false;
195
    setObjectName("Git");
Dāvis Mosāns's avatar
Dāvis Mosāns committed
196

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

202
    m_watcher = new KDirWatch(this);
203 204
    connect(m_watcher, &KDirWatch::dirty, this, &GitPlugin::fileChanged);
    connect(m_watcher, &KDirWatch::created, this, &GitPlugin::fileChanged);
205 206 207
}

GitPlugin::~GitPlugin()
208
{}
209

210 211 212 213 214
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
215

216 217 218 219 220 221 222 223
    return false;
}

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

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

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

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

238
    QDir dir=urlDir(urls);
239
    bool hasSt = hasStashes(dir);
240
    menu->addSeparator()->setText(i18n("Git Stashes"));
241 242 243
    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);
244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261
}

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
262

263 264 265
    delete d;
}

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

273
QString GitPlugin::name() const
274
{
275 276 277
    return QLatin1String("Git");
}

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

Milian Wolff's avatar
Milian Wolff committed
283
bool GitPlugin::isValidDirectory(const QUrl & dirPath)
284
{
285
    QDir dir=dotGitDirectory(dirPath);
286

287
    return dir.cd(".git") && dir.exists("HEAD");
288 289
}

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

300 301
    QString filename = fsObject.fileName();

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

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

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

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

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

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

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

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

351
    return job;
352 353
}

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

359
    DVcsJob* job = new GitJob(dotGitDirectory(fileOrDirectory), this, KDevelop::OutputJob::Silent);
360
    job->setType(VcsJob::Diff);
361
    *job << "git" << "diff" << "--no-color" << "--no-ext-diff";
362 363 364 365 366
    if (!usePrefix()) {
        // KDE's ReviewBoard now requires p1 patchfiles, so `git diff --no-prefix` to generate p0 patches
        // has become optional.
        *job << "--no-prefix";
    }
367 368 369 370 371 372 373 374 375 376
    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
377 378 379 380 381 382 383 384

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

385
    connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitDiffOutput);
386
    return job;
387 388
}

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

394 395 396 397
    QDir repo = urlDir(repositoryRoot(localLocations.first()));
    QString modified;
    for (const auto& file: localLocations) {
        if (hasModifications(repo, file)) {
Milian Wolff's avatar
Milian Wolff committed
398
            modified.append(file.toDisplayString(QUrl::PreferLocalFile) + "<br/>");
399 400 401 402 403 404 405 406 407 408
        }
    }
    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);
        }
    }

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

414
    return job;
415 416 417
}


418 419 420
//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
421
                             const QList<QUrl>& localLocations,
422 423
                             KDevelop::IBasicVersionControl::RecursionMode recursion)
{
424
    if (localLocations.empty() || message.isEmpty())
425
        return errorsFound(i18n("No files or message specified"));
426

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

433
    *job << "git" << "commit" << "-m" << message;
434
    *job << "--" << files;
435
    return job;
436
}
437

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

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

446 447
        otherFiles += v;
    }
Dāvis Mosāns's avatar
Dāvis Mosāns committed
448

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

455 456 457 458
    if(!toadd.isEmpty()) {
        VcsJob* job = add(toadd);
        job->exec();
    }
459 460
}

461 462
bool isEmptyDirStructure(const QDir &dir)
{
463
    foreach (const QFileInfo &i, dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot)) {
464 465 466 467 468 469 470 471 472
        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
473
VcsJob* GitPlugin::remove(const QList<QUrl>& files)
474
{
475 476
    if (files.isEmpty())
        return errorsFound(i18n("No files to remove"));
477 478 479
    QDir dotGitDir = dotGitDirectory(files.front());


Milian Wolff's avatar
Milian Wolff committed
480
    QList<QUrl> files_(files);
481

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

        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
490
            QList<QUrl> otherFiles;
491
            foreach(const QString &f, otherStr) {
Milian Wolff's avatar
Milian Wolff committed
492
                otherFiles << QUrl::fromLocalFile(dotGitDir.path()+'/'+f);
493 494 495 496 497
            }
            if (fileInfo.isFile()) {
                //if it's an unversioned file we are done, don't use git rm on it
                i.remove();
            }
498 499 500

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

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

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

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

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

541

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

553
    *job << "--" << localLocation;
554
    connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitLogOutput);
555
    return job;
556 557
}

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

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

574 575 576 577 578 579 580 581
    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
582

583 584
            continue;
        }
Dāvis Mosāns's avatar
Dāvis Mosāns committed
585

586 587
        if(it->isEmpty())
            continue;
Dāvis Mosāns's avatar
Dāvis Mosāns committed
588

589 590
        QString name = it->left(it->indexOf(' '));
        QString value = it->right(it->size()-name.size()-1);
591

592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608
        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
609

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

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

615 616
            if(!skipNext)
                definedRevisions.insert(name, VcsAnnotationLine());
Dāvis Mosāns's avatar
Dāvis Mosāns committed
617

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

626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641

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
642
VcsJob* GitPlugin::tag(const QUrl& repository, const QString& commitMessage, const VcsRevision& rev, const QString& tagName)
643 644 645 646 647 648 649 650
{
    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
651
VcsJob* GitPlugin::switchBranch(const QUrl &repository, const QString &branch)
652
{
653
    QDir d=urlDir(repository);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
654

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

660
    DVcsJob* job = new DVcsJob(d, this);
661 662
    *job << "git" << "checkout" << branch;
    return job;
663 664
}

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

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

672 673
    if(!rev.prettyValue().isEmpty())
        *job << rev.revisionValue().toString();
674
    return job;
675 676
}

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

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

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

702
void GitPlugin::parseGitCurrentBranch(DVcsJob* job)
703
{
704
    QString out = job->output().trimmed();
705

706
    job->setResults(out);
707 708
}

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

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

    QStringList branchList;
    foreach(QString branch, branchListDirty)
    {
724 725 726 727
        // 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;
728 729 730 731 732

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

733 734
        if (branch.startsWith('*'))
            branch = branch.right(branch.size()-2);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
735

736
        branchList<<branch.trimmed();
737
    }
Dāvis Mosāns's avatar
Dāvis Mosāns committed
738

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

/* 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)
{
765
    initBranchHash(repo);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
766

767 768
    QStringList args;
    args << "--all" << "--pretty" << "--parents";
769 770 771
    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
772
    Q_UNUSED(ret);
773 774 775 776 777 778 779 780 781 782
    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());
783
    additionalFlags.fill(false);
784 785 786 787 788 789

    //parse output
    for(int i = 0; i < commits.count(); ++i)
    {
        if (commits[i].contains(rx_com))
        {