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

branch
eric7
changeset 9620
9563c83ce83d
child 9621
2b7d5e6863c2
diff -r 7033f25b1462 -r 9563c83ce83d src/eric7/Plugins/VcsPlugins/vcsGit/GitWorktreeDialog.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/Plugins/VcsPlugins/vcsGit/GitWorktreeDialog.py	Thu Dec 15 17:15:09 2022 +0100
@@ -0,0 +1,793 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a dialog to offer the worktree management functionality.
+"""
+
+import os
+
+from PyQt6.QtCore import QDateTime, QProcess, QSize, Qt, QTime, QTimer, pyqtSlot
+from PyQt6.QtWidgets import (
+    QAbstractButton,
+    QDialog,
+    QDialogButtonBox,
+    QHeaderView,
+    QInputDialog,
+    QLineEdit,
+    QMenu,
+    QTreeWidgetItem,
+    QWidget,
+)
+
+from eric7 import Preferences
+from eric7.EricGui import EricPixmapCache
+from eric7.EricWidgets import EricMessageBox, EricPathPickerDialog
+
+from .GitDialog import GitDialog
+from .Ui_GitWorktreeDialog import Ui_GitWorktreeDialog
+
+
+class GitWorktreeDialog(QWidget, Ui_GitWorktreeDialog):
+    """
+    Class implementing a dialog to offer the worktree management functionality.
+    """
+
+    StatusRole = Qt.ItemDataRole.UserRole
+
+    def __init__(self, vcs, parent=None):
+        """
+        Constructor
+
+        @param vcs reference to the vcs object
+        @type Git
+        @param parent reference to the parent widget (defaults to None)
+        @type QWidget (optional)
+        """
+        super().__init__(parent)
+        self.setupUi(self)
+
+        self.__nameColumn = 0
+        self.__pathColumn = 1
+        self.__commitColumn = 2
+        self.__branchColumn = 3
+
+        self.worktreeList.header().setSortIndicator(0, Qt.SortOrder.AscendingOrder)
+
+        self.__refreshButton = self.buttonBox.addButton(
+            self.tr("Refresh"), QDialogButtonBox.ButtonRole.ActionRole
+        )
+        self.__refreshButton.setToolTip(self.tr("Press to refresh the status display"))
+        self.__refreshButton.setEnabled(False)
+        self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setEnabled(False)
+        self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setDefault(True)
+
+        self.__vcs = vcs
+        self.__process = QProcess()
+        self.__process.finished.connect(self.__procFinished)
+        self.__process.readyReadStandardOutput.connect(self.__readStdout)
+        self.__process.readyReadStandardError.connect(self.__readStderr)
+
+        self.__initActionsMenu()
+
+    def __initActionsMenu(self):
+        """
+        Private method to initialize the actions menu.
+        """
+        self.__actionsMenu = QMenu()
+        self.__actionsMenu.setTearOffEnabled(True)
+        self.__actionsMenu.setToolTipsVisible(True)
+        self.__actionsMenu.aboutToShow.connect(self.__showActionsMenu)
+
+        self.__addAct = self.__actionsMenu.addAction(
+            self.tr("Add..."), self.__worktreeAdd
+        )
+        self.__addAct.setToolTip(self.tr("Add a new linked worktree"))
+        self.__actionsMenu.addSeparator()
+        self.__lockAct = self.__actionsMenu.addAction(
+            self.tr("Lock..."), self.__worktreeLock
+        )
+        self.__lockAct.setToolTip(self.tr("Lock the selected worktree"))
+        self.__unlockAct = self.__actionsMenu.addAction(
+            self.tr("Unlock"), self.__worktreeUnlock
+        )
+        self.__unlockAct.setToolTip(self.tr("Unlock the selected worktree"))
+        self.__actionsMenu.addSeparator()
+        self.__moveAct = self.__actionsMenu.addAction(
+            self.tr("Move..."), self.__worktreeMove
+        )
+        self.__moveAct.setToolTip(
+            self.tr("Move the selected worktree to a new location")
+        )
+        self.__actionsMenu.addSeparator()
+        self.__removeAct = self.__actionsMenu.addAction(
+            self.tr("Remove"), self.__worktreeRemove
+        )
+        self.__removeAct.setToolTip(self.tr("Remove the selected worktree"))
+        self.__removeForcedAct = self.__actionsMenu.addAction(
+            self.tr("Forced Remove"), self.__worktreeRemoveForced
+        )
+        self.__removeForcedAct.setToolTip(
+            self.tr("Remove the selected worktree forcefully")
+        )
+        self.__actionsMenu.addSeparator()
+        self.__pruneAct = self.__actionsMenu.addAction(
+            self.tr("Prune..."), self.__worktreePrune
+        )
+        self.__pruneAct.setToolTip(self.tr("Prune worktree information"))
+        self.__actionsMenu.addSeparator()
+        self.__repairAct = self.__actionsMenu.addAction(
+            self.tr("Repair"), self.__worktreeRepair
+        )
+        self.__repairAct.setToolTip(self.tr("Repair worktree administrative files"))
+        self.__repairMultipleAct = self.__actionsMenu.addAction(
+            self.tr("Repair Multiple"), self.__worktreeRepairMultiple
+        )
+        self.__repairMultipleAct.setToolTip(
+            self.tr("Repair administrative files of multiple worktrees")
+        )
+
+        self.actionsButton.setIcon(EricPixmapCache.getIcon("actionsToolButton"))
+        self.actionsButton.setMenu(self.__actionsMenu)
+
+    def closeEvent(self, e):
+        """
+        Protected slot implementing a close event handler.
+
+        @param e close event (QCloseEvent)
+        """
+        if (
+            self.__process is not None
+            and self.__process.state() != QProcess.ProcessState.NotRunning
+        ):
+            self.__process.terminate()
+            QTimer.singleShot(2000, self.__process.kill)
+            self.__process.waitForFinished(3000)
+
+        self.__vcs.getPlugin().setPreferences(
+            "WorktreeDialogGeometry", self.saveGeometry()
+        )
+
+        e.accept()
+
+    def show(self):
+        """
+        Public slot to show the dialog.
+        """
+        super().show()
+
+        geom = self.__vcs.getPlugin().getPreferences("WorktreeDialogGeometry")
+        if geom.isEmpty():
+            s = QSize(900, 600)
+            self.resize(s)
+        else:
+            self.restoreGeometry(geom)
+
+    def __resort(self):
+        """
+        Private method to resort the tree.
+        """
+        self.worktreeList.sortItems(
+            self.worktreeList.sortColumn(),
+            self.worktreeList.header().sortIndicatorOrder(),
+        )
+
+    def __resizeColumns(self):
+        """
+        Private method to resize the list columns.
+        """
+        self.worktreeList.header().resizeSections(
+            QHeaderView.ResizeMode.ResizeToContents
+        )
+        self.worktreeList.header().setStretchLastSection(True)
+
+    def __generateItem(self, dataLines):
+        """
+        Private method to generate a worktree entry with the given data.
+
+        @param dataLines lines extracted from the git worktree list output
+            with porcelain format
+        @type list of str
+        """
+        checkoutPath = worktreeName = commit = branch = status = ""
+        iconName = tooltip = ""
+        for line in dataLines:
+            if " " in line:
+                option, value = line.split(None, 1)
+            else:
+                option, value = line, ""
+
+            if option == "worktree":
+                checkoutPath = value
+                worktreeName = os.path.basename(value)
+            elif option == "HEAD":
+                commit = value[: self.__commitIdLength]
+            elif option == "branch":
+                branch = value.rsplit("/", 1)[-1]
+            elif option == "bare":
+                branch = self.tr("(bare)")
+            elif option == "detached":
+                branch = self.tr("(detached HEAD)")
+            elif option == "prunable":
+                iconName = "trash"
+                tooltip = value
+                status = option
+            elif option == "locked":
+                iconName = "locked"
+                tooltip = value
+                status = option
+
+        itm = QTreeWidgetItem(
+            self.worktreeList, [worktreeName, checkoutPath, commit, branch]
+        )
+        if iconName:
+            itm.setIcon(0, EricPixmapCache.getIcon(iconName))
+            if tooltip:
+                itm.setToolTip(0, tooltip)
+
+        if self.worktreeList.topLevelItemCount() == 1:
+            # the first item is the main worktree
+            status = "main"
+            font = itm.font(0)
+            font.setBold(True)
+            if checkoutPath == self.__projectDir:
+                # it is the current project as well
+                status = "main+current"
+                font.setItalic(True)
+            for col in range(self.worktreeList.columnCount()):
+                itm.setFont(col, font)
+        elif checkoutPath == self.__projectDir:
+            # it is the current project
+            if not status:
+                status = "current"
+            elif status == "locked":
+                status = "locked+current"
+            font = itm.font(0)
+            font.setItalic(True)
+            for col in range(self.worktreeList.columnCount()):
+                itm.setFont(col, font)
+        itm.setData(0, GitWorktreeDialog.StatusRole, status)
+
+    def start(self, projectDir):
+        """
+        Public slot to start the git worktree list command.
+
+        @param projectDir name of the project directory
+        @type str
+        """
+        self.errorGroup.hide()
+        self.worktreeList.clear()
+
+        self.__ioEncoding = Preferences.getSystem("IOEncoding")
+
+        args = self.__vcs.initCommand("worktree")
+        args += ["list", "--porcelain"]
+        if self.expireCheckBox.isChecked():
+            args += [
+                "--expire",
+                self.expireDateTimeEdit.dateTime().toString(Qt.DateFormat.ISODate),
+            ]
+
+        self.__projectDir = projectDir
+
+        # find the root of the repo
+        self.__repodir = self.__vcs.findRepoRoot(projectDir)
+        if not self.__repodir:
+            return
+
+        self.__outputLines = []
+        self.__commitIdLength = self.__vcs.getPlugin().getPreferences("CommitIdLength")
+
+        self.__process.kill()
+        self.__process.setWorkingDirectory(self.__repodir)
+
+        self.__process.start("git", args)
+        procStarted = self.__process.waitForStarted(5000)
+        if not procStarted:
+            EricMessageBox.critical(
+                self,
+                self.tr("Process Generation Error"),
+                self.tr(
+                    "The process {0} could not be started. "
+                    "Ensure, that it is in the search path."
+                ).format("git"),
+            )
+        else:
+            self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setEnabled(
+                False
+            )
+            self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setEnabled(
+                True
+            )
+            self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setDefault(
+                True
+            )
+
+            self.__refreshButton.setEnabled(False)
+
+    def __finish(self):
+        """
+        Private slot called when the process finished or the user pressed
+        the button.
+        """
+        if (
+            self.__process is not None
+            and self.__process.state() != QProcess.ProcessState.NotRunning
+        ):
+            self.__process.terminate()
+            QTimer.singleShot(2000, self.__process.kill)
+            self.__process.waitForFinished(3000)
+
+        self.__refreshButton.setEnabled(True)
+
+        self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setEnabled(True)
+        self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setEnabled(False)
+        self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setDefault(True)
+        self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setFocus(
+            Qt.FocusReason.OtherFocusReason
+        )
+
+        self.__resort()
+        self.__resizeColumns()
+
+    @pyqtSlot(QAbstractButton)
+    def on_buttonBox_clicked(self, button):
+        """
+        Private slot called by a button of the button box clicked.
+
+        @param button button that was clicked
+        @type QAbstractButton
+        """
+        if button == self.buttonBox.button(QDialogButtonBox.StandardButton.Close):
+            self.close()
+        elif button == self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel):
+            self.__finish()
+        elif button == self.__refreshButton:
+            self.__refreshButtonClicked()
+
+    @pyqtSlot()
+    def __refreshButtonClicked(self):
+        """
+        Private slot to refresh the worktree display.
+        """
+        self.start(self.__projectDir)
+
+    def __procFinished(self, exitCode, exitStatus):
+        """
+        Private slot connected to the finished signal.
+
+        @param exitCode exit code of the process (integer)
+        @param exitStatus exit status of the process (QProcess.ExitStatus)
+        """
+        self.__finish()
+
+    def __readStdout(self):
+        """
+        Private slot to handle the readyReadStandardOutput signal.
+
+        It reads the output of the process, formats it and inserts it into
+        the contents pane.
+        """
+        if self.__process is not None:
+            self.__process.setReadChannel(QProcess.ProcessChannel.StandardOutput)
+
+            while self.__process.canReadLine():
+                line = str(
+                    self.__process.readLine(), self.__ioEncoding, "replace"
+                ).strip()
+                if line:
+                    self.__outputLines.append(line)
+                else:
+                    self.__generateItem(self.__outputLines)
+                    self.__outputLines = []
+
+    def __readStderr(self):
+        """
+        Private slot to handle the readyReadStandardError signal.
+
+        It reads the error output of the process and inserts it into the
+        error pane.
+        """
+        if self.__process is not None:
+            s = str(self.__process.readAllStandardError(), self.__ioEncoding, "replace")
+            self.errorGroup.show()
+            self.errors.insertPlainText(s)
+            self.errors.ensureCursorVisible()
+
+    @pyqtSlot(bool)
+    def on_expireCheckBox_toggled(self, checked):
+        """
+        Private slot to handle a change of the expire checkbox.
+
+        @param checked state of the checkbox
+        @type bool
+        """
+        if checked:
+            now = QDateTime.currentDateTime()
+            self.expireDateTimeEdit.setMaximumDateTime(now)
+            self.expireDateTimeEdit.setMinimumDate(now.date().addDays(-2 * 365))
+            self.expireDateTimeEdit.setMinimumTime(QTime(0, 0, 0))
+            self.expireDateTimeEdit.setDateTime(now)
+        else:
+            self.__refreshButtonClicked()
+
+    @pyqtSlot(QDateTime)
+    def on_expireDateTimeEdit_dateTimeChanged(self, dateTime):
+        """
+        Private slot to handle a change of the expire date and time.
+
+        @param dateTime DESCRIPTION
+        @type QDateTime
+        """
+        self.__refreshButtonClicked()
+
+    ###########################################################################
+    ## Menu handling methods
+    ###########################################################################
+
+    def __showActionsMenu(self):
+        """
+        Private slot to prepare the actions button menu before it is shown.
+        """
+        prunableWorktrees = []
+        for row in range(self.worktreeList.topLevelItemCount()):
+            itm = self.worktreeList.topLevelItem(row)
+            status = itm.data(0, GitWorktreeDialog.StatusRole)
+            if status == "prunable":
+                prunableWorktrees.append(itm.text(self.__pathColumn))
+
+        selectedItems = self.worktreeList.selectedItems()
+        enable = bool(selectedItems)
+        status = (
+            selectedItems[0].data(0, GitWorktreeDialog.StatusRole)
+            if selectedItems
+            else ""
+        )
+
+        self.__lockAct.setEnabled(
+            enable
+            and status not in ("locked", "locked+current", "main", "main+current")
+        )
+        self.__unlockAct.setEnabled(enable and status in ("locked", "locked+current"))
+        self.__moveAct.setEnabled(
+            enable
+            and status
+            not in (
+                "prunable",
+                "locked",
+                "main",
+                "main+current",
+                "current",
+                "locked+current",
+            )
+        )
+        self.__removeAct.setEnabled(
+            enable
+            and status
+            not in ("locked", "main", "main+current", "current", "locked+current")
+        )
+        self.__removeForcedAct.setEnabled(
+            enable
+            and status not in ("main", "main+current", "current", "locked+current")
+        )
+        self.__pruneAct.setEnabled(bool(prunableWorktrees))
+
+    @pyqtSlot()
+    def __worktreeAdd(self):
+        """
+        Private slot to add a linked worktree.
+        """
+        # TODO: not yet implemented
+        from .GitWorktreeAddDialog import GitWorktreeAddDialog
+
+        # find current worktree and take its parent path as the parent directory
+        for row in range(self.worktreeList.topLevelItemCount()):
+            itm = self.worktreeList.topLevelItem(row)
+            if "current" in itm.data(0, GitWorktreeDialog.StatusRole):
+                parentDirectory = os.path.dirname(itm.text(self.__pathColumn))
+                break
+        else:
+            parentDirectory = ""
+
+        dlg = GitWorktreeAddDialog(
+            parentDirectory,
+            self.__vcs.gitGetTagsList(self.__repodir),
+            self.__vcs.gitGetBranchesList(self.__repodir, withMaster=True),
+        )
+        if dlg.exec() == QDialog.DialogCode.Accepted:
+            params = dlg.getParameters()
+            args = ["worktree", "add"]
+            if params["force"]:
+                args.append("--force")
+            if params["detach"]:
+                args.append("--detach")
+            if params["lock"]:
+                args.append("--lock")
+                if params["lock_reason"]:
+                    args += ["--reason", params["lock_reason"]]
+            if params["branch"]:
+                args += ["-B" if params["force_branch"] else "-b", params["branch"]]
+            args.append(params["path"])
+            if params["commit"]:
+                args.append(params["commit"])
+
+        dlg = GitDialog(self.tr("Add Worktree"), self.__vcs)
+        started = dlg.startProcess(args, workingDir=self.__repodir)
+        if started:
+            dlg.exec()
+
+            self.__refreshButtonClicked()
+
+    @pyqtSlot()
+    def __worktreeLock(self):
+        """
+        Private slot to lock a worktree.
+        """
+        worktree = self.worktreeList.selectedItems()[0].text(self.__pathColumn)
+        if not worktree:
+            return
+
+        reason, ok = QInputDialog.getText(
+            self,
+            self.tr("Lock Worktree"),
+            self.tr("Enter a reason for the lock:"),
+            QLineEdit.EchoMode.Normal,
+        )
+        if not ok:
+            return
+
+        args = ["worktree", "lock"]
+        if reason:
+            args += ["--reason", reason]
+        args.append(worktree)
+
+        proc = QProcess()
+        ok = self.__vcs.startSynchronizedProcess(proc, "git", args, self.__repodir)
+        if not ok:
+            err = str(proc.readAllStandardError(), self.__ioEncoding, "replace")
+            EricMessageBox.critical(
+                self,
+                self.tr("Lock Worktree"),
+                self.tr(
+                    "<p>Locking the selected worktree failed.</p><p>{0}</p>"
+                ).format(err),
+            )
+
+        self.__refreshButtonClicked()
+
+    @pyqtSlot()
+    def __worktreeUnlock(self):
+        """
+        Private slot to unlock a worktree.
+        """
+        worktree = self.worktreeList.selectedItems()[0].text(self.__pathColumn)
+        if not worktree:
+            return
+
+        args = ["worktree", "unlock", worktree]
+
+        proc = QProcess()
+        ok = self.__vcs.startSynchronizedProcess(proc, "git", args, self.__repodir)
+        if not ok:
+            err = str(proc.readAllStandardError(), self.__ioEncoding, "replace")
+            EricMessageBox.critical(
+                self,
+                self.tr("Unlock Worktree"),
+                self.tr(
+                    "<p>Unlocking the selected worktree failed.</p><p>{0}</p>"
+                ).format(err),
+            )
+
+        self.__refreshButtonClicked()
+
+    @pyqtSlot()
+    def __worktreeMove(self):
+        """
+        Private slot to move a worktree to a new location.
+        """
+        worktree = self.worktreeList.selectedItems()[0].text(self.__pathColumn)
+        if not worktree:
+            return
+
+        newPath, ok = EricPathPickerDialog.getStrPath(
+            self,
+            self.tr("Move Worktree"),
+            self.tr("Enter the new path for the worktree:"),
+            mode=EricPathPickerDialog.EricPathPickerModes.DIRECTORY_MODE,
+            strPath=worktree,
+            defaultDirectory=os.path.dirname(worktree),
+        )
+        if not ok:
+            return
+
+        args = ["worktree", "move", worktree, newPath]
+
+        proc = QProcess()
+        ok = self.__vcs.startSynchronizedProcess(proc, "git", args, self.__repodir)
+        if not ok:
+            err = str(proc.readAllStandardError(), self.__ioEncoding, "replace")
+            EricMessageBox.critical(
+                self,
+                self.tr("Move Worktree"),
+                self.tr("<p>Moving the selected worktree failed.</p><p>{0}</p>").format(
+                    err
+                ),
+            )
+
+        self.__refreshButtonClicked()
+
+    @pyqtSlot()
+    def __worktreeRemove(self, force=False):
+        """
+        Private slot to remove a linked worktree.
+
+        @param force flag indicating a forceful remove (defaults to False)
+        @type bool (optional
+        """
+        worktree = self.worktreeList.selectedItems()[0].text(self.__pathColumn)
+        if not worktree:
+            return
+
+        title = (
+            self.tr("Remove Worktree")
+            if force
+            else self.tr("Remove Worktree Forcefully")
+        )
+
+        ok = EricMessageBox.yesNo(
+            self,
+            title,
+            self.tr(
+                "<p>Do you really want to remove the worktree <b>{0}</b>?</p>"
+            ).format(worktree),
+        )
+        if not ok:
+            return
+
+        args = ["worktree", "remove"]
+        if force:
+            args.append("--force")
+            if (
+                self.worktreeList.selectedItems()[0].data(
+                    0, GitWorktreeDialog.StatusRole
+                )
+                == "locked"
+            ):
+                # a second '--force' is needed
+                args.append("--force")
+        args.append(worktree)
+
+        proc = QProcess()
+        ok = self.__vcs.startSynchronizedProcess(proc, "git", args, self.__repodir)
+        if not ok:
+            err = str(proc.readAllStandardError(), self.__ioEncoding, "replace")
+            EricMessageBox.critical(
+                self,
+                title,
+                self.tr(
+                    "<p>Removing the selected worktree failed.</p><p>{0}</p>"
+                ).format(err),
+            )
+
+        self.__refreshButtonClicked()
+
+    @pyqtSlot()
+    def __worktreeRemoveForced(self):
+        """
+        Private slot to remove a linked worktree forcefully.
+        """
+        self.__worktreeRemove(force=True)
+
+    @pyqtSlot()
+    def __worktreePrune(self):
+        """
+        Private slot to prune worktree information.
+        """
+        from eric7.UI.DeleteFilesConfirmationDialog import DeleteFilesConfirmationDialog
+
+        prunableWorktrees = []
+        for row in range(self.worktreeList.topLevelItemCount()):
+            itm = self.worktreeList.topLevelItem(row)
+            status = itm.data(0, GitWorktreeDialog.StatusRole)
+            if status == "prunable":
+                prunableWorktrees.append(itm.text(self.__pathColumn))
+
+        if prunableWorktrees:
+            dlg = DeleteFilesConfirmationDialog(
+                self,
+                self.tr("Prune Worktree Information"),
+                self.tr(
+                    "Do you really want to prune the information of these worktrees?"
+                ),
+                prunableWorktrees,
+            )
+            if dlg.exec() == QDialog.DialogCode.Accepted:
+                args = ["worktree", "prune"]
+                if self.expireCheckBox.isChecked():
+                    args += [
+                        "--expire",
+                        self.expireDateTimeEdit.dateTime().toString(
+                            Qt.DateFormat.ISODate
+                        ),
+                    ]
+
+                proc = QProcess()
+                ok = self.__vcs.startSynchronizedProcess(
+                    proc, "git", args, self.__repodir
+                )
+                if not ok:
+                    err = str(proc.readAllStandardError(), self.__ioEncoding, "replace")
+                    EricMessageBox.critical(
+                        self,
+                        self.tr("Prune Worktree Information"),
+                        self.tr(
+                            "<p>Pruning of the worktree information failed.</p>"
+                            "<p>{0}</p>"
+                        ).format(err),
+                    )
+
+                self.__refreshButtonClicked()
+
+    @pyqtSlot()
+    def __worktreeRepair(self, worktreePaths=None):
+        """
+        Private slot to repair worktree administrative files.
+
+        @param worktreePaths list of worktree paths to be repaired (defaults to None)
+        @type list of str (optional)
+        """
+        args = ["worktree", "repair"]
+        if worktreePaths:
+            args += worktreePaths
+
+        proc = QProcess()
+        ok = self.__vcs.startSynchronizedProcess(proc, "git", args, self.__repodir)
+        if ok:
+            out = str(proc.readAllStandardError(), self.__ioEncoding, "replace")
+            EricMessageBox.information(
+                self,
+                self.tr("Repair Worktree"),
+                self.tr(
+                    "<p>Repairing of the worktree administrative files succeeded.</p>"
+                    "<p>{0}</p>"
+                ).format(out),
+            )
+
+        else:
+            err = str(proc.readAllStandardError(), self.__ioEncoding, "replace")
+            EricMessageBox.critical(
+                self,
+                self.tr("Repair Worktree"),
+                self.tr(
+                    "<p>Repairing of the worktree administrative files failed.</p>"
+                    "<p>{0}</p>"
+                ).format(err),
+            )
+
+        self.__refreshButtonClicked()
+
+    @pyqtSlot()
+    def __worktreeRepairMultiple(self):
+        """
+        Private slot to repair worktree administrative files for multiple worktree
+        paths.
+        """
+        from .GitWorktreePathsDialog import GitWorktreePathsDialog
+
+        # find current worktree and take its parent path as the parent directory
+        for row in range(self.worktreeList.topLevelItemCount()):
+            itm = self.worktreeList.topLevelItem(row)
+            if "current" in itm.data(0, GitWorktreeDialog.StatusRole):
+                parentDirectory = os.path.dirname(itm.text(self.__pathColumn))
+                break
+        else:
+            parentDirectory = ""
+
+        dlg = GitWorktreePathsDialog(parentDirectory, self)
+        if dlg.exec() == QDialog.DialogCode.Accepted:
+            paths = dlg.getPathsList()
+
+            if paths:
+                self.__worktreeRepair(worktreePaths=paths)

eric ide

mercurial