eric6/Plugins/VcsPlugins/vcsMercurial/HgLogBrowserDialog.py

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

eric ide

mercurial