changeset 51183:028498b04a84

branching: merge with stable This recreates `37b52b938579` right as a `hg branch --rev 5b186ba40001` screwed up the content.
author Pierre-Yves David <pierre-yves.david@octobus.net>
date Fri, 15 Dec 2023 11:08:41 +0100
parents 933551630b0d (diff) 1486d8c63f64 (current diff)
children a9f96e809c2a
files
diffstat 23 files changed, 539 insertions(+), 362 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/cmdutil.py	Thu Dec 07 03:49:48 2023 +0100
+++ b/mercurial/cmdutil.py	Fri Dec 15 11:08:41 2023 +0100
@@ -2381,8 +2381,19 @@
             full=False,
         )
     ):
+        entry = dirstate.get_entry(f)
+        # We don't want to even attmpt to add back files that have been removed
+        # It would lead to a misleading message saying we're adding the path,
+        # and can also lead to file/dir conflicts when attempting to add it.
+        removed = entry and entry.removed
         exact = match.exact(f)
-        if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
+        if (
+            exact
+            or not explicitonly
+            and f not in wctx
+            and repo.wvfs.lexists(f)
+            and not removed
+        ):
             if cca:
                 cca(f)
             names.append(f)
--- a/mercurial/commands.py	Thu Dec 07 03:49:48 2023 +0100
+++ b/mercurial/commands.py	Fri Dec 15 11:08:41 2023 +0100
@@ -29,6 +29,7 @@
     copies,
     debugcommands as debugcommandsmod,
     destutil,
+    diffutil,
     discovery,
     encoding,
     error,
@@ -2655,7 +2656,7 @@
     if change:
         repo = scmutil.unhidehashlikerevs(repo, [change], b'nowarn')
         ctx2 = logcmdutil.revsingle(repo, change, None)
-        ctx1 = logcmdutil.diff_parent(ctx2)
+        ctx1 = diffutil.diff_parent(ctx2)
     elif from_rev or to_rev:
         repo = scmutil.unhidehashlikerevs(
             repo, [from_rev] + [to_rev], b'nowarn'
--- a/mercurial/diffutil.py	Thu Dec 07 03:49:48 2023 +0100
+++ b/mercurial/diffutil.py	Fri Dec 15 11:08:41 2023 +0100
@@ -16,6 +16,7 @@
 )
 
 from .i18n import _
+from .node import nullrev
 
 from . import (
     mdiff,
@@ -155,3 +156,35 @@
         )
 
     return mdiff.diffopts(**pycompat.strkwargs(buildopts))
+
+
+def diff_parent(ctx):
+    """get the context object to use as parent when diffing
+
+
+    If diff.merge is enabled, an overlayworkingctx of the auto-merged parents will be returned.
+    """
+    repo = ctx.repo()
+    if repo.ui.configbool(b"diff", b"merge") and ctx.p2().rev() != nullrev:
+        # avoid circular import
+        from . import (
+            context,
+            merge,
+        )
+
+        wctx = context.overlayworkingctx(repo)
+        wctx.setbase(ctx.p1())
+        with repo.ui.configoverride(
+            {
+                (
+                    b"ui",
+                    b"forcemerge",
+                ): b"internal:merge3-lie-about-conflicts",
+            },
+            b"merge-diff",
+        ):
+            with repo.ui.silent():
+                merge.merge(ctx.p2(), wc=wctx)
+        return wctx
+    else:
+        return ctx.p1()
--- a/mercurial/dirstate.py	Thu Dec 07 03:49:48 2023 +0100
+++ b/mercurial/dirstate.py	Fri Dec 15 11:08:41 2023 +0100
@@ -408,16 +408,6 @@
         """
         return self._changing_level > 0
 
-    def pendingparentchange(self):
-        return self.is_changing_parent()
-
-    def is_changing_parent(self):
-        """Returns true if the dirstate is in the middle of a set of changes
-        that modify the dirstate parent.
-        """
-        self._ui.deprecwarn(b"dirstate.is_changing_parents", b"6.5")
-        return self.is_changing_parents
-
     @property
     def is_changing_parents(self):
         """Returns true if the dirstate is in the middle of a set of changes
--- a/mercurial/extensions.py	Thu Dec 07 03:49:48 2023 +0100
+++ b/mercurial/extensions.py	Fri Dec 15 11:08:41 2023 +0100
@@ -625,9 +625,8 @@
     def __init__(self, container, funcname, wrapper):
         assert callable(wrapper)
         if not isinstance(funcname, str):
-            msg = b"pass wrappedfunction target name as `str`, not `bytes`"
-            util.nouideprecwarn(msg, b"6.6", stacklevel=2)
-            funcname = pycompat.sysstr(funcname)
+            msg = b"wrappedfunction target name should be `str`, not `bytes`"
+            raise TypeError(msg)
         self._container = container
         self._funcname = funcname
         self._wrapper = wrapper
@@ -675,9 +674,8 @@
     assert callable(wrapper)
 
     if not isinstance(funcname, str):
-        msg = b"pass wrapfunction target name as `str`, not `bytes`"
-        util.nouideprecwarn(msg, b"6.6", stacklevel=2)
-        funcname = pycompat.sysstr(funcname)
+        msg = b"wrapfunction target name should be `str`, not `bytes`"
+        raise TypeError(msg)
 
     origfn = getattr(container, funcname)
     assert callable(origfn)
--- a/mercurial/localrepo.py	Thu Dec 07 03:49:48 2023 +0100
+++ b/mercurial/localrepo.py	Fri Dec 15 11:08:41 2023 +0100
@@ -2921,17 +2921,7 @@
 
         unfi = self.unfiltered()
 
-        if full:
-            msg = (
-                "`full` argument for `repo.updatecaches` is deprecated\n"
-                "(use `caches=repository.CACHE_ALL` instead)"
-            )
-            self.ui.deprecwarn(msg, b"5.9")
-            caches = repository.CACHES_ALL
-            if full == b"post-clone":
-                caches = repository.CACHES_POST_CLONE
-            caches = repository.CACHES_ALL
-        elif caches is None:
+        if caches is None:
             caches = repository.CACHES_DEFAULT
 
         if repository.CACHE_BRANCHMAP_SERVED in caches:
