gitwidget.cpp 24.9 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 "gitcommitdialog.h"
9
10
#include "gitstatusmodel.h"
#include "kateproject.h"
Waqar Ahmed's avatar
Waqar Ahmed committed
11
#include "stashdialog.h"
12

Waqar Ahmed's avatar
Waqar Ahmed committed
13
#include <QContextMenuEvent>
14
#include <QCoreApplication>
15
#include <QDebug>
Waqar Ahmed's avatar
Waqar Ahmed committed
16
#include <QDialog>
Waqar Ahmed's avatar
Waqar Ahmed committed
17
#include <QEvent>
18
#include <QFileInfo>
19
#include <QHeaderView>
20
#include <QInputMethodEvent>
Waqar Ahmed's avatar
Waqar Ahmed committed
21
#include <QLineEdit>
Waqar Ahmed's avatar
Waqar Ahmed committed
22
#include <QMenu>
Waqar Ahmed's avatar
Waqar Ahmed committed
23
#include <QPlainTextEdit>
24
25
26
#include <QProcess>
#include <QPushButton>
#include <QStringListModel>
27
#include <QToolButton>
28
29
30
31
#include <QTreeView>
#include <QVBoxLayout>
#include <QtConcurrentRun>

Waqar Ahmed's avatar
Waqar Ahmed committed
32
#include <KLocalizedString>
33
#include <KMessageBox>
Waqar Ahmed's avatar
Waqar Ahmed committed
34

Waqar Ahmed's avatar
Waqar Ahmed committed
35
#include <KTextEditor/ConfigInterface>
36
#include <KTextEditor/Editor>
Waqar Ahmed's avatar
Waqar Ahmed committed
37
#include <KTextEditor/MainWindow>
Waqar Ahmed's avatar
Waqar Ahmed committed
38
#include <KTextEditor/Message>
Waqar Ahmed's avatar
Waqar Ahmed committed
39
#include <KTextEditor/View>
Waqar Ahmed's avatar
Waqar Ahmed committed
40

Waqar Ahmed's avatar
Waqar Ahmed committed
41
42
GitWidget::GitWidget(KateProject *project, KTextEditor::MainWindow *mainWindow, KateProjectPluginView *pluginView)
    : m_project(project)
Waqar Ahmed's avatar
Waqar Ahmed committed
43
    , m_mainWin(mainWindow)
Waqar Ahmed's avatar
Waqar Ahmed committed
44
    , m_pluginView(pluginView)
45
{
Dominik Haumann's avatar
Dominik Haumann committed
46
    m_commitBtn = new QToolButton(this);
47
48
    m_treeView = new QTreeView(this);

49
50
    initGitExe();

51
    buildMenu();
52
    m_menuBtn = new QToolButton(this);
Waqar Ahmed's avatar
Waqar Ahmed committed
53
    m_menuBtn->setAutoRaise(true);
54
    m_menuBtn->setMenu(m_gitMenu);
55
56
    m_menuBtn->setArrowType(Qt::NoArrow);
    m_menuBtn->setStyleSheet(QStringLiteral("QToolButton::menu-indicator{ image: none; }"));
57
58
59
60
    connect(m_menuBtn, &QToolButton::clicked, this, [this](bool) {
        m_menuBtn->showMenu();
    });

61
    m_menuBtn->setIcon(QIcon::fromTheme(QStringLiteral("application-menu")));
Waqar Ahmed's avatar
Waqar Ahmed committed
62
    m_commitBtn->setText(i18n("Commit"));
Dominik Haumann's avatar
Dominik Haumann committed
63
64
65
66
    m_commitBtn->setIcon(QIcon::fromTheme(QStringLiteral("svn-commit"))); // ":/kxmlgui5/kateproject/git-commit-dark.svg"
    m_commitBtn->setAutoRaise(true);
    m_commitBtn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
    m_commitBtn->setSizePolicy(QSizePolicy::Minimum, m_commitBtn->sizePolicy().verticalPolicy());
67
68

    QVBoxLayout *layout = new QVBoxLayout;
Waqar Ahmed's avatar
Waqar Ahmed committed
69
70
    layout->setSpacing(0);
    layout->setContentsMargins(0, 0, 0, 0);
71
    QHBoxLayout *btnsLayout = new QHBoxLayout;
Waqar Ahmed's avatar
Waqar Ahmed committed
72
    btnsLayout->setContentsMargins(0, 0, 0, 0);
73
74
75
76
77
78
79
80

    btnsLayout->addWidget(m_commitBtn);
    btnsLayout->addWidget(m_menuBtn);

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

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

82
    m_treeView->setUniformRowHeights(true);
Waqar Ahmed's avatar
Waqar Ahmed committed
83
    m_treeView->setHeaderHidden(true);
84
    m_treeView->setSelectionMode(QTreeView::ExtendedSelection);
85
    m_treeView->setModel(m_model);
Waqar Ahmed's avatar
Waqar Ahmed committed
86
    m_treeView->installEventFilter(this);
87

88
89
90
    m_treeView->header()->setStretchLastSection(false);
    m_treeView->header()->setSectionResizeMode(0, QHeaderView::Stretch);

91
92
    setLayout(layout);

93
    connect(&m_gitStatusWatcher, &QFutureWatcher<GitUtils::GitParsedStatus>::finished, this, &GitWidget::parseStatusReady);
Waqar Ahmed's avatar
Waqar Ahmed committed
94
    connect(m_commitBtn, &QPushButton::clicked, this, &GitWidget::opencommitChangesDialog);
95
96
}

