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

eric ide

mercurial