eric7/Plugins/VcsPlugins/vcsGit/GitReflogBrowserDialog.py

branch
eric7
changeset 8312
800c432b34c8
parent 8218
7c09585bd960
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 browse the reflog history.
8 """
9
10 import os
11
12 from PyQt5.QtCore import pyqtSlot, Qt, QProcess, QTimer, QPoint
13 from PyQt5.QtWidgets import (
14 QWidget, QDialogButtonBox, QHeaderView, QTreeWidgetItem, QApplication,
15 QLineEdit
16 )
17
18 from E5Gui import E5MessageBox
19 from E5Gui.E5OverrideCursor import E5OverrideCursorProcess
20
21 from .Ui_GitReflogBrowserDialog import Ui_GitReflogBrowserDialog
22
23 import Preferences
24 from Globals import strToQByteArray
25
26
27 class GitReflogBrowserDialog(QWidget, Ui_GitReflogBrowserDialog):
28 """
29 Class implementing a dialog to browse the reflog history.
30 """
31 CommitIdColumn = 0
32 SelectorColumn = 1
33 NameColumn = 2
34 OperationColumn = 3
35 SubjectColumn = 4
36
37 def __init__(self, vcs, parent=None):
38 """
39 Constructor
40
41 @param vcs reference to the vcs object
42 @param parent reference to the parent widget (QWidget)
43 """
44 super().__init__(parent)
45 self.setupUi(self)
46
47 self.__position = QPoint()
48
49 self.buttonBox.button(
50 QDialogButtonBox.StandardButton.Close).setEnabled(False)
51 self.buttonBox.button(
52 QDialogButtonBox.StandardButton.Cancel).setDefault(True)
53
54 self.logTree.headerItem().setText(self.logTree.columnCount(), "")
55
56 self.refreshButton = self.buttonBox.addButton(
57 self.tr("&Refresh"), QDialogButtonBox.ButtonRole.ActionRole)
58 self.refreshButton.setToolTip(
59 self.tr("Press to refresh the list of commits"))
60 self.refreshButton.setEnabled(False)
61
62 self.vcs = vcs
63
64 self.__formatTemplate = (
65 'format:recordstart%n'
66 'commit|%h%n'
67 'selector|%gd%n'
68 'name|%gn%n'
69 'subject|%gs%n'
70 'recordend%n'
71 )
72
73 self.repodir = ""
74 self.__currentCommitId = ""
75
76 self.__initData()
77 self.__resetUI()
78
79 self.__process = E5OverrideCursorProcess()
80 self.__process.finished.connect(self.__procFinished)
81 self.__process.readyReadStandardOutput.connect(self.__readStdout)
82 self.__process.readyReadStandardError.connect(self.__readStderr)
83
84 def __initData(self):
85 """
86 Private method to (re-)initialize some data.
87 """
88 self.buf = [] # buffer for stdout
89 self.__started = False
90 self.__skipEntries = 0
91
92 def closeEvent(self, e):
93 """
94 Protected slot implementing a close event handler.
95
96 @param e close event (QCloseEvent)
97 """
98 if (
99 self.__process is not None and
100 self.__process.state() != QProcess.ProcessState.NotRunning
101 ):
102 self.__process.terminate()
103 QTimer.singleShot(2000, self.__process.kill)
104 self.__process.waitForFinished(3000)
105
106 self.__position = self.pos()
107
108 e.accept()
109
110 def show(self):
111 """
112 Public slot to show the dialog.
113 """
114 if not self.__position.isNull():
115 self.move(self.__position)
116 self.__resetUI()
117
118 super().show()
119
120 def __resetUI(self):
121 """
122 Private method to reset the user interface.
123 """
124 self.limitSpinBox.setValue(self.vcs.getPlugin().getPreferences(
125 "LogLimit"))
126
127 self.logTree.clear()
128
129 def __resizeColumnsLog(self):
130 """
131 Private method to resize the log tree columns.
132 """
133 self.logTree.header().resizeSections(
134 QHeaderView.ResizeMode.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(
166 QDialogButtonBox.StandardButton.Close).setEnabled(False)
167 self.buttonBox.button(
168 QDialogButtonBox.StandardButton.Cancel).setEnabled(True)
169 self.buttonBox.button(
170 QDialogButtonBox.StandardButton.Cancel).setDefault(True)
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 (
247 self.__process is not None and
248 self.__process.state() != QProcess.ProcessState.NotRunning
249 ):
250 self.__process.terminate()
251 QTimer.singleShot(2000, self.__process.kill)
252 self.__process.waitForFinished(3000)
253
254 self.buttonBox.button(
255 QDialogButtonBox.StandardButton.Close).setEnabled(True)
256 self.buttonBox.button(
257 QDialogButtonBox.StandardButton.Cancel).setEnabled(False)
258 self.buttonBox.button(
259 QDialogButtonBox.StandardButton.Close).setDefault(True)
260
261 self.inputGroup.setEnabled(False)
262 self.inputGroup.hide()
263 self.refreshButton.setEnabled(True)
264
265 def __processBuffer(self):
266 """
267 Private method to process the buffered output of the git log command.
268 """
269 noEntries = 0
270
271 for line in self.buf:
272 line = line.rstrip()
273 if line == "recordstart":
274 logEntry = {}
275 elif line == "recordend":
276 if len(logEntry) > 1:
277 self.__generateReflogItem(
278 logEntry["commit"], logEntry["selector"],
279 logEntry["name"], logEntry["subject"],
280 )
281 noEntries += 1
282 else:
283 try:
284 key, value = line.split("|", 1)
285 except ValueError:
286 key = ""
287 value = line
288 if key in ("commit", "selector", "name", "subject"):
289 logEntry[key] = value.strip()
290
291 self.__resizeColumnsLog()
292
293 if self.__started:
294 self.logTree.setCurrentItem(self.logTree.topLevelItem(0))
295 self.__started = False
296
297 self.__skipEntries += noEntries
298 if noEntries < self.limitSpinBox.value() and not self.cancelled:
299 self.nextButton.setEnabled(False)
300 self.limitSpinBox.setEnabled(False)
301 else:
302 self.nextButton.setEnabled(True)
303 self.limitSpinBox.setEnabled(True)
304
305 # restore current item
306 if self.__currentCommitId:
307 items = self.logTree.findItems(
308 self.__currentCommitId, Qt.MatchFlag.MatchExactly,
309 self.CommitIdColumn)
310 if items:
311 self.logTree.setCurrentItem(items[0])
312 self.__currentCommitId = ""
313
314 def __readStdout(self):
315 """
316 Private slot to handle the readyReadStandardOutput signal.
317
318 It reads the output of the process and inserts it into a buffer.
319 """
320 self.__process.setReadChannel(QProcess.ProcessChannel.StandardOutput)
321
322 while self.__process.canReadLine():
323 line = str(self.__process.readLine(),
324 Preferences.getSystem("IOEncoding"),
325 'replace')
326 self.buf.append(line)
327
328 def __readStderr(self):
329 """
330 Private slot to handle the readyReadStandardError signal.
331
332 It reads the error output of the process and inserts it into the
333 error pane.
334 """
335 if self.__process is not None:
336 s = str(self.__process.readAllStandardError(),
337 Preferences.getSystem("IOEncoding"),
338 'replace')
339 self.__showError(s)
340
341 def __showError(self, out):
342 """
343 Private slot to show some error.
344
345 @param out error to be shown (string)
346 """
347 self.errorGroup.show()
348 self.errors.insertPlainText(out)
349 self.errors.ensureCursorVisible()
350
351 def on_buttonBox_clicked(self, button):
352 """
353 Private slot called by a button of the button box clicked.
354
355 @param button button that was clicked (QAbstractButton)
356 """
357 if button == self.buttonBox.button(
358 QDialogButtonBox.StandardButton.Close
359 ):
360 self.close()
361 elif button == self.buttonBox.button(
362 QDialogButtonBox.StandardButton.Cancel
363 ):
364 self.cancelled = True
365 self.__finish()
366 elif button == self.refreshButton:
367 self.on_refreshButton_clicked()
368
369 @pyqtSlot()
370 def on_refreshButton_clicked(self):
371 """
372 Private slot to refresh the log.
373 """
374 # save the current item's commit ID
375 itm = self.logTree.currentItem()
376 if itm is not None:
377 self.__currentCommitId = itm.text(self.CommitIdColumn)
378 else:
379 self.__currentCommitId = ""
380
381 self.start(self.repodir)
382
383 def on_passwordCheckBox_toggled(self, isOn):
384 """
385 Private slot to handle the password checkbox toggled.
386
387 @param isOn flag indicating the status of the check box (boolean)
388 """
389 if isOn:
390 self.input.setEchoMode(QLineEdit.EchoMode.Password)
391 else:
392 self.input.setEchoMode(QLineEdit.EchoMode.Normal)
393
394 @pyqtSlot()
395 def on_sendButton_clicked(self):
396 """
397 Private slot to send the input to the git process.
398 """
399 inputTxt = self.input.text()
400 inputTxt += os.linesep
401
402 if self.passwordCheckBox.isChecked():
403 self.errors.insertPlainText(os.linesep)
404 self.errors.ensureCursorVisible()
405 else:
406 self.errors.insertPlainText(inputTxt)
407 self.errors.ensureCursorVisible()
408 self.errorGroup.show()
409
410 self.__process.write(strToQByteArray(inputTxt))
411
412 self.passwordCheckBox.setChecked(False)
413 self.input.clear()
414
415 def on_input_returnPressed(self):
416 """
417 Private slot to handle the press of the return key in the input field.
418 """
419 self.intercept = True
420 self.on_sendButton_clicked()
421
422 def keyPressEvent(self, evt):
423 """
424 Protected slot to handle a key press event.
425
426 @param evt the key press event (QKeyEvent)
427 """
428 if self.intercept:
429 self.intercept = False
430 evt.accept()
431 return
432 super().keyPressEvent(evt)
433
434 @pyqtSlot()
435 def on_nextButton_clicked(self):
436 """
437 Private slot to handle the Next button.
438 """
439 if self.__skipEntries > 0:
440 self.__getReflogEntries(self.__skipEntries)

eric ide

mercurial