97
98
99
100
101
102
void GitWidget::initGitExe()
{
    git.setProgram(QStringLiteral("git"));
    // we initially use project base dir
    // and then calculate the exit .git path
    git.setWorkingDirectory(m_project->baseDir());
103
    git.setArguments({QStringLiteral("rev-parse"), QStringLiteral("--absolute-git-dir")});
104
    git.start(QProcess::ReadOnly);
105
    if (git.waitForStarted() && git.waitForFinished(-1)) {
106
        if (git.exitStatus() != QProcess::NormalExit || git.exitCode() != 0) {
107
            sendMessage(i18n("Failed to find .git directory. Things may not work correctly. Error:\n%1", QString::fromUtf8(git.readAllStandardError())), true);
108
109
            m_gitPath = m_project->baseDir();
            return;
110
111
112
113
114
115
116
117
118
119
120
121
122
        }
        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);
    }
}

Waqar Ahmed's avatar
Waqar Ahmed committed
123
124
void GitWidget::sendMessage(const QString &message, bool warn)
{
125
126
127
128
129
    // quickfix crash on startup
    if (!m_mainWin->activeView()) {
        return;
    }

Waqar Ahmed's avatar
Waqar Ahmed committed
130
131
    KTextEditor::Message *msg = new KTextEditor::Message(message, warn ? KTextEditor::Message::Warning : KTextEditor::Message::Positive);
    msg->setPosition(KTextEditor::Message::TopInView);
132
    msg->setAutoHide(warn ? 5000 : 3000);
Waqar Ahmed's avatar
Waqar Ahmed committed
133
134
135
136
137
    msg->setAutoHideMode(KTextEditor::Message::Immediate);
    msg->setView(m_mainWin->activeView());
    m_mainWin->activeView()->document()->postMessage(msg);
}

Waqar Ahmed's avatar
Waqar Ahmed committed
138
139
140
141
142
QProcess *GitWidget::gitprocess()
{
    return &git;
}

143
144
145
146
147
148
149
150
151
152
KTextEditor::MainWindow *GitWidget::mainWindow()
{
    return m_mainWin;
}

std::vector<GitWidget::TempFileViewPair> *GitWidget::tempFilesVector()
{
    return &m_filesOpenAtHEAD;
}

