gitplugin.cpp 50.8 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 <KIO/CopyJob>
50
#include <KIO/NetAccess>
51
#include "gitclonejob.h"
52 53 54 55
#include <interfaces/contextmenuextension.h>
#include <QMenu>
#include <interfaces/iruncontroller.h>
#include "stashmanagerdialog.h"
56
#include <KMessageBox>
57
#include <KTextEdit>
58
#include <KDirWatch>
59
#include <KTextEditor/Document>
60
#include "gitjob.h"
61
#include "gitmessagehighlighter.h"
62
#include "gitplugincheckinrepositoryjob.h"
Dāvis Mosāns's avatar
Dāvis Mosāns committed
63 64 65
#include "debug.h"

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

67 68
using namespace KDevelop;

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

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

QDir dotGitDirectory(const QUrl& dirPath)
83
{
84
    const QFileInfo finfo(dirPath.toLocalFile());
Milian Wolff's avatar
Milian Wolff committed
85
    QDir dir = finfo.absoluteDir();
Dāvis Mosāns's avatar
Dāvis Mosāns committed
86

87
    static const QString gitDir(".git");
88
    while (!dir.exists(gitDir) && dir.cdUp()) {} // cdUp, until there is a sub-directory called .git
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 & )
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
179
    : DistributedVersionControlPlugin(parent, "kdevgit"), m_oldVersion(false)
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 196 197
    DVcsJob* versionJob = new DVcsJob(QDir::tempPath(), this, KDevelop::OutputJob::Silent);
    *versionJob << "git" << "--version";
    connect(versionJob, SIGNAL(readyForParsing(KDevelop::DVcsJob*)), SLOT(parseGitVersionOutput(KDevelop::DVcsJob*)));
    ICore::self()->runController()->registerJob(versionJob);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
198

199 200 201
    m_watcher = new KDirWatch(this);
    connect(m_watcher, SIGNAL(dirty(QString)), SLOT(fileChanged(QString)));
    connect(m_watcher, SIGNAL(created(QString)), SLOT(fileChanged(QString)));
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 340 341 342
    if(m_oldVersion) {
        *job << "git" << "ls-files" << "-t" << "-m" << "-c" << "-o" << "-d" << "-k" << "--directory";
        connect(job, SIGNAL(readyForParsing(KDevelop::DVcsJob*)), SLOT(parseGitStatusOutput_old(KDevelop::DVcsJob*)));
    } else {
        *job << "git" << "status" << "--porcelain";
343
        job->setIgnoreError(true);
344 345 346
        connect(job, SIGNAL(readyForParsing(KDevelop::DVcsJob*)), SLOT(parseGitStatusOutput(KDevelop::DVcsJob*)));
    }
    *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 364 365 366 367 368
    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
369 370 371 372 373 374 375 376

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

377
    connect(job, SIGNAL(readyForParsing(KDevelop::DVcsJob*)), SLOT(parseGitDiffOutput(KDevelop::DVcsJob*)));
378
    return job;
379 380
}

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

386 387 388 389
    QDir repo = urlDir(repositoryRoot(localLocations.first()));
    QString modified;
    for (const auto& file: localLocations) {
        if (hasModifications(repo, file)) {
Milian Wolff's avatar
Milian Wolff committed
390
            modified.append(file.toDisplayString(QUrl::PreferLocalFile) + "<br/>");
391 392 393 394 395 396 397 398 399 400
        }
    }
    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);
        }
    }

401
    DVcsJob* job = new GitJob(dotGitDirectory(localLocations.front()), this);
402
    job->setType(VcsJob::Revert);
403 404
    *job << "git" << "checkout" << "--";
    *job << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations));
Dāvis Mosāns's avatar
Dāvis Mosāns committed
405

406
    return job;
407 408 409
}


410 411 412
//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
413
                             const QList<QUrl>& localLocations,
414 415
                             KDevelop::IBasicVersionControl::RecursionMode recursion)
{
416
    if (localLocations.empty() || message.isEmpty())
417
        return errorsFound(i18n("No files or message specified"));
418

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

425
    *job << "git" << "commit" << "-m" << message;
426
    *job << "--" << files;
427
    return job;
428
}
429

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

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

438 439
        otherFiles += v;
    }
Dāvis Mosāns's avatar
Dāvis Mosāns committed
440

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

447 448 449 450
    if(!toadd.isEmpty()) {
        VcsJob* job = add(toadd);
        job->exec();
    }
451 452
}

