gitwidget.cpp 34.7 KB
Newer Older
Waqar Ahmed's avatar
Waqar Ahmed committed
1
2
3
4
5
/*
    SPDX-FileCopyrightText: 2021 Waqar Ahmed <waqar.17a@gmail.com>

    SPDX-License-Identifier: LGPL-2.0-or-later
*/
6
#include "gitwidget.h"
Waqar Ahmed's avatar
Waqar Ahmed committed
7
#include "branchesdialog.h"
Waqar Ahmed's avatar
Waqar Ahmed committed
8
#include "git/gitdiff.h"
Waqar Ahmed's avatar
Waqar Ahmed committed
9
#include "gitcommitdialog.h"
10
11
#include "gitstatusmodel.h"
#include "kateproject.h"
12
#include "kateprojectpluginview.h"
Waqar Ahmed's avatar
Waqar Ahmed committed
13
#include "pushpulldialog.h"
Waqar Ahmed's avatar
Waqar Ahmed committed
14
#include "stashdialog.h"
15

Waqar Ahmed's avatar
Waqar Ahmed committed
16
#include <QContextMenuEvent>
17
#include <QCoreApplication>
18
#include <QDebug>
Waqar Ahmed's avatar
Waqar Ahmed committed
19
#include <QDialog>
Waqar Ahmed's avatar
Waqar Ahmed committed
20
#include <QEvent>
21
#include <QFileInfo>
22
#include <QHeaderView>
23
#include <QInputMethodEvent>
Waqar Ahmed's avatar
Waqar Ahmed committed
24
#include <QLineEdit>
Waqar Ahmed's avatar
Waqar Ahmed committed
25
#include <QMenu>
26
#include <QPainter>
Waqar Ahmed's avatar
Waqar Ahmed committed
27
#include <QPlainTextEdit>
28
29
30
#include <QProcess>
#include <QPushButton>
#include <QStringListModel>
31
#include <QStyledItemDelegate>
Waqar Ahmed's avatar
Waqar Ahmed committed
32
#include <QTimer>
33
#include <QToolButton>
34
35
36
37
#include <QTreeView>
#include <QVBoxLayout>
#include <QtConcurrentRun>

Waqar Ahmed's avatar
Waqar Ahmed committed
38
#include <KLocalizedString>
39
#include <KMessageBox>
40
#include <QPointer>
Waqar Ahmed's avatar
Waqar Ahmed committed
41

42
43
#include <KSyntaxHighlighting/Definition>
#include <KSyntaxHighlighting/Repository>
Waqar Ahmed's avatar
Waqar Ahmed committed
44
#include <KTextEditor/Application>
Waqar Ahmed's avatar
Waqar Ahmed committed
45
#include <KTextEditor/ConfigInterface>
46
#include <KTextEditor/Editor>
Waqar Ahmed's avatar
Waqar Ahmed committed
47
#include <KTextEditor/MainWindow>
Waqar Ahmed's avatar
Waqar Ahmed committed
48
#include <KTextEditor/Message>
Waqar Ahmed's avatar
Waqar Ahmed committed
49
#include <KTextEditor/View>
Waqar Ahmed's avatar
Waqar Ahmed committed
50

51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
class NumStatStyle final : public QStyledItemDelegate
{
public:
    NumStatStyle(QObject *parent, KateProjectPlugin *p)
        : QStyledItemDelegate(parent)
        , m_plugin(p)
    {
    }

    void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
    {
        if (!m_plugin->showGitStatusWithNumStat()) {
            return QStyledItemDelegate::paint(painter, option, index);
        }

        const auto strs = index.data().toString().split(QLatin1Char(' '));
        if (strs.count() < 3) {
            return QStyledItemDelegate::paint(painter, option, index);
        }

        QStyleOptionViewItem options = option;
        initStyleOption(&options, index);
        painter->save();

        // paint background
        if (option.state & QStyle::State_Selected) {
            painter->fillRect(option.rect, option.palette.highlight());
        } else {
            painter->fillRect(option.rect, option.palette.base());
        }

        options.text = QString(); // clear old text
        options.widget->style()->drawControl(QStyle::CE_ItemViewItem, &options, painter, options.widget);

        const QString add = strs.at(0) + QStringLiteral(" ");
        const QString sub = strs.at(1) + QStringLiteral(" ");
        const QString Status = strs.at(2);

        int ha = option.fontMetrics.horizontalAdvance(add);
        int hs = option.fontMetrics.horizontalAdvance(sub);
        int hS = option.fontMetrics.horizontalAdvance(Status);

        QRect r = option.rect;
        int mw = r.width() - (ha + hs + hS);
        r.setX(r.x() + mw);

        static constexpr auto red = QColor(237, 21, 21); // Breeze Danger Red
        static constexpr auto green = QColor(17, 209, 27); // Breeze Verdant Green

        painter->setPen(green);
        painter->drawText(r, Qt::AlignVCenter, add);
        r.setX(r.x() + ha);

        painter->setPen(red);
        painter->drawText(r, Qt::AlignVCenter, sub);
        r.setX(r.x() + hs);

        painter->setPen(index.data(Qt::ForegroundRole).value<QColor>());
        painter->drawText(r, Qt::AlignVCenter, Status);

        painter->restore();
    }

private:
    KateProjectPlugin *m_plugin;
};

118
119
120
121
122
123
124
125
126
127
128
129
130
131
class GitWidgetTreeView : public QTreeView
{
public:
    GitWidgetTreeView(QWidget *parent)
        : QTreeView(parent)
    {
    }

    // we want no branches!
    void drawBranches(QPainter *, const QRect &, const QModelIndex &) const override
    {
    }
};

Waqar Ahmed's avatar
Waqar Ahmed committed
132
133
134
135
136
137
138
139
140
141
142
143
static QToolButton *toolButton(const QString &icon, const QString &tooltip, const QString &text = QString(), Qt::ToolButtonStyle t = Qt::ToolButtonIconOnly)
{
    auto tb = new QToolButton;
    tb->setIcon(QIcon::fromTheme(icon));
    tb->setToolTip(tooltip);
    tb->setText(text);
    tb->setAutoRaise(true);
    tb->setToolButtonStyle(t);
    tb->setSizePolicy(QSizePolicy::Minimum, tb->sizePolicy().verticalPolicy());
    return tb;
}

Waqar Ahmed's avatar
Waqar Ahmed committed
144
145
GitWidget::GitWidget(KateProject *project, KTextEditor::MainWindow *mainWindow, KateProjectPluginView *pluginView)
    : m_project(project)
Waqar Ahmed's avatar
Waqar Ahmed committed
146
    , m_mainWin(mainWindow)
Waqar Ahmed's avatar
Waqar Ahmed committed
147
    , m_pluginView(pluginView)
