gitwidget.cpp 38 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"
7
#include "branchcheckoutdialog.h"
Waqar Ahmed's avatar
Waqar Ahmed committed
8
#include "branchesdialog.h"
9
#include "comarebranchesview.h"
Waqar Ahmed's avatar
Waqar Ahmed committed
10
#include "git/gitdiff.h"
Waqar Ahmed's avatar
Waqar Ahmed committed
11
#include "gitcommitdialog.h"
12
13
#include "gitstatusmodel.h"
#include "kateproject.h"
14
#include "kateprojectpluginview.h"
Waqar Ahmed's avatar
Waqar Ahmed committed
15
#include "pushpulldialog.h"
Waqar Ahmed's avatar
Waqar Ahmed committed
16
#include "stashdialog.h"
17

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

Waqar Ahmed's avatar
Waqar Ahmed committed
41
#include <KLocalizedString>
42
#include <KMessageBox>
43
#include <QPointer>
Waqar Ahmed's avatar
Waqar Ahmed committed
44

45
46
#include <KSyntaxHighlighting/Definition>
#include <KSyntaxHighlighting/Repository>
Waqar Ahmed's avatar
Waqar Ahmed committed
47
#include <KTextEditor/Application>
Waqar Ahmed's avatar
Waqar Ahmed committed
48
#include <KTextEditor/ConfigInterface>
49
#include <KTextEditor/Editor>
Waqar Ahmed's avatar
Waqar Ahmed committed
50
#include <KTextEditor/MainWindow>
Waqar Ahmed's avatar
Waqar Ahmed committed
51
#include <KTextEditor/Message>
Waqar Ahmed's avatar
Waqar Ahmed committed
52
#include <KTextEditor/View>
Waqar Ahmed's avatar
Waqar Ahmed committed
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
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);

100
101
102
        KColorScheme c;
        const auto red = c.shade(c.foreground(KColorScheme::NegativeText).color(), KColorScheme::MidlightShade, 1);
        const auto green = c.shade(c.foreground(KColorScheme::PositiveText).color(), KColorScheme::MidlightShade, 1);
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121

        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;
};

122
123
124
125
126
127
128
129
130
131
132
133
134
135
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
136
137
138
139
140
141
142
143
144
145
146
147
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
148
149
GitWidget::GitWidget(KateProject *project, KTextEditor::MainWindow *mainWindow, KateProjectPluginView *pluginView)
    : m_project(project)
Waqar Ahmed's avatar
Waqar Ahmed committed
150
    , m_mainWin(mainWindow)
Waqar Ahmed's avatar
Waqar Ahmed committed
151
    , m_pluginView(pluginView)
152
    , m_mainView(new QWidget(this))
153
    , m_stackWidget(new QStackedWidget(this))
154
{
Waqar Ahmed's avatar
Waqar Ahmed committed
155
    setDotGitPath();
156

Waqar Ahmed's avatar
Waqar Ahmed committed
157
    m_treeView = new GitWidgetTreeView(this);
158

159
    buildMenu();
Waqar Ahmed's avatar
Waqar Ahmed committed
160
    m_menuBtn = toolButton(QStringLiteral("application-menu"), QString());
161
    m_menuBtn->setMenu(m_gitMenu);
162
163
    m_menuBtn->setArrowType(Qt::NoArrow);
    m_menuBtn->setStyleSheet(QStringLiteral("QToolButton::menu-indicator{ image: none; }"));
164
165
166
167
    connect(m_menuBtn, &QToolButton::clicked, this, [this](bool) {
        m_menuBtn->showMenu();
    });

168
    m_commitBtn = toolButton(QStringLiteral("vcs-commit"), QString(), i18n("Commit"), Qt::ToolButtonTextBesideIcon);
Waqar Ahmed's avatar
Waqar Ahmed committed
169

170
    m_pushBtn = toolButton(QStringLiteral("vcs-push"), i18n("Git push"));
Waqar Ahmed's avatar
Waqar Ahmed committed
171
172
173
174
175
176
    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);
    });

177
    m_pullBtn = toolButton(QStringLiteral("vcs-pull"), i18n("Git pull"));
Waqar Ahmed's avatar
Waqar Ahmed committed
178
179
180
181
182
183
    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
184
185
186
    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
187
        if (m_cancelHandle) {
Waqar Ahmed's avatar
Waqar Ahmed committed
188
            // we don't want error occurred, this is intentional
Waqar Ahmed's avatar
Waqar Ahmed committed
189
190
191
192
            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
193
194
195
196
            hideCancel();
        }
    });

197
    QVBoxLayout *layout = new QVBoxLayout;
Waqar Ahmed's avatar
Waqar Ahmed committed
198
199
    layout->setSpacing(0);
    layout->setContentsMargins(0, 0, 0, 0);
Waqar Ahmed's avatar
Waqar Ahmed committed
200

201
    QHBoxLayout *btnsLayout = new QHBoxLayout;
Waqar Ahmed's avatar
Waqar Ahmed committed
202
    btnsLayout->setContentsMargins(0, 0, 0, 0);
203