--- a/mercurial/logcmdutil.py	Thu Dec 07 03:49:48 2023 +0100
+++ b/mercurial/logcmdutil.py	Fri Dec 15 11:08:41 2023 +0100
@@ -11,18 +11,18 @@
 import posixpath
 
 from .i18n import _
-from .node import nullrev, wdirrev
+from .node import wdirrev
 
 from .thirdparty import attr
 
 from . import (
     dagop,
+    diffutil,
     error,
     formatter,
     graphmod,
     match as matchmod,
     mdiff,
-    merge,
     patch,
     pathutil,
     pycompat,
@@ -69,36 +69,7 @@
     return limit
 
 
-def diff_parent(ctx):
-    """get the context object to use as parent when diffing
-
-
-    If diff.merge is enabled, an overlayworkingctx of the auto-merged parents will be returned.
-    """
-    repo = ctx.repo()
-    if repo.ui.configbool(b"diff", b"merge") and ctx.p2().rev() != nullrev:
-        # avoid cycle context -> subrepo -> cmdutil -> logcmdutil
-        from . import context
-
-        wctx = context.overlayworkingctx(repo)
-        wctx.setbase(ctx.p1())
-        with repo.ui.configoverride(
-            {
-                (
-                    b"ui",
-                    b"forcemerge",
-                ): b"internal:merge3-lie-about-conflicts",
-            },
-            b"merge-diff",
-        ):
-            with repo.ui.silent():
-                merge.merge(ctx.p2(), wc=wctx)
-        return wctx
-    else:
-        return ctx.p1()
-
-
-def diffordiffstat(
+def get_diff_chunks(
     ui,
     repo,
     diffopts,
@@ -107,14 +78,10 @@
     match,
     changes=None,
     stat=False,
-    fp=None,
-    graphwidth=0,
     prefix=b'',
     root=b'',
-    listsubrepos=False,
     hunksfilterfn=None,
 ):
-    '''show diff or diffstat.'''
     if root:
         relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
     else:
@@ -159,14 +126,11 @@
 
     if stat:
         diffopts = diffopts.copy(context=0, noprefix=False)
-        width = 80
-        if not ui.plain():
-            width = ui.termwidth() - graphwidth
         # If an explicit --root was given, don't respect ui.relative-paths
         if not relroot:
             pathfn = compose(scmutil.getuipathfn(repo), pathfn)
 
-    chunks = ctx2.diff(
+    return ctx2.diff(
         ctx1,
         match,
         changes,
@@ -176,6 +140,45 @@
         hunksfilterfn=hunksfilterfn,
     )
 
+
+def diffordiffstat(
+    ui,
+    repo,
+    diffopts,
+    ctx1,
+    ctx2,
+    match,
+    changes=None,
+    stat=False,
+    fp=None,
+    graphwidth=0,
+    prefix=b'',
+    root=b'',
+    listsubrepos=False,
+    hunksfilterfn=None,
+):
+    '''show diff or diffstat.'''
+
+    chunks = get_diff_chunks(
+        ui,
+        repo,
+        diffopts,
+        ctx1,
+        ctx2,
+        match,
+        changes=changes,
+        stat=stat,
+        prefix=prefix,
+        root=root,
+        hunksfilterfn=hunksfilterfn,
+    )
+
+    if stat:
+        diffopts = diffopts.copy(context=0, noprefix=False)
+        width = 80
+        if not ui.plain():
+            width = ui.termwidth() - graphwidth
+
     if fp is not None or ui.canwritewithoutlabels():
         out = fp or ui
         if stat:
@@ -241,7 +244,7 @@
             ui,
             ctx.repo(),
             diffopts,
-            diff_parent(ctx),
+            diffutil.diff_parent(ctx),
             ctx,
             match=self._makefilematcher(ctx),
             stat=stat,
@@ -249,6 +252,33 @@
             hunksfilterfn=self._makehunksfilter(ctx),
         )
 
+    def getdiffstats(self, ui, ctx, diffopts, stat=False):
+        chunks = get_diff_chunks(
+            ui,
+            ctx.repo(),
+            diffopts,
+            diffutil.diff_parent(ctx),
+            ctx,
+            match=self._makefilematcher(ctx),
+            stat=stat,
+            hunksfilterfn=self._makehunksfilter(ctx),
+        )
+
+        diffdata = []
+        for filename, additions, removals, binary in patch.diffstatdata(
+            util.iterlines(chunks)
+        ):
+            diffdata.append(
+                {
+                    b"name": filename,
+                    b"additions": additions,
+                    b"removals": removals,
+                    b"binary": binary,
+                }
+            )
+
+        return diffdata
+
 
 def changesetlabels(ctx):
     labels = [b'log.changeset', b'changeset.%s' % ctx.phasestr()]
@@ -525,9 +555,10 @@
             )
 
         if self._includestat or b'diffstat' in datahint:
-            self.ui.pushbuffer()
-            self._differ.showdiff(self.ui, ctx, self._diffopts, stat=True)
-            fm.data(diffstat=self.ui.popbuffer())
+            data = self._differ.getdiffstats(
+                self.ui, ctx, self._diffopts, stat=True
+            )
+            fm.data(diffstat=fm.formatlist(data, name=b'diffstat'))
         if self._includediff or b'diff' in datahint:
             self.ui.pushbuffer()
             self._differ.showdiff(self.ui, ctx, self._diffopts, stat=False)
--- a/mercurial/pycompat.py	Thu Dec 07 03:49:48 2023 +0100
+++ b/mercurial/pycompat.py	Fri Dec 15 11:08:41 2023 +0100
@@ -12,7 +12,6 @@
 import builtins
 import codecs
 import concurrent.futures as futures
-import functools
 import getopt
 import http.client as httplib
 import http.cookiejar as cookielib
@@ -352,26 +351,11 @@
     return sysbytes(doc)
 
 
-def _wrapattrfunc(f):
-    @functools.wraps(f)
-    def w(object, name, *args):
-        if isinstance(name, bytes):
-            from . import util
-
-            msg = b'function "%s" take `str` as argument, not `bytes`'
-            fname = f.__name__.encode('ascii')
-            msg %= fname
-            util.nouideprecwarn(msg, b"6.6", stacklevel=2)
-        return f(object, sysstr(name), *args)
-
-    return w
-
-
 # these wrappers are automagically imported by hgloader
-delattr = _wrapattrfunc(builtins.delattr)
-getattr = _wrapattrfunc(builtins.getattr)
-hasattr = _wrapattrfunc(builtins.hasattr)
-setattr = _wrapattrfunc(builtins.setattr)
+delattr = builtins.delattr
+getattr = builtins.getattr
+hasattr = builtins.hasattr
+setattr = builtins.setattr
 xrange = builtins.range
 unicode = str
 
@@ -386,7 +370,7 @@
     return builtins.open(name, sysstr(mode), buffering, encoding)
 
 
-safehasattr = _wrapattrfunc(builtins.hasattr)
+safehasattr = builtins.hasattr
 
 
 def _getoptbwrapper(
--- a/mercurial/revlog.py	Thu Dec 07 03:49:48 2023 +0100
+++ b/mercurial/revlog.py	Fri Dec 15 11:08:41 2023 +0100
@@ -1391,194 +1391,6 @@
         self._load_inner(chunk_cache)
         self._concurrencychecker = concurrencychecker
 
-    @property
-    def _generaldelta(self):
-        """temporary compatibility proxy"""
-        util.nouideprecwarn(
-            b"use revlog.delta_config.general_delta", b"6.6", stacklevel=2
-        )
-        return self.delta_config.general_delta
-
-    @property
-    def _checkambig(self):
-        """temporary compatibility proxy"""
-        util.nouideprecwarn(
-            b"use revlog.data_config.checkambig", b"6.6", stacklevel=2
-        )
-        return self.data_config.check_ambig
-
-    @property
-    def _mmaplargeindex(self):
-        """temporary compatibility proxy"""
-        util.nouideprecwarn(
-            b"use revlog.data_config.mmap_large_index", b"6.6", stacklevel=2
-        )
-        return self.data_config.mmap_large_index
-
-    @property
-    def _censorable(self):
-        """temporary compatibility proxy"""
-        util.nouideprecwarn(
-            b"use revlog.feature_config.censorable", b"6.6", stacklevel=2
-        )
-        return self.feature_config.censorable
-
-    @property
-    def _chunkcachesize(self):
-        """temporary compatibility proxy"""
-        util.nouideprecwarn(
-            b"use revlog.data_config.chunk_cache_size", b"6.6", stacklevel=2
-        )
-        return self.data_config.chunk_cache_size
-
-    @property
-    def _maxchainlen(self):
-        """temporary compatibility proxy"""
-        util.nouideprecwarn(
-            b"use revlog.delta_config.max_chain_len", b"6.6", stacklevel=2
-        )
-        return self.delta_config.max_chain_len
-
-    @property
-    def _deltabothparents(self):
-        """temporary compatibility proxy"""
-        util.nouideprecwarn(
-            b"use revlog.delta_config.delta_both_parents", b"6.6", stacklevel=2
-        )
-        return self.delta_config.delta_both_parents
-
-    @property
-    def _candidate_group_chunk_size(self):
-        """temporary compatibility proxy"""
-        util.nouideprecwarn(
-            b"use revlog.delta_config.candidate_group_chunk_size",
-            b"6.6",
-            stacklevel=2,
-        )
-        return self.delta_config.candidate_group_chunk_size
-
-    @property
-    def _debug_delta(self):
-        """temporary compatibility proxy"""
-        util.nouideprecwarn(
-            b"use revlog.delta_config.debug_delta", b"6.6", stacklevel=2
-        )
-        return self.delta_config.debug_delta
-
-    @property
-    def _compengine(self):
-        """temporary compatibility proxy"""
-        util.nouideprecwarn(
-            b"use revlog.feature_config.compression_engine",
-            b"6.6",
-            stacklevel=2,
-        )
-        return self.feature_config.compression_engine
-
-    @property
-    def upperboundcomp(self):
-        """temporary compatibility proxy"""
-        util.nouideprecwarn(
-            b"use revlog.delta_config.upper_bound_comp",
-            b"6.6",
-            stacklevel=2,
-        )
-        return self.delta_config.upper_bound_comp
-
-    @property
-    def _compengineopts(self):
-        """temporary compatibility proxy"""
-        util.nouideprecwarn(
-            b"use revlog.feature_config.compression_engine_options",
-            b"6.6",
-            stacklevel=2,
-        )
-        return self.feature_config.compression_engine_options
-
-    @property
-    def _maxdeltachainspan(self):
-        """temporary compatibility proxy"""
-        util.nouideprecwarn(
-            b"use revlog.delta_config.max_deltachain_span", b"6.6", stacklevel=2
-        )
-        return self.delta_config.max_deltachain_span
-
-    @property
-    def _withsparseread(self):
-        """temporary compatibility proxy"""
-        util.nouideprecwarn(
-            b"use revlog.data_config.with_sparse_read", b"6.6", stacklevel=2
-        )
-        return self.data_config.with_sparse_read
-
-    @property
-    def _sparserevlog(self):
-        """temporary compatibility proxy"""
-        util.nouideprecwarn(
-            b"use revlog.delta_config.sparse_revlog", b"6.6", stacklevel=2
-        )
-        return self.delta_config.sparse_revlog
-
-    @property
-    def hassidedata(self):
-        """temporary compatibility proxy"""
-        util.nouideprecwarn(
-            b"use revlog.feature_config.has_side_data", b"6.6", stacklevel=2
-        )
-        return self.feature_config.has_side_data
-
-    @property
-    def _srdensitythreshold(self):
-        """temporary compatibility proxy"""
-        util.nouideprecwarn(
-            b"use revlog.data_config.sr_density_threshold",
-            b"6.6",
-            stacklevel=2,
-        )
-        return self.data_config.sr_density_threshold
-
-    @property
-    def _srmingapsize(self):
-        """temporary compatibility proxy"""
-        util.nouideprecwarn(
-            b"use revlog.data_config.sr_min_gap_size", b"6.6", stacklevel=2
-        )
-        return self.data_config.sr_min_gap_size
-
-    @property
-    def _compute_rank(self):
-        """temporary compatibility proxy"""
-        util.nouideprecwarn(
-            b"use revlog.feature_config.compute_rank", b"6.6", stacklevel=2
-        )
-        return self.feature_config.compute_rank
-
-    @property
-    def canonical_parent_order(self):
-        """temporary compatibility proxy"""
-        util.nouideprecwarn(
-            b"use revlog.feature_config.canonical_parent_order",
-            b"6.6",
-            stacklevel=2,
-        )
-        return self.feature_config.canonical_parent_order
-
-    @property
-    def _lazydelta(self):
-        """temporary compatibility proxy"""
-        util.nouideprecwarn(
-            b"use revlog.delta_config.lazy_delta", b"6.6", stacklevel=2
-        )
-        return self.delta_config.lazy_delta
-
-    @property
-    def _lazydeltabase(self):
-        """temporary compatibility proxy"""
-        util.nouideprecwarn(
-            b"use revlog.delta_config.lazy_delta_base", b"6.6", stacklevel=2
-        )
-        return self.delta_config.lazy_delta_base
-
     def _init_opts(self):
         """process options (from above/config) to setup associated default revlog mode
 
--- a/mercurial/statprof.py	Thu Dec 07 03:49:48 2023 +0100
+++ b/mercurial/statprof.py	Fri Dec 15 11:08:41 2023 +0100
@@ -384,7 +384,7 @@
             time = sample.time
             stack = sample.stack
             sites = [
-                b'\1'.join([s.path, b'%d' % s.lineno, s.function])
+                b'\1'.join([s.path, b'%d' % s.lineno or -1, s.function])
                 for s in stack
             ]
             file.write(b"%d\0%s\n" % (time, b'\0'.join(sites)))
@@ -663,7 +663,7 @@
                 count / relevant_samples * 100,
                 pycompat.fsencode(parent.filename()),
                 pycompat.sysbytes(parent.function),
-                parent.lineno,
+                parent.lineno or -1,
                 pycompat.sysbytes(parent.getsource(50)),
             )
         )
@@ -705,7 +705,7 @@
             b'        %6.2f%%   line %s: %s\n'
             % (
                 count / relevant_samples * 100,
-                child.lineno,
+                child.lineno or -1,
                 pycompat.sysbytes(child.getsource(50)),
             )
         )
@@ -865,7 +865,7 @@
             stack.append(
                 (
                     pycompat.sysstr(frame.path),
-                    frame.lineno,
+                    frame.lineno or -1,
                     pycompat.sysstr(frame.function),
                 )
             )
@@ -954,7 +954,10 @@
             (
                 (
                     '%s:%d'
-                    % (simplifypath(pycompat.sysstr(frame.path)), frame.lineno),
+                    % (
+                        simplifypath(pycompat.sysstr(frame.path)),
+                        frame.lineno or -1,
+                    ),
                     pycompat.sysstr(frame.function),
                 )
                 for frame in sample.stack
--- a/mercurial/templatekw.py	Thu Dec 07 03:49:48 2023 +0100
+++ b/mercurial/templatekw.py	Fri Dec 15 11:08:41 2023 +0100
@@ -270,7 +270,7 @@
     ui = context.resource(mapping, b'ui')
     ctx = context.resource(mapping, b'ctx')
     diffopts = diffutil.diffallopts(ui, {b'noprefix': False})
-    diff = ctx.diff(opts=diffopts)
+    diff = ctx.diff(diffutil.diff_parent(ctx), opts=diffopts)
     stats = patch.diffstatdata(util.iterlines(diff))
     maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
     return b'%d: +%d/-%d' % (len(stats), adds, removes)
--- a/mercurial/upgrade_utils/actions.py	Thu Dec 07 03:49:48 2023 +0100
+++ b/mercurial/upgrade_utils/actions.py	Fri Dec 15 11:08:41 2023 +0100
@@ -5,6 +5,7 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
+import random
 
 from ..i18n import _
 from .. import (
@@ -409,9 +410,17 @@
     def fromrepo(repo):
         # Mercurial 4.0 changed changelogs to not use delta chains. Search for
         # changelogs with deltas.
-        cl = repo.changelog
+        cl = repo.unfiltered().changelog
+        if len(cl) <= 1000:
+            some_rev = list(cl)
+        else:
+            # do a random sampling to speeds things up Scanning the whole
+            # repository can get really slow on bigger repo.
+            some_rev = sorted(
+                {random.randint(0, len(cl) - 1) for x in range(1000)}
+            )
         chainbase = cl.chainbase
-        return all(rev == chainbase(rev) for rev in cl)
+        return all(rev == chainbase(rev) for rev in some_rev)
 
     @staticmethod
     def fromconfig(repo):
--- a/mercurial/utils/urlutil.py	Thu Dec 07 03:49:48 2023 +0100
+++ b/mercurial/utils/urlutil.py	Fri Dec 15 11:08:41 2023 +0100
@@ -14,7 +14,6 @@
     error,
     pycompat,
     urllibcompat,
-    util,
 )
 
 from . import (
@@ -680,8 +679,7 @@
     """
     if isinstance(attr, bytes):
         msg = b'pathsuboption take `str` as "attr" argument, not `bytes`'
-        util.nouideprecwarn(msg, b"6.6", stacklevel=2)
-        attr = attr.decode('ascii')
+        raise TypeError(msg)
 
     def register(func):
         _pathsuboptions[option] = (attr, func)
@@ -923,14 +921,6 @@
             new._setup_url(self._pushloc)
         return new
 
-    def pushloc(self):
-        """compatibility layer for the deprecated attributes"""
-        from .. import util  # avoid a cycle
-
-        msg = "don't use path.pushloc, use path.get_push_variant()"
-        util.nouideprecwarn(msg, b"6.5")
-        return self._pushloc
-
     def _validate_path(self):
         # When given a raw location but not a symbolic name, validate the
         # location is valid.
--- a/rust/hg-core/src/operations/mod.rs	Thu Dec 07 03:49:48 2023 +0100
+++ b/rust/hg-core/src/operations/mod.rs	Fri Dec 15 11:08:41 2023 +0100
@@ -5,6 +5,8 @@
 mod cat;
 mod debugdata;
 mod list_tracked_files;
+mod status_rev_rev;
 pub use cat::{cat, CatOutput};
 pub use debugdata::{debug_data, DebugDataKind};
 pub use list_tracked_files::{list_rev_tracked_files, FilesForRev};
+pub use status_rev_rev::{status_rev_rev_no_copies, DiffStatus, StatusRevRev};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rust/hg-core/src/operations/status_rev_rev.rs	Fri Dec 15 11:08:41 2023 +0100
@@ -0,0 +1,89 @@
+use crate::errors::HgError;
+use crate::matchers::Matcher;
+use crate::repo::Repo;
+use crate::revlog::manifest::Manifest;
+use crate::utils::filter_map_results;
+use crate::utils::hg_path::HgPath;
+use crate::utils::merge_join_results_by;
+
+use crate::Revision;
+
+use itertools::EitherOrBoth;
+
+#[derive(Debug, Copy, Clone)]
+pub enum DiffStatus {
+    Removed,
+    Added,
+    Matching,
+    Modified,
+}
+
+pub struct StatusRevRev {
+    manifest1: Manifest,
+    manifest2: Manifest,
+    narrow_matcher: Box<dyn Matcher>,
+}
+
+fn manifest_for_rev(repo: &Repo, rev: Revision) -> Result<Manifest, HgError> {
+    repo.manifest_for_rev(rev.into()).map_err(|e| {
+        HgError::corrupted(format!(
+            "manifest lookup failed for revision {}: {}",
+            rev, e
+        ))
+    })
+}
+
+pub fn status_rev_rev_no_copies(
+    repo: &Repo,
+    rev1: Revision,
+    rev2: Revision,
+    narrow_matcher: Box<dyn Matcher>,
+) -> Result<StatusRevRev, HgError> {
+    let manifest1 = manifest_for_rev(repo, rev1)?;
+    let manifest2 = manifest_for_rev(repo, rev2)?;
+    Ok(StatusRevRev {
+        manifest1,
+        manifest2,
+        narrow_matcher,
+    })
+}
+
+impl StatusRevRev {
+    pub fn iter(
+        &self,
+    ) -> impl Iterator<Item = Result<(&HgPath, DiffStatus), HgError>> {
+        let iter1 = self.manifest1.iter();
+        let iter2 = self.manifest2.iter();
+
+        let merged =
+            merge_join_results_by(iter1, iter2, |i1, i2| i1.path.cmp(i2.path));
+
+        filter_map_results(merged, |entry| {
+            let (path, status) = match entry {
+                EitherOrBoth::Left(entry) => {
+                    let path = entry.path;
+                    (path, DiffStatus::Removed)
+                }
+                EitherOrBoth::Right(entry) => {
+                    let path = entry.path;
+                    (path, DiffStatus::Added)
+                }
+                EitherOrBoth::Both(entry1, entry2) => {
+                    let path = entry1.path;
+                    if entry1.node_id().unwrap() == entry2.node_id().unwrap()
+                        && entry1.flags == entry2.flags
+                    {
+                        (path, DiffStatus::Matching)
+                    } else {
+                        (path, DiffStatus::Modified)
+                    }
+                }
+            };
+            Ok(if self.narrow_matcher.matches(path) {
+                Some((path, status))
+            } else {
+                None
+            })
+        })
+    }
+}
--- a/rust/hg-core/src/utils.rs	Thu Dec 07 03:49:48 2023 +0100
+++ b/rust/hg-core/src/utils.rs	Fri Dec 15 11:08:41 2023 +0100
@@ -11,7 +11,10 @@
 use crate::utils::hg_path::HgPath;
 use im_rc::ordmap::DiffItem;
 use im_rc::ordmap::OrdMap;
+use itertools::EitherOrBoth;
+use itertools::Itertools;
 use std::cell::Cell;
+use std::cmp::Ordering;
 use std::fmt;
 use std::{io::Write, ops::Deref};
 
@@ -499,6 +502,43 @@
     })
 }
 
