unexist.dev

Changeset 367:26e76683750f

Added extensions
author unexist
date Fri, 05 Aug 2011 21:40:00 +0200
parents e0eba56587f2
children 7477775a1bc4
files .hgext/crecord/crecord/__init__.py .hgext/crecord/crecord/__init__.pyc .hgext/crecord/crecord/chunk_selector.py .hgext/crecord/crecord/crecord_core.py .hgext/crecord/crecord/crecord_core.pyc .hgext/crecord/crecord/crpatch.py .hgext/histedit/Makefile .hgext/histedit/README .hgext/histedit/hg_histedit.py .hgext/histedit/setup.py .hgext/histedit/tests/histedit-helpers.sh .hgext/histedit/tests/run-tests.py .hgext/histedit/tests/test-histedit-commute .hgext/histedit/tests/test-histedit-commute.out .hgext/histedit/tests/test-histedit-drop .hgext/histedit/tests/test-histedit-drop.out .hgext/histedit/tests/test-histedit-edit .hgext/histedit/tests/test-histedit-edit.out .hgext/histedit/tests/test-histedit-fold .hgext/histedit/tests/test-histedit-fold-non-commute .hgext/histedit/tests/test-histedit-fold-non-commute.out .hgext/histedit/tests/test-histedit-fold.out .hgext/histedit/tests/test-histedit-no-change .hgext/histedit/tests/test-histedit-no-change.out .hgext/histedit/tests/test-histedit-non-commute .hgext/histedit/tests/test-histedit-non-commute-abort .hgext/histedit/tests/test-histedit-non-commute-abort.out .hgext/histedit/tests/test-histedit-non-commute.out .hgext/histedit/tests/test-histedit-outgoing .hgext/histedit/tests/test-histedit-outgoing.out
diffstat 30 files changed, 4851 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgext/crecord/crecord/__init__.py	Fri Aug 05 21:40:00 2011 +0200
@@ -0,0 +1,95 @@
+# crecord.py
+#
+# Copyright 2008 Mark Edgington <edgimar@gmail.com>
+#
+# This software may be used and distributed according to the terms of
+# the GNU General Public License, incorporated herein by reference.
+#
+# Much of this extension is based on Bryan O'Sullivan's record extension.
+
+'''text-gui based change selection during commit or qrefresh'''
+from mercurial.i18n import _
+from mercurial import commands, extensions, util
+
+from crecord_core import dorecord
+
+def crecord(ui, repo, *pats, **opts):
+    '''interactively select changes to commit
+
+    If a list of files is omitted, all changes reported by :hg:`status`
+    will be candidates for recording.
+
+    See :hg:`help dates` for a list of formats valid for -d/--date.
+
+    You will be shown a list of patch hunks from which you can select
+    those you would like to apply to the commit.
+
+    This command is not available when committing a merge.'''
+
+    dorecord(ui, repo, commands.commit, *pats, **opts)
+
+
+def qcrecord(ui, repo, patch, *pats, **opts):
+    '''interactively record a new patch
+
+    See :hg:`help qnew` & :hg:`help crecord` for more information and
+    usage.
+    '''
+
+    try:
+        mq = extensions.find('mq')
+    except KeyError:
+        raise util.Abort(_("'mq' extension not loaded"))
+
+    def committomq(ui, repo, *pats, **opts):
+        mq.new(ui, repo, patch, *pats, **opts)
+
+    opts = opts.copy()
+    opts['force'] = True    # always 'qnew -f'
+    dorecord(ui, repo, committomq, *pats, **opts)
+
+
+cmdtable = {
+    "crecord":
+        (crecord,
+
+         # add commit options
+         commands.table['^commit|ci'][1],
+
+         _('hg crecord [OPTION]... [FILE]...')),
+}
+
+
+def extsetup():
+    try:
+        keyword = extensions.find('keyword')
+        keyword.restricted += ' crecord qcrecord'
+        try:
+            keyword.recordextensions += ' crecord'
+            keyword.recordcommands += ' crecord qcrecord'
+        except AttributeError:
+            pass
+    except KeyError:
+        pass
+
+    try:
+        mq = extensions.find('mq')
+    except KeyError:
+        return
+
+    qnew = '^qnew'
+    if not qnew in mq.cmdtable:
+        # backwards compatible with pre 301633755dec
+        qnew = 'qnew'
+
+    qcmdtable = {
+    "qcrecord":
+        (qcrecord,
+
+         # add qnew options, except '--force'
+         [opt for opt in mq.cmdtable[qnew][1] if opt[1] != 'force'],
+
+         _('hg qcrecord [OPTION]... PATCH [FILE]...')),
+    }
+
+    cmdtable.update(qcmdtable)
Binary file .hgext/crecord/crecord/__init__.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgext/crecord/crecord/chunk_selector.py	Fri Aug 05 21:40:00 2011 +0200
@@ -0,0 +1,1064 @@
+from mercurial.i18n import _
+from mercurial import util
+
+# accomodate older versions where encoding module doesn't yet exist
+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 textwrap
+
+from crpatch import Patch, header, hunk, HunkLine
+
+# 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()
+
+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(opts, 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, opts)
+
+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, HunkLine):
+            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, header):
+            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, header):
+            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, hunk):
+            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, HunkLine):
+            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, header) and item.neverUnfolded):
+            if not isinstance(item, header):
+                # 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, header): # the original OR 'new' item
+                for child in item.allChildren():
+                    child.folded = not item.folded
+
+        if isinstance(item, (header, hunk)):
+            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:
+            # 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 self.colorPairs.has_key((fgColor,bgColor)):
+                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",
+                        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, HunkLine) and item.partial:
+                checkBox = "[~]"
+            else:
+                checkBox = "[X]"
+        else:
+            checkBox = "[ ]"
+
+        try:
+            if item.folded:
+                checkBox += "**"
+                if isinstance(item, header):
+                    # one of "M", "A", or "D" (modified, added, deleted)
+                    fileStatus = item.changetype
+
+                    checkBox += fileStatus + " "
+            else:
+                checkBox += "  "
+                if isinstance(item, header):
+                    # 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
+
+        indentNumChars = self.hunkLineIndentNumChars-1
+        # a bit superfluous, but to avoid hard-coding indent amount
+        checkBox = self.getStatusPrefixString(hunk)
+        for line in hunk.after:
+            lineStr = " "*(indentNumChars + len(checkBox)) + line
+            outStr += self.printString(self.chunkpad, lineStr, toWin=toWin)
+
+        return outStr
+
+    def printHunkChangedLine(self, hunkLine, selected=False, toWin=True):
+        outStr = ""
+        indentNumChars = self.hunkLineIndentNumChars
+        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 = " "*indentNumChars + 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
+            global outStr
+        retStr = self.__printItem(item, ignoreFolding, recurseChildren,
+                                  toWin=toWin)
+        if recurseChildren:
+            # remove the string when finished, so it doesn't accumulate
+            del outStr
+
+        return retStr
+
+    def __printItem(self, item, ignoreFolding, recurseChildren, 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.
+
+        """
+        # keep outStr local, since we're not recursing
+        if recurseChildren:
+            global outStr
+            try:
+                outStr
+            except:
+                outStr = ""
+        else:
+            outStr = ""
+
+        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)
+
+        # Patch object is a list of headers
+        if isinstance(item, Patch):
+            if recurseChildren:
+                for hdr in item:
+                    self.__printItem(hdr, ignoreFolding, recurseChildren, toWin)
+        # TODO: eliminate all isinstance() calls
+        if isinstance(item, header):
+            outStr += self.printHeader(item, selected, toWin=toWin,
+                                       ignoreFolding=ignoreFolding)
+            if recurseChildren:
+                for hnk in item.hunks:
+                    self.__printItem(hnk, ignoreFolding, recurseChildren, toWin)
+        elif (isinstance(item, hunk) and
+              ((not item.header.folded) or ignoreFolding)):
+            # print the hunk data which comes before the changed-lines
+            outStr += self.printHunkLinesBefore(item, selected, toWin=toWin,
+                                                ignoreFolding=ignoreFolding)
+            if recurseChildren:
+                for l in item.changedLines:
+                    self.__printItem(l, ignoreFolding, recurseChildren, toWin)
+                outStr += self.printHunkLinesAfter(item, toWin=toWin,
+                                                   ignoreFolding=ignoreFolding)
+        elif (isinstance(item, HunkLine) and
+              ((not item.hunk.folded) or ignoreFolding)):
+            outStr += 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 self.colorPairNames.has_key(name):
+            # 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 self.colorPairs.has_key((fgColor,bgColor)):
+                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
+                      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 commitMessageWindow(self):
+        "Create a temporary commit message editing window on the screen."
+        if self.commentText == "":
+            self.commentText = textwrap.dedent("""
+            HG: Enter/resume commit message.  Lines beginning with 'HG:' are removed.
+            HG: You can save this message, and edit it again later before committing.
+            HG: After exiting the editor, you will return to the crecord patch view.
+            HG: --
+            HG: user: %s""" % self.ui.username())
+            
+        curses.raw()
+        curses.def_prog_mode()
+        curses.endwin()
+        self.commentText = self.ui.edit(self.commentText, self.ui.username())
+        curses.cbreak()
+        self.stdscr.refresh()
+        self.stdscr.keypad(1) # allow arrow-keys to continue to function
+        
+    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]? ")
+
+        confirmWin = curses.newwin(self.yScreenSize, 0, 0, 0)
+        try:
+            lines = confirmText.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 = "n"
+        if response.lower().startswith("y"):
+            return True
+        else:
+            return False
+
+    def main(self, stdscr, opts):
+        """
+        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)
+
+        # option which enables/disables patch-review (in editor) step
+        opts['crecord_reviewpatch'] = False
+
+        try:
+            self.commentText = opts['message']
+        except KeyError:
+            pass
+
+        while True:
+            self.updateScreen()
+            try:
+                keyPressed = self.statuswin.getkey()
+            except curses.error:
+                keyPressed = "FOOBAR"
+
+            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():
+                    break
+            elif keyPressed in ["r"]:
+                if self.confirmCommit(review=True):
+                    opts['crecord_reviewpatch'] = True
+                    break
+            elif keyPressed in [' ']:
+                self.toggleApply()
+            elif keyPressed in ['A']:
+                self.toggleAll()
+            elif keyPressed in ["f"]:
+                self.toggleFolded()
+            elif keyPressed in ["F"]:
+                self.toggleFolded(foldParent=True)
+            elif keyPressed in ["?"]:
+                self.helpWindow()
+            elif keyPressed in ["m"]:
+                self.commitMessageWindow()
+
+        if self.commentText != "":
+            opts['message'] = self.commentText
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgext/crecord/crecord/crecord_core.py	Fri Aug 05 21:40:00 2011 +0200
@@ -0,0 +1,182 @@
+# crecord.py
+#
+# Copyright 2008 Mark Edgington <edgimar@gmail.com>
+#
+# This software may be used and distributed according to the terms of
+# the GNU General Public License, incorporated herein by reference.
+#
+# Much of this extension is based on Bryan O'Sullivan's record extension.
+
+'''text-gui based change selection during commit or qrefresh'''
+from mercurial.i18n import _
+from mercurial import cmdutil, hg, mdiff, patch
+from mercurial import util
+import cStringIO
+import errno
+import os
+import tempfile
+
+import crpatch
+import chunk_selector
+
+def dorecord(ui, repo, commitfunc, *pats, **opts):
+    try:
+        if not ui.interactive():
+            raise util.Abort(_('running non-interactively, use commit instead'))
+    except TypeError: # backwards compatibility with hg 1.1
+        if not ui.interactive:
+            raise util.Abort(_('running non-interactively, use commit instead'))
+
+    def recordfunc(ui, repo, message, match, opts):
+        """This is generic record driver.
+
+        Its job is to interactively filter local changes, and accordingly
+        prepare working dir into a state, where the job can be delegated to
+        non-interactive commit command such as 'commit' or 'qrefresh'.
+
+        After the actual job is done by non-interactive command, working dir
+        state is restored to original.
+
+        In the end we'll record interesting changes, and everything else will be
+        left in place, so the user can continue his work.
+        """
+
+        merge = len(repo[None].parents()) > 1
+        if merge:
+            raise util.Abort(_('cannot partially commit a merge '
+                               '(use hg commit instead)'))
+
+        changes = repo.status(match=match)[:3]
+        diffopts = mdiff.diffopts(git=True, nodates=True)
+        chunks = patch.diff(repo, changes=changes, opts=diffopts)
+        fp = cStringIO.StringIO()
+        fp.write(''.join(chunks))
+        fp.seek(0)
+
+        # 1. filter patch, so we have intending-to apply subset of it
+        chunks = crpatch.filterpatch(opts,
+                                     crpatch.parsepatch(changes, fp),
+                                     chunk_selector.chunkselector, ui)
+        del fp
+
+        contenders = set()
+        for h in chunks:
+            try:
+                contenders.update(set(h.files()))
+            except AttributeError:
+                pass
+
+        changed = changes[0] + changes[1] + changes[2]
+        newfiles = [f for f in changed if f in contenders]
+
+        if not newfiles:
+            ui.status(_('no changes to record\n'))
+            return 0
+
+        modified = set(changes[0])
+
+        # 2. backup changed files, so we can restore them in the end
+        backups = {}
+        backupdir = repo.join('record-backups')
+        try:
+            os.mkdir(backupdir)
+        except OSError, err:
+            if err.errno != errno.EEXIST:
+                raise
+        try:
+            # backup continues
+            for f in newfiles:
+                if f not in modified:
+                    continue
+                fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
+                                               dir=backupdir)
+                os.close(fd)
+                ui.debug('backup %r as %r\n' % (f, tmpname))
+                util.copyfile(repo.wjoin(f), tmpname)
+                backups[f] = tmpname
+
+            fp = cStringIO.StringIO()
+            for c in chunks:
+                if c.filename() in backups:
+                    c.write(fp)
+            dopatch = fp.tell()
+            fp.seek(0)
+
+            # 2.5 optionally review / modify patch in text editor
+            if opts['crecord_reviewpatch']:
+                patchtext = fp.read()
+                reviewedpatch = ui.edit(patchtext, "")
+                fp.truncate(0)
+                fp.write(reviewedpatch)
+                fp.seek(0)
+
+            # 3a. apply filtered patch to clean repo  (clean)
+            if backups:
+                hg.revert(repo, repo.dirstate.parents()[0],
+                          lambda key: key in backups)
+
+            # 3b. (apply)
+            if dopatch:
+                try:
+                    ui.debug('applying patch\n')
+                    ui.debug(fp.getvalue())
+                    pfiles = {}
+                    try:
+                        from mercurial import scmutil
+                        patch.internalpatch(ui, repo, fp, strip=1, files=pfiles,
+                                            eolmode=None)
+                        scmutil.updatedir(ui, repo, pfiles)
+                    except ImportError: # pre 3438417a6657
+                        try:
+                            patch.internalpatch(ui, repo, fp, 1, eolmode=None)
+                        except (TypeError, AttributeError): # pre 17cea10c343e
+                            try:
+                                patch.internalpatch(ui, repo, fp, 1, repo.root,
+                                                    eolmode=None)
+                            except (TypeError, AttributeError): # pre 00a881581400
+                                try:
+                                    patch.internalpatch(fp, ui, 1, repo.root,
+                                                        files=pfiles, eolmode=None)
+                                except TypeError: # backwards compatible with hg 1.1
+                                    patch.internalpatch(fp, ui, 1,
+                                                        repo.root, files=pfiles)
+                        try:
+                            cmdutil.updatedir(ui, repo, pfiles)
+                        except AttributeError:
+                            try:
+                                patch.updatedir(ui, repo, pfiles)
+                            except AttributeError:
+                                # from 00a881581400 onwards
+                                pass
+                except patch.PatchError, err:
+                    s = str(err)
+                    if s:
+                        raise util.Abort(s)
+                    else:
+                        raise util.Abort(_('patch failed to apply'))
+            del fp
+
+            # 4. We prepared working directory according to filtered patch.
+            #    Now is the time to delegate the job to commit/qrefresh or the like!
+
+            # it is important to first chdir to repo root -- we'll call a
+            # highlevel command with list of pathnames relative to repo root
+            cwd = os.getcwd()
+            os.chdir(repo.root)
+            try:
+                commitfunc(ui, repo, *newfiles, **opts)
+            finally:
+                os.chdir(cwd)
+
+            return 0
+        finally:
+            # 5. finally restore backed-up files
+            try:
+                for realname, tmpname in backups.iteritems():
+                    ui.debug('restoring %r to %r\n' % (tmpname, realname))
+                    util.copyfile(tmpname, repo.wjoin(realname))
+                    os.unlink(tmpname)
+                os.rmdir(backupdir)
+            except OSError:
+                pass
+    return cmdutil.commit(ui, repo, recordfunc, pats, opts)
Binary file .hgext/crecord/crecord/crecord_core.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgext/crecord/crecord/crpatch.py	Fri Aug 05 21:40:00 2011 +0200
@@ -0,0 +1,669 @@
+# stuff related specifically to patch manipulation / parsing
+from mercurial.i18n import _
+from mercurial import patch
+
+import cStringIO
+import re
+
+lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
+
+def scanpatch(fp):
+    """like patch.iterhunks, but yield different events
+
+    - ('file',    [header_lines + fromfile + tofile])
+    - ('context', [context_lines])
+    - ('hunk',    [hunk_lines])
+    - ('range',   (-start,len, +start,len, diffp))
+    """
+    lr = patch.linereader(fp)
+
+    def scanwhile(first, p):
+        """scan lr while predicate holds"""
+        lines = [first]
+        while True:
+            line = lr.readline()
+            if not line:
+                break
+            if p(line):
+                lines.append(line)
+            else:
+                lr.push(line)
+                break
+        return lines
+
+    while True:
+        line = lr.readline()
+        if not line:
+            break
+        if line.startswith('diff --git a/'):
+            def notheader(line):
+                s = line.split(None, 1)
+                return not s or s[0] not in ('---', 'diff')
+            header = scanwhile(line, notheader)
+            fromfile = lr.readline()
+            if fromfile.startswith('---'):
+                tofile = lr.readline()
+                header += [fromfile, tofile]
+            else:
+                lr.push(fromfile)
+            yield 'file', header
+        elif line[0] == ' ':
+            yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
+        elif line[0] in '-+':
+            yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
+        else:
+            m = lines_re.match(line)
+            if m:
+                yield 'range', m.groups()
+            else:
+                raise patch.PatchError('unknown patch content: %r' % line)
+
+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 header(PatchNode):
+    """patch header
+
+    XXX shoudn't we move this to mercurial/patch.py ?
+    """
+    diff_re = re.compile('diff --git a/(.*) b/(.*)$')
+    allhunks_re = re.compile('(?:index|new file|deleted file) ')
+    pretty_re = re.compile('(?:new file|deleted file) ')
+    special_re = re.compile('(?:index|new|deleted|copy|rename) ')
+
+    def __init__(self, header):
+        self.header = header
+        self.hunks = []
+        # 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
+    def binary(self):
+        """
+        Return True if the file represented by the header is a binary file.
+        Otherwise return False.
+
+        """
+        for h in self.header:
+            if h.startswith('index '):
+                return True
+        return False
+
+    def pretty(self, fp):
+        for h in self.header:
+            if h.startswith('index '):
+                fp.write(_('this modifies a binary file (all or nothing)\n'))
+                break
+            if self.pretty_re.match(h):
+                fp.write(h)
+                if self.binary():
+                    fp.write(_('this is a binary file\n'))
+                break
+            if h.startswith('---'):
+                fp.write(_('%d hunks, %d lines changed\n') %
+                         (len(self.hunks),
+                          sum([h.added + h.removed for h in self.hunks])))
+                break
+            fp.write(h)
+
+    def prettyStr(self):
+        x = cStringIO.StringIO()
+        self.pretty(x)
+        return x.getvalue()
+
+    def write(self, fp):
+        fp.write(''.join(self.header))
+
+    def allhunks(self):
+        """
+        Return True if the file which the header represents was changed
+        completely (i.e.  there is no possibility of applying a hunk of changes
+        smaller than the size of the entire file.)  Otherwise return False
+
+        """
+        for h in self.header:
+            if self.allhunks_re.match(h):
+                return True
+        return False
+
+    def files(self):
+        fromfile, tofile = self.diff_re.match(self.header[0]).groups()
+        if fromfile == tofile:
+            return [fromfile]
+        return [fromfile, tofile]
+
+    def filename(self):
+        return self.files()[-1]
+
+    def __repr__(self):
+        return '<header %s>' % (' '.join(map(repr, self.files())))
+
+    def special(self):
+        for h in self.header:
+            if self.special_re.match(h):
+                return True
+
+    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
+class HunkLine(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 hunk(PatchNode):
+    """patch hunk
+
+    XXX shouldn't we merge this with patch.hunk ?
+    """
+    maxcontext = 3
+
+    def __init__(self, header, fromline, toline, proc, before, hunk, after):
+        def trimcontext(number, lines):
+            delta = len(lines) - self.maxcontext
+            if False and delta > 0:
+                return number + delta, lines[:self.maxcontext]
+            return number, lines
+
+        self.header = header
+        self.fromline, self.before = trimcontext(fromline, before)
+        self.toline, self.after = trimcontext(toline, after)
+        self.proc = proc
+        self.changedLines = [HunkLine(line, self) for line in hunk]
+        self.added, self.removed = self.countchanges()
+        # 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
+        delta = len(self.before) + len(self.after) + removedConvertedToContext
+        if self.after and self.after[-1] == '\\ No newline at end of file\n':
+            delta -= 1
+        fromlen = delta + self.removed
+        tolen = delta + self.added
+        fromToLine = '@@ -%d,%d +%d,%d @@%s\n' % (
+            self.fromline, fromlen, self.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 filename(self):
+        return self.header.filename()
+
+    def prettyStr(self):
+        x = cStringIO.StringIO()
+        self.pretty(x)
+        return x.getvalue()
+
+    def __repr__(self):
+        return '<hunk %r@%d>' % (self.filename(), self.fromline)
+
+
+
+def parsepatch(changes, fp):
+    "Parse a patch, returning a list of header and hunk objects."
+    class parser(object):
+        """patch parsing state machine"""
+        def __init__(self):
+            self.fromline = 0
+            self.toline = 0
+            self.proc = ''
+            self.header = None
+            self.context = []
+            self.before = []
+            self.changedlines = []
+            self.stream = []
+            self.modified, self.added, self.removed = changes
+
+        def _range(self, (fromstart, fromend, tostart, toend, proc)):
+            "Store range line info to associated instance variables."
+            self.fromline = int(fromstart)
+            self.toline = int(tostart)
+            self.proc = proc
+
+        def add_new_hunk(self):
+            """
+            Create a new complete hunk object, adding it to the latest header
+            and to self.stream.
+
+            Add all of the previously collected information about
+            the hunk to the new hunk object.  This information includes
+            header, from/to-lines, function (self.proc), preceding context
+            lines, changed lines, as well as the current context lines (which
+            follow the changed lines).
+
+            The size of the from/to lines are updated to be correct for the
+            next hunk we parse.
+
+            """
+            h = hunk(self.header, self.fromline, self.toline, self.proc,
+                     self.before, self.changedlines, self.context)
+            self.header.hunks.append(h)
+            self.stream.append(h)
+            self.fromline += len(self.before) + h.removed
+            self.toline += len(self.before) + h.added
+            self.before = []
+            self.changedlines = []
+            self.context = []
+            self.proc = ''
+
+        def _context(self, context):
+            """
+            Set the value of self.context.
+
+            Also, if an unprocessed set of changelines was previously
+            encountered, this is the condition for creating a complete
+            hunk object.  In this case, we create and add a new hunk object to
+            the most recent header object, and to self.strem. 
+
+            """
+            self.context = context
+            # if there have been changed lines encountered that haven't yet
+            # been add to a hunk.
+            if self.changedlines:
+                self.add_new_hunk()
+
+        def _changedlines(self, changedlines):
+            """
+            Store the changed lines in self.changedlines.
+
+            Mark any context lines in the context-line buffer (self.context) as
+            lines preceding the changed-lines (i.e. stored in self.before), and
+            clear the context-line buffer.
+
+            """
+            self.changedlines = changedlines
+            self.before = self.context
+            self.context = []
+
+        def add_new_header(self, hdr):
+            """
+            Create a header object containing the header lines, and the
+            filename the header applies to.  Add the header to self.stream.
+
+            """
+            # if there are any lines in the unchanged-lines buffer, create a 
+            # new hunk using them, and add it to the last header.
+            if self.changedlines:
+                self.add_new_hunk()
+
+            # create a new header and add it to self.stream
+            self.header = header(hdr)
+            fileName = self.header.filename()
+            if fileName in self.modified:
+                self.header.changetype = "M"
+            elif fileName in self.added:
+                self.header.changetype = "A"
+            elif fileName in self.removed:
+                self.header.changetype = "R"
+            self.stream.append(self.header)
+
+        def finished(self):
+            # if there are any lines in the unchanged-lines buffer, create a 
+            # new hunk using them, and add it to the last header.
+            if self.changedlines:
+                self.add_new_hunk()
+
+            return self.stream
+
+        transitions = {
+            'file': {'context': _context,
+                     'file': add_new_header,
+                     'hunk': _changedlines,
+                     'range': _range},
+            'context': {'file': add_new_header,
+                        'hunk': _changedlines,
+                        'range': _range},
+            'hunk': {'context': _context,
+                     'file': add_new_header,
+                     'range': _range},
+            'range': {'context': _context,
+                      'hunk': _changedlines},
+            }
+
+    p = parser()
+
+    # run the state-machine
+    state = 'context'
+    for newstate, data in scanpatch(fp):
+        try:
+            p.transitions[state][newstate](p, data)
+        except KeyError:
+            raise patch.PatchError('unhandled transition: %s -> %s' %
+                                   (state, newstate))
+        state = newstate
+    return p.finished()
+
+def filterpatch(opts, chunks, chunk_selector, ui):
+    """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, header)]
+
+    # if there are no changed files
+    if len(headers) == 0:
+        return []
+
+    # let user choose headers/hunks/lines, and mark their applied flags accordingly
+    chunk_selector(opts, headers, ui)
+
+    appliedHunkList = []
+    for hdr in headers:
+        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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgext/histedit/Makefile	Fri Aug 05 21:40:00 2011 +0200
@@ -0,0 +1,26 @@
+PYTHON=python
+
+help:
+	@echo 'Commonly used make targets:'
+	@echo '  tests              - run all tests in the automatic test suite'
+	@echo '  all-version-tests - run all tests against many hg versions'
+	@echo '  tests-%s           - run all tests in the specified hg version'
+
+all: help
+
+tests:
+	cd tests && $(PYTHON) run-tests.py --with-hg=`which hg` $(TESTFLAGS)
+
+test-%:
+	cd tests && $(PYTHON) run-tests.py --with-hg=`which hg` $(TESTFLAGS) $@
+
+tests-%:
+	@echo "Path to crew repo is $(CREW) - set this with CREW= if needed."
+	hg -R $(CREW) checkout $$(echo $@ | sed s/tests-//) && \
+	(cd $(CREW) ; $(MAKE) clean ) && \
+	cd tests && $(PYTHON) $(CREW)/tests/run-tests.py $(TESTFLAGS)
+
+all-version-tests: tests-1.3.1 tests-1.4.3 tests-1.5.4 \
+                   tests-1.6.4 tests-1.7.5 tests-1.8 tests-tip
+
+.PHONY: tests all-version-tests
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgext/histedit/README	Fri Aug 05 21:40:00 2011 +0200
@@ -0,0 +1,143 @@
+.. -*-restructuredtext-*-
+
+============
+histedit
+============
+Interactive history editing for Mercurial.
+
+**Warning**: ``histedit`` alters history! It probably won't destroy your data,
+but that chance exists. You should *never* edit already-published history. 
+Caveat Emptor. (You may want to use the --outgoing option to prevent editing
+pushed history, though the changes may already be in another repository.)
+
+**Warning 2**: If you ``drop`` a change, it's *gone* **forever** (unless you 
+passed ``--keep``). You've been warned!
+
+With this extension installed, Mercurial gains one new command: histedit. Usage
+is as follows, assuming the following history::
+
+ @  3[tip]   7c2fd3b9020c   2009-04-27 18:04 -0500   durin42
+ |    Add delta
+ |
+ o  2   030b686bedc4   2009-04-27 18:04 -0500   durin42
+ |    Add gamma
+ |
+ o  1   c561b4e977df   2009-04-27 18:04 -0500   durin42
+ |    Add beta
+ |
+ o  0   d8d2fcd0e319   2009-04-27 18:04 -0500   durin42
+      Add alpha
+
+If you were to run ``hg histedit c561b4e977df``, you would see the following
+file open in your editor::
+
+ pick c561b4e977df Add beta
+ pick 030b686bedc4 Add gamma
+ pick 7c2fd3b9020c Add delta
+ 
+ # Edit history between 633536316234 and 7c2fd3b9020c
+ #
+ # Commands:
+ #  p, pick = use commit
+ #  e, edit = use commit, but stop for amending
+ #  f, fold = use commit, but fold into previous commit
+ #  d, drop = remove commit from history
+ #
+ 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+
+In this file, lines beginning with ``#`` are ignored. You must specify a rule
+for each revision in your history. For example, if you had meant to add gamma 
+before beta, and then wanted to add delta in the same revision as beta, you 
+would reorganize the file to look like this::
+
+ pick 030b686bedc4 Add gamma
+ pick c561b4e977df Add beta
+ fold 7c2fd3b9020c Add delta
+ 
+ # Edit history between 633536316234 and 7c2fd3b9020c
+ #
+ # Commands:
+ #  p, pick = use commit
+ #  e, edit = use commit, but stop for amending
+ #  f, fold = use commit, but fold into previous commit
+ #  d, drop = remove commit from history
+ #
+ 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+
+At which point you close the editor and ``histedit`` starts working. When you
+specify a ``fold`` operation, ``histedit`` will open an editor when it folds
+those revisions together, offering you a chance to clean up the commit message::
+
+ Add beta
+ ***
+ Add delta
+
+Edit the commit message to your liking, then close the editor. For this example,
+let's assume that the commit message was changed to ``Add beta and delta.`` After
+histedit has run and had a chance to remove any old or temporary revisions it
+needed, the history looks like this::
+
+ @  2[tip]   989b4d060121   2009-04-27 18:04 -0500   durin42
+ |    Add beta and delta.
+ |
+ o  1   081603921c3f   2009-04-27 18:04 -0500   durin42
+ |    Add gamma
+ |
+ o  0   d8d2fcd0e319   2009-04-27 18:04 -0500   durin42
+      Add alpha
+
+Note that ``histedit`` does *not* remove any revisions (even its own temporary 
+ones) until after it has completed all the editing operations, so it will 
+probably perform several strip operations when it's done. For the above example,
+it had to run strip twice. Strip can be slow depending on a variety of factors,
+so you might need to be a little patient. You can choose to keep the original
+revisions by passing the ``--keep`` flag.
+
+The ``edit`` operation will drop you back to a command prompt, allowing you to
+edit files freely, or even use ``hg record`` to commit some changes as a separate
+commit. When you're done, any remaining uncommitted changes will be committed as 
+well. When done, run ``hg histedit --continue`` to finish this step. You'll be 
+prompted for a new commit message, but the default commit message will
+be the original message for the ``edit`` ed revision.
+
+If ``histedit`` encounters a conflict when moving a revision (while handling 
+``pick`` or ``fold``), it'll stop in a similar manner to ``edit`` with the 
+difference that it won't prompt you for a commit message when done. If you
+decide at this point that you don't like how much work it will be to rearrange
+history, or that you made a mistake, you can use ``hg histedit --abort`` to 
+abandon the new changes you have made and return to the state before you attempted
+to edit your history.
+
+============
+--outgoing
+============
+Interactive history editing only pushed changes.
+
+If we clone the example repository above and add three more changes, such that
+we have the following history:
+
+ @  6[tip]   038383181893   2009-04-27 18:04 -0500   stefan
+ |    Add theta
+ |
+ o  5   140988835471   2009-04-27 18:04 -0500   stefan
+ |    Add eta
+ |
+ o  4   122930637314   2009-04-27 18:04 -0500   stefan
+ |    Add zeta
+ |
+ o  3   836302820282   2009-04-27 18:04 -0500   stefan
+ |    Add epsilon
+ |
+ o  2   989b4d060121   2009-04-27 18:04 -0500   durin42
+ |    Add beta and delta.
+ |
+ o  1   081603921c3f   2009-04-27 18:04 -0500   durin42
+ |    Add gamma
+ |
+ o  0   d8d2fcd0e319   2009-04-27 18:04 -0500   durin42
+      Add alpha
+
+If you run "hg histedit --outgoing" on the clone then it is the same as running
+"hg histedit 836302820282". If you need plan to push to a repository that
+Mercurial does not detect to be related to the source repo, you can add a
+--force option.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgext/histedit/hg_histedit.py	Fri Aug 05 21:40:00 2011 +0200
@@ -0,0 +1,446 @@
+"""Interactive history editing.
+
+Inspired by git rebase --interactive.
+"""
+try:
+    import cPickle as pickle
+except ImportError:
+    import pickle
+import tempfile
+import os
+
+from mercurial import cmdutil
+from mercurial import error
+from mercurial import hg
+from mercurial import node
+from mercurial import repair
+from mercurial import patch
+from mercurial import util
+from mercurial import url
+from mercurial import discovery
+from mercurial.i18n import _
+
+# Remove this hack and use cmdutil.updatedir when we're 1.7-only
+def _revsingle(repo, parent):
+    return repo[parent]
+_revsingle = getattr(cmdutil, 'revsingle', _revsingle)
+
+# Remove this hack and use cmdutil.updatedir when we're 1.7-only
+if getattr(cmdutil, 'updatedir', None):
+    from mercurial.cmdutil import updatedir
+else:
+    from mercurial.patch import updatedir
+
+# almost entirely stolen from the git-rebase--interactive.sh source
+editcomment = """
+
+# Edit history between %s and %s
+#
+# Commands:
+#  p, pick = use commit
+#  e, edit = use commit, but stop for amending
+#  f, fold = use commit, but fold into previous commit
+#  d, drop = remove commit from history
+#
+"""
+
+def between(repo, old, new, keep):
+    revs = [old, ]
+    current = old
+    while current != new:
+        ctx = repo[current]
+        if not keep and len(ctx.children()) > 1:
+            raise util.Abort('cannot edit history that would orphan nodes')
+        if len(ctx.parents()) != 1 and ctx.parents()[1] != node.nullid:
+            raise util.Abort("can't edit history with merges")
+        if not ctx.children():
+            current = new
+        else:
+            current = ctx.children()[0].node()
+            revs.append(current)
+    if len(repo[current].children()) and not keep:
+        raise util.Abort('cannot edit history that would orphan nodes')
+    return revs
+
+
+def pick(ui, repo, ctx, ha, opts):
+    oldctx = repo[ha]
+    if oldctx.parents()[0] == ctx:
+        ui.debug('node %s unchanged\n' % ha)
+        return oldctx, [], [], []
+    hg.update(repo, ctx.node())
+    fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
+    fp = os.fdopen(fd, 'w')
+    diffopts = patch.diffopts(ui, opts)
+    diffopts.git = True
+    diffopts.ignorews = False
+    diffopts.ignorewsamount = False
+    diffopts.ignoreblanklines = False
+    gen = patch.diff(repo, oldctx.parents()[0].node(), ha, opts=diffopts)
+    for chunk in gen:
+        fp.write(chunk)
+    fp.close()
+    try:
+        files = {}
+        try:
+            patch.patch(patchfile, ui, cwd=repo.root, files=files, eolmode=None)
+            if not files:
+                ui.warn(_('%s: empty changeset')
+                             % node.hex(ha))
+                return ctx, [], [], []
+        finally:
+            files = updatedir(ui, repo, files)
+            os.unlink(patchfile)
+    except Exception, inst:
+        raise util.Abort(_('Fix up the change and run '
+                           'hg histedit --continue'))
+    n = repo.commit(text=oldctx.description(), user=oldctx.user(), date=oldctx.date(),
+                    extra=oldctx.extra())
+    return repo[n], [n, ], [oldctx.node(), ], []
+
+
+def edit(ui, repo, ctx, ha, opts):
+    oldctx = repo[ha]
+    hg.update(repo, ctx.node())
+    fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
+    fp = os.fdopen(fd, 'w')
+    diffopts = patch.diffopts(ui, opts)
+    diffopts.git = True
+    diffopts.ignorews = False
+    diffopts.ignorewsamount = False
+    diffopts.ignoreblanklines = False
+    gen = patch.diff(repo, oldctx.parents()[0].node(), ha, opts=diffopts)
+    for chunk in gen:
+        fp.write(chunk)
+    fp.close()
+    try:
+        files = {}
+        try:
+            patch.patch(patchfile, ui, cwd=repo.root, files=files, eolmode=None)
+        finally:
+            files = updatedir(ui, repo, files)
+            os.unlink(patchfile)
+    except Exception, inst:
+        pass
+    raise util.Abort('Make changes as needed, you may commit or record as '
+                     'needed now.\nWhen you are finished, run hg'
+                     ' histedit --continue to resume.')
+
+def fold(ui, repo, ctx, ha, opts):
+    oldctx = repo[ha]
+    hg.update(repo, ctx.node())
+    fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
+    fp = os.fdopen(fd, 'w')
+    diffopts = patch.diffopts(ui, opts)
+    diffopts.git = True
+    diffopts.ignorews = False
+    diffopts.ignorewsamount = False
+    diffopts.ignoreblanklines = False
+    gen = patch.diff(repo, oldctx.parents()[0].node(), ha, opts=diffopts)
+    for chunk in gen:
+        fp.write(chunk)
+    fp.close()
+    try:
+        files = {}
+        try:
+            patch.patch(patchfile, ui, cwd=repo.root, files=files, eolmode=None)
+            if not files:
+                ui.warn(_('%s: empty changeset')
+                             % node.hex(ha))
+                return ctx, [], [], []
+        finally:
+            files = updatedir(ui, repo, files)
+            os.unlink(patchfile)
+    except Exception, inst:
+        raise util.Abort(_('Fix up the change and run '
+                           'hg histedit --continue'))
+    n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(), date=oldctx.date(),
+                    extra=oldctx.extra())
+    return finishfold(ui, repo, ctx, oldctx, n, opts, [])
+
+def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
+    parent = ctx.parents()[0].node()
+    hg.update(repo, parent)
+    fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
+    fp = os.fdopen(fd, 'w')
+    diffopts = patch.diffopts(ui, opts)
+    diffopts.git = True
+    diffopts.ignorews = False
+    diffopts.ignorewsamount = False
+    diffopts.ignoreblanklines = False
+    gen = patch.diff(repo, parent, newnode, opts=diffopts)
+    for chunk in gen:
+        fp.write(chunk)
+    fp.close()
+    files = {}
+    try:
+        patch.patch(patchfile, ui, cwd=repo.root, files=files, eolmode=None)
+    finally:
+        files = updatedir(ui, repo, files)
+        os.unlink(patchfile)
+    newmessage = '\n***\n'.join(
+        [ctx.description(), ] +
+        [repo[r].description() for r in internalchanges] +
+        [oldctx.description(), ])
+    # If the changesets are from the same author, keep it.
+    if ctx.user() == oldctx.user():
+        username = ctx.user()
+    else:
+        username = ui.username()
+    newmessage = ui.edit(newmessage, username)
+    n = repo.commit(text=newmessage, user=username, date=max(ctx.date(), oldctx.date()),
+                    extra=oldctx.extra())
+    return repo[n], [n, ], [oldctx.node(), ctx.node() ], [newnode, ] # xxx
+
+def drop(ui, repo, ctx, ha, opts):
+    return ctx, [], [repo[ha].node(), ], []
+
+
+actiontable = {'p': pick,
+               'pick': pick,
+               'e': edit,
+               'edit': edit,
+               'f': fold,
+               'fold': fold,
+               'd': drop,
+               'drop': drop,
+               }
+def histedit(ui, repo, *parent, **opts):
+    """hg histedit <parent>
+    """
+    # TODO only abort if we try and histedit mq patches, not just
+    # blanket if mq patches are applied somewhere
+    mq = getattr(repo, 'mq', None)
+    if mq and mq.applied:
+        raise util.Abort(_('source has mq patches applied'))
+
+    parent = list(parent) + opts.get('rev', [])
+    if opts.get('outgoing'):
+        if len(parent) > 1:
+            raise util.Abort('only one repo argument allowed with --outgoing')
+        elif parent:
+            parent = parent[0]
+
+        dest = ui.expandpath(parent or 'default-push', parent or 'default')
+        dest, revs = hg.parseurl(dest, None)[:2]
+        if isinstance(revs, tuple):
+            # python >= 1.6
+            revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
+            other = hg.repository(hg.remoteui(repo, opts), dest)
+            findoutgoing = discovery.findoutgoing
+        else:
+            other = hg.repository(ui, dest)
+            def findoutgoing(repo, other, force=False):
+                return repo.findoutgoing(other, force=force)
+
+        if revs:
+            revs = [repo.lookup(rev) for rev in revs]
+
+        ui.status(_('comparing with %s\n') % url.hidepassword(dest))
+        parent = findoutgoing(repo, other, force=opts.get('force'))
+    else:
+        if opts.get('force'):
+            raise util.Abort('--force only allowed with --outgoing')
+
+    if opts.get('continue', False):
+        if len(parent) != 0:
+            raise util.Abort('no arguments allowed with --continue')
+        (parentctxnode, created, replaced,
+         tmpnodes, existing, rules, keep, tip, ) = readstate(repo)
+        currentparent, wantnull = repo.dirstate.parents()
+        parentctx = repo[parentctxnode]
+        # discover any nodes the user has added in the interim
+        newchildren = [c for c in parentctx.children()
+                       if c.node() not in existing]
+        action, currentnode = rules.pop(0)
+        while newchildren:
+            if action in ['f', 'fold', ]:
+                tmpnodes.extend([n.node() for n in newchildren])
+            else:
+                created.extend([n.node() for n in newchildren])
+            newchildren = filter(lambda x: x.node() not in existing,
+                                 reduce(lambda x, y: x + y,
+                                        map(lambda r: r.children(),
+                                            newchildren)))
+        m, a, r, d = repo.status()[:4]
+        oldctx = repo[currentnode]
+        message = oldctx.description()
+        if action in ('e', 'edit', ):
+            message = ui.edit(message, ui.username())
+        elif action in ('f', 'fold', ):
+            message = 'fold-temp-revision %s' % currentnode
+        new = None
+        if m or a or r or d:
+            new = repo.commit(text=message, user=oldctx.user(), date=oldctx.date(),
+                              extra=oldctx.extra())
+
+        if action in ('e', 'edit', 'p', 'pick', ):
+            if new != oldctx.node():
+                replaced.append(oldctx.node())
+            if new:
+                if new != oldctx.node():
+                    created.append(new)
+                parentctx = repo[new]
+        else: # fold
+            if new:
+                tmpnodes.append(new)
+            else:
+                new = newchildren[-1]
+            (parentctx, created_,
+             replaced_, tmpnodes_, ) = finishfold(ui, repo,
+                                                  parentctx, oldctx, new,
+                                                  opts, newchildren)
+            replaced.extend(replaced_)
+            created.extend(created_)
+            tmpnodes.extend(tmpnodes_)
+
+    elif opts.get('abort', False):
+        if len(parent) != 0:
+            raise util.Abort('no arguments allowed with --abort')
+        (parentctxnode, created, replaced, tmpnodes,
+         existing, rules, keep, tip, ) = readstate(repo)
+        ui.debug('restore wc to old tip %s\n' % node.hex(tip))
+        hg.clean(repo, tip)
+        ui.debug('should strip created nodes %s\n' %
+                 ', '.join([node.hex(n)[:12] for n in created]))
+        ui.debug('should strip temp nodes %s\n' %
+                 ', '.join([node.hex(n)[:12] for n in tmpnodes]))
+        for nodes in (created, tmpnodes, ):
+            for n in reversed(nodes):
+                try:
+                    repair.strip(ui, repo, n)
+                except error.LookupError:
+                    pass
+        os.unlink(os.path.join(repo.path, 'histedit-state'))
+        return
+    else:
+        cmdutil.bail_if_changed(repo)
+        if os.path.exists(os.path.join(repo.path, 'histedit-state')):
+            raise util.Abort('history edit already in progress, try '
+                             '--continue or --abort')
+
+        tip, empty = repo.dirstate.parents()
+
+
+        if len(parent) != 1:
+            raise util.Abort('requires exactly one parent revision')
+        parent = _revsingle(repo, parent[0]).node()
+
+        keep = opts.get('keep', False)
+        revs = between(repo, parent, tip, keep)
+
+        ctxs = [repo[r] for r in revs]
+        existing = [r.node() for r in ctxs]
+        rules = opts.get('commands', '')
+        if not rules:
+            rules = '\n'.join([('pick %s %s' % (
+                c.hex()[:12], c.description().splitlines()[0]))[:80]
+                               for c in ctxs])
+            rules += editcomment % (node.hex(parent)[:12], node.hex(tip)[:12], )
+            rules = ui.edit(rules, ui.username())
+        else:
+            f = open(rules)
+            rules = f.read()
+            f.close()
+        rules = [l for l in (r.strip() for r in rules.splitlines())
+                 if l and not l[0] == '#']
+        rules = verifyrules(rules, repo, ctxs)
+
+        parentctx = repo[parent].parents()[0]
+        keep = opts.get('keep', False)
+        replaced = []
+        tmpnodes = []
+        created = []
+
+
+    while rules:
+        writestate(repo, parentctx.node(), created, replaced, tmpnodes, existing,
+                   rules, keep, tip)
+        action, ha = rules.pop(0)
+        (parentctx, created_,
+         replaced_, tmpnodes_, ) = actiontable[action](ui, repo,
+                                                       parentctx, ha,
+                                                       opts)
+        created.extend(created_)
+        replaced.extend(replaced_)
+        tmpnodes.extend(tmpnodes_)
+
+    hg.update(repo, parentctx.node())
+
+    if not keep:
+        ui.debug('should strip replaced nodes %s\n' %
+                 ', '.join([node.hex(n)[:12] for n in replaced]))
+        for n in sorted(replaced, lambda x, y: cmp(repo[x].rev(), repo[y].rev())):
+            try:
+                repair.strip(ui, repo, n)
+            except error.LookupError:
+                pass
+
+    ui.debug('should strip temp nodes %s\n' %
+             ', '.join([node.hex(n)[:12] for n in tmpnodes]))
+    for n in reversed(tmpnodes):
+        try:
+            repair.strip(ui, repo, n)
+        except error.LookupError:
+            pass
+    os.unlink(os.path.join(repo.path, 'histedit-state'))
+
+
+def writestate(repo, parentctxnode, created, replaced,
+               tmpnodes, existing, rules, keep, oldtip):
+    fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
+    pickle.dump((parentctxnode, created, replaced,
+                 tmpnodes, existing, rules, keep, oldtip,),
+                fp)
+    fp.close()
+
+def readstate(repo):
+    """Returns a tuple of (parentnode, created, replaced, tmp, existing, rules, keep, oldtip, ).
+    """
+    fp = open(os.path.join(repo.path, 'histedit-state'))
+    return pickle.load(fp)
+
+
+def verifyrules(rules, repo, ctxs):
+    """Verify that there exists exactly one edit rule per given changeset.
+
+    Will abort if there are to many or too few rules, a malformed rule,
+    or a rule on a changeset outside of the user-given range.
+    """
+    parsed = []
+    first = True
+    if len(rules) != len(ctxs):
+        raise util.Abort('must specify a rule for each changeset once')
+    for r in rules:
+        if ' ' not in r:
+            raise util.Abort('malformed line "%s"' % r)
+        action, rest = r.split(' ', 1)
+        if ' ' in rest.strip():
+            ha, rest = rest.split(' ', 1)
+        else:
+            ha = r.strip()
+        try:
+            if repo[ha] not in ctxs:
+                raise util.Abort('may not use changesets other than the ones listed')
+        except error.RepoError:
+            raise util.Abort('unknown changeset %s listed' % ha)
+        if action not in actiontable:
+            raise util.Abort('unknown action "%s"' % action)
+        parsed.append([action, ha])
+    return parsed
+
+
+cmdtable = {
+    "histedit":
+        (histedit,
+         [('', 'commands', '', 'Read history edits from the specified file.'),
+          ('c', 'continue', False, 'continue an edit already in progress', ),
+          ('k', 'keep', False, 'strip old nodes after edit is complete', ),
+          ('', 'abort', False, 'abort an edit in progress', ),
+          ('o', 'outgoing', False, 'changesets not found in destination'),
+          ('f', 'force', False, 'force outgoing even for unrelated repositories'),
+          ('r', 'rev', [], _('first revision to be edited')),
+          ],
+         __doc__,
+         ),
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgext/histedit/setup.py	Fri Aug 05 21:40:00 2011 +0200
@@ -0,0 +1,25 @@
+try:
+    from setuptools import setup
+except:
+    from distutils.core import setup
+
+requires = []
+try:
+    import mercurial
+except ImportError:
+    requires.append('mercurial')
+
+setup(
+    name='hg-histedit',
+    version='1.0.0',
+    author='Augie Fackler',
+    maintainer='Augie Fackler',
+    maintainer_email='durin42@gmail.com',
+    url='http://bitbucket.org/durin42/histedit/',
+    description='Interactively edit history in Mercurial.',
+    long_description=open('README').read(),
+    keywords='hg mercurial',
+    license='GPLv2+',
+    py_modules=['hg_histedit'],
+    install_requires=requires,
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgext/histedit/tests/histedit-helpers.sh	Fri Aug 05 21:40:00 2011 +0200
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+fixbundle() {
+    grep -v 'saving bundle' | grep -v 'saved backup' | grep -v added | grep -v adding
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgext/histedit/tests/run-tests.py	Fri Aug 05 21:40:00 2011 +0200
@@ -0,0 +1,708 @@
+#!/usr/bin/env python
+#
+# run-tests.py - Run a set of tests on Mercurial
+#
+# Copyright 2006 Matt Mackall <mpm@selenic.com>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2, incorporated herein by reference.
+
+import difflib
+import errno
+import optparse
+import os
+try:
+    import subprocess
+    subprocess.Popen  # trigger ImportError early
+    closefds = os.name == 'posix'
+    def Popen4(cmd, bufsize=-1):
+        p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
+                             close_fds=closefds,
+                             stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+                             stderr=subprocess.STDOUT)
+        p.fromchild = p.stdout
+        p.tochild = p.stdin
+        p.childerr = p.stderr
+        return p
+except ImportError:
+    subprocess = None
+    from popen2 import Popen4
+import shutil
+import signal
+import sys
+import tempfile
+import time
+
+# reserved exit code to skip test (used by hghave)
+SKIPPED_STATUS = 80
+SKIPPED_PREFIX = 'skipped: '
+FAILED_PREFIX  = 'hghave check failed: '
+PYTHON = sys.executable
+
+requiredtools = ["python", "diff", "grep", "unzip", "gunzip", "bunzip2", "sed"]
+
+defaults = {
+    'jobs': ('HGTEST_JOBS', 1),
+    'timeout': ('HGTEST_TIMEOUT', 180),
+    'port': ('HGTEST_PORT', 20059),
+}
+
+def parseargs():
+    parser = optparse.OptionParser("%prog [options] [tests]")
+    parser.add_option("-C", "--annotate", action="store_true",
+        help="output files annotated with coverage")
+    parser.add_option("--child", type="int",
+        help="run as child process, summary to given fd")
+    parser.add_option("-c", "--cover", action="store_true",
+        help="print a test coverage report")
+    parser.add_option("-f", "--first", action="store_true",
+        help="exit on the first test failure")
+    parser.add_option("-i", "--interactive", action="store_true",
+        help="prompt to accept changed output")
+    parser.add_option("-j", "--jobs", type="int",
+        help="number of jobs to run in parallel"
+             " (default: $%s or %d)" % defaults['jobs'])
+    parser.add_option("--keep-tmpdir", action="store_true",
+        help="keep temporary directory after running tests"
+             " (best used with --tmpdir)")
+    parser.add_option("-R", "--restart", action="store_true",
+        help="restart at last error")
+    parser.add_option("-p", "--port", type="int",
+        help="port on which servers should listen"
+             " (default: $%s or %d)" % defaults['port'])
+    parser.add_option("-r", "--retest", action="store_true",
+        help="retest failed tests")
+    parser.add_option("-s", "--cover_stdlib", action="store_true",
+        help="print a test coverage report inc. standard libraries")
+    parser.add_option("-t", "--timeout", type="int",
+        help="kill errant tests after TIMEOUT seconds"
+             " (default: $%s or %d)" % defaults['timeout'])
+    parser.add_option("--tmpdir", type="string",
+        help="run tests in the given temporary directory")
+    parser.add_option("-v", "--verbose", action="store_true",
+        help="output verbose messages")
+    parser.add_option("-n", "--nodiff", action="store_true",
+        help="skip showing test changes")
+    parser.add_option("--with-hg", type="string",
+        help="test existing install at given location")
+    parser.add_option("--pure", action="store_true",
+        help="use pure Python code instead of C extensions")
+
+    for option, default in defaults.items():
+        defaults[option] = int(os.environ.get(*default))
+    parser.set_defaults(**defaults)
+    (options, args) = parser.parse_args()
+
+    global vlog
+    options.anycoverage = (options.cover or
+                           options.cover_stdlib or
+                           options.annotate)
+
+    if options.verbose:
+        def vlog(*msg):
+            for m in msg:
+                print m,
+            print
+    else:
+        vlog = lambda *msg: None
+
+    if options.jobs < 1:
+        print >> sys.stderr, 'ERROR: -j/--jobs must be positive'
+        sys.exit(1)
+    if options.interactive and options.jobs > 1:
+        print '(--interactive overrides --jobs)'
+        options.jobs = 1
+
+    return (options, args)
+
+def rename(src, dst):
+    """Like os.rename(), trade atomicity and opened files friendliness
+    for existing destination support.
+    """
+    shutil.copy(src, dst)
+    os.remove(src)
+
+def splitnewlines(text):
+    '''like str.splitlines, but only split on newlines.
+    keep line endings.'''
+    i = 0
+    lines = []
+    while True:
+        n = text.find('\n', i)
+        if n == -1:
+            last = text[i:]
+            if last:
+                lines.append(last)
+            return lines
+        lines.append(text[i:n+1])
+        i = n + 1
+
+def parsehghaveoutput(lines):
+    '''Parse hghave log lines.
+    Return tuple of lists (missing, failed):
+      * the missing/unknown features
+      * the features for which existence check failed'''
+    missing = []
+    failed = []
+    for line in lines:
+        if line.startswith(SKIPPED_PREFIX):
+            line = line.splitlines()[0]
+            missing.append(line[len(SKIPPED_PREFIX):])
+        elif line.startswith(FAILED_PREFIX):
+            line = line.splitlines()[0]
+            failed.append(line[len(FAILED_PREFIX):])
+
+    return missing, failed
+
+def showdiff(expected, output):
+    for line in difflib.unified_diff(expected, output,
+            "Expected output", "Test output"):
+        sys.stdout.write(line)
+
+def findprogram(program):
+    """Search PATH for a executable program"""
+    for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
+        name = os.path.join(p, program)
+        if os.access(name, os.X_OK):
+            return name
+    return None
+
+def checktools():
+    # Before we go any further, check for pre-requisite tools
+    # stuff from coreutils (cat, rm, etc) are not tested
+    for p in requiredtools:
+        if os.name == 'nt':
+            p += '.exe'
+        found = findprogram(p)
+        if found:
+            vlog("# Found prerequisite", p, "at", found)
+        else:
+            print "WARNING: Did not find prerequisite tool: "+p
+
+def cleanup(options):
+    if not options.keep_tmpdir:
+        if options.verbose:
+            print "# Cleaning up HGTMP", HGTMP
+        shutil.rmtree(HGTMP, True)
+
+def usecorrectpython():
+    # some tests run python interpreter. they must use same
+    # interpreter we use or bad things will happen.
+    exedir, exename = os.path.split(sys.executable)
+    if exename == 'python':
+        path = findprogram('python')
+        if os.path.dirname(path) == exedir:
+            return
+    vlog('# Making python executable in test path use correct Python')
+    mypython = os.path.join(BINDIR, 'python')
+    try:
+        os.symlink(sys.executable, mypython)
+    except AttributeError:
+        # windows fallback
+        shutil.copyfile(sys.executable, mypython)
+        shutil.copymode(sys.executable, mypython)
+
+def installhg(options):
+    global PYTHON
+    vlog("# Performing temporary installation of HG")
+    installerrs = os.path.join("tests", "install.err")
+    pure = options.pure and "--pure" or ""
+
+    # Run installer in hg root
+    os.chdir(os.path.join(os.path.dirname(sys.argv[0]), '..'))
+    cmd = ('%s setup.py %s clean --all'
+           ' install --force --prefix="%s" --install-lib="%s"'
+           ' --install-scripts="%s" >%s 2>&1'
+           % (sys.executable, pure, INST, PYTHONDIR, BINDIR, installerrs))
+    vlog("# Running", cmd)
+    if os.system(cmd) == 0:
+        if not options.verbose:
+            os.remove(installerrs)
+    else:
+        f = open(installerrs)
+        for line in f:
+            print line,
+        f.close()
+        sys.exit(1)
+    os.chdir(TESTDIR)
+
+    os.environ["PATH"] = "%s%s%s" % (BINDIR, os.pathsep, os.environ["PATH"])
+
+    pydir = os.pathsep.join([PYTHONDIR, TESTDIR])
+    pythonpath = os.environ.get("PYTHONPATH")
+    if pythonpath:
+        pythonpath = pydir + os.pathsep + pythonpath
+    else:
+        pythonpath = pydir
+    os.environ["PYTHONPATH"] = pythonpath
+
+    usecorrectpython()
+    global hgpkg
+    hgpkg = _hgpath()
+
+    vlog("# Installing dummy diffstat")
+    f = open(os.path.join(BINDIR, 'diffstat'), 'w')
+    f.write('#!' + sys.executable + '\n'
+            'import sys\n'
+            'files = 0\n'
+            'for line in sys.stdin:\n'
+            '    if line.startswith("diff "):\n'
+            '        files += 1\n'
+            'sys.stdout.write("files patched: %d\\n" % files)\n')
+    f.close()
+    os.chmod(os.path.join(BINDIR, 'diffstat'), 0700)
+
+    if options.anycoverage:
+        vlog("# Installing coverage wrapper")
+        os.environ['COVERAGE_FILE'] = COVERAGE_FILE
+        if os.path.exists(COVERAGE_FILE):
+            os.unlink(COVERAGE_FILE)
+        # Create a wrapper script to invoke hg via coverage.py
+        os.rename(os.path.join(BINDIR, "hg"), os.path.join(BINDIR, "_hg.py"))
+        f = open(os.path.join(BINDIR, 'hg'), 'w')
+        f.write('#!' + sys.executable + '\n')
+        f.write('import sys, os; os.execv(sys.executable, [sys.executable, '
+                '"%s", "-x", "%s"] + sys.argv[1:])\n' %
+                (os.path.join(TESTDIR, 'coverage.py'),
+                 os.path.join(BINDIR, '_hg.py')))
+        f.close()
+        os.chmod(os.path.join(BINDIR, 'hg'), 0700)
+        PYTHON = '"%s" "%s" -x' % (sys.executable,
+                                   os.path.join(TESTDIR,'coverage.py'))
+
+def _hgpath():
+    cmd = '%s -c "import mercurial; print mercurial.__path__[0]"'
+    hgpath = os.popen(cmd % PYTHON)
+    path = hgpath.read().strip()
+    hgpath.close()
+    return path
+
+def outputcoverage(options):
+    vlog("# Producing coverage report")
+    omit = [BINDIR, TESTDIR, PYTHONDIR]
+    if not options.cover_stdlib:
+        # Exclude as system paths (ignoring empty strings seen on win)
+        omit += [x for x in sys.path if x != '']
+    omit = ','.join(omit)
+    os.chdir(PYTHONDIR)
+    cmd = '"%s" "%s" -i -r "--omit=%s"' % (
+        sys.executable, os.path.join(TESTDIR, 'coverage.py'), omit)
+    vlog("# Running: "+cmd)
+    os.system(cmd)
+    if options.annotate:
+        adir = os.path.join(TESTDIR, 'annotated')
+        if not os.path.isdir(adir):
+            os.mkdir(adir)
+        cmd = '"%s" "%s" -i -a "--directory=%s" "--omit=%s"' % (
+            sys.executable, os.path.join(TESTDIR, 'coverage.py'),
+            adir, omit)
+        vlog("# Running: "+cmd)
+        os.system(cmd)
+
+class Timeout(Exception):
+    pass
+
+def alarmed(signum, frame):
+    raise Timeout
+
+def run(cmd, options):
+    """Run command in a sub-process, capturing the output (stdout and stderr).
+    Return the exist code, and output."""
+    # TODO: Use subprocess.Popen if we're running on Python 2.4
+    if os.name == 'nt' or sys.platform.startswith('java'):
+        tochild, fromchild = os.popen4(cmd)
+        tochild.close()
+        output = fromchild.read()
+        ret = fromchild.close()
+        if ret == None:
+            ret = 0
+    else:
+        proc = Popen4(cmd)
+        try:
+            output = ''
+            proc.tochild.close()
+            output = proc.fromchild.read()
+            ret = proc.wait()
+            if os.WIFEXITED(ret):
+                ret = os.WEXITSTATUS(ret)
+        except Timeout:
+            vlog('# Process %d timed out - killing it' % proc.pid)
+            os.kill(proc.pid, signal.SIGTERM)
+            ret = proc.wait()
+            if ret == 0:
+                ret = signal.SIGTERM << 8
+            output += ("\n### Abort: timeout after %d seconds.\n"
+                       % options.timeout)
+    return ret, splitnewlines(output)
+
+def runone(options, test, skips, fails):
+    '''tristate output:
+    None -> skipped
+    True -> passed
+    False -> failed'''
+
+    def skip(msg):
+        if not options.verbose:
+            skips.append((test, msg))
+        else:
+            print "\nSkipping %s: %s" % (test, msg)
+        return None
+
+    def fail(msg):
+        fails.append((test, msg))
+        if not options.nodiff:
+            print "\nERROR: %s %s" % (test, msg)
+        return None
+
+    vlog("# Test", test)
+
+    # create a fresh hgrc
+    hgrc = file(HGRCPATH, 'w+')
+    hgrc.write('[ui]\n')
+    hgrc.write('slash = True\n')
+    hgrc.write('[defaults]\n')
+    hgrc.write('backout = -d "0 0"\n')
+    hgrc.write('commit = -d "0 0"\n')
+    hgrc.write('debugrawcommit = -d "0 0"\n')
+    hgrc.write('tag = -d "0 0"\n')
+    hgrc.close()
+
+    err = os.path.join(TESTDIR, test+".err")
+    ref = os.path.join(TESTDIR, test+".out")
+    testpath = os.path.join(TESTDIR, test)
+
+    if os.path.exists(err):
+        os.remove(err)       # Remove any previous output files
+
+    # Make a tmp subdirectory to work in
+    tmpd = os.path.join(HGTMP, test)
+    os.mkdir(tmpd)
+    os.chdir(tmpd)
+
+    try:
+        tf = open(testpath)
+        firstline = tf.readline().rstrip()
+        tf.close()
+    except:
+        firstline = ''
+    lctest = test.lower()
+
+    if lctest.endswith('.py') or firstline == '#!/usr/bin/env python':
+        cmd = '%s "%s"' % (PYTHON, testpath)
+    elif lctest.endswith('.bat'):
+        # do not run batch scripts on non-windows
+        if os.name != 'nt':
+            return skip("batch script")
+        # To reliably get the error code from batch files on WinXP,
+        # the "cmd /c call" prefix is needed. Grrr
+        cmd = 'cmd /c call "%s"' % testpath
+    else:
+        # do not run shell scripts on windows
+        if os.name == 'nt':
+            return skip("shell script")
+        # do not try to run non-executable programs
+        if not os.path.exists(testpath):
+            return fail("does not exist")
+        elif not os.access(testpath, os.X_OK):
+            return skip("not executable")
+        cmd = '"%s"' % testpath
+
+    if options.timeout > 0:
+        signal.alarm(options.timeout)
+
+    vlog("# Running", cmd)
+    ret, out = run(cmd, options)
+    vlog("# Ret was:", ret)
+
+    if options.timeout > 0:
+        signal.alarm(0)
+
+    mark = '.'
+
+    skipped = (ret == SKIPPED_STATUS)
+    # If reference output file exists, check test output against it
+    if os.path.exists(ref):
+        f = open(ref, "r")
+        refout = splitnewlines(f.read())
+        f.close()
+    else:
+        refout = []
+    if skipped:
+        mark = 's'
+        missing, failed = parsehghaveoutput(out)
+        if not missing:
+            missing = ['irrelevant']
+        if failed:
+            fail("hghave failed checking for %s" % failed[-1])
+            skipped = False
+        else:
+            skip(missing[-1])
+    elif out != refout:
+        mark = '!'
+        if ret:
+            fail("output changed and returned error code %d" % ret)
+        else:
+            fail("output changed")
+        if not options.nodiff:
+            showdiff(refout, out)
+        ret = 1
+    elif ret:
+        mark = '!'
+        fail("returned error code %d" % ret)
+
+    if not options.verbose:
+        sys.stdout.write(mark)
+        sys.stdout.flush()
+
+    if ret != 0 and not skipped:
+        # Save errors to a file for diagnosis
+        f = open(err, "wb")
+        for line in out:
+            f.write(line)
+        f.close()
+
+    # Kill off any leftover daemon processes
+    try:
+        fp = file(DAEMON_PIDS)
+        for line in fp:
+            try:
+                pid = int(line)
+            except ValueError:
+                continue
+            try:
+                os.kill(pid, 0)
+                vlog('# Killing daemon process %d' % pid)
+                os.kill(pid, signal.SIGTERM)
+                time.sleep(0.25)
+                os.kill(pid, 0)
+                vlog('# Daemon process %d is stuck - really killing it' % pid)
+                os.kill(pid, signal.SIGKILL)
+            except OSError, err:
+                if err.errno != errno.ESRCH:
+                    raise
+        fp.close()
+        os.unlink(DAEMON_PIDS)
+    except IOError:
+        pass
+
+    os.chdir(TESTDIR)
+    if not options.keep_tmpdir:
+        shutil.rmtree(tmpd, True)
+    if skipped:
+        return None
+    return ret == 0
+
+def runchildren(options, expecthg, tests):
+    if not options.with_hg:
+        installhg(options)
+        if hgpkg != expecthg:
+            print '# Testing unexpected mercurial: %s' % hgpkg
+
+    optcopy = dict(options.__dict__)
+    optcopy['jobs'] = 1
+    optcopy['with_hg'] = INST
+    opts = []
+    for opt, value in optcopy.iteritems():
+        name = '--' + opt.replace('_', '-')
+        if value is True:
+            opts.append(name)
+        elif value is not None:
+            opts.append(name + '=' + str(value))
+
+    tests.reverse()
+    jobs = [[] for j in xrange(options.jobs)]
+    while tests:
+        for job in jobs:
+            if not tests: break
+            job.append(tests.pop())
+    fps = {}
+    for j, job in enumerate(jobs):
+        if not job:
+            continue
+        rfd, wfd = os.pipe()
+        childopts = ['--child=%d' % wfd, '--port=%d' % (options.port + j * 3)]
+        cmdline = [PYTHON, sys.argv[0]] + opts + childopts + job
+        vlog(' '.join(cmdline))
+        fps[os.spawnvp(os.P_NOWAIT, cmdline[0], cmdline)] = os.fdopen(rfd, 'r')
+        os.close(wfd)
+    failures = 0
+    tested, skipped, failed = 0, 0, 0
+    skips = []
+    fails = []
+    while fps:
+        pid, status = os.wait()
+        fp = fps.pop(pid)
+        l = fp.read().splitlines()
+        test, skip, fail = map(int, l[:3])
+        split = -fail or len(l)
+        for s in l[3:split]:
+            skips.append(s.split(" ", 1))
+        for s in l[split:]:
+            fails.append(s.split(" ", 1))
+        tested += test
+        skipped += skip
+        failed += fail
+        vlog('pid %d exited, status %d' % (pid, status))
+        failures |= status
+    print
+    for s in skips:
+        print "Skipped %s: %s" % (s[0], s[1])
+    for s in fails:
+        print "Failed %s: %s" % (s[0], s[1])
+
+    if hgpkg != expecthg:
+        print '# Tested unexpected mercurial: %s' % hgpkg
+    print "# Ran %d tests, %d skipped, %d failed." % (
+        tested, skipped, failed)
+    sys.exit(failures != 0)
+
+def runtests(options, expecthg, tests):
+    global DAEMON_PIDS, HGRCPATH
+    DAEMON_PIDS = os.environ["DAEMON_PIDS"] = os.path.join(HGTMP, 'daemon.pids')
+    HGRCPATH = os.environ["HGRCPATH"] = os.path.join(HGTMP, '.hgrc')
+
+    try:
+        if not options.with_hg:
+            installhg(options)
+
+            if hgpkg != expecthg:
+                print '# Testing unexpected mercurial: %s' % hgpkg
+
+        if options.timeout > 0:
+            try:
+                signal.signal(signal.SIGALRM, alarmed)
+                vlog('# Running tests with %d-second timeout' %
+                     options.timeout)
+            except AttributeError:
+                print 'WARNING: cannot run tests with timeouts'
+                options.timeout = 0
+
+        tested = 0
+        failed = 0
+        skipped = 0
+
+        if options.restart:
+            orig = list(tests)
+            while tests:
+                if os.path.exists(tests[0] + ".err"):
+                    break
+                tests.pop(0)
+            if not tests:
+                print "running all tests"
+                tests = orig
+
+        skips = []
+        fails = []
+        for test in tests:
+            if options.retest and not os.path.exists(test + ".err"):
+                skipped += 1
+                continue
+            ret = runone(options, test, skips, fails)
+            if ret is None:
+                skipped += 1
+            elif not ret:
+                if options.interactive:
+                    print "Accept this change? [n] ",
+                    answer = sys.stdin.readline().strip()
+                    if answer.lower() in "y yes".split():
+                        rename(test + ".err", test + ".out")
+                        tested += 1
+                        fails.pop()
+                        continue
+                failed += 1
+                if options.first:
+                    break
+            tested += 1
+
+        if options.child:
+            fp = os.fdopen(options.child, 'w')
+            fp.write('%d\n%d\n%d\n' % (tested, skipped, failed))
+            for s in skips:
+                fp.write("%s %s\n" % s)
+            for s in fails:
+                fp.write("%s %s\n" % s)
+            fp.close()
+        else:
+            print
+            for s in skips:
+                print "Skipped %s: %s" % s
+            for s in fails:
+                print "Failed %s: %s" % s
+            if hgpkg != expecthg:
+                print '# Tested unexpected mercurial: %s' % hgpkg
+            print "# Ran %d tests, %d skipped, %d failed." % (
+                tested, skipped, failed)
+
+        if options.anycoverage:
+            outputcoverage(options)
+    except KeyboardInterrupt:
+        failed = True
+        print "\ninterrupted!"
+
+    if failed:
+        sys.exit(1)
+
+hgpkg = None
+def main():
+    (options, args) = parseargs()
+    if not options.child:
+        os.umask(022)
+
+        checktools()
+
+    # Reset some environment variables to well-known values so that
+    # the tests produce repeatable output.
+    os.environ['LANG'] = os.environ['LC_ALL'] = 'C'
+    os.environ['TZ'] = 'GMT'
+    os.environ["EMAIL"] = "Foo Bar <foo.bar@example.com>"
+    os.environ['CDPATH'] = ''
+
+    global TESTDIR, HGTMP, INST, BINDIR, PYTHONDIR, COVERAGE_FILE
+    TESTDIR = os.environ["TESTDIR"] = os.getcwd()
+    HGTMP = os.environ['HGTMP'] = os.path.realpath(tempfile.mkdtemp('', 'hgtests.',
+                                                   options.tmpdir))
+    DAEMON_PIDS = None
+    HGRCPATH = None
+
+    os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
+    os.environ["HGMERGE"] = "internal:merge"
+    os.environ["HGUSER"]   = "test"
+    os.environ["HGENCODING"] = "ascii"
+    os.environ["HGENCODINGMODE"] = "strict"
+    os.environ["HGPORT"] = str(options.port)
+    os.environ["HGPORT1"] = str(options.port + 1)
+    os.environ["HGPORT2"] = str(options.port + 2)
+
+    if options.with_hg:
+        INST = options.with_hg
+    else:
+        INST = os.path.join(HGTMP, "install")
+    BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin")
+    PYTHONDIR = os.path.join(INST, "lib", "python")
+    COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
+
+    expecthg = os.path.join(HGTMP, 'install', 'lib', 'python', 'mercurial')
+
+    if len(args) == 0:
+        args = os.listdir(".")
+        args.sort()
+
+    tests = []
+    for test in args:
+        if (test.startswith("test-") and '~' not in test and
+            ('.' not in test or test.endswith('.py') or
+             test.endswith('.bat'))):
+            tests.append(test)
+
+    vlog("# Using TESTDIR", TESTDIR)
+    vlog("# Using HGTMP", HGTMP)
+
+    try:
+        if len(tests) > 1 and options.jobs > 1:
+            runchildren(options, expecthg, tests)
+        else:
+            runtests(options, expecthg, tests)
+    finally:
+        cleanup(options)
+
+main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgext/histedit/tests/test-histedit-commute	Fri Aug 05 21:40:00 2011 +0200
@@ -0,0 +1,86 @@
+#!/bin/sh
+
+. "$TESTDIR/histedit-helpers.sh"
+
+cat >> $HGRCPATH <<EOF
+[extensions]
+hgext.graphlog=
+EOF
+
+echo "histedit=$(echo $(dirname $(dirname $0)))/hg_histedit.py" >> $HGRCPATH
+
+
+EDITED=`pwd`/editedhistory
+cat > $EDITED <<EOF
+pick 177f92b77385 c
+pick e860deea161a e
+pick 652413bf663e f
+pick 055a42cdd887 d
+EOF
+initrepo ()
+{
+    hg init r
+    cd r
+    for x in a b c d e f ; do
+        echo $x > $x
+        hg add $x
+        hg ci -m $x
+    done
+}
+
+initrepo
+
+echo % log before edit
+hg log --graph
+
+echo % show the edit commands offered
+HGEDITOR=cat hg histedit 177f92b77385
+
+echo % edit the history
+HGEDITOR="cat $EDITED > " hg histedit 177f92b77385 2>&1 | fixbundle
+
+echo % log after edit
+hg log --graph
+
+echo % put things back
+
+cat > $EDITED <<EOF
+pick 177f92b77385 c
+pick 853c68da763f d
+pick b069cc29fb22 e
+pick 26f6a030ae82 f
+EOF
+HGEDITOR="cat $EDITED > " hg histedit 177f92b77385 2>&1 | fixbundle
+
+hg log --graph
+
+
+echo % slightly different this time
+
+cat > $EDITED <<EOF
+pick 055a42cdd887 d
+pick 652413bf663e f
+pick e860deea161a e
+pick 177f92b77385 c
+EOF
+HGEDITOR="cat $EDITED > " hg histedit 177f92b77385 2>&1 | fixbundle
+hg log --graph
+
+
+echo % keep prevents stripping dead revs
+cat > $EDITED <<EOF
+pick bfe4a5a76b37 d
+pick c4f52e213402 f
+pick 99a62755c625 c
+pick 7c6fdd608667 e
+EOF
+HGEDITOR="cat $EDITED > " hg histedit bfe4a5a76b37 --keep 2>&1 | fixbundle
+hg log --graph
+
+echo '% try with --rev'
+cat > $EDITED <<EOF
+pick 7c6fdd608667 e
+pick 99a62755c625 c
+EOF
+hg histedit --commands "$EDITED" --rev -2 2>&1 | fixbundle
+hg log --graph
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgext/histedit/tests/test-histedit-commute.out	Fri Aug 05 21:40:00 2011 +0200
@@ -0,0 +1,247 @@
+% log before edit
+@  changeset:   5:652413bf663e
+|  tag:         tip
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     f
+|
+o  changeset:   4:e860deea161a
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     e
+|
+o  changeset:   3:055a42cdd887
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     d
+|
+o  changeset:   2:177f92b77385
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     c
+|
+o  changeset:   1:d2ae7f538514
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     b
+|
+o  changeset:   0:cb9a9f314b8b
+   user:        test
+   date:        Thu Jan 01 00:00:00 1970 +0000
+   summary:     a
+
+% show the edit commands offered
+pick 177f92b77385 c
+pick 055a42cdd887 d
+pick e860deea161a e
+pick 652413bf663e f
+
+# Edit history between 177f92b77385 and 652413bf663e
+#
+# Commands:
+#  p, pick = use commit
+#  e, edit = use commit, but stop for amending
+#  f, fold = use commit, but fold into previous commit
+#  d, drop = remove commit from history
+#
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+% edit the history
+0 files updated, 0 files merged, 3 files removed, 0 files unresolved
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+% log after edit
+@  changeset:   5:853c68da763f
+|  tag:         tip
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     d
+|
+o  changeset:   4:26f6a030ae82
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     f
+|
+o  changeset:   3:b069cc29fb22
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     e
+|
+o  changeset:   2:177f92b77385
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     c
+|
+o  changeset:   1:d2ae7f538514
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     b
+|
+o  changeset:   0:cb9a9f314b8b
+   user:        test
+   date:        Thu Jan 01 00:00:00 1970 +0000
+   summary:     a
+
+% put things back
+0 files updated, 0 files merged, 3 files removed, 0 files unresolved
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+@  changeset:   5:652413bf663e
+|  tag:         tip
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     f
+|
+o  changeset:   4:e860deea161a
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     e
+|
+o  changeset:   3:055a42cdd887
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     d
+|
+o  changeset:   2:177f92b77385
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     c
+|
+o  changeset:   1:d2ae7f538514
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     b
+|
+o  changeset:   0:cb9a9f314b8b
+   user:        test
+   date:        Thu Jan 01 00:00:00 1970 +0000
+   summary:     a
+
+% slightly different this time
+0 files updated, 0 files merged, 4 files removed, 0 files unresolved
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+@  changeset:   5:99a62755c625
+|  tag:         tip
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     c
+|
+o  changeset:   4:7c6fdd608667
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     e
+|
+o  changeset:   3:c4f52e213402
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     f
+|
+o  changeset:   2:bfe4a5a76b37
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     d
+|
+o  changeset:   1:d2ae7f538514
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     b
+|
+o  changeset:   0:cb9a9f314b8b
+   user:        test
+   date:        Thu Jan 01 00:00:00 1970 +0000
+   summary:     a
+
+% keep prevents stripping dead revs
+0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+@  changeset:   7:99e266581538
+|  tag:         tip
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     e
+|
+o  changeset:   6:5ad36efb0653
+|  parent:      3:c4f52e213402
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     c
+|
+| o  changeset:   5:99a62755c625
+| |  user:        test
+| |  date:        Thu Jan 01 00:00:00 1970 +0000
+| |  summary:     c
+| |
+| o  changeset:   4:7c6fdd608667
+|/   user:        test
+|    date:        Thu Jan 01 00:00:00 1970 +0000
+|    summary:     e
+|
+o  changeset:   3:c4f52e213402
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     f
+|
+o  changeset:   2:bfe4a5a76b37
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     d
+|
+o  changeset:   1:d2ae7f538514
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     b
+|
+o  changeset:   0:cb9a9f314b8b
+   user:        test
+   date:        Thu Jan 01 00:00:00 1970 +0000
+   summary:     a
+
+% try with --rev
+abort: may not use changesets other than the ones listed
+@  changeset:   7:99e266581538
+|  tag:         tip
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     e
+|
+o  changeset:   6:5ad36efb0653
+|  parent:      3:c4f52e213402
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     c
+|
+| o  changeset:   5:99a62755c625
+| |  user:        test
+| |  date:        Thu Jan 01 00:00:00 1970 +0000
+| |  summary:     c
+| |
+| o  changeset:   4:7c6fdd608667
+|/   user:        test
+|    date:        Thu Jan 01 00:00:00 1970 +0000
+|    summary:     e
+|
+o  changeset:   3:c4f52e213402
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     f
+|
+o  changeset:   2:bfe4a5a76b37
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     d
+|
+o  changeset:   1:d2ae7f538514
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     b
+|
+o  changeset:   0:cb9a9f314b8b
+   user:        test
+   date:        Thu Jan 01 00:00:00 1970 +0000
+   summary:     a
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgext/histedit/tests/test-histedit-drop	Fri Aug 05 21:40:00 2011 +0200
@@ -0,0 +1,43 @@
+#!/bin/sh
+
+. "$TESTDIR/histedit-helpers.sh"
+
+cat >> $HGRCPATH <<EOF
+[extensions]
+hgext.graphlog=
+EOF
+echo "histedit=$(echo $(dirname $(dirname $0)))/hg_histedit.py" >> $HGRCPATH
+
+EDITED=`pwd`/editedhistory
+cat > $EDITED <<EOF
+drop 177f92b77385 c
+pick e860deea161a e
+pick 652413bf663e f
+pick 055a42cdd887 d
+EOF
+initrepo ()
+{
+    hg init r
+    cd r
+    for x in a b c d e f ; do
+        echo $x > $x
+        hg add $x
+        hg ci -m $x
+    done
+}
+
+initrepo
+
+echo % log before edit
+hg log --graph
+
+echo % edit the history
+HGEDITOR="cat $EDITED > " hg histedit 177f92b77385 2>&1 | fixbundle
+
+echo % log after edit
+hg log --graph
+
+echo % manifest after edit
+hg manifest
+
+echo % EOF
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgext/histedit/tests/test-histedit-drop.out	Fri Aug 05 21:40:00 2011 +0200
@@ -0,0 +1,71 @@
+% log before edit
+@  changeset:   5:652413bf663e
+|  tag:         tip
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     f
+|
+o  changeset:   4:e860deea161a
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     e
+|
+o  changeset:   3:055a42cdd887
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     d
+|
+o  changeset:   2:177f92b77385
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     c
+|
+o  changeset:   1:d2ae7f538514
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     b
+|
+o  changeset:   0:cb9a9f314b8b
+   user:        test
+   date:        Thu Jan 01 00:00:00 1970 +0000
+   summary:     a
+
+% edit the history
+0 files updated, 0 files merged, 4 files removed, 0 files unresolved
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+% log after edit
+@  changeset:   4:708943196e52
+|  tag:         tip
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     d
+|
+o  changeset:   3:75cbdffecadb
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     f
+|
+o  changeset:   2:493dc0964412
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     e
+|
+o  changeset:   1:d2ae7f538514
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     b
+|
+o  changeset:   0:cb9a9f314b8b
+   user:        test
+   date:        Thu Jan 01 00:00:00 1970 +0000
+   summary:     a
+
+% manifest after edit
+a
+b
+d
+e
+f
+% EOF
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgext/histedit/tests/test-histedit-edit	Fri Aug 05 21:40:00 2011 +0200
@@ -0,0 +1,54 @@
+#!/bin/sh
+
+. "$TESTDIR/histedit-helpers.sh"
+
+cat >> $HGRCPATH <<EOF
+[extensions]
+hgext.graphlog=
+EOF
+
+echo "histedit=$(echo $(dirname $(dirname $0)))/hg_histedit.py" >> $HGRCPATH
+
+EDITED=`pwd`/editedhistory
+cat > $EDITED <<EOF
+pick 177f92b77385 c
+pick 055a42cdd887 d
+edit e860deea161a e
+pick 652413bf663e f
+EOF
+initrepo ()
+{
+    hg init r
+    cd r
+    for x in a b c d e f ; do
+        echo $x > $x
+        hg add $x
+        hg ci -m $x
+    done
+}
+
+initrepo
+
+echo % log before edit
+hg log --graph
+
+echo % edit the history
+HGEDITOR="cat $EDITED > " hg histedit 177f92b77385 2>&1 | fixbundle
+
+echo % edit the revision
+echo a > e
+HGEDITOR='echo "foobaz" > ' hg histedit --continue 2>&1 | fixbundle
+
+hg log --graph
+
+echo '% contents of e:'
+hg cat e
+
+cat > $EDITED <<EOF
+edit c38516e9ed62 f
+EOF
+HGEDITOR="cat $EDITED > " hg histedit tip 2>&1 | fixbundle
+hg status
+HGEDITOR='true' hg histedit --continue
+hg status
+echo % EOF
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgext/histedit/tests/test-histedit-edit.out	Fri Aug 05 21:40:00 2011 +0200
@@ -0,0 +1,78 @@
+% log before edit
+@  changeset:   5:652413bf663e
+|  tag:         tip
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     f
+|
+o  changeset:   4:e860deea161a
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     e
+|
+o  changeset:   3:055a42cdd887
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     d
+|
+o  changeset:   2:177f92b77385
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     c
+|
+o  changeset:   1:d2ae7f538514
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     b
+|
+o  changeset:   0:cb9a9f314b8b
+   user:        test
+   date:        Thu Jan 01 00:00:00 1970 +0000
+   summary:     a
+
+% edit the history
+0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+abort: Make changes as needed, you may commit or record as needed now.
+When you are finished, run hg histedit --continue to resume.
+% edit the revision
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+@  changeset:   5:c38516e9ed62
+|  tag:         tip
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     f
+|
+o  changeset:   4:1da62d13177d
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     foobaz
+|
+o  changeset:   3:055a42cdd887
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     d
+|
+o  changeset:   2:177f92b77385
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     c
+|
+o  changeset:   1:d2ae7f538514
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     b
+|
+o  changeset:   0:cb9a9f314b8b
+   user:        test
+   date:        Thu Jan 01 00:00:00 1970 +0000
+   summary:     a
+
+% contents of e:
+a
+0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+abort: Make changes as needed, you may commit or record as needed now.
+When you are finished, run hg histedit --continue to resume.
+A f
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+% EOF
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgext/histedit/tests/test-histedit-fold	Fri Aug 05 21:40:00 2011 +0200
@@ -0,0 +1,44 @@
+#!/bin/sh
+
+. "$TESTDIR/histedit-helpers.sh"
+
+cat >> $HGRCPATH <<EOF
+[extensions]
+hgext.graphlog=
+EOF
+
+echo "histedit=$(echo $(dirname $(dirname $0)))/hg_histedit.py" >> $HGRCPATH
+
+EDITED=`pwd`/editedhistory
+cat > $EDITED <<EOF
+pick e860deea161a e
+pick 652413bf663e f
+fold 177f92b77385 c
+pick 055a42cdd887 d
+EOF
+initrepo ()
+{
+    hg init r
+    cd r
+    for x in a b c d e f ; do
+        echo $x > $x
+        hg add $x
+        hg ci -m $x
+    done
+}
+
+initrepo
+
+echo % log before edit
+hg log --graph
+
+echo % edit the history
+HGEDITOR="cat $EDITED > " hg histedit 177f92b77385 2>&1 | fixbundle
+
+echo % log after edit
+hg log --graph
+
+echo % post-fold manifest
+hg manifest
+
+echo % EOF
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgext/histedit/tests/test-histedit-fold-non-commute	Fri Aug 05 21:40:00 2011 +0200
@@ -0,0 +1,59 @@
+#!/bin/sh
+
+. "$TESTDIR/histedit-helpers.sh"
+
+cat >> $HGRCPATH <<EOF
+[extensions]
+hgext.graphlog=
+EOF
+
+echo "histedit=$(echo $(dirname $(dirname $0)))/hg_histedit.py" >> $HGRCPATH
+
+EDITED=`pwd`/editedhistory
+cat > $EDITED <<EOF
+pick 177f92b77385 c
+pick 055a42cdd887 d
+fold bfa474341cc9 does not commute with e
+pick e860deea161a e
+pick 652413bf663e f
+EOF
+initrepo ()
+{
+    hg init r
+    cd r
+    for x in a b c d e f ; do
+        echo $x > $x
+        hg add $x
+        hg ci -m $x
+    done
+    echo a >> e
+    hg ci -m 'does not commute with e'
+}
+
+initrepo
+
+echo % log before edit
+hg log --graph
+
+echo % edit the history
+HGEDITOR="cat $EDITED > " hg histedit 177f92b77385 2>&1 | fixbundle
+
+echo % fix up
+echo a > e
+hg add e
+HGEDITOR=cat hg histedit --continue 2>&1 | fixbundle
+
+echo
+echo % just continue this time
+hg histedit --continue 2>&1 | fixbundle
+
+echo % log after edit
+hg log --graph
+
+echo % contents of e
+hg cat e
+
+echo % manifest
+hg manifest
+
+echo % EOF
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgext/histedit/tests/test-histedit-fold-non-commute.out	Fri Aug 05 21:40:00 2011 +0200
@@ -0,0 +1,92 @@
+% log before edit
+@  changeset:   6:bfa474341cc9
+|  tag:         tip
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     does not commute with e
+|
+o  changeset:   5:652413bf663e
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     f
+|
+o  changeset:   4:e860deea161a
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     e
+|
+o  changeset:   3:055a42cdd887
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     d
+|
+o  changeset:   2:177f92b77385
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     c
+|
+o  changeset:   1:d2ae7f538514
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     b
+|
+o  changeset:   0:cb9a9f314b8b
+   user:        test
+   date:        Thu Jan 01 00:00:00 1970 +0000
+   summary:     a
+
+% edit the history
+0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+unable to find 'e' for patching
+1 out of 1 hunks FAILED -- saving rejects to file e.rej
+e: No such file or directory
+abort: Fix up the change and run hg histedit --continue
+% fix up
+d
+***
+does not commute with e0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+file e already exists
+1 out of 1 hunks FAILED -- saving rejects to file e.rej
+abort: Fix up the change and run hg histedit --continue
+
+% just continue this time
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+% log after edit
+@  changeset:   4:f768fd60ca34
+|  tag:         tip
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     f
+|
+o  changeset:   3:671efe372e33
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     d
+|
+o  changeset:   2:177f92b77385
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     c
+|
+o  changeset:   1:d2ae7f538514
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     b
+|
+o  changeset:   0:cb9a9f314b8b
+   user:        test
+   date:        Thu Jan 01 00:00:00 1970 +0000
+   summary:     a
+
+% contents of e
+a
+% manifest
+a
+b
+c
+d
+e
+f
+% EOF
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgext/histedit/tests/test-histedit-fold.out	Fri Aug 05 21:40:00 2011 +0200
@@ -0,0 +1,74 @@
+% log before edit
+@  changeset:   5:652413bf663e
+|  tag:         tip
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     f
+|
+o  changeset:   4:e860deea161a
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     e
+|
+o  changeset:   3:055a42cdd887
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     d
+|
+o  changeset:   2:177f92b77385
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     c
+|
+o  changeset:   1:d2ae7f538514
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     b
+|
+o  changeset:   0:cb9a9f314b8b
+   user:        test
+   date:        Thu Jan 01 00:00:00 1970 +0000
+   summary:     a
+
+% edit the history
+0 files updated, 0 files merged, 4 files removed, 0 files unresolved
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+% log after edit
+@  changeset:   4:82b0c1ff1777
+|  tag:         tip
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     d
+|
+o  changeset:   3:150aafb44a91
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     pick e860deea161a e
+|
+o  changeset:   2:493dc0964412
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     e
+|
+o  changeset:   1:d2ae7f538514
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     b
+|
+o  changeset:   0:cb9a9f314b8b
+   user:        test
+   date:        Thu Jan 01 00:00:00 1970 +0000
+   summary:     a
+
+% post-fold manifest
+a
+b
+c
+d
+e
+f
+% EOF
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgext/histedit/tests/test-histedit-no-change	Fri Aug 05 21:40:00 2011 +0200
@@ -0,0 +1,110 @@
+#!/bin/sh
+
+# test for issue #6:
+# editing a changeset without any actual change would corrupt the repository
+
+. "$TESTDIR/histedit-helpers.sh"
+
+cat >> $HGRCPATH <<EOF
+[extensions]
+hgext.graphlog=
+hg_histedit=$(echo $(dirname $(dirname $0)))/hg_histedit.py
+EOF
+
+initrepo ()
+{
+    dir="$1"
+    comment="$2"
+
+    if [ -n "${comment}" ]; then
+        echo % ${comment}
+        echo % ${comment} | sed 's:.:-:g'
+    fi
+
+    hg init ${dir}
+    cd ${dir}
+    for x in a b c d e f ; do
+        echo $x > $x
+        hg add $x
+        hg ci -m $x
+    done
+}
+
+geneditor ()
+{
+    # generate an editor script for selecting changesets to be edited
+
+    choice=$1  # changesets that should be edited (using sed line ranges)
+
+    cat <<EOF | sed 's:^....::'
+    #!/bin/sh
+
+    # editing the rules, replacing 'pick' with 'edit' for the chosen lines
+    sed '${choice}s:^pick:edit:' \$1 > \${1}.tmp
+    mv \${1}.tmp \$1
+
+    # displaying the resulting rules, minus comments and empty lines
+    sed '/^#/d;/^$/d;s:^:| :' \$1 >&2
+EOF
+}
+
+startediting ()
+{
+    # begin an editing session
+
+    choice="$1"  # changesets that should be edited
+    number="$2"  # number of changesets considered (from tip)
+    comment="$3"
+
+    geneditor "${choice}" > edit.sh
+    chmod +x edit.sh
+
+    echo % start editing the history ${comment}
+    HGEDITOR=./edit.sh hg histedit -- -${number} 2>&1 | fixbundle
+}
+
+continueediting ()
+{
+    # continue an edit already in progress
+
+    editor="$1"  # message editor when finalizing editing
+    comment="$2"
+
+    echo % finalize changeset editing ${comment}
+    HGEDITOR=${editor} hg histedit --continue 2>&1 | fixbundle
+}
+
+graphlog ()
+{
+    comment="${1:-log}"
+
+    echo % "${comment}"
+    hg glog --template '{rev} {node} \"{desc|firstline}\"\n'
+}
+
+
+
+initrepo r1 "test editing with no change"
+graphlog "log before editing"
+startediting 2 3 "(not changing anything)" # edit the 2nd of 3 changesets
+continueediting true "(leaving commit message unaltered)"
+
+echo "% check state of working copy"
+hg id
+
+graphlog "log after history editing"
+
+
+cd ..
+initrepo r2 "test editing with no change, then abort"
+graphlog "log before editing"
+startediting 1,2 3 "(not changing anything)" # edit the 1st two of 3 changesets
+continueediting true "(leaving commit message unaltered)"
+graphlog "log after first edit"
+
+echo % abort editing session
+hg histedit --abort 2>&1 | fixbundle
+
+graphlog "log after abort"
+
+echo % EOF
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgext/histedit/tests/test-histedit-no-change.out	Fri Aug 05 21:40:00 2011 +0200
@@ -0,0 +1,94 @@
+% test editing with no change
+-----------------------------
+% log before editing
+@  5 652413bf663ef2a641cab26574e46d5f5a64a55a "f"
+|
+o  4 e860deea161a2f77de56603b340ebbb4536308ae "e"
+|
+o  3 055a42cdd88768532f9cf79daa407fc8d138de9b "d"
+|
+o  2 177f92b773850b59254aa5e923436f921b55483b "c"
+|
+o  1 d2ae7f538514cd87c17547b0de4cea71fe1af9fb "b"
+|
+o  0 cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b "a"
+
+% start editing the history (not changing anything)
+| pick 055a42cdd887 d
+| edit e860deea161a e
+| pick 652413bf663e f
+0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+abort: Make changes as needed, you may commit or record as needed now.
+When you are finished, run hg histedit --continue to resume.
+% finalize changeset editing (leaving commit message unaltered)
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+% check state of working copy
+652413bf663e tip
+% log after history editing
+@  5 652413bf663ef2a641cab26574e46d5f5a64a55a "f"
+|
+o  4 e860deea161a2f77de56603b340ebbb4536308ae "e"
+|
+o  3 055a42cdd88768532f9cf79daa407fc8d138de9b "d"
+|
+o  2 177f92b773850b59254aa5e923436f921b55483b "c"
+|
+o  1 d2ae7f538514cd87c17547b0de4cea71fe1af9fb "b"
+|
+o  0 cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b "a"
+
+% test editing with no change, then abort
+-----------------------------------------
+% log before editing
+@  5 652413bf663ef2a641cab26574e46d5f5a64a55a "f"
+|
+o  4 e860deea161a2f77de56603b340ebbb4536308ae "e"
+|
+o  3 055a42cdd88768532f9cf79daa407fc8d138de9b "d"
+|
+o  2 177f92b773850b59254aa5e923436f921b55483b "c"
+|
+o  1 d2ae7f538514cd87c17547b0de4cea71fe1af9fb "b"
+|
+o  0 cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b "a"
+
+% start editing the history (not changing anything)
+| edit 055a42cdd887 d
+| edit e860deea161a e
+| pick 652413bf663e f
+0 files updated, 0 files merged, 3 files removed, 0 files unresolved
+abort: Make changes as needed, you may commit or record as needed now.
+When you are finished, run hg histedit --continue to resume.
+% finalize changeset editing (leaving commit message unaltered)
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+abort: Make changes as needed, you may commit or record as needed now.
+When you are finished, run hg histedit --continue to resume.
+% log after first edit
+o  5 652413bf663ef2a641cab26574e46d5f5a64a55a "f"
+|
+o  4 e860deea161a2f77de56603b340ebbb4536308ae "e"
+|
+@  3 055a42cdd88768532f9cf79daa407fc8d138de9b "d"
+|
+o  2 177f92b773850b59254aa5e923436f921b55483b "c"
+|
+o  1 d2ae7f538514cd87c17547b0de4cea71fe1af9fb "b"
+|
+o  0 cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b "a"
+
+% abort editing session
+2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+% log after abort
+@  5 652413bf663ef2a641cab26574e46d5f5a64a55a "f"
+|
+o  4 e860deea161a2f77de56603b340ebbb4536308ae "e"
+|
+o  3 055a42cdd88768532f9cf79daa407fc8d138de9b "d"
+|
+o  2 177f92b773850b59254aa5e923436f921b55483b "c"
+|
+o  1 d2ae7f538514cd87c17547b0de4cea71fe1af9fb "b"
+|
+o  0 cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b "a"
+
+% EOF
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgext/histedit/tests/test-histedit-non-commute	Fri Aug 05 21:40:00 2011 +0200
@@ -0,0 +1,65 @@
+#!/bin/sh
+
+. "$TESTDIR/histedit-helpers.sh"
+
+cat >> $HGRCPATH <<EOF
+[extensions]
+hgext.graphlog=
+EOF
+
+echo "histedit=$(echo $(dirname $(dirname $0)))/hg_histedit.py" >> $HGRCPATH
+
+EDITED=`pwd`/editedhistory
+cat > $EDITED <<EOF
+pick 177f92b77385 c
+pick 055a42cdd887 d
+pick bfa474341cc9 does not commute with e
+pick e860deea161a e
+pick 652413bf663e f
+EOF
+initrepo ()
+{
+    hg init r
+    cd r
+    for x in a b c d e f ; do
+        echo $x > $x
+        hg add $x
+        hg ci -m $x
+    done
+    echo a >> e
+    hg ci -m 'does not commute with e'
+}
+
+initrepo
+
+echo % log before edit
+hg log --graph
+
+echo % edit the history
+HGEDITOR="cat $EDITED > " hg histedit 177f92b77385 2>&1 | fixbundle
+
+echo % abort the edit
+hg histedit --abort 2>&1 | fixbundle
+
+echo
+echo
+echo % second edit set
+
+hg log --graph
+
+echo % edit the history
+HGEDITOR="cat $EDITED > " hg histedit 177f92b77385 2>&1 | fixbundle
+
+echo % fix up
+echo a > e
+hg add e
+hg histedit --continue 2>&1 | fixbundle
+
+echo
+echo % just continue this time
+hg histedit --continue 2>&1 | fixbundle
+
+echo % log after edit
+hg log --graph
+
+echo % EOF
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgext/histedit/tests/test-histedit-non-commute-abort	Fri Aug 05 21:40:00 2011 +0200
@@ -0,0 +1,51 @@
+#!/bin/sh
+
+. "$TESTDIR/histedit-helpers.sh"
+
+cat >> $HGRCPATH <<EOF
+[extensions]
+hgext.graphlog=
+EOF
+
+echo "histedit=$(echo $(dirname $(dirname $0)))/hg_histedit.py" >> $HGRCPATH
+
+EDITED=`pwd`/editedhistory
+cat > $EDITED <<EOF
+pick 177f92b77385 c
+pick 055a42cdd887 d
+pick bfa474341cc9 does not commute with e
+pick e860deea161a e
+pick 652413bf663e f
+EOF
+initrepo ()
+{
+    hg init r
+    cd r
+    for x in a b c d e f ; do
+        echo $x > $x
+        hg add $x
+        hg ci -m $x
+    done
+    echo a >> e
+    hg ci -m 'does not commute with e'
+}
+
+initrepo
+
+echo % log before edit
+hg log --graph
+
+echo % edit the history
+HGEDITOR="cat $EDITED > " hg histedit 177f92b77385 2>&1 | fixbundle
+
+echo '% fix up (pre abort)'
+echo a > e
+hg add e
+hg histedit --continue 2>&1 | fixbundle
+
+echo % abort the edit
+hg histedit --abort 2>&1 | fixbundle
+
+echo % log after abort
+hg log --graph
+echo % EOF
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgext/histedit/tests/test-histedit-non-commute-abort.out	Fri Aug 05 21:40:00 2011 +0200
@@ -0,0 +1,88 @@
+% log before edit
+@  changeset:   6:bfa474341cc9
+|  tag:         tip
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     does not commute with e
+|
+o  changeset:   5:652413bf663e
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     f
+|
+o  changeset:   4:e860deea161a
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     e
+|
+o  changeset:   3:055a42cdd887
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     d
+|
+o  changeset:   2:177f92b77385
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     c
+|
+o  changeset:   1:d2ae7f538514
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     b
+|
+o  changeset:   0:cb9a9f314b8b
+   user:        test
+   date:        Thu Jan 01 00:00:00 1970 +0000
+   summary:     a
+
+% edit the history
+0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+unable to find 'e' for patching
+1 out of 1 hunks FAILED -- saving rejects to file e.rej
+e: No such file or directory
+abort: Fix up the change and run hg histedit --continue
+% fix up (pre abort)
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+file e already exists
+1 out of 1 hunks FAILED -- saving rejects to file e.rej
+abort: Fix up the change and run hg histedit --continue
+% abort the edit
+2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+% log after abort
+@  changeset:   6:bfa474341cc9
+|  tag:         tip
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     does not commute with e
+|
+o  changeset:   5:652413bf663e
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     f
+|
+o  changeset:   4:e860deea161a
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     e
+|
+o  changeset:   3:055a42cdd887
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     d
+|
+o  changeset:   2:177f92b77385
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     c
+|
+o  changeset:   1:d2ae7f538514
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     b
+|
+o  changeset:   0:cb9a9f314b8b
+   user:        test
+   date:        Thu Jan 01 00:00:00 1970 +0000
+   summary:     a
+
+% EOF
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgext/histedit/tests/test-histedit-non-commute.out	Fri Aug 05 21:40:00 2011 +0200
@@ -0,0 +1,132 @@
+% log before edit
+@  changeset:   6:bfa474341cc9
+|  tag:         tip
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     does not commute with e
+|
+o  changeset:   5:652413bf663e
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     f
+|
+o  changeset:   4:e860deea161a
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     e
+|
+o  changeset:   3:055a42cdd887
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     d
+|
+o  changeset:   2:177f92b77385
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     c
+|
+o  changeset:   1:d2ae7f538514
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     b
+|
+o  changeset:   0:cb9a9f314b8b
+   user:        test
+   date:        Thu Jan 01 00:00:00 1970 +0000
+   summary:     a
+
+% edit the history
+0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+unable to find 'e' for patching
+1 out of 1 hunks FAILED -- saving rejects to file e.rej
+e: No such file or directory
+abort: Fix up the change and run hg histedit --continue
+% abort the edit
+2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+
+
+% second edit set
+@  changeset:   6:bfa474341cc9
+|  tag:         tip
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     does not commute with e
+|
+o  changeset:   5:652413bf663e
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     f
+|
+o  changeset:   4:e860deea161a
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     e
+|
+o  changeset:   3:055a42cdd887
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     d
+|
+o  changeset:   2:177f92b77385
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     c
+|
+o  changeset:   1:d2ae7f538514
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     b
+|
+o  changeset:   0:cb9a9f314b8b
+   user:        test
+   date:        Thu Jan 01 00:00:00 1970 +0000
+   summary:     a
+
+% edit the history
+0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+unable to find 'e' for patching
+1 out of 1 hunks FAILED -- saving rejects to file e.rej
+e: No such file or directory
+abort: Fix up the change and run hg histedit --continue
+% fix up
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+file e already exists
+1 out of 1 hunks FAILED -- saving rejects to file e.rej
+abort: Fix up the change and run hg histedit --continue
+
+% just continue this time
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+% log after edit
+@  changeset:   5:9ab84894b459
+|  tag:         tip
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     f
+|
+o  changeset:   4:1fff3ae8199d
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     does not commute with e
+|
+o  changeset:   3:055a42cdd887
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     d
+|
+o  changeset:   2:177f92b77385
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     c
+|
+o  changeset:   1:d2ae7f538514
+|  user:        test
+|  date:        Thu Jan 01 00:00:00 1970 +0000
+|  summary:     b
+|
+o  changeset:   0:cb9a9f314b8b
+   user:        test
+   date:        Thu Jan 01 00:00:00 1970 +0000
+   summary:     a
+
+% EOF
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgext/histedit/tests/test-histedit-outgoing	Fri Aug 05 21:40:00 2011 +0200
@@ -0,0 +1,62 @@
+#!/bin/sh
+
+cat >> $HGRCPATH <<EOF
+[extensions]
+hgext.graphlog=
+EOF
+
+echo "histedit=$(echo $(dirname $(dirname $0)))/hg_histedit.py" >> $HGRCPATH
+
+EDITED=`pwd`/editedhistory
+cat > $EDITED <<EOF
+pick 177f92b77385 c
+pick e860deea161a e
+pick 652413bf663e f
+pick 055a42cdd887 d
+EOF
+initrepo ()
+{
+    hg init r
+    cd r
+    for x in a b c ; do
+        echo $x > $x
+        hg add $x
+        hg ci -m $x
+    done
+
+    cd ..
+    hg clone r r2 | grep -v updating
+    cd r2
+    for x in d e f ; do
+        echo $x > $x
+        hg add $x
+        hg ci -m $x
+    done
+
+    cd ..
+    hg init r3
+    cd r3
+    for x in g h i ; do
+        echo $x > $x
+        hg add $x
+        hg ci -m $x
+    done
+    cd ..
+}
+
+initrepo
+
+echo % show the edit commands offered by outgoing
+cd r2
+HGEDITOR=cat hg histedit --outgoing ../r
+cd ..
+
+echo % show the error from unrelated repos
+cd r3
+HGEDITOR=cat hg histedit --outgoing ../r
+cd ..
+
+echo % show the error from unrelated repos
+cd r3
+HGEDITOR=cat hg histedit --force --outgoing ../r
+cd ..
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgext/histedit/tests/test-histedit-outgoing.out	Fri Aug 05 21:40:00 2011 +0200
@@ -0,0 +1,38 @@
+3 files updated, 0 files merged, 0 files removed, 0 files unresolved
+% show the edit commands offered by outgoing
+pick 055a42cdd887 d
+pick e860deea161a e
+pick 652413bf663e f
+
+# Edit history between 055a42cdd887 and 652413bf663e
+#
+# Commands:
+#  p, pick = use commit
+#  e, edit = use commit, but stop for amending
+#  f, fold = use commit, but fold into previous commit
+#  d, drop = remove commit from history
+#
+comparing with ../r
+searching for changes
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+% show the error from unrelated repos
+comparing with ../r
+searching for changes
+abort: repository is unrelated
+% show the error from unrelated repos
+comparing with ../r
+searching for changes
+warning: repository is unrelated
+pick 2a4042b45417 g
+pick 68c46b4927ce h
+pick 51281e65ba79 i
+
+# Edit history between 2a4042b45417 and 51281e65ba79
+#
+# Commands:
+#  p, pick = use commit
+#  e, edit = use commit, but stop for amending
+#  f, fold = use commit, but fold into previous commit
+#  d, drop = remove commit from history
+#
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved