Commit 9f135202 authored by Waqar Ahmed's avatar Waqar Ahmed Committed by Christoph Cullmann
Browse files

Implement git stash



Following operations are implemented in this change:
- Stash (with message)
- Pop / Pop last
- Apply / Apply last
- Drop
Signed-off-by: Waqar Ahmed's avatarWaqar Ahmed <waqar.17a@gmail.com>
parent fca6d77d
......@@ -63,6 +63,7 @@ target_sources(
gitwidget.cpp
gitstatusmodel.cpp
gitcommitdialog.cpp
stashdialog.cpp
tools/kateprojectcodeanalysistoolcppcheck.cpp
tools/kateprojectcodeanalysistoolflake8.cpp
......
......@@ -7,6 +7,7 @@
#include "gitcommitdialog.h"
#include "gitstatusmodel.h"
#include "kateproject.h"
#include "stashdialog.h"
#include <QContextMenuEvent>
#include <QCoreApplication>
......@@ -124,6 +125,11 @@ void GitWidget::sendMessage(const QString &message, bool warn)
m_mainWin->activeView()->document()->postMessage(msg);
}
QProcess *GitWidget::gitprocess()
{
return &git;
}
void GitWidget::getStatus(bool untracked, bool submodules)
{
disconnect(&git, &QProcess::finished, nullptr, nullptr);
......@@ -446,6 +452,56 @@ void GitWidget::buildMenu()
}
});
m_gitMenu->addAction(i18n("Checkout Branch"), this, &GitWidget::checkoutBranch);
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"));
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);
});
return menu;
}
void GitWidget::treeViewContextMenuEvent(QContextMenuEvent *e)
......
......@@ -9,7 +9,6 @@
#include <QFutureWatcher>
#include <QProcess>
#include <QWidget>
#include <memory>
#include "git/gitstatus.h"
......@@ -37,6 +36,8 @@ public:
bool eventFilter(QObject *o, QEvent *e) override;
void getStatus(bool untracked = true, bool submodules = false);
void sendMessage(const QString &message, bool warn);
QProcess *gitprocess();
private:
QToolButton *m_menuBtn;
......@@ -63,9 +64,10 @@ private:
void clean(const QStringList &files);
void openAtHEAD(const QString &file);
void showDiff(const QString &file, bool staged);
void stash();
void launchExternalDiffTool(const QString &file, bool staged);
void commitChanges(const QString &msg, const QString &desc, bool signOff);
void sendMessage(const QString &message, bool warn);
QMenu *stashMenu();
void hideEmptyTreeNodes();
void treeViewContextMenuEvent(QContextMenuEvent *e);
......
/*
SPDX-FileCopyrightText: 2021 Waqar Ahmed <waqar.17a@gmail.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "stashdialog.h"
#include "git/gitutils.h"
#include "gitwidget.h"
#include <QCoreApplication>
#include <QKeyEvent>
#include <QLineEdit>
#include <QPainter>
#include <QProcess>
#include <QSortFilterProxyModel>
#include <QStandardItemModel>
#include <QStyledItemDelegate>
#include <QTextDocument>
#include <QTreeView>
#include <QVBoxLayout>
#include <QWidget>
#include <QtConcurrentRun>
#include <KTextEditor/MainWindow>
#include <KTextEditor/Message>
#include <KTextEditor/View>
#include <KLocalizedString>
#include <kfts_fuzzy_match.h>
constexpr int StashIndexRole = Qt::UserRole + 2;
class StashFilterModel : public QSortFilterProxyModel
{
public:
StashFilterModel(QObject *parent = nullptr)
: QSortFilterProxyModel(parent)
{
}
Q_SLOT void setFilterString(const QString &string)
{
beginResetModel();
m_pattern = string;
endResetModel();
}
protected:
bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const override
{
return sourceLeft.data(FuzzyScore).toInt() < sourceRight.data(FuzzyScore).toInt();
}
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
{
if (m_pattern.isEmpty()) {
return true;
}
int score = 0;
const auto idx = sourceModel()->index(sourceRow, 0, sourceParent);
const QString string = idx.data().toString();
const bool res = kfts::fuzzy_match(m_pattern, string, score);
sourceModel()->setData(idx, score, FuzzyScore);
return res;
}
private:
QString m_pattern;
static constexpr int FuzzyScore = Qt::UserRole + 1;
};
class StyleDelegate : public QStyledItemDelegate
{
public:
StyleDelegate(QObject *parent = nullptr)
: QStyledItemDelegate(parent)
{
}
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
{
QStyleOptionViewItem options = option;
initStyleOption(&options, index);
QString name = index.data().toString();
QVector<QTextLayout::FormatRange> formats;
int colon = name.indexOf(QLatin1Char(':'));
QString stashMessage = name.mid(colon + 1, name.length() - (colon + 1));
++colon;
QTextCharFormat bold;
bold.setFontWeight(QFont::Bold);
formats.append({0, colon, bold});
QTextCharFormat fmt;
fmt.setForeground(options.palette.link());
fmt.setFontWeight(QFont::Bold);
auto resFmts = kfts::get_fuzzy_match_formats(m_filterString, stashMessage, colon, fmt);
formats.append(resFmts);
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);
kfts::paintItemViewText(painter, name, options, formats);
painter->restore();
}
public Q_SLOTS:
void setFilterString(const QString &text)
{
m_filterString = text;
}
private:
QString m_filterString;
};
StashDialog::StashDialog(QWidget *parent, KTextEditor::MainWindow *mainWindow)
: QMenu(parent)
, m_mainWindow(mainWindow)
, m_gitwidget(qobject_cast<GitWidget *>(parent))
{
QVBoxLayout *layout = new QVBoxLayout();
layout->setSpacing(0);
layout->setContentsMargins(4, 4, 4, 4);
setLayout(layout);
m_lineEdit = new QLineEdit(this);
setFocusProxy(m_lineEdit);
layout->addWidget(m_lineEdit);
m_treeView = new QTreeView();
layout->addWidget(m_treeView, 1);
m_treeView->setTextElideMode(Qt::ElideLeft);
m_treeView->setUniformRowHeights(true);
m_model = new QStandardItemModel(this);
StyleDelegate *delegate = new StyleDelegate(this);
m_treeView->setItemDelegateForColumn(0, delegate);
m_proxyModel = new StashFilterModel(this);
m_proxyModel->setFilterRole(Qt::DisplayRole);
connect(m_lineEdit, &QLineEdit::returnPressed, this, &StashDialog::slotReturnPressed);
connect(m_lineEdit, &QLineEdit::textChanged, m_proxyModel, &StashFilterModel::setFilterString);
connect(m_lineEdit, &QLineEdit::textChanged, delegate, &StyleDelegate::setFilterString);
connect(m_lineEdit, &QLineEdit::textChanged, this, [this]() {
m_treeView->viewport()->update();
reselectFirst();
});
connect(m_treeView, &QTreeView::clicked, this, &StashDialog::slotReturnPressed);
m_proxyModel->setSourceModel(m_model);
m_treeView->setSortingEnabled(true);
m_treeView->setModel(m_proxyModel);
m_treeView->installEventFilter(this);
m_lineEdit->installEventFilter(this);
m_treeView->setHeaderHidden(true);
m_treeView->setRootIsDecorated(false);
m_treeView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_treeView->setSelectionMode(QTreeView::SingleSelection);
}
void StashDialog::openDialog(StashDialog::Mode m)
{
m_model->clear();
switch (m) {
case Mode::Stash:
case Mode::StashKeepIndex:
case Mode::StashUntrackIncluded:
m_lineEdit->setPlaceholderText(i18n("Stash message (optional). Enter to confirm, Esc to leave."));
m_currentMode = m;
break;
case Mode::StashPop:
case Mode::StashDrop:
case Mode::StashApply:
m_lineEdit->setPlaceholderText(i18n("Type to filter, Enter to pop stash, Esc to leave."));
m_currentMode = m;
getStashList();
break;
case Mode::StashApplyLast:
applyStash({});
return;
case Mode::StashPopLast:
popStash({});
return;
default:
return;
}
reselectFirst();
updateViewGeometry();
setFocus();
exec();
}
bool StashDialog::eventFilter(QObject *obj, QEvent *event)
{
// catch key presses + shortcut overrides to allow to have ESC as application wide shortcut, too, see bug 409856
if (event->type() == QEvent::KeyPress || event->type() == QEvent::ShortcutOverride) {
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
if (obj == m_lineEdit) {
const bool forward2list = (keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down) || (keyEvent->key() == Qt::Key_PageUp)
|| (keyEvent->key() == Qt::Key_PageDown);
if (forward2list) {
QCoreApplication::sendEvent(m_treeView, event);
return true;
}
if (keyEvent->key() == Qt::Key_Escape) {
m_lineEdit->clear();
keyEvent->accept();
hide();
return true;
}
} else {
const bool forward2input = (keyEvent->key() != Qt::Key_Up) && (keyEvent->key() != Qt::Key_Down) && (keyEvent->key() != Qt::Key_PageUp)
&& (keyEvent->key() != Qt::Key_PageDown) && (keyEvent->key() != Qt::Key_Tab) && (keyEvent->key() != Qt::Key_Backtab);
if (forward2input) {
QCoreApplication::sendEvent(m_lineEdit, event);
return true;
}
}
}
// hide on focus out, if neither input field nor list have focus!
else if (event->type() == QEvent::FocusOut && !(m_lineEdit->hasFocus() || m_treeView->hasFocus())) {
m_lineEdit->clear();
hide();
return true;
}
return QWidget::eventFilter(obj, event);
}
void StashDialog::slotReturnPressed()
{
switch (m_currentMode) {
case Mode::Stash:
stash(false, false);
break;
case Mode::StashKeepIndex:
stash(true, false);
break;
case Mode::StashUntrackIncluded:
stash(false, true);
break;
case Mode::StashApply:
applyStash(m_treeView->currentIndex().data(StashIndexRole).toByteArray());
break;
case Mode::StashPop:
popStash(m_treeView->currentIndex().data(StashIndexRole).toByteArray());
break;
case Mode::StashDrop:
dropStash(m_treeView->currentIndex().data(StashIndexRole).toByteArray());
break;
default:
break;
}
m_lineEdit->clear();
hide();
}
void StashDialog::reselectFirst()
{
QModelIndex index = m_proxyModel->index(0, 0);
m_treeView->setCurrentIndex(index);
}
void StashDialog::sendMessage(const QString &message, bool warn)
{
KTextEditor::Message *msg = new KTextEditor::Message(message, warn ? KTextEditor::Message::Warning : KTextEditor::Message::Positive);
msg->setPosition(KTextEditor::Message::TopInView);
msg->setAutoHide(3000);
msg->setAutoHideMode(KTextEditor::Message::Immediate);
msg->setView(m_mainWindow->activeView());
m_mainWindow->activeView()->document()->postMessage(msg);
}
void StashDialog::stash(bool keepIndex, bool includeUntracked)
{
QStringList args{QStringLiteral("stash"), QStringLiteral("-q")};
if (keepIndex) {
args.append(QStringLiteral("--keep-index"));
}
if (includeUntracked) {
args.append(QStringLiteral("-u"));
}
if (!m_lineEdit->text().isEmpty()) {
args.append(QStringLiteral("-m"));
args.append(m_lineEdit->text());
}
auto git = m_gitwidget->gitprocess();
auto gitWidget = m_gitwidget;
if (!git) {
return;
}
disconnect(git, &QProcess::finished, nullptr, nullptr);
git->setArguments(args);
git->start();
connect(git, &QProcess::finished, m_gitwidget, [gitWidget](int exitCode, QProcess::ExitStatus es) {
if (es != QProcess::NormalExit || exitCode != 0) {
gitWidget->sendMessage(i18n("Failed to stash changes"), true);
} else {
gitWidget->getStatus();
gitWidget->sendMessage(i18n("Changes stashed successfully."), true);
}
});
}
void StashDialog::getStashList()
{
auto git = m_gitwidget->gitprocess();
if (!git) {
return;
}
git->setArguments({QStringLiteral("stash"), QStringLiteral("list")});
git->start();
QList<QByteArray> stashList;
if (git->waitForStarted() && git->waitForFinished(-1)) {
if (git->exitStatus() == QProcess::NormalExit && git->exitCode() == 0) {
stashList = git->readAllStandardOutput().split('\n');
} else {
m_gitwidget->sendMessage(i18n("Failed to get stash list. Error:\n %1", QString::fromUtf8(git->readAllStandardError())), true);
}
}
// format stash@{}: message
for (const auto &stash : stashList) {
if (!stash.startsWith("stash@{")) {
continue;
}
int brackCloseIdx = stash.indexOf('}', 7);
if (brackCloseIdx < 0) {
continue;
}
QByteArray stashIdx = stash.mid(0, brackCloseIdx + 1);
QStandardItem *item = new QStandardItem(QString::fromUtf8(stash));
item->setData(stashIdx, StashIndexRole);
m_model->appendRow(item);
}
}
void StashDialog::popStash(const QByteArray &index, const QString &command)
{
auto git = m_gitwidget->gitprocess();
auto gitWidget = m_gitwidget;
if (!git) {
return;
}
disconnect(git, &QProcess::finished, nullptr, nullptr);
QStringList args{QStringLiteral("stash"), command};
if (!index.isEmpty()) {
args.append(QString::fromUtf8(index));
}
git->setArguments(args);
git->start();
connect(git, &QProcess::finished, gitWidget, [gitWidget, command](int exitCode, QProcess::ExitStatus es) {
disconnect(gitWidget->gitprocess(), &QProcess::finished, nullptr, nullptr);
if (es != QProcess::NormalExit || exitCode != 0) {
auto git = gitWidget->gitprocess();
if (command == QLatin1String("apply")) {
gitWidget->sendMessage(i18n("Failed to apply stash. Error:\n%1", QString::fromUtf8(git->readAllStandardError())), true);
} else if (command == QLatin1String("drop")) {
gitWidget->sendMessage(i18n("Failed to drop stash. Error:\n%1", QString::fromUtf8(git->readAllStandardError())), true);
} else {
gitWidget->sendMessage(i18n("Failed to pop stash. Error:\n%1", QString::fromUtf8(git->readAllStandardError())), true);
}
} else {
gitWidget->getStatus();
if (command == QLatin1String("apply")) {
gitWidget->sendMessage(i18n("Stash applied successfully."), true);
} else if (command == QLatin1String("drop")) {
gitWidget->sendMessage(i18n("Stash dropped successfully."), true);
} else {
gitWidget->sendMessage(i18n("Stash popped successfully."), true);
}
}
});
}
void StashDialog::applyStash(const QByteArray &index)
{
popStash(index, QStringLiteral("apply"));
}
void StashDialog::dropStash(const QByteArray &index)
{
popStash(index, QStringLiteral("drop"));
}
void StashDialog::updateViewGeometry()
{
m_treeView->resizeColumnToContents(0);
m_treeView->resizeColumnToContents(1);
QWidget *window = m_mainWindow->window();
const QSize centralSize = window->size();
// width: 2.4 of editor, height: 1/2 of editor
const QSize viewMaxSize(centralSize.width() / 2.4, centralSize.height() / 2);
const QSize widgetSize(centralSize.width() / 2.4, m_model->rowCount() == 0 ? m_lineEdit->sizeHint().height() * 2 : centralSize.height() / 2);
// Position should be central over window
const int xPos = std::max(0, (centralSize.width() - viewMaxSize.width()) / 2);
const int yPos = std::max(0, (centralSize.height() - viewMaxSize.height()) * 1 / 4);
const QPoint p(xPos, yPos);
move(p + window->pos());
this->setFixedSize(widgetSize);
}
/*
SPDX-FileCopyrightText: 2021 Waqar Ahmed <waqar.17a@gmail.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include <QMenu>
class QTreeView;
class QLineEdit;
class BranchesDialogModel;
class QAction;
class StashFilterModel;
class KActionCollection;
class QStandardItemModel;
class QProcess;
class GitWidget;
namespace KTextEditor
{
class MainWindow;
}
namespace GitUtils
{
struct CheckoutResult;
}
class StashDialog : public QMenu
{
Q_OBJECT
public:
enum Mode { None, Stash, StashKeepIndex, StashUntrackIncluded, StashPopLast, StashPop, StashDrop, StashApply, StashApplyLast };
StashDialog(QWidget *parent, KTextEditor::MainWindow *mainWindow);
void openDialog(Mode mode);
void updateViewGeometry();
Q_SIGNAL void branchChanged(const QString &branch);
protected:
bool eventFilter(QObject *obj, QEvent *event) override;
private Q_SLOTS:
void slotReturnPressed();
void reselectFirst();