153
void GitWidget::getStatus(bool untracked, bool submodules)
154
{
Waqar Ahmed's avatar
Waqar Ahmed committed
155
156
    disconnect(&git, &QProcess::finished, nullptr, nullptr);
    connect(&git, &QProcess::finished, this, &GitWidget::gitStatusReady);
157

158
159
160
161
162
163
    auto args = QStringList{QStringLiteral("status"), QStringLiteral("-z")};
    if (!untracked) {
        args.append(QStringLiteral("-uno"));
    } else {
        args.append(QStringLiteral("-u"));
    }
164
165
166
167
    if (!submodules) {
        args.append(QStringLiteral("--ignore-submodules"));
    }
    git.setArguments(args);
168
    git.start(QProcess::ReadOnly);
169
170
}

Waqar Ahmed's avatar
Waqar Ahmed committed
171
void GitWidget::runGitCmd(const QStringList &args, const QString &i18error)
172
173
{
    disconnect(&git, &QProcess::finished, nullptr, nullptr);
Waqar Ahmed's avatar
Waqar Ahmed committed
174
    connect(&git, &QProcess::finished, this, [this, i18error](int exitCode, QProcess::ExitStatus es) {
175
176
177
        // sever connection
        disconnect(&git, &QProcess::finished, nullptr, nullptr);
        if (es != QProcess::NormalExit || exitCode != 0) {
Waqar Ahmed's avatar
Waqar Ahmed committed
178
            sendMessage(i18error + QStringLiteral("\n") + QString::fromUtf8(git.readAllStandardError()), true);
179
180
181
182
        } else {
            getStatus();
        }
    });
Waqar Ahmed's avatar
Waqar Ahmed committed
183
    git.setArguments(args);
184
    git.start(QProcess::ReadOnly);
185
186
}

187
void GitWidget::stage(const QStringList &files, bool)
188
{
189
190
191
    if (files.isEmpty()) {
        return;
    }
192

193
    auto args = QStringList{QStringLiteral("add"), QStringLiteral("-A"), QStringLiteral("--")};
194
    args.append(files);
195

Waqar Ahmed's avatar
Waqar Ahmed committed
196
    runGitCmd(args, i18n("Failed to stage file. Error:"));
197
198
}

199
void GitWidget::unstage(const QStringList &files)
Waqar Ahmed's avatar
Waqar Ahmed committed
200
{
201
202
203
204
    if (files.isEmpty()) {
        return;
    }

Waqar Ahmed's avatar
Waqar Ahmed committed
205
206
    // git reset -q HEAD --
    auto args = QStringList{QStringLiteral("reset"), QStringLiteral("-q"), QStringLiteral("HEAD"), QStringLiteral("--")};
207
    args.append(files);
Waqar Ahmed's avatar
Waqar Ahmed committed
208

Waqar Ahmed's avatar
Waqar Ahmed committed
209
    runGitCmd(args, i18n("Failed to unstage file. Error:"));
Waqar Ahmed's avatar
Waqar Ahmed committed
210
211
}

212
213
214
215
216
217
218
219
220
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
221
    runGitCmd(args, i18n("Failed to discard changes. Error:"));
222
}
223

224
225
226
227
228
229
230
231
232
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
233
    runGitCmd(args, i18n("Failed to remove. Error:"));
234
235
}

