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

#include "gitplugin.h"

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

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

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

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

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

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

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

65 66
using namespace KDevelop;

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

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

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

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

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

92 93 94
    return dir;
}

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

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

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

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

178 179
}

180
GitPlugin::GitPlugin( QObject *parent, const QVariantList & )
181
    : DistributedVersionControlPlugin(parent, QStringLiteral("kdevgit")), m_oldVersion(false), m_usePrefix(true)
182
{
183
    if (QStandardPaths::findExecutable(QStringLiteral("git")).isEmpty()) {
184
        setErrorDescription(i18n("Unable to find git executable. Is it installed on the system?"));
185 186 187
        return;
    }

188
    setObjectName(QStringLiteral("Git"));
Dāvis Mosāns's avatar
Dāvis Mosāns committed
189

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

195
    m_watcher = new KDirWatch(this);
196 197
    connect(m_watcher, &KDirWatch::dirty, this, &GitPlugin::fileChanged);
    connect(m_watcher, &KDirWatch::created, this, &GitPlugin::fileChanged);
198 199 200
}

GitPlugin::~GitPlugin()
201
{}
202

203 204 205 206 207
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
208

209 210 211 212 213
    return false;
}

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

217
bool GitPlugin::hasModifications(const QDir& d)
218
{
219
    return !emptyOutput(lsFiles(d, QStringList(QStringLiteral("-m")), OutputJob::Silent));
220 221
}

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

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

231
    QDir dir=urlDir(urls);
232
    bool hasSt = hasStashes(dir);
233
    menu->addSeparator()->setText(i18n("Git Stashes"));
234 235 236
    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);
237 238 239 240 241 242 243 244 245 246
}

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

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

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

256 257 258
    delete d;
}

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

266
QString GitPlugin::name() const
267
{
268
    return QStringLiteral("Git");
269 270
}

Milian Wolff's avatar
Milian Wolff committed
271
QUrl GitPlugin::repositoryRoot(const QUrl& path)
272
{
Milian Wolff's avatar
Milian Wolff committed
273
    return QUrl::fromLocalFile(dotGitDirectory(path).absolutePath());
274 275
}

Milian Wolff's avatar
Milian Wolff committed
276
bool GitPlugin::isValidDirectory(const QUrl & dirPath)
277
{
278
    QDir dir=dotGitDirectory(dirPath);
Thibault North's avatar
Thibault North committed
279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296
    QFile dotGitPotentialFile(dir.filePath(".git"));
    // if .git is a file, we may be in a git worktree
    QFileInfo dotGitPotentialFileInfo(dotGitPotentialFile);
    if (!dotGitPotentialFileInfo.isDir() && dotGitPotentialFile.exists()) {
        QString gitWorktreeFileContent;
        if (dotGitPotentialFile.open(QFile::ReadOnly)) {
            // the content should be gitdir: /path/to/the/.git/worktree
            gitWorktreeFileContent = QString::fromUtf8(dotGitPotentialFile.readAll());
            dotGitPotentialFile.close();
        } else {
            return false;
        }
        const auto items = gitWorktreeFileContent.split(' ');
        if (items.size() == 2 && items.at(0) == "gitdir:") {
            qCDebug(PLUGIN_GIT) << "we are in a git worktree" << items.at(1);
            return true;
        }
    }
297
    return dir.exists(QStringLiteral(".git/HEAD"));
298 299
}

300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
bool GitPlugin::isValidRemoteRepositoryUrl(const QUrl& remoteLocation)
{
    if (remoteLocation.isLocalFile()) {
        QFileInfo fileInfo(remoteLocation.toLocalFile());
        if (fileInfo.isDir()) {
            QDir dir(fileInfo.filePath());
            if (dir.exists(QStringLiteral(".git/HEAD"))) {
                return true;
            }
            // TODO: check also for bare repo
        }
    } else {
        const QString scheme = remoteLocation.scheme();
        if (scheme == QLatin1String("git")) {
            return true;
        }
        // heuristic check, anything better we can do here without talking to server?
        if ((scheme == QLatin1String("http") ||
             scheme == QLatin1String("https")) &&
            remoteLocation.path().endsWith(QLatin1String(".git"))) {
            return true;
        }
    }
    return false;
}

