branching: merge default into stable stable 6.3rc0
authorRaphaël Gomès <rgomes@octobus.net>
Mon, 24 Oct 2022 15:32:14 +0200
branchstable
changeset 49527 a3356ab610fc
parent 49526 192949b68159 (current diff)
parent 49522 52dd7a43ad5c (diff)
child 49528 c1cb90d72196
child 49531 f68d285158b2
branching: merge default into stable This marks the feature freeze for the 6.3 release
contrib/perf-utils/compare-discovery-case
mercurial/configitems.py
mercurial/shelve.py
tests/test-shelve.t
tests/test-shelve2.t
--- a/Makefile	Thu Oct 20 12:05:17 2022 -0400
+++ b/Makefile	Mon Oct 24 15:32:14 2022 +0200
@@ -203,9 +203,11 @@
 packaging_targets := \
   rhel7 \
   rhel8 \
+  rhel9 \
   deb \
   docker-rhel7 \
   docker-rhel8 \
+  docker-rhel9 \
   docker-debian-bullseye \
   docker-debian-buster \
   docker-debian-stretch \
--- a/contrib/heptapod-ci.yml	Thu Oct 20 12:05:17 2022 -0400
+++ b/contrib/heptapod-ci.yml	Mon Oct 24 15:32:14 2022 +0200
@@ -89,7 +89,7 @@
       - hg -R /tmp/mercurial-ci/ update `hg log --rev '.' --template '{node}'`
       - cd /tmp/mercurial-ci/
       - make local PYTHON=$PYTHON
-      - $PYTHON -m pip install --user -U pytype==2021.04.15
+      - $PYTHON -m pip install --user -U libcst==0.3.20 pytype==2022.03.29
     script:
       - echo "Entering script section"
       - sh contrib/check-pytype.sh
--- a/contrib/packaging/Makefile	Thu Oct 20 12:05:17 2022 -0400
+++ b/contrib/packaging/Makefile	Mon Oct 24 15:32:14 2022 +0200
@@ -15,7 +15,8 @@
 
 RHEL_RELEASES := \
   7 \
-  8
+  8 \
+  9
 
 # Build a Python for these RHEL (and derivatives) releases.
 RHEL_WITH_PYTHON_RELEASES :=
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/packaging/docker/rhel9	Mon Oct 24 15:32:14 2022 +0200
@@ -0,0 +1,25 @@
+FROM rockylinux/rockylinux:9
+
+RUN groupadd -g %GID% build && \
+    useradd -u %UID% -g %GID% -s /bin/bash -d /build -m build
+
+RUN dnf install 'dnf-command(config-manager)' -y
+# crb repository is necessary for docutils
+RUN dnf config-manager --set-enabled crb
+
+RUN yum install -y \
+	gcc \
+	gettext \
+	make \
+	python3-devel \
+	python3-docutils \
+	rpm-build
+
+# For creating repo meta data
+RUN yum install -y createrepo
+
+# For rust extensions
+RUN yum install -y cargo
+
+# avoid incorrect docker image permissions on /tmp preventing writes by non-root users
+RUN chmod 1777 /tmp
--- a/contrib/packaging/packagelib.sh	Thu Oct 20 12:05:17 2022 -0400
+++ b/contrib/packaging/packagelib.sh	Mon Oct 24 15:32:14 2022 +0200
@@ -9,7 +9,7 @@
 # node: the node|short hg was built from, or empty if built from a tag
 gethgversion() {
     if [ -z "${1+x}" ]; then
-        python="python"
+        python="python3"
     else
         python="$1"
     fi
--- a/contrib/perf-utils/compare-discovery-case	Thu Oct 20 12:05:17 2022 -0400
+++ b/contrib/perf-utils/compare-discovery-case	Mon Oct 24 15:32:14 2022 +0200
@@ -97,6 +97,16 @@
 assert set(VARIANTS.keys()) == set(VARIANTS_KEYS)
 
 
+def parse_case(case):
+    case_type, case_args = case.split('-', 1)
+    if case_type == 'file':
+        case_args = (case_args,)
+    else:
+        case_args = tuple(int(x) for x in case_args.split('-'))
+    case = (case_type,) + case_args
+    return case
+
+
 def format_case(case):
     return '-'.join(str(s) for s in case)
 
@@ -109,12 +119,41 @@
         return '::randomantichain(all(), "%d")' % case[1]
     elif t == 'rev':
         return '::%d' % case[1]
+    elif t == 'file':
+        return '::nodefromfile("%s")' % case[1]
     else:
         assert False
 
 
-def compare(repo, local_case, remote_case):
+def compare(
+    repo,
+    local_case,
+    remote_case,
+    display_header=True,
+    display_case=True,
+):
     case = (repo, local_case, remote_case)
+    if display_header:
+        pieces = ['#']
+        if display_case:
+            pieces += [
+                "repo",
+                "local-subset",
+                "remote-subset",
+            ]
+
+        pieces += [
+            "discovery-variant",
+            "roundtrips",
+            "queries",
+            "revs",
+            "local-heads",
+            "common-heads",
+            "undecided-initial",
+            "undecided-common",
+            "undecided-missing",
+        ]
+        print(*pieces)
     for variant in VARIANTS_KEYS:
         res = process(case, VARIANTS[variant])
         revs = res["nb-revs"]
@@ -122,36 +161,31 @@
         common_heads = res["nb-common-heads"]
         roundtrips = res["total-roundtrips"]
         queries = res["total-queries"]
-        if 'tree-discovery' in variant:
-            print(
+        pieces = []
+        if display_case:
+            pieces += [
                 repo,
                 format_case(local_case),
                 format_case(remote_case),
-                variant,
-                roundtrips,
-                queries,
-                revs,
-                local_heads,
-                common_heads,
-            )
-        else:
+            ]
+        pieces += [
+            variant,
+            roundtrips,
+            queries,
+            revs,
+            local_heads,
+            common_heads,
+        ]
+        if 'tree-discovery' not in variant:
             undecided_common = res["nb-ini_und-common"]
             undecided_missing = res["nb-ini_und-missing"]
             undecided = undecided_common + undecided_missing
-            print(
-                repo,
-                format_case(local_case),
-                format_case(remote_case),
-                variant,
-                roundtrips,
-                queries,
-                revs,
-                local_heads,
-                common_heads,
+            pieces += [
                 undecided,
                 undecided_common,
                 undecided_missing,
-            )
+            ]
+        print(*pieces)
     return 0
 
 
@@ -171,13 +205,23 @@
 
 
 if __name__ == '__main__':
-    if len(sys.argv) != 4:
+
+    argv = sys.argv[:]
+
+    kwargs = {}
+    # primitive arg parsing
+    if '--no-header' in argv:
+        kwargs['display_header'] = False
+        argv = [a for a in argv if a != '--no-header']
+    if '--no-case' in argv:
+        kwargs['display_case'] = False
+        argv = [a for a in argv if a != '--no-case']
+
+    if len(argv) != 4:
         usage = f'USAGE: {script_name} REPO LOCAL_CASE REMOTE_CASE'
         print(usage, file=sys.stderr)
         sys.exit(128)
-    repo = sys.argv[1]
-    local_case = sys.argv[2].split('-')
-    local_case = (local_case[0],) + tuple(int(x) for x in local_case[1:])
-    remote_case = sys.argv[3].split('-')
-    remote_case = (remote_case[0],) + tuple(int(x) for x in remote_case[1:])
-    sys.exit(compare(repo, local_case, remote_case))
+    repo = argv[1]
+    local_case = parse_case(argv[2])
+    remote_case = parse_case(argv[3])
+    sys.exit(compare(repo, local_case, remote_case, **kwargs))
--- a/contrib/perf.py	Thu Oct 20 12:05:17 2022 -0400
+++ b/contrib/perf.py	Mon Oct 24 15:32:14 2022 +0200
@@ -925,6 +925,71 @@
     fm.end()
 
 
+@command(
+    b'perf::delta-find',
+    revlogopts + formatteropts,
+    b'-c|-m|FILE REV',
+)
+def perf_delta_find(ui, repo, arg_1, arg_2=None, **opts):
+    """benchmark the process of finding a valid delta for a revlog revision
+
+    When a revlog receives a new revision (e.g. from a commit, or from an
+    incoming bundle), it searches for a suitable delta-base to produce a delta.
+    This perf command measures how much time we spend in this process. It
+    operates on an already stored revision.
+
+    See `hg help debug-delta-find` for another related command.
+    """
+    from mercurial import revlogutils
+    import mercurial.revlogutils.deltas as deltautil
+
+    opts = _byteskwargs(opts)
+    if arg_2 is None:
+        file_ = None
+        rev = arg_1
+    else:
+        file_ = arg_1
+        rev = arg_2
+
+    repo = repo.unfiltered()
+
+    timer, fm = gettimer(ui, opts)
+
+    rev = int(rev)
+
+    revlog = cmdutil.openrevlog(repo, b'perf::delta-find', file_, opts)
+
+    deltacomputer = deltautil.deltacomputer(revlog)
+
+    node = revlog.node(rev)
+    p1r, p2r = revlog.parentrevs(rev)
+    p1 = revlog.node(p1r)
+    p2 = revlog.node(p2r)
+    full_text = revlog.revision(rev)
+    textlen = len(full_text)
+    cachedelta = None
+    flags = revlog.flags(rev)
+
+    revinfo = revlogutils.revisioninfo(
+        node,
+        p1,
+        p2,
+        [full_text],  # btext
+        textlen,
+        cachedelta,
+        flags,
+    )
+
+    # Note: we should probably purge the potential caches (like the full
+    # manifest cache) between runs.
+    def find_one():
+        with revlog._datafp() as fh:
+            deltacomputer.finddeltainfo(revinfo, fh, target_rev=rev)
+
+    timer(find_one)
+    fm.end()
+
+
 @command(b'perf::discovery|perfdiscovery', formatteropts, b'PATH')
 def perfdiscovery(ui, repo, path, **opts):
     """benchmark discovery between local repo and the peer at given path"""
@@ -974,6 +1039,111 @@
     fm.end()
 
 
+@command(
+    b'perf::bundle',
+    [
+        (
+            b'r',
+            b'rev',
+            [],
+            b'changesets to bundle',
+            b'REV',
+        ),
+        (
+            b't',
+            b'type',
+            b'none',
+            b'bundlespec to use (see `hg help bundlespec`)',
+            b'TYPE',
+        ),
+    ]
+    + formatteropts,
+    b'REVS',
+)
+def perfbundle(ui, repo, *revs, **opts):
+    """benchmark the creation of a bundle from a repository
+
+    For now, this only supports "none" compression.
+    """
+    try:
+        from mercurial import bundlecaches
+
+        parsebundlespec = bundlecaches.parsebundlespec
+    except ImportError:
+        from mercurial import exchange
+
+        parsebundlespec = exchange.parsebundlespec
+
+    from mercurial import discovery
+    from mercurial import bundle2
+
+    opts = _byteskwargs(opts)
+    timer, fm = gettimer(ui, opts)
+
+    cl = repo.changelog
+    revs = list(revs)
+    revs.extend(opts.get(b'rev', ()))
+    revs = scmutil.revrange(repo, revs)
+    if not revs:
+        raise error.Abort(b"not revision specified")
+    # make it a consistent set (ie: without topological gaps)
+    old_len = len(revs)
+    revs = list(repo.revs(b"%ld::%ld", revs, revs))
+    if old_len != len(revs):
+        new_count = len(revs) - old_len
+        msg = b"add %d new revisions to make it a consistent set\n"
+        ui.write_err(msg % new_count)
+
+    targets = [cl.node(r) for r in repo.revs(b"heads(::%ld)", revs)]
+    bases = [cl.node(r) for r in repo.revs(b"heads(::%ld - %ld)", revs, revs)]
+    outgoing = discovery.outgoing(repo, bases, targets)
+
+    bundle_spec = opts.get(b'type')
+
+    bundle_spec = parsebundlespec(repo, bundle_spec, strict=False)
+
+    cgversion = bundle_spec.params.get(b"cg.version")
+    if cgversion is None:
+        if bundle_spec.version == b'v1':
+            cgversion = b'01'
+        if bundle_spec.version == b'v2':
+            cgversion = b'02'
+    if cgversion not in changegroup.supportedoutgoingversions(repo):
+        err = b"repository does not support bundle version %s"
+        raise error.Abort(err % cgversion)
+
+    if cgversion == b'01':  # bundle1
+        bversion = b'HG10' + bundle_spec.wirecompression
+        bcompression = None
+    elif cgversion in (b'02', b'03'):
+        bversion = b'HG20'
+        bcompression = bundle_spec.wirecompression
+    else:
+        err = b'perf::bundle: unexpected changegroup version %s'
+        raise error.ProgrammingError(err % cgversion)
+
+    if bcompression is None:
+        bcompression = b'UN'
+
+    if bcompression != b'UN':
+        err = b'perf::bundle: compression currently unsupported: %s'
+        raise error.ProgrammingError(err % bcompression)
+
+    def do_bundle():
+        bundle2.writenewbundle(
+            ui,
+            repo,
+            b'perf::bundle',
+            os.devnull,
+            bversion,
+            outgoing,
+            bundle_spec.params,
+        )
+
+    timer(do_bundle)
+    fm.end()
+
+
 @command(b'perf::bundleread|perfbundleread', formatteropts, b'BUNDLE')
 def perfbundleread(ui, repo, bundlepath, **opts):
     """Benchmark reading of bundle files.
@@ -2498,6 +2668,60 @@
 
 
 @command(
+    b'perf::unbundle',
+    formatteropts,
+    b'BUNDLE_FILE',
+)
+def perf_unbundle(ui, repo, fname, **opts):
+    """benchmark application of a bundle in a repository.
+
+    This does not include the final transaction processing"""
+    from mercurial import exchange
+    from mercurial import bundle2
+
+    opts = _byteskwargs(opts)
+
+    with repo.lock():
+        bundle = [None, None]
+        orig_quiet = repo.ui.quiet
+        try:
+            repo.ui.quiet = True
+            with open(fname, mode="rb") as f:
+
+                def noop_report(*args, **kwargs):
+                    pass
+
+                def setup():
+                    gen, tr = bundle
+                    if tr is not None:
+                        tr.abort()
+                    bundle[:] = [None, None]
+                    f.seek(0)
+                    bundle[0] = exchange.readbundle(ui, f, fname)
+                    bundle[1] = repo.transaction(b'perf::unbundle')
+                    bundle[1]._report = noop_report  # silence the transaction
+
+                def apply():
+                    gen, tr = bundle
+                    bundle2.applybundle(
+                        repo,
+                        gen,
+                        tr,
+                        source=b'perf::unbundle',
+                        url=fname,
+                    )
+
+                timer, fm = gettimer(ui, opts)
+                timer(apply, setup=setup)
+                fm.end()
+        finally:
+            repo.ui.quiet == orig_quiet
+            gen, tr = bundle
+            if tr is not None:
+                tr.abort()
+
+
+@command(
     b'perf::unidiff|perfunidiff',
     revlogopts
     + formatteropts
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/pull_logger.py	Mon Oct 24 15:32:14 2022 +0200
@@ -0,0 +1,141 @@
+# pull_logger.py - Logs pulls to a JSON-line file in the repo's VFS.
+#
+# Copyright 2022  Pacien TRAN-GIRARD <pacien.trangirard@pacien.net>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+
+'''logs pull parameters to a file
+
+This extension logs the pull parameters, i.e. the remote and common heads,
+when pulling from the local repository.
+
+The collected data should give an idea of the state of a pair of repositories
+and allow replaying past synchronisations between them. This is particularly
+useful for working on data exchange, bundling and caching-related
+optimisations.
+
+The record is a JSON-line file located in the repository's VFS at
+.hg/pull_log.jsonl.
+
+Log write failures are not considered fatal: log writes may be skipped for any
+reason such as insufficient storage or a timeout.
+
+Some basic log file rotation can be enabled by setting 'rotate-size' to a value
+greater than 0. This causes the current log file to be moved to
+.hg/pull_log.jsonl.rotated when this threshold is met, discarding any
+previously rotated log file.
+
+The timeouts of the exclusive lock used when writing to the lock file can be
+configured through the 'timeout.lock' and 'timeout.warn' options of this
+plugin. Those are not expected to be held for a significant time in practice.::
+
+  [pull-logger]
+  timeout.lock = 300
+  timeout.warn = 100
+  rotate-size = 1kb
+'''
+
+
+import json
+import time
+
+from mercurial.i18n import _
+from mercurial.utils import stringutil
+from mercurial import (
+    error,
+    extensions,
+    lock,
+    registrar,
+    wireprotov1server,
+)
+
+EXT_NAME = b'pull-logger'
+EXT_VERSION_CODE = 0
+
+LOG_FILE = b'pull_log.jsonl'
+OLD_LOG_FILE = LOG_FILE + b'.rotated'
+LOCK_NAME = LOG_FILE + b'.lock'
+
+configtable = {}
+configitem = registrar.configitem(configtable)
+configitem(EXT_NAME, b'timeout.lock', default=600)
+configitem(EXT_NAME, b'timeout.warn', default=120)
+configitem(EXT_NAME, b'rotate-size', default=b'100MB')
+
+
+def wrap_getbundle(orig, repo, proto, others, *args, **kwargs):
+    heads, common = extract_pull_heads(others)
+    log_entry = {
+        'timestamp': time.time(),
+        'logger_version': EXT_VERSION_CODE,
+        'heads': sorted(heads),
+        'common': sorted(common),
+    }
+
+    try:
+        write_to_log(repo, log_entry)
+    except (IOError, error.LockError) as err:
+        msg = stringutil.forcebytestr(err)
+        repo.ui.warn(_(b'unable to append to pull log: %s\n') % msg)
+
+    return orig(repo, proto, others, *args, **kwargs)
+
+
+def extract_pull_heads(bundle_args):
+    opts = wireprotov1server.options(
+        b'getbundle',
+        wireprotov1server.wireprototypes.GETBUNDLE_ARGUMENTS.keys(),
+        bundle_args.copy(),  # this call consumes the args destructively
+    )
+
+    heads = opts.get(b'heads', b'').decode('utf-8').split(' ')
+    common = opts.get(b'common', b'').decode('utf-8').split(' ')
+    return (heads, common)
+
+
+def write_to_log(repo, entry):
+    locktimeout = repo.ui.configint(EXT_NAME, b'timeout.lock')
+    lockwarntimeout = repo.ui.configint(EXT_NAME, b'timeout.warn')
+    rotatesize = repo.ui.configbytes(EXT_NAME, b'rotate-size')
+
+    with lock.trylock(
+        ui=repo.ui,
+        vfs=repo.vfs,
+        lockname=LOCK_NAME,
+        timeout=locktimeout,
+        warntimeout=lockwarntimeout,
+    ):
+        if rotatesize > 0 and repo.vfs.exists(LOG_FILE):
+            if repo.vfs.stat(LOG_FILE).st_size >= rotatesize:
+                repo.vfs.rename(LOG_FILE, OLD_LOG_FILE)
+
+        with repo.vfs.open(LOG_FILE, b'a+') as logfile:
+            serialised = json.dumps(entry, sort_keys=True)
+            logfile.write(serialised.encode('utf-8'))
+            logfile.write(b'\n')
+            logfile.flush()
+
+
+def reposetup(ui, repo):
+    if repo.local():
+        repo._wlockfreeprefix.add(LOG_FILE)
+        repo._wlockfreeprefix.add(OLD_LOG_FILE)
+
+
+def uisetup(ui):
+    del wireprotov1server.commands[b'getbundle']
+    decorator = wireprotov1server.wireprotocommand(
+        name=b'getbundle',
+        args=b'*',
+        permission=b'pull',
+    )
+
+    extensions.wrapfunction(
+        container=wireprotov1server,
+        funcname='getbundle',
+        wrapper=wrap_getbundle,
+    )
+
+    decorator(wireprotov1server.getbundle)
--- a/hgext/fsmonitor/pywatchman/pybser.py	Thu Oct 20 12:05:17 2022 -0400
+++ b/hgext/fsmonitor/pywatchman/pybser.py	Mon Oct 24 15:32:14 2022 +0200
@@ -36,6 +36,7 @@
 
 from . import compat
 
+abc = collections.abc
 
 BSER_ARRAY = b"\x00"
 BSER_OBJECT = b"\x01"
@@ -207,9 +208,7 @@
             self.ensure_size(needed)
             struct.pack_into(b"=cd", self.buf, self.wpos, BSER_REAL, val)
             self.wpos += needed
-        elif isinstance(val, collections.Mapping) and isinstance(
-            val, collections.Sized
-        ):
+        elif isinstance(val, abc.Mapping) and isinstance(val, abc.Sized):
             val_len = len(val)
             size = _int_size(val_len)
             needed = 2 + size
@@ -260,9 +259,7 @@
             for k, v in iteritems:
                 self.append_string(k)
                 self.append_recursive(v)
-        elif isinstance(val, collections.Iterable) and isinstance(
-            val, collections.Sized
-        ):
+        elif isinstance(val, abc.Iterable) and isinstance(val, abc.Sized):
             val_len = len(val)
             size = _int_size(val_len)
             needed = 2 + size
--- a/hgext/rebase.py	Thu Oct 20 12:05:17 2022 -0400
+++ b/hgext/rebase.py	Mon Oct 24 15:32:14 2022 +0200
@@ -546,7 +546,9 @@
         date = self.date
         if date is None:
             date = ctx.date()
-        extra = {b'rebase_source': ctx.hex()}
+        extra = {}
+        if repo.ui.configbool(b'rebase', b'store-source'):
+            extra = {b'rebase_source': ctx.hex()}
         for c in self.extrafns:
             c(ctx, extra)
         destphase = max(ctx.phase(), phases.draft)
--- a/hgext/releasenotes.py	Thu Oct 20 12:05:17 2022 -0400
+++ b/hgext/releasenotes.py	Mon Oct 24 15:32:14 2022 +0200
@@ -70,7 +70,7 @@
     (b'api', _(b'API Changes')),
 ]
 
-RE_DIRECTIVE = re.compile(br'^\.\. ([a-zA-Z0-9_]+)::\s*([^$]+)?$')
+RE_DIRECTIVE = re.compile(br'^\.\. ([a-zA-Z0-9_]+)::\s*([^$]+)?$', re.MULTILINE)
 RE_ISSUE = br'\bissue ?[0-9]{4,6}(?![0-9])\b'
 
 BULLET_SECTION = _(b'Other Changes')
--- a/mercurial/cext/revlog.c	Thu Oct 20 12:05:17 2022 -0400
+++ b/mercurial/cext/revlog.c	Mon Oct 24 15:32:14 2022 +0200
@@ -1382,6 +1382,7 @@
 static int index_issnapshotrev(indexObject *self, Py_ssize_t rev)
 {
 	int ps[2];
+	int b;
 	Py_ssize_t base;
 	while (rev >= 0) {
 		base = (Py_ssize_t)index_baserev(self, rev);
@@ -1399,6 +1400,20 @@
 			assert(PyErr_Occurred());
 			return -1;
 		};
+		while ((index_get_length(self, ps[0]) == 0) && ps[0] >= 0) {
+			b = index_baserev(self, ps[0]);
+			if (b == ps[0]) {
+				break;
+			}
+			ps[0] = b;
+		}
+		while ((index_get_length(self, ps[1]) == 0) && ps[1] >= 0) {
+			b = index_baserev(self, ps[1]);
+			if (b == ps[1]) {
+				break;
+			}
+			ps[1] = b;
+		}
 		if (base == ps[0] || base == ps[1]) {
 			return 0;
 		}
--- a/mercurial/cmdutil.py	Thu Oct 20 12:05:17 2022 -0400
+++ b/mercurial/cmdutil.py	Mon Oct 24 15:32:14 2022 +0200
@@ -832,7 +832,7 @@
 
 @attr.s(frozen=True)
 class morestatus:
-    reporoot = attr.ib()
+    repo = attr.ib()
     unfinishedop = attr.ib()
     unfinishedmsg = attr.ib()
     activemerge = attr.ib()
@@ -876,7 +876,7 @@
             mergeliststr = b'\n'.join(
                 [
                     b'    %s'
-                    % util.pathto(self.reporoot, encoding.getcwd(), path)
+                    % util.pathto(self.repo.root, encoding.getcwd(), path)
                     for path in self.unresolvedpaths
                 ]
             )
@@ -898,6 +898,7 @@
                     # Already output.
                     continue
                 fm.startitem()
+                fm.context(repo=self.repo)
                 # We can't claim to know the status of the file - it may just
                 # have been in one of the states that were not requested for
                 # display, so it could be anything.
@@ -923,7 +924,7 @@
     if activemerge:
         unresolved = sorted(mergestate.unresolved())
     return morestatus(
-        repo.root, unfinishedop, unfinishedmsg, activemerge, unresolved
+        repo, unfinishedop, unfinishedmsg, activemerge, unresolved
     )
 
 
--- a/mercurial/commands.py	Thu Oct 20 12:05:17 2022 -0400
+++ b/mercurial/commands.py	Mon Oct 24 15:32:14 2022 +0200
@@ -1035,7 +1035,14 @@
     state = hbisect.load_state(repo)
 
     if rev:
-        nodes = [repo[i].node() for i in logcmdutil.revrange(repo, rev)]
+        revs = logcmdutil.revrange(repo, rev)
+        goodnodes = state[b'good']
+        badnodes = state[b'bad']
+        if goodnodes and badnodes:
+            candidates = repo.revs(b'(%ln)::(%ln)', goodnodes, badnodes)
+            candidates += repo.revs(b'(%ln)::(%ln)', badnodes, goodnodes)
+            revs = candidates & revs
+        nodes = [repo.changelog.node(i) for i in revs]
     else:
         nodes = [repo.lookup(b'.')]
 
@@ -1485,6 +1492,12 @@
     b'bundle',
     [
         (
+            b'',
+            b'exact',
+            None,
+            _(b'compute the base from the revision specified'),
+        ),
+        (
             b'f',
             b'force',
             None,
@@ -1553,6 +1566,7 @@
     Returns 0 on success, 1 if no changes found.
     """
     opts = pycompat.byteskwargs(opts)
+
     revs = None
     if b'rev' in opts:
         revstrings = opts[b'rev']
@@ -1586,7 +1600,19 @@
             )
         if opts.get(b'base'):
             ui.warn(_(b"ignoring --base because --all was specified\n"))