148
{
Waqar Ahmed's avatar
Waqar Ahmed committed
149
    setDotGitPath();
150

Waqar Ahmed's avatar
Waqar Ahmed committed
151
    m_treeView = new GitWidgetTreeView(this);
152

153
    buildMenu();
Waqar Ahmed's avatar
Waqar Ahmed committed
154
    m_menuBtn = toolButton(QStringLiteral("application-menu"), QString());
155
    m_menuBtn->setMenu(m_gitMenu);
156
157
    m_menuBtn->setArrowType(Qt::NoArrow);
    m_menuBtn->setStyleSheet(QStringLiteral("QToolButton::menu-indicator{ image: none; }"));
158
159
160
161
    connect(m_menuBtn, &QToolButton::clicked, this, [this](bool) {
        m_menuBtn->showMenu();
    });

Waqar Ahmed's avatar
Waqar Ahmed committed
162
163
164
    m_commitBtn = toolButton(QStringLiteral("svn-commit"), QString(), i18n("Commit"), Qt::ToolButtonTextBesideIcon);

    m_pushBtn = toolButton(QStringLiteral("arrow-up"), i18n("Git push"));
Waqar Ahmed's avatar
Waqar Ahmed committed
165
166
167
168
169
170
    connect(m_pushBtn, &QToolButton::clicked, this, [this]() {
        PushPullDialog ppd(m_mainWin->window(), m_gitPath);
        connect(&ppd, &PushPullDialog::runGitCommand, this, &GitWidget::runPushPullCmd);
        ppd.openDialog(PushPullDialog::Push);
    });

Waqar Ahmed's avatar
Waqar Ahmed committed
171
    m_pullBtn = toolButton(QStringLiteral("arrow-down"), i18n("Git pull"));
Waqar Ahmed's avatar
Waqar Ahmed committed
172
173
174
175
176
177
    connect(m_pullBtn, &QToolButton::clicked, this, [this]() {
        PushPullDialog ppd(m_mainWin->window(), m_gitPath);
        connect(&ppd, &PushPullDialog::runGitCommand, this, &GitWidget::runPushPullCmd);
        ppd.openDialog(PushPullDialog::Pull);
    });

Waqar Ahmed's avatar
Waqar Ahmed committed
178
179
180
    m_cancelBtn = toolButton(QStringLiteral("dialog-cancel"), i18n("Cancel Operation"));
    m_cancelBtn->setHidden(true);
    connect(m_cancelBtn, &QToolButton::clicked, this, [this] {
Waqar Ahmed's avatar
Waqar Ahmed committed
181
        if (m_cancelHandle) {
Waqar Ahmed's avatar
Waqar Ahmed committed
182
            // we don't want error occurred, this is intentional
Waqar Ahmed's avatar
Waqar Ahmed committed
183
184
185
186
            disconnect(m_cancelHandle, &QProcess::errorOccurred, nullptr, nullptr);
            const auto args = m_cancelHandle->arguments();
            m_cancelHandle->kill();
            sendMessage(QStringLiteral("git ") + args.join(QLatin1Char(' ')) + i18n(" canceled."), false);
Waqar Ahmed's avatar
Waqar Ahmed committed
187
188
189
190
            hideCancel();
        }
    });

191
    QVBoxLayout *layout = new QVBoxLayout;
Waqar Ahmed's avatar
Waqar Ahmed committed
192
193
    layout->setSpacing(0);
    layout->setContentsMargins(0, 0, 0, 0);
Waqar Ahmed's avatar
Waqar Ahmed committed
194

195
    QHBoxLayout *btnsLayout = new QHBoxLayout;
Waqar Ahmed's avatar
Waqar Ahmed committed
196
    btnsLayout->setContentsMargins(0, 0, 0, 0);
197

Waqar Ahmed's avatar
Waqar Ahmed committed
198
199
200
    for (auto *btn : {m_commitBtn, m_cancelBtn, m_pushBtn, m_pullBtn, m_menuBtn}) {
        btnsLayout->addWidget(btn);
    }
Waqar Ahmed's avatar
Waqar Ahmed committed
201
    btnsLayout->setStretch(0, 1);
202
203
204
205
206

    layout->addLayout(btnsLayout);
    layout->addWidget(m_treeView);

    m_model = new GitStatusModel(this);
Waqar Ahmed's avatar
Waqar Ahmed committed
207

208
    m_treeView->setUniformRowHeights(true);
Waqar Ahmed's avatar
Waqar Ahmed committed
209
    m_treeView->setHeaderHidden(true);
210
    m_treeView->setSelectionMode(QTreeView::ExtendedSelection);
211
    m_treeView->setModel(m_model);
Waqar Ahmed's avatar
Waqar Ahmed committed
212
    m_treeView->installEventFilter(this);
213
    m_treeView->setRootIsDecorated(false);
Waqar Ahmed's avatar
Waqar Ahmed committed
214
215
216
217
218

    if (m_treeView->style()) {
        auto indent = m_treeView->style()->pixelMetric(QStyle::PM_TreeViewIndentation, nullptr, m_treeView);
        m_treeView->setIndentation(indent / 4);
    }
219

220
221
222
    m_treeView->header()->setStretchLastSection(false);
    m_treeView->header()->setSectionResizeMode(0, QHeaderView::Stretch);

223
224
    m_treeView->setItemDelegateForColumn(1, new NumStatStyle(this, m_pluginView->plugin()));

225
226
    setLayout(layout);

227
    connect(&m_gitStatusWatcher, &QFutureWatcher<GitUtils::GitParsedStatus>::finished, this, &GitWidget::parseStatusReady);
Waqar Ahmed's avatar
Waqar Ahmed committed
228
    connect(m_commitBtn, &QPushButton::clicked, this, &GitWidget::opencommitChangesDialog);
229
230
231
232

    // single / double click
    connect(m_treeView, &QTreeView::clicked, this, &GitWidget::treeViewSingleClicked);
    connect(m_treeView, &QTreeView::doubleClicked, this, &GitWidget::treeViewDoubleClicked);
233
234
}

Waqar Ahmed's avatar
Waqar Ahmed committed
235
236
GitWidget::~GitWidget()
{
Waqar Ahmed's avatar
Waqar Ahmed committed
237
238
    if (m_cancelHandle) {
        m_cancelHandle->kill();
Waqar Ahmed's avatar
Waqar Ahmed committed
239
240
241
    }
}