Milian Wolff's avatar
Milian Wolff committed
326
bool GitPlugin::isVersionControlled(const QUrl &path)
327
{
328
    QFileInfo fsObject(path.toLocalFile());
329 330 331
    if (!fsObject.exists()) {
        return false;
    }
332
    if (fsObject.isDir()) {
333
        return isValidDirectory(path);
334
    }
335

336 337
    QString filename = fsObject.fileName();

338
    QStringList otherFiles = getLsFiles(fsObject.dir(), QStringList(QStringLiteral("--")) << filename, KDevelop::OutputJob::Silent);
339
    return !otherFiles.empty();
340 341
}

Milian Wolff's avatar
Milian Wolff committed
342
VcsJob* GitPlugin::init(const QUrl &directory)
343
{
344
    DVcsJob* job = new DVcsJob(urlDir(directory), this);
345
    job->setType(VcsJob::Import);
346 347
    *job << "git" << "init";
    return job;
348 349
}

Milian Wolff's avatar
Milian Wolff committed
350
VcsJob* GitPlugin::createWorkingCopy(const KDevelop::VcsLocation & source, const QUrl& dest, KDevelop::IBasicVersionControl::RecursionMode)
351
{
352
    DVcsJob* job = new GitCloneJob(urlDir(dest), this);
353
    job->setType(VcsJob::Import);
354 355
    *job << "git" << "clone" << "--progress" << "--" << source.localUrl().url() << dest;
    return job;
356 357
}

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

363
    DVcsJob* job = new GitJob(dotGitDirectory(localLocations.front()), this);
364
    job->setType(VcsJob::Add);
365
    *job << "git" << "add" << "--" << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations));
366
    return job;
367 368
}

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

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

377 378
    if(m_oldVersion) {
        *job << "git" << "ls-files" << "-t" << "-m" << "-c" << "-o" << "-d" << "-k" << "--directory";
379
        connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitStatusOutput_old);
380 381
    } else {
        *job << "git" << "status" << "--porcelain";
382
        job->setIgnoreError(true);
383
        connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitStatusOutput);
384 385
    }
    *job << "--" << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations));
386

387
    return job;
388 389
}

Milian Wolff's avatar
Milian Wolff committed
390
VcsJob* GitPlugin::diff(const QUrl& fileOrDirectory, const KDevelop::VcsRevision& srcRevision, const KDevelop::VcsRevision& dstRevision,
391
                        VcsDiff::Type /*type*/, IBasicVersionControl::RecursionMode recursion)
392
{
393
    //TODO: control different types
Milian Wolff's avatar
Milian Wolff committed
394

395
    DVcsJob* job = new GitJob(dotGitDirectory(fileOrDirectory), this, KDevelop::OutputJob::Silent);
396
    job->setType(VcsJob::Diff);
397
    *job << "git" << "diff" << "--no-color" << "--no-ext-diff";
398 399 400 401 402
    if (!usePrefix()) {
        // KDE's ReviewBoard now requires p1 patchfiles, so `git diff --no-prefix` to generate p0 patches
        // has become optional.
        *job << "--no-prefix";
    }
403 404 405 406 407 408 409 410
    if (dstRevision.revisionType() == VcsRevision::Special &&
         dstRevision.specialType() == VcsRevision::Working) {
        if (srcRevision.revisionType() == VcsRevision::Special &&
             srcRevision.specialType() == VcsRevision::Base) {
            *job << "HEAD";
        } else {
            *job << "--cached" << srcRevision.revisionValue().toString();
        }
411
    } else {
412 413 414 415
        QString revstr = revisionInterval(srcRevision, dstRevision);
        if(!revstr.isEmpty())
            *job << revstr;
    }
Milian Wolff's avatar
Milian Wolff committed
416 417 418 419 420 421 422 423

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

424
    connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitDiffOutput);
425
    return job;
426 427
}

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

433 434 435 436
    QDir repo = urlDir(repositoryRoot(localLocations.first()));
    QString modified;
    for (const auto& file: localLocations) {
        if (hasModifications(repo, file)) {
Milian Wolff's avatar
Milian Wolff committed
437
            modified.append(file.toDisplayString(QUrl::PreferLocalFile) + "<br/>");
438 439 440 441 442 443 444 445 446 447
        }
    }
    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);
        }
    }