+/// Like `itertools::merge_join_by`, but merges fallible iterators.
+///
+/// The callback is only used for Ok values. Errors are passed through as-is.
+/// Errors compare less than Ok values, which makes the error handling
+/// conservative.
+pub fn merge_join_results_by<'a, I1, I2, F, A, B, E>(
+    iter1: I1,
+    iter2: I2,
+    f: F,
+) -> impl Iterator<Item = Result<EitherOrBoth<A, B>, E>> + 'a
+where
+    I1: Iterator<Item = Result<A, E>> + 'a,
+    I2: Iterator<Item = Result<B, E>> + 'a,
+    F: FnMut(&A, &B) -> Ordering + 'a,
+{
+    let mut g = f;
+    iter1
+        .merge_join_by(iter2, move |i1, i2| match i1 {
+            Err(_) => Ordering::Less,
+            Ok(i1) => match i2 {
+                Err(_) => Ordering::Greater,
+                Ok(i2) => g(i1, i2),
+            },
+        })
+        .map(|result| match result {
+            EitherOrBoth::Left(Err(e)) => Err(e),
+            EitherOrBoth::Right(Err(e)) => Err(e),
+            EitherOrBoth::Both(Err(e), _) => Err(e),
+            EitherOrBoth::Both(_, Err(e)) => Err(e),
+            EitherOrBoth::Left(Ok(v)) => Ok(EitherOrBoth::Left(v)),
+            EitherOrBoth::Right(Ok(v)) => Ok(EitherOrBoth::Right(v)),
+            EitherOrBoth::Both(Ok(v1), Ok(v2)) => {
+                Ok(EitherOrBoth::Both(v1, v2))
+            }
+        })
+}
+
 /// Force the global rayon threadpool to not exceed 16 concurrent threads
 /// unless the user has specified a value.
 /// This is a stop-gap measure until we figure out why using more than 16