Waqar Ahmed's avatar
Waqar Ahmed committed
204
205
206
    for (auto *btn : {m_commitBtn, m_cancelBtn, m_pushBtn, m_pullBtn, m_menuBtn}) {
        btnsLayout->addWidget(btn);
    }
Waqar Ahmed's avatar
Waqar Ahmed committed
207
    btnsLayout->setStretch(0, 1);
208
209
210
211
212

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

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

214
    m_treeView->setUniformRowHeights(true);
Waqar Ahmed's avatar
Waqar Ahmed committed
215
    m_treeView->setHeaderHidden(true);
216
    m_treeView->setSelectionMode(QTreeView::ExtendedSelection);
217
    m_treeView->setModel(m_model);
Waqar Ahmed's avatar
Waqar Ahmed committed
218
    m_treeView->installEventFilter(this);
219
    m_treeView->setRootIsDecorated(false);
Waqar Ahmed's avatar
Waqar Ahmed committed
220
221
222
223
224

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

226
227
228
    m_treeView->header()->setStretchLastSection(false);
    m_treeView->header()->setSectionResizeMode(0, QHeaderView::Stretch);

229
230
    m_treeView->setItemDelegateForColumn(1, new NumStatStyle(this, m_pluginView->plugin()));

231
232
    // our main view - status view + btns
    m_mainView->setLayout(layout);
233

234
    connect(&m_gitStatusWatcher, &QFutureWatcher<GitUtils::GitParsedStatus>::finished, this, &GitWidget::parseStatusReady);
Waqar Ahmed's avatar
Waqar Ahmed committed
235
    connect(m_commitBtn, &QPushButton::clicked, this, &GitWidget::opencommitChangesDialog);
236
237
238
239

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

241
    m_stackWidget->addWidget(m_mainView);
242
243
244

    // This Widget's layout
    setLayout(new QVBoxLayout);
245
    this->layout()->addWidget(m_stackWidget);
246
247
}

Waqar Ahmed's avatar
Waqar Ahmed committed
248
249
GitWidget::~GitWidget()
{
Waqar Ahmed's avatar
Waqar Ahmed committed
250
251
    if (m_cancelHandle) {
        m_cancelHandle->kill();
Waqar Ahmed's avatar
Waqar Ahmed committed
252
253
254
    }
}

Waqar Ahmed's avatar
Waqar Ahmed committed
255
void GitWidget::setDotGitPath()
256
{
Waqar Ahmed's avatar
Waqar Ahmed committed
257
258
    /* This call is intentionally blocking because we need git path for everything else */
    QProcess git;
259
260
    git.setProgram(QStringLiteral("git"));
    git.setWorkingDirectory(m_project->baseDir());
261
    git.setArguments({QStringLiteral("rev-parse"), QStringLiteral("--absolute-git-dir")});
262
    git.start(QProcess::ReadOnly);
263
    if (git.waitForStarted() && git.waitForFinished(-1)) {
264
        if (git.exitStatus() != QProcess::NormalExit || git.exitCode() != 0) {
265
            sendMessage(i18n("Failed to find .git directory, things may not work correctly: %1", QString::fromUtf8(git.readAllStandardError())), true);
266
267
            m_gitPath = m_project->baseDir();
            return;
268
269
270
271
272
273
274
275
276
277
278
279
280
        }
        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
281
void GitWidget::sendMessage(const QString &plainText, bool warn)
Waqar Ahmed's avatar
Waqar Ahmed committed
282
{
Christoph Cullmann's avatar
Christoph Cullmann committed
283
284
    // use generic output view
    QVariantMap genericMessage;
285
    genericMessage.insert(QStringLiteral("type"), warn ? QStringLiteral("Error") : QStringLiteral("Info"));
Christoph Cullmann's avatar
Christoph Cullmann committed
286
    genericMessage.insert(QStringLiteral("category"), i18n("Git"));
Christoph Cullmann's avatar
Christoph Cullmann committed
287
    genericMessage.insert(QStringLiteral("categoryIcon"), QIcon(QStringLiteral(":/icons/icons/sc-apps-git.svg")));
288
    genericMessage.insert(QStringLiteral("text"), plainText);
Christoph Cullmann's avatar
Christoph Cullmann committed
289
    Q_EMIT m_pluginView->message(genericMessage);
Waqar Ahmed's avatar
Waqar Ahmed committed
290
291
}

292
293
294
295
296
KTextEditor::MainWindow *GitWidget::mainWindow()
{
    return m_mainWin;
}

Waqar Ahmed's avatar
Waqar Ahmed committed
297
298
299
300
301
QProcess *GitWidget::gitp()
{
    auto git = new QProcess(this);
    git->setProgram(QStringLiteral("git"));
    git->setWorkingDirectory(m_gitPath);
Waqar Ahmed's avatar
Waqar Ahmed committed
302
303
304
305
    connect(git, &QProcess::errorOccurred, this, [this, git](QProcess::ProcessError) {
        sendMessage(git->errorString(), true);
        git->deleteLater();
    });
Waqar Ahmed's avatar
Waqar Ahmed committed
306
307
308
    return git;
}

309
void GitWidget::getStatus(bool untracked, bool submodules)
310
{
Waqar Ahmed's avatar
Waqar Ahmed committed
311
312
313
314
315
316
317
318
319
320
321
    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();
    });
322

323
324
325
326
327
328
    auto args = QStringList{QStringLiteral("status"), QStringLiteral("-z")};
    if (!untracked) {
        args.append(QStringLiteral("-uno"));
    } else {
        args.append(QStringLiteral("-u"));
    }
329
330
331
    if (!submodules) {
        args.append(QStringLiteral("--ignore-submodules"));
    }
Waqar Ahmed's avatar
Waqar Ahmed committed
332
333
    git->setArguments(args);
    git->start(QProcess::ReadOnly);
334
335
}

