eric6/Plugins/VcsPlugins/vcsPySvn/SvnDiffDialog.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
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
13 import os
14 import sys
15
16 import pysvn
17
18 from PyQt5.QtCore import QMutexLocker, QFileInfo, QDateTime, Qt, pyqtSlot
19 from PyQt5.QtGui import QCursor, QTextCursor
20 from PyQt5.QtWidgets import QWidget, QApplication, QDialogButtonBox
21
22 from E5Gui.E5Application import e5App
23 from E5Gui import E5MessageBox, E5FileDialog
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(SvnDiffDialog, self).__init__(parent)
45 self.setupUi(self)
46 SvnDialogMixin.__init__(self)
47
48 self.refreshButton = self.buttonBox.addButton(
49 self.tr("Refresh"), QDialogButtonBox.ActionRole)
50 self.refreshButton.setToolTip(
51 self.tr("Press to refresh the display"))
52 self.refreshButton.setEnabled(False)
53 self.buttonBox.button(QDialogButtonBox.Save).setEnabled(False)
54 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
55 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
56
57 self.searchWidget.attachTextEdit(self.contents)
58
59 self.vcs = vcs
60
61 font = Preferences.getEditorOtherFonts("MonospacedFont")
62 self.contents.setFontFamily(font.family())
63 self.contents.setFontPointSize(font.pointSize())
64
65 self.highlighter = SvnDiffHighlighter(self.contents.document())
66
67 self.client = self.vcs.getClient()
68 self.client.callback_cancel = \
69 self._clientCancelCallback
70 self.client.callback_get_login = \
71 self._clientLoginCallback
72 self.client.callback_ssl_server_trust_prompt = \
73 self._clientSslServerTrustPromptCallback
74
75 def __getVersionArg(self, version):
76 """
77 Private method to get a pysvn revision object for the given version
78 number.
79
80 @param version revision (integer or string)
81 @return revision object (pysvn.Revision)
82 """
83 if isinstance(version, int):
84 return pysvn.Revision(pysvn.opt_revision_kind.number, version)
85 elif version.startswith("{"):
86 dateStr = version[1:-1]
87 secs = QDateTime.fromString(dateStr, Qt.ISODate).toTime_t()
88 return pysvn.Revision(pysvn.opt_revision_kind.date, secs)
89 elif version == "HEAD":
90 return pysvn.Revision(pysvn.opt_revision_kind.head)
91 elif version == "COMMITTED":
92 return pysvn.Revision(pysvn.opt_revision_kind.committed)
93 elif version == "BASE":
94 return pysvn.Revision(pysvn.opt_revision_kind.base)
95 elif version == "WORKING":
96 return pysvn.Revision(pysvn.opt_revision_kind.working)
97 elif version == "PREV":
98 return pysvn.Revision(pysvn.opt_revision_kind.previous)
99 else:
100 return pysvn.Revision(pysvn.opt_revision_kind.unspecified)
101
102 def __getDiffSummaryKind(self, summaryKind):
103 """
104 Private method to get a string descripion of the diff summary.
105
106 @param summaryKind (pysvn.diff_summarize.summarize_kind)
107 @return one letter string indicating the change type (string)
108 """
109 if summaryKind == pysvn.diff_summarize_kind.delete:
110 return "D"
111 elif summaryKind == pysvn.diff_summarize_kind.modified:
112 return "M"
113 elif summaryKind == pysvn.diff_summarize_kind.added:
114 return "A"
115 elif summaryKind == pysvn.diff_summarize_kind.normal:
116 return "N"
117 else:
118 return " "
119
120 def start(self, fn, versions=None, urls=None, summary=False, pegRev=None,
121 refreshable=False):
122 """
123 Public slot to start the svn diff command.
124
125 @param fn filename to be diffed (string)
126 @param versions list of versions to be diffed (list of up to 2 integer
127 or None)
128 @keyparam urls list of repository URLs (list of 2 strings)
129 @keyparam summary flag indicating a summarizing diff
130 (only valid for URL diffs) (boolean)
131 @keyparam pegRev revision number the filename is valid (integer)
132 @keyparam refreshable flag indicating a refreshable diff (boolean)
133 """
134 self.refreshButton.setVisible(refreshable)
135
136 self.buttonBox.button(QDialogButtonBox.Save).setEnabled(False)
137 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
138 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
139 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
140
141 self._reset()
142 self.errorGroup.hide()
143
144 QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
145 QApplication.processEvents()
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'):
165 tmpdir = '/var/tmp'
166 elif os.path.exists('/usr/tmp'):
167 tmpdir = '/usr/tmp'
168 elif os.path.exists('/tmp'):
169 tmpdir = '/tmp'
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 locker = QMutexLocker(self.vcs.vcsExecutionMutex)
207 cwd = os.getcwd()
208 os.chdir(dname)
209 try:
210 dname = e5App().getObject('Project').getRelativePath(dname)
211 if dname:
212 dname += "/"
213 for name in fnames:
214 self.__showError(
215 self.tr("Processing file '{0}'...\n").format(name))
216 if urls is not None:
217 url1 = "{0}/{1}{2}".format(urls[0], dname, name)
218 url2 = "{0}/{1}{2}".format(urls[1], dname, name)
219 if summary:
220 diff_summary = self.client.diff_summarize(
221 url1, revision1=rev1,
222 url_or_path2=url2, revision2=rev2,
223 recurse=recurse)
224 diff_list = []
225 for diff_sum in diff_summary:
226 path = diff_sum['path']
227 if sys.version_info[0] == 2:
228 path = path.decode('utf-8')
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 if sys.version_info[0] == 2:
241 diffText = diffText.decode('utf-8')
242 else:
243 if pegRev is not None:
244 diffText = self.client.diff_peg(
245 tmpdir, name,
246 peg_revision=self.__getVersionArg(pegRev),
247 revision_start=rev1, revision_end=rev2,
248 recurse=recurse)
249 else:
250 diffText = self.client.diff(
251 tmpdir, name,
252 revision1=rev1, revision2=rev2, recurse=recurse)
253 if sys.version_info[0] == 2:
254 diffText = diffText.decode('utf-8')
255 counter = 0
256 for line in diffText.splitlines():
257 if line.startswith("--- ") or \
258 line.startswith("+++ "):
259 self.__processFileLine(line)
260
261 self.__appendText("{0}{1}".format(line, os.linesep))
262 counter += 1
263 if counter == 30:
264 # check for cancel every 30 lines
265 counter = 0
266 if self._clientCancelCallback():
267 break
268 if self._clientCancelCallback():
269 break
270 except pysvn.ClientError as e:
271 self.__showError(e.args[0])
272 locker.unlock()
273 os.chdir(cwd)
274 self.__finish()
275
276 if self.paras == 0:
277 self.contents.setPlainText(self.tr('There is no difference.'))
278
279 self.buttonBox.button(QDialogButtonBox.Save).setEnabled(self.paras > 0)
280
281 def __appendText(self, line):
282 """
283 Private method to append text to the end of the contents pane.
284
285 @param line line of text to insert (string)
286 """
287 tc = self.contents.textCursor()
288 tc.movePosition(QTextCursor.End)
289 self.contents.setTextCursor(tc)
290 self.contents.insertPlainText(line)
291 self.paras += 1
292
293 def __extractFileName(self, line):
294 """
295 Private method to extract the file name out of a file separator line.
296
297 @param line line to be processed (string)
298 @return extracted file name (string)
299 """
300 f = line.split(None, 1)[1]
301 f = f.rsplit(None, 2)[0]
302 return f
303
304 def __processFileLine(self, line):
305 """
306 Private slot to process a line giving the old/new file.
307
308 @param line line to be processed (string)
309 """
310 if line.startswith('---'):
311 self.__oldFileLine = self.paras
312 self.__oldFile = self.__extractFileName(line)
313 else:
314 self.__fileSeparators.append(
315 (self.__oldFile, self.__extractFileName(line),
316 self.__oldFileLine))
317
318 def __finish(self):
319 """
320 Private slot called when the user pressed the button.
321 """
322 QApplication.restoreOverrideCursor()
323
324 self.refreshButton.setEnabled(True)
325 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
326 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
327 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
328 self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
329
330 tc = self.contents.textCursor()
331 tc.movePosition(QTextCursor.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(QDialogButtonBox.Close):
353 self.close()
354 elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
355 self.__finish()
356 elif button == self.buttonBox.button(QDialogButtonBox.Save):
357 self.on_saveButton_clicked()
358 elif button == self.refreshButton:
359 self.on_refreshButton_clicked()
360
361 @pyqtSlot(int)
362 def on_filesCombo_activated(self, index):
363 """
364 Private slot to handle the selection of a file.
365
366 @param index activated row (integer)
367 """
368 para = self.filesCombo.itemData(index)
369
370 if para == 0:
371 tc = self.contents.textCursor()
372 tc.movePosition(QTextCursor.Start)
373 self.contents.setTextCursor(tc)
374 self.contents.ensureCursorVisible()
375 elif para == -1:
376 tc = self.contents.textCursor()
377 tc.movePosition(QTextCursor.End)
378 self.contents.setTextCursor(tc)
379 self.contents.ensureCursorVisible()
380 else:
381 # step 1: move cursor to end
382 tc = self.contents.textCursor()
383 tc.movePosition(QTextCursor.End)
384 self.contents.setTextCursor(tc)
385 self.contents.ensureCursorVisible()
386
387 # step 2: move cursor to desired line
388 tc = self.contents.textCursor()
389 delta = tc.blockNumber() - para
390 tc.movePosition(QTextCursor.PreviousBlock, QTextCursor.MoveAnchor,
391 delta)
392 self.contents.setTextCursor(tc)
393 self.contents.ensureCursorVisible()
394
395 @pyqtSlot()
396 def on_saveButton_clicked(self):
397 """
398 Private slot to handle the Save button press.
399
400 It saves the diff shown in the dialog to a file in the local
401 filesystem.
402 """
403 if isinstance(self.filename, list):
404 if len(self.filename) > 1:
405 fname = self.vcs.splitPathList(self.filename)[0]
406 else:
407 dname, fname = self.vcs.splitPath(self.filename[0])
408 if fname != '.':
409 fname = "{0}.diff".format(self.filename[0])
410 else:
411 fname = dname
412 else:
413 fname = self.vcs.splitPath(self.filename)[0]
414
415 fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
416 self,
417 self.tr("Save Diff"),
418 fname,
419 self.tr("Patch Files (*.diff)"),
420 None,
421 E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
422
423 if not fname:
424 return # user aborted
425
426 ext = QFileInfo(fname).suffix()
427 if not ext:
428 ex = selectedFilter.split("(*")[1].split(")")[0]
429 if ex:
430 fname += ex
431 if QFileInfo(fname).exists():
432 res = E5MessageBox.yesNo(
433 self,
434 self.tr("Save Diff"),
435 self.tr("<p>The patch file <b>{0}</b> already exists."
436 " Overwrite it?</p>").format(fname),
437 icon=E5MessageBox.Warning)
438 if not res:
439 return
440 fname = Utilities.toNativeSeparators(fname)
441
442 eol = e5App().getObject("Project").getEolString()
443 try:
444 f = open(fname, "w", encoding="utf-8", newline="")
445 f.write(eol.join(self.contents.toPlainText().splitlines()))
446 f.close()
447 except IOError as why:
448 E5MessageBox.critical(
449 self, self.tr('Save Diff'),
450 self.tr(
451 '<p>The patch file <b>{0}</b> could not be saved.'
452 '<br>Reason: {1}</p>')
453 .format(fname, str(why)))
454
455 @pyqtSlot()
456 def on_refreshButton_clicked(self):
457 """
458 Private slot to refresh the display.
459 """
460 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
461
462 self.buttonBox.button(QDialogButtonBox.Save).setEnabled(False)
463 self.refreshButton.setEnabled(False)
464
465 self.start(self.filename, refreshable=True)
466
467 def __showError(self, msg):
468 """
469 Private slot to show an error message.
470
471 @param msg error message to show (string)
472 """
473 self.errorGroup.show()
474 self.errors.insertPlainText(msg)
475 self.errors.ensureCursorVisible()

eric ide

mercurial