453 454
bool isEmptyDirStructure(const QDir &dir)
{
455
    foreach (const QFileInfo &i, dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot)) {
456 457 458 459 460 461 462 463 464
        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
465
VcsJob* GitPlugin::remove(const QList<QUrl>& files)
466
{
467 468
    if (files.isEmpty())
        return errorsFound(i18n("No files to remove"));
469 470 471
    QDir dotGitDir = dotGitDirectory(files.front());


Milian Wolff's avatar
Milian Wolff committed
472
    QList<QUrl> files_(files);
473

Milian Wolff's avatar
Milian Wolff committed
474
    QMutableListIterator<QUrl> i(files_);
475
    while (i.hasNext()) {
Milian Wolff's avatar
Milian Wolff committed
476
        QUrl file = i.next();
477
        QFileInfo fileInfo(file.toLocalFile());
478 479 480 481

        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
482
            QList<QUrl> otherFiles;
483
            foreach(const QString &f, otherStr) {
Milian Wolff's avatar
Milian Wolff committed
484
                otherFiles << QUrl::fromLocalFile(dotGitDir.path()+'/'+f);
485 486 487 488 489 490
            }
            if (fileInfo.isFile()) {
                //if it's an unversioned file we are done, don't use git rm on it
                i.remove();
            }
            KIO::NetAccess::synchronousRun(KIO::trash(otherFiles), 0);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
491
            qCDebug(PLUGIN_GIT) << "other files" << otherFiles;
492 493
        }

494
        if (fileInfo.isDir()) {
495 496
            if (isEmptyDirStructure(QDir(file.toLocalFile()))) {
                //remove empty folders, git doesn't do that
497
                KIO::NetAccess::synchronousRun(KIO::trash(file), 0);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
498
                qCDebug(PLUGIN_GIT) << "empty folder, removing" << file;
499
                //we already deleted it, don't use git rm on it
500 501 502 503 504 505
                i.remove();
            }
        }
    }

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

507 508
    DVcsJob* job = new GitJob(dotGitDir, this);
    job->setType(VcsJob::Remove);
Kevin Funk's avatar
Kevin Funk committed
509 510 511
    // git refuses to delete files with local modifications
    // use --force to overcome this
    *job << "git" << "rm" << "-r" << "--force";
512 513
    *job << "--" << files_;
    return job;
514 515
}

Milian Wolff's avatar
Milian Wolff committed
516
VcsJob* GitPlugin::log(const QUrl& localLocation,
517
                const KDevelop::VcsRevision& src, const KDevelop::VcsRevision& dst)
518
{
519
    DVcsJob* job = new GitJob(dotGitDirectory(localLocation), this, KDevelop::OutputJob::Silent);
520
    job->setType(VcsJob::Log);
521
    *job << "git" << "log" << "--date=raw" << "--name-status" << "-M80%" << "--follow";
522 523 524
    QString rev = revisionInterval(dst, src);
    if(!rev.isEmpty())
        *job << rev;
525
    *job << "--" << localLocation;
526
    connect(job, SIGNAL(readyForParsing(KDevelop::DVcsJob*)), this, SLOT(parseGitLogOutput(KDevelop::DVcsJob*)));
527
    return job;
528 529
}

530

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

542 543
    *job << "--" << localLocation;
    connect(job, SIGNAL(readyForParsing(KDevelop::DVcsJob*)), this, SLOT(parseGitLogOutput(KDevelop::DVcsJob*)));
544
    return job;
545 546
}

Milian Wolff's avatar
Milian Wolff committed
547
KDevelop::VcsJob* GitPlugin::annotate(const QUrl &localLocation, const KDevelop::VcsRevision&)
548
{
549
    DVcsJob* job = new GitJob(dotGitDirectory(localLocation), this, KDevelop::OutputJob::Silent);
550
    job->setType(VcsJob::Annotate);
551
    *job << "git" << "blame" << "--porcelain" << "-w";
552
    *job << "--" << localLocation;
553
    connect(job, SIGNAL(readyForParsing(KDevelop::DVcsJob*)), this, SLOT(parseGitBlameOutput(KDevelop::DVcsJob*)));
554
    return job;
555 556
}

557 558
void GitPlugin::parseGitBlameOutput(DVcsJob *job)
{
559
    QVariantList results;
Milian Wolff's avatar
Milian Wolff committed
560
    VcsAnnotationLine* annotation = 0;
561
    QStringList lines = job->output().split('\n');
Dāvis Mosāns's avatar
Dāvis Mosāns committed
562

563 564 565 566 567 568 569 570
    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
571

572 573
            continue;
        }
Dāvis Mosāns's avatar
Dāvis Mosāns committed
574

575 576
        if(it->isEmpty())
            continue;
Dāvis Mosāns's avatar
Dāvis Mosāns committed
577

578 579
        QString name = it->left(it->indexOf(' '));
        QString value = it->right(it->size()-name.size()-1);
580

581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597
        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
598

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

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

604 605
            if(!skipNext)
                definedRevisions.insert(name, VcsAnnotationLine());
Dāvis Mosāns's avatar
Dāvis Mosāns committed
606

607
            annotation = &definedRevisions[name];
608
            annotation->setLineNumber(values[1].toInt() - 1);