Waqar Ahmed's avatar
Waqar Ahmed committed
336
void GitWidget::runGitCmd(const QStringList &args, const QString &i18error)
337
{
Waqar Ahmed's avatar
Waqar Ahmed committed
338
339
    auto git = gitp();
    connect(git, &QProcess::finished, this, [this, i18error, git](int exitCode, QProcess::ExitStatus es) {
340
        if (es != QProcess::NormalExit || exitCode != 0) {
Waqar Ahmed's avatar
Waqar Ahmed committed
341
            sendMessage(i18error + QStringLiteral(": ") + QString::fromUtf8(git->readAllStandardError()), true);
Waqar Ahmed's avatar
Waqar Ahmed committed
342
343
344
        } else {
            getStatus();
        }
Waqar Ahmed's avatar
Waqar Ahmed committed
345
        git->deleteLater();
Waqar Ahmed's avatar
Waqar Ahmed committed
346
    });
Waqar Ahmed's avatar
Waqar Ahmed committed
347
348
    git->setArguments(args);
    git->start(QProcess::ReadOnly);
Waqar Ahmed's avatar
Waqar Ahmed committed
349
350
351
352
}

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

Waqar Ahmed's avatar
Waqar Ahmed committed
356
    connect(git, &QProcess::finished, this, [this, args, git](int exitCode, QProcess::ExitStatus es) {
Waqar Ahmed's avatar
Waqar Ahmed committed
357
        if (es != QProcess::NormalExit || exitCode != 0) {
Waqar Ahmed's avatar
Waqar Ahmed committed
358
            sendMessage(QStringLiteral("git ") + args.first() + i18n(" error: %1", QString::fromUtf8(git->readAll())), true);
359
        } else {
Waqar Ahmed's avatar
Waqar Ahmed committed
360
361
362
            auto gargs = args;
            gargs.push_front(QStringLiteral("git"));
            QString cmd = gargs.join(QStringLiteral(" "));
Waqar Ahmed's avatar
Waqar Ahmed committed
363
            QString out = QString::fromUtf8(git->readAll());
Waqar Ahmed's avatar
Waqar Ahmed committed
364
            sendMessage(i18n("\"%1\" executed successfully: %2", cmd, out), false);
365
366
            getStatus();
        }
Waqar Ahmed's avatar
Waqar Ahmed committed
367
        hideCancel();
Waqar Ahmed's avatar
Waqar Ahmed committed
368
        git->deleteLater();
369
    });
Waqar Ahmed's avatar
Waqar Ahmed committed
370

Waqar Ahmed's avatar
Waqar Ahmed committed
371
    enableCancel(git);
Waqar Ahmed's avatar
Waqar Ahmed committed
372
373
    git->setArguments(args);
    git->start(QProcess::ReadOnly);
374
375
}

376
void GitWidget::stage(const QStringList &files, bool)
377
{
378
379
380
    if (files.isEmpty()) {
        return;
    }
381

382
    auto args = QStringList{QStringLiteral("add"), QStringLiteral("-A"), QStringLiteral("--")};
383
    args.append(files);
384

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

388
void GitWidget::unstage(const QStringList &files)
Waqar Ahmed's avatar
Waqar Ahmed committed
389
{
390
391
392
393
    if (files.isEmpty()) {
        return;
    }

Waqar Ahmed's avatar
Waqar Ahmed committed
394
395
    // git reset -q HEAD --
    auto args = QStringList{QStringLiteral("reset"), QStringLiteral("-q"), QStringLiteral("HEAD"), QStringLiteral("--")};
396
    args.append(files);
Waqar Ahmed's avatar
Waqar Ahmed committed
397

Waqar Ahmed's avatar
Waqar Ahmed committed
398
    runGitCmd(args, i18n("Failed to unstage file. Error:"));
Waqar Ahmed's avatar
Waqar Ahmed committed
399
400
}

401
402
403
404
405
406
407
408
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
409
    runGitCmd(args, i18n("Failed to discard changes. Error:"));
410
}
411

412
413
414
415
416
417
418
419
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
420
    runGitCmd(args, i18n("Failed to remove. Error:"));
421
422
}

