Plugins/VcsPlugins/vcsGit/GitReflogBrowserDialog.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 browse the reflog history.
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, QProcess, QTimer, QPoint
19 from PyQt5.QtGui import QCursor
20 from PyQt5.QtWidgets import QWidget, QDialogButtonBox, QHeaderView, \
21 QTreeWidgetItem, QApplication, QLineEdit
22
23 from E5Gui import E5MessageBox
24
25 from .Ui_GitReflogBrowserDialog import Ui_GitReflogBrowserDialog
26
27 from .GitUtilities import strToQByteArray
28
29 import Preferences
30
31
32 class GitReflogBrowserDialog(QWidget, Ui_GitReflogBrowserDialog):
33 """
34 Class implementing a dialog to browse the reflog history.
35 """
36 CommitIdColumn = 0
37 SelectorColumn = 1
38 NameColumn = 2
39 OperationColumn = 3
40 SubjectColumn = 4
41
42 def __init__(self, vcs, parent=None):
43 """
44 Constructor
45
46 @param vcs reference to the vcs object
47 @param parent reference to the parent widget (QWidget)
48 """
49 super(GitReflogBrowserDialog, self).__init__(parent)
50 self.setupUi(self)
51
52 self.__position = QPoint()
53
54 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
55 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
56
57 self.logTree.headerItem().setText(self.logTree.columnCount(), "")
58
59 self.refreshButton = self.buttonBox.addButton(
60 self.tr("&Refresh"), QDialogButtonBox.ActionRole)
61 self.refreshButton.setToolTip(
62 self.tr("Press to refresh the list of commits"))
63 self.refreshButton.setEnabled(False)
64
65 self.vcs = vcs
66
67 self.__formatTemplate = (
68 'format:recordstart%n'
69 'commit|%h%n'
70 'selector|%gd%n'
71 'name|%gn%n'
72 'subject|%gs%n'
73 'recordend%n'
74 )
75
76 self.repodir = ""
77 self.__currentCommitId = ""
78
79 self.__initData()
80 self.__resetUI()
81
82 self.process = QProcess()
83 self.process.finished.connect(self.__procFinished)
84 self.process.readyReadStandardOutput.connect(self.__readStdout)
85 self.process.readyReadStandardError.connect(self.__readStderr)
86
87 def __initData(self):
88 """
89 Private method to (re-)initialize some data.
90 """
91 self.buf = [] # buffer for stdout
92 self.__started = False
93 self.__skipEntries = 0
94
95 def closeEvent(self, e):
96 """
97 Protected slot implementing a close event handler.
98
99 @param e close event (QCloseEvent)
100 """
101 if self.process is not None and \
102 self.process.state() != QProcess.NotRunning:
103 self.process.terminate()
104 QTimer.singleShot(2000, self.process.kill)
105 self.process.waitForFinished(3000)
106
107 self.__position = self.pos()
108
109 e.accept()
110
111 def show(self):
112 """
113 Public slot to show the dialog.
114 """
115 if not self.__position.isNull():
116 self.move(self.__position)
117 self.__resetUI()
118
119 super(GitReflogBrowserDialog, self).show()
120
121 def __resetUI(self):
122 """
123 Private method to reset the user interface.
124 """
125 self.limitSpinBox.setValue(self.vcs.getPlugin().getPreferences(
126 "LogLimit"))
127
128 self.logTree.clear()
129
130 def __resizeColumnsLog(self):
131 """
132 Private method to resize the log tree columns.
133 """
134 self.logTree.header().resizeSections(QHeaderView.ResizeToContents)
135 self.logTree.header().setStretchLastSection(True)
136
137 def __generateReflogItem(self, commitId, selector, name, subject):
138 """
139 Private method to generate a reflog tree entry.
140
141 @param commitId commit id info (string)
142 @param selector selector info (string)
143 @param name name info (string)
144 @param subject subject of the reflog entry (string)
145 @return reference to the generated item (QTreeWidgetItem)
146 """
147 operation, subject = subject.strip().split(": ", 1)
148 columnLabels = [
149 commitId,
150 selector,
151 name,
152 operation,
153 subject,
154 ]
155 itm = QTreeWidgetItem(self.logTree, columnLabels)
156 return itm
157
158 def __getReflogEntries(self, skip=0):
159 """
160 Private method to retrieve reflog entries from the repository.
161
162 @param skip number of reflog entries to skip (integer)
163 """
164 self.refreshButton.setEnabled(False)
165 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
166 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
167 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
168 QApplication.processEvents()
169
170 QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
171 QApplication.processEvents()
172
173 self.buf = []
174 self.cancelled = False
175 self.errors.clear()
176 self.intercept = False
177
178 args = self.vcs.initCommand("log")
179 args.append("--walk-reflogs")
180 args.append('--max-count={0}'.format(self.limitSpinBox.value()))
181 args.append('--abbrev={0}'.format(
182 self.vcs.getPlugin().getPreferences("CommitIdLength")))
183 args.append('--format={0}'.format(self.__formatTemplate))
184 args.append('--skip={0}'.format(skip))
185
186 self.process.kill()
187
188 self.process.setWorkingDirectory(self.repodir)
189
190 self.inputGroup.setEnabled(True)
191 self.inputGroup.show()
192
193 self.process.start('git', args)
194 procStarted = self.process.waitForStarted(5000)
195 if not procStarted:
196 self.inputGroup.setEnabled(False)
197 self.inputGroup.hide()
198 E5MessageBox.critical(
199 self,
200 self.tr('Process Generation Error'),
201 self.tr(
202 'The process {0} could not be started. '
203 'Ensure, that it is in the search path.'
204 ).format('git'))
205
206 def start(self, projectdir):
207 """
208 Public slot to start the git log command.
209
210 @param projectdir directory name of the project (string)
211 """
212 self.errorGroup.hide()
213 QApplication.processEvents()
214
215 self.__initData()
216
217 # find the root of the repo
218 self.repodir = projectdir
219 while not os.path.isdir(os.path.join(self.repodir, self.vcs.adminDir)):
220 self.repodir = os.path.dirname(self.repodir)
221 if os.path.splitdrive(self.repodir)[1] == os.sep:
222 return
223
224 self.activateWindow()
225 self.raise_()
226
227 self.logTree.clear()
228 self.__started = True
229 self.__getReflogEntries()
230
231 def __procFinished(self, exitCode, exitStatus):
232 """
233 Private slot connected to the finished signal.
234
235 @param exitCode exit code of the process (integer)
236 @param exitStatus exit status of the process (QProcess.ExitStatus)
237 """
238 self.__processBuffer()
239 self.__finish()
240
241 def __finish(self):
242 """
243 Private slot called when the process finished or the user pressed
244 the button.
245 """
246 if self.process is not None and \
247 self.process.state() != QProcess.NotRunning:
248 self.process.terminate()
249 QTimer.singleShot(2000, self.process.kill)
250 self.process.waitForFinished(3000)
251
252 QApplication.restoreOverrideCursor()
253
254 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
255 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
256 self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
257
258 self.inputGroup.setEnabled(False)
259 self.inputGroup.hide()
260 self.refreshButton.setEnabled(True)
261
262 def __processBuffer(self):
263 """
264 Private method to process the buffered output of the git log command.
265 """
266 noEntries = 0
267
268 for line in self.buf:
269 line = line.rstrip()
270 if line == "recordstart":
271 logEntry = {}
272 elif line == "recordend":
273 if len(logEntry) > 1:
274 self.__generateReflogItem(
275 logEntry["commit"], logEntry["selector"],
276 logEntry["name"], logEntry["subject"],
277 )
278 noEntries += 1
279 else:
280 try:
281 key, value = line.split("|", 1)
282 except ValueError:
283 key = ""
284 value = line
285 if key in ("commit", "selector", "name", "subject"):
286 logEntry[key] = value.strip()
287
288 self.__resizeColumnsLog()
289
290 if self.__started:
291 self.logTree.setCurrentItem(self.logTree.topLevelItem(0))
292 self.__started = False
293
294 self.__skipEntries += noEntries
295 if noEntries < self.limitSpinBox.value() and not self.cancelled:
296 self.nextButton.setEnabled(False)
297 self.limitSpinBox.setEnabled(False)
298 else:
299 self.nextButton.setEnabled(True)
300 self.limitSpinBox.setEnabled(True)
301
302 # restore current item
303 if self.__currentCommitId:
304 items = self.logTree.findItems(
305 self.__currentCommitId, Qt.MatchExactly, self.CommitIdColumn)
306 if items:
307 self.logTree.setCurrentItem(items[0])
308 self.__currentCommitId = ""
309
310 def __readStdout(self):
311 """
312 Private slot to handle the readyReadStandardOutput signal.
313
314 It reads the output of the process and inserts it into a buffer.
315 """
316 self.process.setReadChannel(QProcess.StandardOutput)
317
318 while self.process.canReadLine():
319 line = str(self.process.readLine(),
320 Preferences.getSystem("IOEncoding"),
321 'replace')
322 self.buf.append(line)
323
324 def __readStderr(self):
325 """
326 Private slot to handle the readyReadStandardError signal.
327
328 It reads the error output of the process and inserts it into the
329 error pane.
330 """
331 if self.process is not None:
332 s = str(self.process.readAllStandardError(),
333 Preferences.getSystem("IOEncoding"),
334 'replace')
335 self.__showError(s)
336
337 def __showError(self, out):
338 """
339 Private slot to show some error.
340
341 @param out error to be shown (string)
342 """
343 self.errorGroup.show()
344 self.errors.insertPlainText(out)
345 self.errors.ensureCursorVisible()
346
347 def on_buttonBox_clicked(self, button):
348 """
349 Private slot called by a button of the button box clicked.
350
351 @param button button that was clicked (QAbstractButton)
352 """
353 if button == self.buttonBox.button(QDialogButtonBox.Close):
354 self.close()
355 elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
356 self.cancelled = True
357 self.__finish()
358 elif button == self.refreshButton:
359 self.on_refreshButton_clicked()
360
361 @pyqtSlot()
362 def on_refreshButton_clicked(self):
363 """
364 Private slot to refresh the log.
365 """
366 # save the current item's commit ID
367 itm = self.logTree.currentItem()
368 if itm is not None:
369 self.__currentCommitId = itm.text(self.CommitIdColumn)
370 else:
371 self.__currentCommitId = ""
372
373 self.start(self.repodir)
374
375 def on_passwordCheckBox_toggled(self, isOn):
376 """
377 Private slot to handle the password checkbox toggled.
378
379 @param isOn flag indicating the status of the check box (boolean)
380 """
381 if isOn:
382 self.input.setEchoMode(QLineEdit.Password)
383 else:
384 self.input.setEchoMode(QLineEdit.Normal)
385
386 @pyqtSlot()
387 def on_sendButton_clicked(self):
388 """
389 Private slot to send the input to the git process.
390 """
391 inputTxt = self.input.text()
392 inputTxt += os.linesep
393
394 if self.passwordCheckBox.isChecked():
395 self.errors.insertPlainText(os.linesep)
396 self.errors.ensureCursorVisible()
397 else:
398 self.errors.insertPlainText(inputTxt)
399 self.errors.ensureCursorVisible()
400 self.errorGroup.show()
401
402 self.process.write(strToQByteArray(inputTxt))
403
404 self.passwordCheckBox.setChecked(False)
405 self.input.clear()
406
407 def on_input_returnPressed(self):
408 """
409 Private slot to handle the press of the return key in the input field.
410 """
411 self.intercept = True
412 self.on_sendButton_clicked()
413
414 def keyPressEvent(self, evt):
415 """
416 Protected slot to handle a key press event.
417
418 @param evt the key press event (QKeyEvent)
419 """
420 if self.intercept:
421 self.intercept = False
422 evt.accept()
423 return
424 super(GitReflogBrowserDialog, self).keyPressEvent(evt)
425
426 @pyqtSlot()
427 def on_nextButton_clicked(self):
428 """
429 Private slot to handle the Next button.
430 """
431 if self.__skipEntries > 0:
432 self.__getReflogEntries(self.__skipEntries)

eric ide

mercurial