[PATCH 4 of 5] record: add crecord's ui logic to core

Laurent Charignon lcharignon at fb.com
Thu Mar 12 17:00:44 CDT 2015


# HG changeset patch
# User Laurent Charignon <lcharignon at fb.com>
# Date 1426195151 25200
#      Thu Mar 12 14:19:11 2015 -0700
# Node ID 80306e0957be92b28d1d7ce02450b9e5e200dabd
# Parent  eb5ff33e1774cd9897c61919dbc8484eab1705b8
record: add crecord's ui logic to core

Code adapted from https://bitbucket.org/edgimar/crecord/src to respect
coding convention an record's interface

diff --git a/mercurial/crecord.py b/mercurial/crecord.py
new file mode 100644
--- /dev/null
+++ b/mercurial/crecord.py
@@ -0,0 +1,1614 @@
+# stuff related specifically to patch manipulation / parsing
+from mercurial.i18n import _
+
+from mercurial import patch as patchmod
+from mercurial import util
+from mercurial import demandimport
+demandimport.ignore.append('mercurial.encoding')
+try:
+    import mercurial.encoding as encoding
+    code = encoding.encoding
+except ImportError:
+    encoding = util
+    code = encoding._encoding
+
+import os
+import re
+import sys
+import fcntl
+import struct
+import termios
+import signal
+import tempfile
+import locale
+import cStringIO
+# This is required for ncurses to display non-ASCII characters in default user
+# locale encoding correctly.  --immerrr
+locale.setlocale(locale.LC_ALL, '')
+# os.name is one of: 'posix', 'nt', 'dos', 'os2', 'mac', or 'ce'
+if os.name == 'posix':
+    import curses
+else:
+    # I have no idea if wcurses works with crecord...
+    import wcurses as curses
+
+try:
+    curses
+except NameError:
+    raise util.Abort(
+        _('the python curses/wcurses module is not available/installed'))
+
+
+orig_stdout = sys.__stdout__ # used by gethw()
+
+
+
+class patchnode(object):
+    """abstract class for patch graph nodes
+    (i.e. patchroot, header, hunk, hunkline)
+    """
+
+    def firstchild(self):
+        raise NotImplementedError("method must be implemented by subclass")
+
+    def lastchild(self):
+        raise NotImplementedError("method must be implemented by subclass")
+
+    def allchildren(self):
+        "Return a list of all of the direct children of this node"
+        raise NotImplementedError("method must be implemented by subclass")
+    def nextsibling(self):
+        """
+        Return the closest next item of the same type where there are no items
+        of different types between the current item and this closest item.
+        If no such item exists, return None.
+
+        """
+        raise NotImplementedError("method must be implemented by subclass")
+
+    def prevsibling(self):
+        """
+        Return the closest previous item of the same type where there are no
+        items of different types between the current item and this closest item.
+        If no such item exists, return None.
+
+        """
+        raise NotImplementedError("method must be implemented by subclass")
+
+    def parentitem(self):
+        raise NotImplementedError("method must be implemented by subclass")
+
+
+    def nextitem(self, constrainlevel=True, skipfolded=True):
+        """
+        If constrainLevel == True, return the closest next item
+        of the same type where there are no items of different types between
+        the current item and this closest item.
+
+        If constrainLevel == False, then try to return the next item
+        closest to this item, regardless of item's type (header, hunk, or
+        HunkLine).
+
+        If skipFolded == True, and the current item is folded, then the child
+        items that are hidden due to folding will be skipped when determining
+        the next item.
+
+        If it is not possible to get the next item, return None.
+
+        """
+        try:
+            itemfolded = self.folded
+        except AttributeError:
+            itemfolded = False
+        if constrainlevel:
+            return self.nextsibling()
+        elif skipfolded and itemfolded:
+            nextitem = self.nextsibling()
+            if nextitem is None:
+                try:
+                    nextitem = self.parentitem().nextsibling()
+                except AttributeError:
+                    nextitem = None
+            return nextitem
+        else:
+            # try child
+            item = self.firstchild()
+            if item is not None:
+                return item
+
+            # else try next sibling
+            item = self.nextsibling()
+            if item is not None:
+                return item
+
+            try:
+                # else try parent's next sibling
+                item = self.parentitem().nextsibling()
+                if item is not None:
+                    return item
+
+                # else return grandparent's next sibling (or None)
+                return self.parentitem().parentitem().nextsibling()
+
+            except AttributeError: # parent and/or grandparent was None
+                return None
+
+    def previtem(self, constrainlevel=True, skipfolded=True):
+        """
+        If constrainLevel == True, return the closest previous item
+        of the same type where there are no items of different types between
+        the current item and this closest item.
+
+        If constrainLevel == False, then try to return the previous item
+        closest to this item, regardless of item's type (header, hunk, or
+        HunkLine).
+
+        If skipFolded == True, and the current item is folded, then the items
+        that are hidden due to folding will be skipped when determining the
+        next item.
+
+        If it is not possible to get the previous item, return None.
+
+        """
+        if constrainlevel:
+            return self.prevsibling()
+        else:
+            # try previous sibling's last child's last child,
+            # else try previous sibling's last child, else try previous sibling
+            prevsibling = self.prevsibling()
+            if prevsibling is not None:
+                prevsiblinglastchild = prevsibling.lastchild()
+                if ((prevsiblinglastchild is not None) and
+                    not prevsibling.folded):
+                    prevsiblinglclc = prevsiblinglastchild.lastchild()
+                    if ((prevsiblinglclc is not None) and
+                        not prevsiblinglastchild.folded):
+                        return prevsiblinglclc
+                    else:
+                        return prevsiblinglastchild
+                else:
+                    return prevsibling
+
+            # try parent (or None)
+            return self.parentitem()
+
+class patch(patchnode, list): # todo: rename patchroot
+    """
+    list of header objects representing the patch.
+
+    """
+    def __init__(self, headerlist):
+        self.extend(headerlist)
+        # add parent patch object reference to each header
+        for header in self:
+            header.patch = self
+
+class uiheader(patchnode):
+    """patch header
+
+    xxx shoudn't we move this to mercurial/patch.py ?
+    """
+
+    def __init__(self, header):
+        self.nonuiheader = header
+        # flag to indicate whether to apply this chunk
+        self.applied = True
+        # flag which only affects the status display indicating if a node's
+        # children are partially applied (i.e. some applied, some not).
+        self.partial = False
+
+        # flag to indicate whether to display as folded/unfolded to user
+        self.folded = True
+
+        # list of all headers in patch
+        self.patch = None
+
+        # flag is False if this header was ever unfolded from initial state
+        self.neverunfolded = True
+        self.hunks = [uihunk(h, self) for h in self.hunks]
+
+
+    def prettystr(self):
+        x = cStringIO.StringIO()
+        self.pretty(x)
+        return x.getvalue()
+
+    def nextsibling(self):
+        numheadersinpatch = len(self.patch)
+        indexofthisheader = self.patch.index(self)
+
+        if indexofthisheader < numheadersinpatch - 1:
+            nextheader = self.patch[indexofthisheader + 1]
+            return nextheader
+        else:
+            return None
+
+    def prevsibling(self):
+        indexofthisheader = self.patch.index(self)
+        if indexofthisheader > 0:
+            previousheader = self.patch[indexofthisheader - 1]
+            return previousheader
+        else:
+            return None
+
+    def parentitem(self):
+        """
+        there is no 'real' parent item of a header that can be selected,
+        so return None.
+        """
+        return None
+
+    def firstchild(self):
+        "return the first child of this item, if one exists.  otherwise None."
+        if len(self.hunks) > 0:
+            return self.hunks[0]
+        else:
+            return None
+
+    def lastchild(self):
+        "return the last child of this item, if one exists.  otherwise None."
+        if len(self.hunks) > 0:
+            return self.hunks[-1]
+        else:
+            return None
+
+    def allchildren(self):
+        "return a list of all of the direct children of this node"
+        return self.hunks
+
+    def __getattr__(self, name):
+        return getattr(self.nonuiheader, name)
+
+class uihunkline(patchnode):
+    "represents a changed line in a hunk"
+    def __init__(self, linetext, hunk):
+        self.linetext = linetext
+        self.applied = True
+        # the parent hunk to which this line belongs
+        self.hunk = hunk
+        # folding lines currently is not used/needed, but this flag is needed
+        # in the previtem method.
+        self.folded = False
+
+    def prettystr(self):
+        return self.linetext
+
+    def nextsibling(self):
+        numlinesinhunk = len(self.hunk.changedlines)
+        indexofthisline = self.hunk.changedlines.index(self)
+
+        if (indexofthisline < numlinesinhunk - 1):
+            nextline = self.hunk.changedlines[indexofthisline + 1]
+            return nextline
+        else:
+            return None
+
+    def prevsibling(self):
+        indexofthisline = self.hunk.changedlines.index(self)
+        if indexofthisline > 0:
+            previousline = self.hunk.changedlines[indexofthisline - 1]
+            return previousline
+        else:
+            return None
+
+    def parentitem(self):
+        "return the parent to the current item"
+        return self.hunk
+
+    def firstchild(self):
+        "return the first child of this item, if one exists.  otherwise None."
+        # hunk-lines don't have children
+        return None
+
+    def lastchild(self):
+        "return the last child of this item, if one exists.  otherwise None."
+        # hunk-lines don't have children
+        return None
+
+class uihunk(patchnode):
+    """ui patch hunk, wraps a hunk and keep track of ui behavior """
+    maxcontext = 3
+
+    def __init__(self, hunk, header):
+        self._hunk = hunk
+        self.changedlines = [uihunkline(line, self) for line in hunk.hunk]
+        self.header = header
+        # used at end for detecting how many removed lines were un-applied
+        self.originalremoved = self.removed
+
+        # flag to indicate whether to display as folded/unfolded to user
+        self.folded = True
+        # flag to indicate whether to apply this chunk
+        self.applied = True
+        # flag which only affects the status display indicating if a node's
+        # children are partially applied (i.e. some applied, some not).
+        self.partial = False
+
+    def nextsibling(self):
+        numhunksinheader = len(self.header.hunks)
+        indexofthishunk = self.header.hunks.index(self)
+
+        if (indexofthishunk < numhunksinheader - 1):
+            nexthunk = self.header.hunks[indexofthishunk + 1]
+            return nexthunk
+        else:
+            return None
+
+    def prevsibling(self):
+        indexofthishunk = self.header.hunks.index(self)
+        if indexofthishunk > 0:
+            previoushunk = self.header.hunks[indexofthishunk - 1]
+            return previoushunk
+        else:
+            return None
+
+    def parentitem(self):
+        "return the parent to the current item"
+        return self.header
+
+    def firstchild(self):
+        "return the first child of this item, if one exists.  otherwise None."
+        if len(self.changedlines) > 0:
+            return self.changedlines[0]
+        else:
+            return None
+
+    def lastchild(self):
+        "return the last child of this item, if one exists.  otherwise None."
+        if len(self.changedlines) > 0:
+            return self.changedlines[-1]
+        else:
+            return None
+
+    def allchildren(self):
+        "return a list of all of the direct children of this node"
+        return self.changedlines
+    def countchanges(self):
+        """changedlines -> (n+,n-)"""
+        add = len([l for l in self.changedlines if l.applied
+                   and l.prettystr()[0] == '+'])
+        rem = len([l for l in self.changedlines if l.applied
+                   and l.prettystr()[0] == '-'])
+        return add, rem
+
+    def getfromtoline(self):
+        # calculate the number of removed lines converted to context lines
+        removedconvertedtocontext = self.originalremoved - self.removed
+
+        contextlen = (len(self.before) + len(self.after) +
+                      removedconvertedtocontext)
+        if self.after and self.after[-1] == '\\ no newline at end of file\n':
+            contextlen -= 1
+        fromlen = contextlen + self.removed
+        tolen = contextlen + self.added
+
+        # diffutils manual, section "2.2.2.2 detailed description of unified
+        # format": "an empty hunk is considered to end at the line that
+        # precedes the hunk."
+        #
+        # so, if either of hunks is empty, decrease its line start. --immerrr
+        # but only do this if fromline > 0, to avoid having, e.g fromline=-1.
+        fromline, toline = self.fromline, self.toline
+        if fromline != 0:
+            if fromlen == 0:
+                fromline -= 1
+            if tolen == 0:
+                toline -= 1
+
+        fromtoline = '@@ -%d,%d +%d,%d @@%s\n' % (
+            fromline, fromlen, toline, tolen,
+            self.proc and (' ' + self.proc))
+        return fromtoline
+
+    def write(self, fp):
+        # updated self.added/removed, which are used by getfromtoline()
+        self.added, self.removed = self.countchanges()
+        fp.write(self.getfromtoline())
+
+        hunklinelist = []
+        # add the following to the list: (1) all applied lines, and
+        # (2) all unapplied removal lines (convert these to context lines)
+        for changedline in self.changedlines:
+            changedlinestr = changedline.prettystr()
+            if changedline.applied:
+                hunklinelist.append(changedlinestr)
+            elif changedlinestr[0] == "-":
+                hunklinelist.append(" " + changedlinestr[1:])
+
+        fp.write(''.join(self.before + hunklinelist + self.after))
+
+    pretty = write
+
+    def prettystr(self):
+        x = cStringIO.StringIO()
+        self.pretty(x)
+        return x.getvalue()
+
+    def __getattr__(self, name):
+        return getattr(self._hunk, name)
+    def __repr__(self):
+        return '<hunk %r@%d>' % (self.filename(), self.fromline)
+
+def filterpatch(ui, chunks, chunk_selector):
+    """interactively filter patch chunks into applied-only chunks"""
+
+    chunks = list(chunks)
+    # convert chunks list into structure suitable for displaying/modifying
+    # with curses.  create a list of headers only.
+    headers = [c for c in chunks if isinstance(c, patchmod.header)]
+
+    # if there are no changed files
+    if len(headers) == 0:
+        return []
+    uiheaders = [uiheader(h) for h in headers]
+    # let user choose headers/hunks/lines, and mark their applied flags
+    # accordingly
+    chunk_selector(uiheaders, ui)
+    appliedhunklist = []
+    for hdr in uiheaders:
+        if (hdr.applied and
+            (hdr.special() or len([h for h in hdr.hunks if h.applied]) > 0)):
+            appliedhunklist.append(hdr)
+            fixoffset = 0
+            for hnk in hdr.hunks:
+                if hnk.applied:
+                    appliedhunklist.append(hnk)
+                    # adjust the 'to'-line offset of the hunk to be correct
+                    # after de-activating some of the other hunks for this file
+                    if fixoffset:
+                        #hnk = copy.copy(hnk) # necessary??
+                        hnk.toline += fixoffset
+                else:
+                    fixoffset += hnk.removed - hnk.added
+
+    return appliedhunklist
+
+
+
+def gethw():
+    """
+    magically get the current height and width of the window (without initscr)
+
+    this is a rip-off of a rip-off - taken from the bpython code.  it is
+    useful / necessary because otherwise curses.initscr() must be called,
+    which can leave the terminal in a nasty state after exiting.
+
+    """
+    h, w = struct.unpack(
+        "hhhh", fcntl.ioctl(orig_stdout, termios.TIOCGWINSZ, "\000"*8))[0:2]
+    return h, w
+
+
+def chunkselector(headerlist, ui):
+    """
+    curses interface to get selection of chunks, and mark the applied flags
+    of the chosen chunks.
+
+    """
+    chunkselector = curseschunkselector(headerlist, ui)
+    curses.wrapper(chunkselector.main)
+
+def testdecorator(testfn, f):
+    def u(*args, **kwargs):
+        return f(testfn, *args, **kwargs)
+    return u
+
+def testchunkselector(testfn, headerlist, ui):
+    """
+    test interface to get selection of chunks, and mark the applied flags
+    of the chosen chunks.
+
+    """
+    chunkselector = curseschunkselector(headerlist, ui)
+    if testfn and os.path.exists(testfn):
+        testf = open(testfn)
+        testcommands = map(lambda x: x.rstrip('\n'), testf.readlines())
+        testf.close()
+        while True:
+            if chunkselector.handlekeypressed(testcommands.pop(0), test=True):
+                break
+
+class curseschunkselector(object):
+    def __init__(self, headerlist, ui):
+        # put the headers into a patch object
+        self.headerlist = patch(headerlist)
+
+        self.ui = ui
+
+        # list of all chunks
+        self.chunklist = []
+        for h in headerlist:
+            self.chunklist.append(h)
+            self.chunklist.extend(h.hunks)
+
+        # dictionary mapping (fgcolor, bgcolor) pairs to the
+        # corresponding curses color-pair value.
+        self.colorpairs = {}
+        # maps custom nicknames of color-pairs to curses color-pair values
+        self.colorpairnames = {}
+
+        # the currently selected header, hunk, or hunk-line
+        self.currentselecteditem = self.headerlist[0]
+
+        # updated when printing out patch-display -- the 'lines' here are the
+        # line positions *in the pad*, not on the screen.
+        self.selecteditemstartline = 0
+        self.selecteditemendline = None
+
+        # define indentation levels
+        self.headerindentnumchars = 0
+        self.hunkindentnumchars = 3
+        self.hunklineindentnumchars = 6
+
+        # the first line of the pad to print to the screen
+        self.firstlineofpadtoprint = 0
+
+        # keeps track of the number of lines in the pad
+        self.numpadlines = None
+
+        self.numstatuslines = 2
+
+        # keep a running count of the number of lines printed to the pad
+        # (used for determining when the selected item begins/ends)
+        self.linesprintedtopadsofar = 0
+
+        # the first line of the pad which is visible on the screen
+        self.firstlineofpadtoprint = 0
+
+        # stores optional text for a commit comment provided by the user
+        self.commenttext = ""
+
+        # if the last 'toggle all' command caused all changes to be applied
+        self.waslasttoggleallapplied = True
+
+    def uparrowevent(self):
+        """
+        try to select the previous item to the current item that has the
+        most-indented level.  for example, if a hunk is selected, try to select
+        the last hunkline of the hunk prior to the selected hunk.  or, if
+        the first hunkline of a hunk is currently selected, then select the
+        hunk itself.
+
+        if the currently selected item is already at the top of the screen,
+        scroll the screen down to show the new-selected item.
+
+        """
+        currentitem = self.currentselecteditem
+
+        nextitem = currentitem.previtem(constrainlevel=False)
+
+        if nextitem is None:
+            # if no parent item (i.e. currentitem is the first header), then
+            # no change...
+            nextitem = currentitem
+
+        self.currentselecteditem = nextitem
+
+    def uparrowshiftevent(self):
+        """
+        select (if possible) the previous item on the same level as the
+        currently selected item.  otherwise, select (if possible) the
+        parent-item of the currently selected item.
+
+        if the currently selected item is already at the top of the screen,
+        scroll the screen down to show the new-selected item.
+
+        """
+        currentitem = self.currentselecteditem
+        nextitem = currentitem.previtem()
+        # if there's no previous item on this level, try choosing the parent
+        if nextitem is None:
+            nextitem = currentitem.parentitem()
+        if nextitem is None:
+            # if no parent item (i.e. currentitem is the first header), then
+            # no change...
+            nextitem = currentitem
+
+        self.currentselecteditem = nextitem
+
+    def downarrowevent(self):
+        """
+        try to select the next item to the current item that has the
+        most-indented level.  for example, if a hunk is selected, select
+        the first hunkline of the selected hunk.  or, if the last hunkline of
+        a hunk is currently selected, then select the next hunk, if one exists,
+        or if not, the next header if one exists.
+
+        if the currently selected item is already at the bottom of the screen,
+        scroll the screen up to show the new-selected item.
+
+        """
+        #self.startprintline += 1 #debug
+        currentitem = self.currentselecteditem
+
+        nextitem = currentitem.nextitem(constrainlevel=False)
+        # if there's no next item, keep the selection as-is
+        if nextitem is None:
+            nextitem = currentitem
+
+        self.currentselecteditem = nextitem
+
+    def downarrowshiftevent(self):
+        """
+        if the cursor is already at the bottom chunk, scroll the screen up and
+        move the cursor-position to the subsequent chunk.  otherwise, only move
+        the cursor position down one chunk.
+
+        """
+        # todo: update docstring
+
+        currentitem = self.currentselecteditem
+        nextitem = currentitem.nextitem()
+        # if there's no previous item on this level, try choosing the parent's
+        # nextitem.
+        if nextitem is None:
+            try:
+                nextitem = currentitem.parentitem().nextitem()
+            except AttributeError:
+                # parentitem returned None, so nextitem() can't be called
+                nextitem = None
+        if nextitem is None:
+            # if no next item on parent-level, then no change...
+            nextitem = currentitem
+
+        self.currentselecteditem = nextitem
+
+    def rightarrowevent(self):
+        """
+        select (if possible) the first of this item's child-items.
+
+        """
+        currentitem = self.currentselecteditem
+        nextitem = currentitem.firstchild()
+
+        # turn off folding if we want to show a child-item
+        if currentitem.folded:
+            self.togglefolded(currentitem)
+
+        if nextitem is None:
+            # if no next item on parent-level, then no change...
+            nextitem = currentitem
+
+        self.currentselecteditem = nextitem
+
+    def leftarrowevent(self):
+        """
+        if the current item can be folded (i.e. it is an unfolded header or
+        hunk), then fold it.  otherwise try select (if possible) the parent
+        of this item.
+
+        """
+        currentitem = self.currentselecteditem
+
+        # try to fold the item
+        if not isinstance(currentitem, uihunkline):
+            if not currentitem.folded:
+                self.togglefolded(item=currentitem)
+                return
+
+        # if it can't be folded, try to select the parent item
+        nextitem = currentitem.parentitem()
+
+        if nextitem is None:
+            # if no item on parent-level, then no change...
+            nextitem = currentitem
+            if not nextitem.folded:
+                self.togglefolded(item=nextitem)
+
+        self.currentselecteditem = nextitem
+
+    def leftarrowshiftevent(self):
+        """
+        select the header of the current item (or fold current item if the
+        current item is already a header).
+
+        """
+        currentitem = self.currentselecteditem
+
+        if isinstance(currentitem, uiheader):
+            if not currentitem.folded:
+                self.togglefolded(item=currentitem)
+                return
+
+        # select the parent item recursively until we're at a header
+        while True:
+            nextitem = currentitem.parentitem()
+            if nextitem is None:
+                break
+            else:
+                currentitem = nextitem
+
+        self.currentselecteditem = currentitem
+
+    def updatescroll(self):
+        "scroll the screen to fully show the currently-selected"
+        selstart = self.selecteditemstartline
+        selend = self.selecteditemendline
+        #selnumlines = selend - selstart
+        padstart = self.firstlineofpadtoprint
+        padend = padstart + self.yscreensize - self.numstatuslines - 1
+        # 'buffered' pad start/end values which scroll with a certain
+        # top/bottom context margin
+        padstartbuffered = padstart + 3
+        padendbuffered = padend - 3
+
+        if selend > padendbuffered:
+            self.scrolllines(selend - padendbuffered)
+        elif selstart < padstartbuffered:
+            # negative values scroll in pgup direction
+            self.scrolllines(selstart - padstartbuffered)
+
+
+    def scrolllines(self, numlines):
+        "scroll the screen up (down) by numlines when numlines >0 (<0)."
+        self.firstlineofpadtoprint += numlines
+        if self.firstlineofpadtoprint < 0:
+            self.firstlineofpadtoprint = 0
+        if self.firstlineofpadtoprint > self.numpadlines - 1:
+            self.firstlineofpadtoprint = self.numpadlines - 1
+
+    def toggleapply(self, item=None):
+        """
+        toggle the applied flag of the specified item.  if no item is specified,
+        toggle the flag of the currently selected item.
+
+        """
+        if item is None:
+            item = self.currentselecteditem
+
+        item.applied = not item.applied
+
+        if isinstance(item, uiheader):
+            item.partial = False
+            if item.applied:
+                if not item.special():
+                    # apply all its hunks
+                    for hnk in item.hunks:
+                        hnk.applied = True
+                        # apply all their hunklines
+                        for hunkline in hnk.changedlines:
+                            hunkline.applied = True
+                else:
+                    # all children are off (but the header is on)
+                    if len(item.allchildren()) > 0:
+                        item.partial = True
+            else:
+                # un-apply all its hunks
+                for hnk in item.hunks:
+                    hnk.applied = False
+                    hnk.partial = False
+                    # un-apply all their hunklines
+                    for hunkline in hnk.changedlines:
+                        hunkline.applied = False
+        elif isinstance(item, uihunk):
+            item.partial = False
+            # apply all it's hunklines
+            for hunkline in item.changedlines:
+                hunkline.applied = item.applied
+
+            siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
+            allsiblingsapplied = not (False in siblingappliedstatus)
+            nosiblingsapplied = not (True in siblingappliedstatus)
+
+            siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
+            somesiblingspartial = (True in siblingspartialstatus)
+
+            #cases where applied or partial should be removed from header
+
+            # if no 'sibling' hunks are applied (including this hunk)
+            if nosiblingsapplied:
+                if not item.header.special():
+                    item.header.applied = False
+                    item.header.partial = False
+            else: # some/all parent siblings are applied
+                item.header.applied = True
+                item.header.partial = (somesiblingspartial or
+                                        not allsiblingsapplied)
+
+        elif isinstance(item, uihunkline):
+            siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
+            allsiblingsapplied = not (False in siblingappliedstatus)
+            nosiblingsapplied = not (True in siblingappliedstatus)
+
+            # if no 'sibling' lines are applied
+            if nosiblingsapplied:
+                item.hunk.applied = False
+                item.hunk.partial = False
+            elif allsiblingsapplied:
+                item.hunk.applied = True
+                item.hunk.partial = False
+            else: # some siblings applied
+                item.hunk.applied = True
+                item.hunk.partial = True
+
+            parentsiblingsapplied = [hnk.applied for hnk
+                                     in item.hunk.header.hunks]
+            noparentsiblingsapplied = not (True in parentsiblingsapplied)
+            allparentsiblingsapplied = not (False in parentsiblingsapplied)
+
+            parentsiblingspartial = [hnk.partial for hnk
+                                     in item.hunk.header.hunks]
+            someparentsiblingspartial = (True in parentsiblingspartial)
+
+            # if all parent hunks are not applied, un-apply header
+            if noparentsiblingsapplied:
+                if not item.hunk.header.special():
+                    item.hunk.header.applied = False
+                    item.hunk.header.partial = False
+            # set the applied and partial status of the header if needed
+            else: # some/all parent siblings are applied
+                item.hunk.header.applied = True
+                item.hunk.header.partial = (someparentsiblingspartial or
+                                            not allparentsiblingsapplied)
+
+    def toggleall(self):
+        "toggle the applied flag of all items."
+        if self.waslasttoggleallapplied: # then unapply them this time
+            for item in self.headerlist:
+                if item.applied:
+                    self.toggleapply(item)
+        else:
+            for item in self.headerlist:
+                if not item.applied:
+                    self.toggleapply(item)
+        self.waslasttoggleallapplied = not self.waslasttoggleallapplied
+
+    def togglefolded(self, item=None, foldparent=False):
+        "toggle folded flag of specified item (defaults to currently selected)"
+        if item is None:
+            item = self.currentselecteditem
+        if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
+            if not isinstance(item, uiheader):
+                # we need to select the parent item in this case
+                self.currentselecteditem = item = item.parentitem()
+            elif item.neverunfolded:
+                item.neverunfolded = False
+
+            # also fold any foldable children of the parent/current item
+            if isinstance(item, uiheader): # the original or 'new' item
+                for child in item.allchildren():
+                    child.folded = not item.folded
+
+        if isinstance(item, (uiheader, uihunk)):
+            item.folded = not item.folded
+
+
+    def alignstring(self, instr, window):
+        """
+        add whitespace to the end of a string in order to make it fill
+        the screen in the x direction.  the current cursor position is
+        taken into account when making this calculation.  the string can span
+        multiple lines.
+
+        """
+        y, xstart = window.getyx()
+        width = self.xscreensize
+        # turn tabs into spaces
+        instr = instr.expandtabs(4)
+        try:
+            strlen = len(unicode(encoding.fromlocal(instr), code))
+        except Exception:
+            # if text is not utf8, then assume an 8-bit single-byte encoding.
+            strlen = len(instr)
+
+        numspaces = (width - ((strlen + xstart) % width) - 1)
+        return instr + " " * numspaces + "\n"
+
+    def printstring(self, window, text, fgcolor=None, bgcolor=None, pair=None,
+        pairname=None, attrlist=None, towin=True, align=True, showwhtspc=False):
+        """
+        print the string, text, with the specified colors and attributes, to
+        the specified curses window object.
+
+        the foreground and background colors are of the form
+        curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green,
+        magenta, red, white, yellow].  if pairname is provided, a color
+        pair will be looked up in the self.colorpairnames dictionary.
+
+        attrlist is a list containing text attributes in the form of
+        curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout,
+        underline].
+
+        if align == True, whitespace is added to the printed string such that
+        the string stretches to the right border of the window.
+
+        if showwhtspc == True, trailing whitespace of a string is highlighted.
+
+        """
+        # preprocess the text, converting tabs to spaces
+        text = text.expandtabs(4)
+        # strip \n, and convert control characters to ^[char] representation
+        text = re.sub(r'[\x00-\x08\x0a-\x1f]',
+                lambda m:'^' + chr(ord(m.group()) + 64), text.strip('\n'))
+
+        if pair is not None:
+            colorpair = pair
+        elif pairname is not None:
+            colorpair = self.colorpairnames[pairname]
+        else:
+            if fgcolor is None:
+                fgcolor = -1
+            if bgcolor is None:
+                bgcolor = -1
+            if (fgcolor, bgcolor) in self.colorpairs:
+                colorpair = self.colorpairs[(fgcolor, bgcolor)]
+            else:
+                colorpair = self.getcolorpair(fgcolor, bgcolor)
+        # add attributes if possible
+        if attrlist is None:
+            attrlist = []
+        if colorpair < 256:
+            # then it is safe to apply all attributes
+            for textattr in attrlist:
+                colorpair |= textattr
+        else:
+            # just apply a select few (safe?) attributes
+            for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
+                if textattr in attrlist:
+                    colorpair |= textattr
+
+        y, xstart = self.chunkpad.getyx()
+        t = "" # variable for counting lines printed
+        # if requested, show trailing whitespace
+        if showwhtspc:
+            origlen = len(text)
+            text = text.rstrip(' \n') # tabs have already been expanded
+            strippedlen = len(text)
+            numtrailingspaces = origlen - strippedlen
+
+        if towin:
+            window.addstr(text, colorpair)
+        t += text
+
+        if showwhtspc:
+                wscolorpair = colorpair | curses.A_REVERSE
+                if towin:
+                    for i in range(numtrailingspaces):
+                        window.addch(curses.ACS_CKBOARD, wscolorpair)
+                t += " " * numtrailingspaces
+
+        if align:
+            if towin:
+                extrawhitespace = self.alignstring("", window)
+                window.addstr(extrawhitespace, colorpair)
+            else:
+                # need to use t, since the x position hasn't incremented
+                extrawhitespace = self.alignstring(t, window)
+            t += extrawhitespace
+
+        # is reset to 0 at the beginning of printitem()
+
+        linesprinted = (xstart + len(t)) / self.xscreensize
+        self.linesprintedtopadsofar += linesprinted
+        return t
+
+    def updatescreen(self):
+        self.statuswin.erase()
+        self.chunkpad.erase()
+
+        printstring = self.printstring
+
+        # print out the status lines at the top
+        try:
+            printstring(self.statuswin,
+                        "SELECT CHUNKS: (j/k/up/dn/pgup/pgdn) move cursor; "
+                        "(space/A) toggle hunk/all; (e)dit hunk;",
+                        pairname="legend")
+            printstring(self.statuswin,
+                        " (f)old/unfold; (c)ommit applied; (q)uit; (?) help "
+                        "| [X]=hunk applied **=folded",
+                        pairname="legend")
+        except curses.error:
+            pass
+
+        # print out the patch in the remaining part of the window
+        try:
+            self.printitem()
+            self.updatescroll()
+            self.chunkpad.refresh(self.firstlineofpadtoprint, 0,
+                                  self.numstatuslines, 0,
+                                  self.yscreensize + 1 - self.numstatuslines,
+                                  self.xscreensize)
+        except curses.error:
+            pass
+
+        # refresh([pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol])
+        self.statuswin.refresh()
+
+    def getstatusprefixstring(self, item):
+        """
+        create a string to prefix a line with which indicates whether 'item'
+        is applied and/or folded.
+
+        """
+        # create checkbox string
+        if item.applied:
+            if not isinstance(item, uihunkline) and item.partial:
+                checkbox = "[~]"
+            else:
+                checkbox = "[x]"
+        else:
+            checkbox = "[ ]"
+
+        try:
+            if item.folded:
+                checkbox += "**"
+                if isinstance(item, uiheader):
+                    # one of "m", "a", or "d" (modified, added, deleted)
+                    filestatus = item.changetype
+
+                    checkbox += filestatus + " "
+            else:
+                checkbox += "  "
+                if isinstance(item, uiheader):
+                    # add two more spaces for headers
+                    checkbox += "  "
+        except AttributeError: # not foldable
+            checkbox += "  "
+
+        return checkbox
+
+    def printheader(self, header, selected=False, towin=True,
+                    ignorefolding=False):
+        """
+        print the header to the pad.  if countlines is True, don't print
+        anything, but just count the number of lines which would be printed.
+
+        """
+        outstr = ""
+        text = header.prettystr()
+        chunkindex = self.chunklist.index(header)
+
+        if chunkindex != 0 and not header.folded:
+            # add separating line before headers
+            outstr += self.printstring(self.chunkpad, '_' * self.xscreensize,
+                                       towin=towin, align=False)
+        # select color-pair based on if the header is selected
+        colorpair = self.getcolorpair(name=selected and "selected" or "normal",
+                                      attrlist=[curses.A_BOLD])
+
+        # print out each line of the chunk, expanding it to screen width
+
+        # number of characters to indent lines on this level by
+        indentnumchars = 0
+        checkbox = self.getstatusprefixstring(header)
+        if not header.folded or ignorefolding:
+            textlist = text.split("\n")
+            linestr = checkbox + textlist[0]
+        else:
+            linestr = checkbox + header.filename()
+        outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
+                                   towin=towin)
+        if not header.folded or ignorefolding:
+            if len(textlist) > 1:
+                for line in textlist[1:]:
+                    linestr = " "*(indentnumchars + len(checkbox)) + line
+                    outstr += self.printstring(self.chunkpad, linestr,
+                                               pair=colorpair, towin=towin)
+
+        return outstr
+
+    def printhunklinesbefore(self, hunk, selected=False, towin=True,
+                             ignorefolding=False):
+        "includes start/end line indicator"
+        outstr = ""
+        # where hunk is in list of siblings
+        hunkindex = hunk.header.hunks.index(hunk)
+
+        if hunkindex != 0:
+            # add separating line before headers
+            outstr += self.printstring(self.chunkpad, ' '*self.xscreensize,
+                                       towin=towin, align=False)
+
+        colorpair = self.getcolorpair(name=selected and "selected" or "normal",
+                                      attrlist=[curses.A_BOLD])
+
+        # print out from-to line with checkbox
+        checkbox = self.getstatusprefixstring(hunk)
+
+        lineprefix = " "*self.hunkindentnumchars + checkbox
+        frtoline = "   " + hunk.getfromtoline().strip("\n")
+
+
+        outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
+                                   align=False) # add uncolored checkbox/indent
+        outstr += self.printstring(self.chunkpad, frtoline, pair=colorpair,
+                                   towin=towin)
+
+        if hunk.folded and not ignorefolding:
+            # skip remainder of output
+            return outstr
+
+        # print out lines of the chunk preceeding changed-lines
+        for line in hunk.before:
+            linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
+            outstr += self.printstring(self.chunkpad, linestr, towin=towin)
+
+        return outstr
+
+    def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
+        outstr = ""
+        if hunk.folded and not ignorefolding:
+            return outstr
+
+        # a bit superfluous, but to avoid hard-coding indent amount
+        checkbox = self.getstatusprefixstring(hunk)
+        for line in hunk.after:
+            linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
+            outstr += self.printstring(self.chunkpad, linestr, towin=towin)
+
+        return outstr
+
+    def printhunkchangedline(self, hunkline, selected=False, towin=True):
+        outstr = ""
+        checkbox = self.getstatusprefixstring(hunkline)
+
+        linestr = hunkline.prettystr().strip("\n")
+
+        # select color-pair based on whether line is an addition/removal
+        if selected:
+            colorpair = self.getcolorpair(name="selected")
+        elif linestr.startswith("+"):
+            colorpair = self.getcolorpair(name="addition")
+        elif linestr.startswith("-"):
+            colorpair = self.getcolorpair(name="deletion")
+        elif linestr.startswith("\\"):
+            colorpair = self.getcolorpair(name="normal")
+
+        lineprefix = " "*self.hunklineindentnumchars + checkbox
+        outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
+                                   align=False) # add uncolored checkbox/indent
+        outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
+                                   towin=towin, showwhtspc=True)
+        return outstr
+
+    def printitem(self, item=None, ignorefolding=False, recursechildren=True,
+                  towin=True):
+        """
+        use __printitem() to print the the specified item.applied.
+        if item is not specified, then print the entire patch.
+        (hiding folded elements, etc. -- see __printitem() docstring)
+        """
+        if item is None:
+            item = self.headerlist
+        if recursechildren:
+            self.linesprintedtopadsofar = 0
+
+        outstr = []
+        self.__printitem(item, ignorefolding, recursechildren, outstr,
+                                  towin=towin)
+        return ''.join(outstr)
+
+    def outofdisplayedarea(self):
+        y, _ = self.chunkpad.getyx() # cursor location
+        # * 2 here works but an optimization would be the max number of
+        # consecutive non selectable lines
+        # i.e the max number of context line for any hunk in the patch
+        miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
+        maxy = self.firstlineofpadtoprint + self.yscreensize * 2
+        return y < miny or y > maxy
+
+    def handleselection(self, item, recursechildren):
+        selected = (item is self.currentselecteditem)
+        if selected and recursechildren:
+            # assumes line numbering starting from line 0
+            self.selecteditemstartline = self.linesprintedtopadsofar
+            selecteditemlines = self.getnumlinesdisplayed(item,
+                                                          recursechildren=False)
+            self.selecteditemendline = (self.selecteditemstartline +
+                                        selecteditemlines - 1)
+        return selected
+
+    def __printitem(self, item, ignorefolding, recursechildren, outstr,
+                    towin=True):
+        """
+        recursive method for printing out patch/header/hunk/hunk-line data to
+        screen.  also returns a string with all of the content of the displayed
+        patch (not including coloring, etc.).
+
+        if ignorefolding is True, then folded items are printed out.
+
+        if recursechildren is False, then only print the item without its
+        child items.
+
+        """
+        if towin and self.outofdisplayedarea():
+            return
+
+        selected = self.handleselection(item, recursechildren)
+
+        # patch object is a list of headers
+        if isinstance(item, patch):
+            if recursechildren:
+                for hdr in item:
+                    self.__printitem(hdr, ignorefolding,
+                            recursechildren, outstr, towin)
+        # todo: eliminate all isinstance() calls
+        if isinstance(item, uiheader):
+            outstr.append(self.printheader(item, selected, towin=towin,
+                                       ignorefolding=ignorefolding))
+            if recursechildren:
+                for hnk in item.hunks:
+                    self.__printitem(hnk, ignorefolding,
+                            recursechildren, outstr, towin)
+        elif (isinstance(item, uihunk) and
+              ((not item.header.folded) or ignorefolding)):
+            # print the hunk data which comes before the changed-lines
+            outstr.append(self.printhunklinesbefore(item, selected, towin=towin,
+                                                ignorefolding=ignorefolding))
+            if recursechildren:
+                for l in item.changedlines:
+                    self.__printitem(l, ignorefolding,
+                            recursechildren, outstr, towin)
+                outstr.append(self.printhunklinesafter(item, towin=towin,
+                                                ignorefolding=ignorefolding))
+        elif (isinstance(item, uihunkline) and
+              ((not item.hunk.folded) or ignorefolding)):
+            outstr.append(self.printhunkchangedline(item, selected,
+                towin=towin))
+
+        return outstr
+
+    def getnumlinesdisplayed(self, item=None, ignorefolding=False,
+                             recursechildren=True):
+        """
+        return the number of lines which would be displayed if the item were
+        to be printed to the display.  the item will not be printed to the
+        display (pad).
+        if no item is given, assume the entire patch.
+        if ignorefolding is True, folded items will be unfolded when counting
+        the number of lines.
+
+        """
+        # temporarily disable printing to windows by printstring
+        patchdisplaystring = self.printitem(item, ignorefolding,
+                                            recursechildren, towin=False)
+        numlines = len(patchdisplaystring) / self.xscreensize
+        return numlines
+
+    def sigwinchhandler(self, n, frame):
+        "handle window resizing"
+        try:
+            curses.endwin()
+            self.yscreensize, self.xscreensize = gethw()
+            self.statuswin.resize(self.numstatuslines, self.xscreensize)
+            self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
+            self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
+            # todo: try to resize commit message window if possible
+        except curses.error:
+            pass
+
+    def getcolorpair(self, fgcolor=None, bgcolor=None, name=None,
+                     attrlist=None):
+        """
+        get a curses color pair, adding it to self.colorpairs if it is not
+        already defined.  an optional string, name, can be passed as a shortcut
+        for referring to the color-pair.  by default, if no arguments are
+        specified, the white foreground / black background color-pair is
+        returned.
+
+        it is expected that this function will be used exclusively for
+        initializing color pairs, and not curses.init_pair().
+
+        attrlist is used to 'flavor' the returned color-pair.  this information
+        is not stored in self.colorpairs.  it contains attribute values like
+        curses.A_BOLD.
+
+        """
+        if (name is not None) and name in self.colorpairnames:
+            # then get the associated color pair and return it
+            colorpair = self.colorpairnames[name]
+        else:
+            if fgcolor is None:
+                fgcolor = -1
+            if bgcolor is None:
+                bgcolor = -1
+            if (fgcolor, bgcolor) in self.colorpairs:
+                colorpair = self.colorpairs[(fgcolor, bgcolor)]
+            else:
+                pairindex = len(self.colorpairs) + 1
+                curses.init_pair(pairindex, fgcolor, bgcolor)
+                colorpair = self.colorpairs[(fgcolor, bgcolor)] = (
+                    curses.color_pair(pairindex))
+                if name is not None:
+                    self.colorpairnames[name] = curses.color_pair(pairindex)
+
+        # add attributes if possible
+        if attrlist is None:
+            attrlist = []
+        if colorpair < 256:
+            # then it is safe to apply all attributes
+            for textattr in attrlist:
+                colorpair |= textattr
+        else:
+            # just apply a select few (safe?) attributes
+            for textattrib in (curses.A_UNDERLINE, curses.A_BOLD):
+                if textattrib in attrlist:
+                    colorpair |= textattrib
+        return colorpair
+
+    def initcolorpair(self, *args, **kwargs):
+        "same as getcolorpair."
+        self.getcolorpair(*args, **kwargs)
+
+    def helpwindow(self):
+        "print a help window to the screen.  exit after any keypress."
+        helptext = """            [press any key to return to the patch-display]
+
+crecord allows you to interactively choose among the changes you have made,
+and commit only those changes you select.  after committing the selected
+changes, the unselected changes are still present in your working copy, so you
+can use crecord multiple times to split large changes into smaller changesets.
+the following are valid keystrokes:
+
+                [space] : (un-)select item ([~]/[x] = partly/fully applied)
+                      a : (un-)select all items
+    up/down-arrow [k/j] : go to previous/next unfolded item
+        pgup/pgdn [k/j] : go to previous/next item of same type
+ right/left-arrow [l/h] : go to child item / parent item
+ shift-left-arrow   [h] : go to parent header / fold selected header
+                      f : fold / unfold item, hiding/revealing its children
+                      f : fold / unfold parent item and all of its ancestors
+                      m : edit / resume editing the commit message
+                      e : edit the currently selected hunk
+                      a : toggle amend mode (hg rev >= 2.2)
+                      c : commit selected changes
+                      r : review/edit and commit selected changes
+                      q : quit without committing (no changes will be made)
+                      ? : help (what you're currently reading)"""
+
+        helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
+        helplines = helptext.split("\n")
+        helplines = helplines + [" "]*(
+            self.yscreensize - self.numstatuslines - len(helplines) - 1)
+        try:
+            for line in helplines:
+                self.printstring(helpwin, line, pairname="legend")
+        except curses.error:
+            pass
+        helpwin.refresh()
+        try:
+            helpwin.getkey()
+        except curses.error:
+            pass
+
+    def confirmationwindow(self, windowtext):
+        "display an informational window, then wait for and return a keypress."
+
+        confirmwin = curses.newwin(self.yscreensize, 0, 0, 0)
+        try:
+            lines = windowtext.split("\n")
+            for line in lines:
+                self.printstring(confirmwin, line, pairname="selected")
+        except curses.error:
+            pass
+        self.stdscr.refresh()
+        confirmwin.refresh()
+        try:
+            response = chr(self.stdscr.getch())
+        except ValueError:
+            response = None
+
+        return response
+
+    def confirmcommit(self, review=False):
+        "ask for 'y' to be pressed to confirm commit. return True if confirmed."
+        if review:
+            confirmtext = (
+"""if you answer yes to the following, the your currently chosen patch chunks
+will be loaded into an editor.  you may modify the patch from the editor, and
+save the changes if you wish to change the patch.  otherwise, you can just
+close the editor without saving to accept the current patch as-is.
+
+note: don't add/remove lines unless you also modify the range information.
+      failing to follow this rule will result in the commit aborting.
+
+are you sure you want to review/edit and commit the selected changes [yn]? """)
+        else:
+            confirmtext = (
+                "are you sure you want to commit the selected changes [yn]? ")
+
+        response = self.confirmationwindow(confirmtext)
+        if response is None:
+            response = "n"
+        if response.lower().startswith("y"):
+            return True
+        else:
+            return False
+
+    def recenterdisplayedarea(self):
+        """
+        once we scrolled with pg up pg down we can be pointing outside of the
+        display zone. we print the patch with towin=False to compute the
+        location of the selected item eventhough it is outside of the displayed
+        zone and then update the scroll.
+        """
+        self.printitem(towin=False)
+        self.updatescroll()
+
+    def toggleedit(self, item=None, test=False):
+        """
+            edit the currently chelected chunk
+        """
+
+        def editpatchwitheditor(self, chunk):
+            if chunk is None:
+                self.ui.write(_('cannot edit patch for whole file'))
+                self.ui.write("\n")
+                return None
+            if chunk.header.binary():
+                self.ui.write(_('cannot edit patch for binary file'))
+                self.ui.write("\n")
+                return None
+            # patch comment based on the git one (based on comment at end of
+            # http://mercurial.selenic.com/wiki/recordextension)
+            phelp = '---' + _("""
+    to remove '-' lines, make them ' ' lines (context).
+    to remove '+' lines, delete them.
+    lines starting with # will be removed from the patch.
+
+    if the patch applies cleanly, the edited hunk will immediately be
+    added to the record list. if it does not apply cleanly, a rejects
+    file will be generated: you can use that when you try again. if
+    all lines of the hunk are removed, then the edit is aborted and
+    the hunk is left unchanged.
+    """)
+            (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-",
+                    suffix=".diff", text=True)
+            ncpatchfp = None
+            try:
+                # write the initial patch
+                f = os.fdopen(patchfd, "w")
+                chunk.header.write(f)
+                chunk.write(f)
+                f.write('\n'.join(['# ' + i for i in phelp.splitlines()]))
+                f.close()
+                # start the editor and wait for it to complete
+                editor = self.ui.geteditor()
+                self.ui.system("%s \"%s\"" % (editor, patchfn),
+                          environ={'hguser': self.ui.username()},
+                          onerr=util.Abort, errprefix=_("edit failed"))
+                # remove comment lines
+                patchfp = open(patchfn)
+                ncpatchfp = cStringIO.StringIO()
+                for line in patchfp:
+                    if not line.startswith('#'):
+                        ncpatchfp.write(line)
+                patchfp.close()
+                ncpatchfp.seek(0)
+                newpatches = patchmod.parsepatch(ncpatchfp)
+            finally:
+                os.unlink(patchfn)
+                del ncpatchfp
+            return newpatches
+        if item is None:
+            item = self.currentselecteditem
+        if isinstance(item, uiheader):
+            return
+        if isinstance(item, uihunkline):
+            item = item.parentitem()
+        if not isinstance(item, uihunk):
+            return
+
+        beforeadded, beforeremoved = item.added, item.removed
+        newpatches = editpatchwitheditor(self, item)
+        header = item.header
+        editedhunkindex = header.hunks.index(item)
+        hunksbefore = header.hunks[:editedhunkindex]
+        hunksafter = header.hunks[editedhunkindex + 1:]
+        newpatchheader = newpatches[0]
+        newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
+        newadded = sum([h.added for h in newhunks])
+        newremoved = sum([h.removed for h in newhunks])
+        offset = (newadded - beforeadded) - (newremoved - beforeremoved)
+
+        for h in hunksafter:
+            h.toline += offset
+        for h in newhunks:
+            h.folded = False
+        header.hunks = hunksbefore + newhunks + hunksafter
+        if self.emptypatch():
+            header.hunks = hunksbefore + [item] + hunksafter
+        self.currentselecteditem = header
+
+        if not test:
+            self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
+            self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
+            self.updatescroll()
+            self.stdscr.refresh()
+            self.statuswin.refresh()
+            self.stdscr.keypad(1)
+
+    def emptypatch(self):
+        item = self.headerlist
+        if not item:
+            return True
+        for header in item:
+            if header.hunks:
+                return False
+        return True
+
+    def handlekeypressed(self, keypressed, test=False):
+        if keypressed in ["k", "KEY_UP"]:
+            self.uparrowevent()
+        if keypressed in ["k", "KEY_PPAGE"]:
+            self.uparrowshiftevent()
+        elif keypressed in ["j", "KEY_DOWN"]:
+            self.downarrowevent()
+        elif keypressed in ["j", "KEY_NPAGE"]:
+            self.downarrowshiftevent()
+        elif keypressed in ["l", "KEY_RIGHT"]:
+            self.rightarrowevent()
+        elif keypressed in ["h", "KEY_LEFT"]:
+            self.leftarrowevent()
+        elif keypressed in ["h", "KEY_SLEFT"]:
+            self.leftarrowshiftevent()
+        elif keypressed in ["q"]:
+            raise util.Abort(_('user quit'))
+        elif keypressed in ["c"]:
+            if self.confirmcommit():
+                return True
+        elif keypressed in ["r"]:
+            if self.confirmcommit(review=True):
+                return True
+        elif test and keypressed in ['X']:
+            return True
+        elif keypressed in [' '] or (test and keypressed in ["TOGGLE"]):
+            self.toggleapply()
+        elif keypressed in ['A']:
+            self.toggleall()
+        elif keypressed in ['e']:
+            self.toggleedit(test=test)
+        elif keypressed in ["f"]:
+            self.togglefolded()
+        elif keypressed in ["f"]:
+            self.togglefolded(foldparent=True)
+        elif keypressed in ["?"]:
+            self.helpwindow()
+
+    def main(self, stdscr):
+        """
+        method to be wrapped by curses.wrapper() for selecting chunks.
+
+        """
+        signal.signal(signal.SIGWINCH, self.sigwinchhandler)
+        self.stdscr = stdscr
+        self.yscreensize, self.xscreensize = self.stdscr.getmaxyx()
+
+        curses.start_color()
+        curses.use_default_colors()
+
+        # available colors: black, blue, cyan, green, magenta, white, yellow
+        # init_pair(color_id, foreground_color, background_color)
+        self.initcolorpair(None, None, name="normal")
+        self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_MAGENTA,
+                           name="selected")
+        self.initcolorpair(curses.COLOR_RED, None, name="deletion")
+        self.initcolorpair(curses.COLOR_GREEN, None, name="addition")
+        self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_BLUE, name="legend")
+        # newwin([height, width,] begin_y, begin_x)
+        self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
+        self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences
+
+        # figure out how much space to allocate for the chunk-pad which is
+        # used for displaying the patch
+
+        # stupid hack to prevent getnumlinesdisplayed from failing
+        self.chunkpad = curses.newpad(1, self.xscreensize)
+
+        # add 1 so to account for last line text reaching end of line
+        self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
+        self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
+
+        # initialize selecteitemendline (initial start-line is 0)
+        self.selecteditemendline = self.getnumlinesdisplayed(
+            self.currentselecteditem, recursechildren=False)
+
+        while True:
+            self.updatescreen()
+            try:
+                keypressed = self.statuswin.getkey()
+            except curses.error:
+                keypressed = "foobar"
+            if self.handlekeypressed(keypressed):
+                break


More information about the Mercurial-devel mailing list