423
424
425
426
427
428
void GitWidget::openAtHEAD(const QString &file)
{
    if (file.isEmpty()) {
        return;
    }

Waqar Ahmed's avatar
Waqar Ahmed committed
429
    auto git = gitp();
430
431
    auto args = QStringList{QStringLiteral("show"), QStringLiteral("--textconv")};
    args.append(QStringLiteral(":") + file);
Waqar Ahmed's avatar
Waqar Ahmed committed
432
433
    git->setArguments(args);
    git->start(QProcess::ReadOnly);
434

Waqar Ahmed's avatar
Waqar Ahmed committed
435
    connect(git, &QProcess::finished, this, [this, file, git](int exitCode, QProcess::ExitStatus es) {
436
        if (es != QProcess::NormalExit || exitCode != 0) {
Waqar Ahmed's avatar
Waqar Ahmed committed
437
            sendMessage(i18n("Failed to open file at HEAD: %1", QString::fromUtf8(git->readAllStandardError())), true);
438
        } else {
439
            auto view = m_mainWin->openUrl(QUrl());
Waqar Ahmed's avatar
Waqar Ahmed committed
440
            if (view) {
441
442
443
                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
444
                view->document()->setModified(false); // no save file dialog when closing
445
            }
Waqar Ahmed's avatar
Waqar Ahmed committed
446
        }
Waqar Ahmed's avatar
Waqar Ahmed committed
447
        git->deleteLater();
Waqar Ahmed's avatar
Waqar Ahmed committed
448
    });
Waqar Ahmed's avatar
Waqar Ahmed committed
449

Waqar Ahmed's avatar
Waqar Ahmed committed
450
451
    git->setArguments(args);
    git->start(QProcess::ReadOnly);
Waqar Ahmed's avatar
Waqar Ahmed committed
452
453
}

Waqar Ahmed's avatar
Waqar Ahmed committed
454
void GitWidget::showDiff(const QString &file, bool staged)
Waqar Ahmed's avatar
Waqar Ahmed committed
455
{
Waqar Ahmed's avatar
Waqar Ahmed committed
456
457
458
459
    auto args = QStringList{QStringLiteral("diff")};
    if (staged) {
        args.append(QStringLiteral("--staged"));
    }
460

461
462
463
464
    if (!file.isEmpty()) {
        args.append(QStringLiteral("--"));
        args.append(file);
    }
Waqar Ahmed's avatar
Waqar Ahmed committed
465

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

473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
            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);
505
        }
Waqar Ahmed's avatar
Waqar Ahmed committed
506
        git->deleteLater();
507
    });
Waqar Ahmed's avatar
Waqar Ahmed committed
508

Waqar Ahmed's avatar
Waqar Ahmed committed
509
510
    git->setArguments(args);
    git->start(QProcess::ReadOnly);
511
512
}

Waqar Ahmed's avatar
Waqar Ahmed committed
513
514
515
516
517
518
519
520
521
522
523
524
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
525
526
    QProcess git;
    git.startDetached(QStringLiteral("git"), args, m_gitPath);
Waqar Ahmed's avatar
Waqar Ahmed committed
527
528
}

529
void GitWidget::commitChanges(const QString &msg, const QString &desc, bool signOff)
Waqar Ahmed's avatar
Waqar Ahmed committed
530
{
531
532
533
534
535
536
    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
537
538
539
540
541
    if (!desc.isEmpty()) {
        args.append(QStringLiteral("-m"));
        args.append(desc);
    }

Waqar Ahmed's avatar
Waqar Ahmed committed
542
543
544
    auto git = gitp();

    connect(git, &QProcess::finished, this, [this, git](int exitCode, QProcess::ExitStatus es) {
545
        if (es != QProcess::NormalExit || exitCode != 0) {
Waqar Ahmed's avatar
Waqar Ahmed committed
546
            sendMessage(i18n("Failed to commit: %1", QString::fromUtf8(git->readAllStandardError())), true);
Waqar Ahmed's avatar
Waqar Ahmed committed
547
        } else {
548
            m_commitMessage.clear();
549
            getStatus();
Waqar Ahmed's avatar
Waqar Ahmed committed
550
            sendMessage(i18n("Changes committed successfully."), false);
Waqar Ahmed's avatar
Waqar Ahmed committed
551
        }
Waqar Ahmed's avatar
Waqar Ahmed committed
552
        git->deleteLater();
Waqar Ahmed's avatar
Waqar Ahmed committed
553
    });
Waqar Ahmed's avatar
Waqar Ahmed committed
554
555
    git->setArguments(args);
    git->start(QProcess::ReadOnly);
Waqar Ahmed's avatar
Waqar Ahmed committed
556
557
}

Waqar Ahmed's avatar
Waqar Ahmed committed
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
583
584
585
586
587
588
589
590
591
592
593
594
595
596
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
597
    auto git = gitp();
Waqar Ahmed's avatar
Waqar Ahmed committed
598
599
    QStringList args{QStringLiteral("apply"), QStringLiteral("--index"), QStringLiteral("--cached"), file->fileName()};

