src/eric7/Plugins/VcsPlugins/vcsGit/GitWorktreeDialog.py

branch
eric7
changeset 9620
9563c83ce83d
child 9621
2b7d5e6863c2
equal deleted inserted replaced
9619:7033f25b1462 9620:9563c83ce83d
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a dialog to offer the worktree management functionality.
8 """
9
10 import os
11
12 from PyQt6.QtCore import QDateTime, QProcess, QSize, Qt, QTime, QTimer, pyqtSlot
13 from PyQt6.QtWidgets import (
14 QAbstractButton,
15 QDialog,
16 QDialogButtonBox,
17 QHeaderView,
18 QInputDialog,
19 QLineEdit,
20 QMenu,
21 QTreeWidgetItem,
22 QWidget,
23 )
24
25 from eric7 import Preferences
26 from eric7.EricGui import EricPixmapCache
27 from eric7.EricWidgets import EricMessageBox, EricPathPickerDialog
28
29 from .GitDialog import GitDialog
30 from .Ui_GitWorktreeDialog import Ui_GitWorktreeDialog
31
32
33 class GitWorktreeDialog(QWidget, Ui_GitWorktreeDialog):
34 """
35 Class implementing a dialog to offer the worktree management functionality.
36 """
37
38 StatusRole = Qt.ItemDataRole.UserRole
39
40 def __init__(self, vcs, parent=None):
41 """
42 Constructor
43
44 @param vcs reference to the vcs object
45 @type Git
46 @param parent reference to the parent widget (defaults to None)
47 @type QWidget (optional)
48 """
49 super().__init__(parent)
50 self.setupUi(self)
51
52 self.__nameColumn = 0
53 self.__pathColumn = 1
54 self.__commitColumn = 2
55 self.__branchColumn = 3
56
57 self.worktreeList.header().setSortIndicator(0, Qt.SortOrder.AscendingOrder)
58
59 self.__refreshButton = self.buttonBox.addButton(
60 self.tr("Refresh"), QDialogButtonBox.ButtonRole.ActionRole
61 )
62 self.__refreshButton.setToolTip(self.tr("Press to refresh the status display"))
63 self.__refreshButton.setEnabled(False)
64 self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setEnabled(False)
65 self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setDefault(True)
66
67 self.__vcs = vcs
68 self.__process = QProcess()
69 self.__process.finished.connect(self.__procFinished)
70 self.__process.readyReadStandardOutput.connect(self.__readStdout)
71 self.__process.readyReadStandardError.connect(self.__readStderr)
72
73 self.__initActionsMenu()
74
75 def __initActionsMenu(self):
76 """
77 Private method to initialize the actions menu.
78 """
79 self.__actionsMenu = QMenu()
80 self.__actionsMenu.setTearOffEnabled(True)
81 self.__actionsMenu.setToolTipsVisible(True)
82 self.__actionsMenu.aboutToShow.connect(self.__showActionsMenu)
83
84 self.__addAct = self.__actionsMenu.addAction(
85 self.tr("Add..."), self.__worktreeAdd
86 )
87 self.__addAct.setToolTip(self.tr("Add a new linked worktree"))
88 self.__actionsMenu.addSeparator()
89 self.__lockAct = self.__actionsMenu.addAction(
90 self.tr("Lock..."), self.__worktreeLock
91 )
92 self.__lockAct.setToolTip(self.tr("Lock the selected worktree"))
93 self.__unlockAct = self.__actionsMenu.addAction(
94 self.tr("Unlock"), self.__worktreeUnlock
95 )
96 self.__unlockAct.setToolTip(self.tr("Unlock the selected worktree"))
97 self.__actionsMenu.addSeparator()
98 self.__moveAct = self.__actionsMenu.addAction(
99 self.tr("Move..."), self.__worktreeMove
100 )
101 self.__moveAct.setToolTip(
102 self.tr("Move the selected worktree to a new location")
103 )
104 self.__actionsMenu.addSeparator()
105 self.__removeAct = self.__actionsMenu.addAction(
106 self.tr("Remove"), self.__worktreeRemove
107 )
108 self.__removeAct.setToolTip(self.tr("Remove the selected worktree"))
109 self.__removeForcedAct = self.__actionsMenu.addAction(
110 self.tr("Forced Remove"), self.__worktreeRemoveForced
111 )
112 self.__removeForcedAct.setToolTip(
113 self.tr("Remove the selected worktree forcefully")
114 )
115 self.__actionsMenu.addSeparator()
116 self.__pruneAct = self.__actionsMenu.addAction(
117 self.tr("Prune..."), self.__worktreePrune
118 )
119 self.__pruneAct.setToolTip(self.tr("Prune worktree information"))
120 self.__actionsMenu.addSeparator()
121 self.__repairAct = self.__actionsMenu.addAction(
122 self.tr("Repair"), self.__worktreeRepair
123 )
124 self.__repairAct.setToolTip(self.tr("Repair worktree administrative files"))
125 self.__repairMultipleAct = self.__actionsMenu.addAction(
126 self.tr("Repair Multiple"), self.__worktreeRepairMultiple
127 )
128 self.__repairMultipleAct.setToolTip(
129 self.tr("Repair administrative files of multiple worktrees")
130 )
131
132 self.actionsButton.setIcon(EricPixmapCache.getIcon("actionsToolButton"))
133 self.actionsButton.setMenu(self.__actionsMenu)
134
135 def closeEvent(self, e):
136 """
137 Protected slot implementing a close event handler.
138
139 @param e close event (QCloseEvent)
140 """
141 if (
142 self.__process is not None
143 and self.__process.state() != QProcess.ProcessState.NotRunning
144 ):
145 self.__process.terminate()
146 QTimer.singleShot(2000, self.__process.kill)
147 self.__process.waitForFinished(3000)
148
149 self.__vcs.getPlugin().setPreferences(
150 "WorktreeDialogGeometry", self.saveGeometry()
151 )
152
153 e.accept()
154
155 def show(self):
156 """
157 Public slot to show the dialog.
158 """
159 super().show()
160
161 geom = self.__vcs.getPlugin().getPreferences("WorktreeDialogGeometry")
162 if geom.isEmpty():
163 s = QSize(900, 600)
164 self.resize(s)
165 else:
166 self.restoreGeometry(geom)
167
168 def __resort(self):
169 """
170 Private method to resort the tree.
171 """
172 self.worktreeList.sortItems(
173 self.worktreeList.sortColumn(),
174 self.worktreeList.header().sortIndicatorOrder(),
175 )
176
177 def __resizeColumns(self):
178 """
179 Private method to resize the list columns.
180 """
181 self.worktreeList.header().resizeSections(
182 QHeaderView.ResizeMode.ResizeToContents
183 )
184 self.worktreeList.header().setStretchLastSection(True)
185
186 def __generateItem(self, dataLines):
187 """
188 Private method to generate a worktree entry with the given data.
189
190 @param dataLines lines extracted from the git worktree list output
191 with porcelain format
192 @type list of str
193 """
194 checkoutPath = worktreeName = commit = branch = status = ""
195 iconName = tooltip = ""
196 for line in dataLines:
197 if " " in line:
198 option, value = line.split(None, 1)
199 else:
200 option, value = line, ""
201
202 if option == "worktree":
203 checkoutPath = value
204 worktreeName = os.path.basename(value)
205 elif option == "HEAD":
206 commit = value[: self.__commitIdLength]
207 elif option == "branch":
208 branch = value.rsplit("/", 1)[-1]
209 elif option == "bare":
210 branch = self.tr("(bare)")
211 elif option == "detached":
212 branch = self.tr("(detached HEAD)")
213 elif option == "prunable":
214 iconName = "trash"
215 tooltip = value
216 status = option
217 elif option == "locked":
218 iconName = "locked"
219 tooltip = value
220 status = option
221
222 itm = QTreeWidgetItem(
223 self.worktreeList, [worktreeName, checkoutPath, commit, branch]
224 )
225 if iconName:
226 itm.setIcon(0, EricPixmapCache.getIcon(iconName))
227 if tooltip:
228 itm.setToolTip(0, tooltip)
229
230 if self.worktreeList.topLevelItemCount() == 1:
231 # the first item is the main worktree
232 status = "main"
233 font = itm.font(0)
234 font.setBold(True)
235 if checkoutPath == self.__projectDir:
236 # it is the current project as well
237 status = "main+current"
238 font.setItalic(True)
239 for col in range(self.worktreeList.columnCount()):
240 itm.setFont(col, font)
241 elif checkoutPath == self.__projectDir:
242 # it is the current project
243 if not status:
244 status = "current"
245 elif status == "locked":
246 status = "locked+current"
247 font = itm.font(0)
248 font.setItalic(True)
249 for col in range(self.worktreeList.columnCount()):
250 itm.setFont(col, font)
251 itm.setData(0, GitWorktreeDialog.StatusRole, status)
252
253 def start(self, projectDir):
254 """
255 Public slot to start the git worktree list command.
256
257 @param projectDir name of the project directory
258 @type str
259 """
260 self.errorGroup.hide()
261 self.worktreeList.clear()
262
263 self.__ioEncoding = Preferences.getSystem("IOEncoding")
264
265 args = self.__vcs.initCommand("worktree")
266 args += ["list", "--porcelain"]
267 if self.expireCheckBox.isChecked():
268 args += [
269 "--expire",
270 self.expireDateTimeEdit.dateTime().toString(Qt.DateFormat.ISODate),
271 ]
272
273 self.__projectDir = projectDir
274
275 # find the root of the repo
276 self.__repodir = self.__vcs.findRepoRoot(projectDir)
277 if not self.__repodir:
278 return
279
280 self.__outputLines = []
281 self.__commitIdLength = self.__vcs.getPlugin().getPreferences("CommitIdLength")
282
283 self.__process.kill()
284 self.__process.setWorkingDirectory(self.__repodir)
285
286 self.__process.start("git", args)
287 procStarted = self.__process.waitForStarted(5000)
288 if not procStarted:
289 EricMessageBox.critical(
290 self,
291 self.tr("Process Generation Error"),
292 self.tr(
293 "The process {0} could not be started. "
294 "Ensure, that it is in the search path."
295 ).format("git"),
296 )
297 else:
298 self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setEnabled(
299 False
300 )
301 self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setEnabled(
302 True
303 )
304 self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setDefault(
305 True
306 )
307
308 self.__refreshButton.setEnabled(False)
309
310 def __finish(self):
311 """
312 Private slot called when the process finished or the user pressed
313 the button.
314 """
315 if (
316 self.__process is not None
317 and self.__process.state() != QProcess.ProcessState.NotRunning
318 ):
319 self.__process.terminate()
320 QTimer.singleShot(2000, self.__process.kill)
321 self.__process.waitForFinished(3000)
322
323 self.__refreshButton.setEnabled(True)
324
325 self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setEnabled(True)
326 self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setEnabled(False)
327 self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setDefault(True)
328 self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setFocus(
329 Qt.FocusReason.OtherFocusReason
330 )
331
332 self.__resort()
333 self.__resizeColumns()
334
335 @pyqtSlot(QAbstractButton)
336 def on_buttonBox_clicked(self, button):
337 """
338 Private slot called by a button of the button box clicked.
339
340 @param button button that was clicked
341 @type QAbstractButton
342 """
343 if button == self.buttonBox.button(QDialogButtonBox.StandardButton.Close):
344 self.close()
345 elif button == self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel):
346 self.__finish()
347 elif button == self.__refreshButton:
348 self.__refreshButtonClicked()
349
350 @pyqtSlot()
351 def __refreshButtonClicked(self):
352 """
353 Private slot to refresh the worktree display.
354 """
355 self.start(self.__projectDir)
356
357 def __procFinished(self, exitCode, exitStatus):
358 """
359 Private slot connected to the finished signal.
360
361 @param exitCode exit code of the process (integer)
362 @param exitStatus exit status of the process (QProcess.ExitStatus)
363 """
364 self.__finish()
365
366 def __readStdout(self):
367 """
368 Private slot to handle the readyReadStandardOutput signal.
369
370 It reads the output of the process, formats it and inserts it into
371 the contents pane.
372 """
373 if self.__process is not None:
374 self.__process.setReadChannel(QProcess.ProcessChannel.StandardOutput)
375
376 while self.__process.canReadLine():
377 line = str(
378 self.__process.readLine(), self.__ioEncoding, "replace"
379 ).strip()
380 if line:
381 self.__outputLines.append(line)
382 else:
383 self.__generateItem(self.__outputLines)
384 self.__outputLines = []
385
386 def __readStderr(self):
387 """
388 Private slot to handle the readyReadStandardError signal.
389
390 It reads the error output of the process and inserts it into the
391 error pane.
392 """
393 if self.__process is not None:
394 s = str(self.__process.readAllStandardError(), self.__ioEncoding, "replace")
395 self.errorGroup.show()
396 self.errors.insertPlainText(s)
397 self.errors.ensureCursorVisible()
398
399 @pyqtSlot(bool)
400 def on_expireCheckBox_toggled(self, checked):
401 """
402 Private slot to handle a change of the expire checkbox.
403
404 @param checked state of the checkbox
405 @type bool
406 """
407 if checked:
408 now = QDateTime.currentDateTime()
409 self.expireDateTimeEdit.setMaximumDateTime(now)
410 self.expireDateTimeEdit.setMinimumDate(now.date().addDays(-2 * 365))
411 self.expireDateTimeEdit.setMinimumTime(QTime(0, 0, 0))
412 self.expireDateTimeEdit.setDateTime(now)
413 else:
414 self.__refreshButtonClicked()
415
416 @pyqtSlot(QDateTime)
417 def on_expireDateTimeEdit_dateTimeChanged(self, dateTime):
418 """
419 Private slot to handle a change of the expire date and time.
420
421 @param dateTime DESCRIPTION
422 @type QDateTime
423 """
424 self.__refreshButtonClicked()
425
426 ###########################################################################
427 ## Menu handling methods
428 ###########################################################################
429
430 def __showActionsMenu(self):
431 """
432 Private slot to prepare the actions button menu before it is shown.
433 """
434 prunableWorktrees = []
435 for row in range(self.worktreeList.topLevelItemCount()):
436 itm = self.worktreeList.topLevelItem(row)
437 status = itm.data(0, GitWorktreeDialog.StatusRole)
438 if status == "prunable":
439 prunableWorktrees.append(itm.text(self.__pathColumn))
440
441 selectedItems = self.worktreeList.selectedItems()
442 enable = bool(selectedItems)
443 status = (
444 selectedItems[0].data(0, GitWorktreeDialog.StatusRole)
445 if selectedItems
446 else ""
447 )
448
449 self.__lockAct.setEnabled(
450 enable
451 and status not in ("locked", "locked+current", "main", "main+current")
452 )
453 self.__unlockAct.setEnabled(enable and status in ("locked", "locked+current"))
454 self.__moveAct.setEnabled(
455 enable
456 and status
457 not in (
458 "prunable",
459 "locked",
460 "main",
461 "main+current",
462 "current",
463 "locked+current",
464 )
465 )
466 self.__removeAct.setEnabled(
467 enable
468 and status
469 not in ("locked", "main", "main+current", "current", "locked+current")
470 )
471 self.__removeForcedAct.setEnabled(
472 enable
473 and status not in ("main", "main+current", "current", "locked+current")
474 )
475 self.__pruneAct.setEnabled(bool(prunableWorktrees))
476
477 @pyqtSlot()
478 def __worktreeAdd(self):
479 """
480 Private slot to add a linked worktree.
481 """
482 # TODO: not yet implemented
483 from .GitWorktreeAddDialog import GitWorktreeAddDialog
484
485 # find current worktree and take its parent path as the parent directory
486 for row in range(self.worktreeList.topLevelItemCount()):
487 itm = self.worktreeList.topLevelItem(row)
488 if "current" in itm.data(0, GitWorktreeDialog.StatusRole):
489 parentDirectory = os.path.dirname(itm.text(self.__pathColumn))
490 break
491 else:
492 parentDirectory = ""
493
494 dlg = GitWorktreeAddDialog(
495 parentDirectory,
496 self.__vcs.gitGetTagsList(self.__repodir),
497 self.__vcs.gitGetBranchesList(self.__repodir, withMaster=True),
498 )
499 if dlg.exec() == QDialog.DialogCode.Accepted:
500 params = dlg.getParameters()
501 args = ["worktree", "add"]
502 if params["force"]:
503 args.append("--force")
504 if params["detach"]:
505 args.append("--detach")
506 if params["lock"]:
507 args.append("--lock")
508 if params["lock_reason"]:
509 args += ["--reason", params["lock_reason"]]
510 if params["branch"]:
511 args += ["-B" if params["force_branch"] else "-b", params["branch"]]
512 args.append(params["path"])
513 if params["commit"]:
514 args.append(params["commit"])
515
516 dlg = GitDialog(self.tr("Add Worktree"), self.__vcs)
517 started = dlg.startProcess(args, workingDir=self.__repodir)
518 if started:
519 dlg.exec()
520
521 self.__refreshButtonClicked()
522
523 @pyqtSlot()
524 def __worktreeLock(self):
525 """
526 Private slot to lock a worktree.
527 """
528 worktree = self.worktreeList.selectedItems()[0].text(self.__pathColumn)
529 if not worktree:
530 return
531
532 reason, ok = QInputDialog.getText(
533 self,
534 self.tr("Lock Worktree"),
535 self.tr("Enter a reason for the lock:"),
536 QLineEdit.EchoMode.Normal,
537 )
538 if not ok:
539 return
540
541 args = ["worktree", "lock"]
542 if reason:
543 args += ["--reason", reason]
544 args.append(worktree)
545
546 proc = QProcess()
547 ok = self.__vcs.startSynchronizedProcess(proc, "git", args, self.__repodir)
548 if not ok:
549 err = str(proc.readAllStandardError(), self.__ioEncoding, "replace")
550 EricMessageBox.critical(
551 self,
552 self.tr("Lock Worktree"),
553 self.tr(
554 "<p>Locking the selected worktree failed.</p><p>{0}</p>"
555 ).format(err),
556 )
557
558 self.__refreshButtonClicked()
559
560 @pyqtSlot()
561 def __worktreeUnlock(self):
562 """
563 Private slot to unlock a worktree.
564 """
565 worktree = self.worktreeList.selectedItems()[0].text(self.__pathColumn)
566 if not worktree:
567 return
568
569 args = ["worktree", "unlock", worktree]
570
571 proc = QProcess()
572 ok = self.__vcs.startSynchronizedProcess(proc, "git", args, self.__repodir)
573 if not ok:
574 err = str(proc.readAllStandardError(), self.__ioEncoding, "replace")
575 EricMessageBox.critical(
576 self,
577 self.tr("Unlock Worktree"),
578 self.tr(
579 "<p>Unlocking the selected worktree failed.</p><p>{0}</p>"
580 ).format(err),
581 )
582
583 self.__refreshButtonClicked()
584
585 @pyqtSlot()
586 def __worktreeMove(self):
587 """
588 Private slot to move a worktree to a new location.
589 """
590 worktree = self.worktreeList.selectedItems()[0].text(self.__pathColumn)
591 if not worktree:
592 return
593
594 newPath, ok = EricPathPickerDialog.getStrPath(
595 self,
596 self.tr("Move Worktree"),
597 self.tr("Enter the new path for the worktree:"),
598 mode=EricPathPickerDialog.EricPathPickerModes.DIRECTORY_MODE,
599 strPath=worktree,
600 defaultDirectory=os.path.dirname(worktree),
601 )
602 if not ok:
603 return
604
605 args = ["worktree", "move", worktree, newPath]
606
607 proc = QProcess()
608 ok = self.__vcs.startSynchronizedProcess(proc, "git", args, self.__repodir)
609 if not ok:
610 err = str(proc.readAllStandardError(), self.__ioEncoding, "replace")
611 EricMessageBox.critical(
612 self,
613 self.tr("Move Worktree"),
614 self.tr("<p>Moving the selected worktree failed.</p><p>{0}</p>").format(
615 err
616 ),
617 )
618
619 self.__refreshButtonClicked()
620
621 @pyqtSlot()
622 def __worktreeRemove(self, force=False):
623 """
624 Private slot to remove a linked worktree.
625
626 @param force flag indicating a forceful remove (defaults to False)
627 @type bool (optional
628 """
629 worktree = self.worktreeList.selectedItems()[0].text(self.__pathColumn)
630 if not worktree:
631 return
632
633 title = (
634 self.tr("Remove Worktree")
635 if force
636 else self.tr("Remove Worktree Forcefully")
637 )
638
639 ok = EricMessageBox.yesNo(
640 self,
641 title,
642 self.tr(
643 "<p>Do you really want to remove the worktree <b>{0}</b>?</p>"
644 ).format(worktree),
645 )
646 if not ok:
647 return
648
649 args = ["worktree", "remove"]
650 if force:
651 args.append("--force")
652 if (
653 self.worktreeList.selectedItems()[0].data(
654 0, GitWorktreeDialog.StatusRole
655 )
656 == "locked"
657 ):
658 # a second '--force' is needed
659 args.append("--force")
660 args.append(worktree)
661
662 proc = QProcess()
663 ok = self.__vcs.startSynchronizedProcess(proc, "git", args, self.__repodir)
664 if not ok:
665 err = str(proc.readAllStandardError(), self.__ioEncoding, "replace")
666 EricMessageBox.critical(
667 self,
668 title,
669 self.tr(
670 "<p>Removing the selected worktree failed.</p><p>{0}</p>"
671 ).format(err),
672 )
673
674 self.__refreshButtonClicked()
675
676 @pyqtSlot()
677 def __worktreeRemoveForced(self):
678 """
679 Private slot to remove a linked worktree forcefully.
680 """
681 self.__worktreeRemove(force=True)
682
683 @pyqtSlot()
684 def __worktreePrune(self):
685 """
686 Private slot to prune worktree information.
687 """
688 from eric7.UI.DeleteFilesConfirmationDialog import DeleteFilesConfirmationDialog
689
690 prunableWorktrees = []
691 for row in range(self.worktreeList.topLevelItemCount()):
692 itm = self.worktreeList.topLevelItem(row)
693 status = itm.data(0, GitWorktreeDialog.StatusRole)
694 if status == "prunable":
695 prunableWorktrees.append(itm.text(self.__pathColumn))
696
697 if prunableWorktrees:
698 dlg = DeleteFilesConfirmationDialog(
699 self,
700 self.tr("Prune Worktree Information"),
701 self.tr(
702 "Do you really want to prune the information of these worktrees?"
703 ),
704 prunableWorktrees,
705 )
706 if dlg.exec() == QDialog.DialogCode.Accepted:
707 args = ["worktree", "prune"]
708 if self.expireCheckBox.isChecked():
709 args += [
710 "--expire",
711 self.expireDateTimeEdit.dateTime().toString(
712 Qt.DateFormat.ISODate
713 ),
714 ]
715
716 proc = QProcess()
717 ok = self.__vcs.startSynchronizedProcess(
718 proc, "git", args, self.__repodir
719 )
720 if not ok:
721 err = str(proc.readAllStandardError(), self.__ioEncoding, "replace")
722 EricMessageBox.critical(
723 self,
724 self.tr("Prune Worktree Information"),
725 self.tr(
726 "<p>Pruning of the worktree information failed.</p>"
727 "<p>{0}</p>"
728 ).format(err),
729 )
730
731 self.__refreshButtonClicked()
732
733 @pyqtSlot()
734 def __worktreeRepair(self, worktreePaths=None):
735 """
736 Private slot to repair worktree administrative files.
737
738 @param worktreePaths list of worktree paths to be repaired (defaults to None)
739 @type list of str (optional)
740 """
741 args = ["worktree", "repair"]
742 if worktreePaths:
743 args += worktreePaths
744
745 proc = QProcess()
746 ok = self.__vcs.startSynchronizedProcess(proc, "git", args, self.__repodir)
747 if ok:
748 out = str(proc.readAllStandardError(), self.__ioEncoding, "replace")
749 EricMessageBox.information(
750 self,
751 self.tr("Repair Worktree"),
752 self.tr(
753 "<p>Repairing of the worktree administrative files succeeded.</p>"
754 "<p>{0}</p>"
755 ).format(out),
756 )
757
758 else:
759 err = str(proc.readAllStandardError(), self.__ioEncoding, "replace")
760 EricMessageBox.critical(
761 self,
762 self.tr("Repair Worktree"),
763 self.tr(
764 "<p>Repairing of the worktree administrative files failed.</p>"
765 "<p>{0}</p>"
766 ).format(err),
767 )
768
769 self.__refreshButtonClicked()
770
771 @pyqtSlot()
772 def __worktreeRepairMultiple(self):
773 """
774 Private slot to repair worktree administrative files for multiple worktree
775 paths.
776 """
777 from .GitWorktreePathsDialog import GitWorktreePathsDialog
778
779 # find current worktree and take its parent path as the parent directory
780 for row in range(self.worktreeList.topLevelItemCount()):
781 itm = self.worktreeList.topLevelItem(row)
782 if "current" in itm.data(0, GitWorktreeDialog.StatusRole):
783 parentDirectory = os.path.dirname(itm.text(self.__pathColumn))
784 break
785 else:
786 parentDirectory = ""
787
788 dlg = GitWorktreePathsDialog(parentDirectory, self)
789 if dlg.exec() == QDialog.DialogCode.Accepted:
790 paths = dlg.getPathsList()
791
792 if paths:
793 self.__worktreeRepair(worktreePaths=paths)

eric ide

mercurial