1# -*- coding: utf-8 -*- 2 3# Copyright (c) 2014 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> 4# 5 6""" 7Module implementing a dialog to browse the reflog history. 8""" 9 10import os 11 12from PyQt5.QtCore import pyqtSlot, Qt, QProcess, QTimer, QPoint 13from PyQt5.QtWidgets import ( 14 QWidget, QDialogButtonBox, QHeaderView, QTreeWidgetItem, QApplication, 15 QLineEdit 16) 17 18from E5Gui import E5MessageBox 19from E5Gui.E5OverrideCursor import E5OverrideCursorProcess 20 21from .Ui_GitReflogBrowserDialog import Ui_GitReflogBrowserDialog 22 23import Preferences 24from Globals import strToQByteArray 25 26 27class 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) 441