eric7/Plugins/VcsPlugins/vcsGit/GitDiffDialog.py

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

eric ide

mercurial