src/eric7/Plugins/VcsPlugins/vcsSubversion/SvnDiffDialog.py

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

eric ide

mercurial