+        if opts.get(b'exact'):
+            ui.warn(_(b"ignoring --exact because --all was specified\n"))
         base = [nullrev]
+    elif opts.get(b'exact'):
+        if dests:
+            raise error.InputError(
+                _(b"--exact is incompatible with specifying destinations")
+            )
+        if opts.get(b'base'):
+            ui.warn(_(b"ignoring --base because --exact was specified\n"))
+        base = repo.revs(b'parents(%ld) - %ld', revs, revs)
+        if not base:
+            base = [nullrev]
     else:
         base = logcmdutil.revrange(repo, opts.get(b'base'))
     if cgversion not in changegroup.supportedoutgoingversions(repo):
@@ -6954,11 +6980,13 @@
     )
 
     copy = {}
-    if (
-        opts.get(b'all')
-        or opts.get(b'copies')
-        or ui.configbool(b'ui', b'statuscopies')
-    ) and not opts.get(b'no_status'):
+    show_copies = ui.configbool(b'ui', b'statuscopies')
+    if opts.get(b'copies') is not None:
+        show_copies = opts.get(b'copies')
+    show_copies = (show_copies or opts.get(b'all')) and not opts.get(
+        b'no_status'
+    )
+    if show_copies:
         copy = copies.pathcopies(ctx1, ctx2, m)
 
     morestatus = None
--- a/mercurial/configitems.py	Thu Oct 20 12:05:17 2022 -0400
+++ b/mercurial/configitems.py	Mon Oct 24 15:32:14 2022 +0200
@@ -1425,12 +1425,38 @@
     default=False,
     experimental=True,
 )
+
+# Moving this on by default means we are confident about the scaling of phases.
+# This is not garanteed to be the case at the time this message is written.
 coreconfigitem(
     b'format',
-    b'internal-phase',
+    b'use-internal-phase',
     default=False,
     experimental=True,
 )
+# The interaction between the archived phase and obsolescence markers needs to
+# be sorted out before wider usage of this are to be considered.
+#
+# At the time this message is written, behavior when archiving obsolete
+# changeset differ significantly from stripping. As part of stripping, we also
+# remove the obsolescence marker associated to the stripped changesets,
+# revealing the precedecessors changesets when applicable. When archiving, we
+# don't touch the obsolescence markers, keeping everything hidden. This can
+# result in quite confusing situation for people combining exchanging draft
+# with the archived phases. As some markers needed by others may be skipped
+# during exchange.
+coreconfigitem(
+    b'format',
+    b'exp-archived-phase',
+    default=False,
+    experimental=True,
+)
+coreconfigitem(
+    b'shelve',
+    b'store',
+    default=b'internal',
+    experimental=True,
+)
 coreconfigitem(
     b'fsmonitor',
     b'warn_when_unused',
@@ -2835,3 +2861,17 @@
     b'experimental.inmemory',
     default=False,
 )
+
+# This setting controls creation of a rebase_source extra field
+# during rebase. When False, no such field is created. This is
+# useful eg for incrementally converting changesets and then
+# rebasing them onto an existing repo.
+# WARNING: this is an advanced setting reserved for people who know
+# exactly what they are doing. Misuse of this setting can easily
+# result in obsmarker cycles and a vivid headache.
+coreconfigitem(
+    b'rebase',
+    b'store-source',
+    default=True,
+    experimental=True,
+)
--- a/mercurial/debugcommands.py	Thu Oct 20 12:05:17 2022 -0400
+++ b/mercurial/debugcommands.py	Mon Oct 24 15:32:14 2022 +0200
@@ -1021,7 +1021,7 @@
     deltacomputer = deltautil.deltacomputer(
         revlog,
         write_debug=ui.write,
-        debug_search=True,
+        debug_search=not ui.quiet,
     )
 
     node = revlog.node(rev)
--- a/mercurial/defaultrc/mergetools.rc	Thu Oct 20 12:05:17 2022 -0400
+++ b/mercurial/defaultrc/mergetools.rc	Mon Oct 24 15:32:14 2022 +0200
@@ -107,7 +107,7 @@
 
 meld.args=--label=$labellocal $local --label='merged' $base --label=$labelother $other -o $output --auto-merge
 meld.check=changed
-meld.diffargs=-a --label=$plabel1 $parent --label=$clabel $child
+meld.diffargs=--label=$plabel1 $parent --label=$clabel $child
 meld.gui=True
 
 merge.check=conflicts
