eric6/Plugins/VcsPlugins/vcsSubversion/SvnDiffDialog.py

changeset 6942
2602857055c5
parent 6735
31e263d49c04
child 7192
a22eee00b052
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2003 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a dialog to show the output of the svn diff command
8 process.
9 """
10
11 from __future__ import unicode_literals
12 try:
13 str = unicode
14 except NameError:
15 pass
16
17 import os
18
19 from PyQt5.QtCore import QTimer, QFileInfo, QProcess, pyqtSlot, Qt
20 from PyQt5.QtGui import QTextCursor
21 from PyQt5.QtWidgets import QWidget, QLineEdit, QDialogButtonBox
22
23 from E5Gui.E5Application import e5App
24 from E5Gui import E5MessageBox, E5FileDialog
25
26 from .Ui_SvnDiffDialog import Ui_SvnDiffDialog
27 from .SvnDiffHighlighter import SvnDiffHighlighter
28
29 import Utilities
30 import Preferences
31 from Globals import strToQByteArray
32
33
34 class SvnDiffDialog(QWidget, Ui_SvnDiffDialog):
35 """
36 Class implementing a dialog to show the output of the svn diff command
37 process.
38 """
39 def __init__(self, vcs, parent=None):
40 """
41 Constructor
42
43 @param vcs reference to the vcs object
44 @param parent parent widget (QWidget)
45 """
46 super(SvnDiffDialog, self).__init__(parent)
47 self.setupUi(self)
48
49 self.refreshButton = self.buttonBox.addButton(
50 self.tr("Refresh"), QDialogButtonBox.ActionRole)
51 self.refreshButton.setToolTip(
52 self.tr("Press to refresh the display"))
53 self.refreshButton.setEnabled(False)
54 self.buttonBox.button(QDialogButtonBox.Save).setEnabled(False)
55 self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
56
57 self.searchWidget.attachTextEdit(self.contents)
58
59 self.process = QProcess()
60 self.vcs = vcs
61
62 font = Preferences.getEditorOtherFonts("MonospacedFont")
63 self.contents.setFontFamily(font.family())
64 self.contents.setFontPointSize(font.pointSize())
65
66 self.highlighter = SvnDiffHighlighter(self.contents.document())
67
68 self.process.finished.connect(self.__procFinished)
69 self.process.readyReadStandardOutput.connect(self.__readStdout)
70 self.process.readyReadStandardError.connect(self.__readStderr)
71
72 def closeEvent(self, e):
73 """
74 Protected slot implementing a close event handler.
75
76 @param e close event (QCloseEvent)
77 """
78 if self.process is not None and \
79 self.process.state() != QProcess.NotRunning:
80 self.process.terminate()
81 QTimer.singleShot(2000, self.process.kill)
82 self.process.waitForFinished(3000)
83
84 e.accept()
85
86 def __getVersionArg(self, version):
87 """
88 Private method to get a svn revision argument for the given revision.
89
90 @param version revision (integer or string)
91 @return version argument (string)
92 """
93 if version == "WORKING":
94 return None
95 else:
96 return str(version)
97
98 def start(self, fn, versions=None, urls=None, summary=False,
99 refreshable=False):
100 """
101 Public slot to start the svn diff command.
102
103 @param fn filename to be diffed (string)
104 @param versions list of versions to be diffed (list of up to 2 strings
105 or None)
106 @keyparam urls list of repository URLs (list of 2 strings)
107 @keyparam summary flag indicating a summarizing diff
108 (only valid for URL diffs) (boolean)
109 @keyparam refreshable flag indicating a refreshable diff (boolean)
110 """
111 self.refreshButton.setVisible(refreshable)
112
113 self.errorGroup.hide()
114 self.inputGroup.show()
115 self.inputGroup.setEnabled(True)
116 self.intercept = False
117 self.filename = fn
118
119 self.process.kill()
120
121 self.contents.clear()
122 self.highlighter.regenerateRules()
123 self.paras = 0
124
125 self.filesCombo.clear()
126
127 self.__oldFile = ""
128 self.__oldFileLine = -1
129 self.__fileSeparators = []
130
131 args = []
132 args.append('diff')
133 self.vcs.addArguments(args, self.vcs.options['global'])
134 self.vcs.addArguments(args, self.vcs.options['diff'])
135 if '--diff-cmd' in self.vcs.options['diff']:
136 self.buttonBox.button(QDialogButtonBox.Save).hide()
137
138 if versions is not None:
139 self.raise_()
140 self.activateWindow()
141
142 rev1 = self.__getVersionArg(versions[0])
143 rev2 = None
144 if len(versions) == 2:
145 rev2 = self.__getVersionArg(versions[1])
146
147 if rev1 is not None or rev2 is not None:
148 args.append('-r')
149 if rev1 is not None and rev2 is not None:
150 args.append('{0}:{1}'.format(rev1, rev2))
151 elif rev2 is None:
152 args.append(rev1)
153 elif rev1 is None:
154 args.append(rev2)
155
156 self.summaryPath = None
157 if urls is not None:
158 if summary:
159 args.append("--summarize")
160 self.summaryPath = urls[0]
161 args.append("--old={0}".format(urls[0]))
162 args.append("--new={0}".format(urls[1]))
163 if isinstance(fn, list):
164 dname, fnames = self.vcs.splitPathList(fn)
165 else:
166 dname, fname = self.vcs.splitPath(fn)
167 fnames = [fname]
168 project = e5App().getObject('Project')
169 if dname == project.getProjectPath():
170 path = ""
171 else:
172 path = project.getRelativePath(dname)
173 if path:
174 path += "/"
175 for fname in fnames:
176 args.append(path + fname)
177 else:
178 if isinstance(fn, list):
179 dname, fnames = self.vcs.splitPathList(fn)
180 self.vcs.addArguments(args, fnames)
181 else:
182 dname, fname = self.vcs.splitPath(fn)
183 args.append(fname)
184
185 self.process.setWorkingDirectory(dname)
186
187 self.process.start('svn', args)
188 procStarted = self.process.waitForStarted(5000)
189 if not procStarted:
190 self.inputGroup.setEnabled(False)
191 self.inputGroup.hide()
192 E5MessageBox.critical(
193 self,
194 self.tr('Process Generation Error'),
195 self.tr(
196 'The process {0} could not be started. '
197 'Ensure, that it is in the search path.'
198 ).format('svn'))
199
200 def __procFinished(self, exitCode, exitStatus):
201 """
202 Private slot connected to the finished signal.
203
204 @param exitCode exit code of the process (integer)
205 @param exitStatus exit status of the process (QProcess.ExitStatus)
206 """
207 self.inputGroup.setEnabled(False)
208 self.inputGroup.hide()
209 self.refreshButton.setEnabled(True)
210
211 if self.paras == 0:
212 self.contents.setPlainText(self.tr('There is no difference.'))
213
214 self.buttonBox.button(QDialogButtonBox.Save).setEnabled(self.paras > 0)
215 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
216 self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
217 self.buttonBox.button(QDialogButtonBox.Close).setFocus(
218 Qt.OtherFocusReason)
219
220 tc = self.contents.textCursor()
221 tc.movePosition(QTextCursor.Start)
222 self.contents.setTextCursor(tc)
223 self.contents.ensureCursorVisible()
224
225 self.filesCombo.addItem(self.tr("<Start>"), 0)
226 self.filesCombo.addItem(self.tr("<End>"), -1)
227 for oldFile, newFile, pos in sorted(self.__fileSeparators):
228 if oldFile != newFile:
229 self.filesCombo.addItem(
230 "{0}\n{1}".format(oldFile, newFile), pos)
231 else:
232 self.filesCombo.addItem(oldFile, pos)
233
234 def __appendText(self, txt):
235 """
236 Private method to append text to the end of the contents pane.
237
238 @param txt text to insert (string)
239 """
240 tc = self.contents.textCursor()
241 tc.movePosition(QTextCursor.End)
242 self.contents.setTextCursor(tc)
243 self.contents.insertPlainText(txt)
244
245 def __extractFileName(self, line):
246 """
247 Private method to extract the file name out of a file separator line.
248
249 @param line line to be processed (string)
250 @return extracted file name (string)
251 """
252 f = line.split(None, 1)[1]
253 f = f.rsplit(None, 2)[0]
254 return f
255
256 def __processFileLine(self, line):
257 """
258 Private slot to process a line giving the old/new file.
259
260 @param line line to be processed (string)
261 """
262 if line.startswith('---'):
263 self.__oldFileLine = self.paras
264 self.__oldFile = self.__extractFileName(line)
265 else:
266 self.__fileSeparators.append(
267 (self.__oldFile, self.__extractFileName(line),
268 self.__oldFileLine))
269
270 def __readStdout(self):
271 """
272 Private slot to handle the readyReadStandardOutput signal.
273
274 It reads the output of the process, formats it and inserts it into
275 the contents pane.
276 """
277 self.process.setReadChannel(QProcess.StandardOutput)
278
279 while self.process.canReadLine():
280 line = str(self.process.readLine(),
281 Preferences.getSystem("IOEncoding"),
282 'replace')
283 if self.summaryPath:
284 line = line.replace(self.summaryPath + '/', '')
285 line = " ".join(line.split())
286 if line.startswith("--- ") or line.startswith("+++ "):
287 self.__processFileLine(line)
288
289 self.__appendText(line)
290 self.paras += 1
291
292 def __readStderr(self):
293 """
294 Private slot to handle the readyReadStandardError signal.
295
296 It reads the error output of the process and inserts it into the
297 error pane.
298 """
299 if self.process is not None:
300 self.errorGroup.show()
301 s = str(self.process.readAllStandardError(),
302 Preferences.getSystem("IOEncoding"),
303 'replace')
304 self.errors.insertPlainText(s)
305 self.errors.ensureCursorVisible()
306
307 def on_buttonBox_clicked(self, button):
308 """
309 Private slot called by a button of the button box clicked.
310
311 @param button button that was clicked (QAbstractButton)
312 """
313 if button == self.buttonBox.button(QDialogButtonBox.Save):
314 self.on_saveButton_clicked()
315 elif button == self.refreshButton:
316 self.on_refreshButton_clicked()
317
318 @pyqtSlot(int)
319 def on_filesCombo_activated(self, index):
320 """
321 Private slot to handle the selection of a file.
322
323 @param index activated row (integer)
324 """
325 para = self.filesCombo.itemData(index)
326
327 if para == 0:
328 tc = self.contents.textCursor()
329 tc.movePosition(QTextCursor.Start)
330 self.contents.setTextCursor(tc)
331 self.contents.ensureCursorVisible()
332 elif para == -1:
333 tc = self.contents.textCursor()
334 tc.movePosition(QTextCursor.End)
335 self.contents.setTextCursor(tc)
336 self.contents.ensureCursorVisible()
337 else:
338 # step 1: move cursor to end
339 tc = self.contents.textCursor()
340 tc.movePosition(QTextCursor.End)
341 self.contents.setTextCursor(tc)
342 self.contents.ensureCursorVisible()
343
344 # step 2: move cursor to desired line
345 tc = self.contents.textCursor()
346 delta = tc.blockNumber() - para
347 tc.movePosition(QTextCursor.PreviousBlock, QTextCursor.MoveAnchor,
348 delta)
349 self.contents.setTextCursor(tc)
350 self.contents.ensureCursorVisible()
351
352 @pyqtSlot()
353 def on_saveButton_clicked(self):
354 """
355 Private slot to handle the Save button press.
356
357 It saves the diff shown in the dialog to a file in the local
358 filesystem.
359 """
360 if isinstance(self.filename, list):
361 if len(self.filename) > 1:
362 fname = self.vcs.splitPathList(self.filename)[0]
363 else:
364 dname, fname = self.vcs.splitPath(self.filename[0])
365 if fname != '.':
366 fname = "{0}.diff".format(self.filename[0])
367 else:
368 fname = dname
369 else:
370 fname = self.vcs.splitPath(self.filename)[0]
371
372 fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
373 self,
374 self.tr("Save Diff"),
375 fname,
376 self.tr("Patch Files (*.diff)"),
377 None,
378 E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
379
380 if not fname:
381 return # user aborted
382
383 ext = QFileInfo(fname).suffix()
384 if not ext:
385 ex = selectedFilter.split("(*")[1].split(")")[0]
386 if ex:
387 fname += ex
388 if QFileInfo(fname).exists():
389 res = E5MessageBox.yesNo(
390 self,
391 self.tr("Save Diff"),
392 self.tr("<p>The patch file <b>{0}</b> already exists."
393 " Overwrite it?</p>").format(fname),
394 icon=E5MessageBox.Warning)
395 if not res:
396 return
397 fname = Utilities.toNativeSeparators(fname)
398
399 eol = e5App().getObject("Project").getEolString()
400 try:
401 f = open(fname, "w", encoding="utf-8", newline="")
402 f.write(eol.join(self.contents.toPlainText().splitlines()))
403 f.close()
404 except IOError as why:
405 E5MessageBox.critical(
406 self, self.tr('Save Diff'),
407 self.tr(
408 '<p>The patch file <b>{0}</b> could not be saved.'
409 '<br>Reason: {1}</p>')
410 .format(fname, str(why)))
411
412 @pyqtSlot()
413 def on_refreshButton_clicked(self):
414 """
415 Private slot to refresh the display.
416 """
417 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
418
419 self.buttonBox.button(QDialogButtonBox.Save).setEnabled(False)
420 self.refreshButton.setEnabled(False)
421
422 self.start(self.filename, refreshable=True)
423
424 def on_passwordCheckBox_toggled(self, isOn):
425 """
426 Private slot to handle the password checkbox toggled.
427
428 @param isOn flag indicating the status of the check box (boolean)
429 """
430 if isOn:
431 self.input.setEchoMode(QLineEdit.Password)
432 else:
433 self.input.setEchoMode(QLineEdit.Normal)
434
435 @pyqtSlot()
436 def on_sendButton_clicked(self):
437 """
438 Private slot to send the input to the subversion process.
439 """
440 inputTxt = self.input.text()
441 inputTxt += os.linesep
442
443 if self.passwordCheckBox.isChecked():
444 self.errors.insertPlainText(os.linesep)
445 self.errors.ensureCursorVisible()
446 else:
447 self.errors.insertPlainText(inputTxt)
448 self.errors.ensureCursorVisible()
449
450 self.process.write(strToQByteArray(inputTxt))
451
452 self.passwordCheckBox.setChecked(False)
453 self.input.clear()
454
455 def on_input_returnPressed(self):
456 """
457 Private slot to handle the press of the return key in the input field.
458 """
459 self.intercept = True
460 self.on_sendButton_clicked()
461
462 def keyPressEvent(self, evt):
463 """
464 Protected slot to handle a key press event.
465
466 @param evt the key press event (QKeyEvent)
467 """
468 if self.intercept:
469 self.intercept = False
470 evt.accept()
471 return
472 super(SvnDiffDialog, self).keyPressEvent(evt)

eric ide

mercurial