changeset 49506:44bc045a43ca

branching: merge stable into default
author Raphaël Gomès <rgomes@octobus.net>
date Tue, 04 Oct 2022 10:56:27 +0200
parents a6efc1f1a73b (diff) b6c6ac6ef359 (current diff)
children 791050360486
files contrib/heptapod-ci.yml rust/rhg/src/main.rs
diffstat 68 files changed, 1938 insertions(+), 256 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile	Tue Oct 04 10:24:56 2022 +0200
+++ b/Makefile	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/contrib/heptapod-ci.yml	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/contrib/packaging/Makefile	Tue Oct 04 10:56:27 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	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/contrib/packaging/packagelib.sh	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/contrib/perf-utils/compare-discovery-case	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/contrib/perf.py	Tue Oct 04 10:56:27 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
--- a/hgext/fsmonitor/pywatchman/pybser.py	Tue Oct 04 10:24:56 2022 +0200
+++ b/hgext/fsmonitor/pywatchman/pybser.py	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/hgext/rebase.py	Tue Oct 04 10:56:27 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/mercurial/cext/revlog.c	Tue Oct 04 10:24:56 2022 +0200
+++ b/mercurial/cext/revlog.c	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/mercurial/cmdutil.py	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/mercurial/commands.py	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/mercurial/configitems.py	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/mercurial/debugcommands.py	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/mercurial/defaultrc/mergetools.rc	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/mercurial/dirstate.py	Tue Oct 04 10:56:27 2022 +0200
@@ -1287,6 +1287,7 @@
 
         allowed_matchers = (
             matchmod.alwaysmatcher,
+            matchmod.differencematcher,
             matchmod.exactmatcher,
             matchmod.includematcher,
             matchmod.intersectionmatcher,
--- a/mercurial/dispatch.py	Tue Oct 04 10:24:56 2022 +0200
+++ b/mercurial/dispatch.py	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/mercurial/hbisect.py	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/mercurial/helptext/bundlespec.txt	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/mercurial/hgweb/__init__.py	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/mercurial/localrepo.py	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/mercurial/obsolete.py	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/mercurial/phases.py	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/mercurial/requirements.py	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/mercurial/revlog.py	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/mercurial/revset.py	Tue Oct 04 10:56:27 2022 +0200
@@ -7,7 +7,10 @@
 
 
 import binascii
+import functools
+import random
 import re
+import sys
 
 from .i18n import _
 from .pycompat import getattr
@@ -2347,6 +2350,15 @@
     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 +2367,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 +2417,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 +2450,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)
@@ -2448,7 +2482,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	Tue Oct 04 10:24:56 2022 +0200
+++ b/mercurial/scmutil.py	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/mercurial/shelve.py	Tue Oct 04 10:56:27 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)
--- a/mercurial/subrepo.py	Tue Oct 04 10:24:56 2022 +0200
+++ b/mercurial/subrepo.py	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/mercurial/templatefilters.py	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/mercurial/url.py	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/relnotes/next	Tue Oct 04 10:56:27 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/hg-core/src/config/layer.rs	Tue Oct 04 10:24:56 2022 +0200
+++ b/rust/hg-core/src/config/layer.rs	Tue Oct 04 10:56:27 2022 +0200
@@ -81,6 +81,7 @@
                         String::from_utf8_lossy(arg),
                     ),
                     CONFIG_PARSE_ERROR_ABORT,
+                    None,
                 ))?
             }
         }
--- a/rust/hg-core/src/dirstate.rs	Tue Oct 04 10:24:56 2022 +0200
+++ b/rust/hg-core/src/dirstate.rs	Tue Oct 04 10:56:27 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/errors.rs	Tue Oct 04 10:24:56 2022 +0200
+++ b/rust/hg-core/src/errors.rs	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/rust/hg-core/src/exit_codes.rs	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/rust/hg-core/src/filepatterns.rs	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/rust/hg-core/src/lib.rs	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/rust/hg-core/src/matchers.rs	Tue Oct 04 10:56:27 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	Tue Oct 04 10:56:27 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(())
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rust/hg-core/src/sparse.rs	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/rust/hg-core/src/vfs.rs	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/rust/hg-cpython/src/dirstate/status.rs	Tue Oct 04 10:56:27 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/src/commands/debugdata.rs	Tue Oct 04 10:24:56 2022 +0200
+++ b/rust/rhg/src/commands/debugdata.rs	Tue Oct 04 10:56:27 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	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/rust/rhg/src/commands/status.rs	Tue Oct 04 10:56:27 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,6 +27,7 @@
 use hg::PatternFileWarning;
 use hg::StatusError;
 use hg::StatusOptions;
+use hg::{self, narrow, sparse};
 use log::info;
 use std::io;
 use std::path::PathBuf;
@@ -104,6 +104,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,6 +156,33 @@
     }
 }
 
+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")? {
@@ -178,13 +211,9 @@
 
     let verbose = !ui.plain(None)
         && !args.is_present("print0")
-        && (config.get_bool(b"ui", b"verbose")?
+        && (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 +243,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 +270,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 {
@@ -350,9 +360,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,
@@ -545,3 +591,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	Tue Oct 04 10:24:56 2022 +0200
+++ b/rust/rhg/src/error.rs	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/rust/rhg/src/main.rs	Tue Oct 04 10:56:27 2022 +0200
@@ -372,8 +372,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 +479,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 +545,7 @@
     debugdata
     debugrequirements
     debugignorerhg
+    debugrhgsparse
     files
     root
     config
@@ -677,8 +677,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 +694,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/setup.py	Tue Oct 04 10:24:56 2022 +0200
+++ b/setup.py	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/tests/run-tests.py	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/tests/test-bisect2.t	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/tests/test-bundle.t	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/tests/test-chg.t	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/tests/test-completion.t	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/tests/test-contrib-perf.t	Tue Oct 04 10:56:27 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
 ------------------------------
--- a/tests/test-http-bad-server.t	Tue Oct 04 10:24:56 2022 +0200
+++ b/tests/test-http-bad-server.t	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/tests/test-log.t	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/tests/test-mq-subrepo-svn.t	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/tests/test-phase-archived.t	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/tests/test-phases.t	Tue Oct 04 10:56:27 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-revset.t	Tue Oct 04 10:24:56 2022 +0200
+++ b/tests/test-revset.t	Tue Oct 04 10:56:27 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-rhg-sparse-narrow.t	Tue Oct 04 10:24:56 2022 +0200
+++ b/tests/test-rhg-sparse-narrow.t	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/tests/test-shelve.t	Tue Oct 04 10:56:27 2022 +0200
@@ -14,7 +14,7 @@
 
   $ cat <<EOF >> $HGRCPATH
   > [format]
-  > internal-phase = yes
+  > use-internal-phase = yes
   > EOF
 
 #endif
@@ -243,12 +243,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'
@@ -1534,4 +1534,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	Tue Oct 04 10:24:56 2022 +0200
+++ b/tests/test-shelve2.t	Tue Oct 04 10:56:27 2022 +0200
@@ -16,7 +16,7 @@
 
   $ cat <<EOF >> $HGRCPATH
   > [format]
-  > internal-phase = yes
+  > use-internal-phase = yes
   > EOF
 
 #endif
--- a/tests/test-status.t	Tue Oct 04 10:24:56 2022 +0200
+++ b/tests/test-status.t	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/tests/test-subrepo-svn.t	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/tests/test-template-functions.t	Tue Oct 04 10:56:27 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	Tue Oct 04 10:24:56 2022 +0200
+++ b/tests/test-update-branches.t	Tue Oct 04 10:56:27 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
   [
    {