Waqar Ahmed's avatar
Waqar Ahmed committed
600
    connect(git, &QProcess::finished, this, [=](int exitCode, QProcess::ExitStatus es) {
Waqar Ahmed's avatar
Waqar Ahmed committed
601
        if (es != QProcess::NormalExit || exitCode != 0) {
Waqar Ahmed's avatar
Waqar Ahmed committed
602
            sendMessage(i18n("Failed to stage: %1", QString::fromUtf8(git->readAllStandardError())), true);
Waqar Ahmed's avatar
Waqar Ahmed committed
603
604
605
606
607
608
        } else {
            // close and reopen doc to show updated diff
            if (v && v->document()) {
                showDiff(fileName, staged);
            }
            // must come at the end
609
            QTimer::singleShot(10, this, [this] {
Waqar Ahmed's avatar
Waqar Ahmed committed
610
611
612
                getStatus();
            });
        }
Waqar Ahmed's avatar
Waqar Ahmed committed
613
614
        delete file;
        git->deleteLater();
Waqar Ahmed's avatar
Waqar Ahmed committed
615
    });
Waqar Ahmed's avatar
Waqar Ahmed committed
616
617
    git->setArguments(args);
    git->start(QProcess::ReadOnly);
Waqar Ahmed's avatar
Waqar Ahmed committed
618
619
}

Waqar Ahmed's avatar
Waqar Ahmed committed
620
621
622
void GitWidget::opencommitChangesDialog()
{
    if (m_model->stagedFiles().isEmpty()) {
Christoph Cullmann's avatar
Christoph Cullmann committed
623
        return sendMessage(i18n("Nothing to commit. Please stage your changes first."), true);
Waqar Ahmed's avatar
Waqar Ahmed committed
624
625
    }

Waqar Ahmed's avatar
Waqar Ahmed committed
626
    auto ciface = qobject_cast<KTextEditor::ConfigInterface *>(m_mainWin->activeView());
Waqar Ahmed's avatar
Waqar Ahmed committed
627
628
629
630
631
632
633
    QFont font;
    if (ciface) {
        font = ciface->configValue(QStringLiteral("font")).value<QFont>();
    } else {
        font = QFontDatabase::systemFont(QFontDatabase::FixedFont);
    }

634
    GitCommitDialog *dialog = new GitCommitDialog(m_commitMessage, font, this);
Waqar Ahmed's avatar
Waqar Ahmed committed
635

636
637
638
639
640
641
642
    connect(dialog, &QDialog::finished, this, [this, dialog](int res) {
        if (res == QDialog::Accepted) {
            if (dialog->subject().isEmpty()) {
                return sendMessage(i18n("Commit message cannot be empty."), true);
            }
            m_commitMessage = dialog->subject() + QStringLiteral("[[\n\n]]") + dialog->description();
            commitChanges(dialog->subject(), dialog->description(), dialog->signoff());
Waqar Ahmed's avatar
Waqar Ahmed committed
643
        }
644
645
646
647
        dialog->deleteLater();
    });

    dialog->open();
Waqar Ahmed's avatar
Waqar Ahmed committed
648
649
}

