1 | #include <CommitChangesWidget.h> |
2 | #include <ui_CommitChangesWidget.h> |
3 | |
4 | #include <ClickableFrame.h> |
5 | #include <CommitInfo.h> |
6 | #include <FileWidget.h> |
7 | #include <GitBase.h> |
8 | #include <GitCache.h> |
9 | #include <GitLocal.h> |
10 | #include <GitQlientSettings.h> |
11 | #include <GitQlientStyles.h> |
12 | #include <GitRepoLoader.h> |
13 | #include <GitWip.h> |
14 | #include <RevisionFiles.h> |
15 | #include <UnstagedMenu.h> |
16 | |
17 | #include <QDir> |
18 | #include <QItemDelegate> |
19 | #include <QKeyEvent> |
20 | #include <QListWidgetItem> |
21 | #include <QMenu> |
22 | #include <QMessageBox> |
23 | #include <QPainter> |
24 | #include <QProcess> |
25 | #include <QRegExp> |
26 | #include <QScrollBar> |
27 | #include <QTextCodec> |
28 | #include <QTextStream> |
29 | #include <QToolTip> |
30 | |
31 | #include <QLogger.h> |
32 | |
33 | using namespace QLogger; |
34 | |
35 | QString CommitChangesWidget::lastMsgBeforeError; |
36 | |
37 | enum GitQlientRole |
38 | { |
39 | U_ListRole = Qt::UserRole, |
40 | U_IsConflict, |
41 | U_IsUntracked, |
42 | U_FullPath |
43 | }; |
44 | |
45 | CommitChangesWidget::CommitChangesWidget(const QSharedPointer<GitCache> &cache, const QSharedPointer<GitBase> &git, |
46 | QWidget *parent) |
47 | : QWidget(parent) |
48 | , ui(new Ui::CommitChangesWidget) |
49 | , mCache(cache) |
50 | , mGit(git) |
51 | { |
52 | ui->setupUi(this); |
53 | setAttribute(Qt::WA_DeleteOnClose); |
54 | |
55 | ui->amendFrame->setVisible(false); |
56 | |
57 | mTitleMaxLength = GitQlientSettings().globalValue("commitTitleMaxLength" , mTitleMaxLength).toInt(); |
58 | |
59 | ui->lCounter->setText(QString::number(mTitleMaxLength)); |
60 | ui->leCommitTitle->setMaxLength(mTitleMaxLength); |
61 | ui->teDescription->setMaximumHeight(100); |
62 | |
63 | connect(ui->leCommitTitle, &QLineEdit::textChanged, this, &CommitChangesWidget::updateCounter); |
64 | connect(ui->leCommitTitle, &QLineEdit::returnPressed, this, &CommitChangesWidget::commitChanges); |
65 | connect(ui->applyActionBtn, &QPushButton::clicked, this, &CommitChangesWidget::commitChanges); |
66 | connect(ui->warningButton, &QPushButton::clicked, this, [this]() { emit signalCancelAmend(mCurrentSha); }); |
67 | connect(ui->stagedFilesList, &StagedFilesList::signalResetFile, this, &CommitChangesWidget::resetFile); |
68 | connect(ui->stagedFilesList, &StagedFilesList::signalShowDiff, this, |
69 | [this](const QString &fileName) { requestDiff(mGit->getWorkingDir() + "/" + fileName); }); |
70 | connect(ui->unstagedFilesList, &QListWidget::customContextMenuRequested, this, |
71 | &CommitChangesWidget::showUnstagedMenu); |
72 | connect(ui->unstagedFilesList, &QListWidget::itemDoubleClicked, this, |
73 | [this](QListWidgetItem *item) { requestDiff(mGit->getWorkingDir() + "/" + item->toolTip()); }); |
74 | |
75 | ui->warningButton->setVisible(false); |
76 | ui->applyActionBtn->setText(tr("Commit" )); |
77 | } |
78 | |
79 | CommitChangesWidget::~CommitChangesWidget() |
80 | { |
81 | delete ui; |
82 | } |
83 | |
84 | void CommitChangesWidget::reload() |
85 | { |
86 | configure(mCurrentSha); |
87 | } |
88 | |
89 | void CommitChangesWidget::resetFile(QListWidgetItem *item) |
90 | { |
91 | QScopedPointer<GitLocal> git(new GitLocal(mGit)); |
92 | const auto ret = git->resetFile(item->toolTip()); |
93 | const auto revInfo = mCache->commitInfo(mCurrentSha); |
94 | const auto files = mCache->revisionFile(mCurrentSha, revInfo.firstParent()); |
95 | |
96 | for (auto i = 0; i < files->count(); ++i) |
97 | { |
98 | auto fileName = files->getFile(i); |
99 | |
100 | if (fileName == item->toolTip()) |
101 | { |
102 | const auto isUnknown = files->statusCmp(i, RevisionFiles::UNKNOWN); |
103 | const auto isInIndex = files->statusCmp(i, RevisionFiles::IN_INDEX); |
104 | const auto untrackedFile = !isInIndex && isUnknown; |
105 | const auto row = ui->stagedFilesList->row(item); |
106 | const auto iconPath = QString(":/icons/add" ); |
107 | const auto fileWidget = qobject_cast<FileWidget *>(ui->stagedFilesList->itemWidget(item)); |
108 | |
109 | QFontMetrics metrix(item->font()); |
110 | const auto clippedText = metrix.elidedText(item->toolTip(), Qt::ElideMiddle, width() - 10); |
111 | |
112 | if (isInIndex || untrackedFile) |
113 | { |
114 | item->setData(GitQlientRole::U_ListRole, QVariant::fromValue(ui->unstagedFilesList)); |
115 | |
116 | ui->stagedFilesList->takeItem(row); |
117 | ui->unstagedFilesList->addItem(item); |
118 | |
119 | const auto newFileWidget = new FileWidget(iconPath, clippedText, this); |
120 | newFileWidget->setTextColor(fileWidget->getTextColor()); |
121 | newFileWidget->setToolTip(fileName); |
122 | |
123 | connect(newFileWidget, &FileWidget::clicked, this, [this, item]() { addFileToCommitList(item); }); |
124 | ui->unstagedFilesList->setItemWidget(item, newFileWidget); |
125 | |
126 | delete fileWidget; |
127 | } |
128 | } |
129 | } |
130 | |
131 | if (ret.success) |
132 | emit signalUpdateWip(); |
133 | } |
134 | |
135 | QColor CommitChangesWidget::getColorForFile(const RevisionFiles &files, int index) const |
136 | { |
137 | const auto isUnknown = files.statusCmp(index, RevisionFiles::UNKNOWN); |
138 | const auto isInIndex = files.statusCmp(index, RevisionFiles::IN_INDEX); |
139 | const auto isConflict = files.statusCmp(index, RevisionFiles::CONFLICT); |
140 | const auto untrackedFile = !isInIndex && isUnknown; |
141 | const auto isPartiallyCached = files.statusCmp(index, RevisionFiles::PARTIALLY_CACHED); |
142 | |
143 | QColor myColor; |
144 | const auto isDeleted = files.statusCmp(index, RevisionFiles::DELETED); |
145 | |
146 | if (isConflict) |
147 | myColor = GitQlientStyles::getBlue(); |
148 | else if (isDeleted) |
149 | myColor = GitQlientStyles::getRed(); |
150 | else if (untrackedFile) |
151 | myColor = GitQlientStyles::getOrange(); |
152 | else if (files.statusCmp(index, RevisionFiles::NEW) || isUnknown || isInIndex || isPartiallyCached) |
153 | myColor = GitQlientStyles::getGreen(); |
154 | else |
155 | myColor = GitQlientStyles::getTextColor(); |
156 | |
157 | return myColor; |
158 | } |
159 | |
160 | void CommitChangesWidget::deleteUntrackedFiles() |
161 | { |
162 | for (auto i = 0; i < ui->unstagedFilesList->count(); ++i) |
163 | { |
164 | const auto item = ui->unstagedFilesList->item(i); |
165 | |
166 | if (item->data(GitQlientRole::U_IsUntracked).toBool()) |
167 | { |
168 | const auto path = QString("%1" ).arg(item->data(GitQlientRole::U_FullPath).toString()); |
169 | |
170 | QLog_Info("UI" , "Removing path: " + path); |
171 | |
172 | QProcess p; |
173 | p.setWorkingDirectory(mGit->getWorkingDir()); |
174 | p.start("rm" , { "-rf" , path }); |
175 | |
176 | p.waitForFinished(); |
177 | } |
178 | } |
179 | |
180 | emit signalCheckoutPerformed(); |
181 | } |
182 | |
183 | void CommitChangesWidget::prepareCache() |
184 | { |
185 | for (auto it = mInternalCache.begin(); it != mInternalCache.end(); ++it) |
186 | it.value().keep = false; |
187 | } |
188 | |
189 | void CommitChangesWidget::clearCache() |
190 | { |
191 | |
192 | for (auto it = mInternalCache.begin(); it != mInternalCache.end();) |
193 | { |
194 | if (!it.value().keep) |
195 | { |
196 | if (it.value().item) |
197 | delete it.value().item; |
198 | |
199 | it = mInternalCache.erase(it); |
200 | } |
201 | else |
202 | ++it; |
203 | } |
204 | } |
205 | |
206 | void CommitChangesWidget::insertFiles(const RevisionFiles &files, QListWidget *fileList) |
207 | { |
208 | for (auto &cachedItem : mInternalCache) // Move to prepareCache |
209 | cachedItem.keep = false; |
210 | |
211 | for (auto i = 0; i < files.count(); ++i) |
212 | { |
213 | const auto fileName = files.getFile(i); |
214 | const auto isUnknown = files.statusCmp(i, RevisionFiles::UNKNOWN); |
215 | const auto isInIndex = files.statusCmp(i, RevisionFiles::IN_INDEX); |
216 | const auto isConflict = files.statusCmp(i, RevisionFiles::CONFLICT); |
217 | const auto isPartiallyCached = files.statusCmp(i, RevisionFiles::PARTIALLY_CACHED); |
218 | const auto staged = isInIndex && !isUnknown && !isConflict; |
219 | const auto untrackedFile = !isInIndex && isUnknown; |
220 | auto wip = QString("%1-%2" ).arg(fileName, ui->stagedFilesList->objectName()); |
221 | |
222 | if (staged || isPartiallyCached) |
223 | { |
224 | if (!mInternalCache.contains(wip)) |
225 | { |
226 | const auto color = getColorForFile(files, i); |
227 | const auto itemPair = fillFileItemInfo(fileName, isConflict, untrackedFile, QString(":/icons/remove" ), |
228 | color, ui->stagedFilesList); |
229 | connect(itemPair.second, &FileWidget::clicked, this, [this, item = itemPair.first]() { resetFile(item); }); |
230 | |
231 | ui->stagedFilesList->setItemWidget(itemPair.first, itemPair.second); |
232 | itemPair.first->setSizeHint(itemPair.second->sizeHint()); |
233 | } |
234 | else |
235 | mInternalCache[wip].keep = true; |
236 | } |
237 | |
238 | if (!staged) |
239 | { |
240 | wip = QString("%1-%2" ).arg(fileName, fileList->objectName()); |
241 | if (!mInternalCache.contains(wip)) |
242 | { |
243 | // Item configuration |
244 | auto color = getColorForFile(files, i); |
245 | |
246 | // If the item is not new but the color is green this is not correct. |
247 | // It means that the file was partially staged so the color backs to default. |
248 | if (!files.statusCmp(i, RevisionFiles::NEW) && color == GitQlientStyles::getGreen()) |
249 | color = GitQlientStyles::getTextColor(); |
250 | |
251 | const auto itemPair |
252 | = fillFileItemInfo(fileName, isConflict, untrackedFile, QString(":/icons/add" ), color, fileList); |
253 | |
254 | connect(itemPair.second, &FileWidget::clicked, this, |
255 | [this, item = itemPair.first]() { addFileToCommitList(item); }); |
256 | |
257 | fileList->setItemWidget(itemPair.first, itemPair.second); |
258 | itemPair.first->setSizeHint(itemPair.second->sizeHint()); |
259 | } |
260 | else |
261 | mInternalCache[wip].keep = true; |
262 | } |
263 | } |
264 | } |
265 | |
266 | QPair<QListWidgetItem *, FileWidget *> CommitChangesWidget::fillFileItemInfo(const QString &file, bool isConflict, |
267 | bool isUntracked, const QString &icon, |
268 | const QColor &color, QListWidget *parent) |
269 | { |
270 | auto modName = file; |
271 | auto item = new QListWidgetItem(parent); |
272 | |
273 | item->setData(GitQlientRole::U_FullPath, file); |
274 | |
275 | if (isConflict) |
276 | { |
277 | modName = QString(file + " (conflicts)" ); |
278 | |
279 | item->setData(GitQlientRole::U_IsConflict, isConflict); |
280 | } |
281 | |
282 | item->setData(GitQlientRole::U_ListRole, QVariant::fromValue(parent)); |
283 | item->setData(GitQlientRole::U_IsUntracked, isUntracked); |
284 | item->setToolTip(modName); |
285 | |
286 | QFontMetrics metrix(item->font()); |
287 | const auto clippedText = metrix.elidedText(modName, Qt::ElideMiddle, width() - 10); |
288 | |
289 | const auto fileWidget = new FileWidget(icon, clippedText, this); |
290 | fileWidget->setTextColor(color); |
291 | fileWidget->setToolTip(modName); |
292 | |
293 | mInternalCache.insert(QString("%1-%2" ).arg(file, parent->objectName()), { true, item }); |
294 | |
295 | return qMakePair(item, fileWidget); |
296 | } |
297 | |
298 | void CommitChangesWidget::addAllFilesToCommitList() |
299 | { |
300 | QStringList files; |
301 | |
302 | for (auto i = ui->unstagedFilesList->count() - 1; i >= 0; --i) |
303 | files += addFileToCommitList(ui->unstagedFilesList->item(i), false); |
304 | |
305 | const auto git = QScopedPointer<GitLocal>(new GitLocal(mGit)); |
306 | |
307 | if (const auto ret = git->markFilesAsResolved(files); ret.success) |
308 | { |
309 | QScopedPointer<GitWip> git(new GitWip(mGit, mCache)); |
310 | git->updateWip(); |
311 | } |
312 | |
313 | ui->applyActionBtn->setEnabled(ui->stagedFilesList->count() > 0); |
314 | } |
315 | |
316 | void CommitChangesWidget::requestDiff(const QString &fileName) |
317 | { |
318 | const auto isCached = qobject_cast<StagedFilesList *>(sender()) == ui->stagedFilesList; |
319 | emit signalShowDiff(CommitInfo::ZERO_SHA, mCache->commitInfo(CommitInfo::ZERO_SHA).firstParent(), fileName, |
320 | isCached); |
321 | } |
322 | |
323 | QString CommitChangesWidget::addFileToCommitList(QListWidgetItem *item, bool updateGit) |
324 | { |
325 | const auto fileList = qvariant_cast<QListWidget *>(item->data(GitQlientRole::U_ListRole)); |
326 | const auto fileWidget = qobject_cast<FileWidget *>(fileList->itemWidget(item)); |
327 | const auto fileName = fileWidget->toolTip().remove(tr("(conflicts)" )).trimmed(); |
328 | |
329 | if (updateGit) |
330 | { |
331 | const auto git = QScopedPointer<GitLocal>(new GitLocal(mGit)); |
332 | |
333 | if (const auto ret = git->stageFile(fileName); ret.success) |
334 | { |
335 | QScopedPointer<GitWip> git(new GitWip(mGit, mCache)); |
336 | git->updateWip(); |
337 | } |
338 | } |
339 | |
340 | const auto row = fileList->row(item); |
341 | fileList->removeItemWidget(item); |
342 | fileList->takeItem(row); |
343 | |
344 | const auto newKey = QString("%1-%2" ).arg(fileName, ui->stagedFilesList->objectName()); |
345 | |
346 | if (!mInternalCache.contains(newKey)) |
347 | { |
348 | const auto wip = mInternalCache.take(QString("%1-%2" ).arg(fileName, fileList->objectName())); |
349 | mInternalCache.insert(newKey, wip); |
350 | |
351 | QFontMetrics metrix(item->font()); |
352 | const auto clippedText = metrix.elidedText(fileName, Qt::ElideMiddle, width() - 10); |
353 | |
354 | const auto newFileWidget = new FileWidget(":/icons/remove" , clippedText, this); |
355 | newFileWidget->setTextColor(fileWidget->getTextColor()); |
356 | newFileWidget->setToolTip(fileName); |
357 | |
358 | connect(newFileWidget, &FileWidget::clicked, this, [this, item]() { removeFileFromCommitList(item); }); |
359 | |
360 | ui->stagedFilesList->addItem(item); |
361 | ui->stagedFilesList->setItemWidget(item, newFileWidget); |
362 | |
363 | if (item->data(GitQlientRole::U_IsConflict).toBool()) |
364 | { |
365 | newFileWidget->setText(fileName); |
366 | } |
367 | } |
368 | |
369 | delete fileWidget; |
370 | |
371 | ui->applyActionBtn->setEnabled(true); |
372 | |
373 | return fileName; |
374 | } |
375 | |
376 | void CommitChangesWidget::revertAllChanges() |
377 | { |
378 | auto needsUpdate = false; |
379 | |
380 | for (auto i = ui->unstagedFilesList->count() - 1; i >= 0; --i) |
381 | { |
382 | QScopedPointer<GitLocal> git(new GitLocal(mGit)); |
383 | needsUpdate |= git->checkoutFile(ui->unstagedFilesList->takeItem(i)->toolTip()); |
384 | } |
385 | |
386 | if (needsUpdate) |
387 | emit signalCheckoutPerformed(); |
388 | } |
389 | |
390 | void CommitChangesWidget::removeFileFromCommitList(QListWidgetItem *item) |
391 | { |
392 | if (item->flags() & Qt::ItemIsSelectable) |
393 | { |
394 | const auto itemOriginalList = qvariant_cast<QListWidget *>(item->data(GitQlientRole::U_ListRole)); |
395 | const auto row = ui->stagedFilesList->row(item); |
396 | const auto fileWidget = qobject_cast<FileWidget *>(ui->stagedFilesList->itemWidget(item)); |
397 | const auto fileName = fileWidget->toolTip(); |
398 | |
399 | const auto wip = mInternalCache.take(QString("%1-%2" ).arg(fileName, ui->stagedFilesList->objectName())); |
400 | mInternalCache.insert(QString("%1-%2" ).arg(fileName, itemOriginalList->objectName()), wip); |
401 | |
402 | QFontMetrics metrix(item->font()); |
403 | const auto clippedText = metrix.elidedText(fileName, Qt::ElideMiddle, width() - 10); |
404 | |
405 | const auto newFileWidget = new FileWidget(":/icons/add" , clippedText, this); |
406 | newFileWidget->setTextColor(fileWidget->getTextColor()); |
407 | newFileWidget->setToolTip(fileName); |
408 | |
409 | connect(newFileWidget, &FileWidget::clicked, this, [this, item]() { addFileToCommitList(item); }); |
410 | |
411 | QScopedPointer<GitLocal> git = QScopedPointer<GitLocal>(new GitLocal(mGit)); |
412 | git->resetFile(fileName); |
413 | |
414 | if (item->data(GitQlientRole::U_IsConflict).toBool()) |
415 | { |
416 | newFileWidget->setText(fileName + tr(" (conflicts)" )); |
417 | } |
418 | |
419 | delete fileWidget; |
420 | |
421 | ui->stagedFilesList->removeItemWidget(item); |
422 | const auto item = ui->stagedFilesList->takeItem(row); |
423 | |
424 | itemOriginalList->addItem(item); |
425 | itemOriginalList->setItemWidget(item, newFileWidget); |
426 | |
427 | ui->applyActionBtn->setDisabled(ui->stagedFilesList->count() == 0); |
428 | } |
429 | } |
430 | |
431 | QStringList CommitChangesWidget::getFiles() |
432 | { |
433 | QStringList selFiles; |
434 | const auto totalItems = ui->stagedFilesList->count(); |
435 | |
436 | for (auto i = 0; i < totalItems; ++i) |
437 | { |
438 | const auto fileWidget = static_cast<FileWidget *>(ui->stagedFilesList->itemWidget(ui->stagedFilesList->item(i))); |
439 | selFiles.append(fileWidget->toolTip()); |
440 | } |
441 | |
442 | return selFiles; |
443 | } |
444 | |
445 | bool CommitChangesWidget::checkMsg(QString &msg) |
446 | { |
447 | const auto title = ui->leCommitTitle->text(); |
448 | |
449 | if (title.isEmpty()) |
450 | QMessageBox::warning(this, "Commit changes" , "Please, add a title." ); |
451 | |
452 | msg = title; |
453 | |
454 | if (!ui->teDescription->toPlainText().isEmpty()) |
455 | { |
456 | auto description = QString("\n\n%1" ).arg(ui->teDescription->toPlainText()); |
457 | description.remove(QRegExp("(^|\\n)\\s*#[^\\n]*" )); // strip comments |
458 | msg += description; |
459 | } |
460 | |
461 | msg.replace(QRegExp("[ \\t\\r\\f\\v]+\\n" ), "\n" ); // strip line trailing cruft |
462 | msg = msg.trimmed(); |
463 | |
464 | if (msg.isEmpty()) |
465 | { |
466 | QMessageBox::warning(this, "Commit changes" , "Please, add a title." ); |
467 | return false; |
468 | } |
469 | |
470 | msg = QString("%1\n%2\n" ) |
471 | .arg(msg.section('\n', 0, 0, QString::SectionIncludeTrailingSep), msg.section('\n', 1).trimmed()); |
472 | |
473 | return true; |
474 | } |
475 | |
476 | void CommitChangesWidget::updateCounter(const QString &text) |
477 | { |
478 | ui->lCounter->setText(QString::number(mTitleMaxLength - text.count())); |
479 | } |
480 | |
481 | bool CommitChangesWidget::hasConflicts() |
482 | { |
483 | for (const auto &iter : qAsConst(mInternalCache)) |
484 | if (iter.item->data(GitQlientRole::U_IsConflict).toBool()) |
485 | return true; |
486 | |
487 | return false; |
488 | } |
489 | |
490 | void CommitChangesWidget::clear() |
491 | { |
492 | ui->unstagedFilesList->clear(); |
493 | ui->stagedFilesList->clear(); |
494 | mInternalCache.clear(); |
495 | ui->leCommitTitle->clear(); |
496 | ui->teDescription->clear(); |
497 | ui->applyActionBtn->setEnabled(false); |
498 | } |
499 | |
500 | void CommitChangesWidget::clearStaged() |
501 | { |
502 | ui->stagedFilesList->clear(); |
503 | |
504 | const auto end = mInternalCache.end(); |
505 | for (auto it = mInternalCache.begin(); it != end;) |
506 | { |
507 | if (it.key().contains(QString("-%1" ).arg(ui->stagedFilesList->objectName()))) |
508 | it = mInternalCache.erase(it); |
509 | else |
510 | ++it; |
511 | } |
512 | |
513 | ui->applyActionBtn->setEnabled(false); |
514 | } |
515 | |
516 | void CommitChangesWidget::setCommitTitleMaxLength() |
517 | { |
518 | mTitleMaxLength = GitQlientSettings().globalValue("commitTitleMaxLength" , mTitleMaxLength).toInt(); |
519 | |
520 | ui->lCounter->setText(QString::number(mTitleMaxLength)); |
521 | ui->leCommitTitle->setMaxLength(mTitleMaxLength); |
522 | updateCounter(ui->leCommitTitle->text()); |
523 | } |
524 | |
525 | void CommitChangesWidget::(const QPoint &pos) |
526 | { |
527 | const auto item = ui->unstagedFilesList->itemAt(pos); |
528 | |
529 | if (item) |
530 | { |
531 | const auto fileName = item->toolTip(); |
532 | const auto = new UnstagedMenu(mGit, fileName, this); |
533 | connect(contextMenu, &UnstagedMenu::signalEditFile, this, &CommitChangesWidget::signalEditFile); |
534 | connect(contextMenu, &UnstagedMenu::signalShowDiff, this, &CommitChangesWidget::requestDiff); |
535 | connect(contextMenu, &UnstagedMenu::signalCommitAll, this, &CommitChangesWidget::addAllFilesToCommitList); |
536 | connect(contextMenu, &UnstagedMenu::signalRevertAll, this, &CommitChangesWidget::revertAllChanges); |
537 | connect(contextMenu, &UnstagedMenu::changeReverted, this, &CommitChangesWidget::changeReverted); |
538 | connect(contextMenu, &UnstagedMenu::signalCheckedOut, this, &CommitChangesWidget::signalCheckoutPerformed); |
539 | connect(contextMenu, &UnstagedMenu::signalShowFileHistory, this, &CommitChangesWidget::signalShowFileHistory); |
540 | connect(contextMenu, &UnstagedMenu::signalStageFile, this, [this, item] { addFileToCommitList(item); }); |
541 | connect(contextMenu, &UnstagedMenu::deleteUntracked, this, &CommitChangesWidget::deleteUntrackedFiles); |
542 | |
543 | const auto parentPos = ui->unstagedFilesList->mapToParent(pos); |
544 | contextMenu->popup(mapToGlobal(parentPos)); |
545 | } |
546 | } |
547 | |