236
237
238
239
240
241
242
243
244
void GitWidget::openAtHEAD(const QString &file)
{
    if (file.isEmpty()) {
        return;
    }

    auto args = QStringList{QStringLiteral("show"), QStringLiteral("--textconv")};
    args.append(QStringLiteral(":") + file);
    git.setArguments(args);
245
    git.start(QProcess::ReadOnly);
246
247

    disconnect(&git, &QProcess::finished, nullptr, nullptr);
248
    connect(&git, &QProcess::finished, this, [this, file](int exitCode, QProcess::ExitStatus es) {
249
250
        // sever connection
        disconnect(&git, &QProcess::finished, nullptr, nullptr);
251
        if (es != QProcess::NormalExit || exitCode != 0) {
252
253
254
            sendMessage(i18n("Failed to open file at HEAD. Error:\n%1", QString::fromUtf8(git.readAllStandardError())), true);
        } else {
            std::unique_ptr<QTemporaryFile> f(new QTemporaryFile);
Waqar Ahmed's avatar
Waqar Ahmed committed
255
256
            QFileInfo fi(file);
            f->setFileTemplate(QString(QStringLiteral("XXXXXX - (HEAD) - %1").arg(fi.fileName())));
Waqar Ahmed's avatar
Waqar Ahmed committed
257
258
            if (!f->open()) {
                return;
Waqar Ahmed's avatar
Waqar Ahmed committed
259
            }
Waqar Ahmed's avatar
Waqar Ahmed committed
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274

            auto xx = git.readAllStandardOutput();
            f->write(xx);
            f->flush();
            const QUrl tempFileUrl(QUrl::fromLocalFile(f->fileName()));
            auto v = m_mainWin->openUrl(tempFileUrl);
            if (!v || !v->document()) {
                return;
            }

            TempFileViewPair tfvp = std::make_pair(std::move(f), v);
            m_filesOpenAtHEAD.push_back(std::move(tfvp));

            // close temp on document close
            auto clearTemp = [this](KTextEditor::Document *document) {
Waqar Ahmed's avatar
Waqar Ahmed committed
275
276
277
278
279
280
281
282
283
                m_filesOpenAtHEAD.erase(std::remove_if(m_filesOpenAtHEAD.begin(),
                                                       m_filesOpenAtHEAD.end(),
                                                       [document](const GitWidget::TempFileViewPair &tf) {
                                                           if (tf.second && tf.second->document() == document) {
                                                               return true;
                                                           }
                                                           return false;
                                                       }),
                                        m_filesOpenAtHEAD.end());
Waqar Ahmed's avatar
Waqar Ahmed committed
284
285
            };
            connect(v->document(), &KTextEditor::Document::aboutToClose, this, clearTemp);
Waqar Ahmed's avatar
Waqar Ahmed committed
286
287
        }
    });
Waqar Ahmed's avatar
Waqar Ahmed committed
288
289

    git.setArguments(args);
290
    git.start(QProcess::ReadOnly);
Waqar Ahmed's avatar
Waqar Ahmed committed
291
292
}

Waqar Ahmed's avatar
Waqar Ahmed committed
293
void GitWidget::showDiff(const QString &file, bool staged)
Waqar Ahmed's avatar
Waqar Ahmed committed
294
295
296
297
298
{
    if (file.isEmpty()) {
        return;
    }

Waqar Ahmed's avatar
Waqar Ahmed committed
299
300
301
302
    auto args = QStringList{QStringLiteral("diff")};
    if (staged) {
        args.append(QStringLiteral("--staged"));
    }
303

304
    args.append(QStringLiteral("--"));
Waqar Ahmed's avatar
Waqar Ahmed committed
305
    args.append(file);
Waqar Ahmed's avatar
Waqar Ahmed committed
306
307

    disconnect(&git, &QProcess::finished, nullptr, nullptr);
308
    connect(&git, &QProcess::finished, this, [this, file](int exitCode, QProcess::ExitStatus es) {
309
310
        // sever connection
        disconnect(&git, &QProcess::finished, nullptr, nullptr);
311
        if (es != QProcess::NormalExit || exitCode != 0) {
Waqar Ahmed's avatar
Waqar Ahmed committed
312
313
314
            sendMessage(i18n("Failed to get Diff of file. Error:\n%1", QString::fromUtf8(git.readAllStandardError())), true);
        } else {
            std::unique_ptr<QTemporaryFile> f(new QTemporaryFile);
315
316
            QFileInfo fi(file);
            f->setFileTemplate(QString(QStringLiteral("XXXXXX %1.diff").arg(fi.fileName())));
Waqar Ahmed's avatar
Waqar Ahmed committed
317
318
            if (!f->open()) {
                return;
319
            }
Waqar Ahmed's avatar
Waqar Ahmed committed
320
321
322
323
324
325
326
327
328
329
330
331
332
333
            f->write(git.readAllStandardOutput());
            f->flush();
            const QUrl tempFileUrl(QUrl::fromLocalFile(f->fileName()));
            auto v = m_mainWin->openUrl(tempFileUrl);

            if (!v || !v->document()) {
                return;
            }

            TempFileViewPair tfvp = std::make_pair(std::move(f), v);
            m_filesOpenAtHEAD.push_back(std::move(tfvp));

            // close temp on document close
            auto clearTemp = [this](KTextEditor::Document *document) {
Waqar Ahmed's avatar
Waqar Ahmed committed
334
335
336
337
338
339
340
341
342
                m_filesOpenAtHEAD.erase(std::remove_if(m_filesOpenAtHEAD.begin(),
                                                       m_filesOpenAtHEAD.end(),
                                                       [document](const GitWidget::TempFileViewPair &tf) {
                                                           if (tf.second && tf.second->document() == document) {
                                                               return true;
                                                           }
                                                           return false;
                                                       }),
                                        m_filesOpenAtHEAD.end());
Waqar Ahmed's avatar
Waqar Ahmed committed
343
344
            };
            connect(v->document(), &KTextEditor::Document::aboutToClose, this, clearTemp);
345
346
        }
    });
Waqar Ahmed's avatar
Waqar Ahmed committed
347
348

    git.setArguments(args);
349
    git.start(QProcess::ReadOnly);
350
351
}