Waqar Ahmed's avatar
Waqar Ahmed committed
242
void GitWidget::setDotGitPath()
243
{
Waqar Ahmed's avatar
Waqar Ahmed committed
244
245
    /* This call is intentionally blocking because we need git path for everything else */
    QProcess git;
246
247
    git.setProgram(QStringLiteral("git"));
    git.setWorkingDirectory(m_project->baseDir());
248
    git.setArguments({QStringLiteral("rev-parse"), QStringLiteral("--absolute-git-dir")});
249
    git.start(QProcess::ReadOnly);
250
    if (git.waitForStarted() && git.waitForFinished(-1)) {
251
        if (git.exitStatus() != QProcess::NormalExit || git.exitCode() != 0) {
252
            sendMessage(i18n("Failed to find .git directory, things may not work correctly: %1", QString::fromUtf8(git.readAllStandardError())), true);
253
254
            m_gitPath = m_project->baseDir();
            return;
255
256
257
258
259
260
261
262
263
264
265
266
267
        }
        m_gitPath = QString::fromUtf8(git.readAllStandardOutput());
        if (m_gitPath.endsWith(QLatin1String("\n"))) {
            m_gitPath.remove(QLatin1String(".git\n"));
        } else {
            m_gitPath.remove(QLatin1String(".git"));
        }
        // set once, use everywhere
        // This is per project
        git.setWorkingDirectory(m_gitPath);
    }
}

Christoph Cullmann's avatar
Christoph Cullmann committed
268
void GitWidget::sendMessage(const QString &plainText, bool warn)
Waqar Ahmed's avatar
Waqar Ahmed committed
269
{
Christoph Cullmann's avatar
Christoph Cullmann committed
270
271
    // use generic output view
    QVariantMap genericMessage;
272
    genericMessage.insert(QStringLiteral("type"), warn ? QStringLiteral("Error") : QStringLiteral("Info"));
Christoph Cullmann's avatar
Christoph Cullmann committed
273
    genericMessage.insert(QStringLiteral("category"), i18n("Git"));
Christoph Cullmann's avatar
Christoph Cullmann committed
274
    genericMessage.insert(QStringLiteral("categoryIcon"), QIcon(QStringLiteral(":/icons/icons/sc-apps-git.svg")));
275
    genericMessage.insert(QStringLiteral("text"), plainText);
Christoph Cullmann's avatar
Christoph Cullmann committed
276
    Q_EMIT m_pluginView->message(genericMessage);
Waqar Ahmed's avatar
Waqar Ahmed committed
277
278
}

279
280
281
282
283
KTextEditor::MainWindow *GitWidget::mainWindow()
{
    return m_mainWin;
}

Waqar Ahmed's avatar
Waqar Ahmed committed
284
285
286
287
288
QProcess *GitWidget::gitp()
{
    auto git = new QProcess(this);
    git->setProgram(QStringLiteral("git"));
    git->setWorkingDirectory(m_gitPath);
Waqar Ahmed's avatar
Waqar Ahmed committed
289
290
291
292
    connect(git, &QProcess::errorOccurred, this, [this, git](QProcess::ProcessError) {
        sendMessage(git->errorString(), true);
        git->deleteLater();
    });
Waqar Ahmed's avatar
Waqar Ahmed committed
293
294
295
    return git;
}

296
void GitWidget::getStatus(bool untracked, bool submodules)
297
{
Waqar Ahmed's avatar
Waqar Ahmed committed
298
299
300
301
302
303
304
305
306
307
308
    auto git = gitp();
    connect(git, &QProcess::finished, this, [this, git](int exitCode, QProcess::ExitStatus es) {
        if (es != QProcess::NormalExit || exitCode != 0) {
            // no error on status failure
            //            sendMessage(QString::fromUtf8(git->readAllStandardError()), true);
        } else {
            auto future = QtConcurrent::run(GitUtils::parseStatus, git->readAllStandardOutput());
            m_gitStatusWatcher.setFuture(future);
        }
        git->deleteLater();
    });
309

310
311
312
313
314
315
    auto args = QStringList{QStringLiteral("status"), QStringLiteral("-z")};
    if (!untracked) {
        args.append(QStringLiteral("-uno"));
    } else {
        args.append(QStringLiteral("-u"));
    }
316
317
318
    if (!submodules) {
        args.append(QStringLiteral("--ignore-submodules"));
    }
Waqar Ahmed's avatar
Waqar Ahmed committed
319
320
    git->setArguments(args);
    git->start(QProcess::ReadOnly);
321
322
}

Waqar Ahmed's avatar
Waqar Ahmed committed
323
void GitWidget::runGitCmd(const QStringList &args, const QString &i18error)
324
{
Waqar Ahmed's avatar
Waqar Ahmed committed
325
326
    auto git = gitp();
    connect(git, &QProcess::finished, this, [this, i18error, git](int exitCode, QProcess::ExitStatus es) {
327
        if (es != QProcess::NormalExit || exitCode != 0) {
Waqar Ahmed's avatar
Waqar Ahmed committed
328
            sendMessage(i18error + QStringLiteral(": ") + QString::fromUtf8(git->readAllStandardError()), true);
Waqar Ahmed's avatar
Waqar Ahmed committed
329
330
331
        } else {
            getStatus();
        }
Waqar Ahmed's avatar
Waqar Ahmed committed
332
        git->deleteLater();
Waqar Ahmed's avatar
Waqar Ahmed committed
333
    });
Waqar Ahmed's avatar
Waqar Ahmed committed
334
335
    git->setArguments(args);
    git->start(QProcess::ReadOnly);
Waqar Ahmed's avatar
Waqar Ahmed committed
336
337
338
339
}

void GitWidget::runPushPullCmd(const QStringList &args)
{
Waqar Ahmed's avatar
Waqar Ahmed committed
340
    auto git = gitp();
Waqar Ahmed's avatar
Waqar Ahmed committed
341
342
    git->setProcessChannelMode(QProcess::MergedChannels);

Waqar Ahmed's avatar
Waqar Ahmed committed
343
    connect(git, &QProcess::finished, this, [this, args, git](int exitCode, QProcess::ExitStatus es) {
Waqar Ahmed's avatar
Waqar Ahmed committed
344
        if (es != QProcess::NormalExit || exitCode != 0) {
Waqar Ahmed's avatar
Waqar Ahmed committed
345
            sendMessage(QStringLiteral("git ") + args.first() + i18n(" error: %1", QString::fromUtf8(git->readAll())), true);
346
        } else {
Waqar Ahmed's avatar
Waqar Ahmed committed
347
348
349
            auto gargs = args;
            gargs.push_front(QStringLiteral("git"));
            QString cmd = gargs.join(QStringLiteral(" "));
Waqar Ahmed's avatar
Waqar Ahmed committed
350
            QString out = QString::fromUtf8(git->readAll());
Waqar Ahmed's avatar
Waqar Ahmed committed
351
            sendMessage(i18n("\"%1\" executed successfully: %2", cmd, out), false);
352
353
            getStatus();
        }
Waqar Ahmed's avatar
Waqar Ahmed committed
354
        hideCancel();
Waqar Ahmed's avatar
Waqar Ahmed committed
355
        git->deleteLater();
356
    });
Waqar Ahmed's avatar
Waqar Ahmed committed
357

Waqar Ahmed's avatar
Waqar Ahmed committed
358
    enableCancel(git);
Waqar Ahmed's avatar
Waqar Ahmed committed
359
360
    git->setArguments(args);
    git->start(QProcess::ReadOnly);
361
362
}

