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