28 COLORNAMES = ["blue", "darkgreen", "red", "green", "darkblue", "purple", |
28 COLORNAMES = ["blue", "darkgreen", "red", "green", "darkblue", "purple", |
29 "cyan", "olive", "magenta", "darkred", "darkmagenta", |
29 "cyan", "olive", "magenta", "darkred", "darkmagenta", |
30 "darkcyan", "gray", "yellow"] |
30 "darkcyan", "gray", "yellow"] |
31 COLORS = [str(QColor(x).name()) for x in COLORNAMES] |
31 COLORS = [str(QColor(x).name()) for x in COLORNAMES] |
32 |
32 |
|
33 |
33 class HgLogBrowserDialog(QDialog, Ui_HgLogBrowserDialog): |
34 class HgLogBrowserDialog(QDialog, Ui_HgLogBrowserDialog): |
34 """ |
35 """ |
35 Class implementing a dialog to browse the log history. |
36 Class implementing a dialog to browse the log history. |
36 """ |
37 """ |
37 IconColumn = 0 |
38 IconColumn = 0 |
38 BranchColumn = 1 |
39 BranchColumn = 1 |
39 RevisionColumn = 2 |
40 RevisionColumn = 2 |
40 AuthorColumn = 3 |
41 AuthorColumn = 3 |
41 DateColumn = 4 |
42 DateColumn = 4 |
42 MessageColumn = 5 |
43 MessageColumn = 5 |
43 TagsColumn = 6 |
44 TagsColumn = 6 |
44 |
45 |
45 def __init__(self, vcs, mode = "log", bundle = None, parent = None): |
46 def __init__(self, vcs, mode="log", bundle=None, parent=None): |
46 """ |
47 """ |
47 Constructor |
48 Constructor |
48 |
49 |
49 @param vcs reference to the vcs object |
50 @param vcs reference to the vcs object |
50 @param mode mode of the dialog (string; one of log, incoming, outgoing) |
51 @param mode mode of the dialog (string; one of log, incoming, outgoing) |
94 self.nextButton.setEnabled(False) |
95 self.nextButton.setEnabled(False) |
95 self.limitSpinBox.setEnabled(False) |
96 self.limitSpinBox.setEnabled(False) |
96 |
97 |
97 self.__messageRole = Qt.UserRole |
98 self.__messageRole = Qt.UserRole |
98 self.__changesRole = Qt.UserRole + 1 |
99 self.__changesRole = Qt.UserRole + 1 |
99 self.__edgesRole = Qt.UserRole + 2 |
100 self.__edgesRole = Qt.UserRole + 2 |
100 self.__parentsRole = Qt.UserRole + 3 |
101 self.__parentsRole = Qt.UserRole + 3 |
101 |
102 |
102 self.process = QProcess() |
103 self.process = QProcess() |
103 self.process.finished.connect(self.__procFinished) |
104 self.process.finished.connect(self.__procFinished) |
104 self.process.readyReadStandardOutput.connect(self.__readStdout) |
105 self.process.readyReadStandardOutput.connect(self.__readStdout) |
105 self.process.readyReadStandardError.connect(self.__readStderr) |
106 self.process.readyReadStandardError.connect(self.__readStderr) |
106 |
107 |
107 self.flags = { |
108 self.flags = { |
108 'A' : self.trUtf8('Added'), |
109 'A': self.trUtf8('Added'), |
109 'D' : self.trUtf8('Deleted'), |
110 'D': self.trUtf8('Deleted'), |
110 'M' : self.trUtf8('Modified'), |
111 'M': self.trUtf8('Modified'), |
111 } |
112 } |
112 |
113 |
113 self.buf = [] # buffer for stdout |
114 self.buf = [] # buffer for stdout |
114 self.diff = None |
115 self.diff = None |
115 self.__started = False |
116 self.__started = False |
163 def __resortFiles(self): |
164 def __resortFiles(self): |
164 """ |
165 """ |
165 Private method to resort the changed files tree. |
166 Private method to resort the changed files tree. |
166 """ |
167 """ |
167 sortColumn = self.filesTree.sortColumn() |
168 sortColumn = self.filesTree.sortColumn() |
168 self.filesTree.sortItems(1, |
169 self.filesTree.sortItems(1, |
169 self.filesTree.header().sortIndicatorOrder()) |
170 self.filesTree.header().sortIndicatorOrder()) |
170 self.filesTree.sortItems(sortColumn, |
171 self.filesTree.sortItems(sortColumn, |
171 self.filesTree.header().sortIndicatorOrder()) |
172 self.filesTree.header().sortIndicatorOrder()) |
172 |
173 |
173 def __getColor(self, n): |
174 def __getColor(self, n): |
174 """ |
175 """ |
175 Private method to get the (rotating) name of the color given an index. |
176 Private method to get the (rotating) name of the color given an index. |
197 |
198 |
198 @param rev revision to calculate edge info for (integer) |
199 @param rev revision to calculate edge info for (integer) |
199 @param parents list of parent revisions (list of integers) |
200 @param parents list of parent revisions (list of integers) |
200 @return tuple containing the column and color index for |
201 @return tuple containing the column and color index for |
201 the given node and a list of tuples indicating the edges |
202 the given node and a list of tuples indicating the edges |
202 between the given node and its parents |
203 between the given node and its parents |
203 (integer, integer, [(integer, integer, integer), ...]) |
204 (integer, integer, [(integer, integer, integer), ...]) |
204 """ |
205 """ |
205 if rev not in self.__revs: |
206 if rev not in self.__revs: |
206 # new head |
207 # new head |
207 self.__revs.append(rev) |
208 self.__revs.append(rev) |
245 Private method to generate an icon containing the revision tree for the |
246 Private method to generate an icon containing the revision tree for the |
246 given data. |
247 given data. |
247 |
248 |
248 @param column column index of the revision (integer) |
249 @param column column index of the revision (integer) |
249 @param color color of the node (integer) |
250 @param color color of the node (integer) |
250 @param bottomedges list of edges for the bottom of the node |
251 @param bottomedges list of edges for the bottom of the node |
251 (list of tuples of three integers) |
252 (list of tuples of three integers) |
252 @param topedges list of edges for the top of the node |
253 @param topedges list of edges for the top of the node |
253 (list of tuples of three integers) |
254 (list of tuples of three integers) |
254 @param dotColor color to be used for the dot (QColor) |
255 @param dotColor color to be used for the dot (QColor) |
255 @param currentRev flag indicating to draw the icon for the |
256 @param currentRev flag indicating to draw the icon for the |
256 current revision (boolean) |
257 current revision (boolean) |
257 @param closed flag indicating to draw an icon for a closed |
258 @param closed flag indicating to draw an icon for a closed |
320 painter.drawRect(dot_x - 2, dot_y + 1, |
321 painter.drawRect(dot_x - 2, dot_y + 1, |
321 radius + 4, radius - 2) |
322 radius + 4, radius - 2) |
322 elif self.commandMode in ("incoming", "outgoing"): |
323 elif self.commandMode in ("incoming", "outgoing"): |
323 offset = radius // 2 |
324 offset = radius // 2 |
324 painter.drawConvexPolygon( |
325 painter.drawConvexPolygon( |
325 QPoint(dot_x + offset, dot_y), |
326 QPoint(dot_x + offset, dot_y), |
326 QPoint(dot_x, dot_y + offset), |
327 QPoint(dot_x, dot_y + offset), |
327 QPoint(dot_x + offset, dot_y + 2 * offset), |
328 QPoint(dot_x + offset, dot_y + 2 * offset), |
328 QPoint(dot_x + 2 * offset, dot_y + offset) |
329 QPoint(dot_x + 2 * offset, dot_y + offset) |
329 ) |
330 ) |
330 else: |
331 else: |
331 painter.drawEllipse(dot_x, dot_y, radius, radius) |
332 painter.drawEllipse(dot_x, dot_y, radius, radius) |
332 painter.end() |
333 painter.end() |
365 procStarted = process.waitForStarted() |
366 procStarted = process.waitForStarted() |
366 if procStarted: |
367 if procStarted: |
367 finished = process.waitForFinished(30000) |
368 finished = process.waitForFinished(30000) |
368 if finished and process.exitCode() == 0: |
369 if finished and process.exitCode() == 0: |
369 output = \ |
370 output = \ |
370 str(process.readAllStandardOutput(), |
371 str(process.readAllStandardOutput(), |
371 Preferences.getSystem("IOEncoding"), |
372 Preferences.getSystem("IOEncoding"), |
372 'replace') |
373 'replace') |
373 parents = [int(p) for p in output.strip().splitlines()] |
374 parents = [int(p) for p in output.strip().splitlines()] |
374 else: |
375 else: |
375 if not finished: |
376 if not finished: |
376 errMsg = self.trUtf8( |
377 errMsg = self.trUtf8( |
401 procStarted = process.waitForStarted() |
402 procStarted = process.waitForStarted() |
402 if procStarted: |
403 if procStarted: |
403 finished = process.waitForFinished(30000) |
404 finished = process.waitForFinished(30000) |
404 if finished and process.exitCode() == 0: |
405 if finished and process.exitCode() == 0: |
405 output = \ |
406 output = \ |
406 str(process.readAllStandardOutput(), |
407 str(process.readAllStandardOutput(), |
407 Preferences.getSystem("IOEncoding"), |
408 Preferences.getSystem("IOEncoding"), |
408 'replace') |
409 'replace') |
409 self.__projectRevision = output.strip() |
410 self.__projectRevision = output.strip() |
410 if self.__projectRevision.endswith("+"): |
411 if self.__projectRevision.endswith("+"): |
411 self.__projectRevision = self.__projectRevision[:-1] |
412 self.__projectRevision = self.__projectRevision[:-1] |
412 else: |
413 else: |
438 procStarted = process.waitForStarted() |
439 procStarted = process.waitForStarted() |
439 if procStarted: |
440 if procStarted: |
440 finished = process.waitForFinished(30000) |
441 finished = process.waitForFinished(30000) |
441 if finished and process.exitCode() == 0: |
442 if finished and process.exitCode() == 0: |
442 output = \ |
443 output = \ |
443 str(process.readAllStandardOutput(), |
444 str(process.readAllStandardOutput(), |
444 Preferences.getSystem("IOEncoding"), |
445 Preferences.getSystem("IOEncoding"), |
445 'replace') |
446 'replace') |
446 for line in output.splitlines(): |
447 for line in output.splitlines(): |
447 if line.strip().endswith("(closed)"): |
448 if line.strip().endswith("(closed)"): |
448 parts = line.split() |
449 parts = line.split() |
449 self.__closedBranchesRevs.append( |
450 self.__closedBranchesRevs.append( |
487 closedStr = "" |
488 closedStr = "" |
488 msgtxt = msg[0] |
489 msgtxt = msg[0] |
489 if len(msgtxt) > 30: |
490 if len(msgtxt) > 30: |
490 msgtxt = "{0}...".format(msgtxt[:30]) |
491 msgtxt = "{0}...".format(msgtxt[:30]) |
491 itm = QTreeWidgetItem(self.logTree, [ |
492 itm = QTreeWidgetItem(self.logTree, [ |
492 "", |
493 "", |
493 branches[0] + closedStr, |
494 branches[0] + closedStr, |
494 "{0:>7}:{1}".format(rev, node), |
495 "{0:>7}:{1}".format(rev, node), |
495 author, |
496 author, |
496 date, |
497 date, |
497 msgtxt, |
498 msgtxt, |
498 ", ".join(tags), |
499 ", ".join(tags), |
499 ]) |
500 ]) |
500 |
501 |
501 itm.setForeground(self.BranchColumn, |
502 itm.setForeground(self.BranchColumn, |
502 QBrush(QColor(self.__branchColor(branches[0])))) |
503 QBrush(QColor(self.__branchColor(branches[0])))) |
503 |
504 |
504 if not self.projectMode: |
505 if not self.projectMode: |
505 parents = self.__getParents(rev) |
506 parents = self.__getParents(rev) |
506 if not parents: |
507 if not parents: |
518 self.logTree.indexOfTopLevelItem(itm) - 1)\ |
519 self.logTree.indexOfTopLevelItem(itm) - 1)\ |
519 .data(0, self.__edgesRole) |
520 .data(0, self.__edgesRole) |
520 else: |
521 else: |
521 topedges = None |
522 topedges = None |
522 |
523 |
523 icon = self.__generateIcon(column, color, edges, topedges, |
524 icon = self.__generateIcon(column, color, edges, topedges, |
524 QColor(self.__branchColor(branches[0])), |
525 QColor(self.__branchColor(branches[0])), |
525 rev == self.__projectRevision, |
526 rev == self.__projectRevision, |
526 rev in self.__closedBranchesRevs) |
527 rev in self.__closedBranchesRevs) |
527 itm.setIcon(0, icon) |
528 itm.setIcon(0, icon) |
528 |
529 |
529 try: |
530 try: |
530 self.__lastRev = int(revision.split(":")[0]) |
531 self.__lastRev = int(revision.split(":")[0]) |
541 @param path path of the file in the repository (string) |
542 @param path path of the file in the repository (string) |
542 @param copyfrom path the file was copied from (string) |
543 @param copyfrom path the file was copied from (string) |
543 @return reference to the generated item (QTreeWidgetItem) |
544 @return reference to the generated item (QTreeWidgetItem) |
544 """ |
545 """ |
545 itm = QTreeWidgetItem(self.filesTree, [ |
546 itm = QTreeWidgetItem(self.filesTree, [ |
546 self.flags[action], |
547 self.flags[action], |
547 path, |
548 path, |
548 copyfrom, |
549 copyfrom, |
549 ]) |
550 ]) |
550 |
551 |
551 return itm |
552 return itm |
552 |
553 |
553 def __getLogEntries(self, startRev = None): |
554 def __getLogEntries(self, startRev=None): |
554 """ |
555 """ |
555 Private method to retrieve log entries from the repository. |
556 Private method to retrieve log entries from the repository. |
556 |
557 |
557 @param startRev revision number to start from (integer, string) |
558 @param startRev revision number to start from (integer, string) |
558 """ |
559 """ |
592 not self.stopCheckBox.isChecked(): |
593 not self.stopCheckBox.isChecked(): |
593 args.append('--follow') |
594 args.append('--follow') |
594 if self.commandMode == "log": |
595 if self.commandMode == "log": |
595 args.append('--copies') |
596 args.append('--copies') |
596 args.append('--style') |
597 args.append('--style') |
597 args.append(os.path.join(os.path.dirname(__file__), |
598 args.append(os.path.join(os.path.dirname(__file__), |
598 "styles", "logBrowser.style")) |
599 "styles", "logBrowser.style")) |
599 if self.commandMode == "incoming": |
600 if self.commandMode == "incoming": |
600 if self.bundle: |
601 if self.bundle: |
601 args.append(self.bundle) |
602 args.append(self.bundle) |
602 else: |
603 else: |
702 log["revision"] = value.strip() |
703 log["revision"] = value.strip() |
703 elif key == "user": |
704 elif key == "user": |
704 log["author"] = value.strip() |
705 log["author"] = value.strip() |
705 elif key == "parents": |
706 elif key == "parents": |
706 log["parents"] = \ |
707 log["parents"] = \ |
707 [int(x.split(":", 1)[0]) |
708 [int(x.split(":", 1)[0]) |
708 for x in value.strip().split()] |
709 for x in value.strip().split()] |
709 elif key == "date": |
710 elif key == "date": |
710 log["date"] = " ".join(value.strip().split()[:2]) |
711 log["date"] = " ".join(value.strip().split()[:2]) |
711 elif key == "description": |
712 elif key == "description": |
712 log["message"].append(value.strip()) |
713 log["message"].append(value.strip()) |
713 elif key == "file_adds": |
714 elif key == "file_adds": |
714 if value.strip(): |
715 if value.strip(): |
715 for f in value.strip().split(", "): |
716 for f in value.strip().split(", "): |
716 if f in fileCopies: |
717 if f in fileCopies: |
717 changedPaths.append({ |
718 changedPaths.append({ |
718 "action" : "A", |
719 "action": "A", |
719 "path" : f, |
720 "path": f, |
720 "copyfrom" : fileCopies[f], |
721 "copyfrom": fileCopies[f], |
721 }) |
722 }) |
722 else: |
723 else: |
723 changedPaths.append({ |
724 changedPaths.append({ |
724 "action" : "A", |
725 "action": "A", |
725 "path" : f, |
726 "path": f, |
726 "copyfrom" : "", |
727 "copyfrom": "", |
727 }) |
728 }) |
728 elif key == "files_mods": |
729 elif key == "files_mods": |
729 if value.strip(): |
730 if value.strip(): |
730 for f in value.strip().split(", "): |
731 for f in value.strip().split(", "): |
731 changedPaths.append({ |
732 changedPaths.append({ |
732 "action" : "M", |
733 "action": "M", |
733 "path" : f, |
734 "path": f, |
734 "copyfrom" : "", |
735 "copyfrom": "", |
735 }) |
736 }) |
736 elif key == "file_dels": |
737 elif key == "file_dels": |
737 if value.strip(): |
738 if value.strip(): |
738 for f in value.strip().split(", "): |
739 for f in value.strip().split(", "): |
739 changedPaths.append({ |
740 changedPaths.append({ |
740 "action" : "D", |
741 "action": "D", |
741 "path" : f, |
742 "path": f, |
742 "copyfrom" : "", |
743 "copyfrom": "", |
743 }) |
744 }) |
744 elif key == "file_copies": |
745 elif key == "file_copies": |
745 if value.strip(): |
746 if value.strip(): |
746 for entry in value.strip().split(", "): |
747 for entry in value.strip().split(", "): |
747 newName, oldName = entry[:-1].split(" (") |
748 newName, oldName = entry[:-1].split(" (") |
758 continue |
759 continue |
759 if value.strip(): |
760 if value.strip(): |
760 log["message"].append(value.strip()) |
761 log["message"].append(value.strip()) |
761 else: |
762 else: |
762 if len(log) > 1: |
763 if len(log) > 1: |
763 self.__generateLogItem(log["author"], log["date"], |
764 self.__generateLogItem(log["author"], log["date"], |
764 log["message"], log["revision"], changedPaths, |
765 log["message"], log["revision"], changedPaths, |
765 log["parents"], log["branches"], log["tags"]) |
766 log["parents"], log["branches"], log["tags"]) |
766 dt = QDate.fromString(log["date"], Qt.ISODate) |
767 dt = QDate.fromString(log["date"], Qt.ISODate) |
767 if not self.__maxDate.isValid() and \ |
768 if not self.__maxDate.isValid() and \ |
768 not self.__minDate.isValid(): |
769 not self.__minDate.isValid(): |
816 self.__filterLogsEnabled = True |
817 self.__filterLogsEnabled = True |
817 self.__filterLogs() |
818 self.__filterLogs() |
818 |
819 |
819 def __readStdout(self): |
820 def __readStdout(self): |
820 """ |
821 """ |
821 Private slot to handle the readyReadStandardOutput signal. |
822 Private slot to handle the readyReadStandardOutput signal. |
822 |
823 |
823 It reads the output of the process and inserts it into a buffer. |
824 It reads the output of the process and inserts it into a buffer. |
824 """ |
825 """ |
825 self.process.setReadChannel(QProcess.StandardOutput) |
826 self.process.setReadChannel(QProcess.StandardOutput) |
826 |
827 |
827 while self.process.canReadLine(): |
828 while self.process.canReadLine(): |
828 line = str(self.process.readLine(), |
829 line = str(self.process.readLine(), |
829 Preferences.getSystem("IOEncoding"), |
830 Preferences.getSystem("IOEncoding"), |
830 'replace') |
831 'replace') |
831 self.buf.append(line) |
832 self.buf.append(line) |
832 |
833 |
833 def __readStderr(self): |
834 def __readStderr(self): |
834 """ |
835 """ |
837 It reads the error output of the process and inserts it into the |
838 It reads the error output of the process and inserts it into the |
838 error pane. |
839 error pane. |
839 """ |
840 """ |
840 if self.process is not None: |
841 if self.process is not None: |
841 self.errorGroup.show() |
842 self.errorGroup.show() |
842 s = str(self.process.readAllStandardError(), |
843 s = str(self.process.readAllStandardError(), |
843 Preferences.getSystem("IOEncoding"), |
844 Preferences.getSystem("IOEncoding"), |
844 'replace') |
845 'replace') |
845 self.errors.insertPlainText(s) |
846 self.errors.insertPlainText(s) |
846 self.errors.ensureCursorVisible() |
847 self.errors.ensureCursorVisible() |
847 |
848 |
848 def __diffRevisions(self, rev1, rev2): |
849 def __diffRevisions(self, rev1, rev2): |
1082 @pyqtSlot(bool) |
1083 @pyqtSlot(bool) |
1083 def on_stopCheckBox_clicked(self, checked): |
1084 def on_stopCheckBox_clicked(self, checked): |
1084 """ |
1085 """ |
1085 Private slot called, when the stop on copy/move checkbox is clicked |
1086 Private slot called, when the stop on copy/move checkbox is clicked |
1086 """ |
1087 """ |
1087 self.vcs.getPlugin().setPreferences("StopLogOnCopy", |
1088 self.vcs.getPlugin().setPreferences("StopLogOnCopy", |
1088 self.stopCheckBox.isChecked()) |
1089 self.stopCheckBox.isChecked()) |
1089 self.nextButton.setEnabled(True) |
1090 self.nextButton.setEnabled(True) |
1090 self.limitSpinBox.setEnabled(True) |
1091 self.limitSpinBox.setEnabled(True) |
1091 |
1092 |
1092 def on_passwordCheckBox_toggled(self, isOn): |
1093 def on_passwordCheckBox_toggled(self, isOn): |