Commits

Steve Borho  committed b8599ff

enable change (chunk) selection within the commit tool (incomplete)

The commit tool currently ignores the chunk selection and commits all checked
or partially checked files as it always has.

  • Participants
  • Parent commits 069667c

Comments (0)

Files changed (4)

File tortoisehg/hgqt/commit.py

 
     @pyqtSlot(bool)
     def commitSetAction(self, refresh=False, actionName=None):
+        allowfolding = False
         if actionName:
             selectedAction = \
                 [act for act in self.mqgroup.actions() \
             elif curraction._name == 'commit':
                 refreshwctx = refresh and oldpctx is not None
                 self.stwidget.setPatchContext(None)
+                allowfolding = True
         if curraction._name in ('qref', 'amend'):
             if self.lastAction not in ('qref', 'amend'):
                 self.lastCommitMsg = self.msgte.text()
         else:
             if self.lastAction in ('qref', 'amend'):
                 self.setMessage(self.lastCommitMsg)
+        self.stwidget.fileview.enableDiffFolding(allowfolding)
         if refreshwctx:
             self.stwidget.refreshWctx()
         self.committb.setText(curraction._text)

File tortoisehg/hgqt/filedata.py

 # GNU General Public License version 2, incorporated herein by reference.
 
 import os
+import cStringIO
 
 from mercurial import error, match, patch, util, mdiff
 from mercurial import ui as uimod
+from hgext import record
 
 from tortoisehg.util import hglib, patchctx
 from tortoisehg.hgqt.i18n import _
     return False
 
 class FileData(object):
-    def __init__(self, ctx, ctx2, wfile, status=None):
+    def __init__(self, ctx, ctx2, wfile, status=None, changeselect=False):
         self.contents = None
         self.ucontents = None
         self.error = None
         self.diff = None
         self.flabel = u''
         self.elabel = u''
+        self.changes = None
         try:
-            self.readStatus(ctx, ctx2, wfile, status)
+            self.readStatus(ctx, ctx2, wfile, status, changeselect)
         except (EnvironmentError, error.LookupError), e:
             self.error = hglib.tounicode(str(e))
 
     def isValid(self):
         return self.error is None
 
-    def readStatus(self, ctx, ctx2, wfile, status):
+    def readStatus(self, ctx, ctx2, wfile, status, changeselect):
         def getstatus(repo, n1, n2, wfile):
             m = match.exact(repo.root, repo.getcwd(), [wfile])
             modified, added, removed = repo.status(n1, n2, match=m)[:3]
         newdate = util.datestr(ctx.date())
         olddate = util.datestr(ctx2.date())
         diffopts = patch.diffopts(repo.ui, {})
-        diffopts.git = False
+        diffopts.git = changeselect
         if isbfile:
             olddata += '\0'
             newdata += '\0'
 
-        self.diff = 'diff -r %s -r %s %s\n' % (ctx, ctx2, oldname)
-        self.diff += mdiff.unidiff(olddata, olddate, newdata, newdate,
-                                   oldname, wfile, opts=diffopts)
+        diff = 'diff -r %s -r %s %s\n' % (ctx, ctx2, oldname)
+        diff += mdiff.unidiff(olddata, olddate, newdata, newdate, oldname,
+                              wfile, opts=diffopts)
+        if changeselect:
+            # feed diffs through record.parsepatch() for more fine grained
+            # chunk selection
+            self.changes = record.parsepatch(cStringIO.StringIO(diff))[0]
+            self.changes.excludecount = 0
+            values = []
+            lines = 0
+            for chunk in self.changes.hunks:
+                buf = cStringIO.StringIO()
+                chunk.write(buf)
+                chunk.excluded = False
+                chunk.lineno = lines
+                val = buf.getvalue()
+                values.append(val)
+                lines += len(val.splitlines())
+            self.diff = ''.join(values)
+        else:
+            self.diff = diff

File tortoisehg/hgqt/fileview.py

     showMessage = pyqtSignal(QString)
     revisionSelected = pyqtSignal(int)
     shelveToolExited = pyqtSignal()
+    newChunkList = pyqtSignal(QString, object)
+    chunkSelectionChanged = pyqtSignal(QString, bool)
 
     grepRequested = pyqtSignal(unicode, dict)
     """Emitted (pattern, opts) when user request to search changelog"""
 
         self.repo = repo
         self._diffs = []
+        self.changes = None
+        self.folddiffs = False
+        self.chunkatline = {}
 
         self.topLayout = QVBoxLayout()
 
         self.sci.setAnnotationEnabled(False)
         self.sci.setContextMenuPolicy(Qt.CustomContextMenu)
         self.sci.customContextMenuRequested.connect(self.menuRequest)
+        self.sci.SCN_MARGINCLICK.connect(self.marginClicked)
 
         self.blk.linkScrollBar(self.sci.verticalScrollBar())
         self.blk.setVisible(False)
         self.repo = repo
         self.sci.repo = repo
 
+    def enableDiffFolding(self, enable):
+        'Enable the use of a folding margin when a diff view is active'
+        # Should only be called with True from the commit tool when it is in
+        # a 'commit' mode and False for other uses
+        if not enable and self.folddiffs:
+            self.sci.clearFolds()
+        self.folddiffs = enable
+        self._showFoldMargin(enable)
+
+    def updateChunk(self, chunk, exclude):
+        'change chunk exclusion state, update display when necessary'
+        # TODO: create a decent QsciStyle for these annotations
+        if exclude:
+            if self.changes.excludecount == 0:
+                self.sci.annotate(chunk.lineno,
+                                _('folded changes are excluded from commit'), 4)
+            if chunk.excluded:
+                return
+            chunk.excluded = True
+            self.changes.excludecount += 1
+            uf = hglib.tounicode(self._filename)
+            state = self.changes.excludecount < len(self.changes.hunks)
+            self.chunkSelectionChanged.emit(uf, state)
+        else:
+            self.sci.clearAnnotations(chunk.lineno)
+            if not chunk.excluded:
+                return
+            chunk.excluded = False
+            self.changes.excludecount -= 1
+            uf = hglib.tounicode(self._filename)
+            self.chunkSelectionChanged.emit(uf, True)
+
+    def updateFolds(self):
+        'should be called after chunk states are modified programatically'
+        self.sci.clearFolds()
+        if self.changes is None:
+            return
+        folds = []
+        for chunk in self.changes.hunks:
+            if chunk.excluded:
+                folds.append(chunk.lineno)
+        self.sci.setContractedFolds(folds)
+
     @pyqtSlot(QAction)
     def setMode(self, action):
         'One of the mode toolbar buttons has been toggled'
         self.actionPrevDiff.setEnabled(False)
 
         self.maxWidth = 0
+        self.changes = None
+        self.chunkatline = {}
+        self._showFoldMargin(False)
         self.sci.showHScrollBar(False)
 
+    def _showFoldMargin(self, show):
+        'toggle the display of the diff folding margin'
+        self.sci.setFolding(show and qsci.BoxedTreeFoldStyle or qsci.NoFoldStyle, 3)
+        self.sci.setMarginSensitivity(3, show)
+
+    @pyqtSlot(int, int, int)
+    def marginClicked(self, pos, modifiers, margin):
+        'raw margin clicked event, received before folding has responded'
+        line, index = self.sci.lineIndexFromPosition(pos)
+        if line not in self.chunkatline:
+            return
+        assert margin == 3 and index == 0
+        chunk = self.chunkatline[line]
+        self.updateChunk(chunk, not chunk.excluded)
+
     def displayFile(self, filename=None, status=None):
         if isinstance(filename, (unicode, QString)):
             filename = hglib.fromunicode(filename)
             ctx2 = self._ctx.p1()
         else:
             ctx2 = self._ctx.p2()
-        fd = filedata.FileData(self._ctx, ctx2, filename, status)
+        cs = (self._mode == DiffMode and self.folddiffs)
+        fd = filedata.FileData(self._ctx, ctx2, filename, status, cs)
 
         if fd.elabel:
             self.extralabel.setText(fd.elabel)
             self.sci.setLexer(lexer)
             if lexer is None:
                 self.sci.setFont(qtlib.getfont('fontlog').font())
-            # trim first three lines, for example:
-            # diff -r f6bfc41af6d7 -r c1b18806486d tortoisehg/hgqt/thgrepo.py
-            # --- a/tortoisehg/hgqt/thgrepo.py
-            # +++ b/tortoisehg/hgqt/thgrepo.py
-            if fd.diff:
+            if fd.changes:
+                self._showFoldMargin(True)
+                self.changes = fd.changes
+                for chunk in self.changes.hunks:
+                    self.chunkatline[chunk.lineno] = chunk
+                self.sci.setText(hglib.tounicode(fd.diff))
+            elif fd.diff:
+                # trim first three lines, for example:
+                # diff -r f6bfc41af6d7 -r c1b18806486d tortoisehg/hgqt/mq.py
+                # --- a/tortoisehg/hgqt/mq.py
+                # +++ b/tortoisehg/hgqt/mq.py
                 out = fd.diff.split('\n', 3)
                 if len(out) == 4:
                     self.sci.setText(hglib.tounicode(out[3]))
         uf = hglib.tounicode(filename)
         uc = hglib.tounicode(fd.contents) or ''
         self.fileDisplayed.emit(uf, uc)
+        if self.changes:
+            self.newChunkList.emit(uf, self.changes)
 
         if self._mode != DiffMode:
             self.blk.setVisible(True)

File tortoisehg/hgqt/status.py

         self.pctx = None
         self.savechecks = True
         self.refthread = None
+        self.partials = {}
 
         # determine the user configured status colors
         # (in the future, we could support full rich-text tags)
         self.fileview.linkActivated.connect(self.linkActivated)
         self.fileview.fileDisplayed.connect(self.fileDisplayed)
         self.fileview.shelveToolExited.connect(self.refreshWctx)
+        self.fileview.newChunkList.connect(self.updatePartials)
+        self.fileview.chunkSelectionChanged.connect(self.updateCheckbox)
         self.fileview.setContext(self.repo[None])
         self.fileview.setMinimumSize(QSize(16, 16))
         vbox.addWidget(self.fileview, 1)
         self.fileview.saveSettings(qs, prefix+'/fileview')
         qs.setValue(prefix+'/state', self.split.saveState())
 
+    @pyqtSlot(QString, object)
+    def updatePartials(self, wfile, changes):
+        # remove files from the partials dictionary if they are not partial
+        # selections, in order to simplify refresh.
+        dels = []
+        for file, oldchanges in self.partials.iteritems():
+            if oldchanges.excludecount == 0:
+                dels.append(file)
+            elif oldchanges.excludecount == len(oldchanges.hunks):
+                dels.append(file)
+        for file in dels:
+            del self.partials[file]
+
+        if not wfile or not changes:
+            return
+
+        wfile = unicode(wfile)
+        if wfile in self.partials:
+            # merge selection state from prev hunk list to new hunk list
+            oldchanges = self.partials[wfile]
+            for chunk in changes.hunks:
+                for ochunk in oldchanges.hunks[:]:
+                    if ochunk.fromline < chunk.fromline:
+                        oldchanges.hunks.remove(ochunk)
+                    elif ochunk.fromline == chunk.fromline:
+                        self.fileview.updateChunk(chunk, ochunk.excluded)
+                    else:
+                        break
+        else:
+            # the file was not in the partials dictionary, so it is either
+            # checked (all changes enabled) or unchecked (all changes
+            # excluded).
+            if wfile not in self.getChecked():
+                for chunk in changes.hunks:
+                    self.fileview.updateChunk(chunk, True)
+        self.fileview.updateFolds()
+        self.partials[wfile] = changes
+
+    @pyqtSlot(QString, bool)
+    def updateCheckbox(self, wfile, state):
+        'checkbox state has changed via chunk selection'
+        # mark row as checked if any chunks are enabled
+        wfile = unicode(wfile)
+        self.tv.model().check([wfile], state, False)
+
     @pyqtSlot(QPoint, object)
     def onMenuRequest(self, point, selected):
         menu = self.actions.makeMenu(selected)
                        checked, self, checkable=self.checkable,
                        defcheck=self.defcheck)
         if self.checkable:
