File indexing completed on 2024-05-12 12:39:20
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"