Waqar Ahmed's avatar
Waqar Ahmed committed
650
void GitWidget::handleClick(const QModelIndex &idx, ClickAction clickAction)
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
{
    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
690
691
692
void GitWidget::hideEmptyTreeNodes()
{
    const auto emptyRows = m_model->emptyRows();
Waqar Ahmed's avatar
Waqar Ahmed committed
693
    m_treeView->expand(m_model->getModelIndex((GitStatusModel::NodeStage)));
694
695
    // 1 because "Staged" will always be visible
    for (int i = 1; i < 4; ++i) {
Waqar Ahmed's avatar
Waqar Ahmed committed
696
697
698
699
        if (emptyRows.contains(i)) {
            m_treeView->setRowHidden(i, QModelIndex(), true);
        } else {
            m_treeView->setRowHidden(i, QModelIndex(), false);
700
701
702
            if (i != GitStatusModel::NodeUntrack) {
                m_treeView->expand(m_model->getModelIndex((GitStatusModel::ItemType)i));
            }
Waqar Ahmed's avatar
Waqar Ahmed committed
703
        }
Waqar Ahmed's avatar
Waqar Ahmed committed
704
    }
705
706
707

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

710
711
void GitWidget::parseStatusReady()
{
712
    GitUtils::GitParsedStatus s = m_gitStatusWatcher.result();
Waqar Ahmed's avatar
Waqar Ahmed committed
713

714
    if (m_pluginView->plugin()->showGitStatusWithNumStat()) {
715
716
        numStatForStatus(s.changed, true);
        numStatForStatus(s.staged, false);
717
718
    }

719
    m_model->addItems(std::move(s), m_pluginView->plugin()->showGitStatusWithNumStat());
Waqar Ahmed's avatar
Waqar Ahmed committed
720
    hideEmptyTreeNodes();
721
}
Waqar Ahmed's avatar
Waqar Ahmed committed
722

723
void GitWidget::numStatForStatus(QVector<GitUtils::StatusItem> &list, bool modified)
724
{
725
726
    const auto args = modified ? QStringList{QStringLiteral("diff"), QStringLiteral("--numstat"), QStringLiteral("-z")}
                               : QStringList{QStringLiteral("diff"), QStringLiteral("--numstat"), QStringLiteral("--staged"), QStringLiteral("-z")};
727

Waqar Ahmed's avatar
Waqar Ahmed committed
728
729
730
    QProcess git;
    git.setWorkingDirectory(m_gitPath);
    git.start(QStringLiteral("git"), args, QProcess::ReadOnly);
731
732
733
734
735
736
737
    if (git.waitForStarted() && git.waitForFinished(-1)) {
        if (git.exitStatus() != QProcess::NormalExit || git.exitCode() != 0) {
            return;
        }
    }

    GitUtils::parseDiffNumStat(list, git.readAllStandardOutput());
738
739
}

740
741
void GitWidget::branchCompareFiles(const QString &from, const QString &to)
{
742
743
744
745
    if (from.isEmpty() && to.isEmpty()) {
        return;
    }

746
    // git diff br...br2 --name-only -z
Waqar Ahmed's avatar
Waqar Ahmed committed
747
    auto args = QStringList{QStringLiteral("diff"), QStringLiteral("%1...%2").arg(from).arg(to), QStringLiteral("--name-status")};
748
749
750
751
752
753
754
755
756
757

    QProcess git;
    git.setWorkingDirectory(m_gitPath);
    git.start(QStringLiteral("git"), args, QProcess::ReadOnly);
    if (git.waitForStarted() && git.waitForFinished(-1)) {
        if (git.exitStatus() != QProcess::NormalExit || git.exitCode() != 0) {
            return;
        }
    }

Waqar Ahmed's avatar
Waqar Ahmed committed
758
759
760
761
762
763
764
    const QByteArray diff = git.readAllStandardOutput();
    if (diff.isEmpty()) {
        sendMessage(i18n("No diff for %1...%2", from, to), false);
        return;
    }

    auto filesWithNameStatus = GitUtils::parseDiffNameStatus(diff);
Waqar Ahmed's avatar
Waqar Ahmed committed
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
    if (filesWithNameStatus.isEmpty()) {
        sendMessage(i18n("Failed to compare %1...%2", from, to), true);
        return;
    }

    // get --num-stat
    args = QStringList{QStringLiteral("diff"), QStringLiteral("%1...%2").arg(from).arg(to), QStringLiteral("--numstat"), QStringLiteral("-z")};
    git.setArguments(args);
    git.start(QStringLiteral("git"), args, QProcess::ReadOnly);
    if (git.waitForStarted() && git.waitForFinished(-1)) {
        if (git.exitStatus() != QProcess::NormalExit || git.exitCode() != 0) {
            sendMessage(i18n("Failed to get numstat when diffing %1...%2", from, to), true);
            return;
        }
    }

    GitUtils::parseDiffNumStat(filesWithNameStatus, git.readAllStandardOutput());

    CompareBranchesView *w = new CompareBranchesView(this, m_gitPath, from, to, filesWithNameStatus);
784
785
    w->setPluginView(m_pluginView);
    connect(w, &CompareBranchesView::backClicked, this, [this] {
786
        auto x = m_stackWidget->currentWidget();
787
        if (x) {
788
            m_stackWidget->setCurrentWidget(m_mainView);
789
790
791
            x->deleteLater();
        }
    });
792
793
    m_stackWidget->addWidget(w);
    m_stackWidget->setCurrentWidget(w);
794
795
}

Waqar Ahmed's avatar
Waqar Ahmed committed
796
797
798
799
800
801
802
803
804
805
806
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);
}

807
808
809
810
811
void GitWidget::buildMenu()
{
    m_gitMenu = new QMenu(this);
    m_gitMenu->addAction(i18n("Refresh"), this, [this] {
        if (m_project) {
812
            getStatus();
813
814
        }
    });
815
    auto a = m_gitMenu->addAction(i18n("Checkout Branch"), this, [this] {
816
        BranchCheckoutDialog bd(m_mainWin->window(), m_pluginView, m_project->baseDir());
Waqar Ahmed's avatar
Waqar Ahmed committed
817
818
        bd.openDialog();
    });
819
    a->setIcon(QIcon::fromTheme(QStringLiteral("vcs-branch")));
820

821
    a = m_gitMenu->addAction(i18n("Compare Branch with ..."), this, [this] {
822
        BranchesDialog bd(m_mainWin->window(), m_pluginView, m_project->baseDir());
Waqar Ahmed's avatar
Waqar Ahmed committed
823
824
        using GitUtils::RefType;
        bd.openDialog(static_cast<GitUtils::RefType>(RefType::Head | RefType::Remote));
825
826
827
        QString branch = bd.branch();
        branchCompareFiles(branch, QString());
    });
828
    a->setIcon(QIcon::fromTheme(QStringLiteral("vcs-diff")));
829
830

    m_gitMenu->addAction(i18n("Stash"))->setMenu(stashMenu());
Waqar Ahmed's avatar
Waqar Ahmed committed
831
832
}

