src/eric7/Plugins/VcsPlugins/vcsPySvn/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 import pysvn
15
16 from PyQt6.QtCore import QDateTime, Qt, pyqtSlot
17 from PyQt6.QtGui import QTextCursor
18 from PyQt6.QtWidgets import QWidget, QDialogButtonBox
19
20 from EricWidgets.EricApplication import ericApp
21 from EricWidgets import EricMessageBox, EricFileDialog
22 from EricGui.EricOverrideCursor import EricOverrideCursor
23
24 from EricUtilities.EricMutexLocker import EricMutexLocker
25
26 from .SvnDialogMixin import SvnDialogMixin
27 from .Ui_SvnDiffDialog import Ui_SvnDiffDialog
28 from .SvnDiffHighlighter import SvnDiffHighlighter
29
30 import Utilities
31 import Preferences
32
33
34 class SvnDiffDialog(QWidget, SvnDialogMixin, Ui_SvnDiffDialog):
35 """
36 Class implementing a dialog to show the output of the svn diff command.
37 """
38 def __init__(self, vcs, parent=None):
39 """
40 Constructor
41
42 @param vcs reference to the vcs object
43 @param parent parent widget (QWidget)
44 """
45 super().__init__(parent)
46 self.setupUi(self)
47 SvnDialogMixin.__init__(self)
48
49 self.refreshButton = self.buttonBox.addButton(
50 self.tr("Refresh"), QDialogButtonBox.ButtonRole.ActionRole)
51 self.refreshButton.setToolTip(
52 self.tr("Press to refresh the display"))
53 self.refreshButton.setEnabled(False)
54 self.buttonBox.button(
55 QDialogButtonBox.StandardButton.Save).setEnabled(False)
56 self.buttonBox.button(
57 QDialogButtonBox.StandardButton.Close).setEnabled(False)
58 self.buttonBox.button(
59 QDialogButtonBox.StandardButton.Cancel).setDefault(True)
60
61 self.searchWidget.attachTextEdit(self.contents)
62
63 self.vcs = vcs
64
65 font = Preferences.getEditorOtherFonts("MonospacedFont")
66 self.contents.document().setDefaultFont(font)
67
68 self.highlighter = SvnDiffHighlighter(self.contents.document())
69
70 self.client = self.vcs.getClient()
71 self.client.callback_cancel = self._clientCancelCallback
72 self.client.callback_get_login = self._clientLoginCallback
73 self.client.callback_ssl_server_trust_prompt = (
74 self._clientSslServerTrustPromptCallback
75 )
76
77 def __getVersionArg(self, version):
78 """
79 Private method to get a pysvn revision object for the given version
80 number.
81
82 @param version revision (integer or string)
83 @return revision object (pysvn.Revision)
84 """
85 if isinstance(version, int):
86 return pysvn.Revision(pysvn.opt_revision_kind.number, version)
87 elif version.startswith("{"):
88 dateStr = version[1:-1]
89 secs = QDateTime.fromString(
90 dateStr, Qt.DateFormat.ISODate).toTime_t()
91 return pysvn.Revision(pysvn.opt_revision_kind.date, secs)
92 else:
93 return {
94 "HEAD": pysvn.Revision(pysvn.opt_revision_kind.head),
95 "COMMITTED": pysvn.Revision(pysvn.opt_revision_kind.committed),
96 "BASE": pysvn.Revision(pysvn.opt_revision_kind.base),
97 "WORKING": pysvn.Revision(pysvn.opt_revision_kind.working),
98 "PREV": pysvn.Revision(pysvn.opt_revision_kind.previous),
99 }.get(version, pysvn.Revision(pysvn.opt_revision_kind.unspecified))
100
101 def __getDiffSummaryKind(self, summaryKind):
102 """
103 Private method to get a string descripion of the diff summary.
104
105 @param summaryKind (pysvn.diff_summarize.summarize_kind)
106 @return one letter string indicating the change type (string)
107 """
108 if summaryKind == pysvn.diff_summarize_kind.delete:
109 return "D"
110 elif summaryKind == pysvn.diff_summarize_kind.modified:
111 return "M"
112 elif summaryKind == pysvn.diff_summarize_kind.added:
113 return "A"
114 elif summaryKind == pysvn.diff_summarize_kind.normal:
115 return "N"
116 else:
117 return " "
118
119 def start(self, fn, versions=None, urls=None, summary=False, pegRev=None,
120 refreshable=False):
121 """
122 Public slot to start the svn diff command.
123
124 @param fn filename to be diffed (string)
125 @param versions list of versions to be diffed (list of up to 2 integer
126 or None)
127 @param urls list of repository URLs (list of 2 strings)
128 @param summary flag indicating a summarizing diff
129 (only valid for URL diffs) (boolean)
130 @param pegRev revision number the filename is valid (integer)
131 @param refreshable flag indicating a refreshable diff (boolean)
132 """
133 self.refreshButton.setVisible(refreshable)
134
135 self.buttonBox.button(
136 QDialogButtonBox.StandardButton.Save).setEnabled(False)
137 self.buttonBox.button(
138 QDialogButtonBox.StandardButton.Close).setEnabled(False)
139 self.buttonBox.button(
140 QDialogButtonBox.StandardButton.Cancel).setEnabled(True)
141 self.buttonBox.button(
142 QDialogButtonBox.StandardButton.Cancel).setDefault(True)
143
144 self._reset()
145 self.errorGroup.hide()
146
147 self.filename = fn
148
149 self.contents.clear()
150 self.highlighter.regenerateRules()
151 self.paras = 0
152
153 self.filesCombo.clear()
154
155 self.__oldFile = ""
156 self.__oldFileLine = -1
157 self.__fileSeparators = []
158
159 if Utilities.hasEnvironmentEntry('TEMP'):
160 tmpdir = Utilities.getEnvironmentEntry('TEMP')
161 elif Utilities.hasEnvironmentEntry('TMPDIR'):
162 tmpdir = Utilities.getEnvironmentEntry('TMPDIR')
163 elif Utilities.hasEnvironmentEntry('TMP'):
164 tmpdir = Utilities.getEnvironmentEntry('TMP')
165 elif os.path.exists('/var/tmp'): # secok
166 tmpdir = '/var/tmp' # secok
167 elif os.path.exists('/usr/tmp'):
168 tmpdir = '/usr/tmp'
169 elif os.path.exists('/tmp'): # secok
170 tmpdir = '/tmp' # secok
171 else:
172 EricMessageBox.critical(
173 self,
174 self.tr("Subversion Diff"),
175 self.tr("""There is no temporary directory available."""))
176 return
177
178 tmpdir = os.path.join(tmpdir, 'svn_tmp')
179 if not os.path.exists(tmpdir):
180 os.mkdir(tmpdir)
181
182 opts = self.vcs.options['global'] + self.vcs.options['diff']
183 recurse = "--non-recursive" not in opts
184
185 if versions is not None:
186 self.raise_()
187 self.activateWindow()
188 rev1 = self.__getVersionArg(versions[0])
189 if len(versions) == 1:
190 rev2 = self.__getVersionArg("WORKING")
191 else:
192 rev2 = self.__getVersionArg(versions[1])
193 else:
194 rev1 = self.__getVersionArg("BASE")
195 rev2 = self.__getVersionArg("WORKING")
196
197 if urls is not None:
198 rev1 = self.__getVersionArg("HEAD")
199 rev2 = self.__getVersionArg("HEAD")
200
201 if isinstance(fn, list):
202 dname, fnames = self.vcs.splitPathList(fn)
203 else:
204 dname, fname = self.vcs.splitPath(fn)
205 fnames = [fname]
206
207 with EricOverrideCursor():
208 cwd = os.getcwd()
209 os.chdir(dname)
210 try:
211 dname = ericApp().getObject('Project').getRelativePath(dname)
212 if dname:
213 dname += "/"
214 with EricMutexLocker(self.vcs.vcsExecutionMutex):
215 for name in fnames:
216 self.__showError(
217 self.tr("Processing file '{0}'...\n").format(name))
218 if urls is not None:
219 url1 = "{0}/{1}{2}".format(urls[0], dname, name)
220 url2 = "{0}/{1}{2}".format(urls[1], dname, name)
221 if summary:
222 diff_summary = self.client.diff_summarize(
223 url1, revision1=rev1,
224 url_or_path2=url2, revision2=rev2,
225 recurse=recurse)
226 diff_list = []
227 for diff_sum in diff_summary:
228 path = diff_sum['path']
229 diff_list.append("{0} {1}".format(
230 self.__getDiffSummaryKind(
231 diff_sum['summarize_kind']),
232 path))
233 diffText = os.linesep.join(diff_list)
234 else:
235 diffText = self.client.diff(
236 tmpdir,
237 url1, revision1=rev1,
238 url_or_path2=url2, revision2=rev2,
239 recurse=recurse)
240 else:
241 if pegRev is not None:
242 diffText = self.client.diff_peg(
243 tmpdir, name,
244 peg_revision=self.__getVersionArg(pegRev),
245 revision_start=rev1, revision_end=rev2,
246 recurse=recurse)
247 else:
248 diffText = self.client.diff(
249 tmpdir, name,
250 revision1=rev1, revision2=rev2,
251 recurse=recurse)
252 for counter, line in enumerate(diffText.splitlines()):
253 if (
254 line.startswith("--- ") or
255 line.startswith("+++ ")
256 ):
257 self.__processFileLine(line)
258
259 self.__appendText(
260 "{0}{1}".format(line, os.linesep))
261 if (
262 counter % 30 == 0 and
263 self._clientCancelCallback()
264 ):
265 # check for cancel every 30 lines
266 break
267 if self._clientCancelCallback():
268 break
269 except pysvn.ClientError as e:
270 self.__showError(e.args[0])
271 os.chdir(cwd)
272 self.__finish()
273
274 if self.paras == 0:
275 self.contents.setPlainText(self.tr('There is no difference.'))
276
277 self.buttonBox.button(
278 QDialogButtonBox.StandardButton.Save).setEnabled(self.paras > 0)
279
280 def __appendText(self, line):
281 """
282 Private method to append text to the end of the contents pane.
283
284 @param line line of text to insert (string)
285 """
286 tc = self.contents.textCursor()
287 tc.movePosition(QTextCursor.MoveOperation.End)
288 self.contents.setTextCursor(tc)
289 self.contents.insertPlainText(line)
290 self.paras += 1
291
292 def __extractFileName(self, line):
293 """
294 Private method to extract the file name out of a file separator line.
295
296 @param line line to be processed (string)
297 @return extracted file name (string)
298 """
299 f = line.split(None, 1)[1]
300 f = f.rsplit(None, 2)[0]
301 return f
302
303 def __processFileLine(self, line):
304 """
305 Private slot to process a line giving the old/new file.
306
307 @param line line to be processed (string)
308 """
309 if line.startswith('---'):
310 self.__oldFileLine = self.paras
311 self.__oldFile = self.__extractFileName(line)
312 else:
313 self.__fileSeparators.append(
314 (self.__oldFile, self.__extractFileName(line),
315 self.__oldFileLine))
316
317 def __finish(self):
318 """
319 Private slot called when the user pressed the button.
320 """
321 self.refreshButton.setEnabled(True)
322 self.buttonBox.button(
323 QDialogButtonBox.StandardButton.Cancel).setEnabled(False)
324 self.buttonBox.button(
325 QDialogButtonBox.StandardButton.Close).setEnabled(True)
326 self.buttonBox.button(
327 QDialogButtonBox.StandardButton.Close).setEnabled(True)
328 self.buttonBox.button(
329 QDialogButtonBox.StandardButton.Close).setDefault(True)
330
331 tc = self.contents.textCursor()
332 tc.movePosition(QTextCursor.MoveOperation.Start)
333 self.contents.setTextCursor(tc)
334 self.contents.ensureCursorVisible()
335
336 self.filesCombo.addItem(self.tr("<Start>"), 0)
337 self.filesCombo.addItem(self.tr("<End>"), -1)
338 for oldFile, newFile, pos in sorted(self.__fileSeparators):
339 if oldFile != newFile:
340 self.filesCombo.addItem(
341 "{0}\n{1}".format(oldFile, newFile), pos)
342 else:
343 self.filesCombo.addItem(oldFile, pos)
344
345 self._cancel()
346
347 def on_buttonBox_clicked(self, button):
348 """
349 Private slot called by a button of the button box clicked.
350
351 @param button button that was clicked (QAbstractButton)
352 """
353 if button == self.buttonBox.button(
354 QDialogButtonBox.StandardButton.Close
355 ):
356 self.close()
357 elif button == self.buttonBox.button(
358 QDialogButtonBox.StandardButton.Cancel
359 ):
360 self.__finish()
361 elif button == self.buttonBox.button(
362 QDialogButtonBox.StandardButton.Save
363 ):
364 self.on_saveButton_clicked()
365 elif button == self.refreshButton:
366 self.on_refreshButton_clicked()
367
368 @pyqtSlot(int)
369 def on_filesCombo_activated(self, index):
370 """
371 Private slot to handle the selection of a file.
372
373 @param index activated row (integer)
374 """
375 para = self.filesCombo.itemData(index)
376
377 if para == 0:
378 tc = self.contents.textCursor()
379 tc.movePosition(QTextCursor.MoveOperation.Start)
380 self.contents.setTextCursor(tc)
381 self.contents.ensureCursorVisible()
382 elif para == -1:
383 tc = self.contents.textCursor()
384 tc.movePosition(QTextCursor.MoveOperation.End)
385 self.contents.setTextCursor(tc)
386 self.contents.ensureCursorVisible()
387 else:
388 # step 1: move cursor to end
389 tc = self.contents.textCursor()
390 tc.movePosition(QTextCursor.MoveOperation.End)
391 self.contents.setTextCursor(tc)
392 self.contents.ensureCursorVisible()
393
394 # step 2: move cursor to desired line
395 tc = self.contents.textCursor()
396 delta = tc.blockNumber() - para
397 tc.movePosition(
398 QTextCursor.MoveOperation.PreviousBlock,
399 QTextCursor.MoveMode.MoveAnchor,
400 delta)
401 self.contents.setTextCursor(tc)
402 self.contents.ensureCursorVisible()
403
404 @pyqtSlot()
405 def on_saveButton_clicked(self):
406 """
407 Private slot to handle the Save button press.
408
409 It saves the diff shown in the dialog to a file in the local
410 filesystem.
411 """
412 if isinstance(self.filename, list):
413 if len(self.filename) > 1:
414 fname = self.vcs.splitPathList(self.filename)[0]
415 else:
416 dname, fname = self.vcs.splitPath(self.filename[0])
417 if fname != '.':
418 fname = "{0}.diff".format(self.filename[0])
419 else:
420 fname = dname
421 else:
422 fname = self.vcs.splitPath(self.filename)[0]
423
424 fname, selectedFilter = EricFileDialog.getSaveFileNameAndFilter(
425 self,
426 self.tr("Save Diff"),
427 fname,
428 self.tr("Patch Files (*.diff)"),
429 None,
430 EricFileDialog.DontConfirmOverwrite)
431
432 if not fname:
433 return # user aborted
434
435 fpath = pathlib.Path(fname)
436 if not fpath.suffix:
437 ex = selectedFilter.split("(*")[1].split(")")[0]
438 if ex:
439 fpath = fpath.with_suffix(ex)
440 if fpath.exists():
441 res = EricMessageBox.yesNo(
442 self,
443 self.tr("Save Diff"),
444 self.tr("<p>The patch file <b>{0}</b> already exists."
445 " Overwrite it?</p>").format(fpath),
446 icon=EricMessageBox.Warning)
447 if not res:
448 return
449
450 eol = ericApp().getObject("Project").getEolString()
451 try:
452 with fpath.open("w", encoding="utf-8", newline="") as f:
453 f.write(eol.join(self.contents.toPlainText().splitlines()))
454 except OSError as why:
455 EricMessageBox.critical(
456 self, self.tr('Save Diff'),
457 self.tr(
458 '<p>The patch file <b>{0}</b> could not be saved.'
459 '<br>Reason: {1}</p>')
460 .format(fpath, str(why)))
461
462 @pyqtSlot()
463 def on_refreshButton_clicked(self):
464 """
465 Private slot to refresh the display.
466 """
467 self.buttonBox.button(
468 QDialogButtonBox.StandardButton.Close).setEnabled(False)
469
470 self.buttonBox.button(
471 QDialogButtonBox.StandardButton.Save).setEnabled(False)
472 self.refreshButton.setEnabled(False)
473
474 self.start(self.filename, refreshable=True)
475
476 def __showError(self, msg):
477 """
478 Private slot to show an error message.
479
480 @param msg error message to show (string)
481 """
482 self.errorGroup.show()
483 self.errors.insertPlainText(msg)
484 self.errors.ensureCursorVisible()

eric ide

mercurial