File indexing completed on 2024-04-28 04:38:49

0001 /*
0002     SPDX-FileCopyrightText: 2020 Jonathan L. Verner <jonathan.verner@matfyz.cz>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "committoolview.h"
0008 
0009 #include "diffviewsctrl.h"
0010 #include "gitplugin.h"
0011 #include "repostatusmodel.h"
0012 #include "simplecommitform.h"
0013 
0014 #include "debug.h"
0015 
0016 #include <interfaces/icore.h>
0017 #include <interfaces/idocument.h>
0018 #include <interfaces/idocumentcontroller.h>
0019 #include <interfaces/iplugin.h>
0020 #include <interfaces/iproject.h>
0021 #include <interfaces/iprojectcontroller.h>
0022 #include <interfaces/iruncontroller.h>
0023 #include <interfaces/iuicontroller.h>
0024 #include <project/projectmodel.h>
0025 #include <util/path.h>
0026 #include <vcs/vcsjob.h>
0027 
0028 #include <KActionCollection>
0029 #include <KColorScheme>
0030 #include <KLocalizedString>
0031 #include <KTextEditor/Document>
0032 #include <KTextEditor/MovingInterface>
0033 #include <KTextEditor/MovingRange>
0034 #include <KTextEditor/View>
0035 
0036 
0037 #include <QAbstractItemView>
0038 #include <QAction>
0039 #include <QBoxLayout>
0040 #include <QDockWidget>
0041 #include <QLineEdit>
0042 #include <QList>
0043 #include <QMenu>
0044 #include <QSortFilterProxyModel>
0045 #include <QSplitter>
0046 #include <QStandardItemModel>
0047 #include <QStyledItemDelegate>
0048 #include <QTreeView>
0049 #include <QUrl>
0050 #include <QWidgetAction>
0051 
0052 using namespace KDevelop;
0053 
0054 CommitToolViewFactory::CommitToolViewFactory(RepoStatusModel* model)
0055     :
0056     m_statusmodel(model),
0057     m_diffViewsCtrl(new DiffViewsCtrl)
0058 {
0059 }
0060 
0061 QWidget* CommitToolViewFactory::create(QWidget* parent)
0062 {
0063     auto* tool =  new CommitToolView(parent, m_statusmodel);
0064     tool->connect(tool, &CommitToolView::updateDiff, m_diffViewsCtrl, [=](const QUrl& url, const RepoStatusModel::Areas area){
0065         m_diffViewsCtrl->updateDiff(url, area, DiffViewsCtrl::NoActivate);
0066     });
0067     tool->connect(tool, &CommitToolView::updateUrlDiffs, m_diffViewsCtrl, &DiffViewsCtrl::updateUrlDiffs);
0068     tool->connect(tool, &CommitToolView::updateProjectDiffs, m_diffViewsCtrl, &DiffViewsCtrl::updateProjectDiffs);
0069     tool->connect(tool, &CommitToolView::showDiff, m_diffViewsCtrl, [=](const QUrl& url, const RepoStatusModel::Areas area){
0070         m_diffViewsCtrl->updateDiff(url, area, DiffViewsCtrl::Activate);
0071     });
0072     tool->connect(tool, &CommitToolView::showSource, m_diffViewsCtrl, [=](const QUrl& url) {
0073         if (url.fileName().isEmpty()) return;
0074         auto* docCtrl = ICore::self()->documentController();
0075         if (auto* srcDoc = docCtrl->openDocument(url)) {
0076             docCtrl->activateDocument(srcDoc);
0077         }
0078     });
0079     return tool;
0080 }
0081 
0082 CommitToolViewFactory::~CommitToolViewFactory()
0083 {
0084     delete m_diffViewsCtrl;
0085 }
0086 
0087 
0088 Qt::DockWidgetArea CommitToolViewFactory::defaultPosition() const
0089 {
0090     return Qt::RightDockWidgetArea;
0091 }
0092 
0093 QString CommitToolViewFactory::id() const
0094 {
0095     return QStringLiteral("org.kdevelop.CommitToolView");
0096 }
0097 
0098 /**
0099  * A filter to be used on RepoStatusModel to hide the areas
0100  * (index, worktree, conflicts, untracked) which are empty
0101  */
0102 class FilterEmptyItemsProxyModel : public QSortFilterProxyModel
0103 {
0104 public:
0105     using QSortFilterProxyModel::QSortFilterProxyModel;
0106     bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override
0107     {
0108         const QModelIndex rowIndex = sourceModel()->index(sourceRow, 0, sourceParent);
0109 
0110         if (!QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent))
0111             return false;
0112 
0113         if (sourceModel()->hasChildren(rowIndex))
0114             return true;
0115 
0116         const auto area = rowIndex.data(RepoStatusModel::AreaRole);
0117         return area == RepoStatusModel::Index || area == RepoStatusModel::WorkTree || area == RepoStatusModel::Conflicts
0118             || area == RepoStatusModel::Untracked;
0119     }
0120 };
0121 
0122 /**
0123  * A style delegate to show active project in bold
0124  */
0125 class ActiveStyledDelegate : public QStyledItemDelegate
0126 {
0127     Q_OBJECT
0128 public:
0129     void initStyleOption(QStyleOptionViewItem* option, const QModelIndex& idx) const override
0130     {
0131         QStyledItemDelegate::initStyleOption(option, idx);
0132         if (idx == m_activeProject)
0133             option->font.setBold(true);
0134     }
0135     /**
0136      * Sets the active project which will be styled in bold.
0137      */
0138     void setActive(const QModelIndex& idx) { m_activeProject = idx; }
0139 
0140 private:
0141     QPersistentModelIndex m_activeProject;
0142 };
0143 
0144 void CommitToolView::doLayOut(const Qt::DockWidgetArea area)
0145 {
0146     if (layout()) {
0147         delete layout();
0148     }
0149 
0150     QSplitter* _splitter;
0151     QBoxLayout* _layout;
0152     if (area == Qt::LeftDockWidgetArea || area == Qt::RightDockWidgetArea || area == Qt::NoDockWidgetArea) {
0153         _layout = new QHBoxLayout(this);
0154         _splitter = new QSplitter(Qt::Vertical, this);
0155         _splitter->addWidget(m_commitForm);
0156         _splitter->addWidget(m_filter);
0157         _splitter->addWidget(m_view);
0158         _splitter->setStretchFactor(0, 1);
0159         _splitter->setStretchFactor(2, 5);
0160     } else {
0161         _layout = new QVBoxLayout(this);
0162         _splitter = new QSplitter(Qt::Horizontal, this);
0163         auto _filter_plus_view = new QSplitter(Qt::Vertical, this);
0164         _filter_plus_view->addWidget(m_filter);
0165         _filter_plus_view->addWidget(m_view);
0166         _splitter->addWidget(m_commitForm);
0167         _splitter->addWidget(_filter_plus_view);
0168     }
0169     _layout->addWidget(_splitter);
0170     m_filter->setMaximumHeight(35);
0171     setLayout(_layout);
0172 }
0173 
0174 
0175 
0176 CommitToolView::CommitToolView ( QWidget* parent, RepoStatusModel* repostatusmodel)
0177     : QWidget(parent)
0178     , m_statusmodel(repostatusmodel)
0179     , m_proxymodel(new FilterEmptyItemsProxyModel(this))
0180     , m_commitForm(new SimpleCommitForm(this))
0181     , m_view(new QTreeView(this))
0182     , m_filter(new QLineEdit(this))
0183     , m_refreshMenu(new QMenu(this))
0184     , m_toolviewMenu(new QMenu(this))
0185     , m_styleDelegate(new ActiveStyledDelegate)
0186 {
0187     setWindowIcon(QIcon::fromTheme(QStringLiteral("git")));
0188 
0189 
0190     // FIXME: We should get the current area dock area from somewhere (the sublime area?)
0191     //        Right now we initially assume it is in the Qt::RightWidgetArea and layout
0192     //        accordingly (this may be wrong if the user previously moved it to, e.g. the
0193     //        bottom so its restored in the bottom area); when the user moves the dock
0194     //        widget we re-layout it and then the layout is always correct, since the signal
0195     doLayOut(Qt::RightDockWidgetArea);
0196     connect(dynamic_cast<QDockWidget*>(parent), &QDockWidget::dockLocationChanged, this, &CommitToolView::doLayOut);
0197 
0198     // Creates a proxy model so that we can filter the
0199     // items by the text entered into the filter lineedit
0200     m_proxymodel->setSourceModel(m_statusmodel);
0201     m_proxymodel->setFilterCaseSensitivity(Qt::CaseInsensitive);
0202     m_proxymodel->setSortCaseSensitivity(Qt::CaseInsensitive);
0203     m_proxymodel->setSortRole(Qt::DisplayRole);
0204     m_proxymodel->setRecursiveFilteringEnabled(true);
0205     connect(m_filter, &QLineEdit::textEdited, m_proxymodel, &QSortFilterProxyModel::setFilterWildcard);
0206     m_filter->setToolTip(i18n("Filter by filename/project name"));
0207     m_filter->setPlaceholderText(i18n("Filter by filename/project name"));
0208 
0209     // Sets up the view
0210     m_view->setModel(m_proxymodel);
0211     m_view->setHeaderHidden(true);
0212     m_view->setEditTriggers(QAbstractItemView::NoEditTriggers);
0213     m_view->setSelectionMode(QAbstractItemView::SelectionMode::ContiguousSelection);
0214     m_view->setContextMenuPolicy(Qt::CustomContextMenu);
0215     m_view->setAnimated(true);
0216     m_view->setItemDelegate(m_styleDelegate);
0217 
0218     connect(m_view, &QTreeView::customContextMenuRequested, this, &CommitToolView::popupContextMenu);
0219     connect(m_view, &QTreeView::doubleClicked, this, &CommitToolView::dblClicked);
0220     connect(m_view, &QTreeView::clicked, this, &CommitToolView::clicked);
0221     connect(m_view, &QTreeView::expanded, this, &CommitToolView::activateProject);
0222 
0223     // Construct the tool view context menus & actions
0224     m_refreshModelAct = m_refreshMenu->addAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18n("Refresh"));
0225     m_stageFilesAct = m_toolviewMenu->addAction(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Stage selected"));
0226     m_unstageFilesAct
0227         = m_toolviewMenu->addAction(QIcon::fromTheme(QStringLiteral("list-remove")), i18n("Unstage selected"));
0228     m_revertFilesAct
0229         = m_toolviewMenu->addAction(QIcon::fromTheme(QStringLiteral("edit-undo")), i18n("Revert unstaged edits"));
0230 
0231     m_toolviewMenu->addAction(m_refreshModelAct);
0232 
0233     // Refresh diffs when documents are saved
0234     connect(ICore::self()->documentController(), &IDocumentController::documentSaved, this,
0235             [=](KDevelop::IDocument* doc) {
0236                 emit updateUrlDiffs(doc->url());
0237     });
0238 
0239 
0240 
0241     // Connect the commit form
0242     connect(m_commitForm, &SimpleCommitForm::committed, this, &CommitToolView::commitActiveProject);
0243 
0244     // Disable the commit button if the active project has no staged changes
0245     connect(m_statusmodel, &QAbstractItemModel::rowsRemoved, this, [=](const QModelIndex& parent) {
0246         if (parent.data(RepoStatusModel::AreaRole) == RepoStatusModel::IndexRoot
0247             && m_statusmodel->itemFromIndex(parent)->rowCount() == 0 && isActiveProject(parent.parent()))
0248             m_commitForm->disableCommitButton();
0249     });
0250     connect(m_statusmodel, &QAbstractItemModel::rowsInserted, this, [=](const QModelIndex& parent) {
0251         if (parent.data(RepoStatusModel::AreaRole) == RepoStatusModel::IndexRoot
0252             && m_statusmodel->itemFromIndex(parent)->rowCount() > 0 && isActiveProject(parent.parent()))
0253             m_commitForm->enableCommitButton();
0254     });
0255 }
0256 
0257 CommitToolView::~CommitToolView()
0258 {
0259     delete m_styleDelegate;
0260 }
0261 
0262 KDevelop::IProject* CommitToolView::activeProject() const
0263 {
0264     auto* proj_item = activeProjectItem();
0265     if (proj_item && isActiveProject(proj_item->index())) {
0266         return ICore::self()->projectController()->findProjectByName(
0267             proj_item->data(RepoStatusModel::NameRole).toString());
0268     }
0269     return nullptr;
0270 }
0271 
0272 QStandardItem* CommitToolView::activeProjectItem() const
0273 {
0274     for (auto* pr : m_statusmodel->projectRoots()) {
0275         if (isActiveProject(pr->index()))
0276             return pr;
0277     }
0278     return nullptr;
0279 }
0280 
0281 bool CommitToolView::isActiveProject(const QModelIndex& idx) const
0282 {
0283     return (m_view->isExpanded(m_proxymodel->mapFromSource(idx)));
0284 }
0285 
0286 void CommitToolView::activateProject(const QModelIndex& idx)
0287 {
0288     if (idx.data(RepoStatusModel::AreaRole).toInt() == RepoStatusModel::ProjectRoot) {
0289         m_styleDelegate->setActive(idx);
0290         auto repoIdx = m_proxymodel->mapToSource(idx);
0291         for (const auto* pr : m_statusmodel->projectRoots()) {
0292             if (pr->index() != repoIdx)
0293                 m_view->collapse(m_proxymodel->mapFromSource(pr->index()));
0294         }
0295         m_commitForm->setProjectName(idx.data(RepoStatusModel::NameRole).toString());
0296         m_commitForm->setBranchName(idx.data(RepoStatusModel::BranchNameRole).toString());
0297         m_commitForm->clearError();
0298         m_commitForm->enable();
0299         if (m_statusmodel->projectItem(m_statusmodel->itemFromIndex(repoIdx)).index->rowCount() == 0)
0300             m_commitForm->disableCommitButton();
0301         else
0302             m_commitForm->enableCommitButton();
0303     }
0304 }
0305 
0306 void CommitToolView::popupContextMenu(const QPoint& pos)
0307 {
0308     QList<QUrl> urls;
0309     const QModelIndexList selectionIdxs = m_view->selectionModel()->selectedIndexes();
0310 
0311     // If there are no selected files just show an action to refresh the model
0312     if (selectionIdxs.isEmpty()) {
0313         QModelIndex idx = m_view->indexAt(pos);
0314         IProject* project
0315             = ICore::self()->projectController()->findProjectByName(idx.data(RepoStatusModel::NameRole).toString());
0316 
0317         // Show the context menu & evaluate the results
0318         QAction* res = m_refreshMenu->exec(m_view->viewport()->mapToGlobal(pos));
0319         if (res == m_refreshModelAct) {
0320             if (project)
0321                 m_statusmodel->reload({ project });
0322             else
0323                 m_statusmodel->reloadAll();
0324         }
0325         return;
0326     }
0327 
0328     // Convert the selection into a list of urls;
0329     for (const QModelIndex& idx : selectionIdxs) {
0330         if (idx.column() == 0) {
0331             if (idx.parent().isValid())
0332                 urls += idx.data(RepoStatusModel::UrlRole).value<QUrl>();
0333         }
0334     }
0335 
0336     // Show the context menu & evaluate the results
0337     QAction* res = m_toolviewMenu->exec(m_view->viewport()->mapToGlobal(pos));
0338     if (res == m_refreshModelAct) {
0339         if (!urls.isEmpty())
0340             m_statusmodel->reload(urls);
0341         else
0342             m_statusmodel->reloadAll();
0343     } else if (res == m_stageFilesAct) {
0344         if (!urls.isEmpty())
0345             stageSelectedFiles(urls);
0346     } else if (res == m_unstageFilesAct) {
0347         if (!urls.isEmpty())
0348             unstageSelectedFiles(urls);
0349     } else if (res == m_revertFilesAct) {
0350         if (!urls.isEmpty())
0351             revertSelectedFiles(urls);
0352     }
0353 }
0354 
0355 void CommitToolView::dblClicked ( const QModelIndex& idx )
0356 {
0357     // A different action is performed based on where the
0358     // file that was double-clicked on is.
0359     switch (idx.data(RepoStatusModel::AreaRole).toInt()) {
0360 
0361     // Files in the staging area are unstaged
0362     case RepoStatusModel::Index:
0363         unstageSelectedFiles({ idx.data(RepoStatusModel::UrlRole).toUrl() });
0364         break;
0365     // Files in the other areas are staged for commit
0366     // (including marking conflicts as resolved and adding the
0367     //  untracked files into the repo)
0368     case RepoStatusModel::WorkTree:
0369     case RepoStatusModel::Conflicts:
0370     case RepoStatusModel::Untracked:
0371         idx.data(RepoStatusModel::UrlRole).toUrl();
0372         stageSelectedFiles({ idx.data(RepoStatusModel::UrlRole).toUrl() });
0373         break;
0374     default:
0375         break;
0376     }
0377 }
0378 
0379 void CommitToolView::clicked ( const QModelIndex& idx )
0380 {
0381     auto url = idx.data(RepoStatusModel::UrlRole).toUrl();
0382     auto projectUrl = idx.data(RepoStatusModel::ProjectUrlRole).toUrl();
0383 
0384     switch (idx.data(RepoStatusModel::AreaRole).toInt()) {
0385     case RepoStatusModel::IndexRoot:
0386         emit showDiff(projectUrl, RepoStatusModel::IndexRoot);
0387         break;
0388     case RepoStatusModel::Index:
0389         emit showDiff(url, RepoStatusModel::Index);
0390         break;
0391     case RepoStatusModel::WorkTreeRoot:
0392         emit showDiff(projectUrl, RepoStatusModel::WorkTreeRoot);
0393         break;
0394     case RepoStatusModel::WorkTree:
0395         emit showDiff(url, RepoStatusModel::WorkTree);
0396         break;
0397     case RepoStatusModel::Untracked:
0398         emit showSource(url);
0399         break;
0400     }
0401 }
0402 
0403 IBasicVersionControl* CommitToolView::vcsPluginForUrl ( const QUrl& url ) const
0404 {
0405     IProject* project = ICore::self()->projectController()->findProjectForUrl(url);
0406     IPlugin* vcsplugin = project ? project->versionControlPlugin() : nullptr;
0407     return vcsplugin ? vcsplugin->extension<IBasicVersionControl>() : nullptr;
0408 }
0409 
0410 // Note that the this is a dangerous operation;
0411 // we rely on the vcs job to show a confirmation dialog
0412 void CommitToolView::revertSelectedFiles ( const QList<QUrl>& urls )
0413 {
0414 
0415     IProject* project = ICore::self()->projectController()->findProjectForUrl(urls.front());
0416     IBasicVersionControl* vcs = vcsPluginForUrl(urls.front());
0417 
0418     if (vcs) {
0419         VcsJob* job = vcs->revert(urls, IBasicVersionControl::NonRecursive);
0420         job->setProperty("urls", QVariant::fromValue<QList<QUrl>>(urls));
0421         job->setProperty("project", QVariant::fromValue(project));
0422         ICore::self()->runController()->registerJob(job);
0423         connect(job, &VcsJob::resultsReady, this, [=]() {
0424             // Close the document tabs showing diffs for the urls
0425             for (const auto& url : urls) {
0426                 emit updateUrlDiffs(url);
0427             }
0428         });
0429     }
0430 }
0431 
0432 void CommitToolView::stageSelectedFiles ( const QList<QUrl>& urls )
0433 {
0434     IProject* project = ICore::self()->projectController()->findProjectForUrl(urls.front());
0435     IBasicVersionControl* vcs = vcsPluginForUrl(urls.front());
0436     if (vcs) {
0437         VcsJob* job = vcs->add(urls, IBasicVersionControl::NonRecursive);
0438         job->setProperty("urls", QVariant::fromValue<QList<QUrl>>(urls));
0439         job->setProperty("project", QVariant::fromValue(project));
0440         connect(job, &VcsJob::resultsReady, this, [=]() {
0441             // Close the document tabs showing diffs for the urls
0442             for (const auto& url : urls) {
0443                 emit updateUrlDiffs(url);
0444             }
0445         });
0446         ICore::self()->runController()->registerJob(job);
0447     }
0448 }
0449 
0450 void CommitToolView::unstageSelectedFiles(const QList<QUrl>& urls)
0451 {
0452     if (GitPlugin* git = dynamic_cast<GitPlugin*>(vcsPluginForUrl(urls.front()))) {
0453         IProject* project = ICore::self()->projectController()->findProjectForUrl(urls.front());
0454         VcsJob* job = git->reset(urls, IBasicVersionControl::NonRecursive);
0455         job->setProperty("urls", QVariant::fromValue<QList<QUrl>>(urls));
0456         job->setProperty("project", QVariant::fromValue(project));
0457         connect(job, &VcsJob::resultsReady, this, [=]() {
0458             for (const auto& url : urls) {
0459                 emit updateUrlDiffs(url);
0460             }
0461         });
0462         ICore::self()->runController()->registerJob(job);
0463     }
0464 }
0465 
0466 void CommitToolView::commitActiveProject()
0467 {
0468     if (auto* proj = activeProject()) {
0469         if (auto* vcs = proj->versionControlPlugin()->extension<GitPlugin>()) {
0470             QString msg = m_commitForm->summary();
0471             QString extended = m_commitForm->extendedDescription(70);
0472             if (extended.length() > 0)
0473                 msg += QStringLiteral("\n\n") + extended;
0474             VcsJob* job = vcs->commitStaged(msg, proj->projectItem()->path().toUrl());
0475             m_commitForm->clearError();
0476             m_commitForm->disable();
0477             connect(job, &VcsJob::finished, m_commitForm, [=]{
0478                 if (job->status() == VcsJob::JobSucceeded){
0479                     m_commitForm->clear();
0480                     emit updateProjectDiffs(proj);
0481                 } else {
0482                     m_commitForm->showError(i18n("Committing failed. See Version Control tool view."));
0483                 }
0484                 m_commitForm->enable();
0485             });
0486             ICore::self()->runController()->registerJob(job);
0487         }
0488     }
0489 }
0490 
0491 #include "committoolview.moc"
0492 #include "moc_committoolview.cpp"