833
834
835
836
void GitWidget::createStashDialog(StashMode m, const QString &gitPath)
{
    auto stashDialog = new StashDialog(this, mainWindow()->window(), gitPath);
    connect(stashDialog, &StashDialog::message, this, &GitWidget::sendMessage);
837
838
    connect(stashDialog, &StashDialog::showStashDiff, this, [this](const QByteArray &r) {
        m_pluginView->showDiffInFixedView(r);
839
840
841
842
843
844
845
846
    });
    connect(stashDialog, &StashDialog::done, this, [this, stashDialog] {
        getStatus();
        stashDialog->deleteLater();
    });
    stashDialog->openDialog(m);
}

Waqar Ahmed's avatar
Waqar Ahmed committed
847
void GitWidget::enableCancel(QProcess *git)
Waqar Ahmed's avatar
Waqar Ahmed committed
848
{
Waqar Ahmed's avatar
Waqar Ahmed committed
849
    m_cancelHandle = git;
Waqar Ahmed's avatar
Waqar Ahmed committed
850
851
852
853
854
855
856
857
858
859
    m_pushBtn->hide();
    m_cancelBtn->show();
}

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

Waqar Ahmed's avatar
Waqar Ahmed committed
860
861
862
QMenu *GitWidget::stashMenu()
{
    QMenu *menu = new QMenu(this);
863
864
    auto stashAct = menu->addAction(QIcon::fromTheme(QStringLiteral("vcs-stash")), i18n("Stash"));
    auto popLastAct = menu->addAction(QIcon::fromTheme(QStringLiteral("vcs-stash-pop")), i18n("Pop Last Stash"));
Waqar Ahmed's avatar
Waqar Ahmed committed
865
866
867
868
869
870
    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"));
871
    auto showStashAct = menu->addAction(i18n("Show Stash Content"));
Waqar Ahmed's avatar
Waqar Ahmed committed
872
873

    connect(stashAct, &QAction::triggered, this, [this] {
874
        createStashDialog(StashMode::Stash, m_gitPath);
Waqar Ahmed's avatar
Waqar Ahmed committed
875
876
    });
    connect(stashUAct, &QAction::triggered, this, [this] {
877
        createStashDialog(StashMode::StashUntrackIncluded, m_gitPath);
Waqar Ahmed's avatar
Waqar Ahmed committed
878
879
    });
    connect(stashKeepStagedAct, &QAction::triggered, this, [this] {
880
        createStashDialog(StashMode::StashKeepIndex, m_gitPath);
Waqar Ahmed's avatar
Waqar Ahmed committed
881
882
    });
    connect(popAct, &QAction::triggered, this, [this] {
883
        createStashDialog(StashMode::StashPop, m_gitPath);
Waqar Ahmed's avatar
Waqar Ahmed committed
884
885
    });
    connect(applyStashAct, &QAction::triggered, this, [this] {
886
        createStashDialog(StashMode::StashApply, m_gitPath);
Waqar Ahmed's avatar
Waqar Ahmed committed
887
888
    });
    connect(dropAct, &QAction::triggered, this, [this] {
889
        createStashDialog(StashMode::StashDrop, m_gitPath);
Waqar Ahmed's avatar
Waqar Ahmed committed
890
891
    });
    connect(popLastAct, &QAction::triggered, this, [this] {
892
        createStashDialog(StashMode::StashPopLast, m_gitPath);
Waqar Ahmed's avatar
Waqar Ahmed committed
893
894
    });
    connect(applyLastAct, &QAction::triggered, this, [this] {
895
        createStashDialog(StashMode::StashApplyLast, m_gitPath);
Waqar Ahmed's avatar
Waqar Ahmed committed
896
    });
897
    connect(showStashAct, &QAction::triggered, this, [this] {
898
        createStashDialog(StashMode::ShowStashContent, m_gitPath);
899
    });
Waqar Ahmed's avatar
Waqar Ahmed committed
900
901

    return menu;
902
903
}

904
905
906
907
908
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
909
910
void GitWidget::treeViewContextMenuEvent(QContextMenuEvent *e)
{
911
    if (auto selModel = m_treeView->selectionModel()) {
912
        if (selModel->selectedRows().count() > 1) {
913
914
915
916
            return selectedContextMenu(e);
        }
    }

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

920
    if (type == GitStatusModel::NodeChanges || type == GitStatusModel::NodeUntrack) {
Waqar Ahmed's avatar
Waqar Ahmed committed
921
        QMenu menu;
Waqar Ahmed's avatar
Waqar Ahmed committed
922
        auto stageAct = menu.addAction(i18n("Stage All"));
923
        bool untracked = type == GitStatusModel::NodeUntrack;
924
925
        auto discardAct = untracked ? menu.addAction(i18n("Remove All")) : menu.addAction(i18n("Discard All"));
        auto ignoreAct = untracked ? menu.addAction(i18n("Open .gitignore")) : nullptr;
926
        auto diff = !untracked ? menu.addAction(i18n("Show diff")) : nullptr;
927
        // get files
Waqar Ahmed's avatar
Waqar Ahmed committed
928
929
930
931
932
        auto act = menu.exec(m_treeView->viewport()->mapToGlobal(e->pos()));
        if (!act) {
            return;
        }

Waqar Ahmed's avatar
Waqar Ahmed committed
933
934
935
936
937
938
        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);
        });
