1 /*
2     SPDX-FileCopyrightText: 2008 Evgeniy Ivanov <powerfox@kde.ru>
3     SPDX-FileCopyrightText: 2009 Hugo Parente Lima <hugo.pl@gmail.com>
4     SPDX-FileCopyrightText: 2010 Aleix Pol Gonzalez <aleixpol@kde.org>
5 
6     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
7 */
8 
9 #include "gitplugin.h"
10 
11 #include "repostatusmodel.h"
12 #include "committoolview.h"
13 
14 #include <QDateTime>
15 #include <QProcess>
16 #include <QDir>
17 #include <QFileInfo>
18 #include <QMenu>
19 #include <QTimer>
20 #include <QRegularExpression>
21 #include <QPointer>
22 #include <QTemporaryFile>
23 #include <QVersionNumber>
24 
25 #include <interfaces/icore.h>
26 #include <interfaces/iproject.h>
27 #include <interfaces/iruncontroller.h>
28 #include <interfaces/iuicontroller.h>
29 
30 #include <util/path.h>
31 
32 #include <vcs/vcsjob.h>
33 #include <vcs/vcsrevision.h>
34 #include <vcs/vcsevent.h>
35 #include <vcs/vcslocation.h>
36 #include <vcs/dvcs/dvcsjob.h>
37 #include <vcs/vcsannotation.h>
38 #include <vcs/widgets/standardvcslocationwidget.h>
39 #include "gitclonejob.h"
40 #include "rebasedialog.h"
41 #include "stashmanagerdialog.h"
42 
43 #include <KDirWatch>
44 #include <KIO/CopyJob>
45 #include <KLocalizedString>
46 #include <KMessageBox>
47 #include <KTextEdit>
48 #include <KTextEditor/Document>
49 
50 #include "gitjob.h"
51 #include "gitmessagehighlighter.h"
52 #include "gitplugincheckinrepositoryjob.h"
53 #include "gitnameemaildialog.h"
54 #include "debug.h"
55 
56 #include <array>
57 
58 using namespace KDevelop;
59 
runSynchronously(KDevelop::VcsJob * job)60 QVariant runSynchronously(KDevelop::VcsJob* job)
61 {
62     QVariant ret;
63     if(job->exec() && job->status()==KDevelop::VcsJob::JobSucceeded) {
64         ret = job->fetchResults();
65     }
66     delete job;
67     return ret;
68 }
69 
70 namespace
71 {
72 
dotGitDirectory(const QUrl & dirPath,bool silent=false)73 QDir dotGitDirectory(const QUrl& dirPath, bool silent = false)
74 {
75     const QFileInfo finfo(dirPath.toLocalFile());
76     QDir dir = finfo.isDir() ? QDir(finfo.filePath()): finfo.absoluteDir();
77 
78     const QString gitDir = QStringLiteral(".git");
79     while (!dir.exists(gitDir) && dir.cdUp()) {} // cdUp, until there is a sub-directory called .git
80 
81     if (!silent && dir.isRoot()) {
82         qCWarning(PLUGIN_GIT) << "couldn't find the git root for" << dirPath;
83     }
84 
85     return dir;
86 }
87 
88 /**
89  * Whenever a directory is provided, change it for all the files in it but not inner directories,
90  * that way we make sure we won't get into recursion,
91  */
preventRecursion(const QList<QUrl> & urls)92 static QList<QUrl> preventRecursion(const QList<QUrl>& urls)
93 {
94     QList<QUrl> ret;
95     for (const QUrl& url : urls) {
96         QDir d(url.toLocalFile());
97         if(d.exists()) {
98             const QStringList entries = d.entryList(QDir::Files | QDir::NoDotAndDotDot);
99             ret.reserve(ret.size() + entries.size());
100             for (const QString& entry : entries) {
101                 QUrl entryUrl = QUrl::fromLocalFile(d.absoluteFilePath(entry));
102                 ret += entryUrl;
103             }
104         } else
105             ret += url;
106     }
107     return ret;
108 }
109 
toRevisionName(const KDevelop::VcsRevision & rev,const QString & currentRevision=QString ())110 QString toRevisionName(const KDevelop::VcsRevision& rev, const QString& currentRevision=QString())
111 {
112     switch(rev.revisionType()) {
113         case VcsRevision::Special:
114             switch(rev.revisionValue().value<VcsRevision::RevisionSpecialType>()) {
115                 case VcsRevision::Head:
116                     return QStringLiteral("^HEAD");
117                 case VcsRevision::Base:
118                     return QString();
119                 case VcsRevision::Working:
120                     return QString();
121                 case VcsRevision::Previous:
122                     Q_ASSERT(!currentRevision.isEmpty());
123                     return currentRevision + QLatin1String("^1");
124                 case VcsRevision::Start:
125                     return QString();
126                 case VcsRevision::UserSpecialType: //Not used
127                     Q_ASSERT(false && "i don't know how to do that");
128             }
129             break;
130         case VcsRevision::GlobalNumber:
131             return rev.revisionValue().toString();
132         case VcsRevision::Date:
133         case VcsRevision::FileNumber:
134         case VcsRevision::Invalid:
135         case VcsRevision::UserType:
136             Q_ASSERT(false);
137     }
138     return QString();
139 }
140 
revisionInterval(const KDevelop::VcsRevision & rev,const KDevelop::VcsRevision & limit)141 QString revisionInterval(const KDevelop::VcsRevision& rev, const KDevelop::VcsRevision& limit)
142 {
143     QString ret;
144     if(rev.revisionType()==VcsRevision::Special &&
145                 rev.revisionValue().value<VcsRevision::RevisionSpecialType>()==VcsRevision::Start) //if we want it to the beginning just put the revisionInterval
146         ret = toRevisionName(limit, QString());
147     else {
148         QString dst = toRevisionName(limit);
149         if(dst.isEmpty())
150             ret = dst;
151         else {
152             QString src = toRevisionName(rev, dst);
153             if(src.isEmpty())
154                 ret = src;
155             else
156                 ret = src + QLatin1String("..") + dst;
157         }
158     }
159     return ret;
160 }
161 
urlDir(const QUrl & url)162 QDir urlDir(const QUrl& url)
163 {
164     QFileInfo f(url.toLocalFile());
165     if(f.isDir())
166         return QDir(url.toLocalFile());
167     else
168         return f.absoluteDir();
169 }
urlDir(const QList<QUrl> & urls)170 QDir urlDir(const QList<QUrl>& urls) { return urlDir(urls.first()); } //TODO: could be improved
171 
172 }
173 
GitPlugin(QObject * parent,const QVariantList &)174 GitPlugin::GitPlugin(QObject* parent, const QVariantList&)
175     : DistributedVersionControlPlugin(parent, QStringLiteral("kdevgit"))
176     , m_repoStatusModel(new RepoStatusModel(this))
177     , m_commitToolViewFactory(new CommitToolViewFactory(m_repoStatusModel))
178 {
179     if (QStandardPaths::findExecutable(QStringLiteral("git")).isEmpty()) {
180         setErrorDescription(i18n("Unable to find git executable. Is it installed on the system?"));
181         return;
182     }
183 
184     // FIXME: Is this needed (I don't quite understand the comment
185     // in vcsstatusinfo.h which says we need to do this if we want to
186     // use VcsStatusInfo in queued signals/slots)
187     qRegisterMetaType<VcsStatusInfo>();
188 
189     ICore::self()->uiController()->addToolView(i18n("Git Commit"), m_commitToolViewFactory);
190 
191     setObjectName(QStringLiteral("Git"));
192 
193     auto* versionJob = new GitJob(QDir::tempPath(), this, KDevelop::OutputJob::Silent);
194     *versionJob << "git" << "--version";
195     connect(versionJob, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitVersionOutput);
196     ICore::self()->runController()->registerJob(versionJob);
197 
198     m_watcher = new KDirWatch(this);
199     connect(m_watcher, &KDirWatch::dirty, this, &GitPlugin::fileChanged);
200     connect(m_watcher, &KDirWatch::created, this, &GitPlugin::fileChanged);
201 }
202 
~GitPlugin()203 GitPlugin::~GitPlugin()
204 {}
205 
emptyOutput(DVcsJob * job)206 bool emptyOutput(DVcsJob* job)
207 {
208     QScopedPointer<DVcsJob> _job(job);
209     if(job->exec() && job->status()==VcsJob::JobSucceeded)
210         return job->rawOutput().trimmed().isEmpty();
211 
212     return false;
213 }
214 
hasStashes(const QDir & repository)215 bool GitPlugin::hasStashes(const QDir& repository)
216 {
217     return !emptyOutput(gitStash(repository, QStringList(QStringLiteral("list")), KDevelop::OutputJob::Silent));
218 }
219 
hasModifications(const QDir & d)220 bool GitPlugin::hasModifications(const QDir& d)
221 {
222     return !emptyOutput(lsFiles(d, QStringList(QStringLiteral("-m")), OutputJob::Silent));
223 }
224 
hasModifications(const QDir & repo,const QUrl & file)225 bool GitPlugin::hasModifications(const QDir& repo, const QUrl& file)
226 {
227     return !emptyOutput(lsFiles(repo, QStringList{QStringLiteral("-m"), file.path()}, OutputJob::Silent));
228 }
229 
additionalMenuEntries(QMenu * menu,const QList<QUrl> & urls)230 void GitPlugin::additionalMenuEntries(QMenu* menu, const QList<QUrl>& urls)
231 {
232     m_urls = urls;
233 
234     QDir dir=urlDir(urls);
235     bool hasSt = hasStashes(dir);
236 
237     menu->addAction(i18nc("@action:inmenu", "Rebase"), this, SLOT(ctxRebase()));
238     menu->addSeparator()->setText(i18nc("@title:menu", "Git Stashes"));
239     menu->addAction(i18nc("@action:inmenu", "Stash Manager"), this, SLOT(ctxStashManager()))->setEnabled(hasSt);
240     menu->addAction(QIcon::fromTheme(QStringLiteral("vcs-stash")), i18nc("@action:inmenu", "Push Stash"), this, SLOT(ctxPushStash()));
241     menu->addAction(QIcon::fromTheme(QStringLiteral("vcs-stash-pop")), i18nc("@action:inmenu", "Pop Stash"), this, SLOT(ctxPopStash()))->setEnabled(hasSt);
242 }
243 
ctxRebase()244 void GitPlugin::ctxRebase()
245 {
246     auto* dialog = new RebaseDialog(this, m_urls.first(), nullptr);
247     dialog->setAttribute(Qt::WA_DeleteOnClose);
248     dialog->open();
249 }
250 
ctxPushStash()251 void GitPlugin::ctxPushStash()
252 {
253     VcsJob* job = gitStash(urlDir(m_urls), QStringList(), KDevelop::OutputJob::Verbose);
254     ICore::self()->runController()->registerJob(job);
255 }
256 
ctxPopStash()257 void GitPlugin::ctxPopStash()
258 {
259     VcsJob* job = gitStash(urlDir(m_urls), QStringList(QStringLiteral("pop")), KDevelop::OutputJob::Verbose);
260     ICore::self()->runController()->registerJob(job);
261 }
262 
ctxStashManager()263 void GitPlugin::ctxStashManager()
264 {
265     QPointer<StashManagerDialog> d = new StashManagerDialog(urlDir(m_urls), this, nullptr);
266     d->exec();
267 
268     delete d;
269 }
270 
errorsFound(const QString & error,KDevelop::OutputJob::OutputJobVerbosity verbosity=OutputJob::Verbose)271 DVcsJob* GitPlugin::errorsFound(const QString& error, KDevelop::OutputJob::OutputJobVerbosity verbosity=OutputJob::Verbose)
272 {
273     auto* j = new GitJob(QDir::temp(), this, verbosity);
274     *j << "echo" << i18n("error: %1", error) << "-n";
275     return j;
276 }
277 
name() const278 QString GitPlugin::name() const
279 {
280     return QStringLiteral("Git");
281 }
282 
repositoryRoot(const QUrl & path)283 QUrl GitPlugin::repositoryRoot(const QUrl& path)
284 {
285     return QUrl::fromLocalFile(dotGitDirectory(path).absolutePath());
286 }
287 
isValidDirectory(const QUrl & dirPath)288 bool GitPlugin::isValidDirectory(const QUrl & dirPath)
289 {
290     QDir dir = dotGitDirectory(dirPath, true);
291     QFile dotGitPotentialFile(dir.filePath(QStringLiteral(".git")));
292     // if .git is a file, we may be in a git worktree
293     QFileInfo dotGitPotentialFileInfo(dotGitPotentialFile);
294     if (!dotGitPotentialFileInfo.isDir() && dotGitPotentialFile.exists()) {
295         QString gitWorktreeFileContent;
296         if (dotGitPotentialFile.open(QFile::ReadOnly)) {
297             // the content should be gitdir: /path/to/the/.git/worktree
298             gitWorktreeFileContent = QString::fromUtf8(dotGitPotentialFile.readAll());
299             dotGitPotentialFile.close();
300         } else {
301             return false;
302         }
303         const auto items = gitWorktreeFileContent.split(QLatin1Char(' '));
304         if (items.size() == 2 && items.at(0) == QLatin1String("gitdir:")) {
305             qCDebug(PLUGIN_GIT) << "we are in a git worktree" << items.at(1);
306             return true;
307         }
308     }
309     return dir.exists(QStringLiteral(".git/HEAD"));
310 }
311 
isValidRemoteRepositoryUrl(const QUrl & remoteLocation)312 bool GitPlugin::isValidRemoteRepositoryUrl(const QUrl& remoteLocation)
313 {
314     if (remoteLocation.isLocalFile()) {
315         QFileInfo fileInfo(remoteLocation.toLocalFile());
316         if (fileInfo.isDir()) {
317             QDir dir(fileInfo.filePath());
318             if (dir.exists(QStringLiteral(".git/HEAD"))) {
319                 return true;
320             }
321             // TODO: check also for bare repo
322         }
323     } else {
324         const QString scheme = remoteLocation.scheme();
325         if (scheme == QLatin1String("git") || scheme == QLatin1String("git+ssh")) {
326             return true;
327         }
328         // heuristic check, anything better we can do here without talking to server?
329         if ((scheme == QLatin1String("http") ||
330              scheme == QLatin1String("https")) &&
331             remoteLocation.path().endsWith(QLatin1String(".git"))) {
332             return true;
333         }
334     }
335     return false;
336 }
337 
isVersionControlled(const QUrl & path)338 bool GitPlugin::isVersionControlled(const QUrl &path)
339 {
340     QFileInfo fsObject(path.toLocalFile());
341     if (!fsObject.exists()) {
342         return false;
343     }
344     if (fsObject.isDir()) {
345         return isValidDirectory(path);
346     }
347 
348     QString filename = fsObject.fileName();
349 
350     QStringList otherFiles = getLsFiles(fsObject.dir(), QStringList(QStringLiteral("--")) << filename, KDevelop::OutputJob::Silent);
351     return !otherFiles.empty();
352 }
353 
init(const QUrl & directory)354 VcsJob* GitPlugin::init(const QUrl &directory)
355 {
356     auto* job = new GitJob(urlDir(directory), this);
357     job->setType(VcsJob::Import);
358     *job << "git" << "init";
359     return job;
360 }
361 
createWorkingCopy(const KDevelop::VcsLocation & source,const QUrl & dest,KDevelop::IBasicVersionControl::RecursionMode)362 VcsJob* GitPlugin::createWorkingCopy(const KDevelop::VcsLocation & source, const QUrl& dest, KDevelop::IBasicVersionControl::RecursionMode)
363 {
364     DVcsJob* job = new GitCloneJob(urlDir(dest), this);
365     job->setType(VcsJob::Import);
366     *job << "git" << "clone" << "--progress" << "--" << source.localUrl().url() << dest;
367     return job;
368 }
369 
add(const QList<QUrl> & localLocations,KDevelop::IBasicVersionControl::RecursionMode recursion)370 VcsJob* GitPlugin::add(const QList<QUrl>& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion)
371 {
372     if (localLocations.empty())
373         return errorsFound(i18n("Did not specify the list of files"), OutputJob::Verbose);
374 
375     DVcsJob* job = new GitJob(dotGitDirectory(localLocations.front()), this);
376     job->setType(VcsJob::Add);
377     *job << "git" << "add" << "--" << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations));
378     return job;
379 }
380 
status(const QList<QUrl> & localLocations,KDevelop::IBasicVersionControl::RecursionMode recursion)381 KDevelop::VcsJob* GitPlugin::status(const QList<QUrl>& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion)
382 {
383     if (localLocations.empty())
384         return errorsFound(i18n("Did not specify the list of files"), OutputJob::Verbose);
385 
386     DVcsJob* job = new GitJob(urlDir(localLocations), this, OutputJob::Silent);
387     job->setType(VcsJob::Status);
388 
389     if(m_oldVersion) {
390         *job << "git" << "ls-files" << "-t" << "-m" << "-c" << "-o" << "-d" << "-k" << "--directory";
391         connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitStatusOutput_old);
392     } else {
393         *job << "git" << "status" << "--porcelain";
394         job->setIgnoreError(true);
395         connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitStatusOutput);
396     }
397     *job << "--" << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations));
398 
399     return job;
400 }
401 
diff(const QUrl & fileOrDirectory,const KDevelop::VcsRevision & srcRevision,const KDevelop::VcsRevision & dstRevision,IBasicVersionControl::RecursionMode recursion)402 VcsJob* GitPlugin::diff(const QUrl& fileOrDirectory, const KDevelop::VcsRevision& srcRevision, const KDevelop::VcsRevision& dstRevision,
403                         IBasicVersionControl::RecursionMode recursion)
404 {
405     DVcsJob* job = static_cast<DVcsJob*>(diff(fileOrDirectory, srcRevision, dstRevision));
406     *job << "--";
407     if (recursion == IBasicVersionControl::Recursive) {
408         *job << fileOrDirectory;
409     } else {
410         *job << preventRecursion(QList<QUrl>() << fileOrDirectory);
411     }
412     return job;
413 }
414 
diff(const QUrl & repoPath,const KDevelop::VcsRevision & srcRevision,const KDevelop::VcsRevision & dstRevision)415 KDevelop::VcsJob * GitPlugin::diff(const QUrl& repoPath, const KDevelop::VcsRevision& srcRevision, const KDevelop::VcsRevision& dstRevision)
416 {
417     DVcsJob* job = new GitJob(dotGitDirectory(repoPath), this, KDevelop::OutputJob::Silent);
418     job->setType(VcsJob::Diff);
419     *job << "git" << "diff" << "--no-color" << "--no-ext-diff";
420     if (!usePrefix()) {
421         // KDE's ReviewBoard now requires p1 patchfiles, so `git diff --no-prefix` to generate p0 patches
422         // has become optional.
423         *job << "--no-prefix";
424     }
425     if (dstRevision.revisionType() == VcsRevision::Special &&
426          dstRevision.specialType() == VcsRevision::Working) {
427         if (srcRevision.revisionType() == VcsRevision::Special &&
428              srcRevision.specialType() == VcsRevision::Base) {
429             *job << "HEAD";
430         } else {
431             *job << "--cached" << srcRevision.revisionValue().toString();
432         }
433     } else {
434         QString revstr = revisionInterval(srcRevision, dstRevision);
435         if(!revstr.isEmpty())
436             *job << revstr;
437     }
438     connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitDiffOutput);
439     return job;
440 }
441 
442 
reset(const QList<QUrl> & localLocations,KDevelop::IBasicVersionControl::RecursionMode recursion)443 KDevelop::VcsJob * GitPlugin::reset ( const QList<QUrl>& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion )
444 {
445     if(localLocations.isEmpty() )
446         return errorsFound(i18n("Could not reset changes (empty list of paths)"), OutputJob::Verbose);
447 
448     DVcsJob* job = new GitJob(dotGitDirectory(localLocations.front()), this);
449     job->setType(VcsJob::Reset);
450     *job << "git" << "reset" << "--";
451     *job << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations));
452     return job;
453 }
454 
apply(const KDevelop::VcsDiff & diff,const ApplyParams applyTo)455 KDevelop::VcsJob * GitPlugin::apply(const KDevelop::VcsDiff& diff, const ApplyParams applyTo)
456 {
457     DVcsJob* job = new GitJob(dotGitDirectory(diff.baseDiff()), this);
458     job->setType(VcsJob::Apply);
459     *job << "git" << "apply";
460     if (applyTo == Index) {
461         *job << "--index";   // Applies the diff also to the index
462         *job << "--cached";  // Does not touch the work tree
463     }
464     auto* diffFile = new QTemporaryFile(this);
465     if (diffFile->open()) {
466         *job << diffFile->fileName();
467         diffFile->write(diff.diff().toUtf8());
468         diffFile->close();
469         connect(job, &KDevelop::VcsJob::resultsReady, [=](){delete diffFile;});
470     } else {
471         job->cancel();
472         delete diffFile;
473     }
474     return job;
475 }
476 
477 
revert(const QList<QUrl> & localLocations,IBasicVersionControl::RecursionMode recursion)478 VcsJob* GitPlugin::revert(const QList<QUrl>& localLocations, IBasicVersionControl::RecursionMode recursion)
479 {
480     if(localLocations.isEmpty() )
481         return errorsFound(i18n("Could not revert changes"), OutputJob::Verbose);
482 
483     QDir repo = urlDir(repositoryRoot(localLocations.first()));
484     QString modified;
485     for (const auto& file: localLocations) {
486         if (hasModifications(repo, file)) {
487             modified.append(file.toDisplayString(QUrl::PreferLocalFile) + QLatin1String("<br/>"));
488         }
489     }
490     if (!modified.isEmpty()) {
491         auto res = KMessageBox::questionYesNo(nullptr, i18n("The following files have uncommitted changes, "
492                                               "which will be lost. Continue?") + QLatin1String("<br/><br/>") + modified);
493         if (res != KMessageBox::Yes) {
494             return errorsFound(QString(), OutputJob::Silent);
495         }
496     }
497 
498     DVcsJob* job = new GitJob(dotGitDirectory(localLocations.front()), this);
499     job->setType(VcsJob::Revert);
500     *job << "git" << "checkout" << "--";
501     *job << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations));
502 
503     return job;
504 }
505 
506 
507 //TODO: git doesn't like empty messages, but "KDevelop didn't provide any message, it may be a bug" looks ugly...
508 //If no files specified then commit already added files
commit(const QString & message,const QList<QUrl> & localLocations,KDevelop::IBasicVersionControl::RecursionMode recursion)509 VcsJob* GitPlugin::commit(const QString& message,
510                              const QList<QUrl>& localLocations,
511                              KDevelop::IBasicVersionControl::RecursionMode recursion)
512 {
513     if (localLocations.empty() || message.isEmpty())
514         return errorsFound(i18n("No files or message specified"));
515 
516     const QDir dir = dotGitDirectory(localLocations.front());
517     if (!ensureValidGitIdentity(dir)) {
518         return errorsFound(i18n("Email or name for Git not specified"));
519     }
520 
521     auto* job = new GitJob(dir, this);
522     job->setType(VcsJob::Commit);
523     QList<QUrl> files = (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations));
524     addNotVersionedFiles(dir, files);
525 
526     *job << "git" << "commit" << "-m" << message;
527     *job << "--" << files;
528     return job;
529 }
530 
commitStaged(const QString & message,const QUrl & repoUrl)531 KDevelop::VcsJob * GitPlugin::commitStaged(const QString& message, const QUrl& repoUrl)
532 {
533     if (message.isEmpty())
534         return errorsFound(i18n("No message specified"));
535     const QDir dir = dotGitDirectory(repoUrl);
536     if (!ensureValidGitIdentity(dir)) {
537         return errorsFound(i18n("Email or name for Git not specified"));
538     }
539     auto* job = new GitJob(dir, this);
540     job->setType(VcsJob::Commit);
541     *job << "git" << "commit" << "-m" << message;
542     return job;
543 }
544 
545 
ensureValidGitIdentity(const QDir & dir)546 bool GitPlugin::ensureValidGitIdentity(const QDir& dir)
547 {
548     const QUrl url = QUrl::fromLocalFile(dir.absolutePath());
549 
550     const QString name = readConfigOption(url, QStringLiteral("user.name"));
551     const QString email = readConfigOption(url, QStringLiteral("user.email"));
552     if (!email.isEmpty() && !name.isEmpty()) {
553         return true; // already okay
554     }
555 
556     GitNameEmailDialog dialog;
557     dialog.setName(name);
558     dialog.setEmail(email);
559     if (!dialog.exec()) {
560         return false;
561     }
562 
563     runSynchronously(setConfigOption(url, QStringLiteral("user.name"), dialog.name(), dialog.isGlobal()));
564     runSynchronously(setConfigOption(url, QStringLiteral("user.email"), dialog.email(), dialog.isGlobal()));
565     return true;
566 }
567 
addNotVersionedFiles(const QDir & dir,const QList<QUrl> & files)568 void GitPlugin::addNotVersionedFiles(const QDir& dir, const QList<QUrl>& files)
569 {
570     const QStringList otherStr = getLsFiles(dir, QStringList() << QStringLiteral("--others"), KDevelop::OutputJob::Silent);
571     QList<QUrl> toadd, otherFiles;
572 
573     otherFiles.reserve(otherStr.size());
574     for (const QString& file : otherStr) {
575         QUrl v = QUrl::fromLocalFile(dir.absoluteFilePath(file));
576 
577         otherFiles += v;
578     }
579 
580     //We add the files that are not versioned
581     for (const QUrl& file : files) {
582         if(otherFiles.contains(file) && QFileInfo(file.toLocalFile()).isFile())
583             toadd += file;
584     }
585 
586     if(!toadd.isEmpty()) {
587         VcsJob* job = add(toadd);
588         job->exec(); // krazy:exclude=crashy
589     }
590 }
591 
isEmptyDirStructure(const QDir & dir)592 bool isEmptyDirStructure(const QDir &dir)
593 {
594     const auto infos = dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot);
595     for (const QFileInfo& i : infos) {
596         if (i.isDir()) {
597             if (!isEmptyDirStructure(QDir(i.filePath()))) return false;
598         } else if (i.isFile()) {
599             return false;
600         }
601     }
602     return true;
603 }
604 
remove(const QList<QUrl> & files)605 VcsJob* GitPlugin::remove(const QList<QUrl>& files)
606 {
607     if (files.isEmpty())
608         return errorsFound(i18n("No files to remove"));
609     QDir dotGitDir = dotGitDirectory(files.front());
610 
611 
612     QList<QUrl> files_(files);
613 
614     QMutableListIterator<QUrl> i(files_);
615     while (i.hasNext()) {
616         QUrl file = i.next();
617         QFileInfo fileInfo(file.toLocalFile());
618 
619         const QStringList otherStr = getLsFiles(dotGitDir, QStringList{QStringLiteral("--others"), QStringLiteral("--"), file.toLocalFile()}, KDevelop::OutputJob::Silent);
620         if(!otherStr.isEmpty()) {
621             //remove files not under version control
622             QList<QUrl> otherFiles;
623             otherFiles.reserve(otherStr.size());
624             for (const QString& f : otherStr) {
625                 otherFiles << QUrl::fromLocalFile(dotGitDir.path() + QLatin1Char('/') + f);
626             }
627             if (fileInfo.isFile()) {
628                 //if it's an unversioned file we are done, don't use git rm on it
629                 i.remove();
630             }
631 
632             auto trashJob = KIO::trash(otherFiles);
633             trashJob->exec();
634             qCDebug(PLUGIN_GIT) << "other files" << otherFiles;
635         }
636 
637         if (fileInfo.isDir()) {
638             if (isEmptyDirStructure(QDir(file.toLocalFile()))) {
639                 //remove empty folders, git doesn't do that
640                 auto trashJob = KIO::trash(file);
641                 trashJob->exec();
642                 qCDebug(PLUGIN_GIT) << "empty folder, removing" << file;
643                 //we already deleted it, don't use git rm on it
644                 i.remove();
645             }
646         }
647     }
648 
649     if (files_.isEmpty()) return nullptr;
650 
651     DVcsJob* job = new GitJob(dotGitDir, this);
652     job->setType(VcsJob::Remove);
653     // git refuses to delete files with local modifications
654     // use --force to overcome this
655     *job << "git" << "rm" << "-r" << "--force";
656     *job << "--" << files_;
657     return job;
658 }
659 
log(const QUrl & localLocation,const KDevelop::VcsRevision & src,const KDevelop::VcsRevision & dst)660 VcsJob* GitPlugin::log(const QUrl& localLocation,
661                 const KDevelop::VcsRevision& src, const KDevelop::VcsRevision& dst)
662 {
663     DVcsJob* job = new GitJob(dotGitDirectory(localLocation), this, KDevelop::OutputJob::Silent);
664     job->setType(VcsJob::Log);
665     *job << "git" << "log" << "--date=raw" << "--name-status" << "-M80%" << "--follow";
666     QString rev = revisionInterval(dst, src);
667     if(!rev.isEmpty())
668         *job << rev;
669     *job << "--" << localLocation;
670     connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitLogOutput);
671     return job;
672 }
673 
674 
log(const QUrl & localLocation,const KDevelop::VcsRevision & rev,unsigned long int limit)675 VcsJob* GitPlugin::log(const QUrl& localLocation, const KDevelop::VcsRevision& rev, unsigned long int limit)
676 {
677     DVcsJob* job = new GitJob(dotGitDirectory(localLocation), this, KDevelop::OutputJob::Silent);
678     job->setType(VcsJob::Log);
679     *job << "git" << "log" << "--date=raw" << "--name-status" << "-M80%" << "--follow";
680     QString revStr = toRevisionName(rev, QString());
681     if(!revStr.isEmpty())
682         *job << revStr;
683     if(limit>0)
684         *job << QStringLiteral("-%1").arg(limit);
685 
686     *job << "--" << localLocation;
687     connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitLogOutput);
688     return job;
689 }
690 
annotate(const QUrl & localLocation,const KDevelop::VcsRevision &)691 KDevelop::VcsJob* GitPlugin::annotate(const QUrl &localLocation, const KDevelop::VcsRevision&)
692 {
693     DVcsJob* job = new GitJob(dotGitDirectory(localLocation), this, KDevelop::OutputJob::Silent);
694     job->setType(VcsJob::Annotate);
695     *job << "git" << "blame" << "--porcelain" << "-w";
696     *job << "--" << localLocation;
697     connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitBlameOutput);
698     return job;
699 }
700 
parseGitBlameOutput(DVcsJob * job)701 void GitPlugin::parseGitBlameOutput(DVcsJob *job)
702 {
703     QVariantList results;
704     VcsAnnotationLine* annotation = nullptr;
705     const auto output = job->output();
706     const auto lines = output.splitRef(QLatin1Char('\n'));
707 
708     bool skipNext=false;
709     QMap<QString, VcsAnnotationLine> definedRevisions;
710     for (auto& line : lines) {
711         if(skipNext) {
712             skipNext=false;
713             results += QVariant::fromValue(*annotation);
714 
715             continue;
716         }
717 
718         if (line.isEmpty())
719             continue;
720 
721         QStringRef name = line.left(line.indexOf(QLatin1Char(' ')));
722         QStringRef value = line.mid(name.size()+1);
723 
724         if(name==QLatin1String("author"))
725             annotation->setAuthor(value.toString());
726         else if(name==QLatin1String("author-mail")) {} //TODO: do smth with the e-mail?
727         else if(name==QLatin1String("author-tz")) {} //TODO: does it really matter?
728         else if(name==QLatin1String("author-time"))
729             annotation->setDate(QDateTime::fromSecsSinceEpoch(value.toUInt(), Qt::LocalTime));
730         else if(name==QLatin1String("summary"))
731             annotation->setCommitMessage(value.toString());
732         else if(name.startsWith(QLatin1String("committer"))) {} //We will just store the authors
733         else if(name==QLatin1String("previous")) {} //We don't need that either
734         else if(name==QLatin1String("filename")) { skipNext=true; }
735         else if(name==QLatin1String("boundary")) {
736             definedRevisions.insert(QStringLiteral("boundary"), VcsAnnotationLine());
737         }
738         else
739         {
740             const auto values = value.split(QLatin1Char(' '));
741 
742             VcsRevision rev;
743             rev.setRevisionValue(name.left(8).toString(), KDevelop::VcsRevision::GlobalNumber);
744 
745             skipNext = definedRevisions.contains(name.toString());
746 
747             if(!skipNext)
748                 definedRevisions.insert(name.toString(), VcsAnnotationLine());
749 
750             annotation = &definedRevisions[name.toString()];
751             annotation->setLineNumber(values[1].toInt() - 1);
752             annotation->setRevision(rev);
753         }
754     }
755     job->setResults(results);
756 }
757 
758 
lsFiles(const QDir & repository,const QStringList & args,OutputJob::OutputJobVerbosity verbosity)759 DVcsJob* GitPlugin::lsFiles(const QDir &repository, const QStringList &args,
760                             OutputJob::OutputJobVerbosity verbosity)
761 {
762     auto* job = new GitJob(repository, this, verbosity);
763     *job << "git" << "ls-files" << args;
764     return job;
765 }
766 
gitStash(const QDir & repository,const QStringList & args,OutputJob::OutputJobVerbosity verbosity)767 DVcsJob* GitPlugin::gitStash(const QDir& repository, const QStringList& args, OutputJob::OutputJobVerbosity verbosity)
768 {
769     auto* job = new GitJob(repository, this, verbosity);
770     *job << "git" << "stash" << args;
771     return job;
772 }
773 
tag(const QUrl & repository,const QString & commitMessage,const VcsRevision & rev,const QString & tagName)774 VcsJob* GitPlugin::tag(const QUrl& repository, const QString& commitMessage, const VcsRevision& rev, const QString& tagName)
775 {
776     auto* job = new GitJob(urlDir(repository), this);
777     *job << "git" << "tag" << "-m" << commitMessage << tagName;
778     if(rev.revisionValue().isValid())
779         *job << rev.revisionValue().toString();
780     return job;
781 }
782 
switchBranch(const QUrl & repository,const QString & branch)783 VcsJob* GitPlugin::switchBranch(const QUrl &repository, const QString &branch)
784 {
785     QDir d=urlDir(repository);
786 
787     if(hasModifications(d)) {
788         auto answer = KMessageBox::questionYesNoCancel(nullptr, i18n("There are pending changes, do you want to stash them first?"));
789         if (answer == KMessageBox::Yes) {
790             QScopedPointer<DVcsJob> stash(gitStash(d, QStringList(), KDevelop::OutputJob::Verbose));
791             stash->exec();
792         } else if (answer == KMessageBox::Cancel) {
793             return nullptr;
794         }
795     }
796 
797     auto* job = new GitJob(d, this);
798     *job << "git" << "checkout" << branch;
799     return job;
800 }
801 
branch(const QUrl & repository,const KDevelop::VcsRevision & rev,const QString & branchName)802 VcsJob* GitPlugin::branch(const QUrl& repository, const KDevelop::VcsRevision& rev, const QString& branchName)
803 {
804     Q_ASSERT(!branchName.isEmpty());
805 
806     auto* job = new GitJob(urlDir(repository), this);
807     *job << "git" << "branch" << "--" << branchName;
808 
809     if(!rev.prettyValue().isEmpty())
810         *job << rev.revisionValue().toString();
811     return job;
812 }
813 
deleteBranch(const QUrl & repository,const QString & branchName)814 VcsJob* GitPlugin::deleteBranch(const QUrl& repository, const QString& branchName)
815 {
816     auto* job = new GitJob(urlDir(repository), this, OutputJob::Silent);
817     *job << "git" << "branch" << "-D" << branchName;
818     connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitCurrentBranch);
819     return job;
820 }
821 
renameBranch(const QUrl & repository,const QString & oldBranchName,const QString & newBranchName)822 VcsJob* GitPlugin::renameBranch(const QUrl& repository, const QString& oldBranchName, const QString& newBranchName)
823 {
824     auto* job = new GitJob(urlDir(repository), this, OutputJob::Silent);
825     *job << "git" << "branch" << "-m" << newBranchName << oldBranchName;
826     connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitCurrentBranch);
827     return job;
828 }
829 
mergeBranch(const QUrl & repository,const QString & branchName)830 VcsJob* GitPlugin::mergeBranch(const QUrl& repository, const QString& branchName)
831 {
832     Q_ASSERT(!branchName.isEmpty());
833 
834     auto* job = new GitJob(urlDir(repository), this);
835     *job << "git" << "merge" << branchName;
836 
837     return job;
838 }
839 
rebase(const QUrl & repository,const QString & branchName)840 VcsJob* GitPlugin::rebase(const QUrl& repository, const QString& branchName)
841 {
842     auto* job = new GitJob(urlDir(repository), this);
843     *job << "git" << "rebase" << branchName;
844 
845     return job;
846 }
847 
currentBranch(const QUrl & repository)848 VcsJob* GitPlugin::currentBranch(const QUrl& repository)
849 {
850     auto* job = new GitJob(urlDir(repository), this, OutputJob::Silent);
851     job->setIgnoreError(true);
852     *job << "git" << "symbolic-ref" << "-q" << "--short" << "HEAD";
853     connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitCurrentBranch);
854     return job;
855 }
856 
parseGitCurrentBranch(DVcsJob * job)857 void GitPlugin::parseGitCurrentBranch(DVcsJob* job)
858 {
859     QString out = job->output().trimmed();
860 
861     job->setResults(out);
862 }
863 
branches(const QUrl & repository)864 VcsJob* GitPlugin::branches(const QUrl &repository)
865 {
866     auto* job = new GitJob(urlDir(repository));
867     *job << "git" << "branch" << "-a";
868     connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitBranchOutput);
869     return job;
870 }
871 
parseGitBranchOutput(DVcsJob * job)872 void GitPlugin::parseGitBranchOutput(DVcsJob* job)
873 {
874     const auto output = job->output();
875 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
876     const auto branchListDirty = output.splitRef(QLatin1Char('\n'), Qt::SkipEmptyParts);
877 #else
878     const auto branchListDirty = output.splitRef(QLatin1Char('\n'), QString::SkipEmptyParts);
879 #endif
880 
881     QStringList branchList;
882     for (const auto& branch : branchListDirty) {
883         // Skip pointers to another branches (one example of this is "origin/HEAD -> origin/master");
884         // "git rev-list" chokes on these entries and we do not need duplicate branches altogether.
885         if (branch.contains(QLatin1String("->")))
886             continue;
887 
888         // Skip entries such as '(no branch)'
889         if (branch.contains(QLatin1String("(no branch)")))
890             continue;
891 
892         QStringRef name = branch;
893         if (name.startsWith(QLatin1Char('*')))
894             name = branch.mid(2);
895 
896         branchList << name.trimmed().toString();
897     }
898 
899     job->setResults(branchList);
900 }
901 
902 /* Few words about how this hardcore works:
903 1. get all commits (with --parents)
904 2. select master (root) branch and get all unique commits for branches (git-rev-list br2 ^master ^br3)
905 3. parse allCommits. While parsing set mask (columns state for every row) for BRANCH, INITIAL, CROSS,
906    MERGE and INITIAL are also set in DVCScommit::setParents (depending on parents count)
907    another setType(INITIAL) is used for "bottom/root/first" commits of branches
908 4. find and set merges, HEADS. It's an iteration through all commits.
909     - first we check if parent is from the same branch, if no then we go through all commits searching parent's index
910       and set CROSS/HCROSS for rows (in 3 rows are set EMPTY after commit with parent from another tree met)
911     - then we check branchesShas[i][0] to mark heads
912 
913 4 can be a separate function. TODO: All this porn require refactoring (rewriting is better)!
914 
915 It's a very dirty implementation.
916 FIXME:
917 1. HEAD which is head has extra line to connect it with further commit
918 2. If you merge branch2 to master, only new commits of branch2 will be visible (it's fine, but there will be
919 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).
920 3. Commits that have additional commit-data (not only history merging, but changes to fix conflicts) are shown incorrectly
921 */
922 
allCommits(const QString & repo)923 QVector<DVcsEvent> GitPlugin::allCommits(const QString& repo)
924 {
925     initBranchHash(repo);
926 
927     const QStringList args{
928         QStringLiteral("--all"),
929         QStringLiteral("--pretty"),
930         QStringLiteral("--parents"),
931     };
932     QScopedPointer<DVcsJob> job(gitRevList(repo, args));
933     bool ret = job->exec();
934     Q_ASSERT(ret && job->status()==VcsJob::JobSucceeded && "TODO: provide a fall back in case of failing");
935     Q_UNUSED(ret);
936 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
937     const QStringList commits = job->output().split(QLatin1Char('\n'), Qt::SkipEmptyParts);
938 #else
939     QStringList commits = job->output().split(QLatin1Char('\n'), QString::SkipEmptyParts);
940 #endif
941 
942     static QRegExp rx_com(QStringLiteral("commit \\w{40,40}"));
943 
944     QVector<DVcsEvent> commitList;
945     DVcsEvent item;
946 
947     //used to keep where we have empty/cross/branch entry
948     //true if it's an active branch (then cross or branch) and false if not
949     QVector<bool> additionalFlags(branchesShas.count());
950     additionalFlags.fill(false);
951 
952     //parse output
953     for(int i = 0; i < commits.count(); ++i)
954     {
955         if (commits[i].contains(rx_com))
956         {
957             qCDebug(PLUGIN_GIT) << "commit found in " << commits[i];
958             item.setCommit(commits[i].section(QLatin1Char(' '), 1, 1).trimmed());
959 //             qCDebug(PLUGIN_GIT) << "commit is: " << commits[i].section(' ', 1);
960 
961             QStringList parents;
962             QString parent = commits[i].section(QLatin1Char(' '), 2);
963             int section = 2;
964             while (!parent.isEmpty())
965             {
966                 /*                qCDebug(PLUGIN_GIT) << "Parent is: " << parent;*/
967                 parents.append(parent.trimmed());
968                 section++;
969                 parent = commits[i].section(QLatin1Char(' '), section);
970             }
971             item.setParents(parents);
972 
973             //Avoid Merge string
974             while (!commits[i].contains(QLatin1String("Author: ")))
975                     ++i;
976 
977             item.setAuthor(commits[i].section(QStringLiteral("Author: "), 1).trimmed());
978 //             qCDebug(PLUGIN_GIT) << "author is: " << commits[i].section("Author: ", 1);
979 
980             item.setDate(commits[++i].section(QStringLiteral("Date:   "), 1).trimmed());
981 //             qCDebug(PLUGIN_GIT) << "date is: " << commits[i].section("Date:   ", 1);
982 
983             QString log;
984             i++; //next line!
985             while (i < commits.count() && !commits[i].contains(rx_com))
986                 log += commits[i++];
987             --i; //while took commit line
988             item.setLog(log.trimmed());
989 //             qCDebug(PLUGIN_GIT) << "log is: " << log;
990 
991             //mask is used in CommitViewDelegate to understand what we should draw for each branch
992             QList<int> mask;
993             mask.reserve(branchesShas.count());
994 
995             //set mask (properties for each graph column in row)
996             for(int i = 0; i < branchesShas.count(); ++i)
997             {
998                 qCDebug(PLUGIN_GIT)<<"commit: " << item.commit();
999                 if (branchesShas[i].contains(item.commit()))
1000                 {
1001                     mask.append(item.type()); //we set type in setParents
1002 
1003                     //check if parent from the same branch, if not then we have found a root of the branch
1004                     //and will use empty column for all further (from top to bottom) revisions
1005                     //FIXME: we should set CROSS between parent and child (and do it when find merge point)
1006                     additionalFlags[i] = false;
1007                     const auto parentShas = item.parents();
1008                     for (const QString& sha : parentShas) {
1009                         if (branchesShas[i].contains(sha))
1010                             additionalFlags[i] = true;
1011                     }
1012                     if (additionalFlags[i] == false)
1013                        item.setType(DVcsEvent::INITIAL); //hasn't parents from the same branch, used in drawing
1014                 }
1015                 else
1016                 {
1017                     if (additionalFlags[i] == false)
1018                         mask.append(DVcsEvent::EMPTY);
1019                     else
1020                         mask.append(DVcsEvent::CROSS);
1021                 }
1022                 qCDebug(PLUGIN_GIT) << "mask " << i << "is " << mask[i];
1023             }
1024             item.setProperties(mask);
1025             commitList.append(item);
1026         }
1027     }
1028 
1029     //find and set merges, HEADS, require refactoring!
1030     for (auto iter = commitList.begin();
1031         iter != commitList.end(); ++iter)
1032     {
1033         QStringList parents = iter->parents();
1034         //we need only only child branches
1035         if (parents.count() != 1)
1036             break;
1037 
1038         QString parent = parents[0];
1039         const QString commit = iter->commit();
1040         bool parent_checked = false;
1041         int heads_checked = 0;
1042 
1043         for(int i = 0; i < branchesShas.count(); ++i)
1044         {
1045             //check parent
1046             if (branchesShas[i].contains(commit))
1047             {
1048                 if (!branchesShas[i].contains(parent))
1049                 {
1050                     //parent and child are not in same branch
1051                     //since it is list, than parent has i+1 index
1052                     //set CROSS and HCROSS
1053                     for (auto f_iter = iter;
1054                         f_iter != commitList.end(); ++f_iter)
1055                     {
1056                         if (parent == f_iter->commit())
1057                         {
1058                             for(int j = 0; j < i; ++j)
1059                             {
1060                                 if(branchesShas[j].contains(parent))
1061                                     f_iter->setProperty(j, DVcsEvent::MERGE);
1062                                 else
1063                                     f_iter->setProperty(j, DVcsEvent::HCROSS);
1064                             }
1065                             f_iter->setType(DVcsEvent::MERGE);
1066                             f_iter->setProperty(i, DVcsEvent::MERGE_RIGHT);
1067                             qCDebug(PLUGIN_GIT) << parent << " is parent of " << commit;
1068                             qCDebug(PLUGIN_GIT) << f_iter->commit() << " is merge";
1069                             parent_checked = true;
1070                             break;
1071                         }
1072                         else
1073                             f_iter->setProperty(i, DVcsEvent::CROSS);
1074                     }
1075                 }
1076             }
1077             //mark HEADs
1078 
1079             if (!branchesShas[i].empty() && commit == branchesShas[i][0])
1080             {
1081                 iter->setType(DVcsEvent::HEAD);
1082                 iter->setProperty(i, DVcsEvent::HEAD);
1083                 heads_checked++;
1084                 qCDebug(PLUGIN_GIT) << "HEAD found";
1085             }
1086             //some optimization
1087             if (heads_checked == branchesShas.count() && parent_checked)
1088                 break;
1089         }
1090     }
1091 
1092     return commitList;
1093 }
1094 
initBranchHash(const QString & repo)1095 void GitPlugin::initBranchHash(const QString &repo)
1096 {
1097     const QUrl repoUrl = QUrl::fromLocalFile(repo);
1098     const QStringList gitBranches = runSynchronously(branches(repoUrl)).toStringList();
1099     qCDebug(PLUGIN_GIT) << "BRANCHES: " << gitBranches;
1100     //Now root branch is the current branch. In future it should be the longest branch
1101     //other commitLists are got with git-rev-lits branch ^br1 ^ br2
1102     QString root = runSynchronously(currentBranch(repoUrl)).toString();
1103     QScopedPointer<DVcsJob> job(gitRevList(repo, QStringList(root)));
1104     bool ret = job->exec();
1105     Q_ASSERT(ret && job->status()==VcsJob::JobSucceeded && "TODO: provide a fall back in case of failing");
1106     Q_UNUSED(ret);
1107 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
1108     const QStringList commits = job->output().split(QLatin1Char('\n'), Qt::SkipEmptyParts);
1109 #else
1110     const QStringList commits = job->output().split(QLatin1Char('\n'), QString::SkipEmptyParts);
1111 #endif
1112 //     qCDebug(PLUGIN_GIT) << "\n\n\n commits" << commits << "\n\n\n";
1113     branchesShas.append(commits);
1114     for (const QString& branch : gitBranches) {
1115         if (branch == root)
1116             continue;
1117         QStringList args(branch);
1118         for (const QString& branch_arg : gitBranches) {
1119             if (branch_arg != branch)
1120                 //man gitRevList for '^'
1121                 args << QLatin1Char('^') + branch_arg;
1122         }
1123         QScopedPointer<DVcsJob> job(gitRevList(repo, args));
1124         bool ret = job->exec();
1125         Q_ASSERT(ret && job->status()==VcsJob::JobSucceeded && "TODO: provide a fall back in case of failing");
1126         Q_UNUSED(ret);
1127 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
1128         const QStringList commits = job->output().split(QLatin1Char('\n'), Qt::SkipEmptyParts);
1129 #else
1130         const QStringList commits = job->output().split(QLatin1Char('\n'), QString::SkipEmptyParts);
1131 #endif
1132 //         qCDebug(PLUGIN_GIT) << "\n\n\n commits" << commits << "\n\n\n";
1133         branchesShas.append(commits);
1134     }
1135 }
1136 
1137 //Actually we can just copy the output without parsing. So it's a kind of draft for future
parseLogOutput(const DVcsJob * job,QVector<DVcsEvent> & commits) const1138 void GitPlugin::parseLogOutput(const DVcsJob* job, QVector<DVcsEvent>& commits) const
1139 {
1140 //     static QRegExp rx_sep( "[-=]+" );
1141 //     static QRegExp rx_date( "date:\\s+([^;]*);\\s+author:\\s+([^;]*).*" );
1142 
1143     static QRegularExpression rx_com( QStringLiteral("commit \\w{1,40}") );
1144 
1145     const auto output = job->output();
1146 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
1147     const auto lines = output.splitRef(QLatin1Char('\n'), Qt::SkipEmptyParts);
1148 #else
1149     const auto lines = output.splitRef(QLatin1Char('\n'), QString::SkipEmptyParts);
1150 #endif
1151 
1152     DVcsEvent item;
1153     QString commitLog;
1154 
1155     for (int i=0; i<lines.count(); ++i) {
1156 //         qCDebug(PLUGIN_GIT) << "line:" << s;
1157         if (rx_com.match(lines[i]).hasMatch()) {
1158 //             qCDebug(PLUGIN_GIT) << "MATCH COMMIT";
1159             item.setCommit(lines[++i].toString());
1160             item.setAuthor(lines[++i].toString());
1161             item.setDate(lines[++i].toString());
1162             item.setLog(commitLog);
1163             commits.append(item);
1164         }
1165         else
1166         {
1167             //FIXME: add this in a loop to the if, like in getAllCommits()
1168             commitLog += lines[i].toString() + QLatin1Char('\n');
1169         }
1170     }
1171 }
1172 
actionsFromString(char c)1173 VcsItemEvent::Actions actionsFromString(char c)
1174 {
1175     switch(c) {
1176         case 'A': return VcsItemEvent::Added;
1177         case 'D': return VcsItemEvent::Deleted;
1178         case 'R': return VcsItemEvent::Replaced;
1179         case 'M': return VcsItemEvent::Modified;
1180     }
1181     return VcsItemEvent::Modified;
1182 }
1183 
parseGitLogOutput(DVcsJob * job)1184 void GitPlugin::parseGitLogOutput(DVcsJob * job)
1185 {
1186     static QRegExp commitRegex(QStringLiteral("^commit (\\w{8})\\w{32}"));
1187     static QRegExp infoRegex(QStringLiteral("^(\\w+):(.*)"));
1188     static QRegExp modificationsRegex(QStringLiteral("^([A-Z])[0-9]*\t([^\t]+)\t?(.*)"), Qt::CaseSensitive, QRegExp::RegExp2);
1189     //R099    plugins/git/kdevgit.desktop     plugins/git/kdevgit.desktop.cmake
1190     //M       plugins/grepview/CMakeLists.txt
1191 
1192     QList<QVariant> commits;
1193 
1194     QString contents = job->output();
1195     // check if git-log returned anything
1196     if (contents.isEmpty()) {
1197         job->setResults(commits); // empty list
1198         return;
1199     }
1200 
1201     // start parsing the output
1202     QTextStream s(&contents);
1203 
1204     VcsEvent item;
1205     QString message;
1206     bool pushCommit = false;
1207 
1208     while (!s.atEnd()) {
1209         QString line = s.readLine();
1210 
1211         if (commitRegex.exactMatch(line)) {
1212             if (pushCommit) {
1213                 item.setMessage(message.trimmed());
1214                 commits.append(QVariant::fromValue(item));
1215                 item.setItems(QList<VcsItemEvent>());
1216             } else {
1217                 pushCommit = true;
1218             }
1219             VcsRevision rev;
1220             rev.setRevisionValue(commitRegex.cap(1), KDevelop::VcsRevision::GlobalNumber);
1221             item.setRevision(rev);
1222             message.clear();
1223         } else if (infoRegex.exactMatch(line)) {
1224             QString cap1 = infoRegex.cap(1);
1225             if (cap1 == QLatin1String("Author")) {
1226                 item.setAuthor(infoRegex.cap(2).trimmed());
1227             } else if (cap1 == QLatin1String("Date")) {
1228                 item.setDate(QDateTime::fromSecsSinceEpoch(infoRegex.cap(2).trimmed().split(QLatin1Char(' '))[0].toUInt(), Qt::LocalTime));
1229             }
1230         } else if (modificationsRegex.exactMatch(line)) {
1231             VcsItemEvent::Actions a = actionsFromString(modificationsRegex.cap(1).at(0).toLatin1());
1232             QString filenameA = modificationsRegex.cap(2);
1233 
1234             VcsItemEvent itemEvent;
1235             itemEvent.setActions(a);
1236             itemEvent.setRepositoryLocation(filenameA);
1237             if(a==VcsItemEvent::Replaced) {
1238                 QString filenameB = modificationsRegex.cap(3);
1239                 itemEvent.setRepositoryCopySourceLocation(filenameB);
1240             }
1241 
1242             item.addItem(itemEvent);
1243         } else if (line.startsWith(QLatin1String("    "))) {
1244             message += line.midRef(4) + QLatin1Char('\n');
1245         }
1246     }
1247 
1248     item.setMessage(message.trimmed());
1249     commits.append(QVariant::fromValue(item));
1250     job->setResults(commits);
1251 }
1252 
parseGitDiffOutput(DVcsJob * job)1253 void GitPlugin::parseGitDiffOutput(DVcsJob* job)
1254 {
1255     VcsDiff diff;
1256     diff.setDiff(job->output());
1257     diff.setBaseDiff(repositoryRoot(QUrl::fromLocalFile(job->directory().absolutePath())));
1258     diff.setDepth(usePrefix()? 1 : 0);
1259 
1260     job->setResults(QVariant::fromValue(diff));
1261 }
1262 
lsfilesToState(char id)1263 static VcsStatusInfo::State lsfilesToState(char id)
1264 {
1265     switch(id) {
1266         case 'H': return VcsStatusInfo::ItemUpToDate; //Cached
1267         case 'S': return VcsStatusInfo::ItemUpToDate; //Skip work tree
1268         case 'M': return VcsStatusInfo::ItemHasConflicts; //unmerged
1269         case 'R': return VcsStatusInfo::ItemDeleted; //removed/deleted
1270         case 'C': return VcsStatusInfo::ItemModified; //modified/changed
1271         case 'K': return VcsStatusInfo::ItemDeleted; //to be killed
1272         case '?': return VcsStatusInfo::ItemUnknown; //other
1273     }
1274     Q_ASSERT(false);
1275     return VcsStatusInfo::ItemUnknown;
1276 }
1277 
parseGitStatusOutput_old(DVcsJob * job)1278 void GitPlugin::parseGitStatusOutput_old(DVcsJob* job)
1279 {
1280     const QString output = job->output();
1281 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
1282     const auto outputLines = output.splitRef(QLatin1Char('\n'), Qt::SkipEmptyParts);
1283 #else
1284     const auto outputLines = output.splitRef(QLatin1Char('\n'), QString::SkipEmptyParts);
1285 #endif
1286 
1287     QDir dir = job->directory();
1288     QMap<QUrl, VcsStatusInfo::State> allStatus;
1289     for (const auto& line : outputLines) {
1290         VcsStatusInfo::State status = lsfilesToState(line[0].toLatin1());
1291 
1292         QUrl url = QUrl::fromLocalFile(dir.absoluteFilePath(line.mid(2).toString()));
1293 
1294         allStatus[url] = status;
1295     }
1296 
1297     QVariantList statuses;
1298     statuses.reserve(allStatus.size());
1299     QMap< QUrl, VcsStatusInfo::State >::const_iterator it = allStatus.constBegin(), itEnd=allStatus.constEnd();
1300     for(; it!=itEnd; ++it) {
1301 
1302         VcsStatusInfo status;
1303         status.setUrl(it.key());
1304         status.setState(it.value());
1305 
1306         statuses.append(QVariant::fromValue<VcsStatusInfo>(status));
1307     }
1308 
1309     job->setResults(statuses);
1310 }
1311 
parseGitStatusOutput(DVcsJob * job)1312 void GitPlugin::parseGitStatusOutput(DVcsJob* job)
1313 {
1314     const auto output = job->output();
1315 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
1316     const auto outputLines = output.splitRef(QLatin1Char('\n'), Qt::SkipEmptyParts);
1317 #else
1318     const auto outputLines = output.splitRef(QLatin1Char('\n'), QString::SkipEmptyParts);
1319 #endif
1320     QDir workingDir = job->directory();
1321     QDir dotGit = dotGitDirectory(QUrl::fromLocalFile(workingDir.absolutePath()));
1322 
1323     QVariantList statuses;
1324     QList<QUrl> processedFiles;
1325 
1326     for (const QStringRef& line : outputLines) {
1327         //every line is 2 chars for the status, 1 space then the file desc
1328         QStringRef curr=line.mid(3);
1329         QStringRef state = line.left(2);
1330 
1331         int arrow = curr.indexOf(QLatin1String(" -> "));
1332         if(arrow>=0) {
1333             VcsStatusInfo status;
1334             status.setUrl(QUrl::fromLocalFile(dotGit.absoluteFilePath(curr.toString().left(arrow))));
1335             status.setState(VcsStatusInfo::ItemDeleted);
1336             statuses.append(QVariant::fromValue<VcsStatusInfo>(status));
1337             processedFiles += status.url();
1338 
1339             curr = curr.mid(arrow+4);
1340         }
1341 
1342         if (curr.startsWith(QLatin1Char('\"')) && curr.endsWith(QLatin1Char('\"'))) { //if the path is quoted, unquote
1343             curr = curr.mid(1, curr.size()-2);
1344         }
1345 
1346         VcsStatusInfo status;
1347         ExtendedState ex_state = parseGitState(state);
1348         status.setUrl(QUrl::fromLocalFile(dotGit.absoluteFilePath(curr.toString())));
1349         status.setExtendedState(ex_state);
1350         status.setState(extendedStateToBasic(ex_state));
1351         processedFiles.append(status.url());
1352 
1353         qCDebug(PLUGIN_GIT) << "Checking git status for " << line << curr << status.state();
1354 
1355         statuses.append(QVariant::fromValue<VcsStatusInfo>(status));
1356     }
1357     QStringList paths;
1358     QStringList oldcmd=job->dvcsCommand();
1359     QStringList::const_iterator it=oldcmd.constBegin()+oldcmd.indexOf(QStringLiteral("--"))+1, itEnd=oldcmd.constEnd();
1360     paths.reserve(oldcmd.size());
1361     for(; it!=itEnd; ++it)
1362         paths += *it;
1363 
1364     //here we add the already up to date files
1365     const QStringList files = getLsFiles(job->directory(), QStringList{QStringLiteral("-c"), QStringLiteral("--")} << paths, OutputJob::Silent);
1366     for (const QString& file : files) {
1367         QUrl fileUrl = QUrl::fromLocalFile(workingDir.absoluteFilePath(file));
1368 
1369         if(!processedFiles.contains(fileUrl)) {
1370             VcsStatusInfo status;
1371             status.setUrl(fileUrl);
1372             status.setState(VcsStatusInfo::ItemUpToDate);
1373 
1374             statuses.append(QVariant::fromValue<VcsStatusInfo>(status));
1375         }
1376     }
1377     job->setResults(statuses);
1378 }
1379 
parseGitVersionOutput(DVcsJob * job)1380 void GitPlugin::parseGitVersionOutput(DVcsJob* job)
1381 {
1382     const auto output = job->output().trimmed();
1383     auto versionString = output.midRef(output.lastIndexOf(QLatin1Char(' ')));
1384     const auto minimumVersion = QVersionNumber(1, 7);
1385     const auto actualVersion = QVersionNumber::fromString(versionString);
1386     m_oldVersion = actualVersion < minimumVersion;
1387     qCDebug(PLUGIN_GIT) << "checking git version" << versionString << actualVersion << "against" << minimumVersion
1388                         << m_oldVersion;
1389 }
1390 
getLsFiles(const QDir & directory,const QStringList & args,KDevelop::OutputJob::OutputJobVerbosity verbosity)1391 QStringList GitPlugin::getLsFiles(const QDir &directory, const QStringList &args,
1392     KDevelop::OutputJob::OutputJobVerbosity verbosity)
1393 {
1394     QScopedPointer<DVcsJob> job(lsFiles(directory, args, verbosity));
1395     if (job->exec() && job->status() == KDevelop::VcsJob::JobSucceeded)
1396 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
1397         return job->output().split(QLatin1Char('\n'), Qt::SkipEmptyParts);
1398 #else
1399         return job->output().split(QLatin1Char('\n'), QString::SkipEmptyParts);
1400 #endif
1401 
1402     return QStringList();
1403 }
1404 
gitRevParse(const QString & repository,const QStringList & args,KDevelop::OutputJob::OutputJobVerbosity verbosity)1405 DVcsJob* GitPlugin::gitRevParse(const QString &repository, const QStringList &args,
1406     KDevelop::OutputJob::OutputJobVerbosity verbosity)
1407 {
1408     auto* job = new GitJob(QDir(repository), this, verbosity);
1409     *job << "git" << "rev-parse" << args;
1410 
1411     return job;
1412 }
1413 
gitRevList(const QString & directory,const QStringList & args)1414 DVcsJob* GitPlugin::gitRevList(const QString& directory, const QStringList& args)
1415 {
1416     auto* job = new GitJob(urlDir(QUrl::fromLocalFile(directory)), this, KDevelop::OutputJob::Silent);
1417     {
1418         *job << "git" << "rev-list" << args;
1419         return job;
1420     }
1421 }
1422 
_pair(char a,char b)1423 constexpr int _pair(char a, char b) { return a*256 + b;}
1424 
parseGitState(const QStringRef & msg)1425 GitPlugin::ExtendedState GitPlugin::parseGitState(const QStringRef& msg)
1426 {
1427     Q_ASSERT(msg.size()==1 || msg.size()==2);
1428     ExtendedState ret = GitInvalid;
1429 
1430     if(msg.contains(QLatin1Char('U')) || msg == QLatin1String("AA") || msg == QLatin1String("DD"))
1431         ret = GitConflicts;
1432     else switch(_pair(msg.at(0).toLatin1(), msg.at(1).toLatin1()))
1433     {
1434         case _pair(' ', ' '):
1435             ret = GitXX;
1436             break;
1437         case _pair(' ','M'):
1438             ret = GitXM;
1439             break;
1440         case _pair ( ' ','D' ) :
1441             ret = GitXD;
1442             break;
1443         case _pair ( ' ','R' ) :
1444             ret = GitXR;
1445             break;
1446         case _pair ( ' ','C' ) :
1447             ret = GitXC;
1448             break;
1449         case _pair ( 'M',' ' ) :
1450             ret = GitMX;
1451             break;
1452         case _pair ( 'M','M' ) :
1453             ret = GitMM;
1454             break;
1455         case _pair ( 'M','D' ) :
1456             ret = GitMD;
1457             break;
1458         case _pair ( 'A',' ' ) :
1459             ret = GitAX;
1460             break;
1461         case _pair ( 'A','M' ) :
1462             ret = GitAM;
1463             break;
1464         case _pair ( 'A','D' ) :
1465             ret = GitAD;
1466             break;
1467         case _pair ( 'D',' ' ) :
1468             ret = GitDX;
1469             break;
1470         case _pair ( 'D','R' ) :
1471             ret = GitDR;
1472             break;
1473         case _pair ( 'D','C' ) :
1474             ret = GitDC;
1475             break;
1476         case _pair ( 'R',' ' ) :
1477             ret = GitRX;
1478             break;
1479         case _pair ( 'R','M' ) :
1480             ret = GitRM;
1481             break;
1482         case _pair ( 'R','D' ) :
1483             ret = GitRD;
1484             break;
1485         case _pair ( 'C',' ' ) :
1486             ret = GitCX;
1487             break;
1488         case _pair ( 'C','M' ) :
1489             ret = GitCM;
1490             break;
1491         case _pair ( 'C','D' ) :
1492             ret = GitCD;
1493             break;
1494         case _pair ( '?','?' ) :
1495             ret = GitUntracked;
1496             break;
1497         default:
1498             qCDebug(PLUGIN_GIT) << "Git status not identified:" << msg;
1499             ret = GitInvalid;
1500             break;
1501     }
1502 
1503     return ret;
1504 }
1505 
extendedStateToBasic(const GitPlugin::ExtendedState state)1506 KDevelop::VcsStatusInfo::State GitPlugin::extendedStateToBasic(const GitPlugin::ExtendedState state)
1507 {
1508     switch(state) {
1509         case GitXX: return VcsStatusInfo::ItemUpToDate;
1510         case GitXM: return VcsStatusInfo::ItemModified;
1511         case GitXD: return VcsStatusInfo::ItemDeleted;
1512         case GitXR: return VcsStatusInfo::ItemModified;
1513         case GitXC: return VcsStatusInfo::ItemModified;
1514         case GitMX: return VcsStatusInfo::ItemModified;
1515         case GitMM: return VcsStatusInfo::ItemModified;
1516         case GitMD: return VcsStatusInfo::ItemDeleted;
1517         case GitAX: return VcsStatusInfo::ItemAdded;
1518         case GitAM: return VcsStatusInfo::ItemAdded;
1519         case GitAD: return VcsStatusInfo::ItemAdded;
1520         case GitDX: return VcsStatusInfo::ItemDeleted;
1521         case GitDR: return VcsStatusInfo::ItemDeleted;
1522         case GitDC: return VcsStatusInfo::ItemDeleted;
1523         case GitRX: return VcsStatusInfo::ItemModified;
1524         case GitRM: return VcsStatusInfo::ItemModified;
1525         case GitRD: return VcsStatusInfo::ItemDeleted;
1526         case GitCX: return VcsStatusInfo::ItemModified;
1527         case GitCM: return VcsStatusInfo::ItemModified;
1528         case GitCD: return VcsStatusInfo::ItemDeleted;
1529         case GitUntracked: return VcsStatusInfo::ItemUnknown;
1530         case GitConflicts: return VcsStatusInfo::ItemHasConflicts;
1531         case GitInvalid: return VcsStatusInfo::ItemUnknown;
1532     }
1533     return VcsStatusInfo::ItemUnknown;
1534 }
1535 
1536 
StandardJob(IPlugin * parent,KJob * job,OutputJob::OutputJobVerbosity verbosity)1537 StandardJob::StandardJob(IPlugin* parent, KJob* job,
1538                                  OutputJob::OutputJobVerbosity verbosity)
1539     : VcsJob(parent, verbosity)
1540     , m_job(job)
1541     , m_plugin(parent)
1542     , m_status(JobNotStarted)
1543 {}
1544 
start()1545 void StandardJob::start()
1546 {
1547     connect(m_job, &KJob::result, this, &StandardJob::result);
1548     m_job->start();
1549     m_status=JobRunning;
1550 }
1551 
result(KJob * job)1552 void StandardJob::result(KJob* job)
1553 {
1554     if (job->error() == 0) {
1555         m_status = JobSucceeded;
1556         setError(NoError);
1557     } else {
1558         m_status = JobFailed;
1559         setError(UserDefinedError);
1560     }
1561     emitResult();
1562 }
1563 
copy(const QUrl & localLocationSrc,const QUrl & localLocationDstn)1564 VcsJob* GitPlugin::copy(const QUrl& localLocationSrc, const QUrl& localLocationDstn)
1565 {
1566     //TODO: Probably we should "git add" after
1567     return new StandardJob(this, KIO::copy(localLocationSrc, localLocationDstn), KDevelop::OutputJob::Silent);
1568 }
1569 
move(const QUrl & source,const QUrl & destination)1570 VcsJob* GitPlugin::move(const QUrl& source, const QUrl& destination)
1571 {
1572     QDir dir = urlDir(source);
1573 
1574     QFileInfo fileInfo(source.toLocalFile());
1575     if (fileInfo.isDir()) {
1576         if (isEmptyDirStructure(QDir(source.toLocalFile()))) {
1577             //move empty folder, git doesn't do that
1578             qCDebug(PLUGIN_GIT) << "empty folder" << source;
1579             return new StandardJob(this, KIO::move(source, destination), KDevelop::OutputJob::Silent);
1580         }
1581     }
1582 
1583     const QStringList otherStr = getLsFiles(dir, QStringList{QStringLiteral("--others"), QStringLiteral("--"), source.toLocalFile()}, KDevelop::OutputJob::Silent);
1584     if(otherStr.isEmpty()) {
1585         auto* job = new GitJob(dir, this, KDevelop::OutputJob::Verbose);
1586         *job << "git" << "mv" << source.toLocalFile() << destination.toLocalFile();
1587         return job;
1588     } else {
1589         return new StandardJob(this, KIO::move(source, destination), KDevelop::OutputJob::Silent);
1590     }
1591 }
1592 
parseGitRepoLocationOutput(DVcsJob * job)1593 void GitPlugin::parseGitRepoLocationOutput(DVcsJob* job)
1594 {
1595     job->setResults(QVariant::fromValue(QUrl::fromLocalFile(job->output())));
1596 }
1597 
repositoryLocation(const QUrl & localLocation)1598 VcsJob* GitPlugin::repositoryLocation(const QUrl& localLocation)
1599 {
1600     auto* job = new GitJob(urlDir(localLocation), this);
1601     //Probably we should check first if origin is the proper remote we have to use but as a first attempt it works
1602     *job << "git" << "config" << "remote.origin.url";
1603     connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitRepoLocationOutput);
1604     return job;
1605 }
1606 
pull(const KDevelop::VcsLocation & localOrRepoLocationSrc,const QUrl & localRepositoryLocation)1607 VcsJob* GitPlugin::pull(const KDevelop::VcsLocation& localOrRepoLocationSrc, const QUrl& localRepositoryLocation)
1608 {
1609     auto* job = new GitJob(urlDir(localRepositoryLocation), this);
1610     job->setCommunicationMode(KProcess::MergedChannels);
1611     *job << "git" << "pull";
1612     if(!localOrRepoLocationSrc.localUrl().isEmpty())
1613         *job << localOrRepoLocationSrc.localUrl().url();
1614     return job;
1615 }
1616 
push(const QUrl & localRepositoryLocation,const KDevelop::VcsLocation & localOrRepoLocationDst)1617 VcsJob* GitPlugin::push(const QUrl& localRepositoryLocation, const KDevelop::VcsLocation& localOrRepoLocationDst)
1618 {
1619     auto* job = new GitJob(urlDir(localRepositoryLocation), this);
1620     job->setCommunicationMode(KProcess::MergedChannels);
1621     *job << "git" << "push";
1622     if(!localOrRepoLocationDst.localUrl().isEmpty())
1623         *job << localOrRepoLocationDst.localUrl().url();
1624     return job;
1625 }
1626 
resolve(const QList<QUrl> & localLocations,IBasicVersionControl::RecursionMode recursion)1627 VcsJob* GitPlugin::resolve(const QList<QUrl>& localLocations, IBasicVersionControl::RecursionMode recursion)
1628 {
1629     return add(localLocations, recursion);
1630 }
1631 
update(const QList<QUrl> & localLocations,const KDevelop::VcsRevision & rev,IBasicVersionControl::RecursionMode recursion)1632 VcsJob* GitPlugin::update(const QList<QUrl>& localLocations, const KDevelop::VcsRevision& rev, IBasicVersionControl::RecursionMode recursion)
1633 {
1634     if(rev.revisionType()==VcsRevision::Special && rev.revisionValue().value<VcsRevision::RevisionSpecialType>()==VcsRevision::Head) {
1635         return pull(VcsLocation(), localLocations.first());
1636     } else {
1637         auto* job = new GitJob(urlDir(localLocations.first()), this);
1638         {
1639             //Probably we should check first if origin is the proper remote we have to use but as a first attempt it works
1640             *job << "git" << "checkout" << rev.revisionValue().toString() << "--";
1641             *job << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations));
1642             return job;
1643         }
1644     }
1645 }
1646 
setupCommitMessageEditor(const QUrl & localLocation,KTextEdit * editor) const1647 void GitPlugin::setupCommitMessageEditor(const QUrl& localLocation, KTextEdit* editor) const
1648 {
1649     new GitMessageHighlighter(editor);
1650     QFile mergeMsgFile(dotGitDirectory(localLocation).filePath(QStringLiteral(".git/MERGE_MSG")));
1651     // Some limit on the file size should be set since whole content is going to be read into
1652     // the memory. 1Mb seems to be good value since it's rather strange to have so huge commit
1653     // message.
1654     static const qint64 maxMergeMsgFileSize = 1024*1024;
1655     if (mergeMsgFile.size() > maxMergeMsgFileSize || !mergeMsgFile.open(QIODevice::ReadOnly))
1656         return;
1657 
1658     QString mergeMsg = QString::fromLocal8Bit(mergeMsgFile.read(maxMergeMsgFileSize));
1659     editor->setPlainText(mergeMsg);
1660 }
1661 
1662 class GitVcsLocationWidget : public KDevelop::StandardVcsLocationWidget
1663 {
1664     Q_OBJECT
1665     public:
GitVcsLocationWidget(QWidget * parent=nullptr)1666         explicit GitVcsLocationWidget(QWidget* parent = nullptr)
1667             : StandardVcsLocationWidget(parent)
1668         {}
1669 
isCorrect() const1670         bool isCorrect() const override
1671         {
1672             return !url().isEmpty();
1673         }
1674 };
1675 
vcsLocation(QWidget * parent) const1676 KDevelop::VcsLocationWidget* GitPlugin::vcsLocation(QWidget* parent) const
1677 {
1678     return new GitVcsLocationWidget(parent);
1679 }
1680 
registerRepositoryForCurrentBranchChanges(const QUrl & repository)1681 void GitPlugin::registerRepositoryForCurrentBranchChanges(const QUrl& repository)
1682 {
1683     QDir dir = dotGitDirectory(repository);
1684     QString headFile = dir.absoluteFilePath(QStringLiteral(".git/HEAD"));
1685     m_watcher->addFile(headFile);
1686 }
1687 
fileChanged(const QString & file)1688 void GitPlugin::fileChanged(const QString& file)
1689 {
1690     Q_ASSERT(file.endsWith(QLatin1String("HEAD")));
1691     //SMTH/.git/HEAD -> SMTH/
1692     const QUrl fileUrl = Path(file).parent().parent().toUrl();
1693 
1694     //We need to delay the emitted signal, otherwise the branch hasn't change yet
1695     //and the repository is not functional
1696     m_branchesChange.append(fileUrl);
1697     QTimer::singleShot(1000, this, &GitPlugin::delayedBranchChanged);
1698 }
1699 
delayedBranchChanged()1700 void GitPlugin::delayedBranchChanged()
1701 {
1702     emit repositoryBranchChanged(m_branchesChange.takeFirst());
1703 }
1704 
isInRepository(KTextEditor::Document * document)1705 CheckInRepositoryJob* GitPlugin::isInRepository(KTextEditor::Document* document)
1706 {
1707     CheckInRepositoryJob* job = new GitPluginCheckInRepositoryJob(document, repositoryRoot(document->url()).path());
1708     job->start();
1709     return job;
1710 }
1711 
setConfigOption(const QUrl & repository,const QString & key,const QString & value,bool global)1712 DVcsJob* GitPlugin::setConfigOption(const QUrl& repository, const QString& key, const QString& value, bool global)
1713 {
1714     auto job = new GitJob(urlDir(repository), this);
1715     QStringList args;
1716     args << QStringLiteral("git") << QStringLiteral("config");
1717     if(global)
1718         args << QStringLiteral("--global");
1719     args << key << value;
1720     *job << args;
1721     return job;
1722 }
1723 
readConfigOption(const QUrl & repository,const QString & key)1724 QString GitPlugin::readConfigOption(const QUrl& repository, const QString& key)
1725 {
1726     QProcess exec;
1727     exec.setWorkingDirectory(urlDir(repository).absolutePath());
1728     exec.start(QStringLiteral("git"), QStringList{QStringLiteral("config"), QStringLiteral("--get"), key});
1729     exec.waitForFinished();
1730     return QString::fromUtf8(exec.readAllStandardOutput().trimmed());
1731 }
1732 
1733 #include "gitplugin.moc"
1734