eric7/Plugins/VcsPlugins/vcsMercurial/HgLogBrowserDialog.py

Sun, 03 Jul 2022 13:52:59 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sun, 03 Jul 2022 13:52:59 +0200
branch
eric7
changeset 9202
81388c6065e8
parent 9153
506e35e424d5
permissions
-rw-r--r--

Refined the setup wizard a little bit.

# -*- coding: utf-8 -*-

# Copyright (c) 2010 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing a dialog to browse the log history.
"""

import os
import re
import collections
import contextlib
import pathlib

from PyQt6.QtCore import pyqtSlot, Qt, QDate, QSize, QPoint
from PyQt6.QtGui import (
    QColor, QPixmap, QPainter, QPen, QBrush, QIcon, QTextCursor, QPalette
)
from PyQt6.QtWidgets import (
    QWidget, QDialogButtonBox, QHeaderView, QTreeWidgetItem, QApplication,
    QLineEdit, QMenu, QInputDialog
)

from EricWidgets.EricApplication import ericApp
from EricWidgets import EricMessageBox, EricFileDialog
from EricGui.EricOverrideCursor import EricOverrideCursor

from .Ui_HgLogBrowserDialog import Ui_HgLogBrowserDialog

from .HgDiffHighlighter import HgDiffHighlighter
from .HgDiffGenerator import HgDiffGenerator

import UI.PixmapCache
import Preferences
import Utilities

COLORNAMES = ["blue", "darkgreen", "red", "green", "darkblue", "purple",
              "cyan", "olive", "magenta", "darkred", "darkmagenta",
              "darkcyan", "gray", "yellow"]
COLORS = [str(QColor(x).name()) for x in COLORNAMES]

LIGHTCOLORS = ["#aaaaff", "#7faa7f", "#ffaaaa", "#aaffaa", "#7f7faa",
               "#ffaaff", "#aaffff", "#d5d579", "#ffaaff", "#d57979",
               "#d579d5", "#79d5d5", "#d5d5d5", "#d5d500",
               ]


class HgLogBrowserDialog(QWidget, Ui_HgLogBrowserDialog):
    """
    Class implementing a dialog to browse the log history.
    """
    IconColumn = 0
    BranchColumn = 1
    RevisionColumn = 2
    PhaseColumn = 3
    AuthorColumn = 4
    DateColumn = 5
    MessageColumn = 6
    TagsColumn = 7
    BookmarksColumn = 8
    
    LargefilesCacheL = ".hglf/"
    LargefilesCacheW = ".hglf\\"
    PathSeparatorRe = re.compile(r"/|\\")
    
    GraftedRe = re.compile(r"\(grafted from ([0-9a-fA-F]+)\)")
    GraftedTemplate = '(grafted from <a href="chg:{0}">{0}</a>)'
    
    ClosedIndicator = " \u2612"
    
    def __init__(self, vcs, mode="", parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @type Hg
        @param mode mode of the dialog
        @type str (one of log, full_log, incoming, outgoing)
        @param parent parent widget
        @type QWidget
        """
        super().__init__(parent)
        self.setupUi(self)
        
        windowFlags = self.windowFlags()
        windowFlags |= Qt.WindowType.WindowContextHelpButtonHint
        self.setWindowFlags(windowFlags)
        
        self.mainSplitter.setSizes([300, 400])
        self.mainSplitter.setStretchFactor(0, 1)
        self.mainSplitter.setStretchFactor(1, 2)
        self.diffSplitter.setStretchFactor(0, 1)
        self.diffSplitter.setStretchFactor(1, 2)
        
        if not mode:
            if vcs.getPlugin().getPreferences("LogBrowserShowFullLog"):
                mode = "full_log"
            else:
                mode = "log"
        
        if mode == "log":
            self.setWindowTitle(self.tr("Mercurial Log"))
        elif mode == "incoming":
            self.setWindowTitle(self.tr("Mercurial Log (Incoming)"))
        elif mode == "outgoing":
            self.setWindowTitle(self.tr("Mercurial Log (Outgoing)"))
        elif mode == "full_log":
            self.setWindowTitle(self.tr("Mercurial Full Log"))
        
        self.buttonBox.button(
            QDialogButtonBox.StandardButton.Close).setEnabled(False)
        self.buttonBox.button(
            QDialogButtonBox.StandardButton.Cancel).setDefault(True)
        
        self.filesTree.headerItem().setText(self.filesTree.columnCount(), "")
        self.filesTree.header().setSortIndicator(
            0, Qt.SortOrder.AscendingOrder)
        
        self.upButton.setIcon(UI.PixmapCache.getIcon("1uparrow"))
        self.downButton.setIcon(UI.PixmapCache.getIcon("1downarrow"))
        
        self.refreshButton = self.buttonBox.addButton(
            self.tr("&Refresh"), QDialogButtonBox.ButtonRole.ActionRole)
        self.refreshButton.setToolTip(
            self.tr("Press to refresh the list of changesets"))
        self.refreshButton.setEnabled(False)
        
        self.findPrevButton.setIcon(UI.PixmapCache.getIcon("1leftarrow"))
        self.findNextButton.setIcon(UI.PixmapCache.getIcon("1rightarrow"))
        self.__findBackwards = False
        
        self.modeComboBox.addItem(self.tr("Find"), "find")
        self.modeComboBox.addItem(self.tr("Filter"), "filter")
        
        self.fieldCombo.addItem(self.tr("Revision"), "revision")
        self.fieldCombo.addItem(self.tr("Author"), "author")
        self.fieldCombo.addItem(self.tr("Message"), "message")
        self.fieldCombo.addItem(self.tr("File"), "file")
        self.fieldCombo.addItem(self.tr("Phase"), "phase")
        
        font = Preferences.getEditorOtherFonts("MonospacedFont")
        self.diffEdit.document().setDefaultFont(font)
        
        self.diffHighlighter = HgDiffHighlighter(self.diffEdit.document())
        self.__diffGenerator = HgDiffGenerator(vcs, self)
        self.__diffGenerator.finished.connect(self.__generatorFinished)
        
        self.vcs = vcs
        if mode in ("log", "incoming", "outgoing", "full_log"):
            if mode == "full_log":
                self.commandMode = "incoming"
            else:
                self.commandMode = mode
            self.initialCommandMode = mode
        else:
            self.commandMode = "log"
            self.initialCommandMode = "log"
        self.__hgClient = vcs.getClient()
        
        self.__detailsTemplate = self.tr(
            "<table>"
            "<tr><td><b>Revision</b></td><td>{0}</td></tr>"
            "<tr><td><b>Date</b></td><td>{1}</td></tr>"
            "<tr><td><b>Author</b></td><td>{2}</td></tr>"
            "<tr><td><b>Branch</b></td><td>{3}</td></tr>"
            "{4}"
            "<tr><td><b>Message</b></td><td>{5}</td></tr>"
            "</table>"
        )
        self.__parentsTemplate = self.tr(
            "<tr><td><b>Parents</b></td><td>{0}</td></tr>"
        )
        self.__childrenTemplate = self.tr(
            "<tr><td><b>Children</b></td><td>{0}</td></tr>"
        )
        self.__tagsTemplate = self.tr(
            "<tr><td><b>Tags</b></td><td>{0}</td></tr>"
        )
        self.__latestTagTemplate = self.tr(
            "<tr><td><b>Latest Tag</b></td><td>{0}</td></tr>"
        )
        self.__bookmarksTemplate = self.tr(
            "<tr><td><b>Bookmarks</b></td><td>{0}</td></tr>"
        )
        
        self.__bundle = ""
        self.__filename = ""
        self.__isFile = False
        self.__selectedRevisions = []
        self.intercept = False
        
        self.__initData()
        
        self.__allBranchesFilter = self.tr("All")
        
        self.fromDate.setDisplayFormat("yyyy-MM-dd")
        self.toDate.setDisplayFormat("yyyy-MM-dd")
        self.__resetUI()
        
        # roles used in the log tree
        self.__messageRole = Qt.ItemDataRole.UserRole
        self.__changesRole = Qt.ItemDataRole.UserRole + 1
        self.__edgesRole = Qt.ItemDataRole.UserRole + 2
        self.__parentsRole = Qt.ItemDataRole.UserRole + 3
        self.__latestTagRole = Qt.ItemDataRole.UserRole + 4
        self.__incomingRole = Qt.ItemDataRole.UserRole + 5
        
        # roles used in the file tree
        self.__diffFileLineRole = Qt.ItemDataRole.UserRole
        
        self.flags = {
            'A': self.tr('Added'),
            'D': self.tr('Deleted'),
            'M': self.tr('Modified'),
        }
        
        self.phases = {
            'draft': self.tr("Draft"),
            'public': self.tr("Public"),
            'secret': self.tr("Secret"),
        }
        
        self.__dotRadius = 8
        self.__rowHeight = 20
        
        self.logTree.setIconSize(
            QSize(100 * self.__rowHeight, self.__rowHeight))
        self.BookmarksColumn = self.logTree.columnCount()
        self.logTree.headerItem().setText(
            self.BookmarksColumn, self.tr("Bookmarks"))
        
        self.__logTreeNormalFont = self.logTree.font()
        self.__logTreeNormalFont.setBold(False)
        self.__logTreeBoldFont = self.logTree.font()
        self.__logTreeBoldFont.setBold(True)
        self.__logTreeHasDarkBackground = ericApp().usesDarkPalette()
        
        self.detailsEdit.anchorClicked.connect(self.__revisionClicked)
        
        self.__initActionsMenu()
        
        self.__finishCallbacks = []
        if self.initialCommandMode == "full_log":
            self.__addFinishCallback(self.on_nextButton_clicked)
    
    def __addFinishCallback(self, callback):
        """
        Private method to add a method to be called once the process finished.
        
        The callback methods are invoke in a FIFO style and are consumed. If
        a callback method needs to be called again, it must be added again.
        
        @param callback callback method
        @type function
        """
        if callback not in self.__finishCallbacks:
            self.__finishCallbacks.append(callback)
    
    def __initActionsMenu(self):
        """
        Private method to initialize the actions menu.
        """
        self.__actionsMenu = QMenu()
        self.__actionsMenu.setTearOffEnabled(True)
        self.__actionsMenu.setToolTipsVisible(True)
        
        self.__graftAct = self.__actionsMenu.addAction(
            UI.PixmapCache.getIcon("vcsGraft"),
            self.tr("Copy Changesets"), self.__graftActTriggered)
        self.__graftAct.setToolTip(self.tr(
            "Copy the selected changesets to the current branch"))
        
        self.__mergeAct = self.__actionsMenu.addAction(
            UI.PixmapCache.getIcon("vcsMerge"),
            self.tr("Merge with Changeset"), self.__mergeActTriggered)
        self.__mergeAct.setToolTip(self.tr(
            "Merge the working directory with the selected changeset"))
        
        self.__phaseAct = self.__actionsMenu.addAction(
            self.tr("Change Phase"), self.__phaseActTriggered)
        self.__phaseAct.setToolTip(self.tr(
            "Change the phase of the selected revisions"))
        self.__phaseAct.setWhatsThis(self.tr(
            """<b>Change Phase</b>\n<p>This changes the phase of the"""
            """ selected revisions. The selected revisions have to have"""
            """ the same current phase.</p>"""))
        
        self.__tagAct = self.__actionsMenu.addAction(
            UI.PixmapCache.getIcon("vcsTag"), self.tr("Tag"),
            self.__tagActTriggered)
        self.__tagAct.setToolTip(self.tr("Tag the selected revision"))
        
        self.__closeHeadsAct = self.__actionsMenu.addAction(
            UI.PixmapCache.getIcon("closehead"), self.tr("Close Heads"),
            self.__closeHeadsActTriggered)
        self.__closeHeadsAct.setToolTip(self.tr("Close the selected heads"))
        
        self.__switchAct = self.__actionsMenu.addAction(
            UI.PixmapCache.getIcon("vcsSwitch"), self.tr("Switch"),
            self.__switchActTriggered)
        self.__switchAct.setToolTip(self.tr(
            "Switch the working directory to the selected revision"))
        
        self.__actionsMenu.addSeparator()
        
        self.__bookmarkAct = self.__actionsMenu.addAction(
            UI.PixmapCache.getIcon("addBookmark"),
            self.tr("Define Bookmark..."), self.__bookmarkActTriggered)
        self.__bookmarkAct.setToolTip(
            self.tr("Bookmark the selected revision"))
        self.__bookmarkMoveAct = self.__actionsMenu.addAction(
            UI.PixmapCache.getIcon("moveBookmark"),
            self.tr("Move Bookmark..."), self.__bookmarkMoveActTriggered)
        self.__bookmarkMoveAct.setToolTip(
            self.tr("Move bookmark to the selected revision"))
        
        self.__actionsMenu.addSeparator()
        
        self.__pullAct = self.__actionsMenu.addAction(
            UI.PixmapCache.getIcon("vcsUpdate"), self.tr("Pull Changes"),
            self.__pullActTriggered)
        self.__pullAct.setToolTip(self.tr(
            "Pull changes from a remote repository"))
        self.__lfPullAct = self.__actionsMenu.addAction(
            self.tr("Pull Large Files"), self.__lfPullActTriggered)
        self.__lfPullAct.setToolTip(self.tr(
            "Pull large files for selected revisions"))
        
        self.__actionsMenu.addSeparator()
        
        self.__pushAct = self.__actionsMenu.addAction(
            UI.PixmapCache.getIcon("vcsCommit"),
            self.tr("Push Selected Changes"), self.__pushActTriggered)
        self.__pushAct.setToolTip(self.tr(
            "Push changes of the selected changeset and its ancestors"
            " to a remote repository"))
        self.__pushAllAct = self.__actionsMenu.addAction(
            UI.PixmapCache.getIcon("vcsCommit"),
            self.tr("Push All Changes"), self.__pushAllActTriggered)
        self.__pushAllAct.setToolTip(self.tr(
            "Push all changes to a remote repository"))
        
        self.__actionsMenu.addSeparator()
        
        self.__bundleAct = self.__actionsMenu.addAction(
            UI.PixmapCache.getIcon("vcsCreateChangegroup"),
            self.tr("Create Changegroup"), self.__bundleActTriggered)
        self.__bundleAct.setToolTip(self.tr(
            "Create a changegroup file containing the selected changesets"))
        self.__bundleAct.setWhatsThis(self.tr(
            """<b>Create Changegroup</b>\n<p>This creates a changegroup"""
            """ file containing the selected revisions. If no revisions"""
            """ are selected, all changesets will be bundled. If one"""
            """ revision is selected, it will be interpreted as the base"""
            """ revision. Otherwise the lowest revision will be used as"""
            """ the base revision and all other revision will be bundled."""
            """ If the dialog is showing outgoing changesets, all"""
            """ selected changesets will be bundled.</p>"""))
        self.__unbundleAct = self.__actionsMenu.addAction(
            UI.PixmapCache.getIcon("vcsApplyChangegroup"),
            self.tr("Apply Changegroup"), self.__unbundleActTriggered)
        self.__unbundleAct.setToolTip(self.tr(
            "Apply the currently viewed changegroup file"))
        
        self.__actionsMenu.addSeparator()
        
        self.__gpgSignAct = self.__actionsMenu.addAction(
            UI.PixmapCache.getIcon("changesetSign"),
            self.tr("Sign Revisions"), self.__gpgSignActTriggered)
        self.__gpgSignAct.setToolTip(self.tr(
            "Add a signature for the selected revisions"))
        self.__gpgVerifyAct = self.__actionsMenu.addAction(
            UI.PixmapCache.getIcon("changesetSignVerify"),
            self.tr("Verify Signatures"), self.__gpgVerifyActTriggered)
        self.__gpgVerifyAct.setToolTip(self.tr(
            "Verify all signatures there may be for the selected revision"))
        
        self.__actionsMenu.addSeparator()
        
        self.__stripAct = self.__actionsMenu.addAction(
            UI.PixmapCache.getIcon("fileDelete"),
            self.tr("Strip Changesets"), self.__stripActTriggered)
        self.__stripAct.setToolTip(self.tr(
            "Strip changesets from a repository"))
        
        self.__actionsMenu.addSeparator()
        
        self.__selectAllAct = self.__actionsMenu.addAction(
            self.tr("Select All Entries"), self.__selectAllActTriggered)
        self.__unselectAllAct = self.__actionsMenu.addAction(
            self.tr("Deselect All Entries"),
            lambda: self.__selectAllActTriggered(False))
        
        self.actionsButton.setIcon(
            UI.PixmapCache.getIcon("actionsToolButton"))
        self.actionsButton.setMenu(self.__actionsMenu)
    
    def __initData(self):
        """
        Private method to (re-)initialize some data.
        """
        self.__maxDate = QDate()
        self.__minDate = QDate()
        self.__filterLogsEnabled = True
        
        self.buf = []        # buffer for stdout
        self.diff = None
        self.__started = False
        self.__lastRev = 0
        self.projectMode = False
        
        # attributes to store log graph data
        self.__revs = []
        self.__revColors = {}
        self.__revColor = 0
        
        self.__branchColors = {}
        
        self.__projectWorkingDirParents = []
        self.__projectBranch = ""
        
        self.__childrenInfo = collections.defaultdict(list)
    
    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.__hgClient.isExecuting():
            self.__hgClient.cancel()
        
        self.vcs.getPlugin().setPreferences(
            "LogBrowserGeometry", self.saveGeometry())
        self.vcs.getPlugin().setPreferences(
            "LogBrowserSplitterStates", [
                self.mainSplitter.saveState(),
                self.detailsSplitter.saveState(),
                self.diffSplitter.saveState(),
            ]
        )
        
        e.accept()
    
    def show(self):
        """
        Public slot to show the dialog.
        """
        self.__reloadGeometry()
        self.__restoreSplitterStates()
        self.__resetUI()
        
        super().show()

    def __reloadGeometry(self):
        """
        Private method to restore the geometry.
        """
        geom = self.vcs.getPlugin().getPreferences("LogBrowserGeometry")
        if geom.isEmpty():
            s = QSize(1000, 800)
            self.resize(s)
        else:
            self.restoreGeometry(geom)
    
    def __restoreSplitterStates(self):
        """
        Private method to restore the state of the various splitters.
        """
        states = self.vcs.getPlugin().getPreferences(
            "LogBrowserSplitterStates")
        if len(states) == 3:
            # we have three splitters
            self.mainSplitter.restoreState(states[0])
            self.detailsSplitter.restoreState(states[1])
            self.diffSplitter.restoreState(states[2])
    
    def __resetUI(self):
        """
        Private method to reset the user interface.
        """
        self.branchCombo.clear()
        self.fromDate.setDate(QDate.currentDate())
        self.toDate.setDate(QDate.currentDate())
        self.fieldCombo.setCurrentIndex(self.fieldCombo.findData("message"))
        self.limitSpinBox.setValue(self.vcs.getPlugin().getPreferences(
            "LogLimit"))
        self.stopCheckBox.setChecked(self.vcs.getPlugin().getPreferences(
            "StopLogOnCopy"))
        
        if self.initialCommandMode in ("incoming", "outgoing"):
            self.nextButton.setEnabled(False)
            self.limitSpinBox.setEnabled(False)
        else:
            self.nextButton.setEnabled(True)
            self.limitSpinBox.setEnabled(True)
        
        self.logTree.clear()
        
        if self.initialCommandMode == "full_log":
            self.commandMode = "incoming"
        else:
            self.commandMode = self.initialCommandMode
    
    def __resizeColumnsLog(self):
        """
        Private method to resize the log tree columns.
        """
        self.logTree.header().resizeSections(
            QHeaderView.ResizeMode.ResizeToContents)
        self.logTree.header().setStretchLastSection(True)
    
    def __resizeColumnsFiles(self):
        """
        Private method to resize the changed files tree columns.
        """
        self.filesTree.header().resizeSections(
            QHeaderView.ResizeMode.ResizeToContents)
        self.filesTree.header().setStretchLastSection(True)
    
    def __resortFiles(self):
        """
        Private method to resort the changed files tree.
        """
        sortColumn = self.filesTree.sortColumn()
        self.filesTree.sortItems(
            1, self.filesTree.header().sortIndicatorOrder())
        self.filesTree.sortItems(
            sortColumn, self.filesTree.header().sortIndicatorOrder())
    
    def __getColor(self, n):
        """
        Private method to get the (rotating) name of the color given an index.
        
        @param n color index
        @type int
        @return color name
        @rtype str
        """
        if self.__logTreeHasDarkBackground:
            return LIGHTCOLORS[n % len(LIGHTCOLORS)]
        else:
            return COLORS[n % len(COLORS)]
    
    def __branchColor(self, branchName):
        """
        Private method to calculate a color for a given branch name.
        
        @param branchName name of the branch (string)
        @return name of the color to use (string)
        """
        if branchName not in self.__branchColors:
            self.__branchColors[branchName] = self.__getColor(
                len(self.__branchColors))
        return self.__branchColors[branchName]
    
    def __generateEdges(self, rev, parents):
        """
        Private method to generate edge info for the give data.
        
        @param rev revision to calculate edge info for (integer)
        @param parents list of parent revisions (list of integers)
        @return tuple containing the column and color index for
            the given node and a list of tuples indicating the edges
            between the given node and its parents
            (integer, integer, [(integer, integer, integer), ...])
        """
        if rev not in self.__revs:
            # new head
            self.__revs.append(rev)
            self.__revColors[rev] = self.__revColor
            self.__revColor += 1
        
        col = self.__revs.index(rev)
        color = self.__revColors.pop(rev)
        nextRevs = self.__revs[:]
        
        # add parents to next
        addparents = [p for p in parents if p not in nextRevs]
        nextRevs[col:col + 1] = addparents
        
        # set colors for the parents
        for i, p in enumerate(addparents):
            if not i:
                self.__revColors[p] = color
            else:
                self.__revColors[p] = self.__revColor
                self.__revColor += 1
        
        # add edges to the graph
        edges = []
        if parents[0] != -1:
            for ecol, erev in enumerate(self.__revs):
                if erev in nextRevs:
                    edges.append(
                        (ecol, nextRevs.index(erev), self.__revColors[erev]))
                elif erev == rev:
                    for p in parents:
                        edges.append(
                            (ecol, nextRevs.index(p), self.__revColors[p]))
        
        self.__revs = nextRevs
        return col, color, edges
    
    def __generateIcon(self, column, color, bottomedges, topedges, dotColor,
                       currentRev, closed, isPushableDraft):
        """
        Private method to generate an icon containing the revision tree for the
        given data.
        
        @param column column index of the revision
        @type int
        @param color color of the node
        @type int
        @param bottomedges list of edges for the bottom of the node
        @type list of tuples of (int, int, int)
        @param topedges list of edges for the top of the node
        @type list of tuples of (int, int, int)
        @param dotColor color to be used for the dot
        @type QColor
        @param currentRev flag indicating to draw the icon for the
            current revision
        @type bool
        @param closed flag indicating to draw an icon for a closed
            branch
        @type bool
        @param isPushableDraft flag indicating an entry of phase 'draft',
            that can by pushed
        @type bool
        @return icon for the node
        @rtype QIcon
        """
        def col2x(col, radius):
            """
            Local function to calculate a x-position for a column.
            
            @param col column number (integer)
            @param radius radius of the indicator circle (integer)
            """
            return int(1.2 * radius) * col + radius // 2 + 3
        
        textColor = self.logTree.palette().color(QPalette.ColorRole.Text)
        
        radius = self.__dotRadius
        w = len(bottomedges) * radius + 20
        h = self.__rowHeight
        
        dot_x = col2x(column, radius) - radius // 2
        dot_y = h // 2
        
        pix = QPixmap(w, h)
        pix.fill(QColor(0, 0, 0, 0))        # draw transparent background
        painter = QPainter(pix)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)
        
        # draw the revision history lines
        for y1, y2, lines in ((0, h, bottomedges),
                              (-h, 0, topedges)):
            if lines:
                for start, end, ecolor in lines:
                    lpen = QPen(QColor(self.__getColor(ecolor)))
                    lpen.setWidth(2)
                    painter.setPen(lpen)
                    x1 = col2x(start, radius)
                    x2 = col2x(end, radius)
                    painter.drawLine(x1, dot_y + y1, x2, dot_y + y2)
        
        penradius = 1
        pencolor = textColor
        
        dot_y = (h // 2) - radius // 2
        
        # draw an indicator for the revision
        if currentRev:
            # enlarge for the current revision
            delta = 1
            radius += 2 * delta
            dot_y -= delta
            dot_x -= delta
            penradius = 3
        painter.setBrush(dotColor)
        pen = QPen(pencolor)
        pen.setWidth(penradius)
        painter.setPen(pen)
        if closed:
            painter.drawRect(dot_x - 2, dot_y + 1,
                             radius + 4, radius - 2)
        elif self.commandMode in ("incoming", "outgoing"):
            offset = radius // 2
            if self.commandMode == "incoming":
                # incoming: draw a down arrow
                painter.drawConvexPolygon(
                    QPoint(dot_x, dot_y),
                    QPoint(dot_x + 2 * offset, dot_y),
                    QPoint(dot_x + offset, dot_y + 2 * offset)
                )
            else:
                # outgoing: draw an up arrow
                painter.drawConvexPolygon(
                    QPoint(dot_x + offset, dot_y),
                    QPoint(dot_x, dot_y + 2 * offset),
                    QPoint(dot_x + 2 * offset, dot_y + 2 * offset)
                )
        else:
            if isPushableDraft:
                # 'draft' phase: draw an up arrow like outgoing,
                # if it can be pushed
                offset = radius // 2
                painter.drawConvexPolygon(
                    QPoint(dot_x + offset, dot_y),
                    QPoint(dot_x, dot_y + 2 * offset),
                    QPoint(dot_x + 2 * offset, dot_y + 2 * offset)
                )
            else:
                painter.drawEllipse(dot_x, dot_y, radius, radius)
        painter.end()
        return QIcon(pix)
    
    def __getParents(self, rev):
        """
        Private method to get the parents of the currently viewed
        file/directory.
        
        @param rev revision number to get parents for (string)
        @return list of parent revisions (list of integers)
        """
        errMsg = ""
        parents = [-1]
        
        if int(rev) > 0:
            args = self.vcs.initCommand("parents")
            if self.commandMode == "incoming":
                if self.__bundle:
                    args.append("--repository")
                    args.append(self.__bundle)
                elif (
                    self.vcs.bundleFile and
                    os.path.exists(self.vcs.bundleFile)
                ):
                    args.append("--repository")
                    args.append(self.vcs.bundleFile)
            args.append("--template")
            args.append("{rev}\n")
            args.append("-r")
            args.append(rev)
            if not self.projectMode:
                args.append(self.__filename)
            
            output, errMsg = self.__hgClient.runcommand(args)
            
            if output:
                parents = [int(p) for p in output.strip().splitlines()]
        
        return parents
    
    def __identifyProject(self):
        """
        Private method to determine the revision of the project directory.
        """
        errMsg = ""
        
        args = self.vcs.initCommand("identify")
        args.append("-nb")
        
        output, errMsg = self.__hgClient.runcommand(args)
        
        if errMsg:
            EricMessageBox.critical(
                self,
                self.tr("Mercurial Error"),
                errMsg)
        
        if output:
            outputList = output.strip().split(None, 1)
            if len(outputList) == 2:
                outputRevs = outputList[0].strip()
                if outputRevs.endswith("+"):
                    outputRevs = outputRevs[:-1]
                    self.__projectWorkingDirParents = outputRevs.split('+')
                else:
                    self.__projectWorkingDirParents = [outputRevs]
                self.__projectBranch = outputList[1].strip()
    
    def __getClosedBranches(self):
        """
        Private method to get the list of closed branches.
        """
        self.__closedBranchesRevs = []
        errMsg = ""
        
        args = self.vcs.initCommand("branches")
        args.append("--closed")
        
        output, errMsg = self.__hgClient.runcommand(args)
        
        if errMsg:
            EricMessageBox.critical(
                self,
                self.tr("Mercurial Error"),
                errMsg)
        
        if output:
            for line in output.splitlines():
                if line.strip().endswith("(closed)"):
                    parts = line.split()
                    self.__closedBranchesRevs.append(
                        parts[-2].split(":", 1)[0])
    
    def __getHeads(self):
        """
        Private method to get the list of all heads.
        """
        self.__headRevisions = []
        errMsg = ""
        
        args = self.vcs.initCommand("heads")
        args.append("--closed")
        args.append("--template")
        args.append("{rev}\n")
        
        output, errMsg = self.__hgClient.runcommand(args)
        
        if errMsg:
            EricMessageBox.critical(
                self,
                self.tr("Mercurial Error"),
                errMsg)
        
        if output:
            for line in output.splitlines():
                line = line.strip()
                if line:
                    self.__headRevisions.append(line)
    
    def __getRevisionOfTag(self, tag):
        """
        Private method to get the revision of a tag.
        
        @param tag tag name
        @type str
        @return tuple containing the revision and changeset ID
        @rtype tuple of (str, str)
        """
        errMsg = ""
        
        args = self.vcs.initCommand("tags")
        
        output, errMsg = self.__hgClient.runcommand(args)
        
        if errMsg:
            EricMessageBox.critical(
                self,
                self.tr("Mercurial Error"),
                errMsg)
        
        res = ("", "")
        if output:
            for line in output.splitlines():
                if line.strip():
                    with contextlib.suppress(ValueError):
                        name, rev = line.strip().rsplit(None, 1)
                        if name == tag:
                            res = tuple(rev.split(":", 1))
                            break
        
        return res
    
    def __generateLogItem(self, author, date, message, revision, changedPaths,
                          parents, branches, tags, phase, bookmarks,
                          latestTag, canPush=False):
        """
        Private method to generate a log tree entry.
        
        @param author author info
        @type str
        @param date date info
        @type str
        @param message text of the log message
        @type list of str
        @param revision revision info
        @type str
        @param changedPaths list of dictionary objects containing
            info about the changed files/directories
        @type dict
        @param parents list of parent revisions
        @type list of int
        @param branches list of branches
        @type list of str
        @param tags list of tags
        @type str
        @param phase phase of the entry
        @type str
        @param bookmarks list of bookmarks
        @type str
        @param latestTag the latest tag(s) reachable from the changeset
        @type list of str
        @param canPush flag indicating that changesets can be pushed
        @type bool
        @return reference to the generated item
        @rtype QTreeWidgetItem
        """
        logMessageColumnWidth = self.vcs.getPlugin().getPreferences(
            "LogMessageColumnWidth")
        msgtxt = ""
        for line in message:
            if ". " in line:
                msgtxt += " " + line.strip().split(". ", 1)[0] + "."
                break
            else:
                msgtxt += " " + line.strip()
        if len(msgtxt) > logMessageColumnWidth:
            msgtxt = "{0}...".format(msgtxt[:logMessageColumnWidth])
        
        rev, node = revision.split(":")
        closedStr = (self.ClosedIndicator
                     if rev in self.__closedBranchesRevs else "")
        phaseStr = self.phases.get(phase, phase)
        columnLabels = [
            "",
            branches[0] + closedStr,
            "{0:>7}:{1}".format(rev, node),
            phaseStr,
            author,
            date,
            msgtxt,
            ", ".join(tags),
        ]
        if bookmarks is not None:
            columnLabels.append(", ".join(bookmarks))
        itm = QTreeWidgetItem(self.logTree, columnLabels)
        
        itm.setForeground(self.BranchColumn,
                          QBrush(QColor(self.__branchColor(branches[0]))))
        
        if not self.projectMode:
            parents = self.__getParents(rev)
        if not parents:
            parents = [int(rev) - 1]
        column, color, edges = self.__generateEdges(int(rev), parents)
        
        itm.setData(0, self.__messageRole, message)
        itm.setData(0, self.__changesRole, changedPaths)
        itm.setData(0, self.__edgesRole, edges)
        itm.setData(0, self.__latestTagRole, latestTag)
        if parents == [-1]:
            itm.setData(0, self.__parentsRole, [])
        else:
            itm.setData(0, self.__parentsRole, parents)
            for parent in parents:
                self.__childrenInfo[parent].append(int(rev))
        itm.setData(0, self.__incomingRole, self.commandMode == "incoming")
        
        topedges = (
            self.logTree.topLevelItem(
                self.logTree.indexOfTopLevelItem(itm) - 1
            ).data(0, self.__edgesRole)
            if self.logTree.topLevelItemCount() > 1 else
            None
        )
        
        icon = self.__generateIcon(column, color, edges, topedges,
                                   QColor(self.__branchColor(branches[0])),
                                   rev in self.__projectWorkingDirParents,
                                   rev in self.__closedBranchesRevs,
                                   phase == "draft" and canPush)
        itm.setIcon(0, icon)
        
        try:
            self.__lastRev = int(revision.split(":")[0])
        except ValueError:
            self.__lastRev = 0
        
        return itm
    
    def __getLogEntries(self, startRev=None, noEntries=0):
        """
        Private method to retrieve log entries from the repository.
        
        @param startRev revision number to start from (integer, string)
        @param noEntries number of entries to get (0 = default) (int)
        """
        self.buttonBox.button(
            QDialogButtonBox.StandardButton.Close).setEnabled(False)
        self.buttonBox.button(
            QDialogButtonBox.StandardButton.Cancel).setEnabled(True)
        self.buttonBox.button(
            QDialogButtonBox.StandardButton.Cancel).setDefault(True)
        QApplication.processEvents()
        
        with EricOverrideCursor():
            self.buf = []
            self.cancelled = False
            self.errors.clear()
            self.intercept = False
            
            if noEntries == 0:
                noEntries = self.limitSpinBox.value()
            
            preargs = []
            args = self.vcs.initCommand(self.commandMode)
            args.append('--verbose')
            if self.commandMode not in ("incoming", "outgoing"):
                args.append('--limit')
                args.append(str(noEntries))
            if self.commandMode in ("incoming", "outgoing"):
                args.append("--newest-first")
                if self.vcs.hasSubrepositories():
                    args.append("--subrepos")
            if startRev is not None:
                args.append('--rev')
                args.append('{0}:0'.format(startRev))
            if (
                not self.projectMode and
                not self.stopCheckBox.isChecked()
            ):
                args.append('--follow')
            if self.commandMode == "log":
                args.append('--copies')
            args.append('--template')
            args.append(os.path.join(os.path.dirname(__file__),
                                     "templates",
                                     "logBrowserBookmarkPhase.tmpl"))
            if self.commandMode == "incoming":
                if self.__bundle:
                    args.append(self.__bundle)
                elif not self.vcs.hasSubrepositories():
                    project = ericApp().getObject("Project")
                    self.vcs.bundleFile = os.path.join(
                        project.getProjectManagementDir(), "hg-bundle.hg")
                    if os.path.exists(self.vcs.bundleFile):
                        os.remove(self.vcs.bundleFile)
                    preargs = args[:]
                    preargs.append("--quiet")
                    preargs.append('--bundle')
                    preargs.append(self.vcs.bundleFile)
                    args.append(self.vcs.bundleFile)
            if not self.projectMode:
                args.append(self.__filename)
            
            if preargs:
                out, err = self.__hgClient.runcommand(preargs)
            else:
                err = ""
            if err:
                if (
                    self.commandMode == "incoming" and
                    self.initialCommandMode == "full_log"
                ):
                    # ignore the error
                    self.commandMode = "log"
                else:
                    self.__showError(err)
            elif (
                self.commandMode != "incoming" or
                (self.vcs.bundleFile and
                 os.path.exists(self.vcs.bundleFile)) or
                self.__bundle
            ):
                out, err = self.__hgClient.runcommand(args)
                self.buf = out.splitlines(True)
                if err:
                    self.__showError(err)
                self.__processBuffer()
            elif (
                self.commandMode == "incoming" and
                self.initialCommandMode == "full_log"
            ):
                # no incoming changesets, just switch to log mode
                self.commandMode = "log"
        self.__finish()
    
    def start(self, name=None, bundle=None, isFile=False, noEntries=0):
        """
        Public slot to start the hg log command.
        
        @param name file/directory name to show the log for
        @type str
        @param bundle name of a bundle file
        @type str
        @param isFile flag indicating log for a file is to be shown
        @type bool
        @param noEntries number of entries to get (0 = default)
        @type int
        """
        self.__bundle = bundle
        self.__isFile = isFile
        
        if self.initialCommandMode == "full_log":
            if isFile:
                self.commandMode = "log"
                self.__finishCallbacks = []
            else:
                self.commandMode = "incoming"
                self.__addFinishCallback(self.on_nextButton_clicked)
        
        self.sbsSelectLabel.clear()
        
        self.errorGroup.hide()
        self.errors.clear()
        QApplication.processEvents()
        
        self.__initData()
        
        self.__filename = name
        
        self.projectMode = name is None
        self.stopCheckBox.setDisabled(self.projectMode)
        self.activateWindow()
        self.raise_()
        
        self.logTree.clear()
        self.__started = True
        self.__identifyProject()
        self.__getClosedBranches()
        self.__getHeads()
        self.__getLogEntries(noEntries=noEntries)
    
    def __finish(self):
        """
        Private slot called when the process finished or the user pressed
        the button.
        """
        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.refreshButton.setEnabled(True)
        
        while self.__finishCallbacks:
            self.__finishCallbacks.pop(0)()
    
    def __modifyForLargeFiles(self, filename):
        """
        Private method to convert the displayed file name for a large file.
        
        @param filename file name to be processed (string)
        @return processed file name (string)
        """
        if filename.startswith((self.LargefilesCacheL, self.LargefilesCacheW)):
            return self.tr("{0} (large file)").format(
                self.PathSeparatorRe.split(filename, 1)[1])
        else:
            return filename
    
    def __processBuffer(self):
        """
        Private method to process the buffered output of the hg log command.
        """
        noEntries = 0
        log = {"message": [], "bookmarks": None, "phase": ""}
        changedPaths = []
        initialText = True
        fileCopies = {}
        canPush = self.vcs.canPush()
        for s in self.buf:
            if s != "@@@\n":
                try:
                    key, value = s.split("|", 1)
                except ValueError:
                    key = ""
                    value = s
                if key == "change":
                    initialText = False
                    log["revision"] = value.strip()
                elif key == "user":
                    log["author"] = value.strip()
                elif key == "parents":
                    log["parents"] = [
                        int(x.split(":", 1)[0])
                        for x in value.strip().split()
                    ]
                elif key == "date":
                    log["date"] = " ".join(value.strip().split()[:2])
                elif key == "description":
                    log["message"].append(value.strip())
                elif key == "file_adds":
                    if value.strip():
                        for f in value.strip().split(", "):
                            if f in fileCopies:
                                changedPaths.append({
                                    "action": "A",
                                    "path": self.__modifyForLargeFiles(f),
                                    "copyfrom": self.__modifyForLargeFiles(
                                        fileCopies[f]),
                                })
                            else:
                                changedPaths.append({
                                    "action": "A",
                                    "path": self.__modifyForLargeFiles(f),
                                    "copyfrom": "",
                                })
                elif key == "files_mods":
                    if value.strip():
                        for f in value.strip().split(", "):
                            changedPaths.append({
                                "action": "M",
                                "path": self.__modifyForLargeFiles(f),
                                "copyfrom": "",
                            })
                elif key == "file_dels":
                    if value.strip():
                        for f in value.strip().split(", "):
                            changedPaths.append({
                                "action": "D",
                                "path": self.__modifyForLargeFiles(f),
                                "copyfrom": "",
                            })
                elif key == "file_copies":
                    if value.strip():
                        for entry in value.strip().split(", "):
                            newName, oldName = entry[:-1].split(" (")
                            fileCopies[newName] = oldName
                elif key == "branches":
                    if value.strip():
                        log["branches"] = value.strip().split(", ")
                    else:
                        log["branches"] = ["default"]
                elif key == "tags":
                    log["tags"] = value.strip().split(", ")
                elif key == "bookmarks":
                    log["bookmarks"] = value.strip().split(", ")
                elif key == "phase":
                    log["phase"] = value.strip()
                elif key == "latesttag":
                    tag = value.strip()
                    if tag == "null":
                        log["latesttag"] = []
                    elif ":" in tag:
                        log["latesttag"] = [
                            t.strip() for t in tag.split(":") if t.strip()]
                    else:
                        log["latesttag"] = [tag]
                else:
                    if initialText:
                        continue
                    if value.strip():
                        log["message"].append(value.strip())
            else:
                if len(log) > 1:
                    self.__generateLogItem(
                        log["author"], log["date"],
                        log["message"], log["revision"], changedPaths,
                        log["parents"], log["branches"], log["tags"],
                        log["phase"], log["bookmarks"], log["latesttag"],
                        canPush=canPush)
                    dt = QDate.fromString(log["date"], Qt.DateFormat.ISODate)
                    if (
                        not self.__maxDate.isValid() and
                        not self.__minDate.isValid()
                    ):
                        self.__maxDate = dt
                        self.__minDate = dt
                    else:
                        if self.__maxDate < dt:
                            self.__maxDate = dt
                        if self.__minDate > dt:
                            self.__minDate = dt
                    noEntries += 1
                    log = {"message": [], "bookmarks": None, "phase": ""}
                    changedPaths = []
                    fileCopies = {}
        
        self.__resizeColumnsLog()
        
        if self.__started and not self.__finishCallbacks:
            # we are really done
            if self.__selectedRevisions:
                foundItems = self.logTree.findItems(
                    self.__selectedRevisions[0], Qt.MatchFlag.MatchExactly,
                    self.RevisionColumn)
                if foundItems:
                    self.logTree.setCurrentItem(foundItems[0])
                else:
                    self.logTree.setCurrentItem(
                        self.logTree.topLevelItem(0))
            elif self.__projectWorkingDirParents:
                for rev in self.__projectWorkingDirParents:
                    # rev string format must match with the format of the
                    # __generateLogItem() method
                    items = self.logTree.findItems(
                        "{0:>7}:".format(rev),
                        Qt.MatchFlag.MatchStartsWith,
                        self.RevisionColumn)
                    if items:
                        self.logTree.setCurrentItem(items[0])
                        break
                else:
                    self.logTree.setCurrentItem(
                        self.logTree.topLevelItem(0))
            else:
                self.logTree.setCurrentItem(self.logTree.topLevelItem(0))
            self.__started = False
        
        if self.commandMode in ("incoming", "outgoing"):
            self.commandMode = "log"    # switch to log mode
            if self.__lastRev > 0:
                self.nextButton.setEnabled(True)
                self.limitSpinBox.setEnabled(True)
        else:
            if noEntries < self.limitSpinBox.value() and not self.cancelled:
                self.nextButton.setEnabled(False)
                self.limitSpinBox.setEnabled(False)
        
        # update the log filters
        self.__filterLogsEnabled = False
        self.fromDate.setMinimumDate(self.__minDate)
        self.fromDate.setMaximumDate(self.__maxDate)
        self.fromDate.setDate(self.__minDate)
        self.toDate.setMinimumDate(self.__minDate)
        self.toDate.setMaximumDate(self.__maxDate)
        self.toDate.setDate(self.__maxDate)
        
        branchFilter = self.branchCombo.currentText()
        if not branchFilter:
            branchFilter = self.__allBranchesFilter
        self.branchCombo.clear()
        self.branchCombo.addItems(
            [self.__allBranchesFilter] + sorted(self.__branchColors.keys()))
        self.branchCombo.setCurrentIndex(
            self.branchCombo.findText(branchFilter))
        
        self.__filterLogsEnabled = True
        if self.__actionMode() == "filter":
            self.__filterLogs()
        self.__updateToolMenuActions()
        
        # restore selected item
        if self.__selectedRevisions and not self.__finishCallbacks:
            # we are really done
            for revision in self.__selectedRevisions:
                items = self.logTree.findItems(
                    revision, Qt.MatchFlag.MatchExactly, self.RevisionColumn)
                if items:
                    items[0].setSelected(True)
            self.__selectedRevisions = []
    
    def __showError(self, out):
        """
        Private slot to show some error.
        
        @param out error to be shown (string)
        """
        self.errorGroup.show()
        self.errors.insertPlainText(out)
        self.errors.ensureCursorVisible()
    
    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(
            QDialogButtonBox.StandardButton.Close
        ):
            self.close()
        elif button == self.buttonBox.button(
            QDialogButtonBox.StandardButton.Cancel
        ):
            self.cancelled = True
            self.__hgClient.cancel()
        elif button == self.refreshButton:
            self.on_refreshButton_clicked()
    
    def __updateSbsSelectLabel(self):
        """
        Private slot to update the enabled status of the diff buttons.
        """
        self.sbsSelectLabel.clear()
        if self.__isFile:
            selectedItems = self.logTree.selectedItems()
            if len(selectedItems) == 1:
                currentItem = selectedItems[0]
                rev2 = (
                    currentItem.text(self.RevisionColumn).split(":", 1)[0]
                    .strip()
                )
                parents = currentItem.data(0, self.__parentsRole)
                if parents:
                    parentLinks = []
                    for index in range(len(parents)):
                        parentLinks.append(
                            '<a href="sbsdiff:{0}_{1}">&nbsp;{2}&nbsp;</a>'
                            .format(parents[index], rev2, index + 1))
                    self.sbsSelectLabel.setText(
                        self.tr('Side-by-Side Diff to Parent {0}').format(
                            " ".join(parentLinks)))
            elif len(selectedItems) == 2:
                rev1 = int(selectedItems[0].text(self.RevisionColumn)
                           .split(":", 1)[0])
                rev2 = int(selectedItems[1].text(self.RevisionColumn)
                           .split(":", 1)[0])
                if rev1 > rev2:
                    # Swap the entries, so that rev1 < rev2
                    rev1, rev2 = rev2, rev1
                self.sbsSelectLabel.setText(self.tr(
                    '<a href="sbsdiff:{0}_{1}">Side-by-Side Compare</a>')
                    .format(rev1, rev2))
    
    def __updateToolMenuActions(self):
        """
        Private slot to update the status of the tool menu actions and
        the tool menu button.
        """
        if self.initialCommandMode in ("log", "full_log") and self.projectMode:
            # do the phase action
            # step 1: count entries with changeable phases
            secret = 0
            draft = 0
            public = 0
            for itm in [item for item in self.logTree.selectedItems()
                        if not item.data(0, self.__incomingRole)]:
                # count phase for local items only
                phase = itm.text(self.PhaseColumn)
                if phase == self.phases["draft"]:
                    draft += 1
                elif phase == self.phases["secret"]:
                    secret += 1
                else:
                    public += 1
            
            # step 2: set the status of the phase action
            if (
                public == 0 and
                ((secret > 0 and draft == 0) or
                 (secret == 0 and draft > 0))
            ):
                self.__phaseAct.setEnabled(True)
            else:
                self.__phaseAct.setEnabled(False)
            
            # do the graft action
            # step 1: count selected entries not belonging to the
            #         current branch
            otherBranches = 0
            for itm in [item for item in self.logTree.selectedItems()
                        if not item.data(0, self.__incomingRole)]:
                # for local items only
                branch = itm.text(self.BranchColumn)
                if branch != self.__projectBranch:
                    otherBranches += 1
            
            # step 2: set the status of the graft action
            self.__graftAct.setEnabled(otherBranches > 0)
            
            selectedItemsCount = len([
                itm for itm in self.logTree.selectedItems()
                if not itm.data(0, self.__incomingRole)
            ])
            selectedIncomingItemsCount = len([
                itm for itm in self.logTree.selectedItems()
                if itm.data(0, self.__incomingRole)
            ])
            
            self.__mergeAct.setEnabled(selectedItemsCount == 1)
            self.__tagAct.setEnabled(selectedItemsCount == 1)
            self.__switchAct.setEnabled(selectedItemsCount == 1)
            self.__bookmarkAct.setEnabled(selectedItemsCount == 1)
            self.__bookmarkMoveAct.setEnabled(selectedItemsCount == 1)
            
            if selectedIncomingItemsCount > 0:
                self.__pullAct.setText(self.tr("Pull Selected Changes"))
            else:
                self.__pullAct.setText(self.tr("Pull Changes"))
            if self.vcs.canPull():
                self.__pullAct.setEnabled(True)
                self.__lfPullAct.setEnabled(
                    self.vcs.isExtensionActive("largefiles") and
                    selectedItemsCount > 0)
            else:
                self.__pullAct.setEnabled(False)
                self.__lfPullAct.setEnabled(False)
            
            if self.vcs.canPush():
                self.__pushAct.setEnabled(
                    selectedItemsCount == 1 and
                    not self.logTree.selectedItems()[0].data(
                        0, self.__incomingRole) and
                    self.logTree.selectedItems()[0].text(self.PhaseColumn) ==
                    self.phases["draft"])
                self.__pushAllAct.setEnabled(True)
            else:
                self.__pushAct.setEnabled(False)
                self.__pushAllAct.setEnabled(False)
            
            self.__stripAct.setEnabled(
                self.vcs.isExtensionActive("strip") and
                selectedItemsCount == 1)
            
            # count incoming items for 'full_log'
            if self.initialCommandMode == "full_log":
                # incoming items are at the top
                incomingCount = 0
                for row in range(self.logTree.topLevelItemCount()):
                    if self.logTree.topLevelItem(row).data(
                            0, self.__incomingRole):
                        incomingCount += 1
                    else:
                        break
                localCount = self.logTree.topLevelItemCount() - incomingCount
            else:
                localCount = self.logTree.topLevelItemCount()
            self.__bundleAct.setEnabled(localCount > 0)
            self.__unbundleAct.setEnabled(False)
            
            self.__gpgSignAct.setEnabled(
                self.vcs.isExtensionActive("gpg") and
                selectedItemsCount > 0)
            self.__gpgVerifyAct.setEnabled(
                self.vcs.isExtensionActive("gpg") and
                selectedItemsCount == 1)
            
            if self.vcs.isExtensionActive("closehead"):
                revs = [itm.text(self.RevisionColumn).strip().split(":", 1)[0]
                        for itm in self.logTree.selectedItems()
                        if not itm.data(0, self.__incomingRole)]
                revs = [rev for rev in revs if rev in self.__headRevisions]
                self.__closeHeadsAct.setEnabled(len(revs) > 0)
            else:
                self.__closeHeadsAct.setEnabled(False)
            self.actionsButton.setEnabled(True)
        
        elif self.initialCommandMode == "incoming" and self.projectMode:
            for act in [self.__phaseAct, self.__graftAct, self.__mergeAct,
                        self.__tagAct, self.__closeHeadsAct, self.__switchAct,
                        self.__bookmarkAct, self.__bookmarkMoveAct,
                        self.__pushAct, self.__pushAllAct, self.__stripAct,
                        self.__bundleAct, self.__gpgSignAct,
                        self.__gpgVerifyAct]:
                act.setEnabled(False)
            
            self.__pullAct.setText(self.tr("Pull Selected Changes"))
            if self.vcs.canPull() and not bool(self.__bundle):
                selectedIncomingItemsCount = len([
                    itm for itm in self.logTree.selectedItems()
                    if itm.data(0, self.__incomingRole)
                ])
                self.__pullAct.setEnabled(selectedIncomingItemsCount > 0)
                self.__lfPullAct.setEnabled(
                    self.vcs.isExtensionActive("largefiles") and
                    selectedIncomingItemsCount > 0)
            else:
                self.__pullAct.setEnabled(False)
                self.__lfPullAct.setEnabled(False)
            
            self.__unbundleAct.setEnabled(bool(self.__bundle))
            
            self.actionsButton.setEnabled(True)
        
        elif self.initialCommandMode == "outgoing" and self.projectMode:
            for act in [self.__phaseAct, self.__graftAct, self.__mergeAct,
                        self.__tagAct, self.__closeHeadsAct, self.__switchAct,
                        self.__bookmarkAct, self.__bookmarkMoveAct,
                        self.__pullAct, self.__lfPullAct,
                        self.__stripAct, self.__gpgSignAct,
                        self.__gpgVerifyAct, self.__unbundleAct]:
                act.setEnabled(False)
            
            selectedItemsCount = len(self.logTree.selectedItems())
            if self.vcs.canPush():
                self.__pushAct.setEnabled(
                    selectedItemsCount == 1 and
                    self.logTree.selectedItems()[0].text(self.PhaseColumn) ==
                    self.phases["draft"])
                self.__pushAllAct.setEnabled(True)
            else:
                self.__pushAct.setEnabled(False)
                self.__pushAllAct.setEnabled(False)
            
            self.__bundleAct.setEnabled(selectedItemsCount > 0)
        
        else:
            self.actionsButton.setEnabled(False)
    
    def __updateDetailsAndFiles(self):
        """
        Private slot to update the details and file changes panes.
        """
        self.detailsEdit.clear()
        self.filesTree.clear()
        self.__diffUpdatesFiles = False
        
        selectedItems = self.logTree.selectedItems()
        if len(selectedItems) == 1:
            self.detailsEdit.setHtml(
                self.__generateDetailsTableText(selectedItems[0]))
            self.__updateFilesTree(self.filesTree, selectedItems[0])
            self.__resizeColumnsFiles()
            self.__resortFiles()
        elif len(selectedItems) == 2:
            self.__diffUpdatesFiles = True
            index1 = self.logTree.indexOfTopLevelItem(selectedItems[0])
            index2 = self.logTree.indexOfTopLevelItem(selectedItems[1])
            if index1 > index2:
                # Swap the entries
                selectedItems[0], selectedItems[1] = (
                    selectedItems[1], selectedItems[0]
                )
            html = "{0}<hr/>{1}".format(
                self.__generateDetailsTableText(selectedItems[0]),
                self.__generateDetailsTableText(selectedItems[1]),
            )
            self.detailsEdit.setHtml(html)
            # self.filesTree is updated by the diff
    
    def __generateDetailsTableText(self, itm):
        """
        Private method to generate an HTML table with the details of the given
        changeset.
        
        @param itm reference to the item the table should be based on
        @type QTreeWidgetItem
        @return HTML table containing details
        @rtype str
        """
        if itm is not None:
            if itm.text(self.TagsColumn):
                tagsStr = self.__tagsTemplate.format(itm.text(self.TagsColumn))
            else:
                tagsStr = ""
            
            if itm.text(self.BookmarksColumn):
                bookmarksStr = self.__bookmarksTemplate.format(
                    itm.text(self.BookmarksColumn))
            else:
                bookmarksStr = ""
            
            if self.projectMode and itm.data(0, self.__latestTagRole):
                latestTagLinks = []
                for tag in itm.data(0, self.__latestTagRole):
                    latestTagLinks.append('<a href="rev:{0}">{1}</a>'.format(
                        self.__getRevisionOfTag(tag)[0], tag))
                latestTagStr = self.__latestTagTemplate.format(
                    ", ".join(latestTagLinks))
            else:
                latestTagStr = ""
            
            rev = int(itm.text(self.RevisionColumn).split(":", 1)[0])
            
            if itm.data(0, self.__parentsRole):
                parentLinks = []
                for parent in [str(x) for x in
                               itm.data(0, self.__parentsRole)]:
                    parentLinks.append(
                        '<a href="rev:{0}">{0}</a>'.format(parent))
                parentsStr = self.__parentsTemplate.format(
                    ", ".join(parentLinks))
            else:
                parentsStr = ""
            
            if self.__childrenInfo[rev]:
                childLinks = []
                for child in [str(x) for x in self.__childrenInfo[rev]]:
                    childLinks.append(
                        '<a href="rev:{0}">{0}</a>'.format(child))
                childrenStr = self.__childrenTemplate.format(
                    ", ".join(childLinks))
            else:
                childrenStr = ""
            
            messagesList = []
            for line in itm.data(0, self.__messageRole):
                match = HgLogBrowserDialog.GraftedRe.fullmatch(line)
                if match:
                    messagesList.append(
                        HgLogBrowserDialog.GraftedTemplate.format(
                            match.group(1)))
                else:
                    messagesList.append(Utilities.html_encode(line.strip()))
            messageStr = "<br />\n".join(messagesList)
            
            html = self.__detailsTemplate.format(
                itm.text(self.RevisionColumn),
                itm.text(self.DateColumn),
                itm.text(self.AuthorColumn),
                itm.text(self.BranchColumn).replace(
                    self.ClosedIndicator, ""),
                parentsStr + childrenStr + tagsStr + latestTagStr +
                bookmarksStr,
                messageStr,
            )
        else:
            html = ""
        
        return html
    
    def __updateFilesTree(self, parent, itm):
        """
        Private method to update the files tree with changes of the given item.
        
        @param parent parent for the items to be added
        @type QTreeWidget or QTreeWidgetItem
        @param itm reference to the item the update should be based on
        @type QTreeWidgetItem
        """
        if itm is not None:
            changes = itm.data(0, self.__changesRole)
            if len(changes) > 0:
                for change in changes:
                    QTreeWidgetItem(parent, [
                        self.flags[change["action"]],
                        change["path"].strip(),
                        change["copyfrom"].strip(),
                    ])
    
    @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem)
    def on_logTree_currentItemChanged(self, current, previous):
        """
        Private slot called, when the current item of the log tree changes.
        
        @param current reference to the new current item (QTreeWidgetItem)
        @param previous reference to the old current item (QTreeWidgetItem)
        """
        self.__updateToolMenuActions()
        
        # Highlight the current entry using a bold font
        for col in range(self.logTree.columnCount()):
            current and current.setFont(col, self.__logTreeBoldFont)
            previous and previous.setFont(col, self.__logTreeNormalFont)
        
        # set the state of the up and down buttons
        self.upButton.setEnabled(
            current is not None and
            self.logTree.indexOfTopLevelItem(current) > 0)
        self.downButton.setEnabled(
            current is not None and
            int(current.text(self.RevisionColumn).split(":")[0]) > 0 and
            (self.logTree.indexOfTopLevelItem(current) <
                self.logTree.topLevelItemCount() - 1 or
             self.nextButton.isEnabled()))
    
    @pyqtSlot()
    def on_logTree_itemSelectionChanged(self):
        """
        Private slot called, when the selection has changed.
        """
        self.__updateDetailsAndFiles()
        self.__updateSbsSelectLabel()
        self.__updateToolMenuActions()
        self.__generateDiffs()
    
    @pyqtSlot()
    def on_upButton_clicked(self):
        """
        Private slot to move the current item up one entry.
        """
        itm = self.logTree.itemAbove(self.logTree.currentItem())
        if itm:
            self.logTree.setCurrentItem(itm)
    
    @pyqtSlot()
    def on_downButton_clicked(self):
        """
        Private slot to move the current item down one entry.
        """
        itm = self.logTree.itemBelow(self.logTree.currentItem())
        if itm:
            self.logTree.setCurrentItem(itm)
        else:
            # load the next bunch and try again
            if self.nextButton.isEnabled():
                self.__addFinishCallback(self.on_downButton_clicked)
                self.on_nextButton_clicked()
    
    @pyqtSlot()
    def on_nextButton_clicked(self):
        """
        Private slot to handle the Next button.
        """
        if self.nextButton.isEnabled():
            if self.__lastRev > 0:
                self.__getLogEntries(startRev=self.__lastRev - 1)
            else:
                self.__getLogEntries()
    
    @pyqtSlot(QDate)
    def on_fromDate_dateChanged(self, date):
        """
        Private slot called, when the from date changes.
        
        @param date new date (QDate)
        """
        if self.__actionMode() == "filter":
            self.__filterLogs()
    
    @pyqtSlot(QDate)
    def on_toDate_dateChanged(self, date):
        """
        Private slot called, when the from date changes.
        
        @param date new date (QDate)
        """
        if self.__actionMode() == "filter":
            self.__filterLogs()
    
    @pyqtSlot(int)
    def on_branchCombo_activated(self, index):
        """
        Private slot called, when a new branch is selected.
        
        @param index index of the selected entry
        @type int
        """
        if self.__actionMode() == "filter":
            self.__filterLogs()
    
    @pyqtSlot(int)
    def on_fieldCombo_activated(self, index):
        """
        Private slot called, when a new filter field is selected.
        
        @param index index of the selected entry
        @type int
        """
        if self.__actionMode() == "filter":
            self.__filterLogs()
    
    @pyqtSlot(str)
    def on_rxEdit_textChanged(self, txt):
        """
        Private slot called, when a filter expression is entered.
        
        @param txt filter expression (string)
        """
        if self.__actionMode() == "filter":
            self.__filterLogs()
        elif self.__actionMode() == "find":
            self.__findItem(self.__findBackwards, interactive=True)
    
    @pyqtSlot()
    def on_rxEdit_returnPressed(self):
        """
        Private slot handling a press of the Return key in the rxEdit input.
        """
        if self.__actionMode() == "find":
            self.__findItem(self.__findBackwards, interactive=True)
    
    def __filterLogs(self):
        """
        Private method to filter the log entries.
        """
        if self.__filterLogsEnabled:
            from_ = self.fromDate.date().toString("yyyy-MM-dd")
            to_ = self.toDate.date().addDays(1).toString("yyyy-MM-dd")
            branch = self.branchCombo.currentText()
            closedBranch = branch + '--'
            fieldIndex, searchRx, indexIsRole = self.__prepareFieldSearch()
            
            visibleItemCount = self.logTree.topLevelItemCount()
            currentItem = self.logTree.currentItem()
            for topIndex in range(self.logTree.topLevelItemCount()):
                topItem = self.logTree.topLevelItem(topIndex)
                if indexIsRole:
                    if fieldIndex == self.__changesRole:
                        changes = topItem.data(0, self.__changesRole)
                        txt = "\n".join(
                            [c["path"] for c in changes] +
                            [c["copyfrom"] for c in changes]
                        )
                    else:
                        # Find based on complete message text
                        txt = "\n".join(topItem.data(0, self.__messageRole))
                else:
                    txt = topItem.text(fieldIndex)
                if (
                    topItem.text(self.DateColumn) <= to_ and
                    topItem.text(self.DateColumn) >= from_ and
                    (branch == self.__allBranchesFilter or
                     topItem.text(self.BranchColumn) in
                        [branch, closedBranch]) and
                    searchRx.search(txt) is not None
                ):
                    topItem.setHidden(False)
                    if topItem is currentItem:
                        self.on_logTree_currentItemChanged(topItem, None)
                else:
                    topItem.setHidden(True)
                    if topItem is currentItem:
                        self.filesTree.clear()
                    visibleItemCount -= 1
            self.logTree.header().setSectionHidden(
                self.IconColumn,
                visibleItemCount != self.logTree.topLevelItemCount())
    
    def __prepareFieldSearch(self):
        """
        Private slot to prepare the filed search data.
        
        @return tuple of field index, search expression and flag indicating
            that the field index is a data role (integer, string, boolean)
        """
        indexIsRole = False
        txt = self.fieldCombo.itemData(self.fieldCombo.currentIndex())
        if txt == "author":
            fieldIndex = self.AuthorColumn
            searchRx = re.compile(self.rxEdit.text(), re.IGNORECASE)
        elif txt == "revision":
            fieldIndex = self.RevisionColumn
            txt = self.rxEdit.text()
            if txt.startswith("^"):
                searchRx = re.compile(r"^\s*{0}".format(txt[1:]),
                                      re.IGNORECASE)
            else:
                searchRx = re.compile(txt, re.IGNORECASE)
        elif txt == "file":
            fieldIndex = self.__changesRole
            searchRx = re.compile(self.rxEdit.text(), re.IGNORECASE)
            indexIsRole = True
        elif txt == "phase":
            fieldIndex = self.PhaseColumn
            searchRx = re.compile(self.rxEdit.text(), re.IGNORECASE)
        else:
            fieldIndex = self.__messageRole
            searchRx = re.compile(self.rxEdit.text(), re.IGNORECASE)
            indexIsRole = True
        
        return fieldIndex, searchRx, indexIsRole
    
    @pyqtSlot(bool)
    def on_stopCheckBox_clicked(self, checked):
        """
        Private slot called, when the stop on copy/move checkbox is clicked.
        
        @param checked flag indicating the state of the check box (boolean)
        """
        self.vcs.getPlugin().setPreferences("StopLogOnCopy",
                                            self.stopCheckBox.isChecked())
        self.nextButton.setEnabled(True)
        self.limitSpinBox.setEnabled(True)
    
    @pyqtSlot()
    def on_refreshButton_clicked(self, addNext=False):
        """
        Private slot to refresh the log.
        
        @param addNext flag indicating to get a second batch of log entries as
            well
        @type bool
        """
        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)
        
        # save the selected items commit IDs
        self.__selectedRevisions = []
        for item in self.logTree.selectedItems():
            self.__selectedRevisions.append(item.text(self.RevisionColumn))
        
        if self.initialCommandMode in ("incoming", "outgoing"):
            self.nextButton.setEnabled(False)
            self.limitSpinBox.setEnabled(False)
            if addNext:
                self.__addFinishCallback(self.on_nextButton_clicked)
        else:
            self.nextButton.setEnabled(True)
            self.limitSpinBox.setEnabled(True)
        
        if self.initialCommandMode == "full_log":
            self.commandMode = "incoming"
            self.__addFinishCallback(self.on_nextButton_clicked)
        else:
            self.commandMode = self.initialCommandMode
        self.start(self.__filename, bundle=self.__bundle, isFile=self.__isFile,
                   noEntries=self.logTree.topLevelItemCount())
    
    @pyqtSlot()
    def __phaseActTriggered(self):
        """
        Private slot to handle the Change Phase action.
        """
        itm = self.logTree.selectedItems()[0]
        if not itm.data(0, self.__incomingRole):
            currentPhase = itm.text(self.PhaseColumn)
            revs = []
            for itm in self.logTree.selectedItems():
                if itm.text(self.PhaseColumn) == currentPhase:
                    revs.append(
                        itm.text(self.RevisionColumn).split(":")[0].strip())
            
            if not revs:
                self.__phaseAct.setEnabled(False)
                return
            
            if currentPhase == self.phases["draft"]:
                newPhase = self.phases["secret"]
                data = (revs, "s", True)
            else:
                newPhase = self.phases["draft"]
                data = (revs, "d", False)
            res = self.vcs.hgPhase(data)
            if res:
                for itm in self.logTree.selectedItems():
                    itm.setText(self.PhaseColumn, newPhase)
    
    @pyqtSlot()
    def __graftActTriggered(self):
        """
        Private slot to handle the Copy Changesets action.
        """
        revs = []
        
        for itm in [item for item in self.logTree.selectedItems()
                    if not item.data(0, self.__incomingRole)]:
            branch = itm.text(self.BranchColumn)
            if branch != self.__projectBranch:
                revs.append(
                    itm.text(self.RevisionColumn).strip().split(":", 1)[0])
        
        if revs:
            shouldReopen = self.vcs.hgGraft(revs)
            if shouldReopen:
                res = EricMessageBox.yesNo(
                    None,
                    self.tr("Copy Changesets"),
                    self.tr(
                        """The project should be reread. Do this now?"""),
                    yesDefault=True)
                if res:
                    ericApp().getObject("Project").reopenProject()
                    return
            
            self.on_refreshButton_clicked()
    
    @pyqtSlot()
    def __tagActTriggered(self):
        """
        Private slot to tag the selected revision.
        """
        if len([itm for itm in self.logTree.selectedItems()
                if not itm.data(0, self.__incomingRole)]) == 1:
            itm = self.logTree.selectedItems()[0]
            rev = itm.text(self.RevisionColumn).strip().split(":", 1)[0]
            tag = itm.text(self.TagsColumn).strip().split(", ", 1)[0]
            res = self.vcs.vcsTag(revision=rev, tagName=tag)
            if res:
                self.on_refreshButton_clicked()
    
    @pyqtSlot()
    def __closeHeadsActTriggered(self):
        """
        Private slot to close the selected head revisions.
        """
        if self.vcs.isExtensionActive("closehead"):
            revs = [itm.text(self.RevisionColumn).strip().split(":", 1)[0]
                    for itm in self.logTree.selectedItems()
                    if not itm.data(0, self.__incomingRole)]
            revs = [rev for rev in revs if rev in self.__headRevisions]
            if revs:
                closeheadExtension = self.vcs.getExtensionObject("closehead")
                if closeheadExtension is not None:
                    closeheadExtension.hgCloseheads(revisions=revs)
                    
                    self.on_refreshButton_clicked()
    
    @pyqtSlot()
    def __switchActTriggered(self):
        """
        Private slot to switch the working directory to the
        selected revision.
        """
        if len([itm for itm in self.logTree.selectedItems()
                if not itm.data(0, self.__incomingRole)]) == 1:
            itm = self.logTree.selectedItems()[0]
            rev = itm.text(self.RevisionColumn).strip().split(":", 1)[0]
            bookmarks = [bm.strip() for bm in
                         itm.text(self.BookmarksColumn).strip().split(",")
                         if bm.strip()]
            if bookmarks:
                bookmark, ok = QInputDialog.getItem(
                    self,
                    self.tr("Switch"),
                    self.tr("Select bookmark to switch to (leave empty to"
                            " use revision):"),
                    [""] + bookmarks,
                    0, False)
                if not ok:
                    return
                if bookmark:
                    rev = bookmark
            if rev:
                shouldReopen = self.vcs.vcsUpdate(revision=rev)
                if shouldReopen:
                    res = EricMessageBox.yesNo(
                        None,
                        self.tr("Switch"),
                        self.tr(
                            """The project should be reread. Do this now?"""),
                        yesDefault=True)
                    if res:
                        ericApp().getObject("Project").reopenProject()
                        return
                
                self.on_refreshButton_clicked()
    
    @pyqtSlot()
    def __bookmarkActTriggered(self):
        """
        Private slot to bookmark the selected revision.
        """
        if len([itm for itm in self.logTree.selectedItems()
                if not itm.data(0, self.__incomingRole)]) == 1:
            itm = self.logTree.selectedItems()[0]
            rev, changeset = (
                itm.text(self.RevisionColumn).strip().split(":", 1)
            )
            bookmark, ok = QInputDialog.getText(
                self,
                self.tr("Define Bookmark"),
                self.tr('Enter bookmark name for changeset "{0}":').format(
                    changeset),
                QLineEdit.EchoMode.Normal)
            if ok and bool(bookmark):
                self.vcs.hgBookmarkDefine(
                    revision="rev({0})".format(rev),
                    bookmark=bookmark)
                self.on_refreshButton_clicked()
    
    @pyqtSlot()
    def __bookmarkMoveActTriggered(self):
        """
        Private slot to move a bookmark to the selected revision.
        """
        if len([itm for itm in self.logTree.selectedItems()
                if not itm.data(0, self.__incomingRole)]) == 1:
            itm = self.logTree.selectedItems()[0]
            rev, changeset = (
                itm.text(self.RevisionColumn).strip().split(":", 1)
            )
            bookmarksList = self.vcs.hgGetBookmarksList()
            bookmark, ok = QInputDialog.getItem(
                self,
                self.tr("Move Bookmark"),
                self.tr('Select the bookmark to be moved  to changeset'
                        ' "{0}":').format(changeset),
                [""] + bookmarksList,
                0, False)
            if ok and bool(bookmark):
                self.vcs.hgBookmarkMove(
                    revision="rev({0})".format(rev),
                    bookmark=bookmark)
                self.on_refreshButton_clicked()
    
    @pyqtSlot()
    def __lfPullActTriggered(self):
        """
        Private slot to pull large files of selected revisions.
        """
        revs = []
        for itm in [item for item in self.logTree.selectedItems()
                    if not item.data(0, self.__incomingRole)]:
            rev = itm.text(self.RevisionColumn).strip().split(":", 1)[0]
            if rev:
                revs.append(rev)
        
        if revs:
            self.vcs.getExtensionObject("largefiles").hgLfPull(revisions=revs)
    
    @pyqtSlot()
    def __pullActTriggered(self):
        """
        Private slot to pull changes from a remote repository.
        """
        shouldReopen = False
        refresh = False
        addNext = False
        
        if self.initialCommandMode in ("log", "full_log", "incoming"):
            revs = []
            for itm in [item for item in self.logTree.selectedItems()
                        if item.data(0, self.__incomingRole)]:
                rev = itm.text(self.RevisionColumn).split(":")[1].strip()
                if rev:
                    revs.append(rev)
            shouldReopen = self.vcs.hgPull(revisions=revs)
            refresh = True
            if self.initialCommandMode == "incoming":
                addNext = True
        
        if shouldReopen:
            res = EricMessageBox.yesNo(
                None,
                self.tr("Pull Changes"),
                self.tr(
                    """The project should be reread. Do this now?"""),
                yesDefault=True)
            if res:
                ericApp().getObject("Project").reopenProject()
                return
        
        if refresh:
            self.on_refreshButton_clicked(addNext=addNext)
    
    @pyqtSlot()
    def __pushActTriggered(self):
        """
        Private slot to push changes to a remote repository up to a selected
        changeset.
        """
        itm = self.logTree.selectedItems()[0]
        if not itm.data(0, self.__incomingRole):
            rev = itm.text(self.RevisionColumn).strip().split(":", 1)[0]
            if rev:
                self.vcs.hgPush(rev=rev)
                self.on_refreshButton_clicked(
                    addNext=self.initialCommandMode == "outgoing")
    
    @pyqtSlot()
    def __pushAllActTriggered(self):
        """
        Private slot to push all changes to a remote repository.
        """
        self.vcs.hgPush()
        self.on_refreshButton_clicked()
    
    @pyqtSlot()
    def __stripActTriggered(self):
        """
        Private slot to strip changesets from the repository.
        """
        itm = self.logTree.selectedItems()[0]
        if not itm.data(0, self.__incomingRole):
            rev = itm.text(self.RevisionColumn).strip().split(":", 1)[1]
            shouldReopen = self.vcs.getExtensionObject("strip").hgStrip(
                rev=rev)
            if shouldReopen:
                res = EricMessageBox.yesNo(
                    None,
                    self.tr("Strip Changesets"),
                    self.tr(
                        """The project should be reread. Do this now?"""),
                    yesDefault=True)
                if res:
                    ericApp().getObject("Project").reopenProject()
                    return
            
        self.on_refreshButton_clicked()
    
    @pyqtSlot()
    def __mergeActTriggered(self):
        """
        Private slot to merge the working directory with the selected
        changeset.
        """
        itm = self.logTree.selectedItems()[0]
        if not itm.data(0, self.__incomingRole):
            rev = "rev({0})".format(
                itm.text(self.RevisionColumn).strip().split(":", 1)[0])
            self.vcs.vcsMerge("", rev=rev)
    
    @pyqtSlot()
    def __bundleActTriggered(self):
        """
        Private slot to create a changegroup file.
        """
        if self.initialCommandMode in ("log", "full_log"):
            selectedItems = [itm for itm in self.logTree.selectedItems()
                             if not itm.data(0, self.__incomingRole)]
            if len(selectedItems) == 0:
                # all revisions of the local repository will be bundled
                bundleData = {
                    "revs": [],
                    "base": "",
                    "all": True,
                }
            elif len(selectedItems) == 1:
                # the selected changeset is the base
                rev = selectedItems[0].text(self.RevisionColumn).split(
                    ":", 1)[0].strip()
                bundleData = {
                    "revs": [],
                    "base": rev,
                    "all": False,
                }
            else:
                # lowest revision is the base, others will be bundled
                revs = []
                for itm in selectedItems:
                    rev = itm.text(self.RevisionColumn).split(":", 1)[0]
                    with contextlib.suppress(ValueError):
                        revs.append(int(rev))
                baseRev = min(revs)
                while baseRev in revs:
                    revs.remove(baseRev)
                
                bundleData = {
                    "revs": [str(rev) for rev in revs],
                    "base": str(baseRev),
                    "all": False,
                }
        elif self.initialCommandMode == "outgoing":
            selectedItems = self.logTree.selectedItems()
            if len(selectedItems) > 0:
                revs = []
                for itm in selectedItems:
                    rev = itm.text(self.RevisionColumn).split(":", 1)[0]
                    revs.append(rev.strip())
                
                bundleData = {
                    "revs": revs,
                    "base": "",
                    "all": False,
                }
        
        self.vcs.hgBundle(bundleData=bundleData)
    
    @pyqtSlot()
    def __unbundleActTriggered(self):
        """
        Private slot to apply the currently previewed bundle file.
        """
        if self.initialCommandMode == "incoming" and bool(self.__bundle):
            shouldReopen = self.vcs.hgUnbundle(files=[self.__bundle])
            if shouldReopen:
                res = EricMessageBox.yesNo(
                    None,
                    self.tr("Apply Changegroup"),
                    self.tr("""The project should be reread. Do this now?"""),
                    yesDefault=True)
                if res:
                    ericApp().getObject("Project").reopenProject()
                    return
            
            self.vcs.vcsLogBrowser()
            self.close()
    
    @pyqtSlot()
    def __gpgSignActTriggered(self):
        """
        Private slot to sign the selected revisions.
        """
        revs = []
        for itm in [item for item in self.logTree.selectedItems()
                    if not item.data(0, self.__incomingRole)]:
            rev = itm.text(self.RevisionColumn).split(":", 1)[0].strip()
            if rev:
                revs.append(rev)
        
        if revs:
            self.vcs.getExtensionObject("gpg").hgGpgSign(revisions=revs)
    
    @pyqtSlot()
    def __gpgVerifyActTriggered(self):
        """
        Private slot to verify the signatures of a selected revisions.
        """
        itm = self.logTree.selectedItems()[0]
        if not itm.data(0, self.__incomingRole):
            rev = itm.text(self.RevisionColumn).split(":", 1)[0].strip()
            if rev:
                self.vcs.getExtensionObject("gpg").hgGpgVerifySignatures(
                    rev=rev)
    
    def __selectAllActTriggered(self, select=True):
        """
        Private method to select or unselect all log entries.
        
        @param select flag indicating to select all entries
        @type bool
        """
        blocked = self.logTree.blockSignals(True)
        for row in range(self.logTree.topLevelItemCount()):
            self.logTree.topLevelItem(row).setSelected(select)
        self.logTree.blockSignals(blocked)
        self.on_logTree_itemSelectionChanged()
    
    def __actionMode(self):
        """
        Private method to get the selected action mode.
        
        @return selected action mode (string, one of filter or find)
        """
        return self.modeComboBox.itemData(
            self.modeComboBox.currentIndex())
    
    @pyqtSlot(int)
    def on_modeComboBox_currentIndexChanged(self, index):
        """
        Private slot to react on mode changes.
        
        @param index index of the selected entry (integer)
        """
        mode = self.modeComboBox.itemData(index)
        findMode = mode == "find"
        filterMode = mode == "filter"
        
        self.fromDate.setEnabled(filterMode)
        self.toDate.setEnabled(filterMode)
        self.branchCombo.setEnabled(filterMode)
        self.findPrevButton.setVisible(findMode)
        self.findNextButton.setVisible(findMode)
        
        if findMode:
            for topIndex in range(self.logTree.topLevelItemCount()):
                self.logTree.topLevelItem(topIndex).setHidden(False)
            self.logTree.header().setSectionHidden(self.IconColumn, False)
        elif filterMode:
            self.__filterLogs()
    
    @pyqtSlot()
    def on_findPrevButton_clicked(self):
        """
        Private slot to find the previous item matching the entered criteria.
        """
        self.__findItem(True)
    
    @pyqtSlot()
    def on_findNextButton_clicked(self):
        """
        Private slot to find the next item matching the entered criteria.
        """
        self.__findItem(False)
    
    def __findItem(self, backwards=False, interactive=False):
        """
        Private slot to find an item matching the entered criteria.
        
        @param backwards flag indicating to search backwards (boolean)
        @param interactive flag indicating an interactive search (boolean)
        """
        self.__findBackwards = backwards
        
        fieldIndex, searchRx, indexIsRole = self.__prepareFieldSearch()
        currentIndex = self.logTree.indexOfTopLevelItem(
            self.logTree.currentItem())
        if backwards:
            if interactive:
                indexes = range(currentIndex, -1, -1)
            else:
                indexes = range(currentIndex - 1, -1, -1)
        else:
            if interactive:
                indexes = range(currentIndex, self.logTree.topLevelItemCount())
            else:
                indexes = range(currentIndex + 1,
                                self.logTree.topLevelItemCount())
        
        for index in indexes:
            topItem = self.logTree.topLevelItem(index)
            if indexIsRole:
                if fieldIndex == self.__changesRole:
                    changes = topItem.data(0, self.__changesRole)
                    txt = "\n".join(
                        [c["path"] for c in changes] +
                        [c["copyfrom"] for c in changes]
                    )
                else:
                    # Find based on complete message text
                    txt = "\n".join(topItem.data(0, self.__messageRole))
            else:
                txt = topItem.text(fieldIndex)
            if searchRx.search(txt) is not None:
                self.logTree.setCurrentItem(self.logTree.topLevelItem(index))
                break
        else:
            EricMessageBox.information(
                self,
                self.tr("Find Commit"),
                self.tr("""'{0}' was not found.""").format(self.rxEdit.text()))
    
    def __revisionClicked(self, url):
        """
        Private slot to handle the anchorClicked signal of the changeset
        details pane.
        
        @param url URL that was clicked
        @type QUrl
        """
        if url.scheme() in ("rev", "chg"):
            if url.scheme() == "rev":
                # a parent or child revision was clicked, show the item
                rev = url.path()
                searchStr = "{0:>7}:".format(rev)
                # format must be in sync with item generation format
                searchFlags = Qt.MatchFlag.MatchStartsWith
            elif url.scheme() == "chg":
                # a changeset hash was clicked, show the item
                changeset = url.path()
                searchStr = ":{0}".format(changeset[:12])  # max. 12 hash chars
                # format must be in sync with item generation format
                searchFlags = Qt.MatchFlag.MatchContains
            items = self.logTree.findItems(
                searchStr, searchFlags, self.RevisionColumn)
            if items:
                itm = items[0]
                if itm.isHidden():
                    itm.setHidden(False)
                self.logTree.setCurrentItem(itm)
            else:
                # load the next batch and try again
                if not self.cancelled and self.nextButton.isEnabled():
                    self.__addFinishCallback(
                        lambda: self.__revisionClicked(url))
                    self.on_nextButton_clicked()
    
    ###########################################################################
    ## Diff handling methods below
    ###########################################################################
    
    def __generateDiffs(self, parent=1):
        """
        Private slot to generate diff outputs for the selected item.
        
        @param parent number of parent to diff against
        @type int
        """
        self.diffEdit.setPlainText(self.tr("Generating differences ..."))
        self.diffLabel.setText(self.tr("Differences"))
        self.diffSelectLabel.clear()
        self.diffHighlighter.regenerateRules()
        
        selectedItems = self.logTree.selectedItems()
        if len(selectedItems) == 1:
            currentItem = selectedItems[0]
            rev2 = currentItem.text(self.RevisionColumn).split(":", 1)[0]
            parents = currentItem.data(0, self.__parentsRole)
            if len(parents) >= parent:
                self.diffLabel.setText(
                    self.tr("Differences to Parent {0}").format(parent))
                rev1 = parents[parent - 1]
                
                self.__diffGenerator.start(self.__filename, [rev1, rev2],
                                           self.__bundle)
            
            if len(parents) > 1:
                if parent == 1:
                    par1 = "&nbsp;1&nbsp;"
                else:
                    par1 = '<a href="diff:1">&nbsp;1&nbsp;</a>'
                if parent == 2:
                    par2 = "&nbsp;2&nbsp;"
                else:
                    par2 = '<a href="diff:2">&nbsp;2&nbsp;</a>'
                self.diffSelectLabel.setText(
                    self.tr('Diff to Parent {0}{1}').format(par1, par2))
        elif len(selectedItems) == 2:
            rev2 = int(selectedItems[0].text(
                self.RevisionColumn).split(":")[0])
            rev1 = int(selectedItems[1].text(
                self.RevisionColumn).split(":")[0])
            
            self.__diffGenerator.start(self.__filename,
                                       [min(rev1, rev2), max(rev1, rev2)],
                                       self.__bundle)
        else:
            self.diffEdit.clear()
    
    def __generatorFinished(self):
        """
        Private slot connected to the finished signal of the diff generator.
        """
        diff, errors, fileSeparators = self.__diffGenerator.getResult()
        
        if diff:
            self.diffEdit.setPlainText("".join(diff))
        elif errors:
            self.diffEdit.setPlainText("".join(errors))
        else:
            self.diffEdit.setPlainText(self.tr('There is no difference.'))
        
        self.saveLabel.setVisible(bool(diff))
        
        if self.__diffUpdatesFiles:
            for oldFileName, newFileName, lineNumber in fileSeparators:
                if oldFileName == newFileName:
                    fileName = oldFileName
                elif oldFileName == "__NULL__":
                    fileName = newFileName
                else:
                    fileName = oldFileName
                item = QTreeWidgetItem(self.filesTree, ["", fileName, ""])
                item.setData(0, self.__diffFileLineRole, lineNumber)
            self.__resizeColumnsFiles()
            self.__resortFiles()
        else:
            for oldFileName, newFileName, lineNumber in fileSeparators:
                for fileName in (oldFileName, newFileName):
                    if fileName != "__NULL__":
                        items = self.filesTree.findItems(
                            fileName, Qt.MatchFlag.MatchExactly, 1)
                        for item in items:
                            item.setData(0, self.__diffFileLineRole,
                                         lineNumber)
        
        tc = self.diffEdit.textCursor()
        tc.movePosition(QTextCursor.MoveOperation.Start)
        self.diffEdit.setTextCursor(tc)
        self.diffEdit.ensureCursorVisible()
    
    @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem)
    def on_filesTree_currentItemChanged(self, current, previous):
        """
        Private slot called, when the current item of the files tree changes.
        
        @param current reference to the new current item (QTreeWidgetItem)
        @param previous reference to the old current item (QTreeWidgetItem)
        """
        if current:
            para = current.data(0, self.__diffFileLineRole)
            if para is not None:
                if para == 0:
                    tc = self.diffEdit.textCursor()
                    tc.movePosition(QTextCursor.MoveOperation.Start)
                    self.diffEdit.setTextCursor(tc)
                    self.diffEdit.ensureCursorVisible()
                elif para == -1:
                    tc = self.diffEdit.textCursor()
                    tc.movePosition(QTextCursor.MoveOperation.End)
                    self.diffEdit.setTextCursor(tc)
                    self.diffEdit.ensureCursorVisible()
                else:
                    # step 1: move cursor to end
                    tc = self.diffEdit.textCursor()
                    tc.movePosition(QTextCursor.MoveOperation.End)
                    self.diffEdit.setTextCursor(tc)
                    self.diffEdit.ensureCursorVisible()
                    
                    # step 2: move cursor to desired line
                    tc = self.diffEdit.textCursor()
                    delta = tc.blockNumber() - para
                    tc.movePosition(QTextCursor.MoveOperation.PreviousBlock,
                                    QTextCursor.MoveMode.MoveAnchor, delta)
                    self.diffEdit.setTextCursor(tc)
                    self.diffEdit.ensureCursorVisible()
    
    @pyqtSlot(str)
    def on_diffSelectLabel_linkActivated(self, link):
        """
        Private slot to handle the selection of a diff target.
        
        @param link activated link
        @type str
        """
        if ":" in link:
            scheme, parent = link.split(":", 1)
            if scheme == "diff":
                with contextlib.suppress(ValueError):
                    parent = int(parent)
                    self.__generateDiffs(parent)
    
    @pyqtSlot(str)
    def on_saveLabel_linkActivated(self, link):
        """
        Private slot to handle the selection of the save link.
        
        @param link activated link
        @type str
        """
        if ":" not in link:
            return
        
        scheme, rest = link.split(":", 1)
        if scheme != "save" or rest != "me":
            return
        
        if self.projectMode:
            if self.__filename is None:
                fname = "{0}.diff".format(os.path.splitext(
                    ericApp().getObject("Project").getProjectFile())[0])
            else:
                fname = self.vcs.splitPath(self.__filename)[0]
                fname += "/{0}.diff".format(os.path.split(fname)[-1])
        else:
            dname, fname = self.vcs.splitPath(self.__filename)
            if fname != '.':
                fname = "{0}.diff".format(self.__filename)
            else:
                fname = dname
        
        fname, selectedFilter = EricFileDialog.getSaveFileNameAndFilter(
            self,
            self.tr("Save Diff"),
            fname,
            self.tr("Patch Files (*.diff)"),
            None,
            EricFileDialog.DontConfirmOverwrite)
        
        if not fname:
            return  # user aborted
        
        fpath = pathlib.Path(fname)
        if not fpath.suffix:
            ex = selectedFilter.split("(*")[1].split(")")[0]
            if ex:
                fpath = fpath.with_suffix(ex)
        if fpath.exists():
            res = EricMessageBox.yesNo(
                self,
                self.tr("Save Diff"),
                self.tr("<p>The patch file <b>{0}</b> already exists."
                        " Overwrite it?</p>").format(fpath),
                icon=EricMessageBox.Warning)
            if not res:
                return
        
        eol = ericApp().getObject("Project").getEolString()
        try:
            with fpath.open("w", encoding="utf-8", newline="") as f:
                f.write(eol.join(self.diffEdit.toPlainText().splitlines()))
        except OSError as why:
            EricMessageBox.critical(
                self, self.tr('Save Diff'),
                self.tr(
                    '<p>The patch file <b>{0}</b> could not be saved.'
                    '<br>Reason: {1}</p>')
                .format(fpath, str(why)))
    
    @pyqtSlot(str)
    def on_sbsSelectLabel_linkActivated(self, link):
        """
        Private slot to handle selection of a side-by-side link.
        
        @param link text of the selected link
        @type str
        """
        if ":" in link and self.__filename is not None:
            scheme, path = link.split(":", 1)
            if scheme == "sbsdiff" and "_" in path:
                rev1, rev2 = path.split("_", 1)
                self.vcs.vcsSbsDiff(self.__filename, revisions=(rev1, rev2))

eric ide

mercurial