Plugins/VcsPlugins/vcsMercurial/HgDiffDialog.py

changeset 4130
c5af9f10a061
parent 4111
82e21ab01491
child 4152
d90464d2ccaa
equal deleted inserted replaced
4128:6e1ee14d5f23 4130:c5af9f10a061
11 try: 11 try:
12 str = unicode 12 str = unicode
13 except NameError: 13 except NameError:
14 pass 14 pass
15 15
16 import os 16 from PyQt5.QtCore import pyqtSlot, QFileInfo, Qt
17
18 from PyQt5.QtCore import pyqtSlot, QProcess, QTimer, QFileInfo, Qt
19 from PyQt5.QtGui import QTextCursor, QCursor 17 from PyQt5.QtGui import QTextCursor, QCursor
20 from PyQt5.QtWidgets import QWidget, QDialogButtonBox, QLineEdit, QApplication 18 from PyQt5.QtWidgets import QWidget, QDialogButtonBox, QApplication
21 19
22 from E5Gui import E5MessageBox, E5FileDialog 20 from E5Gui import E5MessageBox, E5FileDialog
23 from E5Gui.E5Application import e5App 21 from E5Gui.E5Application import e5App
24 22
25 from .Ui_HgDiffDialog import Ui_HgDiffDialog 23 from .Ui_HgDiffDialog import Ui_HgDiffDialog
26 from .HgDiffHighlighter import HgDiffHighlighter 24 from .HgDiffHighlighter import HgDiffHighlighter
25 from .HgDiffGenerator import HgDiffGenerator
27 26
28 import Utilities 27 import Utilities
29 import Preferences 28 import Preferences
30 29
31 30
52 self.buttonBox.button(QDialogButtonBox.Save).setEnabled(False) 51 self.buttonBox.button(QDialogButtonBox.Save).setEnabled(False)
53 self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) 52 self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
54 53
55 self.searchWidget.attachTextEdit(self.contents) 54 self.searchWidget.attachTextEdit(self.contents)
56 55
57 self.process = QProcess()
58 self.vcs = vcs
59 self.__hgClient = self.vcs.getClient()
60
61 font = Preferences.getEditorOtherFonts("MonospacedFont") 56 font = Preferences.getEditorOtherFonts("MonospacedFont")
62 self.contents.setFontFamily(font.family()) 57 self.contents.setFontFamily(font.family())
63 self.contents.setFontPointSize(font.pointSize()) 58 self.contents.setFontPointSize(font.pointSize())
64 59
65 self.highlighter = HgDiffHighlighter(self.contents.document()) 60 self.highlighter = HgDiffHighlighter(self.contents.document())
66 61
67 self.process.finished.connect(self.__procFinished) 62 self.__diffGenerator = HgDiffGenerator(vcs, self)
68 self.process.readyReadStandardOutput.connect(self.__readStdout) 63 self.__diffGenerator.finished.connect(self.__generatorFinished)
69 self.process.readyReadStandardError.connect(self.__readStderr)
70 64
71 def closeEvent(self, e): 65 def closeEvent(self, e):
72 """ 66 """
73 Protected slot implementing a close event handler. 67 Protected slot implementing a close event handler.
74 68
75 @param e close event (QCloseEvent) 69 @param e close event (QCloseEvent)
76 """ 70 """
77 if self.__hgClient: 71 self.__diffGenerator.stopProcess()
78 if self.__hgClient.isExecuting():
79 self.__hgClient.cancel()
80 else:
81 if self.process is not None and \
82 self.process.state() != QProcess.NotRunning:
83 self.process.terminate()
84 QTimer.singleShot(2000, self.process.kill)
85 self.process.waitForFinished(3000)
86
87 e.accept() 72 e.accept()
88
89 def __getVersionArg(self, version):
90 """
91 Private method to get a hg revision argument for the given revision.
92
93 @param version revision (integer or string)
94 @return version argument (string)
95 """
96 if version == "WORKING":
97 return None
98 else:
99 return str(version)
100 73
101 def start(self, fn, versions=None, bundle=None, qdiff=False, 74 def start(self, fn, versions=None, bundle=None, qdiff=False,
102 refreshable=False): 75 refreshable=False):
103 """ 76 """
104 Public slot to start the hg diff command. 77 Public slot to start the hg diff command.
111 @keyparam refreshable flag indicating a refreshable diff (boolean) 84 @keyparam refreshable flag indicating a refreshable diff (boolean)
112 """ 85 """
113 self.refreshButton.setVisible(refreshable) 86 self.refreshButton.setVisible(refreshable)
114 87
115 self.errorGroup.hide() 88 self.errorGroup.hide()
116 self.inputGroup.show()
117 self.inputGroup.setEnabled(True)
118 self.intercept = False
119 self.filename = fn 89 self.filename = fn
120 90
121 self.contents.clear() 91 self.contents.clear()
122 self.paras = 0
123
124 self.filesCombo.clear() 92 self.filesCombo.clear()
125 93
126 if qdiff: 94 if qdiff:
127 args = self.vcs.initCommand("qdiff")
128 self.setWindowTitle(self.tr("Patch Contents")) 95 self.setWindowTitle(self.tr("Patch Contents"))
96
97 self.raise_()
98 self.activateWindow()
99
100 QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
101 procStarted = self.__diffGenerator.start(
102 fn, versions=versions, bundle=bundle, qdiff=qdiff)
103 if not procStarted:
104 E5MessageBox.critical(
105 self,
106 self.tr('Process Generation Error'),
107 self.tr(
108 'The process {0} could not be started. '
109 'Ensure, that it is in the search path.'
110 ).format('hg'))
111
112 def __generatorFinished(self):
113 """
114 Private slot connected to the finished signal.
115 """
116 QApplication.restoreOverrideCursor()
117 self.refreshButton.setEnabled(True)
118
119 diff, errors, fileSeparators = self.__diffGenerator.getResult()
120
121 if diff:
122 self.contents.setPlainText("".join(diff))
129 else: 123 else:
130 args = self.vcs.initCommand("diff") 124 self.contents.setPlainText(
131 125 self.tr('There is no difference.'))
132 if self.vcs.hasSubrepositories(): 126
133 args.append("--subrepos") 127 if errors:
134 128 self.errorGroup.show()
135 if bundle: 129 self.errors.setPlainText("".join(errors))
136 args.append('--repository') 130 self.errors.ensureCursorVisible()
137 args.append(bundle) 131
138 elif self.vcs.bundleFile and os.path.exists(self.vcs.bundleFile): 132 self.buttonBox.button(QDialogButtonBox.Save).setEnabled(bool(diff))
139 args.append('--repository')
140 args.append(self.vcs.bundleFile)
141
142 if versions is not None:
143 self.raise_()
144 self.activateWindow()
145
146 rev1 = self.__getVersionArg(versions[0])
147 rev2 = None
148 if len(versions) == 2:
149 rev2 = self.__getVersionArg(versions[1])
150
151 if rev1 is not None or rev2 is not None:
152 args.append('-r')
153 if rev1 is not None and rev2 is not None:
154 args.append('{0}:{1}'.format(rev1, rev2))
155 elif rev2 is None:
156 args.append(rev1)
157 elif rev1 is None:
158 args.append(':{0}'.format(rev2))
159
160 if isinstance(fn, list):
161 dname, fnames = self.vcs.splitPathList(fn)
162 self.vcs.addArguments(args, fn)
163 else:
164 dname, fname = self.vcs.splitPath(fn)
165 args.append(fn)
166
167 self.__oldFile = ""
168 self.__oldFileLine = -1
169 self.__fileSeparators = []
170
171 QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
172 if self.__hgClient:
173 self.inputGroup.setEnabled(False)
174 self.inputGroup.hide()
175
176 out, err = self.__hgClient.runcommand(args)
177
178 if err:
179 self.__showError(err)
180 if out:
181 for line in out.splitlines(True):
182 self.__processOutputLine(line)
183 if self.__hgClient.wasCanceled():
184 break
185
186 self.__finish()
187 else:
188 # find the root of the repo
189 repodir = dname
190 while not os.path.isdir(os.path.join(repodir, self.vcs.adminDir)):
191 repodir = os.path.dirname(repodir)
192 if os.path.splitdrive(repodir)[1] == os.sep:
193 return
194
195 self.process.kill()
196
197 self.process.setWorkingDirectory(repodir)
198
199 self.process.start('hg', args)
200 procStarted = self.process.waitForStarted(5000)
201 if not procStarted:
202 QApplication.restoreOverrideCursor()
203 self.inputGroup.setEnabled(False)
204 self.inputGroup.hide()
205 E5MessageBox.critical(
206 self,
207 self.tr('Process Generation Error'),
208 self.tr(
209 'The process {0} could not be started. '
210 'Ensure, that it is in the search path.'
211 ).format('hg'))
212
213 def __procFinished(self, exitCode, exitStatus):
214 """
215 Private slot connected to the finished signal.
216
217 @param exitCode exit code of the process (integer)
218 @param exitStatus exit status of the process (QProcess.ExitStatus)
219 """
220 self.__finish()
221
222 def __finish(self):
223 """
224 Private slot called when the process finished or the user pressed
225 the button.
226 """
227 QApplication.restoreOverrideCursor()
228 self.inputGroup.setEnabled(False)
229 self.inputGroup.hide()
230 self.refreshButton.setEnabled(True)
231
232 if self.paras == 0:
233 self.contents.setPlainText(self.tr('There is no difference.'))
234
235 self.buttonBox.button(QDialogButtonBox.Save).setEnabled(self.paras > 0)
236 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) 133 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
237 self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) 134 self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
238 self.buttonBox.button(QDialogButtonBox.Close).setFocus( 135 self.buttonBox.button(QDialogButtonBox.Close).setFocus(
239 Qt.OtherFocusReason) 136 Qt.OtherFocusReason)
240 137
243 self.contents.setTextCursor(tc) 140 self.contents.setTextCursor(tc)
244 self.contents.ensureCursorVisible() 141 self.contents.ensureCursorVisible()
245 142
246 self.filesCombo.addItem(self.tr("<Start>"), 0) 143 self.filesCombo.addItem(self.tr("<Start>"), 0)
247 self.filesCombo.addItem(self.tr("<End>"), -1) 144 self.filesCombo.addItem(self.tr("<End>"), -1)
248 for oldFile, newFile, pos in sorted(self.__fileSeparators): 145 for oldFile, newFile, pos in sorted(fileSeparators):
249 if not oldFile: 146 if not oldFile:
250 self.filesCombo.addItem(newFile, pos) 147 self.filesCombo.addItem(newFile, pos)
251 elif oldFile != newFile: 148 elif oldFile != newFile:
252 self.filesCombo.addItem( 149 self.filesCombo.addItem(
253 "{0}\n{1}".format(oldFile, newFile), pos) 150 "{0}\n{1}".format(oldFile, newFile), pos)
254 else: 151 else:
255 self.filesCombo.addItem(oldFile, pos) 152 self.filesCombo.addItem(oldFile, pos)
256
257 def __appendText(self, txt):
258 """
259 Private method to append text to the end of the contents pane.
260
261 @param txt text to insert (string)
262 """
263 tc = self.contents.textCursor()
264 tc.movePosition(QTextCursor.End)
265 self.contents.setTextCursor(tc)
266 self.contents.insertPlainText(txt)
267
268 def __extractFileName(self, line):
269 """
270 Private method to extract the file name out of a file separator line.
271
272 @param line line to be processed (string)
273 @return extracted file name (string)
274 """
275 f = line.split(None, 1)[1]
276 f = f.rsplit(None, 6)[0]
277 if f == "/dev/null":
278 f = "__NULL__"
279 else:
280 f = f.split("/", 1)[1]
281 return f
282
283 def __processFileLine(self, line):
284 """
285 Private slot to process a line giving the old/new file.
286
287 @param line line to be processed (string)
288 """
289 if line.startswith('---'):
290 self.__oldFileLine = self.paras
291 self.__oldFile = self.__extractFileName(line)
292 else:
293 newFile = self.__extractFileName(line)
294 if self.__oldFile == "__NULL__":
295 self.__fileSeparators.append(
296 (newFile, newFile, self.__oldFileLine))
297 else:
298 self.__fileSeparators.append(
299 (self.__oldFile, newFile, self.__oldFileLine))
300
301 def __processOutputLine(self, line):
302 """
303 Private method to process the lines of output.
304
305 @param line output line to be processed (string)
306 """
307 if line.startswith("--- ") or \
308 line.startswith("+++ "):
309 self.__processFileLine(line)
310
311 self.__appendText(line)
312 self.paras += 1
313
314 def __readStdout(self):
315 """
316 Private slot to handle the readyReadStandardOutput signal.
317
318 It reads the output of the process, formats it and inserts it into
319 the contents pane.
320 """
321 self.process.setReadChannel(QProcess.StandardOutput)
322
323 while self.process.canReadLine():
324 line = str(self.process.readLine(), self.vcs.getEncoding(),
325 'replace')
326 self.__processOutputLine(line)
327
328 def __readStderr(self):
329 """
330 Private slot to handle the readyReadStandardError signal.
331
332 It reads the error output of the process and inserts it into the
333 error pane.
334 """
335 if self.process is not None:
336 s = str(self.process.readAllStandardError(),
337 self.vcs.getEncoding(), 'replace')
338 self.__showError(s)
339
340 def __showError(self, out):
341 """
342 Private slot to show some error.
343
344 @param out error to be shown (string)
345 """
346 self.errorGroup.show()
347 self.errors.insertPlainText(out)
348 self.errors.ensureCursorVisible()
349 153
350 def on_buttonBox_clicked(self, button): 154 def on_buttonBox_clicked(self, button):
351 """ 155 """
352 Private slot called by a button of the button box clicked. 156 Private slot called by a button of the button box clicked.
353 157
461 265
462 self.buttonBox.button(QDialogButtonBox.Save).setEnabled(False) 266 self.buttonBox.button(QDialogButtonBox.Save).setEnabled(False)
463 self.refreshButton.setEnabled(False) 267 self.refreshButton.setEnabled(False)
464 268
465 self.start(self.filename, refreshable=True) 269 self.start(self.filename, refreshable=True)
466
467 def on_passwordCheckBox_toggled(self, isOn):
468 """
469 Private slot to handle the password checkbox toggled.
470
471 @param isOn flag indicating the status of the check box (boolean)
472 """
473 if isOn:
474 self.input.setEchoMode(QLineEdit.Password)
475 else:
476 self.input.setEchoMode(QLineEdit.Normal)
477
478 @pyqtSlot()
479 def on_sendButton_clicked(self):
480 """
481 Private slot to send the input to the subversion process.
482 """
483 input = self.input.text()
484 input += os.linesep
485
486 if self.passwordCheckBox.isChecked():
487 self.errors.insertPlainText(os.linesep)
488 self.errors.ensureCursorVisible()
489 else:
490 self.errors.insertPlainText(input)
491 self.errors.ensureCursorVisible()
492
493 self.process.write(input)
494
495 self.passwordCheckBox.setChecked(False)
496 self.input.clear()
497
498 def on_input_returnPressed(self):
499 """
500 Private slot to handle the press of the return key in the input field.
501 """
502 self.intercept = True
503 self.on_sendButton_clicked()
504
505 def keyPressEvent(self, evt):
506 """
507 Protected slot to handle a key press event.
508
509 @param evt the key press event (QKeyEvent)
510 """
511 if self.intercept:
512 self.intercept = False
513 evt.accept()
514 return
515 super(HgDiffDialog, self).keyPressEvent(evt)

eric ide

mercurial