eric6/Plugins/VcsPlugins/vcsMercurial/HgLogBrowserDialog.py

branch
maintenance
changeset 6989
8b8cadf8d7e9
parent 6986
23886c1dd3d0
child 7009
eaf5ed6ef298
equal deleted inserted replaced
6938:7926553b7509 6989:8b8cadf8d7e9
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 if self.commandMode == "incoming":
681 # incoming, draw a down arrow
682 painter.drawConvexPolygon(
683 QPoint(dot_x, dot_y),
684 QPoint(dot_x + 2 * offset, dot_y),
685 QPoint(dot_x + offset, dot_y + 2 * offset)
686 )
687 else:
688 # outgoing, draw an up arrow
689 painter.drawConvexPolygon(
690 QPoint(dot_x + offset, dot_y),
691 QPoint(dot_x, dot_y + 2 * offset),
692 QPoint(dot_x + 2 * offset, dot_y + 2 * offset)
693 )
694 else:
695 painter.drawEllipse(dot_x, dot_y, radius, radius)
696 painter.end()
697 return QIcon(pix)
698
699 def __getParents(self, rev):
700 """
701 Private method to get the parents of the currently viewed
702 file/directory.
703
704 @param rev revision number to get parents for (string)
705 @return list of parent revisions (list of integers)
706 """
707 errMsg = ""
708 parents = [-1]
709
710 if int(rev) > 0:
711 args = self.vcs.initCommand("parents")
712 if self.commandMode == "incoming":
713 if self.__bundle:
714 args.append("--repository")
715 args.append(self.__bundle)
716 elif self.vcs.bundleFile and \
717 os.path.exists(self.vcs.bundleFile):
718 args.append("--repository")
719 args.append(self.vcs.bundleFile)
720 args.append("--template")
721 args.append("{rev}\n")
722 args.append("-r")
723 args.append(rev)
724 if not self.projectMode:
725 args.append(self.__filename)
726
727 output = ""
728 if self.__hgClient:
729 output, errMsg = self.__hgClient.runcommand(args)
730 else:
731 process = QProcess()
732 process.setWorkingDirectory(self.repodir)
733 process.start('hg', args)
734 procStarted = process.waitForStarted(5000)
735 if procStarted:
736 finished = process.waitForFinished(30000)
737 if finished and process.exitCode() == 0:
738 output = str(process.readAllStandardOutput(),
739 self.vcs.getEncoding(), 'replace')
740 else:
741 if not finished:
742 errMsg = self.tr(
743 "The hg process did not finish within 30s.")
744 else:
745 errMsg = self.tr("Could not start the hg executable.")
746
747 if errMsg:
748 E5MessageBox.critical(
749 self,
750 self.tr("Mercurial Error"),
751 errMsg)
752
753 if output:
754 parents = [int(p) for p in output.strip().splitlines()]
755
756 return parents
757
758 def __identifyProject(self):
759 """
760 Private method to determine the revision of the project directory.
761 """
762 errMsg = ""
763
764 args = self.vcs.initCommand("identify")
765 args.append("-nb")
766
767 output = ""
768 if self.__hgClient:
769 output, errMsg = self.__hgClient.runcommand(args)
770 else:
771 process = QProcess()
772 process.setWorkingDirectory(self.repodir)
773 process.start('hg', args)
774 procStarted = process.waitForStarted(5000)
775 if procStarted:
776 finished = process.waitForFinished(30000)
777 if finished and process.exitCode() == 0:
778 output = str(process.readAllStandardOutput(),
779 self.vcs.getEncoding(), 'replace')
780 else:
781 if not finished:
782 errMsg = self.tr(
783 "The hg process did not finish within 30s.")
784 else:
785 errMsg = self.tr("Could not start the hg executable.")
786
787 if errMsg:
788 E5MessageBox.critical(
789 self,
790 self.tr("Mercurial Error"),
791 errMsg)
792
793 if output:
794 outputList = output.strip().split(None, 1)
795 if len(outputList) == 2:
796 self.__projectRevision = outputList[0].strip()
797 if self.__projectRevision.endswith("+"):
798 self.__projectRevision = self.__projectRevision[:-1]
799 self.__projectBranch = outputList[1].strip()
800
801 def __getClosedBranches(self):
802 """
803 Private method to get the list of closed branches.
804 """
805 self.__closedBranchesRevs = []
806 errMsg = ""
807
808 args = self.vcs.initCommand("branches")
809 args.append("--closed")
810
811 output = ""
812 if self.__hgClient:
813 output, errMsg = self.__hgClient.runcommand(args)
814 else:
815 process = QProcess()
816 process.setWorkingDirectory(self.repodir)
817 process.start('hg', args)
818 procStarted = process.waitForStarted(5000)
819 if procStarted:
820 finished = process.waitForFinished(30000)
821 if finished and process.exitCode() == 0:
822 output = str(process.readAllStandardOutput(),
823 self.vcs.getEncoding(), 'replace')
824 else:
825 if not finished:
826 errMsg = self.tr(
827 "The hg process did not finish within 30s.")
828 else:
829 errMsg = self.tr("Could not start the hg executable.")
830
831 if errMsg:
832 E5MessageBox.critical(
833 self,
834 self.tr("Mercurial Error"),
835 errMsg)
836
837 if output:
838 for line in output.splitlines():
839 if line.strip().endswith("(closed)"):
840 parts = line.split()
841 self.__closedBranchesRevs.append(
842 parts[-2].split(":", 1)[0])
843
844 def __getRevisionOfTag(self, tag):
845 """
846 Private method to get the revision of a tag.
847
848 @param tag tag name
849 @type str
850 @return tuple containing the revision and changeset ID
851 @rtype tuple of (str, str)
852 """
853 errMsg = ""
854
855 args = self.vcs.initCommand("tags")
856
857 output = ""
858 if self.__hgClient:
859 output, errMsg = self.__hgClient.runcommand(args)
860 else:
861 process = QProcess()
862 process.setWorkingDirectory(self.repodir)
863 process.start('hg', args)
864 procStarted = process.waitForStarted(5000)
865 if procStarted:
866 finished = process.waitForFinished(30000)
867 if finished and process.exitCode() == 0:
868 output = str(process.readAllStandardOutput(),
869 self.vcs.getEncoding(), 'replace')
870 else:
871 if not finished:
872 errMsg = self.tr(
873 "The hg process did not finish within 30s.")
874 else:
875 errMsg = self.tr("Could not start the hg executable.")
876
877 if errMsg:
878 E5MessageBox.critical(
879 self,
880 self.tr("Mercurial Error"),
881 errMsg)
882
883 res = ("", "")
884 if output:
885 for line in output.splitlines():
886 if line.strip():
887 try:
888 name, rev = line.strip().rsplit(None, 1)
889 if name == tag:
890 res = tuple(rev.split(":", 1))
891 break
892 except ValueError:
893 # ignore silently
894 pass
895
896 return res
897
898 def __generateLogItem(self, author, date, message, revision, changedPaths,
899 parents, branches, tags, phase, bookmarks,
900 latestTag):
901 """
902 Private method to generate a log tree entry.
903
904 @param author author info (string)
905 @param date date info (string)
906 @param message text of the log message (list of strings)
907 @param revision revision info (string)
908 @param changedPaths list of dictionary objects containing
909 info about the changed files/directories
910 @param parents list of parent revisions (list of integers)
911 @param branches list of branches (list of strings)
912 @param tags list of tags (string)
913 @param phase phase of the entry (string)
914 @param bookmarks list of bookmarks (string)
915 @param latestTag the latest tag(s) reachable from the changeset
916 (list of strings)
917 @return reference to the generated item (QTreeWidgetItem)
918 """
919 logMessageColumnWidth = self.vcs.getPlugin().getPreferences(
920 "LogMessageColumnWidth")
921 msgtxt = ""
922 for line in message:
923 if ". " in line:
924 msgtxt += " " + line.strip().split(". ", 1)[0] + "."
925 break
926 else:
927 msgtxt += " " + line.strip()
928 if len(msgtxt) > logMessageColumnWidth:
929 msgtxt = "{0}...".format(msgtxt[:logMessageColumnWidth])
930
931 rev, node = revision.split(":")
932 if rev in self.__closedBranchesRevs:
933 closedStr = self.ClosedIndicator
934 else:
935 closedStr = ""
936 if phase in self.phases:
937 phaseStr = self.phases[phase]
938 else:
939 phaseStr = phase
940 columnLabels = [
941 "",
942 branches[0] + closedStr,
943 "{0:>7}:{1}".format(rev, node),
944 phaseStr,
945 author,
946 date,
947 msgtxt,
948 ", ".join(tags),
949 ]
950 if bookmarks is not None:
951 columnLabels.append(", ".join(bookmarks))
952 itm = QTreeWidgetItem(self.logTree, columnLabels)
953
954 itm.setForeground(self.BranchColumn,
955 QBrush(QColor(self.__branchColor(branches[0]))))
956
957 if not self.projectMode:
958 parents = self.__getParents(rev)
959 if not parents:
960 parents = [int(rev) - 1]
961 column, color, edges = self.__generateEdges(int(rev), parents)
962
963 itm.setData(0, self.__messageRole, message)
964 itm.setData(0, self.__changesRole, changedPaths)
965 itm.setData(0, self.__edgesRole, edges)
966 itm.setData(0, self.__latestTagRole, latestTag)
967 if parents == [-1]:
968 itm.setData(0, self.__parentsRole, [])
969 else:
970 itm.setData(0, self.__parentsRole, parents)
971 for parent in parents:
972 self.__childrenInfo[parent].append(int(rev))
973
974 if self.logTree.topLevelItemCount() > 1:
975 topedges = \
976 self.logTree.topLevelItem(
977 self.logTree.indexOfTopLevelItem(itm) - 1)\
978 .data(0, self.__edgesRole)
979 else:
980 topedges = None
981
982 icon = self.__generateIcon(column, color, edges, topedges,
983 QColor(self.__branchColor(branches[0])),
984 rev == self.__projectRevision,
985 rev in self.__closedBranchesRevs)
986 itm.setIcon(0, icon)
987
988 try:
989 self.__lastRev = int(revision.split(":")[0])
990 except ValueError:
991 self.__lastRev = 0
992
993 return itm
994
995 def __getLogEntries(self, startRev=None, noEntries=0):
996 """
997 Private method to retrieve log entries from the repository.
998
999 @param startRev revision number to start from (integer, string)
1000 @keyparam noEntries number of entries to get (0 = default) (int)
1001 """
1002 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
1003 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
1004 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
1005 QApplication.processEvents()
1006
1007 QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
1008 QApplication.processEvents()
1009
1010 self.buf = []
1011 self.cancelled = False
1012 self.errors.clear()
1013 self.intercept = False
1014
1015 if noEntries == 0:
1016 noEntries = self.limitSpinBox.value()
1017
1018 preargs = []
1019 args = self.vcs.initCommand(self.commandMode)
1020 args.append('--verbose')
1021 if self.commandMode not in ("incoming", "outgoing"):
1022 args.append('--limit')
1023 args.append(str(noEntries))
1024 if self.commandMode in ("incoming", "outgoing"):
1025 args.append("--newest-first")
1026 if self.vcs.hasSubrepositories():
1027 args.append("--subrepos")
1028 if startRev is not None:
1029 args.append('--rev')
1030 args.append('{0}:0'.format(startRev))
1031 if not self.projectMode and \
1032 not self.fname == "." and \
1033 not self.stopCheckBox.isChecked():
1034 args.append('--follow')
1035 if self.commandMode == "log":
1036 args.append('--copies')
1037 args.append('--template')
1038 args.append(os.path.join(os.path.dirname(__file__),
1039 "templates",
1040 "logBrowserBookmarkPhase.tmpl"))
1041 if self.commandMode == "incoming":
1042 if self.__bundle:
1043 args.append(self.__bundle)
1044 elif not self.vcs.hasSubrepositories():
1045 project = e5App().getObject("Project")
1046 self.vcs.bundleFile = os.path.join(
1047 project.getProjectManagementDir(), "hg-bundle.hg")
1048 if os.path.exists(self.vcs.bundleFile):
1049 os.remove(self.vcs.bundleFile)
1050 preargs = args[:]
1051 preargs.append("--quiet")
1052 preargs.append('--bundle')
1053 preargs.append(self.vcs.bundleFile)
1054 args.append(self.vcs.bundleFile)
1055 if not self.projectMode:
1056 args.append(self.__filename)
1057
1058 if self.__hgClient:
1059 self.inputGroup.setEnabled(False)
1060 self.inputGroup.hide()
1061
1062 if preargs:
1063 out, err = self.__hgClient.runcommand(preargs)
1064 else:
1065 err = ""
1066 if err:
1067 self.__showError(err)
1068 elif self.commandMode != "incoming" or \
1069 (self.vcs.bundleFile and
1070 os.path.exists(self.vcs.bundleFile)) or \
1071 self.__bundle:
1072 out, err = self.__hgClient.runcommand(args)
1073 self.buf = out.splitlines(True)
1074 if err:
1075 self.__showError(err)
1076 self.__processBuffer()
1077 self.__finish()
1078 else:
1079 self.process.kill()
1080
1081 self.process.setWorkingDirectory(self.repodir)
1082
1083 if preargs:
1084 process = QProcess()
1085 process.setWorkingDirectory(self.repodir)
1086 process.start('hg', args)
1087 procStarted = process.waitForStarted(5000)
1088 if procStarted:
1089 process.waitForFinished(30000)
1090
1091 if self.commandMode != "incoming" or \
1092 (self.vcs.bundleFile and
1093 os.path.exists(self.vcs.bundleFile)) or \
1094 self.__bundle:
1095 self.process.start('hg', args)
1096 procStarted = self.process.waitForStarted(5000)
1097 if not procStarted:
1098 self.inputGroup.setEnabled(False)
1099 self.inputGroup.hide()
1100 E5MessageBox.critical(
1101 self,
1102 self.tr('Process Generation Error'),
1103 self.tr(
1104 'The process {0} could not be started. '
1105 'Ensure, that it is in the search path.'
1106 ).format('hg'))
1107 else:
1108 self.__finish()
1109
1110 def start(self, fn, bundle=None, isFile=False, noEntries=0):
1111 """
1112 Public slot to start the hg log command.
1113
1114 @param fn filename to show the log for (string)
1115 @keyparam bundle name of a bundle file (string)
1116 @keyparam isFile flag indicating log for a file is to be shown
1117 (boolean)
1118 @keyparam noEntries number of entries to get (0 = default) (int)
1119 """
1120 self.__bundle = bundle
1121 self.__isFile = isFile
1122
1123 self.sbsSelectLabel.clear()
1124
1125 self.errorGroup.hide()
1126 QApplication.processEvents()
1127
1128 self.__initData()
1129
1130 self.__filename = fn
1131 self.dname, self.fname = self.vcs.splitPath(fn)
1132
1133 # find the root of the repo
1134 self.repodir = self.dname
1135 while not os.path.isdir(os.path.join(self.repodir, self.vcs.adminDir)):
1136 self.repodir = os.path.dirname(self.repodir)
1137 if os.path.splitdrive(self.repodir)[1] == os.sep:
1138 return
1139
1140 self.projectMode = (self.fname == "." and self.dname == self.repodir)
1141 self.stopCheckBox.setDisabled(self.projectMode or self.fname == ".")
1142 self.activateWindow()
1143 self.raise_()
1144
1145 self.logTree.clear()
1146 self.__started = True
1147 self.__identifyProject()
1148 self.__getClosedBranches()
1149 self.__getLogEntries(noEntries=noEntries)
1150
1151 def __procFinished(self, exitCode, exitStatus):
1152 """
1153 Private slot connected to the finished signal.
1154
1155 @param exitCode exit code of the process (integer)
1156 @param exitStatus exit status of the process (QProcess.ExitStatus)
1157 """
1158 self.__processBuffer()
1159 self.__finish()
1160
1161 def __finish(self):
1162 """
1163 Private slot called when the process finished or the user pressed
1164 the button.
1165 """
1166 if self.process is not None and \
1167 self.process.state() != QProcess.NotRunning:
1168 self.process.terminate()
1169 QTimer.singleShot(2000, self.process.kill)
1170 self.process.waitForFinished(3000)
1171
1172 QApplication.restoreOverrideCursor()
1173
1174 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
1175 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
1176 self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
1177
1178 self.inputGroup.setEnabled(False)
1179 self.inputGroup.hide()
1180 self.refreshButton.setEnabled(True)
1181
1182 while self.__finishCallbacks:
1183 self.__finishCallbacks.pop(0)()
1184
1185 def __modifyForLargeFiles(self, filename):
1186 """
1187 Private method to convert the displayed file name for a large file.
1188
1189 @param filename file name to be processed (string)
1190 @return processed file name (string)
1191 """
1192 if filename.startswith((self.LargefilesCacheL, self.LargefilesCacheW)):
1193 return self.tr("{0} (large file)").format(
1194 self.PathSeparatorRe.split(filename, 1)[1])
1195 else:
1196 return filename
1197
1198 def __processBuffer(self):
1199 """
1200 Private method to process the buffered output of the hg log command.
1201 """
1202 noEntries = 0
1203 log = {"message": [], "bookmarks": None, "phase": ""}
1204 changedPaths = []
1205 initialText = True
1206 fileCopies = {}
1207 for s in self.buf:
1208 if s != "@@@\n":
1209 try:
1210 key, value = s.split("|", 1)
1211 except ValueError:
1212 key = ""
1213 value = s
1214 if key == "change":
1215 initialText = False
1216 log["revision"] = value.strip()
1217 elif key == "user":
1218 log["author"] = value.strip()
1219 elif key == "parents":
1220 log["parents"] = \
1221 [int(x.split(":", 1)[0])
1222 for x in value.strip().split()]
1223 elif key == "date":
1224 log["date"] = " ".join(value.strip().split()[:2])
1225 elif key == "description":
1226 log["message"].append(value.strip())
1227 elif key == "file_adds":
1228 if value.strip():
1229 for f in value.strip().split(", "):
1230 if f in fileCopies:
1231 changedPaths.append({
1232 "action": "A",
1233 "path": self.__modifyForLargeFiles(f),
1234 "copyfrom": self.__modifyForLargeFiles(
1235 fileCopies[f]),
1236 })
1237 else:
1238 changedPaths.append({
1239 "action": "A",
1240 "path": self.__modifyForLargeFiles(f),
1241 "copyfrom": "",
1242 })
1243 elif key == "files_mods":
1244 if value.strip():
1245 for f in value.strip().split(", "):
1246 changedPaths.append({
1247 "action": "M",
1248 "path": self.__modifyForLargeFiles(f),
1249 "copyfrom": "",
1250 })
1251 elif key == "file_dels":
1252 if value.strip():
1253 for f in value.strip().split(", "):
1254 changedPaths.append({
1255 "action": "D",
1256 "path": self.__modifyForLargeFiles(f),
1257 "copyfrom": "",
1258 })
1259 elif key == "file_copies":
1260 if value.strip():
1261 for entry in value.strip().split(", "):
1262 newName, oldName = entry[:-1].split(" (")
1263 fileCopies[newName] = oldName
1264 elif key == "branches":
1265 if value.strip():
1266 log["branches"] = value.strip().split(", ")
1267 else:
1268 log["branches"] = ["default"]
1269 elif key == "tags":
1270 log["tags"] = value.strip().split(", ")
1271 elif key == "bookmarks":
1272 log["bookmarks"] = value.strip().split(", ")
1273 elif key == "phase":
1274 log["phase"] = value.strip()
1275 elif key == "latesttag":
1276 tag = value.strip()
1277 if tag == "null":
1278 log["latesttag"] = []
1279 elif ":" in tag:
1280 log["latesttag"] = [
1281 t.strip() for t in tag.split(":") if t.strip()]
1282 else:
1283 log["latesttag"] = [tag]
1284 else:
1285 if initialText:
1286 continue
1287 if value.strip():
1288 log["message"].append(value.strip())
1289 else:
1290 if len(log) > 1:
1291 self.__generateLogItem(
1292 log["author"], log["date"],
1293 log["message"], log["revision"], changedPaths,
1294 log["parents"], log["branches"], log["tags"],
1295 log["phase"], log["bookmarks"], log["latesttag"])
1296 dt = QDate.fromString(log["date"], Qt.ISODate)
1297 if not self.__maxDate.isValid() and \
1298 not self.__minDate.isValid():
1299 self.__maxDate = dt
1300 self.__minDate = dt
1301 else:
1302 if self.__maxDate < dt:
1303 self.__maxDate = dt
1304 if self.__minDate > dt:
1305 self.__minDate = dt
1306 noEntries += 1
1307 log = {"message": [], "bookmarks": None, "phase": ""}
1308 changedPaths = []
1309 fileCopies = {}
1310
1311 self.__resizeColumnsLog()
1312
1313 if self.__started:
1314 if self.__selectedRevisions:
1315 foundItems = self.logTree.findItems(
1316 self.__selectedRevisions[0], Qt.MatchExactly,
1317 self.RevisionColumn)
1318 if foundItems:
1319 self.logTree.setCurrentItem(foundItems[0])
1320 else:
1321 self.logTree.setCurrentItem(self.logTree.topLevelItem(0))
1322 else:
1323 self.logTree.setCurrentItem(self.logTree.topLevelItem(0))
1324 self.__started = False
1325
1326 if self.commandMode in ("incoming", "outgoing"):
1327 self.commandMode = "log" # switch to log mode
1328 if self.__lastRev > 0:
1329 self.nextButton.setEnabled(True)
1330 self.limitSpinBox.setEnabled(True)
1331 else:
1332 if noEntries < self.limitSpinBox.value() and not self.cancelled:
1333 self.nextButton.setEnabled(False)
1334 self.limitSpinBox.setEnabled(False)
1335
1336 # update the log filters
1337 self.__filterLogsEnabled = False
1338 self.fromDate.setMinimumDate(self.__minDate)
1339 self.fromDate.setMaximumDate(self.__maxDate)
1340 self.fromDate.setDate(self.__minDate)
1341 self.toDate.setMinimumDate(self.__minDate)
1342 self.toDate.setMaximumDate(self.__maxDate)
1343 self.toDate.setDate(self.__maxDate)
1344
1345 branchFilter = self.branchCombo.currentText()
1346 if not branchFilter:
1347 branchFilter = self.__allBranchesFilter
1348 self.branchCombo.clear()
1349 self.branchCombo.addItems(
1350 [self.__allBranchesFilter] + sorted(self.__branchColors.keys()))
1351 self.branchCombo.setCurrentIndex(
1352 self.branchCombo.findText(branchFilter))
1353
1354 self.__filterLogsEnabled = True
1355 if self.__actionMode() == "filter":
1356 self.__filterLogs()
1357 self.__updateToolMenuActions()
1358
1359 # restore current item
1360 if self.__selectedRevisions:
1361 for revision in self.__selectedRevisions:
1362 items = self.logTree.findItems(
1363 revision, Qt.MatchExactly, self.RevisionColumn)
1364 if items:
1365 items[0].setSelected(True)
1366 self.__selectedRevisions = []
1367
1368 def __readStdout(self):
1369 """
1370 Private slot to handle the readyReadStandardOutput signal.
1371
1372 It reads the output of the process and inserts it into a buffer.
1373 """
1374 self.process.setReadChannel(QProcess.StandardOutput)
1375
1376 while self.process.canReadLine():
1377 line = str(self.process.readLine(), self.vcs.getEncoding(),
1378 'replace')
1379 self.buf.append(line)
1380
1381 def __readStderr(self):
1382 """
1383 Private slot to handle the readyReadStandardError signal.
1384
1385 It reads the error output of the process and inserts it into the
1386 error pane.
1387 """
1388 if self.process is not None:
1389 s = str(self.process.readAllStandardError(),
1390 self.vcs.getEncoding(), 'replace')
1391 self.__showError(s)
1392
1393 def __showError(self, out):
1394 """
1395 Private slot to show some error.
1396
1397 @param out error to be shown (string)
1398 """
1399 self.errorGroup.show()
1400 self.errors.insertPlainText(out)
1401 self.errors.ensureCursorVisible()
1402
1403 if not self.__hgClient:
1404 # show input in case the process asked for some input
1405 self.inputGroup.setEnabled(True)
1406 self.inputGroup.show()
1407
1408 def on_buttonBox_clicked(self, button):
1409 """
1410 Private slot called by a button of the button box clicked.
1411
1412 @param button button that was clicked (QAbstractButton)
1413 """
1414 if button == self.buttonBox.button(QDialogButtonBox.Close):
1415 self.close()
1416 elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
1417 self.cancelled = True
1418 if self.__hgClient:
1419 self.__hgClient.cancel()
1420 else:
1421 self.__finish()
1422 elif button == self.refreshButton:
1423 self.on_refreshButton_clicked()
1424
1425 def __updateSbsSelectLabel(self):
1426 """
1427 Private slot to update the enabled status of the diff buttons.
1428 """
1429 self.sbsSelectLabel.clear()
1430 if self.__isFile:
1431 selectedItems = self.logTree.selectedItems()
1432 if len(selectedItems) == 1:
1433 currentItem = selectedItems[0]
1434 rev2 = currentItem.text(self.RevisionColumn).split(":", 1)[0]\
1435 .strip()
1436 parents = currentItem.data(0, self.__parentsRole)
1437 if parents:
1438 parentLinks = []
1439 for index in range(len(parents)):
1440 parentLinks.append(
1441 '<a href="sbsdiff:{0}_{1}">&nbsp;{2}&nbsp;</a>'
1442 .format(parents[index], rev2, index + 1))
1443 self.sbsSelectLabel.setText(
1444 self.tr('Side-by-Side Diff to Parent {0}').format(
1445 " ".join(parentLinks)))
1446 elif len(selectedItems) == 2:
1447 rev1 = int(selectedItems[0].text(self.RevisionColumn)
1448 .split(":", 1)[0])
1449 rev2 = int(selectedItems[1].text(self.RevisionColumn)
1450 .split(":", 1)[0])
1451 if rev1 > rev2:
1452 # Swap the entries, so that rev1 < rev2
1453 rev1, rev2 = rev2, rev1
1454 self.sbsSelectLabel.setText(self.tr(
1455 '<a href="sbsdiff:{0}_{1}">Side-by-Side Compare</a>')
1456 .format(rev1, rev2))
1457
1458 def __updateToolMenuActions(self):
1459 """
1460 Private slot to update the status of the tool menu actions and
1461 the tool menu button.
1462 """
1463 if self.initialCommandMode == "log" and self.projectMode:
1464 # do the phase action
1465 # step 1: count entries with changeable phases
1466 secret = 0
1467 draft = 0
1468 public = 0
1469 for itm in self.logTree.selectedItems():
1470 phase = itm.text(self.PhaseColumn)
1471 if phase == self.phases["draft"]:
1472 draft += 1
1473 elif phase == self.phases["secret"]:
1474 secret += 1
1475 else:
1476 public += 1
1477
1478 # step 2: set the status of the phase action
1479 if public == 0 and \
1480 ((secret > 0 and draft == 0) or
1481 (secret == 0 and draft > 0)):
1482 self.__phaseAct.setEnabled(True)
1483 else:
1484 self.__phaseAct.setEnabled(False)
1485
1486 # do the graft action
1487 # step 1: count selected entries not belonging to the
1488 # current branch
1489 otherBranches = 0
1490 for itm in self.logTree.selectedItems():
1491 branch = itm.text(self.BranchColumn)
1492 if branch != self.__projectBranch:
1493 otherBranches += 1
1494
1495 # step 2: set the status of the graft action
1496 self.__graftAct.setEnabled(otherBranches > 0)
1497
1498 selectedItemsCount = len(self.logTree.selectedItems())
1499 self.__mergeAct.setEnabled(selectedItemsCount == 1)
1500 self.__tagAct.setEnabled(selectedItemsCount == 1)
1501 self.__switchAct.setEnabled(selectedItemsCount == 1)
1502 self.__bookmarkAct.setEnabled(selectedItemsCount == 1)
1503 self.__bookmarkMoveAct.setEnabled(selectedItemsCount == 1)
1504
1505 self.__pullAct.setText(self.tr("Pull Changes"))
1506 self.__fetchAct.setText(self.tr("Fetch Changes"))
1507 if self.vcs.canPull():
1508 self.__pullAct.setEnabled(True)
1509 self.__lfPullAct.setEnabled(
1510 self.vcs.isExtensionActive("largefiles") and
1511 selectedItemsCount > 0)
1512 self.__fetchAct.setEnabled(
1513 self.vcs.isExtensionActive("fetch"))
1514 else:
1515 self.__pullAct.setEnabled(False)
1516 self.__lfPullAct.setEnabled(False)
1517 self.__fetchAct.setEnabled(False)
1518
1519 if self.vcs.canPush():
1520 self.__pushAct.setEnabled(
1521 selectedItemsCount == 1 and
1522 self.logTree.selectedItems()[0].text(self.PhaseColumn) ==
1523 self.phases["draft"])
1524 self.__pushAllAct.setEnabled(True)
1525 else:
1526 self.__pushAct.setEnabled(False)
1527 self.__pushAllAct.setEnabled(False)
1528
1529 self.__stripAct.setEnabled(
1530 self.vcs.isExtensionActive("strip") and
1531 selectedItemsCount == 1)
1532
1533 self.__bundleAct.setEnabled(self.logTree.topLevelItemCount() > 0)
1534 self.__unbundleAct.setEnabled(False)
1535
1536 self.__gpgSignAct.setEnabled(
1537 self.vcs.isExtensionActive("gpg") and
1538 selectedItemsCount > 0)
1539 self.__gpgVerifyAct.setEnabled(
1540 self.vcs.isExtensionActive("gpg") and
1541 selectedItemsCount == 1)
1542
1543 self.actionsButton.setEnabled(True)
1544
1545 elif self.initialCommandMode == "incoming" and self.projectMode:
1546 for act in [self.__phaseAct, self.__graftAct, self.__mergeAct,
1547 self.__tagAct, self.__switchAct, self.__bookmarkAct,
1548 self.__bookmarkMoveAct, self.__pushAct,
1549 self.__pushAllAct, self.__stripAct, self.__bundleAct,
1550 self.__gpgSignAct, self.__gpgVerifyAct]:
1551 act.setEnabled(False)
1552
1553 self.__pullAct.setText(self.tr("Pull Selected Changes"))
1554 self.__fetchAct.setText(self.tr("Fetch Selected Changes"))
1555 if self.vcs.canPull() and not bool(self.__bundle):
1556 # step 1: determine number of selected draft changesets
1557 # i.e. those that can be pulled
1558 selectedDraftItemsCount = 0
1559 for itm in self.logTree.selectedItems():
1560 phase = itm.text(self.PhaseColumn)
1561 if phase == self.phases["draft"]:
1562 selectedDraftItemsCount += 1
1563 self.__pullAct.setEnabled(selectedDraftItemsCount > 0)
1564 self.__lfPullAct.setEnabled(
1565 self.vcs.isExtensionActive("largefiles") and
1566 selectedItemsCount > 0)
1567 self.__fetchAct.setEnabled(
1568 self.vcs.isExtensionActive("fetch") and
1569 selectedDraftItemsCount > 0)
1570 else:
1571 self.__pullAct.setEnabled(False)
1572 self.__lfPullAct.setEnabled(False)
1573 self.__fetchAct.setEnabled(False)
1574
1575 self.__unbundleAct.setEnabled(bool(self.__bundle))
1576
1577 self.actionsButton.setEnabled(True)
1578
1579 elif self.initialCommandMode == "outgoing" and self.projectMode:
1580 for act in [self.__phaseAct, self.__graftAct, self.__mergeAct,
1581 self.__tagAct, self.__switchAct, self.__bookmarkAct,
1582 self.__bookmarkMoveAct, self.__pullAct,
1583 self.__lfPullAct, self.__fetchAct, self.__stripAct,
1584 self.__gpgSignAct, self.__gpgVerifyAct,
1585 self.__unbundleAct]:
1586 act.setEnabled(False)
1587
1588 selectedItemsCount = len(self.logTree.selectedItems())
1589 if self.vcs.canPush():
1590 self.__pushAct.setEnabled(
1591 selectedItemsCount == 1 and
1592 self.logTree.selectedItems()[0].text(self.PhaseColumn) ==
1593 self.phases["draft"])
1594 self.__pushAllAct.setEnabled(True)
1595 else:
1596 self.__pushAct.setEnabled(False)
1597 self.__pushAllAct.setEnabled(False)
1598
1599 self.__bundleAct.setEnabled(selectedItemsCount > 0)
1600
1601 else:
1602 self.actionsButton.setEnabled(False)
1603
1604 def __updateDetailsAndFiles(self):
1605 """
1606 Private slot to update the details and file changes panes.
1607 """
1608 self.detailsEdit.clear()
1609 self.filesTree.clear()
1610 self.__diffUpdatesFiles = False
1611
1612 selectedItems = self.logTree.selectedItems()
1613 if len(selectedItems) == 1:
1614 self.detailsEdit.setHtml(
1615 self.__generateDetailsTableText(selectedItems[0]))
1616 self.__updateFilesTree(self.filesTree, selectedItems[0])
1617 self.__resizeColumnsFiles()
1618 self.__resortFiles()
1619 elif len(selectedItems) == 2:
1620 self.__diffUpdatesFiles = True
1621 index1 = self.logTree.indexOfTopLevelItem(selectedItems[0])
1622 index2 = self.logTree.indexOfTopLevelItem(selectedItems[1])
1623 if index1 > index2:
1624 # Swap the entries
1625 selectedItems[0], selectedItems[1] = \
1626 selectedItems[1], selectedItems[0]
1627 html = "{0}<hr/>{1}".format(
1628 self.__generateDetailsTableText(selectedItems[0]),
1629 self.__generateDetailsTableText(selectedItems[1]),
1630 )
1631 self.detailsEdit.setHtml(html)
1632 # self.filesTree is updated by the diff
1633
1634 def __generateDetailsTableText(self, itm):
1635 """
1636 Private method to generate an HTML table with the details of the given
1637 changeset.
1638
1639 @param itm reference to the item the table should be based on
1640 @type QTreeWidgetItem
1641 @return HTML table containing details
1642 @rtype str
1643 """
1644 if itm is not None:
1645 if itm.text(self.TagsColumn):
1646 tagsStr = self.__tagsTemplate.format(itm.text(self.TagsColumn))
1647 else:
1648 tagsStr = ""
1649
1650 if itm.text(self.BookmarksColumn):
1651 bookmarksStr = self.__bookmarksTemplate.format(
1652 itm.text(self.BookmarksColumn))
1653 else:
1654 bookmarksStr = ""
1655
1656 if self.projectMode and itm.data(0, self.__latestTagRole):
1657 latestTagLinks = []
1658 for tag in itm.data(0, self.__latestTagRole):
1659 latestTagLinks.append('<a href="rev:{0}">{1}</a>'.format(
1660 self.__getRevisionOfTag(tag)[0], tag))
1661 latestTagStr = self.__latestTagTemplate.format(
1662 ", ".join(latestTagLinks))
1663 else:
1664 latestTagStr = ""
1665
1666 rev = int(itm.text(self.RevisionColumn).split(":", 1)[0])
1667
1668 if itm.data(0, self.__parentsRole):
1669 parentLinks = []
1670 for parent in [str(x) for x in
1671 itm.data(0, self.__parentsRole)]:
1672 parentLinks.append(
1673 '<a href="rev:{0}">{0}</a>'.format(parent))
1674 parentsStr = self.__parentsTemplate.format(
1675 ", ".join(parentLinks))
1676 else:
1677 parentsStr = ""
1678
1679 if self.__childrenInfo[rev]:
1680 childLinks = []
1681 for child in [str(x) for x in self.__childrenInfo[rev]]:
1682 childLinks.append(
1683 '<a href="rev:{0}">{0}</a>'.format(child))
1684 childrenStr = self.__childrenTemplate.format(
1685 ", ".join(childLinks))
1686 else:
1687 childrenStr = ""
1688
1689 messageStr = "<br />\n".join([
1690 Utilities.html_encode(line.strip())
1691 for line in itm.data(0, self.__messageRole)
1692 ])
1693
1694 html = self.__detailsTemplate.format(
1695 itm.text(self.RevisionColumn),
1696 itm.text(self.DateColumn),
1697 itm.text(self.AuthorColumn),
1698 itm.text(self.BranchColumn).replace(
1699 self.ClosedIndicator, ""),
1700 parentsStr + childrenStr + tagsStr + latestTagStr +
1701 bookmarksStr,
1702 messageStr,
1703 )
1704 else:
1705 html = ""
1706
1707 return html
1708
1709 def __updateFilesTree(self, parent, itm):
1710 """
1711 Private method to update the files tree with changes of the given item.
1712
1713 @param parent parent for the items to be added
1714 @type QTreeWidget or QTreeWidgetItem
1715 @param itm reference to the item the update should be based on
1716 @type QTreeWidgetItem
1717 """
1718 if itm is not None:
1719 changes = itm.data(0, self.__changesRole)
1720 if len(changes) > 0:
1721 for change in changes:
1722 QTreeWidgetItem(parent, [
1723 self.flags[change["action"]],
1724 change["path"].strip(),
1725 change["copyfrom"].strip(),
1726 ])
1727
1728 @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem)
1729 def on_logTree_currentItemChanged(self, current, previous):
1730 """
1731 Private slot called, when the current item of the log tree changes.
1732
1733 @param current reference to the new current item (QTreeWidgetItem)
1734 @param previous reference to the old current item (QTreeWidgetItem)
1735 """
1736 self.__updateToolMenuActions()
1737
1738 # Highlight the current entry using a bold font
1739 for col in range(self.logTree.columnCount()):
1740 current and current.setFont(col, self.__logTreeBoldFont)
1741 previous and previous.setFont(col, self.__logTreeNormalFont)
1742
1743 # set the state of the up and down buttons
1744 self.upButton.setEnabled(
1745 current is not None and
1746 self.logTree.indexOfTopLevelItem(current) > 0)
1747 self.downButton.setEnabled(
1748 current is not None and
1749 int(current.text(self.RevisionColumn).split(":")[0]) > 0 and
1750 (self.logTree.indexOfTopLevelItem(current) <
1751 self.logTree.topLevelItemCount() - 1 or
1752 self.nextButton.isEnabled()))
1753
1754 @pyqtSlot()
1755 def on_logTree_itemSelectionChanged(self):
1756 """
1757 Private slot called, when the selection has changed.
1758 """
1759 self.__updateDetailsAndFiles()
1760 self.__updateSbsSelectLabel()
1761 self.__updateToolMenuActions()
1762 self.__generateDiffs()
1763
1764 @pyqtSlot()
1765 def on_upButton_clicked(self):
1766 """
1767 Private slot to move the current item up one entry.
1768 """
1769 itm = self.logTree.itemAbove(self.logTree.currentItem())
1770 if itm:
1771 self.logTree.setCurrentItem(itm)
1772
1773 @pyqtSlot()
1774 def on_downButton_clicked(self):
1775 """
1776 Private slot to move the current item down one entry.
1777 """
1778 itm = self.logTree.itemBelow(self.logTree.currentItem())
1779 if itm:
1780 self.logTree.setCurrentItem(itm)
1781 else:
1782 # load the next bunch and try again
1783 if self.nextButton.isEnabled():
1784 self.__addFinishCallback(self.on_downButton_clicked)
1785 self.on_nextButton_clicked()
1786
1787 @pyqtSlot()
1788 def on_nextButton_clicked(self):
1789 """
1790 Private slot to handle the Next button.
1791 """
1792 if self.__lastRev > 0 and self.nextButton.isEnabled():
1793 self.__getLogEntries(startRev=self.__lastRev - 1)
1794
1795 @pyqtSlot(QDate)
1796 def on_fromDate_dateChanged(self, date):
1797 """
1798 Private slot called, when the from date changes.
1799
1800 @param date new date (QDate)
1801 """
1802 if self.__actionMode() == "filter":
1803 self.__filterLogs()
1804
1805 @pyqtSlot(QDate)
1806 def on_toDate_dateChanged(self, date):
1807 """
1808 Private slot called, when the from date changes.
1809
1810 @param date new date (QDate)
1811 """
1812 if self.__actionMode() == "filter":
1813 self.__filterLogs()
1814
1815 @pyqtSlot(str)
1816 def on_branchCombo_activated(self, txt):
1817 """
1818 Private slot called, when a new branch is selected.
1819
1820 @param txt text of the selected branch (string)
1821 """
1822 if self.__actionMode() == "filter":
1823 self.__filterLogs()
1824
1825 @pyqtSlot(str)
1826 def on_fieldCombo_activated(self, txt):
1827 """
1828 Private slot called, when a new filter field is selected.
1829
1830 @param txt text of the selected field (string)
1831 """
1832 if self.__actionMode() == "filter":
1833 self.__filterLogs()
1834
1835 @pyqtSlot(str)
1836 def on_rxEdit_textChanged(self, txt):
1837 """
1838 Private slot called, when a filter expression is entered.
1839
1840 @param txt filter expression (string)
1841 """
1842 if self.__actionMode() == "filter":
1843 self.__filterLogs()
1844 elif self.__actionMode() == "find":
1845 self.__findItem(self.__findBackwards, interactive=True)
1846
1847 @pyqtSlot()
1848 def on_rxEdit_returnPressed(self):
1849 """
1850 Private slot handling a press of the Return key in the rxEdit input.
1851 """
1852 if self.__actionMode() == "find":
1853 self.__findItem(self.__findBackwards, interactive=True)
1854
1855 def __filterLogs(self):
1856 """
1857 Private method to filter the log entries.
1858 """
1859 if self.__filterLogsEnabled:
1860 from_ = self.fromDate.date().toString("yyyy-MM-dd")
1861 to_ = self.toDate.date().addDays(1).toString("yyyy-MM-dd")
1862 branch = self.branchCombo.currentText()
1863 closedBranch = branch + '--'
1864 fieldIndex, searchRx, indexIsRole = self.__prepareFieldSearch()
1865
1866 visibleItemCount = self.logTree.topLevelItemCount()
1867 currentItem = self.logTree.currentItem()
1868 for topIndex in range(self.logTree.topLevelItemCount()):
1869 topItem = self.logTree.topLevelItem(topIndex)
1870 if indexIsRole:
1871 if fieldIndex == self.__changesRole:
1872 changes = topItem.data(0, self.__changesRole)
1873 txt = "\n".join(
1874 [c["path"] for c in changes] +
1875 [c["copyfrom"] for c in changes]
1876 )
1877 else:
1878 # Find based on complete message text
1879 txt = "\n".join(topItem.data(0, self.__messageRole))
1880 else:
1881 txt = topItem.text(fieldIndex)
1882 if topItem.text(self.DateColumn) <= to_ and \
1883 topItem.text(self.DateColumn) >= from_ and \
1884 (branch == self.__allBranchesFilter or
1885 topItem.text(self.BranchColumn) in
1886 [branch, closedBranch]) and \
1887 searchRx.indexIn(txt) > -1:
1888 topItem.setHidden(False)
1889 if topItem is currentItem:
1890 self.on_logTree_currentItemChanged(topItem, None)
1891 else:
1892 topItem.setHidden(True)
1893 if topItem is currentItem:
1894 self.filesTree.clear()
1895 visibleItemCount -= 1
1896 self.logTree.header().setSectionHidden(
1897 self.IconColumn,
1898 visibleItemCount != self.logTree.topLevelItemCount())
1899
1900 def __prepareFieldSearch(self):
1901 """
1902 Private slot to prepare the filed search data.
1903
1904 @return tuple of field index, search expression and flag indicating
1905 that the field index is a data role (integer, string, boolean)
1906 """
1907 indexIsRole = False
1908 txt = self.fieldCombo.itemData(self.fieldCombo.currentIndex())
1909 if txt == "author":
1910 fieldIndex = self.AuthorColumn
1911 searchRx = QRegExp(self.rxEdit.text(), Qt.CaseInsensitive)
1912 elif txt == "revision":
1913 fieldIndex = self.RevisionColumn
1914 txt = self.rxEdit.text()
1915 if txt.startswith("^"):
1916 searchRx = QRegExp(r"^\s*{0}".format(txt[1:]),
1917 Qt.CaseInsensitive)
1918 else:
1919 searchRx = QRegExp(txt, Qt.CaseInsensitive)
1920 elif txt == "file":
1921 fieldIndex = self.__changesRole
1922 searchRx = QRegExp(self.rxEdit.text(), Qt.CaseInsensitive)
1923 indexIsRole = True
1924 else:
1925 fieldIndex = self.__messageRole
1926 searchRx = QRegExp(self.rxEdit.text(), Qt.CaseInsensitive)
1927 indexIsRole = True
1928
1929 return fieldIndex, searchRx, indexIsRole
1930
1931 @pyqtSlot(bool)
1932 def on_stopCheckBox_clicked(self, checked):
1933 """
1934 Private slot called, when the stop on copy/move checkbox is clicked.
1935
1936 @param checked flag indicating the state of the check box (boolean)
1937 """
1938 self.vcs.getPlugin().setPreferences("StopLogOnCopy",
1939 self.stopCheckBox.isChecked())
1940 self.nextButton.setEnabled(True)
1941 self.limitSpinBox.setEnabled(True)
1942
1943 @pyqtSlot()
1944 def on_refreshButton_clicked(self):
1945 """
1946 Private slot to refresh the log.
1947 """
1948 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
1949 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
1950 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
1951
1952 self.refreshButton.setEnabled(False)
1953
1954 # save the selected items commit IDs
1955 self.__selectedRevisions = []
1956 for item in self.logTree.selectedItems():
1957 self.__selectedRevisions.append(item.text(self.RevisionColumn))
1958
1959 if self.initialCommandMode in ("incoming", "outgoing"):
1960 self.nextButton.setEnabled(False)
1961 self.limitSpinBox.setEnabled(False)
1962 else:
1963 self.nextButton.setEnabled(True)
1964 self.limitSpinBox.setEnabled(True)
1965
1966 self.commandMode = self.initialCommandMode
1967 self.start(self.__filename, isFile=self.__isFile,
1968 noEntries=self.logTree.topLevelItemCount())
1969
1970 def on_passwordCheckBox_toggled(self, isOn):
1971 """
1972 Private slot to handle the password checkbox toggled.
1973
1974 @param isOn flag indicating the status of the check box (boolean)
1975 """
1976 if isOn:
1977 self.input.setEchoMode(QLineEdit.Password)
1978 else:
1979 self.input.setEchoMode(QLineEdit.Normal)
1980
1981 @pyqtSlot()
1982 def on_sendButton_clicked(self):
1983 """
1984 Private slot to send the input to the mercurial process.
1985 """
1986 inputTxt = self.input.text()
1987 inputTxt += os.linesep
1988
1989 if self.passwordCheckBox.isChecked():
1990 self.errors.insertPlainText(os.linesep)
1991 self.errors.ensureCursorVisible()
1992 else:
1993 self.errors.insertPlainText(inputTxt)
1994 self.errors.ensureCursorVisible()
1995
1996 self.process.write(strToQByteArray(inputTxt))
1997
1998 self.passwordCheckBox.setChecked(False)
1999 self.input.clear()
2000
2001 def on_input_returnPressed(self):
2002 """
2003 Private slot to handle the press of the return key in the input field.
2004 """
2005 self.intercept = True
2006 self.on_sendButton_clicked()
2007
2008 def keyPressEvent(self, evt):
2009 """
2010 Protected slot to handle a key press event.
2011
2012 @param evt the key press event (QKeyEvent)
2013 """
2014 if self.intercept:
2015 self.intercept = False
2016 evt.accept()
2017 return
2018 super(HgLogBrowserDialog, self).keyPressEvent(evt)
2019
2020 @pyqtSlot()
2021 def __phaseActTriggered(self):
2022 """
2023 Private slot to handle the Change Phase action.
2024 """
2025 currentPhase = self.logTree.selectedItems()[0].text(self.PhaseColumn)
2026 revs = []
2027 for itm in self.logTree.selectedItems():
2028 if itm.text(self.PhaseColumn) == currentPhase:
2029 revs.append(
2030 itm.text(self.RevisionColumn).split(":")[0].strip())
2031
2032 if not revs:
2033 self.__phaseAct.setEnabled(False)
2034 return
2035
2036 if currentPhase == self.phases["draft"]:
2037 newPhase = self.phases["secret"]
2038 data = (revs, "s", True)
2039 else:
2040 newPhase = self.phases["draft"]
2041 data = (revs, "d", False)
2042 res = self.vcs.hgPhase(self.repodir, data)
2043 if res:
2044 for itm in self.logTree.selectedItems():
2045 itm.setText(self.PhaseColumn, newPhase)
2046
2047 @pyqtSlot()
2048 def __graftActTriggered(self):
2049 """
2050 Private slot to handle the Copy Changesets action.
2051 """
2052 revs = []
2053
2054 for itm in self.logTree.selectedItems():
2055 branch = itm.text(self.BranchColumn)
2056 if branch != self.__projectBranch:
2057 revs.append(
2058 itm.text(self.RevisionColumn).strip().split(":", 1)[0])
2059
2060 if revs:
2061 shouldReopen = self.vcs.hgGraft(self.repodir, revs)
2062 if shouldReopen:
2063 res = E5MessageBox.yesNo(
2064 None,
2065 self.tr("Copy Changesets"),
2066 self.tr(
2067 """The project should be reread. Do this now?"""),
2068 yesDefault=True)
2069 if res:
2070 e5App().getObject("Project").reopenProject()
2071 return
2072
2073 self.on_refreshButton_clicked()
2074
2075 @pyqtSlot()
2076 def __tagActTriggered(self):
2077 """
2078 Private slot to tag the selected revision.
2079 """
2080 if len(self.logTree.selectedItems()) == 1:
2081 itm = self.logTree.selectedItems()[0]
2082 rev = itm.text(self.RevisionColumn).strip().split(":", 1)[0]
2083 tag = itm.text(self.TagsColumn).strip().split(", ", 1)[0]
2084 res = self.vcs.vcsTag(self.repodir, revision=rev, tagName=tag)
2085 if res:
2086 self.on_refreshButton_clicked()
2087
2088 @pyqtSlot()
2089 def __switchActTriggered(self):
2090 """
2091 Private slot to switch the working directory to the
2092 selected revision.
2093 """
2094 if len(self.logTree.selectedItems()) == 1:
2095 itm = self.logTree.selectedItems()[0]
2096 rev = itm.text(self.RevisionColumn).strip().split(":", 1)[0]
2097 bookmarks = [bm.strip() for bm in
2098 itm.text(self.BookmarksColumn).strip().split(",")
2099 if bm.strip()]
2100 if bookmarks:
2101 bookmark, ok = QInputDialog.getItem(
2102 self,
2103 self.tr("Switch"),
2104 self.tr("Select bookmark to switch to (leave empty to"
2105 " use revision):"),
2106 [""] + bookmarks,
2107 0, False)
2108 if not ok:
2109 return
2110 if bookmark:
2111 rev = bookmark
2112 if rev:
2113 shouldReopen = self.vcs.vcsUpdate(self.repodir, revision=rev)
2114 if shouldReopen:
2115 res = E5MessageBox.yesNo(
2116 None,
2117 self.tr("Switch"),
2118 self.tr(
2119 """The project should be reread. Do this now?"""),
2120 yesDefault=True)
2121 if res:
2122 e5App().getObject("Project").reopenProject()
2123 return
2124
2125 self.on_refreshButton_clicked()
2126
2127 @pyqtSlot()
2128 def __bookmarkActTriggered(self):
2129 """
2130 Private slot to bookmark the selected revision.
2131 """
2132 if len(self.logTree.selectedItems()) == 1:
2133 itm = self.logTree.selectedItems()[0]
2134 rev, changeset = \
2135 itm.text(self.RevisionColumn).strip().split(":", 1)
2136 bookmark, ok = QInputDialog.getText(
2137 self,
2138 self.tr("Define Bookmark"),
2139 self.tr('Enter bookmark name for changeset "{0}":').format(
2140 changeset),
2141 QLineEdit.Normal)
2142 if ok and bool(bookmark):
2143 self.vcs.hgBookmarkDefine(
2144 self.repodir, revision="rev({0})".format(rev),
2145 bookmark=bookmark)
2146 self.on_refreshButton_clicked()
2147
2148 @pyqtSlot()
2149 def __bookmarkMoveActTriggered(self):
2150 """
2151 Private slot to move a bookmark to the selected revision.
2152 """
2153 if len(self.logTree.selectedItems()) == 1:
2154 itm = self.logTree.selectedItems()[0]
2155 rev, changeset = \
2156 itm.text(self.RevisionColumn).strip().split(":", 1)
2157 bookmarksList = self.vcs.hgGetBookmarksList(self.repodir)
2158 bookmark, ok = QInputDialog.getItem(
2159 self,
2160 self.tr("Move Bookmark"),
2161 self.tr('Select the bookmark to be moved to changeset'
2162 ' "{0}":').format(changeset),
2163 [""] + bookmarksList,
2164 0, False)
2165 if ok and bool(bookmark):
2166 self.vcs.hgBookmarkMove(
2167 self.repodir, revision="rev({0})".format(rev),
2168 bookmark=bookmark)
2169 self.on_refreshButton_clicked()
2170
2171 @pyqtSlot()
2172 def __lfPullActTriggered(self):
2173 """
2174 Private slot to pull large files of selected revisions.
2175 """
2176 revs = []
2177 for itm in self.logTree.selectedItems():
2178 rev = itm.text(self.RevisionColumn).strip().split(":", 1)[0]
2179 if rev:
2180 revs.append(rev)
2181
2182 if revs:
2183 self.vcs.getExtensionObject("largefiles").hgLfPull(
2184 self.repodir, revisions=revs)
2185
2186 @pyqtSlot()
2187 def __fetchActTriggered(self):
2188 """
2189 Private slot to fetch changes from a remote repository.
2190 """
2191 shouldReopen = False
2192 refresh = False
2193
2194 if self.initialCommandMode == "log":
2195 shouldReopen = self.vcs.getExtensionObject("fetch").hgFetch(
2196 self.repodir)
2197 refresh = True
2198 elif self.initialCommandMode == "incoming":
2199 revs = []
2200 for itm in self.logTree.selectedItems():
2201 rev = itm.text(self.RevisionColumn).split(":")[1].strip()
2202 phase = itm.text(self.PhaseColumn).strip()
2203 if rev and phase == self.phases["draft"]:
2204 revs.append(rev)
2205 if revs:
2206 shouldReopen = self.vcs.getExtensionObject("fetch").hgFetch(
2207 self.repodir, )
2208 refresh = True
2209 if shouldReopen:
2210 res = E5MessageBox.yesNo(
2211 None,
2212 self.tr("Fetch Changes"),
2213 self.tr(
2214 """The project should be reread. Do this now?"""),
2215 yesDefault=True)
2216 if res:
2217 e5App().getObject("Project").reopenProject()
2218 return
2219
2220 if refresh:
2221 self.on_refreshButton_clicked()
2222
2223 @pyqtSlot()
2224 def __pullActTriggered(self):
2225 """
2226 Private slot to pull changes from a remote repository.
2227 """
2228 shouldReopen = False
2229 refresh = False
2230
2231 if self.initialCommandMode == "log":
2232 shouldReopen = self.vcs.hgPull(self.repodir)
2233 refresh = True
2234 elif self.initialCommandMode == "incoming":
2235 revs = []
2236 for itm in self.logTree.selectedItems():
2237 rev = itm.text(self.RevisionColumn).split(":")[1].strip()
2238 phase = itm.text(self.PhaseColumn).strip()
2239 if rev and phase == self.phases["draft"]:
2240 revs.append(rev)
2241 if revs:
2242 shouldReopen = self.vcs.hgPull(self.repodir, revisions=revs)
2243 refresh = True
2244
2245 if shouldReopen:
2246 res = E5MessageBox.yesNo(
2247 None,
2248 self.tr("Pull Changes"),
2249 self.tr(
2250 """The project should be reread. Do this now?"""),
2251 yesDefault=True)
2252 if res:
2253 e5App().getObject("Project").reopenProject()
2254 return
2255
2256 if refresh:
2257 self.on_refreshButton_clicked()
2258
2259 @pyqtSlot()
2260 def __pushActTriggered(self):
2261 """
2262 Private slot to push changes to a remote repository up to a selected
2263 changeset.
2264 """
2265 itm = self.logTree.selectedItems()[0]
2266 rev = itm.text(self.RevisionColumn).strip().split(":", 1)[0]
2267 if rev:
2268 self.vcs.hgPush(self.repodir, rev=rev)
2269 self.on_refreshButton_clicked()
2270
2271 @pyqtSlot()
2272 def __pushAllActTriggered(self):
2273 """
2274 Private slot to push all changes to a remote repository.
2275 """
2276 self.vcs.hgPush(self.repodir)
2277 self.on_refreshButton_clicked()
2278
2279 @pyqtSlot()
2280 def __stripActTriggered(self):
2281 """
2282 Private slot to strip changesets from the repository.
2283 """
2284 itm = self.logTree.selectedItems()[0]
2285 rev = itm.text(self.RevisionColumn).strip().split(":", 1)[1]
2286 shouldReopen = self.vcs.getExtensionObject("strip").hgStrip(
2287 self.repodir, rev=rev)
2288 if shouldReopen:
2289 res = E5MessageBox.yesNo(
2290 None,
2291 self.tr("Strip Changesets"),
2292 self.tr(
2293 """The project should be reread. Do this now?"""),
2294 yesDefault=True)
2295 if res:
2296 e5App().getObject("Project").reopenProject()
2297 return
2298
2299 self.on_refreshButton_clicked()
2300
2301 @pyqtSlot()
2302 def __mergeActTriggered(self):
2303 """
2304 Private slot to merge the working directory with the selected
2305 changeset.
2306 """
2307 itm = self.logTree.selectedItems()[0]
2308 rev = "rev({0})".format(
2309 itm.text(self.RevisionColumn).strip().split(":", 1)[0])
2310 self.vcs.vcsMerge(self.repodir, rev=rev)
2311
2312 @pyqtSlot()
2313 def __bundleActTriggered(self):
2314 """
2315 Private slot to create a changegroup file.
2316 """
2317 if self.initialCommandMode == "log":
2318 selectedItems = self.logTree.selectedItems()
2319 if len(selectedItems) == 0:
2320 # all revisions of the local repository will be bundled
2321 bundleData = {
2322 "revs": [],
2323 "base": "",
2324 "all": True,
2325 }
2326 elif len(selectedItems) == 1:
2327 # the selected changeset is the base
2328 rev = selectedItems[0].text(self.RevisionColumn)\
2329 .split(":", 1)[0].strip()
2330 bundleData = {
2331 "revs": [],
2332 "base": rev,
2333 "all": False,
2334 }
2335 else:
2336 # lowest revision is the base, others will be bundled
2337 revs = []
2338 for itm in selectedItems:
2339 rev = itm.text(self.RevisionColumn).split(":", 1)[0]
2340 try:
2341 revs.append(int(rev))
2342 except ValueError:
2343 # ignore silently
2344 pass
2345 baseRev = min(revs)
2346 while baseRev in revs:
2347 revs.remove(baseRev)
2348
2349 bundleData = {
2350 "revs": [str(rev) for rev in revs],
2351 "base": str(baseRev),
2352 "all": False,
2353 }
2354 elif self.initialCommandMode == "outgoing":
2355 selectedItems = self.logTree.selectedItems()
2356 if len(selectedItems) > 0:
2357 revs = []
2358 for itm in selectedItems:
2359 rev = itm.text(self.RevisionColumn).split(":", 1)[0]
2360 revs.append(rev.strip())
2361
2362 bundleData = {
2363 "revs": revs,
2364 "base": "",
2365 "all": False,
2366 }
2367
2368 self.vcs.hgBundle(self.repodir, bundleData=bundleData)
2369
2370 @pyqtSlot()
2371 def __unbundleActTriggered(self):
2372 """
2373 Private slot to apply the currently previewed bundle file.
2374 """
2375 if self.initialCommandMode == "incoming" and bool(self.__bundle):
2376 shouldReopen = self.vcs.hgUnbundle(self.repodir,
2377 files=[self.__bundle])
2378 if shouldReopen:
2379 res = E5MessageBox.yesNo(
2380 None,
2381 self.tr("Apply Changegroup"),
2382 self.tr("""The project should be reread. Do this now?"""),
2383 yesDefault=True)
2384 if res:
2385 e5App().getObject("Project").reopenProject()
2386 return
2387
2388 self.vcs.vcsLogBrowser(self.repodir)
2389 self.close()
2390
2391 @pyqtSlot()
2392 def __gpgSignActTriggered(self):
2393 """
2394 Private slot to sign the selected revisions.
2395 """
2396 revs = []
2397 for itm in self.logTree.selectedItems():
2398 rev = itm.text(self.RevisionColumn).split(":", 1)[0].strip()
2399 if rev:
2400 revs.append(rev)
2401
2402 if revs:
2403 self.vcs.getExtensionObject("gpg").hgGpgSign(
2404 self.repodir, revisions=revs)
2405
2406 @pyqtSlot()
2407 def __gpgVerifyActTriggered(self):
2408 """
2409 Private slot to verify the signatures of a selected revisions.
2410 """
2411 rev = self.logTree.selectedItems()[0].text(self.RevisionColumn)\
2412 .split(":", 1)[0].strip()
2413 if rev:
2414 self.vcs.getExtensionObject("gpg").hgGpgVerifySignatures(
2415 self.repodir, rev=rev)
2416
2417 def __selectAllActTriggered(self, select=True):
2418 """
2419 Private method to select or unselect all log entries.
2420
2421 @param select flag indicating to select all entries
2422 @type bool
2423 """
2424 blocked = self.logTree.blockSignals(True)
2425 for row in range(self.logTree.topLevelItemCount()):
2426 self.logTree.topLevelItem(row).setSelected(select)
2427 self.logTree.blockSignals(blocked)
2428 self.on_logTree_itemSelectionChanged()
2429
2430 def __actionMode(self):
2431 """
2432 Private method to get the selected action mode.
2433
2434 @return selected action mode (string, one of filter or find)
2435 """
2436 return self.modeComboBox.itemData(
2437 self.modeComboBox.currentIndex())
2438
2439 @pyqtSlot(int)
2440 def on_modeComboBox_currentIndexChanged(self, index):
2441 """
2442 Private slot to react on mode changes.
2443
2444 @param index index of the selected entry (integer)
2445 """
2446 mode = self.modeComboBox.itemData(index)
2447 findMode = mode == "find"
2448 filterMode = mode == "filter"
2449
2450 self.fromDate.setEnabled(filterMode)
2451 self.toDate.setEnabled(filterMode)
2452 self.branchCombo.setEnabled(filterMode)
2453 self.findPrevButton.setVisible(findMode)
2454 self.findNextButton.setVisible(findMode)
2455
2456 if findMode:
2457 for topIndex in range(self.logTree.topLevelItemCount()):
2458 self.logTree.topLevelItem(topIndex).setHidden(False)
2459 self.logTree.header().setSectionHidden(self.IconColumn, False)
2460 elif filterMode:
2461 self.__filterLogs()
2462
2463 @pyqtSlot()
2464 def on_findPrevButton_clicked(self):
2465 """
2466 Private slot to find the previous item matching the entered criteria.
2467 """
2468 self.__findItem(True)
2469
2470 @pyqtSlot()
2471 def on_findNextButton_clicked(self):
2472 """
2473 Private slot to find the next item matching the entered criteria.
2474 """
2475 self.__findItem(False)
2476
2477 def __findItem(self, backwards=False, interactive=False):
2478 """
2479 Private slot to find an item matching the entered criteria.
2480
2481 @param backwards flag indicating to search backwards (boolean)
2482 @param interactive flag indicating an interactive search (boolean)
2483 """
2484 self.__findBackwards = backwards
2485
2486 fieldIndex, searchRx, indexIsRole = self.__prepareFieldSearch()
2487 currentIndex = self.logTree.indexOfTopLevelItem(
2488 self.logTree.currentItem())
2489 if backwards:
2490 if interactive:
2491 indexes = range(currentIndex, -1, -1)
2492 else:
2493 indexes = range(currentIndex - 1, -1, -1)
2494 else:
2495 if interactive:
2496 indexes = range(currentIndex, self.logTree.topLevelItemCount())
2497 else:
2498 indexes = range(currentIndex + 1,
2499 self.logTree.topLevelItemCount())
2500
2501 for index in indexes:
2502 topItem = self.logTree.topLevelItem(index)
2503 if indexIsRole:
2504 if fieldIndex == self.__changesRole:
2505 changes = topItem.data(0, self.__changesRole)
2506 txt = "\n".join(
2507 [c["path"] for c in changes] +
2508 [c["copyfrom"] for c in changes]
2509 )
2510 else:
2511 # Find based on complete message text
2512 txt = "\n".join(topItem.data(0, self.__messageRole))
2513 else:
2514 txt = topItem.text(fieldIndex)
2515 if searchRx.indexIn(txt) > -1:
2516 self.logTree.setCurrentItem(self.logTree.topLevelItem(index))
2517 break
2518 else:
2519 E5MessageBox.information(
2520 self,
2521 self.tr("Find Commit"),
2522 self.tr("""'{0}' was not found.""").format(self.rxEdit.text()))
2523
2524 def __revisionClicked(self, url):
2525 """
2526 Private slot to handle the anchorClicked signal of the changeset
2527 details pane.
2528
2529 @param url URL that was clicked
2530 @type QUrl
2531 """
2532 if url.scheme() == "rev":
2533 # a parent or child revision was clicked, show the respective item
2534 rev = url.path()
2535 searchStr = "{0:>7}:".format(rev)
2536 # format must be in sync with item generation format
2537 items = self.logTree.findItems(searchStr, Qt.MatchStartsWith,
2538 self.RevisionColumn)
2539 if items:
2540 itm = items[0]
2541 if itm.isHidden():
2542 itm.setHidden(False)
2543 self.logTree.setCurrentItem(itm)
2544 else:
2545 # load the next batch and try again
2546 if self.nextButton.isEnabled():
2547 self.__addFinishCallback(
2548 lambda: self.__revisionClicked(url))
2549 self.on_nextButton_clicked()
2550
2551 ###########################################################################
2552 ## Diff handling methods below
2553 ###########################################################################
2554
2555 def __generateDiffs(self, parent=1):
2556 """
2557 Private slot to generate diff outputs for the selected item.
2558
2559 @param parent number of parent to diff against
2560 @type int
2561 """
2562 self.diffEdit.setPlainText(self.tr("Generating differences ..."))
2563 self.diffLabel.setText(self.tr("Differences"))
2564 self.diffSelectLabel.clear()
2565 self.diffHighlighter.regenerateRules()
2566
2567 selectedItems = self.logTree.selectedItems()
2568 if len(selectedItems) == 1:
2569 currentItem = selectedItems[0]
2570 rev2 = currentItem.text(self.RevisionColumn).split(":", 1)[0]
2571 parents = currentItem.data(0, self.__parentsRole)
2572 if len(parents) >= parent:
2573 self.diffLabel.setText(
2574 self.tr("Differences to Parent {0}").format(parent))
2575 rev1 = parents[parent - 1]
2576
2577 self.__diffGenerator.start(self.__filename, [rev1, rev2],
2578 self.__bundle)
2579
2580 if len(parents) > 1:
2581 if parent == 1:
2582 par1 = "&nbsp;1&nbsp;"
2583 else:
2584 par1 = '<a href="diff:1">&nbsp;1&nbsp;</a>'
2585 if parent == 2:
2586 par2 = "&nbsp;2&nbsp;"
2587 else:
2588 par2 = '<a href="diff:2">&nbsp;2&nbsp;</a>'
2589 self.diffSelectLabel.setText(
2590 self.tr('Diff to Parent {0}{1}').format(par1, par2))
2591 elif len(selectedItems) == 2:
2592 rev2 = int(selectedItems[0].text(
2593 self.RevisionColumn).split(":")[0])
2594 rev1 = int(selectedItems[1].text(
2595 self.RevisionColumn).split(":")[0])
2596
2597 self.__diffGenerator.start(self.__filename,
2598 [min(rev1, rev2), max(rev1, rev2)],
2599 self.__bundle)
2600 else:
2601 self.diffEdit.clear()
2602
2603 def __generatorFinished(self):
2604 """
2605 Private slot connected to the finished signal of the diff generator.
2606 """
2607 diff, errors, fileSeparators = self.__diffGenerator.getResult()
2608
2609 if diff:
2610 self.diffEdit.setPlainText("".join(diff))
2611 elif errors:
2612 self.diffEdit.setPlainText("".join(errors))
2613 else:
2614 self.diffEdit.setPlainText(self.tr('There is no difference.'))
2615
2616 self.saveLabel.setVisible(bool(diff))
2617
2618 if self.__diffUpdatesFiles:
2619 for oldFileName, newFileName, lineNumber in fileSeparators:
2620 if oldFileName == newFileName:
2621 fileName = oldFileName
2622 elif oldFileName == "__NULL__":
2623 fileName = newFileName
2624 else:
2625 fileName = oldFileName
2626 item = QTreeWidgetItem(self.filesTree, ["", fileName, ""])
2627 item.setData(0, self.__diffFileLineRole, lineNumber)
2628 self.__resizeColumnsFiles()
2629 self.__resortFiles()
2630 else:
2631 for oldFileName, newFileName, lineNumber in fileSeparators:
2632 for fileName in (oldFileName, newFileName):
2633 if fileName != "__NULL__":
2634 items = self.filesTree.findItems(
2635 fileName, Qt.MatchExactly, 1)
2636 for item in items:
2637 item.setData(0, self.__diffFileLineRole,
2638 lineNumber)
2639
2640 tc = self.diffEdit.textCursor()
2641 tc.movePosition(QTextCursor.Start)
2642 self.diffEdit.setTextCursor(tc)
2643 self.diffEdit.ensureCursorVisible()
2644
2645 @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem)
2646 def on_filesTree_currentItemChanged(self, current, previous):
2647 """
2648 Private slot called, when the current item of the files tree changes.
2649
2650 @param current reference to the new current item (QTreeWidgetItem)
2651 @param previous reference to the old current item (QTreeWidgetItem)
2652 """
2653 if current:
2654 para = current.data(0, self.__diffFileLineRole)
2655 if para is not None:
2656 if para == 0:
2657 tc = self.diffEdit.textCursor()
2658 tc.movePosition(QTextCursor.Start)
2659 self.diffEdit.setTextCursor(tc)
2660 self.diffEdit.ensureCursorVisible()
2661 elif para == -1:
2662 tc = self.diffEdit.textCursor()
2663 tc.movePosition(QTextCursor.End)
2664 self.diffEdit.setTextCursor(tc)
2665 self.diffEdit.ensureCursorVisible()
2666 else:
2667 # step 1: move cursor to end
2668 tc = self.diffEdit.textCursor()
2669 tc.movePosition(QTextCursor.End)
2670 self.diffEdit.setTextCursor(tc)
2671 self.diffEdit.ensureCursorVisible()
2672
2673 # step 2: move cursor to desired line
2674 tc = self.diffEdit.textCursor()
2675 delta = tc.blockNumber() - para
2676 tc.movePosition(QTextCursor.PreviousBlock,
2677 QTextCursor.MoveAnchor, delta)
2678 self.diffEdit.setTextCursor(tc)
2679 self.diffEdit.ensureCursorVisible()
2680
2681 @pyqtSlot(str)
2682 def on_diffSelectLabel_linkActivated(self, link):
2683 """
2684 Private slot to handle the selection of a diff target.
2685
2686 @param link activated link
2687 @type str
2688 """
2689 if ":" in link:
2690 scheme, parent = link.split(":", 1)
2691 if scheme == "diff":
2692 try:
2693 parent = int(parent)
2694 self.__generateDiffs(parent)
2695 except ValueError:
2696 # ignore silently
2697 pass
2698
2699 @pyqtSlot(str)
2700 def on_saveLabel_linkActivated(self, link):
2701 """
2702 Private slot to handle the selection of the save link.
2703
2704 @param link activated link
2705 @type str
2706 """
2707 if ":" not in link:
2708 return
2709
2710 scheme, rest = link.split(":", 1)
2711 if scheme != "save" or rest != "me":
2712 return
2713
2714 if self.projectMode:
2715 fname = self.vcs.splitPath(self.__filename)[0]
2716 fname += "/{0}.diff".format(os.path.split(fname)[-1])
2717 else:
2718 dname, fname = self.vcs.splitPath(self.__filename)
2719 if fname != '.':
2720 fname = "{0}.diff".format(self.__filename)
2721 else:
2722 fname = dname
2723
2724 fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
2725 self,
2726 self.tr("Save Diff"),
2727 fname,
2728 self.tr("Patch Files (*.diff)"),
2729 None,
2730 E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
2731
2732 if not fname:
2733 return # user aborted
2734
2735 ext = QFileInfo(fname).suffix()
2736 if not ext:
2737 ex = selectedFilter.split("(*")[1].split(")")[0]
2738 if ex:
2739 fname += ex
2740 if QFileInfo(fname).exists():
2741 res = E5MessageBox.yesNo(
2742 self,
2743 self.tr("Save Diff"),
2744 self.tr("<p>The patch file <b>{0}</b> already exists."
2745 " Overwrite it?</p>").format(fname),
2746 icon=E5MessageBox.Warning)
2747 if not res:
2748 return
2749 fname = Utilities.toNativeSeparators(fname)
2750
2751 eol = e5App().getObject("Project").getEolString()
2752 try:
2753 f = open(fname, "w", encoding="utf-8", newline="")
2754 f.write(eol.join(self.diffEdit.toPlainText().splitlines()))
2755 f.close()
2756 except IOError as why:
2757 E5MessageBox.critical(
2758 self, self.tr('Save Diff'),
2759 self.tr(
2760 '<p>The patch file <b>{0}</b> could not be saved.'
2761 '<br>Reason: {1}</p>')
2762 .format(fname, str(why)))
2763
2764 @pyqtSlot(str)
2765 def on_sbsSelectLabel_linkActivated(self, link):
2766 """
2767 Private slot to handle selection of a side-by-side link.
2768
2769 @param link text of the selected link
2770 @type str
2771 """
2772 if ":" in link:
2773 scheme, path = link.split(":", 1)
2774 if scheme == "sbsdiff" and "_" in path:
2775 rev1, rev2 = path.split("_", 1)
2776 self.vcs.hgSbsDiff(self.__filename, revisions=(rev1, rev2))

eric ide

mercurial