changeset 21550:b4f0e15d1dab

Merge with stable
author Augie Fackler <raf@durin42.com>
date Mon, 26 May 2014 12:39:31 -0400
parents ea3d75ebea6d (diff) 565d45919db8 (current diff)
children bde505f47141
files tests/test-bundle2.t
diffstat 128 files changed, 4957 insertions(+), 2959 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile	Thu May 15 23:53:21 2014 -0700
+++ b/Makefile	Mon May 26 12:39:31 2014 -0400
@@ -132,6 +132,29 @@
 	msgmerge --no-location --update $@.tmp $^
 	mv -f $@.tmp $@
 
+# Packaging targets
+
+fedora:
+	mkdir -p build/fedora
+	echo y | contrib/buildrpm
+	cp rpmbuild/RPMS/*/* build/fedora
+	cp rpmbuild/SRPMS/* build/fedora
+	rm -rf rpmbuild
+
+docker-fedora:
+	mkdir -p build/fedora
+	contrib/dockerrpm fedora
+
+centos6:
+	mkdir -p build/centos6
+	echo y | contrib/buildrpm
+	cp rpmbuild/RPMS/*/* build/centos6
+	cp rpmbuild/SRPMS/* build/centos6
+
+docker-centos6:
+	mkdir -p build/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/check-code.py	Thu May 15 23:53:21 2014 -0700
+++ b/contrib/check-code.py	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/contrib/debugshell.py	Mon May 26 12:39:31 2014 -0400
@@ -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	Mon May 26 12:39:31 2014 -0400
@@ -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	Mon May 26 12:39:31 2014 -0400
@@ -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	Mon May 26 12:39:31 2014 -0400
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+BUILDDIR=$(dirname $0)
+ROOTDIR=$(cd $BUILDDIR/..; pwd)
+
+if which docker >> /dev/null ; then
+  DOCKER=docker
+elif which docker.io >> /dev/null ; then
+  DOCKER=docker.io
+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 build/$1/* /hg/build/$1/"
--- a/contrib/revsetbenchmarks.py	Thu May 15 23:53:21 2014 -0700
+++ b/contrib/revsetbenchmarks.py	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/contrib/revsetbenchmarks.txt	Mon May 26 12:39:31 2014 -0400
@@ -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/hgext/children.py	Thu May 15 23:53:21 2014 -0700
+++ b/hgext/children.py	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/hgext/churn.py	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/hgext/color.py	Mon May 26 12:39:31 2014 -0400
@@ -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
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/hgext/convert/__init__.py	Mon May 26 12:39:31 2014 -0400
@@ -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/hg.py	Thu May 15 23:53:21 2014 -0700
+++ b/hgext/convert/hg.py	Mon May 26 12:39:31 2014 -0400
@@ -394,7 +394,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/extdiff.py	Thu May 15 23:53:21 2014 -0700
+++ b/hgext/extdiff.py	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/hgext/factotum.py	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/hgext/fetch.py	Mon May 26 12:39:31 2014 -0400
@@ -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/hgk.py	Thu May 15 23:53:21 2014 -0700
+++ b/hgext/hgk.py	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/hgext/histedit.py	Mon May 26 12:39:31 2014 -0400
@@ -298,9 +298,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 +401,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 +680,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	Thu May 15 23:53:21 2014 -0700
+++ b/hgext/largefiles/lfcommands.py	Mon May 26 12:39:31 2014 -0400
@@ -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
 
@@ -519,6 +531,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 +569,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	Thu May 15 23:53:21 2014 -0700
+++ b/hgext/largefiles/overrides.py	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/hgext/mq.py	Mon May 26 12:39:31 2014 -0400
@@ -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()
@@ -2469,16 +2490,8 @@
     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:
@@ -2564,7 +2577,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 +2585,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	Thu May 15 23:53:21 2014 -0700
+++ b/hgext/pager.py	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/hgext/patchbomb.py	Mon May 26 12:39:31 2014 -0400
@@ -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 = [
--- a/hgext/rebase.py	Thu May 15 23:53:21 2014 -0700
+++ b/hgext/rebase.py	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/hgext/record.py	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/hgext/relink.py	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/hgext/share.py	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/hgext/transplant.py	Mon May 26 12:39:31 2014 -0400
@@ -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/changegroup.py	Thu May 15 23:53:21 2014 -0700
+++ b/mercurial/changegroup.py	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/mercurial/cmdutil.py	Mon May 26 12:39:31 2014 -0400
@@ -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,9 +586,7 @@
     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)
@@ -653,8 +675,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()
@@ -2045,12 +2066,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 +2081,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 +2147,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 +2156,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 +2182,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"))
 
--- a/mercurial/commands.py	Thu May 15 23:53:21 2014 -0700
+++ b/mercurial/commands.py	Mon May 26 12:39:31 2014 -0400
@@ -487,13 +487,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()
+            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)
@@ -1346,8 +1345,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'))
@@ -1409,10 +1406,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'):
@@ -1422,7 +1415,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,
@@ -3076,9 +3071,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']:
@@ -3185,7 +3178,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
@@ -4927,43 +4921,62 @@
                            'use --all to remerge all files'))
 
     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 resolved\n"))
+
+    # 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',
@@ -5683,16 +5696,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)
 
@@ -5878,7 +5890,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	Thu May 15 23:53:21 2014 -0700
+++ b/mercurial/context.py	Mon May 26 12:39:31 2014 -0400
@@ -63,6 +63,71 @@
         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.
+        """
+        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)
@@ -208,9 +273,7 @@
     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):
@@ -933,22 +996,6 @@
     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 user(self):
         return self._user or self._repo.ui.username()
     def date(self):
@@ -1180,6 +1227,178 @@
             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.
+        """
+        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])
+        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, ignored=False, clean=False, unknown=False, match=None):
+        """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."""
+        listignored, listclean, listunknown = ignored, clean, unknown
+        s = self._dirstatestatus(match=match, ignored=listignored,
+                                 clean=listclean, unknown=listunknown)
+        modified, added, removed, deleted, unknown, ignored, clean = s
+
+        modified = self._filtersuspectsymlink(modified)
+
+        self._unknown = self._ignored = self._clean = None
+        if listunknown:
+            self._unknown = unknown
+        if listignored:
+            self._ignored = ignored
+        if listclean:
+            self._clean = clean
+        self._status = modified, added, removed, deleted
+
+        return modified, added, removed, deleted, unknown, ignored, clean
+
+
 class committablefilectx(basefilectx):
     """A committablefilectx provides common functionality for a file context
     that wants the ability to commit, e.g. workingfilectx or memfilectx."""
@@ -1287,7 +1506,7 @@
     is a dictionary of metadata or is left empty.
     """
     def __init__(self, repo, parents, text, files, filectxfn, user=None,
-                 date=None, extra=None):
+                 date=None, extra=None, editor=False):
         self._repo = repo
         self._rev = None
         self._node = None
@@ -1305,6 +1524,10 @@
         if self._extra.get('branch', '') == '':
             self._extra['branch'] = 'default'
 
+        if editor:
+            self._text = editor(self._repo, self, [])
+            self._repo.savecommitmessage(self._text)
+
     def __str__(self):
         return str(self._parents[0]) + "+"
 
--- a/mercurial/demandimport.py	Thu May 15 23:53:21 2014 -0700
+++ b/mercurial/demandimport.py	Mon May 26 12:39:31 2014 -0400
@@ -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/exchange.py	Thu May 15 23:53:21 2014 -0700
+++ b/mercurial/exchange.py	Mon May 26 12:39:31 2014 -0400
@@ -537,7 +537,7 @@
     lock = pullop.repo.lock()
     try:
         _pulldiscovery(pullop)
-        if (pullop.repo.ui.configbool('server', 'bundle2', False)
+        if (pullop.repo.ui.configbool('experimental', 'bundle2-exp', False)
             and pullop.remote.capable('bundle2-exp')):
             _pullbundle2(pullop)
         if 'changegroup' in pullop.todosteps:
@@ -573,12 +573,13 @@
     kwargs['bundlecaps'].add('bundle2=' + urllib.quote(capsblob))
     # pulling changegroup
     pullop.todosteps.remove('changegroup')
+
+    kwargs['common'] = pullop.common
+    kwargs['heads'] = pullop.heads or pullop.rheads
     if not pullop.fetch:
-            pullop.repo.ui.status(_("no changes found\n"))
-            pullop.cgresult = 0
+        pullop.repo.ui.status(_("no changes found\n"))
+        pullop.cgresult = 0
     else:
-        kwargs['common'] = pullop.common
-        kwargs['heads'] = pullop.heads or pullop.rheads
         if pullop.heads is None and list(pullop.common) == [nullid]:
             pullop.repo.ui.status(_("requesting all changes\n"))
     _pullbundle2extraprepare(pullop, kwargs)
@@ -589,8 +590,10 @@
         op = bundle2.processbundle(pullop.repo, bundle, pullop.gettransaction)
     except bundle2.UnknownPartError, exc:
         raise util.Abort('missing support for %s' % exc)
-    assert len(op.records['changegroup']) == 1
-    pullop.cgresult = op.records['changegroup'][0]['return']
+
+    if pullop.fetch:
+        assert len(op.records['changegroup']) == 1
+        pullop.cgresult = op.records['changegroup'][0]['return']
 
 def _pullbundle2extraprepare(pullop, kwargs):
     """hook function so that extensions can extend the getbundle call"""
@@ -684,7 +687,7 @@
     The implementation is at a very early stage and will get massive rework
     when the API of bundle is refined.
     """
-    # build bundle here.
+    # build changegroup bundle here.
     cg = changegroup.getbundle(repo, source, heads=heads,
                                common=common, bundlecaps=bundlecaps)
     if bundlecaps is None or 'HG2X' not in bundlecaps:
@@ -697,10 +700,11 @@
             blob = urllib.unquote(bcaps[len('bundle2='):])
             b2caps.update(bundle2.decodecaps(blob))
     bundler = bundle2.bundle20(repo.ui, b2caps)
-    part = bundle2.bundlepart('b2x:changegroup', data=cg.getchunks())
-    bundler.addpart(part)
-    _getbundleextrapart(bundler, repo, source, heads=None, common=None,
-                        bundlecaps=None, **kwargs)
+    if cg:
+        part = bundle2.bundlepart('b2x:changegroup', data=cg.getchunks())
+        bundler.addpart(part)
+    _getbundleextrapart(bundler, repo, source, heads=heads, common=common,
+                        bundlecaps=bundlecaps, **kwargs)
     return util.chunkbuffer(bundler.getchunks())
 
 def _getbundleextrapart(bundler, repo, source, heads=None, common=None,
--- a/mercurial/filemerge.py	Thu May 15 23:53:21 2014 -0700
+++ b/mercurial/filemerge.py	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/mercurial/help.py	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/mercurial/help/config.txt	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/mercurial/hg.py	Mon May 26 12:39:31 2014 -0400
@@ -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/localrepo.py	Thu May 15 23:53:21 2014 -0700
+++ b/mercurial/localrepo.py	Mon May 26 12:39:31 2014 -0400
@@ -479,7 +479,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 +540,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 +576,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 +857,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(tr)
@@ -1508,121 +1511,48 @@
         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]
 
+        # 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, context.changectx)
+            and isinstance(ctx2, context.changectx)):
+            reversed = True
+            ctx1, ctx2 = ctx2, ctx1
+
         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
+        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 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 reversed:
+            # since we are maintaining whether we reversed ctx1 and ctx2 (due
+            # to comparing the workingctx with its parent), we need to switch
+            # back added files (r[1]) and removed files (r[2])
+            r[1], r[2] = r[2], r[1]
 
         if listsubrepos:
             for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
--- a/mercurial/merge.py	Thu May 15 23:53:21 2014 -0700
+++ b/mercurial/merge.py	Mon May 26 12:39:31 2014 -0400
@@ -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,32 +549,25 @@
         # 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':
+    for f, args, msg in actions:
+        repo.ui.debug(" %s: %s -> r\n" % (f, msg))
+        if True:
             if verbose:
                 repo.ui.note(_("removing %s\n") % f)
             audit(f)
@@ -579,10 +576,6 @@
             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])
         if i == 100:
             yield i, f
             i = 0
@@ -590,7 +583,31 @@
     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 True:
+            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,17 +621,16 @@
     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
+    for f, args, msg in actions['m']:
+        if True:
             f1, f2, fa, move, anc = args
             if f == '.hgsubstate': # merged internally
                 continue
-            repo.ui.debug("  preserving %s for resolve of %s\n" % (f1, f))
+            repo.ui.debug(" preserving %s for resolve of %s\n" % (f1, f))
             fcl = wctx[f1]
             fco = mctx[f2]
             actx = repo[anc]
@@ -627,6 +643,9 @@
                 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,50 +654,60 @@
             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
+        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'))
-    prog = worker.worker(repo.ui, 0.001, getremove, (repo, mctx, overwrite),
-                         updateactions)
-    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
+    if True:
+
+        # 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)
 
-    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
+        # 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
+
+        # 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)
+            r = ms.resolve(f, wctx, labels=labels)
             if r is not None and r > 0:
                 unresolved += 1
             else:
@@ -686,35 +715,61 @@
                     updated += 1
                 else:
                     merged += 1
-        elif m == "dm": # directory rename, move local
+
+        # 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
-        elif m == "dg": # local directory rename, get
+
+        # 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
-        elif m == "dr": # divergent renames
+
+        # 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)
-        elif m == "rd": # rename and delete
+
+        # 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)
-        elif m == "e": # exec
+
+        # 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,119 +790,127 @@
             (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)
-        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 True:
+        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):
-                newactions.append((f, "r", None, "prompt delete"))
+                actions['r'].append((f, None, "prompt delete"))
             else:
-                newactions.append((f, "a", None, "prompt keep"))
-        elif m == "dc":
+                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:
-                newactions.append((f, "g", (flags,), "prompt recreating"))
-        else: assert False, m
+                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 True:
+        # remove (must come first)
+        for f, args, msg in actions['r']:
             if branchmerge:
                 repo.dirstate.remove(f)
             else:
                 repo.dirstate.drop(f)
-        elif m == "a": # re-add
+
+        # 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)
-        elif m == "f": # forget
-            repo.dirstate.drop(f)
-        elif m == "e": # exec change
+
+        # exec change
+        for f, args, msg in actions['e']:
             repo.dirstate.normallookup(f)
-        elif m == "k": # keep
+
+        # keep
+        for f, args, msg in actions['k']:
             pass
-        elif m == "g": # get
+
+        # get
+        for f, args, msg in actions['g']:
             if branchmerge:
                 repo.dirstate.otherparent(f)
             else:
                 repo.dirstate.normal(f)
-        elif m == "m": # merge
+
+        # 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
@@ -870,7 +933,9 @@
                     repo.dirstate.normallookup(f)
                 if move:
                     repo.dirstate.drop(f1)
-        elif m == "dm": # directory rename, move local
+
+        # directory rename, move local
+        for f, args, msg in actions['dm']:
             f0, flag = args
             if f0 not in repo.dirstate:
                 # untracked file moved
@@ -882,7 +947,9 @@
             else:
                 repo.dirstate.normal(f)
                 repo.dirstate.drop(f0)
-        elif m == "dg": # directory rename, get
+
+        # directory rename, get
+        for f, args, msg in actions['dg']:
             f0, flag = args
             if branchmerge:
                 repo.dirstate.add(f)
@@ -891,7 +958,7 @@
                 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 +1138,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/py3kcompat.py	Thu May 15 23:53:21 2014 -0700
+++ b/mercurial/py3kcompat.py	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/mercurial/revset.py	Mon May 26 12:39:31 2014 -0400
@@ -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/simplemerge.py	Thu May 15 23:53:21 2014 -0700
+++ b/mercurial/simplemerge.py	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/mercurial/subrepo.py	Mon May 26 12:39:31 2014 -0400
@@ -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
@@ -501,6 +502,9 @@
             % (substate[0], substate[2]))
         return []
 
+    def shortid(self, revid):
+        return revid
+
 class hgsubrepo(abstractsubrepo):
     def __init__(self, ctx, path, state):
         self._path = path
@@ -866,6 +870,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
@@ -1561,6 +1568,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	Thu May 15 23:53:21 2014 -0700
+++ b/mercurial/templates/atom/changelogentry.tmpl	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/mercurial/templates/rss/changelogentry.tmpl	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/mercurial/templates/rss/filelogentry.tmpl	Mon May 26 12:39:31 2014 -0400
@@ -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/tests/autodiff.py	Thu May 15 23:53:21 2014 -0700
+++ b/tests/autodiff.py	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/filterpyflakes.py	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/run-tests.py	Mon May 26 12:39:31 2014 -0400
@@ -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,30 @@
     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)
+        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 +315,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 +327,408 @@
     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.
+                result.addFailure(self, str(e))
+            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:
+            # 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'
 
-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)
+            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()
+
+            self.fail(msg)
+        elif ret:
+            self.fail(describe(ret))
 
-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
+    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)
+
+        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()
 
-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 += '[/\\\\]'
+        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 +736,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 +1029,757 @@
     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()
 
-    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
+    def addError(self, *args, **kwargs):
+        super(TestResult, self).addError(*args, **kwargs)
+
+        if self._options.first:
+            self.stop()
+
+    # Polyfill.
+    def addSkip(self, test, reason):
+        self.skipped.append((test, reason))
+
+        if self.showAll:
+            self.stream.writeln('skipped %s' % reason)
+        else:
+            self.stream.write('s')
+            self.stream.flush()
+
+    def addIgnore(self, test, reason):
+        self.ignored.append((test, reason))
 
-    def success():
-        return '.', test, ''
+        if self.showAll:
+            self.stream.writeln('ignored %s' % reason)
+        else:
+            self.stream.write('i')
+            self.stream.flush()
 
-    def ignore(msg):
-        return 'i', test, msg
+    def addWarn(self, test, reason):
+        self.warned.append((test, reason))
 
-    def describe(ret):
-        if ret < 0:
-            return 'killed by signal %d' % -ret
-        return 'returned error code %d' % ret
+        if self._options.first:
+            self.stop()
 
-    testpath = os.path.join(TESTDIR, test)
-    err = os.path.join(TESTDIR, test + ".err")
-    lctest = test.lower()
+        if self.showAll:
+            self.stream.writeln('warned %s' % reason)
+        else:
+            self.stream.write('~')
+            self.stream.flush()
 
-    if not os.path.exists(testpath):
-            return skip("doesn't exist")
+    def addOutputMismatch(self, test, ret, got, expected):
+        """Record a mismatch in test output for a particular test."""
 
-    if not (options.whitelisted and test in options.whitelisted):
-        if options.blacklist and test in options.blacklist:
-            return skip("blacklisted")
+        if self._options.nodiff:
+            return
 
-        if options.retest and not os.path.exists(test + ".err"):
-            return ignore("not retesting")
+        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 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")
+        iolock.acquire()
+        print '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)
+
+    def startTest(self, test):
+        super(TestResult, self).startTest(test)
 
-    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")
+        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')
+
+        # 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)
+
+    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)
+            runner.run(suite)
 
-    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)
-
-    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)
+            # 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)
 
-        # 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")
+        self._usecorrectpython()
+
+        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'
 
-    path = [BINDIR] + os.environ["PATH"].split(os.pathsep)
-    if TMPBINDIR != BINDIR:
-        path = [TMPBINDIR] + path
-    os.environ["PATH"] = os.pathsep.join(path)
+        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
+
+    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."""
+        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
 
-    # 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 _outputcoverage(self):
+        """Produce code coverage output."""
+        vlog('# Producing coverage report')
+        os.chdir(self._pythondir)
+
+        def covrun(*args):
+            cmd = 'coverage %s' % ' '.join(args)
+            vlog('# Running: %s' % cmd)
+            os.system(cmd)
 
-    COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
+        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)
 
-    vlog("# Using TESTDIR", TESTDIR)
-    vlog("# Using HGTMP", HGTMP)
-    vlog("# Using PATH", os.environ["PATH"])
-    vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
+    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
 
-    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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-add.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-backout.t	Mon May 26 12:39:31 2014 -0400
@@ -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
@@ -490,6 +505,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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-bookmarks-current.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-bookmarks-merge.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-bookmarks-pushpull.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-bookmarks-strip.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-bookmarks.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-bundle2.t	Mon May 26 12:39:31 2014 -0400
@@ -799,6 +799,12 @@
   added 1 changesets with 1 changes to 1 files (+1 heads)
   (run 'hg heads' to see heads, 'hg merge' to merge)
 
+pull empty
+
+  $ hg -R other pull -r 24b6387c8c8c
+  pulling from $TESTTMP/main (glob)
+  no changes found
+
 push
 
   $ hg -R main push other --rev eea13746799a
--- a/tests/test-check-code-hg.t	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-check-code-hg.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-check-code.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-check-pyflakes.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-commandserver.py.out	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-commit-amend.t	Mon May 26 12:39:31 2014 -0400
@@ -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:29ee7aa200c8
   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:ba3eb3e8e8c2
   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:0ce8747233f6
   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:b8235574e741
   tag:         tip
-  parent:      26:7fb89c461f81
+  parent:      26:0ce8747233f6
   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:dbafc132c18a
   tag:         tip
-  parent:      26:7fb89c461f81
+  parent:      26:0ce8747233f6
   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:8b0c83445ff5
   tag:         tip
-  parent:      32:01780b896f58
+  parent:      32:f60ace0fe178
   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:f9b6726d8bd2
   tag:         tip
-  parent:      32:01780b896f58
+  parent:      32:f60ace0fe178
   parent:      31:67db8847a540
   user:        test
   date:        Thu Jan 01 00:00:00 1970 +0000
--- a/tests/test-commit-unresolved.t	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-commit-unresolved.t	Mon May 26 12:39:31 2014 -0400
@@ -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-conflict.t	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-conflict.t	Mon May 26 12:39:31 2014 -0400
@@ -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-convert-hg-sink.t	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-convert-hg-sink.t	Mon May 26 12:39:31 2014 -0400
@@ -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
--- a/tests/test-convert-hg-source.t	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-convert-hg-source.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-convert-svn-sink.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-copy-move-merge.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-double-merge.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-encoding-align.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-encoding-textwrap.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-extension.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-fetch.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-fileset.t	Mon May 26 12:39:31 2014 -0400
@@ -154,6 +154,7 @@
   b2
   $ echo e > b2
   $ hg resolve -m b2
+  no more unresolved files
   $ fileset 'resolved()'
   b2
   $ fileset 'unresolved()'
--- a/tests/test-graft.t	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-graft.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-help.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-hgweb-commands.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-histedit-edit.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-histedit-fold-non-commute.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-histedit-fold.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-histedit-non-commute.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-import-bypass.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-import.t	Mon May 26 12:39:31 2014 -0400
@@ -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
--- a/tests/test-issue1877.t	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-issue1877.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-issue672.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-journal-exists.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-keyword.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Mon May 26 12:39:31 2014 -0400
@@ -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	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-largefiles.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-lfconvert.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-log.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-merge-commit.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-merge-criss-cross.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-merge-revert2.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-merge-tools.t	Mon May 26 12:39:31 2014 -0400
@@ -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
--- a/tests/test-merge-types.t	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-merge-types.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-merge7.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-mq-qfold.t	Mon May 26 12:39:31 2014 -0400
@@ -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,94 @@
   > (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)
+
   $ rm -f .hg/last-message.txt
   $ HGEDITOR="sh $TESTTMP/editor.sh" hg qfold -e p3
-  ==== before editing
-  original message====
   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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-mq-qnew.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-mq-qrefresh-replace-log-message.t	Mon May 26 12:39:31 2014 -0400
@@ -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'
 
@@ -59,3 +61,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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-patchbomb.t	Mon May 26 12:39:31 2014 -0400
@@ -17,6 +17,8 @@
   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)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:01:00 +0000
@@ -87,6 +89,8 @@
   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)
   In-Reply-To: <patchbomb.120@*> (glob)
   References: <patchbomb.120@*> (glob)
@@ -116,6 +120,8 @@
   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)
   In-Reply-To: <patchbomb.120@*> (glob)
   References: <patchbomb.120@*> (glob)
@@ -251,6 +257,8 @@
   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)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:04:00 +0000
@@ -294,6 +302,8 @@
   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)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:04:00 +0000
@@ -353,6 +363,8 @@
   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)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:04:00 +0000
@@ -404,6 +416,8 @@
   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)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:04:00 +0000
@@ -463,6 +477,8 @@
   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)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:05:00 +0000
@@ -508,6 +524,8 @@
   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)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:01:00 +0000
@@ -584,6 +602,8 @@
   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)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
@@ -617,6 +637,8 @@
   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)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
@@ -655,6 +677,8 @@
   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)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:01:00 +0000
@@ -695,6 +719,8 @@
   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)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:01:00 +0000
@@ -767,6 +793,8 @@
   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)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
@@ -802,6 +830,8 @@
   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)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
@@ -837,6 +867,8 @@
   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)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
@@ -894,6 +926,8 @@
   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)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:01:00 +0000
@@ -942,6 +976,8 @@
   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)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:01:00 +0000
@@ -1006,6 +1042,8 @@
   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)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:01:00 +0000
@@ -1081,6 +1119,8 @@
   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)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
@@ -1125,6 +1165,8 @@
   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)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
@@ -1169,6 +1211,8 @@
   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)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
@@ -1253,6 +1297,8 @@
   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)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
@@ -1304,6 +1350,8 @@
   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)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
@@ -1356,6 +1404,8 @@
   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)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
@@ -1385,6 +1435,8 @@
   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)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
@@ -1421,6 +1473,8 @@
   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)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:01:00 +0000
@@ -1456,6 +1510,8 @@
   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)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:01:00 +0000
@@ -1494,6 +1550,8 @@
   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)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:01:00 +0000
@@ -1549,6 +1607,8 @@
   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)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
@@ -1584,6 +1644,8 @@
   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)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
@@ -1628,6 +1690,8 @@
   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)
   In-Reply-To: <baz>
   References: <baz>
@@ -1668,6 +1732,8 @@
   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)
   In-Reply-To: <baz>
   References: <baz>
@@ -1697,6 +1763,8 @@
   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)
   In-Reply-To: <baz>
   References: <baz>
@@ -1752,6 +1820,8 @@
   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)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
@@ -1781,6 +1851,8 @@
   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)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
@@ -1819,6 +1891,8 @@
   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)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:01:00 +0000
@@ -1870,6 +1944,8 @@
   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)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
@@ -1899,6 +1975,8 @@
   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)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
@@ -1937,6 +2015,8 @@
   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)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:01:00 +0000
@@ -1987,6 +2067,8 @@
   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)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
@@ -2016,6 +2098,8 @@
   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)
   In-Reply-To: <patchbomb.60@*> (glob)
   References: <patchbomb.60@*> (glob)
@@ -2055,6 +2139,8 @@
   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)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Tue, 01 Jan 1980 00:01:00 +0000
@@ -2097,6 +2183,8 @@
   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)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Tue, 01 Jan 1980 00:01:00 +0000
@@ -2184,6 +2272,8 @@
   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)
   In-Reply-To: <patchbomb.315532860@*> (glob)
   References: <patchbomb.315532860@*> (glob)
@@ -2212,6 +2302,8 @@
   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)
   In-Reply-To: <patchbomb.315532860@*> (glob)
   References: <patchbomb.315532860@*> (glob)
@@ -2247,6 +2339,8 @@
   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)
   In-Reply-To: <patchbomb.315532860@*> (glob)
   References: <patchbomb.315532860@*> (glob)
@@ -2291,6 +2385,8 @@
   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)
   In-Reply-To: <patchbomb.315532860@*> (glob)
   References: <patchbomb.315532860@*> (glob)
@@ -2319,6 +2415,8 @@
   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)
   In-Reply-To: <patchbomb.315532860@*> (glob)
   References: <patchbomb.315532860@*> (glob)
@@ -2348,6 +2446,8 @@
   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)
   In-Reply-To: <patchbomb.315532860@*> (glob)
   References: <patchbomb.315532860@*> (glob)
@@ -2386,6 +2486,8 @@
   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)
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Tue, 01 Jan 1980 00:01:00 +0000
--- a/tests/test-progress.t	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-progress.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-rebase-bookmarks.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-rebase-check-restore.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-rebase-conflicts.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-rebase-detach.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-rebase-interruptions.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-rebase-mq-skip.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-rebase-mq.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-rebase-parameters.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-rebase-scenario-global.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-rename-dir-merge.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-rename-merge1.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-rename-merge2.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-resolve.t	Mon May 26 12:39:31 2014 -0400
@@ -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 resolved
+
+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-rollback.t	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-rollback.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-run-tests.py	Mon May 26 12:39:31 2014 -0400
@@ -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-shelve.t	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-shelve.t	Mon May 26 12:39:31 2014 -0400
@@ -210,11 +210,11 @@
   +++ b/a/a
   @@ -1,2 +1,6 @@
    a
-  +<<<<<<< local
+  +<<<<<<< dest: *  - shelve: "pending changes temporary commit" (glob)
    c
   +=======
   +a
-  +>>>>>>> other
+  +>>>>>>> source: * - shelve: "changes to '[mq]: second.patch'" (glob)
   diff --git a/b.rename/b b/b.rename/b
   new file mode 100644
   --- /dev/null
@@ -292,6 +292,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
@@ -601,11 +602,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 +645,11 @@
   M f
   ? f.orig
   $ cat f
-  <<<<<<< local
+  <<<<<<< dest:   6b563750f973  - test: "intermediate other change"
   g
   =======
   f
-  >>>>>>> other
+  >>>>>>> source: 23b29cada8ba - shelve: "changes to 'commit stuff'"
   $ cat f.orig
   g
   $ hg unshelve --abort
--- a/tests/test-strip.t	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-strip.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-subrepo-git.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-subrepo-svn.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-subrepo.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-tag.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-transplant.t	Mon May 26 12:39:31 2014 -0400
@@ -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
 
 
--- a/tests/test-up-local-change.t	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-up-local-change.t	Mon May 26 12:39:31 2014 -0400
@@ -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	Thu May 15 23:53:21 2014 -0700
+++ b/tests/test-update-reverse.t	Mon May 26 12:39:31 2014 -0400
@@ -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