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