1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "vcsbasesubmiteditor.h"
27 
28 #include "commonvcssettings.h"
29 #include "nicknamedialog.h"
30 #include "submiteditorfile.h"
31 #include "submiteditorwidget.h"
32 #include "submitfieldwidget.h"
33 #include "submitfilemodel.h"
34 #include "vcsbaseplugin.h"
35 #include "vcsoutputwindow.h"
36 #include "vcsplugin.h"
37 
38 #include <aggregation/aggregate.h>
39 
40 #include <coreplugin/find/basetextfind.h>
41 #include <coreplugin/icore.h>
42 #include <coreplugin/editormanager/editormanager.h>
43 
44 #include <extensionsystem/invoker.h>
45 #include <extensionsystem/pluginmanager.h>
46 
47 #include <utils/algorithm.h>
48 #include <utils/checkablemessagebox.h>
49 #include <utils/completingtextedit.h>
50 #include <utils/fileutils.h>
51 #include <utils/icon.h>
52 #include <utils/qtcassert.h>
53 #include <utils/qtcprocess.h>
54 #include <utils/temporarydirectory.h>
55 #include <utils/theme/theme.h>
56 
57 #include <texteditor/fontsettings.h>
58 #include <texteditor/texteditorsettings.h>
59 
60 #include <projectexplorer/project.h>
61 #include <projectexplorer/session.h>
62 
63 #include <QDir>
64 #include <QFileInfo>
65 #include <QPointer>
66 #include <QProcess>
67 #include <QPushButton>
68 #include <QSet>
69 #include <QStringListModel>
70 #include <QStyle>
71 #include <QToolBar>
72 #include <QAction>
73 #include <QApplication>
74 #include <QMessageBox>
75 #include <QCompleter>
76 
77 #include <cstring>
78 
79 enum { debug = 0 };
80 enum { wantToolBar = 0 };
81 
82 // Return true if word is meaningful and can be added to a completion model
acceptsWordForCompletion(const QString & word)83 static bool acceptsWordForCompletion(const QString &word)
84 {
85     return word.size() >= 7;
86 }
87 
88 /*!
89     \class VcsBase::VcsBaseSubmitEditorParameters
90 
91     \brief The VcsBaseSubmitEditorParameters class is a utility class
92     to parametrize a VcsBaseSubmitEditor.
93 */
94 
95 /*!
96     \class  VcsBase::VcsBaseSubmitEditor
97 
98     \brief The VcsBaseSubmitEditor class is the base class for a submit editor
99     based on the SubmitEditorWidget.
100 
101     Presents the commit message in a text editor and an
102     checkable list of modified files in a list window. The user can delete
103     files from the list by pressing unchecking them or diff the selection
104     by doubleclicking.
105 
106     The action matching the ids (unless 0) of the parameter struct will be
107     registered with the EditorWidget and submit/diff actions will be added to
108     a toolbar.
109 
110     For the given context, there must be only one instance of the editor
111     active.
112     To start a submit, set the submit template on the editor and the output
113     of the VCS status command listing the modified files as fileList and open
114     it.
115 
116     The submit process is started by listening on the editor close
117     signal and then asking the IDocument interface of the editor to save the file
118     within a DocumentManager::blockFileChange() section
119     and to launch the submit process. In addition, the action registered
120     for submit should be connected to a slot triggering the close of the
121     current editor in the editor manager.
122 */
123 
124 namespace VcsBase {
125 
126 using namespace Internal;
127 using namespace Utils;
128 
submitMessageCheckScript()129 static inline QString submitMessageCheckScript()
130 {
131     return VcsPlugin::instance()->settings().submitMessageCheckScript.value();
132 }
133 
134 class VcsBaseSubmitEditorPrivate
135 {
136 public:
137     VcsBaseSubmitEditorPrivate(SubmitEditorWidget *editorWidget,
138                                VcsBaseSubmitEditor *q);
139 
140     SubmitEditorWidget *m_widget;
141     QToolBar *m_toolWidget = nullptr;
142     VcsBaseSubmitEditorParameters m_parameters;
143     QString m_displayName;
144     QString m_checkScriptWorkingDirectory;
145     SubmitEditorFile m_file;
146 
147     QPointer<QAction> m_diffAction;
148     QPointer<QAction> m_submitAction;
149 
150     NickNameDialog *m_nickNameDialog = nullptr;
151 };
152 
VcsBaseSubmitEditorPrivate(SubmitEditorWidget * editorWidget,VcsBaseSubmitEditor * q)153 VcsBaseSubmitEditorPrivate::VcsBaseSubmitEditorPrivate(SubmitEditorWidget *editorWidget,
154                                                        VcsBaseSubmitEditor *q) :
155     m_widget(editorWidget), m_file(q)
156 {
157     auto completer = new QCompleter(q);
158     completer->setCaseSensitivity(Qt::CaseSensitive);
159     completer->setModelSorting(QCompleter::CaseSensitivelySortedModel);
160     m_widget->descriptionEdit()->setCompleter(completer);
161     m_widget->descriptionEdit()->setCompletionLengthThreshold(4);
162 }
163 
VcsBaseSubmitEditor(SubmitEditorWidget * editorWidget)164 VcsBaseSubmitEditor::VcsBaseSubmitEditor(SubmitEditorWidget *editorWidget)
165 {
166     setWidget(editorWidget);
167     d = new VcsBaseSubmitEditorPrivate(editorWidget, this);
168 }
169 
setParameters(const VcsBaseSubmitEditorParameters & parameters)170 void VcsBaseSubmitEditor::setParameters(const VcsBaseSubmitEditorParameters &parameters)
171 {
172     d->m_parameters = parameters;
173     d->m_file.setId(parameters.id);
174     d->m_file.setMimeType(QLatin1String(parameters.mimeType));
175 
176     setWidget(d->m_widget);
177     document()->setPreferredDisplayName(QCoreApplication::translate("VCS", d->m_parameters.displayName));
178 
179     // Message font according to settings
180     CompletingTextEdit *descriptionEdit = d->m_widget->descriptionEdit();
181     const TextEditor::FontSettings fs = TextEditor::TextEditorSettings::fontSettings();
182     const QTextCharFormat tf = fs.toTextCharFormat(TextEditor::C_TEXT);
183     descriptionEdit->setFont(tf.font());
184     const QTextCharFormat selectionFormat = fs.toTextCharFormat(TextEditor::C_SELECTION);
185     QPalette pal;
186     pal.setColor(QPalette::Base, tf.background().color());
187     pal.setColor(QPalette::Text, tf.foreground().color());
188     pal.setColor(QPalette::WindowText, tf.foreground().color());
189     if (selectionFormat.background().style() != Qt::NoBrush)
190         pal.setColor(QPalette::Highlight, selectionFormat.background().color());
191     pal.setBrush(QPalette::HighlightedText, selectionFormat.foreground());
192     descriptionEdit->setPalette(pal);
193 
194     d->m_file.setModified(false);
195     // We are always clean to prevent the editor manager from asking to save.
196 
197     connect(d->m_widget, &SubmitEditorWidget::diffSelected,
198             this, &VcsBaseSubmitEditor::slotDiffSelectedVcsFiles);
199     connect(descriptionEdit, &QTextEdit::textChanged,
200             this, &VcsBaseSubmitEditor::fileContentsChanged);
201 
202     const CommonVcsSettings &settings = VcsPlugin::instance()->settings();
203     // Add additional context menu settings
204     if (!settings.submitMessageCheckScript.value().isEmpty()
205             || !settings.nickNameMailMap.value().isEmpty()) {
206         auto sep = new QAction(this);
207         sep->setSeparator(true);
208         d->m_widget->addDescriptionEditContextMenuAction(sep);
209         // Run check action
210         if (!settings.submitMessageCheckScript.value().isEmpty()) {
211             auto checkAction = new QAction(tr("Check Message"), this);
212             connect(checkAction, &QAction::triggered,
213                     this, &VcsBaseSubmitEditor::slotCheckSubmitMessage);
214             d->m_widget->addDescriptionEditContextMenuAction(checkAction);
215         }
216         // Insert nick
217         if (!settings.nickNameMailMap.value().isEmpty()) {
218             auto insertAction = new QAction(tr("Insert Name..."), this);
219             connect(insertAction, &QAction::triggered, this, &VcsBaseSubmitEditor::slotInsertNickName);
220             d->m_widget->addDescriptionEditContextMenuAction(insertAction);
221         }
222     }
223     // Do we have user fields?
224     if (!settings.nickNameFieldListFile.value().isEmpty())
225         createUserFields(settings.nickNameFieldListFile.value());
226 
227     // wrapping. etc
228     slotUpdateEditorSettings();
229     connect(VcsPlugin::instance(), &VcsPlugin::settingsChanged,
230             this, &VcsBaseSubmitEditor::slotUpdateEditorSettings);
231     connect(Core::EditorManager::instance(), &Core::EditorManager::currentEditorChanged,
232             this, [this]() {
233                 if (Core::EditorManager::currentEditor() == this)
234                     updateFileModel();
235             });
236     connect(qApp, &QApplication::applicationStateChanged,
237             this, [this](Qt::ApplicationState state) {
238                 if (state == Qt::ApplicationActive)
239                     updateFileModel();
240             });
241 
242     auto aggregate = new Aggregation::Aggregate;
243     aggregate->add(new Core::BaseTextFind(descriptionEdit));
244     aggregate->add(this);
245 }
246 
~VcsBaseSubmitEditor()247 VcsBaseSubmitEditor::~VcsBaseSubmitEditor()
248 {
249     delete d->m_toolWidget;
250     delete d->m_widget;
251     delete d;
252 }
253 
slotUpdateEditorSettings()254 void VcsBaseSubmitEditor::slotUpdateEditorSettings()
255 {
256     const CommonVcsSettings &s = VcsPlugin::instance()->settings();
257     setLineWrapWidth(s.lineWrapWidth.value());
258     setLineWrap(s.lineWrap.value());
259 }
260 
261 // Return a trimmed list of non-empty field texts
fieldTexts(const QString & fileContents)262 static inline QStringList fieldTexts(const QString &fileContents)
263 {
264     QStringList rc;
265     const QStringList rawFields = fileContents.trimmed().split(QLatin1Char('\n'));
266     foreach (const QString &field, rawFields) {
267         const QString trimmedField = field.trimmed();
268         if (!trimmedField.isEmpty())
269             rc.push_back(trimmedField);
270     }
271     return rc;
272 }
273 
createUserFields(const QString & fieldConfigFile)274 void VcsBaseSubmitEditor::createUserFields(const QString &fieldConfigFile)
275 {
276     FileReader reader;
277     if (!reader.fetch(FilePath::fromString(fieldConfigFile),
278                       QIODevice::Text,
279                       Core::ICore::dialogParent())) {
280         return;
281     }
282     // Parse into fields
283     const QStringList fields = fieldTexts(QString::fromUtf8(reader.data()));
284     if (fields.empty())
285         return;
286     // Create a completer on user names
287     const QStandardItemModel *nickNameModel = VcsPlugin::instance()->nickNameModel();
288     auto completer = new QCompleter(NickNameDialog::nickNameList(nickNameModel), this);
289 
290     auto fieldWidget = new SubmitFieldWidget;
291     connect(fieldWidget, &SubmitFieldWidget::browseButtonClicked,
292             this, &VcsBaseSubmitEditor::slotSetFieldNickName);
293     fieldWidget->setCompleter(completer);
294     fieldWidget->setAllowDuplicateFields(true);
295     fieldWidget->setHasBrowseButton(true);
296     fieldWidget->setFields(fields);
297     d->m_widget->addSubmitFieldWidget(fieldWidget);
298 }
299 
registerActions(QAction * editorUndoAction,QAction * editorRedoAction,QAction * submitAction,QAction * diffAction)300 void VcsBaseSubmitEditor::registerActions(QAction *editorUndoAction, QAction *editorRedoAction,
301                                           QAction *submitAction, QAction *diffAction)
302 {
303     d->m_widget->registerActions(editorUndoAction, editorRedoAction, submitAction, diffAction);
304     d->m_diffAction = diffAction;
305     d->m_submitAction = submitAction;
306 }
307 
fileListSelectionMode() const308 QAbstractItemView::SelectionMode VcsBaseSubmitEditor::fileListSelectionMode() const
309 {
310     return d->m_widget->fileListSelectionMode();
311 }
312 
setFileListSelectionMode(QAbstractItemView::SelectionMode sm)313 void VcsBaseSubmitEditor::setFileListSelectionMode(QAbstractItemView::SelectionMode sm)
314 {
315     d->m_widget->setFileListSelectionMode(sm);
316 }
317 
isEmptyFileListEnabled() const318 bool VcsBaseSubmitEditor::isEmptyFileListEnabled() const
319 {
320     return d->m_widget->isEmptyFileListEnabled();
321 }
322 
setEmptyFileListEnabled(bool e)323 void VcsBaseSubmitEditor::setEmptyFileListEnabled(bool e)
324 {
325     d->m_widget->setEmptyFileListEnabled(e);
326 }
327 
lineWrap() const328 bool VcsBaseSubmitEditor::lineWrap() const
329 {
330     return d->m_widget->lineWrap();
331 }
332 
setLineWrap(bool w)333 void VcsBaseSubmitEditor::setLineWrap(bool w)
334 {
335     d->m_widget->setLineWrap(w);
336 }
337 
lineWrapWidth() const338 int VcsBaseSubmitEditor::lineWrapWidth() const
339 {
340     return d->m_widget->lineWrapWidth();
341 }
342 
setLineWrapWidth(int w)343 void VcsBaseSubmitEditor::setLineWrapWidth(int w)
344 {
345     d->m_widget->setLineWrapWidth(w);
346 }
347 
document() const348 Core::IDocument *VcsBaseSubmitEditor::document() const
349 {
350     return &d->m_file;
351 }
352 
checkScriptWorkingDirectory() const353 QString VcsBaseSubmitEditor::checkScriptWorkingDirectory() const
354 {
355     return d->m_checkScriptWorkingDirectory;
356 }
357 
setCheckScriptWorkingDirectory(const QString & s)358 void VcsBaseSubmitEditor::setCheckScriptWorkingDirectory(const QString &s)
359 {
360     d->m_checkScriptWorkingDirectory = s;
361 }
362 
createToolBar(const QWidget * someWidget,QAction * submitAction,QAction * diffAction)363 static QToolBar *createToolBar(const QWidget *someWidget, QAction *submitAction, QAction *diffAction)
364 {
365     // Create
366     auto toolBar = new QToolBar;
367     toolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
368     const int size = someWidget->style()->pixelMetric(QStyle::PM_SmallIconSize);
369     toolBar->setIconSize(QSize(size, size));
370     toolBar->addSeparator();
371 
372     if (submitAction)
373         toolBar->addAction(submitAction);
374     if (diffAction)
375         toolBar->addAction(diffAction);
376     return toolBar;
377 }
378 
toolBar()379 QWidget *VcsBaseSubmitEditor::toolBar()
380 {
381     if (!wantToolBar)
382         return nullptr;
383 
384     if (d->m_toolWidget)
385         return d->m_toolWidget;
386 
387     if (!d->m_diffAction && !d->m_submitAction)
388         return nullptr;
389 
390     // Create
391     d->m_toolWidget = createToolBar(d->m_widget, d->m_submitAction, d->m_diffAction);
392     return d->m_toolWidget;
393 }
394 
checkedFiles() const395 QStringList VcsBaseSubmitEditor::checkedFiles() const
396 {
397     return d->m_widget->checkedFiles();
398 }
399 
filesFromModel(SubmitFileModel * model)400 static QSet<FilePath> filesFromModel(SubmitFileModel *model)
401 {
402     QSet<FilePath> result;
403     result.reserve(model->rowCount());
404     for (int row = 0; row < model->rowCount(); ++row) {
405         result.insert(FilePath::fromString(
406             QFileInfo(model->repositoryRoot(), model->file(row)).absoluteFilePath()));
407     }
408     return result;
409 }
410 
setFileModel(SubmitFileModel * model)411 void VcsBaseSubmitEditor::setFileModel(SubmitFileModel *model)
412 {
413     QTC_ASSERT(model, return);
414     SubmitFileModel *oldModel = d->m_widget->fileModel();
415     QList<int> selected;
416     if (oldModel) {
417         model->updateSelections(oldModel);
418         selected = d->m_widget->selectedRows();
419     }
420     d->m_widget->setFileModel(model);
421     delete oldModel;
422     if (!selected.isEmpty())
423         d->m_widget->setSelectedRows(selected);
424 
425     const QSet<FilePath> files = filesFromModel(model);
426     // add file names to completion
427     QSet<QString> completionItems = Utils::transform(files, &FilePath::fileName);
428     QObject *cppModelManager = ExtensionSystem::PluginManager::getObjectByName("CppModelManager");
429     if (cppModelManager) {
430         const auto symbols = ExtensionSystem::invoke<QSet<QString>>(cppModelManager,
431                                                                     "symbolsInFiles",
432                                                                     files);
433         completionItems += Utils::filtered(symbols, acceptsWordForCompletion);
434     }
435 
436     // Populate completer with symbols
437     if (!completionItems.isEmpty()) {
438         QCompleter *completer = d->m_widget->descriptionEdit()->completer();
439         QStringList symbolsList = Utils::toList(completionItems);
440         symbolsList.sort();
441         completer->setModel(new QStringListModel(symbolsList, completer));
442     }
443 }
444 
fileModel() const445 SubmitFileModel *VcsBaseSubmitEditor::fileModel() const
446 {
447     return d->m_widget->fileModel();
448 }
449 
rowsToFiles(const QList<int> & rows) const450 QStringList VcsBaseSubmitEditor::rowsToFiles(const QList<int> &rows) const
451 {
452     if (rows.empty())
453         return QStringList();
454 
455     QStringList rc;
456     const SubmitFileModel *model = fileModel();
457     const int count = rows.size();
458     for (int i = 0; i < count; i++)
459         rc.push_back(model->file(rows.at(i)));
460     return rc;
461 }
462 
slotDiffSelectedVcsFiles(const QList<int> & rawList)463 void VcsBaseSubmitEditor::slotDiffSelectedVcsFiles(const QList<int> &rawList)
464 {
465     if (d->m_parameters.diffType == VcsBaseSubmitEditorParameters::DiffRows)
466         emit diffSelectedRows(rawList);
467     else
468         emit diffSelectedFiles(rowsToFiles(rawList));
469 }
470 
fileContents() const471 QByteArray VcsBaseSubmitEditor::fileContents() const
472 {
473     return description().toLocal8Bit();
474 }
475 
setFileContents(const QByteArray & contents)476 bool VcsBaseSubmitEditor::setFileContents(const QByteArray &contents)
477 {
478     setDescription(QString::fromUtf8(contents));
479     return true;
480 }
481 
description() const482 QString VcsBaseSubmitEditor::description() const
483 {
484     return d->m_widget->descriptionText();
485 }
486 
setDescription(const QString & text)487 void VcsBaseSubmitEditor::setDescription(const QString &text)
488 {
489     d->m_widget->setDescriptionText(text);
490 }
491 
isDescriptionMandatory() const492 bool VcsBaseSubmitEditor::isDescriptionMandatory() const
493 {
494     return d->m_widget->isDescriptionMandatory();
495 }
496 
setDescriptionMandatory(bool v)497 void VcsBaseSubmitEditor::setDescriptionMandatory(bool v)
498 {
499     d->m_widget->setDescriptionMandatory(v);
500 }
501 
502 enum { checkDialogMinimumWidth = 500 };
503 
withUnusedMnemonic(QString string,const QList<QPushButton * > & otherButtons)504 static QString withUnusedMnemonic(QString string, const QList<QPushButton *> &otherButtons)
505 {
506     QSet<QChar> mnemonics;
507     for (QPushButton *button : otherButtons) {
508         const QString text = button->text();
509         const int ampersandPos = text.indexOf('&');
510         if (ampersandPos >= 0 && ampersandPos < text.size() - 1)
511             mnemonics.insert(text.at(ampersandPos + 1));
512     }
513     for (int i = 0, total = string.length(); i < total; ++i) {
514         if (!mnemonics.contains(string.at(i)))
515             return string.insert(i, '&');
516     }
517     return string;
518 }
519 
520 VcsBaseSubmitEditor::PromptSubmitResult
promptSubmit(VcsBasePluginPrivate * plugin,bool * promptSettingOld,bool forcePrompt,bool canCommitOnFailure,BoolAspect * promptSetting)521         VcsBaseSubmitEditor::promptSubmit(VcsBasePluginPrivate *plugin,
522                                           bool *promptSettingOld,
523                                           bool forcePrompt,
524                                           bool canCommitOnFailure,
525                                           BoolAspect *promptSetting)
526 {
527     BoolAspect dummySetting;
528     if (!promptSetting && !promptSettingOld)
529         promptSetting = &dummySetting;
530     auto submitWidget = static_cast<SubmitEditorWidget *>(this->widget());
531 
532     Core::EditorManager::activateEditor(this, Core::EditorManager::IgnoreNavigationHistory);
533 
534     if (!submitWidget->isEnabled())
535         return SubmitDiscarded;
536 
537     QString errorMessage;
538 
539     const bool value = promptSettingOld ? *promptSettingOld : promptSetting->value();
540     const bool prompt = forcePrompt || value;
541 
542     // Pop up a message depending on whether the check succeeded and the
543     // user wants to be prompted
544     bool canCommit = checkSubmitMessage(&errorMessage) && submitWidget->canSubmit(&errorMessage);
545     if (canCommit && !prompt)
546         return SubmitConfirmed;
547     CheckableMessageBox mb(Core::ICore::dialogParent());
548     const QString commitName = plugin->commitDisplayName();
549     mb.setWindowTitle(tr("Close %1 %2 Editor").arg(plugin->displayName(), commitName));
550     mb.setIcon(QMessageBox::Question);
551     QString message;
552     if (canCommit) {
553         message = tr("What do you want to do with these changes?");
554     } else {
555         message = tr("Cannot %1%2.\nWhat do you want to do?",
556                      "%2 is an optional error message with ': ' prefix. Don't add space in front.")
557                 .arg(commitName.toLower(),
558                      errorMessage.isEmpty() ? errorMessage : ": " + errorMessage);
559     }
560     mb.setText(message);
561     mb.setCheckBoxText(tr("Prompt to %1").arg(commitName.toLower()));
562     mb.setChecked(value);
563     // Provide check box to turn off prompt ONLY if it was not forced
564     mb.setCheckBoxVisible(value && !forcePrompt);
565     QDialogButtonBox::StandardButtons buttons = QDialogButtonBox::Close | QDialogButtonBox::Cancel;
566     if (canCommit || canCommitOnFailure)
567         buttons |= QDialogButtonBox::Ok;
568     mb.setStandardButtons(buttons);
569     QPushButton *cancelButton = mb.button(QDialogButtonBox::Cancel);
570     // On Windows there is no mnemonic for Close. Set it explicitly.
571     mb.button(QDialogButtonBox::Close)->setText(tr("&Close"));
572     cancelButton->setText(tr("&Keep Editing"));
573     // forcePrompt is true when the editor is closed, and false when triggered by the submit action
574     if (forcePrompt)
575         cancelButton->setDefault(true);
576     if (QPushButton *commitButton = mb.button(QDialogButtonBox::Ok)) {
577         commitButton->setText(withUnusedMnemonic(commitName,
578                               {cancelButton, mb.button(QDialogButtonBox::Close)}));
579     }
580     if (mb.exec() == QDialog::Accepted) {
581         if (promptSettingOld)
582             *promptSettingOld = mb.isChecked();
583         else
584             promptSetting->setValue(mb.isChecked());
585     }
586     QAbstractButton *chosen = mb.clickedButton();
587     if (!chosen || chosen == cancelButton)
588         return SubmitCanceled;
589     if (chosen == mb.button(QDialogButtonBox::Close))
590         return SubmitDiscarded;
591     return SubmitConfirmed;
592 }
593 
promptForNickName()594 QString VcsBaseSubmitEditor::promptForNickName()
595 {
596     if (!d->m_nickNameDialog)
597         d->m_nickNameDialog = new NickNameDialog(VcsPlugin::instance()->nickNameModel(), d->m_widget);
598     if (d->m_nickNameDialog->exec() == QDialog::Accepted)
599        return d->m_nickNameDialog->nickName();
600     return QString();
601 }
602 
slotInsertNickName()603 void VcsBaseSubmitEditor::slotInsertNickName()
604 {
605     const QString nick = promptForNickName();
606     if (!nick.isEmpty())
607         d->m_widget->descriptionEdit()->textCursor().insertText(nick);
608 }
609 
slotSetFieldNickName(int i)610 void VcsBaseSubmitEditor::slotSetFieldNickName(int i)
611 {
612     if (SubmitFieldWidget *sfw = d->m_widget->submitFieldWidgets().constFirst()) {
613         const QString nick = promptForNickName();
614         if (!nick.isEmpty())
615             sfw->setFieldValue(i, nick);
616     }
617 }
618 
slotCheckSubmitMessage()619 void VcsBaseSubmitEditor::slotCheckSubmitMessage()
620 {
621     QString errorMessage;
622     if (!checkSubmitMessage(&errorMessage)) {
623         QMessageBox msgBox(QMessageBox::Warning, tr("Submit Message Check Failed"),
624                            errorMessage, QMessageBox::Ok, d->m_widget);
625         msgBox.setMinimumWidth(checkDialogMinimumWidth);
626         msgBox.exec();
627     }
628 }
629 
checkSubmitMessage(QString * errorMessage) const630 bool VcsBaseSubmitEditor::checkSubmitMessage(QString *errorMessage) const
631 {
632     const QString checkScript = submitMessageCheckScript();
633     if (checkScript.isEmpty())
634         return true;
635     QApplication::setOverrideCursor(Qt::WaitCursor);
636     const bool rc = runSubmitMessageCheckScript(checkScript, errorMessage);
637     QApplication::restoreOverrideCursor();
638     return rc;
639 }
640 
msgCheckScript(const QString & workingDir,const QString & cmd)641 static inline QString msgCheckScript(const QString &workingDir, const QString &cmd)
642 {
643     const QString nativeCmd = QDir::toNativeSeparators(cmd);
644     return workingDir.isEmpty() ?
645            VcsBaseSubmitEditor::tr("Executing %1").arg(nativeCmd) :
646            VcsBaseSubmitEditor::tr("Executing [%1] %2").
647            arg(QDir::toNativeSeparators(workingDir), nativeCmd);
648 }
649 
runSubmitMessageCheckScript(const QString & checkScript,QString * errorMessage) const650 bool VcsBaseSubmitEditor::runSubmitMessageCheckScript(const QString &checkScript, QString *errorMessage) const
651 {
652     // Write out message
653     TempFileSaver saver(Utils::TemporaryDirectory::masterDirectoryPath() + "/msgXXXXXX.txt");
654     saver.write(fileContents());
655     if (!saver.finalize(errorMessage))
656         return false;
657     // Run check process
658     VcsOutputWindow::appendShellCommandLine(msgCheckScript(d->m_checkScriptWorkingDirectory,
659                                                            checkScript));
660     QtcProcess checkProcess;
661     if (!d->m_checkScriptWorkingDirectory.isEmpty())
662         checkProcess.setWorkingDirectory(d->m_checkScriptWorkingDirectory);
663     checkProcess.setCommand({checkScript, {saver.filePath().toString()}});
664     checkProcess.start();
665     checkProcess.closeWriteChannel();
666     if (!checkProcess.waitForStarted()) {
667         *errorMessage = tr("The check script \"%1\" could not be started: %2").arg(checkScript, checkProcess.errorString());
668         return false;
669     }
670     QByteArray stdOutData;
671     QByteArray stdErrData;
672     if (!checkProcess.readDataFromProcess(30, &stdOutData, &stdErrData, false)) {
673         checkProcess.stopProcess();
674         *errorMessage = tr("The check script \"%1\" timed out.").
675                         arg(QDir::toNativeSeparators(checkScript));
676         return false;
677     }
678     if (checkProcess.exitStatus() != QProcess::NormalExit) {
679         *errorMessage = tr("The check script \"%1\" crashed.").
680                         arg(QDir::toNativeSeparators(checkScript));
681         return false;
682     }
683     if (!stdOutData.isEmpty())
684         VcsOutputWindow::appendSilently(QString::fromLocal8Bit(stdOutData));
685     const QString stdErr = QString::fromLocal8Bit(stdErrData);
686     if (!stdErr.isEmpty())
687         VcsOutputWindow::appendSilently(stdErr);
688     const int exitCode = checkProcess.exitCode();
689     if (exitCode != 0) {
690         const QString exMessage = tr("The check script returned exit code %1.").
691                                   arg(exitCode);
692         VcsOutputWindow::appendError(exMessage);
693         *errorMessage = stdErr;
694         if (errorMessage->isEmpty())
695             *errorMessage = exMessage;
696         return false;
697     }
698     return true;
699 }
700 
diffIcon()701 QIcon VcsBaseSubmitEditor::diffIcon()
702 {
703     using namespace Utils;
704     return Icon({
705         {":/vcsbase/images/diff_documents.png", Theme::PanelTextColorDark},
706         {":/vcsbase/images/diff_arrows.png", Theme::IconsStopColor}
707     }, Icon::Tint).icon();
708 }
709 
submitIcon()710 QIcon VcsBaseSubmitEditor::submitIcon()
711 {
712     using namespace Utils;
713     return Icon({
714         {":/vcsbase/images/submit_db.png", Theme::PanelTextColorDark},
715         {":/vcsbase/images/submit_arrow.png", Theme::IconsRunColor}
716     }, Icon::Tint | Icon::PunchEdges).icon();
717 }
718 
719 // Reduce a list of untracked files reported by a VCS down to the files
720 // that are actually part of the current project(s).
filterUntrackedFilesOfProject(const QString & repositoryDirectory,QStringList * untrackedFiles)721 void VcsBaseSubmitEditor::filterUntrackedFilesOfProject(const QString &repositoryDirectory,
722                                                         QStringList *untrackedFiles)
723 {
724     const QDir repoDir(repositoryDirectory);
725     for (QStringList::iterator it = untrackedFiles->begin(); it != untrackedFiles->end(); ) {
726         const QString path = repoDir.absoluteFilePath(*it);
727         if (ProjectExplorer::SessionManager::projectForFile(FilePath::fromString(path)))
728             ++it;
729         else
730             it = untrackedFiles->erase(it);
731     }
732 }
733 
734 } // namespace VcsBase
735