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 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());
85
    QDir dir = finfo.isDir() ? QDir(finfo.filePath()): finfo.absoluteDir();
Dāvis Mosāns's avatar
Dāvis Mosāns committed
86

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

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

94 95 96
    return dir;
}

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

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

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

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

180 181
}

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

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

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

198 199 200 201
    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
202

203 204 205
    m_watcher = new KDirWatch(this);
    connect(m_watcher, SIGNAL(dirty(QString)), SLOT(fileChanged(QString)));
    connect(m_watcher, SIGNAL(created(QString)), SLOT(fileChanged(QString)));
206 207 208
}

GitPlugin::~GitPlugin()
209
{}
210

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

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

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

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

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

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

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

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
263

264 265 266
    delete d;
}

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

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

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

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

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

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

301 302
    QString filename = fsObject.fileName();

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

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

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

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

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

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

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

342 343 344 345 346
    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";
347
        job->setIgnoreError(true);
348 349 350
        connect(job, SIGNAL(readyForParsing(KDevelop::DVcsJob*)), SLOT(parseGitStatusOutput(KDevelop::DVcsJob*)));
    }
    *job << "--" << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations));
351

352
    return job;
353 354
}

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

360
    DVcsJob* job = new GitJob(dotGitDirectory(fileOrDirectory), this, KDevelop::OutputJob::Silent);
361
    job->setType(VcsJob::Diff);
362
    *job << "git" << "diff" << "--no-color" << "--no-ext-diff";
363 364 365 366 367 368 369 370 371 372
    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
373 374 375 376 377 378 379 380

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

381
    connect(job, SIGNAL(readyForParsing(KDevelop::DVcsJob*)), SLOT(parseGitDiffOutput(KDevelop::DVcsJob*)));
382
    return job;
383 384
}

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

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

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

410
    return job;
411 412 413
}


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

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

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

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

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

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

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

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

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


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

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

        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
486
            QList<QUrl> otherFiles;
487
            foreach(const QString &f, otherStr) {
Milian Wolff's avatar
Milian Wolff committed
488
                otherFiles << QUrl::fromLocalFile(dotGitDir.path()+'/'+f);
489 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();
            }
            KIO::NetAccess::synchronousRun(KIO::trash(otherFiles), 0);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
495
            qCDebug(PLUGIN_GIT) << "other files" << otherFiles;
496 497
        }

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

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

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

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

534

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

546 547
    *job << "--" << localLocation;
    connect(job, SIGNAL(readyForParsing(KDevelop::DVcsJob*)), this, SLOT(parseGitLogOutput(KDevelop::DVcsJob*)));
548
    return job;
549 550
}

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

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

567 568 569 570 571 572 573 574
    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
575

576 577
            continue;
        }
Dāvis Mosāns's avatar
Dāvis Mosāns committed
578

579 580
        if(it->isEmpty())
            continue;
Dāvis Mosāns's avatar
Dāvis Mosāns committed
581

582 583
        QString name = it->left(it->indexOf(' '));
        QString value = it->right(it->size()-name.size()-1);
584

585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601
        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
602

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

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

608 609
            if(!skipNext)
                definedRevisions.insert(name, VcsAnnotationLine());
Dāvis Mosāns's avatar
Dāvis Mosāns committed
610

611
            annotation = &definedRevisions[name];
612
            annotation->setLineNumber(values[1].toInt() - 1);
613
            annotation->setRevision(rev);
614 615 616 617
        }
    }
    job->setResults(results);
}
618

619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634

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

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

653
    DVcsJob* job = new DVcsJob(d, this);
654 655
    *job << "git" << "checkout" << branch;
    return job;
656 657
}

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

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

665 666
    if(!rev.prettyValue().isEmpty())
        *job << rev.revisionValue().toString();
667
    return job;
668 669
}

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

Milian Wolff's avatar
Milian Wolff committed
678
VcsJob* GitPlugin::renameBranch(const QUrl& repository, const QString& oldBranchName, const QString& newBranchName)
679
{
680 681 682
    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*)));
683 684 685
    return job;
}

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

695
void GitPlugin::parseGitCurrentBranch(DVcsJob* job)
696
{
697
    QString out = job->output().trimmed();
698

699
    job->setResults(out);
700 701
}

Milian Wolff's avatar
Milian Wolff committed
702
VcsJob* GitPlugin::branches(const QUrl &repository)
703 704 705 706 707 708 709 710 711 712
{
    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);
713 714 715 716

    QStringList branchList;
    foreach(QString branch, branchListDirty)
    {
717 718 719 720
        // 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;
721 722 723 724 725

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

726 727
        if (branch.startsWith('*'))
            branch = branch.right(branch.size()-2);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
728

729
        branchList<<branch.trimmed();
730
    }
Dāvis Mosāns's avatar
Dāvis Mosāns committed
731

732
    job->setResults(branchList);
733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757
}

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

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

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

            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
792
                /*                qCDebug(PLUGIN_GIT) << "Parent is: " << parent;*/
793 794 795 796 797 798 799 800 801 802 803
                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
804
//             qCDebug(PLUGIN_GIT) << "author is: " << commits[i].section("Author: ", 1);