1 /*
2 SPDX-FileCopyrightText: 2006-2009 David Nolden <david.nolden.kdevelop@art-master.de>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
7 #include "patchreview.h"
8
9 #include <QDir>
10 #include <QFileInfo>
11 #include <QStandardPaths>
12 #include <QTimer>
13 #include <QMimeDatabase>
14
15 #include <KActionCollection>
16 #include <KLocalizedString>
17 #include <KPluginFactory>
18 #include <KMessageBox>
19 #include <KIO/CopyJob>
20
21 #include <interfaces/idocument.h>
22 #include <interfaces/icore.h>
23 #include <interfaces/idocumentcontroller.h>
24 #include <interfaces/iuicontroller.h>
25 #include <interfaces/contextmenuextension.h>
26 #include <interfaces/context.h>
27 #include <interfaces/editorcontext.h>
28
29 #include <project/projectmodel.h>
30
31 #include <sublime/message.h>
32 #include <util/path.h>
33
34 #include <libkomparediff2/komparemodellist.h>
35 #include <libkomparediff2/kompare.h>
36 #include <libkomparediff2/diffsettings.h>
37
38 #include <KTextEditor/Document>
39 #include <KTextEditor/ModificationInterface>
40 #include <KTextEditor/MovingRange>
41 #include <KTextEditor/View>
42
43 ///Whether arbitrary exceptions that occurred while diff-parsing within the library should be caught
44 #define CATCHLIBDIFF
45
46 /* Exclude this file from doublequote_chars check as krazy doesn't understand
47 std::string*/
48 //krazy:excludeall=doublequote_chars
49 #include <sublime/controller.h>
50 #include <sublime/mainwindow.h>
51 #include <sublime/area.h>
52 #include <sublime/document.h>
53 #include <sublime/view.h>
54 #include <vcs/widgets/vcsdiffpatchsources.h>
55 #include "patchhighlighter.h"
56 #include "patchreviewtoolview.h"
57 #include "localpatchsource.h"
58 #include "debug.h"
59
60
61 using namespace KDevelop;
62
63 namespace
64 {
65 // Maximum number of files to open directly within a tab when the review is started
66 const int maximumFilesToOpenDirectly = 15;
67 }
68
seekHunk(bool forwards,const QUrl & fileName)69 void PatchReviewPlugin::seekHunk( bool forwards, const QUrl& fileName ) {
70 try {
71 qCDebug(PLUGIN_PATCHREVIEW) << forwards << fileName << fileName.isEmpty();
72 if ( !m_modelList )
73 throw "no model";
74
75 for ( int a = 0; a < m_modelList->modelCount(); ++a ) {
76 const Diff2::DiffModel* model = m_modelList->modelAt( a );
77 if ( !model || !model->differences() )
78 continue;
79
80 QUrl file = urlForFileModel( model );
81
82 if ( !fileName.isEmpty() && fileName != file )
83 continue;
84
85 IDocument* doc = ICore::self()->documentController()->documentForUrl( file );
86
87 if ( doc && m_highlighters.contains( doc->url() ) && m_highlighters[doc->url()] ) {
88 if ( doc->textDocument() ) {
89 const QList<KTextEditor::MovingRange*> ranges = m_highlighters[doc->url()]->ranges();
90
91 KTextEditor::View * v = doc->activeTextView();
92 if ( v ) {
93 int bestLine = -1;
94 KTextEditor::Cursor c = v->cursorPosition();
95 for (auto* range : ranges) {
96 const int line = range->start().line();
97
98 if ( forwards ) {
99 if ( line > c.line() && ( bestLine == -1 || line < bestLine ) )
100 bestLine = line;
101 } else {
102 if ( line < c.line() && ( bestLine == -1 || line > bestLine ) )
103 bestLine = line;
104 }
105 }
106 if ( bestLine != -1 ) {
107 v->setCursorPosition( KTextEditor::Cursor( bestLine, 0 ) );
108 return;
109 } else if(fileName.isEmpty()) {
110 int next = qBound(0, forwards ? a+1 : a-1, m_modelList->modelCount()-1);
111 if (next < maximumFilesToOpenDirectly) {
112 ICore::self()->documentController()->openDocument(urlForFileModel(m_modelList->modelAt(next)));
113 }
114 }
115 }
116 }
117 }
118 }
119 } catch ( const QString & str ) {
120 qCDebug(PLUGIN_PATCHREVIEW) << "seekHunk():" << str;
121 } catch ( const char * str ) {
122 qCDebug(PLUGIN_PATCHREVIEW) << "seekHunk():" << str;
123 }
124 qCDebug(PLUGIN_PATCHREVIEW) << "no matching hunk found";
125 }
126
addHighlighting(const QUrl & highlightFile,IDocument * document)127 void PatchReviewPlugin::addHighlighting( const QUrl& highlightFile, IDocument* document ) {
128 try {
129 if ( !modelList() )
130 throw "no model";
131
132 for ( int a = 0; a < modelList()->modelCount(); ++a ) {
133 Diff2::DiffModel* model = modelList()->modelAt( a );
134 if ( !model )
135 continue;
136
137 QUrl file = urlForFileModel( model );
138
139 if ( file != highlightFile )
140 continue;
141
142 qCDebug(PLUGIN_PATCHREVIEW) << "highlighting" << file.toDisplayString();
143
144 IDocument* doc = document;
145 if( !doc )
146 doc = ICore::self()->documentController()->documentForUrl( file );
147
148 qCDebug(PLUGIN_PATCHREVIEW) << "highlighting file" << file << "with doc" << doc;
149
150 if ( !doc || !doc->textDocument() )
151 continue;
152
153 removeHighlighting( file );
154
155 m_highlighters[file] = new PatchHighlighter(model, doc, this, (qobject_cast<LocalPatchSource*>(m_patch.data()) == nullptr));
156 }
157 } catch ( const QString & str ) {
158 qCDebug(PLUGIN_PATCHREVIEW) << "highlightFile():" << str;
159 } catch ( const char * str ) {
160 qCDebug(PLUGIN_PATCHREVIEW) << "highlightFile():" << str;
161 }
162 }
163
highlightPatch()164 void PatchReviewPlugin::highlightPatch() {
165 try {
166 if ( !modelList() )
167 throw "no model";
168
169 for ( int a = 0; a < modelList()->modelCount(); ++a ) {
170 const Diff2::DiffModel* model = modelList()->modelAt( a );
171 if ( !model )
172 continue;
173
174 QUrl file = urlForFileModel( model );
175
176 addHighlighting( file );
177 }
178 } catch ( const QString & str ) {
179 qCDebug(PLUGIN_PATCHREVIEW) << "highlightFile():" << str;
180 } catch ( const char * str ) {
181 qCDebug(PLUGIN_PATCHREVIEW) << "highlightFile():" << str;
182 }
183 }
184
removeHighlighting(const QUrl & file)185 void PatchReviewPlugin::removeHighlighting( const QUrl& file ) {
186 if ( file.isEmpty() ) {
187 ///Remove all highlighting
188 qDeleteAll( m_highlighters );
189 m_highlighters.clear();
190 } else {
191 HighlightMap::iterator it = m_highlighters.find( file );
192 if ( it != m_highlighters.end() ) {
193 delete *it;
194 m_highlighters.erase( it );
195 }
196 }
197 }
198
notifyPatchChanged()199 void PatchReviewPlugin::notifyPatchChanged() {
200 if (m_patch) {
201 qCDebug(PLUGIN_PATCHREVIEW) << "notifying patch change: " << m_patch->file();
202 m_updateKompareTimer->start();
203 } else {
204 m_updateKompareTimer->stop();
205 }
206 }
207
forceUpdate()208 void PatchReviewPlugin::forceUpdate() {
209 if( m_patch ) {
210 // don't trigger an update if we know the plugin cannot update itself
211 auto* vcsPatch = qobject_cast<VCSDiffPatchSource*>(m_patch.data());
212 if (!vcsPatch || vcsPatch->m_updater) {
213 m_patch->update();
214 notifyPatchChanged();
215 }
216 }
217 }
218
updateKompareModel()219 void PatchReviewPlugin::updateKompareModel() {
220 if ( !m_patch ) {
221 ///TODO: this method should be cleaned up, it can be called by the timer and
222 /// e.g. https://bugs.kde.org/show_bug.cgi?id=267187 shows how it could
223 /// lead to asserts before...
224 return;
225 }
226
227 qCDebug(PLUGIN_PATCHREVIEW) << "updating model";
228 removeHighlighting();
229 m_modelList.reset( nullptr );
230 m_depth = 0;
231 delete m_diffSettings;
232 {
233 IDocument* patchDoc = ICore::self()->documentController()->documentForUrl( m_patch->file() );
234 if( patchDoc )
235 patchDoc->reload();
236 }
237
238 QString patchFile;
239 if( m_patch->file().isLocalFile() )
240 patchFile = m_patch->file().toLocalFile();
241 else if( m_patch->file().isValid() && !m_patch->file().isEmpty() ) {
242 patchFile = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
243 bool ret = KIO::copy(m_patch->file(), QUrl::fromLocalFile(patchFile), KIO::HideProgressInfo)->exec();
244 if( !ret ) {
245 qCWarning(PLUGIN_PATCHREVIEW) << "Problem while downloading: " << m_patch->file() << "to" << patchFile;
246 patchFile.clear();
247 }
248 }
249
250 if (!patchFile.isEmpty()) //only try to construct the model if we have a patch to load
251 try {
252 m_diffSettings = new DiffSettings( nullptr );
253 m_kompareInfo.reset( new Kompare::Info() );
254 m_kompareInfo->localDestination = patchFile;
255 m_kompareInfo->localSource = m_patch->baseDir().toLocalFile();
256 m_kompareInfo->depth = m_patch->depth();
257 m_kompareInfo->applied = m_patch->isAlreadyApplied();
258
259 m_modelList.reset( new Diff2::KompareModelList( m_diffSettings.data(), new QWidget, this ) );
260 m_modelList->slotKompareInfo( m_kompareInfo.data() );
261
262 try {
263 m_modelList->openDirAndDiff();
264 } catch ( const QString & /*str*/ ) {
265 throw;
266 } catch ( ... ) {
267 throw QStringLiteral( "lib/libdiff2 crashed, memory may be corrupted. Please restart kdevelop." );
268 }
269
270 for (m_depth = 0; m_depth < 10; ++m_depth) {
271 bool allFound = true;
272 for( int i = 0; i < m_modelList->modelCount(); i++ ) {
273 if (!QFile::exists(urlForFileModel(m_modelList->modelAt(i)).toLocalFile())) {
274 allFound = false;
275 }
276 }
277 if (allFound) {
278 break; // found depth
279 }
280 }
281
282 emit patchChanged();
283
284 for( int i = 0; i < m_modelList->modelCount(); i++ ) {
285 const Diff2::DiffModel* model = m_modelList->modelAt( i );
286 for (auto* difference : *model->differences()) {
287 difference->apply(m_patch->isAlreadyApplied());
288 }
289 }
290
291 highlightPatch();
292
293 return;
294 } catch ( const QString & str ) {
295 KMessageBox::error(nullptr, str, i18nc("@title:window", "Kompare Model Update"));
296 } catch ( const char * str ) {
297 KMessageBox::error(nullptr, QLatin1String(str), i18nc("@title:window", "Kompare Model Update"));
298 }
299 removeHighlighting();
300 m_modelList.reset( nullptr );
301 m_depth = 0;
302 m_kompareInfo.reset( nullptr );
303 delete m_diffSettings;
304
305 emit patchChanged();
306 }
307
308 K_PLUGIN_FACTORY_WITH_JSON(KDevPatchReviewFactory, "kdevpatchreview.json",
309 registerPlugin<PatchReviewPlugin>();)
310
311 class PatchReviewToolViewFactory : public KDevelop::IToolViewFactory
312 {
313 public:
PatchReviewToolViewFactory(PatchReviewPlugin * plugin)314 explicit PatchReviewToolViewFactory( PatchReviewPlugin *plugin ) : m_plugin( plugin ) {}
315
create(QWidget * parent=nullptr)316 QWidget* create( QWidget *parent = nullptr ) override {
317 return new PatchReviewToolView( parent, m_plugin );
318 }
319
defaultPosition() const320 Qt::DockWidgetArea defaultPosition() const override
321 {
322 return Qt::BottomDockWidgetArea;
323 }
324
id() const325 QString id() const override {
326 return QStringLiteral("org.kdevelop.PatchReview");
327 }
328
329 private:
330 PatchReviewPlugin *m_plugin;
331 };
332
~PatchReviewPlugin()333 PatchReviewPlugin::~PatchReviewPlugin()
334 {
335 removeHighlighting();
336
337 // Tweak to work around a crash on OS X; see https://bugs.kde.org/show_bug.cgi?id=338829
338 // and http://qt-project.org/forums/viewthread/38406/#162801
339 // modified tweak: use setPatch() and deleteLater in that method.
340 setPatch(nullptr);
341 }
342
closeReview()343 void PatchReviewPlugin::closeReview()
344 {
345 if( m_patch ) {
346 IDocument* patchDocument = ICore::self()->documentController()->documentForUrl( m_patch->file() );
347 if (patchDocument) {
348 // Revert modifications to the text document which we've done in updateReview
349 patchDocument->setPrettyName( QString() );
350 patchDocument->textDocument()->setReadWrite( true );
351 auto* modif = qobject_cast<KTextEditor::ModificationInterface*>(patchDocument->textDocument());
352 modif->setModifiedOnDiskWarning( true );
353 }
354
355 removeHighlighting();
356 m_modelList.reset( nullptr );
357 m_depth = 0;
358
359 if (!qobject_cast<LocalPatchSource*>(m_patch.data())) {
360 // make sure "show" button still openes the file dialog to open a custom patch file
361 setPatch( new LocalPatchSource );
362 } else
363 emit patchChanged();
364
365 auto oldArea = ICore::self()->uiController()->activeArea();
366 if (oldArea->objectName() == QLatin1String("review")) {
367 if (ICore::self()->documentController()->saveAllDocumentsForWindow(ICore::self()->uiController()->activeMainWindow(),
368 IDocument::Default, true))
369 {
370 ICore::self()->uiController()->switchToArea(m_lastArea.isEmpty() ? QStringLiteral("code") : m_lastArea,
371 KDevelop::IUiController::ThisWindow);
372 if (oldArea->workingSetPersistent()) {
373 ICore::self()->uiController()->activeArea()->setWorkingSet(oldArea->workingSet(), true, oldArea);
374 }
375 }
376 }
377 }
378 }
379
cancelReview()380 void PatchReviewPlugin::cancelReview() {
381 if( m_patch ) {
382 m_patch->cancelReview();
383 closeReview();
384 }
385 }
386
finishReview(const QList<QUrl> & selection)387 void PatchReviewPlugin::finishReview(const QList<QUrl>& selection)
388 {
389 if( m_patch && m_patch->finishReview( selection ) ) {
390 closeReview();
391 }
392 }
393
startReview(IPatchSource * patch,IPatchReview::ReviewMode mode)394 void PatchReviewPlugin::startReview( IPatchSource* patch, IPatchReview::ReviewMode mode ) {
395 Q_UNUSED( mode );
396 emit startingNewReview();
397 setPatch( patch );
398 QMetaObject::invokeMethod(this, &PatchReviewPlugin::updateReview, Qt::QueuedConnection);
399 }
400
switchToEmptyReviewArea()401 void PatchReviewPlugin::switchToEmptyReviewArea()
402 {
403 const auto allAreas = ICore::self()->uiController()->allAreas();
404 for (Sublime::Area* area : allAreas) {
405 if (area->objectName() == QLatin1String("review")) {
406 area->setWorkingSet(QString(), false);
407 }
408 }
409
410 QString areaName = ICore::self()->uiController()->activeArea()->objectName();
411 if (areaName != QLatin1String("review")) {
412 m_lastArea = areaName;
413 ICore::self()->uiController()->switchToArea(QStringLiteral("review"), KDevelop::IUiController::ThisWindow);
414 } else {
415 m_lastArea.clear();
416 }
417 }
418
urlForFileModel(const Diff2::DiffModel * model)419 QUrl PatchReviewPlugin::urlForFileModel( const Diff2::DiffModel* model )
420 {
421 KDevelop::Path path(QDir::cleanPath(m_patch->baseDir().toLocalFile()));
422 QVector<QString> destPath = KDevelop::Path(QLatin1Char('/') + model->destinationPath()).segments();
423 if (destPath.size() >= (int)m_depth) {
424 destPath.remove(0, m_depth);
425 }
426 for (const QString& segment : qAsConst(destPath)) {
427 path.addPath(segment);
428 }
429 path.addPath(model->destinationFile());
430
431 return path.toUrl();
432 }
433
updateReview()434 void PatchReviewPlugin::updateReview()
435 {
436 if( !m_patch )
437 return;
438
439 m_updateKompareTimer->stop();
440
441 switchToEmptyReviewArea();
442
443 KDevelop::IDocumentController *docController = ICore::self()->documentController();
444 // don't add documents opened automatically to the Files/Open Recent list
445 IDocument* futureActiveDoc = docController->openDocument( m_patch->file(), KTextEditor::Range::invalid(),
446 IDocumentController::DoNotAddToRecentOpen );
447
448 updateKompareModel();
449
450 if ( !m_modelList || !futureActiveDoc || !futureActiveDoc->textDocument() ) {
451 // might happen if e.g. openDocument dialog was cancelled by user
452 // or under the theoretic possibility of a non-text document getting opened
453 return;
454 }
455
456 futureActiveDoc->textDocument()->setReadWrite( false );
457 futureActiveDoc->setPrettyName(i18nc("@title complete patch", "Overview"));
458 auto* modif = qobject_cast<KTextEditor::ModificationInterface*>(futureActiveDoc->textDocument());
459 modif->setModifiedOnDiskWarning( false );
460
461 docController->activateDocument( futureActiveDoc );
462
463 auto* toolView = qobject_cast<PatchReviewToolView*>(ICore::self()->uiController()->findToolView(i18nc("@title:window", "Patch Review"), m_factory));
464 Q_ASSERT( toolView );
465
466 //Open all relates files
467 for( int a = 0; a < m_modelList->modelCount() && a < maximumFilesToOpenDirectly; ++a ) {
468 QUrl absoluteUrl = urlForFileModel( m_modelList->modelAt( a ) );
469 if (absoluteUrl.isRelative()) {
470 const QString messageText = i18n("The base directory of the patch must be an absolute directory.");
471 auto* message = new Sublime::Message(messageText, Sublime::Message::Error);
472 ICore::self()->uiController()->postMessage(message);
473 break;
474 }
475
476 if( QFileInfo::exists( absoluteUrl.toLocalFile() ) && absoluteUrl.toLocalFile() != QLatin1String("/dev/null") )
477 {
478 toolView->open( absoluteUrl, false );
479 }else{
480 // Maybe the file was deleted
481 qCDebug(PLUGIN_PATCHREVIEW) << "could not open" << absoluteUrl << "because it doesn't exist";
482 }
483 }
484 }
485
setPatch(IPatchSource * patch)486 void PatchReviewPlugin::setPatch( IPatchSource* patch ) {
487 if ( patch == m_patch ) {
488 return;
489 }
490
491 if( m_patch ) {
492 disconnect( m_patch.data(), &IPatchSource::patchChanged, this, &PatchReviewPlugin::notifyPatchChanged );
493 m_patch->deleteLater();
494 }
495 m_patch = patch;
496
497 if( m_patch ) {
498 qCDebug(PLUGIN_PATCHREVIEW) << "setting new patch" << patch->name() << "with file" << patch->file() << "basedir" << patch->baseDir();
499
500 connect( m_patch.data(), &IPatchSource::patchChanged, this, &PatchReviewPlugin::notifyPatchChanged );
501 }
502 QString finishText = i18nc("@action", "Finish Review");
503 if( m_patch && !m_patch->finishReviewCustomText().isEmpty() )
504 finishText = m_patch->finishReviewCustomText();
505 m_finishReview->setText( finishText );
506 m_finishReview->setEnabled( patch );
507
508 notifyPatchChanged();
509 }
510
PatchReviewPlugin(QObject * parent,const QVariantList &)511 PatchReviewPlugin::PatchReviewPlugin( QObject *parent, const QVariantList & )
512 : KDevelop::IPlugin( QStringLiteral("kdevpatchreview"), parent ),
513 m_patch( nullptr ), m_factory( new PatchReviewToolViewFactory( this ) )
514 {
515 qRegisterMetaType<const Diff2::DiffModel*>( "const Diff2::DiffModel*" );
516
517 setXMLFile( QStringLiteral("kdevpatchreview.rc") );
518
519 connect( ICore::self()->documentController(), &IDocumentController::documentClosed, this, &PatchReviewPlugin::documentClosed );
520 connect( ICore::self()->documentController(), &IDocumentController::textDocumentCreated, this, &PatchReviewPlugin::textDocumentCreated );
521 connect( ICore::self()->documentController(), &IDocumentController::documentSaved, this, &PatchReviewPlugin::documentSaved );
522
523 m_updateKompareTimer = new QTimer( this );
524 m_updateKompareTimer->setSingleShot( true );
525 m_updateKompareTimer->setInterval(500);
526 connect( m_updateKompareTimer, &QTimer::timeout, this, &PatchReviewPlugin::updateKompareModel );
527
528 m_finishReview = new QAction(i18nc("@action", "Finish Review"), this);
529 m_finishReview->setIcon( QIcon::fromTheme( QStringLiteral("dialog-ok") ) );
530 actionCollection()->setDefaultShortcut( m_finishReview, Qt::CTRL|Qt::Key_Return );
531 actionCollection()->addAction(QStringLiteral("commit_or_finish_review"), m_finishReview);
532
533 const auto allAreas = ICore::self()->uiController()->allAreas();
534 for (Sublime::Area* area : allAreas) {
535 if (area->objectName() == QLatin1String("review"))
536 area->addAction(m_finishReview);
537 }
538
539 core()->uiController()->addToolView(i18nc("@title:window", "Patch Review"), m_factory, IUiController::None);
540
541 areaChanged(ICore::self()->uiController()->activeArea());
542 }
543
documentClosed(IDocument * doc)544 void PatchReviewPlugin::documentClosed( IDocument* doc ) {
545 removeHighlighting( doc->url() );
546 }
547
documentSaved(IDocument * doc)548 void PatchReviewPlugin::documentSaved( IDocument* doc ) {
549 // Only update if the url is not the patch-file, because our call to
550 // the reload() KTextEditor function also causes this signal,
551 // which would lead to an endless update loop.
552 // Also, don't automatically update local patch sources, because
553 // they may correspond to static files which don't match any more
554 // after an edit was done.
555 if (m_patch && doc->url() != m_patch->file() && !qobject_cast<LocalPatchSource*>(m_patch.data())) {
556 forceUpdate();
557 }
558 }
559
textDocumentCreated(IDocument * doc)560 void PatchReviewPlugin::textDocumentCreated( IDocument* doc ) {
561 if (m_patch) {
562 addHighlighting( doc->url(), doc );
563 }
564 }
565
unload()566 void PatchReviewPlugin::unload() {
567 core()->uiController()->removeToolView( m_factory );
568
569 KDevelop::IPlugin::unload();
570 }
571
areaChanged(Sublime::Area * area)572 void PatchReviewPlugin::areaChanged(Sublime::Area* area)
573 {
574 bool reviewing = area->objectName() == QLatin1String("review");
575 m_finishReview->setEnabled(reviewing);
576 if(!reviewing) {
577 closeReview();
578 }
579 }
580
contextMenuExtension(KDevelop::Context * context,QWidget * parent)581 KDevelop::ContextMenuExtension PatchReviewPlugin::contextMenuExtension(KDevelop::Context* context, QWidget* parent)
582 {
583 QList<QUrl> urls;
584
585 if ( context->type() == KDevelop::Context::FileContext ) {
586 auto* filectx = static_cast<KDevelop::FileContext*>(context);
587 urls = filectx->urls();
588 } else if ( context->type() == KDevelop::Context::ProjectItemContext ) {
589 auto* projctx = static_cast<KDevelop::ProjectItemContext*>(context);
590 const auto items = projctx->items();
591 for (KDevelop::ProjectBaseItem* item : items) {
592 if ( item->file() ) {
593 urls << item->file()->path().toUrl();
594 }
595 }
596 } else if ( context->type() == KDevelop::Context::EditorContext ) {
597 auto* econtext = static_cast<KDevelop::EditorContext*>(context);
598 urls << econtext->url();
599 }
600
601 if (urls.size() == 1) {
602 auto* reviewAction = new QAction(QIcon::fromTheme(QStringLiteral("text-x-patch")),
603 i18nc("@action:inmenu", "Review Patch"), parent);
604 reviewAction->setData(QVariant(urls[0]));
605 connect( reviewAction, &QAction::triggered, this, &PatchReviewPlugin::executeFileReviewAction );
606 ContextMenuExtension cm;
607 cm.addAction( KDevelop::ContextMenuExtension::VcsGroup, reviewAction );
608 return cm;
609 }
610
611 return KDevelop::IPlugin::contextMenuExtension(context, parent);
612 }
613
executeFileReviewAction()614 void PatchReviewPlugin::executeFileReviewAction()
615 {
616 auto* reviewAction = qobject_cast<QAction*>(sender());
617 KDevelop::Path path(reviewAction->data().toUrl());
618 auto* ps = new LocalPatchSource();
619 ps->setFilename(path.toUrl());
620 ps->setBaseDir(path.parent().toUrl());
621 ps->setAlreadyApplied(true);
622 ps->createWidget();
623 startReview(ps, OpenAndRaise);
624 }
625
626 #include "patchreview.moc"
627