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 ¶meters)
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