changeset 51834:454feddab720

brancing: merge stable into default
author Pierre-Yves David <pierre-yves.david@octobus.net>
date Fri, 06 Sep 2024 02:12:19 +0200
parents 4eccb65e444f (diff) 6388fd855f66 (current diff)
children 6cc18075f82a
files hgext/histedit.py hgext/largefiles/overrides.py hgext/zeroconf/Zeroconf.py mercurial/hgweb/webcommands.py mercurial/keepalive.py mercurial/merge.py mercurial/metadata.py mercurial/patch.py mercurial/posix.py mercurial/statprof.py setup.py
diffstat 201 files changed, 2521 insertions(+), 1385 deletions(-) [+]
line wrap: on
line diff
--- a/contrib/automation/hgautomation/cli.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/contrib/automation/hgautomation/cli.py	Fri Sep 06 02:12:19 2024 +0200
@@ -201,7 +201,6 @@
     with aws.temporary_linux_dev_instances(
         c, image, instance_type, ensure_extra_volume=ensure_extra_volume
     ) as insts:
-
         instance = insts[0]
 
         linux.prepare_exec_environment(
--- a/contrib/check-config.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/contrib/check-config.py	Fri Sep 06 02:12:19 2024 +0200
@@ -57,7 +57,6 @@
             return b
         return b.decode('utf8')
 
-
 else:
     mkstr = lambda x: x
 
--- a/contrib/check-pytype.sh	Thu Sep 05 15:37:14 2024 -0400
+++ b/contrib/check-pytype.sh	Fri Sep 06 02:12:19 2024 +0200
@@ -5,6 +5,9 @@
 
 cd "$(hg root)"
 
+printf "pytype version: "
+pytype --version
+
 # Many of the individual files that are excluded here confuse pytype
 # because they do a mix of Python 2 and Python 3 things
 # conditionally. There's no good way to help it out with that as far as
@@ -54,9 +57,7 @@
 # mercurial/interfaces          # No attribute 'capabilities' on peer [attribute-error]
 # mercurial/keepalive.py        # [attribute-error]
 # mercurial/localrepo.py        # [attribute-error]
-# mercurial/manifest.py         # [unsupported-operands], [wrong-arg-types]
 # mercurial/minirst.py          # [unsupported-operands], [attribute-error]
-# mercurial/pure/osutil.py      # [invalid-typevar], [not-callable]
 # mercurial/pure/parsers.py     # [attribute-error]
 # mercurial/repoview.py         # [attribute-error]
 # mercurial/testing/storage.py  # tons of [attribute-error]
@@ -114,9 +115,7 @@
     -x mercurial/interfaces \
     -x mercurial/keepalive.py \
     -x mercurial/localrepo.py \
-    -x mercurial/manifest.py \
     -x mercurial/minirst.py \
-    -x mercurial/pure/osutil.py \
     -x mercurial/pure/parsers.py \
     -x mercurial/repoview.py \
     -x mercurial/testing/storage.py \
--- a/contrib/docker/pytype/Dockerfile	Thu Sep 05 15:37:14 2024 -0400
+++ b/contrib/docker/pytype/Dockerfile	Fri Sep 06 02:12:19 2024 +0200
@@ -1,4 +1,4 @@
-FROM registry.heptapod.net/mercurial/ci-images/mercurial-core:v2.0
+FROM registry.heptapod.net/mercurial/ci-images/mercurial-core:v2.1
 
 USER ci-runner
 
--- a/contrib/fuzz/mpatch_corpus.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/contrib/fuzz/mpatch_corpus.py	Fri Sep 06 02:12:19 2024 +0200
@@ -25,7 +25,6 @@
             """Py2 calls __repr__ for `bytes(foo)`, forward to __bytes__"""
             return self.__bytes__()
 
-
 else:
 
     class py2reprhack:
--- a/contrib/heptapod-ci.yml	Thu Sep 05 15:37:14 2024 -0400
+++ b/contrib/heptapod-ci.yml	Fri Sep 06 02:12:19 2024 +0200
@@ -27,7 +27,7 @@
 variables:
     PYTHON: python
     TEST_HGMODULEPOLICY: "allow"
-    HG_CI_IMAGE_TAG: "v1.0"
+    HG_CI_IMAGE_TAG: "v2.1"
     TEST_HGTESTS_ALLOW_NETIO: "0"
 
 .all_template: &all
@@ -39,15 +39,16 @@
     # The runner made a clone as root.
     # We make a new clone owned by user used to run the step.
     before_script:
+      - export PATH="/home/ci-runner/vendor/pyenv/pyenv-2.4.7-adf3c2bccf09cdb81febcfd15b186711a33ac7a8/shims:/home/ci-runner/vendor/pyenv/pyenv-2.4.7-adf3c2bccf09cdb81febcfd15b186711a33ac7a8/bin:$PATH"
+      - echo "python used, $PYTHON"
+      - $PYTHON --version
+      - black --version
+      - clang-format --version
       - hg clone . /tmp/mercurial-ci/ --noupdate --config phases.publish=no
       - hg -R /tmp/mercurial-ci/ update `hg log --rev '.' --template '{node}'`
       - cd /tmp/mercurial-ci/
       - ls -1 tests/test-check-*.* > /tmp/check-tests.txt
-      - black --version
-      - clang-format --version
     script:
-        - echo "python used, $PYTHON"
-        - $PYTHON --version
         - echo "$RUNTEST_ARGS"
         - HGTESTS_ALLOW_NETIO="$TEST_HGTESTS_ALLOW_NETIO" HGMODULEPOLICY="$TEST_HGMODULEPOLICY" "$PYTHON" tests/run-tests.py --color=always $RUNTEST_ARGS
 
@@ -55,25 +56,21 @@
     <<: *runtests
     variables:
         RUNTEST_ARGS: "--time --test-list /tmp/check-tests.txt"
-        PYTHON: python3
         CI_CLEVER_CLOUD_FLAVOR: S
 
 rust-cargo-test:
     <<: *all
     stage: tests
     script:
-        - echo "python used, $PYTHON"
         - make rust-tests
         - make cargo-clippy
     variables:
-        PYTHON: python3
         CI_CLEVER_CLOUD_FLAVOR: S
 
 test-c:
     <<: *runtests
     variables:
         RUNTEST_ARGS: " --no-rust --blacklist /tmp/check-tests.txt"
-        PYTHON: python3
         TEST_HGMODULEPOLICY: "c"
         TEST_HGTESTS_ALLOW_NETIO: "1"
 
@@ -81,7 +78,6 @@
     <<: *runtests
     variables:
         RUNTEST_ARGS: "--pure --blacklist /tmp/check-tests.txt"
-        PYTHON: python3
         TEST_HGMODULEPOLICY: "py"
 
 test-rust:
@@ -89,7 +85,6 @@
     variables:
         HGWITHRUSTEXT: cpython
         RUNTEST_ARGS: "--rust --blacklist /tmp/check-tests.txt"
-        PYTHON: python3
         TEST_HGMODULEPOLICY: "rust+c"
 
 test-rhg:
@@ -97,30 +92,27 @@
     variables:
         HGWITHRUSTEXT: cpython
         RUNTEST_ARGS: "--rust --rhg --blacklist /tmp/check-tests.txt"
-        PYTHON: python3
         TEST_HGMODULEPOLICY: "rust+c"
 
 test-chg:
     <<: *runtests
     variables:
-        PYTHON: python3
         RUNTEST_ARGS: "--blacklist /tmp/check-tests.txt --chg"
         TEST_HGMODULEPOLICY: "c"
 
 check-pytype:
     extends: .runtests_template
     before_script:
+      - export PATH="/home/ci-runner/vendor/pyenv/pyenv-2.4.7-adf3c2bccf09cdb81febcfd15b186711a33ac7a8/shims:/home/ci-runner/vendor/pyenv/pyenv-2.4.7-adf3c2bccf09cdb81febcfd15b186711a33ac7a8/bin:$PATH"
+      - echo "PATH, $PATH"
       - hg clone . /tmp/mercurial-ci/ --noupdate --config phases.publish=no
       - 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 libcst==0.3.20 pytype==2022.11.18
       - ./contrib/setup-pytype.sh
     script:
       - echo "Entering script section"
       - sh contrib/check-pytype.sh
-    variables:
-        PYTHON: python3
 
 # `sh.exe --login` sets a couple of extra environment variables that are defined
 # in the MinGW shell, but switches CWD to /home/$username.  The previous value
--- a/contrib/hgclient.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/contrib/hgclient.py	Fri Sep 06 02:12:19 2024 +0200
@@ -21,7 +21,6 @@
         pargs = [re.sub(br'''\bb(['"])''', br'\1', b'%s' % a) for a in args]
         stdout.write(b' '.join(pargs) + b'\n')
 
-
 else:
     import cStringIO
 
--- a/contrib/nix/flake.nix	Thu Sep 05 15:37:14 2024 -0400
+++ b/contrib/nix/flake.nix	Fri Sep 06 02:12:19 2024 +0200
@@ -53,7 +53,7 @@
       # but uses formatter features from nightly.
       # TODO: make cargo use the formatter from nightly automatically
       #       (not supported by rustup/cargo yet? workaround?)
-      # rustPlatform = pkgs.rust-bin.stable."1.61.0".default;
+      # rustPlatform = pkgs.rust-bin.stable."1.79.0".default;
       # rustPlatformFormatter = pkgs.rust-bin.nightly."2023-04-20".default;
 
       # The CI uses an old version of the Black code formatter,
--- a/contrib/perf-utils/compare-discovery-case	Thu Sep 05 15:37:14 2024 -0400
+++ b/contrib/perf-utils/compare-discovery-case	Fri Sep 06 02:12:19 2024 +0200
@@ -205,7 +205,6 @@
 
 
 if __name__ == '__main__':
-
     argv = sys.argv[:]
 
     kwargs = {}
--- a/contrib/perf.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/contrib/perf.py	Fri Sep 06 02:12:19 2024 +0200
@@ -130,7 +130,6 @@
     def revlog(opener, *args, **kwargs):
         return mercurial.revlog.revlog(opener, perf_rl_kind, *args, **kwargs)
 
-
 except (ImportError, AttributeError):
     perf_rl_kind = None
 
@@ -261,7 +260,6 @@
                 commands.norepo += b' %s' % b' '.join(parsealiases(name))
             return _command(name, list(options), synopsis)
 
-
 else:
     # for "historical portability":
     # define "@command" annotation locally, because cmdutil.command
@@ -1926,7 +1924,7 @@
 
     opts = _byteskwargs(opts)
     timer, fm = gettimer(ui, opts)
-    mercurial.revlog._prereadsize = 2 ** 24  # disable lazy parser in old hg
+    mercurial.revlog._prereadsize = 2**24  # disable lazy parser in old hg
     if opts[b'no_lookup']:
         if opts['rev']:
             raise error.Abort('--no-lookup and --rev are mutually exclusive')
@@ -1985,7 +1983,7 @@
 
     opts = _byteskwargs(opts)
     timer, fm = gettimer(ui, opts)
-    mercurial.revlog._prereadsize = 2 ** 24  # disable lazy parser in old hg
+    mercurial.revlog._prereadsize = 2**24  # disable lazy parser in old hg
 
     unfi = repo.unfiltered()
     clearcaches = opts[b'clear_caches']
@@ -2389,7 +2387,7 @@
     timer, fm = gettimer(ui, opts)
     import mercurial.revlog
 
-    mercurial.revlog._prereadsize = 2 ** 24  # disable lazy parser in old hg
+    mercurial.revlog._prereadsize = 2**24  # disable lazy parser in old hg
     n = scmutil.revsingle(repo, rev).node()
 
     try:
@@ -3102,7 +3100,7 @@
             # disable inlining
             old_max_inline = mercurial.revlog._maxinline
             # large enough to never happen
-            mercurial.revlog._maxinline = 2 ** 50
+            mercurial.revlog._maxinline = 2**50
 
         with repo.lock():
             bundle = [None, None]
--- a/contrib/python-zstandard/tests/test_compressor_fuzzing.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/contrib/python-zstandard/tests/test_compressor_fuzzing.py	Fri Sep 06 02:12:19 2024 +0200
@@ -137,7 +137,6 @@
     def test_buffer_source_read_variance(
         self, original, level, source_read_size, read_sizes
     ):
-
         refctx = zstd.ZstdCompressor(level=level)
         ref_frame = refctx.compress(original)
 
@@ -203,7 +202,6 @@
     def test_buffer_source_readinto(
         self, original, level, source_read_size, read_size
     ):
-
         refctx = zstd.ZstdCompressor(level=level)
         ref_frame = refctx.compress(original)
 
@@ -273,7 +271,6 @@
     def test_buffer_source_readinto_variance(
         self, original, level, source_read_size, read_sizes
     ):
-
         refctx = zstd.ZstdCompressor(level=level)
         ref_frame = refctx.compress(original)
 
@@ -410,7 +407,6 @@
     def test_buffer_source_read1_variance(
         self, original, level, source_read_size, read_sizes
     ):
-
         refctx = zstd.ZstdCompressor(level=level)
         ref_frame = refctx.compress(original)
 
@@ -551,7 +547,6 @@
     def test_buffer_source_readinto1_variance(
         self, original, level, source_read_size, read_sizes
     ):
-
         refctx = zstd.ZstdCompressor(level=level)
         ref_frame = refctx.compress(original)
 
--- a/contrib/python-zstandard/tests/test_decompressor.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/contrib/python-zstandard/tests/test_decompressor.py	Fri Sep 06 02:12:19 2024 +0200
@@ -189,7 +189,7 @@
         # Will get OverflowError on some Python distributions that can't
         # handle really large integers.
         with self.assertRaises((MemoryError, OverflowError)):
-            dctx.decompress(compressed, max_output_size=2 ** 62)
+            dctx.decompress(compressed, max_output_size=2**62)
 
     def test_dictionary(self):
         samples = []
@@ -238,7 +238,7 @@
         cctx = zstd.ZstdCompressor(write_content_size=False)
         frame = cctx.compress(source)
 
-        dctx = zstd.ZstdDecompressor(max_window_size=2 ** zstd.WINDOWLOG_MIN)
+        dctx = zstd.ZstdDecompressor(max_window_size=2**zstd.WINDOWLOG_MIN)
 
         with self.assertRaisesRegex(
             zstd.ZstdError,
--- a/contrib/python-zstandard/tests/test_decompressor_fuzzing.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/contrib/python-zstandard/tests/test_decompressor_fuzzing.py	Fri Sep 06 02:12:19 2024 +0200
@@ -353,7 +353,6 @@
     def test_multiple_frames(
         self, originals, frame_count, level, source_read_size, read_sizes
     ):
-
         cctx = zstd.ZstdCompressor(level=level)
         source = io.BytesIO()
         buffer = io.BytesIO()
--- a/contrib/python-zstandard/zstandard/cffi.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/contrib/python-zstandard/zstandard/cffi.py	Fri Sep 06 02:12:19 2024 +0200
@@ -273,7 +273,6 @@
         ldm_hash_every_log=-1,
         threads=0,
     ):
-
         params = lib.ZSTD_createCCtxParams()
         if params == ffi.NULL:
             raise MemoryError()
@@ -1423,7 +1422,6 @@
         read_size=COMPRESSION_RECOMMENDED_INPUT_SIZE,
         write_size=COMPRESSION_RECOMMENDED_OUTPUT_SIZE,
     ):
-
         if not hasattr(ifh, "read"):
             raise ValueError("first argument must have a read() method")
         if not hasattr(ofh, "write"):
@@ -1523,7 +1521,6 @@
         write_size=COMPRESSION_RECOMMENDED_OUTPUT_SIZE,
         write_return_read=False,
     ):
-
         if not hasattr(writer, "write"):
             raise ValueError("must pass an object with a write() method")
 
--- a/contrib/revsetbenchmarks.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/contrib/revsetbenchmarks.py	Fri Sep 06 02:12:19 2024 +0200
@@ -191,7 +191,7 @@
 
 def formattiming(value):
     """format a value to strictly 8 char, dropping some precision if needed"""
-    if value < 10 ** 7:
+    if value < 10**7:
         return ('%.6f' % value)[:8]
     else:
         # value is HUGE very unlikely to happen (4+ month run)
@@ -371,7 +371,6 @@
 print()
 
 for ridx, rset in enumerate(revsets):
-
     print("revset #%i: %s" % (ridx, rset))
     printheader(variants, len(results), verbose=options.verbose, relative=True)
     ref = None
--- a/contrib/setup-pytype.sh	Thu Sep 05 15:37:14 2024 -0400
+++ b/contrib/setup-pytype.sh	Fri Sep 06 02:12:19 2024 +0200
@@ -5,7 +5,7 @@
 
 # Find the python3 setup that would run pytype
 PYTYPE=`which pytype`
-PYTHON3=`head -n1 ${PYTYPE} | sed -s 's/#!//'`
+PYTHON3=${PYTHON:-`head -n1 ${PYTYPE} | sed -s 's/#!//'`}
 
 # Existing stubs that pytype processes live here
 TYPESHED=$(${PYTHON3} -c "import pytype; print(pytype.__path__[0])")/typeshed/stubs
--- a/contrib/win32/hgwebdir_wsgi.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/contrib/win32/hgwebdir_wsgi.py	Fri Sep 06 02:12:19 2024 +0200
@@ -101,6 +101,7 @@
 import isapi_wsgi
 from mercurial.hgweb.hgwebdir_mod import hgwebdir
 
+
 # Example tweak: Replace isapi_wsgi's handler to provide better error message
 # Other stuff could also be done here, like logging errors etc.
 class WsgiHandler(isapi_wsgi.IsapiWsgiHandler):
@@ -114,7 +115,6 @@
 
 
 def handler(environ, start_response):
-
     # Translate IIS's weird URLs
     url = environ['SCRIPT_NAME'] + environ['PATH_INFO']
     paths = url[1:].split('/')[path_strip:]
--- a/doc/hgmanpage.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/doc/hgmanpage.py	Fri Sep 06 02:12:19 2024 +0200
@@ -95,7 +95,6 @@
 
 
 class Writer(writers.Writer):
-
     supported = 'manpage'
     """Formats this writer supports."""
 
@@ -297,7 +296,7 @@
             (u'´', u"\\'"),
             (u'`', u'\\(ga'),
         ]
-        for (in_char, out_markup) in replace_pairs:
+        for in_char, out_markup in replace_pairs:
             text = text.replace(in_char, out_markup)
         # unicode
         text = self.deunicode(text)
--- a/hgext/acl.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/acl.py	Fri Sep 06 02:12:19 2024 +0200
@@ -279,7 +279,6 @@
 
 
 def _getusers(ui, group):
-
     # First, try to use group definition from section [acl.groups]
     hgrcusers = ui.configlist(b'acl.groups', group)
     if hgrcusers:
@@ -294,12 +293,10 @@
 
 
 def _usermatch(ui, user, usersorgroups):
-
     if usersorgroups == b'*':
         return True
 
     for ug in usersorgroups.replace(b',', b' ').split():
-
         if ug.startswith(b'!'):
             # Test for excluded user or group. Format:
             # if ug is a user  name: !username
@@ -368,7 +365,6 @@
 
 
 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
-
     ensureenabled(ui)
 
     if hooktype not in [b'pretxnchangegroup', b'pretxncommit', b'prepushkey']:
--- a/hgext/convert/common.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/convert/common.py	Fri Sep 06 02:12:19 2024 +0200
@@ -6,12 +6,18 @@
 # GNU General Public License version 2 or any later version.
 
 import base64
-import datetime
 import os
 import pickle
 import re
 import shlex
 import subprocess
+import typing
+
+from typing import (
+    Any,
+    AnyStr,
+    Optional,
+)
 
 from mercurial.i18n import _
 from mercurial.pycompat import open
@@ -22,11 +28,33 @@
     pycompat,
     util,
 )
-from mercurial.utils import procutil
+from mercurial.utils import (
+    dateutil,
+    procutil,
+)
+
+if typing.TYPE_CHECKING:
+    from typing import (
+        overload,
+    )
+    from mercurial import (
+        ui as uimod,
+    )
 
 propertycache = util.propertycache
 
 
+if typing.TYPE_CHECKING:
+
+    @overload
+    def _encodeornone(d: str) -> bytes:
+        pass
+
+    @overload
+    def _encodeornone(d: None) -> None:
+        pass
+
+
 def _encodeornone(d):
     if d is None:
         return
@@ -34,7 +62,7 @@
 
 
 class _shlexpy3proxy:
-    def __init__(self, l):
+    def __init__(self, l: shlex.shlex) -> None:
         self._l = l
 
     def __iter__(self):
@@ -44,15 +72,22 @@
         return _encodeornone(self._l.get_token())
 
     @property
-    def infile(self):
-        return self._l.infile or b'<unknown>'
+    def infile(self) -> bytes:
+        if self._l.infile is not None:
+            return encoding.strtolocal(self._l.infile)
+        return b'<unknown>'
 
     @property
-    def lineno(self):
+    def lineno(self) -> int:
         return self._l.lineno
 
 
-def shlexer(data=None, filepath=None, wordchars=None, whitespace=None):
+def shlexer(
+    data=None,
+    filepath: Optional[bytes] = None,
+    wordchars: Optional[bytes] = None,
+    whitespace: Optional[bytes] = None,
+):
     if data is None:
         data = open(filepath, b'r', encoding='latin1')
     else:
@@ -61,7 +96,8 @@
                 b'shlexer only accepts data or filepath, not both'
             )
         data = data.decode('latin1')
-    l = shlex.shlex(data, infile=filepath, posix=True)
+    infile = encoding.strfromlocal(filepath) if filepath is not None else None
+    l = shlex.shlex(data, infile=infile, posix=True)
     if whitespace is not None:
         l.whitespace_split = True
         l.whitespace += whitespace.decode('latin1')
@@ -70,8 +106,8 @@
     return _shlexpy3proxy(l)
 
 
-def encodeargs(args):
-    def encodearg(s):
+def encodeargs(args: Any) -> bytes:
+    def encodearg(s: bytes) -> bytes:
         lines = base64.encodebytes(s)
         lines = [l.splitlines()[0] for l in pycompat.iterbytestr(lines)]
         return b''.join(lines)
@@ -80,7 +116,7 @@
     return encodearg(s)
 
 
-def decodeargs(s):
+def decodeargs(s: bytes) -> Any:
     s = base64.decodebytes(s)
     return pickle.loads(s)
 
@@ -89,7 +125,9 @@
     pass
 
 
-def checktool(exe, name=None, abort=True):
+def checktool(
+    exe: bytes, name: Optional[bytes] = None, abort: bool = True
+) -> None:
     name = name or exe
     if not procutil.findexe(exe):
         if abort:
@@ -103,25 +141,25 @@
     pass
 
 
-SKIPREV = b'SKIP'
+SKIPREV: bytes = b'SKIP'
 
 
 class commit:
     def __init__(
         self,
-        author,
-        date,
-        desc,
+        author: bytes,
+        date: bytes,
+        desc: bytes,
         parents,
-        branch=None,
+        branch: Optional[bytes] = None,
         rev=None,
         extra=None,
         sortkey=None,
         saverev=True,
-        phase=phases.draft,
+        phase: int = phases.draft,
         optparents=None,
         ctx=None,
-    ):
+    ) -> None:
         self.author = author or b'unknown'
         self.date = date or b'0 0'
         self.desc = desc
@@ -139,7 +177,13 @@
 class converter_source:
     """Conversion source interface"""
 
-    def __init__(self, ui, repotype, path=None, revs=None):
+    def __init__(
+        self,
+        ui: "uimod.ui",
+        repotype: bytes,
+        path: Optional[bytes] = None,
+        revs=None,
+    ) -> None:
         """Initialize conversion source (or raise NoRepo("message")
         exception if path is not a valid repository)"""
         self.ui = ui
@@ -149,7 +193,9 @@
 
         self.encoding = b'utf-8'
 
-    def checkhexformat(self, revstr, mapname=b'splicemap'):
+    def checkhexformat(
+        self, revstr: bytes, mapname: bytes = b'splicemap'
+    ) -> None:
         """fails if revstr is not a 40 byte hex. mercurial and git both uses
         such format for their revision numbering
         """
@@ -159,10 +205,10 @@
                 % (mapname, revstr)
             )
 
-    def before(self):
+    def before(self) -> None:
         pass
 
-    def after(self):
+    def after(self) -> None:
         pass
 
     def targetfilebelongstosource(self, targetfilename):
@@ -221,7 +267,7 @@
         """
         raise NotImplementedError
 
-    def recode(self, s, encoding=None):
+    def recode(self, s: AnyStr, encoding: Optional[bytes] = None) -> bytes:
         if not encoding:
             encoding = self.encoding or b'utf-8'
 
@@ -250,17 +296,17 @@
         """
         raise NotImplementedError
 
-    def converted(self, rev, sinkrev):
+    def converted(self, rev, sinkrev) -> None:
         '''Notify the source that a revision has been converted.'''
 
-    def hasnativeorder(self):
+    def hasnativeorder(self) -> bool:
         """Return true if this source has a meaningful, native revision
         order. For instance, Mercurial revisions are store sequentially
         while there is no such global ordering with Darcs.
         """
         return False
 
-    def hasnativeclose(self):
+    def hasnativeclose(self) -> bool:
         """Return true if this source has ability to close branch."""
         return False
 
@@ -278,7 +324,7 @@
         """
         return {}
 
-    def checkrevformat(self, revstr, mapname=b'splicemap'):
+    def checkrevformat(self, revstr, mapname: bytes = b'splicemap') -> bool:
         """revstr is a string that describes a revision in the given
         source control system.  Return true if revstr has correct
         format.
@@ -289,7 +335,7 @@
 class converter_sink:
     """Conversion sink (target) interface"""
 
-    def __init__(self, ui, repotype, path):
+    def __init__(self, ui: "uimod.ui", repotype: bytes, path: bytes) -> None:
         """Initialize conversion sink (or raise NoRepo("message")
         exception if path is not a valid repository)
 
@@ -357,10 +403,10 @@
         filter empty revisions.
         """
 
-    def before(self):
+    def before(self) -> None:
         pass
 
-    def after(self):
+    def after(self) -> None:
         pass
 
     def putbookmarks(self, bookmarks):
@@ -383,17 +429,17 @@
 
 
 class commandline:
-    def __init__(self, ui, command):
+    def __init__(self, ui: "uimod.ui", command: bytes) -> None:
         self.ui = ui
         self.command = command
 
-    def prerun(self):
+    def prerun(self) -> None:
         pass
 
-    def postrun(self):
+    def postrun(self) -> None:
         pass
 
-    def _cmdline(self, cmd, *args, **kwargs):
+    def _cmdline(self, cmd: bytes, *args: bytes, **kwargs) -> bytes:
         kwargs = pycompat.byteskwargs(kwargs)
         cmdline = [self.command, cmd] + list(args)
         for k, v in kwargs.items():
@@ -414,7 +460,7 @@
         cmdline = b' '.join(cmdline)
         return cmdline
 
-    def _run(self, cmd, *args, **kwargs):
+    def _run(self, cmd: bytes, *args: bytes, **kwargs):
         def popen(cmdline):
             p = subprocess.Popen(
                 procutil.tonativestr(cmdline),
@@ -427,13 +473,13 @@
 
         return self._dorun(popen, cmd, *args, **kwargs)
 
-    def _run2(self, cmd, *args, **kwargs):
+    def _run2(self, cmd: bytes, *args: bytes, **kwargs):
         return self._dorun(procutil.popen2, cmd, *args, **kwargs)
 
-    def _run3(self, cmd, *args, **kwargs):
+    def _run3(self, cmd: bytes, *args: bytes, **kwargs):
         return self._dorun(procutil.popen3, cmd, *args, **kwargs)
 
-    def _dorun(self, openfunc, cmd, *args, **kwargs):
+    def _dorun(self, openfunc, cmd: bytes, *args: bytes, **kwargs):
         cmdline = self._cmdline(cmd, *args, **kwargs)
         self.ui.debug(b'running: %s\n' % (cmdline,))
         self.prerun()
@@ -442,20 +488,20 @@
         finally:
             self.postrun()
 
-    def run(self, cmd, *args, **kwargs):
+    def run(self, cmd: bytes, *args: bytes, **kwargs):
         p = self._run(cmd, *args, **kwargs)
         output = p.communicate()[0]
         self.ui.debug(output)
         return output, p.returncode
 
-    def runlines(self, cmd, *args, **kwargs):
+    def runlines(self, cmd: bytes, *args: bytes, **kwargs):
         p = self._run(cmd, *args, **kwargs)
         output = p.stdout.readlines()
         p.wait()
         self.ui.debug(b''.join(output))
         return output, p.returncode
 
-    def checkexit(self, status, output=b''):
+    def checkexit(self, status, output: bytes = b'') -> None:
         if status:
             if output:
                 self.ui.warn(_(b'%s error:\n') % self.command)
@@ -463,12 +509,12 @@
             msg = procutil.explainexit(status)
             raise error.Abort(b'%s %s' % (self.command, msg))
 
-    def run0(self, cmd, *args, **kwargs):
+    def run0(self, cmd: bytes, *args: bytes, **kwargs):
         output, status = self.run(cmd, *args, **kwargs)
         self.checkexit(status, output)
         return output
 
-    def runlines0(self, cmd, *args, **kwargs):
+    def runlines0(self, cmd: bytes, *args: bytes, **kwargs):
         output, status = self.runlines(cmd, *args, **kwargs)
         self.checkexit(status, b''.join(output))
         return output
@@ -491,7 +537,7 @@
         # (and make happy Windows shells while doing this).
         return argmax // 2 - 1
 
-    def _limit_arglist(self, arglist, cmd, *args, **kwargs):
+    def _limit_arglist(self, arglist, cmd: bytes, *args: bytes, **kwargs):
         cmdlen = len(self._cmdline(cmd, *args, **kwargs))
         limit = self.argmax - cmdlen
         numbytes = 0
@@ -508,13 +554,13 @@
         if fl:
             yield fl
 
-    def xargs(self, arglist, cmd, *args, **kwargs):
+    def xargs(self, arglist, cmd: bytes, *args: bytes, **kwargs):
         for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
             self.run0(cmd, *(list(args) + l), **kwargs)
 
 
 class mapfile(dict):
-    def __init__(self, ui, path):
+    def __init__(self, ui: "uimod.ui", path: bytes) -> None:
         super(mapfile, self).__init__()
         self.ui = ui
         self.path = path
@@ -522,31 +568,34 @@
         self.order = []
         self._read()
 
-    def _read(self):
+    def _read(self) -> None:
         if not self.path:
             return
         try:
             fp = open(self.path, b'rb')
         except FileNotFoundError:
             return
-        for i, line in enumerate(fp):
-            line = line.splitlines()[0].rstrip()
-            if not line:
-                # Ignore blank lines
-                continue
-            try:
-                key, value = line.rsplit(b' ', 1)
-            except ValueError:
-                raise error.Abort(
-                    _(b'syntax error in %s(%d): key/value pair expected')
-                    % (self.path, i + 1)
-                )
-            if key not in self:
-                self.order.append(key)
-            super(mapfile, self).__setitem__(key, value)
-        fp.close()
 
-    def __setitem__(self, key, value):
+        try:
+            for i, line in enumerate(fp):
+                line = line.splitlines()[0].rstrip()
+                if not line:
+                    # Ignore blank lines
+                    continue
+                try:
+                    key, value = line.rsplit(b' ', 1)
+                except ValueError:
+                    raise error.Abort(
+                        _(b'syntax error in %s(%d): key/value pair expected')
+                        % (self.path, i + 1)
+                    )
+                if key not in self:
+                    self.order.append(key)
+                super(mapfile, self).__setitem__(key, value)
+        finally:
+            fp.close()
+
+    def __setitem__(self, key, value) -> None:
         if self.fp is None:
             try:
                 self.fp = open(self.path, b'ab')
@@ -559,18 +608,11 @@
         self.fp.flush()
         super(mapfile, self).__setitem__(key, value)
 
-    def close(self):
+    def close(self) -> None:
         if self.fp:
             self.fp.close()
             self.fp = None
 
 
-def makedatetimestamp(t):
-    """Like dateutil.makedate() but for time t instead of current time"""
-    tz = round(
-        t
-        - datetime.datetime.fromtimestamp(t)
-        .replace(tzinfo=datetime.timezone.utc)
-        .timestamp()
-    )
-    return t, tz
+def makedatetimestamp(t: float) -> dateutil.hgdate:
+    return dateutil.makedate(t)
--- a/hgext/convert/convcmd.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/convert/convcmd.py	Fri Sep 06 02:12:19 2024 +0200
@@ -9,6 +9,16 @@
 import heapq
 import os
 import shutil
+import typing
+
+from typing import (
+    AnyStr,
+    Dict,
+    List,
+    Mapping,
+    Optional,
+    Union,
+)
 
 from mercurial.i18n import _
 from mercurial.pycompat import open
@@ -36,6 +46,11 @@
     subversion,
 )
 
+if typing.TYPE_CHECKING:
+    from mercurial import (
+        ui as uimod,
+    )
+
 mapfile = common.mapfile
 MissingTool = common.MissingTool
 NoRepo = common.NoRepo
@@ -53,15 +68,14 @@
 svn_sink = subversion.svn_sink
 svn_source = subversion.svn_source
 
-orig_encoding = b'ascii'
+orig_encoding: bytes = b'ascii'
 
 
-def readauthormap(ui, authorfile, authors=None):
+def readauthormap(ui: "uimod.ui", authorfile, authors=None):
     if authors is None:
         authors = {}
     with open(authorfile, b'rb') as afile:
         for line in afile:
-
             line = line.strip()
             if not line or line.startswith(b'#'):
                 continue
@@ -86,7 +100,7 @@
     return authors
 
 
-def recode(s):
+def recode(s: AnyStr) -> bytes:
     if isinstance(s, str):
         return s.encode(pycompat.sysstr(orig_encoding), 'replace')
     else:
@@ -95,7 +109,7 @@
         )
 
 
-def mapbranch(branch, branchmap):
+def mapbranch(branch: bytes, branchmap: Mapping[bytes, bytes]) -> bytes:
     """
     >>> bmap = {b'default': b'branch1'}
     >>> for i in [b'', None]:
@@ -147,7 +161,7 @@
 ]
 
 
-def convertsource(ui, path, type, revs):
+def convertsource(ui: "uimod.ui", path: bytes, type: bytes, revs):
     exceptions = []
     if type and type not in [s[0] for s in source_converters]:
         raise error.Abort(_(b'%s: invalid source repository type') % type)
@@ -163,7 +177,9 @@
     raise error.Abort(_(b'%s: missing or unsupported repository') % path)
 
 
-def convertsink(ui, path, type):
+def convertsink(
+    ui: "uimod.ui", path: bytes, type: bytes
+) -> Union[hgconvert.mercurial_sink, subversion.svn_sink]:
     if type and type not in [s[0] for s in sink_converters]:
         raise error.Abort(_(b'%s: invalid destination repository type') % type)
     for name, sink in sink_converters:
@@ -178,7 +194,9 @@
 
 
 class progresssource:
-    def __init__(self, ui, source, filecount):
+    def __init__(
+        self, ui: "uimod.ui", source, filecount: Optional[int]
+    ) -> None:
         self.ui = ui
         self.source = source
         self.progress = ui.makeprogress(
@@ -253,8 +271,7 @@
 
 
 class converter:
-    def __init__(self, ui, source, dest, revmapfile, opts):
-
+    def __init__(self, ui: "uimod.ui", source, dest, revmapfile, opts) -> None:
         self.source = source
         self.dest = dest
         self.ui = ui
@@ -280,7 +297,7 @@
         self.splicemap = self.parsesplicemap(opts.get(b'splicemap'))
         self.branchmap = mapfile(ui, opts.get(b'branchmap'))
 
-    def parsesplicemap(self, path):
+    def parsesplicemap(self, path: bytes) -> Dict[bytes, List[bytes]]:
         """check and validate the splicemap format and
         return a child/parents dictionary.
         Format checking has two parts.
@@ -295,31 +312,31 @@
             return {}
         m = {}
         try:
-            fp = open(path, b'rb')
-            for i, line in enumerate(fp):
-                line = line.splitlines()[0].rstrip()
-                if not line:
-                    # Ignore blank lines
-                    continue
-                # split line
-                lex = common.shlexer(data=line, whitespace=b',')
-                line = list(lex)
-                # check number of parents
-                if not (2 <= len(line) <= 3):
-                    raise error.Abort(
-                        _(
-                            b'syntax error in %s(%d): child parent1'
-                            b'[,parent2] expected'
+            with open(path, b'rb') as fp:
+                for i, line in enumerate(fp):
+                    line = line.splitlines()[0].rstrip()
+                    if not line:
+                        # Ignore blank lines
+                        continue
+                    # split line
+                    lex = common.shlexer(data=line, whitespace=b',')
+                    line = list(lex)
+                    # check number of parents
+                    if not (2 <= len(line) <= 3):
+                        raise error.Abort(
+                            _(
+                                b'syntax error in %s(%d): child parent1'
+                                b'[,parent2] expected'
+                            )
+                            % (path, i + 1)
                         )
-                        % (path, i + 1)
-                    )
-                for part in line:
-                    self.source.checkrevformat(part)
-                child, p1, p2 = line[0], line[1:2], line[2:]
-                if p1 == p2:
-                    m[child] = p1
-                else:
-                    m[child] = p1 + p2
+                    for part in line:
+                        self.source.checkrevformat(part)
+                    child, p1, p2 = line[0], line[1:2], line[2:]
+                    if p1 == p2:
+                        m[child] = p1
+                    else:
+                        m[child] = p1 + p2
         # if file does not exist or error reading, exit
         except IOError:
             raise error.Abort(
@@ -356,7 +373,7 @@
 
         return parents
 
-    def mergesplicemap(self, parents, splicemap):
+    def mergesplicemap(self, parents, splicemap) -> None:
         """A splicemap redefines child/parent relationships. Check the
         map contains valid revision identifiers and merge the new
         links in the source graph.
@@ -488,20 +505,19 @@
 
         return s
 
-    def writeauthormap(self):
+    def writeauthormap(self) -> None:
         authorfile = self.authorfile
         if authorfile:
             self.ui.status(_(b'writing author map file %s\n') % authorfile)
-            ofile = open(authorfile, b'wb+')
-            for author in self.authors:
-                ofile.write(
-                    util.tonativeeol(
-                        b"%s=%s\n" % (author, self.authors[author])
+            with open(authorfile, b'wb+') as ofile:
+                for author in self.authors:
+                    ofile.write(
+                        util.tonativeeol(
+                            b"%s=%s\n" % (author, self.authors[author])
+                        )
                     )
-                )
-            ofile.close()
 
-    def readauthormap(self, authorfile):
+    def readauthormap(self, authorfile) -> None:
         self.authors = readauthormap(self.ui, authorfile, self.authors)
 
     def cachecommit(self, rev):
@@ -511,7 +527,7 @@
         self.commitcache[rev] = commit
         return commit
 
-    def copy(self, rev):
+    def copy(self, rev) -> None:
         commit = self.commitcache[rev]
         full = self.opts.get(b'full')
         changes = self.source.getchanges(rev, full)
@@ -563,7 +579,7 @@
         self.source.converted(rev, newnode)
         self.map[rev] = newnode
 
-    def convert(self, sortmode):
+    def convert(self, sortmode) -> None:
         try:
             self.source.before()
             self.dest.before()
@@ -628,7 +644,7 @@
         finally:
             self.cleanup()
 
-    def cleanup(self):
+    def cleanup(self) -> None:
         try:
             self.dest.after()
         finally:
@@ -636,7 +652,9 @@
         self.map.close()
 
 
-def convert(ui, src, dest=None, revmapfile=None, **opts):
+def convert(
+    ui: "uimod.ui", src, dest: Optional[bytes] = None, revmapfile=None, **opts
+) -> None:
     opts = pycompat.byteskwargs(opts)
     global orig_encoding
     orig_encoding = encoding.encoding
--- a/hgext/convert/cvs.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/convert/cvs.py	Fri Sep 06 02:12:19 2024 +0200
@@ -11,9 +11,6 @@
 import socket
 
 from mercurial.i18n import _
-from mercurial.pycompat import (
-    open,
-)
 from mercurial import (
     encoding,
     error,
@@ -52,8 +49,8 @@
         self.tags = {}
         self.lastbranch = {}
         self.socket = None
-        self.cvsroot = open(os.path.join(cvs, b"Root"), b'rb').read()[:-1]
-        self.cvsrepo = open(os.path.join(cvs, b"Repository"), b'rb').read()[:-1]
+        self.cvsroot = util.readfile(os.path.join(cvs, b"Root"))[:-1]
+        self.cvsrepo = util.readfile(os.path.join(cvs, b"Repository"))[:-1]
         self.encoding = encoding.encoding
 
         self._connect()
@@ -160,8 +157,7 @@
                     passw = b"A"
                     cvspass = os.path.expanduser(b"~/.cvspass")
                     try:
-                        pf = open(cvspass, b'rb')
-                        for line in pf.read().splitlines():
+                        for line in util.readfile(cvspass).splitlines():
                             part1, part2 = line.split(b' ', 1)
                             # /1 :pserver:user@example.com:2401/cvsroot/foo
                             # Ah<Z
@@ -174,7 +170,6 @@
                             if part1 == format:
                                 passw = part2
                                 break
-                        pf.close()
                     except IOError as inst:
                         if inst.errno != errno.ENOENT:
                             if not getattr(inst, 'filename', None):
--- a/hgext/convert/cvsps.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/convert/cvsps.py	Fri Sep 06 02:12:19 2024 +0200
@@ -161,7 +161,7 @@
 
         # Use the Root file in the sandbox, if it exists
         try:
-            root = open(os.path.join(b'CVS', b'Root'), b'rb').read().strip()
+            root = util.readfile(os.path.join(b'CVS', b'Root')).strip()
         except IOError:
             pass
 
@@ -195,16 +195,17 @@
     if cache == b'update':
         try:
             ui.note(_(b'reading cvs log cache %s\n') % cachefile)
-            oldlog = pickle.load(open(cachefile, b'rb'))
-            for e in oldlog:
-                if not (
-                    hasattr(e, 'branchpoints')
-                    and hasattr(e, 'commitid')
-                    and hasattr(e, 'mergepoint')
-                ):
-                    ui.status(_(b'ignoring old cache\n'))
-                    oldlog = []
-                    break
+            with open(cachefile, b'rb') as fp:
+                oldlog = pickle.load(fp)
+                for e in oldlog:
+                    if not (
+                        hasattr(e, 'branchpoints')
+                        and hasattr(e, 'commitid')
+                        and hasattr(e, 'mergepoint')
+                    ):
+                        ui.status(_(b'ignoring old cache\n'))
+                        oldlog = []
+                        break
 
             ui.note(_(b'cache has %d log entries\n') % len(oldlog))
         except Exception as e:
@@ -526,7 +527,9 @@
 
             # write the new cachefile
             ui.note(_(b'writing cvs log cache %s\n') % cachefile)
-            pickle.dump(log, open(cachefile, b'wb'))
+
+            with open(cachefile, b'wb') as fp:
+                pickle.dump(log, fp)
         else:
             log = oldlog
 
@@ -636,7 +639,6 @@
     files = set()
     c = None
     for i, e in enumerate(log):
-
         # Check if log entry belongs to the current changeset or not.
 
         # Since CVS is file-centric, two different file revisions with
@@ -980,7 +982,6 @@
     branches = {}  # latest version number in each branch
     ancestors = {}  # parent branch
     for cs in changesets:
-
         if opts[b"ancestors"]:
             if cs.branch not in branches and cs.parents and cs.parents[0].id:
                 ancestors[cs.branch] = (
--- a/hgext/convert/filemap.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/convert/filemap.py	Fri Sep 06 02:12:19 2024 +0200
@@ -6,6 +6,17 @@
 
 
 import posixpath
+import typing
+
+from typing import (
+    Iterator,
+    Mapping,
+    MutableMapping,
+    Optional,
+    Set,
+    Tuple,
+    overload,
+)
 
 from mercurial.i18n import _
 from mercurial import (
@@ -14,10 +25,15 @@
 )
 from . import common
 
+if typing.TYPE_CHECKING:
+    from mercurial import (
+        ui as uimod,
+    )
+
 SKIPREV = common.SKIPREV
 
 
-def rpairs(path):
+def rpairs(path: bytes) -> Iterator[Tuple[bytes, bytes]]:
     """Yield tuples with path split at '/', starting with the full path.
     No leading, trailing or double '/', please.
     >>> for x in rpairs(b'foo/bar/baz'): print(x)
@@ -33,6 +49,17 @@
     yield b'.', path
 
 
+if typing.TYPE_CHECKING:
+
+    @overload
+    def normalize(path: bytes) -> bytes:
+        pass
+
+    @overload
+    def normalize(path: None) -> None:
+        pass
+
+
 def normalize(path):
     """We use posixpath.normpath to support cross-platform path format.
     However, it doesn't handle None input. So we wrap it up."""
@@ -46,7 +73,10 @@
     A name can be mapped to itself, a new name, or None (omit from new
     repository)."""
 
-    def __init__(self, ui, path=None):
+    rename: MutableMapping[bytes, bytes]
+    targetprefixes: Optional[Set[bytes]]
+
+    def __init__(self, ui: "uimod.ui", path: Optional[bytes] = None) -> None:
         self.ui = ui
         self.include = {}
         self.exclude = {}
@@ -56,10 +86,10 @@
             if self.parse(path):
                 raise error.Abort(_(b'errors in filemap'))
 
-    def parse(self, path):
+    def parse(self, path: Optional[bytes]) -> int:
         errs = 0
 
-        def check(name, mapping, listname):
+        def check(name: bytes, mapping, listname: bytes):
             if not name:
                 self.ui.warn(
                     _(b'%s:%d: path to %s is missing\n')
@@ -110,7 +140,9 @@
             cmd = lex.get_token()
         return errs
 
-    def lookup(self, name, mapping):
+    def lookup(
+        self, name: bytes, mapping: Mapping[bytes, bytes]
+    ) -> Tuple[bytes, bytes, bytes]:
         name = normalize(name)
         for pre, suf in rpairs(name):
             try:
@@ -119,7 +151,7 @@
                 pass
         return b'', name, b''
 
-    def istargetfile(self, filename):
+    def istargetfile(self, filename: bytes) -> bool:
         """Return true if the given target filename is covered as a destination
         of the filemap. This is useful for identifying what parts of the target
         repo belong to the source repo and what parts don't."""
@@ -143,7 +175,7 @@
                 return True
         return False
 
-    def __call__(self, name):
+    def __call__(self, name: bytes) -> Optional[bytes]:
         if self.include:
             inc = self.lookup(name, self.include)[0]
         else:
@@ -165,7 +197,7 @@
             return newpre
         return name
 
-    def active(self):
+    def active(self) -> bool:
         return bool(self.include or self.exclude or self.rename)
 
 
@@ -185,7 +217,9 @@
 
 
 class filemap_source(common.converter_source):
-    def __init__(self, ui, baseconverter, filemap):
+    def __init__(
+        self, ui: "uimod.ui", baseconverter, filemap: Optional[bytes]
+    ) -> None:
         super(filemap_source, self).__init__(ui, baseconverter.repotype)
         self.base = baseconverter
         self.filemapper = filemapper(ui, filemap)
@@ -206,10 +240,10 @@
             b'convert', b'ignoreancestorcheck'
         )
 
-    def before(self):
+    def before(self) -> None:
         self.base.before()
 
-    def after(self):
+    def after(self) -> None:
         self.base.after()
 
     def setrevmap(self, revmap):
@@ -243,7 +277,7 @@
         self.convertedorder = converted
         return self.base.setrevmap(revmap)
 
-    def rebuild(self):
+    def rebuild(self) -> bool:
         if self._rebuilt:
             return True
         self._rebuilt = True
@@ -276,7 +310,7 @@
     def getheads(self):
         return self.base.getheads()
 
-    def getcommit(self, rev):
+    def getcommit(self, rev: bytes):
         # We want to save a reference to the commit objects to be able
         # to rewrite their parents later on.
         c = self.commits[rev] = self.base.getcommit(rev)
@@ -292,7 +326,7 @@
             return self.commits[rev]
         return self.base.getcommit(rev)
 
-    def _discard(self, *revs):
+    def _discard(self, *revs) -> None:
         for r in revs:
             if r is None:
                 continue
@@ -304,7 +338,7 @@
                 if self._rebuilt:
                     del self.children[r]
 
-    def wanted(self, rev, i):
+    def wanted(self, rev, i) -> bool:
         # Return True if we're directly interested in rev.
         #
         # i is an index selecting one of the parents of rev (if rev
@@ -332,7 +366,7 @@
         # doesn't consider it significant, and this revision should be dropped.
         return not files and b'close' not in self.commits[rev].extra
 
-    def mark_not_wanted(self, rev, p):
+    def mark_not_wanted(self, rev, p) -> None:
         # Mark rev as not interesting and update data structures.
 
         if p is None:
@@ -347,7 +381,7 @@
         self.parentmap[rev] = self.parentmap[p]
         self.wantedancestors[rev] = self.wantedancestors[p]
 
-    def mark_wanted(self, rev, parents):
+    def mark_wanted(self, rev, parents) -> None:
         # Mark rev ss wanted and update data structures.
 
         # rev will be in the restricted graph, so children of rev in
@@ -474,7 +508,7 @@
 
         return files, ncopies, ncleanp2
 
-    def targetfilebelongstosource(self, targetfilename):
+    def targetfilebelongstosource(self, targetfilename: bytes) -> bool:
         return self.filemapper.istargetfile(targetfilename)
 
     def getfile(self, name, rev):
@@ -484,7 +518,7 @@
     def gettags(self):
         return self.base.gettags()
 
-    def hasnativeorder(self):
+    def hasnativeorder(self) -> bool:
         return self.base.hasnativeorder()
 
     def lookuprev(self, rev):
--- a/hgext/convert/monotone.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/convert/monotone.py	Fri Sep 06 02:12:19 2024 +0200
@@ -8,6 +8,9 @@
 
 import os
 import re
+from typing import (
+    Tuple,
+)
 
 from mercurial.i18n import _
 from mercurial.pycompat import open
@@ -43,9 +46,8 @@
         if not os.path.exists(os.path.join(path, b'_MTN')):
             # Could be a monotone repository (SQLite db file)
             try:
-                f = open(path, b'rb')
-                header = f.read(16)
-                f.close()
+                with open(path, b'rb') as f:
+                    header = f.read(16)
             except IOError:
                 header = b''
             if header != b'SQLite format 3\x00':
@@ -122,7 +124,7 @@
 
         return self.mtnstdioreadcommandoutput(command)
 
-    def mtnstdioreadpacket(self):
+    def mtnstdioreadpacket(self) -> Tuple[bytes, bytes, int, bytes]:
         read = None
         commandnbr = b''
         while read != b':':
@@ -161,14 +163,14 @@
             raise error.Abort(
                 _(
                     b"bad mtn packet - unable to read full packet "
-                    b"read %s of %s"
+                    b"read %s of %d"
                 )
                 % (len(read), length)
             )
 
         return (commandnbr, stream, length, read)
 
-    def mtnstdioreadcommandoutput(self, command):
+    def mtnstdioreadcommandoutput(self, command) -> bytes:
         retval = []
         while True:
             commandnbr, stream, length, output = self.mtnstdioreadpacket()
@@ -399,7 +401,7 @@
                     )
         else:
             self.ui.debug(
-                b"mtn automate version %s - not using automate stdio "
+                b"mtn automate version %f - not using automate stdio "
                 b"(automate >= 12.0 - mtn >= 0.46 is needed)\n" % version
             )
 
--- a/hgext/convert/subversion.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/convert/subversion.py	Fri Sep 06 02:12:19 2024 +0200
@@ -1425,7 +1425,6 @@
         return self.join(b'hg-authormap')
 
     def __init__(self, ui, repotype, path):
-
         converter_sink.__init__(self, ui, repotype, path)
         commandline.__init__(self, ui, b'svn')
         self.delete = []
@@ -1488,9 +1487,11 @@
                 prop_actions_allowed.append((b'M', b'svn:date'))
 
             hook = os.path.join(created, b'hooks', b'pre-revprop-change')
-            fp = open(hook, b'wb')
-            fp.write(gen_pre_revprop_change_hook(prop_actions_allowed))
-            fp.close()
+
+            util.writefile(
+                hook, gen_pre_revprop_change_hook(prop_actions_allowed)
+            )
+
             util.setflags(hook, False, True)
 
         output = self.run0(b'info')
--- a/hgext/extdiff.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/extdiff.py	Fri Sep 06 02:12:19 2024 +0200
@@ -405,7 +405,6 @@
     guitool,
     opts,
 ):
-
     subrepos = opts.get(b'subrepos')
 
     # calculate list of files changed between both revs
--- a/hgext/fastannotate/commands.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/fastannotate/commands.py	Fri Sep 06 02:12:19 2024 +0200
@@ -7,6 +7,9 @@
 
 
 import os
+from typing import (
+    Set,
+)
 
 from mercurial.i18n import _
 from mercurial import (
@@ -254,7 +257,7 @@
 
 
 _newopts = set()
-_knownopts = {
+_knownopts: Set[bytes] = {
     opt[1].replace(b'-', b'_')
     for opt in (fastannotatecommandargs['options'] + commands.globalopts)
 }
--- a/hgext/fastannotate/context.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/fastannotate/context.py	Fri Sep 06 02:12:19 2024 +0200
@@ -38,6 +38,7 @@
     revmap as revmapmod,
 )
 
+
 # given path, get filelog, cached
 @util.lrucachefunc
 def _getflog(repo, path):
@@ -173,12 +174,16 @@
         'followmerge': True,
     }
 
+    diffopts: mdiff.diffopts
+    followrename: bool
+    followmerge: bool
+
     def __init__(self, **opts):
         for k, v in self.defaults.items():
             setattr(self, k, opts.get(k, v))
 
     @util.propertycache
-    def shortstr(self):
+    def shortstr(self) -> bytes:
         """represent opts in a short string, suitable for a directory name"""
         result = b''
         if not self.followrename:
--- a/hgext/fastannotate/formatter.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/fastannotate/formatter.py	Fri Sep 06 02:12:19 2024 +0200
@@ -17,6 +17,7 @@
 )
 from mercurial.utils import dateutil
 
+
 # imitating mercurial.commands.annotate, not using the vanilla formatter since
 # the data structures are a bit different, and we have some fast paths.
 class defaultformatter:
--- a/hgext/fsmonitor/__init__.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/fsmonitor/__init__.py	Fri Sep 06 02:12:19 2024 +0200
@@ -893,7 +893,6 @@
     matcher=None,
     **kwargs
 ):
-
     distance = 0
     partial = True
     oldnode = repo[b'.'].node()
--- a/hgext/fsmonitor/pywatchman/__init__.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/fsmonitor/pywatchman/__init__.py	Fri Sep 06 02:12:19 2024 +0200
@@ -210,7 +210,6 @@
             )
         )
 
-
 else:
 
     def log(fmt, *args):
--- a/hgext/fsmonitor/pywatchman/encoding.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/fsmonitor/pywatchman/encoding.py	Fri Sep 06 02:12:19 2024 +0200
@@ -46,7 +46,6 @@
         # returns None.
         return sys.getfilesystemencoding()
 
-
 else:
     # Python 2 doesn't support surrogateescape, so use 'strict' by
     # default. Users can register a custom surrogateescape error handler and use
--- a/hgext/highlight/highlight.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/highlight/highlight.py	Fri Sep 06 02:12:19 2024 +0200
@@ -43,7 +43,6 @@
 
 
 def pygmentize(field, fctx, style, tmpl, guessfilenameonly=False):
-
     # append a <link ...> to the syntax highlighting css
     tmpl.load(b'header')
     old_header = tmpl.cache[b'header']
--- a/hgext/histedit.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/histedit.py	Fri Sep 06 02:12:19 2024 +0200
@@ -1526,7 +1526,8 @@
 
     def move_cursor(self, oldpos, newpos):
         """Change the rule/changeset that the cursor is pointing to, regardless of
-        current mode (you can switch between patches from the view patch window)."""
+        current mode (you can switch between patches from the view patch window).
+        """
         self.pos = newpos
 
         mode, _ = self.mode
@@ -1605,7 +1606,8 @@
 
     def change_view(self, delta, unit):
         """Change the region of whatever is being viewed (a patch or the list of
-        changesets). 'delta' is an amount (+/- 1) and 'unit' is 'page' or 'line'."""
+        changesets). 'delta' is an amount (+/- 1) and 'unit' is 'page' or 'line'.
+        """
         mode, _ = self.mode
         if mode != MODE_PATCH:
             return
--- a/hgext/journal.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/journal.py	Fri Sep 06 02:12:19 2024 +0200
@@ -64,6 +64,7 @@
     bookmarktype: hg.sharedbookmarks,
 }
 
+
 # Journal recording, register hooks and storage object
 def extsetup(ui):
     extensions.wrapfunction(dispatch, 'runcommand', runcommand)
--- a/hgext/keyword.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/keyword.py	Fri Sep 06 02:12:19 2024 +0200
@@ -160,6 +160,8 @@
     b'svn',
     default=False,
 )
+
+
 # date like in cvs' $Date
 @templatefilter(b'utcdate', intype=templateutil.date)
 def utcdate(date):
--- a/hgext/largefiles/lfutil.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/largefiles/lfutil.py	Fri Sep 06 02:12:19 2024 +0200
@@ -162,36 +162,18 @@
     _large_file_dirstate = True
     _tr_key_suffix = b'-large-files'
 
-    def __getitem__(self, key):
-        return super(largefilesdirstate, self).__getitem__(unixpath(key))
+    # XXX: why are there overrides to fix the path, if the path should already
+    #   be in unix form for the superclass?
 
-    def set_tracked(self, f):
-        return super(largefilesdirstate, self).set_tracked(unixpath(f))
+    def set_tracked(self, f, reset_copy=False):
+        return super(largefilesdirstate, self).set_tracked(
+            unixpath(f), reset_copy=reset_copy
+        )
 
     def set_untracked(self, f):
         return super(largefilesdirstate, self).set_untracked(unixpath(f))
 
-    def normal(self, f, parentfiledata=None):
-        # not sure if we should pass the `parentfiledata` down or throw it
-        # away. So throwing it away to stay on the safe side.
-        return super(largefilesdirstate, self).normal(unixpath(f))
-
-    def remove(self, f):
-        return super(largefilesdirstate, self).remove(unixpath(f))
-
-    def add(self, f):
-        return super(largefilesdirstate, self).add(unixpath(f))
-
-    def drop(self, f):
-        return super(largefilesdirstate, self).drop(unixpath(f))
-
-    def forget(self, f):
-        return super(largefilesdirstate, self).forget(unixpath(f))
-
-    def normallookup(self, f):
-        return super(largefilesdirstate, self).normallookup(unixpath(f))
-
-    def _ignore(self, f):
+    def _dirignore(self, f):
         return False
 
     def write(self, tr):
--- a/hgext/largefiles/overrides.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/largefiles/overrides.py	Fri Sep 06 02:12:19 2024 +0200
@@ -11,6 +11,9 @@
 import contextlib
 import copy
 import os
+from typing import (
+    Optional,
+)
 
 from mercurial.i18n import _
 
@@ -827,11 +830,11 @@
         if not os.path.isdir(makestandin(dest)):
             os.makedirs(makestandin(dest))
 
+    # When we call orig below it creates the standins but we don't add
+    # them to the dir state until later so lock during that time.
+    wlock = repo.wlock()
+
     try:
-        # When we call orig below it creates the standins but we don't add
-        # them to the dir state until later so lock during that time.
-        wlock = repo.wlock()
-
         manifest = repo[None].manifest()
 
         def overridematch(
@@ -899,7 +902,7 @@
                 result += orig(ui, repo, listpats, opts, rename)
 
         lfdirstate = lfutil.openlfdirstate(ui, repo)
-        for (src, dest) in copiedfiles:
+        for src, dest in copiedfiles:
             if lfutil.shortname in src and dest.startswith(
                 repo.wjoin(lfutil.shortname)
             ):
@@ -1229,7 +1232,7 @@
     node,
     kind,
     decode=True,
-    match=None,
+    match: Optional[matchmod.basematcher] = None,
     prefix=b'',
     mtime=None,
     subrepos=None,
@@ -1257,8 +1260,11 @@
     else:
         prefix = archival.tidyprefix(dest, kind, prefix)
 
+    if not match:
+        match = scmutil.matchall(repo)
+
     def write(name, mode, islink, getdata):
-        if match and not match(name):
+        if not match(name):
             return
         data = getdata()
         if decode:
@@ -1319,7 +1325,9 @@
 
 
 @eh.wrapfunction(subrepo.hgsubrepo, 'archive')
-def hgsubrepoarchive(orig, repo, archiver, prefix, match=None, decode=True):
+def hgsubrepoarchive(
+    orig, repo, archiver, prefix, match: matchmod.basematcher, decode=True
+):
     lfenabled = hasattr(repo._repo, '_largefilesenabled')
     if not lfenabled or not repo._repo.lfstatus:
         return orig(repo, archiver, prefix, match, decode)
@@ -1334,7 +1342,7 @@
     def write(name, mode, islink, getdata):
         # At this point, the standin has been replaced with the largefile name,
         # so the normal matcher works here without the lfutil variants.
-        if match and not match(f):
+        if not match(f):
             return
         data = getdata()
         if decode:
--- a/hgext/largefiles/reposetup.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/largefiles/reposetup.py	Fri Sep 06 02:12:19 2024 +0200
@@ -140,7 +140,6 @@
                 wlock = util.nullcontextmanager()
                 gotlock = False
             with wlock, self.dirstate.running_status(self):
-
                 # First check if paths or patterns were specified on the
                 # command line.  If there were, and they don't match any
                 # largefiles, we should just bail here and let super
--- a/hgext/narrow/narrowbundle2.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/narrow/narrowbundle2.py	Fri Sep 06 02:12:19 2024 +0200
@@ -37,6 +37,7 @@
 _CSHEADERSIZE = struct.calcsize(_ELIDEDCSHEADER)
 _MFHEADERSIZE = struct.calcsize(_ELIDEDMFHEADER)
 
+
 # Serve a changegroup for a client with a narrow clone.
 def getbundlechangegrouppart_narrow(
     bundler,
--- a/hgext/notify.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/notify.py	Fri Sep 06 02:12:19 2024 +0200
@@ -543,7 +543,6 @@
             )
 
     def diff(self, ctx, ref=None):
-
         maxdiff = int(self.ui.config(b'notify', b'maxdiff'))
         prev = ctx.p1().node()
         if ref:
--- a/hgext/patchbomb.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/patchbomb.py	Fri Sep 06 02:12:19 2024 +0200
@@ -261,7 +261,6 @@
     numbered,
     patchname=None,
 ):
-
     desc = []
     node = None
     body = b''
--- a/hgext/phabricator.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/phabricator.py	Fri Sep 06 02:12:19 2024 +0200
@@ -68,10 +68,17 @@
 import operator
 import re
 import time
+import typing
 
 from mercurial.node import bin, short
 from mercurial.i18n import _
 from mercurial.thirdparty import attr
+
+# Force pytype to use the non-vendored package
+if typing.TYPE_CHECKING:
+    # noinspection PyPackageRequirements
+    import attr
+
 from mercurial import (
     cmdutil,
     context,
@@ -698,7 +705,7 @@
     oldLength = attr.ib(default=0)  # camelcase-required
     newOffset = attr.ib(default=0)  # camelcase-required
     newLength = attr.ib(default=0)  # camelcase-required
-    corpus = attr.ib(default='')
+    corpus = attr.ib(default=b'')
     # These get added to the phabchange's equivalents
     addLines = attr.ib(default=0)  # camelcase-required
     delLines = attr.ib(default=0)  # camelcase-required
--- a/hgext/rebase.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/rebase.py	Fri Sep 06 02:12:19 2024 +0200
@@ -830,7 +830,6 @@
                 cleanup = False
 
             if cleanup:
-
                 if rebased:
                     strippoints = [
                         c.node() for c in repo.set(b'roots(%ld)', rebased)
--- a/hgext/remotefilelog/basepack.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/remotefilelog/basepack.py	Fri Sep 06 02:12:19 2024 +0200
@@ -45,7 +45,7 @@
 # bisect) with (8 step fanout scan + 1 step bisect)
 # 5 step bisect = log(2^16 / 8 / 255)  # fanout
 # 10 step fanout scan = 2^16 / (2^16 / 8)  # fanout space divided by entries
-SMALLFANOUTCUTOFF = 2 ** 16 // 8
+SMALLFANOUTCUTOFF = 2**16 // 8
 
 # The amount of time to wait between checking for new packs. This prevents an
 # exception when data is moved to a new pack after the process has already
@@ -275,7 +275,7 @@
 class basepack(versionmixin):
     # The maximum amount we should read via mmap before remmaping so the old
     # pages can be released (100MB)
-    MAXPAGEDIN = 100 * 1024 ** 2
+    MAXPAGEDIN = 100 * 1024**2
 
     SUPPORTED_VERSIONS = [2]
 
--- a/hgext/remotefilelog/connectionpool.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/remotefilelog/connectionpool.py	Fri Sep 06 02:12:19 2024 +0200
@@ -38,7 +38,6 @@
                 pass
 
         if conn is None:
-
             peer = hg.peer(self._repo.ui, {}, path)
             if hasattr(peer, '_cleanup'):
 
--- a/hgext/remotefilelog/datapack.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/remotefilelog/datapack.py	Fri Sep 06 02:12:19 2024 +0200
@@ -414,7 +414,7 @@
 
     def add(self, name, node, deltabasenode, delta, metadata=None):
         # metadata is a dict, ex. {METAKEYFLAG: flag}
-        if len(name) > 2 ** 16:
+        if len(name) > 2**16:
             raise RuntimeError(_(b"name too long %s") % name)
         if len(node) != 20:
             raise RuntimeError(_(b"node should be 20 bytes %s") % node)
--- a/hgext/remotefilelog/debugcommands.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/remotefilelog/debugcommands.py	Fri Sep 06 02:12:19 2024 +0200
@@ -34,7 +34,7 @@
 )
 
 
-def debugremotefilelog(ui, path, **opts):
+def debugremotefilelog(ui, path, **opts) -> None:
     decompress = opts.get('decompress')
 
     size, firstnode, mapping = parsefileblob(path, decompress)
@@ -62,7 +62,7 @@
             queue.append(p2)
 
 
-def buildtemprevlog(repo, file):
+def buildtemprevlog(repo, file) -> filelog.FileLog:
     # get filename key
     filekey = hex(hashutil.sha1(file).digest())
     filedir = os.path.join(repo.path, b'store/data', filekey)
@@ -115,11 +115,11 @@
     r = buildtemprevlog(repo, file_)
 
     # debugindex like normal
-    format = opts.get(b'format', 0)
+    format = opts.get('format', 0)
     if format not in (0, 1):
         raise error.Abort(_(b"unknown format %d") % format)
 
-    generaldelta = r.version & revlog.FLAG_GENERALDELTA
+    generaldelta = r.get_revlog()._format_flags & revlog.FLAG_GENERALDELTA
     if generaldelta:
         basehdr = b' delta'
     else:
@@ -144,9 +144,9 @@
     for i in r:
         node = r.node(i)
         if generaldelta:
-            base = r.deltaparent(i)
+            base = r.get_revlog().deltaparent(i)
         else:
-            base = r.chainbase(i)
+            base = r.get_revlog().chainbase(i)
         if format == 0:
             try:
                 pp = r.parents(node)
@@ -156,8 +156,8 @@
                 b"% 6d % 9d % 7d % 6d % 7d %s %s %s\n"
                 % (
                     i,
-                    r.start(i),
-                    r.length(i),
+                    r.get_revlog().start(i),
+                    r.get_revlog().length(i),
                     base,
                     r.linkrev(i),
                     short(node),
@@ -171,10 +171,10 @@
                 b"% 6d %04x % 8d % 8d % 8d % 6d % 6d % 6d % 6d %s\n"
                 % (
                     i,
-                    r.flags(i),
-                    r.start(i),
-                    r.length(i),
-                    r.rawsize(i),
+                    r.get_revlog().flags(i),
+                    r.get_revlog().start(i),
+                    r.get_revlog().length(i),
+                    r.get_revlog().rawsize(i),
                     base,
                     r.linkrev(i),
                     pr[0],
--- a/hgext/remotefilelog/remotefilectx.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/remotefilelog/remotefilectx.py	Fri Sep 06 02:12:19 2024 +0200
@@ -100,7 +100,11 @@
             if path in data[3]:  # checking the 'files' field.
                 # The file has been touched, check if the hash is what we're
                 # looking for.
-                if fileid == mfl[data[0]].readfast().get(path):
+                #
+                # The change has to be against a parent, otherwise we might be
+                # missing linkrev worthy changes.
+                m = mfl[data[0]].read_delta_parents(exact=False)
+                if fileid == m.get(path):
                     return rev
 
         # Couldn't find the linkrev. This should generally not happen, and will
@@ -199,8 +203,10 @@
         manifestnode, files = ancctx[0], ancctx[3]
         # If the file was touched in this ancestor, and the content is similar
         # to the one we are searching for.
-        if path in files and fnode == mfl[manifestnode].readfast().get(path):
-            return cl.node(ancrev)
+        if path in files:
+            m = mfl[manifestnode].read_delta_parents(exact=False)
+            if fnode == m.get(path):
+                return cl.node(ancrev)
         return None
 
     def _adjustlinknode(self, path, filelog, fnode, srcrev, inclusive=False):
--- a/hgext/remotefilelog/remotefilelog.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/remotefilelog/remotefilelog.py	Fri Sep 06 02:12:19 2024 +0200
@@ -8,6 +8,10 @@
 
 import collections
 
+from typing import (
+    Iterator,
+)
+
 from mercurial.node import bin
 from mercurial.i18n import _
 from mercurial import (
@@ -41,7 +45,6 @@
 
 
 class remotefilelog:
-
     _flagserrorclass = error.RevlogError
 
     def __init__(self, opener, path, repo):
@@ -297,7 +300,7 @@
         deltamode=None,
         sidedata_helpers=None,
         debug_info=None,
-    ):
+    ) -> Iterator[revlog.RevLogRevisionDelta]:
         # we don't use any of these parameters here
         del nodesorder, revisiondata, assumehaveparentrevisions, deltaprevious
         del deltamode
--- a/hgext/remotefilelog/repack.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/remotefilelog/repack.py	Fri Sep 06 02:12:19 2024 +0200
@@ -468,18 +468,14 @@
 
     # process the commits in toposorted order starting from the oldest
     for r in reversed(keep._list):
-        if repo[r].p1().rev() in processed:
-            # if the direct parent has already been processed
-            # then we only need to process the delta
-            m = repo[r].manifestctx().readdelta()
-        else:
-            # otherwise take the manifest and diff it
-            # with the previous manifest if one exists
+        delta_from, m = repo[r].manifestctx().read_any_fast_delta(processed)
+        if delta_from is None and lastmanifest is not None:
+            # could not find a delta, compute one.
+            # XXX (is this really faster?)
+            full = m
             if lastmanifest:
-                m = repo[r].manifest().diff(lastmanifest)
-            else:
-                m = repo[r].manifest()
-        lastmanifest = repo[r].manifest()
+                m = m.diff(lastmanifest)
+            lastmanifest = full
         processed.add(r)
 
         # populate keepkeys with keys from the current manifest
--- a/hgext/remotefilelog/shallowrepo.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/remotefilelog/shallowrepo.py	Fri Sep 06 02:12:19 2024 +0200
@@ -32,6 +32,7 @@
     shallowutil,
 )
 
+
 # These make*stores functions are global so that other extensions can replace
 # them.
 def makelocalstores(repo):
@@ -291,11 +292,7 @@
 
                 # Decompressing manifests is expensive.
                 # When possible, only read the deltas.
-                p1, p2 = mfrevlog.parentrevs(mfrev)
-                if p1 in visited and p2 in visited:
-                    mfdict = mfl[mfnode].readfast()
-                else:
-                    mfdict = mfl[mfnode].read()
+                mfdict = mfl[mfnode].read_any_fast_delta(visited)[1]
 
                 diff = mfdict.items()
                 if pats:
--- a/hgext/remotenames.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/remotenames.py	Fri Sep 06 02:12:19 2024 +0200
@@ -259,7 +259,6 @@
 
 
 def reposetup(ui, repo):
-
     # set the config option to store remotenames
     repo.ui.setconfig(b'experimental', b'remotenames', True, b'remotenames-ext')
 
--- a/hgext/sqlitestore.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/sqlitestore.py	Fri Sep 06 02:12:19 2024 +0200
@@ -47,6 +47,7 @@
 import sqlite3
 import struct
 import threading
+import typing
 import zlib
 
 from mercurial.i18n import _
@@ -56,6 +57,12 @@
     short,
 )
 from mercurial.thirdparty import attr
+
+# Force pytype to use the non-vendored package
+if typing.TYPE_CHECKING:
+    # noinspection PyPackageRequirements
+    import attr
+
 from mercurial import (
     ancestor,
     dagop,
@@ -649,7 +656,6 @@
             deltamode=deltamode,
             sidedata_helpers=sidedata_helpers,
         ):
-
             yield delta
 
     # End of ifiledata interface.
--- a/hgext/uncommit.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/uncommit.py	Fri Sep 06 02:12:19 2024 +0200
@@ -154,7 +154,6 @@
     cmdutil.resolve_commit_options(ui, opts)
 
     with repo.wlock(), repo.lock():
-
         st = repo.status()
         m, a, r, d = st.modified, st.added, st.removed, st.deleted
         isdirtypath = any(set(m + a + r + d) & set(pats))
@@ -264,7 +263,6 @@
 
     unfi = repo.unfiltered()
     with repo.wlock(), repo.lock(), repo.transaction(b'unamend'):
-
         # identify the commit from which to unamend
         curctx = repo[b'.']
 
--- a/hgext/zeroconf/Zeroconf.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/hgext/zeroconf/Zeroconf.py	Fri Sep 06 02:12:19 2024 +0200
@@ -1307,6 +1307,7 @@
         delay = _LISTENER_TIME
         next = now + delay
         last = now + timeout
+        result = False
         try:
             zeroconf.addListener(
                 self, DNSQuestion(self.name, _TYPE_ANY, _CLASS_IN)
@@ -1352,7 +1353,7 @@
 
                 zeroconf.wait(min(next, last) - now)
                 now = currentTimeMillis()
-            result = 1
+            result = True
         finally:
             zeroconf.removeListener(self)
 
--- a/i18n/polib.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/i18n/polib.py	Fri Sep 06 02:12:19 2024 +0200
@@ -64,7 +64,6 @@
     def u(s):
         return unicode(s, "unicode_escape")
 
-
 else:
     PY3 = True
     text_type = str
@@ -1889,7 +1888,6 @@
         chunks.reverse()
 
         while chunks:
-
             # Start the list of chunks that will make up the current line.
             # cur_len is just the length of all the chunks in cur_line.
             cur_line = []
--- a/mercurial/ancestor.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/ancestor.py	Fri Sep 06 02:12:19 2024 +0200
@@ -88,7 +88,7 @@
         depth = [0] * count
         seen = [0] * count
         mapping = []
-        for (i, n) in enumerate(sorted(nodes)):
+        for i, n in enumerate(sorted(nodes)):
             depth[n] = 1
             b = 1 << i
             seen[n] = b
--- a/mercurial/bookmarks.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/bookmarks.py	Fri Sep 06 02:12:19 2024 +0200
@@ -685,7 +685,7 @@
     remotemarks"""
     changed = []
     localmarks = repo._bookmarks
-    for (b, id) in remotemarks.items():
+    for b, id in remotemarks.items():
         if id != localmarks.get(b, None) and id in repo:
             changed.append((b, id, ui.debug, _(b"updating bookmark %s\n") % b))
     for b in localmarks:
--- a/mercurial/bundle2.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/bundle2.py	Fri Sep 06 02:12:19 2024 +0200
@@ -153,6 +153,7 @@
 import string
 import struct
 import sys
+import typing
 
 from .i18n import _
 from .node import (
@@ -181,6 +182,17 @@
 )
 from .interfaces import repository
 
+if typing.TYPE_CHECKING:
+    from typing import (
+        Dict,
+        List,
+        Optional,
+        Tuple,
+        Union,
+    )
+
+    Capabilities = Dict[bytes, Union[List[bytes], Tuple[bytes, ...]]]
+
 urlerr = util.urlerr
 urlreq = util.urlreq
 
@@ -602,7 +614,7 @@
             )
 
 
-def decodecaps(blob):
+def decodecaps(blob: bytes) -> "Capabilities":
     """decode a bundle2 caps bytes blob into a dictionary
 
     The blob is a list of capabilities (one per line)
@@ -662,11 +674,14 @@
 
     _magicstring = b'HG20'
 
-    def __init__(self, ui, capabilities=()):
+    def __init__(self, ui, capabilities: "Optional[Capabilities]" = None):
+        if capabilities is None:
+            capabilities = {}
+
         self.ui = ui
         self._params = []
         self._parts = []
-        self.capabilities = dict(capabilities)
+        self.capabilities: "Capabilities" = dict(capabilities)
         self._compengine = util.compengines.forbundletype(b'UN')
         self._compopts = None
         # If compression is being handled by a consumer of the raw
@@ -1271,7 +1286,6 @@
         return None
 
     def __call__(self):
-
         self.ui.debug(
             b'bundle2-input-stream-interrupt: opening out of band context\n'
         )
@@ -1612,7 +1626,7 @@
 
 # These are only the static capabilities.
 # Check the 'getrepocaps' function for the rest.
-capabilities = {
+capabilities: "Capabilities" = {
     b'HG20': (),
     b'bookmarks': (),
     b'error': (b'abort', b'unsupportedcontent', b'pushraced', b'pushkey'),
@@ -1626,7 +1640,8 @@
 }
 
 
-def getrepocaps(repo, allowpushback=False, role=None):
+# TODO: drop the default value for 'role'
+def getrepocaps(repo, allowpushback: bool = False, role=None) -> "Capabilities":
     """return the bundle2 capabilities for a given repo
 
     Exists to allow extensions (like evolution) to mutate the capabilities.
@@ -1675,7 +1690,7 @@
     return caps
 
 
-def bundle2caps(remote):
+def bundle2caps(remote) -> "Capabilities":
     """return the bundle capabilities of a peer as dict"""
     raw = remote.capable(b'bundle2')
     if not raw and raw != b'':
@@ -1684,7 +1699,7 @@
     return decodecaps(capsblob)
 
 
-def obsmarkersversion(caps):
+def obsmarkersversion(caps: "Capabilities"):
     """extract the list of supported obsmarkers versions from a bundle2caps dict"""
     obscaps = caps.get(b'obsmarkers', ())
     return [int(c[1:]) for c in obscaps if c.startswith(b'V')]
@@ -1725,7 +1740,7 @@
         msg %= count
         raise error.ProgrammingError(msg)
 
-    caps = {}
+    caps: "Capabilities" = {}
     if opts.get(b'obsolescence', False):
         caps[b'obsmarkers'] = (b'V1',)
     stream_version = opts.get(b'stream', b"")
@@ -2598,7 +2613,6 @@
 
 @parthandler(b'stream2', (b'requirements', b'filecount', b'bytecount'))
 def handlestreamv2bundle(op, part):
-
     requirements = urlreq.unquote(part.params[b'requirements'])
     requirements = requirements.split(b',') if requirements else []
     filecount = int(part.params[b'filecount'])
--- a/mercurial/bundlecaches.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/bundlecaches.py	Fri Sep 06 02:12:19 2024 +0200
@@ -4,6 +4,7 @@
 # GNU General Public License version 2 or any later version.
 
 import collections
+import typing
 
 from typing import (
     Dict,
@@ -15,6 +16,11 @@
 
 from .thirdparty import attr
 
+# Force pytype to use the non-vendored package
+if typing.TYPE_CHECKING:
+    # noinspection PyPackageRequirements
+    import attr
+
 from . import (
     error,
     requirements as requirementsmod,
@@ -30,7 +36,7 @@
 CLONEBUNDLESCHEME = b"peer-bundle-cache://"
 
 
-def get_manifest(repo):
+def get_manifest(repo) -> bytes:
     """get the bundle manifest to be served to a client from a server"""
     raw_text = repo.vfs.tryread(CB_MANIFEST_FILE)
     entries = [e.split(b' ', 1) for e in raw_text.splitlines()]
@@ -46,7 +52,7 @@
     return b''.join(new_lines)
 
 
-def alter_bundle_url(repo, url):
+def alter_bundle_url(repo, url: bytes) -> bytes:
     """a function that exist to help extension and hosting to alter the url
 
     This will typically be used to inject authentication information in the url
--- a/mercurial/bundlerepo.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/bundlerepo.py	Fri Sep 06 02:12:19 2024 +0200
@@ -408,7 +408,7 @@
         with os.fdopen(fdtemp, 'wb') as fptemp:
             fptemp.write(header)
             while True:
-                chunk = readfn(2 ** 18)
+                chunk = readfn(2**18)
                 if not chunk:
                     break
                 fptemp.write(chunk)
--- a/mercurial/cext/base85.c	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/cext/base85.c	Fri Sep 06 02:12:19 2024 +0200
@@ -38,7 +38,7 @@
 	unsigned int acc, val, ch;
 	int pad = 0;
 
-	if (!PyArg_ParseTuple(args, "y#|i", &text, &len, &pad)) {
+	if (!PyArg_ParseTuple(args, "y#|p", &text, &len, &pad)) {
 		return NULL;
 	}
 
--- a/mercurial/cext/base85.pyi	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/cext/base85.pyi	Fri Sep 06 02:12:19 2024 +0200
@@ -1,6 +1,4 @@
-from typing import Optional
-
 version: int
 
-def b85encode(text: bytes, pad: Optional[int]) -> bytes: ...
+def b85encode(text: bytes, pad: bool = False) -> bytes: ...
 def b85decode(text: bytes) -> bytes: ...
--- a/mercurial/cext/osutil.c	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/cext/osutil.c	Fri Sep 06 02:12:19 2024 +0200
@@ -36,6 +36,12 @@
 #endif
 #endif
 
+#ifndef _WIN32
+#include <sys/mman.h>
+#include <pthread.h>
+#endif
+
+
 #ifdef __APPLE__
 #include <sys/attr.h>
 #include <sys/vnode.h>
@@ -1203,6 +1209,49 @@
 }
 #endif
 
+#ifdef MADV_POPULATE_READ
+
+typedef struct {
+    void * mmap_address;
+    size_t length;
+} mmap_info;
+
+static void _mmap_populate(mmap_info *info) {
+    /* We explicitly does not check the return value as we don't care about it.
+     * the madvise is here to help performance and we don't care if it fails
+     * (for example because the mapping is no longer valid) */
+    void * mmap_address = info->mmap_address;
+    size_t length = info->length;
+    free(info);
+    madvise(mmap_address, length, MADV_POPULATE_READ);
+}
+
+static PyObject *background_mmap_populate(PyObject *self, PyObject *mmap) {
+    Py_buffer b;
+    pthread_t thread_id;
+    mmap_info *info;
+    if (PyObject_GetBuffer(mmap, &b, PyBUF_CONTIG_RO | PyBUF_C_CONTIGUOUS) == -1) {
+        return NULL;
+    }
+    info = (mmap_info *)malloc(sizeof(mmap_info));
+    info->mmap_address=b.buf;
+    info->length=b.len;
+    /* note: for very large map, we could spin multiple thread populating
+     * different area */
+    pthread_create(&thread_id, NULL, (void *) &_mmap_populate, info);
+    /* We don't keep track of this thread as it is fine for it to die when we
+     * exit. */
+    pthread_detach(thread_id);
+    /* We release the PyBuffer in the main thread to let the object be garbage
+     * collected as soon as possible. This might result in the memory map being
+     * closed while the background thread is working. That will result in a
+     * error in the background thread we can ignore. */
+    PyBuffer_Release(&b);
+	Py_RETURN_NONE;
+}
+
+#endif
+
 static char osutil_doc[] = "Native operating system services.";
 
 static PyMethodDef methods[] = {
@@ -1237,6 +1286,10 @@
 		"Is a CoreGraphics session available?"
 	},
 #endif
+#ifdef MADV_POPULATE_READ
+	{"background_mmap_populate", (PyCFunction)background_mmap_populate, METH_O,
+	 "populate a mmap in the background\n"},
+#endif
 	{NULL, NULL}
 };
 
--- a/mercurial/cext/parsers.c	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/cext/parsers.c	Fri Sep 06 02:12:19 2024 +0200
@@ -1232,6 +1232,15 @@
 	 * should only occur in unusual circumstances (e.g. if sys.hexversion
 	 * is manually set to an invalid value). */
 	if ((hexversion == -1) || (hexversion >> 16 != PY_VERSION_HEX >> 16)) {
+		PyObject *sys = PyImport_ImportModule("sys"), *executable;
+		if (!sys) {
+			return -1;
+		}
+		executable = PyObject_GetAttrString(sys, "executable");
+		Py_DECREF(sys);
+		if (!executable) {
+			return -1;
+		}
 		PyErr_Format(PyExc_ImportError,
 		             "%s: The Mercurial extension "
 		             "modules were compiled with Python " PY_VERSION
@@ -1240,7 +1249,8 @@
 		             "sys.hexversion=%ld: "
 		             "Python %s\n at: %s",
 		             versionerrortext, hexversion, Py_GetVersion(),
-		             Py_GetProgramFullPath());
+		             PyUnicode_AsUTF8(executable));
+		Py_DECREF(executable);
 		return -1;
 	}
 	return 0;
--- a/mercurial/changegroup.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/changegroup.py	Fri Sep 06 02:12:19 2024 +0200
@@ -407,7 +407,7 @@
                 yield chunkheader(len(chunk))
                 pos = 0
                 while pos < len(chunk):
-                    next = pos + 2 ** 20
+                    next = pos + 2**20
                     yield chunk[pos:next]
                     pos = next
             yield closechunk()
@@ -611,7 +611,7 @@
                 # validate incoming csets have their manifests
                 for cset in range(clstart, clend):
                     mfnode = cl.changelogrevision(cset).manifest
-                    mfest = ml[mfnode].readdelta()
+                    mfest = ml[mfnode].read_delta_new_entries()
                     # store file nodes we must see
                     for f, n in mfest.items():
                         needfiles.setdefault(f, set()).add(n)
@@ -1830,7 +1830,8 @@
                 treemanifests to send.
                 """
                 clnode = nodes[x]
-                mdata = mfl.get(tree, x).readfast(shallow=True)
+                mctx = mfl.get(tree, x)
+                mdata = mctx.read_delta_parents(shallow=True, exact=False)
                 for p, n, fl in mdata.iterentries():
                     if fl == b't':  # subdirectory manifest
                         subtree = tree + p + b'/'
--- a/mercurial/changelog.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/changelog.py	Fri Sep 06 02:12:19 2024 +0200
@@ -6,6 +6,8 @@
 # GNU General Public License version 2 or any later version.
 
 
+import typing
+
 from .i18n import _
 from .node import (
     bin,
@@ -13,6 +15,11 @@
 )
 from .thirdparty import attr
 
+# Force pytype to use the non-vendored package
+if typing.TYPE_CHECKING:
+    # noinspection PyPackageRequirements
+    import attr
+
 from . import (
     encoding,
     error,
--- a/mercurial/chgserver.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/chgserver.py	Fri Sep 06 02:12:19 2024 +0200
@@ -151,7 +151,7 @@
     """
     modules = [m for n, m in extensions.extensions(ui)]
     try:
-        from . import __version__
+        from . import __version__  # pytype: disable=import-error
 
         modules.append(__version__)
     except ImportError:
--- a/mercurial/cmdutil.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/cmdutil.py	Fri Sep 06 02:12:19 2024 +0200
@@ -11,6 +11,7 @@
 import functools
 import os
 import re
+import typing
 
 from typing import (
     Any,
@@ -33,6 +34,11 @@
 )
 from .thirdparty import attr
 
+# Force pytype to use the non-vendored package
+if typing.TYPE_CHECKING:
+    # noinspection PyPackageRequirements
+    import attr
+
 from . import (
     bookmarks,
     bundle2,
@@ -1112,7 +1118,7 @@
         ui.status(_(b"changed branch on %d changesets\n") % len(replacements))
 
 
-def findrepo(p):
+def findrepo(p: bytes) -> Optional[bytes]:
     while not os.path.isdir(os.path.join(p, b".hg")):
         oldp, p = p, os.path.dirname(p)
         if p == oldp:
@@ -3833,7 +3839,6 @@
         original_headers = patch.parsepatch(diff)
 
         try:
-
             chunks, opts = recordfilter(
                 repo.ui, original_headers, match, operation=operation
             )
--- a/mercurial/commit.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/commit.py	Fri Sep 06 02:12:19 2024 +0200
@@ -214,15 +214,15 @@
             elif narrow_action == mergestate.CHANGE_ADDED:
                 files.mark_added(f)
                 added.append(f)
-                m[f] = m2[f]
+                fnode = m2[f]
                 flags = m2ctx.find(f)[1] or b''
-                m.setflag(f, flags)
+                m.set(f, fnode, flags)
             elif narrow_action == mergestate.CHANGE_MODIFIED:
                 files.mark_touched(f)
                 added.append(f)
-                m[f] = m2[f]
+                fnode = m2[f]
                 flags = m2ctx.find(f)[1] or b''
-                m.setflag(f, flags)
+                m.set(f, fnode, flags)
             else:
                 msg = _(b"corrupted mergestate, unknown narrow action: %b")
                 hint = _(b"restart the merge")
@@ -234,7 +234,7 @@
                 removed.append(f)
             else:
                 added.append(f)
-                m[f], is_touched = _filecommit(
+                fnode, is_touched = _filecommit(
                     repo, fctx, m1, m2, linkrev, tr, writefilecopymeta, ms
                 )
                 if is_touched:
@@ -244,7 +244,7 @@
                         files.mark_merged(f)
                     else:
                         files.mark_touched(f)
-                m.setflag(f, fctx.flags())
+                m.set(f, fnode, fctx.flags())
         except OSError:
             repo.ui.warn(_(b"trouble committing %s!\n") % uipathfn(f))
             raise
--- a/mercurial/configitems.toml	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/configitems.toml	Fri Sep 06 02:12:19 2024 +0200
@@ -987,10 +987,6 @@
 
 [[items]]
 section = "experimental"
-name = "mmapindexthreshold"
-
-[[items]]
-section = "experimental"
 name = "narrow"
 default = false
 
@@ -1815,6 +1811,20 @@
 default = "stat"
 
 [[items]]
+section = "profiling"
+name = "py-spy.exe"
+default = "py-spy"
+
+[[items]]
+section = "profiling"
+name = "py-spy.freq"
+default = 100
+
+[[items]]
+section = "profiling"
+name = "py-spy.format"
+
+[[items]]
 section = "progress"
 name = "assume-tty"
 default = false
@@ -2167,6 +2177,16 @@
 
 [[items]]
 section = "storage"
+name = "revlog.mmap.index"
+default-type = "dynamic"
+
+[[items]]
+section = "storage"
+name = "revlog.mmap.index:size-threshold"
+default = "1 MB"
+
+[[items]]
+section = "storage"
 name = "revlog.optimize-delta-parent-choice"
 default = true
 alias = [["format", "aggressivemergedeltas"]]
--- a/mercurial/context.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/context.py	Fri Sep 06 02:12:19 2024 +0200
@@ -265,6 +265,9 @@
                 raise error.ManifestLookupError(
                     self._node or b'None', path, _(b'not found in manifest')
                 )
+        # Try to find the file in the manifest delta that can be faster to read
+        # than a full manifest. If we fail to find the file, it might still
+        # exist in the full manifest, so lets look for it there.
         if '_manifestdelta' in self.__dict__ or path in self.files():
             if path in self._manifestdelta:
                 return (
@@ -538,7 +541,10 @@
 
     @propertycache
     def _manifestdelta(self):
-        return self._manifestctx.readdelta()
+        base, delta = self._manifestctx.read_any_fast_delta()
+        if base is None:
+            self._manifest = delta
+        return delta
 
     @propertycache
     def _parents(self):
@@ -1057,7 +1063,12 @@
                 if path in ac[3]:  # checking the 'files' field.
                     # The file has been touched, check if the content is
                     # similar to the one we search for.
-                    if fnode == mfl[ac[0]].readfast().get(path):
+                    #
+                    # If the content is similar to one of the parents, then it
+                    # cannot be an adjusted linkrev
+                    if fnode == (
+                        mfl[ac[0]].read_delta_parents(exact=False).get(path)
+                    ):
                         return a
             # In theory, we should never get out of that loop without a result.
             # But if manifest uses a buggy file revision (not children of the
--- a/mercurial/copies.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/copies.py	Fri Sep 06 02:12:19 2024 +0200
@@ -915,11 +915,14 @@
         self.movewithdir = {} if movewithdir is None else movewithdir
 
     def __repr__(self):
-        return '<branch_copies\n  copy=%r\n  renamedelete=%r\n  dirmove=%r\n  movewithdir=%r\n>' % (
-            self.copy,
-            self.renamedelete,
-            self.dirmove,
-            self.movewithdir,
+        return (
+            '<branch_copies\n  copy=%r\n  renamedelete=%r\n  dirmove=%r\n  movewithdir=%r\n>'
+            % (
+                self.copy,
+                self.renamedelete,
+                self.dirmove,
+                self.movewithdir,
+            )
         )
 
 
--- a/mercurial/dagop.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/dagop.py	Fri Sep 06 02:12:19 2024 +0200
@@ -7,8 +7,18 @@
 
 
 import heapq
+import typing
+from typing import (
+    List,
+)
 
 from .thirdparty import attr
+
+# Force pytype to use the non-vendored package
+if typing.TYPE_CHECKING:
+    # noinspection PyPackageRequirements
+    import attr
+
 from .node import nullrev
 from . import (
     error,
@@ -747,7 +757,7 @@
     return child
 
 
-def annotate(base, parents, skiprevs=None, diffopts=None):
+def annotate(base, parents, skiprevs=None, diffopts=None) -> List[annotateline]:
     """Core algorithm for filectx.annotate()
 
     `parents(fctx)` is a function returning a list of parent filectxs.
--- a/mercurial/debugcommands.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/debugcommands.py	Fri Sep 06 02:12:19 2024 +0200
@@ -4510,8 +4510,10 @@
         # TODO consider not doing this because we skip
         # ``hg.wirepeersetupfuncs`` and potentially other useful functionality.
         u = urlutil.url(path)
-        if u.scheme != b'http':
-            raise error.Abort(_(b'only http:// paths are currently supported'))
+        if u.scheme not in (b'http', b'https'):
+            raise error.Abort(
+                _(b'only http:// and https:// paths are currently supported')
+            )
 
         url, authinfo = u.authinfo()
         openerargs = {
--- a/mercurial/dirstate.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/dirstate.py	Fri Sep 06 02:12:19 2024 +0200
@@ -10,6 +10,7 @@
 import contextlib
 import os
 import stat
+import typing
 import uuid
 
 from .i18n import _
@@ -134,9 +135,7 @@
 CHANGE_TYPE_FILES = "files"
 
 
-@interfaceutil.implementer(intdirstate.idirstate)
-class dirstate:
-
+class DirState:
     # used by largefile to avoid overwritting transaction callback
     _tr_key_suffix = b''
 
@@ -644,8 +643,8 @@
             p2 = self._nodeconstants.nullid
         if self._changing_level == 0:
             raise ValueError(
-                b"cannot set dirstate parent outside of "
-                b"dirstate.changing_parents context manager"
+                "cannot set dirstate parent outside of "
+                "dirstate.changing_parents context manager"
             )
 
         self._dirty = True
@@ -789,7 +788,7 @@
         a `with dirstate.changing_parents(repo):` context.
         """
         if self.in_merge:
-            msg = b'update_file_reference should not be called when merging'
+            msg = 'update_file_reference should not be called when merging'
             raise error.ProgrammingError(msg)
         entry = self._map.get(filename)
         if entry is None:
@@ -880,7 +879,6 @@
         possibly_dirty=False,
         parentfiledata=None,
     ):
-
         # note: I do not think we need to double check name clash here since we
         # are in a update/merge case that should already have taken care of
         # this. The test agrees
@@ -1092,7 +1090,6 @@
 
         write_key = self._use_tracked_hint and self._dirty_tracked_set
         if tr:
-
             self._setup_tr_abort(tr)
             self._attached_to_a_transaction = True
 
@@ -1286,7 +1283,7 @@
                     badfn(ff, badtype(kind))
                     if nf in dmap:
                         results[nf] = None
-            except (OSError) as inst:
+            except OSError as inst:
                 # nf not found on disk - it is dirstate only
                 if nf in dmap:  # does it exactly match a missing file?
                     results[nf] = None
@@ -1810,3 +1807,9 @@
             entry = self.get_entry(f)
             if not entry.p1_tracked:
                 yield missing_from_ds % (f, node.short(p1))
+
+
+dirstate = interfaceutil.implementer(intdirstate.idirstate)(DirState)
+
+if typing.TYPE_CHECKING:
+    dirstate = DirState
--- a/mercurial/dirstatemap.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/dirstatemap.py	Fri Sep 06 02:12:19 2024 +0200
@@ -4,6 +4,11 @@
 # GNU General Public License version 2 or any later version.
 
 
+from typing import (
+    Optional,
+    TYPE_CHECKING,
+)
+
 from .i18n import _
 
 from . import (
@@ -12,6 +17,7 @@
     policy,
     testing,
     txnutil,
+    typelib,
     util,
 )
 
@@ -20,6 +26,11 @@
     v2,
 )
 
+if TYPE_CHECKING:
+    from . import (
+        ui as uimod,
+    )
+
 parsers = policy.importmod('parsers')
 rustmod = policy.importrust('dirstate')
 
@@ -46,12 +57,31 @@
     class, with and without Rust extensions enabled.
     """
 
+    _use_dirstate_v2: bool
+    _nodeconstants: typelib.NodeConstants
+    _ui: "uimod.ui"
+    _root: bytes
+    _filename: bytes
+    _nodelen: int
+    _dirtyparents: bool
+    _docket: Optional["docketmod.DirstateDocket"]
+    _write_mode: int
+    _pendingmode: Optional[bool]
+    identity: Optional[typelib.CacheStat]
+
     # please pytype
 
     _map = None
     copymap = None
 
-    def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
+    def __init__(
+        self,
+        ui: "uimod.ui",
+        opener,
+        root: bytes,
+        nodeconstants: typelib.NodeConstants,
+        use_dirstate_v2: bool,
+    ) -> None:
         self._use_dirstate_v2 = use_dirstate_v2
         self._nodeconstants = nodeconstants
         self._ui = ui
@@ -76,16 +106,16 @@
         # for consistent view between _pl() and _read() invocations
         self._pendingmode = None
 
-    def _set_identity(self):
+    def _set_identity(self) -> None:
         self.identity = self._get_current_identity()
 
-    def _get_current_identity(self):
+    def _get_current_identity(self) -> Optional[typelib.CacheStat]:
         try:
             return util.cachestat(self._opener.join(self._filename))
         except FileNotFoundError:
             return None
 
-    def may_need_refresh(self):
+    def may_need_refresh(self) -> bool:
         if 'identity' not in vars(self):
             # no existing identity, we need a refresh
             return True
@@ -104,7 +134,7 @@
             return True
         return current_identity != self.identity
 
-    def preload(self):
+    def preload(self) -> None:
         """Loads the underlying data, if it's not already loaded"""
         self._map
 
@@ -135,7 +165,7 @@
         self._pendingmode = mode
         return fp
 
-    def _readdirstatefile(self, size=-1):
+    def _readdirstatefile(self, size: int = -1) -> bytes:
         try:
             with self._opendirstatefile() as fp:
                 return fp.read(size)
@@ -144,7 +174,7 @@
             return b''
 
     @property
-    def docket(self):
+    def docket(self) -> "docketmod.DirstateDocket":
         if not self._docket:
             if not self._use_dirstate_v2:
                 raise error.ProgrammingError(
@@ -331,7 +361,7 @@
 
         `all` is unused when Rust is not enabled
         """
-        for (filename, item) in self.items():
+        for filename, item in self.items():
             yield (filename, item.state, item.mode, item.size, item.mtime)
 
     def keys(self):
@@ -617,7 +647,8 @@
 
         This should also drop associated copy information
 
-        The fact we actually need to drop it is the responsability of the caller"""
+        The fact we actually need to drop it is the responsability of the caller
+        """
         self._map.pop(f, None)
         self.copymap.pop(f, None)
 
@@ -625,7 +656,6 @@
 if rustmod is not None:
 
     class dirstatemap(_dirstatemapcommon):
-
         ### Core data storage and access
 
         @propertycache
--- a/mercurial/dirstateutils/v2.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/dirstateutils/v2.py	Fri Sep 06 02:12:19 2024 +0200
@@ -7,8 +7,15 @@
 
 
 import struct
+import typing
 
 from ..thirdparty import attr
+
+# Force pytype to use the non-vendored package
+if typing.TYPE_CHECKING:
+    # noinspection PyPackageRequirements
+    import attr
+
 from .. import error, policy
 
 parsers = policy.importmod('parsers')
--- a/mercurial/dispatch.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/dispatch.py	Fri Sep 06 02:12:19 2024 +0200
@@ -85,7 +85,7 @@
         # store the parsed and canonical command
         self.canonical_command = None
 
-    def _runexithandlers(self):
+    def _runexithandlers(self) -> None:
         exc = None
         handlers = self.ui._exithandlers
         try:
@@ -239,7 +239,7 @@
     return status
 
 
-def _rundispatch(req):
+def _rundispatch(req) -> int:
     with tracing.log('dispatch._rundispatch'):
         if req.ferr:
             ferr = req.ferr
--- a/mercurial/encoding.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/encoding.py	Fri Sep 06 02:12:19 2024 +0200
@@ -40,6 +40,16 @@
 
 unichr = chr
 
+if typing.TYPE_CHECKING:
+    # TODO: make a stub file for .cext.charencode, and import here
+    from .pure.charencode import (
+        asciilower,
+        asciiupper,
+        isasciistr,
+        jsonescapeu8fast as _jsonescapeu8fast,
+    )
+
+
 # These unicode characters are ignored by HFS+ (Apple Technote 1150,
 # "Unicode Subtleties"), so we need to ignore them in some places for
 # sanity.
@@ -103,14 +113,15 @@
 if pycompat.iswindows:
     _encodingrewrites[b'cp65001'] = b'utf-8'
 
+encoding: bytes = b''  # help pytype avoid seeing None value
 try:
-    encoding = environ.get(b"HGENCODING")
+    encoding = environ.get(b"HGENCODING", b'')
     if not encoding:
         encoding = locale.getpreferredencoding().encode('ascii') or b'ascii'
         encoding = _encodingrewrites.get(encoding, encoding)
 except locale.Error:
     encoding = b'ascii'
-encodingmode = environ.get(b"HGENCODINGMODE", b"strict")
+encodingmode: bytes = environ.get(b"HGENCODINGMODE", b"strict")
 fallbackencoding = b'ISO-8859-1'
 
 
@@ -366,7 +377,6 @@
             cwd = cwd[0:1].upper() + cwd[1:]
         return cwd
 
-
 else:
     getcwd = os.getcwdb  # re-exports
 
@@ -524,7 +534,7 @@
     other = 0
 
 
-def jsonescape(s: Any, paranoid: Any = False) -> Any:
+def jsonescape(s: bytes, paranoid: bool = False) -> bytes:
     """returns a string suitable for JSON
 
     JSON is problematic for us because it doesn't support non-Unicode
--- a/mercurial/exchange.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/exchange.py	Fri Sep 06 02:12:19 2024 +0200
@@ -1278,18 +1278,18 @@
     ):
         # push everything,
         # use the fast path, no race possible on push
-        cg = changegroup.makechangegroup(
-            pushop.repo,
-            outgoing,
-            b'01',
-            b'push',
-            fastpath=True,
-            bundlecaps=bundlecaps,
-        )
+        fastpath = True
     else:
-        cg = changegroup.makechangegroup(
-            pushop.repo, outgoing, b'01', b'push', bundlecaps=bundlecaps
-        )
+        fastpath = False
+
+    cg = changegroup.makechangegroup(
+        pushop.repo,
+        outgoing,
+        b'01',
+        b'push',
+        fastpath=fastpath,
+        bundlecaps=bundlecaps,
+    )
 
     # apply changegroup to remote
     # local repo finds heads on server, finds out what
--- a/mercurial/extensions.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/extensions.py	Fri Sep 06 02:12:19 2024 +0200
@@ -290,7 +290,7 @@
     with util.timedcm('load all extensions') as stats:
         default_sub_options = ui.configsuboptions(b"extensions", b"*")[1]
 
-        for (name, path) in result:
+        for name, path in result:
             if path:
                 if path[0:1] == b'!':
                     if name not in _disabledextensions:
--- a/mercurial/filelog.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/filelog.py	Fri Sep 06 02:12:19 2024 +0200
@@ -6,6 +6,13 @@
 # GNU General Public License version 2 or any later version.
 
 
+import typing
+
+from typing import (
+    Iterable,
+    Iterator,
+)
+
 from .i18n import _
 from .node import nullrev
 from . import (
@@ -23,8 +30,11 @@
 )
 
 
-@interfaceutil.implementer(repository.ifilestorage)
-class filelog:
+class FileLog:
+    _revlog: revlog.revlog
+    nullid: bytes
+    _fix_issue6528: bool
+
     def __init__(self, opener, path, try_split=False):
         self._revlog = revlog.revlog(
             opener,
@@ -42,7 +52,7 @@
         opts = opener.options
         self._fix_issue6528 = opts.get(b'issue6528.fix-incoming', True)
 
-    def get_revlog(self):
+    def get_revlog(self) -> revlog.revlog:
         """return an actual revlog instance if any
 
         This exist because a lot of code leverage the fact the underlying
@@ -51,10 +61,10 @@
         """
         return self._revlog
 
-    def __len__(self):
+    def __len__(self) -> int:
         return len(self._revlog)
 
-    def __iter__(self):
+    def __iter__(self) -> Iterator[int]:
         return self._revlog.__iter__()
 
     def hasnode(self, node):
@@ -175,7 +185,6 @@
             )
 
         with self._revlog._writing(transaction):
-
             if self._fix_issue6528:
                 deltas = rewrite.filter_delta_issue6528(self._revlog, deltas)
 
@@ -234,7 +243,7 @@
         """
         return not storageutil.filedataequivalent(self, node, text)
 
-    def verifyintegrity(self, state):
+    def verifyintegrity(self, state) -> Iterable[revlog.RevLogProblem]:
         return self._revlog.verifyintegrity(state)
 
     def storageinfo(
@@ -263,6 +272,12 @@
         return self._revlog.clone(tr, destrevlog._revlog, **kwargs)
 
 
+filelog = interfaceutil.implementer(repository.ifilestorage)(FileLog)
+
+if typing.TYPE_CHECKING:
+    filelog = FileLog
+
+
 class narrowfilelog(filelog):
     """Filelog variation to be used with narrow stores."""
 
--- a/mercurial/formatter.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/formatter.py	Fri Sep 06 02:12:19 2024 +0200
@@ -110,6 +110,7 @@
 import itertools
 import os
 import pickle
+import typing
 
 from .i18n import _
 from .node import (
@@ -118,6 +119,11 @@
 )
 from .thirdparty import attr
 
+# Force pytype to use the non-vendored package
+if typing.TYPE_CHECKING:
+    # noinspection PyPackageRequirements
+    import attr
+
 from . import (
     error,
     pycompat,
@@ -176,7 +182,6 @@
 
 
 class baseformatter:
-
     # set to True if the formater output a strict format that does not support
     # arbitrary output in the stream.
     strict_format = False
@@ -421,7 +426,6 @@
 
 
 class jsonformatter(baseformatter):
-
     strict_format = True
 
     def __init__(self, ui, out, topic, opts):
--- a/mercurial/graphmod.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/graphmod.py	Fri Sep 06 02:12:19 2024 +0200
@@ -18,8 +18,16 @@
 """
 
 
+import typing
+
 from .node import nullrev
 from .thirdparty import attr
+
+# Force pytype to use the non-vendored package
+if typing.TYPE_CHECKING:
+    # noinspection PyPackageRequirements
+    import attr
+
 from . import (
     dagop,
     smartset,
@@ -133,8 +141,7 @@
     else:
         getconf = lambda rev: {}
 
-    for (cur, type, data, parents) in dag:
-
+    for cur, type, data, parents in dag:
         # Compute seen and next
         if cur not in seen:
             seen.append(cur)  # new head
@@ -244,7 +251,7 @@
 
 
 def _fixlongrightedges(edges):
-    for (i, (start, end)) in enumerate(edges):
+    for i, (start, end) in enumerate(edges):
         if end > start:
             edges[i] = (start, end + 1)
 
@@ -265,7 +272,7 @@
 
 
 def _drawedges(echars, edges, nodeline, interline):
-    for (start, end) in edges:
+    for start, end in edges:
         if start == end + 1:
             interline[2 * end + 1] = b"/"
         elif start == end - 1:
@@ -381,7 +388,7 @@
     this function can be monkey-patched by extensions to alter graph display
     without needing to mimic all of the edge-fixup logic in ascii()
     """
-    for (ln, logstr) in graph:
+    for ln, logstr in graph:
         ui.write((ln + logstr).rstrip() + b"\n")
 
 
--- a/mercurial/helptext/config.txt	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/helptext/config.txt	Fri Sep 06 02:12:19 2024 +0200
@@ -2344,6 +2344,17 @@
         you do, consider talking with the mercurial developer community about your
         repositories.
 
+``revlog.mmap.index``
+    Whether to use the Operating System "memory mapping" feature (when
+    possible) to access the revlog index. This improves performance
+    and reduces memory pressure.
+
+.. container:: verbose
+
+    ``revlog.mmap.index:size-threshold``
+
+        The size of index above which to use the "memory mapping" feature.
+
 ``revlog.optimize-delta-parent-choice``
     When storing a merge revision, both parents will be equally considered as
     a possible delta base. This results in better delta selection and improved
--- a/mercurial/hg.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/hg.py	Fri Sep 06 02:12:19 2024 +0200
@@ -11,6 +11,7 @@
 import posixpath
 import shutil
 import stat
+import typing
 import weakref
 
 from .i18n import _
@@ -57,6 +58,11 @@
     urlutil,
 )
 
+if typing.TYPE_CHECKING:
+    from typing import (
+        List,
+        Tuple,
+    )
 
 release = lock.release
 
@@ -1597,7 +1603,7 @@
 # Files of interest
 # Used to check if the repository has changed looking at mtime and size of
 # these files.
-foi = [
+foi: "List[Tuple[str, bytes]]" = [
     ('spath', b'00changelog.i'),
     ('spath', b'phaseroots'),  # ! phase can change content at the same size
     ('spath', b'obsstore'),
--- a/mercurial/hgweb/hgwebdir_mod.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/hgweb/hgwebdir_mod.py	Fri Sep 06 02:12:19 2024 +0200
@@ -120,7 +120,6 @@
     seenrepos = set()
     seendirs = set()
     for name, path in repos:
-
         if not name.startswith(subdir):
             continue
         name = name[len(subdir) :]
--- a/mercurial/hgweb/request.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/hgweb/request.py	Fri Sep 06 02:12:19 2024 +0200
@@ -9,7 +9,15 @@
 
 # import wsgiref.validate
 
+import typing
+
 from ..thirdparty import attr
+
+# Force pytype to use the non-vendored package
+if typing.TYPE_CHECKING:
+    # noinspection PyPackageRequirements
+    import attr
+
 from .. import (
     error,
     pycompat,
--- a/mercurial/hgweb/server.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/hgweb/server.py	Fri Sep 06 02:12:19 2024 +0200
@@ -66,7 +66,6 @@
 
 
 class _httprequesthandler(httpservermod.basehttprequesthandler):
-
     url_scheme = b'http'
 
     @staticmethod
@@ -358,7 +357,6 @@
 
 
 class MercurialHTTPServer(_mixin, httpservermod.httpserver, object):
-
     # SO_REUSEADDR has broken semantics on windows
     if pycompat.iswindows:
         allow_reuse_address = 0
@@ -396,7 +394,6 @@
 
 
 def create_server(ui, app):
-
     if ui.config(b'web', b'certificate'):
         handler = _httprequesthandlerssl
     else:
--- a/mercurial/hgweb/webcommands.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/hgweb/webcommands.py	Fri Sep 06 02:12:19 2024 +0200
@@ -601,7 +601,6 @@
 
     def dirlist(context):
         for d in sorted(dirs):
-
             emptydirs = []
             h = dirs[d]
             while isinstance(h, dict) and len(h) == 1:
@@ -1427,7 +1426,7 @@
         return tree
 
     def jsdata(context):
-        for (id, type, ctx, vtx, edges) in fulltree():
+        for id, type, ctx, vtx, edges in fulltree():
             yield {
                 b'node': pycompat.bytestr(ctx),
                 b'graphnode': webutil.getgraphnode(web.repo, ctx),
--- a/mercurial/httpconnection.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/httpconnection.py	Fri Sep 06 02:12:19 2024 +0200
@@ -25,6 +25,7 @@
 urlerr = util.urlerr
 urlreq = util.urlreq
 
+
 # moved here from url.py to avoid a cycle
 class httpsendfile:
     """This is a wrapper around the objects returned by python's "open".
--- a/mercurial/httppeer.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/httppeer.py	Fri Sep 06 02:12:19 2024 +0200
@@ -245,8 +245,9 @@
     Returns the response object.
     """
     dbg = ui.debug
+    line = b'devel-peer-request: %s\n'
+
     if ui.debugflag and ui.configbool(b'devel', b'debug.peer-request'):
-        line = b'devel-peer-request: %s\n'
         dbg(
             line
             % b'%s %s'
@@ -491,6 +492,9 @@
             # boolean capability. They only support headerless/uncompressed
             # bundles.
             types = [b""]
+
+        type = b""
+
         for x in types:
             if x in bundle2.bundletypes:
                 type = x
@@ -520,10 +524,9 @@
             os.unlink(tempname)
 
     def _calltwowaystream(self, cmd, fp, **args):
-        filename = None
+        # dump bundle to disk
+        fd, filename = pycompat.mkstemp(prefix=b"hg-bundle-", suffix=b".hg")
         try:
-            # dump bundle to disk
-            fd, filename = pycompat.mkstemp(prefix=b"hg-bundle-", suffix=b".hg")
             with os.fdopen(fd, "wb") as fh:
                 d = fp.read(4096)
                 while d:
@@ -534,8 +537,7 @@
                 headers = {'Content-Type': 'application/mercurial-0.1'}
                 return self._callstream(cmd, data=fp_, headers=headers, **args)
         finally:
-            if filename is not None:
-                os.unlink(filename)
+            os.unlink(filename)
 
     def _callcompressable(self, cmd, **args):
         return self._callstream(cmd, _compressible=True, **args)
--- a/mercurial/i18n.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/i18n.py	Fri Sep 06 02:12:19 2024 +0200
@@ -12,6 +12,7 @@
 import sys
 
 from typing import (
+    Dict,
     List,
 )
 
@@ -61,7 +62,9 @@
     _ugettext = t.gettext
 
 
-_msgcache = {}  # encoding: {message: translation}
+_msgcache: Dict[
+    bytes, Dict[bytes, bytes]
+] = {}  # encoding: {message: translation}
 
 
 def gettext(message: bytes) -> bytes:
@@ -119,6 +122,5 @@
     def _(message: bytes) -> bytes:
         return message
 
-
 else:
     _ = gettext
--- a/mercurial/interfaces/repository.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/interfaces/repository.py	Fri Sep 06 02:12:19 2024 +0200
@@ -1021,6 +1021,12 @@
 
     __bool__ = __nonzero__
 
+    def set(path, node, flags):
+        """Define the node value and flags for a path in the manifest.
+
+        Equivalent to __setitem__ followed by setflag, but can be more efficient.
+        """
+
     def __setitem__(path, node):
         """Define the node value for a path in the manifest.
 
@@ -1169,12 +1175,70 @@
     def readdelta(shallow=False):
         """Obtain the manifest data structure representing changes from parent.
 
-        This manifest is compared to its 1st parent. A new manifest representing
-        those differences is constructed.
+        This manifest is compared to its 1st parent. A new manifest
+        representing those differences is constructed.
+
+        If `shallow` is True, this will read the delta for this directory,
+        without recursively reading subdirectory manifests. Instead, any
+        subdirectory entry will be reported as it appears in the manifest, i.e.
+        the subdirectory will be reported among files and distinguished only by
+        its 't' flag. This only apply if the underlying manifest support it.
 
         The returned object conforms to the ``imanifestdict`` interface.
         """
 
+    def read_any_fast_delta(valid_bases=None, *, shallow=False):
+        """read some manifest information as fast if possible
+
+        This might return a "delta", a manifest object containing only file
+        changed compared to another revisions. The `valid_bases` argument
+        control the set of revision that might be used as a base.
+
+        If no delta can be retrieved quickly, a full read of the manifest will
+        be performed instead.
+
+        The function return a tuple with two elements. The first one is the
+        delta base used (or None if we did a full read), the second one is the
+        manifest information.
+
+        If `shallow` is True, this will read the delta for this directory,
+        without recursively reading subdirectory manifests. Instead, any
+        subdirectory entry will be reported as it appears in the manifest, i.e.
+        the subdirectory will be reported among files and distinguished only by
+        its 't' flag. This only apply if the underlying manifest support it.
+
+        The returned object conforms to the ``imanifestdict`` interface.
+        """
+
+    def read_delta_parents(*, shallow=False, exact=True):
+        """return a diff from this revision against both parents.
+
+        If `exact` is False, this might return a superset of the diff, containing
+        files that are actually present as is in one of the parents.
+
+        If `shallow` is True, this will read the delta for this directory,
+        without recursively reading subdirectory manifests. Instead, any
+        subdirectory entry will be reported as it appears in the manifest, i.e.
+        the subdirectory will be reported among files and distinguished only by
+        its 't' flag. This only apply if the underlying manifest support it.
+
+        The returned object conforms to the ``imanifestdict`` interface."""
+
+    def read_delta_new_entries(*, shallow=False):
+        """Return a manifest containing just the entries that might be new to
+        the repository.
+
+        This is often equivalent to a diff against both parents, but without
+        garantee. For performance reason, It might contains more files in some cases.
+
+        If `shallow` is True, this will read the delta for this directory,
+        without recursively reading subdirectory manifests. Instead, any
+        subdirectory entry will be reported as it appears in the manifest, i.e.
+        the subdirectory will be reported among files and distinguished only by
+        its 't' flag. This only apply if the underlying manifest support it.
+
+        The returned object conforms to the ``imanifestdict`` interface."""
+
     def readfast(shallow=False):
         """Calls either ``read()`` or ``readdelta()``.
 
@@ -1428,6 +1492,10 @@
         """nodeconstants used by the current repository."""
     )
 
+    narrowed = interfaceutil.Attribute(
+        """True, is the manifest is narrowed by a matcher"""
+    )
+
     def __getitem__(node):
         """Obtain a manifest instance for a given binary node.
 
--- a/mercurial/keepalive.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/keepalive.py	Fri Sep 06 02:12:19 2024 +0200
@@ -380,22 +380,9 @@
 
 class HTTPResponse(httplib.HTTPResponse):
     # we need to subclass HTTPResponse in order to
-    # 1) add readline(), readlines(), and readinto() methods
-    # 2) add close_connection() methods
-    # 3) add info() and geturl() methods
-
-    # in order to add readline(), read must be modified to deal with a
-    # buffer.  example: readline must read a buffer and then spit back
-    # one line at a time.  The only real alternative is to read one
-    # BYTE at a time (ick).  Once something has been read, it can't be
-    # put back (ok, maybe it can, but that's even uglier than this),
-    # so if you THEN do a normal read, you must first take stuff from
-    # the buffer.
-
-    # the read method wraps the original to accommodate buffering,
-    # although read() never adds to the buffer.
-    # Both readline and readlines have been stolen with almost no
-    # modification from socket.py
+    # 1) add close_connection() method
+    # 2) add geturl() method
+    # 3) add accounting for read(), readlines() and readinto()
 
     def __init__(self, sock, debuglevel=0, strict=0, method=None):
         httplib.HTTPResponse.__init__(
@@ -411,9 +398,6 @@
         self._url = None  # (same)
         self._connection = None  # (same)
 
-    _raw_read = httplib.HTTPResponse.read
-    _raw_readinto = getattr(httplib.HTTPResponse, 'readinto', None)
-
     # Python 2.7 has a single close() which closes the socket handle.
     # This method was effectively renamed to _close_conn() in Python 3. But
     # there is also a close(). _close_conn() is called by methods like
@@ -435,183 +419,34 @@
         self._handler._remove_connection(self._host, self._connection, close=1)
         self.close()
 
-    def info(self):
-        return self.headers
-
     def geturl(self):
         return self._url
 
     def read(self, amt=None):
-        # the _rbuf test is only in this first if for speed.  It's not
-        # logically necessary
-        if self._rbuf and amt is not None:
-            L = len(self._rbuf)
-            if amt > L:
-                amt -= L
-            else:
-                s = self._rbuf[:amt]
-                self._rbuf = self._rbuf[amt:]
-                return s
-        # Careful! http.client.HTTPResponse.read() on Python 3 is
-        # implemented using readinto(), which can duplicate self._rbuf
-        # if it's not empty.
-        s = self._rbuf
-        self._rbuf = b''
-        data = self._raw_read(amt)
-
+        data = super().read(amt)
         self.receivedbytescount += len(data)
-        try:
+        if self._connection is not None:
             self._connection.receivedbytescount += len(data)
-        except AttributeError:
-            pass
-        try:
+        if self._handler is not None:
             self._handler.parent.receivedbytescount += len(data)
-        except AttributeError:
-            pass
-
-        s += data
-        return s
-
-    # stolen from Python SVN #68532 to fix issue1088
-    def _read_chunked(self, amt):
-        chunk_left = self.chunk_left
-        parts = []
-
-        while True:
-            if chunk_left is None:
-                line = self.fp.readline()
-                i = line.find(b';')
-                if i >= 0:
-                    line = line[:i]  # strip chunk-extensions
-                try:
-                    chunk_left = int(line, 16)
-                except ValueError:
-                    # close the connection as protocol synchronization is
-                    # probably lost
-                    self.close()
-                    raise httplib.IncompleteRead(b''.join(parts))
-                if chunk_left == 0:
-                    break
-            if amt is None:
-                parts.append(self._safe_read(chunk_left))
-            elif amt < chunk_left:
-                parts.append(self._safe_read(amt))
-                self.chunk_left = chunk_left - amt
-                return b''.join(parts)
-            elif amt == chunk_left:
-                parts.append(self._safe_read(amt))
-                self._safe_read(2)  # toss the CRLF at the end of the chunk
-                self.chunk_left = None
-                return b''.join(parts)
-            else:
-                parts.append(self._safe_read(chunk_left))
-                amt -= chunk_left
-
-            # we read the whole chunk, get another
-            self._safe_read(2)  # toss the CRLF at the end of the chunk
-            chunk_left = None
-
-        # read and discard trailer up to the CRLF terminator
-        ### note: we shouldn't have any trailers!
-        while True:
-            line = self.fp.readline()
-            if not line:
-                # a vanishingly small number of sites EOF without
-                # sending the trailer
-                break
-            if line == b'\r\n':
-                break
-
-        # we read everything; close the "file"
-        self.close()
-
-        return b''.join(parts)
+        return data
 
     def readline(self):
-        # Fast path for a line is already available in read buffer.
-        i = self._rbuf.find(b'\n')
-        if i >= 0:
-            i += 1
-            line = self._rbuf[:i]
-            self._rbuf = self._rbuf[i:]
-            return line
-
-        # No newline in local buffer. Read until we find one.
-        # readinto read via readinto will already return _rbuf
-        if self._raw_readinto is None:
-            chunks = [self._rbuf]
-        else:
-            chunks = []
-        i = -1
-        readsize = self._rbufsize
-        while True:
-            new = self._raw_read(readsize)
-            if not new:
-                break
-
-            self.receivedbytescount += len(new)
-            self._connection.receivedbytescount += len(new)
-            try:
-                self._handler.parent.receivedbytescount += len(new)
-            except AttributeError:
-                pass
-
-            chunks.append(new)
-            i = new.find(b'\n')
-            if i >= 0:
-                break
-
-        # We either have exhausted the stream or have a newline in chunks[-1].
-
-        # EOF
-        if i == -1:
-            self._rbuf = b''
-            return b''.join(chunks)
-
-        i += 1
-        self._rbuf = chunks[-1][i:]
-        chunks[-1] = chunks[-1][:i]
-        return b''.join(chunks)
-
-    def readlines(self, sizehint=0):
-        total = 0
-        list = []
-        while True:
-            line = self.readline()
-            if not line:
-                break
-            list.append(line)
-            total += len(line)
-            if sizehint and total >= sizehint:
-                break
-        return list
+        data = super().readline()
+        self.receivedbytescount += len(data)
+        if self._connection is not None:
+            self._connection.receivedbytescount += len(data)
+        if self._handler is not None:
+            self._handler.parent.receivedbytescount += len(data)
+        return data
 
     def readinto(self, dest):
-        if self._raw_readinto is None:
-            res = self.read(len(dest))
-            if not res:
-                return 0
-            dest[0 : len(res)] = res
-            return len(res)
-        total = len(dest)
-        have = len(self._rbuf)
-        if have >= total:
-            dest[0:total] = self._rbuf[:total]
-            self._rbuf = self._rbuf[total:]
-            return total
-        mv = memoryview(dest)
-        got = self._raw_readinto(mv[have:total])
-
+        got = super().readinto(dest)
         self.receivedbytescount += got
-        self._connection.receivedbytescount += got
-        try:
-            self._handler.receivedbytescount += got
-        except AttributeError:
-            pass
-
-        dest[0:have] = self._rbuf
-        got += len(self._rbuf)
-        self._rbuf = b''
+        if self._connection is not None:
+            self._connection.receivedbytescount += got
+        if self._handler is not None:
+            self._handler.parent.receivedbytescount += got
         return got
 
 
--- a/mercurial/linelog.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/linelog.py	Fri Sep 06 02:12:19 2024 +0200
@@ -21,8 +21,19 @@
 
 import abc
 import struct
+import typing
+
+from typing import (
+    List,
+)
 
 from .thirdparty import attr
+
+# Force pytype to use the non-vendored package
+if typing.TYPE_CHECKING:
+    # noinspection PyPackageRequirements
+    import attr
+
 from . import pycompat
 
 _llentry = struct.Struct(b'>II')
@@ -45,7 +56,7 @@
 @attr.s
 class annotateresult:
     rev = attr.ib()
-    lines = attr.ib(type=bytearray)
+    lines = attr.ib(type=List[lineinfo])
     _eof = attr.ib()
 
     def __iter__(self):
@@ -53,7 +64,6 @@
 
 
 class _llinstruction:  # pytype: disable=ignored-metaclass
-
     __metaclass__ = abc.ABCMeta
 
     @abc.abstractmethod
@@ -401,7 +411,7 @@
 
     def annotate(self, rev):
         pc = 1
-        lines = []
+        lines: List[lineinfo] = []
         executed = 0
         # Sanity check: if instructions executed exceeds len(program), we
         # hit an infinite loop in the linelog program somehow and we
--- a/mercurial/localrepo.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/localrepo.py	Fri Sep 06 02:12:19 2024 +0200
@@ -523,20 +523,6 @@
     return sharedvfs
 
 
-def _readrequires(vfs, allowmissing):
-    """reads the require file present at root of this vfs
-    and return a set of requirements
-
-    If allowmissing is True, we suppress FileNotFoundError if raised"""
-    # requires file contains a newline-delimited list of
-    # features/capabilities the opener (us) must have in order to use
-    # 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.
-    read = vfs.tryread if allowmissing else vfs.read
-    return set(read(b'requires').splitlines())
-
-
 def makelocalrepository(baseui, path: bytes, intents=None):
     """Create a local repository object.
 
@@ -598,7 +584,7 @@
 
         raise error.RepoError(_(b'repository %s not found') % path)
 
-    requirements = _readrequires(hgvfs, True)
+    requirements = scmutil.readrequires(hgvfs, True)
     shared = (
         requirementsmod.SHARED_REQUIREMENT in requirements
         or requirementsmod.RELATIVE_SHARED_REQUIREMENT in requirements
@@ -626,7 +612,7 @@
         if (
             shared
             and requirementsmod.SHARESAFE_REQUIREMENT
-            not in _readrequires(sharedvfs, True)
+            not in scmutil.readrequires(sharedvfs, True)
         ):
             mismatch_warn = ui.configbool(
                 b'share', b'safe-mismatch.source-not-safe.warn'
@@ -670,9 +656,9 @@
                     hint=hint,
                 )
         else:
-            requirements |= _readrequires(storevfs, False)
+            requirements |= scmutil.readrequires(storevfs, False)
     elif shared:
-        sourcerequires = _readrequires(sharedvfs, False)
+        sourcerequires = scmutil.readrequires(sharedvfs, False)
         if requirementsmod.SHARESAFE_REQUIREMENT in sourcerequires:
             mismatch_config = ui.config(b'share', b'safe-mismatch.source-safe')
             mismatch_warn = ui.configbool(
@@ -1123,9 +1109,12 @@
     if 0 <= chainspan:
         delta_config.max_deltachain_span = chainspan
 
-    mmapindexthreshold = ui.configbytes(b'experimental', b'mmapindexthreshold')
-    if mmapindexthreshold is not None:
-        data_config.mmap_index_threshold = mmapindexthreshold
+    has_populate = util.has_mmap_populate()
+    if ui.configbool(b'storage', b'revlog.mmap.index', has_populate):
+        data_config.mmap_index_threshold = ui.configbytes(
+            b'storage',
+            b'revlog.mmap.index:size-threshold',
+        )
 
     withsparseread = ui.configbool(b'experimental', b'sparse-read')
     srdensitythres = float(
--- a/mercurial/lock.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/lock.py	Fri Sep 06 02:12:19 2024 +0200
@@ -110,7 +110,7 @@
         raiseinterrupt(assertedsigs[0])
 
 
-def trylock(ui, vfs, lockname, timeout, warntimeout, *args, **kwargs):
+def trylock(ui, vfs, lockname, timeout, warntimeout, *args, **kwargs) -> "lock":
     """return an acquired lock or raise an a LockHeld exception
 
     This function is responsible to issue warnings and or debug messages about
@@ -256,7 +256,7 @@
         # wrapper around procutil.getpid() to make testing easier
         return procutil.getpid()
 
-    def lock(self):
+    def lock(self) -> int:
         timeout = self.timeout
         while True:
             try:
@@ -272,7 +272,7 @@
                     errno.ETIMEDOUT, inst.filename, self.desc, inst.locker
                 )
 
-    def _trylock(self):
+    def _trylock(self) -> None:
         if self.held:
             self.held += 1
             return
--- a/mercurial/logcmdutil.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/logcmdutil.py	Fri Sep 06 02:12:19 2024 +0200
@@ -9,6 +9,7 @@
 import itertools
 import os
 import posixpath
+import typing
 
 from typing import (
     Any,
@@ -24,6 +25,11 @@
 
 from .thirdparty import attr
 
+# Force pytype to use the non-vendored package
+if typing.TYPE_CHECKING:
+    # noinspection PyPackageRequirements
+    import attr
+
 from . import (
     dagop,
     diffutil,
@@ -569,6 +575,10 @@
     functions that use changesest_templater.
     """
 
+    _tresources: formatter.templateresources
+    lastheader: Optional[bytes]
+    t: templater.templater
+
     # Arguments before "buffered" used to be positional. Consider not
     # adding/removing arguments before "buffered" to not break callers.
     def __init__(
@@ -659,7 +669,7 @@
                 self.footer = self.t.render(self._parts[b'footer'], props)
 
 
-def templatespec(tmpl, mapfile):
+def templatespec(tmpl, mapfile) -> formatter.templatespec:
     assert not (tmpl and mapfile)
     if mapfile:
         return formatter.mapfile_templatespec(b'changeset', mapfile)
@@ -667,7 +677,7 @@
         return formatter.literal_templatespec(tmpl)
 
 
-def _lookuptemplate(ui, tmpl, style):
+def _lookuptemplate(ui, tmpl, style) -> formatter.templatespec:
     """Find the template matching the given template spec or style
 
     See formatter.lookuptemplate() for details.
--- a/mercurial/manifest.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/manifest.py	Fri Sep 06 02:12:19 2024 +0200
@@ -9,8 +9,24 @@
 import heapq
 import itertools
 import struct
+import typing
 import weakref
 
+from typing import (
+    ByteString,
+    Callable,
+    Collection,
+    Dict,
+    Iterable,
+    Iterator,
+    List,
+    Optional,
+    Set,
+    Tuple,
+    Union,
+    cast,
+)
+
 from .i18n import _
 from .node import (
     bin,
@@ -43,7 +59,7 @@
 FASTDELTA_TEXTDIFF_THRESHOLD = 1000
 
 
-def _parse(nodelen, data):
+def _parse(nodelen, data: bytes):
     # This method does a little bit of excessive-looking
     # precondition checking. This is so that the behavior of this
     # class exactly matches its C counterpart to try and help
@@ -84,21 +100,23 @@
 
 
 class lazymanifestiter:
-    def __init__(self, lm):
+    def __init__(self, lm: '_LazyManifest') -> None:
         self.pos = 0
         self.lm = lm
 
-    def __iter__(self):
+    def __iter__(self) -> 'lazymanifestiter':
         return self
 
-    def next(self):
+    def next(self) -> bytes:
         try:
             data, pos = self.lm._get(self.pos)
         except IndexError:
             raise StopIteration
         if pos == -1:
+            assert isinstance(data, tuple)
             self.pos += 1
             return data[0]
+        assert isinstance(data, bytes)
         self.pos += 1
         zeropos = data.find(b'\x00', pos)
         return data[pos:zeropos]
@@ -107,21 +125,23 @@
 
 
 class lazymanifestiterentries:
-    def __init__(self, lm):
+    def __init__(self, lm: '_LazyManifest') -> None:
         self.lm = lm
         self.pos = 0
 
-    def __iter__(self):
+    def __iter__(self) -> 'lazymanifestiterentries':
         return self
 
-    def next(self):
+    def next(self) -> Tuple[bytes, bytes, bytes]:
         try:
             data, pos = self.lm._get(self.pos)
         except IndexError:
             raise StopIteration
         if pos == -1:
+            assert isinstance(data, tuple)
             self.pos += 1
             return data
+        assert isinstance(data, bytes)
         zeropos = data.find(b'\x00', pos)
         nlpos = data.find(b'\n', pos)
         if zeropos == -1 or nlpos == -1 or nlpos < zeropos:
@@ -143,10 +163,10 @@
     __next__ = next
 
 
-def unhexlify(data, extra, pos, length):
+def unhexlify(data: bytes, extra: int, pos, length: int):
     s = bin(data[pos : pos + length])
     if extra:
-        s += chr(extra & 0xFF)
+        s += bytes([extra & 0xFF])
     return s
 
 
@@ -157,7 +177,7 @@
 _manifestflags = {b'', b'l', b't', b'x'}
 
 
-class _lazymanifest:
+class _LazyManifest:
     """A pure python manifest backed by a byte string.  It is supplimented with
     internal lists as it is modified, until it is compacted back to a pure byte
     string.
@@ -177,12 +197,12 @@
 
     def __init__(
         self,
-        nodelen,
-        data,
+        nodelen: int,
+        data: bytes,
         positions=None,
         extrainfo=None,
         extradata=None,
-        hasremovals=False,
+        hasremovals: bool = False,
     ):
         self._nodelen = nodelen
         if positions is None:
@@ -198,7 +218,7 @@
             self.data = data
             self.hasremovals = hasremovals
 
-    def findlines(self, data):
+    def findlines(self, data: bytes) -> List[int]:
         if not data:
             return []
         pos = data.find(b"\n")
@@ -215,7 +235,9 @@
             pos = data.find(b"\n", pos + 1)
         return positions
 
-    def _get(self, index):
+    def _get(
+        self, index: int
+    ) -> Tuple[Union[bytes, Tuple[bytes, bytes, bytes]], int]:
         # get the position encoded in pos:
         #   positive number is an index in 'data'
         #   negative number is in extrapieces
@@ -224,12 +246,12 @@
             return self.data, pos
         return self.extradata[-pos - 1], -1
 
-    def _getkey(self, pos):
+    def _getkey(self, pos) -> bytes:
         if pos >= 0:
             return self.data[pos : self.data.find(b'\x00', pos + 1)]
         return self.extradata[-pos - 1][0]
 
-    def bsearch(self, key):
+    def bsearch(self, key: bytes) -> int:
         first = 0
         last = len(self.positions) - 1
 
@@ -247,7 +269,7 @@
                     first = midpoint + 1
         return -1
 
-    def bsearch2(self, key):
+    def bsearch2(self, key: bytes) -> Tuple[int, bool]:
         # same as the above, but will always return the position
         # done for performance reasons
         first = 0
@@ -267,10 +289,10 @@
                     first = midpoint + 1
         return (first, False)
 
-    def __contains__(self, key):
+    def __contains__(self, key: bytes) -> bool:
         return self.bsearch(key) != -1
 
-    def __getitem__(self, key):
+    def __getitem__(self, key: bytes) -> Tuple[bytes, bytes]:
         if not isinstance(key, bytes):
             raise TypeError(b"getitem: manifest keys must be a bytes.")
         needle = self.bsearch(key)
@@ -278,7 +300,10 @@
             raise KeyError
         data, pos = self._get(needle)
         if pos == -1:
+            assert isinstance(data, tuple)
             return (data[1], data[2])
+
+        assert isinstance(data, bytes)
         zeropos = data.find(b'\x00', pos)
         nlpos = data.find(b'\n', zeropos)
         assert 0 <= needle <= len(self.positions)
@@ -296,7 +321,7 @@
         hashval = unhexlify(data, self.extrainfo[needle], zeropos + 1, hlen)
         return (hashval, flags)
 
-    def __delitem__(self, key):
+    def __delitem__(self, key: bytes) -> None:
         needle, found = self.bsearch2(key)
         if not found:
             raise KeyError
@@ -309,7 +334,7 @@
             self.data = self.data[:cur] + b'\x00' + self.data[cur + 1 :]
             self.hasremovals = True
 
-    def __setitem__(self, key, value):
+    def __setitem__(self, key: bytes, value: Tuple[bytes, bytes]):
         if not isinstance(key, bytes):
             raise TypeError(b"setitem: manifest keys must be a byte string.")
         if not isinstance(value, tuple) or len(value) != 2:
@@ -344,7 +369,7 @@
                 self.extrainfo[:needle] + [0] + self.extrainfo[needle:]
             )
 
-    def copy(self):
+    def copy(self) -> '_LazyManifest':
         # XXX call _compact like in C?
         return _lazymanifest(
             self._nodelen,
@@ -355,7 +380,7 @@
             self.hasremovals,
         )
 
-    def _compact(self):
+    def _compact(self) -> None:
         # hopefully not called TOO often
         if len(self.extradata) == 0 and not self.hasremovals:
             return
@@ -414,16 +439,23 @@
         self.hasremovals = False
         self.extradata = []
 
-    def _pack(self, d):
+    def _pack(self, d: Tuple[bytes, bytes, bytes]) -> bytes:
         n = d[1]
         assert len(n) in (20, 32)
         return d[0] + b'\x00' + hex(n) + d[2] + b'\n'
 
-    def text(self):
+    def text(self) -> ByteString:
         self._compact()
         return self.data
 
-    def diff(self, m2, clean=False):
+    def diff(
+        self, m2: '_LazyManifest', clean: bool = False
+    ) -> Dict[
+        bytes,
+        Optional[
+            Tuple[Tuple[Optional[bytes], bytes], Tuple[Optional[bytes], bytes]]
+        ],
+    ]:
         '''Finds changes between the current manifest and m2.'''
         # XXX think whether efficiency matters here
         diff = {}
@@ -444,19 +476,19 @@
 
         return diff
 
-    def iterentries(self):
+    def iterentries(self) -> lazymanifestiterentries:
         return lazymanifestiterentries(self)
 
-    def iterkeys(self):
+    def iterkeys(self) -> lazymanifestiter:
         return lazymanifestiter(self)
 
-    def __iter__(self):
+    def __iter__(self) -> lazymanifestiter:
         return lazymanifestiter(self)
 
-    def __len__(self):
+    def __len__(self) -> int:
         return len(self.positions)
 
-    def filtercopy(self, filterfn):
+    def filtercopy(self, filterfn: Callable[[bytes], bool]) -> '_LazyManifest':
         # XXX should be optimized
         c = _lazymanifest(self._nodelen, b'')
         for f, n, fl in self.iterentries():
@@ -468,52 +500,54 @@
 try:
     _lazymanifest = parsers.lazymanifest
 except AttributeError:
-    pass
-
-
-@interfaceutil.implementer(repository.imanifestdict)
-class manifestdict:
-    def __init__(self, nodelen, data=b''):
+    _lazymanifest = _LazyManifest
+
+
+class ManifestDict:
+    def __init__(self, nodelen: int, data: ByteString = b''):
         self._nodelen = nodelen
         self._lm = _lazymanifest(nodelen, data)
 
-    def __getitem__(self, key):
+    def __getitem__(self, key: bytes) -> bytes:
         return self._lm[key][0]
 
-    def find(self, key):
+    def find(self, key: bytes) -> Tuple[bytes, bytes]:
         return self._lm[key]
 
-    def __len__(self):
+    def __len__(self) -> int:
         return len(self._lm)
 
-    def __nonzero__(self):
+    def __nonzero__(self) -> bool:
         # nonzero is covered by the __len__ function, but implementing it here
         # makes it easier for extensions to override.
         return len(self._lm) != 0
 
     __bool__ = __nonzero__
 
-    def __setitem__(self, key, node):
+    def set(self, key: bytes, node: bytes, flags: bytes) -> None:
+        self._lm[key] = node, flags
+
+    def __setitem__(self, key: bytes, node: bytes) -> None:
         self._lm[key] = node, self.flags(key)
 
-    def __contains__(self, key):
+    def __contains__(self, key: bytes) -> bool:
         if key is None:
             return False
         return key in self._lm
 
-    def __delitem__(self, key):
+    def __delitem__(self, key: bytes) -> None:
         del self._lm[key]
 
-    def __iter__(self):
+    def __iter__(self) -> Iterator[bytes]:
         return self._lm.__iter__()
 
-    def iterkeys(self):
+    def iterkeys(self) -> Iterator[bytes]:
         return self._lm.iterkeys()
 
-    def keys(self):
+    def keys(self) -> List[bytes]:
         return list(self.iterkeys())
 
-    def filesnotin(self, m2, match=None):
+    def filesnotin(self, m2, match=None) -> Set[bytes]:
         '''Set of files in this manifest that are not in the other'''
         if match is not None:
             match = matchmod.badmatch(match, lambda path, msg: None)
@@ -522,16 +556,16 @@
         return {f for f in self if f not in m2}
 
     @propertycache
-    def _dirs(self):
+    def _dirs(self) -> pathutil.dirs:
         return pathutil.dirs(self)
 
-    def dirs(self):
+    def dirs(self) -> pathutil.dirs:
         return self._dirs
 
-    def hasdir(self, dir):
+    def hasdir(self, dir: bytes) -> bool:
         return dir in self._dirs
 
-    def _filesfastpath(self, match):
+    def _filesfastpath(self, match: matchmod.basematcher) -> bool:
         """Checks whether we can correctly and quickly iterate over matcher
         files instead of over manifest files."""
         files = match.files()
@@ -540,7 +574,7 @@
             or (match.prefix() and all(fn in self for fn in files))
         )
 
-    def walk(self, match):
+    def walk(self, match: matchmod.basematcher) -> Iterator[bytes]:
         """Generates matching file names.
 
         Equivalent to manifest.matches(match).iterkeys(), but without creating
@@ -577,7 +611,7 @@
             if not self.hasdir(fn):
                 match.bad(fn, None)
 
-    def _matches(self, match):
+    def _matches(self, match: matchmod.basematcher) -> 'ManifestDict':
         '''generate a new manifest filtered by the match argument'''
         if match.always():
             return self.copy()
@@ -594,7 +628,17 @@
         m._lm = self._lm.filtercopy(match)
         return m
 
-    def diff(self, m2, match=None, clean=False):
+    def diff(
+        self,
+        m2: 'ManifestDict',
+        match: Optional[matchmod.basematcher] = None,
+        clean: bool = False,
+    ) -> Dict[
+        bytes,
+        Optional[
+            Tuple[Tuple[Optional[bytes], bytes], Tuple[Optional[bytes], bytes]]
+        ],
+    ]:
         """Finds changes between the current manifest and m2.
 
         Args:
@@ -615,42 +659,44 @@
             return m1.diff(m2, clean=clean)
         return self._lm.diff(m2._lm, clean)
 
-    def setflag(self, key, flag):
+    def setflag(self, key: bytes, flag: bytes) -> None:
         if flag not in _manifestflags:
             raise TypeError(b"Invalid manifest flag set.")
         self._lm[key] = self[key], flag
 
-    def get(self, key, default=None):
+    def get(self, key: bytes, default=None) -> Optional[bytes]:
         try:
             return self._lm[key][0]
         except KeyError:
             return default
 
-    def flags(self, key):
+    def flags(self, key: bytes) -> bytes:
         try:
             return self._lm[key][1]
         except KeyError:
             return b''
 
-    def copy(self):
+    def copy(self) -> 'ManifestDict':
         c = manifestdict(self._nodelen)
         c._lm = self._lm.copy()
         return c
 
-    def items(self):
+    def items(self) -> Iterator[Tuple[bytes, bytes]]:
         return (x[:2] for x in self._lm.iterentries())
 
-    def iteritems(self):
+    def iteritems(self) -> Iterator[Tuple[bytes, bytes]]:
         return (x[:2] for x in self._lm.iterentries())
 
-    def iterentries(self):
+    def iterentries(self) -> Iterator[Tuple[bytes, bytes, bytes]]:
         return self._lm.iterentries()
 
-    def text(self):
+    def text(self) -> ByteString:
         # most likely uses native version
         return self._lm.text()
 
-    def fastdelta(self, base, changes):
+    def fastdelta(
+        self, base: ByteString, changes: Iterable[Tuple[bytes, bool]]
+    ) -> Tuple[ByteString, ByteString]:
         """Given a base manifest text as a bytearray and a list of changes
         relative to that text, compute a delta that can be used by revlog.
         """
@@ -686,13 +732,13 @@
                         dline.append(l)
                 else:
                     if dstart is not None:
-                        delta.append([dstart, dend, b"".join(dline)])
+                        delta.append((dstart, dend, b"".join(dline)))
                     dstart = start
                     dend = end
                     dline = [l]
 
             if dstart is not None:
-                delta.append([dstart, dend, b"".join(dline)])
+                delta.append((dstart, dend, b"".join(dline)))
             # apply the delta to the base, and get a delta for addrevision
             deltatext, arraytext = _addlistdelta(base, delta)
         else:
@@ -706,17 +752,23 @@
         return arraytext, deltatext
 
 
-def _msearch(m, s, lo=0, hi=None):
+manifestdict = interfaceutil.implementer(repository.imanifestdict)(ManifestDict)
+
+if typing.TYPE_CHECKING:
+    manifestdict = ManifestDict
+
+
+def _msearch(
+    m: ByteString, s: bytes, lo: int = 0, hi: Optional[int] = None
+) -> Tuple[int, int]:
     """return a tuple (start, end) that says where to find s within m.
 
     If the string is found m[start:end] are the line containing
     that string.  If start == end the string was not found and
     they indicate the proper sorted insertion point.
-
-    m should be a buffer, a memoryview or a byte string.
-    s is a byte string"""
-
-    def advance(i, c):
+    """
+
+    def advance(i: int, c: bytes):
         while i < lenm and m[i : i + 1] != c:
             i += 1
         return i
@@ -749,7 +801,7 @@
         return (lo, lo)
 
 
-def _checkforbidden(l):
+def _checkforbidden(l: Iterable[bytes]) -> None:
     """Check filenames for illegal characters."""
     for f in l:
         if b'\n' in f or b'\r' in f:
@@ -761,7 +813,10 @@
 
 # apply the changes collected during the bisect loop to our addlist
 # return a delta suitable for addrevision
-def _addlistdelta(addlist, x):
+def _addlistdelta(
+    addlist: ByteString,
+    x: Iterable[Tuple[int, int, bytes]],
+) -> Tuple[bytes, ByteString]:
     # for large addlist arrays, building a new array is cheaper
     # than repeatedly modifying the existing one
     currentposition = 0
@@ -783,7 +838,7 @@
     return deltatext, newaddlist
 
 
-def _splittopdir(f):
+def _splittopdir(f: bytes) -> Tuple[bytes, bytes]:
     if b'/' in f:
         dir, subpath = f.split(b'/', 1)
         return dir + b'/', subpath
@@ -794,9 +849,14 @@
 _noop = lambda s: None
 
 
-@interfaceutil.implementer(repository.imanifestdict)
-class treemanifest:
-    def __init__(self, nodeconstants, dir=b'', text=b''):
+class TreeManifest:
+    _dir: bytes
+    _dirs: Dict[bytes, 'TreeManifest']
+    _dirty: bool
+    _files: Dict[bytes, bytes]
+    _flags: Dict[bytes, bytes]
+
+    def __init__(self, nodeconstants, dir: bytes = b'', text: bytes = b''):
         self._dir = dir
         self.nodeconstants = nodeconstants
         self._node = self.nodeconstants.nullid
@@ -805,7 +865,10 @@
         self._copyfunc = _noop
         self._dirty = False
         self._dirs = {}
-        self._lazydirs = {}
+        self._lazydirs: Dict[
+            bytes,
+            Tuple[bytes, Callable[[bytes, bytes], 'TreeManifest'], bool],
+        ] = {}
         # Using _lazymanifest here is a little slower than plain old dicts
         self._files = {}
         self._flags = {}
@@ -819,10 +882,10 @@
             self.parse(text, readsubtree)
             self._dirty = True  # Mark flat manifest dirty after parsing
 
-    def _subpath(self, path):
+    def _subpath(self, path: bytes) -> bytes:
         return self._dir + path
 
-    def _loadalllazy(self):
+    def _loadalllazy(self) -> None:
         selfdirs = self._dirs
         subpath = self._subpath
         for d, (node, readsubtree, docopy) in self._lazydirs.items():
@@ -830,11 +893,11 @@
                 selfdirs[d] = readsubtree(subpath(d), node).copy()
             else:
                 selfdirs[d] = readsubtree(subpath(d), node)
-        self._lazydirs = {}
-
-    def _loadlazy(self, d):
+        self._lazydirs.clear()
+
+    def _loadlazy(self, d: bytes) -> None:
         v = self._lazydirs.get(d)
-        if v:
+        if v is not None:
             node, readsubtree, docopy = v
             if docopy:
                 self._dirs[d] = readsubtree(self._subpath(d), node).copy()
@@ -842,19 +905,23 @@
                 self._dirs[d] = readsubtree(self._subpath(d), node)
             del self._lazydirs[d]
 
-    def _loadchildrensetlazy(self, visit):
+    def _loadchildrensetlazy(
+        self, visit: Union[Set[bytes], bytes]
+    ) -> Optional[Set[bytes]]:
         if not visit:
             return None
         if visit == b'all' or visit == b'this':
             self._loadalllazy()
             return None
 
+        visit = cast(Set[bytes], visit)
+
         loadlazy = self._loadlazy
         for k in visit:
             loadlazy(k + b'/')
         return visit
 
-    def _loaddifflazy(self, t1, t2):
+    def _loaddifflazy(self, t1: 'TreeManifest', t2: 'TreeManifest'):
         """load items in t1 and t2 if they're needed for diffing.
 
         The criteria currently is:
@@ -866,7 +933,7 @@
         toloadlazy = []
         for d, v1 in t1._lazydirs.items():
             v2 = t2._lazydirs.get(d)
-            if not v2 or v2[0] != v1[0]:
+            if v2 is None or v2[0] != v1[0]:
                 toloadlazy.append(d)
         for d, v1 in t2._lazydirs.items():
             if d not in t1._lazydirs:
@@ -876,7 +943,7 @@
             t1._loadlazy(d)
             t2._loadlazy(d)
 
-    def __len__(self):
+    def __len__(self) -> int:
         self._load()
         size = len(self._files)
         self._loadalllazy()
@@ -884,13 +951,13 @@
             size += m.__len__()
         return size
 
-    def __nonzero__(self):
-        # Faster than "__len() != 0" since it avoids loading sub-manifests
+    def __nonzero__(self) -> bool:
+        # Faster than "__len__() != 0" since it avoids loading sub-manifests
         return not self._isempty()
 
     __bool__ = __nonzero__
 
-    def _isempty(self):
+    def _isempty(self) -> bool:
         self._load()  # for consistency; already loaded by all callers
         # See if we can skip loading everything.
         if self._files or (
@@ -901,7 +968,7 @@
         return not self._dirs or all(m._isempty() for m in self._dirs.values())
 
     @encoding.strmethod
-    def __repr__(self):
+    def __repr__(self) -> bytes:
         return (
             b'<treemanifest dir=%s, node=%s, loaded=%r, dirty=%r at 0x%x>'
             % (
@@ -913,23 +980,25 @@
             )
         )
 
-    def dir(self):
+    def dir(self) -> bytes:
         """The directory that this tree manifest represents, including a
         trailing '/'. Empty string for the repo root directory."""
         return self._dir
 
-    def node(self):
+    def node(self) -> bytes:
         """This node of this instance. nullid for unsaved instances. Should
         be updated when the instance is read or written from a revlog.
         """
         assert not self._dirty
         return self._node
 
-    def setnode(self, node):
+    def setnode(self, node: bytes) -> None:
         self._node = node
         self._dirty = False
 
-    def iterentries(self):
+    def iterentries(
+        self,
+    ) -> Iterator[Tuple[bytes, Union[bytes, 'TreeManifest'], bytes]]:
         self._load()
         self._loadalllazy()
         for p, n in sorted(
@@ -941,7 +1010,7 @@
                 for x in n.iterentries():
                     yield x
 
-    def items(self):
+    def items(self) -> Iterator[Tuple[bytes, Union[bytes, 'TreeManifest']]]:
         self._load()
         self._loadalllazy()
         for p, n in sorted(
@@ -955,7 +1024,7 @@
 
     iteritems = items
 
-    def iterkeys(self):
+    def iterkeys(self) -> Iterator[bytes]:
         self._load()
         self._loadalllazy()
         for p in sorted(itertools.chain(self._dirs, self._files)):
@@ -965,13 +1034,13 @@
                 for f in self._dirs[p]:
                     yield f
 
-    def keys(self):
+    def keys(self) -> List[bytes]:
         return list(self.iterkeys())
 
-    def __iter__(self):
+    def __iter__(self) -> Iterator[bytes]:
         return self.iterkeys()
 
-    def __contains__(self, f):
+    def __contains__(self, f: bytes) -> bool:
         if f is None:
             return False
         self._load()
@@ -986,7 +1055,7 @@
         else:
             return f in self._files
 
-    def get(self, f, default=None):
+    def get(self, f: bytes, default: Optional[bytes] = None) -> Optional[bytes]:
         self._load()
         dir, subpath = _splittopdir(f)
         if dir:
@@ -998,7 +1067,7 @@
         else:
             return self._files.get(f, default)
 
-    def __getitem__(self, f):
+    def __getitem__(self, f: bytes) -> bytes:
         self._load()
         dir, subpath = _splittopdir(f)
         if dir:
@@ -1008,7 +1077,7 @@
         else:
             return self._files[f]
 
-    def flags(self, f):
+    def flags(self, f: bytes) -> bytes:
         self._load()
         dir, subpath = _splittopdir(f)
         if dir:
@@ -1022,7 +1091,7 @@
                 return b''
             return self._flags.get(f, b'')
 
-    def find(self, f):
+    def find(self, f: bytes) -> Tuple[bytes, bytes]:
         self._load()
         dir, subpath = _splittopdir(f)
         if dir:
@@ -1032,7 +1101,7 @@
         else:
             return self._files[f], self._flags.get(f, b'')
 
-    def __delitem__(self, f):
+    def __delitem__(self, f: bytes) -> None:
         self._load()
         dir, subpath = _splittopdir(f)
         if dir:
@@ -1048,7 +1117,27 @@
                 del self._flags[f]
         self._dirty = True
 
-    def __setitem__(self, f, n):
+    def set(self, f: bytes, node: bytes, flags: bytes) -> None:
+        """Set both the node and the flags for path f."""
+        assert node is not None
+        if flags not in _manifestflags:
+            raise TypeError(b"Invalid manifest flag set.")
+        self._load()
+        dir, subpath = _splittopdir(f)
+        if dir:
+            self._loadlazy(dir)
+            if dir not in self._dirs:
+                self._dirs[dir] = treemanifest(
+                    self.nodeconstants, self._subpath(dir)
+                )
+            self._dirs[dir].set(subpath, node, flags)
+        else:
+            assert len(node) in (20, 32)
+            self._files[f] = node
+            self._flags[f] = flags
+        self._dirty = True
+
+    def __setitem__(self, f: bytes, n: bytes) -> None:
         assert n is not None
         self._load()
         dir, subpath = _splittopdir(f)
@@ -1067,7 +1156,7 @@
             self._files[f] = n
         self._dirty = True
 
-    def _load(self):
+    def _load(self) -> None:
         if self._loadfunc is not _noop:
             lf, self._loadfunc = self._loadfunc, _noop
             lf(self)
@@ -1075,7 +1164,7 @@
             cf, self._copyfunc = self._copyfunc, _noop
             cf(self)
 
-    def setflag(self, f, flags):
+    def setflag(self, f: bytes, flags: bytes) -> None:
         """Set the flags (symlink, executable) for path f."""
         if flags not in _manifestflags:
             raise TypeError(b"Invalid manifest flag set.")
@@ -1092,7 +1181,7 @@
             self._flags[f] = flags
         self._dirty = True
 
-    def copy(self):
+    def copy(self) -> 'TreeManifest':
         copy = treemanifest(self.nodeconstants, self._dir)
         copy._node = self._node
         copy._dirty = self._dirty
@@ -1117,7 +1206,9 @@
             copy._copyfunc = self._copyfunc
         return copy
 
-    def filesnotin(self, m2, match=None):
+    def filesnotin(
+        self, m2: 'TreeManifest', match: Optional[matchmod.basematcher] = None
+    ) -> Set[bytes]:
         '''Set of files in this manifest that are not in the other'''
         if match and not match.always():
             m1 = self._matches(match)
@@ -1147,13 +1238,13 @@
         return files
 
     @propertycache
-    def _alldirs(self):
+    def _alldirs(self) -> pathutil.dirs:
         return pathutil.dirs(self)
 
-    def dirs(self):
+    def dirs(self) -> pathutil.dirs:
         return self._alldirs
 
-    def hasdir(self, dir):
+    def hasdir(self, dir: bytes) -> bool:
         self._load()
         topdir, subdir = _splittopdir(dir)
         if topdir:
@@ -1164,7 +1255,7 @@
         dirslash = dir + b'/'
         return dirslash in self._dirs or dirslash in self._lazydirs
 
-    def walk(self, match):
+    def walk(self, match: matchmod.basematcher) -> Iterator[bytes]:
         """Generates matching file names.
 
         It also reports nonexistent files by marking them bad with match.bad().
@@ -1190,7 +1281,7 @@
             if not self.hasdir(fn):
                 match.bad(fn, None)
 
-    def _walk(self, match):
+    def _walk(self, match: matchmod.basematcher) -> Iterator[bytes]:
         '''Recursively generates matching file names for walk().'''
         visit = match.visitchildrenset(self._dir[:-1])
         if not visit:
@@ -1209,13 +1300,13 @@
                     for f in self._dirs[p]._walk(match):
                         yield f
 
-    def _matches(self, match):
+    def _matches(self, match: matchmod.basematcher) -> 'TreeManifest':
         """recursively generate a new manifest filtered by the match argument."""
         if match.always():
             return self.copy()
         return self._matches_inner(match)
 
-    def _matches_inner(self, match):
+    def _matches_inner(self, match: matchmod.basematcher) -> 'TreeManifest':
         if match.always():
             return self.copy()
 
@@ -1256,10 +1347,22 @@
             ret._dirty = True
         return ret
 
-    def fastdelta(self, base, changes):
+    def fastdelta(
+        self, base: ByteString, changes: Iterable[Tuple[bytes, bool]]
+    ) -> ByteString:
         raise FastdeltaUnavailable()
 
-    def diff(self, m2, match=None, clean=False):
+    def diff(
+        self,
+        m2: 'TreeManifest',
+        match: Optional[matchmod.basematcher] = None,
+        clean: bool = False,
+    ) -> Dict[
+        bytes,
+        Optional[
+            Tuple[Tuple[Optional[bytes], bytes], Tuple[Optional[bytes], bytes]]
+        ],
+    ]:
         """Finds changes between the current manifest and m2.
 
         Args:
@@ -1320,10 +1423,14 @@
             _iterativediff(t1, t2, stackls)
         return result
 
-    def unmodifiedsince(self, m2):
+    def unmodifiedsince(self, m2: 'TreeManifest') -> bool:
         return not self._dirty and not m2._dirty and self._node == m2._node
 
-    def parse(self, text, readsubtree):
+    def parse(
+        self,
+        text: bytes,
+        readsubtree: Callable[[bytes, bytes], 'TreeManifest'],
+    ) -> None:
         selflazy = self._lazydirs
         for f, n, fl in _parse(self._nodelen, text):
             if fl == b't':
@@ -1346,12 +1453,12 @@
                 if fl:
                     self._flags[f] = fl
 
-    def text(self):
+    def text(self) -> ByteString:
         """Get the full data of this manifest as a bytestring."""
         self._load()
         return _text(self.iterentries())
 
-    def dirtext(self):
+    def dirtext(self) -> ByteString:
         """Get the full data of this directory as a bytestring. Make sure that
         any submanifests have been written first, so their nodeids are correct.
         """
@@ -1362,14 +1469,32 @@
         files = [(f, self._files[f], flags(f)) for f in self._files]
         return _text(sorted(dirs + files + lazydirs))
 
-    def read(self, gettext, readsubtree):
+    def read(
+        self,
+        gettext: Callable[[], ByteString],
+        readsubtree: Callable[[bytes, bytes], 'TreeManifest'],
+    ) -> None:
         def _load_for_read(s):
             s.parse(gettext(), readsubtree)
             s._dirty = False
 
         self._loadfunc = _load_for_read
 
-    def writesubtrees(self, m1, m2, writesubtree, match):
+    def writesubtrees(
+        self,
+        m1: 'TreeManifest',
+        m2: 'TreeManifest',
+        writesubtree: Callable[
+            [
+                Callable[['TreeManifest'], None],
+                bytes,
+                bytes,
+                matchmod.basematcher,
+            ],
+            None,
+        ],
+        match: matchmod.basematcher,
+    ) -> None:
         self._load()  # for consistency; should never have any effect here
         m1._load()
         m2._load()
@@ -1379,7 +1504,9 @@
             ld = m._lazydirs.get(d)
             if ld:
                 return ld[0]
-            return m._dirs.get(d, emptytree)._node
+            tree = m._dirs.get(d, emptytree)
+            assert tree is not None  # helps pytype
+            return tree._node
 
         # let's skip investigating things that `match` says we do not need.
         visit = match.visitchildrenset(self._dir[:-1])
@@ -1395,7 +1522,9 @@
                 subp1, subp2 = subp2, subp1
             writesubtree(subm, subp1, subp2, match)
 
-    def walksubtrees(self, matcher=None):
+    def walksubtrees(
+        self, matcher: Optional[matchmod.basematcher] = None
+    ) -> Iterator['TreeManifest']:
         """Returns an iterator of the subtrees of this manifest, including this
         manifest itself.
 
@@ -1414,6 +1543,12 @@
                 yield subtree
 
 
+treemanifest = interfaceutil.implementer(repository.imanifestdict)(TreeManifest)
+
+if typing.TYPE_CHECKING:
+    treemanifest = TreeManifest
+
+
 class manifestfulltextcache(util.lrucachedict):
     """File-backed LRU cache for the manifest cache
 
@@ -1548,8 +1683,7 @@
     """Exception raised when fastdelta isn't usable on a manifest."""
 
 
-@interfaceutil.implementer(repository.imanifeststorage)
-class manifestrevlog:
+class ManifestRevlog:
     """A revlog that stores manifest texts. This is responsible for caching the
     full-text manifest contents.
     """
@@ -1578,10 +1712,12 @@
         # stacks of commits, the number can go up, hence the config knob below.
         cachesize = 4
         optiontreemanifest = False
+        persistentnodemap = False
         opts = getattr(opener, 'options', None)
         if opts is not None:
             cachesize = opts.get(b'manifestcachesize', cachesize)
             optiontreemanifest = opts.get(b'treemanifest', False)
+            persistentnodemap = opts.get(b'persistent-nodemap', False)
 
         self._treeondisk = optiontreemanifest or treemanifest
 
@@ -1610,7 +1746,7 @@
             checkambig=not bool(tree),
             mmaplargeindex=True,
             upperboundcomp=MAXCOMPRESSION,
-            persistentnodemap=opener.options.get(b'persistent-nodemap', False),
+            persistentnodemap=persistentnodemap,
         )
 
         self.index = self._revlog.index
@@ -1682,8 +1818,8 @@
         link,
         p1,
         p2,
-        added,
-        removed,
+        added: Iterable[bytes],
+        removed: Iterable[bytes],
         readtree=None,
         match=None,
     ):
@@ -1920,8 +2056,18 @@
         self._revlog.opener = value
 
 
-@interfaceutil.implementer(repository.imanifestlog)
-class manifestlog:
+manifestrevlog = interfaceutil.implementer(repository.imanifeststorage)(
+    ManifestRevlog
+)
+
+if typing.TYPE_CHECKING:
+    manifestrevlog = ManifestRevlog
+
+AnyManifestCtx = Union['ManifestCtx', 'TreeManifestCtx']
+AnyManifestDict = Union[ManifestDict, TreeManifest]
+
+
+class ManifestLog:
     """A collection class representing the collection of manifest snapshots
     referenced by commits in the repository.
 
@@ -1958,7 +2104,13 @@
         """
         return self.get(b'', node)
 
-    def get(self, tree, node, verify=True):
+    @property
+    def narrowed(self):
+        return not (self._narrowmatch is None or self._narrowmatch.always())
+
+    def get(
+        self, tree: bytes, node: bytes, verify: bool = True
+    ) -> AnyManifestCtx:
         """Retrieves the manifest instance for the given node. Throws a
         LookupError if not found.
 
@@ -2008,32 +2160,39 @@
     def getstorage(self, tree):
         return self._rootstore.dirlog(tree)
 
-    def clearcaches(self, clear_persisted_data=False):
+    def clearcaches(self, clear_persisted_data: bool = False) -> None:
         self._dirmancache.clear()
         self._rootstore.clearcaches(clear_persisted_data=clear_persisted_data)
 
-    def rev(self, node):
+    def rev(self, node) -> int:
         return self._rootstore.rev(node)
 
-    def update_caches(self, transaction):
+    def update_caches(self, transaction) -> None:
         return self._rootstore._revlog.update_caches(transaction=transaction)
 
 
-@interfaceutil.implementer(repository.imanifestrevisionwritable)
-class memmanifestctx:
+manifestlog = interfaceutil.implementer(repository.imanifestlog)(ManifestLog)
+
+if typing.TYPE_CHECKING:
+    manifestlog = ManifestLog
+
+
+class MemManifestCtx:
+    _manifestdict: ManifestDict
+
     def __init__(self, manifestlog):
         self._manifestlog = manifestlog
         self._manifestdict = manifestdict(manifestlog.nodeconstants.nodelen)
 
-    def _storage(self):
+    def _storage(self) -> ManifestRevlog:
         return self._manifestlog.getstorage(b'')
 
-    def copy(self):
+    def copy(self) -> 'MemManifestCtx':
         memmf = memmanifestctx(self._manifestlog)
         memmf._manifestdict = self.read().copy()
         return memmf
 
-    def read(self):
+    def read(self) -> 'ManifestDict':
         return self._manifestdict
 
     def write(self, transaction, link, p1, p2, added, removed, match=None):
@@ -2049,12 +2208,21 @@
         )
 
 
-@interfaceutil.implementer(repository.imanifestrevisionstored)
-class manifestctx:
+memmanifestctx = interfaceutil.implementer(
+    repository.imanifestrevisionwritable
+)(MemManifestCtx)
+
+if typing.TYPE_CHECKING:
+    memmanifestctx = MemManifestCtx
+
+
+class ManifestCtx:
     """A class representing a single revision of a manifest, including its
     contents, its parent revs, and its linkrev.
     """
 
+    _data: Optional[ManifestDict]
+
     def __init__(self, manifestlog, node):
         self._manifestlog = manifestlog
         self._data = None
@@ -2068,22 +2236,22 @@
         # rev = store.rev(node)
         # self.linkrev = store.linkrev(rev)
 
-    def _storage(self):
+    def _storage(self) -> 'ManifestRevlog':
         return self._manifestlog.getstorage(b'')
 
-    def node(self):
+    def node(self) -> bytes:
         return self._node
 
-    def copy(self):
+    def copy(self) -> MemManifestCtx:
         memmf = memmanifestctx(self._manifestlog)
         memmf._manifestdict = self.read().copy()
         return memmf
 
     @propertycache
-    def parents(self):
+    def parents(self) -> Tuple[bytes, bytes]:
         return self._storage().parents(self._node)
 
-    def read(self):
+    def read(self) -> 'ManifestDict':
         if self._data is None:
             nc = self._manifestlog.nodeconstants
             if self._node == nc.nullid:
@@ -2099,13 +2267,18 @@
                 self._data = manifestdict(nc.nodelen, text)
         return self._data
 
-    def readfast(self, shallow=False):
+    def readfast(self, shallow: bool = False) -> 'ManifestDict':
         """Calls either readdelta or read, based on which would be less work.
         readdelta is called if the delta is against the p1, and therefore can be
         read quickly.
 
         If `shallow` is True, nothing changes since this is a flat manifest.
         """
+        util.nouideprecwarn(
+            b'"readfast" is deprecated use "read_any_fast_delta" or "read_delta_parents"',
+            b"6.9",
+            stacklevel=2,
+        )
         store = self._storage()
         r = store.rev(self._node)
         deltaparent = store.deltaparent(r)
@@ -2113,38 +2286,121 @@
             return self.readdelta()
         return self.read()
 
-    def readdelta(self, shallow=False):
+    def readdelta(self, shallow: bool = False) -> 'ManifestDict':
         """Returns a manifest containing just the entries that are present
         in this manifest, but not in its p1 manifest. This is efficient to read
         if the revlog delta is already p1.
 
         Changing the value of `shallow` has no effect on flat manifests.
         """
+        util.nouideprecwarn(
+            b'"readfast" is deprecated use "read_any_fast_delta" or "read_delta_new_entries"',
+            b"6.9",
+            stacklevel=2,
+        )
         store = self._storage()
         r = store.rev(self._node)
         d = mdiff.patchtext(store.revdiff(store.deltaparent(r), r))
         return manifestdict(store.nodeconstants.nodelen, d)
 
-    def find(self, key):
+    def read_any_fast_delta(
+        self,
+        valid_bases: Optional[Collection[int]] = None,
+        *,
+        shallow: bool = False,
+    ) -> Tuple[Optional[int], ManifestDict]:
+        """see `imanifestrevisionstored` documentation"""
+        store = self._storage()
+        r = store.rev(self._node)
+        deltaparent = store.deltaparent(r)
+        if valid_bases is None:
+            # make sure the next check is True
+            valid_bases = (deltaparent,)
+        if deltaparent != nullrev and deltaparent in valid_bases:
+            d = mdiff.patchtext(store.revdiff(deltaparent, r))
+            return (
+                deltaparent,
+                manifestdict(store.nodeconstants.nodelen, d),
+            )
+        return (None, self.read())
+
+    def read_delta_parents(
+        self,
+        *,
+        shallow: bool = False,
+        exact: bool = True,
+    ) -> ManifestDict:
+        """see `interface.imanifestrevisionbase` documentations"""
+        store = self._storage()
+        r = store.rev(self._node)
+        deltaparent = store.deltaparent(r)
+        parents = [p for p in store.parentrevs(r) if p is not nullrev]
+        if not exact and deltaparent in parents:
+            d = mdiff.patchtext(store.revdiff(store.deltaparent(r), r))
+            return manifestdict(store.nodeconstants.nodelen, d)
+        elif not exact or len(parents) == 0:
+            return self.read()
+        elif len(parents) == 1:
+            p = parents[0]
+            d = mdiff.patchtext(store.revdiff(p, r))
+            return manifestdict(store.nodeconstants.nodelen, d)
+        else:
+            p1, p2 = parents
+            d1 = mdiff.patchtext(store.revdiff(p1, r))
+            d2 = mdiff.patchtext(store.revdiff(p2, r))
+            d1 = manifestdict(store.nodeconstants.nodelen, d1)
+            d2 = manifestdict(store.nodeconstants.nodelen, d2)
+            md = manifestdict(store.nodeconstants.nodelen)
+            for f, new_node, new_flag in d1.iterentries():
+                if f not in d2:
+                    continue
+                if new_node is not None:
+                    md.set(f, new_node, new_flag)
+            return md
+
+    def read_delta_new_entries(self, *, shallow=False) -> ManifestDict:
+        """see `interface.imanifestrevisionbase` documentations"""
+        # If we are using narrow, returning a delta against an arbitrary
+        # changeset might return file outside the narrowspec. This can create
+        # issue when running validation server side with strict security as
+        # push from low priviledge usage might be seen as adding new revision
+        # for files they cannot touch. So we are strict if narrow is involved.
+        if self._manifestlog.narrowed:
+            return self.read_delta_parents(shallow=shallow, exact=True)
+        store = self._storage()
+        r = store.rev(self._node)
+        d = mdiff.patchtext(store.revdiff(store.deltaparent(r), r))
+        return manifestdict(store.nodeconstants.nodelen, d)
+
+    def find(self, key: bytes) -> Tuple[bytes, bytes]:
         return self.read().find(key)
 
 
-@interfaceutil.implementer(repository.imanifestrevisionwritable)
-class memtreemanifestctx:
+manifestctx = interfaceutil.implementer(repository.imanifestrevisionstored)(
+    ManifestCtx
+)
+
+if typing.TYPE_CHECKING:
+    manifestctx = ManifestCtx
+
+
+class MemTreeManifestCtx:
+    _treemanifest: TreeManifest
+
     def __init__(self, manifestlog, dir=b''):
         self._manifestlog = manifestlog
         self._dir = dir
         self._treemanifest = treemanifest(manifestlog.nodeconstants)
 
-    def _storage(self):
+    def _storage(self) -> ManifestRevlog:
         return self._manifestlog.getstorage(b'')
 
-    def copy(self):
+    def copy(self) -> 'MemTreeManifestCtx':
         memmf = memtreemanifestctx(self._manifestlog, dir=self._dir)
         memmf._treemanifest = self._treemanifest.copy()
         return memmf
 
-    def read(self):
+    def read(self) -> 'TreeManifest':
         return self._treemanifest
 
     def write(self, transaction, link, p1, p2, added, removed, match=None):
@@ -2164,8 +2420,17 @@
         )
 
 
-@interfaceutil.implementer(repository.imanifestrevisionstored)
-class treemanifestctx:
+memtreemanifestctx = interfaceutil.implementer(
+    repository.imanifestrevisionwritable
+)(MemTreeManifestCtx)
+
+if typing.TYPE_CHECKING:
+    memtreemanifestctx = MemTreeManifestCtx
+
+
+class TreeManifestCtx:
+    _data: Optional[TreeManifest]
+
     def __init__(self, manifestlog, dir, node):
         self._manifestlog = manifestlog
         self._dir = dir
@@ -2180,7 +2445,7 @@
         # rev = store.rev(node)
         # self.linkrev = store.linkrev(rev)
 
-    def _storage(self):
+    def _storage(self) -> ManifestRevlog:
         narrowmatch = self._manifestlog._narrowmatch
         if not narrowmatch.always():
             if not narrowmatch.visitdir(self._dir[:-1]):
@@ -2189,7 +2454,7 @@
                 )
         return self._manifestlog.getstorage(self._dir)
 
-    def read(self):
+    def read(self) -> 'TreeManifest':
         if self._data is None:
             store = self._storage()
             if self._node == self._manifestlog.nodeconstants.nullid:
@@ -2222,29 +2487,25 @@
 
         return self._data
 
-    def node(self):
+    def node(self) -> bytes:
         return self._node
 
-    def copy(self):
+    def copy(self) -> 'MemTreeManifestCtx':
         memmf = memtreemanifestctx(self._manifestlog, dir=self._dir)
         memmf._treemanifest = self.read().copy()
         return memmf
 
     @propertycache
-    def parents(self):
+    def parents(self) -> Tuple[bytes, bytes]:
         return self._storage().parents(self._node)
 
-    def readdelta(self, shallow=False):
-        """Returns a manifest containing just the entries that are present
-        in this manifest, but not in its p1 manifest. This is efficient to read
-        if the revlog delta is already p1.
-
-        If `shallow` is True, this will read the delta for this directory,
-        without recursively reading subdirectory manifests. Instead, any
-        subdirectory entry will be reported as it appears in the manifest, i.e.
-        the subdirectory will be reported among files and distinguished only by
-        its 't' flag.
-        """
+    def readdelta(self, shallow: bool = False) -> AnyManifestDict:
+        """see `imanifestrevisionstored` documentation"""
+        util.nouideprecwarn(
+            b'"readdelta" is deprecated use "read_any_fast_delta" or "read_delta_new_entries"',
+            b"6.9",
+            stacklevel=2,
+        )
         store = self._storage()
         if shallow:
             r = store.rev(self._node)
@@ -2263,7 +2524,146 @@
                         md.setflag(f, fl1)
             return md
 
-    def readfast(self, shallow=False):
+    def read_any_fast_delta(
+        self,
+        valid_bases: Optional[Collection[int]] = None,
+        *,
+        shallow: bool = False,
+    ) -> Tuple[Optional[int], AnyManifestDict]:
+        """see `imanifestrevisionstored` documentation"""
+        store = self._storage()
+        r = store.rev(self._node)
+        deltaparent = store.deltaparent(r)
+
+        if valid_bases is None:
+            # make sure the next check is True
+            valid_bases = (deltaparent,)
+        can_use_delta = deltaparent != nullrev and deltaparent in valid_bases
+
+        if shallow:
+            if can_use_delta:
+                return (deltaparent, self._read_storage_delta_shallow())
+            else:
+                d = store.revision(self._node)
+                return (None, manifestdict(store.nodeconstants.nodelen, d))
+        else:
+            # note: This use "slow_delta" here is cargo culted from the previous
+            # implementation. I am not sure it make sense since the goal here is to
+            # be fast, so why are we computing a delta? On the other hand, tree
+            # manifest delta as fairly "cheap" and allow for skipping whole part of
+            # the tree that a full read would access. So it might be a good idea.
+            #
+            # If we realize we don't need delta here, we should simply use:
+            #
+            #     return (None, self.read())
+            if can_use_delta:
+                return (None, self._read_storage_slow_delta(base=deltaparent))
+            else:
+                parents = [
+                    p
+                    for p in store.parentrevs(r)
+                    if p is not nullrev and p in valid_bases
+                ]
+                if parents:
+                    best_base = max(parents)
+                else:
+                    best_base = max(valid_bases)
+                return (None, self._read_storage_slow_delta(base=best_base))
+
+    def _read_storage_delta_shallow(self) -> ManifestDict:
+        store = self._storage()
+        r = store.rev(self._node)
+        d = mdiff.patchtext(store.revdiff(store.deltaparent(r), r))
+        return manifestdict(store.nodeconstants.nodelen, d)
+
+    def _read_storage_slow_delta(self, base) -> 'TreeManifest':
+        store = self._storage()
+        if base is None:
+            base = store.deltaparent(store.rev(self._node))
+        m0 = self._manifestlog.get(self._dir, store.node(base)).read()
+        m1 = self.read()
+        md = treemanifest(self._manifestlog.nodeconstants, dir=self._dir)
+        for f, ((n0, fl0), (n1, fl1)) in m0.diff(m1).items():
+            if n1:
+                md[f] = n1
+                if fl1:
+                    md.setflag(f, fl1)
+        return md
+
+    def read_delta_parents(
+        self,
+        *,
+        shallow: bool = False,
+        exact: bool = True,
+    ) -> AnyManifestDict:
+        """see `interface.imanifestrevisionbase` documentations"""
+        store = self._storage()
+        r = store.rev(self._node)
+        parents = [p for p in store.parentrevs(r) if p is not nullrev]
+        if not exact:
+            return self.read_any_fast_delta(parents, shallow=shallow)[1]
+        elif len(parents) == 0:
+            if shallow:
+                d = store.revision(self._node)
+                return manifestdict(store.nodeconstants.nodelen, d)
+            else:
+                return self.read()
+        elif len(parents) == 1:
+            p = parents[0]
+            if shallow:
+                d = mdiff.patchtext(store.revdiff(p, r))
+                return manifestdict(store.nodeconstants.nodelen, d)
+            else:
+                return self._read_storage_slow_delta(base=p)
+        else:
+            p1, p2 = parents
+            if shallow:
+                d1 = mdiff.patchtext(store.revdiff(p1, r))
+                d2 = mdiff.patchtext(store.revdiff(p2, r))
+                d1 = manifestdict(store.nodeconstants.nodelen, d1)
+                d2 = manifestdict(store.nodeconstants.nodelen, d2)
+                md = manifestdict(store.nodeconstants.nodelen)
+                for f, new_node, new_flag in d1.iterentries():
+                    if f not in d2:
+                        continue
+                    if new_node is not None:
+                        md.set(f, new_node, new_flag)
+                return md
+            else:
+                m1 = self._manifestlog.get(self._dir, store.node(p1)).read()
+                m2 = self._manifestlog.get(self._dir, store.node(p2)).read()
+                mc = self.read()
+                d1 = m1.diff(mc)
+                d2 = m2.diff(mc)
+                md = treemanifest(
+                    self._manifestlog.nodeconstants,
+                    dir=self._dir,
+                )
+                for f, new_node, new_flag in d1.iterentries():
+                    if f not in d2:
+                        continue
+                    if new_node is not None:
+                        md.set(f, new_node, new_flag)
+                return md
+
+    def read_delta_new_entries(
+        self, *, shallow: bool = False
+    ) -> AnyManifestDict:
+        """see `interface.imanifestrevisionbase` documentations"""
+        # If we are using narrow, returning a delta against an arbitrary
+        # changeset might return file outside the narrowspec. This can create
+        # issue when running validation server side with strict security as
+        # push from low priviledge usage might be seen as adding new revision
+        # for files they cannot touch. So we are strict if narrow is involved.
+        if self._manifestlog.narrowed:
+            return self.read_delta_parents(shallow=shallow, exact=True)
+        # delegate to existing another existing method for simplicity
+        store = self._storage()
+        r = store.rev(self._node)
+        bases = (store.deltaparent(r),)
+        return self.read_any_fast_delta(bases, shallow=shallow)[1]
+
+    def readfast(self, shallow=False) -> AnyManifestDict:
         """Calls either readdelta or read, based on which would be less work.
         readdelta is called if the delta is against the p1, and therefore can be
         read quickly.
@@ -2271,6 +2671,11 @@
         If `shallow` is True, it only returns the entries from this manifest,
         and not any submanifests.
         """
+        util.nouideprecwarn(
+            b'"readdelta" is deprecated use "read_any_fast_delta" or "read_delta_parents"',
+            b"6.9",
+            stacklevel=2,
+        )
         store = self._storage()
         r = store.rev(self._node)
         deltaparent = store.deltaparent(r)
@@ -2284,10 +2689,18 @@
         else:
             return self.read()
 
-    def find(self, key):
+    def find(self, key: bytes) -> Tuple[bytes, bytes]:
         return self.read().find(key)
 
 
+treemanifestctx = interfaceutil.implementer(repository.imanifestrevisionstored)(
+    TreeManifestCtx
+)
+
+if typing.TYPE_CHECKING:
+    treemanifestctx = TreeManifestCtx
+
+
 class excludeddir(treemanifest):
     """Stand-in for a directory that is excluded from the repository.
 
@@ -2300,6 +2713,9 @@
     whose contents are unknown.
     """
 
+    _files: Dict[bytes, bytes]
+    _flags: Dict[bytes, bytes]
+
     def __init__(self, nodeconstants, dir, node):
         super(excludeddir, self).__init__(nodeconstants, dir)
         self._node = node
--- a/mercurial/match.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/match.py	Fri Sep 06 02:12:19 2024 +0200
@@ -399,12 +399,12 @@
         if badfn is not None:
             self.bad = badfn
 
-    def was_tampered_with_nonrec(self):
+    def was_tampered_with_nonrec(self) -> bool:
         # [_was_tampered_with] is used to track if when extensions changed the matcher
         # behavior (crazy stuff!), so we disable the rust fast path.
         return self._was_tampered_with
 
-    def was_tampered_with(self):
+    def was_tampered_with(self) -> bool:
         return self.was_tampered_with_nonrec()
 
     def __call__(self, fn):
@@ -894,7 +894,7 @@
         self.bad = m1.bad
         self.traversedir = m1.traversedir
 
-    def was_tampered_with(self):
+    def was_tampered_with(self) -> bool:
         return (
             self.was_tampered_with_nonrec()
             or self._m1.was_tampered_with()
@@ -984,7 +984,7 @@
         self.bad = m1.bad
         self.traversedir = m1.traversedir
 
-    def was_tampered_with(self):
+    def was_tampered_with(self) -> bool:
         return (
             self.was_tampered_with_nonrec()
             or self._m1.was_tampered_with()
@@ -1071,7 +1071,7 @@
     sub/x.txt: No such file
     """
 
-    def __init__(self, path, matcher):
+    def __init__(self, path: bytes, matcher: basematcher) -> None:
         super(subdirmatcher, self).__init__()
         self._path = path
         self._matcher = matcher
@@ -1088,7 +1088,7 @@
         if matcher.prefix():
             self._always = any(f == path for f in matcher._files)
 
-    def was_tampered_with(self):
+    def was_tampered_with(self) -> bool:
         return (
             self.was_tampered_with_nonrec() or self._matcher.was_tampered_with()
         )
@@ -1227,7 +1227,7 @@
         self.traversedir = m1.traversedir
         self._matchers = matchers
 
-    def was_tampered_with(self):
+    def was_tampered_with(self) -> bool:
         return self.was_tampered_with_nonrec() or any(
             map(lambda m: m.was_tampered_with(), self._matchers)
         )
--- a/mercurial/merge.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/merge.py	Fri Sep 06 02:12:19 2024 +0200
@@ -8,10 +8,17 @@
 
 import collections
 import struct
+import typing
 
 from .i18n import _
 from .node import nullrev
 from .thirdparty import attr
+
+# Force pytype to use the non-vendored package
+if typing.TYPE_CHECKING:
+    # noinspection PyPackageRequirements
+    import attr
+
 from .utils import stringutil
 from .dirstateutils import timestamp
 from . import (
@@ -420,11 +427,11 @@
     # Track the names of all deleted files.
     for f in mresult.files((mergestatemod.ACTION_REMOVE,)):
         deletedfiles.add(f)
-    for (f, args, msg) in mresult.getactions((mergestatemod.ACTION_MERGE,)):
+    for f, args, msg in mresult.getactions((mergestatemod.ACTION_MERGE,)):
         f1, f2, fa, move, anc = args
         if move:
             deletedfiles.add(f1)
-    for (f, args, msg) in mresult.getactions(
+    for f, args, msg in mresult.getactions(
         (mergestatemod.ACTION_DIR_RENAME_MOVE_LOCAL,)
     ):
         f2, flags = args
--- a/mercurial/mergestate.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/mergestate.py	Fri Sep 06 02:12:19 2024 +0200
@@ -495,7 +495,6 @@
 
 
 class mergestate(_mergestate_base):
-
     statepathv1 = b'merge/state'
     statepathv2 = b'merge/state2'
 
--- a/mercurial/metadata.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/metadata.py	Fri Sep 06 02:12:19 2024 +0200
@@ -433,14 +433,12 @@
     # Iteration over d1 content will deal with all cases, but the one in the
     # first column of the table.
     for filename, d1 in diff_p1.items():
-
         d2 = diff_p2.pop(filename, None)
 
         if d2 is None:
             # this deal with the first line of the table.
             _process_other_unchanged(md, mas, filename, d1)
         else:
-
             if d1[0][0] is None and d2[0][0] is None:
                 # case 🄼 — both deleted the file.
                 md.mark_added(filename)
--- a/mercurial/narrowspec.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/narrowspec.py	Fri Sep 06 02:12:19 2024 +0200
@@ -225,7 +225,6 @@
         m = "changing narrow spec outside of a transaction"
         raise error.ProgrammingError(m)
     else:
-
         reporef = weakref.ref(repo)
 
         def clean_pending(tr):
--- a/mercurial/obsolete.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/obsolete.py	Fri Sep 06 02:12:19 2024 +0200
@@ -1038,7 +1038,6 @@
 
 
 def makefoldid(relation, user):
-
     folddigest = hashutil.sha1(user)
     for p in relation[0] + relation[1]:
         folddigest.update(b'%d' % p.rev())
--- a/mercurial/obsutil.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/obsutil.py	Fri Sep 06 02:12:19 2024 +0200
@@ -947,7 +947,7 @@
 }
 
 
-def _getfilteredreason(repo, changeid, ctx):
+def _getfilteredreason(repo, changeid, ctx) -> bytes:
     """return a human-friendly string on why a obsolete changeset is hidden"""
     successors = successorssets(repo, ctx.node())
     fate = _getobsfate(successors)
@@ -961,7 +961,6 @@
         single_successor = short(successors[0][0])
         return filteredmsgtable[b'superseded'] % (changeid, single_successor)
     elif fate == b'superseded_split':
-
         succs = []
         for node_id in successors[0]:
             succs.append(short(node_id))
@@ -975,6 +974,8 @@
 
             args = (changeid, firstsuccessors, remainingnumber)
             return filteredmsgtable[b'superseded_split_several'] % args
+    else:
+        raise error.ProgrammingError("unhandled fate: %r" % fate)
 
 
 def divergentsets(repo, ctx):
--- a/mercurial/patch.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/patch.py	Fri Sep 06 02:12:19 2024 +0200
@@ -229,7 +229,6 @@
 
 
 def _extract(ui, fileobj, tmpname, tmpfp):
-
     # attempt to detect the start of a patch
     # (this heuristic is borrowed from quilt)
     diffre = re.compile(
@@ -596,7 +595,7 @@
         self.created = 0
         self.maxsize = maxsize
         if self.maxsize is None:
-            self.maxsize = 4 * (2 ** 20)
+            self.maxsize = 4 * (2**20)
         self.size = 0
         self.data = {}
 
--- a/mercurial/phases.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/phases.py	Fri Sep 06 02:12:19 2024 +0200
@@ -116,6 +116,7 @@
     Optional,
     Set,
     Tuple,
+    overload,
 )
 
 from .i18n import _
@@ -138,6 +139,9 @@
 PhaseSets = Dict[int, Set[int]]
 
 if typing.TYPE_CHECKING:
+    from typing_extensions import (
+        Literal,  # py3.8+
+    )
     from . import (
         localrepo,
         ui as uimod,
@@ -375,11 +379,31 @@
 
 
 class phasecache:
+    if typing.TYPE_CHECKING:
+
+        @overload
+        def __init__(
+            self,
+            repo: Any,
+            phasedefaults: Any,
+            _load: Literal[False],
+        ) -> None:
+            pass
+
+        @overload
+        def __init__(
+            self,
+            repo: "localrepo.localrepository",
+            phasedefaults: Optional["Phasedefaults"],
+            _load: bool = True,
+        ) -> None:
+            pass
+
     def __init__(
         self,
-        repo: "localrepo.localrepository",
-        phasedefaults: Optional["Phasedefaults"],
-        _load: bool = True,
+        repo,
+        phasedefaults,
+        _load=True,
     ):
         if _load:
             # Cheap trick to allow shallow-copy without copy module
@@ -387,7 +411,7 @@
             self._phaseroots: Phaseroots = loaded[0]
             self.dirty: bool = loaded[1]
             self._loadedrevslen = 0
-            self._phasesets: PhaseSets = None
+            self._phasesets: Optional[PhaseSets] = None
 
     def hasnonpublicphases(self, repo: "localrepo.localrepository") -> bool:
         """detect if there are revisions with non-public phase"""
@@ -893,7 +917,6 @@
 
         this_phase_set = self._phasesets[targetphase]
         for r in range(start, end):
-
             # gather information about the current_rev
             r_phase = phase(repo, r)
             p_phase = None  # phase inherited from parents
--- a/mercurial/policy.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/policy.py	Fri Sep 06 02:12:19 2024 +0200
@@ -8,6 +8,14 @@
 
 import os
 import sys
+import typing
+
+if typing.TYPE_CHECKING:
+    from typing import (
+        Dict,
+        Optional,
+        Tuple,
+    )
 
 # Rules for how modules can be loaded. Values are:
 #
@@ -23,8 +31,8 @@
 # By default, fall back to the pure modules so the in-place build can
 # run without recompiling the C extensions. This will be overridden by
 # __modulepolicy__ generated by setup.py.
-policy = b'allow'
-_packageprefs = {
+policy: bytes = b'allow'
+_packageprefs: "Dict[bytes, Tuple[Optional[str], Optional[str]]]" = {
     # policy: (versioned package, pure package)
     b'c': ('cext', None),
     b'allow': ('cext', 'pure'),
@@ -37,9 +45,9 @@
 }
 
 try:
-    from . import __modulepolicy__
+    from . import __modulepolicy__  # pytype: disable=import-error
 
-    policy = __modulepolicy__.modulepolicy
+    policy: bytes = __modulepolicy__.modulepolicy
 except ImportError:
     pass
 
@@ -48,14 +56,14 @@
 # The canonical way to do this is to test platform.python_implementation().
 # But we don't import platform and don't bloat for it here.
 if '__pypy__' in sys.builtin_module_names:
-    policy = b'cffi'
+    policy: bytes = b'cffi'
 
 # Environment variable can always force settings.
 if 'HGMODULEPOLICY' in os.environ:
-    policy = os.environ['HGMODULEPOLICY'].encode('utf-8')
+    policy: bytes = os.environ['HGMODULEPOLICY'].encode('utf-8')
 
 
-def _importfrom(pkgname, modname):
+def _importfrom(pkgname: str, modname: str):
     # from .<pkgname> import <modname> (where . is looked through this module)
     fakelocals = {}
     pkg = __import__(pkgname, globals(), fakelocals, [modname], level=1)
@@ -69,7 +77,7 @@
 
 
 # keep in sync with "version" in C modules
-_cextversions = {
+_cextversions: "Dict[Tuple[str, str], int]" = {
     ('cext', 'base85'): 1,
     ('cext', 'bdiff'): 3,
     ('cext', 'mpatch'): 1,
@@ -78,7 +86,7 @@
 }
 
 # map import request to other package or module
-_modredirects = {
+_modredirects: "Dict[Tuple[str, str], Tuple[str, str]]" = {
     ('cext', 'charencode'): ('cext', 'parsers'),
     ('cffi', 'base85'): ('pure', 'base85'),
     ('cffi', 'charencode'): ('pure', 'charencode'),
@@ -86,7 +94,7 @@
 }
 
 
-def _checkmod(pkgname, modname, mod):
+def _checkmod(pkgname: str, modname: str, mod) -> None:
     expected = _cextversions.get((pkgname, modname))
     actual = getattr(mod, 'version', None)
     if actual != expected:
@@ -97,7 +105,7 @@
         )
 
 
-def importmod(modname):
+def importmod(modname: str):
     """Import module according to policy and check API version"""
     try:
         verpkg, purepkg = _packageprefs[policy]
@@ -118,12 +126,12 @@
     return _importfrom(pn, mn)
 
 
-def _isrustpermissive():
+def _isrustpermissive() -> bool:
     """Assuming the policy is a Rust one, tell if it's permissive."""
     return policy.endswith(b'-allow')
 
 
-def importrust(modname, member=None, default=None):
+def importrust(modname: str, member: "Optional[str]" = None, default=None):
     """Import Rust module according to policy and availability.
 
     If policy isn't a Rust one, this returns `default`.
--- a/mercurial/posix.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/posix.py	Fri Sep 06 02:12:19 2024 +0200
@@ -214,7 +214,7 @@
 def copymode(
     src: bytes,
     dst: bytes,
-    mode: Optional[bytes] = None,
+    mode: Optional[int] = None,
     enforcewritable: bool = False,
 ) -> None:
     """Copy the file mode from the file at path src to dst.
@@ -387,20 +387,20 @@
     return None  # on posix platforms, every path is ok
 
 
-def getfsmountpoint(dirpath: bytes) -> Optional[bytes]:
+def getfsmountpoint(path: bytes) -> Optional[bytes]:
     """Get the filesystem mount point from a directory (best-effort)
 
     Returns None if we are unsure. Raises OSError on ENOENT, EPERM, etc.
     """
-    return getattr(osutil, 'getfsmountpoint', lambda x: None)(dirpath)
+    return getattr(osutil, 'getfsmountpoint', lambda x: None)(path)
 
 
-def getfstype(dirpath: bytes) -> Optional[bytes]:
+def getfstype(path: bytes) -> Optional[bytes]:
     """Get the filesystem type name from a directory (best-effort)
 
     Returns None if we are unsure. Raises OSError on ENOENT, EPERM, etc.
     """
-    return getattr(osutil, 'getfstype', lambda x: None)(dirpath)
+    return getattr(osutil, 'getfstype', lambda x: None)(path)
 
 
 def get_password() -> bytes:
@@ -549,7 +549,7 @@
 
 if pycompat.sysplatform == b'OpenVMS':
     # OpenVMS's symlink emulation is broken on some OpenVMS versions.
-    def checklink(path):
+    def checklink(path: bytes) -> bool:
         return False
 
 
@@ -692,7 +692,7 @@
 
 def lookupreg(
     key: bytes,
-    name: Optional[bytes] = None,
+    valname: Optional[bytes] = None,
     scope: Optional[Union[int, Iterable[int]]] = None,
 ) -> Optional[bytes]:
     return None
@@ -708,6 +708,8 @@
 
 
 class cachestat:
+    stat: os.stat_result
+
     def __init__(self, path: bytes) -> None:
         self.stat = os.stat(path)
 
--- a/mercurial/profiling.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/profiling.py	Fri Sep 06 02:12:19 2024 +0200
@@ -7,6 +7,9 @@
 
 
 import contextlib
+import os
+import signal
+import subprocess
 
 from .i18n import _
 from .pycompat import (
@@ -175,6 +178,50 @@
         fp.flush()
 
 
+@contextlib.contextmanager
+def pyspy_profile(ui, fp):
+    exe = ui.config(b'profiling', b'py-spy.exe')
+
+    freq = ui.configint(b'profiling', b'py-spy.freq')
+
+    format = ui.config(b'profiling', b'py-spy.format')
+
+    fd = fp.fileno()
+
+    output_path = "/dev/fd/%d" % (fd)
+
+    my_pid = os.getpid()
+
+    cmd = [
+        exe,
+        "record",
+        "--pid",
+        str(my_pid),
+        "--native",
+        "--rate",
+        str(freq),
+        "--output",
+        output_path,
+    ]
+
+    if format:
+        cmd.extend(["--format", format])
+
+    proc = subprocess.Popen(
+        cmd,
+        pass_fds={fd},
+        stdout=subprocess.PIPE,
+    )
+
+    _ = proc.stdout.readline()
+
+    try:
+        yield
+    finally:
+        os.kill(proc.pid, signal.SIGINT)
+        proc.communicate()
+
+
 class profile:
     """Start profiling.
 
@@ -214,7 +261,7 @@
         proffn = None
         if profiler is None:
             profiler = self._ui.config(b'profiling', b'type')
-        if profiler not in (b'ls', b'stat', b'flame'):
+        if profiler not in (b'ls', b'stat', b'flame', b'py-spy'):
             # try load profiler from extension with the same name
             proffn = _loadprofiler(self._ui, profiler)
             if proffn is None:
@@ -257,6 +304,8 @@
                 proffn = lsprofile
             elif profiler == b'flame':
                 proffn = flameprofile
+            elif profiler == b'py-spy':
+                proffn = pyspy_profile
             else:
                 proffn = statprofile
 
--- a/mercurial/pure/base85.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/pure/base85.py	Fri Sep 06 02:12:19 2024 +0200
@@ -23,7 +23,7 @@
         _b85dec[c] = i
 
 
-def b85encode(text, pad=False):
+def b85encode(text: bytes, pad: bool = False) -> bytes:
     """encode text in base85 format"""
     l = len(text)
     r = l % 4
@@ -50,7 +50,7 @@
     return out[:olen]
 
 
-def b85decode(text):
+def b85decode(text: bytes) -> bytes:
     """decode base85-encoded text"""
     if not _b85dec:
         _mkb85dec()
--- a/mercurial/pure/charencode.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/pure/charencode.py	Fri Sep 06 02:12:19 2024 +0200
@@ -11,7 +11,7 @@
 from .. import pycompat
 
 
-def isasciistr(s):
+def isasciistr(s: bytes) -> bool:
     try:
         s.decode('ascii')
         return True
@@ -19,7 +19,7 @@
         return False
 
 
-def asciilower(s):
+def asciilower(s: bytes) -> bytes:
     """convert a string to lowercase if ASCII
 
     Raises UnicodeDecodeError if non-ASCII characters are found."""
@@ -27,7 +27,7 @@
     return s.lower()
 
 
-def asciiupper(s):
+def asciiupper(s: bytes) -> bytes:
     """convert a string to uppercase if ASCII
 
     Raises UnicodeDecodeError if non-ASCII characters are found."""
@@ -52,7 +52,7 @@
 _jsonmap.extend(pycompat.bytechr(x) for x in range(128, 256))
 
 
-def jsonescapeu8fast(u8chars, paranoid):
+def jsonescapeu8fast(u8chars: bytes, paranoid: bool) -> bytes:
     """Convert a UTF-8 byte string to JSON-escaped form (fast path)
 
     Raises ValueError if non-ASCII characters have to be escaped.
@@ -70,7 +70,7 @@
 _utf8strict = r'surrogatepass'
 
 
-def jsonescapeu8fallback(u8chars, paranoid):
+def jsonescapeu8fallback(u8chars: bytes, paranoid: bool) -> bytes:
     """Convert a UTF-8 byte string to JSON-escaped form (slow path)
 
     Escapes all non-ASCII characters no matter if paranoid is False.
--- a/mercurial/pure/mpatch.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/pure/mpatch.py	Fri Sep 06 02:12:19 2024 +0200
@@ -106,7 +106,7 @@
             try:
                 p1, p2, l = struct.unpack(b">lll", m.read(12))
             except struct.error:
-                raise mpatchError(b"patch cannot be decoded")
+                raise mpatchError("patch cannot be decoded")
             _pull(new, frags, p1 - last)  # what didn't change
             _pull([], frags, p2 - p1)  # what got deleted
             new.append((l, pos + 12))  # what got added
@@ -137,7 +137,7 @@
         outlen += length
 
     if bin != binend:
-        raise mpatchError(b"patch cannot be decoded")
+        raise mpatchError("patch cannot be decoded")
 
     outlen += orig - last
     return outlen
--- a/mercurial/pure/parsers.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/pure/parsers.py	Fri Sep 06 02:12:19 2024 +0200
@@ -9,6 +9,7 @@
 import io
 import stat
 import struct
+import typing
 import zlib
 
 from ..node import (
@@ -16,8 +17,15 @@
     sha1nodeconstants,
 )
 from ..thirdparty import attr
+
+# Force pytype to use the non-vendored package
+if typing.TYPE_CHECKING:
+    # noinspection PyPackageRequirements
+    import attr
+
 from .. import (
     error,
+    pycompat,
     revlogutils,
     util,
 )
@@ -228,7 +236,7 @@
                     parentfiledata=(mode, size, (mtime, 0, False)),
                 )
         else:
-            raise RuntimeError(b'unknown state: %s' % state)
+            raise RuntimeError('unknown state: %s' % pycompat.sysstr(state))
 
     def set_possibly_dirty(self):
         """Mark a file as "possibly dirty"
@@ -644,7 +652,7 @@
 
     def _check_index(self, i):
         if not isinstance(i, int):
-            raise TypeError(b"expecting int indexes")
+            raise TypeError("expecting int indexes")
         if i < 0 or i >= len(self):
             raise IndexError(i)
 
@@ -704,7 +712,7 @@
 
     def __delitem__(self, i):
         if not isinstance(i, slice) or not i.stop == -1 or i.step is not None:
-            raise ValueError(b"deleting slices only supports a:-1 with step 1")
+            raise ValueError("deleting slices only supports a:-1 with step 1")
         i = i.start
         self._check_index(i)
         self._stripnodes(i)
@@ -783,12 +791,12 @@
             count += 1
             off += self.entry_size + s
         if off != len(self._data):
-            raise ValueError(b"corrupted data")
+            raise ValueError("corrupted data")
         return count
 
     def __delitem__(self, i):
         if not isinstance(i, slice) or not i.stop == -1 or i.step is not None:
-            raise ValueError(b"deleting slices only supports a:-1 with step 1")
+            raise ValueError("deleting slices only supports a:-1 with step 1")
         i = i.start
         self._check_index(i)
         self._stripnodes(i)
@@ -841,7 +849,7 @@
             raise KeyError
         self._check_index(rev)
         if rev < self._lgt:
-            msg = b"cannot rewrite entries outside of this transaction"
+            msg = "cannot rewrite entries outside of this transaction"
             raise KeyError(msg)
         else:
             entry = list(self[rev])
@@ -911,7 +919,6 @@
         )
 
     def _pack_entry(self, rev, entry):
-
         base = entry[revlog_constants.ENTRY_DELTA_BASE]
         link_rev = entry[revlog_constants.ENTRY_LINK_REV]
         assert base == rev, (base, rev)
--- a/mercurial/revlog.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/revlog.py	Fri Sep 06 02:12:19 2024 +0200
@@ -20,9 +20,17 @@
 import io
 import os
 import struct
+import typing
 import weakref
 import zlib
 
+from typing import (
+    Iterable,
+    Iterator,
+    Optional,
+    Tuple,
+)
+
 # import stuff from node for others to import from revlog
 from .node import (
     bin,
@@ -69,6 +77,12 @@
     REVIDX_RAWTEXT_CHANGING_FLAGS,
 )
 from .thirdparty import attr
+
+# Force pytype to use the non-vendored package
+if typing.TYPE_CHECKING:
+    # noinspection PyPackageRequirements
+    import attr
+
 from . import (
     ancestor,
     dagop,
@@ -79,6 +93,7 @@
     revlogutils,
     templatefilters,
     util,
+    vfs as vfsmod,
 )
 from .interfaces import (
     repository,
@@ -173,9 +188,8 @@
 )
 
 
-@interfaceutil.implementer(repository.irevisiondelta)
 @attr.s(slots=True)
-class revlogrevisiondelta:
+class RevLogRevisionDelta:
     node = attr.ib()
     p1node = attr.ib()
     p2node = attr.ib()
@@ -189,12 +203,27 @@
     linknode = attr.ib(default=None)
 
 
-@interfaceutil.implementer(repository.iverifyproblem)
+revlogrevisiondelta = interfaceutil.implementer(repository.irevisiondelta)(
+    RevLogRevisionDelta
+)
+
+if typing.TYPE_CHECKING:
+    revlogrevisiondelta = RevLogRevisionDelta
+
+
 @attr.s(frozen=True)
-class revlogproblem:
-    warning = attr.ib(default=None)
-    error = attr.ib(default=None)
-    node = attr.ib(default=None)
+class RevLogProblem:
+    warning = attr.ib(default=None, type=Optional[bytes])
+    error = attr.ib(default=None, type=Optional[bytes])
+    node = attr.ib(default=None, type=Optional[bytes])
+
+
+revlogproblem = interfaceutil.implementer(repository.iverifyproblem)(
+    RevLogProblem
+)
+
+if typing.TYPE_CHECKING:
+    revlogproblem = RevLogProblem
 
 
 def parse_index_v1(data, inline):
@@ -221,7 +250,6 @@
         index, cache = parsers.parse_index_devel_nodemap(data, inline)
         return index, cache
 
-
 else:
     parse_index_v1_nodemap = None
 
@@ -352,9 +380,11 @@
     boundaries are arbitrary and based on what we can delegate to Rust.
     """
 
+    opener: vfsmod.vfs
+
     def __init__(
         self,
-        opener,
+        opener: vfsmod.vfs,
         index,
         index_file,
         data_file,
@@ -558,7 +588,7 @@
         c = self._get_decompressor(t)
         return c.decompress
 
-    def _get_decompressor(self, t):
+    def _get_decompressor(self, t: bytes):
         try:
             compressor = self._decompressors[t]
         except KeyError:
@@ -574,7 +604,7 @@
                 )
         return compressor
 
-    def compress(self, data):
+    def compress(self, data: bytes) -> Tuple[bytes, bytes]:
         """Generate a possibly-compressed representation of data."""
         if not data:
             return b'', data
@@ -589,7 +619,7 @@
             return b'', data
         return b'u', data
 
-    def decompress(self, data):
+    def decompress(self, data: bytes):
         """Decompress a revlog chunk.
 
         The chunk is expected to begin with a header identifying the
@@ -1282,6 +1312,8 @@
 
     _flagserrorclass = error.RevlogError
 
+    opener: vfsmod.vfs
+
     @staticmethod
     def is_inline_index(header_bytes):
         """Determine if a revlog is inline from the initial bytes of the index"""
@@ -1296,9 +1328,11 @@
         features = FEATURES_BY_VERSION[_format_version]
         return features[b'inline'](_format_flags)
 
+    _docket_file: Optional[bytes]
+
     def __init__(
         self,
-        opener,
+        opener: vfsmod.vfs,
         target,
         radix,
         postfix=None,  # only exist for `tmpcensored` now
@@ -1794,7 +1828,7 @@
     def __len__(self):
         return len(self.index)
 
-    def __iter__(self):
+    def __iter__(self) -> Iterator[int]:
         return iter(range(len(self)))
 
     def revs(self, start=0, stop=None):
@@ -3081,7 +3115,7 @@
                 sidedata=sidedata,
             )
 
-    def compress(self, data):
+    def compress(self, data: bytes) -> Tuple[bytes, bytes]:
         return self._inner.compress(data)
 
     def decompress(self, data):
@@ -3870,7 +3904,7 @@
         else:
             rewrite.v2_censor(self, tr, censor_nodes, tombstone)
 
-    def verifyintegrity(self, state):
+    def verifyintegrity(self, state) -> Iterable[RevLogProblem]:
         """Verifies the integrity of the revlog.
 
         Yields ``revlogproblem`` instances describing problems that are
--- a/mercurial/revlogutils/__init__.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/revlogutils/__init__.py	Fri Sep 06 02:12:19 2024 +0200
@@ -6,7 +6,15 @@
 # GNU General Public License version 2 or any later version.
 
 
+import typing
+
 from ..thirdparty import attr
+
+# Force pytype to use the non-vendored package
+if typing.TYPE_CHECKING:
+    # noinspection PyPackageRequirements
+    import attr
+
 from ..interfaces import repository
 
 # See mercurial.revlogutils.constants for doc
--- a/mercurial/revlogutils/deltas.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/revlogutils/deltas.py	Fri Sep 06 02:12:19 2024 +0200
@@ -11,6 +11,7 @@
 import abc
 import collections
 import struct
+import typing
 
 # import stuff from node for others to import from revlog
 from ..node import nullrev
@@ -31,6 +32,11 @@
 
 from ..thirdparty import attr
 
+# Force pytype to use the non-vendored package
+if typing.TYPE_CHECKING:
+    # noinspection PyPackageRequirements
+    import attr
+
 from .. import (
     error,
     mdiff,
@@ -428,7 +434,6 @@
     # Cut the revs at collected indices
     previdx = 0
     for idx in selected:
-
         chunk = _trimchunk(revlog, revs, previdx, idx)
         if chunk:
             yield chunk
--- a/mercurial/revlogutils/nodemap.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/revlogutils/nodemap.py	Fri Sep 06 02:12:19 2024 +0200
@@ -553,7 +553,7 @@
 
     Children blocks are always yield before their parent block.
     """
-    for (__, item) in sorted(block.items()):
+    for __, item in sorted(block.items()):
         if isinstance(item, dict):
             for sub_block in _walk_trie(item):
                 yield sub_block
--- a/mercurial/revlogutils/rewrite.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/revlogutils/rewrite.py	Fri Sep 06 02:12:19 2024 +0200
@@ -258,7 +258,6 @@
                 # this revision is empty, we can delta against nullrev
                 rewritten_entries[rev] = (nullrev, 0, 0, COMP_MODE_PLAIN)
             else:
-
                 text = revlog.rawdata(rev)
                 info = revlogutils.revisioninfo(
                     node=entry[ENTRY_NODE_ID],
--- a/mercurial/scmutil.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/scmutil.py	Fri Sep 06 02:12:19 2024 +0200
@@ -13,8 +13,20 @@
 import posixpath
 import re
 import subprocess
+import typing
 import weakref
 
+from typing import (
+    Callable,
+    Dict,
+    Iterable,
+    Iterator,
+    List,
+    Optional,
+    Set,
+    Tuple,
+)
+
 from .i18n import _
 from .node import (
     bin,
@@ -24,6 +36,12 @@
     wdirrev,
 )
 from .thirdparty import attr
+
+# Force pytype to use the non-vendored package
+if typing.TYPE_CHECKING:
+    # noinspection PyPackageRequirements
+    import attr
+
 from . import (
     copies as copiesmod,
     encoding,
@@ -39,6 +57,7 @@
     revsetlang,
     similar,
     smartset,
+    typelib,
     url,
     util,
     vfs,
@@ -55,6 +74,11 @@
 else:
     from . import scmposix as scmplatform
 
+if typing.TYPE_CHECKING:
+    from . import (
+        ui as uimod,
+    )
+
 parsers = policy.importmod('parsers')
 rustrevlog = policy.importrust('revlog')
 
@@ -69,15 +93,15 @@
     relevant to the working copy.
     """
 
-    modified = attr.ib(default=attr.Factory(list))
-    added = attr.ib(default=attr.Factory(list))
-    removed = attr.ib(default=attr.Factory(list))
-    deleted = attr.ib(default=attr.Factory(list))
-    unknown = attr.ib(default=attr.Factory(list))
-    ignored = attr.ib(default=attr.Factory(list))
-    clean = attr.ib(default=attr.Factory(list))
-
-    def __iter__(self):
+    modified = attr.ib(default=attr.Factory(list), type=List[bytes])
+    added = attr.ib(default=attr.Factory(list), type=List[bytes])
+    removed = attr.ib(default=attr.Factory(list), type=List[bytes])
+    deleted = attr.ib(default=attr.Factory(list), type=List[bytes])
+    unknown = attr.ib(default=attr.Factory(list), type=List[bytes])
+    ignored = attr.ib(default=attr.Factory(list), type=List[bytes])
+    clean = attr.ib(default=attr.Factory(list), type=List[bytes])
+
+    def __iter__(self) -> Iterator[List[bytes]]:
         yield self.modified
         yield self.added
         yield self.removed
@@ -86,7 +110,7 @@
         yield self.ignored
         yield self.clean
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return (
             r'<status modified=%s, added=%s, removed=%s, deleted=%s, '
             r'unknown=%s, ignored=%s, clean=%s>'
@@ -119,7 +143,7 @@
         yield subpath, ctx2.nullsub(subpath, ctx1)
 
 
-def nochangesfound(ui, repo, excluded=None):
+def nochangesfound(ui: "uimod.ui", repo, excluded=None) -> None:
     """Report no changes for push/pull, excluded is None or a list of
     nodes excluded from the push/pull.
     """
@@ -139,7 +163,7 @@
         ui.status(_(b"no changes found\n"))
 
 
-def callcatch(ui, func):
+def callcatch(ui: "uimod.ui", func: Callable[[], int]) -> int:
     """call func() with global exception handling
 
     return func() if no exception happens. otherwise do some error handling
@@ -261,7 +285,7 @@
         return coarse_exit_code
 
 
-def checknewlabel(repo, lbl, kind):
+def checknewlabel(repo, lbl: bytes, kind) -> None:
     # Do not use the "kind" parameter in ui output.
     # It makes strings difficult to translate.
     if lbl in [b'tip', b'.', b'null']:
@@ -287,7 +311,7 @@
         )
 
 
-def checkfilename(f):
+def checkfilename(f: bytes) -> None:
     '''Check that the filename f is an acceptable filename for a tracked file'''
     if b'\r' in f or b'\n' in f:
         raise error.InputError(
@@ -296,7 +320,7 @@
         )
 
 
-def checkportable(ui, f):
+def checkportable(ui: "uimod.ui", f: bytes) -> None:
     '''Check if filename f is portable and warn or abort depending on config'''
     checkfilename(f)
     abort, warn = checkportabilityalert(ui)
@@ -309,7 +333,7 @@
             ui.warn(_(b"warning: %s\n") % msg)
 
 
-def checkportabilityalert(ui):
+def checkportabilityalert(ui: "uimod.ui") -> Tuple[bool, bool]:
     """check if the user's config requests nothing, a warning, or abort for
     non-portable filenames"""
     val = ui.config(b'ui', b'portablefilenames')
@@ -325,7 +349,7 @@
 
 
 class casecollisionauditor:
-    def __init__(self, ui, abort, dirstate):
+    def __init__(self, ui: "uimod.ui", abort: bool, dirstate) -> None:
         self._ui = ui
         self._abort = abort
         allfiles = b'\0'.join(dirstate)
@@ -336,7 +360,7 @@
         # same filename twice.
         self._newfiles = set()
 
-    def __call__(self, f):
+    def __call__(self, f: bytes) -> None:
         if f in self._newfiles:
             return
         fl = encoding.lower(f)
@@ -349,7 +373,9 @@
         self._newfiles.add(f)
 
 
-def combined_filtered_and_obsolete_hash(repo, maxrev, needobsolete=False):
+def combined_filtered_and_obsolete_hash(
+    repo, maxrev, needobsolete: bool = False
+):
     """build hash of filtered revisions in the current repoview.
 
     Multiple caches perform up-to-date validation by checking that the
@@ -430,7 +456,7 @@
     return (filtered_set, obs_set)
 
 
-def _hash_revs(revs):
+def _hash_revs(revs: Iterable[int]) -> bytes:
     """return a hash from a list of revision numbers"""
     s = hashutil.sha1()
     for rev in revs:
@@ -438,7 +464,12 @@
     return s.digest()
 
 
-def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
+def walkrepos(
+    path,
+    followsym: bool = False,
+    seen_dirs: Optional[List[bytes]] = None,
+    recurse: bool = False,
+) -> Iterable[bytes]:
     """yield every hg repository under path, always recursively.
     The recurse flag will only control recursion into repo working dirs"""
 
@@ -487,7 +518,7 @@
             dirs[:] = newdirs
 
 
-def binnode(ctx):
+def binnode(ctx) -> bytes:
     """Return binary node id for a given basectx"""
     node = ctx.node()
     if node is None:
@@ -495,7 +526,7 @@
     return node
 
 
-def intrev(ctx):
+def intrev(ctx) -> int:
     """Return integer for a given basectx that can be used in comparison or
     arithmetic operation"""
     rev = ctx.rev()
@@ -504,14 +535,14 @@
     return rev
 
 
-def formatchangeid(ctx):
+def formatchangeid(ctx) -> bytes:
     """Format changectx as '{rev}:{node|formatnode}', which is the default
     template provided by logcmdutil.changesettemplater"""
     repo = ctx.repo()
     return formatrevnode(repo.ui, intrev(ctx), binnode(ctx))
 
 
-def formatrevnode(ui, rev, node):
+def formatrevnode(ui: "uimod.ui", rev: int, node: bytes) -> bytes:
     """Format given revision and node depending on the current verbosity"""
     if ui.debugflag:
         hexfunc = hex
@@ -520,7 +551,7 @@
     return b'%d:%s' % (rev, hexfunc(node))
 
 
-def resolvehexnodeidprefix(repo, prefix):
+def resolvehexnodeidprefix(repo, prefix: bytes):
     if prefix.startswith(b'x'):
         prefix = prefix[1:]
     try:
@@ -552,7 +583,7 @@
     return node
 
 
-def mayberevnum(repo, prefix):
+def mayberevnum(repo, prefix: bytes) -> bool:
     """Checks if the given prefix may be mistaken for a revision number"""
     try:
         i = int(prefix)
@@ -567,7 +598,7 @@
         return False
 
 
-def shortesthexnodeidprefix(repo, node, minlength=1, cache=None):
+def shortesthexnodeidprefix(repo, node: bytes, minlength: int = 1, cache=None):
     """Find the shortest unambiguous prefix that matches hexnode.
 
     If "cache" is not None, it must be a dictionary that can be used for
@@ -579,7 +610,7 @@
 
     minlength = max(minlength, 1)
 
-    def disambiguate(prefix):
+    def disambiguate(prefix: bytes):
         """Disambiguate against revnums."""
         if repo.ui.configbool(b'experimental', b'revisions.prefixhexnode'):
             if mayberevnum(repo, prefix):
@@ -644,7 +675,7 @@
         raise error.RepoLookupError()
 
 
-def isrevsymbol(repo, symbol):
+def isrevsymbol(repo, symbol: bytes) -> bool:
     """Checks if a symbol exists in the repo.
 
     See revsymbol() for details. Raises error.AmbiguousPrefixLookupError if the
@@ -657,7 +688,7 @@
         return False
 
 
-def revsymbol(repo, symbol):
+def revsymbol(repo, symbol: bytes):
     """Returns a context given a single revision symbol (as string).
 
     This is similar to revsingle(), but accepts only a single revision symbol,
@@ -724,13 +755,12 @@
         raise _filterederror(repo, symbol)
 
 
-def _filterederror(repo, changeid):
+def _filterederror(repo, changeid: bytes) -> error.FilteredRepoLookupError:
     """build an exception to be raised about a filtered changeid
 
     This is extracted in a function to help extensions (eg: evolve) to
     experiment with various message variants."""
     if repo.filtername.startswith(b'visible'):
-
         # Check if the changeset is obsolete
         unfilteredrepo = repo.unfiltered()
         ctx = revsymbol(unfilteredrepo, changeid)
@@ -760,7 +790,7 @@
     return repo[l.last()]
 
 
-def _pairspec(revspec):
+def _pairspec(revspec) -> bool:
     tree = revsetlang.parse(revspec)
     return tree and tree[0] in (
         b'range',
@@ -825,7 +855,9 @@
     return repo.anyrevs(allspecs, user=True, localalias=localalias)
 
 
-def increasingwindows(windowsize=8, sizelimit=512):
+def increasingwindows(
+    windowsize: int = 8, sizelimit: int = 512
+) -> Iterable[int]:
     while True:
         yield windowsize
         if windowsize < sizelimit:
@@ -891,7 +923,11 @@
     return parents
 
 
-def getuipathfn(repo, legacyrelativevalue=False, forcerelativevalue=None):
+def getuipathfn(
+    repo,
+    legacyrelativevalue: bool = False,
+    forcerelativevalue: Optional[bool] = None,
+) -> typelib.UiPathFn:
     """Return a function that produced paths for presenting to the user.
 
     The returned function takes a repo-relative path and produces a path
@@ -931,12 +967,14 @@
         return util.localpath
 
 
-def subdiruipathfn(subpath, uipathfn):
+def subdiruipathfn(
+    subpath: bytes, uipathfn: typelib.UiPathFn
+) -> typelib.UiPathFn:
     '''Create a new uipathfn that treats the file as relative to subpath.'''
     return lambda f: uipathfn(posixpath.join(subpath, f))
 
 
-def anypats(pats, opts):
+def anypats(pats, opts) -> bool:
     """Checks if any patterns, including --include and --exclude were given.
 
     Some commands (e.g. addremove) use this condition for deciding whether to
@@ -945,7 +983,7 @@
     return bool(pats or opts.get(b'include') or opts.get(b'exclude'))
 
 
-def expandpats(pats):
+def expandpats(pats: Iterable[bytes]) -> List[bytes]:
     """Expand bare globs when running on windows.
     On posix we assume it already has already been done by sh."""
     if not util.expandglobs:
@@ -966,7 +1004,12 @@
 
 
 def matchandpats(
-    ctx, pats=(), opts=None, globbed=False, default=b'relpath', badfn=None
+    ctx,
+    pats=(),
+    opts=None,
+    globbed: bool = False,
+    default: bytes = b'relpath',
+    badfn=None,
 ):
     """Return a matcher and the patterns that were used.
     The matcher will warn about bad matches, unless an alternate badfn callback
@@ -999,7 +1042,12 @@
 
 
 def match(
-    ctx, pats=(), opts=None, globbed=False, default=b'relpath', badfn=None
+    ctx,
+    pats=(),
+    opts=None,
+    globbed: bool = False,
+    default: bytes = b'relpath',
+    badfn=None,
 ):
     '''Return a matcher that will warn about bad matches.'''
     return matchandpats(ctx, pats, opts, globbed, default, badfn=badfn)[0]
@@ -1010,12 +1058,12 @@
     return matchmod.always()
 
 
-def matchfiles(repo, files, badfn=None):
+def matchfiles(repo, files, badfn=None) -> matchmod.exactmatcher:
     '''Return a matcher that will efficiently match exactly these files.'''
     return matchmod.exact(files, badfn=badfn)
 
 
-def parsefollowlinespattern(repo, rev, pat, msg):
+def parsefollowlinespattern(repo, rev, pat: bytes, msg: bytes) -> bytes:
     """Return a file name from `pat` pattern suitable for usage in followlines
     logic.
     """
@@ -1030,7 +1078,7 @@
         return files[0]
 
 
-def getorigvfs(ui, repo):
+def getorigvfs(ui: "uimod.ui", repo):
     """return a vfs suitable to save 'orig' file
 
     return None if no special directory is configured"""
@@ -1040,7 +1088,7 @@
     return vfs.vfs(repo.wvfs.join(origbackuppath))
 
 
-def backuppath(ui, repo, filepath):
+def backuppath(ui: "uimod.ui", repo, filepath: bytes) -> bytes:
     """customize where working copy backup files (.orig files) are created
 
     Fetch user defined path from config file: [ui] origbackuppath = <path>
@@ -1083,7 +1131,7 @@
         self._torev = repo.changelog.rev
         self._revcontains = revcontainer.__contains__
 
-    def __contains__(self, node):
+    def __contains__(self, node) -> bool:
         return self._revcontains(self._torev(node))
 
 
@@ -1096,7 +1144,7 @@
     fixphase=False,
     targetphase=None,
     backup=True,
-):
+) -> None:
     """do common cleanups when old nodes are replaced by new nodes
 
     That includes writing obsmarkers or stripping nodes, and moving bookmarks.
@@ -1270,7 +1318,14 @@
                 )
 
 
-def addremove(repo, matcher, prefix, uipathfn, opts=None, open_tr=None):
+def addremove(
+    repo,
+    matcher,
+    prefix: bytes,
+    uipathfn: typelib.UiPathFn,
+    opts=None,
+    open_tr=None,
+) -> int:
     if opts is None:
         opts = {}
     m = matcher
@@ -1303,7 +1358,7 @@
 
     rejected = []
 
-    def badfn(f, msg):
+    def badfn(f: bytes, msg: bytes) -> None:
         if f in m.files():
             m.bad(f, msg)
         rejected.append(f)
@@ -1341,7 +1396,7 @@
     return ret
 
 
-def marktouched(repo, files, similarity=0.0):
+def marktouched(repo, files, similarity: float = 0.0) -> int:
     """Assert that files have somehow been operated upon. files are relative to
     the repo root."""
     m = matchfiles(repo, files, badfn=lambda x, y: rejected.append(x))
@@ -1376,7 +1431,9 @@
     return 0
 
 
-def _interestingfiles(repo, matcher):
+def _interestingfiles(
+    repo, matcher
+) -> Tuple[List[bytes], List[bytes], List[bytes], List[bytes], List[bytes]]:
     """Walk dirstate with matcher, looking for files that addremove would care
     about.
 
@@ -1412,7 +1469,9 @@
     return added, unknown, deleted, removed, forgotten
 
 
-def _findrenames(repo, matcher, added, removed, similarity, uipathfn):
+def _findrenames(
+    repo, matcher, added, removed, similarity, uipathfn: typelib.UiPathFn
+) -> Dict[bytes, bytes]:
     '''Find renames from removed files to added ones.'''
     renames = {}
     if similarity > 0:
@@ -1435,7 +1494,7 @@
     return renames
 
 
-def _markchanges(repo, unknown, deleted, renames):
+def _markchanges(repo, unknown, deleted, renames) -> None:
     """Marks the files in unknown as added, the files in deleted as removed,
     and the files in renames as copied."""
     wctx = repo[None]
@@ -1518,7 +1577,15 @@
     return copiesfn
 
 
-def dirstatecopy(ui, repo, wctx, src, dst, dryrun=False, cwd=None):
+def dirstatecopy(
+    ui: "uimod.ui",
+    repo,
+    wctx,
+    src,
+    dst,
+    dryrun: bool = False,
+    cwd: Optional[bytes] = None,
+) -> None:
     """Update the dirstate to reflect the intent of copying src to dst. For
     different reasons it might not end with dst being marked as copied from src.
     """
@@ -1543,7 +1610,7 @@
             wctx.copy(origsrc, dst)
 
 
-def movedirstate(repo, newctx, match=None):
+def movedirstate(repo, newctx, match=None) -> None:
     """Move the dirstate to newctx and adjust it as necessary.
 
     A matcher can be provided as an optimization. It is probably a bug to pass
@@ -1596,12 +1663,12 @@
     return requirements, None
 
 
-def istreemanifest(repo):
+def istreemanifest(repo) -> bool:
     """returns whether the repository is using treemanifest or not"""
     return requirementsmod.TREEMANIFEST_REQUIREMENT in repo.requirements
 
 
-def writereporequirements(repo, requirements=None):
+def writereporequirements(repo, requirements=None, maywritestore=True) -> None:
     """writes requirements for the repo
 
     Requirements are written to .hg/requires and .hg/store/requires based
@@ -1614,20 +1681,41 @@
     if wcreq is not None:
         writerequires(repo.vfs, wcreq)
     if storereq is not None:
-        writerequires(repo.svfs, storereq)
+        writerequires(repo.svfs, storereq, maywrite=maywritestore)
     elif repo.ui.configbool(b'format', b'usestore'):
         # only remove store requires if we are using store
-        repo.svfs.tryunlink(b'requires')
-
-
-def writerequires(opener, requirements):
-    with opener(b'requires', b'w', atomictemp=True) as fp:
-        for r in sorted(requirements):
-            fp.write(b"%s\n" % r)
+        if maywritestore:
+            repo.svfs.tryunlink(b'requires')
+
+
+def readrequires(vfs, allowmissing):
+    """reads the require file present at root of this vfs
+    and return a set of requirements
+
+    If allowmissing is True, we suppress FileNotFoundError if raised"""
+    # requires file contains a newline-delimited list of
+    # features/capabilities the opener (us) must have in order to use
+    # 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.
+    read = vfs.tryread if allowmissing else vfs.read
+    return set(read(b'requires').splitlines())
+
+
+def writerequires(opener, requirements, maywrite=True) -> None:
+    on_disk = readrequires(opener, True)
+    if not (on_disk == set(requirements)):
+        if not maywrite:
+            raise error.Abort(_(b"store requirements are not as expected"))
+        with opener(b'requires', b'w', atomictemp=True) as fp:
+            for r in sorted(requirements):
+                fp.write(b"%s\n" % r)
 
 
 class filecachesubentry:
-    def __init__(self, path, stat):
+    _cacheable: Optional[bool] = None
+
+    def __init__(self, path, stat: bool):
         self.path = path
         self.cachestat = None
         self._cacheable = None
@@ -1641,18 +1729,18 @@
                 # None means we don't know yet
                 self._cacheable = None
 
-    def refresh(self):
+    def refresh(self) -> None:
         if self.cacheable():
             self.cachestat = filecachesubentry.stat(self.path)
 
-    def cacheable(self):
+    def cacheable(self) -> bool:
         if self._cacheable is not None:
             return self._cacheable
 
         # we don't know yet, assume it is for now
         return True
 
-    def changed(self):
+    def changed(self) -> bool:
         # no point in going further if we can't cache it
         if not self.cacheable():
             return True
@@ -1674,7 +1762,7 @@
             return False
 
     @staticmethod
-    def stat(path):
+    def stat(path: bytes) -> Optional[typelib.CacheStat]:
         try:
             return util.cachestat(path)
         except FileNotFoundError:
@@ -1682,19 +1770,19 @@
 
 
 class filecacheentry:
-    def __init__(self, paths, stat=True):
+    def __init__(self, paths, stat: bool = True) -> None:
         self._entries = []
         for path in paths:
             self._entries.append(filecachesubentry(path, stat))
 
-    def changed(self):
+    def changed(self) -> bool:
         '''true if any entry has changed'''
         for entry in self._entries:
             if entry.changed():
                 return True
         return False
 
-    def refresh(self):
+    def refresh(self) -> None:
         for entry in self._entries:
             entry.refresh()
 
@@ -1725,13 +1813,15 @@
     remove the ``filecacheentry``.
     """
 
-    def __init__(self, *paths):
+    paths: Tuple[bytes, ...]
+
+    def __init__(self, *paths: bytes) -> None:
         self.paths = paths
 
     def tracked_paths(self, obj):
         return [self.join(obj, path) for path in self.paths]
 
-    def join(self, obj, fname):
+    def join(self, obj, fname: bytes):
         """Used to compute the runtime path of a cached file.
 
         Users should subclass filecache and provide their own version of this
@@ -1792,7 +1882,7 @@
         obj.__dict__[self.sname] = value  # update copy returned by obj.x
 
 
-def extdatasource(repo, source):
+def extdatasource(repo, source: bytes):
     """Gather a map of rev -> value dict from the specified source
 
     A source spec is treated as a URL, with a special case shell: type
@@ -1861,7 +1951,21 @@
 
 
 class progress:
-    def __init__(self, ui, updatebar, topic, unit=b"", total=None):
+    ui: "uimod.ui"
+    pos: Optional[int]  # None once complete
+    topic: bytes
+    unit: bytes
+    total: Optional[int]
+    debug: bool
+
+    def __init__(
+        self,
+        ui: "uimod.ui",
+        updatebar,
+        topic: bytes,
+        unit: bytes = b"",
+        total: Optional[int] = None,
+    ) -> None:
         self.ui = ui
         self.pos = 0
         self.topic = topic
@@ -1876,7 +1980,9 @@
     def __exit__(self, exc_type, exc_value, exc_tb):
         self.complete()
 
-    def update(self, pos, item=b"", total=None):
+    def update(
+        self, pos: int, item: bytes = b"", total: Optional[int] = None
+    ) -> None:
         assert pos is not None
         if total:
             self.total = total
@@ -1885,16 +1991,18 @@
         if self.debug:
             self._printdebug(item)
 
-    def increment(self, step=1, item=b"", total=None):
+    def increment(
+        self, step: int = 1, item: bytes = b"", total: Optional[int] = None
+    ) -> None:
         self.update(self.pos + step, item, total)
 
-    def complete(self):
+    def complete(self) -> None:
         self.pos = None
         self.unit = b""
         self.total = None
         self._updatebar(self.topic, self.pos, b"", self.unit, self.total)
 
-    def _printdebug(self, item):
+    def _printdebug(self, item: bytes) -> None:
         unit = b''
         if self.unit:
             unit = b' ' + self.unit
@@ -1911,7 +2019,7 @@
             self.ui.debug(b'%s:%s %d%s\n' % (self.topic, item, self.pos, unit))
 
 
-def gdinitconfig(ui):
+def gdinitconfig(ui: "uimod.ui"):
     """helper function to know if a repo should be created as general delta"""
     # experimental config: format.generaldelta
     return ui.configbool(b'format', b'generaldelta') or ui.configbool(
@@ -1919,7 +2027,7 @@
     )
 
 
-def gddeltaconfig(ui):
+def gddeltaconfig(ui: "uimod.ui"):
     """helper function to know if incoming deltas should be optimized
 
     The `format.generaldelta` config is an old form of the config that also
@@ -1938,11 +2046,11 @@
 
     firstlinekey = b'__firstline'
 
-    def __init__(self, vfs, path, keys=None):
+    def __init__(self, vfs, path: bytes, keys=None) -> None:
         self.vfs = vfs
         self.path = path
 
-    def read(self, firstlinenonkeyval=False):
+    def read(self, firstlinenonkeyval: bool = False):
         """Read the contents of a simple key-value file
 
         'firstlinenonkeyval' indicates whether the first line of file should
@@ -1973,7 +2081,7 @@
             raise error.CorruptedState(stringutil.forcebytestr(e))
         return d
 
-    def write(self, data, firstline=None):
+    def write(self, data, firstline: Optional[bytes] = None) -> None:
         """Write key=>value mapping to a file
         data is a dict. Keys must be alphanumerical and start with a letter.
         Values must not contain newline characters.
@@ -2002,7 +2110,7 @@
             fp.write(b''.join(lines))
 
 
-_reportobsoletedsource = [
+_reportobsoletedsource: List[bytes] = [
     b'debugobsolete',
     b'pull',
     b'push',
@@ -2010,13 +2118,13 @@
     b'unbundle',
 ]
 
-_reportnewcssource = [
+_reportnewcssource: List[bytes] = [
     b'pull',
     b'unbundle',
 ]
 
 
-def prefetchfiles(repo, revmatches):
+def prefetchfiles(repo, revmatches) -> None:
     """Invokes the registered file prefetch functions, allowing extensions to
     ensure the corresponding files are available locally, before the command
     uses them.
@@ -2045,10 +2153,12 @@
 fileprefetchhooks = util.hooks()
 
 # A marker that tells the evolve extension to suppress its own reporting
-_reportstroubledchangesets = True
-
-
-def registersummarycallback(repo, otr, txnname=b'', as_validator=False):
+_reportstroubledchangesets: bool = True
+
+
+def registersummarycallback(
+    repo, otr, txnname: bytes = b'', as_validator: bool = False
+) -> None:
     """register a callback to issue a summary after the transaction is closed
 
     If as_validator is true, then the callbacks are registered as transaction
@@ -2219,7 +2329,7 @@
             repo.ui.status(msg % len(published))
 
 
-def getinstabilitymessage(delta, instability):
+def getinstabilitymessage(delta: int, instability: bytes) -> Optional[bytes]:
     """function to return the message to show warning about new instabilities
 
     exists as a separate function so that extension can wrap to show more
@@ -2228,14 +2338,14 @@
         return _(b'%i new %s changesets\n') % (delta, instability)
 
 
-def nodesummaries(repo, nodes, maxnumnodes=4):
+def nodesummaries(repo, nodes, maxnumnodes: int = 4) -> bytes:
     if len(nodes) <= maxnumnodes or repo.ui.verbose:
         return b' '.join(short(h) for h in nodes)
     first = b' '.join(short(h) for h in nodes[:maxnumnodes])
     return _(b"%s and %d others") % (first, len(nodes) - maxnumnodes)
 
 
-def enforcesinglehead(repo, tr, desc, accountclosed, filtername):
+def enforcesinglehead(repo, tr, desc: bytes, accountclosed, filtername) -> None:
     """check that no named branch has multiple heads"""
     if desc in (b'strip', b'repair'):
         # skip the logic during strip
@@ -2260,7 +2370,7 @@
     return sink
 
 
-def unhidehashlikerevs(repo, specs, hiddentype):
+def unhidehashlikerevs(repo, specs, hiddentype: bytes):
     """parse the user specs and unhide changesets whose hash or revision number
     is passed.
 
@@ -2313,7 +2423,7 @@
     return repo.filtered(b'visible-hidden', revs)
 
 
-def _getrevsfromsymbols(repo, symbols):
+def _getrevsfromsymbols(repo, symbols) -> Set[int]:
     """parse the list of symbols and returns a set of revision numbers of hidden
     changesets present in symbols"""
     revs = set()
@@ -2348,7 +2458,7 @@
     return revs
 
 
-def bookmarkrevs(repo, mark):
+def bookmarkrevs(repo, mark: bytes):
     """Select revisions reachable by a given bookmark
 
     If the bookmarked revision isn't a head, an empty set will be returned.
@@ -2356,7 +2466,7 @@
     return repo.revs(format_bookmark_revspec(mark))
 
 
-def format_bookmark_revspec(mark):
+def format_bookmark_revspec(mark: bytes) -> bytes:
     """Build a revset expression to select revisions reachable by a given
     bookmark"""
     mark = b'literal:' + mark
@@ -2370,7 +2480,7 @@
     )
 
 
-def ismember(ui, username, userlist):
+def ismember(ui: "uimod.ui", username: bytes, userlist: List[bytes]) -> bool:
     """Check if username is a member of userlist.
 
     If userlist has a single '*' member, all users are considered members.
@@ -2380,22 +2490,24 @@
     return userlist == [b'*'] or username in userlist
 
 
-RESOURCE_HIGH = 3
-RESOURCE_MEDIUM = 2
-RESOURCE_LOW = 1
-RESOURCE_DEFAULT = 0
-
-RESOURCE_MAPPING = {
+RESOURCE_HIGH: int = 3
+RESOURCE_MEDIUM: int = 2
+RESOURCE_LOW: int = 1
+RESOURCE_DEFAULT: int = 0
+
+RESOURCE_MAPPING: Dict[bytes, int] = {
     b'default': RESOURCE_DEFAULT,
     b'low': RESOURCE_LOW,
     b'medium': RESOURCE_MEDIUM,
     b'high': RESOURCE_HIGH,
 }
 
-DEFAULT_RESOURCE = RESOURCE_MEDIUM
-
-
-def get_resource_profile(ui, dimension=None):
+DEFAULT_RESOURCE: int = RESOURCE_MEDIUM
+
+
+def get_resource_profile(
+    ui: "uimod.ui", dimension: Optional[bytes] = None
+) -> int:
     """return the resource profile for a dimension
 
     If no dimension is specified, the generic value is returned"""
--- a/mercurial/setdiscovery.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/setdiscovery.py	Fri Sep 06 02:12:19 2024 +0200
@@ -190,7 +190,6 @@
         return getparents
 
     def _childrengetter(self):
-
         if self._childrenmap is not None:
             # During discovery, the `undecided` set keep shrinking.
             # Therefore, the map computed for an iteration N will be
@@ -454,7 +453,6 @@
     full = not initial_head_exchange
     progress = ui.makeprogress(_(b'searching'), unit=_(b'queries'))
     while not disco.iscomplete():
-
         if full or disco.hasinfo():
             if full:
                 ui.note(_(b"sampling from both directions\n"))
--- a/mercurial/shelve.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/shelve.py	Fri Sep 06 02:12:19 2024 +0200
@@ -26,6 +26,16 @@
 import itertools
 import stat
 
+from typing import (
+    Any,
+    Dict,
+    Iterable,
+    Iterator,
+    List,
+    Sequence,
+    Tuple,
+)
+
 from .i18n import _
 from .node import (
     bin,
@@ -37,6 +47,7 @@
     bundle2,
     changegroup,
     cmdutil,
+    context as contextmod,
     discovery,
     error,
     exchange,
@@ -69,16 +80,16 @@
 
 
 class ShelfDir:
-    def __init__(self, repo, for_backups=False):
+    def __init__(self, repo, for_backups: bool = False) -> None:
         if for_backups:
             self.vfs = vfsmod.vfs(repo.vfs.join(backupdir))
         else:
             self.vfs = vfsmod.vfs(repo.vfs.join(shelvedir))
 
-    def get(self, name):
+    def get(self, name: bytes) -> "Shelf":
         return Shelf(self.vfs, name)
 
-    def listshelves(self):
+    def listshelves(self) -> List[Tuple[float, bytes]]:
         """return all shelves in repo as list of (time, name)"""
         try:
             names = self.vfs.listdir()
@@ -99,14 +110,14 @@
         return sorted(info, reverse=True)
 
 
-def _use_internal_phase(repo):
+def _use_internal_phase(repo) -> bool:
     return (
         phases.supportinternal(repo)
         and repo.ui.config(b'shelve', b'store') == b'internal'
     )
 
 
-def _target_phase(repo):
+def _target_phase(repo) -> int:
     return phases.internal if _use_internal_phase(repo) else phases.secret
 
 
@@ -118,29 +129,29 @@
     differences and lets you work with the shelf as a whole.
     """
 
-    def __init__(self, vfs, name):
+    def __init__(self, vfs: vfsmod.vfs, name: bytes) -> None:
         self.vfs = vfs
         self.name = name
 
-    def exists(self):
+    def exists(self) -> bool:
         return self._exists(b'.shelve') or self._exists(b'.patch', b'.hg')
 
-    def _exists(self, *exts):
+    def _exists(self, *exts: bytes) -> bool:
         return all(self.vfs.exists(self.name + ext) for ext in exts)
 
-    def mtime(self):
+    def mtime(self) -> float:
         try:
             return self._stat(b'.shelve')[stat.ST_MTIME]
         except FileNotFoundError:
             return self._stat(b'.patch')[stat.ST_MTIME]
 
-    def _stat(self, ext):
+    def _stat(self, ext: bytes):
         return self.vfs.stat(self.name + ext)
 
-    def writeinfo(self, info):
+    def writeinfo(self, info) -> None:
         scmutil.simplekeyvaluefile(self.vfs, self.name + b'.shelve').write(info)
 
-    def hasinfo(self):
+    def hasinfo(self) -> bool:
         return self.vfs.exists(self.name + b'.shelve')
 
     def readinfo(self):
@@ -148,7 +159,7 @@
             self.vfs, self.name + b'.shelve'
         ).read()
 
-    def writebundle(self, repo, bases, node):
+    def writebundle(self, repo, bases, node) -> None:
         cgversion = changegroup.safeversion(repo)
         if cgversion == b'01':
             btype = b'HG10BZ'
@@ -174,7 +185,7 @@
             compression=compression,
         )
 
-    def applybundle(self, repo, tr):
+    def applybundle(self, repo, tr) -> contextmod.changectx:
         filename = self.name + b'.hg'
         fp = self.vfs(filename)
         try:
@@ -197,10 +208,10 @@
         finally:
             fp.close()
 
-    def open_patch(self, mode=b'rb'):
+    def open_patch(self, mode: bytes = b'rb'):
         return self.vfs(self.name + b'.patch', mode)
 
-    def patch_from_node(self, repo, node):
+    def patch_from_node(self, repo, node) -> io.BytesIO:
         repo = repo.unfiltered()
         match = _optimized_match(repo, node)
         fp = io.BytesIO()
@@ -221,8 +232,8 @@
         except (FileNotFoundError, error.RepoLookupError):
             return self.open_patch()
 
-    def _backupfilename(self, backupvfs, filename):
-        def gennames(base):
+    def _backupfilename(self, backupvfs: vfsmod.vfs, filename: bytes) -> bytes:
+        def gennames(base: bytes):
             yield base
             base, ext = base.rsplit(b'.', 1)
             for i in itertools.count(1):
@@ -232,7 +243,10 @@
             if not backupvfs.exists(n):
                 return backupvfs.join(n)
 
-    def movetobackup(self, backupvfs):
+        # Help pytype- gennames() yields infinitely
+        raise error.ProgrammingError("unreachable")
+
+    def movetobackup(self, backupvfs: vfsmod.vfs) -> None:
         if not backupvfs.isdir():
             backupvfs.makedir()
         for suffix in shelvefileextensions:
@@ -243,7 +257,7 @@
                     self._backupfilename(backupvfs, filename),
                 )
 
-    def delete(self):
+    def delete(self) -> None:
         for ext in shelvefileextensions:
             self.vfs.tryunlink(self.name + b'.' + ext)
 
@@ -256,7 +270,7 @@
             return patch.changedfiles(ui, repo, filename)
 
 
-def _optimized_match(repo, node):
+def _optimized_match(repo, node: bytes):
     """
     Create a matcher so that prefetch doesn't attempt to fetch
     the entire repository pointlessly, and as an optimisation
@@ -272,6 +286,7 @@
     versions of a shelved state are possible and handles them appropriately.
     """
 
+    # Class-wide constants
     _version = 2
     _filename = b'shelvedstate'
     _keep = b'keep'
@@ -280,8 +295,19 @@
     _noactivebook = b':no-active-bookmark'
     _interactive = b'interactive'
 
+    # Per instance attrs
+    name: bytes
+    wctx: contextmod.workingctx
+    pendingctx: contextmod.changectx
+    parents: List[bytes]
+    nodestoremove: List[bytes]
+    branchtorestore: bytes
+    keep: bool
+    activebookmark: bytes
+    interactive: bool
+
     @classmethod
-    def _verifyandtransform(cls, d):
+    def _verifyandtransform(cls, d: Dict[bytes, Any]) -> None:
         """Some basic shelvestate syntactic verification and transformation"""
         try:
             d[b'originalwctx'] = bin(d[b'originalwctx'])
@@ -294,7 +320,7 @@
             raise error.CorruptedState(stringutil.forcebytestr(err))
 
     @classmethod
-    def _getversion(cls, repo):
+    def _getversion(cls, repo) -> int:
         """Read version information from shelvestate file"""
         fp = repo.vfs(cls._filename)
         try:
@@ -306,7 +332,7 @@
         return version
 
     @classmethod
-    def _readold(cls, repo):
+    def _readold(cls, repo) -> Dict[bytes, Any]:
         """Read the old position-based version of a shelvestate file"""
         # Order is important, because old shelvestate file uses it
         # to detemine values of fields (i.g. name is on the second line,
@@ -365,7 +391,7 @@
                 obj.activebookmark = d.get(b'activebook', b'')
             obj.interactive = d.get(b'interactive') == cls._interactive
         except (error.RepoLookupError, KeyError) as err:
-            raise error.CorruptedState(pycompat.bytestr(err))
+            raise error.CorruptedState(stringutil.forcebytestr(err))
 
         return obj
 
@@ -373,15 +399,15 @@
     def save(
         cls,
         repo,
-        name,
-        originalwctx,
-        pendingctx,
-        nodestoremove,
-        branchtorestore,
-        keep=False,
-        activebook=b'',
-        interactive=False,
-    ):
+        name: bytes,
+        originalwctx: contextmod.workingctx,
+        pendingctx: contextmod.changectx,
+        nodestoremove: List[bytes],
+        branchtorestore: bytes,
+        keep: bool = False,
+        activebook: bytes = b'',
+        interactive: bool = False,
+    ) -> None:
         info = {
             b"name": name,
             b"originalwctx": hex(originalwctx.node()),
@@ -399,11 +425,11 @@
         )
 
     @classmethod
-    def clear(cls, repo):
+    def clear(cls, repo) -> None:
         repo.vfs.unlinkpath(cls._filename, ignoremissing=True)
 
 
-def cleanupoldbackups(repo):
+def cleanupoldbackups(repo) -> None:
     maxbackups = repo.ui.configint(b'shelve', b'maxbackups')
     backup_dir = ShelfDir(repo, for_backups=True)
     hgfiles = backup_dir.listshelves()
@@ -418,19 +444,19 @@
         backup_dir.get(name).delete()
 
 
-def _backupactivebookmark(repo):
+def _backupactivebookmark(repo) -> bytes:
     activebookmark = repo._activebookmark
     if activebookmark:
         bookmarks.deactivate(repo)
     return activebookmark
 
 
-def _restoreactivebookmark(repo, mark):
+def _restoreactivebookmark(repo, mark) -> None:
     if mark:
         bookmarks.activate(repo, mark)
 
 
-def _aborttransaction(repo, tr):
+def _aborttransaction(repo, tr) -> None:
     """Abort current transaction for shelve/unshelve, but keep dirstate"""
     # disable the transaction invalidation of the dirstate, to preserve the
     # current change in memory.
@@ -456,7 +482,7 @@
     ds.setbranch(current_branch, None)
 
 
-def getshelvename(repo, parent, opts):
+def getshelvename(repo, parent, opts) -> bytes:
     """Decide on the name this shelve is going to have"""
 
     def gennames():
@@ -496,7 +522,7 @@
     return name
 
 
-def mutableancestors(ctx):
+def mutableancestors(ctx) -> Iterator[bytes]:
     """return all mutable ancestors for ctx (included)
 
     Much faster than the revset ancestors(ctx) & draft()"""
@@ -514,7 +540,7 @@
                     visit.append(parent)
 
 
-def getcommitfunc(extra, interactive, editor=False):
+def getcommitfunc(extra, interactive: bool, editor: bool = False):
     def commitfunc(ui, repo, message, match, opts):
         hasmq = hasattr(repo, 'mq')
         if hasmq:
@@ -550,7 +576,7 @@
     return interactivecommitfunc if interactive else commitfunc
 
 
-def _nothingtoshelvemessaging(ui, repo, pats, opts):
+def _nothingtoshelvemessaging(ui, repo, pats, opts) -> None:
     stat = repo.status(match=scmutil.match(repo[None], pats, opts))
     if stat.deleted:
         ui.status(
@@ -561,7 +587,7 @@
         ui.status(_(b"nothing changed\n"))
 
 
-def _shelvecreatedcommit(repo, node, name, match):
+def _shelvecreatedcommit(repo, node: bytes, name: bytes, match) -> None:
     info = {b'node': hex(node)}
     shelf = ShelfDir(repo).get(name)
     shelf.writeinfo(info)
@@ -573,14 +599,14 @@
         )
 
 
-def _includeunknownfiles(repo, pats, opts, extra):
+def _includeunknownfiles(repo, pats, opts, extra) -> None:
     s = repo.status(match=scmutil.match(repo[None], pats, opts), unknown=True)
     if s.unknown:
         extra[b'shelve_unknown'] = b'\0'.join(s.unknown)
         repo[None].add(s.unknown)
 
 
-def _finishshelve(repo, tr):
+def _finishshelve(repo, tr) -> None:
     if _use_internal_phase(repo):
         tr.close()
     else:
@@ -675,7 +701,7 @@
         lockmod.release(tr, lock)
 
 
-def _isbareshelve(pats, opts):
+def _isbareshelve(pats, opts) -> bool:
     return (
         not pats
         and not opts.get(b'interactive', False)
@@ -684,11 +710,11 @@
     )
 
 
-def _iswctxonnewbranch(repo):
+def _iswctxonnewbranch(repo) -> bool:
     return repo[None].branch() != repo[b'.'].branch()
 
 
-def cleanupcmd(ui, repo):
+def cleanupcmd(ui, repo) -> None:
     """subcommand that deletes all shelves"""
 
     with repo.wlock():
@@ -699,7 +725,7 @@
             cleanupoldbackups(repo)
 
 
-def deletecmd(ui, repo, pats):
+def deletecmd(ui, repo, pats) -> None:
     """subcommand that deletes a specific shelve"""
     if not pats:
         raise error.InputError(_(b'no shelved changes specified!'))
@@ -715,7 +741,7 @@
             cleanupoldbackups(repo)
 
 
-def listcmd(ui, repo, pats, opts):
+def listcmd(ui, repo, pats: Iterable[bytes], opts) -> None:
     """subcommand that displays the list of shelves"""
     pats = set(pats)
     width = 80
@@ -762,7 +788,7 @@
                     ui.write(chunk, label=label)
 
 
-def patchcmds(ui, repo, pats, opts):
+def patchcmds(ui, repo, pats: Sequence[bytes], opts) -> None:
     """subcommand that displays shelves"""
     shelf_dir = ShelfDir(repo)
     if len(pats) == 0:
@@ -779,7 +805,7 @@
     listcmd(ui, repo, pats, opts)
 
 
-def checkparents(repo, state):
+def checkparents(repo, state: shelvedstate) -> None:
     """check parent while resuming an unshelve"""
     if state.parents != repo.dirstate.parents():
         raise error.Abort(
@@ -787,11 +813,12 @@
         )
 
 
-def _loadshelvedstate(ui, repo, opts):
+def _loadshelvedstate(ui, repo, opts) -> shelvedstate:
     try:
         state = shelvedstate.load(repo)
         if opts.get(b'keep') is None:
             opts[b'keep'] = state.keep
+        return state
     except FileNotFoundError:
         cmdutil.wrongtooltocontinue(repo, _(b'unshelve'))
     except error.CorruptedState as err:
@@ -812,10 +839,13 @@
                     b'please update to some commit\n'
                 )
             )
-    return state
+        else:
+            raise error.ProgrammingError(
+                "a corrupted shelvedstate exists without --abort or --continue"
+            )
 
 
-def unshelveabort(ui, repo, state):
+def unshelveabort(ui, repo, state: shelvedstate) -> None:
     """subcommand that abort an in-progress unshelve"""
     with repo.lock():
         try:
@@ -834,14 +864,14 @@
             ui.warn(_(b"unshelve of '%s' aborted\n") % state.name)
 
 
-def hgabortunshelve(ui, repo):
+def hgabortunshelve(ui, repo) -> None:
     """logic to  abort unshelve using 'hg abort"""
     with repo.wlock():
         state = _loadshelvedstate(ui, repo, {b'abort': True})
         return unshelveabort(ui, repo, state)
 
 
-def mergefiles(ui, repo, wctx, shelvectx):
+def mergefiles(ui, repo, wctx, shelvectx) -> None:
     """updates to wctx and merges the changes from shelvectx into the
     dirstate."""
     with ui.configoverride({(b'ui', b'quiet'): True}):
@@ -849,7 +879,7 @@
         cmdutil.revert(ui, repo, shelvectx)
 
 
-def restorebranch(ui, repo, branchtorestore):
+def restorebranch(ui, repo, branchtorestore: bytes) -> None:
     if branchtorestore and branchtorestore != repo.dirstate.branch():
         repo.dirstate.setbranch(branchtorestore, repo.currenttransaction())
         ui.status(
@@ -857,7 +887,7 @@
         )
 
 
-def unshelvecleanup(ui, repo, name, opts):
+def unshelvecleanup(ui, repo, name: bytes, opts) -> None:
     """remove related files after an unshelve"""
     if not opts.get(b'keep'):
         backupvfs = vfsmod.vfs(repo.vfs.join(backupdir))
@@ -865,7 +895,7 @@
         cleanupoldbackups(repo)
 
 
-def unshelvecontinue(ui, repo, state, opts):
+def unshelvecontinue(ui, repo, state: shelvedstate, opts) -> None:
     """subcommand to continue an in-progress unshelve"""
     # We're finishing off a merge. First parent is our original
     # parent, second is the temporary "fake" commit we're unshelving.
@@ -923,7 +953,7 @@
         ui.status(_(b"unshelve of '%s' complete\n") % state.name)
 
 
-def hgcontinueunshelve(ui, repo):
+def hgcontinueunshelve(ui, repo) -> None:
     """logic to resume unshelve using 'hg continue'"""
     with repo.wlock():
         state = _loadshelvedstate(ui, repo, {b'continue': True})
@@ -955,7 +985,7 @@
     return tmpwctx, addedbefore
 
 
-def _unshelverestorecommit(ui, repo, tr, basename):
+def _unshelverestorecommit(ui, repo, tr, basename: bytes):
     """Recreate commit in the repository during the unshelve"""
     repo = repo.unfiltered()
     node = None
@@ -976,7 +1006,9 @@
     return repo, shelvectx
 
 
-def _createunshelvectx(ui, repo, shelvectx, basename, interactive, opts):
+def _createunshelvectx(
+    ui, repo, shelvectx, basename: bytes, interactive: bool, opts
+) -> Tuple[bytes, bool]:
     """Handles the creation of unshelve commit and updates the shelve if it
     was partially unshelved.
 
@@ -1038,7 +1070,7 @@
     opts,
     tr,
     oldtiprev,
-    basename,
+    basename: bytes,
     pctx,
     tmpwctx,
     shelvectx,
@@ -1109,7 +1141,7 @@
     return shelvectx, ispartialunshelve
 
 
-def _forgetunknownfiles(repo, shelvectx, addedbefore):
+def _forgetunknownfiles(repo, shelvectx, addedbefore) -> None:
     # Forget any files that were unknown before the shelve, unknown before
     # unshelve started, but are now added.
     shelveunknown = shelvectx.extra().get(b'shelve_unknown')
@@ -1121,7 +1153,7 @@
     repo[None].forget(toforget)
 
 
-def _finishunshelve(repo, oldtiprev, tr, activebookmark):
+def _finishunshelve(repo, oldtiprev, tr, activebookmark) -> None:
     _restoreactivebookmark(repo, activebookmark)
     # We used to manually strip the commit to update inmemory structure and
     # prevent some issue around hooks. This no longer seems to be the case, so
@@ -1129,7 +1161,7 @@
     _aborttransaction(repo, tr)
 
 
-def _checkunshelveuntrackedproblems(ui, repo, shelvectx):
+def _checkunshelveuntrackedproblems(ui, repo, shelvectx) -> None:
     """Check potential problems which may result from working
     copy having untracked changes."""
     wcdeleted = set(repo.status().deleted)
@@ -1141,7 +1173,7 @@
         raise error.Abort(m, hint=hint)
 
 
-def unshelvecmd(ui, repo, *shelved, **opts):
+def unshelvecmd(ui, repo, *shelved, **opts) -> None:
     opts = pycompat.byteskwargs(opts)
     abortf = opts.get(b'abort')
     continuef = opts.get(b'continue')
@@ -1178,6 +1210,11 @@
             )
         elif continuef:
             return unshelvecontinue(ui, repo, state, opts)
+        else:
+            # Unreachable code, but help type checkers not think that
+            # 'basename' may be used before initialization when checking
+            # ShelfDir below.
+            raise error.ProgrammingError("neither abort nor continue specified")
     elif len(shelved) > 1:
         raise error.InputError(_(b'can only unshelve one change at a time'))
     elif not shelved:
@@ -1195,7 +1232,7 @@
     return _dounshelve(ui, repo, basename, opts)
 
 
-def _dounshelve(ui, repo, basename, opts):
+def _dounshelve(ui, repo, basename: bytes, opts) -> None:
     repo = repo.unfiltered()
     lock = tr = None
     try:
--- a/mercurial/similar.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/similar.py	Fri Sep 06 02:12:19 2024 +0200
@@ -118,14 +118,14 @@
 
     # Find exact matches.
     matchedfiles = set()
-    for (a, b) in _findexactmatches(repo, addedfiles, removedfiles):
+    for a, b in _findexactmatches(repo, addedfiles, removedfiles):
         matchedfiles.add(b)
         yield (a.path(), b.path(), 1.0)
 
     # If the user requested similar files to be matched, search for them also.
     if threshold < 1.0:
         addedfiles = [x for x in addedfiles if x not in matchedfiles]
-        for (a, b, score) in _findsimilarmatches(
+        for a, b, score in _findsimilarmatches(
             repo, addedfiles, removedfiles, threshold
         ):
             yield (a.path(), b.path(), score)
--- a/mercurial/sparse.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/sparse.py	Fri Sep 06 02:12:19 2024 +0200
@@ -612,7 +612,7 @@
     repo, includes, excludes, profiles, force=False, removing=False
 ):
     """Update the sparse config and working directory state."""
-    with repo.lock():
+    with repo.wlock():
         raw = repo.vfs.tryread(b'sparse')
         oldincludes, oldexcludes, oldprofiles = parseconfig(
             repo.ui, raw, b'sparse'
@@ -632,10 +632,10 @@
 
         if requirements.SPARSE_REQUIREMENT in oldrequires and removing:
             repo.requirements.discard(requirements.SPARSE_REQUIREMENT)
-            scmutil.writereporequirements(repo)
+            scmutil.writereporequirements(repo, maywritestore=False)
         elif requirements.SPARSE_REQUIREMENT not in oldrequires:
             repo.requirements.add(requirements.SPARSE_REQUIREMENT)
-            scmutil.writereporequirements(repo)
+            scmutil.writereporequirements(repo, maywritestore=False)
 
         try:
             writeconfig(repo, includes, excludes, profiles)
@@ -644,7 +644,7 @@
             if repo.requirements != oldrequires:
                 repo.requirements.clear()
                 repo.requirements |= oldrequires
-                scmutil.writereporequirements(repo)
+                scmutil.writereporequirements(repo, maywritestore=False)
             writeconfig(repo, oldincludes, oldexcludes, oldprofiles)
             raise
 
@@ -730,7 +730,7 @@
 
     The new config is written out and a working directory refresh is performed.
     """
-    with repo.wlock(), repo.lock(), repo.dirstate.changing_parents(repo):
+    with repo.wlock(), repo.dirstate.changing_parents(repo):
         raw = repo.vfs.tryread(b'sparse')
         oldinclude, oldexclude, oldprofiles = parseconfig(
             repo.ui, raw, b'sparse'
--- a/mercurial/sslutil.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/sslutil.py	Fri Sep 06 02:12:19 2024 +0200
@@ -497,7 +497,6 @@
                     )
 
             elif e.reason == 'CERTIFICATE_VERIFY_FAILED' and pycompat.iswindows:
-
                 ui.warn(
                     _(
                         b'(the full certificate chain may not be available '
--- a/mercurial/statichttprepo.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/statichttprepo.py	Fri Sep 06 02:12:19 2024 +0200
@@ -160,6 +160,8 @@
 ):
     supported = localrepo.localrepository._basesupported
 
+    manifestlog: manifest.ManifestLog
+
     def __init__(self, ui, path):
         self._url = path
         self.ui = ui
--- a/mercurial/statprof.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/statprof.py	Fri Sep 06 02:12:19 2024 +0200
@@ -113,6 +113,10 @@
 import threading
 import time
 
+from typing import (
+    List,
+)
+
 from .pycompat import open
 from . import (
     encoding,
@@ -155,6 +159,8 @@
 
 
 class ProfileState:
+    samples: List["Sample"]
+
     def __init__(self, frequency=None):
         self.reset(frequency)
         self.track = b'cpu'
--- a/mercurial/store.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/store.py	Fri Sep 06 02:12:19 2024 +0200
@@ -10,10 +10,18 @@
 import os
 import re
 import stat
+import typing
+
 from typing import Generator, List
 
 from .i18n import _
 from .thirdparty import attr
+
+# Force pytype to use the non-vendored package
+if typing.TYPE_CHECKING:
+    # noinspection PyPackageRequirements
+    import attr
+
 from .node import hex
 from .revlogutils.constants import (
     INDEX_HEADER,
@@ -37,10 +45,10 @@
 parsers = policy.importmod('parsers')
 # how much bytes should be read from fncache in one read
 # It is done to prevent loading large fncache files into memory
-fncache_chunksize = 10 ** 6
+fncache_chunksize = 10**6
 
 
-def _match_tracked_entry(entry, matcher):
+def _match_tracked_entry(entry: "BaseStoreEntry", matcher):
     """parses a fncache entry and returns whether the entry is tracking a path
     matched by matcher or not.
 
@@ -48,10 +56,16 @@
 
     if matcher is None:
         return True
+
+    # TODO: make this safe for other entry types.  Currently, the various
+    #  store.data_entry generators only yield  RevlogStoreEntry, so the
+    #  attributes do exist on `entry`.
+    # pytype: disable=attribute-error
     if entry.is_filelog:
         return matcher(entry.target_id)
     elif entry.is_manifestlog:
         return matcher.visitdir(entry.target_id.rstrip(b'/'))
+    # pytype: enable=attribute-error
     raise error.ProgrammingError(b"cannot process entry %r" % entry)
 
 
@@ -803,7 +817,7 @@
             concurrencychecker=concurrencychecker,
         )
 
-    def manifestlog(self, repo, storenarrowmatch):
+    def manifestlog(self, repo, storenarrowmatch) -> manifest.ManifestLog:
         rootstore = manifest.manifestrevlog(repo.nodeconstants, self.vfs)
         return manifest.manifestlog(self.vfs, repo, rootstore, storenarrowmatch)
 
--- a/mercurial/streamclone.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/streamclone.py	Fri Sep 06 02:12:19 2024 +0200
@@ -547,6 +547,7 @@
 _srcstore = b's'  # store (svfs)
 _srccache = b'c'  # cache (cache)
 
+
 # This is it's own function so extensions can override it.
 def _walkstreamfullstorefiles(repo):
     """list snapshot file from the store"""
@@ -809,7 +810,6 @@
     """
 
     with repo.lock():
-
         repo.ui.debug(b'scanning\n')
 
         entries = _entries_walk(
@@ -857,7 +857,6 @@
     # considering the files to preserve, disabling the gc while we do so helps
     # performance a lot.
     with repo.lock(), util.nogc():
-
         repo.ui.debug(b'scanning\n')
 
         entries = _entries_walk(
@@ -990,7 +989,6 @@
         with repo.transaction(b'clone'):
             ctxs = (vfs.backgroundclosing(repo.ui) for vfs in vfsmap.values())
             with nested(*ctxs):
-
                 for i in range(entrycount):
                     filecount = util.uvarintdecodestream(fp)
                     if filecount == 0:
@@ -1123,7 +1121,6 @@
 
     with dest_repo.lock():
         with src_repo.lock():
-
             # bookmark is not integrated to the streaming as it might use the
             # `repo.vfs` and they are too many sentitive data accessible
             # through `repo.vfs` to expose it to streaming clone.
--- a/mercurial/strip.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/strip.py	Fri Sep 06 02:12:19 2024 +0200
@@ -63,7 +63,6 @@
     soft=False,
 ):
     with repo.wlock(), repo.lock():
-
         if update:
             checklocalchanges(repo, force=force)
             urev = _findupdatetarget(repo, revs)
--- a/mercurial/subrepo.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/subrepo.py	Fri Sep 06 02:12:19 2024 +0200
@@ -363,11 +363,10 @@
         """handle the files command for this subrepo"""
         return 1
 
-    def archive(self, archiver, prefix, match=None, decode=True):
-        if match is not None:
-            files = [f for f in self.files() if match(f)]
-        else:
-            files = self.files()
+    def archive(
+        self, archiver, prefix, match: matchmod.basematcher, decode=True
+    ):
+        files = [f for f in self.files() if match(f)]
         total = len(files)
         relpath = subrelpath(self)
         progress = self.ui.makeprogress(
@@ -652,11 +651,11 @@
             )
 
     @annotatesubrepoerror
-    def archive(self, archiver, prefix, match=None, decode=True):
+    def archive(
+        self, archiver, prefix, match: matchmod.basematcher, decode=True
+    ):
         self._get(self._state + (b'hg',))
-        files = self.files()
-        if match:
-            files = [f for f in files if match(f)]
+        files = [f for f in self.files() if match(f)]
         rev = self._state[1]
         ctx = self._repo[rev]
         scmutil.prefetchfiles(
@@ -1227,16 +1226,12 @@
                 externals.append(path)
             elif item == 'missing':
                 missing.append(path)
-            if (
-                item
-                not in (
-                    '',
-                    'normal',
-                    'unversioned',
-                    'external',
-                )
-                or props not in ('', 'none', 'normal')
-            ):
+            if item not in (
+                '',
+                'normal',
+                'unversioned',
+                'external',
+            ) or props not in ('', 'none', 'normal'):
                 changes.append(path)
         for path in changes:
             for ext in externals:
@@ -1915,7 +1910,9 @@
             else:
                 self.wvfs.unlink(f)
 
-    def archive(self, archiver, prefix, match=None, decode=True):
+    def archive(
+        self, archiver, prefix, match: matchmod.basematcher, decode=True
+    ):
         total = 0
         source, revision = self._state
         if not revision:
@@ -1936,7 +1933,7 @@
             if info.isdir():
                 continue
             bname = pycompat.fsencode(info.name)
-            if match and not match(bname):
+            if not match(bname):
                 continue
             if info.issym():
                 data = info.linkname
--- a/mercurial/subrepoutil.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/subrepoutil.py	Fri Sep 06 02:12:19 2024 +0200
@@ -131,7 +131,7 @@
             # extra escapes are needed because re.sub string decodes.
             repl = re.sub(br'\\\\([0-9]+)', br'\\\1', repl)
             try:
-                src = re.sub(pattern, repl, src, 1)
+                src = re.sub(pattern, repl, src, count=1)
             except re.error as e:
                 raise error.Abort(
                     _(b"bad subrepository pattern in %s: %s")
--- a/mercurial/tags.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/tags.py	Fri Sep 06 02:12:19 2024 +0200
@@ -601,7 +601,7 @@
     # we keep them in UTF-8 throughout this module.  If we converted
     # them local encoding on input, we would lose info writing them to
     # the cache.
-    for (name, (node, hist)) in sorted(cachetags.items()):
+    for name, (node, hist) in sorted(cachetags.items()):
         for n in hist:
             cachefile.write(b"%s %s\n" % (hex(n), name))
         cachefile.write(b"%s %s\n" % (hex(node), name))
@@ -851,25 +851,45 @@
         rev = ctx.rev()
         fnode = None
         cl = self._repo.changelog
+        ml = self._repo.manifestlog
+        mctx = ctx.manifestctx()
+        base_values = {}
         p1rev, p2rev = cl._uncheckedparentrevs(rev)
-        p1node = cl.node(p1rev)
-        p1fnode = self.getfnode(p1node, computemissing=False)
+        m_p1_node, m_p2_node = mctx.parents
+        if p1rev != nullrev:
+            p1_node = cl.node(p1rev)
+            fnode = self.getfnode(p1_node, computemissing=False)
+            # when unknown, fnode is None or False
+            if fnode:
+                p1_manifest_rev = ml.rev(m_p1_node)
+                base_values[p1_manifest_rev] = fnode
         if p2rev != nullrev:
-            # There is some no-merge changeset where p1 is null and p2 is set
-            # Processing them as merge is just slower, but still gives a good
-            # result.
-            p2node = cl.node(p2rev)
-            p2fnode = self.getfnode(p2node, computemissing=False)
-            if p1fnode != p2fnode:
-                # we cannot rely on readfast because we don't know against what
-                # parent the readfast delta is computed
-                p1fnode = None
-        if p1fnode:
-            mctx = ctx.manifestctx()
-            fnode = mctx.readfast().get(b'.hgtags')
+            p2_node = cl.node(p2rev)
+            fnode = self.getfnode(p2_node, computemissing=False)
+            # when unknown, fnode is None or False
+            if fnode:
+                p2_manifest_rev = ml.rev(m_p2_node)
+                base_values[p2_manifest_rev] = fnode
+        # XXX: Beware that using delta to speed things up here is actually
+        # buggy as it will fails to detect a `.hgtags` deletion. That buggy
+        # behavior has been cargo culted from the previous version of this code
+        # as "in practice this seems fine" and not using delta is just too
+        # slow.
+        #
+        # However note that we only consider delta from p1 or p2 because it is
+        # far less likely to have a .hgtags delete in a child than missing from
+        # one branch to another. As the delta chain construction keep being
+        # optimized, it means we will not use delta as often as we could.
+        if base_values:
+            base, m = mctx.read_any_fast_delta(base_values)
+            fnode = m.get(b'.hgtags')
             if fnode is None:
-                fnode = p1fnode
-        if fnode is None:
+                if base is not None:
+                    fnode = base_values[base]
+                else:
+                    # No delta and .hgtags file on this revision.
+                    fnode = self._repo.nullid
+        else:
             # Populate missing entry.
             try:
                 fnode = ctx.filenode(b'.hgtags')
--- a/mercurial/templatekw.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/templatekw.py	Fri Sep 06 02:12:19 2024 +0200
@@ -482,7 +482,7 @@
     return showlatesttags(context, mapping, None)
 
 
-def showlatesttags(context, mapping, pattern):
+def showlatesttags(context, mapping, pattern) -> _hybrid:
     """helper method for the latesttag keyword and function"""
     latesttags = getlatesttags(context, mapping, pattern)
 
--- a/mercurial/templater.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/templater.py	Fri Sep 06 02:12:19 2024 +0200
@@ -69,6 +69,12 @@
 import abc
 import os
 
+from typing import (
+    BinaryIO,
+    Optional,
+    Tuple,
+)
+
 from .i18n import _
 from .pycompat import (
     FileNotFoundError,
@@ -1121,7 +1127,9 @@
     return path if os.path.isdir(path) else None
 
 
-def open_template(name, templatepath=None):
+def open_template(
+    name: bytes, templatepath: Optional[bytes] = None
+) -> Tuple[bytes, BinaryIO]:
     """returns a file-like object for the given template, and its full path
 
     If the name is a relative path and we're in a frozen binary, the template
@@ -1156,7 +1164,9 @@
     )
 
 
-def try_open_template(name, templatepath=None):
+def try_open_template(
+    name: bytes, templatepath: Optional[bytes] = None
+) -> Tuple[Optional[bytes], Optional[BinaryIO]]:
     try:
         return open_template(name, templatepath)
     except (EnvironmentError, ImportError):
--- a/mercurial/typelib.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/typelib.py	Fri Sep 06 02:12:19 2024 +0200
@@ -7,6 +7,10 @@
 
 import typing
 
+from typing import (
+    Callable,
+)
+
 # Note: this is slightly different from pycompat.TYPE_CHECKING, as using
 # pycompat causes the BinaryIO_Proxy type to be resolved to ``object`` when
 # used as the base class during a pytype run.
@@ -21,8 +25,24 @@
 if TYPE_CHECKING:
     from typing import (
         BinaryIO,
+        Union,
+    )
+
+    from . import (
+        node,
+        posix,
+        windows,
     )
 
     BinaryIO_Proxy = BinaryIO
+    CacheStat = Union[posix.cachestat, windows.cachestat]
+    NodeConstants = node.sha1nodeconstants
 else:
+    from typing import Any
+
     BinaryIO_Proxy = object
+    CacheStat = Any
+    NodeConstants = Any
+
+# scmutil.getuipathfn() related callback.
+UiPathFn = Callable[[bytes], bytes]
--- a/mercurial/unionrepo.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/unionrepo.py	Fri Sep 06 02:12:19 2024 +0200
@@ -204,6 +204,9 @@
 
 
 class unionmanifest(unionrevlog, manifest.manifestrevlog):
+    repotiprev: int
+    revlog2: manifest.ManifestRevlog
+
     def __init__(self, nodeconstants, opener, opener2, linkmapper):
         # XXX manifestrevlog is not actually a revlog , so mixing it with
         # bundlerevlog is not a good idea.
@@ -215,6 +218,10 @@
 
 
 class unionfilelog(filelog.filelog):
+    _revlog: unionrevlog
+    repotiprev: int
+    revlog2: revlog.revlog
+
     def __init__(self, opener, path, opener2, linkmapper, repo):
         filelog.filelog.__init__(self, opener, path)
         filelog2 = filelog.filelog(opener2, path)
--- a/mercurial/upgrade.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/upgrade.py	Fri Sep 06 02:12:19 2024 +0200
@@ -194,7 +194,6 @@
                 onlydefault.append(d)
 
         if fromconfig or onlydefault:
-
             if fromconfig:
                 ui.status(
                     _(
@@ -309,7 +308,7 @@
 ):
     """Upgrades a share to use share-safe mechanism"""
     wlock = None
-    store_requirements = localrepo._readrequires(storevfs, False)
+    store_requirements = scmutil.readrequires(storevfs, False)
     original_crequirements = current_requirements.copy()
     # after upgrade, store requires will be shared, so lets find
     # the requirements which are not present in store and
@@ -326,7 +325,7 @@
         wlock = lockmod.trylock(ui, hgvfs, b'wlock', 0, 0)
         # some process might change the requirement in between, re-read
         # and update current_requirements
-        locked_requirements = localrepo._readrequires(hgvfs, True)
+        locked_requirements = scmutil.readrequires(hgvfs, True)
         if locked_requirements != original_crequirements:
             removed = current_requirements - locked_requirements
             # update current_requirements in place because it's passed
@@ -372,7 +371,7 @@
 ):
     """Downgrades a share which use share-safe to not use it"""
     wlock = None
-    source_requirements = localrepo._readrequires(sharedvfs, True)
+    source_requirements = scmutil.readrequires(sharedvfs, True)
     original_crequirements = current_requirements.copy()
     # we cannot be 100% sure on which requirements were present in store when
     # the source supported share-safe. However, we do know that working
@@ -387,7 +386,7 @@
         wlock = lockmod.trylock(ui, hgvfs, b'wlock', 0, 0)
         # some process might change the requirement in between, re-read
         # and update current_requirements
-        locked_requirements = localrepo._readrequires(hgvfs, True)
+        locked_requirements = scmutil.readrequires(hgvfs, True)
         if locked_requirements != original_crequirements:
             removed = current_requirements - locked_requirements
             # update current_requirements in place because it's passed
--- a/mercurial/url.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/url.py	Fri Sep 06 02:12:19 2024 +0200
@@ -231,36 +231,29 @@
         return keepalive.HTTPHandler._start_transaction(self, h, req)
 
 
-class logginghttpconnection(keepalive.HTTPConnection):
-    def __init__(self, createconn, *args, **kwargs):
-        keepalive.HTTPConnection.__init__(self, *args, **kwargs)
-        self._create_connection = createconn
-
+class logginghttphandler(httphandler):
+    """HTTP(S) handler that logs socket I/O."""
 
-class logginghttphandler(httphandler):
-    """HTTP handler that logs socket I/O."""
-
-    def __init__(self, logfh, name, observeropts, timeout=None):
-        super(logginghttphandler, self).__init__(timeout=timeout)
+    def __init__(self, logfh, name, observeropts, *args, **kwargs):
+        super().__init__(*args, **kwargs)
 
         self._logfh = logfh
         self._logname = name
         self._observeropts = observeropts
 
-    # do_open() calls the passed class to instantiate an HTTPConnection. We
-    # pass in a callable method that creates a custom HTTPConnection instance
-    # whose callback to create the socket knows how to proxy the socket.
-    def http_open(self, req):
-        return self.do_open(self._makeconnection, req)
+    def do_open(self, http_class, *args, **kwargs):
+        _logfh = self._logfh
+        _logname = self._logname
+        _observeropts = self._observeropts
 
-    def _makeconnection(self, *args, **kwargs):
-        def createconnection(*args, **kwargs):
-            sock = socket.create_connection(*args, **kwargs)
-            return util.makeloggingsocket(
-                self._logfh, sock, self._logname, **self._observeropts
-            )
+        class logginghttpconnection(http_class):
+            def connect(self):
+                super().connect()
+                self.sock = util.makeloggingsocket(
+                    _logfh, self.sock, _logname, **_observeropts
+                )
 
-        return logginghttpconnection(createconnection, *args, **kwargs)
+        return super().do_open(logginghttpconnection, *args, **kwargs)
 
 
 if has_https:
@@ -462,41 +455,47 @@
             return None
 
 
-class cookiehandler(urlreq.basehandler):
-    def __init__(self, ui):
-        self.cookiejar = None
-
-        cookiefile = ui.config(b'auth', b'cookiefile')
-        if not cookiefile:
-            return
-
-        cookiefile = util.expandpath(cookiefile)
-        try:
-            cookiejar = util.cookielib.MozillaCookieJar(
-                pycompat.fsdecode(cookiefile)
+def load_cookiejar(ui):
+    cookiefile = ui.config(b'auth', b'cookiefile')
+    if not cookiefile:
+        return
+    cookiefile = util.expandpath(cookiefile)
+    try:
+        cookiejar = util.cookielib.MozillaCookieJar(
+            pycompat.fsdecode(cookiefile)
+        )
+        cookiejar.load()
+        return cookiejar
+    except util.cookielib.LoadError as e:
+        ui.warn(
+            _(
+                b'(error loading cookie file %s: %s; continuing without '
+                b'cookies)\n'
             )
-            cookiejar.load()
-            self.cookiejar = cookiejar
-        except util.cookielib.LoadError as e:
-            ui.warn(
-                _(
-                    b'(error loading cookie file %s: %s; continuing without '
-                    b'cookies)\n'
-                )
-                % (cookiefile, stringutil.forcebytestr(e))
-            )
+            % (cookiefile, stringutil.forcebytestr(e))
+        )
+
 
-    def http_request(self, request):
-        if self.cookiejar:
-            self.cookiejar.add_cookie_header(request)
-
-        return request
+class readlinehandler(urlreq.basehandler):
+    def http_response(self, request, response):
+        class readlineresponse(response.__class__):
+            def readlines(self, sizehint=0):
+                total = 0
+                list = []
+                while True:
+                    line = self.readline()
+                    if not line:
+                        break
+                    list.append(line)
+                    total += len(line)
+                    if sizehint and total >= sizehint:
+                        break
+                return list
 
-    def https_request(self, request):
-        if self.cookiejar:
-            self.cookiejar.add_cookie_header(request)
+        response.__class__ = readlineresponse
+        return response
 
-        return request
+    https_response = http_response
 
 
 handlerfuncs = []
@@ -535,16 +534,13 @@
                 loggingfh, loggingname, loggingopts or {}, timeout=timeout
             )
         )
-        # We don't yet support HTTPS when logging I/O. If we attempt to open
-        # an HTTPS URL, we'll likely fail due to unknown protocol.
-
     else:
         handlers.append(httphandler(timeout=timeout))
-        if has_https:
-            # pytype get confused about the conditional existence for httpshandler here.
-            handlers.append(
-                httpshandler(ui, timeout=timeout)  # pytype: disable=name-error
-            )
+    if has_https:
+        # pytype get confused about the conditional existence for httpshandler here.
+        handlers.append(
+            httpshandler(ui, timeout=timeout)  # pytype: disable=name-error
+        )
 
     handlers.append(proxyhandler(ui))
 
@@ -563,7 +559,8 @@
         (httpbasicauthhandler(passmgr), httpdigestauthhandler(passmgr))
     )
     handlers.extend([h(ui, passmgr) for h in handlerfuncs])
-    handlers.append(cookiehandler(ui))
+    handlers.append(urlreq.httpcookieprocessor(cookiejar=load_cookiejar(ui)))
+    handlers.append(readlinehandler())
     opener = urlreq.buildopener(*handlers)
 
     # keepalive.py's handlers will populate these attributes if they exist.
--- a/mercurial/urllibcompat.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/urllibcompat.py	Fri Sep 06 02:12:19 2024 +0200
@@ -68,6 +68,7 @@
         b"FileHandler",
         b"FTPHandler",
         b"ftpwrapper",
+        b"HTTPCookieProcessor",
         b"HTTPHandler",
         b"HTTPSHandler",
         b"install_opener",
@@ -109,6 +110,7 @@
     ),
 )
 
+
 # urllib.parse.quote() accepts both str and bytes, decodes bytes
 # (if necessary), and returns str. This is wonky. We provide a custom
 # implementation that only accepts bytes and emits bytes.
--- a/mercurial/util.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/util.py	Fri Sep 06 02:12:19 2024 +0200
@@ -32,10 +32,13 @@
 import sys
 import time
 import traceback
+import typing
 import warnings
 
 from typing import (
     Any,
+    BinaryIO,
+    Callable,
     Iterable,
     Iterator,
     List,
@@ -45,6 +48,12 @@
 
 from .node import hex
 from .thirdparty import attr
+
+# Force pytype to use the non-vendored package
+if typing.TYPE_CHECKING:
+    # noinspection PyPackageRequirements
+    import attr
+
 from .pycompat import (
     open,
 )
@@ -55,6 +64,7 @@
     i18n,
     policy,
     pycompat,
+    typelib,
     urllibcompat,
 )
 from .utils import (
@@ -440,13 +450,24 @@
         return data
 
 
-def mmapread(fp, size=None):
+def has_mmap_populate():
+    return hasattr(osutil, "background_mmap_populate") or hasattr(
+        mmap, 'MAP_POPULATE'
+    )
+
+
+def mmapread(fp, size=None, pre_populate=True):
     """Read a file content using mmap
 
     The responsability of checking the file system is mmap safe is the
-    responsability of the caller.
+    responsability of the caller (see `vfs.is_mmap_safe`).
 
     In some case, a normal string might be returned.
+
+    If `pre_populate` is True (the default), the mmapped data will be
+    pre-populated in memory if the system support this option, this slow down
+    the initial mmaping but avoid potentially crippling page fault on later
+    access. If this is not the desired behavior, set `pre_populate` to False.
     """
     if size == 0:
         # size of 0 to mmap.mmap() means "all data"
@@ -455,8 +476,15 @@
     elif size is None:
         size = 0
     fd = getattr(fp, 'fileno', lambda: fp)()
+    flags = mmap.MAP_PRIVATE
+    bg_populate = hasattr(osutil, "background_mmap_populate")
+    if pre_populate and not bg_populate:
+        flags |= getattr(mmap, 'MAP_POPULATE', 0)
     try:
-        return mmap.mmap(fd, size, access=mmap.ACCESS_READ)
+        m = mmap.mmap(fd, size, flags=flags, prot=mmap.PROT_READ)
+        if pre_populate and bg_populate:
+            osutil.background_mmap_populate(m)
+        return m
     except ValueError:
         # Empty files cannot be mmapped, but mmapread should still work.  Check
         # if the file is empty, and if so, return an empty buffer.
@@ -1139,7 +1167,7 @@
 def version():
     """Return version information if available."""
     try:
-        from . import __version__
+        from . import __version__  # pytype: disable=import-error
 
         return __version__.version
     except ImportError:
@@ -1315,7 +1343,7 @@
                 self[k] = f[k]
 
     def insert(self, position, key, value):
-        for (i, (k, v)) in enumerate(list(self.items())):
+        for i, (k, v) in enumerate(list(self.items())):
             if i == position:
                 self[key] = value
             if i >= position:
@@ -2404,7 +2432,12 @@
     return path.split(pycompat.ossep)
 
 
-def mktempcopy(name, emptyok=False, createmode=None, enforcewritable=False):
+def mktempcopy(
+    name: bytes,
+    emptyok: bool = False,
+    createmode: Optional[int] = None,
+    enforcewritable: bool = False,
+) -> bytes:
     """Create a temporary file with the same contents from name
 
     The permission bits are copied from the original file.
@@ -2711,10 +2744,10 @@
 
         def splitbig(chunks):
             for chunk in chunks:
-                if len(chunk) > 2 ** 20:
+                if len(chunk) > 2**20:
                     pos = 0
                     while pos < len(chunk):
-                        end = pos + 2 ** 18
+                        end = pos + 2**18
                         yield chunk[pos:end]
                         pos = end
                 else:
@@ -2738,7 +2771,7 @@
         while left > 0:
             # refill the queue
             if not queue:
-                target = 2 ** 18
+                target = 2**18
                 for chunk in self.iter:
                     queue.append(chunk)
                     target -= len(chunk)
@@ -2894,20 +2927,20 @@
 )
 
 
-class transformingwriter:
+class transformingwriter(typelib.BinaryIO_Proxy):
     """Writable file wrapper to transform data by function"""
 
-    def __init__(self, fp, encode):
+    def __init__(self, fp: BinaryIO, encode: Callable[[bytes], bytes]) -> None:
         self._fp = fp
         self._encode = encode
 
-    def close(self):
+    def close(self) -> None:
         self._fp.close()
 
-    def flush(self):
+    def flush(self) -> None:
         self._fp.flush()
 
-    def write(self, data):
+    def write(self, data: bytes) -> int:
         return self._fp.write(self._encode(data))
 
 
@@ -2925,7 +2958,7 @@
     return _eolre.sub(b'\r\n', s)
 
 
-def _crlfwriter(fp):
+def _crlfwriter(fp: typelib.BinaryIO_Proxy) -> typelib.BinaryIO_Proxy:
     return transformingwriter(fp, tocrlf)
 
 
@@ -2938,6 +2971,21 @@
     fromnativeeol = pycompat.identity
     nativeeolwriter = pycompat.identity
 
+if typing.TYPE_CHECKING:
+    # Replace the various overloads that come along with aliasing other methods
+    # with the narrow definition that we care about in the type checking phase
+    # only.  This ensures that both Windows and POSIX see only the definition
+    # that is actually available.
+
+    def tonativeeol(s: bytes) -> bytes:
+        raise NotImplementedError
+
+    def fromnativeeol(s: bytes) -> bytes:
+        raise NotImplementedError
+
+    def nativeeolwriter(fp: typelib.BinaryIO_Proxy) -> typelib.BinaryIO_Proxy:
+        raise NotImplementedError
+
 
 # TODO delete since workaround variant for Python 2 no longer needed.
 def iterfile(fp):
@@ -3068,12 +3116,12 @@
 
 
 _sizeunits = (
-    (b'm', 2 ** 20),
-    (b'k', 2 ** 10),
-    (b'g', 2 ** 30),
-    (b'kb', 2 ** 10),
-    (b'mb', 2 ** 20),
-    (b'gb', 2 ** 30),
+    (b'm', 2**20),
+    (b'k', 2**10),
+    (b'g', 2**30),
+    (b'kb', 2**10),
+    (b'mb', 2**20),
+    (b'gb', 2**30),
     (b'b', 1),
 )
 
--- a/mercurial/utils/compression.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/utils/compression.py	Fri Sep 06 02:12:19 2024 +0200
@@ -511,7 +511,7 @@
                 parts = []
                 pos = 0
                 while pos < insize:
-                    pos2 = pos + 2 ** 20
+                    pos2 = pos + 2**20
                     parts.append(z.compress(data[pos:pos2]))
                     pos = pos2
                 parts.append(z.flush())
--- a/mercurial/utils/dateutil.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/utils/dateutil.py	Fri Sep 06 02:12:19 2024 +0200
@@ -83,7 +83,37 @@
 
 def makedate(timestamp: Optional[float] = None) -> hgdate:
     """Return a unix timestamp (or the current time) as a (unixtime,
-    offset) tuple based off the local timezone."""
+    offset) tuple based off the local timezone.
+
+    >>> import os, time
+    >>> os.environ['TZ'] = 'Asia/Novokuznetsk'
+    >>> time.tzset()
+
+    >>> def dtu(*a):
+    ...    return datetime.datetime(*a, tzinfo=datetime.timezone.utc)
+
+    # Old winter timezone, +7
+    >>> makedate(dtu(2010,  1,  1,  5,  0,  0).timestamp())
+    (1262322000.0, -25200)
+
+    # Same timezone in summer, +7, so no DST
+    >>> makedate(dtu(2010,  7,  1,  5,  0,  0).timestamp())
+    (1277960400.0, -25200)
+
+    # Changing to new winter timezone, from +7 to +6 (ae04af1ce78d testcase)
+    >>> makedate(dtu(2010, 10, 30, 20,  0,  0).timestamp() - 1)
+    (1288468799.0, -25200)
+    >>> makedate(dtu(2010, 10, 30, 20,  0,  0).timestamp())
+    (1288468800.0, -21600)
+    >>> makedate(dtu(2011,  1,  1,  5,  0,  0).timestamp())
+    (1293858000.0, -21600)
+
+    # Introducing DST, changing +6 to +7
+    >>> makedate(dtu(2011,  3, 26, 20,  0,  0).timestamp() - 1)
+    (1301169599.0, -21600)
+    >>> makedate(dtu(2011,  3, 26, 20,  0,  0).timestamp())
+    (1301169600.0, -25200)
+    """
     if timestamp is None:
         timestamp = time.time()
     if timestamp < 0:
--- a/mercurial/utils/procutil.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/utils/procutil.py	Fri Sep 06 02:12:19 2024 +0200
@@ -711,7 +711,6 @@
             if stdin is not None:
                 stdin.close()
 
-
 else:
 
     def runbgcommand(
--- a/mercurial/utils/resourceutil.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/utils/resourceutil.py	Fri Sep 06 02:12:19 2024 +0200
@@ -8,13 +8,20 @@
 # GNU General Public License version 2 or any later version.
 
 
-import _imp
 import os
 import sys
+import typing
 
 from .. import pycompat
 
 
+if typing.TYPE_CHECKING:
+    from typing import (
+        BinaryIO,
+        Iterator,
+    )
+
+
 def mainfrozen():
     """return True if we are a frozen executable.
 
@@ -24,7 +31,12 @@
     return (
         hasattr(sys, "frozen")  # new py2exe
         or hasattr(sys, "importers")  # old py2exe
-        or _imp.is_frozen("__main__")  # tools/freeze
+        or getattr(
+            getattr(sys.modules.get('__main__'), '__spec__', None),
+            'origin',
+            None,
+        )
+        == 'frozen'  # tools/freeze
     )
 
 
@@ -39,17 +51,16 @@
     # leading "mercurial." off of the package name, so that these
     # pseudo resources are found in their directory next to the
     # executable.
-    def _package_path(package):
+    def _package_path(package: bytes) -> bytes:
         dirs = package.split(b".")
         assert dirs[0] == b"mercurial"
         return os.path.join(_rootpath, *dirs[1:])
 
-
 else:
     datapath = os.path.dirname(os.path.dirname(pycompat.fsencode(__file__)))
     _rootpath = os.path.dirname(datapath)
 
-    def _package_path(package):
+    def _package_path(package: bytes) -> bytes:
         return os.path.join(_rootpath, *package.split(b"."))
 
 
@@ -59,7 +70,7 @@
     from importlib import resources  # pytype: disable=import-error
 
     # Force loading of the resources module
-    if hasattr(resources, 'files'):
+    if hasattr(resources, 'files'):  # Introduced in Python 3.9
         resources.files  # pytype: disable=module-attr
     else:
         resources.open_binary  # pytype: disable=module-attr
@@ -72,11 +83,11 @@
     # importlib.resources was not found (almost definitely because we're on a
     # Python version before 3.7)
 
-    def open_resource(package, name):
+    def open_resource(package: bytes, name: bytes) -> "BinaryIO":
         path = os.path.join(_package_path(package), name)
         return open(path, "rb")
 
-    def is_resource(package, name):
+    def is_resource(package: bytes, name: bytes) -> bool:
         path = os.path.join(_package_path(package), name)
 
         try:
@@ -84,17 +95,16 @@
         except (IOError, OSError):
             return False
 
-    def contents(package):
+    def contents(package: bytes) -> "Iterator[bytes]":
         path = pycompat.fsdecode(_package_path(package))
 
         for p in os.listdir(path):
             yield pycompat.fsencode(p)
 
-
 else:
     from .. import encoding
 
-    def open_resource(package, name):
+    def open_resource(package: bytes, name: bytes) -> "BinaryIO":
         if hasattr(resources, 'files'):
             return (
                 resources.files(  # pytype: disable=module-attr
@@ -108,13 +118,25 @@
                 pycompat.sysstr(package), pycompat.sysstr(name)
             )
 
-    def is_resource(package, name):
-        return resources.is_resource(  # pytype: disable=module-attr
-            pycompat.sysstr(package), encoding.strfromlocal(name)
-        )
+    def is_resource(package: bytes, name: bytes) -> bool:
+        if hasattr(resources, 'files'):  # Introduced in Python 3.9
+            return (
+                resources.files(pycompat.sysstr(package))
+                .joinpath(encoding.strfromlocal(name))
+                .is_file()
+            )
+        else:
+            return resources.is_resource(  # pytype: disable=module-attr
+                pycompat.sysstr(package), encoding.strfromlocal(name)
+            )
 
-    def contents(package):
-        # pytype: disable=module-attr
-        for r in resources.contents(pycompat.sysstr(package)):
-            # pytype: enable=module-attr
-            yield encoding.strtolocal(r)
+    def contents(package: bytes) -> "Iterator[bytes]":
+        if hasattr(resources, 'files'):  # Introduced in Python 3.9
+            for path in resources.files(pycompat.sysstr(package)).iterdir():
+                if path.is_file():
+                    yield encoding.strtolocal(path.name)
+        else:
+            # pytype: disable=module-attr
+            for r in resources.contents(pycompat.sysstr(package)):
+                # pytype: enable=module-attr
+                yield encoding.strtolocal(r)
--- a/mercurial/utils/stringutil.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/utils/stringutil.py	Fri Sep 06 02:12:19 2024 +0200
@@ -13,6 +13,7 @@
 import re as remod
 import textwrap
 import types
+import typing
 
 from typing import (
     Optional,
@@ -22,6 +23,11 @@
 from ..i18n import _
 from ..thirdparty import attr
 
+# Force pytype to use the non-vendored package
+if typing.TYPE_CHECKING:
+    # noinspection PyPackageRequirements
+    import attr
+
 from .. import (
     encoding,
     error,
@@ -574,7 +580,6 @@
         return mailmap
 
     for line in mailmapcontent.splitlines():
-
         # Don't bother checking the line if it is a comment or
         # is an improperly formed author field
         if line.lstrip().startswith(b'#'):
@@ -719,7 +724,7 @@
 
 def escapestr(s: bytes) -> bytes:
     # "bytes" is also a typing shortcut for bytes, bytearray, and memoryview
-    if isinstance(s, memoryview):
+    if isinstance(s, (memoryview, bytearray)):
         s = bytes(s)
     # call underlying function of s.encode('string_escape') directly for
     # Python 3 compatibility
@@ -801,7 +806,6 @@
             chunks.reverse()
 
             while chunks:
-
                 # Start the list of chunks that will make up the current line.
                 # cur_len is just the length of all the chunks in cur_line.
                 cur_line = []
--- a/mercurial/utils/urlutil.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/utils/urlutil.py	Fri Sep 06 02:12:19 2024 +0200
@@ -9,6 +9,9 @@
 import socket
 
 from typing import (
+    Callable,
+    Dict,
+    Tuple,
     Union,
 )
 
@@ -29,7 +32,7 @@
 )
 
 # keeps pyflakes happy
-assert [Union]
+assert [Callable, Dict, Tuple, Union]
 
 urlreq = urllibcompat.urlreq
 
@@ -652,12 +655,12 @@
             self[name] = new_paths
 
 
-_pathsuboptions = {}
+_pathsuboptions: "Dict[bytes, Tuple[str, Callable]]" = {}
 # a dictionnary of methods that can be used to format a sub-option value
 path_suboptions_display = {}
 
 
-def pathsuboption(option, attr, display=pycompat.bytestr):
+def pathsuboption(option: bytes, attr: str, display=pycompat.bytestr):
     """Decorator used to declare a path sub-option.
 
     Arguments are the sub-option name and the attribute it should set on
--- a/mercurial/verify.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/verify.py	Fri Sep 06 02:12:19 2024 +0200
@@ -31,7 +31,7 @@
         return v.verify()
 
 
-def _normpath(f):
+def _normpath(f: bytes) -> bytes:
     # under hg < 2.4, convert didn't sanitize paths properly, so a
     # converted repo may contain repeated slashes
     while b'//' in f:
@@ -360,7 +360,7 @@
                 self._err(lr, _(b"%s not in changesets") % short(n), label)
 
             try:
-                mfdelta = mfl.get(dir, n).readdelta(shallow=True)
+                mfdelta = mfl.get(dir, n).read_delta_new_entries(shallow=True)
                 for f, fn, fl in mfdelta.iterentries():
                     if not f:
                         self._err(lr, _(b"entry without name in manifest"))
--- a/mercurial/vfs.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/vfs.py	Fri Sep 06 02:12:19 2024 +0200
@@ -291,7 +291,7 @@
 
     def rmtree(
         self, path: Optional[bytes] = None, ignore_errors=False, forcibly=False
-    ):
+    ) -> None:
         """Remove a directory tree recursively
 
         If ``forcibly``, this tries to remove READ-ONLY files, too.
--- a/mercurial/win32.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/win32.py	Fri Sep 06 02:12:19 2024 +0200
@@ -172,6 +172,7 @@
 X509_ASN_ENCODING = 0x00000001
 PKCS_7_ASN_ENCODING = 0x00010000
 
+
 # These structs are only complete enough to achieve what we need.
 class CERT_CHAIN_CONTEXT(ctypes.Structure):
     _fields_ = (
@@ -368,7 +369,7 @@
     # See https://bugs.python.org/issue28474
     code = _kernel32.GetLastError()
     if code > 0x7FFFFFFF:
-        code -= 2 ** 32
+        code -= 2**32
     err = ctypes.WinError(code=code)  # pytype: disable=module-attr
     raise OSError(
         err.errno, '%s: %s' % (encoding.strfromlocal(name), err.strerror)
@@ -459,10 +460,10 @@
     return _getfileinfo(name).nNumberOfLinks
 
 
-def samefile(path1: bytes, path2: bytes) -> bool:
-    '''Returns whether path1 and path2 refer to the same file or directory.'''
-    res1 = _getfileinfo(path1)
-    res2 = _getfileinfo(path2)
+def samefile(fpath1: bytes, fpath2: bytes) -> bool:
+    '''Returns whether fpath1 and fpath2 refer to the same file or directory.'''
+    res1 = _getfileinfo(fpath1)
+    res2 = _getfileinfo(fpath2)
     return (
         res1.dwVolumeSerialNumber == res2.dwVolumeSerialNumber
         and res1.nFileIndexHigh == res2.nFileIndexHigh
@@ -470,10 +471,10 @@
     )
 
 
-def samedevice(path1: bytes, path2: bytes) -> bool:
-    '''Returns whether path1 and path2 are on the same device.'''
-    res1 = _getfileinfo(path1)
-    res2 = _getfileinfo(path2)
+def samedevice(fpath1: bytes, fpath2: bytes) -> bool:
+    '''Returns whether fpath1 and fpath2 are on the same device.'''
+    res1 = _getfileinfo(fpath1)
+    res2 = _getfileinfo(fpath2)
     return res1.dwVolumeSerialNumber == res2.dwVolumeSerialNumber
 
 
@@ -710,16 +711,16 @@
     return pi.dwProcessId
 
 
-def unlink(f: bytes) -> None:
+def unlink(path: bytes) -> None:
     '''try to implement POSIX' unlink semantics on Windows'''
 
-    if os.path.isdir(f):
+    if os.path.isdir(path):
         # use EPERM because it is POSIX prescribed value, even though
         # unlink(2) on directories returns EISDIR on Linux
         raise IOError(
             errno.EPERM,
             r"Unlinking directory not permitted: '%s'"
-            % encoding.strfromlocal(f),
+            % encoding.strfromlocal(path),
         )
 
     # POSIX allows to unlink and rename open files. Windows has serious
@@ -740,9 +741,9 @@
     # implicit zombie filename blocking on a temporary name.
 
     for tries in range(10):
-        temp = b'%s-%08x' % (f, random.randint(0, 0xFFFFFFFF))
+        temp = b'%s-%08x' % (path, random.randint(0, 0xFFFFFFFF))
         try:
-            os.rename(f, temp)
+            os.rename(path, temp)
             break
         except FileExistsError:
             pass
--- a/mercurial/windows.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/windows.py	Fri Sep 06 02:12:19 2024 +0200
@@ -297,7 +297,7 @@
 def copymode(
     src: bytes,
     dst: bytes,
-    mode: Optional[bytes] = None,
+    mode: Optional[int] = None,
     enforcewritable: bool = False,
 ) -> None:
     pass
@@ -620,10 +620,10 @@
     return None
 
 
-def readlink(pathname: bytes) -> bytes:
-    path = pycompat.fsdecode(pathname)
+def readlink(path: bytes) -> bytes:
+    path_str = pycompat.fsdecode(path)
     try:
-        link = os.readlink(path)
+        link = os.readlink(path_str)
     except ValueError as e:
         # On py2, os.readlink() raises an AttributeError since it is
         # unsupported.  On py3, reading a non-link raises a ValueError.  Simply
@@ -675,8 +675,10 @@
 
 
 class cachestat:
+    stat: Optional[os.stat_result]
+
     def __init__(self, path: bytes) -> None:
-        pass
+        self.stat = None
 
     def cacheable(self) -> bool:
         return False
--- a/mercurial/wireprotoframing.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/wireprotoframing.py	Fri Sep 06 02:12:19 2024 +0200
@@ -12,9 +12,16 @@
 
 import collections
 import struct
+import typing
 
 from .i18n import _
 from .thirdparty import attr
+
+# Force pytype to use the non-vendored package
+if typing.TYPE_CHECKING:
+    # noinspection PyPackageRequirements
+    import attr
+
 from . import (
     encoding,
     error,
@@ -546,7 +553,7 @@
     """
     atomdicts = []
 
-    for (formatting, args, labels) in atoms:
+    for formatting, args, labels in atoms:
         # TODO look for localstr, other types here?
 
         if not isinstance(formatting, bytes):
@@ -1198,7 +1205,6 @@
                         b'%s' % stringutil.forcebytestr(e),
                         errtype=b'server',
                     ):
-
                         yield frame
 
                     break
@@ -1259,7 +1265,6 @@
                         for chunk in cborutil.streamencodebytestringfromiter(
                             o.chunks
                         ):
-
                             for frame in emitter.send(chunk):
                                 yield frame
 
--- a/mercurial/wireprototypes.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/mercurial/wireprototypes.py	Fri Sep 06 02:12:19 2024 +0200
@@ -4,12 +4,20 @@
 # GNU General Public License version 2 or any later version.
 
 
+import typing
+
 from .node import (
     bin,
     hex,
 )
 from .i18n import _
 from .thirdparty import attr
+
+# Force pytype to use the non-vendored package
+if typing.TYPE_CHECKING:
+    # noinspection PyPackageRequirements
+    import attr
+
 from . import (
     error,
     util,
--- a/rust/.cargo/config	Thu Sep 05 15:37:14 2024 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,7 +0,0 @@
-# Rust builds with a modern MSVC and uses a newer CRT.
-# Python 2.7 has a shared library dependency on an older CRT (msvcr90.dll).
-# We statically link the modern CRT to avoid multiple msvcr*.dll libraries
-# being loaded and Python possibly picking up symbols from the newer runtime
-# (which would be loaded first).
-[target.'cfg(target_os = "windows")']
-rustflags = ["-Ctarget-feature=+crt-static"]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rust/.cargo/config.toml	Fri Sep 06 02:12:19 2024 +0200
@@ -0,0 +1,7 @@
+# Rust builds with a modern MSVC and uses a newer CRT.
+# Python 2.7 has a shared library dependency on an older CRT (msvcr90.dll).
+# We statically link the modern CRT to avoid multiple msvcr*.dll libraries
+# being loaded and Python possibly picking up symbols from the newer runtime
+# (which would be loaded first).
+[target.'cfg(target_os = "windows")']
+rustflags = ["-Ctarget-feature=+crt-static"]
--- a/rust/README.rst	Thu Sep 05 15:37:14 2024 -0400
+++ b/rust/README.rst	Fri Sep 06 02:12:19 2024 +0200
@@ -83,7 +83,7 @@
 ------------------------------
 
 The minimum supported rust version (MSRV) is specified in the `Clippy`_
-configuration file at ``rust/clippy.toml``. It is set to be ``1.61.0`` as of
+configuration file at ``rust/clippy.toml``. It is set to be ``1.79.0`` as of
 this writing, but keep in mind that the authoritative value is the one
 from the configuration file.
 
--- a/rust/clippy.toml	Thu Sep 05 15:37:14 2024 -0400
+++ b/rust/clippy.toml	Fri Sep 06 02:12:19 2024 +0200
@@ -1,1 +1,1 @@
-msrv = "1.61.0"
+msrv = "1.79.0"
--- a/rust/hg-core/src/dirstate_tree/dirstate_map.rs	Thu Sep 05 15:37:14 2024 -0400
+++ b/rust/hg-core/src/dirstate_tree/dirstate_map.rs	Fri Sep 06 02:12:19 2024 +0200
@@ -427,19 +427,16 @@
     pub(super) tracked_descendants_count: u32,
 }
 
-#[derive(Debug)]
+#[derive(Debug, Default)]
 pub(super) enum NodeData {
     Entry(DirstateEntry),
-    CachedDirectory { mtime: TruncatedTimestamp },
+    CachedDirectory {
+        mtime: TruncatedTimestamp,
+    },
+    #[default]
     None,
 }
 
-impl Default for NodeData {
-    fn default() -> Self {
-        NodeData::None
-    }
-}
-
 impl NodeData {
     fn has_entry(&self) -> bool {
         matches!(self, NodeData::Entry(_))
--- a/rust/hg-core/src/dirstate_tree/on_disk.rs	Thu Sep 05 15:37:14 2024 -0400
+++ b/rust/hg-core/src/dirstate_tree/on_disk.rs	Fri Sep 06 02:12:19 2024 +0200
@@ -332,9 +332,7 @@
     ) -> Result<usize, DirstateV2ParseError> {
         let start = self.base_name_start.get();
         if start < self.full_path.len.get() {
-            let start = usize::try_from(start)
-                // u32 -> usize, could only panic on a 16-bit CPU
-                .expect("dirstate-v2 base_name_start out of bounds");
+            let start = usize::from(start);
             Ok(start)
         } else {
             Err(DirstateV2ParseError::new("not enough bytes for base name"))
@@ -593,8 +591,8 @@
 {
     // Either `usize::MAX` would result in "out of bounds" error since a single
     // `&[u8]` cannot occupy the entire addess space.
-    let start = start.get().try_into().unwrap_or(std::usize::MAX);
-    let len = len.try_into().unwrap_or(std::usize::MAX);
+    let start = start.get().try_into().unwrap_or(usize::MAX);
+    let len = len.try_into().unwrap_or(usize::MAX);
     let bytes = match on_disk.get(start..) {
         Some(bytes) => bytes,
         None => {
--- a/rust/hg-core/src/dirstate_tree/status.rs	Thu Sep 05 15:37:14 2024 -0400
+++ b/rust/hg-core/src/dirstate_tree/status.rs	Fri Sep 06 02:12:19 2024 +0200
@@ -677,14 +677,14 @@
                 // The directory was modified too recently,
                 // don’t cache its `read_dir` results.
                 //
-                // 1. A change to this directory (direct child was
-                //    added or removed) cause its mtime to be set
-                //    (possibly truncated) to `directory_mtime`
+                // 1. A change to this directory (direct child was added or
+                //    removed) cause its mtime to be set (possibly truncated)
+                //    to `directory_mtime`
                 // 2. This `status` algorithm calls `read_dir`
-                // 3. An other change is made to the same directory is
-                //    made so that calling `read_dir` agin would give
-                //    different results, but soon enough after 1. that
-                //    the mtime stays the same
+                // 3. An other change is made to the same directory is made so
+                //    that calling `read_dir` agin would give different
+                //    results, but soon enough after 1. that the mtime stays
+                //    the same
                 //
                 // On a system where the time resolution poor, this
                 // scenario is not unlikely if all three steps are caused
--- a/rust/hg-core/src/matchers.rs	Thu Sep 05 15:37:14 2024 -0400
+++ b/rust/hg-core/src/matchers.rs	Fri Sep 06 02:12:19 2024 +0200
@@ -617,7 +617,11 @@
                 std::mem::swap(&mut m1, &mut m2);
             }
             m1.file_set().map(|m1_files| {
-                m1_files.iter().cloned().filter(|f| m2.matches(f)).collect()
+                m1_files
+                    .iter()
+                    .filter(|&f| m2.matches(f))
+                    .cloned()
+                    .collect()
             })
         } else {
             // without exact input file sets, we can't do an exact
@@ -710,7 +714,7 @@
         };
         if base_is_exact {
             new.files = base_files.map(|files| {
-                files.iter().cloned().filter(|f| new.matches(f)).collect()
+                files.iter().filter(|&f| new.matches(f)).cloned().collect()
             });
         }
         new
--- a/rust/hg-core/src/revlog/changelog.rs	Thu Sep 05 15:37:14 2024 -0400
+++ b/rust/hg-core/src/revlog/changelog.rs	Fri Sep 06 02:12:19 2024 +0200
@@ -8,6 +8,7 @@
 use itertools::{Either, Itertools};
 
 use crate::errors::HgError;
+use crate::revlog::Index;
 use crate::revlog::Revision;
 use crate::revlog::{Node, NodePrefix};
 use crate::revlog::{Revlog, RevlogEntry, RevlogError};
@@ -81,6 +82,10 @@
     ) -> Result<Revision, RevlogError> {
         self.revlog.rev_from_node(node)
     }
+
+    pub fn get_index(&self) -> &Index {
+        &self.revlog.index
+    }
 }
 
 impl Graph for Changelog {
@@ -617,7 +622,7 @@
     #[test]
     fn test_unescape_nul_followed_by_octal() {
         // Escaped NUL chars followed by octal digits are decoded correctly.
-        let expected = b"\012";
+        let expected = b"\x0012";
         let escaped = br"\012";
         let unescaped = unescape_extra(escaped);
         assert_eq!(&expected[..], &unescaped[..]);
@@ -713,7 +718,7 @@
 
         for (extra, msg) in test_cases {
             assert!(
-                decode_extra(&extra).is_err(),
+                decode_extra(extra).is_err(),
                 "corrupt extra should have failed to parse: {}",
                 msg
             );
--- a/rust/hg-core/src/revlog/index.rs	Thu Sep 05 15:37:14 2024 -0400
+++ b/rust/hg-core/src/revlog/index.rs	Fri Sep 06 02:12:19 2024 +0200
@@ -1387,6 +1387,7 @@
     fn vec_of_empty(sets_size: usize, vec_len: usize) -> Vec<Self>;
 
     /// The size of the bit mask in memory
+    #[allow(unused)]
     fn size(&self) -> usize;
 
     /// The number of elements that can be represented in the set.
@@ -1394,12 +1395,14 @@
     /// Another way to put it is that it is the highest integer `C` such that
     /// the set is guaranteed to always be a subset of the integer range
     /// `[0, C)`
+    #[allow(unused)]
     fn capacity(&self) -> usize;
 
     /// Declare `n` to belong to the set
     fn add(&mut self, n: usize);
 
     /// Declare `n` not to belong to the set
+    #[allow(unused)]
     fn discard(&mut self, n: usize);
 
     /// Replace this bit set by its union with other
@@ -1749,6 +1752,9 @@
 }
 
 #[cfg(test)]
+pub use tests::IndexEntryBuilder;
+
+#[cfg(test)]
 mod tests {
     use super::*;
     use crate::node::NULL_NODE;
@@ -2027,6 +2033,3 @@
         assert_eq!(get_version(&bytes), 2)
     }
 }
-
-#[cfg(test)]
-pub use tests::IndexEntryBuilder;
--- a/rust/hg-core/src/revlog/node.rs	Thu Sep 05 15:37:14 2024 -0400
+++ b/rust/hg-core/src/revlog/node.rs	Fri Sep 06 02:12:19 2024 +0200
@@ -83,7 +83,7 @@
     #[inline]
     fn try_from(bytes: &'a [u8]) -> Result<Self, Self::Error> {
         match Node::from_bytes(bytes) {
-            Ok((node, rest)) if rest.is_empty() => Ok(node),
+            Ok((node, [])) => Ok(node),
             _ => Err(()),
         }
     }
@@ -323,6 +323,9 @@
 }
 
 #[cfg(test)]
+pub use tests::hex_pad_right;
+
+#[cfg(test)]
 mod tests {
     use super::*;
 
@@ -428,6 +431,3 @@
         assert_eq!(prefix.first_different_nybble(&node), None);
     }
 }
-
-#[cfg(test)]
-pub use tests::hex_pad_right;
--- a/rust/hg-core/tests/test_missing_ancestors.rs	Thu Sep 05 15:37:14 2024 -0400
+++ b/rust/hg-core/tests/test_missing_ancestors.rs	Fri Sep 06 02:12:19 2024 +0200
@@ -69,6 +69,7 @@
     ancs
 }
 
+#[allow(unused)] // Useful when debugging
 #[derive(Clone, Debug)]
 enum MissingAncestorsAction {
     InitialBases(HashSet<Revision>),
--- a/rust/hg-cpython/src/ancestors.rs	Thu Sep 05 15:37:14 2024 -0400
+++ b/rust/hg-cpython/src/ancestors.rs	Fri Sep 06 02:12:19 2024 +0200
@@ -19,8 +19,8 @@
 //!   `mercurial.ancestor.incrementalmissingancestors`.
 //!
 //!   API differences:
-//!    + it is instantiated with a C `parsers.index`
-//!      instance instead of a parents function.
+//!    + it is instantiated with a C `parsers.index` instance instead of a
+//!      parents function.
 //!    + `MissingAncestors.bases` is a method returning a tuple instead of
 //!      a set-valued attribute. We could return a Python set easily if our
 //!      [PySet PR](https://github.com/dgrunwald/rust-cpython/pull/165)
--- a/setup.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/setup.py	Fri Sep 06 02:12:19 2024 +0200
@@ -463,7 +463,6 @@
 
 
 class hgbuildmo(build):
-
     description = "build translations (.mo files)"
 
     def run(self):
@@ -1056,7 +1055,6 @@
 
 
 class hginstall(install):
-
     user_options = install.user_options + [
         (
             'old-and-unmanageable',
--- a/tests/dumbhttp.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/tests/dumbhttp.py	Fri Sep 06 02:12:19 2024 +0200
@@ -26,7 +26,6 @@
     class simplehttpserver(httpserver.httpserver):
         address_family = socket.AF_INET6
 
-
 else:
     simplehttpserver = httpserver.httpserver
 
--- a/tests/dummysmtpd.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/tests/dummysmtpd.py	Fri Sep 06 02:12:19 2024 +0200
@@ -31,8 +31,15 @@
 def mocksmtpserversession(conn, addr):
     conn.send(b'220 smtp.example.com ESMTP\r\n')
 
-    line = conn.recv(1024)
+    try:
+        # Newer versions of OpenSSL raise on EOF
+        line = conn.recv(1024)
+    except ssl.SSLError:
+        log('no hello: EOF\n')
+        return
+
     if not line.lower().startswith(b'ehlo '):
+        # Older versions of OpenSSl don't raise
         log('no hello: %s\n' % line)
         return
 
--- a/tests/fsmonitor-run-tests.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/tests/fsmonitor-run-tests.py	Fri Sep 06 02:12:19 2024 +0200
@@ -30,7 +30,6 @@
     def _sys2bytes(p):
         return p.encode('utf-8')
 
-
 elif sys.version_info >= (3, 0, 0):
     print(
         '%s is only supported on Python 3.5+ and 2.7, not %s'
--- a/tests/generate-working-copy-states.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/tests/generate-working-copy-states.py	Fri Sep 06 02:12:19 2024 +0200
@@ -33,6 +33,7 @@
 import os
 import sys
 
+
 # Generates pairs of (filename, contents), where 'contents' is a list
 # describing the file's content at each revision (or in the working copy).
 # At each revision, it is either None or the file's actual content. When not
--- a/tests/hghave.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/tests/hghave.py	Fri Sep 06 02:12:19 2024 +0200
@@ -1,4 +1,3 @@
-import distutils.version
 import os
 import re
 import socket
@@ -7,6 +6,11 @@
 import sys
 import tempfile
 
+try:
+    from setuptools.extern.packaging.version import Version
+except ImportError:
+    from distutils.version import StrictVersion as Version
+
 tempprefix = 'hg-hghave-'
 
 checks = {
@@ -1113,28 +1117,30 @@
     return matchoutput('emacs --version', b'GNU Emacs 2(4.4|4.5|5|6|7|8|9)')
 
 
-@check('black', 'the black formatter for python (>= 20.8b1)')
+@check('black', 'the black formatter for python >=23.3.0')
 def has_black():
     blackcmd = 'black --version'
     version_regex = b'black, (?:version )?([0-9a-b.]+)'
     version = matchoutput(blackcmd, version_regex)
-    sv = distutils.version.StrictVersion
-    return version and sv(_bytes2sys(version.group(1))) >= sv('20.8b1')
+    if not version:
+        return False
+    return Version(_bytes2sys(version.group(1))) >= Version('23.3.0')
 
 
 @check('pytype', 'the pytype type checker')
 def has_pytype():
     pytypecmd = 'pytype --version'
     version = matchoutput(pytypecmd, b'[0-9a-b.]+')
-    sv = distutils.version.StrictVersion
-    return version and sv(_bytes2sys(version.group(0))) >= sv('2019.10.17')
+    if not version:
+        return False
+    return Version(_bytes2sys(version.group(0))) >= Version('2019.10.17')
 
 
-@check("rustfmt", "rustfmt tool at version nightly-2021-11-02")
+@check("rustfmt", "rustfmt tool at version nightly-2024-07-16")
 def has_rustfmt():
     # We use Nightly's rustfmt due to current unstable config options.
     return matchoutput(
-        '`rustup which --toolchain nightly-2021-11-02 rustfmt` --version',
+        '`rustup which --toolchain nightly-2024-07-16 rustfmt` --version',
         b'rustfmt',
     )
 
--- a/tests/killdaemons.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/tests/killdaemons.py	Fri Sep 06 02:12:19 2024 +0200
@@ -75,7 +75,6 @@
             raise
         _check(ctypes.windll.kernel32.CloseHandle(handle))
 
-
 else:
 
     def kill(pid, logfn, tryhard=True):
--- a/tests/mockblackbox.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/tests/mockblackbox.py	Fri Sep 06 02:12:19 2024 +0200
@@ -1,5 +1,6 @@
 from mercurial.utils import procutil
 
+
 # XXX: we should probably offer a devel option to do this in blackbox directly
 def getuser():
     return b'bob'
--- a/tests/run-tests.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/tests/run-tests.py	Fri Sep 06 02:12:19 2024 +0200
@@ -223,6 +223,7 @@
 # For Windows support
 wifexited = getattr(os, "WIFEXITED", lambda x: False)
 
+
 # Whether to use IPv6
 def checksocketfamily(name, port=20058):
     """return true if we can listen on localhost using family=name
@@ -3397,7 +3398,6 @@
                 os.path.basename(t).startswith(b'test-')
                 and (t.endswith(b'.py') or t.endswith(b'.t'))
             ):
-
                 m = testcasepattern.match(os.path.basename(t))
                 if m is not None:
                     t_basename, casestr = m.groups()
--- a/tests/test-ancestor.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/tests/test-ancestor.py	Fri Sep 06 02:12:19 2024 +0200
@@ -87,6 +87,7 @@
     testcount = 10
     inccount = 10
     nerrs = [0]
+
     # the default mu and sigma give us a nice distribution of mostly
     # single-digit counts (including 0) with some higher ones
     def lognormrandom(mu, sigma):
--- a/tests/test-batching.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/tests/test-batching.py	Fri Sep 06 02:12:19 2024 +0200
@@ -55,7 +55,6 @@
 
 # usage of "thing" interface
 def use(it):
-
     # Direct call to base method shared between client and server.
     bprint(it.hello())
 
@@ -106,6 +105,7 @@
 
 # server side
 
+
 # equivalent of wireproto's global functions
 class server:
     def __init__(self, local):
@@ -156,6 +156,7 @@
 
 # local side
 
+
 # equivalent of wireproto.encode/decodelist, that is, type-specific marshalling
 # here we just transform the strings a bit to check we're properly en-/decoding
 def mangle(s):
--- a/tests/test-cbor.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/tests/test-cbor.py	Fri Sep 06 02:12:19 2024 +0200
@@ -216,11 +216,11 @@
         for size in lens:
             if size < 24:
                 hlen = 1
-            elif size < 2 ** 8:
+            elif size < 2**8:
                 hlen = 2
-            elif size < 2 ** 16:
+            elif size < 2**16:
                 hlen = 3
-            elif size < 2 ** 32:
+            elif size < 2**32:
                 hlen = 5
             else:
                 assert False
@@ -487,7 +487,7 @@
         )
 
     def testdecodepartialushort(self):
-        encoded = b''.join(cborutil.streamencode(2 ** 15))
+        encoded = b''.join(cborutil.streamencode(2**15))
 
         self.assertEqual(
             cborutil.decodeitem(encoded[0:1]),
@@ -499,7 +499,7 @@
         )
         self.assertEqual(
             cborutil.decodeitem(encoded[0:5]),
-            (True, 2 ** 15, 3, cborutil.SPECIAL_NONE),
+            (True, 2**15, 3, cborutil.SPECIAL_NONE),
         )
 
     def testdecodepartialshort(self):
@@ -519,7 +519,7 @@
         )
 
     def testdecodepartialulong(self):
-        encoded = b''.join(cborutil.streamencode(2 ** 28))
+        encoded = b''.join(cborutil.streamencode(2**28))
 
         self.assertEqual(
             cborutil.decodeitem(encoded[0:1]),
@@ -539,7 +539,7 @@
         )
         self.assertEqual(
             cborutil.decodeitem(encoded[0:5]),
-            (True, 2 ** 28, 5, cborutil.SPECIAL_NONE),
+            (True, 2**28, 5, cborutil.SPECIAL_NONE),
         )
 
     def testdecodepartiallong(self):
@@ -567,7 +567,7 @@
         )
 
     def testdecodepartialulonglong(self):
-        encoded = b''.join(cborutil.streamencode(2 ** 32))
+        encoded = b''.join(cborutil.streamencode(2**32))
 
         self.assertEqual(
             cborutil.decodeitem(encoded[0:1]),
@@ -603,7 +603,7 @@
         )
         self.assertEqual(
             cborutil.decodeitem(encoded[0:9]),
-            (True, 2 ** 32, 9, cborutil.SPECIAL_NONE),
+            (True, 2**32, 9, cborutil.SPECIAL_NONE),
         )
 
         with self.assertRaisesRegex(
--- a/tests/test-check-rust-format.t	Thu Sep 05 15:37:14 2024 -0400
+++ b/tests/test-check-rust-format.t	Fri Sep 06 02:12:19 2024 +0200
@@ -5,7 +5,7 @@
   $ cd "$TESTDIR"/..
 
 Warning: Keep this in sync with hghave.py
-  $ RUSTFMT=$(rustup which --toolchain nightly-2021-11-02 rustfmt)
+  $ RUSTFMT=$(rustup which --toolchain nightly-2024-07-16 rustfmt)
   $ for f in `testrepohg files 'glob:**/*.rs'` ; do
-  >   $RUSTFMT --check --edition=2018 --unstable-features --color=never $f
+  >   $RUSTFMT --check --edition=2021 --unstable-features --color=never $f
   > done
--- a/tests/test-config-env.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/tests/test-config-env.py	Fri Sep 06 02:12:19 2024 +0200
@@ -15,6 +15,7 @@
 
 testtmp = encoding.environ[b'TESTTMP']
 
+
 # prepare hgrc files
 def join(name):
     return os.path.join(testtmp, name)
@@ -26,6 +27,7 @@
 with open(join(b'userrc'), 'wb') as f:
     f.write(b'[ui]\neditor=e1')
 
+
 # replace rcpath functions so they point to the files above
 def systemrcpath():
     return [join(b'sysrc')]
@@ -40,6 +42,7 @@
 rcutil.systemrcpath = systemrcpath
 rcutil.userrcpath = userrcpath
 
+
 # utility to print configs
 def printconfigs(env):
     encoding.environ = env
--- a/tests/test-demandimport.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/tests/test-demandimport.py	Fri Sep 06 02:12:19 2024 +0200
@@ -31,18 +31,6 @@
 except ImportError:
     moduletype = types.ModuleType
 
-if os.name != 'nt':
-    try:
-        import distutils.msvc9compiler
-
-        print(
-            'distutils.msvc9compiler needs to be an immediate '
-            'importerror on non-windows platforms'
-        )
-        distutils.msvc9compiler
-    except ImportError:
-        pass
-
 import re
 
 rsub = re.sub
--- a/tests/test-doctest.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/tests/test-doctest.py	Fri Sep 06 02:12:19 2024 +0200
@@ -21,9 +21,11 @@
             r'''^mercurial\.\w+\.(\w+): (['"])(.*?)\2''',
             r'\1: \3',
             got2,
-            re.MULTILINE,
+            flags=re.MULTILINE,
         )
-        got2 = re.sub(r'^mercurial\.\w+\.(\w+): ', r'\1: ', got2, re.MULTILINE)
+        got2 = re.sub(
+            r'^mercurial\.\w+\.(\w+): ', r'\1: ', got2, flags=re.MULTILINE
+        )
         return any(
             doctest.OutputChecker.check_output(self, w, g, optionflags)
             for w, g in [(want, got), (want2, got2)]
--- a/tests/test-extensions-wrapfunction.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/tests/test-extensions-wrapfunction.py	Fri Sep 06 02:12:19 2024 +0200
@@ -66,6 +66,7 @@
     print('context manager', dummy.getstack())
 print('context manager', dummy.getstack())
 
+
 # Wrap callable object which has no __name__
 class callableobj:
     def __call__(self):
--- a/tests/test-hg-parseurl.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/tests/test-hg-parseurl.py	Fri Sep 06 02:12:19 2024 +0200
@@ -5,7 +5,6 @@
 
 class ParseRequestTests(unittest.TestCase):
     def testparse(self):
-
         self.assertEqual(
             urlutil.parseurl(b'http://example.com/no/anchor'),
             (b'http://example.com/no/anchor', (None, [])),
--- a/tests/test-hybridencode.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/tests/test-hybridencode.py	Fri Sep 06 02:12:19 2024 +0200
@@ -5,7 +5,6 @@
 
 class hybridencodetests(unittest.TestCase):
     def hybridencode(self, input, want):
-
         # Check the C implementation if it's in use
         got = store._pathencode(input)
         self.assertEqual(want, got)
--- a/tests/test-install.t	Thu Sep 05 15:37:14 2024 -0400
+++ b/tests/test-install.t	Fri Sep 06 02:12:19 2024 +0200
@@ -216,6 +216,9 @@
     Failed building wheel for mercurial (?)
   WARNING: You are using pip version *; however, version * is available. (glob) (?)
   You should consider upgrading via the '$TESTTMP/installenv/bin/python* -m pip install --upgrade pip' command. (glob) (?)
+   (?)
+  [notice] A new release of pip is available: * -> * (glob) (?)
+  [notice] To update, run: python -m pip install --upgrade pip (?)
   $ ./installenv/*/hg debuginstall || cat pip.log
   checking encoding (ascii)...
   checking Python executable (*) (glob)
--- a/tests/test-parseindex2.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/tests/test-parseindex2.py	Fri Sep 06 02:12:19 2024 +0200
@@ -26,6 +26,7 @@
 
 parsers = policy.importmod('parsers')
 
+
 # original python implementation
 def gettype(q):
     return int(q & 0xFFFF)
--- a/tests/test-patchbomb-tls.t	Thu Sep 05 15:37:14 2024 -0400
+++ b/tests/test-patchbomb-tls.t	Fri Sep 06 02:12:19 2024 +0200
@@ -98,7 +98,7 @@
   $ wait_log "no hello:"
   $ cat ../log
   connection from * (glob)
-  no hello: b''
+  no hello: (b''|EOF) (re)
   $ : > ../log
 
 With global certificates:
--- a/tests/test-remotefilelog-datapack.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/tests/test-remotefilelog-datapack.py	Fri Sep 06 02:12:19 2024 +0200
@@ -186,7 +186,7 @@
             content = b'put-something-here \n' * i
             node = self.getHash(content)
             meta = {
-                constants.METAKEYFLAG: i ** 4,
+                constants.METAKEYFLAG: i**4,
                 constants.METAKEYSIZE: len(content),
                 b'Z': b'random_string',
                 b'_': b'\0' * i,
--- a/tests/test-remotefilelog-histpack.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/tests/test-remotefilelog-histpack.py	Fri Sep 06 02:12:19 2024 +0200
@@ -177,7 +177,7 @@
         pack = self.createPack(revisions)
 
         # Verify the pack contents
-        for (filename, node) in allentries:
+        for filename, node in allentries:
             ancestors = pack.getancestors(filename, node)
             self.assertEqual(ancestorcounts[(filename, node)], len(ancestors))
             for anode, (ap1, ap2, alinknode, copyfrom) in ancestors.items():
--- a/tests/test-revlog-mmapindex.t	Thu Sep 05 15:37:14 2024 -0400
+++ b/tests/test-revlog-mmapindex.t	Fri Sep 06 02:12:19 2024 +0200
@@ -36,7 +36,9 @@
   > EOF
 
 mmap index which is now more than 4k long
-  $ hg log -l 5 -T '{rev}\n' --config experimental.mmapindexthreshold=4k
+  $ hg log -l 5 -T '{rev}\n' \
+  >     --config storage.revlog.mmap.index=yes \
+  >     --config storage.revlog.mmap.index:size-threshold=4k
   mmapping $TESTTMP/a/.hg/store/00changelog.i (no-pure !)
   mmapping $TESTTMP/a/.hg/store/00changelog-????????.nd (glob) (rust !)
   100
@@ -46,7 +48,9 @@
   96
 
 do not mmap index which is still less than 32k
-  $ hg log -l 5 -T '{rev}\n' --config experimental.mmapindexthreshold=32k
+  $ hg log -l 5 -T '{rev}\n' \
+  >     --config storage.revlog.mmap.index=yes \
+  >     --config storage.revlog.mmap.index:size-threshold=32k
   mmapping $TESTTMP/a/.hg/store/00changelog-????????.nd (glob) (rust !)
   100
   99
--- a/tests/test-revlog-raw.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/tests/test-revlog-raw.py	Fri Sep 06 02:12:19 2024 +0200
@@ -246,7 +246,7 @@
     That is to say, given any x, y where both x, and y are in range(2 ** n),
     there is an x followed immediately by y in the generated sequence.
     """
-    m = 2 ** n
+    m = 2**n
 
     # Gray Code. See https://en.wikipedia.org/wiki/Gray_code
     gray = lambda x: x ^ (x >> 1)
--- a/tests/test-simplemerge.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/tests/test-simplemerge.py	Fri Sep 06 02:12:19 2024 +0200
@@ -24,6 +24,8 @@
 from mercurial.utils import stringutil
 
 TestCase = unittest.TestCase
+
+
 # bzr compatible interface, for the tests
 class Merge3(simplemerge.Merge3Text):
     """3-way merge of texts.
--- a/tests/test-symlink-os-yes-fs-no.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/tests/test-symlink-os-yes-fs-no.py	Fri Sep 06 02:12:19 2024 +0200
@@ -30,6 +30,7 @@
 time.sleep(1)
 commands.status(u, repo)
 
+
 # now disable symlink support -- this is what os.symlink would do on a
 # non-symlink file system
 def symlink_failure(src, dst):
--- a/tests/test-ui-color.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/tests/test-ui-color.py	Fri Sep 06 02:12:19 2024 +0200
@@ -25,6 +25,7 @@
 # we're not interested in the output, so write that to devnull
 ui_.fout = open(os.devnull, 'wb')
 
+
 # call some arbitrary command just so we go through
 # color's wrapped _runcommand twice.
 def runcmd():
--- a/tests/test-verify-repo-operations.py	Thu Sep 05 15:37:14 2024 -0400
+++ b/tests/test-verify-repo-operations.py	Fri Sep 06 02:12:19 2024 +0200
@@ -615,8 +615,8 @@
     settings(
         timeout=-1,
         stateful_step_count=1000,
-        max_examples=10 ** 8,
-        max_iterations=10 ** 8,
+        max_examples=10**8,
+        max_iterations=10**8,
         database=writeonlydatabase(settings.default.database),
     ),
 )