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