7 Module implementing a dialog to browse the log history. |
7 Module implementing a dialog to browse the log history. |
8 """ |
8 """ |
9 |
9 |
10 import os |
10 import os |
11 |
11 |
12 from PyQt4.QtCore import pyqtSlot, Qt, QDate, QProcess, QTimer, QRegExp, QSize, QPoint |
12 from PyQt4.QtCore import pyqtSlot, Qt, QDate, QProcess, QTimer, QRegExp, \ |
13 from PyQt4.QtGui import QDialog, QDialogButtonBox, QHeaderView, QTreeWidgetItem, \ |
13 QSize, QPoint |
14 QApplication, QCursor, QWidget, QLineEdit, QColor, QPixmap, \ |
14 from PyQt4.QtGui import QDialog, QDialogButtonBox, QHeaderView, \ |
15 QPainter, QPen, QBrush, QIcon |
15 QTreeWidgetItem, QApplication, QCursor, QWidget, QLineEdit, QColor, \ |
|
16 QPixmap, QPainter, QPen, QBrush, QIcon |
16 |
17 |
17 from E5Gui.E5Application import e5App |
18 from E5Gui.E5Application import e5App |
18 from E5Gui import E5MessageBox |
19 from E5Gui import E5MessageBox |
19 |
20 |
20 from .Ui_HgLogBrowserDialog import Ui_HgLogBrowserDialog |
21 from .Ui_HgLogBrowserDialog import Ui_HgLogBrowserDialog |
79 |
80 |
80 self.fromDate.setDisplayFormat("yyyy-MM-dd") |
81 self.fromDate.setDisplayFormat("yyyy-MM-dd") |
81 self.toDate.setDisplayFormat("yyyy-MM-dd") |
82 self.toDate.setDisplayFormat("yyyy-MM-dd") |
82 self.fromDate.setDate(QDate.currentDate()) |
83 self.fromDate.setDate(QDate.currentDate()) |
83 self.toDate.setDate(QDate.currentDate()) |
84 self.toDate.setDate(QDate.currentDate()) |
84 self.fieldCombo.setCurrentIndex(self.fieldCombo.findText(self.trUtf8("Message"))) |
85 self.fieldCombo.setCurrentIndex(self.fieldCombo.findText( |
|
86 self.trUtf8("Message"))) |
85 self.clearRxEditButton.setIcon(UI.PixmapCache.getIcon("clearLeft.png")) |
87 self.clearRxEditButton.setIcon(UI.PixmapCache.getIcon("clearLeft.png")) |
86 self.limitSpinBox.setValue(self.vcs.getPlugin().getPreferences("LogLimit")) |
88 self.limitSpinBox.setValue(self.vcs.getPlugin().getPreferences( |
87 self.stopCheckBox.setChecked(self.vcs.getPlugin().getPreferences("StopLogOnCopy")) |
89 "LogLimit")) |
|
90 self.stopCheckBox.setChecked(self.vcs.getPlugin().getPreferences( |
|
91 "StopLogOnCopy")) |
88 |
92 |
89 if mode in ("incoming", "outgoing"): |
93 if mode in ("incoming", "outgoing"): |
90 self.nextButton.setEnabled(False) |
94 self.nextButton.setEnabled(False) |
91 self.limitSpinBox.setEnabled(False) |
95 self.limitSpinBox.setEnabled(False) |
92 |
96 |
121 self.__rowHeight = 20 |
125 self.__rowHeight = 20 |
122 |
126 |
123 self.__branchColors = {} |
127 self.__branchColors = {} |
124 self.__allBranchesFilter = self.trUtf8("All") |
128 self.__allBranchesFilter = self.trUtf8("All") |
125 |
129 |
126 self.logTree.setIconSize(QSize(100 * self.__rowHeight, self.__rowHeight)) |
130 self.logTree.setIconSize( |
|
131 QSize(100 * self.__rowHeight, self.__rowHeight)) |
127 |
132 |
128 self.__projectRevision = -1 |
133 self.__projectRevision = -1 |
129 |
134 |
130 def closeEvent(self, e): |
135 def closeEvent(self, e): |
131 """ |
136 """ |
180 |
185 |
181 @param branchName name of the branch (string) |
186 @param branchName name of the branch (string) |
182 @return name of the color to use (string) |
187 @return name of the color to use (string) |
183 """ |
188 """ |
184 if branchName not in self.__branchColors: |
189 if branchName not in self.__branchColors: |
185 self.__branchColors[branchName] = self.__getColor(len(self.__branchColors)) |
190 self.__branchColors[branchName] = self.__getColor( |
|
191 len(self.__branchColors)) |
186 return self.__branchColors[branchName] |
192 return self.__branchColors[branchName] |
187 |
193 |
188 def __generateEdges(self, rev, parents): |
194 def __generateEdges(self, rev, parents): |
189 """ |
195 """ |
190 Private method to generate edge info for the give data. |
196 Private method to generate edge info for the give data. |
221 # add edges to the graph |
227 # add edges to the graph |
222 edges = [] |
228 edges = [] |
223 if parents[0] != -1: |
229 if parents[0] != -1: |
224 for ecol, erev in enumerate(self.__revs): |
230 for ecol, erev in enumerate(self.__revs): |
225 if erev in next: |
231 if erev in next: |
226 edges.append((ecol, next.index(erev), self.__revColors[erev])) |
232 edges.append( |
|
233 (ecol, next.index(erev), self.__revColors[erev])) |
227 elif erev == rev: |
234 elif erev == rev: |
228 for p in parents: |
235 for p in parents: |
229 edges.append((ecol, next.index(p), self.__revColors[p])) |
236 edges.append( |
|
237 (ecol, next.index(p), self.__revColors[p])) |
230 |
238 |
231 self.__revs = next |
239 self.__revs = next |
232 return col, color, edges |
240 return col, color, edges |
233 |
241 |
234 def __generateIcon(self, column, color, bottomedges, topedges, dotColor, currentRev): |
242 def __generateIcon(self, column, color, bottomedges, topedges, dotColor, |
|
243 currentRev, closed): |
235 """ |
244 """ |
236 Private method to generate an icon containing the revision tree for the |
245 Private method to generate an icon containing the revision tree for the |
237 given data. |
246 given data. |
238 |
247 |
239 @param column column index of the revision (integer) |
248 @param column column index of the revision (integer) |
243 @param topedges list of edges for the top of the node |
252 @param topedges list of edges for the top of the node |
244 (list of tuples of three integers) |
253 (list of tuples of three integers) |
245 @param dotColor color to be used for the dot (QColor) |
254 @param dotColor color to be used for the dot (QColor) |
246 @param currentRev flag indicating to draw the icon for the |
255 @param currentRev flag indicating to draw the icon for the |
247 current revision (boolean) |
256 current revision (boolean) |
|
257 @param closed flag indicating to draw an icon for a closed |
|
258 branch (boolean) |
248 @return icon for the node (QIcon) |
259 @return icon for the node (QIcon) |
249 """ |
260 """ |
250 def col2x(col, radius): |
261 def col2x(col, radius): |
251 """ |
262 """ |
252 Local function to calculate a x-position for a column. |
263 Local function to calculate a x-position for a column. |
303 dot_x -= delta |
314 dot_x -= delta |
304 painter.setBrush(dotColor) |
315 painter.setBrush(dotColor) |
305 pen = QPen(pencolor) |
316 pen = QPen(pencolor) |
306 pen.setWidth(penradius) |
317 pen.setWidth(penradius) |
307 painter.setPen(pen) |
318 painter.setPen(pen) |
308 if self.commandMode in ("incoming", "outgoing"): |
319 if closed: |
|
320 painter.drawRect(dot_x - 2, dot_y + 1, |
|
321 radius + 4, radius - 2) |
|
322 elif self.commandMode in ("incoming", "outgoing"): |
309 offset = radius // 2 |
323 offset = radius // 2 |
310 painter.drawConvexPolygon( |
324 painter.drawConvexPolygon( |
311 QPoint(dot_x + offset, dot_y), |
325 QPoint(dot_x + offset, dot_y), |
312 QPoint(dot_x, dot_y + offset), |
326 QPoint(dot_x, dot_y + offset), |
313 QPoint(dot_x + offset, dot_y + 2 * offset), |
327 QPoint(dot_x + offset, dot_y + 2 * offset), |
356 Preferences.getSystem("IOEncoding"), |
371 Preferences.getSystem("IOEncoding"), |
357 'replace') |
372 'replace') |
358 parents = [int(p) for p in output.strip().splitlines()] |
373 parents = [int(p) for p in output.strip().splitlines()] |
359 else: |
374 else: |
360 if not finished: |
375 if not finished: |
361 errMsg = self.trUtf8("The hg process did not finish within 30s.") |
376 errMsg = self.trUtf8( |
|
377 "The hg process did not finish within 30s.") |
362 else: |
378 else: |
363 errMsg = self.trUtf8("Could not start the hg executable.") |
379 errMsg = self.trUtf8("Could not start the hg executable.") |
364 |
380 |
365 if errMsg: |
381 if errMsg: |
366 E5MessageBox.critical(self, |
382 E5MessageBox.critical(self, |
393 self.__projectRevision = output.strip() |
409 self.__projectRevision = output.strip() |
394 if self.__projectRevision.endswith("+"): |
410 if self.__projectRevision.endswith("+"): |
395 self.__projectRevision = self.__projectRevision[:-1] |
411 self.__projectRevision = self.__projectRevision[:-1] |
396 else: |
412 else: |
397 if not finished: |
413 if not finished: |
398 errMsg = self.trUtf8("The hg process did not finish within 30s.") |
414 errMsg = self.trUtf8( |
|
415 "The hg process did not finish within 30s.") |
399 else: |
416 else: |
400 errMsg = self.trUtf8("Could not start the hg executable.") |
417 errMsg = self.trUtf8("Could not start the hg executable.") |
401 |
418 |
402 if errMsg: |
419 if errMsg: |
403 E5MessageBox.critical(self, |
420 E5MessageBox.critical(self, |
431 parts = line.split() |
448 parts = line.split() |
432 self.__closedBranchesRevs.append( |
449 self.__closedBranchesRevs.append( |
433 parts[-2].split(":", 1)[0]) |
450 parts[-2].split(":", 1)[0]) |
434 else: |
451 else: |
435 if not finished: |
452 if not finished: |
436 errMsg = self.trUtf8("The hg process did not finish within 30s.") |
453 errMsg = self.trUtf8( |
|
454 "The hg process did not finish within 30s.") |
437 else: |
455 else: |
438 errMsg = self.trUtf8("Could not start the hg executable.") |
456 errMsg = self.trUtf8("Could not start the hg executable.") |
439 |
457 |
440 if errMsg: |
458 if errMsg: |
441 E5MessageBox.critical(self, |
459 E5MessageBox.critical(self, |
442 self.trUtf8("Mercurial Error"), |
460 self.trUtf8("Mercurial Error"), |
443 errMsg) |
461 errMsg) |
444 |
462 |
445 def __generateLogItem(self, author, date, message, revision, changedPaths, parents, |
463 def __generateLogItem(self, author, date, message, revision, changedPaths, |
446 branches, tags): |
464 parents, branches, tags): |
447 """ |
465 """ |
448 Private method to generate a log tree entry. |
466 Private method to generate a log tree entry. |
449 |
467 |
450 @param author author info (string) |
468 @param author author info (string) |
451 @param date date info (string) |
469 @param date date info (string) |
494 itm.setData(0, self.__edgesRole, edges) |
512 itm.setData(0, self.__edgesRole, edges) |
495 itm.setData(0, self.__parentsRole, parents) |
513 itm.setData(0, self.__parentsRole, parents) |
496 |
514 |
497 if self.logTree.topLevelItemCount() > 1: |
515 if self.logTree.topLevelItemCount() > 1: |
498 topedges = \ |
516 topedges = \ |
499 self.logTree.topLevelItem(self.logTree.indexOfTopLevelItem(itm) - 1)\ |
517 self.logTree.topLevelItem( |
|
518 self.logTree.indexOfTopLevelItem(itm) - 1)\ |
500 .data(0, self.__edgesRole) |
519 .data(0, self.__edgesRole) |
501 else: |
520 else: |
502 topedges = None |
521 topedges = None |
503 |
522 |
504 icon = self.__generateIcon(column, color, edges, topedges, |
523 icon = self.__generateIcon(column, color, edges, topedges, |
505 QColor(self.__branchColor(branches[0])), |
524 QColor(self.__branchColor(branches[0])), |
506 rev == self.__projectRevision) |
525 rev == self.__projectRevision, |
|
526 rev in self.__closedBranchesRevs) |
507 itm.setIcon(0, icon) |
527 itm.setIcon(0, icon) |
508 |
528 |
509 try: |
529 try: |
510 self.__lastRev = int(revision.split(":")[0]) |
530 self.__lastRev = int(revision.split(":")[0]) |
511 except ValueError: |
531 except ValueError: |
572 not self.stopCheckBox.isChecked(): |
592 not self.stopCheckBox.isChecked(): |
573 args.append('--follow') |
593 args.append('--follow') |
574 if self.commandMode == "log": |
594 if self.commandMode == "log": |
575 args.append('--copies') |
595 args.append('--copies') |
576 args.append('--style') |
596 args.append('--style') |
577 args.append(os.path.join(os.path.dirname(__file__), "styles", "logBrowser.style")) |
597 args.append(os.path.join(os.path.dirname(__file__), |
|
598 "styles", "logBrowser.style")) |
578 if self.commandMode == "incoming": |
599 if self.commandMode == "incoming": |
579 if self.bundle: |
600 if self.bundle: |
580 args.append(self.bundle) |
601 args.append(self.bundle) |
581 else: |
602 else: |
582 project = e5App().getObject("Project") |
603 project = e5App().getObject("Project") |
640 self.__processBuffer() |
661 self.__processBuffer() |
641 self.__finish() |
662 self.__finish() |
642 |
663 |
643 def __finish(self): |
664 def __finish(self): |
644 """ |
665 """ |
645 Private slot called when the process finished or the user pressed the button. |
666 Private slot called when the process finished or the user pressed |
|
667 the button. |
646 """ |
668 """ |
647 if self.process is not None and \ |
669 if self.process is not None and \ |
648 self.process.state() != QProcess.NotRunning: |
670 self.process.state() != QProcess.NotRunning: |
649 self.process.terminate() |
671 self.process.terminate() |
650 QTimer.singleShot(2000, self.process.kill) |
672 QTimer.singleShot(2000, self.process.kill) |
680 log["revision"] = value.strip() |
702 log["revision"] = value.strip() |
681 elif key == "user": |
703 elif key == "user": |
682 log["author"] = value.strip() |
704 log["author"] = value.strip() |
683 elif key == "parents": |
705 elif key == "parents": |
684 log["parents"] = \ |
706 log["parents"] = \ |
685 [int(x.split(":", 1)[0]) for x in value.strip().split()] |
707 [int(x.split(":", 1)[0]) |
|
708 for x in value.strip().split()] |
686 elif key == "date": |
709 elif key == "date": |
687 log["date"] = " ".join(value.strip().split()[:2]) |
710 log["date"] = " ".join(value.strip().split()[:2]) |
688 elif key == "description": |
711 elif key == "description": |
689 log["message"].append(value.strip()) |
712 log["message"].append(value.strip()) |
690 elif key == "file_adds": |
713 elif key == "file_adds": |
739 if len(log) > 1: |
762 if len(log) > 1: |
740 self.__generateLogItem(log["author"], log["date"], |
763 self.__generateLogItem(log["author"], log["date"], |
741 log["message"], log["revision"], changedPaths, |
764 log["message"], log["revision"], changedPaths, |
742 log["parents"], log["branches"], log["tags"]) |
765 log["parents"], log["branches"], log["tags"]) |
743 dt = QDate.fromString(log["date"], Qt.ISODate) |
766 dt = QDate.fromString(log["date"], Qt.ISODate) |
744 if not self.__maxDate.isValid() and not self.__minDate.isValid(): |
767 if not self.__maxDate.isValid() and \ |
|
768 not self.__minDate.isValid(): |
745 self.__maxDate = dt |
769 self.__maxDate = dt |
746 self.__minDate = dt |
770 self.__minDate = dt |
747 else: |
771 else: |
748 if self.__maxDate < dt: |
772 if self.__maxDate < dt: |
749 self.__maxDate = dt |
773 self.__maxDate = dt |
784 if not branchFilter: |
808 if not branchFilter: |
785 branchFilter = self.__allBranchesFilter |
809 branchFilter = self.__allBranchesFilter |
786 self.branchCombo.clear() |
810 self.branchCombo.clear() |
787 self.branchCombo.addItems( |
811 self.branchCombo.addItems( |
788 [self.__allBranchesFilter] + sorted(self.__branchColors.keys())) |
812 [self.__allBranchesFilter] + sorted(self.__branchColors.keys())) |
789 self.branchCombo.setCurrentIndex(self.branchCombo.findText(branchFilter)) |
813 self.branchCombo.setCurrentIndex( |
|
814 self.branchCombo.findText(branchFilter)) |
790 |
815 |
791 self.__filterLogsEnabled = True |
816 self.__filterLogsEnabled = True |
792 self.__filterLogs() |
817 self.__filterLogs() |
793 |
818 |
794 def __readStdout(self): |
819 def __readStdout(self): |
1009 """ |
1034 """ |
1010 if self.__filterLogsEnabled: |
1035 if self.__filterLogsEnabled: |
1011 from_ = self.fromDate.date().toString("yyyy-MM-dd") |
1036 from_ = self.fromDate.date().toString("yyyy-MM-dd") |
1012 to_ = self.toDate.date().addDays(1).toString("yyyy-MM-dd") |
1037 to_ = self.toDate.date().addDays(1).toString("yyyy-MM-dd") |
1013 branch = self.branchCombo.currentText() |
1038 branch = self.branchCombo.currentText() |
|
1039 closedBranch = branch + '--' |
1014 |
1040 |
1015 txt = self.fieldCombo.currentText() |
1041 txt = self.fieldCombo.currentText() |
1016 if txt == self.trUtf8("Author"): |
1042 if txt == self.trUtf8("Author"): |
1017 fieldIndex = self.AuthorColumn |
1043 fieldIndex = self.AuthorColumn |
1018 searchRx = QRegExp(self.rxEdit.text(), Qt.CaseInsensitive) |
1044 searchRx = QRegExp(self.rxEdit.text(), Qt.CaseInsensitive) |
1019 elif txt == self.trUtf8("Revision"): |
1045 elif txt == self.trUtf8("Revision"): |
1020 fieldIndex = self.RevisionColumn |
1046 fieldIndex = self.RevisionColumn |
1021 txt = self.rxEdit.text() |
1047 txt = self.rxEdit.text() |
1022 if txt.startswith("^"): |
1048 if txt.startswith("^"): |
1023 searchRx = QRegExp("^\s*{0}".format(txt[1:]), Qt.CaseInsensitive) |
1049 searchRx = QRegExp("^\s*{0}".format(txt[1:]), |
|
1050 Qt.CaseInsensitive) |
1024 else: |
1051 else: |
1025 searchRx = QRegExp(txt, Qt.CaseInsensitive) |
1052 searchRx = QRegExp(txt, Qt.CaseInsensitive) |
1026 else: |
1053 else: |
1027 fieldIndex = self.MessageColumn |
1054 fieldIndex = self.MessageColumn |
1028 searchRx = QRegExp(self.rxEdit.text(), Qt.CaseInsensitive) |
1055 searchRx = QRegExp(self.rxEdit.text(), Qt.CaseInsensitive) |
1031 for topIndex in range(self.logTree.topLevelItemCount()): |
1058 for topIndex in range(self.logTree.topLevelItemCount()): |
1032 topItem = self.logTree.topLevelItem(topIndex) |
1059 topItem = self.logTree.topLevelItem(topIndex) |
1033 if topItem.text(self.DateColumn) <= to_ and \ |
1060 if topItem.text(self.DateColumn) <= to_ and \ |
1034 topItem.text(self.DateColumn) >= from_ and \ |
1061 topItem.text(self.DateColumn) >= from_ and \ |
1035 (branch == self.__allBranchesFilter or \ |
1062 (branch == self.__allBranchesFilter or \ |
1036 topItem.text(self.BranchColumn) == branch) and \ |
1063 topItem.text(self.BranchColumn) in \ |
|
1064 [branch, closedBranch]) and \ |
1037 searchRx.indexIn(topItem.text(fieldIndex)) > -1: |
1065 searchRx.indexIn(topItem.text(fieldIndex)) > -1: |
1038 topItem.setHidden(False) |
1066 topItem.setHidden(False) |
1039 if topItem is currentItem: |
1067 if topItem is currentItem: |
1040 self.on_logTree_currentItemChanged(topItem, None) |
1068 self.on_logTree_currentItemChanged(topItem, None) |
1041 else: |
1069 else: |