1# -*- coding: utf-8 -*- 2 3# Copyright (c) 2014 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> 4# 5 6""" 7Module implementing a dialog to show available remote repositories. 8""" 9 10import os 11 12from PyQt5.QtCore import pyqtSlot, Qt, QProcess, QTimer 13from PyQt5.QtWidgets import ( 14 QWidget, QHeaderView, QTreeWidgetItem, QDialogButtonBox, QLineEdit 15) 16 17from E5Gui import E5MessageBox 18 19from .Ui_GitRemoteRepositoriesDialog import Ui_GitRemoteRepositoriesDialog 20 21import Preferences 22from Globals import strToQByteArray 23 24 25class GitRemoteRepositoriesDialog(QWidget, Ui_GitRemoteRepositoriesDialog): 26 """ 27 Class implementing a dialog to show available remote repositories. 28 """ 29 def __init__(self, vcs, parent=None): 30 """ 31 Constructor 32 33 @param vcs reference to the vcs object 34 @param parent parent widget (QWidget) 35 """ 36 super().__init__(parent) 37 self.setupUi(self) 38 39 self.vcs = vcs 40 self.process = QProcess() 41 self.process.finished.connect(self.__procFinished) 42 self.process.readyReadStandardOutput.connect(self.__readStdout) 43 self.process.readyReadStandardError.connect(self.__readStderr) 44 45 self.refreshButton = self.buttonBox.addButton( 46 self.tr("Refresh"), QDialogButtonBox.ButtonRole.ActionRole) 47 self.refreshButton.setToolTip( 48 self.tr("Press to refresh the repositories display")) 49 self.refreshButton.setEnabled(False) 50 self.buttonBox.button( 51 QDialogButtonBox.StandardButton.Close).setEnabled(False) 52 self.buttonBox.button( 53 QDialogButtonBox.StandardButton.Cancel).setDefault(True) 54 55 self.__lastColumn = self.repolist.columnCount() 56 57 self.repolist.headerItem().setText(self.__lastColumn, "") 58 self.repolist.header().setSortIndicator(0, Qt.SortOrder.AscendingOrder) 59 60 self.__ioEncoding = Preferences.getSystem("IOEncoding") 61 62 def __resort(self): 63 """ 64 Private method to resort the list. 65 """ 66 self.repolist.sortItems( 67 self.repolist.sortColumn(), 68 self.repolist.header().sortIndicatorOrder()) 69 70 def __resizeColumns(self): 71 """ 72 Private method to resize the list columns. 73 """ 74 self.repolist.header().resizeSections( 75 QHeaderView.ResizeMode.ResizeToContents) 76 self.repolist.header().setStretchLastSection(True) 77 78 def __generateItem(self, name, url, oper): 79 """ 80 Private method to generate a status item in the status list. 81 82 @param name name of the remote repository (string) 83 @param url URL of the remote repository (string) 84 @param oper operation the remote repository may be used for (string) 85 """ 86 foundItems = self.repolist.findItems( 87 name, Qt.MatchFlag.MatchExactly, 0) 88 if foundItems: 89 # modify the operations column only 90 foundItems[0].setText( 91 2, "{0} + {1}".format(foundItems[0].text(2), oper)) 92 else: 93 QTreeWidgetItem(self.repolist, [name, url, oper]) 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 ( 102 self.process is not None and 103 self.process.state() != QProcess.ProcessState.NotRunning 104 ): 105 self.process.terminate() 106 QTimer.singleShot(2000, self.process.kill) 107 self.process.waitForFinished(3000) 108 109 e.accept() 110 111 def start(self, projectDir): 112 """ 113 Public slot to start the git remote command. 114 115 @param projectDir name of the project directory (string) 116 """ 117 self.repolist.clear() 118 119 self.errorGroup.hide() 120 self.intercept = False 121 self.projectDir = projectDir 122 123 self.__ioEncoding = Preferences.getSystem("IOEncoding") 124 125 self.removeButton.setEnabled(False) 126 self.renameButton.setEnabled(False) 127 self.pruneButton.setEnabled(False) 128 self.showInfoButton.setEnabled(False) 129 130 args = self.vcs.initCommand("remote") 131 args.append('--verbose') 132 133 # find the root of the repo 134 repodir = self.projectDir 135 while not os.path.isdir(os.path.join(repodir, self.vcs.adminDir)): 136 repodir = os.path.dirname(repodir) 137 if os.path.splitdrive(repodir)[1] == os.sep: 138 return 139 140 self.process.kill() 141 self.process.setWorkingDirectory(repodir) 142 143 self.process.start('git', args) 144 procStarted = self.process.waitForStarted(5000) 145 if not procStarted: 146 self.inputGroup.setEnabled(False) 147 self.inputGroup.hide() 148 E5MessageBox.critical( 149 self, 150 self.tr('Process Generation Error'), 151 self.tr( 152 'The process {0} could not be started. ' 153 'Ensure, that it is in the search path.' 154 ).format('git')) 155 else: 156 self.buttonBox.button( 157 QDialogButtonBox.StandardButton.Close).setEnabled(False) 158 self.buttonBox.button( 159 QDialogButtonBox.StandardButton.Cancel).setEnabled(True) 160 self.buttonBox.button( 161 QDialogButtonBox.StandardButton.Cancel).setDefault(True) 162 163 self.inputGroup.setEnabled(True) 164 self.inputGroup.show() 165 self.refreshButton.setEnabled(False) 166 167 def __finish(self): 168 """ 169 Private slot called when the process finished or the user pressed 170 the button. 171 """ 172 if ( 173 self.process is not None and 174 self.process.state() != QProcess.ProcessState.NotRunning 175 ): 176 self.process.terminate() 177 QTimer.singleShot(2000, self.process.kill) 178 self.process.waitForFinished(3000) 179 180 self.inputGroup.setEnabled(False) 181 self.inputGroup.hide() 182 self.refreshButton.setEnabled(True) 183 184 self.buttonBox.button( 185 QDialogButtonBox.StandardButton.Close).setEnabled(True) 186 self.buttonBox.button( 187 QDialogButtonBox.StandardButton.Cancel).setEnabled(False) 188 self.buttonBox.button( 189 QDialogButtonBox.StandardButton.Close).setDefault(True) 190 self.buttonBox.button( 191 QDialogButtonBox.StandardButton.Close).setFocus( 192 Qt.FocusReason.OtherFocusReason) 193 194 self.__resort() 195 self.__resizeColumns() 196 197 self.__updateButtons() 198 199 def on_buttonBox_clicked(self, button): 200 """ 201 Private slot called by a button of the button box clicked. 202 203 @param button button that was clicked (QAbstractButton) 204 """ 205 if button == self.buttonBox.button( 206 QDialogButtonBox.StandardButton.Close 207 ): 208 self.close() 209 elif button == self.buttonBox.button( 210 QDialogButtonBox.StandardButton.Cancel 211 ): 212 self.__finish() 213 elif button == self.refreshButton: 214 self.on_refreshButton_clicked() 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.__finish() 224 225 def __readStdout(self): 226 """ 227 Private slot to handle the readyReadStandardOutput signal. 228 229 It reads the output of the process, formats it and inserts it into 230 the contents pane. 231 """ 232 if self.process is not None: 233 self.process.setReadChannel(QProcess.ProcessChannel.StandardOutput) 234 235 while self.process.canReadLine(): 236 line = str(self.process.readLine(), self.__ioEncoding, 237 'replace').strip() 238 239 name, line = line.split(None, 1) 240 url, oper = line.rsplit(None, 1) 241 oper = oper[1:-1] # it is enclosed in () 242 self.__generateItem(name, url, oper) 243 244 def __readStderr(self): 245 """ 246 Private slot to handle the readyReadStandardError signal. 247 248 It reads the error output of the process and inserts it into the 249 error pane. 250 """ 251 if self.process is not None: 252 s = str(self.process.readAllStandardError(), 253 self.__ioEncoding, 'replace') 254 self.errorGroup.show() 255 self.errors.insertPlainText(s) 256 self.errors.ensureCursorVisible() 257 258 def on_passwordCheckBox_toggled(self, isOn): 259 """ 260 Private slot to handle the password checkbox toggled. 261 262 @param isOn flag indicating the status of the check box (boolean) 263 """ 264 if isOn: 265 self.input.setEchoMode(QLineEdit.EchoMode.Password) 266 else: 267 self.input.setEchoMode(QLineEdit.EchoMode.Normal) 268 269 @pyqtSlot() 270 def on_sendButton_clicked(self): 271 """ 272 Private slot to send the input to the git process. 273 """ 274 inputTxt = self.input.text() 275 inputTxt += os.linesep 276 277 if self.passwordCheckBox.isChecked(): 278 self.errors.insertPlainText(os.linesep) 279 self.errors.ensureCursorVisible() 280 else: 281 self.errors.insertPlainText(inputTxt) 282 self.errors.ensureCursorVisible() 283 284 self.process.write(strToQByteArray(inputTxt)) 285 286 self.passwordCheckBox.setChecked(False) 287 self.input.clear() 288 289 def on_input_returnPressed(self): 290 """ 291 Private slot to handle the press of the return key in the input field. 292 """ 293 self.intercept = True 294 self.on_sendButton_clicked() 295 296 def keyPressEvent(self, evt): 297 """ 298 Protected slot to handle a key press event. 299 300 @param evt the key press event (QKeyEvent) 301 """ 302 if self.intercept: 303 self.intercept = False 304 evt.accept() 305 return 306 super().keyPressEvent(evt) 307 308 @pyqtSlot() 309 def on_refreshButton_clicked(self): 310 """ 311 Private slot to refresh the status display. 312 """ 313 self.start(self.projectDir) 314 315 def __updateButtons(self): 316 """ 317 Private method to update the buttons status. 318 """ 319 enable = len(self.repolist.selectedItems()) == 1 320 321 self.removeButton.setEnabled(enable) 322 self.pruneButton.setEnabled(enable) 323 self.showInfoButton.setEnabled(enable) 324 self.renameButton.setEnabled(enable) 325 self.changeUrlButton.setEnabled(enable) 326 self.credentialsButton.setEnabled(enable) 327 328 @pyqtSlot() 329 def on_repolist_itemSelectionChanged(self): 330 """ 331 Private slot to act upon changes of selected items. 332 """ 333 self.__updateButtons() 334 335 @pyqtSlot() 336 def on_addButton_clicked(self): 337 """ 338 Private slot to add a remote repository. 339 """ 340 self.vcs.gitAddRemote(self.projectDir) 341 self.on_refreshButton_clicked() 342 343 @pyqtSlot() 344 def on_removeButton_clicked(self): 345 """ 346 Private slot to remove a remote repository. 347 """ 348 remoteName = self.repolist.selectedItems()[0].text(0) 349 self.vcs.gitRemoveRemote(self.projectDir, remoteName) 350 self.on_refreshButton_clicked() 351 352 @pyqtSlot() 353 def on_showInfoButton_clicked(self): 354 """ 355 Private slot to show information about a remote repository. 356 """ 357 remoteName = self.repolist.selectedItems()[0].text(0) 358 self.vcs.gitShowRemote(self.projectDir, remoteName) 359 360 @pyqtSlot() 361 def on_pruneButton_clicked(self): 362 """ 363 Private slot to prune all stale remote-tracking branches. 364 """ 365 remoteName = self.repolist.selectedItems()[0].text(0) 366 self.vcs.gitPruneRemote(self.projectDir, remoteName) 367 368 @pyqtSlot() 369 def on_renameButton_clicked(self): 370 """ 371 Private slot to rename a remote repository. 372 """ 373 remoteName = self.repolist.selectedItems()[0].text(0) 374 self.vcs.gitRenameRemote(self.projectDir, remoteName) 375 self.on_refreshButton_clicked() 376 377 @pyqtSlot() 378 def on_changeUrlButton_clicked(self): 379 """ 380 Private slot to change the URL of a remote repository. 381 """ 382 repositoryItem = self.repolist.selectedItems()[0] 383 remoteName = repositoryItem.text(0) 384 remoteUrl = repositoryItem.text(1) 385 self.vcs.gitChangeRemoteUrl(self.projectDir, remoteName, remoteUrl) 386 self.on_refreshButton_clicked() 387 388 @pyqtSlot() 389 def on_credentialsButton_clicked(self): 390 """ 391 Private slot to change the credentials of a remote repository. 392 """ 393 repositoryItem = self.repolist.selectedItems()[0] 394 remoteName = repositoryItem.text(0) 395 remoteUrl = repositoryItem.text(1) 396 self.vcs.gitChangeRemoteCredentials(self.projectDir, remoteName, 397 remoteUrl) 398 self.on_refreshButton_clicked() 399