--- a/mercurial/dirstate.py	Thu Oct 20 12:05:17 2022 -0400
+++ b/mercurial/dirstate.py	Mon Oct 24 15:32:14 2022 +0200
@@ -1287,6 +1287,7 @@
 
         allowed_matchers = (
             matchmod.alwaysmatcher,
+            matchmod.differencematcher,
             matchmod.exactmatcher,
             matchmod.includematcher,
             matchmod.intersectionmatcher,
--- a/mercurial/dispatch.py	Thu Oct 20 12:05:17 2022 -0400
+++ b/mercurial/dispatch.py	Mon Oct 24 15:32:14 2022 +0200
@@ -952,14 +952,22 @@
 
     Takes paths in [cwd]/.hg/hgrc into account."
     """
+    try:
+        cwd = encoding.getcwd()
+    except OSError as e:
+        raise error.Abort(
+            _(b"error getting current working directory: %s")
+            % encoding.strtolocal(e.strerror)
+        )
+
+    # If using an alternate wd, temporarily switch to it so that relative
+    # paths are resolved correctly during config loading.
+    oldcwd = None
     if wd is None:
-        try:
-            wd = encoding.getcwd()
-        except OSError as e:
-            raise error.Abort(
-                _(b"error getting current working directory: %s")
-                % encoding.strtolocal(e.strerror)
-            )
+        wd = cwd
+    else:
+        oldcwd = cwd
+        os.chdir(wd)
 
     path = cmdutil.findrepo(wd) or b""
     if not path:
@@ -979,6 +987,9 @@
             lui.readconfig(os.path.join(path, b".hg", b"hgrc"), path)
             lui.readconfig(os.path.join(path, b".hg", b"hgrc-not-shared"), path)
 
+    if oldcwd:
+        os.chdir(oldcwd)
+
     return path, lui
 
 
--- a/mercurial/hbisect.py	Thu Oct 20 12:05:17 2022 -0400
+++ b/mercurial/hbisect.py	Mon Oct 24 15:32:14 2022 +0200
@@ -39,7 +39,7 @@
     def buildancestors(bad, good):
         badrev = min([changelog.rev(n) for n in bad])
         ancestors = collections.defaultdict(lambda: None)
-        for rev in repo.revs(b"descendants(%ln) - ancestors(%ln)", good, good):
+        for rev in repo.revs(b"(%ln::%d) - (::%ln)", good, badrev, good):
             ancestors[rev] = []
         if ancestors[badrev] is None:
             return badrev, None
@@ -115,11 +115,21 @@
             poison.update(children.get(rev, []))
             continue
 
+        unvisited = []
         for c in children.get(rev, []):
             if ancestors[c]:
                 ancestors[c] = list(set(ancestors[c] + a))
             else:
+                unvisited.append(c)
+
+        # Reuse existing ancestor list for the first unvisited child to avoid
+        # excessive copying for linear portions of history.
+        if unvisited:
+            first = unvisited.pop(0)
+            for c in unvisited:
                 ancestors[c] = a + [c]
+            a.append(first)
+            ancestors[first] = a
 
     assert best_rev is not None
     best_node = changelog.node(best_rev)
--- a/mercurial/helptext/bundlespec.txt	Thu Oct 20 12:05:17 2022 -0400
+++ b/mercurial/helptext/bundlespec.txt	Mon Oct 24 15:32:14 2022 +0200
@@ -67,6 +67,33 @@
 
 .. bundlecompressionmarker
 
+Available Options
+=================
+
+The following options exist:
+
+changegroup
+    Include the changegroup data in the bundle (default to True).
+
+cg.version
+    Select the version of the changegroup to use. Available options are : 01, 02
+    or 03. By default it will be automatically selected according to the current
+    repository format.
+
+obsolescence
+    Include obsolescence-markers relevant to the bundled changesets.
+
+phases
+    Include phase information relevant to the bundled changesets.
+
+revbranchcache
+    Include the "tags-fnodes" cache inside the bundle.
+
+
+tagsfnodescache
+    Include the "tags-fnodes" cache inside the bundle.
+
+
 Examples
 ========
 
--- a/mercurial/hgweb/__init__.py	Thu Oct 20 12:05:17 2022 -0400
+++ b/mercurial/hgweb/__init__.py	Mon Oct 24 15:32:14 2022 +0200
@@ -18,12 +18,15 @@
 
 from ..utils import procutil
 
+# pytype: disable=pyi-error
 from . import (
     hgweb_mod,
     hgwebdir_mod,
     server,
 )
 
+# pytype: enable=pyi-error
+
 
 def hgweb(config, name=None, baseui=None):
     """create an hgweb wsgi object
--- a/mercurial/localrepo.py	Thu Oct 20 12:05:17 2022 -0400
+++ b/mercurial/localrepo.py	Mon Oct 24 15:32:14 2022 +0200
@@ -522,12 +522,8 @@
     # the repository. This file was introduced in Mercurial 0.9.2,
     # which means very old repositories may not have one. We assume
     # a missing file translates to no requirements.
-    try:
-        return set(vfs.read(b'requires').splitlines())
-    except FileNotFoundError:
-        if not allowmissing:
-            raise
-        return set()
+    read = vfs.tryread if allowmissing else vfs.read
+    return set(read(b'requires').splitlines())
 
 
 def makelocalrepository(baseui, path, intents=None):
@@ -1281,6 +1277,7 @@
     """
 
     _basesupported = {
+        requirementsmod.ARCHIVED_PHASE_REQUIREMENT,
         requirementsmod.BOOKMARKS_IN_STORE_REQUIREMENT,
         requirementsmod.CHANGELOGV2_REQUIREMENT,
         requirementsmod.COPIESSDC_REQUIREMENT,
@@ -3668,9 +3665,13 @@
         requirements.discard(requirementsmod.REVLOGV1_REQUIREMENT)
         requirements.add(requirementsmod.REVLOGV2_REQUIREMENT)
     # experimental config: format.internal-phase
-    if ui.configbool(b'format', b'internal-phase'):
+    if ui.configbool(b'format', b'use-internal-phase'):
         requirements.add(requirementsmod.INTERNAL_PHASE_REQUIREMENT)
 
+    # experimental config: format.exp-archived-phase
+    if ui.configbool(b'format', b'exp-archived-phase'):
+        requirements.add(requirementsmod.ARCHIVED_PHASE_REQUIREMENT)
+
     if createopts.get(b'narrowfiles'):
         requirements.add(requirementsmod.NARROW_REQUIREMENT)
 
--- a/mercurial/obsolete.py	Thu Oct 20 12:05:17 2022 -0400
+++ b/mercurial/obsolete.py	Mon Oct 24 15:32:14 2022 +0200
@@ -70,6 +70,7 @@
 
 import binascii
 import struct
+import weakref
 
 from .i18n import _
 from .pycompat import getattr
@@ -561,10 +562,18 @@
         # caches for various obsolescence related cache
         self.caches = {}
         self.svfs = svfs
-        self.repo = repo
+        self._repo = weakref.ref(repo)
         self._defaultformat = defaultformat
         self._readonly = readonly
 
+    @property
+    def repo(self):
+        r = self._repo()
+        if r is None:
+            msg = "using the obsstore of a deallocated repo"
+            raise error.ProgrammingError(msg)
+        return r
+
     def __iter__(self):
         return iter(self._all)
 
--- a/mercurial/phases.py	Thu Oct 20 12:05:17 2022 -0400
+++ b/mercurial/phases.py	Mon Oct 24 15:32:14 2022 +0200
@@ -178,6 +178,12 @@
     return requirements.INTERNAL_PHASE_REQUIREMENT in repo.requirements
 
 
+def supportarchived(repo):
+    # type: (localrepo.localrepository) -> bool
+    """True if the archived phase can be used on a repository"""
+    return requirements.ARCHIVED_PHASE_REQUIREMENT in repo.requirements
+
+
 def _readroots(repo, phasedefaults=None):
     # type: (localrepo.localrepository, Optional[Phasedefaults]) -> Tuple[Phaseroots, bool]
     """Read phase roots from disk
@@ -642,7 +648,12 @@
         # phaseroots values, replace them.
         if revs is None:
             revs = []
-        if targetphase in (archived, internal) and not supportinternal(repo):
+        if (
+            targetphase == internal
+            and not supportinternal(repo)
+            or targetphase == archived
+            and not supportarchived(repo)
+        ):
             name = phasenames[targetphase]
             msg = b'this repository does not support the %s phase' % name
             raise error.ProgrammingError(msg)
--- a/mercurial/requirements.py	Thu Oct 20 12:05:17 2022 -0400
+++ b/mercurial/requirements.py	Mon Oct 24 15:32:14 2022 +0200
@@ -29,7 +29,11 @@
 
 # Enables the internal phase which is used to hide changesets instead
 # of stripping them
-INTERNAL_PHASE_REQUIREMENT = b'internal-phase'
+INTERNAL_PHASE_REQUIREMENT = b'internal-phase-2'
+
+# Enables the internal phase which is used to hide changesets instead
+# of stripping them
+ARCHIVED_PHASE_REQUIREMENT = b'exp-archived-phase'
 
 # Stores manifest in Tree structure
 TREEMANIFEST_REQUIREMENT = b'treemanifest'
@@ -107,6 +111,7 @@
 #
 # note: the list is currently inherited from previous code and miss some relevant requirement while containing some irrelevant ones.
 STREAM_FIXED_REQUIREMENTS = {
+    ARCHIVED_PHASE_REQUIREMENT,
     BOOKMARKS_IN_STORE_REQUIREMENT,
     CHANGELOGV2_REQUIREMENT,
     COPIESSDC_REQUIREMENT,
--- a/mercurial/revlog.py	Thu Oct 20 12:05:17 2022 -0400
+++ b/mercurial/revlog.py	Mon Oct 24 15:32:14 2022 +0200
@@ -235,6 +235,8 @@
     b'  expected %d bytes from offset %d, data size is %d'
 )
 
+hexdigits = b'0123456789abcdefABCDEF'
+
 
 class revlog:
     """
@@ -1509,7 +1511,7 @@
                 ambiguous = True
             # fall through to slow path that filters hidden revisions
         except (AttributeError, ValueError):
-            # we are pure python, or key was too short to search radix tree
+            # we are pure python, or key is not hex
             pass
         if ambiguous:
             raise error.AmbiguousPrefixLookupError(
@@ -1523,6 +1525,11 @@
             # hex(node)[:...]
             l = len(id) // 2 * 2  # grab an even number of digits
             try:
+                # we're dropping the last digit, so let's check that it's hex,
+                # to avoid the expensive computation below if it's not
+                if len(id) % 2 > 0:
+                    if not (id[-1] in hexdigits):
+                        return None
                 prefix = bin(id[:l])
             except binascii.Error:
                 pass
@@ -1768,7 +1775,17 @@
         if base == nullrev:
             return True
         p1 = entry[5]
+        while self.length(p1) == 0:
+            b = self.deltaparent(p1)
+            if b == p1:
+                break
+            p1 = b
         p2 = entry[6]
+        while self.length(p2) == 0:
+            b = self.deltaparent(p2)
+            if b == p2:
+                break
+            p2 = b
         if base == p1 or base == p2:
             return False
         return self.issnapshot(base)
--- a/mercurial/revset.py	Thu Oct 20 12:05:17 2022 -0400
+++ b/mercurial/revset.py	Mon Oct 24 15:32:14 2022 +0200
@@ -7,7 +7,10 @@
 
 
 import binascii
+import functools
+import random
 import re
+import sys
 
 from .i18n import _
 from .pycompat import getattr
@@ -2339,14 +2342,28 @@
     parents = repo.changelog.parentrevs
 
     def filter(r):
-        for p in parents(r):
-            if 0 <= p and p in s:
-                return False
+        try:
+            for p in parents(r):
+                if 0 <= p and p in s:
+                    return False
+        except error.WdirUnsupported:
+            for p in repo[None].parents():
+                if p.rev() in s:
+                    return False
         return True
 
     return subset & s.filter(filter, condrepr=b'<roots>')
 
 
+MAXINT = sys.maxsize
+MININT = -MAXINT - 1
+
+
+def pick_random(c, gen=random):
+    # exists as its own function to make it possible to overwrite the seed
+    return gen.randint(MININT, MAXINT)
+
+
 _sortkeyfuncs = {
     b'rev': scmutil.intrev,
     b'branch': lambda c: c.branch(),
@@ -2355,12 +2372,17 @@
     b'author': lambda c: c.user(),
     b'date': lambda c: c.date()[0],
     b'node': scmutil.binnode,
+    b'random': pick_random,
 }
 
 
 def _getsortargs(x):
     """Parse sort options into (set, [(key, reverse)], opts)"""
-    args = getargsdict(x, b'sort', b'set keys topo.firstbranch')
+    args = getargsdict(
+        x,
+        b'sort',
+        b'set keys topo.firstbranch random.seed',
+    )
     if b'set' not in args:
         # i18n: "sort" is a keyword
         raise error.ParseError(_(b'sort requires one or two arguments'))
@@ -2400,6 +2422,20 @@
                 )
             )
 
+    if b'random.seed' in args:
+        if any(k == b'random' for k, reverse in keyflags):
+            s = args[b'random.seed']
+            seed = getstring(s, _(b"random.seed must be a string"))
+            opts[b'random.seed'] = seed
+        else:
+            # i18n: "random" and "random.seed" are keywords
+            raise error.ParseError(
+                _(
+                    b'random.seed can only be used '
+                    b'when using the random sort key'
+                )
+            )
+
     return args[b'set'], keyflags, opts
 
 
@@ -2419,11 +2455,14 @@
     - ``date`` for the commit date
     - ``topo`` for a reverse topographical sort
     - ``node`` the nodeid of the revision
+    - ``random`` randomly shuffle revisions
 
     The ``topo`` sort order cannot be combined with other sort keys. This sort
     takes one optional argument, ``topo.firstbranch``, which takes a revset that
     specifies what topographical branches to prioritize in the sort.
 
+    The ``random`` sort takes one optional ``random.seed`` argument to control
+    the pseudo-randomness of the result.
     """
     s, keyflags, opts = _getsortargs(x)
     revs = getset(repo, subset, s, order)
@@ -2435,10 +2474,20 @@
         return revs
     elif keyflags[0][0] == b"topo":
         firstbranch = ()
+        parentrevs = repo.changelog.parentrevs
+        parentsfunc = parentrevs
+        if wdirrev in revs:
+
+            def parentsfunc(r):
+                try:
+                    return parentrevs(r)
+                except error.WdirUnsupported:
+                    return [p.rev() for p in repo[None].parents()]
+
         if b'topo.firstbranch' in opts:
             firstbranch = getset(repo, subset, opts[b'topo.firstbranch'])
         revs = baseset(
-            dagop.toposort(revs, repo.changelog.parentrevs, firstbranch),
+            dagop.toposort(revs, parentsfunc, firstbranch),
             istopo=True,
         )
         if keyflags[0][1]:
@@ -2448,7 +2497,12 @@
     # sort() is guaranteed to be stable
     ctxs = [repo[r] for r in revs]
     for k, reverse in reversed(keyflags):
-        ctxs.sort(key=_sortkeyfuncs[k], reverse=reverse)
+        func = _sortkeyfuncs[k]
+        if k == b'random' and b'random.seed' in opts:
+            seed = opts[b'random.seed']
+            r = random.Random(seed)
+            func = functools.partial(func, gen=r)
+        ctxs.sort(key=func, reverse=reverse)
     return baseset([c.rev() for c in ctxs])
 
 
--- a/mercurial/scmutil.py	Thu Oct 20 12:05:17 2022 -0400
+++ b/mercurial/scmutil.py	Mon Oct 24 15:32:14 2022 +0200
@@ -1191,7 +1191,7 @@
                 obsolete.createmarkers(
                     repo, rels, operation=operation, metadata=metadata
                 )
-        elif phases.supportinternal(repo) and mayusearchived:
+        elif phases.supportarchived(repo) and mayusearchived:
             # this assume we do not have "unstable" nodes above the cleaned ones
             allreplaced = set()
             for ns in replacements.keys():
--- a/mercurial/shelve.py	Thu Oct 20 12:05:17 2022 -0400
+++ b/mercurial/shelve.py	Mon Oct 24 15:32:14 2022 +0200
@@ -22,6 +22,7 @@
 """
 
 import collections
+import io
 import itertools
 import stat
 
@@ -98,6 +99,17 @@
         return sorted(info, reverse=True)
 
 
+def _use_internal_phase(repo):
+    return (
+        phases.supportinternal(repo)
+        and repo.ui.config(b'shelve', b'store') == b'internal'
+    )
+
+
+def _target_phase(repo):
+    return phases.internal if _use_internal_phase(repo) else phases.secret
+
+
 class Shelf:
     """Represents a shelf, including possibly multiple files storing it.
 
@@ -111,12 +123,19 @@
         self.name = name
 
     def exists(self):
-        return self.vfs.exists(self.name + b'.patch') and self.vfs.exists(
-            self.name + b'.hg'
-        )
+        return self._exists(b'.shelve') or self._exists(b'.patch', b'.hg')
+
+    def _exists(self, *exts):
+        return all(self.vfs.exists(self.name + ext) for ext in exts)
 
     def mtime(self):
-        return self.vfs.stat(self.name + b'.patch')[stat.ST_MTIME]
+        try:
+            return self._stat(b'.shelve')[stat.ST_MTIME]
+        except FileNotFoundError:
+            return self._stat(b'.patch')[stat.ST_MTIME]
+
+    def _stat(self, ext):
+        return self.vfs.stat(self.name + ext)
 
     def writeinfo(self, info):
         scmutil.simplekeyvaluefile(self.vfs, self.name + b'.shelve').write(info)
@@ -159,9 +178,7 @@
         filename = self.name + b'.hg'
         fp = self.vfs(filename)
         try:
-            targetphase = phases.internal
-            if not phases.supportinternal(repo):
-                targetphase = phases.secret
+            targetphase = _target_phase(repo)
             gen = exchange.readbundle(repo.ui, fp, filename, self.vfs)
             pretip = repo[b'tip']
             bundle2.applybundle(
@@ -183,6 +200,27 @@
     def open_patch(self, mode=b'rb'):
         return self.vfs(self.name + b'.patch', mode)
 
+    def patch_from_node(self, repo, node):
+        repo = repo.unfiltered()
+        match = _optimized_match(repo, node)
+        fp = io.BytesIO()
+        cmdutil.exportfile(
+            repo,
+            [node],
+            fp,
+            opts=mdiff.diffopts(git=True),
+            match=match,
+        )
+        fp.seek(0)
+        return fp
+
+    def load_patch(self, repo):
+        try:
+            # prefer node-based shelf
+            return self.patch_from_node(repo, self.readinfo()[b'node'])
+        except (FileNotFoundError, error.RepoLookupError):
+            return self.open_patch()
+
     def _backupfilename(self, backupvfs, filename):
         def gennames(base):
             yield base
@@ -210,6 +248,15 @@
             self.vfs.tryunlink(self.name + b'.' + ext)
 
 
+def _optimized_match(repo, node):
+    """
+    Create a matcher so that prefetch doesn't attempt to fetch
+    the entire repository pointlessly, and as an optimisation
+    for movedirstate, if needed.
+    """
+    return scmutil.matchfiles(repo, repo[node].files())
+
+
 class shelvedstate:
     """Handle persistence during unshelving operations.
 
@@ -447,9 +494,7 @@
         if hasmq:
             saved, repo.mq.checkapplied = repo.mq.checkapplied, False
 
-        targetphase = phases.internal
-        if not phases.supportinternal(repo):
-            targetphase = phases.secret
+        targetphase = _target_phase(repo)
         overrides = {(b'phases', b'new-commit'): targetphase}
         try:
             editor_ = False
@@ -510,7 +555,7 @@
 
 
 def _finishshelve(repo, tr):
-    if phases.supportinternal(repo):
+    if _use_internal_phase(repo):
         tr.close()
     else:
         _aborttransaction(repo, tr)
@@ -579,10 +624,7 @@
             _nothingtoshelvemessaging(ui, repo, pats, opts)
             return 1
 
-        # Create a matcher so that prefetch doesn't attempt to fetch
-        # the entire repository pointlessly, and as an optimisation
-        # for movedirstate, if needed.
-        match = scmutil.matchfiles(repo, repo[node].files())
+        match = _optimized_match(repo, node)
         _shelvecreatedcommit(repo, node, name, match)
 
         ui.status(_(b'shelved as %s\n') % name)
@@ -668,7 +710,7 @@
         ui.write(age, label=b'shelve.age')
         ui.write(b' ' * (12 - len(age)))
         used += 12
-        with shelf_dir.get(name).open_patch() as fp:
+        with shelf_dir.get(name).load_patch(repo) as fp:
             while True:
                 line = fp.readline()
                 if not line:
@@ -754,7 +796,7 @@
             if state.activebookmark and state.activebookmark in repo._bookmarks:
                 bookmarks.activate(repo, state.activebookmark)
             mergefiles(ui, repo, state.wctx, state.pendingctx)
-            if not phases.supportinternal(repo):
+            if not _use_internal_phase(repo):
                 repair.strip(
                     ui, repo, state.nodestoremove, backup=False, topic=b'shelve'
                 )
@@ -816,9 +858,7 @@
             repo.setparents(state.pendingctx.node(), repo.nullid)
             repo.dirstate.write(repo.currenttransaction())
 
-        targetphase = phases.internal
-        if not phases.supportinternal(repo):
-            targetphase = phases.secret
+        targetphase = _target_phase(repo)
         overrides = {(b'phases', b'new-commit'): targetphase}
         with repo.ui.configoverride(overrides, b'unshelve'):
             with repo.dirstate.parentchange():
@@ -843,7 +883,7 @@
         mergefiles(ui, repo, state.wctx, shelvectx)
         restorebranch(ui, repo, state.branchtorestore)
 
-        if not phases.supportinternal(repo):
+        if not _use_internal_phase(repo):
             repair.strip(
                 ui, repo, state.nodestoremove, backup=False, topic=b'shelve'
             )
@@ -957,7 +997,7 @@
         user=shelvectx.user(),
     )
     if snode:
-        m = scmutil.matchfiles(repo, repo[snode].files())
+        m = _optimized_match(repo, snode)
         _shelvecreatedcommit(repo, snode, basename, m)
 
     return newnode, bool(snode)
@@ -1137,7 +1177,6 @@
         oldtiprev = len(repo)
 
         pctx = repo[b'.']
-        tmpwctx = pctx
         # The goal is to have a commit structure like so:
         # ...-> pctx -> tmpwctx -> shelvectx
         # where tmpwctx is an optional commit with the user's pending changes
@@ -1145,9 +1184,7 @@
         # to the original pctx.
 
         activebookmark = _backupactivebookmark(repo)
-        tmpwctx, addedbefore = _commitworkingcopychanges(
-            ui, repo, opts, tmpwctx
-        )
+        tmpwctx, addedbefore = _commitworkingcopychanges(ui, repo, opts, pctx)
         repo, shelvectx = _unshelverestorecommit(ui, repo, tr, basename)
         _checkunshelveuntrackedproblems(ui, repo, shelvectx)
         branchtorestore = b''
--- a/mercurial/subrepo.py	Thu Oct 20 12:05:17 2022 -0400
+++ b/mercurial/subrepo.py	Mon Oct 24 15:32:14 2022 +0200
@@ -1099,6 +1099,10 @@
             # --non-interactive.
             if commands[0] in (b'update', b'checkout', b'commit'):
                 cmd.append(b'--non-interactive')
+        if util.safehasattr(subprocess, 'CREATE_NO_WINDOW'):
+            # On Windows, prevent command prompts windows from popping up when
+            # running in pythonw.
+            extrakw['creationflags'] = getattr(subprocess, 'CREATE_NO_WINDOW')
         cmd.extend(commands)
         if filename is not None:
             path = self.wvfs.reljoin(
@@ -1150,7 +1154,7 @@
         # commit revision so we can compare the subrepo state with
         # both. We used to store the working directory one.
         output, err = self._svncommand([b'info', b'--xml'])
-        doc = xml.dom.minidom.parseString(output)
+        doc = xml.dom.minidom.parseString(output)  # pytype: disable=pyi-error
         entries = doc.getElementsByTagName('entry')
         lastrev, rev = b'0', b'0'
         if entries:
@@ -1174,7 +1178,7 @@
         """
         output, err = self._svncommand([b'status', b'--xml'])
         externals, changes, missing = [], [], []
-        doc = xml.dom.minidom.parseString(output)
+        doc = xml.dom.minidom.parseString(output)  # pytype: disable=pyi-error
         for e in doc.getElementsByTagName('entry'):
             s = e.getElementsByTagName('wc-status')
             if not s:
@@ -1319,7 +1323,7 @@
     @annotatesubrepoerror
     def files(self):
         output = self._svncommand([b'list', b'--recursive', b'--xml'])[0]
-        doc = xml.dom.minidom.parseString(output)
+        doc = xml.dom.minidom.parseString(output)  # pytype: disable=pyi-error
         paths = []
         for e in doc.getElementsByTagName('entry'):
             kind = pycompat.bytestr(e.getAttribute('kind'))
@@ -1469,6 +1473,11 @@
             # insert the argument in the front,
             # the end of git diff arguments is used for paths
             commands.insert(1, b'--color')
+        extrakw = {}
+        if util.safehasattr(subprocess, 'CREATE_NO_WINDOW'):
+            # On Windows, prevent command prompts windows from popping up when
+            # running in pythonw.
+            extrakw['creationflags'] = getattr(subprocess, 'CREATE_NO_WINDOW')
         p = subprocess.Popen(
             pycompat.rapply(
                 procutil.tonativestr, [self._gitexecutable] + commands
@@ -1479,6 +1488,7 @@
             close_fds=procutil.closefds,
             stdout=subprocess.PIPE,
             stderr=errpipe,
+            **extrakw
         )
         if stream:
             return p.stdout, None
--- a/mercurial/templatefilters.py	Thu Oct 20 12:05:17 2022 -0400
+++ b/mercurial/templatefilters.py	Mon Oct 24 15:32:14 2022 +0200
@@ -390,6 +390,14 @@
     return stringutil.person(author)
 
 
+@templatefilter(b'reverse')
+def reverse(list_):
+    """List. Reverses the order of list items."""
+    if isinstance(list_, list):
+        return templateutil.hybridlist(list_[::-1], name=b'item')
+    raise error.ParseError(_(b'not reversible'))
+
+
 @templatefilter(b'revescape', intype=bytes)
 def revescape(text):
     """Any text. Escapes all "special" characters, except @.
--- a/mercurial/url.py	Thu Oct 20 12:05:17 2022 -0400
+++ b/mercurial/url.py	Mon Oct 24 15:32:14 2022 +0200
@@ -222,13 +222,16 @@
     h.headers = None
 
 
-def _generic_proxytunnel(self):
+def _generic_proxytunnel(self: "httpsconnection"):
+    headers = self.headers  # pytype: disable=attribute-error
     proxyheaders = {
-        pycompat.bytestr(x): pycompat.bytestr(self.headers[x])
-        for x in self.headers
+        pycompat.bytestr(x): pycompat.bytestr(headers[x])
+        for x in headers
         if x.lower().startswith('proxy-')
     }
-    self.send(b'CONNECT %s HTTP/1.0\r\n' % self.realhostport)
+    realhostport = self.realhostport  # pytype: disable=attribute-error
+    self.send(b'CONNECT %s HTTP/1.0\r\n' % realhostport)
+
     for header in proxyheaders.items():
         self.send(b'%s: %s\r\n' % header)
     self.send(b'\r\n')
@@ -237,10 +240,14 @@
     # httplib.HTTPConnection as there are no adequate places to
     # override functions to provide the needed functionality.
 
+    # pytype: disable=attribute-error
     res = self.response_class(self.sock, method=self._method)
+    # pytype: enable=attribute-error
 
     while True:
+        # pytype: disable=attribute-error
         version, status, reason = res._read_status()
+        # pytype: enable=attribute-error
         if status != httplib.CONTINUE:
             break
         # skip lines that are all whitespace
@@ -323,14 +330,15 @@
             self.sock = socket.create_connection((self.host, self.port))
 
             host = self.host
-            if self.realhostport:  # use CONNECT proxy
+            realhostport = self.realhostport  # pytype: disable=attribute-error
+            if realhostport:  # use CONNECT proxy
                 _generic_proxytunnel(self)
-                host = self.realhostport.rsplit(b':', 1)[0]
+                host = realhostport.rsplit(b':', 1)[0]
             self.sock = sslutil.wrapsocket(
                 self.sock,
                 self.key_file,
                 self.cert_file,
-                ui=self.ui,
+                ui=self.ui,  # pytype: disable=attribute-error
                 serverhostname=host,
             )
             sslutil.validatesocket(self.sock)
--- a/relnotes/next	Thu Oct 20 12:05:17 2022 -0400
+++ b/relnotes/next	Mon Oct 24 15:32:14 2022 +0200
@@ -13,6 +13,12 @@
 
 == Backwards Compatibility Changes ==
 
+ * chg worker processes will now correctly load per-repository configuration 
+   when given a both a relative `--repository` path and an alternate working
+   directory via `--cwd`. A side-effect of this change is that these workers
+   will now return an error if hg cannot find the current working directory,
+   even when a different directory is specified via `--cwd`.
+
 == Internal API Changes ==
 
 == Miscellaneous ==
--- a/rust/Cargo.lock	Thu Oct 20 12:05:17 2022 -0400
+++ b/rust/Cargo.lock	Mon Oct 24 15:32:14 2022 +0200
@@ -468,6 +468,7 @@
  "log",
  "memmap2",
  "micro-timer",
+ "once_cell",
  "ouroboros",
  "pretty_assertions",
  "rand 0.8.5",
@@ -687,6 +688,12 @@
 ]
 
 [[package]]
+name = "once_cell"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0"
+
+[[package]]
 name = "opaque-debug"
 version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -981,6 +988,7 @@
  "lazy_static",
  "log",
  "micro-timer",
+ "rayon",
  "regex",
  "users",
  "which",
--- a/rust/hg-core/Cargo.toml	Thu Oct 20 12:05:17 2022 -0400
+++ b/rust/hg-core/Cargo.toml	Mon Oct 24 15:32:14 2022 +0200
@@ -35,6 +35,9 @@
 memmap2 = { version = "0.5.3", features = ["stable_deref_trait"] }
 zstd = "0.5.3"
 format-bytes = "0.3.0"
+# once_cell 1.15 uses edition 2021, while the heptapod CI 
+# uses an old version of Cargo that doesn't support it.
+once_cell = "=1.14.0"
 
 # We don't use the `miniz-oxide` backend to not change rhg benchmarks and until
 # we have a clearer view of which backend is the fastest.
--- a/rust/hg-core/src/config.rs	Thu Oct 20 12:05:17 2022 -0400
+++ b/rust/hg-core/src/config.rs	Mon Oct 24 15:32:14 2022 +0200
@@ -11,6 +11,8 @@
 
 mod config;
 mod layer;
+mod plain_info;
 mod values;
 pub use config::{Config, ConfigSource, ConfigValueParseError};
 pub use layer::{ConfigError, ConfigOrigin, ConfigParseError};
+pub use plain_info::PlainInfo;
--- a/rust/hg-core/src/config/config.rs	Thu Oct 20 12:05:17 2022 -0400
+++ b/rust/hg-core/src/config/config.rs	Mon Oct 24 15:32:14 2022 +0200
@@ -12,6 +12,7 @@
 use crate::config::layer::{
     ConfigError, ConfigLayer, ConfigOrigin, ConfigValue,
 };
+use crate::config::plain_info::PlainInfo;
 use crate::utils::files::get_bytes_from_os_str;
 use format_bytes::{write_bytes, DisplayBytes};
 use std::collections::HashSet;
@@ -27,6 +28,7 @@
 #[derive(Clone)]
 pub struct Config {
     layers: Vec<layer::ConfigLayer>,
+    plain: PlainInfo,
 }
 
 impl DisplayBytes for Config {
@@ -83,17 +85,55 @@
     }
 }
 
+/// Returns true if the config item is disabled by PLAIN or PLAINEXCEPT
+fn should_ignore(plain: &PlainInfo, section: &[u8], item: &[u8]) -> bool {
+    // duplication with [_applyconfig] in [ui.py],
+    if !plain.is_plain() {
+        return false;
+    }
+    if section == b"alias" {
+        return plain.plainalias();
+    }
+    if section == b"revsetalias" {
+        return plain.plainrevsetalias();
+    }
+    if section == b"templatealias" {
+        return plain.plaintemplatealias();
+    }
+    if section == b"ui" {
+        let to_delete: &[&[u8]] = &[
+            b"debug",
+            b"fallbackencoding",
+            b"quiet",
+            b"slash",
+            b"logtemplate",
+            b"message-output",
+            b"statuscopies",
+            b"style",
+            b"traceback",
+            b"verbose",
+        ];
+        return to_delete.contains(&item);
+    }
+    let sections_to_delete: &[&[u8]] =
+        &[b"defaults", b"commands", b"command-templates"];
+    return sections_to_delete.contains(&section);
+}
+
 impl Config {
     /// The configuration to use when printing configuration-loading errors
     pub fn empty() -> Self {
-        Self { layers: Vec::new() }
+        Self {
+            layers: Vec::new(),
+            plain: PlainInfo::empty(),
+        }
     }
 
     /// Load system and user configuration from various files.
     ///
     /// This is also affected by some environment variables.
     pub fn load_non_repo() -> Result<Self, ConfigError> {
-        let mut config = Self { layers: Vec::new() };
+        let mut config = Self::empty();
         let opt_rc_path = env::var_os("HGRCPATH");
         // HGRCPATH replaces system config
         if opt_rc_path.is_none() {
@@ -266,7 +306,10 @@
             }
         }
 
-        Ok(Config { layers })
+        Ok(Config {
+            layers,
+            plain: PlainInfo::empty(),
+        })
     }
 
     /// Loads the per-repository config into a new `Config` which is combined
@@ -283,6 +326,7 @@
 
         let mut repo_config = Self {
             layers: other_layers,
+            plain: PlainInfo::empty(),
         };
         for path in repo_config_files {
             // TODO: check if this file should be trusted:
@@ -293,6 +337,10 @@
         Ok(repo_config)
     }
 
+    pub fn apply_plain(&mut self, plain: PlainInfo) {
+        self.plain = plain;
+    }
+
     fn get_parse<'config, T: 'config>(
         &'config self,
         section: &[u8],
@@ -413,10 +461,25 @@
         section: &[u8],
         item: &[u8],
     ) -> Option<(&ConfigLayer, &ConfigValue)> {
+        // Filter out the config items that are hidden by [PLAIN].
+        // This differs from python hg where we delete them from the config.
+        let should_ignore = should_ignore(&self.plain, &section, &item);
         for layer in self.layers.iter().rev() {
             if !layer.trusted {
                 continue;
             }
+            //The [PLAIN] config should not affect the defaults.
+            //
+            // However, PLAIN should also affect the "tweaked" defaults (unless
+            // "tweakdefault" is part of "HGPLAINEXCEPT").
+            //
+            // In practice the tweak-default layer is only added when it is
+            // relevant, so we can safely always take it into
+            // account here.
+            if should_ignore && !(layer.origin == ConfigOrigin::Tweakdefaults)
+            {
+                continue;
+            }
             if let Some(v) = layer.get(&section, &item) {
                 return Some((&layer, v));
             }
@@ -504,6 +567,38 @@
         }
         res
     }
+
+    // a config layer that's introduced by ui.tweakdefaults
+    fn tweakdefaults_layer() -> ConfigLayer {
+        let mut layer = ConfigLayer::new(ConfigOrigin::Tweakdefaults);
+
+        let mut add = |section: &[u8], item: &[u8], value: &[u8]| {
+            layer.add(
+                section[..].into(),
+                item[..].into(),
+                value[..].into(),
+                None,
+            );
+        };
+        // duplication of [tweakrc] from [ui.py]
+        add(b"ui", b"rollback", b"False");
+        add(b"ui", b"statuscopies", b"yes");
+        add(b"ui", b"interface", b"curses");
+        add(b"ui", b"relative-paths", b"yes");
+        add(b"commands", b"grep.all-files", b"True");
+        add(b"commands", b"update.check", b"noconflict");
+        add(b"commands", b"status.verbose", b"True");
+        add(b"commands", b"resolve.explicit-re-merge", b"True");
+        add(b"git", b"git", b"1");
+        add(b"git", b"showfunc", b"1");
+        add(b"git", b"word-diff", b"1");
+        return layer;
+    }
+
+    // introduce the tweaked defaults as implied by ui.tweakdefaults
+    pub fn tweakdefaults<'a>(&mut self) -> () {
+        self.layers.insert(0, Config::tweakdefaults_layer());
+    }
 }
 
 #[cfg(test)]
--- a/rust/hg-core/src/config/layer.rs	Thu Oct 20 12:05:17 2022 -0400
+++ b/rust/hg-core/src/config/layer.rs	Mon Oct 24 15:32:14 2022 +0200
@@ -81,6 +81,7 @@
                         String::from_utf8_lossy(arg),
                     ),
                     CONFIG_PARSE_ERROR_ABORT,
+                    None,
                 ))?
             }
         }
@@ -299,6 +300,8 @@
 pub enum ConfigOrigin {
     /// From a configuration file
     File(PathBuf),
+    /// From [ui.tweakdefaults]
+    Tweakdefaults,
     /// From a `--config` CLI argument
     CommandLine,
     /// From a `--color` CLI argument
@@ -321,6 +324,9 @@
             ConfigOrigin::CommandLine => out.write_all(b"--config"),
             ConfigOrigin::CommandLineColor => out.write_all(b"--color"),
             ConfigOrigin::Environment(e) => write_bytes!(out, b"${}", e),
+            ConfigOrigin::Tweakdefaults => {
+                write_bytes!(out, b"ui.tweakdefaults")
+            }
         }
     }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rust/hg-core/src/config/plain_info.rs	Mon Oct 24 15:32:14 2022 +0200
@@ -0,0 +1,79 @@
+use crate::utils::files::get_bytes_from_os_string;
+use std::env;
+
+/// Keeps information on whether plain mode is active.
+///
+/// Plain mode means that all configuration variables which affect
+/// the behavior and output of Mercurial should be
+/// ignored. Additionally, the output should be stable,
+/// reproducible and suitable for use in scripts or applications.
+///
+/// The only way to trigger plain mode is by setting either the
+/// `HGPLAIN' or `HGPLAINEXCEPT' environment variables.
+///
+/// The return value can either be
+/// - False if HGPLAIN is not set, or feature is in HGPLAINEXCEPT
+/// - False if feature is disabled by default and not included in HGPLAIN
+/// - True otherwise
+#[derive(Clone)]
+pub struct PlainInfo {
+    is_plain: bool,
+    except: Vec<Vec<u8>>,
+}
+
+impl PlainInfo {
+    fn plain_except(except: Vec<Vec<u8>>) -> Self {
+        PlainInfo {
+            is_plain: true,
+            except,
+        }
+    }
+
+    pub fn empty() -> PlainInfo {
+        PlainInfo {
+            is_plain: false,
+            except: vec![],
+        }
+    }
+
+    pub fn from_env() -> PlainInfo {
+        if let Some(except) = env::var_os("HGPLAINEXCEPT") {
+            PlainInfo::plain_except(
+                get_bytes_from_os_string(except)
+                    .split(|&byte| byte == b',')
+                    .map(|x| x.to_vec())
+                    .collect(),
+            )
+        } else {
+            PlainInfo {
+                is_plain: env::var_os("HGPLAIN").is_some(),
+                except: vec![],
+            }
+        }
+    }
+
+    pub fn is_feature_plain(&self, feature: &str) -> bool {
+        return self.is_plain
+            && !self
+                .except
+                .iter()
+                .any(|exception| exception.as_slice() == feature.as_bytes());
+    }
+
+    pub fn is_plain(&self) -> bool {
+        self.is_plain
+    }
+
+    pub fn plainalias(&self) -> bool {
+        self.is_feature_plain("alias")
+    }
+    pub fn plainrevsetalias(&self) -> bool {
+        self.is_feature_plain("revsetalias")
+    }
+    pub fn plaintemplatealias(&self) -> bool {
+        self.is_feature_plain("templatealias")
+    }
+    pub fn plaintweakdefaults(&self) -> bool {
+        self.is_feature_plain("tweakdefaults")
+    }
+}
--- a/rust/hg-core/src/dirstate.rs	Thu Oct 20 12:05:17 2022 -0400
+++ b/rust/hg-core/src/dirstate.rs	Mon Oct 24 15:32:14 2022 +0200
@@ -30,6 +30,10 @@
         p1: NULL_NODE,
         p2: NULL_NODE,
     };
+
+    pub fn is_merge(&self) -> bool {
+        return !(self.p2 == NULL_NODE);
+    }
 }
 
 pub type StateMapIter<'a> = Box<
--- a/rust/hg-core/src/dirstate_tree/status.rs	Thu Oct 20 12:05:17 2022 -0400
+++ b/rust/hg-core/src/dirstate_tree/status.rs	Mon Oct 24 15:32:14 2022 +0200
@@ -20,6 +20,7 @@
 use crate::StatusError;
 use crate::StatusOptions;
 use micro_timer::timed;
+use once_cell::sync::OnceCell;
 use rayon::prelude::*;
 use sha1::{Digest, Sha1};
 use std::borrow::Cow;
@@ -126,14 +127,14 @@
     };
     let is_at_repo_root = true;
     let hg_path = &BorrowedPath::OnDisk(HgPath::new(""));
-    let has_ignored_ancestor = false;
+    let has_ignored_ancestor = HasIgnoredAncestor::create(None, hg_path);
     let root_cached_mtime = None;
     let root_dir_metadata = None;
     // If the path we have for the repository root is a symlink, do follow it.
     // (As opposed to symlinks within the working directory which are not
     // followed, using `std::fs::symlink_metadata`.)
     common.traverse_fs_directory_and_dirstate(
-        has_ignored_ancestor,
+        &has_ignored_ancestor,
         dmap.root.as_ref(),
         hg_path,
         &root_dir,
@@ -196,6 +197,40 @@
     Unsure,
 }
 
+/// Lazy computation of whether a given path has a hgignored
+/// ancestor.
+struct HasIgnoredAncestor<'a> {
+    /// `path` and `parent` constitute the inputs to the computation,
+    /// `cache` stores the outcome.
+    path: &'a HgPath,
+    parent: Option<&'a HasIgnoredAncestor<'a>>,
+    cache: OnceCell<bool>,
+}
+
+impl<'a> HasIgnoredAncestor<'a> {
+    fn create(
+        parent: Option<&'a HasIgnoredAncestor<'a>>,
+        path: &'a HgPath,
+    ) -> HasIgnoredAncestor<'a> {
+        Self {
+            path,
+            parent,
+            cache: OnceCell::new(),
+        }
+    }
+
+    fn force<'b>(&self, ignore_fn: &IgnoreFnType<'b>) -> bool {
+        match self.parent {
+            None => false,
+            Some(parent) => {
+                *(parent.cache.get_or_init(|| {
+                    parent.force(ignore_fn) || ignore_fn(&self.path)
+                }))
+            }
+        }
+    }
+}
+
 impl<'a, 'tree, 'on_disk> StatusCommon<'a, 'tree, 'on_disk> {
     fn push_outcome(
         &self,
@@ -318,9 +353,9 @@
 
     /// Returns whether all child entries of the filesystem directory have a
     /// corresponding dirstate node or are ignored.
-    fn traverse_fs_directory_and_dirstate(
+    fn traverse_fs_directory_and_dirstate<'ancestor>(
         &self,
-        has_ignored_ancestor: bool,
+        has_ignored_ancestor: &'ancestor HasIgnoredAncestor<'ancestor>,
         dirstate_nodes: ChildNodesRef<'tree, 'on_disk>,
         directory_hg_path: &BorrowedPath<'tree, 'on_disk>,
         directory_fs_path: &Path,
@@ -418,7 +453,7 @@
                 }
                 Right(fs_entry) => {
                     has_dirstate_node_or_is_ignored = self.traverse_fs_only(
-                        has_ignored_ancestor,
+                        has_ignored_ancestor.force(&self.ignore_fn),
                         directory_hg_path,
                         fs_entry,
                     )
@@ -429,12 +464,12 @@
         .try_reduce(|| true, |a, b| Ok(a && b))
     }
 
-    fn traverse_fs_and_dirstate(
+    fn traverse_fs_and_dirstate<'ancestor>(
         &self,
         fs_path: &Path,
         fs_metadata: &std::fs::Metadata,
         dirstate_node: NodeRef<'tree, 'on_disk>,
-        has_ignored_ancestor: bool,
+        has_ignored_ancestor: &'ancestor HasIgnoredAncestor<'ancestor>,
     ) -> Result<(), DirstateV2ParseError> {
         self.check_for_outdated_directory_cache(&dirstate_node)?;
         let hg_path = &dirstate_node.full_path_borrowed(self.dmap.on_disk)?;
@@ -454,11 +489,14 @@
                     .traversed
                     .push(hg_path.detach_from_tree())
             }
-            let is_ignored = has_ignored_ancestor || (self.ignore_fn)(hg_path);
+            let is_ignored = HasIgnoredAncestor::create(
+                Some(&has_ignored_ancestor),
+                hg_path,
+            );
             let is_at_repo_root = false;
             let children_all_have_dirstate_node_or_are_ignored = self
                 .traverse_fs_directory_and_dirstate(
-                    is_ignored,
+                    &is_ignored,
                     dirstate_node.children(self.dmap.on_disk)?,
                     hg_path,
                     fs_path,
@@ -472,14 +510,14 @@
                 dirstate_node,
             )?
         } else {
-            if file_or_symlink && self.matcher.matches(hg_path) {
+            if file_or_symlink && self.matcher.matches(&hg_path) {
                 if let Some(entry) = dirstate_node.entry()? {
                     if !entry.any_tracked() {
                         // Forward-compat if we start tracking unknown/ignored
                         // files for caching reasons
                         self.mark_unknown_or_ignored(
-                            has_ignored_ancestor,
-                            hg_path,
+                            has_ignored_ancestor.force(&self.ignore_fn),
+                            &hg_path,
                         );
                     }
                     if entry.added() {
@@ -495,7 +533,7 @@
                     // `node.entry.is_none()` indicates a "directory"
                     // node, but the filesystem has a file
                     self.mark_unknown_or_ignored(
-                        has_ignored_ancestor,
+                        has_ignored_ancestor.force(&self.ignore_fn),
                         hg_path,
                     );
                 }
--- a/rust/hg-core/src/errors.rs	Thu Oct 20 12:05:17 2022 -0400
+++ b/rust/hg-core/src/errors.rs	Mon Oct 24 15:32:14 2022 +0200
@@ -33,6 +33,7 @@
     Abort {
         message: String,
         detailed_exit_code: exit_codes::ExitCode,
+        hint: Option<String>,
     },
 
     /// A configuration value is not in the expected syntax.
@@ -82,10 +83,12 @@
     pub fn abort(
         explanation: impl Into<String>,
         exit_code: exit_codes::ExitCode,
+        hint: Option<String>,
     ) -> Self {
         HgError::Abort {
             message: explanation.into(),
             detailed_exit_code: exit_code,
+            hint,
         }
     }
 }
--- a/rust/hg-core/src/exit_codes.rs	Thu Oct 20 12:05:17 2022 -0400
+++ b/rust/hg-core/src/exit_codes.rs	Mon Oct 24 15:32:14 2022 +0200
@@ -9,6 +9,10 @@
 // Abort when there is a config related error
 pub const CONFIG_ERROR_ABORT: ExitCode = 30;
 
+/// Indicates that the operation might work if retried in a different state.
+/// Examples: Unresolved merge conflicts, unfinished operations
+pub const STATE_ERROR: ExitCode = 20;
+
 // Abort when there is an error while parsing config
 pub const CONFIG_PARSE_ERROR_ABORT: ExitCode = 10;
 
--- a/rust/hg-core/src/filepatterns.rs	Thu Oct 20 12:05:17 2022 -0400
+++ b/rust/hg-core/src/filepatterns.rs	Mon Oct 24 15:32:14 2022 +0200
@@ -314,6 +314,8 @@
         m.insert(b"rootglob".as_ref(), b"rootglob:".as_ref());
         m.insert(b"include".as_ref(), b"include:".as_ref());
         m.insert(b"subinclude".as_ref(), b"subinclude:".as_ref());
+        m.insert(b"path".as_ref(), b"path:".as_ref());
+        m.insert(b"rootfilesin".as_ref(), b"rootfilesin:".as_ref());
         m
     };
 }
@@ -329,6 +331,7 @@
 pub fn parse_pattern_file_contents(
     lines: &[u8],
     file_path: &Path,
+    default_syntax_override: Option<&[u8]>,
     warn: bool,
 ) -> Result<(Vec<IgnorePattern>, Vec<PatternFileWarning>), PatternError> {
     let comment_regex = Regex::new(r"((?:^|[^\\])(?:\\\\)*)#.*").unwrap();
@@ -338,7 +341,8 @@
     let mut inputs: Vec<IgnorePattern> = vec![];
     let mut warnings: Vec<PatternFileWarning> = vec![];
 
-    let mut current_syntax = b"relre:".as_ref();
+    let mut current_syntax =
+        default_syntax_override.unwrap_or(b"relre:".as_ref());
 
     for (line_number, mut line) in lines.split(|c| *c == b'\n').enumerate() {
         let line_number = line_number + 1;
@@ -413,7 +417,7 @@
     match std::fs::read(file_path) {
         Ok(contents) => {
             inspect_pattern_bytes(&contents);
-            parse_pattern_file_contents(&contents, file_path, warn)
+            parse_pattern_file_contents(&contents, file_path, None, warn)
         }
         Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok((
             vec![],
@@ -601,9 +605,14 @@
         let lines = b"syntax: glob\n*.elc";
 
         assert_eq!(
-            parse_pattern_file_contents(lines, Path::new("file_path"), false)
-                .unwrap()
-                .0,
+            parse_pattern_file_contents(
+                lines,
+                Path::new("file_path"),
+                None,
+                false
+            )
+            .unwrap()
+            .0,
             vec![IgnorePattern::new(
                 PatternSyntax::RelGlob,
                 b"*.elc",
@@ -614,16 +623,26 @@
         let lines = b"syntax: include\nsyntax: glob";
 
         assert_eq!(
-            parse_pattern_file_contents(lines, Path::new("file_path"), false)
-                .unwrap()
-                .0,
+            parse_pattern_file_contents(
+                lines,
+                Path::new("file_path"),
+                None,
+                false
+            )
+            .unwrap()
+            .0,
             vec![]
         );
         let lines = b"glob:**.o";
         assert_eq!(
-            parse_pattern_file_contents(lines, Path::new("file_path"), false)
-                .unwrap()
-                .0,
+            parse_pattern_file_contents(
+                lines,
+                Path::new("file_path"),
+                None,
+                false
+            )
+            .unwrap()
+            .0,
             vec![IgnorePattern::new(
                 PatternSyntax::RelGlob,
                 b"**.o",
--- a/rust/hg-core/src/lib.rs	Thu Oct 20 12:05:17 2022 -0400
+++ b/rust/hg-core/src/lib.rs	Mon Oct 24 15:32:14 2022 +0200
@@ -7,6 +7,8 @@
 mod ancestors;
 pub mod dagops;
 pub mod errors;
+pub mod narrow;
+pub mod sparse;
 pub use ancestors::{AncestorsIterator, MissingAncestors};
 pub mod dirstate;
 pub mod dirstate_tree;
--- a/rust/hg-core/src/matchers.rs	Thu Oct 20 12:05:17 2022 -0400
+++ b/rust/hg-core/src/matchers.rs	Mon Oct 24 15:32:14 2022 +0200
@@ -46,7 +46,7 @@
     Recursive,
 }
 
-pub trait Matcher {
+pub trait Matcher: core::fmt::Debug {
     /// Explicitly listed files
     fn file_set(&self) -> Option<&HashSet<HgPathBuf>>;
     /// Returns whether `filename` is in `file_set`
@@ -283,6 +283,18 @@
     parents: HashSet<HgPathBuf>,
 }
 
+impl core::fmt::Debug for IncludeMatcher<'_> {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("IncludeMatcher")
+            .field("patterns", &String::from_utf8_lossy(&self.patterns))
+            .field("prefix", &self.prefix)
+            .field("roots", &self.roots)
+            .field("dirs", &self.dirs)
+            .field("parents", &self.parents)
+            .finish()
+    }
+}
+
 impl<'a> Matcher for IncludeMatcher<'a> {
     fn file_set(&self) -> Option<&HashSet<HgPathBuf>> {
         None
@@ -330,6 +342,7 @@
 }
 
 /// The union of multiple matchers. Will match if any of the matchers match.
+#[derive(Debug)]
 pub struct UnionMatcher {
     matchers: Vec<Box<dyn Matcher + Sync>>,
 }
@@ -393,6 +406,7 @@
     }
 }
 
+#[derive(Debug)]
 pub struct IntersectionMatcher {
     m1: Box<dyn Matcher + Sync>,
     m2: Box<dyn Matcher + Sync>,
@@ -474,6 +488,91 @@
     }
 }
 
+#[derive(Debug)]
+pub struct DifferenceMatcher {
+    base: Box<dyn Matcher + Sync>,
+    excluded: Box<dyn Matcher + Sync>,
+    files: Option<HashSet<HgPathBuf>>,
+}
+
+impl Matcher for DifferenceMatcher {
+    fn file_set(&self) -> Option<&HashSet<HgPathBuf>> {
+        self.files.as_ref()
+    }
+
+    fn exact_match(&self, filename: &HgPath) -> bool {
+        self.files.as_ref().map_or(false, |f| f.contains(filename))
+    }
+
+    fn matches(&self, filename: &HgPath) -> bool {
+        self.base.matches(filename) && !self.excluded.matches(filename)
+    }
+
+    fn visit_children_set(&self, directory: &HgPath) -> VisitChildrenSet {
+        let excluded_set = self.excluded.visit_children_set(directory);
+        if excluded_set == VisitChildrenSet::Recursive {
+            return VisitChildrenSet::Empty;
+        }
+        let base_set = self.base.visit_children_set(directory);
+        // Possible values for base: 'recursive', 'this', set(...), set()
+        // Possible values for excluded:          'this', set(...), set()
+        // If excluded has nothing under here that we care about, return base,
+        // even if it's 'recursive'.
+        if excluded_set == VisitChildrenSet::Empty {
+            return base_set;
+        }
+        match base_set {
+            VisitChildrenSet::This | VisitChildrenSet::Recursive => {
+                // Never return 'recursive' here if excluded_set is any kind of
+                // non-empty (either 'this' or set(foo)), since excluded might
+                // return set() for a subdirectory.
+                VisitChildrenSet::This
+            }
+            set => {
+                // Possible values for base:         set(...), set()
+                // Possible values for excluded: 'this', set(...)
+                // We ignore excluded set results. They're possibly incorrect:
+                //  base = path:dir/subdir
+                //  excluded=rootfilesin:dir,
+                //  visit_children_set(''):
+                //   base returns {'dir'}, excluded returns {'dir'}, if we
+                //   subtracted we'd return set(), which is *not* correct, we
+                //   still need to visit 'dir'!
+                set
+            }
+        }
+    }
+
+    fn matches_everything(&self) -> bool {
+        false
+    }
+
+    fn is_exact(&self) -> bool {
+        self.base.is_exact()
+    }
+}
+
+impl DifferenceMatcher {
+    pub fn new(
+        base: Box<dyn Matcher + Sync>,
+        excluded: Box<dyn Matcher + Sync>,
+    ) -> Self {
+        let base_is_exact = base.is_exact();
+        let base_files = base.file_set().map(ToOwned::to_owned);
+        let mut new = Self {
+            base,
+            excluded,
+            files: None,
+        };
+        if base_is_exact {
+            new.files = base_files.map(|files| {
+                files.iter().cloned().filter(|f| new.matches(f)).collect()
+            });
+        }
+        new
+    }
+}
+
 /// Returns a function that matches an `HgPath` against the given regex
 /// pattern.
 ///
@@ -1489,4 +1588,101 @@
             VisitChildrenSet::Empty
         );
     }
+
+    #[test]
+    fn test_differencematcher() {
+        // Two alwaysmatchers should function like a nevermatcher
+        let m1 = AlwaysMatcher;
+        let m2 = AlwaysMatcher;
+        let matcher = DifferenceMatcher::new(Box::new(m1), Box::new(m2));
+
+        for case in &[
+            &b""[..],
+            b"dir",
+            b"dir/subdir",
+            b"dir/subdir/z",
+            b"dir/foo",
+            b"dir/subdir/x",
+            b"folder",
+        ] {
+            assert_eq!(
+                matcher.visit_children_set(HgPath::new(case)),
+                VisitChildrenSet::Empty
+            );
+        }
+
+        // One always and one never should behave the same as an always
+        let m1 = AlwaysMatcher;
+        let m2 = NeverMatcher;
+        let matcher = DifferenceMatcher::new(Box::new(m1), Box::new(m2));
+
+        for case in &[
+            &b""[..],
+            b"dir",
+            b"dir/subdir",
+            b"dir/subdir/z",
+            b"dir/foo",
+            b"dir/subdir/x",
+            b"folder",
+        ] {
+            assert_eq!(
+                matcher.visit_children_set(HgPath::new(case)),
+                VisitChildrenSet::Recursive
+            );
+        }
+
+        // Two include matchers
+        let m1 = Box::new(
+            IncludeMatcher::new(vec![IgnorePattern::new(
+                PatternSyntax::RelPath,
+                b"dir/subdir",
+                Path::new("/repo"),
+            )])
+            .unwrap(),
+        );
+        let m2 = Box::new(
+            IncludeMatcher::new(vec![IgnorePattern::new(
+                PatternSyntax::RootFiles,
+                b"dir",
+                Path::new("/repo"),
+            )])
+            .unwrap(),
+        );
+
+        let matcher = DifferenceMatcher::new(m1, m2);
+
+        let mut set = HashSet::new();
+        set.insert(HgPathBuf::from_bytes(b"dir"));
+        assert_eq!(
+            matcher.visit_children_set(HgPath::new(b"")),
+            VisitChildrenSet::Set(set)
+        );
+
+        let mut set = HashSet::new();
+        set.insert(HgPathBuf::from_bytes(b"subdir"));
+        assert_eq!(
+            matcher.visit_children_set(HgPath::new(b"dir")),
+            VisitChildrenSet::Set(set)
+        );
+        assert_eq!(
+            matcher.visit_children_set(HgPath::new(b"dir/subdir")),
+            VisitChildrenSet::Recursive
+        );
+        assert_eq!(
+            matcher.visit_children_set(HgPath::new(b"dir/foo")),
+            VisitChildrenSet::Empty
+        );
+        assert_eq!(
+            matcher.visit_children_set(HgPath::new(b"folder")),
+            VisitChildrenSet::Empty
+        );
+        assert_eq!(
+            matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
+            VisitChildrenSet::This
+        );
+        assert_eq!(
+            matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
+            VisitChildrenSet::This
+        );
+    }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rust/hg-core/src/narrow.rs	Mon Oct 24 15:32:14 2022 +0200
@@ -0,0 +1,111 @@
+use std::path::Path;
+
+use crate::{
+    errors::HgError,
+    exit_codes,
+    filepatterns::parse_pattern_file_contents,
+    matchers::{
+        AlwaysMatcher, DifferenceMatcher, IncludeMatcher, Matcher,
+        NeverMatcher,
+    },
+    repo::Repo,
+    requirements::NARROW_REQUIREMENT,
+    sparse::{self, SparseConfigError, SparseWarning},
+};
+
+/// The file in .hg/store/ that indicates which paths exit in the store
+const FILENAME: &str = "narrowspec";
+/// The file in .hg/ that indicates which paths exit in the dirstate
+const DIRSTATE_FILENAME: &str = "narrowspec.dirstate";
+
+/// Pattern prefixes that are allowed in narrow patterns. This list MUST
+/// only contain patterns that are fast and safe to evaluate. Keep in mind
+/// that patterns are supplied by clients and executed on remote servers
+/// as part of wire protocol commands. That means that changes to this
+/// data structure influence the wire protocol and should not be taken
+/// lightly - especially removals.
+const VALID_PREFIXES: [&str; 2] = ["path:", "rootfilesin:"];
+
+/// Return the matcher for the current narrow spec, and all configuration
+/// warnings to display.
+pub fn matcher(
+    repo: &Repo,
+) -> Result<(Box<dyn Matcher + Sync>, Vec<SparseWarning>), SparseConfigError> {
+    let mut warnings = vec![];
+    if !repo.requirements().contains(NARROW_REQUIREMENT) {
+        return Ok((Box::new(AlwaysMatcher), warnings));
+    }
+    // Treat "narrowspec does not exist" the same as "narrowspec file exists
+    // and is empty".
+    let store_spec = repo.store_vfs().try_read(FILENAME)?.unwrap_or(vec![]);
+    let working_copy_spec =
+        repo.hg_vfs().try_read(DIRSTATE_FILENAME)?.unwrap_or(vec![]);
+    if store_spec != working_copy_spec {
+        return Err(HgError::abort(
+            "working copy's narrowspec is stale",
+            exit_codes::STATE_ERROR,
+            Some("run 'hg tracked --update-working-copy'".into()),
+        )
+        .into());
+    }
+
+    let config = sparse::parse_config(
+        &store_spec,
+        sparse::SparseConfigContext::Narrow,
+    )?;
+
+    warnings.extend(config.warnings);
+
+    if !config.profiles.is_empty() {
+        // TODO (from Python impl) maybe do something with profiles?
+        return Err(SparseConfigError::IncludesInNarrow);
+    }
+    validate_patterns(&config.includes)?;
+    validate_patterns(&config.excludes)?;
+
+    if config.includes.is_empty() {
+        return Ok((Box::new(NeverMatcher), warnings));
+    }
+
+    let (patterns, subwarnings) = parse_pattern_file_contents(
+        &config.includes,
+        Path::new(""),
+        None,
+        false,
+    )?;
+    warnings.extend(subwarnings.into_iter().map(From::from));
+
+    let mut m: Box<dyn Matcher + Sync> =
+        Box::new(IncludeMatcher::new(patterns)?);
+
+    let (patterns, subwarnings) = parse_pattern_file_contents(
+        &config.excludes,
+        Path::new(""),
+        None,
+        false,
+    )?;
+    if !patterns.is_empty() {
+        warnings.extend(subwarnings.into_iter().map(From::from));
+        let exclude_matcher = Box::new(IncludeMatcher::new(patterns)?);
+        m = Box::new(DifferenceMatcher::new(m, exclude_matcher));
+    }
+
+    Ok((m, warnings))
+}
+
+fn validate_patterns(patterns: &[u8]) -> Result<(), SparseConfigError> {
+    for pattern in patterns.split(|c| *c == b'\n') {
+        if pattern.is_empty() {
+            continue;
+        }
+        for prefix in VALID_PREFIXES.iter() {
+            if pattern.starts_with(prefix.as_bytes()) {
+                break;
+            }
+            return Err(SparseConfigError::InvalidNarrowPrefix(
+                pattern.to_owned(),
+            ));
+        }
+    }
+    Ok(())
+}
--- a/rust/hg-core/src/revlog/filelog.rs	Thu Oct 20 12:05:17 2022 -0400
+++ b/rust/hg-core/src/revlog/filelog.rs	Mon Oct 24 15:32:14 2022 +0200
@@ -17,18 +17,21 @@
 }
 
 impl Filelog {
-    pub fn open(repo: &Repo, file_path: &HgPath) -> Result<Self, HgError> {
+    pub fn open_vfs(
+        store_vfs: &crate::vfs::Vfs<'_>,
+        file_path: &HgPath,
+    ) -> Result<Self, HgError> {
         let index_path = store_path(file_path, b".i");
         let data_path = store_path(file_path, b".d");
-        let revlog = Revlog::open(
-            &repo.store_vfs(),
-            index_path,
-            Some(&data_path),
-            false,
-        )?;
+        let revlog =
+            Revlog::open(store_vfs, index_path, Some(&data_path), false)?;
         Ok(Self { revlog })
     }
 
+    pub fn open(repo: &Repo, file_path: &HgPath) -> Result<Self, HgError> {
+        Self::open_vfs(&repo.store_vfs(), file_path)
+    }
+
     /// The given node ID is that of the file as found in a filelog, not of a
     /// changeset.
     pub fn data_for_node(
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rust/hg-core/src/sparse.rs	Mon Oct 24 15:32:14 2022 +0200
@@ -0,0 +1,338 @@
+use std::{collections::HashSet, path::Path};
+
+use format_bytes::{write_bytes, DisplayBytes};
+
+use crate::{
+    errors::HgError,
+    filepatterns::parse_pattern_file_contents,
+    matchers::{
+        AlwaysMatcher, DifferenceMatcher, IncludeMatcher, Matcher,
+        UnionMatcher,
+    },
+    operations::cat,
+    repo::Repo,
+    requirements::SPARSE_REQUIREMENT,
+    utils::{hg_path::HgPath, SliceExt},
+    IgnorePattern, PatternError, PatternFileWarning, PatternSyntax, Revision,
+    NULL_REVISION,
+};
+
+/// Command which is triggering the config read
+#[derive(Copy, Clone, Debug)]
+pub enum SparseConfigContext {
+    Sparse,
+    Narrow,
+}
+
+impl DisplayBytes for SparseConfigContext {
+    fn display_bytes(
+        &self,
+        output: &mut dyn std::io::Write,
+    ) -> std::io::Result<()> {
+        match self {
+            SparseConfigContext::Sparse => write_bytes!(output, b"sparse"),
+            SparseConfigContext::Narrow => write_bytes!(output, b"narrow"),
+        }
+    }
+}
+
+/// Possible warnings when reading sparse configuration
+#[derive(Debug, derive_more::From)]
+pub enum SparseWarning {
+    /// Warns about improper paths that start with "/"
+    RootWarning {
+        context: SparseConfigContext,
+        line: Vec<u8>,
+    },
+    /// Warns about a profile missing from the given changelog revision
+    ProfileNotFound { profile: Vec<u8>, rev: Revision },
+    #[from]
+    Pattern(PatternFileWarning),
+}
+
+/// Parsed sparse config
+#[derive(Debug, Default)]
+pub struct SparseConfig {
+    // Line-separated
+    pub(crate) includes: Vec<u8>,
+    // Line-separated
+    pub(crate) excludes: Vec<u8>,
+    pub(crate) profiles: HashSet<Vec<u8>>,
+    pub(crate) warnings: Vec<SparseWarning>,
+}
+
+/// All possible errors when reading sparse/narrow config
+#[derive(Debug, derive_more::From)]
+pub enum SparseConfigError {
+    IncludesAfterExcludes {
+        context: SparseConfigContext,
+    },
+    EntryOutsideSection {
+        context: SparseConfigContext,
+        line: Vec<u8>,
+    },
+    /// Narrow config does not support '%include' directives
+    IncludesInNarrow,
+    /// An invalid pattern prefix was given to the narrow spec. Includes the
+    /// entire pattern for context.
+    InvalidNarrowPrefix(Vec<u8>),
+    #[from]
+    HgError(HgError),
+    #[from]
+    PatternError(PatternError),
+}
+
+/// Parse sparse config file content.
+pub(crate) fn parse_config(
+    raw: &[u8],
+    context: SparseConfigContext,
+) -> Result<SparseConfig, SparseConfigError> {
+    let mut includes = vec![];
+    let mut excludes = vec![];
+    let mut profiles = HashSet::new();
+    let mut warnings = vec![];
+
+    #[derive(PartialEq, Eq)]
+    enum Current {
+        Includes,
+        Excludes,
+        None,
+    }
+
+    let mut current = Current::None;
+    let mut in_section = false;
+
+    for line in raw.split(|c| *c == b'\n') {
+        let line = line.trim();
+        if line.is_empty() || line[0] == b'#' {
+            // empty or comment line, skip
+            continue;
+        }
+        if line.starts_with(b"%include ") {
+            let profile = line[b"%include ".len()..].trim();
+            if !profile.is_empty() {
+                profiles.insert(profile.into());
+            }
+        } else if line == b"[include]" {
+            if in_section && current == Current::Includes {
+                return Err(SparseConfigError::IncludesAfterExcludes {
+                    context,
+                });
+            }
+            in_section = true;
+            current = Current::Includes;
+            continue;
+        } else if line == b"[exclude]" {
+            in_section = true;
+            current = Current::Excludes;
+        } else {
+            if current == Current::None {
+                return Err(SparseConfigError::EntryOutsideSection {
+                    context,
+                    line: line.into(),
+                });
+            }
+            if line.trim().starts_with(b"/") {
+                warnings.push(SparseWarning::RootWarning {
+                    context,
+                    line: line.into(),
+                });
+                continue;
+            }
+            match current {
+                Current::Includes => {
+                    includes.push(b'\n');
+                    includes.extend(line.iter());
+                }
+                Current::Excludes => {
+                    excludes.push(b'\n');
+                    excludes.extend(line.iter());
+                }
+                Current::None => unreachable!(),
+            }
+        }
+    }
+
+    Ok(SparseConfig {
+        includes,
+        excludes,
+        profiles,
+        warnings,
+    })
+}
+
+fn read_temporary_includes(
+    repo: &Repo,
+) -> Result<Vec<Vec<u8>>, SparseConfigError> {
+    let raw = repo.hg_vfs().try_read("tempsparse")?.unwrap_or(vec![]);
+    if raw.is_empty() {
+        return Ok(vec![]);
+    }
+    Ok(raw.split(|c| *c == b'\n').map(ToOwned::to_owned).collect())
+}
+
+/// Obtain sparse checkout patterns for the given revision
+fn patterns_for_rev(
+    repo: &Repo,
+    rev: Revision,
+) -> Result<Option<SparseConfig>, SparseConfigError> {
+    if !repo.has_sparse() {
+        return Ok(None);
+    }
+    let raw = repo.hg_vfs().try_read("sparse")?.unwrap_or(vec![]);
+
+    if raw.is_empty() {
+        return Ok(None);
+    }
+
+    let mut config = parse_config(&raw, SparseConfigContext::Sparse)?;
+
+    if !config.profiles.is_empty() {
+        let mut profiles: Vec<Vec<u8>> = config.profiles.into_iter().collect();
+        let mut visited = HashSet::new();
+
+        while let Some(profile) = profiles.pop() {
+            if visited.contains(&profile) {
+                continue;
+            }
+            visited.insert(profile.to_owned());
+
+            let output =
+                cat(repo, &rev.to_string(), vec![HgPath::new(&profile)])
+                    .map_err(|_| {
+                        HgError::corrupted(format!(
+                            "dirstate points to non-existent parent node"
+                        ))
+                    })?;
+            if output.results.is_empty() {
+                config.warnings.push(SparseWarning::ProfileNotFound {
+                    profile: profile.to_owned(),
+                    rev,
+                })
+            }
+
+            let subconfig = parse_config(
+                &output.results[0].1,
+                SparseConfigContext::Sparse,
+            )?;
+            if !subconfig.includes.is_empty() {
+                config.includes.push(b'\n');
+                config.includes.extend(&subconfig.includes);
+            }
+            if !subconfig.includes.is_empty() {
+                config.includes.push(b'\n');
+                config.excludes.extend(&subconfig.excludes);
+            }
+            config.warnings.extend(subconfig.warnings.into_iter());
+            profiles.extend(subconfig.profiles.into_iter());
+        }
+
+        config.profiles = visited;
+    }
+
+    if !config.includes.is_empty() {
+        config.includes.extend(b"\n.hg*");
+    }
+
+    Ok(Some(config))
+}
+
+/// Obtain a matcher for sparse working directories.
+pub fn matcher(
+    repo: &Repo,
+) -> Result<(Box<dyn Matcher + Sync>, Vec<SparseWarning>), SparseConfigError> {
+    let mut warnings = vec![];
+    if !repo.requirements().contains(SPARSE_REQUIREMENT) {
+        return Ok((Box::new(AlwaysMatcher), warnings));
+    }
+
+    let parents = repo.dirstate_parents()?;
+    let mut revs = vec![];
+    let p1_rev =
+        repo.changelog()?
+            .rev_from_node(parents.p1.into())
+            .map_err(|_| {
+                HgError::corrupted(format!(
+                    "dirstate points to non-existent parent node"
+                ))
+            })?;
+    if p1_rev != NULL_REVISION {
+        revs.push(p1_rev)
+    }
+    let p2_rev =
+        repo.changelog()?
+            .rev_from_node(parents.p2.into())
+            .map_err(|_| {
+                HgError::corrupted(format!(
+                    "dirstate points to non-existent parent node"
+                ))
+            })?;
+    if p2_rev != NULL_REVISION {
+        revs.push(p2_rev)
+    }
+    let mut matchers = vec![];
+
+    for rev in revs.iter() {
+        let config = patterns_for_rev(repo, *rev);
+        if let Ok(Some(config)) = config {
+            warnings.extend(config.warnings);
+            let mut m: Box<dyn Matcher + Sync> = Box::new(AlwaysMatcher);
+            if !config.includes.is_empty() {
+                let (patterns, subwarnings) = parse_pattern_file_contents(
+                    &config.includes,
+                    Path::new(""),
+                    Some(b"relglob:".as_ref()),
+                    false,
+                )?;
+                warnings.extend(subwarnings.into_iter().map(From::from));
+                m = Box::new(IncludeMatcher::new(patterns)?);
+            }
+            if !config.excludes.is_empty() {
+                let (patterns, subwarnings) = parse_pattern_file_contents(
+                    &config.excludes,
+                    Path::new(""),
+                    Some(b"relglob:".as_ref()),
+                    false,
+                )?;
+                warnings.extend(subwarnings.into_iter().map(From::from));
+                m = Box::new(DifferenceMatcher::new(
+                    m,
+                    Box::new(IncludeMatcher::new(patterns)?),
+                ));
+            }
+            matchers.push(m);
+        }
+    }
+    let result: Box<dyn Matcher + Sync> = match matchers.len() {
+        0 => Box::new(AlwaysMatcher),
+        1 => matchers.pop().expect("1 is equal to 0"),
+        _ => Box::new(UnionMatcher::new(matchers)),
+    };
+
+    let matcher =
+        force_include_matcher(result, &read_temporary_includes(repo)?)?;
+    Ok((matcher, warnings))
+}
+
+/// Returns a matcher that returns true for any of the forced includes before
+/// testing against the actual matcher
+fn force_include_matcher(
+    result: Box<dyn Matcher + Sync>,
+    temp_includes: &[Vec<u8>],
+) -> Result<Box<dyn Matcher + Sync>, PatternError> {
+    if temp_includes.is_empty() {
+        return Ok(result);
+    }
+    let forced_include_matcher = IncludeMatcher::new(
+        temp_includes
+            .into_iter()
+            .map(|include| {
+                IgnorePattern::new(PatternSyntax::Path, include, Path::new(""))
+            })
+            .collect(),
+    )?;
+    Ok(Box::new(UnionMatcher::new(vec![
+        Box::new(forced_include_matcher),
+        result,
+    ])))
+}
--- a/rust/hg-core/src/vfs.rs	Thu Oct 20 12:05:17 2022 -0400
+++ b/rust/hg-core/src/vfs.rs	Mon Oct 24 15:32:14 2022 +0200
@@ -40,6 +40,23 @@
         std::fs::read(&path).when_reading_file(&path)
     }
 
+    /// Returns `Ok(None)` if the file does not exist.
+    pub fn try_read(
+        &self,
+        relative_path: impl AsRef<Path>,
+    ) -> Result<Option<Vec<u8>>, HgError> {
+        match self.read(relative_path) {
+            Err(e) => match &e {
+                HgError::IoError { error, .. } => match error.kind() {
+                    ErrorKind::NotFound => return Ok(None),
+                    _ => Err(e),
+                },
+                _ => Err(e),
+            },
+            Ok(v) => Ok(Some(v)),
+        }
+    }
+
     fn mmap_open_gen(
         &self,
         relative_path: impl AsRef<Path>,
--- a/rust/hg-cpython/src/dirstate/status.rs	Thu Oct 20 12:05:17 2022 -0400
+++ b/rust/hg-cpython/src/dirstate/status.rs	Mon Oct 24 15:32:14 2022 +0200
@@ -15,7 +15,10 @@
     PyResult, PyTuple, Python, PythonObject, ToPyObject,
 };
 use hg::dirstate::status::StatusPath;
-use hg::matchers::{IntersectionMatcher, Matcher, NeverMatcher, UnionMatcher};
+use hg::matchers::{
+    DifferenceMatcher, IntersectionMatcher, Matcher, NeverMatcher,
+    UnionMatcher,
+};
 use hg::{
     matchers::{AlwaysMatcher, FileMatcher, IncludeMatcher},
     parse_pattern_syntax,
@@ -233,6 +236,12 @@
 
             Ok(Box::new(IntersectionMatcher::new(m1, m2)))
         }
+        "differencematcher" => {
+            let m1 = extract_matcher(py, matcher.getattr(py, "_m1")?)?;
+            let m2 = extract_matcher(py, matcher.getattr(py, "_m2")?)?;
+
+            Ok(Box::new(DifferenceMatcher::new(m1, m2)))
+        }
         e => Err(PyErr::new::<FallbackError, _>(
             py,
             format!("Unsupported matcher {}", e),
--- a/rust/rhg/Cargo.toml	Thu Oct 20 12:05:17 2022 -0400
+++ b/rust/rhg/Cargo.toml	Mon Oct 24 15:32:14 2022 +0200
@@ -22,3 +22,4 @@
 format-bytes = "0.3.0"
 users = "0.11.0"
 which = "4.2.5"
+rayon = "1.5.1"
--- a/rust/rhg/src/commands/debugdata.rs	Thu Oct 20 12:05:17 2022 -0400
+++ b/rust/rhg/src/commands/debugdata.rs	Mon Oct 24 15:32:14 2022 +0200
@@ -55,6 +55,11 @@
         };
 
     let repo = invocation.repo?;
+    if repo.has_narrow() {
+        return Err(CommandError::unsupported(
+            "support for ellipsis nodes is missing and repo has narrow enabled",
+        ));
+    }
     let data = debug_data(repo, rev, kind).map_err(|e| (e, rev))?;
 
     let mut stdout = invocation.ui.stdout_buffer();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rust/rhg/src/commands/debugrhgsparse.rs	Mon Oct 24 15:32:14 2022 +0200
@@ -0,0 +1,43 @@
+use std::os::unix::prelude::OsStrExt;
+
+use crate::error::CommandError;
+use clap::SubCommand;
+use hg::{self, utils::hg_path::HgPath};
+
+pub const HELP_TEXT: &str = "";
+
+pub fn args() -> clap::App<'static, 'static> {
+    SubCommand::with_name("debugrhgsparse")
+        .arg(
+            clap::Arg::with_name("files")
+                .required(true)
+                .multiple(true)
+                .empty_values(false)
+                .value_name("FILES")
+                .help("Files to check against sparse profile"),
+        )
+        .about(HELP_TEXT)
+}
+
+pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
+    let repo = invocation.repo?;
+
+    let (matcher, _warnings) = hg::sparse::matcher(&repo).unwrap();
+    let files = invocation.subcommand_args.values_of_os("files");
+    if let Some(files) = files {
+        for file in files {
+            invocation.ui.write_stdout(b"matches: ")?;
+            invocation.ui.write_stdout(
+                if matcher.matches(HgPath::new(file.as_bytes())) {
+                    b"yes"
+                } else {
+                    b"no"
+                },
+            )?;
+            invocation.ui.write_stdout(b" | file: ")?;
+            invocation.ui.write_stdout(file.as_bytes())?;
+            invocation.ui.write_stdout(b"\n")?;
+        }
+    }
+    Ok(())
+}
--- a/rust/rhg/src/commands/status.rs	Thu Oct 20 12:05:17 2022 -0400
+++ b/rust/rhg/src/commands/status.rs	Mon Oct 24 15:32:14 2022 +0200
@@ -10,7 +10,6 @@
 use crate::utils::path_utils::RelativizePaths;
 use clap::{Arg, SubCommand};
 use format_bytes::format_bytes;
-use hg;
 use hg::config::Config;
 use hg::dirstate::has_exec_bit;
 use hg::dirstate::status::StatusPath;
@@ -18,7 +17,7 @@
 use hg::errors::{HgError, IoResultExt};
 use hg::lock::LockError;
 use hg::manifest::Manifest;
-use hg::matchers::AlwaysMatcher;
+use hg::matchers::{AlwaysMatcher, IntersectionMatcher};
 use hg::repo::Repo;
 use hg::utils::files::get_bytes_from_os_string;
 use hg::utils::files::get_bytes_from_path;
@@ -28,7 +27,9 @@
 use hg::PatternFileWarning;
 use hg::StatusError;
 use hg::StatusOptions;
+use hg::{self, narrow, sparse};
 use log::info;
+use rayon::prelude::*;
 use std::io;
 use std::path::PathBuf;
 
@@ -104,6 +105,12 @@
                 .short("-n")
                 .long("--no-status"),
         )
+        .arg(
+            Arg::with_name("verbose")
+                .help("enable additional output")
+                .short("-v")
+                .long("--verbose"),
+        )
 }
 
 /// Pure data type allowing the caller to specify file states to display
@@ -150,18 +157,35 @@
     }
 }
 
+fn has_unfinished_merge(repo: &Repo) -> Result<bool, CommandError> {
+    return Ok(repo.dirstate_parents()?.is_merge());
+}
+
+fn has_unfinished_state(repo: &Repo) -> Result<bool, CommandError> {
+    // These are all the known values for the [fname] argument of
+    // [addunfinished] function in [state.py]
+    let known_state_files: &[&str] = &[
+        "bisect.state",
+        "graftstate",
+        "histedit-state",
+        "rebasestate",
+        "shelvedstate",
+        "transplant/journal",
+        "updatestate",
+    ];
+    if has_unfinished_merge(repo)? {
+        return Ok(true);
+    };
+    for f in known_state_files {
+        if repo.hg_vfs().join(f).exists() {
+            return Ok(true);
+        }
+    }
+    return Ok(false);
+}
+
 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
     // TODO: lift these limitations
-    if invocation.config.get_bool(b"ui", b"tweakdefaults")? {
-        return Err(CommandError::unsupported(
-            "ui.tweakdefaults is not yet supported with rhg status",
-        ));
-    }
-    if invocation.config.get_bool(b"ui", b"statuscopies")? {
-        return Err(CommandError::unsupported(
-            "ui.statuscopies is not yet supported with rhg status",
-        ));
-    }
     if invocation
         .config
         .get(b"commands", b"status.terse")
@@ -176,15 +200,10 @@
     let config = invocation.config;
     let args = invocation.subcommand_args;
 
-    let verbose = !ui.plain(None)
-        && !args.is_present("print0")
-        && (config.get_bool(b"ui", b"verbose")?
+    let verbose = !args.is_present("print0")
+        && (args.is_present("verbose")
+            || config.get_bool(b"ui", b"verbose")?
             || config.get_bool(b"commands", b"status.verbose")?);
-    if verbose {
-        return Err(CommandError::unsupported(
-            "verbose status is not supported yet",
-        ));
-    }
 
     let all = args.is_present("all");
     let display_states = if all {
@@ -214,10 +233,12 @@
 
     let repo = invocation.repo?;
 
-    if repo.has_sparse() || repo.has_narrow() {
-        return Err(CommandError::unsupported(
-            "rhg status is not supported for sparse checkouts or narrow clones yet"
-        ));
+    if verbose {
+        if has_unfinished_state(repo)? {
+            return Err(CommandError::unsupported(
+                "verbose status output is not supported by rhg (and is needed because we're in an unfinished operation)",
+            ));
+        };
     }
 
     let mut dmap = repo.dirstate_map_mut()?;
@@ -239,28 +260,7 @@
     let after_status = |res: StatusResult| -> Result<_, CommandError> {
         let (mut ds_status, pattern_warnings) = res?;
         for warning in pattern_warnings {
-            match warning {
-                hg::PatternFileWarning::InvalidSyntax(path, syntax) => ui
-                    .write_stderr(&format_bytes!(
-                        b"{}: ignoring invalid syntax '{}'\n",
-                        get_bytes_from_path(path),
-                        &*syntax
-                    ))?,
-                hg::PatternFileWarning::NoSuchFile(path) => {
-                    let path = if let Ok(relative) =
-                        path.strip_prefix(repo.working_directory_path())
-                    {
-                        relative
-                    } else {
-                        &*path
-                    };
-                    ui.write_stderr(&format_bytes!(
-                        b"skipping unreadable pattern file '{}': \
-                          No such file or directory\n",
-                        get_bytes_from_path(path),
-                    ))?
-                }
-            }
+            ui.write_stderr(&print_pattern_file_warning(&warning, &repo))?;
         }
 
         for (path, error) in ds_status.bad {
@@ -292,23 +292,37 @@
             let manifest = repo.manifest_for_node(p1).map_err(|e| {
                 CommandError::from((e, &*format!("{:x}", p1.short())))
             })?;
-            for to_check in ds_status.unsure {
-                if unsure_is_modified(repo, &manifest, &to_check.path)? {
+            let working_directory_vfs = repo.working_directory_vfs();
+            let store_vfs = repo.store_vfs();
+            let res: Vec<_> = ds_status
+                .unsure
+                .into_par_iter()
+                .map(|to_check| {
+                    unsure_is_modified(
+                        working_directory_vfs,
+                        store_vfs,
+                        &manifest,
+                        &to_check.path,
+                    )
+                    .map(|modified| (to_check, modified))
+                })
+                .collect::<Result<_, _>>()?;
+            for (status_path, is_modified) in res.into_iter() {
+                if is_modified {
                     if display_states.modified {
-                        ds_status.modified.push(to_check);
+                        ds_status.modified.push(status_path);
                     }
                 } else {
                     if display_states.clean {
-                        ds_status.clean.push(to_check.clone());
+                        ds_status.clean.push(status_path.clone());
                     }
-                    fixup.push(to_check.path.into_owned())
+                    fixup.push(status_path.path.into_owned())
                 }
             }
         }
-        let relative_paths = (!ui.plain(None))
-            && config
-                .get_option(b"commands", b"status.relative")?
-                .unwrap_or(config.get_bool(b"ui", b"relative-paths")?);
+        let relative_paths = config
+            .get_option(b"commands", b"status.relative")?
+            .unwrap_or(config.get_bool(b"ui", b"relative-paths")?);
         let output = DisplayStatusPaths {
             ui,
             no_status,
@@ -350,9 +364,45 @@
             filesystem_time_at_status_start,
         ))
     };
+    let (narrow_matcher, narrow_warnings) = narrow::matcher(repo)?;
+    let (sparse_matcher, sparse_warnings) = sparse::matcher(repo)?;
+    let matcher = match (repo.has_narrow(), repo.has_sparse()) {
+        (true, true) => {
+            Box::new(IntersectionMatcher::new(narrow_matcher, sparse_matcher))
+        }
+        (true, false) => narrow_matcher,
+        (false, true) => sparse_matcher,
+        (false, false) => Box::new(AlwaysMatcher),
+    };
+
+    for warning in narrow_warnings.into_iter().chain(sparse_warnings) {
+        match &warning {
+            sparse::SparseWarning::RootWarning { context, line } => {
+                let msg = format_bytes!(
+                    b"warning: {} profile cannot use paths \"
+                    starting with /, ignoring {}\n",
+                    context,
+                    line
+                );
+                ui.write_stderr(&msg)?;
+            }
+            sparse::SparseWarning::ProfileNotFound { profile, rev } => {
+                let msg = format_bytes!(
+                    b"warning: sparse profile '{}' not found \"
+                    in rev {} - ignoring it\n",
+                    profile,
+                    rev
+                );
+                ui.write_stderr(&msg)?;
+            }
+            sparse::SparseWarning::Pattern(e) => {
+                ui.write_stderr(&print_pattern_file_warning(e, &repo))?;
+            }
+        }
+    }
     let (fixup, mut dirstate_write_needed, filesystem_time_at_status_start) =
         dmap.with_status(
-            &AlwaysMatcher,
+            matcher.as_ref(),
             repo.working_directory_path().to_owned(),
             ignore_files(repo, config),
             options,
@@ -491,11 +541,12 @@
 /// This meant to be used for those that the dirstate cannot resolve, due
 /// to time resolution limits.
 fn unsure_is_modified(
-    repo: &Repo,
+    working_directory_vfs: hg::vfs::Vfs,
+    store_vfs: hg::vfs::Vfs,
     manifest: &Manifest,
     hg_path: &HgPath,
 ) -> Result<bool, HgError> {
-    let vfs = repo.working_directory_vfs();
+    let vfs = working_directory_vfs;
     let fs_path = hg_path_to_path_buf(hg_path).expect("HgPath conversion");
     let fs_metadata = vfs.symlink_metadata(&fs_path)?;
     let is_symlink = fs_metadata.file_type().is_symlink();
@@ -515,7 +566,7 @@
     if entry.flags != fs_flags {
         return Ok(true);
     }
-    let filelog = repo.filelog(hg_path)?;
+    let filelog = hg::filelog::Filelog::open_vfs(&store_vfs, hg_path)?;
     let fs_len = fs_metadata.len();
     let file_node = entry.node_id()?;
     let filelog_entry = filelog.entry_for_node(file_node).map_err(|_| {
@@ -545,3 +596,30 @@
     };
     Ok(p1_contents != &*fs_contents)
 }
+
+fn print_pattern_file_warning(
+    warning: &PatternFileWarning,
+    repo: &Repo,
+) -> Vec<u8> {
+    match warning {
+        PatternFileWarning::InvalidSyntax(path, syntax) => format_bytes!(
+            b"{}: ignoring invalid syntax '{}'\n",
+            get_bytes_from_path(path),
+            &*syntax
+        ),
+        PatternFileWarning::NoSuchFile(path) => {
+            let path = if let Ok(relative) =
+                path.strip_prefix(repo.working_directory_path())
+            {
+                relative
+            } else {
+                &*path
+            };
+            format_bytes!(
+                b"skipping unreadable pattern file '{}': \
+                    No such file or directory\n",
+                get_bytes_from_path(path),
+            )
+        }
+    }
+}
--- a/rust/rhg/src/error.rs	Thu Oct 20 12:05:17 2022 -0400
+++ b/rust/rhg/src/error.rs	Mon Oct 24 15:32:14 2022 +0200
@@ -8,6 +8,7 @@
 use hg::exit_codes;
 use hg::repo::RepoError;
 use hg::revlog::revlog::RevlogError;
+use hg::sparse::SparseConfigError;
 use hg::utils::files::get_bytes_from_path;
 use hg::{DirstateError, DirstateMapError, StatusError};
 use std::convert::From;
@@ -19,6 +20,7 @@
     Abort {
         message: Vec<u8>,
         detailed_exit_code: exit_codes::ExitCode,
+        hint: Option<Vec<u8>>,
     },
 
     /// Exit with a failure exit code but no message.
@@ -49,6 +51,32 @@
             // https://www.mercurial-scm.org/wiki/EncodingStrategy#Mixing_output
             message: utf8_to_local(message.as_ref()).into(),
             detailed_exit_code: detailed_exit_code,
+            hint: None,
+        }
+    }
+
+    pub fn abort_with_exit_code_and_hint(
+        message: impl AsRef<str>,
+        detailed_exit_code: exit_codes::ExitCode,
+        hint: Option<impl AsRef<str>>,
+    ) -> Self {
+        CommandError::Abort {
+            message: utf8_to_local(message.as_ref()).into(),
+            detailed_exit_code,
+            hint: hint.map(|h| utf8_to_local(h.as_ref()).into()),
+        }
+    }
+
+    pub fn abort_with_exit_code_bytes(
+        message: impl AsRef<[u8]>,
+        detailed_exit_code: exit_codes::ExitCode,
+    ) -> Self {
+        // TODO: use this everywhere it makes sense instead of the string
+        // version.
+        CommandError::Abort {
+            message: message.as_ref().into(),
+            detailed_exit_code,
+            hint: None,
         }
     }
 
@@ -79,9 +107,12 @@
             HgError::Abort {
                 message,
                 detailed_exit_code,
-            } => {
-                CommandError::abort_with_exit_code(message, detailed_exit_code)
-            }
+                hint,
+            } => CommandError::abort_with_exit_code_and_hint(
+                message,
+                detailed_exit_code,
+                hint,
+            ),
             _ => CommandError::abort(error.to_string()),
         }
     }
@@ -108,13 +139,15 @@
 impl From<RepoError> for CommandError {
     fn from(error: RepoError) -> Self {
         match error {
-            RepoError::NotFound { at } => CommandError::Abort {
-                message: format_bytes!(
-                    b"abort: repository {} not found",
-                    get_bytes_from_path(at)
-                ),
-                detailed_exit_code: exit_codes::ABORT,
-            },
+            RepoError::NotFound { at } => {
+                CommandError::abort_with_exit_code_bytes(
+                    format_bytes!(
+                        b"abort: repository {} not found",
+                        get_bytes_from_path(at)
+                    ),
+                    exit_codes::ABORT,
+                )
+            }
             RepoError::ConfigParseError(error) => error.into(),
             RepoError::Other(error) => error.into(),
         }
@@ -124,13 +157,13 @@
 impl<'a> From<&'a NoRepoInCwdError> for CommandError {
     fn from(error: &'a NoRepoInCwdError) -> Self {
         let NoRepoInCwdError { cwd } = error;
-        CommandError::Abort {
-            message: format_bytes!(
+        CommandError::abort_with_exit_code_bytes(
+            format_bytes!(
                 b"abort: no repository found in '{}' (.hg not found)!",
                 get_bytes_from_path(cwd)
             ),
-            detailed_exit_code: exit_codes::ABORT,
-        }
+            exit_codes::ABORT,
+        )
     }
 }
 
@@ -155,15 +188,15 @@
         } else {
             Vec::new()
         };
-        CommandError::Abort {
-            message: format_bytes!(
+        CommandError::abort_with_exit_code_bytes(
+            format_bytes!(
                 b"config error at {}{}: {}",
                 origin,
                 line_message,
                 message
             ),
-            detailed_exit_code: exit_codes::CONFIG_ERROR_ABORT,
-        }
+            exit_codes::CONFIG_ERROR_ABORT,
+        )
     }
 }
 
@@ -212,3 +245,46 @@
         HgError::from(error).into()
     }
 }
+
+impl From<SparseConfigError> for CommandError {
+    fn from(e: SparseConfigError) -> Self {
+        match e {
+            SparseConfigError::IncludesAfterExcludes { context } => {
+                Self::abort_with_exit_code_bytes(
+                    format_bytes!(
+                        b"{} config cannot have includes after excludes",
+                        context
+                    ),
+                    exit_codes::CONFIG_PARSE_ERROR_ABORT,
+                )
+            }
+            SparseConfigError::EntryOutsideSection { context, line } => {
+                Self::abort_with_exit_code_bytes(
+                    format_bytes!(
+                        b"{} config entry outside of section: {}",
+                        context,
+                        &line,
+                    ),
+                    exit_codes::CONFIG_PARSE_ERROR_ABORT,
+                )
+            }
+            SparseConfigError::InvalidNarrowPrefix(prefix) => {
+                Self::abort_with_exit_code_bytes(
+                    format_bytes!(
+                        b"invalid prefix on narrow pattern: {}",
+                        &prefix
+                    ),
+                    exit_codes::ABORT,
+                )
+            }
+            SparseConfigError::IncludesInNarrow => Self::abort(
+                "including other spec files using '%include' \
+                    is not supported in narrowspec",
+            ),
+            SparseConfigError::HgError(e) => Self::from(e),
+            SparseConfigError::PatternError(e) => {
+                Self::unsupported(format!("{}", e))
+            }
+        }
+    }
+}
--- a/rust/rhg/src/main.rs	Thu Oct 20 12:05:17 2022 -0400
+++ b/rust/rhg/src/main.rs	Mon Oct 24 15:32:14 2022 +0200
@@ -6,11 +6,12 @@
 use clap::Arg;
 use clap::ArgMatches;
 use format_bytes::{format_bytes, join};
-use hg::config::{Config, ConfigSource};
+use hg::config::{Config, ConfigSource, PlainInfo};
 use hg::repo::{Repo, RepoError};
 use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes};
 use hg::utils::SliceExt;
 use hg::{exit_codes, requirements};
+use std::borrow::Cow;
 use std::collections::HashSet;
 use std::ffi::OsString;
 use std::os::unix::prelude::CommandExt;
@@ -300,6 +301,24 @@
         }
     };
 
+    let exit =
+        |ui: &Ui, config: &Config, result: Result<(), CommandError>| -> ! {
+            exit(
+                &argv,
+                &initial_current_dir,
+                ui,
+                OnUnsupported::from_config(config),
+                result,
+                // TODO: show a warning or combine with original error if
+                // `get_bool` returns an error
+                non_repo_config
+                    .get_bool(b"ui", b"detailed-exit-code")
+                    .unwrap_or(false),
+            )
+        };
+    let early_exit = |config: &Config, error: CommandError| -> ! {
+        exit(&Ui::new_infallible(config), &config, Err(error))
+    };
     let repo_result = match Repo::find(&non_repo_config, repo_path.to_owned())
     {
         Ok(repo) => Ok(repo),
@@ -307,18 +326,7 @@
             // Not finding a repo is not fatal yet, if `-R` was not given
             Err(NoRepoInCwdError { cwd: at })
         }
-        Err(error) => exit(
-            &argv,
-            &initial_current_dir,
-            &Ui::new_infallible(&non_repo_config),
-            OnUnsupported::from_config(&non_repo_config),
-            Err(error.into()),
-            // TODO: show a warning or combine with original error if
-            // `get_bool` returns an error
-            non_repo_config
-                .get_bool(b"ui", b"detailed-exit-code")
-                .unwrap_or(false),
-        ),
+        Err(error) => early_exit(&non_repo_config, error.into()),
     };
 
     let config = if let Ok(repo) = &repo_result {
@@ -326,20 +334,20 @@
     } else {
         &non_repo_config
     };
-    let ui = Ui::new(&config).unwrap_or_else(|error| {
-        exit(
-            &argv,
-            &initial_current_dir,
-            &Ui::new_infallible(&config),
-            OnUnsupported::from_config(&config),
-            Err(error.into()),
-            config
-                .get_bool(b"ui", b"detailed-exit-code")
-                .unwrap_or(false),
-        )
-    });
-    let on_unsupported = OnUnsupported::from_config(config);
 
+    let mut config_cow = Cow::Borrowed(config);
+    config_cow.to_mut().apply_plain(PlainInfo::from_env());
+    if !ui::plain(Some("tweakdefaults"))
+        && config_cow
+            .as_ref()
+            .get_bool(b"ui", b"tweakdefaults")
+            .unwrap_or_else(|error| early_exit(&config, error.into()))
+    {
+        config_cow.to_mut().tweakdefaults()
+    };
+    let config = config_cow.as_ref();
+    let ui = Ui::new(&config)
+        .unwrap_or_else(|error| early_exit(&config, error.into()));
     let result = main_with_result(
         argv.iter().map(|s| s.to_owned()).collect(),
         &process_start_time,
@@ -347,18 +355,7 @@
         repo_result.as_ref(),
         config,
     );
-    exit(
-        &argv,
-        &initial_current_dir,
-        &ui,
-        on_unsupported,
-        result,
-        // TODO: show a warning or combine with original error if `get_bool`
-        // returns an error
-        config
-            .get_bool(b"ui", b"detailed-exit-code")
-            .unwrap_or(false),
-    )
+    exit(&ui, &config, result)
 }
 
 fn main() -> ! {
@@ -372,8 +369,7 @@
     match result {
         Ok(()) => exit_codes::OK,
         Err(CommandError::Abort {
-            message: _,
-            detailed_exit_code,
+            detailed_exit_code, ..
         }) => {
             if use_detailed_exit_code {
                 *detailed_exit_code
@@ -480,15 +476,15 @@
     match &result {
         Ok(_) => {}
         Err(CommandError::Unsuccessful) => {}
-        Err(CommandError::Abort {
-            message,
-            detailed_exit_code: _,
-        }) => {
+        Err(CommandError::Abort { message, hint, .. }) => {
+            // Ignore errors when writing to stderr, we’re already exiting
+            // with failure code so there’s not much more we can do.
             if !message.is_empty() {
-                // Ignore errors when writing to stderr, we’re already exiting
-                // with failure code so there’s not much more we can do.
                 let _ = ui.write_stderr(&format_bytes!(b"{}\n", message));
             }
+            if let Some(hint) = hint {
+                let _ = ui.write_stderr(&format_bytes!(b"({})\n", hint));
+            }
         }
         Err(CommandError::UnsupportedFeature { message }) => {
             match on_unsupported {
@@ -546,6 +542,7 @@
     debugdata
     debugrequirements
     debugignorerhg
+    debugrhgsparse
     files
     root
     config
@@ -677,8 +674,15 @@
 /// The `*` extension is an edge-case for config sub-options that apply to all
 /// extensions. For now, only `:required` exists, but that may change in the
 /// future.
-const SUPPORTED_EXTENSIONS: &[&[u8]] =
-    &[b"blackbox", b"share", b"sparse", b"narrow", b"*"];
+const SUPPORTED_EXTENSIONS: &[&[u8]] = &[
+    b"blackbox",
+    b"share",
+    b"sparse",
+    b"narrow",
+    b"*",
+    b"strip",
+    b"rebase",
+];
 
 fn check_extensions(config: &Config) -> Result<(), CommandError> {
     if let Some(b"*") = config.get(b"rhg", b"ignored-extensions") {
@@ -687,13 +691,18 @@
     }
 
     let enabled: HashSet<&[u8]> = config
-        .get_section_keys(b"extensions")
-        .into_iter()
-        .map(|extension| {
+        .iter_section(b"extensions")
+        .filter_map(|(extension, value)| {
+            if value == b"!" {
+                // Filter out disabled extensions
+                return None;
+            }
             // Ignore extension suboptions. Only `required` exists for now.
             // `rhg` either supports an extension or doesn't, so it doesn't
             // make sense to consider the loading of an extension.
-            extension.split_2(b':').unwrap_or((extension, b"")).0
+            let actual_extension =
+                extension.split_2(b':').unwrap_or((extension, b"")).0;
+            Some(actual_extension)
         })
         .collect();
 
--- a/rust/rhg/src/ui.rs	Thu Oct 20 12:05:17 2022 -0400
+++ b/rust/rhg/src/ui.rs	Mon Oct 24 15:32:14 2022 +0200
@@ -3,10 +3,9 @@
 use format_bytes::format_bytes;
 use format_bytes::write_bytes;
 use hg::config::Config;
+use hg::config::PlainInfo;
 use hg::errors::HgError;
-use hg::utils::files::get_bytes_from_os_string;
 use std::borrow::Cow;
-use std::env;
 use std::io;
 use std::io::{ErrorKind, Write};
 
@@ -127,35 +126,15 @@
         }
         stdout.flush()
     }
-
-    /// Return whether plain mode is active.
-    ///
-    /// Plain mode means that all configuration variables which affect
-    /// the behavior and output of Mercurial should be
-    /// ignored. Additionally, the output should be stable,
-    /// reproducible and suitable for use in scripts or applications.
-    ///
-    /// The only way to trigger plain mode is by setting either the
-    /// `HGPLAIN' or `HGPLAINEXCEPT' environment variables.
-    ///
-    /// The return value can either be
-    /// - False if HGPLAIN is not set, or feature is in HGPLAINEXCEPT
-    /// - False if feature is disabled by default and not included in HGPLAIN
-    /// - True otherwise
-    pub fn plain(&self, feature: Option<&str>) -> bool {
-        plain(feature)
-    }
 }
 
+// TODO: pass the PlainInfo to call sites directly and
+// delete this function
 pub fn plain(opt_feature: Option<&str>) -> bool {
-    if let Some(except) = env::var_os("HGPLAINEXCEPT") {
-        opt_feature.map_or(true, |feature| {
-            get_bytes_from_os_string(except)
-                .split(|&byte| byte == b',')
-                .all(|exception| exception != feature.as_bytes())
-        })
-    } else {
-        env::var_os("HGPLAIN").is_some()
+    let plain_info = PlainInfo::from_env();
+    match opt_feature {
+        None => plain_info.is_plain(),
+        Some(feature) => plain_info.is_feature_plain(feature),
     }
 }
 
--- a/setup.py	Thu Oct 20 12:05:17 2022 -0400
+++ b/setup.py	Mon Oct 24 15:32:14 2022 +0200
@@ -666,30 +666,55 @@
 
 class buildhgexe(build_ext):
     description = 'compile hg.exe from mercurial/exewrapper.c'
-    user_options = build_ext.user_options + [
-        (
-            'long-paths-support',
-            None,
-            'enable support for long paths on '
-            'Windows (off by default and '
-            'experimental)',
-        ),
-    ]
 
-    LONG_PATHS_MANIFEST = """
-    <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-    <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
-        <application>
-            <windowsSettings
-            xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
-                <ws2:longPathAware>true</ws2:longPathAware>
-            </windowsSettings>
-        </application>
-    </assembly>"""
+    LONG_PATHS_MANIFEST = """\
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
+  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
+    <security>
+      <requestedPrivileges>
+        <requestedExecutionLevel
+          level="asInvoker"
+          uiAccess="false"
+        />
+      </requestedPrivileges>
+    </security>
+  </trustInfo>
+  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+    <application>
+      <!-- Windows Vista -->
+      <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
+      <!-- Windows 7 -->
+      <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
+      <!-- Windows 8 -->
+      <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
+      <!-- Windows 8.1 -->
+      <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
+      <!-- Windows 10 and Windows 11 -->
+      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
+    </application>
+  </compatibility>
+  <application xmlns="urn:schemas-microsoft-com:asm.v3">
+    <windowsSettings
+        xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
+      <ws2:longPathAware>true</ws2:longPathAware>
+    </windowsSettings>
+  </application>
+  <dependency>
+    <dependentAssembly>
+      <assemblyIdentity type="win32"
+                        name="Microsoft.Windows.Common-Controls"
+                        version="6.0.0.0"
+                        processorArchitecture="*"
+                        publicKeyToken="6595b64144ccf1df"
+                        language="*" />
+    </dependentAssembly>
+  </dependency>
+</assembly>
+"""
 
     def initialize_options(self):
         build_ext.initialize_options(self)
-        self.long_paths_support = False
 
     def build_extensions(self):
         if os.name != 'nt':
@@ -700,8 +725,8 @@
 
         pythonlib = None
 
-        dir = os.path.dirname(self.get_ext_fullpath('dummy'))
-        self.hgtarget = os.path.join(dir, 'hg')
+        dirname = os.path.dirname(self.get_ext_fullpath('dummy'))
+        self.hgtarget = os.path.join(dirname, 'hg')
 
         if getattr(sys, 'dllhandle', None):
             # Different Python installs can have different Python library
@@ -774,22 +799,11 @@
         self.compiler.link_executable(
             objects, self.hgtarget, libraries=[], output_dir=self.build_temp
         )
-        if self.long_paths_support:
-            self.addlongpathsmanifest()
+
+        self.addlongpathsmanifest()
 
     def addlongpathsmanifest(self):
-        r"""Add manifest pieces so that hg.exe understands long paths
-
-        This is an EXPERIMENTAL feature, use with care.
-        To enable long paths support, one needs to do two things:
-        - build Mercurial with --long-paths-support option
-        - change HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\
-                 LongPathsEnabled to have value 1.
-
-        Please ignore 'warning 81010002: Unrecognized Element "longPathAware"';
-        it happens because Mercurial uses mt.exe circa 2008, which is not
-        yet aware of long paths support in the manifest (I think so at least).
-        This does not stop mt.exe from embedding/merging the XML properly.
+        """Add manifest pieces so that hg.exe understands long paths
 
         Why resource #1 should be used for .exe manifests? I don't know and
         wasn't able to find an explanation for mortals. But it seems to work.
@@ -797,21 +811,18 @@
         exefname = self.compiler.executable_filename(self.hgtarget)
         fdauto, manfname = tempfile.mkstemp(suffix='.hg.exe.manifest')
         os.close(fdauto)
-        with open(manfname, 'w') as f:
+        with open(manfname, 'w', encoding="UTF-8") as f:
             f.write(self.LONG_PATHS_MANIFEST)
         log.info("long paths manifest is written to '%s'" % manfname)
-        inputresource = '-inputresource:%s;#1' % exefname
         outputresource = '-outputresource:%s;#1' % exefname
         log.info("running mt.exe to update hg.exe's manifest in-place")
-        # supplying both -manifest and -inputresource to mt.exe makes
-        # it merge the embedded and supplied manifests in the -outputresource
+
         self.spawn(
             [
-                'mt.exe',
+                self.compiler.mt,
                 '-nologo',
                 '-manifest',
                 manfname,
-                inputresource,
                 outputresource,
             ]
         )
--- a/tests/run-tests.py	Thu Oct 20 12:05:17 2022 -0400
+++ b/tests/run-tests.py	Mon Oct 24 15:32:14 2022 +0200
@@ -48,7 +48,7 @@
 import collections
 import contextlib
 import difflib
-import distutils.version as version
+
 import errno
 import functools
 import json
@@ -72,6 +72,13 @@
 import uuid
 import xml.dom.minidom as minidom
 
+try:
+    # PEP 632 recommend the use of `packaging.version` to replace the
+    # deprecated `distutil.version`. So lets do it.
+    import packaging.version as version
+except ImportError:
+    import distutils.version as version
+
 if sys.version_info < (3, 5, 0):
     print(
         '%s is only supported on Python 3.5+, not %s'
@@ -3437,6 +3444,7 @@
             if self.options.list_tests:
                 result = runner.listtests(suite)
             else:
+                install_start_time = time.monotonic()
                 self._usecorrectpython()
                 if self._installdir:
                     self._installhg()
@@ -3450,6 +3458,11 @@
                 elif self.options.pyoxidized:
                     self._build_pyoxidized()
                 self._use_correct_mercurial()
+                install_end_time = time.monotonic()
+                if self._installdir:
+                    msg = 'installed Mercurial in %.2f seconds'
+                    msg %= install_end_time - install_start_time
+                    log(msg)
 
                 log(
                     'running %d tests using %d parallel processes'
--- a/tests/test-bisect2.t	Thu Oct 20 12:05:17 2022 -0400
+++ b/tests/test-bisect2.t	Mon Oct 24 15:32:14 2022 +0200
@@ -784,7 +784,6 @@
   $ hg log -q -r 'bisect(pruned)'
   0:33b1f9bc8bc5
   1:4ca5088da217
-  2:051e12f87bf1
   8:dab8161ac8fc
   11:82ca6f06eccd
   12:9f259202bbe7
--- a/tests/test-bundle.t	Thu Oct 20 12:05:17 2022 -0400
+++ b/tests/test-bundle.t	Mon Oct 24 15:32:14 2022 +0200
@@ -718,7 +718,7 @@
   $ hg init empty
   $ hg -R test bundle --base null -r 0 ../0.hg
   1 changesets found
-  $ hg -R test bundle --base 0    -r 1 ../1.hg
+  $ hg -R test bundle --exact -r 1 ../1.hg
   1 changesets found
   $ hg -R empty unbundle -u ../0.hg ../1.hg
   adding changesets
--- a/tests/test-chg.t	Thu Oct 20 12:05:17 2022 -0400
+++ b/tests/test-chg.t	Mon Oct 24 15:32:14 2022 +0200
@@ -432,6 +432,20 @@
   YYYY/MM/DD HH:MM:SS (PID)> log -R cached
   YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached (in  ...s)
 
+Test that -R is interpreted relative to --cwd.
+
+  $ hg init repo1
+  $ mkdir -p a/b
+  $ hg init a/b/repo2
+  $ printf "[alias]\ntest=repo1\n" >> repo1/.hg/hgrc
+  $ printf "[alias]\ntest=repo2\n" >> a/b/repo2/.hg/hgrc
+  $ cd a
+  $ chg --cwd .. -R repo1 show alias.test
+  repo1
+  $ chg --cwd . -R b/repo2 show alias.test
+  repo2
+  $ cd ..
+
 Test that chg works (sets to the user's actual LC_CTYPE) even when python
 "coerces" the locale (py3.7+)
 
--- a/tests/test-completion.t	Thu Oct 20 12:05:17 2022 -0400
+++ b/tests/test-completion.t	Mon Oct 24 15:32:14 2022 +0200
@@ -261,7 +261,7 @@
   bookmarks: force, rev, delete, rename, inactive, list, template
   branch: force, clean, rev
   branches: active, closed, rev, template
-  bundle: force, rev, branch, base, all, type, ssh, remotecmd, insecure
+  bundle: exact, force, rev, branch, base, all, type, ssh, remotecmd, insecure
   cat: output, rev, decode, include, exclude, template
   clone: noupdate, updaterev, rev, branch, pull, uncompressed, stream, ssh, remotecmd, insecure
   commit: addremove, close-branch, amend, secret, edit, force-close-branch, interactive, include, exclude, message, logfile, date, user, subrepos
--- a/tests/test-contrib-perf.t	Thu Oct 20 12:05:17 2022 -0400
+++ b/tests/test-contrib-perf.t	Mon Oct 24 15:32:14 2022 +0200
@@ -96,6 +96,7 @@
    perf::branchmapupdate
                  benchmark branchmap update from for <base> revs to <target>
                  revs
+   perf::bundle  benchmark the creation of a bundle from a repository
    perf::bundleread
                  Benchmark reading of bundle files.
    perf::cca     (no help text available)
@@ -105,6 +106,9 @@
                  (no help text available)
    perf::ctxfiles
                  (no help text available)
+   perf::delta-find
+                 benchmark the process of finding a valid delta for a revlog
+                 revision
    perf::diffwd  Profile diff of working directory changes
    perf::dirfoldmap
                  benchmap a 'dirstate._map.dirfoldmap.get()' request
@@ -187,6 +191,8 @@
    perf::tags    (no help text available)
    perf::templating
                  test the rendering time of a given template
+   perf::unbundle
+                 benchmark application of a bundle in a repository.
    perf::unidiff
                  benchmark a unified diff between revisions
    perf::volatilesets
@@ -385,6 +391,11 @@
   searching for changes
   searching for changes
   searching for changes
+  $ hg perf::bundle 'last(all(), 5)'
+  $ hg bundle --exact --rev 'last(all(), 5)' last-5.hg
+  4 changesets found
+  $ hg perf::unbundle last-5.hg
+
 
 test  profile-benchmark option
 ------------------------------
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-contrib-pull-logger.t	Mon Oct 24 15:32:14 2022 +0200
@@ -0,0 +1,78 @@
+Check that the pull logger plugins logs pulls
+=============================================
+
+Enable the extension
+
+  $ echo "[extensions]" >> $HGRCPATH
+  $ echo "pull-logger = $TESTDIR/../contrib/pull_logger.py" >> $HGRCPATH
+
+
+Check the format of the generated log entries, with a bunch of elements in the
+common and heads set
+
+  $ hg init server
+  $ hg -R server debugbuilddag '.*2+2'
+  $ hg clone ssh://user@dummy/server client --rev 0
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 0 changes to 0 files
+  new changesets 1ea73414a91b
+  updating to branch default
+  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ tail -1 server/.hg/pull_log.jsonl
+  {"common": ["0000000000000000000000000000000000000000"], "heads": ["1ea73414a91b0920940797d8fc6a11e447f8ea1e"], "logger_version": 0, "timestamp": *} (glob)
+  $ hg -R client pull --rev 1 --rev 2
+  pulling from ssh://user@dummy/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 2 changesets with 0 changes to 0 files (+1 heads)
+  new changesets d8736c3a2c84:fa28e81e283b
+  (run 'hg heads' to see heads, 'hg merge' to merge)
+  $ tail -1 server/.hg/pull_log.jsonl
+  {"common": ["1ea73414a91b0920940797d8fc6a11e447f8ea1e"], "heads": ["d8736c3a2c84ee759a2821385804bcb67f266ade", "fa28e81e283b3416de4d48ee0dd2d446e9e38d7c"], "logger_version": 0, "timestamp": *} (glob)
+  $ hg -R client pull --rev 2 --rev 3
+  pulling from ssh://user@dummy/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 0 changes to 0 files
+  new changesets 944641ddcaef
+  (run 'hg update' to get a working copy)
+  $ tail -1 server/.hg/pull_log.jsonl
+  {"common": ["1ea73414a91b0920940797d8fc6a11e447f8ea1e", "fa28e81e283b3416de4d48ee0dd2d446e9e38d7c"], "heads": ["944641ddcaef174df7ce1bc2751a5f165129778b", "fa28e81e283b3416de4d48ee0dd2d446e9e38d7c"], "logger_version": 0, "timestamp": *} (glob)
+
+
+Check the number of entries generated in the log when pulling from multiple
+clients at the same time
+
+  $ rm -f server/.hg/pull_log.jsonl
+  $ for i in $($TESTDIR/seq.py 32); do
+  >   hg clone ssh://user@dummy/server client_$i --rev 0
+  > done > /dev/null
+  $ for i in $($TESTDIR/seq.py 32); do
+  >   hg -R client_$i pull --rev 1 &
+  > done > /dev/null
+  $ wait
+  $ wc -l server/.hg/pull_log.jsonl
+  \s*64 .* (re)
+
+
+Test log rotation when reaching some size threshold
+
+  $ cat >> $HGRCPATH << EOF
+  > [pull-logger]
+  > rotate-size = 1kb
+  > EOF
+
+  $ rm -f server/.hg/pull_log.jsonl
+  $ for i in $($TESTDIR/seq.py 10); do
+  >   hg -R client pull --rev 1
+  > done > /dev/null
+  $ wc -l server/.hg/pull_log.jsonl
+  \s*3 .* (re)
+  $ wc -l server/.hg/pull_log.jsonl.rotated
+  \s*7 .* (re)
--- a/tests/test-http-bad-server.t	Thu Oct 20 12:05:17 2022 -0400
+++ b/tests/test-http-bad-server.t	Mon Oct 24 15:32:14 2022 +0200
@@ -659,7 +659,7 @@
 
   $ hg clone http://localhost:$HGPORT/ clone
   requesting all changes
-  abort: HTTP request error (incomplete response) (py3 !)
+  abort: HTTP request error (incomplete response*) (glob)
   (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator)
   [255]
 
@@ -703,7 +703,7 @@
 
   $ hg clone http://localhost:$HGPORT/ clone
   requesting all changes
-  abort: HTTP request error (incomplete response) (py3 !)
+  abort: HTTP request error (incomplete response*) (glob)
   (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator)
   [255]
 
@@ -904,7 +904,7 @@
   adding changesets
   transaction abort!
   rollback completed
-  abort: HTTP request error (incomplete response) (py3 !)
+  abort: HTTP request error (incomplete response*) (glob)
   (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator)
   [255]
 
@@ -1021,7 +1021,7 @@
   adding file changes
   transaction abort!
   rollback completed
-  abort: HTTP request error (incomplete response) (py3 !)
+  abort: HTTP request error (incomplete response*) (glob)
   (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator)
   [255]
 
--- a/tests/test-log.t	Thu Oct 20 12:05:17 2022 -0400
+++ b/tests/test-log.t	Mon Oct 24 15:32:14 2022 +0200
@@ -2157,6 +2157,8 @@
   ... '''.encode('utf-8')) and None
   $ sh < setup.sh
 
+#if no-rhg
+
 test in problematic encoding
   >>> with open('test.sh', 'wb') as f:
   ...     f.write(u'''
@@ -2179,6 +2181,8 @@
   3
   1
 
+#endif
+
   $ cd ..
 
 test hg log on non-existent files and on directories
--- a/tests/test-mq-subrepo-svn.t	Thu Oct 20 12:05:17 2022 -0400
+++ b/tests/test-mq-subrepo-svn.t	Mon Oct 24 15:32:14 2022 +0200
@@ -38,7 +38,7 @@
   A .hgsub
   $ hg qnew -m0 0.diff
   $ cd sub
-  $ echo a > a
+  $ echo foo > a
   $ svn add a
   A         a
   $ svn st
--- a/tests/test-phase-archived.t	Thu Oct 20 12:05:17 2022 -0400
+++ b/tests/test-phase-archived.t	Mon Oct 24 15:32:14 2022 +0200
@@ -4,7 +4,7 @@
 
   $ cat << EOF >> $HGRCPATH
   > [format]
-  > internal-phase=yes
+  > exp-archived-phase=yes
   > [extensions]
   > strip=
   > [experimental]
--- a/tests/test-phases.t	Thu Oct 20 12:05:17 2022 -0400
+++ b/tests/test-phases.t	Mon Oct 24 15:32:14 2022 +0200
@@ -879,7 +879,7 @@
 
 Check we deny its usage on older repository
 
-  $ hg init no-internal-phase --config format.internal-phase=no
+  $ hg init no-internal-phase --config format.use-internal-phase=no
   $ cd no-internal-phase
   $ hg debugrequires | grep internal-phase
   [1]
@@ -900,10 +900,10 @@
 
 Check it works fine with repository that supports it.
 
-  $ hg init internal-phase --config format.internal-phase=yes
+  $ hg init internal-phase --config format.use-internal-phase=yes
   $ cd internal-phase
   $ hg debugrequires | grep internal-phase
-  internal-phase
+  internal-phase-2
   $ mkcommit A
   test-debug-phase: new rev 0:  x -> 1
   test-hook-close-phase: 4a2df7238c3b48766b5e22fafbb8a2f506ec8256:   -> draft
@@ -951,21 +951,28 @@
 
 Commit an archived changesets
 
+  $ cd ..
+  $ hg clone --quiet --pull internal-phase archived-phase \
+  > --config format.exp-archived-phase=yes \
+  > --config extensions.phasereport='!' \
+  > --config hooks.txnclose-phase.test=
+
+  $ cd archived-phase
+
   $ echo B > B
   $ hg add B
   $ hg status
   A B
   $ hg --config "phases.new-commit=archived" commit -m "my test archived commit"
-  test-debug-phase: new rev 2:  x -> 32
+  test-debug-phase: new rev 1:  x -> 32
   test-hook-close-phase: 8df5997c3361518f733d1ae67cd3adb9b0eaf125:   -> archived
 
 The changeset is a working parent descendant.
 Per the usual visibility rules, it is made visible.
 
   $ hg log -G -l 3
-  @  changeset:   2:8df5997c3361
+  @  changeset:   1:8df5997c3361
   |  tag:         tip
-  |  parent:      0:4a2df7238c3b
   |  user:        test
   |  date:        Thu Jan 01 00:00:00 1970 +0000
   |  summary:     my test archived commit
--- a/tests/test-releasenotes-formatting.t	Thu Oct 20 12:05:17 2022 -0400
+++ b/tests/test-releasenotes-formatting.t	Mon Oct 24 15:32:14 2022 +0200
@@ -387,6 +387,8 @@
 
   $ touch a
   $ hg -q commit -A -l - << EOF
+  > commit 2
+  > 
   > .. asf::
   > 
   >    First paragraph under this admonition.
@@ -395,7 +397,7 @@
 Suggest similar admonition in place of the invalid one.
 
   $ hg releasenotes -r . -c
-  Invalid admonition 'asf' present in changeset 4026fe9e1c20
+  Invalid admonition 'asf' present in changeset 99fa3c800c5e
 
   $ touch b
   $ hg -q commit -A -l - << EOF
@@ -405,7 +407,7 @@
   > EOF
 
   $ hg releasenotes -r . -c
-  Invalid admonition 'fixes' present in changeset 0e7130d2705c
+  Invalid admonition 'fixes' present in changeset 4737b1b5afd1
   (did you mean fix?)
 
   $ cd ..
--- a/tests/test-revset.t	Thu Oct 20 12:05:17 2022 -0400
+++ b/tests/test-revset.t	Mon Oct 24 15:32:14 2022 +0200
@@ -2974,6 +2974,25 @@
   1 b11  m12  u111 112 7200
   0 b12  m111 u112 111 10800
 
+random sort
+
+  $ hg log --rev 'sort(all(), "random")' | wc -l
+  \s*8 (re)
+  $ hg log --rev 'sort(all(), "-random")' | wc -l
+  \s*8 (re)
+  $ hg log --rev 'sort(all(), "random", random.seed=celeste)'
+  6 b111 t2   tu   130 0
+  7 b111 t3   tu   130 0
+  4 b111 m112 u111 110 14400
+  3 b112 m111 u11  120 0
+  5 b111 t1   tu   130 0
+  0 b12  m111 u112 111 10800
+  1 b11  m12  u111 112 7200
+  2 b111 m11  u12  111 3600
+  $ hg log --rev 'first(sort(all(), "random", random.seed=celeste))'
+  6 b111 t2   tu   130 0
+
+
 topographical sorting can't be combined with other sort keys, and you can't
 use the topo.firstbranch option when topo sort is not active:
 
--- a/tests/test-revset2.t	Thu Oct 20 12:05:17 2022 -0400
+++ b/tests/test-revset2.t	Mon Oct 24 15:32:14 2022 +0200
@@ -1481,6 +1481,20 @@
   $ hg init namedbranch
   $ cd namedbranch
 
+  $ log 'roots(.)'
+  -1
+  $ log 'roots(. or wdir())'
+  -1
+  $ log 'roots(wdir())'
+  2147483647
+  $ log 'sort(., -topo)'
+  -1
+  $ log 'sort(. or wdir(), -topo)'
+  -1
+  2147483647
+  $ log 'sort(wdir(), -topo)'
+  2147483647
+
   $ echo default0 >> a
   $ hg ci -Aqm0
   $ echo default1 >> a
@@ -1498,6 +1512,17 @@
   $ echo default5 >> a
   $ hg ci -m5
 
+  $ log 'roots(. or wdir())'
+  5
+  $ log 'roots(wdir())'
+  2147483647
+  $ log 'sort(. or wdir() or .^, -topo)'
+  4
+  5
+  2147483647
+  $ log 'sort(wdir(), -topo)'
+  2147483647
+
 "null" revision belongs to "default" branch (issue4683)
 
   $ log 'branch(null)'
--- a/tests/test-rhg-sparse-narrow.t	Thu Oct 20 12:05:17 2022 -0400
+++ b/tests/test-rhg-sparse-narrow.t	Mon Oct 24 15:32:14 2022 +0200
@@ -85,15 +85,12 @@
   dir1/x
   dir1/y
 
-Hg status needs to do some filtering based on narrow spec, so we don't
-support it in rhg for narrow clones yet.
+Hg status needs to do some filtering based on narrow spec
 
   $ mkdir dir2
   $ touch dir2/q
   $ "$real_hg" status
   $ $NO_FALLBACK rhg --config rhg.status=true status
-  unsupported feature: rhg status is not supported for sparse checkouts or narrow clones yet
-  [252]
 
 Adding "orphaned" index files:
 
--- a/tests/test-shelve.t	Thu Oct 20 12:05:17 2022 -0400
+++ b/tests/test-shelve.t	Mon Oct 24 15:32:14 2022 +0200
@@ -24,7 +24,7 @@
 
   $ cat <<EOF >> $HGRCPATH
   > [format]
-  > internal-phase = yes
+  > use-internal-phase = yes
   > EOF
 
 #endif
@@ -253,12 +253,12 @@
 (this also tests that same timestamp prevents backups from being
 removed, even though there are more than 'maxbackups' backups)
 
-  $ f -t .hg/shelve-backup/default.patch
-  .hg/shelve-backup/default.patch: file
-  $ touch -t 200001010000 .hg/shelve-backup/default.patch
-  $ f -t .hg/shelve-backup/default-1.patch
-  .hg/shelve-backup/default-1.patch: file
-  $ touch -t 200001010000 .hg/shelve-backup/default-1.patch
+  $ f -t .hg/shelve-backup/default.shelve
+  .hg/shelve-backup/default.shelve: file
+  $ touch -t 200001010000 .hg/shelve-backup/default.shelve
+  $ f -t .hg/shelve-backup/default-1.shelve
+  .hg/shelve-backup/default-1.shelve: file
+  $ touch -t 200001010000 .hg/shelve-backup/default-1.shelve
 
   $ hg unshelve
   unshelving change 'default-01'
@@ -1544,4 +1544,87 @@
   $ hg update -q --clean .
   $ hg patch -p1 test_patch.patch
   applying test_patch.patch
+
+  $ hg strip -q -r .
 #endif
+
+Check the comment of the last commit for consistency
+
+  $ hg log -r . --template '{desc}\n'
+  add C to bars
+
+-- if phasebased, shelve works without patch and bundle
+
+  $ hg update -q --clean .
+  $ rm -r .hg/shelve*
+  $ echo import antigravity >> somefile.py
+  $ hg add somefile.py
+  $ hg shelve -q
+#if phasebased
+  $ rm .hg/shelved/default.hg
+  $ rm .hg/shelved/default.patch
+#endif
+
+shelve --list --patch should work even with no patch file.
+
+  $ hg shelve --list --patch
+  default         (*s ago) * changes to: add C to bars (glob)
+  
+  diff --git a/somefile.py b/somefile.py
+  new file mode 100644
+  --- /dev/null
+  +++ b/somefile.py
+  @@ -0,0 +1,1 @@
+  +import antigravity
+
+  $ hg unshelve
+  unshelving change 'default'
+
+#if phasebased
+  $ ls .hg/shelve-backup
+  default.shelve
+#endif
+
+#if stripbased
+  $ ls .hg/shelve-backup
+  default.hg
+  default.patch
+  default.shelve
+#endif
+
+
+-- allow for phase-based shelves to be disabled
+
+  $ hg update -q --clean .
+  $ hg strip -q --hidden -r 0
+  $ rm -r .hg/shelve*
+
+#if phasebased
+  $ cat <<EOF >> $HGRCPATH
+  > [shelve]
+  > store = strip
+  > EOF
+#endif
+
+  $ echo import this >> somefile.py
+  $ hg add somefile.py
+  $ hg shelve -q
+  $ hg log --hidden
+  $ ls .hg/shelved
+  default.hg
+  default.patch
+  default.shelve
+  $ hg unshelve -q
+
+Override the disabling, re-enabling phase-based shelves
+
+  $ hg shelve --config shelve.store=internal -q
+
+#if phasebased
+  $ hg log --hidden --template '{user}\n'
+  shelve@localhost
+#endif
+
+#if stripbased
+  $ hg log --hidden --template '{user}\n'
+#endif
--- a/tests/test-shelve2.t	Thu Oct 20 12:05:17 2022 -0400
+++ b/tests/test-shelve2.t	Mon Oct 24 15:32:14 2022 +0200
@@ -26,7 +26,7 @@
 
   $ cat <<EOF >> $HGRCPATH
   > [format]
-  > internal-phase = yes
+  > use-internal-phase = yes
   > EOF
 
 #endif
--- a/tests/test-status.t	Thu Oct 20 12:05:17 2022 -0400
+++ b/tests/test-status.t	Mon Oct 24 15:32:14 2022 +0200
@@ -637,9 +637,16 @@
   M a
     b
   R b
+  $ hg st --config ui.statuscopies=true --no-copies
+  M a
+  R b
   $ hg st --config ui.statuscopies=false
   M a
   R b
+  $ hg st --config ui.statuscopies=false --copies
+  M a
+    b
+  R b
   $ hg st --config ui.tweakdefaults=yes
   M a
     b
--- a/tests/test-subrepo-svn.t	Thu Oct 20 12:05:17 2022 -0400
+++ b/tests/test-subrepo-svn.t	Mon Oct 24 15:32:14 2022 +0200
@@ -591,7 +591,7 @@
   $ cd "$WCROOT"
   $ svn up > /dev/null
   $ mkdir trunk/subdir branches
-  $ echo a > trunk/subdir/a
+  $ echo foo > trunk/subdir/a
   $ svn add trunk/subdir branches
   A         trunk/subdir
   A         trunk/subdir/a
--- a/tests/test-template-functions.t	Thu Oct 20 12:05:17 2022 -0400
+++ b/tests/test-template-functions.t	Mon Oct 24 15:32:14 2022 +0200
@@ -1718,4 +1718,19 @@
   $ hg log -T "{config('templateconfig', 'knob', if(true, 'foo', 'bar'))}\n"
   foo
 
+reverse filter:
+
+  $ hg log -T "{'abc\ndef\nghi'|splitlines|reverse}\n"
+  ghi def abc
+
+  $ hg log -T "{'abc'|reverse}\n"
+  hg: parse error: not reversible
+  (incompatible use of template filter 'reverse')
+  [10]
+
+  $ hg log -T "{date|reverse}\n"
+  hg: parse error: not reversible
+  (template filter 'reverse' is not compatible with keyword 'date')
+  [10]
+
   $ cd ..
--- a/tests/test-update-branches.t	Thu Oct 20 12:05:17 2022 -0400
+++ b/tests/test-update-branches.t	Mon Oct 24 15:32:14 2022 +0200
@@ -633,6 +633,10 @@
   # 
   # To mark files as resolved:  hg resolve --mark FILE
   
+  $ hg status -T '{status} {path} - {relpath(path)}\n'
+  M foo - foo
+   a - a
+
   $ hg status -Tjson
   [
    {