File indexing completed on 2024-04-14 05:34:12

0001 /*
0002     SPDX-FileCopyrightText: 2010 Peter Penz <peter.penz@gmx.at>
0003     SPDX-FileCopyrightText: 2010 Sebastian Doerner <sebastian@sebastian-doerner.de>
0004     SPDX-FileCopyrightText: 2010 Johannes Steffen <jsteffen@st.ovgu.de>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 #include "fileviewgitplugin.h"
0010 #include "checkoutdialog.h"
0011 #include "commitdialog.h"
0012 #include "tagdialog.h"
0013 #include "pushdialog.h"
0014 #include "gitwrapper.h"
0015 #include "pulldialog.h"
0016 
0017 #include <KLocalizedString>
0018 #include <KShell>
0019 #include <KPluginFactory>
0020 
0021 #include <QTemporaryFile>
0022 #include <QProcess>
0023 #include <QString>
0024 #include <QStringList>
0025 #include <QTextCodec>
0026 #include <QDir>
0027 #include <QTextBrowser>
0028 #include <QVBoxLayout>
0029 
0030 K_PLUGIN_CLASS_WITH_JSON(FileViewGitPlugin, "fileviewgitplugin.json")
0031 
0032 FileViewGitPlugin::FileViewGitPlugin(QObject* parent, const QList<QVariant>& args) :
0033     KVersionControlPlugin(parent),
0034     m_pendingOperation(false),
0035     m_addAction(nullptr),
0036     m_removeAction(nullptr),
0037     m_checkoutAction(nullptr),
0038     m_commitAction(nullptr),
0039     m_tagAction(nullptr),
0040     m_pushAction(nullptr),
0041     m_pullAction(nullptr)
0042 {
0043     Q_UNUSED(args);
0044 
0045     m_parentWidget = qobject_cast<QWidget*>(parent);
0046 
0047     m_revertAction = new QAction(this);
0048     m_revertAction->setIcon(QIcon::fromTheme(QStringLiteral("document-revert")));
0049     m_revertAction->setText(xi18nd("@action:inmenu", "<application>Git</application> Revert"));
0050     connect(m_revertAction, &QAction::triggered,
0051             this, &FileViewGitPlugin::revertFiles);
0052 
0053     m_addAction = new QAction(this);
0054     m_addAction->setIcon(QIcon::fromTheme(QStringLiteral("list-add")));
0055     m_addAction->setText(xi18nd("@action:inmenu", "<application>Git</application> Add"));
0056     connect(m_addAction, &QAction::triggered,
0057             this, &FileViewGitPlugin::addFiles);
0058 
0059     m_showLocalChangesAction = new QAction(this);
0060     m_showLocalChangesAction->setIcon(QIcon::fromTheme(QStringLiteral("vcs-diff")));
0061     m_showLocalChangesAction->setText(xi18nd("@item:inmenu", "Show Local <application>Git</application> Changes"));
0062     connect(m_showLocalChangesAction, &QAction::triggered,
0063             this, &FileViewGitPlugin::showLocalChanges);
0064 
0065     m_removeAction = new QAction(this);
0066     m_removeAction->setIcon(QIcon::fromTheme(QStringLiteral("list-remove")));
0067     m_removeAction->setText(xi18nd("@action:inmenu", "<application>Git</application> Remove"));
0068     connect(m_removeAction, &QAction::triggered,
0069             this, &FileViewGitPlugin::removeFiles);
0070 
0071     m_checkoutAction = new QAction(this);
0072     m_checkoutAction->setIcon(QIcon::fromTheme(QStringLiteral("vcs-branch")));
0073     m_checkoutAction->setText(xi18nd("@action:inmenu", "<application>Git</application> Checkout..."));
0074     connect(m_checkoutAction, &QAction::triggered,
0075             this, &FileViewGitPlugin::checkout);
0076 
0077     m_commitAction = new QAction(this);
0078     m_commitAction->setIcon(QIcon::fromTheme(QStringLiteral("vcs-commit")));
0079     m_commitAction->setText(xi18nd("@action:inmenu", "<application>Git</application> Commit..."));
0080     connect(m_commitAction, &QAction::triggered,
0081             this, &FileViewGitPlugin::commit);
0082 
0083     m_tagAction = new QAction(this);
0084 //     m_tagAction->setIcon(QIcon::fromTheme(QStringLiteral("svn-commit")));
0085     m_tagAction->setText(xi18nd("@action:inmenu", "<application>Git</application> Create Tag..."));
0086     connect(m_tagAction, &QAction::triggered,
0087             this, &FileViewGitPlugin::createTag);
0088     m_pushAction = new QAction(this);
0089     m_pushAction->setIcon(QIcon::fromTheme(QStringLiteral("vcs-push")));
0090     m_pushAction->setText(xi18nd("@action:inmenu", "<application>Git</application> Push..."));
0091     connect(m_pushAction, &QAction::triggered,
0092             this, &FileViewGitPlugin::push);
0093     m_pullAction = new QAction(this);
0094     m_pullAction->setIcon(QIcon::fromTheme(QStringLiteral("vcs-pull")));
0095     m_pullAction->setText(xi18nd("@action:inmenu", "<application>Git</application> Pull..."));
0096     connect(m_pullAction, &QAction::triggered,
0097             this, &FileViewGitPlugin::pull);
0098     m_mergeAction = new QAction(this);
0099     m_mergeAction->setIcon(QIcon::fromTheme(QStringLiteral("vcs-merge")));
0100     m_mergeAction->setText(xi18nd("@action:inmenu", "<application>Git</application> Merge..."));
0101     connect(m_mergeAction, &QAction::triggered, this, &FileViewGitPlugin::merge);
0102 
0103     m_logAction = new QAction(this);
0104     m_logAction->setText(xi18nd("@action:inmenu", "<application>Git</application> Log..."));
0105     connect(m_logAction, &QAction::triggered, this, &FileViewGitPlugin::log);
0106 
0107     connect(&m_process, &QProcess::finished,
0108             this, &FileViewGitPlugin::slotOperationCompleted);
0109     connect(&m_process, &QProcess::errorOccurred,
0110             this, &FileViewGitPlugin::slotOperationError);
0111 }
0112 
0113 FileViewGitPlugin::~FileViewGitPlugin()
0114 {
0115     GitWrapper::freeInstance();
0116 }
0117 
0118 QString FileViewGitPlugin::fileName() const
0119 {
0120     return QLatin1String(".git");
0121 }
0122 
0123 QString FileViewGitPlugin::localRepositoryRoot(const QString& directory) const
0124 {
0125     QProcess process;
0126     process.setWorkingDirectory(directory);
0127     process.start(QStringLiteral("git"), {QStringLiteral("rev-parse"), QStringLiteral("--show-toplevel")});
0128     if (process.waitForReadyRead(100) && process.exitCode() == 0) {
0129         return QString::fromUtf8(process.readAll().chopped(1));
0130     }
0131     return QString();
0132 }
0133 
0134 int FileViewGitPlugin::readUntilZeroChar(QIODevice* device, char* buffer, const int maxChars) {
0135     if (buffer == nullptr) { // discard until next \0
0136         char c;
0137         while (device->getChar(&c) && c != '\0')
0138             ;
0139         return 0;
0140     }
0141     int index = -1;
0142     while (++index < maxChars) {
0143         if (!device->getChar(&buffer[index])) {
0144             if (device->waitForReadyRead(30000)) {  // 30 seconds to be consistent with QProcess::waitForReadyRead default
0145                 --index;
0146                 continue;
0147             } else {
0148                 buffer[index] = '\0';
0149                 return index <= 0 ? 0 : index + 1;
0150             }
0151         }
0152         if (buffer[index] == '\0') {  // line end or we put it there (see above)
0153             return index + 1;
0154         }
0155     }
0156     return maxChars;
0157 }
0158 
0159 bool FileViewGitPlugin::beginRetrieval(const QString& directory)
0160 {
0161     Q_ASSERT(directory.endsWith(QLatin1Char('/')));
0162 
0163     GitWrapper::instance()->setWorkingDirectory(directory);
0164     m_currentDir = directory;
0165 
0166     // ----- find path below git base dir -----
0167     QProcess process;
0168     process.setWorkingDirectory(directory);
0169     process.start(QStringLiteral("git"), {QStringLiteral("rev-parse"), QStringLiteral("--show-prefix")});
0170     QString dirBelowBaseDir;
0171     while (process.waitForReadyRead()) {
0172         char buffer[512];
0173         while (process.readLine(buffer, sizeof(buffer)) > 0)  {
0174             dirBelowBaseDir = QString::fromLocal8Bit(buffer).trimmed(); // ends in "/" or is empty
0175         }
0176     }
0177 
0178     m_versionInfoHash.clear();
0179 
0180     // ----- find files with special status -----
0181     process.start(QStringLiteral("git"), {QStringLiteral("--no-optional-locks"), QStringLiteral("status"), QStringLiteral("--porcelain"), QStringLiteral("-z"), QStringLiteral("-u"), QStringLiteral("--ignored")});
0182     while (process.waitForReadyRead()) {
0183         char buffer[1024];
0184         while (readUntilZeroChar(&process, buffer, sizeof(buffer)) > 0 ) {
0185             QString line = QTextCodec::codecForLocale()->toUnicode(buffer);
0186             // ----- recognize file status -----
0187             char X = line[0].toLatin1();  // X and Y from the table in `man git-status`
0188             char Y = line[1].toLatin1();
0189             const QString fileName= line.mid(3);
0190             ItemVersion state = NormalVersion;
0191             switch (X) {
0192                 case '!':
0193                     state = IgnoredVersion;
0194                     break;
0195                 case '?':
0196                     state = UnversionedVersion;
0197                     break;
0198                 case 'C': // handle copied as added version
0199                 case 'A':
0200                     state = AddedVersion;
0201                     break;
0202                 case 'D':
0203                     state = RemovedVersion;
0204                     break;
0205                 case 'M':
0206                     state = LocallyModifiedVersion;
0207                     break;
0208                 case 'R':
0209                     state = LocallyModifiedVersion;
0210                     // Renames list the old file name directly afterwards, separated by \0.
0211                     readUntilZeroChar(&process, nullptr, 0); // discard old file name
0212                     break;
0213             }
0214             // overwrite status depending on the working tree
0215             switch (Y) {
0216                 case 'D': // handle "deleted in working tree" as "modified in working tree"
0217                 case 'M':
0218                     state = LocallyModifiedUnstagedVersion;
0219                     break;
0220             }
0221             // overwrite status in case of conflicts (lower part of the table in `man git-status`)
0222             if (X == 'U' ||
0223                 Y == 'U' ||
0224                 (X == 'A' && Y == 'A') ||
0225                 (X == 'D' && Y == 'D')) {
0226                 state = ConflictingVersion;
0227             }
0228 
0229             // ----- decide what to record about that file -----
0230             if (state == NormalVersion || !fileName.startsWith(dirBelowBaseDir)) {
0231                 continue;
0232             }
0233             /// File name relative to the current working directory.
0234             const QString relativeFileName = fileName.mid(dirBelowBaseDir.length());
0235             //if file is part of a sub-directory, record the directory
0236             if (relativeFileName.contains(QLatin1Char('/'))) {
0237                 if (state == IgnoredVersion)
0238                     continue;
0239                 if (state == AddedVersion || state == RemovedVersion) {
0240                     state = LocallyModifiedVersion;
0241                 }
0242                 const QString absoluteDirName = directory + relativeFileName.left(relativeFileName.indexOf(QLatin1Char('/')));
0243                 if (m_versionInfoHash.contains(absoluteDirName)) {
0244                     ItemVersion oldState = m_versionInfoHash.value(absoluteDirName);
0245                     //only keep the most important state for a directory
0246                     if (oldState == ConflictingVersion)
0247                         continue;
0248                     if (oldState == LocallyModifiedUnstagedVersion && state != ConflictingVersion)
0249                         continue;
0250                     if (oldState == LocallyModifiedVersion &&
0251                         state != LocallyModifiedUnstagedVersion && state != ConflictingVersion)
0252                         continue;
0253                     m_versionInfoHash.insert(absoluteDirName, state);
0254                 } else {
0255                     m_versionInfoHash.insert(absoluteDirName, state);
0256                 }
0257             } else { //normal file, no directory
0258                 m_versionInfoHash.insert(directory + relativeFileName, state);
0259             }
0260         }
0261     }
0262     return true;
0263 }
0264 
0265 void FileViewGitPlugin::endRetrieval()
0266 {
0267 }
0268 
0269 KVersionControlPlugin::ItemVersion FileViewGitPlugin::itemVersion(const KFileItem& item) const
0270 {
0271     const QString itemUrl = item.localPath();
0272     if (m_versionInfoHash.contains(itemUrl)) {
0273         return m_versionInfoHash.value(itemUrl);
0274     } else {
0275         // files that are not in our map are normal, tracked files by definition
0276         return NormalVersion;
0277     }
0278 }
0279 
0280 QList<QAction*> FileViewGitPlugin::versionControlActions(const KFileItemList& items) const
0281 {
0282     if (items.count() == 1 && items.first().isDir()) {
0283         QString directory = items.first().localPath();
0284         if (!directory.endsWith(QLatin1Char('/'))) {
0285             directory += QLatin1Char('/');
0286         }
0287 
0288         if (directory == m_currentDir) {
0289             return contextMenuDirectoryActions(directory);
0290         } else {
0291             return contextMenuFilesActions(items);
0292         }
0293     } else {
0294         return contextMenuFilesActions(items);
0295     }
0296 }
0297 
0298 QList<QAction*> FileViewGitPlugin::outOfVersionControlActions(const KFileItemList& items) const
0299 {
0300     Q_UNUSED(items)
0301 
0302     return {};
0303 }
0304 
0305 QList<QAction*> FileViewGitPlugin::contextMenuFilesActions(const KFileItemList& items) const
0306 {
0307     Q_ASSERT(!items.isEmpty());
0308 
0309     if (!m_pendingOperation){
0310         m_contextDir = QFileInfo(items.first().localPath()).canonicalPath();
0311         m_contextItems.clear();
0312         for (const KFileItem& item : items){
0313             m_contextItems.append(item);
0314         }
0315 
0316         //see which actions should be enabled
0317         int versionedCount = 0;
0318         int addableCount = 0;
0319         int revertCount = 0;
0320         for (const KFileItem& item : items){
0321             const ItemVersion state = itemVersion(item);
0322             if (state != UnversionedVersion && state != RemovedVersion &&
0323                 state != IgnoredVersion) {
0324                 ++versionedCount;
0325             }
0326             if (state == UnversionedVersion || state == LocallyModifiedUnstagedVersion ||
0327                 state == IgnoredVersion || state == ConflictingVersion) {
0328                 ++addableCount;
0329             }
0330             if (state == LocallyModifiedVersion || state == LocallyModifiedUnstagedVersion ||
0331                 state == ConflictingVersion) {
0332                 ++revertCount;
0333             }
0334         }
0335 
0336         m_logAction->setEnabled(versionedCount == items.count());
0337         m_addAction->setEnabled(addableCount == items.count());
0338         m_revertAction->setEnabled(revertCount == items.count());
0339         m_removeAction->setEnabled(versionedCount == items.count());
0340     }
0341     else{
0342         m_logAction->setEnabled(false);
0343         m_addAction->setEnabled(false);
0344         m_revertAction->setEnabled(false);
0345         m_removeAction->setEnabled(false);
0346     }
0347 
0348     QList<QAction*> actions;
0349     actions.append(m_logAction);
0350     actions.append(m_addAction);
0351     actions.append(m_removeAction);
0352     actions.append(m_revertAction);
0353     return actions;
0354 }
0355 
0356 QList<QAction*> FileViewGitPlugin::contextMenuDirectoryActions(const QString& directory) const
0357 {
0358     QList<QAction*> actions;
0359     if (!m_pendingOperation) {
0360         m_contextItems.clear();
0361         m_contextDir = directory;
0362     }
0363     m_checkoutAction->setEnabled(!m_pendingOperation);
0364     actions.append(m_checkoutAction);
0365 
0366     bool canCommit = false;
0367     bool showChanges = false;
0368     bool shouldMerge = false;
0369     QHash<QString, ItemVersion>::const_iterator it = m_versionInfoHash.constBegin();
0370     while (it != m_versionInfoHash.constEnd()) {
0371         const ItemVersion state = it.value();
0372         if (state == LocallyModifiedVersion || state == AddedVersion || state == RemovedVersion) {
0373             canCommit = true;
0374         }
0375         if (state == LocallyModifiedUnstagedVersion || state == LocallyModifiedVersion) {
0376             showChanges = true;
0377         }
0378         if (state == ConflictingVersion) {
0379             canCommit = false;
0380             showChanges = true;
0381             shouldMerge = true;
0382             break;
0383         }
0384         ++it;
0385     }
0386 
0387     m_logAction->setEnabled(!m_pendingOperation);
0388     actions.append(m_logAction);
0389 
0390     m_showLocalChangesAction->setEnabled(!m_pendingOperation && showChanges);
0391     actions.append(m_showLocalChangesAction);
0392 
0393     if (!shouldMerge) {
0394         m_commitAction->setEnabled(!m_pendingOperation && canCommit);
0395         actions.append(m_commitAction);
0396     } else {
0397         m_mergeAction->setEnabled(!m_pendingOperation);
0398         actions.append(m_mergeAction);
0399     }
0400 
0401     m_tagAction->setEnabled(!m_pendingOperation);
0402     actions.append(m_tagAction);
0403     m_pushAction->setEnabled(!m_pendingOperation);
0404     actions.append(m_pushAction);
0405     m_pullAction->setEnabled(!m_pendingOperation);
0406     actions.append(m_pullAction);
0407 
0408     return actions;
0409 }
0410 
0411 void FileViewGitPlugin::addFiles()
0412 {
0413     execGitCommand(QStringLiteral("add"), QStringList(),
0414                    xi18nd("@info:status", "Adding files to <application>Git</application> repository..."),
0415                    xi18nd("@info:status", "Adding files to <application>Git</application> repository failed."),
0416                    xi18nd("@info:status", "Added files to <application>Git</application> repository."));
0417 }
0418 
0419 void FileViewGitPlugin::removeFiles()
0420 {
0421     const QStringList arguments{
0422         QStringLiteral("-r"), //recurse through directories
0423         QStringLiteral("--force"), //also remove files that have not been committed yet
0424     };
0425     execGitCommand(QStringLiteral("rm"), arguments,
0426                    xi18nd("@info:status", "Removing files from <application>Git</application> repository..."),
0427                    xi18nd("@info:status", "Removing files from <application>Git</application> repository failed."),
0428                    xi18nd("@info:status", "Removed files from <application>Git</application> repository."));
0429 }
0430 
0431 void FileViewGitPlugin::revertFiles()
0432 {
0433     execGitCommand(QStringLiteral("checkout"), { QStringLiteral("--") },
0434                    xi18nd("@info:status", "Reverting files from <application>Git</application> repository..."),
0435                    xi18nd("@info:status", "Reverting files from <application>Git</application> repository failed."),
0436                    xi18nd("@info:status", "Reverted files from <application>Git</application> repository."));
0437 }
0438 
0439 void FileViewGitPlugin::showLocalChanges()
0440 {
0441     Q_ASSERT(!m_contextDir.isEmpty());
0442 
0443     runCommand(QStringLiteral("git difftool --dir-diff ."));
0444 }
0445 
0446 void FileViewGitPlugin::showDiff(const QUrl &link)
0447 {
0448     if (link.scheme() != QLatin1String("rev")) {
0449         return;
0450     }
0451     runCommand(QStringLiteral("git difftool --dir-diff %1^ %1").arg(link.path()));
0452 }
0453 
0454 void FileViewGitPlugin::log()
0455 {
0456     QStringList items;
0457     if (m_contextItems.isEmpty()) {
0458         items << QStringLiteral(".");
0459     } else {
0460         for (auto &item : std::as_const(m_contextItems)) {
0461             items << item.url().fileName();
0462         }
0463     }
0464 
0465     QProcess process;
0466     process.setWorkingDirectory(m_contextDir);
0467     process.start(
0468         QStringLiteral("git"),
0469         QStringList {
0470             QStringLiteral("log"),
0471             QStringLiteral("--date=format:%d-%m-%Y"),
0472             QStringLiteral("-n 100"),
0473             QStringLiteral("--pretty=format:<tr> <td><a href=\"rev:%h\">%h</a></td> <td>%ad</td> <td>%s</td> <td>%an</td> </tr>")
0474         } + items
0475     );
0476 
0477     if (!process.waitForFinished() || process.exitCode() != 0) {
0478         Q_EMIT errorMessage(xi18nd("@info:status", "<application>Git</application> Log failed."));
0479         return;
0480     }
0481 
0482     const QString gitOutput = QString::fromLocal8Bit(process.readAllStandardOutput());
0483 
0484     QPalette palette;
0485     const QString styleSheet = QStringLiteral(
0486         "body { background: %1; color: %2; }" \
0487         "table.logtable td { padding: 9px 8px 9px; }" \
0488         "a { color: %3; }" \
0489         "a:visited { color: %4; } "
0490     ).arg(palette.window().color().name(),
0491           palette.text().color().name(),
0492           palette.link().color().name(),
0493           palette.linkVisited().color().name());
0494 
0495     QDialog* dlg = new QDialog(m_parentWidget);
0496     QVBoxLayout* layout = new QVBoxLayout;
0497     auto view = new QTextBrowser(dlg);
0498     layout->addWidget(view);
0499     dlg->setLayout(layout);
0500     dlg->setAttribute(Qt::WA_DeleteOnClose);
0501     dlg->setWindowTitle(xi18nd("@title:window", "<application>Git</application> Log"));
0502     view->setOpenLinks(false);
0503     view->setOpenExternalLinks(false);
0504     connect(view, &QTextBrowser::anchorClicked, this, &FileViewGitPlugin::showDiff);
0505     view->setHtml(QStringLiteral(
0506         "<html>" \
0507         "<style> %1 </style>" \
0508         "<table class=\"logtable\">" \
0509         "<tr bgcolor=\"%2\">" \
0510         "<td> %3 </td> <td> %4 </td> <td> %5 </p> </td> <td> %6 </td>" \
0511         "</tr>" \
0512         "%7" \
0513         "</table>" \
0514         "</html>"
0515     ).arg(styleSheet,
0516           palette.highlight().color().name(),
0517           i18nc("Git commit hash", "Commit"),
0518           i18nc("Git commit date", "Date"),
0519           i18nc("Git commit message", "Message"),
0520           i18nc("Git commit author", "Author"),
0521           gitOutput));
0522 
0523     dlg->resize(QSize(720, 560));
0524     dlg->show();
0525 }
0526 
0527 void FileViewGitPlugin::merge()
0528 {
0529     Q_ASSERT(!m_contextDir.isEmpty());
0530 
0531     runCommand(QStringLiteral("git mergetool"));
0532 }
0533 
0534 void FileViewGitPlugin::checkout()
0535 {
0536     CheckoutDialog dialog(m_parentWidget);
0537     if (dialog.exec() == QDialog::Accepted){
0538         QProcess process;
0539         process.setWorkingDirectory(m_contextDir);
0540         QStringList arguments;
0541         arguments << QStringLiteral("checkout");
0542         if (dialog.force()) {
0543             arguments << QStringLiteral("-f");
0544         }
0545         const QString newBranchName = dialog.newBranchName();
0546         if (!newBranchName.isEmpty()) {
0547             arguments << QStringLiteral("-b");
0548             arguments << newBranchName;
0549         }
0550         const QString checkoutIdentifier = dialog.checkoutIdentifier();
0551         if (!checkoutIdentifier.isEmpty()) {
0552             arguments << checkoutIdentifier;
0553         }
0554         //to appear in messages
0555         const QString currentBranchName = newBranchName.isEmpty() ? checkoutIdentifier : newBranchName;
0556         process.start(QStringLiteral("git"), arguments);
0557         process.setReadChannel(QProcess::StandardError); //git writes info messages to stderr as well
0558         QString completedMessage;
0559         while (process.waitForReadyRead()) {
0560             char buffer[512];
0561             while (process.readLine(buffer, sizeof(buffer)) > 0){
0562                 const QString currentLine = QString::fromLocal8Bit(buffer);
0563                 if (currentLine.startsWith(QLatin1String("Switched to branch"))) {
0564                     completedMessage = xi18nd("@info:status", "Switched to branch '%1'", currentBranchName);
0565                 }
0566                 if (currentLine.startsWith(QLatin1String("HEAD is now at"))) {
0567                     const QString headIdentifier = currentLine.
0568                         mid(QLatin1String("HEAD is now at ").size()).trimmed();
0569                     completedMessage = xi18nd("@info:status Git HEAD pointer, parameter includes "
0570                     "short SHA-1 & commit message ", "HEAD is now at %1", headIdentifier);
0571                 }
0572                 //special output for checkout -b
0573                 if (currentLine.startsWith(QLatin1String("Switched to a new branch"))) {
0574                     completedMessage = xi18nd("@info:status", "Switched to a new branch '%1'", currentBranchName);
0575                 }
0576             }
0577         }
0578         if (process.exitCode() == 0 && process.exitStatus() == QProcess::NormalExit) {
0579             if (!completedMessage.isEmpty()) {
0580                 Q_EMIT operationCompletedMessage(completedMessage);
0581                 Q_EMIT itemVersionsChanged();
0582             }
0583         }
0584         else {
0585             Q_EMIT errorMessage(xi18nd("@info:status", "<application>Git</application> Checkout failed."
0586             " Maybe your working directory is dirty."));
0587         }
0588     }
0589 }
0590 
0591 void FileViewGitPlugin::commit()
0592 {
0593     CommitDialog dialog(m_parentWidget);
0594     if (dialog.exec() == QDialog::Accepted) {
0595         QTemporaryFile tmpCommitMessageFile;
0596         tmpCommitMessageFile.open();
0597         tmpCommitMessageFile.write(dialog.commitMessage());
0598         tmpCommitMessageFile.close();
0599         QProcess process;
0600         process.setWorkingDirectory(m_contextDir);
0601         QStringList args = {QStringLiteral("commit")};
0602         if (dialog.amend()) {
0603             args << QStringLiteral("--amend");
0604         }
0605         args << QStringLiteral("-F");
0606         args << tmpCommitMessageFile.fileName();
0607         process.start(QStringLiteral("git"), args);
0608         QString completedMessage;
0609         while (process.waitForReadyRead()){
0610             char buffer[512];
0611             while (process.readLine(buffer, sizeof(buffer)) > 0) {
0612                 if (strlen(buffer) > 0 && buffer[0] == '[') {
0613                     completedMessage = QTextCodec::codecForLocale()->toUnicode(buffer).trimmed();
0614                     break;
0615                 }
0616             }
0617         }
0618         if (!completedMessage.isEmpty()) {
0619             Q_EMIT operationCompletedMessage(completedMessage);
0620             Q_EMIT itemVersionsChanged();
0621         }
0622     }
0623 }
0624 
0625 void FileViewGitPlugin::createTag()
0626 {
0627    TagDialog dialog(m_parentWidget);
0628     if (dialog.exec() == QDialog::Accepted) {
0629         QTemporaryFile tempTagMessageFile;
0630         tempTagMessageFile.open();
0631         tempTagMessageFile.write(dialog.tagMessage());
0632         tempTagMessageFile.close();
0633         QProcess process;
0634         process.setWorkingDirectory(m_contextDir);
0635         process.setReadChannel(QProcess::StandardError);
0636         process.start(QStringLiteral("git"), {QStringLiteral("tag"), QStringLiteral("-a"), QStringLiteral("-F"), tempTagMessageFile.fileName(), dialog.tagName(), dialog.baseBranch()});
0637         QString completedMessage;
0638         bool gotTagAlreadyExistsMessage = false;
0639         while (process.waitForReadyRead()) {
0640             char buffer[512];
0641             while (process.readLine(buffer, sizeof(buffer)) > 0) {
0642                 const QString line = QString::fromLocal8Bit(buffer);
0643                 if (line.contains(QLatin1String("already exists"))) {
0644                     gotTagAlreadyExistsMessage = true;
0645                 }
0646             }
0647         }
0648         if (process.exitCode() == 0 && process.exitStatus() == QProcess::NormalExit) {
0649             completedMessage = xi18nd("@info:status","Successfully created tag '%1'", dialog.tagName());
0650             Q_EMIT operationCompletedMessage(completedMessage);
0651         } else {
0652             //I don't know any other error, but in case one occurs, the user doesn't get FALSE error messages
0653             Q_EMIT errorMessage(gotTagAlreadyExistsMessage ?
0654                 xi18nd("@info:status", "<application>Git</application> tag creation failed."
0655                                       " A tag with the name '%1' already exists.", dialog.tagName()) :
0656                 xi18nd("@info:status", "<application>Git</application> tag creation failed.")
0657             );
0658         }
0659     }
0660 }
0661 
0662 void FileViewGitPlugin::push()
0663 {
0664     PushDialog dialog(m_parentWidget);
0665     if (dialog.exec() == QDialog::Accepted) {
0666         m_process.setWorkingDirectory(m_contextDir);
0667 
0668         m_errorMsg = xi18nd("@info:status", "Pushing branch %1 to %2:%3 failed.",
0669                         dialog.localBranch(), dialog.destination(), dialog.remoteBranch());
0670         m_operationCompletedMsg = xi18nd("@info:status", "Pushed branch %1 to %2:%3.",
0671                         dialog.localBranch(), dialog.destination(), dialog.remoteBranch());
0672         Q_EMIT infoMessage(xi18nd("@info:status", "Pushing branch %1 to %2:%3...",
0673                  dialog.localBranch(), dialog.destination(), dialog.remoteBranch()));
0674 
0675         m_command = QStringLiteral("push");
0676         m_pendingOperation = true;
0677         QStringList args;
0678         args << QStringLiteral("push");
0679         if (dialog.force()) {
0680             args << QStringLiteral("--force");
0681         }
0682         args << dialog.destination();
0683         args << QStringLiteral("%1:%2").arg(dialog.localBranch(), dialog.remoteBranch());
0684         m_process.start(QStringLiteral("git"), args);
0685     }
0686 }
0687 
0688 void FileViewGitPlugin::pull()
0689 {
0690     PullDialog dialog(m_parentWidget);
0691     if (dialog.exec()  == QDialog::Accepted) {
0692         m_process.setWorkingDirectory(m_contextDir);
0693 
0694         m_errorMsg = xi18nd("@info:status", "Pulling branch %1 from %2 failed.",
0695                         dialog.remoteBranch(), dialog.source());
0696         m_operationCompletedMsg = xi18nd("@info:status", "Pulled branch %1 from %2 successfully.",
0697                         dialog.remoteBranch(), dialog.source());
0698         Q_EMIT infoMessage(xi18nd("@info:status", "Pulling branch %1 from %2...", dialog.remoteBranch(),
0699                     dialog.source()));
0700 
0701         m_command = QStringLiteral("pull");
0702         m_pendingOperation = true;
0703         m_process.start(QStringLiteral("git"), {QStringLiteral("pull"), dialog.source(), dialog.remoteBranch()});
0704     }
0705 }
0706 
0707 void FileViewGitPlugin::slotOperationCompleted(int exitCode, QProcess::ExitStatus exitStatus)
0708 {
0709     m_pendingOperation = false;
0710 
0711     QString message;
0712     if (m_command == QLatin1String("push")) { //output parsing for push
0713         message = parsePushOutput();
0714         m_command = QString();
0715     }
0716     if (m_command == QLatin1String("pull")) {
0717         message = parsePullOutput();
0718         m_command = QString();
0719     }
0720 
0721     if ((exitStatus != QProcess::NormalExit) || (exitCode != 0)) {
0722         Q_EMIT errorMessage(message.isNull() ? m_errorMsg : message);
0723     } else if (m_contextItems.isEmpty()) {
0724         Q_EMIT operationCompletedMessage(message.isNull() ? m_operationCompletedMsg : message);
0725         Q_EMIT itemVersionsChanged();
0726     } else {
0727         startGitCommandProcess();
0728     }
0729 }
0730 
0731 void FileViewGitPlugin::slotOperationError()
0732 {
0733     // don't do any operation on other items anymore
0734     m_contextItems.clear();
0735     m_pendingOperation = false;
0736 
0737     Q_EMIT errorMessage(m_errorMsg);
0738 }
0739 
0740 QString FileViewGitPlugin::parsePushOutput()
0741 {
0742     m_process.setReadChannel(QProcess::StandardError);
0743     QString message;
0744     char buffer[256];
0745     while (m_process.readLine(buffer, sizeof(buffer)) > 0) {
0746         const QString line = QString::fromLocal8Bit(buffer);
0747         if (line.contains(QLatin1String("->")) || (line.contains(QLatin1String("fatal")) && message.isNull())) {
0748             message = line.trimmed();
0749         }
0750         if (line.contains(QLatin1String("Everything up-to-date")) && message.isNull()) {
0751             message = xi18nd("@info:status", "Branch is already up-to-date.");
0752         }
0753     }
0754     return message;
0755 }
0756 
0757 QString FileViewGitPlugin::parsePullOutput()
0758 {
0759     char buffer[256];
0760     while (m_process.readLine(buffer, sizeof(buffer)) > 0) {
0761         const QString line = QString::fromLocal8Bit(buffer);
0762         if (line.contains(QLatin1String("Already up-to-date"))) {
0763             return xi18nd("@info:status", "Branch is already up-to-date.");
0764         }
0765         if (line.contains(QLatin1String("CONFLICT"))) {
0766             Q_EMIT itemVersionsChanged();
0767             return xi18nd("@info:status", "Merge conflicts occurred. Fix them and commit the result.");
0768         }
0769     }
0770     return QString();
0771 }
0772 
0773 void FileViewGitPlugin::execGitCommand(const QString& gitCommand,
0774                                        const QStringList& arguments,
0775                                        const QString& infoMsg,
0776                                        const QString& errorMsg,
0777                                        const QString& operationCompletedMsg)
0778 {
0779     Q_EMIT infoMessage(infoMsg);
0780 
0781     m_command = gitCommand;
0782     m_arguments = arguments;
0783     m_errorMsg = errorMsg;
0784     m_operationCompletedMsg = operationCompletedMsg;
0785 
0786     startGitCommandProcess();
0787 }
0788 
0789 
0790 void FileViewGitPlugin::startGitCommandProcess()
0791 {
0792     Q_ASSERT(!m_contextItems.isEmpty());
0793     Q_ASSERT(m_process.state() == QProcess::NotRunning);
0794     m_pendingOperation = true;
0795 
0796     const KFileItem item = m_contextItems.takeLast();
0797     m_process.setWorkingDirectory(m_contextDir);
0798     QStringList arguments;
0799     arguments << m_command;
0800     arguments << m_arguments;
0801     //force explicitly selected files but no files in selected directories
0802     if (m_command == QLatin1String("add") && !item.isDir()){
0803         arguments<< QStringLiteral("-f");
0804     }
0805     arguments << item.url().fileName();
0806     m_process.start(QStringLiteral("git"), arguments);
0807     // the remaining items of m_contextItems will be executed
0808     // after the process has finished (see slotOperationFinished())
0809 }
0810 
0811 #include "fileviewgitplugin.moc"
0812 
0813 #include "moc_fileviewgitplugin.cpp"