src/eric7/Plugins/VcsPlugins/vcsGit/GitStashBrowserDialog.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 8881
54e42bc2437a
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 stashes.
8 """
9
10 import os
11
12 from PyQt6.QtCore import pyqtSlot, Qt, QPoint, QProcess, QTimer
13 from PyQt6.QtWidgets import (
14 QWidget, QDialogButtonBox, QTreeWidgetItem, QAbstractButton, QMenu,
15 QHeaderView, QApplication, QLineEdit
16 )
17
18 from EricWidgets import EricMessageBox
19 from EricGui.EricOverrideCursor import EricOverrideCursorProcess
20
21 from .Ui_GitStashBrowserDialog import Ui_GitStashBrowserDialog
22
23 import Preferences
24 from Globals import strToQByteArray
25
26
27 class GitStashBrowserDialog(QWidget, Ui_GitStashBrowserDialog):
28 """
29 Class implementing a dialog to show the stashes.
30 """
31 NameColumn = 0
32 DateColumn = 1
33 MessageColumn = 2
34
35 Separator = "@@||@@"
36
37 TotalStatisticsRole = Qt.ItemDataRole.UserRole
38 FileStatisticsRole = Qt.ItemDataRole.UserRole + 1
39
40 def __init__(self, vcs, parent=None):
41 """
42 Constructor
43
44 @param vcs reference to the vcs object
45 @param parent reference to the parent widget (QWidget)
46 """
47 super().__init__(parent)
48 self.setupUi(self)
49
50 self.buttonBox.button(
51 QDialogButtonBox.StandardButton.Close).setEnabled(False)
52 self.buttonBox.button(
53 QDialogButtonBox.StandardButton.Cancel).setDefault(True)
54
55 self.__position = QPoint()
56
57 self.__fileStatisticsRole = Qt.ItemDataRole.UserRole
58 self.__totalStatisticsRole = Qt.ItemDataRole.UserRole + 1
59
60 self.stashList.header().setSortIndicator(
61 0, Qt.SortOrder.AscendingOrder)
62
63 self.refreshButton = self.buttonBox.addButton(
64 self.tr("&Refresh"), QDialogButtonBox.ButtonRole.ActionRole)
65 self.refreshButton.setToolTip(
66 self.tr("Press to refresh the list of stashes"))
67 self.refreshButton.setEnabled(False)
68
69 self.vcs = vcs
70 self.__resetUI()
71
72 self.__ioEncoding = Preferences.getSystem("IOEncoding")
73
74 self.__process = EricOverrideCursorProcess()
75 self.__process.finished.connect(self.__procFinished)
76 self.__process.readyReadStandardOutput.connect(self.__readStdout)
77 self.__process.readyReadStandardError.connect(self.__readStderr)
78
79 self.__contextMenu = QMenu()
80 self.__differencesAct = self.__contextMenu.addAction(
81 self.tr("Show"), self.__showPatch)
82 self.__contextMenu.addSeparator()
83 self.__applyAct = self.__contextMenu.addAction(
84 self.tr("Restore && Keep"), self.__apply)
85 self.__popAct = self.__contextMenu.addAction(
86 self.tr("Restore && Delete"), self.__pop)
87 self.__contextMenu.addSeparator()
88 self.__branchAct = self.__contextMenu.addAction(
89 self.tr("Create Branch"), self.__branch)
90 self.__contextMenu.addSeparator()
91 self.__dropAct = self.__contextMenu.addAction(
92 self.tr("Delete"), self.__drop)
93 self.__clearAct = self.__contextMenu.addAction(
94 self.tr("Delete All"), self.__clear)
95
96 def closeEvent(self, e):
97 """
98 Protected slot implementing a close event handler.
99
100 @param e close event (QCloseEvent)
101 """
102 if (
103 self.__process is not None and
104 self.__process.state() != QProcess.ProcessState.NotRunning
105 ):
106 self.__process.terminate()
107 QTimer.singleShot(2000, self.__process.kill)
108 self.__process.waitForFinished(3000)
109
110 self.__position = self.pos()
111
112 e.accept()
113
114 def show(self):
115 """
116 Public slot to show the dialog.
117 """
118 if not self.__position.isNull():
119 self.move(self.__position)
120 self.__resetUI()
121
122 super().show()
123
124 def __resetUI(self):
125 """
126 Private method to reset the user interface.
127 """
128 self.stashList.clear()
129
130 def __resizeColumnsStashes(self):
131 """
132 Private method to resize the shelve list columns.
133 """
134 self.stashList.header().resizeSections(
135 QHeaderView.ResizeMode.ResizeToContents)
136 self.stashList.header().setStretchLastSection(True)
137
138 def __generateStashEntry(self, name, date, message):
139 """
140 Private method to generate the stash items.
141
142 @param name name of the stash (string)
143 @param date date the stash was created (string)
144 @param message stash message (string)
145 """
146 QTreeWidgetItem(self.stashList, [name, date, message])
147
148 def __getStashEntries(self):
149 """
150 Private method to retrieve the list of stashes.
151 """
152 self.buttonBox.button(
153 QDialogButtonBox.StandardButton.Close).setEnabled(False)
154 self.buttonBox.button(
155 QDialogButtonBox.StandardButton.Cancel).setEnabled(True)
156 self.buttonBox.button(
157 QDialogButtonBox.StandardButton.Cancel).setDefault(True)
158
159 self.inputGroup.setEnabled(True)
160 self.inputGroup.show()
161 self.refreshButton.setEnabled(False)
162
163 self.buf = []
164 self.errors.clear()
165 self.intercept = False
166
167 args = self.vcs.initCommand("stash")
168 args.append("list")
169 args.append("--format=format:%gd{0}%ai{0}%gs%n".format(self.Separator))
170
171 self.__process.kill()
172
173 self.__process.setWorkingDirectory(self.repodir)
174
175 self.inputGroup.setEnabled(True)
176 self.inputGroup.show()
177
178 self.__process.start('git', args)
179 procStarted = self.__process.waitForStarted(5000)
180 if not procStarted:
181 self.inputGroup.setEnabled(False)
182 self.inputGroup.hide()
183 EricMessageBox.critical(
184 self,
185 self.tr('Process Generation Error'),
186 self.tr(
187 'The process {0} could not be started. '
188 'Ensure, that it is in the search path.'
189 ).format('git'))
190
191 def start(self, projectDir):
192 """
193 Public slot to start the git stash command.
194
195 @param projectDir name of the project directory (string)
196 """
197 self.errorGroup.hide()
198 QApplication.processEvents()
199
200 self.__projectDir = projectDir
201
202 # find the root of the repo
203 self.repodir = self.__projectDir
204 while not os.path.isdir(os.path.join(self.repodir, self.vcs.adminDir)):
205 self.repodir = os.path.dirname(self.repodir)
206 if os.path.splitdrive(self.repodir)[1] == os.sep:
207 return
208
209 self.activateWindow()
210 self.raise_()
211
212 self.stashList.clear()
213 self.__started = True
214 self.__getStashEntries()
215
216 def __procFinished(self, exitCode, exitStatus):
217 """
218 Private slot connected to the finished signal.
219
220 @param exitCode exit code of the process (integer)
221 @param exitStatus exit status of the process (QProcess.ExitStatus)
222 """
223 self.__processBuffer()
224 self.__finish()
225
226 def __finish(self):
227 """
228 Private slot called when the process finished or the user pressed
229 the button.
230 """
231 if (
232 self.__process is not None and
233 self.__process.state() != QProcess.ProcessState.NotRunning
234 ):
235 self.__process.terminate()
236 QTimer.singleShot(2000, self.__process.kill)
237 self.__process.waitForFinished(3000)
238
239 self.buttonBox.button(
240 QDialogButtonBox.StandardButton.Close).setEnabled(True)
241 self.buttonBox.button(
242 QDialogButtonBox.StandardButton.Cancel).setEnabled(False)
243 self.buttonBox.button(
244 QDialogButtonBox.StandardButton.Close).setDefault(True)
245
246 self.inputGroup.setEnabled(False)
247 self.inputGroup.hide()
248 self.refreshButton.setEnabled(True)
249
250 def __processBuffer(self):
251 """
252 Private method to process the buffered output of the git stash command.
253 """
254 for line in self.buf:
255 name, date, message = line.split(self.Separator)
256 date = date.strip().rsplit(":", 1)[0]
257 self.__generateStashEntry(name, date, message.strip())
258
259 self.__resizeColumnsStashes()
260
261 if self.__started:
262 self.stashList.setCurrentItem(self.stashList.topLevelItem(0))
263 self.__started = False
264
265 def __readStdout(self):
266 """
267 Private slot to handle the readyReadStandardOutput signal.
268
269 It reads the output of the process and inserts it into a buffer.
270 """
271 self.__process.setReadChannel(QProcess.ProcessChannel.StandardOutput)
272
273 while self.__process.canReadLine():
274 line = str(self.__process.readLine(), self.__ioEncoding,
275 'replace').strip()
276 if line:
277 self.buf.append(line)
278
279 def __readStderr(self):
280 """
281 Private slot to handle the readyReadStandardError signal.
282
283 It reads the error output of the process and inserts it into the
284 error pane.
285 """
286 if self.__process is not None:
287 s = str(self.__process.readAllStandardError(),
288 self.__ioEncoding, 'replace')
289 self.errorGroup.show()
290 self.errors.insertPlainText(s)
291 self.errors.ensureCursorVisible()
292
293 @pyqtSlot(QAbstractButton)
294 def on_buttonBox_clicked(self, button):
295 """
296 Private slot called by a button of the button box clicked.
297
298 @param button button that was clicked (QAbstractButton)
299 """
300 if button == self.buttonBox.button(
301 QDialogButtonBox.StandardButton.Close
302 ):
303 self.close()
304 elif button == self.buttonBox.button(
305 QDialogButtonBox.StandardButton.Cancel
306 ):
307 self.cancelled = True
308 self.__finish()
309 elif button == self.refreshButton:
310 self.on_refreshButton_clicked()
311
312 @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem)
313 def on_stashList_currentItemChanged(self, current, previous):
314 """
315 Private slot called, when the current item of the stash list changes.
316
317 @param current reference to the new current item (QTreeWidgetItem)
318 @param previous reference to the old current item (QTreeWidgetItem)
319 """
320 self.statisticsList.clear()
321 self.filesLabel.setText("")
322 self.insertionsLabel.setText("")
323 self.deletionsLabel.setText("")
324
325 if current:
326 if current.data(0, self.TotalStatisticsRole) is None:
327 args = self.vcs.initCommand("stash")
328 args.append("show")
329 args.append('--numstat')
330 args.append(current.text(self.NameColumn))
331
332 output = ""
333 process = QProcess()
334 process.setWorkingDirectory(self.repodir)
335 process.start('git', args)
336 procStarted = process.waitForStarted(5000)
337 if procStarted:
338 finished = process.waitForFinished(30000)
339 if finished and process.exitCode() == 0:
340 output = str(process.readAllStandardOutput(),
341 Preferences.getSystem("IOEncoding"),
342 'replace')
343
344 if output:
345 totals = {"files": 0, "additions": 0, "deletions": 0}
346 fileData = []
347 for line in output.splitlines():
348 additions, deletions, name = (
349 line.strip().split(None, 2)
350 )
351 totals["files"] += 1
352 if additions != "-":
353 totals["additions"] += int(additions)
354 if deletions != "-":
355 totals["deletions"] += int(deletions)
356 fileData.append({
357 "file": name,
358 "total": ("-" if additions == "-" else
359 str(int(additions) + int(deletions))),
360 "added": additions,
361 "deleted": deletions
362 })
363 current.setData(0, self.TotalStatisticsRole, totals)
364 current.setData(0, self.FileStatisticsRole, fileData)
365 else:
366 return
367
368 for dataDict in current.data(0, self.FileStatisticsRole):
369 QTreeWidgetItem(self.statisticsList, [
370 dataDict["file"], dataDict["total"],
371 dataDict["added"], dataDict["deleted"]])
372 self.statisticsList.header().resizeSections(
373 QHeaderView.ResizeMode.ResizeToContents)
374 self.statisticsList.header().setStretchLastSection(True)
375
376 totals = current.data(0, self.TotalStatisticsRole)
377 self.filesLabel.setText(
378 self.tr("%n file(s) changed", None, totals["files"]))
379 self.insertionsLabel.setText(
380 self.tr("%n line(s) inserted", None, int(totals["additions"])))
381 self.deletionsLabel.setText(
382 self.tr("%n line(s) deleted", None, int(totals["deletions"])))
383
384 @pyqtSlot(QPoint)
385 def on_stashList_customContextMenuRequested(self, pos):
386 """
387 Private slot to show the context menu of the stash list.
388
389 @param pos position of the mouse pointer (QPoint)
390 """
391 enable = len(self.stashList.selectedItems()) == 1
392 self.__differencesAct.setEnabled(enable)
393 self.__applyAct.setEnabled(enable)
394 self.__popAct.setEnabled(enable)
395 self.__branchAct.setEnabled(enable)
396 self.__dropAct.setEnabled(enable)
397 self.__clearAct.setEnabled(self.stashList.topLevelItemCount() > 0)
398
399 self.__contextMenu.popup(self.mapToGlobal(pos))
400
401 @pyqtSlot()
402 def on_refreshButton_clicked(self):
403 """
404 Private slot to refresh the list of shelves.
405 """
406 self.start(self.__projectDir)
407
408 @pyqtSlot()
409 def on_sendButton_clicked(self):
410 """
411 Private slot to send the input to the git process.
412 """
413 inputTxt = self.input.text()
414 inputTxt += os.linesep
415
416 if self.passwordCheckBox.isChecked():
417 self.errors.insertPlainText(os.linesep)
418 self.errors.ensureCursorVisible()
419 else:
420 self.errors.insertPlainText(inputTxt)
421 self.errors.ensureCursorVisible()
422 self.errorGroup.show()
423
424 self.__process.write(strToQByteArray(inputTxt))
425
426 self.passwordCheckBox.setChecked(False)
427 self.input.clear()
428
429 @pyqtSlot()
430 def on_input_returnPressed(self):
431 """
432 Private slot to handle the press of the return key in the input field.
433 """
434 self.intercept = True
435 self.on_sendButton_clicked()
436
437 @pyqtSlot(bool)
438 def on_passwordCheckBox_toggled(self, checked):
439 """
440 Private slot to handle the password checkbox toggled.
441
442 @param checked flag indicating the status of the check box (boolean)
443 """
444 if checked:
445 self.input.setEchoMode(QLineEdit.EchoMode.Password)
446 else:
447 self.input.setEchoMode(QLineEdit.EchoMode.Normal)
448
449 def keyPressEvent(self, evt):
450 """
451 Protected slot to handle a key press event.
452
453 @param evt the key press event (QKeyEvent)
454 """
455 if self.intercept:
456 self.intercept = False
457 evt.accept()
458 return
459 super().keyPressEvent(evt)
460
461 def __showPatch(self):
462 """
463 Private slot to show the contents of the selected stash.
464 """
465 stashName = self.stashList.selectedItems()[0].text(self.NameColumn)
466 self.vcs.gitStashShowPatch(self.__projectDir, stashName)
467
468 def __apply(self):
469 """
470 Private slot to apply the selected stash but keep it.
471 """
472 stashName = self.stashList.selectedItems()[0].text(self.NameColumn)
473 self.vcs.gitStashApply(self.__projectDir, stashName)
474
475 def __pop(self):
476 """
477 Private slot to apply the selected stash and delete it.
478 """
479 stashName = self.stashList.selectedItems()[0].text(self.NameColumn)
480 self.vcs.gitStashPop(self.__projectDir, stashName)
481 self.on_refreshButton_clicked()
482
483 def __branch(self):
484 """
485 Private slot to create a branch from the selected stash.
486 """
487 stashName = self.stashList.selectedItems()[0].text(self.NameColumn)
488 self.vcs.gitStashBranch(self.__projectDir, stashName)
489 self.on_refreshButton_clicked()
490
491 def __drop(self):
492 """
493 Private slot to delete the selected stash.
494 """
495 stashName = self.stashList.selectedItems()[0].text(self.NameColumn)
496 res = self.vcs.gitStashDrop(self.__projectDir, stashName)
497 if res:
498 self.on_refreshButton_clicked()
499
500 def __clear(self):
501 """
502 Private slot to delete all stashes.
503 """
504 res = self.vcs.gitStashClear(self.__projectDir)
505 if res:
506 self.on_refreshButton_clicked()

eric ide

mercurial