Beispiel #1
0
    def write(self, file_name, storage_device, mesh_data):
        Logger.log("i", "In X3gWriter.write")
        if "x3g" in file_name:
            scene = Application.getInstance().getController().getScene()
            gcode_list = getattr(scene, "gcode_list")
            if gcode_list:
                # f = storage_device.openFile(file_name, "wt")
                Logger.log("i", "Writing X3g to file %s", file_name)
                p = QProcess()
                p.setReadChannel(1)
                p.setStandardOutputFile(file_name)
                p.start("gpx", ["-i"])
                p.waitForStarted()

                for gcode in gcode_list:
                    p.write(gcode)
                    if p.canReadLine():
                        Logger.log("i", "gpx: %s", p.readLine().data().decode("utf-8"))

                p.closeWriteChannel()
                if p.waitForFinished():
                    Logger.log("i", "gpx: %s", p.readAll().data().decode("utf-8"))
                p.close()
                # storage_device.closeFile(f)
                return True

        return False
Beispiel #2
0
    def write(self, file_name, storage_device, mesh_data):
        Logger.log("i", "In X3gWriter.write")
        if "x3g" in file_name:
            scene = Application.getInstance().getController().getScene()
            gcode_list = getattr(scene, "gcode_list")
            if gcode_list:
                # f = storage_device.openFile(file_name, "wt")
                Logger.log("i", "Writing X3g to file %s", file_name)
                p = QProcess()
                p.setReadChannel(1)
                p.setStandardOutputFile(file_name)
                p.start("gpx", ["-i"])
                p.waitForStarted()

                for gcode in gcode_list:
                    p.write(gcode)
                    if (p.canReadLine()):
                        Logger.log("i", "gpx: %s", p.readLine().data().decode('utf-8'))

                p.closeWriteChannel()
                if p.waitForFinished():
                    Logger.log("i", "gpx: %s", p.readAll().data().decode('utf-8'))
                p.close()
                # storage_device.closeFile(f)
                return True

        return False
class LinuxRecorder(QWidget):

    keystroke = pyqtSignal(object)
    stopped = pyqtSignal()

    def __init__(self):
        super().__init__()

        self.process = QProcess()
        self.process.readyReadStandardOutput.connect(self.on_output)

        self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint
                            | QtCore.Qt.X11BypassWindowManagerHint)

        layout = QVBoxLayout()
        btn = QPushButton(tr("MacroRecorder", "Stop recording"))
        btn.clicked.connect(self.on_stop)
        layout.addWidget(btn)

        self.setLayout(layout)

    def start(self):
        self.show()

        center = QApplication.desktop().availableGeometry(self).center()
        self.move(center.x() - self.width() * 0.5, 0)

        args = [sys.executable]
        if os.getenv("APPIMAGE"):
            args = [os.getenv("APPIMAGE")]
        elif is_frozen():
            args += sys.argv[1:]
        else:
            args += sys.argv
        args += ["--linux-recorder"]

        self.process.start("pkexec", args,
                           QProcess.Unbuffered | QProcess.ReadWrite)

    def on_stop(self):
        self.stop()

    def stop(self):
        self.process.write(b"q")
        self.process.waitForFinished()
        self.process.close()
        self.hide()
        self.stopped.emit()

    def on_output(self):
        if self.process.canReadLine():
            line = bytes(self.process.readLine()).decode("utf-8")
            action, key = line.strip().split(":")
            code = Keycode.find_by_recorder_alias(key)
            if code is not None:
                action2cls = {"down": KeyDown, "up": KeyUp}
                self.keystroke.emit(action2cls[action](code))
Beispiel #4
0
class OutputPane(QTextEdit):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.process = None
        self.setAcceptRichText(False)
        self.setReadOnly(True)
        self.setLineWrapMode(QTextEdit.NoWrap)
        self.setObjectName('outputpane')

    def append(self, txt):
        tc = self.textCursor()
        tc.movePosition(QTextCursor.End)
        self.setTextCursor(tc)
        self.insertPlainText(txt)
        self.ensureCursorVisible()

    def clear(self):
        self.setText('')

    def get_subprocess_env(self):
        """Get the environment variables for running the subprocess."""
        return {'PYTHONIOENCODING': ENCODING}

    def run(self, *args, cwd=None):
        env = QProcessEnvironment().systemEnvironment()
        for k, v in self.get_subprocess_env().items():
            env.insert(k, v)

        self.process = QProcess(self)
        self.process.setProcessEnvironment(env)
        if cwd:
            self.process.setWorkingDirectory(cwd)
        # self.process.stateChanged.connect(self.stateChanged)
        self.process.readyReadStandardOutput.connect(self.on_stdout_read)
        self.process.readyReadStandardError.connect(self.on_stderr_read)
        self.process.finished.connect(self.on_process_end)

        self.clear()
        self.process.start(args[0], args[1:], QIODevice.ReadWrite)

    def on_stdout_read(self):
        while self.process and self.process.canReadLine():
            text = self.process.readLine().data().decode(ENCODING)
            self.append(text)

    def on_stderr_read(self):
        text = self.process.readAllStandardError().data().decode(ENCODING)
        self.append(text)

    def kill(self):
        if self.process:
            self.process.kill()

    def on_process_end(self):
        self.process = None
class HgLogDialog(QWidget, Ui_HgLogDialog):
    """
    Class implementing a dialog to show the output of the hg log command
    process.
    
    The dialog is nonmodal. Clicking a link in the upper text pane shows
    a diff of the revisions.
    """
    def __init__(self,
                 vcs,
                 mode="log",
                 bundle=None,
                 isFile=False,
                 parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param mode mode of the dialog (string; one of log, incoming, outgoing)
        @param bundle name of a bundle file (string)
        @param isFile flag indicating log for a file is to be shown (boolean)
        @param parent parent widget (QWidget)
        """
        super(HgLogDialog, self).__init__(parent)
        self.setupUi(self)

        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)

        self.process = QProcess()
        self.vcs = vcs
        if mode in ("log", "incoming", "outgoing"):
            self.mode = mode
        else:
            self.mode = "log"
        self.bundle = bundle
        self.__hgClient = self.vcs.getClient()

        self.contents.setHtml(
            self.tr('<b>Processing your request, please wait...</b>'))

        self.process.finished.connect(self.__procFinished)
        self.process.readyReadStandardOutput.connect(self.__readStdout)
        self.process.readyReadStandardError.connect(self.__readStderr)

        self.contents.anchorClicked.connect(self.__sourceChanged)

        self.revisions = []  # stack of remembered revisions
        self.revString = self.tr('Revision')
        self.projectMode = False

        self.logEntries = []  # list of log entries
        self.lastLogEntry = {}
        self.fileCopies = {}
        self.endInitialText = False
        self.initialText = []

        self.diff = None

        self.sbsCheckBox.setEnabled(isFile)
        self.sbsCheckBox.setVisible(isFile)

    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.__hgClient:
            if self.__hgClient.isExecuting():
                self.__hgClient.cancel()
        else:
            if self.process is not None and \
               self.process.state() != QProcess.NotRunning:
                self.process.terminate()
                QTimer.singleShot(2000, self.process.kill)
                self.process.waitForFinished(3000)

        e.accept()

    def start(self, fn, noEntries=0, revisions=None):
        """
        Public slot to start the hg log command.
        
        @param fn filename to show the log for (string)
        @param noEntries number of entries to show (integer)
        @param revisions revisions to show log for (list of strings)
        """
        self.errorGroup.hide()
        QApplication.processEvents()

        self.intercept = False
        self.filename = fn
        self.dname, self.fname = self.vcs.splitPath(fn)

        # find the root of the repo
        self.repodir = self.dname
        while not os.path.isdir(os.path.join(self.repodir, self.vcs.adminDir)):
            self.repodir = os.path.dirname(self.repodir)
            if os.path.splitdrive(self.repodir)[1] == os.sep:
                return

        self.projectMode = (self.fname == "." and self.dname == self.repodir)

        self.activateWindow()
        self.raise_()

        preargs = []
        args = self.vcs.initCommand(self.mode)
        if noEntries and self.mode == "log":
            args.append('--limit')
            args.append(str(noEntries))
        if self.mode in ("incoming", "outgoing"):
            args.append("--newest-first")
            if self.vcs.hasSubrepositories():
                args.append("--subrepos")
        if self.mode == "log":
            args.append('--copies')
        if self.vcs.version >= (3, 0):
            args.append('--template')
            args.append(
                os.path.join(os.path.dirname(__file__), "templates",
                             "logDialogBookmarkPhase.tmpl"))
        else:
            args.append('--style')
            if self.vcs.version >= (2, 1):
                args.append(
                    os.path.join(os.path.dirname(__file__), "styles",
                                 "logDialogBookmarkPhase.style"))
            else:
                args.append(
                    os.path.join(os.path.dirname(__file__), "styles",
                                 "logDialogBookmark.style"))
        if self.mode == "incoming":
            if self.bundle:
                args.append(self.bundle)
            elif not self.vcs.hasSubrepositories():
                project = e5App().getObject("Project")
                self.vcs.bundleFile = os.path.join(
                    project.getProjectManagementDir(), "hg-bundle.hg")
                if os.path.exists(self.vcs.bundleFile):
                    os.remove(self.vcs.bundleFile)
                preargs = args[:]
                preargs.append("--quiet")
                preargs.append('--bundle')
                preargs.append(self.vcs.bundleFile)
                args.append(self.vcs.bundleFile)
        if revisions:
            for rev in revisions:
                args.append("--rev")
                args.append(rev)
        if not self.projectMode:
            args.append(self.filename)

        if self.__hgClient:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()

            if preargs:
                out, err = self.__hgClient.runcommand(preargs)
            else:
                err = ""
            if err:
                self.__showError(err)
            elif self.mode != "incoming" or \
                (self.vcs.bundleFile and
                 os.path.exists(self.vcs.bundleFile)) or \
                    self.bundle:
                out, err = self.__hgClient.runcommand(args)
                if err:
                    self.__showError(err)
                if out and self.isVisible():
                    for line in out.splitlines(True):
                        self.__processOutputLine(line)
                        if self.__hgClient.wasCanceled():
                            break
            self.__finish()
        else:
            self.process.kill()

            self.process.setWorkingDirectory(self.repodir)

            if preargs:
                process = QProcess()
                process.setWorkingDirectory(self.repodir)
                process.start('hg', args)
                procStarted = process.waitForStarted(5000)
                if procStarted:
                    process.waitForFinished(30000)

            if self.mode != "incoming" or \
                (self.vcs.bundleFile and
                 os.path.exists(self.vcs.bundleFile)) or \
                    self.bundle:
                self.process.start('hg', args)
                procStarted = self.process.waitForStarted(5000)
                if not procStarted:
                    self.inputGroup.setEnabled(False)
                    self.inputGroup.hide()
                    E5MessageBox.critical(
                        self, self.tr('Process Generation Error'),
                        self.tr(
                            'The process {0} could not be started. '
                            'Ensure, that it is in the search path.').format(
                                'hg'))
            else:
                self.__finish()

    def __getParents(self, rev):
        """
        Private method to get the parents of the currently viewed
        file/directory.
        
        @param rev revision number to get parents for (string)
        @return list of parent revisions (list of strings)
        """
        errMsg = ""
        parents = []

        if int(rev) > 0:
            args = self.vcs.initCommand("parents")
            if self.mode == "incoming":
                if self.bundle:
                    args.append("--repository")
                    args.append(self.bundle)
                elif self.vcs.bundleFile and \
                        os.path.exists(self.vcs.bundleFile):
                    args.append("--repository")
                    args.append(self.vcs.bundleFile)
            args.append("--template")
            args.append("{rev}:{node|short}\n")
            args.append("-r")
            args.append(rev)
            if not self.projectMode:
                args.append(self.filename)

            output = ""
            if self.__hgClient:
                output, errMsg = self.__hgClient.runcommand(args)
            else:
                process = QProcess()
                process.setWorkingDirectory(self.repodir)
                process.start('hg', args)
                procStarted = process.waitForStarted(5000)
                if procStarted:
                    finished = process.waitForFinished(30000)
                    if finished and process.exitCode() == 0:
                        output = str(process.readAllStandardOutput(),
                                     self.vcs.getEncoding(), 'replace')
                    else:
                        if not finished:
                            errMsg = self.tr(
                                "The hg process did not finish within 30s.")
                else:
                    errMsg = self.tr("Could not start the hg executable.")

            if errMsg:
                E5MessageBox.critical(self, self.tr("Mercurial Error"), errMsg)

            if output:
                parents = [p for p in output.strip().splitlines()]

        return parents

    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__finish()

    def __finish(self):
        """
        Private slot called when the process finished or the user pressed
        the button.
        """
        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()

        self.contents.clear()

        if not self.logEntries:
            self.errors.append(
                self.tr("No log available for '{0}'").format(self.filename))
            self.errorGroup.show()
            return

        html = ""

        if self.initialText:
            for line in self.initialText:
                html += Utilities.html_encode(line.strip())
                html += '<br />\n'
            html += '{0}<br/>\n'.format(80 * "=")

        for entry in self.logEntries:
            fileCopies = {}
            if entry["file_copies"]:
                for fentry in entry["file_copies"].split(", "):
                    newName, oldName = fentry[:-1].split(" (")
                    fileCopies[newName] = oldName

            rev, hexRev = entry["change"].split(":")
            dstr = '<p><b>{0} {1}</b>'.format(self.revString, entry["change"])
            if entry["parents"]:
                parents = entry["parents"].split()
            else:
                parents = self.__getParents(rev)
            for parent in parents:
                url = QUrl()
                url.setScheme("file")
                url.setPath(self.filename)
                if qVersion() >= "5.0.0":
                    query = parent.split(":")[0] + '_' + rev
                    url.setQuery(query)
                else:
                    query = QByteArray()
                    query.append(parent.split(":")[0]).append('_').append(rev)
                    url.setEncodedQuery(query)
                dstr += ' [<a href="{0}" name="{1}" id="{1}">{2}</a>]'.format(
                    url.toString(),
                    query,
                    self.tr('diff to {0}').format(parent),
                )
            dstr += '<br />\n'
            html += dstr

            if "phase" in entry:
                html += self.tr("Phase: {0}<br />\n")\
                    .format(entry["phase"])

            html += self.tr("Branch: {0}<br />\n")\
                .format(entry["branches"])

            html += self.tr("Tags: {0}<br />\n").format(entry["tags"])

            if "bookmarks" in entry:
                html += self.tr("Bookmarks: {0}<br />\n")\
                    .format(entry["bookmarks"])

            html += self.tr("Parents: {0}<br />\n")\
                .format(entry["parents"])

            html += self.tr('<i>Author: {0}</i><br />\n')\
                .format(Utilities.html_encode(entry["user"]))

            date, time = entry["date"].split()[:2]
            html += self.tr('<i>Date: {0}, {1}</i><br />\n')\
                .format(date, time)

            for line in entry["description"]:
                html += Utilities.html_encode(line.strip())
                html += '<br />\n'

            if entry["file_adds"]:
                html += '<br />\n'
                for f in entry["file_adds"].strip().split(", "):
                    if f in fileCopies:
                        html += self.tr(
                            'Added {0} (copied from {1})<br />\n')\
                            .format(Utilities.html_encode(f),
                                    Utilities.html_encode(fileCopies[f]))
                    else:
                        html += self.tr('Added {0}<br />\n')\
                            .format(Utilities.html_encode(f))

            if entry["files_mods"]:
                html += '<br />\n'
                for f in entry["files_mods"].strip().split(", "):
                    html += self.tr('Modified {0}<br />\n')\
                        .format(Utilities.html_encode(f))

            if entry["file_dels"]:
                html += '<br />\n'
                for f in entry["file_dels"].strip().split(", "):
                    html += self.tr('Deleted {0}<br />\n')\
                        .format(Utilities.html_encode(f))

            html += '</p>{0}<br/>\n'.format(60 * "=")

        self.contents.setHtml(html)
        tc = self.contents.textCursor()
        tc.movePosition(QTextCursor.Start)
        self.contents.setTextCursor(tc)
        self.contents.ensureCursorVisible()

    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.
        
        It reads the output of the process and inserts it into a buffer.
        """
        self.process.setReadChannel(QProcess.StandardOutput)

        while self.process.canReadLine():
            s = str(self.process.readLine(), self.vcs.getEncoding(), 'replace')
            self.__processOutputLine(s)

    def __processOutputLine(self, line):
        """
        Private method to process the lines of output.
        
        @param line output line to be processed (string)
        """
        if line == "@@@\n":
            self.logEntries.append(self.lastLogEntry)
            self.lastLogEntry = {}
            self.fileCopies = {}
        else:
            try:
                key, value = line.split("|", 1)
            except ValueError:
                key = ""
                value = line
            if key == "change":
                self.endInitialText = True
            if key in ("change", "tags", "parents", "user", "date",
                       "file_copies", "file_adds", "files_mods", "file_dels",
                       "bookmarks", "phase"):
                self.lastLogEntry[key] = value.strip()
            elif key == "branches":
                if value.strip():
                    self.lastLogEntry[key] = value.strip()
                else:
                    self.lastLogEntry[key] = "default"
            elif key == "description":
                self.lastLogEntry[key] = [value.strip()]
            else:
                if self.endInitialText:
                    self.lastLogEntry["description"].append(value.strip())
                else:
                    self.initialText.append(value)

    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            s = str(self.process.readAllStandardError(),
                    self.vcs.getEncoding(), 'replace')
            self.__showError(s)

    def __showError(self, out):
        """
        Private slot to show some error.
        
        @param out error to be shown (string)
        """
        self.errorGroup.show()
        self.errors.insertPlainText(out)
        self.errors.ensureCursorVisible()

    def __sourceChanged(self, url):
        """
        Private slot to handle the sourceChanged signal of the contents pane.
        
        @param url the url that was clicked (QUrl)
        """
        filename = url.path()
        if Utilities.isWindowsPlatform():
            if filename.startswith("/"):
                filename = filename[1:]
        if qVersion() >= "5.0.0":
            ver = url.query()
        else:
            ver = bytes(url.encodedQuery()).decode()
        v1, v2 = ver.split('_')
        if v1 == "" or v2 == "":
            return
        self.contents.scrollToAnchor(ver)

        if self.sbsCheckBox.isEnabled() and self.sbsCheckBox.isChecked():
            self.vcs.hgSbsDiff(filename, revisions=(v1, v2))
        else:
            if self.diff is None:
                from .HgDiffDialog import HgDiffDialog
                self.diff = HgDiffDialog(self.vcs)
            self.diff.show()
            self.diff.start(filename, [v1, v2], self.bundle)

    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)

    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the hg process.
        """
        input = self.input.text()
        input += os.linesep

        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(input)
            self.errors.ensureCursorVisible()

        self.process.write(input)

        self.passwordCheckBox.setChecked(False)
        self.input.clear()

    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()

    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(HgLogDialog, self).keyPressEvent(evt)
Beispiel #6
0
class SvnTagBranchListDialog(QDialog, Ui_SvnTagBranchListDialog):
    """
    Class implementing a dialog to show a list of tags or branches.
    """
    def __init__(self, vcs, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param parent parent widget (QWidget)
        """
        super(SvnTagBranchListDialog, self).__init__(parent)
        self.setupUi(self)

        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)

        self.process = QProcess()
        self.vcs = vcs
        self.tagsList = None
        self.allTagsList = None

        self.tagList.headerItem().setText(self.tagList.columnCount(), "")
        self.tagList.header().setSortIndicator(3, Qt.AscendingOrder)

        self.process.finished.connect(self.__procFinished)
        self.process.readyReadStandardOutput.connect(self.__readStdout)
        self.process.readyReadStandardError.connect(self.__readStderr)

        self.rx_list = QRegExp(
            r"""\w*\s*(\d+)\s+(\w+)\s+\d*\s*"""
            r"""((?:\w+\s+\d+|[0-9.]+\s+\w+)\s+[0-9:]+)\s+(.+)/\s*""")

    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)

        e.accept()

    def start(self, path, tags, tagsList, allTagsList):
        """
        Public slot to start the svn status command.
        
        @param path name of directory to be listed (string)
        @param tags flag indicating a list of tags is requested
            (False = branches, True = tags)
        @param tagsList reference to string list receiving the tags
            (list of strings)
        @param allTagsList reference to string list all tags (list of strings)
        """
        self.errorGroup.hide()

        self.intercept = False
        if not tags:
            self.setWindowTitle(self.tr("Subversion Branches List"))
        self.activateWindow()

        self.tagsList = tagsList
        self.allTagsList = allTagsList
        dname, fname = self.vcs.splitPath(path)

        self.process.kill()

        reposURL = self.vcs.svnGetReposName(dname)
        if reposURL is None:
            E5MessageBox.critical(
                self, self.tr("Subversion Error"),
                self.tr(
                    """The URL of the project repository could not be"""
                    """ retrieved from the working copy. The list operation"""
                    """ will be aborted"""))
            self.close()
            return

        args = []
        args.append('list')
        self.vcs.addArguments(args, self.vcs.options['global'])
        args.append('--verbose')

        if self.vcs.otherData["standardLayout"]:
            # determine the base path of the project in the repository
            rx_base = QRegExp('(.+)/(trunk|tags|branches).*')
            if not rx_base.exactMatch(reposURL):
                E5MessageBox.critical(
                    self, self.tr("Subversion Error"),
                    self.tr("""The URL of the project repository has an"""
                            """ invalid format. The list operation will"""
                            """ be aborted"""))
                return

            reposRoot = rx_base.cap(1)

            if tags:
                args.append("{0}/tags".format(reposRoot))
            else:
                args.append("{0}/branches".format(reposRoot))
            self.path = None
        else:
            reposPath, ok = QInputDialog.getText(
                self, self.tr("Subversion List"),
                self.tr("Enter the repository URL containing the tags"
                        " or branches"), QLineEdit.Normal,
                self.vcs.svnNormalizeURL(reposURL))
            if not ok:
                self.close()
                return
            if not reposPath:
                E5MessageBox.critical(
                    self, self.tr("Subversion List"),
                    self.tr("""The repository URL is empty."""
                            """ Aborting..."""))
                self.close()
                return
            args.append(reposPath)
            self.path = reposPath

        self.process.setWorkingDirectory(dname)

        self.process.start('svn', args)
        procStarted = self.process.waitForStarted(5000)
        if not procStarted:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            E5MessageBox.critical(
                self, self.tr('Process Generation Error'),
                self.tr(
                    'The process {0} could not be started. '
                    'Ensure, that it is in the search path.').format('svn'))
        else:
            self.inputGroup.setEnabled(True)
            self.inputGroup.show()

    def __finish(self):
        """
        Private slot called when the process finished or the user pressed the
        button.
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)

        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        self.buttonBox.button(QDialogButtonBox.Close).setFocus(
            Qt.OtherFocusReason)

        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()

        self.process = None

        self.__resizeColumns()
        self.__resort()

    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.close()
        elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
            self.__finish()

    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__finish()

    def __resort(self):
        """
        Private method to resort the tree.
        """
        self.tagList.sortItems(self.tagList.sortColumn(),
                               self.tagList.header().sortIndicatorOrder())

    def __resizeColumns(self):
        """
        Private method to resize the list columns.
        """
        self.tagList.header().resizeSections(QHeaderView.ResizeToContents)
        self.tagList.header().setStretchLastSection(True)

    def __generateItem(self, revision, author, date, name):
        """
        Private method to generate a tag item in the taglist.
        
        @param revision revision string (string)
        @param author author of the tag (string)
        @param date date of the tag (string)
        @param name name (path) of the tag (string)
        """
        itm = QTreeWidgetItem(self.tagList)
        itm.setData(0, Qt.DisplayRole, int(revision))
        itm.setData(1, Qt.DisplayRole, author)
        itm.setData(2, Qt.DisplayRole, date)
        itm.setData(3, Qt.DisplayRole, name)
        itm.setTextAlignment(0, Qt.AlignRight)

    def __readStdout(self):
        """
        Private slot to handle the readyReadStdout signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        self.process.setReadChannel(QProcess.StandardOutput)

        while self.process.canReadLine():
            s = str(self.process.readLine(),
                    Preferences.getSystem("IOEncoding"), 'replace')
            if self.rx_list.exactMatch(s):
                rev = "{0:6}".format(self.rx_list.cap(1))
                author = self.rx_list.cap(2)
                date = self.rx_list.cap(3)
                path = self.rx_list.cap(4)
                if path == ".":
                    continue
                self.__generateItem(rev, author, date, path)
                if not self.vcs.otherData["standardLayout"]:
                    path = self.path + '/' + path
                if self.tagsList is not None:
                    self.tagsList.append(path)
                if self.allTagsList is not None:
                    self.allTagsList.append(path)

    def __readStderr(self):
        """
        Private slot to handle the readyReadStderr signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            self.errorGroup.show()
            s = str(self.process.readAllStandardError(),
                    Preferences.getSystem("IOEncoding"), 'replace')
            self.errors.insertPlainText(s)
            self.errors.ensureCursorVisible()

    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)

    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the subversion process.
        """
        input = self.input.text()
        input += os.linesep

        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(input)
            self.errors.ensureCursorVisible()

        self.process.write(input)

        self.passwordCheckBox.setChecked(False)
        self.input.clear()

    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()

    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(SvnTagBranchListDialog, self).keyPressEvent(evt)
Beispiel #7
0
class Process(QObject):
    """Abstraction over a running test subprocess process.

    Reads the log from its stdout and parses it.

    Attributes:
        _invalid: A list of lines which could not be parsed.
        _data: A list of parsed lines.
        _started: Whether the process was ever started.
        proc: The QProcess for the underlying process.
        exit_expected: Whether the process is expected to quit.
        request: The request object for the current test.

    Signals:
        ready: Emitted when the server finished starting up.
        new_data: Emitted when a new line was parsed.
    """

    ready = pyqtSignal()
    new_data = pyqtSignal(object)
    KEYS = ['data']

    def __init__(self, request, parent=None):
        super().__init__(parent)
        self.request = request
        self.captured_log = []
        self._started = False
        self._invalid = []
        self._data = []
        self.proc = QProcess()
        self.proc.setReadChannel(QProcess.StandardError)
        self.exit_expected = None  # Not started at all yet

    def _log(self, line):
        """Add the given line to the captured log output."""
        if self.request.config.getoption('--capture') == 'no':
            print(line)
        self.captured_log.append(line)

    def log_summary(self, text):
        """Log the given line as summary/title."""
        text = '\n{line} {text} {line}\n'.format(line='=' * 30, text=text)
        self._log(text)

    def _parse_line(self, line):
        """Parse the given line from the log.

        Return:
            A self.ParseResult member.
        """
        raise NotImplementedError

    def _executable_args(self):
        """Get the executable and necessary arguments as a tuple."""
        raise NotImplementedError

    def _default_args(self):
        """Get the default arguments to use if none were passed to start()."""
        raise NotImplementedError

    def _get_data(self):
        """Get the parsed data for this test.

        Also waits for 0.5s to make sure any new data is received.

        Subprocesses are expected to alias this to a public method with a
        better name.
        """
        self.proc.waitForReadyRead(500)
        self.read_log()
        return self._data

    def _wait_signal(self, signal, timeout=5000, raising=True):
        """Wait for a signal to be emitted.

        Should be used in a contextmanager.
        """
        blocker = pytestqt.plugin.SignalBlocker(timeout=timeout,
                                                raising=raising)
        blocker.connect(signal)
        return blocker

    @pyqtSlot()
    def read_log(self):
        """Read the log from the process' stdout."""
        if not hasattr(self, 'proc'):
            # I have no idea how this happens, but it does...
            return
        while self.proc.canReadLine():
            line = self.proc.readLine()
            line = bytes(line).decode('utf-8', errors='ignore').rstrip('\r\n')

            try:
                parsed = self._parse_line(line)
            except InvalidLine:
                self._invalid.append(line)
                self._log("INVALID: {}".format(line))
                continue

            if parsed is None:
                if self._invalid:
                    self._log("IGNORED: {}".format(line))
            else:
                self._data.append(parsed)
                self.new_data.emit(parsed)

    def start(self, args=None, *, env=None):
        """Start the process and wait until it started."""
        self._start(args, env=env)
        self._started = True
        verbose = self.request.config.getoption('--verbose')

        timeout = 60 if 'CI' in os.environ else 20
        for _ in range(timeout):
            with self._wait_signal(self.ready, timeout=1000,
                                   raising=False) as blocker:
                pass

            if not self.is_running():
                if self.exit_expected:
                    return
                # _start ensures it actually started, but it might quit shortly
                # afterwards
                raise ProcessExited(
                    '\n' + _render_log(self.captured_log, verbose=verbose))

            if blocker.signal_triggered:
                self._after_start()
                return

        raise WaitForTimeout("Timed out while waiting for process start.\n" +
                             _render_log(self.captured_log, verbose=verbose))

    def _start(self, args, env):
        """Actually start the process."""
        executable, exec_args = self._executable_args()
        if args is None:
            args = self._default_args()

        procenv = QProcessEnvironment.systemEnvironment()
        if env is not None:
            for k, v in env.items():
                procenv.insert(k, v)

        self.proc.readyRead.connect(self.read_log)
        self.proc.setProcessEnvironment(procenv)
        self.proc.start(executable, exec_args + args)
        ok = self.proc.waitForStarted()
        assert ok
        assert self.is_running()

    def _after_start(self):
        """Do things which should be done immediately after starting."""

    def before_test(self):
        """Restart process before a test if it exited before."""
        self._invalid = []
        if not self.is_running():
            self.start()

    def after_test(self):
        """Clean up data after each test.

        Also checks self._invalid so the test counts as failed if there were
        unexpected output lines earlier.
        """
        __tracebackhide__ = lambda e: e.errisinstance(ProcessExited)
        self.captured_log = []
        if self._invalid:
            # Wait for a bit so the full error has a chance to arrive
            time.sleep(1)
            # Exit the process to make sure we're in a defined state again
            self.terminate()
            self.clear_data()
            raise InvalidLine('\n' + '\n'.join(self._invalid))

        self.clear_data()
        if not self.is_running() and not self.exit_expected and self._started:
            raise ProcessExited
        self.exit_expected = False

    def clear_data(self):
        """Clear the collected data."""
        self._data.clear()

    def terminate(self):
        """Clean up and shut down the process."""
        if not self.is_running():
            return

        if quteutils.is_windows:
            self.proc.kill()
        else:
            self.proc.terminate()

        ok = self.proc.waitForFinished()
        if not ok:
            self.proc.kill()
            self.proc.waitForFinished()

    def is_running(self):
        """Check if the process is currently running."""
        return self.proc.state() == QProcess.Running

    def _match_data(self, value, expected):
        """Helper for wait_for to match a given value.

        The behavior of this method is slightly different depending on the
        types of the filtered values:

        - If expected is None, the filter always matches.
        - If the value is a string or bytes object and the expected value is
          too, the pattern is treated as a glob pattern (with only * active).
        - If the value is a string or bytes object and the expected value is a
          compiled regex, it is used for matching.
        - If the value is any other type, == is used.

        Return:
            A bool
        """
        regex_type = type(re.compile(''))
        if expected is None:
            return True
        elif isinstance(expected, regex_type):
            return expected.search(value)
        elif isinstance(value, (bytes, str)):
            return utils.pattern_match(pattern=expected, value=value)
        else:
            return value == expected

    def _wait_for_existing(self, override_waited_for, after, **kwargs):
        """Check if there are any line in the history for wait_for.

        Return: either the found line or None.
        """
        for line in self._data:
            matches = []

            for key, expected in kwargs.items():
                value = getattr(line, key)
                matches.append(self._match_data(value, expected))

            if after is None:
                too_early = False
            else:
                too_early = ((line.timestamp, line.msecs) <
                             (after.timestamp, after.msecs))

            if (all(matches) and (not line.waited_for or override_waited_for)
                    and not too_early):
                # If we waited for this line, chances are we don't mean the
                # same thing the next time we use wait_for and it matches
                # this line again.
                line.waited_for = True
                self._log("\n----> Already found {!r} in the log: {}".format(
                    kwargs.get('message', 'line'), line))
                return line
        return None

    def _wait_for_new(self, timeout, do_skip, **kwargs):
        """Wait for a log message which doesn't exist yet.

        Called via wait_for.
        """
        __tracebackhide__ = lambda e: e.errisinstance(WaitForTimeout)
        message = kwargs.get('message', None)
        if message is not None:
            elided = quteutils.elide(repr(message), 100)
            self._log("\n----> Waiting for {} in the log".format(elided))

        spy = QSignalSpy(self.new_data)
        elapsed_timer = QElapsedTimer()
        elapsed_timer.start()

        while True:
            # Skip if there are pending messages causing a skip
            self._maybe_skip()
            got_signal = spy.wait(timeout)
            if not got_signal or elapsed_timer.hasExpired(timeout):
                msg = "Timed out after {}ms waiting for {!r}.".format(
                    timeout, kwargs)
                if do_skip:
                    pytest.skip(msg)
                else:
                    raise WaitForTimeout(msg)

            match = self._wait_for_match(spy, kwargs)
            if match is not None:
                if message is not None:
                    self._log("----> found it")
                return match

        raise quteutils.Unreachable

    def _wait_for_match(self, spy, kwargs):
        """Try matching the kwargs with the given QSignalSpy."""
        for args in spy:
            assert len(args) == 1
            line = args[0]

            matches = []

            for key, expected in kwargs.items():
                value = getattr(line, key)
                matches.append(self._match_data(value, expected))

            if all(matches):
                # If we waited for this line, chances are we don't mean the
                # same thing the next time we use wait_for and it matches
                # this line again.
                line.waited_for = True
                return line
        return None

    def _maybe_skip(self):
        """Can be overridden by subclasses to skip on certain log lines.

        We can't run pytest.skip directly while parsing the log, as that would
        lead to a pytest.skip.Exception error in a virtual Qt method, which
        means pytest-qt fails the test.

        Instead, we check for skip messages periodically in
        QuteProc._maybe_skip, and call _maybe_skip after every parsed message
        in wait_for (where it's most likely that new messages arrive).
        """

    def wait_for(self,
                 timeout=None,
                 *,
                 override_waited_for=False,
                 do_skip=False,
                 divisor=1,
                 after=None,
                 **kwargs):
        """Wait until a given value is found in the data.

        Keyword arguments to this function get interpreted as attributes of the
        searched data. Every given argument is treated as a pattern which
        the attribute has to match against.

        Args:
            timeout: How long to wait for the message.
            override_waited_for: If set, gets triggered by previous messages
                                 again.
            do_skip: If set, call pytest.skip on a timeout.
            divisor: A factor to decrease the timeout by.
            after: If it's an existing line, ensure it's after the given one.

        Return:
            The matched line.
        """
        __tracebackhide__ = lambda e: e.errisinstance(WaitForTimeout)

        if timeout is None:
            if do_skip:
                timeout = 2000
            elif 'CI' in os.environ:
                timeout = 15000
            else:
                timeout = 5000

        timeout //= divisor

        if not kwargs:
            raise TypeError("No keyword arguments given!")
        for key in kwargs:
            assert key in self.KEYS

        existing = self._wait_for_existing(override_waited_for, after,
                                           **kwargs)
        if existing is not None:
            return existing
        else:
            return self._wait_for_new(timeout=timeout,
                                      do_skip=do_skip,
                                      **kwargs)

    def ensure_not_logged(self, delay=500, **kwargs):
        """Make sure the data matching the given arguments is not logged.

        If nothing is found in the log, we wait for delay ms to make sure
        nothing arrives.
        """
        __tracebackhide__ = lambda e: e.errisinstance(BlacklistedMessageError)
        try:
            line = self.wait_for(timeout=delay,
                                 override_waited_for=True,
                                 **kwargs)
        except WaitForTimeout:
            return
        else:
            raise BlacklistedMessageError(line)

    def wait_for_quit(self):
        """Wait until the process has quit."""
        self.exit_expected = True
        with self._wait_signal(self.proc.finished, timeout=15000):
            pass
Beispiel #8
0
class HgAnnotateDialog(QDialog, Ui_HgAnnotateDialog):
    """
    Class implementing a dialog to show the output of the hg annotate command.
    """
    def __init__(self, vcs, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param parent parent widget (QWidget)
        """
        super(HgAnnotateDialog, self).__init__(parent)
        self.setupUi(self)
        self.setWindowFlags(Qt.Window)
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
        
        self.vcs = vcs
        self.__hgClient = vcs.getClient()
        
        self.__annotateRe = re.compile(
            r"""(.+)\s+(\d+)\s+([0-9a-fA-F]+)\s+([0-9-]+)\s+(.+)""")
        
        self.annotateList.headerItem().setText(
            self.annotateList.columnCount(), "")
        font = Preferences.getEditorOtherFonts("MonospacedFont")
        self.annotateList.setFont(font)
        
        if self.__hgClient:
            self.process = None
        else:
            self.process = QProcess()
            self.process.finished.connect(self.__procFinished)
            self.process.readyReadStandardOutput.connect(self.__readStdout)
            self.process.readyReadStandardError.connect(self.__readStderr)
        
        self.show()
        QCoreApplication.processEvents()
    
    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.__hgClient:
            if self.__hgClient.isExecuting():
                self.__hgClient.cancel()
        else:
            if self.process is not None and \
               self.process.state() != QProcess.NotRunning:
                self.process.terminate()
                QTimer.singleShot(2000, self.process.kill)
                self.process.waitForFinished(3000)
        
        e.accept()
    
    def start(self, fn):
        """
        Public slot to start the annotate command.
        
        @param fn filename to show the annotation for (string)
        """
        self.annotateList.clear()
        self.errorGroup.hide()
        self.intercept = False
        self.activateWindow()
        self.lineno = 1
        
        dname, fname = self.vcs.splitPath(fn)
        
        # find the root of the repo
        repodir = dname
        while not os.path.isdir(os.path.join(repodir, self.vcs.adminDir)):
            repodir = os.path.dirname(repodir)
            if os.path.splitdrive(repodir)[1] == os.sep:
                return
        
        args = self.vcs.initCommand("annotate")
        args.append('--follow')
        args.append('--user')
        args.append('--date')
        args.append('--number')
        args.append('--changeset')
        args.append('--quiet')
        args.append(fn)
        
        if self.__hgClient:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            
            out, err = self.__hgClient.runcommand(args)
            if err:
                self.__showError(err)
            if out:
                for line in out.splitlines():
                    self.__processOutputLine(line)
                    if self.__hgClient.wasCanceled():
                        break
            self.__finish()
        else:
            self.process.kill()
            self.process.setWorkingDirectory(repodir)
            
            self.process.start('hg', args)
            procStarted = self.process.waitForStarted(5000)
            if not procStarted:
                self.inputGroup.setEnabled(False)
                self.inputGroup.hide()
                E5MessageBox.critical(
                    self,
                    self.tr('Process Generation Error'),
                    self.tr(
                        'The process {0} could not be started. '
                        'Ensure, that it is in the search path.'
                    ).format('hg'))
            else:
                self.inputGroup.setEnabled(True)
                self.inputGroup.show()
    
    def __finish(self):
        """
        Private slot called when the process finished or the user pressed
        the button.
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        self.buttonBox.button(QDialogButtonBox.Close).setFocus(
            Qt.OtherFocusReason)
        
        self.__resizeColumns()
    
    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.close()
        elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
            if self.__hgClient:
                self.__hgClient.cancel()
            else:
                self.__finish()
    
    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__finish()
    
    def __resizeColumns(self):
        """
        Private method to resize the list columns.
        """
        self.annotateList.header().resizeSections(QHeaderView.ResizeToContents)
    
    def __generateItem(self, revision, changeset, author, date, text):
        """
        Private method to generate an annotate item in the annotation list.
        
        @param revision revision string (string)
        @param changeset changeset string (string)
        @param author author of the change (string)
        @param date date of the change (string)
        @param text text of the change (string)
        """
        itm = QTreeWidgetItem(
            self.annotateList,
            [revision, changeset, author, date, "{0:d}".format(self.lineno),
             text])
        self.lineno += 1
        itm.setTextAlignment(0, Qt.AlignRight)
        itm.setTextAlignment(4, Qt.AlignRight)
    
    def __readStdout(self):
        """
        Private slot to handle the readyReadStdout signal.
        
        It reads the output of the process, formats it and inserts it into
        the annotation list.
        """
        self.process.setReadChannel(QProcess.StandardOutput)
        
        while self.process.canReadLine():
            s = str(self.process.readLine(), self.vcs.getEncoding(),
                    'replace').strip()
            self.__processOutputLine(s)
    
    def __processOutputLine(self, line):
        """
        Private method to process the lines of output.
        
        @param line output line to be processed (string)
        """
        try:
            info, text = line.split(": ", 1)
        except ValueError:
            info = line[:-2]
            text = ""
        match = self.__annotateRe.match(info)
        author, rev, changeset, date, file = match.groups()
        self.__generateItem(rev.strip(), changeset.strip(), author.strip(),
                            date.strip(), text)
    
    def __readStderr(self):
        """
        Private slot to handle the readyReadStderr signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            s = str(self.process.readAllStandardError(),
                    self.vcs.getEncoding(), 'replace')
            self.__showError(s)
    
    def __showError(self, out):
        """
        Private slot to show some error.
        
        @param out error to be shown (string)
        """
        self.errorGroup.show()
        self.errors.insertPlainText(out)
        self.errors.ensureCursorVisible()
    
    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)
    
    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the hg process.
        """
        input = self.input.text()
        input += os.linesep
        
        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(input)
            self.errors.ensureCursorVisible()
        
        self.process.write(input)
        
        self.passwordCheckBox.setChecked(False)
        self.input.clear()
    
    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()
    
    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(HgAnnotateDialog, self).keyPressEvent(evt)
Beispiel #9
0
class SvnStatusDialog(QWidget, Ui_SvnStatusDialog):
    """
    Class implementing a dialog to show the output of the svn status command
    process.
    """
    def __init__(self, vcs, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param parent parent widget (QWidget)
        """
        super(SvnStatusDialog, self).__init__(parent)
        self.setupUi(self)

        self.__toBeCommittedColumn = 0
        self.__changelistColumn = 1
        self.__statusColumn = 2
        self.__propStatusColumn = 3
        self.__lockedColumn = 4
        self.__historyColumn = 5
        self.__switchedColumn = 6
        self.__lockinfoColumn = 7
        self.__upToDateColumn = 8
        self.__pathColumn = 12
        self.__lastColumn = self.statusList.columnCount()

        self.refreshButton = \
            self.buttonBox.addButton(self.tr("Refresh"),
                                     QDialogButtonBox.ActionRole)
        self.refreshButton.setToolTip(
            self.tr("Press to refresh the status display"))
        self.refreshButton.setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)

        self.diff = None
        self.vcs = vcs
        self.vcs.committed.connect(self.__committed)

        self.process = QProcess()
        self.process.finished.connect(self.__procFinished)
        self.process.readyReadStandardOutput.connect(self.__readStdout)
        self.process.readyReadStandardError.connect(self.__readStderr)

        self.statusList.headerItem().setText(self.__lastColumn, "")
        self.statusList.header().setSortIndicator(self.__pathColumn,
                                                  Qt.AscendingOrder)
        if self.vcs.version < (1, 5, 0):
            self.statusList.header().hideSection(self.__changelistColumn)

        self.menuactions = []
        self.menu = QMenu()
        self.menuactions.append(
            self.menu.addAction(self.tr("Commit changes to repository..."),
                                self.__commit))
        self.menuactions.append(
            self.menu.addAction(self.tr("Select all for commit"),
                                self.__commitSelectAll))
        self.menuactions.append(
            self.menu.addAction(self.tr("Deselect all from commit"),
                                self.__commitDeselectAll))
        self.menu.addSeparator()
        self.menuactions.append(
            self.menu.addAction(self.tr("Add to repository"), self.__add))
        self.menuactions.append(
            self.menu.addAction(self.tr("Show differences"), self.__diff))
        self.menuactions.append(
            self.menu.addAction(self.tr("Show differences side-by-side"),
                                self.__sbsDiff))
        self.menuactions.append(
            self.menu.addAction(self.tr("Revert changes"), self.__revert))
        self.menuactions.append(
            self.menu.addAction(self.tr("Restore missing"),
                                self.__restoreMissing))
        if self.vcs.version >= (1, 5, 0):
            self.menu.addSeparator()
            self.menuactions.append(
                self.menu.addAction(self.tr("Add to Changelist"),
                                    self.__addToChangelist))
            self.menuactions.append(
                self.menu.addAction(self.tr("Remove from Changelist"),
                                    self.__removeFromChangelist))
        if self.vcs.version >= (1, 2, 0):
            self.menu.addSeparator()
            self.menuactions.append(
                self.menu.addAction(self.tr("Lock"), self.__lock))
            self.menuactions.append(
                self.menu.addAction(self.tr("Unlock"), self.__unlock))
            self.menuactions.append(
                self.menu.addAction(self.tr("Break lock"), self.__breakLock))
            self.menuactions.append(
                self.menu.addAction(self.tr("Steal lock"), self.__stealLock))
        self.menu.addSeparator()
        self.menuactions.append(
            self.menu.addAction(self.tr("Adjust column sizes"),
                                self.__resizeColumns))
        for act in self.menuactions:
            act.setEnabled(False)

        self.statusList.setContextMenuPolicy(Qt.CustomContextMenu)
        self.statusList.customContextMenuRequested.connect(
            self.__showContextMenu)

        self.modifiedIndicators = [
            self.tr('added'),
            self.tr('deleted'),
            self.tr('modified'),
        ]

        self.missingIndicators = [
            self.tr('missing'),
        ]

        self.unversionedIndicators = [
            self.tr('unversioned'),
        ]

        self.lockedIndicators = [
            self.tr('locked'),
        ]

        self.stealBreakLockIndicators = [
            self.tr('other lock'),
            self.tr('stolen lock'),
            self.tr('broken lock'),
        ]

        self.unlockedIndicators = [
            self.tr('not locked'),
        ]

        self.status = {
            ' ': self.tr('normal'),
            'A': self.tr('added'),
            'D': self.tr('deleted'),
            'M': self.tr('modified'),
            'R': self.tr('replaced'),
            'C': self.tr('conflict'),
            'X': self.tr('external'),
            'I': self.tr('ignored'),
            '?': self.tr('unversioned'),
            '!': self.tr('missing'),
            '~': self.tr('type error'),
        }
        self.propStatus = {
            ' ': self.tr('normal'),
            'M': self.tr('modified'),
            'C': self.tr('conflict'),
        }
        self.locked = {
            ' ': self.tr('no'),
            'L': self.tr('yes'),
        }
        self.history = {
            ' ': self.tr('no'),
            '+': self.tr('yes'),
        }
        self.switched = {
            ' ': self.tr('no'),
            'S': self.tr('yes'),
        }
        self.lockinfo = {
            ' ': self.tr('not locked'),
            'K': self.tr('locked'),
            'O': self.tr('other lock'),
            'T': self.tr('stolen lock'),
            'B': self.tr('broken lock'),
        }
        self.uptodate = {
            ' ': self.tr('yes'),
            '*': self.tr('no'),
        }

        self.rx_status = QRegExp(
            '(.{8,9})\\s+([0-9-]+)\\s+([0-9?]+)\\s+(\\S+)\\s+(.+)\\s*')
        # flags (8 or 9 anything), revision, changed rev, author, path
        self.rx_status2 = \
            QRegExp('(.{8,9})\\s+(.+)\\s*')
        # flags (8 or 9 anything), path
        self.rx_changelist = \
            QRegExp('--- \\S+ .([\\w\\s]+).:\\s+')
        # three dashes, Changelist (translated), quote,
        # changelist name, quote, :

        self.__nonverbose = True

    def __resort(self):
        """
        Private method to resort the tree.
        """
        self.statusList.sortItems(
            self.statusList.sortColumn(),
            self.statusList.header().sortIndicatorOrder())

    def __resizeColumns(self):
        """
        Private method to resize the list columns.
        """
        self.statusList.header().resizeSections(QHeaderView.ResizeToContents)
        self.statusList.header().setStretchLastSection(True)

    def __generateItem(self, status, propStatus, locked, history, switched,
                       lockinfo, uptodate, revision, change, author, path):
        """
        Private method to generate a status item in the status list.
        
        @param status status indicator (string)
        @param propStatus property status indicator (string)
        @param locked locked indicator (string)
        @param history history indicator (string)
        @param switched switched indicator (string)
        @param lockinfo lock indicator (string)
        @param uptodate up to date indicator (string)
        @param revision revision string (string)
        @param change revision of last change (string)
        @param author author of the last change (string)
        @param path path of the file or directory (string)
        """
        if self.__nonverbose and \
           status == " " and \
           propStatus == " " and \
           locked == " " and \
           history == " " and \
           switched == " " and \
           lockinfo == " " and \
           uptodate == " " and \
           self.currentChangelist == "":
            return

        if revision == "":
            rev = ""
        else:
            try:
                rev = int(revision)
            except ValueError:
                rev = revision
        if change == "":
            chg = ""
        else:
            try:
                chg = int(change)
            except ValueError:
                chg = change
        statusText = self.status[status]

        itm = QTreeWidgetItem(self.statusList)
        itm.setData(0, Qt.DisplayRole, "")
        itm.setData(1, Qt.DisplayRole, self.currentChangelist)
        itm.setData(2, Qt.DisplayRole, statusText)
        itm.setData(3, Qt.DisplayRole, self.propStatus[propStatus])
        itm.setData(4, Qt.DisplayRole, self.locked[locked])
        itm.setData(5, Qt.DisplayRole, self.history[history])
        itm.setData(6, Qt.DisplayRole, self.switched[switched])
        itm.setData(7, Qt.DisplayRole, self.lockinfo[lockinfo])
        itm.setData(8, Qt.DisplayRole, self.uptodate[uptodate])
        itm.setData(9, Qt.DisplayRole, rev)
        itm.setData(10, Qt.DisplayRole, chg)
        itm.setData(11, Qt.DisplayRole, author)
        itm.setData(12, Qt.DisplayRole, path)

        itm.setTextAlignment(1, Qt.AlignLeft)
        itm.setTextAlignment(2, Qt.AlignHCenter)
        itm.setTextAlignment(3, Qt.AlignHCenter)
        itm.setTextAlignment(4, Qt.AlignHCenter)
        itm.setTextAlignment(5, Qt.AlignHCenter)
        itm.setTextAlignment(6, Qt.AlignHCenter)
        itm.setTextAlignment(7, Qt.AlignHCenter)
        itm.setTextAlignment(8, Qt.AlignHCenter)
        itm.setTextAlignment(9, Qt.AlignRight)
        itm.setTextAlignment(10, Qt.AlignRight)
        itm.setTextAlignment(11, Qt.AlignLeft)
        itm.setTextAlignment(12, Qt.AlignLeft)

        if status in "ADM" or propStatus in "M":
            itm.setFlags(itm.flags() | Qt.ItemIsUserCheckable)
            itm.setCheckState(self.__toBeCommittedColumn, Qt.Checked)
        else:
            itm.setFlags(itm.flags() & ~Qt.ItemIsUserCheckable)

        self.hidePropertyStatusColumn = self.hidePropertyStatusColumn and \
            propStatus == " "
        self.hideLockColumns = self.hideLockColumns and \
            locked == " " and lockinfo == " "
        self.hideUpToDateColumn = self.hideUpToDateColumn and uptodate == " "
        self.hideHistoryColumn = self.hideHistoryColumn and history == " "
        self.hideSwitchedColumn = self.hideSwitchedColumn and switched == " "

        if statusText not in self.__statusFilters:
            self.__statusFilters.append(statusText)

    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)

        e.accept()

    def start(self, fn):
        """
        Public slot to start the svn status command.
        
        @param fn filename(s)/directoryname(s) to show the status of
            (string or list of strings)
        """
        self.errorGroup.hide()
        self.intercept = False
        self.args = fn

        for act in self.menuactions:
            act.setEnabled(False)

        self.addButton.setEnabled(False)
        self.commitButton.setEnabled(False)
        self.diffButton.setEnabled(False)
        self.sbsDiffButton.setEnabled(False)
        self.revertButton.setEnabled(False)
        self.restoreButton.setEnabled(False)

        self.statusFilterCombo.clear()
        self.__statusFilters = []
        self.statusList.clear()

        self.currentChangelist = ""
        self.changelistFound = False

        self.hidePropertyStatusColumn = True
        self.hideLockColumns = True
        self.hideUpToDateColumn = True
        self.hideHistoryColumn = True
        self.hideSwitchedColumn = True

        self.process.kill()

        args = []
        args.append('status')
        self.vcs.addArguments(args, self.vcs.options['global'])
        self.vcs.addArguments(args, self.vcs.options['status'])
        if '--verbose' not in self.vcs.options['global'] and \
           '--verbose' not in self.vcs.options['status']:
            args.append('--verbose')
            self.__nonverbose = True
        else:
            self.__nonverbose = False
        if '--show-updates' in self.vcs.options['status'] or \
           '-u' in self.vcs.options['status']:
            self.activateWindow()
            self.raise_()
        if isinstance(fn, list):
            self.dname, fnames = self.vcs.splitPathList(fn)
            self.vcs.addArguments(args, fnames)
        else:
            self.dname, fname = self.vcs.splitPath(fn)
            args.append(fname)

        self.process.setWorkingDirectory(self.dname)

        self.setWindowTitle(self.tr('Subversion Status'))

        self.process.start('svn', args)
        procStarted = self.process.waitForStarted(5000)
        if not procStarted:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            E5MessageBox.critical(
                self, self.tr('Process Generation Error'),
                self.tr(
                    'The process {0} could not be started. '
                    'Ensure, that it is in the search path.').format('svn'))
        else:
            self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
            self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
            self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)

            self.inputGroup.setEnabled(True)
            self.inputGroup.show()
            self.refreshButton.setEnabled(False)

    def __finish(self):
        """
        Private slot called when the process finished or the user pressed
        the button.
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)

        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        self.buttonBox.button(QDialogButtonBox.Close).setFocus(
            Qt.OtherFocusReason)

        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()
        self.refreshButton.setEnabled(True)

        self.__statusFilters.sort()
        self.__statusFilters.insert(0, "<{0}>".format(self.tr("all")))
        self.statusFilterCombo.addItems(self.__statusFilters)

        for act in self.menuactions:
            act.setEnabled(True)

        self.__resort()
        self.__resizeColumns()

        self.statusList.setColumnHidden(self.__changelistColumn,
                                        not self.changelistFound)
        self.statusList.setColumnHidden(self.__propStatusColumn,
                                        self.hidePropertyStatusColumn)
        self.statusList.setColumnHidden(self.__lockedColumn,
                                        self.hideLockColumns)
        self.statusList.setColumnHidden(self.__lockinfoColumn,
                                        self.hideLockColumns)
        self.statusList.setColumnHidden(self.__upToDateColumn,
                                        self.hideUpToDateColumn)
        self.statusList.setColumnHidden(self.__historyColumn,
                                        self.hideHistoryColumn)
        self.statusList.setColumnHidden(self.__switchedColumn,
                                        self.hideSwitchedColumn)

        self.__updateButtons()
        self.__updateCommitButton()

    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.close()
        elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
            self.__finish()
        elif button == self.refreshButton:
            self.on_refreshButton_clicked()

    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__finish()

    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        if self.process is not None:
            self.process.setReadChannel(QProcess.StandardOutput)

            while self.process.canReadLine():
                s = str(self.process.readLine(),
                        Preferences.getSystem("IOEncoding"), 'replace')
                if self.rx_status.exactMatch(s):
                    flags = self.rx_status.cap(1)
                    rev = self.rx_status.cap(2)
                    change = self.rx_status.cap(3)
                    author = self.rx_status.cap(4)
                    path = self.rx_status.cap(5).strip()

                    self.__generateItem(flags[0], flags[1], flags[2], flags[3],
                                        flags[4], flags[5], flags[-1], rev,
                                        change, author, path)
                elif self.rx_status2.exactMatch(s):
                    flags = self.rx_status2.cap(1)
                    path = self.rx_status2.cap(2).strip()

                    self.__generateItem(flags[0], flags[1], flags[2], flags[3],
                                        flags[4], flags[5], flags[-1], "", "",
                                        "", path)
                elif self.rx_changelist.exactMatch(s):
                    self.currentChangelist = self.rx_changelist.cap(1)
                    self.changelistFound = True

    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            self.errorGroup.show()
            s = str(self.process.readAllStandardError(),
                    Preferences.getSystem("IOEncoding"), 'replace')
            self.errors.insertPlainText(s)
            self.errors.ensureCursorVisible()

    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)

    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the subversion process.
        """
        input = self.input.text()
        input += os.linesep

        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(input)
            self.errors.ensureCursorVisible()

        self.process.write(input)

        self.passwordCheckBox.setChecked(False)
        self.input.clear()

    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()

    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(SvnStatusDialog, self).keyPressEvent(evt)

    @pyqtSlot()
    def on_refreshButton_clicked(self):
        """
        Private slot to refresh the status display.
        """
        self.start(self.args)

    def __updateButtons(self):
        """
        Private method to update the VCS buttons status.
        """
        modified = len(self.__getModifiedItems())
        unversioned = len(self.__getUnversionedItems())
        missing = len(self.__getMissingItems())

        self.addButton.setEnabled(unversioned)
        self.diffButton.setEnabled(modified)
        self.sbsDiffButton.setEnabled(modified == 1)
        self.revertButton.setEnabled(modified)
        self.restoreButton.setEnabled(missing)

    def __updateCommitButton(self):
        """
        Private method to update the Commit button status.
        """
        commitable = len(self.__getCommitableItems())
        self.commitButton.setEnabled(commitable)

    @pyqtSlot(str)
    def on_statusFilterCombo_activated(self, txt):
        """
        Private slot to react to the selection of a status filter.
        
        @param txt selected status filter (string)
        """
        if txt == "<{0}>".format(self.tr("all")):
            for topIndex in range(self.statusList.topLevelItemCount()):
                topItem = self.statusList.topLevelItem(topIndex)
                topItem.setHidden(False)
        else:
            for topIndex in range(self.statusList.topLevelItemCount()):
                topItem = self.statusList.topLevelItem(topIndex)
                topItem.setHidden(topItem.text(self.__statusColumn) != txt)

    @pyqtSlot(QTreeWidgetItem, int)
    def on_statusList_itemChanged(self, item, column):
        """
        Private slot to act upon item changes.
        
        @param item reference to the changed item (QTreeWidgetItem)
        @param column index of column that changed (integer)
        """
        if column == self.__toBeCommittedColumn:
            self.__updateCommitButton()

    @pyqtSlot()
    def on_statusList_itemSelectionChanged(self):
        """
        Private slot to act upon changes of selected items.
        """
        self.__updateButtons()

    @pyqtSlot()
    def on_commitButton_clicked(self):
        """
        Private slot to handle the press of the Commit button.
        """
        self.__commit()

    @pyqtSlot()
    def on_addButton_clicked(self):
        """
        Private slot to handle the press of the Add button.
        """
        self.__add()

    @pyqtSlot()
    def on_diffButton_clicked(self):
        """
        Private slot to handle the press of the Differences button.
        """
        self.__diff()

    @pyqtSlot()
    def on_sbsDiffButton_clicked(self):
        """
        Private slot to handle the press of the Side-by-Side Diff button.
        """
        self.__sbsDiff()

    @pyqtSlot()
    def on_revertButton_clicked(self):
        """
        Private slot to handle the press of the Revert button.
        """
        self.__revert()

    @pyqtSlot()
    def on_restoreButton_clicked(self):
        """
        Private slot to handle the press of the Restore button.
        """
        self.__restoreMissing()

    ###########################################################################
    ## Context menu handling methods
    ###########################################################################

    def __showContextMenu(self, coord):
        """
        Private slot to show the context menu of the status list.
        
        @param coord the position of the mouse pointer (QPoint)
        """
        self.menu.popup(self.statusList.mapToGlobal(coord))

    def __commit(self):
        """
        Private slot to handle the Commit context menu entry.
        """
        names = [
            os.path.join(self.dname, itm.text(self.__pathColumn))
            for itm in self.__getCommitableItems()
        ]
        if not names:
            E5MessageBox.information(
                self, self.tr("Commit"),
                self.tr("""There are no entries selected to be"""
                        """ committed."""))
            return

        if Preferences.getVCS("AutoSaveFiles"):
            vm = e5App().getObject("ViewManager")
            for name in names:
                vm.saveEditor(name)
        self.vcs.vcsCommit(names, '')

    def __committed(self):
        """
        Private slot called after the commit has finished.
        """
        if self.isVisible():
            self.on_refreshButton_clicked()
            self.vcs.checkVCSStatus()

    def __commitSelectAll(self):
        """
        Private slot to select all entries for commit.
        """
        self.__commitSelect(True)

    def __commitDeselectAll(self):
        """
        Private slot to deselect all entries from commit.
        """
        self.__commitSelect(False)

    def __add(self):
        """
        Private slot to handle the Add context menu entry.
        """
        names = [
            os.path.join(self.dname, itm.text(self.__pathColumn))
            for itm in self.__getUnversionedItems()
        ]
        if not names:
            E5MessageBox.information(
                self, self.tr("Add"),
                self.tr("""There are no unversioned entries"""
                        """ available/selected."""))
            return

        self.vcs.vcsAdd(names)
        self.on_refreshButton_clicked()

        project = e5App().getObject("Project")
        for name in names:
            project.getModel().updateVCSStatus(name)
        self.vcs.checkVCSStatus()

    def __revert(self):
        """
        Private slot to handle the Revert context menu entry.
        """
        names = [
            os.path.join(self.dname, itm.text(self.__pathColumn))
            for itm in self.__getModifiedItems()
        ]
        if not names:
            E5MessageBox.information(
                self, self.tr("Revert"),
                self.tr("""There are no uncommitted changes"""
                        """ available/selected."""))
            return

        self.vcs.vcsRevert(names)
        self.raise_()
        self.activateWindow()
        self.on_refreshButton_clicked()

        project = e5App().getObject("Project")
        for name in names:
            project.getModel().updateVCSStatus(name)
        self.vcs.checkVCSStatus()

    def __restoreMissing(self):
        """
        Private slot to handle the Restore Missing context menu entry.
        """
        names = [
            os.path.join(self.dname, itm.text(self.__pathColumn))
            for itm in self.__getMissingItems()
        ]
        if not names:
            E5MessageBox.information(
                self, self.tr("Revert"),
                self.tr("""There are no missing entries"""
                        """ available/selected."""))
            return

        self.vcs.vcsRevert(names)
        self.on_refreshButton_clicked()
        self.vcs.checkVCSStatus()

    def __diff(self):
        """
        Private slot to handle the Diff context menu entry.
        """
        names = [
            os.path.join(self.dname, itm.text(self.__pathColumn))
            for itm in self.__getModifiedItems()
        ]
        if not names:
            E5MessageBox.information(
                self, self.tr("Differences"),
                self.tr("""There are no uncommitted changes"""
                        """ available/selected."""))
            return

        if self.diff is None:
            from .SvnDiffDialog import SvnDiffDialog
            self.diff = SvnDiffDialog(self.vcs)
        self.diff.show()
        QApplication.processEvents()
        self.diff.start(names, refreshable=True)

    def __sbsDiff(self):
        """
        Private slot to handle the Side-by-Side Diff context menu entry.
        """
        names = [
            os.path.join(self.dname, itm.text(self.__pathColumn))
            for itm in self.__getModifiedItems()
        ]
        if not names:
            E5MessageBox.information(
                self, self.tr("Side-by-Side Diff"),
                self.tr("""There are no uncommitted changes"""
                        """ available/selected."""))
            return
        elif len(names) > 1:
            E5MessageBox.information(
                self, self.tr("Side-by-Side Diff"),
                self.tr("""Only one file with uncommitted changes"""
                        """ must be selected."""))
            return

        self.vcs.svnSbsDiff(names[0])

    def __lock(self):
        """
        Private slot to handle the Lock context menu entry.
        """
        names = [
            os.path.join(self.dname, itm.text(self.__pathColumn))
            for itm in self.__getLockActionItems(self.unlockedIndicators)
        ]
        if not names:
            E5MessageBox.information(
                self, self.tr("Lock"),
                self.tr("""There are no unlocked files"""
                        """ available/selected."""))
            return

        self.vcs.svnLock(names, parent=self)
        self.on_refreshButton_clicked()

    def __unlock(self):
        """
        Private slot to handle the Unlock context menu entry.
        """
        names = [
            os.path.join(self.dname, itm.text(self.__pathColumn))
            for itm in self.__getLockActionItems(self.lockedIndicators)
        ]
        if not names:
            E5MessageBox.information(
                self, self.tr("Unlock"),
                self.tr("""There are no locked files"""
                        """ available/selected."""))
            return

        self.vcs.svnUnlock(names, parent=self)
        self.on_refreshButton_clicked()

    def __breakLock(self):
        """
        Private slot to handle the Break Lock context menu entry.
        """
        names = [
            os.path.join(self.dname, itm.text(self.__pathColumn))
            for itm in self.__getLockActionItems(self.stealBreakLockIndicators)
        ]
        if not names:
            E5MessageBox.information(
                self, self.tr("Break Lock"),
                self.tr("""There are no locked files"""
                        """ available/selected."""))
            return

        self.vcs.svnUnlock(names, parent=self, breakIt=True)
        self.on_refreshButton_clicked()

    def __stealLock(self):
        """
        Private slot to handle the Break Lock context menu entry.
        """
        names = [
            os.path.join(self.dname, itm.text(self.__pathColumn))
            for itm in self.__getLockActionItems(self.stealBreakLockIndicators)
        ]
        if not names:
            E5MessageBox.information(
                self, self.tr("Steal Lock"),
                self.tr("""There are no locked files"""
                        """ available/selected."""))
            return

        self.vcs.svnLock(names, parent=self, stealIt=True)
        self.on_refreshButton_clicked()

    def __addToChangelist(self):
        """
        Private slot to add entries to a changelist.
        """
        names = [
            os.path.join(self.dname, itm.text(self.__pathColumn))
            for itm in self.__getNonChangelistItems()
        ]
        if not names:
            E5MessageBox.information(
                self, self.tr("Remove from Changelist"),
                self.tr("""There are no files available/selected not """
                        """belonging to a changelist."""))
            return
        self.vcs.svnAddToChangelist(names)
        self.on_refreshButton_clicked()

    def __removeFromChangelist(self):
        """
        Private slot to remove entries from their changelists.
        """
        names = [
            os.path.join(self.dname, itm.text(self.__pathColumn))
            for itm in self.__getChangelistItems()
        ]
        if not names:
            E5MessageBox.information(
                self, self.tr("Remove from Changelist"),
                self.tr("""There are no files available/selected belonging"""
                        """ to a changelist."""))
            return
        self.vcs.svnRemoveFromChangelist(names)
        self.on_refreshButton_clicked()

    def __getCommitableItems(self):
        """
        Private method to retrieve all entries the user wants to commit.
        
        @return list of all items, the user has checked
        """
        commitableItems = []
        for index in range(self.statusList.topLevelItemCount()):
            itm = self.statusList.topLevelItem(index)
            if itm.checkState(self.__toBeCommittedColumn) == Qt.Checked:
                commitableItems.append(itm)
        return commitableItems

    def __getModifiedItems(self):
        """
        Private method to retrieve all entries, that have a modified status.
        
        @return list of all items with a modified status
        """
        modifiedItems = []
        for itm in self.statusList.selectedItems():
            if itm.text(self.__statusColumn) in self.modifiedIndicators or \
               itm.text(self.__propStatusColumn) in self.modifiedIndicators:
                modifiedItems.append(itm)
        return modifiedItems

    def __getUnversionedItems(self):
        """
        Private method to retrieve all entries, that have an unversioned
        status.
        
        @return list of all items with an unversioned status
        """
        unversionedItems = []
        for itm in self.statusList.selectedItems():
            if itm.text(self.__statusColumn) in self.unversionedIndicators:
                unversionedItems.append(itm)
        return unversionedItems

    def __getMissingItems(self):
        """
        Private method to retrieve all entries, that have a missing status.
        
        @return list of all items with a missing status
        """
        missingItems = []
        for itm in self.statusList.selectedItems():
            if itm.text(self.__statusColumn) in self.missingIndicators:
                missingItems.append(itm)
        return missingItems

    def __getLockActionItems(self, indicators):
        """
        Private method to retrieve all emtries, that have a locked status.
        
        @param indicators list of indicators to check against (list of strings)
        @return list of all items with a locked status
        """
        lockitems = []
        for itm in self.statusList.selectedItems():
            if itm.text(self.__lockinfoColumn) in indicators:
                lockitems.append(itm)
        return lockitems

    def __getChangelistItems(self):
        """
        Private method to retrieve all entries, that are members of
        a changelist.
        
        @return list of all items belonging to a changelist
        """
        clitems = []
        for itm in self.statusList.selectedItems():
            if itm.text(self.__changelistColumn) != "":
                clitems.append(itm)
        return clitems

    def __getNonChangelistItems(self):
        """
        Private method to retrieve all entries, that are not members of
        a changelist.
        
        @return list of all items not belonging to a changelist
        """
        clitems = []
        for itm in self.statusList.selectedItems():
            if itm.text(self.__changelistColumn) == "":
                clitems.append(itm)
        return clitems

    def __commitSelect(self, selected):
        """
        Private slot to select or deselect all entries.
        
        @param selected commit selection state to be set (boolean)
        """
        for index in range(self.statusList.topLevelItemCount()):
            itm = self.statusList.topLevelItem(index)
            if itm.flags() & Qt.ItemIsUserCheckable:
                if selected:
                    itm.setCheckState(self.__toBeCommittedColumn, Qt.Checked)
                else:
                    itm.setCheckState(self.__toBeCommittedColumn, Qt.Unchecked)
Beispiel #10
0
class HgTagBranchListDialog(QDialog, Ui_HgTagBranchListDialog):
    """
    Class implementing a dialog to show a list of tags or branches.
    """
    def __init__(self, vcs, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param parent parent widget (QWidget)
        """
        super(HgTagBranchListDialog, self).__init__(parent)
        self.setupUi(self)
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
        
        self.process = QProcess()
        self.vcs = vcs
        self.tagsList = None
        self.allTagsList = None
        self.__hgClient = vcs.getClient()
        
        self.tagList.headerItem().setText(self.tagList.columnCount(), "")
        self.tagList.header().setSortIndicator(3, Qt.AscendingOrder)
        
        self.process.finished.connect(self.__procFinished)
        self.process.readyReadStandardOutput.connect(self.__readStdout)
        self.process.readyReadStandardError.connect(self.__readStderr)
        
        self.show()
        QCoreApplication.processEvents()
    
    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.__hgClient:
            if self.__hgClient.isExecuting():
                self.__hgClient.cancel()
        else:
            if self.process is not None and \
               self.process.state() != QProcess.NotRunning:
                self.process.terminate()
                QTimer.singleShot(2000, self.process.kill)
                self.process.waitForFinished(3000)
        
        e.accept()
    
    def start(self, path, tags, tagsList, allTagsList):
        """
        Public slot to start the tags command.
        
        @param path name of directory to be listed (string)
        @param tags flag indicating a list of tags is requested
            (False = branches, True = tags)
        @param tagsList reference to string list receiving the tags
            (list of strings)
        @param allTagsList reference to string list all tags (list of strings)
        """
        self.errorGroup.hide()
        
        self.intercept = False
        self.tagsMode = tags
        if not tags:
            self.setWindowTitle(self.tr("Mercurial Branches List"))
            self.tagList.headerItem().setText(2, self.tr("Status"))
        self.activateWindow()
        
        self.tagsList = tagsList
        self.allTagsList = allTagsList
        dname, fname = self.vcs.splitPath(path)
        
        # find the root of the repo
        repodir = dname
        while not os.path.isdir(os.path.join(repodir, self.vcs.adminDir)):
            repodir = os.path.dirname(repodir)
            if os.path.splitdrive(repodir)[1] == os.sep:
                return
        
        if self.tagsMode:
            args = self.vcs.initCommand("tags")
            args.append('--verbose')
        else:
            args = self.vcs.initCommand("branches")
            args.append('--closed')
        
        if self.__hgClient:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            
            out, err = self.__hgClient.runcommand(args)
            if err:
                self.__showError(err)
            if out:
                for line in out.splitlines():
                    self.__processOutputLine(line)
                    if self.__hgClient.wasCanceled():
                        break
            self.__finish()
        else:
            self.process.kill()
            self.process.setWorkingDirectory(repodir)
            
            self.process.start('hg', args)
            procStarted = self.process.waitForStarted(5000)
            if not procStarted:
                self.inputGroup.setEnabled(False)
                self.inputGroup.hide()
                E5MessageBox.critical(
                    self,
                    self.tr('Process Generation Error'),
                    self.tr(
                        'The process {0} could not be started. '
                        'Ensure, that it is in the search path.'
                    ).format('hg'))
            else:
                self.inputGroup.setEnabled(True)
                self.inputGroup.show()
    
    def __finish(self):
        """
        Private slot called when the process finished or the user pressed
        the button.
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        self.buttonBox.button(QDialogButtonBox.Close).setFocus(
            Qt.OtherFocusReason)
        
        self.process = None
        
        self.__resizeColumns()
        self.__resort()
    
    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.close()
        elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
            if self.__hgClient:
                self.__hgClient.cancel()
            else:
                self.__finish()
    
    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__finish()
    
    def __resort(self):
        """
        Private method to resort the tree.
        """
        self.tagList.sortItems(
            self.tagList.sortColumn(),
            self.tagList.header().sortIndicatorOrder())
    
    def __resizeColumns(self):
        """
        Private method to resize the list columns.
        """
        self.tagList.header().resizeSections(QHeaderView.ResizeToContents)
        self.tagList.header().setStretchLastSection(True)
    
    def __generateItem(self, revision, changeset, status, name):
        """
        Private method to generate a tag item in the tag list.
        
        @param revision revision of the tag/branch (string)
        @param changeset changeset of the tag/branch (string)
        @param status of the tag/branch (string)
        @param name name of the tag/branch (string)
        """
        itm = QTreeWidgetItem(self.tagList)
        itm.setData(0, Qt.DisplayRole, int(revision))
        itm.setData(1, Qt.DisplayRole, changeset)
        itm.setData(2, Qt.DisplayRole, status)
        itm.setData(3, Qt.DisplayRole, name)
        itm.setTextAlignment(0, Qt.AlignRight)
        itm.setTextAlignment(1, Qt.AlignRight)
        itm.setTextAlignment(2, Qt.AlignHCenter)
    
    def __readStdout(self):
        """
        Private slot to handle the readyReadStdout signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        self.process.setReadChannel(QProcess.StandardOutput)
        
        while self.process.canReadLine():
            s = str(self.process.readLine(), self.vcs.getEncoding(),
                    'replace').strip()
            self.__processOutputLine(s)
    
    def __processOutputLine(self, line):
        """
        Private method to process the lines of output.
        
        @param line output line to be processed (string)
        """
        li = line.split()
        if li[-1][0] in "1234567890":
            # last element is a rev:changeset
            if self.tagsMode:
                status = ""
            else:
                status = self.tr("active")
            rev, changeset = li[-1].split(":", 1)
            del li[-1]
        else:
            if self.tagsMode:
                status = self.tr("yes")
            else:
                status = li[-1][1:-1]
            rev, changeset = li[-2].split(":", 1)
            del li[-2:]
        name = " ".join(li)
        self.__generateItem(rev, changeset, status, name)
        if name not in ["tip", "default"]:
            if self.tagsList is not None:
                self.tagsList.append(name)
            if self.allTagsList is not None:
                self.allTagsList.append(name)
    
    def __readStderr(self):
        """
        Private slot to handle the readyReadStderr signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            s = str(self.process.readAllStandardError(),
                    self.vcs.getEncoding(), 'replace')
            self.__showError(s)
    
    def __showError(self, out):
        """
        Private slot to show some error.
        
        @param out error to be shown (string)
        """
        self.errorGroup.show()
        self.errors.insertPlainText(out)
        self.errors.ensureCursorVisible()
    
    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)
    
    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the subversion process.
        """
        input = self.input.text()
        input += os.linesep
        
        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(input)
            self.errors.ensureCursorVisible()
        
        self.process.write(input)
        
        self.passwordCheckBox.setChecked(False)
        self.input.clear()
    
    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()
    
    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(HgTagBranchListDialog, self).keyPressEvent(evt)
Beispiel #11
0
class HgShelveBrowserDialog(QWidget, Ui_HgShelveBrowserDialog):
    """
    Class implementing Mercurial shelve browser dialog.
    """
    NameColumn = 0
    AgeColumn = 1
    MessageColumn = 2
    
    def __init__(self, vcs, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param parent parent widget (QWidget)
        """
        super(HgShelveBrowserDialog, self).__init__(parent)
        self.setupUi(self)
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
        
        self.__position = QPoint()
        
        self.__fileStatisticsRole = Qt.UserRole
        self.__totalStatisticsRole = Qt.UserRole + 1
        
        self.shelveList.header().setSortIndicator(0, Qt.AscendingOrder)
        
        self.refreshButton = self.buttonBox.addButton(
            self.tr("&Refresh"), QDialogButtonBox.ActionRole)
        self.refreshButton.setToolTip(
            self.tr("Press to refresh the list of shelves"))
        self.refreshButton.setEnabled(False)
        
        self.vcs = vcs
        self.__hgClient = vcs.getClient()
        self.__resetUI()
        
        if self.__hgClient:
            self.process = None
        else:
            self.process = QProcess()
            self.process.finished.connect(self.__procFinished)
            self.process.readyReadStandardOutput.connect(self.__readStdout)
            self.process.readyReadStandardError.connect(self.__readStderr)
        
        self.__contextMenu = QMenu()
        self.__unshelveAct = self.__contextMenu.addAction(
            self.tr("Restore selected shelve"), self.__unshelve)
        self.__deleteAct = self.__contextMenu.addAction(
            self.tr("Delete selected shelves"), self.__deleteShelves)
        self.__contextMenu.addAction(
            self.tr("Delete all shelves"), self.__cleanupShelves)
    
    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.__hgClient:
            if self.__hgClient.isExecuting():
                self.__hgClient.cancel()
        else:
            if self.process is not None and \
               self.process.state() != QProcess.NotRunning:
                self.process.terminate()
                QTimer.singleShot(2000, self.process.kill)
                self.process.waitForFinished(3000)
        
        self.__position = self.pos()
        
        e.accept()
    
    def show(self):
        """
        Public slot to show the dialog.
        """
        if not self.__position.isNull():
            self.move(self.__position)
        self.__resetUI()
        
        super(HgShelveBrowserDialog, self).show()
    
    def __resetUI(self):
        """
        Private method to reset the user interface.
        """
        self.shelveList.clear()
    
    def __resizeColumnsShelves(self):
        """
        Private method to resize the shelve list columns.
        """
        self.shelveList.header().resizeSections(QHeaderView.ResizeToContents)
        self.shelveList.header().setStretchLastSection(True)
    
    def __generateShelveEntry(self, name, age, message, fileStatistics,
                              totals):
        """
        Private method to generate the shelve items.
        
        @param name name of the shelve (string)
        @param age age of the shelve (string)
        @param message shelve message (string)
        @param fileStatistics per file change statistics (tuple of
            four strings with file name, number of changes, number of
            added lines and number of deleted lines)
        @param totals overall statistics (tuple of three strings with
            number of changed files, number of added lines and number
            of deleted lines)
        """
        itm = QTreeWidgetItem(self.shelveList, [name, age, message])
        itm.setData(0, self.__fileStatisticsRole, fileStatistics)
        itm.setData(0, self.__totalStatisticsRole, totals)
    
    def __getShelveEntries(self):
        """
        Private method to retrieve the list of shelves.
        """
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
        QApplication.processEvents()
        
        QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
        QApplication.processEvents()
        
        self.buf = []
        self.errors.clear()
        self.intercept = False
        
        args = self.vcs.initCommand("shelve")
        args.append("--list")
        args.append("--stat")
        
        if self.__hgClient:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            
            out, err = self.__hgClient.runcommand(args)
            self.buf = out.splitlines(True)
            if err:
                self.__showError(err)
            self.__processBuffer()
            self.__finish()
        else:
            self.process.kill()
            
            self.process.setWorkingDirectory(self.repodir)
            
            self.inputGroup.setEnabled(True)
            self.inputGroup.show()
            
            self.process.start('hg', args)
            procStarted = self.process.waitForStarted(5000)
            if not procStarted:
                self.inputGroup.setEnabled(False)
                self.inputGroup.hide()
                E5MessageBox.critical(
                    self,
                    self.tr('Process Generation Error'),
                    self.tr(
                        'The process {0} could not be started. '
                        'Ensure, that it is in the search path.'
                    ).format('hg'))
    
    def start(self, projectDir):
        """
        Public slot to start the hg shelve command.
        
        @param projectDir name of the project directory (string)
        """
        self.errorGroup.hide()
        QApplication.processEvents()
        
        self.__projectDir = projectDir
        
        # find the root of the repo
        self.repodir = self.__projectDir
        while not os.path.isdir(os.path.join(self.repodir, self.vcs.adminDir)):
            self.repodir = os.path.dirname(self.repodir)
            if os.path.splitdrive(self.repodir)[1] == os.sep:
                return
        
        self.activateWindow()
        self.raise_()
        
        self.shelveList.clear()
        self.__started = True
        self.__getShelveEntries()
    
    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__processBuffer()
        self.__finish()
    
    def __finish(self):
        """
        Private slot called when the process finished or the user pressed
        the button.
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        QApplication.restoreOverrideCursor()
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        
        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()
        self.refreshButton.setEnabled(True)
    
    def __processBuffer(self):
        """
        Private method to process the buffered output of the hg shelve command.
        """
        lastWasFileStats = False
        firstLine = True
        itemData = {}
        for line in self.buf:
            if firstLine:
                name, line = line.split("(", 1)
                age, message = line.split(")", 1)
                itemData["name"] = name.strip()
                itemData["age"] = age.strip()
                itemData["message"] = message.strip()
                itemData["files"] = []
                firstLine = False
            elif '|' in line:
                # file stats: foo.py |  3 ++-
                file, changes = line.strip().split("|", 1)
                if changes.strip().endswith(("+", "-")):
                    total, addDelete = changes.strip().split(None, 1)
                    additions = str(addDelete.count("+"))
                    deletions = str(addDelete.count("-"))
                else:
                    total = changes.strip()
                    additions = '0'
                    deletions = '0'
                itemData["files"].append((file, total, additions, deletions))
                lastWasFileStats = True
            elif lastWasFileStats:
                # summary line
                # 2 files changed, 15 insertions(+), 1 deletions(-)
                total, added, deleted = line.strip().split(",", 2)
                total = total.split()[0]
                added = added.split()[0]
                deleted = deleted.split()[0]
                itemData["summary"] = (total, added, deleted)
                
                self.__generateShelveEntry(
                    itemData["name"], itemData["age"], itemData["message"],
                    itemData["files"], itemData["summary"])
                
                lastWasFileStats = False
                firstLine = True
                itemData = {}
        
        self.__resizeColumnsShelves()
        
        if self.__started:
            self.shelveList.setCurrentItem(self.shelveList.topLevelItem(0))
            self.__started = False
    
    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.
        
        It reads the output of the process and inserts it into a buffer.
        """
        self.process.setReadChannel(QProcess.StandardOutput)
        
        while self.process.canReadLine():
            line = str(self.process.readLine(), self.vcs.getEncoding(),
                       'replace')
            self.buf.append(line)
    
    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            s = str(self.process.readAllStandardError(),
                    self.vcs.getEncoding(), 'replace')
            self.__showError(s)
    
    def __showError(self, out):
        """
        Private slot to show some error.
        
        @param out error to be shown (string)
        """
        self.errorGroup.show()
        self.errors.insertPlainText(out)
        self.errors.ensureCursorVisible()
    
    @pyqtSlot(QAbstractButton)
    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.close()
        elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
            self.cancelled = True
            if self.__hgClient:
                self.__hgClient.cancel()
            else:
                self.__finish()
        elif button == self.refreshButton:
            self.on_refreshButton_clicked()
    
    @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem)
    def on_shelveList_currentItemChanged(self, current, previous):
        """
        Private slot called, when the current item of the shelve list changes.
        
        @param current reference to the new current item (QTreeWidgetItem)
        @param previous reference to the old current item (QTreeWidgetItem)
        """
        self.statisticsList.clear()
        if current:
            for dataSet in current.data(0, self.__fileStatisticsRole):
                QTreeWidgetItem(self.statisticsList, list(dataSet))
            self.statisticsList.header().resizeSections(
                QHeaderView.ResizeToContents)
            self.statisticsList.header().setStretchLastSection(True)
            
            totals = current.data(0, self.__totalStatisticsRole)
            self.filesLabel.setText(
                self.tr("%n file(s) changed", None, int(totals[0])))
            self.insertionsLabel.setText(
                self.tr("%n line(s) inserted", None, int(totals[1])))
            self.deletionsLabel.setText(
                self.tr("%n line(s) deleted", None, int(totals[2])))
        else:
            self.filesLabel.setText("")
            self.insertionsLabel.setText("")
            self.deletionsLabel.setText("")
    
    @pyqtSlot(QPoint)
    def on_shelveList_customContextMenuRequested(self, pos):
        """
        Private slot to show the context menu of the shelve list.
        
        @param pos position of the mouse pointer (QPoint)
        """
        selectedItemsCount = len(self.shelveList.selectedItems())
        self.__unshelveAct.setEnabled(selectedItemsCount == 1)
        self.__deleteAct.setEnabled(selectedItemsCount > 0)
        
        self.__contextMenu.popup(self.mapToGlobal(pos))
    
    @pyqtSlot()
    def on_refreshButton_clicked(self):
        """
        Private slot to refresh the list of shelves.
        """
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
        
        self.inputGroup.setEnabled(True)
        self.inputGroup.show()
        self.refreshButton.setEnabled(False)
        
        self.start(self.__projectDir)
    
    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)
    
    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the mercurial process.
        """
        input = self.input.text()
        input += os.linesep
        
        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(input)
            self.errors.ensureCursorVisible()
        self.errorGroup.show()
        
        self.process.write(input)
        
        self.passwordCheckBox.setChecked(False)
        self.input.clear()
    
    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()
    
    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(HgShelveBrowserDialog, self).keyPressEvent(evt)
    
    def __unshelve(self):
        """
        Private slot to restore the selected shelve of changes.
        """
        itm = self.shelveList.selectedItems()[0]
        if itm is not None:
            name = itm.text(self.NameColumn)
            self.vcs.getExtensionObject("shelve")\
                .hgUnshelve(self.__projectDir, shelveName=name)
            self.on_refreshButton_clicked()
    
    def __deleteShelves(self):
        """
        Private slot to delete the selected shelves.
        """
        shelveNames = []
        for itm in self.shelveList.selectedItems():
            shelveNames.append(itm.text(self.NameColumn))
        if shelveNames:
            self.vcs.getExtensionObject("shelve")\
                .hgDeleteShelves(self.__projectDir, shelveNames=shelveNames)
            self.on_refreshButton_clicked()
    
    def __cleanupShelves(self):
        """
        Private slot to delete all shelves.
        """
        self.vcs.getExtensionObject("shelve")\
            .hgCleanupShelves(self.__projectDir)
        self.on_refreshButton_clicked()
Beispiel #12
0
class HgSummaryDialog(QDialog, Ui_HgSummaryDialog):
    """
    Class implementing a dialog to show some summary information of the working
    directory state.
    """
    def __init__(self, vcs, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param parent parent widget (QWidget)
        """
        super(HgSummaryDialog, self).__init__(parent)
        self.setupUi(self)
        
        self.refreshButton = self.buttonBox.addButton(
            self.tr("Refresh"), QDialogButtonBox.ActionRole)
        self.refreshButton.setToolTip(
            self.tr("Press to refresh the summary display"))
        self.refreshButton.setEnabled(False)
        
        self.process = None
        self.vcs = vcs
        self.vcs.committed.connect(self.__committed)
    
    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        e.accept()
    
    def start(self, path, mq=False, largefiles=False):
        """
        Public slot to start the hg summary command.
        
        @param path path name of the working directory (string)
        @param mq flag indicating to show the queue status as well (boolean)
        @param largefiles flag indicating to show the largefiles status as
            well (boolean)
        """
        self.errorGroup.hide()
        self.__path = path
        self.__mq = mq
        self.__largefiles = largefiles
        
        args = self.vcs.initCommand("summary")
        if self.vcs.canPull():
            args.append("--remote")
        if self.__mq:
            args.append("--mq")
        if self.__largefiles:
            args.append("--large")
        
        # find the root of the repo
        repodir = self.__path
        while not os.path.isdir(os.path.join(repodir, self.vcs.adminDir)):
            repodir = os.path.dirname(repodir)
            if os.path.splitdrive(repodir)[1] == os.sep:
                return
        
        if self.process:
            self.process.kill()
        else:
            self.process = QProcess()
            prepareProcess(self.process, language="C")
            self.process.finished.connect(self.__procFinished)
            self.process.readyReadStandardOutput.connect(self.__readStdout)
            self.process.readyReadStandardError.connect(self.__readStderr)
        
        self.process.setWorkingDirectory(repodir)
        
        self.__buffer = []
        
        self.process.start('hg', args)
        procStarted = self.process.waitForStarted(5000)
        if not procStarted:
            E5MessageBox.critical(
                self,
                self.tr('Process Generation Error'),
                self.tr(
                    'The process {0} could not be started. '
                    'Ensure, that it is in the search path.'
                ).format('hg'))
    
    def __finish(self):
        """
        Private slot called when the process finished or the user pressed
        the button.
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        self.refreshButton.setEnabled(True)
        self.process = None
    
    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.close()
        elif button == self.refreshButton:
            self.on_refreshButton_clicked()
    
    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__processOutput(self.__buffer)
        self.__finish()
    
    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        if self.process is not None:
            self.process.setReadChannel(QProcess.StandardOutput)
            
            while self.process.canReadLine():
                line = str(self.process.readLine(), self.vcs.getEncoding(),
                           'replace')
                self.__buffer.append(line)
    
    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            s = str(self.process.readAllStandardError(),
                    self.vcs.getEncoding(), 'replace')
            self.__showError(s)
    
    def __showError(self, out):
        """
        Private slot to show some error.
        
        @param out error to be shown (string)
        """
        self.errorGroup.show()
        self.errors.insertPlainText(out)
        self.errors.ensureCursorVisible()
    
    @pyqtSlot()
    def on_refreshButton_clicked(self):
        """
        Private slot to refresh the status display.
        """
        self.refreshButton.setEnabled(False)
        self.summary.clear()
        
        self.start(self.__path, mq=self.__mq)
    
    def __committed(self):
        """
        Private slot called after the commit has finished.
        """
        if self.isVisible():
            self.on_refreshButton_clicked()
    
    def __processOutput(self, output):
        """
        Private method to process the output into nice readable text.
        
        @param output output from the summary command (string)
        """
        infoDict = {}
        
        # step 1: parse the output
        while output:
            line = output.pop(0)
            if ':' not in line:
                continue
            name, value = line.split(": ", 1)
            value = value.strip()
            
            if name == "parent":
                if " " in value:
                    parent, tags = value.split(" ", 1)
                else:
                    parent = value
                    tags = ""
                rev, node = parent.split(":")
                
                remarks = []
                if tags:
                    if " (empty repository)" in tags:
                        remarks.append("@EMPTY@")
                        tags = tags.replace(" (empty repository)", "")
                    if " (no revision checked out)" in tags:
                        remarks.append("@NO_REVISION@")
                        tags = tags.replace(" (no revision checked out)", "")
                else:
                    tags = None
                
                value = infoDict.get(name, [])
                
                if rev == "-1":
                    value.append((int(rev), node, tags, None, remarks))
                else:
                    message = output.pop(0).strip()
                    value.append((int(rev), node, tags, message, remarks))
            elif name == "branch":
                pass
            elif name == "bookmarks":
                pass
            elif name == "commit":
                stateDict = {}
                if "(" in value:
                    if value.startswith("("):
                        states = ""
                        remark = value[1:-1]
                    else:
                        states, remark = value.rsplit(" (", 1)
                        remark = remark[:-1]
                else:
                    states = value
                    remark = ""
                states = states.split(", ")
                for state in states:
                    if state:
                        count, category = state.split(" ")
                        stateDict[category] = count
                value = (stateDict, remark)
            elif name == "update":
                if value.endswith("(current)"):
                    value = ("@CURRENT@", 0, 0)
                elif value.endswith("(update)"):
                    value = ("@UPDATE@", int(value.split(" ", 1)[0]), 0)
                elif value.endswith("(merge)"):
                    parts = value.split(", ")
                    value = ("@MERGE@", int(parts[0].split(" ", 1)[0]),
                             int(parts[1].split(" ", 1)[0]))
                else:
                    value = ("@UNKNOWN@", 0, 0)
            elif name == "remote":
                if value == "(synced)":
                    value = (0, 0, 0, 0)
                else:
                    inc = incb = outg = outgb = 0
                    for val in value.split(", "):
                        count, category = val.split(" ", 1)
                        if category == "outgoing":
                            outg = int(count)
                        elif category.endswith("incoming"):
                            inc = int(count)
                        elif category == "incoming bookmarks":
                            incb = int(count)
                        elif category == "outgoing bookmarks":
                            outgb = int(count)
                    value = (inc, outg, incb, outgb)
            elif name == "mq":
                if value == "(empty queue)":
                    value = (0, 0)
                else:
                    applied = unapplied = 0
                    for val in value.split(", "):
                        count, category = val.split(" ", 1)
                        if category == "applied":
                            applied = int(count)
                        elif category == "unapplied":
                            unapplied = int(count)
                    value = (applied, unapplied)
            elif name == "largefiles":
                if not value[0].isdigit():
                    value = 0
                else:
                    value = int(value.split(None, 1)[0])
            else:
                # ignore unknown entries
                continue
            
            infoDict[name] = value
        
        # step 2: build the output
        if infoDict:
            info = ["<table>"]
            pindex = 0
            for rev, node, tags, message, remarks in infoDict["parent"]:
                pindex += 1
                changeset = "{0}:{1}".format(rev, node)
                if len(infoDict["parent"]) > 1:
                    info.append(self.tr(
                        "<tr><td><b>Parent #{0}</b></td><td>{1}</td></tr>")
                        .format(pindex, changeset))
                else:
                    info.append(self.tr(
                        "<tr><td><b>Parent</b></td><td>{0}</td></tr>")
                        .format(changeset))
                if tags:
                    info.append(self.tr(
                        "<tr><td><b>Tags</b></td><td>{0}</td></tr>")
                        .format('<br/>'.join(tags.split())))
                if message:
                    info.append(self.tr(
                        "<tr><td><b>Commit Message</b></td><td>{0}</td></tr>")
                        .format(message))
                if remarks:
                    rem = []
                    if "@EMPTY@" in remarks:
                        rem.append(self.tr("empty repository"))
                    if "@NO_REVISION@" in remarks:
                        rem.append(self.tr("no revision checked out"))
                    info.append(self.tr(
                        "<tr><td><b>Remarks</b></td><td>{0}</td></tr>")
                        .format(", ".join(rem)))
            if "branch" in infoDict:
                info.append(self.tr(
                    "<tr><td><b>Branch</b></td><td>{0}</td></tr>")
                    .format(infoDict["branch"]))
            if "bookmarks" in infoDict:
                bookmarks = infoDict["bookmarks"].split()
                for i in range(len(bookmarks)):
                    if bookmarks[i].startswith("*"):
                        bookmarks[i] = "<b>{0}</b>".format(bookmarks[i])
                info.append(self.tr(
                    "<tr><td><b>Bookmarks</b></td><td>{0}</td></tr>")
                    .format('<br/>'.join(bookmarks)))
            if "commit" in infoDict:
                cinfo = []
                for category, count in infoDict["commit"][0].items():
                    if category == "modified":
                        cinfo.append(self.tr("{0} modified").format(count))
                    elif category == "added":
                        cinfo.append(self.tr("{0} added").format(count))
                    elif category == "removed":
                        cinfo.append(self.tr("{0} removed").format(count))
                    elif category == "renamed":
                        cinfo.append(self.tr("{0} renamed").format(count))
                    elif category == "copied":
                        cinfo.append(self.tr("{0} copied").format(count))
                    elif category == "deleted":
                        cinfo.append(self.tr("{0} deleted").format(count))
                    elif category == "unknown":
                        cinfo.append(self.tr("{0} unknown").format(count))
                    elif category == "ignored":
                        cinfo.append(self.tr("{0} ignored").format(count))
                    elif category == "unresolved":
                        cinfo.append(
                            self.tr("{0} unresolved").format(count))
                    elif category == "subrepos":
                        cinfo.append(self.tr("{0} subrepos").format(count))
                remark = infoDict["commit"][1]
                if remark == "merge":
                    cinfo.append(self.tr("Merge needed"))
                elif remark == "new branch":
                    cinfo.append(self.tr("New Branch"))
                elif remark == "head closed":
                    cinfo.append(self.tr("Head is closed"))
                elif remark == "clean":
                    cinfo.append(self.tr("No commit required"))
                elif remark == "new branch head":
                    cinfo.append(self.tr("New Branch Head"))
                info.append(self.tr(
                    "<tr><td><b>Commit Status</b></td><td>{0}</td></tr>")
                    .format("<br/>".join(cinfo)))
            if "update" in infoDict:
                if infoDict["update"][0] == "@CURRENT@":
                    uinfo = self.tr("current")
                elif infoDict["update"][0] == "@UPDATE@":
                    uinfo = self.tr(
                        "%n new changeset(s)<br/>Update required", "",
                        infoDict["update"][1])
                elif infoDict["update"][0] == "@MERGE@":
                    uinfo1 = self.tr(
                        "%n new changeset(s)", "", infoDict["update"][1])
                    uinfo2 = self.tr(
                        "%n branch head(s)", "", infoDict["update"][2])
                    uinfo = self.tr(
                        "{0}<br/>{1}<br/>Merge required",
                        "0 is changesets, 1 is branch heads")\
                        .format(uinfo1, uinfo2)
                else:
                    uinfo = self.tr("unknown status")
                info.append(self.tr(
                    "<tr><td><b>Update Status</b></td><td>{0}</td></tr>")
                    .format(uinfo))
            if "remote" in infoDict:
                if infoDict["remote"] == (0, 0, 0, 0):
                    rinfo = self.tr("synched")
                else:
                    li = []
                    if infoDict["remote"][0]:
                        li.append(self.tr("1 or more incoming"))
                    if infoDict["remote"][1]:
                        li.append(self.tr("{0} outgoing")
                                  .format(infoDict["remote"][1]))
                    if infoDict["remote"][2]:
                        li.append(self.tr("%n incoming bookmark(s)", "",
                                  infoDict["remote"][2]))
                    if infoDict["remote"][3]:
                        li.append(self.tr("%n outgoing bookmark(s)", "",
                                  infoDict["remote"][3]))
                    rinfo = "<br/>".join(li)
                info.append(self.tr(
                    "<tr><td><b>Remote Status</b></td><td>{0}</td></tr>")
                    .format(rinfo))
            if "mq" in infoDict:
                if infoDict["mq"] == (0, 0):
                    qinfo = self.tr("empty queue")
                else:
                    li = []
                    if infoDict["mq"][0]:
                        li.append(self.tr("{0} applied")
                                  .format(infoDict["mq"][0]))
                    if infoDict["mq"][1]:
                        li.append(self.tr("{0} unapplied")
                                  .format(infoDict["mq"][1]))
                    qinfo = "<br/>".join(li)
                info.append(self.tr(
                    "<tr><td><b>Queues Status</b></td><td>{0}</td></tr>")
                    .format(qinfo))
            if "largefiles" in infoDict:
                if infoDict["largefiles"] == 0:
                    lfInfo = self.tr("No files to upload")
                else:
                    lfInfo = self.tr("%n file(s) to upload", "",
                                     infoDict["largefiles"])
                info.append(self.tr(
                    "<tr><td><b>Large Files</b></td><td>{0}</td></tr>")
                    .format(lfInfo))
            info.append("</table>")
        else:
            info = [self.tr("<p>No status information available.</p>")]
        
        self.summary.insertHtml("\n".join(info))
Beispiel #13
0
class HgStatusDialog(QWidget, Ui_HgStatusDialog):
    """
    Class implementing a dialog to show the output of the hg status command
    process.
    """

    def __init__(self, vcs, mq=False, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param mq flag indicating to show a queue repo status (boolean)
        @param parent parent widget (QWidget)
        """
        super(HgStatusDialog, self).__init__(parent)
        self.setupUi(self)

        self.__toBeCommittedColumn = 0
        self.__statusColumn = 1
        self.__pathColumn = 2
        self.__lastColumn = self.statusList.columnCount()

        self.refreshButton = self.buttonBox.addButton(self.tr("Refresh"), QDialogButtonBox.ActionRole)
        self.refreshButton.setToolTip(self.tr("Press to refresh the status display"))
        self.refreshButton.setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)

        self.diff = None
        self.vcs = vcs
        self.vcs.committed.connect(self.__committed)
        self.__hgClient = self.vcs.getClient()
        self.__mq = mq
        if self.__hgClient:
            self.process = None
        else:
            self.process = QProcess()
            self.process.finished.connect(self.__procFinished)
            self.process.readyReadStandardOutput.connect(self.__readStdout)
            self.process.readyReadStandardError.connect(self.__readStderr)

        self.diffSplitter.setSizes([350, 250])
        self.__diffSplitterState = None

        self.statusList.headerItem().setText(self.__lastColumn, "")
        self.statusList.header().setSortIndicator(self.__pathColumn, Qt.AscendingOrder)

        font = Preferences.getEditorOtherFonts("MonospacedFont")
        self.diffEdit.setFontFamily(font.family())
        self.diffEdit.setFontPointSize(font.pointSize())

        self.diffHighlighter = HgDiffHighlighter(self.diffEdit.document())
        self.__diffGenerator = HgDiffGenerator(vcs, self)
        self.__diffGenerator.finished.connect(self.__generatorFinished)

        self.__selectedName = ""

        if mq:
            self.buttonsLine.setVisible(False)
            self.addButton.setVisible(False)
            self.diffButton.setVisible(False)
            self.sbsDiffButton.setVisible(False)
            self.revertButton.setVisible(False)
            self.forgetButton.setVisible(False)
            self.restoreButton.setVisible(False)

            self.diffEdit.setVisible(False)

        self.menuactions = []
        self.lfActions = []
        self.menu = QMenu()
        if not mq:
            self.__commitAct = self.menu.addAction(self.tr("Commit changes to repository..."), self.__commit)
            self.menuactions.append(self.__commitAct)
            self.menuactions.append(self.menu.addAction(self.tr("Select all for commit"), self.__commitSelectAll))
            self.menuactions.append(self.menu.addAction(self.tr("Deselect all from commit"), self.__commitDeselectAll))
            self.menu.addSeparator()
            self.__addAct = self.menu.addAction(self.tr("Add to repository"), self.__add)
            self.menuactions.append(self.__addAct)
            if self.vcs.version >= (2, 0):
                self.lfActions.append(self.menu.addAction(self.tr("Add as Large File"), lambda: self.__lfAdd("large")))
                self.lfActions.append(
                    self.menu.addAction(self.tr("Add as Normal File"), lambda: self.__lfAdd("normal"))
                )
            self.__diffAct = self.menu.addAction(self.tr("Show differences"), self.__diff)
            self.menuactions.append(self.__diffAct)
            self.__sbsDiffAct = self.menu.addAction(self.tr("Show differences side-by-side"), self.__sbsDiff)
            self.menuactions.append(self.__sbsDiffAct)
            self.__revertAct = self.menu.addAction(self.tr("Revert changes"), self.__revert)
            self.menuactions.append(self.__revertAct)
            self.__forgetAct = self.menu.addAction(self.tr("Forget missing"), self.__forget)
            self.menuactions.append(self.__forgetAct)
            self.__restoreAct = self.menu.addAction(self.tr("Restore missing"), self.__restoreMissing)
            self.menuactions.append(self.__restoreAct)
            self.menu.addSeparator()
            self.menuactions.append(self.menu.addAction(self.tr("Adjust column sizes"), self.__resizeColumns))
            for act in self.menuactions:
                act.setEnabled(False)
            for act in self.lfActions:
                act.setEnabled(False)

            self.statusList.setContextMenuPolicy(Qt.CustomContextMenu)
            self.statusList.customContextMenuRequested.connect(self.__showContextMenu)

        if not mq and self.vcs.version >= (2, 0):
            self.__lfAddActions = []
            self.__addButtonMenu = QMenu()
            self.__addButtonMenu.addAction(self.tr("Add"), self.__add)
            self.__lfAddActions.append(
                self.__addButtonMenu.addAction(self.tr("Add as Large File"), lambda: self.__lfAdd("large"))
            )
            self.__lfAddActions.append(
                self.__addButtonMenu.addAction(self.tr("Add as Normal File"), lambda: self.__lfAdd("normal"))
            )
            self.addButton.setMenu(self.__addButtonMenu)
            self.__addButtonMenu.aboutToShow.connect(self.__showAddMenu)

        self.modifiedIndicators = [self.tr("added"), self.tr("modified"), self.tr("removed")]

        self.unversionedIndicators = [self.tr("not tracked")]

        self.missingIndicators = [self.tr("missing")]

        self.status = {
            "A": self.tr("added"),
            "C": self.tr("normal"),
            "I": self.tr("ignored"),
            "M": self.tr("modified"),
            "R": self.tr("removed"),
            "?": self.tr("not tracked"),
            "!": self.tr("missing"),
        }

    def show(self):
        """
        Public slot to show the dialog.
        """
        super(HgStatusDialog, self).show()

        if not self.__mq and self.__diffSplitterState:
            self.diffSplitter.restoreState(self.__diffSplitterState)

    def __resort(self):
        """
        Private method to resort the tree.
        """
        self.statusList.sortItems(self.statusList.sortColumn(), self.statusList.header().sortIndicatorOrder())

    def __resizeColumns(self):
        """
        Private method to resize the list columns.
        """
        self.statusList.header().resizeSections(QHeaderView.ResizeToContents)
        self.statusList.header().setStretchLastSection(True)

    def __generateItem(self, status, path):
        """
        Private method to generate a status item in the status list.
        
        @param status status indicator (string)
        @param path path of the file or directory (string)
        """
        statusText = self.status[status]
        itm = QTreeWidgetItem(self.statusList, ["", statusText, path])

        itm.setTextAlignment(1, Qt.AlignHCenter)
        itm.setTextAlignment(2, Qt.AlignLeft)

        if status in "AMR":
            itm.setFlags(itm.flags() | Qt.ItemIsUserCheckable)
            itm.setCheckState(self.__toBeCommittedColumn, Qt.Checked)
        else:
            itm.setFlags(itm.flags() & ~Qt.ItemIsUserCheckable)

        if statusText not in self.__statusFilters:
            self.__statusFilters.append(statusText)

    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.__hgClient:
            if self.__hgClient.isExecuting():
                self.__hgClient.cancel()
        else:
            if self.process is not None and self.process.state() != QProcess.NotRunning:
                self.process.terminate()
                QTimer.singleShot(2000, self.process.kill)
                self.process.waitForFinished(3000)

        if not self.__mq:
            self.__diffSplitterState = self.diffSplitter.saveState()

        e.accept()

    def start(self, fn):
        """
        Public slot to start the hg status command.
        
        @param fn filename(s)/directoryname(s) to show the status of
            (string or list of strings)
        """
        self.errorGroup.hide()
        self.intercept = False
        self.args = fn

        for act in self.menuactions:
            act.setEnabled(False)
        for act in self.lfActions:
            act.setEnabled(False)

        self.addButton.setEnabled(False)
        self.commitButton.setEnabled(False)
        self.diffButton.setEnabled(False)
        self.sbsDiffButton.setEnabled(False)
        self.revertButton.setEnabled(False)
        self.forgetButton.setEnabled(False)
        self.restoreButton.setEnabled(False)

        self.statusFilterCombo.clear()
        self.__statusFilters = []
        self.statusList.clear()

        if self.__mq:
            self.setWindowTitle(self.tr("Mercurial Queue Repository Status"))
        else:
            self.setWindowTitle(self.tr("Mercurial Status"))

        args = self.vcs.initCommand("status")
        if self.__mq:
            args.append("--mq")
            if isinstance(fn, list):
                self.dname, fnames = self.vcs.splitPathList(fn)
            else:
                self.dname, fname = self.vcs.splitPath(fn)
        else:
            if self.vcs.hasSubrepositories():
                args.append("--subrepos")

            if isinstance(fn, list):
                self.dname, fnames = self.vcs.splitPathList(fn)
                self.vcs.addArguments(args, fn)
            else:
                self.dname, fname = self.vcs.splitPath(fn)
                args.append(fn)

        # find the root of the repo
        repodir = self.dname
        while not os.path.isdir(os.path.join(repodir, self.vcs.adminDir)):
            repodir = os.path.dirname(repodir)
            if os.path.splitdrive(repodir)[1] == os.sep:
                return

        if self.__hgClient:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            self.refreshButton.setEnabled(False)

            out, err = self.__hgClient.runcommand(args)
            if err:
                self.__showError(err)
            if out:
                for line in out.splitlines():
                    self.__processOutputLine(line)
                    if self.__hgClient.wasCanceled():
                        break
            self.__finish()
        else:
            if self.process:
                self.process.kill()

            self.process.setWorkingDirectory(repodir)

            self.process.start("hg", args)
            procStarted = self.process.waitForStarted(5000)
            if not procStarted:
                self.inputGroup.setEnabled(False)
                self.inputGroup.hide()
                E5MessageBox.critical(
                    self,
                    self.tr("Process Generation Error"),
                    self.tr("The process {0} could not be started. " "Ensure, that it is in the search path.").format(
                        "hg"
                    ),
                )
            else:
                self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
                self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
                self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)

                self.inputGroup.setEnabled(True)
                self.inputGroup.show()
                self.refreshButton.setEnabled(False)

    def __finish(self):
        """
        Private slot called when the process finished or the user pressed
        the button.
        """
        if self.process is not None and self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)

        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()
        self.refreshButton.setEnabled(True)

        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        self.buttonBox.button(QDialogButtonBox.Close).setFocus(Qt.OtherFocusReason)

        self.__statusFilters.sort()
        self.__statusFilters.insert(0, "<{0}>".format(self.tr("all")))
        self.statusFilterCombo.addItems(self.__statusFilters)

        for act in self.menuactions:
            act.setEnabled(True)

        self.__resort()
        self.__resizeColumns()

        self.__updateButtons()
        self.__updateCommitButton()

        self.__refreshDiff()

    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.close()
        elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
            if self.__hgClient:
                self.__hgClient.cancel()
            else:
                self.__finish()
        elif button == self.refreshButton:
            self.on_refreshButton_clicked()

    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__finish()

    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        if self.process is not None:
            self.process.setReadChannel(QProcess.StandardOutput)

            while self.process.canReadLine():
                line = str(self.process.readLine(), self.vcs.getEncoding(), "replace")
                self.__processOutputLine(line)

    def __processOutputLine(self, line):
        """
        Private method to process the lines of output.
        
        @param line output line to be processed (string)
        """
        if line[0] in "ACIMR?!" and line[1] == " ":
            status, path = line.strip().split(" ", 1)
            self.__generateItem(status, path)

    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            s = str(self.process.readAllStandardError(), self.vcs.getEncoding(), "replace")
            self.__showError(s)

    def __showError(self, out):
        """
        Private slot to show some error.
        
        @param out error to be shown (string)
        """
        self.errorGroup.show()
        self.errors.insertPlainText(out)
        self.errors.ensureCursorVisible()

    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)

    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the subversion process.
        """
        input = self.input.text()
        input += os.linesep

        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(input)
            self.errors.ensureCursorVisible()

        self.process.write(input)

        self.passwordCheckBox.setChecked(False)
        self.input.clear()

    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()

    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(HgStatusDialog, self).keyPressEvent(evt)

    @pyqtSlot()
    def on_refreshButton_clicked(self):
        """
        Private slot to refresh the status display.
        """
        selectedItems = self.statusList.selectedItems()
        if len(selectedItems) == 1:
            self.__selectedName = selectedItems[0].text(self.__pathColumn)
        else:
            self.__selectedName = ""

        self.start(self.args)

    def __updateButtons(self):
        """
        Private method to update the VCS buttons status.
        """
        modified = len(self.__getModifiedItems())
        unversioned = len(self.__getUnversionedItems())
        missing = len(self.__getMissingItems())

        self.addButton.setEnabled(unversioned)
        self.diffButton.setEnabled(modified)
        self.sbsDiffButton.setEnabled(modified == 1)
        self.revertButton.setEnabled(modified)
        self.forgetButton.setEnabled(missing)
        self.restoreButton.setEnabled(missing)

    def __updateCommitButton(self):
        """
        Private method to update the Commit button status.
        """
        commitable = len(self.__getCommitableItems())
        self.commitButton.setEnabled(commitable)

    @pyqtSlot(str)
    def on_statusFilterCombo_activated(self, txt):
        """
        Private slot to react to the selection of a status filter.
        
        @param txt selected status filter (string)
        """
        if txt == "<{0}>".format(self.tr("all")):
            for topIndex in range(self.statusList.topLevelItemCount()):
                topItem = self.statusList.topLevelItem(topIndex)
                topItem.setHidden(False)
        else:
            for topIndex in range(self.statusList.topLevelItemCount()):
                topItem = self.statusList.topLevelItem(topIndex)
                topItem.setHidden(topItem.text(self.__statusColumn) != txt)

    @pyqtSlot(QTreeWidgetItem, int)
    def on_statusList_itemChanged(self, item, column):
        """
        Private slot to act upon item changes.
        
        @param item reference to the changed item (QTreeWidgetItem)
        @param column index of column that changed (integer)
        """
        if column == self.__toBeCommittedColumn:
            self.__updateCommitButton()

    @pyqtSlot()
    def on_statusList_itemSelectionChanged(self):
        """
        Private slot to act upon changes of selected items.
        """
        self.__updateButtons()
        self.__generateDiffs()

    @pyqtSlot()
    def on_commitButton_clicked(self):
        """
        Private slot to handle the press of the Commit button.
        """
        self.__commit()

    @pyqtSlot()
    def on_addButton_clicked(self):
        """
        Private slot to handle the press of the Add button.
        """
        self.__add()

    @pyqtSlot()
    def on_diffButton_clicked(self):
        """
        Private slot to handle the press of the Differences button.
        """
        self.__diff()

    @pyqtSlot()
    def on_sbsDiffButton_clicked(self):
        """
        Private slot to handle the press of the Side-by-Side Diff button.
        """
        self.__sbsDiff()

    @pyqtSlot()
    def on_revertButton_clicked(self):
        """
        Private slot to handle the press of the Revert button.
        """
        self.__revert()

    @pyqtSlot()
    def on_forgetButton_clicked(self):
        """
        Private slot to handle the press of the Forget button.
        """
        self.__forget()

    @pyqtSlot()
    def on_restoreButton_clicked(self):
        """
        Private slot to handle the press of the Restore button.
        """
        self.__restoreMissing()

    ###########################################################################
    ## Context menu handling methods
    ###########################################################################

    def __showContextMenu(self, coord):
        """
        Private slot to show the context menu of the status list.
        
        @param coord the position of the mouse pointer (QPoint)
        """
        modified = len(self.__getModifiedItems())
        unversioned = len(self.__getUnversionedItems())
        missing = len(self.__getMissingItems())
        commitable = len(self.__getCommitableItems())

        self.__addAct.setEnabled(unversioned)
        self.__diffAct.setEnabled(modified)
        self.__sbsDiffAct.setEnabled(modified == 1)
        self.__revertAct.setEnabled(modified)
        self.__forgetAct.setEnabled(missing)
        self.__restoreAct.setEnabled(missing)
        self.__commitAct.setEnabled(commitable)

        if self.vcs.isExtensionActive("largefiles"):
            enable = len(self.__getUnversionedItems()) > 0
        else:
            enable = False
        for act in self.lfActions:
            act.setEnabled(enable)
        self.menu.popup(self.statusList.mapToGlobal(coord))

    def __showAddMenu(self):
        """
        Private slot to prepare the Add button menu before it is shown.
        """
        enable = self.vcs.isExtensionActive("largefiles")
        for act in self.__lfAddActions:
            act.setEnabled(enable)

    def __commit(self):
        """
        Private slot to handle the Commit context menu entry.
        """
        if self.__mq:
            self.vcs.vcsCommit(self.dname, "", mq=True)
        else:
            names = [os.path.join(self.dname, itm.text(self.__pathColumn)) for itm in self.__getCommitableItems()]
            if not names:
                E5MessageBox.information(
                    self, self.tr("Commit"), self.tr("""There are no entries selected to be""" """ committed.""")
                )
                return

            if Preferences.getVCS("AutoSaveFiles"):
                vm = e5App().getObject("ViewManager")
                for name in names:
                    vm.saveEditor(name)
            self.vcs.vcsCommit(names, "")

    def __committed(self):
        """
        Private slot called after the commit has finished.
        """
        if self.isVisible():
            self.on_refreshButton_clicked()
            self.vcs.checkVCSStatus()

    def __commitSelectAll(self):
        """
        Private slot to select all entries for commit.
        """
        self.__commitSelect(True)

    def __commitDeselectAll(self):
        """
        Private slot to deselect all entries from commit.
        """
        self.__commitSelect(False)

    def __add(self):
        """
        Private slot to handle the Add context menu entry.
        """
        names = [os.path.join(self.dname, itm.text(self.__pathColumn)) for itm in self.__getUnversionedItems()]
        if not names:
            E5MessageBox.information(
                self, self.tr("Add"), self.tr("""There are no unversioned entries""" """ available/selected.""")
            )
            return

        self.vcs.vcsAdd(names)
        self.on_refreshButton_clicked()

        project = e5App().getObject("Project")
        for name in names:
            project.getModel().updateVCSStatus(name)
        self.vcs.checkVCSStatus()

    def __lfAdd(self, mode):
        """
        Private slot to add a file to the repository.
        
        @param mode add mode (string one of 'normal' or 'large')
        """
        names = [os.path.join(self.dname, itm.text(self.__pathColumn)) for itm in self.__getUnversionedItems()]
        if not names:
            E5MessageBox.information(
                self, self.tr("Add"), self.tr("""There are no unversioned entries""" """ available/selected.""")
            )
            return

        self.vcs.getExtensionObject("largefiles").hgAdd(names, mode)
        self.on_refreshButton_clicked()

        project = e5App().getObject("Project")
        for name in names:
            project.getModel().updateVCSStatus(name)
        self.vcs.checkVCSStatus()

    def __forget(self):
        """
        Private slot to handle the Remove context menu entry.
        """
        names = [os.path.join(self.dname, itm.text(self.__pathColumn)) for itm in self.__getMissingItems()]
        if not names:
            E5MessageBox.information(
                self, self.tr("Remove"), self.tr("""There are no missing entries""" """ available/selected.""")
            )
            return

        self.vcs.hgForget(names)
        self.on_refreshButton_clicked()

    def __revert(self):
        """
        Private slot to handle the Revert context menu entry.
        """
        names = [os.path.join(self.dname, itm.text(self.__pathColumn)) for itm in self.__getModifiedItems()]
        if not names:
            E5MessageBox.information(
                self, self.tr("Revert"), self.tr("""There are no uncommitted changes""" """ available/selected.""")
            )
            return

        self.vcs.hgRevert(names)
        self.raise_()
        self.activateWindow()
        self.on_refreshButton_clicked()

        project = e5App().getObject("Project")
        for name in names:
            project.getModel().updateVCSStatus(name)
        self.vcs.checkVCSStatus()

    def __restoreMissing(self):
        """
        Private slot to handle the Restore Missing context menu entry.
        """
        names = [os.path.join(self.dname, itm.text(self.__pathColumn)) for itm in self.__getMissingItems()]
        if not names:
            E5MessageBox.information(
                self, self.tr("Revert"), self.tr("""There are no missing entries""" """ available/selected.""")
            )
            return

        self.vcs.hgRevert(names)
        self.on_refreshButton_clicked()
        self.vcs.checkVCSStatus()

    def __diff(self):
        """
        Private slot to handle the Diff context menu entry.
        """
        names = [os.path.join(self.dname, itm.text(self.__pathColumn)) for itm in self.__getModifiedItems()]
        if not names:
            E5MessageBox.information(
                self, self.tr("Differences"), self.tr("""There are no uncommitted changes""" """ available/selected.""")
            )
            return

        if self.diff is None:
            from .HgDiffDialog import HgDiffDialog

            self.diff = HgDiffDialog(self.vcs)
        self.diff.show()
        self.diff.start(names, refreshable=True)

    def __sbsDiff(self):
        """
        Private slot to handle the Diff context menu entry.
        """
        names = [os.path.join(self.dname, itm.text(self.__pathColumn)) for itm in self.__getModifiedItems()]
        if not names:
            E5MessageBox.information(
                self,
                self.tr("Side-by-Side Diff"),
                self.tr("""There are no uncommitted changes""" """ available/selected."""),
            )
            return
        elif len(names) > 1:
            E5MessageBox.information(
                self,
                self.tr("Side-by-Side Diff"),
                self.tr("""Only one file with uncommitted changes""" """ must be selected."""),
            )
            return

        self.vcs.hgSbsDiff(names[0])

    def __getCommitableItems(self):
        """
        Private method to retrieve all entries the user wants to commit.
        
        @return list of all items, the user has checked
        """
        commitableItems = []
        for index in range(self.statusList.topLevelItemCount()):
            itm = self.statusList.topLevelItem(index)
            if itm.checkState(self.__toBeCommittedColumn) == Qt.Checked:
                commitableItems.append(itm)
        return commitableItems

    def __getModifiedItems(self):
        """
        Private method to retrieve all entries, that have a modified status.
        
        @return list of all items with a modified status
        """
        modifiedItems = []
        for itm in self.statusList.selectedItems():
            if itm.text(self.__statusColumn) in self.modifiedIndicators:
                modifiedItems.append(itm)
        return modifiedItems

    def __getUnversionedItems(self):
        """
        Private method to retrieve all entries, that have an unversioned
        status.
        
        @return list of all items with an unversioned status
        """
        unversionedItems = []
        for itm in self.statusList.selectedItems():
            if itm.text(self.__statusColumn) in self.unversionedIndicators:
                unversionedItems.append(itm)
        return unversionedItems

    def __getMissingItems(self):
        """
        Private method to retrieve all entries, that have a missing status.
        
        @return list of all items with a missing status
        """
        missingItems = []
        for itm in self.statusList.selectedItems():
            if itm.text(self.__statusColumn) in self.missingIndicators:
                missingItems.append(itm)
        return missingItems

    def __commitSelect(self, selected):
        """
        Private slot to select or deselect all entries.
        
        @param selected commit selection state to be set (boolean)
        """
        for index in range(self.statusList.topLevelItemCount()):
            itm = self.statusList.topLevelItem(index)
            if itm.flags() & Qt.ItemIsUserCheckable:
                if selected:
                    itm.setCheckState(self.__toBeCommittedColumn, Qt.Checked)
                else:
                    itm.setCheckState(self.__toBeCommittedColumn, Qt.Unchecked)

    ###########################################################################
    ## Diff handling methods below
    ###########################################################################

    def __generateDiffs(self):
        """
        Private slot to generate diff outputs for the selected item.
        """
        self.diffEdit.clear()

        if not self.__mq:
            selectedItems = self.statusList.selectedItems()
            if len(selectedItems) == 1:
                fn = os.path.join(self.dname, selectedItems[0].text(self.__pathColumn))
                self.__diffGenerator.start(fn)

    def __generatorFinished(self):
        """
        Private slot connected to the finished signal of the diff generator.
        """
        diff = self.__diffGenerator.getResult()[0]

        if diff:
            for line in diff[:]:
                if line.startswith("@@ "):
                    break
                else:
                    diff.pop(0)
            self.diffEdit.setPlainText("".join(diff))

        tc = self.diffEdit.textCursor()
        tc.movePosition(QTextCursor.Start)
        self.diffEdit.setTextCursor(tc)
        self.diffEdit.ensureCursorVisible()

    def __refreshDiff(self):
        """
        Private method to refresh the diff output after a refresh.
        """
        if self.__selectedName and not self.__mq:
            for index in range(self.statusList.topLevelItemCount()):
                itm = self.statusList.topLevelItem(index)
                if itm.text(self.__pathColumn) == self.__selectedName:
                    itm.setSelected(True)
                    break

        self.__selectedName = ""
Beispiel #14
0
class HgDiffGenerator(QObject):
    """
    Class implementing the generation of output of the hg diff command.
    
    @signal finished() emitted when all processes have finished
    """
    finished = pyqtSignal()
    
    def __init__(self, vcs, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param parent parent widget (QWidget)
        """
        super(HgDiffGenerator, self).__init__(parent)
        
        self.vcs = vcs
        
        self.__hgClient = self.vcs.getClient()
        if self.__hgClient:
            self.process = None
        else:
            self.process = QProcess()
            self.process.finished.connect(self.__finish)
            self.process.readyReadStandardOutput.connect(self.__readStdout)
            self.process.readyReadStandardError.connect(self.__readStderr)
    
    def stopProcess(self):
        """
        Public slot to stop the diff process.
        """
        if self.__hgClient:
            if self.__hgClient.isExecuting():
                self.__hgClient.cancel()
        else:
            if self.process is not None and \
               self.process.state() != QProcess.NotRunning:
                self.process.terminate()
                QTimer.singleShot(2000, self.process.kill)
                self.process.waitForFinished(3000)
    
    def __getVersionArg(self, version):
        """
        Private method to get a hg revision argument for the given revision.
        
        @param version revision (integer or string)
        @return version argument (string)
        """
        if version == "WORKING":
            return None
        else:
            return str(version)
    
    def start(self, fn, versions=None, bundle=None, qdiff=False):
        """
        Public slot to start the hg diff command.
        
        @param fn filename to be diffed (string)
        @keyparam versions list of versions to be diffed (list of up to
            2 strings or None)
        @keyparam bundle name of a bundle file (string)
        @keyparam qdiff flag indicating qdiff command shall be used (boolean)
        @return flag indicating a successful start of the diff command
            (boolean)
        """
        if qdiff:
            args = self.vcs.initCommand("qdiff")
        else:
            args = self.vcs.initCommand("diff")
            
            if self.vcs.hasSubrepositories():
                args.append("--subrepos")
            
            if bundle:
                args.append('--repository')
                args.append(bundle)
            elif self.vcs.bundleFile and os.path.exists(self.vcs.bundleFile):
                args.append('--repository')
                args.append(self.vcs.bundleFile)
            
            if versions is not None:
                rev1 = self.__getVersionArg(versions[0])
                rev2 = None
                if len(versions) == 2:
                    rev2 = self.__getVersionArg(versions[1])
                
                if rev1 is not None or rev2 is not None:
                    args.append('-r')
                    if rev1 is not None and rev2 is not None:
                        args.append('{0}:{1}'.format(rev1, rev2))
                    elif rev2 is None:
                        args.append(rev1)
                    elif rev1 is None:
                        args.append(':{0}'.format(rev2))
        
        if isinstance(fn, list):
            dname, fnames = self.vcs.splitPathList(fn)
            self.vcs.addArguments(args, fn)
        else:
            dname, fname = self.vcs.splitPath(fn)
            args.append(fn)
        
        self.__oldFile = ""
        self.__oldFileLine = -1
        self.__fileSeparators = []
        self.__output = []
        self.__errors = []
        
        if self.__hgClient:
            out, err = self.__hgClient.runcommand(args)
            
            if err:
                self.__errors = err.splitlines(True)
            
            if out:
                for line in out.splitlines(True):
                    self.__processOutputLine(line)
                    if self.__hgClient.wasCanceled():
                        break
            
            self.__finish()
        else:
            # find the root of the repo
            repodir = dname
            while not os.path.isdir(os.path.join(repodir, self.vcs.adminDir)):
                repodir = os.path.dirname(repodir)
                if os.path.splitdrive(repodir)[1] == os.sep:
                    return
            
            self.process.kill()
            self.process.setWorkingDirectory(repodir)
            
            self.process.start('hg', args)
            procStarted = self.process.waitForStarted(5000)
            if not procStarted:
                return False
        
        return True
    
    def __finish(self):
        """
        Private slot called when the process finished or the user pressed
        the button.
        """
        self.finished.emit()
    
    def getResult(self):
        """
        Public method to return the result data.
        
        @return tuple of lists of string containing lines of the diff, the
            list of errors and a list of tuples of filenames and the line
            into the diff output.
        """
        return (self.__output, self.__errors, self.__fileSeparators)
    
    def __extractFileName(self, line):
        """
        Private method to extract the file name out of a file separator line.
        
        @param line line to be processed (string)
        @return extracted file name (string)
        """
        f = line.split(None, 1)[1]
        f = f.rsplit(None, 6)[0]
        if f == "/dev/null":
            f = "__NULL__"
        else:
            f = f.split("/", 1)[1]
        return f
    
    def __processFileLine(self, line):
        """
        Private slot to process a line giving the old/new file.
        
        @param line line to be processed (string)
        """
        if line.startswith('---'):
            self.__oldFileLine = len(self.__output)
            self.__oldFile = self.__extractFileName(line)
        else:
            newFile = self.__extractFileName(line)
            if self.__oldFile == "__NULL__":
                self.__fileSeparators.append(
                    (newFile, newFile, self.__oldFileLine))
            else:
                self.__fileSeparators.append(
                    (self.__oldFile, newFile, self.__oldFileLine))
    
    def __processOutputLine(self, line):
        """
        Private method to process the lines of output.
        
        @param line output line to be processed (string)
        """
        if line.startswith("--- ") or \
           line.startswith("+++ "):
            self.__processFileLine(line)
        
        self.__output.append(line)
    
    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        self.process.setReadChannel(QProcess.StandardOutput)
        
        while self.process.canReadLine():
            line = str(self.process.readLine(), self.vcs.getEncoding(),
                       'replace')
            self.__processOutputLine(line)
    
    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            s = str(self.process.readAllStandardError(),
                    self.vcs.getEncoding(), 'replace')
            self.__errors.append(s)
class EricapiExecDialog(QDialog, Ui_EricapiExecDialog):
    """
    Class implementing a dialog to show the output of the ericapi process.
    
    This class starts a QProcess and displays a dialog that
    shows the output of the documentation command process.
    """
    def __init__(self, cmdname, parent=None):
        """
        Constructor
        
        @param cmdname name of the ericapi generator (string)
        @param parent parent widget of this dialog (QWidget)
        """
        super(EricapiExecDialog, self).__init__(parent)
        self.setModal(True)
        self.setupUi(self)

        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)

        self.process = None
        self.cmdname = cmdname

    def start(self, args, fn):
        """
        Public slot to start the ericapi command.
        
        @param args commandline arguments for ericapi program (list of strings)
        @param fn filename or dirname to be processed by ericapi program
            (string)
        @return flag indicating the successful start of the process (boolean)
        """
        self.errorGroup.hide()

        self.filename = fn
        if os.path.isdir(self.filename):
            dname = os.path.abspath(self.filename)
            fname = "."
            if os.path.exists(os.path.join(dname, "__init__.py")):
                fname = os.path.basename(dname)
                dname = os.path.dirname(dname)
        else:
            dname = os.path.dirname(self.filename)
            fname = os.path.basename(self.filename)

        self.contents.clear()
        self.errors.clear()

        program = args[0]
        del args[0]
        args.append(fname)

        self.process = QProcess()
        self.process.setWorkingDirectory(dname)

        self.process.readyReadStandardOutput.connect(self.__readStdout)
        self.process.readyReadStandardError.connect(self.__readStderr)
        self.process.finished.connect(self.__finish)

        self.setWindowTitle(
            self.tr('{0} - {1}').format(self.cmdname, self.filename))
        self.process.start(program, args)
        procStarted = self.process.waitForStarted(5000)
        if not procStarted:
            E5MessageBox.critical(
                self, self.tr('Process Generation Error'),
                self.tr(
                    'The process {0} could not be started. '
                    'Ensure, that it is in the search path.').format(program))
        return procStarted

    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.accept()
        elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
            self.__finish()

    def __finish(self):
        """
        Private slot called when the process finished.
        
        It is called when the process finished or
        the user pressed the button.
        """
        if (self.process is not None
                and self.process.state() != QProcess.NotRunning):
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)

        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)

        self.process = None

        self.contents.insertPlainText(
            self.tr('\n{0} finished.\n').format(self.cmdname))
        self.contents.ensureCursorVisible()

    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        self.process.setReadChannel(QProcess.StandardOutput)

        while self.process.canReadLine():
            s = str(self.process.readLine(),
                    Preferences.getSystem("IOEncoding"), 'replace')
            self.contents.insertPlainText(s)
            self.contents.ensureCursorVisible()

    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        self.process.setReadChannel(QProcess.StandardError)

        while self.process.canReadLine():
            self.errorGroup.show()
            s = str(self.process.readLine(),
                    Preferences.getSystem("IOEncoding"), 'replace')
            self.errors.insertPlainText(s)
            self.errors.ensureCursorVisible()
Beispiel #16
0
class Process(QObject):

    """Abstraction over a running test subprocess process.

    Reads the log from its stdout and parses it.

    Signals:
        ready: Emitted when the server finished starting up.
        new_data: Emitted when a new line was parsed.
    """

    ready = pyqtSignal()
    new_data = pyqtSignal(object)

    def __init__(self, parent=None):
        super().__init__(parent)
        self._invalid = []
        self._data = []
        self.proc = QProcess()
        self.proc.setReadChannel(QProcess.StandardError)

    def _parse_line(self, line):
        """Parse the given line from the log.

        Return:
            A self.ParseResult member.
        """
        raise NotImplementedError

    def _executable_args(self):
        """Get the executable and arguments to pass to it as a tuple."""
        raise NotImplementedError

    def _get_data(self):
        """Get the parsed data for this test.

        Also waits for 0.5s to make sure any new data is received.

        Subprocesses are expected to alias this to a public method with a
        better name.
        """
        self.proc.waitForReadyRead(500)
        self.read_log()
        return self._data

    def _wait_signal(self, signal, timeout=5000, raising=True):
        """Wait for a signal to be emitted.

        Should be used in a contextmanager.
        """
        blocker = pytestqt.plugin.SignalBlocker(
            timeout=timeout, raising=raising)
        blocker.connect(signal)
        return blocker

    @pyqtSlot()
    def read_log(self):
        """Read the log from the process' stdout."""
        if not hasattr(self, 'proc'):
            # I have no idea how this happens, but it does...
            return
        while self.proc.canReadLine():
            line = self.proc.readLine()
            line = bytes(line).decode('utf-8', errors='ignore').rstrip('\r\n')

            try:
                parsed = self._parse_line(line)
            except InvalidLine:
                self._invalid.append(line)
                print("INVALID: {}".format(line))
                continue

            if parsed is None:
                print("IGNORED: {}".format(line))
            else:
                self._data.append(parsed)
                self.new_data.emit(parsed)

    def start(self):
        """Start the process and wait until it started."""
        with self._wait_signal(self.ready, timeout=60000):
            self._start()

    def _start(self):
        """Actually start the process."""
        executable, args = self._executable_args()
        self.proc.readyRead.connect(self.read_log)
        self.proc.start(executable, args)
        ok = self.proc.waitForStarted()
        assert ok
        assert self.is_running()

    def before_test(self):
        """Restart process before a test if it exited before."""
        self._invalid = []
        if not self.is_running():
            self.start()

    def after_test(self):
        """Clean up data after each test.

        Also checks self._invalid so the test counts as failed if there were
        unexpected output lines earlier.
        """
        if self._invalid:
            # Wait for a bit so the full error has a chance to arrive
            time.sleep(1)
            # Exit the process to make sure we're in a defined state again
            self.terminate()
            self._data.clear()
            raise InvalidLine(self._invalid)

        self._data.clear()
        if not self.is_running():
            raise ProcessExited

    def terminate(self):
        """Clean up and shut down the process."""
        self.proc.terminate()
        self.proc.waitForFinished()

    def is_running(self):
        """Check if the process is currently running."""
        return self.proc.state() == QProcess.Running

    def _match_data(self, value, expected):
        """Helper for wait_for to match a given value.

        The behavior of this method is slightly different depending on the
        types of the filtered values:

        - If expected is None, the filter always matches.
        - If the value is a string or bytes object and the expected value is
          too, the pattern is treated as a fnmatch glob pattern.
        - If the value is a string or bytes object and the expected value is a
          compiled regex, it is used for matching.
        - If the value is any other type, == is used.

        Return:
            A bool
        """
        regex_type = type(re.compile(''))
        if expected is None:
            return True
        elif isinstance(expected, regex_type):
            return expected.match(value)
        elif isinstance(value, (bytes, str)):
            return fnmatch.fnmatchcase(value, expected)
        else:
            return value == expected

    def wait_for(self, timeout=None, **kwargs):
        """Wait until a given value is found in the data.

        Keyword arguments to this function get interpreted as attributes of the
        searched data. Every given argument is treated as a pattern which
        the attribute has to match against.

        Return:
            The matched line.
        """
        if timeout is None:
            if 'CI' in os.environ:
                timeout = 15000
            else:
                timeout = 5000
        # Search existing messages
        for line in self._data:
            matches = []

            for key, expected in kwargs.items():
                value = getattr(line, key)
                matches.append(self._match_data(value, expected))

            if all(matches) and not line.waited_for:
                # If we waited for this line, chances are we don't mean the
                # same thing the next time we use wait_for and it matches
                # this line again.
                line.waited_for = True
                return line

        # If there is none, wait for the message
        spy = QSignalSpy(self.new_data)
        elapsed_timer = QElapsedTimer()
        elapsed_timer.start()

        while True:
            got_signal = spy.wait(timeout)
            if not got_signal or elapsed_timer.hasExpired(timeout):
                raise WaitForTimeout("Timed out after {}ms waiting for "
                                     "{!r}.".format(timeout, kwargs))

            for args in spy:
                assert len(args) == 1
                line = args[0]

                matches = []

                for key, expected in kwargs.items():
                    value = getattr(line, key)
                    matches.append(self._match_data(value, expected))

                if all(matches):
                    # If we waited for this line, chances are we don't mean the
                    # same thing the next time we use wait_for and it matches
                    # this line again.
                    line.waited_for = True
                    return line
Beispiel #17
0
class SvnLogBrowserDialog(QWidget, Ui_SvnLogBrowserDialog):
    """
    Class implementing a dialog to browse the log history.
    """
    def __init__(self, vcs, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param parent parent widget (QWidget)
        """
        super(SvnLogBrowserDialog, self).__init__(parent)
        self.setupUi(self)

        self.__position = QPoint()

        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)

        self.filesTree.headerItem().setText(self.filesTree.columnCount(), "")
        self.filesTree.header().setSortIndicator(0, Qt.AscendingOrder)

        self.vcs = vcs

        self.__initData()

        self.fromDate.setDisplayFormat("yyyy-MM-dd")
        self.toDate.setDisplayFormat("yyyy-MM-dd")
        self.__resetUI()

        self.__messageRole = Qt.UserRole
        self.__changesRole = Qt.UserRole + 1

        self.process = QProcess()
        self.process.finished.connect(self.__procFinished)
        self.process.readyReadStandardOutput.connect(self.__readStdout)
        self.process.readyReadStandardError.connect(self.__readStderr)

        self.rx_sep1 = QRegExp('\\-+\\s*')
        self.rx_sep2 = QRegExp('=+\\s*')
        self.rx_rev1 = QRegExp(
            'rev ([0-9]+):  ([^|]*) \| ([^|]*) \| ([0-9]+) .*')
        # "rev" followed by one or more decimals followed by a colon followed
        # anything up to " | " (twice) followed by one or more decimals
        # followed by anything
        self.rx_rev2 = QRegExp(
            'r([0-9]+) \| ([^|]*) \| ([^|]*) \| ([0-9]+) .*')
        # "r" followed by one or more decimals followed by " | " followed
        # anything up to " | " (twice) followed by one or more decimals
        # followed by anything
        self.rx_flags1 = QRegExp(
            r"""   ([ADM])\s(.*)\s+\(\w+\s+(.*):([0-9]+)\)\s*""")
        # three blanks followed by A or D or M followed by path followed by
        # path copied from followed by copied from revision
        self.rx_flags2 = QRegExp('   ([ADM]) (.*)\\s*')
        # three blanks followed by A or D or M followed by path

        self.flags = {
            'A': self.tr('Added'),
            'D': self.tr('Deleted'),
            'M': self.tr('Modified'),
            'R': self.tr('Replaced'),
        }

    def __initData(self):
        """
        Private method to (re-)initialize some data.
        """
        self.__maxDate = QDate()
        self.__minDate = QDate()
        self.__filterLogsEnabled = True

        self.buf = []  # buffer for stdout
        self.diff = None
        self.__started = False
        self.__lastRev = 0

    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)

        self.__position = self.pos()

        e.accept()

    def show(self):
        """
        Public slot to show the dialog.
        """
        if not self.__position.isNull():
            self.move(self.__position)
        self.__resetUI()

        super(SvnLogBrowserDialog, self).show()

    def __resetUI(self):
        """
        Private method to reset the user interface.
        """
        self.fromDate.setDate(QDate.currentDate())
        self.toDate.setDate(QDate.currentDate())
        self.fieldCombo.setCurrentIndex(
            self.fieldCombo.findText(self.tr("Message")))
        self.limitSpinBox.setValue(
            self.vcs.getPlugin().getPreferences("LogLimit"))
        self.stopCheckBox.setChecked(
            self.vcs.getPlugin().getPreferences("StopLogOnCopy"))

        self.logTree.clear()

        self.nextButton.setEnabled(True)
        self.limitSpinBox.setEnabled(True)

    def __resizeColumnsLog(self):
        """
        Private method to resize the log tree columns.
        """
        self.logTree.header().resizeSections(QHeaderView.ResizeToContents)
        self.logTree.header().setStretchLastSection(True)

    def __resortLog(self):
        """
        Private method to resort the log tree.
        """
        self.logTree.sortItems(self.logTree.sortColumn(),
                               self.logTree.header().sortIndicatorOrder())

    def __resizeColumnsFiles(self):
        """
        Private method to resize the changed files tree columns.
        """
        self.filesTree.header().resizeSections(QHeaderView.ResizeToContents)
        self.filesTree.header().setStretchLastSection(True)

    def __resortFiles(self):
        """
        Private method to resort the changed files tree.
        """
        sortColumn = self.filesTree.sortColumn()
        self.filesTree.sortItems(1,
                                 self.filesTree.header().sortIndicatorOrder())
        self.filesTree.sortItems(sortColumn,
                                 self.filesTree.header().sortIndicatorOrder())

    def __generateLogItem(self, author, date, message, revision, changedPaths):
        """
        Private method to generate a log tree entry.
        
        @param author author info (string)
        @param date date info (string)
        @param message text of the log message (list of strings)
        @param revision revision info (string)
        @param changedPaths list of dictionary objects containing
            info about the changed files/directories
        @return reference to the generated item (QTreeWidgetItem)
        """
        msg = []
        for line in message:
            msg.append(line.strip())

        itm = QTreeWidgetItem(self.logTree)
        itm.setData(0, Qt.DisplayRole, int(revision))
        itm.setData(1, Qt.DisplayRole, author)
        itm.setData(2, Qt.DisplayRole, date)
        itm.setData(3, Qt.DisplayRole, " ".join(msg))

        itm.setData(0, self.__messageRole, message)
        itm.setData(0, self.__changesRole, changedPaths)

        itm.setTextAlignment(0, Qt.AlignRight)
        itm.setTextAlignment(1, Qt.AlignLeft)
        itm.setTextAlignment(2, Qt.AlignLeft)
        itm.setTextAlignment(3, Qt.AlignLeft)
        itm.setTextAlignment(4, Qt.AlignLeft)

        try:
            self.__lastRev = int(revision)
        except ValueError:
            self.__lastRev = 0

        return itm

    def __generateFileItem(self, action, path, copyFrom, copyRev):
        """
        Private method to generate a changed files tree entry.
        
        @param action indicator for the change action ("A", "D" or "M")
        @param path path of the file in the repository (string)
        @param copyFrom path the file was copied from (None, string)
        @param copyRev revision the file was copied from (None, string)
        @return reference to the generated item (QTreeWidgetItem)
        """
        itm = QTreeWidgetItem(self.filesTree, [
            self.flags[action],
            path,
            copyFrom,
            copyRev,
        ])

        itm.setTextAlignment(3, Qt.AlignRight)

        return itm

    def __getLogEntries(self, startRev=None):
        """
        Private method to retrieve log entries from the repository.
        
        @param startRev revision number to start from (integer, string)
        """
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
        QApplication.processEvents()

        QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
        QApplication.processEvents()

        self.intercept = False
        self.process.kill()

        self.buf = []
        self.cancelled = False
        self.errors.clear()

        args = []
        args.append('log')
        self.vcs.addArguments(args, self.vcs.options['global'])
        self.vcs.addArguments(args, self.vcs.options['log'])
        args.append('--verbose')
        args.append('--limit')
        args.append('{0:d}'.format(self.limitSpinBox.value()))
        if startRev is not None:
            args.append('--revision')
            args.append('{0}:0'.format(startRev))
        if self.stopCheckBox.isChecked():
            args.append('--stop-on-copy')
        args.append(self.fname)

        self.process.setWorkingDirectory(self.dname)

        self.inputGroup.setEnabled(True)
        self.inputGroup.show()

        self.process.start('svn', args)
        procStarted = self.process.waitForStarted(5000)
        if not procStarted:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            E5MessageBox.critical(
                self, self.tr('Process Generation Error'),
                self.tr(
                    'The process {0} could not be started. '
                    'Ensure, that it is in the search path.').format('svn'))

    def start(self, fn, isFile=False):
        """
        Public slot to start the svn log command.
        
        @param fn filename to show the log for (string)
        @keyparam isFile flag indicating log for a file is to be shown
            (boolean)
        """
        self.sbsCheckBox.setEnabled(isFile)
        self.sbsCheckBox.setVisible(isFile)

        self.errorGroup.hide()
        QApplication.processEvents()

        self.__initData()

        self.filename = fn
        self.dname, self.fname = self.vcs.splitPath(fn)

        self.activateWindow()
        self.raise_()

        self.logTree.clear()
        self.__started = True
        self.__getLogEntries()

    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__processBuffer()
        self.__finish()

    def __finish(self):
        """
        Private slot called when the process finished or the user pressed the
        button.
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)

        QApplication.restoreOverrideCursor()

        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)

        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()

    def __processBuffer(self):
        """
        Private method to process the buffered output of the svn log command.
        """
        noEntries = 0
        log = {"message": []}
        changedPaths = []
        for s in self.buf:
            if self.rx_rev1.exactMatch(s):
                log["revision"] = self.rx_rev.cap(1)
                log["author"] = self.rx_rev.cap(2)
                log["date"] = self.rx_rev.cap(3)
                # number of lines is ignored
            elif self.rx_rev2.exactMatch(s):
                log["revision"] = self.rx_rev2.cap(1)
                log["author"] = self.rx_rev2.cap(2)
                log["date"] = self.rx_rev2.cap(3)
                # number of lines is ignored
            elif self.rx_flags1.exactMatch(s):
                changedPaths.append({
                    "action":
                    self.rx_flags1.cap(1).strip(),
                    "path":
                    self.rx_flags1.cap(2).strip(),
                    "copyfrom_path":
                    self.rx_flags1.cap(3).strip(),
                    "copyfrom_revision":
                    self.rx_flags1.cap(4).strip(),
                })
            elif self.rx_flags2.exactMatch(s):
                changedPaths.append({
                    "action": self.rx_flags2.cap(1).strip(),
                    "path": self.rx_flags2.cap(2).strip(),
                    "copyfrom_path": "",
                    "copyfrom_revision": "",
                })
            elif self.rx_sep1.exactMatch(s) or self.rx_sep2.exactMatch(s):
                if len(log) > 1:
                    self.__generateLogItem(log["author"], log["date"],
                                           log["message"], log["revision"],
                                           changedPaths)
                    dt = QDate.fromString(log["date"], Qt.ISODate)
                    if not self.__maxDate.isValid() and \
                            not self.__minDate.isValid():
                        self.__maxDate = dt
                        self.__minDate = dt
                    else:
                        if self.__maxDate < dt:
                            self.__maxDate = dt
                        if self.__minDate > dt:
                            self.__minDate = dt
                    noEntries += 1
                    log = {"message": []}
                    changedPaths = []
            else:
                if s.strip().endswith(":") or not s.strip():
                    continue
                else:
                    log["message"].append(s)

        self.__resizeColumnsLog()
        self.__resortLog()

        if self.__started:
            self.logTree.setCurrentItem(self.logTree.topLevelItem(0))
            self.__started = False

        if noEntries < self.limitSpinBox.value() and not self.cancelled:
            self.nextButton.setEnabled(False)
            self.limitSpinBox.setEnabled(False)

        self.__filterLogsEnabled = False
        self.fromDate.setMinimumDate(self.__minDate)
        self.fromDate.setMaximumDate(self.__maxDate)
        self.fromDate.setDate(self.__minDate)
        self.toDate.setMinimumDate(self.__minDate)
        self.toDate.setMaximumDate(self.__maxDate)
        self.toDate.setDate(self.__maxDate)
        self.__filterLogsEnabled = True
        self.__filterLogs()

    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.
        
        It reads the output of the process and inserts it into a buffer.
        """
        self.process.setReadChannel(QProcess.StandardOutput)

        while self.process.canReadLine():
            line = str(self.process.readLine(),
                       Preferences.getSystem("IOEncoding"), 'replace')
            self.buf.append(line)

    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            self.errorGroup.show()
            s = str(self.process.readAllStandardError(),
                    Preferences.getSystem("IOEncoding"), 'replace')
            self.errors.insertPlainText(s)
            self.errors.ensureCursorVisible()

    def __diffRevisions(self, rev1, rev2):
        """
        Private method to do a diff of two revisions.
        
        @param rev1 first revision number (integer)
        @param rev2 second revision number (integer)
        """
        if self.sbsCheckBox.isEnabled() and self.sbsCheckBox.isChecked():
            self.vcs.svnSbsDiff(self.filename,
                                revisions=(str(rev1), str(rev2)))
        else:
            if self.diff is None:
                from .SvnDiffDialog import SvnDiffDialog
                self.diff = SvnDiffDialog(self.vcs)
            self.diff.show()
            self.diff.raise_()
            self.diff.start(self.filename, [rev1, rev2])

    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.close()
        elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
            self.cancelled = True
            self.__finish()

    @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem)
    def on_logTree_currentItemChanged(self, current, previous):
        """
        Private slot called, when the current item of the log tree changes.
        
        @param current reference to the new current item (QTreeWidgetItem)
        @param previous reference to the old current item (QTreeWidgetItem)
        """
        if current is not None:
            self.messageEdit.clear()
            for line in current.data(0, self.__messageRole):
                self.messageEdit.append(line.strip())

            self.filesTree.clear()
            changes = current.data(0, self.__changesRole)
            if len(changes) > 0:
                for change in changes:
                    self.__generateFileItem(change["action"], change["path"],
                                            change["copyfrom_path"],
                                            change["copyfrom_revision"])
                self.__resizeColumnsFiles()
            self.__resortFiles()

        self.diffPreviousButton.setEnabled(
            current != self.logTree.topLevelItem(
                self.logTree.topLevelItemCount() - 1))

    @pyqtSlot()
    def on_logTree_itemSelectionChanged(self):
        """
        Private slot called, when the selection has changed.
        """
        self.diffRevisionsButton.setEnabled(
            len(self.logTree.selectedItems()) == 2)

    @pyqtSlot()
    def on_nextButton_clicked(self):
        """
        Private slot to handle the Next button.
        """
        if self.__lastRev > 1:
            self.__getLogEntries(self.__lastRev - 1)

    @pyqtSlot()
    def on_diffPreviousButton_clicked(self):
        """
        Private slot to handle the Diff to Previous button.
        """
        itm = self.logTree.currentItem()
        if itm is None:
            self.diffPreviousButton.setEnabled(False)
            return
        rev2 = int(itm.text(0))

        itm = self.logTree.topLevelItem(
            self.logTree.indexOfTopLevelItem(itm) + 1)
        if itm is None:
            self.diffPreviousButton.setEnabled(False)
            return
        rev1 = int(itm.text(0))

        self.__diffRevisions(rev1, rev2)

    @pyqtSlot()
    def on_diffRevisionsButton_clicked(self):
        """
        Private slot to handle the Compare Revisions button.
        """
        items = self.logTree.selectedItems()
        if len(items) != 2:
            self.diffRevisionsButton.setEnabled(False)
            return

        rev2 = int(items[0].text(0))
        rev1 = int(items[1].text(0))

        self.__diffRevisions(min(rev1, rev2), max(rev1, rev2))

    @pyqtSlot(QDate)
    def on_fromDate_dateChanged(self, date):
        """
        Private slot called, when the from date changes.
        
        @param date new date (QDate)
        """
        self.__filterLogs()

    @pyqtSlot(QDate)
    def on_toDate_dateChanged(self, date):
        """
        Private slot called, when the from date changes.
        
        @param date new date (QDate)
        """
        self.__filterLogs()

    @pyqtSlot(str)
    def on_fieldCombo_activated(self, txt):
        """
        Private slot called, when a new filter field is selected.
        
        @param txt text of the selected field (string)
        """
        self.__filterLogs()

    @pyqtSlot(str)
    def on_rxEdit_textChanged(self, txt):
        """
        Private slot called, when a filter expression is entered.
        
        @param txt filter expression (string)
        """
        self.__filterLogs()

    def __filterLogs(self):
        """
        Private method to filter the log entries.
        """
        if self.__filterLogsEnabled:
            from_ = self.fromDate.date().toString("yyyy-MM-dd")
            to_ = self.toDate.date().addDays(1).toString("yyyy-MM-dd")
            txt = self.fieldCombo.currentText()
            if txt == self.tr("Author"):
                fieldIndex = 1
                searchRx = QRegExp(self.rxEdit.text(), Qt.CaseInsensitive)
            elif txt == self.tr("Revision"):
                fieldIndex = 0
                txt = self.rxEdit.text()
                if txt.startswith("^"):
                    searchRx = QRegExp("^\s*{0}".format(txt[1:]),
                                       Qt.CaseInsensitive)
                else:
                    searchRx = QRegExp(txt, Qt.CaseInsensitive)
            else:
                fieldIndex = 3
                searchRx = QRegExp(self.rxEdit.text(), Qt.CaseInsensitive)

            currentItem = self.logTree.currentItem()
            for topIndex in range(self.logTree.topLevelItemCount()):
                topItem = self.logTree.topLevelItem(topIndex)
                if topItem.text(2) <= to_ and topItem.text(2) >= from_ and \
                   searchRx.indexIn(topItem.text(fieldIndex)) > -1:
                    topItem.setHidden(False)
                    if topItem is currentItem:
                        self.on_logTree_currentItemChanged(topItem, None)
                else:
                    topItem.setHidden(True)
                    if topItem is currentItem:
                        self.messageEdit.clear()
                        self.filesTree.clear()

    @pyqtSlot(bool)
    def on_stopCheckBox_clicked(self, checked):
        """
        Private slot called, when the stop on copy/move checkbox is clicked.
        
        @param checked flag indicating the checked state (boolean)
        """
        self.vcs.getPlugin().setPreferences("StopLogOnCopy",
                                            self.stopCheckBox.isChecked())
        self.nextButton.setEnabled(True)
        self.limitSpinBox.setEnabled(True)

    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)

    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the subversion process.
        """
        input = self.input.text()
        input += os.linesep

        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(input)
            self.errors.ensureCursorVisible()
        self.errorGroup.show()

        self.process.write(input)

        self.passwordCheckBox.setChecked(False)
        self.input.clear()

    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()

    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(SvnLogBrowserDialog, self).keyPressEvent(evt)
Beispiel #18
0
class HgDiffDialog(QWidget, Ui_HgDiffDialog):
    """
    Class implementing a dialog to show the output of the hg diff command
    process.
    """
    def __init__(self, vcs, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param parent parent widget (QWidget)
        """
        super(HgDiffDialog, self).__init__(parent)
        self.setupUi(self)
        
        self.buttonBox.button(QDialogButtonBox.Save).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        
        self.process = QProcess()
        self.vcs = vcs
        self.__hgClient = self.vcs.getClient()
        
        font = Preferences.getEditorOtherFonts("MonospacedFont")
        self.contents.setFontFamily(font.family())
        self.contents.setFontPointSize(font.pointSize())
        
        self.cNormalFormat = self.contents.currentCharFormat()
        self.cAddedFormat = self.contents.currentCharFormat()
        self.cAddedFormat.setBackground(QBrush(QColor(190, 237, 190)))
        self.cRemovedFormat = self.contents.currentCharFormat()
        self.cRemovedFormat.setBackground(QBrush(QColor(237, 190, 190)))
        self.cLineNoFormat = self.contents.currentCharFormat()
        self.cLineNoFormat.setBackground(QBrush(QColor(255, 220, 168)))
        
        self.process.finished.connect(self.__procFinished)
        self.process.readyReadStandardOutput.connect(self.__readStdout)
        self.process.readyReadStandardError.connect(self.__readStderr)
    
    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.__hgClient:
            if self.__hgClient.isExecuting():
                self.__hgClient.cancel()
        else:
            if self.process is not None and \
               self.process.state() != QProcess.NotRunning:
                self.process.terminate()
                QTimer.singleShot(2000, self.process.kill)
                self.process.waitForFinished(3000)
        
        e.accept()
    
    def __getVersionArg(self, version):
        """
        Private method to get a hg revision argument for the given revision.
        
        @param version revision (integer or string)
        @return version argument (string)
        """
        if version == "WORKING":
            return None
        else:
            return str(version)
    
    def start(self, fn, versions=None, bundle=None, qdiff=False):
        """
        Public slot to start the hg diff command.
        
        @param fn filename to be diffed (string)
        @param versions list of versions to be diffed (list of up to 2 strings
            or None)
        @param bundle name of a bundle file (string)
        @param qdiff flag indicating qdiff command shall be used (boolean)
        """
        self.errorGroup.hide()
        self.inputGroup.show()
        self.intercept = False
        self.filename = fn
        
        self.contents.clear()
        self.paras = 0
        
        self.filesCombo.clear()
        
        if qdiff:
            args = self.vcs.initCommand("qdiff")
            self.setWindowTitle(self.tr("Patch Contents"))
        else:
            args = self.vcs.initCommand("diff")
            
            if self.vcs.hasSubrepositories():
                args.append("--subrepos")
            
            if bundle:
                args.append('--repository')
                args.append(bundle)
            elif self.vcs.bundleFile and os.path.exists(self.vcs.bundleFile):
                args.append('--repository')
                args.append(self.vcs.bundleFile)
            
            if versions is not None:
                self.raise_()
                self.activateWindow()
                
                rev1 = self.__getVersionArg(versions[0])
                rev2 = None
                if len(versions) == 2:
                    rev2 = self.__getVersionArg(versions[1])
                
                if rev1 is not None or rev2 is not None:
                    args.append('-r')
                    if rev1 is not None and rev2 is not None:
                        args.append('{0}:{1}'.format(rev1, rev2))
                    elif rev2 is None:
                        args.append(rev1)
                    elif rev1 is None:
                        args.append(':{0}'.format(rev2))
        
        if isinstance(fn, list):
            dname, fnames = self.vcs.splitPathList(fn)
            self.vcs.addArguments(args, fn)
        else:
            dname, fname = self.vcs.splitPath(fn)
            args.append(fn)
        
        self.__oldFile = ""
        self.__oldFileLine = -1
        self.__fileSeparators = []
        
        QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
        if self.__hgClient:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            
            out, err = self.__hgClient.runcommand(args)
            
            if err:
                self.__showError(err)
            if out:
                for line in out.splitlines(True):
                    self.__processOutputLine(line)
                    if self.__hgClient.wasCanceled():
                        break
            
            self.__finish()
        else:
            # find the root of the repo
            repodir = dname
            while not os.path.isdir(os.path.join(repodir, self.vcs.adminDir)):
                repodir = os.path.dirname(repodir)
                if os.path.splitdrive(repodir)[1] == os.sep:
                    return
            
            self.process.kill()
            
            self.process.setWorkingDirectory(repodir)
            
            self.process.start('hg', args)
            procStarted = self.process.waitForStarted(5000)
            if not procStarted:
                QApplication.restoreOverrideCursor()
                self.inputGroup.setEnabled(False)
                self.inputGroup.hide()
                E5MessageBox.critical(
                    self,
                    self.tr('Process Generation Error'),
                    self.tr(
                        'The process {0} could not be started. '
                        'Ensure, that it is in the search path.'
                    ).format('hg'))
    
    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__finish()
    
    def __finish(self):
        """
        Private slot called when the process finished or the user pressed
        the button.
        """
        QApplication.restoreOverrideCursor()
        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()
        
        if self.paras == 0:
            self.contents.insertPlainText(
                self.tr('There is no difference.'))
            return
        
        self.buttonBox.button(QDialogButtonBox.Save).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        self.buttonBox.button(QDialogButtonBox.Close).setFocus(
            Qt.OtherFocusReason)
        
        tc = self.contents.textCursor()
        tc.movePosition(QTextCursor.Start)
        self.contents.setTextCursor(tc)
        self.contents.ensureCursorVisible()
        
        self.filesCombo.addItem(self.tr("<Start>"), 0)
        self.filesCombo.addItem(self.tr("<End>"), -1)
        for oldFile, newFile, pos in sorted(self.__fileSeparators):
            if oldFile != newFile:
                self.filesCombo.addItem(
                    "{0}\n{1}".format(oldFile, newFile), pos)
            else:
                self.filesCombo.addItem(oldFile, pos)
    
    def __appendText(self, txt, format):
        """
        Private method to append text to the end of the contents pane.
        
        @param txt text to insert (string)
        @param format text format to be used (QTextCharFormat)
        """
        tc = self.contents.textCursor()
        tc.movePosition(QTextCursor.End)
        self.contents.setTextCursor(tc)
        self.contents.setCurrentCharFormat(format)
        self.contents.insertPlainText(txt)
    
    def __extractFileName(self, line):
        """
        Private method to extract the file name out of a file separator line.
        
        @param line line to be processed (string)
        @return extracted file name (string)
        """
        f = line.split(None, 1)[1]
        f = f.rsplit(None, 6)[0]
        f = f.split("/", 1)[1]
        return f
    
    def __processFileLine(self, line):
        """
        Private slot to process a line giving the old/new file.
        
        @param line line to be processed (string)
        """
        if line.startswith('---'):
            self.__oldFileLine = self.paras
            self.__oldFile = self.__extractFileName(line)
        else:
            self.__fileSeparators.append(
                (self.__oldFile, self.__extractFileName(line),
                 self.__oldFileLine))
    
    def __processOutputLine(self, line):
        """
        Private method to process the lines of output.
        
        @param line output line to be processed (string)
        """
        if line.startswith("--- ") or \
           line.startswith("+++ "):
            self.__processFileLine(line)
        
        if line.startswith('+'):
            format = self.cAddedFormat
        elif line.startswith('-'):
            format = self.cRemovedFormat
        elif line.startswith('@@'):
            format = self.cLineNoFormat
        else:
            format = self.cNormalFormat
        self.__appendText(line, format)
        self.paras += 1
    
    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        self.process.setReadChannel(QProcess.StandardOutput)
        
        while self.process.canReadLine():
            line = str(self.process.readLine(), self.vcs.getEncoding(),
                       'replace')
            self.__processOutputLine(line)
    
    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            s = str(self.process.readAllStandardError(),
                    self.vcs.getEncoding(), 'replace')
            self.__showError(s)
    
    def __showError(self, out):
        """
        Private slot to show some error.
        
        @param out error to be shown (string)
        """
        self.errorGroup.show()
        self.errors.insertPlainText(out)
        self.errors.ensureCursorVisible()
    
    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Save):
            self.on_saveButton_clicked()
    
    @pyqtSlot(int)
    def on_filesCombo_activated(self, index):
        """
        Private slot to handle the selection of a file.
        
        @param index activated row (integer)
        """
        para = self.filesCombo.itemData(index)
        
        if para == 0:
            tc = self.contents.textCursor()
            tc.movePosition(QTextCursor.Start)
            self.contents.setTextCursor(tc)
            self.contents.ensureCursorVisible()
        elif para == -1:
            tc = self.contents.textCursor()
            tc.movePosition(QTextCursor.End)
            self.contents.setTextCursor(tc)
            self.contents.ensureCursorVisible()
        else:
            # step 1: move cursor to end
            tc = self.contents.textCursor()
            tc.movePosition(QTextCursor.End)
            self.contents.setTextCursor(tc)
            self.contents.ensureCursorVisible()
            
            # step 2: move cursor to desired line
            tc = self.contents.textCursor()
            delta = tc.blockNumber() - para
            tc.movePosition(QTextCursor.PreviousBlock, QTextCursor.MoveAnchor,
                            delta)
            self.contents.setTextCursor(tc)
            self.contents.ensureCursorVisible()
    
    @pyqtSlot()
    def on_saveButton_clicked(self):
        """
        Private slot to handle the Save button press.
        
        It saves the diff shown in the dialog to a file in the local
        filesystem.
        """
        if isinstance(self.filename, list):
            if len(self.filename) > 1:
                fname = self.vcs.splitPathList(self.filename)[0]
            else:
                dname, fname = self.vcs.splitPath(self.filename[0])
                if fname != '.':
                    fname = "{0}.diff".format(self.filename[0])
                else:
                    fname = dname
        else:
            fname = self.vcs.splitPath(self.filename)[0]
        
        fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
            self,
            self.tr("Save Diff"),
            fname,
            self.tr("Patch Files (*.diff)"),
            None,
            E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
        
        if not fname:
            return  # user aborted
        
        ext = QFileInfo(fname).suffix()
        if not ext:
            ex = selectedFilter.split("(*")[1].split(")")[0]
            if ex:
                fname += ex
        if QFileInfo(fname).exists():
            res = E5MessageBox.yesNo(
                self,
                self.tr("Save Diff"),
                self.tr("<p>The patch file <b>{0}</b> already exists."
                        " Overwrite it?</p>").format(fname),
                icon=E5MessageBox.Warning)
            if not res:
                return
        fname = Utilities.toNativeSeparators(fname)
        
        eol = e5App().getObject("Project").getEolString()
        try:
            f = open(fname, "w", encoding="utf-8", newline="")
            f.write(eol.join(self.contents.toPlainText().splitlines()))
            f.close()
        except IOError as why:
            E5MessageBox.critical(
                self, self.tr('Save Diff'),
                self.tr(
                    '<p>The patch file <b>{0}</b> could not be saved.'
                    '<br>Reason: {1}</p>')
                .format(fname, str(why)))
    
    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)
    
    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the subversion process.
        """
        input = self.input.text()
        input += os.linesep
        
        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(input)
            self.errors.ensureCursorVisible()
        
        self.process.write(input)
        
        self.passwordCheckBox.setChecked(False)
        self.input.clear()
    
    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()
    
    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(HgDiffDialog, self).keyPressEvent(evt)
class HgAnnotateDialog(QDialog, Ui_HgAnnotateDialog):
    """
    Class implementing a dialog to show the output of the hg annotate command.
    """
    def __init__(self, vcs, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param parent parent widget (QWidget)
        """
        super(HgAnnotateDialog, self).__init__(parent)
        self.setupUi(self)
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
        
        self.vcs = vcs
        self.__hgClient = vcs.getClient()
        
        self.__annotateRe = re.compile(
            r"""(.+)\s+(\d+)\s+([0-9a-fA-F]+)\s+([0-9-]+)\s+(.+)""")
        
        self.annotateList.headerItem().setText(
            self.annotateList.columnCount(), "")
        font = Preferences.getEditorOtherFonts("MonospacedFont")
        self.annotateList.setFont(font)
        
        if self.__hgClient:
            self.process = None
        else:
            self.process = QProcess()
            self.process.finished.connect(self.__procFinished)
            self.process.readyReadStandardOutput.connect(self.__readStdout)
            self.process.readyReadStandardError.connect(self.__readStderr)
        
        self.show()
        QCoreApplication.processEvents()
    
    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.__hgClient:
            if self.__hgClient.isExecuting():
                self.__hgClient.cancel()
        else:
            if self.process is not None and \
               self.process.state() != QProcess.NotRunning:
                self.process.terminate()
                QTimer.singleShot(2000, self.process.kill)
                self.process.waitForFinished(3000)
        
        e.accept()
    
    def start(self, fn):
        """
        Public slot to start the annotate command.
        
        @param fn filename to show the log for (string)
        """
        self.errorGroup.hide()
        self.intercept = False
        self.activateWindow()
        self.lineno = 1
        
        dname, fname = self.vcs.splitPath(fn)
        
        # find the root of the repo
        repodir = dname
        while not os.path.isdir(os.path.join(repodir, self.vcs.adminDir)):
            repodir = os.path.dirname(repodir)
            if os.path.splitdrive(repodir)[1] == os.sep:
                return
        
        args = self.vcs.initCommand("annotate")
        args.append('--follow')
        args.append('--user')
        args.append('--date')
        args.append('--number')
        args.append('--changeset')
        args.append('--quiet')
        args.append(fn)
        
        if self.__hgClient:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            
            out, err = self.__hgClient.runcommand(args)
            if err:
                self.__showError(err)
            if out:
                for line in out.splitlines():
                    self.__processOutputLine(line)
                    if self.__hgClient.wasCanceled():
                        break
            self.__finish()
        else:
            self.process.kill()
            self.process.setWorkingDirectory(repodir)
            
            self.process.start('hg', args)
            procStarted = self.process.waitForStarted(5000)
            if not procStarted:
                self.inputGroup.setEnabled(False)
                self.inputGroup.hide()
                E5MessageBox.critical(
                    self,
                    self.tr('Process Generation Error'),
                    self.tr(
                        'The process {0} could not be started. '
                        'Ensure, that it is in the search path.'
                    ).format('hg'))
            else:
                self.inputGroup.setEnabled(True)
                self.inputGroup.show()
    
    def __finish(self):
        """
        Private slot called when the process finished or the user pressed
        the button.
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        self.buttonBox.button(QDialogButtonBox.Close).setFocus(
            Qt.OtherFocusReason)
        
        self.process = None
        
        self.__resizeColumns()
    
    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.close()
        elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
            if self.__hgClient:
                self.__hgClient.cancel()
            else:
                self.__finish()
    
    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__finish()
    
    def __resizeColumns(self):
        """
        Private method to resize the list columns.
        """
        self.annotateList.header().resizeSections(QHeaderView.ResizeToContents)
    
    def __generateItem(self, revision, changeset, author, date, text):
        """
        Private method to generate an annotate item in the annotation list.
        
        @param revision revision string (string)
        @param changeset changeset string (string)
        @param author author of the change (string)
        @param date date of the change (string)
        @param text text of the change (string)
        """
        itm = QTreeWidgetItem(
            self.annotateList,
            [revision, changeset, author, date, "{0:d}".format(self.lineno),
             text])
        self.lineno += 1
        itm.setTextAlignment(0, Qt.AlignRight)
        itm.setTextAlignment(4, Qt.AlignRight)
    
    def __readStdout(self):
        """
        Private slot to handle the readyReadStdout signal.
        
        It reads the output of the process, formats it and inserts it into
        the annotation list.
        """
        self.process.setReadChannel(QProcess.StandardOutput)
        
        while self.process.canReadLine():
            s = str(self.process.readLine(), self.vcs.getEncoding(),
                    'replace').strip()
            self.__processOutputLine(s)
    
    def __processOutputLine(self, line):
        """
        Private method to process the lines of output.
        
        @param line output line to be processed (string)
        """
        try:
            info, text = line.split(": ", 1)
        except ValueError:
            info = line[:-2]
            text = ""
        match = self.__annotateRe.match(info)
        author, rev, changeset, date, file = match.groups()
        self.__generateItem(rev.strip(), changeset.strip(), author.strip(),
                            date.strip(), text)
    
    def __readStderr(self):
        """
        Private slot to handle the readyReadStderr signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            s = str(self.process.readAllStandardError(),
                    self.vcs.getEncoding(), 'replace')
            self.__showError(s)
    
    def __showError(self, out):
        """
        Private slot to show some error.
        
        @param out error to be shown (string)
        """
        self.errorGroup.show()
        self.errors.insertPlainText(out)
        self.errors.ensureCursorVisible()
    
    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)
    
    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the hg process.
        """
        input = self.input.text()
        input += os.linesep
        
        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(input)
            self.errors.ensureCursorVisible()
        
        self.process.write(input)
        
        self.passwordCheckBox.setChecked(False)
        self.input.clear()
    
    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()
    
    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(HgAnnotateDialog, self).keyPressEvent(evt)
Beispiel #20
0
class HTTPBin(QObject):

    """Abstraction over a running HTTPbin server process.

    Reads the log from its stdout and parses it.

    Class attributes:
        LOG_RE: Used to parse the CLF log which httpbin outputs.

    Signals:
        ready: Emitted when the server finished starting up.
        new_request: Emitted when there's a new request received.
    """

    ready = pyqtSignal()
    new_request = pyqtSignal(Request)

    LOG_RE = re.compile(r"""
        (?P<host>[^ ]*)
        \ ([^ ]*) # ignored
        \ (?P<user>[^ ]*)
        \ \[(?P<date>[^]]*)\]
        \ "(?P<request>
            (?P<verb>[^ ]*)
            \ (?P<url>[^ ]*)
            \ (?P<protocol>[^ ]*)
        )"
        \ (?P<status>[^ ]*)
        \ (?P<size>[^ ]*)
    """, re.VERBOSE)

    def __init__(self, parent=None):
        super().__init__(parent)
        self._invalid = False
        self._requests = []
        self.port = self._get_port()
        self.proc = QProcess()
        self.proc.setReadChannel(QProcess.StandardError)

    def _get_port(self):
        """Get a random free port to use for the server."""
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.bind(('localhost', 0))
        port = sock.getsockname()[1]
        sock.close()
        return port

    def get_requests(self):
        """Get the requests to the server during this test.

        Also waits for 0.5s to make sure any new requests are received.
        """
        self.proc.waitForReadyRead(500)
        self.read_log()
        return self._requests

    @pyqtSlot()
    def read_log(self):
        """Read the log from httpbin's stdout and parse it."""
        while self.proc.canReadLine():
            line = self.proc.readLine()
            line = bytes(line).decode('utf-8').rstrip('\r\n')
            print(line)

            if line == (' * Running on http://127.0.0.1:{}/ (Press CTRL+C to '
                        'quit)'.format(self.port)):
                self.ready.emit()
                continue

            match = self.LOG_RE.match(line)
            if match is None:
                self._invalid = True
                print("INVALID: {}".format(line))
                continue

            # FIXME do we need to allow other options?
            assert match.group('protocol') == 'HTTP/1.1'

            request = Request(verb=match.group('verb'), url=match.group('url'))
            print(request)
            self._requests.append(request)
            self.new_request.emit(request)

    def start(self):
        """Start the webserver."""
        if hasattr(sys, 'frozen'):
            executable = os.path.join(os.path.dirname(sys.executable),
                                      'webserver_sub')
            args = []
        else:
            executable = sys.executable
            args = [os.path.join(os.path.dirname(__file__),
                                 'webserver_sub.py')]

        self.proc.start(executable, args + [str(self.port)])
        ok = self.proc.waitForStarted()
        assert ok
        self.proc.readyRead.connect(self.read_log)

    def after_test(self):
        """Clean request list after each test.

        Also checks self._invalid so the test counts as failed if there were
        unexpected output lines earlier.
        """
        self._requests.clear()
        if self._invalid:
            raise InvalidLine

    def cleanup(self):
        """Clean up and shut down the process."""
        self.proc.terminate()
        self.proc.waitForFinished()
Beispiel #21
0
class HgQueuesListDialog(QDialog, Ui_HgQueuesListDialog):
    """
    Class implementing a dialog to show a list of applied and unapplied
    patches.
    """
    def __init__(self, vcs, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param parent parent widget (QWidget)
        """
        super(HgQueuesListDialog, self).__init__(parent)
        self.setupUi(self)
        self.setWindowFlags(Qt.Window)
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
        
        self.vcs = vcs
        self.__hgClient = vcs.getClient()
        
        self.patchesList.header().setSortIndicator(0, Qt.AscendingOrder)
        
        if self.__hgClient:
            self.process = None
        else:
            self.process = QProcess()
            self.process.finished.connect(self.__procFinished)
            self.process.readyReadStandardOutput.connect(self.__readStdout)
            self.process.readyReadStandardError.connect(self.__readStderr)
        
        self.__statusDict = {
            "A": self.tr("applied"),
            "U": self.tr("not applied"),
            "G": self.tr("guarded"),
            "D": self.tr("missing"),
        }
        
        self.show()
        QCoreApplication.processEvents()
    
    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.__hgClient:
            if self.__hgClient.isExecuting():
                self.__hgClient.cancel()
        else:
            if self.process is not None and \
               self.process.state() != QProcess.NotRunning:
                self.process.terminate()
                QTimer.singleShot(2000, self.process.kill)
                self.process.waitForFinished(3000)
        
        e.accept()
    
    def start(self, path):
        """
        Public slot to start the list command.
        
        @param path name of directory to be listed (string)
        """
        self.errorGroup.hide()
        
        self.intercept = False
        self.activateWindow()
        
        dname, fname = self.vcs.splitPath(path)
        
        # find the root of the repo
        repodir = dname
        while not os.path.isdir(os.path.join(repodir, self.vcs.adminDir)):
            repodir = os.path.dirname(repodir)
            if os.path.splitdrive(repodir)[1] == os.sep:
                return
        
        self.__repodir = repodir
        self.__getSeries()
    
    def __getSeries(self, missing=False):
        """
        Private slot to get the list of applied, unapplied and guarded patches
        and patches missing in the series file.
        
        @param missing flag indicating to get the patches missing in the
            series file (boolean)
        """
        if missing:
            self.__mode = "missing"
        else:
            self.__mode = "qseries"
        
        args = self.vcs.initCommand("qseries")
        args.append('--summary')
        args.append('--verbose')
        if missing:
            args.append('--missing')
        
        if self.__hgClient:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            
            out, err = self.__hgClient.runcommand(args)
            if err:
                self.__showError(err)
            if out:
                for line in out.splitlines():
                    self.__processOutputLine(line)
                    if self.__hgClient.wasCanceled():
                        self.__mode = ""
                        break
            if self.__mode == "qseries":
                self.__getSeries(True)
            elif self.__mode == "missing":
                self.__getTop()
            else:
                self.__finish()
        else:
            self.process.kill()
            self.process.setWorkingDirectory(self.__repodir)
            
            self.process.start('hg', args)
            procStarted = self.process.waitForStarted(5000)
            if not procStarted:
                self.inputGroup.setEnabled(False)
                self.inputGroup.hide()
                E5MessageBox.critical(
                    self,
                    self.tr('Process Generation Error'),
                    self.tr(
                        'The process {0} could not be started. '
                        'Ensure, that it is in the search path.'
                    ).format('hg'))
            else:
                self.inputGroup.setEnabled(True)
                self.inputGroup.show()
    
    def __getTop(self):
        """
        Private slot to get patch at the top of the stack.
        """
        self.__mode = "qtop"
        
        args = self.vcs.initCommand("qtop")
        
        if self.__hgClient:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            
            out, err = self.__hgClient.runcommand(args)
            if err:
                self.__showError(err)
            if out:
                for line in out.splitlines():
                    self.__processOutputLine(line)
                    if self.__hgClient.wasCanceled():
                        break
            self.__finish()
        else:
            self.process.kill()
            self.process.setWorkingDirectory(self.__repodir)
            
            self.process.start('hg', args)
            procStarted = self.process.waitForStarted(5000)
            if not procStarted:
                self.inputGroup.setEnabled(False)
                self.inputGroup.hide()
                E5MessageBox.critical(
                    self,
                    self.tr('Process Generation Error'),
                    self.tr(
                        'The process {0} could not be started. '
                        'Ensure, that it is in the search path.'
                    ).format('hg'))
            else:
                self.inputGroup.setEnabled(True)
                self.inputGroup.show()
    
    def __finish(self):
        """
        Private slot called when the process finished or the user pressed
        the button.
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        self.buttonBox.button(QDialogButtonBox.Close).setFocus(
            Qt.OtherFocusReason)
        
        if self.patchesList.topLevelItemCount() == 0:
            # no patches present
            self.__generateItem(
                0, "", self.tr("no patches found"), "", True)
        self.__resizeColumns()
        self.__resort()
    
    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.close()
        elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
            self.__mode = ""
            if self.__hgClient:
                self.__hgClient.cancel()
            else:
                self.__finish()
    
    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        if self.__mode == "qseries":
            self.__getSeries(True)
        elif self.__mode == "missing":
            self.__getTop()
        else:
            self.__finish()
    
    def __resort(self):
        """
        Private method to resort the tree.
        """
        self.patchesList.sortItems(
            self.patchesList.sortColumn(),
            self.patchesList.header().sortIndicatorOrder())
    
    def __resizeColumns(self):
        """
        Private method to resize the list columns.
        """
        self.patchesList.header().resizeSections(QHeaderView.ResizeToContents)
        self.patchesList.header().setStretchLastSection(True)
    
    def __generateItem(self, index, status, name, summary, error=False):
        """
        Private method to generate a patch item in the list of patches.
        
        @param index index of the patch (integer, -1 for missing)
        @param status status of the patch (string)
        @param name name of the patch (string)
        @param summary first line of the patch header (string)
        @param error flag indicating an error entry (boolean)
        """
        if error:
            itm = QTreeWidgetItem(self.patchesList, [
                "",
                name,
                "",
                summary
            ])
        else:
            if index == -1:
                index = ""
            try:
                statusStr = self.__statusDict[status]
            except KeyError:
                statusStr = self.tr("unknown")
            itm = QTreeWidgetItem(self.patchesList)
            itm.setData(0, Qt.DisplayRole, index)
            itm.setData(1, Qt.DisplayRole, name)
            itm.setData(2, Qt.DisplayRole, statusStr)
            itm.setData(3, Qt.DisplayRole, summary)
            if status == "A":
                # applied
                for column in range(itm.columnCount()):
                    itm.setForeground(column, Qt.blue)
            elif status == "D":
                # missing
                for column in range(itm.columnCount()):
                    itm.setForeground(column, Qt.red)
        
        itm.setTextAlignment(0, Qt.AlignRight)
        itm.setTextAlignment(2, Qt.AlignHCenter)
    
    def __markTopItem(self, name):
        """
        Private slot to mark the top patch entry.
        
        @param name name of the patch (string)
        """
        items = self.patchesList.findItems(name, Qt.MatchCaseSensitive, 1)
        if items:
            itm = items[0]
            for column in range(itm.columnCount()):
                font = itm.font(column)
                font.setBold(True)
                itm.setFont(column, font)
    
    def __readStdout(self):
        """
        Private slot to handle the readyReadStdout signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        self.process.setReadChannel(QProcess.StandardOutput)
        
        while self.process.canReadLine():
            s = str(self.process.readLine(), self.vcs.getEncoding(),
                    'replace').strip()
            self.__processOutputLine(s)
    
    def __processOutputLine(self, line):
        """
        Private method to process the lines of output.
        
        @param line output line to be processed (string)
        """
        if self.__mode == "qtop":
            self.__markTopItem(line)
        else:
            li = line.split(": ", 1)
            if len(li) == 1:
                data, summary = li[0][:-1], ""
            else:
                data, summary = li[0], li[1]
            li = data.split(None, 2)
            if len(li) == 2:
                # missing entry
                index, status, name = -1, li[0], li[1]
            elif len(li) == 3:
                index, status, name = li[:3]
            else:
                return
            self.__generateItem(index, status, name, summary)
    
    def __readStderr(self):
        """
        Private slot to handle the readyReadStderr signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            s = str(self.process.readAllStandardError(),
                    self.vcs.getEncoding(), 'replace')
            self.__showError(s)
    
    def __showError(self, out):
        """
        Private slot to show some error.
        
        @param out error to be shown (string)
        """
        self.errorGroup.show()
        self.errors.insertPlainText(out)
        self.errors.ensureCursorVisible()
    
    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)
    
    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the subversion process.
        """
        input = self.input.text()
        input += os.linesep
        
        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(input)
            self.errors.ensureCursorVisible()
        
        self.process.write(input)
        
        self.passwordCheckBox.setChecked(False)
        self.input.clear()
    
    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()
    
    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(HgQueuesListDialog, self).keyPressEvent(evt)
class SevenZipExtractor(Extractor):

    sig_entry_extracted = pyqtSignal(str, str)

    def __init__(self, filename: str, outdir: str) -> None:
        super().__init__()

        self._filename = os.path.abspath(filename)
        self._outdir = outdir

        self._process: Optional[QProcess] = None
        self._errors: List[str] = []

        self._error_summary = False
        self._result: Optional[ExtractorResult] = None

    def interrupt(self):
        if self._process is not None:
            self._process.terminate()
            # self._process.waitForBytesWritten(int msecs = 30000)
            # self._process.kill()

    def extract(self) -> ExtractorResult:
        assert self._process is None

        try:
            self._start_extract(self._outdir)
            self._process.waitForFinished(-1)
            assert self._result is not None
            return self._result
        except Exception as err:
            message = "{}: failure when extracting archive".format(self._filename)
            logger.exception(message)
            message += "\n\n" + traceback.format_exc()
            return ExtractorResult.failure(message)

    def _start_extract(self, outdir: str) -> None:
        program = "7z"
        argv = ["x", "-ba", "-bb1", "-bd", "-aos", "-o" + outdir, self._filename]

        logger.debug("SevenZipExtractorWorker: launching %s %s", program, argv)
        self._process = QProcess()
        self._process.setProgram(program)
        self._process.setArguments(argv)

        self._process.readyReadStandardOutput.connect(self._on_ready_read_stdout)
        self._process.readyReadStandardError.connect(self._on_ready_read_stderr)
        self._process.finished.connect(self._on_process_finished)

        self._process.start()
        self._process.closeWriteChannel()

    def _on_process_finished(self, exit_code, exit_status):
        self._process.setCurrentReadChannel(QProcess.StandardOutput)
        for line in os.fsdecode(self._process.readAll().data()).splitlines():
            self._process_stdout(line)

        self._process.setCurrentReadChannel(QProcess.StandardError)
        for line in os.fsdecode(self._process.readAll().data()).splitlines():
            self._process_stderr(line)

        if self._errors != []:
            message = "7-Zip: " + "\n".join(self._errors)
        else:
            message = ""

        if message:
            logger.error("SevenZipExtractorWorker: errors: %s", message)

        if exit_status != QProcess.NormalExit or exit_code != 0:
            logger.error("SevenZipExtractorWorker: something went wrong: %s  %s", exit_code, exit_status)
            self._result = ExtractorResult.failure(message)
        else:
            logger.debug("SevenZipExtractorWorker: finished successfully: %s  %s", exit_code, exit_status)
            self._result = ExtractorResult.success()

    def _process_stdout(self, line):
        if line.startswith("- "):
            entry = line[2:]
            self.sig_entry_extracted.emit(entry, os.path.join(self._outdir, entry))

    def _process_stderr(self, line):
        if line == "ERRORS:":
            self._error_summary = True
        else:
            if self._error_summary:
                if line != "":
                    self._errors.append(line)

    def _on_ready_read_stdout(self) -> None:
        assert self._process is not None

        while self._process.canReadLine():
            buf: QByteArray = self._process.readLine()
            line = os.fsdecode(buf.data()).rstrip("\n")
            # print("stdout:", repr(line))
            self._process_stdout(line)

    def _on_ready_read_stderr(self) -> None:
        assert self._process is not None

        while self._process.canReadLine():
            # print("stderr:", repr(line))
            buf = self._process.readLine()
            line = os.fsdecode(buf.data()).rstrip("\n")
            self._process_stderr(line)
Beispiel #23
0
class SvnChangeListsDialog(QDialog, Ui_SvnChangeListsDialog):
    """
    Class implementing a dialog to browse the change lists.
    """
    def __init__(self, vcs, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param parent parent widget (QWidget)
        """
        super(SvnChangeListsDialog, self).__init__(parent)
        self.setupUi(self)
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
        
        self.process = None
        self.vcs = vcs
        
        self.rx_status = QRegExp(
            '(.{8,9})\\s+([0-9-]+)\\s+([0-9?]+)\\s+(\\S+)\\s+(.+)\\s*')
        # flags (8 or 9 anything), revision, changed rev, author, path
        self.rx_status2 = \
            QRegExp('(.{8,9})\\s+(.+)\\s*')
        # flags (8 or 9 anything), path
        self.rx_changelist = \
            QRegExp('--- \\S+ .([\\w\\s]+).:\\s+')
        # three dashes, Changelist (translated), quote,
        # changelist name, quote, :
    
    @pyqtSlot(QListWidgetItem, QListWidgetItem)
    def on_changeLists_currentItemChanged(self, current, previous):
        """
        Private slot to handle the selection of a new item.
        
        @param current current item (QListWidgetItem)
        @param previous previous current item (QListWidgetItem)
        """
        self.filesList.clear()
        if current is not None:
            changelist = current.text()
            if changelist in self.changeListsDict:
                self.filesList.addItems(
                    sorted(self.changeListsDict[changelist]))
    
    def start(self, path):
        """
        Public slot to populate the data.
        
        @param path directory name to show change lists for (string)
        """
        self.changeListsDict = {}
        
        self.filesLabel.setText(
            self.tr("Files (relative to {0}):").format(path))
        
        self.errorGroup.hide()
        self.intercept = False
        
        self.path = path
        self.currentChangelist = ""
        
        self.process = QProcess()
        self.process.finished.connect(self.__procFinished)
        self.process.readyReadStandardOutput.connect(self.__readStdout)
        self.process.readyReadStandardError.connect(self.__readStderr)
        
        args = []
        args.append('status')
        self.vcs.addArguments(args, self.vcs.options['global'])
        self.vcs.addArguments(args, self.vcs.options['status'])
        if '--verbose' not in self.vcs.options['global'] and \
           '--verbose' not in self.vcs.options['status']:
            args.append('--verbose')
        if isinstance(path, list):
            self.dname, fnames = self.vcs.splitPathList(path)
            self.vcs.addArguments(args, fnames)
        else:
            self.dname, fname = self.vcs.splitPath(path)
            args.append(fname)
        
        self.process.setWorkingDirectory(self.dname)
        
        self.process.start('svn', args)
        procStarted = self.process.waitForStarted(5000)
        if not procStarted:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            E5MessageBox.critical(
                self,
                self.tr('Process Generation Error'),
                self.tr(
                    'The process {0} could not be started. '
                    'Ensure, that it is in the search path.'
                ).format('svn'))
        else:
            self.inputGroup.setEnabled(True)
            self.inputGroup.show()
    
    def __finish(self):
        """
        Private slot called when the process finished or the user pressed
        the button.
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        
        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()
        
        if len(self.changeListsDict) == 0:
            self.changeLists.addItem(self.tr("No changelists found"))
            self.buttonBox.button(QDialogButtonBox.Close).setFocus(
                Qt.OtherFocusReason)
        else:
            self.changeLists.addItems(sorted(self.changeListsDict.keys()))
            self.changeLists.setCurrentRow(0)
            self.changeLists.setFocus(Qt.OtherFocusReason)
    
    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.close()
        elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
            self.__finish()
        
    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__finish()
        
    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        if self.process is not None:
            self.process.setReadChannel(QProcess.StandardOutput)
            
            while self.process.canReadLine():
                s = str(self.process.readLine(),
                        Preferences.getSystem("IOEncoding"),
                        'replace')
                if self.currentChangelist != "" and \
                        self.rx_status.exactMatch(s):
                    file = self.rx_status.cap(5).strip()
                    filename = file.replace(self.path + os.sep, "")
                    if filename not in \
                            self.changeListsDict[self.currentChangelist]:
                        self.changeListsDict[self.currentChangelist].append(
                            filename)
                elif self.currentChangelist != "" and \
                        self.rx_status2.exactMatch(s):
                    file = self.rx_status2.cap(2).strip()
                    filename = file.replace(self.path + os.sep, "")
                    if filename not in \
                            self.changeListsDict[self.currentChangelist]:
                        self.changeListsDict[self.currentChangelist].append(
                            filename)
                elif self.rx_changelist.exactMatch(s):
                    self.currentChangelist = self.rx_changelist.cap(1)
                    if self.currentChangelist not in self.changeListsDict:
                        self.changeListsDict[self.currentChangelist] = []
        
    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            self.errorGroup.show()
            s = str(self.process.readAllStandardError(),
                    Preferences.getSystem("IOEncoding"),
                    'replace')
            self.errors.insertPlainText(s)
            self.errors.ensureCursorVisible()
        
    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)
        
    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the subversion process.
        """
        input = self.input.text()
        input += os.linesep
        
        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(input)
            self.errors.ensureCursorVisible()
        
        self.process.write(input)
        
        self.passwordCheckBox.setChecked(False)
        self.input.clear()
        
    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()
        
    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(SvnChangeListsDialog, self).keyPressEvent(evt)
Beispiel #24
0
class HgGpgSignaturesDialog(QDialog, Ui_HgGpgSignaturesDialog):
    """
    Class implementing a dialog showing signed changesets.
    """
    def __init__(self, vcs, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param parent reference to the parent widget (QWidget)
        """
        super(HgGpgSignaturesDialog, self).__init__(parent)
        self.setupUi(self)
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
        
        self.process = QProcess()
        self.vcs = vcs
        self.__hgClient = vcs.getClient()
        
        self.process.finished.connect(self.__procFinished)
        self.process.readyReadStandardOutput.connect(self.__readStdout)
        self.process.readyReadStandardError.connect(self.__readStderr)
        
        self.show()
        QCoreApplication.processEvents()
    
    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.__hgClient:
            if self.__hgClient.isExecuting():
                self.__hgClient.cancel()
        else:
            if self.process is not None and \
               self.process.state() != QProcess.NotRunning:
                self.process.terminate()
                QTimer.singleShot(2000, self.process.kill)
                self.process.waitForFinished(3000)
        
        e.accept()
    
    def start(self, path):
        """
        Public slot to start the list command.
        
        @param path name of directory (string)
        """
        self.errorGroup.hide()
        
        self.intercept = False
        self.activateWindow()
        
        self.__path = path
        dname, fname = self.vcs.splitPath(path)
        
        # find the root of the repo
        repodir = dname
        while not os.path.isdir(os.path.join(repodir, self.vcs.adminDir)):
            repodir = os.path.dirname(repodir)
            if os.path.splitdrive(repodir)[1] == os.sep:
                return
        
        args = self.vcs.initCommand("sigs")
        
        if self.__hgClient:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            
            out, err = self.__hgClient.runcommand(args)
            if err:
                self.__showError(err)
            if out:
                for line in out.splitlines():
                    self.__processOutputLine(line)
                    if self.__hgClient.wasCanceled():
                        break
            self.__finish()
        else:
            self.process.kill()
            self.process.setWorkingDirectory(repodir)
            
            self.process.start('hg', args)
            procStarted = self.process.waitForStarted(5000)
            if not procStarted:
                self.inputGroup.setEnabled(False)
                self.inputGroup.hide()
                E5MessageBox.critical(
                    self,
                    self.tr('Process Generation Error'),
                    self.tr(
                        'The process {0} could not be started. '
                        'Ensure, that it is in the search path.'
                    ).format('hg'))
            else:
                self.inputGroup.setEnabled(True)
                self.inputGroup.show()
    
    def __finish(self):
        """
        Private slot called when the process finished or the user pressed
        the button.
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        self.buttonBox.button(QDialogButtonBox.Close).setFocus(
            Qt.OtherFocusReason)
        
        self.process = None
        
        if self.signaturesList.topLevelItemCount() == 0:
            # no patches present
            self.__generateItem("", "", self.tr("no signatures found"))
        self.__resizeColumns()
        self.__resort()
    
    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.close()
        elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
            if self.__hgClient:
                self.__hgClient.cancel()
            else:
                self.__finish()
    
    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__finish()
    
    def __resort(self):
        """
        Private method to resort the tree.
        """
        self.signaturesList.sortItems(
            self.signaturesList.sortColumn(),
            self.signaturesList.header().sortIndicatorOrder())
    
    def __resizeColumns(self):
        """
        Private method to resize the list columns.
        """
        self.signaturesList.header().resizeSections(
            QHeaderView.ResizeToContents)
        self.signaturesList.header().setStretchLastSection(True)
    
    def __generateItem(self, revision, changeset, signature):
        """
        Private method to generate a patch item in the list of patches.
        
        @param revision revision number (string)
        @param changeset changeset of the bookmark (string)
        @param signature signature of the changeset (string)
        """
        if revision == "" and changeset == "":
            QTreeWidgetItem(self.signaturesList, [signature])
        else:
            revString = "{0:>7}:{1}".format(revision, changeset)
            topItems = self.signaturesList.findItems(
                revString, Qt.MatchExactly)
            if len(topItems) == 0:
                # first signature for this changeset
                topItm = QTreeWidgetItem(self.signaturesList, [
                    "{0:>7}:{1}".format(revision, changeset)])
                topItm.setExpanded(True)
                font = topItm.font(0)
                font.setBold(True)
                topItm.setFont(0, font)
            else:
                topItm = topItems[0]
            QTreeWidgetItem(topItm, [signature])
    
    def __readStdout(self):
        """
        Private slot to handle the readyReadStdout signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        self.process.setReadChannel(QProcess.StandardOutput)
        
        while self.process.canReadLine():
            s = str(self.process.readLine(), self.vcs.getEncoding(),
                    'replace').strip()
            self.__processOutputLine(s)
    
    def __processOutputLine(self, line):
        """
        Private method to process the lines of output.
        
        @param line output line to be processed (string)
        """
        li = line.split()
        if li[-1][0] in "1234567890":
            # last element is a rev:changeset
            rev, changeset = li[-1].split(":", 1)
            del li[-1]
            signature = " ".join(li)
            self.__generateItem(rev, changeset, signature)
    
    def __readStderr(self):
        """
        Private slot to handle the readyReadStderr signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            s = str(self.process.readAllStandardError(),
                    self.vcs.getEncoding(), 'replace')
            self.__showError(s)
    
    def __showError(self, out):
        """
        Private slot to show some error.
        
        @param out error to be shown (string)
        """
        self.errorGroup.show()
        self.errors.insertPlainText(out)
        self.errors.ensureCursorVisible()
    
    @pyqtSlot()
    def on_signaturesList_itemSelectionChanged(self):
        """
        Private slot handling changes of the selection.
        """
        selectedItems = self.signaturesList.selectedItems()
        if len(selectedItems) == 1 and \
           self.signaturesList.indexOfTopLevelItem(selectedItems[0]) != -1:
            self.verifyButton.setEnabled(True)
        else:
            self.verifyButton.setEnabled(False)
    
    @pyqtSlot()
    def on_verifyButton_clicked(self):
        """
        Private slot to verify the signatures of the selected revision.
        """
        rev = self.signaturesList.selectedItems()[0].text(0)\
            .split(":")[0].strip()
        self.vcs.getExtensionObject("gpg")\
            .hgGpgVerifySignatures(self.__path, rev)
    
    @pyqtSlot(str)
    def on_categoryCombo_activated(self, txt):
        """
        Private slot called, when a new filter category is selected.
        
        @param txt text of the selected category (string)
        """
        self.__filterSignatures()
    
    @pyqtSlot(str)
    def on_rxEdit_textChanged(self, txt):
        """
        Private slot called, when a filter expression is entered.
        
        @param txt filter expression (string)
        """
        self.__filterSignatures()
    
    def __filterSignatures(self):
        """
        Private method to filter the log entries.
        """
        searchRxText = self.rxEdit.text()
        filterTop = self.categoryCombo.currentText() == self.tr("Revision")
        if filterTop and searchRxText.startswith("^"):
            searchRx = QRegExp(
                "^\s*{0}".format(searchRxText[1:]), Qt.CaseInsensitive)
        else:
            searchRx = QRegExp(searchRxText, Qt.CaseInsensitive)
        for topIndex in range(self.signaturesList.topLevelItemCount()):
            topLevelItem = self.signaturesList.topLevelItem(topIndex)
            if filterTop:
                topLevelItem.setHidden(
                    searchRx.indexIn(topLevelItem.text(0)) == -1)
            else:
                visibleChildren = topLevelItem.childCount()
                for childIndex in range(topLevelItem.childCount()):
                    childItem = topLevelItem.child(childIndex)
                    if searchRx.indexIn(childItem.text(0)) == -1:
                        childItem.setHidden(True)
                        visibleChildren -= 1
                    else:
                        childItem.setHidden(False)
                topLevelItem.setHidden(visibleChildren == 0)
    
    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)
    
    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the subversion process.
        """
        input = self.input.text()
        input += os.linesep
        
        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(input)
            self.errors.ensureCursorVisible()
        
        self.process.write(input)
        
        self.passwordCheckBox.setChecked(False)
        self.input.clear()
    
    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()
    
    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(HgGpgSignaturesDialog, self).keyPressEvent(evt)
class GitRemoteRepositoriesDialog(QWidget, Ui_GitRemoteRepositoriesDialog):
    """
    Class implementing a dialog to show available remote repositories.
    """
    def __init__(self, vcs, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param parent parent widget (QWidget)
        """
        super(GitRemoteRepositoriesDialog, self).__init__(parent)
        self.setupUi(self)
        
        self.vcs = vcs
        self.process = QProcess()
        self.process.finished.connect(self.__procFinished)
        self.process.readyReadStandardOutput.connect(self.__readStdout)
        self.process.readyReadStandardError.connect(self.__readStderr)
        
        self.refreshButton = self.buttonBox.addButton(
            self.tr("Refresh"), QDialogButtonBox.ActionRole)
        self.refreshButton.setToolTip(
            self.tr("Press to refresh the repositories display"))
        self.refreshButton.setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
        
        self.__lastColumn = self.repolist.columnCount()
        
        self.repolist.headerItem().setText(self.__lastColumn, "")
        self.repolist.header().setSortIndicator(0, Qt.AscendingOrder)
        
        self.__ioEncoding = Preferences.getSystem("IOEncoding")
    
    def __resort(self):
        """
        Private method to resort the list.
        """
        self.repolist.sortItems(
            self.repolist.sortColumn(),
            self.repolist.header().sortIndicatorOrder())
    
    def __resizeColumns(self):
        """
        Private method to resize the list columns.
        """
        self.repolist.header().resizeSections(QHeaderView.ResizeToContents)
        self.repolist.header().setStretchLastSection(True)
    
    def __generateItem(self, name, url, oper):
        """
        Private method to generate a status item in the status list.
        
        @param name name of the remote repository (string)
        @param url URL of the remote repository (string)
        @param oper operation the remote repository may be used for (string)
        """
        foundItems = self.repolist.findItems(name, Qt.MatchExactly, 0)
        if foundItems:
            # modify the operations column only
            foundItems[0].setText(
                2, "{0} + {1}".format(foundItems[0].text(2), oper))
        else:
            QTreeWidgetItem(self.repolist, [name, url, oper])
    
    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        e.accept()
    
    def start(self, projectDir):
        """
        Public slot to start the git remote command.
        
        @param projectDir name of the project directory (string)
        """
        self.repolist.clear()
        
        self.errorGroup.hide()
        self.intercept = False
        self.projectDir = projectDir
        
        self.__ioEncoding = Preferences.getSystem("IOEncoding")
        
        self.removeButton.setEnabled(False)
        self.renameButton.setEnabled(False)
        self.pruneButton.setEnabled(False)
        self.showInfoButton.setEnabled(False)
        
        args = self.vcs.initCommand("remote")
        args.append('--verbose')
        
        # find the root of the repo
        repodir = self.projectDir
        while not os.path.isdir(os.path.join(repodir, self.vcs.adminDir)):
            repodir = os.path.dirname(repodir)
            if os.path.splitdrive(repodir)[1] == os.sep:
                return
        
        self.process.kill()
        self.process.setWorkingDirectory(repodir)
        
        self.process.start('git', args)
        procStarted = self.process.waitForStarted(5000)
        if not procStarted:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            E5MessageBox.critical(
                self,
                self.tr('Process Generation Error'),
                self.tr(
                    'The process {0} could not be started. '
                    'Ensure, that it is in the search path.'
                ).format('git'))
        else:
            self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
            self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
            self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
            
            self.inputGroup.setEnabled(True)
            self.inputGroup.show()
            self.refreshButton.setEnabled(False)
    
    def __finish(self):
        """
        Private slot called when the process finished or the user pressed
        the button.
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()
        self.refreshButton.setEnabled(True)
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        self.buttonBox.button(QDialogButtonBox.Close).setFocus(
            Qt.OtherFocusReason)
        
        self.__resort()
        self.__resizeColumns()
        
        self.__updateButtons()
    
    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.close()
        elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
            self.__finish()
        elif button == self.refreshButton:
            self.on_refreshButton_clicked()
    
    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__finish()
    
    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        if self.process is not None:
            self.process.setReadChannel(QProcess.StandardOutput)
            
            while self.process.canReadLine():
                line = str(self.process.readLine(), self.__ioEncoding,
                           'replace').strip()
                
                name, line = line.split(None, 1)
                url, oper = line.rsplit(None, 1)
                oper = oper[1:-1]   # it is enclosed in ()
                self.__generateItem(name, url, oper)
    
    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            s = str(self.process.readAllStandardError(),
                    self.__ioEncoding, 'replace')
            self.errorGroup.show()
            self.errors.insertPlainText(s)
            self.errors.ensureCursorVisible()
    
    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)
    
    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the git process.
        """
        inputTxt = self.input.text()
        inputTxt += os.linesep
        
        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(inputTxt)
            self.errors.ensureCursorVisible()
        
        self.process.write(inputTxt)
        
        self.passwordCheckBox.setChecked(False)
        self.input.clear()
    
    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()
    
    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(GitRemoteRepositoriesDialog, self).keyPressEvent(evt)
    
    @pyqtSlot()
    def on_refreshButton_clicked(self):
        """
        Private slot to refresh the status display.
        """
        self.start(self.projectDir)
    
    def __updateButtons(self):
        """
        Private method to update the buttons status.
        """
        enable = len(self.repolist.selectedItems()) == 1
        
        self.removeButton.setEnabled(enable)
        self.renameButton.setEnabled(enable)
        self.pruneButton.setEnabled(enable)
        self.showInfoButton.setEnabled(enable)
    
    @pyqtSlot()
    def on_repolist_itemSelectionChanged(self):
        """
        Private slot to act upon changes of selected items.
        """
        self.__updateButtons()
    
    @pyqtSlot()
    def on_addButton_clicked(self):
        """
        Private slot to add a remote repository.
        """
        self.vcs.gitAddRemote(self.projectDir)
        self.on_refreshButton_clicked()
    
    @pyqtSlot()
    def on_renameButton_clicked(self):
        """
        Private slot to rename a remote repository.
        """
        remoteName = self.repolist.selectedItems()[0].text(0)
        self.vcs.gitRenameRemote(self.projectDir, remoteName)
        self.on_refreshButton_clicked()
    
    @pyqtSlot()
    def on_removeButton_clicked(self):
        """
        Private slot to remove a remote repository.
        """
        remoteName = self.repolist.selectedItems()[0].text(0)
        self.vcs.gitRemoveRemote(self.projectDir, remoteName)
        self.on_refreshButton_clicked()
    
    @pyqtSlot()
    def on_showInfoButton_clicked(self):
        """
        Private slot to show information about a remote repository.
        """
        remoteName = self.repolist.selectedItems()[0].text(0)
        self.vcs.gitShowRemote(self.projectDir, remoteName)
    
    @pyqtSlot()
    def on_pruneButton_clicked(self):
        """
        Private slot to prune all stale remote-tracking branches.
        """
        remoteName = self.repolist.selectedItems()[0].text(0)
        self.vcs.gitPruneRemote(self.projectDir, remoteName)
Beispiel #26
0
class FluidPropertyWidget(QWidget):

    UNITS_WIDTH = 55

    def __init__(self, parent):
        super(FluidPropertyWidget, self).__init__(parent)
        self.parent = parent
        self.modified = False

        self.layoutMain = QVBoxLayout(self)
        self.layoutMain.setContentsMargins(15, 7, 15, 7)

        self.layoutForm = QFormLayout()
        self.layoutForm.setLabelAlignment(Qt.AlignLeft)
        self.layoutForm.setFormAlignment(Qt.AlignLeft)
        self.layoutForm.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
        self.layoutForm.setVerticalSpacing(5)

        self.lblInputs = {}
        self.ctlInputs = {}
        self.lblUnit = {}
        self.lblProp = {}
        self.ctlProp = {}

        for i, p in enumerate(self.inputs()):
            name = p['name']
            unit = p['unit']
            hint = p['hint']

            self.lblInputs[name] = QLabel(name, self)
            self.lblInputs[name].setToolTip(hint)

            self.ctlInputs[name] = QLineEdit(self)
            self.ctlInputs[name].setToolTip(hint)
            self.ctlInputs[name].textChanged.connect(self.onModified)

            self.lblUnit[name] = QLabel(unit, self)
            self.lblUnit[name].setFixedWidth(self.UNITS_WIDTH)

            hbox = QHBoxLayout()
            hbox.addWidget(self.ctlInputs[name])
            hbox.addWidget(self.lblUnit[name])

            self.layoutForm.addRow(self.lblInputs[name], hbox)

        self.btnCalculate = QPushButton()
        self.btnCalculate.clicked.connect(self.computeProperties)
        icon_path = os.path.abspath(
            os.path.join(os.path.dirname(__file__), 'icons', 'calculator.svg'))
        icon = QIcon(icon_path)
        self.btnCalculate.setIcon(icon)
        self.btnCalculate.setToolTip(
            "Calculate fluid properties for given inputs")
        self.btnCalculate.setAutoDefault(True)
        self.btnCalculate.setMaximumWidth(62)
        self.layoutForm.addRow("", self.btnCalculate)

        self.CalculateShortcut = QtWidgets.QShortcut(
            QtGui.QKeySequence("Ctrl+Return"), self)
        self.CalculateShortcut.activated.connect(self.onCtrlReturn)

        lbl = QFrame()
        lbl.setMinimumHeight(2)
        self.layoutForm.addRow("", lbl)

        for p in self.outputs():
            name = p['name']
            unit = p['unit']
            hint = p['hint']

            self.lblProp[name] = QLabel(name, self)
            self.lblProp[name].setToolTip(hint)
            self.lblProp[name].setEnabled(False)

            self.ctlProp[name] = QLineEdit(self)
            self.ctlProp[name].setReadOnly(True)
            self.ctlProp[name].setToolTip(hint)

            self.lblUnit[name] = QLabel(unit, self)
            self.lblUnit[name].setFixedWidth(self.UNITS_WIDTH)
            self.lblUnit[name].setEnabled(False)

            hbox = QHBoxLayout()
            hbox.addWidget(self.ctlProp[name])
            hbox.addWidget(self.lblUnit[name])

            self.layoutForm.addRow(self.lblProp[name], hbox)

        self.layoutMain.addLayout(self.layoutForm)

        self.layoutMain.addStretch()

        self.lblError = QLabel("")
        self.lblError.setWordWrap(True)
        self.lblError.setStyleSheet("QLabel { color: red; }")
        self.lblError.setVisible(False)
        self.layoutMain.addWidget(self.lblError)
        self.updateWidgets()

        # process used to execute the binary that will compute the fluid properties
        self.process = QProcess(self)
        self.process.setProcessChannelMode(QProcess.SeparateChannels)
        self.process.started.connect(self._onStarted)
        self.process.readyReadStandardOutput.connect(self._onReadStdOut)
        self.process.readyReadStandardError.connect(self._onReadStdErr)
        self.process.finished.connect(self._onJobFinished)
        self.process.error.connect(self._onError)

        # create a temporary file which is used to generate the input file
        self.fd, self.input_file_name = mkstemp()

    def updateWidgets(self):
        enable = True
        for input in self.inputs():
            if len(self.ctlInputs[input['name']].text()) == 0:
                enable = False
                break
        self.btnCalculate.setEnabled(enable)
        self.CalculateShortcut.setEnabled(enable)

        title = "Fluid Property Interrogator"
        if self.modified:
            title += " *"
        self.parent.setWindowTitle(title)

    def onModified(self):
        self.setModified(True)
        self.updateWidgets()

    def setExecutablePath(self, exe_path):
        self.exe_path = exe_path

    def onCtrlReturn(self):
        self.btnCalculate.animateClick()

    def computeProperties(self):
        """
        Called when the computation of properties is requested
        """

        self.buildInputFile(self.input_file_name)

        args = ['-i', self.input_file_name, '--no-color']
        self.process.start(self.exe_path, args)
        self.process.waitForStarted()
        self.setModified(False)
        self.updateWidgets()

    def setModified(self, modified):
        self.modified = modified

    def buildInputFile(self, file_name):
        """
        Write the input file into a file

        Inputs:
            file_name[str]: File name which we write into
        """

        with open(file_name, "w+") as f:
            f.write("[FluidPropertiesInterrogator]\n")
            f.write("  fp = fp\n")
            f.write("  json = true\n")
            for p in self.inputs():
                name = p['name']
                f.write("  {} = {}\n".format(name,
                                             self.ctlInputs[name].text()))

            f.write("[]\n")
            f.write("[Modules]\n")
            f.write("  [./FluidProperties]\n")
            f.write("    {}\n".format(
                self.parent.fluidPropertyInputFileBlock()))
            f.write("  [../]\n")
            f.write("[]\n")

    @pyqtSlot()
    def _onStarted(self):
        self.json_str = ""
        self.json_data_on = False
        self.error_str = ""
        self.error_data_on = False

    @pyqtSlot()
    def _onReadStdOut(self):
        # store only the JSON data
        self.process.setReadChannel(QProcess.StandardOutput)
        while self.process.canReadLine():
            line = self.process.readLine().data().decode("utf-8").rstrip()
            if line == '**START JSON DATA**':
                self.json_data_on = True
            elif line == '**END JSON DATA**':
                self.json_data_on = False
            elif self.json_data_on:
                self.json_str += line

    @pyqtSlot()
    def _onReadStdErr(self):
        self.process.setReadChannel(QProcess.StandardError)
        while self.process.canReadLine():
            line = self.process.readLine().data().decode("utf-8").rstrip()
            if line == '*** ERROR ***':
                self.error_data_on = True
            elif line[:13] == 'Stack frames:':
                self.error_data_on = False
            elif self.error_data_on:
                self.error_str += line

    @pyqtSlot(int, QProcess.ExitStatus)
    def _onJobFinished(self, code, status):
        if code == 0:
            try:
                j = json.loads(self.json_str)
                # enter the data into the controls
                for p in self.outputs():
                    name = p['name']
                    val = float(j[self.jsonSectionName()][name])
                    if abs(val) < 0.1:
                        s = "{:.5e}".format(val)
                    else:
                        s = "{:.5f}".format(val)
                    self.ctlProp[name].setText(s)
            except:
                # this would happen if people used MOOSE that does not support
                # printing fluid properties in JSON format
                pass
            self.lblError.setVisible(False)
        else:
            self.lblError.setText(self.error_str)
            self.lblError.setVisible(True)

    @pyqtSlot(QProcess.ProcessError)
    def _onError(self, err):
        print("error:", err)
Beispiel #27
0
class EricapiExecDialog(QDialog, Ui_EricapiExecDialog):
    """
    Class implementing a dialog to show the output of the ericapi process.
    
    This class starts a QProcess and displays a dialog that
    shows the output of the documentation command process.
    """
    def __init__(self, cmdname, parent=None):
        """
        Constructor
        
        @param cmdname name of the ericapi generator (string)
        @param parent parent widget of this dialog (QWidget)
        """
        super(EricapiExecDialog, self).__init__(parent)
        self.setModal(True)
        self.setupUi(self)
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
        
        self.process = None
        self.cmdname = cmdname
        
    def start(self, args, fn):
        """
        Public slot to start the ericapi command.
        
        @param args commandline arguments for ericapi program (list of strings)
        @param fn filename or dirname to be processed by ericapi program
            (string)
        @return flag indicating the successful start of the process (boolean)
        """
        self.errorGroup.hide()
        
        self.filename = fn
        if os.path.isdir(self.filename):
            dname = os.path.abspath(self.filename)
            fname = "."
            if os.path.exists(os.path.join(dname, "__init__.py")):
                fname = os.path.basename(dname)
                dname = os.path.dirname(dname)
        else:
            dname = os.path.dirname(self.filename)
            fname = os.path.basename(self.filename)
        
        self.contents.clear()
        self.errors.clear()
        
        program = args[0]
        del args[0]
        args.append(fname)
        
        self.process = QProcess()
        self.process.setWorkingDirectory(dname)
        
        self.process.readyReadStandardOutput.connect(self.__readStdout)
        self.process.readyReadStandardError.connect(self.__readStderr)
        self.process.finished.connect(self.__finish)
            
        self.setWindowTitle(
            self.tr('{0} - {1}').format(self.cmdname, self.filename))
        self.process.start(program, args)
        procStarted = self.process.waitForStarted(5000)
        if not procStarted:
            E5MessageBox.critical(
                self,
                self.tr('Process Generation Error'),
                self.tr(
                    'The process {0} could not be started. '
                    'Ensure, that it is in the search path.'
                ).format(program))
        return procStarted
        
    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.accept()
        elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
            self.__finish()
        
    def __finish(self):
        """
        Private slot called when the process finished.
        
        It is called when the process finished or
        the user pressed the button.
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        
        self.process = None
        
        self.contents.insertPlainText(
            self.tr('\n{0} finished.\n').format(self.cmdname))
        self.contents.ensureCursorVisible()
        
    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        self.process.setReadChannel(QProcess.StandardOutput)
        
        while self.process.canReadLine():
            s = str(self.process.readLine(),
                    Preferences.getSystem("IOEncoding"),
                    'replace')
            self.contents.insertPlainText(s)
            self.contents.ensureCursorVisible()
        
    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        self.process.setReadChannel(QProcess.StandardError)
        
        while self.process.canReadLine():
            self.errorGroup.show()
            s = str(self.process.readLine(),
                    Preferences.getSystem("IOEncoding"),
                    'replace')
            self.errors.insertPlainText(s)
            self.errors.ensureCursorVisible()
Beispiel #28
0
class HgBookmarksInOutDialog(QDialog, Ui_HgBookmarksInOutDialog):
    """
    Class implementing a dialog to show a list of incoming or outgoing
    bookmarks.
    """
    INCOMING = 0
    OUTGOING = 1
    
    def __init__(self, vcs, mode, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param mode mode of the dialog (HgBookmarksInOutDialog.INCOMING,
            HgBookmarksInOutDialog.OUTGOING)
        @param parent reference to the parent widget (QWidget)
        @exception ValueError raised to indicate an invalid dialog mode
        """
        super(HgBookmarksInOutDialog, self).__init__(parent)
        self.setupUi(self)
        self.setWindowFlags(Qt.Window)
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
        
        if mode not in [self.INCOMING, self.OUTGOING]:
            raise ValueError("Bad value for mode")
        if mode == self.INCOMING:
            self.setWindowTitle(self.tr("Mercurial Incoming Bookmarks"))
        elif mode == self.OUTGOING:
            self.setWindowTitle(self.tr("Mercurial Outgoing Bookmarks"))
        
        self.process = QProcess()
        self.vcs = vcs
        self.mode = mode
        self.__hgClient = vcs.getClient()
        
        self.bookmarksList.headerItem().setText(
            self.bookmarksList.columnCount(), "")
        self.bookmarksList.header().setSortIndicator(3, Qt.AscendingOrder)
        
        self.process.finished.connect(self.__procFinished)
        self.process.readyReadStandardOutput.connect(self.__readStdout)
        self.process.readyReadStandardError.connect(self.__readStderr)
        
        self.show()
        QCoreApplication.processEvents()
    
    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.__hgClient:
            if self.__hgClient.isExecuting():
                self.__hgClient.cancel()
        else:
            if self.process is not None and \
               self.process.state() != QProcess.NotRunning:
                self.process.terminate()
                QTimer.singleShot(2000, self.process.kill)
                self.process.waitForFinished(3000)
        
        e.accept()
    
    def start(self, path):
        """
        Public slot to start the bookmarks command.
        
        @param path name of directory to be listed (string)
        @exception ValueError raised to indicate an invalid dialog mode
        """
        self.errorGroup.hide()
        
        self.intercept = False
        self.activateWindow()
        
        dname, fname = self.vcs.splitPath(path)
        
        # find the root of the repo
        repodir = dname
        while not os.path.isdir(os.path.join(repodir, self.vcs.adminDir)):
            repodir = os.path.dirname(repodir)
            if os.path.splitdrive(repodir)[1] == os.sep:
                return
        
        if self.mode == self.INCOMING:
            args = self.vcs.initCommand("incoming")
        elif self.mode == self.OUTGOING:
            args = self.vcs.initCommand("outgoing")
        else:
            raise ValueError("Bad value for mode")
        args.append('--bookmarks')
        
        if self.__hgClient:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            
            out, err = self.__hgClient.runcommand(args)
            if err:
                self.__showError(err)
            if out:
                for line in out.splitlines():
                    self.__processOutputLine(line)
                    if self.__hgClient.wasCanceled():
                        break
            self.__finish()
        else:
            self.process.kill()
            self.process.setWorkingDirectory(repodir)
            
            self.process.start('hg', args)
            procStarted = self.process.waitForStarted(5000)
            if not procStarted:
                self.inputGroup.setEnabled(False)
                self.inputGroup.hide()
                E5MessageBox.critical(
                    self,
                    self.tr('Process Generation Error'),
                    self.tr(
                        'The process {0} could not be started. '
                        'Ensure, that it is in the search path.'
                    ).format('hg'))
            else:
                self.inputGroup.setEnabled(True)
                self.inputGroup.show()
    
    def __finish(self):
        """
        Private slot called when the process finished or the user pressed
        the button.
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        self.buttonBox.button(QDialogButtonBox.Close).setFocus(
            Qt.OtherFocusReason)
        
        if self.bookmarksList.topLevelItemCount() == 0:
            # no bookmarks defined
            self.__generateItem(self.tr("no bookmarks found"), "")
        self.__resizeColumns()
        self.__resort()
    
    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.close()
        elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
            if self.__hgClient:
                self.__hgClient.cancel()
            else:
                self.__finish()
    
    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__finish()
    
    def __resort(self):
        """
        Private method to resort the tree.
        """
        self.bookmarksList.sortItems(
            self.bookmarksList.sortColumn(),
            self.bookmarksList.header().sortIndicatorOrder())
    
    def __resizeColumns(self):
        """
        Private method to resize the list columns.
        """
        self.bookmarksList.header().resizeSections(
            QHeaderView.ResizeToContents)
        self.bookmarksList.header().setStretchLastSection(True)
    
    def __generateItem(self, changeset, name):
        """
        Private method to generate a bookmark item in the bookmarks list.
        
        @param changeset changeset of the bookmark (string)
        @param name name of the bookmark (string)
        """
        QTreeWidgetItem(self.bookmarksList, [
            name,
            changeset])
    
    def __readStdout(self):
        """
        Private slot to handle the readyReadStdout signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        self.process.setReadChannel(QProcess.StandardOutput)
        
        while self.process.canReadLine():
            s = str(self.process.readLine(), self.vcs.getEncoding(), 'replace')
            self.__processOutputLine(s)
    
    def __processOutputLine(self, line):
        """
        Private method to process the lines of output.
        
        @param line output line to be processed (string)
        """
        if line.startswith(" "):
            li = line.strip().split()
            changeset = li[-1]
            del li[-1]
            name = " ".join(li)
            self.__generateItem(changeset, name)
    
    def __readStderr(self):
        """
        Private slot to handle the readyReadStderr signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            s = str(self.process.readAllStandardError(),
                    self.vcs.getEncoding(), 'replace')
            self.__showError(s)
    
    def __showError(self, out):
        """
        Private slot to show some error.
        
        @param out error to be shown (string)
        """
        self.errorGroup.show()
        self.errors.insertPlainText(out)
        self.errors.ensureCursorVisible()
    
    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)
    
    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the subversion process.
        """
        input = self.input.text()
        input += os.linesep
        
        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(input)
            self.errors.ensureCursorVisible()
        
        self.process.write(input)
        
        self.passwordCheckBox.setChecked(False)
        self.input.clear()
    
    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()
    
    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(HgBookmarksInOutDialog, self).keyPressEvent(evt)
Beispiel #29
0
class SvnLogDialog(QWidget, Ui_SvnLogDialog):
    """
    Class implementing a dialog to show the output of the svn log command
    process.
    
    The dialog is nonmodal. Clicking a link in the upper text pane shows
    a diff of the versions.
    """
    def __init__(self, vcs, isFile=False, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param isFile flag indicating log for a file is to be shown (boolean)
        @param parent parent widget (QWidget)
        """
        super(SvnLogDialog, self).__init__(parent)
        self.setupUi(self)
        
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        
        self.process = QProcess()
        self.vcs = vcs
        
        self.contents.setHtml(
            self.tr('<b>Processing your request, please wait...</b>'))
        
        self.process.finished.connect(self.__procFinished)
        self.process.readyReadStandardOutput.connect(self.__readStdout)
        self.process.readyReadStandardError.connect(self.__readStderr)
        
        self.contents.anchorClicked.connect(self.__sourceChanged)
        
        self.rx_sep = QRegExp('\\-+\\s*')
        self.rx_sep2 = QRegExp('=+\\s*')
        self.rx_rev = QRegExp(
            'rev ([0-9]+):  ([^|]*) \| ([^|]*) \| ([0-9]+) .*')
        # "rev" followed by one or more decimals followed by a colon followed
        # anything up to " | " (twice) followed by one or more decimals
        # followed by anything
        self.rx_rev2 = QRegExp(
            'r([0-9]+) \| ([^|]*) \| ([^|]*) \| ([0-9]+) .*')
        # "r" followed by one or more decimals followed by " | " followed
        # anything up to " | " (twice) followed by one or more decimals
        # followed by anything
        self.rx_flags = QRegExp('   ([ADM])( .*)\\s*')
        # three blanks followed by A or D or M
        self.rx_changed = QRegExp('Changed .*\\s*')
        
        self.flags = {
            'A': self.tr('Added'),
            'D': self.tr('Deleted'),
            'M': self.tr('Modified')
        }
        
        self.revisions = []  # stack of remembered revisions
        self.revString = self.tr('revision')
        
        self.buf = []        # buffer for stdout
        self.diff = None
        
        self.sbsCheckBox.setEnabled(isFile)
        self.sbsCheckBox.setVisible(isFile)
        
    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        e.accept()
        
    def start(self, fn, noEntries=0):
        """
        Public slot to start the cvs log command.
        
        @param fn filename to show the log for (string)
        @param noEntries number of entries to show (integer)
        """
        self.errorGroup.hide()
        QApplication.processEvents()
        
        self.intercept = False
        self.filename = fn
        self.dname, self.fname = self.vcs.splitPath(fn)
        
        self.process.kill()
        
        args = []
        args.append('log')
        self.vcs.addArguments(args, self.vcs.options['global'])
        self.vcs.addArguments(args, self.vcs.options['log'])
        if noEntries:
            args.append('--limit')
            args.append(str(noEntries))
        self.activateWindow()
        self.raise_()
        args.append(self.fname)
        
        self.process.setWorkingDirectory(self.dname)
        
        self.process.start('svn', args)
        procStarted = self.process.waitForStarted(5000)
        if not procStarted:
            self.inputGroup.setEnabled(False)
            E5MessageBox.critical(
                self,
                self.tr('Process Generation Error'),
                self.tr(
                    'The process {0} could not be started. '
                    'Ensure, that it is in the search path.'
                ).format('svn'))
        
    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()
        
        self.contents.clear()
        
        lvers = 1
        for s in self.buf:
            rev_match = False
            if self.rx_rev.exactMatch(s):
                ver = self.rx_rev.cap(1)
                author = self.rx_rev.cap(2)
                date = self.rx_rev.cap(3)
                # number of lines is ignored
                rev_match = True
            elif self.rx_rev2.exactMatch(s):
                ver = self.rx_rev2.cap(1)
                author = self.rx_rev2.cap(2)
                date = self.rx_rev2.cap(3)
                # number of lines is ignored
                rev_match = True
            
            if rev_match:
                dstr = '<b>{0} {1}</b>'.format(self.revString, ver)
                try:
                    lv = self.revisions[lvers]
                    lvers += 1
                    url = QUrl()
                    url.setScheme("file")
                    url.setPath(self.filename)
                    if qVersion() >= "5.0.0":
                        query = lv + '_' + ver
                        url.setQuery(query)
                    else:
                        query = QByteArray()
                        query.append(lv).append('_').append(ver)
                        url.setEncodedQuery(query)
                    dstr += ' [<a href="{0}" name="{1}">{2}</a>]'.format(
                        url.toString(), query,
                        self.tr('diff to {0}').format(lv),
                    )
                except IndexError:
                    pass
                dstr += '<br />\n'
                self.contents.insertHtml(dstr)
                
                dstr = self.tr('<i>author: {0}</i><br />\n').format(author)
                self.contents.insertHtml(dstr)
                
                dstr = self.tr('<i>date: {0}</i><br />\n').format(date)
                self.contents.insertHtml(dstr)
            
            elif self.rx_sep.exactMatch(s) or self.rx_sep2.exactMatch(s):
                self.contents.insertHtml('<hr />\n')
            
            elif self.rx_flags.exactMatch(s):
                dstr = self.flags[self.rx_flags.cap(1)]
                dstr += self.rx_flags.cap(2)
                dstr += '<br />\n'
                self.contents.insertHtml(dstr)
            
            elif self.rx_changed.exactMatch(s):
                dstr = '<br />{0}<br />\n'.format(s)
                self.contents.insertHtml(dstr)
            
            else:
                if s == "":
                    s = self.contents.insertHtml('<br />\n')
                else:
                    self.contents.insertHtml(Utilities.html_encode(s))
                    self.contents.insertHtml('<br />\n')
        
        tc = self.contents.textCursor()
        tc.movePosition(QTextCursor.Start)
        self.contents.setTextCursor(tc)
        self.contents.ensureCursorVisible()
        
    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.
        
        It reads the output of the process and inserts it into a buffer.
        """
        self.process.setReadChannel(QProcess.StandardOutput)
        
        while self.process.canReadLine():
            line = str(self.process.readLine(),
                       Preferences.getSystem("IOEncoding"),
                       'replace')
            self.buf.append(line)
            if self.rx_rev.exactMatch(line):
                ver = self.rx_rev.cap(1)
                # save revision number for later use
                self.revisions.append(ver)
            elif self.rx_rev2.exactMatch(line):
                ver = self.rx_rev2.cap(1)
                # save revision number for later use
                self.revisions.append(ver)
        
    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            self.errorGroup.show()
            s = str(self.process.readAllStandardError(),
                    Preferences.getSystem("IOEncoding"),
                    'replace')
            self.errors.insertPlainText(s)
            self.errors.ensureCursorVisible()
        
    def __sourceChanged(self, url):
        """
        Private slot to handle the sourceChanged signal of the contents pane.
        
        @param url the url that was clicked (QUrl)
        """
        self.contents.setSource(QUrl(''))
        filename = url.path()
        if Utilities.isWindowsPlatform():
            if filename.startswith("/"):
                filename = filename[1:]
        if qVersion() >= "5.0.0":
            ver = url.query()
        else:
            ver = bytes(url.encodedQuery()).decode()
        v1 = ver.split('_')[0]
        v2 = ver.split('_')[1]
        if v1 == "" or v2 == "":
            return
        self.contents.scrollToAnchor(ver)
        
        if self.sbsCheckBox.isEnabled() and self.sbsCheckBox.isChecked():
            self.vcs.svnSbsDiff(filename, revisions=(v1, v2))
        else:
            if self.diff is None:
                from .SvnDiffDialog import SvnDiffDialog
                self.diff = SvnDiffDialog(self.vcs)
            self.diff.show()
            self.diff.start(filename, [v1, v2])
        
    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)
        
    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the subversion process.
        """
        input = self.input.text()
        input += os.linesep
        
        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(input)
            self.errors.ensureCursorVisible()
        
        self.process.write(input)
        
        self.passwordCheckBox.setChecked(False)
        self.input.clear()
        
    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()
        
    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(SvnLogDialog, self).keyPressEvent(evt)
Beispiel #30
0
class HgLogDialog(QWidget, Ui_HgLogDialog):
    """
    Class implementing a dialog to show the output of the hg log command
    process.
    
    The dialog is nonmodal. Clicking a link in the upper text pane shows
    a diff of the revisions.
    """
    def __init__(self, vcs, mode="log", bundle=None, isFile=False,
                 parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param mode mode of the dialog (string; one of log, incoming, outgoing)
        @param bundle name of a bundle file (string)
        @param isFile flag indicating log for a file is to be shown (boolean)
        @param parent parent widget (QWidget)
        """
        super(HgLogDialog, self).__init__(parent)
        self.setupUi(self)
        
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        
        self.process = QProcess()
        self.vcs = vcs
        if mode in ("log", "incoming", "outgoing"):
            self.mode = mode
        else:
            self.mode = "log"
        self.bundle = bundle
        self.__hgClient = self.vcs.getClient()
        
        self.contents.setHtml(
            self.tr('<b>Processing your request, please wait...</b>'))
        
        self.process.finished.connect(self.__procFinished)
        self.process.readyReadStandardOutput.connect(self.__readStdout)
        self.process.readyReadStandardError.connect(self.__readStderr)
        
        self.contents.anchorClicked.connect(self.__sourceChanged)
        
        self.revisions = []  # stack of remembered revisions
        self.revString = self.tr('Revision')
        self.projectMode = False
        
        self.logEntries = []        # list of log entries
        self.lastLogEntry = {}
        self.fileCopies = {}
        self.endInitialText = False
        self.initialText = []
        
        self.diff = None
        
        self.sbsCheckBox.setEnabled(isFile)
        self.sbsCheckBox.setVisible(isFile)
    
    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.__hgClient:
            if self.__hgClient.isExecuting():
                self.__hgClient.cancel()
        else:
            if self.process is not None and \
               self.process.state() != QProcess.NotRunning:
                self.process.terminate()
                QTimer.singleShot(2000, self.process.kill)
                self.process.waitForFinished(3000)
        
        e.accept()
    
    def start(self, fn, noEntries=0, revisions=None):
        """
        Public slot to start the hg log command.
        
        @param fn filename to show the log for (string)
        @param noEntries number of entries to show (integer)
        @param revisions revisions to show log for (list of strings)
        """
        self.errorGroup.hide()
        QApplication.processEvents()
        
        self.intercept = False
        self.filename = fn
        self.dname, self.fname = self.vcs.splitPath(fn)
        
        # find the root of the repo
        self.repodir = self.dname
        while not os.path.isdir(os.path.join(self.repodir, self.vcs.adminDir)):
            self.repodir = os.path.dirname(self.repodir)
            if os.path.splitdrive(self.repodir)[1] == os.sep:
                return
        
        self.projectMode = (self.fname == "." and self.dname == self.repodir)
        
        self.activateWindow()
        self.raise_()
        
        preargs = []
        args = self.vcs.initCommand(self.mode)
        if noEntries and self.mode == "log":
            args.append('--limit')
            args.append(str(noEntries))
        if self.mode in ("incoming", "outgoing"):
            args.append("--newest-first")
            if self.vcs.hasSubrepositories():
                args.append("--subrepos")
        if self.mode == "log":
            args.append('--copies')
        if self.vcs.version >= (3, 0):
            args.append('--template')
            args.append(os.path.join(os.path.dirname(__file__),
                                     "templates",
                                     "logDialogBookmarkPhase.tmpl"))
        else:
            args.append('--style')
            if self.vcs.version >= (2, 1):
                args.append(os.path.join(os.path.dirname(__file__),
                                         "styles",
                                         "logDialogBookmarkPhase.style"))
            else:
                args.append(os.path.join(os.path.dirname(__file__),
                                         "styles",
                                         "logDialogBookmark.style"))
        if self.mode == "incoming":
            if self.bundle:
                args.append(self.bundle)
            elif not self.vcs.hasSubrepositories():
                project = e5App().getObject("Project")
                self.vcs.bundleFile = os.path.join(
                    project.getProjectManagementDir(), "hg-bundle.hg")
                if os.path.exists(self.vcs.bundleFile):
                    os.remove(self.vcs.bundleFile)
                preargs = args[:]
                preargs.append("--quiet")
                preargs.append('--bundle')
                preargs.append(self.vcs.bundleFile)
                args.append(self.vcs.bundleFile)
        if revisions:
            for rev in revisions:
                args.append("--rev")
                args.append(rev)
        if not self.projectMode:
            args.append(self.filename)
        
        if self.__hgClient:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            
            if preargs:
                out, err = self.__hgClient.runcommand(preargs)
            else:
                err = ""
            if err:
                self.__showError(err)
            elif self.mode != "incoming" or \
                (self.vcs.bundleFile and
                 os.path.exists(self.vcs.bundleFile)) or \
                    self.bundle:
                out, err = self.__hgClient.runcommand(args)
                if err:
                    self.__showError(err)
                if out and self.isVisible():
                    for line in out.splitlines(True):
                        self.__processOutputLine(line)
                        if self.__hgClient.wasCanceled():
                            break
            self.__finish()
        else:
            self.process.kill()
            
            self.process.setWorkingDirectory(self.repodir)
            
            if preargs:
                process = QProcess()
                process.setWorkingDirectory(self.repodir)
                process.start('hg', args)
                procStarted = process.waitForStarted(5000)
                if procStarted:
                    process.waitForFinished(30000)
            
            if self.mode != "incoming" or \
                (self.vcs.bundleFile and
                 os.path.exists(self.vcs.bundleFile)) or \
                    self.bundle:
                self.process.start('hg', args)
                procStarted = self.process.waitForStarted(5000)
                if not procStarted:
                    self.inputGroup.setEnabled(False)
                    self.inputGroup.hide()
                    E5MessageBox.critical(
                        self,
                        self.tr('Process Generation Error'),
                        self.tr(
                            'The process {0} could not be started. '
                            'Ensure, that it is in the search path.'
                        ).format('hg'))
            else:
                self.__finish()
    
    def __getParents(self, rev):
        """
        Private method to get the parents of the currently viewed
        file/directory.
        
        @param rev revision number to get parents for (string)
        @return list of parent revisions (list of strings)
        """
        errMsg = ""
        parents = []
        
        if int(rev) > 0:
            args = self.vcs.initCommand("parents")
            if self.mode == "incoming":
                if self.bundle:
                    args.append("--repository")
                    args.append(self.bundle)
                elif self.vcs.bundleFile and \
                        os.path.exists(self.vcs.bundleFile):
                    args.append("--repository")
                    args.append(self.vcs.bundleFile)
            args.append("--template")
            args.append("{rev}:{node|short}\n")
            args.append("-r")
            args.append(rev)
            if not self.projectMode:
                args.append(self.filename)
            
            output = ""
            if self.__hgClient:
                output, errMsg = self.__hgClient.runcommand(args)
            else:
                process = QProcess()
                process.setWorkingDirectory(self.repodir)
                process.start('hg', args)
                procStarted = process.waitForStarted(5000)
                if procStarted:
                    finished = process.waitForFinished(30000)
                    if finished and process.exitCode() == 0:
                        output = str(process.readAllStandardOutput(),
                                     self.vcs.getEncoding(), 'replace')
                    else:
                        if not finished:
                            errMsg = self.tr(
                                "The hg process did not finish within 30s.")
                else:
                    errMsg = self.tr("Could not start the hg executable.")
            
            if errMsg:
                E5MessageBox.critical(
                    self,
                    self.tr("Mercurial Error"),
                    errMsg)
            
            if output:
                parents = [p for p in output.strip().splitlines()]
        
        return parents
    
    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__finish()
    
    def __finish(self):
        """
        Private slot called when the process finished or the user pressed
        the button.
        """
        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()
        
        self.contents.clear()
        
        if not self.logEntries:
            self.errors.append(self.tr("No log available for '{0}'")
                               .format(self.filename))
            self.errorGroup.show()
            return
        
        html = ""
        
        if self.initialText:
            for line in self.initialText:
                html += Utilities.html_encode(line.strip())
                html += '<br />\n'
            html += '{0}<br/>\n'.format(80 * "=")
            
        for entry in self.logEntries:
            fileCopies = {}
            if entry["file_copies"]:
                for fentry in entry["file_copies"].split(", "):
                    newName, oldName = fentry[:-1].split(" (")
                    fileCopies[newName] = oldName
            
            rev, hexRev = entry["change"].split(":")
            dstr = '<p><b>{0} {1}</b>'.format(self.revString, entry["change"])
            if entry["parents"]:
                parents = entry["parents"].split()
            else:
                parents = self.__getParents(rev)
            for parent in parents:
                url = QUrl()
                url.setScheme("file")
                url.setPath(self.filename)
                if qVersion() >= "5.0.0":
                    query = parent.split(":")[0] + '_' + rev
                    url.setQuery(query)
                else:
                    query = QByteArray()
                    query.append(parent.split(":")[0]).append('_').append(rev)
                    url.setEncodedQuery(query)
                dstr += ' [<a href="{0}" name="{1}" id="{1}">{2}</a>]'.format(
                    url.toString(), query,
                    self.tr('diff to {0}').format(parent),
                )
            dstr += '<br />\n'
            html += dstr
            
            if "phase" in entry:
                html += self.tr("Phase: {0}<br />\n")\
                    .format(entry["phase"])
            
            html += self.tr("Branch: {0}<br />\n")\
                .format(entry["branches"])
            
            html += self.tr("Tags: {0}<br />\n").format(entry["tags"])
            
            if "bookmarks" in entry:
                html += self.tr("Bookmarks: {0}<br />\n")\
                    .format(entry["bookmarks"])
            
            html += self.tr("Parents: {0}<br />\n")\
                .format(entry["parents"])
            
            html += self.tr('<i>Author: {0}</i><br />\n')\
                .format(Utilities.html_encode(entry["user"]))
            
            date, time = entry["date"].split()[:2]
            html += self.tr('<i>Date: {0}, {1}</i><br />\n')\
                .format(date, time)
            
            for line in entry["description"]:
                html += Utilities.html_encode(line.strip())
                html += '<br />\n'
            
            if entry["file_adds"]:
                html += '<br />\n'
                for f in entry["file_adds"].strip().split(", "):
                    if f in fileCopies:
                        html += self.tr(
                            'Added {0} (copied from {1})<br />\n')\
                            .format(Utilities.html_encode(f),
                                    Utilities.html_encode(fileCopies[f]))
                    else:
                        html += self.tr('Added {0}<br />\n')\
                            .format(Utilities.html_encode(f))
            
            if entry["files_mods"]:
                html += '<br />\n'
                for f in entry["files_mods"].strip().split(", "):
                    html += self.tr('Modified {0}<br />\n')\
                        .format(Utilities.html_encode(f))
            
            if entry["file_dels"]:
                html += '<br />\n'
                for f in entry["file_dels"].strip().split(", "):
                    html += self.tr('Deleted {0}<br />\n')\
                        .format(Utilities.html_encode(f))
            
            html += '</p>{0}<br/>\n'.format(60 * "=")
        
        self.contents.setHtml(html)
        tc = self.contents.textCursor()
        tc.movePosition(QTextCursor.Start)
        self.contents.setTextCursor(tc)
        self.contents.ensureCursorVisible()
    
    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.
        
        It reads the output of the process and inserts it into a buffer.
        """
        self.process.setReadChannel(QProcess.StandardOutput)
        
        while self.process.canReadLine():
            s = str(self.process.readLine(), self.vcs.getEncoding(), 'replace')
            self.__processOutputLine(s)
    
    def __processOutputLine(self, line):
        """
        Private method to process the lines of output.
        
        @param line output line to be processed (string)
        """
        if line == "@@@\n":
            self.logEntries.append(self.lastLogEntry)
            self.lastLogEntry = {}
            self.fileCopies = {}
        else:
            try:
                key, value = line.split("|", 1)
            except ValueError:
                key = ""
                value = line
            if key == "change":
                self.endInitialText = True
            if key in ("change", "tags", "parents", "user", "date",
                       "file_copies", "file_adds", "files_mods", "file_dels",
                       "bookmarks", "phase"):
                self.lastLogEntry[key] = value.strip()
            elif key == "branches":
                if value.strip():
                    self.lastLogEntry[key] = value.strip()
                else:
                    self.lastLogEntry[key] = "default"
            elif key == "description":
                self.lastLogEntry[key] = [value.strip()]
            else:
                if self.endInitialText:
                    self.lastLogEntry["description"].append(value.strip())
                else:
                    self.initialText.append(value)
    
    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            s = str(self.process.readAllStandardError(),
                    self.vcs.getEncoding(), 'replace')
            self.__showError(s)
    
    def __showError(self, out):
        """
        Private slot to show some error.
        
        @param out error to be shown (string)
        """
        self.errorGroup.show()
        self.errors.insertPlainText(out)
        self.errors.ensureCursorVisible()
    
    def __sourceChanged(self, url):
        """
        Private slot to handle the sourceChanged signal of the contents pane.
        
        @param url the url that was clicked (QUrl)
        """
        filename = url.path()
        if Utilities.isWindowsPlatform():
            if filename.startswith("/"):
                filename = filename[1:]
        if qVersion() >= "5.0.0":
            ver = url.query()
        else:
            ver = bytes(url.encodedQuery()).decode()
        v1, v2 = ver.split('_')
        if v1 == "" or v2 == "":
            return
        self.contents.scrollToAnchor(ver)
        
        if self.sbsCheckBox.isEnabled() and self.sbsCheckBox.isChecked():
            self.vcs.hgSbsDiff(filename, revisions=(v1, v2))
        else:
            if self.diff is None:
                from .HgDiffDialog import HgDiffDialog
                self.diff = HgDiffDialog(self.vcs)
            self.diff.show()
            self.diff.start(filename, [v1, v2], self.bundle)
    
    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)
    
    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the hg process.
        """
        input = self.input.text()
        input += os.linesep
        
        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(input)
            self.errors.ensureCursorVisible()
        
        self.process.write(input)
        
        self.passwordCheckBox.setChecked(False)
        self.input.clear()
    
    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()
    
    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(HgLogDialog, self).keyPressEvent(evt)
Beispiel #31
0
class GitTagBranchListDialog(QDialog, Ui_GitTagBranchListDialog):
    """
    Class implementing a dialog to show a list of tags or branches.
    """
    def __init__(self, vcs, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param parent parent widget (QWidget)
        """
        super(GitTagBranchListDialog, self).__init__(parent)
        self.setupUi(self)
        self.setWindowFlags(Qt.Window)

        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)

        self.process = QProcess()
        self.vcs = vcs

        self.tagList.headerItem().setText(self.tagList.columnCount(), "")
        self.tagList.header().setSortIndicator(1, Qt.AscendingOrder)

        self.process.finished.connect(self.__procFinished)
        self.process.readyReadStandardOutput.connect(self.__readStdout)
        self.process.readyReadStandardError.connect(self.__readStderr)

        self.show()
        QCoreApplication.processEvents()

    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if (self.process is not None
                and self.process.state() != QProcess.NotRunning):
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)

        e.accept()

    def start(self, path, tags, listAll=True, merged=True):
        """
        Public slot to start the tag/branch list command.
        
        @param path name of directory to be listed (string)
        @param tags flag indicating a list of tags is requested
            (False = branches, True = tags)
        @param listAll flag indicating to show all tags or branches (boolean)
        @param merged flag indicating to show only merged or non-merged
            branches (boolean)
        """
        self.tagList.clear()

        self.errorGroup.hide()

        self.intercept = False
        self.tagsMode = tags
        if tags:
            self.tagList.setHeaderItem(
                QTreeWidgetItem([
                    self.tr("Commit"),
                    self.tr("Name"),
                    self.tr("Annotation Message")
                ]))
        else:
            self.setWindowTitle(self.tr("Git Branches List"))
            self.tagList.setHeaderItem(
                QTreeWidgetItem([self.tr("Commit"),
                                 self.tr("Name")]))
        self.activateWindow()

        dname, fname = self.vcs.splitPath(path)

        # find the root of the repo
        self.repodir = dname
        while not os.path.isdir(os.path.join(self.repodir, self.vcs.adminDir)):
            self.repodir = os.path.dirname(self.repodir)
            if os.path.splitdrive(self.repodir)[1] == os.sep:
                return

        if self.tagsMode:
            args = self.vcs.initCommand("tag")
            args.append('--list')
            args.append('-n')
        else:
            args = self.vcs.initCommand("branch")
            args.append('--list')
            args.append('--all')
            args.append('--verbose')
            if not listAll:
                if merged:
                    args.append("--merged")
                else:
                    args.append("--no-merged")

        self.process.kill()
        self.process.setWorkingDirectory(self.repodir)

        self.process.start('git', args)
        procStarted = self.process.waitForStarted(5000)
        if not procStarted:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            E5MessageBox.critical(
                self, self.tr('Process Generation Error'),
                self.tr(
                    'The process {0} could not be started. '
                    'Ensure, that it is in the search path.').format('git'))
        else:
            self.inputGroup.setEnabled(True)
            self.inputGroup.show()

    def __finish(self):
        """
        Private slot called when the process finished or the user pressed
        the button.
        """
        if (self.process is not None
                and self.process.state() != QProcess.NotRunning):
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)

        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()

        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        self.buttonBox.button(QDialogButtonBox.Close).setFocus(
            Qt.OtherFocusReason)

        self.__resizeColumns()
        self.__resort()

    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.close()
        elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
            self.__finish()

    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__finish()

    def __resort(self):
        """
        Private method to resort the tree.
        """
        self.tagList.sortItems(self.tagList.sortColumn(),
                               self.tagList.header().sortIndicatorOrder())

    def __resizeColumns(self):
        """
        Private method to resize the list columns.
        """
        self.tagList.header().resizeSections(QHeaderView.ResizeToContents)
        self.tagList.header().setStretchLastSection(True)

    def __generateItem(self, commit, name, msg="", bold=False, italic=False):
        """
        Private method to generate a tag item in the tag list.
        
        @param commit commit id of the tag/branch (string)
        @param name name of the tag/branch (string)
        @param msg tag annotation message
        @param bold flag indicating to show the entry in bold (boolean)
        @param italic flag indicating to show the entry in italic (boolean)
        """
        itm = QTreeWidgetItem(self.tagList)
        itm.setData(0, Qt.DisplayRole, commit)
        itm.setData(1, Qt.DisplayRole, name)
        if msg:
            itm.setData(2, Qt.DisplayRole, msg)
        itm.setTextAlignment(0, Qt.AlignRight)
        if bold or italic:
            font = itm.font(1)
            if bold:
                font.setBold(True)
            if italic:
                font.setItalic(True)
            itm.setFont(1, font)

    def __getCommit(self, tag):
        """
        Private method to get the commit id for a tag.
        
        @param tag tag name (string)
        @return commit id shortened to 10 characters (string)
        """
        args = self.vcs.initCommand("show")
        args.append("--abbrev-commit")
        args.append("--abbrev={0}".format(
            self.vcs.getPlugin().getPreferences("CommitIdLength")))
        args.append("--no-patch")
        args.append(tag)

        output = ""
        process = QProcess()
        process.setWorkingDirectory(self.repodir)
        process.start('git', args)
        procStarted = process.waitForStarted(5000)
        if procStarted:
            finished = process.waitForFinished(30000)
            if finished and process.exitCode() == 0:
                output = str(process.readAllStandardOutput(),
                             Preferences.getSystem("IOEncoding"), 'replace')

        if output:
            for line in output.splitlines():
                if line.startswith("commit "):
                    commitId = line.split()[1]
                    return commitId

        return ""

    def __readStdout(self):
        """
        Private slot to handle the readyReadStdout signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        self.process.setReadChannel(QProcess.StandardOutput)

        while self.process.canReadLine():
            s = str(self.process.readLine(),
                    Preferences.getSystem("IOEncoding"), 'replace')
            self.__processOutputLine(s)

    def __processOutputLine(self, line):
        """
        Private method to process the lines of output.
        
        @param line output line to be processed (string)
        """
        if self.tagsMode:
            name, msg = line.strip().split(None, 1)
            commit = self.__getCommit(name)
            self.__generateItem(commit, name, msg.strip())
        else:
            bold = line.startswith("* ")
            line = line[2:]
            if line.startswith("("):
                name, line = line[1:].split(")", 1)
                commit = line.strip().split(None, 1)[0]
            else:
                data = line.split(None, 2)
                if data[1].startswith("->"):
                    name = " ".join(line.strip().split())
                    commit = ""
                else:
                    commit = data[1]
                    name = data[0]
            italic = name.startswith("remotes/")
            self.__generateItem(commit, name, bold=bold, italic=italic)

    def __readStderr(self):
        """
        Private slot to handle the readyReadStderr signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            s = str(self.process.readAllStandardError(),
                    Preferences.getSystem("IOEncoding"), 'replace')
            self.__showError(s)

    def __showError(self, out):
        """
        Private slot to show some error.
        
        @param out error to be shown (string)
        """
        self.errorGroup.show()
        self.errors.insertPlainText(out)
        self.errors.ensureCursorVisible()

    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)

    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the git process.
        """
        inputTxt = self.input.text()
        inputTxt += os.linesep

        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(inputTxt)
            self.errors.ensureCursorVisible()

        self.process.write(strToQByteArray(inputTxt))

        self.passwordCheckBox.setChecked(False)
        self.input.clear()

    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()

    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return

        super(GitTagBranchListDialog, self).keyPressEvent(evt)
Beispiel #32
0
class GitStatusDialog(QWidget, Ui_GitStatusDialog):
    """
    Class implementing a dialog to show the output of the git status command
    process.
    """
    ConflictStates = ["AA", "AU", "DD", "DU", "UA", "UD", "UU"]

    ConflictRole = Qt.UserRole

    def __init__(self, vcs, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param parent parent widget (QWidget)
        """
        super(GitStatusDialog, self).__init__(parent)
        self.setupUi(self)

        self.__toBeCommittedColumn = 0
        self.__statusWorkColumn = 1
        self.__statusIndexColumn = 2
        self.__pathColumn = 3
        self.__lastColumn = self.statusList.columnCount()

        self.refreshButton = self.buttonBox.addButton(
            self.tr("Refresh"), QDialogButtonBox.ActionRole)
        self.refreshButton.setToolTip(
            self.tr("Press to refresh the status display"))
        self.refreshButton.setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)

        self.diff = None
        self.vcs = vcs
        self.vcs.committed.connect(self.__committed)
        self.process = QProcess()
        self.process.finished.connect(self.__procFinished)
        self.process.readyReadStandardOutput.connect(self.__readStdout)
        self.process.readyReadStandardError.connect(self.__readStderr)

        self.errorGroup.hide()
        self.inputGroup.hide()

        self.vDiffSplitter.setStretchFactor(0, 2)
        self.vDiffSplitter.setStretchFactor(0, 2)
        self.vDiffSplitter.setSizes([400, 400])
        self.__hDiffSplitterState = None
        self.__vDiffSplitterState = None

        self.statusList.headerItem().setText(self.__lastColumn, "")
        self.statusList.header().setSortIndicator(self.__pathColumn,
                                                  Qt.AscendingOrder)

        font = Preferences.getEditorOtherFonts("MonospacedFont")
        self.lDiffEdit.setFontFamily(font.family())
        self.lDiffEdit.setFontPointSize(font.pointSize())
        self.rDiffEdit.setFontFamily(font.family())
        self.rDiffEdit.setFontPointSize(font.pointSize())
        self.lDiffEdit.customContextMenuRequested.connect(
            self.__showLDiffContextMenu)
        self.rDiffEdit.customContextMenuRequested.connect(
            self.__showRDiffContextMenu)

        self.__lDiffMenu = QMenu()
        self.__stageLinesAct = self.__lDiffMenu.addAction(
            UI.PixmapCache.getIcon("vcsAdd.png"),
            self.tr("Stage Selected Lines"), self.__stageHunkOrLines)
        self.__revertLinesAct = self.__lDiffMenu.addAction(
            UI.PixmapCache.getIcon("vcsRevert.png"),
            self.tr("Revert Selected Lines"), self.__revertHunkOrLines)
        self.__stageHunkAct = self.__lDiffMenu.addAction(
            UI.PixmapCache.getIcon("vcsAdd.png"), self.tr("Stage Hunk"),
            self.__stageHunkOrLines)
        self.__revertHunkAct = self.__lDiffMenu.addAction(
            UI.PixmapCache.getIcon("vcsRevert.png"), self.tr("Revert Hunk"),
            self.__revertHunkOrLines)

        self.__rDiffMenu = QMenu()
        self.__unstageLinesAct = self.__rDiffMenu.addAction(
            UI.PixmapCache.getIcon("vcsRemove.png"),
            self.tr("Unstage Selected Lines"), self.__unstageHunkOrLines)
        self.__unstageHunkAct = self.__rDiffMenu.addAction(
            UI.PixmapCache.getIcon("vcsRemove.png"), self.tr("Unstage Hunk"),
            self.__unstageHunkOrLines)

        self.lDiffHighlighter = GitDiffHighlighter(self.lDiffEdit.document())
        self.rDiffHighlighter = GitDiffHighlighter(self.rDiffEdit.document())

        self.lDiffParser = None
        self.rDiffParser = None

        self.__selectedName = ""

        self.__diffGenerator = GitDiffGenerator(vcs, self)
        self.__diffGenerator.finished.connect(self.__generatorFinished)

        self.modifiedIndicators = [
            self.tr('added'),
            self.tr('copied'),
            self.tr('deleted'),
            self.tr('modified'),
            self.tr('renamed'),
        ]
        self.modifiedOnlyIndicators = [
            self.tr('modified'),
        ]

        self.unversionedIndicators = [
            self.tr('not tracked'),
        ]

        self.missingIndicators = [
            self.tr('deleted'),
        ]

        self.unmergedIndicators = [
            self.tr('unmerged'),
        ]

        self.status = {
            ' ': self.tr("unmodified"),
            'A': self.tr('added'),
            'C': self.tr('copied'),
            'D': self.tr('deleted'),
            'M': self.tr('modified'),
            'R': self.tr('renamed'),
            'U': self.tr('unmerged'),
            '?': self.tr('not tracked'),
            '!': self.tr('ignored'),
        }

        self.__ioEncoding = Preferences.getSystem("IOEncoding")

        self.__initActionsMenu()

    def __initActionsMenu(self):
        """
        Private method to initialize the actions menu.
        """
        self.__actionsMenu = QMenu()
        self.__actionsMenu.setTearOffEnabled(True)
        if qVersion() >= "5.1.0":
            self.__actionsMenu.setToolTipsVisible(True)
        else:
            self.__actionsMenu.hovered.connect(self.__actionsMenuHovered)
        self.__actionsMenu.aboutToShow.connect(self.__showActionsMenu)

        self.__commitAct = self.__actionsMenu.addAction(
            self.tr("Commit"), self.__commit)
        self.__commitAct.setToolTip(self.tr("Commit the selected changes"))
        self.__amendAct = self.__actionsMenu.addAction(self.tr("Amend"),
                                                       self.__amend)
        self.__amendAct.setToolTip(
            self.tr("Amend the latest commit with the selected changes"))
        self.__commitSelectAct = self.__actionsMenu.addAction(
            self.tr("Select all for commit"), self.__commitSelectAll)
        self.__commitDeselectAct = self.__actionsMenu.addAction(
            self.tr("Unselect all from commit"), self.__commitDeselectAll)

        self.__actionsMenu.addSeparator()
        self.__addAct = self.__actionsMenu.addAction(self.tr("Add"),
                                                     self.__add)
        self.__addAct.setToolTip(self.tr("Add the selected files"))
        self.__stageAct = self.__actionsMenu.addAction(
            self.tr("Stage changes"), self.__stage)
        self.__stageAct.setToolTip(
            self.tr("Stages all changes of the selected files"))
        self.__unstageAct = self.__actionsMenu.addAction(
            self.tr("Unstage changes"), self.__unstage)
        self.__unstageAct.setToolTip(
            self.tr("Unstages all changes of the selected files"))

        self.__actionsMenu.addSeparator()

        self.__diffAct = self.__actionsMenu.addAction(self.tr("Differences"),
                                                      self.__diff)
        self.__diffAct.setToolTip(
            self.tr("Shows the differences of the selected entry in a"
                    " separate dialog"))
        self.__sbsDiffAct = self.__actionsMenu.addAction(
            self.tr("Differences Side-By-Side"), self.__sbsDiff)
        self.__sbsDiffAct.setToolTip(
            self.tr(
                "Shows the differences of the selected entry side-by-side in"
                " a separate dialog"))

        self.__actionsMenu.addSeparator()

        self.__revertAct = self.__actionsMenu.addAction(
            self.tr("Revert"), self.__revert)
        self.__revertAct.setToolTip(
            self.tr("Reverts the changes of the selected files"))

        self.__actionsMenu.addSeparator()

        self.__forgetAct = self.__actionsMenu.addAction(
            self.tr("Forget missing"), self.__forget)
        self.__forgetAct.setToolTip(
            self.tr("Forgets about the selected missing files"))
        self.__restoreAct = self.__actionsMenu.addAction(
            self.tr("Restore missing"), self.__restoreMissing)
        self.__restoreAct.setToolTip(
            self.tr("Restores the selected missing files"))

        self.__actionsMenu.addSeparator()

        self.__editAct = self.__actionsMenu.addAction(self.tr("Edit file"),
                                                      self.__editConflict)
        self.__editAct.setToolTip(
            self.tr("Edit the selected conflicting file"))

        self.__actionsMenu.addSeparator()

        act = self.__actionsMenu.addAction(self.tr("Adjust column sizes"),
                                           self.__resizeColumns)
        act.setToolTip(
            self.tr("Adjusts the width of all columns to their contents"))

        self.actionsButton.setIcon(
            UI.PixmapCache.getIcon("actionsToolButton.png"))
        self.actionsButton.setMenu(self.__actionsMenu)

    def __actionsMenuHovered(self, action):
        """
        Private slot to show the tooltip for an action menu entry.
        
        @param action action to show tooltip for
        @type QAction
        """
        QToolTip.showText(QCursor.pos(), action.toolTip(), self.__actionsMenu,
                          self.__actionsMenu.actionGeometry(action))

    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)

        self.vcs.getPlugin().setPreferences("StatusDialogGeometry",
                                            self.saveGeometry())
        self.vcs.getPlugin().setPreferences(
            "StatusDialogSplitterStates",
            [self.vDiffSplitter.saveState(),
             self.hDiffSplitter.saveState()])

        e.accept()

    def show(self):
        """
        Public slot to show the dialog.
        """
        super(GitStatusDialog, self).show()

        geom = self.vcs.getPlugin().getPreferences("StatusDialogGeometry")
        if geom.isEmpty():
            s = QSize(900, 600)
            self.resize(s)
        else:
            self.restoreGeometry(geom)

        states = self.vcs.getPlugin().getPreferences(
            "StatusDialogSplitterStates")
        if len(states) == 2:
            # we have two splitters
            self.vDiffSplitter.restoreState(states[0])
            self.hDiffSplitter.restoreState(states[1])

    def __resort(self):
        """
        Private method to resort the tree.
        """
        self.statusList.sortItems(
            self.statusList.sortColumn(),
            self.statusList.header().sortIndicatorOrder())

    def __resizeColumns(self):
        """
        Private method to resize the list columns.
        """
        self.statusList.header().resizeSections(QHeaderView.ResizeToContents)
        self.statusList.header().setStretchLastSection(True)

    def __generateItem(self, status, path):
        """
        Private method to generate a status item in the status list.
        
        @param status status indicator (string)
        @param path path of the file or directory (string)
        """
        statusWorkText = self.status[status[1]]
        statusIndexText = self.status[status[0]]
        itm = QTreeWidgetItem(self.statusList, [
            "",
            statusWorkText,
            statusIndexText,
            path,
        ])

        itm.setTextAlignment(self.__statusWorkColumn, Qt.AlignHCenter)
        itm.setTextAlignment(self.__statusIndexColumn, Qt.AlignHCenter)
        itm.setTextAlignment(self.__pathColumn, Qt.AlignLeft)

        if status not in self.ConflictStates + ["??", "!!"] and \
                statusIndexText in self.modifiedIndicators:
            itm.setFlags(itm.flags() | Qt.ItemIsUserCheckable)
            itm.setCheckState(self.__toBeCommittedColumn, Qt.Checked)
        else:
            itm.setFlags(itm.flags() & ~Qt.ItemIsUserCheckable)

        if statusWorkText not in self.__statusFilters:
            self.__statusFilters.append(statusWorkText)
        if statusIndexText not in self.__statusFilters:
            self.__statusFilters.append(statusIndexText)

        if status in self.ConflictStates:
            itm.setIcon(
                self.__statusWorkColumn,
                UI.PixmapCache.getIcon(
                    os.path.join("vcsGit", "icons", "conflict.png")))
        itm.setData(0, self.ConflictRole, status in self.ConflictStates)

    def start(self, fn):
        """
        Public slot to start the git status command.
        
        @param fn filename(s)/directoryname(s) to show the status of
            (string or list of strings)
        """
        self.errorGroup.hide()
        self.intercept = False
        self.args = fn

        self.__ioEncoding = Preferences.getSystem("IOEncoding")

        self.statusFilterCombo.clear()
        self.__statusFilters = []
        self.statusList.clear()

        self.setWindowTitle(self.tr('Git Status'))

        args = self.vcs.initCommand("status")
        args.append('--porcelain')
        args.append("--")
        if isinstance(fn, list):
            self.dname, fnames = self.vcs.splitPathList(fn)
            self.vcs.addArguments(args, fn)
        else:
            self.dname, fname = self.vcs.splitPath(fn)
            args.append(fn)

        # find the root of the repo
        self.__repodir = self.dname
        while not os.path.isdir(os.path.join(self.__repodir,
                                             self.vcs.adminDir)):
            self.__repodir = os.path.dirname(self.__repodir)
            if os.path.splitdrive(self.__repodir)[1] == os.sep:
                return

        self.process.kill()
        self.process.setWorkingDirectory(self.__repodir)

        self.process.start('git', args)
        procStarted = self.process.waitForStarted(5000)
        if not procStarted:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            E5MessageBox.critical(
                self, self.tr('Process Generation Error'),
                self.tr(
                    'The process {0} could not be started. '
                    'Ensure, that it is in the search path.').format('git'))
        else:
            self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
            self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
            self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)

            self.refreshButton.setEnabled(False)

    def __finish(self):
        """
        Private slot called when the process finished or the user pressed
        the button.
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)

        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()
        self.refreshButton.setEnabled(True)

        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        self.buttonBox.button(QDialogButtonBox.Close).setFocus(
            Qt.OtherFocusReason)

        self.__statusFilters.sort()
        self.__statusFilters.insert(0, "<{0}>".format(self.tr("all")))
        self.statusFilterCombo.addItems(self.__statusFilters)

        self.__resort()
        self.__resizeColumns()

        self.__refreshDiff()

    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.close()
        elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
            self.__finish()
        elif button == self.refreshButton:
            self.on_refreshButton_clicked()

    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__finish()

    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        if self.process is not None:
            self.process.setReadChannel(QProcess.StandardOutput)

            while self.process.canReadLine():
                line = str(self.process.readLine(), self.__ioEncoding,
                           'replace')

                status = line[:2]
                path = line[3:].strip().split(" -> ")[-1].strip('"')
                self.__generateItem(status, path)

    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            s = str(self.process.readAllStandardError(), self.__ioEncoding,
                    'replace')
            self.errorGroup.show()
            self.errors.insertPlainText(s)
            self.errors.ensureCursorVisible()

            # show input in case the process asked for some input
            self.inputGroup.setEnabled(True)
            self.inputGroup.show()

    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)

    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the git process.
        """
        inputTxt = self.input.text()
        inputTxt += os.linesep

        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(inputTxt)
            self.errors.ensureCursorVisible()

        self.process.write(inputTxt)

        self.passwordCheckBox.setChecked(False)
        self.input.clear()

    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()

    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(GitStatusDialog, self).keyPressEvent(evt)

    @pyqtSlot()
    def on_refreshButton_clicked(self):
        """
        Private slot to refresh the status display.
        """
        selectedItems = self.statusList.selectedItems()
        if len(selectedItems) == 1:
            self.__selectedName = selectedItems[0].text(self.__pathColumn)
        else:
            self.__selectedName = ""

        self.start(self.args)

    @pyqtSlot(str)
    def on_statusFilterCombo_activated(self, txt):
        """
        Private slot to react to the selection of a status filter.
        
        @param txt selected status filter (string)
        """
        if txt == "<{0}>".format(self.tr("all")):
            for topIndex in range(self.statusList.topLevelItemCount()):
                topItem = self.statusList.topLevelItem(topIndex)
                topItem.setHidden(False)
        else:
            for topIndex in range(self.statusList.topLevelItemCount()):
                topItem = self.statusList.topLevelItem(topIndex)
                topItem.setHidden(
                    topItem.text(self.__statusWorkColumn) != txt
                    and topItem.text(self.__statusIndexColumn) != txt)

    @pyqtSlot()
    def on_statusList_itemSelectionChanged(self):
        """
        Private slot to act upon changes of selected items.
        """
        self.__generateDiffs()

    ###########################################################################
    ## Menu handling methods
    ###########################################################################

    def __showActionsMenu(self):
        """
        Private slot to prepare the actions button menu before it is shown.
        """
        modified = len(self.__getModifiedItems())
        modifiedOnly = len(self.__getModifiedOnlyItems())
        unversioned = len(self.__getUnversionedItems())
        missing = len(self.__getMissingItems())
        commitable = len(self.__getCommitableItems())
        commitableUnselected = len(self.__getCommitableUnselectedItems())
        stageable = len(self.__getStageableItems())
        unstageable = len(self.__getUnstageableItems())
        conflicting = len(self.__getConflictingItems())

        self.__commitAct.setEnabled(commitable)
        self.__amendAct.setEnabled(commitable)
        self.__commitSelectAct.setEnabled(commitableUnselected)
        self.__commitDeselectAct.setEnabled(commitable)
        self.__addAct.setEnabled(unversioned)
        self.__stageAct.setEnabled(stageable)
        self.__unstageAct.setEnabled(unstageable)
        self.__diffAct.setEnabled(modified)
        self.__sbsDiffAct.setEnabled(modifiedOnly == 1)
        self.__revertAct.setEnabled(stageable)
        self.__forgetAct.setEnabled(missing)
        self.__restoreAct.setEnabled(missing)
        self.__editAct.setEnabled(conflicting == 1)

    def __amend(self):
        """
        Private slot to handle the Amend context menu entry.
        """
        self.__commit(amend=True)

    def __commit(self, amend=False):
        """
        Private slot to handle the Commit context menu entry.
        
        @param amend flag indicating to perform an amend operation (boolean)
        """
        names = [
            os.path.join(self.dname, itm.text(self.__pathColumn))
            for itm in self.__getCommitableItems()
        ]
        if not names:
            E5MessageBox.information(
                self, self.tr("Commit"),
                self.tr("""There are no entries selected to be"""
                        """ committed."""))
            return

        if Preferences.getVCS("AutoSaveFiles"):
            vm = e5App().getObject("ViewManager")
            for name in names:
                vm.saveEditor(name)
        self.vcs.vcsCommit(names, commitAll=False, amend=amend)
        # staged changes

    def __committed(self):
        """
        Private slot called after the commit has finished.
        """
        if self.isVisible():
            self.on_refreshButton_clicked()
            self.vcs.checkVCSStatus()

    def __commitSelectAll(self):
        """
        Private slot to select all entries for commit.
        """
        self.__commitSelect(True)

    def __commitDeselectAll(self):
        """
        Private slot to deselect all entries from commit.
        """
        self.__commitSelect(False)

    def __add(self):
        """
        Private slot to handle the Add context menu entry.
        """
        names = [
            os.path.join(self.dname, itm.text(self.__pathColumn))
            for itm in self.__getUnversionedItems()
        ]
        if not names:
            E5MessageBox.information(
                self, self.tr("Add"),
                self.tr("""There are no unversioned entries"""
                        """ available/selected."""))
            return

        self.vcs.vcsAdd(names)
        self.on_refreshButton_clicked()

        project = e5App().getObject("Project")
        for name in names:
            project.getModel().updateVCSStatus(name)
        self.vcs.checkVCSStatus()

    def __stage(self):
        """
        Private slot to handle the Stage context menu entry.
        """
        names = [
            os.path.join(self.dname, itm.text(self.__pathColumn))
            for itm in self.__getStageableItems()
        ]
        if not names:
            E5MessageBox.information(
                self, self.tr("Stage"),
                self.tr("""There are no stageable entries"""
                        """ available/selected."""))
            return

        self.vcs.vcsAdd(names)
        self.on_refreshButton_clicked()

        project = e5App().getObject("Project")
        for name in names:
            project.getModel().updateVCSStatus(name)
        self.vcs.checkVCSStatus()

    def __unstage(self):
        """
        Private slot to handle the Unstage context menu entry.
        """
        names = [
            os.path.join(self.dname, itm.text(self.__pathColumn))
            for itm in self.__getUnstageableItems()
        ]
        if not names:
            E5MessageBox.information(
                self, self.tr("Unstage"),
                self.tr("""There are no unstageable entries"""
                        """ available/selected."""))
            return

        self.vcs.gitUnstage(names)
        self.on_refreshButton_clicked()

        project = e5App().getObject("Project")
        for name in names:
            project.getModel().updateVCSStatus(name)
        self.vcs.checkVCSStatus()

    def __forget(self):
        """
        Private slot to handle the Forget Missing context menu entry.
        """
        names = [
            os.path.join(self.dname, itm.text(self.__pathColumn))
            for itm in self.__getMissingItems()
        ]
        if not names:
            E5MessageBox.information(
                self, self.tr("Forget Missing"),
                self.tr("""There are no missing entries"""
                        """ available/selected."""))
            return

        self.vcs.vcsRemove(names, stageOnly=True)
        self.on_refreshButton_clicked()

    def __revert(self):
        """
        Private slot to handle the Revert context menu entry.
        """
        names = [
            os.path.join(self.dname, itm.text(self.__pathColumn))
            for itm in self.__getStageableItems()
        ]
        if not names:
            E5MessageBox.information(
                self, self.tr("Revert"),
                self.tr("""There are no uncommitted, unstaged changes"""
                        """ available/selected."""))
            return

        self.vcs.gitRevert(names)
        self.raise_()
        self.activateWindow()
        self.on_refreshButton_clicked()

        project = e5App().getObject("Project")
        for name in names:
            project.getModel().updateVCSStatus(name)
        self.vcs.checkVCSStatus()

    def __restoreMissing(self):
        """
        Private slot to handle the Restore Missing context menu entry.
        """
        names = [
            os.path.join(self.dname, itm.text(self.__pathColumn))
            for itm in self.__getMissingItems()
        ]
        if not names:
            E5MessageBox.information(
                self, self.tr("Restore Missing"),
                self.tr("""There are no missing entries"""
                        """ available/selected."""))
            return

        self.vcs.gitRevert(names)
        self.on_refreshButton_clicked()
        self.vcs.checkVCSStatus()

    def __editConflict(self):
        """
        Private slot to handle the Edit file context menu entry.
        """
        itm = self.__getConflictingItems()[0]
        filename = os.path.join(self.__repodir, itm.text(self.__pathColumn))
        if Utilities.MimeTypes.isTextFile(filename):
            e5App().getObject("ViewManager").getEditor(filename)

    def __diff(self):
        """
        Private slot to handle the Diff context menu entry.
        """
        namesW = [
            os.path.join(self.dname, itm.text(self.__pathColumn))
            for itm in self.__getStageableItems()
        ]
        namesS = [
            os.path.join(self.dname, itm.text(self.__pathColumn))
            for itm in self.__getUnstageableItems()
        ]
        if not namesW and not namesS:
            E5MessageBox.information(
                self, self.tr("Differences"),
                self.tr("""There are no uncommitted changes"""
                        """ available/selected."""))
            return

        diffMode = "work2stage2repo"
        names = namesW + namesS

        if self.diff is None:
            from .GitDiffDialog import GitDiffDialog
            self.diff = GitDiffDialog(self.vcs)
        self.diff.show()
        self.diff.start(names, diffMode=diffMode, refreshable=True)

    def __sbsDiff(self):
        """
        Private slot to handle the Diff context menu entry.
        """
        itm = self.__getModifiedOnlyItems()[0]
        workModified = (itm.text(self.__statusWorkColumn)
                        in self.modifiedOnlyIndicators)
        stageModified = (itm.text(self.__statusIndexColumn)
                         in self.modifiedOnlyIndicators)
        names = [os.path.join(self.dname, itm.text(self.__pathColumn))]

        if workModified and stageModified:
            # select from all three variants
            messages = [
                self.tr("Working Tree to Staging Area"),
                self.tr("Staging Area to HEAD Commit"),
                self.tr("Working Tree to HEAD Commit"),
            ]
            result, ok = QInputDialog.getItem(
                None, self.tr("Side-by-Side Difference"),
                self.tr("Select the compare method."), messages, 0, False)
            if not ok:
                return

            if result == messages[0]:
                revisions = ["", ""]
            elif result == messages[1]:
                revisions = ["HEAD", "Stage"]
            else:
                revisions = ["HEAD", ""]
        elif workModified:
            # select from work variants
            messages = [
                self.tr("Working Tree to Staging Area"),
                self.tr("Working Tree to HEAD Commit"),
            ]
            result, ok = QInputDialog.getItem(
                None, self.tr("Side-by-Side Difference"),
                self.tr("Select the compare method."), messages, 0, False)
            if not ok:
                return

            if result == messages[0]:
                revisions = ["", ""]
            else:
                revisions = ["HEAD", ""]
        else:
            revisions = ["HEAD", "Stage"]

        self.vcs.gitSbsDiff(names[0], revisions=revisions)

    def __getCommitableItems(self):
        """
        Private method to retrieve all entries the user wants to commit.
        
        @return list of all items, the user has checked
        """
        commitableItems = []
        for index in range(self.statusList.topLevelItemCount()):
            itm = self.statusList.topLevelItem(index)
            if itm.checkState(self.__toBeCommittedColumn) == Qt.Checked:
                commitableItems.append(itm)
        return commitableItems

    def __getCommitableUnselectedItems(self):
        """
        Private method to retrieve all entries the user may commit but hasn't
        selected.
        
        @return list of all items, the user has not checked
        """
        items = []
        for index in range(self.statusList.topLevelItemCount()):
            itm = self.statusList.topLevelItem(index)
            if itm.flags() & Qt.ItemIsUserCheckable and \
               itm.checkState(self.__toBeCommittedColumn) == Qt.Unchecked:
                items.append(itm)
        return items

    def __getModifiedItems(self):
        """
        Private method to retrieve all entries, that have a modified status.
        
        @return list of all items with a modified status
        """
        modifiedItems = []
        for itm in self.statusList.selectedItems():
            if (itm.text(self.__statusWorkColumn) in self.modifiedIndicators
                    or itm.text(
                        self.__statusIndexColumn) in self.modifiedIndicators):
                modifiedItems.append(itm)
        return modifiedItems

    def __getModifiedOnlyItems(self):
        """
        Private method to retrieve all entries, that have a modified status.
        
        @return list of all items with a modified status
        """
        modifiedItems = []
        for itm in self.statusList.selectedItems():
            if (itm.text(
                    self.__statusWorkColumn) in self.modifiedOnlyIndicators
                    or itm.text(self.__statusIndexColumn)
                    in self.modifiedOnlyIndicators):
                modifiedItems.append(itm)
        return modifiedItems

    def __getUnversionedItems(self):
        """
        Private method to retrieve all entries, that have an unversioned
        status.
        
        @return list of all items with an unversioned status
        """
        unversionedItems = []
        for itm in self.statusList.selectedItems():
            if itm.text(self.__statusWorkColumn) in self.unversionedIndicators:
                unversionedItems.append(itm)
        return unversionedItems

    def __getStageableItems(self):
        """
        Private method to retrieve all entries, that have a stageable
        status.
        
        @return list of all items with a stageable status
        """
        stageableItems = []
        for itm in self.statusList.selectedItems():
            if itm.text(self.__statusWorkColumn) in \
                    self.modifiedIndicators + self.unmergedIndicators:
                stageableItems.append(itm)
        return stageableItems

    def __getUnstageableItems(self):
        """
        Private method to retrieve all entries, that have an unstageable
        status.
        
        @return list of all items with an unstageable status
        """
        unstageableItems = []
        for itm in self.statusList.selectedItems():
            if itm.text(self.__statusIndexColumn) in self.modifiedIndicators:
                unstageableItems.append(itm)
        return unstageableItems

    def __getMissingItems(self):
        """
        Private method to retrieve all entries, that have a missing status.
        
        @return list of all items with a missing status
        """
        missingItems = []
        for itm in self.statusList.selectedItems():
            if itm.text(self.__statusWorkColumn) in self.missingIndicators:
                missingItems.append(itm)
        return missingItems

    def __getConflictingItems(self):
        """
        Private method to retrieve all entries, that have a conflict status.
        
        @return list of all items with a conflict status
        """
        conflictingItems = []
        for itm in self.statusList.selectedItems():
            if itm.data(0, self.ConflictRole):
                conflictingItems.append(itm)
        return conflictingItems

    def __commitSelect(self, selected):
        """
        Private slot to select or deselect all entries.
        
        @param selected commit selection state to be set (boolean)
        """
        for index in range(self.statusList.topLevelItemCount()):
            itm = self.statusList.topLevelItem(index)
            if itm.flags() & Qt.ItemIsUserCheckable:
                if selected:
                    itm.setCheckState(self.__toBeCommittedColumn, Qt.Checked)
                else:
                    itm.setCheckState(self.__toBeCommittedColumn, Qt.Unchecked)

    ###########################################################################
    ## Diff handling methods below
    ###########################################################################

    def __generateDiffs(self):
        """
        Private slot to generate diff outputs for the selected item.
        """
        self.lDiffEdit.clear()
        self.rDiffEdit.clear()

        selectedItems = self.statusList.selectedItems()
        if len(selectedItems) == 1:
            fn = os.path.join(self.dname,
                              selectedItems[0].text(self.__pathColumn))
            self.__diffGenerator.start(fn, diffMode="work2stage2repo")

    def __generatorFinished(self):
        """
        Private slot connected to the finished signal of the diff generator.
        """
        diff1, diff2 = self.__diffGenerator.getResult()[:2]

        if diff1:
            self.lDiffParser = GitDiffParser(diff1)
            for line in diff1[:]:
                if line.startswith("@@ "):
                    break
                else:
                    diff1.pop(0)
            self.lDiffEdit.setPlainText("".join(diff1))
        else:
            self.lDiffParser = None

        if diff2:
            self.rDiffParser = GitDiffParser(diff2)
            for line in diff2[:]:
                if line.startswith("@@ "):
                    break
                else:
                    diff2.pop(0)
            self.rDiffEdit.setPlainText("".join(diff2))
        else:
            self.rDiffParser = None

        for diffEdit in [self.lDiffEdit, self.rDiffEdit]:
            tc = diffEdit.textCursor()
            tc.movePosition(QTextCursor.Start)
            diffEdit.setTextCursor(tc)
            diffEdit.ensureCursorVisible()

    def __showLDiffContextMenu(self, coord):
        """
        Private slot to show the context menu of the status list.
        
        @param coord position of the mouse pointer (QPoint)
        """
        if bool(self.lDiffEdit.toPlainText()):
            cursor = self.lDiffEdit.textCursor()
            if cursor.hasSelection():
                self.__stageLinesAct.setEnabled(True)
                self.__revertLinesAct.setEnabled(True)
                self.__stageHunkAct.setEnabled(False)
                self.__revertHunkAct.setEnabled(False)
            else:
                self.__stageLinesAct.setEnabled(False)
                self.__revertLinesAct.setEnabled(False)
                self.__stageHunkAct.setEnabled(True)
                self.__revertHunkAct.setEnabled(True)

                cursor = self.lDiffEdit.cursorForPosition(coord)
                self.lDiffEdit.setTextCursor(cursor)

            self.__lDiffMenu.popup(self.lDiffEdit.mapToGlobal(coord))

    def __showRDiffContextMenu(self, coord):
        """
        Private slot to show the context menu of the status list.
        
        @param coord position of the mouse pointer (QPoint)
        """
        if bool(self.rDiffEdit.toPlainText()):
            cursor = self.rDiffEdit.textCursor()
            if cursor.hasSelection():
                self.__unstageLinesAct.setEnabled(True)
                self.__unstageHunkAct.setEnabled(False)
            else:
                self.__unstageLinesAct.setEnabled(False)
                self.__unstageHunkAct.setEnabled(True)

                cursor = self.rDiffEdit.cursorForPosition(coord)
                self.rDiffEdit.setTextCursor(cursor)

            self.__rDiffMenu.popup(self.rDiffEdit.mapToGlobal(coord))

    def __stageHunkOrLines(self):
        """
        Private method to stage the selected lines or hunk.
        """
        cursor = self.lDiffEdit.textCursor()
        startIndex, endIndex = self.__selectedLinesIndexes(self.lDiffEdit)
        if cursor.hasSelection():
            patch = self.lDiffParser.createLinesPatch(startIndex, endIndex)
        else:
            patch = self.lDiffParser.createHunkPatch(startIndex)
        if patch:
            patchFile = self.__tmpPatchFileName()
            try:
                f = open(patchFile, "w")
                f.write(patch)
                f.close()
                self.vcs.gitApply(self.dname,
                                  patchFile,
                                  cached=True,
                                  noDialog=True)
                self.on_refreshButton_clicked()
            finally:
                os.remove(patchFile)

    def __unstageHunkOrLines(self):
        """
        Private method to unstage the selected lines or hunk.
        """
        cursor = self.rDiffEdit.textCursor()
        startIndex, endIndex = self.__selectedLinesIndexes(self.rDiffEdit)
        if cursor.hasSelection():
            patch = self.rDiffParser.createLinesPatch(startIndex,
                                                      endIndex,
                                                      reverse=True)
        else:
            patch = self.rDiffParser.createHunkPatch(startIndex)
        if patch:
            patchFile = self.__tmpPatchFileName()
            try:
                f = open(patchFile, "w")
                f.write(patch)
                f.close()
                self.vcs.gitApply(self.dname,
                                  patchFile,
                                  cached=True,
                                  reverse=True,
                                  noDialog=True)
                self.on_refreshButton_clicked()
            finally:
                os.remove(patchFile)

    def __revertHunkOrLines(self):
        """
        Private method to revert the selected lines or hunk.
        """
        cursor = self.lDiffEdit.textCursor()
        startIndex, endIndex = self.__selectedLinesIndexes(self.lDiffEdit)
        if cursor.hasSelection():
            title = self.tr("Revert selected lines")
        else:
            title = self.tr("Revert hunk")
        res = E5MessageBox.yesNo(
            self, title,
            self.tr("""Are you sure you want to revert the selected"""
                    """ changes?"""))
        if res:
            if cursor.hasSelection():
                patch = self.lDiffParser.createLinesPatch(startIndex,
                                                          endIndex,
                                                          reverse=True)
            else:
                patch = self.lDiffParser.createHunkPatch(startIndex)
            if patch:
                patchFile = self.__tmpPatchFileName()
                try:
                    f = open(patchFile, "w")
                    f.write(patch)
                    f.close()
                    self.vcs.gitApply(self.dname,
                                      patchFile,
                                      reverse=True,
                                      noDialog=True)
                    self.on_refreshButton_clicked()
                finally:
                    os.remove(patchFile)

    def __selectedLinesIndexes(self, diffEdit):
        """
        Private method to extract the indexes of the selected lines.
        
        @param diffEdit reference to the edit widget (QTextEdit)
        @return tuple of start and end indexes (integer, integer)
        """
        cursor = diffEdit.textCursor()
        selectionStart = cursor.selectionStart()
        selectionEnd = cursor.selectionEnd()

        startIndex = -1

        lineStart = 0
        for lineIdx, line in enumerate(diffEdit.toPlainText().splitlines()):
            lineEnd = lineStart + len(line)
            if lineStart <= selectionStart <= lineEnd:
                startIndex = lineIdx
            if lineStart <= selectionEnd <= lineEnd:
                endIndex = lineIdx
                break
            lineStart = lineEnd + 1

        return startIndex, endIndex

    def __tmpPatchFileName(self):
        """
        Private method to generate a temporary patch file.
        
        @return name of the temporary file (string)
        """
        prefix = 'eric-git-{0}-'.format(os.getpid())
        suffix = '-patch'
        fd, path = tempfile.mkstemp(suffix, prefix)
        os.close(fd)
        return path

    def __refreshDiff(self):
        """
        Private method to refresh the diff output after a refresh.
        """
        if self.__selectedName:
            for index in range(self.statusList.topLevelItemCount()):
                itm = self.statusList.topLevelItem(index)
                if itm.text(self.__pathColumn) == self.__selectedName:
                    itm.setSelected(True)
                    break

        self.__selectedName = ""
Beispiel #33
0
class SvnLogBrowserDialog(QWidget, Ui_SvnLogBrowserDialog):
    """
    Class implementing a dialog to browse the log history.
    """
    def __init__(self, vcs, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param parent parent widget (QWidget)
        """
        super(SvnLogBrowserDialog, self).__init__(parent)
        self.setupUi(self)
        
        self.__position = QPoint()
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
        
        self.filesTree.headerItem().setText(self.filesTree.columnCount(), "")
        self.filesTree.header().setSortIndicator(0, Qt.AscendingOrder)
        
        self.vcs = vcs
        
        self.__initData()
        
        self.fromDate.setDisplayFormat("yyyy-MM-dd")
        self.toDate.setDisplayFormat("yyyy-MM-dd")
        self.__resetUI()
        
        self.__messageRole = Qt.UserRole
        self.__changesRole = Qt.UserRole + 1
        
        self.process = QProcess()
        self.process.finished.connect(self.__procFinished)
        self.process.readyReadStandardOutput.connect(self.__readStdout)
        self.process.readyReadStandardError.connect(self.__readStderr)
        
        self.rx_sep1 = QRegExp('\\-+\\s*')
        self.rx_sep2 = QRegExp('=+\\s*')
        self.rx_rev1 = QRegExp(
            'rev ([0-9]+):  ([^|]*) \| ([^|]*) \| ([0-9]+) .*')
        # "rev" followed by one or more decimals followed by a colon followed
        # anything up to " | " (twice) followed by one or more decimals
        # followed by anything
        self.rx_rev2 = QRegExp(
            'r([0-9]+) \| ([^|]*) \| ([^|]*) \| ([0-9]+) .*')
        # "r" followed by one or more decimals followed by " | " followed
        # anything up to " | " (twice) followed by one or more decimals
        # followed by anything
        self.rx_flags1 = QRegExp(
            r"""   ([ADM])\s(.*)\s+\(\w+\s+(.*):([0-9]+)\)\s*""")
        # three blanks followed by A or D or M followed by path followed by
        # path copied from followed by copied from revision
        self.rx_flags2 = QRegExp('   ([ADM]) (.*)\\s*')
        # three blanks followed by A or D or M followed by path
        
        self.flags = {
            'A': self.tr('Added'),
            'D': self.tr('Deleted'),
            'M': self.tr('Modified'),
            'R': self.tr('Replaced'),
        }
        self.intercept = False
    
    def __initData(self):
        """
        Private method to (re-)initialize some data.
        """
        self.__maxDate = QDate()
        self.__minDate = QDate()
        self.__filterLogsEnabled = True
        
        self.buf = []        # buffer for stdout
        self.diff = None
        self.__started = False
        self.__lastRev = 0
    
    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        self.__position = self.pos()
        
        e.accept()
        
    def show(self):
        """
        Public slot to show the dialog.
        """
        if not self.__position.isNull():
            self.move(self.__position)
        self.__resetUI()
        
        super(SvnLogBrowserDialog, self).show()
    
    def __resetUI(self):
        """
        Private method to reset the user interface.
        """
        self.fromDate.setDate(QDate.currentDate())
        self.toDate.setDate(QDate.currentDate())
        self.fieldCombo.setCurrentIndex(self.fieldCombo.findText(
            self.tr("Message")))
        self.limitSpinBox.setValue(self.vcs.getPlugin().getPreferences(
            "LogLimit"))
        self.stopCheckBox.setChecked(self.vcs.getPlugin().getPreferences(
            "StopLogOnCopy"))
        
        self.logTree.clear()
        
        self.nextButton.setEnabled(True)
        self.limitSpinBox.setEnabled(True)

    def __resizeColumnsLog(self):
        """
        Private method to resize the log tree columns.
        """
        self.logTree.header().resizeSections(QHeaderView.ResizeToContents)
        self.logTree.header().setStretchLastSection(True)
    
    def __resortLog(self):
        """
        Private method to resort the log tree.
        """
        self.logTree.sortItems(
            self.logTree.sortColumn(),
            self.logTree.header().sortIndicatorOrder())
    
    def __resizeColumnsFiles(self):
        """
        Private method to resize the changed files tree columns.
        """
        self.filesTree.header().resizeSections(QHeaderView.ResizeToContents)
        self.filesTree.header().setStretchLastSection(True)
    
    def __resortFiles(self):
        """
        Private method to resort the changed files tree.
        """
        sortColumn = self.filesTree.sortColumn()
        self.filesTree.sortItems(
            1, self.filesTree.header().sortIndicatorOrder())
        self.filesTree.sortItems(
            sortColumn, self.filesTree.header().sortIndicatorOrder())
    
    def __generateLogItem(self, author, date, message, revision, changedPaths):
        """
        Private method to generate a log tree entry.
        
        @param author author info (string)
        @param date date info (string)
        @param message text of the log message (list of strings)
        @param revision revision info (string)
        @param changedPaths list of dictionary objects containing
            info about the changed files/directories
        @return reference to the generated item (QTreeWidgetItem)
        """
        msg = []
        for line in message:
            msg.append(line.strip())
        
        itm = QTreeWidgetItem(self.logTree)
        itm.setData(0, Qt.DisplayRole, int(revision))
        itm.setData(1, Qt.DisplayRole, author)
        itm.setData(2, Qt.DisplayRole, date)
        itm.setData(3, Qt.DisplayRole, " ".join(msg))
        
        itm.setData(0, self.__messageRole, message)
        itm.setData(0, self.__changesRole, changedPaths)
        
        itm.setTextAlignment(0, Qt.AlignRight)
        itm.setTextAlignment(1, Qt.AlignLeft)
        itm.setTextAlignment(2, Qt.AlignLeft)
        itm.setTextAlignment(3, Qt.AlignLeft)
        itm.setTextAlignment(4, Qt.AlignLeft)
        
        try:
            self.__lastRev = int(revision)
        except ValueError:
            self.__lastRev = 0
        
        return itm
    
    def __generateFileItem(self, action, path, copyFrom, copyRev):
        """
        Private method to generate a changed files tree entry.
        
        @param action indicator for the change action ("A", "D" or "M")
        @param path path of the file in the repository (string)
        @param copyFrom path the file was copied from (None, string)
        @param copyRev revision the file was copied from (None, string)
        @return reference to the generated item (QTreeWidgetItem)
        """
        itm = QTreeWidgetItem(self.filesTree, [
            self.flags[action],
            path,
            copyFrom,
            copyRev,
        ])
        
        itm.setTextAlignment(3, Qt.AlignRight)
        
        return itm
    
    def __getLogEntries(self, startRev=None):
        """
        Private method to retrieve log entries from the repository.
        
        @param startRev revision number to start from (integer, string)
        """
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
        QApplication.processEvents()
        
        QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
        QApplication.processEvents()
        
        self.intercept = False
        self.process.kill()
        
        self.buf = []
        self.cancelled = False
        self.errors.clear()
        
        args = []
        args.append('log')
        self.vcs.addArguments(args, self.vcs.options['global'])
        self.vcs.addArguments(args, self.vcs.options['log'])
        args.append('--verbose')
        args.append('--limit')
        args.append('{0:d}'.format(self.limitSpinBox.value()))
        if startRev is not None:
            args.append('--revision')
            args.append('{0}:0'.format(startRev))
        if self.stopCheckBox.isChecked():
            args.append('--stop-on-copy')
        args.append(self.fname)
        
        self.process.setWorkingDirectory(self.dname)
        
        self.inputGroup.setEnabled(True)
        self.inputGroup.show()
        
        self.process.start('svn', args)
        procStarted = self.process.waitForStarted(5000)
        if not procStarted:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            E5MessageBox.critical(
                self,
                self.tr('Process Generation Error'),
                self.tr(
                    'The process {0} could not be started. '
                    'Ensure, that it is in the search path.'
                ).format('svn'))
    
    def start(self, fn, isFile=False):
        """
        Public slot to start the svn log command.
        
        @param fn filename to show the log for (string)
        @keyparam isFile flag indicating log for a file is to be shown
            (boolean)
        """
        self.sbsCheckBox.setEnabled(isFile)
        self.sbsCheckBox.setVisible(isFile)
        
        self.errorGroup.hide()
        QApplication.processEvents()
        
        self.__initData()
        
        self.filename = fn
        self.dname, self.fname = self.vcs.splitPath(fn)
        
        self.activateWindow()
        self.raise_()
        
        self.logTree.clear()
        self.__started = True
        self.__getLogEntries()
    
    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__processBuffer()
        self.__finish()
    
    def __finish(self):
        """
        Private slot called when the process finished or the user pressed the
        button.
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        QApplication.restoreOverrideCursor()
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        
        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()
    
    def __processBuffer(self):
        """
        Private method to process the buffered output of the svn log command.
        """
        noEntries = 0
        log = {"message": []}
        changedPaths = []
        for s in self.buf:
            if self.rx_rev1.exactMatch(s):
                log["revision"] = self.rx_rev.cap(1)
                log["author"] = self.rx_rev.cap(2)
                log["date"] = self.rx_rev.cap(3)
                # number of lines is ignored
            elif self.rx_rev2.exactMatch(s):
                log["revision"] = self.rx_rev2.cap(1)
                log["author"] = self.rx_rev2.cap(2)
                log["date"] = self.rx_rev2.cap(3)
                # number of lines is ignored
            elif self.rx_flags1.exactMatch(s):
                changedPaths.append({
                    "action":
                    self.rx_flags1.cap(1).strip(),
                    "path":
                    self.rx_flags1.cap(2).strip(),
                    "copyfrom_path":
                    self.rx_flags1.cap(3).strip(),
                    "copyfrom_revision":
                    self.rx_flags1.cap(4).strip(),
                })
            elif self.rx_flags2.exactMatch(s):
                changedPaths.append({
                    "action":
                    self.rx_flags2.cap(1).strip(),
                    "path":
                    self.rx_flags2.cap(2).strip(),
                    "copyfrom_path": "",
                    "copyfrom_revision": "",
                })
            elif self.rx_sep1.exactMatch(s) or self.rx_sep2.exactMatch(s):
                if len(log) > 1:
                    self.__generateLogItem(
                        log["author"], log["date"], log["message"],
                        log["revision"], changedPaths)
                    dt = QDate.fromString(log["date"], Qt.ISODate)
                    if not self.__maxDate.isValid() and \
                            not self.__minDate.isValid():
                        self.__maxDate = dt
                        self.__minDate = dt
                    else:
                        if self.__maxDate < dt:
                            self.__maxDate = dt
                        if self.__minDate > dt:
                            self.__minDate = dt
                    noEntries += 1
                    log = {"message": []}
                    changedPaths = []
            else:
                if s.strip().endswith(":") or not s.strip():
                    continue
                else:
                    log["message"].append(s)
        
        self.__resizeColumnsLog()
        self.__resortLog()
        
        if self.__started:
            self.logTree.setCurrentItem(self.logTree.topLevelItem(0))
            self.__started = False
        
        if noEntries < self.limitSpinBox.value() and not self.cancelled:
            self.nextButton.setEnabled(False)
            self.limitSpinBox.setEnabled(False)
        
        self.__filterLogsEnabled = False
        self.fromDate.setMinimumDate(self.__minDate)
        self.fromDate.setMaximumDate(self.__maxDate)
        self.fromDate.setDate(self.__minDate)
        self.toDate.setMinimumDate(self.__minDate)
        self.toDate.setMaximumDate(self.__maxDate)
        self.toDate.setDate(self.__maxDate)
        self.__filterLogsEnabled = True
        self.__filterLogs()
    
    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.
        
        It reads the output of the process and inserts it into a buffer.
        """
        self.process.setReadChannel(QProcess.StandardOutput)
        
        while self.process.canReadLine():
            line = str(self.process.readLine(),
                       Preferences.getSystem("IOEncoding"),
                       'replace')
            self.buf.append(line)
    
    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            self.errorGroup.show()
            s = str(self.process.readAllStandardError(),
                    Preferences.getSystem("IOEncoding"),
                    'replace')
            self.errors.insertPlainText(s)
            self.errors.ensureCursorVisible()
    
    def __diffRevisions(self, rev1, rev2):
        """
        Private method to do a diff of two revisions.
        
        @param rev1 first revision number (integer)
        @param rev2 second revision number (integer)
        """
        if self.sbsCheckBox.isEnabled() and self.sbsCheckBox.isChecked():
            self.vcs.svnSbsDiff(self.filename,
                                revisions=(str(rev1), str(rev2)))
        else:
            if self.diff is None:
                from .SvnDiffDialog import SvnDiffDialog
                self.diff = SvnDiffDialog(self.vcs)
            self.diff.show()
            self.diff.raise_()
            self.diff.start(self.filename, [rev1, rev2])
    
    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.close()
        elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
            self.cancelled = True
            self.__finish()
    
    @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem)
    def on_logTree_currentItemChanged(self, current, previous):
        """
        Private slot called, when the current item of the log tree changes.
        
        @param current reference to the new current item (QTreeWidgetItem)
        @param previous reference to the old current item (QTreeWidgetItem)
        """
        if current is not None:
            self.messageEdit.clear()
            for line in current.data(0, self.__messageRole):
                self.messageEdit.append(line.strip())
            
            self.filesTree.clear()
            changes = current.data(0, self.__changesRole)
            if len(changes) > 0:
                for change in changes:
                    self.__generateFileItem(
                        change["action"], change["path"],
                        change["copyfrom_path"],
                        change["copyfrom_revision"])
                self.__resizeColumnsFiles()
            self.__resortFiles()
        
        self.diffPreviousButton.setEnabled(
            current != self.logTree.topLevelItem(
                self.logTree.topLevelItemCount() - 1))
    
    @pyqtSlot()
    def on_logTree_itemSelectionChanged(self):
        """
        Private slot called, when the selection has changed.
        """
        self.diffRevisionsButton.setEnabled(
            len(self.logTree.selectedItems()) == 2)
    
    @pyqtSlot()
    def on_nextButton_clicked(self):
        """
        Private slot to handle the Next button.
        """
        if self.__lastRev > 1:
            self.__getLogEntries(self.__lastRev - 1)
    
    @pyqtSlot()
    def on_diffPreviousButton_clicked(self):
        """
        Private slot to handle the Diff to Previous button.
        """
        itm = self.logTree.currentItem()
        if itm is None:
            self.diffPreviousButton.setEnabled(False)
            return
        rev2 = int(itm.text(0))
        
        itm = self.logTree.topLevelItem(
            self.logTree.indexOfTopLevelItem(itm) + 1)
        if itm is None:
            self.diffPreviousButton.setEnabled(False)
            return
        rev1 = int(itm.text(0))
        
        self.__diffRevisions(rev1, rev2)
    
    @pyqtSlot()
    def on_diffRevisionsButton_clicked(self):
        """
        Private slot to handle the Compare Revisions button.
        """
        items = self.logTree.selectedItems()
        if len(items) != 2:
            self.diffRevisionsButton.setEnabled(False)
            return
        
        rev2 = int(items[0].text(0))
        rev1 = int(items[1].text(0))
        
        self.__diffRevisions(min(rev1, rev2), max(rev1, rev2))
    
    @pyqtSlot(QDate)
    def on_fromDate_dateChanged(self, date):
        """
        Private slot called, when the from date changes.
        
        @param date new date (QDate)
        """
        self.__filterLogs()
    
    @pyqtSlot(QDate)
    def on_toDate_dateChanged(self, date):
        """
        Private slot called, when the from date changes.
        
        @param date new date (QDate)
        """
        self.__filterLogs()
    
    @pyqtSlot(str)
    def on_fieldCombo_activated(self, txt):
        """
        Private slot called, when a new filter field is selected.
        
        @param txt text of the selected field (string)
        """
        self.__filterLogs()
    
    @pyqtSlot(str)
    def on_rxEdit_textChanged(self, txt):
        """
        Private slot called, when a filter expression is entered.
        
        @param txt filter expression (string)
        """
        self.__filterLogs()
    
    def __filterLogs(self):
        """
        Private method to filter the log entries.
        """
        if self.__filterLogsEnabled:
            from_ = self.fromDate.date().toString("yyyy-MM-dd")
            to_ = self.toDate.date().addDays(1).toString("yyyy-MM-dd")
            txt = self.fieldCombo.currentText()
            if txt == self.tr("Author"):
                fieldIndex = 1
                searchRx = QRegExp(self.rxEdit.text(), Qt.CaseInsensitive)
            elif txt == self.tr("Revision"):
                fieldIndex = 0
                txt = self.rxEdit.text()
                if txt.startswith("^"):
                    searchRx = QRegExp(
                        "^\s*{0}".format(txt[1:]), Qt.CaseInsensitive)
                else:
                    searchRx = QRegExp(txt, Qt.CaseInsensitive)
            else:
                fieldIndex = 3
                searchRx = QRegExp(self.rxEdit.text(), Qt.CaseInsensitive)
            
            currentItem = self.logTree.currentItem()
            for topIndex in range(self.logTree.topLevelItemCount()):
                topItem = self.logTree.topLevelItem(topIndex)
                if topItem.text(2) <= to_ and topItem.text(2) >= from_ and \
                   searchRx.indexIn(topItem.text(fieldIndex)) > -1:
                    topItem.setHidden(False)
                    if topItem is currentItem:
                        self.on_logTree_currentItemChanged(topItem, None)
                else:
                    topItem.setHidden(True)
                    if topItem is currentItem:
                        self.messageEdit.clear()
                        self.filesTree.clear()
    
    @pyqtSlot(bool)
    def on_stopCheckBox_clicked(self, checked):
        """
        Private slot called, when the stop on copy/move checkbox is clicked.
        
        @param checked flag indicating the checked state (boolean)
        """
        self.vcs.getPlugin().setPreferences("StopLogOnCopy",
                                            self.stopCheckBox.isChecked())
        self.nextButton.setEnabled(True)
        self.limitSpinBox.setEnabled(True)
    
    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)
    
    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the subversion process.
        """
        input = self.input.text()
        input += os.linesep
        
        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(input)
            self.errors.ensureCursorVisible()
        self.errorGroup.show()
        
        self.process.write(input)
        
        self.passwordCheckBox.setChecked(False)
        self.input.clear()
    
    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()
    
    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(SvnLogBrowserDialog, self).keyPressEvent(evt)
Beispiel #34
0
class SyncScreen(QWidget):

    PYTHON_ARGS = ['-m', 'kitovu', 'sync']
    status_message = pyqtSignal(str)
    close_requested = pyqtSignal()
    finished = pyqtSignal(int, QProcess.ExitStatus)

    def __init__(self, parent: QWidget = None) -> None:
        super().__init__(parent)

        self._vbox = QVBoxLayout(self)

        self._output = QTextEdit()
        self._output.setReadOnly(True)
        self._vbox.addWidget(self._output)

        self._progress = ProgressBar()
        self._progress.show_empty()
        self._vbox.addWidget(self._progress)

        self._cancel_button = QPushButton("Zurück")
        self._cancel_button.clicked.connect(self.on_cancel_clicked)
        self._vbox.addWidget(self._cancel_button)

        self._process = QProcess()
        self._process.setProcessChannelMode(QProcess.MergedChannels)
        self._process.readyRead.connect(self.on_process_ready_read)
        self._process.started.connect(self.on_process_started)
        self._process.finished.connect(self.on_process_finished)

        self.status_message.connect(self.on_status_message)

    @pyqtSlot()
    def on_process_started(self) -> None:
        self._cancel_button.setText("Abbrechen")
        self._progress.show_pulse()
        self.status_message.emit("Synchronisation läuft...")

    @pyqtSlot()
    def on_process_ready_read(self) -> None:
        if self._process.canReadLine():
            data: bytes = bytes(self._process.readLine())
            self._output.append(data.decode('utf-8'))

    @pyqtSlot(int, QProcess.ExitStatus)
    def on_process_finished(self, exit_code: int,
                            exit_status: QProcess.ExitStatus) -> None:
        self._cancel_button.setText("Zurück")
        self._progress.show_full()

        data: bytes = bytes(self._process.readAll())
        self._output.append(data.decode('utf-8'))

        if exit_status == QProcess.CrashExit:
            self.status_message.emit("Fehler: Kitovu-Prozess ist abgestürzt.")
        elif exit_code != 0:
            self.status_message.emit(
                f"Fehler: Kitovu-Prozess wurde mit Status {exit_code} "
                "beendet.")
        else:
            self.status_message.emit("Synchronisation erfolgreich beendet.")

        self.finished.emit(exit_code, exit_status)

    @pyqtSlot(str)
    def on_status_message(self, message: str) -> None:
        self._output.append(message)

    @pyqtSlot()
    def on_cancel_clicked(self) -> None:
        if self._process.state() != QProcess.NotRunning:
            if sys.platform.startswith('win'):  # pragma: no cover
                self._process.kill()
            else:
                self._process.terminate()

        self.close_requested.emit()

    def start_sync(self) -> None:
        self._output.setPlainText("")
        self._progress.show_empty()
        self._process.start(sys.executable, self.PYTHON_ARGS)
Beispiel #35
0
class HgDiffGenerator(QObject):
    """
    Class implementing the generation of output of the hg diff command.
    
    @signal finished() emitted when all processes have finished
    """
    finished = pyqtSignal()

    def __init__(self, vcs, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param parent parent widget (QWidget)
        """
        super(HgDiffGenerator, self).__init__(parent)

        self.vcs = vcs

        self.__hgClient = self.vcs.getClient()
        if self.__hgClient:
            self.process = None
        else:
            self.process = QProcess()
            self.process.finished.connect(self.__finish)
            self.process.readyReadStandardOutput.connect(self.__readStdout)
            self.process.readyReadStandardError.connect(self.__readStderr)

    def stopProcess(self):
        """
        Public slot to stop the diff process.
        """
        if self.__hgClient:
            if self.__hgClient.isExecuting():
                self.__hgClient.cancel()
        else:
            if self.process is not None and \
               self.process.state() != QProcess.NotRunning:
                self.process.terminate()
                QTimer.singleShot(2000, self.process.kill)
                self.process.waitForFinished(3000)

    def __getVersionArg(self, version):
        """
        Private method to get a hg revision argument for the given revision.
        
        @param version revision (integer or string)
        @return version argument (string)
        """
        if version == "WORKING":
            return None
        else:
            return str(version)

    def start(self, fn, versions=None, bundle=None, qdiff=False):
        """
        Public slot to start the hg diff command.
        
        @param fn filename to be diffed (string)
        @keyparam versions list of versions to be diffed (list of up to
            2 strings or None)
        @keyparam bundle name of a bundle file (string)
        @keyparam qdiff flag indicating qdiff command shall be used (boolean)
        @return flag indicating a successful start of the diff command
            (boolean)
        """
        if qdiff:
            args = self.vcs.initCommand("qdiff")
        else:
            args = self.vcs.initCommand("diff")

            if self.vcs.hasSubrepositories():
                args.append("--subrepos")

            if bundle:
                args.append('--repository')
                args.append(bundle)
            elif self.vcs.bundleFile and os.path.exists(self.vcs.bundleFile):
                args.append('--repository')
                args.append(self.vcs.bundleFile)

            if versions is not None:
                rev1 = self.__getVersionArg(versions[0])
                rev2 = None
                if len(versions) == 2:
                    rev2 = self.__getVersionArg(versions[1])

                if rev1 is not None or rev2 is not None:
                    args.append('-r')
                    if rev1 is not None and rev2 is not None:
                        args.append('{0}:{1}'.format(rev1, rev2))
                    elif rev2 is None:
                        args.append(rev1)
                    elif rev1 is None:
                        args.append(':{0}'.format(rev2))

        if isinstance(fn, list):
            dname, fnames = self.vcs.splitPathList(fn)
            self.vcs.addArguments(args, fn)
        else:
            dname, fname = self.vcs.splitPath(fn)
            args.append(fn)

        self.__oldFile = ""
        self.__oldFileLine = -1
        self.__fileSeparators = []
        self.__output = []
        self.__errors = []

        if self.__hgClient:
            out, err = self.__hgClient.runcommand(args)

            if err:
                self.__errors = err.splitlines(True)

            if out:
                for line in out.splitlines(True):
                    self.__processOutputLine(line)
                    if self.__hgClient.wasCanceled():
                        break

            self.__finish()
        else:
            # find the root of the repo
            repodir = dname
            while not os.path.isdir(os.path.join(repodir, self.vcs.adminDir)):
                repodir = os.path.dirname(repodir)
                if os.path.splitdrive(repodir)[1] == os.sep:
                    return

            self.process.kill()
            self.process.setWorkingDirectory(repodir)

            self.process.start('hg', args)
            procStarted = self.process.waitForStarted(5000)
            if not procStarted:
                return False

        return True

    def __finish(self):
        """
        Private slot called when the process finished or the user pressed
        the button.
        """
        self.finished.emit()

    def getResult(self):
        """
        Public method to return the result data.
        
        @return tuple of lists of string containing lines of the diff, the
            list of errors and a list of tuples of filenames and the line
            into the diff output.
        """
        return (self.__output, self.__errors, self.__fileSeparators)

    def __extractFileName(self, line):
        """
        Private method to extract the file name out of a file separator line.
        
        @param line line to be processed (string)
        @return extracted file name (string)
        """
        f = line.split(None, 1)[1]
        f = f.rsplit(None, 6)[0]
        if f == "/dev/null":
            f = "__NULL__"
        else:
            f = f.split("/", 1)[1]
        return f

    def __processFileLine(self, line):
        """
        Private slot to process a line giving the old/new file.
        
        @param line line to be processed (string)
        """
        if line.startswith('---'):
            self.__oldFileLine = len(self.__output)
            self.__oldFile = self.__extractFileName(line)
        else:
            newFile = self.__extractFileName(line)
            if self.__oldFile == "__NULL__":
                self.__fileSeparators.append(
                    (newFile, newFile, self.__oldFileLine))
            else:
                self.__fileSeparators.append(
                    (self.__oldFile, newFile, self.__oldFileLine))

    def __processOutputLine(self, line):
        """
        Private method to process the lines of output.
        
        @param line output line to be processed (string)
        """
        if line.startswith("--- ") or \
           line.startswith("+++ "):
            self.__processFileLine(line)

        self.__output.append(line)

    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        self.process.setReadChannel(QProcess.StandardOutput)

        while self.process.canReadLine():
            line = str(self.process.readLine(), self.vcs.getEncoding(),
                       'replace')
            self.__processOutputLine(line)

    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            s = str(self.process.readAllStandardError(),
                    self.vcs.getEncoding(), 'replace')
            self.__errors.append(s)
Beispiel #36
0
class SvnChangeListsDialog(QDialog, Ui_SvnChangeListsDialog):
    """
    Class implementing a dialog to browse the change lists.
    """
    def __init__(self, vcs, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param parent parent widget (QWidget)
        """
        super(SvnChangeListsDialog, self).__init__(parent)
        self.setupUi(self)
        self.setWindowFlags(Qt.Window)

        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)

        self.process = None
        self.vcs = vcs

        self.rx_status = QRegExp(
            '(.{8,9})\\s+([0-9-]+)\\s+([0-9?]+)\\s+(\\S+)\\s+(.+)\\s*')
        # flags (8 or 9 anything), revision, changed rev, author, path
        self.rx_status2 = \
            QRegExp('(.{8,9})\\s+(.+)\\s*')
        # flags (8 or 9 anything), path
        self.rx_changelist = \
            QRegExp('--- \\S+ .([\\w\\s]+).:\\s+')
        # three dashes, Changelist (translated), quote,
        # changelist name, quote, :

    @pyqtSlot(QListWidgetItem, QListWidgetItem)
    def on_changeLists_currentItemChanged(self, current, previous):
        """
        Private slot to handle the selection of a new item.
        
        @param current current item (QListWidgetItem)
        @param previous previous current item (QListWidgetItem)
        """
        self.filesList.clear()
        if current is not None:
            changelist = current.text()
            if changelist in self.changeListsDict:
                self.filesList.addItems(
                    sorted(self.changeListsDict[changelist]))

    def start(self, path):
        """
        Public slot to populate the data.
        
        @param path directory name to show change lists for (string)
        """
        self.changeListsDict = {}

        self.filesLabel.setText(
            self.tr("Files (relative to {0}):").format(path))

        self.errorGroup.hide()
        self.intercept = False

        self.path = path
        self.currentChangelist = ""

        self.process = QProcess()
        self.process.finished.connect(self.__procFinished)
        self.process.readyReadStandardOutput.connect(self.__readStdout)
        self.process.readyReadStandardError.connect(self.__readStderr)

        args = []
        args.append('status')
        self.vcs.addArguments(args, self.vcs.options['global'])
        self.vcs.addArguments(args, self.vcs.options['status'])
        if '--verbose' not in self.vcs.options['global'] and \
           '--verbose' not in self.vcs.options['status']:
            args.append('--verbose')
        if isinstance(path, list):
            self.dname, fnames = self.vcs.splitPathList(path)
            self.vcs.addArguments(args, fnames)
        else:
            self.dname, fname = self.vcs.splitPath(path)
            args.append(fname)

        self.process.setWorkingDirectory(self.dname)

        self.process.start('svn', args)
        procStarted = self.process.waitForStarted(5000)
        if not procStarted:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            E5MessageBox.critical(
                self, self.tr('Process Generation Error'),
                self.tr(
                    'The process {0} could not be started. '
                    'Ensure, that it is in the search path.').format('svn'))
        else:
            self.inputGroup.setEnabled(True)
            self.inputGroup.show()

    def __finish(self):
        """
        Private slot called when the process finished or the user pressed
        the button.
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)

        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)

        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()

        if len(self.changeListsDict) == 0:
            self.changeLists.addItem(self.tr("No changelists found"))
            self.buttonBox.button(QDialogButtonBox.Close).setFocus(
                Qt.OtherFocusReason)
        else:
            self.changeLists.addItems(sorted(self.changeListsDict.keys()))
            self.changeLists.setCurrentRow(0)
            self.changeLists.setFocus(Qt.OtherFocusReason)

    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.close()
        elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
            self.__finish()

    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__finish()

    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        if self.process is not None:
            self.process.setReadChannel(QProcess.StandardOutput)

            while self.process.canReadLine():
                s = str(self.process.readLine(),
                        Preferences.getSystem("IOEncoding"), 'replace')
                if self.currentChangelist != "" and \
                        self.rx_status.exactMatch(s):
                    file = self.rx_status.cap(5).strip()
                    filename = file.replace(self.path + os.sep, "")
                    if filename not in \
                            self.changeListsDict[self.currentChangelist]:
                        self.changeListsDict[self.currentChangelist].append(
                            filename)
                elif self.currentChangelist != "" and \
                        self.rx_status2.exactMatch(s):
                    file = self.rx_status2.cap(2).strip()
                    filename = file.replace(self.path + os.sep, "")
                    if filename not in \
                            self.changeListsDict[self.currentChangelist]:
                        self.changeListsDict[self.currentChangelist].append(
                            filename)
                elif self.rx_changelist.exactMatch(s):
                    self.currentChangelist = self.rx_changelist.cap(1)
                    if self.currentChangelist not in self.changeListsDict:
                        self.changeListsDict[self.currentChangelist] = []

    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            self.errorGroup.show()
            s = str(self.process.readAllStandardError(),
                    Preferences.getSystem("IOEncoding"), 'replace')
            self.errors.insertPlainText(s)
            self.errors.ensureCursorVisible()

    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)

    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the subversion process.
        """
        input = self.input.text()
        input += os.linesep

        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(input)
            self.errors.ensureCursorVisible()

        self.process.write(input)

        self.passwordCheckBox.setChecked(False)
        self.input.clear()

    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()

    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(SvnChangeListsDialog, self).keyPressEvent(evt)
Beispiel #37
0
class SvnDiffDialog(QWidget, Ui_SvnDiffDialog):
    """
    Class implementing a dialog to show the output of the svn diff command
    process.
    """
    def __init__(self, vcs, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param parent parent widget (QWidget)
        """
        super(SvnDiffDialog, self).__init__(parent)
        self.setupUi(self)
        
        self.refreshButton = self.buttonBox.addButton(
            self.tr("Refresh"), QDialogButtonBox.ActionRole)
        self.refreshButton.setToolTip(
            self.tr("Press to refresh the display"))
        self.refreshButton.setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Save).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        
        self.searchWidget.attachTextEdit(self.contents)
        
        self.process = QProcess()
        self.vcs = vcs
        
        font = Preferences.getEditorOtherFonts("MonospacedFont")
        self.contents.setFontFamily(font.family())
        self.contents.setFontPointSize(font.pointSize())
        
        self.highlighter = SvnDiffHighlighter(self.contents.document())
        
        self.process.finished.connect(self.__procFinished)
        self.process.readyReadStandardOutput.connect(self.__readStdout)
        self.process.readyReadStandardError.connect(self.__readStderr)
        
    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        e.accept()
        
    def __getVersionArg(self, version):
        """
        Private method to get a svn revision argument for the given revision.
        
        @param version revision (integer or string)
        @return version argument (string)
        """
        if version == "WORKING":
            return None
        else:
            return str(version)
        
    def start(self, fn, versions=None, urls=None, summary=False,
              refreshable=False):
        """
        Public slot to start the svn diff command.
        
        @param fn filename to be diffed (string)
        @param versions list of versions to be diffed (list of up to 2 strings
            or None)
        @keyparam urls list of repository URLs (list of 2 strings)
        @keyparam summary flag indicating a summarizing diff
            (only valid for URL diffs) (boolean)
        @keyparam refreshable flag indicating a refreshable diff (boolean)
        """
        self.refreshButton.setVisible(refreshable)
        
        self.errorGroup.hide()
        self.inputGroup.show()
        self.inputGroup.setEnabled(True)
        self.intercept = False
        self.filename = fn
        
        self.process.kill()
        
        self.contents.clear()
        self.paras = 0
        
        self.filesCombo.clear()
        
        self.__oldFile = ""
        self.__oldFileLine = -1
        self.__fileSeparators = []
        
        args = []
        args.append('diff')
        self.vcs.addArguments(args, self.vcs.options['global'])
        self.vcs.addArguments(args, self.vcs.options['diff'])
        if '--diff-cmd' in self.vcs.options['diff']:
            self.buttonBox.button(QDialogButtonBox.Save).hide()
        
        if versions is not None:
            self.raise_()
            self.activateWindow()
            
            rev1 = self.__getVersionArg(versions[0])
            rev2 = None
            if len(versions) == 2:
                rev2 = self.__getVersionArg(versions[1])
            
            if rev1 is not None or rev2 is not None:
                args.append('-r')
                if rev1 is not None and rev2 is not None:
                    args.append('{0}:{1}'.format(rev1, rev2))
                elif rev2 is None:
                    args.append(rev1)
                elif rev1 is None:
                    args.append(rev2)
        
        self.summaryPath = None
        if urls is not None:
            if summary:
                args.append("--summarize")
                self.summaryPath = urls[0]
            args.append("--old={0}".format(urls[0]))
            args.append("--new={0}".format(urls[1]))
            if isinstance(fn, list):
                dname, fnames = self.vcs.splitPathList(fn)
            else:
                dname, fname = self.vcs.splitPath(fn)
                fnames = [fname]
            project = e5App().getObject('Project')
            if dname == project.getProjectPath():
                path = ""
            else:
                path = project.getRelativePath(dname)
            if path:
                path += "/"
            for fname in fnames:
                args.append(path + fname)
        else:
            if isinstance(fn, list):
                dname, fnames = self.vcs.splitPathList(fn)
                self.vcs.addArguments(args, fnames)
            else:
                dname, fname = self.vcs.splitPath(fn)
                args.append(fname)
        
        self.process.setWorkingDirectory(dname)
        
        self.process.start('svn', args)
        procStarted = self.process.waitForStarted(5000)
        if not procStarted:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            E5MessageBox.critical(
                self,
                self.tr('Process Generation Error'),
                self.tr(
                    'The process {0} could not be started. '
                    'Ensure, that it is in the search path.'
                ).format('svn'))
        
    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()
        self.refreshButton.setEnabled(True)
        
        if self.paras == 0:
            self.contents.setPlainText(self.tr('There is no difference.'))
            
        self.buttonBox.button(QDialogButtonBox.Save).setEnabled(self.paras > 0)
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        self.buttonBox.button(QDialogButtonBox.Close).setFocus(
            Qt.OtherFocusReason)
        
        tc = self.contents.textCursor()
        tc.movePosition(QTextCursor.Start)
        self.contents.setTextCursor(tc)
        self.contents.ensureCursorVisible()
        
        self.filesCombo.addItem(self.tr("<Start>"), 0)
        self.filesCombo.addItem(self.tr("<End>"), -1)
        for oldFile, newFile, pos in sorted(self.__fileSeparators):
            if oldFile != newFile:
                self.filesCombo.addItem(
                    "{0}\n{1}".format(oldFile, newFile), pos)
            else:
                self.filesCombo.addItem(oldFile, pos)
        
    def __appendText(self, txt):
        """
        Private method to append text to the end of the contents pane.
        
        @param txt text to insert (string)
        """
        tc = self.contents.textCursor()
        tc.movePosition(QTextCursor.End)
        self.contents.setTextCursor(tc)
        self.contents.insertPlainText(txt)
        
    def __extractFileName(self, line):
        """
        Private method to extract the file name out of a file separator line.
        
        @param line line to be processed (string)
        @return extracted file name (string)
        """
        f = line.split(None, 1)[1]
        f = f.rsplit(None, 2)[0]
        return f
    
    def __processFileLine(self, line):
        """
        Private slot to process a line giving the old/new file.
        
        @param line line to be processed (string)
        """
        if line.startswith('---'):
            self.__oldFileLine = self.paras
            self.__oldFile = self.__extractFileName(line)
        else:
            self.__fileSeparators.append(
                (self.__oldFile, self.__extractFileName(line),
                 self.__oldFileLine))
    
    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        self.process.setReadChannel(QProcess.StandardOutput)
        
        while self.process.canReadLine():
            line = str(self.process.readLine(),
                       Preferences.getSystem("IOEncoding"),
                       'replace')
            if self.summaryPath:
                line = line.replace(self.summaryPath + '/', '')
                line = " ".join(line.split())
            if line.startswith("--- ") or line.startswith("+++ "):
                    self.__processFileLine(line)
                
            self.__appendText(line)
            self.paras += 1
        
    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            self.errorGroup.show()
            s = str(self.process.readAllStandardError(),
                    Preferences.getSystem("IOEncoding"),
                    'replace')
            self.errors.insertPlainText(s)
            self.errors.ensureCursorVisible()
        
    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Save):
            self.on_saveButton_clicked()
        elif button == self.refreshButton:
            self.on_refreshButton_clicked()
    
    @pyqtSlot(int)
    def on_filesCombo_activated(self, index):
        """
        Private slot to handle the selection of a file.
        
        @param index activated row (integer)
        """
        para = self.filesCombo.itemData(index)
        
        if para == 0:
            tc = self.contents.textCursor()
            tc.movePosition(QTextCursor.Start)
            self.contents.setTextCursor(tc)
            self.contents.ensureCursorVisible()
        elif para == -1:
            tc = self.contents.textCursor()
            tc.movePosition(QTextCursor.End)
            self.contents.setTextCursor(tc)
            self.contents.ensureCursorVisible()
        else:
            # step 1: move cursor to end
            tc = self.contents.textCursor()
            tc.movePosition(QTextCursor.End)
            self.contents.setTextCursor(tc)
            self.contents.ensureCursorVisible()
            
            # step 2: move cursor to desired line
            tc = self.contents.textCursor()
            delta = tc.blockNumber() - para
            tc.movePosition(QTextCursor.PreviousBlock, QTextCursor.MoveAnchor,
                            delta)
            self.contents.setTextCursor(tc)
            self.contents.ensureCursorVisible()
    
    @pyqtSlot()
    def on_saveButton_clicked(self):
        """
        Private slot to handle the Save button press.
        
        It saves the diff shown in the dialog to a file in the local
        filesystem.
        """
        if isinstance(self.filename, list):
            if len(self.filename) > 1:
                fname = self.vcs.splitPathList(self.filename)[0]
            else:
                dname, fname = self.vcs.splitPath(self.filename[0])
                if fname != '.':
                    fname = "{0}.diff".format(self.filename[0])
                else:
                    fname = dname
        else:
            fname = self.vcs.splitPath(self.filename)[0]
        
        fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
            self,
            self.tr("Save Diff"),
            fname,
            self.tr("Patch Files (*.diff)"),
            None,
            E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
        
        if not fname:
            return  # user aborted
        
        ext = QFileInfo(fname).suffix()
        if not ext:
            ex = selectedFilter.split("(*")[1].split(")")[0]
            if ex:
                fname += ex
        if QFileInfo(fname).exists():
            res = E5MessageBox.yesNo(
                self,
                self.tr("Save Diff"),
                self.tr("<p>The patch file <b>{0}</b> already exists."
                        " Overwrite it?</p>").format(fname),
                icon=E5MessageBox.Warning)
            if not res:
                return
        fname = Utilities.toNativeSeparators(fname)
        
        eol = e5App().getObject("Project").getEolString()
        try:
            f = open(fname, "w", encoding="utf-8", newline="")
            f.write(eol.join(self.contents.toPlainText().splitlines()))
            f.close()
        except IOError as why:
            E5MessageBox.critical(
                self, self.tr('Save Diff'),
                self.tr(
                    '<p>The patch file <b>{0}</b> could not be saved.'
                    '<br>Reason: {1}</p>')
                .format(fname, str(why)))
        
    @pyqtSlot()
    def on_refreshButton_clicked(self):
        """
        Private slot to refresh the display.
        """
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        
        self.buttonBox.button(QDialogButtonBox.Save).setEnabled(False)
        self.refreshButton.setEnabled(False)
        
        self.start(self.filename, refreshable=True)
    
    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)
        
    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the subversion process.
        """
        input = self.input.text()
        input += os.linesep
        
        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(input)
            self.errors.ensureCursorVisible()
        
        self.process.write(input)
        
        self.passwordCheckBox.setChecked(False)
        self.input.clear()
        
    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()
        
    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(SvnDiffDialog, self).keyPressEvent(evt)
Beispiel #38
0
class SvnStatusDialog(QWidget, Ui_SvnStatusDialog):
    """
    Class implementing a dialog to show the output of the svn status command
    process.
    """
    def __init__(self, vcs, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param parent parent widget (QWidget)
        """
        super(SvnStatusDialog, self).__init__(parent)
        self.setupUi(self)
        
        self.__toBeCommittedColumn = 0
        self.__changelistColumn = 1
        self.__statusColumn = 2
        self.__propStatusColumn = 3
        self.__lockedColumn = 4
        self.__historyColumn = 5
        self.__switchedColumn = 6
        self.__lockinfoColumn = 7
        self.__upToDateColumn = 8
        self.__pathColumn = 12
        self.__lastColumn = self.statusList.columnCount()
        
        self.refreshButton = \
            self.buttonBox.addButton(self.tr("Refresh"),
                                     QDialogButtonBox.ActionRole)
        self.refreshButton.setToolTip(
            self.tr("Press to refresh the status display"))
        self.refreshButton.setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
        
        self.diff = None
        self.process = None
        self.vcs = vcs
        self.vcs.committed.connect(self.__committed)
        
        self.statusList.headerItem().setText(self.__lastColumn, "")
        self.statusList.header().setSortIndicator(self.__pathColumn,
                                                  Qt.AscendingOrder)
        if self.vcs.version < (1, 5, 0):
            self.statusList.header().hideSection(self.__changelistColumn)
        
        self.menuactions = []
        self.menu = QMenu()
        self.menuactions.append(self.menu.addAction(
            self.tr("Commit changes to repository..."), self.__commit))
        self.menuactions.append(self.menu.addAction(
            self.tr("Select all for commit"), self.__commitSelectAll))
        self.menuactions.append(self.menu.addAction(
            self.tr("Deselect all from commit"), self.__commitDeselectAll))
        self.menu.addSeparator()
        self.menuactions.append(self.menu.addAction(
            self.tr("Add to repository"), self.__add))
        self.menuactions.append(self.menu.addAction(
            self.tr("Show differences"), self.__diff))
        self.menuactions.append(self.menu.addAction(
            self.tr("Show differences side-by-side"), self.__sbsDiff))
        self.menuactions.append(self.menu.addAction(
            self.tr("Revert changes"), self.__revert))
        self.menuactions.append(self.menu.addAction(
            self.tr("Restore missing"), self.__restoreMissing))
        if self.vcs.version >= (1, 5, 0):
            self.menu.addSeparator()
            self.menuactions.append(self.menu.addAction(
                self.tr("Add to Changelist"), self.__addToChangelist))
            self.menuactions.append(self.menu.addAction(
                self.tr("Remove from Changelist"),
                self.__removeFromChangelist))
        if self.vcs.version >= (1, 2, 0):
            self.menu.addSeparator()
            self.menuactions.append(self.menu.addAction(
                self.tr("Lock"), self.__lock))
            self.menuactions.append(self.menu.addAction(
                self.tr("Unlock"), self.__unlock))
            self.menuactions.append(self.menu.addAction(
                self.tr("Break lock"),
                self.__breakLock))
            self.menuactions.append(self.menu.addAction(
                self.tr("Steal lock"),
                self.__stealLock))
        self.menu.addSeparator()
        self.menuactions.append(self.menu.addAction(
            self.tr("Adjust column sizes"),
            self.__resizeColumns))
        for act in self.menuactions:
            act.setEnabled(False)
        
        self.statusList.setContextMenuPolicy(Qt.CustomContextMenu)
        self.statusList.customContextMenuRequested.connect(
            self.__showContextMenu)
        
        self.modifiedIndicators = [
            self.tr('added'),
            self.tr('deleted'),
            self.tr('modified'),
        ]
        
        self.missingIndicators = [
            self.tr('missing'),
        ]
        
        self.unversionedIndicators = [
            self.tr('unversioned'),
        ]
        
        self.lockedIndicators = [
            self.tr('locked'),
        ]
        
        self.stealBreakLockIndicators = [
            self.tr('other lock'),
            self.tr('stolen lock'),
            self.tr('broken lock'),
        ]
        
        self.unlockedIndicators = [
            self.tr('not locked'),
        ]
        
        self.status = {
            ' ': self.tr('normal'),
            'A': self.tr('added'),
            'D': self.tr('deleted'),
            'M': self.tr('modified'),
            'R': self.tr('replaced'),
            'C': self.tr('conflict'),
            'X': self.tr('external'),
            'I': self.tr('ignored'),
            '?': self.tr('unversioned'),
            '!': self.tr('missing'),
            '~': self.tr('type error'),
        }
        self.propStatus = {
            ' ': self.tr('normal'),
            'M': self.tr('modified'),
            'C': self.tr('conflict'),
        }
        self.locked = {
            ' ': self.tr('no'),
            'L': self.tr('yes'),
        }
        self.history = {
            ' ': self.tr('no'),
            '+': self.tr('yes'),
        }
        self.switched = {
            ' ': self.tr('no'),
            'S': self.tr('yes'),
        }
        self.lockinfo = {
            ' ': self.tr('not locked'),
            'K': self.tr('locked'),
            'O': self.tr('other lock'),
            'T': self.tr('stolen lock'),
            'B': self.tr('broken lock'),
        }
        self.uptodate = {
            ' ': self.tr('yes'),
            '*': self.tr('no'),
        }
        
        self.rx_status = QRegExp(
            '(.{8,9})\\s+([0-9-]+)\\s+([0-9?]+)\\s+(\\S+)\\s+(.+)\\s*')
        # flags (8 or 9 anything), revision, changed rev, author, path
        self.rx_status2 = \
            QRegExp('(.{8,9})\\s+(.+)\\s*')
        # flags (8 or 9 anything), path
        self.rx_changelist = \
            QRegExp('--- \\S+ .([\\w\\s]+).:\\s+')
        # three dashes, Changelist (translated), quote,
        # changelist name, quote, :
        
        self.__nonverbose = True
        
    def __resort(self):
        """
        Private method to resort the tree.
        """
        self.statusList.sortItems(
            self.statusList.sortColumn(),
            self.statusList.header().sortIndicatorOrder())
        
    def __resizeColumns(self):
        """
        Private method to resize the list columns.
        """
        self.statusList.header().resizeSections(QHeaderView.ResizeToContents)
        self.statusList.header().setStretchLastSection(True)
        
    def __generateItem(self, status, propStatus, locked, history, switched,
                       lockinfo, uptodate, revision, change, author, path):
        """
        Private method to generate a status item in the status list.
        
        @param status status indicator (string)
        @param propStatus property status indicator (string)
        @param locked locked indicator (string)
        @param history history indicator (string)
        @param switched switched indicator (string)
        @param lockinfo lock indicator (string)
        @param uptodate up to date indicator (string)
        @param revision revision string (string)
        @param change revision of last change (string)
        @param author author of the last change (string)
        @param path path of the file or directory (string)
        """
        if self.__nonverbose and \
           status == " " and \
           propStatus == " " and \
           locked == " " and \
           history == " " and \
           switched == " " and \
           lockinfo == " " and \
           uptodate == " " and \
           self.currentChangelist == "":
            return
        
        if revision == "":
            rev = ""
        else:
            try:
                rev = int(revision)
            except ValueError:
                rev = revision
        if change == "":
            chg = ""
        else:
            try:
                chg = int(change)
            except ValueError:
                chg = change
        statusText = self.status[status]
        
        itm = QTreeWidgetItem(self.statusList)
        itm.setData(0, Qt.DisplayRole, "")
        itm.setData(1, Qt.DisplayRole, self.currentChangelist)
        itm.setData(2, Qt.DisplayRole, statusText)
        itm.setData(3, Qt.DisplayRole, self.propStatus[propStatus])
        itm.setData(4, Qt.DisplayRole, self.locked[locked])
        itm.setData(5, Qt.DisplayRole, self.history[history])
        itm.setData(6, Qt.DisplayRole, self.switched[switched])
        itm.setData(7, Qt.DisplayRole, self.lockinfo[lockinfo])
        itm.setData(8, Qt.DisplayRole, self.uptodate[uptodate])
        itm.setData(9, Qt.DisplayRole, rev)
        itm.setData(10, Qt.DisplayRole, chg)
        itm.setData(11, Qt.DisplayRole, author)
        itm.setData(12, Qt.DisplayRole, path)
        
        itm.setTextAlignment(1, Qt.AlignLeft)
        itm.setTextAlignment(2, Qt.AlignHCenter)
        itm.setTextAlignment(3, Qt.AlignHCenter)
        itm.setTextAlignment(4, Qt.AlignHCenter)
        itm.setTextAlignment(5, Qt.AlignHCenter)
        itm.setTextAlignment(6, Qt.AlignHCenter)
        itm.setTextAlignment(7, Qt.AlignHCenter)
        itm.setTextAlignment(8, Qt.AlignHCenter)
        itm.setTextAlignment(9, Qt.AlignRight)
        itm.setTextAlignment(10, Qt.AlignRight)
        itm.setTextAlignment(11, Qt.AlignLeft)
        itm.setTextAlignment(12, Qt.AlignLeft)
        
        if status in "ADM" or propStatus in "M":
            itm.setFlags(itm.flags() | Qt.ItemIsUserCheckable)
            itm.setCheckState(self.__toBeCommittedColumn, Qt.Checked)
        else:
            itm.setFlags(itm.flags() & ~Qt.ItemIsUserCheckable)
        
        self.hidePropertyStatusColumn = self.hidePropertyStatusColumn and \
            propStatus == " "
        self.hideLockColumns = self.hideLockColumns and \
            locked == " " and lockinfo == " "
        self.hideUpToDateColumn = self.hideUpToDateColumn and uptodate == " "
        self.hideHistoryColumn = self.hideHistoryColumn and history == " "
        self.hideSwitchedColumn = self.hideSwitchedColumn and switched == " "
        
        if statusText not in self.__statusFilters:
            self.__statusFilters.append(statusText)
        
    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        e.accept()
        
    def start(self, fn):
        """
        Public slot to start the svn status command.
        
        @param fn filename(s)/directoryname(s) to show the status of
            (string or list of strings)
        """
        self.errorGroup.hide()
        self.intercept = False
        self.args = fn
        
        for act in self.menuactions:
            act.setEnabled(False)
        
        self.addButton.setEnabled(False)
        self.commitButton.setEnabled(False)
        self.diffButton.setEnabled(False)
        self.sbsDiffButton.setEnabled(False)
        self.revertButton.setEnabled(False)
        self.restoreButton.setEnabled(False)
        
        self.statusFilterCombo.clear()
        self.__statusFilters = []
        
        self.currentChangelist = ""
        self.changelistFound = False
        
        self.hidePropertyStatusColumn = True
        self.hideLockColumns = True
        self.hideUpToDateColumn = True
        self.hideHistoryColumn = True
        self.hideSwitchedColumn = True
        
        if self.process:
            self.process.kill()
        else:
            self.process = QProcess()
            self.process.finished.connect(self.__procFinished)
            self.process.readyReadStandardOutput.connect(self.__readStdout)
            self.process.readyReadStandardError.connect(self.__readStderr)
        
        args = []
        args.append('status')
        self.vcs.addArguments(args, self.vcs.options['global'])
        self.vcs.addArguments(args, self.vcs.options['status'])
        if '--verbose' not in self.vcs.options['global'] and \
           '--verbose' not in self.vcs.options['status']:
            args.append('--verbose')
            self.__nonverbose = True
        else:
            self.__nonverbose = False
        if '--show-updates' in self.vcs.options['status'] or \
           '-u' in self.vcs.options['status']:
            self.activateWindow()
            self.raise_()
        if isinstance(fn, list):
            self.dname, fnames = self.vcs.splitPathList(fn)
            self.vcs.addArguments(args, fnames)
        else:
            self.dname, fname = self.vcs.splitPath(fn)
            args.append(fname)
        
        self.process.setWorkingDirectory(self.dname)
        
        self.setWindowTitle(self.tr('Subversion Status'))
        
        self.process.start('svn', args)
        procStarted = self.process.waitForStarted(5000)
        if not procStarted:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            E5MessageBox.critical(
                self,
                self.tr('Process Generation Error'),
                self.tr(
                    'The process {0} could not be started. '
                    'Ensure, that it is in the search path.'
                ).format('svn'))
        else:
            self.inputGroup.setEnabled(True)
            self.inputGroup.show()
        
    def __finish(self):
        """
        Private slot called when the process finished or the user pressed
        the button.
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        self.buttonBox.button(QDialogButtonBox.Close).setFocus(
            Qt.OtherFocusReason)
        
        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()
        self.refreshButton.setEnabled(True)
        
        self.__statusFilters.sort()
        self.__statusFilters.insert(0, "<{0}>".format(self.tr("all")))
        self.statusFilterCombo.addItems(self.__statusFilters)
        
        for act in self.menuactions:
            act.setEnabled(True)
        
        self.process = None
        
        self.__resort()
        self.__resizeColumns()
        
        self.statusList.setColumnHidden(self.__changelistColumn,
                                        not self.changelistFound)
        self.statusList.setColumnHidden(self.__propStatusColumn,
                                        self.hidePropertyStatusColumn)
        self.statusList.setColumnHidden(self.__lockedColumn,
                                        self.hideLockColumns)
        self.statusList.setColumnHidden(self.__lockinfoColumn,
                                        self.hideLockColumns)
        self.statusList.setColumnHidden(self.__upToDateColumn,
                                        self.hideUpToDateColumn)
        self.statusList.setColumnHidden(self.__historyColumn,
                                        self.hideHistoryColumn)
        self.statusList.setColumnHidden(self.__switchedColumn,
                                        self.hideSwitchedColumn)
        
        self.__updateButtons()
        self.__updateCommitButton()
        
    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.close()
        elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
            self.__finish()
        elif button == self.refreshButton:
            self.on_refreshButton_clicked()
        
    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__finish()
        
    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        if self.process is not None:
            self.process.setReadChannel(QProcess.StandardOutput)
            
            while self.process.canReadLine():
                s = str(self.process.readLine(),
                        Preferences.getSystem("IOEncoding"),
                        'replace')
                if self.rx_status.exactMatch(s):
                    flags = self.rx_status.cap(1)
                    rev = self.rx_status.cap(2)
                    change = self.rx_status.cap(3)
                    author = self.rx_status.cap(4)
                    path = self.rx_status.cap(5).strip()
                    
                    self.__generateItem(flags[0], flags[1], flags[2], flags[3],
                                        flags[4], flags[5], flags[-1], rev,
                                        change, author, path)
                elif self.rx_status2.exactMatch(s):
                    flags = self.rx_status2.cap(1)
                    path = self.rx_status2.cap(2).strip()
                    
                    self.__generateItem(flags[0], flags[1], flags[2], flags[3],
                                        flags[4], flags[5], flags[-1], "", "",
                                        "", path)
                elif self.rx_changelist.exactMatch(s):
                    self.currentChangelist = self.rx_changelist.cap(1)
                    self.changelistFound = True
        
    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            self.errorGroup.show()
            s = str(self.process.readAllStandardError(),
                    Preferences.getSystem("IOEncoding"),
                    'replace')
            self.errors.insertPlainText(s)
            self.errors.ensureCursorVisible()
        
    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)
        
    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the subversion process.
        """
        input = self.input.text()
        input += os.linesep
        
        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(input)
            self.errors.ensureCursorVisible()
        
        self.process.write(input)
        
        self.passwordCheckBox.setChecked(False)
        self.input.clear()
        
    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()
        
    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(SvnStatusDialog, self).keyPressEvent(evt)
        
    @pyqtSlot()
    def on_refreshButton_clicked(self):
        """
        Private slot to refresh the status display.
        """
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
        
        self.inputGroup.setEnabled(True)
        self.inputGroup.show()
        self.refreshButton.setEnabled(False)
        
        self.statusList.clear()
        
        self.start(self.args)
        
    def __updateButtons(self):
        """
        Private method to update the VCS buttons status.
        """
        modified = len(self.__getModifiedItems())
        unversioned = len(self.__getUnversionedItems())
        missing = len(self.__getMissingItems())

        self.addButton.setEnabled(unversioned)
        self.diffButton.setEnabled(modified)
        self.sbsDiffButton.setEnabled(modified == 1)
        self.revertButton.setEnabled(modified)
        self.restoreButton.setEnabled(missing)
    
    def __updateCommitButton(self):
        """
        Private method to update the Commit button status.
        """
        commitable = len(self.__getCommitableItems())
        self.commitButton.setEnabled(commitable)
    
    @pyqtSlot(str)
    def on_statusFilterCombo_activated(self, txt):
        """
        Private slot to react to the selection of a status filter.
        
        @param txt selected status filter (string)
        """
        if txt == "<{0}>".format(self.tr("all")):
            for topIndex in range(self.statusList.topLevelItemCount()):
                topItem = self.statusList.topLevelItem(topIndex)
                topItem.setHidden(False)
        else:
            for topIndex in range(self.statusList.topLevelItemCount()):
                topItem = self.statusList.topLevelItem(topIndex)
                topItem.setHidden(topItem.text(self.__statusColumn) != txt)
    
    @pyqtSlot(QTreeWidgetItem, int)
    def on_statusList_itemChanged(self, item, column):
        """
        Private slot to act upon item changes.
        
        @param item reference to the changed item (QTreeWidgetItem)
        @param column index of column that changed (integer)
        """
        if column == self.__toBeCommittedColumn:
            self.__updateCommitButton()
    
    @pyqtSlot()
    def on_statusList_itemSelectionChanged(self):
        """
        Private slot to act upon changes of selected items.
        """
        self.__updateButtons()
    
    @pyqtSlot()
    def on_commitButton_clicked(self):
        """
        Private slot to handle the press of the Commit button.
        """
        self.__commit()
    
    @pyqtSlot()
    def on_addButton_clicked(self):
        """
        Private slot to handle the press of the Add button.
        """
        self.__add()
    
    @pyqtSlot()
    def on_diffButton_clicked(self):
        """
        Private slot to handle the press of the Differences button.
        """
        self.__diff()
    
    @pyqtSlot()
    def on_sbsDiffButton_clicked(self):
        """
        Private slot to handle the press of the Side-by-Side Diff button.
        """
        self.__sbsDiff()
    
    @pyqtSlot()
    def on_revertButton_clicked(self):
        """
        Private slot to handle the press of the Revert button.
        """
        self.__revert()
    
    @pyqtSlot()
    def on_restoreButton_clicked(self):
        """
        Private slot to handle the press of the Restore button.
        """
        self.__restoreMissing()
    
    ###########################################################################
    ## Context menu handling methods
    ###########################################################################
    
    def __showContextMenu(self, coord):
        """
        Private slot to show the context menu of the status list.
        
        @param coord the position of the mouse pointer (QPoint)
        """
        self.menu.popup(self.statusList.mapToGlobal(coord))
        
    def __commit(self):
        """
        Private slot to handle the Commit context menu entry.
        """
        names = [os.path.join(self.dname, itm.text(self.__pathColumn))
                 for itm in self.__getCommitableItems()]
        if not names:
            E5MessageBox.information(
                self,
                self.tr("Commit"),
                self.tr("""There are no entries selected to be"""
                        """ committed."""))
            return
        
        if Preferences.getVCS("AutoSaveFiles"):
            vm = e5App().getObject("ViewManager")
            for name in names:
                vm.saveEditor(name)
        self.vcs.vcsCommit(names, '')
       
    def __committed(self):
        """
        Private slot called after the commit has finished.
        """
        if self.isVisible():
            self.on_refreshButton_clicked()
            self.vcs.checkVCSStatus()
        
    def __commitSelectAll(self):
        """
        Private slot to select all entries for commit.
        """
        self.__commitSelect(True)
    
    def __commitDeselectAll(self):
        """
        Private slot to deselect all entries from commit.
        """
        self.__commitSelect(False)
    
    def __add(self):
        """
        Private slot to handle the Add context menu entry.
        """
        names = [os.path.join(self.dname, itm.text(self.__pathColumn))
                 for itm in self.__getUnversionedItems()]
        if not names:
            E5MessageBox.information(
                self,
                self.tr("Add"),
                self.tr("""There are no unversioned entries"""
                        """ available/selected."""))
            return
        
        self.vcs.vcsAdd(names)
        self.on_refreshButton_clicked()
        
        project = e5App().getObject("Project")
        for name in names:
            project.getModel().updateVCSStatus(name)
        self.vcs.checkVCSStatus()
        
    def __revert(self):
        """
        Private slot to handle the Revert context menu entry.
        """
        names = [os.path.join(self.dname, itm.text(self.__pathColumn))
                 for itm in self.__getModifiedItems()]
        if not names:
            E5MessageBox.information(
                self,
                self.tr("Revert"),
                self.tr("""There are no uncommitted changes"""
                        """ available/selected."""))
            return
        
        self.vcs.vcsRevert(names)
        self.raise_()
        self.activateWindow()
        self.on_refreshButton_clicked()
        
        project = e5App().getObject("Project")
        for name in names:
            project.getModel().updateVCSStatus(name)
        self.vcs.checkVCSStatus()
        
    def __restoreMissing(self):
        """
        Private slot to handle the Restore Missing context menu entry.
        """
        names = [os.path.join(self.dname, itm.text(self.__pathColumn))
                 for itm in self.__getMissingItems()]
        if not names:
            E5MessageBox.information(
                self,
                self.tr("Revert"),
                self.tr("""There are no missing entries"""
                        """ available/selected."""))
            return
        
        self.vcs.vcsRevert(names)
        self.on_refreshButton_clicked()
        self.vcs.checkVCSStatus()
        
    def __diff(self):
        """
        Private slot to handle the Diff context menu entry.
        """
        names = [os.path.join(self.dname, itm.text(self.__pathColumn))
                 for itm in self.__getModifiedItems()]
        if not names:
            E5MessageBox.information(
                self,
                self.tr("Differences"),
                self.tr("""There are no uncommitted changes"""
                        """ available/selected."""))
            return
        
        if self.diff is None:
            from .SvnDiffDialog import SvnDiffDialog
            self.diff = SvnDiffDialog(self.vcs)
        self.diff.show()
        QApplication.processEvents()
        self.diff.start(names)
    
    def __sbsDiff(self):
        """
        Private slot to handle the Side-by-Side Diff context menu entry.
        """
        names = [os.path.join(self.dname, itm.text(self.__pathColumn))
                 for itm in self.__getModifiedItems()]
        if not names:
            E5MessageBox.information(
                self,
                self.tr("Side-by-Side Diff"),
                self.tr("""There are no uncommitted changes"""
                        """ available/selected."""))
            return
        elif len(names) > 1:
            E5MessageBox.information(
                self,
                self.tr("Side-by-Side Diff"),
                self.tr("""Only one file with uncommitted changes"""
                        """ must be selected."""))
            return
        
        self.vcs.svnSbsDiff(names[0])
    
    def __lock(self):
        """
        Private slot to handle the Lock context menu entry.
        """
        names = [os.path.join(self.dname, itm.text(self.__pathColumn))
                 for itm in self.__getLockActionItems(self.unlockedIndicators)]
        if not names:
            E5MessageBox.information(
                self,
                self.tr("Lock"),
                self.tr("""There are no unlocked files"""
                        """ available/selected."""))
            return
        
        self.vcs.svnLock(names, parent=self)
        self.on_refreshButton_clicked()
        
    def __unlock(self):
        """
        Private slot to handle the Unlock context menu entry.
        """
        names = [os.path.join(self.dname, itm.text(self.__pathColumn))
                 for itm in self.__getLockActionItems(self.lockedIndicators)]
        if not names:
            E5MessageBox.information(
                self,
                self.tr("Unlock"),
                self.tr("""There are no locked files"""
                        """ available/selected."""))
            return
        
        self.vcs.svnUnlock(names, parent=self)
        self.on_refreshButton_clicked()
        
    def __breakLock(self):
        """
        Private slot to handle the Break Lock context menu entry.
        """
        names = [os.path.join(self.dname, itm.text(self.__pathColumn))
                 for itm in self.__getLockActionItems(
                     self.stealBreakLockIndicators)]
        if not names:
            E5MessageBox.information(
                self,
                self.tr("Break Lock"),
                self.tr("""There are no locked files"""
                        """ available/selected."""))
            return
        
        self.vcs.svnUnlock(names, parent=self, breakIt=True)
        self.on_refreshButton_clicked()

    def __stealLock(self):
        """
        Private slot to handle the Break Lock context menu entry.
        """
        names = [os.path.join(self.dname, itm.text(self.__pathColumn))
                 for itm in self.__getLockActionItems(
                     self.stealBreakLockIndicators)]
        if not names:
            E5MessageBox.information(
                self,
                self.tr("Steal Lock"),
                self.tr("""There are no locked files"""
                        """ available/selected."""))
            return
        
        self.vcs.svnLock(names, parent=self, stealIt=True)
        self.on_refreshButton_clicked()

    def __addToChangelist(self):
        """
        Private slot to add entries to a changelist.
        """
        names = [os.path.join(self.dname, itm.text(self.__pathColumn))
                 for itm in self.__getNonChangelistItems()]
        if not names:
            E5MessageBox.information(
                self,
                self.tr("Remove from Changelist"),
                self.tr(
                    """There are no files available/selected not """
                    """belonging to a changelist."""
                )
            )
            return
        self.vcs.svnAddToChangelist(names)
        self.on_refreshButton_clicked()

    def __removeFromChangelist(self):
        """
        Private slot to remove entries from their changelists.
        """
        names = [os.path.join(self.dname, itm.text(self.__pathColumn))
                 for itm in self.__getChangelistItems()]
        if not names:
            E5MessageBox.information(
                self,
                self.tr("Remove from Changelist"),
                self.tr(
                    """There are no files available/selected belonging"""
                    """ to a changelist."""
                )
            )
            return
        self.vcs.svnRemoveFromChangelist(names)
        self.on_refreshButton_clicked()

    def __getCommitableItems(self):
        """
        Private method to retrieve all entries the user wants to commit.
        
        @return list of all items, the user has checked
        """
        commitableItems = []
        for index in range(self.statusList.topLevelItemCount()):
            itm = self.statusList.topLevelItem(index)
            if itm.checkState(self.__toBeCommittedColumn) == Qt.Checked:
                commitableItems.append(itm)
        return commitableItems
    
    def __getModifiedItems(self):
        """
        Private method to retrieve all entries, that have a modified status.
        
        @return list of all items with a modified status
        """
        modifiedItems = []
        for itm in self.statusList.selectedItems():
            if itm.text(self.__statusColumn) in self.modifiedIndicators or \
               itm.text(self.__propStatusColumn) in self.modifiedIndicators:
                modifiedItems.append(itm)
        return modifiedItems
        
    def __getUnversionedItems(self):
        """
        Private method to retrieve all entries, that have an unversioned
        status.
        
        @return list of all items with an unversioned status
        """
        unversionedItems = []
        for itm in self.statusList.selectedItems():
            if itm.text(self.__statusColumn) in self.unversionedIndicators:
                unversionedItems.append(itm)
        return unversionedItems
        
    def __getMissingItems(self):
        """
        Private method to retrieve all entries, that have a missing status.
        
        @return list of all items with a missing status
        """
        missingItems = []
        for itm in self.statusList.selectedItems():
            if itm.text(self.__statusColumn) in self.missingIndicators:
                missingItems.append(itm)
        return missingItems
        
    def __getLockActionItems(self, indicators):
        """
        Private method to retrieve all emtries, that have a locked status.
        
        @param indicators list of indicators to check against (list of strings)
        @return list of all items with a locked status
        """
        lockitems = []
        for itm in self.statusList.selectedItems():
            if itm.text(self.__lockinfoColumn) in indicators:
                lockitems.append(itm)
        return lockitems
        
    def __getChangelistItems(self):
        """
        Private method to retrieve all entries, that are members of
        a changelist.
        
        @return list of all items belonging to a changelist
        """
        clitems = []
        for itm in self.statusList.selectedItems():
            if itm.text(self.__changelistColumn) != "":
                clitems.append(itm)
        return clitems
        
    def __getNonChangelistItems(self):
        """
        Private method to retrieve all entries, that are not members of
        a changelist.
        
        @return list of all items not belonging to a changelist
        """
        clitems = []
        for itm in self.statusList.selectedItems():
            if itm.text(self.__changelistColumn) == "":
                clitems.append(itm)
        return clitems
    
    def __commitSelect(self, selected):
        """
        Private slot to select or deselect all entries.
        
        @param selected commit selection state to be set (boolean)
        """
        for index in range(self.statusList.topLevelItemCount()):
            itm = self.statusList.topLevelItem(index)
            if itm.flags() & Qt.ItemIsUserCheckable:
                if selected:
                    itm.setCheckState(self.__toBeCommittedColumn, Qt.Checked)
                else:
                    itm.setCheckState(self.__toBeCommittedColumn, Qt.Unchecked)
Beispiel #39
0
class HgStatusDialog(QWidget, Ui_HgStatusDialog):
    """
    Class implementing a dialog to show the output of the hg status command
    process.
    """
    def __init__(self, vcs, mq=False, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param mq flag indicating to show a queue repo status (boolean)
        @param parent parent widget (QWidget)
        """
        super(HgStatusDialog, self).__init__(parent)
        self.setupUi(self)

        self.__toBeCommittedColumn = 0
        self.__statusColumn = 1
        self.__pathColumn = 2
        self.__lastColumn = self.statusList.columnCount()

        self.refreshButton = self.buttonBox.addButton(
            self.tr("Refresh"), QDialogButtonBox.ActionRole)
        self.refreshButton.setToolTip(
            self.tr("Press to refresh the status display"))
        self.refreshButton.setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)

        self.diff = None
        self.vcs = vcs
        self.vcs.committed.connect(self.__committed)
        self.__hgClient = self.vcs.getClient()
        self.__mq = mq
        if self.__hgClient:
            self.process = None
        else:
            self.process = QProcess()
            self.process.finished.connect(self.__procFinished)
            self.process.readyReadStandardOutput.connect(self.__readStdout)
            self.process.readyReadStandardError.connect(self.__readStderr)

        self.diffSplitter.setSizes([350, 250])
        self.__diffSplitterState = None

        self.statusList.headerItem().setText(self.__lastColumn, "")
        self.statusList.header().setSortIndicator(self.__pathColumn,
                                                  Qt.AscendingOrder)

        font = Preferences.getEditorOtherFonts("MonospacedFont")
        self.diffEdit.setFontFamily(font.family())
        self.diffEdit.setFontPointSize(font.pointSize())

        self.diffHighlighter = HgDiffHighlighter(self.diffEdit.document())
        self.__diffGenerator = HgDiffGenerator(vcs, self)
        self.__diffGenerator.finished.connect(self.__generatorFinished)

        self.__selectedName = ""

        if mq:
            self.buttonsLine.setVisible(False)
            self.addButton.setVisible(False)
            self.diffButton.setVisible(False)
            self.sbsDiffButton.setVisible(False)
            self.revertButton.setVisible(False)
            self.forgetButton.setVisible(False)
            self.restoreButton.setVisible(False)

            self.diffEdit.setVisible(False)

        self.menuactions = []
        self.lfActions = []
        self.menu = QMenu()
        if not mq:
            self.__commitAct = self.menu.addAction(
                self.tr("Commit changes to repository..."), self.__commit)
            self.menuactions.append(self.__commitAct)
            self.menuactions.append(
                self.menu.addAction(self.tr("Select all for commit"),
                                    self.__commitSelectAll))
            self.menuactions.append(
                self.menu.addAction(self.tr("Deselect all from commit"),
                                    self.__commitDeselectAll))
            self.menu.addSeparator()
            self.__addAct = self.menu.addAction(self.tr("Add to repository"),
                                                self.__add)
            self.menuactions.append(self.__addAct)
            if self.vcs.version >= (2, 0):
                self.lfActions.append(
                    self.menu.addAction(self.tr("Add as Large File"),
                                        lambda: self.__lfAdd("large")))
                self.lfActions.append(
                    self.menu.addAction(self.tr("Add as Normal File"),
                                        lambda: self.__lfAdd("normal")))
            self.menu.addSeparator()
            self.__diffAct = self.menu.addAction(self.tr("Show differences"),
                                                 self.__diff)
            self.menuactions.append(self.__diffAct)
            self.__sbsDiffAct = self.menu.addAction(
                self.tr("Show differences side-by-side"), self.__sbsDiff)
            self.menuactions.append(self.__sbsDiffAct)
            self.menu.addSeparator()
            self.__revertAct = self.menu.addAction(self.tr("Revert changes"),
                                                   self.__revert)
            self.menuactions.append(self.__revertAct)
            self.__forgetAct = self.menu.addAction(self.tr("Forget missing"),
                                                   self.__forget)
            self.menuactions.append(self.__forgetAct)
            self.__restoreAct = self.menu.addAction(self.tr("Restore missing"),
                                                    self.__restoreMissing)
            self.menuactions.append(self.__restoreAct)
            self.menu.addSeparator()
            self.menuactions.append(
                self.menu.addAction(self.tr("Adjust column sizes"),
                                    self.__resizeColumns))
            for act in self.menuactions:
                act.setEnabled(False)
            for act in self.lfActions:
                act.setEnabled(False)

            self.statusList.setContextMenuPolicy(Qt.CustomContextMenu)
            self.statusList.customContextMenuRequested.connect(
                self.__showContextMenu)

        if not mq and self.vcs.version >= (2, 0):
            self.__lfAddActions = []
            self.__addButtonMenu = QMenu()
            self.__addButtonMenu.addAction(self.tr("Add"), self.__add)
            self.__lfAddActions.append(
                self.__addButtonMenu.addAction(self.tr("Add as Large File"),
                                               lambda: self.__lfAdd("large")))
            self.__lfAddActions.append(
                self.__addButtonMenu.addAction(self.tr("Add as Normal File"),
                                               lambda: self.__lfAdd("normal")))
            self.__addButtonMenu.aboutToShow.connect(self.__showAddMenu)
            if self.vcs.isExtensionActive("largefiles"):
                self.addButton.setMenu(self.__addButtonMenu)

        if not mq:
            self.vcs.activeExtensionsChanged.connect(
                self.__activeExtensionsChanged)

        self.modifiedIndicators = [
            self.tr('added'),
            self.tr('modified'),
            self.tr('removed'),
        ]

        self.unversionedIndicators = [
            self.tr('not tracked'),
        ]

        self.missingIndicators = [self.tr('missing')]

        self.status = {
            'A': self.tr('added'),
            'C': self.tr('normal'),
            'I': self.tr('ignored'),
            'M': self.tr('modified'),
            'R': self.tr('removed'),
            '?': self.tr('not tracked'),
            '!': self.tr('missing'),
        }

    def __activeExtensionsChanged(self):
        """
        Private slot handling a change in the activated extensions.
        """
        if self.vcs.isExtensionActive("largefiles"):
            if self.addButton.menu() is None:
                self.addButton.setMenu(self.__addButtonMenu)
        else:
            if self.addButton.menu() is not None:
                self.addButton.setMenu(None)

    def show(self):
        """
        Public slot to show the dialog.
        """
        super(HgStatusDialog, self).show()

        if not self.__mq and self.__diffSplitterState:
            self.diffSplitter.restoreState(self.__diffSplitterState)

    def __resort(self):
        """
        Private method to resort the tree.
        """
        self.statusList.sortItems(
            self.statusList.sortColumn(),
            self.statusList.header().sortIndicatorOrder())

    def __resizeColumns(self):
        """
        Private method to resize the list columns.
        """
        self.statusList.header().resizeSections(QHeaderView.ResizeToContents)
        self.statusList.header().setStretchLastSection(True)

    def __generateItem(self, status, path):
        """
        Private method to generate a status item in the status list.
        
        @param status status indicator (string)
        @param path path of the file or directory (string)
        """
        statusText = self.status[status]
        itm = QTreeWidgetItem(self.statusList, [
            "",
            statusText,
            path,
        ])

        itm.setTextAlignment(1, Qt.AlignHCenter)
        itm.setTextAlignment(2, Qt.AlignLeft)

        if status in "AMR":
            itm.setFlags(itm.flags() | Qt.ItemIsUserCheckable)
            itm.setCheckState(self.__toBeCommittedColumn, Qt.Checked)
        else:
            itm.setFlags(itm.flags() & ~Qt.ItemIsUserCheckable)

        if statusText not in self.__statusFilters:
            self.__statusFilters.append(statusText)

    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.__hgClient:
            if self.__hgClient.isExecuting():
                self.__hgClient.cancel()
        else:
            if self.process is not None and \
               self.process.state() != QProcess.NotRunning:
                self.process.terminate()
                QTimer.singleShot(2000, self.process.kill)
                self.process.waitForFinished(3000)

        if not self.__mq:
            self.__diffSplitterState = self.diffSplitter.saveState()

        e.accept()

    def start(self, fn):
        """
        Public slot to start the hg status command.
        
        @param fn filename(s)/directoryname(s) to show the status of
            (string or list of strings)
        """
        self.errorGroup.hide()
        self.intercept = False
        self.args = fn

        for act in self.menuactions:
            act.setEnabled(False)
        for act in self.lfActions:
            act.setEnabled(False)

        self.addButton.setEnabled(False)
        self.commitButton.setEnabled(False)
        self.diffButton.setEnabled(False)
        self.sbsDiffButton.setEnabled(False)
        self.revertButton.setEnabled(False)
        self.forgetButton.setEnabled(False)
        self.restoreButton.setEnabled(False)

        self.statusFilterCombo.clear()
        self.__statusFilters = []
        self.statusList.clear()

        if self.__mq:
            self.setWindowTitle(self.tr("Mercurial Queue Repository Status"))
        else:
            self.setWindowTitle(self.tr('Mercurial Status'))

        args = self.vcs.initCommand("status")
        if self.__mq:
            args.append('--mq')
            if isinstance(fn, list):
                self.dname, fnames = self.vcs.splitPathList(fn)
            else:
                self.dname, fname = self.vcs.splitPath(fn)
        else:
            if self.vcs.hasSubrepositories():
                args.append("--subrepos")

            if isinstance(fn, list):
                self.dname, fnames = self.vcs.splitPathList(fn)
                self.vcs.addArguments(args, fn)
            else:
                self.dname, fname = self.vcs.splitPath(fn)
                args.append(fn)

        # find the root of the repo
        repodir = self.dname
        while not os.path.isdir(os.path.join(repodir, self.vcs.adminDir)):
            repodir = os.path.dirname(repodir)
            if os.path.splitdrive(repodir)[1] == os.sep:
                return

        if self.__hgClient:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            self.refreshButton.setEnabled(False)

            out, err = self.__hgClient.runcommand(args)
            if err:
                self.__showError(err)
            if out:
                for line in out.splitlines():
                    self.__processOutputLine(line)
                    if self.__hgClient.wasCanceled():
                        break
            self.__finish()
        else:
            if self.process:
                self.process.kill()

            self.process.setWorkingDirectory(repodir)

            self.process.start('hg', args)
            procStarted = self.process.waitForStarted(5000)
            if not procStarted:
                self.inputGroup.setEnabled(False)
                self.inputGroup.hide()
                E5MessageBox.critical(
                    self, self.tr('Process Generation Error'),
                    self.tr(
                        'The process {0} could not be started. '
                        'Ensure, that it is in the search path.').format('hg'))
            else:
                self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
                self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
                self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)

                self.inputGroup.setEnabled(True)
                self.inputGroup.show()
                self.refreshButton.setEnabled(False)

    def __finish(self):
        """
        Private slot called when the process finished or the user pressed
        the button.
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)

        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()
        self.refreshButton.setEnabled(True)

        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        self.buttonBox.button(QDialogButtonBox.Close).setFocus(
            Qt.OtherFocusReason)

        self.__statusFilters.sort()
        self.__statusFilters.insert(0, "<{0}>".format(self.tr("all")))
        self.statusFilterCombo.addItems(self.__statusFilters)

        for act in self.menuactions:
            act.setEnabled(True)

        self.__resort()
        self.__resizeColumns()

        self.__updateButtons()
        self.__updateCommitButton()

        self.__refreshDiff()

    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.close()
        elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
            if self.__hgClient:
                self.__hgClient.cancel()
            else:
                self.__finish()
        elif button == self.refreshButton:
            self.on_refreshButton_clicked()

    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__finish()

    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        if self.process is not None:
            self.process.setReadChannel(QProcess.StandardOutput)

            while self.process.canReadLine():
                line = str(self.process.readLine(), self.vcs.getEncoding(),
                           'replace')
                self.__processOutputLine(line)

    def __processOutputLine(self, line):
        """
        Private method to process the lines of output.
        
        @param line output line to be processed (string)
        """
        if line[0] in "ACIMR?!" and line[1] == " ":
            status, path = line.strip().split(" ", 1)
            self.__generateItem(status, path)

    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            s = str(self.process.readAllStandardError(),
                    self.vcs.getEncoding(), 'replace')
            self.__showError(s)

    def __showError(self, out):
        """
        Private slot to show some error.
        
        @param out error to be shown (string)
        """
        self.errorGroup.show()
        self.errors.insertPlainText(out)
        self.errors.ensureCursorVisible()

    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)

    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the subversion process.
        """
        input = self.input.text()
        input += os.linesep

        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(input)
            self.errors.ensureCursorVisible()

        self.process.write(input)

        self.passwordCheckBox.setChecked(False)
        self.input.clear()

    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()

    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(HgStatusDialog, self).keyPressEvent(evt)

    @pyqtSlot()
    def on_refreshButton_clicked(self):
        """
        Private slot to refresh the status display.
        """
        selectedItems = self.statusList.selectedItems()
        if len(selectedItems) == 1:
            self.__selectedName = selectedItems[0].text(self.__pathColumn)
        else:
            self.__selectedName = ""

        self.start(self.args)

    def __updateButtons(self):
        """
        Private method to update the VCS buttons status.
        """
        modified = len(self.__getModifiedItems())
        unversioned = len(self.__getUnversionedItems())
        missing = len(self.__getMissingItems())

        self.addButton.setEnabled(unversioned)
        self.diffButton.setEnabled(modified)
        self.sbsDiffButton.setEnabled(modified == 1)
        self.revertButton.setEnabled(modified)
        self.forgetButton.setEnabled(missing)
        self.restoreButton.setEnabled(missing)

    def __updateCommitButton(self):
        """
        Private method to update the Commit button status.
        """
        commitable = len(self.__getCommitableItems())
        self.commitButton.setEnabled(commitable)

    @pyqtSlot(str)
    def on_statusFilterCombo_activated(self, txt):
        """
        Private slot to react to the selection of a status filter.
        
        @param txt selected status filter (string)
        """
        if txt == "<{0}>".format(self.tr("all")):
            for topIndex in range(self.statusList.topLevelItemCount()):
                topItem = self.statusList.topLevelItem(topIndex)
                topItem.setHidden(False)
        else:
            for topIndex in range(self.statusList.topLevelItemCount()):
                topItem = self.statusList.topLevelItem(topIndex)
                topItem.setHidden(topItem.text(self.__statusColumn) != txt)

    @pyqtSlot(QTreeWidgetItem, int)
    def on_statusList_itemChanged(self, item, column):
        """
        Private slot to act upon item changes.
        
        @param item reference to the changed item (QTreeWidgetItem)
        @param column index of column that changed (integer)
        """
        if column == self.__toBeCommittedColumn:
            self.__updateCommitButton()

    @pyqtSlot()
    def on_statusList_itemSelectionChanged(self):
        """
        Private slot to act upon changes of selected items.
        """
        self.__updateButtons()
        self.__generateDiffs()

    @pyqtSlot()
    def on_commitButton_clicked(self):
        """
        Private slot to handle the press of the Commit button.
        """
        self.__commit()

    @pyqtSlot()
    def on_addButton_clicked(self):
        """
        Private slot to handle the press of the Add button.
        """
        self.__add()

    @pyqtSlot()
    def on_diffButton_clicked(self):
        """
        Private slot to handle the press of the Differences button.
        """
        self.__diff()

    @pyqtSlot()
    def on_sbsDiffButton_clicked(self):
        """
        Private slot to handle the press of the Side-by-Side Diff button.
        """
        self.__sbsDiff()

    @pyqtSlot()
    def on_revertButton_clicked(self):
        """
        Private slot to handle the press of the Revert button.
        """
        self.__revert()

    @pyqtSlot()
    def on_forgetButton_clicked(self):
        """
        Private slot to handle the press of the Forget button.
        """
        self.__forget()

    @pyqtSlot()
    def on_restoreButton_clicked(self):
        """
        Private slot to handle the press of the Restore button.
        """
        self.__restoreMissing()

    ###########################################################################
    ## Context menu handling methods
    ###########################################################################

    def __showContextMenu(self, coord):
        """
        Private slot to show the context menu of the status list.
        
        @param coord the position of the mouse pointer (QPoint)
        """
        modified = len(self.__getModifiedItems())
        unversioned = len(self.__getUnversionedItems())
        missing = len(self.__getMissingItems())
        commitable = len(self.__getCommitableItems())

        self.__addAct.setEnabled(unversioned)
        self.__diffAct.setEnabled(modified)
        self.__sbsDiffAct.setEnabled(modified == 1)
        self.__revertAct.setEnabled(modified)
        self.__forgetAct.setEnabled(missing)
        self.__restoreAct.setEnabled(missing)
        self.__commitAct.setEnabled(commitable)

        if self.vcs.isExtensionActive("largefiles"):
            enable = len(self.__getUnversionedItems()) > 0
        else:
            enable = False
        for act in self.lfActions:
            act.setEnabled(enable)
        self.menu.popup(self.statusList.mapToGlobal(coord))

    def __showAddMenu(self):
        """
        Private slot to prepare the Add button menu before it is shown.
        """
        enable = self.vcs.isExtensionActive("largefiles")
        for act in self.__lfAddActions:
            act.setEnabled(enable)

    def __commit(self):
        """
        Private slot to handle the Commit context menu entry.
        """
        if self.__mq:
            self.vcs.vcsCommit(self.dname, "", mq=True)
        else:
            names = [
                os.path.join(self.dname, itm.text(self.__pathColumn))
                for itm in self.__getCommitableItems()
            ]
            if not names:
                E5MessageBox.information(
                    self, self.tr("Commit"),
                    self.tr("""There are no entries selected to be"""
                            """ committed."""))
                return

            if Preferences.getVCS("AutoSaveFiles"):
                vm = e5App().getObject("ViewManager")
                for name in names:
                    vm.saveEditor(name)
            self.vcs.vcsCommit(names, '')

    def __committed(self):
        """
        Private slot called after the commit has finished.
        """
        if self.isVisible():
            self.on_refreshButton_clicked()
            self.vcs.checkVCSStatus()

    def __commitSelectAll(self):
        """
        Private slot to select all entries for commit.
        """
        self.__commitSelect(True)

    def __commitDeselectAll(self):
        """
        Private slot to deselect all entries from commit.
        """
        self.__commitSelect(False)

    def __add(self):
        """
        Private slot to handle the Add context menu entry.
        """
        names = [
            os.path.join(self.dname, itm.text(self.__pathColumn))
            for itm in self.__getUnversionedItems()
        ]
        if not names:
            E5MessageBox.information(
                self, self.tr("Add"),
                self.tr("""There are no unversioned entries"""
                        """ available/selected."""))
            return

        self.vcs.vcsAdd(names)
        self.on_refreshButton_clicked()

        project = e5App().getObject("Project")
        for name in names:
            project.getModel().updateVCSStatus(name)
        self.vcs.checkVCSStatus()

    def __lfAdd(self, mode):
        """
        Private slot to add a file to the repository.
        
        @param mode add mode (string one of 'normal' or 'large')
        """
        names = [
            os.path.join(self.dname, itm.text(self.__pathColumn))
            for itm in self.__getUnversionedItems()
        ]
        if not names:
            E5MessageBox.information(
                self, self.tr("Add"),
                self.tr("""There are no unversioned entries"""
                        """ available/selected."""))
            return

        self.vcs.getExtensionObject("largefiles").hgAdd(names, mode)
        self.on_refreshButton_clicked()

        project = e5App().getObject("Project")
        for name in names:
            project.getModel().updateVCSStatus(name)
        self.vcs.checkVCSStatus()

    def __forget(self):
        """
        Private slot to handle the Remove context menu entry.
        """
        names = [
            os.path.join(self.dname, itm.text(self.__pathColumn))
            for itm in self.__getMissingItems()
        ]
        if not names:
            E5MessageBox.information(
                self, self.tr("Remove"),
                self.tr("""There are no missing entries"""
                        """ available/selected."""))
            return

        self.vcs.hgForget(names)
        self.on_refreshButton_clicked()

    def __revert(self):
        """
        Private slot to handle the Revert context menu entry.
        """
        names = [
            os.path.join(self.dname, itm.text(self.__pathColumn))
            for itm in self.__getModifiedItems()
        ]
        if not names:
            E5MessageBox.information(
                self, self.tr("Revert"),
                self.tr("""There are no uncommitted changes"""
                        """ available/selected."""))
            return

        self.vcs.hgRevert(names)
        self.raise_()
        self.activateWindow()
        self.on_refreshButton_clicked()

        project = e5App().getObject("Project")
        for name in names:
            project.getModel().updateVCSStatus(name)
        self.vcs.checkVCSStatus()

    def __restoreMissing(self):
        """
        Private slot to handle the Restore Missing context menu entry.
        """
        names = [
            os.path.join(self.dname, itm.text(self.__pathColumn))
            for itm in self.__getMissingItems()
        ]
        if not names:
            E5MessageBox.information(
                self, self.tr("Revert"),
                self.tr("""There are no missing entries"""
                        """ available/selected."""))
            return

        self.vcs.hgRevert(names)
        self.on_refreshButton_clicked()
        self.vcs.checkVCSStatus()

    def __diff(self):
        """
        Private slot to handle the Diff context menu entry.
        """
        names = [
            os.path.join(self.dname, itm.text(self.__pathColumn))
            for itm in self.__getModifiedItems()
        ]
        if not names:
            E5MessageBox.information(
                self, self.tr("Differences"),
                self.tr("""There are no uncommitted changes"""
                        """ available/selected."""))
            return

        if self.diff is None:
            from .HgDiffDialog import HgDiffDialog
            self.diff = HgDiffDialog(self.vcs)
        self.diff.show()
        self.diff.start(names, refreshable=True)

    def __sbsDiff(self):
        """
        Private slot to handle the Diff context menu entry.
        """
        names = [
            os.path.join(self.dname, itm.text(self.__pathColumn))
            for itm in self.__getModifiedItems()
        ]
        if not names:
            E5MessageBox.information(
                self, self.tr("Side-by-Side Diff"),
                self.tr("""There are no uncommitted changes"""
                        """ available/selected."""))
            return
        elif len(names) > 1:
            E5MessageBox.information(
                self, self.tr("Side-by-Side Diff"),
                self.tr("""Only one file with uncommitted changes"""
                        """ must be selected."""))
            return

        self.vcs.hgSbsDiff(names[0])

    def __getCommitableItems(self):
        """
        Private method to retrieve all entries the user wants to commit.
        
        @return list of all items, the user has checked
        """
        commitableItems = []
        for index in range(self.statusList.topLevelItemCount()):
            itm = self.statusList.topLevelItem(index)
            if itm.checkState(self.__toBeCommittedColumn) == Qt.Checked:
                commitableItems.append(itm)
        return commitableItems

    def __getModifiedItems(self):
        """
        Private method to retrieve all entries, that have a modified status.
        
        @return list of all items with a modified status
        """
        modifiedItems = []
        for itm in self.statusList.selectedItems():
            if itm.text(self.__statusColumn) in self.modifiedIndicators:
                modifiedItems.append(itm)
        return modifiedItems

    def __getUnversionedItems(self):
        """
        Private method to retrieve all entries, that have an unversioned
        status.
        
        @return list of all items with an unversioned status
        """
        unversionedItems = []
        for itm in self.statusList.selectedItems():
            if itm.text(self.__statusColumn) in self.unversionedIndicators:
                unversionedItems.append(itm)
        return unversionedItems

    def __getMissingItems(self):
        """
        Private method to retrieve all entries, that have a missing status.
        
        @return list of all items with a missing status
        """
        missingItems = []
        for itm in self.statusList.selectedItems():
            if itm.text(self.__statusColumn) in self.missingIndicators:
                missingItems.append(itm)
        return missingItems

    def __commitSelect(self, selected):
        """
        Private slot to select or deselect all entries.
        
        @param selected commit selection state to be set (boolean)
        """
        for index in range(self.statusList.topLevelItemCount()):
            itm = self.statusList.topLevelItem(index)
            if itm.flags() & Qt.ItemIsUserCheckable:
                if selected:
                    itm.setCheckState(self.__toBeCommittedColumn, Qt.Checked)
                else:
                    itm.setCheckState(self.__toBeCommittedColumn, Qt.Unchecked)

    ###########################################################################
    ## Diff handling methods below
    ###########################################################################

    def __generateDiffs(self):
        """
        Private slot to generate diff outputs for the selected item.
        """
        self.diffEdit.clear()

        if not self.__mq:
            selectedItems = self.statusList.selectedItems()
            if len(selectedItems) == 1:
                fn = os.path.join(self.dname,
                                  selectedItems[0].text(self.__pathColumn))
                self.__diffGenerator.start(fn)

    def __generatorFinished(self):
        """
        Private slot connected to the finished signal of the diff generator.
        """
        diff = self.__diffGenerator.getResult()[0]

        if diff:
            for line in diff[:]:
                if line.startswith("@@ "):
                    break
                else:
                    diff.pop(0)
            self.diffEdit.setPlainText("".join(diff))

        tc = self.diffEdit.textCursor()
        tc.movePosition(QTextCursor.Start)
        self.diffEdit.setTextCursor(tc)
        self.diffEdit.ensureCursorVisible()

    def __refreshDiff(self):
        """
        Private method to refresh the diff output after a refresh.
        """
        if self.__selectedName and not self.__mq:
            for index in range(self.statusList.topLevelItemCount()):
                itm = self.statusList.topLevelItem(index)
                if itm.text(self.__pathColumn) == self.__selectedName:
                    itm.setSelected(True)
                    break

        self.__selectedName = ""
Beispiel #40
0
class JobRunner(QObject, MooseWidget):
    """
    Actually runs the process. It will read the output and
    translate any terminal color codes into html.
    It will also attempt to parse the output to check to
    see if we are at a new time step and emit the
    timestep_updated signal.
    Signals:
        started: Emitted when we start running.
        finished: Emitted when we are finished. Arguments are exit code and status message.
        outputAdded: Emitted when there is new output.
        timeStepUpdated: A new time step has started
        error: Emitted when an error is encountered. Arguments are QProcess code and error description
    """
    started = pyqtSignal()
    finished = pyqtSignal(int, str)
    outputAdded = pyqtSignal(str)
    timeStepUpdated = pyqtSignal(int)
    error = pyqtSignal(int, str)

    def __init__(self, **kwds):
        super(JobRunner, self).__init__(**kwds)

        self.process = QProcess(self)
        self.process.setProcessChannelMode(QProcess.MergedChannels)
        self.process.readyReadStandardOutput.connect(self._readOutput)
        self.process.finished.connect(self._jobFinished)
        self.process.started.connect(self.started)
        self.process.error.connect(self._error)
        self._error_map = { QProcess.FailedToStart: "Failed to start",
                QProcess.Crashed: "Crashed",
                QProcess.Timedout: "Timedout",
                QProcess.WriteError: "Write error",
                QProcess.ReadError: "Read error",
                QProcess.UnknownError: "Unknown error",
                }

        self.killed = False
        self.setup()

    def run(self, cmd, args):
        """
        Start the command.

        Arguments:
            cmd: The command to run
            args: A list of string arguments
        """
        self.killed = False
        self._sendMessage("Running command: %s %s" % (cmd, ' '.join(args)))
        self._sendMessage("Working directory: %s" % os.getcwd())
        self.process.start(cmd, args)
        self.process.waitForStarted()

    def _sendMessage(self, msg):
        mooseutils.mooseMessage(msg, color="MAGENTA")
        self.outputAdded.emit('<span style="color:magenta;">%s</span>' % msg)

    @pyqtSlot(QProcess.ProcessError)
    def _error(self, err):
        """
        Slot called when the QProcess encounters an error.
        Inputs:
            err: One of the QProcess.ProcessError enums
        """
        if not self.killed:
            msg = self._error_map.get(err, "Unknown error")
            self.error.emit(int(err), msg)
            mooseutils.mooseMessage(msg, color="RED")
            self.outputAdded.emit(msg)

    @pyqtSlot(int, QProcess.ExitStatus)
    def _jobFinished(self, code, status):
        """
        Slot called when the QProcess is finished.
        Inputs:
            code: Exit code of the process.
            status: QProcess.ExitStatus
        """
        exit_status = "Finished"
        if status != QProcess.NormalExit:
            if self.killed:
                exit_status = "Killed by user"
            else:
                exit_status = "Crashed"
        self.finished.emit(code, exit_status)
        self._sendMessage("%s: Exit code: %s" % (exit_status, code))

    def kill(self):
        """
        Kills the QProcess
        """
        self.killed = True
        mooseutils.mooseMessage("Killing")
        self.process.terminate()
        self.process.waitForFinished(1000)
        if self.isRunning():
            mooseutils.mooseMessage("Failed to terminate job cleanly. Doing a hard kill.")
            self.process.kill()
            self.process.waitForFinished()

    @pyqtSlot()
    def _readOutput(self):
        """
        Slot called when the QProcess produces output.
        """
        lines = []
        while self.process.canReadLine():
            tmp = self.process.readLine().data().decode("utf-8").rstrip()
            lines.append(TerminalUtils.terminalOutputToHtml(tmp))
            match = re.search(r'Time\sStep\s*([0-9]{1,})', tmp)
            if match:
                ts = int(match.group(1))
                self.timeStepUpdated.emit(ts)

        output = '<pre style="display: inline; margin: 0;">%s</pre>' % '\n'.join(lines)
        self.outputAdded.emit(output)

    def isRunning(self):
        return self.process.state() == QProcess.Running
class HgConflictsListDialog(QWidget, Ui_HgConflictsListDialog):
    """
    Class implementing a dialog to show a list of files which had or still
    have conflicts.
    """
    StatusRole = Qt.UserRole + 1
    FilenameRole = Qt.UserRole + 2
    
    def __init__(self, vcs, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param parent parent widget (QWidget)
        """
        super(HgConflictsListDialog, self).__init__(parent)
        self.setupUi(self)
        
        self.__position = QPoint()
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
        
        self.conflictsList.headerItem().setText(
            self.conflictsList.columnCount(), "")
        self.conflictsList.header().setSortIndicator(0, Qt.AscendingOrder)
        
        self.refreshButton = self.buttonBox.addButton(
            self.tr("&Refresh"), QDialogButtonBox.ActionRole)
        self.refreshButton.setToolTip(
            self.tr("Press to refresh the list of conflicts"))
        self.refreshButton.setEnabled(False)
        
        self.vcs = vcs
        self.project = e5App().getObject("Project")
        
        self.__hgClient = vcs.getClient()
        if self.__hgClient:
            self.process = None
        else:
            self.process = QProcess()
            self.process.finished.connect(self.__procFinished)
            self.process.readyReadStandardOutput.connect(self.__readStdout)
            self.process.readyReadStandardError.connect(self.__readStderr)
    
    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.__hgClient:
            if self.__hgClient.isExecuting():
                self.__hgClient.cancel()
        else:
            if self.process is not None and \
               self.process.state() != QProcess.NotRunning:
                self.process.terminate()
                QTimer.singleShot(2000, self.process.kill)
                self.process.waitForFinished(3000)
        
        self.__position = self.pos()
        
        e.accept()
    
    def show(self):
        """
        Public slot to show the dialog.
        """
        if not self.__position.isNull():
            self.move(self.__position)
        
        super(HgConflictsListDialog, self).show()
    
    def start(self, path):
        """
        Public slot to start the tags command.
        
        @param path name of directory to list conflicts for (string)
        """
        self.errorGroup.hide()
        QApplication.processEvents()
            
        self.intercept = False
        dname, fname = self.vcs.splitPath(path)
        
        # find the root of the repo
        self.__repodir = dname
        while not os.path.isdir(
                os.path.join(self.__repodir, self.vcs.adminDir)):
            self.__repodir = os.path.dirname(self.__repodir)
            if os.path.splitdrive(self.__repodir)[1] == os.sep:
                return
        
        self.activateWindow()
        self.raise_()
        
        self.conflictsList.clear()
        self.__started = True
        self.__getEntries()
    
    def __getEntries(self):
        """
        Private method to get the conflict entries.
        """
        args = self.vcs.initCommand("resolve")
        args.append('--list')
        
        if self.__hgClient:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            
            out, err = self.__hgClient.runcommand(args)
            if err:
                self.__showError(err)
            if out:
                for line in out.splitlines():
                    self.__processOutputLine(line)
                    if self.__hgClient.wasCanceled():
                        break
            self.__finish()
        else:
            self.process.kill()
            self.process.setWorkingDirectory(self.__repodir)
            
            self.process.start('hg', args)
            procStarted = self.process.waitForStarted(5000)
            if not procStarted:
                self.inputGroup.setEnabled(False)
                self.inputGroup.hide()
                E5MessageBox.critical(
                    self,
                    self.tr('Process Generation Error'),
                    self.tr(
                        'The process {0} could not be started. '
                        'Ensure, that it is in the search path.'
                    ).format('hg'))
            else:
                self.inputGroup.setEnabled(True)
                self.inputGroup.show()

    def __finish(self):
        """
        Private slot called when the process finished or the user pressed
        the button.
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        QApplication.restoreOverrideCursor()
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        
        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()
        self.refreshButton.setEnabled(True)
        
        self.__resizeColumns()
        self.__resort()
        self.on_conflictsList_itemSelectionChanged()
    
    @pyqtSlot(QAbstractButton)
    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.close()
        elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
            if self.__hgClient:
                self.__hgClient.cancel()
            else:
                self.__finish()
        elif button == self.refreshButton:
            self.on_refreshButton_clicked()
    
    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__finish()
    
    def __resort(self):
        """
        Private method to resort the tree.
        """
        self.conflictsList.sortItems(
            self.conflictsList.sortColumn(),
            self.conflictsList.header().sortIndicatorOrder())
    
    def __resizeColumns(self):
        """
        Private method to resize the list columns.
        """
        self.conflictsList.header().resizeSections(
            QHeaderView.ResizeToContents)
        self.conflictsList.header().setStretchLastSection(True)
    
    def __generateItem(self, status, name):
        """
        Private method to generate a tag item in the tag list.
        
        @param status status of the file (string)
        @param name name of the file (string)
        """
        itm = QTreeWidgetItem(self.conflictsList)
        if status == "U":
            itm.setText(0, self.tr("Unresolved"))
        elif status == "R":
            itm.setText(0, self.tr("Resolved"))
        else:
            itm.setText(0, self.tr("Unknown Status"))
        itm.setText(1, name)
        
        itm.setData(0, self.StatusRole, status)
        itm.setData(0, self.FilenameRole, self.project.getAbsolutePath(name))
    
    def __readStdout(self):
        """
        Private slot to handle the readyReadStdout signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        self.process.setReadChannel(QProcess.StandardOutput)
        
        while self.process.canReadLine():
            s = str(self.process.readLine(), self.vcs.getEncoding(),
                    'replace').strip()
            self.__processOutputLine(s)
    
    def __processOutputLine(self, line):
        """
        Private method to process the lines of output.
        
        @param line output line to be processed (string)
        """
        status, filename = line.strip().split(None, 1)
        self.__generateItem(status, filename)
    
    @pyqtSlot()
    def on_refreshButton_clicked(self):
        """
        Private slot to refresh the log.
        """
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
        
        self.inputGroup.setEnabled(True)
        self.inputGroup.show()
        self.refreshButton.setEnabled(False)
        self.start(self.__repodir)
    
    def __readStderr(self):
        """
        Private slot to handle the readyReadStderr signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            s = str(self.process.readAllStandardError(),
                    self.vcs.getEncoding(), 'replace')
            self.__showError(s)
    
    def __showError(self, out):
        """
        Private slot to show some error.
        
        @param out error to be shown (string)
        """
        self.errorGroup.show()
        self.errors.insertPlainText(out)
        self.errors.ensureCursorVisible()
    
    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)
    
    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the subversion process.
        """
        input = self.input.text()
        input += os.linesep
        
        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(input)
            self.errors.ensureCursorVisible()
        
        self.process.write(input)
        
        self.passwordCheckBox.setChecked(False)
        self.input.clear()
    
    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()
    
    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(HgConflictsListDialog, self).keyPressEvent(evt)
    
    @pyqtSlot(QTreeWidgetItem, int)
    def on_conflictsList_itemDoubleClicked(self, item, column):
        """
        Private slot to open the double clicked entry.
        
        @param item reference to the double clicked item (QTreeWidgetItem)
        @param column column that was double clicked (integer)
        """
        self.on_editButton_clicked()
    
    @pyqtSlot()
    def on_conflictsList_itemSelectionChanged(self):
        """
        Private slot to handle a change of selected conflict entries.
        """
        selectedCount = len(self.conflictsList.selectedItems())
        unresolved = resolved = 0
        for itm in self.conflictsList.selectedItems():
            status = itm.data(0, self.StatusRole)
            if status == "U":
                unresolved += 1
            elif status == "R":
                resolved += 1
        
        self.resolvedButton.setEnabled(unresolved > 0)
        self.unresolvedButton.setEnabled(resolved > 0)
        self.reMergeButton.setEnabled(unresolved > 0)
        self.editButton.setEnabled(
            selectedCount == 1 and
            Utilities.MimeTypes.isTextFile(
                self.conflictsList.selectedItems()[0].data(
                    0, self.FilenameRole)))
    
    @pyqtSlot()
    def on_resolvedButton_clicked(self):
        """
        Private slot to mark the selected entries as resolved.
        """
        names = [
            itm.data(0, self.FilenameRole)
            for itm in self.conflictsList.selectedItems()
            if itm.data(0, self.StatusRole) == "U"
        ]
        if names:
            self.vcs.hgResolved(names)
            self.on_refreshButton_clicked()
    
    @pyqtSlot()
    def on_unresolvedButton_clicked(self):
        """
        Private slot to mark the selected entries as unresolved.
        """
        names = [
            itm.data(0, self.FilenameRole)
            for itm in self.conflictsList.selectedItems()
            if itm.data(0, self.StatusRole) == "R"
        ]
        if names:
            self.vcs.hgResolved(names, unresolve=True)
            self.on_refreshButton_clicked()
    
    @pyqtSlot()
    def on_reMergeButton_clicked(self):
        """
        Private slot to re-merge the selected entries.
        """
        names = [
            itm.data(0, self.FilenameRole)
            for itm in self.conflictsList.selectedItems()
            if itm.data(0, self.StatusRole) == "U"
        ]
        if names:
            self.vcs.hgReMerge(names)
    
    @pyqtSlot()
    def on_editButton_clicked(self):
        """
        Private slot to open the selected file in an editor.
        """
        itm = self.conflictsList.selectedItems()[0]
        filename = itm.data(0, self.FilenameRole)
        if Utilities.MimeTypes.isTextFile(filename):
            e5App().getObject("ViewManager").getEditor(filename)
class VirtualenvExecDialog(QDialog, Ui_VirtualenvExecDialog):
    """
    Class implementing the virtualenv execution dialog.
    
    This class starts a QProcess and displays a dialog that
    shows the output of the virtualenv or pyvenv process.
    """
    def __init__(self,
                 pyvenv,
                 targetDir,
                 openTarget,
                 createLog,
                 createScript,
                 interpreter,
                 parent=None):
        """
        Constructor
        
        @param pyvenv flag indicating the use of 'pyvenv' (boolean)
        @param targetDir name of the virtualenv directory (string)
        @param openTarget flag indicating to open the virtualenv directory
            in a file manager (boolean)
        @param createLog flag indicating to create a log file of the
            creation process (boolean)
        @param createScript flag indicating to create a script to recreate
            the virtual environment (boolean)
        @param interpreter name of the python interpreter to use (string)
        @param parent reference to the parent widget (QWidget)
        """
        super(VirtualenvExecDialog, self).__init__(parent)
        self.setupUi(self)

        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)

        self.__pyvenv = pyvenv
        self.__targetDir = targetDir
        self.__openTarget = openTarget
        self.__createLog = createLog
        self.__createScript = createScript

        self.process = None
        self.__cmd = ""

        if pyvenv:
            self.__calls = []
            if interpreter:
                self.__calls.append((interpreter, ["-m", "venv"]))
            self.__calls.extend([
                (sys.executable.replace("w.exe", ".exe"), ["-m", "venv"]),
                ("python3", ["-m", "venv"]),
                ("python", ["-m", "venv"]),
            ])
        else:
            self.__calls = [
                (sys.executable.replace("w.exe", ".exe"), ["-m",
                                                           "virtualenv"]),
                ("virtualenv", []),
            ]
        self.__callIndex = 0
        self.__callArgs = []

    def start(self, arguments):
        """
        Public slot to start the virtualenv command.
        
        @param arguments commandline arguments for virtualenv/pyvenv program
            (list of strings)
        """
        if self.__callIndex == 0:
            # first attempt, add a given python interpreter and do
            # some other setup
            self.errorGroup.hide()
            self.contents.clear()
            self.errors.clear()

            self.process = QProcess()
            self.process.readyReadStandardOutput.connect(self.__readStdout)
            self.process.readyReadStandardError.connect(self.__readStderr)
            self.process.finished.connect(self.__finish)

            if not self.__pyvenv:
                for arg in arguments:
                    if arg.startswith("--python="):
                        prog = arg.replace("--python=", "")
                        self.__calls.insert(0, (prog, ["-m", "virtualenv"]))
                        break
            self.__callArgs = arguments

        prog, args = self.__calls[self.__callIndex]
        args.extend(self.__callArgs)
        self.__cmd = "{0} {1}".format(prog, " ".join(args))
        self.__logOutput(self.tr("Executing: {0}\n").format(self.__cmd))
        self.process.start(prog, args)
        procStarted = self.process.waitForStarted(5000)
        if not procStarted:
            self.__logOutput(self.tr("Failed\n\n"))
            self.__nextAttempt()

    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.accept()
        elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
            self.__finish()

    def __finish(self, exitCode, exitStatus, giveUp=False):
        """
        Private slot called when the process finished.
        
        It is called when the process finished or
        the user pressed the button.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        @keyparam giveUp flag indicating to not start another attempt (boolean)
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)

        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)

        if not giveUp:
            if exitCode != 0:
                self.__logOutput(self.tr("Failed\n\n"))
                if len(self.errors.toPlainText().splitlines()) == 1:
                    self.errors.clear()
                    self.errorGroup.hide()
                    self.__nextAttempt()
                    return

            self.process = None

            if self.__pyvenv:
                self.__logOutput(self.tr('\npyvenv finished.\n'))
            else:
                self.__logOutput(self.tr('\nvirtualenv finished.\n'))

            if os.path.exists(self.__targetDir):
                if self.__createScript:
                    self.__writeScriptFile()

                if self.__createLog:
                    self.__writeLogFile()

                if self.__openTarget:
                    QDesktopServices.openUrl(
                        QUrl.fromLocalFile(self.__targetDir))

    def __nextAttempt(self):
        """
        Private method to start another attempt.
        """
        self.__callIndex += 1
        if self.__callIndex < len(self.__calls):
            self.start(self.__callArgs)
        else:
            if self.__pyvenv:
                self.__logError(
                    self.tr('No suitable pyvenv program could be'
                            ' started.\n'))
            else:
                self.__logError(
                    self.tr('No suitable virtualenv program could be'
                            ' started.\n'))
            self.__cmd = ""
            self.__finish(0, 0, giveUp=True)

    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        self.process.setReadChannel(QProcess.StandardOutput)

        while self.process.canReadLine():
            s = str(self.process.readLine(),
                    Preferences.getSystem("IOEncoding"), 'replace')
            self.__logOutput(s)

    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        self.process.setReadChannel(QProcess.StandardError)

        while self.process.canReadLine():
            s = str(self.process.readLine(),
                    Preferences.getSystem("IOEncoding"), 'replace')
            self.__logError(s)

    def __logOutput(self, s):
        """
        Private method to log some output.
        
        @param s output sstring to log (string)
        """
        self.contents.insertPlainText(s)
        self.contents.ensureCursorVisible()

    def __logError(self, s):
        """
        Private method to log an error.
        
        @param s error string to log (string)
        """
        self.errorGroup.show()
        self.errors.insertPlainText(s)
        self.errors.ensureCursorVisible()

    def __writeLogFile(self):
        """
        Private method to write a log file to the virtualenv directory.
        """
        outtxt = self.contents.toPlainText()
        if self.__pyvenv:
            logFile = os.path.join(self.__targetDir, "pyvenv.log")
        else:
            logFile = os.path.join(self.__targetDir, "virtualenv.log")
        self.__logOutput(
            self.tr("\nWriting log file '{0}'.\n").format(logFile))

        try:
            f = open(logFile, "w", encoding="utf-8")
            f.write(self.tr("Output:\n"))
            f.write(outtxt)
            errtxt = self.errors.toPlainText()
            if errtxt:
                f.write("\n")
                f.write(self.tr("Errors:\n"))
                f.write(errtxt)
            f.close()
        except (IOError, OSError) as err:
            self.__logError(
                self.tr("""The logfile '{0}' could not be written.\n"""
                        """Reason: {1}\n""").format(logFile, str(err)))
        self.__logOutput(self.tr("Done.\n"))

    def __writeScriptFile(self):
        """
        Private method to write a script file to the virtualenv directory.
        """
        if self.__pyvenv:
            basename = "create_pyvenv"
        else:
            basename = "create_virtualenv"
        if isWindowsPlatform():
            script = os.path.join(self.__targetDir, basename + ".bat")
            txt = self.__cmd
        else:
            script = os.path.join(self.__targetDir, basename + ".sh")
            txt = "#!/usr/bin/env sh\n\n" + self.__cmd

        self.__logOutput(
            self.tr("\nWriting script file '{0}'.\n").format(script))

        try:
            f = open(script, "w", encoding="utf-8")
            f.write(txt)
            f.close()
        except (IOError, OSError) as err:
            self.__logError(
                self.tr("""The script file '{0}' could not be written.\n"""
                        """Reason: {1}\n""").format(script, str(err)))
        self.__logOutput(self.tr("Done.\n"))
class JobRunner(QObject, MooseWidget):
    """
    Actually runs the process. It will read the output and
    translate any terminal color codes into html.
    It will also attempt to parse the output to check to
    see if we are at a new time step and emit the
    timestep_updated signal.
    Signals:
        started: Emitted when we start running.
        finished: Emitted when we are finished. Arguments are exit code and status message.
        outputAdded: Emitted when there is new output.
        timeStepUpdated: A new time step has started
        error: Emitted when an error is encountered. Arguments are QProcess code and error description
    """
    started = pyqtSignal()
    finished = pyqtSignal(int, str)
    outputAdded = pyqtSignal(str)
    timeStepUpdated = pyqtSignal(int)
    error = pyqtSignal(int, str)

    def __init__(self, **kwds):
        super(JobRunner, self).__init__(**kwds)

        self.process = QProcess(self)
        self.process.setProcessChannelMode(QProcess.MergedChannels)
        self.process.readyReadStandardOutput.connect(self._readOutput)
        self.process.finished.connect(self._jobFinished)
        self.process.started.connect(self.started)
        self.process.error.connect(self._error)
        self._error_map = { QProcess.FailedToStart: "Failed to start",
                QProcess.Crashed: "Crashed",
                QProcess.Timedout: "Timedout",
                QProcess.WriteError: "Write error",
                QProcess.ReadError: "Read error",
                QProcess.UnknownError: "Unknown error",
                }

        self.killed = False
        self.setup()

    def run(self, cmd, args):
        """
        Start the command.

        Arguments:
            cmd: The command to run
            args: A list of string arguments
        """
        self.killed = False
        self._sendMessage("Running command: %s %s" % (cmd, ' '.join(args)))
        self._sendMessage("Working directory: %s" % os.getcwd())
        self.process.start(cmd, args)
        self.process.waitForStarted()

    def _sendMessage(self, msg):
        mooseutils.mooseMessage(msg, color="MAGENTA")
        self.outputAdded.emit('<span style="color:magenta;">%s</span>' % msg)

    @pyqtSlot(QProcess.ProcessError)
    def _error(self, err):
        """
        Slot called when the QProcess encounters an error.
        Inputs:
            err: One of the QProcess.ProcessError enums
        """
        if not self.killed:
            msg = self._error_map.get(err, "Unknown error")
            self.error.emit(int(err), msg)
            mooseutils.mooseMessage(msg, color="RED")
            self.outputAdded.emit(msg)

    @pyqtSlot(int, QProcess.ExitStatus)
    def _jobFinished(self, code, status):
        """
        Slot called when the QProcess is finished.
        Inputs:
            code: Exit code of the process.
            status: QProcess.ExitStatus
        """
        exit_status = "Finished"
        if status != QProcess.NormalExit:
            if self.killed:
                exit_status = "Killed by user"
            else:
                exit_status = "Crashed"
        self.finished.emit(code, exit_status)
        self._sendMessage("%s: Exit code: %s" % (exit_status, code))

    def kill(self):
        """
        Kills the QProcess
        """
        self.killed = True
        mooseutils.mooseMessage("Killing")
        self.process.kill()
        self.process.waitForFinished()

    @pyqtSlot()
    def _readOutput(self):
        """
        Slot called when the QProcess produces output.
        """
        lines = []
        while self.process.canReadLine():
            tmp = self.process.readLine().data().decode("utf-8").rstrip()
            lines.append(TerminalUtils.terminalOutputToHtml(tmp))
            match = re.search(r'Time\sStep\s*([0-9]{1,})', tmp)
            if match:
                ts = int(match.group(1))
                self.timeStepUpdated.emit(ts)

        output = '<pre style="display: inline; margin: 0;">%s</pre>' % '\n'.join(lines)
        self.outputAdded.emit(output)

    def isRunning(self):
        return self.process.state() == QProcess.Running
Beispiel #44
0
class SvnRepoBrowserDialog(QDialog, Ui_SvnRepoBrowserDialog):
    """
    Class implementing the subversion repository browser dialog.
    """
    def __init__(self, vcs, mode="browse", parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param mode mode of the dialog (string, "browse" or "select")
        @param parent parent widget (QWidget)
        """
        super(SvnRepoBrowserDialog, self).__init__(parent)
        self.setupUi(self)
        self.setWindowFlags(Qt.Window)
        
        self.repoTree.headerItem().setText(self.repoTree.columnCount(), "")
        self.repoTree.header().setSortIndicator(0, Qt.AscendingOrder)
        
        self.vcs = vcs
        self.mode = mode
        
        self.process = QProcess()
        self.process.finished.connect(self.__procFinished)
        self.process.readyReadStandardOutput.connect(self.__readStdout)
        self.process.readyReadStandardError.connect(self.__readStderr)
        
        if self.mode == "select":
            self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(False)
            self.buttonBox.button(QDialogButtonBox.Close).hide()
        else:
            self.buttonBox.button(QDialogButtonBox.Ok).hide()
            self.buttonBox.button(QDialogButtonBox.Cancel).hide()
        
        self.__dirIcon = UI.PixmapCache.getIcon("dirClosed.png")
        self.__fileIcon = UI.PixmapCache.getIcon("fileMisc.png")
        
        self.__urlRole = Qt.UserRole
        self.__ignoreExpand = False
        self.intercept = False
        
        self.__rx_dir = QRegExp(
            r"""\s*([0-9]+)\s+(\w+)\s+"""
            r"""((?:\w+\s+\d+|[0-9.]+\s+\w+)\s+[0-9:]+)\s+(.+)\s*""")
        self.__rx_file = QRegExp(
            r"""\s*([0-9]+)\s+(\w+)\s+([0-9]+)\s"""
            r"""((?:\w+\s+\d+|[0-9.]+\s+\w+)\s+[0-9:]+)\s+(.+)\s*""")
    
    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        e.accept()
        
    def __resort(self):
        """
        Private method to resort the tree.
        """
        self.repoTree.sortItems(
            self.repoTree.sortColumn(),
            self.repoTree.header().sortIndicatorOrder())
    
    def __resizeColumns(self):
        """
        Private method to resize the tree columns.
        """
        self.repoTree.header().resizeSections(QHeaderView.ResizeToContents)
        self.repoTree.header().setStretchLastSection(True)
    
    def __generateItem(self, repopath, revision, author, size, date,
                       nodekind, url):
        """
        Private method to generate a tree item in the repository tree.
        
        @param repopath path of the item (string)
        @param revision revision info (string)
        @param author author info (string)
        @param size size info (string)
        @param date date info (string)
        @param nodekind node kind info (string, "dir" or "file")
        @param url url of the entry (string)
        @return reference to the generated item (QTreeWidgetItem)
        """
        path = repopath
        
        if revision == "":
            rev = ""
        else:
            rev = int(revision)
        if size == "":
            sz = ""
        else:
            sz = int(size)
        
        itm = QTreeWidgetItem(self.parentItem)
        itm.setData(0, Qt.DisplayRole, path)
        itm.setData(1, Qt.DisplayRole, rev)
        itm.setData(2, Qt.DisplayRole, author)
        itm.setData(3, Qt.DisplayRole, sz)
        itm.setData(4, Qt.DisplayRole, date)
        
        if nodekind == "dir":
            itm.setIcon(0, self.__dirIcon)
            itm.setChildIndicatorPolicy(QTreeWidgetItem.ShowIndicator)
        elif nodekind == "file":
            itm.setIcon(0, self.__fileIcon)
        
        itm.setData(0, self.__urlRole, url)
        
        itm.setTextAlignment(0, Qt.AlignLeft)
        itm.setTextAlignment(1, Qt.AlignRight)
        itm.setTextAlignment(2, Qt.AlignLeft)
        itm.setTextAlignment(3, Qt.AlignRight)
        itm.setTextAlignment(4, Qt.AlignLeft)
        
        return itm
    
    def __repoRoot(self, url):
        """
        Private method to get the repository root using the svn info command.
        
        @param url the repository URL to browser (string)
        @return repository root (string)
        """
        ioEncoding = Preferences.getSystem("IOEncoding")
        repoRoot = None
        
        process = QProcess()
        
        args = []
        args.append('info')
        self.vcs.addArguments(args, self.vcs.options['global'])
        args.append('--xml')
        args.append(url)
        
        process.start('svn', args)
        procStarted = process.waitForStarted(5000)
        if procStarted:
            finished = process.waitForFinished(30000)
            if finished:
                if process.exitCode() == 0:
                    output = str(process.readAllStandardOutput(), ioEncoding,
                                 'replace')
                    for line in output.splitlines():
                        line = line.strip()
                        if line.startswith('<root>'):
                            repoRoot = line.replace('<root>', '')\
                                .replace('</root>', '')
                            break
                else:
                    error = str(process.readAllStandardError(),
                                Preferences.getSystem("IOEncoding"),
                                'replace')
                    self.errors.insertPlainText(error)
                    self.errors.ensureCursorVisible()
        else:
            QApplication.restoreOverrideCursor()
            E5MessageBox.critical(
                self,
                self.tr('Process Generation Error'),
                self.tr(
                    'The process {0} could not be started. '
                    'Ensure, that it is in the search path.'
                ).format('svn'))
        return repoRoot
    
    def __listRepo(self, url, parent=None):
        """
        Private method to perform the svn list command.
        
        @param url the repository URL to browse (string)
        @param parent reference to the item, the data should be appended to
            (QTreeWidget or QTreeWidgetItem)
        """
        self.errorGroup.hide()
        
        QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
        QApplication.processEvents()
        
        self.repoUrl = url
        
        if parent is None:
            self.parentItem = self.repoTree
        else:
            self.parentItem = parent
        
        if self.parentItem == self.repoTree:
            repoRoot = self.__repoRoot(url)
            if repoRoot is None:
                self.__finish()
                return
            self.__ignoreExpand = True
            itm = self.__generateItem(
                repoRoot, "", "", "", "", "dir", repoRoot)
            itm.setExpanded(True)
            self.parentItem = itm
            urlPart = repoRoot
            for element in url.replace(repoRoot, "").split("/"):
                if element:
                    urlPart = "{0}/{1}".format(urlPart, element)
                    itm = self.__generateItem(
                        element, "", "", "", "", "dir", urlPart)
                    itm.setExpanded(True)
                    self.parentItem = itm
            itm.setExpanded(False)
            self.__ignoreExpand = False
            self.__finish()
            return
        
        self.intercept = False
        
        self.process.kill()
        
        args = []
        args.append('list')
        self.vcs.addArguments(args, self.vcs.options['global'])
        if '--verbose' not in self.vcs.options['global']:
            args.append('--verbose')
        args.append(url)
        
        self.process.start('svn', args)
        procStarted = self.process.waitForStarted(5000)
        if not procStarted:
            self.__finish()
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            E5MessageBox.critical(
                self,
                self.tr('Process Generation Error'),
                self.tr(
                    'The process {0} could not be started. '
                    'Ensure, that it is in the search path.'
                ).format('svn'))
        else:
            self.inputGroup.setEnabled(True)
            self.inputGroup.show()
    
    def __normalizeUrl(self, url):
        """
        Private method to normalite the url.
        
        @param url the url to normalize (string)
        @return normalized URL (string)
        """
        if url.endswith("/"):
            return url[:-1]
        return url
    
    def start(self, url):
        """
        Public slot to start the svn info command.
        
        @param url the repository URL to browser (string)
        """
        self.repoTree.clear()
        
        self.url = ""
        
        url = self.__normalizeUrl(url)
        if self.urlCombo.findText(url) == -1:
            self.urlCombo.addItem(url)
    
    @pyqtSlot(str)
    def on_urlCombo_currentIndexChanged(self, text):
        """
        Private slot called, when a new repository URL is entered or selected.
        
        @param text the text of the current item (string)
        """
        url = self.__normalizeUrl(text)
        if url != self.url:
            self.url = url
            self.repoTree.clear()
            self.__listRepo(url)
    
    @pyqtSlot(QTreeWidgetItem)
    def on_repoTree_itemExpanded(self, item):
        """
        Private slot called when an item is expanded.
        
        @param item reference to the item to be expanded (QTreeWidgetItem)
        """
        if not self.__ignoreExpand:
            url = item.data(0, self.__urlRole)
            self.__listRepo(url, item)
    
    @pyqtSlot(QTreeWidgetItem)
    def on_repoTree_itemCollapsed(self, item):
        """
        Private slot called when an item is collapsed.
        
        @param item reference to the item to be collapsed (QTreeWidgetItem)
        """
        for child in item.takeChildren():
            del child
    
    @pyqtSlot()
    def on_repoTree_itemSelectionChanged(self):
        """
        Private slot called when the selection changes.
        """
        if self.mode == "select":
            self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(True)
    
    def accept(self):
        """
        Public slot called when the dialog is accepted.
        """
        if self.focusWidget() == self.urlCombo:
            return
        
        super(SvnRepoBrowserDialog, self).accept()
    
    def getSelectedUrl(self):
        """
        Public method to retrieve the selected repository URL.
        
        @return the selected repository URL (string)
        """
        items = self.repoTree.selectedItems()
        if len(items) == 1:
            return items[0].data(0, self.__urlRole)
        else:
            return ""
    
    def __finish(self):
        """
        Private slot called when the process finished or the user pressed the
        button.
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()
        
        self.__resizeColumns()
        self.__resort()
        QApplication.restoreOverrideCursor()
    
    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__finish()
    
    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        if self.process is not None:
            self.process.setReadChannel(QProcess.StandardOutput)
            
            while self.process.canReadLine():
                s = str(self.process.readLine(),
                        Preferences.getSystem("IOEncoding"),
                        'replace')
                if self.__rx_dir.exactMatch(s):
                    revision = self.__rx_dir.cap(1)
                    author = self.__rx_dir.cap(2)
                    date = self.__rx_dir.cap(3)
                    name = self.__rx_dir.cap(4).strip()
                    if name.endswith("/"):
                        name = name[:-1]
                    size = ""
                    nodekind = "dir"
                    if name == ".":
                        continue
                elif self.__rx_file.exactMatch(s):
                    revision = self.__rx_file.cap(1)
                    author = self.__rx_file.cap(2)
                    size = self.__rx_file.cap(3)
                    date = self.__rx_file.cap(4)
                    name = self.__rx_file.cap(5).strip()
                    nodekind = "file"
                else:
                    continue
                url = "{0}/{1}".format(self.repoUrl, name)
                self.__generateItem(
                    name, revision, author, size, date, nodekind, url)
   
    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            s = str(self.process.readAllStandardError(),
                    Preferences.getSystem("IOEncoding"),
                    'replace')
            self.errors.insertPlainText(s)
            self.errors.ensureCursorVisible()
            self.errorGroup.show()
    
    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)
    
    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the subversion process.
        """
        input = self.input.text()
        input += os.linesep
        
        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(input)
            self.errors.ensureCursorVisible()
        
        self.process.write(input)
        
        self.passwordCheckBox.setChecked(False)
        self.input.clear()
    
    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()
    
    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(SvnRepoBrowserDialog, self).keyPressEvent(evt)
Beispiel #45
0
class HgDiffDialog(QWidget, Ui_HgDiffDialog):
    """
    Class implementing a dialog to show the output of the hg diff command
    process.
    """
    def __init__(self, vcs, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param parent parent widget (QWidget)
        """
        super(HgDiffDialog, self).__init__(parent)
        self.setupUi(self)

        self.buttonBox.button(QDialogButtonBox.Save).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)

        self.process = QProcess()
        self.vcs = vcs
        self.__hgClient = self.vcs.getClient()

        font = Preferences.getEditorOtherFonts("MonospacedFont")
        self.contents.setFontFamily(font.family())
        self.contents.setFontPointSize(font.pointSize())

        self.cNormalFormat = self.contents.currentCharFormat()
        self.cAddedFormat = self.contents.currentCharFormat()
        self.cAddedFormat.setBackground(QBrush(QColor(190, 237, 190)))
        self.cRemovedFormat = self.contents.currentCharFormat()
        self.cRemovedFormat.setBackground(QBrush(QColor(237, 190, 190)))
        self.cLineNoFormat = self.contents.currentCharFormat()
        self.cLineNoFormat.setBackground(QBrush(QColor(255, 220, 168)))

        self.process.finished.connect(self.__procFinished)
        self.process.readyReadStandardOutput.connect(self.__readStdout)
        self.process.readyReadStandardError.connect(self.__readStderr)

    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.__hgClient:
            if self.__hgClient.isExecuting():
                self.__hgClient.cancel()
        else:
            if self.process is not None and \
               self.process.state() != QProcess.NotRunning:
                self.process.terminate()
                QTimer.singleShot(2000, self.process.kill)
                self.process.waitForFinished(3000)

        e.accept()

    def __getVersionArg(self, version):
        """
        Private method to get a hg revision argument for the given revision.
        
        @param version revision (integer or string)
        @return version argument (string)
        """
        if version == "WORKING":
            return None
        else:
            return str(version)

    def start(self, fn, versions=None, bundle=None, qdiff=False):
        """
        Public slot to start the hg diff command.
        
        @param fn filename to be diffed (string)
        @param versions list of versions to be diffed (list of up to 2 strings
            or None)
        @param bundle name of a bundle file (string)
        @param qdiff flag indicating qdiff command shall be used (boolean)
        """
        self.errorGroup.hide()
        self.inputGroup.show()
        self.intercept = False
        self.filename = fn

        self.contents.clear()
        self.paras = 0

        self.filesCombo.clear()

        if qdiff:
            args = self.vcs.initCommand("qdiff")
            self.setWindowTitle(self.tr("Patch Contents"))
        else:
            args = self.vcs.initCommand("diff")

            if self.vcs.hasSubrepositories():
                args.append("--subrepos")

            if bundle:
                args.append('--repository')
                args.append(bundle)
            elif self.vcs.bundleFile and os.path.exists(self.vcs.bundleFile):
                args.append('--repository')
                args.append(self.vcs.bundleFile)

            if versions is not None:
                self.raise_()
                self.activateWindow()

                rev1 = self.__getVersionArg(versions[0])
                rev2 = None
                if len(versions) == 2:
                    rev2 = self.__getVersionArg(versions[1])

                if rev1 is not None or rev2 is not None:
                    args.append('-r')
                    if rev1 is not None and rev2 is not None:
                        args.append('{0}:{1}'.format(rev1, rev2))
                    elif rev2 is None:
                        args.append(rev1)
                    elif rev1 is None:
                        args.append(':{0}'.format(rev2))

        if isinstance(fn, list):
            dname, fnames = self.vcs.splitPathList(fn)
            self.vcs.addArguments(args, fn)
        else:
            dname, fname = self.vcs.splitPath(fn)
            args.append(fn)

        self.__oldFile = ""
        self.__oldFileLine = -1
        self.__fileSeparators = []

        QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
        if self.__hgClient:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()

            out, err = self.__hgClient.runcommand(args)

            if err:
                self.__showError(err)
            if out:
                for line in out.splitlines(True):
                    self.__processOutputLine(line)
                    if self.__hgClient.wasCanceled():
                        break

            self.__finish()
        else:
            # find the root of the repo
            repodir = dname
            while not os.path.isdir(os.path.join(repodir, self.vcs.adminDir)):
                repodir = os.path.dirname(repodir)
                if os.path.splitdrive(repodir)[1] == os.sep:
                    return

            self.process.kill()

            self.process.setWorkingDirectory(repodir)

            self.process.start('hg', args)
            procStarted = self.process.waitForStarted(5000)
            if not procStarted:
                QApplication.restoreOverrideCursor()
                self.inputGroup.setEnabled(False)
                self.inputGroup.hide()
                E5MessageBox.critical(
                    self, self.tr('Process Generation Error'),
                    self.tr(
                        'The process {0} could not be started. '
                        'Ensure, that it is in the search path.').format('hg'))

    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__finish()

    def __finish(self):
        """
        Private slot called when the process finished or the user pressed
        the button.
        """
        QApplication.restoreOverrideCursor()
        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()

        if self.paras == 0:
            self.contents.setCurrentCharFormat(self.cNormalFormat)
            self.contents.setPlainText(self.tr('There is no difference.'))

        self.buttonBox.button(QDialogButtonBox.Save).setEnabled(self.paras > 0)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        self.buttonBox.button(QDialogButtonBox.Close).setFocus(
            Qt.OtherFocusReason)

        tc = self.contents.textCursor()
        tc.movePosition(QTextCursor.Start)
        self.contents.setTextCursor(tc)
        self.contents.ensureCursorVisible()

        self.filesCombo.addItem(self.tr("<Start>"), 0)
        self.filesCombo.addItem(self.tr("<End>"), -1)
        for oldFile, newFile, pos in sorted(self.__fileSeparators):
            if oldFile != newFile:
                self.filesCombo.addItem("{0}\n{1}".format(oldFile, newFile),
                                        pos)
            else:
                self.filesCombo.addItem(oldFile, pos)

    def __appendText(self, txt, format):
        """
        Private method to append text to the end of the contents pane.
        
        @param txt text to insert (string)
        @param format text format to be used (QTextCharFormat)
        """
        tc = self.contents.textCursor()
        tc.movePosition(QTextCursor.End)
        self.contents.setTextCursor(tc)
        self.contents.setCurrentCharFormat(format)
        self.contents.insertPlainText(txt)

    def __extractFileName(self, line):
        """
        Private method to extract the file name out of a file separator line.
        
        @param line line to be processed (string)
        @return extracted file name (string)
        """
        f = line.split(None, 1)[1]
        f = f.rsplit(None, 6)[0]
        f = f.split("/", 1)[1]
        return f

    def __processFileLine(self, line):
        """
        Private slot to process a line giving the old/new file.
        
        @param line line to be processed (string)
        """
        if line.startswith('---'):
            self.__oldFileLine = self.paras
            self.__oldFile = self.__extractFileName(line)
        else:
            self.__fileSeparators.append(
                (self.__oldFile, self.__extractFileName(line),
                 self.__oldFileLine))

    def __processOutputLine(self, line):
        """
        Private method to process the lines of output.
        
        @param line output line to be processed (string)
        """
        if line.startswith("--- ") or \
           line.startswith("+++ "):
            self.__processFileLine(line)

        if line.startswith('+'):
            format = self.cAddedFormat
        elif line.startswith('-'):
            format = self.cRemovedFormat
        elif line.startswith('@@'):
            format = self.cLineNoFormat
        else:
            format = self.cNormalFormat
        self.__appendText(line, format)
        self.paras += 1

    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        self.process.setReadChannel(QProcess.StandardOutput)

        while self.process.canReadLine():
            line = str(self.process.readLine(), self.vcs.getEncoding(),
                       'replace')
            self.__processOutputLine(line)

    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            s = str(self.process.readAllStandardError(),
                    self.vcs.getEncoding(), 'replace')
            self.__showError(s)

    def __showError(self, out):
        """
        Private slot to show some error.
        
        @param out error to be shown (string)
        """
        self.errorGroup.show()
        self.errors.insertPlainText(out)
        self.errors.ensureCursorVisible()

    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Save):
            self.on_saveButton_clicked()

    @pyqtSlot(int)
    def on_filesCombo_activated(self, index):
        """
        Private slot to handle the selection of a file.
        
        @param index activated row (integer)
        """
        para = self.filesCombo.itemData(index)

        if para == 0:
            tc = self.contents.textCursor()
            tc.movePosition(QTextCursor.Start)
            self.contents.setTextCursor(tc)
            self.contents.ensureCursorVisible()
        elif para == -1:
            tc = self.contents.textCursor()
            tc.movePosition(QTextCursor.End)
            self.contents.setTextCursor(tc)
            self.contents.ensureCursorVisible()
        else:
            # step 1: move cursor to end
            tc = self.contents.textCursor()
            tc.movePosition(QTextCursor.End)
            self.contents.setTextCursor(tc)
            self.contents.ensureCursorVisible()

            # step 2: move cursor to desired line
            tc = self.contents.textCursor()
            delta = tc.blockNumber() - para
            tc.movePosition(QTextCursor.PreviousBlock, QTextCursor.MoveAnchor,
                            delta)
            self.contents.setTextCursor(tc)
            self.contents.ensureCursorVisible()

    @pyqtSlot()
    def on_saveButton_clicked(self):
        """
        Private slot to handle the Save button press.
        
        It saves the diff shown in the dialog to a file in the local
        filesystem.
        """
        if isinstance(self.filename, list):
            if len(self.filename) > 1:
                fname = self.vcs.splitPathList(self.filename)[0]
            else:
                dname, fname = self.vcs.splitPath(self.filename[0])
                if fname != '.':
                    fname = "{0}.diff".format(self.filename[0])
                else:
                    fname = dname
        else:
            fname = self.vcs.splitPath(self.filename)[0]

        fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
            self, self.tr("Save Diff"), fname, self.tr("Patch Files (*.diff)"),
            None, E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))

        if not fname:
            return  # user aborted

        ext = QFileInfo(fname).suffix()
        if not ext:
            ex = selectedFilter.split("(*")[1].split(")")[0]
            if ex:
                fname += ex
        if QFileInfo(fname).exists():
            res = E5MessageBox.yesNo(
                self,
                self.tr("Save Diff"),
                self.tr("<p>The patch file <b>{0}</b> already exists."
                        " Overwrite it?</p>").format(fname),
                icon=E5MessageBox.Warning)
            if not res:
                return
        fname = Utilities.toNativeSeparators(fname)

        eol = e5App().getObject("Project").getEolString()
        try:
            f = open(fname, "w", encoding="utf-8", newline="")
            f.write(eol.join(self.contents.toPlainText().splitlines()))
            f.close()
        except IOError as why:
            E5MessageBox.critical(
                self, self.tr('Save Diff'),
                self.tr('<p>The patch file <b>{0}</b> could not be saved.'
                        '<br>Reason: {1}</p>').format(fname, str(why)))

    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)

    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the subversion process.
        """
        input = self.input.text()
        input += os.linesep

        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(input)
            self.errors.ensureCursorVisible()

        self.process.write(input)

        self.passwordCheckBox.setChecked(False)
        self.input.clear()

    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()

    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(HgDiffDialog, self).keyPressEvent(evt)
Beispiel #46
0
class SvnDiffDialog(QWidget, Ui_SvnDiffDialog):
    """
    Class implementing a dialog to show the output of the svn diff command
    process.
    """
    def __init__(self, vcs, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param parent parent widget (QWidget)
        """
        super(SvnDiffDialog, self).__init__(parent)
        self.setupUi(self)
        
        self.refreshButton = self.buttonBox.addButton(
            self.tr("Refresh"), QDialogButtonBox.ActionRole)
        self.refreshButton.setToolTip(
            self.tr("Press to refresh the display"))
        self.refreshButton.setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Save).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        
        self.searchWidget.attachTextEdit(self.contents)
        
        self.process = QProcess()
        self.vcs = vcs
        
        font = Preferences.getEditorOtherFonts("MonospacedFont")
        self.contents.setFontFamily(font.family())
        self.contents.setFontPointSize(font.pointSize())
        
        self.highlighter = SvnDiffHighlighter(self.contents.document())
        
        self.process.finished.connect(self.__procFinished)
        self.process.readyReadStandardOutput.connect(self.__readStdout)
        self.process.readyReadStandardError.connect(self.__readStderr)
        
    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if (
            self.process is not None and
            self.process.state() != QProcess.NotRunning
        ):
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        e.accept()
        
    def __getVersionArg(self, version):
        """
        Private method to get a svn revision argument for the given revision.
        
        @param version revision (integer or string)
        @return version argument (string)
        """
        if version == "WORKING":
            return None
        else:
            return str(version)
        
    def start(self, fn, versions=None, urls=None, summary=False,
              refreshable=False):
        """
        Public slot to start the svn diff command.
        
        @param fn filename to be diffed (string)
        @param versions list of versions to be diffed (list of up to 2 strings
            or None)
        @keyparam urls list of repository URLs (list of 2 strings)
        @keyparam summary flag indicating a summarizing diff
            (only valid for URL diffs) (boolean)
        @keyparam refreshable flag indicating a refreshable diff (boolean)
        """
        self.refreshButton.setVisible(refreshable)
        
        self.errorGroup.hide()
        self.inputGroup.show()
        self.inputGroup.setEnabled(True)
        self.intercept = False
        self.filename = fn
        
        self.process.kill()
        
        self.contents.clear()
        self.highlighter.regenerateRules()
        self.paras = 0
        
        self.filesCombo.clear()
        
        self.__oldFile = ""
        self.__oldFileLine = -1
        self.__fileSeparators = []
        
        args = []
        args.append('diff')
        self.vcs.addArguments(args, self.vcs.options['global'])
        self.vcs.addArguments(args, self.vcs.options['diff'])
        if '--diff-cmd' in self.vcs.options['diff']:
            self.buttonBox.button(QDialogButtonBox.Save).hide()
        
        if versions is not None:
            self.raise_()
            self.activateWindow()
            
            rev1 = self.__getVersionArg(versions[0])
            rev2 = None
            if len(versions) == 2:
                rev2 = self.__getVersionArg(versions[1])
            
            if rev1 is not None or rev2 is not None:
                args.append('-r')
                if rev1 is not None and rev2 is not None:
                    args.append('{0}:{1}'.format(rev1, rev2))
                elif rev2 is None:
                    args.append(rev1)
                elif rev1 is None:
                    args.append(rev2)
        
        self.summaryPath = None
        if urls is not None:
            if summary:
                args.append("--summarize")
                self.summaryPath = urls[0]
            args.append("--old={0}".format(urls[0]))
            args.append("--new={0}".format(urls[1]))
            if isinstance(fn, list):
                dname, fnames = self.vcs.splitPathList(fn)
            else:
                dname, fname = self.vcs.splitPath(fn)
                fnames = [fname]
            project = e5App().getObject('Project')
            if dname == project.getProjectPath():
                path = ""
            else:
                path = project.getRelativePath(dname)
            if path:
                path += "/"
            for fname in fnames:
                args.append(path + fname)
        else:
            if isinstance(fn, list):
                dname, fnames = self.vcs.splitPathList(fn)
                self.vcs.addArguments(args, fnames)
            else:
                dname, fname = self.vcs.splitPath(fn)
                args.append(fname)
        
        self.process.setWorkingDirectory(dname)
        
        self.process.start('svn', args)
        procStarted = self.process.waitForStarted(5000)
        if not procStarted:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            E5MessageBox.critical(
                self,
                self.tr('Process Generation Error'),
                self.tr(
                    'The process {0} could not be started. '
                    'Ensure, that it is in the search path.'
                ).format('svn'))
        
    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()
        self.refreshButton.setEnabled(True)
        
        if self.paras == 0:
            self.contents.setPlainText(self.tr('There is no difference.'))
            
        self.buttonBox.button(QDialogButtonBox.Save).setEnabled(self.paras > 0)
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        self.buttonBox.button(QDialogButtonBox.Close).setFocus(
            Qt.OtherFocusReason)
        
        tc = self.contents.textCursor()
        tc.movePosition(QTextCursor.Start)
        self.contents.setTextCursor(tc)
        self.contents.ensureCursorVisible()
        
        self.filesCombo.addItem(self.tr("<Start>"), 0)
        self.filesCombo.addItem(self.tr("<End>"), -1)
        for oldFile, newFile, pos in sorted(self.__fileSeparators):
            if oldFile != newFile:
                self.filesCombo.addItem(
                    "{0}\n{1}".format(oldFile, newFile), pos)
            else:
                self.filesCombo.addItem(oldFile, pos)
        
    def __appendText(self, txt):
        """
        Private method to append text to the end of the contents pane.
        
        @param txt text to insert (string)
        """
        tc = self.contents.textCursor()
        tc.movePosition(QTextCursor.End)
        self.contents.setTextCursor(tc)
        self.contents.insertPlainText(txt)
        
    def __extractFileName(self, line):
        """
        Private method to extract the file name out of a file separator line.
        
        @param line line to be processed (string)
        @return extracted file name (string)
        """
        f = line.split(None, 1)[1]
        f = f.rsplit(None, 2)[0]
        return f
    
    def __processFileLine(self, line):
        """
        Private slot to process a line giving the old/new file.
        
        @param line line to be processed (string)
        """
        if line.startswith('---'):
            self.__oldFileLine = self.paras
            self.__oldFile = self.__extractFileName(line)
        else:
            self.__fileSeparators.append(
                (self.__oldFile, self.__extractFileName(line),
                 self.__oldFileLine))
    
    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        self.process.setReadChannel(QProcess.StandardOutput)
        
        while self.process.canReadLine():
            line = str(self.process.readLine(),
                       Preferences.getSystem("IOEncoding"),
                       'replace')
            if self.summaryPath:
                line = line.replace(self.summaryPath + '/', '')
                line = " ".join(line.split())
            if line.startswith("--- ") or line.startswith("+++ "):
                self.__processFileLine(line)
                
            self.__appendText(line)
            self.paras += 1
        
    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            self.errorGroup.show()
            s = str(self.process.readAllStandardError(),
                    Preferences.getSystem("IOEncoding"),
                    'replace')
            self.errors.insertPlainText(s)
            self.errors.ensureCursorVisible()
        
    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Save):
            self.on_saveButton_clicked()
        elif button == self.refreshButton:
            self.on_refreshButton_clicked()
    
    @pyqtSlot(int)
    def on_filesCombo_activated(self, index):
        """
        Private slot to handle the selection of a file.
        
        @param index activated row (integer)
        """
        para = self.filesCombo.itemData(index)
        
        if para == 0:
            tc = self.contents.textCursor()
            tc.movePosition(QTextCursor.Start)
            self.contents.setTextCursor(tc)
            self.contents.ensureCursorVisible()
        elif para == -1:
            tc = self.contents.textCursor()
            tc.movePosition(QTextCursor.End)
            self.contents.setTextCursor(tc)
            self.contents.ensureCursorVisible()
        else:
            # step 1: move cursor to end
            tc = self.contents.textCursor()
            tc.movePosition(QTextCursor.End)
            self.contents.setTextCursor(tc)
            self.contents.ensureCursorVisible()
            
            # step 2: move cursor to desired line
            tc = self.contents.textCursor()
            delta = tc.blockNumber() - para
            tc.movePosition(QTextCursor.PreviousBlock, QTextCursor.MoveAnchor,
                            delta)
            self.contents.setTextCursor(tc)
            self.contents.ensureCursorVisible()
    
    @pyqtSlot()
    def on_saveButton_clicked(self):
        """
        Private slot to handle the Save button press.
        
        It saves the diff shown in the dialog to a file in the local
        filesystem.
        """
        if isinstance(self.filename, list):
            if len(self.filename) > 1:
                fname = self.vcs.splitPathList(self.filename)[0]
            else:
                dname, fname = self.vcs.splitPath(self.filename[0])
                if fname != '.':
                    fname = "{0}.diff".format(self.filename[0])
                else:
                    fname = dname
        else:
            fname = self.vcs.splitPath(self.filename)[0]
        
        fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
            self,
            self.tr("Save Diff"),
            fname,
            self.tr("Patch Files (*.diff)"),
            None,
            E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
        
        if not fname:
            return  # user aborted
        
        ext = QFileInfo(fname).suffix()
        if not ext:
            ex = selectedFilter.split("(*")[1].split(")")[0]
            if ex:
                fname += ex
        if QFileInfo(fname).exists():
            res = E5MessageBox.yesNo(
                self,
                self.tr("Save Diff"),
                self.tr("<p>The patch file <b>{0}</b> already exists."
                        " Overwrite it?</p>").format(fname),
                icon=E5MessageBox.Warning)
            if not res:
                return
        fname = Utilities.toNativeSeparators(fname)
        
        eol = e5App().getObject("Project").getEolString()
        try:
            f = open(fname, "w", encoding="utf-8", newline="")
            f.write(eol.join(self.contents.toPlainText().splitlines()))
            f.close()
        except IOError as why:
            E5MessageBox.critical(
                self, self.tr('Save Diff'),
                self.tr(
                    '<p>The patch file <b>{0}</b> could not be saved.'
                    '<br>Reason: {1}</p>')
                .format(fname, str(why)))
        
    @pyqtSlot()
    def on_refreshButton_clicked(self):
        """
        Private slot to refresh the display.
        """
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        
        self.buttonBox.button(QDialogButtonBox.Save).setEnabled(False)
        self.refreshButton.setEnabled(False)
        
        self.start(self.filename, refreshable=True)
    
    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)
        
    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the subversion process.
        """
        inputTxt = self.input.text()
        inputTxt += os.linesep
        
        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(inputTxt)
            self.errors.ensureCursorVisible()
        
        self.process.write(strToQByteArray(inputTxt))
        
        self.passwordCheckBox.setChecked(False)
        self.input.clear()
        
    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()
        
    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(SvnDiffDialog, self).keyPressEvent(evt)
Beispiel #47
0
class Process(QObject):

    """Abstraction over a running test subprocess process.

    Reads the log from its stdout and parses it.

    Attributes:
        _invalid: A list of lines which could not be parsed.
        _data: A list of parsed lines.
        proc: The QProcess for the underlying process.
        exit_expected: Whether the process is expected to quit.

    Signals:
        ready: Emitted when the server finished starting up.
        new_data: Emitted when a new line was parsed.
    """

    ready = pyqtSignal()
    new_data = pyqtSignal(object)
    KEYS = ['data']

    def __init__(self, parent=None):
        super().__init__(parent)
        self.captured_log = []
        self._invalid = []
        self._data = []
        self.proc = QProcess()
        self.proc.setReadChannel(QProcess.StandardError)
        self.exit_expected = False

    def _log(self, line):
        """Add the given line to the captured log output."""
        # pylint: disable=no-member
        if pytest.config.getoption('--capture') == 'no':
            print(line)
        self.captured_log.append(line)

    def _parse_line(self, line):
        """Parse the given line from the log.

        Return:
            A self.ParseResult member.
        """
        raise NotImplementedError

    def _executable_args(self):
        """Get the executable and arguments to pass to it as a tuple."""
        raise NotImplementedError

    def _get_data(self):
        """Get the parsed data for this test.

        Also waits for 0.5s to make sure any new data is received.

        Subprocesses are expected to alias this to a public method with a
        better name.
        """
        self.proc.waitForReadyRead(500)
        self.read_log()
        return self._data

    def _wait_signal(self, signal, timeout=5000, raising=True):
        """Wait for a signal to be emitted.

        Should be used in a contextmanager.
        """
        blocker = pytestqt.plugin.SignalBlocker(
            timeout=timeout, raising=raising)
        blocker.connect(signal)
        return blocker

    @pyqtSlot()
    def read_log(self):
        """Read the log from the process' stdout."""
        if not hasattr(self, 'proc'):
            # I have no idea how this happens, but it does...
            return
        while self.proc.canReadLine():
            line = self.proc.readLine()
            line = bytes(line).decode('utf-8', errors='ignore').rstrip('\r\n')

            try:
                parsed = self._parse_line(line)
            except InvalidLine:
                self._invalid.append(line)
                self._log("INVALID: {}".format(line))
                continue

            if parsed is None:
                if self._invalid:
                    self._log("IGNORED: {}".format(line))
            else:
                self._data.append(parsed)
                self.new_data.emit(parsed)

    def start(self):
        """Start the process and wait until it started."""
        with self._wait_signal(self.ready, timeout=60000):
            self._start()

    def _start(self):
        """Actually start the process."""
        executable, args = self._executable_args()
        self.proc.readyRead.connect(self.read_log)
        self.proc.start(executable, args)
        ok = self.proc.waitForStarted()
        assert ok
        assert self.is_running()

    def before_test(self):
        """Restart process before a test if it exited before."""
        self._invalid = []
        if not self.is_running():
            self.start()

    def after_test(self):
        """Clean up data after each test.

        Also checks self._invalid so the test counts as failed if there were
        unexpected output lines earlier.
        """
        self.captured_log = []
        if self._invalid:
            # Wait for a bit so the full error has a chance to arrive
            time.sleep(1)
            # Exit the process to make sure we're in a defined state again
            self.terminate()
            self.clear_data()
            raise InvalidLine(self._invalid)

        self.clear_data()
        if not self.is_running() and not self.exit_expected:
            raise ProcessExited
        self.exit_expected = False

    def clear_data(self):
        """Clear the collected data."""
        self._data.clear()

    def terminate(self):
        """Clean up and shut down the process."""
        self.proc.terminate()
        self.proc.waitForFinished()

    def is_running(self):
        """Check if the process is currently running."""
        return self.proc.state() == QProcess.Running

    def _match_data(self, value, expected):
        """Helper for wait_for to match a given value.

        The behavior of this method is slightly different depending on the
        types of the filtered values:

        - If expected is None, the filter always matches.
        - If the value is a string or bytes object and the expected value is
          too, the pattern is treated as a glob pattern (with only * active).
        - If the value is a string or bytes object and the expected value is a
          compiled regex, it is used for matching.
        - If the value is any other type, == is used.

        Return:
            A bool
        """
        regex_type = type(re.compile(''))
        if expected is None:
            return True
        elif isinstance(expected, regex_type):
            return expected.match(value)
        elif isinstance(value, (bytes, str)):
            return utils.pattern_match(pattern=expected, value=value)
        else:
            return value == expected

    def _wait_for_existing(self, override_waited_for, **kwargs):
        """Check if there are any line in the history for wait_for.

        Return: either the found line or None.
        """
        for line in self._data:
            matches = []

            for key, expected in kwargs.items():
                value = getattr(line, key)
                matches.append(self._match_data(value, expected))

            if all(matches) and (not line.waited_for or override_waited_for):
                # If we waited for this line, chances are we don't mean the
                # same thing the next time we use wait_for and it matches
                # this line again.
                line.waited_for = True
                return line
        return None

    def _wait_for_match(self, spy, kwargs):
        """Try matching the kwargs with the given QSignalSpy."""
        for args in spy:
            assert len(args) == 1
            line = args[0]

            matches = []

            for key, expected in kwargs.items():
                value = getattr(line, key)
                matches.append(self._match_data(value, expected))

            if all(matches):
                # If we waited for this line, chances are we don't mean the
                # same thing the next time we use wait_for and it matches
                # this line again.
                line.waited_for = True
                return line
        return None

    def _maybe_skip(self):
        """Can be overridden by subclasses to skip on certain log lines.

        We can't run pytest.skip directly while parsing the log, as that would
        lead to a pytest.skip.Exception error in a virtual Qt method, which
        means pytest-qt fails the test.

        Instead, we check for skip messages periodically in
        QuteProc._maybe_skip, and call _maybe_skip after every parsed message
        in wait_for (where it's most likely that new messages arrive).
        """
        pass

    def wait_for(self, timeout=None, *, override_waited_for=False,
                 do_skip=False, **kwargs):
        """Wait until a given value is found in the data.

        Keyword arguments to this function get interpreted as attributes of the
        searched data. Every given argument is treated as a pattern which
        the attribute has to match against.

        Args:
            timeout: How long to wait for the message.
            override_waited_for: If set, gets triggered by previous messages
                                 again.
            do_skip: If set, call pytest.skip on a timeout.

        Return:
            The matched line.
        """
        __tracebackhide__ = True

        if timeout is None:
            if do_skip:
                timeout = 2000
            elif 'CI' in os.environ:
                timeout = 15000
            else:
                timeout = 5000
        if not kwargs:
            raise TypeError("No keyword arguments given!")
        for key in kwargs:
            assert key in self.KEYS

        # Search existing messages
        existing = self._wait_for_existing(override_waited_for, **kwargs)
        if existing is not None:
            return existing

        # If there is none, wait for the message
        spy = QSignalSpy(self.new_data)
        elapsed_timer = QElapsedTimer()
        elapsed_timer.start()

        while True:
            # Skip if there are pending messages causing a skip
            self._maybe_skip()
            got_signal = spy.wait(timeout)
            if not got_signal or elapsed_timer.hasExpired(timeout):
                msg = "Timed out after {}ms waiting for {!r}.".format(
                    timeout, kwargs)
                if do_skip:
                    pytest.skip(msg)
                else:
                    raise WaitForTimeout(msg)

            match = self._wait_for_match(spy, kwargs)
            if match is not None:
                return match

    def ensure_not_logged(self, delay=500, **kwargs):
        """Make sure the data matching the given arguments is not logged.

        If nothing is found in the log, we wait for delay ms to make sure
        nothing arrives.
        """
        __tracebackhide__ = True
        try:
            line = self.wait_for(timeout=delay, override_waited_for=True,
                                 **kwargs)
        except WaitForTimeout:
            return
        else:
            raise BlacklistedMessageError(line)
Beispiel #48
0
class Process(QObject):

    """Abstraction over a running test subprocess process.

    Reads the log from its stdout and parses it.

    Signals:
        new_data: Emitted when a new line was parsed.
    """

    PROCESS_NAME = None
    new_data = pyqtSignal(object)

    def __init__(self, parent=None):
        super().__init__(parent)
        assert self.PROCESS_NAME is not None
        self._invalid = False
        self._data = []
        self.proc = QProcess()
        self.proc.setReadChannel(QProcess.StandardError)

    def _parse_line(self, line):
        """Parse the given line from the log.

        Return:
            A self.ParseResult member.
        """
        raise NotImplementedError

    def _executable_args(self):
        """Get the arguments to pass to the executable."""
        raise NotImplementedError

    def _get_data(self):
        """Get the parsed data for this test.

        Also waits for 0.5s to make sure any new data is received.

        Subprocesses are expected to alias this to a public method with a
        better name.
        """
        self.proc.waitForReadyRead(500)
        self.read_log()
        return self._data

    @pyqtSlot()
    def read_log(self):
        """Read the log from the process' stdout."""
        while self.proc.canReadLine():
            line = self.proc.readLine()
            line = bytes(line).decode('utf-8').rstrip('\r\n')
            print(line)

            try:
                parsed = self._parse_line(line)
            except InvalidLine:
                self._invalid = True
                print("INVALID: {}".format(line))
                continue

            print('parsed: {}'.format(parsed))
            if parsed is not None:
                self._data.append(parsed)
                self.new_data.emit(parsed)

    def start(self):
        """Start the process and wait until it started."""
        blocker = pytestqt.plugin.SignalBlocker(timeout=5000, raising=True)
        blocker.connect(self.ready)
        with blocker:
            self._start()

    def _start(self):
        """Actually start the process."""
        if hasattr(sys, 'frozen'):
            executable = os.path.join(os.path.dirname(sys.executable),
                                      self.PROCESS_NAME)
            args = []
        else:
            executable = sys.executable
            args = [os.path.join(os.path.dirname(__file__),
                                 self.PROCESS_NAME + '.py')]

        self.proc.start(executable, args + self._executable_args())
        ok = self.proc.waitForStarted()
        assert ok
        self.proc.readyRead.connect(self.read_log)

    def after_test(self):
        """Clean up data after each test.

        Also checks self._invalid so the test counts as failed if there were
        unexpected output lines earlier.
        """
        self._data.clear()
        if self._invalid:
            raise InvalidLine

    def cleanup(self):
        """Clean up and shut down the process."""
        self.proc.terminate()
        self.proc.waitForFinished()
Beispiel #49
0
class SvnBlameDialog(QDialog, Ui_SvnBlameDialog):
    """
    Class implementing a dialog to show the output of the svn blame command.
    """
    def __init__(self, vcs, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param parent parent widget (QWidget)
        """
        super(SvnBlameDialog, self).__init__(parent)
        self.setupUi(self)
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
        
        self.process = QProcess()
        self.vcs = vcs
        
        self.blameList.headerItem().setText(self.blameList.columnCount(), "")
        font = Preferences.getEditorOtherFonts("MonospacedFont")
        self.blameList.setFont(font)
        
        self.__ioEncoding = Preferences.getSystem("IOEncoding")
        
        self.process.finished.connect(self.__procFinished)
        self.process.readyReadStandardOutput.connect(self.__readStdout)
        self.process.readyReadStandardError.connect(self.__readStderr)
        
    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        e.accept()
        
    def start(self, fn):
        """
        Public slot to start the svn status command.
        
        @param fn filename to show the log for (string)
        """
        self.errorGroup.hide()
        self.intercept = False
        self.activateWindow()
        self.lineno = 1
        
        dname, fname = self.vcs.splitPath(fn)
        
        self.process.kill()
        
        args = []
        args.append('blame')
        self.vcs.addArguments(args, self.vcs.options['global'])
        args.append(fname)
        
        self.process.setWorkingDirectory(dname)
        
        self.process.start('svn', args)
        procStarted = self.process.waitForStarted(5000)
        if not procStarted:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            E5MessageBox.critical(
                self,
                self.tr('Process Generation Error'),
                self.tr(
                    'The process {0} could not be started. '
                    'Ensure, that it is in the search path.'
                ).format('svn'))
        else:
            self.inputGroup.setEnabled(True)
            self.inputGroup.show()
        
    def __finish(self):
        """
        Private slot called when the process finished or the user pressed the
        button.
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        self.buttonBox.button(QDialogButtonBox.Close).setFocus(
            Qt.OtherFocusReason)
        
        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()
        
        self.process = None
        
        self.__resizeColumns()
        
    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.close()
        elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
            self.__finish()
        
    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__finish()
        
    def __resizeColumns(self):
        """
        Private method to resize the list columns.
        """
        self.blameList.header().resizeSections(QHeaderView.ResizeToContents)
        
    def __generateItem(self, revision, author, text):
        """
        Private method to generate a blame item in the blame list.
        
        @param revision revision string (string)
        @param author author of the change (string)
        @param text line of text from the annotated file (string)
        """
        itm = QTreeWidgetItem(
            self.blameList,
            [revision, author, "{0:d}".format(self.lineno), text])
        self.lineno += 1
        itm.setTextAlignment(0, Qt.AlignRight)
        itm.setTextAlignment(2, Qt.AlignRight)
        
    def __readStdout(self):
        """
        Private slot to handle the readyReadStdout signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        self.process.setReadChannel(QProcess.StandardOutput)
        
        while self.process.canReadLine():
            s = str(self.process.readLine(), self.__ioEncoding, 'replace')\
                .strip()
            rev, s = s.split(None, 1)
            try:
                author, text = s.split(' ', 1)
            except ValueError:
                author = s.strip()
                text = ""
            self.__generateItem(rev, author, text)
        
    def __readStderr(self):
        """
        Private slot to handle the readyReadStderr signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            self.errorGroup.show()
            s = str(self.process.readAllStandardError(),
                    Preferences.getSystem("IOEncoding"),
                    'replace')
            self.errors.insertPlainText(s)
            self.errors.ensureCursorVisible()
        
    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)
        
    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the subversion process.
        """
        input = self.input.text()
        input += os.linesep
        
        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(input)
            self.errors.ensureCursorVisible()
        
        self.process.write(input)
        
        self.passwordCheckBox.setChecked(False)
        self.input.clear()
        
    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()
        
    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(SvnBlameDialog, self).keyPressEvent(evt)
Beispiel #50
0
class GitReflogBrowserDialog(QWidget, Ui_GitReflogBrowserDialog):
    """
    Class implementing a dialog to browse the reflog history.
    """
    CommitIdColumn = 0
    SelectorColumn = 1
    NameColumn = 2
    OperationColumn = 3
    SubjectColumn = 4

    def __init__(self, vcs, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param parent reference to the parent widget (QWidget)
        """
        super(GitReflogBrowserDialog, self).__init__(parent)
        self.setupUi(self)

        self.__position = QPoint()

        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)

        self.logTree.headerItem().setText(self.logTree.columnCount(), "")

        self.refreshButton = self.buttonBox.addButton(
            self.tr("&Refresh"), QDialogButtonBox.ActionRole)
        self.refreshButton.setToolTip(
            self.tr("Press to refresh the list of commits"))
        self.refreshButton.setEnabled(False)

        self.vcs = vcs

        self.__formatTemplate = ('format:recordstart%n'
                                 'commit|%h%n'
                                 'selector|%gd%n'
                                 'name|%gn%n'
                                 'subject|%gs%n'
                                 'recordend%n')

        self.repodir = ""
        self.__currentCommitId = ""

        self.__initData()
        self.__resetUI()

        self.process = QProcess()
        self.process.finished.connect(self.__procFinished)
        self.process.readyReadStandardOutput.connect(self.__readStdout)
        self.process.readyReadStandardError.connect(self.__readStderr)

    def __initData(self):
        """
        Private method to (re-)initialize some data.
        """
        self.buf = []  # buffer for stdout
        self.__started = False
        self.__skipEntries = 0

    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if (self.process is not None
                and self.process.state() != QProcess.NotRunning):
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)

        self.__position = self.pos()

        e.accept()

    def show(self):
        """
        Public slot to show the dialog.
        """
        if not self.__position.isNull():
            self.move(self.__position)
        self.__resetUI()

        super(GitReflogBrowserDialog, self).show()

    def __resetUI(self):
        """
        Private method to reset the user interface.
        """
        self.limitSpinBox.setValue(
            self.vcs.getPlugin().getPreferences("LogLimit"))

        self.logTree.clear()

    def __resizeColumnsLog(self):
        """
        Private method to resize the log tree columns.
        """
        self.logTree.header().resizeSections(QHeaderView.ResizeToContents)
        self.logTree.header().setStretchLastSection(True)

    def __generateReflogItem(self, commitId, selector, name, subject):
        """
        Private method to generate a reflog tree entry.
        
        @param commitId commit id info (string)
        @param selector selector info (string)
        @param name name info (string)
        @param subject subject of the reflog entry (string)
        @return reference to the generated item (QTreeWidgetItem)
        """
        operation, subject = subject.strip().split(": ", 1)
        columnLabels = [
            commitId,
            selector,
            name,
            operation,
            subject,
        ]
        itm = QTreeWidgetItem(self.logTree, columnLabels)
        return itm

    def __getReflogEntries(self, skip=0):
        """
        Private method to retrieve reflog entries from the repository.
        
        @param skip number of reflog entries to skip (integer)
        """
        self.refreshButton.setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
        QApplication.processEvents()

        QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
        QApplication.processEvents()

        self.buf = []
        self.cancelled = False
        self.errors.clear()
        self.intercept = False

        args = self.vcs.initCommand("log")
        args.append("--walk-reflogs")
        args.append('--max-count={0}'.format(self.limitSpinBox.value()))
        args.append('--abbrev={0}'.format(
            self.vcs.getPlugin().getPreferences("CommitIdLength")))
        args.append('--format={0}'.format(self.__formatTemplate))
        args.append('--skip={0}'.format(skip))

        self.process.kill()

        self.process.setWorkingDirectory(self.repodir)

        self.inputGroup.setEnabled(True)
        self.inputGroup.show()

        self.process.start('git', args)
        procStarted = self.process.waitForStarted(5000)
        if not procStarted:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            E5MessageBox.critical(
                self, self.tr('Process Generation Error'),
                self.tr(
                    'The process {0} could not be started. '
                    'Ensure, that it is in the search path.').format('git'))

    def start(self, projectdir):
        """
        Public slot to start the git log command.
        
        @param projectdir directory name of the project (string)
        """
        self.errorGroup.hide()
        QApplication.processEvents()

        self.__initData()

        # find the root of the repo
        self.repodir = projectdir
        while not os.path.isdir(os.path.join(self.repodir, self.vcs.adminDir)):
            self.repodir = os.path.dirname(self.repodir)
            if os.path.splitdrive(self.repodir)[1] == os.sep:
                return

        self.activateWindow()
        self.raise_()

        self.logTree.clear()
        self.__started = True
        self.__getReflogEntries()

    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__processBuffer()
        self.__finish()

    def __finish(self):
        """
        Private slot called when the process finished or the user pressed
        the button.
        """
        if (self.process is not None
                and self.process.state() != QProcess.NotRunning):
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)

        QApplication.restoreOverrideCursor()

        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)

        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()
        self.refreshButton.setEnabled(True)

    def __processBuffer(self):
        """
        Private method to process the buffered output of the git log command.
        """
        noEntries = 0

        for line in self.buf:
            line = line.rstrip()
            if line == "recordstart":
                logEntry = {}
            elif line == "recordend":
                if len(logEntry) > 1:
                    self.__generateReflogItem(
                        logEntry["commit"],
                        logEntry["selector"],
                        logEntry["name"],
                        logEntry["subject"],
                    )
                    noEntries += 1
            else:
                try:
                    key, value = line.split("|", 1)
                except ValueError:
                    key = ""
                    value = line
                if key in ("commit", "selector", "name", "subject"):
                    logEntry[key] = value.strip()

        self.__resizeColumnsLog()

        if self.__started:
            self.logTree.setCurrentItem(self.logTree.topLevelItem(0))
            self.__started = False

        self.__skipEntries += noEntries
        if noEntries < self.limitSpinBox.value() and not self.cancelled:
            self.nextButton.setEnabled(False)
            self.limitSpinBox.setEnabled(False)
        else:
            self.nextButton.setEnabled(True)
            self.limitSpinBox.setEnabled(True)

        # restore current item
        if self.__currentCommitId:
            items = self.logTree.findItems(self.__currentCommitId,
                                           Qt.MatchExactly,
                                           self.CommitIdColumn)
            if items:
                self.logTree.setCurrentItem(items[0])
                self.__currentCommitId = ""

    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.
        
        It reads the output of the process and inserts it into a buffer.
        """
        self.process.setReadChannel(QProcess.StandardOutput)

        while self.process.canReadLine():
            line = str(self.process.readLine(),
                       Preferences.getSystem("IOEncoding"), 'replace')
            self.buf.append(line)

    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            s = str(self.process.readAllStandardError(),
                    Preferences.getSystem("IOEncoding"), 'replace')
            self.__showError(s)

    def __showError(self, out):
        """
        Private slot to show some error.
        
        @param out error to be shown (string)
        """
        self.errorGroup.show()
        self.errors.insertPlainText(out)
        self.errors.ensureCursorVisible()

    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.close()
        elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
            self.cancelled = True
            self.__finish()
        elif button == self.refreshButton:
            self.on_refreshButton_clicked()

    @pyqtSlot()
    def on_refreshButton_clicked(self):
        """
        Private slot to refresh the log.
        """
        # save the current item's commit ID
        itm = self.logTree.currentItem()
        if itm is not None:
            self.__currentCommitId = itm.text(self.CommitIdColumn)
        else:
            self.__currentCommitId = ""

        self.start(self.repodir)

    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)

    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the git process.
        """
        inputTxt = self.input.text()
        inputTxt += os.linesep

        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(inputTxt)
            self.errors.ensureCursorVisible()
        self.errorGroup.show()

        self.process.write(strToQByteArray(inputTxt))

        self.passwordCheckBox.setChecked(False)
        self.input.clear()

    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()

    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(GitReflogBrowserDialog, self).keyPressEvent(evt)

    @pyqtSlot()
    def on_nextButton_clicked(self):
        """
        Private slot to handle the Next button.
        """
        if self.__skipEntries > 0:
            self.__getReflogEntries(self.__skipEntries)
Beispiel #51
0
class SvnPropListDialog(QWidget, Ui_SvnPropListDialog):
    """
    Class implementing a dialog to show the output of the svn proplist command
    process.
    """
    def __init__(self, vcs, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param parent parent widget (QWidget)
        """
        super(SvnPropListDialog, self).__init__(parent)
        self.setupUi(self)
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
        
        self.process = QProcess()
        env = QProcessEnvironment.systemEnvironment()
        env.insert("LANG", "C")
        self.process.setProcessEnvironment(env)
        self.vcs = vcs
        
        self.propsList.headerItem().setText(self.propsList.columnCount(), "")
        self.propsList.header().setSortIndicator(0, Qt.AscendingOrder)
        
        self.process.finished.connect(self.__procFinished)
        self.process.readyReadStandardOutput.connect(self.__readStdout)
        self.process.readyReadStandardError.connect(self.__readStderr)
        
        self.rx_path = QRegExp(r"Properties on '([^']+)':\s*")
        self.rx_prop = QRegExp(r"  (.*) *: *(.*)[\r\n]")
        self.lastPath = None
        self.lastProp = None
        self.propBuffer = ""
        
    def __resort(self):
        """
        Private method to resort the tree.
        """
        self.propsList.sortItems(
            self.propsList.sortColumn(),
            self.propsList.header().sortIndicatorOrder())
        
    def __resizeColumns(self):
        """
        Private method to resize the list columns.
        """
        self.propsList.header().resizeSections(QHeaderView.ResizeToContents)
        self.propsList.header().setStretchLastSection(True)
        
    def __generateItem(self, path, propName, propValue):
        """
        Private method to generate a properties item in the properties list.
        
        @param path file/directory name the property applies to (string)
        @param propName name of the property (string)
        @param propValue value of the property (string)
        """
        QTreeWidgetItem(self.propsList, [path, propName, propValue.strip()])
        
    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        e.accept()
        
    def start(self, fn, recursive=False):
        """
        Public slot to start the svn status command.
        
        @param fn filename(s) (string or list of string)
        @param recursive flag indicating a recursive list is requested
        """
        self.errorGroup.hide()
        
        self.process.kill()
        
        args = []
        args.append('proplist')
        self.vcs.addArguments(args, self.vcs.options['global'])
        args.append('--verbose')
        if recursive:
            args.append('--recursive')
        if isinstance(fn, list):
            dname, fnames = self.vcs.splitPathList(fn)
            self.vcs.addArguments(args, fnames)
        else:
            dname, fname = self.vcs.splitPath(fn)
            args.append(fname)
        
        self.process.setWorkingDirectory(dname)
        
        self.process.start('svn', args)
        procStarted = self.process.waitForStarted(5000)
        if not procStarted:
            E5MessageBox.critical(
                self,
                self.tr('Process Generation Error'),
                self.tr(
                    'The process {0} could not be started. '
                    'Ensure, that it is in the search path.'
                ).format('svn'))
        
    def __finish(self):
        """
        Private slot called when the process finished or the user pressed the
        button.
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        
        self.process = None
        if self.lastProp:
            self.__generateItem(self.lastPath, self.lastProp, self.propBuffer)
        
        self.__resort()
        self.__resizeColumns()
        
    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.close()
        elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
            self.__finish()
        
    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        if self.lastPath is None:
            self.__generateItem('', 'None', '')
        
        self.__finish()
        
    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        self.process.setReadChannel(QProcess.StandardOutput)
        
        while self.process.canReadLine():
            s = str(self.process.readLine(),
                    Preferences.getSystem("IOEncoding"),
                    'replace')
            if self.rx_path.exactMatch(s):
                if self.lastProp:
                    self.__generateItem(
                        self.lastPath, self.lastProp, self.propBuffer)
                self.lastPath = self.rx_path.cap(1)
                self.lastProp = None
                self.propBuffer = ""
            elif self.rx_prop.exactMatch(s):
                if self.lastProp:
                    self.__generateItem(
                        self.lastPath, self.lastProp, self.propBuffer)
                self.lastProp = self.rx_prop.cap(1)
                self.propBuffer = self.rx_prop.cap(2)
            else:
                self.propBuffer += ' '
                self.propBuffer += s
        
    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            self.errorGroup.show()
            s = str(self.process.readAllStandardError(),
                    Preferences.getSystem("IOEncoding"),
                    'replace')
            self.errors.insertPlainText(s)
            self.errors.ensureCursorVisible()
class HgShelveBrowserDialog(QWidget, Ui_HgShelveBrowserDialog):
    """
    Class implementing Mercurial shelve browser dialog.
    """
    NameColumn = 0
    AgeColumn = 1
    MessageColumn = 2

    def __init__(self, vcs, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param parent parent widget (QWidget)
        """
        super(HgShelveBrowserDialog, self).__init__(parent)
        self.setupUi(self)

        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)

        self.__position = QPoint()

        self.__fileStatisticsRole = Qt.UserRole
        self.__totalStatisticsRole = Qt.UserRole + 1

        self.shelveList.header().setSortIndicator(0, Qt.AscendingOrder)

        self.refreshButton = self.buttonBox.addButton(
            self.tr("&Refresh"), QDialogButtonBox.ActionRole)
        self.refreshButton.setToolTip(
            self.tr("Press to refresh the list of shelves"))
        self.refreshButton.setEnabled(False)

        self.vcs = vcs
        self.__hgClient = vcs.getClient()
        self.__resetUI()

        if self.__hgClient:
            self.process = None
        else:
            self.process = QProcess()
            self.process.finished.connect(self.__procFinished)
            self.process.readyReadStandardOutput.connect(self.__readStdout)
            self.process.readyReadStandardError.connect(self.__readStderr)

        self.__contextMenu = QMenu()
        self.__unshelveAct = self.__contextMenu.addAction(
            self.tr("Restore selected shelve"), self.__unshelve)
        self.__deleteAct = self.__contextMenu.addAction(
            self.tr("Delete selected shelves"), self.__deleteShelves)
        self.__contextMenu.addAction(self.tr("Delete all shelves"),
                                     self.__cleanupShelves)

    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.__hgClient:
            if self.__hgClient.isExecuting():
                self.__hgClient.cancel()
        else:
            if self.process is not None and \
               self.process.state() != QProcess.NotRunning:
                self.process.terminate()
                QTimer.singleShot(2000, self.process.kill)
                self.process.waitForFinished(3000)

        self.__position = self.pos()

        e.accept()

    def show(self):
        """
        Public slot to show the dialog.
        """
        if not self.__position.isNull():
            self.move(self.__position)
        self.__resetUI()

        super(HgShelveBrowserDialog, self).show()

    def __resetUI(self):
        """
        Private method to reset the user interface.
        """
        self.shelveList.clear()

    def __resizeColumnsShelves(self):
        """
        Private method to resize the shelve list columns.
        """
        self.shelveList.header().resizeSections(QHeaderView.ResizeToContents)
        self.shelveList.header().setStretchLastSection(True)

    def __generateShelveEntry(self, name, age, message, fileStatistics,
                              totals):
        """
        Private method to generate the shelve items.
        
        @param name name of the shelve (string)
        @param age age of the shelve (string)
        @param message shelve message (string)
        @param fileStatistics per file change statistics (tuple of
            four strings with file name, number of changes, number of
            added lines and number of deleted lines)
        @param totals overall statistics (tuple of three strings with
            number of changed files, number of added lines and number
            of deleted lines)
        """
        itm = QTreeWidgetItem(self.shelveList, [name, age, message])
        itm.setData(0, self.__fileStatisticsRole, fileStatistics)
        itm.setData(0, self.__totalStatisticsRole, totals)

    def __getShelveEntries(self):
        """
        Private method to retrieve the list of shelves.
        """
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
        QApplication.processEvents()

        QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
        QApplication.processEvents()

        self.buf = []
        self.errors.clear()
        self.intercept = False

        args = self.vcs.initCommand("shelve")
        args.append("--list")
        args.append("--stat")

        if self.__hgClient:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()

            out, err = self.__hgClient.runcommand(args)
            self.buf = out.splitlines(True)
            if err:
                self.__showError(err)
            self.__processBuffer()
            self.__finish()
        else:
            self.process.kill()

            self.process.setWorkingDirectory(self.repodir)

            self.inputGroup.setEnabled(True)
            self.inputGroup.show()

            self.process.start('hg', args)
            procStarted = self.process.waitForStarted(5000)
            if not procStarted:
                self.inputGroup.setEnabled(False)
                self.inputGroup.hide()
                E5MessageBox.critical(
                    self, self.tr('Process Generation Error'),
                    self.tr(
                        'The process {0} could not be started. '
                        'Ensure, that it is in the search path.').format('hg'))

    def start(self, projectDir):
        """
        Public slot to start the hg shelve command.
        
        @param projectDir name of the project directory (string)
        """
        self.errorGroup.hide()
        QApplication.processEvents()

        self.__projectDir = projectDir

        # find the root of the repo
        self.repodir = self.__projectDir
        while not os.path.isdir(os.path.join(self.repodir, self.vcs.adminDir)):
            self.repodir = os.path.dirname(self.repodir)
            if os.path.splitdrive(self.repodir)[1] == os.sep:
                return

        self.activateWindow()
        self.raise_()

        self.shelveList.clear()
        self.__started = True
        self.__getShelveEntries()

    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__processBuffer()
        self.__finish()

    def __finish(self):
        """
        Private slot called when the process finished or the user pressed
        the button.
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)

        QApplication.restoreOverrideCursor()

        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)

        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()
        self.refreshButton.setEnabled(True)

    def __processBuffer(self):
        """
        Private method to process the buffered output of the hg shelve command.
        """
        lastWasFileStats = False
        firstLine = True
        itemData = {}
        for line in self.buf:
            if firstLine:
                name, line = line.split("(", 1)
                age, message = line.split(")", 1)
                itemData["name"] = name.strip()
                itemData["age"] = age.strip()
                itemData["message"] = message.strip()
                itemData["files"] = []
                firstLine = False
            elif '|' in line:
                # file stats: foo.py |  3 ++-
                file, changes = line.strip().split("|", 1)
                if changes.strip().endswith(("+", "-")):
                    total, addDelete = changes.strip().split(None, 1)
                    additions = str(addDelete.count("+"))
                    deletions = str(addDelete.count("-"))
                else:
                    total = changes.strip()
                    additions = '0'
                    deletions = '0'
                itemData["files"].append((file, total, additions, deletions))
                lastWasFileStats = True
            elif lastWasFileStats:
                # summary line
                # 2 files changed, 15 insertions(+), 1 deletions(-)
                total, added, deleted = line.strip().split(",", 2)
                total = total.split()[0]
                added = added.split()[0]
                deleted = deleted.split()[0]
                itemData["summary"] = (total, added, deleted)

                self.__generateShelveEntry(itemData["name"], itemData["age"],
                                           itemData["message"],
                                           itemData["files"],
                                           itemData["summary"])

                lastWasFileStats = False
                firstLine = True
                itemData = {}

        self.__resizeColumnsShelves()

        if self.__started:
            self.shelveList.setCurrentItem(self.shelveList.topLevelItem(0))
            self.__started = False

    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.
        
        It reads the output of the process and inserts it into a buffer.
        """
        self.process.setReadChannel(QProcess.StandardOutput)

        while self.process.canReadLine():
            line = str(self.process.readLine(), self.vcs.getEncoding(),
                       'replace')
            self.buf.append(line)

    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            s = str(self.process.readAllStandardError(),
                    self.vcs.getEncoding(), 'replace')
            self.__showError(s)

    def __showError(self, out):
        """
        Private slot to show some error.
        
        @param out error to be shown (string)
        """
        self.errorGroup.show()
        self.errors.insertPlainText(out)
        self.errors.ensureCursorVisible()

    @pyqtSlot(QAbstractButton)
    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.close()
        elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
            self.cancelled = True
            if self.__hgClient:
                self.__hgClient.cancel()
            else:
                self.__finish()
        elif button == self.refreshButton:
            self.on_refreshButton_clicked()

    @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem)
    def on_shelveList_currentItemChanged(self, current, previous):
        """
        Private slot called, when the current item of the shelve list changes.
        
        @param current reference to the new current item (QTreeWidgetItem)
        @param previous reference to the old current item (QTreeWidgetItem)
        """
        self.statisticsList.clear()
        if current:
            for dataSet in current.data(0, self.__fileStatisticsRole):
                QTreeWidgetItem(self.statisticsList, list(dataSet))
            self.statisticsList.header().resizeSections(
                QHeaderView.ResizeToContents)
            self.statisticsList.header().setStretchLastSection(True)

            totals = current.data(0, self.__totalStatisticsRole)
            self.filesLabel.setText(
                self.tr("%n file(s) changed", None, int(totals[0])))
            self.insertionsLabel.setText(
                self.tr("%n line(s) inserted", None, int(totals[1])))
            self.deletionsLabel.setText(
                self.tr("%n line(s) deleted", None, int(totals[2])))
        else:
            self.filesLabel.setText("")
            self.insertionsLabel.setText("")
            self.deletionsLabel.setText("")

    @pyqtSlot(QPoint)
    def on_shelveList_customContextMenuRequested(self, pos):
        """
        Private slot to show the context menu of the shelve list.
        
        @param pos position of the mouse pointer (QPoint)
        """
        selectedItemsCount = len(self.shelveList.selectedItems())
        self.__unshelveAct.setEnabled(selectedItemsCount == 1)
        self.__deleteAct.setEnabled(selectedItemsCount > 0)

        self.__contextMenu.popup(self.mapToGlobal(pos))

    @pyqtSlot()
    def on_refreshButton_clicked(self):
        """
        Private slot to refresh the list of shelves.
        """
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)

        self.inputGroup.setEnabled(True)
        self.inputGroup.show()
        self.refreshButton.setEnabled(False)

        self.start(self.__projectDir)

    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)

    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the mercurial process.
        """
        input = self.input.text()
        input += os.linesep

        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(input)
            self.errors.ensureCursorVisible()
        self.errorGroup.show()

        self.process.write(input)

        self.passwordCheckBox.setChecked(False)
        self.input.clear()

    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()

    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(HgShelveBrowserDialog, self).keyPressEvent(evt)

    def __unshelve(self):
        """
        Private slot to restore the selected shelve of changes.
        """
        itm = self.shelveList.selectedItems()[0]
        if itm is not None:
            name = itm.text(self.NameColumn)
            self.vcs.getExtensionObject("shelve")\
                .hgUnshelve(self.__projectDir, shelveName=name)
            self.on_refreshButton_clicked()

    def __deleteShelves(self):
        """
        Private slot to delete the selected shelves.
        """
        shelveNames = []
        for itm in self.shelveList.selectedItems():
            shelveNames.append(itm.text(self.NameColumn))
        if shelveNames:
            self.vcs.getExtensionObject("shelve")\
                .hgDeleteShelves(self.__projectDir, shelveNames=shelveNames)
            self.on_refreshButton_clicked()

    def __cleanupShelves(self):
        """
        Private slot to delete all shelves.
        """
        self.vcs.getExtensionObject("shelve")\
            .hgCleanupShelves(self.__projectDir)
        self.on_refreshButton_clicked()
Beispiel #53
0
class HgTagBranchListDialog(QDialog, Ui_HgTagBranchListDialog):
    """
    Class implementing a dialog to show a list of tags or branches.
    """
    def __init__(self, vcs, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param parent parent widget (QWidget)
        """
        super(HgTagBranchListDialog, self).__init__(parent)
        self.setupUi(self)
        self.setWindowFlags(Qt.Window)
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
        
        self.vcs = vcs
        self.tagsList = None
        self.allTagsList = None
        self.__hgClient = vcs.getClient()
        
        self.tagList.headerItem().setText(self.tagList.columnCount(), "")
        self.tagList.header().setSortIndicator(3, Qt.AscendingOrder)
        
        if self.__hgClient:
            self.process = None
        else:
            self.process = QProcess()
            self.process.finished.connect(self.__procFinished)
            self.process.readyReadStandardOutput.connect(self.__readStdout)
            self.process.readyReadStandardError.connect(self.__readStderr)
        
        self.show()
        QCoreApplication.processEvents()
    
    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.__hgClient:
            if self.__hgClient.isExecuting():
                self.__hgClient.cancel()
        else:
            if self.process is not None and \
               self.process.state() != QProcess.NotRunning:
                self.process.terminate()
                QTimer.singleShot(2000, self.process.kill)
                self.process.waitForFinished(3000)
        
        e.accept()
    
    def start(self, path, tags, tagsList, allTagsList):
        """
        Public slot to start the tags command.
        
        @param path name of directory to be listed (string)
        @param tags flag indicating a list of tags is requested
            (False = branches, True = tags)
        @param tagsList reference to string list receiving the tags
            (list of strings)
        @param allTagsList reference to string list all tags (list of strings)
        """
        self.errorGroup.hide()
        
        self.intercept = False
        self.tagsMode = tags
        if not tags:
            self.setWindowTitle(self.tr("Mercurial Branches List"))
            self.tagList.headerItem().setText(2, self.tr("Status"))
        self.activateWindow()
        
        self.tagsList = tagsList
        self.allTagsList = allTagsList
        dname, fname = self.vcs.splitPath(path)
        
        # find the root of the repo
        repodir = dname
        while not os.path.isdir(os.path.join(repodir, self.vcs.adminDir)):
            repodir = os.path.dirname(repodir)
            if os.path.splitdrive(repodir)[1] == os.sep:
                return
        
        if self.tagsMode:
            args = self.vcs.initCommand("tags")
            args.append('--verbose')
        else:
            args = self.vcs.initCommand("branches")
            args.append('--closed')
        
        if self.__hgClient:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            
            out, err = self.__hgClient.runcommand(args)
            if err:
                self.__showError(err)
            if out:
                for line in out.splitlines():
                    self.__processOutputLine(line)
                    if self.__hgClient.wasCanceled():
                        break
            self.__finish()
        else:
            self.process.kill()
            self.process.setWorkingDirectory(repodir)
            
            self.process.start('hg', args)
            procStarted = self.process.waitForStarted(5000)
            if not procStarted:
                self.inputGroup.setEnabled(False)
                self.inputGroup.hide()
                E5MessageBox.critical(
                    self,
                    self.tr('Process Generation Error'),
                    self.tr(
                        'The process {0} could not be started. '
                        'Ensure, that it is in the search path.'
                    ).format('hg'))
            else:
                self.inputGroup.setEnabled(True)
                self.inputGroup.show()
    
    def __finish(self):
        """
        Private slot called when the process finished or the user pressed
        the button.
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        self.buttonBox.button(QDialogButtonBox.Close).setFocus(
            Qt.OtherFocusReason)
        
        self.__resizeColumns()
        self.__resort()
    
    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.close()
        elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
            if self.__hgClient:
                self.__hgClient.cancel()
            else:
                self.__finish()
    
    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__finish()
    
    def __resort(self):
        """
        Private method to resort the tree.
        """
        self.tagList.sortItems(
            self.tagList.sortColumn(),
            self.tagList.header().sortIndicatorOrder())
    
    def __resizeColumns(self):
        """
        Private method to resize the list columns.
        """
        self.tagList.header().resizeSections(QHeaderView.ResizeToContents)
        self.tagList.header().setStretchLastSection(True)
    
    def __generateItem(self, revision, changeset, status, name):
        """
        Private method to generate a tag item in the tag list.
        
        @param revision revision of the tag/branch (string)
        @param changeset changeset of the tag/branch (string)
        @param status of the tag/branch (string)
        @param name name of the tag/branch (string)
        """
        itm = QTreeWidgetItem(self.tagList)
        itm.setData(0, Qt.DisplayRole, int(revision))
        itm.setData(1, Qt.DisplayRole, changeset)
        itm.setData(2, Qt.DisplayRole, status)
        itm.setData(3, Qt.DisplayRole, name)
        itm.setTextAlignment(0, Qt.AlignRight)
        itm.setTextAlignment(1, Qt.AlignRight)
        itm.setTextAlignment(2, Qt.AlignHCenter)
    
    def __readStdout(self):
        """
        Private slot to handle the readyReadStdout signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        self.process.setReadChannel(QProcess.StandardOutput)
        
        while self.process.canReadLine():
            s = str(self.process.readLine(), self.vcs.getEncoding(),
                    'replace').strip()
            self.__processOutputLine(s)
    
    def __processOutputLine(self, line):
        """
        Private method to process the lines of output.
        
        @param line output line to be processed (string)
        """
        li = line.split()
        if li[-1][0] in "1234567890":
            # last element is a rev:changeset
            if self.tagsMode:
                status = ""
            else:
                status = self.tr("active")
            rev, changeset = li[-1].split(":", 1)
            del li[-1]
        else:
            if self.tagsMode:
                status = self.tr("yes")
            else:
                status = li[-1][1:-1]
            rev, changeset = li[-2].split(":", 1)
            del li[-2:]
        name = " ".join(li)
        self.__generateItem(rev, changeset, status, name)
        if name not in ["tip", "default"]:
            if self.tagsList is not None:
                self.tagsList.append(name)
            if self.allTagsList is not None:
                self.allTagsList.append(name)
    
    def __readStderr(self):
        """
        Private slot to handle the readyReadStderr signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            s = str(self.process.readAllStandardError(),
                    self.vcs.getEncoding(), 'replace')
            self.__showError(s)
    
    def __showError(self, out):
        """
        Private slot to show some error.
        
        @param out error to be shown (string)
        """
        self.errorGroup.show()
        self.errors.insertPlainText(out)
        self.errors.ensureCursorVisible()
    
    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)
    
    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the subversion process.
        """
        input = self.input.text()
        input += os.linesep
        
        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(input)
            self.errors.ensureCursorVisible()
        
        self.process.write(input)
        
        self.passwordCheckBox.setChecked(False)
        self.input.clear()
    
    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()
    
    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(HgTagBranchListDialog, self).keyPressEvent(evt)
class SvnRepoBrowserDialog(QDialog, Ui_SvnRepoBrowserDialog):
    """
    Class implementing the subversion repository browser dialog.
    """
    def __init__(self, vcs, mode="browse", parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param mode mode of the dialog (string, "browse" or "select")
        @param parent parent widget (QWidget)
        """
        super(SvnRepoBrowserDialog, self).__init__(parent)
        self.setupUi(self)
        self.setWindowFlags(Qt.Window)

        self.repoTree.headerItem().setText(self.repoTree.columnCount(), "")
        self.repoTree.header().setSortIndicator(0, Qt.AscendingOrder)

        self.process = None
        self.vcs = vcs
        self.mode = mode

        if self.mode == "select":
            self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(False)
            self.buttonBox.button(QDialogButtonBox.Close).hide()
        else:
            self.buttonBox.button(QDialogButtonBox.Ok).hide()
            self.buttonBox.button(QDialogButtonBox.Cancel).hide()

        self.__dirIcon = UI.PixmapCache.getIcon("dirClosed.png")
        self.__fileIcon = UI.PixmapCache.getIcon("fileMisc.png")

        self.__urlRole = Qt.UserRole
        self.__ignoreExpand = False
        self.intercept = False

        self.__rx_dir = QRegExp(
            r"""\s*([0-9]+)\s+(\w+)\s+"""
            r"""((?:\w+\s+\d+|[0-9.]+\s+\w+)\s+[0-9:]+)\s+(.+)\s*""")
        self.__rx_file = QRegExp(
            r"""\s*([0-9]+)\s+(\w+)\s+([0-9]+)\s"""
            r"""((?:\w+\s+\d+|[0-9.]+\s+\w+)\s+[0-9:]+)\s+(.+)\s*""")

    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)

        e.accept()

    def __resort(self):
        """
        Private method to resort the tree.
        """
        self.repoTree.sortItems(self.repoTree.sortColumn(),
                                self.repoTree.header().sortIndicatorOrder())

    def __resizeColumns(self):
        """
        Private method to resize the tree columns.
        """
        self.repoTree.header().resizeSections(QHeaderView.ResizeToContents)
        self.repoTree.header().setStretchLastSection(True)

    def __generateItem(self, repopath, revision, author, size, date, nodekind,
                       url):
        """
        Private method to generate a tree item in the repository tree.
        
        @param repopath path of the item (string)
        @param revision revision info (string)
        @param author author info (string)
        @param size size info (string)
        @param date date info (string)
        @param nodekind node kind info (string, "dir" or "file")
        @param url url of the entry (string)
        @return reference to the generated item (QTreeWidgetItem)
        """
        path = repopath

        if revision == "":
            rev = ""
        else:
            rev = int(revision)
        if size == "":
            sz = ""
        else:
            sz = int(size)

        itm = QTreeWidgetItem(self.parentItem)
        itm.setData(0, Qt.DisplayRole, path)
        itm.setData(1, Qt.DisplayRole, rev)
        itm.setData(2, Qt.DisplayRole, author)
        itm.setData(3, Qt.DisplayRole, sz)
        itm.setData(4, Qt.DisplayRole, date)

        if nodekind == "dir":
            itm.setIcon(0, self.__dirIcon)
            itm.setChildIndicatorPolicy(QTreeWidgetItem.ShowIndicator)
        elif nodekind == "file":
            itm.setIcon(0, self.__fileIcon)

        itm.setData(0, self.__urlRole, url)

        itm.setTextAlignment(0, Qt.AlignLeft)
        itm.setTextAlignment(1, Qt.AlignRight)
        itm.setTextAlignment(2, Qt.AlignLeft)
        itm.setTextAlignment(3, Qt.AlignRight)
        itm.setTextAlignment(4, Qt.AlignLeft)

        return itm

    def __repoRoot(self, url):
        """
        Private method to get the repository root using the svn info command.
        
        @param url the repository URL to browser (string)
        @return repository root (string)
        """
        ioEncoding = Preferences.getSystem("IOEncoding")
        repoRoot = None

        process = QProcess()

        args = []
        args.append('info')
        self.vcs.addArguments(args, self.vcs.options['global'])
        args.append('--xml')
        args.append(url)

        process.start('svn', args)
        procStarted = process.waitForStarted(5000)
        if procStarted:
            finished = process.waitForFinished(30000)
            if finished:
                if process.exitCode() == 0:
                    output = str(process.readAllStandardOutput(), ioEncoding,
                                 'replace')
                    for line in output.splitlines():
                        line = line.strip()
                        if line.startswith('<root>'):
                            repoRoot = line.replace('<root>', '')\
                                .replace('</root>', '')
                            break
                else:
                    error = str(process.readAllStandardError(),
                                Preferences.getSystem("IOEncoding"), 'replace')
                    self.errors.insertPlainText(error)
                    self.errors.ensureCursorVisible()
        else:
            QApplication.restoreOverrideCursor()
            E5MessageBox.critical(
                self, self.tr('Process Generation Error'),
                self.tr(
                    'The process {0} could not be started. '
                    'Ensure, that it is in the search path.').format('svn'))
        return repoRoot

    def __listRepo(self, url, parent=None):
        """
        Private method to perform the svn list command.
        
        @param url the repository URL to browse (string)
        @param parent reference to the item, the data should be appended to
            (QTreeWidget or QTreeWidgetItem)
        """
        QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
        QApplication.processEvents()

        self.repoUrl = url

        if parent is None:
            self.parentItem = self.repoTree
        else:
            self.parentItem = parent

        if self.parentItem == self.repoTree:
            repoRoot = self.__repoRoot(url)
            if repoRoot is None:
                self.__finish()
                return
            self.__ignoreExpand = True
            itm = self.__generateItem(repoRoot, "", "", "", "", "dir",
                                      repoRoot)
            itm.setExpanded(True)
            self.parentItem = itm
            urlPart = repoRoot
            for element in url.replace(repoRoot, "").split("/"):
                if element:
                    urlPart = "{0}/{1}".format(urlPart, element)
                    itm = self.__generateItem(element, "", "", "", "", "dir",
                                              urlPart)
                    itm.setExpanded(True)
                    self.parentItem = itm
            itm.setExpanded(False)
            self.__ignoreExpand = False
            self.__finish()
            return

        self.intercept = False

        if self.process:
            self.process.kill()
        else:
            self.process = QProcess()
            self.process.finished.connect(self.__procFinished)
            self.process.readyReadStandardOutput.connect(self.__readStdout)
            self.process.readyReadStandardError.connect(self.__readStderr)

        args = []
        args.append('list')
        self.vcs.addArguments(args, self.vcs.options['global'])
        if '--verbose' not in self.vcs.options['global']:
            args.append('--verbose')
        args.append(url)

        self.process.start('svn', args)
        procStarted = self.process.waitForStarted(5000)
        if not procStarted:
            self.__finish()
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            E5MessageBox.critical(
                self, self.tr('Process Generation Error'),
                self.tr(
                    'The process {0} could not be started. '
                    'Ensure, that it is in the search path.').format('svn'))
        else:
            self.inputGroup.setEnabled(True)
            self.inputGroup.show()

    def __normalizeUrl(self, url):
        """
        Private method to normalite the url.
        
        @param url the url to normalize (string)
        @return normalized URL (string)
        """
        if url.endswith("/"):
            return url[:-1]
        return url

    def start(self, url):
        """
        Public slot to start the svn info command.
        
        @param url the repository URL to browser (string)
        """
        self.url = ""

        self.urlCombo.addItem(self.__normalizeUrl(url))

    @pyqtSlot(str)
    def on_urlCombo_currentIndexChanged(self, text):
        """
        Private slot called, when a new repository URL is entered or selected.
        
        @param text the text of the current item (string)
        """
        url = self.__normalizeUrl(text)
        if url != self.url:
            self.url = url
            self.repoTree.clear()
            self.__listRepo(url)

    @pyqtSlot(QTreeWidgetItem)
    def on_repoTree_itemExpanded(self, item):
        """
        Private slot called when an item is expanded.
        
        @param item reference to the item to be expanded (QTreeWidgetItem)
        """
        if not self.__ignoreExpand:
            url = item.data(0, self.__urlRole)
            self.__listRepo(url, item)

    @pyqtSlot(QTreeWidgetItem)
    def on_repoTree_itemCollapsed(self, item):
        """
        Private slot called when an item is collapsed.
        
        @param item reference to the item to be collapsed (QTreeWidgetItem)
        """
        for child in item.takeChildren():
            del child

    @pyqtSlot()
    def on_repoTree_itemSelectionChanged(self):
        """
        Private slot called when the selection changes.
        """
        if self.mode == "select":
            self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(True)

    def accept(self):
        """
        Public slot called when the dialog is accepted.
        """
        if self.focusWidget() == self.urlCombo:
            return

        super(SvnRepoBrowserDialog, self).accept()

    def getSelectedUrl(self):
        """
        Public method to retrieve the selected repository URL.
        
        @return the selected repository URL (string)
        """
        items = self.repoTree.selectedItems()
        if len(items) == 1:
            return items[0].data(0, self.__urlRole)
        else:
            return ""

    def __finish(self):
        """
        Private slot called when the process finished or the user pressed the
        button.
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)

        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()

        self.__resizeColumns()
        self.__resort()
        QApplication.restoreOverrideCursor()

    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__finish()

    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        if self.process is not None:
            self.process.setReadChannel(QProcess.StandardOutput)

            while self.process.canReadLine():
                s = str(self.process.readLine(),
                        Preferences.getSystem("IOEncoding"), 'replace')
                if self.__rx_dir.exactMatch(s):
                    revision = self.__rx_dir.cap(1)
                    author = self.__rx_dir.cap(2)
                    date = self.__rx_dir.cap(3)
                    name = self.__rx_dir.cap(4).strip()
                    if name.endswith("/"):
                        name = name[:-1]
                    size = ""
                    nodekind = "dir"
                elif self.__rx_file.exactMatch(s):
                    revision = self.__rx_file.cap(1)
                    author = self.__rx_file.cap(2)
                    size = self.__rx_file.cap(3)
                    date = self.__rx_file.cap(4)
                    name = self.__rx_file.cap(5).strip()
                    nodekind = "file"
                else:
                    continue
                url = "{0}/{1}".format(self.repoUrl, name)
                self.__generateItem(name, revision, author, size, date,
                                    nodekind, url)

    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            s = str(self.process.readAllStandardError(),
                    Preferences.getSystem("IOEncoding"), 'replace')
            self.errors.insertPlainText(s)
            self.errors.ensureCursorVisible()

    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)

    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the subversion process.
        """
        input = self.input.text()
        input += os.linesep

        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(input)
            self.errors.ensureCursorVisible()

        self.process.write(input)

        self.passwordCheckBox.setChecked(False)
        self.input.clear()

    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()

    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(SvnRepoBrowserDialog, self).keyPressEvent(evt)
class SvnTagBranchListDialog(QDialog, Ui_SvnTagBranchListDialog):
    """
    Class implementing a dialog to show a list of tags or branches.
    """
    def __init__(self, vcs, parent=None):
        """
        Constructor
        
        @param vcs reference to the vcs object
        @param parent parent widget (QWidget)
        """
        super(SvnTagBranchListDialog, self).__init__(parent)
        self.setupUi(self)
        self.setWindowFlags(Qt.Window)
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
        
        self.vcs = vcs
        self.tagsList = None
        self.allTagsList = None
        
        self.tagList.headerItem().setText(self.tagList.columnCount(), "")
        self.tagList.header().setSortIndicator(3, Qt.AscendingOrder)
        
        self.process = QProcess()
        self.process.finished.connect(self.__procFinished)
        self.process.readyReadStandardOutput.connect(self.__readStdout)
        self.process.readyReadStandardError.connect(self.__readStderr)
        
        self.rx_list = QRegExp(
            r"""\w*\s*(\d+)\s+(\w+)\s+\d*\s*"""
            r"""((?:\w+\s+\d+|[0-9.]+\s+\w+)\s+[0-9:]+)\s+(.+)/\s*""")
        
    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.
        
        @param e close event (QCloseEvent)
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        e.accept()
        
    def start(self, path, tags, tagsList, allTagsList):
        """
        Public slot to start the svn status command.
        
        @param path name of directory to be listed (string)
        @param tags flag indicating a list of tags is requested
            (False = branches, True = tags)
        @param tagsList reference to string list receiving the tags
            (list of strings)
        @param allTagsList reference to string list all tags (list of strings)
        """
        self.errorGroup.hide()
        
        self.tagList.clear()
        
        self.intercept = False
        if not tags:
            self.setWindowTitle(self.tr("Subversion Branches List"))
        self.activateWindow()
        
        self.tagsList = tagsList
        self.allTagsList = allTagsList
        dname, fname = self.vcs.splitPath(path)
        
        self.process.kill()
        
        reposURL = self.vcs.svnGetReposName(dname)
        if reposURL is None:
            E5MessageBox.critical(
                self,
                self.tr("Subversion Error"),
                self.tr(
                    """The URL of the project repository could not be"""
                    """ retrieved from the working copy. The list operation"""
                    """ will be aborted"""))
            self.close()
            return
        
        args = []
        args.append('list')
        self.vcs.addArguments(args, self.vcs.options['global'])
        args.append('--verbose')
        
        if self.vcs.otherData["standardLayout"]:
            # determine the base path of the project in the repository
            rx_base = QRegExp('(.+)/(trunk|tags|branches).*')
            if not rx_base.exactMatch(reposURL):
                E5MessageBox.critical(
                    self,
                    self.tr("Subversion Error"),
                    self.tr(
                        """The URL of the project repository has an"""
                        """ invalid format. The list operation will"""
                        """ be aborted"""))
                return
            
            reposRoot = rx_base.cap(1)
            
            if tags:
                args.append("{0}/tags".format(reposRoot))
            else:
                args.append("{0}/branches".format(reposRoot))
            self.path = None
        else:
            reposPath, ok = QInputDialog.getText(
                self,
                self.tr("Subversion List"),
                self.tr("Enter the repository URL containing the tags"
                        " or branches"),
                QLineEdit.Normal,
                self.vcs.svnNormalizeURL(reposURL))
            if not ok:
                self.close()
                return
            if not reposPath:
                E5MessageBox.critical(
                    self,
                    self.tr("Subversion List"),
                    self.tr("""The repository URL is empty."""
                            """ Aborting..."""))
                self.close()
                return
            args.append(reposPath)
            self.path = reposPath
        
        self.process.setWorkingDirectory(dname)
        
        self.process.start('svn', args)
        procStarted = self.process.waitForStarted(5000)
        if not procStarted:
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            E5MessageBox.critical(
                self,
                self.tr('Process Generation Error'),
                self.tr(
                    'The process {0} could not be started. '
                    'Ensure, that it is in the search path.'
                ).format('svn'))
        else:
            self.inputGroup.setEnabled(True)
            self.inputGroup.show()
        
    def __finish(self):
        """
        Private slot called when the process finished or the user pressed the
        button.
        """
        if self.process is not None and \
           self.process.state() != QProcess.NotRunning:
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        self.buttonBox.button(QDialogButtonBox.Close).setFocus(
            Qt.OtherFocusReason)
        
        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()
        
        self.__resizeColumns()
        self.__resort()
        
    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.close()
        elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
            self.__finish()
        
    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.
        
        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__finish()
        
    def __resort(self):
        """
        Private method to resort the tree.
        """
        self.tagList.sortItems(
            self.tagList.sortColumn(),
            self.tagList.header().sortIndicatorOrder())
        
    def __resizeColumns(self):
        """
        Private method to resize the list columns.
        """
        self.tagList.header().resizeSections(QHeaderView.ResizeToContents)
        self.tagList.header().setStretchLastSection(True)
        
    def __generateItem(self, revision, author, date, name):
        """
        Private method to generate a tag item in the taglist.
        
        @param revision revision string (string)
        @param author author of the tag (string)
        @param date date of the tag (string)
        @param name name (path) of the tag (string)
        """
        itm = QTreeWidgetItem(self.tagList)
        itm.setData(0, Qt.DisplayRole, int(revision))
        itm.setData(1, Qt.DisplayRole, author)
        itm.setData(2, Qt.DisplayRole, date)
        itm.setData(3, Qt.DisplayRole, name)
        itm.setTextAlignment(0, Qt.AlignRight)
        
    def __readStdout(self):
        """
        Private slot to handle the readyReadStdout signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        self.process.setReadChannel(QProcess.StandardOutput)
        
        while self.process.canReadLine():
            s = str(self.process.readLine(),
                    Preferences.getSystem("IOEncoding"),
                    'replace')
            if self.rx_list.exactMatch(s):
                rev = "{0:6}".format(self.rx_list.cap(1))
                author = self.rx_list.cap(2)
                date = self.rx_list.cap(3)
                path = self.rx_list.cap(4)
                if path == ".":
                    continue
                self.__generateItem(rev, author, date, path)
                if not self.vcs.otherData["standardLayout"]:
                    path = self.path + '/' + path
                if self.tagsList is not None:
                    self.tagsList.append(path)
                if self.allTagsList is not None:
                    self.allTagsList.append(path)
        
    def __readStderr(self):
        """
        Private slot to handle the readyReadStderr signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.process is not None:
            self.errorGroup.show()
            s = str(self.process.readAllStandardError(),
                    Preferences.getSystem("IOEncoding"),
                    'replace')
            self.errors.insertPlainText(s)
            self.errors.ensureCursorVisible()
        
    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.
        
        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.Password)
        else:
            self.input.setEchoMode(QLineEdit.Normal)
        
    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the subversion process.
        """
        input = self.input.text()
        input += os.linesep
        
        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(input)
            self.errors.ensureCursorVisible()
        
        self.process.write(input)
        
        self.passwordCheckBox.setChecked(False)
        self.input.clear()
        
    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()
        
    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.
        
        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super(SvnTagBranchListDialog, self).keyPressEvent(evt)
Beispiel #56
0
class RarExtractor(Extractor):

    sig_entry_extracted = pyqtSignal(str, str)

    def __init__(self, filename: str, outdir: str) -> None:
        super().__init__()

        self._filename = os.path.abspath(filename)
        self._outdir = outdir

        self._process: Optional[QProcess] = None
        self._errors: List[str] = []

        self._result: Optional[ExtractorResult] = None

        self._output_state = State.HEADER

    def interrupt(self):
        if self._process is not None:
            self._process.terminate()
            # self._process.kill()

    def extract(self) -> ExtractorResult:
        try:
            self._start_extract(self._outdir)
            self._process.waitForFinished(-1)
            return self._result
        except Exception as err:
            message = "{}: failure when extracting archive".format(self._filename)
            logger.exception(message)
            return ExtractorResult.failure(message)

    def _start_extract(self, outdir: str) -> None:
        # The directory is already created in ArchiveExtractor
        # os.mkdir(outdir)
        assert os.path.isdir(outdir)

        program = "rar"
        argv = ["x", "-p-", "-c-", self._filename]
        # "-w" + outdir has no effect

        logger.debug("RarExtractorWorker: launching %s %s", program, argv)
        self._process = QProcess()
        self._process.setProgram(program)
        self._process.setArguments(argv)
        self._process.setWorkingDirectory(outdir)
        self._process.start()
        self._process.closeWriteChannel()

        self._process.readyReadStandardOutput.connect(self._on_ready_read_stdout)
        self._process.readyReadStandardError.connect(self._on_ready_read_stderr)
        self._process.errorOccurred.connect(self._on_error_occured)
        self._process.finished.connect(self._on_process_finished)

    def _on_process_finished(self, exit_code, exit_status):
        self._process.setCurrentReadChannel(QProcess.StandardOutput)
        for line in os.fsdecode(self._process.readAll().data()).splitlines():
            self._process_stdout(line)

        self._process.setCurrentReadChannel(QProcess.StandardError)
        for line in os.fsdecode(self._process.readAll().data()).splitlines():
            self._process_stderr(line)

        if self._errors != []:
            message = "RAR: " + "\n".join(self._errors)
        else:
            message = ""

        if exit_status != QProcess.NormalExit or exit_code != 0:
            logger.error("RarExtractorWorker: something went wrong: %s  %s", exit_code, exit_status)
            self._result = ExtractorResult.failure(message)
        else:
            logger.debug("RarExtractorWorker: finished successfully: %s  %s", exit_code, exit_status)
            self._result = ExtractorResult.success(message)

    def _on_error_occured(self, error) -> None:
        logger.error("RarExtractorWorker: an error occured: %s", error)
        self._result = ExtractorResult.failure("RarExtractorWorker: an error occured: {}".format(error))

    def _process_stdout(self, line):
        # print("stdout:", repr(line))

        if self._output_state == State.HEADER:
            if BEGIN_RX.match(line):
                self._output_state = State.FILELIST
        elif self._output_state == State.FILELIST:
            m = FILE_RX.match(line)
            if m:
                entry = m.group(1).rstrip(" ")
                self.sig_entry_extracted.emit(entry, os.path.join(self._outdir, entry))
            else:
                m = DIR_RX.match(line)
                if m:
                    entry = m.group(1).rstrip(" ")
                    self.sig_entry_extracted.emit(entry, os.path.join(self._outdir, entry))
                elif line == "":
                    pass  # ignore empty line at the start
                else:
                    # self._errors.append(line)
                    # self._output_state = State.RESULT
                    pass
        else:
            # self._errors.append(line)
            pass

    def _process_stderr(self, line):
        # print("stderr:", repr(line))
        if line:
            self._errors.append(line)

    def _on_ready_read_stdout(self) -> None:
        assert self._process is not None

        self._process.setCurrentReadChannel(QProcess.StandardOutput)
        while self._process.canReadLine():
            buf: QByteArray = self._process.readLine()
            line = os.fsdecode(buf.data())
            line = line.rstrip("\n")
            self._process_stdout(line)

    def _on_ready_read_stderr(self) -> None:
        assert self._process is not None

        self._process.setCurrentReadChannel(QProcess.StandardError)
        while self._process.canReadLine():
            buf: QByteArray = self._process.readLine()
            line = os.fsdecode(buf.data())
            line = line.rstrip("\n")
            self._process_stderr(line)