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
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))
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)
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)
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
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)
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)
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)
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()
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))
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 = ""
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()
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
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)
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)
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()
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)
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)
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)
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)
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()
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)
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)
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)
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)
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 = ""
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)
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)
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)
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)
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)
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 = ""
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
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)
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)
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)
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)
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()
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)
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)
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()
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)
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)