5 |
5 |
6 """ |
6 """ |
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 |
|
11 import os |
10 import os |
12 import re |
11 import re |
13 import collections |
12 import collections |
14 |
13 |
15 from PyQt5.QtCore import ( |
14 from PyQt5.QtCore import pyqtSlot, Qt, QDate, QRegExp, QSize, QPoint, QFileInfo |
16 pyqtSlot, Qt, QDate, QProcess, QTimer, QRegExp, QSize, QPoint, QFileInfo |
|
17 ) |
|
18 from PyQt5.QtGui import ( |
15 from PyQt5.QtGui import ( |
19 QCursor, QColor, QPixmap, QPainter, QPen, QBrush, QIcon, QTextCursor |
16 QCursor, QColor, QPixmap, QPainter, QPen, QBrush, QIcon, QTextCursor |
20 ) |
17 ) |
21 from PyQt5.QtWidgets import ( |
18 from PyQt5.QtWidgets import ( |
22 QWidget, QDialogButtonBox, QHeaderView, QTreeWidgetItem, QApplication, |
19 QWidget, QDialogButtonBox, QHeaderView, QTreeWidgetItem, QApplication, |
32 from .HgDiffGenerator import HgDiffGenerator |
29 from .HgDiffGenerator import HgDiffGenerator |
33 |
30 |
34 import UI.PixmapCache |
31 import UI.PixmapCache |
35 import Preferences |
32 import Preferences |
36 import Utilities |
33 import Utilities |
37 from Globals import strToQByteArray |
|
38 |
34 |
39 COLORNAMES = ["blue", "darkgreen", "red", "green", "darkblue", "purple", |
35 COLORNAMES = ["blue", "darkgreen", "red", "green", "darkblue", "purple", |
40 "cyan", "olive", "magenta", "darkred", "darkmagenta", |
36 "cyan", "olive", "magenta", "darkred", "darkmagenta", |
41 "darkcyan", "gray", "yellow"] |
37 "darkcyan", "gray", "yellow"] |
42 COLORS = [str(QColor(x).name()) for x in COLORNAMES] |
38 COLORS = [str(QColor(x).name()) for x in COLORNAMES] |
198 self.__incomingRole = Qt.UserRole + 5 |
194 self.__incomingRole = Qt.UserRole + 5 |
199 |
195 |
200 # roles used in the file tree |
196 # roles used in the file tree |
201 self.__diffFileLineRole = Qt.UserRole |
197 self.__diffFileLineRole = Qt.UserRole |
202 |
198 |
203 if self.__hgClient: |
|
204 self.process = None |
|
205 else: |
|
206 self.process = QProcess() |
|
207 self.process.finished.connect(self.__procFinished) |
|
208 self.process.readyReadStandardOutput.connect(self.__readStdout) |
|
209 self.process.readyReadStandardError.connect(self.__readStderr) |
|
210 |
|
211 self.flags = { |
199 self.flags = { |
212 'A': self.tr('Added'), |
200 'A': self.tr('Added'), |
213 'D': self.tr('Deleted'), |
201 'D': self.tr('Deleted'), |
214 'M': self.tr('Modified'), |
202 'M': self.tr('Modified'), |
215 } |
203 } |
428 """ |
416 """ |
429 Protected slot implementing a close event handler. |
417 Protected slot implementing a close event handler. |
430 |
418 |
431 @param e close event (QCloseEvent) |
419 @param e close event (QCloseEvent) |
432 """ |
420 """ |
433 if self.__hgClient: |
421 if self.__hgClient.isExecuting(): |
434 if self.__hgClient.isExecuting(): |
422 self.__hgClient.cancel() |
435 self.__hgClient.cancel() |
|
436 else: |
|
437 if ( |
|
438 self.process is not None and |
|
439 self.process.state() != QProcess.NotRunning |
|
440 ): |
|
441 self.process.terminate() |
|
442 QTimer.singleShot(2000, self.process.kill) |
|
443 self.process.waitForFinished(3000) |
|
444 |
423 |
445 self.vcs.getPlugin().setPreferences( |
424 self.vcs.getPlugin().setPreferences( |
446 "LogBrowserGeometry", self.saveGeometry()) |
425 "LogBrowserGeometry", self.saveGeometry()) |
447 self.vcs.getPlugin().setPreferences( |
426 self.vcs.getPlugin().setPreferences( |
448 "LogBrowserSplitterStates", [ |
427 "LogBrowserSplitterStates", [ |
754 args.append("-r") |
733 args.append("-r") |
755 args.append(rev) |
734 args.append(rev) |
756 if not self.projectMode: |
735 if not self.projectMode: |
757 args.append(self.__filename) |
736 args.append(self.__filename) |
758 |
737 |
759 output = "" |
738 output, errMsg = self.__hgClient.runcommand(args) |
760 if self.__hgClient: |
|
761 output, errMsg = self.__hgClient.runcommand(args) |
|
762 else: |
|
763 process = QProcess() |
|
764 process.setWorkingDirectory(self.repodir) |
|
765 process.start('hg', args) |
|
766 procStarted = process.waitForStarted(5000) |
|
767 if procStarted: |
|
768 finished = process.waitForFinished(30000) |
|
769 if finished and process.exitCode() == 0: |
|
770 output = str(process.readAllStandardOutput(), |
|
771 self.vcs.getEncoding(), 'replace') |
|
772 else: |
|
773 if not finished: |
|
774 errMsg = self.tr( |
|
775 "The hg process did not finish within 30s.") |
|
776 else: |
|
777 errMsg = self.tr("Could not start the hg executable.") |
|
778 |
|
779 if errMsg: |
|
780 E5MessageBox.critical( |
|
781 self, |
|
782 self.tr("Mercurial Error"), |
|
783 errMsg) |
|
784 |
739 |
785 if output: |
740 if output: |
786 parents = [int(p) for p in output.strip().splitlines()] |
741 parents = [int(p) for p in output.strip().splitlines()] |
787 |
742 |
788 return parents |
743 return parents |
794 errMsg = "" |
749 errMsg = "" |
795 |
750 |
796 args = self.vcs.initCommand("identify") |
751 args = self.vcs.initCommand("identify") |
797 args.append("-nb") |
752 args.append("-nb") |
798 |
753 |
799 output = "" |
754 output, errMsg = self.__hgClient.runcommand(args) |
800 if self.__hgClient: |
|
801 output, errMsg = self.__hgClient.runcommand(args) |
|
802 else: |
|
803 process = QProcess() |
|
804 process.setWorkingDirectory(self.repodir) |
|
805 process.start('hg', args) |
|
806 procStarted = process.waitForStarted(5000) |
|
807 if procStarted: |
|
808 finished = process.waitForFinished(30000) |
|
809 if finished and process.exitCode() == 0: |
|
810 output = str(process.readAllStandardOutput(), |
|
811 self.vcs.getEncoding(), 'replace') |
|
812 else: |
|
813 if not finished: |
|
814 errMsg = self.tr( |
|
815 "The hg process did not finish within 30s.") |
|
816 else: |
|
817 errMsg = self.tr("Could not start the hg executable.") |
|
818 |
755 |
819 if errMsg: |
756 if errMsg: |
820 E5MessageBox.critical( |
757 E5MessageBox.critical( |
821 self, |
758 self, |
822 self.tr("Mercurial Error"), |
759 self.tr("Mercurial Error"), |
841 errMsg = "" |
778 errMsg = "" |
842 |
779 |
843 args = self.vcs.initCommand("branches") |
780 args = self.vcs.initCommand("branches") |
844 args.append("--closed") |
781 args.append("--closed") |
845 |
782 |
846 output = "" |
783 output, errMsg = self.__hgClient.runcommand(args) |
847 if self.__hgClient: |
|
848 output, errMsg = self.__hgClient.runcommand(args) |
|
849 else: |
|
850 process = QProcess() |
|
851 process.setWorkingDirectory(self.repodir) |
|
852 process.start('hg', args) |
|
853 procStarted = process.waitForStarted(5000) |
|
854 if procStarted: |
|
855 finished = process.waitForFinished(30000) |
|
856 if finished and process.exitCode() == 0: |
|
857 output = str(process.readAllStandardOutput(), |
|
858 self.vcs.getEncoding(), 'replace') |
|
859 else: |
|
860 if not finished: |
|
861 errMsg = self.tr( |
|
862 "The hg process did not finish within 30s.") |
|
863 else: |
|
864 errMsg = self.tr("Could not start the hg executable.") |
|
865 |
784 |
866 if errMsg: |
785 if errMsg: |
867 E5MessageBox.critical( |
786 E5MessageBox.critical( |
868 self, |
787 self, |
869 self.tr("Mercurial Error"), |
788 self.tr("Mercurial Error"), |
886 args = self.vcs.initCommand("heads") |
805 args = self.vcs.initCommand("heads") |
887 args.append("--closed") |
806 args.append("--closed") |
888 args.append("--template") |
807 args.append("--template") |
889 args.append("{rev}\n") |
808 args.append("{rev}\n") |
890 |
809 |
891 output = "" |
810 output, errMsg = self.__hgClient.runcommand(args) |
892 if self.__hgClient: |
|
893 output, errMsg = self.__hgClient.runcommand(args) |
|
894 else: |
|
895 process = QProcess() |
|
896 process.setWorkingDirectory(self.repodir) |
|
897 process.start('hg', args) |
|
898 procStarted = process.waitForStarted(5000) |
|
899 if procStarted: |
|
900 finished = process.waitForFinished(30000) |
|
901 if finished and process.exitCode() == 0: |
|
902 output = str(process.readAllStandardOutput(), |
|
903 self.vcs.getEncoding(), 'replace') |
|
904 else: |
|
905 if not finished: |
|
906 errMsg = self.tr( |
|
907 "The hg process did not finish within 30s.") |
|
908 else: |
|
909 errMsg = self.tr("Could not start the hg executable.") |
|
910 |
811 |
911 if errMsg: |
812 if errMsg: |
912 E5MessageBox.critical( |
813 E5MessageBox.critical( |
913 self, |
814 self, |
914 self.tr("Mercurial Error"), |
815 self.tr("Mercurial Error"), |
931 """ |
832 """ |
932 errMsg = "" |
833 errMsg = "" |
933 |
834 |
934 args = self.vcs.initCommand("tags") |
835 args = self.vcs.initCommand("tags") |
935 |
836 |
936 output = "" |
837 output, errMsg = self.__hgClient.runcommand(args) |
937 if self.__hgClient: |
|
938 output, errMsg = self.__hgClient.runcommand(args) |
|
939 else: |
|
940 process = QProcess() |
|
941 process.setWorkingDirectory(self.repodir) |
|
942 process.start('hg', args) |
|
943 procStarted = process.waitForStarted(5000) |
|
944 if procStarted: |
|
945 finished = process.waitForFinished(30000) |
|
946 if finished and process.exitCode() == 0: |
|
947 output = str(process.readAllStandardOutput(), |
|
948 self.vcs.getEncoding(), 'replace') |
|
949 else: |
|
950 if not finished: |
|
951 errMsg = self.tr( |
|
952 "The hg process did not finish within 30s.") |
|
953 else: |
|
954 errMsg = self.tr("Could not start the hg executable.") |
|
955 |
838 |
956 if errMsg: |
839 if errMsg: |
957 E5MessageBox.critical( |
840 E5MessageBox.critical( |
958 self, |
841 self, |
959 self.tr("Mercurial Error"), |
842 self.tr("Mercurial Error"), |
1135 preargs.append(self.vcs.bundleFile) |
1018 preargs.append(self.vcs.bundleFile) |
1136 args.append(self.vcs.bundleFile) |
1019 args.append(self.vcs.bundleFile) |
1137 if not self.projectMode: |
1020 if not self.projectMode: |
1138 args.append(self.__filename) |
1021 args.append(self.__filename) |
1139 |
1022 |
1140 if self.__hgClient: |
1023 if preargs: |
1141 self.inputGroup.setEnabled(False) |
1024 out, err = self.__hgClient.runcommand(preargs) |
1142 self.inputGroup.hide() |
1025 else: |
1143 |
1026 err = "" |
1144 if preargs: |
1027 if err: |
1145 out, err = self.__hgClient.runcommand(preargs) |
1028 if ( |
1146 else: |
|
1147 err = "" |
|
1148 if err: |
|
1149 if ( |
|
1150 self.commandMode == "incoming" and |
|
1151 self.initialCommandMode == "full_log" |
|
1152 ): |
|
1153 # ignore the error |
|
1154 self.commandMode = "log" |
|
1155 else: |
|
1156 self.__showError(err) |
|
1157 elif ( |
|
1158 self.commandMode != "incoming" or |
|
1159 (self.vcs.bundleFile and |
|
1160 os.path.exists(self.vcs.bundleFile)) or |
|
1161 self.__bundle |
|
1162 ): |
|
1163 out, err = self.__hgClient.runcommand(args) |
|
1164 self.buf = out.splitlines(True) |
|
1165 if err: |
|
1166 self.__showError(err) |
|
1167 self.__processBuffer() |
|
1168 elif ( |
|
1169 self.commandMode == "incoming" and |
1029 self.commandMode == "incoming" and |
1170 self.initialCommandMode == "full_log" |
1030 self.initialCommandMode == "full_log" |
1171 ): |
1031 ): |
1172 # no incoming changesets, just switch to log mode |
1032 # ignore the error |
1173 self.commandMode = "log" |
1033 self.commandMode = "log" |
1174 self.__finish() |
1034 else: |
1175 else: |
1035 self.__showError(err) |
1176 self.process.kill() |
1036 elif ( |
1177 |
1037 self.commandMode != "incoming" or |
1178 self.process.setWorkingDirectory(self.repodir) |
1038 (self.vcs.bundleFile and |
1179 |
1039 os.path.exists(self.vcs.bundleFile)) or |
1180 if preargs: |
1040 self.__bundle |
1181 process = QProcess() |
1041 ): |
1182 process.setWorkingDirectory(self.repodir) |
1042 out, err = self.__hgClient.runcommand(args) |
1183 process.start('hg', args) |
1043 self.buf = out.splitlines(True) |
1184 procStarted = process.waitForStarted(5000) |
1044 if err: |
1185 if procStarted: |
1045 self.__showError(err) |
1186 process.waitForFinished(30000) |
1046 self.__processBuffer() |
1187 |
1047 elif ( |
1188 if ( |
1048 self.commandMode == "incoming" and |
1189 self.commandMode != "incoming" or |
1049 self.initialCommandMode == "full_log" |
1190 (self.vcs.bundleFile and |
1050 ): |
1191 os.path.exists(self.vcs.bundleFile)) or |
1051 # no incoming changesets, just switch to log mode |
1192 self.__bundle |
1052 self.commandMode = "log" |
1193 ): |
1053 self.__finish() |
1194 self.process.start('hg', args) |
|
1195 procStarted = self.process.waitForStarted(5000) |
|
1196 if not procStarted: |
|
1197 self.inputGroup.setEnabled(False) |
|
1198 self.inputGroup.hide() |
|
1199 E5MessageBox.critical( |
|
1200 self, |
|
1201 self.tr('Process Generation Error'), |
|
1202 self.tr( |
|
1203 'The process {0} could not be started. ' |
|
1204 'Ensure, that it is in the search path.' |
|
1205 ).format('hg')) |
|
1206 else: |
|
1207 self.__finish() |
|
1208 |
1054 |
1209 def start(self, fn, bundle=None, isFile=False, noEntries=0): |
1055 def start(self, fn, bundle=None, isFile=False, noEntries=0): |
1210 """ |
1056 """ |
1211 Public slot to start the hg log command. |
1057 Public slot to start the hg log command. |
1212 |
1058 |
1255 self.__identifyProject() |
1101 self.__identifyProject() |
1256 self.__getClosedBranches() |
1102 self.__getClosedBranches() |
1257 self.__getHeads() |
1103 self.__getHeads() |
1258 self.__getLogEntries(noEntries=noEntries) |
1104 self.__getLogEntries(noEntries=noEntries) |
1259 |
1105 |
1260 def __procFinished(self, exitCode, exitStatus): |
|
1261 """ |
|
1262 Private slot connected to the finished signal. |
|
1263 |
|
1264 @param exitCode exit code of the process (integer) |
|
1265 @param exitStatus exit status of the process (QProcess.ExitStatus) |
|
1266 """ |
|
1267 self.__processBuffer() |
|
1268 self.__finish() |
|
1269 |
|
1270 def __finish(self): |
1106 def __finish(self): |
1271 """ |
1107 """ |
1272 Private slot called when the process finished or the user pressed |
1108 Private slot called when the process finished or the user pressed |
1273 the button. |
1109 the button. |
1274 """ |
1110 """ |
1275 if ( |
|
1276 self.process is not None and |
|
1277 self.process.state() != QProcess.NotRunning |
|
1278 ): |
|
1279 self.process.terminate() |
|
1280 QTimer.singleShot(2000, self.process.kill) |
|
1281 self.process.waitForFinished(3000) |
|
1282 |
|
1283 QApplication.restoreOverrideCursor() |
1111 QApplication.restoreOverrideCursor() |
1284 |
1112 |
1285 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) |
1113 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) |
1286 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) |
1114 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) |
1287 self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) |
1115 self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) |
1288 |
1116 |
1289 self.inputGroup.setEnabled(False) |
|
1290 self.inputGroup.hide() |
|
1291 self.refreshButton.setEnabled(True) |
1117 self.refreshButton.setEnabled(True) |
1292 |
1118 |
1293 while self.__finishCallbacks: |
1119 while self.__finishCallbacks: |
1294 self.__finishCallbacks.pop(0)() |
1120 self.__finishCallbacks.pop(0)() |
1295 |
1121 |
1494 revision, Qt.MatchExactly, self.RevisionColumn) |
1320 revision, Qt.MatchExactly, self.RevisionColumn) |
1495 if items: |
1321 if items: |
1496 items[0].setSelected(True) |
1322 items[0].setSelected(True) |
1497 self.__selectedRevisions = [] |
1323 self.__selectedRevisions = [] |
1498 |
1324 |
1499 def __readStdout(self): |
|
1500 """ |
|
1501 Private slot to handle the readyReadStandardOutput signal. |
|
1502 |
|
1503 It reads the output of the process and inserts it into a buffer. |
|
1504 """ |
|
1505 self.process.setReadChannel(QProcess.StandardOutput) |
|
1506 |
|
1507 while self.process.canReadLine(): |
|
1508 line = str(self.process.readLine(), self.vcs.getEncoding(), |
|
1509 'replace') |
|
1510 self.buf.append(line) |
|
1511 |
|
1512 def __readStderr(self): |
|
1513 """ |
|
1514 Private slot to handle the readyReadStandardError signal. |
|
1515 |
|
1516 It reads the error output of the process and inserts it into the |
|
1517 error pane. |
|
1518 """ |
|
1519 if self.process is not None: |
|
1520 s = str(self.process.readAllStandardError(), |
|
1521 self.vcs.getEncoding(), 'replace') |
|
1522 self.__showError(s) |
|
1523 |
|
1524 def __showError(self, out): |
1325 def __showError(self, out): |
1525 """ |
1326 """ |
1526 Private slot to show some error. |
1327 Private slot to show some error. |
1527 |
1328 |
1528 @param out error to be shown (string) |
1329 @param out error to be shown (string) |
1529 """ |
1330 """ |
1530 self.errorGroup.show() |
1331 self.errorGroup.show() |
1531 self.errors.insertPlainText(out) |
1332 self.errors.insertPlainText(out) |
1532 self.errors.ensureCursorVisible() |
1333 self.errors.ensureCursorVisible() |
1533 |
|
1534 if not self.__hgClient: |
|
1535 # show input in case the process asked for some input |
|
1536 self.inputGroup.setEnabled(True) |
|
1537 self.inputGroup.show() |
|
1538 |
1334 |
1539 def on_buttonBox_clicked(self, button): |
1335 def on_buttonBox_clicked(self, button): |
1540 """ |
1336 """ |
1541 Private slot called by a button of the button box clicked. |
1337 Private slot called by a button of the button box clicked. |
1542 |
1338 |
1544 """ |
1340 """ |
1545 if button == self.buttonBox.button(QDialogButtonBox.Close): |
1341 if button == self.buttonBox.button(QDialogButtonBox.Close): |
1546 self.close() |
1342 self.close() |
1547 elif button == self.buttonBox.button(QDialogButtonBox.Cancel): |
1343 elif button == self.buttonBox.button(QDialogButtonBox.Cancel): |
1548 self.cancelled = True |
1344 self.cancelled = True |
1549 if self.__hgClient: |
1345 self.__hgClient.cancel() |
1550 self.__hgClient.cancel() |
|
1551 else: |
|
1552 self.__finish() |
|
1553 elif button == self.refreshButton: |
1346 elif button == self.refreshButton: |
1554 self.on_refreshButton_clicked() |
1347 self.on_refreshButton_clicked() |
1555 |
1348 |
1556 def __updateSbsSelectLabel(self): |
1349 def __updateSbsSelectLabel(self): |
1557 """ |
1350 """ |
2156 else: |
1949 else: |
2157 self.commandMode = self.initialCommandMode |
1950 self.commandMode = self.initialCommandMode |
2158 self.start(self.__filename, bundle=self.__bundle, isFile=self.__isFile, |
1951 self.start(self.__filename, bundle=self.__bundle, isFile=self.__isFile, |
2159 noEntries=self.logTree.topLevelItemCount()) |
1952 noEntries=self.logTree.topLevelItemCount()) |
2160 |
1953 |
2161 def on_passwordCheckBox_toggled(self, isOn): |
|
2162 """ |
|
2163 Private slot to handle the password checkbox toggled. |
|
2164 |
|
2165 @param isOn flag indicating the status of the check box (boolean) |
|
2166 """ |
|
2167 if isOn: |
|
2168 self.input.setEchoMode(QLineEdit.Password) |
|
2169 else: |
|
2170 self.input.setEchoMode(QLineEdit.Normal) |
|
2171 |
|
2172 @pyqtSlot() |
|
2173 def on_sendButton_clicked(self): |
|
2174 """ |
|
2175 Private slot to send the input to the mercurial process. |
|
2176 """ |
|
2177 inputTxt = self.input.text() |
|
2178 inputTxt += os.linesep |
|
2179 |
|
2180 if self.passwordCheckBox.isChecked(): |
|
2181 self.errors.insertPlainText(os.linesep) |
|
2182 self.errors.ensureCursorVisible() |
|
2183 else: |
|
2184 self.errors.insertPlainText(inputTxt) |
|
2185 self.errors.ensureCursorVisible() |
|
2186 |
|
2187 self.process.write(strToQByteArray(inputTxt)) |
|
2188 |
|
2189 self.passwordCheckBox.setChecked(False) |
|
2190 self.input.clear() |
|
2191 |
|
2192 def on_input_returnPressed(self): |
|
2193 """ |
|
2194 Private slot to handle the press of the return key in the input field. |
|
2195 """ |
|
2196 self.intercept = True |
|
2197 self.on_sendButton_clicked() |
|
2198 |
|
2199 def keyPressEvent(self, evt): |
|
2200 """ |
|
2201 Protected slot to handle a key press event. |
|
2202 |
|
2203 @param evt the key press event (QKeyEvent) |
|
2204 """ |
|
2205 if self.intercept: |
|
2206 self.intercept = False |
|
2207 evt.accept() |
|
2208 return |
|
2209 super(HgLogBrowserDialog, self).keyPressEvent(evt) |
|
2210 |
|
2211 @pyqtSlot() |
1954 @pyqtSlot() |
2212 def __phaseActTriggered(self): |
1955 def __phaseActTriggered(self): |
2213 """ |
1956 """ |
2214 Private slot to handle the Change Phase action. |
1957 Private slot to handle the Change Phase action. |
2215 """ |
1958 """ |