Plugins/VcsPlugins/vcsGit/GitDiffDialog.py

changeset 6020
baf6da1ae288
child 6048
82ad8ec9548c
equal deleted inserted replaced
6019:58ecdaf0b789 6020:baf6da1ae288
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2014 - 2017 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a dialog to show the output of the git diff command
8 process.
9 """
10
11 from __future__ import unicode_literals
12 try:
13 str = unicode
14 except NameError:
15 pass
16
17 from PyQt5.QtCore import pyqtSlot, QFileInfo, Qt
18 from PyQt5.QtGui import QTextCursor, QCursor
19 from PyQt5.QtWidgets import QWidget, QDialogButtonBox, QApplication
20
21 from E5Gui import E5MessageBox, E5FileDialog
22 from E5Gui.E5Application import e5App
23
24 from .Ui_GitDiffDialog import Ui_GitDiffDialog
25
26 from .GitDiffHighlighter import GitDiffHighlighter
27 from .GitDiffGenerator import GitDiffGenerator
28
29 import Utilities
30 import Preferences
31
32
33 class GitDiffDialog(QWidget, Ui_GitDiffDialog):
34 """
35 Class implementing a dialog to show the output of the git diff command
36 process.
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(GitDiffDialog, self).__init__(parent)
46 self.setupUi(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).setDefault(True)
55
56 try:
57 # insert the search widget if it is available
58 from E5Gui.E5TextEditSearchWidget import E5TextEditSearchWidget
59 self.searchWidget = E5TextEditSearchWidget(self.contentsGroup)
60 self.searchWidget.setFocusPolicy(Qt.WheelFocus)
61 self.searchWidget.setObjectName("searchWidget")
62 self.contentsGroup.layout().insertWidget(1, self.searchWidget)
63 self.searchWidget.attachTextEdit(self.contents)
64
65 self.searchWidget2 = E5TextEditSearchWidget(self.contentsGroup)
66 self.searchWidget2.setFocusPolicy(Qt.WheelFocus)
67 self.searchWidget2.setObjectName("searchWidget2")
68 self.contentsGroup.layout().addWidget(self.searchWidget2)
69 self.searchWidget2.attachTextEdit(self.contents2)
70
71 self.setTabOrder(self.filesCombo, self.searchWidget)
72 self.setTabOrder(self.searchWidget, self.contents)
73 self.setTabOrder(self.contents, self.contents2)
74 self.setTabOrder(self.contents2, self.searchWidget2)
75 self.setTabOrder(self.searchWidget2, self.errors)
76 except ImportError:
77 # eric6 version without search widget
78 self.searchWidget = None
79 self.searchWidget2 = None
80
81 self.vcs = vcs
82
83 font = Preferences.getEditorOtherFonts("MonospacedFont")
84 self.contents.setFontFamily(font.family())
85 self.contents.setFontPointSize(font.pointSize())
86 self.contents2.setFontFamily(font.family())
87 self.contents2.setFontPointSize(font.pointSize())
88
89 self.highlighter = GitDiffHighlighter(self.contents.document())
90 self.highlighter2 = GitDiffHighlighter(self.contents2.document())
91
92 self.__diffGenerator = GitDiffGenerator(vcs, self)
93 self.__diffGenerator.finished.connect(self.__generatorFinished)
94
95 self.__modeMessages = {
96 "work2stage": self.tr("Working Tree to Staging Area"),
97 "stage2repo": self.tr("Staging Area to HEAD Commit"),
98 "work2repo": self.tr("Working Tree to HEAD Commit"),
99 "work2stage2repo": self.tr("Working to Staging (top)"
100 " and Staging to HEAD (bottom)"),
101 "stash": self.tr("Stash Contents"),
102 "stashName": self.tr("Stash Contents of {0}"),
103 }
104
105 def closeEvent(self, e):
106 """
107 Protected slot implementing a close event handler.
108
109 @param e close event (QCloseEvent)
110 """
111 self.__diffGenerator.stopProcesses()
112 e.accept()
113
114 def start(self, fn, versions=None, diffMode="work2repo", stashName="",
115 refreshable=False):
116 """
117 Public slot to start the git diff command.
118
119 @param fn filename to be diffed (string)
120 @param versions list of versions to be diffed (list of up to 2 strings
121 or None)
122 @param diffMode indication for the type of diff to be performed (
123 'work2repo' compares the working tree with the HEAD commit,
124 'work2stage' compares the working tree with the staging area,
125 'stage2repo' compares the staging area with the HEAD commit,
126 'work2stage2repo' compares the working tree with the staging area
127 and the staging area with the HEAD commit,
128 'stash' shows the diff for a stash)
129 @param stashName name of the stash to show a diff for (string)
130 @param refreshable flag indicating a refreshable diff (boolean)
131 """
132 assert diffMode in ["work2repo", "work2stage", "stage2repo",
133 "work2stage2repo", "stash"]
134
135 self.refreshButton.setVisible(refreshable)
136
137 self.__filename = fn
138 self.__diffMode = diffMode
139
140 self.errorGroup.hide()
141
142 self.contents.clear()
143 self.contents2.clear()
144 self.contents2.setVisible(diffMode == "work2stage2repo")
145 if self.searchWidget2:
146 self.searchWidget2.setVisible(diffMode == "work2stage2repo")
147
148 self.filesCombo.clear()
149
150 try:
151 self.highlighter.regenerateRules()
152 self.highlighter2.regenerateRules()
153 except AttributeError:
154 # backward compatibility
155 pass
156
157 if diffMode in ["work2repo", "work2stage", "stage2repo",
158 "work2stage2repo"]:
159 self.contentsGroup.setTitle(
160 self.tr("Difference ({0})")
161 .format(self.__modeMessages[diffMode]))
162
163 if versions is not None:
164 self.raise_()
165 self.activateWindow()
166 elif diffMode == "stash":
167 if stashName:
168 msg = self.__modeMessages["stashName"].format(stashName)
169 else:
170 msg = self.__modeMessages["stash"]
171 self.contentsGroup.setTitle(
172 self.tr("Difference ({0})").format(msg))
173
174 QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
175 procStarted = self.__diffGenerator.start(
176 fn, versions=versions, diffMode=diffMode, stashName=stashName)
177 if not procStarted:
178 QApplication.restoreOverrideCursor()
179 E5MessageBox.critical(
180 self,
181 self.tr('Process Generation Error'),
182 self.tr(
183 'The process {0} could not be started. '
184 'Ensure, that it is in the search path.'
185 ).format('git'))
186 return
187
188 def __generatorFinished(self):
189 """
190 Private slot connected to the finished signal.
191 """
192 QApplication.restoreOverrideCursor()
193 self.refreshButton.setEnabled(True)
194
195 diff1, diff2, errors, fileSeparators = self.__diffGenerator.getResult()
196
197 if diff1:
198 self.contents.setPlainText("".join(diff1))
199 else:
200 self.contents.setPlainText(
201 self.tr('There is no difference.'))
202
203 if diff2:
204 self.contents2.setPlainText("".join(diff2))
205 else:
206 self.contents2.setPlainText(
207 self.tr('There is no difference.'))
208
209 if errors:
210 self.errorGroup.show()
211 self.errors.setPlainText(errors)
212 self.errors.ensureCursorVisible()
213
214 self.buttonBox.button(QDialogButtonBox.Save).setEnabled(bool(diff2))
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 for contents in [self.contents, self.contents2]:
221 tc = contents.textCursor()
222 tc.movePosition(QTextCursor.Start)
223 contents.setTextCursor(tc)
224 contents.ensureCursorVisible()
225
226 fileSeparators = self.__mergeFileSeparators(fileSeparators)
227 self.filesCombo.addItem(self.tr("<Start>"), (0, 0))
228 self.filesCombo.addItem(self.tr("<End>"), (-1, -1))
229 for oldFile, newFile, pos1, pos2 in sorted(fileSeparators):
230 if oldFile != newFile:
231 self.filesCombo.addItem(
232 "{0} -- {1}".format(oldFile, newFile), (pos1, pos2))
233 else:
234 self.filesCombo.addItem(oldFile, (pos1, pos2))
235
236 def __mergeFileSeparators(self, fileSeparators):
237 """
238 Private method to merge the file separator entries.
239
240 @param fileSeparators list of file separator entries to be merged
241 @return merged list of file separator entries
242 """
243 separators = {}
244 for oldFile, newFile, pos1, pos2 in sorted(fileSeparators):
245 if (oldFile, newFile) not in separators:
246 separators[(oldFile, newFile)] = [oldFile, newFile, pos1, pos2]
247 else:
248 if pos1 != -2:
249 separators[(oldFile, newFile)][2] = pos1
250 if pos2 != -2:
251 separators[(oldFile, newFile)][3] = pos2
252 return list(separators.values())
253
254 def on_buttonBox_clicked(self, button):
255 """
256 Private slot called by a button of the button box clicked.
257
258 @param button button that was clicked (QAbstractButton)
259 """
260 if button == self.buttonBox.button(QDialogButtonBox.Save):
261 self.on_saveButton_clicked()
262 elif button == self.refreshButton:
263 self.on_refreshButton_clicked()
264
265 @pyqtSlot(int)
266 def on_filesCombo_activated(self, index):
267 """
268 Private slot to handle the selection of a file.
269
270 @param index activated row (integer)
271 """
272 para1, para2 = self.filesCombo.itemData(index)
273
274 for para, contents in [(para1, self.contents),
275 (para2, self.contents2)]:
276 if para == 0:
277 tc = contents.textCursor()
278 tc.movePosition(QTextCursor.Start)
279 contents.setTextCursor(tc)
280 contents.ensureCursorVisible()
281 elif para == -1:
282 tc = contents.textCursor()
283 tc.movePosition(QTextCursor.End)
284 contents.setTextCursor(tc)
285 contents.ensureCursorVisible()
286 else:
287 # step 1: move cursor to end
288 tc = contents.textCursor()
289 tc.movePosition(QTextCursor.End)
290 contents.setTextCursor(tc)
291 contents.ensureCursorVisible()
292
293 # step 2: move cursor to desired line
294 tc = contents.textCursor()
295 delta = tc.blockNumber() - para
296 tc.movePosition(QTextCursor.PreviousBlock,
297 QTextCursor.MoveAnchor,
298 delta)
299 contents.setTextCursor(tc)
300 contents.ensureCursorVisible()
301
302 @pyqtSlot()
303 def on_saveButton_clicked(self):
304 """
305 Private slot to handle the Save button press.
306
307 It saves the diff shown in the dialog to a file in the local
308 filesystem.
309 """
310 if isinstance(self.__filename, list):
311 if len(self.__filename) > 1:
312 fname = self.vcs.splitPathList(self.__filename)[0]
313 else:
314 dname, fname = self.vcs.splitPath(self.__filename[0])
315 if fname != '.':
316 fname = "{0}.diff".format(self.__filename[0])
317 else:
318 fname = dname
319 else:
320 fname = self.vcs.splitPath(self.__filename)[0]
321
322 fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
323 self,
324 self.tr("Save Diff"),
325 fname,
326 self.tr("Patch Files (*.diff)"),
327 None,
328 E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
329
330 if not fname:
331 return # user aborted
332
333 ext = QFileInfo(fname).suffix()
334 if not ext:
335 ex = selectedFilter.split("(*")[1].split(")")[0]
336 if ex:
337 fname += ex
338 if QFileInfo(fname).exists():
339 res = E5MessageBox.yesNo(
340 self,
341 self.tr("Save Diff"),
342 self.tr("<p>The patch file <b>{0}</b> already exists."
343 " Overwrite it?</p>").format(fname),
344 icon=E5MessageBox.Warning)
345 if not res:
346 return
347 fname = Utilities.toNativeSeparators(fname)
348
349 eol = e5App().getObject("Project").getEolString()
350 try:
351 f = open(fname, "w", encoding="utf-8", newline="")
352 f.write(eol.join(self.contents2.toPlainText().splitlines()))
353 f.write(eol)
354 f.close()
355 except IOError as why:
356 E5MessageBox.critical(
357 self, self.tr('Save Diff'),
358 self.tr(
359 '<p>The patch file <b>{0}</b> could not be saved.'
360 '<br>Reason: {1}</p>')
361 .format(fname, str(why)))
362
363 @pyqtSlot()
364 def on_refreshButton_clicked(self):
365 """
366 Private slot to refresh the display.
367 """
368 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
369
370 self.buttonBox.button(QDialogButtonBox.Save).setEnabled(False)
371 self.refreshButton.setEnabled(False)
372
373 self.start(self.__filename, diffMode=self.__diffMode, refreshable=True)

eric ide

mercurial