609
            annotation->setRevision(rev);
610 611 612 613
        }
    }
    job->setResults(results);
}
614

615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630

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
631
VcsJob* GitPlugin::tag(const QUrl& repository, const QString& commitMessage, const VcsRevision& rev, const QString& tagName)
632 633 634 635 636 637 638 639
{
    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
640
VcsJob* GitPlugin::switchBranch(const QUrl &repository, const QString &branch)
641
{
642
    QDir d=urlDir(repository);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
643

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

649
    DVcsJob* job = new DVcsJob(d, this);
650 651
    *job << "git" << "checkout" << branch;
    return job;
652 653
}

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

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

661 662
    if(!rev.prettyValue().isEmpty())
        *job << rev.revisionValue().toString();
663
    return job;
664 665
}

Milian Wolff's avatar
Milian Wolff committed
666
VcsJob* GitPlugin::deleteBranch(const QUrl& repository, const QString& branchName)
667
{
668 669 670
    DVcsJob* job = new DVcsJob(urlDir(repository), this, OutputJob::Silent);
    *job << "git" << "branch" << "-D" << branchName;
    connect(job, SIGNAL(readyForParsing(KDevelop::DVcsJob*)), SLOT(parseGitCurrentBranch(KDevelop::DVcsJob*)));
671
    return job;
672 673
}

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

Milian Wolff's avatar
Milian Wolff committed
682
VcsJob* GitPlugin::currentBranch(const QUrl& repository)
683
{
684
    DVcsJob* job = new DVcsJob(urlDir(repository), this, OutputJob::Silent);
685
    job->setIgnoreError(true);
686
    *job << "git" << "symbolic-ref" << "-q" << "--short" << "HEAD";
687 688
    connect(job, SIGNAL(readyForParsing(KDevelop::DVcsJob*)), SLOT(parseGitCurrentBranch(KDevelop::DVcsJob*)));
    return job;
689 690
}

691
void GitPlugin::parseGitCurrentBranch(DVcsJob* job)
692
{
693
    QString out = job->output().trimmed();
694

695
    job->setResults(out);
696 697
}

Milian Wolff's avatar
Milian Wolff committed
698
VcsJob* GitPlugin::branches(const QUrl &repository)
699 700 701 702 703 704 705 706 707 708
{
    DVcsJob* job=new DVcsJob(urlDir(repository));
    *job << "git" << "branch" << "-a";
    connect(job, SIGNAL(readyForParsing(KDevelop::DVcsJob*)), SLOT(parseGitBranchOutput(KDevelop::DVcsJob*)));
    return job;
}

void GitPlugin::parseGitBranchOutput(DVcsJob* job)
{
    QStringList branchListDirty = job->output().split('\n', QString::SkipEmptyParts);
709 710 711 712

    QStringList branchList;
    foreach(QString branch, branchListDirty)
    {
713 714 715 716
        // 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;
717 718 719 720 721

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

722 723
        if (branch.startsWith('*'))
            branch = branch.right(branch.size()-2);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
724

725
        branchList<<branch.trimmed();
726
    }
Dāvis Mosāns's avatar
Dāvis Mosāns committed
727

728
    job->setResults(branchList);
729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753
}

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

756 757
    QStringList args;
    args << "--all" << "--pretty" << "--parents";
758 759 760
    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
761
    Q_UNUSED(ret);
762 763 764 765 766 767 768 769 770 771
    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());
772
    additionalFlags.fill(false);
773 774 775 776 777 778

    //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
779
            qCDebug(PLUGIN_GIT) << "commit found in " << commits[i];
780
            item.setCommit(commits[i].section(' ', 1, 1).trimmed());
Dāvis Mosāns's avatar
Dāvis Mosāns committed
781
//             qCDebug(PLUGIN_GIT) << "commit is: " << commits[i].section(' ', 1);
782 783 784 785 786 787

            QStringList parents;
            QString parent = commits[i].section(' ', 2);
            int section = 2;
            while (!parent.isEmpty())
            {
Dāvis Mosāns's avatar
Dāvis Mosāns committed
788
                /*                qCDebug(PLUGIN_GIT) << "Parent is: " << parent;*/
789 790 791 792 793 794 795 796 797 798 799
                parents.append(parent.trimmed());
                section++;
                parent = commits[i].section(' ', section);
            }
            item.setParents(parents);

            //Avoid Merge string
            while (!commits[i].contains("Author: "))
                    ++i;

            item.setAuthor(commits[i].section("Author: ", 1).trimmed());
Dāvis Mosāns's avatar
Dāvis Mosāns committed
800
//             qCDebug(PLUGIN_GIT) << "author is: " << commits[i].section("Author: ", 1);
801 802

            item.setDate(commits[++i].section("Date:   ", 1).trimmed());
Dāvis Mosāns's avatar
Dāvis Mosāns committed
803