Sun, 31 Dec 2017 16:52:09 +0100
Updated copyright for 2018.
# -*- coding: utf-8 -*- # Copyright (c) 2014 - 2018 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing a dialog to show the output of the git status command process. """ from __future__ import unicode_literals try: str = unicode except NameError: pass import os import tempfile from PyQt5.QtCore import pyqtSlot, qVersion, Qt, QProcess, QTimer, QSize from PyQt5.QtGui import QTextCursor, QCursor from PyQt5.QtWidgets import QWidget, QDialogButtonBox, QMenu, QHeaderView, \ QTreeWidgetItem, QLineEdit, QInputDialog, QToolTip from E5Gui.E5Application import e5App from E5Gui import E5MessageBox from .Ui_GitStatusDialog import Ui_GitStatusDialog from .GitDiffHighlighter import GitDiffHighlighter from .GitDiffGenerator import GitDiffGenerator from .GitDiffParser import GitDiffParser from .GitUtilities import strToQByteArray import Preferences import UI.PixmapCache import Utilities 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("VcsPlugins", "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(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(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() try: self.lDiffHighlighter.regenerateRules() self.rDiffHighlighter.regenerateRules() except AttributeError: # backward compatibility pass 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 = ""