Waqar Ahmed's avatar
Waqar Ahmed committed
939

Waqar Ahmed's avatar
Waqar Ahmed committed
940
        if (act == stageAct) {
Waqar Ahmed's avatar
Waqar Ahmed committed
941
            stage(files, type == GitStatusModel::NodeUntrack);
942
        } else if (act == discardAct && !untracked) {
943
944
            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
945
                discard(files);
946
            }
947
        } else if (act == discardAct && untracked) {
948
949
            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
950
                clean(files);
951
            }
Waqar Ahmed's avatar
Waqar Ahmed committed
952
        } else if (untracked && act == ignoreAct) {
953
954
955
956
957
958
959
960
961
962
            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));
            }
Waqar Ahmed's avatar
Waqar Ahmed committed
963
        } else if (!untracked && act == diff) {
964
            showDiff(QString(), false);
Waqar Ahmed's avatar
Waqar Ahmed committed
965
        }
966
    } else if (type == GitStatusModel::NodeFile) {
Waqar Ahmed's avatar
Waqar Ahmed committed
967
        QMenu menu;
Waqar Ahmed's avatar
Waqar Ahmed committed
968
        bool staged = idx.internalId() == GitStatusModel::NodeStage;
969
970
        bool untracked = idx.internalId() == GitStatusModel::NodeUntrack;

971
        auto openFile = menu.addAction(i18n("Open file"));
Waqar Ahmed's avatar
Waqar Ahmed committed
972
        auto showDiffAct = untracked ? nullptr : menu.addAction(i18n("Show raw diff"));
973
        auto launchDifftoolAct = untracked ? nullptr : menu.addAction(i18n("Show in external git diff tool"));
Waqar Ahmed's avatar
Waqar Ahmed committed
974
        auto openAtHead = untracked ? nullptr : menu.addAction(i18n("Open at HEAD"));
Waqar Ahmed's avatar
Waqar Ahmed committed
975
        auto stageAct = staged ? menu.addAction(i18n("Unstage file")) : menu.addAction(i18n("Stage file"));
976
        auto discardAct = staged ? nullptr : untracked ? menu.addAction(i18n("Remove")) : menu.addAction(i18n("Discard"));
Waqar Ahmed's avatar
Waqar Ahmed committed
977

978
        auto act = menu.exec(m_treeView->viewport()->mapToGlobal(e->pos()));
Waqar Ahmed's avatar
Waqar Ahmed committed
979
980
981
982
        if (!act) {
            return;
        }

983
        const QString file = m_gitPath + idx.data(GitStatusModel::FileNameRole).toString();
Waqar Ahmed's avatar
Waqar Ahmed committed
984
        if (act == stageAct) {
Waqar Ahmed's avatar
Waqar Ahmed committed
985
            if (staged) {
986
                return unstage({file});
Waqar Ahmed's avatar
Waqar Ahmed committed
987
            }
988
            return stage({file});
Waqar Ahmed's avatar
Waqar Ahmed committed
989
        } else if (act == discardAct && !untracked) {
990
991
992
993
            auto ret = confirm(this, i18n("Are you sure you want to discard the changes in this file?"));
            if (ret == KMessageBox::Yes) {
                discard({file});
            }
Waqar Ahmed's avatar
Waqar Ahmed committed
994
        } else if (act == openAtHead && !untracked) {
995
            openAtHEAD(idx.data(GitStatusModel::FileNameRole).toString());
996
        } else if (showDiffAct && act == showDiffAct && !untracked) {
997
            showDiff(file, staged);
Waqar Ahmed's avatar
Waqar Ahmed committed
998
        } else if (act == discardAct && untracked) {
999
1000
1001
1002
            auto ret = confirm(this, i18n("Are you sure you want to remove this file?"));
            if (ret == KMessageBox::Yes) {
                clean({file});
            }
Waqar Ahmed's avatar
Waqar Ahmed committed
1003
        } else if (act == launchDifftoolAct) {
1004
            launchExternalDiffTool(idx.data(GitStatusModel::FileNameRole).toString(), staged);
1005
1006
        } else if (act == openFile) {
            m_mainWin->openUrl(QUrl::fromLocalFile(file));
1007
1008
1009
1010
        }
    } else if (type == GitStatusModel::NodeStage) {
        QMenu menu;
        auto stage = menu.addAction(i18n("Unstage All"));
1011
        auto diff = menu.addAction(i18n("Show diff"));
1012
        auto act = menu.exec(m_treeView->viewport()->mapToGlobal(e->pos()));
Waqar Ahmed's avatar
Waqar Ahmed committed
1013
1014
1015
        if (!act) {
            return;
        }
1016

Waqar Ahmed's avatar
Waqar Ahmed committed
1017
        // git reset -q HEAD --
1018
        if (act == stage) {
1019
            const QVector<GitUtils::StatusItem> &items = m_model->stagedFiles();