File indexing completed on 2024-04-28 04:38:50
0001 /* 0002 SPDX-FileCopyrightText: 2008 Evgeniy Ivanov <powerfox@kde.ru> 0003 SPDX-FileCopyrightText: 2009 Hugo Parente Lima <hugo.pl@gmail.com> 0004 SPDX-FileCopyrightText: 2010 Aleix Pol Gonzalez <aleixpol@kde.org> 0005 0006 SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0007 */ 0008 0009 #include "gitplugin.h" 0010 0011 #include "repostatusmodel.h" 0012 #include "committoolview.h" 0013 0014 #include <QDateTime> 0015 #include <QProcess> 0016 #include <QDir> 0017 #include <QFileInfo> 0018 #include <QMenu> 0019 #include <QTimer> 0020 #include <QRegularExpression> 0021 #include <QPointer> 0022 #include <QTemporaryFile> 0023 #include <QVersionNumber> 0024 0025 #include <interfaces/icore.h> 0026 #include <interfaces/iproject.h> 0027 #include <interfaces/iruncontroller.h> 0028 #include <interfaces/iuicontroller.h> 0029 0030 #include <util/path.h> 0031 0032 #include <vcs/vcsjob.h> 0033 #include <vcs/vcsrevision.h> 0034 #include <vcs/vcsevent.h> 0035 #include <vcs/vcslocation.h> 0036 #include <vcs/dvcs/dvcsjob.h> 0037 #include <vcs/vcsannotation.h> 0038 #include <vcs/widgets/standardvcslocationwidget.h> 0039 #include "gitclonejob.h" 0040 #include "rebasedialog.h" 0041 #include "stashmanagerdialog.h" 0042 0043 #include <KDirWatch> 0044 #include <KIO/CopyJob> 0045 #include <KIO/DeleteJob> 0046 #include <KLocalizedString> 0047 #include <KMessageBox> 0048 #include <KMessageBox_KDevCompat> 0049 #include <KTextEdit> 0050 #include <KTextEditor/Document> 0051 0052 #include "gitjob.h" 0053 #include "gitmessagehighlighter.h" 0054 #include "gitplugincheckinrepositoryjob.h" 0055 #include "gitnameemaildialog.h" 0056 #include "debug.h" 0057 0058 #include <array> 0059 0060 using namespace KDevelop; 0061 0062 QVariant runSynchronously(KDevelop::VcsJob* job) 0063 { 0064 QVariant ret; 0065 if(job->exec() && job->status()==KDevelop::VcsJob::JobSucceeded) { 0066 ret = job->fetchResults(); 0067 } 0068 delete job; 0069 return ret; 0070 } 0071 0072 namespace 0073 { 0074 0075 QDir dotGitDirectory(const QUrl& dirPath, bool silent = false) 0076 { 0077 const QFileInfo finfo(dirPath.toLocalFile()); 0078 QDir dir = finfo.isDir() ? QDir(finfo.filePath()): finfo.absoluteDir(); 0079 0080 const QString gitDir = QStringLiteral(".git"); 0081 while (!dir.exists(gitDir) && dir.cdUp()) {} // cdUp, until there is a sub-directory called .git 0082 0083 if (!silent && dir.isRoot()) { 0084 qCWarning(PLUGIN_GIT) << "couldn't find the git root for" << dirPath; 0085 } 0086 0087 return dir; 0088 } 0089 0090 /** 0091 * Whenever a directory is provided, change it for all the files in it but not inner directories, 0092 * that way we make sure we won't get into recursion, 0093 */ 0094 static QList<QUrl> preventRecursion(const QList<QUrl>& urls) 0095 { 0096 QList<QUrl> ret; 0097 for (const QUrl& url : urls) { 0098 QDir d(url.toLocalFile()); 0099 if(d.exists()) { 0100 const QStringList entries = d.entryList(QDir::Files | QDir::NoDotAndDotDot); 0101 ret.reserve(ret.size() + entries.size()); 0102 for (const QString& entry : entries) { 0103 QUrl entryUrl = QUrl::fromLocalFile(d.absoluteFilePath(entry)); 0104 ret += entryUrl; 0105 } 0106 } else 0107 ret += url; 0108 } 0109 return ret; 0110 } 0111 0112 QString toRevisionName(const KDevelop::VcsRevision& rev, const QString& currentRevision=QString()) 0113 { 0114 switch(rev.revisionType()) { 0115 case VcsRevision::Special: 0116 switch(rev.revisionValue().value<VcsRevision::RevisionSpecialType>()) { 0117 case VcsRevision::Head: 0118 return QStringLiteral("^HEAD"); 0119 case VcsRevision::Base: 0120 return QString(); 0121 case VcsRevision::Working: 0122 return QString(); 0123 case VcsRevision::Previous: 0124 Q_ASSERT(!currentRevision.isEmpty()); 0125 return currentRevision + QLatin1String("^1"); 0126 case VcsRevision::Start: 0127 return QString(); 0128 case VcsRevision::UserSpecialType: //Not used 0129 Q_ASSERT(false && "i don't know how to do that"); 0130 } 0131 break; 0132 case VcsRevision::GlobalNumber: 0133 return rev.revisionValue().toString(); 0134 case VcsRevision::Date: 0135 case VcsRevision::FileNumber: 0136 case VcsRevision::Invalid: 0137 case VcsRevision::UserType: 0138 Q_ASSERT(false); 0139 } 0140 return QString(); 0141 } 0142 0143 QString revisionInterval(const KDevelop::VcsRevision& rev, const KDevelop::VcsRevision& limit) 0144 { 0145 QString ret; 0146 if(rev.revisionType()==VcsRevision::Special && 0147 rev.revisionValue().value<VcsRevision::RevisionSpecialType>()==VcsRevision::Start) //if we want it to the beginning just put the revisionInterval 0148 ret = toRevisionName(limit, QString()); 0149 else { 0150 QString dst = toRevisionName(limit); 0151 if(dst.isEmpty()) 0152 ret = dst; 0153 else { 0154 QString src = toRevisionName(rev, dst); 0155 if(src.isEmpty()) 0156 ret = src; 0157 else 0158 ret = src + QLatin1String("..") + dst; 0159 } 0160 } 0161 return ret; 0162 } 0163 0164 QDir urlDir(const QUrl& url) 0165 { 0166 QFileInfo f(url.toLocalFile()); 0167 if(f.isDir()) 0168 return QDir(url.toLocalFile()); 0169 else 0170 return f.absoluteDir(); 0171 } 0172 QDir urlDir(const QList<QUrl>& urls) { return urlDir(urls.first()); } //TODO: could be improved 0173 0174 } 0175 0176 GitPlugin::GitPlugin(QObject* parent, const QVariantList&) 0177 : DistributedVersionControlPlugin(parent, QStringLiteral("kdevgit")) 0178 , m_repoStatusModel(new RepoStatusModel(this)) 0179 , m_commitToolViewFactory(new CommitToolViewFactory(m_repoStatusModel)) 0180 { 0181 if (QStandardPaths::findExecutable(QStringLiteral("git")).isEmpty()) { 0182 setErrorDescription(i18n("Unable to find git executable. Is it installed on the system?")); 0183 return; 0184 } 0185 0186 // FIXME: Is this needed (I don't quite understand the comment 0187 // in vcsstatusinfo.h which says we need to do this if we want to 0188 // use VcsStatusInfo in queued signals/slots) 0189 qRegisterMetaType<VcsStatusInfo>(); 0190 0191 ICore::self()->uiController()->addToolView(i18n("Git Commit"), m_commitToolViewFactory); 0192 0193 setObjectName(QStringLiteral("Git")); 0194 0195 auto* versionJob = new GitJob(QDir::tempPath(), this, KDevelop::OutputJob::Silent); 0196 *versionJob << "git" << "--version"; 0197 connect(versionJob, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitVersionOutput); 0198 ICore::self()->runController()->registerJob(versionJob); 0199 0200 m_watcher = new KDirWatch(this); 0201 connect(m_watcher, &KDirWatch::dirty, this, &GitPlugin::fileChanged); 0202 connect(m_watcher, &KDirWatch::created, this, &GitPlugin::fileChanged); 0203 } 0204 0205 GitPlugin::~GitPlugin() 0206 {} 0207 0208 bool emptyOutput(DVcsJob* job) 0209 { 0210 QScopedPointer<DVcsJob> _job(job); 0211 if(job->exec() && job->status()==VcsJob::JobSucceeded) 0212 return job->rawOutput().trimmed().isEmpty(); 0213 0214 return false; 0215 } 0216 0217 bool GitPlugin::hasStashes(const QDir& repository) 0218 { 0219 if (auto *job = qobject_cast<DVcsJob*>(gitStash(repository, QStringList(QStringLiteral("list")), KDevelop::OutputJob::Silent))) { 0220 return !emptyOutput(job); 0221 } 0222 Q_ASSERT(false); // gitStash should always return a DVcsJob ! 0223 return false; 0224 } 0225 0226 bool GitPlugin::hasModifications(const QDir& d) 0227 { 0228 return !emptyOutput(lsFiles(d, QStringList(QStringLiteral("-m")), OutputJob::Silent)); 0229 } 0230 0231 bool GitPlugin::hasModifications(const QDir& repo, const QUrl& file) 0232 { 0233 return !emptyOutput(lsFiles(repo, QStringList{QStringLiteral("-m"), file.path()}, OutputJob::Silent)); 0234 } 0235 0236 void GitPlugin::additionalMenuEntries(QMenu* menu, const QList<QUrl>& urls) 0237 { 0238 m_urls = urls; 0239 0240 QDir dir=urlDir(urls); 0241 bool hasSt = hasStashes(dir); 0242 0243 menu->addAction(i18nc("@action:inmenu", "Rebase"), this, SLOT(ctxRebase())); 0244 menu->addSeparator()->setText(i18nc("@title:menu", "Git Stashes")); 0245 menu->addAction(i18nc("@action:inmenu", "Stash Manager"), this, SLOT(ctxStashManager()))->setEnabled(hasSt); 0246 menu->addAction(QIcon::fromTheme(QStringLiteral("vcs-stash")), i18nc("@action:inmenu", "Push Stash"), this, SLOT(ctxPushStash())); 0247 menu->addAction(QIcon::fromTheme(QStringLiteral("vcs-stash-pop")), i18nc("@action:inmenu", "Pop Stash"), this, SLOT(ctxPopStash()))->setEnabled(hasSt); 0248 } 0249 0250 void GitPlugin::ctxRebase() 0251 { 0252 auto* dialog = new RebaseDialog(this, m_urls.first(), nullptr); 0253 dialog->setAttribute(Qt::WA_DeleteOnClose); 0254 dialog->open(); 0255 } 0256 0257 void GitPlugin::ctxPushStash() 0258 { 0259 VcsJob* job = gitStash(urlDir(m_urls), QStringList(), KDevelop::OutputJob::Verbose); 0260 ICore::self()->runController()->registerJob(job); 0261 } 0262 0263 void GitPlugin::ctxPopStash() 0264 { 0265 VcsJob* job = gitStash(urlDir(m_urls), QStringList(QStringLiteral("pop")), KDevelop::OutputJob::Verbose); 0266 ICore::self()->runController()->registerJob(job); 0267 } 0268 0269 void GitPlugin::ctxStashManager() 0270 { 0271 QPointer<StashManagerDialog> d = new StashManagerDialog(urlDir(m_urls), this, nullptr); 0272 d->exec(); 0273 0274 delete d; 0275 } 0276 0277 DVcsJob* GitPlugin::errorsFound(const QString& error, KDevelop::OutputJob::OutputJobVerbosity verbosity=OutputJob::Verbose) 0278 { 0279 auto* j = new GitJob(QDir::temp(), this, verbosity); 0280 *j << "echo" << i18n("error: %1", error) << "-n"; 0281 return j; 0282 } 0283 0284 QString GitPlugin::name() const 0285 { 0286 return QStringLiteral("Git"); 0287 } 0288 0289 QUrl GitPlugin::repositoryRoot(const QUrl& path) 0290 { 0291 return QUrl::fromLocalFile(dotGitDirectory(path).absolutePath()); 0292 } 0293 0294 bool GitPlugin::isValidDirectory(const QUrl & dirPath) 0295 { 0296 QDir dir = dotGitDirectory(dirPath, true); 0297 QFile dotGitPotentialFile(dir.filePath(QStringLiteral(".git"))); 0298 // if .git is a file, we may be in a git worktree 0299 QFileInfo dotGitPotentialFileInfo(dotGitPotentialFile); 0300 if (!dotGitPotentialFileInfo.isDir() && dotGitPotentialFile.exists()) { 0301 QString gitWorktreeFileContent; 0302 if (dotGitPotentialFile.open(QFile::ReadOnly)) { 0303 // the content should be gitdir: /path/to/the/.git/worktree 0304 gitWorktreeFileContent = QString::fromUtf8(dotGitPotentialFile.readAll()); 0305 dotGitPotentialFile.close(); 0306 } else { 0307 return false; 0308 } 0309 const auto items = gitWorktreeFileContent.split(QLatin1Char(' ')); 0310 if (items.size() == 2 && items.at(0) == QLatin1String("gitdir:")) { 0311 qCDebug(PLUGIN_GIT) << "we are in a git worktree" << items.at(1); 0312 return true; 0313 } 0314 } 0315 return dir.exists(QStringLiteral(".git/HEAD")); 0316 } 0317 0318 bool GitPlugin::isValidRemoteRepositoryUrl(const QUrl& remoteLocation) 0319 { 0320 if (remoteLocation.isLocalFile()) { 0321 QFileInfo fileInfo(remoteLocation.toLocalFile()); 0322 if (fileInfo.isDir()) { 0323 QDir dir(fileInfo.filePath()); 0324 if (dir.exists(QStringLiteral(".git/HEAD"))) { 0325 return true; 0326 } 0327 // TODO: check also for bare repo 0328 } 0329 } else { 0330 const QString scheme = remoteLocation.scheme(); 0331 if (scheme == QLatin1String("git") || scheme == QLatin1String("git+ssh")) { 0332 return true; 0333 } 0334 // heuristic check, anything better we can do here without talking to server? 0335 if ((scheme == QLatin1String("http") || 0336 scheme == QLatin1String("https")) && 0337 remoteLocation.path().endsWith(QLatin1String(".git"))) { 0338 return true; 0339 } 0340 } 0341 return false; 0342 } 0343 0344 bool GitPlugin::isVersionControlled(const QUrl &path) 0345 { 0346 QFileInfo fsObject(path.toLocalFile()); 0347 if (!fsObject.exists()) { 0348 return false; 0349 } 0350 if (fsObject.isDir()) { 0351 return isValidDirectory(path); 0352 } 0353 0354 QString filename = fsObject.fileName(); 0355 0356 QStringList otherFiles = getLsFiles(fsObject.dir(), QStringList(QStringLiteral("--")) << filename, KDevelop::OutputJob::Silent); 0357 return !otherFiles.empty(); 0358 } 0359 0360 VcsJob* GitPlugin::init(const QUrl &directory) 0361 { 0362 auto* job = new GitJob(urlDir(directory), this); 0363 job->setType(VcsJob::Import); 0364 *job << "git" << "init"; 0365 return job; 0366 } 0367 0368 VcsJob* GitPlugin::createWorkingCopy(const KDevelop::VcsLocation & source, const QUrl& dest, KDevelop::IBasicVersionControl::RecursionMode) 0369 { 0370 DVcsJob* job = new GitCloneJob(urlDir(dest), this); 0371 job->setType(VcsJob::Import); 0372 *job << "git" << "clone" << "--progress" << "--" << source.localUrl().url() << dest; 0373 return job; 0374 } 0375 0376 VcsJob* GitPlugin::add(const QList<QUrl>& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) 0377 { 0378 if (localLocations.empty()) 0379 return errorsFound(i18n("Did not specify the list of files"), OutputJob::Verbose); 0380 0381 DVcsJob* job = new GitJob(dotGitDirectory(localLocations.front()), this); 0382 job->setType(VcsJob::Add); 0383 *job << "git" << "add" << "--" << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations)); 0384 return job; 0385 } 0386 0387 KDevelop::VcsJob* GitPlugin::status(const QList<QUrl>& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) 0388 { 0389 if (localLocations.empty()) 0390 return errorsFound(i18n("Did not specify the list of files"), OutputJob::Verbose); 0391 0392 DVcsJob* job = new GitJob(urlDir(localLocations), this, OutputJob::Silent); 0393 job->setType(VcsJob::Status); 0394 0395 if(m_oldVersion) { 0396 *job << "git" << "ls-files" << "-t" << "-m" << "-c" << "-o" << "-d" << "-k" << "--directory"; 0397 connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitStatusOutput_old); 0398 } else { 0399 *job << "git" << "status" << "--porcelain"; 0400 job->setIgnoreError(true); 0401 connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitStatusOutput); 0402 } 0403 *job << "--" << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations)); 0404 0405 return job; 0406 } 0407 0408 VcsJob* GitPlugin::diff(const QUrl& fileOrDirectory, const KDevelop::VcsRevision& srcRevision, const KDevelop::VcsRevision& dstRevision, 0409 IBasicVersionControl::RecursionMode recursion) 0410 { 0411 DVcsJob* job = static_cast<DVcsJob*>(diff(fileOrDirectory, srcRevision, dstRevision)); 0412 *job << "--"; 0413 if (recursion == IBasicVersionControl::Recursive) { 0414 *job << fileOrDirectory; 0415 } else { 0416 *job << preventRecursion(QList<QUrl>() << fileOrDirectory); 0417 } 0418 return job; 0419 } 0420 0421 KDevelop::VcsJob * GitPlugin::diff(const QUrl& repoPath, const KDevelop::VcsRevision& srcRevision, const KDevelop::VcsRevision& dstRevision) 0422 { 0423 DVcsJob* job = new GitJob(dotGitDirectory(repoPath), this, KDevelop::OutputJob::Silent); 0424 job->setType(VcsJob::Diff); 0425 *job << "git" << "diff" << "--no-color" << "--no-ext-diff"; 0426 if (!usePrefix()) { 0427 // KDE's ReviewBoard now requires p1 patchfiles, so `git diff --no-prefix` to generate p0 patches 0428 // has become optional. 0429 *job << "--no-prefix"; 0430 } 0431 if (dstRevision.revisionType() == VcsRevision::Special && 0432 dstRevision.specialType() == VcsRevision::Working) { 0433 if (srcRevision.revisionType() == VcsRevision::Special && 0434 srcRevision.specialType() == VcsRevision::Base) { 0435 *job << "HEAD"; 0436 } else { 0437 *job << "--cached" << srcRevision.revisionValue().toString(); 0438 } 0439 } else { 0440 QString revstr = revisionInterval(srcRevision, dstRevision); 0441 if(!revstr.isEmpty()) 0442 *job << revstr; 0443 } 0444 connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitDiffOutput); 0445 return job; 0446 } 0447 0448 0449 KDevelop::VcsJob * GitPlugin::reset ( const QList<QUrl>& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion ) 0450 { 0451 if(localLocations.isEmpty() ) 0452 return errorsFound(i18n("Could not reset changes (empty list of paths)"), OutputJob::Verbose); 0453 0454 DVcsJob* job = new GitJob(dotGitDirectory(localLocations.front()), this); 0455 job->setType(VcsJob::Reset); 0456 *job << "git" << "reset" << "--"; 0457 *job << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations)); 0458 return job; 0459 } 0460 0461 KDevelop::VcsJob * GitPlugin::apply(const KDevelop::VcsDiff& diff, const ApplyParams applyTo) 0462 { 0463 DVcsJob* job = new GitJob(dotGitDirectory(diff.baseDiff()), this); 0464 job->setType(VcsJob::Apply); 0465 *job << "git" << "apply"; 0466 if (applyTo == Index) { 0467 *job << "--index"; // Applies the diff also to the index 0468 *job << "--cached"; // Does not touch the work tree 0469 } 0470 auto* diffFile = new QTemporaryFile(this); 0471 if (diffFile->open()) { 0472 *job << diffFile->fileName(); 0473 diffFile->write(diff.diff().toUtf8()); 0474 diffFile->close(); 0475 connect(job, &KDevelop::VcsJob::resultsReady, [=](){delete diffFile;}); 0476 } else { 0477 job->cancel(); 0478 delete diffFile; 0479 } 0480 return job; 0481 } 0482 0483 0484 VcsJob* GitPlugin::revert(const QList<QUrl>& localLocations, IBasicVersionControl::RecursionMode recursion) 0485 { 0486 if(localLocations.isEmpty() ) 0487 return errorsFound(i18n("Could not revert changes"), OutputJob::Verbose); 0488 0489 QDir repo = urlDir(repositoryRoot(localLocations.first())); 0490 QString modified; 0491 for (const auto& file: localLocations) { 0492 if (hasModifications(repo, file)) { 0493 modified.append(file.toDisplayString(QUrl::PreferLocalFile) + QLatin1String("<br/>")); 0494 } 0495 } 0496 if (!modified.isEmpty()) { 0497 auto res = KMessageBox::questionTwoActions(nullptr, 0498 i18n("The following files have uncommitted changes, " 0499 "which will be lost. Continue?") 0500 + QLatin1String("<br/><br/>") + modified, 0501 {}, KStandardGuiItem::discard(), KStandardGuiItem::cancel()); 0502 if (res != KMessageBox::PrimaryAction) { 0503 return errorsFound(QString(), OutputJob::Silent); 0504 } 0505 } 0506 0507 DVcsJob* job = new GitJob(dotGitDirectory(localLocations.front()), this); 0508 job->setType(VcsJob::Revert); 0509 *job << "git" << "checkout" << "--"; 0510 *job << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations)); 0511 0512 return job; 0513 } 0514 0515 0516 //TODO: git doesn't like empty messages, but "KDevelop didn't provide any message, it may be a bug" looks ugly... 0517 //If no files specified then commit already added files 0518 VcsJob* GitPlugin::commit(const QString& message, 0519 const QList<QUrl>& localLocations, 0520 KDevelop::IBasicVersionControl::RecursionMode recursion) 0521 { 0522 if (localLocations.empty() || message.isEmpty()) 0523 return errorsFound(i18n("No files or message specified")); 0524 0525 const QDir dir = dotGitDirectory(localLocations.front()); 0526 if (!ensureValidGitIdentity(dir)) { 0527 return errorsFound(i18n("Email or name for Git not specified")); 0528 } 0529 0530 auto* job = new GitJob(dir, this); 0531 job->setType(VcsJob::Commit); 0532 QList<QUrl> files = (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations)); 0533 addNotVersionedFiles(dir, files); 0534 0535 *job << "git" << "commit" << "-m" << message; 0536 *job << "--" << files; 0537 return job; 0538 } 0539 0540 KDevelop::VcsJob * GitPlugin::commitStaged(const QString& message, const QUrl& repoUrl) 0541 { 0542 if (message.isEmpty()) 0543 return errorsFound(i18n("No message specified")); 0544 const QDir dir = dotGitDirectory(repoUrl); 0545 if (!ensureValidGitIdentity(dir)) { 0546 return errorsFound(i18n("Email or name for Git not specified")); 0547 } 0548 auto* job = new GitJob(dir, this); 0549 job->setType(VcsJob::Commit); 0550 *job << "git" << "commit" << "-m" << message; 0551 return job; 0552 } 0553 0554 0555 bool GitPlugin::ensureValidGitIdentity(const QDir& dir) 0556 { 0557 const QUrl url = QUrl::fromLocalFile(dir.absolutePath()); 0558 0559 const QString name = readConfigOption(url, QStringLiteral("user.name")); 0560 const QString email = readConfigOption(url, QStringLiteral("user.email")); 0561 if (!email.isEmpty() && !name.isEmpty()) { 0562 return true; // already okay 0563 } 0564 0565 GitNameEmailDialog dialog; 0566 dialog.setName(name); 0567 dialog.setEmail(email); 0568 if (!dialog.exec()) { 0569 return false; 0570 } 0571 0572 runSynchronously(setConfigOption(url, QStringLiteral("user.name"), dialog.name(), dialog.isGlobal())); 0573 runSynchronously(setConfigOption(url, QStringLiteral("user.email"), dialog.email(), dialog.isGlobal())); 0574 return true; 0575 } 0576 0577 void GitPlugin::addNotVersionedFiles(const QDir& dir, const QList<QUrl>& files) 0578 { 0579 const QStringList otherStr = getLsFiles(dir, QStringList() << QStringLiteral("--others"), KDevelop::OutputJob::Silent); 0580 QList<QUrl> toadd, otherFiles; 0581 0582 otherFiles.reserve(otherStr.size()); 0583 for (const QString& file : otherStr) { 0584 QUrl v = QUrl::fromLocalFile(dir.absoluteFilePath(file)); 0585 0586 otherFiles += v; 0587 } 0588 0589 //We add the files that are not versioned 0590 for (const QUrl& file : files) { 0591 if(otherFiles.contains(file) && QFileInfo(file.toLocalFile()).isFile()) 0592 toadd += file; 0593 } 0594 0595 if(!toadd.isEmpty()) { 0596 VcsJob* job = add(toadd); 0597 job->exec(); // krazy:exclude=crashy 0598 } 0599 } 0600 0601 bool isEmptyDirStructure(const QDir &dir) 0602 { 0603 const auto infos = dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot); 0604 for (const QFileInfo& i : infos) { 0605 if (i.isDir()) { 0606 if (!isEmptyDirStructure(QDir(i.filePath()))) return false; 0607 } else if (i.isFile()) { 0608 return false; 0609 } 0610 } 0611 return true; 0612 } 0613 0614 VcsJob* GitPlugin::remove(const QList<QUrl>& files) 0615 { 0616 if (files.isEmpty()) 0617 return errorsFound(i18n("No files to remove")); 0618 QDir dotGitDir = dotGitDirectory(files.front()); 0619 0620 0621 QList<QUrl> files_(files); 0622 0623 QMutableListIterator<QUrl> i(files_); 0624 while (i.hasNext()) { 0625 QUrl file = i.next(); 0626 QFileInfo fileInfo(file.toLocalFile()); 0627 0628 const QStringList otherStr = getLsFiles(dotGitDir, QStringList{QStringLiteral("--others"), QStringLiteral("--"), file.toLocalFile()}, KDevelop::OutputJob::Silent); 0629 if(!otherStr.isEmpty()) { 0630 //remove files not under version control 0631 QList<QUrl> otherFiles; 0632 otherFiles.reserve(otherStr.size()); 0633 for (const QString& f : otherStr) { 0634 otherFiles << QUrl::fromLocalFile(dotGitDir.path() + QLatin1Char('/') + f); 0635 } 0636 if (fileInfo.isFile()) { 0637 //if it's an unversioned file we are done, don't use git rm on it 0638 i.remove(); 0639 } 0640 0641 auto deleteJob = KIO::del(otherFiles); 0642 deleteJob->exec(); 0643 qCDebug(PLUGIN_GIT) << "other files" << otherFiles; 0644 } 0645 0646 if (fileInfo.isDir()) { 0647 if (isEmptyDirStructure(QDir(file.toLocalFile()))) { 0648 //remove empty folders, git doesn't do that 0649 auto deleteJob = KIO::del(file); 0650 deleteJob->exec(); 0651 qCDebug(PLUGIN_GIT) << "empty folder, removing" << file; 0652 //we already deleted it, don't use git rm on it 0653 i.remove(); 0654 } 0655 } 0656 } 0657 0658 if (files_.isEmpty()) return nullptr; 0659 0660 DVcsJob* job = new GitJob(dotGitDir, this); 0661 job->setType(VcsJob::Remove); 0662 // git refuses to delete files with local modifications 0663 // use --force to overcome this 0664 *job << "git" << "rm" << "-r" << "--force"; 0665 *job << "--" << files_; 0666 return job; 0667 } 0668 0669 VcsJob* GitPlugin::log(const QUrl& localLocation, 0670 const KDevelop::VcsRevision& src, const KDevelop::VcsRevision& dst) 0671 { 0672 DVcsJob* job = new GitJob(dotGitDirectory(localLocation), this, KDevelop::OutputJob::Silent); 0673 job->setType(VcsJob::Log); 0674 *job << "git" << "log" << "--date=raw" << "--name-status" << "-M80%" << "--follow"; 0675 QString rev = revisionInterval(dst, src); 0676 if(!rev.isEmpty()) 0677 *job << rev; 0678 *job << "--" << localLocation; 0679 connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitLogOutput); 0680 return job; 0681 } 0682 0683 0684 VcsJob* GitPlugin::log(const QUrl& localLocation, const KDevelop::VcsRevision& rev, unsigned long int limit) 0685 { 0686 DVcsJob* job = new GitJob(dotGitDirectory(localLocation), this, KDevelop::OutputJob::Silent); 0687 job->setType(VcsJob::Log); 0688 *job << "git" << "log" << "--date=raw" << "--name-status" << "-M80%" << "--follow"; 0689 QString revStr = toRevisionName(rev, QString()); 0690 if(!revStr.isEmpty()) 0691 *job << revStr; 0692 if(limit>0) 0693 *job << QStringLiteral("-%1").arg(limit); 0694 0695 *job << "--" << localLocation; 0696 connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitLogOutput); 0697 return job; 0698 } 0699 0700 KDevelop::VcsJob* GitPlugin::annotate(const QUrl &localLocation, const KDevelop::VcsRevision&) 0701 { 0702 DVcsJob* job = new GitJob(dotGitDirectory(localLocation), this, KDevelop::OutputJob::Silent); 0703 job->setType(VcsJob::Annotate); 0704 *job << "git" << "blame" << "--porcelain" << "-w"; 0705 *job << "--" << localLocation; 0706 connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitBlameOutput); 0707 return job; 0708 } 0709 0710 void GitPlugin::parseGitBlameOutput(DVcsJob *job) 0711 { 0712 QVariantList results; 0713 VcsAnnotationLine* annotation = nullptr; 0714 const auto output = job->output(); 0715 const auto lines = output.splitRef(QLatin1Char('\n')); 0716 0717 bool skipNext=false; 0718 QMap<QString, VcsAnnotationLine> definedRevisions; 0719 for (auto& line : lines) { 0720 if(skipNext) { 0721 skipNext=false; 0722 results += QVariant::fromValue(*annotation); 0723 0724 continue; 0725 } 0726 0727 if (line.isEmpty()) 0728 continue; 0729 0730 QStringRef name = line.left(line.indexOf(QLatin1Char(' '))); 0731 QStringRef value = line.mid(name.size()+1); 0732 0733 if(name==QLatin1String("author")) 0734 annotation->setAuthor(value.toString()); 0735 else if(name==QLatin1String("author-mail")) {} //TODO: do smth with the e-mail? 0736 else if(name==QLatin1String("author-tz")) {} //TODO: does it really matter? 0737 else if(name==QLatin1String("author-time")) 0738 annotation->setDate(QDateTime::fromSecsSinceEpoch(value.toUInt(), Qt::LocalTime)); 0739 else if(name==QLatin1String("summary")) 0740 annotation->setCommitMessage(value.toString()); 0741 else if(name.startsWith(QLatin1String("committer"))) {} //We will just store the authors 0742 else if(name==QLatin1String("previous")) {} //We don't need that either 0743 else if(name==QLatin1String("filename")) { skipNext=true; } 0744 else if(name==QLatin1String("boundary")) { 0745 definedRevisions.insert(QStringLiteral("boundary"), VcsAnnotationLine()); 0746 } 0747 else 0748 { 0749 const auto values = value.split(QLatin1Char(' ')); 0750 0751 VcsRevision rev; 0752 rev.setRevisionValue(name.left(8).toString(), KDevelop::VcsRevision::GlobalNumber); 0753 0754 skipNext = definedRevisions.contains(name.toString()); 0755 0756 if(!skipNext) 0757 definedRevisions.insert(name.toString(), VcsAnnotationLine()); 0758 0759 annotation = &definedRevisions[name.toString()]; 0760 annotation->setLineNumber(values[1].toInt() - 1); 0761 annotation->setRevision(rev); 0762 } 0763 } 0764 job->setResults(results); 0765 } 0766 0767 0768 DVcsJob* GitPlugin::lsFiles(const QDir &repository, const QStringList &args, 0769 OutputJob::OutputJobVerbosity verbosity) 0770 { 0771 auto* job = new GitJob(repository, this, verbosity); 0772 *job << "git" << "ls-files" << args; 0773 return job; 0774 } 0775 0776 VcsJob* GitPlugin::gitStash(const QDir& repository, const QStringList& args, OutputJob::OutputJobVerbosity verbosity) 0777 { 0778 auto* job = new GitJob(repository, this, verbosity); 0779 *job << "git" << "stash" << args; 0780 return job; 0781 } 0782 0783 VcsJob* GitPlugin::stashList(const QDir& repository, 0784 KDevelop::OutputJob::OutputJobVerbosity verbosity) 0785 { 0786 /* The format returns 4 fields separated by a 0-byte character (%x00): 0787 * 0788 * %gd ... shortened reflog selector 0789 * %p ... abbreviated parent hashes (separated by a space, the first is the commit 0790 * on which the stash was made) 0791 * %s ... subject (the stash message) 0792 * %ct ... committer timestamp 0793 * 0794 * see man git-log, PRETTY FORMATS section and man git-stash for details. 0795 */ 0796 auto* job=qobject_cast<DVcsJob*>(gitStash(repository, QStringList({ 0797 QStringLiteral("list"), 0798 QStringLiteral("--format=format:%gd%x00%P%x00%s%x00%ct"), 0799 }), verbosity)); 0800 connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitStashList); 0801 return job; 0802 } 0803 0804 void GitPlugin::parseGitStashList(KDevelop::VcsJob* _job) 0805 { 0806 auto* job = qobject_cast<DVcsJob*>(_job); 0807 const QList<QByteArray> output = job->rawOutput().split('\n'); 0808 QList<StashItem> results; 0809 0810 for (const QByteArray& line : output) { 0811 if (line.isEmpty()) continue; 0812 0813 const QList<QByteArray> fields = line.split('\x00'); 0814 0815 /* Extract the fields */ 0816 Q_ASSERT(fields.length() >= 4); 0817 const auto message = QString::fromUtf8(fields[2]); 0818 const auto parentHash = QString::fromUtf8(fields[1].split(' ')[0]); 0819 const auto creationTime = QDateTime::fromSecsSinceEpoch(fields[3].toInt()); 0820 const auto shortRef = QString::fromUtf8(fields[0]); 0821 const auto stackDepth = fields[0].mid(7, fields[0].indexOf('}')-7).toInt(); 0822 QStringRef branch {}; 0823 QStringRef parentCommitDesc {}; 0824 if (message.startsWith(QStringLiteral("WIP on "))) { 0825 const int colPos = message.indexOf(QLatin1Char(':'), 7); 0826 branch = message.midRef(7, colPos-7); 0827 parentCommitDesc = message.midRef(colPos+2); 0828 } 0829 0830 results << StashItem { 0831 stackDepth, 0832 shortRef, 0833 parentHash, 0834 parentCommitDesc.toString(), 0835 branch.toString(), 0836 message, 0837 creationTime, 0838 }; 0839 } 0840 job->setResults(QVariant::fromValue(results)); 0841 } 0842 0843 VcsJob* GitPlugin::tag(const QUrl& repository, const QString& commitMessage, const VcsRevision& rev, const QString& tagName) 0844 { 0845 auto* job = new GitJob(urlDir(repository), this); 0846 *job << "git" << "tag" << "-m" << commitMessage << tagName; 0847 if(rev.revisionValue().isValid()) 0848 *job << rev.revisionValue().toString(); 0849 return job; 0850 } 0851 0852 VcsJob* GitPlugin::switchBranch(const QUrl &repository, const QString &branch) 0853 { 0854 QDir d=urlDir(repository); 0855 0856 if(hasModifications(d)) { 0857 auto answer = KMessageBox::questionTwoActionsCancel( 0858 nullptr, i18n("There are pending changes, do you want to stash them first?"), {}, 0859 KGuiItem(i18nc("@action:button", "Stash"), QStringLiteral("vcs-stash")), 0860 KGuiItem(i18nc("@action:button", "Keep"), QStringLiteral("dialog-cancel"))); 0861 if (answer == KMessageBox::PrimaryAction) { 0862 QScopedPointer<VcsJob> stash(gitStash(d, QStringList(), KDevelop::OutputJob::Verbose)); 0863 stash->exec(); 0864 } else if (answer == KMessageBox::Cancel) { 0865 return nullptr; 0866 } 0867 } 0868 0869 auto* job = new GitJob(d, this); 0870 *job << "git" << "checkout" << branch; 0871 return job; 0872 } 0873 0874 VcsJob* GitPlugin::branch(const QUrl& repository, const KDevelop::VcsRevision& rev, const QString& branchName) 0875 { 0876 Q_ASSERT(!branchName.isEmpty()); 0877 0878 auto* job = new GitJob(urlDir(repository), this); 0879 *job << "git" << "branch" << "--" << branchName; 0880 0881 if(rev.revisionType() == VcsRevision::Special && rev.specialType() == VcsRevision::Head) { 0882 *job << "HEAD"; 0883 } else if(!rev.prettyValue().isEmpty()) { 0884 *job << rev.revisionValue().toString(); 0885 } 0886 return job; 0887 } 0888 0889 VcsJob* GitPlugin::deleteBranch(const QUrl& repository, const QString& branchName) 0890 { 0891 auto* job = new GitJob(urlDir(repository), this, OutputJob::Silent); 0892 *job << "git" << "branch" << "-D" << branchName; 0893 connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitCurrentBranch); 0894 return job; 0895 } 0896 0897 VcsJob* GitPlugin::renameBranch(const QUrl& repository, const QString& oldBranchName, const QString& newBranchName) 0898 { 0899 auto* job = new GitJob(urlDir(repository), this, OutputJob::Silent); 0900 *job << "git" << "branch" << "-m" << newBranchName << oldBranchName; 0901 connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitCurrentBranch); 0902 return job; 0903 } 0904 0905 VcsJob* GitPlugin::mergeBranch(const QUrl& repository, const QString& branchName) 0906 { 0907 Q_ASSERT(!branchName.isEmpty()); 0908 0909 auto* job = new GitJob(urlDir(repository), this); 0910 *job << "git" << "merge" << branchName; 0911 0912 return job; 0913 } 0914 0915 VcsJob* GitPlugin::rebase(const QUrl& repository, const QString& branchName) 0916 { 0917 auto* job = new GitJob(urlDir(repository), this); 0918 *job << "git" << "rebase" << branchName; 0919 0920 return job; 0921 } 0922 0923 VcsJob* GitPlugin::currentBranch(const QUrl& repository) 0924 { 0925 auto* job = new GitJob(urlDir(repository), this, OutputJob::Silent); 0926 job->setIgnoreError(true); 0927 *job << "git" << "symbolic-ref" << "-q" << "--short" << "HEAD"; 0928 connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitCurrentBranch); 0929 return job; 0930 } 0931 0932 void GitPlugin::parseGitCurrentBranch(DVcsJob* job) 0933 { 0934 QString out = job->output().trimmed(); 0935 0936 job->setResults(out); 0937 } 0938 0939 VcsJob* GitPlugin::branches(const QUrl &repository) 0940 { 0941 auto* job = new GitJob(urlDir(repository)); 0942 *job << "git" << "branch" << "-a"; 0943 connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitBranchOutput); 0944 return job; 0945 } 0946 0947 void GitPlugin::parseGitBranchOutput(DVcsJob* job) 0948 { 0949 const auto output = job->output(); 0950 const auto branchListDirty = output.splitRef(QLatin1Char('\n'), Qt::SkipEmptyParts); 0951 0952 QStringList branchList; 0953 for (const auto& branch : branchListDirty) { 0954 // Skip pointers to another branches (one example of this is "origin/HEAD -> origin/master"); 0955 // "git rev-list" chokes on these entries and we do not need duplicate branches altogether. 0956 if (branch.contains(QLatin1String("->"))) 0957 continue; 0958 0959 // Skip entries such as '(no branch)' 0960 if (branch.contains(QLatin1String("(no branch)"))) 0961 continue; 0962 0963 QStringRef name = branch; 0964 if (name.startsWith(QLatin1Char('*'))) 0965 name = branch.mid(2); 0966 0967 branchList << name.trimmed().toString(); 0968 } 0969 0970 job->setResults(branchList); 0971 } 0972 0973 /* Few words about how this hardcore works: 0974 1. get all commits (with --parents) 0975 2. select master (root) branch and get all unique commits for branches (git-rev-list br2 ^master ^br3) 0976 3. parse allCommits. While parsing set mask (columns state for every row) for BRANCH, INITIAL, CROSS, 0977 MERGE and INITIAL are also set in DVCScommit::setParents (depending on parents count) 0978 another setType(INITIAL) is used for "bottom/root/first" commits of branches 0979 4. find and set merges, HEADS. It's an iteration through all commits. 0980 - first we check if parent is from the same branch, if no then we go through all commits searching parent's index 0981 and set CROSS/HCROSS for rows (in 3 rows are set EMPTY after commit with parent from another tree met) 0982 - then we check branchesShas[i][0] to mark heads 0983 0984 4 can be a separate function. TODO: All this porn require refactoring (rewriting is better)! 0985 0986 It's a very dirty implementation. 0987 FIXME: 0988 1. HEAD which is head has extra line to connect it with further commit 0989 2. If you merge branch2 to master, only new commits of branch2 will be visible (it's fine, but there will be 0990 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). 0991 3. Commits that have additional commit-data (not only history merging, but changes to fix conflicts) are shown incorrectly 0992 */ 0993 0994 QVector<DVcsEvent> GitPlugin::allCommits(const QString& repo) 0995 { 0996 initBranchHash(repo); 0997 0998 const QStringList args{ 0999 QStringLiteral("--all"), 1000 QStringLiteral("--pretty"), 1001 QStringLiteral("--parents"), 1002 }; 1003 QScopedPointer<DVcsJob> job(gitRevList(repo, args)); 1004 bool ret = job->exec(); 1005 Q_ASSERT(ret && job->status()==VcsJob::JobSucceeded && "TODO: provide a fall back in case of failing"); 1006 Q_UNUSED(ret); 1007 const QStringList commits = job->output().split(QLatin1Char('\n'), Qt::SkipEmptyParts); 1008 1009 static QRegExp rx_com(QStringLiteral("commit \\w{40,40}")); 1010 1011 QVector<DVcsEvent> commitList; 1012 DVcsEvent item; 1013 1014 //used to keep where we have empty/cross/branch entry 1015 //true if it's an active branch (then cross or branch) and false if not 1016 QVector<bool> additionalFlags(branchesShas.count()); 1017 additionalFlags.fill(false); 1018 1019 //parse output 1020 for(int i = 0; i < commits.count(); ++i) 1021 { 1022 if (commits[i].contains(rx_com)) 1023 { 1024 qCDebug(PLUGIN_GIT) << "commit found in " << commits[i]; 1025 item.setCommit(commits[i].section(QLatin1Char(' '), 1, 1).trimmed()); 1026 // qCDebug(PLUGIN_GIT) << "commit is: " << commits[i].section(' ', 1); 1027 1028 QStringList parents; 1029 QString parent = commits[i].section(QLatin1Char(' '), 2); 1030 int section = 2; 1031 while (!parent.isEmpty()) 1032 { 1033 /* qCDebug(PLUGIN_GIT) << "Parent is: " << parent;*/ 1034 parents.append(parent.trimmed()); 1035 section++; 1036 parent = commits[i].section(QLatin1Char(' '), section); 1037 } 1038 item.setParents(parents); 1039 1040 //Avoid Merge string 1041 while (!commits[i].contains(QLatin1String("Author: "))) 1042 ++i; 1043 1044 item.setAuthor(commits[i].section(QStringLiteral("Author: "), 1).trimmed()); 1045 // qCDebug(PLUGIN_GIT) << "author is: " << commits[i].section("Author: ", 1); 1046 1047 item.setDate(commits[++i].section(QStringLiteral("Date: "), 1).trimmed()); 1048 // qCDebug(PLUGIN_GIT) << "date is: " << commits[i].section("Date: ", 1); 1049 1050 QString log; 1051 i++; //next line! 1052 while (i < commits.count() && !commits[i].contains(rx_com)) 1053 log += commits[i++]; 1054 --i; //while took commit line 1055 item.setLog(log.trimmed()); 1056 // qCDebug(PLUGIN_GIT) << "log is: " << log; 1057 1058 //mask is used in CommitViewDelegate to understand what we should draw for each branch 1059 QList<int> mask; 1060 mask.reserve(branchesShas.count()); 1061 1062 //set mask (properties for each graph column in row) 1063 for(int i = 0; i < branchesShas.count(); ++i) 1064 { 1065 qCDebug(PLUGIN_GIT)<<"commit: " << item.commit(); 1066 if (branchesShas[i].contains(item.commit())) 1067 { 1068 mask.append(item.type()); //we set type in setParents 1069 1070 //check if parent from the same branch, if not then we have found a root of the branch 1071 //and will use empty column for all further (from top to bottom) revisions 1072 //FIXME: we should set CROSS between parent and child (and do it when find merge point) 1073 additionalFlags[i] = false; 1074 const auto parentShas = item.parents(); 1075 for (const QString& sha : parentShas) { 1076 if (branchesShas[i].contains(sha)) 1077 additionalFlags[i] = true; 1078 } 1079 if (additionalFlags[i] == false) 1080 item.setType(DVcsEvent::INITIAL); //hasn't parents from the same branch, used in drawing 1081 } 1082 else 1083 { 1084 if (additionalFlags[i] == false) 1085 mask.append(DVcsEvent::EMPTY); 1086 else 1087 mask.append(DVcsEvent::CROSS); 1088 } 1089 qCDebug(PLUGIN_GIT) << "mask " << i << "is " << mask[i]; 1090 } 1091 item.setProperties(mask); 1092 commitList.append(item); 1093 } 1094 } 1095 1096 //find and set merges, HEADS, require refactoring! 1097 for (auto iter = commitList.begin(); 1098 iter != commitList.end(); ++iter) 1099 { 1100 QStringList parents = iter->parents(); 1101 //we need only only child branches 1102 if (parents.count() != 1) 1103 break; 1104 1105 QString parent = parents[0]; 1106 const QString commit = iter->commit(); 1107 bool parent_checked = false; 1108 int heads_checked = 0; 1109 1110 for(int i = 0; i < branchesShas.count(); ++i) 1111 { 1112 //check parent 1113 if (branchesShas[i].contains(commit)) 1114 { 1115 if (!branchesShas[i].contains(parent)) 1116 { 1117 //parent and child are not in same branch 1118 //since it is list, than parent has i+1 index 1119 //set CROSS and HCROSS 1120 for (auto f_iter = iter; 1121 f_iter != commitList.end(); ++f_iter) 1122 { 1123 if (parent == f_iter->commit()) 1124 { 1125 for(int j = 0; j < i; ++j) 1126 { 1127 if(branchesShas[j].contains(parent)) 1128 f_iter->setProperty(j, DVcsEvent::MERGE); 1129 else 1130 f_iter->setProperty(j, DVcsEvent::HCROSS); 1131 } 1132 f_iter->setType(DVcsEvent::MERGE); 1133 f_iter->setProperty(i, DVcsEvent::MERGE_RIGHT); 1134 qCDebug(PLUGIN_GIT) << parent << " is parent of " << commit; 1135 qCDebug(PLUGIN_GIT) << f_iter->commit() << " is merge"; 1136 parent_checked = true; 1137 break; 1138 } 1139 else 1140 f_iter->setProperty(i, DVcsEvent::CROSS); 1141 } 1142 } 1143 } 1144 //mark HEADs 1145 1146 if (!branchesShas[i].empty() && commit == branchesShas[i][0]) 1147 { 1148 iter->setType(DVcsEvent::HEAD); 1149 iter->setProperty(i, DVcsEvent::HEAD); 1150 heads_checked++; 1151 qCDebug(PLUGIN_GIT) << "HEAD found"; 1152 } 1153 //some optimization 1154 if (heads_checked == branchesShas.count() && parent_checked) 1155 break; 1156 } 1157 } 1158 1159 return commitList; 1160 } 1161 1162 void GitPlugin::initBranchHash(const QString &repo) 1163 { 1164 const QUrl repoUrl = QUrl::fromLocalFile(repo); 1165 const QStringList gitBranches = runSynchronously(branches(repoUrl)).toStringList(); 1166 qCDebug(PLUGIN_GIT) << "BRANCHES: " << gitBranches; 1167 //Now root branch is the current branch. In future it should be the longest branch 1168 //other commitLists are got with git-rev-lits branch ^br1 ^ br2 1169 QString root = runSynchronously(currentBranch(repoUrl)).toString(); 1170 QScopedPointer<DVcsJob> job(gitRevList(repo, QStringList(root))); 1171 bool ret = job->exec(); 1172 Q_ASSERT(ret && job->status()==VcsJob::JobSucceeded && "TODO: provide a fall back in case of failing"); 1173 Q_UNUSED(ret); 1174 const QStringList commits = job->output().split(QLatin1Char('\n'), Qt::SkipEmptyParts); 1175 // qCDebug(PLUGIN_GIT) << "\n\n\n commits" << commits << "\n\n\n"; 1176 branchesShas.append(commits); 1177 for (const QString& branch : gitBranches) { 1178 if (branch == root) 1179 continue; 1180 QStringList args(branch); 1181 for (const QString& branch_arg : gitBranches) { 1182 if (branch_arg != branch) 1183 //man gitRevList for '^' 1184 args << QLatin1Char('^') + branch_arg; 1185 } 1186 QScopedPointer<DVcsJob> job(gitRevList(repo, args)); 1187 bool ret = job->exec(); 1188 Q_ASSERT(ret && job->status()==VcsJob::JobSucceeded && "TODO: provide a fall back in case of failing"); 1189 Q_UNUSED(ret); 1190 const QStringList commits = job->output().split(QLatin1Char('\n'), Qt::SkipEmptyParts); 1191 // qCDebug(PLUGIN_GIT) << "\n\n\n commits" << commits << "\n\n\n"; 1192 branchesShas.append(commits); 1193 } 1194 } 1195 1196 //Actually we can just copy the output without parsing. So it's a kind of draft for future 1197 void GitPlugin::parseLogOutput(const DVcsJob* job, QVector<DVcsEvent>& commits) const 1198 { 1199 // static QRegExp rx_sep( "[-=]+" ); 1200 // static QRegExp rx_date( "date:\\s+([^;]*);\\s+author:\\s+([^;]*).*" ); 1201 1202 static QRegularExpression rx_com( QStringLiteral("commit \\w{1,40}") ); 1203 1204 const auto output = job->output(); 1205 const auto lines = output.splitRef(QLatin1Char('\n'), Qt::SkipEmptyParts); 1206 1207 DVcsEvent item; 1208 QString commitLog; 1209 1210 for (int i=0; i<lines.count(); ++i) { 1211 // qCDebug(PLUGIN_GIT) << "line:" << s; 1212 if (rx_com.match(lines[i]).hasMatch()) { 1213 // qCDebug(PLUGIN_GIT) << "MATCH COMMIT"; 1214 item.setCommit(lines[++i].toString()); 1215 item.setAuthor(lines[++i].toString()); 1216 item.setDate(lines[++i].toString()); 1217 item.setLog(commitLog); 1218 commits.append(item); 1219 } 1220 else 1221 { 1222 //FIXME: add this in a loop to the if, like in getAllCommits() 1223 commitLog += lines[i].toString() + QLatin1Char('\n'); 1224 } 1225 } 1226 } 1227 1228 VcsItemEvent::Actions actionsFromString(char c) 1229 { 1230 switch(c) { 1231 case 'A': return VcsItemEvent::Added; 1232 case 'D': return VcsItemEvent::Deleted; 1233 case 'R': return VcsItemEvent::Replaced; 1234 case 'M': return VcsItemEvent::Modified; 1235 } 1236 return VcsItemEvent::Modified; 1237 } 1238 1239 void GitPlugin::parseGitLogOutput(DVcsJob * job) 1240 { 1241 static QRegExp commitRegex(QStringLiteral("^commit (\\w{8})\\w{32}")); 1242 static QRegExp infoRegex(QStringLiteral("^(\\w+):(.*)")); 1243 static QRegExp modificationsRegex(QStringLiteral("^([A-Z])[0-9]*\t([^\t]+)\t?(.*)"), Qt::CaseSensitive, QRegExp::RegExp2); 1244 //R099 plugins/git/kdevgit.desktop plugins/git/kdevgit.desktop.cmake 1245 //M plugins/grepview/CMakeLists.txt 1246 1247 QList<QVariant> commits; 1248 1249 QString contents = job->output(); 1250 // check if git-log returned anything 1251 if (contents.isEmpty()) { 1252 job->setResults(commits); // empty list 1253 return; 1254 } 1255 1256 // start parsing the output 1257 QTextStream s(&contents); 1258 1259 VcsEvent item; 1260 QString message; 1261 bool pushCommit = false; 1262 1263 while (!s.atEnd()) { 1264 QString line = s.readLine(); 1265 1266 if (commitRegex.exactMatch(line)) { 1267 if (pushCommit) { 1268 item.setMessage(message.trimmed()); 1269 commits.append(QVariant::fromValue(item)); 1270 item.setItems(QList<VcsItemEvent>()); 1271 } else { 1272 pushCommit = true; 1273 } 1274 VcsRevision rev; 1275 rev.setRevisionValue(commitRegex.cap(1), KDevelop::VcsRevision::GlobalNumber); 1276 item.setRevision(rev); 1277 message.clear(); 1278 } else if (infoRegex.exactMatch(line)) { 1279 QString cap1 = infoRegex.cap(1); 1280 if (cap1 == QLatin1String("Author")) { 1281 item.setAuthor(infoRegex.cap(2).trimmed()); 1282 } else if (cap1 == QLatin1String("Date")) { 1283 item.setDate(QDateTime::fromSecsSinceEpoch(infoRegex.cap(2).trimmed().split(QLatin1Char(' '))[0].toUInt(), Qt::LocalTime)); 1284 } 1285 } else if (modificationsRegex.exactMatch(line)) { 1286 VcsItemEvent::Actions a = actionsFromString(modificationsRegex.cap(1).at(0).toLatin1()); 1287 QString filenameA = modificationsRegex.cap(2); 1288 1289 VcsItemEvent itemEvent; 1290 itemEvent.setActions(a); 1291 itemEvent.setRepositoryLocation(filenameA); 1292 if(a==VcsItemEvent::Replaced) { 1293 QString filenameB = modificationsRegex.cap(3); 1294 itemEvent.setRepositoryCopySourceLocation(filenameB); 1295 } 1296 1297 item.addItem(itemEvent); 1298 } else if (line.startsWith(QLatin1String(" "))) { 1299 message += line.midRef(4) + QLatin1Char('\n'); 1300 } 1301 } 1302 1303 item.setMessage(message.trimmed()); 1304 commits.append(QVariant::fromValue(item)); 1305 job->setResults(commits); 1306 } 1307 1308 void GitPlugin::parseGitDiffOutput(DVcsJob* job) 1309 { 1310 VcsDiff diff; 1311 diff.setDiff(job->output()); 1312 diff.setBaseDiff(repositoryRoot(QUrl::fromLocalFile(job->directory().absolutePath()))); 1313 diff.setDepth(usePrefix()? 1 : 0); 1314 1315 job->setResults(QVariant::fromValue(diff)); 1316 } 1317 1318 static VcsStatusInfo::State lsfilesToState(char id) 1319 { 1320 switch(id) { 1321 case 'H': return VcsStatusInfo::ItemUpToDate; //Cached 1322 case 'S': return VcsStatusInfo::ItemUpToDate; //Skip work tree 1323 case 'M': return VcsStatusInfo::ItemHasConflicts; //unmerged 1324 case 'R': return VcsStatusInfo::ItemDeleted; //removed/deleted 1325 case 'C': return VcsStatusInfo::ItemModified; //modified/changed 1326 case 'K': return VcsStatusInfo::ItemDeleted; //to be killed 1327 case '?': return VcsStatusInfo::ItemUnknown; //other 1328 } 1329 Q_ASSERT(false); 1330 return VcsStatusInfo::ItemUnknown; 1331 } 1332 1333 void GitPlugin::parseGitStatusOutput_old(DVcsJob* job) 1334 { 1335 const QString output = job->output(); 1336 const auto outputLines = output.splitRef(QLatin1Char('\n'), Qt::SkipEmptyParts); 1337 1338 QDir dir = job->directory(); 1339 QMap<QUrl, VcsStatusInfo::State> allStatus; 1340 for (const auto& line : outputLines) { 1341 VcsStatusInfo::State status = lsfilesToState(line[0].toLatin1()); 1342 1343 QUrl url = QUrl::fromLocalFile(dir.absoluteFilePath(line.mid(2).toString())); 1344 1345 allStatus[url] = status; 1346 } 1347 1348 QVariantList statuses; 1349 statuses.reserve(allStatus.size()); 1350 QMap< QUrl, VcsStatusInfo::State >::const_iterator it = allStatus.constBegin(), itEnd=allStatus.constEnd(); 1351 for(; it!=itEnd; ++it) { 1352 1353 VcsStatusInfo status; 1354 status.setUrl(it.key()); 1355 status.setState(it.value()); 1356 1357 statuses.append(QVariant::fromValue<VcsStatusInfo>(status)); 1358 } 1359 1360 job->setResults(statuses); 1361 } 1362 1363 void GitPlugin::parseGitStatusOutput(DVcsJob* job) 1364 { 1365 const auto output = job->output(); 1366 const auto outputLines = output.splitRef(QLatin1Char('\n'), Qt::SkipEmptyParts); 1367 QDir workingDir = job->directory(); 1368 QDir dotGit = dotGitDirectory(QUrl::fromLocalFile(workingDir.absolutePath())); 1369 1370 QVariantList statuses; 1371 QList<QUrl> processedFiles; 1372 1373 for (const QStringRef& line : outputLines) { 1374 //every line is 2 chars for the status, 1 space then the file desc 1375 QStringRef curr=line.mid(3); 1376 QStringRef state = line.left(2); 1377 1378 int arrow = curr.indexOf(QLatin1String(" -> ")); 1379 if(arrow>=0) { 1380 VcsStatusInfo status; 1381 status.setUrl(QUrl::fromLocalFile(dotGit.absoluteFilePath(curr.toString().left(arrow)))); 1382 status.setState(VcsStatusInfo::ItemDeleted); 1383 statuses.append(QVariant::fromValue<VcsStatusInfo>(status)); 1384 processedFiles += status.url(); 1385 1386 curr = curr.mid(arrow+4); 1387 } 1388 1389 if (curr.startsWith(QLatin1Char('\"')) && curr.endsWith(QLatin1Char('\"'))) { //if the path is quoted, unquote 1390 curr = curr.mid(1, curr.size()-2); 1391 } 1392 1393 VcsStatusInfo status; 1394 ExtendedState ex_state = parseGitState(state); 1395 status.setUrl(QUrl::fromLocalFile(dotGit.absoluteFilePath(curr.toString()))); 1396 status.setExtendedState(ex_state); 1397 status.setState(extendedStateToBasic(ex_state)); 1398 processedFiles.append(status.url()); 1399 1400 qCDebug(PLUGIN_GIT) << "Checking git status for " << line << curr << status.state(); 1401 1402 statuses.append(QVariant::fromValue<VcsStatusInfo>(status)); 1403 } 1404 QStringList paths; 1405 QStringList oldcmd=job->dvcsCommand(); 1406 QStringList::const_iterator it=oldcmd.constBegin()+oldcmd.indexOf(QStringLiteral("--"))+1, itEnd=oldcmd.constEnd(); 1407 paths.reserve(oldcmd.size()); 1408 for(; it!=itEnd; ++it) 1409 paths += *it; 1410 1411 //here we add the already up to date files 1412 const QStringList files = getLsFiles(job->directory(), QStringList{QStringLiteral("-c"), QStringLiteral("--")} << paths, OutputJob::Silent); 1413 for (const QString& file : files) { 1414 QUrl fileUrl = QUrl::fromLocalFile(workingDir.absoluteFilePath(file)); 1415 1416 if(!processedFiles.contains(fileUrl)) { 1417 VcsStatusInfo status; 1418 status.setUrl(fileUrl); 1419 status.setState(VcsStatusInfo::ItemUpToDate); 1420 1421 statuses.append(QVariant::fromValue<VcsStatusInfo>(status)); 1422 } 1423 } 1424 job->setResults(statuses); 1425 } 1426 1427 void GitPlugin::parseGitVersionOutput(DVcsJob* job) 1428 { 1429 const auto output = job->output().trimmed(); 1430 auto versionString = output.midRef(output.lastIndexOf(QLatin1Char(' '))); 1431 const auto minimumVersion = QVersionNumber(1, 7); 1432 const auto actualVersion = QVersionNumber::fromString(versionString); 1433 m_oldVersion = actualVersion < minimumVersion; 1434 qCDebug(PLUGIN_GIT) << "checking git version" << versionString << actualVersion << "against" << minimumVersion 1435 << m_oldVersion; 1436 } 1437 1438 QStringList GitPlugin::getLsFiles(const QDir &directory, const QStringList &args, 1439 KDevelop::OutputJob::OutputJobVerbosity verbosity) 1440 { 1441 QScopedPointer<DVcsJob> job(lsFiles(directory, args, verbosity)); 1442 if (job->exec() && job->status() == KDevelop::VcsJob::JobSucceeded) 1443 return job->output().split(QLatin1Char('\n'), Qt::SkipEmptyParts); 1444 1445 return QStringList(); 1446 } 1447 1448 DVcsJob* GitPlugin::gitRevParse(const QString &repository, const QStringList &args, 1449 KDevelop::OutputJob::OutputJobVerbosity verbosity) 1450 { 1451 auto* job = new GitJob(QDir(repository), this, verbosity); 1452 *job << "git" << "rev-parse" << args; 1453 1454 return job; 1455 } 1456 1457 DVcsJob* GitPlugin::gitRevList(const QString& directory, const QStringList& args) 1458 { 1459 auto* job = new GitJob(urlDir(QUrl::fromLocalFile(directory)), this, KDevelop::OutputJob::Silent); 1460 { 1461 *job << "git" << "rev-list" << args; 1462 return job; 1463 } 1464 } 1465 1466 constexpr int _pair(char a, char b) { return a*256 + b;} 1467 1468 GitPlugin::ExtendedState GitPlugin::parseGitState(const QStringRef& msg) 1469 { 1470 Q_ASSERT(msg.size()==1 || msg.size()==2); 1471 ExtendedState ret = GitInvalid; 1472 1473 if(msg.contains(QLatin1Char('U')) || msg == QLatin1String("AA") || msg == QLatin1String("DD")) 1474 ret = GitConflicts; 1475 else switch(_pair(msg.at(0).toLatin1(), msg.at(1).toLatin1())) 1476 { 1477 case _pair(' ', ' '): 1478 ret = GitXX; 1479 break; 1480 case _pair(' ','T'): // Typechange 1481 case _pair(' ','M'): 1482 ret = GitXM; 1483 break; 1484 case _pair ( ' ','D' ) : 1485 ret = GitXD; 1486 break; 1487 case _pair ( ' ','R' ) : 1488 ret = GitXR; 1489 break; 1490 case _pair ( ' ','C' ) : 1491 ret = GitXC; 1492 break; 1493 case _pair ( 'T',' ' ) : // Typechange 1494 case _pair ( 'M',' ' ) : 1495 ret = GitMX; 1496 break; 1497 case _pair ( 'M','M' ) : 1498 ret = GitMM; 1499 break; 1500 case _pair ( 'M','D' ) : 1501 ret = GitMD; 1502 break; 1503 case _pair ( 'A',' ' ) : 1504 ret = GitAX; 1505 break; 1506 case _pair ( 'A','M' ) : 1507 ret = GitAM; 1508 break; 1509 case _pair ( 'A','D' ) : 1510 ret = GitAD; 1511 break; 1512 case _pair ( 'D',' ' ) : 1513 ret = GitDX; 1514 break; 1515 case _pair ( 'D','R' ) : 1516 ret = GitDR; 1517 break; 1518 case _pair ( 'D','C' ) : 1519 ret = GitDC; 1520 break; 1521 case _pair ( 'R',' ' ) : 1522 ret = GitRX; 1523 break; 1524 case _pair ( 'R','M' ) : 1525 ret = GitRM; 1526 break; 1527 case _pair ( 'R','D' ) : 1528 ret = GitRD; 1529 break; 1530 case _pair ( 'C',' ' ) : 1531 ret = GitCX; 1532 break; 1533 case _pair ( 'C','M' ) : 1534 ret = GitCM; 1535 break; 1536 case _pair ( 'C','D' ) : 1537 ret = GitCD; 1538 break; 1539 case _pair ( '?','?' ) : 1540 ret = GitUntracked; 1541 break; 1542 default: 1543 qCDebug(PLUGIN_GIT) << "Git status not identified:" << msg; 1544 ret = GitInvalid; 1545 break; 1546 } 1547 1548 return ret; 1549 } 1550 1551 KDevelop::VcsStatusInfo::State GitPlugin::extendedStateToBasic(const GitPlugin::ExtendedState state) 1552 { 1553 switch(state) { 1554 case GitXX: return VcsStatusInfo::ItemUpToDate; 1555 case GitXM: return VcsStatusInfo::ItemModified; 1556 case GitXD: return VcsStatusInfo::ItemDeleted; 1557 case GitXR: return VcsStatusInfo::ItemModified; 1558 case GitXC: return VcsStatusInfo::ItemModified; 1559 case GitMX: return VcsStatusInfo::ItemModified; 1560 case GitMM: return VcsStatusInfo::ItemModified; 1561 case GitMD: return VcsStatusInfo::ItemDeleted; 1562 case GitAX: return VcsStatusInfo::ItemAdded; 1563 case GitAM: return VcsStatusInfo::ItemAdded; 1564 case GitAD: return VcsStatusInfo::ItemAdded; 1565 case GitDX: return VcsStatusInfo::ItemDeleted; 1566 case GitDR: return VcsStatusInfo::ItemDeleted; 1567 case GitDC: return VcsStatusInfo::ItemDeleted; 1568 case GitRX: return VcsStatusInfo::ItemModified; 1569 case GitRM: return VcsStatusInfo::ItemModified; 1570 case GitRD: return VcsStatusInfo::ItemDeleted; 1571 case GitCX: return VcsStatusInfo::ItemModified; 1572 case GitCM: return VcsStatusInfo::ItemModified; 1573 case GitCD: return VcsStatusInfo::ItemDeleted; 1574 case GitUntracked: return VcsStatusInfo::ItemUnknown; 1575 case GitConflicts: return VcsStatusInfo::ItemHasConflicts; 1576 case GitInvalid: return VcsStatusInfo::ItemUnknown; 1577 } 1578 return VcsStatusInfo::ItemUnknown; 1579 } 1580 1581 1582 StandardJob::StandardJob(IPlugin* parent, KJob* job, 1583 OutputJob::OutputJobVerbosity verbosity) 1584 : VcsJob(parent, verbosity) 1585 , m_job(job) 1586 , m_plugin(parent) 1587 , m_status(JobNotStarted) 1588 {} 1589 1590 void StandardJob::start() 1591 { 1592 connect(m_job, &KJob::result, this, &StandardJob::result); 1593 m_job->start(); 1594 m_status=JobRunning; 1595 } 1596 1597 void StandardJob::result(KJob* job) 1598 { 1599 if (job->error() == 0) { 1600 m_status = JobSucceeded; 1601 setError(NoError); 1602 } else { 1603 m_status = JobFailed; 1604 setError(UserDefinedError); 1605 } 1606 emitResult(); 1607 } 1608 1609 VcsJob* GitPlugin::copy(const QUrl& localLocationSrc, const QUrl& localLocationDstn) 1610 { 1611 //TODO: Probably we should "git add" after 1612 return new StandardJob(this, KIO::copy(localLocationSrc, localLocationDstn), KDevelop::OutputJob::Silent); 1613 } 1614 1615 VcsJob* GitPlugin::move(const QUrl& source, const QUrl& destination) 1616 { 1617 QDir dir = urlDir(source); 1618 1619 QFileInfo fileInfo(source.toLocalFile()); 1620 if (fileInfo.isDir()) { 1621 if (isEmptyDirStructure(QDir(source.toLocalFile()))) { 1622 //move empty folder, git doesn't do that 1623 qCDebug(PLUGIN_GIT) << "empty folder" << source; 1624 return new StandardJob(this, KIO::move(source, destination), KDevelop::OutputJob::Silent); 1625 } 1626 } 1627 1628 const QStringList otherStr = getLsFiles(dir, QStringList{QStringLiteral("--others"), QStringLiteral("--"), source.toLocalFile()}, KDevelop::OutputJob::Silent); 1629 if(otherStr.isEmpty()) { 1630 auto* job = new GitJob(dir, this, KDevelop::OutputJob::Verbose); 1631 *job << "git" << "mv" << source.toLocalFile() << destination.toLocalFile(); 1632 return job; 1633 } else { 1634 return new StandardJob(this, KIO::move(source, destination), KDevelop::OutputJob::Silent); 1635 } 1636 } 1637 1638 void GitPlugin::parseGitRepoLocationOutput(DVcsJob* job) 1639 { 1640 job->setResults(QVariant::fromValue(QUrl::fromLocalFile(job->output()))); 1641 } 1642 1643 VcsJob* GitPlugin::repositoryLocation(const QUrl& localLocation) 1644 { 1645 auto* job = new GitJob(urlDir(localLocation), this); 1646 //Probably we should check first if origin is the proper remote we have to use but as a first attempt it works 1647 *job << "git" << "config" << "remote.origin.url"; 1648 connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitRepoLocationOutput); 1649 return job; 1650 } 1651 1652 VcsJob* GitPlugin::pull(const KDevelop::VcsLocation& localOrRepoLocationSrc, const QUrl& localRepositoryLocation) 1653 { 1654 auto* job = new GitJob(urlDir(localRepositoryLocation), this); 1655 job->setCommunicationMode(KProcess::MergedChannels); 1656 *job << "git" << "pull"; 1657 if(!localOrRepoLocationSrc.localUrl().isEmpty()) 1658 *job << localOrRepoLocationSrc.localUrl().url(); 1659 return job; 1660 } 1661 1662 VcsJob* GitPlugin::push(const QUrl& localRepositoryLocation, const KDevelop::VcsLocation& localOrRepoLocationDst) 1663 { 1664 auto* job = new GitJob(urlDir(localRepositoryLocation), this); 1665 job->setCommunicationMode(KProcess::MergedChannels); 1666 *job << "git" << "push"; 1667 if(!localOrRepoLocationDst.localUrl().isEmpty()) 1668 *job << localOrRepoLocationDst.localUrl().url(); 1669 return job; 1670 } 1671 1672 VcsJob* GitPlugin::resolve(const QList<QUrl>& localLocations, IBasicVersionControl::RecursionMode recursion) 1673 { 1674 return add(localLocations, recursion); 1675 } 1676 1677 VcsJob* GitPlugin::update(const QList<QUrl>& localLocations, const KDevelop::VcsRevision& rev, IBasicVersionControl::RecursionMode recursion) 1678 { 1679 if(rev.revisionType()==VcsRevision::Special && rev.revisionValue().value<VcsRevision::RevisionSpecialType>()==VcsRevision::Head) { 1680 return pull(VcsLocation(), localLocations.first()); 1681 } else { 1682 auto* job = new GitJob(urlDir(localLocations.first()), this); 1683 { 1684 //Probably we should check first if origin is the proper remote we have to use but as a first attempt it works 1685 *job << "git" << "checkout" << rev.revisionValue().toString() << "--"; 1686 *job << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations)); 1687 return job; 1688 } 1689 } 1690 } 1691 1692 void GitPlugin::setupCommitMessageEditor(const QUrl& localLocation, KTextEdit* editor) const 1693 { 1694 new GitMessageHighlighter(editor); 1695 QFile mergeMsgFile(dotGitDirectory(localLocation).filePath(QStringLiteral(".git/MERGE_MSG"))); 1696 // Some limit on the file size should be set since whole content is going to be read into 1697 // the memory. 1Mb seems to be good value since it's rather strange to have so huge commit 1698 // message. 1699 static const qint64 maxMergeMsgFileSize = 1024*1024; 1700 if (mergeMsgFile.size() > maxMergeMsgFileSize || !mergeMsgFile.open(QIODevice::ReadOnly)) 1701 return; 1702 1703 QString mergeMsg = QString::fromLocal8Bit(mergeMsgFile.read(maxMergeMsgFileSize)); 1704 editor->setPlainText(mergeMsg); 1705 } 1706 1707 class GitVcsLocationWidget : public KDevelop::StandardVcsLocationWidget 1708 { 1709 Q_OBJECT 1710 public: 1711 explicit GitVcsLocationWidget(QWidget* parent = nullptr) 1712 : StandardVcsLocationWidget(parent) 1713 {} 1714 1715 bool isCorrect() const override 1716 { 1717 return !url().isEmpty(); 1718 } 1719 }; 1720 1721 KDevelop::VcsLocationWidget* GitPlugin::vcsLocation(QWidget* parent) const 1722 { 1723 return new GitVcsLocationWidget(parent); 1724 } 1725 1726 void GitPlugin::registerRepositoryForCurrentBranchChanges(const QUrl& repository) 1727 { 1728 QDir dir = dotGitDirectory(repository); 1729 QString headFile = dir.absoluteFilePath(QStringLiteral(".git/HEAD")); 1730 m_watcher->addFile(headFile); 1731 } 1732 1733 void GitPlugin::fileChanged(const QString& file) 1734 { 1735 Q_ASSERT(file.endsWith(QLatin1String("HEAD"))); 1736 //SMTH/.git/HEAD -> SMTH/ 1737 const QUrl fileUrl = Path(file).parent().parent().toUrl(); 1738 1739 //We need to delay the emitted signal, otherwise the branch hasn't change yet 1740 //and the repository is not functional 1741 m_branchesChange.append(fileUrl); 1742 QTimer::singleShot(1000, this, &GitPlugin::delayedBranchChanged); 1743 } 1744 1745 void GitPlugin::delayedBranchChanged() 1746 { 1747 emit repositoryBranchChanged(m_branchesChange.takeFirst()); 1748 } 1749 1750 CheckInRepositoryJob* GitPlugin::isInRepository(KTextEditor::Document* document) 1751 { 1752 CheckInRepositoryJob* job = new GitPluginCheckInRepositoryJob(document, repositoryRoot(document->url()).path()); 1753 job->start(); 1754 return job; 1755 } 1756 1757 DVcsJob* GitPlugin::setConfigOption(const QUrl& repository, const QString& key, const QString& value, bool global) 1758 { 1759 auto job = new GitJob(urlDir(repository), this); 1760 QStringList args; 1761 args << QStringLiteral("git") << QStringLiteral("config"); 1762 if(global) 1763 args << QStringLiteral("--global"); 1764 args << key << value; 1765 *job << args; 1766 return job; 1767 } 1768 1769 QString GitPlugin::readConfigOption(const QUrl& repository, const QString& key) 1770 { 1771 QProcess exec; 1772 exec.setWorkingDirectory(urlDir(repository).absolutePath()); 1773 exec.start(QStringLiteral("git"), QStringList{QStringLiteral("config"), QStringLiteral("--get"), key}); 1774 exec.waitForFinished(); 1775 return QString::fromUtf8(exec.readAllStandardOutput().trimmed()); 1776 } 1777 1778 #include "gitplugin.moc" 1779 #include "moc_gitplugin.cpp"