src/eric7/Plugins/VcsPlugins/vcsMercurial/HgLogBrowserDialog.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 9153
506e35e424d5
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2010 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a dialog to browse the log history.
8 """
9
10 import os
11 import re
12 import collections
13 import contextlib
14 import pathlib
15
16 from PyQt6.QtCore import pyqtSlot, Qt, QDate, QSize, QPoint
17 from PyQt6.QtGui import (
18 QColor, QPixmap, QPainter, QPen, QBrush, QIcon, QTextCursor, QPalette
19 )
20 from PyQt6.QtWidgets import (
21 QWidget, QDialogButtonBox, QHeaderView, QTreeWidgetItem, QApplication,
22 QLineEdit, QMenu, QInputDialog
23 )
24
25 from EricWidgets.EricApplication import ericApp
26 from EricWidgets import EricMessageBox, EricFileDialog
27 from EricGui.EricOverrideCursor import EricOverrideCursor
28
29 from .Ui_HgLogBrowserDialog import Ui_HgLogBrowserDialog
30
31 from .HgDiffHighlighter import HgDiffHighlighter
32 from .HgDiffGenerator import HgDiffGenerator
33
34 import UI.PixmapCache
35 import Preferences
36 import Utilities
37
38 COLORNAMES = ["blue", "darkgreen", "red", "green", "darkblue", "purple",
39 "cyan", "olive", "magenta", "darkred", "darkmagenta",
40 "darkcyan", "gray", "yellow"]
41 COLORS = [str(QColor(x).name()) for x in COLORNAMES]
42
43 LIGHTCOLORS = ["#aaaaff", "#7faa7f", "#ffaaaa", "#aaffaa", "#7f7faa",
44 "#ffaaff", "#aaffff", "#d5d579", "#ffaaff", "#d57979",
45 "#d579d5", "#79d5d5", "#d5d5d5", "#d5d500",
46 ]
47
48
49 class HgLogBrowserDialog(QWidget, Ui_HgLogBrowserDialog):
50 """
51 Class implementing a dialog to browse the log history.
52 """
53 IconColumn = 0
54 BranchColumn = 1
55 RevisionColumn = 2
56 PhaseColumn = 3
57 AuthorColumn = 4
58 DateColumn = 5
59 MessageColumn = 6
60 TagsColumn = 7
61 BookmarksColumn = 8
62
63 LargefilesCacheL = ".hglf/"
64 LargefilesCacheW = ".hglf\\"
65 PathSeparatorRe = re.compile(r"/|\\")
66
67 GraftedRe = re.compile(r"\(grafted from ([0-9a-fA-F]+)\)")
68 GraftedTemplate = '(grafted from <a href="chg:{0}">{0}</a>)'
69
70 ClosedIndicator = " \u2612"
71
72 def __init__(self, vcs, mode="", parent=None):
73 """
74 Constructor
75
76 @param vcs reference to the vcs object
77 @type Hg
78 @param mode mode of the dialog
79 @type str (one of log, full_log, incoming, outgoing)
80 @param parent parent widget
81 @type QWidget
82 """
83 super().__init__(parent)
84 self.setupUi(self)
85
86 windowFlags = self.windowFlags()
87 windowFlags |= Qt.WindowType.WindowContextHelpButtonHint
88 self.setWindowFlags(windowFlags)
89
90 self.mainSplitter.setSizes([300, 400])
91 self.mainSplitter.setStretchFactor(0, 1)
92 self.mainSplitter.setStretchFactor(1, 2)
93 self.diffSplitter.setStretchFactor(0, 1)
94 self.diffSplitter.setStretchFactor(1, 2)
95
96 if not mode:
97 if vcs.getPlugin().getPreferences("LogBrowserShowFullLog"):
98 mode = "full_log"
99 else:
100 mode = "log"
101
102 if mode == "log":
103 self.setWindowTitle(self.tr("Mercurial Log"))
104 elif mode == "incoming":
105 self.setWindowTitle(self.tr("Mercurial Log (Incoming)"))
106 elif mode == "outgoing":
107 self.setWindowTitle(self.tr("Mercurial Log (Outgoing)"))
108 elif mode == "full_log":
109 self.setWindowTitle(self.tr("Mercurial Full Log"))
110
111 self.buttonBox.button(
112 QDialogButtonBox.StandardButton.Close).setEnabled(False)
113 self.buttonBox.button(
114 QDialogButtonBox.StandardButton.Cancel).setDefault(True)
115
116 self.filesTree.headerItem().setText(self.filesTree.columnCount(), "")
117 self.filesTree.header().setSortIndicator(
118 0, Qt.SortOrder.AscendingOrder)
119
120 self.upButton.setIcon(UI.PixmapCache.getIcon("1uparrow"))
121 self.downButton.setIcon(UI.PixmapCache.getIcon("1downarrow"))
122
123 self.refreshButton = self.buttonBox.addButton(
124 self.tr("&Refresh"), QDialogButtonBox.ButtonRole.ActionRole)
125 self.refreshButton.setToolTip(
126 self.tr("Press to refresh the list of changesets"))
127 self.refreshButton.setEnabled(False)
128
129 self.findPrevButton.setIcon(UI.PixmapCache.getIcon("1leftarrow"))
130 self.findNextButton.setIcon(UI.PixmapCache.getIcon("1rightarrow"))
131 self.__findBackwards = False
132
133 self.modeComboBox.addItem(self.tr("Find"), "find")
134 self.modeComboBox.addItem(self.tr("Filter"), "filter")
135
136 self.fieldCombo.addItem(self.tr("Revision"), "revision")
137 self.fieldCombo.addItem(self.tr("Author"), "author")
138 self.fieldCombo.addItem(self.tr("Message"), "message")
139 self.fieldCombo.addItem(self.tr("File"), "file")
140 self.fieldCombo.addItem(self.tr("Phase"), "phase")
141
142 font = Preferences.getEditorOtherFonts("MonospacedFont")
143 self.diffEdit.document().setDefaultFont(font)
144
145 self.diffHighlighter = HgDiffHighlighter(self.diffEdit.document())
146 self.__diffGenerator = HgDiffGenerator(vcs, self)
147 self.__diffGenerator.finished.connect(self.__generatorFinished)
148
149 self.vcs = vcs
150 if mode in ("log", "incoming", "outgoing", "full_log"):
151 if mode == "full_log":
152 self.commandMode = "incoming"
153 else:
154 self.commandMode = mode
155 self.initialCommandMode = mode
156 else:
157 self.commandMode = "log"
158 self.initialCommandMode = "log"
159 self.__hgClient = vcs.getClient()
160
161 self.__detailsTemplate = self.tr(
162 "<table>"
163 "<tr><td><b>Revision</b></td><td>{0}</td></tr>"
164 "<tr><td><b>Date</b></td><td>{1}</td></tr>"
165 "<tr><td><b>Author</b></td><td>{2}</td></tr>"
166 "<tr><td><b>Branch</b></td><td>{3}</td></tr>"
167 "{4}"
168 "<tr><td><b>Message</b></td><td>{5}</td></tr>"
169 "</table>"
170 )
171 self.__parentsTemplate = self.tr(
172 "<tr><td><b>Parents</b></td><td>{0}</td></tr>"
173 )
174 self.__childrenTemplate = self.tr(
175 "<tr><td><b>Children</b></td><td>{0}</td></tr>"
176 )
177 self.__tagsTemplate = self.tr(
178 "<tr><td><b>Tags</b></td><td>{0}</td></tr>"
179 )
180 self.__latestTagTemplate = self.tr(
181 "<tr><td><b>Latest Tag</b></td><td>{0}</td></tr>"
182 )
183 self.__bookmarksTemplate = self.tr(
184 "<tr><td><b>Bookmarks</b></td><td>{0}</td></tr>"
185 )
186
187 self.__bundle = ""
188 self.__filename = ""
189 self.__isFile = False
190 self.__selectedRevisions = []
191 self.intercept = False
192
193 self.__initData()
194
195 self.__allBranchesFilter = self.tr("All")
196
197 self.fromDate.setDisplayFormat("yyyy-MM-dd")
198 self.toDate.setDisplayFormat("yyyy-MM-dd")
199 self.__resetUI()
200
201 # roles used in the log tree
202 self.__messageRole = Qt.ItemDataRole.UserRole
203 self.__changesRole = Qt.ItemDataRole.UserRole + 1
204 self.__edgesRole = Qt.ItemDataRole.UserRole + 2
205 self.__parentsRole = Qt.ItemDataRole.UserRole + 3
206 self.__latestTagRole = Qt.ItemDataRole.UserRole + 4
207 self.__incomingRole = Qt.ItemDataRole.UserRole + 5
208
209 # roles used in the file tree
210 self.__diffFileLineRole = Qt.ItemDataRole.UserRole
211
212 self.flags = {
213 'A': self.tr('Added'),
214 'D': self.tr('Deleted'),
215 'M': self.tr('Modified'),
216 }
217
218 self.phases = {
219 'draft': self.tr("Draft"),
220 'public': self.tr("Public"),
221 'secret': self.tr("Secret"),
222 }
223
224 self.__dotRadius = 8
225 self.__rowHeight = 20
226
227 self.logTree.setIconSize(
228 QSize(100 * self.__rowHeight, self.__rowHeight))
229 self.BookmarksColumn = self.logTree.columnCount()
230 self.logTree.headerItem().setText(
231 self.BookmarksColumn, self.tr("Bookmarks"))
232
233 self.__logTreeNormalFont = self.logTree.font()
234 self.__logTreeNormalFont.setBold(False)
235 self.__logTreeBoldFont = self.logTree.font()
236 self.__logTreeBoldFont.setBold(True)
237 self.__logTreeHasDarkBackground = ericApp().usesDarkPalette()
238
239 self.detailsEdit.anchorClicked.connect(self.__revisionClicked)
240
241 self.__initActionsMenu()
242
243 self.__finishCallbacks = []
244 if self.initialCommandMode == "full_log":
245 self.__addFinishCallback(self.on_nextButton_clicked)
246
247 def __addFinishCallback(self, callback):
248 """
249 Private method to add a method to be called once the process finished.
250
251 The callback methods are invoke in a FIFO style and are consumed. If
252 a callback method needs to be called again, it must be added again.
253
254 @param callback callback method
255 @type function
256 """
257 if callback not in self.__finishCallbacks:
258 self.__finishCallbacks.append(callback)
259
260 def __initActionsMenu(self):
261 """
262 Private method to initialize the actions menu.
263 """
264 self.__actionsMenu = QMenu()
265 self.__actionsMenu.setTearOffEnabled(True)
266 self.__actionsMenu.setToolTipsVisible(True)
267
268 self.__graftAct = self.__actionsMenu.addAction(
269 UI.PixmapCache.getIcon("vcsGraft"),
270 self.tr("Copy Changesets"), self.__graftActTriggered)
271 self.__graftAct.setToolTip(self.tr(
272 "Copy the selected changesets to the current branch"))
273
274 self.__mergeAct = self.__actionsMenu.addAction(
275 UI.PixmapCache.getIcon("vcsMerge"),
276 self.tr("Merge with Changeset"), self.__mergeActTriggered)
277 self.__mergeAct.setToolTip(self.tr(
278 "Merge the working directory with the selected changeset"))
279
280 self.__phaseAct = self.__actionsMenu.addAction(
281 self.tr("Change Phase"), self.__phaseActTriggered)
282 self.__phaseAct.setToolTip(self.tr(
283 "Change the phase of the selected revisions"))
284 self.__phaseAct.setWhatsThis(self.tr(
285 """<b>Change Phase</b>\n<p>This changes the phase of the"""
286 """ selected revisions. The selected revisions have to have"""
287 """ the same current phase.</p>"""))
288
289 self.__tagAct = self.__actionsMenu.addAction(
290 UI.PixmapCache.getIcon("vcsTag"), self.tr("Tag"),
291 self.__tagActTriggered)
292 self.__tagAct.setToolTip(self.tr("Tag the selected revision"))
293
294 self.__closeHeadsAct = self.__actionsMenu.addAction(
295 UI.PixmapCache.getIcon("closehead"), self.tr("Close Heads"),
296 self.__closeHeadsActTriggered)
297 self.__closeHeadsAct.setToolTip(self.tr("Close the selected heads"))
298
299 self.__switchAct = self.__actionsMenu.addAction(
300 UI.PixmapCache.getIcon("vcsSwitch"), self.tr("Switch"),
301 self.__switchActTriggered)
302 self.__switchAct.setToolTip(self.tr(
303 "Switch the working directory to the selected revision"))
304
305 self.__actionsMenu.addSeparator()
306
307 self.__bookmarkAct = self.__actionsMenu.addAction(
308 UI.PixmapCache.getIcon("addBookmark"),
309 self.tr("Define Bookmark..."), self.__bookmarkActTriggered)
310 self.__bookmarkAct.setToolTip(
311 self.tr("Bookmark the selected revision"))
312 self.__bookmarkMoveAct = self.__actionsMenu.addAction(
313 UI.PixmapCache.getIcon("moveBookmark"),
314 self.tr("Move Bookmark..."), self.__bookmarkMoveActTriggered)
315 self.__bookmarkMoveAct.setToolTip(
316 self.tr("Move bookmark to the selected revision"))
317
318 self.__actionsMenu.addSeparator()
319
320 self.__pullAct = self.__actionsMenu.addAction(
321 UI.PixmapCache.getIcon("vcsUpdate"), self.tr("Pull Changes"),
322 self.__pullActTriggered)
323 self.__pullAct.setToolTip(self.tr(
324 "Pull changes from a remote repository"))
325 self.__lfPullAct = self.__actionsMenu.addAction(
326 self.tr("Pull Large Files"), self.__lfPullActTriggered)
327 self.__lfPullAct.setToolTip(self.tr(
328 "Pull large files for selected revisions"))
329
330 self.__actionsMenu.addSeparator()
331
332 self.__pushAct = self.__actionsMenu.addAction(
333 UI.PixmapCache.getIcon("vcsCommit"),
334 self.tr("Push Selected Changes"), self.__pushActTriggered)
335 self.__pushAct.setToolTip(self.tr(
336 "Push changes of the selected changeset and its ancestors"
337 " to a remote repository"))
338 self.__pushAllAct = self.__actionsMenu.addAction(
339 UI.PixmapCache.getIcon("vcsCommit"),
340 self.tr("Push All Changes"), self.__pushAllActTriggered)
341 self.__pushAllAct.setToolTip(self.tr(
342 "Push all changes to a remote repository"))
343
344 self.__actionsMenu.addSeparator()
345
346 self.__bundleAct = self.__actionsMenu.addAction(
347 UI.PixmapCache.getIcon("vcsCreateChangegroup"),
348 self.tr("Create Changegroup"), self.__bundleActTriggered)
349 self.__bundleAct.setToolTip(self.tr(
350 "Create a changegroup file containing the selected changesets"))
351 self.__bundleAct.setWhatsThis(self.tr(
352 """<b>Create Changegroup</b>\n<p>This creates a changegroup"""
353 """ file containing the selected revisions. If no revisions"""
354 """ are selected, all changesets will be bundled. If one"""
355 """ revision is selected, it will be interpreted as the base"""
356 """ revision. Otherwise the lowest revision will be used as"""
357 """ the base revision and all other revision will be bundled."""
358 """ If the dialog is showing outgoing changesets, all"""
359 """ selected changesets will be bundled.</p>"""))
360 self.__unbundleAct = self.__actionsMenu.addAction(
361 UI.PixmapCache.getIcon("vcsApplyChangegroup"),
362 self.tr("Apply Changegroup"), self.__unbundleActTriggered)
363 self.__unbundleAct.setToolTip(self.tr(
364 "Apply the currently viewed changegroup file"))
365
366 self.__actionsMenu.addSeparator()
367
368 self.__gpgSignAct = self.__actionsMenu.addAction(
369 UI.PixmapCache.getIcon("changesetSign"),
370 self.tr("Sign Revisions"), self.__gpgSignActTriggered)
371 self.__gpgSignAct.setToolTip(self.tr(
372 "Add a signature for the selected revisions"))
373 self.__gpgVerifyAct = self.__actionsMenu.addAction(
374 UI.PixmapCache.getIcon("changesetSignVerify"),
375 self.tr("Verify Signatures"), self.__gpgVerifyActTriggered)
376 self.__gpgVerifyAct.setToolTip(self.tr(
377 "Verify all signatures there may be for the selected revision"))
378
379 self.__actionsMenu.addSeparator()
380
381 self.__stripAct = self.__actionsMenu.addAction(
382 UI.PixmapCache.getIcon("fileDelete"),
383 self.tr("Strip Changesets"), self.__stripActTriggered)
384 self.__stripAct.setToolTip(self.tr(
385 "Strip changesets from a repository"))
386
387 self.__actionsMenu.addSeparator()
388
389 self.__selectAllAct = self.__actionsMenu.addAction(
390 self.tr("Select All Entries"), self.__selectAllActTriggered)
391 self.__unselectAllAct = self.__actionsMenu.addAction(
392 self.tr("Deselect All Entries"),
393 lambda: self.__selectAllActTriggered(False))
394
395 self.actionsButton.setIcon(
396 UI.PixmapCache.getIcon("actionsToolButton"))
397 self.actionsButton.setMenu(self.__actionsMenu)
398
399 def __initData(self):
400 """
401 Private method to (re-)initialize some data.
402 """
403 self.__maxDate = QDate()
404 self.__minDate = QDate()
405 self.__filterLogsEnabled = True
406
407 self.buf = [] # buffer for stdout
408 self.diff = None
409 self.__started = False
410 self.__lastRev = 0
411 self.projectMode = False
412
413 # attributes to store log graph data
414 self.__revs = []
415 self.__revColors = {}
416 self.__revColor = 0
417
418 self.__branchColors = {}
419
420 self.__projectWorkingDirParents = []
421 self.__projectBranch = ""
422
423 self.__childrenInfo = collections.defaultdict(list)
424
425 def closeEvent(self, e):
426 """
427 Protected slot implementing a close event handler.
428
429 @param e close event (QCloseEvent)
430 """
431 if self.__hgClient.isExecuting():
432 self.__hgClient.cancel()
433
434 self.vcs.getPlugin().setPreferences(
435 "LogBrowserGeometry", self.saveGeometry())
436 self.vcs.getPlugin().setPreferences(
437 "LogBrowserSplitterStates", [
438 self.mainSplitter.saveState(),
439 self.detailsSplitter.saveState(),
440 self.diffSplitter.saveState(),
441 ]
442 )
443
444 e.accept()
445
446 def show(self):
447 """
448 Public slot to show the dialog.
449 """
450 self.__reloadGeometry()
451 self.__restoreSplitterStates()
452 self.__resetUI()
453
454 super().show()
455
456 def __reloadGeometry(self):
457 """
458 Private method to restore the geometry.
459 """
460 geom = self.vcs.getPlugin().getPreferences("LogBrowserGeometry")
461 if geom.isEmpty():
462 s = QSize(1000, 800)
463 self.resize(s)
464 else:
465 self.restoreGeometry(geom)
466
467 def __restoreSplitterStates(self):
468 """
469 Private method to restore the state of the various splitters.
470 """
471 states = self.vcs.getPlugin().getPreferences(
472 "LogBrowserSplitterStates")
473 if len(states) == 3:
474 # we have three splitters
475 self.mainSplitter.restoreState(states[0])
476 self.detailsSplitter.restoreState(states[1])
477 self.diffSplitter.restoreState(states[2])
478
479 def __resetUI(self):
480 """
481 Private method to reset the user interface.
482 """
483 self.branchCombo.clear()
484 self.fromDate.setDate(QDate.currentDate())
485 self.toDate.setDate(QDate.currentDate())
486 self.fieldCombo.setCurrentIndex(self.fieldCombo.findData("message"))
487 self.limitSpinBox.setValue(self.vcs.getPlugin().getPreferences(
488 "LogLimit"))
489 self.stopCheckBox.setChecked(self.vcs.getPlugin().getPreferences(
490 "StopLogOnCopy"))
491
492 if self.initialCommandMode in ("incoming", "outgoing"):
493 self.nextButton.setEnabled(False)
494 self.limitSpinBox.setEnabled(False)
495 else:
496 self.nextButton.setEnabled(True)
497 self.limitSpinBox.setEnabled(True)
498
499 self.logTree.clear()
500
501 if self.initialCommandMode == "full_log":
502 self.commandMode = "incoming"
503 else:
504 self.commandMode = self.initialCommandMode
505
506 def __resizeColumnsLog(self):
507 """
508 Private method to resize the log tree columns.
509 """
510 self.logTree.header().resizeSections(
511 QHeaderView.ResizeMode.ResizeToContents)
512 self.logTree.header().setStretchLastSection(True)
513
514 def __resizeColumnsFiles(self):
515 """
516 Private method to resize the changed files tree columns.
517 """
518 self.filesTree.header().resizeSections(
519 QHeaderView.ResizeMode.ResizeToContents)
520 self.filesTree.header().setStretchLastSection(True)
521
522 def __resortFiles(self):
523 """
524 Private method to resort the changed files tree.
525 """
526 sortColumn = self.filesTree.sortColumn()
527 self.filesTree.sortItems(
528 1, self.filesTree.header().sortIndicatorOrder())
529 self.filesTree.sortItems(
530 sortColumn, self.filesTree.header().sortIndicatorOrder())
531
532 def __getColor(self, n):
533 """
534 Private method to get the (rotating) name of the color given an index.
535
536 @param n color index
537 @type int
538 @return color name
539 @rtype str
540 """
541 if self.__logTreeHasDarkBackground:
542 return LIGHTCOLORS[n % len(LIGHTCOLORS)]
543 else:
544 return COLORS[n % len(COLORS)]
545
546 def __branchColor(self, branchName):
547 """
548 Private method to calculate a color for a given branch name.
549
550 @param branchName name of the branch (string)
551 @return name of the color to use (string)
552 """
553 if branchName not in self.__branchColors:
554 self.__branchColors[branchName] = self.__getColor(
555 len(self.__branchColors))
556 return self.__branchColors[branchName]
557
558 def __generateEdges(self, rev, parents):
559 """
560 Private method to generate edge info for the give data.
561
562 @param rev revision to calculate edge info for (integer)
563 @param parents list of parent revisions (list of integers)
564 @return tuple containing the column and color index for
565 the given node and a list of tuples indicating the edges
566 between the given node and its parents
567 (integer, integer, [(integer, integer, integer), ...])
568 """
569 if rev not in self.__revs:
570 # new head
571 self.__revs.append(rev)
572 self.__revColors[rev] = self.__revColor
573 self.__revColor += 1
574
575 col = self.__revs.index(rev)
576 color = self.__revColors.pop(rev)
577 nextRevs = self.__revs[:]
578
579 # add parents to next
580 addparents = [p for p in parents if p not in nextRevs]
581 nextRevs[col:col + 1] = addparents
582
583 # set colors for the parents
584 for i, p in enumerate(addparents):
585 if not i:
586 self.__revColors[p] = color
587 else:
588 self.__revColors[p] = self.__revColor
589 self.__revColor += 1
590
591 # add edges to the graph
592 edges = []
593 if parents[0] != -1:
594 for ecol, erev in enumerate(self.__revs):
595 if erev in nextRevs:
596 edges.append(
597 (ecol, nextRevs.index(erev), self.__revColors[erev]))
598 elif erev == rev:
599 for p in parents:
600 edges.append(
601 (ecol, nextRevs.index(p), self.__revColors[p]))
602
603 self.__revs = nextRevs
604 return col, color, edges
605
606 def __generateIcon(self, column, color, bottomedges, topedges, dotColor,
607 currentRev, closed, isPushableDraft):
608 """
609 Private method to generate an icon containing the revision tree for the
610 given data.
611
612 @param column column index of the revision
613 @type int
614 @param color color of the node
615 @type int
616 @param bottomedges list of edges for the bottom of the node
617 @type list of tuples of (int, int, int)
618 @param topedges list of edges for the top of the node
619 @type list of tuples of (int, int, int)
620 @param dotColor color to be used for the dot
621 @type QColor
622 @param currentRev flag indicating to draw the icon for the
623 current revision
624 @type bool
625 @param closed flag indicating to draw an icon for a closed
626 branch
627 @type bool
628 @param isPushableDraft flag indicating an entry of phase 'draft',
629 that can by pushed
630 @type bool
631 @return icon for the node
632 @rtype QIcon
633 """
634 def col2x(col, radius):
635 """
636 Local function to calculate a x-position for a column.
637
638 @param col column number (integer)
639 @param radius radius of the indicator circle (integer)
640 """
641 return int(1.2 * radius) * col + radius // 2 + 3
642
643 textColor = self.logTree.palette().color(QPalette.ColorRole.Text)
644
645 radius = self.__dotRadius
646 w = len(bottomedges) * radius + 20
647 h = self.__rowHeight
648
649 dot_x = col2x(column, radius) - radius // 2
650 dot_y = h // 2
651
652 pix = QPixmap(w, h)
653 pix.fill(QColor(0, 0, 0, 0)) # draw transparent background
654 painter = QPainter(pix)
655 painter.setRenderHint(QPainter.RenderHint.Antialiasing)
656
657 # draw the revision history lines
658 for y1, y2, lines in ((0, h, bottomedges),
659 (-h, 0, topedges)):
660 if lines:
661 for start, end, ecolor in lines:
662 lpen = QPen(QColor(self.__getColor(ecolor)))
663 lpen.setWidth(2)
664 painter.setPen(lpen)
665 x1 = col2x(start, radius)
666 x2 = col2x(end, radius)
667 painter.drawLine(x1, dot_y + y1, x2, dot_y + y2)
668
669 penradius = 1
670 pencolor = textColor
671
672 dot_y = (h // 2) - radius // 2
673
674 # draw an indicator for the revision
675 if currentRev:
676 # enlarge for the current revision
677 delta = 1
678 radius += 2 * delta
679 dot_y -= delta
680 dot_x -= delta
681 penradius = 3
682 painter.setBrush(dotColor)
683 pen = QPen(pencolor)
684 pen.setWidth(penradius)
685 painter.setPen(pen)
686 if closed:
687 painter.drawRect(dot_x - 2, dot_y + 1,
688 radius + 4, radius - 2)
689 elif self.commandMode in ("incoming", "outgoing"):
690 offset = radius // 2
691 if self.commandMode == "incoming":
692 # incoming: draw a down arrow
693 painter.drawConvexPolygon(
694 QPoint(dot_x, dot_y),
695 QPoint(dot_x + 2 * offset, dot_y),
696 QPoint(dot_x + offset, dot_y + 2 * offset)
697 )
698 else:
699 # outgoing: draw an up arrow
700 painter.drawConvexPolygon(
701 QPoint(dot_x + offset, dot_y),
702 QPoint(dot_x, dot_y + 2 * offset),
703 QPoint(dot_x + 2 * offset, dot_y + 2 * offset)
704 )
705 else:
706 if isPushableDraft:
707 # 'draft' phase: draw an up arrow like outgoing,
708 # if it can be pushed
709 offset = radius // 2
710 painter.drawConvexPolygon(
711 QPoint(dot_x + offset, dot_y),
712 QPoint(dot_x, dot_y + 2 * offset),
713 QPoint(dot_x + 2 * offset, dot_y + 2 * offset)
714 )
715 else:
716 painter.drawEllipse(dot_x, dot_y, radius, radius)
717 painter.end()
718 return QIcon(pix)
719
720 def __getParents(self, rev):
721 """
722 Private method to get the parents of the currently viewed
723 file/directory.
724
725 @param rev revision number to get parents for (string)
726 @return list of parent revisions (list of integers)
727 """
728 errMsg = ""
729 parents = [-1]
730
731 if int(rev) > 0:
732 args = self.vcs.initCommand("parents")
733 if self.commandMode == "incoming":
734 if self.__bundle:
735 args.append("--repository")
736 args.append(self.__bundle)
737 elif (
738 self.vcs.bundleFile and
739 os.path.exists(self.vcs.bundleFile)
740 ):
741 args.append("--repository")
742 args.append(self.vcs.bundleFile)
743 args.append("--template")
744 args.append("{rev}\n")
745 args.append("-r")
746 args.append(rev)
747 if not self.projectMode:
748 args.append(self.__filename)
749
750 output, errMsg = self.__hgClient.runcommand(args)
751
752 if output:
753 parents = [int(p) for p in output.strip().splitlines()]
754
755 return parents
756
757 def __identifyProject(self):
758 """
759 Private method to determine the revision of the project directory.
760 """
761 errMsg = ""
762
763 args = self.vcs.initCommand("identify")
764 args.append("-nb")
765
766 output, errMsg = self.__hgClient.runcommand(args)
767
768 if errMsg:
769 EricMessageBox.critical(
770 self,
771 self.tr("Mercurial Error"),
772 errMsg)
773
774 if output:
775 outputList = output.strip().split(None, 1)
776 if len(outputList) == 2:
777 outputRevs = outputList[0].strip()
778 if outputRevs.endswith("+"):
779 outputRevs = outputRevs[:-1]
780 self.__projectWorkingDirParents = outputRevs.split('+')
781 else:
782 self.__projectWorkingDirParents = [outputRevs]
783 self.__projectBranch = outputList[1].strip()
784
785 def __getClosedBranches(self):
786 """
787 Private method to get the list of closed branches.
788 """
789 self.__closedBranchesRevs = []
790 errMsg = ""
791
792 args = self.vcs.initCommand("branches")
793 args.append("--closed")
794
795 output, errMsg = self.__hgClient.runcommand(args)
796
797 if errMsg:
798 EricMessageBox.critical(
799 self,
800 self.tr("Mercurial Error"),
801 errMsg)
802
803 if output:
804 for line in output.splitlines():
805 if line.strip().endswith("(closed)"):
806 parts = line.split()
807 self.__closedBranchesRevs.append(
808 parts[-2].split(":", 1)[0])
809
810 def __getHeads(self):
811 """
812 Private method to get the list of all heads.
813 """
814 self.__headRevisions = []
815 errMsg = ""
816
817 args = self.vcs.initCommand("heads")
818 args.append("--closed")
819 args.append("--template")
820 args.append("{rev}\n")
821
822 output, errMsg = self.__hgClient.runcommand(args)
823
824 if errMsg:
825 EricMessageBox.critical(
826 self,
827 self.tr("Mercurial Error"),
828 errMsg)
829
830 if output:
831 for line in output.splitlines():
832 line = line.strip()
833 if line:
834 self.__headRevisions.append(line)
835
836 def __getRevisionOfTag(self, tag):
837 """
838 Private method to get the revision of a tag.
839
840 @param tag tag name
841 @type str
842 @return tuple containing the revision and changeset ID
843 @rtype tuple of (str, str)
844 """
845 errMsg = ""
846
847 args = self.vcs.initCommand("tags")
848
849 output, errMsg = self.__hgClient.runcommand(args)
850
851 if errMsg:
852 EricMessageBox.critical(
853 self,
854 self.tr("Mercurial Error"),
855 errMsg)
856
857 res = ("", "")
858 if output:
859 for line in output.splitlines():
860 if line.strip():
861 with contextlib.suppress(ValueError):
862 name, rev = line.strip().rsplit(None, 1)
863 if name == tag:
864 res = tuple(rev.split(":", 1))
865 break
866
867 return res
868
869 def __generateLogItem(self, author, date, message, revision, changedPaths,
870 parents, branches, tags, phase, bookmarks,
871 latestTag, canPush=False):
872 """
873 Private method to generate a log tree entry.
874
875 @param author author info
876 @type str
877 @param date date info
878 @type str
879 @param message text of the log message
880 @type list of str
881 @param revision revision info
882 @type str
883 @param changedPaths list of dictionary objects containing
884 info about the changed files/directories
885 @type dict
886 @param parents list of parent revisions
887 @type list of int
888 @param branches list of branches
889 @type list of str
890 @param tags list of tags
891 @type str
892 @param phase phase of the entry
893 @type str
894 @param bookmarks list of bookmarks
895 @type str
896 @param latestTag the latest tag(s) reachable from the changeset
897 @type list of str
898 @param canPush flag indicating that changesets can be pushed
899 @type bool
900 @return reference to the generated item
901 @rtype QTreeWidgetItem
902 """
903 logMessageColumnWidth = self.vcs.getPlugin().getPreferences(
904 "LogMessageColumnWidth")
905 msgtxt = ""
906 for line in message:
907 if ". " in line:
908 msgtxt += " " + line.strip().split(". ", 1)[0] + "."
909 break
910 else:
911 msgtxt += " " + line.strip()
912 if len(msgtxt) > logMessageColumnWidth:
913 msgtxt = "{0}...".format(msgtxt[:logMessageColumnWidth])
914
915 rev, node = revision.split(":")
916 closedStr = (self.ClosedIndicator
917 if rev in self.__closedBranchesRevs else "")
918 phaseStr = self.phases.get(phase, phase)
919 columnLabels = [
920 "",
921 branches[0] + closedStr,
922 "{0:>7}:{1}".format(rev, node),
923 phaseStr,
924 author,
925 date,
926 msgtxt,
927 ", ".join(tags),
928 ]
929 if bookmarks is not None:
930 columnLabels.append(", ".join(bookmarks))
931 itm = QTreeWidgetItem(self.logTree, columnLabels)
932
933 itm.setForeground(self.BranchColumn,
934 QBrush(QColor(self.__branchColor(branches[0]))))
935
936 if not self.projectMode:
937 parents = self.__getParents(rev)
938 if not parents:
939 parents = [int(rev) - 1]
940 column, color, edges = self.__generateEdges(int(rev), parents)
941
942 itm.setData(0, self.__messageRole, message)
943 itm.setData(0, self.__changesRole, changedPaths)
944 itm.setData(0, self.__edgesRole, edges)
945 itm.setData(0, self.__latestTagRole, latestTag)
946 if parents == [-1]:
947 itm.setData(0, self.__parentsRole, [])
948 else:
949 itm.setData(0, self.__parentsRole, parents)
950 for parent in parents:
951 self.__childrenInfo[parent].append(int(rev))
952 itm.setData(0, self.__incomingRole, self.commandMode == "incoming")
953
954 topedges = (
955 self.logTree.topLevelItem(
956 self.logTree.indexOfTopLevelItem(itm) - 1
957 ).data(0, self.__edgesRole)
958 if self.logTree.topLevelItemCount() > 1 else
959 None
960 )
961
962 icon = self.__generateIcon(column, color, edges, topedges,
963 QColor(self.__branchColor(branches[0])),
964 rev in self.__projectWorkingDirParents,
965 rev in self.__closedBranchesRevs,
966 phase == "draft" and canPush)
967 itm.setIcon(0, icon)
968
969 try:
970 self.__lastRev = int(revision.split(":")[0])
971 except ValueError:
972 self.__lastRev = 0
973
974 return itm
975
976 def __getLogEntries(self, startRev=None, noEntries=0):
977 """
978 Private method to retrieve log entries from the repository.
979
980 @param startRev revision number to start from (integer, string)
981 @param noEntries number of entries to get (0 = default) (int)
982 """
983 self.buttonBox.button(
984 QDialogButtonBox.StandardButton.Close).setEnabled(False)
985 self.buttonBox.button(
986 QDialogButtonBox.StandardButton.Cancel).setEnabled(True)
987 self.buttonBox.button(
988 QDialogButtonBox.StandardButton.Cancel).setDefault(True)
989 QApplication.processEvents()
990
991 with EricOverrideCursor():
992 self.buf = []
993 self.cancelled = False
994 self.errors.clear()
995 self.intercept = False
996
997 if noEntries == 0:
998 noEntries = self.limitSpinBox.value()
999
1000 preargs = []
1001 args = self.vcs.initCommand(self.commandMode)
1002 args.append('--verbose')
1003 if self.commandMode not in ("incoming", "outgoing"):
1004 args.append('--limit')
1005 args.append(str(noEntries))
1006 if self.commandMode in ("incoming", "outgoing"):
1007 args.append("--newest-first")
1008 if self.vcs.hasSubrepositories():
1009 args.append("--subrepos")
1010 if startRev is not None:
1011 args.append('--rev')
1012 args.append('{0}:0'.format(startRev))
1013 if (
1014 not self.projectMode and
1015 not self.stopCheckBox.isChecked()
1016 ):
1017 args.append('--follow')
1018 if self.commandMode == "log":
1019 args.append('--copies')
1020 args.append('--template')
1021 args.append(os.path.join(os.path.dirname(__file__),
1022 "templates",
1023 "logBrowserBookmarkPhase.tmpl"))
1024 if self.commandMode == "incoming":
1025 if self.__bundle:
1026 args.append(self.__bundle)
1027 elif not self.vcs.hasSubrepositories():
1028 project = ericApp().getObject("Project")
1029 self.vcs.bundleFile = os.path.join(
1030 project.getProjectManagementDir(), "hg-bundle.hg")
1031 if os.path.exists(self.vcs.bundleFile):
1032 os.remove(self.vcs.bundleFile)
1033 preargs = args[:]
1034 preargs.append("--quiet")
1035 preargs.append('--bundle')
1036 preargs.append(self.vcs.bundleFile)
1037 args.append(self.vcs.bundleFile)
1038 if not self.projectMode:
1039 args.append(self.__filename)
1040
1041 if preargs:
1042 out, err = self.__hgClient.runcommand(preargs)
1043 else:
1044 err = ""
1045 if err:
1046 if (
1047 self.commandMode == "incoming" and
1048 self.initialCommandMode == "full_log"
1049 ):
1050 # ignore the error
1051 self.commandMode = "log"
1052 else:
1053 self.__showError(err)
1054 elif (
1055 self.commandMode != "incoming" or
1056 (self.vcs.bundleFile and
1057 os.path.exists(self.vcs.bundleFile)) or
1058 self.__bundle
1059 ):
1060 out, err = self.__hgClient.runcommand(args)
1061 self.buf = out.splitlines(True)
1062 if err:
1063 self.__showError(err)
1064 self.__processBuffer()
1065 elif (
1066 self.commandMode == "incoming" and
1067 self.initialCommandMode == "full_log"
1068 ):
1069 # no incoming changesets, just switch to log mode
1070 self.commandMode = "log"
1071 self.__finish()
1072
1073 def start(self, name=None, bundle=None, isFile=False, noEntries=0):
1074 """
1075 Public slot to start the hg log command.
1076
1077 @param name file/directory name to show the log for
1078 @type str
1079 @param bundle name of a bundle file
1080 @type str
1081 @param isFile flag indicating log for a file is to be shown
1082 @type bool
1083 @param noEntries number of entries to get (0 = default)
1084 @type int
1085 """
1086 self.__bundle = bundle
1087 self.__isFile = isFile
1088
1089 if self.initialCommandMode == "full_log":
1090 if isFile:
1091 self.commandMode = "log"
1092 self.__finishCallbacks = []
1093 else:
1094 self.commandMode = "incoming"
1095 self.__addFinishCallback(self.on_nextButton_clicked)
1096
1097 self.sbsSelectLabel.clear()
1098
1099 self.errorGroup.hide()
1100 self.errors.clear()
1101 QApplication.processEvents()
1102
1103 self.__initData()
1104
1105 self.__filename = name
1106
1107 self.projectMode = name is None
1108 self.stopCheckBox.setDisabled(self.projectMode)
1109 self.activateWindow()
1110 self.raise_()
1111
1112 self.logTree.clear()
1113 self.__started = True
1114 self.__identifyProject()
1115 self.__getClosedBranches()
1116 self.__getHeads()
1117 self.__getLogEntries(noEntries=noEntries)
1118
1119 def __finish(self):
1120 """
1121 Private slot called when the process finished or the user pressed
1122 the button.
1123 """
1124 self.buttonBox.button(
1125 QDialogButtonBox.StandardButton.Close).setEnabled(True)
1126 self.buttonBox.button(
1127 QDialogButtonBox.StandardButton.Cancel).setEnabled(False)
1128 self.buttonBox.button(
1129 QDialogButtonBox.StandardButton.Close).setDefault(True)
1130
1131 self.refreshButton.setEnabled(True)
1132
1133 while self.__finishCallbacks:
1134 self.__finishCallbacks.pop(0)()
1135
1136 def __modifyForLargeFiles(self, filename):
1137 """
1138 Private method to convert the displayed file name for a large file.
1139
1140 @param filename file name to be processed (string)
1141 @return processed file name (string)
1142 """
1143 if filename.startswith((self.LargefilesCacheL, self.LargefilesCacheW)):
1144 return self.tr("{0} (large file)").format(
1145 self.PathSeparatorRe.split(filename, 1)[1])
1146 else:
1147 return filename
1148
1149 def __processBuffer(self):
1150 """
1151 Private method to process the buffered output of the hg log command.
1152 """
1153 noEntries = 0
1154 log = {"message": [], "bookmarks": None, "phase": ""}
1155 changedPaths = []
1156 initialText = True
1157 fileCopies = {}
1158 canPush = self.vcs.canPush()
1159 for s in self.buf:
1160 if s != "@@@\n":
1161 try:
1162 key, value = s.split("|", 1)
1163 except ValueError:
1164 key = ""
1165 value = s
1166 if key == "change":
1167 initialText = False
1168 log["revision"] = value.strip()
1169 elif key == "user":
1170 log["author"] = value.strip()
1171 elif key == "parents":
1172 log["parents"] = [
1173 int(x.split(":", 1)[0])
1174 for x in value.strip().split()
1175 ]
1176 elif key == "date":
1177 log["date"] = " ".join(value.strip().split()[:2])
1178 elif key == "description":
1179 log["message"].append(value.strip())
1180 elif key == "file_adds":
1181 if value.strip():
1182 for f in value.strip().split(", "):
1183 if f in fileCopies:
1184 changedPaths.append({
1185 "action": "A",
1186 "path": self.__modifyForLargeFiles(f),
1187 "copyfrom": self.__modifyForLargeFiles(
1188 fileCopies[f]),
1189 })
1190 else:
1191 changedPaths.append({
1192 "action": "A",
1193 "path": self.__modifyForLargeFiles(f),
1194 "copyfrom": "",
1195 })
1196 elif key == "files_mods":
1197 if value.strip():
1198 for f in value.strip().split(", "):
1199 changedPaths.append({
1200 "action": "M",
1201 "path": self.__modifyForLargeFiles(f),
1202 "copyfrom": "",
1203 })
1204 elif key == "file_dels":
1205 if value.strip():
1206 for f in value.strip().split(", "):
1207 changedPaths.append({
1208 "action": "D",
1209 "path": self.__modifyForLargeFiles(f),
1210 "copyfrom": "",
1211 })
1212 elif key == "file_copies":
1213 if value.strip():
1214 for entry in value.strip().split(", "):
1215 newName, oldName = entry[:-1].split(" (")
1216 fileCopies[newName] = oldName
1217 elif key == "branches":
1218 if value.strip():
1219 log["branches"] = value.strip().split(", ")
1220 else:
1221 log["branches"] = ["default"]
1222 elif key == "tags":
1223 log["tags"] = value.strip().split(", ")
1224 elif key == "bookmarks":
1225 log["bookmarks"] = value.strip().split(", ")
1226 elif key == "phase":
1227 log["phase"] = value.strip()
1228 elif key == "latesttag":
1229 tag = value.strip()
1230 if tag == "null":
1231 log["latesttag"] = []
1232 elif ":" in tag:
1233 log["latesttag"] = [
1234 t.strip() for t in tag.split(":") if t.strip()]
1235 else:
1236 log["latesttag"] = [tag]
1237 else:
1238 if initialText:
1239 continue
1240 if value.strip():
1241 log["message"].append(value.strip())
1242 else:
1243 if len(log) > 1:
1244 self.__generateLogItem(
1245 log["author"], log["date"],
1246 log["message"], log["revision"], changedPaths,
1247 log["parents"], log["branches"], log["tags"],
1248 log["phase"], log["bookmarks"], log["latesttag"],
1249 canPush=canPush)
1250 dt = QDate.fromString(log["date"], Qt.DateFormat.ISODate)
1251 if (
1252 not self.__maxDate.isValid() and
1253 not self.__minDate.isValid()
1254 ):
1255 self.__maxDate = dt
1256 self.__minDate = dt
1257 else:
1258 if self.__maxDate < dt:
1259 self.__maxDate = dt
1260 if self.__minDate > dt:
1261 self.__minDate = dt
1262 noEntries += 1
1263 log = {"message": [], "bookmarks": None, "phase": ""}
1264 changedPaths = []
1265 fileCopies = {}
1266
1267 self.__resizeColumnsLog()
1268
1269 if self.__started and not self.__finishCallbacks:
1270 # we are really done
1271 if self.__selectedRevisions:
1272 foundItems = self.logTree.findItems(
1273 self.__selectedRevisions[0], Qt.MatchFlag.MatchExactly,
1274 self.RevisionColumn)
1275 if foundItems:
1276 self.logTree.setCurrentItem(foundItems[0])
1277 else:
1278 self.logTree.setCurrentItem(
1279 self.logTree.topLevelItem(0))
1280 elif self.__projectWorkingDirParents:
1281 for rev in self.__projectWorkingDirParents:
1282 # rev string format must match with the format of the
1283 # __generateLogItem() method
1284 items = self.logTree.findItems(
1285 "{0:>7}:".format(rev),
1286 Qt.MatchFlag.MatchStartsWith,
1287 self.RevisionColumn)
1288 if items:
1289 self.logTree.setCurrentItem(items[0])
1290 break
1291 else:
1292 self.logTree.setCurrentItem(
1293 self.logTree.topLevelItem(0))
1294 else:
1295 self.logTree.setCurrentItem(self.logTree.topLevelItem(0))
1296 self.__started = False
1297
1298 if self.commandMode in ("incoming", "outgoing"):
1299 self.commandMode = "log" # switch to log mode
1300 if self.__lastRev > 0:
1301 self.nextButton.setEnabled(True)
1302 self.limitSpinBox.setEnabled(True)
1303 else:
1304 if noEntries < self.limitSpinBox.value() and not self.cancelled:
1305 self.nextButton.setEnabled(False)
1306 self.limitSpinBox.setEnabled(False)
1307
1308 # update the log filters
1309 self.__filterLogsEnabled = False
1310 self.fromDate.setMinimumDate(self.__minDate)
1311 self.fromDate.setMaximumDate(self.__maxDate)
1312 self.fromDate.setDate(self.__minDate)
1313 self.toDate.setMinimumDate(self.__minDate)
1314 self.toDate.setMaximumDate(self.__maxDate)
1315 self.toDate.setDate(self.__maxDate)
1316
1317 branchFilter = self.branchCombo.currentText()
1318 if not branchFilter:
1319 branchFilter = self.__allBranchesFilter
1320 self.branchCombo.clear()
1321 self.branchCombo.addItems(
1322 [self.__allBranchesFilter] + sorted(self.__branchColors.keys()))
1323 self.branchCombo.setCurrentIndex(
1324 self.branchCombo.findText(branchFilter))
1325
1326 self.__filterLogsEnabled = True
1327 if self.__actionMode() == "filter":
1328 self.__filterLogs()
1329 self.__updateToolMenuActions()
1330
1331 # restore selected item
1332 if self.__selectedRevisions and not self.__finishCallbacks:
1333 # we are really done
1334 for revision in self.__selectedRevisions:
1335 items = self.logTree.findItems(
1336 revision, Qt.MatchFlag.MatchExactly, self.RevisionColumn)
1337 if items:
1338 items[0].setSelected(True)
1339 self.__selectedRevisions = []
1340
1341 def __showError(self, out):
1342 """
1343 Private slot to show some error.
1344
1345 @param out error to be shown (string)
1346 """
1347 self.errorGroup.show()
1348 self.errors.insertPlainText(out)
1349 self.errors.ensureCursorVisible()
1350
1351 def on_buttonBox_clicked(self, button):
1352 """
1353 Private slot called by a button of the button box clicked.
1354
1355 @param button button that was clicked (QAbstractButton)
1356 """
1357 if button == self.buttonBox.button(
1358 QDialogButtonBox.StandardButton.Close
1359 ):
1360 self.close()
1361 elif button == self.buttonBox.button(
1362 QDialogButtonBox.StandardButton.Cancel
1363 ):
1364 self.cancelled = True
1365 self.__hgClient.cancel()
1366 elif button == self.refreshButton:
1367 self.on_refreshButton_clicked()
1368
1369 def __updateSbsSelectLabel(self):
1370 """
1371 Private slot to update the enabled status of the diff buttons.
1372 """
1373 self.sbsSelectLabel.clear()
1374 if self.__isFile:
1375 selectedItems = self.logTree.selectedItems()
1376 if len(selectedItems) == 1:
1377 currentItem = selectedItems[0]
1378 rev2 = (
1379 currentItem.text(self.RevisionColumn).split(":", 1)[0]
1380 .strip()
1381 )
1382 parents = currentItem.data(0, self.__parentsRole)
1383 if parents:
1384 parentLinks = []
1385 for index in range(len(parents)):
1386 parentLinks.append(
1387 '<a href="sbsdiff:{0}_{1}">&nbsp;{2}&nbsp;</a>'
1388 .format(parents[index], rev2, index + 1))
1389 self.sbsSelectLabel.setText(
1390 self.tr('Side-by-Side Diff to Parent {0}').format(
1391 " ".join(parentLinks)))
1392 elif len(selectedItems) == 2:
1393 rev1 = int(selectedItems[0].text(self.RevisionColumn)
1394 .split(":", 1)[0])
1395 rev2 = int(selectedItems[1].text(self.RevisionColumn)
1396 .split(":", 1)[0])
1397 if rev1 > rev2:
1398 # Swap the entries, so that rev1 < rev2
1399 rev1, rev2 = rev2, rev1
1400 self.sbsSelectLabel.setText(self.tr(
1401 '<a href="sbsdiff:{0}_{1}">Side-by-Side Compare</a>')
1402 .format(rev1, rev2))
1403
1404 def __updateToolMenuActions(self):
1405 """
1406 Private slot to update the status of the tool menu actions and
1407 the tool menu button.
1408 """
1409 if self.initialCommandMode in ("log", "full_log") and self.projectMode:
1410 # do the phase action
1411 # step 1: count entries with changeable phases
1412 secret = 0
1413 draft = 0
1414 public = 0
1415 for itm in [item for item in self.logTree.selectedItems()
1416 if not item.data(0, self.__incomingRole)]:
1417 # count phase for local items only
1418 phase = itm.text(self.PhaseColumn)
1419 if phase == self.phases["draft"]:
1420 draft += 1
1421 elif phase == self.phases["secret"]:
1422 secret += 1
1423 else:
1424 public += 1
1425
1426 # step 2: set the status of the phase action
1427 if (
1428 public == 0 and
1429 ((secret > 0 and draft == 0) or
1430 (secret == 0 and draft > 0))
1431 ):
1432 self.__phaseAct.setEnabled(True)
1433 else:
1434 self.__phaseAct.setEnabled(False)
1435
1436 # do the graft action
1437 # step 1: count selected entries not belonging to the
1438 # current branch
1439 otherBranches = 0
1440 for itm in [item for item in self.logTree.selectedItems()
1441 if not item.data(0, self.__incomingRole)]:
1442 # for local items only
1443 branch = itm.text(self.BranchColumn)
1444 if branch != self.__projectBranch:
1445 otherBranches += 1
1446
1447 # step 2: set the status of the graft action
1448 self.__graftAct.setEnabled(otherBranches > 0)
1449
1450 selectedItemsCount = len([
1451 itm for itm in self.logTree.selectedItems()
1452 if not itm.data(0, self.__incomingRole)
1453 ])
1454 selectedIncomingItemsCount = len([
1455 itm for itm in self.logTree.selectedItems()
1456 if itm.data(0, self.__incomingRole)
1457 ])
1458
1459 self.__mergeAct.setEnabled(selectedItemsCount == 1)
1460 self.__tagAct.setEnabled(selectedItemsCount == 1)
1461 self.__switchAct.setEnabled(selectedItemsCount == 1)
1462 self.__bookmarkAct.setEnabled(selectedItemsCount == 1)
1463 self.__bookmarkMoveAct.setEnabled(selectedItemsCount == 1)
1464
1465 if selectedIncomingItemsCount > 0:
1466 self.__pullAct.setText(self.tr("Pull Selected Changes"))
1467 else:
1468 self.__pullAct.setText(self.tr("Pull Changes"))
1469 if self.vcs.canPull():
1470 self.__pullAct.setEnabled(True)
1471 self.__lfPullAct.setEnabled(
1472 self.vcs.isExtensionActive("largefiles") and
1473 selectedItemsCount > 0)
1474 else:
1475 self.__pullAct.setEnabled(False)
1476 self.__lfPullAct.setEnabled(False)
1477
1478 if self.vcs.canPush():
1479 self.__pushAct.setEnabled(
1480 selectedItemsCount == 1 and
1481 not self.logTree.selectedItems()[0].data(
1482 0, self.__incomingRole) and
1483 self.logTree.selectedItems()[0].text(self.PhaseColumn) ==
1484 self.phases["draft"])
1485 self.__pushAllAct.setEnabled(True)
1486 else:
1487 self.__pushAct.setEnabled(False)
1488 self.__pushAllAct.setEnabled(False)
1489
1490 self.__stripAct.setEnabled(
1491 self.vcs.isExtensionActive("strip") and
1492 selectedItemsCount == 1)
1493
1494 # count incoming items for 'full_log'
1495 if self.initialCommandMode == "full_log":
1496 # incoming items are at the top
1497 incomingCount = 0
1498 for row in range(self.logTree.topLevelItemCount()):
1499 if self.logTree.topLevelItem(row).data(
1500 0, self.__incomingRole):
1501 incomingCount += 1
1502 else:
1503 break
1504 localCount = self.logTree.topLevelItemCount() - incomingCount
1505 else:
1506 localCount = self.logTree.topLevelItemCount()
1507 self.__bundleAct.setEnabled(localCount > 0)
1508 self.__unbundleAct.setEnabled(False)
1509
1510 self.__gpgSignAct.setEnabled(
1511 self.vcs.isExtensionActive("gpg") and
1512 selectedItemsCount > 0)
1513 self.__gpgVerifyAct.setEnabled(
1514 self.vcs.isExtensionActive("gpg") and
1515 selectedItemsCount == 1)
1516
1517 if self.vcs.isExtensionActive("closehead"):
1518 revs = [itm.text(self.RevisionColumn).strip().split(":", 1)[0]
1519 for itm in self.logTree.selectedItems()
1520 if not itm.data(0, self.__incomingRole)]
1521 revs = [rev for rev in revs if rev in self.__headRevisions]
1522 self.__closeHeadsAct.setEnabled(len(revs) > 0)
1523 else:
1524 self.__closeHeadsAct.setEnabled(False)
1525 self.actionsButton.setEnabled(True)
1526
1527 elif self.initialCommandMode == "incoming" and self.projectMode:
1528 for act in [self.__phaseAct, self.__graftAct, self.__mergeAct,
1529 self.__tagAct, self.__closeHeadsAct, self.__switchAct,
1530 self.__bookmarkAct, self.__bookmarkMoveAct,
1531 self.__pushAct, self.__pushAllAct, self.__stripAct,
1532 self.__bundleAct, self.__gpgSignAct,
1533 self.__gpgVerifyAct]:
1534 act.setEnabled(False)
1535
1536 self.__pullAct.setText(self.tr("Pull Selected Changes"))
1537 if self.vcs.canPull() and not bool(self.__bundle):
1538 selectedIncomingItemsCount = len([
1539 itm for itm in self.logTree.selectedItems()
1540 if itm.data(0, self.__incomingRole)
1541 ])
1542 self.__pullAct.setEnabled(selectedIncomingItemsCount > 0)
1543 self.__lfPullAct.setEnabled(
1544 self.vcs.isExtensionActive("largefiles") and
1545 selectedIncomingItemsCount > 0)
1546 else:
1547 self.__pullAct.setEnabled(False)
1548 self.__lfPullAct.setEnabled(False)
1549
1550 self.__unbundleAct.setEnabled(bool(self.__bundle))
1551
1552 self.actionsButton.setEnabled(True)
1553
1554 elif self.initialCommandMode == "outgoing" and self.projectMode:
1555 for act in [self.__phaseAct, self.__graftAct, self.__mergeAct,
1556 self.__tagAct, self.__closeHeadsAct, self.__switchAct,
1557 self.__bookmarkAct, self.__bookmarkMoveAct,
1558 self.__pullAct, self.__lfPullAct,
1559 self.__stripAct, self.__gpgSignAct,
1560 self.__gpgVerifyAct, self.__unbundleAct]:
1561 act.setEnabled(False)
1562
1563 selectedItemsCount = len(self.logTree.selectedItems())
1564 if self.vcs.canPush():
1565 self.__pushAct.setEnabled(
1566 selectedItemsCount == 1 and
1567 self.logTree.selectedItems()[0].text(self.PhaseColumn) ==
1568 self.phases["draft"])
1569 self.__pushAllAct.setEnabled(True)
1570 else:
1571 self.__pushAct.setEnabled(False)
1572 self.__pushAllAct.setEnabled(False)
1573
1574 self.__bundleAct.setEnabled(selectedItemsCount > 0)
1575
1576 else:
1577 self.actionsButton.setEnabled(False)
1578
1579 def __updateDetailsAndFiles(self):
1580 """
1581 Private slot to update the details and file changes panes.
1582 """
1583 self.detailsEdit.clear()
1584 self.filesTree.clear()
1585 self.__diffUpdatesFiles = False
1586
1587 selectedItems = self.logTree.selectedItems()
1588 if len(selectedItems) == 1:
1589 self.detailsEdit.setHtml(
1590 self.__generateDetailsTableText(selectedItems[0]))
1591 self.__updateFilesTree(self.filesTree, selectedItems[0])
1592 self.__resizeColumnsFiles()
1593 self.__resortFiles()
1594 elif len(selectedItems) == 2:
1595 self.__diffUpdatesFiles = True
1596 index1 = self.logTree.indexOfTopLevelItem(selectedItems[0])
1597 index2 = self.logTree.indexOfTopLevelItem(selectedItems[1])
1598 if index1 > index2:
1599 # Swap the entries
1600 selectedItems[0], selectedItems[1] = (
1601 selectedItems[1], selectedItems[0]
1602 )
1603 html = "{0}<hr/>{1}".format(
1604 self.__generateDetailsTableText(selectedItems[0]),
1605 self.__generateDetailsTableText(selectedItems[1]),
1606 )
1607 self.detailsEdit.setHtml(html)
1608 # self.filesTree is updated by the diff
1609
1610 def __generateDetailsTableText(self, itm):
1611 """
1612 Private method to generate an HTML table with the details of the given
1613 changeset.
1614
1615 @param itm reference to the item the table should be based on
1616 @type QTreeWidgetItem
1617 @return HTML table containing details
1618 @rtype str
1619 """
1620 if itm is not None:
1621 if itm.text(self.TagsColumn):
1622 tagsStr = self.__tagsTemplate.format(itm.text(self.TagsColumn))
1623 else:
1624 tagsStr = ""
1625
1626 if itm.text(self.BookmarksColumn):
1627 bookmarksStr = self.__bookmarksTemplate.format(
1628 itm.text(self.BookmarksColumn))
1629 else:
1630 bookmarksStr = ""
1631
1632 if self.projectMode and itm.data(0, self.__latestTagRole):
1633 latestTagLinks = []
1634 for tag in itm.data(0, self.__latestTagRole):
1635 latestTagLinks.append('<a href="rev:{0}">{1}</a>'.format(
1636 self.__getRevisionOfTag(tag)[0], tag))
1637 latestTagStr = self.__latestTagTemplate.format(
1638 ", ".join(latestTagLinks))
1639 else:
1640 latestTagStr = ""
1641
1642 rev = int(itm.text(self.RevisionColumn).split(":", 1)[0])
1643
1644 if itm.data(0, self.__parentsRole):
1645 parentLinks = []
1646 for parent in [str(x) for x in
1647 itm.data(0, self.__parentsRole)]:
1648 parentLinks.append(
1649 '<a href="rev:{0}">{0}</a>'.format(parent))
1650 parentsStr = self.__parentsTemplate.format(
1651 ", ".join(parentLinks))
1652 else:
1653 parentsStr = ""
1654
1655 if self.__childrenInfo[rev]:
1656 childLinks = []
1657 for child in [str(x) for x in self.__childrenInfo[rev]]:
1658 childLinks.append(
1659 '<a href="rev:{0}">{0}</a>'.format(child))
1660 childrenStr = self.__childrenTemplate.format(
1661 ", ".join(childLinks))
1662 else:
1663 childrenStr = ""
1664
1665 messagesList = []
1666 for line in itm.data(0, self.__messageRole):
1667 match = HgLogBrowserDialog.GraftedRe.fullmatch(line)
1668 if match:
1669 messagesList.append(
1670 HgLogBrowserDialog.GraftedTemplate.format(
1671 match.group(1)))
1672 else:
1673 messagesList.append(Utilities.html_encode(line.strip()))
1674 messageStr = "<br />\n".join(messagesList)
1675
1676 html = self.__detailsTemplate.format(
1677 itm.text(self.RevisionColumn),
1678 itm.text(self.DateColumn),
1679 itm.text(self.AuthorColumn),
1680 itm.text(self.BranchColumn).replace(
1681 self.ClosedIndicator, ""),
1682 parentsStr + childrenStr + tagsStr + latestTagStr +
1683 bookmarksStr,
1684 messageStr,
1685 )
1686 else:
1687 html = ""
1688
1689 return html
1690
1691 def __updateFilesTree(self, parent, itm):
1692 """
1693 Private method to update the files tree with changes of the given item.
1694
1695 @param parent parent for the items to be added
1696 @type QTreeWidget or QTreeWidgetItem
1697 @param itm reference to the item the update should be based on
1698 @type QTreeWidgetItem
1699 """
1700 if itm is not None:
1701 changes = itm.data(0, self.__changesRole)
1702 if len(changes) > 0:
1703 for change in changes:
1704 QTreeWidgetItem(parent, [
1705 self.flags[change["action"]],
1706 change["path"].strip(),
1707 change["copyfrom"].strip(),
1708 ])
1709
1710 @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem)
1711 def on_logTree_currentItemChanged(self, current, previous):
1712 """
1713 Private slot called, when the current item of the log tree changes.
1714
1715 @param current reference to the new current item (QTreeWidgetItem)
1716 @param previous reference to the old current item (QTreeWidgetItem)
1717 """
1718 self.__updateToolMenuActions()
1719
1720 # Highlight the current entry using a bold font
1721 for col in range(self.logTree.columnCount()):
1722 current and current.setFont(col, self.__logTreeBoldFont)
1723 previous and previous.setFont(col, self.__logTreeNormalFont)
1724
1725 # set the state of the up and down buttons
1726 self.upButton.setEnabled(
1727 current is not None and
1728 self.logTree.indexOfTopLevelItem(current) > 0)
1729 self.downButton.setEnabled(
1730 current is not None and
1731 int(current.text(self.RevisionColumn).split(":")[0]) > 0 and
1732 (self.logTree.indexOfTopLevelItem(current) <
1733 self.logTree.topLevelItemCount() - 1 or
1734 self.nextButton.isEnabled()))
1735
1736 @pyqtSlot()
1737 def on_logTree_itemSelectionChanged(self):
1738 """
1739 Private slot called, when the selection has changed.
1740 """
1741 self.__updateDetailsAndFiles()
1742 self.__updateSbsSelectLabel()
1743 self.__updateToolMenuActions()
1744 self.__generateDiffs()
1745
1746 @pyqtSlot()
1747 def on_upButton_clicked(self):
1748 """
1749 Private slot to move the current item up one entry.
1750 """
1751 itm = self.logTree.itemAbove(self.logTree.currentItem())
1752 if itm:
1753 self.logTree.setCurrentItem(itm)
1754
1755 @pyqtSlot()
1756 def on_downButton_clicked(self):
1757 """
1758 Private slot to move the current item down one entry.
1759 """
1760 itm = self.logTree.itemBelow(self.logTree.currentItem())
1761 if itm:
1762 self.logTree.setCurrentItem(itm)
1763 else:
1764 # load the next bunch and try again
1765 if self.nextButton.isEnabled():
1766 self.__addFinishCallback(self.on_downButton_clicked)
1767 self.on_nextButton_clicked()
1768
1769 @pyqtSlot()
1770 def on_nextButton_clicked(self):
1771 """
1772 Private slot to handle the Next button.
1773 """
1774 if self.nextButton.isEnabled():
1775 if self.__lastRev > 0:
1776 self.__getLogEntries(startRev=self.__lastRev - 1)
1777 else:
1778 self.__getLogEntries()
1779
1780 @pyqtSlot(QDate)
1781 def on_fromDate_dateChanged(self, date):
1782 """
1783 Private slot called, when the from date changes.
1784
1785 @param date new date (QDate)
1786 """
1787 if self.__actionMode() == "filter":
1788 self.__filterLogs()
1789
1790 @pyqtSlot(QDate)
1791 def on_toDate_dateChanged(self, date):
1792 """
1793 Private slot called, when the from date changes.
1794
1795 @param date new date (QDate)
1796 """
1797 if self.__actionMode() == "filter":
1798 self.__filterLogs()
1799
1800 @pyqtSlot(int)
1801 def on_branchCombo_activated(self, index):
1802 """
1803 Private slot called, when a new branch is selected.
1804
1805 @param index index of the selected entry
1806 @type int
1807 """
1808 if self.__actionMode() == "filter":
1809 self.__filterLogs()
1810
1811 @pyqtSlot(int)
1812 def on_fieldCombo_activated(self, index):
1813 """
1814 Private slot called, when a new filter field is selected.
1815
1816 @param index index of the selected entry
1817 @type int
1818 """
1819 if self.__actionMode() == "filter":
1820 self.__filterLogs()
1821
1822 @pyqtSlot(str)
1823 def on_rxEdit_textChanged(self, txt):
1824 """
1825 Private slot called, when a filter expression is entered.
1826
1827 @param txt filter expression (string)
1828 """
1829 if self.__actionMode() == "filter":
1830 self.__filterLogs()
1831 elif self.__actionMode() == "find":
1832 self.__findItem(self.__findBackwards, interactive=True)
1833
1834 @pyqtSlot()
1835 def on_rxEdit_returnPressed(self):
1836 """
1837 Private slot handling a press of the Return key in the rxEdit input.
1838 """
1839 if self.__actionMode() == "find":
1840 self.__findItem(self.__findBackwards, interactive=True)
1841
1842 def __filterLogs(self):
1843 """
1844 Private method to filter the log entries.
1845 """
1846 if self.__filterLogsEnabled:
1847 from_ = self.fromDate.date().toString("yyyy-MM-dd")
1848 to_ = self.toDate.date().addDays(1).toString("yyyy-MM-dd")
1849 branch = self.branchCombo.currentText()
1850 closedBranch = branch + '--'
1851 fieldIndex, searchRx, indexIsRole = self.__prepareFieldSearch()
1852
1853 visibleItemCount = self.logTree.topLevelItemCount()
1854 currentItem = self.logTree.currentItem()
1855 for topIndex in range(self.logTree.topLevelItemCount()):
1856 topItem = self.logTree.topLevelItem(topIndex)
1857 if indexIsRole:
1858 if fieldIndex == self.__changesRole:
1859 changes = topItem.data(0, self.__changesRole)
1860 txt = "\n".join(
1861 [c["path"] for c in changes] +
1862 [c["copyfrom"] for c in changes]
1863 )
1864 else:
1865 # Find based on complete message text
1866 txt = "\n".join(topItem.data(0, self.__messageRole))
1867 else:
1868 txt = topItem.text(fieldIndex)
1869 if (
1870 topItem.text(self.DateColumn) <= to_ and
1871 topItem.text(self.DateColumn) >= from_ and
1872 (branch == self.__allBranchesFilter or
1873 topItem.text(self.BranchColumn) in
1874 [branch, closedBranch]) and
1875 searchRx.search(txt) is not None
1876 ):
1877 topItem.setHidden(False)
1878 if topItem is currentItem:
1879 self.on_logTree_currentItemChanged(topItem, None)
1880 else:
1881 topItem.setHidden(True)
1882 if topItem is currentItem:
1883 self.filesTree.clear()
1884 visibleItemCount -= 1
1885 self.logTree.header().setSectionHidden(
1886 self.IconColumn,
1887 visibleItemCount != self.logTree.topLevelItemCount())
1888
1889 def __prepareFieldSearch(self):
1890 """
1891 Private slot to prepare the filed search data.
1892
1893 @return tuple of field index, search expression and flag indicating
1894 that the field index is a data role (integer, string, boolean)
1895 """
1896 indexIsRole = False
1897 txt = self.fieldCombo.itemData(self.fieldCombo.currentIndex())
1898 if txt == "author":
1899 fieldIndex = self.AuthorColumn
1900 searchRx = re.compile(self.rxEdit.text(), re.IGNORECASE)
1901 elif txt == "revision":
1902 fieldIndex = self.RevisionColumn
1903 txt = self.rxEdit.text()
1904 if txt.startswith("^"):
1905 searchRx = re.compile(r"^\s*{0}".format(txt[1:]),
1906 re.IGNORECASE)
1907 else:
1908 searchRx = re.compile(txt, re.IGNORECASE)
1909 elif txt == "file":
1910 fieldIndex = self.__changesRole
1911 searchRx = re.compile(self.rxEdit.text(), re.IGNORECASE)
1912 indexIsRole = True
1913 elif txt == "phase":
1914 fieldIndex = self.PhaseColumn
1915 searchRx = re.compile(self.rxEdit.text(), re.IGNORECASE)
1916 else:
1917 fieldIndex = self.__messageRole
1918 searchRx = re.compile(self.rxEdit.text(), re.IGNORECASE)
1919 indexIsRole = True
1920
1921 return fieldIndex, searchRx, indexIsRole
1922
1923 @pyqtSlot(bool)
1924 def on_stopCheckBox_clicked(self, checked):
1925 """
1926 Private slot called, when the stop on copy/move checkbox is clicked.
1927
1928 @param checked flag indicating the state of the check box (boolean)
1929 """
1930 self.vcs.getPlugin().setPreferences("StopLogOnCopy",
1931 self.stopCheckBox.isChecked())
1932 self.nextButton.setEnabled(True)
1933 self.limitSpinBox.setEnabled(True)
1934
1935 @pyqtSlot()
1936 def on_refreshButton_clicked(self, addNext=False):
1937 """
1938 Private slot to refresh the log.
1939
1940 @param addNext flag indicating to get a second batch of log entries as
1941 well
1942 @type bool
1943 """
1944 self.buttonBox.button(
1945 QDialogButtonBox.StandardButton.Close).setEnabled(False)
1946 self.buttonBox.button(
1947 QDialogButtonBox.StandardButton.Cancel).setEnabled(True)
1948 self.buttonBox.button(
1949 QDialogButtonBox.StandardButton.Cancel).setDefault(True)
1950
1951 self.refreshButton.setEnabled(False)
1952
1953 # save the selected items commit IDs
1954 self.__selectedRevisions = []
1955 for item in self.logTree.selectedItems():
1956 self.__selectedRevisions.append(item.text(self.RevisionColumn))
1957
1958 if self.initialCommandMode in ("incoming", "outgoing"):
1959 self.nextButton.setEnabled(False)
1960 self.limitSpinBox.setEnabled(False)
1961 if addNext:
1962 self.__addFinishCallback(self.on_nextButton_clicked)
1963 else:
1964 self.nextButton.setEnabled(True)
1965 self.limitSpinBox.setEnabled(True)
1966
1967 if self.initialCommandMode == "full_log":
1968 self.commandMode = "incoming"
1969 self.__addFinishCallback(self.on_nextButton_clicked)
1970 else:
1971 self.commandMode = self.initialCommandMode
1972 self.start(self.__filename, bundle=self.__bundle, isFile=self.__isFile,
1973 noEntries=self.logTree.topLevelItemCount())
1974
1975 @pyqtSlot()
1976 def __phaseActTriggered(self):
1977 """
1978 Private slot to handle the Change Phase action.
1979 """
1980 itm = self.logTree.selectedItems()[0]
1981 if not itm.data(0, self.__incomingRole):
1982 currentPhase = itm.text(self.PhaseColumn)
1983 revs = []
1984 for itm in self.logTree.selectedItems():
1985 if itm.text(self.PhaseColumn) == currentPhase:
1986 revs.append(
1987 itm.text(self.RevisionColumn).split(":")[0].strip())
1988
1989 if not revs:
1990 self.__phaseAct.setEnabled(False)
1991 return
1992
1993 if currentPhase == self.phases["draft"]:
1994 newPhase = self.phases["secret"]
1995 data = (revs, "s", True)
1996 else:
1997 newPhase = self.phases["draft"]
1998 data = (revs, "d", False)
1999 res = self.vcs.hgPhase(data)
2000 if res:
2001 for itm in self.logTree.selectedItems():
2002 itm.setText(self.PhaseColumn, newPhase)
2003
2004 @pyqtSlot()
2005 def __graftActTriggered(self):
2006 """
2007 Private slot to handle the Copy Changesets action.
2008 """
2009 revs = []
2010
2011 for itm in [item for item in self.logTree.selectedItems()
2012 if not item.data(0, self.__incomingRole)]:
2013 branch = itm.text(self.BranchColumn)
2014 if branch != self.__projectBranch:
2015 revs.append(
2016 itm.text(self.RevisionColumn).strip().split(":", 1)[0])
2017
2018 if revs:
2019 shouldReopen = self.vcs.hgGraft(revs)
2020 if shouldReopen:
2021 res = EricMessageBox.yesNo(
2022 None,
2023 self.tr("Copy Changesets"),
2024 self.tr(
2025 """The project should be reread. Do this now?"""),
2026 yesDefault=True)
2027 if res:
2028 ericApp().getObject("Project").reopenProject()
2029 return
2030
2031 self.on_refreshButton_clicked()
2032
2033 @pyqtSlot()
2034 def __tagActTriggered(self):
2035 """
2036 Private slot to tag the selected revision.
2037 """
2038 if len([itm for itm in self.logTree.selectedItems()
2039 if not itm.data(0, self.__incomingRole)]) == 1:
2040 itm = self.logTree.selectedItems()[0]
2041 rev = itm.text(self.RevisionColumn).strip().split(":", 1)[0]
2042 tag = itm.text(self.TagsColumn).strip().split(", ", 1)[0]
2043 res = self.vcs.vcsTag(revision=rev, tagName=tag)
2044 if res:
2045 self.on_refreshButton_clicked()
2046
2047 @pyqtSlot()
2048 def __closeHeadsActTriggered(self):
2049 """
2050 Private slot to close the selected head revisions.
2051 """
2052 if self.vcs.isExtensionActive("closehead"):
2053 revs = [itm.text(self.RevisionColumn).strip().split(":", 1)[0]
2054 for itm in self.logTree.selectedItems()
2055 if not itm.data(0, self.__incomingRole)]
2056 revs = [rev for rev in revs if rev in self.__headRevisions]
2057 if revs:
2058 closeheadExtension = self.vcs.getExtensionObject("closehead")
2059 if closeheadExtension is not None:
2060 closeheadExtension.hgCloseheads(revisions=revs)
2061
2062 self.on_refreshButton_clicked()
2063
2064 @pyqtSlot()
2065 def __switchActTriggered(self):
2066 """
2067 Private slot to switch the working directory to the
2068 selected revision.
2069 """
2070 if len([itm for itm in self.logTree.selectedItems()
2071 if not itm.data(0, self.__incomingRole)]) == 1:
2072 itm = self.logTree.selectedItems()[0]
2073 rev = itm.text(self.RevisionColumn).strip().split(":", 1)[0]
2074 bookmarks = [bm.strip() for bm in
2075 itm.text(self.BookmarksColumn).strip().split(",")
2076 if bm.strip()]
2077 if bookmarks:
2078 bookmark, ok = QInputDialog.getItem(
2079 self,
2080 self.tr("Switch"),
2081 self.tr("Select bookmark to switch to (leave empty to"
2082 " use revision):"),
2083 [""] + bookmarks,
2084 0, False)
2085 if not ok:
2086 return
2087 if bookmark:
2088 rev = bookmark
2089 if rev:
2090 shouldReopen = self.vcs.vcsUpdate(revision=rev)
2091 if shouldReopen:
2092 res = EricMessageBox.yesNo(
2093 None,
2094 self.tr("Switch"),
2095 self.tr(
2096 """The project should be reread. Do this now?"""),
2097 yesDefault=True)
2098 if res:
2099 ericApp().getObject("Project").reopenProject()
2100 return
2101
2102 self.on_refreshButton_clicked()
2103
2104 @pyqtSlot()
2105 def __bookmarkActTriggered(self):
2106 """
2107 Private slot to bookmark the selected revision.
2108 """
2109 if len([itm for itm in self.logTree.selectedItems()
2110 if not itm.data(0, self.__incomingRole)]) == 1:
2111 itm = self.logTree.selectedItems()[0]
2112 rev, changeset = (
2113 itm.text(self.RevisionColumn).strip().split(":", 1)
2114 )
2115 bookmark, ok = QInputDialog.getText(
2116 self,
2117 self.tr("Define Bookmark"),
2118 self.tr('Enter bookmark name for changeset "{0}":').format(
2119 changeset),
2120 QLineEdit.EchoMode.Normal)
2121 if ok and bool(bookmark):
2122 self.vcs.hgBookmarkDefine(
2123 revision="rev({0})".format(rev),
2124 bookmark=bookmark)
2125 self.on_refreshButton_clicked()
2126
2127 @pyqtSlot()
2128 def __bookmarkMoveActTriggered(self):
2129 """
2130 Private slot to move a bookmark to the selected revision.
2131 """
2132 if len([itm for itm in self.logTree.selectedItems()
2133 if not itm.data(0, self.__incomingRole)]) == 1:
2134 itm = self.logTree.selectedItems()[0]
2135 rev, changeset = (
2136 itm.text(self.RevisionColumn).strip().split(":", 1)
2137 )
2138 bookmarksList = self.vcs.hgGetBookmarksList()
2139 bookmark, ok = QInputDialog.getItem(
2140 self,
2141 self.tr("Move Bookmark"),
2142 self.tr('Select the bookmark to be moved to changeset'
2143 ' "{0}":').format(changeset),
2144 [""] + bookmarksList,
2145 0, False)
2146 if ok and bool(bookmark):
2147 self.vcs.hgBookmarkMove(
2148 revision="rev({0})".format(rev),
2149 bookmark=bookmark)
2150 self.on_refreshButton_clicked()
2151
2152 @pyqtSlot()
2153 def __lfPullActTriggered(self):
2154 """
2155 Private slot to pull large files of selected revisions.
2156 """
2157 revs = []
2158 for itm in [item for item in self.logTree.selectedItems()
2159 if not item.data(0, self.__incomingRole)]:
2160 rev = itm.text(self.RevisionColumn).strip().split(":", 1)[0]
2161 if rev:
2162 revs.append(rev)
2163
2164 if revs:
2165 self.vcs.getExtensionObject("largefiles").hgLfPull(revisions=revs)
2166
2167 @pyqtSlot()
2168 def __pullActTriggered(self):
2169 """
2170 Private slot to pull changes from a remote repository.
2171 """
2172 shouldReopen = False
2173 refresh = False
2174 addNext = False
2175
2176 if self.initialCommandMode in ("log", "full_log", "incoming"):
2177 revs = []
2178 for itm in [item for item in self.logTree.selectedItems()
2179 if item.data(0, self.__incomingRole)]:
2180 rev = itm.text(self.RevisionColumn).split(":")[1].strip()
2181 if rev:
2182 revs.append(rev)
2183 shouldReopen = self.vcs.hgPull(revisions=revs)
2184 refresh = True
2185 if self.initialCommandMode == "incoming":
2186 addNext = True
2187
2188 if shouldReopen:
2189 res = EricMessageBox.yesNo(
2190 None,
2191 self.tr("Pull Changes"),
2192 self.tr(
2193 """The project should be reread. Do this now?"""),
2194 yesDefault=True)
2195 if res:
2196 ericApp().getObject("Project").reopenProject()
2197 return
2198
2199 if refresh:
2200 self.on_refreshButton_clicked(addNext=addNext)
2201
2202 @pyqtSlot()
2203 def __pushActTriggered(self):
2204 """
2205 Private slot to push changes to a remote repository up to a selected
2206 changeset.
2207 """
2208 itm = self.logTree.selectedItems()[0]
2209 if not itm.data(0, self.__incomingRole):
2210 rev = itm.text(self.RevisionColumn).strip().split(":", 1)[0]
2211 if rev:
2212 self.vcs.hgPush(rev=rev)
2213 self.on_refreshButton_clicked(
2214 addNext=self.initialCommandMode == "outgoing")
2215
2216 @pyqtSlot()
2217 def __pushAllActTriggered(self):
2218 """
2219 Private slot to push all changes to a remote repository.
2220 """
2221 self.vcs.hgPush()
2222 self.on_refreshButton_clicked()
2223
2224 @pyqtSlot()
2225 def __stripActTriggered(self):
2226 """
2227 Private slot to strip changesets from the repository.
2228 """
2229 itm = self.logTree.selectedItems()[0]
2230 if not itm.data(0, self.__incomingRole):
2231 rev = itm.text(self.RevisionColumn).strip().split(":", 1)[1]
2232 shouldReopen = self.vcs.getExtensionObject("strip").hgStrip(
2233 rev=rev)
2234 if shouldReopen:
2235 res = EricMessageBox.yesNo(
2236 None,
2237 self.tr("Strip Changesets"),
2238 self.tr(
2239 """The project should be reread. Do this now?"""),
2240 yesDefault=True)
2241 if res:
2242 ericApp().getObject("Project").reopenProject()
2243 return
2244
2245 self.on_refreshButton_clicked()
2246
2247 @pyqtSlot()
2248 def __mergeActTriggered(self):
2249 """
2250 Private slot to merge the working directory with the selected
2251 changeset.
2252 """
2253 itm = self.logTree.selectedItems()[0]
2254 if not itm.data(0, self.__incomingRole):
2255 rev = "rev({0})".format(
2256 itm.text(self.RevisionColumn).strip().split(":", 1)[0])
2257 self.vcs.vcsMerge("", rev=rev)
2258
2259 @pyqtSlot()
2260 def __bundleActTriggered(self):
2261 """
2262 Private slot to create a changegroup file.
2263 """
2264 if self.initialCommandMode in ("log", "full_log"):
2265 selectedItems = [itm for itm in self.logTree.selectedItems()
2266 if not itm.data(0, self.__incomingRole)]
2267 if len(selectedItems) == 0:
2268 # all revisions of the local repository will be bundled
2269 bundleData = {
2270 "revs": [],
2271 "base": "",
2272 "all": True,
2273 }
2274 elif len(selectedItems) == 1:
2275 # the selected changeset is the base
2276 rev = selectedItems[0].text(self.RevisionColumn).split(
2277 ":", 1)[0].strip()
2278 bundleData = {
2279 "revs": [],
2280 "base": rev,
2281 "all": False,
2282 }
2283 else:
2284 # lowest revision is the base, others will be bundled
2285 revs = []
2286 for itm in selectedItems:
2287 rev = itm.text(self.RevisionColumn).split(":", 1)[0]
2288 with contextlib.suppress(ValueError):
2289 revs.append(int(rev))
2290 baseRev = min(revs)
2291 while baseRev in revs:
2292 revs.remove(baseRev)
2293
2294 bundleData = {
2295 "revs": [str(rev) for rev in revs],
2296 "base": str(baseRev),
2297 "all": False,
2298 }
2299 elif self.initialCommandMode == "outgoing":
2300 selectedItems = self.logTree.selectedItems()
2301 if len(selectedItems) > 0:
2302 revs = []
2303 for itm in selectedItems:
2304 rev = itm.text(self.RevisionColumn).split(":", 1)[0]
2305 revs.append(rev.strip())
2306
2307 bundleData = {
2308 "revs": revs,
2309 "base": "",
2310 "all": False,
2311 }
2312
2313 self.vcs.hgBundle(bundleData=bundleData)
2314
2315 @pyqtSlot()
2316 def __unbundleActTriggered(self):
2317 """
2318 Private slot to apply the currently previewed bundle file.
2319 """
2320 if self.initialCommandMode == "incoming" and bool(self.__bundle):
2321 shouldReopen = self.vcs.hgUnbundle(files=[self.__bundle])
2322 if shouldReopen:
2323 res = EricMessageBox.yesNo(
2324 None,
2325 self.tr("Apply Changegroup"),
2326 self.tr("""The project should be reread. Do this now?"""),
2327 yesDefault=True)
2328 if res:
2329 ericApp().getObject("Project").reopenProject()
2330 return
2331
2332 self.vcs.vcsLogBrowser()
2333 self.close()
2334
2335 @pyqtSlot()
2336 def __gpgSignActTriggered(self):
2337 """
2338 Private slot to sign the selected revisions.
2339 """
2340 revs = []
2341 for itm in [item for item in self.logTree.selectedItems()
2342 if not item.data(0, self.__incomingRole)]:
2343 rev = itm.text(self.RevisionColumn).split(":", 1)[0].strip()
2344 if rev:
2345 revs.append(rev)
2346
2347 if revs:
2348 self.vcs.getExtensionObject("gpg").hgGpgSign(revisions=revs)
2349
2350 @pyqtSlot()
2351 def __gpgVerifyActTriggered(self):
2352 """
2353 Private slot to verify the signatures of a selected revisions.
2354 """
2355 itm = self.logTree.selectedItems()[0]
2356 if not itm.data(0, self.__incomingRole):
2357 rev = itm.text(self.RevisionColumn).split(":", 1)[0].strip()
2358 if rev:
2359 self.vcs.getExtensionObject("gpg").hgGpgVerifySignatures(
2360 rev=rev)
2361
2362 def __selectAllActTriggered(self, select=True):
2363 """
2364 Private method to select or unselect all log entries.
2365
2366 @param select flag indicating to select all entries
2367 @type bool
2368 """
2369 blocked = self.logTree.blockSignals(True)
2370 for row in range(self.logTree.topLevelItemCount()):
2371 self.logTree.topLevelItem(row).setSelected(select)
2372 self.logTree.blockSignals(blocked)
2373 self.on_logTree_itemSelectionChanged()
2374
2375 def __actionMode(self):
2376 """
2377 Private method to get the selected action mode.
2378
2379 @return selected action mode (string, one of filter or find)
2380 """
2381 return self.modeComboBox.itemData(
2382 self.modeComboBox.currentIndex())
2383
2384 @pyqtSlot(int)
2385 def on_modeComboBox_currentIndexChanged(self, index):
2386 """
2387 Private slot to react on mode changes.
2388
2389 @param index index of the selected entry (integer)
2390 """
2391 mode = self.modeComboBox.itemData(index)
2392 findMode = mode == "find"
2393 filterMode = mode == "filter"
2394
2395 self.fromDate.setEnabled(filterMode)
2396 self.toDate.setEnabled(filterMode)
2397 self.branchCombo.setEnabled(filterMode)
2398 self.findPrevButton.setVisible(findMode)
2399 self.findNextButton.setVisible(findMode)
2400
2401 if findMode:
2402 for topIndex in range(self.logTree.topLevelItemCount()):
2403 self.logTree.topLevelItem(topIndex).setHidden(False)
2404 self.logTree.header().setSectionHidden(self.IconColumn, False)
2405 elif filterMode:
2406 self.__filterLogs()
2407
2408 @pyqtSlot()
2409 def on_findPrevButton_clicked(self):
2410 """
2411 Private slot to find the previous item matching the entered criteria.
2412 """
2413 self.__findItem(True)
2414
2415 @pyqtSlot()
2416 def on_findNextButton_clicked(self):
2417 """
2418 Private slot to find the next item matching the entered criteria.
2419 """
2420 self.__findItem(False)
2421
2422 def __findItem(self, backwards=False, interactive=False):
2423 """
2424 Private slot to find an item matching the entered criteria.
2425
2426 @param backwards flag indicating to search backwards (boolean)
2427 @param interactive flag indicating an interactive search (boolean)
2428 """
2429 self.__findBackwards = backwards
2430
2431 fieldIndex, searchRx, indexIsRole = self.__prepareFieldSearch()
2432 currentIndex = self.logTree.indexOfTopLevelItem(
2433 self.logTree.currentItem())
2434 if backwards:
2435 if interactive:
2436 indexes = range(currentIndex, -1, -1)
2437 else:
2438 indexes = range(currentIndex - 1, -1, -1)
2439 else:
2440 if interactive:
2441 indexes = range(currentIndex, self.logTree.topLevelItemCount())
2442 else:
2443 indexes = range(currentIndex + 1,
2444 self.logTree.topLevelItemCount())
2445
2446 for index in indexes:
2447 topItem = self.logTree.topLevelItem(index)
2448 if indexIsRole:
2449 if fieldIndex == self.__changesRole:
2450 changes = topItem.data(0, self.__changesRole)
2451 txt = "\n".join(
2452 [c["path"] for c in changes] +
2453 [c["copyfrom"] for c in changes]
2454 )
2455 else:
2456 # Find based on complete message text
2457 txt = "\n".join(topItem.data(0, self.__messageRole))
2458 else:
2459 txt = topItem.text(fieldIndex)
2460 if searchRx.search(txt) is not None:
2461 self.logTree.setCurrentItem(self.logTree.topLevelItem(index))
2462 break
2463 else:
2464 EricMessageBox.information(
2465 self,
2466 self.tr("Find Commit"),
2467 self.tr("""'{0}' was not found.""").format(self.rxEdit.text()))
2468
2469 def __revisionClicked(self, url):
2470 """
2471 Private slot to handle the anchorClicked signal of the changeset
2472 details pane.
2473
2474 @param url URL that was clicked
2475 @type QUrl
2476 """
2477 if url.scheme() in ("rev", "chg"):
2478 if url.scheme() == "rev":
2479 # a parent or child revision was clicked, show the item
2480 rev = url.path()
2481 searchStr = "{0:>7}:".format(rev)
2482 # format must be in sync with item generation format
2483 searchFlags = Qt.MatchFlag.MatchStartsWith
2484 elif url.scheme() == "chg":
2485 # a changeset hash was clicked, show the item
2486 changeset = url.path()
2487 searchStr = ":{0}".format(changeset[:12]) # max. 12 hash chars
2488 # format must be in sync with item generation format
2489 searchFlags = Qt.MatchFlag.MatchContains
2490 items = self.logTree.findItems(
2491 searchStr, searchFlags, self.RevisionColumn)
2492 if items:
2493 itm = items[0]
2494 if itm.isHidden():
2495 itm.setHidden(False)
2496 self.logTree.setCurrentItem(itm)
2497 else:
2498 # load the next batch and try again
2499 if not self.cancelled and self.nextButton.isEnabled():
2500 self.__addFinishCallback(
2501 lambda: self.__revisionClicked(url))
2502 self.on_nextButton_clicked()
2503
2504 ###########################################################################
2505 ## Diff handling methods below
2506 ###########################################################################
2507
2508 def __generateDiffs(self, parent=1):
2509 """
2510 Private slot to generate diff outputs for the selected item.
2511
2512 @param parent number of parent to diff against
2513 @type int
2514 """
2515 self.diffEdit.setPlainText(self.tr("Generating differences ..."))
2516 self.diffLabel.setText(self.tr("Differences"))
2517 self.diffSelectLabel.clear()
2518 self.diffHighlighter.regenerateRules()
2519
2520 selectedItems = self.logTree.selectedItems()
2521 if len(selectedItems) == 1:
2522 currentItem = selectedItems[0]
2523 rev2 = currentItem.text(self.RevisionColumn).split(":", 1)[0]
2524 parents = currentItem.data(0, self.__parentsRole)
2525 if len(parents) >= parent:
2526 self.diffLabel.setText(
2527 self.tr("Differences to Parent {0}").format(parent))
2528 rev1 = parents[parent - 1]
2529
2530 self.__diffGenerator.start(self.__filename, [rev1, rev2],
2531 self.__bundle)
2532
2533 if len(parents) > 1:
2534 if parent == 1:
2535 par1 = "&nbsp;1&nbsp;"
2536 else:
2537 par1 = '<a href="diff:1">&nbsp;1&nbsp;</a>'
2538 if parent == 2:
2539 par2 = "&nbsp;2&nbsp;"
2540 else:
2541 par2 = '<a href="diff:2">&nbsp;2&nbsp;</a>'
2542 self.diffSelectLabel.setText(
2543 self.tr('Diff to Parent {0}{1}').format(par1, par2))
2544 elif len(selectedItems) == 2:
2545 rev2 = int(selectedItems[0].text(
2546 self.RevisionColumn).split(":")[0])
2547 rev1 = int(selectedItems[1].text(
2548 self.RevisionColumn).split(":")[0])
2549
2550 self.__diffGenerator.start(self.__filename,
2551 [min(rev1, rev2), max(rev1, rev2)],
2552 self.__bundle)
2553 else:
2554 self.diffEdit.clear()
2555
2556 def __generatorFinished(self):
2557 """
2558 Private slot connected to the finished signal of the diff generator.
2559 """
2560 diff, errors, fileSeparators = self.__diffGenerator.getResult()
2561
2562 if diff:
2563 self.diffEdit.setPlainText("".join(diff))
2564 elif errors:
2565 self.diffEdit.setPlainText("".join(errors))
2566 else:
2567 self.diffEdit.setPlainText(self.tr('There is no difference.'))
2568
2569 self.saveLabel.setVisible(bool(diff))
2570
2571 if self.__diffUpdatesFiles:
2572 for oldFileName, newFileName, lineNumber in fileSeparators:
2573 if oldFileName == newFileName:
2574 fileName = oldFileName
2575 elif oldFileName == "__NULL__":
2576 fileName = newFileName
2577 else:
2578 fileName = oldFileName
2579 item = QTreeWidgetItem(self.filesTree, ["", fileName, ""])
2580 item.setData(0, self.__diffFileLineRole, lineNumber)
2581 self.__resizeColumnsFiles()
2582 self.__resortFiles()
2583 else:
2584 for oldFileName, newFileName, lineNumber in fileSeparators:
2585 for fileName in (oldFileName, newFileName):
2586 if fileName != "__NULL__":
2587 items = self.filesTree.findItems(
2588 fileName, Qt.MatchFlag.MatchExactly, 1)
2589 for item in items:
2590 item.setData(0, self.__diffFileLineRole,
2591 lineNumber)
2592
2593 tc = self.diffEdit.textCursor()
2594 tc.movePosition(QTextCursor.MoveOperation.Start)
2595 self.diffEdit.setTextCursor(tc)
2596 self.diffEdit.ensureCursorVisible()
2597
2598 @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem)
2599 def on_filesTree_currentItemChanged(self, current, previous):
2600 """
2601 Private slot called, when the current item of the files tree changes.
2602
2603 @param current reference to the new current item (QTreeWidgetItem)
2604 @param previous reference to the old current item (QTreeWidgetItem)
2605 """
2606 if current:
2607 para = current.data(0, self.__diffFileLineRole)
2608 if para is not None:
2609 if para == 0:
2610 tc = self.diffEdit.textCursor()
2611 tc.movePosition(QTextCursor.MoveOperation.Start)
2612 self.diffEdit.setTextCursor(tc)
2613 self.diffEdit.ensureCursorVisible()
2614 elif para == -1:
2615 tc = self.diffEdit.textCursor()
2616 tc.movePosition(QTextCursor.MoveOperation.End)
2617 self.diffEdit.setTextCursor(tc)
2618 self.diffEdit.ensureCursorVisible()
2619 else:
2620 # step 1: move cursor to end
2621 tc = self.diffEdit.textCursor()
2622 tc.movePosition(QTextCursor.MoveOperation.End)
2623 self.diffEdit.setTextCursor(tc)
2624 self.diffEdit.ensureCursorVisible()
2625
2626 # step 2: move cursor to desired line
2627 tc = self.diffEdit.textCursor()
2628 delta = tc.blockNumber() - para
2629 tc.movePosition(QTextCursor.MoveOperation.PreviousBlock,
2630 QTextCursor.MoveMode.MoveAnchor, delta)
2631 self.diffEdit.setTextCursor(tc)
2632 self.diffEdit.ensureCursorVisible()
2633
2634 @pyqtSlot(str)
2635 def on_diffSelectLabel_linkActivated(self, link):
2636 """
2637 Private slot to handle the selection of a diff target.
2638
2639 @param link activated link
2640 @type str
2641 """
2642 if ":" in link:
2643 scheme, parent = link.split(":", 1)
2644 if scheme == "diff":
2645 with contextlib.suppress(ValueError):
2646 parent = int(parent)
2647 self.__generateDiffs(parent)
2648
2649 @pyqtSlot(str)
2650 def on_saveLabel_linkActivated(self, link):
2651 """
2652 Private slot to handle the selection of the save link.
2653
2654 @param link activated link
2655 @type str
2656 """
2657 if ":" not in link:
2658 return
2659
2660 scheme, rest = link.split(":", 1)
2661 if scheme != "save" or rest != "me":
2662 return
2663
2664 if self.projectMode:
2665 if self.__filename is None:
2666 fname = "{0}.diff".format(os.path.splitext(
2667 ericApp().getObject("Project").getProjectFile())[0])
2668 else:
2669 fname = self.vcs.splitPath(self.__filename)[0]
2670 fname += "/{0}.diff".format(os.path.split(fname)[-1])
2671 else:
2672 dname, fname = self.vcs.splitPath(self.__filename)
2673 if fname != '.':
2674 fname = "{0}.diff".format(self.__filename)
2675 else:
2676 fname = dname
2677
2678 fname, selectedFilter = EricFileDialog.getSaveFileNameAndFilter(
2679 self,
2680 self.tr("Save Diff"),
2681 fname,
2682 self.tr("Patch Files (*.diff)"),
2683 None,
2684 EricFileDialog.DontConfirmOverwrite)
2685
2686 if not fname:
2687 return # user aborted
2688
2689 fpath = pathlib.Path(fname)
2690 if not fpath.suffix:
2691 ex = selectedFilter.split("(*")[1].split(")")[0]
2692 if ex:
2693 fpath = fpath.with_suffix(ex)
2694 if fpath.exists():
2695 res = EricMessageBox.yesNo(
2696 self,
2697 self.tr("Save Diff"),
2698 self.tr("<p>The patch file <b>{0}</b> already exists."
2699 " Overwrite it?</p>").format(fpath),
2700 icon=EricMessageBox.Warning)
2701 if not res:
2702 return
2703
2704 eol = ericApp().getObject("Project").getEolString()
2705 try:
2706 with fpath.open("w", encoding="utf-8", newline="") as f:
2707 f.write(eol.join(self.diffEdit.toPlainText().splitlines()))
2708 except OSError as why:
2709 EricMessageBox.critical(
2710 self, self.tr('Save Diff'),
2711 self.tr(
2712 '<p>The patch file <b>{0}</b> could not be saved.'
2713 '<br>Reason: {1}</p>')
2714 .format(fpath, str(why)))
2715
2716 @pyqtSlot(str)
2717 def on_sbsSelectLabel_linkActivated(self, link):
2718 """
2719 Private slot to handle selection of a side-by-side link.
2720
2721 @param link text of the selected link
2722 @type str
2723 """
2724 if ":" in link and self.__filename is not None:
2725 scheme, path = link.split(":", 1)
2726 if scheme == "sbsdiff" and "_" in path:
2727 rev1, rev2 = path.split("_", 1)
2728 self.vcs.vcsSbsDiff(self.__filename, revisions=(rev1, rev2))

eric ide

mercurial