448
    DVcsJob* job = new GitJob(dotGitDirectory(localLocations.front()), this);
449
    job->setType(VcsJob::Revert);
450 451
    *job << "git" << "checkout" << "--";
    *job << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations));
Dāvis Mosāns's avatar
Dāvis Mosāns committed
452

453
    return job;
454 455 456
}


457 458 459
//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
460
                             const QList<QUrl>& localLocations,
461 462
                             KDevelop::IBasicVersionControl::RecursionMode recursion)
{
463
    if (localLocations.empty() || message.isEmpty())
464
        return errorsFound(i18n("No files or message specified"));
465 466 467 468

    const QDir dir = dotGitDirectory(localLocations.front());
    if (!ensureValidGitIdentity(dir)) {
        return errorsFound(i18n("Email or name for Git not specified"));
469
    }
470

471
    DVcsJob* job = new DVcsJob(dir, this);
472
    job->setType(VcsJob::Commit);
Milian Wolff's avatar
Milian Wolff committed
473
    QList<QUrl> files = (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations));
474
    addNotVersionedFiles(dir, files);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
475

476
    *job << "git" << "commit" << "-m" << message;
477
    *job << "--" << files;
478
    return job;
479
}
480

481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502
bool GitPlugin::ensureValidGitIdentity(const QDir& dir)
{
    const QUrl url = QUrl::fromLocalFile(dir.absolutePath());

    const QString name = readConfigOption(url, QStringLiteral("user.name"));
    const QString email = readConfigOption(url, QStringLiteral("user.email"));
    if (!email.isEmpty() && !name.isEmpty()) {
        return true; // already okay
    }

    GitNameEmailDialog dialog;
    dialog.setName(name);
    dialog.setEmail(email);
    if (!dialog.exec()) {
        return false;
    }

    runSynchronously(setConfigOption(url, QStringLiteral("user.name"), dialog.name(), dialog.isGlobal()));
    runSynchronously(setConfigOption(url, QStringLiteral("user.email"), dialog.email(), dialog.isGlobal()));
    return true;
}

Milian Wolff's avatar
Milian Wolff committed
503
void GitPlugin::addNotVersionedFiles(const QDir& dir, const QList<QUrl>& files)
504
{
505
    QStringList otherStr = getLsFiles(dir, QStringList() << QStringLiteral("--others"), KDevelop::OutputJob::Silent);
Milian Wolff's avatar
Milian Wolff committed
506
    QList<QUrl> toadd, otherFiles;
Dāvis Mosāns's avatar
Dāvis Mosāns committed
507

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

511 512
        otherFiles += v;
    }
Dāvis Mosāns's avatar
Dāvis Mosāns committed
513

514
    //We add the files that are not versioned
Milian Wolff's avatar
Milian Wolff committed
515
    foreach(const QUrl& file, files) {
516 517 518
        if(otherFiles.contains(file) && QFileInfo(file.toLocalFile()).isFile())
            toadd += file;
    }
Dāvis Mosāns's avatar
Dāvis Mosāns committed
519

520 521 522 523
    if(!toadd.isEmpty()) {
        VcsJob* job = add(toadd);
        job->exec();
    }
524 525
}