363
void GitWidget::stage(const QStringList &files, bool)
364
{
365
366
367
    if (files.isEmpty()) {
        return;
    }
368

369
    auto args = QStringList{QStringLiteral("add"), QStringLiteral("-A"), QStringLiteral("--")};
370
    args.append(files);
371

Waqar Ahmed's avatar
Waqar Ahmed committed
372
    runGitCmd(args, i18n("Failed to stage file. Error:"));
373
374
}

375
void GitWidget::unstage(const QStringList &files)
Waqar Ahmed's avatar
Waqar Ahmed committed
376
{
377
378
379
380
    if (files.isEmpty()) {
        return;
    }

Waqar Ahmed's avatar
Waqar Ahmed committed
381
382
    // git reset -q HEAD --
    auto args = QStringList{QStringLiteral("reset"), QStringLiteral("-q"), QStringLiteral("HEAD"), QStringLiteral("--")};
383
    args.append(files);
Waqar Ahmed's avatar
Waqar Ahmed committed
384

Waqar Ahmed's avatar
Waqar Ahmed committed
385
    runGitCmd(args, i18n("Failed to unstage file. Error:"));
Waqar Ahmed's avatar
Waqar Ahmed committed
386
387
}

388
389
390
391
392
393
394
395
void GitWidget::discard(const QStringList &files)
{
    if (files.isEmpty()) {
        return;
    }
    // discard=>git checkout -q -- xx.cpp
    auto args = QStringList{QStringLiteral("checkout"), QStringLiteral("-q"), QStringLiteral("--")};
    args.append(files);
Waqar Ahmed's avatar
Waqar Ahmed committed
396
    runGitCmd(args, i18n("Failed to discard changes. Error:"));
397
}
398

399
400
401
402
403
404
405
406
void GitWidget::clean(const QStringList &files)
{
    if (files.isEmpty()) {
        return;
    }
    // discard=>git clean -q -f -- xx.cpp
    auto args = QStringList{QStringLiteral("clean"), QStringLiteral("-q"), QStringLiteral("-f"), QStringLiteral("--")};
    args.append(files);
Waqar Ahmed's avatar
Waqar Ahmed committed
407
    runGitCmd(args, i18n("Failed to remove. Error:"));
408
409
}

410
411
412
413
414
415
void GitWidget::openAtHEAD(const QString &file)
{
    if (file.isEmpty()) {
        return;
    }

Waqar Ahmed's avatar
Waqar Ahmed committed
416
    auto git = gitp();
417
418
    auto args = QStringList{QStringLiteral("show"), QStringLiteral("--textconv")};
    args.append(QStringLiteral(":") + file);
Waqar Ahmed's avatar
Waqar Ahmed committed
419
420
    git->setArguments(args);
    git->start(QProcess::ReadOnly);
421

Waqar Ahmed's avatar
Waqar Ahmed committed
422
    connect(git, &QProcess::finished, this, [this, file, git](int exitCode, QProcess::ExitStatus es) {
423
        if (es != QProcess::NormalExit || exitCode != 0) {
Waqar Ahmed's avatar
Waqar Ahmed committed
424
            sendMessage(i18n("Failed to open file at HEAD: %1", QString::fromUtf8(git->readAllStandardError())), true);
425
        } else {
426
427
428
429
430
431
            auto view = m_mainWin->openUrl(QUrl());
            if (view && view->document()) {
                view->document()->setText(QString::fromUtf8(git->readAllStandardOutput()));
                auto mode = KTextEditor::Editor::instance()->repository().definitionForFileName(file).name();
                view->document()->setHighlightingMode(mode);
            }
Waqar Ahmed's avatar
Waqar Ahmed committed
432
        }
Waqar Ahmed's avatar
Waqar Ahmed committed
433
        git->deleteLater();
Waqar Ahmed's avatar
Waqar Ahmed committed
434
    });
Waqar Ahmed's avatar
Waqar Ahmed committed
435

Waqar Ahmed's avatar
Waqar Ahmed committed
436
437
    git->setArguments(args);
    git->start(QProcess::ReadOnly);
Waqar Ahmed's avatar
Waqar Ahmed committed
438
439
}

Waqar Ahmed's avatar
Waqar Ahmed committed
440
void GitWidget::showDiff(const QString &file, bool staged)
Waqar Ahmed's avatar
Waqar Ahmed committed
441
{
Waqar Ahmed's avatar
Waqar Ahmed committed
442
443
444
445
    auto args = QStringList{QStringLiteral("diff")};
    if (staged) {
        args.append(QStringLiteral("--staged"));
    }
446

447
448
449
450
    if (!file.isEmpty()) {
        args.append(QStringLiteral("--"));
        args.append(file);
    }
Waqar Ahmed's avatar
Waqar Ahmed committed
451

Waqar Ahmed's avatar
Waqar Ahmed committed
452
453
    auto git = gitp();
    connect(git, &QProcess::finished, this, [this, file, staged, git](int exitCode, QProcess::ExitStatus es) {
454
        if (es != QProcess::NormalExit || exitCode != 0) {
Waqar Ahmed's avatar
Waqar Ahmed committed
455
            sendMessage(i18n("Failed to get Diff of file: %1", QString::fromUtf8(git->readAllStandardError())), true);
Waqar Ahmed's avatar
Waqar Ahmed committed
456
        } else {
457
            const QString filename = file.isEmpty() ? QString() : QFileInfo(file).fileName();
458

459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
            auto addContextMenuActions = [this, file, staged](KTextEditor::View *v) {
                auto m = v->contextMenu();
                if (!staged) {
                    QMenu *menu = new QMenu(v);
                    auto sh = menu->addAction(i18n("Stage Hunk"));
                    auto sl = menu->addAction(i18n("Stage Lines"));
                    menu->addActions(m->actions());
                    v->setContextMenu(menu);

                    connect(sh, &QAction::triggered, v, [=] {
                        applyDiff(file, false, true, v);
                    });
                    connect(sl, &QAction::triggered, v, [=] {
                        applyDiff(file, false, false, v);
                    });
                } else {
                    QMenu *menu = new QMenu(v);
                    auto ush = menu->addAction(i18n("Unstage Hunk"));
                    auto usl = menu->addAction(i18n("Unstage Lines"));
                    menu->addActions(m->actions());
                    v->setContextMenu(menu);

                    connect(ush, &QAction::triggered, v, [=] {
                        applyDiff(file, true, true, v);
                    });
                    connect(usl, &QAction::triggered, v, [=] {
                        applyDiff(file, true, false, v);
                    });
                }
            };

            m_pluginView->showDiffInFixedView(git->readAllStandardOutput(), addContextMenuActions);
491
        }
Waqar Ahmed's avatar
Waqar Ahmed committed
492
        git->deleteLater();
493
    });
Waqar Ahmed's avatar
Waqar Ahmed committed
494

Waqar Ahmed's avatar
Waqar Ahmed committed
495
496
    git->setArguments(args);
    git->start(QProcess::ReadOnly);
497
498
}