-            tm.checkToggled.connect(self.updateCheckCount)
+            tm.checkToggled.connect(self.checkToggled)
+            tm.checkCountChanged.connect(self.updateCheckCount)
 
         oldtm = self.tv.model()
         self.tv.setModel(tm)
             model.setFilter(match)
             self.tv.enablefilterpalette(bool(match))
 
+    @pyqtSlot()
     def updateCheckCount(self):
+        'user has toggled one or more checkboxes, update counts and checkall'
         model = self.tv.model()
         if model:
             model.checkCount = len(self.getChecked())
                 state = Qt.PartiallyChecked
             self.checkAllNoneBtn.setCheckState(state)
 
+    @pyqtSlot(QString, bool)
+    def checkToggled(self, wfile, checked):
+        'user has toggled a checkbox, update partial chunk selection status'
+        wfile = unicode(wfile)
+        if wfile in self.partials:
+            if wfile == hglib.tounicode(self.fileview._filename):
+                for chunk in self.partials[wfile].hunks:
+                    self.fileview.updateChunk(chunk, not checked)
+                self.fileview.updateFolds()
+            else:
+                del self.partials[wfile]
+
     def checkAll(self):
         model = self.tv.model()
         if model:
         self._paletteswitcher.enablefilterpalette(enable)
 
 class WctxModel(QAbstractTableModel):
