eric7/Plugins/VcsPlugins/vcsPySvn/SvnDiffDialog.py

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