Waqar Ahmed's avatar
Waqar Ahmed committed
499
500
501
502
503
504
505
506
507
508
509
510
void GitWidget::launchExternalDiffTool(const QString &file, bool staged)
{
    if (file.isEmpty()) {
        return;
    }

    auto args = QStringList{QStringLiteral("difftool"), QStringLiteral("-y")};
    if (staged) {
        args.append(QStringLiteral("--staged"));
    }
    args.append(file);

Waqar Ahmed's avatar
Waqar Ahmed committed
511
512
    QProcess git;
    git.startDetached(QStringLiteral("git"), args, m_gitPath);
Waqar Ahmed's avatar
Waqar Ahmed committed
513
514
}

515
void GitWidget::commitChanges(const QString &msg, const QString &desc, bool signOff)
Waqar Ahmed's avatar
Waqar Ahmed committed
516
{
517
518
519
520
521
522
    auto args = QStringList{QStringLiteral("commit")};
    if (signOff) {
        args.append(QStringLiteral("-s"));
    }
    args.append(QStringLiteral("-m"));
    args.append(msg);
Waqar Ahmed's avatar
Waqar Ahmed committed
523
524
525
526
527
    if (!desc.isEmpty()) {
        args.append(QStringLiteral("-m"));
        args.append(desc);
    }

Waqar Ahmed's avatar
Waqar Ahmed committed
528
529
530
    auto git = gitp();

    connect(git, &QProcess::finished, this, [this, git](int exitCode, QProcess::ExitStatus es) {
531
        if (es != QProcess::NormalExit || exitCode != 0) {
Waqar Ahmed's avatar
Waqar Ahmed committed
532
            sendMessage(i18n("Failed to commit: %1", QString::fromUtf8(git->readAllStandardError())), true);
Waqar Ahmed's avatar
Waqar Ahmed committed
533
        } else {
534
            m_commitMessage.clear();
535
            getStatus();
Waqar Ahmed's avatar
Waqar Ahmed committed
536
            sendMessage(i18n("Changes committed successfully."), false);
Waqar Ahmed's avatar
Waqar Ahmed committed
537
        }
Waqar Ahmed's avatar
Waqar Ahmed committed
538
        git->deleteLater();
Waqar Ahmed's avatar
Waqar Ahmed committed
539
    });
Waqar Ahmed's avatar
Waqar Ahmed committed
540
541
    git->setArguments(args);
    git->start(QProcess::ReadOnly);
Waqar Ahmed's avatar
Waqar Ahmed committed
542
543
}

Waqar Ahmed's avatar
Waqar Ahmed committed
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
QString GitWidget::getDiff(KTextEditor::View *v, bool hunk, bool alreadyStaged)
{
    auto range = v->selectionRange();
    int startLine = range.start().line();
    int endLine = range.end().line();
    if (range.isEmpty() || hunk) {
        startLine = endLine = v->cursorPosition().line();
    }

    VcsDiff full;
    full.setDiff(v->document()->text());
    auto p = QUrl::fromUserInput(m_gitPath);
    full.setBaseDiff(QUrl::fromUserInput(m_gitPath));

    const auto dir = alreadyStaged ? VcsDiff::Reverse : VcsDiff::Forward;

    VcsDiff selected = hunk ? full.subDiffHunk(startLine, dir) : full.subDiff(startLine, endLine, dir);
    return selected.diff();
}

void GitWidget::applyDiff(const QString &fileName, bool staged, bool hunk, KTextEditor::View *v)
{
    if (!v) {
        return;
    }

    const QString diff = getDiff(v, hunk, staged);
    if (diff.isEmpty()) {
        return;
    }

    QTemporaryFile *file = new QTemporaryFile(this);
    if (!file->open()) {
        sendMessage(i18n("Failed to stage selection"), true);
        return;
    }
    file->write(diff.toUtf8());
    file->close();

Waqar Ahmed's avatar
Waqar Ahmed committed
583
    auto git = gitp();
Waqar Ahmed's avatar
Waqar Ahmed committed
584
585
    QStringList args{QStringLiteral("apply"), QStringLiteral("--index"), QStringLiteral("--cached"), file->fileName()};

Waqar Ahmed's avatar
Waqar Ahmed committed
586
    connect(git, &QProcess::finished, this, [=](int exitCode, QProcess::ExitStatus es) {
Waqar Ahmed's avatar
Waqar Ahmed committed
587
        if (es != QProcess::NormalExit || exitCode != 0) {
Waqar Ahmed's avatar
Waqar Ahmed committed
588
            sendMessage(i18n("Failed to stage: %1", QString::fromUtf8(git->readAllStandardError())), true);
Waqar Ahmed's avatar
Waqar Ahmed committed
589
590
591
592
593
594
        } else {
            // close and reopen doc to show updated diff
            if (v && v->document()) {
                showDiff(fileName, staged);
            }
            // must come at the end
595
            QTimer::singleShot(10, this, [this] {
Waqar Ahmed's avatar
Waqar Ahmed committed
596
597
598
                getStatus();
            });
        }
Waqar Ahmed's avatar
Waqar Ahmed committed
599
600
        delete file;
        git->deleteLater();
Waqar Ahmed's avatar
Waqar Ahmed committed
601
    });
Waqar Ahmed's avatar
Waqar Ahmed committed
602
603
    git->setArguments(args);
    git->start(QProcess::ReadOnly);
Waqar Ahmed's avatar
Waqar Ahmed committed
604
605
}

Waqar Ahmed's avatar
Waqar Ahmed committed
606
607
608
void GitWidget::opencommitChangesDialog()
{
    if (m_model->stagedFiles().isEmpty()) {
Christoph Cullmann's avatar
Christoph Cullmann committed
609
        return sendMessage(i18n("Nothing to commit. Please stage your changes first."), true);
Waqar Ahmed's avatar
Waqar Ahmed committed
610
611
    }

Waqar Ahmed's avatar
Waqar Ahmed committed
612
    auto ciface = qobject_cast<KTextEditor::ConfigInterface *>(m_mainWin->activeView());
Waqar Ahmed's avatar
Waqar Ahmed committed
613
614
615
616
617
618
619
    QFont font;
    if (ciface) {
        font = ciface->configValue(QStringLiteral("font")).value<QFont>();
    } else {
        font = QFontDatabase::systemFont(QFontDatabase::FixedFont);
    }

Waqar Ahmed's avatar
Waqar Ahmed committed
620
    GitCommitDialog dialog(m_commitMessage, font);
Waqar Ahmed's avatar
Waqar Ahmed committed
621
622
623
624
    dialog.setWindowFlags(Qt::WindowSystemMenuHint | Qt::WindowTitleHint);

    int res = dialog.exec();
    if (res == QDialog::Accepted) {
Waqar Ahmed's avatar
Waqar Ahmed committed
625
        if (dialog.subject().isEmpty()) {
Waqar Ahmed's avatar
Waqar Ahmed committed
626
627
            return sendMessage(i18n("Commit message cannot be empty."), true);
        }
Waqar Ahmed's avatar
Waqar Ahmed committed
628
        m_commitMessage = dialog.subject() + QStringLiteral("[[\n\n]]") + dialog.description();
629
        commitChanges(dialog.subject(), dialog.description(), dialog.signoff());
Waqar Ahmed's avatar
Waqar Ahmed committed
630
631
632
    }
}

