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

eric ide

mercurial