--- a/rust/rhg/src/commands/status.rs	Thu Dec 07 03:49:48 2023 +0100
+++ b/rust/rhg/src/commands/status.rs	Fri Dec 15 11:08:41 2023 +0100
@@ -30,12 +30,15 @@
 use hg::utils::hg_path::{hg_path_to_path_buf, HgPath};
 use hg::DirstateStatus;
 use hg::PatternFileWarning;
+use hg::Revision;
 use hg::StatusError;
 use hg::StatusOptions;
 use hg::{self, narrow, sparse};
 use log::info;
 use rayon::prelude::*;
+use std::borrow::Cow;
 use std::io;
+use std::mem::take;
 use std::path::PathBuf;
 
 pub const HELP_TEXT: &str = "
@@ -140,6 +143,38 @@
                 .action(clap::ArgAction::SetTrue)
                 .long("verbose"),
         )
+        .arg(
+            Arg::new("rev")
+                .help("show difference from/to revision")
+                .long("rev")
+                .num_args(1)
+                .action(clap::ArgAction::Append)
+                .value_name("REV"),
+        )
+}
+
+fn parse_revpair(
+    repo: &Repo,
+    revs: Option<Vec<String>>,
+) -> Result<Option<(Revision, Revision)>, CommandError> {
+    let revs = match revs {
+        None => return Ok(None),
+        Some(revs) => revs,
+    };
+    if revs.is_empty() {
+        return Ok(None);
+    }
+    if revs.len() != 2 {
+        return Err(CommandError::unsupported("expected 0 or 2 --rev flags"));
+    }
+
+    let rev1 = &revs[0];
+    let rev2 = &revs[1];
+    let rev1 = hg::revset::resolve_single(rev1, repo)
+        .map_err(|e| (e, rev1.as_str()))?;
+    let rev2 = hg::revset::resolve_single(rev2, repo)
+        .map_err(|e| (e, rev2.as_str()))?;
+    Ok(Some((rev1, rev2)))
 }
 
 /// Pure data type allowing the caller to specify file states to display
