Wed, 30 Dec 2020 11:00:05 +0100
Updated copyright for 2021.
# -*- coding: utf-8 -*- # Copyright (c) 2010 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing a dialog to browse the log history. """ import os import re import collections from PyQt5.QtCore import pyqtSlot, Qt, QDate, QSize, QPoint, QFileInfo from PyQt5.QtGui import ( QColor, QPixmap, QPainter, QPen, QBrush, QIcon, QTextCursor, QPalette ) from PyQt5.QtWidgets import ( QWidget, QDialogButtonBox, QHeaderView, QTreeWidgetItem, QApplication, QLineEdit, QMenu, QInputDialog ) from E5Gui.E5Application import e5App from E5Gui import E5MessageBox, E5FileDialog from E5Gui.E5OverrideCursor import E5OverrideCursor 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"/|\\") 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(HgLogBrowserDialog, self).__init__(parent) self.setupUi(self) windowFlags = self.windowFlags() windowFlags |= Qt.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.Close).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) self.filesTree.headerItem().setText(self.filesTree.columnCount(), "") self.filesTree.header().setSortIndicator(0, Qt.AscendingOrder) self.upButton.setIcon(UI.PixmapCache.getIcon("1uparrow")) self.downButton.setIcon(UI.PixmapCache.getIcon("1downarrow")) self.refreshButton = self.buttonBox.addButton( self.tr("&Refresh"), QDialogButtonBox.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.UserRole self.__changesRole = Qt.UserRole + 1 self.__edgesRole = Qt.UserRole + 2 self.__parentsRole = Qt.UserRole + 3 self.__latestTagRole = Qt.UserRole + 4 self.__incomingRole = Qt.UserRole + 5 # roles used in the file tree self.__diffFileLineRole = Qt.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 = e5App().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(HgLogBrowserDialog, self).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.ResizeToContents) self.logTree.header().setStretchLastSection(True) def __resizeColumnsFiles(self): """ Private method to resize the changed files tree columns. """ self.filesTree.header().resizeSections(QHeaderView.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, isDraft): """ 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 isDraft flag indicating an entry of phase 'draft' @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.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.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 isDraft: # 'draft' phase: draw an up arrow like outgoing 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: E5MessageBox.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: E5MessageBox.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: E5MessageBox.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: E5MessageBox.critical( self, self.tr("Mercurial Error"), errMsg) res = ("", "") if output: for line in output.splitlines(): if line.strip(): try: name, rev = line.strip().rsplit(None, 1) if name == tag: res = tuple(rev.split(":", 1)) break except ValueError: # ignore silently pass return res def __generateLogItem(self, author, date, message, revision, changedPaths, parents, branches, tags, phase, bookmarks, latestTag): """ Private method to generate a log tree entry. @param author author info (string) @param date date info (string) @param message text of the log message (list of strings) @param revision revision info (string) @param changedPaths list of dictionary objects containing info about the changed files/directories @param parents list of parent revisions (list of integers) @param branches list of branches (list of strings) @param tags list of tags (string) @param phase phase of the entry (string) @param bookmarks list of bookmarks (string) @param latestTag the latest tag(s) reachable from the changeset (list of strings) @return reference to the generated item (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(":") if rev in self.__closedBranchesRevs: closedStr = self.ClosedIndicator else: closedStr = "" if phase in self.phases: phaseStr = self.phases[phase] else: phaseStr = 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") if self.logTree.topLevelItemCount() > 1: topedges = self.logTree.topLevelItem( self.logTree.indexOfTopLevelItem(itm) - 1 ).data(0, self.__edgesRole) else: topedges = None icon = self.__generateIcon(column, color, edges, topedges, QColor(self.__branchColor(branches[0])), rev in self.__projectWorkingDirParents, rev in self.__closedBranchesRevs, phase == "draft") 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.Close).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) QApplication.processEvents() with E5OverrideCursor(): 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.fname == "." 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 = e5App().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, fn, bundle=None, isFile=False, noEntries=0): """ Public slot to start the hg log command. @param fn filename to show the log for (string) @param bundle name of a bundle file (string) @param isFile flag indicating log for a file is to be shown (boolean) @param noEntries number of entries to get (0 = default) (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 = fn self.dname, self.fname = self.vcs.splitPath(fn) # find the root of the repo self.repodir = self.dname while not os.path.isdir(os.path.join(self.repodir, self.vcs.adminDir)): self.repodir = os.path.dirname(self.repodir) if os.path.splitdrive(self.repodir)[1] == os.sep: return self.projectMode = (self.fname == "." and self.dname == self.repodir) self.stopCheckBox.setDisabled(self.projectMode or self.fname == ".") 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.Close).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) self.buttonBox.button(QDialogButtonBox.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 = {} 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"]) dt = QDate.fromString(log["date"], Qt.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: if not self.__finishCallbacks: # we are really done if self.__selectedRevisions: foundItems = self.logTree.findItems( self.__selectedRevisions[0], Qt.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.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.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.Close): self.close() elif button == self.buttonBox.button(QDialogButtonBox.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}"> {2} </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 = "" messageStr = "<br />\n".join([ Utilities.html_encode(line.strip()) for line in itm.data(0, self.__messageRole) ]) 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(str) def on_branchCombo_activated(self, txt): """ Private slot called, when a new branch is selected. @param txt text of the selected branch (string) """ if self.__actionMode() == "filter": self.__filterLogs() @pyqtSlot(str) def on_fieldCombo_activated(self, txt): """ Private slot called, when a new filter field is selected. @param txt text of the selected field (string) """ 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.Close).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True) self.buttonBox.button(QDialogButtonBox.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(self.repodir, 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(self.repodir, revs) if shouldReopen: res = E5MessageBox.yesNo( None, self.tr("Copy Changesets"), self.tr( """The project should be reread. Do this now?"""), yesDefault=True) if res: e5App().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(self.repodir, 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( self.repodir, 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(self.repodir, revision=rev) if shouldReopen: res = E5MessageBox.yesNo( None, self.tr("Switch"), self.tr( """The project should be reread. Do this now?"""), yesDefault=True) if res: e5App().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.Normal) if ok and bool(bookmark): self.vcs.hgBookmarkDefine( self.repodir, 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(self.repodir) 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( self.repodir, 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( self.repodir, 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(self.repodir, revisions=revs) refresh = True if self.initialCommandMode == "incoming": addNext = True if shouldReopen: res = E5MessageBox.yesNo( None, self.tr("Pull Changes"), self.tr( """The project should be reread. Do this now?"""), yesDefault=True) if res: e5App().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(self.repodir, 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.repodir) 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( self.repodir, rev=rev) if shouldReopen: res = E5MessageBox.yesNo( None, self.tr("Strip Changesets"), self.tr( """The project should be reread. Do this now?"""), yesDefault=True) if res: e5App().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(self.repodir, 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] try: revs.append(int(rev)) except ValueError: # ignore silently pass 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(self.repodir, 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(self.repodir, files=[self.__bundle]) if shouldReopen: res = E5MessageBox.yesNo( None, self.tr("Apply Changegroup"), self.tr("""The project should be reread. Do this now?"""), yesDefault=True) if res: e5App().getObject("Project").reopenProject() return self.vcs.vcsLogBrowser(self.repodir) 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( self.repodir, 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( self.repodir, 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: E5MessageBox.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() == "rev": # a parent or child revision was clicked, show the respective item rev = url.path() searchStr = "{0:>7}:".format(rev) # format must be in sync with item generation format items = self.logTree.findItems(searchStr, Qt.MatchStartsWith, 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 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 = " 1 " else: par1 = '<a href="diff:1"> 1 </a>' if parent == 2: par2 = " 2 " else: par2 = '<a href="diff:2"> 2 </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.MatchExactly, 1) for item in items: item.setData(0, self.__diffFileLineRole, lineNumber) tc = self.diffEdit.textCursor() tc.movePosition(QTextCursor.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.Start) self.diffEdit.setTextCursor(tc) self.diffEdit.ensureCursorVisible() elif para == -1: tc = self.diffEdit.textCursor() tc.movePosition(QTextCursor.End) self.diffEdit.setTextCursor(tc) self.diffEdit.ensureCursorVisible() else: # step 1: move cursor to end tc = self.diffEdit.textCursor() tc.movePosition(QTextCursor.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.PreviousBlock, QTextCursor.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": try: parent = int(parent) self.__generateDiffs(parent) except ValueError: # ignore silently pass @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: 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 = E5FileDialog.getSaveFileNameAndFilter( self, self.tr("Save Diff"), fname, self.tr("Patch Files (*.diff)"), None, E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite)) if not fname: return # user aborted ext = QFileInfo(fname).suffix() if not ext: ex = selectedFilter.split("(*")[1].split(")")[0] if ex: fname += ex if QFileInfo(fname).exists(): res = E5MessageBox.yesNo( self, self.tr("Save Diff"), self.tr("<p>The patch file <b>{0}</b> already exists." " Overwrite it?</p>").format(fname), icon=E5MessageBox.Warning) if not res: return fname = Utilities.toNativeSeparators(fname) eol = e5App().getObject("Project").getEolString() try: with open(fname, "w", encoding="utf-8", newline="") as f: f.write(eol.join(self.diffEdit.toPlainText().splitlines())) except OSError as why: E5MessageBox.critical( self, self.tr('Save Diff'), self.tr( '<p>The patch file <b>{0}</b> could not be saved.' '<br>Reason: {1}</p>') .format(fname, 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: scheme, path = link.split(":", 1) if scheme == "sbsdiff" and "_" in path: rev1, rev2 = path.split("_", 1) self.vcs.hgSbsDiff(self.__filename, revisions=(rev1, rev2))