526 527
bool isEmptyDirStructure(const QDir &dir)
{
528
    foreach (const QFileInfo &i, dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot)) {
529 530 531 532 533 534 535 536 537
        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
538
VcsJob* GitPlugin::remove(const QList<QUrl>& files)
539
{
540 541
    if (files.isEmpty())
        return errorsFound(i18n("No files to remove"));
542 543 544
    QDir dotGitDir = dotGitDirectory(files.front());


Milian Wolff's avatar
Milian Wolff committed
545
    QList<QUrl> files_(files);
546

Milian Wolff's avatar
Milian Wolff committed
547
    QMutableListIterator<QUrl> i(files_);
548
    while (i.hasNext()) {
Milian Wolff's avatar
Milian Wolff committed
549
        QUrl file = i.next();
550
        QFileInfo fileInfo(file.toLocalFile());
551

552
        QStringList otherStr = getLsFiles(dotGitDir, QStringList() << QStringLiteral("--others") << QStringLiteral("--") << file.toLocalFile(), KDevelop::OutputJob::Silent);
553 554
        if(!otherStr.isEmpty()) {
            //remove files not under version control
Milian Wolff's avatar
Milian Wolff committed
555
            QList<QUrl> otherFiles;
556
            foreach(const QString &f, otherStr) {
Milian Wolff's avatar
Milian Wolff committed
557
                otherFiles << QUrl::fromLocalFile(dotGitDir.path()+'/'+f);
558 559 560 561 562
            }
            if (fileInfo.isFile()) {
                //if it's an unversioned file we are done, don't use git rm on it
                i.remove();
            }
563 564 565

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

569
        if (fileInfo.isDir()) {
570 571
            if (isEmptyDirStructure(QDir(file.toLocalFile()))) {
                //remove empty folders, git doesn't do that
572 573
                auto trashJob = KIO::trash(file);
                trashJob->exec();
Dāvis Mosāns's avatar
Dāvis Mosāns committed
574
                qCDebug(PLUGIN_GIT) << "empty folder, removing" << file;
575
                //we already deleted it, don't use git rm on it
576 577 578 579 580
                i.remove();
            }
        }
    }

581
    if (files_.isEmpty()) return nullptr;
582

583 584
    DVcsJob* job = new GitJob(dotGitDir, this);
    job->setType(VcsJob::Remove);
Kevin Funk's avatar
Kevin Funk committed
585 586 587
    // git refuses to delete files with local modifications
    // use --force to overcome this
    *job << "git" << "rm" << "-r" << "--force";
588 589
    *job << "--" << files_;
    return job;
590 591
}

Milian Wolff's avatar
Milian Wolff committed
592
VcsJob* GitPlugin::log(const QUrl& localLocation,
593
                const KDevelop::VcsRevision& src, const KDevelop::VcsRevision& dst)
594
{
595
    DVcsJob* job = new GitJob(dotGitDirectory(localLocation), this, KDevelop::OutputJob::Silent);
596
    job->setType(VcsJob::Log);
597
    *job << "git" << "log" << "--date=raw" << "--name-status" << "-M80%" << "--follow";
598 599 600
    QString rev = revisionInterval(dst, src);
    if(!rev.isEmpty())
        *job << rev;
601
    *job << "--" << localLocation;
602
    connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitLogOutput);
603
    return job;
604 605
}

606

Milian Wolff's avatar
Milian Wolff committed
607
VcsJob* GitPlugin::log(const QUrl& localLocation, const KDevelop::VcsRevision& rev, unsigned long int limit)
608
{
609
    DVcsJob* job = new GitJob(dotGitDirectory(localLocation), this, KDevelop::OutputJob::Silent);
610
    job->setType(VcsJob::Log);
611
    *job << "git" << "log" << "--date=raw" << "--name-status" << "-M80%" << "--follow";
612 613 614
    QString revStr = toRevisionName(rev, QString());
    if(!revStr.isEmpty())
        *job << revStr;
615
    if(limit>0)
616
        *job << QStringLiteral("-%1").arg(limit);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
617

618
    *job << "--" << localLocation;
619
    connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitLogOutput);
620
    return job;
621 622
}

Milian Wolff's avatar
Milian Wolff committed
623
KDevelop::VcsJob* GitPlugin::annotate(const QUrl &localLocation, const KDevelop::VcsRevision&)
624
{
625
    DVcsJob* job = new GitJob(dotGitDirectory(localLocation), this, KDevelop::OutputJob::Silent);
626
    job->setType(VcsJob::Annotate);
627
    *job << "git" << "blame" << "--porcelain" << "-w";
628
    *job << "--" << localLocation;
629
    connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitBlameOutput);
630
    return job;
631 632
}