Waqar Ahmed's avatar
Waqar Ahmed committed
633
void GitWidget::handleClick(const QModelIndex &idx, ClickAction clickAction)
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
{
    auto type = idx.data(GitStatusModel::TreeItemType);
    if (type != GitStatusModel::NodeFile) {
        return;
    }

    if (clickAction == ClickAction::NoAction) {
        return;
    }

    const QString file = m_gitPath + idx.data(GitStatusModel::FileNameRole).toString();
    bool staged = idx.internalId() == GitStatusModel::NodeStage;

    if (clickAction == ClickAction::StageUnstage) {
        if (staged) {
            return unstage({file});
        }
        return stage({file});
    }

    if (clickAction == ClickAction::ShowDiff) {
        showDiff(file, staged);
    }

    if (clickAction == ClickAction::OpenFile) {
        m_mainWin->openUrl(QUrl::fromLocalFile(file));
    }
}

void GitWidget::treeViewSingleClicked(const QModelIndex &idx)
{
    handleClick(idx, m_pluginView->plugin()->singleClickAcion());
}

void GitWidget::treeViewDoubleClicked(const QModelIndex &idx)
{
    handleClick(idx, m_pluginView->plugin()->doubleClickAcion());
}

Waqar Ahmed's avatar
Waqar Ahmed committed
673
674
675
void GitWidget::hideEmptyTreeNodes()
{
    const auto emptyRows = m_model->emptyRows();
Waqar Ahmed's avatar
Waqar Ahmed committed
676
    m_treeView->expand(m_model->getModelIndex((GitStatusModel::NodeStage)));
677
678
    // 1 because "Staged" will always be visible
    for (int i = 1; i < 4; ++i) {
Waqar Ahmed's avatar
Waqar Ahmed committed
679
680
681
682
        if (emptyRows.contains(i)) {
            m_treeView->setRowHidden(i, QModelIndex(), true);
        } else {
            m_treeView->setRowHidden(i, QModelIndex(), false);
683
684
685
            if (i != GitStatusModel::NodeUntrack) {
                m_treeView->expand(m_model->getModelIndex((GitStatusModel::ItemType)i));
            }
Waqar Ahmed's avatar
Waqar Ahmed committed
686
        }
Waqar Ahmed's avatar
Waqar Ahmed committed
687
    }
688
689
690

    m_treeView->resizeColumnToContents(0);
    m_treeView->resizeColumnToContents(1);
Waqar Ahmed's avatar
Waqar Ahmed committed
691
692
}

693
694
void GitWidget::parseStatusReady()
{
695
    GitUtils::GitParsedStatus s = m_gitStatusWatcher.result();
Waqar Ahmed's avatar
Waqar Ahmed committed
696

697
    if (m_pluginView->plugin()->showGitStatusWithNumStat()) {
698
699
        numStatForStatus(s.changed, true);
        numStatForStatus(s.staged, false);
700
701
    }

702
    m_model->addItems(std::move(s), m_pluginView->plugin()->showGitStatusWithNumStat());
Waqar Ahmed's avatar
Waqar Ahmed committed
703
    hideEmptyTreeNodes();
704
}
Waqar Ahmed's avatar
Waqar Ahmed committed
705

706
void GitWidget::numStatForStatus(QVector<GitUtils::StatusItem> &list, bool modified)
707
{
708
709
    const auto args = modified ? QStringList{QStringLiteral("diff"), QStringLiteral("--numstat"), QStringLiteral("-z")}
                               : QStringList{QStringLiteral("diff"), QStringLiteral("--numstat"), QStringLiteral("--staged"), QStringLiteral("-z")};
710

Waqar Ahmed's avatar
Waqar Ahmed committed
711
712
713
    QProcess git;
    git.setWorkingDirectory(m_gitPath);
    git.start(QStringLiteral("git"), args, QProcess::ReadOnly);
714
715
716
717
718
719
720
    if (git.waitForStarted() && git.waitForFinished(-1)) {
        if (git.exitStatus() != QProcess::NormalExit || git.exitCode() != 0) {
            return;
        }
    }

    GitUtils::parseDiffNumStat(list, git.readAllStandardOutput());
721
722
}

Waqar Ahmed's avatar
Waqar Ahmed committed
723
724
725
726
727
728
729
730
731
732
733
bool GitWidget::eventFilter(QObject *o, QEvent *e)
{
    if (e->type() == QEvent::ContextMenu) {
        if (o != m_treeView)
            return QWidget::eventFilter(o, e);
        QContextMenuEvent *cme = static_cast<QContextMenuEvent *>(e);
        treeViewContextMenuEvent(cme);
    }
    return QWidget::eventFilter(o, e);
}

734
735
736
737
738
void GitWidget::buildMenu()
{
    m_gitMenu = new QMenu(this);
    m_gitMenu->addAction(i18n("Refresh"), this, [this] {
        if (m_project) {
739
            getStatus();
740
741
        }
    });
Waqar Ahmed's avatar
Waqar Ahmed committed
742
    m_gitMenu->addAction(i18n("Checkout Branch"), this, [this] {
743
        BranchesDialog bd(m_mainWin->window(), m_pluginView, m_project->baseDir());
Waqar Ahmed's avatar
Waqar Ahmed committed
744
745
        bd.openDialog();
    });
Waqar Ahmed's avatar
Waqar Ahmed committed
746
747
748
749

    m_gitMenu->addAction(i18n("Stash"))->setMenu(stashMenu());
}

750
751
752
753
void GitWidget::createStashDialog(StashMode m, const QString &gitPath)
{
    auto stashDialog = new StashDialog(this, mainWindow()->window(), gitPath);
    connect(stashDialog, &StashDialog::message, this, &GitWidget::sendMessage);
754
755
    connect(stashDialog, &StashDialog::showStashDiff, this, [this](const QByteArray &r) {
        m_pluginView->showDiffInFixedView(r);
756
757
758
759
760
761
762
763
    });
    connect(stashDialog, &StashDialog::done, this, [this, stashDialog] {
        getStatus();
        stashDialog->deleteLater();
    });
    stashDialog->openDialog(m);
}

