File indexing completed on 2024-04-28 05:49:06

0001 /*
0002     SPDX-FileCopyrightText: 2021 Waqar Ahmed <waqar.17a@gmail.com>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "gitwidget.h"
0008 #include "branchcheckoutdialog.h"
0009 #include "branchdeletedialog.h"
0010 #include "branchesdialog.h"
0011 #include "comparebranchesview.h"
0012 #include "diffparams.h"
0013 #include "gitcommitdialog.h"
0014 #include "gitstatusmodel.h"
0015 #include "hostprocess.h"
0016 #include "kateproject.h"
0017 #include "kateprojectplugin.h"
0018 #include "kateprojectpluginview.h"
0019 #include "ktexteditor_utils.h"
0020 #include "pushpulldialog.h"
0021 #include "stashdialog.h"
0022 
0023 #include <commitfilesview.h>
0024 #include <gitprocess.h>
0025 
0026 #include <KColorScheme>
0027 #include <QContextMenuEvent>
0028 #include <QDialog>
0029 #include <QEvent>
0030 #include <QGuiApplication>
0031 #include <QHeaderView>
0032 #include <QInputDialog>
0033 #include <QInputMethodEvent>
0034 #include <QKeySequence>
0035 #include <QLineEdit>
0036 #include <QMenu>
0037 #include <QPainter>
0038 #include <QPushButton>
0039 #include <QStyledItemDelegate>
0040 #include <QToolButton>
0041 #include <QTreeView>
0042 #include <QVBoxLayout>
0043 #include <QtConcurrentRun>
0044 
0045 #include <KActionCollection>
0046 #include <KLocalizedString>
0047 #include <KMessageBox>
0048 #include <kwidgetsaddons_version.h>
0049 
0050 #include <KColorUtils>
0051 #include <KFuzzyMatcher>
0052 #include <KSyntaxHighlighting/Definition>
0053 #include <KSyntaxHighlighting/Repository>
0054 #include <KTextEditor/Editor>
0055 #include <KTextEditor/MainWindow>
0056 #include <KTextEditor/Message>
0057 #include <KTextEditor/View>
0058 
0059 static QString ksshaskpass()
0060 {
0061     static const QString res = safeExecutableName(QStringLiteral("ksshaskpass"));
0062     return res;
0063 }
0064 
0065 static void adjustColorContrast(QColor &bg, QColor &fg)
0066 {
0067     if (KColorUtils::contrastRatio(bg, fg) < 3.0) {
0068         if (KColorUtils::luma(fg) > KColorUtils::luma(bg)) {
0069             fg = KColorUtils::lighten(fg);
0070         } else {
0071             fg = KColorUtils::darken(fg);
0072         }
0073     }
0074 }
0075 
0076 class NumStatStyle final : public QStyledItemDelegate
0077 {
0078     static const int RightMargin = 2;
0079 
0080 public:
0081     explicit NumStatStyle(QObject *parent)
0082         : QStyledItemDelegate(parent)
0083     {
0084     }
0085 
0086     void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
0087     {
0088         const auto strs = index.data().toString().split(QLatin1Char(' '));
0089         if (strs.count() < 3) {
0090             return QStyledItemDelegate::paint(painter, option, index);
0091         }
0092 
0093         QStyleOptionViewItem options = option;
0094         initStyleOption(&options, index);
0095         painter->save();
0096 
0097         options.text = QString(); // clear old text
0098         options.widget->style()->drawControl(QStyle::CE_ItemViewItem, &options, painter, options.widget);
0099 
0100         const QString add = strs.at(0) + QStringLiteral(" ");
0101         const QString sub = strs.at(1) + QStringLiteral(" ");
0102         const QString Status = strs.at(2);
0103 
0104         int ha = option.fontMetrics.horizontalAdvance(add);
0105         int hs = option.fontMetrics.horizontalAdvance(sub);
0106         int hS = option.fontMetrics.horizontalAdvance(Status);
0107 
0108         QRect r = option.rect;
0109         int mw = r.width() - (ha + hs + hS + RightMargin); // 2px margin on the right
0110         r.setX(r.x() + mw);
0111 
0112         KColorScheme c;
0113         auto red = c.foreground(KColorScheme::NegativeText).color();
0114         auto green = c.foreground(KColorScheme::PositiveText).color();
0115         if (options.state.testFlag(QStyle::State_Selected) && options.state.testFlag(QStyle::State_Active)) {
0116             auto bg = options.palette.highlight().color();
0117             adjustColorContrast(bg, red);
0118             adjustColorContrast(bg, green);
0119         }
0120 
0121         painter->setPen(green);
0122         painter->drawText(r, Qt::AlignVCenter, add);
0123         r.setX(r.x() + ha);
0124 
0125         painter->setPen(red);
0126         painter->drawText(r, Qt::AlignVCenter, sub);
0127         r.setX(r.x() + hs);
0128 
0129         painter->setPen(index.data(Qt::ForegroundRole).value<QColor>());
0130         painter->drawText(r, Qt::AlignVCenter, Status);
0131 
0132         painter->restore();
0133     }
0134 
0135     QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override
0136     {
0137         const auto str = index.data().toString();
0138         auto sh = QStyledItemDelegate::sizeHint(option, index);
0139         sh.setWidth(option.fontMetrics.horizontalAdvance(str) + RightMargin);
0140         return sh;
0141     }
0142 };
0143 
0144 class StatusProxyModel : public QSortFilterProxyModel
0145 {
0146 public:
0147     using QSortFilterProxyModel::QSortFilterProxyModel;
0148 
0149     static bool isTopLevel(const QModelIndex &idx)
0150     {
0151         return !idx.isValid();
0152     }
0153 
0154     bool filterAcceptsRow(int sourceRow, const QModelIndex &parent) const override
0155     {
0156         // top level node
0157         auto index = sourceModel()->index(sourceRow, 0, parent);
0158         if (isTopLevel(parent)) {
0159             // Staged are always visible
0160             if (index.row() == GitStatusModel::ItemType::NodeStage)
0161                 return true;
0162 
0163             // otherwise visible only if rowCount > 0
0164             return sourceModel()->rowCount(index) > 0;
0165         }
0166 
0167         if (!index.isValid()) {
0168             return false;
0169         }
0170 
0171         // no pattern => everything visible
0172         if (m_text.isEmpty()) {
0173             return true;
0174         }
0175 
0176         const QString file = index.data().toString();
0177         // not using score atm
0178         return KFuzzyMatcher::matchSimple(m_text, file);
0179     }
0180 
0181     void setFilterText(const QString &text)
0182     {
0183         beginResetModel();
0184         m_text = text;
0185         endResetModel();
0186     }
0187 
0188 private:
0189     QString m_text;
0190 };
0191 
0192 class GitWidgetTreeView : public QTreeView
0193 {
0194 public:
0195     GitWidgetTreeView(QWidget *parent)
0196         : QTreeView(parent)
0197     {
0198     }
0199 
0200     // we want no branches!
0201     void drawBranches(QPainter *, const QRect &, const QModelIndex &) const override
0202     {
0203     }
0204 };
0205 
0206 static QToolButton *toolButton(Qt::ToolButtonStyle t = Qt::ToolButtonIconOnly)
0207 {
0208     auto tb = new QToolButton;
0209     tb->setAutoRaise(true);
0210     tb->setToolButtonStyle(t);
0211     tb->setSizePolicy(QSizePolicy::Minimum, tb->sizePolicy().verticalPolicy());
0212     return tb;
0213 }
0214 
0215 static QToolButton *toolButton(const QString &icon, const QString &tooltip, const QString &text = QString(), Qt::ToolButtonStyle t = Qt::ToolButtonIconOnly)
0216 {
0217     auto tb = toolButton(t);
0218     tb->setToolTip(tooltip);
0219     tb->setIcon(QIcon::fromTheme(icon));
0220     tb->setText(text);
0221     return tb;
0222 }
0223 
0224 GitWidget::GitWidget(KateProject *project, KTextEditor::MainWindow *mainWindow, KateProjectPluginView *pluginView)
0225     : m_project(project)
0226     , m_mainWin(mainWindow)
0227     , m_pluginView(pluginView)
0228     , m_mainView(new QWidget(this))
0229     , m_stackWidget(new QStackedWidget(this))
0230 {
0231     // We init delayed when the widget will be shown
0232 }
0233 
0234 void GitWidget::showEvent(QShowEvent *e)
0235 {
0236     init();
0237     selectActiveFileInStatus();
0238     QWidget::showEvent(e);
0239 }
0240 
0241 void GitWidget::init()
0242 {
0243     if (m_initialized) {
0244         return;
0245     }
0246     m_initialized = true;
0247 
0248     setDotGitPath();
0249 
0250     m_treeView = new GitWidgetTreeView(this);
0251     auto ac = m_pluginView->actionCollection();
0252 
0253     buildMenu(ac);
0254     m_menuBtn = toolButton(QStringLiteral("application-menu"), QString());
0255     m_menuBtn->setMenu(m_gitMenu);
0256     m_menuBtn->setArrowType(Qt::NoArrow);
0257     m_menuBtn->setStyleSheet(QStringLiteral("QToolButton::menu-indicator{ image: none; }"));
0258     connect(m_menuBtn, &QToolButton::clicked, this, [this](bool) {
0259         m_menuBtn->showMenu();
0260     });
0261 
0262     const QString &commitText = i18n("Commit");
0263     const QIcon &commitIcon = QIcon::fromTheme(QStringLiteral("vcs-commit"));
0264     m_commitBtn = toolButton(Qt::ToolButtonTextBesideIcon);
0265     m_commitBtn->setIcon(commitIcon);
0266     m_commitBtn->setText(commitText);
0267     m_commitBtn->setToolTip(commitText);
0268     m_commitBtn->setMinimumHeight(16);
0269 
0270     const QString &pushText = i18n("Git Push");
0271     m_pushBtn = toolButton();
0272     auto a = ac->addAction(QStringLiteral("vcs_push"), this, [this]() {
0273         PushPullDialog ppd(m_mainWin, m_activeGitDirPath);
0274         connect(&ppd, &PushPullDialog::runGitCommand, this, &GitWidget::runPushPullCmd);
0275         ppd.openDialog(PushPullDialog::Push);
0276     });
0277     a->setIcon(QIcon::fromTheme(QStringLiteral("vcs-push")));
0278     a->setText(pushText);
0279     a->setToolTip(pushText);
0280     ac->setDefaultShortcut(a, QKeySequence(QStringLiteral("Ctrl+T, P"), QKeySequence::PortableText));
0281     m_pushBtn->setDefaultAction(a);
0282 
0283     const QString &pullText = i18n("Git Pull");
0284     m_pullBtn = toolButton(QStringLiteral("vcs-pull"), pullText);
0285     a = ac->addAction(QStringLiteral("vcs_pull"), this, [this]() {
0286         PushPullDialog ppd(m_mainWin, m_activeGitDirPath);
0287         connect(&ppd, &PushPullDialog::runGitCommand, this, &GitWidget::runPushPullCmd);
0288         ppd.openDialog(PushPullDialog::Pull);
0289     });
0290     ac->setDefaultShortcut(a, QKeySequence(QStringLiteral("Ctrl+T, U"), QKeySequence::PortableText));
0291     a->setIcon(QIcon::fromTheme(QStringLiteral("vcs-pull")));
0292     a->setText(pullText);
0293     a->setToolTip(pullText);
0294     m_pullBtn->setDefaultAction(a);
0295 
0296     m_cancelBtn = toolButton(QStringLiteral("dialog-cancel"), i18n("Cancel Operation"));
0297     m_cancelBtn->setHidden(true);
0298     connect(m_cancelBtn, &QToolButton::clicked, this, [this] {
0299         if (m_cancelHandle) {
0300             // we don't want error occurred, this is intentional
0301             disconnect(m_cancelHandle, &QProcess::errorOccurred, nullptr, nullptr);
0302             const auto args = m_cancelHandle->arguments();
0303             m_cancelHandle->kill();
0304             sendMessage(QStringLiteral("git ") + args.join(QLatin1Char(' ')) + i18n(" canceled."), false);
0305             hideCancel();
0306         }
0307     });
0308 
0309     QVBoxLayout *layout = new QVBoxLayout;
0310     layout->setSpacing(0);
0311     layout->setContentsMargins(0, 0, 0, 0);
0312 
0313     QHBoxLayout *btnsLayout = new QHBoxLayout;
0314     btnsLayout->setContentsMargins(0, 0, 0, 0);
0315 
0316     for (auto *btn : {m_commitBtn, m_cancelBtn, m_pushBtn, m_pullBtn, m_menuBtn}) {
0317         btnsLayout->addWidget(btn);
0318     }
0319     btnsLayout->setStretch(0, 1);
0320 
0321     layout->addLayout(btnsLayout);
0322     layout->addWidget(m_treeView);
0323 
0324     m_filterLineEdit = new QLineEdit(this);
0325     m_filterLineEdit->setPlaceholderText(i18n("Filter..."));
0326     m_filterLineEdit->setProperty("_breeze_borders_sides", QVariant::fromValue(QFlags{Qt::TopEdge}));
0327     layout->addWidget(m_filterLineEdit);
0328 
0329     m_model = new GitStatusModel(this);
0330     auto proxy = new StatusProxyModel(this);
0331     proxy->setSourceModel(m_model);
0332 
0333     connect(m_filterLineEdit, &QLineEdit::textChanged, proxy, &StatusProxyModel::setFilterText);
0334     connect(m_filterLineEdit, &QLineEdit::textChanged, m_treeView, &QTreeView::expandAll);
0335 
0336     m_treeView->setUniformRowHeights(true);
0337     m_treeView->setHeaderHidden(true);
0338     m_treeView->setSelectionMode(QTreeView::ExtendedSelection);
0339     m_treeView->setModel(proxy);
0340     m_treeView->installEventFilter(this);
0341     m_treeView->setRootIsDecorated(false);
0342     m_treeView->setAllColumnsShowFocus(true);
0343     m_treeView->setProperty("_breeze_borders_sides", QVariant::fromValue(QFlags{Qt::TopEdge}));
0344     m_treeView->expandAll();
0345 
0346     if (m_treeView->style()) {
0347         auto indent = m_treeView->style()->pixelMetric(QStyle::PM_TreeViewIndentation, nullptr, m_treeView);
0348         m_treeView->setIndentation(indent / 4);
0349     }
0350 
0351     m_treeView->header()->setStretchLastSection(false);
0352     m_treeView->header()->setSectionResizeMode(0, QHeaderView::Stretch);
0353 
0354     m_treeView->setItemDelegateForColumn(1, new NumStatStyle(this));
0355 
0356     // our main view - status view + btns
0357     m_mainView->setLayout(layout);
0358 
0359     a = ac->addAction(QStringLiteral("vcs_commit"), this, [this] {
0360         openCommitChangesDialog();
0361         slotUpdateStatus();
0362     });
0363     ac->setDefaultShortcut(a, QKeySequence(QStringLiteral("Ctrl+T, K"), QKeySequence::PortableText));
0364     a->setText(commitText);
0365     a->setToolTip(commitText);
0366     a->setIcon(commitIcon);
0367 
0368     connect(&m_gitStatusWatcher, &QFutureWatcher<GitUtils::GitParsedStatus>::finished, this, &GitWidget::parseStatusReady);
0369     connect(m_commitBtn, &QPushButton::clicked, this, &GitWidget::openCommitChangesDialog);
0370     // This may not needed anylonger, but we do it anyway, just to be on the save side, see e239cb310
0371     connect(m_commitBtn, &QPushButton::pressed, this, &GitWidget::slotUpdateStatus);
0372 
0373     // single / double click
0374     connect(m_treeView, &QTreeView::clicked, this, &GitWidget::treeViewSingleClicked);
0375     connect(m_treeView, &QTreeView::doubleClicked, this, &GitWidget::treeViewDoubleClicked);
0376 
0377     m_stackWidget->addWidget(m_mainView);
0378 
0379     // This Widget's layout
0380     setLayout(new QVBoxLayout);
0381     this->layout()->addWidget(m_stackWidget);
0382     this->layout()->setContentsMargins(0, 0, 0, 0);
0383 
0384     // Setup update protection
0385     m_updateTrigger.setSingleShot(true);
0386     m_updateTrigger.setInterval(500);
0387     connect(&m_updateTrigger, &QTimer::timeout, this, &GitWidget::slotUpdateStatus);
0388     slotUpdateStatus();
0389 
0390     connect(m_mainWin, &KTextEditor::MainWindow::viewChanged, this, &GitWidget::setActiveGitDir);
0391     connect(m_mainWin, &KTextEditor::MainWindow::viewChanged, this, &GitWidget::selectActiveFileInStatus);
0392 }
0393 
0394 GitWidget::~GitWidget()
0395 {
0396     if (m_cancelHandle) {
0397         m_cancelHandle->kill();
0398         m_cancelHandle->waitForFinished();
0399     }
0400 
0401     // if there are any living processes, disconnect them now before gitwidget get destroyed
0402     for (QObject *child : children()) {
0403         QProcess *p = qobject_cast<QProcess *>(child);
0404         if (p) {
0405             disconnect(p, nullptr, nullptr, nullptr);
0406         }
0407     }
0408 }
0409 
0410 void GitWidget::setDotGitPath()
0411 {
0412     const auto dotGitPath = getRepoBasePath(m_project->baseDir());
0413     if (!dotGitPath.has_value()) {
0414         QTimer::singleShot(1, this, [this] {
0415             sendMessage(i18n("Failed to find .git directory for '%1', things may not work correctly", m_project->baseDir()), false);
0416         });
0417         m_topLevelGitPath = m_project->baseDir();
0418         return;
0419     }
0420 
0421     m_topLevelGitPath = dotGitPath.value();
0422     m_activeGitDirPath = m_topLevelGitPath;
0423 
0424     QMetaObject::invokeMethod(this, &GitWidget::setSubmodulesPaths, Qt::QueuedConnection);
0425 }
0426 
0427 void GitWidget::setSubmodulesPaths()
0428 {
0429     // git submodule foreach --recursive -q git rev-parse --show-toplevel
0430     QStringList args{QStringLiteral("submodule"),
0431                      QStringLiteral("foreach"),
0432                      QStringLiteral("--recursive"),
0433                      QStringLiteral("-q"),
0434                      QStringLiteral("git"),
0435                      QStringLiteral("rev-parse"),
0436                      QStringLiteral("--show-toplevel")};
0437     auto git = gitp(args);
0438     startHostProcess(*git, QProcess::ReadOnly);
0439     connect(git, &QProcess::finished, this, [this, git](int exitCode, QProcess::ExitStatus es) {
0440         if (es != QProcess::NormalExit || exitCode != 0) {
0441             // no error on status failure
0442             sendMessage(QString::fromUtf8(git->readAllStandardError()), true);
0443         } else {
0444             QString s = QString::fromUtf8(git->readAllStandardOutput());
0445             static const QRegularExpression lineEndings(QStringLiteral("\r\n?"));
0446             s.replace(lineEndings, QStringLiteral("\n"));
0447             m_submodulePaths = s.split(QLatin1Char('\n'), Qt::SkipEmptyParts);
0448             for (auto &p : m_submodulePaths) {
0449                 if (!p.endsWith(QLatin1Char('/'))) {
0450                     p.append(QLatin1Char('/'));
0451                 }
0452             }
0453             // Sort by size so that we can early out on matching paths later.
0454             std::sort(m_submodulePaths.begin(), m_submodulePaths.end(), [](const QString &l, const QString &r) {
0455                 return l.size() > r.size();
0456             });
0457             setActiveGitDir();
0458         }
0459         git->deleteLater();
0460     });
0461 }
0462 
0463 void GitWidget::selectActiveFileInStatus()
0464 {
0465     auto av = m_mainWin->activeView();
0466     if (!isVisible() || !av || !av->document() || !av->document()->url().isValid()) {
0467         return;
0468     }
0469 
0470     const QString path = av->document()->url().toLocalFile();
0471     if (path.isEmpty()) {
0472         return;
0473     }
0474 
0475     const auto currIdxPath = m_treeView->currentIndex().data(GitStatusModel::FileNameRole).toString();
0476     if (!currIdxPath.isEmpty() && path.endsWith(currIdxPath)) {
0477         return;
0478     }
0479 
0480     const auto index = m_model->indexForFilename(path);
0481     auto proxy = qobject_cast<QSortFilterProxyModel *>(m_treeView->model());
0482     auto viewIdx = proxy->mapFromSource(index);
0483     if (viewIdx.isValid()) {
0484         auto oldValue = m_treeView->blockSignals(true);
0485         m_treeView->setCurrentIndex(viewIdx);
0486         m_treeView->blockSignals(oldValue);
0487         auto par = proxy->index(viewIdx.parent().row(), 0);
0488         if (!m_treeView->isExpanded(par)) {
0489             m_treeView->expand(par);
0490         }
0491         m_treeView->scrollTo(viewIdx);
0492     }
0493 }
0494 
0495 void GitWidget::setActiveGitDir()
0496 {
0497     // No submodules
0498     if (m_submodulePaths.size() <= 1) {
0499         return;
0500     }
0501 
0502     auto av = m_mainWin->activeView();
0503     if (!av || !av->document() || !av->document()->url().isValid()) {
0504         return;
0505     }
0506 
0507     int idx = 0;
0508     int activeSubmoduleIdx = -1;
0509     const QString path = av->document()->url().toLocalFile();
0510     for (const auto &submodulePath : std::as_const(m_submodulePaths)) {
0511         if (path.startsWith(submodulePath)) {
0512             activeSubmoduleIdx = idx;
0513             break;
0514         }
0515         idx++;
0516     }
0517 
0518     if (activeSubmoduleIdx == -1) {
0519         if (m_activeGitDirPath != m_topLevelGitPath) {
0520             m_activeGitDirPath = m_topLevelGitPath;
0521             updateStatus();
0522         }
0523         return;
0524     }
0525 
0526     // Only trigger update if this is a different path
0527     auto foundPath = m_submodulePaths.at(activeSubmoduleIdx);
0528     if (foundPath != m_activeGitDirPath) {
0529         m_activeGitDirPath = foundPath;
0530         updateStatus();
0531     }
0532 }
0533 
0534 void GitWidget::sendMessage(const QString &plainText, bool warn)
0535 {
0536     Utils::showMessage(plainText, gitIcon(), i18n("Git"), warn ? MessageType::Error : MessageType::Info);
0537 }
0538 
0539 KTextEditor::MainWindow *GitWidget::mainWindow()
0540 {
0541     return m_mainWin;
0542 }
0543 
0544 QProcess *GitWidget::gitp(const QStringList &arguments)
0545 {
0546     auto git = new QProcess(this);
0547     setupGitProcess(*git, m_activeGitDirPath, arguments);
0548     connect(git, &QProcess::errorOccurred, this, [this, git](QProcess::ProcessError pe) {
0549         // git program missing is not an error
0550         sendMessage(git->errorString(), pe != QProcess::FailedToStart);
0551         git->deleteLater();
0552     });
0553     return git;
0554 }
0555 
0556 void GitWidget::updateStatus()
0557 {
0558     if (m_initialized) {
0559         m_updateTrigger.start();
0560     }
0561 }
0562 
0563 void GitWidget::slotUpdateStatus()
0564 {
0565     if (!isVisible()) {
0566         return; // No need to update
0567     }
0568 
0569     const auto args = QStringList{QStringLiteral("status"), QStringLiteral("-z"), QStringLiteral("-u")};
0570 
0571     auto git = gitp(args);
0572     connect(git, &QProcess::finished, this, [this, git](int exitCode, QProcess::ExitStatus es) {
0573         if (es != QProcess::NormalExit || exitCode != 0) {
0574             // no error on status failure
0575             // sendMessage(QString::fromUtf8(git->readAllStandardError()), true);
0576         } else {
0577             auto future = QtConcurrent::run(GitUtils::parseStatus, git->readAllStandardOutput(), m_activeGitDirPath);
0578             m_gitStatusWatcher.setFuture(future);
0579         }
0580         git->deleteLater();
0581     });
0582     startHostProcess(*git, QProcess::ReadOnly);
0583 }
0584 
0585 void GitWidget::runGitCmd(const QStringList &args, const QString &i18error)
0586 {
0587     auto git = gitp(args);
0588     connect(git, &QProcess::finished, this, [this, i18error, git](int exitCode, QProcess::ExitStatus es) {
0589         if (es != QProcess::NormalExit || exitCode != 0) {
0590             sendMessage(i18error + QStringLiteral(": ") + QString::fromUtf8(git->readAllStandardError()), true);
0591         } else {
0592             updateStatus();
0593         }
0594         git->deleteLater();
0595     });
0596     startHostProcess(*git, QProcess::ReadOnly);
0597 }
0598 
0599 void GitWidget::runPushPullCmd(const QStringList &args)
0600 {
0601     auto git = gitp(args);
0602     // Honor the user's SSH_ASKPASS env if set
0603     QString pass = QString::fromUtf8(qgetenv("SSH_ASKPASS"));
0604     // otherwise try to use ksshaskpass
0605     if (pass.isEmpty()) {
0606         pass = ksshaskpass();
0607     }
0608 
0609     if (!pass.isEmpty()) {
0610         QStringList env = QProcess::systemEnvironment();
0611         env.append(QStringLiteral("SSH_ASKPASS=%1").arg(pass));
0612         // put this fu**ing env var in so the above one is actually honored
0613         env.append(QStringLiteral("SSH_ASKPASS_REQUIRE=force"));
0614         git->setEnvironment(env);
0615     }
0616     git->setProcessChannelMode(QProcess::MergedChannels);
0617 
0618     connect(git, &QProcess::finished, this, [this, args, git](int exitCode, QProcess::ExitStatus es) {
0619         if (es != QProcess::NormalExit || exitCode != 0) {
0620             sendMessage(QStringLiteral("git ") + args.first() + i18n(" error: %1", QString::fromUtf8(git->readAll())), true);
0621         } else {
0622             auto gargs = args;
0623             gargs.push_front(QStringLiteral("git"));
0624             QString cmd = gargs.join(QStringLiteral(" "));
0625             QString out = QString::fromUtf8(git->readAll());
0626             sendMessage(i18n("\"%1\" executed successfully: %2", cmd, out), false);
0627             updateStatus();
0628         }
0629         hideCancel();
0630         git->deleteLater();
0631     });
0632 
0633     enableCancel(git);
0634     startHostProcess(*git, QProcess::ReadOnly);
0635 }
0636 
0637 void GitWidget::stage(const QStringList &files, bool)
0638 {
0639     if (files.isEmpty()) {
0640         return;
0641     }
0642 
0643     auto args = QStringList{QStringLiteral("add"), QStringLiteral("-A"), QStringLiteral("--")};
0644     args.append(files);
0645 
0646     runGitCmd(args, i18n("Failed to stage file. Error:"));
0647 }
0648 
0649 void GitWidget::unstage(const QStringList &files)
0650 {
0651     if (files.isEmpty()) {
0652         return;
0653     }
0654 
0655     // git reset -q HEAD --
0656     auto args = QStringList{QStringLiteral("reset"), QStringLiteral("-q"), QStringLiteral("HEAD"), QStringLiteral("--")};
0657     args.append(files);
0658 
0659     runGitCmd(args, i18n("Failed to unstage file. Error:"));
0660 }
0661 
0662 void GitWidget::discard(const QStringList &files)
0663 {
0664     if (files.isEmpty()) {
0665         return;
0666     }
0667     // discard=>git checkout -q -- xx.cpp
0668     auto args = QStringList{QStringLiteral("checkout"), QStringLiteral("-q"), QStringLiteral("--")};
0669     args.append(files);
0670     runGitCmd(args, i18n("Failed to discard changes. Error:"));
0671 }
0672 
0673 void GitWidget::clean(const QStringList &files)
0674 {
0675     if (files.isEmpty()) {
0676         return;
0677     }
0678     // discard=>git clean -q -f -- xx.cpp
0679     auto args = QStringList{QStringLiteral("clean"), QStringLiteral("-q"), QStringLiteral("-f"), QStringLiteral("--")};
0680     args.append(files);
0681     runGitCmd(args, i18n("Failed to remove. Error:"));
0682 }
0683 
0684 void GitWidget::openAtHEAD(const QString &file)
0685 {
0686     if (file.isEmpty()) {
0687         return;
0688     }
0689 
0690     auto args = QStringList{QStringLiteral("show"), QStringLiteral("--textconv")};
0691     args.append(QStringLiteral(":") + file);
0692     auto git = gitp(args);
0693     startHostProcess(*git, QProcess::ReadOnly);
0694 
0695     connect(git, &QProcess::finished, this, [this, file, git](int exitCode, QProcess::ExitStatus es) {
0696         if (es != QProcess::NormalExit || exitCode != 0) {
0697             sendMessage(i18n("Failed to open file at HEAD: %1", QString::fromUtf8(git->readAllStandardError())), true);
0698         } else {
0699             auto view = m_mainWin->openUrl(QUrl());
0700             if (view) {
0701                 view->document()->setText(QString::fromUtf8(git->readAllStandardOutput()));
0702                 auto mode = KTextEditor::Editor::instance()->repository().definitionForFileName(file).name();
0703                 view->document()->setHighlightingMode(mode);
0704                 view->document()->setModified(false); // no save file dialog when closing
0705             }
0706         }
0707         git->deleteLater();
0708     });
0709 
0710     git->setArguments(args);
0711     startHostProcess(*git, QProcess::ReadOnly);
0712 }
0713 
0714 void GitWidget::showDiff(const QString &file, bool staged)
0715 {
0716     auto args = QStringList{QStringLiteral("diff")};
0717     if (staged) {
0718         args.append(QStringLiteral("--staged"));
0719     }
0720 
0721     if (!file.isEmpty()) {
0722         args.append(QStringLiteral("--"));
0723         args.append(file);
0724     }
0725 
0726     auto git = gitp(args);
0727     connect(git, &QProcess::finished, this, [this, file, staged, git](int exitCode, QProcess::ExitStatus es) {
0728         if (es != QProcess::NormalExit || exitCode != 0) {
0729             sendMessage(i18n("Failed to get Diff of file: %1", QString::fromUtf8(git->readAllStandardError())), true);
0730         } else {
0731             DiffParams d;
0732             d.srcFile = file;
0733             d.workingDir = m_activeGitDirPath;
0734             d.arguments = git->arguments();
0735             d.flags.setFlag(DiffParams::Flag::ShowStage, !staged);
0736             d.flags.setFlag(DiffParams::Flag::ShowUnstage, staged);
0737             d.flags.setFlag(DiffParams::Flag::ShowDiscard, !staged);
0738             d.flags.setFlag(DiffParams::Flag::ReloadOnShow, true);
0739             QPointer<GitWidget> _this = QPointer<GitWidget>(this);
0740             d.updateStatusCallback = [_this] {
0741                 if (_this) {
0742                     _this->updateStatus();
0743                 }
0744             };
0745             // When file is empty, we are showing diff of multiple file usually
0746             const bool showfile = file.isEmpty() && (staged ? m_model->stagedFiles().size() > 1 : m_model->changedFiles().size() > 1);
0747             d.flags.setFlag(DiffParams::Flag::ShowFileName, showfile);
0748             Utils::showDiff(git->readAllStandardOutput(), d, mainWindow());
0749         }
0750         git->deleteLater();
0751     });
0752     startHostProcess(*git, QProcess::ReadOnly);
0753 }
0754 
0755 void GitWidget::launchExternalDiffTool(const QString &file, bool staged)
0756 {
0757     if (file.isEmpty()) {
0758         return;
0759     }
0760 
0761     auto args = QStringList{QStringLiteral("difftool"), QStringLiteral("-y")};
0762     if (staged) {
0763         args.append(QStringLiteral("--staged"));
0764     }
0765     args.append(file);
0766 
0767     QProcess git;
0768     if (setupGitProcess(git, m_activeGitDirPath, args)) {
0769         git.startDetached();
0770     }
0771 }
0772 
0773 void GitWidget::commitChanges(const QString &msg, const QString &desc, bool signOff, bool amend)
0774 {
0775     auto args = QStringList{QStringLiteral("commit")};
0776 
0777     if (amend) {
0778         args.append(QStringLiteral("--amend"));
0779     }
0780 
0781     if (signOff) {
0782         args.append(QStringLiteral("-s"));
0783     }
0784 
0785     args.append(QStringLiteral("-m"));
0786     args.append(msg);
0787     if (!desc.isEmpty()) {
0788         args.append(QStringLiteral("-m"));
0789         args.append(desc);
0790     }
0791 
0792     auto git = gitp(args);
0793 
0794     connect(git, &QProcess::finished, this, [this, git](int exitCode, QProcess::ExitStatus es) {
0795         if (es != QProcess::NormalExit || exitCode != 0) {
0796             sendMessage(i18n("Failed to commit: %1", QString::fromUtf8(git->readAllStandardError())), true);
0797         } else {
0798             m_commitMessage.clear();
0799             updateStatus();
0800             sendMessage(i18n("Changes committed successfully."), false);
0801         }
0802         git->deleteLater();
0803     });
0804     startHostProcess(*git, QProcess::ReadOnly);
0805 }
0806 
0807 void GitWidget::openCommitChangesDialog(bool amend)
0808 {
0809     if (!amend && m_model->stagedFiles().isEmpty()) {
0810         return sendMessage(i18n("Nothing to commit. Please stage your changes first."), true);
0811     }
0812 
0813     GitCommitDialog *dialog = new GitCommitDialog(m_commitMessage, this);
0814 
0815     if (amend) {
0816         dialog->setAmendingCommit();
0817     }
0818 
0819     connect(dialog, &QDialog::finished, this, [this, dialog](int res) {
0820         dialog->deleteLater();
0821         if (res == QDialog::Accepted) {
0822             if (dialog->subject().isEmpty()) {
0823                 return sendMessage(i18n("Commit message cannot be empty."), true);
0824             }
0825             m_commitMessage = dialog->subject() + QStringLiteral("[[\n\n]]") + dialog->description();
0826             commitChanges(dialog->subject(), dialog->description(), dialog->signoff(), dialog->amendingLastCommit());
0827         }
0828     });
0829 
0830     dialog->open();
0831 }
0832 
0833 void GitWidget::handleClick(const QModelIndex &idx, ClickAction clickAction)
0834 {
0835     const auto type = idx.data(GitStatusModel::TreeItemType);
0836     if (type != GitStatusModel::NodeFile) {
0837         return;
0838     }
0839 
0840     if (clickAction == ClickAction::NoAction) {
0841         return;
0842     }
0843 
0844     const QString file = m_activeGitDirPath + idx.data(GitStatusModel::FileNameRole).toString();
0845     const auto statusItemType = idx.data(GitStatusModel::GitItemType).value<GitStatusModel::ItemType>();
0846     const bool staged = statusItemType == GitStatusModel::NodeStage;
0847 
0848     if (clickAction == ClickAction::StageUnstage) {
0849         if (staged) {
0850             return unstage({file});
0851         }
0852         return stage({file});
0853     }
0854 
0855     if (clickAction == ClickAction::ShowDiff && statusItemType != GitStatusModel::NodeUntrack) {
0856         showDiff(file, staged);
0857     }
0858 
0859     if (clickAction == ClickAction::OpenFile) {
0860         m_mainWin->openUrl(QUrl::fromLocalFile(file));
0861     }
0862 }
0863 
0864 void GitWidget::treeViewSingleClicked(const QModelIndex &idx)
0865 {
0866     if (qGuiApp->keyboardModifiers() == Qt::NoModifier) {
0867         handleClick(idx, m_pluginView->plugin()->singleClickAcion());
0868     }
0869 }
0870 
0871 void GitWidget::treeViewDoubleClicked(const QModelIndex &idx)
0872 {
0873     if (qGuiApp->keyboardModifiers() == Qt::NoModifier) {
0874         handleClick(idx, m_pluginView->plugin()->doubleClickAcion());
0875     }
0876 }
0877 
0878 void GitWidget::parseStatusReady()
0879 {
0880     // Remember collapse/expand state
0881     // The default is expanded, so only add here which should be not expanded
0882     std::map<int, bool> nodeIsExpanded;
0883     nodeIsExpanded[GitStatusModel::NodeUntrack] = false;
0884 
0885     const auto *model = m_treeView->model();
0886     for (int i = 0; i < model->rowCount(); ++i) {
0887         const auto index = model->index(i, 0);
0888         if (!index.isValid()) {
0889             continue;
0890         }
0891         const auto t = index.data(GitStatusModel::TreeItemType).toInt();
0892         nodeIsExpanded[t] = m_treeView->isExpanded(index);
0893     }
0894 
0895     // Set new data
0896     m_model->setStatusItems(m_gitStatusWatcher.result());
0897 
0898     // Restore collapse/expand state
0899     for (int i = 0; i < model->rowCount(); ++i) {
0900         const auto index = model->index(i, 0);
0901         if (!index.isValid()) {
0902             continue;
0903         }
0904         const auto t = index.data(GitStatusModel::TreeItemType).toInt();
0905         if (nodeIsExpanded.find(t) == nodeIsExpanded.end() || nodeIsExpanded[t]) {
0906             m_treeView->expand(index);
0907         } else {
0908             m_treeView->collapse(index);
0909         }
0910     }
0911 
0912     m_treeView->resizeColumnToContents(0);
0913     m_treeView->resizeColumnToContents(1);
0914 
0915     selectActiveFileInStatus();
0916 }
0917 
0918 void GitWidget::branchCompareFiles(const QString &from, const QString &to)
0919 {
0920     if (from.isEmpty() && to.isEmpty()) {
0921         return;
0922     }
0923 
0924     // git diff br...br2 --name-only -z
0925     auto args = QStringList{QStringLiteral("diff"), QStringLiteral("%1...%2").arg(from).arg(to), QStringLiteral("--name-status")};
0926 
0927     QProcess git;
0928 
0929     // early out if we can't find git
0930     if (!setupGitProcess(git, m_activeGitDirPath, args)) {
0931         return;
0932     }
0933 
0934     startHostProcess(git, QProcess::ReadOnly);
0935     if (git.waitForStarted() && git.waitForFinished(-1)) {
0936         if (git.exitStatus() != QProcess::NormalExit || git.exitCode() != 0) {
0937             return;
0938         }
0939     }
0940 
0941     const QByteArray diff = git.readAllStandardOutput();
0942     if (diff.isEmpty()) {
0943         sendMessage(i18n("No diff for %1...%2", from, to), false);
0944         return;
0945     }
0946 
0947     auto filesWithNameStatus = GitUtils::parseDiffNameStatus(diff);
0948     if (filesWithNameStatus.isEmpty()) {
0949         sendMessage(i18n("Failed to compare %1...%2", from, to), true);
0950         return;
0951     }
0952 
0953     // get --num-stat
0954     args = QStringList{QStringLiteral("diff"), QStringLiteral("%1...%2").arg(from).arg(to), QStringLiteral("--numstat"), QStringLiteral("-z")};
0955 
0956     // early out if we can't find git
0957     if (!setupGitProcess(git, m_activeGitDirPath, args)) {
0958         return;
0959     }
0960 
0961     startHostProcess(git, QProcess::ReadOnly);
0962     if (git.waitForStarted() && git.waitForFinished(-1)) {
0963         if (git.exitStatus() != QProcess::NormalExit || git.exitCode() != 0) {
0964             sendMessage(i18n("Failed to get numstat when diffing %1...%2", from, to), true);
0965             return;
0966         }
0967     }
0968 
0969     GitUtils::parseDiffNumStat(filesWithNameStatus, git.readAllStandardOutput());
0970 
0971     CompareBranchesView *w = new CompareBranchesView(this, m_activeGitDirPath, from, to, filesWithNameStatus);
0972     w->setPluginView(m_pluginView);
0973     connect(w, &CompareBranchesView::backClicked, this, [this] {
0974         auto x = m_stackWidget->currentWidget();
0975         if (x) {
0976             m_stackWidget->setCurrentWidget(m_mainView);
0977             x->deleteLater();
0978         }
0979     });
0980     m_stackWidget->addWidget(w);
0981     m_stackWidget->setCurrentWidget(w);
0982 }
0983 
0984 bool GitWidget::eventFilter(QObject *o, QEvent *e)
0985 {
0986     if (e->type() == QEvent::ContextMenu) {
0987         if (o != m_treeView)
0988             return QWidget::eventFilter(o, e);
0989         QContextMenuEvent *cme = static_cast<QContextMenuEvent *>(e);
0990         treeViewContextMenuEvent(cme);
0991     }
0992     return QWidget::eventFilter(o, e);
0993 }
0994 
0995 void GitWidget::buildMenu(KActionCollection *ac)
0996 {
0997     m_gitMenu = new QMenu(this);
0998     auto a = ac->addAction(QStringLiteral("vcs_status_refresh"), this, [this] {
0999         if (m_project) {
1000             updateStatus();
1001         }
1002     });
1003     a->setText(i18n("Refresh"));
1004     a->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh")));
1005     m_gitMenu->addAction(a);
1006 
1007     a = ac->addAction(QStringLiteral("vcs_amend"), this, [this] {
1008         openCommitChangesDialog(/* amend = */ true);
1009     });
1010     a->setIcon(QIcon::fromTheme(QStringLiteral("document-edit")));
1011     a->setText(i18n("Amend Last Commit"));
1012     ac->setDefaultShortcut(a, QKeySequence(QStringLiteral("Ctrl+T, Ctrl+K"), QKeySequence::PortableText));
1013     m_gitMenu->addAction(a);
1014 
1015     a = ac->addAction(QStringLiteral("vcs_branch_checkout"), this, [this] {
1016         BranchCheckoutDialog bd(m_mainWin->window(), m_project->baseDir());
1017         bd.openDialog();
1018     });
1019     a->setText(i18n("Checkout Branch"));
1020     a->setIcon(QIcon::fromTheme(QStringLiteral("vcs-branch")));
1021     ac->setDefaultShortcut(a, QKeySequence(QStringLiteral("Ctrl+T, C"), QKeySequence::PortableText));
1022     m_gitMenu->addAction(a);
1023 
1024     a = ac->addAction(QStringLiteral("vcs_branch_delete"), this, [this] {
1025         BranchDeleteDialog dlg(m_activeGitDirPath, this);
1026         if (dlg.exec() == QDialog::Accepted) {
1027             auto result = GitUtils::deleteBranches(dlg.branchesToDelete(), m_activeGitDirPath);
1028             sendMessage(result.error, result.returnCode != 0);
1029         }
1030     });
1031     a->setText(i18n("Delete Branch"));
1032     a->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete")));
1033     m_gitMenu->addAction(a);
1034 
1035     a = ac->addAction(QStringLiteral("vcs_branch_diff"), this, [this] {
1036         BranchesDialog bd(m_mainWin->window(), m_project->baseDir());
1037         using GitUtils::RefType;
1038         bd.openDialog(static_cast<GitUtils::RefType>(RefType::Head | RefType::Remote));
1039         QString branch = bd.branch();
1040         branchCompareFiles(branch, QString());
1041     });
1042     a->setIcon(QIcon::fromTheme(QStringLiteral("vcs-diff")));
1043     a->setText(i18n("Compare Branch with..."));
1044     m_gitMenu->addAction(a);
1045 
1046     a = ac->addAction(QStringLiteral("git_show_commit"), this, [this] {
1047         bool ok = false;
1048         const QString hash = QInputDialog::getText(this, i18n("Show Commit"), i18n("Commit hash"), QLineEdit::Normal, {}, &ok);
1049         if (ok && !hash.isEmpty()) {
1050             const QString base = m_activeGitDirPath;
1051             CommitView::openCommit(hash, base, m_mainWin);
1052         }
1053     });
1054     a->setIcon(QIcon::fromTheme(QStringLiteral("vcs-diff")));
1055     a->setText(i18n("Open Commit..."));
1056     m_gitMenu->addAction(a);
1057 
1058     auto stashMenu = m_gitMenu->addAction(QIcon::fromTheme(QStringLiteral("vcs-stash")), i18n("Stash"));
1059     stashMenu->setMenu(this->stashMenu(ac));
1060 }
1061 
1062 void GitWidget::createStashDialog(StashMode m, const QString &gitPath)
1063 {
1064     auto stashDialog = new StashDialog(this, mainWindow()->window(), gitPath);
1065     connect(stashDialog, &StashDialog::message, this, &GitWidget::sendMessage);
1066     connect(stashDialog, &StashDialog::showStashDiff, this, [this](const QByteArray &r) {
1067         DiffParams d;
1068         d.tabTitle = i18n("Diff - stash");
1069         d.workingDir = m_activeGitDirPath;
1070         Utils::showDiff(r, d, mainWindow());
1071     });
1072     connect(stashDialog, &StashDialog::done, this, [this, stashDialog] {
1073         updateStatus();
1074         stashDialog->deleteLater();
1075     });
1076     stashDialog->openDialog(m);
1077 }
1078 
1079 void GitWidget::enableCancel(QProcess *git)
1080 {
1081     m_cancelHandle = git;
1082     m_pushBtn->hide();
1083     m_pullBtn->hide();
1084     m_cancelBtn->show();
1085 }
1086 
1087 void GitWidget::hideCancel()
1088 {
1089     m_cancelBtn->hide();
1090     m_pushBtn->show();
1091     m_pullBtn->show();
1092 }
1093 
1094 QMenu *GitWidget::stashMenu(KActionCollection *ac)
1095 {
1096     QMenu *menu = new QMenu(this);
1097     auto a = stashMenuAction(ac, QStringLiteral("vcs_stash"), i18n("Stash"), StashMode::Stash);
1098     a->setIcon(QIcon::fromTheme(QStringLiteral("vcs-stash")));
1099     menu->addAction(a);
1100 
1101     a = stashMenuAction(ac, QStringLiteral("vcs_stash_pop_last"), i18n("Pop Last Stash"), StashMode::StashPopLast);
1102     a->setIcon(QIcon::fromTheme(QStringLiteral("vcs-stash-pop")));
1103     menu->addAction(a);
1104 
1105     a = stashMenuAction(ac, QStringLiteral("vcs_stash_pop"), i18n("Pop Stash"), StashMode::StashPop);
1106     a->setIcon(QIcon::fromTheme(QStringLiteral("vcs-stash-pop")));
1107     menu->addAction(a);
1108 
1109     menu->addAction(stashMenuAction(ac, QStringLiteral("vcs_stash_apply_last"), i18n("Apply Last Stash"), StashMode::StashApplyLast));
1110 
1111     a = stashMenuAction(ac, QStringLiteral("vcs_stash_keep_staged"), i18n("Stash (Keep Staged)"), StashMode::StashKeepIndex);
1112     a->setIcon(QIcon::fromTheme(QStringLiteral("vcs-stash")));
1113     menu->addAction(a);
1114 
1115     a = stashMenuAction(ac, QStringLiteral("vcs_stash_include_untracked"), i18n("Stash (Include Untracked)"), StashMode::StashUntrackIncluded);
1116     a->setIcon(QIcon::fromTheme(QStringLiteral("vcs-stash")));
1117     menu->addAction(a);
1118 
1119     menu->addAction(stashMenuAction(ac, QStringLiteral("vcs_stash_apply"), i18n("Apply Stash"), StashMode::StashApply));
1120     menu->addAction(stashMenuAction(ac, QStringLiteral("vcs_stash_drop"), i18n("Drop Stash"), StashMode::StashDrop));
1121     menu->addAction(stashMenuAction(ac, QStringLiteral("vcs_stash_show"), i18n("Show Stash Content"), StashMode::ShowStashContent));
1122 
1123     return menu;
1124 }
1125 
1126 QAction *GitWidget::stashMenuAction(KActionCollection *ac, const QString &name, const QString &text, StashMode m)
1127 {
1128     auto a = ac->addAction(name, this, [this, m] {
1129         createStashDialog(m, m_activeGitDirPath);
1130     });
1131     a->setText(text);
1132     return a;
1133 }
1134 
1135 static KMessageBox::ButtonCode confirm(GitWidget *_this, const QString &text, const KGuiItem &confirmItem)
1136 {
1137     return KMessageBox::questionTwoActions(_this, text, {}, confirmItem, KStandardGuiItem::cancel(), {}, KMessageBox::Dangerous);
1138 }
1139 
1140 void GitWidget::treeViewContextMenuEvent(QContextMenuEvent *e)
1141 {
1142     if (auto selModel = m_treeView->selectionModel()) {
1143         if (selModel->selectedRows().count() > 1) {
1144             return selectedContextMenu(e);
1145         }
1146     }
1147 
1148     const QPersistentModelIndex idx = m_treeView->indexAt(e->pos());
1149     if (!idx.isValid())
1150         return;
1151     auto treeItem = idx.data(GitStatusModel::TreeItemType);
1152 
1153     if (treeItem == GitStatusModel::NodeChanges || treeItem == GitStatusModel::NodeUntrack) {
1154         QMenu menu(this);
1155         bool untracked = treeItem == GitStatusModel::NodeUntrack;
1156 
1157         auto stageAct = menu.addAction(i18n("Stage All"));
1158 
1159         auto discardAct = untracked ? menu.addAction(i18n("Remove All")) : menu.addAction(i18n("Discard All"));
1160         discardAct->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete-remove")));
1161 
1162         auto ignoreAct = untracked ? menu.addAction(i18n("Open .gitignore")) : nullptr;
1163         auto diff = !untracked ? menu.addAction(QIcon::fromTheme(QStringLiteral("vcs-diff")), i18n("Show Diff")) : nullptr;
1164         // get files
1165         auto act = menu.exec(m_treeView->viewport()->mapToGlobal(e->pos()));
1166         if (!act) {
1167             return;
1168         }
1169 
1170         const QList<GitUtils::StatusItem> &items = untracked ? m_model->untrackedFiles() : m_model->changedFiles();
1171         QStringList files;
1172         files.reserve(items.size());
1173         std::transform(items.begin(), items.end(), std::back_inserter(files), [](const GitUtils::StatusItem &i) {
1174             return QString::fromUtf8(i.file);
1175         });
1176 
1177         if (act == stageAct) {
1178             stage(files, treeItem == GitStatusModel::NodeUntrack);
1179         } else if (act == discardAct && !untracked) {
1180             auto ret = confirm(this, i18n("Are you sure you want to remove these files?"), KStandardGuiItem::remove());
1181             if (ret == KMessageBox::PrimaryAction) {
1182                 discard(files);
1183             }
1184         } else if (act == discardAct && untracked) {
1185             auto ret = confirm(this, i18n("Are you sure you want to discard all changes?"), KStandardGuiItem::discard());
1186             if (ret == KMessageBox::PrimaryAction) {
1187                 clean(files);
1188             }
1189         } else if (untracked && act == ignoreAct) {
1190             const auto files = m_project->files();
1191             const auto it = std::find_if(files.cbegin(), files.cend(), [](const QString &s) {
1192                 if (s.contains(QStringLiteral(".gitignore"))) {
1193                     return true;
1194                 }
1195                 return false;
1196             });
1197             if (it != files.cend()) {
1198                 m_mainWin->openUrl(QUrl::fromLocalFile(*it));
1199             }
1200         } else if (!untracked && act == diff) {
1201             showDiff(QString(), false);
1202         }
1203     } else if (treeItem == GitStatusModel::NodeFile) {
1204         QMenu menu(this);
1205         const auto statusItemType = idx.data(GitStatusModel::GitItemType).value<GitStatusModel::ItemType>();
1206         const bool staged = statusItemType == GitStatusModel::NodeStage;
1207         const bool untracked = statusItemType == GitStatusModel::NodeUntrack;
1208 
1209         auto openFile = menu.addAction(i18n("Open File"));
1210         auto showDiffAct = untracked ? nullptr : menu.addAction(QIcon::fromTheme(QStringLiteral("vcs-diff")), i18n("Show Diff"));
1211         auto launchDifftoolAct = untracked ? nullptr : menu.addAction(QIcon::fromTheme(QStringLiteral("kdiff3")), i18n("Show in External Git Diff Tool"));
1212         auto openAtHead = untracked ? nullptr : menu.addAction(i18n("Open at HEAD"));
1213         auto stageAct = staged ? menu.addAction(i18n("Unstage File")) : menu.addAction(i18n("Stage File"));
1214         auto discardAct = staged ? nullptr : untracked ? menu.addAction(i18n("Remove")) : menu.addAction(i18n("Discard"));
1215         if (discardAct) {
1216             discardAct->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete-remove")));
1217         }
1218 
1219         auto act = menu.exec(m_treeView->viewport()->mapToGlobal(e->pos()));
1220         if (!act || !idx.isValid()) {
1221             return;
1222         }
1223 
1224         const QString file = m_activeGitDirPath + idx.data(GitStatusModel::FileNameRole).toString();
1225         if (act == stageAct) {
1226             if (staged) {
1227                 return unstage({file});
1228             }
1229             return stage({file});
1230         } else if (act == discardAct && !untracked) {
1231             auto ret = confirm(this, i18n("Are you sure you want to discard the changes in this file?"), KStandardGuiItem::discard());
1232             if (ret == KMessageBox::PrimaryAction) {
1233                 discard({file});
1234             }
1235         } else if (act == openAtHead && !untracked) {
1236             openAtHEAD(idx.data(GitStatusModel::FileNameRole).toString());
1237         } else if (showDiffAct && act == showDiffAct && !untracked) {
1238             showDiff(file, staged);
1239         } else if (act == discardAct && untracked) {
1240             auto ret = confirm(this, i18n("Are you sure you want to remove this file?"), KStandardGuiItem::remove());
1241             if (ret == KMessageBox::PrimaryAction) {
1242                 clean({file});
1243             }
1244         } else if (act == launchDifftoolAct) {
1245             launchExternalDiffTool(idx.data(GitStatusModel::FileNameRole).toString(), staged);
1246         } else if (act == openFile) {
1247             m_mainWin->openUrl(QUrl::fromLocalFile(file));
1248         }
1249     } else if (treeItem == GitStatusModel::NodeStage) {
1250         QMenu menu(this);
1251         auto stage = menu.addAction(i18n("Unstage All"));
1252         auto diff = menu.addAction(i18n("Show Diff"));
1253         auto model = m_treeView->model();
1254         bool disable = model->rowCount(idx) == 0;
1255         stage->setDisabled(disable);
1256         diff->setDisabled(disable);
1257 
1258         auto act = menu.exec(m_treeView->viewport()->mapToGlobal(e->pos()));
1259         if (!act) {
1260             return;
1261         }
1262 
1263         // git reset -q HEAD --
1264         if (act == stage) {
1265             const QList<GitUtils::StatusItem> &items = m_model->stagedFiles();
1266             QStringList files;
1267             files.reserve(items.size());
1268             std::transform(items.begin(), items.end(), std::back_inserter(files), [](const GitUtils::StatusItem &i) {
1269                 return QString::fromUtf8(i.file);
1270             });
1271             unstage(files);
1272         } else if (act == diff) {
1273             showDiff(QString(), true);
1274         }
1275     }
1276 }
1277 
1278 void GitWidget::selectedContextMenu(QContextMenuEvent *e)
1279 {
1280     QStringList files;
1281 
1282     bool selectionHasStagedItems = false;
1283     bool selectionHasChangedItems = false;
1284     bool selectionHasUntrackedItems = false;
1285 
1286     if (auto selModel = m_treeView->selectionModel()) {
1287         const auto idxList = selModel->selectedIndexes();
1288         for (const auto &idx : idxList) {
1289             // no context menu for multi selection of top level nodes
1290             const bool isTopLevel = idx.data(GitStatusModel::TreeItemType).value<GitStatusModel::ItemType>() != GitStatusModel::NodeFile;
1291             if (isTopLevel) {
1292                 return;
1293             }
1294 
1295             // what type of status item is this?
1296             auto type = idx.data(GitStatusModel::GitItemType).value<GitStatusModel::ItemType>();
1297 
1298             if (type == GitStatusModel::NodeStage) {
1299                 selectionHasStagedItems = true;
1300             } else if (type == GitStatusModel::NodeUntrack) {
1301                 selectionHasUntrackedItems = true;
1302             } else if (type == GitStatusModel::NodeChanges) {
1303                 selectionHasChangedItems = true;
1304             }
1305 
1306             files.append(idx.data(GitStatusModel::FileNameRole).toString());
1307         }
1308     }
1309 
1310     const bool selHasUnstagedItems = selectionHasUntrackedItems || selectionHasChangedItems;
1311 
1312     // cant allow both
1313     if (selHasUnstagedItems && selectionHasStagedItems) {
1314         return;
1315     }
1316 
1317     QMenu menu(this);
1318     auto stageAct = selectionHasStagedItems ? menu.addAction(i18n("Unstage Selected Files")) : menu.addAction(i18n("Stage Selected Files"));
1319     auto discardAct = selectionHasChangedItems && !selectionHasUntrackedItems ? menu.addAction(i18n("Discard Selected Files")) : nullptr;
1320     if (discardAct) {
1321         discardAct->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete-remove")));
1322     }
1323     auto removeAct = !selectionHasChangedItems && selectionHasUntrackedItems ? menu.addAction(i18n("Remove Selected Files")) : nullptr;
1324     auto execAct = menu.exec(m_treeView->viewport()->mapToGlobal(e->pos()));
1325     if (!execAct) {
1326         return;
1327     }
1328 
1329     if (execAct == stageAct) {
1330         if (selectionHasChangedItems || selectionHasUntrackedItems) {
1331             stage(files);
1332         } else {
1333             unstage(files);
1334         }
1335     } else if (selectionHasChangedItems && !selectionHasUntrackedItems && execAct == discardAct) {
1336         auto ret = confirm(this, i18n("Are you sure you want to discard the changes?"), KStandardGuiItem::discard());
1337         if (ret == KMessageBox::PrimaryAction) {
1338             discard(files);
1339         }
1340     } else if (!selectionHasChangedItems && selectionHasUntrackedItems && execAct == removeAct) {
1341         auto ret = confirm(this, i18n("Are you sure you want to remove these untracked changes?"), KStandardGuiItem::remove());
1342         if (ret == KMessageBox::PrimaryAction) {
1343             clean(files);
1344         }
1345     }
1346 }
1347 
1348 #include "moc_gitwidget.cpp"