633 634
void GitPlugin::parseGitBlameOutput(DVcsJob *job)
{
635
    QVariantList results;
636
    VcsAnnotationLine* annotation = nullptr;
637 638
    const auto output = job->output();
    const auto lines = output.splitRef('\n');
Dāvis Mosāns's avatar
Dāvis Mosāns committed
639

640 641
    bool skipNext=false;
    QMap<QString, VcsAnnotationLine> definedRevisions;
642
    for(QVector<QStringRef>::const_iterator it=lines.constBegin(), itEnd=lines.constEnd();
643 644 645 646 647
        it!=itEnd; ++it)
    {
        if(skipNext) {
            skipNext=false;
            results += qVariantFromValue(*annotation);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
648

649 650
            continue;
        }
Dāvis Mosāns's avatar
Dāvis Mosāns committed
651

652 653
        if(it->isEmpty())
            continue;
Dāvis Mosāns's avatar
Dāvis Mosāns committed
654

655 656
        QStringRef name = it->left(it->indexOf(' '));
        QStringRef value = it->right(it->size()-name.size()-1);
657

658
        if(name==QLatin1String("author"))
659
            annotation->setAuthor(value.toString());
660 661 662
        else if(name==QLatin1String("author-mail")) {} //TODO: do smth with the e-mail?
        else if(name==QLatin1String("author-tz")) {} //TODO: does it really matter?
        else if(name==QLatin1String("author-time"))
663
            annotation->setDate(QDateTime::fromTime_t(value.toUInt()));
664
        else if(name==QLatin1String("summary"))
665
            annotation->setCommitMessage(value.toString());
666
        else if(name.startsWith(QStringLiteral("committer"))) {} //We will just store the authors
667 668 669
        else if(name==QLatin1String("previous")) {} //We don't need that either
        else if(name==QLatin1String("filename")) { skipNext=true; }
        else if(name==QLatin1String("boundary")) {
670
            definedRevisions.insert(QStringLiteral("boundary"), VcsAnnotationLine());
671 672 673
        }
        else
        {
674
            const auto values = value.split(' ');
Dāvis Mosāns's avatar
Dāvis Mosāns committed
675

676
            VcsRevision rev;
677
            rev.setRevisionValue(name.left(8).toString(), KDevelop::VcsRevision::GlobalNumber);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
678

679
            skipNext = definedRevisions.contains(name.toString());
Dāvis Mosāns's avatar
Dāvis Mosāns committed
680

681
            if(!skipNext)
682
                definedRevisions.insert(name.toString(), VcsAnnotationLine());
Dāvis Mosāns's avatar
Dāvis Mosāns committed
683

684
            annotation = &definedRevisions[name.toString()];
685
            annotation->setLineNumber(values[1].toInt() - 1);
686
            annotation->setRevision(rev);
687 688 689 690
        }
    }
    job->setResults(results);
}
691

692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707

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
708
VcsJob* GitPlugin::tag(const QUrl& repository, const QString& commitMessage, const VcsRevision& rev, const QString& tagName)
709 710 711 712 713 714 715 716
{
    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
717
VcsJob* GitPlugin::switchBranch(const QUrl &repository, const QString &branch)
718
{
719
    QDir d=urlDir(repository);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
720

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

726
    DVcsJob* job = new DVcsJob(d, this);
727 728
    *job << "git" << "checkout" << branch;
    return job;
729 730
}

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

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

738 739
    if(!rev.prettyValue().isEmpty())
        *job << rev.revisionValue().toString();
740
    return job;
741 742
}

Milian Wolff's avatar
Milian Wolff committed
743
VcsJob* GitPlugin::deleteBranch(const QUrl& repository, const QString& branchName)
744
{
745 746
    DVcsJob* job = new DVcsJob(urlDir(repository), this, OutputJob::Silent);
    *job << "git" << "branch" << "-D" << branchName;
747
    connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitCurrentBranch);
748
    return job;
749 750
}

Milian Wolff's avatar
Milian Wolff committed
751
VcsJob* GitPlugin::renameBranch(const QUrl& repository, const QString& oldBranchName, const QString& newBranchName)
752
{
753 754
    DVcsJob* job = new DVcsJob(urlDir(repository), this, OutputJob::Silent);
    *job << "git" << "branch" << "-m" << newBranchName << oldBranchName;
755
    connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitCurrentBranch);
756 757 758
    return job;
}

759 760 761 762 763 764 765 766 767 768
VcsJob* GitPlugin::mergeBranch(const QUrl& repository, const QString& branchName)
{
    Q_ASSERT(!branchName.isEmpty());

    DVcsJob* job = new DVcsJob(urlDir(repository), this);
    *job << "git" << "merge" << branchName;

    return job;
}

Milian Wolff's avatar
Milian Wolff committed
769
VcsJob* GitPlugin::currentBranch(const QUrl& repository)
770
{
771
    DVcsJob* job = new DVcsJob(urlDir(repository), this, OutputJob::Silent);
772
    job->setIgnoreError(true);