Plugins/VcsPlugins/vcsMercurial/HgLogBrowserDialog.py

changeset 5477
fb8875e356d4
parent 5463
d84b854d59c0
child 5486
a74fafdb67e0
equal deleted inserted replaced
5476:fb81a812ed9c 5477:fb8875e356d4
16 import os 16 import os
17 import re 17 import re
18 import collections 18 import collections
19 19
20 from PyQt5.QtCore import pyqtSlot, Qt, QDate, QProcess, QTimer, QRegExp, \ 20 from PyQt5.QtCore import pyqtSlot, Qt, QDate, QProcess, QTimer, QRegExp, \
21 QSize, QPoint, QUrl 21 QSize, QPoint, QFileInfo
22 from PyQt5.QtGui import QCursor, QColor, QPixmap, QPainter, QPen, QBrush, QIcon 22 from PyQt5.QtGui import QCursor, QColor, QPixmap, QPainter, QPen, QBrush, \
23 QIcon, QTextCursor
23 from PyQt5.QtWidgets import QWidget, QDialogButtonBox, QHeaderView, \ 24 from PyQt5.QtWidgets import QWidget, QDialogButtonBox, QHeaderView, \
24 QTreeWidgetItem, QApplication, QLineEdit, QMenu, QInputDialog 25 QTreeWidgetItem, QApplication, QLineEdit, QMenu, QInputDialog
25 26
26 from E5Gui.E5Application import e5App 27 from E5Gui.E5Application import e5App
27 from E5Gui import E5MessageBox 28 from E5Gui import E5MessageBox, E5FileDialog
28 29
29 from .Ui_HgLogBrowserDialog import Ui_HgLogBrowserDialog 30 from .Ui_HgLogBrowserDialog import Ui_HgLogBrowserDialog
30 31
32 from .HgDiffHighlighter import HgDiffHighlighter
33 from .HgDiffGenerator import HgDiffGenerator
34
31 import UI.PixmapCache 35 import UI.PixmapCache
36 import Preferences
37 import Utilities
32 38
33 COLORNAMES = ["blue", "darkgreen", "red", "green", "darkblue", "purple", 39 COLORNAMES = ["blue", "darkgreen", "red", "green", "darkblue", "purple",
34 "cyan", "olive", "magenta", "darkred", "darkmagenta", 40 "cyan", "olive", "magenta", "darkred", "darkmagenta",
35 "darkcyan", "gray", "yellow"] 41 "darkcyan", "gray", "yellow"]
36 COLORS = [str(QColor(x).name()) for x in COLORNAMES] 42 COLORS = [str(QColor(x).name()) for x in COLORNAMES]
65 @param parent parent widget (QWidget) 71 @param parent parent widget (QWidget)
66 """ 72 """
67 super(HgLogBrowserDialog, self).__init__(parent) 73 super(HgLogBrowserDialog, self).__init__(parent)
68 self.setupUi(self) 74 self.setupUi(self)
69 75
76 self.diffSplitter.setStretchFactor(0, 1)
77 self.diffSplitter.setStretchFactor(1, 2)
78
70 self.__position = QPoint() 79 self.__position = QPoint()
71 80
72 if mode == "log": 81 if mode == "log":
73 self.setWindowTitle(self.tr("Mercurial Log")) 82 self.setWindowTitle(self.tr("Mercurial Log"))
74 elif mode == "incoming": 83 elif mode == "incoming":
101 self.fieldCombo.addItem(self.tr("Revision"), "revision") 110 self.fieldCombo.addItem(self.tr("Revision"), "revision")
102 self.fieldCombo.addItem(self.tr("Author"), "author") 111 self.fieldCombo.addItem(self.tr("Author"), "author")
103 self.fieldCombo.addItem(self.tr("Message"), "message") 112 self.fieldCombo.addItem(self.tr("Message"), "message")
104 self.fieldCombo.addItem(self.tr("File"), "file") 113 self.fieldCombo.addItem(self.tr("File"), "file")
105 114
115 font = Preferences.getEditorOtherFonts("MonospacedFont")
116 self.diffEdit.setFontFamily(font.family())
117 self.diffEdit.setFontPointSize(font.pointSize())
118
119 self.diffHighlighter = HgDiffHighlighter(self.diffEdit.document())
120 self.__diffGenerator = HgDiffGenerator(vcs, self)
121 self.__diffGenerator.finished.connect(self.__generatorFinished)
122
106 self.vcs = vcs 123 self.vcs = vcs
107 if mode in ("log", "incoming", "outgoing"): 124 if mode in ("log", "incoming", "outgoing"):
108 self.commandMode = mode 125 self.commandMode = mode
109 self.initialCommandMode = mode 126 self.initialCommandMode = mode
110 else: 127 else:
116 "<table>" 133 "<table>"
117 "<tr><td><b>Revision</b></td><td>{0}</td></tr>" 134 "<tr><td><b>Revision</b></td><td>{0}</td></tr>"
118 "<tr><td><b>Date</b></td><td>{1}</td></tr>" 135 "<tr><td><b>Date</b></td><td>{1}</td></tr>"
119 "<tr><td><b>Author</b></td><td>{2}</td></tr>" 136 "<tr><td><b>Author</b></td><td>{2}</td></tr>"
120 "<tr><td><b>Branch</b></td><td>{3}</td></tr>" 137 "<tr><td><b>Branch</b></td><td>{3}</td></tr>"
121 "<tr><td><b>Parents</b></td><td>{4}</td></tr>" 138 "{4}"
122 "<tr><td><b>Children</b></td><td>{5}</td></tr>" 139 "<tr><td><b>Message</b></td><td>{5}</td></tr>"
123 "{6}"
124 "</table>" 140 "</table>"
141 )
142 self.__parentsTemplate = self.tr(
143 "<tr><td><b>Parents</b></td><td>{0}</td></tr>"
144 )
145 self.__childrenTemplate = self.tr(
146 "<tr><td><b>Children</b></td><td>{0}</td></tr>"
125 ) 147 )
126 self.__tagsTemplate = self.tr( 148 self.__tagsTemplate = self.tr(
127 "<tr><td><b>Tags</b></td><td>{0}</td></tr>" 149 "<tr><td><b>Tags</b></td><td>{0}</td></tr>"
128 ) 150 )
129 self.__latestTagTemplate = self.tr( 151 self.__latestTagTemplate = self.tr(
134 ) 156 )
135 157
136 self.__bundle = "" 158 self.__bundle = ""
137 self.__filename = "" 159 self.__filename = ""
138 self.__isFile = False 160 self.__isFile = False
139 self.__currentRevision = "" 161 self.__selectedRevisions = []
140 self.intercept = False 162 self.intercept = False
141 163
142 self.__initData() 164 self.__initData()
143 165
144 self.__allBranchesFilter = self.tr("All") 166 self.__allBranchesFilter = self.tr("All")
145 167
146 self.fromDate.setDisplayFormat("yyyy-MM-dd") 168 self.fromDate.setDisplayFormat("yyyy-MM-dd")
147 self.toDate.setDisplayFormat("yyyy-MM-dd") 169 self.toDate.setDisplayFormat("yyyy-MM-dd")
148 self.__resetUI() 170 self.__resetUI()
149 171
172 # roles used in the log tree
150 self.__messageRole = Qt.UserRole 173 self.__messageRole = Qt.UserRole
151 self.__changesRole = Qt.UserRole + 1 174 self.__changesRole = Qt.UserRole + 1
152 self.__edgesRole = Qt.UserRole + 2 175 self.__edgesRole = Qt.UserRole + 2
153 self.__parentsRole = Qt.UserRole + 3 176 self.__parentsRole = Qt.UserRole + 3
154 self.__latestTagRole = Qt.UserRole + 4 177 self.__latestTagRole = Qt.UserRole + 4
178
179 # roles used in the file tree
180 self.__diffFileLineRole = Qt.UserRole
155 181
156 if self.__hgClient: 182 if self.__hgClient:
157 self.process = None 183 self.process = None
158 else: 184 else:
159 self.process = QProcess() 185 self.process = QProcess()
604 if not finished: 630 if not finished:
605 errMsg = self.tr( 631 errMsg = self.tr(
606 "The hg process did not finish within 30s.") 632 "The hg process did not finish within 30s.")
607 else: 633 else:
608 errMsg = self.tr("Could not start the hg executable.") 634 errMsg = self.tr("Could not start the hg executable.")
609 635
610 if errMsg: 636 if errMsg:
611 E5MessageBox.critical( 637 E5MessageBox.critical(
612 self, 638 self,
613 self.tr("Mercurial Error"), 639 self.tr("Mercurial Error"),
614 errMsg) 640 errMsg)
615 641
616 if output: 642 if output:
617 parents = [int(p) for p in output.strip().splitlines()] 643 parents = [int(p) for p in output.strip().splitlines()]
618 644
619 return parents 645 return parents
744 errMsg) 770 errMsg)
745 771
746 res = ("", "") 772 res = ("", "")
747 if output: 773 if output:
748 for line in output.splitlines(): 774 for line in output.splitlines():
749 name, rev = line.strip().rsplit(None, 1) 775 if line.strip():
750 if name == tag: 776 try:
751 res = tuple(rev.split(":", 1)) 777 name, rev = line.strip().rsplit(None, 1)
752 break 778 if name == tag:
779 res = tuple(rev.split(":", 1))
780 break
781 except ValueError:
782 # ignore silently
783 pass
753 784
754 return res 785 return res
755 786
756 def __generateLogItem(self, author, date, message, revision, changedPaths, 787 def __generateLogItem(self, author, date, message, revision, changedPaths,
757 parents, branches, tags, phase, bookmarks, 788 parents, branches, tags, phase, bookmarks,
845 876
846 try: 877 try:
847 self.__lastRev = int(revision.split(":")[0]) 878 self.__lastRev = int(revision.split(":")[0])
848 except ValueError: 879 except ValueError:
849 self.__lastRev = 0 880 self.__lastRev = 0
850
851 return itm
852
853 def __generateFileItem(self, action, path, copyfrom):
854 """
855 Private method to generate a changed files tree entry.
856
857 @param action indicator for the change action ("A", "D" or "M")
858 @param path path of the file in the repository (string)
859 @param copyfrom path the file was copied from (string)
860 @return reference to the generated item (QTreeWidgetItem)
861 """
862 itm = QTreeWidgetItem(self.filesTree, [
863 self.flags[action],
864 path,
865 copyfrom,
866 ])
867 881
868 return itm 882 return itm
869 883
870 def __getLogEntries(self, startRev=None, noEntries=0): 884 def __getLogEntries(self, startRev=None, noEntries=0):
871 """ 885 """
993 @keyparam noEntries number of entries to get (0 = default) (int) 1007 @keyparam noEntries number of entries to get (0 = default) (int)
994 """ 1008 """
995 self.__bundle = bundle 1009 self.__bundle = bundle
996 self.__isFile = isFile 1010 self.__isFile = isFile
997 1011
998 self.sbsCheckBox.setEnabled(isFile) 1012 self.sbsSelectLabel.clear()
999 self.sbsCheckBox.setVisible(isFile)
1000 1013
1001 self.errorGroup.hide() 1014 self.errorGroup.hide()
1002 QApplication.processEvents() 1015 QApplication.processEvents()
1003 1016
1004 self.__initData() 1017 self.__initData()
1185 fileCopies = {} 1198 fileCopies = {}
1186 1199
1187 self.__resizeColumnsLog() 1200 self.__resizeColumnsLog()
1188 1201
1189 if self.__started: 1202 if self.__started:
1190 self.logTree.setCurrentItem(self.logTree.topLevelItem(0)) 1203 if self.__selectedRevisions:
1204 self.logTree.setCurrentItem(self.logTree.findItems(
1205 self.__selectedRevisions[0], Qt.MatchExactly,
1206 self.RevisionColumn)[0])
1207 else:
1208 self.logTree.setCurrentItem(self.logTree.topLevelItem(0))
1191 self.__started = False 1209 self.__started = False
1192 1210
1193 if self.commandMode in ("incoming", "outgoing"): 1211 if self.commandMode in ("incoming", "outgoing"):
1194 self.commandMode = "log" # switch to log mode 1212 self.commandMode = "log" # switch to log mode
1195 if self.__lastRev > 0: 1213 if self.__lastRev > 0:
1219 self.branchCombo.findText(branchFilter)) 1237 self.branchCombo.findText(branchFilter))
1220 1238
1221 self.__filterLogsEnabled = True 1239 self.__filterLogsEnabled = True
1222 if self.__actionMode() == "filter": 1240 if self.__actionMode() == "filter":
1223 self.__filterLogs() 1241 self.__filterLogs()
1224 self.__updateDiffButtons()
1225 self.__updateToolMenuActions() 1242 self.__updateToolMenuActions()
1226 1243
1227 # restore current item 1244 # restore current item
1228 if self.__currentRevision: 1245 if self.__selectedRevisions:
1229 items = self.logTree.findItems( 1246 for revision in self.__selectedRevisions:
1230 self.__currentRevision, Qt.MatchExactly, self.RevisionColumn) 1247 items = self.logTree.findItems(
1231 if items: 1248 revision, Qt.MatchExactly, self.RevisionColumn)
1232 self.logTree.setCurrentItem(items[0]) 1249 if items:
1233 self.__currentRevision = "" 1250 items[0].setSelected(True)
1251 self.__selectedRevisions = []
1234 1252
1235 def __readStdout(self): 1253 def __readStdout(self):
1236 """ 1254 """
1237 Private slot to handle the readyReadStandardOutput signal. 1255 Private slot to handle the readyReadStandardOutput signal.
1238 1256
1269 1287
1270 if not self.__hgClient: 1288 if not self.__hgClient:
1271 # show input in case the process asked for some input 1289 # show input in case the process asked for some input
1272 self.inputGroup.setEnabled(True) 1290 self.inputGroup.setEnabled(True)
1273 self.inputGroup.show() 1291 self.inputGroup.show()
1274
1275 def __diffRevisions(self, rev1, rev2):
1276 """
1277 Private method to do a diff of two revisions.
1278
1279 @param rev1 first revision number (integer)
1280 @param rev2 second revision number (integer)
1281 """
1282 if self.sbsCheckBox.isEnabled() and self.sbsCheckBox.isChecked():
1283 self.vcs.hgSbsDiff(self.__filename,
1284 revisions=(str(rev1), str(rev2)))
1285 else:
1286 if self.diff is None:
1287 from .HgDiffDialog import HgDiffDialog
1288 self.diff = HgDiffDialog(self.vcs)
1289 self.diff.show()
1290 self.diff.raise_()
1291 self.diff.start(self.__filename, [rev1, rev2], self.__bundle)
1292 1292
1293 def on_buttonBox_clicked(self, button): 1293 def on_buttonBox_clicked(self, button):
1294 """ 1294 """
1295 Private slot called by a button of the button box clicked. 1295 Private slot called by a button of the button box clicked.
1296 1296
1305 else: 1305 else:
1306 self.__finish() 1306 self.__finish()
1307 elif button == self.refreshButton: 1307 elif button == self.refreshButton:
1308 self.on_refreshButton_clicked() 1308 self.on_refreshButton_clicked()
1309 1309
1310 def __updateDiffButtons(self): 1310 def __updateSbsSelectLabel(self):
1311 """ 1311 """
1312 Private slot to update the enabled status of the diff buttons. 1312 Private slot to update the enabled status of the diff buttons.
1313 """ 1313 """
1314 selectionLength = len(self.logTree.selectedItems()) 1314 self.sbsSelectLabel.clear()
1315 if selectionLength <= 1: 1315 if self.__isFile:
1316 current = self.logTree.currentItem() 1316 selectedItems = self.logTree.selectedItems()
1317 if current is None: 1317 if len(selectedItems) == 1:
1318 self.diffP1Button.setEnabled(False) 1318 currentItem = selectedItems[0]
1319 self.diffP2Button.setEnabled(False) 1319 rev2 = currentItem.text(self.RevisionColumn).split(":", 1)[0]\
1320 else: 1320 .strip()
1321 parents = current.data(0, self.__parentsRole) 1321 parents = currentItem.data(0, self.__parentsRole)
1322 self.diffP1Button.setEnabled(len(parents) > 0) 1322 if parents:
1323 self.diffP2Button.setEnabled(len(parents) > 1) 1323 parentLinks = []
1324 1324 for index in range(len(parents)):
1325 self.diffRevisionsButton.setEnabled(False) 1325 parentLinks.append(
1326 elif selectionLength == 2: 1326 '<a href="sbsdiff:{0}_{1}">&nbsp;{2}&nbsp;</a>'
1327 self.diffP1Button.setEnabled(False) 1327 .format(parents[index], rev2, index + 1))
1328 self.diffP2Button.setEnabled(False) 1328 self.sbsSelectLabel.setText(
1329 1329 self.tr('Side-by-Side Diff to Parent {0}').format(
1330 self.diffRevisionsButton.setEnabled(True) 1330 " ".join(parentLinks)))
1331 else: 1331 elif len(selectedItems) == 2:
1332 self.diffP1Button.setEnabled(False) 1332 rev1 = int(selectedItems[0].text(self.RevisionColumn)
1333 self.diffP2Button.setEnabled(False) 1333 .split(":", 1)[0])
1334 1334 rev2 = int(selectedItems[1].text(self.RevisionColumn)
1335 self.diffRevisionsButton.setEnabled(False) 1335 .split(":", 1)[0])
1336 if rev1 > rev2:
1337 # Swap the entries, so that rev1 < rev2
1338 rev1, rev2 = rev2, rev1
1339 self.sbsSelectLabel.setText(self.tr(
1340 '<a href="sbsdiff:{0}_{1}">Side-by-Side Compare</a>')
1341 .format(rev1, rev2))
1336 1342
1337 def __updateToolMenuActions(self): 1343 def __updateToolMenuActions(self):
1338 """ 1344 """
1339 Private slot to update the status of the tool menu actions and 1345 Private slot to update the status of the tool menu actions and
1340 the tool menu button. 1346 the tool menu button.
1409 1415
1410 self.actionsButton.setEnabled(True) 1416 self.actionsButton.setEnabled(True)
1411 else: 1417 else:
1412 self.actionsButton.setEnabled(False) 1418 self.actionsButton.setEnabled(False)
1413 1419
1414 def __updateGui(self, itm): 1420 def __updateDetailsAndFiles(self):
1415 """ 1421 """
1416 Private slot to update GUI elements except tool menu actions. 1422 Private slot to update the details and file changes panes.
1417
1418 @param itm reference to the item the update should be based on
1419 (QTreeWidgetItem)
1420 """ 1423 """
1421 self.detailsEdit.clear() 1424 self.detailsEdit.clear()
1422 self.messageEdit.clear()
1423 self.filesTree.clear() 1425 self.filesTree.clear()
1424 1426 self.__diffUpdatesFiles = False
1427
1428 selectedItems = self.logTree.selectedItems()
1429 if len(selectedItems) == 1:
1430 self.detailsEdit.setHtml(
1431 self.__generateDetailsTableText(selectedItems[0]))
1432 self.__updateFilesTree(self.filesTree, selectedItems[0])
1433 self.__resizeColumnsFiles()
1434 self.__resortFiles()
1435 elif len(selectedItems) == 2:
1436 self.__diffUpdatesFiles = True
1437 index1 = self.logTree.indexOfTopLevelItem(selectedItems[0])
1438 index2 = self.logTree.indexOfTopLevelItem(selectedItems[1])
1439 if index1 > index2:
1440 # Swap the entries
1441 selectedItems[0], selectedItems[1] = \
1442 selectedItems[1], selectedItems[0]
1443 html = "{0}<hr/>{1}".format(
1444 self.__generateDetailsTableText(selectedItems[0]),
1445 self.__generateDetailsTableText(selectedItems[1]),
1446 )
1447 self.detailsEdit.setHtml(html)
1448 # self.filesTree is updated by the diff
1449
1450 def __generateDetailsTableText(self, itm):
1451 """
1452 Private method to generate an HTML table with the details of the given
1453 changeset.
1454
1455 @param itm reference to the item the table should be based on
1456 @type QTreeWidgetItem
1457 @return HTML table containing details
1458 @rtype str
1459 """
1425 if itm is not None: 1460 if itm is not None:
1426 if itm.text(self.TagsColumn): 1461 if itm.text(self.TagsColumn):
1427 tagsStr = self.__tagsTemplate.format(itm.text(self.TagsColumn)) 1462 tagsStr = self.__tagsTemplate.format(itm.text(self.TagsColumn))
1428 else: 1463 else:
1429 tagsStr = "" 1464 tagsStr = ""
1465
1430 if itm.text(self.BookmarksColumn): 1466 if itm.text(self.BookmarksColumn):
1431 bookmarksStr = self.__bookmarksTemplate.format( 1467 bookmarksStr = self.__bookmarksTemplate.format(
1432 itm.text(self.BookmarksColumn)) 1468 itm.text(self.BookmarksColumn))
1433 else: 1469 else:
1434 bookmarksStr = "" 1470 bookmarksStr = ""
1435 1471
1436 if itm.data(0, self.__latestTagRole): 1472 if self.projectMode and itm.data(0, self.__latestTagRole):
1437 latestTagLinks = [] 1473 latestTagLinks = []
1438 for tag in itm.data(0, self.__latestTagRole): 1474 for tag in itm.data(0, self.__latestTagRole):
1439 url = QUrl() 1475 latestTagLinks.append('<a href="rev:{0}">{1}</a>'.format(
1440 url.setScheme("rev") 1476 self.__getRevisionOfTag(tag)[0], tag))
1441 url.setPath(self.__getRevisionOfTag(tag)[0])
1442 latestTagLinks.append('<a href="{0}">{1}</a>'.format(
1443 url.toString(), tag))
1444 latestTagStr = self.__latestTagTemplate.format( 1477 latestTagStr = self.__latestTagTemplate.format(
1445 ", ".join(latestTagLinks)) 1478 ", ".join(latestTagLinks))
1446 else: 1479 else:
1447 latestTagStr = "" 1480 latestTagStr = ""
1448 1481
1449 rev = int(itm.text(self.RevisionColumn).split(":", 1)[0]) 1482 rev = int(itm.text(self.RevisionColumn).split(":", 1)[0])
1450 1483
1451 parentLinks = [] 1484 if itm.data(0, self.__parentsRole):
1452 for parent in [str(x) for x in itm.data(0, self.__parentsRole)]: 1485 parentLinks = []
1453 url = QUrl() 1486 for parent in [str(x) for x in
1454 url.setScheme("rev") 1487 itm.data(0, self.__parentsRole)]:
1455 url.setPath(parent) 1488 parentLinks.append(
1456 parentLinks.append('<a href="{0}">{1}</a>'.format( 1489 '<a href="rev:{0}">{0}</a>'.format(parent))
1457 url.toString(), parent)) 1490 parentsStr = self.__parentsTemplate.format(
1491 ", ".join(parentLinks))
1492 else:
1493 parentsStr = ""
1458 1494
1459 childLinks = [] 1495 if self.__childrenInfo[rev]:
1460 for child in [str(x) for x in self.__childrenInfo[rev]]: 1496 childLinks = []
1461 url = QUrl() 1497 for child in [str(x) for x in self.__childrenInfo[rev]]:
1462 url.setScheme("rev") 1498 childLinks.append(
1463 url.setPath(child) 1499 '<a href="rev:{0}">{0}</a>'.format(child))
1464 childLinks.append('<a href="{0}">{1}</a>'.format( 1500 childrenStr = self.__childrenTemplate.format(
1465 url.toString(), child)) 1501 ", ".join(childLinks))
1502 else:
1503 childrenStr = ""
1466 1504
1467 self.detailsEdit.setHtml(self.__detailsTemplate.format( 1505 messageStr = "<br />\n".join([
1506 line.strip() for line in itm.data(0, self.__messageRole)
1507 ])
1508
1509 html = self.__detailsTemplate.format(
1468 itm.text(self.RevisionColumn), 1510 itm.text(self.RevisionColumn),
1469 itm.text(self.DateColumn), 1511 itm.text(self.DateColumn),
1470 itm.text(self.AuthorColumn), 1512 itm.text(self.AuthorColumn),
1471 itm.text(self.BranchColumn).replace( 1513 itm.text(self.BranchColumn).replace(
1472 self.ClosedIndicator, ""), 1514 self.ClosedIndicator, ""),
1473 ", ".join(parentLinks), 1515 parentsStr + childrenStr + tagsStr + latestTagStr +
1474 ", ".join(childLinks), 1516 bookmarksStr,
1475 tagsStr + latestTagStr + bookmarksStr, 1517 messageStr,
1476 )) 1518 )
1477 1519 else:
1478 for line in itm.data(0, self.__messageRole): 1520 html = ""
1479 self.messageEdit.append(line.strip()) 1521
1480 1522 return html
1523
1524 def __updateFilesTree(self, parent, itm):
1525 """
1526 Private method to update the files tree with changes of the given item.
1527
1528 @param parent parent for the items to be added
1529 @type QTreeWidget or QTreeWidgetItem
1530 @param itm reference to the item the update should be based on
1531 @type QTreeWidgetItem
1532 """
1533 if itm is not None:
1481 changes = itm.data(0, self.__changesRole) 1534 changes = itm.data(0, self.__changesRole)
1482 if len(changes) > 0: 1535 if len(changes) > 0:
1483 for change in changes: 1536 for change in changes:
1484 self.__generateFileItem( 1537 QTreeWidgetItem(parent, [
1485 change["action"], change["path"], change["copyfrom"]) 1538 self.flags[change["action"]],
1486 self.__resizeColumnsFiles() 1539 change["path"].strip(),
1487 self.__resortFiles() 1540 change["copyfrom"].strip(),
1541 ])
1488 1542
1489 @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem) 1543 @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem)
1490 def on_logTree_currentItemChanged(self, current, previous): 1544 def on_logTree_currentItemChanged(self, current, previous):
1491 """ 1545 """
1492 Private slot called, when the current item of the log tree changes. 1546 Private slot called, when the current item of the log tree changes.
1493 1547
1494 @param current reference to the new current item (QTreeWidgetItem) 1548 @param current reference to the new current item (QTreeWidgetItem)
1495 @param previous reference to the old current item (QTreeWidgetItem) 1549 @param previous reference to the old current item (QTreeWidgetItem)
1496 """ 1550 """
1497 self.__updateGui(current)
1498 self.__updateDiffButtons()
1499 self.__updateToolMenuActions() 1551 self.__updateToolMenuActions()
1500 1552
1501 # Highlight the current entry using a bold font 1553 # Highlight the current entry using a bold font
1502 for col in range(self.logTree.columnCount()): 1554 for col in range(self.logTree.columnCount()):
1503 current and current.setFont(col, self.__logTreeBoldFont) 1555 current and current.setFont(col, self.__logTreeBoldFont)
1504 previous and previous.setFont(col, self.__logTreeNormalFont) 1556 previous and previous.setFont(col, self.__logTreeNormalFont)
1505 1557
1506 # set the state of the up and down buttons 1558 # set the state of the up and down buttons
1507 self.upButton.setEnabled( 1559 self.upButton.setEnabled(
1508 current is not None and 1560 current is not None and
1509 self.logTree.indexOfTopLevelItem(current) > 0) 1561 self.logTree.indexOfTopLevelItem(current) > 0)
1510 self.downButton.setEnabled( 1562 self.downButton.setEnabled(
1511 current is not None and 1563 current is not None and
1512 int(current.text(self.RevisionColumn).split(":")[0]) > 0) 1564 int(current.text(self.RevisionColumn).split(":")[0]) > 0)
1513 1565
1514 @pyqtSlot() 1566 @pyqtSlot()
1515 def on_logTree_itemSelectionChanged(self): 1567 def on_logTree_itemSelectionChanged(self):
1516 """ 1568 """
1517 Private slot called, when the selection has changed. 1569 Private slot called, when the selection has changed.
1518 """ 1570 """
1519 self.__updateDiffButtons() 1571 self.__updateDetailsAndFiles()
1572 self.__updateSbsSelectLabel()
1520 self.__updateToolMenuActions() 1573 self.__updateToolMenuActions()
1574 self.__generateDiffs()
1521 1575
1522 @pyqtSlot() 1576 @pyqtSlot()
1523 def on_upButton_clicked(self): 1577 def on_upButton_clicked(self):
1524 """ 1578 """
1525 Private slot to move the current item up one entry. 1579 Private slot to move the current item up one entry.
1546 """ 1600 """
1547 Private slot to handle the Next button. 1601 Private slot to handle the Next button.
1548 """ 1602 """
1549 if self.__lastRev > 0: 1603 if self.__lastRev > 0:
1550 self.__getLogEntries(startRev=self.__lastRev - 1) 1604 self.__getLogEntries(startRev=self.__lastRev - 1)
1551
1552 @pyqtSlot()
1553 def on_diffP1Button_clicked(self):
1554 """
1555 Private slot to handle the Diff to Parent 1 button.
1556 """
1557 if len(self.logTree.selectedItems()):
1558 itm = self.logTree.selectedItems()[0]
1559 else:
1560 itm = self.logTree.currentItem()
1561 if itm is None:
1562 self.diffP1Button.setEnabled(False)
1563 return
1564 rev2 = int(itm.text(self.RevisionColumn).split(":")[0])
1565
1566 rev1 = itm.data(0, self.__parentsRole)[0]
1567 if rev1 < 0:
1568 self.diffP1Button.setEnabled(False)
1569 return
1570
1571 self.__diffRevisions(rev1, rev2)
1572
1573 @pyqtSlot()
1574 def on_diffP2Button_clicked(self):
1575 """
1576 Private slot to handle the Diff to Parent 2 button.
1577 """
1578 if len(self.logTree.selectedItems()):
1579 itm = self.logTree.selectedItems()[0]
1580 else:
1581 itm = self.logTree.currentItem()
1582 if itm is None:
1583 self.diffP2Button.setEnabled(False)
1584 return
1585 rev2 = int(itm.text(self.RevisionColumn).split(":")[0])
1586
1587 rev1 = itm.data(0, self.__parentsRole)[1]
1588 if rev1 < 0:
1589 self.diffP2Button.setEnabled(False)
1590 return
1591
1592 self.__diffRevisions(rev1, rev2)
1593
1594 @pyqtSlot()
1595 def on_diffRevisionsButton_clicked(self):
1596 """
1597 Private slot to handle the Compare Revisions button.
1598 """
1599 items = self.logTree.selectedItems()
1600
1601 rev2 = int(items[0].text(self.RevisionColumn).split(":")[0])
1602 rev1 = int(items[1].text(self.RevisionColumn).split(":")[0])
1603
1604 self.__diffRevisions(min(rev1, rev2), max(rev1, rev2))
1605 1605
1606 @pyqtSlot(QDate) 1606 @pyqtSlot(QDate)
1607 def on_fromDate_dateChanged(self, date): 1607 def on_fromDate_dateChanged(self, date):
1608 """ 1608 """
1609 Private slot called, when the from date changes. 1609 Private slot called, when the from date changes.
1700 if topItem is currentItem: 1700 if topItem is currentItem:
1701 self.on_logTree_currentItemChanged(topItem, None) 1701 self.on_logTree_currentItemChanged(topItem, None)
1702 else: 1702 else:
1703 topItem.setHidden(True) 1703 topItem.setHidden(True)
1704 if topItem is currentItem: 1704 if topItem is currentItem:
1705 self.messageEdit.clear()
1706 self.filesTree.clear() 1705 self.filesTree.clear()
1707 visibleItemCount -= 1 1706 visibleItemCount -= 1
1708 self.logTree.header().setSectionHidden( 1707 self.logTree.header().setSectionHidden(
1709 self.IconColumn, 1708 self.IconColumn,
1710 visibleItemCount != self.logTree.topLevelItemCount()) 1709 visibleItemCount != self.logTree.topLevelItemCount())
1761 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True) 1760 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
1762 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) 1761 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
1763 1762
1764 self.refreshButton.setEnabled(False) 1763 self.refreshButton.setEnabled(False)
1765 1764
1766 # save the current items commit ID 1765 # save the selected items commit IDs
1767 itm = self.logTree.currentItem() 1766 self.__selectedRevisions = []
1768 if itm is not None: 1767 for item in self.logTree.selectedItems():
1769 self.__currentRevision = itm.text(self.RevisionColumn) 1768 self.__selectedRevisions.append(item.text(self.RevisionColumn))
1770 else:
1771 self.__currentRevision = ""
1772 1769
1773 if self.initialCommandMode in ("incoming", "outgoing"): 1770 if self.initialCommandMode in ("incoming", "outgoing"):
1774 self.nextButton.setEnabled(False) 1771 self.nextButton.setEnabled(False)
1775 self.limitSpinBox.setEnabled(False) 1772 self.limitSpinBox.setEnabled(False)
1776 else: 1773 else:
2194 self.logTree.setCurrentItem(itm) 2191 self.logTree.setCurrentItem(itm)
2195 else: 2192 else:
2196 # load the next batch and try again 2193 # load the next batch and try again
2197 self.on_nextButton_clicked() 2194 self.on_nextButton_clicked()
2198 self.__revisionClicked(url) 2195 self.__revisionClicked(url)
2196
2197 ###########################################################################
2198 ## Diff handling methods below
2199 ###########################################################################
2200
2201 def __generateDiffs(self, parent=1):
2202 """
2203 Private slot to generate diff outputs for the selected item.
2204
2205 @param parent number of parent to diff against
2206 @type int
2207 """
2208 self.diffEdit.clear()
2209 self.diffLabel.setText(self.tr("Differences"))
2210 self.diffSelectLabel.clear()
2211
2212 selectedItems = self.logTree.selectedItems()
2213 if len(selectedItems) == 1:
2214 currentItem = selectedItems[0]
2215 rev2 = currentItem.text(self.RevisionColumn).split(":", 1)[0]
2216 parents = currentItem.data(0, self.__parentsRole)
2217 if len(parents) >= parent:
2218 self.diffLabel.setText(
2219 self.tr("Differences to Parent {0}").format(parent))
2220 rev1 = parents[parent - 1]
2221
2222 self.__diffGenerator.start(self.__filename, [rev1, rev2],
2223 self.__bundle)
2224
2225 if len(parents) > 1:
2226 if parent == 1:
2227 par1 = "&nbsp;1&nbsp;"
2228 else:
2229 par1 = '<a href="diff:1">&nbsp;1&nbsp;</a>'
2230 if parent == 2:
2231 par2 = "&nbsp;2&nbsp;"
2232 else:
2233 par2 = '<a href="diff:2">&nbsp;2&nbsp;</a>'
2234 self.diffSelectLabel.setText(
2235 self.tr('Diff to Parent {0}{1}').format(par1, par2))
2236 elif len(selectedItems) == 2:
2237 rev2 = int(selectedItems[0].text(
2238 self.RevisionColumn).split(":")[0])
2239 rev1 = int(selectedItems[1].text(
2240 self.RevisionColumn).split(":")[0])
2241
2242 self.__diffGenerator.start(self.__filename,
2243 [min(rev1, rev2), max(rev1, rev2)],
2244 self.__bundle)
2245
2246 def __generatorFinished(self):
2247 """
2248 Private slot connected to the finished signal of the diff generator.
2249 """
2250 diff, errors, fileSeparators = self.__diffGenerator.getResult()
2251
2252 if diff:
2253 self.diffEdit.setPlainText("".join(diff))
2254 elif errors:
2255 self.diffEdit.setPlainText("".join(errors))
2256 else:
2257 self.diffEdit.setPlainText(self.tr('There is no difference.'))
2258
2259 self.saveLabel.setVisible(bool(diff))
2260
2261 if self.__diffUpdatesFiles:
2262 for oldFileName, newFileName, lineNumber in fileSeparators:
2263 if oldFileName == newFileName:
2264 fileName = oldFileName
2265 elif oldFileName == "__NULL__":
2266 fileName = newFileName
2267 else:
2268 fileName = oldFileName
2269 item = QTreeWidgetItem(self.filesTree, ["", fileName, ""])
2270 item.setData(0, self.__diffFileLineRole, lineNumber)
2271 self.__resizeColumnsFiles()
2272 self.__resortFiles()
2273 else:
2274 for oldFileName, newFileName, lineNumber in fileSeparators:
2275 for fileName in (oldFileName, newFileName):
2276 if fileName != "__NULL__":
2277 items = self.filesTree.findItems(
2278 fileName, Qt.MatchExactly, 1)
2279 for item in items:
2280 item.setData(0, self.__diffFileLineRole,
2281 lineNumber)
2282
2283 tc = self.diffEdit.textCursor()
2284 tc.movePosition(QTextCursor.Start)
2285 self.diffEdit.setTextCursor(tc)
2286 self.diffEdit.ensureCursorVisible()
2287
2288 @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem)
2289 def on_filesTree_currentItemChanged(self, current, previous):
2290 """
2291 Private slot called, when the current item of the files tree changes.
2292
2293 @param current reference to the new current item (QTreeWidgetItem)
2294 @param previous reference to the old current item (QTreeWidgetItem)
2295 """
2296 if current:
2297 para = current.data(0, self.__diffFileLineRole)
2298 if para is not None:
2299 if para == 0:
2300 tc = self.diffEdit.textCursor()
2301 tc.movePosition(QTextCursor.Start)
2302 self.diffEdit.setTextCursor(tc)
2303 self.diffEdit.ensureCursorVisible()
2304 elif para == -1:
2305 tc = self.diffEdit.textCursor()
2306 tc.movePosition(QTextCursor.End)
2307 self.diffEdit.setTextCursor(tc)
2308 self.diffEdit.ensureCursorVisible()
2309 else:
2310 # step 1: move cursor to end
2311 tc = self.diffEdit.textCursor()
2312 tc.movePosition(QTextCursor.End)
2313 self.diffEdit.setTextCursor(tc)
2314 self.diffEdit.ensureCursorVisible()
2315
2316 # step 2: move cursor to desired line
2317 tc = self.diffEdit.textCursor()
2318 delta = tc.blockNumber() - para
2319 tc.movePosition(QTextCursor.PreviousBlock,
2320 QTextCursor.MoveAnchor, delta)
2321 self.diffEdit.setTextCursor(tc)
2322 self.diffEdit.ensureCursorVisible()
2323
2324 @pyqtSlot(str)
2325 def on_diffSelectLabel_linkActivated(self, link):
2326 """
2327 Private slot to handle the selection of a diff target.
2328
2329 @param link activated link
2330 @type str
2331 """
2332 if ":" in link:
2333 scheme, parent = link.split(":", 1)
2334 if scheme == "diff":
2335 try:
2336 parent = int(parent)
2337 self.__generateDiffs(parent)
2338 except ValueError:
2339 # ignore silently
2340 pass
2341
2342 @pyqtSlot(str)
2343 def on_saveLabel_linkActivated(self, link):
2344 """
2345 Private slot to handle the selection of the save link.
2346
2347 @param link activated link
2348 @type str
2349 """
2350 if ":" not in link:
2351 return
2352
2353 scheme, rest = link.split(":", 1)
2354 if scheme != "save" or rest != "me":
2355 return
2356
2357 if self.projectMode:
2358 fname = self.vcs.splitPath(self.__filename)[0]
2359 fname += "/{0}.diff".format(os.path.split(fname)[-1])
2360 else:
2361 dname, fname = self.vcs.splitPath(self.__filename)
2362 if fname != '.':
2363 fname = "{0}.diff".format(self.__filename)
2364 else:
2365 fname = dname
2366
2367 fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
2368 self,
2369 self.tr("Save Diff"),
2370 fname,
2371 self.tr("Patch Files (*.diff)"),
2372 None,
2373 E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
2374
2375 if not fname:
2376 return # user aborted
2377
2378 ext = QFileInfo(fname).suffix()
2379 if not ext:
2380 ex = selectedFilter.split("(*")[1].split(")")[0]
2381 if ex:
2382 fname += ex
2383 if QFileInfo(fname).exists():
2384 res = E5MessageBox.yesNo(
2385 self,
2386 self.tr("Save Diff"),
2387 self.tr("<p>The patch file <b>{0}</b> already exists."
2388 " Overwrite it?</p>").format(fname),
2389 icon=E5MessageBox.Warning)
2390 if not res:
2391 return
2392 fname = Utilities.toNativeSeparators(fname)
2393
2394 eol = e5App().getObject("Project").getEolString()
2395 try:
2396 f = open(fname, "w", encoding="utf-8", newline="")
2397 f.write(eol.join(self.diffEdit.toPlainText().splitlines()))
2398 f.close()
2399 except IOError as why:
2400 E5MessageBox.critical(
2401 self, self.tr('Save Diff'),
2402 self.tr(
2403 '<p>The patch file <b>{0}</b> could not be saved.'
2404 '<br>Reason: {1}</p>')
2405 .format(fname, str(why)))
2406
2407 @pyqtSlot(str)
2408 def on_sbsSelectLabel_linkActivated(self, link):
2409 """
2410 Private slot to handle selection of a side-by-side link.
2411
2412 @param link text of the selected link
2413 @type str
2414 """
2415 if ":" in link:
2416 scheme, path = link.split(":", 1)
2417 if scheme == "sbsdiff" and "_" in path:
2418 rev1, rev2 = path.split("_", 1)
2419 self.vcs.hgSbsDiff(self.__filename, revisions=(rev1, rev2))

eric ide

mercurial