@@ -229,6 +264,7 @@
     let config = invocation.config;
     let args = invocation.subcommand_args;
 
+    let revs = args.get_many::<String>("rev");
     let print0 = args.get_flag("print0");
     let verbose = args.get_flag("verbose")
         || config.get_bool(b"ui", b"verbose")?
@@ -262,6 +298,7 @@
         || config.get_bool(b"ui", b"statuscopies")?;
 
     let repo = invocation.repo?;
+    let revpair = parse_revpair(repo, revs.map(|i| i.cloned().collect()))?;
 
     if verbose && has_unfinished_state(repo)? {
         return Err(CommandError::unsupported(
@@ -285,13 +322,37 @@
     type StatusResult<'a> =
         Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>;
 
+    let relative_status = config
+        .get_option(b"commands", b"status.relative")?
+        .expect("commands.status.relative should have a default value");
+
+    let relativize_paths = relative_status || {
+        // See in Python code with `getuipathfn` usage in `commands.py`.
+        let legacy_relative_behavior = args.contains_id("file");
+        match relative_paths(invocation.config)? {
+            RelativePaths::Legacy => legacy_relative_behavior,
+            RelativePaths::Bool(v) => v,
+        }
+    };
+
+    let mut output = DisplayStatusPaths {
+        ui,
+        no_status,
+        relativize: if relativize_paths {
+            Some(RelativizePaths::new(repo)?)
+        } else {
+            None
+        },
+        print0,
+    };
+
     let after_status = |res: StatusResult| -> Result<_, CommandError> {
         let (mut ds_status, pattern_warnings) = res?;
         for warning in pattern_warnings {
             ui.write_stderr(&format_pattern_file_warning(&warning, repo))?;
         }
 
-        for (path, error) in ds_status.bad {
+        for (path, error) in take(&mut ds_status.bad) {
             let error = match error {
                 hg::BadMatch::OsError(code) => {
                     std::io::Error::from_raw_os_error(code).to_string()
@@ -322,8 +383,7 @@
             })?;
             let working_directory_vfs = repo.working_directory_vfs();
             let store_vfs = repo.store_vfs();
-            let res: Vec<_> = ds_status
-                .unsure
+            let res: Vec<_> = take(&mut ds_status.unsure)
                 .into_par_iter()
                 .map(|to_check| {
                     // The compiler seems to get a bit confused with complex
@@ -370,55 +430,12 @@
             }
         }
 
-        let relative_status = config
-            .get_option(b"commands", b"status.relative")?
-            .expect("commands.status.relative should have a default value");
-
-        let relativize_paths = relative_status || {
-            // See in Python code with `getuipathfn` usage in `commands.py`.
-            let legacy_relative_behavior = args.contains_id("file");
-            match relative_paths(invocation.config)? {
-                RelativePaths::Legacy => legacy_relative_behavior,
-                RelativePaths::Bool(v) => v,
-            }
-        };
-
-        let output = DisplayStatusPaths {
-            ui,
-            no_status,
-            relativize: if relativize_paths {
-                Some(RelativizePaths::new(repo)?)
-            } else {
-                None
-            },
-            print0,
-        };
-        if display_states.modified {
-            output.display(b"M ", "status.modified", ds_status.modified)?;
-        }
-        if display_states.added {
-            output.display(b"A ", "status.added", ds_status.added)?;
-        }
-        if display_states.removed {
-            output.display(b"R ", "status.removed", ds_status.removed)?;
-        }
-        if display_states.deleted {
-            output.display(b"! ", "status.deleted", ds_status.deleted)?;
-        }
-        if display_states.unknown {
-            output.display(b"? ", "status.unknown", ds_status.unknown)?;
-        }
-        if display_states.ignored {
-            output.display(b"I ", "status.ignored", ds_status.ignored)?;
-        }
-        if display_states.clean {
-            output.display(b"C ", "status.clean", ds_status.clean)?;
-        }
-
         let dirstate_write_needed = ds_status.dirty;
         let filesystem_time_at_status_start =
             ds_status.filesystem_time_at_status_start;
 
+        output.output(display_states, ds_status)?;
+
         Ok((
             fixup,
             dirstate_write_needed,
@@ -426,6 +443,57 @@
         ))
     };
     let (narrow_matcher, narrow_warnings) = narrow::matcher(repo)?;
+
+    match revpair {
+        Some((rev1, rev2)) => {
+            let mut ds_status = DirstateStatus::default();
+            if list_copies {
+                return Err(CommandError::unsupported(
+                    "status --rev --rev with copy information is not implemented yet",
+                ));
+            }
+
+            let stat = hg::operations::status_rev_rev_no_copies(
+                repo,
+                rev1,
+                rev2,
+                narrow_matcher,
+            )?;
+            for entry in stat.iter() {
+                let (path, status) = entry?;
+                let path = StatusPath {
+                    path: Cow::Borrowed(path),
+                    copy_source: None,
+                };
+                match status {
+                    hg::operations::DiffStatus::Removed => {
+                        if display_states.removed {
+                            ds_status.removed.push(path)
+                        }
+                    }
+                    hg::operations::DiffStatus::Added => {
+                        if display_states.added {
+                            ds_status.added.push(path)
+                        }
+                    }
+                    hg::operations::DiffStatus::Modified => {
+                        if display_states.modified {
+                            ds_status.modified.push(path)
+                        }
+                    }
+                    hg::operations::DiffStatus::Matching => {
+                        if display_states.clean {
+                            ds_status.clean.push(path)
+                        }
+                    }
+                }
+            }
+            output.output(display_states, ds_status)?;
+            return Ok(());
+        }
+        None => (),
+    }
+
     let (sparse_matcher, sparse_warnings) = sparse::matcher(repo)?;
     let matcher = match (repo.has_narrow(), repo.has_sparse()) {
         (true, true) => {
@@ -628,6 +696,35 @@
         }
         Ok(())
     }
+
+    fn output(
+        &mut self,
+        display_states: DisplayStates,
+        ds_status: DirstateStatus,
+    ) -> Result<(), CommandError> {
+        if display_states.modified {
+            self.display(b"M ", "status.modified", ds_status.modified)?;
+        }
+        if display_states.added {
+            self.display(b"A ", "status.added", ds_status.added)?;
+        }
+        if display_states.removed {
+            self.display(b"R ", "status.removed", ds_status.removed)?;
+        }
+        if display_states.deleted {
+            self.display(b"! ", "status.deleted", ds_status.deleted)?;
+        }
+        if display_states.unknown {
+            self.display(b"? ", "status.unknown", ds_status.unknown)?;
+        }
+        if display_states.ignored {
+            self.display(b"I ", "status.ignored", ds_status.ignored)?;
+        }
+        if display_states.clean {
+            self.display(b"C ", "status.clean", ds_status.clean)?;
+        }
+        Ok(())
+    }
 }
 
 /// Outcome of the additional check for an ambiguous tracked file
--- a/rust/rhg/src/main.rs	Thu Dec 07 03:49:48 2023 +0100
+++ b/rust/rhg/src/main.rs	Fri Dec 15 11:08:41 2023 +0100
@@ -524,13 +524,20 @@
     std::process::exit(exit_code(&result, use_detailed_exit_code))
 }
 
+mod commands {
+    pub mod cat;
+    pub mod config;
+    pub mod debugdata;
+    pub mod debugignorerhg;
+    pub mod debugrequirements;
+    pub mod debugrhgsparse;
+    pub mod files;
+    pub mod root;
+    pub mod status;
+}
+
 macro_rules! subcommands {
     ($( $command: ident )+) => {
-        mod commands {
-            $(
-                pub mod $command;
-            )+
-        }
 
         fn add_subcommand_args(app: clap::Command) -> clap::Command {
             app
--- a/setup.py	Thu Dec 07 03:49:48 2023 +0100
+++ b/setup.py	Fri Dec 15 11:08:41 2023 +0100
@@ -221,6 +221,9 @@
         self.cmd = cmd
         self.env = env
 
+    def __repr__(self):
+        return f"<hgcommand cmd={self.cmd} env={self.env}>"
+
     def run(self, args):
         cmd = self.cmd + args
         returncode, out, err = runcmd(cmd, self.env)
@@ -295,9 +298,15 @@
         if attempt(hgcmd + check_cmd, hgenv):
             return hgcommand(hgcmd, hgenv)
 
-    # Fall back to trying the local hg installation.
+    # Fall back to trying the local hg installation (pure python)
+    repo_hg = os.path.join(os.path.dirname(__file__), 'hg')
     hgenv = localhgenv()
-    hgcmd = [sys.executable, 'hg']
+    hgcmd = [sys.executable, repo_hg]
+    if attempt(hgcmd + check_cmd, hgenv):
+        return hgcommand(hgcmd, hgenv)
+    # Fall back to trying the local hg installation (whatever we can)
+    hgenv = localhgenv(pure_python=False)
+    hgcmd = [sys.executable, repo_hg]
     if attempt(hgcmd + check_cmd, hgenv):
         return hgcommand(hgcmd, hgenv)
 
@@ -319,17 +328,18 @@
     return None
 
 
-def localhgenv():
+def localhgenv(pure_python=True):
     """Get an environment dictionary to use for invoking or importing
     mercurial from the local repository."""
     # Execute hg out of this directory with a custom environment which takes
     # care to not use any hgrc files and do no localization.
     env = {
-        'HGMODULEPOLICY': 'py',
         'HGRCPATH': '',
         'LANGUAGE': 'C',
         'PATH': '',
     }  # make pypi modules that use os.environ['PATH'] happy
+    if pure_python:
+        env['HGMODULEPOLICY'] = 'py'
     if 'LD_LIBRARY_PATH' in os.environ:
         env['LD_LIBRARY_PATH'] = os.environ['LD_LIBRARY_PATH']
     if 'SystemRoot' in os.environ:
@@ -1821,5 +1831,5 @@
             'welcome': 'contrib/packaging/macosx/Welcome.html',
         },
     },
-    **extra
+    **extra,
 )
--- a/tests/test-log.t	Thu Dec 07 03:49:48 2023 +0100
+++ b/tests/test-log.t	Fri Dec 15 11:08:41 2023 +0100
@@ -2001,6 +2001,8 @@
   @@ -0,0 +1,1 @@
   +b
   
+  $ hg log -r 3 -T'{diffstat}\n'
+  2: +2/-1
 
 Test that diff.merge is respected (file b was added on one side and
 and therefore merged cleanly)
@@ -2021,6 +2023,9 @@
   -b
   +c
   
+  $ hg log -r 3 -T'{diffstat}\n' --config diff.merge=yes
+  1: +1/-1
+
   $ cd ..
 
 'hg log -r rev fn' when last(filelog(fn)) != rev
--- a/tests/test-status-rev.t	Thu Dec 07 03:49:48 2023 +0100
+++ b/tests/test-status-rev.t	Fri Dec 15 11:08:41 2023 +0100
@@ -88,6 +88,33 @@
 
 Status between first and second commit. Should ignore dirstate status.
 
+  $ hg status -marc --rev 0 --rev 1 --config rhg.on-unsupported=abort
+  M content1_content2_content1-tracked
+  M content1_content2_content1-untracked
+  M content1_content2_content2-tracked
+  M content1_content2_content2-untracked
+  M content1_content2_content3-tracked
+  M content1_content2_content3-untracked
+  M content1_content2_missing-tracked
+  M content1_content2_missing-untracked
+  A missing_content2_content2-tracked
+  A missing_content2_content2-untracked
+  A missing_content2_content3-tracked
+  A missing_content2_content3-untracked
+  A missing_content2_missing-tracked
+  A missing_content2_missing-untracked
+  R content1_missing_content1-tracked
+  R content1_missing_content1-untracked
+  R content1_missing_content3-tracked
+  R content1_missing_content3-untracked
+  R content1_missing_missing-tracked
+  R content1_missing_missing-untracked
+  C content1_content1_content1-tracked
+  C content1_content1_content1-untracked
+  C content1_content1_content3-tracked
+  C content1_content1_content3-untracked
+  C content1_content1_missing-tracked
+  C content1_content1_missing-untracked
   $ hg status -A --rev 0:1 'glob:content1_content2_*'
   M content1_content2_content1-tracked
   M content1_content2_content1-untracked
--- a/tests/test-symlinks.t	Thu Dec 07 03:49:48 2023 +0100
+++ b/tests/test-symlinks.t	Fri Dec 15 11:08:41 2023 +0100
@@ -188,6 +188,35 @@
 
   $ cd ..
 
+== symlinks and add with --include ==
+
+directory moved and symlinked
+
+  $ hg init add-include
+  $ cd add-include
+  $ mkdir foo
+  $ touch foo/a
+  $ hg ci -Ama
+  adding foo/a
+  $ hg mv foo bar
+  moving foo/a to bar/a
+  $ ln -s bar foo
+  $ hg status
+  A bar/a
+  R foo/a
+  ? foo
+
+can add with --include
+
+  $ hg add -I foo
+  adding foo
+  $ hg status
+  A bar/a
+  A foo
+  R foo/a
+
+  $ cd ..
+
 == root of repository is symlinked ==
 
   $ hg init root
--- a/tests/test-template-map.t	Thu Dec 07 03:49:48 2023 +0100
+++ b/tests/test-template-map.t	Fri Dec 15 11:08:41 2023 +0100
@@ -766,7 +766,26 @@
     ],
     'desc': 'third',
     'diff': 'diff -r 29114dbae42b -r 95c24699272e fourth\n--- /dev/null\tThu Jan 01 00:00:00 1970 +0000\n+++ b/fourth\tWed Jan 01 10:01:00 2020 +0000\n@@ -0,0 +1,1 @@\n+second\ndiff -r 29114dbae42b -r 95c24699272e second\n--- a/second\tMon Jan 12 13:46:40 1970 +0000\n+++ /dev/null\tThu Jan 01 00:00:00 1970 +0000\n@@ -1,1 +0,0 @@\n-second\ndiff -r 29114dbae42b -r 95c24699272e third\n--- /dev/null\tThu Jan 01 00:00:00 1970 +0000\n+++ b/third\tWed Jan 01 10:01:00 2020 +0000\n@@ -0,0 +1,1 @@\n+third\n',
-    'diffstat': ' fourth |  1 +\n second |  1 -\n third  |  1 +\n 3 files changed, 2 insertions(+), 1 deletions(-)\n',
+    'diffstat': [
+     {
+      'additions': 1,
+      'binary': False,
+      'name': 'fourth',
+      'removals': 0
+     },
+     {
+      'additions': 0,
+      'binary': False,
+      'name': 'second',
+      'removals': 1
+     },
+     {
+      'additions': 1,
+      'binary': False,
+      'name': 'third',
+      'removals': 0
+     }
+    ],
     'files': [
      'fourth',
      'second',
@@ -820,7 +839,7 @@
     "date": [1577872860, 0],
     "desc": "third",
     "diff": "diff -r 29114dbae42b -r 95c24699272e fourth\n--- /dev/null\tThu Jan 01 00:00:00 1970 +0000\n+++ b/fourth\tWed Jan 01 10:01:00 2020 +0000\n@@ -0,0 +1,1 @@\n+second\ndiff -r 29114dbae42b -r 95c24699272e second\n--- a/second\tMon Jan 12 13:46:40 1970 +0000\n+++ /dev/null\tThu Jan 01 00:00:00 1970 +0000\n@@ -1,1 +0,0 @@\n-second\ndiff -r 29114dbae42b -r 95c24699272e third\n--- /dev/null\tThu Jan 01 00:00:00 1970 +0000\n+++ b/third\tWed Jan 01 10:01:00 2020 +0000\n@@ -0,0 +1,1 @@\n+third\n",
-    "diffstat": " fourth |  1 +\n second |  1 -\n third  |  1 +\n 3 files changed, 2 insertions(+), 1 deletions(-)\n",
+    "diffstat": [{"additions": 1, "binary": false, "name": "fourth", "removals": 0}, {"additions": 0, "binary": false, "name": "second", "removals": 1}, {"additions": 1, "binary": false, "name": "third", "removals": 0}],
     "files": ["fourth", "second", "third"],
     "node": "95c24699272ef57d062b8bccc32c878bf841784a",
     "parents": ["29114dbae42b9f078cf2714dbe3a86bba8ec7453"],
@@ -1180,7 +1199,7 @@
 
   $ hg log -r. -T'json(diffstat)'
   [
-   {"diffstat": " fourth |  1 +\n second |  1 -\n third  |  1 +\n 3 files changed, 2 insertions(+), 1 deletions(-)\n"}
+   {"diffstat": [{"additions": 1, "binary": false, "name": "fourth", "removals": 0}, {"additions": 0, "binary": false, "name": "second", "removals": 1}, {"additions": 1, "binary": false, "name": "third", "removals": 0}]}
   ]
 
   $ hg log -r. -T'json(manifest)'