Waqar Ahmed's avatar
Waqar Ahmed committed
764
void GitWidget::enableCancel(QProcess *git)
Waqar Ahmed's avatar
Waqar Ahmed committed
765
{
Waqar Ahmed's avatar
Waqar Ahmed committed
766
    m_cancelHandle = git;
Waqar Ahmed's avatar
Waqar Ahmed committed
767
768
769
770
771
772
773
774
775
776
    m_pushBtn->hide();
    m_cancelBtn->show();
}

void GitWidget::hideCancel()
{
    m_cancelBtn->hide();
    m_pushBtn->show();
}

Waqar Ahmed's avatar
Waqar Ahmed committed
777
778
779
780
781
782
783
784
785
786
787
QMenu *GitWidget::stashMenu()
{
    QMenu *menu = new QMenu(this);
    auto stashAct = menu->addAction(i18n("Stash"));
    auto popLastAct = menu->addAction(i18n("Pop Last Stash"));
    auto popAct = menu->addAction(i18n("Pop Stash"));
    auto applyLastAct = menu->addAction(i18n("Apply Last Stash"));
    auto stashKeepStagedAct = menu->addAction(i18n("Stash (Keep Staged)"));
    auto stashUAct = menu->addAction(i18n("Stash (Include Untracked)"));
    auto applyStashAct = menu->addAction(i18n("Apply Stash"));
    auto dropAct = menu->addAction(i18n("Drop Stash"));
788
    auto showStashAct = menu->addAction(i18n("Show Stash Content"));
Waqar Ahmed's avatar
Waqar Ahmed committed
789
790

    connect(stashAct, &QAction::triggered, this, [this] {
791
        createStashDialog(StashMode::Stash, m_gitPath);
Waqar Ahmed's avatar
Waqar Ahmed committed
792
793
    });
    connect(stashUAct, &QAction::triggered, this, [this] {
794
        createStashDialog(StashMode::StashUntrackIncluded, m_gitPath);
Waqar Ahmed's avatar
Waqar Ahmed committed
795
796
    });
    connect(stashKeepStagedAct, &QAction::triggered, this, [this] {
797
        createStashDialog(StashMode::StashKeepIndex, m_gitPath);
Waqar Ahmed's avatar
Waqar Ahmed committed
798
799
    });
    connect(popAct, &QAction::triggered, this, [this] {
800
        createStashDialog(StashMode::StashPop, m_gitPath);
Waqar Ahmed's avatar
Waqar Ahmed committed
801
802
    });
    connect(applyStashAct, &QAction::triggered, this, [this] {
803
        createStashDialog(StashMode::StashApply, m_gitPath);
Waqar Ahmed's avatar
Waqar Ahmed committed
804
805
    });
    connect(dropAct, &QAction::triggered, this, [this] {
806
        createStashDialog(StashMode::StashDrop, m_gitPath);
Waqar Ahmed's avatar
Waqar Ahmed committed
807
808
    });
    connect(popLastAct, &QAction::triggered, this, [this] {
809
        createStashDialog(StashMode::StashPopLast, m_gitPath);
Waqar Ahmed's avatar
Waqar Ahmed committed
810
811
    });
    connect(applyLastAct, &QAction::triggered, this, [this] {
812
        createStashDialog(StashMode::StashApplyLast, m_gitPath);
Waqar Ahmed's avatar
Waqar Ahmed committed
813
    });
814
    connect(showStashAct, &QAction::triggered, this, [this] {
815
        createStashDialog(StashMode::ShowStashContent, m_gitPath);
816
    });
Waqar Ahmed's avatar
Waqar Ahmed committed
817
818

    return menu;
819
820
}

821
822
823
824
825
static KMessageBox::ButtonCode confirm(GitWidget *_this, const QString &text)
{
    return KMessageBox::questionYesNo(_this, text, {}, KStandardGuiItem::yes(), KStandardGuiItem::no(), {}, KMessageBox::Dangerous);
}