Waqar Ahmed's avatar
Waqar Ahmed committed
352
353
354
355
356
357
358
359
360
361
362
363
364
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);

    git.setArguments(args);
365
    git.start(QProcess::ReadOnly);
Waqar Ahmed's avatar
Waqar Ahmed committed
366
367
}

368
void GitWidget::commitChanges(const QString &msg, const QString &desc, bool signOff)
Waqar Ahmed's avatar
Waqar Ahmed committed
369
{
370
371
372
373
374
375
    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
376
377
378
379
380
    if (!desc.isEmpty()) {
        args.append(QStringLiteral("-m"));
        args.append(desc);
    }

381
    disconnect(&git, &QProcess::finished, nullptr, nullptr);
Waqar Ahmed's avatar
Waqar Ahmed committed
382
    connect(&git, &QProcess::finished, this, [this](int exitCode, QProcess::ExitStatus es) {
383
384
        // sever connection
        disconnect(&git, &QProcess::finished, nullptr, nullptr);
385
        if (es != QProcess::NormalExit || exitCode != 0) {
Waqar Ahmed's avatar
Waqar Ahmed committed
386
            sendMessage(i18n("Failed to commit.\n %1", QString::fromUtf8(git.readAllStandardError())), true);
Waqar Ahmed's avatar
Waqar Ahmed committed
387
        } else {
388
            m_commitMessage.clear();
389
            getStatus();
Waqar Ahmed's avatar
Waqar Ahmed committed
390
            sendMessage(i18n("Changes committed successfully."), false);
Waqar Ahmed's avatar
Waqar Ahmed committed
391
392
        }
    });
Waqar Ahmed's avatar
Waqar Ahmed committed
393
    git.setArguments(args);
394
    git.start(QProcess::ReadOnly);
Waqar Ahmed's avatar
Waqar Ahmed committed
395
396
397
398
399
}

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

Waqar Ahmed's avatar
Waqar Ahmed committed
403
    auto ciface = qobject_cast<KTextEditor::ConfigInterface *>(m_mainWin->activeView());
Waqar Ahmed's avatar
Waqar Ahmed committed
404
405
406
407
408
409
410
    QFont font;
    if (ciface) {
        font = ciface->configValue(QStringLiteral("font")).value<QFont>();
    } else {
        font = QFontDatabase::systemFont(QFontDatabase::FixedFont);
    }

Waqar Ahmed's avatar
Waqar Ahmed committed
411
    GitCommitDialog dialog(m_commitMessage, font);
