--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/crecord.py Thu Mar 12 14:19:11 2015 -0700
@@ -0,0 +1,1623 @@
+# stuff related specifically to patch manipulation / parsing
+#
+# Copyright 2008 Mark Edgington <edgimar@gmail.com>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+#
+# This code is based on the Mark Edgington's crecord extension.
+# (Itself based on Bryan O'Sullivan's record extension.)
+
+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