Waqar Ahmed's avatar
Waqar Ahmed committed
826
827
void GitWidget::treeViewContextMenuEvent(QContextMenuEvent *e)
{
828
    if (auto selModel = m_treeView->selectionModel()) {
829
        if (selModel->selectedRows().count() > 1) {
830
831
832
833
            return selectedContextMenu(e);
        }
    }

Waqar Ahmed's avatar
Waqar Ahmed committed
834
835
836
    auto idx = m_model->index(m_treeView->currentIndex().row(), 0, m_treeView->currentIndex().parent());
    auto type = idx.data(GitStatusModel::TreeItemType);

837
    if (type == GitStatusModel::NodeChanges || type == GitStatusModel::NodeUntrack) {
Waqar Ahmed's avatar
Waqar Ahmed committed
838
        QMenu menu;
Waqar Ahmed's avatar
Waqar Ahmed committed
839
        auto stageAct = menu.addAction(i18n("Stage All"));
840
        bool untracked = type == GitStatusModel::NodeUntrack;
841
842
        auto discardAct = untracked ? menu.addAction(i18n("Remove All")) : menu.addAction(i18n("Discard All"));
        auto ignoreAct = untracked ? menu.addAction(i18n("Open .gitignore")) : nullptr;
843
        auto diff = !untracked ? menu.addAction(i18n("Show diff")) : nullptr;
844
        // get files
Waqar Ahmed's avatar
Waqar Ahmed committed
845
846
847
848
849
850
        const QVector<GitUtils::StatusItem> &items = untracked ? m_model->untrackedFiles() : m_model->changedFiles();
        QStringList files;
        files.reserve(items.size());
        std::transform(items.begin(), items.end(), std::back_inserter(files), [](const GitUtils::StatusItem &i) {
            return QString::fromUtf8(i.file);
        });
851
        // execute action
Waqar Ahmed's avatar
Waqar Ahmed committed
852
        auto act = menu.exec(m_treeView->viewport()->mapToGlobal(e->pos()));
Waqar Ahmed's avatar
Waqar Ahmed committed
853
        if (act == stageAct) {
Waqar Ahmed's avatar
Waqar Ahmed committed
854
            stage(files, type == GitStatusModel::NodeUntrack);
855
        } else if (act == discardAct && !untracked) {
856
857
            auto ret = confirm(this, i18n("Are you sure you want to remove these files?"));
            if (ret == KMessageBox::Yes) {
Waqar Ahmed's avatar
Waqar Ahmed committed
858
                discard(files);
859
            }
860
        } else if (act == discardAct && untracked) {
861
862
            auto ret = confirm(this, i18n("Are you sure you want to discard all changes?"));
            if (ret == KMessageBox::Yes) {
Waqar Ahmed's avatar
Waqar Ahmed committed
863
                clean(files);
864
            }
865
        } else if (ignoreAct && untracked && act == ignoreAct) {
866
867
868
869
870
871
872
873
874
875
            const auto files = m_project->files();
            const auto it = std::find_if(files.cbegin(), files.cend(), [](const QString &s) {
                if (s.contains(QStringLiteral(".gitignore"))) {
                    return true;
                }
                return false;
            });
            if (it != files.cend()) {
                m_mainWin->openUrl(QUrl::fromLocalFile(*it));
            }
876
        } else if (diff && !untracked && act == diff) {
877
            showDiff(QString(), false);
Waqar Ahmed's avatar
Waqar Ahmed committed
878
        }
879
    } else if (type == GitStatusModel::NodeFile) {
Waqar Ahmed's avatar
Waqar Ahmed committed
880
        QMenu menu;
Waqar Ahmed's avatar
Waqar Ahmed committed
881
        bool staged = idx.internalId() == GitStatusModel::NodeStage;
882
883
        bool untracked = idx.internalId() == GitStatusModel::NodeUntrack;

884
        auto openFile = menu.addAction(i18n("Open file"));
Waqar Ahmed's avatar
Waqar Ahmed committed
885
        auto showDiffAct = untracked ? nullptr : menu.addAction(i18n("Show raw diff"));
886
        auto launchDifftoolAct = untracked ? nullptr : menu.addAction(i18n("Show in external git diff tool"));
Waqar Ahmed's avatar
Waqar Ahmed committed
887
        auto openAtHead = untracked ? nullptr : menu.addAction(i18n("Open at HEAD"));
Waqar Ahmed's avatar
Waqar Ahmed committed
888
        auto stageAct = staged ? menu.addAction(i18n("Unstage file")) : menu.addAction(i18n("Stage file"));
889
        auto discardAct = staged ? nullptr : untracked ? menu.addAction(i18n("Remove")) : menu.addAction(i18n("Discard"));
Waqar Ahmed's avatar
Waqar Ahmed committed
890

891
        auto act = menu.exec(m_treeView->viewport()->mapToGlobal(e->pos()));
892
        const QString file = m_gitPath + idx.data(GitStatusModel::FileNameRole).toString();
Waqar Ahmed's avatar
Waqar Ahmed committed
893
        if (act == stageAct) {
Waqar Ahmed's avatar
Waqar Ahmed committed
894
            if (staged) {
895
                return unstage({file});
Waqar Ahmed's avatar
Waqar Ahmed committed
896
            }
897
            return stage({file});
898
        } else if (discardAct && act == discardAct && !untracked) {
899
900
901
902
            auto ret = confirm(this, i18n("Are you sure you want to discard the changes in this file?"));
            if (ret == KMessageBox::Yes) {
                discard({file});
            }
903
        } else if (openAtHead && act == openAtHead && !untracked) {
904
            openAtHEAD(idx.data(GitStatusModel::FileNameRole).toString());
905
        } else if (showDiffAct && act == showDiffAct && !untracked) {
906
            showDiff(file, staged);
907
        } else if (discardAct && act == discardAct && untracked) {
908
909
910
911
            auto ret = confirm(this, i18n("Are you sure you want to remove this file?"));
            if (ret == KMessageBox::Yes) {
                clean({file});
            }
912
        } else if (launchDifftoolAct && act == launchDifftoolAct) {
913
            launchExternalDiffTool(idx.data(GitStatusModel::FileNameRole).toString(), staged);
914
915
        } else if (act == openFile) {
            m_mainWin->openUrl(QUrl::fromLocalFile(file));
916
917
918
919
        }
    } else if (type == GitStatusModel::NodeStage) {
        QMenu menu;
        auto stage = menu.addAction(i18n("Unstage All"));
920
        auto diff = menu.addAction(i18n("Show diff"));
921
922
        auto act = menu.exec(m_treeView->viewport()->mapToGlobal(e->pos()));

Waqar Ahmed's avatar
Waqar Ahmed committed
923
        // git reset -q HEAD --
924
        if (act == stage) {
925
            const QVector<GitUtils::StatusItem> &items = m_model->stagedFiles();
Waqar Ahmed's avatar
Waqar Ahmed committed
926
927
928
929
930
931
            QStringList files;
            files.reserve(items.size());
            std::transform(items.begin(), items.end(), std::back_inserter(files), [](const GitUtils::StatusItem &i) {
                return QString::fromUtf8(i.file);
            });
            unstage(files);
932
933
        } else if (act == diff) {
            showDiff(QString(), true);
934
935
936
937
938
939
940
941
        }
    }
}

void GitWidget::selectedContextMenu(QContextMenuEvent *e)
{
    QStringList files;

942
    bool selectionHasStagedItems = false;
943
    bool selectionHasChangedItems = false;
944
    bool selectionHasUntrackedItems = false;
945
946
947
948

    if (auto selModel = m_treeView->selectionModel()) {
        const auto idxList = selModel->selectedIndexes();
        for (const auto &idx : idxList) {
949
            if (idx.internalId() == GitStatusModel::NodeStage) {
950
                selectionHasStagedItems = true;
951
952
            } else if (!idx.parent().isValid()) {
                // can't allow main nodes to be selected
953
                return;
954
955
956
            } else if (idx.internalId() == GitStatusModel::NodeUntrack) {
                selectionHasUntrackedItems = true;
            } else if (idx.internalId() == GitStatusModel::NodeChanges) {
957
                selectionHasChangedItems = true;
958
            }
959
            files.append(idx.data(GitStatusModel::FileNameRole).toString());
960
961
962
        }
    }

963
964
    const bool selHasUnstagedItems = selectionHasUntrackedItems || selectionHasChangedItems;

965
    // cant allow both
966
    if (selHasUnstagedItems && selectionHasStagedItems) {
967
        return;
968
    }
969
970

    QMenu menu;
971
972
973
    auto stageAct = selectionHasStagedItems ? menu.addAction(i18n("Unstage Selected Files")) : menu.addAction(i18n("Stage Selected Files"));
    auto discardAct = selectionHasChangedItems && !selectionHasUntrackedItems ? menu.addAction(i18n("Discard Selected Files")) : nullptr;
    auto removeAct = !selectionHasChangedItems && selectionHasUntrackedItems ? menu.addAction(i18n("Remove Selected Files")) : nullptr;
974
975
976
977
978
979
980
    auto execAct = menu.exec(m_treeView->viewport()->mapToGlobal(e->pos()));

    if (execAct == stageAct) {
        if (selectionHasChangedItems) {
            stage(files);
        } else {
            unstage(files);
Waqar Ahmed's avatar
Waqar Ahmed committed
981
        }
982
983
984
985
986
987
988
989
990
991
    } else if (selectionHasChangedItems && !selectionHasUntrackedItems && execAct == discardAct) {
        auto ret = confirm(this, i18n("Are you sure you want to discard the changes?"));
        if (ret == KMessageBox::Yes) {
            discard(files);
        }
    } else if (!selectionHasChangedItems && selectionHasUntrackedItems && execAct == removeAct) {
        auto ret = confirm(this, i18n("Are you sure you want to remove these untracked changes?"));
        if (ret == KMessageBox::Yes) {
            clean(files);
        }
Waqar Ahmed's avatar
Waqar Ahmed committed
992
993
    }
}