Waqar Ahmed's avatar
Waqar Ahmed committed
412
413
414
415
    dialog.setWindowFlags(Qt::WindowSystemMenuHint | Qt::WindowTitleHint);

    int res = dialog.exec();
    if (res == QDialog::Accepted) {
Waqar Ahmed's avatar
Waqar Ahmed committed
416
        if (dialog.subject().isEmpty()) {
Waqar Ahmed's avatar
Waqar Ahmed committed
417
418
            return sendMessage(i18n("Commit message cannot be empty."), true);
        }
Waqar Ahmed's avatar
Waqar Ahmed committed
419
        m_commitMessage = dialog.subject() + QStringLiteral("[[\n\n]]") + dialog.description();
420
        commitChanges(dialog.subject(), dialog.description(), dialog.signoff());
Waqar Ahmed's avatar
Waqar Ahmed committed
421
422
423
    }
}

424
void GitWidget::gitStatusReady(int exit, QProcess::ExitStatus status)
425
{
426
427
428
    // sever connection
    disconnect(&git, &QProcess::finished, nullptr, nullptr);

429
430
431
    if (status != QProcess::NormalExit || exit != 0) {
        // we don't want to disturb non-git users
        // sendMessage(i18n("Failed to get git-status. Error: %1", QString::fromUtf8(git.readAllStandardError())), true);
Waqar Ahmed's avatar
Waqar Ahmed committed
432
433
434
        return;
    }

435
    QByteArray s = git.readAllStandardOutput();
436
    auto future = QtConcurrent::run(&GitUtils::parseStatus, s);
437
438
439
    m_gitStatusWatcher.setFuture(future);
}