-    checkToggled = pyqtSignal()
+    checkCountChanged = pyqtSignal()
+    checkToggled = pyqtSignal(QString, bool)
 
     def __init__(self, wctx, ms, pctx, savechecks, opts, checked, parent,
                  checkable=True, defcheck='MAR!S'):
         QAbstractTableModel.__init__(self, parent)
+        self.partials = parent.partials
         self.checkCount = 0
         rows = []
         nchecked = {}
             return 0 # no child
         return len(self.rows)
 
-    def check(self, files, state=True):
+    def check(self, files, state=True, emit=True):
         for f in files:
             self.checked[f] = state
+            if emit:
+                self.checkToggled.emit(f, state)
         self.layoutChanged.emit()
-        self.checkToggled.emit()
-        
+        self.checkCountChanged.emit()
+
     def checkAll(self, state):
         for data in self.rows:
             self.checked[data[0]] = state
+            self.checkToggled.emit(data[3], state)
         self.layoutChanged.emit()
-        self.checkToggled.emit()
+        self.checkCountChanged.emit()
 
     def columnCount(self, parent):
         if parent.isValid():
         path, status, mst, upath, ext, sz = self.rows[index.row()]
         if index.column() == COL_PATH:
             if role == Qt.CheckStateRole and self.checkable:
-                # also Qt.PartiallyChecked
+                if upath in self.partials:
+                    changes = self.partials[upath]
+                    if changes.excludecount == 0:
+                        return Qt.Checked
+                    elif changes.excludecount == len(changes.hunks):
+                        return Qt.Unchecked
+                    else:
+                        return Qt.PartiallyChecked
                 if self.checked[path]:
                     return Qt.Checked
                 else:
         for index in indexes:
             assert index.isValid()
             fname = self.rows[index.row()][COL_PATH]
+            uname = self.rows[index.row()][COL_PATH_DISPLAY]
             self.checked[fname] = not self.checked[fname]
+            self.checkToggled.emit(uname, self.checked[fname])
         self.layoutChanged.emit()
-        self.checkToggled.emit()
+        self.checkCountChanged.emit()
 
     def sort(self, col, order):
         self.layoutAboutToBeChanged.emit()