changeset 21757:c00822e0b8ea

merge with stable
author Matt Mackall <mpm@selenic.com>
date Sat, 14 Jun 2014 11:56:20 -0500
parents f5fd1580a649 (diff) e250a482478e (current diff)
children 0c6cdbb697d9
files
diffstat 157 files changed, 6928 insertions(+), 3767 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Sat Jun 14 11:49:02 2014 -0500
+++ b/.hgignore	Sat Jun 14 11:56:20 2014 -0500
@@ -26,6 +26,7 @@
 build
 contrib/hgsh/hgsh
 dist
+packages
 doc/common.txt
 doc/*.[0-9]
 doc/*.[0-9].txt
--- a/Makefile	Sat Jun 14 11:49:02 2014 -0500
+++ b/Makefile	Sat Jun 14 11:56:20 2014 -0500
@@ -132,6 +132,37 @@
 	msgmerge --no-location --update $@.tmp $^
 	mv -f $@.tmp $@
 
+# Packaging targets
+
+osx:
+	@which -s bdist_mpkg || \
+	   (echo "Missing bdist_mpkg (easy_install bdist_mpkg)"; false)
+	bdist_mpkg setup.py
+	mkdir -p packages/osx
+	rm -rf dist/mercurial-*.mpkg
+	mv dist/mercurial*macosx*.zip packages/osx
+
+fedora:
+	mkdir -p packages/fedora
+	contrib/buildrpm
+	cp rpmbuild/RPMS/*/* packages/fedora
+	cp rpmbuild/SRPMS/* packages/fedora
+	rm -rf rpmbuild
+
+docker-fedora:
+	mkdir -p packages/fedora
+	contrib/dockerrpm fedora
+
+centos6:
+	mkdir -p packages/centos6
+	contrib/buildrpm
+	cp rpmbuild/RPMS/*/* packages/centos6
+	cp rpmbuild/SRPMS/* packages/centos6
+
+docker-centos6:
+	mkdir -p packages/centos6
+	contrib/dockerrpm centos6
+
 .PHONY: help all local build doc clean install install-bin install-doc \
 	install-home install-home-bin install-home-doc dist dist-notests tests \
-	update-pot
+	update-pot fedora docker-fedora
--- a/contrib/bash_completion	Sat Jun 14 11:49:02 2014 -0500
+++ b/contrib/bash_completion	Sat Jun 14 11:56:20 2014 -0500
@@ -629,7 +629,7 @@
 
 _hg_cmd_shelve()
 {
-    if [[ "$prev" = @(-d|--delete) ]]; then
+    if [[ "$prev" = @(-d|--delete|-l|--list) ]]; then
         _hg_shelves
     else
         _hg_status "mard"
--- a/contrib/buildrpm	Sat Jun 14 11:49:02 2014 -0500
+++ b/contrib/buildrpm	Sat Jun 14 11:56:20 2014 -0500
@@ -1,16 +1,13 @@
-#!/bin/sh
+#!/bin/sh -e
 #
-# Build a Mercurial RPM in place.
+# Build a Mercurial RPM from the current repo
 #
 # Tested on
-# - Fedora 8 (with docutils 0.5)
-# - Fedora 11
-# - OpenSuse 11.2
+# - Fedora 20
+# - CentOS 5
+# - centOS 6
 
 cd "`dirname $0`/.."
-HG="$PWD/hg"
-PYTHONPATH="$PWD/mercurial/pure"
-export PYTHONPATH
 
 specfile=contrib/mercurial.spec
 if [ ! -f $specfile ]; then
@@ -23,21 +20,17 @@
     exit 1
 fi
 
-if $HG id -i | grep '+$' > /dev/null 2>&1; then
-    echo -n "Your local changes will NOT be in the RPM. Continue [y/n] ? "
-    read answer
-    if echo $answer | grep -iv '^y'; then
-        exit
-    fi
-fi
+# build local hg and use it
+python setup.py build_py -c -d .
+HG="$PWD/hg"
+PYTHONPATH="$PWD/mercurial/pure"
+export PYTHONPATH
 
 rpmdir="$PWD/rpmbuild"
 
 rm -rf $rpmdir
 mkdir -p $rpmdir/SOURCES $rpmdir/SPECS $rpmdir/RPMS $rpmdir/SRPMS $rpmdir/BUILD
 
-# make setup.py build the version string
-python setup.py build_py -c -d .
 hgversion=`$HG version | sed -ne 's/.*(version \(.*\))$/\1/p'`
 
 if echo $hgversion | grep -- '-' > /dev/null 2>&1; then
@@ -50,8 +43,8 @@
     release='0'
 fi
 
-$HG archive -t tgz $rpmdir/SOURCES/mercurial-$version.tar.gz
-rpmspec=$rpmdir/SPECS/mercurial-$version.spec
+$HG archive -t tgz $rpmdir/SOURCES/mercurial-$version-$release.tar.gz
+rpmspec=$rpmdir/SPECS/mercurial.spec
 
 sed -e "s,^Version:.*,Version: $version," \
     -e "s,^Release:.*,Release: $release," \
--- a/contrib/check-code.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/contrib/check-code.py	Sat Jun 14 11:56:20 2014 -0500
@@ -367,16 +367,28 @@
   []
 ]
 
+webtemplatefilters = []
+
+webtemplatepats = [
+  [],
+  [
+    (r'{desc(\|(?!websub|firstline)[^\|]*)+}',
+     'follow desc keyword with either firstline or websub'),
+  ]
+]
+
 checks = [
-    ('python', r'.*\.(py|cgi)$', pyfilters, pypats),
-    ('test script', r'(.*/)?test-[^.~]*$', testfilters, testpats),
-    ('c', r'.*\.[ch]$', cfilters, cpats),
-    ('unified test', r'.*\.t$', utestfilters, utestpats),
-    ('layering violation repo in revlog', r'mercurial/revlog\.py', pyfilters,
-     inrevlogpats),
-    ('layering violation ui in util', r'mercurial/util\.py', pyfilters,
+    ('python', r'.*\.(py|cgi)$', r'^#!.*python', pyfilters, pypats),
+    ('test script', r'(.*/)?test-[^.~]*$', '', testfilters, testpats),
+    ('c', r'.*\.[ch]$', '', cfilters, cpats),
+    ('unified test', r'.*\.t$', '', utestfilters, utestpats),
+    ('layering violation repo in revlog', r'mercurial/revlog\.py', '',
+     pyfilters, inrevlogpats),
+    ('layering violation ui in util', r'mercurial/util\.py', '', pyfilters,
      inutilpats),
-    ('txt', r'.*\.txt$', txtfilters, txtpats),
+    ('txt', r'.*\.txt$', '', txtfilters, txtpats),
+    ('web template', r'mercurial/templates/.*\.tmpl', '',
+     webtemplatefilters, webtemplatepats),
 ]
 
 def _preparepats():
@@ -392,7 +404,7 @@
                 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
 
                 pats[i] = (re.compile(p, re.MULTILINE),) + pseq[1:]
-        filters = c[2]
+        filters = c[3]
         for i, flt in enumerate(filters):
             filters[i] = re.compile(flt[0]), flt[1]
 _preparepats()
@@ -446,22 +458,24 @@
     """
     blamecache = None
     result = True
-    for name, match, filters, pats in checks:
+
+    try:
+        fp = open(f)
+    except IOError, e:
+        print "Skipping %s, %s" % (f, str(e).split(':', 1)[0])
+        return result
+    pre = post = fp.read()
+    fp.close()
+
+    for name, match, magic, filters, pats in checks:
         if debug:
             print name, f
         fc = 0
-        if not re.match(match, f):
+        if not (re.match(match, f) or (magic and re.search(magic, f))):
             if debug:
                 print "Skipping %s for %s it doesn't match %s" % (
                        name, match, f)
             continue
-        try:
-            fp = open(f)
-        except IOError, e:
-            print "Skipping %s, %s" % (f, str(e).split(':', 1)[0])
-            continue
-        pre = post = fp.read()
-        fp.close()
         if "no-" "check-code" in pre:
             print "Skipping %s it has no-" "check-code" % f
             return "Skip" # skip checking this file
--- a/contrib/debugshell.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/contrib/debugshell.py	Sat Jun 14 11:56:20 2014 -0500
@@ -4,6 +4,10 @@
 import sys
 import mercurial
 import code
+from mercurial import cmdutil
+
+cmdtable = {}
+command = cmdutil.command(cmdtable)
 
 def pdb(ui, repo, msg, **opts):
     objects = {
@@ -24,6 +28,7 @@
 
     IPython.embed()
 
+@command('debugshell|dbsh', [])
 def debugshell(ui, repo, **opts):
     bannermsg = "loaded repo : %s\n" \
                 "using source: %s" % (repo.root,
@@ -47,7 +52,3 @@
         debugger = 'pdb'
 
     getattr(sys.modules[__name__], debugger)(ui, repo, bannermsg, **opts)
-
-cmdtable = {
-    "debugshell|dbsh": (debugshell, [])
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/docker/centos6	Sat Jun 14 11:56:20 2014 -0500
@@ -0,0 +1,7 @@
+FROM centos
+RUN yum install -y gcc
+RUN yum install -y python-devel python-docutils
+RUN yum install -y make
+RUN yum install -y rpm-build
+RUN yum install -y gettext
+RUN yum install -y tar
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/docker/fedora	Sat Jun 14 11:56:20 2014 -0500
@@ -0,0 +1,6 @@
+FROM fedora
+RUN yum install -y gcc
+RUN yum install -y python-devel python-docutils
+RUN yum install -y make
+RUN yum install -y rpm-build
+RUN yum install -y gettext
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/dockerrpm	Sat Jun 14 11:56:20 2014 -0500
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+BUILDDIR=$(dirname $0)
+ROOTDIR=$(cd $BUILDDIR/..; pwd)
+
+if which docker.io >> /dev/null ; then
+  DOCKER=docker.io
+elif which docker >> /dev/null ; then
+  DOCKER=docker
+fi
+
+$DOCKER build --tag "hg-dockerrpm-$1" - < $BUILDDIR/docker/$1
+$DOCKER run --rm -v $ROOTDIR:/hg "hg-dockerrpm-$1" bash -c \
+    "cp -a hg hg-build; cd hg-build; make clean local $1; cp packages/$1/* /hg/packages/$1/"
--- a/contrib/hgfixes/fix_bytes.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/contrib/hgfixes/fix_bytes.py	Sat Jun 14 11:56:20 2014 -0500
@@ -12,10 +12,10 @@
 # XXX: Implementing a blacklist in 2to3 turned out to be more troublesome than
 # blacklisting some modules inside the fixers. So, this is what I came with.
 
-blacklist = ['mercurial/demandimport.py',
+blacklist = ('mercurial/demandimport.py',
              'mercurial/py3kcompat.py', # valid python 3 already
              'mercurial/i18n.py',
-            ]
+            )
 
 def isdocstring(node):
     def isclassorfunction(ancestor):
@@ -83,7 +83,8 @@
     PATTERN = 'STRING'
 
     def transform(self, node, results):
-        if self.filename in blacklist:
+        # The filename may be prefixed with a build directory.
+        if self.filename.endswith(blacklist):
             return
         if node.type == token.STRING:
             if _re.match(node.value):
--- a/contrib/mercurial.spec	Sat Jun 14 11:49:02 2014 -0500
+++ b/contrib/mercurial.spec	Sat Jun 14 11:56:20 2014 -0500
@@ -1,3 +1,6 @@
+%global emacs_lispdir %{_datadir}/emacs/site-lisp
+%global pythonver %(python -c 'import sys;print ".".join(map(str, sys.version_info[:2]))')
+
 Summary: A fast, lightweight Source Control Management system
 Name: mercurial
 Version: snapshot
@@ -5,33 +8,21 @@
 License: GPLv2+
 Group: Development/Tools
 URL: http://mercurial.selenic.com/
-Source0: http://mercurial.selenic.com/release/%{name}-%{version}.tar.gz
+Source0: %{name}-%{version}-%{release}.tar.gz
 BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
 
-# From the README:
-#
-#   Note: some distributions fails to include bits of distutils by
-#   default, you'll need python-dev to install. You'll also need a C
-#   compiler and a 3-way merge tool like merge, tkdiff, or kdiff3.
-#
-# python-devel provides an adequate python-dev.  The merge tool is a
-# run-time dependency.
-#
 BuildRequires: python >= 2.4, python-devel, make, gcc, python-docutils >= 0.5, gettext
 Provides: hg = %{version}-%{release}
 Requires: python >= 2.4
 # The hgk extension uses the wish tcl interpreter, but we don't enforce it
 #Requires: tk
 
-%define pythonver %(python -c 'import sys;print ".".join(map(str, sys.version_info[:2]))')
-%define emacs_lispdir %{_datadir}/emacs/site-lisp
-
 %description
 Mercurial is a fast, lightweight source control management system designed
 for efficient handling of very large distributed projects.
 
 %prep
-%setup -q
+%setup -q -n mercurial-%{version}-%{release}
 
 %build
 make all
@@ -40,8 +31,8 @@
 rm -rf $RPM_BUILD_ROOT
 make install DESTDIR=$RPM_BUILD_ROOT PREFIX=%{_prefix} MANDIR=%{_mandir}
 
-install -m 755 contrib/hgk $RPM_BUILD_ROOT%{_bindir}
-install -m 755 contrib/hg-ssh $RPM_BUILD_ROOT%{_bindir}
+install -m 755 contrib/hgk $RPM_BUILD_ROOT%{_bindir}/
+install -m 755 contrib/hg-ssh $RPM_BUILD_ROOT%{_bindir}/
 
 bash_completion_dir=$RPM_BUILD_ROOT%{_sysconfdir}/bash_completion.d
 mkdir -p $bash_completion_dir
@@ -52,8 +43,8 @@
 install -m 644 contrib/zsh_completion $zsh_completion_dir/_mercurial
 
 mkdir -p $RPM_BUILD_ROOT%{emacs_lispdir}
-install -m 644 contrib/mercurial.el $RPM_BUILD_ROOT%{emacs_lispdir}
-install -m 644 contrib/mq.el $RPM_BUILD_ROOT%{emacs_lispdir}
+install -m 644 contrib/mercurial.el $RPM_BUILD_ROOT%{emacs_lispdir}/
+install -m 644 contrib/mq.el $RPM_BUILD_ROOT%{emacs_lispdir}/
 
 mkdir -p $RPM_BUILD_ROOT/%{_sysconfdir}/mercurial/hgrc.d
 install -m 644 contrib/mergetools.hgrc $RPM_BUILD_ROOT%{_sysconfdir}/mercurial/hgrc.d/mergetools.rc
--- a/contrib/mergetools.hgrc	Sat Jun 14 11:49:02 2014 -0500
+++ b/contrib/mergetools.hgrc	Sat Jun 14 11:56:20 2014 -0500
@@ -71,6 +71,12 @@
 ecmerge.gui=True
 ecmerge.diffargs=$parent $child --mode=diff2 --title1='$plabel1' --title2='$clabel'
 
+# editmerge is a small script shipped in contrib.
+# It needs this config otherwise it behaves the same as internal:local
+editmerge.args=$output
+editmerge.check=changed
+editmerge.premerge=keep
+
 filemerge.executable=/Developer/Applications/Utilities/FileMerge.app/Contents/MacOS/FileMerge
 filemerge.args=-left $other -right $local -ancestor $base -merge $output
 filemerge.gui=True
--- a/contrib/revsetbenchmarks.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/contrib/revsetbenchmarks.py	Sat Jun 14 11:56:20 2014 -0500
@@ -14,7 +14,12 @@
 # to compare performance.
 
 import sys
+import os
 from subprocess import check_call, Popen, CalledProcessError, STDOUT, PIPE
+# cannot use argparse, python 2.7 only
+from optparse import OptionParser
+
+
 
 def check_output(*args, **kwargs):
     kwargs.setdefault('stderr', PIPE)
@@ -33,15 +38,19 @@
         print >> sys.stderr, 'update to revision %s failed, aborting' % rev
         sys.exit(exc.returncode)
 
-def perf(revset):
+def perf(revset, target=None):
     """run benchmark for this very revset"""
     try:
-        output = check_output(['./hg',
-                               '--config',
-                               'extensions.perf=contrib/perf.py',
-                               'perfrevset',
-                               revset],
-                               stderr=STDOUT)
+        cmd = ['./hg',
+               '--config',
+               'extensions.perf='
+               + os.path.join(contribdir, 'perf.py'),
+               'perfrevset',
+               revset]
+        if target is not None:
+            cmd.append('-R')
+            cmd.append(target)
+        output = check_output(cmd, stderr=STDOUT)
         output = output.lstrip('!') # remove useless ! in this context
         return output.strip()
     except CalledProcessError, exc:
@@ -65,12 +74,26 @@
     return [r for r in out.split() if r]
 
 
+parser = OptionParser(usage="usage: %prog [options] <revs>")
+parser.add_option("-f", "--file",
+                  help="read revset from FILE", metavar="FILE")
+parser.add_option("-R", "--repo",
+                  help="run benchmark on REPO", metavar="REPO")
 
-target_rev = sys.argv[1]
+(options, args) = parser.parse_args()
+
+if len(sys.argv) < 2:
+    parser.print_help()
+    sys.exit(255)
+
+# the directory where both this script and the perf.py extension live.
+contribdir = os.path.dirname(__file__)
+
+target_rev = args[0]
 
 revsetsfile = sys.stdin
-if len(sys.argv) > 2:
-    revsetsfile = open(sys.argv[2])
+if options.file:
+    revsetsfile = open(options.file)
 
 revsets = [l.strip() for l in revsetsfile]
 
@@ -95,7 +118,7 @@
     res = []
     results.append(res)
     for idx, rset in enumerate(revsets):
-        data = perf(rset)
+        data = perf(rset, target=options.repo)
         res.append(data)
         print "%i)" % idx, data
         sys.stdout.flush()
--- a/contrib/revsetbenchmarks.txt	Sat Jun 14 11:49:02 2014 -0500
+++ b/contrib/revsetbenchmarks.txt	Sat Jun 14 11:56:20 2014 -0500
@@ -2,11 +2,13 @@
 draft()
 ::tip
 draft() and ::tip
+::tip and draft()
 0::tip
 roots(0::tip)
 author(lmoscovicz)
 author(mpm)
 author(lmoscovicz) or author(mpm)
+author(mpm) or author(lmoscovicz)
 tip:0
 max(tip:0)
 min(0:tip)
@@ -18,3 +20,4 @@
 :10000 and public()
 draft()
 :10000 and draft()
+max(::(tip~20) - obsolete())
--- a/contrib/synthrepo.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/contrib/synthrepo.py	Sat Jun 14 11:56:20 2014 -0500
@@ -334,7 +334,7 @@
                 for __ in xrange(add):
                     lines.insert(random.randint(0, len(lines)), makeline())
                 path = fctx.path()
-                changes[path] = context.memfilectx(path,
+                changes[path] = context.memfilectx(repo, path,
                                                    '\n'.join(lines) + '\n')
             for __ in xrange(pick(filesremoved)):
                 path = random.choice(mfk)
@@ -354,7 +354,7 @@
             path = '/'.join(filter(None, path))
             data = '\n'.join(makeline()
                              for __ in xrange(pick(linesinfilesadded))) + '\n'
-            changes[path] = context.memfilectx(path, data)
+            changes[path] = context.memfilectx(repo, path, data)
         def filectxfn(repo, memctx, path):
             data = changes[path]
             if data is None:
--- a/contrib/vim/hgcommand.vim	Sat Jun 14 11:49:02 2014 -0500
+++ b/contrib/vim/hgcommand.vim	Sat Jun 14 11:56:20 2014 -0500
@@ -105,7 +105,7 @@
   let fileName=<SID>HGResolveLink(a:fileName)
   let newCwd=fnamemodify(fileName, ':h')
   if strlen(newCwd) > 0
-    execute 'cd' escape(newCwd, ' ')
+    try | execute 'cd' escape(newCwd, ' ') | catch | | endtry
   endif
   return oldCwd
 endfunction
@@ -396,7 +396,7 @@
 
     return returnExpression
   finally
-    execute 'cd' escape(oldCwd, ' ')
+    try | execute 'cd' escape(oldCwd, ' ') | catch | | endtry
   endtry
 endfunction
 
--- a/hgext/children.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/hgext/children.py	Sat Jun 14 11:56:20 2014 -0500
@@ -18,8 +18,15 @@
 from mercurial.commands import templateopts
 from mercurial.i18n import _
 
+cmdtable = {}
+command = cmdutil.command(cmdtable)
 testedwith = 'internal'
 
+@command('children',
+    [('r', 'rev', '',
+     _('show children of the specified revision'), _('REV')),
+    ] + templateopts,
+    _('hg children [-r REV] [FILE]'))
 def children(ui, repo, file_=None, **opts):
     """show the children of the given or working directory revision
 
@@ -40,13 +47,4 @@
         displayer.show(cctx)
     displayer.close()
 
-cmdtable = {
-    "children":
-        (children,
-         [('r', 'rev', '',
-           _('show children of the specified revision'), _('REV')),
-         ] + templateopts,
-         _('hg children [-r REV] [FILE]')),
-}
-
 commands.inferrepo += " children"
--- a/hgext/churn.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/hgext/churn.py	Sat Jun 14 11:56:20 2014 -0500
@@ -14,6 +14,8 @@
 import os
 import time, datetime
 
+cmdtable = {}
+command = cmdutil.command(cmdtable)
 testedwith = 'internal'
 
 def maketemplater(ui, repo, tmpl):
@@ -88,6 +90,21 @@
     return rate
 
 
+@command('churn',
+    [('r', 'rev', [],
+     _('count rate for the specified revision or range'), _('REV')),
+    ('d', 'date', '',
+     _('count rate for revisions matching date spec'), _('DATE')),
+    ('t', 'template', '{author|email}',
+     _('template to group changesets'), _('TEMPLATE')),
+    ('f', 'dateformat', '',
+     _('strftime-compatible format for grouping by date'), _('FORMAT')),
+    ('c', 'changesets', False, _('count rate by number of changesets')),
+    ('s', 'sort', False, _('sort by key (default: sort by count)')),
+    ('', 'diffstat', False, _('display added/removed lines separately')),
+    ('', 'aliases', '', _('file with email aliases'), _('FILE')),
+    ] + commands.walkopts,
+    _("hg churn [-d DATE] [-r REV] [--aliases FILE] [FILE]"))
 def churn(ui, repo, *pats, **opts):
     '''histogram of changes to the repository
 
@@ -181,25 +198,4 @@
     for name, count in rate:
         ui.write(format(name, count))
 
-
-cmdtable = {
-    "churn":
-        (churn,
-         [('r', 'rev', [],
-           _('count rate for the specified revision or range'), _('REV')),
-          ('d', 'date', '',
-           _('count rate for revisions matching date spec'), _('DATE')),
-          ('t', 'template', '{author|email}',
-           _('template to group changesets'), _('TEMPLATE')),
-          ('f', 'dateformat', '',
-           _('strftime-compatible format for grouping by date'), _('FORMAT')),
-          ('c', 'changesets', False, _('count rate by number of changesets')),
-          ('s', 'sort', False, _('sort by key (default: sort by count)')),
-          ('', 'diffstat', False, _('display added/removed lines separately')),
-          ('', 'aliases', '',
-           _('file with email aliases'), _('FILE')),
-          ] + commands.walkopts,
-         _("hg churn [-d DATE] [-r REV] [--aliases FILE] [FILE]")),
-}
-
 commands.inferrepo += " churn"
--- a/hgext/color.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/hgext/color.py	Sat Jun 14 11:56:20 2014 -0500
@@ -111,10 +111,12 @@
 
 import os
 
-from mercurial import commands, dispatch, extensions, ui as uimod, util
+from mercurial import cmdutil, commands, dispatch, extensions, ui as uimod, util
 from mercurial import templater, error
 from mercurial.i18n import _
 
+cmdtable = {}
+command = cmdutil.command(cmdtable)
 testedwith = 'internal'
 
 # start and stop parameters for effects
@@ -166,7 +168,7 @@
 def _modesetup(ui, coloropt):
     global _terminfo_params
 
-    auto = coloropt == 'auto'
+    auto = (coloropt == 'auto')
     always = not auto and util.parsebool(coloropt)
     if not always and not auto:
         return None
@@ -440,6 +442,7 @@
          _("when to colorize (boolean, always, auto, or never)"),
          _('TYPE')))
 
+@command('debugcolor', [], 'hg debugcolor')
 def debugcolor(ui, repo, **opts):
     global _styles
     _styles = {}
@@ -579,8 +582,3 @@
         finally:
             # Explicitly reset original attributes
             _kernel32.SetConsoleTextAttribute(stdout, origattr)
-
-cmdtable = {
-    'debugcolor':
-        (debugcolor, [], ('hg debugcolor'))
-}
--- a/hgext/convert/__init__.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/hgext/convert/__init__.py	Sat Jun 14 11:56:20 2014 -0500
@@ -10,13 +10,34 @@
 import convcmd
 import cvsps
 import subversion
-from mercurial import commands, templatekw
+from mercurial import cmdutil, commands, templatekw
 from mercurial.i18n import _
 
+cmdtable = {}
+command = cmdutil.command(cmdtable)
 testedwith = 'internal'
 
 # Commands definition was moved elsewhere to ease demandload job.
 
+@command('convert',
+    [('', 'authors', '',
+      _('username mapping filename (DEPRECATED, use --authormap instead)'),
+      _('FILE')),
+    ('s', 'source-type', '', _('source repository type'), _('TYPE')),
+    ('d', 'dest-type', '', _('destination repository type'), _('TYPE')),
+    ('r', 'rev', '', _('import up to source revision REV'), _('REV')),
+    ('A', 'authormap', '', _('remap usernames using this file'), _('FILE')),
+    ('', 'filemap', '', _('remap file names using contents of file'),
+     _('FILE')),
+    ('', 'splicemap', '', _('splice synthesized history into place'),
+     _('FILE')),
+    ('', 'branchmap', '', _('change branch names while converting'),
+     _('FILE')),
+    ('', 'branchsort', None, _('try to sort changesets by branches')),
+    ('', 'datesort', None, _('try to sort changesets by date')),
+    ('', 'sourcesort', None, _('preserve source changesets order')),
+    ('', 'closesort', None, _('try to reorder closed revisions'))],
+   _('hg convert [OPTION]... SOURCE [DEST [REVMAP]]'))
 def convert(ui, src, dest=None, revmapfile=None, **opts):
     """convert a foreign SCM repository to a Mercurial one.
 
@@ -282,9 +303,28 @@
     """
     return convcmd.convert(ui, src, dest, revmapfile, **opts)
 
+@command('debugsvnlog', [], 'hg debugsvnlog')
 def debugsvnlog(ui, **opts):
     return subversion.debugsvnlog(ui, **opts)
 
+@command('debugcvsps',
+    [
+    # Main options shared with cvsps-2.1
+    ('b', 'branches', [], _('only return changes on specified branches')),
+    ('p', 'prefix', '', _('prefix to remove from file names')),
+    ('r', 'revisions', [],
+     _('only return changes after or between specified tags')),
+    ('u', 'update-cache', None, _("update cvs log cache")),
+    ('x', 'new-cache', None, _("create new cvs log cache")),
+    ('z', 'fuzz', 60, _('set commit time fuzz in seconds')),
+    ('', 'root', '', _('specify cvsroot')),
+    # Options specific to builtin cvsps
+    ('', 'parents', '', _('show parent changesets')),
+    ('', 'ancestors', '', _('show current changeset in ancestor branches')),
+    # Options that are ignored for compatibility with cvsps-2.1
+    ('A', 'cvs-direct', None, _('ignored for compatibility')),
+    ],
+    _('hg debugcvsps [OPTION]... [PATH]...'))
 def debugcvsps(ui, *args, **opts):
     '''create changeset information from CVS
 
@@ -300,57 +340,6 @@
 
 commands.norepo += " convert debugsvnlog debugcvsps"
 
-cmdtable = {
-    "convert":
-        (convert,
-         [('', 'authors', '',
-           _('username mapping filename (DEPRECATED, use --authormap instead)'),
-           _('FILE')),
-          ('s', 'source-type', '',
-           _('source repository type'), _('TYPE')),
-          ('d', 'dest-type', '',
-           _('destination repository type'), _('TYPE')),
-          ('r', 'rev', '',
-           _('import up to source revision REV'), _('REV')),
-          ('A', 'authormap', '',
-           _('remap usernames using this file'), _('FILE')),
-          ('', 'filemap', '',
-           _('remap file names using contents of file'), _('FILE')),
-          ('', 'splicemap', '',
-           _('splice synthesized history into place'), _('FILE')),
-          ('', 'branchmap', '',
-           _('change branch names while converting'), _('FILE')),
-          ('', 'branchsort', None, _('try to sort changesets by branches')),
-          ('', 'datesort', None, _('try to sort changesets by date')),
-          ('', 'sourcesort', None, _('preserve source changesets order')),
-          ('', 'closesort', None, _('try to reorder closed revisions'))],
-         _('hg convert [OPTION]... SOURCE [DEST [REVMAP]]')),
-    "debugsvnlog":
-        (debugsvnlog,
-         [],
-         'hg debugsvnlog'),
-    "debugcvsps":
-        (debugcvsps,
-         [
-          # Main options shared with cvsps-2.1
-          ('b', 'branches', [], _('only return changes on specified branches')),
-          ('p', 'prefix', '', _('prefix to remove from file names')),
-          ('r', 'revisions', [],
-           _('only return changes after or between specified tags')),
-          ('u', 'update-cache', None, _("update cvs log cache")),
-          ('x', 'new-cache', None, _("create new cvs log cache")),
-          ('z', 'fuzz', 60, _('set commit time fuzz in seconds')),
-          ('', 'root', '', _('specify cvsroot')),
-          # Options specific to builtin cvsps
-          ('', 'parents', '', _('show parent changesets')),
-          ('', 'ancestors', '',
-           _('show current changeset in ancestor branches')),
-          # Options that are ignored for compatibility with cvsps-2.1
-          ('A', 'cvs-direct', None, _('ignored for compatibility')),
-         ],
-         _('hg debugcvsps [OPTION]... [PATH]...')),
-}
-
 def kwconverted(ctx, name):
     rev = ctx.extra().get('convert_revision', '')
     if rev.startswith('svn:'):
--- a/hgext/convert/common.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/hgext/convert/common.py	Sat Jun 14 11:56:20 2014 -0500
@@ -260,8 +260,15 @@
         """
         pass
 
-    def hascommit(self, rev):
-        """Return True if the sink contains rev"""
+    def hascommitfrommap(self, rev):
+        """Return False if a rev mentioned in a filemap is known to not be
+        present."""
+        raise NotImplementedError
+
+    def hascommitforsplicemap(self, rev):
+        """This method is for the special needs for splicemap handling and not
+        for general use. Returns True if the sink contains rev, aborts on some
+        special cases."""
         raise NotImplementedError
 
 class commandline(object):
--- a/hgext/convert/convcmd.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/hgext/convert/convcmd.py	Sat Jun 14 11:56:20 2014 -0500
@@ -173,8 +173,12 @@
         parents = {}
         while visit:
             n = visit.pop(0)
-            if n in known or n in self.map:
+            if n in known:
                 continue
+            if n in self.map:
+                m = self.map[n]
+                if m == SKIPREV or self.dest.hascommitfrommap(m):
+                    continue
             known.add(n)
             self.ui.progress(_('scanning'), len(known), unit=_('revisions'))
             commit = self.cachecommit(n)
@@ -193,7 +197,7 @@
         """
         for c in sorted(splicemap):
             if c not in parents:
-                if not self.dest.hascommit(self.map.get(c, c)):
+                if not self.dest.hascommitforsplicemap(self.map.get(c, c)):
                     # Could be in source but not converted during this run
                     self.ui.warn(_('splice map revision %s is not being '
                                    'converted, ignoring\n') % c)
@@ -201,7 +205,7 @@
             pc = []
             for p in splicemap[c]:
                 # We do not have to wait for nodes already in dest.
-                if self.dest.hascommit(self.map.get(p, p)):
+                if self.dest.hascommitforsplicemap(self.map.get(p, p)):
                     continue
                 # Parent is not in dest and not being converted, not good
                 if p not in parents:
--- a/hgext/convert/git.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/hgext/convert/git.py	Sat Jun 14 11:56:20 2014 -0500
@@ -46,6 +46,18 @@
                     del os.environ['GIT_DIR']
                 else:
                     os.environ['GIT_DIR'] = prevgitdir
+
+        def gitpipe(self, s):
+            prevgitdir = os.environ.get('GIT_DIR')
+            os.environ['GIT_DIR'] = self.path
+            try:
+                return util.popen3(s)
+            finally:
+                if prevgitdir is None:
+                    del os.environ['GIT_DIR']
+                else:
+                    os.environ['GIT_DIR'] = prevgitdir
+
     else:
         def gitopen(self, s, err=None):
             if err == subprocess.PIPE:
@@ -56,6 +68,9 @@
             else:
                 return util.popen('GIT_DIR=%s %s' % (self.path, s), 'rb')
 
+        def gitpipe(self, s):
+            return util.popen3('GIT_DIR=%s %s' % (self.path, s))
+
     def popen_with_stderr(self, s):
         p = subprocess.Popen(s, shell=True, bufsize=-1,
                              close_fds=util.closefds,
@@ -84,6 +99,12 @@
         self.path = path
         self.submodules = []
 
+        self.catfilepipe = self.gitpipe('git cat-file --batch')
+
+    def after(self):
+        for f in self.catfilepipe:
+            f.close()
+
     def getheads(self):
         if not self.rev:
             heads, ret = self.gitread('git rev-parse --branches --remotes')
@@ -98,9 +119,17 @@
     def catfile(self, rev, type):
         if rev == hex(nullid):
             raise IOError
-        data, ret = self.gitread("git cat-file %s %s" % (type, rev))
-        if ret:
+        self.catfilepipe[0].write(rev+'\n')
+        self.catfilepipe[0].flush()
+        info = self.catfilepipe[1].readline().split()
+        if info[1] != type:
             raise util.Abort(_('cannot read %r object at %s') % (type, rev))
+        size = int(info[2])
+        data = self.catfilepipe[1].read(size)
+        if len(data) < size:
+            raise util.Abort(_('cannot read %r object at %s: %s') % (type, rev))
+        # read the trailing newline
+        self.catfilepipe[1].read(1)
         return data
 
     def getfile(self, name, rev):
--- a/hgext/convert/hg.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/hgext/convert/hg.py	Sat Jun 14 11:56:20 2014 -0500
@@ -136,8 +136,8 @@
             data, mode = source.getfile(f, v)
             if f == '.hgtags':
                 data = self._rewritetags(source, revmap, data)
-            return context.memfilectx(f, data, 'l' in mode, 'x' in mode,
-                                      copies.get(f))
+            return context.memfilectx(self.repo, f, data, 'l' in mode,
+                                      'x' in mode, copies.get(f))
 
         pl = []
         for p in parents:
@@ -229,7 +229,7 @@
 
         data = "".join(newlines)
         def getfilectx(repo, memctx, f):
-            return context.memfilectx(f, data, False, False, None)
+            return context.memfilectx(repo, f, data, False, False, None)
 
         self.ui.status(_("updating tags\n"))
         date = "%s 0" % int(time.mktime(time.gmtime()))
@@ -253,7 +253,11 @@
             destmarks[bookmark] = bin(updatedbookmark[bookmark])
         destmarks.write()
 
-    def hascommit(self, rev):
+    def hascommitfrommap(self, rev):
+        # the exact semantics of clonebranches is unclear so we can't say no
+        return rev in self.repo or self.clonebranches
+
+    def hascommitforsplicemap(self, rev):
         if rev not in self.repo and self.clonebranches:
             raise util.Abort(_('revision %s not found in destination '
                                'repository (lookups with clonebranches=true '
@@ -394,7 +398,9 @@
                       sortkey=ctx.rev())
 
     def gettags(self):
-        tags = [t for t in self.repo.tagslist() if t[0] != 'tip']
+        # This will get written to .hgtags, filter non global tags out.
+        tags = [t for t in self.repo.tagslist()
+                if self.repo.tagtype(t[0]) == 'global']
         return dict([(name, hex(node)) for name, node in tags
                      if self.keep(node)])
 
--- a/hgext/convert/subversion.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/hgext/convert/subversion.py	Sat Jun 14 11:56:20 2014 -0500
@@ -1300,7 +1300,12 @@
         self.ui.warn(_('writing Subversion tags is not yet implemented\n'))
         return None, None
 
-    def hascommit(self, rev):
+    def hascommitfrommap(self, rev):
+        # We trust that revisions referenced in a map still is present
+        # TODO: implement something better if necessary and feasible
+        return True
+
+    def hascommitforsplicemap(self, rev):
         # This is not correct as one can convert to an existing subversion
         # repository and childmap would not list all revisions. Too bad.
         if rev in self.childmap:
--- a/hgext/extdiff.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/hgext/extdiff.py	Sat Jun 14 11:56:20 2014 -0500
@@ -63,9 +63,11 @@
 
 from mercurial.i18n import _
 from mercurial.node import short, nullid
-from mercurial import scmutil, scmutil, util, commands, encoding
+from mercurial import cmdutil, scmutil, scmutil, util, commands, encoding
 import os, shlex, shutil, tempfile, re
 
+cmdtable = {}
+command = cmdutil.command(cmdtable)
 testedwith = 'internal'
 
 def snapshot(ui, repo, files, node, tmproot):
@@ -238,6 +240,15 @@
         ui.note(_('cleaning up temp directory\n'))
         shutil.rmtree(tmproot)
 
+@command('extdiff',
+    [('p', 'program', '',
+     _('comparison program to run'), _('CMD')),
+    ('o', 'option', [],
+     _('pass option to comparison program'), _('OPT')),
+    ('r', 'rev', [], _('revision'), _('REV')),
+    ('c', 'change', '', _('change made by revision'), _('REV')),
+    ] + commands.walkopts,
+    _('hg extdiff [OPT]... [FILE]...'))
 def extdiff(ui, repo, *pats, **opts):
     '''use external program to diff repository (or selected files)
 
@@ -262,21 +273,6 @@
         option = option or ['-Npru']
     return dodiff(ui, repo, program, option, pats, opts)
 
-cmdtable = {
-    "extdiff":
-    (extdiff,
-     [('p', 'program', '',
-       _('comparison program to run'), _('CMD')),
-      ('o', 'option', [],
-       _('pass option to comparison program'), _('OPT')),
-      ('r', 'rev', [],
-       _('revision'), _('REV')),
-      ('c', 'change', '',
-       _('change made by revision'), _('REV')),
-     ] + commands.walkopts,
-     _('hg extdiff [OPT]... [FILE]...')),
-    }
-
 def uisetup(ui):
     for cmd, path in ui.configitems('extdiff'):
         if cmd.startswith('cmd.'):
--- a/hgext/factotum.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/hgext/factotum.py	Sat Jun 14 11:56:20 2014 -0500
@@ -52,6 +52,8 @@
 
 ERRMAX = 128
 
+_executable = _mountpoint = _service = None
+
 def auth_getkey(self, params):
     if not self.ui.interactive():
         raise util.Abort(_('factotum not interactive'))
--- a/hgext/fetch.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/hgext/fetch.py	Sat Jun 14 11:56:20 2014 -0500
@@ -12,8 +12,18 @@
 from mercurial import commands, cmdutil, hg, util, error
 from mercurial.lock import release
 
+cmdtable = {}
+command = cmdutil.command(cmdtable)
 testedwith = 'internal'
 
+@command('fetch',
+    [('r', 'rev', [],
+     _('a specific revision you would like to pull'), _('REV')),
+    ('e', 'edit', None, _('edit commit message')),
+    ('', 'force-editor', None, _('edit commit message (DEPRECATED)')),
+    ('', 'switch-parent', None, _('switch parents when merging')),
+    ] + commands.commitopts + commands.commitopts2 + commands.remoteopts,
+    _('hg fetch [SOURCE]'))
 def fetch(ui, repo, source='default', **opts):
     '''pull changes from a remote repository, merge new changes if needed.
 
@@ -132,10 +142,9 @@
             message = (cmdutil.logmessage(ui, opts) or
                        ('Automated merge with %s' %
                         util.removeauth(other.url())))
-            editor = cmdutil.commiteditor
-            if opts.get('force_editor') or opts.get('edit'):
-                editor = cmdutil.commitforceeditor
-            n = repo.commit(message, opts['user'], opts['date'], editor=editor)
+            editopt = opts.get('edit') or opts.get('force_editor')
+            n = repo.commit(message, opts['user'], opts['date'],
+                            editor=cmdutil.getcommiteditor(edit=editopt))
             ui.status(_('new changeset %d:%s merges remote changes '
                         'with local\n') % (repo.changelog.rev(n),
                                            short(n)))
@@ -144,15 +153,3 @@
 
     finally:
         release(lock, wlock)
-
-cmdtable = {
-    'fetch':
-        (fetch,
-        [('r', 'rev', [],
-          _('a specific revision you would like to pull'), _('REV')),
-         ('e', 'edit', None, _('edit commit message')),
-         ('', 'force-editor', None, _('edit commit message (DEPRECATED)')),
-         ('', 'switch-parent', None, _('switch parents when merging')),
-        ] + commands.commitopts + commands.commitopts2 + commands.remoteopts,
-        _('hg fetch [SOURCE]')),
-}
--- a/hgext/gpg.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/hgext/gpg.py	Sat Jun 14 11:56:20 2014 -0500
@@ -204,6 +204,7 @@
            _('the key id to sign with'), _('ID')),
           ('m', 'message', '',
            _('commit message'), _('TEXT')),
+          ('e', 'edit', False, _('invoke editor on commit messages')),
          ] + commands.commitopts2,
          _('hg sign [OPTION]... [REV]...'))
 def sign(ui, repo, *revs, **opts):
@@ -276,7 +277,8 @@
                              % hgnode.short(n)
                              for n in nodes])
     try:
-        repo.commit(message, opts['user'], opts['date'], match=msigs)
+        repo.commit(message, opts['user'], opts['date'], match=msigs,
+                    editor=cmdutil.getcommiteditor(**opts))
     except ValueError, inst:
         raise util.Abort(str(inst))
 
--- a/hgext/hgk.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/hgext/hgk.py	Sat Jun 14 11:56:20 2014 -0500
@@ -35,12 +35,22 @@
 '''
 
 import os
-from mercurial import commands, util, patch, revlog, scmutil
+from mercurial import cmdutil, commands, util, patch, revlog, scmutil
 from mercurial.node import nullid, nullrev, short
 from mercurial.i18n import _
 
+cmdtable = {}
+command = cmdutil.command(cmdtable)
 testedwith = 'internal'
 
+@command('debug-diff-tree',
+    [('p', 'patch', None, _('generate patch')),
+    ('r', 'recursive', None, _('recursive')),
+    ('P', 'pretty', None, _('pretty')),
+    ('s', 'stdin', None, _('stdin')),
+    ('C', 'copy', None, _('detect copies')),
+    ('S', 'search', "", _('search'))],
+    ('hg git-diff-tree [OPTION]... NODE1 NODE2 [FILE]...'))
 def difftree(ui, repo, node1=None, node2=None, *files, **opts):
     """diff trees from two commits"""
     def __difftree(repo, node1, node2, files=[]):
@@ -125,6 +135,7 @@
     if prefix:
         ui.write('\0')
 
+@command('debug-merge-base', [], _('hg debug-merge-base REV REV'))
 def base(ui, repo, node1, node2):
     """output common ancestor information"""
     node1 = repo.lookup(node1)
@@ -132,6 +143,9 @@
     n = repo.changelog.ancestor(node1, node2)
     ui.write(short(n) + "\n")
 
+@command('debug-cat-file',
+    [('s', 'stdin', None, _('stdin'))],
+    _('hg debug-cat-file [OPTION]... TYPE FILE'))
 def catfile(ui, repo, type=None, r=None, **opts):
     """cat a specific revision"""
     # in stdin mode, every line except the commit is prefixed with two
@@ -276,6 +290,9 @@
                 break
             count += 1
 
+@command('debug-rev-parse',
+    [('', 'default', '', _('ignored'))],
+    _('hg debug-rev-parse REV'))
 def revparse(ui, repo, *revs, **opts):
     """parse given revisions"""
     def revstr(rev):
@@ -292,6 +309,12 @@
 # git rev-list tries to order things by date, and has the ability to stop
 # at a given commit without walking the whole repo.  TODO add the stop
 # parameter
+@command('debug-rev-list',
+    [('H', 'header', None, _('header')),
+    ('t', 'topo-order', None, _('topo-order')),
+    ('p', 'parents', None, _('parents')),
+    ('n', 'max-count', 0, _('max-count'))],
+    ('hg debug-rev-list [OPTION]... REV...'))
 def revlist(ui, repo, *revs, **opts):
     """print revisions"""
     if opts['header']:
@@ -301,6 +324,7 @@
     copy = [x for x in revs]
     revtree(ui, copy, repo, full, opts['max_count'], opts['parents'])
 
+@command('debug-config', [], _('hg debug-config'))
 def config(ui, repo, **opts):
     """print extension options"""
     def writeopt(name, value):
@@ -309,6 +333,10 @@
     writeopt('vdiff', ui.config('hgk', 'vdiff', ''))
 
 
+@command('view',
+    [('l', 'limit', '',
+     _('limit number of changes displayed'), _('NUM'))],
+    _('hg view [-l LIMIT] [REVRANGE]'))
 def view(ui, repo, *etc, **opts):
     "start interactive history viewer"
     os.chdir(repo.root)
@@ -317,40 +345,4 @@
     ui.debug("running %s\n" % cmd)
     util.system(cmd)
 
-cmdtable = {
-    "^view":
-        (view,
-         [('l', 'limit', '',
-           _('limit number of changes displayed'), _('NUM'))],
-         _('hg view [-l LIMIT] [REVRANGE]')),
-    "debug-diff-tree":
-        (difftree,
-         [('p', 'patch', None, _('generate patch')),
-          ('r', 'recursive', None, _('recursive')),
-          ('P', 'pretty', None, _('pretty')),
-          ('s', 'stdin', None, _('stdin')),
-          ('C', 'copy', None, _('detect copies')),
-          ('S', 'search', "", _('search'))],
-         _('hg git-diff-tree [OPTION]... NODE1 NODE2 [FILE]...')),
-    "debug-cat-file":
-        (catfile,
-         [('s', 'stdin', None, _('stdin'))],
-         _('hg debug-cat-file [OPTION]... TYPE FILE')),
-    "debug-config":
-        (config, [], _('hg debug-config')),
-    "debug-merge-base":
-        (base, [], _('hg debug-merge-base REV REV')),
-    "debug-rev-parse":
-        (revparse,
-         [('', 'default', '', _('ignored'))],
-         _('hg debug-rev-parse REV')),
-    "debug-rev-list":
-        (revlist,
-         [('H', 'header', None, _('header')),
-          ('t', 'topo-order', None, _('topo-order')),
-          ('p', 'parents', None, _('parents')),
-          ('n', 'max-count', 0, _('max-count'))],
-         _('hg debug-rev-list [OPTION]... REV...')),
-}
-
 commands.inferrepo += " debug-diff-tree debug-cat-file"
--- a/hgext/histedit.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/hgext/histedit.py	Sat Jun 14 11:56:20 2014 -0500
@@ -275,7 +275,8 @@
         if path in headmf:
             fctx = last[path]
             flags = fctx.flags()
-            mctx = context.memfilectx(fctx.path(), fctx.data(),
+            mctx = context.memfilectx(repo,
+                                      fctx.path(), fctx.data(),
                                       islink='l' in flags,
                                       isexec='x' in flags,
                                       copied=copied.get(path))
@@ -298,9 +299,8 @@
                          filectxfn=filectxfn,
                          user=user,
                          date=date,
-                         extra=extra)
-    new._text = cmdutil.commitforceeditor(repo, new, [])
-    repo.savecommitmessage(new.description())
+                         extra=extra,
+                         editor=cmdutil.getcommiteditor(edit=True))
     return repo.commitctx(new)
 
 def pick(ui, repo, ctx, ha, opts):
@@ -402,12 +402,11 @@
     if stats and stats[3] > 0:
         raise error.InterventionRequired(
             _('Fix up the change and run hg histedit --continue'))
-    message = oldctx.description() + '\n'
-    message = ui.edit(message, ui.username())
-    repo.savecommitmessage(message)
+    message = oldctx.description()
     commit = commitfuncfor(repo, oldctx)
     new = commit(text=message, user=oldctx.user(), date=oldctx.date(),
-                 extra=oldctx.extra())
+                 extra=oldctx.extra(),
+                 editor=cmdutil.getcommiteditor(edit=True))
     newctx = repo[new]
     if oldctx.node() != newctx.node():
         return newctx, [(oldctx.node(), (new,))]
@@ -682,11 +681,9 @@
         if action in ('f', 'fold'):
             message = 'fold-temp-revision %s' % currentnode
         else:
-            message = ctx.description() + '\n'
-        if action in ('e', 'edit', 'm', 'mess'):
-            editor = cmdutil.commitforceeditor
-        else:
-            editor = False
+            message = ctx.description()
+        editopt = action in ('e', 'edit', 'm', 'mess')
+        editor = cmdutil.getcommiteditor(edit=editopt)
         commit = commitfuncfor(repo, ctx)
         new = commit(text=message, user=ctx.user(),
                      date=ctx.date(), extra=ctx.extra(),
--- a/hgext/largefiles/lfcommands.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/hgext/largefiles/lfcommands.py	Sat Jun 14 11:56:20 2014 -0500
@@ -21,6 +21,18 @@
 
 # -- Commands ----------------------------------------------------------
 
+cmdtable = {}
+command = cmdutil.command(cmdtable)
+
+commands.inferrepo += " lfconvert"
+
+@command('lfconvert',
+    [('s', 'size', '',
+      _('minimum size (MB) for files to be converted as largefiles'), 'SIZE'),
+    ('', 'to-normal', False,
+     _('convert from a largefiles repo to a normal repo')),
+    ],
+    _('hg lfconvert SOURCE DEST [FILE ...]'))
 def lfconvert(ui, src, dest, *pats, **opts):
     '''convert a normal repository to a largefiles repository
 
@@ -160,10 +172,10 @@
             finally:
                 if fd:
                     fd.close()
-            return context.memfilectx(f, data, 'l' in fctx.flags(),
+            return context.memfilectx(repo, f, data, 'l' in fctx.flags(),
                                       'x' in fctx.flags(), renamed)
         else:
-            return _getnormalcontext(repo.ui, ctx, f, revmap)
+            return _getnormalcontext(repo, ctx, f, revmap)
 
     dstfiles = []
     for file in files:
@@ -243,10 +255,11 @@
                 # doesn't change after rename or copy
                 renamed = lfutil.standin(renamed[0])
 
-            return context.memfilectx(f, lfiletohash[srcfname] + '\n', 'l' in
-                fctx.flags(), 'x' in fctx.flags(), renamed)
+            return context.memfilectx(repo, f, lfiletohash[srcfname] + '\n',
+                                      'l' in fctx.flags(), 'x' in fctx.flags(),
+                                      renamed)
         else:
-            return _getnormalcontext(repo.ui, ctx, f, revmap)
+            return _getnormalcontext(repo, ctx, f, revmap)
 
     # Commit
     _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap)
@@ -281,7 +294,7 @@
     return parents
 
 # Get memfilectx for a normal file
-def _getnormalcontext(ui, ctx, f, revmap):
+def _getnormalcontext(repo, ctx, f, revmap):
     try:
         fctx = ctx.filectx(f)
     except error.LookupError:
@@ -292,8 +305,8 @@
 
     data = fctx.data()
     if f == '.hgtags':
-        data = _converttags (ui, revmap, data)
-    return context.memfilectx(f, data, 'l' in fctx.flags(),
+        data = _converttags (repo.ui, revmap, data)
+    return context.memfilectx(repo, f, data, 'l' in fctx.flags(),
                               'x' in fctx.flags(), renamed)
 
 # Remap tag data using a revision map
@@ -519,6 +532,10 @@
     finally:
         wlock.release()
 
+@command('lfpull',
+    [('r', 'rev', [], _('pull largefiles for these revisions'))
+    ] + commands.remoteopts,
+    _('-r REV... [-e CMD] [--remotecmd CMD] [SOURCE]'))
 def lfpull(ui, repo, source="default", **opts):
     """pull largefiles for the specified revisions from the specified source
 
@@ -553,24 +570,3 @@
         (cached, missing) = cachelfiles(ui, repo, rev)
         numcached += len(cached)
     ui.status(_("%d largefiles cached\n") % numcached)
-
-# -- hg commands declarations ------------------------------------------------
-
-cmdtable = {
-    'lfconvert': (lfconvert,
-                  [('s', 'size', '',
-                    _('minimum size (MB) for files to be converted '
-                      'as largefiles'),
-                    'SIZE'),
-                  ('', 'to-normal', False,
-                   _('convert from a largefiles repo to a normal repo')),
-                  ],
-                  _('hg lfconvert SOURCE DEST [FILE ...]')),
-    'lfpull': (lfpull,
-               [('r', 'rev', [], _('pull largefiles for these revisions'))
-                ] +  commands.remoteopts,
-               _('-r REV... [-e CMD] [--remotecmd CMD] [SOURCE]')
-               ),
-    }
-
-commands.inferrepo += " lfconvert"
--- a/hgext/largefiles/overrides.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/hgext/largefiles/overrides.py	Sat Jun 14 11:56:20 2014 -0500
@@ -282,6 +282,8 @@
             standin = lfutil.standin(m._files[i])
             if standin in repo[ctx.node()]:
                 m._files[i] = standin
+            elif m._files[i] not in repo[ctx.node()]:
+                m._files.append(standin)
             pats.add(standin)
 
         m._fmap = set(m._files)
@@ -408,14 +410,13 @@
     if overwrite:
         return actions
 
-    removes = set(a[0] for a in actions if a[1] == 'r')
-    processed = []
+    removes = set(a[0] for a in actions['r'])
 
-    for action in actions:
-        f, m, args, msg = action
-
+    newglist = []
+    for action in actions['g']:
+        f, args, msg = action
         splitstandin = f and lfutil.splitstandin(f)
-        if (m == "g" and splitstandin is not None and
+        if (splitstandin is not None and
             splitstandin in p1 and splitstandin not in removes):
             # Case 1: normal file in the working copy, largefile in
             # the second parent
@@ -425,12 +426,11 @@
                     'use (l)argefile or keep (n)ormal file?'
                     '$$ &Largefile $$ &Normal file') % lfile
             if repo.ui.promptchoice(msg, 0) == 0:
-                processed.append((lfile, "r", None, msg))
-                processed.append((standin, "g", (p2.flags(standin),), msg))
+                actions['r'].append((lfile, None, msg))
+                newglist.append((standin, (p2.flags(standin),), msg))
             else:
-                processed.append((standin, "r", None, msg))
-        elif (m == "g" and
-            lfutil.standin(f) in p1 and lfutil.standin(f) not in removes):
+                actions['r'].append((standin, None, msg))
+        elif lfutil.standin(f) in p1 and lfutil.standin(f) not in removes:
             # Case 2: largefile in the working copy, normal file in
             # the second parent
             standin = lfutil.standin(f)
@@ -439,20 +439,23 @@
                     'keep (l)argefile or use (n)ormal file?'
                     '$$ &Largefile $$ &Normal file') % lfile
             if repo.ui.promptchoice(msg, 0) == 0:
-                processed.append((lfile, "r", None, msg))
+                actions['r'].append((lfile, None, msg))
             else:
-                processed.append((standin, "r", None, msg))
-                processed.append((lfile, "g", (p2.flags(lfile),), msg))
+                actions['r'].append((standin, None, msg))
+                newglist.append((lfile, (p2.flags(lfile),), msg))
         else:
-            processed.append(action)
+            newglist.append(action)
 
-    return processed
+    newglist.sort()
+    actions['g'] = newglist
+
+    return actions
 
 # Override filemerge to prompt the user about how they wish to merge
 # largefiles. This will handle identical edits without prompting the user.
-def overridefilemerge(origfn, repo, mynode, orig, fcd, fco, fca):
+def overridefilemerge(origfn, repo, mynode, orig, fcd, fco, fca, labels=None):
     if not lfutil.isstandin(orig):
-        return origfn(repo, mynode, orig, fcd, fco, fca)
+        return origfn(repo, mynode, orig, fcd, fco, fca, labels=labels)
 
     ahash = fca.data().strip().lower()
     dhash = fcd.data().strip().lower()
--- a/hgext/mq.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/hgext/mq.py	Sat Jun 14 11:56:20 2014 -0500
@@ -1026,6 +1026,7 @@
            msg: a string or a no-argument function returning a string
         """
         msg = opts.get('msg')
+        edit = opts.get('edit')
         user = opts.get('user')
         date = opts.get('date')
         if date:
@@ -1078,12 +1079,25 @@
                         p.write("# User " + user + "\n")
                     if date:
                         p.write("# Date %s %s\n\n" % date)
-                if util.safehasattr(msg, '__call__'):
-                    msg = msg()
-                    repo.savecommitmessage(msg)
-                commitmsg = msg and msg or ("[mq]: %s" % patchfn)
+
+                defaultmsg = "[mq]: %s" % patchfn
+                editor = cmdutil.getcommiteditor()
+                if edit:
+                    def finishdesc(desc):
+                        if desc.rstrip():
+                            return desc
+                        else:
+                            return defaultmsg
+                    # i18n: this message is shown in editor with "HG: " prefix
+                    extramsg = _('Leave message empty to use default message.')
+                    editor = cmdutil.getcommiteditor(finishdesc=finishdesc,
+                                                     extramsg=extramsg)
+                    commitmsg = msg
+                else:
+                    commitmsg = msg or defaultmsg
+
                 n = newcommit(repo, None, commitmsg, user, date, match=match,
-                              force=True)
+                              force=True, editor=editor)
                 if n is None:
                     raise util.Abort(_("repo commit failed"))
                 try:
@@ -1092,8 +1106,9 @@
                     self.parseseries()
                     self.seriesdirty = True
                     self.applieddirty = True
-                    if msg:
-                        msg = msg + "\n\n"
+                    nctx = repo[n]
+                    if nctx.description() != defaultmsg.rstrip():
+                        msg = nctx.description() + "\n\n"
                         p.write(msg)
                     if commitfiles:
                         parent = self.qparents(repo, n)
@@ -1471,6 +1486,7 @@
             self.ui.write(_("no patches applied\n"))
             return 1
         msg = opts.get('msg', '').rstrip()
+        edit = opts.get('edit')
         newuser = opts.get('user')
         newdate = opts.get('date')
         if newdate:
@@ -1495,8 +1511,6 @@
 
             ph = patchheader(self.join(patchfn), self.plainmode)
             diffopts = self.diffopts({'git': opts.get('git')}, patchfn)
-            if msg:
-                ph.setmessage(msg)
             if newuser:
                 ph.setuser(newuser)
             if newdate:
@@ -1506,10 +1520,6 @@
             # only commit new patch when write is complete
             patchf = self.opener(patchfn, 'w', atomictemp=True)
 
-            comments = str(ph)
-            if comments:
-                patchf.write(comments)
-
             # update the dirstate in place, strip off the qtip commit
             # and then commit.
             #
@@ -1629,14 +1639,6 @@
                 for f in forget:
                     repo.dirstate.drop(f)
 
-                if not msg:
-                    if not ph.message:
-                        message = "[mq]: %s\n" % patchfn
-                    else:
-                        message = "\n".join(ph.message)
-                else:
-                    message = msg
-
                 user = ph.user or changes[1]
 
                 oldphase = repo[top].phase()
@@ -1653,16 +1655,41 @@
             try:
                 # might be nice to attempt to roll back strip after this
 
+                defaultmsg = "[mq]: %s" % patchfn
+                editor = cmdutil.getcommiteditor()
+                if edit:
+                    def finishdesc(desc):
+                        if desc.rstrip():
+                            ph.setmessage(desc)
+                            return desc
+                        return defaultmsg
+                    # i18n: this message is shown in editor with "HG: " prefix
+                    extramsg = _('Leave message empty to use default message.')
+                    editor = cmdutil.getcommiteditor(finishdesc=finishdesc,
+                                                     extramsg=extramsg)
+                    message = msg or "\n".join(ph.message)
+                elif not msg:
+                    if not ph.message:
+                        message = defaultmsg
+                    else:
+                        message = "\n".join(ph.message)
+                else:
+                    message = msg
+                    ph.setmessage(msg)
+
                 # Ensure we create a new changeset in the same phase than
                 # the old one.
                 n = newcommit(repo, oldphase, message, user, ph.date,
-                              match=match, force=True)
+                              match=match, force=True, editor=editor)
                 # only write patch after a successful commit
                 c = [list(x) for x in refreshchanges]
                 if inclsubs:
                     self.putsubstate2changes(substatestate, c)
                 chunks = patchmod.diff(repo, patchparent,
                                        changes=c, opts=diffopts)
+                comments = str(ph)
+                if comments:
+                    patchf.write(comments)
                 for chunk in chunks:
                     patchf.write(chunk)
                 patchf.close()
@@ -2417,14 +2444,8 @@
     Returns 0 on successful creation of a new patch.
     """
     msg = cmdutil.logmessage(ui, opts)
-    def getmsg():
-        return ui.edit(msg, opts.get('user') or ui.username())
     q = repo.mq
     opts['msg'] = msg
-    if opts.get('edit'):
-        opts['msg'] = getmsg
-    else:
-        opts['msg'] = msg
     setupheaderopts(ui, opts)
     q.new(repo, patch, *args, **opts)
     q.savedirty()
@@ -2468,17 +2489,6 @@
     """
     q = repo.mq
     message = cmdutil.logmessage(ui, opts)
-    if opts.get('edit'):
-        if not q.applied:
-            ui.write(_("no patches applied\n"))
-            return 1
-        if message:
-            raise util.Abort(_('option "-e" incompatible with "-m" or "-l"'))
-        patch = q.applied[-1].name
-        ph = patchheader(q.join(patch), q.plainmode)
-        message = ui.edit('\n'.join(ph.message), ph.user or ui.username())
-        # We don't want to lose the patch message if qrefresh fails (issue2062)
-        repo.savecommitmessage(message)
     setupheaderopts(ui, opts)
     wlock = repo.wlock()
     try:
@@ -2536,9 +2546,6 @@
     q.checklocalchanges(repo)
 
     message = cmdutil.logmessage(ui, opts)
-    if opts.get('edit'):
-        if message:
-            raise util.Abort(_('option "-e" incompatible with "-m" or "-l"'))
 
     parent = q.lookup('qtip')
     patches = []
@@ -2564,7 +2571,7 @@
 
     if not message:
         ph = patchheader(q.join(parent), q.plainmode)
-        message, user = ph.message, ph.user
+        message = ph.message
         for msg in messages:
             if msg:
                 if message:
@@ -2572,14 +2579,10 @@
                 message.extend(msg)
         message = '\n'.join(message)
 
-    if opts.get('edit'):
-        message = ui.edit(message, user or ui.username())
-        repo.savecommitmessage(message)
-
     diffopts = q.patchopts(q.diffopts(), *patches)
     wlock = repo.wlock()
     try:
-        q.refresh(repo, msg=message, git=diffopts.git)
+        q.refresh(repo, msg=message, git=diffopts.git, edit=opts.get('edit'))
         q.delete(repo, patches, opts)
         q.savedirty()
     finally:
--- a/hgext/pager.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/hgext/pager.py	Sat Jun 14 11:56:20 2014 -0500
@@ -39,12 +39,20 @@
 
 If pager.attend is present, pager.ignore will be ignored.
 
+Lastly, you can enable and disable paging for individual commands with
+the attend-<command> option. This setting takes precedence over
+existing attend and ignore options and defaults::
+
+  [pager]
+  attend-cat = false
+
 To ignore global commands like :hg:`version` or :hg:`help`, you have
 to specify them in your user configuration file.
 
 The --pager=... option can also be used to control when the pager is
 used. Use a boolean value like yes, no, on, off, or use auto for
 normal behavior.
+
 '''
 
 import atexit, sys, os, signal, subprocess, errno, shlex
@@ -116,25 +124,37 @@
 
     def pagecmd(orig, ui, options, cmd, cmdfunc):
         p = ui.config("pager", "pager", os.environ.get("PAGER"))
+        usepager = False
+        always = util.parsebool(options['pager'])
+        auto = options['pager'] == 'auto'
 
-        if p:
+        if not p:
+            pass
+        elif always:
+            usepager = True
+        elif not auto:
+            usepager = False
+        else:
             attend = ui.configlist('pager', 'attend', attended)
-            auto = options['pager'] == 'auto'
-            always = util.parsebool(options['pager'])
-
+            ignore = ui.configlist('pager', 'ignore')
             cmds, _ = cmdutil.findcmd(cmd, commands.table)
 
-            ignore = ui.configlist('pager', 'ignore')
             for cmd in cmds:
-                if (always or auto and
-                    (cmd in attend or
-                     (cmd not in ignore and not attend))):
-                    ui.setconfig('ui', 'formatted', ui.formatted(), 'pager')
-                    ui.setconfig('ui', 'interactive', False, 'pager')
-                    if util.safehasattr(signal, "SIGPIPE"):
-                        signal.signal(signal.SIGPIPE, signal.SIG_DFL)
-                    _runpager(ui, p)
+                var = 'attend-%s' % cmd
+                if ui.config('pager', var):
+                    usepager = ui.configbool('pager', var)
+                    break
+                if (cmd in attend or
+                     (cmd not in ignore and not attend)):
+                    usepager = True
                     break
+
+        if usepager:
+            ui.setconfig('ui', 'formatted', ui.formatted(), 'pager')
+            ui.setconfig('ui', 'interactive', False, 'pager')
+            if util.safehasattr(signal, "SIGPIPE"):
+                signal.signal(signal.SIGPIPE, signal.SIG_DFL)
+            _runpager(ui, p)
         return orig(ui, options, cmd, cmdfunc)
 
     extensions.wrapfunction(dispatch, '_runcommand', pagecmd)
--- a/hgext/patchbomb.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/hgext/patchbomb.py	Sat Jun 14 11:56:20 2014 -0500
@@ -149,6 +149,8 @@
         subj = '[PATCH %0*d of %d%s] %s' % (tlen, idx, total, flag, subj)
     msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
     msg['X-Mercurial-Node'] = node
+    msg['X-Mercurial-Series-Index'] = '%i' % idx
+    msg['X-Mercurial-Series-Total'] = '%i' % total
     return msg, subj, ds
 
 emailopts = [
@@ -507,9 +509,13 @@
     sender_addr = email.Utils.parseaddr(sender)[1]
     sender = mail.addressencode(ui, sender, _charsets, opts.get('test'))
     sendmail = None
+    firstpatch = None
     for i, (m, subj, ds) in enumerate(msgs):
         try:
             m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
+            if not firstpatch:
+                firstpatch = m['Message-Id']
+            m['X-Mercurial-Series-Id'] = firstpatch
         except TypeError:
             m['Message-Id'] = genmsgid('patchbomb')
         if parent:
--- a/hgext/rebase.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/hgext/rebase.py	Sat Jun 14 11:56:20 2014 -0500
@@ -138,9 +138,7 @@
     skipped = set()
     targetancestors = set()
 
-    editor = None
-    if opts.get('edit'):
-        editor = cmdutil.commitforceeditor
+    editor = cmdutil.getcommiteditor(**opts)
 
     lock = wlock = None
     try:
@@ -376,7 +374,7 @@
                 for rebased in state:
                     if rebased not in skipped and state[rebased] > nullmerge:
                         commitmsg += '\n* %s' % repo[rebased].description()
-                editor = cmdutil.commitforceeditor
+                editor = cmdutil.getcommiteditor(edit=True)
             newrev = concludenode(repo, rev, p1, external, commitmsg=commitmsg,
                                   extrafn=extrafn, editor=editor)
             for oldrev in state.iterkeys():
@@ -533,7 +531,8 @@
         repo.ui.debug("   detach base %d:%s\n" % (repo[base].rev(), repo[base]))
     # When collapsing in-place, the parent is the common ancestor, we
     # have to allow merging with it.
-    return merge.update(repo, rev, True, True, False, base, collapse)
+    return merge.update(repo, rev, True, True, False, base, collapse,
+                        labels=['dest', 'source'])
 
 def nearestrebased(repo, rev, state):
     """return the nearest ancestors of rev in the rebase result"""
--- a/hgext/record.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/hgext/record.py	Sat Jun 14 11:56:20 2014 -0500
@@ -459,6 +459,8 @@
     # backup all changed files
     dorecord(ui, repo, committomq, 'qrefresh', True, *pats, **opts)
 
+# This command registration is replaced during uisetup().
+@command('qrecord', [], _('hg qrecord [OPTION]... PATCH [FILE]...'))
 def qrecord(ui, repo, patch, *pats, **opts):
     '''interactively record a new patch
 
@@ -637,10 +639,6 @@
     finally:
         ui.write = oldwrite
 
-cmdtable["qrecord"] = \
-    (qrecord, [], # placeholder until mq is available
-     _('hg qrecord [OPTION]... PATCH [FILE]...'))
-
 def uisetup(ui):
     try:
         mq = extensions.find('mq')
--- a/hgext/relink.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/hgext/relink.py	Sat Jun 14 11:56:20 2014 -0500
@@ -7,12 +7,15 @@
 
 """recreates hardlinks between repository clones"""
 
-from mercurial import hg, util
+from mercurial import cmdutil, hg, util
 from mercurial.i18n import _
 import os, stat
 
+cmdtable = {}
+command = cmdutil.command(cmdtable)
 testedwith = 'internal'
 
+@command('relink', [], _('[ORIGIN]'))
 def relink(ui, repo, origin=None, **opts):
     """recreate hardlinks between two repositories
 
@@ -178,11 +181,3 @@
 
     ui.status(_('relinked %d files (%s reclaimed)\n') %
               (relinked, util.bytecount(savedbytes)))
-
-cmdtable = {
-    'relink': (
-        relink,
-        [],
-        _('[ORIGIN]')
-    )
-}
--- a/hgext/share.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/hgext/share.py	Sat Jun 14 11:56:20 2014 -0500
@@ -6,10 +6,15 @@
 '''share a common history between several working directories'''
 
 from mercurial.i18n import _
-from mercurial import hg, commands, util
+from mercurial import cmdutil, hg, commands, util
 
+cmdtable = {}
+command = cmdutil.command(cmdtable)
 testedwith = 'internal'
 
+@command('share',
+    [('U', 'noupdate', None, _('do not create a working copy'))],
+    _('[-U] SOURCE [DEST]'))
 def share(ui, source, dest=None, noupdate=False):
     """create a new shared repository
 
@@ -30,6 +35,7 @@
 
     return hg.share(ui, source, dest, not noupdate)
 
+@command('unshare', [], '')
 def unshare(ui, repo):
     """convert a shared repository to a normal one
 
@@ -61,15 +67,4 @@
     # update store, spath, sopener and sjoin of repo
     repo.unfiltered().__init__(repo.baseui, repo.root)
 
-cmdtable = {
-    "share":
-    (share,
-     [('U', 'noupdate', None, _('do not create a working copy'))],
-     _('[-U] SOURCE [DEST]')),
-    "unshare":
-    (unshare,
-    [],
-    ''),
-}
-
 commands.norepo += " share"
--- a/hgext/transplant.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/hgext/transplant.py	Sat Jun 14 11:56:20 2014 -0500
@@ -80,13 +80,13 @@
             self.dirty = True
 
 class transplanter(object):
-    def __init__(self, ui, repo):
+    def __init__(self, ui, repo, opts):
         self.ui = ui
         self.path = repo.join('transplant')
         self.opener = scmutil.opener(self.path)
         self.transplants = transplants(self.path, 'transplants',
                                        opener=self.opener)
-        self.editor = None
+        self.editor = cmdutil.getcommiteditor(**opts)
 
     def applied(self, repo, node, parent):
         '''returns True if a node is already an ancestor of parent
@@ -599,9 +599,7 @@
     if not opts.get('filter'):
         opts['filter'] = ui.config('transplant', 'filter')
 
-    tp = transplanter(ui, repo)
-    if opts.get('edit'):
-        tp.editor = cmdutil.commitforceeditor
+    tp = transplanter(ui, repo, opts)
 
     cmdutil.checkunfinished(repo)
     p1, p2 = repo.dirstate.parents()
--- a/mercurial/bundle2.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/mercurial/bundle2.py	Sat Jun 14 11:56:20 2014 -0500
@@ -113,6 +113,8 @@
 
             Mandatory parameters comes first, then the advisory ones.
 
+            Each parameter's key MUST be unique within the part.
+
 :payload:
 
     payload is a series of `<chunksize><chunkdata>`.
@@ -144,6 +146,7 @@
 import struct
 import urllib
 import string
+import pushkey
 
 import changegroup, error
 from i18n import _
@@ -170,18 +173,14 @@
     """
     return '>'+('BB'*nbparams)
 
-class UnknownPartError(KeyError):
-    """error raised when no handler is found for a Mandatory part"""
-    pass
-
 parthandlermapping = {}
 
-def parthandler(parttype):
+def parthandler(parttype, params=()):
     """decorator that register a function as a bundle2 part handler
 
     eg::
 
-        @parthandler('myparttype')
+        @parthandler('myparttype', ('mandatory', 'param', 'handled'))
         def myparttypehandler(...):
             '''process a part of type "my part".'''
             ...
@@ -190,6 +189,7 @@
         lparttype = parttype.lower() # enforce lower case matching.
         assert lparttype not in parthandlermapping
         parthandlermapping[lparttype] = func
+        func.params = frozenset(params)
         return func
     return _decorator
 
@@ -295,18 +295,25 @@
             # part key are matched lower case
             key = parttype.lower()
             try:
-                handler = parthandlermapping[key]
+                handler = parthandlermapping.get(key)
+                if handler is None:
+                    raise error.BundleValueError(parttype=key)
                 op.ui.debug('found a handler for part %r\n' % parttype)
-            except KeyError:
+                unknownparams = part.mandatorykeys - handler.params
+                if unknownparams:
+                    unknownparams = list(unknownparams)
+                    unknownparams.sort()
+                    raise error.BundleValueError(parttype=key,
+                                                   params=unknownparams)
+            except error.BundleValueError, exc:
                 if key != parttype: # mandatory parts
-                    # todo:
-                    # - use a more precise exception
-                    raise UnknownPartError(key)
-                op.ui.debug('ignoring unknown advisory part %r\n' % key)
+                    raise
+                op.ui.debug('ignoring unsupported advisory part %s\n' % exc)
                 # consuming the part
                 part.read()
                 continue
 
+
             # handler is called outside the above try block so that we don't
             # risk catching KeyErrors from anything other than the
             # parthandlermapping lookup (any KeyError raised by handler()
@@ -321,11 +328,8 @@
                 if output is not None:
                     output = op.ui.popbuffer()
             if output:
-                outpart = bundlepart('b2x:output',
-                                     advisoryparams=[('in-reply-to',
-                                                      str(part.id))],
-                                     data=output)
-                op.reply.addpart(outpart)
+                outpart = op.reply.newpart('b2x:output', data=output)
+                outpart.addparam('in-reply-to', str(part.id), mandatory=False)
             part.read()
     except Exception, exc:
         if part is not None:
@@ -381,7 +385,7 @@
 class bundle20(object):
     """represent an outgoing bundle2 container
 
-    Use the `addparam` method to add stream level parameter. and `addpart` to
+    Use the `addparam` method to add stream level parameter. and `newpart` to
     populate it. Then call `getchunks` to retrieve all the binary chunks of
     data that compose the bundle2 container."""
 
@@ -391,6 +395,7 @@
         self._parts = []
         self.capabilities = dict(capabilities)
 
+    # methods used to defines the bundle2 content
     def addparam(self, name, value=None):
         """add a stream level parameter"""
         if not name:
@@ -407,6 +412,20 @@
         part.id = len(self._parts) # very cheap counter
         self._parts.append(part)
 
+    def newpart(self, typeid, *args, **kwargs):
+        """create a new part and add it to the containers
+
+        As the part is directly added to the containers. For now, this means
+        that any failure to properly initialize the part after calling
+        ``newpart`` should result in a failure of the whole bundling process.
+
+        You can still fall back to manually create and add if you need better
+        control."""
+        part = bundlepart(typeid, *args, **kwargs)
+        self.addpart(part)
+        return part
+
+    # methods used to generate the bundle2 stream
     def getchunks(self):
         self.ui.debug('start emission of %s stream\n' % _magicstring)
         yield _magicstring
@@ -505,7 +524,7 @@
         if name[0].islower():
             self.ui.debug("ignoring unknown parameter %r\n" % name)
         else:
-            raise KeyError(name)
+            raise error.BundleValueError(params=(name,))
 
 
     def iterparts(self):
@@ -536,17 +555,71 @@
 
     The part `type` is used to route the part to the application level
     handler.
+
+    The part payload is contained in ``part.data``. It could be raw bytes or a
+    generator of byte chunks.
+
+    You can add parameters to the part using the ``addparam`` method.
+    Parameters can be either mandatory (default) or advisory. Remote side
+    should be able to safely ignore the advisory ones.
+
+    Both data and parameters cannot be modified after the generation has begun.
     """
 
     def __init__(self, parttype, mandatoryparams=(), advisoryparams=(),
                  data=''):
         self.id = None
         self.type = parttype
-        self.data = data
-        self.mandatoryparams = mandatoryparams
-        self.advisoryparams = advisoryparams
+        self._data = data
+        self._mandatoryparams = list(mandatoryparams)
+        self._advisoryparams = list(advisoryparams)
+        # checking for duplicated entries
+        self._seenparams = set()
+        for pname, __ in self._mandatoryparams + self._advisoryparams:
+            if pname in self._seenparams:
+                raise RuntimeError('duplicated params: %s' % pname)
+            self._seenparams.add(pname)
+        # status of the part's generation:
+        # - None: not started,
+        # - False: currently generated,
+        # - True: generation done.
+        self._generated = None
+
+    # methods used to defines the part content
+    def __setdata(self, data):
+        if self._generated is not None:
+            raise error.ReadOnlyPartError('part is being generated')
+        self._data = data
+    def __getdata(self):
+        return self._data
+    data = property(__getdata, __setdata)
 
+    @property
+    def mandatoryparams(self):
+        # make it an immutable tuple to force people through ``addparam``
+        return tuple(self._mandatoryparams)
+
+    @property
+    def advisoryparams(self):
+        # make it an immutable tuple to force people through ``addparam``
+        return tuple(self._advisoryparams)
+
+    def addparam(self, name, value='', mandatory=True):
+        if self._generated is not None:
+            raise error.ReadOnlyPartError('part is being generated')
+        if name in self._seenparams:
+            raise ValueError('duplicated params: %s' % name)
+        self._seenparams.add(name)
+        params = self._advisoryparams
+        if mandatory:
+            params = self._mandatoryparams
+        params.append((name, value))
+
+    # methods used to generates the bundle2 stream
     def getchunks(self):
+        if self._generated is not None:
+            raise RuntimeError('part can only be consumed once')
+        self._generated = False
         #### header
         ## parttype
         header = [_pack(_fparttypesize, len(self.type)),
@@ -584,6 +657,7 @@
             yield chunk
         # end of payload
         yield _pack(_fpayloadsize, 0)
+        self._generated = True
 
     def _payloadchunks(self):
         """yield chunks of a the part payload
@@ -616,6 +690,8 @@
         self.type = None
         self.mandatoryparams = None
         self.advisoryparams = None
+        self.params = None
+        self.mandatorykeys = ()
         self._payloadstream = None
         self._readheader()
 
@@ -633,6 +709,16 @@
         data = self._fromheader(struct.calcsize(format))
         return _unpack(format, data)
 
+    def _initparams(self, mandatoryparams, advisoryparams):
+        """internal function to setup all logic related parameters"""
+        # make it read only to prevent people touching it by mistake.
+        self.mandatoryparams = tuple(mandatoryparams)
+        self.advisoryparams  = tuple(advisoryparams)
+        # user friendly UI
+        self.params = dict(self.mandatoryparams)
+        self.params.update(dict(self.advisoryparams))
+        self.mandatorykeys = frozenset(p[0] for p in mandatoryparams)
+
     def _readheader(self):
         """read the header and setup the object"""
         typesize = self._unpackheader(_fparttypesize)[0]
@@ -659,8 +745,7 @@
         advparams = []
         for key, value in advsizes:
             advparams.append((self._fromheader(key), self._fromheader(value)))
-        self.mandatoryparams = manparams
-        self.advisoryparams  = advparams
+        self._initparams(manparams, advparams)
         ## part payload
         def payloadchunks():
             payloadsize = self._unpack(_fpayloadsize)[0]
@@ -685,6 +770,13 @@
             self.consumed = True
         return data
 
+def bundle2caps(remote):
+    """return the bundlecapabilities of a peer as dict"""
+    raw = remote.capable('bundle2-exp')
+    if not raw and raw != '':
+        return {}
+    capsblob = urllib.unquote(remote.capable('bundle2-exp'))
+    return decodecaps(capsblob)
 
 @parthandler('b2x:changegroup')
 def handlechangegroup(op, inpart):
@@ -705,17 +797,16 @@
     if op.reply is not None:
         # This is definitly not the final form of this
         # return. But one need to start somewhere.
-        part = bundlepart('b2x:reply:changegroup', (),
-                           [('in-reply-to', str(inpart.id)),
-                            ('return', '%i' % ret)])
-        op.reply.addpart(part)
+        part = op.reply.newpart('b2x:reply:changegroup')
+        part.addparam('in-reply-to', str(inpart.id), mandatory=False)
+        part.addparam('return', '%i' % ret, mandatory=False)
     assert not inpart.read()
 
-@parthandler('b2x:reply:changegroup')
+@parthandler('b2x:reply:changegroup', ('return', 'in-reply-to'))
 def handlechangegroup(op, inpart):
-    p = dict(inpart.advisoryparams)
-    ret = int(p['return'])
-    op.records.add('changegroup', {'return': ret}, int(p['in-reply-to']))
+    ret = int(inpart.params['return'])
+    replyto = int(inpart.params['in-reply-to'])
+    op.records.add('changegroup', {'return': ret}, replyto)
 
 @parthandler('b2x:check:heads')
 def handlechangegroup(op, inpart):
@@ -748,21 +839,58 @@
     if op.reply is None:
         op.reply = bundle20(op.ui, caps)
 
-@parthandler('b2x:error:abort')
+@parthandler('b2x:error:abort', ('message', 'hint'))
 def handlereplycaps(op, inpart):
     """Used to transmit abort error over the wire"""
-    manargs = dict(inpart.mandatoryparams)
-    advargs = dict(inpart.advisoryparams)
-    raise util.Abort(manargs['message'], hint=advargs.get('hint'))
+    raise util.Abort(inpart.params['message'], hint=inpart.params.get('hint'))
 
-@parthandler('b2x:error:unknownpart')
+@parthandler('b2x:error:unsupportedcontent', ('parttype', 'params'))
 def handlereplycaps(op, inpart):
-    """Used to transmit unknown part error over the wire"""
-    manargs = dict(inpart.mandatoryparams)
-    raise UnknownPartError(manargs['parttype'])
+    """Used to transmit unknown content error over the wire"""
+    kwargs = {}
+    parttype = inpart.params.get('parttype')
+    if parttype is not None:
+        kwargs['parttype'] = parttype
+    params = inpart.params.get('params')
+    if params is not None:
+        kwargs['params'] = params.split('\0')
 
-@parthandler('b2x:error:pushraced')
+    raise error.BundleValueError(**kwargs)
+
+@parthandler('b2x:error:pushraced', ('message',))
 def handlereplycaps(op, inpart):
     """Used to transmit push race error over the wire"""
-    manargs = dict(inpart.mandatoryparams)
-    raise error.ResponseError(_('push failed:'), manargs['message'])
+    raise error.ResponseError(_('push failed:'), inpart.params['message'])
+
+@parthandler('b2x:listkeys', ('namespace',))
+def handlelistkeys(op, inpart):
+    """retrieve pushkey namespace content stored in a bundle2"""
+    namespace = inpart.params['namespace']
+    r = pushkey.decodekeys(inpart.read())
+    op.records.add('listkeys', (namespace, r))
+
+@parthandler('b2x:pushkey', ('namespace', 'key', 'old', 'new'))
+def handlepushkey(op, inpart):
+    """process a pushkey request"""
+    dec = pushkey.decode
+    namespace = dec(inpart.params['namespace'])
+    key = dec(inpart.params['key'])
+    old = dec(inpart.params['old'])
+    new = dec(inpart.params['new'])
+    ret = op.repo.pushkey(namespace, key, old, new)
+    record = {'namespace': namespace,
+              'key': key,
+              'old': old,
+              'new': new}
+    op.records.add('pushkey', record)
+    if op.reply is not None:
+        rpart = op.reply.newpart('b2x:reply:pushkey')
+        rpart.addparam('in-reply-to', str(inpart.id), mandatory=False)
+        rpart.addparam('return', '%i' % ret, mandatory=False)
+
+@parthandler('b2x:reply:pushkey', ('return', 'in-reply-to'))
+def handlepushkeyreply(op, inpart):
+    """retrieve the result of a pushkey request"""
+    ret = int(inpart.params['return'])
+    partid = int(inpart.params['in-reply-to'])
+    op.records.add('pushkey', {'return': ret}, partid)
--- a/mercurial/changegroup.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/mercurial/changegroup.py	Sat Jun 14 11:56:20 2014 -0500
@@ -493,6 +493,25 @@
     bundler = bundle10(repo, bundlecaps)
     return getsubset(repo, outgoing, bundler, source)
 
+def _computeoutgoing(repo, heads, common):
+    """Computes which revs are outgoing given a set of common
+    and a set of heads.
+
+    This is a separate function so extensions can have access to
+    the logic.
+
+    Returns a discovery.outgoing object.
+    """
+    cl = repo.changelog
+    if common:
+        hasnode = cl.hasnode
+        common = [n for n in common if hasnode(n)]
+    else:
+        common = [nullid]
+    if not heads:
+        heads = cl.heads()
+    return discovery.outgoing(cl, common, heads)
+
 def getbundle(repo, source, heads=None, common=None, bundlecaps=None):
     """Like changegroupsubset, but returns the set difference between the
     ancestors of heads and the ancestors common.
@@ -502,15 +521,7 @@
     The nodes in common might not all be known locally due to the way the
     current discovery protocol works.
     """
-    cl = repo.changelog
-    if common:
-        hasnode = cl.hasnode
-        common = [n for n in common if hasnode(n)]
-    else:
-        common = [nullid]
-    if not heads:
-        heads = cl.heads()
-    outgoing = discovery.outgoing(cl, common, heads)
+    outgoing = _computeoutgoing(repo, heads, common)
     return getlocalbundle(repo, source, outgoing, bundlecaps=bundlecaps)
 
 def changegroup(repo, basenodes, source):
--- a/mercurial/cmdutil.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/mercurial/cmdutil.py	Sat Jun 14 11:56:20 2014 -0500
@@ -109,6 +109,30 @@
                              (logfile, inst.strerror))
     return message
 
+def getcommiteditor(edit=False, finishdesc=None, extramsg=None, **opts):
+    """get appropriate commit message editor according to '--edit' option
+
+    'finishdesc' is a function to be called with edited commit message
+    (= 'description' of the new changeset) just after editing, but
+    before checking empty-ness. It should return actual text to be
+    stored into history. This allows to change description before
+    storing.
+
+    'extramsg' is a extra message to be shown in the editor instead of
+    'Leave message empty to abort commit' line. 'HG: ' prefix and EOL
+    is automatically added.
+
+    'getcommiteditor' returns 'commitforceeditor' regardless of
+    'edit', if one of 'finishdesc' or 'extramsg' is specified, because
+    they are specific for usage in MQ.
+    """
+    if edit or finishdesc or extramsg:
+        return lambda r, c, s: commitforceeditor(r, c, s,
+                                                 finishdesc=finishdesc,
+                                                 extramsg=extramsg)
+    else:
+        return commiteditor
+
 def loglimit(opts):
     """get the log limit according to option -l/--limit"""
     limit = opts.get('limit')
@@ -562,16 +586,16 @@
     tmpname, message, user, date, branch, nodeid, p1, p2 = \
         patch.extract(ui, hunk)
 
-    editor = commiteditor
-    if opts.get('edit'):
-        editor = commitforceeditor
+    editor = getcommiteditor(**opts)
     update = not opts.get('bypass')
     strip = opts["strip"]
     sim = float(opts.get('similarity') or 0)
     if not tmpname:
-        return (None, None)
+        return (None, None, False)
     msg = _('applied to working directory')
 
+    rejects = False
+
     try:
         cmdline_message = logmessage(ui, opts)
         if cmdline_message:
@@ -617,9 +641,17 @@
             if opts.get('exact') or opts.get('import_branch'):
                 repo.dirstate.setbranch(branch or 'default')
 
+            partial = opts.get('partial', False)
             files = set()
-            patch.patch(ui, repo, tmpname, strip=strip, files=files,
-                        eolmode=None, similarity=sim / 100.0)
+            try:
+                patch.patch(ui, repo, tmpname, strip=strip, files=files,
+                            eolmode=None, similarity=sim / 100.0)
+            except patch.PatchError, e:
+                if not partial:
+                    raise util.Abort(str(e))
+                if partial:
+                    rejects = True
+
             files = list(files)
             if opts.get('no_commit'):
                 if message:
@@ -634,7 +666,7 @@
                     m = scmutil.matchfiles(repo, files or [])
                 n = repo.commit(message, opts.get('user') or user,
                                 opts.get('date') or date, match=m,
-                                editor=editor)
+                                editor=editor, force=partial)
         else:
             if opts.get('exact') or opts.get('import_branch'):
                 branch = branch or 'default'
@@ -653,8 +685,7 @@
                                             opts.get('user') or user,
                                             opts.get('date') or date,
                                             branch, files, store,
-                                            editor=commiteditor)
-                repo.savecommitmessage(memctx.description())
+                                            editor=getcommiteditor())
                 n = memctx.commit()
             finally:
                 store.close()
@@ -663,7 +694,7 @@
         if n:
             # i18n: refers to a short changeset id
             msg = _('created %s') % short(n)
-        return (msg, n)
+        return (msg, n, rejects)
     finally:
         os.unlink(tmpname)
 
@@ -2026,7 +2057,8 @@
                     try:
                         fctx = ctx[path]
                         flags = fctx.flags()
-                        mctx = context.memfilectx(fctx.path(), fctx.data(),
+                        mctx = context.memfilectx(repo,
+                                                  fctx.path(), fctx.data(),
                                                   islink='l' in flags,
                                                   isexec='x' in flags,
                                                   copied=copied.get(path))
@@ -2045,12 +2077,10 @@
 
                 user = opts.get('user') or old.user()
                 date = opts.get('date') or old.date()
-            editmsg = False
+            editor = getcommiteditor(**opts)
             if not message:
-                editmsg = True
+                editor = getcommiteditor(edit=True)
                 message = old.description()
-            elif opts.get('edit'):
-                editmsg = True
 
             pureextra = extra.copy()
             extra['amend_source'] = old.hex()
@@ -2062,10 +2092,8 @@
                                  filectxfn=filectxfn,
                                  user=user,
                                  date=date,
-                                 extra=extra)
-            if editmsg:
-                new._text = commitforceeditor(repo, new, [])
-            repo.savecommitmessage(new.description())
+                                 extra=extra,
+                                 editor=editor)
 
             newdesc =  changelog.stripdesc(new.description())
             if ((not node)
@@ -2130,7 +2158,7 @@
         return ctx.description()
     return commitforceeditor(repo, ctx, subs)
 
-def commitforceeditor(repo, ctx, subs):
+def commitforceeditor(repo, ctx, subs, finishdesc=None, extramsg=None):
     edittext = []
     modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
     if ctx.description():
@@ -2139,7 +2167,10 @@
     edittext.append("") # Empty line between message and comments.
     edittext.append(_("HG: Enter commit message."
                       "  Lines beginning with 'HG:' are removed."))
-    edittext.append(_("HG: Leave message empty to abort commit."))
+    if extramsg:
+        edittext.append("HG: %s" % extramsg)
+    else:
+        edittext.append(_("HG: Leave message empty to abort commit."))
     edittext.append("HG: --")
     edittext.append(_("HG: user: %s") % ctx.user())
     if ctx.p2():
@@ -2162,6 +2193,8 @@
     text = re.sub("(?m)^HG:.*(\n|$)", "", text)
     os.chdir(olddir)
 
+    if finishdesc:
+        text = finishdesc(text)
     if not text.strip():
         raise util.Abort(_("empty commit message"))
 
@@ -2218,6 +2251,8 @@
     node = ctx.node()
 
     mf = ctx.manifest()
+    if node == p2:
+        parent = p2
     if node == parent:
         pmf = mf
     else:
@@ -2227,18 +2262,22 @@
     # so have to walk both. do not print errors if files exist in one
     # but not other.
 
+    # `names` is a mapping for all elements in working copy and target revision
+    # The mapping is in the form:
+    #   <asb path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
     names = {}
 
     wlock = repo.wlock()
     try:
-        # walk dirstate.
+        ## filling of the `names` mapping
+        # walk dirstate to fill `names`
 
         m = scmutil.match(repo[None], pats, opts)
         m.bad = lambda x, y: False
         for abs in repo.walk(m):
             names[abs] = m.rel(abs), m.exact(abs)
 
-        # walk target manifest.
+        # walk target manifest to fill `names`
 
         def badfn(path, msg):
             if path in names:
@@ -2259,11 +2298,13 @@
 
         # get the list of subrepos that must be reverted
         targetsubs = sorted(s for s in ctx.substate if m(s))
+
+        # Find status of all file in `names`. (Against working directory parent)
         m = scmutil.matchfiles(repo, names)
-        changes = repo.status(match=m)[:4]
+        changes = repo.status(node1=parent, match=m)[:4]
         modified, added, removed, deleted = map(set, changes)
 
-        # if f is a rename, also revert the source
+        # if f is a rename, update `names` to also revert the source
         cwd = repo.getcwd()
         for f in added:
             src = repo.dirstate.copied(f)
@@ -2271,15 +2312,19 @@
                 removed.add(src)
                 names[src] = (repo.pathto(src, cwd), True)
 
+        ## computation of the action to performs on `names` content.
+
         def removeforget(abs):
             if repo.dirstate[abs] == 'a':
                 return _('forgetting %s\n')
             return _('removing %s\n')
 
-        revert = ([], _('reverting %s\n'))
-        add = ([], _('adding %s\n'))
-        remove = ([], removeforget)
-        undelete = ([], _('undeleting %s\n'))
+        # action to be actually performed by revert
+        # (<list of file>, message>) tuple
+        actions = {'revert': ([], _('reverting %s\n')),
+                   'add': ([], _('adding %s\n')),
+                   'remove': ([], removeforget),
+                   'undelete': ([], _('undeleting %s\n'))}
 
         disptable = (
             # dispatch table:
@@ -2288,14 +2333,20 @@
             #   action if not in target manifest
             #   make backup if in target manifest
             #   make backup if not in target manifest
-            (modified, revert, remove, True, True),
-            (added, revert, remove, True, False),
-            (removed, undelete, None, True, False),
-            (deleted, revert, remove, False, False),
+            (modified, (actions['revert'],   True),
+                       (actions['remove'],   True)),
+            (added,    (actions['revert'],   True),
+                       (actions['remove'],   False)),
+            (removed,  (actions['undelete'], True),
+                       (None,                False)),
+            (deleted,  (actions['revert'], False),
+                       (actions['remove'], False)),
             )
 
         for abs, (rel, exact) in sorted(names.items()):
+            # hash on file in target manifest (or None if missing from target)
             mfentry = mf.get(abs)
+            # target file to be touch on disk (relative to cwd)
             target = repo.wjoin(abs)
             def handle(xlist, dobackup):
                 xlist[0].append(abs)
@@ -2312,27 +2363,35 @@
                     if not isinstance(msg, basestring):
                         msg = msg(abs)
                     ui.status(msg % rel)
-            for table, hitlist, misslist, backuphit, backupmiss in disptable:
+            # search the entry in the dispatch table.
+            # if the file is in any of this sets, it was touched in the working
+            # directory parent and we are sure it needs to be reverted.
+            for table, hit, miss in disptable:
                 if abs not in table:
                     continue
                 # file has changed in dirstate
                 if mfentry:
-                    handle(hitlist, backuphit)
-                elif misslist is not None:
-                    handle(misslist, backupmiss)
+                    handle(*hit)
+                elif miss[0] is not None:
+                    handle(*miss)
                 break
             else:
+                # Not touched in current dirstate.
+
+                # file is unknown in parent, restore older version or ignore.
                 if abs not in repo.dirstate:
                     if mfentry:
-                        handle(add, True)
+                        handle(actions['add'], True)
                     elif exact:
                         ui.warn(_('file not managed: %s\n') % rel)
                     continue
-                # file has not changed in dirstate
+
+                # parent is target, no changes mean no changes
                 if node == parent:
                     if exact:
                         ui.warn(_('no changes needed to %s\n') % rel)
                     continue
+                # no change in dirstate but parent and target may differ
                 if pmf is None:
                     # only need parent manifest in this unlikely case,
                     # so do not read by default
@@ -2342,11 +2401,12 @@
                     # manifests, do nothing
                     if (pmf[abs] != mfentry or
                         pmf.flags(abs) != mf.flags(abs)):
-                        handle(revert, False)
+                        handle(actions['revert'], False)
                 else:
-                    handle(remove, False)
+                    handle(actions['remove'], False)
+
         if not opts.get('dry_run'):
-            _performrevert(repo, parents, ctx, revert, add, remove, undelete)
+            _performrevert(repo, parents, ctx, actions)
 
             if targetsubs:
                 # Revert the subrepos on the revert list
@@ -2355,8 +2415,8 @@
     finally:
         wlock.release()
 
-def _performrevert(repo, parents, ctx, revert, add, remove, undelete):
-    """function that actually perform all the action computed for revert
+def _performrevert(repo, parents, ctx, actions):
+    """function that actually perform all the actions computed for revert
 
     This is an independent function to let extension to plug in and react to
     the imminent revert.
@@ -2370,7 +2430,7 @@
         repo.wwrite(f, fc.data(), fc.flags())
 
     audit_path = pathutil.pathauditor(repo.root)
-    for f in remove[0]:
+    for f in actions['remove'][0]:
         if repo.dirstate[f] == 'a':
             repo.dirstate.drop(f)
             continue
@@ -2390,25 +2450,25 @@
             normal = repo.dirstate.normallookup
         else:
             normal = repo.dirstate.normal
-    for f in revert[0]:
+    for f in actions['revert'][0]:
         checkout(f)
         if normal:
             normal(f)
 
-    for f in add[0]:
+    for f in actions['add'][0]:
         checkout(f)
         repo.dirstate.add(f)
 
     normal = repo.dirstate.normallookup
     if node == parent and p2 == nullid:
         normal = repo.dirstate.normal
-    for f in undelete[0]:
+    for f in actions['undelete'][0]:
         checkout(f)
         normal(f)
 
     copied = copies.pathcopies(repo[parent], ctx)
 
-    for f in add[0] + undelete[0] + revert[0]:
+    for f in actions['add'][0] + actions['undelete'][0] + actions['revert'][0]:
         if f in copied:
             repo.dirstate.copy(copied[f], f)
 
--- a/mercurial/commands.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/mercurial/commands.py	Sat Jun 14 11:56:20 2014 -0500
@@ -386,6 +386,7 @@
     ('', 'parent', '',
      _('parent to choose when backing out merge (DEPRECATED)'), _('REV')),
     ('r', 'rev', '', _('revision to backout'), _('REV')),
+    ('e', 'edit', False, _('invoke editor on commit messages')),
     ] + mergetoolopts + walkopts + commitopts + commitopts2,
     _('[OPTION]... [-r] REV'))
 def backout(ui, repo, node=None, rev=None, **opts):
@@ -487,13 +488,12 @@
             cmdutil.revert(ui, repo, rctx, repo.dirstate.parents())
 
 
-        e = cmdutil.commiteditor
-        if not opts['message'] and not opts['logfile']:
-            # we don't translate commit messages
-            opts['message'] = "Backed out changeset %s" % short(node)
-            e = cmdutil.commitforceeditor
-
         def commitfunc(ui, repo, message, match, opts):
+            e = cmdutil.getcommiteditor(**opts)
+            if not message:
+                # we don't translate commit messages
+                message = "Backed out changeset %s" % short(node)
+                e = cmdutil.getcommiteditor(edit=True)
             return repo.commit(message, opts.get('user'), opts.get('date'),
                                match, editor=e)
         newnode = cmdutil.commit(ui, repo, commitfunc, [], opts)
@@ -1347,8 +1347,6 @@
 
     Returns 0 on success, 1 if nothing changed.
     """
-    forceeditor = opts.get('edit')
-
     if opts.get('subrepos'):
         if opts.get('amend'):
             raise util.Abort(_('cannot amend with --subrepos'))
@@ -1410,10 +1408,6 @@
                     bookmarks.setcurrent(repo, bm)
             newmarks.write()
     else:
-        e = cmdutil.commiteditor
-        if forceeditor:
-            e = cmdutil.commitforceeditor
-
         def commitfunc(ui, repo, message, match, opts):
             try:
                 if opts.get('secret'):
@@ -1423,7 +1417,9 @@
                                           'commit')
 
                 return repo.commit(message, opts.get('user'), opts.get('date'),
-                                   match, editor=e, extra=extra)
+                                   match,
+                                   editor=cmdutil.getcommiteditor(**opts),
+                                   extra=extra)
             finally:
                 ui.setconfig('phases', 'new-commit', oldcommitphase, 'commit')
                 repo.baseui.setconfig('phases', 'new-commit', oldcommitphase,
@@ -1686,17 +1682,17 @@
                     ml[id * linesperrev] += " r%i" % id
                     mergedtext = "\n".join(ml)
                     files.append(fn)
-                    fctxs[fn] = context.memfilectx(fn, mergedtext)
+                    fctxs[fn] = context.memfilectx(repo, fn, mergedtext)
 
                 if overwritten_file:
                     fn = "of"
                     files.append(fn)
-                    fctxs[fn] = context.memfilectx(fn, "r%i\n" % id)
+                    fctxs[fn] = context.memfilectx(repo, fn, "r%i\n" % id)
 
                 if new_file:
                     fn = "nf%i" % id
                     files.append(fn)
-                    fctxs[fn] = context.memfilectx(fn, "r%i\n" % id)
+                    fctxs[fn] = context.memfilectx(repo, fn, "r%i\n" % id)
                     if len(ps) > 1:
                         if not p2:
                             p2 = repo[ps[1]]
@@ -3077,9 +3073,7 @@
     if not opts.get('date') and opts.get('currentdate'):
         opts['date'] = "%d %d" % util.makedate()
 
-    editor = None
-    if opts.get('edit'):
-        editor = cmdutil.commitforceeditor
+    editor = cmdutil.getcommiteditor(**opts)
 
     cont = False
     if opts['continue']:
@@ -3186,7 +3180,8 @@
                     repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
                                       'graft')
                     stats = mergemod.update(repo, ctx.node(), True, True, False,
-                                            ctx.p1().node())
+                                            ctx.p1().node(),
+                                            labels=['local', 'graft'])
                 finally:
                     repo.ui.setconfig('ui', 'forcemerge', '', 'graft')
                 # report any conflicts
@@ -3692,6 +3687,8 @@
      _("don't commit, just update the working directory")),
     ('', 'bypass', None,
      _("apply patch without touching the working directory")),
+    ('', 'partial', None,
+     _('commit even if some hunks fail')),
     ('', 'exact', None,
      _('apply patch to the nodes from which it was generated')),
     ('', 'import-branch', None,
@@ -3733,6 +3730,16 @@
     With -s/--similarity, hg will attempt to discover renames and
     copies in the patch in the same way as :hg:`addremove`.
 
+    Use --partial to ensure a changeset will be created from the patch
+    even if some hunks fail to apply. Hunks that fail to apply will be
+    written to a <target-file>.rej file. Conflicts can then be resolved
+    by hand before :hg:`commit --amend` is run to update the created
+    changeset. This flag exists to let people import patches that
+    partially apply without losing the associated metadata (author,
+    date, description, ...), Note that when none of the hunk applies
+    cleanly, :hg:`import --partial` will create an empty changeset,
+    importing only the patch metadata.
+
     To read a patch from standard input, use "-" as the patch name. If
     a URL is specified, the patch will be downloaded from it.
     See :hg:`help dates` for a list of formats valid for -d/--date.
@@ -3758,7 +3765,7 @@
 
           hg import --exact proposed-fix.patch
 
-    Returns 0 on success.
+    Returns 0 on success, 1 on partial success (see --partial).
     """
 
     if not patch1:
@@ -3790,6 +3797,7 @@
     base = opts["base"]
     wlock = lock = tr = None
     msgs = []
+    ret = 0
 
 
     try:
@@ -3811,8 +3819,9 @@
 
                 haspatch = False
                 for hunk in patch.split(patchfile):
-                    (msg, node) = cmdutil.tryimportone(ui, repo, hunk, parents,
-                                                       opts, msgs, hg.clean)
+                    (msg, node, rej) = cmdutil.tryimportone(ui, repo, hunk,
+                                                            parents, opts,
+                                                            msgs, hg.clean)
                     if msg:
                         haspatch = True
                         ui.note(msg + '\n')
@@ -3820,6 +3829,12 @@
                         parents = repo.parents()
                     else:
                         parents = [repo[node]]
+                    if rej:
+                        ui.write_err(_("patch applied partially\n"))
+                        ui.write_err(("(fix the .rej files and run "
+                                      "`hg commit --amend`)\n"))
+                        ret = 1
+                        break
 
                 if not haspatch:
                     raise util.Abort(_('%s: no diffs found') % patchurl)
@@ -3828,6 +3843,7 @@
                 tr.close()
             if msgs:
                 repo.savecommitmessage('\n* * *\n'.join(msgs))
+            return ret
         except: # re-raises
             # wlock.release() indirectly calls dirstate.write(): since
             # we're crashing, we do not want to change the working dir
@@ -4930,46 +4946,66 @@
     wlock = repo.wlock()
     try:
         ms = mergemod.mergestate(repo)
+
+        if not ms.active() and not show:
+            raise util.Abort(
+                _('resolve command not applicable when not merging'))
+
         m = scmutil.match(repo[None], pats, opts)
         ret = 0
+        didwork = False
 
         for f in ms:
-            if m(f):
-                if show:
-                    if nostatus:
-                        ui.write("%s\n" % f)
-                    else:
-                        ui.write("%s %s\n" % (ms[f].upper(), f),
-                                 label='resolve.' +
-                                 {'u': 'unresolved', 'r': 'resolved'}[ms[f]])
-                elif mark:
-                    ms.mark(f, "r")
-                elif unmark:
-                    ms.mark(f, "u")
+            if not m(f):
+                continue
+
+            didwork = True
+
+            if show:
+                if nostatus:
+                    ui.write("%s\n" % f)
                 else:
-                    wctx = repo[None]
-
-                    # backup pre-resolve (merge uses .orig for its own purposes)
-                    a = repo.wjoin(f)
-                    util.copyfile(a, a + ".resolve")
-
-                    try:
-                        # resolve file
-                        ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
-                                     'resolve')
-                        if ms.resolve(f, wctx):
-                            ret = 1
-                    finally:
-                        ui.setconfig('ui', 'forcemerge', '', 'resolve')
-                        ms.commit()
-
-                    # replace filemerge's .orig file with our resolve file
-                    util.rename(a + ".resolve", a + ".orig")
+                    ui.write("%s %s\n" % (ms[f].upper(), f),
+                             label='resolve.' +
+                             {'u': 'unresolved', 'r': 'resolved'}[ms[f]])
+            elif mark:
+                ms.mark(f, "r")
+            elif unmark:
+                ms.mark(f, "u")
+            else:
+                wctx = repo[None]
+
+                # backup pre-resolve (merge uses .orig for its own purposes)
+                a = repo.wjoin(f)
+                util.copyfile(a, a + ".resolve")
+
+                try:
+                    # resolve file
+                    ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
+                                 'resolve')
+                    if ms.resolve(f, wctx):
+                        ret = 1
+                finally:
+                    ui.setconfig('ui', 'forcemerge', '', 'resolve')
+                    ms.commit()
+
+                # replace filemerge's .orig file with our resolve file
+                util.rename(a + ".resolve", a + ".orig")
 
         ms.commit()
+
+        if not didwork and pats:
+            ui.warn(_("arguments do not match paths that need resolving\n"))
+
     finally:
         wlock.release()
 
+    # Nudge users into finishing an unfinished operation. We don't print
+    # this with the list/show operation because we want list/show to remain
+    # machine readable.
+    if not list(ms.unresolved()) and not show:
+        ui.status(_('no more unresolved files\n'))
+
     return ret
 
 @command('revert',
@@ -5689,16 +5725,15 @@
         if date:
             date = util.parsedate(date)
 
-        if opts.get('edit'):
-            message = ui.edit(message, ui.username())
-            repo.savecommitmessage(message)
+        editor = cmdutil.getcommiteditor(**opts)
 
         # don't allow tagging the null rev
         if (not opts.get('remove') and
             scmutil.revsingle(repo, rev_).rev() == nullrev):
             raise util.Abort(_("cannot tag null revision"))
 
-        repo.tag(names, r, message, opts.get('local'), opts.get('user'), date)
+        repo.tag(names, r, message, opts.get('local'), opts.get('user'), date,
+                 editor=editor)
     finally:
         release(lock, wlock)
 
@@ -5791,9 +5826,11 @@
     ('c', 'check', None,
      _('update across branches if no uncommitted changes')),
     ('d', 'date', '', _('tipmost revision matching date'), _('DATE')),
-    ('r', 'rev', '', _('revision'), _('REV'))],
+    ('r', 'rev', '', _('revision'), _('REV'))
+     ] + mergetoolopts,
     _('[-c] [-C] [-d DATE] [[-r] REV]'))
-def update(ui, repo, node=None, rev=None, clean=False, date=None, check=False):
+def update(ui, repo, node=None, rev=None, clean=False, date=None, check=False,
+           tool=None):
     """update working directory (or switch revisions)
 
     Update the repository's working directory to the specified
@@ -5874,6 +5911,8 @@
             rev = repo[repo[None].branch()].rev()
         mergemod._checkunknown(repo, repo[None], repo[rev])
 
+    repo.ui.setconfig('ui', 'forcemerge', tool, 'update')
+
     if clean:
         ret = hg.clean(repo, rev)
     else:
@@ -5884,7 +5923,11 @@
             ui.status(_("updating bookmark %s\n") % repo._bookmarkcurrent)
     elif brev in repo._bookmarks:
         bookmarks.setcurrent(repo, brev)
+        ui.status(_("(activating bookmark %s)\n") % brev)
     elif brev:
+        if repo._bookmarkcurrent:
+            ui.status(_("(leaving bookmark %s)\n") %
+                      repo._bookmarkcurrent)
         bookmarks.unsetcurrent(repo)
 
     return ret
--- a/mercurial/context.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/mercurial/context.py	Sat Jun 14 11:56:20 2014 -0500
@@ -63,10 +63,81 @@
         for f in sorted(self._manifest):
             yield f
 
+    def _manifestmatches(self, match, s):
+        """generate a new manifest filtered by the match argument
+
+        This method is for internal use only and mainly exists to provide an
+        object oriented way for other contexts to customize the manifest
+        generation.
+        """
+        mf = self.manifest().copy()
+        if match.always():
+            return mf
+        for fn in mf.keys():
+            if not match(fn):
+                del mf[fn]
+        return mf
+
+    def _matchstatus(self, other, s, match, listignored, listclean,
+                     listunknown):
+        """return match.always if match is none
+
+        This internal method provides a way for child objects to override the
+        match operator.
+        """
+        return match or matchmod.always(self._repo.root, self._repo.getcwd())
+
+    def _prestatus(self, other, s, match, listignored, listclean, listunknown):
+        """provide a hook to allow child objects to preprocess status results
+
+        For example, this allows other contexts, such as workingctx, to query
+        the dirstate before comparing the manifests.
+        """
+        # load earliest manifest first for caching reasons
+        if self.rev() < other.rev():
+            self.manifest()
+        return s
+
+    def _poststatus(self, other, s, match, listignored, listclean, listunknown):
+        """provide a hook to allow child objects to postprocess status results
+
+        For example, this allows other contexts, such as workingctx, to filter
+        suspect symlinks in the case of FAT32 and NTFS filesytems.
+        """
+        return s
+
+    def _buildstatus(self, other, s, match, listignored, listclean,
+                     listunknown):
+        """build a status with respect to another context"""
+        mf1 = other._manifestmatches(match, s)
+        mf2 = self._manifestmatches(match, s)
+
+        modified, added, clean = [], [], []
+        deleted, unknown, ignored = s[3], [], []
+        withflags = mf1.withflags() | mf2.withflags()
+        for fn, mf2node in mf2.iteritems():
+            if fn in mf1:
+                if (fn not in deleted and
+                    ((fn in withflags and mf1.flags(fn) != mf2.flags(fn)) or
+                     (mf1[fn] != mf2node and
+                      (mf2node or self[fn].cmp(other[fn]))))):
+                    modified.append(fn)
+                elif listclean:
+                    clean.append(fn)
+                del mf1[fn]
+            elif fn not in deleted:
+                added.append(fn)
+        removed = mf1.keys()
+
+        return [modified, added, removed, deleted, unknown, ignored, clean]
+
     @propertycache
     def substate(self):
         return subrepo.state(self, self._repo.ui)
 
+    def subrev(self, subpath):
+        return self.substate[subpath][1]
+
     def rev(self):
         return self._rev
     def node(self):
@@ -198,19 +269,81 @@
     def dirty(self):
         return False
 
+    def status(self, other=None, match=None, listignored=False,
+               listclean=False, listunknown=False, listsubrepos=False):
+        """return status of files between two nodes or node and working
+        directory.
+
+        If other is None, compare this node with working directory.
+
+        returns (modified, added, removed, deleted, unknown, ignored, clean)
+        """
+
+        ctx1 = self
+        ctx2 = self._repo[other]
+
+        # This next code block is, admittedly, fragile logic that tests for
+        # reversing the contexts and wouldn't need to exist if it weren't for
+        # the fast (and common) code path of comparing the working directory
+        # with its first parent.
+        #
+        # What we're aiming for here is the ability to call:
+        #
+        # workingctx.status(parentctx)
+        #
+        # If we always built the manifest for each context and compared those,
+        # then we'd be done. But the special case of the above call means we
+        # just copy the manifest of the parent.
+        reversed = False
+        if (not isinstance(ctx1, changectx)
+            and isinstance(ctx2, changectx)):
+            reversed = True
+            ctx1, ctx2 = ctx2, ctx1
+
+        r = [[], [], [], [], [], [], []]
+        match = ctx2._matchstatus(ctx1, r, match, listignored, listclean,
+                                  listunknown)
+        r = ctx2._prestatus(ctx1, r, match, listignored, listclean, listunknown)
+        r = ctx2._buildstatus(ctx1, r, match, listignored, listclean,
+                              listunknown)
+        r = ctx2._poststatus(ctx1, r, match, listignored, listclean,
+                             listunknown)
+
+        if reversed:
+            r[1], r[2], r[3], r[4] = r[2], r[1], r[4], r[3]
+
+        if listsubrepos:
+            for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
+                rev2 = ctx2.subrev(subpath)
+                try:
+                    submatch = matchmod.narrowmatcher(subpath, match)
+                    s = sub.status(rev2, match=submatch, ignored=listignored,
+                                   clean=listclean, unknown=listunknown,
+                                   listsubrepos=True)
+                    for rfiles, sfiles in zip(r, s):
+                        rfiles.extend("%s/%s" % (subpath, f) for f in sfiles)
+                except error.LookupError:
+                    self._repo.ui.status(_("skipping missing "
+                                           "subrepository: %s\n") % subpath)
+
+        for l in r:
+            l.sort()
+
+        # we return a tuple to signify that this list isn't changing
+        return tuple(r)
+
+
 def makememctx(repo, parents, text, user, date, branch, files, store,
                editor=None):
     def getfilectx(repo, memctx, path):
         data, (islink, isexec), copied = store.getfile(path)
-        return memfilectx(path, data, islink=islink, isexec=isexec,
-                                  copied=copied)
+        return memfilectx(repo, path, data, islink=islink, isexec=isexec,
+                                  copied=copied, memctx=memctx)
     extra = {}
     if branch:
         extra['branch'] = encoding.fromlocal(branch)
     ctx =  memctx(repo, parents, text, files, getfilectx, user,
-                          date, extra)
-    if editor:
-        ctx._text = editor(repo, ctx, [])
+                          date, extra, editor)
     return ctx
 
 class changectx(basectx):
@@ -823,14 +956,7 @@
         if user:
             self._user = user
         if changes:
-            self._status = list(changes[:4])
-            self._unknown = changes[4]
-            self._ignored = changes[5]
-            self._clean = changes[6]
-        else:
-            self._unknown = None
-            self._ignored = None
-            self._clean = None
+            self._status = changes
 
         self._extra = {}
         if extra:
@@ -891,7 +1017,7 @@
 
     @propertycache
     def _manifest(self):
-        """generate a manifest corresponding to the working directory"""
+        """generate a manifest corresponding to the values in self._status"""
 
         man = self._parents[0].manifest().copy()
         if len(self._parents) > 1:
@@ -905,7 +1031,7 @@
 
         copied = self._repo.dirstate.copies()
         ff = self._flagfunc
-        modified, added, removed, deleted = self._status
+        modified, added, removed, deleted = self._status[:4]
         for i, l in (("a", added), ("m", modified)):
             for f in l:
                 orig = copied.get(f, f)
@@ -923,7 +1049,7 @@
 
     @propertycache
     def _status(self):
-        return self._repo.status()[:4]
+        return self._repo.status()
 
     @propertycache
     def _user(self):
@@ -933,21 +1059,8 @@
     def _date(self):
         return util.makedate()
 
-    def status(self, ignored=False, clean=False, unknown=False):
-        """Explicit status query
-        Unless this method is used to query the working copy status, the
-        _status property will implicitly read the status using its default
-        arguments."""
-        stat = self._repo.status(ignored=ignored, clean=clean, unknown=unknown)
-        self._unknown = self._ignored = self._clean = None
-        if unknown:
-            self._unknown = stat[4]
-        if ignored:
-            self._ignored = stat[5]
-        if clean:
-            self._clean = stat[6]
-        self._status = stat[:4]
-        return stat
+    def subrev(self, subpath):
+        return None
 
     def user(self):
         return self._user or self._repo.ui.username()
@@ -967,14 +1080,11 @@
     def deleted(self):
         return self._status[3]
     def unknown(self):
-        assert self._unknown is not None  # must call status first
-        return self._unknown
+        return self._status[4]
     def ignored(self):
-        assert self._ignored is not None  # must call status first
-        return self._ignored
+        return self._status[5]
     def clean(self):
-        assert self._clean is not None  # must call status first
-        return self._clean
+        return self._status[6]
     def branch(self):
         return encoding.tolocal(self._extra['branch'])
     def closesbranch(self):
@@ -1180,6 +1290,170 @@
             finally:
                 wlock.release()
 
+    def _filtersuspectsymlink(self, files):
+        if not files or self._repo.dirstate._checklink:
+            return files
+
+        # Symlink placeholders may get non-symlink-like contents
+        # via user error or dereferencing by NFS or Samba servers,
+        # so we filter out any placeholders that don't look like a
+        # symlink
+        sane = []
+        for f in files:
+            if self.flags(f) == 'l':
+                d = self[f].data()
+                if d == '' or len(d) >= 1024 or '\n' in d or util.binary(d):
+                    self._repo.ui.debug('ignoring suspect symlink placeholder'
+                                        ' "%s"\n' % f)
+                    continue
+            sane.append(f)
+        return sane
+
+    def _checklookup(self, files):
+        # check for any possibly clean files
+        if not files:
+            return [], []
+
+        modified = []
+        fixup = []
+        pctx = self._parents[0]
+        # do a full compare of any files that might have changed
+        for f in sorted(files):
+            if (f not in pctx or self.flags(f) != pctx.flags(f)
+                or pctx[f].cmp(self[f])):
+                modified.append(f)
+            else:
+                fixup.append(f)
+
+        # update dirstate for files that are actually clean
+        if fixup:
+            try:
+                # updating the dirstate is optional
+                # so we don't wait on the lock
+                normal = self._repo.dirstate.normal
+                wlock = self._repo.wlock(False)
+                try:
+                    for f in fixup:
+                        normal(f)
+                finally:
+                    wlock.release()
+            except error.LockError:
+                pass
+        return modified, fixup
+
+    def _manifestmatches(self, match, s):
+        """Slow path for workingctx
+
+        The fast path is when we compare the working directory to its parent
+        which means this function is comparing with a non-parent; therefore we
+        need to build a manifest and return what matches.
+        """
+        mf = self._repo['.']._manifestmatches(match, s)
+        modified, added, removed = s[0:3]
+        for f in modified + added:
+            mf[f] = None
+            mf.set(f, self.flags(f))
+        for f in removed:
+            if f in mf:
+                del mf[f]
+        return mf
+
+    def _prestatus(self, other, s, match, listignored, listclean, listunknown):
+        """override the parent hook with a dirstate query
+
+        We use this prestatus hook to populate the status with information from
+        the dirstate.
+        """
+        # doesn't need to call super; if that changes, be aware that super
+        # calls self.manifest which would slow down the common case of calling
+        # status against a workingctx's parent
+        return self._dirstatestatus(match, listignored, listclean, listunknown)
+
+    def _poststatus(self, other, s, match, listignored, listclean, listunknown):
+        """override the parent hook with a filter for suspect symlinks
+
+        We use this poststatus hook to filter out symlinks that might have
+        accidentally ended up with the entire contents of the file they are
+        susposed to be linking to.
+        """
+        s[0] = self._filtersuspectsymlink(s[0])
+        self._status = s[:]
+        return s
+
+    def _dirstatestatus(self, match=None, ignored=False, clean=False,
+                        unknown=False):
+        '''Gets the status from the dirstate -- internal use only.'''
+        listignored, listclean, listunknown = ignored, clean, unknown
+        match = match or matchmod.always(self._repo.root, self._repo.getcwd())
+        subrepos = []
+        if '.hgsub' in self:
+            subrepos = sorted(self.substate)
+        s = self._repo.dirstate.status(match, subrepos, listignored,
+                                       listclean, listunknown)
+        cmp, modified, added, removed, deleted, unknown, ignored, clean = s
+
+        # check for any possibly clean files
+        if cmp:
+            modified2, fixup = self._checklookup(cmp)
+            modified += modified2
+
+            # update dirstate for files that are actually clean
+            if fixup and listclean:
+                clean += fixup
+
+        return [modified, added, removed, deleted, unknown, ignored, clean]
+
+    def _buildstatus(self, other, s, match, listignored, listclean,
+                     listunknown):
+        """build a status with respect to another context
+
+        This includes logic for maintaining the fast path of status when
+        comparing the working directory against its parent, which is to skip
+        building a new manifest if self (working directory) is not comparing
+        against its parent (repo['.']).
+        """
+        if other != self._repo['.']:
+            s = super(workingctx, self)._buildstatus(other, s, match,
+                                                     listignored, listclean,
+                                                     listunknown)
+        return s
+
+    def _matchstatus(self, other, s, match, listignored, listclean,
+                     listunknown):
+        """override the match method with a filter for directory patterns
+
+        We use inheritance to customize the match.bad method only in cases of
+        workingctx since it belongs only to the working directory when
+        comparing against the parent changeset.
+
+        If we aren't comparing against the working directory's parent, then we
+        just use the default match object sent to us.
+        """
+        superself = super(workingctx, self)
+        match = superself._matchstatus(other, s, match, listignored, listclean,
+                                       listunknown)
+        if other != self._repo['.']:
+            def bad(f, msg):
+                # 'f' may be a directory pattern from 'match.files()',
+                # so 'f not in ctx1' is not enough
+                if f not in other and f not in other.dirs():
+                    self._repo.ui.warn('%s: %s\n' %
+                                       (self._repo.dirstate.pathto(f), msg))
+            match.bad = bad
+        return match
+
+    def status(self, other='.', match=None, listignored=False,
+               listclean=False, listunknown=False, listsubrepos=False):
+        # yet to be determined: what to do if 'other' is a 'workingctx' or a
+        # 'memctx'?
+        s = super(workingctx, self).status(other, match, listignored, listclean,
+                                           listunknown, listsubrepos)
+        # calling 'super' subtly reveresed the contexts, so we flip the results
+        # (s[1] is 'added' and s[2] is 'removed')
+        s = list(s)
+        s[1], s[2] = s[2], s[1]
+        return tuple(s)
+
 class committablefilectx(basefilectx):
     """A committablefilectx provides common functionality for a file context
     that wants the ability to commit, e.g. workingfilectx or memfilectx."""
@@ -1259,7 +1533,7 @@
         # invert comparison to reuse the same code path
         return fctx.cmp(self)
 
-class memctx(object):
+class memctx(committablectx):
     """Use memctx to perform in-memory commits via localrepo.commitctx().
 
     Revision information is supplied at initialization time while
@@ -1287,13 +1561,10 @@
     is a dictionary of metadata or is left empty.
     """
     def __init__(self, repo, parents, text, files, filectxfn, user=None,
-                 date=None, extra=None):
-        self._repo = repo
+                 date=None, extra=None, editor=False):
+        super(memctx, self).__init__(repo, text, user, date, extra)
         self._rev = None
         self._node = None
-        self._text = text
-        self._date = date and util.parsedate(date) or util.makedate()
-        self._user = user
         parents = [(p or nullid) for p in parents]
         p1, p2 = parents
         self._parents = [changectx(self._repo, p) for p in (p1, p2)]
@@ -1305,55 +1576,9 @@
         if self._extra.get('branch', '') == '':
             self._extra['branch'] = 'default'
 
-    def __str__(self):
-        return str(self._parents[0]) + "+"
-
-    def __int__(self):
-        return self._rev
-
-    def __nonzero__(self):
-        return True
-
-    def __getitem__(self, key):
-        return self.filectx(key)
-
-    def p1(self):
-        return self._parents[0]
-    def p2(self):
-        return self._parents[1]
-
-    def user(self):
-        return self._user or self._repo.ui.username()
-    def date(self):
-        return self._date
-    def description(self):
-        return self._text
-    def files(self):
-        return self.modified()
-    def modified(self):
-        return self._status[0]
-    def added(self):
-        return self._status[1]
-    def removed(self):
-        return self._status[2]
-    def deleted(self):
-        return self._status[3]
-    def unknown(self):
-        return self._status[4]
-    def ignored(self):
-        return self._status[5]
-    def clean(self):
-        return self._status[6]
-    def branch(self):
-        return encoding.tolocal(self._extra['branch'])
-    def extra(self):
-        return self._extra
-    def flags(self, f):
-        return self[f].flags()
-
-    def parents(self):
-        """return contexts for each parent changeset"""
-        return self._parents
+        if editor:
+            self._text = editor(self._repo, self, [])
+            self._repo.savecommitmessage(self._text)
 
     def filectx(self, path, filelog=None):
         """get a file context from the working directory"""
@@ -1363,12 +1588,13 @@
         """commit context to the repo"""
         return self._repo.commitctx(self)
 
-class memfilectx(object):
+class memfilectx(committablefilectx):
     """memfilectx represents an in-memory file to commit.
 
-    See memctx for more details.
+    See memctx and commitablefilectx for more details.
     """
-    def __init__(self, path, data, islink=False, isexec=False, copied=None):
+    def __init__(self, repo, path, data, islink=False,
+                 isexec=False, copied=None, memctx=None):
         """
         path is the normalized file path relative to repository root.
         data is the file content as a string.
@@ -1376,21 +1602,17 @@
         isexec is True if the file is executable.
         copied is the source file path if current file was copied in the
         revision being committed, or None."""
-        self._path = path
+        super(memfilectx, self).__init__(repo, path, None, memctx)
         self._data = data
         self._flags = (islink and 'l' or '') + (isexec and 'x' or '')
         self._copied = None
         if copied:
             self._copied = (copied, nullid)
 
-    def __nonzero__(self):
-        return True
-    def __str__(self):
-        return "%s@%s" % (self.path(), self._changectx)
-    def path(self):
-        return self._path
     def data(self):
         return self._data
+    def size(self):
+        return len(self.data())
     def flags(self):
         return self._flags
     def isexec(self):
--- a/mercurial/demandimport.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/mercurial/demandimport.py	Sat Jun 14 11:56:20 2014 -0500
@@ -24,13 +24,17 @@
   b = __import__(a)
 '''
 
-import __builtin__, os
+import __builtin__, os, sys
 _origimport = __import__
 
 nothing = object()
 
 try:
-    _origimport(__builtin__.__name__, {}, {}, None, -1)
+    # Python 3 doesn't have relative imports nor level -1.
+    level = -1
+    if sys.version_info[0] >= 3:
+        level = 0
+    _origimport(__builtin__.__name__, {}, {}, None, level)
 except TypeError: # no level argument
     def _import(name, globals, locals, fromlist, level):
         "call _origimport with no level argument"
@@ -55,7 +59,7 @@
 
 class _demandmod(object):
     """module demand-loader and proxy"""
-    def __init__(self, name, globals, locals, level=-1):
+    def __init__(self, name, globals, locals, level=level):
         if '.' in name:
             head, rest = name.split('.', 1)
             after = [rest]
@@ -105,7 +109,7 @@
         self._load()
         setattr(self._module, attr, val)
 
-def _demandimport(name, globals=None, locals=None, fromlist=None, level=-1):
+def _demandimport(name, globals=None, locals=None, fromlist=None, level=level):
     if not locals or name in ignore or fromlist == ('*',):
         # these cases we can't really delay
         return _hgextimport(_import, name, globals, locals, fromlist, level)
--- a/mercurial/discovery.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/mercurial/discovery.py	Sat Jun 14 11:56:20 2014 -0500
@@ -341,6 +341,10 @@
                 if branch not in ('default', None):
                     error = _("push creates new remote head %s "
                               "on branch '%s'!") % (short(dhs[0]), branch)
+                elif repo[dhs[0]].bookmarks():
+                    error = _("push creates new remote head %s "
+                              "with bookmark '%s'!") % (
+                              short(dhs[0]), repo[dhs[0]].bookmarks()[0])
                 else:
                     error = _("push creates new remote head %s!"
                               ) % short(dhs[0])
--- a/mercurial/error.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/mercurial/error.py	Sat Jun 14 11:56:20 2014 -0500
@@ -98,3 +98,22 @@
 class PushRaced(RuntimeError):
     """An exception raised during unbundling that indicate a push race"""
 
+# bundle2 related errors
+class BundleValueError(ValueError):
+    """error raised when bundle2 cannot be processed"""
+
+    def __init__(self, parttype=None, params=()):
+        self.parttype = parttype
+        self.params = params
+        if self.parttype is None:
+            msg = 'Stream Parameter'
+        else:
+            msg = parttype
+        if self.params:
+            msg = '%s - %s' % (msg, ', '.join(self.params))
+        ValueError.__init__(self, msg)
+
+class ReadOnlyPartError(RuntimeError):
+    """error raised when code tries to alter a part being generated"""
+    pass
+
--- a/mercurial/exchange.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/mercurial/exchange.py	Sat Jun 14 11:56:20 2014 -0500
@@ -9,7 +9,7 @@
 from node import hex, nullid
 import errno, urllib
 import util, scmutil, changegroup, base85, error
-import discovery, phases, obsolete, bookmarks, bundle2
+import discovery, phases, obsolete, bookmarks, bundle2, pushkey
 
 def readbundle(ui, fh, fname, vfs=None):
     header = changegroup.readexactly(fh, 4)
@@ -208,30 +208,25 @@
 
     The only currently supported type of data is changegroup but this will
     evolve in the future."""
-    # Send known head to the server for race detection.
-    capsblob = urllib.unquote(pushop.remote.capable('bundle2-exp'))
-    caps = bundle2.decodecaps(capsblob)
-    bundler = bundle2.bundle20(pushop.ui, caps)
+    bundler = bundle2.bundle20(pushop.ui, bundle2.bundle2caps(pushop.remote))
     # create reply capability
     capsblob = bundle2.encodecaps(pushop.repo.bundle2caps)
-    bundler.addpart(bundle2.bundlepart('b2x:replycaps', data=capsblob))
+    bundler.newpart('b2x:replycaps', data=capsblob)
+    # Send known heads to the server for race detection.
     if not pushop.force:
-        part = bundle2.bundlepart('B2X:CHECK:HEADS',
-                                  data=iter(pushop.remoteheads))
-        bundler.addpart(part)
+        bundler.newpart('B2X:CHECK:HEADS', data=iter(pushop.remoteheads))
     extrainfo = _pushbundle2extraparts(pushop, bundler)
     # add the changegroup bundle
     cg = changegroup.getlocalbundle(pushop.repo, 'push', pushop.outgoing)
-    cgpart = bundle2.bundlepart('B2X:CHANGEGROUP', data=cg.getchunks())
-    bundler.addpart(cgpart)
+    cgpart = bundler.newpart('B2X:CHANGEGROUP', data=cg.getchunks())
     stream = util.chunkbuffer(bundler.getchunks())
     try:
         reply = pushop.remote.unbundle(stream, ['force'], 'push')
-    except bundle2.UnknownPartError, exc:
+    except error.BundleValueError, exc:
         raise util.Abort('missing support for %s' % exc)
     try:
         op = bundle2.processbundle(pushop.repo, reply)
-    except bundle2.UnknownPartError, exc:
+    except error.BundleValueError, exc:
         raise util.Abort('missing support for %s' % exc)
     cgreplies = op.records.getreplies(cgpart.id)
     assert len(cgreplies['changegroup']) == 1
@@ -330,37 +325,6 @@
     """synchronise phase information locally and remotely"""
     unfi = pushop.repo.unfiltered()
     cheads = pushop.commonheads
-    if pushop.ret:
-        # push succeed, synchronize target of the push
-        cheads = pushop.outgoing.missingheads
-    elif pushop.revs is None:
-        # All out push fails. synchronize all common
-        cheads = pushop.outgoing.commonheads
-    else:
-        # I want cheads = heads(::missingheads and ::commonheads)
-        # (missingheads is revs with secret changeset filtered out)
-        #
-        # This can be expressed as:
-        #     cheads = ( (missingheads and ::commonheads)
-        #              + (commonheads and ::missingheads))"
-        #              )
-        #
-        # while trying to push we already computed the following:
-        #     common = (::commonheads)
-        #     missing = ((commonheads::missingheads) - commonheads)
-        #
-        # We can pick:
-        # * missingheads part of common (::commonheads)
-        common = set(pushop.outgoing.common)
-        nm = pushop.repo.changelog.nodemap
-        cheads = [node for node in pushop.revs if nm[node] in common]
-        # and
-        # * commonheads parents on missing
-        revset = unfi.set('%ln and parents(roots(%ln))',
-                         pushop.outgoing.commonheads,
-                         pushop.outgoing.missing)
-        cheads.extend(c.node() for c in revset)
-    pushop.commonheads = cheads
     # even when we don't push, exchanging phase data is useful
     remotephases = pushop.remote.listkeys('phases')
     if (pushop.ui.configbool('ui', '_usedassubrepo', False)
@@ -395,16 +359,54 @@
         # Get the list of all revs draft on remote by public here.
         # XXX Beware that revset break if droots is not strictly
         # XXX root we may want to ensure it is but it is costly
-        outdated =  unfi.set('heads((%ln::%ln) and public())',
-                             droots, cheads)
-        for newremotehead in outdated:
-            r = pushop.remote.pushkey('phases',
-                                      newremotehead.hex(),
-                                      str(phases.draft),
-                                      str(phases.public))
-            if not r:
-                pushop.ui.warn(_('updating %s to public failed!\n')
-                                       % newremotehead)
+        outdated = unfi.set('heads((%ln::%ln) and public())',
+                            droots, cheads)
+
+        b2caps = bundle2.bundle2caps(pushop.remote)
+        if 'b2x:pushkey' in b2caps:
+            # server supports bundle2, let's do a batched push through it
+            #
+            # This will eventually be unified with the changesets bundle2 push
+            bundler = bundle2.bundle20(pushop.ui, b2caps)
+            capsblob = bundle2.encodecaps(pushop.repo.bundle2caps)
+            bundler.newpart('b2x:replycaps', data=capsblob)
+            part2node = []
+            enc = pushkey.encode
+            for newremotehead in outdated:
+                part = bundler.newpart('b2x:pushkey')
+                part.addparam('namespace', enc('phases'))
+                part.addparam('key', enc(newremotehead.hex()))
+                part.addparam('old', enc(str(phases.draft)))
+                part.addparam('new', enc(str(phases.public)))
+                part2node.append((part.id, newremotehead))
+            stream = util.chunkbuffer(bundler.getchunks())
+            try:
+                reply = pushop.remote.unbundle(stream, ['force'], 'push')
+                op = bundle2.processbundle(pushop.repo, reply)
+            except error.BundleValueError, exc:
+                raise util.Abort('missing support for %s' % exc)
+            for partid, node in part2node:
+                partrep = op.records.getreplies(partid)
+                results = partrep['pushkey']
+                assert len(results) <= 1
+                msg = None
+                if not results:
+                    msg = _('server ignored update of %s to public!\n') % node
+                elif not int(results[0]['return']):
+                    msg = _('updating %s to public failed!\n') % node
+                if msg is not None:
+                    pushop.ui.warn(msg)
+
+        else:
+            # fallback to independant pushkey command
+            for newremotehead in outdated:
+                r = pushop.remote.pushkey('phases',
+                                          newremotehead.hex(),
+                                          str(phases.draft),
+                                          str(phases.public))
+                if not r:
+                    pushop.ui.warn(_('updating %s to public failed!\n')
+                                   % newremotehead)
 
 def _localphasemove(pushop, nodes, phase=phases.public):
     """move <nodes> to <phase> in the local source repo"""
@@ -568,14 +570,15 @@
     """pull data using bundle2
 
     For now, the only supported data are changegroup."""
-    kwargs = {'bundlecaps': set(['HG2X'])}
-    capsblob = bundle2.encodecaps(pullop.repo.bundle2caps)
-    kwargs['bundlecaps'].add('bundle2=' + urllib.quote(capsblob))
+    remotecaps = bundle2.bundle2caps(pullop.remote)
+    kwargs = {'bundlecaps': caps20to10(pullop.repo)}
     # pulling changegroup
     pullop.todosteps.remove('changegroup')
 
     kwargs['common'] = pullop.common
     kwargs['heads'] = pullop.heads or pullop.rheads
+    if 'b2x:listkeys' in remotecaps:
+        kwargs['listkeys'] = ['phase']
     if not pullop.fetch:
         pullop.repo.ui.status(_("no changes found\n"))
         pullop.cgresult = 0
@@ -588,13 +591,18 @@
     bundle = pullop.remote.getbundle('pull', **kwargs)
     try:
         op = bundle2.processbundle(pullop.repo, bundle, pullop.gettransaction)
-    except bundle2.UnknownPartError, exc:
+    except error.BundleValueError, exc:
         raise util.Abort('missing support for %s' % exc)
 
     if pullop.fetch:
         assert len(op.records['changegroup']) == 1
         pullop.cgresult = op.records['changegroup'][0]['return']
 
+    # processing phases change
+    for namespace, value in op.records['listkeys']:
+        if namespace == 'phases':
+            _pullapplyphases(pullop, value)
+
 def _pullbundle2extraprepare(pullop, kwargs):
     """hook function so that extensions can extend the getbundle call"""
     pass
@@ -624,8 +632,8 @@
         cg = pullop.remote.changegroup(pullop.fetch, 'pull')
     elif not pullop.remote.capable('changegroupsubset'):
         raise util.Abort(_("partial pull cannot be done because "
-                                   "other repository doesn't support "
-                                   "changegroupsubset."))
+                           "other repository doesn't support "
+                           "changegroupsubset."))
     else:
         cg = pullop.remote.changegroupsubset(pullop.fetch, pullop.heads, 'pull')
     pullop.cgresult = changegroup.addchangegroup(pullop.repo, cg, 'pull',
@@ -633,8 +641,12 @@
 
 def _pullphase(pullop):
     # Get remote phases data from remote
+    remotephases = pullop.remote.listkeys('phases')
+    _pullapplyphases(pullop, remotephases)
+
+def _pullapplyphases(pullop, remotephases):
+    """apply phase movement from observed remote state"""
     pullop.todosteps.remove('phases')
-    remotephases = pullop.remote.listkeys('phases')
     publishing = bool(remotephases.get('publishing', False))
     if remotephases and not publishing:
         # remote is new and unpublishing
@@ -672,6 +684,13 @@
             pullop.repo.invalidatevolatilesets()
     return tr
 
+def caps20to10(repo):
+    """return a set with appropriate options to use bundle20 during getbundle"""
+    caps = set(['HG2X'])
+    capsblob = bundle2.encodecaps(repo.bundle2caps)
+    caps.add('bundle2=' + urllib.quote(capsblob))
+    return caps
+
 def getbundle(repo, source, heads=None, common=None, bundlecaps=None,
               **kwargs):
     """return a full bundle (with potentially multiple kind of parts)
@@ -691,6 +710,9 @@
     cg = changegroup.getbundle(repo, source, heads=heads,
                                common=common, bundlecaps=bundlecaps)
     if bundlecaps is None or 'HG2X' not in bundlecaps:
+        if kwargs:
+            raise ValueError(_('unsupported getbundle arguments: %s')
+                             % ', '.join(sorted(kwargs.keys())))
         return cg
     # very crude first implementation,
     # the bundle API will change and the generation will be done lazily.
@@ -701,8 +723,13 @@
             b2caps.update(bundle2.decodecaps(blob))
     bundler = bundle2.bundle20(repo.ui, b2caps)
     if cg:
-        part = bundle2.bundlepart('b2x:changegroup', data=cg.getchunks())
-        bundler.addpart(part)
+        bundler.newpart('b2x:changegroup', data=cg.getchunks())
+    listkeys = kwargs.get('listkeys', ())
+    for namespace in listkeys:
+        part = bundler.newpart('b2x:listkeys')
+        part.addparam('namespace', namespace)
+        keys = repo.listkeys(namespace).items()
+        part.data = pushkey.encodekeys(keys)
     _getbundleextrapart(bundler, repo, source, heads=heads, common=common,
                         bundlecaps=bundlecaps, **kwargs)
     return util.chunkbuffer(bundler.getchunks())
--- a/mercurial/filemerge.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/mercurial/filemerge.py	Sat Jun 14 11:56:20 2014 -0500
@@ -7,7 +7,7 @@
 
 from node import short
 from i18n import _
-import util, simplemerge, match, error
+import util, simplemerge, match, error, templater, templatekw
 import os, tempfile, re, filecmp
 
 def _toolstr(ui, tool, part, default=""):
@@ -169,7 +169,7 @@
     used to resolve these conflicts."""
     return 1
 
-def _premerge(repo, toolconf, files):
+def _premerge(repo, toolconf, files, labels=None):
     tool, toolpath, binary, symlink = toolconf
     if symlink:
         return 1
@@ -190,7 +190,7 @@
                                     (tool, premerge, _valid))
 
     if premerge:
-        r = simplemerge.simplemerge(ui, a, b, c, quiet=True)
+        r = simplemerge.simplemerge(ui, a, b, c, quiet=True, label=labels)
         if not r:
             ui.debug(" premerge successful\n")
             return 0
@@ -201,7 +201,7 @@
 @internaltool('merge', True,
               _("merging %s incomplete! "
                 "(edit conflicts, then use 'hg resolve --mark')\n"))
-def _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files):
+def _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
     """
     Uses the internal non-interactive simple merge algorithm for merging
     files. It will fail if there are any conflicts and leave markers in
@@ -211,19 +211,18 @@
         repo.ui.warn(_('warning: internal:merge cannot merge symlinks '
                        'for %s\n') % fcd.path())
         return False, 1
-
-    r = _premerge(repo, toolconf, files)
+    r = _premerge(repo, toolconf, files, labels=labels)
     if r:
         a, b, c, back = files
 
         ui = repo.ui
 
-        r = simplemerge.simplemerge(ui, a, b, c, label=['local', 'other'])
+        r = simplemerge.simplemerge(ui, a, b, c, label=labels)
         return True, r
     return False, 0
 
 @internaltool('dump', True)
-def _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files):
+def _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
     """
     Creates three versions of the files to merge, containing the
     contents of local, other and base. These files can then be used to
@@ -231,7 +230,7 @@
     ``a.txt``, these files will accordingly be named ``a.txt.local``,
     ``a.txt.other`` and ``a.txt.base`` and they will be placed in the
     same directory as ``a.txt``."""
-    r = _premerge(repo, toolconf, files)
+    r = _premerge(repo, toolconf, files, labels=labels)
     if r:
         a, b, c, back = files
 
@@ -242,8 +241,8 @@
         repo.wwrite(fd + ".base", fca.data(), fca.flags())
     return False, r
 
-def _xmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files):
-    r = _premerge(repo, toolconf, files)
+def _xmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
+    r = _premerge(repo, toolconf, files, labels=labels)
     if r:
         tool, toolpath, binary, symlink = toolconf
         a, b, c, back = files
@@ -270,7 +269,58 @@
         return True, r
     return False, 0
 
-def filemerge(repo, mynode, orig, fcd, fco, fca):
+def _formatconflictmarker(repo, ctx, template, label, pad):
+    """Applies the given template to the ctx, prefixed by the label.
+
+    Pad is the minimum width of the label prefix, so that multiple markers
+    can have aligned templated parts.
+    """
+    if ctx.node() is None:
+        ctx = ctx.p1()
+
+    props = templatekw.keywords.copy()
+    props['templ'] = template
+    props['ctx'] = ctx
+    props['repo'] = repo
+    templateresult = template('conflictmarker', **props)
+
+    label = ('%s:' % label).ljust(pad + 1)
+    mark = '%s %s' % (label, templater.stringify(templateresult))
+
+    # The <<< marks add 8 to the length, and '...' adds three, so max
+    # length of the actual marker is 69.
+    maxlength = 80 - 8 - 3
+    if len(mark) > maxlength:
+        mark = mark[:maxlength] + '...'
+    return mark
+
+_defaultconflictmarker = ('{node|short} ' +
+    '{ifeq(tags, "tip", "", "{tags} ")}' +
+    '{if(bookmarks, "{bookmarks} ")}' +
+    '{ifeq(branch, "default", "", "{branch} ")}' +
+    '- {author|user}: {desc|firstline}')
+
+_defaultconflictlabels = ['local', 'other']
+
+def _formatlabels(repo, fcd, fco, labels):
+    """Formats the given labels using the conflict marker template.
+
+    Returns a list of formatted labels.
+    """
+    cd = fcd.changectx()
+    co = fco.changectx()
+
+    ui = repo.ui
+    template = ui.config('ui', 'mergemarkertemplate', _defaultconflictmarker)
+    template = templater.parsestring(template, quoted=False)
+    tmpl = templater.templater(None, cache={ 'conflictmarker' : template })
+
+    pad = max(len(labels[0]), len(labels[1]))
+
+    return [_formatconflictmarker(repo, cd, tmpl, labels[0], pad),
+            _formatconflictmarker(repo, co, tmpl, labels[1], pad)]
+
+def filemerge(repo, mynode, orig, fcd, fco, fca, labels=None):
     """perform a 3-way merge in the working directory
 
     mynode = parent node before merge
@@ -327,8 +377,17 @@
 
     ui.debug("my %s other %s ancestor %s\n" % (fcd, fco, fca))
 
+    markerstyle = ui.config('ui', 'mergemarkers', 'detailed')
+    if markerstyle == 'basic':
+        formattedlabels = _defaultconflictlabels
+    else:
+        if not labels:
+            labels = _defaultconflictlabels
+
+        formattedlabels = _formatlabels(repo, fcd, fco, labels)
+
     needcheck, r = func(repo, mynode, orig, fcd, fco, fca, toolconf,
-                        (a, b, c, back))
+                        (a, b, c, back), labels=formattedlabels)
     if not needcheck:
         if r:
             if onfailure:
--- a/mercurial/help.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/mercurial/help.py	Sat Jun 14 11:56:20 2014 -0500
@@ -481,8 +481,11 @@
                 rst.append('%s:\n\n' % title)
                 rst.extend(minirst.maketable(sorted(matches[t]), 1))
                 rst.append('\n')
+        if not rst:
+            msg = _('no matches')
+            hint = _('try "hg help" for a list of topics')
+            raise util.Abort(msg, hint=hint)
     elif name and name != 'shortlist':
-        i = None
         if unknowncmd:
             queries = (helpextcmd,)
         elif opts.get('extension'):
@@ -494,12 +497,16 @@
         for f in queries:
             try:
                 rst = f(name)
-                i = None
                 break
-            except error.UnknownCommand, inst:
-                i = inst
-        if i:
-            raise i
+            except error.UnknownCommand:
+                pass
+        else:
+            if unknowncmd:
+                raise error.UnknownCommand(name)
+            else:
+                msg = _('no such help topic: %s') % name
+                hint = _('try "hg help --keyword %s"') % name
+                raise util.Abort(msg, hint=hint)
     else:
         # program name
         if not ui.quiet:
--- a/mercurial/help/config.txt	Sat Jun 14 11:49:02 2014 -0500
+++ b/mercurial/help/config.txt	Sat Jun 14 11:56:20 2014 -0500
@@ -807,7 +807,9 @@
 ---------------
 
 This section configures external merge tools to use for file-level
-merges.
+merges. This section has likely been preconfigured at install time.
+Use :hg:`config merge-tools` to check the existing configuration.
+Also see :hg:`help merge-tools` for more details.
 
 Example ``~/.hgrc``::
 
@@ -819,6 +821,9 @@
   # Give higher priority
   kdiff3.priority = 1
 
+  # Changing the priority of preconfigured tool
+  vimdiff.priority = 0
+
   # Define new tool
   myHtmlTool.args = -m $local $other $base $output
   myHtmlTool.regkey = Software\FooSoftware\HtmlMerge
@@ -838,7 +843,13 @@
 ``args``
   The arguments to pass to the tool executable. You can refer to the
   files being merged as well as the output file through these
-  variables: ``$base``, ``$local``, ``$other``, ``$output``.
+  variables: ``$base``, ``$local``, ``$other``, ``$output``. The meaning
+  of ``$local`` and ``$other`` can vary depending on which action is being
+  performed. During and update or merge, ``$local`` represents the original
+  state of the file, while ``$other`` represents the commit you are updating
+  to or the commit you are merging with. During a rebase ``$local``
+  represents the destination of the rebase, and ``$other`` represents the
+  commit being rebased.
   Default: ``$local $base $other``
 
 ``premerge``
@@ -1203,6 +1214,20 @@
     For more information on merge tools see :hg:`help merge-tools`.
     For configuring merge tools see the ``[merge-tools]`` section.
 
+``mergemarkers``
+    Sets the merge conflict marker label styling. The default ``detailed``
+    style uses the ``mergemarkertemplate`` setting to style the labels.
+    The ``basic`` style just uses 'local' and 'other' as the marker label.
+    One of ``basic`` or ``detailed``.
+    Default is ``detailed``.
+
+``mergemarkertemplate``
+    The template used to print the commit description next to each conflict
+    marker during merge conflicts. See :hg:`help templates` for the template
+    format.
+    Defaults to showing the hash, tags, branches, bookmarks, author, and
+    the first line of the commit description.
+
 ``portablefilenames``
     Check for portable filenames. Can be ``warn``, ``ignore`` or ``abort``.
     Default is ``warn``.
--- a/mercurial/hg.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/mercurial/hg.py	Sat Jun 14 11:56:20 2014 -0500
@@ -483,7 +483,8 @@
     When overwrite is set, changes are clobbered, merged else
 
     returns stats (see pydoc mercurial.merge.applyupdates)"""
-    return mergemod.update(repo, node, False, overwrite, None)
+    return mergemod.update(repo, node, False, overwrite, None,
+                           labels=['working copy', 'destination'])
 
 def update(repo, node):
     """update the working directory to node, merging linear changes"""
--- a/mercurial/i18n.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/mercurial/i18n.py	Sat Jun 14 11:56:20 2014 -0500
@@ -36,7 +36,11 @@
     if message is None:
         return message
 
-    paragraphs = message.split('\n\n')
+    if type(message) is unicode:
+        # goofy unicode docstrings in test
+        paragraphs = message.split(u'\n\n')
+    else:
+        paragraphs = [p.decode("ascii") for p in message.split('\n\n')]
     # Be careful not to translate the empty string -- it holds the
     # meta data of the .po file.
     u = u'\n\n'.join([p and t.ugettext(p) or '' for p in paragraphs])
--- a/mercurial/localrepo.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/mercurial/localrepo.py	Sat Jun 14 11:56:20 2014 -0500
@@ -180,7 +180,9 @@
     requirements = ['revlogv1']
     filtername = None
 
-    bundle2caps = {'HG2X': ()}
+    bundle2caps = {'HG2X': (),
+                   'b2x:listkeys': (),
+                   'b2x:pushkey': ()}
 
     # a list of (ui, featureset) functions.
     # only functions defined in module of enabled extensions are invoked
@@ -479,7 +481,8 @@
         return hook.hook(self.ui, self, name, throw, **args)
 
     @unfilteredmethod
-    def _tag(self, names, node, message, local, user, date, extra={}):
+    def _tag(self, names, node, message, local, user, date, extra={},
+             editor=False):
         if isinstance(names, str):
             names = (names,)
 
@@ -539,14 +542,15 @@
             self[None].add(['.hgtags'])
 
         m = matchmod.exact(self.root, '', ['.hgtags'])
-        tagnode = self.commit(message, user, date, extra=extra, match=m)
+        tagnode = self.commit(message, user, date, extra=extra, match=m,
+                              editor=editor)
 
         for name in names:
             self.hook('tag', node=hex(node), tag=name, local=local)
 
         return tagnode
 
-    def tag(self, names, node, message, local, user, date):
+    def tag(self, names, node, message, local, user, date, editor=False):
         '''tag a revision with one or more symbolic names.
 
         names is a list of strings or, when adding a single tag, names may be a
@@ -574,7 +578,7 @@
                                        '(please commit .hgtags manually)'))
 
         self.tags() # instantiate the cache
-        self._tag(names, node, message, local, user, date)
+        self._tag(names, node, message, local, user, date, editor=editor)
 
     @filteredpropertycache
     def _tagscache(self):
@@ -855,7 +859,8 @@
         # abort here if the journal already exists
         if self.svfs.exists("journal"):
             raise error.RepoError(
-                _("abandoned transaction found - run hg recover"))
+                _("abandoned transaction found"),
+                hint=_("run 'hg recover' to clean up transaction"))
 
         def onclose():
             self.store.write(self._transref())
@@ -1501,149 +1506,9 @@
     def status(self, node1='.', node2=None, match=None,
                ignored=False, clean=False, unknown=False,
                listsubrepos=False):
-        """return status of files between two nodes or node and working
-        directory.
-
-        If node1 is None, use the first dirstate parent instead.
-        If node2 is None, compare node1 with working directory.
-        """
-
-        def mfmatches(ctx):
-            mf = ctx.manifest().copy()
-            if match.always():
-                return mf
-            for fn in mf.keys():
-                if not match(fn):
-                    del mf[fn]
-            return mf
-
-        ctx1 = self[node1]
-        ctx2 = self[node2]
-
-        working = ctx2.rev() is None
-        parentworking = working and ctx1 == self['.']
-        match = match or matchmod.always(self.root, self.getcwd())
-        listignored, listclean, listunknown = ignored, clean, unknown
-
-        # load earliest manifest first for caching reasons
-        if not working and ctx2.rev() < ctx1.rev():
-            ctx2.manifest()
-
-        if not parentworking:
-            def bad(f, msg):
-                # 'f' may be a directory pattern from 'match.files()',
-                # so 'f not in ctx1' is not enough
-                if f not in ctx1 and f not in ctx1.dirs():
-                    self.ui.warn('%s: %s\n' % (self.dirstate.pathto(f), msg))
-            match.bad = bad
-
-        if working: # we need to scan the working dir
-            subrepos = []
-            if '.hgsub' in self.dirstate:
-                subrepos = sorted(ctx2.substate)
-            s = self.dirstate.status(match, subrepos, listignored,
-                                     listclean, listunknown)
-            cmp, modified, added, removed, deleted, unknown, ignored, clean = s
-
-            # check for any possibly clean files
-            if parentworking and cmp:
-                fixup = []
-                # do a full compare of any files that might have changed
-                for f in sorted(cmp):
-                    if (f not in ctx1 or ctx2.flags(f) != ctx1.flags(f)
-                        or ctx1[f].cmp(ctx2[f])):
-                        modified.append(f)
-                    else:
-                        fixup.append(f)
-
-                # update dirstate for files that are actually clean
-                if fixup:
-                    if listclean:
-                        clean += fixup
-
-                    try:
-                        # updating the dirstate is optional
-                        # so we don't wait on the lock
-                        wlock = self.wlock(False)
-                        try:
-                            for f in fixup:
-                                self.dirstate.normal(f)
-                        finally:
-                            wlock.release()
-                    except error.LockError:
-                        pass
-
-        if not parentworking:
-            mf1 = mfmatches(ctx1)
-            if working:
-                # we are comparing working dir against non-parent
-                # generate a pseudo-manifest for the working dir
-                mf2 = mfmatches(self['.'])
-                for f in cmp + modified + added:
-                    mf2[f] = None
-                    mf2.set(f, ctx2.flags(f))
-                for f in removed:
-                    if f in mf2:
-                        del mf2[f]
-            else:
-                # we are comparing two revisions
-                deleted, unknown, ignored = [], [], []
-                mf2 = mfmatches(ctx2)
-
-            modified, added, clean = [], [], []
-            withflags = mf1.withflags() | mf2.withflags()
-            for fn, mf2node in mf2.iteritems():
-                if fn in mf1:
-                    if (fn not in deleted and
-                        ((fn in withflags and mf1.flags(fn) != mf2.flags(fn)) or
-                         (mf1[fn] != mf2node and
-                          (mf2node or ctx1[fn].cmp(ctx2[fn]))))):
-                        modified.append(fn)
-                    elif listclean:
-                        clean.append(fn)
-                    del mf1[fn]
-                elif fn not in deleted:
-                    added.append(fn)
-            removed = mf1.keys()
-
-        if working and modified and not self.dirstate._checklink:
-            # Symlink placeholders may get non-symlink-like contents
-            # via user error or dereferencing by NFS or Samba servers,
-            # so we filter out any placeholders that don't look like a
-            # symlink
-            sane = []
-            for f in modified:
-                if ctx2.flags(f) == 'l':
-                    d = ctx2[f].data()
-                    if d == '' or len(d) >= 1024 or '\n' in d or util.binary(d):
-                        self.ui.debug('ignoring suspect symlink placeholder'
-                                      ' "%s"\n' % f)
-                        continue
-                sane.append(f)
-            modified = sane
-
-        r = modified, added, removed, deleted, unknown, ignored, clean
-
-        if listsubrepos:
-            for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
-                if working:
-                    rev2 = None
-                else:
-                    rev2 = ctx2.substate[subpath][1]
-                try:
-                    submatch = matchmod.narrowmatcher(subpath, match)
-                    s = sub.status(rev2, match=submatch, ignored=listignored,
-                                   clean=listclean, unknown=listunknown,
-                                   listsubrepos=True)
-                    for rfiles, sfiles in zip(r, s):
-                        rfiles.extend("%s/%s" % (subpath, f) for f in sfiles)
-                except error.LookupError:
-                    self.ui.status(_("skipping missing subrepository: %s\n")
-                                   % subpath)
-
-        for l in r:
-            l.sort()
-        return r
+        '''a convenience method that calls node1.status(node2)'''
+        return self[node1].status(node2, match, ignored, clean, unknown,
+                                  listsubrepos)
 
     def heads(self, start=None):
         heads = self.changelog.heads(start)
--- a/mercurial/merge.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/mercurial/merge.py	Sat Jun 14 11:56:20 2014 -0500
@@ -55,6 +55,8 @@
 
     def reset(self, node=None, other=None):
         self._state = {}
+        self._local = None
+        self._other = None
         if node:
             self._local = node
             self._other = other
@@ -68,6 +70,8 @@
         of on disk file.
         """
         self._state = {}
+        self._local = None
+        self._other = None
         records = self._readrecords()
         for rtype, record in records:
             if rtype == 'L':
@@ -171,6 +175,18 @@
                 raise
         return records
 
+    def active(self):
+        """Whether mergestate is active.
+
+        Returns True if there appears to be mergestate. This is a rough proxy
+        for "is a merge in progress."
+        """
+        # Check local variables before looking at filesystem for performance
+        # reasons.
+        return bool(self._local) or bool(self._state) or \
+               self._repo.opener.exists(self.statepathv1) or \
+               self._repo.opener.exists(self.statepathv2)
+
     def commit(self):
         """Write current state on disk (if necessary)"""
         if self._dirty:
@@ -232,10 +248,7 @@
         return self._state[dfile][0]
 
     def __iter__(self):
-        l = self._state.keys()
-        l.sort()
-        for f in l:
-            yield f
+        return iter(sorted(self._state))
 
     def files(self):
         return self._state.keys()
@@ -244,7 +257,14 @@
         self._state[dfile][0] = state
         self._dirty = True
 
-    def resolve(self, dfile, wctx):
+    def unresolved(self):
+        """Obtain the paths of unresolved files."""
+
+        for f, entry in self._state.items():
+            if entry[0] == 'u':
+                yield f
+
+    def resolve(self, dfile, wctx, labels=None):
         """rerun merge process for file path `dfile`"""
         if self[dfile] == 'r':
             return 0
@@ -267,7 +287,8 @@
         f = self._repo.opener("merge/" + hash)
         self._repo.wwrite(dfile, f.read(), flags)
         f.close()
-        r = filemerge.filemerge(self._repo, self._local, lfile, fcd, fco, fca)
+        r = filemerge.filemerge(self._repo, self._local, lfile, fcd, fco, fca,
+                                labels=labels)
         if r is None:
             # no real conflict
             del self._state[dfile]
@@ -310,62 +331,44 @@
     as removed.
     """
 
-    actions = []
-    state = branchmerge and 'r' or 'f'
+    ractions = []
+    factions = xactions = []
+    if branchmerge:
+        xactions = ractions
     for f in wctx.deleted():
         if f not in mctx:
-            actions.append((f, state, None, "forget deleted"))
+            xactions.append((f, None, "forget deleted"))
 
     if not branchmerge:
         for f in wctx.removed():
             if f not in mctx:
-                actions.append((f, "f", None, "forget removed"))
+                factions.append((f, None, "forget removed"))
 
-    return actions
+    return ractions, factions
 
 def _checkcollision(repo, wmf, actions):
     # build provisional merged manifest up
     pmmf = set(wmf)
 
-    def addop(f, args):
-        pmmf.add(f)
-    def removeop(f, args):
-        pmmf.discard(f)
-    def nop(f, args):
-        pass
-
-    def renamemoveop(f, args):
-        f2, flags = args
-        pmmf.discard(f2)
-        pmmf.add(f)
-    def renamegetop(f, args):
-        f2, flags = args
-        pmmf.add(f)
-    def mergeop(f, args):
-        f1, f2, fa, move, anc = args
-        if move:
-            pmmf.discard(f1)
-        pmmf.add(f)
-
-    opmap = {
-        "a": addop,
-        "dm": renamemoveop,
-        "dg": renamegetop,
-        "dr": nop,
-        "e": nop,
-        "k": nop,
-        "f": addop, # untracked file should be kept in working directory
-        "g": addop,
-        "m": mergeop,
-        "r": removeop,
-        "rd": nop,
-        "cd": addop,
-        "dc": addop,
-    }
-    for f, m, args, msg in actions:
-        op = opmap.get(m)
-        assert op, m
-        op(f, args)
+    if actions:
+        # k, dr, e and rd are no-op
+        for m in 'a', 'f', 'g', 'cd', 'dc':
+            for f, args, msg in actions[m]:
+                pmmf.add(f)
+        for f, args, msg in actions['r']:
+            pmmf.discard(f)
+        for f, args, msg in actions['dm']:
+            f2, flags = args
+            pmmf.discard(f2)
+            pmmf.add(f)
+        for f, args, msg in actions['dg']:
+            f2, flags = args
+            pmmf.add(f)
+        for f, args, msg in actions['m']:
+            f1, f2, fa, move, anc = args
+            if move:
+                pmmf.discard(f1)
+            pmmf.add(f)
 
     # check case-folding collision in provisional merged manifest
     foldmap = {}
@@ -386,7 +389,8 @@
     acceptremote = accept the incoming changes without prompting
     """
 
-    actions, copy, movewithdir = [], {}, {}
+    actions = dict((m, []) for m in 'a f g cd dc r dm dg m dr e rd k'.split())
+    copy, movewithdir = {}, {}
 
     # manifests fetched in order are going to be faster, so prime the caches
     [x.manifest() for x in
@@ -396,9 +400,9 @@
         ret = copies.mergecopies(repo, wctx, p2, pa)
         copy, movewithdir, diverge, renamedelete = ret
         for of, fl in diverge.iteritems():
-            actions.append((of, "dr", (fl,), "divergent renames"))
+            actions['dr'].append((of, (fl,), "divergent renames"))
         for of, fl in renamedelete.iteritems():
-            actions.append((of, "rd", (fl,), "rename and delete"))
+            actions['rd'].append((of, (fl,), "rename and delete"))
 
     repo.ui.note(_("resolving manifests\n"))
     repo.ui.debug(" branchmerge: %s, force: %s, partial: %s\n"
@@ -450,50 +454,50 @@
             fla = ma.flags(fa)
             nol = 'l' not in fl1 + fl2 + fla
             if n2 == a and fl2 == fla:
-                actions.append((f, "k", (), "keep")) # remote unchanged
+                actions['k'].append((f, (), "keep")) # remote unchanged
             elif n1 == a and fl1 == fla: # local unchanged - use remote
                 if n1 == n2: # optimization: keep local content
-                    actions.append((f, "e", (fl2,), "update permissions"))
+                    actions['e'].append((f, (fl2,), "update permissions"))
                 else:
-                    actions.append((f, "g", (fl2,), "remote is newer"))
+                    actions['g'].append((f, (fl2,), "remote is newer"))
             elif nol and n2 == a: # remote only changed 'x'
-                actions.append((f, "e", (fl2,), "update permissions"))
+                actions['e'].append((f, (fl2,), "update permissions"))
             elif nol and n1 == a: # local only changed 'x'
-                actions.append((f, "g", (fl1,), "remote is newer"))
+                actions['g'].append((f, (fl1,), "remote is newer"))
             else: # both changed something
-                actions.append((f, "m", (f, f, fa, False, pa.node()),
+                actions['m'].append((f, (f, f, fa, False, pa.node()),
                                "versions differ"))
         elif f in copied: # files we'll deal with on m2 side
             pass
         elif n1 and f in movewithdir: # directory rename, move local
             f2 = movewithdir[f]
-            actions.append((f2, "dm", (f, fl1),
+            actions['dm'].append((f2, (f, fl1),
                             "remote directory rename - move from " + f))
         elif n1 and f in copy:
             f2 = copy[f]
-            actions.append((f, "m", (f, f2, f2, False, pa.node()),
+            actions['m'].append((f, (f, f2, f2, False, pa.node()),
                             "local copied/moved from " + f2))
         elif n1 and f in ma: # clean, a different, no remote
             if n1 != ma[f]:
                 if acceptremote:
-                    actions.append((f, "r", None, "remote delete"))
+                    actions['r'].append((f, None, "remote delete"))
                 else:
-                    actions.append((f, "cd", None, "prompt changed/deleted"))
+                    actions['cd'].append((f, None, "prompt changed/deleted"))
             elif n1[20:] == "a": # added, no remote
-                actions.append((f, "f", None, "remote deleted"))
+                actions['f'].append((f, None, "remote deleted"))
             else:
-                actions.append((f, "r", None, "other deleted"))
+                actions['r'].append((f, None, "other deleted"))
         elif n2 and f in movewithdir:
             f2 = movewithdir[f]
-            actions.append((f2, "dg", (f, fl2),
+            actions['dg'].append((f2, (f, fl2),
                             "local directory rename - get from " + f))
         elif n2 and f in copy:
             f2 = copy[f]
             if f2 in m2:
-                actions.append((f, "m", (f2, f, f2, False, pa.node()),
+                actions['m'].append((f, (f2, f, f2, False, pa.node()),
                                 "remote copied from " + f2))
             else:
-                actions.append((f, "m", (f2, f, f2, True, pa.node()),
+                actions['m'].append((f, (f2, f, f2, True, pa.node()),
                                 "remote moved from " + f2))
         elif n2 and f not in ma:
             # local unknown, remote created: the logic is described by the
@@ -509,17 +513,17 @@
             # Checking whether the files are different is expensive, so we
             # don't do that when we can avoid it.
             if force and not branchmerge:
-                actions.append((f, "g", (fl2,), "remote created"))
+                actions['g'].append((f, (fl2,), "remote created"))
             else:
                 different = _checkunknownfile(repo, wctx, p2, f)
                 if force and branchmerge and different:
                     # FIXME: This is wrong - f is not in ma ...
-                    actions.append((f, "m", (f, f, f, False, pa.node()),
+                    actions['m'].append((f, (f, f, f, False, pa.node()),
                                     "remote differs from untracked local"))
                 elif not force and different:
                     aborts.append((f, "ud"))
                 else:
-                    actions.append((f, "g", (fl2,), "remote created"))
+                    actions['g'].append((f, (fl2,), "remote created"))
         elif n2 and n2 != ma[f]:
             different = _checkunknownfile(repo, wctx, p2, f)
             if not force and different:
@@ -527,10 +531,10 @@
             else:
                 # if different: old untracked f may be overwritten and lost
                 if acceptremote:
-                    actions.append((f, "g", (m2.flags(f),),
+                    actions['g'].append((f, (m2.flags(f),),
                                    "remote recreating"))
                 else:
-                    actions.append((f, "dc", (m2.flags(f),),
+                    actions['dc'].append((f, (m2.flags(f),),
                                    "prompt deleted/changed"))
 
     for f, m in sorted(aborts):
@@ -545,44 +549,32 @@
         # check collision between files only in p2 for clean update
         if (not branchmerge and
             (force or not wctx.dirty(missing=True, branch=False))):
-            _checkcollision(repo, m2, [])
+            _checkcollision(repo, m2, None)
         else:
             _checkcollision(repo, m1, actions)
 
     return actions
 
-def actionkey(a):
-    return a[1] in "rf" and -1 or 0, a
-
-def getremove(repo, mctx, overwrite, args):
-    """apply usually-non-interactive updates to the working directory
-
-    mctx is the context to be merged into the working copy
+def batchremove(repo, actions):
+    """apply removes to the working directory
 
     yields tuples for progress updates
     """
     verbose = repo.ui.verbose
     unlink = util.unlinkpath
     wjoin = repo.wjoin
-    fctx = mctx.filectx
-    wwrite = repo.wwrite
     audit = repo.wopener.audit
     i = 0
-    for arg in args:
-        f = arg[0]
-        if arg[1] == 'r':
-            if verbose:
-                repo.ui.note(_("removing %s\n") % f)
-            audit(f)
-            try:
-                unlink(wjoin(f), ignoremissing=True)
-            except OSError, inst:
-                repo.ui.warn(_("update failed to remove %s: %s!\n") %
-                             (f, inst.strerror))
-        else:
-            if verbose:
-                repo.ui.note(_("getting %s\n") % f)
-            wwrite(f, fctx(f).data(), arg[2][0])
+    for f, args, msg in actions:
+        repo.ui.debug(" %s: %s -> r\n" % (f, msg))
+        if verbose:
+            repo.ui.note(_("removing %s\n") % f)
+        audit(f)
+        try:
+            unlink(wjoin(f), ignoremissing=True)
+        except OSError, inst:
+            repo.ui.warn(_("update failed to remove %s: %s!\n") %
+                         (f, inst.strerror))
         if i == 100:
             yield i, f
             i = 0
@@ -590,7 +582,30 @@
     if i > 0:
         yield i, f
 
-def applyupdates(repo, actions, wctx, mctx, overwrite):
+def batchget(repo, mctx, actions):
+    """apply gets to the working directory
+
+    mctx is the context to get from
+
+    yields tuples for progress updates
+    """
+    verbose = repo.ui.verbose
+    fctx = mctx.filectx
+    wwrite = repo.wwrite
+    i = 0
+    for f, args, msg in actions:
+        repo.ui.debug(" %s: %s -> g\n" % (f, msg))
+        if verbose:
+            repo.ui.note(_("getting %s\n") % f)
+        wwrite(f, fctx(f).data(), args[0])
+        if i == 100:
+            yield i, f
+            i = 0
+        i += 1
+    if i > 0:
+        yield i, f
+
+def applyupdates(repo, actions, wctx, mctx, overwrite, labels=None):
     """apply the merge action list to the working directory
 
     wctx is the working copy context
@@ -604,29 +619,30 @@
     ms = mergestate(repo)
     ms.reset(wctx.p1().node(), mctx.node())
     moves = []
-    actions.sort(key=actionkey)
+    for m, l in actions.items():
+        l.sort()
 
     # prescan for merges
-    for a in actions:
-        f, m, args, msg = a
-        repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
-        if m == "m": # merge
-            f1, f2, fa, move, anc = args
-            if f == '.hgsubstate': # merged internally
-                continue
-            repo.ui.debug("  preserving %s for resolve of %s\n" % (f1, f))
-            fcl = wctx[f1]
-            fco = mctx[f2]
-            actx = repo[anc]
-            if fa in actx:
-                fca = actx[fa]
-            else:
-                fca = repo.filectx(f1, fileid=nullrev)
-            ms.add(fcl, fco, fca, f)
-            if f1 != f and move:
-                moves.append(f1)
+    for f, args, msg in actions['m']:
+        f1, f2, fa, move, anc = args
+        if f == '.hgsubstate': # merged internally
+            continue
+        repo.ui.debug(" preserving %s for resolve of %s\n" % (f1, f))
+        fcl = wctx[f1]
+        fco = mctx[f2]
+        actx = repo[anc]
+        if fa in actx:
+            fca = actx[fa]
+        else:
+            fca = repo.filectx(f1, fileid=nullrev)
+        ms.add(fcl, fco, fca, f)
+        if f1 != f and move:
+            moves.append(f1)
 
     audit = repo.wopener.audit
+    _updating = _('updating')
+    _files = _('files')
+    progress = repo.ui.progress
 
     # remove renamed files after safely stored
     for f in moves:
@@ -635,86 +651,120 @@
             audit(f)
             util.unlinkpath(repo.wjoin(f))
 
-    numupdates = len([a for a in actions if a[1] != 'k'])
-    workeractions = [a for a in actions if a[1] in 'gr']
-    updateactions = [a for a in workeractions if a[1] == 'g']
-    updated = len(updateactions)
-    removeactions = [a for a in workeractions if a[1] == 'r']
-    removed = len(removeactions)
-    actions = [a for a in actions if a[1] not in 'grk']
+    numupdates = sum(len(l) for m, l in actions.items() if m != 'k')
 
-    hgsub = [a[1] for a in workeractions if a[0] == '.hgsubstate']
-    if hgsub and hgsub[0] == 'r':
+    if [a for a in actions['r'] if a[0] == '.hgsubstate']:
         subrepo.submerge(repo, wctx, mctx, wctx, overwrite)
 
+    # remove in parallel (must come first)
     z = 0
-    prog = worker.worker(repo.ui, 0.001, getremove, (repo, mctx, overwrite),
-                         removeactions)
+    prog = worker.worker(repo.ui, 0.001, batchremove, (repo,), actions['r'])
     for i, item in prog:
         z += i
-        repo.ui.progress(_('updating'), z, item=item, total=numupdates,
-                         unit=_('files'))
-    prog = worker.worker(repo.ui, 0.001, getremove, (repo, mctx, overwrite),
-                         updateactions)
+        progress(_updating, z, item=item, total=numupdates, unit=_files)
+    removed = len(actions['r'])
+
+    # get in parallel
+    prog = worker.worker(repo.ui, 0.001, batchget, (repo, mctx), actions['g'])
     for i, item in prog:
         z += i
-        repo.ui.progress(_('updating'), z, item=item, total=numupdates,
-                         unit=_('files'))
+        progress(_updating, z, item=item, total=numupdates, unit=_files)
+    updated = len(actions['g'])
 
-    if hgsub and hgsub[0] == 'g':
+    if [a for a in actions['g'] if a[0] == '.hgsubstate']:
         subrepo.submerge(repo, wctx, mctx, wctx, overwrite)
 
-    _updating = _('updating')
-    _files = _('files')
-    progress = repo.ui.progress
+    # forget (manifest only, just log it) (must come first)
+    for f, args, msg in actions['f']:
+        repo.ui.debug(" %s: %s -> f\n" % (f, msg))
+        z += 1
+        progress(_updating, z, item=f, total=numupdates, unit=_files)
+
+    # re-add (manifest only, just log it)
+    for f, args, msg in actions['a']:
+        repo.ui.debug(" %s: %s -> a\n" % (f, msg))
+        z += 1
+        progress(_updating, z, item=f, total=numupdates, unit=_files)
+
+    # keep (noop, just log it)
+    for f, args, msg in actions['k']:
+        repo.ui.debug(" %s: %s -> k\n" % (f, msg))
+        # no progress
 
-    for i, a in enumerate(actions):
-        f, m, args, msg = a
-        progress(_updating, z + i + 1, item=f, total=numupdates, unit=_files)
-        if m == "m": # merge
-            f1, f2, fa, move, anc = args
-            if f == '.hgsubstate': # subrepo states need updating
-                subrepo.submerge(repo, wctx, mctx, wctx.ancestor(mctx),
-                                 overwrite)
-                continue
-            audit(f)
-            r = ms.resolve(f, wctx)
-            if r is not None and r > 0:
-                unresolved += 1
+    # merge
+    for f, args, msg in actions['m']:
+        repo.ui.debug(" %s: %s -> m\n" % (f, msg))
+        z += 1
+        progress(_updating, z, item=f, total=numupdates, unit=_files)
+        f1, f2, fa, move, anc = args
+        if f == '.hgsubstate': # subrepo states need updating
+            subrepo.submerge(repo, wctx, mctx, wctx.ancestor(mctx),
+                             overwrite)
+            continue
+        audit(f)
+        r = ms.resolve(f, wctx, labels=labels)
+        if r is not None and r > 0:
+            unresolved += 1
+        else:
+            if r is None:
+                updated += 1
             else:
-                if r is None:
-                    updated += 1
-                else:
-                    merged += 1
-        elif m == "dm": # directory rename, move local
-            f0, flags = args
-            repo.ui.note(_("moving %s to %s\n") % (f0, f))
-            audit(f)
-            repo.wwrite(f, wctx.filectx(f0).data(), flags)
-            util.unlinkpath(repo.wjoin(f0))
-            updated += 1
-        elif m == "dg": # local directory rename, get
-            f0, flags = args
-            repo.ui.note(_("getting %s to %s\n") % (f0, f))
-            repo.wwrite(f, mctx.filectx(f0).data(), flags)
-            updated += 1
-        elif m == "dr": # divergent renames
-            fl, = args
-            repo.ui.warn(_("note: possible conflict - %s was renamed "
-                           "multiple times to:\n") % f)
-            for nf in fl:
-                repo.ui.warn(" %s\n" % nf)
-        elif m == "rd": # rename and delete
-            fl, = args
-            repo.ui.warn(_("note: possible conflict - %s was deleted "
-                           "and renamed to:\n") % f)
-            for nf in fl:
-                repo.ui.warn(" %s\n" % nf)
-        elif m == "e": # exec
-            flags, = args
-            audit(f)
-            util.setflags(repo.wjoin(f), 'l' in flags, 'x' in flags)
-            updated += 1
+                merged += 1
+
+    # directory rename, move local
+    for f, args, msg in actions['dm']:
+        repo.ui.debug(" %s: %s -> dm\n" % (f, msg))
+        z += 1
+        progress(_updating, z, item=f, total=numupdates, unit=_files)
+        f0, flags = args
+        repo.ui.note(_("moving %s to %s\n") % (f0, f))
+        audit(f)
+        repo.wwrite(f, wctx.filectx(f0).data(), flags)
+        util.unlinkpath(repo.wjoin(f0))
+        updated += 1
+
+    # local directory rename, get
+    for f, args, msg in actions['dg']:
+        repo.ui.debug(" %s: %s -> dg\n" % (f, msg))
+        z += 1
+        progress(_updating, z, item=f, total=numupdates, unit=_files)
+        f0, flags = args
+        repo.ui.note(_("getting %s to %s\n") % (f0, f))
+        repo.wwrite(f, mctx.filectx(f0).data(), flags)
+        updated += 1
+
+    # divergent renames
+    for f, args, msg in actions['dr']:
+        repo.ui.debug(" %s: %s -> dr\n" % (f, msg))
+        z += 1
+        progress(_updating, z, item=f, total=numupdates, unit=_files)
+        fl, = args
+        repo.ui.warn(_("note: possible conflict - %s was renamed "
+                       "multiple times to:\n") % f)
+        for nf in fl:
+            repo.ui.warn(" %s\n" % nf)
+
+    # rename and delete
+    for f, args, msg in actions['rd']:
+        repo.ui.debug(" %s: %s -> rd\n" % (f, msg))
+        z += 1
+        progress(_updating, z, item=f, total=numupdates, unit=_files)
+        fl, = args
+        repo.ui.warn(_("note: possible conflict - %s was deleted "
+                       "and renamed to:\n") % f)
+        for nf in fl:
+            repo.ui.warn(" %s\n" % nf)
+
+    # exec
+    for f, args, msg in actions['e']:
+        repo.ui.debug(" %s: %s -> e\n" % (f, msg))
+        z += 1
+        progress(_updating, z, item=f, total=numupdates, unit=_files)
+        flags, = args
+        audit(f)
+        util.setflags(repo.wjoin(f), 'l' in flags, 'x' in flags)
+        updated += 1
+
     ms.commit()
     progress(_updating, None, total=numupdates, unit=_files)
 
@@ -735,163 +785,173 @@
             (wctx, mctx, _(' and ').join(str(anc) for anc in ancestors)))
 
         # Call for bids
-        fbids = {} # mapping filename to list af action bids
+        fbids = {} # mapping filename to bids (action method to list af actions)
         for ancestor in ancestors:
             repo.ui.note(_('\ncalculating bids for ancestor %s\n') % ancestor)
             actions = manifestmerge(repo, wctx, mctx, ancestor,
                                     branchmerge, force,
                                     partial, acceptremote, followcopies)
-            for a in sorted(actions):
-                repo.ui.debug(' %s: %s\n' % (a[0], a[1]))
-                f = a[0]
-                if f in fbids:
-                    fbids[f].append(a)
-                else:
-                    fbids[f] = [a]
+            for m, l in sorted(actions.items()):
+                for a in l:
+                    f, args, msg = a
+                    repo.ui.debug(' %s: %s -> %s\n' % (f, msg, m))
+                    if f in fbids:
+                        d = fbids[f]
+                        if m in d:
+                            d[m].append(a)
+                        else:
+                            d[m] = [a]
+                    else:
+                        fbids[f] = {m: [a]}
 
         # Pick the best bid for each file
         repo.ui.note(_('\nauction for merging merge bids\n'))
-        actions = []
-        for f, bidsl in sorted(fbids.items()):
+        actions = dict((m, []) for m in actions.keys())
+        for f, bids in sorted(fbids.items()):
+            # bids is a mapping from action method to list af actions
             # Consensus?
-            a0 = bidsl[0]
-            if util.all(a == a0 for a in bidsl[1:]): # len(bidsl) is > 1
-                repo.ui.note(" %s: consensus for %s\n" % (f, a0[1]))
-                actions.append(a0)
-                continue
-            # Group bids by kind of action
-            bids = {}
-            for a in bidsl:
-                m = a[1]
-                if m in bids:
-                    bids[m].append(a)
-                else:
-                    bids[m] = [a]
+            if len(bids) == 1: # all bids are the same kind of method
+                m, l = bids.items()[0]
+                if util.all(a == l[0] for a in l[1:]): # len(bids) is > 1
+                    repo.ui.note(" %s: consensus for %s\n" % (f, m))
+                    actions[m].append(l[0])
+                    continue
             # If keep is an option, just do it.
             if "k" in bids:
                 repo.ui.note(" %s: picking 'keep' action\n" % f)
-                actions.append(bids["k"][0])
+                actions['k'].append(bids["k"][0])
                 continue
-            # If all gets agree [how could they not?], just do it.
+            # If there are gets and they all agree [how could they not?], do it.
             if "g" in bids:
                 ga0 = bids["g"][0]
                 if util.all(a == ga0 for a in bids["g"][1:]):
                     repo.ui.note(" %s: picking 'get' action\n" % f)
-                    actions.append(ga0)
+                    actions['g'].append(ga0)
                     continue
             # TODO: Consider other simple actions such as mode changes
             # Handle inefficient democrazy.
             repo.ui.note(_(' %s: multiple bids for merge action:\n') % f)
-            for _f, m, args, msg in bidsl:
-                repo.ui.note('  %s -> %s\n' % (msg, m))
+            for m, l in sorted(bids.items()):
+                for _f, args, msg in l:
+                    repo.ui.note('  %s -> %s\n' % (msg, m))
             # Pick random action. TODO: Instead, prompt user when resolving
-            a0 = bidsl[0]
+            m, l = bids.items()[0]
             repo.ui.warn(_(' %s: ambiguous merge - picked %s action\n') %
-                         (f, a0[1]))
-            actions.append(a0)
+                         (f, m))
+            actions[m].append(l[0])
             continue
         repo.ui.note(_('end of auction\n\n'))
 
-    # Filter out prompts.
-    newactions, prompts = [], []
-    for a in actions:
-        if a[1] in ("cd", "dc"):
-            prompts.append(a)
+    # Prompt and create actions. TODO: Move this towards resolve phase.
+    for f, args, msg in actions['cd']:
+        if repo.ui.promptchoice(
+            _("local changed %s which remote deleted\n"
+              "use (c)hanged version or (d)elete?"
+              "$$ &Changed $$ &Delete") % f, 0):
+            actions['r'].append((f, None, "prompt delete"))
         else:
-            newactions.append(a)
-    # Prompt and create actions. TODO: Move this towards resolve phase.
-    for f, m, args, msg in sorted(prompts):
-        if m == "cd":
-            if repo.ui.promptchoice(
-                _("local changed %s which remote deleted\n"
-                  "use (c)hanged version or (d)elete?"
-                  "$$ &Changed $$ &Delete") % f, 0):
-                newactions.append((f, "r", None, "prompt delete"))
-            else:
-                newactions.append((f, "a", None, "prompt keep"))
-        elif m == "dc":
-            flags, = args
-            if repo.ui.promptchoice(
-                _("remote changed %s which local deleted\n"
-                  "use (c)hanged version or leave (d)eleted?"
-                  "$$ &Changed $$ &Deleted") % f, 0) == 0:
-                newactions.append((f, "g", (flags,), "prompt recreating"))
-        else: assert False, m
+            actions['a'].append((f, None, "prompt keep"))
+    del actions['cd'][:]
+
+    for f, args, msg in actions['dc']:
+        flags, = args
+        if repo.ui.promptchoice(
+            _("remote changed %s which local deleted\n"
+              "use (c)hanged version or leave (d)eleted?"
+              "$$ &Changed $$ &Deleted") % f, 0) == 0:
+            actions['g'].append((f, (flags,), "prompt recreating"))
+    del actions['dc'][:]
 
     if wctx.rev() is None:
-        newactions += _forgetremoved(wctx, mctx, branchmerge)
+        ractions, factions = _forgetremoved(wctx, mctx, branchmerge)
+        actions['r'].extend(ractions)
+        actions['f'].extend(factions)
 
-    return newactions
+    return actions
 
 def recordupdates(repo, actions, branchmerge):
     "record merge actions to the dirstate"
-
-    for a in actions:
-        f, m, args, msg = a
-        if m == "r": # remove
-            if branchmerge:
-                repo.dirstate.remove(f)
-            else:
-                repo.dirstate.drop(f)
-        elif m == "a": # re-add
-            if not branchmerge:
-                repo.dirstate.add(f)
-        elif m == "f": # forget
+    # remove (must come first)
+    for f, args, msg in actions['r']:
+        if branchmerge:
+            repo.dirstate.remove(f)
+        else:
             repo.dirstate.drop(f)
-        elif m == "e": # exec change
-            repo.dirstate.normallookup(f)
-        elif m == "k": # keep
-            pass
-        elif m == "g": # get
-            if branchmerge:
-                repo.dirstate.otherparent(f)
-            else:
-                repo.dirstate.normal(f)
-        elif m == "m": # merge
-            f1, f2, fa, move, anc = args
-            if branchmerge:
-                # We've done a branch merge, mark this file as merged
-                # so that we properly record the merger later
-                repo.dirstate.merge(f)
-                if f1 != f2: # copy/rename
-                    if move:
-                        repo.dirstate.remove(f1)
-                    if f1 != f:
-                        repo.dirstate.copy(f1, f)
-                    else:
-                        repo.dirstate.copy(f2, f)
-            else:
-                # We've update-merged a locally modified file, so
-                # we set the dirstate to emulate a normal checkout
-                # of that file some time in the past. Thus our
-                # merge will appear as a normal local file
-                # modification.
-                if f2 == f: # file not locally copied/moved
-                    repo.dirstate.normallookup(f)
+
+    # forget (must come first)
+    for f, args, msg in actions['f']:
+        repo.dirstate.drop(f)
+
+    # re-add
+    for f, args, msg in actions['a']:
+        if not branchmerge:
+            repo.dirstate.add(f)
+
+    # exec change
+    for f, args, msg in actions['e']:
+        repo.dirstate.normallookup(f)
+
+    # keep
+    for f, args, msg in actions['k']:
+        pass
+
+    # get
+    for f, args, msg in actions['g']:
+        if branchmerge:
+            repo.dirstate.otherparent(f)
+        else:
+            repo.dirstate.normal(f)
+
+    # merge
+    for f, args, msg in actions['m']:
+        f1, f2, fa, move, anc = args
+        if branchmerge:
+            # We've done a branch merge, mark this file as merged
+            # so that we properly record the merger later
+            repo.dirstate.merge(f)
+            if f1 != f2: # copy/rename
                 if move:
-                    repo.dirstate.drop(f1)
-        elif m == "dm": # directory rename, move local
-            f0, flag = args
-            if f0 not in repo.dirstate:
-                # untracked file moved
-                continue
-            if branchmerge:
-                repo.dirstate.add(f)
-                repo.dirstate.remove(f0)
-                repo.dirstate.copy(f0, f)
-            else:
-                repo.dirstate.normal(f)
-                repo.dirstate.drop(f0)
-        elif m == "dg": # directory rename, get
-            f0, flag = args
-            if branchmerge:
-                repo.dirstate.add(f)
-                repo.dirstate.copy(f0, f)
-            else:
-                repo.dirstate.normal(f)
+                    repo.dirstate.remove(f1)
+                if f1 != f:
+                    repo.dirstate.copy(f1, f)
+                else:
+                    repo.dirstate.copy(f2, f)
+        else:
+            # We've update-merged a locally modified file, so
+            # we set the dirstate to emulate a normal checkout
+            # of that file some time in the past. Thus our
+            # merge will appear as a normal local file
+            # modification.
+            if f2 == f: # file not locally copied/moved
+                repo.dirstate.normallookup(f)
+            if move:
+                repo.dirstate.drop(f1)
+
+    # directory rename, move local
+    for f, args, msg in actions['dm']:
+        f0, flag = args
+        if f0 not in repo.dirstate:
+            # untracked file moved
+            continue
+        if branchmerge:
+            repo.dirstate.add(f)
+            repo.dirstate.remove(f0)
+            repo.dirstate.copy(f0, f)
+        else:
+            repo.dirstate.normal(f)
+            repo.dirstate.drop(f0)
+
+    # directory rename, get
+    for f, args, msg in actions['dg']:
+        f0, flag = args
+        if branchmerge:
+            repo.dirstate.add(f)
+            repo.dirstate.copy(f0, f)
+        else:
+            repo.dirstate.normal(f)
 
 def update(repo, node, branchmerge, force, partial, ancestor=None,
-           mergeancestor=False):
+           mergeancestor=False, labels=None):
     """
     Perform a merge between the working directory and the given node
 
@@ -1071,7 +1131,7 @@
             # note that we're in the middle of an update
             repo.vfs.write('updatestate', p2.hex())
 
-        stats = applyupdates(repo, actions, wc, p2, overwrite)
+        stats = applyupdates(repo, actions, wc, p2, overwrite, labels=labels)
 
         if not partial:
             repo.setparents(fp1, fp2)
--- a/mercurial/minirst.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/mercurial/minirst.py	Sat Jun 14 11:56:20 2014 -0500
@@ -56,7 +56,7 @@
     # on strings in local encoding causes invalid byte sequences.
     utext = text.decode(encoding.encoding)
     for f, t in substs:
-        utext = utext.replace(f, t)
+        utext = utext.replace(f.decode("ascii"), t.decode("ascii"))
     return utext.encode(encoding.encoding)
 
 _blockre = re.compile(r"\n(?:\s*\n)+")
--- a/mercurial/patch.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/mercurial/patch.py	Sat Jun 14 11:56:20 2014 -0500
@@ -417,12 +417,12 @@
         return os.path.join(self.opener.base, f)
 
     def getfile(self, fname):
-        path = self._join(fname)
-        if os.path.islink(path):
-            return (os.readlink(path), (True, False))
+        if self.opener.islink(fname):
+            return (self.opener.readlink(fname), (True, False))
+
         isexec = False
         try:
-            isexec = os.lstat(path).st_mode & 0100 != 0
+            isexec = self.opener.lstat(fname).st_mode & 0100 != 0
         except OSError, e:
             if e.errno != errno.ENOENT:
                 raise
@@ -431,17 +431,17 @@
     def setfile(self, fname, data, mode, copysource):
         islink, isexec = mode
         if data is None:
-            util.setflags(self._join(fname), islink, isexec)
+            self.opener.setflags(fname, islink, isexec)
             return
         if islink:
             self.opener.symlink(data, fname)
         else:
             self.opener.write(fname, data)
             if isexec:
-                util.setflags(self._join(fname), False, True)
+                self.opener.setflags(fname, False, True)
 
     def unlink(self, fname):
-        util.unlinkpath(self._join(fname), ignoremissing=True)
+        self.opener.unlinkpath(fname, ignoremissing=True)
 
     def writerej(self, fname, failed, total, lines):
         fname = fname + ".rej"
@@ -453,7 +453,7 @@
         fp.close()
 
     def exists(self, fname):
-        return os.path.lexists(self._join(fname))
+        return self.opener.lexists(fname)
 
 class workingbackend(fsbackend):
     def __init__(self, ui, repo, similarity):
@@ -1521,14 +1521,11 @@
     patcher = ui.config('ui', 'patch')
     if files is None:
         files = set()
-    try:
-        if patcher:
-            return _externalpatch(ui, repo, patcher, patchname, strip,
-                                  files, similarity)
-        return internalpatch(ui, repo, patchname, strip, files, eolmode,
-                             similarity)
-    except PatchError, err:
-        raise util.Abort(str(err))
+    if patcher:
+        return _externalpatch(ui, repo, patcher, patchname, strip,
+                              files, similarity)
+    return internalpatch(ui, repo, patchname, strip, files, eolmode,
+                         similarity)
 
 def changedfiles(ui, repo, patchpath, strip=1):
     backend = fsbackend(ui, repo.root)
--- a/mercurial/pushkey.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/mercurial/pushkey.py	Sat Jun 14 11:56:20 2014 -0500
@@ -5,7 +5,7 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-import bookmarks, phases, obsolete
+import bookmarks, phases, obsolete, encoding
 
 def _nslist(repo):
     n = {}
@@ -37,3 +37,18 @@
     lk = _get(namespace)[1]
     return lk(repo)
 
+encode = encoding.fromlocal
+
+decode = encoding.tolocal
+
+def encodekeys(keys):
+    """encode the content of a pushkey namespace for exchange over the wire"""
+    return '\n'.join(['%s\t%s' % (encode(k), encode(v)) for k, v in keys])
+
+def decodekeys(data):
+    """decode the content of a pushkey namespace from exchange over the wire"""
+    result = {}
+    for l in data.splitlines():
+        k, v = l.split('\t')
+        result[decode(k)] = decode(v)
+    return result
--- a/mercurial/py3kcompat.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/mercurial/py3kcompat.py	Sat Jun 14 11:56:20 2014 -0500
@@ -5,7 +5,7 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-import os, builtins
+import builtins
 
 from numbers import Number
 
@@ -52,13 +52,6 @@
     return ret.encode('utf-8', 'surrogateescape')
 builtins.bytesformatter = bytesformatter
 
-# Create bytes equivalents for os.environ values
-for key in list(os.environ.keys()):
-    # UTF-8 is fine for us
-    bkey = key.encode('utf-8', 'surrogateescape')
-    bvalue = os.environ[key].encode('utf-8', 'surrogateescape')
-    os.environ[bkey] = bvalue
-
 origord = builtins.ord
 def fakeord(char):
     if isinstance(char, int):
--- a/mercurial/revset.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/mercurial/revset.py	Sat Jun 14 11:56:20 2014 -0500
@@ -2764,10 +2764,6 @@
         if self._start < self._end:
             self.reverse()
 
-    def _contained(self, rev):
-        return (rev <= self._start and rev > self._end) or (rev >= self._start
-                and rev < self._end)
-
     def __iter__(self):
         if self._start <= self._end:
             iterrange = xrange(self._start, self._end)
@@ -2825,7 +2821,7 @@
             start = self._start
             end = self._end
             for rev in self._hiddenrevs:
-                if (end < rev <= start) or (start <= rev and rev < end):
+                if (end < rev <= start) or (start <= rev < end):
                     count += 1
             return abs(self._end - self._start) - count
 
--- a/mercurial/scmutil.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/mercurial/scmutil.py	Sat Jun 14 11:56:20 2014 -0500
@@ -178,6 +178,9 @@
     def islink(self, path=None):
         return os.path.islink(self.join(path))
 
+    def lexists(self, path=None):
+        return os.path.lexists(self.join(path))
+
     def lstat(self, path=None):
         return os.lstat(self.join(path))
 
@@ -223,6 +226,9 @@
     def unlink(self, path=None):
         return util.unlink(self.join(path))
 
+    def unlinkpath(self, path=None, ignoremissing=False):
+        return util.unlinkpath(self.join(path), ignoremissing)
+
     def utime(self, path=None, t=None):
         return os.utime(self.join(path), t)
 
--- a/mercurial/simplemerge.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/mercurial/simplemerge.py	Sat Jun 14 11:56:20 2014 -0500
@@ -416,11 +416,11 @@
     name_a = local
     name_b = other
     labels = opts.get('label', [])
-    if labels:
-        name_a = labels.pop(0)
-    if labels:
-        name_b = labels.pop(0)
-    if labels:
+    if len(labels) > 0:
+        name_a = labels[0]
+    if len(labels) > 1:
+        name_b = labels[1]
+    if len(labels) > 2:
         raise util.Abort(_("can only specify two labels."))
 
     try:
--- a/mercurial/subrepo.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/mercurial/subrepo.py	Sat Jun 14 11:56:20 2014 -0500
@@ -205,12 +205,13 @@
                 sm[s] = r
             else:
                 debug(s, "both sides changed")
+                srepo = wctx.sub(s)
                 option = repo.ui.promptchoice(
                     _(' subrepository %s diverged (local revision: %s, '
                       'remote revision: %s)\n'
                       '(M)erge, keep (l)ocal or keep (r)emote?'
                       '$$ &Merge $$ &Local $$ &Remote')
-                    % (s, l[1][:12], r[1][:12]), 0)
+                    % (s, srepo.shortid(l[1]), srepo.shortid(r[1])), 0)
                 if option == 0:
                     wctx.sub(s).merge(r)
                     sm[s] = l
@@ -502,6 +503,9 @@
             % (substate[0], substate[2]))
         return []
 
+    def shortid(self, revid):
+        return revid
+
 class hgsubrepo(abstractsubrepo):
     def __init__(self, ctx, path, state):
         self._path = path
@@ -867,6 +871,9 @@
             pats = []
         cmdutil.revert(ui, self._repo, ctx, parents, *pats, **opts)
 
+    def shortid(self, revid):
+        return revid[:12]
+
 class svnsubrepo(abstractsubrepo):
     def __init__(self, ctx, path, state):
         self._path = path
@@ -1563,6 +1570,9 @@
         deleted = unknown = ignored = clean = []
         return modified, added, removed, deleted, unknown, ignored, clean
 
+    def shortid(self, revid):
+        return revid[:7]
+
 types = {
     'hg': hgsubrepo,
     'svn': svnsubrepo,
--- a/mercurial/templates/atom/changelogentry.tmpl	Sat Jun 14 11:49:02 2014 -0500
+++ b/mercurial/templates/atom/changelogentry.tmpl	Sat Jun 14 11:56:20 2014 -0500
@@ -32,7 +32,7 @@
 	</tr>
 	<tr>
 		<th style="text-align:left;vertical-align:top;">description</th>
-		<td>{desc|strip|escape|addbreaks|nonempty}</td>
+		<td>{desc|strip|escape|websub|addbreaks|nonempty}</td>
 	</tr>
 	<tr>
 		<th style="text-align:left;vertical-align:top;">files</th>
--- a/mercurial/templates/rss/changelogentry.tmpl	Sat Jun 14 11:49:02 2014 -0500
+++ b/mercurial/templates/rss/changelogentry.tmpl	Sat Jun 14 11:56:20 2014 -0500
@@ -27,7 +27,7 @@
 	</tr>
 	<tr>
 		<th style="text-align:left;vertical-align:top;">description</th>
-		<td>{desc|strip|escape|addbreaks|nonempty}</td>
+		<td>{desc|strip|escape|websub|addbreaks|nonempty}</td>
 	</tr>
 	<tr>
 		<th style="text-align:left;vertical-align:top;">files</th>
--- a/mercurial/templates/rss/filelogentry.tmpl	Sat Jun 14 11:49:02 2014 -0500
+++ b/mercurial/templates/rss/filelogentry.tmpl	Sat Jun 14 11:56:20 2014 -0500
@@ -1,7 +1,7 @@
 <item>
     <title>{desc|strip|firstline|strip|escape}</title>
     <link>{urlbase}{url|urlescape}log{node|short}/{file|urlescape}</link>
-    <description><![CDATA[{desc|strip|escape|addbreaks|nonempty}]]></description>
+    <description><![CDATA[{desc|strip|escape|websub|addbreaks|nonempty}]]></description>
     <author>{author|obfuscate}</author>
     <pubDate>{date|rfc822date}</pubDate>
 </item>
--- a/mercurial/wireproto.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/mercurial/wireproto.py	Sat Jun 14 11:56:20 2014 -0500
@@ -8,7 +8,7 @@
 import urllib, tempfile, os, sys
 from i18n import _
 from node import bin, hex
-import changegroup as changegroupmod, bundle2
+import changegroup as changegroupmod, bundle2, pushkey as pushkeymod
 import peer, error, encoding, util, store, exchange
 
 
@@ -190,6 +190,21 @@
             .replace(':,', ',')
             .replace('::', ':'))
 
+# mapping of options accepted by getbundle and their types
+#
+# Meant to be extended by extensions. It is extensions responsibility to ensure
+# such options are properly processed in exchange.getbundle.
+#
+# supported types are:
+#
+# :nodes: list of binary nodes
+# :csv:   list of comma-separated values
+# :plain: string with no transformation needed.
+gboptsmap = {'heads':  'nodes',
+             'common': 'nodes',
+             'bundlecaps': 'csv',
+             'listkeys': 'csv'}
+
 # client side
 
 class wirepeer(peer.peerrepository):
@@ -303,11 +318,7 @@
         self.ui.debug('preparing listkeys for "%s"\n' % namespace)
         yield {'namespace': encoding.fromlocal(namespace)}, f
         d = f.value
-        r = {}
-        for l in d.splitlines():
-            k, v = l.split('\t')
-            r[encoding.tolocal(k)] = encoding.tolocal(v)
-        yield r
+        yield pushkeymod.decodekeys(d)
 
     def stream_out(self):
         return self._callstream('stream_out')
@@ -325,18 +336,25 @@
                                    bases=bases, heads=heads)
         return changegroupmod.unbundle10(f, 'UN')
 
-    def getbundle(self, source, heads=None, common=None, bundlecaps=None,
-                  **kwargs):
+    def getbundle(self, source, **kwargs):
         self.requirecap('getbundle', _('look up remote changes'))
         opts = {}
-        if heads is not None:
-            opts['heads'] = encodelist(heads)
-        if common is not None:
-            opts['common'] = encodelist(common)
-        if bundlecaps is not None:
-            opts['bundlecaps'] = ','.join(bundlecaps)
-        opts.update(kwargs)
+        for key, value in kwargs.iteritems():
+            if value is None:
+                continue
+            keytype = gboptsmap.get(key)
+            if keytype is None:
+                assert False, 'unexpected'
+            elif keytype == 'nodes':
+                value = encodelist(value)
+            elif keytype == 'csv':
+                value = ','.join(value)
+            elif keytype != 'plain':
+                raise KeyError('unknown getbundle option type %s'
+                               % keytype)
+            opts[key] = value
         f = self._callcompressable("getbundle", **opts)
+        bundlecaps = kwargs.get('bundlecaps')
         if bundlecaps is not None and 'HG2X' in bundlecaps:
             return bundle2.unbundle20(self.ui, f)
         else:
@@ -489,7 +507,7 @@
             opts[k] = others[k]
             del others[k]
     if others:
-        sys.stderr.write("abort: %s got unexpected arguments %s\n"
+        sys.stderr.write("warning: %s ignored unexpected arguments %s\n"
                          % (cmd, ",".join(others)))
     return opts
 
@@ -627,12 +645,16 @@
 
 @wireprotocommand('getbundle', '*')
 def getbundle(repo, proto, others):
-    opts = options('getbundle', gboptslist, others)
+    opts = options('getbundle', gboptsmap.keys(), others)
     for k, v in opts.iteritems():
-        if k in ('heads', 'common'):
+        keytype = gboptsmap[k]
+        if keytype == 'nodes':
             opts[k] = decodelist(v)
-        elif k == 'bundlecaps':
+        elif keytype == 'csv':
             opts[k] = set(v.split(','))
+        elif keytype != 'plain':
+            raise KeyError('unknown getbundle option type %s'
+                           % keytype)
     cg = exchange.getbundle(repo, 'serve', **opts)
     return streamres(proto.groupchunks(cg))
 
@@ -655,9 +677,7 @@
 @wireprotocommand('listkeys', 'namespace')
 def listkeys(repo, proto, namespace):
     d = repo.listkeys(encoding.tolocal(namespace)).items()
-    t = '\n'.join(['%s\t%s' % (encoding.fromlocal(k), encoding.fromlocal(v))
-                   for k, v in d])
-    return t
+    return pushkeymod.encodekeys(d)
 
 @wireprotocommand('lookup', 'key')
 def lookup(repo, proto, key):
@@ -809,11 +829,13 @@
         finally:
             fp.close()
             os.unlink(tempname)
-    except bundle2.UnknownPartError, exc:
+    except error.BundleValueError, exc:
             bundler = bundle2.bundle20(repo.ui)
-            part = bundle2.bundlepart('B2X:ERROR:UNKNOWNPART',
-                                      [('parttype', str(exc))])
-            bundler.addpart(part)
+            errpart = bundler.newpart('B2X:ERROR:UNSUPPORTEDCONTENT')
+            if exc.parttype is not None:
+                errpart.addparam('parttype', exc.parttype)
+            if exc.params:
+                errpart.addparam('params', '\0'.join(exc.params))
             return streamres(bundler.getchunks())
     except util.Abort, inst:
         # The old code we moved used sys.stderr directly.
@@ -835,9 +857,7 @@
     except error.PushRaced, exc:
         if getattr(exc, 'duringunbundle2', False):
             bundler = bundle2.bundle20(repo.ui)
-            part = bundle2.bundlepart('B2X:ERROR:PUSHRACED',
-                                      [('message', str(exc))])
-            bundler.addpart(part)
+            bundler.newpart('B2X:ERROR:PUSHRACED', [('message', str(exc))])
             return streamres(bundler.getchunks())
         else:
             return pusherr(str(exc))
--- a/setup.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/setup.py	Sat Jun 14 11:56:20 2014 -0500
@@ -513,7 +513,7 @@
         version = version[0]
         xcode4 = (version.startswith('Xcode') and
                   StrictVersion(version.split()[1]) >= StrictVersion('4.0'))
-        xcode51 = re.match(r'^Xcode\s+5\.1\.', version) is not None
+        xcode51 = re.match(r'^Xcode\s+5\.1', version) is not None
     else:
         # xcodebuild returns empty on OS X Lion with XCode 4.3 not
         # installed, but instead with only command-line tools. Assume
--- a/tests/autodiff.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/autodiff.py	Sat Jun 14 11:56:20 2014 -0500
@@ -1,8 +1,14 @@
 # Extension dedicated to test patch.diff() upgrade modes
 #
 #
-from mercurial import scmutil, patch, util
+from mercurial import cmdutil, scmutil, patch, util
 
+cmdtable = {}
+command = cmdutil.command(cmdtable)
+
+@command('autodiff',
+    [('', 'git', '', 'git upgrade mode (yes/no/auto/warn/abort)')],
+    '[OPTION]... [FILE]...')
 def autodiff(ui, repo, *pats, **opts):
     diffopts = patch.diffopts(ui, opts)
     git = opts.get('git', 'no')
@@ -36,11 +42,3 @@
         ui.write(chunk)
     for fn in sorted(brokenfiles):
         ui.write(('data lost for: %s\n' % fn))
-
-cmdtable = {
-    "autodiff":
-        (autodiff,
-         [('', 'git', '', 'git upgrade mode (yes/no/auto/warn/abort)'),
-          ],
-         '[OPTION]... [FILE]...'),
-}
--- a/tests/filterpyflakes.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/filterpyflakes.py	Sat Jun 14 11:56:20 2014 -0500
@@ -29,12 +29,15 @@
 for line in sys.stdin:
     # We whitelist tests (see more messages in pyflakes.messages)
     pats = [
-            r"imported but unused",
-            r"local variable '.*' is assigned to but never used",
-            r"unable to detect undefined names",
+            (r"imported but unused", None),
+            (r"local variable '.*' is assigned to but never used", None),
+            (r"unable to detect undefined names", None),
+            (r"undefined name '.*'",
+             r"undefined name '(WindowsError|memoryview)'")
            ]
-    for msgtype, pat in enumerate(pats):
-        if re.search(pat, line):
+
+    for msgtype, (pat, excl) in enumerate(pats):
+        if re.search(pat, line) and (not excl or not re.search(excl, line)):
             break # pattern matches
     else:
         continue # no pattern matched, next line
@@ -49,3 +52,7 @@
 for msgtype, line in sorted(lines, key=makekey):
     sys.stdout.write(line)
 print
+
+# self test of "undefined name" detection for other than 'memoryview'
+if False:
+    print undefinedname
--- a/tests/run-tests.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/run-tests.py	Sat Jun 14 11:56:20 2014 -0500
@@ -57,6 +57,7 @@
 import threading
 import killdaemons as killmod
 import Queue as queue
+import unittest
 
 processlock = threading.Lock()
 
@@ -92,18 +93,12 @@
 
     return p
 
-# reserved exit code to skip test (used by hghave)
-SKIPPED_STATUS = 80
-SKIPPED_PREFIX = 'skipped: '
-FAILED_PREFIX  = 'hghave check failed: '
 PYTHON = sys.executable.replace('\\', '/')
 IMPL_PATH = 'PYTHONPATH'
 if 'java' in sys.platform:
     IMPL_PATH = 'JYTHONPATH'
 
-requiredtools = [os.path.basename(sys.executable), "diff", "grep", "unzip",
-                 "gunzip", "bunzip2", "sed"]
-createdfiles = []
+TESTDIR = HGTMP = INST = BINDIR = TMPBINDIR = PYTHONDIR = None
 
 defaults = {
     'jobs': ('HGTEST_JOBS', 1),
@@ -134,6 +129,7 @@
     return entries
 
 def getparser():
+    """Obtain the OptionParser used by the CLI."""
     parser = optparse.OptionParser("%prog [options] [tests]")
 
     # keep these sorted
@@ -214,6 +210,7 @@
     return parser
 
 def parseargs(args, parser):
+    """Parse arguments with our OptionParser and validate results."""
     (options, args) = parser.parse_args(args)
 
     # jython is always pure
@@ -285,47 +282,33 @@
     shutil.copy(src, dst)
     os.remove(src)
 
-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, ref, err):
-    print
+def getdiff(expected, output, ref, err):
     servefail = False
+    lines = []
     for line in difflib.unified_diff(expected, output, ref, err):
-        sys.stdout.write(line)
+        if line.startswith('+++') or line.startswith('---'):
+            if line.endswith(' \n'):
+                line = line[:-2] + '\n'
+        lines.append(line)
         if not servefail and line.startswith(
                              '+  abort: child process failed to start'):
             servefail = True
-    return {'servefail': servefail}
 
+    return servefail, lines
 
 verbose = False
 def vlog(*msg):
-    if verbose is not False:
-        iolock.acquire()
-        if verbose:
-            print verbose,
-        for m in msg:
-            print m,
-        print
-        sys.stdout.flush()
-        iolock.release()
+    """Log only when in verbose mode."""
+    if verbose is False:
+        return
+
+    return log(*msg)
 
 def log(*msg):
+    """Log something to stdout.
+
+    Arguments are strings to print.
+    """
     iolock.acquire()
     if verbose:
         print verbose,
@@ -335,80 +318,6 @@
     sys.stdout.flush()
     iolock.release()
 
-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.name == 'nt' or os.access(name, os.X_OK):
-            return name
-    return None
-
-def createhgrc(path, options):
-    # create a fresh hgrc
-    hgrc = open(path, 'w')
-    hgrc.write('[ui]\n')
-    hgrc.write('slash = True\n')
-    hgrc.write('interactive = False\n')
-    hgrc.write('[defaults]\n')
-    hgrc.write('backout = -d "0 0"\n')
-    hgrc.write('commit = -d "0 0"\n')
-    hgrc.write('shelve = --date "0 0"\n')
-    hgrc.write('tag = -d "0 0"\n')
-    if options.extra_config_opt:
-        for opt in options.extra_config_opt:
-            section, key = opt.split('.', 1)
-            assert '=' in key, ('extra config opt %s must '
-                                'have an = for assignment' % opt)
-            hgrc.write('[%s]\n%s\n' % (section, key))
-    hgrc.close()
-
-def createenv(options, testtmp, threadtmp, port):
-    env = os.environ.copy()
-    env['TESTTMP'] = testtmp
-    env['HOME'] = testtmp
-    env["HGPORT"] = str(port)
-    env["HGPORT1"] = str(port + 1)
-    env["HGPORT2"] = str(port + 2)
-    env["HGRCPATH"] = os.path.join(threadtmp, '.hgrc')
-    env["DAEMON_PIDS"] = os.path.join(threadtmp, 'daemon.pids')
-    env["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
-    env["HGMERGE"] = "internal:merge"
-    env["HGUSER"]   = "test"
-    env["HGENCODING"] = "ascii"
-    env["HGENCODINGMODE"] = "strict"
-
-    # Reset some environment variables to well-known values so that
-    # the tests produce repeatable output.
-    env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
-    env['TZ'] = 'GMT'
-    env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
-    env['COLUMNS'] = '80'
-    env['TERM'] = 'xterm'
-
-    for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
-              'NO_PROXY').split():
-        if k in env:
-            del env[k]
-
-    # unset env related to hooks
-    for k in env.keys():
-        if k.startswith('HG_'):
-            del env[k]
-
-    return env
-
-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' and not p.endswith('.exe'):
-            p += '.exe'
-        found = findprogram(p)
-        if found:
-            vlog("# Found prerequisite", p, "at", found)
-        else:
-            print "WARNING: Did not find prerequisite tool: "+p
-
 def terminate(proc):
     """Terminate subprocess (with fallback for Python versions < 2.6)"""
     vlog('# Terminating process %d' % proc.pid)
@@ -421,264 +330,410 @@
     return killmod.killdaemons(pidfile, tryhard=False, remove=True,
                                logfn=vlog)
 
-def cleanup(options):
-    if not options.keep_tmpdir:
-        vlog("# Cleaning up HGTMP", HGTMP)
-        shutil.rmtree(HGTMP, True)
-        for f in createdfiles:
-            try:
-                os.remove(f)
-            except OSError:
-                pass
+class Test(unittest.TestCase):
+    """Encapsulates a single, runnable test.
+
+    While this class conforms to the unittest.TestCase API, it differs in that
+    instances need to be instantiated manually. (Typically, unittest.TestCase
+    classes are instantiated automatically by scanning modules.)
+    """
+
+    # Status code reserved for skipped tests (used by hghave).
+    SKIPPED_STATUS = 80
+
+    def __init__(self, path, tmpdir, keeptmpdir=False,
+                 debug=False,
+                 timeout=defaults['timeout'],
+                 startport=defaults['port'], extraconfigopts=None,
+                 py3kwarnings=False, shell=None):
+        """Create a test from parameters.
 
-def usecorrectpython():
-    # some tests run python interpreter. they must use same
-    # interpreter we use or bad things will happen.
-    pyexename = sys.platform == 'win32' and 'python.exe' or 'python'
-    if getattr(os, 'symlink', None):
-        vlog("# Making python executable in test path a symlink to '%s'" %
-             sys.executable)
-        mypython = os.path.join(TMPBINDIR, pyexename)
-        try:
-            if os.readlink(mypython) == sys.executable:
-                return
-            os.unlink(mypython)
-        except OSError, err:
-            if err.errno != errno.ENOENT:
-                raise
-        if findprogram(pyexename) != sys.executable:
-            try:
-                os.symlink(sys.executable, mypython)
-                createdfiles.append(mypython)
-            except OSError, err:
-                # child processes may race, which is harmless
-                if err.errno != errno.EEXIST:
-                    raise
-    else:
-        exedir, exename = os.path.split(sys.executable)
-        vlog("# Modifying search path to find %s as %s in '%s'" %
-             (exename, pyexename, exedir))
-        path = os.environ['PATH'].split(os.pathsep)
-        while exedir in path:
-            path.remove(exedir)
-        os.environ['PATH'] = os.pathsep.join([exedir] + path)
-        if not findprogram(pyexename):
-            print "WARNING: Cannot find %s in search path" % pyexename
+        path is the full path to the file defining the test.
+
+        tmpdir is the main temporary directory to use for this test.
+
+        keeptmpdir determines whether to keep the test's temporary directory
+        after execution. It defaults to removal (False).
 
-def installhg(options):
-    vlog("# Performing temporary installation of HG")
-    installerrs = os.path.join("tests", "install.err")
-    compiler = ''
-    if options.compiler:
-        compiler = '--compiler ' + options.compiler
-    pure = options.pure and "--pure" or ""
-    py3 = ''
-    if sys.version_info[0] == 3:
-        py3 = '--c2to3'
+        debug mode will make the test execute verbosely, with unfiltered
+        output.
+
+        timeout controls the maximum run time of the test. It is ignored when
+        debug is True.
+
+        startport controls the starting port number to use for this test. Each
+        test will reserve 3 port numbers for execution. It is the caller's
+        responsibility to allocate a non-overlapping port range to Test
+        instances.
 
-    # Run installer in hg root
-    script = os.path.realpath(sys.argv[0])
-    hgroot = os.path.dirname(os.path.dirname(script))
-    os.chdir(hgroot)
-    nohome = '--home=""'
-    if os.name == 'nt':
-        # The --home="" trick works only on OS where os.sep == '/'
-        # because of a distutils convert_path() fast-path. Avoid it at
-        # least on Windows for now, deal with .pydistutils.cfg bugs
-        # when they happen.
-        nohome = ''
-    cmd = ('%(exe)s setup.py %(py3)s %(pure)s clean --all'
-           ' build %(compiler)s --build-base="%(base)s"'
-           ' install --force --prefix="%(prefix)s" --install-lib="%(libdir)s"'
-           ' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
-           % {'exe': sys.executable, 'py3': py3, 'pure': pure,
-              'compiler': compiler, 'base': os.path.join(HGTMP, "build"),
-              'prefix': INST, 'libdir': PYTHONDIR, 'bindir': BINDIR,
-              'nohome': nohome, 'logfile': 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)
+        extraconfigopts is an iterable of extra hgrc config options. Values
+        must have the form "key=value" (something understood by hgrc). Values
+        of the form "foo.key=value" will result in "[foo] key=value".
+
+        py3kwarnings enables Py3k warnings.
+
+        shell is the shell to execute tests in.
+        """
+
+        self.path = path
+        self.name = os.path.basename(path)
+        self._testdir = os.path.dirname(path)
+        self.errpath = os.path.join(self._testdir, '%s.err' % self.name)
 
-    usecorrectpython()
-
-    if options.py3k_warnings and not options.anycoverage:
-        vlog("# Updating hg command to enable Py3k Warnings switch")
-        f = open(os.path.join(BINDIR, 'hg'), 'r')
-        lines = [line.rstrip() for line in f]
-        lines[0] += ' -3'
-        f.close()
-        f = open(os.path.join(BINDIR, 'hg'), 'w')
-        for line in lines:
-            f.write(line + '\n')
-        f.close()
+        self._threadtmp = tmpdir
+        self._keeptmpdir = keeptmpdir
+        self._debug = debug
+        self._timeout = timeout
+        self._startport = startport
+        self._extraconfigopts = extraconfigopts or []
+        self._py3kwarnings = py3kwarnings
+        self._shell = shell
 
-    hgbat = os.path.join(BINDIR, 'hg.bat')
-    if os.path.isfile(hgbat):
-        # hg.bat expects to be put in bin/scripts while run-tests.py
-        # installation layout put it in bin/ directly. Fix it
-        f = open(hgbat, 'rb')
-        data = f.read()
-        f.close()
-        if '"%~dp0..\python" "%~dp0hg" %*' in data:
-            data = data.replace('"%~dp0..\python" "%~dp0hg" %*',
-                                '"%~dp0python" "%~dp0hg" %*')
-            f = open(hgbat, 'wb')
-            f.write(data)
+        self._aborted = False
+        self._daemonpids = []
+        self._finished = None
+        self._ret = None
+        self._out = None
+        self._skipped = None
+        self._testtmp = None
+
+        # If we're not in --debug mode and reference output file exists,
+        # check test output against it.
+        if debug:
+            self._refout = None # to match "out is None"
+        elif os.path.exists(self.refpath):
+            f = open(self.refpath, 'r')
+            self._refout = f.read().splitlines(True)
             f.close()
         else:
-            print 'WARNING: cannot fix hg.bat reference to python.exe'
+            self._refout = []
+
+    def __str__(self):
+        return self.name
+
+    def shortDescription(self):
+        return self.name
 
-    if options.anycoverage:
-        custom = os.path.join(TESTDIR, 'sitecustomize.py')
-        target = os.path.join(PYTHONDIR, 'sitecustomize.py')
-        vlog('# Installing coverage trigger to %s' % target)
-        shutil.copyfile(custom, target)
-        rc = os.path.join(TESTDIR, '.coveragerc')
-        vlog('# Installing coverage rc to %s' % rc)
-        os.environ['COVERAGE_PROCESS_START'] = rc
-        fn = os.path.join(INST, '..', '.coverage')
-        os.environ['COVERAGE_FILE'] = fn
+    def setUp(self):
+        """Tasks to perform before run()."""
+        self._finished = False
+        self._ret = None
+        self._out = None
+        self._skipped = None
+
+        try:
+            os.mkdir(self._threadtmp)
+        except OSError, e:
+            if e.errno != errno.EEXIST:
+                raise
+
+        self._testtmp = os.path.join(self._threadtmp,
+                                     os.path.basename(self.path))
+        os.mkdir(self._testtmp)
+
+        # Remove any previous output files.
+        if os.path.exists(self.errpath):
+            os.remove(self.errpath)
 
-def outputtimes(options):
-    vlog('# Producing time report')
-    times.sort(key=lambda t: (t[1], t[0]), reverse=True)
-    cols = '%7.3f   %s'
-    print '\n%-7s   %s' % ('Time', 'Test')
-    for test, timetaken in times:
-        print cols % (timetaken, test)
+    def run(self, result):
+        """Run this test and report results against a TestResult instance."""
+        # This function is extremely similar to unittest.TestCase.run(). Once
+        # we require Python 2.7 (or at least its version of unittest), this
+        # function can largely go away.
+        self._result = result
+        result.startTest(self)
+        try:
+            try:
+                self.setUp()
+            except (KeyboardInterrupt, SystemExit):
+                self._aborted = True
+                raise
+            except Exception:
+                result.addError(self, sys.exc_info())
+                return
 
-def outputcoverage(options):
-
-    vlog('# Producing coverage report')
-    os.chdir(PYTHONDIR)
+            success = False
+            try:
+                self.runTest()
+            except KeyboardInterrupt:
+                self._aborted = True
+                raise
+            except SkipTest, e:
+                result.addSkip(self, str(e))
+            except IgnoreTest, e:
+                result.addIgnore(self, str(e))
+            except WarnTest, e:
+                result.addWarn(self, str(e))
+            except self.failureException, e:
+                # This differs from unittest in that we don't capture
+                # the stack trace. This is for historical reasons and
+                # this decision could be revisted in the future,
+                # especially for PythonTest instances.
+                if result.addFailure(self, str(e)):
+                    success = True
+            except Exception:
+                result.addError(self, sys.exc_info())
+            else:
+                success = True
 
-    def covrun(*args):
-        cmd = 'coverage %s' % ' '.join(args)
-        vlog('# Running: %s' % cmd)
-        os.system(cmd)
+            try:
+                self.tearDown()
+            except (KeyboardInterrupt, SystemExit):
+                self._aborted = True
+                raise
+            except Exception:
+                result.addError(self, sys.exc_info())
+                success = False
 
-    covrun('-c')
-    omit = ','.join(os.path.join(x, '*') for x in [BINDIR, TESTDIR])
-    covrun('-i', '-r', '"--omit=%s"' % omit) # report
-    if options.htmlcov:
-        htmldir = os.path.join(TESTDIR, 'htmlcov')
-        covrun('-i', '-b', '"--directory=%s"' % htmldir, '"--omit=%s"' % omit)
-    if options.annotate:
-        adir = os.path.join(TESTDIR, 'annotated')
-        if not os.path.isdir(adir):
-            os.mkdir(adir)
-        covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
+            if success:
+                result.addSuccess(self)
+        finally:
+            result.stopTest(self, interrupted=self._aborted)
+
+    def runTest(self):
+        """Run this test instance.
+
+        This will return a tuple describing the result of the test.
+        """
+        replacements = self._getreplacements()
+        env = self._getenv()
+        self._daemonpids.append(env['DAEMON_PIDS'])
+        self._createhgrc(env['HGRCPATH'])
+
+        vlog('# Test', self.name)
+
+        ret, out = self._run(replacements, env)
+        self._finished = True
+        self._ret = ret
+        self._out = out
+
+        def describe(ret):
+            if ret < 0:
+                return 'killed by signal: %d' % -ret
+            return 'returned error code %d' % ret
+
+        self._skipped = False
+
+        if ret == self.SKIPPED_STATUS:
+            if out is None: # Debug mode, nothing to parse.
+                missing = ['unknown']
+                failed = None
+            else:
+                missing, failed = TTest.parsehghaveoutput(out)
+
+            if not missing:
+                missing = ['irrelevant']
 
-def pytest(test, wd, options, replacements, env):
-    py3kswitch = options.py3k_warnings and ' -3' or ''
-    cmd = '%s%s "%s"' % (PYTHON, py3kswitch, test)
-    vlog("# Running", cmd)
-    if os.name == 'nt':
-        replacements.append((r'\r\n', '\n'))
-    return run(cmd, wd, options, replacements, env)
+            if failed:
+                self.fail('hg have failed checking for %s' % failed[-1])
+            else:
+                self._skipped = True
+                raise SkipTest(missing[-1])
+        elif ret == 'timeout':
+            self.fail('timed out')
+        elif ret is False:
+            raise WarnTest('no result code from test')
+        elif out != self._refout:
+            # Diff generation may rely on written .err file.
+            if (ret != 0 or out != self._refout) and not self._skipped \
+                and not self._debug:
+                f = open(self.errpath, 'wb')
+                for line in out:
+                    f.write(line)
+                f.close()
 
-needescape = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
-escapesub = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
-escapemap = dict((chr(i), r'\x%02x' % i) for i in range(256))
-escapemap.update({'\\': '\\\\', '\r': r'\r'})
-def escapef(m):
-    return escapemap[m.group(0)]
-def stringescape(s):
-    return escapesub(escapef, s)
+            # The result object handles diff calculation for us.
+            self._result.addOutputMismatch(self, ret, out, self._refout)
+
+            if ret:
+                msg = 'output changed and ' + describe(ret)
+            else:
+                msg = 'output changed'
 
-def rematch(el, l):
-    try:
-        # use \Z to ensure that the regex matches to the end of the string
-        if os.name == 'nt':
-            return re.match(el + r'\r?\n\Z', l)
-        return re.match(el + r'\n\Z', l)
-    except re.error:
-        # el is an invalid regex
-        return False
+            self.fail(msg)
+        elif ret:
+            self.fail(describe(ret))
+
+    def tearDown(self):
+        """Tasks to perform after run()."""
+        for entry in self._daemonpids:
+            killdaemons(entry)
+        self._daemonpids = []
+
+        if not self._keeptmpdir:
+            shutil.rmtree(self._testtmp, True)
+            shutil.rmtree(self._threadtmp, True)
 
-def globmatch(el, l):
-    # The only supported special characters are * and ? plus / which also
-    # matches \ on windows. Escaping of these characters is supported.
-    if el + '\n' == l:
-        if os.altsep:
-            # matching on "/" is not needed for this line
-            return '-glob'
-        return True
-    i, n = 0, len(el)
-    res = ''
-    while i < n:
-        c = el[i]
-        i += 1
-        if c == '\\' and el[i] in '*?\\/':
-            res += el[i - 1:i + 1]
-            i += 1
-        elif c == '*':
-            res += '.*'
-        elif c == '?':
-            res += '.'
-        elif c == '/' and os.altsep:
-            res += '[/\\\\]'
+        if (self._ret != 0 or self._out != self._refout) and not self._skipped \
+            and not self._debug and self._out:
+            f = open(self.errpath, 'wb')
+            for line in self._out:
+                f.write(line)
+            f.close()
+
+        vlog("# Ret was:", self._ret)
+
+    def _run(self, replacements, env):
+        # This should be implemented in child classes to run tests.
+        raise SkipTest('unknown test type')
+
+    def abort(self):
+        """Terminate execution of this test."""
+        self._aborted = True
+
+    def _getreplacements(self):
+        """Obtain a mapping of text replacements to apply to test output.
+
+        Test output needs to be normalized so it can be compared to expected
+        output. This function defines how some of that normalization will
+        occur.
+        """
+        r = [
+            (r':%s\b' % self._startport, ':$HGPORT'),
+            (r':%s\b' % (self._startport + 1), ':$HGPORT1'),
+            (r':%s\b' % (self._startport + 2), ':$HGPORT2'),
+            ]
+
+        if os.name == 'nt':
+            r.append(
+                (''.join(c.isalpha() and '[%s%s]' % (c.lower(), c.upper()) or
+                    c in '/\\' and r'[/\\]' or c.isdigit() and c or '\\' + c
+                    for c in self._testtmp), '$TESTTMP'))
         else:
-            res += re.escape(c)
-    return rematch(res, l)
+            r.append((re.escape(self._testtmp), '$TESTTMP'))
+
+        return r
+
+    def _getenv(self):
+        """Obtain environment variables to use during test execution."""
+        env = os.environ.copy()
+        env['TESTTMP'] = self._testtmp
+        env['HOME'] = self._testtmp
+        env["HGPORT"] = str(self._startport)
+        env["HGPORT1"] = str(self._startport + 1)
+        env["HGPORT2"] = str(self._startport + 2)
+        env["HGRCPATH"] = os.path.join(self._threadtmp, '.hgrc')
+        env["DAEMON_PIDS"] = os.path.join(self._threadtmp, 'daemon.pids')
+        env["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
+        env["HGMERGE"] = "internal:merge"
+        env["HGUSER"]   = "test"
+        env["HGENCODING"] = "ascii"
+        env["HGENCODINGMODE"] = "strict"
+
+        # Reset some environment variables to well-known values so that
+        # the tests produce repeatable output.
+        env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
+        env['TZ'] = 'GMT'
+        env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
+        env['COLUMNS'] = '80'
+        env['TERM'] = 'xterm'
+
+        for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
+                  'NO_PROXY').split():
+            if k in env:
+                del env[k]
+
+        # unset env related to hooks
+        for k in env.keys():
+            if k.startswith('HG_'):
+                del env[k]
+
+        return env
 
-def linematch(el, l):
-    if el == l: # perfect match (fast)
-        return True
-    if el:
-        if el.endswith(" (esc)\n"):
-            el = el[:-7].decode('string-escape') + '\n'
-        if el == l or os.name == 'nt' and el[:-1] + '\r\n' == l:
-            return True
-        if el.endswith(" (re)\n"):
-            return rematch(el[:-6], l)
-        if el.endswith(" (glob)\n"):
-            return globmatch(el[:-8], l)
-        if os.altsep and l.replace('\\', '/') == el:
-            return '+glob'
-    return False
+    def _createhgrc(self, path):
+        """Create an hgrc file for this test."""
+        hgrc = open(path, 'w')
+        hgrc.write('[ui]\n')
+        hgrc.write('slash = True\n')
+        hgrc.write('interactive = False\n')
+        hgrc.write('[defaults]\n')
+        hgrc.write('backout = -d "0 0"\n')
+        hgrc.write('commit = -d "0 0"\n')
+        hgrc.write('shelve = --date "0 0"\n')
+        hgrc.write('tag = -d "0 0"\n')
+        for opt in self._extraconfigopts:
+            section, key = opt.split('.', 1)
+            assert '=' in key, ('extra config opt %s must '
+                                'have an = for assignment' % opt)
+            hgrc.write('[%s]\n%s\n' % (section, key))
+        hgrc.close()
+
+    def fail(self, msg):
+        # unittest differentiates between errored and failed.
+        # Failed is denoted by AssertionError (by default at least).
+        raise AssertionError(msg)
+
+class PythonTest(Test):
+    """A Python-based test."""
+
+    @property
+    def refpath(self):
+        return os.path.join(self._testdir, '%s.out' % self.name)
+
+    def _run(self, replacements, env):
+        py3kswitch = self._py3kwarnings and ' -3' or ''
+        cmd = '%s%s "%s"' % (PYTHON, py3kswitch, self.path)
+        vlog("# Running", cmd)
+        if os.name == 'nt':
+            replacements.append((r'\r\n', '\n'))
+        result = run(cmd, self._testtmp, replacements, env,
+                   debug=self._debug, timeout=self._timeout)
+        if self._aborted:
+            raise KeyboardInterrupt()
+
+        return result
+
+class TTest(Test):
+    """A "t test" is a test backed by a .t file."""
 
-def tsttest(test, wd, options, replacements, env):
-    # We generate a shell script which outputs unique markers to line
-    # up script results with our source. These markers include input
-    # line number and the last return code
-    salt = "SALT" + str(time.time())
-    def addsalt(line, inpython):
-        if inpython:
-            script.append('%s %d 0\n' % (salt, line))
-        else:
-            script.append('echo %s %s $?\n' % (salt, line))
+    SKIPPED_PREFIX = 'skipped: '
+    FAILED_PREFIX = 'hghave check failed: '
+    NEEDESCAPE = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
+
+    ESCAPESUB = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
+    ESCAPEMAP = dict((chr(i), r'\x%02x' % i) for i in range(256))
+    ESCAPEMAP.update({'\\': '\\\\', '\r': r'\r'})
+
+    @property
+    def refpath(self):
+        return os.path.join(self._testdir, self.name)
+
+    def _run(self, replacements, env):
+        f = open(self.path)
+        lines = f.readlines()
+        f.close()
+
+        salt, script, after, expected = self._parsetest(lines)
 
-    # After we run the shell script, we re-unify the script output
-    # with non-active parts of the source, with synchronization by our
-    # SALT line number markers. The after table contains the
-    # non-active components, ordered by line number
-    after = {}
-    pos = prepos = -1
+        # Write out the generated script.
+        fname = '%s.sh' % self._testtmp
+        f = open(fname, 'w')
+        for l in script:
+            f.write(l)
+        f.close()
 
-    # Expected shell script output
-    expected = {}
+        cmd = '%s "%s"' % (self._shell, fname)
+        vlog("# Running", cmd)
+
+        exitcode, output = run(cmd, self._testtmp, replacements, env,
+                               debug=self._debug, timeout=self._timeout)
 
-    # We keep track of whether or not we're in a Python block so we
-    # can generate the surrounding doctest magic
-    inpython = False
+        if self._aborted:
+            raise KeyboardInterrupt()
+
+        # Do not merge output if skipped. Return hghave message instead.
+        # Similarly, with --debug, output is None.
+        if exitcode == self.SKIPPED_STATUS or output is None:
+            return exitcode, output
 
-    # True or False when in a true or false conditional section
-    skipping = None
+        return self._processoutput(exitcode, output, salt, after, expected)
 
-    def hghave(reqs):
-        # TODO: do something smarter when all other uses of hghave is gone
-        tdir = TESTDIR.replace('\\', '/')
+    def _hghave(self, reqs):
+        # TODO do something smarter when all other uses of hghave are gone.
+        tdir = self._testdir.replace('\\', '/')
         proc = Popen4('%s -c "%s/hghave %s"' %
-                      (options.shell, tdir, ' '.join(reqs)), wd, 0)
+                      (self._shell, tdir, ' '.join(reqs)),
+                      self._testtmp, 0)
         stdout, stderr = proc.communicate()
         ret = proc.wait()
         if wifexited(ret):
@@ -686,172 +741,271 @@
         if ret == 2:
             print stdout
             sys.exit(1)
+
         return ret == 0
 
-    f = open(test)
-    t = f.readlines()
-    f.close()
+    def _parsetest(self, lines):
+        # We generate a shell script which outputs unique markers to line
+        # up script results with our source. These markers include input
+        # line number and the last return code.
+        salt = "SALT" + str(time.time())
+        def addsalt(line, inpython):
+            if inpython:
+                script.append('%s %d 0\n' % (salt, line))
+            else:
+                script.append('echo %s %s $?\n' % (salt, line))
+
+        script = []
+
+        # After we run the shell script, we re-unify the script output
+        # with non-active parts of the source, with synchronization by our
+        # SALT line number markers. The after table contains the non-active
+        # components, ordered by line number.
+        after = {}
+
+        # Expected shell script output.
+        expected = {}
+
+        pos = prepos = -1
+
+        # True or False when in a true or false conditional section
+        skipping = None
+
+        # We keep track of whether or not we're in a Python block so we
+        # can generate the surrounding doctest magic.
+        inpython = False
+
+        if self._debug:
+            script.append('set -x\n')
+        if os.getenv('MSYSTEM'):
+            script.append('alias pwd="pwd -W"\n')
 
-    script = []
-    if options.debug:
-        script.append('set -x\n')
-    if os.getenv('MSYSTEM'):
-        script.append('alias pwd="pwd -W"\n')
-    n = 0
-    for n, l in enumerate(t):
-        if not l.endswith('\n'):
-            l += '\n'
-        if l.startswith('#if'):
-            lsplit = l.split()
-            if len(lsplit) < 2 or lsplit[0] != '#if':
-                after.setdefault(pos, []).append('  !!! invalid #if\n')
-            if skipping is not None:
-                after.setdefault(pos, []).append('  !!! nested #if\n')
-            skipping = not hghave(lsplit[1:])
-            after.setdefault(pos, []).append(l)
-        elif l.startswith('#else'):
-            if skipping is None:
-                after.setdefault(pos, []).append('  !!! missing #if\n')
-            skipping = not skipping
-            after.setdefault(pos, []).append(l)
-        elif l.startswith('#endif'):
-            if skipping is None:
-                after.setdefault(pos, []).append('  !!! missing #if\n')
-            skipping = None
-            after.setdefault(pos, []).append(l)
-        elif skipping:
-            after.setdefault(pos, []).append(l)
-        elif l.startswith('  >>> '): # python inlines
-            after.setdefault(pos, []).append(l)
-            prepos = pos
-            pos = n
-            if not inpython:
-                # we've just entered a Python block, add the header
-                inpython = True
-                addsalt(prepos, False) # make sure we report the exit code
-                script.append('%s -m heredoctest <<EOF\n' % PYTHON)
-            addsalt(n, True)
-            script.append(l[2:])
-        elif l.startswith('  ... '): # python inlines
-            after.setdefault(prepos, []).append(l)
-            script.append(l[2:])
-        elif l.startswith('  $ '): # commands
-            if inpython:
-                script.append("EOF\n")
-                inpython = False
-            after.setdefault(pos, []).append(l)
-            prepos = pos
-            pos = n
-            addsalt(n, False)
-            cmd = l[4:].split()
-            if len(cmd) == 2 and cmd[0] == 'cd':
-                l = '  $ cd %s || exit 1\n' % cmd[1]
-            script.append(l[4:])
-        elif l.startswith('  > '): # continuations
-            after.setdefault(prepos, []).append(l)
-            script.append(l[4:])
-        elif l.startswith('  '): # results
-            # queue up a list of expected results
-            expected.setdefault(pos, []).append(l[2:])
-        else:
-            if inpython:
-                script.append("EOF\n")
-                inpython = False
-            # non-command/result - queue up for merged output
-            after.setdefault(pos, []).append(l)
+        for n, l in enumerate(lines):
+            if not l.endswith('\n'):
+                l += '\n'
+            if l.startswith('#if'):
+                lsplit = l.split()
+                if len(lsplit) < 2 or lsplit[0] != '#if':
+                    after.setdefault(pos, []).append('  !!! invalid #if\n')
+                if skipping is not None:
+                    after.setdefault(pos, []).append('  !!! nested #if\n')
+                skipping = not self._hghave(lsplit[1:])
+                after.setdefault(pos, []).append(l)
+            elif l.startswith('#else'):
+                if skipping is None:
+                    after.setdefault(pos, []).append('  !!! missing #if\n')
+                skipping = not skipping
+                after.setdefault(pos, []).append(l)
+            elif l.startswith('#endif'):
+                if skipping is None:
+                    after.setdefault(pos, []).append('  !!! missing #if\n')
+                skipping = None
+                after.setdefault(pos, []).append(l)
+            elif skipping:
+                after.setdefault(pos, []).append(l)
+            elif l.startswith('  >>> '): # python inlines
+                after.setdefault(pos, []).append(l)
+                prepos = pos
+                pos = n
+                if not inpython:
+                    # We've just entered a Python block. Add the header.
+                    inpython = True
+                    addsalt(prepos, False) # Make sure we report the exit code.
+                    script.append('%s -m heredoctest <<EOF\n' % PYTHON)
+                addsalt(n, True)
+                script.append(l[2:])
+            elif l.startswith('  ... '): # python inlines
+                after.setdefault(prepos, []).append(l)
+                script.append(l[2:])
+            elif l.startswith('  $ '): # commands
+                if inpython:
+                    script.append('EOF\n')
+                    inpython = False
+                after.setdefault(pos, []).append(l)
+                prepos = pos
+                pos = n
+                addsalt(n, False)
+                cmd = l[4:].split()
+                if len(cmd) == 2 and cmd[0] == 'cd':
+                    l = '  $ cd %s || exit 1\n' % cmd[1]
+                script.append(l[4:])
+            elif l.startswith('  > '): # continuations
+                after.setdefault(prepos, []).append(l)
+                script.append(l[4:])
+            elif l.startswith('  '): # results
+                # Queue up a list of expected results.
+                expected.setdefault(pos, []).append(l[2:])
+            else:
+                if inpython:
+                    script.append('EOF\n')
+                    inpython = False
+                # Non-command/result. Queue up for merged output.
+                after.setdefault(pos, []).append(l)
+
+        if inpython:
+            script.append('EOF\n')
+        if skipping is not None:
+            after.setdefault(pos, []).append('  !!! missing #endif\n')
+        addsalt(n + 1, False)
+
+        return salt, script, after, expected
+
+    def _processoutput(self, exitcode, output, salt, after, expected):
+        # Merge the script output back into a unified test.
+        warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
+        if exitcode != 0:
+            warnonly = 3
+
+        pos = -1
+        postout = []
+        for l in output:
+            lout, lcmd = l, None
+            if salt in l:
+                lout, lcmd = l.split(salt, 1)
+
+            if lout:
+                if not lout.endswith('\n'):
+                    lout += ' (no-eol)\n'
 
-    if inpython:
-        script.append("EOF\n")
-    if skipping is not None:
-        after.setdefault(pos, []).append('  !!! missing #endif\n')
-    addsalt(n + 1, False)
+                # Find the expected output at the current position.
+                el = None
+                if expected.get(pos, None):
+                    el = expected[pos].pop(0)
 
-    # Write out the script and execute it
-    name = wd + '.sh'
-    f = open(name, 'w')
-    for l in script:
-        f.write(l)
-    f.close()
+                r = TTest.linematch(el, lout)
+                if isinstance(r, str):
+                    if r == '+glob':
+                        lout = el[:-1] + ' (glob)\n'
+                        r = '' # Warn only this line.
+                    elif r == '-glob':
+                        lout = ''.join(el.rsplit(' (glob)', 1))
+                        r = '' # Warn only this line.
+                    else:
+                        log('\ninfo, unknown linematch result: %r\n' % r)
+                        r = False
+                if r:
+                    postout.append('  ' + el)
+                else:
+                    if self.NEEDESCAPE(lout):
+                        lout = TTest._stringescape('%s (esc)\n' %
+                                                   lout.rstrip('\n'))
+                    postout.append('  ' + lout) # Let diff deal with it.
+                    if r != '': # If line failed.
+                        warnonly = 3 # for sure not
+                    elif warnonly == 1: # Is "not yet" and line is warn only.
+                        warnonly = 2 # Yes do warn.
 
-    cmd = '%s "%s"' % (options.shell, name)
-    vlog("# Running", cmd)
-    exitcode, output = run(cmd, wd, options, replacements, env)
-    # do not merge output if skipped, return hghave message instead
-    # similarly, with --debug, output is None
-    if exitcode == SKIPPED_STATUS or output is None:
-        return exitcode, output
+            if lcmd:
+                # Add on last return code.
+                ret = int(lcmd.split()[1])
+                if ret != 0:
+                    postout.append('  [%s]\n' % ret)
+                if pos in after:
+                    # Merge in non-active test bits.
+                    postout += after.pop(pos)
+                pos = int(lcmd.split()[0])
 
-    # Merge the script output back into a unified test
+        if pos in after:
+            postout += after.pop(pos)
 
-    warnonly = 1 # 1: not yet, 2: yes, 3: for sure not
-    if exitcode != 0: # failure has been reported
-        warnonly = 3 # set to "for sure not"
-    pos = -1
-    postout = []
-    for l in output:
-        lout, lcmd = l, None
-        if salt in l:
-            lout, lcmd = l.split(salt, 1)
+        if warnonly == 2:
+            exitcode = False # Set exitcode to warned.
+
+        return exitcode, postout
 
-        if lout:
-            if not lout.endswith('\n'):
-                lout += ' (no-eol)\n'
+    @staticmethod
+    def rematch(el, l):
+        try:
+            # use \Z to ensure that the regex matches to the end of the string
+            if os.name == 'nt':
+                return re.match(el + r'\r?\n\Z', l)
+            return re.match(el + r'\n\Z', l)
+        except re.error:
+            # el is an invalid regex
+            return False
 
-            # find the expected output at the current position
-            el = None
-            if pos in expected and expected[pos]:
-                el = expected[pos].pop(0)
-
-            r = linematch(el, lout)
-            if isinstance(r, str):
-                if r == '+glob':
-                    lout = el[:-1] + ' (glob)\n'
-                    r = '' # warn only this line
-                elif r == '-glob':
-                    lout = ''.join(el.rsplit(' (glob)', 1))
-                    r = '' # warn only this line
-                else:
-                    log('\ninfo, unknown linematch result: %r\n' % r)
-                    r = False
-            if r:
-                postout.append("  " + el)
+    @staticmethod
+    def globmatch(el, l):
+        # The only supported special characters are * and ? plus / which also
+        # matches \ on windows. Escaping of these characters is supported.
+        if el + '\n' == l:
+            if os.altsep:
+                # matching on "/" is not needed for this line
+                return '-glob'
+            return True
+        i, n = 0, len(el)
+        res = ''
+        while i < n:
+            c = el[i]
+            i += 1
+            if c == '\\' and el[i] in '*?\\/':
+                res += el[i - 1:i + 1]
+                i += 1
+            elif c == '*':
+                res += '.*'
+            elif c == '?':
+                res += '.'
+            elif c == '/' and os.altsep:
+                res += '[/\\\\]'
             else:
-                if needescape(lout):
-                    lout = stringescape(lout.rstrip('\n')) + " (esc)\n"
-                postout.append("  " + lout) # let diff deal with it
-                if r != '': # if line failed
-                    warnonly = 3 # set to "for sure not"
-                elif warnonly == 1: # is "not yet" (and line is warn only)
-                    warnonly = 2 # set to "yes" do warn
+                res += re.escape(c)
+        return TTest.rematch(res, l)
+
+    @staticmethod
+    def linematch(el, l):
+        if el == l: # perfect match (fast)
+            return True
+        if el:
+            if el.endswith(" (esc)\n"):
+                el = el[:-7].decode('string-escape') + '\n'
+            if el == l or os.name == 'nt' and el[:-1] + '\r\n' == l:
+                return True
+            if el.endswith(" (re)\n"):
+                return TTest.rematch(el[:-6], l)
+            if el.endswith(" (glob)\n"):
+                return TTest.globmatch(el[:-8], l)
+            if os.altsep and l.replace('\\', '/') == el:
+                return '+glob'
+        return False
+
+    @staticmethod
+    def parsehghaveoutput(lines):
+        '''Parse hghave log lines.
 
-        if lcmd:
-            # add on last return code
-            ret = int(lcmd.split()[1])
-            if ret != 0:
-                postout.append("  [%s]\n" % ret)
-            if pos in after:
-                # merge in non-active test bits
-                postout += after.pop(pos)
-            pos = int(lcmd.split()[0])
+        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(TTest.SKIPPED_PREFIX):
+                line = line.splitlines()[0]
+                missing.append(line[len(TTest.SKIPPED_PREFIX):])
+            elif line.startswith(TTest.FAILED_PREFIX):
+                line = line.splitlines()[0]
+                failed.append(line[len(TTest.FAILED_PREFIX):])
 
-    if pos in after:
-        postout += after.pop(pos)
+        return missing, failed
 
-    if warnonly == 2:
-        exitcode = False # set exitcode to warned
-    return exitcode, postout
+    @staticmethod
+    def _escapef(m):
+        return TTest.ESCAPEMAP[m.group(0)]
+
+    @staticmethod
+    def _stringescape(s):
+        return TTest.ESCAPESUB(TTest._escapef, s)
+
 
 wifexited = getattr(os, "WIFEXITED", lambda x: False)
-def run(cmd, wd, options, replacements, env):
+def run(cmd, wd, replacements, env, debug=False, timeout=None):
     """Run command in a sub-process, capturing the output (stdout and stderr).
     Return a tuple (exitcode, output).  output is None in debug mode."""
-    # TODO: Use subprocess.Popen if we're running on Python 2.4
-    if options.debug:
+    if debug:
         proc = subprocess.Popen(cmd, shell=True, cwd=wd, env=env)
         ret = proc.wait()
         return (ret, None)
 
-    proc = Popen4(cmd, wd, options.timeout, env)
+    proc = Popen4(cmd, wd, timeout, env)
     def cleanup():
         terminate(proc)
         ret = proc.wait()
@@ -880,442 +1034,786 @@
     if ret:
         killdaemons(env['DAEMON_PIDS'])
 
-    if abort:
-        raise KeyboardInterrupt()
-
     for s, r in replacements:
         output = re.sub(s, r, output)
     return ret, output.splitlines(True)
 
-def runone(options, test, count):
-    '''returns a result element: (code, test, msg)'''
+iolock = threading.Lock()
+
+class SkipTest(Exception):
+    """Raised to indicate that a test is to be skipped."""
+
+class IgnoreTest(Exception):
+    """Raised to indicate that a test is to be ignored."""
+
+class WarnTest(Exception):
+    """Raised to indicate that a test warned."""
+
+class TestResult(unittest._TextTestResult):
+    """Holds results when executing via unittest."""
+    # Don't worry too much about accessing the non-public _TextTestResult.
+    # It is relatively common in Python testing tools.
+    def __init__(self, options, *args, **kwargs):
+        super(TestResult, self).__init__(*args, **kwargs)
+
+        self._options = options
 
-    def skip(msg):
-        if options.verbose:
-            log("\nSkipping %s: %s" % (testpath, msg))
-        return 's', test, msg
+        # unittest.TestResult didn't have skipped until 2.7. We need to
+        # polyfill it.
+        self.skipped = []
+
+        # We have a custom "ignored" result that isn't present in any Python
+        # unittest implementation. It is very similar to skipped. It may make
+        # sense to map it into skip some day.
+        self.ignored = []
+
+        # We have a custom "warned" result that isn't present in any Python
+        # unittest implementation. It is very similar to failed. It may make
+        # sense to map it into fail some day.
+        self.warned = []
+
+        self.times = []
+        self._started = {}
+
+    def addFailure(self, test, reason):
+        self.failures.append((test, reason))
+
+        if self._options.first:
+            self.stop()
+        else:
+            if not self._options.nodiff:
+                self.stream.write('\nERROR: %s output changed\n' % test)
 
-    def fail(msg, ret):
-        warned = ret is False
-        if not options.nodiff:
-            log("\n%s: %s %s" % (warned and 'Warning' or 'ERROR', test, msg))
-        if (not ret and options.interactive
-            and os.path.exists(testpath + ".err")):
-            iolock.acquire()
-            print "Accept this change? [n] ",
-            answer = sys.stdin.readline().strip()
-            iolock.release()
-            if answer.lower() in "y yes".split():
-                if test.endswith(".t"):
-                    rename(testpath + ".err", testpath)
-                else:
-                    rename(testpath + ".err", testpath + ".out")
-                return '.', test, ''
-        return warned and '~' or '!', test, msg
+            if self._options.interactive:
+                iolock.acquire()
+                self.stream.write('Accept this change? [n] ')
+                answer = sys.stdin.readline().strip()
+                iolock.release()
+                if answer.lower() in ('y', 'yes'):
+                    if test.name.endswith('.t'):
+                        rename(test.errpath, test.path)
+                    else:
+                        rename(test.errpath, '%s.out' % test.path)
+                    self.failures.pop()
+                    return 1
+
+            self.stream.write('!')
+
+    def addError(self, *args, **kwargs):
+        super(TestResult, self).addError(*args, **kwargs)
+
+        if self._options.first:
+            self.stop()
 
-    def success():
-        return '.', test, ''
+    # Polyfill.
+    def addSkip(self, test, reason):
+        self.skipped.append((test, reason))
 
-    def ignore(msg):
-        return 'i', test, msg
+        if self.showAll:
+            self.stream.writeln('skipped %s' % reason)
+        else:
+            self.stream.write('s')
+            self.stream.flush()
 
-    def describe(ret):
-        if ret < 0:
-            return 'killed by signal %d' % -ret
-        return 'returned error code %d' % ret
+    def addIgnore(self, test, reason):
+        self.ignored.append((test, reason))
 
-    testpath = os.path.join(TESTDIR, test)
-    err = os.path.join(TESTDIR, test + ".err")
-    lctest = test.lower()
+        if self.showAll:
+            self.stream.writeln('ignored %s' % reason)
+        else:
+            if reason != 'not retesting':
+                self.stream.write('i')
+            self.stream.flush()
 
-    if not os.path.exists(testpath):
-            return skip("doesn't exist")
+    def addWarn(self, test, reason):
+        self.warned.append((test, reason))
 
-    if not (options.whitelisted and test in options.whitelisted):
-        if options.blacklist and test in options.blacklist:
-            return skip("blacklisted")
+        if self._options.first:
+            self.stop()
 
-        if options.retest and not os.path.exists(test + ".err"):
-            return ignore("not retesting")
+        if self.showAll:
+            self.stream.writeln('warned %s' % reason)
+        else:
+            self.stream.write('~')
+            self.stream.flush()
+
+    def addOutputMismatch(self, test, ret, got, expected):
+        """Record a mismatch in test output for a particular test."""
+
+        if self._options.nodiff:
+            return
 
-        if options.keywords:
-            fp = open(test)
-            t = fp.read().lower() + test.lower()
-            fp.close()
-            for k in options.keywords.lower().split():
-                if k in t:
-                    break
-                else:
-                    return ignore("doesn't match keyword")
+        if self._options.view:
+            os.system("%s %s %s" % (self._view, test.refpath, test.errpath))
+        else:
+            failed, lines = getdiff(expected, got,
+                                    test.refpath, test.errpath)
+            if failed:
+                self.addFailure(test, 'diff generation failed')
+            else:
+                self.stream.write('\n')
+                for line in lines:
+                    self.stream.write(line)
+                self.stream.flush()
+
+        if ret or not self._options.interactive or \
+            not os.path.exists(test.errpath):
+            return
 
-    if not os.path.basename(lctest).startswith("test-"):
-        return skip("not a test file")
-    for ext, func, out in testtypes:
-        if lctest.endswith(ext):
-            runner = func
-            ref = os.path.join(TESTDIR, test + out)
-            break
-    else:
-        return skip("unknown test type")
+    def startTest(self, test):
+        super(TestResult, self).startTest(test)
+
+        self._started[test.name] = time.time()
+
+    def stopTest(self, test, interrupted=False):
+        super(TestResult, self).stopTest(test)
 
-    vlog("# Test", test)
+        self.times.append((test.name, time.time() - self._started[test.name]))
+        del self._started[test.name]
+
+        if interrupted:
+            self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
+                test.name, self.times[-1][1]))
+
+class TestSuite(unittest.TestSuite):
+    """Custom unitest TestSuite that knows how to execute Mercurial tests."""
 
-    if os.path.exists(err):
-        os.remove(err)       # Remove any previous output files
+    def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
+                 retest=False, keywords=None, loop=False,
+                 *args, **kwargs):
+        """Create a new instance that can run tests with a configuration.
+
+        testdir specifies the directory where tests are executed from. This
+        is typically the ``tests`` directory from Mercurial's source
+        repository.
+
+        jobs specifies the number of jobs to run concurrently. Each test
+        executes on its own thread. Tests actually spawn new processes, so
+        state mutation should not be an issue.
 
-    # Make a tmp subdirectory to work in
-    threadtmp = os.path.join(HGTMP, "child%d" % count)
-    testtmp = os.path.join(threadtmp, os.path.basename(test))
-    os.mkdir(threadtmp)
-    os.mkdir(testtmp)
+        whitelist and blacklist denote tests that have been whitelisted and
+        blacklisted, respectively. These arguments don't belong in TestSuite.
+        Instead, whitelist and blacklist should be handled by the thing that
+        populates the TestSuite with tests. They are present to preserve
+        backwards compatible behavior which reports skipped tests as part
+        of the results.
+
+        retest denotes whether to retest failed tests. This arguably belongs
+        outside of TestSuite.
+
+        keywords denotes key words that will be used to filter which tests
+        to execute. This arguably belongs outside of TestSuite.
 
-    port = options.port + count * 3
-    replacements = [
-        (r':%s\b' % port, ':$HGPORT'),
-        (r':%s\b' % (port + 1), ':$HGPORT1'),
-        (r':%s\b' % (port + 2), ':$HGPORT2'),
-        ]
-    if os.name == 'nt':
-        replacements.append(
-            (''.join(c.isalpha() and '[%s%s]' % (c.lower(), c.upper()) or
-                     c in '/\\' and r'[/\\]' or
-                     c.isdigit() and c or
-                     '\\' + c
-                     for c in testtmp), '$TESTTMP'))
-    else:
-        replacements.append((re.escape(testtmp), '$TESTTMP'))
+        loop denotes whether to loop over tests forever.
+        """
+        super(TestSuite, self).__init__(*args, **kwargs)
+
+        self._jobs = jobs
+        self._whitelist = whitelist
+        self._blacklist = blacklist
+        self._retest = retest
+        self._keywords = keywords
+        self._loop = loop
 
-    env = createenv(options, testtmp, threadtmp, port)
-    createhgrc(env['HGRCPATH'], options)
+    def run(self, result):
+        # We have a number of filters that need to be applied. We do this
+        # here instead of inside Test because it makes the running logic for
+        # Test simpler.
+        tests = []
+        for test in self._tests:
+            if not os.path.exists(test.path):
+                result.addSkip(test, "Doesn't exist")
+                continue
+
+            if not (self._whitelist and test.name in self._whitelist):
+                if self._blacklist and test.name in self._blacklist:
+                    result.addSkip(test, 'blacklisted')
+                    continue
 
-    starttime = time.time()
-    try:
-        ret, out = runner(testpath, testtmp, options, replacements, env)
-    except KeyboardInterrupt:
-        endtime = time.time()
-        log('INTERRUPTED: %s (after %d seconds)' % (test, endtime - starttime))
-        raise
-    endtime = time.time()
-    times.append((test, endtime - starttime))
-    vlog("# Ret was:", ret)
+                if self._retest and not os.path.exists(test.errpath):
+                    result.addIgnore(test, 'not retesting')
+                    continue
+
+                if self._keywords:
+                    f = open(test.path)
+                    t = f.read().lower() + test.name.lower()
+                    f.close()
+                    ignored = False
+                    for k in self._keywords.lower().split():
+                        if k not in t:
+                            result.addIgnore(test, "doesn't match keyword")
+                            ignored = True
+                            break
+
+                    if ignored:
+                        continue
+
+            tests.append(test)
+
+        runtests = list(tests)
+        done = queue.Queue()
+        running = 0
+
+        def job(test, result):
+            try:
+                test(result)
+                done.put(None)
+            except KeyboardInterrupt:
+                pass
+            except: # re-raises
+                done.put(('!', test, 'run-test raised an error, see traceback'))
+                raise
 
-    killdaemons(env['DAEMON_PIDS'])
+        try:
+            while tests or running:
+                if not done.empty() or running == self._jobs or not tests:
+                    try:
+                        done.get(True, 1)
+                        if result and result.shouldStop:
+                            break
+                    except queue.Empty:
+                        continue
+                    running -= 1
+                if tests and not running == self._jobs:
+                    test = tests.pop(0)
+                    if self._loop:
+                        tests.append(test)
+                    t = threading.Thread(target=job, name=test.name,
+                                         args=(test, result))
+                    t.start()
+                    running += 1
+        except KeyboardInterrupt:
+            for test in runtests:
+                test.abort()
 
-    skipped = (ret == SKIPPED_STATUS)
+        return result
+
+class TextTestRunner(unittest.TextTestRunner):
+    """Custom unittest test runner that uses appropriate settings."""
+
+    def __init__(self, runner, *args, **kwargs):
+        super(TextTestRunner, self).__init__(*args, **kwargs)
+
+        self._runner = runner
+
+    def run(self, test):
+        result = TestResult(self._runner.options, self.stream,
+                            self.descriptions, self.verbosity)
+
+        test(result)
+
+        failed = len(result.failures)
+        warned = len(result.warned)
+        skipped = len(result.skipped)
+        ignored = len(result.ignored)
+
+        self.stream.writeln('')
 
-    # If we're not in --debug mode and reference output file exists,
-    # check test output against it.
-    if options.debug:
-        refout = None                   # to match "out is None"
-    elif os.path.exists(ref):
-        f = open(ref, "r")
-        refout = f.read().splitlines(True)
-        f.close()
-    else:
-        refout = []
+        if not self._runner.options.noskips:
+            for test, msg in result.skipped:
+                self.stream.writeln('Skipped %s: %s' % (test.name, msg))
+        for test, msg in result.warned:
+            self.stream.writeln('Warned %s: %s' % (test.name, msg))
+        for test, msg in result.failures:
+            self.stream.writeln('Failed %s: %s' % (test.name, msg))
+        for test, msg in result.errors:
+            self.stream.writeln('Errored %s: %s' % (test.name, msg))
+
+        self._runner._checkhglib('Tested')
+
+        # When '--retest' is enabled, only failure tests run. At this point
+        # "result.testsRun" holds the count of failure test that has run. But
+        # as while printing output, we have subtracted the skipped and ignored
+        # count from "result.testsRun". Therefore, to make the count remain
+        # the same, we need to add skipped and ignored count in here.
+        if self._runner.options.retest:
+            result.testsRun = result.testsRun + skipped + ignored
+
+        # This differs from unittest's default output in that we don't count
+        # skipped and ignored tests as part of the total test count.
+        self.stream.writeln('# Ran %d tests, %d skipped, %d warned, %d failed.'
+            % (result.testsRun - skipped - ignored,
+               skipped + ignored, warned, failed))
+        if failed:
+            self.stream.writeln('python hash seed: %s' %
+                os.environ['PYTHONHASHSEED'])
+        if self._runner.options.time:
+            self.printtimes(result.times)
+
+        return result
+
+    def printtimes(self, times):
+        self.stream.writeln('# Producing time report')
+        times.sort(key=lambda t: (t[1], t[0]), reverse=True)
+        cols = '%7.3f   %s'
+        self.stream.writeln('%-7s   %s' % ('Time', 'Test'))
+        for test, timetaken in times:
+            self.stream.writeln(cols % (timetaken, test))
+
+class TestRunner(object):
+    """Holds context for executing tests.
+
+    Tests rely on a lot of state. This object holds it for them.
+    """
 
-    if (ret != 0 or out != refout) and not skipped and not options.debug:
-        # Save errors to a file for diagnosis
-        f = open(err, "wb")
-        for line in out:
-            f.write(line)
-        f.close()
+    # Programs required to run tests.
+    REQUIREDTOOLS = [
+        os.path.basename(sys.executable),
+        'diff',
+        'grep',
+        'unzip',
+        'gunzip',
+        'bunzip2',
+        'sed',
+    ]
+
+    # Maps file extensions to test class.
+    TESTTYPES = [
+        ('.py', PythonTest),
+        ('.t', TTest),
+    ]
 
-    if skipped:
-        if out is None:                 # debug mode: nothing to parse
-            missing = ['unknown']
-            failed = None
-        else:
-            missing, failed = parsehghaveoutput(out)
-        if not missing:
-            missing = ['irrelevant']
-        if failed:
-            result = fail("hghave failed checking for %s" % failed[-1], ret)
-            skipped = False
+    def __init__(self):
+        self.options = None
+        self._testdir = None
+        self._hgtmp = None
+        self._installdir = None
+        self._bindir = None
+        self._tmpbinddir = None
+        self._pythondir = None
+        self._coveragefile = None
+        self._createdfiles = []
+        self._hgpath = None
+
+    def run(self, args, parser=None):
+        """Run the test suite."""
+        oldmask = os.umask(022)
+        try:
+            parser = parser or getparser()
+            options, args = parseargs(args, parser)
+            self.options = options
+
+            self._checktools()
+            tests = self.findtests(args)
+            return self._run(tests)
+        finally:
+            os.umask(oldmask)
+
+    def _run(self, tests):
+        if self.options.random:
+            random.shuffle(tests)
         else:
-            result = skip(missing[-1])
-    elif ret == 'timeout':
-        result = fail("timed out", ret)
-    elif out != refout:
-        info = {}
-        if not options.nodiff:
-            iolock.acquire()
-            if options.view:
-                os.system("%s %s %s" % (options.view, ref, err))
-            else:
-                info = showdiff(refout, out, ref, err)
-            iolock.release()
-        msg = ""
-        if info.get('servefail'): msg += "serve failed and "
-        if ret:
-            msg += "output changed and " + describe(ret)
-        else:
-            msg += "output changed"
-        result = fail(msg, ret)
-    elif ret:
-        result = fail(describe(ret), ret)
-    else:
-        result = success()
-
-    if not options.verbose:
-        iolock.acquire()
-        sys.stdout.write(result[0])
-        sys.stdout.flush()
-        iolock.release()
+            # keywords for slow tests
+            slow = 'svn gendoc check-code-hg'.split()
+            def sortkey(f):
+                # run largest tests first, as they tend to take the longest
+                try:
+                    val = -os.stat(f).st_size
+                except OSError, e:
+                    if e.errno != errno.ENOENT:
+                        raise
+                    return -1e9 # file does not exist, tell early
+                for kw in slow:
+                    if kw in f:
+                        val *= 10
+                return val
+            tests.sort(key=sortkey)
 
-    if not options.keep_tmpdir:
-        shutil.rmtree(threadtmp, True)
-    return result
-
-_hgpath = None
-
-def _gethgpath():
-    """Return the path to the mercurial package that is actually found by
-    the current Python interpreter."""
-    global _hgpath
-    if _hgpath is not None:
-        return _hgpath
+        self._testdir = os.environ['TESTDIR'] = os.getcwd()
 
-    cmd = '%s -c "import mercurial; print (mercurial.__path__[0])"'
-    pipe = os.popen(cmd % PYTHON)
-    try:
-        _hgpath = pipe.read().strip()
-    finally:
-        pipe.close()
-    return _hgpath
-
-def _checkhglib(verb):
-    """Ensure that the 'mercurial' package imported by python is
-    the one we expect it to be.  If not, print a warning to stderr."""
-    expecthg = os.path.join(PYTHONDIR, 'mercurial')
-    actualhg = _gethgpath()
-    if os.path.abspath(actualhg) != os.path.abspath(expecthg):
-        sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
-                         '         (expected %s)\n'
-                         % (verb, actualhg, expecthg))
-
-results = {'.':[], '!':[], '~': [], 's':[], 'i':[]}
-times = []
-iolock = threading.Lock()
-abort = False
+        if 'PYTHONHASHSEED' not in os.environ:
+            # use a random python hash seed all the time
+            # we do the randomness ourself to know what seed is used
+            os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
 
-def scheduletests(options, tests):
-    jobs = options.jobs
-    done = queue.Queue()
-    running = 0
-    count = 0
-    global abort
-
-    def job(test, count):
-        try:
-            done.put(runone(options, test, count))
-        except KeyboardInterrupt:
-            pass
-        except: # re-raises
-            done.put(('!', test, 'run-test raised an error, see traceback'))
-            raise
-
-    try:
-        while tests or running:
-            if not done.empty() or running == jobs or not tests:
-                try:
-                    code, test, msg = done.get(True, 1)
-                    results[code].append((test, msg))
-                    if options.first and code not in '.si':
-                        break
-                except queue.Empty:
-                    continue
-                running -= 1
-            if tests and not running == jobs:
-                test = tests.pop(0)
-                if options.loop:
-                    tests.append(test)
-                t = threading.Thread(target=job, name=test, args=(test, count))
-                t.start()
-                running += 1
-                count += 1
-    except KeyboardInterrupt:
-        abort = True
-
-def runtests(options, tests):
-    try:
-        if INST:
-            installhg(options)
-            _checkhglib("Testing")
-        else:
-            usecorrectpython()
+        if self.options.tmpdir:
+            self.options.keep_tmpdir = True
+            tmpdir = self.options.tmpdir
+            if os.path.exists(tmpdir):
+                # Meaning of tmpdir has changed since 1.3: we used to create
+                # HGTMP inside tmpdir; now HGTMP is tmpdir.  So fail if
+                # tmpdir already exists.
+                print "error: temp dir %r already exists" % tmpdir
+                return 1
 
-        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
-
-        scheduletests(options, tests)
-
-        failed = len(results['!'])
-        warned = len(results['~'])
-        tested = len(results['.']) + failed + warned
-        skipped = len(results['s'])
-        ignored = len(results['i'])
+                # Automatically removing tmpdir sounds convenient, but could
+                # really annoy anyone in the habit of using "--tmpdir=/tmp"
+                # or "--tmpdir=$HOME".
+                #vlog("# Removing temp dir", tmpdir)
+                #shutil.rmtree(tmpdir)
+            os.makedirs(tmpdir)
+        else:
+            d = None
+            if os.name == 'nt':
+                # without this, we get the default temp dir location, but
+                # in all lowercase, which causes troubles with paths (issue3490)
+                d = os.getenv('TMP')
+            tmpdir = tempfile.mkdtemp('', 'hgtests.', d)
+        self._hgtmp = os.environ['HGTMP'] = os.path.realpath(tmpdir)
 
-        print
-        if not options.noskips:
-            for s in results['s']:
-                print "Skipped %s: %s" % s
-        for s in results['~']:
-            print "Warned %s: %s" % s
-        for s in results['!']:
-            print "Failed %s: %s" % s
-        _checkhglib("Tested")
-        print "# Ran %d tests, %d skipped, %d warned, %d failed." % (
-            tested, skipped + ignored, warned, failed)
-        if results['!']:
-            print 'python hash seed:', os.environ['PYTHONHASHSEED']
-        if options.time:
-            outputtimes(options)
-
-        if options.anycoverage:
-            outputcoverage(options)
-    except KeyboardInterrupt:
-        failed = True
-        print "\ninterrupted!"
+        if self.options.with_hg:
+            self._installdir = None
+            self._bindir = os.path.dirname(os.path.realpath(
+                                           self.options.with_hg))
+            self._tmpbindir = os.path.join(self._hgtmp, 'install', 'bin')
+            os.makedirs(self._tmpbindir)
 
-    if failed:
-        return 1
-    if warned:
-        return 80
-
-testtypes = [('.py', pytest, '.out'),
-             ('.t', tsttest, '')]
-
-def main(args, parser=None):
-    parser = parser or getparser()
-    (options, args) = parseargs(args, parser)
-    os.umask(022)
-
-    checktools()
-
-    if not args:
-        if options.changed:
-            proc = Popen4('hg st --rev "%s" -man0 .' % options.changed,
-                          None, 0)
-            stdout, stderr = proc.communicate()
-            args = stdout.strip('\0').split('\0')
+            # This looks redundant with how Python initializes sys.path from
+            # the location of the script being executed.  Needed because the
+            # "hg" specified by --with-hg is not the only Python script
+            # executed in the test suite that needs to import 'mercurial'
+            # ... which means it's not really redundant at all.
+            self._pythondir = self._bindir
         else:
-            args = os.listdir(".")
+            self._installdir = os.path.join(self._hgtmp, "install")
+            self._bindir = os.environ["BINDIR"] = \
+                os.path.join(self._installdir, "bin")
+            self._tmpbindir = self._bindir
+            self._pythondir = os.path.join(self._installdir, "lib", "python")
+
+        os.environ["BINDIR"] = self._bindir
+        os.environ["PYTHON"] = PYTHON
+
+        path = [self._bindir] + os.environ["PATH"].split(os.pathsep)
+        if self._tmpbindir != self._bindir:
+            path = [self._tmpbindir] + path
+        os.environ["PATH"] = os.pathsep.join(path)
 
-    tests = [t for t in args
-             if os.path.basename(t).startswith("test-")
-                 and (t.endswith(".py") or t.endswith(".t"))]
+        # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
+        # can run .../tests/run-tests.py test-foo where test-foo
+        # adds an extension to HGRC. Also include run-test.py directory to
+        # import modules like heredoctest.
+        pypath = [self._pythondir, self._testdir,
+                  os.path.abspath(os.path.dirname(__file__))]
+        # We have to augment PYTHONPATH, rather than simply replacing
+        # it, in case external libraries are only available via current
+        # PYTHONPATH.  (In particular, the Subversion bindings on OS X
+        # are in /opt/subversion.)
+        oldpypath = os.environ.get(IMPL_PATH)
+        if oldpypath:
+            pypath.append(oldpypath)
+        os.environ[IMPL_PATH] = os.pathsep.join(pypath)
+
+        self._coveragefile = os.path.join(self._testdir, '.coverage')
+
+        vlog("# Using TESTDIR", self._testdir)
+        vlog("# Using HGTMP", self._hgtmp)
+        vlog("# Using PATH", os.environ["PATH"])
+        vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
+
+        try:
+            return self._runtests(tests) or 0
+        finally:
+            time.sleep(.1)
+            self._cleanup()
+
+    def findtests(self, args):
+        """Finds possible test files from arguments.
 
-    if options.random:
-        random.shuffle(tests)
-    else:
-        # keywords for slow tests
-        slow = 'svn gendoc check-code-hg'.split()
-        def sortkey(f):
-            # run largest tests first, as they tend to take the longest
-            try:
-                val = -os.stat(f).st_size
-            except OSError, e:
-                if e.errno != errno.ENOENT:
-                    raise
-                return -1e9 # file does not exist, tell early
-            for kw in slow:
-                if kw in f:
-                    val *= 10
-            return val
-        tests.sort(key=sortkey)
+        If you wish to inject custom tests into the test harness, this would
+        be a good function to monkeypatch or override in a derived class.
+        """
+        if not args:
+            if self.options.changed:
+                proc = Popen4('hg st --rev "%s" -man0 .' %
+                              self.options.changed, None, 0)
+                stdout, stderr = proc.communicate()
+                args = stdout.strip('\0').split('\0')
+            else:
+                args = os.listdir('.')
+
+        return [t for t in args
+                if os.path.basename(t).startswith('test-')
+                    and (t.endswith('.py') or t.endswith('.t'))]
+
+    def _runtests(self, tests):
+        try:
+            if self._installdir:
+                self._installhg()
+                self._checkhglib("Testing")
+            else:
+                self._usecorrectpython()
+
+            if self.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
+
+            tests = [self._gettest(t, i) for i, t in enumerate(tests)]
+
+            failed = False
+            warned = False
+
+            suite = TestSuite(self._testdir,
+                              jobs=self.options.jobs,
+                              whitelist=self.options.whitelisted,
+                              blacklist=self.options.blacklist,
+                              retest=self.options.retest,
+                              keywords=self.options.keywords,
+                              loop=self.options.loop,
+                              tests=tests)
+            verbosity = 1
+            if self.options.verbose:
+                verbosity = 2
+            runner = TextTestRunner(self, verbosity=verbosity)
+            result = runner.run(suite)
+
+            if result.failures:
+                failed = True
+            if result.warned:
+                warned = True
 
-    if 'PYTHONHASHSEED' not in os.environ:
-        # use a random python hash seed all the time
-        # we do the randomness ourself to know what seed is used
-        os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
+            if self.options.anycoverage:
+                self._outputcoverage()
+        except KeyboardInterrupt:
+            failed = True
+            print "\ninterrupted!"
+
+        if failed:
+            return 1
+        if warned:
+            return 80
+
+    def _gettest(self, test, count):
+        """Obtain a Test by looking at its filename.
+
+        Returns a Test instance. The Test may not be runnable if it doesn't
+        map to a known type.
+        """
+        lctest = test.lower()
+        testcls = Test
+
+        for ext, cls in self.TESTTYPES:
+            if lctest.endswith(ext):
+                testcls = cls
+                break
+
+        refpath = os.path.join(self._testdir, test)
+        tmpdir = os.path.join(self._hgtmp, 'child%d' % count)
+
+        return testcls(refpath, tmpdir,
+                       keeptmpdir=self.options.keep_tmpdir,
+                       debug=self.options.debug,
+                       timeout=self.options.timeout,
+                       startport=self.options.port + count * 3,
+                       extraconfigopts=self.options.extra_config_opt,
+                       py3kwarnings=self.options.py3k_warnings,
+                       shell=self.options.shell)
+
+    def _cleanup(self):
+        """Clean up state from this test invocation."""
+
+        if self.options.keep_tmpdir:
+            return
+
+        vlog("# Cleaning up HGTMP", self._hgtmp)
+        shutil.rmtree(self._hgtmp, True)
+        for f in self._createdfiles:
+            try:
+                os.remove(f)
+            except OSError:
+                pass
 
-    global TESTDIR, HGTMP, INST, BINDIR, TMPBINDIR, PYTHONDIR, COVERAGE_FILE
-    TESTDIR = os.environ["TESTDIR"] = os.getcwd()
-    if options.tmpdir:
-        options.keep_tmpdir = True
-        tmpdir = options.tmpdir
-        if os.path.exists(tmpdir):
-            # Meaning of tmpdir has changed since 1.3: we used to create
-            # HGTMP inside tmpdir; now HGTMP is tmpdir.  So fail if
-            # tmpdir already exists.
-            print "error: temp dir %r already exists" % tmpdir
-            return 1
+    def _usecorrectpython(self):
+        """Configure the environment to use the appropriate Python in tests."""
+        # Tests must use the same interpreter as us or bad things will happen.
+        pyexename = sys.platform == 'win32' and 'python.exe' or 'python'
+        if getattr(os, 'symlink', None):
+            vlog("# Making python executable in test path a symlink to '%s'" %
+                 sys.executable)
+            mypython = os.path.join(self._tmpbindir, pyexename)
+            try:
+                if os.readlink(mypython) == sys.executable:
+                    return
+                os.unlink(mypython)
+            except OSError, err:
+                if err.errno != errno.ENOENT:
+                    raise
+            if self._findprogram(pyexename) != sys.executable:
+                try:
+                    os.symlink(sys.executable, mypython)
+                    self._createdfiles.append(mypython)
+                except OSError, err:
+                    # child processes may race, which is harmless
+                    if err.errno != errno.EEXIST:
+                        raise
+        else:
+            exedir, exename = os.path.split(sys.executable)
+            vlog("# Modifying search path to find %s as %s in '%s'" %
+                 (exename, pyexename, exedir))
+            path = os.environ['PATH'].split(os.pathsep)
+            while exedir in path:
+                path.remove(exedir)
+            os.environ['PATH'] = os.pathsep.join([exedir] + path)
+            if not self._findprogram(pyexename):
+                print "WARNING: Cannot find %s in search path" % pyexename
 
-            # Automatically removing tmpdir sounds convenient, but could
-            # really annoy anyone in the habit of using "--tmpdir=/tmp"
-            # or "--tmpdir=$HOME".
-            #vlog("# Removing temp dir", tmpdir)
-            #shutil.rmtree(tmpdir)
-        os.makedirs(tmpdir)
-    else:
-        d = None
+    def _installhg(self):
+        """Install hg into the test environment.
+
+        This will also configure hg with the appropriate testing settings.
+        """
+        vlog("# Performing temporary installation of HG")
+        installerrs = os.path.join("tests", "install.err")
+        compiler = ''
+        if self.options.compiler:
+            compiler = '--compiler ' + self.options.compiler
+        pure = self.options.pure and "--pure" or ""
+        py3 = ''
+        if sys.version_info[0] == 3:
+            py3 = '--c2to3'
+
+        # Run installer in hg root
+        script = os.path.realpath(sys.argv[0])
+        hgroot = os.path.dirname(os.path.dirname(script))
+        os.chdir(hgroot)
+        nohome = '--home=""'
         if os.name == 'nt':
-            # without this, we get the default temp dir location, but
-            # in all lowercase, which causes troubles with paths (issue3490)
-            d = os.getenv('TMP')
-        tmpdir = tempfile.mkdtemp('', 'hgtests.', d)
-    HGTMP = os.environ['HGTMP'] = os.path.realpath(tmpdir)
+            # The --home="" trick works only on OS where os.sep == '/'
+            # because of a distutils convert_path() fast-path. Avoid it at
+            # least on Windows for now, deal with .pydistutils.cfg bugs
+            # when they happen.
+            nohome = ''
+        cmd = ('%(exe)s setup.py %(py3)s %(pure)s clean --all'
+               ' build %(compiler)s --build-base="%(base)s"'
+               ' install --force --prefix="%(prefix)s"'
+               ' --install-lib="%(libdir)s"'
+               ' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
+               % {'exe': sys.executable, 'py3': py3, 'pure': pure,
+                  'compiler': compiler,
+                  'base': os.path.join(self._hgtmp, "build"),
+                  'prefix': self._installdir, 'libdir': self._pythondir,
+                  'bindir': self._bindir,
+                  'nohome': nohome, 'logfile': installerrs})
+        vlog("# Running", cmd)
+        if os.system(cmd) == 0:
+            if not self.options.verbose:
+                os.remove(installerrs)
+        else:
+            f = open(installerrs)
+            for line in f:
+                print line,
+            f.close()
+            sys.exit(1)
+        os.chdir(self._testdir)
 
-    if options.with_hg:
-        INST = None
-        BINDIR = os.path.dirname(os.path.realpath(options.with_hg))
-        TMPBINDIR = os.path.join(HGTMP, 'install', 'bin')
-        os.makedirs(TMPBINDIR)
+        self._usecorrectpython()
 
-        # This looks redundant with how Python initializes sys.path from
-        # the location of the script being executed.  Needed because the
-        # "hg" specified by --with-hg is not the only Python script
-        # executed in the test suite that needs to import 'mercurial'
-        # ... which means it's not really redundant at all.
-        PYTHONDIR = BINDIR
-    else:
-        INST = os.path.join(HGTMP, "install")
-        BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin")
-        TMPBINDIR = BINDIR
-        PYTHONDIR = os.path.join(INST, "lib", "python")
+        if self.options.py3k_warnings and not self.options.anycoverage:
+            vlog("# Updating hg command to enable Py3k Warnings switch")
+            f = open(os.path.join(self._bindir, 'hg'), 'r')
+            lines = [line.rstrip() for line in f]
+            lines[0] += ' -3'
+            f.close()
+            f = open(os.path.join(self._bindir, 'hg'), 'w')
+            for line in lines:
+                f.write(line + '\n')
+            f.close()
 
-    os.environ["BINDIR"] = BINDIR
-    os.environ["PYTHON"] = PYTHON
+        hgbat = os.path.join(self._bindir, 'hg.bat')
+        if os.path.isfile(hgbat):
+            # hg.bat expects to be put in bin/scripts while run-tests.py
+            # installation layout put it in bin/ directly. Fix it
+            f = open(hgbat, 'rb')
+            data = f.read()
+            f.close()
+            if '"%~dp0..\python" "%~dp0hg" %*' in data:
+                data = data.replace('"%~dp0..\python" "%~dp0hg" %*',
+                                    '"%~dp0python" "%~dp0hg" %*')
+                f = open(hgbat, 'wb')
+                f.write(data)
+                f.close()
+            else:
+                print 'WARNING: cannot fix hg.bat reference to python.exe'
+
+        if self.options.anycoverage:
+            custom = os.path.join(self._testdir, 'sitecustomize.py')
+            target = os.path.join(self._pythondir, 'sitecustomize.py')
+            vlog('# Installing coverage trigger to %s' % target)
+            shutil.copyfile(custom, target)
+            rc = os.path.join(self._testdir, '.coveragerc')
+            vlog('# Installing coverage rc to %s' % rc)
+            os.environ['COVERAGE_PROCESS_START'] = rc
+            fn = os.path.join(self._installdir, '..', '.coverage')
+            os.environ['COVERAGE_FILE'] = fn
 
-    path = [BINDIR] + os.environ["PATH"].split(os.pathsep)
-    if TMPBINDIR != BINDIR:
-        path = [TMPBINDIR] + path
-    os.environ["PATH"] = os.pathsep.join(path)
+    def _checkhglib(self, verb):
+        """Ensure that the 'mercurial' package imported by python is
+        the one we expect it to be.  If not, print a warning to stderr."""
+        if ((self._bindir == self._pythondir) and
+            (self._bindir != self._tmpbindir)):
+            # The pythondir has been infered from --with-hg flag.
+            # We cannot expect anything sensible here
+            return
+        expecthg = os.path.join(self._pythondir, 'mercurial')
+        actualhg = self._gethgpath()
+        if os.path.abspath(actualhg) != os.path.abspath(expecthg):
+            sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
+                             '         (expected %s)\n'
+                             % (verb, actualhg, expecthg))
+    def _gethgpath(self):
+        """Return the path to the mercurial package that is actually found by
+        the current Python interpreter."""
+        if self._hgpath is not None:
+            return self._hgpath
+
+        cmd = '%s -c "import mercurial; print (mercurial.__path__[0])"'
+        pipe = os.popen(cmd % PYTHON)
+        try:
+            self._hgpath = pipe.read().strip()
+        finally:
+            pipe.close()
+
+        return self._hgpath
+
+    def _outputcoverage(self):
+        """Produce code coverage output."""
+        vlog('# Producing coverage report')
+        os.chdir(self._pythondir)
 
-    # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
-    # can run .../tests/run-tests.py test-foo where test-foo
-    # adds an extension to HGRC. Also include run-test.py directory to import
-    # modules like heredoctest.
-    pypath = [PYTHONDIR, TESTDIR, os.path.abspath(os.path.dirname(__file__))]
-    # We have to augment PYTHONPATH, rather than simply replacing
-    # it, in case external libraries are only available via current
-    # PYTHONPATH.  (In particular, the Subversion bindings on OS X
-    # are in /opt/subversion.)
-    oldpypath = os.environ.get(IMPL_PATH)
-    if oldpypath:
-        pypath.append(oldpypath)
-    os.environ[IMPL_PATH] = os.pathsep.join(pypath)
+        def covrun(*args):
+            cmd = 'coverage %s' % ' '.join(args)
+            vlog('# Running: %s' % cmd)
+            os.system(cmd)
+
+        covrun('-c')
+        omit = ','.join(os.path.join(x, '*') for x in
+                        [self._bindir, self._testdir])
+        covrun('-i', '-r', '"--omit=%s"' % omit) # report
+        if self.options.htmlcov:
+            htmldir = os.path.join(self._testdir, 'htmlcov')
+            covrun('-i', '-b', '"--directory=%s"' % htmldir,
+                   '"--omit=%s"' % omit)
+        if self.options.annotate:
+            adir = os.path.join(self._testdir, 'annotated')
+            if not os.path.isdir(adir):
+                os.mkdir(adir)
+            covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
 
-    COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
+    def _findprogram(self, 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.name == 'nt' or os.access(name, os.X_OK):
+                return name
+        return None
 
-    vlog("# Using TESTDIR", TESTDIR)
-    vlog("# Using HGTMP", HGTMP)
-    vlog("# Using PATH", os.environ["PATH"])
-    vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
-
-    try:
-        return runtests(options, tests) or 0
-    finally:
-        time.sleep(.1)
-        cleanup(options)
+    def _checktools(self):
+        """Ensure tools required to run tests are present."""
+        for p in self.REQUIREDTOOLS:
+            if os.name == 'nt' and not p.endswith('.exe'):
+                p += '.exe'
+            found = self._findprogram(p)
+            if found:
+                vlog("# Found prerequisite", p, "at", found)
+            else:
+                print "WARNING: Did not find prerequisite tool: %s " % p
 
 if __name__ == '__main__':
-    sys.exit(main(sys.argv[1:]))
+    runner = TestRunner()
+    sys.exit(runner.run(sys.argv[1:]))
--- a/tests/test-add.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-add.t	Sat Jun 14 11:56:20 2014 -0500
@@ -107,6 +107,7 @@
   M a
   ? a.orig
   $ hg resolve -m a
+  no more unresolved files
   $ hg ci -m merge
 
 Issue683: peculiarity with hg revert of an removed then added file
--- a/tests/test-backout.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-backout.t	Sat Jun 14 11:56:20 2014 -0500
@@ -11,6 +11,8 @@
   [255]
 
 basic operation
+(this also tests that editor is invoked if the commit message is not
+specified explicitly)
 
   $ echo a > a
   $ hg commit -d '0 0' -A -m a
@@ -18,8 +20,19 @@
   $ echo b >> a
   $ hg commit -d '1 0' -m b
 
-  $ hg backout -d '2 0' tip --tool=true
+  $ hg status --rev tip --rev "tip^1"
+  M a
+  $ HGEDITOR=cat hg backout -d '2 0' tip --tool=true
   reverting a
+  Backed out changeset a820f4f40a57
+  
+  
+  HG: Enter commit message.  Lines beginning with 'HG:' are removed.
+  HG: Leave message empty to abort commit.
+  HG: --
+  HG: user: test
+  HG: branch 'default'
+  HG: changed a
   changeset 2:2929462c3dff backs out changeset 1:a820f4f40a57
   $ cat a
   a
@@ -31,6 +44,8 @@
   update: (current)
 
 file that was removed is recreated
+(this also tests that editor is not invoked if the commit message is
+specified explicitly)
 
   $ cd ..
   $ hg init remove
@@ -43,7 +58,7 @@
   $ hg rm a
   $ hg commit -d '1 0' -m b
 
-  $ hg backout -d '2 0' tip --tool=true
+  $ HGEDITOR=cat hg backout -d '2 0' tip --tool=true -m "Backed out changeset 76862dcce372"
   adding a
   changeset 2:de31bdc76c0d backs out changeset 1:76862dcce372
   $ cat a
@@ -340,9 +355,21 @@
   update: (current)
 
 with --merge
+(this also tests that editor is invoked if '--edit' is specified
+explicitly regardless of '--message')
+
   $ hg update -qC
-  $ hg backout --merge -d '3 0' -r 1 -m 'backout on branch1' --tool=true
+  $ HGEDITOR=cat hg backout --merge -d '3 0' -r 1 -m 'backout on branch1' --tool=true --edit
   removing file1
+  backout on branch1
+  
+  
+  HG: Enter commit message.  Lines beginning with 'HG:' are removed.
+  HG: Leave message empty to abort commit.
+  HG: --
+  HG: user: test
+  HG: branch 'branch2'
+  HG: removed file1
   created new head
   changeset 3:d4e8f6db59fb backs out changeset 1:bf1602f437f3
   merging with changeset 3:d4e8f6db59fb
@@ -490,6 +517,7 @@
   merging foo
   my foo@b71750c4b0fd+ other foo@a30dd8addae3 ancestor foo@913609522437
    premerge successful
+  no more unresolved files
   $ hg status
   M foo
   ? foo.orig
--- a/tests/test-bookmarks-current.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-bookmarks-current.t	Sat Jun 14 11:56:20 2014 -0500
@@ -24,6 +24,7 @@
 
   $ hg update X
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (activating bookmark X)
 
 list bookmarks
 
@@ -71,6 +72,7 @@
 Verify that switching to Z updates the current bookmark:
   $ hg update Z
   0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  (activating bookmark Z)
   $ hg bookmark
      Y                         0:719295282060
    * Z                         -1:000000000000
@@ -78,6 +80,7 @@
 Switch back to Y for the remaining tests in this file:
   $ hg update Y
   1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (activating bookmark Y)
 
 delete bookmarks
 
@@ -152,6 +155,7 @@
   $ hg bookmark X@2 -r 2
   $ hg update X
   0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  (activating bookmark X)
   $ hg bookmarks
    * X                         0:719295282060
      X@1                       1:cc586d725fbe
--- a/tests/test-bookmarks-merge.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-bookmarks-merge.t	Sat Jun 14 11:56:20 2014 -0500
@@ -32,6 +32,7 @@
 
   $ hg up -C 3
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (leaving bookmark c)
   $ echo d > d
   $ hg add d
   $ hg commit -m'd'
@@ -54,6 +55,7 @@
 
   $ hg up -C 4
   1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  (leaving bookmark e)
   $ hg merge
   abort: heads are bookmarked - please merge with an explicit rev
   (run 'hg heads' to see all heads)
@@ -63,6 +65,7 @@
 
   $ hg up -C e
   1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  (activating bookmark e)
   $ hg merge
   abort: no matching bookmark to merge - please merge with an explicit rev or bookmark
   (run 'hg heads' to see all heads)
@@ -72,6 +75,7 @@
 
   $ hg up -C 4
   1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  (leaving bookmark e)
   $ echo f > f
   $ hg commit -Am "f"
   adding f
@@ -96,6 +100,7 @@
   
   $ hg up -C e
   1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  (activating bookmark e)
   $ hg bookmarks
      b                         1:d2ae7f538514
      c                         3:b8f96cf4688b
@@ -114,6 +119,7 @@
 
   $ hg up -C 6
   1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  (leaving bookmark e)
   $ echo g > g
   $ hg commit -Am 'g'
   adding g
--- a/tests/test-bookmarks-pushpull.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-bookmarks-pushpull.t	Sat Jun 14 11:56:20 2014 -0500
@@ -274,7 +274,7 @@
   $ hg push http://localhost:$HGPORT2/
   pushing to http://localhost:$HGPORT2/
   searching for changes
-  abort: push creates new remote head c922c0139ca0!
+  abort: push creates new remote head c922c0139ca0 with bookmark 'Y'!
   (merge or see "hg help push" for details about pushing new heads)
   [255]
   $ hg -R ../a book
@@ -290,7 +290,7 @@
   $ hg push http://localhost:$HGPORT2/
   pushing to http://localhost:$HGPORT2/
   searching for changes
-  abort: push creates new remote head c922c0139ca0!
+  abort: push creates new remote head c922c0139ca0 with bookmark 'Y'!
   (merge or see "hg help push" for details about pushing new heads)
   [255]
   $ hg -R ../a book
@@ -411,6 +411,7 @@
   $ hg commit -m 'add bar'
   $ hg co "tip^"
   0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  (leaving bookmark @)
   $ hg book add-foo
   $ hg book -r tip add-bar
 Note: this push *must* push only a single changeset, as that's the point
--- a/tests/test-bookmarks-strip.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-bookmarks-strip.t	Sat Jun 14 11:56:20 2014 -0500
@@ -38,6 +38,7 @@
 
   $ hg update -r -2
   1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (leaving bookmark test2)
 
   $ echo eee>>qqq.txt
 
--- a/tests/test-bookmarks.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-bookmarks.t	Sat Jun 14 11:56:20 2014 -0500
@@ -118,6 +118,7 @@
 
   $ hg update X
   0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  (activating bookmark X)
   $ echo c > c
   $ hg add c
   $ hg commit -m 2
@@ -501,6 +502,7 @@
   $ hg update
   updating to active bookmark Z
   1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (activating bookmark Z)
   $ hg bookmarks
      X2                        1:925d80f479bb
      Y                         2:db815d6d32e6
@@ -513,6 +515,7 @@
   moving bookmark 'Y' forward from db815d6d32e6
   $ hg -R cloned-bookmarks-update update Y
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (activating bookmark Y)
   $ hg -R cloned-bookmarks-update pull --update .
   pulling from .
   searching for changes
@@ -582,6 +585,7 @@
   $ hg book should-end-on-two
   $ hg co --clean 4
   1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  (leaving bookmark should-end-on-two)
   $ hg book four
   $ hg --config extensions.mq= strip 3
   saved backup bundle to * (glob)
--- a/tests/test-bundle2.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-bundle2.t	Sat Jun 14 11:56:20 2014 -0500
@@ -47,9 +47,7 @@
   >     op.ui.write('received ping request (id %i)\n' % part.id)
   >     if op.reply is not None and 'ping-pong' in op.reply.capabilities:
   >         op.ui.write_err('replying to ping request (id %i)\n' % part.id)
-  >         rpart = bundle2.bundlepart('test:pong',
-  >                                    [('in-reply-to', str(part.id))])
-  >         op.reply.addpart(rpart)
+  >         op.reply.newpart('test:pong', [('in-reply-to', str(part.id))])
   > 
   > @bundle2.parthandler('test:debugreply')
   > def debugreply(op, part):
@@ -66,6 +64,7 @@
   > @command('bundle2',
   >          [('', 'param', [], 'stream level parameter'),
   >           ('', 'unknown', False, 'include an unknown mandatory part in the bundle'),
+  >           ('', 'unknownparams', False, 'include an unknown part parameters in the bundle'),
   >           ('', 'parts', False, 'include some arbitrary parts to the bundle'),
   >           ('', 'reply', False, 'produce a reply bundle'),
   >           ('', 'pushrace', False, 'includes a check:head part with unknown nodes'),
@@ -83,11 +82,12 @@
   > 
   >     if opts['reply']:
   >         capsstring = 'ping-pong\nelephants=babar,celeste\ncity%3D%21=celeste%2Cville'
-  >         bundler.addpart(bundle2.bundlepart('b2x:replycaps', data=capsstring))
+  >         bundler.newpart('b2x:replycaps', data=capsstring)
   > 
   >     if opts['pushrace']:
-  >         dummynode = '01234567890123456789'
-  >         bundler.addpart(bundle2.bundlepart('b2x:check:heads', data=dummynode))
+  >         # also serve to test the assignement of data outside of init
+  >         part = bundler.newpart('b2x:check:heads')
+  >         part.data = '01234567890123456789'
   > 
   >     revs = opts['rev']
   >     if 'rev' in opts:
@@ -99,31 +99,27 @@
   >             headcommon  = [c.node() for c in repo.set('parents(%ld) - %ld', revs, revs)]
   >             outgoing = discovery.outgoing(repo.changelog, headcommon, headmissing)
   >             cg = changegroup.getlocalbundle(repo, 'test:bundle2', outgoing, None)
-  >             part = bundle2.bundlepart('b2x:changegroup', data=cg.getchunks())
-  >             bundler.addpart(part)
+  >             bundler.newpart('b2x:changegroup', data=cg.getchunks())
   > 
   >     if opts['parts']:
-  >        part = bundle2.bundlepart('test:empty')
-  >        bundler.addpart(part)
+  >        bundler.newpart('test:empty')
   >        # add a second one to make sure we handle multiple parts
-  >        part = bundle2.bundlepart('test:empty')
-  >        bundler.addpart(part)
-  >        part = bundle2.bundlepart('test:song', data=ELEPHANTSSONG)
-  >        bundler.addpart(part)
-  >        part = bundle2.bundlepart('test:debugreply')
-  >        bundler.addpart(part)
-  >        part = bundle2.bundlepart('test:math',
-  >                                  [('pi', '3.14'), ('e', '2.72')],
-  >                                  [('cooking', 'raw')],
-  >                                  '42')
-  >        bundler.addpart(part)
+  >        bundler.newpart('test:empty')
+  >        bundler.newpart('test:song', data=ELEPHANTSSONG)
+  >        bundler.newpart('test:debugreply')
+  >        mathpart = bundler.newpart('test:math')
+  >        mathpart.addparam('pi', '3.14')
+  >        mathpart.addparam('e', '2.72')
+  >        mathpart.addparam('cooking', 'raw', mandatory=False)
+  >        mathpart.data = '42'
+  >        # advisory known part with unknown mandatory param
+  >        bundler.newpart('test:song', [('randomparam','')])
   >     if opts['unknown']:
-  >        part = bundle2.bundlepart('test:UNKNOWN',
-  >                                  data='some random content')
-  >        bundler.addpart(part)
+  >        bundler.newpart('test:UNKNOWN', data='some random content')
+  >     if opts['unknownparams']:
+  >        bundler.newpart('test:SONG', [('randomparams', '')])
   >     if opts['parts']:
-  >        part = bundle2.bundlepart('test:ping')
-  >        bundler.addpart(part)
+  >        bundler.newpart('test:ping')
   > 
   >     if path is None:
   >        file = sys.stdout
@@ -144,7 +140,7 @@
   >             unbundler = bundle2.unbundle20(ui, sys.stdin)
   >             op = bundle2.processbundle(repo, unbundler, lambda: tr)
   >             tr.close()
-  >         except KeyError, exc:
+  >         except error.BundleValueError, exc:
   >             raise util.Abort('missing support for %s' % exc)
   >         except error.PushRaced, exc:
   >             raise util.Abort('push race: %s' % exc)
@@ -170,7 +166,7 @@
   >     unbundler = bundle2.unbundle20(ui, sys.stdin)
   >     try:
   >         params = unbundler.params
-  >     except KeyError, exc:
+  >     except error.BundleValueError, exc:
   >        raise util.Abort('unknown parameters: %s' % exc)
   >     ui.write('options count: %i\n' % len(params))
   >     for key in sorted(params):
@@ -194,9 +190,12 @@
   > bundle2-exp=True
   > [ui]
   > ssh=python "$TESTDIR/dummyssh"
+  > logtemplate={rev}:{node|short} {phase} {author} {desc|firstline}
   > [web]
   > push_ssl = false
   > allow_push = *
+  > [phases]
+  > publish=False
   > EOF
 
 The extension requires a repo (currently unused)
@@ -308,7 +307,7 @@
 ---------------------------------------------------
 
   $ hg bundle2 --param 'Gravity' | hg statbundle2
-  abort: unknown parameters: 'Gravity'
+  abort: unknown parameters: Stream Parameter - Gravity
   [255]
 
 Test debug output
@@ -372,6 +371,7 @@
   bundle part: "test:song"
   bundle part: "test:debugreply"
   bundle part: "test:math"
+  bundle part: "test:song"
   bundle part: "test:ping"
   end of bundle
 
@@ -380,7 +380,7 @@
   test:empty\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11 (esc)
   test:empty\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x10	test:song\x00\x00\x00\x02\x00\x00\x00\x00\x00\xb2Patali Dirapata, Cromda Cromda Ripalo, Pata Pata, Ko Ko Ko (esc)
   Bokoro Dipoulito, Rondi Rondi Pepino, Pata Pata, Ko Ko Ko
-  Emana Karassoli, Loucra Loucra Ponponto, Pata Pata, Ko Ko Ko.\x00\x00\x00\x00\x00\x16\x0ftest:debugreply\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00+	test:math\x00\x00\x00\x04\x02\x01\x02\x04\x01\x04\x07\x03pi3.14e2.72cookingraw\x00\x00\x00\x0242\x00\x00\x00\x00\x00\x10	test:ping\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00 (no-eol) (esc)
+  Emana Karassoli, Loucra Loucra Ponponto, Pata Pata, Ko Ko Ko.\x00\x00\x00\x00\x00\x16\x0ftest:debugreply\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00+	test:math\x00\x00\x00\x04\x02\x01\x02\x04\x01\x04\x07\x03pi3.14e2.72cookingraw\x00\x00\x00\x0242\x00\x00\x00\x00\x00\x1d	test:song\x00\x00\x00\x05\x01\x00\x0b\x00randomparam\x00\x00\x00\x00\x00\x10	test:ping\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00 (no-eol) (esc)
 
 
   $ hg statbundle2 < ../parts.hg2
@@ -405,11 +405,15 @@
       mandatory: 2
       advisory: 1
       payload: 2 bytes
+    :test:song:
+      mandatory: 1
+      advisory: 0
+      payload: 0 bytes
     :test:ping:
       mandatory: 0
       advisory: 0
       payload: 0 bytes
-  parts count:   6
+  parts count:   7
 
   $ hg statbundle2 --debug < ../parts.hg2
   start processing of HG2X stream
@@ -463,9 +467,18 @@
   payload chunk size: 2
   payload chunk size: 0
       payload: 2 bytes
+  part header size: 29
+  part type: "test:song"
+  part id: "5"
+  part parameters: 1
+    :test:song:
+      mandatory: 1
+      advisory: 0
+  payload chunk size: 0
+      payload: 0 bytes
   part header size: 16
   part type: "test:ping"
-  part id: "5"
+  part id: "6"
   part parameters: 0
     :test:ping:
       mandatory: 0
@@ -474,7 +487,7 @@
       payload: 0 bytes
   part header size: 0
   end of bundle2 stream
-  parts count:   6
+  parts count:   7
 
 Test actual unbundling of test part
 =======================================
@@ -489,13 +502,13 @@
   part type: "test:empty"
   part id: "0"
   part parameters: 0
-  ignoring unknown advisory part 'test:empty'
+  ignoring unsupported advisory part test:empty
   payload chunk size: 0
   part header size: 17
   part type: "test:empty"
   part id: "1"
   part parameters: 0
-  ignoring unknown advisory part 'test:empty'
+  ignoring unsupported advisory part test:empty
   payload chunk size: 0
   part header size: 16
   part type: "test:song"
@@ -519,15 +532,22 @@
   part type: "test:math"
   part id: "4"
   part parameters: 3
-  ignoring unknown advisory part 'test:math'
+  ignoring unsupported advisory part test:math
   payload chunk size: 2
   payload chunk size: 0
+  part header size: 29
+  part type: "test:song"
+  part id: "5"
+  part parameters: 1
+  found a handler for part 'test:song'
+  ignoring unsupported advisory part test:song - randomparam
+  payload chunk size: 0
   part header size: 16
   part type: "test:ping"
-  part id: "5"
+  part id: "6"
   part parameters: 0
   found a handler for part 'test:ping'
-  received ping request (id 5)
+  received ping request (id 6)
   payload chunk size: 0
   part header size: 0
   end of bundle2 stream
@@ -546,7 +566,17 @@
       Emana Karassoli, Loucra Loucra Ponponto, Pata Pata, Ko Ko Ko.
   debugreply: no reply
   0 unread bytes
-  abort: missing support for 'test:unknown'
+  abort: missing support for test:unknown
+  [255]
+
+Unbundle with an unknown mandatory part parameters
+(should abort)
+
+  $ hg bundle2 --unknownparams ../unknown.hg2
+
+  $ hg unbundle2 < ../unknown.hg2
+  0 unread bytes
+  abort: missing support for test:song - randomparams
   [255]
 
 unbundle with a reply
@@ -572,9 +602,9 @@
   debugreply:         'babar'
   debugreply:         'celeste'
   debugreply:     'ping-pong'
-  \x00\x00\x00\x00\x00\x1e	test:pong\x00\x00\x00\x02\x01\x00\x0b\x01in-reply-to6\x00\x00\x00\x00\x00\x1f (esc)
-  b2x:output\x00\x00\x00\x03\x00\x01\x0b\x01in-reply-to6\x00\x00\x00=received ping request (id 6) (esc)
-  replying to ping request (id 6)
+  \x00\x00\x00\x00\x00\x1e	test:pong\x00\x00\x00\x02\x01\x00\x0b\x01in-reply-to7\x00\x00\x00\x00\x00\x1f (esc)
+  b2x:output\x00\x00\x00\x03\x00\x01\x0b\x01in-reply-to7\x00\x00\x00=received ping request (id 7) (esc)
+  replying to ping request (id 7)
   \x00\x00\x00\x00\x00\x00 (no-eol) (esc)
 
 The reply is valid
@@ -613,8 +643,8 @@
   remote: debugreply:         'babar'
   remote: debugreply:         'celeste'
   remote: debugreply:     'ping-pong'
-  remote: received ping request (id 6)
-  remote: replying to ping request (id 6)
+  remote: received ping request (id 7)
+  remote: replying to ping request (id 7)
   0 unread bytes
 
 Test push race detection
@@ -637,57 +667,23 @@
   (run 'hg heads' to see heads, 'hg merge' to merge)
 
   $ hg log -G
-  o  changeset:   8:02de42196ebe
-  |  tag:         tip
-  |  parent:      6:24b6387c8c8c
-  |  user:        Nicolas Dumazet <nicdumz.commits@gmail.com>
-  |  date:        Sat Apr 30 15:24:48 2011 +0200
-  |  summary:     H
-  |
-  | o  changeset:   7:eea13746799a
-  |/|  parent:      6:24b6387c8c8c
-  | |  parent:      5:9520eea781bc
-  | |  user:        Nicolas Dumazet <nicdumz.commits@gmail.com>
-  | |  date:        Sat Apr 30 15:24:48 2011 +0200
-  | |  summary:     G
-  | |
-  o |  changeset:   6:24b6387c8c8c
-  | |  parent:      1:cd010b8cd998
-  | |  user:        Nicolas Dumazet <nicdumz.commits@gmail.com>
-  | |  date:        Sat Apr 30 15:24:48 2011 +0200
-  | |  summary:     F
-  | |
-  | o  changeset:   5:9520eea781bc
-  |/   parent:      1:cd010b8cd998
-  |    user:        Nicolas Dumazet <nicdumz.commits@gmail.com>
-  |    date:        Sat Apr 30 15:24:48 2011 +0200
-  |    summary:     E
+  o  8:02de42196ebe draft Nicolas Dumazet <nicdumz.commits@gmail.com> H
   |
-  | o  changeset:   4:32af7686d403
-  | |  user:        Nicolas Dumazet <nicdumz.commits@gmail.com>
-  | |  date:        Sat Apr 30 15:24:48 2011 +0200
-  | |  summary:     D
+  | o  7:eea13746799a draft Nicolas Dumazet <nicdumz.commits@gmail.com> G
+  |/|
+  o |  6:24b6387c8c8c draft Nicolas Dumazet <nicdumz.commits@gmail.com> F
   | |
-  | o  changeset:   3:5fddd98957c8
-  | |  user:        Nicolas Dumazet <nicdumz.commits@gmail.com>
-  | |  date:        Sat Apr 30 15:24:48 2011 +0200
-  | |  summary:     C
+  | o  5:9520eea781bc draft Nicolas Dumazet <nicdumz.commits@gmail.com> E
+  |/
+  | o  4:32af7686d403 draft Nicolas Dumazet <nicdumz.commits@gmail.com> D
   | |
-  | o  changeset:   2:42ccdea3bb16
-  |/   user:        Nicolas Dumazet <nicdumz.commits@gmail.com>
-  |    date:        Sat Apr 30 15:24:48 2011 +0200
-  |    summary:     B
-  |
-  o  changeset:   1:cd010b8cd998
-     parent:      -1:000000000000
-     user:        Nicolas Dumazet <nicdumz.commits@gmail.com>
-     date:        Sat Apr 30 15:24:48 2011 +0200
-     summary:     A
+  | o  3:5fddd98957c8 draft Nicolas Dumazet <nicdumz.commits@gmail.com> C
+  | |
+  | o  2:42ccdea3bb16 draft Nicolas Dumazet <nicdumz.commits@gmail.com> B
+  |/
+  o  1:cd010b8cd998 draft Nicolas Dumazet <nicdumz.commits@gmail.com> A
   
-  @  changeset:   0:3903775176ed
-     user:        test
-     date:        Thu Jan 01 00:00:00 1970 +0000
-     summary:     a
+  @  0:3903775176ed draft test a
   
 
   $ hg bundle2 --debug --rev '8+7+5+4' ../rev.hg2
@@ -768,6 +764,7 @@
 clone --pull
 
   $ cd ..
+  $ hg -R main phase --public cd010b8cd998
   $ hg clone main other --pull --rev 9520eea781bc
   adding changesets
   adding manifests
@@ -776,20 +773,14 @@
   updating to branch default
   2 files updated, 0 files merged, 0 files removed, 0 files unresolved
   $ hg -R other log -G
-  @  changeset:   1:9520eea781bc
-  |  tag:         tip
-  |  user:        Nicolas Dumazet <nicdumz.commits@gmail.com>
-  |  date:        Sat Apr 30 15:24:48 2011 +0200
-  |  summary:     E
+  @  1:9520eea781bc draft Nicolas Dumazet <nicdumz.commits@gmail.com> E
   |
-  o  changeset:   0:cd010b8cd998
-     user:        Nicolas Dumazet <nicdumz.commits@gmail.com>
-     date:        Sat Apr 30 15:24:48 2011 +0200
-     summary:     A
+  o  0:cd010b8cd998 public Nicolas Dumazet <nicdumz.commits@gmail.com> A
   
 
 pull
 
+  $ hg -R main phase --public 9520eea781bc
   $ hg -R other pull -r 24b6387c8c8c
   pulling from $TESTTMP/main (glob)
   searching for changes
@@ -798,15 +789,43 @@
   adding file changes
   added 1 changesets with 1 changes to 1 files (+1 heads)
   (run 'hg heads' to see heads, 'hg merge' to merge)
+  $ hg -R other log -G
+  o  2:24b6387c8c8c draft Nicolas Dumazet <nicdumz.commits@gmail.com> F
+  |
+  | @  1:9520eea781bc draft Nicolas Dumazet <nicdumz.commits@gmail.com> E
+  |/
+  o  0:cd010b8cd998 public Nicolas Dumazet <nicdumz.commits@gmail.com> A
+  
 
+pull empty (with phase movement)
+
+  $ hg -R main phase --public 24b6387c8c8c
+  $ hg -R other pull -r 24b6387c8c8c
+  pulling from $TESTTMP/main (glob)
+  no changes found
+  $ hg -R other log -G
+  o  2:24b6387c8c8c public Nicolas Dumazet <nicdumz.commits@gmail.com> F
+  |
+  | @  1:9520eea781bc draft Nicolas Dumazet <nicdumz.commits@gmail.com> E
+  |/
+  o  0:cd010b8cd998 public Nicolas Dumazet <nicdumz.commits@gmail.com> A
+  
 pull empty
 
   $ hg -R other pull -r 24b6387c8c8c
   pulling from $TESTTMP/main (glob)
   no changes found
+  $ hg -R other log -G
+  o  2:24b6387c8c8c public Nicolas Dumazet <nicdumz.commits@gmail.com> F
+  |
+  | @  1:9520eea781bc draft Nicolas Dumazet <nicdumz.commits@gmail.com> E
+  |/
+  o  0:cd010b8cd998 public Nicolas Dumazet <nicdumz.commits@gmail.com> A
+  
 
 push
 
+  $ hg -R main phase --public eea13746799a
   $ hg -R main push other --rev eea13746799a
   pushing to other
   searching for changes
@@ -814,6 +833,15 @@
   remote: adding manifests
   remote: adding file changes
   remote: added 1 changesets with 0 changes to 0 files (-1 heads)
+  $ hg -R other log -G
+  o    3:eea13746799a public Nicolas Dumazet <nicdumz.commits@gmail.com> G
+  |\
+  | o  2:24b6387c8c8c public Nicolas Dumazet <nicdumz.commits@gmail.com> F
+  | |
+  @ |  1:9520eea781bc public Nicolas Dumazet <nicdumz.commits@gmail.com> E
+  |/
+  o  0:cd010b8cd998 public Nicolas Dumazet <nicdumz.commits@gmail.com> A
+  
 
 pull over ssh
 
@@ -850,12 +878,28 @@
   remote: adding manifests
   remote: adding file changes
   remote: added 1 changesets with 1 changes to 1 files
+  $ hg -R other log -G
+  o  6:5fddd98957c8 draft Nicolas Dumazet <nicdumz.commits@gmail.com> C
+  |
+  o  5:42ccdea3bb16 draft Nicolas Dumazet <nicdumz.commits@gmail.com> B
+  |
+  | o  4:02de42196ebe draft Nicolas Dumazet <nicdumz.commits@gmail.com> H
+  | |
+  | | o  3:eea13746799a public Nicolas Dumazet <nicdumz.commits@gmail.com> G
+  | |/|
+  | o |  2:24b6387c8c8c public Nicolas Dumazet <nicdumz.commits@gmail.com> F
+  |/ /
+  | @  1:9520eea781bc public Nicolas Dumazet <nicdumz.commits@gmail.com> E
+  |/
+  o  0:cd010b8cd998 public Nicolas Dumazet <nicdumz.commits@gmail.com> A
+  
 
 push over http
 
   $ hg -R other serve -p $HGPORT2 -d --pid-file=other.pid -E other-error.log
   $ cat other.pid >> $DAEMON_PIDS
 
+  $ hg -R main phase --public 32af7686d403
   $ hg -R main push http://localhost:$HGPORT2/ -r 32af7686d403
   pushing to http://localhost:$HGPORT2/
   searching for changes
@@ -868,51 +912,21 @@
 Check final content.
 
   $ hg -R other log -G
-  o  changeset:   7:32af7686d403
-  |  tag:         tip
-  |  user:        Nicolas Dumazet <nicdumz.commits@gmail.com>
-  |  date:        Sat Apr 30 15:24:48 2011 +0200
-  |  summary:     D
+  o  7:32af7686d403 public Nicolas Dumazet <nicdumz.commits@gmail.com> D
   |
-  o  changeset:   6:5fddd98957c8
-  |  user:        Nicolas Dumazet <nicdumz.commits@gmail.com>
-  |  date:        Sat Apr 30 15:24:48 2011 +0200
-  |  summary:     C
+  o  6:5fddd98957c8 public Nicolas Dumazet <nicdumz.commits@gmail.com> C
   |
-  o  changeset:   5:42ccdea3bb16
-  |  parent:      0:cd010b8cd998
-  |  user:        Nicolas Dumazet <nicdumz.commits@gmail.com>
-  |  date:        Sat Apr 30 15:24:48 2011 +0200
-  |  summary:     B
+  o  5:42ccdea3bb16 public Nicolas Dumazet <nicdumz.commits@gmail.com> B
   |
-  | o  changeset:   4:02de42196ebe
-  | |  parent:      2:24b6387c8c8c
-  | |  user:        Nicolas Dumazet <nicdumz.commits@gmail.com>
-  | |  date:        Sat Apr 30 15:24:48 2011 +0200
-  | |  summary:     H
+  | o  4:02de42196ebe draft Nicolas Dumazet <nicdumz.commits@gmail.com> H
   | |
-  | | o  changeset:   3:eea13746799a
-  | |/|  parent:      2:24b6387c8c8c
-  | | |  parent:      1:9520eea781bc
-  | | |  user:        Nicolas Dumazet <nicdumz.commits@gmail.com>
-  | | |  date:        Sat Apr 30 15:24:48 2011 +0200
-  | | |  summary:     G
-  | | |
-  | o |  changeset:   2:24b6387c8c8c
-  |/ /   parent:      0:cd010b8cd998
-  | |    user:        Nicolas Dumazet <nicdumz.commits@gmail.com>
-  | |    date:        Sat Apr 30 15:24:48 2011 +0200
-  | |    summary:     F
-  | |
-  | @  changeset:   1:9520eea781bc
-  |/   user:        Nicolas Dumazet <nicdumz.commits@gmail.com>
-  |    date:        Sat Apr 30 15:24:48 2011 +0200
-  |    summary:     E
-  |
-  o  changeset:   0:cd010b8cd998
-     user:        Nicolas Dumazet <nicdumz.commits@gmail.com>
-     date:        Sat Apr 30 15:24:48 2011 +0200
-     summary:     A
+  | | o  3:eea13746799a public Nicolas Dumazet <nicdumz.commits@gmail.com> G
+  | |/|
+  | o |  2:24b6387c8c8c public Nicolas Dumazet <nicdumz.commits@gmail.com> F
+  |/ /
+  | @  1:9520eea781bc public Nicolas Dumazet <nicdumz.commits@gmail.com> E
+  |/
+  o  0:cd010b8cd998 public Nicolas Dumazet <nicdumz.commits@gmail.com> A
   
 
 Error Handling
@@ -938,14 +952,12 @@
   >     reason = pushop.ui.config('failpush', 'reason', None)
   >     part = None
   >     if reason == 'abort':
-  >         part = bundle2.bundlepart('test:abort')
+  >         bundler.newpart('test:abort')
   >     if reason == 'unknown':
-  >         part = bundle2.bundlepart('TEST:UNKNOWN')
+  >         bundler.newpart('TEST:UNKNOWN')
   >     if reason == 'race':
   >         # 20 Bytes of crap
-  >         part = bundle2.bundlepart('b2x:check:heads', data='01234567890123456789')
-  >     if part is not None:
-  >         bundler.addpart(part)
+  >         bundler.newpart('b2x:check:heads', data='01234567890123456789')
   >     return extradata
   > 
   > @bundle2.parthandler("test:abort")
@@ -1015,19 +1027,19 @@
   $ hg -R main push other -r e7ec4e813ba6
   pushing to other
   searching for changes
-  abort: missing support for 'test:unknown'
+  abort: missing support for test:unknown
   [255]
 
   $ hg -R main push ssh://user@dummy/other -r e7ec4e813ba6
   pushing to ssh://user@dummy/other
   searching for changes
-  abort: missing support for "'test:unknown'"
+  abort: missing support for test:unknown
   [255]
 
   $ hg -R main push http://localhost:$HGPORT2/ -r e7ec4e813ba6
   pushing to http://localhost:$HGPORT2/
   searching for changes
-  abort: missing support for "'test:unknown'"
+  abort: missing support for test:unknown
   [255]
 
 Doing the actual push: race
--- a/tests/test-check-code-hg.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-check-code-hg.t	Sat Jun 14 11:56:20 2014 -0500
@@ -1,36 +1,17 @@
+#if test-repo
+
   $ check_code="$TESTDIR"/../contrib/check-code.py
   $ cd "$TESTDIR"/..
-  $ if hg identify -q > /dev/null 2>&1; then :
-  > else
-  >     echo "skipped: not a Mercurial working dir" >&2
-  >     exit 80
-  > fi
-
-Prepare check for Python files without py extension
-
-  $ cp \
-  >   hg \
-  >   hgweb.cgi \
-  >   contrib/convert-repo \
-  >   contrib/dumprevlog \
-  >   contrib/hgweb.fcgi \
-  >   contrib/hgweb.wsgi \
-  >   contrib/simplemerge \
-  >   contrib/undumprevlog \
-  >   i18n/hggettext \
-  >   i18n/posplit \
-  >   tests/hghave \
-  >   tests/dummyssh \
-  >   "$TESTTMP"/
-  $ for f in "$TESTTMP"/*; do mv "$f" "$f.py"; done
 
 New errors are not allowed. Warnings are strongly discouraged.
 (The writing "no-che?k-code" is for not skipping this file when checking.)
 
-  $ { hg manifest 2>/dev/null; ls "$TESTTMP"/*.py | sed 's-\\-/-g'; } |
+  $ hg locate | sed 's-\\-/-g' |
   >   xargs "$check_code" --warnings --per-file=0 || false
   Skipping hgext/zeroconf/Zeroconf.py it has no-che?k-code (glob)
   Skipping i18n/polib.py it has no-che?k-code (glob)
   Skipping mercurial/httpclient/__init__.py it has no-che?k-code (glob)
   Skipping mercurial/httpclient/_readers.py it has no-che?k-code (glob)
   Skipping mercurial/httpclient/socketutil.py it has no-che?k-code (glob)
+
+#endif
--- a/tests/test-check-code.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-check-code.t	Sat Jun 14 11:56:20 2014 -0500
@@ -284,3 +284,19 @@
    > print _(
    don't use % inside _()
   [1]
+
+web templates
+
+  $ mkdir -p mercurial/templates
+  $ cat > mercurial/templates/example.tmpl <<EOF
+  > {desc}
+  > {desc|escape}
+  > {desc|firstline}
+  > {desc|websub}
+  > EOF
+
+  $ "$check_code" --warnings mercurial/templates/example.tmpl
+  mercurial/templates/example.tmpl:2:
+   > {desc|escape}
+   warning: follow desc keyword with either firstline or websub
+  [1]
--- a/tests/test-check-pyflakes.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-check-pyflakes.t	Sat Jun 14 11:56:20 2014 -0500
@@ -5,7 +5,7 @@
 run pyflakes on all tracked files ending in .py or without a file ending
 (skipping binary file random-seed)
 
-  $ hg manifest 2>/dev/null | egrep "\.py$|^[^.]*$" | grep -v /random_seed$ \
+  $ hg locate 'set:**.py or grep("^!#.*python")' 2>/dev/null \
   > | xargs pyflakes 2>/dev/null | "$TESTDIR/filterpyflakes.py"
   contrib/win32/hgwebdir_wsgi.py:*: 'win32traceutil' imported but unused (glob)
   setup.py:*: 'sha' imported but unused (glob)
@@ -17,5 +17,6 @@
   tests/hghave.py:*: 'pygments' imported but unused (glob)
   tests/hghave.py:*: 'ssl' imported but unused (glob)
   contrib/win32/hgwebdir_wsgi.py:93: 'from isapi.install import *' used; unable to detect undefined names (glob)
+  tests/filterpyflakes.py:58: undefined name 'undefinedname'
   
 #endif
--- a/tests/test-commandserver.py.out	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-commandserver.py.out	Sat Jun 14 11:56:20 2014 -0500
@@ -177,6 +177,7 @@
 
  runcommand update -C 0
 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
+(leaving bookmark bm3)
  runcommand commit -Am. a
 created new head
  runcommand log -Gq
--- a/tests/test-commit-amend.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-commit-amend.t	Sat Jun 14 11:56:20 2014 -0500
@@ -586,9 +586,10 @@
   merging cc incomplete! (edit conflicts, then use 'hg resolve --mark')
   [1]
   $ hg resolve -m cc
+  no more unresolved files
   $ hg ci -m 'merge bar'
   $ hg log --config diff.git=1 -pr .
-  changeset:   23:d51446492733
+  changeset:   23:93cd4445f720
   tag:         tip
   parent:      22:30d96aeaf27b
   parent:      21:1aa437659d19
@@ -603,11 +604,11 @@
   --- a/cc
   +++ b/cc
   @@ -1,1 +1,5 @@
-  +<<<<<<< local
+  +<<<<<<< local: 30d96aeaf27b - test: aa
    dd
   +=======
   +cc
-  +>>>>>>> other
+  +>>>>>>> other: 1aa437659d19  bar - test: aazzcc
   diff --git a/z b/zz
   rename from z
   rename to zz
@@ -620,7 +621,7 @@
   cc not renamed
   $ hg ci --amend -m 'merge bar (amend message)'
   $ hg log --config diff.git=1 -pr .
-  changeset:   24:59de3dce7a79
+  changeset:   24:832b50f2c271
   tag:         tip
   parent:      22:30d96aeaf27b
   parent:      21:1aa437659d19
@@ -635,11 +636,11 @@
   --- a/cc
   +++ b/cc
   @@ -1,1 +1,5 @@
-  +<<<<<<< local
+  +<<<<<<< local: 30d96aeaf27b - test: aa
    dd
   +=======
   +cc
-  +>>>>>>> other
+  +>>>>>>> other: 1aa437659d19  bar - test: aazzcc
   diff --git a/z b/zz
   rename from z
   rename to zz
@@ -653,7 +654,7 @@
   $ hg mv zz z
   $ hg ci --amend -m 'merge bar (undo rename)'
   $ hg log --config diff.git=1 -pr .
-  changeset:   26:7fb89c461f81
+  changeset:   26:bdafc5c72f74
   tag:         tip
   parent:      22:30d96aeaf27b
   parent:      21:1aa437659d19
@@ -668,11 +669,11 @@
   --- a/cc
   +++ b/cc
   @@ -1,1 +1,5 @@
-  +<<<<<<< local
+  +<<<<<<< local: 30d96aeaf27b - test: aa
    dd
   +=======
   +cc
-  +>>>>>>> other
+  +>>>>>>> other: 1aa437659d19  bar - test: aazzcc
   
   $ hg debugrename z
   z not renamed
@@ -689,9 +690,9 @@
   $ echo aa >> aaa
   $ hg ci -m 'merge bar again'
   $ hg log --config diff.git=1 -pr .
-  changeset:   28:982d7a34ffee
+  changeset:   28:32f19415b634
   tag:         tip
-  parent:      26:7fb89c461f81
+  parent:      26:bdafc5c72f74
   parent:      27:4c94d5bc65f5
   user:        test
   date:        Thu Jan 01 00:00:00 1970 +0000
@@ -724,9 +725,9 @@
   $ hg mv aaa aa
   $ hg ci --amend -m 'merge bar again (undo rename)'
   $ hg log --config diff.git=1 -pr .
-  changeset:   30:522688c0e71b
+  changeset:   30:1e2a06b3d312
   tag:         tip
-  parent:      26:7fb89c461f81
+  parent:      26:bdafc5c72f74
   parent:      27:4c94d5bc65f5
   user:        test
   date:        Thu Jan 01 00:00:00 1970 +0000
@@ -764,9 +765,9 @@
   use (c)hanged version or (d)elete? c
   $ hg ci -m 'merge bar (with conflicts)'
   $ hg log --config diff.git=1 -pr .
-  changeset:   33:5f9904c491b8
+  changeset:   33:97a298b0c59f
   tag:         tip
-  parent:      32:01780b896f58
+  parent:      32:3d78ce4226b8
   parent:      31:67db8847a540
   user:        test
   date:        Thu Jan 01 00:00:00 1970 +0000
@@ -776,9 +777,9 @@
   $ hg rm aa
   $ hg ci --amend -m 'merge bar (with conflicts, amended)'
   $ hg log --config diff.git=1 -pr .
-  changeset:   35:6ce0c89781a3
+  changeset:   35:6de0c1bde1c8
   tag:         tip
-  parent:      32:01780b896f58
+  parent:      32:3d78ce4226b8
   parent:      31:67db8847a540
   user:        test
   date:        Thu Jan 01 00:00:00 1970 +0000
--- a/tests/test-commit-unresolved.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-commit-unresolved.t	Sat Jun 14 11:56:20 2014 -0500
@@ -41,6 +41,7 @@
 Mark the conflict as resolved and commit
 
   $ hg resolve -m A
+  no more unresolved files
   $ hg commit -m "Merged"
 
   $ cd ..
--- a/tests/test-completion.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-completion.t	Sat Jun 14 11:56:20 2014 -0500
@@ -212,10 +212,10 @@
   serve: accesslog, daemon, daemon-pipefds, errorlog, port, address, prefix, name, web-conf, webdir-conf, pid-file, stdio, cmdserver, templates, style, ipv6, certificate
   status: all, modified, added, removed, deleted, clean, unknown, ignored, no-status, copies, print0, rev, change, include, exclude, subrepos
   summary: remote
-  update: clean, check, date, rev
+  update: clean, check, date, rev, tool
   addremove: similarity, include, exclude, dry-run
   archive: no-decode, prefix, rev, type, subrepos, include, exclude
-  backout: merge, parent, rev, tool, include, exclude, message, logfile, date, user
+  backout: merge, parent, rev, edit, tool, include, exclude, message, logfile, date, user
   bisect: reset, good, bad, skip, extend, command, noupdate
   bookmarks: force, rev, delete, rename, inactive
   branch: force, clean
@@ -262,7 +262,7 @@
   heads: rev, topo, active, closed, style, template
   help: extension, command, keyword
   identify: rev, num, id, branch, tags, bookmarks, ssh, remotecmd, insecure
-  import: strip, base, edit, force, no-commit, bypass, exact, import-branch, message, logfile, date, user, similarity
+  import: strip, base, edit, force, no-commit, bypass, partial, exact, import-branch, message, logfile, date, user, similarity
   incoming: force, newest-first, bundle, rev, bookmarks, branch, patch, git, limit, no-merges, stat, graph, style, template, ssh, remotecmd, insecure, subrepos
   locate: rev, print0, fullpath, include, exclude
   manifest: rev, all
--- a/tests/test-conflict.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-conflict.t	Sat Jun 14 11:56:20 2014 -0500
@@ -22,12 +22,52 @@
   32e80765d7fe+75234512624c+ tip
 
   $ cat a
+  <<<<<<< local: 32e80765d7fe - test: branch2
+  something else
+  =======
+  something
+  >>>>>>> other: 75234512624c  - test: branch1
+
+  $ hg status
+  M a
+  ? a.orig
+
+Verify custom conflict markers
+
+  $ hg up -q --clean .
+  $ printf "\n[ui]\nmergemarkertemplate={author} {rev}\n" >> .hg/hgrc
+
+  $ hg merge 1
+  merging a
+  warning: conflicts during merge.
+  merging a incomplete! (edit conflicts, then use 'hg resolve --mark')
+  0 files updated, 0 files merged, 0 files removed, 1 files unresolved
+  use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
+  [1]
+
+  $ cat a
+  <<<<<<< local: test 2
+  something else
+  =======
+  something
+  >>>>>>> other: test 1
+
+Verify basic conflict markers
+
+  $ hg up -q --clean .
+  $ printf "\n[ui]\nmergemarkers=basic\n" >> .hg/hgrc
+
+  $ hg merge 1
+  merging a
+  warning: conflicts during merge.
+  merging a incomplete! (edit conflicts, then use 'hg resolve --mark')
+  0 files updated, 0 files merged, 0 files removed, 1 files unresolved
+  use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
+  [1]
+
+  $ cat a
   <<<<<<< local
   something else
   =======
   something
   >>>>>>> other
-
-  $ hg status
-  M a
-  ? a.orig
--- a/tests/test-context.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-context.py	Sat Jun 14 11:56:20 2014 -0500
@@ -21,7 +21,7 @@
 # test memctx with non-ASCII commit message
 
 def filectxfn(repo, memctx, path):
-    return context.memfilectx("foo", "")
+    return context.memfilectx(repo, "foo", "")
 
 ctx = context.memctx(repo, ['tip', None],
                      encoding.tolocal("Gr\xc3\xbcezi!"),
--- a/tests/test-convert-hg-sink.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-convert-hg-sink.t	Sat Jun 14 11:56:20 2014 -0500
@@ -16,8 +16,10 @@
   $ echo file > foo/file
   $ hg ci -qAm 'add foo/file'
   $ hg tag some-tag
+  $ hg tag -l local-tag
   $ hg log
   changeset:   3:593cbf6fb2b4
+  tag:         local-tag
   tag:         tip
   user:        test
   date:        Thu Jan 01 00:00:00 1970 +0000
@@ -390,3 +392,148 @@
   o  0 a4a1dae0fe35 "1: add a and dir/b" files: 0 a
   
   $ cd ..
+
+Two way tests
+
+  $ hg init 0
+  $ echo f > 0/f
+  $ echo a > 0/a-only
+  $ echo b > 0/b-only
+  $ hg -R 0 ci -Aqm0
+
+  $ cat << EOF > filemap-a
+  > exclude b-only
+  > EOF
+  $ cat << EOF > filemap-b
+  > exclude a-only
+  > EOF
+  $ hg convert --filemap filemap-a 0 a
+  initializing destination a repository
+  scanning source...
+  sorting...
+  converting...
+  0 0
+  $ hg -R a up -q
+  $ echo a > a/f
+  $ hg -R a ci -ma
+
+  $ hg convert --filemap filemap-b 0 b
+  initializing destination b repository
+  scanning source...
+  sorting...
+  converting...
+  0 0
+  $ hg -R b up -q
+  $ echo b > b/f
+  $ hg -R b ci -mb
+
+  $ tail */.hg/shamap
+  ==> 0/.hg/shamap <==
+  86f3f774ffb682bffb5dc3c1d3b3da637cb9a0d6 8a028c7c77f6c7bd6d63bc3f02ca9f779eabf16a
+  dd9f218eb91fb857f2a62fe023e1d64a4e7812fe 8a028c7c77f6c7bd6d63bc3f02ca9f779eabf16a
+  
+  ==> a/.hg/shamap <==
+  8a028c7c77f6c7bd6d63bc3f02ca9f779eabf16a 86f3f774ffb682bffb5dc3c1d3b3da637cb9a0d6
+  
+  ==> b/.hg/shamap <==
+  8a028c7c77f6c7bd6d63bc3f02ca9f779eabf16a dd9f218eb91fb857f2a62fe023e1d64a4e7812fe
+
+  $ hg convert a 0
+  scanning source...
+  sorting...
+  converting...
+  0 a
+
+  $ hg convert b 0
+  scanning source...
+  sorting...
+  converting...
+  0 b
+
+  $ hg -R 0 log -G
+  o  changeset:   2:637fbbbe96b6
+  |  tag:         tip
+  |  parent:      0:8a028c7c77f6
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     b
+  |
+  | o  changeset:   1:ec7b9c96e692
+  |/   user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     a
+  |
+  @  changeset:   0:8a028c7c77f6
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     0
+  
+  $ hg convert --filemap filemap-b 0 a --config convert.hg.revs=1::
+  scanning source...
+  sorting...
+  converting...
+
+  $ hg -R 0 up -r1
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ echo f >> 0/f
+  $ hg -R 0 ci -mx
+
+  $ hg convert --filemap filemap-b 0 a --config convert.hg.revs=1::
+  scanning source...
+  sorting...
+  converting...
+  0 x
+
+  $ hg -R a log -G -T '{rev} {desc|firstline} ({files})\n'
+  o  2 x (f)
+  |
+  @  1 a (f)
+  |
+  o  0 0 (a-only f)
+  
+  $ hg -R a mani -r tip
+  a-only
+  f
+
+An additional round, demonstrating that unchanged files don't get converted
+
+  $ echo f >> 0/f
+  $ echo f >> 0/a-only
+  $ hg -R 0 ci -m "extra f+a-only change"
+
+  $ hg convert --filemap filemap-b 0 a --config convert.hg.revs=1::
+  scanning source...
+  sorting...
+  converting...
+  0 extra f+a-only change
+
+  $ hg -R a log -G -T '{rev} {desc|firstline} ({files})\n'
+  o  3 extra f+a-only change (f)
+  |
+  o  2 x (f)
+  |
+  @  1 a (f)
+  |
+  o  0 0 (a-only f)
+  
+
+Conversion after rollback
+
+  $ hg -R a rollback -f
+  repository tip rolled back to revision 2 (undo commit)
+
+  $ hg convert --filemap filemap-b 0 a --config convert.hg.revs=1::
+  scanning source...
+  sorting...
+  converting...
+  0 extra f+a-only change
+
+  $ hg -R a log -G -T '{rev} {desc|firstline} ({files})\n'
+  o  3 extra f+a-only change (f)
+  |
+  o  2 x (f)
+  |
+  @  1 a (f)
+  |
+  o  0 0 (a-only f)
+  
--- a/tests/test-convert-hg-source.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-convert-hg-source.t	Sat Jun 14 11:56:20 2014 -0500
@@ -24,6 +24,7 @@
   $ hg ci -m 'merge local copy' -d '3 0'
   $ hg up -C 1
   1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  (leaving bookmark premerge1)
   $ hg bookmark premerge2
   $ hg merge 2
   merging foo and baz to baz
--- a/tests/test-convert-svn-sink.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-convert-svn-sink.t	Sat Jun 14 11:56:20 2014 -0500
@@ -352,6 +352,7 @@
   [1]
   $ hg --cwd b revert -r 2 b
   $ hg --cwd b resolve -m b
+  no more unresolved files
   $ hg --cwd b ci -d '5 0' -m 'merge'
 
 Expect 4 changes
--- a/tests/test-copy-move-merge.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-copy-move-merge.t	Sat Jun 14 11:56:20 2014 -0500
@@ -31,16 +31,16 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: b8bf91eeebbc, local: add3f11052fa+, remote: 17c05bb7fcb6
+   preserving a for resolve of b
+   preserving a for resolve of c
+  removing a
    b: remote moved from a -> m
-    preserving a for resolve of b
-   c: remote moved from a -> m
-    preserving a for resolve of c
-  removing a
   updating: b 1/2 files (50.00%)
   picked tool 'internal:merge' for b (binary False symlink False)
   merging a and b to b
   my b@add3f11052fa+ other b@17c05bb7fcb6 ancestor a@b8bf91eeebbc
    premerge successful
+   c: remote moved from a -> m
   updating: c 2/2 files (100.00%)
   picked tool 'internal:merge' for c (binary False symlink False)
   merging a and c to c
--- a/tests/test-double-merge.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-double-merge.t	Sat Jun 14 11:56:20 2014 -0500
@@ -35,15 +35,15 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: e6dc8efe11cc, local: 6a0df1dad128+, remote: 484bf6903104
+   preserving foo for resolve of bar
+   preserving foo for resolve of foo
    bar: remote copied from foo -> m
-    preserving foo for resolve of bar
-   foo: versions differ -> m
-    preserving foo for resolve of foo
   updating: bar 1/2 files (50.00%)
   picked tool 'internal:merge' for bar (binary False symlink False)
   merging foo and bar to bar
   my bar@6a0df1dad128+ other bar@484bf6903104 ancestor foo@e6dc8efe11cc
    premerge successful
+   foo: versions differ -> m
   updating: foo 2/2 files (100.00%)
   picked tool 'internal:merge' for foo (binary False symlink False)
   merging foo
--- a/tests/test-encoding-align.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-encoding-align.t	Sat Jun 14 11:56:20 2014 -0500
@@ -16,19 +16,18 @@
   > f = file('l', 'w'); f.write(l); f.close()
   > # instant extension to show list of options
   > f = file('showoptlist.py', 'w'); f.write("""# encoding: utf-8
+  > from mercurial import cmdutil
+  > cmdtable = {}
+  > command = cmdutil.command(cmdtable)
+  > 
+  > @command('showoptlist',
+  >     [('s', 'opt1', '', 'short width'  + ' %(s)s' * 8, '%(s)s'),
+  >     ('m', 'opt2', '', 'middle width' + ' %(m)s' * 8, '%(m)s'),
+  >     ('l', 'opt3', '', 'long width'   + ' %(l)s' * 8, '%(l)s')],
+  >     '')
   > def showoptlist(ui, repo, *pats, **opts):
   >     '''dummy command to show option descriptions'''
   >     return 0
-  > cmdtable = {
-  >     'showoptlist':
-  >         (showoptlist,
-  >          [('s', 'opt1', '', 'short width'  + ' %(s)s' * 8, '%(s)s'),
-  >           ('m', 'opt2', '', 'middle width' + ' %(m)s' * 8, '%(m)s'),
-  >           ('l', 'opt3', '', 'long width'   + ' %(l)s' * 8, '%(l)s')
-  >          ],
-  >          ""
-  >         )
-  > }
   > """ % globals())
   > f.close()
   > EOF
--- a/tests/test-encoding-textwrap.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-encoding-textwrap.t	Sat Jun 14 11:56:20 2014 -0500
@@ -6,7 +6,13 @@
 define commands to display help text
 
   $ cat << EOF > show.py
+  > from mercurial import cmdutil
+  > 
+  > cmdtable = {}
+  > command = cmdutil.command(cmdtable)
+  > 
   > # Japanese full-width characters:
+  > @command('show_full_ja', [], '')
   > def show_full_ja(ui, **opts):
   >     u'''\u3042\u3044\u3046\u3048\u304a\u304b\u304d\u304f\u3051 \u3042\u3044\u3046\u3048\u304a\u304b\u304d\u304f\u3051 \u3042\u3044\u3046\u3048\u304a\u304b\u304d\u304f\u3051
   > 
@@ -16,6 +22,7 @@
   >     '''
   > 
   > # Japanese half-width characters:
+  > @command('show_half_ja', [], '')
   > def show_half_ja(ui, *opts):
   >     u'''\uff71\uff72\uff73\uff74\uff75\uff76\uff77\uff78\uff79 \uff71\uff72\uff73\uff74\uff75\uff76\uff77\uff78\uff79 \uff71\uff72\uff73\uff74\uff75\uff76\uff77\uff78\uff79 \uff71\uff72\uff73\uff74\uff75\uff76\uff77\uff78\uff79
   > 
@@ -25,6 +32,7 @@
   >     '''
   > 
   > # Japanese ambiguous-width characters:
+  > @command('show_ambig_ja', [], '')
   > def show_ambig_ja(ui, **opts):
   >     u'''\u03b1\u03b2\u03b3\u03b4\u03c5\u03b6\u03b7\u03b8\u25cb \u03b1\u03b2\u03b3\u03b4\u03c5\u03b6\u03b7\u03b8\u25cb \u03b1\u03b2\u03b3\u03b4\u03c5\u03b6\u03b7\u03b8\u25cb
   > 
@@ -34,6 +42,7 @@
   >     '''
   > 
   > # Russian ambiguous-width characters:
+  > @command('show_ambig_ru', [], '')
   > def show_ambig_ru(ui, **opts):
   >     u'''\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438
   > 
@@ -41,13 +50,6 @@
   > 
   >     \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438
   >     '''
-  > 
-  > cmdtable = {
-  >     'show_full_ja': (show_full_ja, [], ""),
-  >     'show_half_ja': (show_half_ja, [], ""),
-  >     'show_ambig_ja': (show_ambig_ja, [], ""),
-  >     'show_ambig_ru': (show_ambig_ru, [], ""),
-  > }
   > EOF
 
 "COLUMNS=60" means that there is no lines which has grater than 58 width
--- a/tests/test-extension.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-extension.t	Sat Jun 14 11:56:20 2014 -0500
@@ -2,7 +2,10 @@
 
   $ cat > foobar.py <<EOF
   > import os
-  > from mercurial import commands
+  > from mercurial import cmdutil, commands
+  > 
+  > cmdtable = {}
+  > command = cmdutil.command(cmdtable)
   > 
   > def uisetup(ui):
   >     ui.write("uisetup called\\n")
@@ -11,17 +14,14 @@
   >     ui.write("reposetup called for %s\\n" % os.path.basename(repo.root))
   >     ui.write("ui %s= repo.ui\\n" % (ui == repo.ui and "=" or "!"))
   > 
+  > @command('foo', [], 'hg foo')
   > def foo(ui, *args, **kwargs):
   >     ui.write("Foo\\n")
   > 
+  > @command('bar', [], 'hg bar')
   > def bar(ui, *args, **kwargs):
   >     ui.write("Bar\\n")
   > 
-  > cmdtable = {
-  >    "foo": (foo, [], "hg foo"),
-  >    "bar": (bar, [], "hg bar"),
-  > }
-  > 
   > commands.norepo += ' bar'
   > EOF
   $ abspath=`pwd`/foobar.py
@@ -288,21 +288,22 @@
   $ cat > debugextension.py <<EOF
   > '''only debugcommands
   > '''
+  > from mercurial import cmdutil
+  > cmdtable = {}
+  > command = cmdutil.command(cmdtable)
+  > 
+  > @command('debugfoobar', [], 'hg debugfoobar')
   > def debugfoobar(ui, repo, *args, **opts):
   >     "yet another debug command"
   >     pass
   > 
+  > @command('foo', [], 'hg foo')
   > def foo(ui, repo, *args, **opts):
   >     """yet another foo command
   > 
   >     This command has been DEPRECATED since forever.
   >     """
   >     pass
-  > 
-  > cmdtable = {
-  >    "debugfoobar": (debugfoobar, (), "hg debugfoobar"),
-  >    "foo": (foo, (), "hg foo")
-  > }
   > EOF
   $ debugpath=`pwd`/debugextension.py
   $ echo "debugextension = $debugpath" >> $HGRCPATH
@@ -475,15 +476,15 @@
 Test help topic with same name as extension
 
   $ cat > multirevs.py <<EOF
-  > from mercurial import commands
+  > from mercurial import cmdutil, commands
+  > cmdtable = {}
+  > command = cmdutil.command(cmdtable)
   > """multirevs extension
   > Big multi-line module docstring."""
+  > @command('multirevs', [], 'ARG')
   > def multirevs(ui, repo, arg, *args, **opts):
   >     """multirevs command"""
   >     pass
-  > cmdtable = {
-  >    "multirevs": (multirevs, [], 'ARG')
-  > }
   > commands.norepo += ' multirevs'
   > EOF
   $ echo "multirevs = multirevs.py" >> $HGRCPATH
@@ -532,13 +533,15 @@
   $ cat > debugissue811.py <<EOF
   > '''show all loaded extensions
   > '''
-  > from mercurial import extensions, commands
+  > from mercurial import cmdutil, commands, extensions
+  > cmdtable = {}
+  > command = cmdutil.command(cmdtable)
   > 
+  > @command('debugextensions', [], 'hg debugextensions')
   > def debugextensions(ui):
   >     "yet another debug command"
   >     ui.write("%s\n" % '\n'.join([x for x, y in extensions.extensions()]))
   > 
-  > cmdtable = {"debugextensions": (debugextensions, (), "hg debugextensions")}
   > commands.norepo += " debugextensions"
   > EOF
   $ echo "debugissue811 = $debugpath" >> $HGRCPATH
@@ -618,8 +621,8 @@
   > EOF
   $ hg --config extensions.path=./path.py help foo > /dev/null
   warning: error finding commands in $TESTTMP/hgext/forest.py (glob)
-  hg: unknown command 'foo'
-  warning: error finding commands in $TESTTMP/hgext/forest.py (glob)
+  abort: no such help topic: foo
+  (try "hg help --keyword foo")
   [255]
 
   $ cat > throw.py <<EOF
--- a/tests/test-fetch.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-fetch.t	Sat Jun 14 11:56:20 2014 -0500
@@ -68,8 +68,9 @@
   $ cat a/hg.pid >> "$DAEMON_PIDS"
 
 fetch over http, no auth
+(this also tests that editor is invoked if '--edit' is specified)
 
-  $ hg --cwd d fetch http://localhost:$HGPORT/
+  $ HGEDITOR=cat hg --cwd d fetch --edit http://localhost:$HGPORT/
   pulling from http://localhost:$HGPORT/
   searching for changes
   adding changesets
@@ -80,13 +81,29 @@
   1 files updated, 0 files merged, 1 files removed, 0 files unresolved
   merging with 1:d36c0562f908
   1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  Automated merge with http://localhost:$HGPORT/
+  
+  
+  HG: Enter commit message.  Lines beginning with 'HG:' are removed.
+  HG: Leave message empty to abort commit.
+  HG: --
+  HG: user: test
+  HG: branch merge
+  HG: branch 'default'
+  HG: changed c
   new changeset 3:* merges remote changes with local (glob)
   $ hg --cwd d tip --template '{desc}\n'
   Automated merge with http://localhost:$HGPORT/
+  $ hg --cwd d status --rev 'tip^1' --rev tip
+  A c
+  $ hg --cwd d status --rev 'tip^2' --rev tip
+  A b
 
 fetch over http with auth (should be hidden in desc)
+(this also tests that editor is not invoked if '--edit' is not
+specified, even though commit message is not specified explicitly)
 
-  $ hg --cwd e fetch http://user:password@localhost:$HGPORT/
+  $ HGEDITOR=cat hg --cwd e fetch http://user:password@localhost:$HGPORT/
   pulling from http://user:***@localhost:$HGPORT/
   searching for changes
   adding changesets
--- a/tests/test-fileset.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-fileset.t	Sat Jun 14 11:56:20 2014 -0500
@@ -154,6 +154,7 @@
   b2
   $ echo e > b2
   $ hg resolve -m b2
+  no more unresolved files
   $ fileset 'resolved()'
   b2
   $ fileset 'unresolved()'
--- a/tests/test-gpg.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-gpg.t	Sat Jun 14 11:56:20 2014 -0500
@@ -16,8 +16,17 @@
 
   $ hg sigs
 
-  $ hg sign 0
+  $ HGEDITOR=cat hg sign -e 0
   signing 0:e63c23eaa88a
+  Added signature for changeset e63c23eaa88a
+  
+  
+  HG: Enter commit message.  Lines beginning with 'HG:' are removed.
+  HG: Leave message empty to abort commit.
+  HG: --
+  HG: user: test
+  HG: branch 'default'
+  HG: added .hgsigs
 
   $ hg sigs
   hgtest                             0:e63c23eaa88ae77967edcf4ea194d31167c478b0
--- a/tests/test-graft.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-graft.t	Sat Jun 14 11:56:20 2014 -0500
@@ -76,10 +76,24 @@
   $ hg revert a
 
 Graft a rename:
+(this also tests that editor is invoked if '--edit' is specified)
 
-  $ hg graft 2 -u foo
+  $ hg status --rev "2^1" --rev 2
+  A b
+  R a
+  $ HGEDITOR=cat hg graft 2 -u foo --edit
   grafting revision 2
   merging a and b to b
+  2
+  
+  
+  HG: Enter commit message.  Lines beginning with 'HG:' are removed.
+  HG: Leave message empty to abort commit.
+  HG: --
+  HG: user: foo
+  HG: branch 'default'
+  HG: changed b
+  HG: removed a
   $ hg export tip --git
   # HG changeset patch
   # User foo
@@ -114,6 +128,7 @@
   
 
 Graft out of order, skipping a merge and a duplicate
+(this also tests that editor is not invoked if '--edit' is not specified)
 
   $ hg graft 1 5 4 3 'merge()' 2 -n
   skipping ungraftable merge revision 6
@@ -123,7 +138,7 @@
   grafting revision 4
   grafting revision 3
 
-  $ hg graft 1 5 4 3 'merge()' 2 --debug
+  $ HGEDITOR=cat hg graft 1 5 4 3 'merge()' 2 --debug
   skipping ungraftable merge revision 6
   scanning for duplicate grafts
   skipping revision 2 (already grafted to 7)
@@ -137,8 +152,8 @@
   resolving manifests
    branchmerge: True, force: True, partial: False
    ancestor: 68795b066622, local: ef0ef43d49e7+, remote: 5d205f8b35b6
+   preserving b for resolve of b
    b: local copied/moved from a -> m
-    preserving b for resolve of b
   updating: b 1/1 files (100.00%)
   picked tool 'internal:merge' for b (binary False symlink False)
   merging b and a to b
@@ -150,22 +165,22 @@
   resolving manifests
    branchmerge: True, force: True, partial: False
    ancestor: 4c60f11aa304, local: 6b9e5368ca4e+, remote: 97f8bfe72746
-   b: keep -> k
    e: remote is newer -> g
   getting e
   updating: e 1/1 files (100.00%)
+   b: keep -> k
   e
   grafting revision 4
     searching for copies back to rev 1
   resolving manifests
    branchmerge: True, force: True, partial: False
    ancestor: 4c60f11aa304, local: 1905859650ec+, remote: 9c233e8e184d
-   b: keep -> k
+   preserving e for resolve of e
    d: remote is newer -> g
-   e: versions differ -> m
-    preserving e for resolve of e
   getting d
   updating: d 1/2 files (50.00%)
+   b: keep -> k
+   e: versions differ -> m
   updating: e 2/2 files (100.00%)
   picked tool 'internal:merge' for e (binary False symlink False)
   merging e
@@ -220,6 +235,7 @@
 
   $ echo b > e
   $ hg resolve -m e
+  no more unresolved files
 
 Continue with a revision should fail:
 
@@ -354,6 +370,7 @@
   [255]
   $ hg resolve --all
   merging a
+  no more unresolved files
   $ hg graft -c
   grafting revision 1
   $ hg export tip --git
@@ -382,6 +399,7 @@
   [255]
   $ hg resolve --all
   merging a and b to b
+  no more unresolved files
   $ hg graft -c
   grafting revision 2
   $ hg export tip --git
--- a/tests/test-help.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-help.t	Sat Jun 14 11:56:20 2014 -0500
@@ -596,30 +596,8 @@
   show changed files in the working directory
 
   $ hg help foo
-  hg: unknown command 'foo'
-  Mercurial Distributed SCM
-  
-  basic commands:
-  
-   add           add the specified files on the next commit
-   annotate      show changeset information by line for each file
-   clone         make a copy of an existing repository
-   commit        commit the specified files or all outstanding changes
-   diff          diff repository (or selected files)
-   export        dump the header and diffs for one or more changesets
-   forget        forget the specified files on the next commit
-   init          create a new repository in the given directory
-   log           show revision history of entire repository or files
-   merge         merge working directory with another revision
-   pull          pull changes from the specified source
-   push          push changes to the specified destination
-   remove        remove the specified files on the next commit
-   serve         start stand-alone webserver
-   status        show changed files in the working directory
-   summary       summarize working directory state
-   update        update working directory (or switch revisions)
-  
-  use "hg help" for the full list of commands or "hg -v" for details
+  abort: no such help topic: foo
+  (try "hg help --keyword foo")
   [255]
 
   $ hg skjdfks
@@ -652,19 +630,20 @@
 
   $ cat > helpext.py <<EOF
   > import os
-  > from mercurial import commands
+  > from mercurial import cmdutil, commands
+  > 
+  > cmdtable = {}
+  > command = cmdutil.command(cmdtable)
   > 
+  > @command('nohelp',
+  >     [('', 'longdesc', 3, 'x'*90),
+  >     ('n', '', None, 'normal desc'),
+  >     ('', 'newline', '', 'line1\nline2')],
+  >     'hg nohelp')
+  > @command('debugoptDEP', [('', 'dopt', None, 'option is DEPRECATED')])
   > def nohelp(ui, *args, **kwargs):
   >     pass
   > 
-  > cmdtable = {
-  >     "debugoptDEP": (nohelp, [('', 'dopt', None, 'option is DEPRECATED')],),
-  >     "nohelp": (nohelp, [('', 'longdesc', 3, 'x'*90),
-  >                         ('n', '', None, 'normal desc'),
-  >                         ('', 'newline', '', 'line1\nline2'),
-  >                        ], "hg nohelp"),
-  > }
-  > 
   > commands.norepo += ' nohelp'
   > EOF
   $ echo '[extensions]' >> $HGRCPATH
@@ -987,6 +966,20 @@
   
    qclone clone main and patch repository at same time
 
+Test unfound topic
+
+  $ hg help nonexistingtopicthatwillneverexisteverever
+  abort: no such help topic: nonexistingtopicthatwillneverexisteverever
+  (try "hg help --keyword nonexistingtopicthatwillneverexisteverever")
+  [255]
+
+Test unfound keyword
+
+  $ hg help --keyword nonexistingwordthatwillneverexisteverever
+  abort: no matches
+  (try "hg help" for a list of topics)
+  [255]
+
 Test omit indicating for help
 
   $ cat > addverboseitems.py <<EOF
--- a/tests/test-hgweb-commands.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-hgweb-commands.t	Sat Jun 14 11:56:20 2014 -0500
@@ -31,10 +31,15 @@
   $ hg ci -l msg
   $ rm msg
 
-  $ echo [graph] >> .hg/hgrc
-  $ echo default.width = 3 >> .hg/hgrc
-  $ echo stable.width = 3 >> .hg/hgrc
-  $ echo stable.color = FF0000 >> .hg/hgrc
+  $ cat > .hg/hgrc <<EOF
+  > [graph]
+  > default.width = 3
+  > stable.width = 3
+  > stable.color = FF0000
+  > [websub]
+  > append = s|(.*)|\1(websub)|
+  > EOF
+
   $ hg serve --config server.uncompressed=False -n test -p $HGPORT -d --pid-file=hg.pid -E errors.log
   $ cat hg.pid >> $DAEMON_PIDS
   $ hg log -G --template '{rev}:{node|short} {desc}\n'
@@ -95,7 +100,7 @@
   	</tr>
   	<tr>
   		<th style="text-align:left;vertical-align:top;">description</th>
-  		<td>branch commit with null character: </td>
+  		<td>branch commit with null character: (websub)</td>
   	</tr>
   	<tr>
   		<th style="text-align:left;vertical-align:top;">files</th>
@@ -138,7 +143,7 @@
   	</tr>
   	<tr>
   		<th style="text-align:left;vertical-align:top;">description</th>
-  		<td>branch</td>
+  		<td>branch(websub)</td>
   	</tr>
   	<tr>
   		<th style="text-align:left;vertical-align:top;">files</th>
@@ -181,7 +186,7 @@
   	</tr>
   	<tr>
   		<th style="text-align:left;vertical-align:top;">description</th>
-  		<td>Added tag 1.0 for changeset 2ef0ac749a14</td>
+  		<td>Added tag 1.0 for changeset 2ef0ac749a14(websub)</td>
   	</tr>
   	<tr>
   		<th style="text-align:left;vertical-align:top;">files</th>
@@ -224,7 +229,7 @@
   	</tr>
   	<tr>
   		<th style="text-align:left;vertical-align:top;">description</th>
-  		<td>base</td>
+  		<td>base(websub)</td>
   	</tr>
   	<tr>
   		<th style="text-align:left;vertical-align:top;">files</th>
@@ -235,6 +240,180 @@
    </entry>
   
   </feed>
+  $ "$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT 'log/?style=rss'
+  200 Script output follows
+  
+  <?xml version="1.0" encoding="ascii"?>
+  <rss version="2.0">
+    <channel>
+      <link>http://*:$HGPORT/</link> (glob)
+      <language>en-us</language>
+  
+      <title>test Changelog</title>
+      <description>test Changelog</description>
+      <item>
+      <title>[unstable] branch commit with null character: </title>
+      <guid isPermaLink="true">http://*:$HGPORT/rev/cad8025a2e87</guid> (glob)
+               <link>http://*:$HGPORT/rev/cad8025a2e87</link> (glob)
+      <description>
+                <![CDATA[
+  	<table>
+  	<tr>
+  		<th style="text-align:left;">changeset</th>
+  		<td>cad8025a2e87</td>
+                </tr>
+                <tr>
+                                <th style="text-align:left;">branch</th>
+                                <td>unstable</td>
+                </tr>
+                <tr>
+                                <th style="text-align:left;">bookmark</th>
+  		<td>something</td>
+  	</tr>
+  	<tr>
+  		<th style="text-align:left;">tag</th>
+  		<td>tip</td>
+  	</tr>
+  	<tr>
+  		<th style="text-align:left;vertical-align:top;">user</th>
+  		<td>&#116;&#101;&#115;&#116;</td>
+  	</tr>
+  	<tr>
+  		<th style="text-align:left;vertical-align:top;">description</th>
+  		<td>branch commit with null character: (websub)</td>
+  	</tr>
+  	<tr>
+  		<th style="text-align:left;vertical-align:top;">files</th>
+  		<td></td>
+  	</tr>
+  	</table>
+  	]]></description>
+      <author>&#116;&#101;&#115;&#116;</author>
+      <pubDate>Thu, 01 Jan 1970 00:00:00 +0000</pubDate>
+  </item>
+  <item>
+      <title>[stable] branch</title>
+      <guid isPermaLink="true">http://*:$HGPORT/rev/1d22e65f027e</guid> (glob)
+               <link>http://*:$HGPORT/rev/1d22e65f027e</link> (glob)
+      <description>
+                <![CDATA[
+  	<table>
+  	<tr>
+  		<th style="text-align:left;">changeset</th>
+  		<td>1d22e65f027e</td>
+                </tr>
+                <tr>
+                                <th style="text-align:left;">branch</th>
+                                <td>stable</td>
+                </tr>
+                <tr>
+                                <th style="text-align:left;">bookmark</th>
+  		<td></td>
+  	</tr>
+  	<tr>
+  		<th style="text-align:left;">tag</th>
+  		<td></td>
+  	</tr>
+  	<tr>
+  		<th style="text-align:left;vertical-align:top;">user</th>
+  		<td>&#116;&#101;&#115;&#116;</td>
+  	</tr>
+  	<tr>
+  		<th style="text-align:left;vertical-align:top;">description</th>
+  		<td>branch(websub)</td>
+  	</tr>
+  	<tr>
+  		<th style="text-align:left;vertical-align:top;">files</th>
+  		<td>foo<br /></td>
+  	</tr>
+  	</table>
+  	]]></description>
+      <author>&#116;&#101;&#115;&#116;</author>
+      <pubDate>Thu, 01 Jan 1970 00:00:00 +0000</pubDate>
+  </item>
+  <item>
+      <title>[default] Added tag 1.0 for changeset 2ef0ac749a14</title>
+      <guid isPermaLink="true">http://*:$HGPORT/rev/a4f92ed23982</guid> (glob)
+               <link>http://*:$HGPORT/rev/a4f92ed23982</link> (glob)
+      <description>
+                <![CDATA[
+  	<table>
+  	<tr>
+  		<th style="text-align:left;">changeset</th>
+  		<td>a4f92ed23982</td>
+                </tr>
+                <tr>
+                                <th style="text-align:left;">branch</th>
+                                <td>default</td>
+                </tr>
+                <tr>
+                                <th style="text-align:left;">bookmark</th>
+  		<td></td>
+  	</tr>
+  	<tr>
+  		<th style="text-align:left;">tag</th>
+  		<td></td>
+  	</tr>
+  	<tr>
+  		<th style="text-align:left;vertical-align:top;">user</th>
+  		<td>&#116;&#101;&#115;&#116;</td>
+  	</tr>
+  	<tr>
+  		<th style="text-align:left;vertical-align:top;">description</th>
+  		<td>Added tag 1.0 for changeset 2ef0ac749a14(websub)</td>
+  	</tr>
+  	<tr>
+  		<th style="text-align:left;vertical-align:top;">files</th>
+  		<td>.hgtags<br /></td>
+  	</tr>
+  	</table>
+  	]]></description>
+      <author>&#116;&#101;&#115;&#116;</author>
+      <pubDate>Thu, 01 Jan 1970 00:00:00 +0000</pubDate>
+  </item>
+  <item>
+      <title>base</title>
+      <guid isPermaLink="true">http://*:$HGPORT/rev/2ef0ac749a14</guid> (glob)
+               <link>http://*:$HGPORT/rev/2ef0ac749a14</link> (glob)
+      <description>
+                <![CDATA[
+  	<table>
+  	<tr>
+  		<th style="text-align:left;">changeset</th>
+  		<td>2ef0ac749a14</td>
+                </tr>
+                <tr>
+                                <th style="text-align:left;">branch</th>
+                                <td></td>
+                </tr>
+                <tr>
+                                <th style="text-align:left;">bookmark</th>
+  		<td>anotherthing</td>
+  	</tr>
+  	<tr>
+  		<th style="text-align:left;">tag</th>
+  		<td>1.0</td>
+  	</tr>
+  	<tr>
+  		<th style="text-align:left;vertical-align:top;">user</th>
+  		<td>&#116;&#101;&#115;&#116;</td>
+  	</tr>
+  	<tr>
+  		<th style="text-align:left;vertical-align:top;">description</th>
+  		<td>base(websub)</td>
+  	</tr>
+  	<tr>
+  		<th style="text-align:left;vertical-align:top;">files</th>
+  		<td>da/foo<br />foo<br /></td>
+  	</tr>
+  	</table>
+  	]]></description>
+      <author>&#116;&#101;&#115;&#116;</author>
+      <pubDate>Thu, 01 Jan 1970 00:00:00 +0000</pubDate>
+  </item>
+  
+    </channel>
+  </rss> (no-eol)
   $ "$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT 'log/1/?style=atom'
   200 Script output follows
   
@@ -281,7 +460,7 @@
   	</tr>
   	<tr>
   		<th style="text-align:left;vertical-align:top;">description</th>
-  		<td>Added tag 1.0 for changeset 2ef0ac749a14</td>
+  		<td>Added tag 1.0 for changeset 2ef0ac749a14(websub)</td>
   	</tr>
   	<tr>
   		<th style="text-align:left;vertical-align:top;">files</th>
@@ -324,7 +503,7 @@
   	</tr>
   	<tr>
   		<th style="text-align:left;vertical-align:top;">description</th>
-  		<td>base</td>
+  		<td>base(websub)</td>
   	</tr>
   	<tr>
   		<th style="text-align:left;vertical-align:top;">files</th>
@@ -335,6 +514,100 @@
    </entry>
   
   </feed>
+  $ "$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT 'log/1/?style=rss'
+  200 Script output follows
+  
+  <?xml version="1.0" encoding="ascii"?>
+  <rss version="2.0">
+    <channel>
+      <link>http://*:$HGPORT/</link> (glob)
+      <language>en-us</language>
+  
+      <title>test Changelog</title>
+      <description>test Changelog</description>
+      <item>
+      <title>[default] Added tag 1.0 for changeset 2ef0ac749a14</title>
+      <guid isPermaLink="true">http://*:$HGPORT/rev/a4f92ed23982</guid> (glob)
+               <link>http://*:$HGPORT/rev/a4f92ed23982</link> (glob)
+      <description>
+                <![CDATA[
+  	<table>
+  	<tr>
+  		<th style="text-align:left;">changeset</th>
+  		<td>a4f92ed23982</td>
+                </tr>
+                <tr>
+                                <th style="text-align:left;">branch</th>
+                                <td>default</td>
+                </tr>
+                <tr>
+                                <th style="text-align:left;">bookmark</th>
+  		<td></td>
+  	</tr>
+  	<tr>
+  		<th style="text-align:left;">tag</th>
+  		<td></td>
+  	</tr>
+  	<tr>
+  		<th style="text-align:left;vertical-align:top;">user</th>
+  		<td>&#116;&#101;&#115;&#116;</td>
+  	</tr>
+  	<tr>
+  		<th style="text-align:left;vertical-align:top;">description</th>
+  		<td>Added tag 1.0 for changeset 2ef0ac749a14(websub)</td>
+  	</tr>
+  	<tr>
+  		<th style="text-align:left;vertical-align:top;">files</th>
+  		<td>.hgtags<br /></td>
+  	</tr>
+  	</table>
+  	]]></description>
+      <author>&#116;&#101;&#115;&#116;</author>
+      <pubDate>Thu, 01 Jan 1970 00:00:00 +0000</pubDate>
+  </item>
+  <item>
+      <title>base</title>
+      <guid isPermaLink="true">http://*:$HGPORT/rev/2ef0ac749a14</guid> (glob)
+               <link>http://*:$HGPORT/rev/2ef0ac749a14</link> (glob)
+      <description>
+                <![CDATA[
+  	<table>
+  	<tr>
+  		<th style="text-align:left;">changeset</th>
+  		<td>2ef0ac749a14</td>
+                </tr>
+                <tr>
+                                <th style="text-align:left;">branch</th>
+                                <td></td>
+                </tr>
+                <tr>
+                                <th style="text-align:left;">bookmark</th>
+  		<td>anotherthing</td>
+  	</tr>
+  	<tr>
+  		<th style="text-align:left;">tag</th>
+  		<td>1.0</td>
+  	</tr>
+  	<tr>
+  		<th style="text-align:left;vertical-align:top;">user</th>
+  		<td>&#116;&#101;&#115;&#116;</td>
+  	</tr>
+  	<tr>
+  		<th style="text-align:left;vertical-align:top;">description</th>
+  		<td>base(websub)</td>
+  	</tr>
+  	<tr>
+  		<th style="text-align:left;vertical-align:top;">files</th>
+  		<td>da/foo<br />foo<br /></td>
+  	</tr>
+  	</table>
+  	]]></description>
+      <author>&#116;&#101;&#115;&#116;</author>
+      <pubDate>Thu, 01 Jan 1970 00:00:00 +0000</pubDate>
+  </item>
+  
+    </channel>
+  </rss> (no-eol)
   $ "$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT 'log/1/foo/?style=atom'
   200 Script output follows
   
@@ -379,7 +652,7 @@
   	</tr>
   	<tr>
   		<th style="text-align:left;vertical-align:top;">description</th>
-  		<td>base</td>
+  		<td>base(websub)</td>
   	</tr>
   	<tr>
   		<th style="text-align:left;vertical-align:top;">files</th>
@@ -390,6 +663,27 @@
    </entry>
   
   </feed>
+  $ "$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT 'log/1/foo/?style=rss'
+  200 Script output follows
+  
+  <?xml version="1.0" encoding="ascii"?>
+  <rss version="2.0">
+    <channel>
+      <link>http://*:$HGPORT/</link> (glob)
+      <language>en-us</language>
+  
+      <title>test: foo history</title>
+      <description>foo revision history</description>
+      <item>
+      <title>base</title>
+      <link>http://*:$HGPORT/log2ef0ac749a14/foo</link> (glob)
+      <description><![CDATA[base(websub)]]></description>
+      <author>&#116;&#101;&#115;&#116;</author>
+      <pubDate>Thu, 01 Jan 1970 00:00:00 +0000</pubDate>
+  </item>
+  
+    </channel>
+  </rss>
   $ "$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT 'shortlog/'
   200 Script output follows
   
@@ -570,7 +864,7 @@
   number or hash, or <a href="/help/revsets">revset expression</a>.</div>
   </form>
   
-  <div class="description">base</div>
+  <div class="description">base(websub)</div>
   
   <table id="changesetEntry">
   <tr>
@@ -992,7 +1286,7 @@
   number or hash, or <a href="/help/revsets">revset expression</a>.</div>
   </form>
   
-  <div class="description">Added tag 1.0 for changeset 2ef0ac749a14</div>
+  <div class="description">Added tag 1.0 for changeset 2ef0ac749a14(websub)</div>
   
   <table id="changesetEntry">
   <tr>
@@ -1116,7 +1410,7 @@
   number or hash, or <a href="/help/revsets">revset expression</a>.</div>
   </form>
   
-  <div class="description">branch</div>
+  <div class="description">branch(websub)</div>
   
   <table id="changesetEntry">
   <tr>
--- a/tests/test-histedit-edit.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-histedit-edit.t	Sat Jun 14 11:56:20 2014 -0500
@@ -156,7 +156,19 @@
   update: 1 new changesets (update)
   hist:   1 remaining (histedit --continue)
 
-  $ HGEDITOR='true' hg histedit --continue
+(test also that editor is invoked if histedit is continued for
+"edit" action)
+
+  $ HGEDITOR='cat' hg histedit --continue
+  f
+  
+  
+  HG: Enter commit message.  Lines beginning with 'HG:' are removed.
+  HG: Leave message empty to abort commit.
+  HG: --
+  HG: user: test
+  HG: branch 'default'
+  HG: added f
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
   saved backup bundle to $TESTTMP/r/.hg/strip-backup/b5f70786f9b0-backup.hg (glob)
 
@@ -200,8 +212,9 @@
   >             raise util.Abort('emulating unexpected abort')
   >     repo.__class__ = commitfailure
   > EOF
-  $ cat > .hg/hgrc <<EOF
+  $ cat >> .hg/hgrc <<EOF
   > [extensions]
+  > # this failure occurs before editor invocation
   > commitfailure = $TESTTMP/commitfailure.py
   > EOF
 
@@ -211,22 +224,87 @@
   > echo "===="
   > echo "check saving last-message.txt" >> \$1
   > EOF
+
+(test that editor is not invoked before transaction starting)
+
   $ rm -f .hg/last-message.txt
   $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit tip --commands - 2>&1 << EOF | fixbundle
   > mess 1fd3b2fe7754 f
   > EOF
   0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  abort: emulating unexpected abort
+  $ cat .hg/last-message.txt
+  cat: .hg/last-message.txt: No such file or directory
+  [1]
+
+  $ cat >> .hg/hgrc <<EOF
+  > [extensions]
+  > commitfailure = !
+  > EOF
+  $ hg histedit --abort -q
+
+(test that editor is invoked and commit message is saved into
+"last-message.txt")
+
+  $ cat >> .hg/hgrc <<EOF
+  > [hooks]
+  > # this failure occurs after editor invocation
+  > pretxncommit.unexpectedabort = false
+  > EOF
+
+  $ hg status --rev '1fd3b2fe7754^1' --rev 1fd3b2fe7754
+  A f
+
+  $ rm -f .hg/last-message.txt
+  $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit tip --commands - 2>&1 << EOF
+  > mess 1fd3b2fe7754 f
+  > EOF
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  adding f
   ==== before editing
   f
+  
+  
+  HG: Enter commit message.  Lines beginning with 'HG:' are removed.
+  HG: Leave message empty to abort commit.
+  HG: --
+  HG: user: test
+  HG: branch 'default'
+  HG: added f
   ====
-  abort: emulating unexpected abort
+  transaction abort!
+  rollback completed
+  note: commit message saved in .hg/last-message.txt
+  abort: pretxncommit.unexpectedabort hook exited with status 1
+  [255]
   $ cat .hg/last-message.txt
   f
+  
+  
   check saving last-message.txt
 
-  $ cat > .hg/hgrc <<EOF
-  > [extensions]
-  > commitfailure = !
+(test also that editor is invoked if histedit is continued for "message"
+action)
+
+  $ HGEDITOR=cat hg histedit --continue
+  f
+  
+  
+  HG: Enter commit message.  Lines beginning with 'HG:' are removed.
+  HG: Leave message empty to abort commit.
+  HG: --
+  HG: user: test
+  HG: branch 'default'
+  HG: added f
+  transaction abort!
+  rollback completed
+  note: commit message saved in .hg/last-message.txt
+  abort: pretxncommit.unexpectedabort hook exited with status 1
+  [255]
+
+  $ cat >> .hg/hgrc <<EOF
+  > [hooks]
+  > pretxncommit.unexpectedabort =
   > EOF
   $ hg histedit --abort -q
 
--- a/tests/test-histedit-fold-non-commute.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-histedit-fold-non-commute.t	Sat Jun 14 11:56:20 2014 -0500
@@ -95,6 +95,7 @@
 fix up
   $ echo 'I can haz no commute' > e
   $ hg resolve --mark e
+  no more unresolved files
   $ cat > cat.py <<EOF
   > import sys
   > print open(sys.argv[1]).read()
@@ -129,6 +130,7 @@
 just continue this time
   $ hg revert -r 'p1()' e
   $ hg resolve --mark e
+  no more unresolved files
   $ hg histedit --continue 2>&1 | fixbundle
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
--- a/tests/test-histedit-fold.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-histedit-fold.t	Sat Jun 14 11:56:20 2014 -0500
@@ -217,6 +217,7 @@
   U file
   $ hg revert -r 'p1()' file
   $ hg resolve --mark file
+  no more unresolved files
   $ hg histedit --continue
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
   saved backup bundle to $TESTTMP/*-backup.hg (glob)
@@ -276,6 +277,7 @@
   > 5
   > EOF
   $ hg resolve --mark file
+  no more unresolved files
   $ hg commit -m '+5.2'
   created new head
   $ echo 6 >> file
--- a/tests/test-histedit-non-commute.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-histedit-non-commute.t	Sat Jun 14 11:56:20 2014 -0500
@@ -154,6 +154,7 @@
 fix up
   $ echo 'I can haz no commute' > e
   $ hg resolve --mark e
+  no more unresolved files
   $ hg histedit --continue 2>&1 | fixbundle
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
   merging e
@@ -167,6 +168,7 @@
 just continue this time
   $ hg revert -r 'p1()' e
   $ hg resolve --mark e
+  no more unresolved files
   $ hg histedit --continue 2>&1 | fixbundle
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
@@ -239,6 +241,7 @@
 
   $ echo 'I can haz no commute' > e
   $ hg resolve --mark e
+  no more unresolved files
   $ hg histedit --continue 2>&1 | fixbundle
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
   merging e
@@ -248,6 +251,7 @@
 second edit also fails, but just continue
   $ hg revert -r 'p1()' e
   $ hg resolve --mark e
+  no more unresolved files
   $ hg histedit --continue 2>&1 | fixbundle
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
--- a/tests/test-import-bypass.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-import-bypass.t	Sat Jun 14 11:56:20 2014 -0500
@@ -22,8 +22,10 @@
   0 files updated, 0 files merged, 1 files removed, 0 files unresolved
 
 Test importing an existing revision
+(this also tests that editor is not invoked for '--bypass', if the
+patch contains the commit message, regardless of '--edit')
 
-  $ hg import --bypass --exact ../test.diff
+  $ HGEDITOR=cat hg import --bypass --exact --edit ../test.diff
   applying ../test.diff
   $ shortlog
   o  1:4e322f7ce8e3 test 0 0 - foo - changea
@@ -107,6 +109,8 @@
   [255]
 
 Test commit editor
+(this also tests that editor is invoked, if the patch doesn't contain
+the commit message, regardless of '--edit')
 
   $ cat > ../test.diff <<EOF
   > diff -r 07f494440405 -r 4e322f7ce8e3 a
@@ -131,10 +135,12 @@
   [255]
 
 Test patch.eol is handled
+(this also tests that editor is not invoked for '--bypass', if the
+commit message is explicitly specified, regardless of '--edit')
 
   $ python -c 'file("a", "wb").write("a\r\n")'
   $ hg ci -m makeacrlf
-  $ hg import -m 'should fail because of eol' --bypass ../test.diff
+  $ HGEDITOR=cat hg import -m 'should fail because of eol' --edit --bypass ../test.diff
   applying ../test.diff
   patching file a
   Hunk #1 FAILED at 0
--- a/tests/test-import.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-import.t	Sat Jun 14 11:56:20 2014 -0500
@@ -23,6 +23,8 @@
 
 
 import exported patch
+(this also tests that editor is not invoked, if the patch contains the
+commit message and '--edit' is not specified)
 
   $ hg clone -r0 a b
   adding changesets
@@ -31,7 +33,7 @@
   added 1 changesets with 2 changes to 2 files
   updating to branch default
   2 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ hg --cwd b import ../exported-tip.patch
+  $ HGEDITOR=cat hg --cwd b import ../exported-tip.patch
   applying ../exported-tip.patch
 
 message and committer and date should be same
@@ -47,6 +49,8 @@
 
 
 import exported patch with external patcher
+(this also tests that editor is invoked, if the '--edit' is specified,
+regardless of the commit message in the patch)
 
   $ cat > dummypatch.py <<EOF
   > print 'patching file a'
@@ -59,14 +63,25 @@
   added 1 changesets with 2 changes to 2 files
   updating to branch default
   2 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ hg --config ui.patch='python ../dummypatch.py' --cwd b import ../exported-tip.patch
+  $ HGEDITOR=cat hg --config ui.patch='python ../dummypatch.py' --cwd b import --edit ../exported-tip.patch
   applying ../exported-tip.patch
+  second change
+  
+  
+  HG: Enter commit message.  Lines beginning with 'HG:' are removed.
+  HG: Leave message empty to abort commit.
+  HG: --
+  HG: user: someone
+  HG: branch 'default'
+  HG: changed a
   $ cat b/a
   line2
   $ rm -r b
 
 
 import of plain diff should fail without message
+(this also tests that editor is invoked, if the patch doesn't contain
+the commit message, regardless of '--edit')
 
   $ hg clone -r0 a b
   adding changesets
@@ -75,8 +90,16 @@
   added 1 changesets with 2 changes to 2 files
   updating to branch default
   2 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ hg --cwd b import ../diffed-tip.patch
+  $ HGEDITOR=cat hg --cwd b import ../diffed-tip.patch
   applying ../diffed-tip.patch
+  
+  
+  HG: Enter commit message.  Lines beginning with 'HG:' are removed.
+  HG: Leave message empty to abort commit.
+  HG: --
+  HG: user: test
+  HG: branch 'default'
+  HG: changed a
   abort: empty commit message
   [255]
   $ rm -r b
@@ -97,6 +120,8 @@
 
 
 import of plain diff with specific date and user
+(this also tests that editor is not invoked, if
+'--message'/'--logfile' is specified and '--edit' is not)
 
   $ hg clone -r0 a b
   adding changesets
@@ -128,6 +153,8 @@
 
 
 import of plain diff should be ok with --no-commit
+(this also tests that editor is not invoked, if '--no-commit' is
+specified, regardless of '--edit')
 
   $ hg clone -r0 a b
   adding changesets
@@ -136,7 +163,7 @@
   added 1 changesets with 2 changes to 2 files
   updating to branch default
   2 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ hg --cwd b import --no-commit ../diffed-tip.patch
+  $ HGEDITOR=cat hg --cwd b import --no-commit --edit ../diffed-tip.patch
   applying ../diffed-tip.patch
   $ hg --cwd b diff --nodates
   diff -r 80971e65b431 a
@@ -1154,5 +1181,272 @@
   3
   4
   line
+  $ cd ..
 
-  $ cd ..
+Test partial application
+------------------------
+
+prepare a stack of patches depending on each other
+
+  $ hg init partial
+  $ cd partial
+  $ cat << EOF > a
+  > one
+  > two
+  > three
+  > four
+  > five
+  > six
+  > seven
+  > EOF
+  $ hg add a
+  $ echo 'b' > b
+  $ hg add b
+  $ hg commit -m 'initial' -u Babar
+  $ cat << EOF > a
+  > one
+  > two
+  > 3
+  > four
+  > five
+  > six
+  > seven
+  > EOF
+  $ hg commit -m 'three' -u Celeste
+  $ cat << EOF > a
+  > one
+  > two
+  > 3
+  > 4
+  > five
+  > six
+  > seven
+  > EOF
+  $ hg commit -m 'four' -u Rataxes
+  $ cat << EOF > a
+  > one
+  > two
+  > 3
+  > 4
+  > 5
+  > six
+  > seven
+  > EOF
+  $ echo bb >> b
+  $ hg commit -m 'five' -u Arthur
+  $ echo 'Babar' > jungle
+  $ hg add jungle
+  $ hg ci -m 'jungle' -u Zephir
+  $ echo 'Celeste' >> jungle
+  $ hg ci -m 'extended jungle' -u Cornelius
+  $ hg log -G --template '{desc|firstline} [{author}] {diffstat}\n'
+  @  extended jungle [Cornelius] 1: +1/-0
+  |
+  o  jungle [Zephir] 1: +1/-0
+  |
+  o  five [Arthur] 2: +2/-1
+  |
+  o  four [Rataxes] 1: +1/-1
+  |
+  o  three [Celeste] 1: +1/-1
+  |
+  o  initial [Babar] 2: +8/-0
+  
+
+Importing with some success and some errors:
+
+  $ hg update --rev 'desc(initial)'
+  2 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ hg export --rev 'desc(five)' | hg import --partial -
+  applying patch from stdin
+  patching file a
+  Hunk #1 FAILED at 1
+  1 out of 1 hunks FAILED -- saving rejects to file a.rej
+  patch applied partially
+  (fix the .rej files and run `hg commit --amend`)
+  [1]
+
+  $ hg log -G --template '{desc|firstline} [{author}] {diffstat}\n'
+  @  five [Arthur] 1: +1/-0
+  |
+  | o  extended jungle [Cornelius] 1: +1/-0
+  | |
+  | o  jungle [Zephir] 1: +1/-0
+  | |
+  | o  five [Arthur] 2: +2/-1
+  | |
+  | o  four [Rataxes] 1: +1/-1
+  | |
+  | o  three [Celeste] 1: +1/-1
+  |/
+  o  initial [Babar] 2: +8/-0
+  
+  $ hg export
+  # HG changeset patch
+  # User Arthur
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID 26e6446bb2526e2be1037935f5fca2b2706f1509
+  # Parent  8e4f0351909eae6b9cf68c2c076cb54c42b54b2e
+  five
+  
+  diff -r 8e4f0351909e -r 26e6446bb252 b
+  --- a/b	Thu Jan 01 00:00:00 1970 +0000
+  +++ b/b	Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,1 +1,2 @@
+   b
+  +bb
+  $ hg status -c .
+  C a
+  C b
+  $ ls
+  a
+  a.rej
+  b
+
+Importing with zero success:
+
+  $ hg update --rev 'desc(initial)'
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg export --rev 'desc(four)' | hg import --partial -
+  applying patch from stdin
+  patching file a
+  Hunk #1 FAILED at 0
+  1 out of 1 hunks FAILED -- saving rejects to file a.rej
+  patch applied partially
+  (fix the .rej files and run `hg commit --amend`)
+  [1]
+
+  $ hg log -G --template '{desc|firstline} [{author}] {diffstat}\n'
+  @  four [Rataxes] 0: +0/-0
+  |
+  | o  five [Arthur] 1: +1/-0
+  |/
+  | o  extended jungle [Cornelius] 1: +1/-0
+  | |
+  | o  jungle [Zephir] 1: +1/-0
+  | |
+  | o  five [Arthur] 2: +2/-1
+  | |
+  | o  four [Rataxes] 1: +1/-1
+  | |
+  | o  three [Celeste] 1: +1/-1
+  |/
+  o  initial [Babar] 2: +8/-0
+  
+  $ hg export
+  # HG changeset patch
+  # User Rataxes
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID cb9b1847a74d9ad52e93becaf14b98dbcc274e1e
+  # Parent  8e4f0351909eae6b9cf68c2c076cb54c42b54b2e
+  four
+  
+  $ hg status -c .
+  C a
+  C b
+  $ ls
+  a
+  a.rej
+  b
+
+Importing with unknown file:
+
+  $ hg update --rev 'desc(initial)'
+  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg export --rev 'desc("extended jungle")' | hg import --partial -
+  applying patch from stdin
+  unable to find 'jungle' for patching
+  1 out of 1 hunks FAILED -- saving rejects to file jungle.rej
+  patch applied partially
+  (fix the .rej files and run `hg commit --amend`)
+  [1]
+
+  $ hg log -G --template '{desc|firstline} [{author}] {diffstat}\n'
+  @  extended jungle [Cornelius] 0: +0/-0
+  |
+  | o  four [Rataxes] 0: +0/-0
+  |/
+  | o  five [Arthur] 1: +1/-0
+  |/
+  | o  extended jungle [Cornelius] 1: +1/-0
+  | |
+  | o  jungle [Zephir] 1: +1/-0
+  | |
+  | o  five [Arthur] 2: +2/-1
+  | |
+  | o  four [Rataxes] 1: +1/-1
+  | |
+  | o  three [Celeste] 1: +1/-1
+  |/
+  o  initial [Babar] 2: +8/-0
+  
+  $ hg export
+  # HG changeset patch
+  # User Cornelius
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID 1fb1f86bef43c5a75918178f8d23c29fb0a7398d
+  # Parent  8e4f0351909eae6b9cf68c2c076cb54c42b54b2e
+  extended jungle
+  
+  $ hg status -c .
+  C a
+  C b
+  $ ls
+  a
+  a.rej
+  b
+  jungle.rej
+
+Importing multiple failing patches:
+
+  $ hg update --rev 'desc(initial)'
+  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ echo 'B' > b # just to make another commit
+  $ hg commit -m "a new base"
+  created new head
+  $ hg export --rev 'desc("extended jungle") + desc("four")' | hg import --partial -
+  applying patch from stdin
+  patching file a
+  Hunk #1 FAILED at 0
+  1 out of 1 hunks FAILED -- saving rejects to file a.rej
+  patch applied partially
+  (fix the .rej files and run `hg commit --amend`)
+  [1]
+  $ hg log -G --template '{desc|firstline} [{author}] {diffstat}\n'
+  @  four [Rataxes] 0: +0/-0
+  |
+  o  a new base [test] 1: +1/-1
+  |
+  | o  extended jungle [Cornelius] 0: +0/-0
+  |/
+  | o  four [Rataxes] 0: +0/-0
+  |/
+  | o  five [Arthur] 1: +1/-0
+  |/
+  | o  extended jungle [Cornelius] 1: +1/-0
+  | |
+  | o  jungle [Zephir] 1: +1/-0
+  | |
+  | o  five [Arthur] 2: +2/-1
+  | |
+  | o  four [Rataxes] 1: +1/-1
+  | |
+  | o  three [Celeste] 1: +1/-1
+  |/
+  o  initial [Babar] 2: +8/-0
+  
+  $ hg export
+  # HG changeset patch
+  # User Rataxes
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID a9d7b6d0ffbb4eb12b7d5939250fcd42e8930a1d
+  # Parent  f59f8d2e95a8ca5b1b4ca64320140da85f3b44fd
+  four
+  
+  $ hg status -c .
+  C a
+  C b
--- a/tests/test-issue1877.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-issue1877.t	Sat Jun 14 11:56:20 2014 -0500
@@ -34,6 +34,7 @@
   
   $ hg up 1e6c11564562
   1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  (leaving bookmark main)
   $ hg merge main
   1 files updated, 0 files merged, 0 files removed, 0 files unresolved
   (branch merge, don't forget to commit)
--- a/tests/test-issue672.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-issue672.t	Sat Jun 14 11:56:20 2014 -0500
@@ -35,12 +35,12 @@
    branchmerge: True, force: False, partial: False
    ancestor: 81f4b099af3d, local: c64f439569a9+, remote: c12dcd37c90a
    1: other deleted -> r
-   1a: remote created -> g
-   2: keep -> k
   removing 1
   updating: 1 1/2 files (50.00%)
+   1a: remote created -> g
   getting 1a
   updating: 1a 2/2 files (100.00%)
+   2: keep -> k
   1 files updated, 0 files merged, 1 files removed, 0 files unresolved
   (branch merge, don't forget to commit)
 
@@ -66,8 +66,8 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: c64f439569a9, local: e327dca35ac8+, remote: 746e9549ea96
+   preserving 1a for resolve of 1a
    1a: local copied/moved from 1 -> m
-    preserving 1a for resolve of 1a
   updating: 1a 1/1 files (100.00%)
   picked tool 'internal:merge' for 1a (binary False symlink False)
   merging 1a and 1 to 1a
@@ -89,9 +89,9 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: c64f439569a9, local: 746e9549ea96+, remote: e327dca35ac8
+   preserving 1 for resolve of 1a
+  removing 1
    1a: remote moved from 1 -> m
-    preserving 1 for resolve of 1a
-  removing 1
   updating: 1a 1/1 files (100.00%)
   picked tool 'internal:merge' for 1a (binary False symlink False)
   merging 1 and 1a to 1a
--- a/tests/test-journal-exists.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-journal-exists.t	Sat Jun 14 11:56:20 2014 -0500
@@ -9,7 +9,8 @@
 
   $ echo foo > a
   $ hg ci -Am0
-  abort: abandoned transaction found - run hg recover!
+  abort: abandoned transaction found!
+  (run 'hg recover' to clean up transaction)
   [255]
 
   $ hg recover
--- a/tests/test-keyword.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-keyword.t	Sat Jun 14 11:56:20 2014 -0500
@@ -1049,15 +1049,16 @@
   [1]
   $ cat m
   $Id$
-  <<<<<<< local
+  <<<<<<< local: 88a80c8d172e - test: 8bar
   bar
   =======
   foo
-  >>>>>>> other
+  >>>>>>> other: 85d2d2d732a5  - test: simplemerge
 
 resolve to local
 
   $ HGMERGE=internal:local hg resolve -a
+  no more unresolved files
   $ hg commit -m localresolve
   $ cat m
   $Id: m 800511b3a22d Thu, 01 Jan 1970 00:00:00 +0000 test $
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-largefiles-misc.t	Sat Jun 14 11:56:20 2014 -0500
@@ -0,0 +1,696 @@
+This file contains testcases that tend to be related to special cases or less
+common commands affecting largefile.
+
+Each sections should be independent of each others.
+
+  $ USERCACHE="$TESTTMP/cache"; export USERCACHE
+  $ mkdir "${USERCACHE}"
+  $ cat >> $HGRCPATH <<EOF
+  > [extensions]
+  > largefiles=
+  > purge=
+  > rebase=
+  > transplant=
+  > [phases]
+  > publish=False
+  > [largefiles]
+  > minsize=2
+  > patterns=glob:**.dat
+  > usercache=${USERCACHE}
+  > [hooks]
+  > precommit=sh -c "echo \\"Invoking status precommit hook\\"; hg status"
+  > EOF
+
+
+
+Test copies and moves from a directory other than root (issue3516)
+=========================================================================
+
+  $ hg init lf_cpmv
+  $ cd lf_cpmv
+  $ mkdir dira
+  $ mkdir dira/dirb
+  $ touch dira/dirb/largefile
+  $ hg add --large dira/dirb/largefile
+  $ hg commit -m "added"
+  Invoking status precommit hook
+  A dira/dirb/largefile
+  $ cd dira
+  $ hg cp dirb/largefile foo/largefile
+  $ hg ci -m "deep copy"
+  Invoking status precommit hook
+  A dira/foo/largefile
+  $ find . | sort
+  .
+  ./dirb
+  ./dirb/largefile
+  ./foo
+  ./foo/largefile
+  $ hg mv foo/largefile baz/largefile
+  $ hg ci -m "moved"
+  Invoking status precommit hook
+  A dira/baz/largefile
+  R dira/foo/largefile
+  $ find . | sort
+  .
+  ./baz
+  ./baz/largefile
+  ./dirb
+  ./dirb/largefile
+  $ cd ..
+  $ hg mv dira dirc
+  moving .hglf/dira/baz/largefile to .hglf/dirc/baz/largefile (glob)
+  moving .hglf/dira/dirb/largefile to .hglf/dirc/dirb/largefile (glob)
+  $ find * | sort
+  dirc
+  dirc/baz
+  dirc/baz/largefile
+  dirc/dirb
+  dirc/dirb/largefile
+  $ hg up -qC
+  $ cd ..
+
+Clone a local repository owned by another user
+===================================================
+
+#if unix-permissions
+
+We have to simulate that here by setting $HOME and removing write permissions
+  $ ORIGHOME="$HOME"
+  $ mkdir alice
+  $ HOME="`pwd`/alice"
+  $ cd alice
+  $ hg init pubrepo
+  $ cd pubrepo
+  $ dd if=/dev/zero bs=1k count=11k > a-large-file 2> /dev/null
+  $ hg add --large a-large-file
+  $ hg commit -m "Add a large file"
+  Invoking status precommit hook
+  A a-large-file
+  $ cd ..
+  $ chmod -R a-w pubrepo
+  $ cd ..
+  $ mkdir bob
+  $ HOME="`pwd`/bob"
+  $ cd bob
+  $ hg clone --pull ../alice/pubrepo pubrepo
+  requesting all changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  updating to branch default
+  getting changed largefiles
+  1 largefiles updated, 0 removed
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd ..
+  $ chmod -R u+w alice/pubrepo
+  $ HOME="$ORIGHOME"
+
+#endif
+
+
+Symlink to a large largefile should behave the same as a symlink to a normal file
+=====================================================================================
+
+#if symlink
+
+  $ hg init largesymlink
+  $ cd largesymlink
+  $ dd if=/dev/zero bs=1k count=10k of=largefile 2>/dev/null
+  $ hg add --large largefile
+  $ hg commit -m "commit a large file"
+  Invoking status precommit hook
+  A largefile
+  $ ln -s largefile largelink
+  $ hg add largelink
+  $ hg commit -m "commit a large symlink"
+  Invoking status precommit hook
+  A largelink
+  $ rm -f largelink
+  $ hg up >/dev/null
+  $ test -f largelink
+  [1]
+  $ test -L largelink
+  [1]
+  $ rm -f largelink # make next part of the test independent of the previous
+  $ hg up -C >/dev/null
+  $ test -f largelink
+  $ test -L largelink
+  $ cd ..
+
+#endif
+
+
+test for pattern matching on 'hg status':
+==============================================
+
+
+to boost performance, largefiles checks whether specified patterns are
+related to largefiles in working directory (NOT to STANDIN) or not.
+
+  $ hg init statusmatch
+  $ cd statusmatch
+
+  $ mkdir -p a/b/c/d
+  $ echo normal > a/b/c/d/e.normal.txt
+  $ hg add a/b/c/d/e.normal.txt
+  $ echo large > a/b/c/d/e.large.txt
+  $ hg add --large a/b/c/d/e.large.txt
+  $ mkdir -p a/b/c/x
+  $ echo normal > a/b/c/x/y.normal.txt
+  $ hg add a/b/c/x/y.normal.txt
+  $ hg commit -m 'add files'
+  Invoking status precommit hook
+  A a/b/c/d/e.large.txt
+  A a/b/c/d/e.normal.txt
+  A a/b/c/x/y.normal.txt
+
+(1) no pattern: no performance boost
+  $ hg status -A
+  C a/b/c/d/e.large.txt
+  C a/b/c/d/e.normal.txt
+  C a/b/c/x/y.normal.txt
+
+(2) pattern not related to largefiles: performance boost
+  $ hg status -A a/b/c/x
+  C a/b/c/x/y.normal.txt
+
+(3) pattern related to largefiles: no performance boost
+  $ hg status -A a/b/c/d
+  C a/b/c/d/e.large.txt
+  C a/b/c/d/e.normal.txt
+
+(4) pattern related to STANDIN (not to largefiles): performance boost
+  $ hg status -A .hglf/a
+  C .hglf/a/b/c/d/e.large.txt
+
+(5) mixed case: no performance boost
+  $ hg status -A a/b/c/x a/b/c/d
+  C a/b/c/d/e.large.txt
+  C a/b/c/d/e.normal.txt
+  C a/b/c/x/y.normal.txt
+
+verify that largefiles doesn't break filesets
+
+  $ hg log --rev . --exclude "set:binary()"
+  changeset:   0:41bd42f10efa
+  tag:         tip
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     add files
+  
+verify that large files in subrepos handled properly
+  $ hg init subrepo
+  $ echo "subrepo = subrepo" > .hgsub
+  $ hg add .hgsub
+  $ hg ci -m "add subrepo"
+  Invoking status precommit hook
+  A .hgsub
+  ? .hgsubstate
+  $ echo "rev 1" > subrepo/large.txt
+  $ hg -R subrepo add --large subrepo/large.txt
+  $ hg sum
+  parent: 1:8ee150ea2e9c tip
+   add subrepo
+  branch: default
+  commit: 1 subrepos
+  update: (current)
+  $ hg st
+  $ hg st -S
+  A subrepo/large.txt
+  $ hg ci -S -m "commit top repo"
+  committing subrepository subrepo
+  Invoking status precommit hook
+  A large.txt
+  Invoking status precommit hook
+  M .hgsubstate
+# No differences
+  $ hg st -S
+  $ hg sum
+  parent: 2:ce4cd0c527a6 tip
+   commit top repo
+  branch: default
+  commit: (clean)
+  update: (current)
+  $ echo "rev 2" > subrepo/large.txt
+  $ hg st -S
+  M subrepo/large.txt
+  $ hg sum
+  parent: 2:ce4cd0c527a6 tip
+   commit top repo
+  branch: default
+  commit: 1 subrepos
+  update: (current)
+  $ hg ci -m "this commit should fail without -S"
+  abort: uncommitted changes in subrepo subrepo
+  (use --subrepos for recursive commit)
+  [255]
+
+Add a normal file to the subrepo, then test archiving
+
+  $ echo 'normal file' > subrepo/normal.txt
+  $ hg -R subrepo add subrepo/normal.txt
+
+Lock in subrepo, otherwise the change isn't archived
+
+  $ hg ci -S -m "add normal file to top level"
+  committing subrepository subrepo
+  Invoking status precommit hook
+  M large.txt
+  A normal.txt
+  Invoking status precommit hook
+  M .hgsubstate
+  $ hg archive -S ../lf_subrepo_archive
+  $ find ../lf_subrepo_archive | sort
+  ../lf_subrepo_archive
+  ../lf_subrepo_archive/.hg_archival.txt
+  ../lf_subrepo_archive/.hgsub
+  ../lf_subrepo_archive/.hgsubstate
+  ../lf_subrepo_archive/a
+  ../lf_subrepo_archive/a/b
+  ../lf_subrepo_archive/a/b/c
+  ../lf_subrepo_archive/a/b/c/d
+  ../lf_subrepo_archive/a/b/c/d/e.large.txt
+  ../lf_subrepo_archive/a/b/c/d/e.normal.txt
+  ../lf_subrepo_archive/a/b/c/x
+  ../lf_subrepo_archive/a/b/c/x/y.normal.txt
+  ../lf_subrepo_archive/subrepo
+  ../lf_subrepo_archive/subrepo/large.txt
+  ../lf_subrepo_archive/subrepo/normal.txt
+
+Test update with subrepos.
+
+  $ hg update 0
+  getting changed largefiles
+  0 largefiles updated, 1 removed
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  $ hg status -S
+  $ hg update tip
+  getting changed largefiles
+  1 largefiles updated, 0 removed
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg status -S
+# modify a large file
+  $ echo "modified" > subrepo/large.txt
+  $ hg st -S
+  M subrepo/large.txt
+# update -C should revert the change.
+  $ hg update -C
+  getting changed largefiles
+  1 largefiles updated, 0 removed
+  getting changed largefiles
+  0 largefiles updated, 0 removed
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg status -S
+
+Test archiving a revision that references a subrepo that is not yet
+cloned (see test-subrepo-recursion.t):
+
+  $ hg clone -U . ../empty
+  $ cd ../empty
+  $ hg archive --subrepos -r tip ../archive.tar.gz
+  cloning subrepo subrepo from $TESTTMP/statusmatch/subrepo
+  $ cd ..
+
+
+
+
+
+
+Test addremove, forget and others
+==============================================
+
+Test that addremove picks up largefiles prior to the initial commit (issue3541)
+
+  $ hg init addrm2
+  $ cd addrm2
+  $ touch large.dat
+  $ touch large2.dat
+  $ touch normal
+  $ hg add --large large.dat
+  $ hg addremove -v
+  adding large2.dat as a largefile
+  adding normal
+
+Test that forgetting all largefiles reverts to islfilesrepo() == False
+(addremove will add *.dat as normal files now)
+  $ hg forget large.dat
+  $ hg forget large2.dat
+  $ hg addremove -v
+  adding large.dat
+  adding large2.dat
+
+Test commit's addremove option prior to the first commit
+  $ hg forget large.dat
+  $ hg forget large2.dat
+  $ hg add --large large.dat
+  $ hg ci -Am "commit"
+  adding large2.dat as a largefile
+  Invoking status precommit hook
+  A large.dat
+  A large2.dat
+  A normal
+  $ find .hglf | sort
+  .hglf
+  .hglf/large.dat
+  .hglf/large2.dat
+
+Test actions on largefiles using relative paths from subdir
+
+  $ mkdir sub
+  $ cd sub
+  $ echo anotherlarge > anotherlarge
+  $ hg add --large anotherlarge
+  $ hg st
+  A sub/anotherlarge
+  $ hg st anotherlarge
+  A anotherlarge
+  $ hg commit -m anotherlarge anotherlarge
+  Invoking status precommit hook
+  A sub/anotherlarge
+  $ hg log anotherlarge
+  changeset:   1:9627a577c5e9
+  tag:         tip
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     anotherlarge
+  
+  $ hg log -G anotherlarge
+  @  changeset:   1:9627a577c5e9
+  |  tag:         tip
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     anotherlarge
+  |
+  $ echo more >> anotherlarge
+  $ hg st .
+  M anotherlarge
+  $ hg cat anotherlarge
+  anotherlarge
+  $ hg revert anotherlarge
+  $ hg st
+  ? sub/anotherlarge.orig
+  $ cd ..
+
+  $ cd ..
+
+Check error message while exchange
+=========================================================
+
+issue3651: summary/outgoing with largefiles shows "no remote repo"
+unexpectedly
+
+  $ mkdir issue3651
+  $ cd issue3651
+
+  $ hg init src
+  $ echo a > src/a
+  $ hg -R src add --large src/a
+  $ hg -R src commit -m '#0'
+  Invoking status precommit hook
+  A a
+
+check messages when no remote repository is specified:
+"no remote repo" route for "hg outgoing --large" is not tested here,
+because it can't be reproduced easily.
+
+  $ hg init clone1
+  $ hg -R clone1 -q pull src
+  $ hg -R clone1 -q update
+  $ hg -R clone1 paths | grep default
+  [1]
+
+  $ hg -R clone1 summary --large
+  parent: 0:fc0bd45326d3 tip
+   #0
+  branch: default
+  commit: (clean)
+  update: (current)
+  largefiles: (no remote repo)
+
+check messages when there is no files to upload:
+
+  $ hg -q clone src clone2
+  $ hg -R clone2 paths | grep default
+  default = $TESTTMP/issue3651/src (glob)
+
+  $ hg -R clone2 summary --large
+  parent: 0:fc0bd45326d3 tip
+   #0
+  branch: default
+  commit: (clean)
+  update: (current)
+  largefiles: (no files to upload)
+  $ hg -R clone2 outgoing --large
+  comparing with $TESTTMP/issue3651/src (glob)
+  searching for changes
+  no changes found
+  largefiles: no files to upload
+  [1]
+
+  $ hg -R clone2 outgoing --large --graph --template "{rev}"
+  comparing with $TESTTMP/issue3651/src (glob)
+  searching for changes
+  no changes found
+  largefiles: no files to upload
+
+check messages when there are files to upload:
+
+  $ echo b > clone2/b
+  $ hg -R clone2 add --large clone2/b
+  $ hg -R clone2 commit -m '#1'
+  Invoking status precommit hook
+  A b
+  $ hg -R clone2 summary --large
+  parent: 1:1acbe71ce432 tip
+   #1
+  branch: default
+  commit: (clean)
+  update: (current)
+  largefiles: 1 to upload
+  $ hg -R clone2 outgoing --large
+  comparing with $TESTTMP/issue3651/src (glob)
+  searching for changes
+  changeset:   1:1acbe71ce432
+  tag:         tip
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     #1
+  
+  largefiles to upload:
+  b
+  
+  $ hg -R clone2 outgoing --large --graph --template "{rev}"
+  comparing with $TESTTMP/issue3651/src
+  searching for changes
+  @  1
+  
+  largefiles to upload:
+  b
+  
+
+  $ cd ..
+
+merge action 'd' for 'local renamed directory to d2/g' which has no filename
+==================================================================================
+
+  $ hg init merge-action
+  $ cd merge-action
+  $ touch l
+  $ hg add --large l
+  $ mkdir d1
+  $ touch d1/f
+  $ hg ci -Aqm0
+  Invoking status precommit hook
+  A d1/f
+  A l
+  $ echo > d1/f
+  $ touch d1/g
+  $ hg ci -Aqm1
+  Invoking status precommit hook
+  M d1/f
+  A d1/g
+  $ hg up -qr0
+  $ hg mv d1 d2
+  moving d1/f to d2/f (glob)
+  $ hg ci -qm2
+  Invoking status precommit hook
+  A d2/f
+  R d1/f
+  $ hg merge
+  merging d2/f and d1/f to d2/f
+  1 files updated, 1 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+  getting changed largefiles
+  0 largefiles updated, 0 removed
+  $ cd ..
+
+
+Merge conflicts:
+=====================
+
+  $ hg init merge
+  $ cd merge
+  $ echo 0 > f-different
+  $ echo 0 > f-same
+  $ echo 0 > f-unchanged-1
+  $ echo 0 > f-unchanged-2
+  $ hg add --large *
+  $ hg ci -m0
+  Invoking status precommit hook
+  A f-different
+  A f-same
+  A f-unchanged-1
+  A f-unchanged-2
+  $ echo tmp1 > f-unchanged-1
+  $ echo tmp1 > f-unchanged-2
+  $ echo tmp1 > f-same
+  $ hg ci -m1
+  Invoking status precommit hook
+  M f-same
+  M f-unchanged-1
+  M f-unchanged-2
+  $ echo 2 > f-different
+  $ echo 0 > f-unchanged-1
+  $ echo 1 > f-unchanged-2
+  $ echo 1 > f-same
+  $ hg ci -m2
+  Invoking status precommit hook
+  M f-different
+  M f-same
+  M f-unchanged-1
+  M f-unchanged-2
+  $ hg up -qr0
+  $ echo tmp2 > f-unchanged-1
+  $ echo tmp2 > f-unchanged-2
+  $ echo tmp2 > f-same
+  $ hg ci -m3
+  Invoking status precommit hook
+  M f-same
+  M f-unchanged-1
+  M f-unchanged-2
+  created new head
+  $ echo 1 > f-different
+  $ echo 1 > f-unchanged-1
+  $ echo 0 > f-unchanged-2
+  $ echo 1 > f-same
+  $ hg ci -m4
+  Invoking status precommit hook
+  M f-different
+  M f-same
+  M f-unchanged-1
+  M f-unchanged-2
+  $ hg merge
+  largefile f-different has a merge conflict
+  ancestor was 09d2af8dd22201dd8d48e5dcfcaed281ff9422c7
+  keep (l)ocal e5fa44f2b31c1fb553b6021e7360d07d5d91ff5e or
+  take (o)ther 7448d8798a4380162d4b56f9b452e2f6f9e24e7a? l
+  0 files updated, 4 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+  getting changed largefiles
+  1 largefiles updated, 0 removed
+  $ cat f-different
+  1
+  $ cat f-same
+  1
+  $ cat f-unchanged-1
+  1
+  $ cat f-unchanged-2
+  1
+  $ cd ..
+
+Test largefile insulation (do not enabled a side effect
+========================================================
+
+Check whether "largefiles" feature is supported only in repositories
+enabling largefiles extension.
+
+  $ mkdir individualenabling
+  $ cd individualenabling
+
+  $ hg init enabledlocally
+  $ echo large > enabledlocally/large
+  $ hg -R enabledlocally add --large enabledlocally/large
+  $ hg -R enabledlocally commit -m '#0'
+  Invoking status precommit hook
+  A large
+
+  $ hg init notenabledlocally
+  $ echo large > notenabledlocally/large
+  $ hg -R notenabledlocally add --large notenabledlocally/large
+  $ hg -R notenabledlocally commit -m '#0'
+  Invoking status precommit hook
+  A large
+
+  $ cat >> $HGRCPATH <<EOF
+  > [extensions]
+  > # disable globally
+  > largefiles=!
+  > EOF
+  $ cat >> enabledlocally/.hg/hgrc <<EOF
+  > [extensions]
+  > # enable locally
+  > largefiles=
+  > EOF
+  $ hg -R enabledlocally root
+  $TESTTMP/individualenabling/enabledlocally (glob)
+  $ hg -R notenabledlocally root
+  abort: repository requires features unknown to this Mercurial: largefiles!
+  (see http://mercurial.selenic.com/wiki/MissingRequirement for more information)
+  [255]
+
+  $ hg init push-dst
+  $ hg -R enabledlocally push push-dst
+  pushing to push-dst
+  abort: required features are not supported in the destination: largefiles
+  [255]
+
+  $ hg init pull-src
+  $ hg -R pull-src pull enabledlocally
+  pulling from enabledlocally
+  abort: required features are not supported in the destination: largefiles
+  [255]
+
+  $ hg clone enabledlocally clone-dst
+  abort: repository requires features unknown to this Mercurial: largefiles!
+  (see http://mercurial.selenic.com/wiki/MissingRequirement for more information)
+  [255]
+  $ test -d clone-dst
+  [1]
+  $ hg clone --pull enabledlocally clone-pull-dst
+  abort: required features are not supported in the destination: largefiles
+  [255]
+  $ test -d clone-pull-dst
+  [1]
+
+#if serve
+
+Test largefiles specific peer setup, when largefiles is enabled
+locally (issue4109)
+
+  $ hg showconfig extensions | grep largefiles
+  extensions.largefiles=!
+  $ mkdir -p $TESTTMP/individualenabling/usercache
+
+  $ hg serve -R enabledlocally -d -p $HGPORT --pid-file hg.pid
+  $ cat hg.pid >> $DAEMON_PIDS
+
+  $ hg init pull-dst
+  $ cat > pull-dst/.hg/hgrc <<EOF
+  > [extensions]
+  > # enable locally
+  > largefiles=
+  > [largefiles]
+  > # ignore system cache to force largefiles specific wire proto access
+  > usercache=$TESTTMP/individualenabling/usercache
+  > EOF
+  $ hg -R pull-dst -q pull -u http://localhost:$HGPORT
+
+  $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS
+#endif
+
+  $ cd ..
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-largefiles-wireproto.t	Sat Jun 14 11:56:20 2014 -0500
@@ -0,0 +1,293 @@
+This file contains testcases that tend to be related to the wireprotocol part of
+largefile.
+
+  $ USERCACHE="$TESTTMP/cache"; export USERCACHE
+  $ mkdir "${USERCACHE}"
+  $ cat >> $HGRCPATH <<EOF
+  > [extensions]
+  > largefiles=
+  > purge=
+  > rebase=
+  > transplant=
+  > [phases]
+  > publish=False
+  > [largefiles]
+  > minsize=2
+  > patterns=glob:**.dat
+  > usercache=${USERCACHE}
+  > [hooks]
+  > precommit=sh -c "echo \\"Invoking status precommit hook\\"; hg status"
+  > EOF
+
+
+#if serve
+vanilla clients not locked out from largefiles servers on vanilla repos
+  $ mkdir r1
+  $ cd r1
+  $ hg init
+  $ echo c1 > f1
+  $ hg add f1
+  $ hg commit -m "m1"
+  Invoking status precommit hook
+  A f1
+  $ cd ..
+  $ hg serve -R r1 -d -p $HGPORT --pid-file hg.pid
+  $ cat hg.pid >> $DAEMON_PIDS
+  $ hg --config extensions.largefiles=! clone http://localhost:$HGPORT r2
+  requesting all changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  updating to branch default
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+
+largefiles clients still work with vanilla servers
+  $ hg --config extensions.largefiles=! serve -R r1 -d -p $HGPORT1 --pid-file hg.pid
+  $ cat hg.pid >> $DAEMON_PIDS
+  $ hg clone http://localhost:$HGPORT1 r3
+  requesting all changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  updating to branch default
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+#endif
+
+vanilla clients locked out from largefiles http repos
+  $ mkdir r4
+  $ cd r4
+  $ hg init
+  $ echo c1 > f1
+  $ hg add --large f1
+  $ hg commit -m "m1"
+  Invoking status precommit hook
+  A f1
+  $ cd ..
+
+largefiles can be pushed locally (issue3583)
+  $ hg init dest
+  $ cd r4
+  $ hg outgoing ../dest
+  comparing with ../dest
+  searching for changes
+  changeset:   0:639881c12b4c
+  tag:         tip
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     m1
+  
+  $ hg push ../dest
+  pushing to ../dest
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+
+exit code with nothing outgoing (issue3611)
+  $ hg outgoing ../dest
+  comparing with ../dest
+  searching for changes
+  no changes found
+  [1]
+  $ cd ..
+
+#if serve
+  $ hg serve -R r4 -d -p $HGPORT2 --pid-file hg.pid
+  $ cat hg.pid >> $DAEMON_PIDS
+  $ hg --config extensions.largefiles=! clone http://localhost:$HGPORT2 r5
+  abort: remote error:
+  
+  This repository uses the largefiles extension.
+  
+  Please enable it in your Mercurial config file.
+  [255]
+
+used all HGPORTs, kill all daemons
+  $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS
+#endif
+
+vanilla clients locked out from largefiles ssh repos
+  $ hg --config extensions.largefiles=! clone -e "python \"$TESTDIR/dummyssh\"" ssh://user@dummy/r4 r5
+  abort: remote error:
+  
+  This repository uses the largefiles extension.
+  
+  Please enable it in your Mercurial config file.
+  [255]
+
+#if serve
+
+largefiles clients refuse to push largefiles repos to vanilla servers
+  $ mkdir r6
+  $ cd r6
+  $ hg init
+  $ echo c1 > f1
+  $ hg add f1
+  $ hg commit -m "m1"
+  Invoking status precommit hook
+  A f1
+  $ cat >> .hg/hgrc <<!
+  > [web]
+  > push_ssl = false
+  > allow_push = *
+  > !
+  $ cd ..
+  $ hg clone r6 r7
+  updating to branch default
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd r7
+  $ echo c2 > f2
+  $ hg add --large f2
+  $ hg commit -m "m2"
+  Invoking status precommit hook
+  A f2
+  $ hg --config extensions.largefiles=! -R ../r6 serve -d -p $HGPORT --pid-file ../hg.pid
+  $ cat ../hg.pid >> $DAEMON_PIDS
+  $ hg push http://localhost:$HGPORT
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  abort: http://localhost:$HGPORT/ does not appear to be a largefile store
+  [255]
+  $ cd ..
+
+putlfile errors are shown (issue3123)
+Corrupt the cached largefile in r7 and move it out of the servers usercache
+  $ mv r7/.hg/largefiles/4cdac4d8b084d0b599525cf732437fb337d422a8 .
+  $ echo 'client side corruption' > r7/.hg/largefiles/4cdac4d8b084d0b599525cf732437fb337d422a8
+  $ rm "$USERCACHE/4cdac4d8b084d0b599525cf732437fb337d422a8"
+  $ hg init empty
+  $ hg serve -R empty -d -p $HGPORT1 --pid-file hg.pid \
+  >   --config 'web.allow_push=*' --config web.push_ssl=False
+  $ cat hg.pid >> $DAEMON_PIDS
+  $ hg push -R r7 http://localhost:$HGPORT1
+  pushing to http://localhost:$HGPORT1/
+  searching for changes
+  remote: largefiles: failed to put 4cdac4d8b084d0b599525cf732437fb337d422a8 into store: largefile contents do not match hash
+  abort: remotestore: could not put $TESTTMP/r7/.hg/largefiles/4cdac4d8b084d0b599525cf732437fb337d422a8 to remote store http://localhost:$HGPORT1/ (glob)
+  [255]
+  $ mv 4cdac4d8b084d0b599525cf732437fb337d422a8 r7/.hg/largefiles/4cdac4d8b084d0b599525cf732437fb337d422a8
+Push of file that exists on server but is corrupted - magic healing would be nice ... but too magic
+  $ echo "server side corruption" > empty/.hg/largefiles/4cdac4d8b084d0b599525cf732437fb337d422a8
+  $ hg push -R r7 http://localhost:$HGPORT1
+  pushing to http://localhost:$HGPORT1/
+  searching for changes
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 2 changesets with 2 changes to 2 files
+  $ cat empty/.hg/largefiles/4cdac4d8b084d0b599525cf732437fb337d422a8
+  server side corruption
+  $ rm -rf empty
+
+Push a largefiles repository to a served empty repository
+  $ hg init r8
+  $ echo c3 > r8/f1
+  $ hg add --large r8/f1 -R r8
+  $ hg commit -m "m1" -R r8
+  Invoking status precommit hook
+  A f1
+  $ hg init empty
+  $ hg serve -R empty -d -p $HGPORT2 --pid-file hg.pid \
+  >   --config 'web.allow_push=*' --config web.push_ssl=False
+  $ cat hg.pid >> $DAEMON_PIDS
+  $ rm "${USERCACHE}"/*
+  $ hg push -R r8 http://localhost:$HGPORT2/#default
+  pushing to http://localhost:$HGPORT2/
+  searching for changes
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 1 changesets with 1 changes to 1 files
+  $ [ -f "${USERCACHE}"/02a439e5c31c526465ab1a0ca1f431f76b827b90 ]
+  $ [ -f empty/.hg/largefiles/02a439e5c31c526465ab1a0ca1f431f76b827b90 ]
+
+Clone over http, no largefiles pulled on clone.
+
+  $ hg clone http://localhost:$HGPORT2/#default http-clone -U
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+
+test 'verify' with remotestore:
+
+  $ rm "${USERCACHE}"/02a439e5c31c526465ab1a0ca1f431f76b827b90
+  $ mv empty/.hg/largefiles/02a439e5c31c526465ab1a0ca1f431f76b827b90 .
+  $ hg -R http-clone verify --large --lfa
+  checking changesets
+  checking manifests
+  crosschecking files in changesets and manifests
+  checking files
+  1 files, 1 changesets, 1 total revisions
+  searching 1 changesets for largefiles
+  changeset 0:cf03e5bb9936: f1 missing
+  verified existence of 1 revisions of 1 largefiles
+  [1]
+  $ mv 02a439e5c31c526465ab1a0ca1f431f76b827b90 empty/.hg/largefiles/
+  $ hg -R http-clone -q verify --large --lfa
+
+largefiles pulled on update - a largefile missing on the server:
+  $ mv empty/.hg/largefiles/02a439e5c31c526465ab1a0ca1f431f76b827b90 .
+  $ hg -R http-clone up --config largefiles.usercache=http-clone-usercache
+  getting changed largefiles
+  f1: largefile 02a439e5c31c526465ab1a0ca1f431f76b827b90 not available from http://localhost:$HGPORT2/
+  0 largefiles updated, 0 removed
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg -R http-clone st
+  ! f1
+  $ hg -R http-clone up -Cqr null
+
+largefiles pulled on update - a largefile corrupted on the server:
+  $ echo corruption > empty/.hg/largefiles/02a439e5c31c526465ab1a0ca1f431f76b827b90
+  $ hg -R http-clone up --config largefiles.usercache=http-clone-usercache
+  getting changed largefiles
+  f1: data corruption (expected 02a439e5c31c526465ab1a0ca1f431f76b827b90, got 6a7bb2556144babe3899b25e5428123735bb1e27)
+  0 largefiles updated, 0 removed
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg -R http-clone st
+  ! f1
+  $ [ ! -f http-clone/.hg/largefiles/02a439e5c31c526465ab1a0ca1f431f76b827b90 ]
+  $ [ ! -f http-clone/f1 ]
+  $ [ ! -f http-clone-usercache ]
+  $ hg -R http-clone verify --large --lfc
+  checking changesets
+  checking manifests
+  crosschecking files in changesets and manifests
+  checking files
+  1 files, 1 changesets, 1 total revisions
+  searching 1 changesets for largefiles
+  verified contents of 1 revisions of 1 largefiles
+  $ hg -R http-clone up -Cqr null
+
+largefiles pulled on update - no server side problems:
+  $ mv 02a439e5c31c526465ab1a0ca1f431f76b827b90 empty/.hg/largefiles/
+  $ hg -R http-clone --debug up --config largefiles.usercache=http-clone-usercache
+  resolving manifests
+   branchmerge: False, force: False, partial: False
+   ancestor: 000000000000, local: 000000000000+, remote: cf03e5bb9936
+   .hglf/f1: remote created -> g
+  getting .hglf/f1
+  updating: .hglf/f1 1/1 files (100.00%)
+  getting changed largefiles
+  using http://localhost:$HGPORT2/
+  sending capabilities command
+  sending batch command
+  getting largefiles: 0/1 lfile (0.00%)
+  getting f1:02a439e5c31c526465ab1a0ca1f431f76b827b90
+  sending getlfile command
+  found 02a439e5c31c526465ab1a0ca1f431f76b827b90 in store
+  1 largefiles updated, 0 removed
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+
+  $ ls http-clone-usercache/*
+  http-clone-usercache/02a439e5c31c526465ab1a0ca1f431f76b827b90
+
+  $ rm -rf empty http-clone*
+
+used all HGPORTs, kill all daemons
+  $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS
+
+#endif
--- a/tests/test-largefiles.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-largefiles.t	Sat Jun 14 11:56:20 2014 -0500
@@ -1,3 +1,8 @@
+This file used to contains all largefile tests.
+Do not add any new tests in this file as it his already far too long to run.
+
+It contains all the testing of the basic concepts of large file in a single block.
+
   $ USERCACHE="$TESTTMP/cache"; export USERCACHE
   $ mkdir "${USERCACHE}"
   $ cat >> $HGRCPATH <<EOF
@@ -180,52 +185,6 @@
   $ cat sub/large4
   large22
 
-Test copies and moves from a directory other than root (issue3516)
-
-  $ cd ..
-  $ hg init lf_cpmv
-  $ cd lf_cpmv
-  $ mkdir dira
-  $ mkdir dira/dirb
-  $ touch dira/dirb/largefile
-  $ hg add --large dira/dirb/largefile
-  $ hg commit -m "added"
-  Invoking status precommit hook
-  A dira/dirb/largefile
-  $ cd dira
-  $ hg cp dirb/largefile foo/largefile
-  $ hg ci -m "deep copy"
-  Invoking status precommit hook
-  A dira/foo/largefile
-  $ find . | sort
-  .
-  ./dirb
-  ./dirb/largefile
-  ./foo
-  ./foo/largefile
-  $ hg mv foo/largefile baz/largefile
-  $ hg ci -m "moved"
-  Invoking status precommit hook
-  A dira/baz/largefile
-  R dira/foo/largefile
-  $ find . | sort
-  .
-  ./baz
-  ./baz/largefile
-  ./dirb
-  ./dirb/largefile
-  $ cd ..
-  $ hg mv dira dirc
-  moving .hglf/dira/baz/largefile to .hglf/dirc/baz/largefile (glob)
-  moving .hglf/dira/dirb/largefile to .hglf/dirc/dirb/largefile (glob)
-  $ find * | sort
-  dirc
-  dirc/baz
-  dirc/baz/largefile
-  dirc/dirb
-  dirc/dirb/largefile
-  $ hg up -qC
-  $ cd ../a
 
 #if serve
 Test display of largefiles in hgweb
@@ -390,6 +349,14 @@
   A sub2/large7
   A z/y/x/large2
   A z/y/x/m/large1
+
+(and a bit of log testing)
+
+  $ hg log -T '{rev}\n' z/y/x/m/large1
+  7
+  $ hg log -T '{rev}\n' z/y/x/m  # with only a largefile
+  7
+
   $ hg rollback --quiet
   $ touch z/y/x/m/normal
   $ hg add z/y/x/m/normal
@@ -1665,873 +1632,5 @@
   (use 'hg revert new-largefile' to cancel the pending addition)
   $ cd ..
 
-#if serve
-vanilla clients not locked out from largefiles servers on vanilla repos
-  $ mkdir r1
-  $ cd r1
-  $ hg init
-  $ echo c1 > f1
-  $ hg add f1
-  $ hg commit -m "m1"
-  Invoking status precommit hook
-  A f1
-  $ cd ..
-  $ hg serve -R r1 -d -p $HGPORT --pid-file hg.pid
-  $ cat hg.pid >> $DAEMON_PIDS
-  $ hg --config extensions.largefiles=! clone http://localhost:$HGPORT r2
-  requesting all changes
-  adding changesets
-  adding manifests
-  adding file changes
-  added 1 changesets with 1 changes to 1 files
-  updating to branch default
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-
-largefiles clients still work with vanilla servers
-  $ hg --config extensions.largefiles=! serve -R r1 -d -p $HGPORT1 --pid-file hg.pid
-  $ cat hg.pid >> $DAEMON_PIDS
-  $ hg clone http://localhost:$HGPORT1 r3
-  requesting all changes
-  adding changesets
-  adding manifests
-  adding file changes
-  added 1 changesets with 1 changes to 1 files
-  updating to branch default
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-#endif
-
-
-vanilla clients locked out from largefiles http repos
-  $ mkdir r4
-  $ cd r4
-  $ hg init
-  $ echo c1 > f1
-  $ hg add --large f1
-  $ hg commit -m "m1"
-  Invoking status precommit hook
-  A f1
-  $ cd ..
-
-largefiles can be pushed locally (issue3583)
-  $ hg init dest
-  $ cd r4
-  $ hg outgoing ../dest
-  comparing with ../dest
-  searching for changes
-  changeset:   0:639881c12b4c
-  tag:         tip
-  user:        test
-  date:        Thu Jan 01 00:00:00 1970 +0000
-  summary:     m1
-  
-  $ hg push ../dest
-  pushing to ../dest
-  searching for changes
-  adding changesets
-  adding manifests
-  adding file changes
-  added 1 changesets with 1 changes to 1 files
-
-exit code with nothing outgoing (issue3611)
-  $ hg outgoing ../dest
-  comparing with ../dest
-  searching for changes
-  no changes found
-  [1]
-  $ cd ..
-
-#if serve
-  $ hg serve -R r4 -d -p $HGPORT2 --pid-file hg.pid
-  $ cat hg.pid >> $DAEMON_PIDS
-  $ hg --config extensions.largefiles=! clone http://localhost:$HGPORT2 r5
-  abort: remote error:
-  
-  This repository uses the largefiles extension.
-  
-  Please enable it in your Mercurial config file.
-  [255]
-
-used all HGPORTs, kill all daemons
-  $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS
-#endif
-
-vanilla clients locked out from largefiles ssh repos
-  $ hg --config extensions.largefiles=! clone -e "python \"$TESTDIR/dummyssh\"" ssh://user@dummy/r4 r5
-  abort: remote error:
-  
-  This repository uses the largefiles extension.
-  
-  Please enable it in your Mercurial config file.
-  [255]
-
-#if serve
-
-largefiles clients refuse to push largefiles repos to vanilla servers
-  $ mkdir r6
-  $ cd r6
-  $ hg init
-  $ echo c1 > f1
-  $ hg add f1
-  $ hg commit -m "m1"
-  Invoking status precommit hook
-  A f1
-  $ cat >> .hg/hgrc <<!
-  > [web]
-  > push_ssl = false
-  > allow_push = *
-  > !
-  $ cd ..
-  $ hg clone r6 r7
-  updating to branch default
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ cd r7
-  $ echo c2 > f2
-  $ hg add --large f2
-  $ hg commit -m "m2"
-  Invoking status precommit hook
-  A f2
-  $ hg --config extensions.largefiles=! -R ../r6 serve -d -p $HGPORT --pid-file ../hg.pid
-  $ cat ../hg.pid >> $DAEMON_PIDS
-  $ hg push http://localhost:$HGPORT
-  pushing to http://localhost:$HGPORT/
-  searching for changes
-  abort: http://localhost:$HGPORT/ does not appear to be a largefile store
-  [255]
-  $ cd ..
-
-putlfile errors are shown (issue3123)
-Corrupt the cached largefile in r7 and move it out of the servers usercache
-  $ mv r7/.hg/largefiles/4cdac4d8b084d0b599525cf732437fb337d422a8 .
-  $ echo 'client side corruption' > r7/.hg/largefiles/4cdac4d8b084d0b599525cf732437fb337d422a8
-  $ rm "$USERCACHE/4cdac4d8b084d0b599525cf732437fb337d422a8"
-  $ hg init empty
-  $ hg serve -R empty -d -p $HGPORT1 --pid-file hg.pid \
-  >   --config 'web.allow_push=*' --config web.push_ssl=False
-  $ cat hg.pid >> $DAEMON_PIDS
-  $ hg push -R r7 http://localhost:$HGPORT1
-  pushing to http://localhost:$HGPORT1/
-  searching for changes
-  remote: largefiles: failed to put 4cdac4d8b084d0b599525cf732437fb337d422a8 into store: largefile contents do not match hash
-  abort: remotestore: could not put $TESTTMP/r7/.hg/largefiles/4cdac4d8b084d0b599525cf732437fb337d422a8 to remote store http://localhost:$HGPORT1/ (glob)
-  [255]
-  $ mv 4cdac4d8b084d0b599525cf732437fb337d422a8 r7/.hg/largefiles/4cdac4d8b084d0b599525cf732437fb337d422a8
-Push of file that exists on server but is corrupted - magic healing would be nice ... but too magic
-  $ echo "server side corruption" > empty/.hg/largefiles/4cdac4d8b084d0b599525cf732437fb337d422a8
-  $ hg push -R r7 http://localhost:$HGPORT1
-  pushing to http://localhost:$HGPORT1/
-  searching for changes
-  remote: adding changesets
-  remote: adding manifests
-  remote: adding file changes
-  remote: added 2 changesets with 2 changes to 2 files
-  $ cat empty/.hg/largefiles/4cdac4d8b084d0b599525cf732437fb337d422a8
-  server side corruption
-  $ rm -rf empty
-
-Push a largefiles repository to a served empty repository
-  $ hg init r8
-  $ echo c3 > r8/f1
-  $ hg add --large r8/f1 -R r8
-  $ hg commit -m "m1" -R r8
-  Invoking status precommit hook
-  A f1
-  $ hg init empty
-  $ hg serve -R empty -d -p $HGPORT2 --pid-file hg.pid \
-  >   --config 'web.allow_push=*' --config web.push_ssl=False
-  $ cat hg.pid >> $DAEMON_PIDS
-  $ rm "${USERCACHE}"/*
-  $ hg push -R r8 http://localhost:$HGPORT2/#default
-  pushing to http://localhost:$HGPORT2/
-  searching for changes
-  remote: adding changesets
-  remote: adding manifests
-  remote: adding file changes
-  remote: added 1 changesets with 1 changes to 1 files
-  $ [ -f "${USERCACHE}"/02a439e5c31c526465ab1a0ca1f431f76b827b90 ]
-  $ [ -f empty/.hg/largefiles/02a439e5c31c526465ab1a0ca1f431f76b827b90 ]
-
-Clone over http, no largefiles pulled on clone.
-
-  $ hg clone http://localhost:$HGPORT2/#default http-clone -U
-  adding changesets
-  adding manifests
-  adding file changes
-  added 1 changesets with 1 changes to 1 files
-
-test 'verify' with remotestore:
-
-  $ rm "${USERCACHE}"/02a439e5c31c526465ab1a0ca1f431f76b827b90
-  $ mv empty/.hg/largefiles/02a439e5c31c526465ab1a0ca1f431f76b827b90 .
-  $ hg -R http-clone verify --large --lfa
-  checking changesets
-  checking manifests
-  crosschecking files in changesets and manifests
-  checking files
-  1 files, 1 changesets, 1 total revisions
-  searching 1 changesets for largefiles
-  changeset 0:cf03e5bb9936: f1 missing
-  verified existence of 1 revisions of 1 largefiles
-  [1]
-  $ mv 02a439e5c31c526465ab1a0ca1f431f76b827b90 empty/.hg/largefiles/
-  $ hg -R http-clone -q verify --large --lfa
-
-largefiles pulled on update - a largefile missing on the server:
-  $ mv empty/.hg/largefiles/02a439e5c31c526465ab1a0ca1f431f76b827b90 .
-  $ hg -R http-clone up --config largefiles.usercache=http-clone-usercache
-  getting changed largefiles
-  f1: largefile 02a439e5c31c526465ab1a0ca1f431f76b827b90 not available from http://localhost:$HGPORT2/
-  0 largefiles updated, 0 removed
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ hg -R http-clone st
-  ! f1
-  $ hg -R http-clone up -Cqr null
-
-largefiles pulled on update - a largefile corrupted on the server:
-  $ echo corruption > empty/.hg/largefiles/02a439e5c31c526465ab1a0ca1f431f76b827b90
-  $ hg -R http-clone up --config largefiles.usercache=http-clone-usercache
-  getting changed largefiles
-  f1: data corruption (expected 02a439e5c31c526465ab1a0ca1f431f76b827b90, got 6a7bb2556144babe3899b25e5428123735bb1e27)
-  0 largefiles updated, 0 removed
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ hg -R http-clone st
-  ! f1
-  $ [ ! -f http-clone/.hg/largefiles/02a439e5c31c526465ab1a0ca1f431f76b827b90 ]
-  $ [ ! -f http-clone/f1 ]
-  $ [ ! -f http-clone-usercache ]
-  $ hg -R http-clone verify --large --lfc
-  checking changesets
-  checking manifests
-  crosschecking files in changesets and manifests
-  checking files
-  1 files, 1 changesets, 1 total revisions
-  searching 1 changesets for largefiles
-  verified contents of 1 revisions of 1 largefiles
-  $ hg -R http-clone up -Cqr null
-
-largefiles pulled on update - no server side problems:
-  $ mv 02a439e5c31c526465ab1a0ca1f431f76b827b90 empty/.hg/largefiles/
-  $ hg -R http-clone --debug up --config largefiles.usercache=http-clone-usercache
-  resolving manifests
-   branchmerge: False, force: False, partial: False
-   ancestor: 000000000000, local: 000000000000+, remote: cf03e5bb9936
-   .hglf/f1: remote created -> g
-  getting .hglf/f1
-  updating: .hglf/f1 1/1 files (100.00%)
-  getting changed largefiles
-  using http://localhost:$HGPORT2/
-  sending capabilities command
-  sending batch command
-  getting largefiles: 0/1 lfile (0.00%)
-  getting f1:02a439e5c31c526465ab1a0ca1f431f76b827b90
-  sending getlfile command
-  found 02a439e5c31c526465ab1a0ca1f431f76b827b90 in store
-  1 largefiles updated, 0 removed
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-
-  $ ls http-clone-usercache/*
-  http-clone-usercache/02a439e5c31c526465ab1a0ca1f431f76b827b90
-
-  $ rm -rf empty http-clone*
-
-used all HGPORTs, kill all daemons
-  $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS
-
-#endif
 
 
-#if unix-permissions
-
-Clone a local repository owned by another user
-We have to simulate that here by setting $HOME and removing write permissions
-  $ ORIGHOME="$HOME"
-  $ mkdir alice
-  $ HOME="`pwd`/alice"
-  $ cd alice
-  $ hg init pubrepo
-  $ cd pubrepo
-  $ dd if=/dev/zero bs=1k count=11k > a-large-file 2> /dev/null
-  $ hg add --large a-large-file
-  $ hg commit -m "Add a large file"
-  Invoking status precommit hook
-  A a-large-file
-  $ cd ..
-  $ chmod -R a-w pubrepo
-  $ cd ..
-  $ mkdir bob
-  $ HOME="`pwd`/bob"
-  $ cd bob
-  $ hg clone --pull ../alice/pubrepo pubrepo
-  requesting all changes
-  adding changesets
-  adding manifests
-  adding file changes
-  added 1 changesets with 1 changes to 1 files
-  updating to branch default
-  getting changed largefiles
-  1 largefiles updated, 0 removed
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ cd ..
-  $ chmod -R u+w alice/pubrepo
-  $ HOME="$ORIGHOME"
-
-#endif
-
-#if symlink
-
-Symlink to a large largefile should behave the same as a symlink to a normal file
-  $ hg init largesymlink
-  $ cd largesymlink
-  $ dd if=/dev/zero bs=1k count=10k of=largefile 2>/dev/null
-  $ hg add --large largefile
-  $ hg commit -m "commit a large file"
-  Invoking status precommit hook
-  A largefile
-  $ ln -s largefile largelink
-  $ hg add largelink
-  $ hg commit -m "commit a large symlink"
-  Invoking status precommit hook
-  A largelink
-  $ rm -f largelink
-  $ hg up >/dev/null
-  $ test -f largelink
-  [1]
-  $ test -L largelink
-  [1]
-  $ rm -f largelink # make next part of the test independent of the previous
-  $ hg up -C >/dev/null
-  $ test -f largelink
-  $ test -L largelink
-  $ cd ..
-
-#endif
-
-test for pattern matching on 'hg status':
-to boost performance, largefiles checks whether specified patterns are
-related to largefiles in working directory (NOT to STANDIN) or not.
-
-  $ hg init statusmatch
-  $ cd statusmatch
-
-  $ mkdir -p a/b/c/d
-  $ echo normal > a/b/c/d/e.normal.txt
-  $ hg add a/b/c/d/e.normal.txt
-  $ echo large > a/b/c/d/e.large.txt
-  $ hg add --large a/b/c/d/e.large.txt
-  $ mkdir -p a/b/c/x
-  $ echo normal > a/b/c/x/y.normal.txt
-  $ hg add a/b/c/x/y.normal.txt
-  $ hg commit -m 'add files'
-  Invoking status precommit hook
-  A a/b/c/d/e.large.txt
-  A a/b/c/d/e.normal.txt
-  A a/b/c/x/y.normal.txt
-
-(1) no pattern: no performance boost
-  $ hg status -A
-  C a/b/c/d/e.large.txt
-  C a/b/c/d/e.normal.txt
-  C a/b/c/x/y.normal.txt
-
-(2) pattern not related to largefiles: performance boost
-  $ hg status -A a/b/c/x
-  C a/b/c/x/y.normal.txt
-
-(3) pattern related to largefiles: no performance boost
-  $ hg status -A a/b/c/d
-  C a/b/c/d/e.large.txt
-  C a/b/c/d/e.normal.txt
-
-(4) pattern related to STANDIN (not to largefiles): performance boost
-  $ hg status -A .hglf/a
-  C .hglf/a/b/c/d/e.large.txt
-
-(5) mixed case: no performance boost
-  $ hg status -A a/b/c/x a/b/c/d
-  C a/b/c/d/e.large.txt
-  C a/b/c/d/e.normal.txt
-  C a/b/c/x/y.normal.txt
-
-verify that largefiles doesn't break filesets
-
-  $ hg log --rev . --exclude "set:binary()"
-  changeset:   0:41bd42f10efa
-  tag:         tip
-  user:        test
-  date:        Thu Jan 01 00:00:00 1970 +0000
-  summary:     add files
-  
-verify that large files in subrepos handled properly
-  $ hg init subrepo
-  $ echo "subrepo = subrepo" > .hgsub
-  $ hg add .hgsub
-  $ hg ci -m "add subrepo"
-  Invoking status precommit hook
-  A .hgsub
-  ? .hgsubstate
-  $ echo "rev 1" > subrepo/large.txt
-  $ hg -R subrepo add --large subrepo/large.txt
-  $ hg sum
-  parent: 1:8ee150ea2e9c tip
-   add subrepo
-  branch: default
-  commit: 1 subrepos
-  update: (current)
-  $ hg st
-  $ hg st -S
-  A subrepo/large.txt
-  $ hg ci -S -m "commit top repo"
-  committing subrepository subrepo
-  Invoking status precommit hook
-  A large.txt
-  Invoking status precommit hook
-  M .hgsubstate
-# No differences
-  $ hg st -S
-  $ hg sum
-  parent: 2:ce4cd0c527a6 tip
-   commit top repo
-  branch: default
-  commit: (clean)
-  update: (current)
-  $ echo "rev 2" > subrepo/large.txt
-  $ hg st -S
-  M subrepo/large.txt
-  $ hg sum
-  parent: 2:ce4cd0c527a6 tip
-   commit top repo
-  branch: default
-  commit: 1 subrepos
-  update: (current)
-  $ hg ci -m "this commit should fail without -S"
-  abort: uncommitted changes in subrepo subrepo
-  (use --subrepos for recursive commit)
-  [255]
-
-Add a normal file to the subrepo, then test archiving
-
-  $ echo 'normal file' > subrepo/normal.txt
-  $ hg -R subrepo add subrepo/normal.txt
-
-Lock in subrepo, otherwise the change isn't archived
-
-  $ hg ci -S -m "add normal file to top level"
-  committing subrepository subrepo
-  Invoking status precommit hook
-  M large.txt
-  A normal.txt
-  Invoking status precommit hook
-  M .hgsubstate
-  $ hg archive -S ../lf_subrepo_archive
-  $ find ../lf_subrepo_archive | sort
-  ../lf_subrepo_archive
-  ../lf_subrepo_archive/.hg_archival.txt
-  ../lf_subrepo_archive/.hgsub
-  ../lf_subrepo_archive/.hgsubstate
-  ../lf_subrepo_archive/a
-  ../lf_subrepo_archive/a/b
-  ../lf_subrepo_archive/a/b/c
-  ../lf_subrepo_archive/a/b/c/d
-  ../lf_subrepo_archive/a/b/c/d/e.large.txt
-  ../lf_subrepo_archive/a/b/c/d/e.normal.txt
-  ../lf_subrepo_archive/a/b/c/x
-  ../lf_subrepo_archive/a/b/c/x/y.normal.txt
-  ../lf_subrepo_archive/subrepo
-  ../lf_subrepo_archive/subrepo/large.txt
-  ../lf_subrepo_archive/subrepo/normal.txt
-
-Test update with subrepos.
-
-  $ hg update 0
-  getting changed largefiles
-  0 largefiles updated, 1 removed
-  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
-  $ hg status -S
-  $ hg update tip
-  getting changed largefiles
-  1 largefiles updated, 0 removed
-  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ hg status -S
-# modify a large file
-  $ echo "modified" > subrepo/large.txt
-  $ hg st -S
-  M subrepo/large.txt
-# update -C should revert the change.
-  $ hg update -C
-  getting changed largefiles
-  1 largefiles updated, 0 removed
-  getting changed largefiles
-  0 largefiles updated, 0 removed
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ hg status -S
-
-Test archiving a revision that references a subrepo that is not yet
-cloned (see test-subrepo-recursion.t):
-
-  $ hg clone -U . ../empty
-  $ cd ../empty
-  $ hg archive --subrepos -r tip ../archive.tar.gz
-  cloning subrepo subrepo from $TESTTMP/statusmatch/subrepo
-  $ cd ..
-
-Test that addremove picks up largefiles prior to the initial commit (issue3541)
-
-  $ hg init addrm2
-  $ cd addrm2
-  $ touch large.dat
-  $ touch large2.dat
-  $ touch normal
-  $ hg add --large large.dat
-  $ hg addremove -v
-  adding large2.dat as a largefile
-  adding normal
-
-Test that forgetting all largefiles reverts to islfilesrepo() == False
-(addremove will add *.dat as normal files now)
-  $ hg forget large.dat
-  $ hg forget large2.dat
-  $ hg addremove -v
-  adding large.dat
-  adding large2.dat
-
-Test commit's addremove option prior to the first commit
-  $ hg forget large.dat
-  $ hg forget large2.dat
-  $ hg add --large large.dat
-  $ hg ci -Am "commit"
-  adding large2.dat as a largefile
-  Invoking status precommit hook
-  A large.dat
-  A large2.dat
-  A normal
-  $ find .hglf | sort
-  .hglf
-  .hglf/large.dat
-  .hglf/large2.dat
-
-Test actions on largefiles using relative paths from subdir
-
-  $ mkdir sub
-  $ cd sub
-  $ echo anotherlarge > anotherlarge
-  $ hg add --large anotherlarge
-  $ hg st
-  A sub/anotherlarge
-  $ hg st anotherlarge
-  A anotherlarge
-  $ hg commit -m anotherlarge anotherlarge
-  Invoking status precommit hook
-  A sub/anotherlarge
-  $ hg log anotherlarge
-  changeset:   1:9627a577c5e9
-  tag:         tip
-  user:        test
-  date:        Thu Jan 01 00:00:00 1970 +0000
-  summary:     anotherlarge
-  
-  $ hg log -G anotherlarge
-  @  changeset:   1:9627a577c5e9
-  |  tag:         tip
-  |  user:        test
-  |  date:        Thu Jan 01 00:00:00 1970 +0000
-  |  summary:     anotherlarge
-  |
-  $ echo more >> anotherlarge
-  $ hg st .
-  M anotherlarge
-  $ hg cat anotherlarge
-  anotherlarge
-  $ hg revert anotherlarge
-  $ hg st
-  ? sub/anotherlarge.orig
-  $ cd ..
-
-  $ cd ..
-
-issue3651: summary/outgoing with largefiles shows "no remote repo"
-unexpectedly
-
-  $ mkdir issue3651
-  $ cd issue3651
-
-  $ hg init src
-  $ echo a > src/a
-  $ hg -R src add --large src/a
-  $ hg -R src commit -m '#0'
-  Invoking status precommit hook
-  A a
-
-check messages when no remote repository is specified:
-"no remote repo" route for "hg outgoing --large" is not tested here,
-because it can't be reproduced easily.
-
-  $ hg init clone1
-  $ hg -R clone1 -q pull src
-  $ hg -R clone1 -q update
-  $ hg -R clone1 paths | grep default
-  [1]
-
-  $ hg -R clone1 summary --large
-  parent: 0:fc0bd45326d3 tip
-   #0
-  branch: default
-  commit: (clean)
-  update: (current)
-  largefiles: (no remote repo)
-
-check messages when there is no files to upload:
-
-  $ hg -q clone src clone2
-  $ hg -R clone2 paths | grep default
-  default = $TESTTMP/issue3651/src (glob)
-
-  $ hg -R clone2 summary --large
-  parent: 0:fc0bd45326d3 tip
-   #0
-  branch: default
-  commit: (clean)
-  update: (current)
-  largefiles: (no files to upload)
-  $ hg -R clone2 outgoing --large
-  comparing with $TESTTMP/issue3651/src (glob)
-  searching for changes
-  no changes found
-  largefiles: no files to upload
-  [1]
-
-  $ hg -R clone2 outgoing --large --graph --template "{rev}"
-  comparing with $TESTTMP/issue3651/src (glob)
-  searching for changes
-  no changes found
-  largefiles: no files to upload
-
-check messages when there are files to upload:
-
-  $ echo b > clone2/b
-  $ hg -R clone2 add --large clone2/b
-  $ hg -R clone2 commit -m '#1'
-  Invoking status precommit hook
-  A b
-  $ hg -R clone2 summary --large
-  parent: 1:1acbe71ce432 tip
-   #1
-  branch: default
-  commit: (clean)
-  update: (current)
-  largefiles: 1 to upload
-  $ hg -R clone2 outgoing --large
-  comparing with $TESTTMP/issue3651/src (glob)
-  searching for changes
-  changeset:   1:1acbe71ce432
-  tag:         tip
-  user:        test
-  date:        Thu Jan 01 00:00:00 1970 +0000
-  summary:     #1
-  
-  largefiles to upload:
-  b
-  
-  $ hg -R clone2 outgoing --large --graph --template "{rev}"
-  comparing with $TESTTMP/issue3651/src
-  searching for changes
-  @  1
-  
-  largefiles to upload:
-  b
-  
-
-  $ cd ..
-
-merge action 'd' for 'local renamed directory to d2/g' which has no filename
-
-  $ hg init merge-action
-  $ cd merge-action
-  $ touch l
-  $ hg add --large l
-  $ mkdir d1
-  $ touch d1/f
-  $ hg ci -Aqm0
-  Invoking status precommit hook
-  A d1/f
-  A l
-  $ echo > d1/f
-  $ touch d1/g
-  $ hg ci -Aqm1
-  Invoking status precommit hook
-  M d1/f
-  A d1/g
-  $ hg up -qr0
-  $ hg mv d1 d2
-  moving d1/f to d2/f (glob)
-  $ hg ci -qm2
-  Invoking status precommit hook
-  A d2/f
-  R d1/f
-  $ hg merge
-  merging d2/f and d1/f to d2/f
-  1 files updated, 1 files merged, 0 files removed, 0 files unresolved
-  (branch merge, don't forget to commit)
-  getting changed largefiles
-  0 largefiles updated, 0 removed
-  $ cd ..
-
-
-Merge conflicts:
-
-  $ hg init merge
-  $ cd merge
-  $ echo 0 > f-different
-  $ echo 0 > f-same
-  $ echo 0 > f-unchanged-1
-  $ echo 0 > f-unchanged-2
-  $ hg add --large *
-  $ hg ci -m0
-  Invoking status precommit hook
-  A f-different
-  A f-same
-  A f-unchanged-1
-  A f-unchanged-2
-  $ echo tmp1 > f-unchanged-1
-  $ echo tmp1 > f-unchanged-2
-  $ echo tmp1 > f-same
-  $ hg ci -m1
-  Invoking status precommit hook
-  M f-same
-  M f-unchanged-1
-  M f-unchanged-2
-  $ echo 2 > f-different
-  $ echo 0 > f-unchanged-1
-  $ echo 1 > f-unchanged-2
-  $ echo 1 > f-same
-  $ hg ci -m2
-  Invoking status precommit hook
-  M f-different
-  M f-same
-  M f-unchanged-1
-  M f-unchanged-2
-  $ hg up -qr0
-  $ echo tmp2 > f-unchanged-1
-  $ echo tmp2 > f-unchanged-2
-  $ echo tmp2 > f-same
-  $ hg ci -m3
-  Invoking status precommit hook
-  M f-same
-  M f-unchanged-1
-  M f-unchanged-2
-  created new head
-  $ echo 1 > f-different
-  $ echo 1 > f-unchanged-1
-  $ echo 0 > f-unchanged-2
-  $ echo 1 > f-same
-  $ hg ci -m4
-  Invoking status precommit hook
-  M f-different
-  M f-same
-  M f-unchanged-1
-  M f-unchanged-2
-  $ hg merge
-  largefile f-different has a merge conflict
-  ancestor was 09d2af8dd22201dd8d48e5dcfcaed281ff9422c7
-  keep (l)ocal e5fa44f2b31c1fb553b6021e7360d07d5d91ff5e or
-  take (o)ther 7448d8798a4380162d4b56f9b452e2f6f9e24e7a? l
-  0 files updated, 4 files merged, 0 files removed, 0 files unresolved
-  (branch merge, don't forget to commit)
-  getting changed largefiles
-  1 largefiles updated, 0 removed
-  $ cat f-different
-  1
-  $ cat f-same
-  1
-  $ cat f-unchanged-1
-  1
-  $ cat f-unchanged-2
-  1
-  $ cd ..
-
-Check whether "largefiles" feature is supported only in repositories
-enabling largefiles extension.
-
-  $ mkdir individualenabling
-  $ cd individualenabling
-
-  $ hg init enabledlocally
-  $ echo large > enabledlocally/large
-  $ hg -R enabledlocally add --large enabledlocally/large
-  $ hg -R enabledlocally commit -m '#0'
-  Invoking status precommit hook
-  A large
-
-  $ hg init notenabledlocally
-  $ echo large > notenabledlocally/large
-  $ hg -R notenabledlocally add --large notenabledlocally/large
-  $ hg -R notenabledlocally commit -m '#0'
-  Invoking status precommit hook
-  A large
-
-  $ cat >> $HGRCPATH <<EOF
-  > [extensions]
-  > # disable globally
-  > largefiles=!
-  > EOF
-  $ cat >> enabledlocally/.hg/hgrc <<EOF
-  > [extensions]
-  > # enable locally
-  > largefiles=
-  > EOF
-  $ hg -R enabledlocally root
-  $TESTTMP/individualenabling/enabledlocally (glob)
-  $ hg -R notenabledlocally root
-  abort: repository requires features unknown to this Mercurial: largefiles!
-  (see http://mercurial.selenic.com/wiki/MissingRequirement for more information)
-  [255]
-
-  $ hg init push-dst
-  $ hg -R enabledlocally push push-dst
-  pushing to push-dst
-  abort: required features are not supported in the destination: largefiles
-  [255]
-
-  $ hg init pull-src
-  $ hg -R pull-src pull enabledlocally
-  pulling from enabledlocally
-  abort: required features are not supported in the destination: largefiles
-  [255]
-
-  $ hg clone enabledlocally clone-dst
-  abort: repository requires features unknown to this Mercurial: largefiles!
-  (see http://mercurial.selenic.com/wiki/MissingRequirement for more information)
-  [255]
-  $ test -d clone-dst
-  [1]
-  $ hg clone --pull enabledlocally clone-pull-dst
-  abort: required features are not supported in the destination: largefiles
-  [255]
-  $ test -d clone-pull-dst
-  [1]
-
-#if serve
-
-Test largefiles specific peer setup, when largefiles is enabled
-locally (issue4109)
-
-  $ hg showconfig extensions | grep largefiles
-  extensions.largefiles=!
-  $ mkdir -p $TESTTMP/individualenabling/usercache
-
-  $ hg serve -R enabledlocally -d -p $HGPORT --pid-file hg.pid
-  $ cat hg.pid >> $DAEMON_PIDS
-
-  $ hg init pull-dst
-  $ cat > pull-dst/.hg/hgrc <<EOF
-  > [extensions]
-  > # enable locally
-  > largefiles=
-  > [largefiles]
-  > # ignore system cache to force largefiles specific wire proto access
-  > usercache=$TESTTMP/individualenabling/usercache
-  > EOF
-  $ hg -R pull-dst -q pull -u http://localhost:$HGPORT
-
-  $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS
-#endif
-
-  $ cd ..
--- a/tests/test-lfconvert.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-lfconvert.t	Sat Jun 14 11:56:20 2014 -0500
@@ -132,6 +132,7 @@
   [1]
   $ hg cat -r . sub/maybelarge.dat > stuff/maybelarge.dat
   $ hg resolve -m stuff/maybelarge.dat
+  no more unresolved files
   $ hg commit -m"merge"
   $ hg log -G --template "{rev}:{node|short}  {desc|firstline}\n"
   @    5:4884f215abda  merge
--- a/tests/test-log.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-log.t	Sat Jun 14 11:56:20 2014 -0500
@@ -990,6 +990,7 @@
   [1]
   $ echo 'merge 1' > foo
   $ hg resolve -m foo
+  no more unresolved files
   $ hg ci -m "First merge, related"
 
   $ hg merge 4
@@ -1001,6 +1002,7 @@
   [1]
   $ echo 'merge 2' > foo
   $ hg resolve -m foo
+  no more unresolved files
   $ hg ci -m "Last merge, related"
 
   $ hg log --graph
--- a/tests/test-merge-commit.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-merge-commit.t	Sat Jun 14 11:56:20 2014 -0500
@@ -71,8 +71,8 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 0f2ff26688b9, local: 2263c1be0967+, remote: 0555950ead28
+   preserving bar for resolve of bar
    bar: versions differ -> m
-    preserving bar for resolve of bar
   updating: bar 1/1 files (100.00%)
   picked tool 'internal:merge' for bar (binary False symlink False)
   merging bar
@@ -158,8 +158,8 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 0f2ff26688b9, local: 2263c1be0967+, remote: 3ffa6b9e35f0
+   preserving bar for resolve of bar
    bar: versions differ -> m
-    preserving bar for resolve of bar
   updating: bar 1/1 files (100.00%)
   picked tool 'internal:merge' for bar (binary False symlink False)
   merging bar
--- a/tests/test-merge-criss-cross.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-merge-criss-cross.t	Sat Jun 14 11:56:20 2014 -0500
@@ -81,11 +81,11 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 0f6b37dbe527, local: 3b08d01b0ab5+, remote: adfe50279922
+   preserving f2 for resolve of f2
    f1: remote is newer -> g
-   f2: versions differ -> m
-    preserving f2 for resolve of f2
   getting f1
   updating: f1 1/2 files (50.00%)
+   f2: versions differ -> m
   updating: f2 2/2 files (100.00%)
   picked tool 'internal:dump' for f2 (binary False symlink False)
   merging f2
@@ -135,16 +135,16 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 0f6b37dbe527, local: 3b08d01b0ab5+, remote: adfe50279922
-   f1: g
-   f2: m
+   f1: remote is newer -> g
+   f2: versions differ -> m
   
   calculating bids for ancestor 40663881a6dd
     searching for copies back to rev 3
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 40663881a6dd, local: 3b08d01b0ab5+, remote: adfe50279922
-   f1: m
-   f2: k
+   f2: keep -> k
+   f1: versions differ -> m
   
   auction for merging merge bids
    f1: picking 'get' action
@@ -152,9 +152,9 @@
   end of auction
   
    f1: remote is newer -> g
-   f2: keep -> k
   getting f1
   updating: f1 1/1 files (100.00%)
+   f2: keep -> k
   1 files updated, 0 files merged, 0 files removed, 0 files unresolved
   (branch merge, don't forget to commit)
 
@@ -180,26 +180,26 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 0f6b37dbe527, local: adfe50279922+, remote: 3b08d01b0ab5
-   f1: k
-   f2: m
+   f1: keep -> k
+   f2: versions differ -> m
   
   calculating bids for ancestor 40663881a6dd
     searching for copies back to rev 3
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 40663881a6dd, local: adfe50279922+, remote: 3b08d01b0ab5
-   f1: m
-   f2: g
+   f2: remote is newer -> g
+   f1: versions differ -> m
   
   auction for merging merge bids
    f1: picking 'keep' action
    f2: picking 'get' action
   end of auction
   
-   f1: keep -> k
    f2: remote is newer -> g
   getting f2
   updating: f2 1/1 files (100.00%)
+   f1: keep -> k
   1 files updated, 0 files merged, 0 files removed, 0 files unresolved
   (branch merge, don't forget to commit)
 
@@ -246,16 +246,16 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 0f6b37dbe527, local: 3b08d01b0ab5+, remote: adfe50279922
-   f1: g
-   f2: m
+   f1: remote is newer -> g
+   f2: versions differ -> m
   
   calculating bids for ancestor 40663881a6dd
     searching for copies back to rev 3
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 40663881a6dd, local: 3b08d01b0ab5+, remote: adfe50279922
-   f1: m
-   f2: k
+   f2: keep -> k
+   f1: versions differ -> m
   
   auction for merging merge bids
    f1: picking 'get' action
@@ -263,9 +263,9 @@
   end of auction
   
    f1: remote is newer -> g
-   f2: keep -> k
   getting f1
   updating: f1 1/1 files (100.00%)
+   f2: keep -> k
   1 files updated, 0 files merged, 0 files removed, 0 files unresolved
   (branch merge, don't forget to commit)
 
--- a/tests/test-merge-revert2.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-merge-revert2.t	Sat Jun 14 11:56:20 2014 -0500
@@ -57,11 +57,11 @@
   @@ -1,3 +1,7 @@
    added file1
    another line of text
-  +<<<<<<< local
+  +<<<<<<< working copy: c3fa057dd86f  - test: added file1 and file2
   +changed file1 different
   +=======
    changed file1
-  +>>>>>>> other
+  +>>>>>>> destination:  dfab7f3c2efb - test: changed file1
 
   $ hg status
   M file1
--- a/tests/test-merge-tools.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-merge-tools.t	Sat Jun 14 11:56:20 2014 -0500
@@ -66,11 +66,11 @@
   [1]
   $ aftermerge
   # cat f
-  <<<<<<< local
+  <<<<<<< local: ef83787e2614  - test: revision 1
   revision 1
   =======
   revision 2
-  >>>>>>> other
+  >>>>>>> other: 0185f4e0cf02  - test: revision 2
   space
   # hg stat
   M f
@@ -587,6 +587,54 @@
 
   $ unset HGMERGE # make sure HGMERGE doesn't interfere with remaining tests
 
+update is a merge ...
+
+  $ beforemerge
+  [merge-tools]
+  false.whatever=
+  true.priority=1
+  true.executable=cat
+  # hg update -C 1
+  $ hg debugsetparent 0
+  $ hg update -r 2
+  merging f
+  revision 1
+  space
+  revision 0
+  space
+  revision 2
+  space
+  0 files updated, 1 files merged, 0 files removed, 0 files unresolved
+  $ aftermerge
+  # cat f
+  revision 1
+  space
+  # hg stat
+  M f
+
+update should also have --tool
+
+  $ beforemerge
+  [merge-tools]
+  false.whatever=
+  true.priority=1
+  true.executable=cat
+  # hg update -C 1
+  $ hg debugsetparent 0
+  $ hg update -r 2 --tool false
+  merging f
+  merging f failed!
+  0 files updated, 0 files merged, 0 files removed, 1 files unresolved
+  use 'hg resolve' to retry unresolved file merges
+  [1]
+  $ aftermerge
+  # cat f
+  revision 1
+  space
+  # hg stat
+  M f
+  ? f.orig
+
 Default is silent simplemerge:
 
   $ beforemerge
--- a/tests/test-merge-types.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-merge-types.t	Sat Jun 14 11:56:20 2014 -0500
@@ -34,8 +34,8 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: c334dc3be0da, local: 521a1e40188f+, remote: 3574f3e69b1c
+   preserving a for resolve of a
    a: versions differ -> m
-    preserving a for resolve of a
   updating: a 1/1 files (100.00%)
   picked tool 'internal:merge' for a (binary False symlink True)
   merging a
@@ -50,6 +50,7 @@
   a is a symlink:
   a -> symlink
   $ hg resolve a --tool internal:other
+  no more unresolved files
   $ tellmeabout a
   a is an executable file with content:
   a
@@ -67,8 +68,8 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: c334dc3be0da, local: 3574f3e69b1c+, remote: 521a1e40188f
+   preserving a for resolve of a
    a: versions differ -> m
-    preserving a for resolve of a
   updating: a 1/1 files (100.00%)
   picked tool 'internal:merge' for a (binary False symlink True)
   merging a
@@ -101,8 +102,8 @@
   resolving manifests
    branchmerge: False, force: False, partial: False
    ancestor: c334dc3be0da, local: c334dc3be0da+, remote: 521a1e40188f
+   preserving a for resolve of a
    a: versions differ -> m
-    preserving a for resolve of a
   updating: a 1/1 files (100.00%)
   (couldn't find merge tool hgmerge|tool hgmerge can't handle symlinks) (re)
   picked tool 'internal:prompt' for a (binary False symlink True)
@@ -289,18 +290,18 @@
   U h
   $ tellmeabout a
   a is a plain file with content:
-  <<<<<<< local
+  <<<<<<< local: 0139c5610547 - test: 2
   2
   =======
   1
-  >>>>>>> other
+  >>>>>>> other: 97e29675e796  - test: 1
   $ tellmeabout b
   b is a plain file with content:
-  <<<<<<< local
+  <<<<<<< local: 0139c5610547 - test: 2
   2
   =======
   1
-  >>>>>>> other
+  >>>>>>> other: 97e29675e796  - test: 1
   $ tellmeabout c
   c is a plain file with content:
   x
@@ -344,18 +345,18 @@
   [1]
   $ tellmeabout a
   a is a plain file with content:
-  <<<<<<< local
+  <<<<<<< local: 97e29675e796  - test: 1
   1
   =======
   2
-  >>>>>>> other
+  >>>>>>> other: 0139c5610547 - test: 2
   $ tellmeabout b
   b is an executable file with content:
-  <<<<<<< local
+  <<<<<<< local: 97e29675e796  - test: 1
   1
   =======
   2
-  >>>>>>> other
+  >>>>>>> other: 0139c5610547 - test: 2
   $ tellmeabout c
   c is an executable file with content:
   x
--- a/tests/test-merge7.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-merge7.t	Sat Jun 14 11:56:20 2014 -0500
@@ -57,6 +57,7 @@
   > EOF
   $ rm -f *.orig
   $ hg resolve -m test.txt
+  no more unresolved files
   $ hg commit -m "Merge 1"
 
 change test-a again
@@ -83,8 +84,8 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 96b70246a118, local: 50c3a7e29886+, remote: 40d11a4173a8
+   preserving test.txt for resolve of test.txt
    test.txt: versions differ -> m
-    preserving test.txt for resolve of test.txt
   updating: test.txt 1/1 files (100.00%)
   picked tool 'internal:merge' for test.txt (binary False symlink False)
   merging test.txt
@@ -97,11 +98,11 @@
 
   $ cat test.txt
   one
-  <<<<<<< local
+  <<<<<<< local: 50c3a7e29886  - test: Merge 1
   two-point-five
   =======
   two-point-one
-  >>>>>>> other
+  >>>>>>> other: 40d11a4173a8 - test: two -> two-point-one
   three
 
   $ hg debugindex test.txt
--- a/tests/test-mq-qfold.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-mq-qfold.t	Sat Jun 14 11:56:20 2014 -0500
@@ -20,6 +20,8 @@
   $ hg qnew -f p3
 
 Fold in the middle of the queue:
+(this tests also that editor is not invoked if '--edit' is not
+specified)
 
   $ hg qpop p1
   popping p3
@@ -34,7 +36,7 @@
    a
   +a
 
-  $ hg qfold p2
+  $ HGEDITOR=cat hg qfold p2
   $ grep git .hg/patches/p1 && echo 'git patch found!'
   [1]
 
@@ -153,8 +155,9 @@
   >     repo.__class__ = commitfailure
   > EOF
 
-  $ cat > .hg/hgrc <<EOF
+  $ cat >> .hg/hgrc <<EOF
   > [extensions]
+  > # this failure occurs before editor invocation
   > commitfailure = $TESTTMP/commitfailure.py
   > EOF
 
@@ -165,16 +168,95 @@
   > (echo; echo "test saving last-message.txt") >> \$1
   > EOF
 
+  $ hg qapplied
+  p1
+  git
+  $ hg tip --template "{files}\n"
+  aa
+
+(test that editor is not invoked before transaction starting,
+and that combination of '--edit' and '--message' doesn't abort execution)
+
   $ rm -f .hg/last-message.txt
-  $ HGEDITOR="sh $TESTTMP/editor.sh" hg qfold -e p3
-  ==== before editing
-  original message====
+  $ HGEDITOR="sh $TESTTMP/editor.sh" hg qfold -e -m MESSAGE p3
   refresh interrupted while patch was popped! (revert --all, qpush to recover)
   abort: emulating unexpected abort
   [255]
   $ cat .hg/last-message.txt
+  cat: .hg/last-message.txt: No such file or directory
+  [1]
+
+(reset applied patches and directory status)
+
+  $ cat >> .hg/hgrc <<EOF
+  > [extensions]
+  > # this failure occurs after editor invocation
+  > commitfailure = !
+  > EOF
+
+  $ hg qapplied
+  p1
+  $ hg status -A aa
+  ? aa
+  $ rm aa
+  $ hg status -m
+  M a
+  $ hg revert --no-backup -q a
+  $ hg qpush -q git
+  now at: git
+
+(test that editor is invoked and commit message is saved into
+"last-message.txt")
+
+  $ cat >> .hg/hgrc <<EOF
+  > [hooks]
+  > # this failure occurs after editor invocation
+  > pretxncommit.unexpectedabort = false
+  > EOF
+
+  $ rm -f .hg/last-message.txt
+  $ HGEDITOR="sh $TESTTMP/editor.sh" hg qfold -e p3
+  ==== before editing
   original message
+  
+  
+  HG: Enter commit message.  Lines beginning with 'HG:' are removed.
+  HG: Leave message empty to use default message.
+  HG: --
+  HG: user: test
+  HG: branch 'default'
+  HG: added aa
+  HG: changed a
+  ====
+  transaction abort!
+  rollback completed
+  note: commit message saved in .hg/last-message.txt
+  refresh interrupted while patch was popped! (revert --all, qpush to recover)
+  abort: pretxncommit.unexpectedabort hook exited with status 1
+  [255]
+  $ cat .hg/last-message.txt
+  original message
+  
+  
+  
   test saving last-message.txt
 
+(confirm whether files listed up in the commit message editing are correct)
+
+  $ cat >> .hg/hgrc <<EOF
+  > [hooks]
+  > pretxncommit.unexpectedabort =
+  > EOF
+  $ hg status -u | while read f; do rm ${f}; done
+  $ hg revert --no-backup -q --all
+  $ hg qpush -q git
+  now at: git
+  $ hg qpush -q --move p3
+  now at: p3
+
+  $ hg status --rev "git^1" --rev . -arm
+  M a
+  A aa
+
   $ cd ..
 
--- a/tests/test-mq-qnew.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-mq-qnew.t	Sat Jun 14 11:56:20 2014 -0500
@@ -158,6 +158,7 @@
   merging a incomplete! (edit conflicts, then use 'hg resolve --mark')
   0 files updated, 0 files merged, 0 files removed, 1 files unresolved
   use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
+  no more unresolved files
   abort: cannot manage merge changesets
   $ rm -r sandbox
 
@@ -231,6 +232,7 @@
   merging a incomplete! (edit conflicts, then use 'hg resolve --mark')
   0 files updated, 0 files merged, 0 files removed, 1 files unresolved
   use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
+  no more unresolved files
   abort: cannot manage merge changesets
   $ rm -r sandbox
 
@@ -247,8 +249,9 @@
   >             raise util.Abort('emulating unexpected abort')
   >     repo.__class__ = commitfailure
   > EOF
-  $ cat > .hg/hgrc <<EOF
+  $ cat >> .hg/hgrc <<EOF
   > [extensions]
+  > # this failure occurs before editor invocation
   > commitfailure = $TESTTMP/commitfailure.py
   > EOF
 
@@ -259,13 +262,83 @@
   > echo "test saving last-message.txt" >> \$1
   > EOF
 
+(test that editor is not invoked before transaction starting)
+
   $ rm -f .hg/last-message.txt
   $ HGEDITOR="sh $TESTTMP/editor.sh" hg qnew -e patch
-  ==== before editing
-  ====
   abort: emulating unexpected abort
   [255]
   $ cat .hg/last-message.txt
+  cat: .hg/last-message.txt: No such file or directory
+  [1]
+
+(test that editor is invoked and commit message is saved into
+"last-message.txt")
+
+  $ cat >> .hg/hgrc <<EOF
+  > [extensions]
+  > commitfailure = !
+  > [hooks]
+  > # this failure occurs after editor invocation
+  > pretxncommit.unexpectedabort = false
+  > EOF
+
+  $ rm -f .hg/last-message.txt
+  $ hg status
+  $ HGEDITOR="sh $TESTTMP/editor.sh" hg qnew -e patch
+  ==== before editing
+  
+  
+  HG: Enter commit message.  Lines beginning with 'HG:' are removed.
+  HG: Leave message empty to use default message.
+  HG: --
+  HG: user: test
+  HG: branch 'default'
+  HG: no files changed
+  ====
+  transaction abort!
+  rollback completed
+  note: commit message saved in .hg/last-message.txt
+  abort: pretxncommit.unexpectedabort hook exited with status 1
+  [255]
+  $ cat .hg/last-message.txt
+  
+  
   test saving last-message.txt
 
+  $ cat >> .hg/hgrc <<EOF
+  > [hooks]
+  > pretxncommit.unexpectedabort =
+  > EOF
+
+#if unix-permissions
+
+Test handling default message with the patch filename with tail whitespaces
+
+  $ cat > $TESTTMP/editor.sh << EOF
+  > echo "==== before editing"
+  > cat \$1
+  > echo "===="
+  > echo "[mq]: patch        " > \$1
+  > EOF
+
+  $ rm -f .hg/last-message.txt
+  $ hg status
+  $ HGEDITOR="sh $TESTTMP/editor.sh" hg qnew -e "patch "
+  ==== before editing
+  
+  
+  HG: Enter commit message.  Lines beginning with 'HG:' are removed.
+  HG: Leave message empty to use default message.
+  HG: --
+  HG: user: test
+  HG: branch 'default'
+  HG: no files changed
+  ====
+  $ cat ".hg/patches/patch "
+  # HG changeset patch
+  # Parent 0000000000000000000000000000000000000000
+
   $ cd ..
+
+#endif
--- a/tests/test-mq-qrefresh-replace-log-message.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-mq-qrefresh-replace-log-message.t	Sat Jun 14 11:56:20 2014 -0500
@@ -6,6 +6,8 @@
   $ hg qinit
 
 Should fail if no patches applied
+(this tests also that editor is not invoked if '--edit' is not
+specified)
 
   $ hg qrefresh
   no patches applied
@@ -16,7 +18,7 @@
   $ hg qnew -m "First commit message" first-patch
   $ echo aaaa > file
   $ hg add file
-  $ hg qrefresh
+  $ HGEDITOR=cat hg qrefresh
 
 Should display 'First commit message'
 
@@ -24,9 +26,19 @@
   First commit message
 
 Testing changing message with -m
+(this tests also that '--edit' can be used with '--message')
 
   $ echo bbbb > file
-  $ hg qrefresh -m "Second commit message"
+  $ HGEDITOR=cat hg qrefresh -m "Second commit message" -e
+  Second commit message
+  
+  
+  HG: Enter commit message.  Lines beginning with 'HG:' are removed.
+  HG: Leave message empty to use default message.
+  HG: --
+  HG: user: test
+  HG: branch 'default'
+  HG: added file
 
 Should display 'Second commit message'
 
@@ -59,3 +71,98 @@
   $ hg log -l1 --template "{desc}\n"
   Fifth commit message
    This is the 5th log message
+
+Test saving last-message.txt:
+
+  $ cat > $TESTTMP/editor.sh << EOF
+  > echo "==== before editing"
+  > cat \$1
+  > echo "===="
+  > (echo; echo "test saving last-message.txt") >> \$1
+  > EOF
+
+  $ cat > $TESTTMP/commitfailure.py <<EOF
+  > from mercurial import util
+  > def reposetup(ui, repo):
+  >     class commitfailure(repo.__class__):
+  >         def commit(self, *args, **kwargs):
+  >             raise util.Abort('emulating unexpected abort')
+  >     repo.__class__ = commitfailure
+  > EOF
+
+  $ cat >> .hg/hgrc <<EOF
+  > [extensions]
+  > # this failure occurs before editor invocation
+  > commitfailure = $TESTTMP/commitfailure.py
+  > EOF
+
+  $ hg qapplied
+  first-patch
+  second-patch
+  $ hg tip --template "{files}\n"
+  file2
+
+(test that editor is not invoked before transaction starting)
+
+  $ rm -f .hg/last-message.txt
+  $ HGEDITOR="sh $TESTTMP/editor.sh" hg qrefresh -e
+  refresh interrupted while patch was popped! (revert --all, qpush to recover)
+  abort: emulating unexpected abort
+  [255]
+  $ cat .hg/last-message.txt
+  cat: .hg/last-message.txt: No such file or directory
+  [1]
+
+(reset applied patches and directory status)
+
+  $ cat >> .hg/hgrc <<EOF
+  > [extensions]
+  > commitfailure = !
+  > EOF
+
+  $ hg qapplied
+  first-patch
+  $ hg status -A file2
+  ? file2
+  $ rm file2
+  $ hg qpush -q second-patch
+  now at: second-patch
+
+(test that editor is invoked and commit message is saved into
+"last-message.txt")
+
+  $ cat >> .hg/hgrc <<EOF
+  > [hooks]
+  > # this failure occurs after editor invocation
+  > pretxncommit.unexpectedabort = false
+  > EOF
+
+  $ rm -f .hg/last-message.txt
+  $ hg status --rev "second-patch^1" -arm
+  A file2
+  $ HGEDITOR="sh $TESTTMP/editor.sh" hg qrefresh -e
+  ==== before editing
+  Fifth commit message
+   This is the 5th log message
+  
+  
+  HG: Enter commit message.  Lines beginning with 'HG:' are removed.
+  HG: Leave message empty to use default message.
+  HG: --
+  HG: user: test
+  HG: branch 'default'
+  HG: added file2
+  ====
+  transaction abort!
+  rollback completed
+  note: commit message saved in .hg/last-message.txt
+  refresh interrupted while patch was popped! (revert --all, qpush to recover)
+  abort: pretxncommit.unexpectedabort hook exited with status 1
+  [255]
+  $ cat .hg/last-message.txt
+  Fifth commit message
+   This is the 5th log message
+  
+  
+  
+  test saving last-message.txt
--- a/tests/test-patchbomb.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-patchbomb.t	Sat Jun 14 11:56:20 2014 -0500
@@ -1,3 +1,13 @@
+Note for future hackers of patchbomb: this file is a bit heavy on
+wildcards in test expectations due to how many things like hostnames
+tend to make it into outputs. As a result, you may need to perform the
+following regular expression substitutions:
+@$HOSTNAME> -> @*> (glob)
+Mercurial-patchbomb/.* -> Mercurial-patchbomb/* (glob)
+/mixed; boundary="===+[0-9]+==" -> /mixed; boundary="===*== (glob)"
+--===+[0-9]+=+--$ -> --===*=-- (glob)
+--===+[0-9]+=+$ -> --===*= (glob)
+
   $ echo "[extensions]" >> $HGRCPATH
   $ echo "patchbomb=" >> $HGRCPATH
 
@@ -17,7 +27,10 @@
   Content-Transfer-Encoding: 7bit
   Subject: [PATCH] a
   X-Mercurial-Node: 8580ff50825a50c8f716709acdf8de0deddcd6ab
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 1
   Message-Id: <8580ff50825a50c8f716.60@*> (glob)
+  X-Mercurial-Series-Id: <8580ff50825a50c8f716.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:01:00 +0000
   From: quux
@@ -87,7 +100,10 @@
   Content-Transfer-Encoding: 7bit
   Subject: [PATCH 1 of 2] a
   X-Mercurial-Node: 8580ff50825a50c8f716709acdf8de0deddcd6ab
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 2
   Message-Id: <8580ff50825a50c8f716.121@*> (glob)
+  X-Mercurial-Series-Id: <8580ff50825a50c8f716.121@*> (glob)
   In-Reply-To: <patchbomb.120@*> (glob)
   References: <patchbomb.120@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -116,7 +132,10 @@
   Content-Transfer-Encoding: 7bit
   Subject: [PATCH 2 of 2] b
   X-Mercurial-Node: 97d72e5f12c7e84f85064aa72e5a297142c36ed9
+  X-Mercurial-Series-Index: 2
+  X-Mercurial-Series-Total: 2
   Message-Id: <97d72e5f12c7e84f8506.122@*> (glob)
+  X-Mercurial-Series-Id: <8580ff50825a50c8f716.121@*> (glob)
   In-Reply-To: <patchbomb.120@*> (glob)
   References: <patchbomb.120@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -200,7 +219,7 @@
   1 changesets found
   
   displaying test ...
-  Content-Type: multipart/mixed; boundary="===*" (glob)
+  Content-Type: multipart/mixed; boundary="===*==" (glob)
   MIME-Version: 1.0
   Subject: test
   Message-Id: <patchbomb.180@*> (glob)
@@ -210,7 +229,7 @@
   To: foo
   Cc: bar
   
-  --===* (glob)
+  --===*= (glob)
   Content-Type: text/plain; charset="us-ascii"
   MIME-Version: 1.0
   Content-Transfer-Encoding: 7bit
@@ -219,7 +238,7 @@
   
   description
   
-  --===* (glob)
+  --===*= (glob)
   Content-Type: application/x-mercurial-bundle
   MIME-Version: 1.0
   Content-Disposition: attachment; filename="bundle.hg"
@@ -232,7 +251,7 @@
   SlIBpFisgGkyRjX//TMtfcUAEsGu56+YnE1OlTZmzKm8BSu2rvo4rHAYYaadIFFuTy0LYgIkgLVD
   sgVa2F19D1tx9+hgbAygLgQwaIqcDdgA4BjQgIiz/AEP72++llgDKhKducqodGE4B0ETqF3JFOFC
   Q70eyNw=
-  --===*-- (glob)
+  --===*=-- (glob)
 
 utf-8 patch:
   $ python -c 'fp = open("utf", "wb"); fp.write("h\xC3\xB6mma!\n"); fp.close();'
@@ -251,7 +270,10 @@
   Content-Transfer-Encoding: 8bit
   Subject: [PATCH] utf-8 content
   X-Mercurial-Node: 909a00e13e9d78b575aeee23dddbada46d5a143f
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 1
   Message-Id: <909a00e13e9d78b575ae.240@*> (glob)
+  X-Mercurial-Series-Id: <909a00e13e9d78b575ae.240@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:04:00 +0000
   From: quux
@@ -294,7 +316,10 @@
   Content-Transfer-Encoding: base64
   Subject: [PATCH] utf-8 content
   X-Mercurial-Node: 909a00e13e9d78b575aeee23dddbada46d5a143f
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 1
   Message-Id: <909a00e13e9d78b575ae.240@*> (glob)
+  X-Mercurial-Series-Id: <909a00e13e9d78b575ae.240@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:04:00 +0000
   From: Q <quux>
@@ -353,7 +378,10 @@
   Content-Transfer-Encoding: quoted-printable
   Subject: [PATCH] long line
   X-Mercurial-Node: a2ea8fc83dd8b93cfd86ac97b28287204ab806e1
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 1
   Message-Id: <a2ea8fc83dd8b93cfd86.240@*> (glob)
+  X-Mercurial-Series-Id: <a2ea8fc83dd8b93cfd86.240@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:04:00 +0000
   From: quux
@@ -404,7 +432,10 @@
   Content-Transfer-Encoding: quoted-printable
   Subject: [PATCH] long line
   X-Mercurial-Node: a2ea8fc83dd8b93cfd86ac97b28287204ab806e1
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 1
   Message-Id: <a2ea8fc83dd8b93cfd86.240@*> (glob)
+  X-Mercurial-Series-Id: <a2ea8fc83dd8b93cfd86.240@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:04:00 +0000
   From: quux
@@ -463,7 +494,10 @@
   Content-Transfer-Encoding: 8bit
   Subject: [PATCH] isolatin 8-bit encoding
   X-Mercurial-Node: 240fb913fc1b7ff15ddb9f33e73d82bf5277c720
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 1
   Message-Id: <240fb913fc1b7ff15ddb.300@*> (glob)
+  X-Mercurial-Series-Id: <240fb913fc1b7ff15ddb.300@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:05:00 +0000
   From: quux
@@ -508,7 +542,10 @@
   Content-Transfer-Encoding: 7bit
   Subject: [PATCH] test
   X-Mercurial-Node: ff2c9fa2018b15fa74b33363bda9527323e2a99f
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 1
   Message-Id: <ff2c9fa2018b15fa74b3.60@*> (glob)
+  X-Mercurial-Series-Id: <ff2c9fa2018b15fa74b3.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:01:00 +0000
   From: quux
@@ -584,7 +621,10 @@
   Content-Transfer-Encoding: 7bit
   Subject: [PATCH 1 of 2] a
   X-Mercurial-Node: 8580ff50825a50c8f716709acdf8de0deddcd6ab
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 2
   Message-Id: <8580ff50825a50c8f716.61@*> (glob)
+  X-Mercurial-Series-Id: <8580ff50825a50c8f716.61@*> (glob)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -617,7 +657,10 @@
   Content-Transfer-Encoding: 7bit
   Subject: [PATCH 2 of 2] b
   X-Mercurial-Node: 97d72e5f12c7e84f85064aa72e5a297142c36ed9
+  X-Mercurial-Series-Index: 2
+  X-Mercurial-Series-Total: 2
   Message-Id: <97d72e5f12c7e84f8506.62@*> (glob)
+  X-Mercurial-Series-Id: <8580ff50825a50c8f716.61@*> (glob)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -651,18 +694,21 @@
   
   
   displaying [PATCH] test ...
-  Content-Type: multipart/mixed; boundary="===*" (glob)
+  Content-Type: multipart/mixed; boundary="===*==" (glob)
   MIME-Version: 1.0
   Subject: [PATCH] test
   X-Mercurial-Node: ff2c9fa2018b15fa74b33363bda9527323e2a99f
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 1
   Message-Id: <ff2c9fa2018b15fa74b3.60@*> (glob)
+  X-Mercurial-Series-Id: <ff2c9fa2018b15fa74b3.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:01:00 +0000
   From: quux
   To: foo
   Cc: bar
   
-  --===* (glob)
+  --===*= (glob)
   Content-Type: text/x-patch; charset="us-ascii"
   MIME-Version: 1.0
   Content-Transfer-Encoding: 7bit
@@ -682,7 +728,7 @@
   @@ -0,0 +1,1 @@
   +c
   
-  --===*-- (glob)
+  --===*=-- (glob)
 
 
 test inline for single patch (quoted-printable):
@@ -691,18 +737,21 @@
   
   
   displaying [PATCH] test ...
-  Content-Type: multipart/mixed; boundary="===*" (glob)
+  Content-Type: multipart/mixed; boundary="===*==" (glob)
   MIME-Version: 1.0
   Subject: [PATCH] test
   X-Mercurial-Node: a2ea8fc83dd8b93cfd86ac97b28287204ab806e1
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 1
   Message-Id: <a2ea8fc83dd8b93cfd86.60@*> (glob)
+  X-Mercurial-Series-Id: <a2ea8fc83dd8b93cfd86.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:01:00 +0000
   From: quux
   To: foo
   Cc: bar
   
-  --===* (glob)
+  --===*= (glob)
   Content-Type: text/x-patch; charset="us-ascii"
   MIME-Version: 1.0
   Content-Transfer-Encoding: quoted-printable
@@ -738,7 +787,7 @@
   +
   +bar
   
-  --===*-- (glob)
+  --===*=-- (glob)
 
 test inline for multiple patches:
   $ hg email --date '1970-1-1 0:1' -n -f quux -t foo -c bar -s test -i \
@@ -763,11 +812,14 @@
   
   
   displaying [PATCH 1 of 3] a ...
-  Content-Type: multipart/mixed; boundary="===*" (glob)
+  Content-Type: multipart/mixed; boundary="===*==" (glob)
   MIME-Version: 1.0
   Subject: [PATCH 1 of 3] a
   X-Mercurial-Node: 8580ff50825a50c8f716709acdf8de0deddcd6ab
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 3
   Message-Id: <8580ff50825a50c8f716.61@*> (glob)
+  X-Mercurial-Series-Id: <8580ff50825a50c8f716.61@*> (glob)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -776,7 +828,7 @@
   To: foo
   Cc: bar
   
-  --===* (glob)
+  --===*= (glob)
   Content-Type: text/x-patch; charset="us-ascii"
   MIME-Version: 1.0
   Content-Transfer-Encoding: 7bit
@@ -796,13 +848,16 @@
   @@ -0,0 +1,1 @@
   +a
   
-  --===*-- (glob)
+  --===*=-- (glob)
   displaying [PATCH 2 of 3] b ...
-  Content-Type: multipart/mixed; boundary="===*" (glob)
+  Content-Type: multipart/mixed; boundary="===*==" (glob)
   MIME-Version: 1.0
   Subject: [PATCH 2 of 3] b
   X-Mercurial-Node: 97d72e5f12c7e84f85064aa72e5a297142c36ed9
+  X-Mercurial-Series-Index: 2
+  X-Mercurial-Series-Total: 3
   Message-Id: <97d72e5f12c7e84f8506.62@*> (glob)
+  X-Mercurial-Series-Id: <8580ff50825a50c8f716.61@*> (glob)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -811,7 +866,7 @@
   To: foo
   Cc: bar
   
-  --===* (glob)
+  --===*= (glob)
   Content-Type: text/x-patch; charset="us-ascii"
   MIME-Version: 1.0
   Content-Transfer-Encoding: 7bit
@@ -831,13 +886,16 @@
   @@ -0,0 +1,1 @@
   +b
   
-  --===*-- (glob)
+  --===*=-- (glob)
   displaying [PATCH 3 of 3] long line ...
-  Content-Type: multipart/mixed; boundary="===*" (glob)
+  Content-Type: multipart/mixed; boundary="===*==" (glob)
   MIME-Version: 1.0
   Subject: [PATCH 3 of 3] long line
   X-Mercurial-Node: a2ea8fc83dd8b93cfd86ac97b28287204ab806e1
+  X-Mercurial-Series-Index: 3
+  X-Mercurial-Series-Total: 3
   Message-Id: <a2ea8fc83dd8b93cfd86.63@*> (glob)
+  X-Mercurial-Series-Id: <8580ff50825a50c8f716.61@*> (glob)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -846,7 +904,7 @@
   To: foo
   Cc: bar
   
-  --===* (glob)
+  --===*= (glob)
   Content-Type: text/x-patch; charset="us-ascii"
   MIME-Version: 1.0
   Content-Transfer-Encoding: quoted-printable
@@ -882,7 +940,7 @@
   +
   +bar
   
-  --===*-- (glob)
+  --===*=-- (glob)
 
 test attach for single patch:
   $ hg email --date '1970-1-1 0:1' -n -f quux -t foo -c bar -s test -a -r 2
@@ -890,18 +948,21 @@
   
   
   displaying [PATCH] test ...
-  Content-Type: multipart/mixed; boundary="===*" (glob)
+  Content-Type: multipart/mixed; boundary="===*==" (glob)
   MIME-Version: 1.0
   Subject: [PATCH] test
   X-Mercurial-Node: ff2c9fa2018b15fa74b33363bda9527323e2a99f
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 1
   Message-Id: <ff2c9fa2018b15fa74b3.60@*> (glob)
+  X-Mercurial-Series-Id: <ff2c9fa2018b15fa74b3.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:01:00 +0000
   From: quux
   To: foo
   Cc: bar
   
-  --===* (glob)
+  --===*= (glob)
   Content-Type: text/plain; charset="us-ascii"
   MIME-Version: 1.0
   Content-Transfer-Encoding: 7bit
@@ -910,7 +971,7 @@
   
   
   
-  --===* (glob)
+  --===*= (glob)
   Content-Type: text/x-patch; charset="us-ascii"
   MIME-Version: 1.0
   Content-Transfer-Encoding: 7bit
@@ -930,7 +991,7 @@
   @@ -0,0 +1,1 @@
   +c
   
-  --===*-- (glob)
+  --===*=-- (glob)
 
 test attach for single patch (quoted-printable):
   $ hg email --date '1970-1-1 0:1' -n -f quux -t foo -c bar -s test -a -r 4
@@ -938,18 +999,21 @@
   
   
   displaying [PATCH] test ...
-  Content-Type: multipart/mixed; boundary="===*" (glob)
+  Content-Type: multipart/mixed; boundary="===*==" (glob)
   MIME-Version: 1.0
   Subject: [PATCH] test
   X-Mercurial-Node: a2ea8fc83dd8b93cfd86ac97b28287204ab806e1
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 1
   Message-Id: <a2ea8fc83dd8b93cfd86.60@*> (glob)
+  X-Mercurial-Series-Id: <a2ea8fc83dd8b93cfd86.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:01:00 +0000
   From: quux
   To: foo
   Cc: bar
   
-  --===* (glob)
+  --===*= (glob)
   Content-Type: text/plain; charset="us-ascii"
   MIME-Version: 1.0
   Content-Transfer-Encoding: 7bit
@@ -958,7 +1022,7 @@
   
   
   
-  --===* (glob)
+  --===*= (glob)
   Content-Type: text/x-patch; charset="us-ascii"
   MIME-Version: 1.0
   Content-Transfer-Encoding: quoted-printable
@@ -994,7 +1058,7 @@
   +
   +bar
   
-  --===*-- (glob)
+  --===*=-- (glob)
 
 test attach and body for single patch:
   $ hg email --date '1970-1-1 0:1' -n -f quux -t foo -c bar -s test -a --body -r 2
@@ -1002,18 +1066,21 @@
   
   
   displaying [PATCH] test ...
-  Content-Type: multipart/mixed; boundary="===*" (glob)
+  Content-Type: multipart/mixed; boundary="===*==" (glob)
   MIME-Version: 1.0
   Subject: [PATCH] test
   X-Mercurial-Node: ff2c9fa2018b15fa74b33363bda9527323e2a99f
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 1
   Message-Id: <ff2c9fa2018b15fa74b3.60@*> (glob)
+  X-Mercurial-Series-Id: <ff2c9fa2018b15fa74b3.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:01:00 +0000
   From: quux
   To: foo
   Cc: bar
   
-  --===* (glob)
+  --===*= (glob)
   Content-Type: text/plain; charset="us-ascii"
   MIME-Version: 1.0
   Content-Transfer-Encoding: 7bit
@@ -1032,7 +1099,7 @@
   @@ -0,0 +1,1 @@
   +c
   
-  --===* (glob)
+  --===*= (glob)
   Content-Type: text/x-patch; charset="us-ascii"
   MIME-Version: 1.0
   Content-Transfer-Encoding: 7bit
@@ -1052,7 +1119,7 @@
   @@ -0,0 +1,1 @@
   +c
   
-  --===*-- (glob)
+  --===*=-- (glob)
 
 test attach for multiple patches:
   $ hg email --date '1970-1-1 0:1' -n -f quux -t foo -c bar -s test -a \
@@ -1077,11 +1144,14 @@
   
   
   displaying [PATCH 1 of 3] a ...
-  Content-Type: multipart/mixed; boundary="===*" (glob)
+  Content-Type: multipart/mixed; boundary="===*==" (glob)
   MIME-Version: 1.0
   Subject: [PATCH 1 of 3] a
   X-Mercurial-Node: 8580ff50825a50c8f716709acdf8de0deddcd6ab
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 3
   Message-Id: <8580ff50825a50c8f716.61@*> (glob)
+  X-Mercurial-Series-Id: <8580ff50825a50c8f716.61@*> (glob)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -1090,7 +1160,7 @@
   To: foo
   Cc: bar
   
-  --===* (glob)
+  --===*= (glob)
   Content-Type: text/plain; charset="us-ascii"
   MIME-Version: 1.0
   Content-Transfer-Encoding: 7bit
@@ -1099,7 +1169,7 @@
   
   
   
-  --===* (glob)
+  --===*= (glob)
   Content-Type: text/x-patch; charset="us-ascii"
   MIME-Version: 1.0
   Content-Transfer-Encoding: 7bit
@@ -1119,13 +1189,16 @@
   @@ -0,0 +1,1 @@
   +a
   
-  --===*-- (glob)
+  --===*=-- (glob)
   displaying [PATCH 2 of 3] b ...
-  Content-Type: multipart/mixed; boundary="===*" (glob)
+  Content-Type: multipart/mixed; boundary="===*==" (glob)
   MIME-Version: 1.0
   Subject: [PATCH 2 of 3] b
   X-Mercurial-Node: 97d72e5f12c7e84f85064aa72e5a297142c36ed9
+  X-Mercurial-Series-Index: 2
+  X-Mercurial-Series-Total: 3
   Message-Id: <97d72e5f12c7e84f8506.62@*> (glob)
+  X-Mercurial-Series-Id: <8580ff50825a50c8f716.61@*> (glob)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -1134,7 +1207,7 @@
   To: foo
   Cc: bar
   
-  --===* (glob)
+  --===*= (glob)
   Content-Type: text/plain; charset="us-ascii"
   MIME-Version: 1.0
   Content-Transfer-Encoding: 7bit
@@ -1143,7 +1216,7 @@
   
   
   
-  --===* (glob)
+  --===*= (glob)
   Content-Type: text/x-patch; charset="us-ascii"
   MIME-Version: 1.0
   Content-Transfer-Encoding: 7bit
@@ -1163,13 +1236,16 @@
   @@ -0,0 +1,1 @@
   +b
   
-  --===*-- (glob)
+  --===*=-- (glob)
   displaying [PATCH 3 of 3] long line ...
-  Content-Type: multipart/mixed; boundary="===*" (glob)
+  Content-Type: multipart/mixed; boundary="===*==" (glob)
   MIME-Version: 1.0
   Subject: [PATCH 3 of 3] long line
   X-Mercurial-Node: a2ea8fc83dd8b93cfd86ac97b28287204ab806e1
+  X-Mercurial-Series-Index: 3
+  X-Mercurial-Series-Total: 3
   Message-Id: <a2ea8fc83dd8b93cfd86.63@*> (glob)
+  X-Mercurial-Series-Id: <8580ff50825a50c8f716.61@*> (glob)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -1178,7 +1254,7 @@
   To: foo
   Cc: bar
   
-  --===* (glob)
+  --===*= (glob)
   Content-Type: text/plain; charset="us-ascii"
   MIME-Version: 1.0
   Content-Transfer-Encoding: 7bit
@@ -1187,7 +1263,7 @@
   
   
   
-  --===* (glob)
+  --===*= (glob)
   Content-Type: text/x-patch; charset="us-ascii"
   MIME-Version: 1.0
   Content-Transfer-Encoding: quoted-printable
@@ -1223,7 +1299,7 @@
   +
   +bar
   
-  --===*-- (glob)
+  --===*=-- (glob)
 
 test intro for single patch:
   $ hg email --date '1970-1-1 0:1' -n --intro -f quux -t foo -c bar -s test \
@@ -1253,7 +1329,10 @@
   Content-Transfer-Encoding: 7bit
   Subject: [PATCH 1 of 1] c
   X-Mercurial-Node: ff2c9fa2018b15fa74b33363bda9527323e2a99f
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 1
   Message-Id: <ff2c9fa2018b15fa74b3.61@*> (glob)
+  X-Mercurial-Series-Id: <ff2c9fa2018b15fa74b3.61@*> (glob)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -1304,7 +1383,10 @@
   Content-Transfer-Encoding: 7bit
   Subject: [PATCH 1 of 1] c
   X-Mercurial-Node: ff2c9fa2018b15fa74b33363bda9527323e2a99f
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 1
   Message-Id: <ff2c9fa2018b15fa74b3.61@*> (glob)
+  X-Mercurial-Series-Id: <ff2c9fa2018b15fa74b3.61@*> (glob)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -1356,7 +1438,10 @@
   Content-Transfer-Encoding: 7bit
   Subject: [PATCH 1 of 2] a
   X-Mercurial-Node: 8580ff50825a50c8f716709acdf8de0deddcd6ab
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 2
   Message-Id: <8580ff50825a50c8f716.61@*> (glob)
+  X-Mercurial-Series-Id: <8580ff50825a50c8f716.61@*> (glob)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -1385,7 +1470,10 @@
   Content-Transfer-Encoding: 7bit
   Subject: [PATCH 2 of 2] b
   X-Mercurial-Node: 97d72e5f12c7e84f85064aa72e5a297142c36ed9
+  X-Mercurial-Series-Index: 2
+  X-Mercurial-Series-Total: 2
   Message-Id: <97d72e5f12c7e84f8506.62@*> (glob)
+  X-Mercurial-Series-Id: <8580ff50825a50c8f716.61@*> (glob)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -1421,7 +1509,10 @@
   Content-Transfer-Encoding: 7bit
   Subject: [PATCH] test
   X-Mercurial-Node: ff2c9fa2018b15fa74b33363bda9527323e2a99f
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 1
   Message-Id: <ff2c9fa2018b15fa74b3.60@*> (glob)
+  X-Mercurial-Series-Id: <ff2c9fa2018b15fa74b3.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:01:00 +0000
   From: quux
@@ -1456,7 +1547,10 @@
   Content-Transfer-Encoding: 7bit
   Subject: [PATCH] test
   X-Mercurial-Node: ff2c9fa2018b15fa74b33363bda9527323e2a99f
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 1
   Message-Id: <ff2c9fa2018b15fa74b3.60@*> (glob)
+  X-Mercurial-Series-Id: <ff2c9fa2018b15fa74b3.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:01:00 +0000
   From: quux
@@ -1490,18 +1584,21 @@
   
   
   displaying [PATCH] test ...
-  Content-Type: multipart/mixed; boundary="===*" (glob)
+  Content-Type: multipart/mixed; boundary="===*==" (glob)
   MIME-Version: 1.0
   Subject: [PATCH] test
   X-Mercurial-Node: ff2c9fa2018b15fa74b33363bda9527323e2a99f
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 1
   Message-Id: <ff2c9fa2018b15fa74b3.60@*> (glob)
+  X-Mercurial-Series-Id: <ff2c9fa2018b15fa74b3.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:01:00 +0000
   From: quux
   To: foo
   Cc: bar
   
-  --===* (glob)
+  --===*= (glob)
   Content-Type: text/x-patch; charset="us-ascii"
   MIME-Version: 1.0
   Content-Transfer-Encoding: 7bit
@@ -1521,7 +1618,7 @@
   @@ -0,0 +1,1 @@
   +c
   
-  --===*-- (glob)
+  --===*=-- (glob)
 
 test inline for multiple named/unnamed patches:
   $ hg email --date '1970-1-1 0:1' -n -f quux -t foo -c bar -s test -i -r 0:1
@@ -1545,11 +1642,14 @@
   
   
   displaying [PATCH 1 of 2] a ...
-  Content-Type: multipart/mixed; boundary="===*" (glob)
+  Content-Type: multipart/mixed; boundary="===*==" (glob)
   MIME-Version: 1.0
   Subject: [PATCH 1 of 2] a
   X-Mercurial-Node: 8580ff50825a50c8f716709acdf8de0deddcd6ab
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 2
   Message-Id: <8580ff50825a50c8f716.61@*> (glob)
+  X-Mercurial-Series-Id: <8580ff50825a50c8f716.61@*> (glob)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -1558,7 +1658,7 @@
   To: foo
   Cc: bar
   
-  --===* (glob)
+  --===*= (glob)
   Content-Type: text/x-patch; charset="us-ascii"
   MIME-Version: 1.0
   Content-Transfer-Encoding: 7bit
@@ -1578,13 +1678,16 @@
   @@ -0,0 +1,1 @@
   +a
   
-  --===*-- (glob)
+  --===*=-- (glob)
   displaying [PATCH 2 of 2] b ...
-  Content-Type: multipart/mixed; boundary="===*" (glob)
+  Content-Type: multipart/mixed; boundary="===*==" (glob)
   MIME-Version: 1.0
   Subject: [PATCH 2 of 2] b
   X-Mercurial-Node: 97d72e5f12c7e84f85064aa72e5a297142c36ed9
+  X-Mercurial-Series-Index: 2
+  X-Mercurial-Series-Total: 2
   Message-Id: <97d72e5f12c7e84f8506.62@*> (glob)
+  X-Mercurial-Series-Id: <8580ff50825a50c8f716.61@*> (glob)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -1593,7 +1696,7 @@
   To: foo
   Cc: bar
   
-  --===* (glob)
+  --===*= (glob)
   Content-Type: text/x-patch; charset="us-ascii"
   MIME-Version: 1.0
   Content-Transfer-Encoding: 7bit
@@ -1613,7 +1716,7 @@
   @@ -0,0 +1,1 @@
   +b
   
-  --===*-- (glob)
+  --===*=-- (glob)
 
 
 test inreplyto:
@@ -1628,7 +1731,10 @@
   Content-Transfer-Encoding: 7bit
   Subject: [PATCH] Added tag two, two.diff for changeset ff2c9fa2018b
   X-Mercurial-Node: 7aead2484924c445ad8ce2613df91f52f9e502ed
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 1
   Message-Id: <7aead2484924c445ad8c.60@*> (glob)
+  X-Mercurial-Series-Id: <7aead2484924c445ad8c.60@*> (glob)
   In-Reply-To: <baz>
   References: <baz>
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -1668,7 +1774,10 @@
   Content-Transfer-Encoding: 7bit
   Subject: [PATCH 1 of 2] a
   X-Mercurial-Node: 8580ff50825a50c8f716709acdf8de0deddcd6ab
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 2
   Message-Id: <8580ff50825a50c8f716.60@*> (glob)
+  X-Mercurial-Series-Id: <8580ff50825a50c8f716.60@*> (glob)
   In-Reply-To: <baz>
   References: <baz>
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -1697,7 +1806,10 @@
   Content-Transfer-Encoding: 7bit
   Subject: [PATCH 2 of 2] b
   X-Mercurial-Node: 97d72e5f12c7e84f85064aa72e5a297142c36ed9
+  X-Mercurial-Series-Index: 2
+  X-Mercurial-Series-Total: 2
   Message-Id: <97d72e5f12c7e84f8506.61@*> (glob)
+  X-Mercurial-Series-Id: <8580ff50825a50c8f716.60@*> (glob)
   In-Reply-To: <baz>
   References: <baz>
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -1752,7 +1864,10 @@
   Content-Transfer-Encoding: 7bit
   Subject: [PATCH 1 of 2] a
   X-Mercurial-Node: 8580ff50825a50c8f716709acdf8de0deddcd6ab
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 2
   Message-Id: <8580ff50825a50c8f716.61@*> (glob)
+  X-Mercurial-Series-Id: <8580ff50825a50c8f716.61@*> (glob)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -1781,7 +1896,10 @@
   Content-Transfer-Encoding: 7bit
   Subject: [PATCH 2 of 2] b
   X-Mercurial-Node: 97d72e5f12c7e84f85064aa72e5a297142c36ed9
+  X-Mercurial-Series-Index: 2
+  X-Mercurial-Series-Total: 2
   Message-Id: <97d72e5f12c7e84f8506.62@*> (glob)
+  X-Mercurial-Series-Id: <8580ff50825a50c8f716.61@*> (glob)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -1819,7 +1937,10 @@
   Content-Transfer-Encoding: 7bit
   Subject: [PATCH fooFlag] test
   X-Mercurial-Node: ff2c9fa2018b15fa74b33363bda9527323e2a99f
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 1
   Message-Id: <ff2c9fa2018b15fa74b3.60@*> (glob)
+  X-Mercurial-Series-Id: <ff2c9fa2018b15fa74b3.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:01:00 +0000
   From: quux
@@ -1870,7 +1991,10 @@
   Content-Transfer-Encoding: 7bit
   Subject: [PATCH 1 of 2 fooFlag] a
   X-Mercurial-Node: 8580ff50825a50c8f716709acdf8de0deddcd6ab
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 2
   Message-Id: <8580ff50825a50c8f716.61@*> (glob)
+  X-Mercurial-Series-Id: <8580ff50825a50c8f716.61@*> (glob)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -1899,7 +2023,10 @@
   Content-Transfer-Encoding: 7bit
   Subject: [PATCH 2 of 2 fooFlag] b
   X-Mercurial-Node: 97d72e5f12c7e84f85064aa72e5a297142c36ed9
+  X-Mercurial-Series-Index: 2
+  X-Mercurial-Series-Total: 2
   Message-Id: <97d72e5f12c7e84f8506.62@*> (glob)
+  X-Mercurial-Series-Id: <8580ff50825a50c8f716.61@*> (glob)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -1937,7 +2064,10 @@
   Content-Transfer-Encoding: 7bit
   Subject: [PATCH fooFlag barFlag] test
   X-Mercurial-Node: ff2c9fa2018b15fa74b33363bda9527323e2a99f
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 1
   Message-Id: <ff2c9fa2018b15fa74b3.60@*> (glob)
+  X-Mercurial-Series-Id: <ff2c9fa2018b15fa74b3.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:01:00 +0000
   From: quux
@@ -1987,7 +2117,10 @@
   Content-Transfer-Encoding: 7bit
   Subject: [PATCH 1 of 2 fooFlag barFlag] a
   X-Mercurial-Node: 8580ff50825a50c8f716709acdf8de0deddcd6ab
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 2
   Message-Id: <8580ff50825a50c8f716.61@*> (glob)
+  X-Mercurial-Series-Id: <8580ff50825a50c8f716.61@*> (glob)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -2016,7 +2149,10 @@
   Content-Transfer-Encoding: 7bit
   Subject: [PATCH 2 of 2 fooFlag barFlag] b
   X-Mercurial-Node: 97d72e5f12c7e84f85064aa72e5a297142c36ed9
+  X-Mercurial-Series-Index: 2
+  X-Mercurial-Series-Total: 2
   Message-Id: <97d72e5f12c7e84f8506.62@*> (glob)
+  X-Mercurial-Series-Id: <8580ff50825a50c8f716.61@*> (glob)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -2055,7 +2191,10 @@
   Content-Transfer-Encoding: 7bit
   Subject: [PATCH] test
   X-Mercurial-Node: 8580ff50825a50c8f716709acdf8de0deddcd6ab
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 1
   Message-Id: <8580ff50825a50c8f716.315532860@*> (glob)
+  X-Mercurial-Series-Id: <8580ff50825a50c8f716.315532860@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Tue, 01 Jan 1980 00:01:00 +0000
   From: quux
@@ -2097,7 +2236,10 @@
   Content-Transfer-Encoding: 7bit
   Subject: [PATCH] test
   X-Mercurial-Node: 8580ff50825a50c8f716709acdf8de0deddcd6ab
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 1
   Message-Id: <8580ff50825a50c8f716.315532860@*> (glob)
+  X-Mercurial-Series-Id: <8580ff50825a50c8f716.315532860@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Tue, 01 Jan 1980 00:01:00 +0000
   From: quux
@@ -2184,7 +2326,10 @@
   Content-Transfer-Encoding: 7bit
   Subject: [PATCH 1 of 6] c
   X-Mercurial-Node: ff2c9fa2018b15fa74b33363bda9527323e2a99f
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 6
   Message-Id: <ff2c9fa2018b15fa74b3.315532861@*> (glob)
+  X-Mercurial-Series-Id: <ff2c9fa2018b15fa74b3.315532861@*> (glob)
   In-Reply-To: <patchbomb.315532860@*> (glob)
   References: <patchbomb.315532860@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -2212,7 +2357,10 @@
   Content-Transfer-Encoding: 8bit
   Subject: [PATCH 2 of 6] utf-8 content
   X-Mercurial-Node: 909a00e13e9d78b575aeee23dddbada46d5a143f
+  X-Mercurial-Series-Index: 2
+  X-Mercurial-Series-Total: 6
   Message-Id: <909a00e13e9d78b575ae.315532862@*> (glob)
+  X-Mercurial-Series-Id: <ff2c9fa2018b15fa74b3.315532861@*> (glob)
   In-Reply-To: <patchbomb.315532860@*> (glob)
   References: <patchbomb.315532860@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -2247,7 +2395,10 @@
   Content-Transfer-Encoding: quoted-printable
   Subject: [PATCH 3 of 6] long line
   X-Mercurial-Node: a2ea8fc83dd8b93cfd86ac97b28287204ab806e1
+  X-Mercurial-Series-Index: 3
+  X-Mercurial-Series-Total: 6
   Message-Id: <a2ea8fc83dd8b93cfd86.315532863@*> (glob)
+  X-Mercurial-Series-Id: <ff2c9fa2018b15fa74b3.315532861@*> (glob)
   In-Reply-To: <patchbomb.315532860@*> (glob)
   References: <patchbomb.315532860@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -2291,7 +2442,10 @@
   Content-Transfer-Encoding: 8bit
   Subject: [PATCH 4 of 6] isolatin 8-bit encoding
   X-Mercurial-Node: 240fb913fc1b7ff15ddb9f33e73d82bf5277c720
+  X-Mercurial-Series-Index: 4
+  X-Mercurial-Series-Total: 6
   Message-Id: <240fb913fc1b7ff15ddb.315532864@*> (glob)
+  X-Mercurial-Series-Id: <ff2c9fa2018b15fa74b3.315532861@*> (glob)
   In-Reply-To: <patchbomb.315532860@*> (glob)
   References: <patchbomb.315532860@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -2319,7 +2473,10 @@
   Content-Transfer-Encoding: 7bit
   Subject: [PATCH 5 of 6] Added tag zero, zero.foo for changeset 8580ff50825a
   X-Mercurial-Node: 5d5ef15dfe5e7bd3a4ee154b5fff76c7945ec433
+  X-Mercurial-Series-Index: 5
+  X-Mercurial-Series-Total: 6
   Message-Id: <5d5ef15dfe5e7bd3a4ee.315532865@*> (glob)
+  X-Mercurial-Series-Id: <ff2c9fa2018b15fa74b3.315532861@*> (glob)
   In-Reply-To: <patchbomb.315532860@*> (glob)
   References: <patchbomb.315532860@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -2348,7 +2505,10 @@
   Content-Transfer-Encoding: 7bit
   Subject: [PATCH 6 of 6] d
   X-Mercurial-Node: 2f9fa9b998c5fe3ac2bd9a2b14bfcbeecbc7c268
+  X-Mercurial-Series-Index: 6
+  X-Mercurial-Series-Total: 6
   Message-Id: <2f9fa9b998c5fe3ac2bd.315532866@*> (glob)
+  X-Mercurial-Series-Id: <ff2c9fa2018b15fa74b3.315532861@*> (glob)
   In-Reply-To: <patchbomb.315532860@*> (glob)
   References: <patchbomb.315532860@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
@@ -2386,7 +2546,10 @@
   Content-Transfer-Encoding: 7bit
   Subject: [PATCH] test
   X-Mercurial-Node: 2f9fa9b998c5fe3ac2bd9a2b14bfcbeecbc7c268
+  X-Mercurial-Series-Index: 1
+  X-Mercurial-Series-Total: 1
   Message-Id: <2f9fa9b998c5fe3ac2bd.315532860@*> (glob)
+  X-Mercurial-Series-Id: <2f9fa9b998c5fe3ac2bd.315532860@*> (glob)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Tue, 01 Jan 1980 00:01:00 +0000
   From: test
--- a/tests/test-progress.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-progress.t	Sat Jun 14 11:56:20 2014 -0500
@@ -1,7 +1,11 @@
 
   $ cat > loop.py <<EOF
-  > from mercurial import commands
+  > from mercurial import cmdutil, commands
   > import time
+  > 
+  > cmdtable = {}
+  > command = cmdutil.command(cmdtable)
+  > 
   > class incrementingtime(object):
   >     def __init__(self):
   >         self._time = 0.0
@@ -10,6 +14,11 @@
   >         return self._time
   > time.time = incrementingtime()
   > 
+  > @command('loop',
+  >     [('', 'total', '', 'override for total'),
+  >     ('', 'nested', False, 'show nested results'),
+  >     ('', 'parallel', False, 'show parallel sets of results')],
+  >     'hg loop LOOPS')
   > def loop(ui, loops, **opts):
   >     loops = int(loops)
   >     total = None
@@ -38,14 +47,6 @@
   >     ui.progress('loop', None, 'loop.done', 'loopnum', total)
   > 
   > commands.norepo += " loop"
-  > 
-  > cmdtable = {
-  >     "loop": (loop, [('', 'total', '', 'override for total'),
-  >                     ('', 'nested', False, 'show nested results'),
-  >                     ('', 'parallel', False, 'show parallel sets of results'),
-  >                    ],
-  >              'hg loop LOOPS'),
-  > }
   > EOF
 
   $ cp $HGRCPATH $HGRCPATH.orig
--- a/tests/test-rebase-bookmarks.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-rebase-bookmarks.t	Sat Jun 14 11:56:20 2014 -0500
@@ -154,6 +154,7 @@
 
   $ hg up 2
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (leaving bookmark X)
   $ echo 'C' > c
   $ hg add c
   $ hg ci -m 'other C'
@@ -168,6 +169,7 @@
   [1]
   $ echo 'c' > c
   $ hg resolve --mark c
+  no more unresolved files
   $ hg rebase --continue
   saved backup bundle to $TESTTMP/a3/.hg/strip-backup/3d5fa227f4b5-backup.hg (glob)
   $ hg tglog
--- a/tests/test-rebase-check-restore.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-rebase-check-restore.t	Sat Jun 14 11:56:20 2014 -0500
@@ -76,6 +76,7 @@
   $ echo 'conflict solved' > A
   $ rm A.orig
   $ hg resolve -m A
+  no more unresolved files
   $ hg rebase --continue
 
   $ hg tglog
@@ -129,6 +130,7 @@
   $ echo 'conflict solved' > A
   $ rm A.orig
   $ hg resolve -m A
+  no more unresolved files
   $ hg rebase --continue
   saved backup bundle to $TESTTMP/a2/.hg/strip-backup/*-backup.hg (glob)
 
--- a/tests/test-rebase-conflicts.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-rebase-conflicts.t	Sat Jun 14 11:56:20 2014 -0500
@@ -77,6 +77,7 @@
 
   $ echo 'resolved merge' >common
   $ hg resolve -m common
+  no more unresolved files
   $ hg rebase --continue
   saved backup bundle to $TESTTMP/a/.hg/strip-backup/*-backup.hg (glob)
 
@@ -219,9 +220,9 @@
    branchmerge: False, force: True, partial: False
    ancestor: d79e2059b5c0+, local: d79e2059b5c0+, remote: 4bc80088dc6b
    f2.txt: other deleted -> r
-   f1.txt: remote created -> g
   removing f2.txt
   updating: f2.txt 1/2 files (50.00%)
+   f1.txt: remote created -> g
   getting f1.txt
   updating: f1.txt 2/2 files (100.00%)
    merge against 9:e31216eec445
@@ -254,9 +255,9 @@
    branchmerge: False, force: False, partial: False
    ancestor: 2a7f09cac94c, local: 2a7f09cac94c+, remote: d79e2059b5c0
    f1.txt: other deleted -> r
-   f2.txt: remote created -> g
   removing f1.txt
   updating: f1.txt 1/2 files (50.00%)
+   f2.txt: remote created -> g
   getting f2.txt
   updating: f2.txt 2/2 files (100.00%)
   3 changesets found
--- a/tests/test-rebase-detach.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-rebase-detach.t	Sat Jun 14 11:56:20 2014 -0500
@@ -374,6 +374,7 @@
   unresolved conflicts (see hg resolve, then hg rebase --continue)
   [1]
   $ hg resolve --all -t internal:local
+  no more unresolved files
   $ hg rebase -c
   saved backup bundle to $TESTTMP/a7/.hg/strip-backup/6215fafa5447-backup.hg (glob)
   $ hg  log -G --template "{rev}:{phase} '{desc}' {branches}\n"
--- a/tests/test-rebase-interruptions.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-rebase-interruptions.t	Sat Jun 14 11:56:20 2014 -0500
@@ -104,6 +104,7 @@
   $ echo 'conflict solved' > A
   $ rm A.orig
   $ hg resolve -m A
+  no more unresolved files
 
   $ hg rebase --continue
   warning: new changesets detected on source branch, not stripping
--- a/tests/test-rebase-mq-skip.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-rebase-mq-skip.t	Sat Jun 14 11:56:20 2014 -0500
@@ -111,6 +111,7 @@
   [1]
 
   $ HGMERGE=internal:local hg resolve --all
+  no more unresolved files
 
   $ hg rebase --continue
   saved backup bundle to $TESTTMP/b/.hg/strip-backup/*-backup.hg (glob)
--- a/tests/test-rebase-mq.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-rebase-mq.t	Sat Jun 14 11:56:20 2014 -0500
@@ -69,6 +69,7 @@
 
   $ echo mq1r1 > f
   $ hg resolve -m f
+  no more unresolved files
   $ hg rebase -c
   merging f
   warning: conflicts during merge.
@@ -80,6 +81,7 @@
 
   $ echo mq1r1mq2 > f
   $ hg resolve -m f
+  no more unresolved files
   $ hg rebase -c
   saved backup bundle to $TESTTMP/a/.hg/strip-backup/*-backup.hg (glob)
 
--- a/tests/test-rebase-parameters.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-rebase-parameters.t	Sat Jun 14 11:56:20 2014 -0500
@@ -454,6 +454,7 @@
   U c2
 
   $ hg resolve -m c2
+  no more unresolved files
   $ hg rebase -c --tool internal:fail
   tool option will be ignored
   saved backup bundle to $TESTTMP/b3/.hg/strip-backup/*-backup.hg (glob)
--- a/tests/test-rebase-scenario-global.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-rebase-scenario-global.t	Sat Jun 14 11:56:20 2014 -0500
@@ -25,6 +25,7 @@
 
 Rebasing
 D onto H - simple rebase:
+(this also tests that editor is invoked if '--edit' is specified)
 
   $ hg clone -q -u . a a1
   $ cd a1
@@ -47,7 +48,18 @@
   o  0: 'A'
   
 
-  $ hg rebase -s 3 -d 7
+  $ hg status --rev "3^1" --rev 3
+  A D
+  $ HGEDITOR=cat hg rebase -s 3 -d 7 --edit
+  D
+  
+  
+  HG: Enter commit message.  Lines beginning with 'HG:' are removed.
+  HG: Leave message empty to abort commit.
+  HG: --
+  HG: user: Nicolas Dumazet <nicdumz.commits@gmail.com>
+  HG: branch 'default'
+  HG: changed D
   saved backup bundle to $TESTTMP/a1/.hg/strip-backup/*-backup.hg (glob)
 
   $ hg tglog
@@ -71,11 +83,12 @@
 
 
 D onto F - intermediate point:
+(this also tests that editor is not invoked if '--edit' is not specified)
 
   $ hg clone -q -u . a a2
   $ cd a2
 
-  $ hg rebase -s 3 -d 5
+  $ HGEDITOR=cat hg rebase -s 3 -d 5
   saved backup bundle to $TESTTMP/a2/.hg/strip-backup/*-backup.hg (glob)
 
   $ hg tglog
--- a/tests/test-rename-dir-merge.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-rename-dir-merge.t	Sat Jun 14 11:56:20 2014 -0500
@@ -40,16 +40,16 @@
    branchmerge: True, force: False, partial: False
    ancestor: f9b20c0d4c51, local: ce36d17b18fb+, remote: 397f8b00a740
    a/a: other deleted -> r
+  removing a/a
    a/b: other deleted -> r
-   b/a: remote created -> g
-   b/b: remote created -> g
-   b/c: remote directory rename - move from a/c -> dm
-  removing a/a
   removing a/b
   updating: a/b 2/5 files (40.00%)
+   b/a: remote created -> g
   getting b/a
+   b/b: remote created -> g
   getting b/b
   updating: b/b 4/5 files (80.00%)
+   b/c: remote directory rename - move from a/c -> dm
   updating: b/c 5/5 files (100.00%)
   moving a/c to b/c (glob)
   3 files updated, 0 files merged, 2 files removed, 0 files unresolved
--- a/tests/test-rename-merge1.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-rename-merge1.t	Sat Jun 14 11:56:20 2014 -0500
@@ -36,22 +36,22 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: af1939970a1c, local: 044f8520aeeb+, remote: 85c198ef2f6c
-   a2: divergent renames -> dr
-   b: remote moved from a -> m
-    preserving a for resolve of b
+   preserving a for resolve of b
+  removing a
    b2: remote created -> g
-  removing a
   getting b2
   updating: b2 1/3 files (33.33%)
-  updating: a2 2/3 files (66.67%)
-  note: possible conflict - a2 was renamed multiple times to:
-   c2
-   b2
-  updating: b 3/3 files (100.00%)
+   b: remote moved from a -> m
+  updating: b 2/3 files (66.67%)
   picked tool 'internal:merge' for b (binary False symlink False)
   merging a and b to b
   my b@044f8520aeeb+ other b@85c198ef2f6c ancestor a@af1939970a1c
    premerge successful
+   a2: divergent renames -> dr
+  updating: a2 3/3 files (100.00%)
+  note: possible conflict - a2 was renamed multiple times to:
+   c2
+   b2
   1 files updated, 1 files merged, 0 files removed, 0 files unresolved
   (branch merge, don't forget to commit)
 
@@ -181,10 +181,10 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 19d7f95df299, local: 0084274f6b67+, remote: 5d32493049f0
-   file: rename and delete -> rd
    newfile: remote created -> g
   getting newfile
   updating: newfile 1/2 files (50.00%)
+   file: rename and delete -> rd
   updating: file 2/2 files (100.00%)
   note: possible conflict - file was deleted and renamed to:
    newfile
--- a/tests/test-rename-merge2.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-rename-merge2.t	Sat Jun 14 11:56:20 2014 -0500
@@ -86,16 +86,16 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 924404dff337, local: e300d1c794ec+, remote: 4ce40f5aca24
+   preserving a for resolve of b
+   preserving rev for resolve of rev
    a: keep -> k
    b: remote copied from a -> m
-    preserving a for resolve of b
-   rev: versions differ -> m
-    preserving rev for resolve of rev
   updating: b 1/2 files (50.00%)
   picked tool 'python ../merge' for b (binary False symlink False)
   merging a and b to b
   my b@e300d1c794ec+ other b@4ce40f5aca24 ancestor a@924404dff337
    premerge successful
+   rev: versions differ -> m
   updating: rev 2/2 files (100.00%)
   picked tool 'python ../merge' for rev (binary False symlink False)
   merging rev
@@ -122,18 +122,18 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 924404dff337, local: 86a2aa42fc76+, remote: f4db7e329e71
+   preserving b for resolve of b
+   preserving rev for resolve of rev
    a: remote is newer -> g
-   b: local copied/moved from a -> m
-    preserving b for resolve of b
-   rev: versions differ -> m
-    preserving rev for resolve of rev
   getting a
   updating: a 1/3 files (33.33%)
+   b: local copied/moved from a -> m
   updating: b 2/3 files (66.67%)
   picked tool 'python ../merge' for b (binary False symlink False)
   merging b and a to b
   my b@86a2aa42fc76+ other a@f4db7e329e71 ancestor a@924404dff337
    premerge successful
+   rev: versions differ -> m
   updating: rev 3/3 files (100.00%)
   picked tool 'python ../merge' for rev (binary False symlink False)
   merging rev
@@ -160,16 +160,16 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 924404dff337, local: e300d1c794ec+, remote: bdb19105162a
+   preserving a for resolve of b
+   preserving rev for resolve of rev
+  removing a
    b: remote moved from a -> m
-    preserving a for resolve of b
-   rev: versions differ -> m
-    preserving rev for resolve of rev
-  removing a
   updating: b 1/2 files (50.00%)
   picked tool 'python ../merge' for b (binary False symlink False)
   merging a and b to b
   my b@e300d1c794ec+ other b@bdb19105162a ancestor a@924404dff337
    premerge successful
+   rev: versions differ -> m
   updating: rev 2/2 files (100.00%)
   picked tool 'python ../merge' for rev (binary False symlink False)
   merging rev
@@ -195,15 +195,15 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 924404dff337, local: 02963e448370+, remote: f4db7e329e71
+   preserving b for resolve of b
+   preserving rev for resolve of rev
    b: local copied/moved from a -> m
-    preserving b for resolve of b
-   rev: versions differ -> m
-    preserving rev for resolve of rev
   updating: b 1/2 files (50.00%)
   picked tool 'python ../merge' for b (binary False symlink False)
   merging b and a to b
   my b@02963e448370+ other a@f4db7e329e71 ancestor a@924404dff337
    premerge successful
+   rev: versions differ -> m
   updating: rev 2/2 files (100.00%)
   picked tool 'python ../merge' for rev (binary False symlink False)
   merging rev
@@ -229,11 +229,11 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 924404dff337, local: 94b33a1b7f2d+, remote: 4ce40f5aca24
+   preserving rev for resolve of rev
    b: remote created -> g
-   rev: versions differ -> m
-    preserving rev for resolve of rev
   getting b
   updating: b 1/2 files (50.00%)
+   rev: versions differ -> m
   updating: rev 2/2 files (100.00%)
   picked tool 'python ../merge' for rev (binary False symlink False)
   merging rev
@@ -259,8 +259,8 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 924404dff337, local: 86a2aa42fc76+, remote: 97c705ade336
+   preserving rev for resolve of rev
    rev: versions differ -> m
-    preserving rev for resolve of rev
   updating: rev 1/1 files (100.00%)
   picked tool 'python ../merge' for rev (binary False symlink False)
   merging rev
@@ -286,14 +286,14 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 924404dff337, local: 94b33a1b7f2d+, remote: bdb19105162a
+   preserving rev for resolve of rev
    a: other deleted -> r
-   b: remote created -> g
-   rev: versions differ -> m
-    preserving rev for resolve of rev
   removing a
   updating: a 1/3 files (33.33%)
+   b: remote created -> g
   getting b
   updating: b 2/3 files (66.67%)
+   rev: versions differ -> m
   updating: rev 3/3 files (100.00%)
   picked tool 'python ../merge' for rev (binary False symlink False)
   merging rev
@@ -318,8 +318,8 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 924404dff337, local: 02963e448370+, remote: 97c705ade336
+   preserving rev for resolve of rev
    rev: versions differ -> m
-    preserving rev for resolve of rev
   updating: rev 1/1 files (100.00%)
   picked tool 'python ../merge' for rev (binary False symlink False)
   merging rev
@@ -341,14 +341,14 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 924404dff337, local: 62e7bf090eba+, remote: 49b6d8032493
+   preserving b for resolve of b
+   preserving rev for resolve of rev
    b: versions differ -> m
-    preserving b for resolve of b
-   rev: versions differ -> m
-    preserving rev for resolve of rev
   updating: b 1/2 files (50.00%)
   picked tool 'python ../merge' for b (binary False symlink False)
   merging b
   my b@62e7bf090eba+ other b@49b6d8032493 ancestor a@924404dff337
+   rev: versions differ -> m
   updating: rev 2/2 files (100.00%)
   picked tool 'python ../merge' for rev (binary False symlink False)
   merging rev
@@ -379,20 +379,20 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 924404dff337, local: 02963e448370+, remote: fe905ef2c33e
-   a: divergent renames -> dr
+   preserving rev for resolve of rev
    c: remote created -> g
-   rev: versions differ -> m
-    preserving rev for resolve of rev
   getting c
   updating: c 1/3 files (33.33%)
-  updating: a 2/3 files (66.67%)
+   rev: versions differ -> m
+  updating: rev 2/3 files (66.67%)
+  picked tool 'python ../merge' for rev (binary False symlink False)
+  merging rev
+  my rev@02963e448370+ other rev@fe905ef2c33e ancestor rev@924404dff337
+   a: divergent renames -> dr
+  updating: a 3/3 files (100.00%)
   note: possible conflict - a was renamed multiple times to:
    b
    c
-  updating: rev 3/3 files (100.00%)
-  picked tool 'python ../merge' for rev (binary False symlink False)
-  merging rev
-  my rev@02963e448370+ other rev@fe905ef2c33e ancestor rev@924404dff337
   1 files updated, 1 files merged, 0 files removed, 0 files unresolved
   (branch merge, don't forget to commit)
   --------------
@@ -411,14 +411,14 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 924404dff337, local: 86a2aa42fc76+, remote: af30c7647fc7
+   preserving b for resolve of b
+   preserving rev for resolve of rev
    b: versions differ -> m
-    preserving b for resolve of b
-   rev: versions differ -> m
-    preserving rev for resolve of rev
   updating: b 1/2 files (50.00%)
   picked tool 'python ../merge' for b (binary False symlink False)
   merging b
   my b@86a2aa42fc76+ other b@af30c7647fc7 ancestor b@000000000000
+   rev: versions differ -> m
   updating: rev 2/2 files (100.00%)
   picked tool 'python ../merge' for rev (binary False symlink False)
   merging rev
@@ -441,17 +441,17 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 924404dff337, local: 59318016310c+, remote: bdb19105162a
+   preserving b for resolve of b
+   preserving rev for resolve of rev
    a: other deleted -> r
-   b: versions differ -> m
-    preserving b for resolve of b
-   rev: versions differ -> m
-    preserving rev for resolve of rev
   removing a
   updating: a 1/3 files (33.33%)
+   b: versions differ -> m
   updating: b 2/3 files (66.67%)
   picked tool 'python ../merge' for b (binary False symlink False)
   merging b
   my b@59318016310c+ other b@bdb19105162a ancestor b@000000000000
+   rev: versions differ -> m
   updating: rev 3/3 files (100.00%)
   picked tool 'python ../merge' for rev (binary False symlink False)
   merging rev
@@ -473,17 +473,17 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 924404dff337, local: 86a2aa42fc76+, remote: 8dbce441892a
+   preserving b for resolve of b
+   preserving rev for resolve of rev
    a: remote is newer -> g
-   b: versions differ -> m
-    preserving b for resolve of b
-   rev: versions differ -> m
-    preserving rev for resolve of rev
   getting a
   updating: a 1/3 files (33.33%)
+   b: versions differ -> m
   updating: b 2/3 files (66.67%)
   picked tool 'python ../merge' for b (binary False symlink False)
   merging b
   my b@86a2aa42fc76+ other b@8dbce441892a ancestor b@000000000000
+   rev: versions differ -> m
   updating: rev 3/3 files (100.00%)
   picked tool 'python ../merge' for rev (binary False symlink False)
   merging rev
@@ -506,17 +506,17 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 924404dff337, local: 59318016310c+, remote: bdb19105162a
+   preserving b for resolve of b
+   preserving rev for resolve of rev
    a: other deleted -> r
-   b: versions differ -> m
-    preserving b for resolve of b
-   rev: versions differ -> m
-    preserving rev for resolve of rev
   removing a
   updating: a 1/3 files (33.33%)
+   b: versions differ -> m
   updating: b 2/3 files (66.67%)
   picked tool 'python ../merge' for b (binary False symlink False)
   merging b
   my b@59318016310c+ other b@bdb19105162a ancestor b@000000000000
+   rev: versions differ -> m
   updating: rev 3/3 files (100.00%)
   picked tool 'python ../merge' for rev (binary False symlink False)
   merging rev
@@ -538,17 +538,17 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 924404dff337, local: 86a2aa42fc76+, remote: 8dbce441892a
+   preserving b for resolve of b
+   preserving rev for resolve of rev
    a: remote is newer -> g
-   b: versions differ -> m
-    preserving b for resolve of b
-   rev: versions differ -> m
-    preserving rev for resolve of rev
   getting a
   updating: a 1/3 files (33.33%)
+   b: versions differ -> m
   updating: b 2/3 files (66.67%)
   picked tool 'python ../merge' for b (binary False symlink False)
   merging b
   my b@86a2aa42fc76+ other b@8dbce441892a ancestor b@000000000000
+   rev: versions differ -> m
   updating: rev 3/3 files (100.00%)
   picked tool 'python ../merge' for rev (binary False symlink False)
   merging rev
@@ -571,15 +571,15 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 924404dff337, local: 0b76e65c8289+, remote: 4ce40f5aca24
+   preserving b for resolve of b
+   preserving rev for resolve of rev
    a: keep -> k
    b: versions differ -> m
-    preserving b for resolve of b
-   rev: versions differ -> m
-    preserving rev for resolve of rev
   updating: b 1/2 files (50.00%)
   picked tool 'python ../merge' for b (binary False symlink False)
   merging b
   my b@0b76e65c8289+ other b@4ce40f5aca24 ancestor b@000000000000
+   rev: versions differ -> m
   updating: rev 2/2 files (100.00%)
   picked tool 'python ../merge' for rev (binary False symlink False)
   merging rev
@@ -604,17 +604,17 @@
    ancestor: 924404dff337, local: 02963e448370+, remote: 8dbce441892a
   remote changed a which local deleted
   use (c)hanged version or leave (d)eleted? c
+   preserving b for resolve of b
+   preserving rev for resolve of rev
    a: prompt recreating -> g
-   b: versions differ -> m
-    preserving b for resolve of b
-   rev: versions differ -> m
-    preserving rev for resolve of rev
   getting a
   updating: a 1/3 files (33.33%)
+   b: versions differ -> m
   updating: b 2/3 files (66.67%)
   picked tool 'python ../merge' for b (binary False symlink False)
   merging b
   my b@02963e448370+ other b@8dbce441892a ancestor b@000000000000
+   rev: versions differ -> m
   updating: rev 3/3 files (100.00%)
   picked tool 'python ../merge' for rev (binary False symlink False)
   merging rev
@@ -639,16 +639,16 @@
    ancestor: 924404dff337, local: 0b76e65c8289+, remote: bdb19105162a
   local changed a which remote deleted
   use (c)hanged version or (d)elete? c
+   preserving b for resolve of b
+   preserving rev for resolve of rev
    a: prompt keep -> a
+  updating: a 1/3 files (33.33%)
    b: versions differ -> m
-    preserving b for resolve of b
-   rev: versions differ -> m
-    preserving rev for resolve of rev
-  updating: a 1/3 files (33.33%)
   updating: b 2/3 files (66.67%)
   picked tool 'python ../merge' for b (binary False symlink False)
   merging b
   my b@0b76e65c8289+ other b@bdb19105162a ancestor b@000000000000
+   rev: versions differ -> m
   updating: rev 3/3 files (100.00%)
   picked tool 'python ../merge' for rev (binary False symlink False)
   merging rev
@@ -674,15 +674,15 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 924404dff337, local: e300d1c794ec+, remote: 49b6d8032493
+   preserving a for resolve of b
+   preserving rev for resolve of rev
+  removing a
    b: remote moved from a -> m
-    preserving a for resolve of b
-   rev: versions differ -> m
-    preserving rev for resolve of rev
-  removing a
   updating: b 1/2 files (50.00%)
   picked tool 'python ../merge' for b (binary False symlink False)
   merging a and b to b
   my b@e300d1c794ec+ other b@49b6d8032493 ancestor a@924404dff337
+   rev: versions differ -> m
   updating: rev 2/2 files (100.00%)
   picked tool 'python ../merge' for rev (binary False symlink False)
   merging rev
@@ -708,14 +708,14 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 924404dff337, local: 62e7bf090eba+, remote: f4db7e329e71
+   preserving b for resolve of b
+   preserving rev for resolve of rev
    b: local copied/moved from a -> m
-    preserving b for resolve of b
-   rev: versions differ -> m
-    preserving rev for resolve of rev
   updating: b 1/2 files (50.00%)
   picked tool 'python ../merge' for b (binary False symlink False)
   merging b and a to b
   my b@62e7bf090eba+ other a@f4db7e329e71 ancestor a@924404dff337
+   rev: versions differ -> m
   updating: rev 2/2 files (100.00%)
   picked tool 'python ../merge' for rev (binary False symlink False)
   merging rev
@@ -746,18 +746,18 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 924404dff337, local: 02963e448370+, remote: 2b958612230f
-   b: local copied/moved from a -> m
-    preserving b for resolve of b
+   preserving b for resolve of b
+   preserving rev for resolve of rev
    c: remote created -> g
-   rev: versions differ -> m
-    preserving rev for resolve of rev
   getting c
   updating: c 1/3 files (33.33%)
+   b: local copied/moved from a -> m
   updating: b 2/3 files (66.67%)
   picked tool 'python ../merge' for b (binary False symlink False)
   merging b and a to b
   my b@02963e448370+ other a@2b958612230f ancestor a@924404dff337
    premerge successful
+   rev: versions differ -> m
   updating: rev 3/3 files (100.00%)
   picked tool 'python ../merge' for rev (binary False symlink False)
   merging rev
@@ -836,28 +836,18 @@
    ancestor: e6cb3cf11019, local: ec44bf929ab5+, remote: c62e34d0b898
   remote changed 8/f which local deleted
   use (c)hanged version or leave (d)eleted? c
-   0/f: versions differ -> m
-    preserving 0/f for resolve of 0/f
-   1/g: versions differ -> m
-    preserving 1/g for resolve of 1/g
-   2/f: versions differ -> m
-    preserving 2/f for resolve of 2/f
-   3/f: versions differ -> m
-    preserving 3/f for resolve of 3/f
-   3/g: remote copied from 3/f -> m
-    preserving 3/f for resolve of 3/g
-   4/g: remote moved from 4/f -> m
-    preserving 4/f for resolve of 4/g
-   5/f: versions differ -> m
-    preserving 5/f for resolve of 5/f
-   5/g: local copied/moved from 5/f -> m
-    preserving 5/g for resolve of 5/g
-   6/g: local copied/moved from 6/f -> m
-    preserving 6/g for resolve of 6/g
-   7/f: remote differs from untracked local -> m
-    preserving 7/f for resolve of 7/f
+   preserving 0/f for resolve of 0/f
+   preserving 1/g for resolve of 1/g
+   preserving 2/f for resolve of 2/f
+   preserving 3/f for resolve of 3/f
+   preserving 3/f for resolve of 3/g
+   preserving 4/f for resolve of 4/g
+   preserving 5/f for resolve of 5/f
+   preserving 5/g for resolve of 5/g
+   preserving 6/g for resolve of 6/g
+   preserving 7/f for resolve of 7/f
+  removing 4/f
    8/f: prompt recreating -> g
-  removing 4/f
   getting 8/f
   $ hg mani
   0/f
--- a/tests/test-resolve.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-resolve.t	Sat Jun 14 11:56:20 2014 -0500
@@ -26,14 +26,31 @@
   use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
   [1]
 
+resolve -l should contain an unresolved entry
+
+  $ hg resolve -l
+  U file
+
+resolving an unknown path emits a warning
+  $ hg resolve -m does-not-exist
+  arguments do not match paths that need resolving
+
+resolve the failure
+
   $ echo resolved > file
   $ hg resolve -m file
+  no more unresolved files
   $ hg commit -m 'resolved'
 
-resolve -l, should be empty
+resolve -l should be empty
 
   $ hg resolve -l
 
+resolve -m should abort since no merge in progress
+  $ hg resolve -m
+  abort: resolve command not applicable when not merging
+  [255]
+
 test crashed merge with empty mergestate
 
   $ mkdir .hg/merge
--- a/tests/test-revert.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-revert.t	Sat Jun 14 11:56:20 2014 -0500
@@ -299,4 +299,86 @@
     removed
   R ignored
 
-  $ cd ..
+Test revert of a file added by one side of the merge
+
+(remove any pending change)
+
+  $ hg revert --all
+  forgetting allyour
+  forgetting base
+  undeleting ignored
+  $ hg purge --all --config extensions.purge=
+
+(Adds a new commit)
+
+  $ echo foo > newadd
+  $ hg add newadd
+  $ hg commit -m 'other adds'
+  created new head
+
+
+(merge it with the other head)
+
+  $ hg merge # merge 1 into 2
+  2 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+  $ hg summary
+  parent: 2:b8ec310b2d4e tip
+   other adds
+  parent: 1:f6180deb8fbe 
+   rename
+  branch: default
+  commit: 2 modified, 1 removed (merge)
+  update: (current)
+
+(clarifies who added what)
+
+  $ hg status
+  M allyour
+  M base
+  R ignored
+  $ hg status --change 'p1()'
+  A newadd
+  $ hg status --change 'p2()'
+  A allyour
+  A base
+  R ignored
+
+(revert file added by p1() to p1() state)
+
+  $ hg revert -r 'p1()' 'glob:newad?'
+  $ hg status
+  M allyour
+  M base
+  R ignored
+
+(revert file added by p1() to p2() state)
+
+  $ hg revert -r 'p2()' 'glob:newad?'
+  removing newadd
+  $ hg status
+  M allyour
+  M base
+  R ignored
+  R newadd
+
+(revert file added by p2() to p2() state)
+
+  $ hg revert -r 'p2()' 'glob:allyou?'
+  $ hg status
+  M allyour
+  M base
+  R ignored
+  R newadd
+
+(revert file added by p2() to p1() state)
+
+  $ hg revert -r 'p1()' 'glob:allyou?'
+  removing allyour
+  $ hg status
+  M base
+  R allyour
+  R ignored
+  R newadd
+
+
--- a/tests/test-rollback.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-rollback.t	Sat Jun 14 11:56:20 2014 -0500
@@ -82,6 +82,7 @@
   0  default  add a again
   $ hg update default
   1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  (leaving bookmark foo)
   $ hg bookmark bar
   $ cat .hg/undo.branch ; echo
   test
--- a/tests/test-run-tests.py	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-run-tests.py	Sat Jun 14 11:56:20 2014 -0500
@@ -29,7 +29,7 @@
     assert expected.endswith('\n') and output.endswith('\n'), 'missing newline'
     assert not re.search(r'[^ \w\\/\r\n()*?]', expected + output), \
            'single backslash or unknown char'
-    match = run_tests.linematch(expected, output)
+    match = run_tests.TTest.linematch(expected, output)
     if isinstance(match, str):
         return 'special: ' + match
     else:
--- a/tests/test-run-tests.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-run-tests.t	Sat Jun 14 11:56:20 2014 -0500
@@ -1,121 +1,205 @@
-Simple commands:
+This file tests the behavior of run-tests.py itself.
+
+Smoke test
+============
+
+  $ $TESTDIR/run-tests.py
+  
+  # Ran 0 tests, 0 skipped, 0 warned, 0 failed.
+
+a succesful test
+=======================
+
+  $ cat > test-success.t << EOF
+  >   $ echo babar
+  >   babar
+  > EOF
+
+  $ $TESTDIR/run-tests.py --with-hg=`which hg`
+  .
+  # Ran 1 tests, 0 skipped, 0 warned, 0 failed.
 
-  $ echo foo
-  foo
-  $ printf 'oh no'
-  oh no (no-eol)
-  $ printf 'bar\nbaz\n' | cat
-  bar
-  baz
+failing test
+==================
+
+  $ cat > test-failure.t << EOF
+  >   $ echo babar
+  >   rataxes
+  > EOF
+
+  $ $TESTDIR/run-tests.py --with-hg=`which hg`
+  
+  --- $TESTTMP/test-failure.t
+  +++ $TESTTMP/test-failure.t.err
+  @@ -1,2 +1,2 @@
+     $ echo babar
+  -  rataxes
+  +  babar
+  
+  ERROR: test-failure.t output changed
+  !.
+  Failed test-failure.t: output changed
+  # Ran 2 tests, 0 skipped, 0 warned, 1 failed.
+  python hash seed: * (glob)
+  [1]
 
-Multi-line command:
+test for --retest
+====================
+
+  $ $TESTDIR/run-tests.py --with-hg=`which hg` --retest
+  
+  --- $TESTTMP/test-failure.t
+  +++ $TESTTMP/test-failure.t.err
+  @@ -1,2 +1,2 @@
+     $ echo babar
+  -  rataxes
+  +  babar
+  
+  ERROR: test-failure.t output changed
+  !
+  Failed test-failure.t: output changed
+  # Ran 1 tests, 1 skipped, 0 warned, 1 failed.
+  python hash seed: * (glob)
+  [1]
+
+Selecting Tests To Run
+======================
 
-  $ foo() {
-  >     echo bar
-  > }
-  $ foo
-  bar
+successful
+
+  $ $TESTDIR/run-tests.py --with-hg=`which hg` test-success.t
+  .
+  # Ran 1 tests, 0 skipped, 0 warned, 0 failed.
+
+failed
 
-Return codes before inline python:
-
-  $ sh -c 'exit 1'
+  $ $TESTDIR/run-tests.py --with-hg=`which hg` test-failure.t
+  
+  --- $TESTTMP/test-failure.t
+  +++ $TESTTMP/test-failure.t.err
+  @@ -1,2 +1,2 @@
+     $ echo babar
+  -  rataxes
+  +  babar
+  
+  ERROR: test-failure.t output changed
+  !
+  Failed test-failure.t: output changed
+  # Ran 1 tests, 0 skipped, 0 warned, 1 failed.
+  python hash seed: * (glob)
   [1]
 
-Doctest commands:
+Running In Debug Mode
+======================
 
-  >>> print 'foo'
-  foo
-  $ echo interleaved
-  interleaved
-  >>> for c in 'xyz':
-  ...     print c
-  x
-  y
-  z
-  >>> print
-  
+  $ $TESTDIR/run-tests.py --with-hg=`which hg` --debug
+  + echo SALT* 0 0 (glob)
+  SALT* 0 0 (glob)
+  + echo babar
+  babar
+  + echo SALT* 2 0 (glob)
+  SALT* 2 0 (glob)
+  .+ echo SALT* 0 0 (glob)
+  SALT* 0 0 (glob)
+  + echo babar
+  babar
+  + echo SALT* 2 0 (glob)
+  SALT* 2 0 (glob)
+  .
+  # Ran 2 tests, 0 skipped, 0 warned, 0 failed.
 
-Regular expressions:
+Parallel runs
+==============
 
-  $ echo foobarbaz
-  foobar.* (re)
-  $ echo barbazquux
-  .*quux.* (re)
+(duplicate the failing test to get predictable output)
+  $ cp test-failure.t test-failure-copy.t
 
-Globs:
-
-  $ printf '* \\foobarbaz {10}\n'
-  \* \\fo?bar* {10} (glob)
-
-Literal match ending in " (re)":
-
-  $ echo 'foo (re)'
-  foo (re)
+  $ $TESTDIR/run-tests.py --with-hg=`which hg` --jobs 2 test-failure*.t
+  
+  --- $TESTTMP/test-failure*.t (glob)
+  +++ $TESTTMP/test-failure*.t.err (glob)
+  @@ -1,2 +1,2 @@
+     $ echo babar
+  -  rataxes
+  +  babar
+  
+  ERROR: test-failure*.t output changed (glob)
+  !
+  --- $TESTTMP/test-failure*.t (glob)
+  +++ $TESTTMP/test-failure*.t.err (glob)
+  @@ -1,2 +1,2 @@
+     $ echo babar
+  -  rataxes
+  +  babar
+  
+  ERROR: test-failure*.t output changed (glob)
+  !
+  Failed test-failure*.t: output changed (glob)
+  Failed test-failure*.t: output changed (glob)
+  # Ran 2 tests, 0 skipped, 0 warned, 2 failed.
+  python hash seed: * (glob)
+  [1]
 
-Windows: \r\n is handled like \n and can be escaped:
+(delete the duplicated test file)
+  $ rm test-failure-copy.t
 
-#if windows
-  $ printf 'crlf\r\ncr\r\tcrlf\r\ncrlf\r\n'
-  crlf
-  cr\r (no-eol) (esc)
-  \tcrlf (esc)
-  crlf\r (esc)
-#endif
-
-Combining esc with other markups - and handling lines ending with \r instead of \n:
 
-  $ printf 'foo/bar\r'
-  fo?/bar\r (no-eol) (glob) (esc)
-#if windows
-  $ printf 'foo\\bar\r'
-  foo/bar\r (no-eol) (glob) (esc)
-#endif
-  $ printf 'foo/bar\rfoo/bar\r'
-  foo.bar\r \(no-eol\) (re) (esc)
-  foo.bar\r \(no-eol\) (re)
+Interactive run
+===============
+
+(backup the failing test)
+  $ cp test-failure.t backup
+
+Refuse the fix
 
-testing hghave
-
-  $ "$TESTDIR/hghave" true
-  $ "$TESTDIR/hghave" false
-  skipped: missing feature: nail clipper
+  $ echo 'n' | $TESTDIR/run-tests.py --with-hg=`which hg` -i
+  
+  --- $TESTTMP/test-failure.t
+  +++ $TESTTMP/test-failure.t.err
+  @@ -1,2 +1,2 @@
+     $ echo babar
+  -  rataxes
+  +  babar
+  
+  ERROR: test-failure.t output changed
+  Accept this change? [n] !.
+  Failed test-failure.t: output changed
+  # Ran 2 tests, 0 skipped, 0 warned, 1 failed.
+  python hash seed: * (glob)
   [1]
-  $ "$TESTDIR/hghave" no-true
-  skipped: system supports yak shaving
-  [1]
-  $ "$TESTDIR/hghave" no-false
 
-Conditional sections based on hghave:
+  $ cat test-failure.t
+    $ echo babar
+    rataxes
+
+Accept the fix
 
-#if true
-  $ echo tested
-  tested
-#else
-  $ echo skipped
-#endif
-
-#if false
-  $ echo skipped
-#else
-  $ echo tested
-  tested
-#endif
+  $ echo 'y' | $TESTDIR/run-tests.py --with-hg=`which hg` -i
+  
+  --- $TESTTMP/test-failure.t
+  +++ $TESTTMP/test-failure.t.err
+  @@ -1,2 +1,2 @@
+     $ echo babar
+  -  rataxes
+  +  babar
+  
+  ERROR: test-failure.t output changed
+  Accept this change? [n] ..
+  # Ran 2 tests, 0 skipped, 0 warned, 0 failed.
 
-#if no-false
-  $ echo tested
-  tested
-#else
-  $ echo skipped
-#endif
+  $ cat test-failure.t
+    $ echo babar
+    babar
+
+(reinstall)
+  $ mv backup test-failure.t
 
-#if no-true
-  $ echo skipped
-#else
-  $ echo tested
-  tested
-#endif
+No Diff
+===============
 
-Exit code:
-
-  $ (exit 1)
+  $ $TESTDIR/run-tests.py --with-hg=`which hg` --nodiff
+  !.
+  Failed test-failure.t: output changed
+  # Ran 2 tests, 0 skipped, 0 warned, 1 failed.
+  python hash seed: * (glob)
   [1]
--- a/tests/test-shelve.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-shelve.t	Sat Jun 14 11:56:20 2014 -0500
@@ -90,6 +90,10 @@
    a
   +a
 
+  $ hg shelve --list --addremove
+  abort: options '--list' and '--addremove' may not be used together
+  [255]
+
 delete our older shelved change
 
   $ hg shelve -d default
@@ -210,11 +214,11 @@
   +++ b/a/a
   @@ -1,2 +1,6 @@
    a
-  +<<<<<<< local
+  +<<<<<<< dest:   *  - shelve: pending changes temporary commit (glob)
    c
   +=======
   +a
-  +>>>>>>> other
+  +>>>>>>> source: 4702e8911fe0 - shelve: changes to '[mq]: second.patch'
   diff --git a/b.rename/b b/b.rename/b
   new file mode 100644
   --- /dev/null
@@ -292,6 +296,7 @@
 
   $ hg revert -r . a/a
   $ hg resolve -m a/a
+  no more unresolved files
 
   $ hg commit -m 'commit while unshelve in progress'
   abort: unshelve already in progress
@@ -394,6 +399,16 @@
   $ hg shelve --cleanup
   $ hg shelve --list
 
+  $ hg shelve --cleanup --delete
+  abort: options '--cleanup' and '--delete' may not be used together
+  [255]
+  $ hg shelve --cleanup --patch
+  abort: options '--cleanup' and '--patch' may not be used together
+  [255]
+  $ hg shelve --cleanup --message MESSAGE
+  abort: options '--cleanup' and '--message' may not be used together
+  [255]
+
 test bookmarks
 
   $ hg bookmark test
@@ -601,11 +616,11 @@
   M f
   ? f.orig
   $ cat f
-  <<<<<<< local
+  <<<<<<< dest:   5f6b880e719b  - shelve: pending changes temporary commit
   g
   =======
   f
-  >>>>>>> other
+  >>>>>>> source: 23b29cada8ba - shelve: changes to 'commit stuff'
   $ cat f.orig
   g
   $ hg unshelve --abort
@@ -644,11 +659,11 @@
   M f
   ? f.orig
   $ cat f
-  <<<<<<< local
+  <<<<<<< dest:   *  - test: intermediate other change (glob)
   g
   =======
   f
-  >>>>>>> other
+  >>>>>>> source: 23b29cada8ba - shelve: changes to 'commit stuff'
   $ cat f.orig
   g
   $ hg unshelve --abort
@@ -663,4 +678,11 @@
   g
   $ hg shelve --delete default
 
+  $ hg shelve --delete --stat
+  abort: options '--delete' and '--stat' may not be used together
+  [255]
+  $ hg shelve --delete --name NAME
+  abort: options '--delete' and '--name' may not be used together
+  [255]
+
   $ cd ..
--- a/tests/test-strip.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-strip.t	Sat Jun 14 11:56:20 2014 -0500
@@ -490,6 +490,7 @@
   $ hg bookmark -r 'c' 'delete'
   $ hg up -C todelete
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (activating bookmark todelete)
   $ hg strip -B nostrip
   bookmark 'nostrip' deleted
   abort: empty revision set
--- a/tests/test-subrepo-git.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-subrepo-git.t	Sat Jun 14 11:56:20 2014 -0500
@@ -155,7 +155,7 @@
   added 1 changesets with 1 changes to 1 files (+1 heads)
   (run 'hg heads' to see heads, 'hg merge' to merge)
   $ hg merge 2>/dev/null
-   subrepository s diverged (local revision: 796959400868, remote revision: aa84837ccfbd)
+   subrepository s diverged (local revision: 7969594, remote revision: aa84837)
   (M)erge, keep (l)ocal or keep (r)emote? m
   pulling subrepo s from $TESTTMP/gitroot
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
@@ -464,7 +464,7 @@
   da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
   $ cd ..
   $ hg update 4
-   subrepository s diverged (local revision: da5f5b1d8ffc, remote revision: aa84837ccfbd)
+   subrepository s diverged (local revision: da5f5b1, remote revision: aa84837)
   (M)erge, keep (l)ocal or keep (r)emote? m
    subrepository sources for s differ
   use (l)ocal source (da5f5b1) or (r)emote source (aa84837)?
@@ -491,7 +491,7 @@
   HEAD is now at aa84837... f
   $ cd ..
   $ hg update 1
-   subrepository s diverged (local revision: 32a343883b74, remote revision: da5f5b1d8ffc)
+   subrepository s diverged (local revision: 32a3438, remote revision: da5f5b1)
   (M)erge, keep (l)ocal or keep (r)emote? m
    subrepository sources for s differ (in checked out version)
   use (l)ocal source (32a3438) or (r)emote source (da5f5b1)?
@@ -514,7 +514,7 @@
   $ hg id -n
   1+
   $ hg update 7
-   subrepository s diverged (local revision: 32a343883b74, remote revision: 32a343883b74)
+   subrepository s diverged (local revision: 32a3438, remote revision: 32a3438)
   (M)erge, keep (l)ocal or keep (r)emote? m
    subrepository sources for s differ
   use (l)ocal source (32a3438) or (r)emote source (32a3438)?
--- a/tests/test-subrepo-svn.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-subrepo-svn.t	Sat Jun 14 11:56:20 2014 -0500
@@ -470,6 +470,7 @@
   $ hg book other
   $ hg co -r 'p1(tip)'
   2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (leaving bookmark other)
   $ echo "obstruct =        [svn]       $SVNREPOURL/src" >> .hgsub
   $ svn co -r5 --quiet "$SVNREPOURL"/src obstruct
   $ hg commit -m 'Other branch which will be obstructed'
@@ -481,6 +482,7 @@
   A    *obstruct/other (glob)
   Checked out revision 1.
   2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (activating bookmark other)
 
 This is surprising, but is also correct based on the current code:
   $ echo "updating should (maybe) fail" > obstruct/other
@@ -543,6 +545,7 @@
   A    *recreated/somethingold (glob)
   Checked out revision 10.
   1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (leaving bookmark other)
   $ test -f recreated/somethingold
 
 Test archive
--- a/tests/test-subrepo.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-subrepo.t	Sat Jun 14 11:56:20 2014 -0500
@@ -281,8 +281,8 @@
   resolving manifests
    branchmerge: True, force: False, partial: False
    ancestor: 6747d179aa9a, local: 20a0db6fbf6c+, remote: 7af322bc1198
+   preserving t for resolve of t
    t: versions differ -> m
-    preserving t for resolve of t
   updating: t 1/1 files (100.00%)
   picked tool 'internal:merge' for t (binary False symlink False)
   merging t
@@ -298,11 +298,11 @@
 should conflict
 
   $ cat t/t
-  <<<<<<< local
+  <<<<<<< local: 20a0db6fbf6c - test: 10
   conflict
   =======
   t3
-  >>>>>>> other
+  >>>>>>> other: 7af322bc1198  - test: 7
 
 clone
 
--- a/tests/test-tag.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-tag.t	Sat Jun 14 11:56:20 2014 -0500
@@ -16,7 +16,10 @@
   abort: tag names cannot consist entirely of whitespace
   [255]
 
-  $ hg tag "bleah"
+(this tests also that editor is not invoked, if '--edit' is not
+specified)
+
+  $ HGEDITOR=cat hg tag "bleah"
   $ hg history
   changeset:   1:d4f0d2909abc
   tag:         tip
@@ -219,14 +222,20 @@
 test custom commit messages
 
   $ cat > editor.sh << '__EOF__'
+  > echo "==== before editing"
+  > cat "$1"
+  > echo "===="
   > echo "custom tag message" > "$1"
   > echo "second line" >> "$1"
   > __EOF__
 
 at first, test saving last-message.txt
 
+(test that editor is not invoked before transaction starting)
+
   $ cat > .hg/hgrc << '__EOF__'
   > [hooks]
+  > # this failure occurs before editor invocation
   > pretag.test-saving-lastmessage = false
   > __EOF__
   $ rm -f .hg/last-message.txt
@@ -234,16 +243,66 @@
   abort: pretag.test-saving-lastmessage hook exited with status 1
   [255]
   $ cat .hg/last-message.txt
+  cat: .hg/last-message.txt: No such file or directory
+  [1]
+
+(test that editor is invoked and commit message is saved into
+"last-message.txt")
+
+  $ cat >> .hg/hgrc << '__EOF__'
+  > [hooks]
+  > pretag.test-saving-lastmessage =
+  > # this failure occurs after editor invocation
+  > pretxncommit.unexpectedabort = false
+  > __EOF__
+
+(this tests also that editor is invoked, if '--edit' is specified,
+regardless of '--message')
+
+  $ rm -f .hg/last-message.txt
+  $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg tag custom-tag -e -m "foo bar"
+  ==== before editing
+  foo bar
+  
+  
+  HG: Enter commit message.  Lines beginning with 'HG:' are removed.
+  HG: Leave message empty to abort commit.
+  HG: --
+  HG: user: test
+  HG: branch 'tag-and-branch-same-name'
+  HG: changed .hgtags
+  ====
+  transaction abort!
+  rollback completed
+  note: commit message saved in .hg/last-message.txt
+  abort: pretxncommit.unexpectedabort hook exited with status 1
+  [255]
+  $ cat .hg/last-message.txt
   custom tag message
   second line
-  $ cat > .hg/hgrc << '__EOF__'
+
+  $ cat >> .hg/hgrc << '__EOF__'
   > [hooks]
-  > pretag.test-saving-lastmessage =
+  > pretxncommit.unexpectedabort =
   > __EOF__
+  $ hg status .hgtags
+  M .hgtags
+  $ hg revert --no-backup -q .hgtags
 
 then, test custom commit message itself
 
   $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg tag custom-tag -e
+  ==== before editing
+  Added tag custom-tag for changeset 75a534207be6
+  
+  
+  HG: Enter commit message.  Lines beginning with 'HG:' are removed.
+  HG: Leave message empty to abort commit.
+  HG: --
+  HG: user: test
+  HG: branch 'tag-and-branch-same-name'
+  HG: changed .hgtags
+  ====
   $ hg log -l1 --template "{desc}\n"
   custom tag message
   second line
--- a/tests/test-transplant.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-transplant.t	Sat Jun 14 11:56:20 2014 -0500
@@ -43,8 +43,9 @@
   1 files updated, 0 files merged, 3 files removed, 0 files unresolved
 
 rebase b onto r1
+(this also tests that editor is not invoked if '--edit' is not specified)
 
-  $ hg transplant -a -b tip
+  $ HGEDITOR=cat hg transplant -a -b tip
   applying 37a1297eb21b
   37a1297eb21b transplanted to e234d668f844
   applying 722f4667af76
@@ -85,13 +86,26 @@
 
 test destination() revset predicate with a transplant of a transplant; new
 clone so subsequent rollback isn't affected
+(this also tests that editor is invoked if '--edit' is specified)
+
   $ hg clone -q . ../destination
   $ cd ../destination
   $ hg up -Cq 0
   $ hg branch -q b4
   $ hg ci -qm "b4"
-  $ hg transplant 7
+  $ hg status --rev "7^1" --rev 7
+  A b3
+  $ HGEDITOR=cat hg transplant --edit 7
   applying ffd6818a3975
+  b3
+  
+  
+  HG: Enter commit message.  Lines beginning with 'HG:' are removed.
+  HG: Leave message empty to abort commit.
+  HG: --
+  HG: user: test
+  HG: branch 'b4'
+  HG: added b3
   ffd6818a3975 transplanted to 502236fa76bb
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-unified-test.t	Sat Jun 14 11:56:20 2014 -0500
@@ -0,0 +1,124 @@
+Test that the syntax of "unified tests" is properly processed
+==============================================================
+
+Simple commands:
+
+  $ echo foo
+  foo
+  $ printf 'oh no'
+  oh no (no-eol)
+  $ printf 'bar\nbaz\n' | cat
+  bar
+  baz
+
+Multi-line command:
+
+  $ foo() {
+  >     echo bar
+  > }
+  $ foo
+  bar
+
+Return codes before inline python:
+
+  $ sh -c 'exit 1'
+  [1]
+
+Doctest commands:
+
+  >>> print 'foo'
+  foo
+  $ echo interleaved
+  interleaved
+  >>> for c in 'xyz':
+  ...     print c
+  x
+  y
+  z
+  >>> print
+  
+
+Regular expressions:
+
+  $ echo foobarbaz
+  foobar.* (re)
+  $ echo barbazquux
+  .*quux.* (re)
+
+Globs:
+
+  $ printf '* \\foobarbaz {10}\n'
+  \* \\fo?bar* {10} (glob)
+
+Literal match ending in " (re)":
+
+  $ echo 'foo (re)'
+  foo (re)
+
+Windows: \r\n is handled like \n and can be escaped:
+
+#if windows
+  $ printf 'crlf\r\ncr\r\tcrlf\r\ncrlf\r\n'
+  crlf
+  cr\r (no-eol) (esc)
+  \tcrlf (esc)
+  crlf\r (esc)
+#endif
+
+Combining esc with other markups - and handling lines ending with \r instead of \n:
+
+  $ printf 'foo/bar\r'
+  fo?/bar\r (no-eol) (glob) (esc)
+#if windows
+  $ printf 'foo\\bar\r'
+  foo/bar\r (no-eol) (glob) (esc)
+#endif
+  $ printf 'foo/bar\rfoo/bar\r'
+  foo.bar\r \(no-eol\) (re) (esc)
+  foo.bar\r \(no-eol\) (re)
+
+testing hghave
+
+  $ "$TESTDIR/hghave" true
+  $ "$TESTDIR/hghave" false
+  skipped: missing feature: nail clipper
+  [1]
+  $ "$TESTDIR/hghave" no-true
+  skipped: system supports yak shaving
+  [1]
+  $ "$TESTDIR/hghave" no-false
+
+Conditional sections based on hghave:
+
+#if true
+  $ echo tested
+  tested
+#else
+  $ echo skipped
+#endif
+
+#if false
+  $ echo skipped
+#else
+  $ echo tested
+  tested
+#endif
+
+#if no-false
+  $ echo tested
+  tested
+#else
+  $ echo skipped
+#endif
+
+#if no-true
+  $ echo skipped
+#else
+  $ echo tested
+  tested
+#endif
+
+Exit code:
+
+  $ (exit 1)
+  [1]
--- a/tests/test-up-local-change.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-up-local-change.t	Sat Jun 14 11:56:20 2014 -0500
@@ -46,11 +46,11 @@
   resolving manifests
    branchmerge: False, force: False, partial: False
    ancestor: c19d34741b0a, local: c19d34741b0a+, remote: 1e71731e6fbb
-   a: versions differ -> m
-    preserving a for resolve of a
+   preserving a for resolve of a
    b: remote created -> g
   getting b
   updating: b 1/2 files (50.00%)
+   a: versions differ -> m
   updating: a 2/2 files (100.00%)
   picked tool 'true' for a (binary False symlink False)
   merging a
@@ -67,11 +67,11 @@
   resolving manifests
    branchmerge: False, force: False, partial: False
    ancestor: 1e71731e6fbb, local: 1e71731e6fbb+, remote: c19d34741b0a
+   preserving a for resolve of a
    b: other deleted -> r
-   a: versions differ -> m
-    preserving a for resolve of a
   removing b
   updating: b 1/2 files (50.00%)
+   a: versions differ -> m
   updating: a 2/2 files (100.00%)
   picked tool 'true' for a (binary False symlink False)
   merging a
@@ -100,11 +100,11 @@
   resolving manifests
    branchmerge: False, force: False, partial: False
    ancestor: c19d34741b0a, local: c19d34741b0a+, remote: 1e71731e6fbb
-   a: versions differ -> m
-    preserving a for resolve of a
+   preserving a for resolve of a
    b: remote created -> g
   getting b
   updating: b 1/2 files (50.00%)
+   a: versions differ -> m
   updating: a 2/2 files (100.00%)
   picked tool 'true' for a (binary False symlink False)
   merging a
@@ -181,14 +181,14 @@
   resolving manifests
    branchmerge: True, force: True, partial: False
    ancestor: c19d34741b0a, local: 1e71731e6fbb+, remote: 83c51d0caff4
+   preserving a for resolve of a
+   preserving b for resolve of b
    a: versions differ -> m
-    preserving a for resolve of a
-   b: versions differ -> m
-    preserving b for resolve of b
   updating: a 1/2 files (50.00%)
   picked tool 'true' for a (binary False symlink False)
   merging a
   my a@1e71731e6fbb+ other a@83c51d0caff4 ancestor a@c19d34741b0a
+   b: versions differ -> m
   updating: b 2/2 files (100.00%)
   picked tool 'true' for b (binary False symlink False)
   merging b
--- a/tests/test-update-reverse.t	Sat Jun 14 11:49:02 2014 -0500
+++ b/tests/test-update-reverse.t	Sat Jun 14 11:56:20 2014 -0500
@@ -69,11 +69,11 @@
    branchmerge: False, force: True, partial: False
    ancestor: 91ebc10ed028+, local: 91ebc10ed028+, remote: 71a760306caf
    side1: other deleted -> r
+  removing side1
    side2: other deleted -> r
-   main: remote created -> g
-  removing side1
   removing side2
   updating: side2 2/3 files (66.67%)
+   main: remote created -> g
   getting main
   updating: main 3/3 files (100.00%)
   1 files updated, 0 files merged, 2 files removed, 0 files unresolved