Waqar Ahmed's avatar
Waqar Ahmed committed
440
441
442
void GitWidget::hideEmptyTreeNodes()
{
    const auto emptyRows = m_model->emptyRows();
Waqar Ahmed's avatar
Waqar Ahmed committed
443
    m_treeView->expand(m_model->getModelIndex((GitStatusModel::NodeStage)));
444
445
    // 1 because "Staged" will always be visible
    for (int i = 1; i < 4; ++i) {
Waqar Ahmed's avatar
Waqar Ahmed committed
446
447
448
449
        if (emptyRows.contains(i)) {
            m_treeView->setRowHidden(i, QModelIndex(), true);
        } else {
            m_treeView->setRowHidden(i, QModelIndex(), false);
450
451
452
            if (i != GitStatusModel::NodeUntrack) {
                m_treeView->expand(m_model->getModelIndex((GitStatusModel::ItemType)i));
            }
Waqar Ahmed's avatar
Waqar Ahmed committed
453
        }
Waqar Ahmed's avatar
Waqar Ahmed committed
454
    }
455
456
457

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

460
461
void GitWidget::parseStatusReady()
{
462
463
    GitUtils::GitParsedStatus s = m_gitStatusWatcher.result();
    m_model->addItems(std::move(s));
Waqar Ahmed's avatar
Waqar Ahmed committed
464
465

    hideEmptyTreeNodes();
466
}
Waqar Ahmed's avatar
Waqar Ahmed committed
467
468
469
470
471
472
473
474
475
476
477
478

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

479
480
481
482
483
void GitWidget::buildMenu()
{
    m_gitMenu = new QMenu(this);
    m_gitMenu->addAction(i18n("Refresh"), this, [this] {
        if (m_project) {
484
            getStatus();
485
486
        }
    });
Waqar Ahmed's avatar
Waqar Ahmed committed
487
488
489
490
    m_gitMenu->addAction(i18n("Checkout Branch"), this, [this] {
        BranchesDialog bd(this, m_mainWin, m_project->baseDir());
        bd.openDialog();
    });
Waqar Ahmed's avatar
Waqar Ahmed committed
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505

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

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"));
506
    auto showStashAct = menu->addAction(i18n("Show Stash Content"));
Waqar Ahmed's avatar
Waqar Ahmed committed
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539

    connect(stashAct, &QAction::triggered, this, [this] {
        StashDialog stashDialog(this, m_mainWin);
        stashDialog.openDialog(StashDialog::Stash);
    });
    connect(stashUAct, &QAction::triggered, this, [this] {
        StashDialog stashDialog(this, m_mainWin);
        stashDialog.openDialog(StashDialog::StashUntrackIncluded);
    });
    connect(stashKeepStagedAct, &QAction::triggered, this, [this] {
        StashDialog stashDialog(this, m_mainWin);
        stashDialog.openDialog(StashDialog::StashKeepIndex);
    });
    connect(popAct, &QAction::triggered, this, [this] {
        StashDialog stashDialog(this, m_mainWin);
        stashDialog.openDialog(StashDialog::StashPop);
    });
    connect(applyStashAct, &QAction::triggered, this, [this] {
        StashDialog stashDialog(this, m_mainWin);
        stashDialog.openDialog(StashDialog::StashApply);
    });
    connect(dropAct, &QAction::triggered, this, [this] {
        StashDialog stashDialog(this, m_mainWin);
        stashDialog.openDialog(StashDialog::StashDrop);
    });
    connect(popLastAct, &QAction::triggered, this, [this] {
        StashDialog stashDialog(this, m_mainWin);
        stashDialog.openDialog(StashDialog::StashPopLast);
    });
    connect(applyLastAct, &QAction::triggered, this, [this] {
        StashDialog stashDialog(this, m_mainWin);
        stashDialog.openDialog(StashDialog::StashApplyLast);
    });
540
541
542
543
    connect(showStashAct, &QAction::triggered, this, [this] {
        StashDialog stashDialog(this, m_mainWin);
        stashDialog.openDialog(StashDialog::ShowStashContent);
    });
Waqar Ahmed's avatar
Waqar Ahmed committed
544
545

    return menu;
546
547
}

548
549
550
551
552
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
553
554
void GitWidget::treeViewContextMenuEvent(QContextMenuEvent *e)
{
555
    if (auto selModel = m_treeView->selectionModel()) {
556
        if (selModel->selectedRows().count() > 1) {
557
558
559
560
            return selectedContextMenu(e);
        }
    }

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

564
    if (type == GitStatusModel::NodeChanges || type == GitStatusModel::NodeUntrack) {
Waqar Ahmed's avatar
Waqar Ahmed committed
565
        QMenu menu;
Waqar Ahmed's avatar
Waqar Ahmed committed
566
        auto stageAct = menu.addAction(i18n("Stage All"));
567
        bool untracked = type == GitStatusModel::NodeUntrack;
568
569
        auto discardAct = untracked ? menu.addAction(i18n("Remove All")) : menu.addAction(i18n("Discard All"));
        auto ignoreAct = untracked ? menu.addAction(i18n("Open .gitignore")) : nullptr;
570
571
572
573
574
575
576
577

        // get files
        const QVector<GitUtils::StatusItem> &files = untracked ? m_model->untrackedFiles() : m_model->changedFiles();
        QStringList filesList;
        filesList.reserve(files.size());
        for (const auto &file : files) {
            filesList.append(file.file);
        }
Waqar Ahmed's avatar
Waqar Ahmed committed
578

579
        // execute action
Waqar Ahmed's avatar
Waqar Ahmed committed
580
        auto act = menu.exec(m_treeView->viewport()->mapToGlobal(e->pos()));
Waqar Ahmed's avatar
Waqar Ahmed committed
581
        if (act == stageAct) {
582
            stage(filesList, type == GitStatusModel::NodeUntrack);
583
        } else if (act == discardAct && !untracked) {
584
585
586
587
            auto ret = confirm(this, i18n("Are you sure you want to remove these files?"));
            if (ret == KMessageBox::Yes) {
                discard(filesList);
            }
588
        } else if (act == discardAct && untracked) {
589
590
591
592
            auto ret = confirm(this, i18n("Are you sure you want to discard all changes?"));
            if (ret == KMessageBox::Yes) {
                clean(filesList);
            }
593
594
595
596
597
598
599
600
601
602
603
        } else if (untracked && act == ignoreAct) {
            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
604
        }
605
    } else if (type == GitStatusModel::NodeFile) {
Waqar Ahmed's avatar
Waqar Ahmed committed
606
        QMenu menu;
Waqar Ahmed's avatar
Waqar Ahmed committed
607
        bool staged = idx.internalId() == GitStatusModel::NodeStage;
608
609
        bool untracked = idx.internalId() == GitStatusModel::NodeUntrack;

610
        auto openFile = menu.addAction(i18n("Open file"));
Waqar Ahmed's avatar
Waqar Ahmed committed
611
        auto showDiffAct = untracked ? nullptr : menu.addAction(i18n("Show raw diff"));
612
        auto launchDifftoolAct = untracked ? nullptr : menu.addAction(i18n("Show in external git diff tool"));
Waqar Ahmed's avatar
Waqar Ahmed committed
613
        auto openAtHead = untracked ? nullptr : menu.addAction(i18n("Open at HEAD"));
Waqar Ahmed's avatar
Waqar Ahmed committed
614
        auto stageAct = staged ? menu.addAction(i18n("Unstage file")) : menu.addAction(i18n("Stage file"));
615
        auto discardAct = untracked ? menu.addAction(i18n("Remove")) : menu.addAction(i18n("Discard"));
Waqar Ahmed's avatar
Waqar Ahmed committed
616

617
        auto act = menu.exec(m_treeView->viewport()->mapToGlobal(e->pos()));
618
        const QString file = m_gitPath + idx.data(GitStatusModel::FileNameRole).toString();
Waqar Ahmed's avatar
Waqar Ahmed committed
619
        if (act == stageAct) {
Waqar Ahmed's avatar
Waqar Ahmed committed
620
            if (staged) {
621
                return unstage({file});
Waqar Ahmed's avatar
Waqar Ahmed committed
622
            }
623
            return stage({file});
624
        } else if (act == discardAct && !untracked) {
625
626
627
628
            auto ret = confirm(this, i18n("Are you sure you want to discard the changes in this file?"));
            if (ret == KMessageBox::Yes) {
                discard({file});
            }
629
        } else if (act == openAtHead && !untracked) {
630
            openAtHEAD(idx.data(GitStatusModel::FileNameRole).toString());
Waqar Ahmed's avatar
Waqar Ahmed committed
631
        } else if (act == showDiffAct && !untracked) {
632
            showDiff(file, staged);
633
        } else if (act == discardAct && untracked) {
634
635
636
637
            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
638
        } else if (act == launchDifftoolAct) {
639
            launchExternalDiffTool(idx.data(GitStatusModel::FileNameRole).toString(), staged);
640
641
        } else if (act == openFile) {
            m_mainWin->openUrl(QUrl::fromLocalFile(file));
642
643
644
645
646
647
        }
    } else if (type == GitStatusModel::NodeStage) {
        QMenu menu;
        auto stage = menu.addAction(i18n("Unstage All"));
        auto act = menu.exec(m_treeView->viewport()->mapToGlobal(e->pos()));

Waqar Ahmed's avatar
Waqar Ahmed committed
648
        // git reset -q HEAD --
649
        if (act == stage) {
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
            const QVector<GitUtils::StatusItem> &files = m_model->stagedFiles();
            QStringList filesList;
            filesList.reserve(filesList.size() + files.size());
            for (const auto &file : files) {
                filesList.append(file.file);
            }
            unstage(filesList);
        }
    }
}

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

    bool selectionHasStageItems = false;
    bool selectionHasChangedItems = false;

    if (auto selModel = m_treeView->selectionModel()) {
        const auto idxList = selModel->selectedIndexes();
        for (const auto &idx : idxList) {
671
            if (idx.internalId() == GitStatusModel::NodeStage) {
672
                selectionHasStageItems = true;
673
674
            } else if (!idx.parent().isValid()) {
                // can't allow main nodes to be selected
675
                return;
676
            } else {
677
                selectionHasChangedItems = true;
678
            }
679
            files.append(idx.data(GitStatusModel::FileNameRole).toString());
680
681
682
683
        }
    }

    // cant allow both
684
    if (selectionHasChangedItems && selectionHasStageItems) {
685
        return;
686
    }
687
688
689
690
691
692
693
694
695
696

    QMenu menu;
    auto stageAct = selectionHasStageItems ? menu.addAction(i18n("Unstage Selected Files")) : menu.addAction(i18n("Stage Selected Files"));
    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
697
698
699
        }
    }
}