changeset 49164:a932cad26d37

branching: merge stable into default
author Raphaël Gomès <rgomes@octobus.net>
date Wed, 04 May 2022 18:17:44 +0200
parents 10b9f11daf15 (diff) 0ddd5e1f5f67 (current diff)
children 7af798e497f5
files contrib/heptapod-ci.yml doc/gendoc.py mercurial/debugcommands.py mercurial/localrepo.py rust/Cargo.lock rust/hg-core/src/dirstate_tree/on_disk.rs rust/hg-core/src/repo.rs rust/rhg/Cargo.toml rust/rhg/src/main.rs tests/test-dirstate.t tests/test-help.t tests/test-rhg.t
diffstat 724 files changed, 6164 insertions(+), 8210 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Wed May 04 18:00:01 2022 +0200
+++ b/.hgignore	Wed May 04 18:17:44 2022 +0200
@@ -22,6 +22,8 @@
 tests/artifacts/cache/big-file-churn.hg
 tests/.coverage*
 tests/.testtimes*
+# the file is written in the CWD when run-tests is run.
+.testtimes
 tests/.hypothesis
 tests/hypothesis-generated
 tests/annotated
@@ -33,6 +35,7 @@
 contrib/chg/chg
 contrib/hgsh/hgsh
 contrib/vagrant/.vagrant
+contrib/merge-lists/target/
 dist
 packages
 doc/common.txt
--- a/Makefile	Wed May 04 18:00:01 2022 +0200
+++ b/Makefile	Wed May 04 18:17:44 2022 +0200
@@ -151,12 +151,9 @@
         $(MAKE) -f $(HGROOT)/contrib/Makefile.python PYTHONVER=$* PREFIX=$(HGPYTHONS)/$* python )
 	cd tests && $(HGPYTHONS)/$*/bin/python run-tests.py $(TESTFLAGS)
 
-rust-tests: py_feature = $(shell $(PYTHON) -c \
- 'import sys; print(["python27-bin", "python3-bin"][sys.version_info[0] >= 3])')
 rust-tests:
 	cd $(HGROOT)/rust/hg-cpython \
-		&& $(CARGO) test --quiet --all \
-			--no-default-features --features "$(py_feature) $(HG_RUST_FEATURES)"
+		&& $(CARGO) test --quiet --all --features "$(HG_RUST_FEATURES)"
 
 check-code:
 	hg manifest | xargs python contrib/check-code.py
@@ -238,16 +235,6 @@
         # Place a bogon .DS_Store file in the target dir so we can be
         # sure it doesn't get included in the final package.
 	touch build/mercurial/.DS_Store
-        # install zsh completions - this location appears to be
-        # searched by default as of macOS Sierra.
-	install -d build/mercurial/usr/local/share/zsh/site-functions/
-	install -m 0644 contrib/zsh_completion build/mercurial/usr/local/share/zsh/site-functions/_hg
-        # install bash completions - there doesn't appear to be a
-        # place that's searched by default for bash, so we'll follow
-        # the lead of Apple's git install and just put it in a
-        # location of our own.
-	install -d build/mercurial/usr/local/hg/contrib/
-	install -m 0644 contrib/bash_completion build/mercurial/usr/local/hg/contrib/hg-completion.bash
 	make -C contrib/chg \
 	  HGPATH=/usr/local/bin/hg \
 	  PYTHON=/usr/bin/python2.7 \
--- a/contrib/automation/hgautomation/aws.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/automation/hgautomation/aws.py	Wed May 04 18:17:44 2022 +0200
@@ -919,17 +919,12 @@
         'SecurityGroupIds': [c.security_groups['linux-dev-1'].id],
     }
 
-    requirements2_path = (
-        pathlib.Path(__file__).parent.parent / 'linux-requirements-py2.txt'
-    )
     requirements3_path = (
         pathlib.Path(__file__).parent.parent / 'linux-requirements-py3.txt'
     )
     requirements35_path = (
         pathlib.Path(__file__).parent.parent / 'linux-requirements-py3.5.txt'
     )
-    with requirements2_path.open('r', encoding='utf-8') as fh:
-        requirements2 = fh.read()
     with requirements3_path.open('r', encoding='utf-8') as fh:
         requirements3 = fh.read()
     with requirements35_path.open('r', encoding='utf-8') as fh:
@@ -941,7 +936,6 @@
         {
             'instance_config': config,
             'bootstrap_script': BOOTSTRAP_DEBIAN,
-            'requirements_py2': requirements2,
             'requirements_py3': requirements3,
             'requirements_py35': requirements35,
         }
@@ -977,10 +971,6 @@
                 fh.write(BOOTSTRAP_DEBIAN)
                 fh.chmod(0o0700)
 
-            with sftp.open('%s/requirements-py2.txt' % home, 'wb') as fh:
-                fh.write(requirements2)
-                fh.chmod(0o0700)
-
             with sftp.open('%s/requirements-py3.txt' % home, 'wb') as fh:
                 fh.write(requirements3)
                 fh.chmod(0o0700)
--- a/contrib/automation/hgautomation/cli.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/automation/hgautomation/cli.py	Wed May 04 18:17:44 2022 +0200
@@ -65,7 +65,6 @@
 def build_inno(
     hga: HGAutomation,
     aws_region,
-    python_version,
     arch,
     revision,
     version,
@@ -80,21 +79,18 @@
 
         windows.synchronize_hg(SOURCE_ROOT, revision, instance)
 
-        for py_version in python_version:
-            for a in arch:
-                windows.build_inno_installer(
-                    instance.winrm_client,
-                    py_version,
-                    a,
-                    DIST_PATH,
-                    version=version,
-                )
+        for a in arch:
+            windows.build_inno_installer(
+                instance.winrm_client,
+                a,
+                DIST_PATH,
+                version=version,
+            )
 
 
 def build_wix(
     hga: HGAutomation,
     aws_region,
-    python_version,
     arch,
     revision,
     version,
@@ -109,15 +105,13 @@
 
         windows.synchronize_hg(SOURCE_ROOT, revision, instance)
 
-        for py_version in python_version:
-            for a in arch:
-                windows.build_wix_installer(
-                    instance.winrm_client,
-                    py_version,
-                    a,
-                    DIST_PATH,
-                    version=version,
-                )
+        for a in arch:
+            windows.build_wix_installer(
+                instance.winrm_client,
+                a,
+                DIST_PATH,
+                version=version,
+            )
 
 
 def build_windows_wheel(
@@ -158,7 +152,7 @@
 
         windows.synchronize_hg(SOURCE_ROOT, revision, instance)
 
-        for py_version in ("2.7", "3.7", "3.8", "3.9", "3.10"):
+        for py_version in ("3.7", "3.8", "3.9", "3.10"):
             for arch in ("x86", "x64"):
                 windows.purge_hg(winrm_client)
                 windows.build_wheel(
@@ -168,15 +162,14 @@
                     dest_path=DIST_PATH,
                 )
 
-        for py_version in (2, 3):
-            for arch in ('x86', 'x64'):
-                windows.purge_hg(winrm_client)
-                windows.build_inno_installer(
-                    winrm_client, py_version, arch, DIST_PATH, version=version
-                )
-                windows.build_wix_installer(
-                    winrm_client, py_version, arch, DIST_PATH, version=version
-                )
+        for arch in ('x86', 'x64'):
+            windows.purge_hg(winrm_client)
+            windows.build_inno_installer(
+                winrm_client, arch, DIST_PATH, version=version
+            )
+            windows.build_wix_installer(
+                winrm_client, arch, DIST_PATH, version=version
+            )
 
 
 def terminate_ec2_instances(hga: HGAutomation, aws_region):
@@ -340,14 +333,6 @@
         help='Build Inno Setup installer(s)',
     )
     sp.add_argument(
-        '--python-version',
-        help='Which version of Python to target',
-        choices={2, 3},
-        type=int,
-        nargs='*',
-        default=[3],
-    )
-    sp.add_argument(
         '--arch',
         help='Architecture to build for',
         choices={'x86', 'x64'},
@@ -377,7 +362,7 @@
     sp.add_argument(
         '--python-version',
         help='Python version to build for',
-        choices={'2.7', '3.7', '3.8', '3.9', '3.10'},
+        choices={'3.7', '3.8', '3.9', '3.10'},
         nargs='*',
         default=['3.8'],
     )
@@ -402,14 +387,6 @@
 
     sp = subparsers.add_parser('build-wix', help='Build WiX installer(s)')
     sp.add_argument(
-        '--python-version',
-        help='Which version of Python to target',
-        choices={2, 3},
-        type=int,
-        nargs='*',
-        default=[3],
-    )
-    sp.add_argument(
         '--arch',
         help='Architecture to build for',
         choices={'x86', 'x64'},
@@ -469,9 +446,7 @@
         '--python-version',
         help='Python version to use',
         choices={
-            'system2',
             'system3',
-            '2.7',
             '3.5',
             '3.6',
             '3.7',
@@ -480,7 +455,7 @@
             'pypy3.5',
             'pypy3.6',
         },
-        default='system2',
+        default='system3',
     )
     sp.add_argument(
         'test_flags',
@@ -501,8 +476,8 @@
     sp.add_argument(
         '--python-version',
         help='Python version to use',
-        choices={'2.7', '3.5', '3.6', '3.7', '3.8', '3.9', '3.10'},
-        default='2.7',
+        choices={'3.5', '3.6', '3.7', '3.8', '3.9', '3.10'},
+        default='3.9',
     )
     sp.add_argument(
         '--arch',
--- a/contrib/automation/hgautomation/linux.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/automation/hgautomation/linux.py	Wed May 04 18:17:44 2022 +0200
@@ -25,7 +25,6 @@
 }
 
 INSTALL_PYTHONS = r'''
-PYENV2_VERSIONS="2.7.17 pypy2.7-7.2.0"
 PYENV3_VERSIONS="3.5.10 3.6.13 3.7.10 3.8.10 3.9.5 pypy3.5-7.0.0 pypy3.6-7.3.3 pypy3.7-7.3.3"
 
 git clone https://github.com/pyenv/pyenv.git /hgdev/pyenv
@@ -46,13 +45,6 @@
 wget -O ${VIRTUALENV_TARBALL} --progress dot:mega https://files.pythonhosted.org/packages/66/f0/6867af06d2e2f511e4e1d7094ff663acdebc4f15d4a0cb0fed1007395124/${VIRTUALENV_TARBALL}
 echo "${VIRTUALENV_SHA256} ${VIRTUALENV_TARBALL}" | sha256sum --check -
 
-for v in ${PYENV2_VERSIONS}; do
-    pyenv install -v ${v}
-    ${PYENV_ROOT}/versions/${v}/bin/python get-pip.py
-    ${PYENV_ROOT}/versions/${v}/bin/pip install ${VIRTUALENV_TARBALL}
-    ${PYENV_ROOT}/versions/${v}/bin/pip install -r /hgdev/requirements-py2.txt
-done
-
 for v in ${PYENV3_VERSIONS}; do
     pyenv install -v ${v}
     ${PYENV_ROOT}/versions/${v}/bin/python get-pip.py
@@ -72,7 +64,7 @@
     ${PYENV_ROOT}/versions/${v}/bin/pip install -r /hgdev/${REQUIREMENTS}
 done
 
-pyenv global ${PYENV2_VERSIONS} ${PYENV3_VERSIONS} system
+pyenv global ${PYENV3_VERSIONS} system
 '''.lstrip().replace(
     '\r\n', '\n'
 )
@@ -274,17 +266,8 @@
     netbase \
     ntfs-3g \
     nvme-cli \
-    pyflakes \
     pyflakes3 \
-    pylint \
     pylint3 \
-    python-all-dev \
-    python-dev \
-    python-docutils \
-    python-fuzzywuzzy \
-    python-pygments \
-    python-subversion \
-    python-vcr \
     python3-boto3 \
     python3-dev \
     python3-docutils \
@@ -532,7 +515,7 @@
         hg_bin = source_path / 'hg'
 
         res = subprocess.run(
-            ['python2.7', str(hg_bin), 'log', '-r', revision, '-T', '{node}'],
+            ['python3', str(hg_bin), 'log', '-r', revision, '-T', '{node}'],
             cwd=str(source_path),
             env=env,
             check=True,
@@ -542,7 +525,7 @@
         full_revision = res.stdout.decode('ascii')
 
         args = [
-            'python2.7',
+            'python3',
             str(hg_bin),
             '--config',
             'ui.ssh=ssh -F %s' % ssh_config,
@@ -595,9 +578,7 @@
 
     print('running tests')
 
-    if python_version == 'system2':
-        python = '/usr/bin/python2'
-    elif python_version == 'system3':
+    if python_version == 'system3':
         python = '/usr/bin/python3'
     elif python_version.startswith('pypy'):
         python = '/hgdev/pyenv/shims/%s' % python_version
--- a/contrib/automation/hgautomation/windows.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/automation/hgautomation/windows.py	Wed May 04 18:17:44 2022 +0200
@@ -19,30 +19,6 @@
 from .winrm import run_powershell
 
 
-# PowerShell commands to activate a Visual Studio 2008 environment.
-# This is essentially a port of vcvarsall.bat to PowerShell.
-ACTIVATE_VC9_AMD64 = r'''
-Write-Output "activating Visual Studio 2008 environment for AMD64"
-$root = "$env:LOCALAPPDATA\Programs\Common\Microsoft\Visual C++ for Python\9.0"
-$Env:VCINSTALLDIR = "${root}\VC\"
-$Env:WindowsSdkDir = "${root}\WinSDK\"
-$Env:PATH = "${root}\VC\Bin\amd64;${root}\WinSDK\Bin\x64;${root}\WinSDK\Bin;$Env:PATH"
-$Env:INCLUDE = "${root}\VC\Include;${root}\WinSDK\Include;$Env:PATH"
-$Env:LIB = "${root}\VC\Lib\amd64;${root}\WinSDK\Lib\x64;$Env:LIB"
-$Env:LIBPATH = "${root}\VC\Lib\amd64;${root}\WinSDK\Lib\x64;$Env:LIBPATH"
-'''.lstrip()
-
-ACTIVATE_VC9_X86 = r'''
-Write-Output "activating Visual Studio 2008 environment for x86"
-$root = "$env:LOCALAPPDATA\Programs\Common\Microsoft\Visual C++ for Python\9.0"
-$Env:VCINSTALLDIR = "${root}\VC\"
-$Env:WindowsSdkDir = "${root}\WinSDK\"
-$Env:PATH = "${root}\VC\Bin;${root}\WinSDK\Bin;$Env:PATH"
-$Env:INCLUDE = "${root}\VC\Include;${root}\WinSDK\Include;$Env:INCLUDE"
-$Env:LIB = "${root}\VC\Lib;${root}\WinSDK\Lib;$Env:LIB"
-$Env:LIBPATH = "${root}\VC\lib;${root}\WinSDK\Lib;$Env:LIBPATH"
-'''.lstrip()
-
 HG_PURGE = r'''
 $Env:PATH = "C:\hgdev\venv-bootstrap\Scripts;$Env:PATH"
 Set-Location C:\hgdev\src
@@ -78,14 +54,6 @@
 }}
 '''
 
-BUILD_INNO_PYTHON2 = r'''
-Set-Location C:\hgdev\src
-$python = "C:\hgdev\python27-{arch}\python.exe"
-C:\hgdev\python37-x64\python.exe contrib\packaging\packaging.py inno --python $python {extra_args}
-if ($LASTEXITCODE -ne 0) {{
-    throw "process exited non-0: $LASTEXITCODE"
-}}
-'''.lstrip()
 
 BUILD_WHEEL = r'''
 Set-Location C:\hgdev\src
@@ -105,14 +73,6 @@
 }}
 '''
 
-BUILD_WIX_PYTHON2 = r'''
-Set-Location C:\hgdev\src
-$python = "C:\hgdev\python27-{arch}\python.exe"
-C:\hgdev\python37-x64\python.exe contrib\packaging\packaging.py wix --python $python {extra_args}
-if ($LASTEXITCODE -ne 0) {{
-    throw "process exited non-0: $LASTEXITCODE"
-}}
-'''
 
 RUN_TESTS = r'''
 C:\hgdev\MinGW\msys\1.0\bin\sh.exe --login -c "cd /c/hgdev/src/tests && /c/hgdev/{python_path}/python.exe run-tests.py {test_flags}"
@@ -121,8 +81,7 @@
 }}
 '''
 
-WHEEL_FILENAME_PYTHON27_X86 = 'mercurial-{version}-cp27-cp27m-win32.whl'
-WHEEL_FILENAME_PYTHON27_X64 = 'mercurial-{version}-cp27-cp27m-win_amd64.whl'
+
 WHEEL_FILENAME_PYTHON37_X86 = 'mercurial-{version}-cp37-cp37m-win32.whl'
 WHEEL_FILENAME_PYTHON37_X64 = 'mercurial-{version}-cp37-cp37m-win_amd64.whl'
 WHEEL_FILENAME_PYTHON38_X86 = 'mercurial-{version}-cp38-cp38-win32.whl'
@@ -132,13 +91,9 @@
 WHEEL_FILENAME_PYTHON310_X86 = 'mercurial-{version}-cp310-cp310-win32.whl'
 WHEEL_FILENAME_PYTHON310_X64 = 'mercurial-{version}-cp310-cp310-win_amd64.whl'
 
-EXE_FILENAME_PYTHON2_X86 = 'Mercurial-{version}-x86-python2.exe'
-EXE_FILENAME_PYTHON2_X64 = 'Mercurial-{version}-x64-python2.exe'
 EXE_FILENAME_PYTHON3_X86 = 'Mercurial-{version}-x86.exe'
 EXE_FILENAME_PYTHON3_X64 = 'Mercurial-{version}-x64.exe'
 
-MSI_FILENAME_PYTHON2_X86 = 'mercurial-{version}-x86-python2.msi'
-MSI_FILENAME_PYTHON2_X64 = 'mercurial-{version}-x64-python2.msi'
 MSI_FILENAME_PYTHON3_X86 = 'mercurial-{version}-x86.msi'
 MSI_FILENAME_PYTHON3_X64 = 'mercurial-{version}-x64.msi'
 
@@ -147,14 +102,6 @@
 X86_USER_AGENT_PATTERN = '.*Windows.*'
 X64_USER_AGENT_PATTERN = '.*Windows.*(WOW|x)64.*'
 
-EXE_PYTHON2_X86_DESCRIPTION = (
-    'Mercurial {version} Inno Setup installer - x86 Windows (Python 2) '
-    '- does not require admin rights'
-)
-EXE_PYTHON2_X64_DESCRIPTION = (
-    'Mercurial {version} Inno Setup installer - x64 Windows (Python 2) '
-    '- does not require admin rights'
-)
 # TODO remove Python version once Python 2 is dropped.
 EXE_PYTHON3_X86_DESCRIPTION = (
     'Mercurial {version} Inno Setup installer - x86 Windows (Python 3) '
@@ -164,14 +111,6 @@
     'Mercurial {version} Inno Setup installer - x64 Windows (Python 3) '
     '- does not require admin rights'
 )
-MSI_PYTHON2_X86_DESCRIPTION = (
-    'Mercurial {version} MSI installer - x86 Windows (Python 2) '
-    '- requires admin rights'
-)
-MSI_PYTHON2_X64_DESCRIPTION = (
-    'Mercurial {version} MSI installer - x64 Windows (Python 2) '
-    '- requires admin rights'
-)
 MSI_PYTHON3_X86_DESCRIPTION = (
     'Mercurial {version} MSI installer - x86 Windows (Python 3) '
     '- requires admin rights'
@@ -182,15 +121,6 @@
 )
 
 
-def get_vc_prefix(arch):
-    if arch == 'x86':
-        return ACTIVATE_VC9_X86
-    elif arch == 'x64':
-        return ACTIVATE_VC9_AMD64
-    else:
-        raise ValueError('illegal arch: %s; must be x86 or x64' % arch)
-
-
 def fix_authorized_keys_permissions(winrm_client, path):
     commands = [
         '$ErrorActionPreference = "Stop"',
@@ -261,7 +191,7 @@
         hg_bin = hg_repo / 'hg'
 
         res = subprocess.run(
-            ['python2.7', str(hg_bin), 'log', '-r', revision, '-T', '{node}'],
+            ['python3', str(hg_bin), 'log', '-r', revision, '-T', '{node}'],
             cwd=str(hg_repo),
             env=env,
             check=True,
@@ -271,7 +201,7 @@
         full_revision = res.stdout.decode('ascii')
 
         args = [
-            'python2.7',
+            'python3',
             hg_bin,
             '--config',
             'ui.ssh=ssh -F %s' % ssh_config,
@@ -334,7 +264,6 @@
 
 def build_inno_installer(
     winrm_client,
-    python_version: int,
     arch: str,
     dest_path: pathlib.Path,
     version=None,
@@ -344,37 +273,23 @@
     Using a WinRM client, remote commands are executed to build
     a Mercurial Inno Setup installer.
     """
-    print(
-        'building Inno Setup installer for Python %d %s'
-        % (python_version, arch)
-    )
+    print('building Inno Setup installer for %s' % arch)
 
-    if python_version == 3:
-        # TODO fix this limitation in packaging code
-        if not version:
-            raise Exception(
-                "version string is required when building for Python 3"
-            )
+    # TODO fix this limitation in packaging code
+    if not version:
+        raise Exception("version string is required when building for Python 3")
 
-        if arch == "x86":
-            target_triple = "i686-pc-windows-msvc"
-        elif arch == "x64":
-            target_triple = "x86_64-pc-windows-msvc"
-        else:
-            raise Exception("unhandled arch: %s" % arch)
+    if arch == "x86":
+        target_triple = "i686-pc-windows-msvc"
+    elif arch == "x64":
+        target_triple = "x86_64-pc-windows-msvc"
+    else:
+        raise Exception("unhandled arch: %s" % arch)
 
-        ps = BUILD_INNO_PYTHON3.format(
-            pyoxidizer_target=target_triple,
-            version=version,
-        )
-    else:
-        extra_args = []
-        if version:
-            extra_args.extend(['--version', version])
-
-        ps = get_vc_prefix(arch) + BUILD_INNO_PYTHON2.format(
-            arch=arch, extra_args=' '.join(extra_args)
-        )
+    ps = BUILD_INNO_PYTHON3.format(
+        pyoxidizer_target=target_triple,
+        version=version,
+    )
 
     run_powershell(winrm_client, ps)
     copy_latest_dist(winrm_client, '*.exe', dest_path)
@@ -394,17 +309,12 @@
         python_version=python_version.replace(".", ""), arch=arch
     )
 
-    # Python 2.7 requires an activated environment.
-    if python_version == "2.7":
-        ps = get_vc_prefix(arch) + ps
-
     run_powershell(winrm_client, ps)
     copy_latest_dist(winrm_client, '*.whl', dest_path)
 
 
 def build_wix_installer(
     winrm_client,
-    python_version: int,
     arch: str,
     dest_path: pathlib.Path,
     version=None,
@@ -413,34 +323,23 @@
 
     Using a WinRM client, remote commands are executed to build a WiX installer.
     """
-    print('Building WiX installer for Python %d %s' % (python_version, arch))
+    print('Building WiX installer for %s' % arch)
 
-    if python_version == 3:
-        # TODO fix this limitation in packaging code
-        if not version:
-            raise Exception(
-                "version string is required when building for Python 3"
-            )
+    # TODO fix this limitation in packaging code
+    if not version:
+        raise Exception("version string is required when building for Python 3")
 
-        if arch == "x86":
-            target_triple = "i686-pc-windows-msvc"
-        elif arch == "x64":
-            target_triple = "x86_64-pc-windows-msvc"
-        else:
-            raise Exception("unhandled arch: %s" % arch)
+    if arch == "x86":
+        target_triple = "i686-pc-windows-msvc"
+    elif arch == "x64":
+        target_triple = "x86_64-pc-windows-msvc"
+    else:
+        raise Exception("unhandled arch: %s" % arch)
 
-        ps = BUILD_WIX_PYTHON3.format(
-            pyoxidizer_target=target_triple,
-            version=version,
-        )
-    else:
-        extra_args = []
-        if version:
-            extra_args.extend(['--version', version])
-
-        ps = get_vc_prefix(arch) + BUILD_WIX_PYTHON2.format(
-            arch=arch, extra_args=' '.join(extra_args)
-        )
+    ps = BUILD_WIX_PYTHON3.format(
+        pyoxidizer_target=target_triple,
+        version=version,
+    )
 
     run_powershell(winrm_client, ps)
     copy_latest_dist(winrm_client, '*.msi', dest_path)
@@ -474,8 +373,6 @@
 
 def resolve_wheel_artifacts(dist_path: pathlib.Path, version: str):
     return (
-        dist_path / WHEEL_FILENAME_PYTHON27_X86.format(version=version),
-        dist_path / WHEEL_FILENAME_PYTHON27_X64.format(version=version),
         dist_path / WHEEL_FILENAME_PYTHON37_X86.format(version=version),
         dist_path / WHEEL_FILENAME_PYTHON37_X64.format(version=version),
         dist_path / WHEEL_FILENAME_PYTHON38_X86.format(version=version),
@@ -489,8 +386,6 @@
 
 def resolve_all_artifacts(dist_path: pathlib.Path, version: str):
     return (
-        dist_path / WHEEL_FILENAME_PYTHON27_X86.format(version=version),
-        dist_path / WHEEL_FILENAME_PYTHON27_X64.format(version=version),
         dist_path / WHEEL_FILENAME_PYTHON37_X86.format(version=version),
         dist_path / WHEEL_FILENAME_PYTHON37_X64.format(version=version),
         dist_path / WHEEL_FILENAME_PYTHON38_X86.format(version=version),
@@ -499,24 +394,16 @@
         dist_path / WHEEL_FILENAME_PYTHON39_X64.format(version=version),
         dist_path / WHEEL_FILENAME_PYTHON310_X86.format(version=version),
         dist_path / WHEEL_FILENAME_PYTHON310_X64.format(version=version),
-        dist_path / EXE_FILENAME_PYTHON2_X86.format(version=version),
-        dist_path / EXE_FILENAME_PYTHON2_X64.format(version=version),
         dist_path / EXE_FILENAME_PYTHON3_X86.format(version=version),
         dist_path / EXE_FILENAME_PYTHON3_X64.format(version=version),
-        dist_path / MSI_FILENAME_PYTHON2_X86.format(version=version),
-        dist_path / MSI_FILENAME_PYTHON2_X64.format(version=version),
         dist_path / MSI_FILENAME_PYTHON3_X86.format(version=version),
         dist_path / MSI_FILENAME_PYTHON3_X64.format(version=version),
     )
 
 
 def generate_latest_dat(version: str):
-    python2_x86_exe_filename = EXE_FILENAME_PYTHON2_X86.format(version=version)
-    python2_x64_exe_filename = EXE_FILENAME_PYTHON2_X64.format(version=version)
     python3_x86_exe_filename = EXE_FILENAME_PYTHON3_X86.format(version=version)
     python3_x64_exe_filename = EXE_FILENAME_PYTHON3_X64.format(version=version)
-    python2_x86_msi_filename = MSI_FILENAME_PYTHON2_X86.format(version=version)
-    python2_x64_msi_filename = MSI_FILENAME_PYTHON2_X64.format(version=version)
     python3_x86_msi_filename = MSI_FILENAME_PYTHON3_X86.format(version=version)
     python3_x64_msi_filename = MSI_FILENAME_PYTHON3_X64.format(version=version)
 
@@ -536,20 +423,6 @@
             EXE_PYTHON3_X64_DESCRIPTION.format(version=version),
         ),
         (
-            '9',
-            version,
-            X86_USER_AGENT_PATTERN,
-            '%s/%s' % (MERCURIAL_SCM_BASE_URL, python2_x86_exe_filename),
-            EXE_PYTHON2_X86_DESCRIPTION.format(version=version),
-        ),
-        (
-            '9',
-            version,
-            X64_USER_AGENT_PATTERN,
-            '%s/%s' % (MERCURIAL_SCM_BASE_URL, python2_x64_exe_filename),
-            EXE_PYTHON2_X64_DESCRIPTION.format(version=version),
-        ),
-        (
             '10',
             version,
             X86_USER_AGENT_PATTERN,
@@ -563,20 +436,6 @@
             '%s/%s' % (MERCURIAL_SCM_BASE_URL, python3_x64_msi_filename),
             MSI_PYTHON3_X64_DESCRIPTION.format(version=version),
         ),
-        (
-            '9',
-            version,
-            X86_USER_AGENT_PATTERN,
-            '%s/%s' % (MERCURIAL_SCM_BASE_URL, python2_x86_msi_filename),
-            MSI_PYTHON2_X86_DESCRIPTION.format(version=version),
-        ),
-        (
-            '9',
-            version,
-            X64_USER_AGENT_PATTERN,
-            '%s/%s' % (MERCURIAL_SCM_BASE_URL, python2_x64_msi_filename),
-            MSI_PYTHON2_X64_DESCRIPTION.format(version=version),
-        ),
     )
 
     lines = ['\t'.join(e) for e in entries]
--- a/contrib/automation/linux-requirements-py2.txt	Wed May 04 18:00:01 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,129 +0,0 @@
-#
-# This file is autogenerated by pip-compile
-# To update, run:
-#
-#    pip-compile --generate-hashes --output-file=contrib/automation/linux-requirements-py2.txt contrib/automation/linux-requirements.txt.in
-#
-astroid==1.6.6 \
-    --hash=sha256:87de48a92e29cedf7210ffa853d11441e7ad94cb47bacd91b023499b51cbc756 \
-    --hash=sha256:d25869fc7f44f1d9fb7d24fd7ea0639656f5355fc3089cd1f3d18c6ec6b124c7 \
-    # via pylint
-backports.functools-lru-cache==1.6.1 \
-    --hash=sha256:0bada4c2f8a43d533e4ecb7a12214d9420e66eb206d54bf2d682581ca4b80848 \
-    --hash=sha256:8fde5f188da2d593bd5bc0be98d9abc46c95bb8a9dde93429570192ee6cc2d4a \
-    # via astroid, isort, pylint
-bzr==2.7.0 ; python_version <= "2.7" and platform_python_implementation == "CPython" \
-    --hash=sha256:c9f6bbe0a50201dadc5fddadd94ba50174193c6cf6e39e16f6dd0ad98a1df338 \
-    # via -r contrib/automation/linux-requirements.txt.in
-configparser==4.0.2 \
-    --hash=sha256:254c1d9c79f60c45dfde850850883d5aaa7f19a23f13561243a050d5a7c3fe4c \
-    --hash=sha256:c7d282687a5308319bf3d2e7706e575c635b0a470342641c93bea0ea3b5331df \
-    # via pylint
-contextlib2==0.6.0.post1 \
-    --hash=sha256:01f490098c18b19d2bd5bb5dc445b2054d2fa97f09a4280ba2c5f3c394c8162e \
-    --hash=sha256:3355078a159fbb44ee60ea80abd0d87b80b78c248643b49aa6d94673b413609b \
-    # via vcrpy
-docutils==0.16 \
-    --hash=sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af \
-    --hash=sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc \
-    # via -r contrib/automation/linux-requirements.txt.in
-enum34==1.1.10 \
-    --hash=sha256:a98a201d6de3f2ab3db284e70a33b0f896fbf35f8086594e8c9e74b909058d53 \
-    --hash=sha256:c3858660960c984d6ab0ebad691265180da2b43f07e061c0f8dca9ef3cffd328 \
-    --hash=sha256:cce6a7477ed816bd2542d03d53db9f0db935dd013b70f336a95c73979289f248 \
-    # via astroid
-funcsigs==1.0.2 \
-    --hash=sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca \
-    --hash=sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50 \
-    # via mock
-futures==3.3.0 \
-    --hash=sha256:49b3f5b064b6e3afc3316421a3f25f66c137ae88f068abbf72830170033c5e16 \
-    --hash=sha256:7e033af76a5e35f58e56da7a91e687706faf4e7bdfb2cbc3f2cca6b9bcda9794 \
-    # via isort
-fuzzywuzzy==0.18.0 \
-    --hash=sha256:45016e92264780e58972dca1b3d939ac864b78437422beecebb3095f8efd00e8 \
-    --hash=sha256:928244b28db720d1e0ee7587acf660ea49d7e4c632569cad4f1cd7e68a5f0993 \
-    # via -r contrib/automation/linux-requirements.txt.in
-isort==4.3.21 \
-    --hash=sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1 \
-    --hash=sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd \
-    # via pylint
-lazy-object-proxy==1.5.1 \
-    --hash=sha256:00b78a97a79d0dfefa584d44dd1aba9668d3de7ec82335ba0ff51d53ef107143 \
-    --hash=sha256:042b54fd71c2092e6d10e5e66fa60f65c5954f8145e809f5d9f394c9b13d32ee \
-    --hash=sha256:11f87dc06eb5f376cc6d5f0c19a1b4dca202035622777c4ce8e5b72c87b035d6 \
-    --hash=sha256:19ae6f6511a02008ef3554e158c41bb2a8e5c8455935b98d6da076d9f152fd7c \
-    --hash=sha256:22c1935c6f8e3d6ea2e169eb03928adbdb8a2251d2890f8689368d65e70aa176 \
-    --hash=sha256:30ef2068f4f94660144515380ef04b93d15add2214eab8be4cd46ebc900d681c \
-    --hash=sha256:33da47ba3a581860ddd3d38c950a5fe950ca389f7123edd0d6ab0bc473499fe7 \
-    --hash=sha256:3e8698dc384857413580012f4ca322d89e63ef20fc3d4635a5b606d6d4b61f6a \
-    --hash=sha256:4fdd7113fc5143c72dacf415079eec42fcbe69cc9d3d291b4ca742e3a9455807 \
-    --hash=sha256:63b6d9a5077d54db271fcc6772440f7380ec3fa559d0e2497dbfae2f47c2c814 \
-    --hash=sha256:8133b63b05f12751cddd8e3e7f02ba39dc7cfa7d2ba99d80d7436f0ba26d6b75 \
-    --hash=sha256:89b8e5780e49753e2b4cd5aab45d3df092ddcbba3de2c4d4492a029588fe1758 \
-    --hash=sha256:8d82e27cbbea6edb8821751806f39f5dcfd7b46a5e23d27b98d6d8c8ec751df8 \
-    --hash=sha256:92cedd6e26712505adb1c17fab64651a498cc0102a80ba562ff4a2451088f57a \
-    --hash=sha256:9723364577b79ad9958a68851fe2acb94da6fd25170c595516a8289e6a129043 \
-    --hash=sha256:c484020ad26973a14a7cb1e1d2e0bfe97cf6803273ae9bd154e0213cc74bad49 \
-    --hash=sha256:c697bd1b333b3e6abdff04ef9f5fb4b1936633d9cc4e28d90606705c9083254c \
-    --hash=sha256:d0f7e14ff3424639d33e6bc449e77e4b345e52c21bbd6f6004a1d219196e2664 \
-    --hash=sha256:db2df3eff7ed3e6813638686f1bb5934d1a0662d9d3b4196b5164a86be3a1e8f \
-    --hash=sha256:edbcb4c5efabd93ede05b272296a5a78a67e9b6e82ba7f51a07b8103db06ce01 \
-    --hash=sha256:ef355fb3802e0fc5a71dadb65a3c317bfc9bdf567d357f8e0b1900b432ffe486 \
-    --hash=sha256:fe2f61fed5817bf8db01d9a72309ed5990c478a077e9585b58740c26774bce39 \
-    # via astroid
-mccabe==0.6.1 \
-    --hash=sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42 \
-    --hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f \
-    # via pylint
-mock==3.0.5 \
-    --hash=sha256:83657d894c90d5681d62155c82bda9c1187827525880eda8ff5df4ec813437c3 \
-    --hash=sha256:d157e52d4e5b938c550f39eb2fd15610db062441a9c2747d3dbfa9298211d0f8 \
-    # via vcrpy
-pyflakes==2.2.0 \
-    --hash=sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92 \
-    --hash=sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8 \
-    # via -r contrib/automation/linux-requirements.txt.in
-pygments==2.5.2 \
-    --hash=sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b \
-    --hash=sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe \
-    # via -r contrib/automation/linux-requirements.txt.in
-pylint==1.9.5 \
-    --hash=sha256:367e3d49813d349a905390ac27989eff82ab84958731c5ef0bef867452cfdc42 \
-    --hash=sha256:97a42df23d436c70132971d1dcb9efad2fe5c0c6add55b90161e773caf729300 \
-    # via -r contrib/automation/linux-requirements.txt.in
-python-levenshtein==0.12.0 \
-    --hash=sha256:033a11de5e3d19ea25c9302d11224e1a1898fe5abd23c61c7c360c25195e3eb1 \
-    # via -r contrib/automation/linux-requirements.txt.in
-pyyaml==5.3.1 \
-    --hash=sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97 \
-    --hash=sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76 \
-    --hash=sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2 \
-    --hash=sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648 \
-    --hash=sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf \
-    --hash=sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f \
-    --hash=sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2 \
-    --hash=sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee \
-    --hash=sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d \
-    --hash=sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c \
-    --hash=sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a \
-    # via vcrpy
-singledispatch==3.4.0.3 \
-    --hash=sha256:5b06af87df13818d14f08a028e42f566640aef80805c3b50c5056b086e3c2b9c \
-    --hash=sha256:833b46966687b3de7f438c761ac475213e53b306740f1abfaa86e1d1aae56aa8 \
-    # via astroid, pylint
-six==1.15.0 \
-    --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \
-    --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced \
-    # via astroid, mock, pylint, singledispatch, vcrpy
-vcrpy==3.0.0 \
-    --hash=sha256:21168d5ae14263a833d4b71acfd8278d8841114f24be1b4ab4a5719d0c7f07bc \
-    --hash=sha256:a2e6b653a627f9f3d6ded4d68587e470b91e4c1444e7dae939510dfeacb65276 \
-    # via -r contrib/automation/linux-requirements.txt.in
-wrapt==1.12.1 \
-    --hash=sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7 \
-    # via astroid, vcrpy
-
-# WARNING: The following packages were not pinned, but pip requires them to be
-# pinned when the requirements file includes hashes. Consider using the --allow-unsafe flag.
-# setuptools
--- a/contrib/bdiff-torture.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/bdiff-torture.py	Wed May 04 18:17:44 2022 +0200
@@ -1,6 +1,5 @@
 # Randomized torture test generation for bdiff
 
-from __future__ import absolute_import, print_function
 import random
 import sys
 
--- a/contrib/benchmarks/__init__.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/benchmarks/__init__.py	Wed May 04 18:17:44 2022 +0200
@@ -31,7 +31,6 @@
     $ asv --config contrib/asv.conf.json preview
 '''
 
-from __future__ import absolute_import
 
 import functools
 import os
--- a/contrib/benchmarks/perf.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/benchmarks/perf.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from . import perfbench
 
--- a/contrib/benchmarks/revset.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/benchmarks/revset.py	Wed May 04 18:17:44 2022 +0200
@@ -10,7 +10,6 @@
 Each revset benchmark is parameterized with variants (first, last, sort, ...)
 '''
 
-from __future__ import absolute_import
 
 import os
 import string
--- a/contrib/byteify-strings.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/byteify-strings.py	Wed May 04 18:17:44 2022 +0200
@@ -7,7 +7,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import, print_function
 
 import argparse
 import contextlib
--- a/contrib/casesmash.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/casesmash.py	Wed May 04 18:17:44 2022 +0200
@@ -1,4 +1,3 @@
-from __future__ import absolute_import
 import __builtin__
 import os
 from mercurial import util
--- a/contrib/catapipe.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/catapipe.py	Wed May 04 18:17:44 2022 +0200
@@ -34,7 +34,6 @@
 HGCATAPULTSERVERPIPE environment variable, which both run-tests and hg
 understand. To trace *only* run-tests, use HGTESTCATAPULTSERVERPIPE instead.
 """
-from __future__ import absolute_import, print_function
 
 import argparse
 import json
--- a/contrib/check-code.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/check-code.py	Wed May 04 18:17:44 2022 +0200
@@ -19,7 +19,6 @@
  * ONLY use no--check-code for skipping entire files from external sources
 """
 
-from __future__ import absolute_import, print_function
 import glob
 import keyword
 import optparse
@@ -344,16 +343,6 @@
             "linebreak after :",
         ),
         (
-            r'class\s[^( \n]+:',
-            "old-style class, use class foo(object)",
-            r'#.*old-style',
-        ),
-        (
-            r'class\s[^( \n]+\(\):',
-            "class foo() creates old style object, use class foo(object)",
-            r'#.*old-style',
-        ),
-        (
             r'\b(%s)\('
             % '|'.join(k for k in keyword.kwlist if k not in ('print', 'exec')),
             "Python keyword is not a function",
@@ -431,26 +420,6 @@
             "module-level @cachefunc is risky, please avoid",
         ),
         (
-            r'^import Queue',
-            "don't use Queue, use pycompat.queue.Queue + "
-            "pycompat.queue.Empty",
-        ),
-        (
-            r'^import cStringIO',
-            "don't use cStringIO.StringIO, use util.stringio",
-        ),
-        (r'^import urllib', "don't use urllib, use util.urlreq/util.urlerr"),
-        (
-            r'^import SocketServer',
-            "don't use SockerServer, use util.socketserver",
-        ),
-        (r'^import urlparse', "don't use urlparse, use util.urlreq"),
-        (r'^import xmlrpclib', "don't use xmlrpclib, use util.xmlrpclib"),
-        (r'^import cPickle', "don't use cPickle, use util.pickle"),
-        (r'^import pickle', "don't use pickle, use util.pickle"),
-        (r'^import httplib', "don't use httplib, use util.httplib"),
-        (r'^import BaseHTTPServer', "use util.httpserver instead"),
-        (
             r'^(from|import) mercurial\.(cext|pure|cffi)',
             "use mercurial.policy.importmod instead",
         ),
@@ -789,7 +758,7 @@
             preparefilters(filters)
 
 
-class norepeatlogger(object):
+class norepeatlogger:
     def __init__(self):
         self._lastseen = None
 
--- a/contrib/check-commit	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/check-commit	Wed May 04 18:17:44 2022 +0200
@@ -15,7 +15,6 @@
 #
 # See also: https://mercurial-scm.org/wiki/ContributingChanges
 
-from __future__ import absolute_import, print_function
 
 import os
 import re
--- a/contrib/check-config.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/check-config.py	Wed May 04 18:17:44 2022 +0200
@@ -7,7 +7,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import, print_function
 import re
 import sys
 
--- a/contrib/check-py3-compat.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/check-py3-compat.py	Wed May 04 18:17:44 2022 +0200
@@ -7,7 +7,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import, print_function
 
 import ast
 import importlib
@@ -17,31 +16,6 @@
 import warnings
 
 
-def check_compat_py2(f):
-    """Check Python 3 compatibility for a file with Python 2"""
-    with open(f, 'rb') as fh:
-        content = fh.read()
-    root = ast.parse(content)
-
-    # Ignore empty files.
-    if not root.body:
-        return
-
-    futures = set()
-    haveprint = False
-    for node in ast.walk(root):
-        if isinstance(node, ast.ImportFrom):
-            if node.module == '__future__':
-                futures |= {n.name for n in node.names}
-        elif isinstance(node, ast.Print):
-            haveprint = True
-
-    if 'absolute_import' not in futures:
-        print('%s not using absolute_import' % f)
-    if haveprint and 'print_function' not in futures:
-        print('%s requires print_function' % f)
-
-
 def check_compat_py3(f):
     """Check Python 3 compatibility of a file with Python 3."""
     with open(f, 'rb') as fh:
@@ -94,23 +68,19 @@
 
 
 if __name__ == '__main__':
-    if sys.version_info[0] == 2:
-        fn = check_compat_py2
-    else:
-        # check_compat_py3 will import every filename we specify as long as it
-        # starts with one of a few prefixes. It does this by converting
-        # specified filenames like 'mercurial/foo.py' to 'mercurial.foo' and
-        # importing that. When running standalone (not as part of a test), this
-        # means we actually import the installed versions, not the files we just
-        # specified. When running as test-check-py3-compat.t, we technically
-        # would import the correct paths, but it's cleaner to have both cases
-        # use the same import logic.
-        sys.path.insert(0, '.')
-        fn = check_compat_py3
+    # check_compat_py3 will import every filename we specify as long as it
+    # starts with one of a few prefixes. It does this by converting
+    # specified filenames like 'mercurial/foo.py' to 'mercurial.foo' and
+    # importing that. When running standalone (not as part of a test), this
+    # means we actually import the installed versions, not the files we just
+    # specified. When running as test-check-py3-compat.t, we technically
+    # would import the correct paths, but it's cleaner to have both cases
+    # use the same import logic.
+    sys.path.insert(0, '.')
 
     for f in sys.argv[1:]:
         with warnings.catch_warnings(record=True) as warns:
-            fn(f)
+            check_compat_py3(f)
 
         for w in warns:
             print(
--- a/contrib/debugcmdserver.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/debugcmdserver.py	Wed May 04 18:17:44 2022 +0200
@@ -7,7 +7,6 @@
 # $ ./hg serve --cmds pipe | ./contrib/debugcmdserver.py -
 # o, 52   -> 'capabilities: getencoding runcommand\nencoding: UTF-8'
 
-from __future__ import absolute_import, print_function
 import struct
 import sys
 
--- a/contrib/debugshell.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/debugshell.py	Wed May 04 18:17:44 2022 +0200
@@ -1,7 +1,6 @@
 # debugshell extension
 """a python shell with repo, changelog & manifest objects"""
 
-from __future__ import absolute_import
 import code
 import mercurial
 import sys
--- a/contrib/dumprevlog	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/dumprevlog	Wed May 04 18:17:44 2022 +0200
@@ -2,7 +2,6 @@
 # Dump revlogs as raw data stream
 # $ find .hg/store/ -name "*.i" | xargs dumprevlog > repo.dump
 
-from __future__ import absolute_import, print_function
 
 import sys
 from mercurial.node import hex
--- a/contrib/fuzz/dirs_corpus.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/fuzz/dirs_corpus.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function
-
 import argparse
 import zipfile
 
--- a/contrib/fuzz/dirstate_corpus.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/fuzz/dirstate_corpus.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function
-
 import argparse
 import os
 import zipfile
--- a/contrib/fuzz/fm1readmarkers_corpus.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/fuzz/fm1readmarkers_corpus.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function
-
 import argparse
 import zipfile
 
--- a/contrib/fuzz/manifest_corpus.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/fuzz/manifest_corpus.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function
-
 import argparse
 import zipfile
 
--- a/contrib/fuzz/mpatch_corpus.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/fuzz/mpatch_corpus.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function
-
 import argparse
 import os
 import struct
@@ -22,7 +20,7 @@
 
 if sys.version_info[0] < 3:
 
-    class py2reprhack(object):
+    class py2reprhack:
         def __repr__(self):
             """Py2 calls __repr__ for `bytes(foo)`, forward to __bytes__"""
             return self.__bytes__()
@@ -30,7 +28,7 @@
 
 else:
 
-    class py2reprhack(object):
+    class py2reprhack:
         """Not needed on py3."""
 
 
--- a/contrib/fuzz/revlog_corpus.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/fuzz/revlog_corpus.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import argparse
 import os
 import zipfile
--- a/contrib/genosxversion.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/genosxversion.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,4 @@
 #!/usr/bin/env python2
-from __future__ import absolute_import, print_function
 
 import argparse
 import os
--- a/contrib/heptapod-ci.yml	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/heptapod-ci.yml	Wed May 04 18:17:44 2022 +0200
@@ -30,22 +30,19 @@
         - echo "$RUNTEST_ARGS"
         - HGTESTS_ALLOW_NETIO="$TEST_HGTESTS_ALLOW_NETIO" HGMODULEPOLICY="$TEST_HGMODULEPOLICY" "$PYTHON" tests/run-tests.py --color=always $RUNTEST_ARGS
 
-checks-py2:
-    <<: *runtests
-    variables:
-        RUNTEST_ARGS: "--time --test-list /tmp/check-tests.txt"
-
-checks-py3:
+checks:
     <<: *runtests
     variables:
         RUNTEST_ARGS: "--time --test-list /tmp/check-tests.txt"
         PYTHON: python3
 
-rust-cargo-test-py3:
+rust-cargo-test:
     stage: tests
     script:
         - echo "python used, $PYTHON"
         - make rust-tests
+    variables:
+        PYTHON: python3
 
 phabricator-refresh:
     stage: phabricator
@@ -65,14 +62,7 @@
             ./contrib/phab-refresh-stack.sh --comment "$DEFAULT_COMMENT";
         fi
 
-test-py2:
-    <<: *runtests
-    variables:
-        RUNTEST_ARGS: " --no-rust --blacklist /tmp/check-tests.txt"
-        TEST_HGMODULEPOLICY: "c"
-        TEST_HGTESTS_ALLOW_NETIO: "1"
-
-test-py3:
+test-c:
     <<: *runtests
     variables:
         RUNTEST_ARGS: " --no-rust --blacklist /tmp/check-tests.txt"
@@ -80,20 +70,14 @@
         TEST_HGMODULEPOLICY: "c"
         TEST_HGTESTS_ALLOW_NETIO: "1"
 
-test-py2-pure:
-    <<: *runtests
-    variables:
-        RUNTEST_ARGS: "--pure --blacklist /tmp/check-tests.txt"
-        TEST_HGMODULEPOLICY: "py"
-
-test-py3-pure:
+test-pure:
     <<: *runtests
     variables:
         RUNTEST_ARGS: "--pure --blacklist /tmp/check-tests.txt"
         PYTHON: python3
         TEST_HGMODULEPOLICY: "py"
 
-test-py3-rust:
+test-rust:
     <<: *runtests
     variables:
         HGWITHRUSTEXT: cpython
@@ -101,7 +85,7 @@
         PYTHON: python3
         TEST_HGMODULEPOLICY: "rust+c"
 
-test-py3-rhg:
+test-rhg:
     <<: *runtests
     variables:
         HGWITHRUSTEXT: cpython
@@ -109,20 +93,14 @@
         PYTHON: python3
         TEST_HGMODULEPOLICY: "rust+c"
 
-test-py2-chg:
-    <<: *runtests
-    variables:
-        RUNTEST_ARGS: "--blacklist /tmp/check-tests.txt --chg"
-        TEST_HGMODULEPOLICY: "c"
-
-test-py3-chg:
+test-chg:
     <<: *runtests
     variables:
         PYTHON: python3
         RUNTEST_ARGS: "--blacklist /tmp/check-tests.txt --chg"
         TEST_HGMODULEPOLICY: "c"
 
-check-pytype-py3:
+check-pytype:
     extends: .runtests_template
     before_script:
       - hg clone . /tmp/mercurial-ci/ --noupdate --config phases.publish=no
@@ -160,7 +138,7 @@
 
         - C:/MinGW/msys/1.0/bin/sh.exe --login -c 'cd "$OLDPWD" && HGTESTS_ALLOW_NETIO="$TEST_HGTESTS_ALLOW_NETIO" HGMODULEPOLICY="$TEST_HGMODULEPOLICY" $PYTHON tests/run-tests.py --color=always $RUNTEST_ARGS'
 
-windows-py3:
+windows:
     <<: *windows_runtests
     tags:
       - windows
@@ -169,7 +147,7 @@
         RUNTEST_ARGS: "--blacklist C:/Temp/check-tests.txt"
         PYTHON: py -3
 
-windows-py3-pyox:
+windows-pyox:
     <<: *windows_runtests
     tags:
       - windows
--- a/contrib/hg-ssh	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/hg-ssh	Wed May 04 18:17:44 2022 +0200
@@ -28,7 +28,6 @@
 You can also add a --read-only flag to allow read-only access to a key, e.g.:
 command="hg-ssh --read-only repos/*"
 """
-from __future__ import absolute_import
 
 import os
 import re
--- a/contrib/hgclient.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/hgclient.py	Wed May 04 18:17:44 2022 +0200
@@ -1,6 +1,5 @@
 # A minimal client for Mercurial's command server
 
-from __future__ import absolute_import, print_function
 
 import io
 import os
@@ -50,7 +49,7 @@
     return server
 
 
-class unixconnection(object):
+class unixconnection:
     def __init__(self, sockpath):
         self.sock = sock = socket.socket(socket.AF_UNIX)
         sock.connect(sockpath)
@@ -63,7 +62,7 @@
         self.sock.close()
 
 
-class unixserver(object):
+class unixserver:
     def __init__(self, sockpath, logpath=None, repopath=None):
         self.sockpath = sockpath
         cmdline = [b'hg', b'serve', b'--cmdserver', b'unix', b'-a', sockpath]
--- a/contrib/import-checker.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/import-checker.py	Wed May 04 18:17:44 2022 +0200
@@ -1,6 +1,5 @@
 #!/usr/bin/env python3
 
-from __future__ import absolute_import, print_function
 
 import ast
 import collections
@@ -20,10 +19,11 @@
 
 import testparseutil
 
-# Whitelist of modules that symbols can be directly imported from.
+# Allow list of modules that symbols can be directly imported from.
 allowsymbolimports = (
     '__future__',
     'breezy',
+    'concurrent',
     'hgclient',
     'mercurial',
     'mercurial.hgweb.common',
@@ -46,9 +46,10 @@
     'mercurial.thirdparty.attr',
     'mercurial.thirdparty.zope',
     'mercurial.thirdparty.zope.interface',
+    'typing',
 )
 
-# Whitelist of symbols that can be directly imported.
+# Allow list of symbols that can be directly imported.
 directsymbols = ('demandimport',)
 
 # Modules that must be aliased because they are commonly confused with
@@ -58,21 +59,6 @@
 }
 
 
-def usingabsolute(root):
-    """Whether absolute imports are being used."""
-    if sys.version_info[0] >= 3:
-        return True
-
-    for node in ast.walk(root):
-        if isinstance(node, ast.ImportFrom):
-            if node.module == '__future__':
-                for n in node.names:
-                    if n.name == 'absolute_import':
-                        return True
-
-    return False
-
-
 def walklocal(root):
     """Recursively yield all descendant nodes but not in a different scope"""
     todo = collections.deque(ast.iter_child_nodes(root))
@@ -402,21 +388,10 @@
 
 
 def verify_import_convention(module, source, localmods):
-    """Verify imports match our established coding convention.
-
-    We have 2 conventions: legacy and modern. The modern convention is in
-    effect when using absolute imports.
+    """Verify imports match our established coding convention."""
+    root = ast.parse(source)
 
-    The legacy convention only looks for mixed imports. The modern convention
-    is much more thorough.
-    """
-    root = ast.parse(source)
-    absolute = usingabsolute(root)
-
-    if absolute:
-        return verify_modern_convention(module, root, localmods)
-    else:
-        return verify_stdlib_on_own_line(root)
+    return verify_modern_convention(module, root, localmods)
 
 
 def verify_modern_convention(module, root, localmods, root_col_offset=0):
@@ -617,33 +592,6 @@
                     )
 
 
-def verify_stdlib_on_own_line(root):
-    """Given some python source, verify that stdlib imports are done
-    in separate statements from relative local module imports.
-
-    >>> list(verify_stdlib_on_own_line(ast.parse('import sys, foo')))
-    [('mixed imports\\n   stdlib:    sys\\n   relative:  foo', 1)]
-    >>> list(verify_stdlib_on_own_line(ast.parse('import sys, os')))
-    []
-    >>> list(verify_stdlib_on_own_line(ast.parse('import foo, bar')))
-    []
-    """
-    for node in ast.walk(root):
-        if isinstance(node, ast.Import):
-            from_stdlib = {False: [], True: []}
-            for n in node.names:
-                from_stdlib[n.name in stdlib_modules].append(n.name)
-            if from_stdlib[True] and from_stdlib[False]:
-                yield (
-                    'mixed imports\n   stdlib:    %s\n   relative:  %s'
-                    % (
-                        ', '.join(sorted(from_stdlib[True])),
-                        ', '.join(sorted(from_stdlib[False])),
-                    ),
-                    node.lineno,
-                )
-
-
 class CircularImport(Exception):
     pass
 
@@ -679,7 +627,6 @@
 
     All module names recorded in `imports` should be absolute one.
 
-    >>> from __future__ import print_function
     >>> imports = {'top.foo': ['top.bar', 'os.path', 'top.qux'],
     ...            'top.bar': ['top.baz', 'sys'],
     ...            'top.baz': ['top.foo'],
--- a/contrib/install-windows-dependencies.ps1	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/install-windows-dependencies.ps1	Wed May 04 18:17:44 2022 +0200
@@ -29,19 +29,19 @@
 $PYTHON38_x64_URL = "https://www.python.org/ftp/python/3.8.10/python-3.8.10-amd64.exe"
 $PYTHON38_x64_SHA256 = "7628244cb53408b50639d2c1287c659f4e29d3dfdb9084b11aed5870c0c6a48a"
 
-$PYTHON39_x86_URL = "https://www.python.org/ftp/python/3.9.9/python-3.9.9.exe"
-$PYTHON39_x86_SHA256 = "6646a5683adf14d35e8c53aab946895bc0f0b825f7acac3a62cc85ee7d0dc71a"
-$PYTHON39_X64_URL = "https://www.python.org/ftp/python/3.9.9/python-3.9.9-amd64.exe"
-$PYTHON39_x64_SHA256 = "137d59e5c0b01a8f1bdcba08344402ae658c81c6bf03b6602bd8b4e951ad0714"
+$PYTHON39_x86_URL = "https://www.python.org/ftp/python/3.9.12/python-3.9.12.exe"
+$PYTHON39_x86_SHA256 = "3d883326f30ac231c06b33f2a8ea700a185c20bf98d01da118079e9134d5fd20"
+$PYTHON39_X64_URL = "https://www.python.org/ftp/python/3.9.12/python-3.9.12-amd64.exe"
+$PYTHON39_x64_SHA256 = "2ba57ab2281094f78fc0227a27f4d47c90d94094e7cca35ce78419e616b3cb63"
 
-$PYTHON310_x86_URL = "https://www.python.org/ftp/python/3.10.0/python-3.10.0.exe"
-$PYTHON310_x86_SHA256 = "ea896eeefb1db9e12fb89ec77a6e28c9fe52b4a162a34c85d9688be2ec2392e8"
-$PYTHON310_X64_URL = "https://www.python.org/ftp/python/3.10.0/python-3.10.0-amd64.exe"
-$PYTHON310_x64_SHA256 = "cb580eb7dc55f9198e650f016645023e8b2224cf7d033857d12880b46c5c94ef"
+$PYTHON310_x86_URL = "https://www.python.org/ftp/python/3.10.4/python-3.10.4.exe"
+$PYTHON310_x86_SHA256 = "97c37c53c7a826f5b00e185754ab2a324a919f7afc469b20764b71715c80041d"
+$PYTHON310_X64_URL = "https://www.python.org/ftp/python/3.10.4/python-3.10.4-amd64.exe"
+$PYTHON310_x64_SHA256 = "a81fc4180f34e5733c3f15526c668ff55de096366f9006d8a44c0336704e50f1"
 
-# PIP 19.2.3.
-$PIP_URL = "https://github.com/pypa/get-pip/raw/309a56c5fd94bd1134053a541cb4657a4e47e09d/get-pip.py"
-$PIP_SHA256 = "57e3643ff19f018f8a00dfaa6b7e4620e3c1a7a2171fd218425366ec006b3bfe"
+# PIP 22.0.4.
+$PIP_URL = "https://github.com/pypa/get-pip/raw/38e54e5de07c66e875c11a1ebbdb938854625dd8/public/get-pip.py"
+$PIP_SHA256 = "e235c437e5c7d7524fbce3880ca39b917a73dc565e0c813465b7a7a329bb279a"
 
 $INNO_SETUP_URL = "http://files.jrsoftware.org/is/5/innosetup-5.6.1-unicode.exe"
 $INNO_SETUP_SHA256 = "27D49E9BC769E9D1B214C153011978DB90DC01C2ACD1DDCD9ED7B3FE3B96B538"
@@ -90,7 +90,13 @@
     $p = Start-Process -FilePath $path -ArgumentList $arguments -Wait -PassThru -WindowStyle Hidden
 
     if ($p.ExitCode -ne 0) {
-        throw "process exited non-0: $($p.ExitCode)"
+        # If the MSI is already installed, ignore the error
+        if ($p.ExitCode -eq 1638) {
+            Write-Output "program already installed; continuing..."
+        }
+        else {
+            throw "process exited non-0: $($p.ExitCode)"
+        }
     }
 }
 
@@ -150,7 +156,7 @@
     Install-Python3 "Python 3.7 32-bit" ${prefix}\assets\python37-x86.exe ${prefix}\python37-x86 ${pip}
     Install-Python3 "Python 3.7 64-bit" ${prefix}\assets\python37-x64.exe ${prefix}\python37-x64 ${pip}
     Install-Python3 "Python 3.8 32-bit" ${prefix}\assets\python38-x86.exe ${prefix}\python38-x86 ${pip}
-#    Install-Python3 "Python 3.8 64-bit" ${prefix}\assets\python38-x64.exe ${prefix}\python38-x64 ${pip}
+    Install-Python3 "Python 3.8 64-bit" ${prefix}\assets\python38-x64.exe ${prefix}\python38-x64 ${pip}
     Install-Python3 "Python 3.9 32-bit" ${prefix}\assets\python39-x86.exe ${prefix}\python39-x86 ${pip}
     Install-Python3 "Python 3.9 64-bit" ${prefix}\assets\python39-x64.exe ${prefix}\python39-x64 ${pip}
     Install-Python3 "Python 3.10 32-bit" ${prefix}\assets\python310-x86.exe ${prefix}\python310-x86 ${pip}
--- a/contrib/memory.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/memory.py	Wed May 04 18:17:44 2022 +0200
@@ -11,8 +11,6 @@
 prints it to ``stderr`` on exit.
 '''
 
-from __future__ import absolute_import
-
 
 def memusage(ui):
     """Report memory usage of the current process."""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/merge-lists/Cargo.lock	Wed May 04 18:17:44 2022 +0200
@@ -0,0 +1,560 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "assert_cmd"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93ae1ddd39efd67689deb1979d80bad3bf7f2b09c6e6117c8d1f2443b5e2f83e"
+dependencies = [
+ "bstr",
+ "doc-comment",
+ "predicates",
+ "predicates-core",
+ "predicates-tree",
+ "wait-timeout",
+]
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bstr"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
+dependencies = [
+ "lazy_static",
+ "memchr",
+ "regex-automata",
+]
+
+[[package]]
+name = "clap"
+version = "3.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8c93436c21e4698bacadf42917db28b23017027a4deccb35dbe47a7e7840123"
+dependencies = [
+ "atty",
+ "bitflags",
+ "clap_derive",
+ "indexmap",
+ "lazy_static",
+ "os_str_bytes",
+ "strsim",
+ "termcolor",
+ "textwrap",
+]
+
+[[package]]
+name = "clap_derive"
+version = "3.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da95d038ede1a964ce99f49cbe27a7fb538d1da595e4b4f70b8c8f338d17bf16"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "console"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a28b32d32ca44b70c3e4acd7db1babf555fa026e385fb95f18028f88848b3c31"
+dependencies = [
+ "encode_unicode",
+ "libc",
+ "once_cell",
+ "terminal_size",
+ "winapi",
+]
+
+[[package]]
+name = "difflib"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
+
+[[package]]
+name = "doc-comment"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
+
+[[package]]
+name = "either"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
+
+[[package]]
+name = "encode_unicode"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
+
+[[package]]
+name = "fuchsia-cprng"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
+
+[[package]]
+name = "hashbrown"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+
+[[package]]
+name = "heck"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "insta"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30a7e1911532a662f6b08b68f884080850f2fd9544963c3ab23a5af42bda1eac"
+dependencies = [
+ "console",
+ "once_cell",
+ "serde",
+ "serde_json",
+ "serde_yaml",
+ "similar",
+]
+
+[[package]]
+name = "itertools"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4"
+
+[[package]]
+name = "linked-hash-map"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
+
+[[package]]
+name = "memchr"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
+
+[[package]]
+name = "merge-lists"
+version = "0.1.0"
+dependencies = [
+ "assert_cmd",
+ "clap",
+ "insta",
+ "itertools",
+ "regex",
+ "similar",
+ "tempdir",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9"
+
+[[package]]
+name = "os_str_bytes"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "predicates"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5aab5be6e4732b473071984b3164dbbfb7a3674d30ea5ff44410b6bcd960c3c"
+dependencies = [
+ "difflib",
+ "itertools",
+ "predicates-core",
+]
+
+[[package]]
+name = "predicates-core"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da1c2388b1513e1b605fcec39a95e0a9e8ef088f71443ef37099fa9ae6673fcb"
+
+[[package]]
+name = "predicates-tree"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d86de6de25020a36c6d3643a86d9a6a9f552107c0559c60ea03551b5e16c032"
+dependencies = [
+ "predicates-core",
+ "termtree",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
+dependencies = [
+ "fuchsia-cprng",
+ "libc",
+ "rand_core 0.3.1",
+ "rdrand",
+ "winapi",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
+dependencies = [
+ "rand_core 0.4.2",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
+
+[[package]]
+name = "rdrand"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "regex"
+version = "1.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
+
+[[package]]
+name = "serde"
+version = "1.0.136"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.136"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_yaml"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4a521f2940385c165a24ee286aa8599633d162077a54bdcae2a6fd5a7bfa7a0"
+dependencies = [
+ "indexmap",
+ "ryu",
+ "serde",
+ "yaml-rust",
+]
+
+[[package]]
+name = "similar"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e24979f63a11545f5f2c60141afe249d4f19f84581ea2138065e400941d83d3"
+dependencies = [
+ "bstr",
+]
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "syn"
+version = "1.0.87"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e59d925cf59d8151f25a3bedf97c9c157597c9df7324d32d68991cc399ed08b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "tempdir"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8"
+dependencies = [
+ "rand",
+ "remove_dir_all",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "terminal_size"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "termtree"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "507e9898683b6c43a9aa55b64259b721b52ba226e0f3779137e50ad114a4c90b"
+
+[[package]]
+name = "textwrap"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "wait-timeout"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "yaml-rust"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
+dependencies = [
+ "linked-hash-map",
+]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/merge-lists/Cargo.toml	Wed May 04 18:17:44 2022 +0200
@@ -0,0 +1,21 @@
+# A tool that performs a 3-way merge, resolving conflicts in sorted lists and
+# leaving other conflicts unchanged. This is useful with Mercurial's support
+# for partial merge tools (configured in `[partial-merge-tools]`).
+
+[package]
+name = "merge-lists"
+version = "0.1.0"
+edition = "2021"
+# We need https://github.com/rust-lang/rust/pull/89825
+rust-version = "1.59"
+
+[dependencies]
+clap = { version = "3.1.6", features = ["derive"] }
+itertools = "0.10.3"
+regex = "1.5.5"
+similar = { version="2.1.0", features = ["bytes"] }
+
+[dev-dependencies]
+assert_cmd = "2.0.4"
+insta = "1.13.0"
+tempdir = "0.3.7"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/merge-lists/src/main.rs	Wed May 04 18:17:44 2022 +0200
@@ -0,0 +1,300 @@
+use clap::{ArgGroup, Parser};
+use itertools::Itertools;
+use regex::bytes::Regex;
+use similar::ChangeTag;
+use std::cmp::{max, min, Ordering};
+use std::collections::HashSet;
+use std::ffi::OsString;
+use std::ops::Range;
+use std::path::PathBuf;
+
+fn find_unchanged_ranges(
+    old_bytes: &[u8],
+    new_bytes: &[u8],
+) -> Vec<(Range<usize>, Range<usize>)> {
+    let diff = similar::TextDiff::configure()
+        .algorithm(similar::Algorithm::Patience)
+        .diff_lines(old_bytes, new_bytes);
+    let mut new_unchanged_ranges = vec![];
+    let mut old_index = 0;
+    let mut new_index = 0;
+    for diff in diff.iter_all_changes() {
+        match diff.tag() {
+            ChangeTag::Equal => {
+                new_unchanged_ranges.push((
+                    old_index..old_index + diff.value().len(),
+                    new_index..new_index + diff.value().len(),
+                ));
+                old_index += diff.value().len();
+                new_index += diff.value().len();
+            }
+            ChangeTag::Delete => {
+                old_index += diff.value().len();
+            }
+            ChangeTag::Insert => {
+                new_index += diff.value().len();
+            }
+        }
+    }
+    new_unchanged_ranges
+}
+
+/// Returns a list of all the lines in the input (including trailing newlines),
+/// but only if they all match the regex and they are sorted.
+fn get_lines<'input>(
+    input: &'input [u8],
+    regex: &Regex,
+) -> Option<Vec<&'input [u8]>> {
+    let lines = input.split_inclusive(|x| *x == b'\n').collect_vec();
+    let mut previous_line = "".as_bytes();
+    for line in &lines {
+        if *line < previous_line {
+            return None;
+        }
+        if !regex.is_match(line) {
+            return None;
+        }
+        previous_line = line;
+    }
+    Some(lines)
+}
+
+fn resolve_conflict(
+    base_slice: &[u8],
+    local_slice: &[u8],
+    other_slice: &[u8],
+    regex: &Regex,
+) -> Option<Vec<u8>> {
+    let base_lines = get_lines(base_slice, regex)?;
+    let local_lines = get_lines(local_slice, regex)?;
+    let other_lines = get_lines(other_slice, regex)?;
+    let base_lines_set: HashSet<_> = base_lines.iter().copied().collect();
+    let local_lines_set: HashSet<_> = local_lines.iter().copied().collect();
+    let other_lines_set: HashSet<_> = other_lines.iter().copied().collect();
+    let mut result = local_lines_set;
+    for to_add in other_lines_set.difference(&base_lines_set) {
+        result.insert(to_add);
+    }
+    for to_remove in base_lines_set.difference(&other_lines_set) {
+        result.remove(to_remove);
+    }
+    Some(result.into_iter().sorted().collect_vec().concat())
+}
+
+fn resolve(
+    base_bytes: &[u8],
+    local_bytes: &[u8],
+    other_bytes: &[u8],
+    regex: &Regex,
+) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
+    // Find unchanged ranges between the base and the two sides. We do that by
+    // initially considering the whole base unchanged. Then we compare each
+    // side with the base and intersect the unchanged ranges we find with
+    // what we had before.
+    let unchanged_ranges = vec![UnchangedRange {
+        base_range: 0..base_bytes.len(),
+        offsets: vec![],
+    }];
+    let unchanged_ranges = intersect_regions(
+        unchanged_ranges,
+        &find_unchanged_ranges(base_bytes, local_bytes),
+    );
+    let mut unchanged_ranges = intersect_regions(
+        unchanged_ranges,
+        &find_unchanged_ranges(base_bytes, other_bytes),
+    );
+    // Add an empty UnchangedRange at the end to make it easier to find change
+    // ranges. That way there's a changed range before each UnchangedRange.
+    unchanged_ranges.push(UnchangedRange {
+        base_range: base_bytes.len()..base_bytes.len(),
+        offsets: vec![
+            local_bytes.len().wrapping_sub(base_bytes.len()) as isize,
+            other_bytes.len().wrapping_sub(base_bytes.len()) as isize,
+        ],
+    });
+
+    let mut new_base_bytes: Vec<u8> = vec![];
+    let mut new_local_bytes: Vec<u8> = vec![];
+    let mut new_other_bytes: Vec<u8> = vec![];
+    let mut previous = UnchangedRange {
+        base_range: 0..0,
+        offsets: vec![0, 0],
+    };
+    for current in unchanged_ranges {
+        let base_slice =
+            &base_bytes[previous.base_range.end..current.base_range.start];
+        let local_slice = &local_bytes[previous.end(0)..current.start(0)];
+        let other_slice = &other_bytes[previous.end(1)..current.start(1)];
+        if let Some(resolution) =
+            resolve_conflict(base_slice, local_slice, other_slice, regex)
+        {
+            new_base_bytes.extend(&resolution);
+            new_local_bytes.extend(&resolution);
+            new_other_bytes.extend(&resolution);
+        } else {
+            new_base_bytes.extend(base_slice);
+            new_local_bytes.extend(local_slice);
+            new_other_bytes.extend(other_slice);
+        }
+        new_base_bytes.extend(&base_bytes[current.base_range.clone()]);
+        new_local_bytes.extend(&local_bytes[current.start(0)..current.end(0)]);
+        new_other_bytes.extend(&other_bytes[current.start(1)..current.end(1)]);
+        previous = current;
+    }
+
+    (new_base_bytes, new_local_bytes, new_other_bytes)
+}
+
+/// A tool that performs a 3-way merge, resolving conflicts in sorted lists and
+/// leaving other conflicts unchanged. This is useful with Mercurial's support
+/// for partial merge tools (configured in `[partial-merge-tools]`).
+#[derive(Parser, Debug)]
+#[clap(version, about, long_about = None)]
+#[clap(group(ArgGroup::new("match").required(true).args(&["pattern", "python-imports"])))]
+struct Args {
+    /// Path to the file's content in the "local" side
+    local: OsString,
+
+    /// Path to the file's content in the base
+    base: OsString,
+
+    /// Path to the file's content in the "other" side
+    other: OsString,
+
+    /// Regular expression to use
+    #[clap(long, short)]
+    pattern: Option<String>,
+
+    /// Use built-in regular expression for Python imports
+    #[clap(long)]
+    python_imports: bool,
+}
+
+fn get_regex(args: &Args) -> Regex {
+    let pattern = if args.python_imports {
+        r"import \w+(\.\w+)*( +#.*)?\n|from (\w+(\.\w+)* import \w+( as \w+)?(, \w+( as \w+)?)*( +#.*)?)"
+    } else if let Some(pattern) = &args.pattern {
+        pattern
+    } else {
+        ".*"
+    };
+    let pattern = format!(r"{}\r?\n?", pattern);
+    regex::bytes::Regex::new(&pattern).unwrap()
+}
+
+fn main() {
+    let args: Args = Args::parse();
+
+    let base_path = PathBuf::from(&args.base);
+    let local_path = PathBuf::from(&args.local);
+    let other_path = PathBuf::from(&args.other);
+
+    let base_bytes = std::fs::read(&base_path).unwrap();
+    let local_bytes = std::fs::read(&local_path).unwrap();
+    let other_bytes = std::fs::read(&other_path).unwrap();
+
+    let regex = get_regex(&args);
+    let (new_base_bytes, new_local_bytes, new_other_bytes) =
+        resolve(&base_bytes, &local_bytes, &other_bytes, &regex);
+
+    // Write out the result if anything changed
+    if new_base_bytes != base_bytes {
+        std::fs::write(&base_path, new_base_bytes).unwrap();
+    }
+    if new_local_bytes != local_bytes {
+        std::fs::write(&local_path, new_local_bytes).unwrap();
+    }
+    if new_other_bytes != other_bytes {
+        std::fs::write(&other_path, new_other_bytes).unwrap();
+    }
+}
+
+fn checked_add(base: usize, offset: isize) -> usize {
+    if offset < 0 {
+        base.checked_sub(offset.checked_abs().unwrap() as usize)
+            .unwrap()
+    } else {
+        base.checked_add(offset as usize).unwrap()
+    }
+}
+
+// The remainder of the file is copied from
+// https://github.com/martinvonz/jj/blob/main/lib/src/diff.rs
+
+#[derive(Clone, PartialEq, Eq, Debug)]
+struct UnchangedRange {
+    base_range: Range<usize>,
+    offsets: Vec<isize>,
+}
+
+impl UnchangedRange {
+    fn start(&self, side: usize) -> usize {
+        checked_add(self.base_range.start, self.offsets[side])
+    }
+
+    fn end(&self, side: usize) -> usize {
+        checked_add(self.base_range.end, self.offsets[side])
+    }
+}
+
+impl PartialOrd for UnchangedRange {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl Ord for UnchangedRange {
+    fn cmp(&self, other: &Self) -> Ordering {
+        self.base_range
+            .start
+            .cmp(&other.base_range.start)
+            .then_with(|| self.base_range.end.cmp(&other.base_range.end))
+    }
+}
+
+/// Takes the current regions and intersects it with the new unchanged ranges
+/// from a 2-way diff. The result is a map of unchanged regions with one more
+/// offset in the map's values.
+fn intersect_regions(
+    current_ranges: Vec<UnchangedRange>,
+    new_unchanged_ranges: &[(Range<usize>, Range<usize>)],
+) -> Vec<UnchangedRange> {
+    let mut result = vec![];
+    let mut current_ranges_iter = current_ranges.into_iter().peekable();
+    for (new_base_range, other_range) in new_unchanged_ranges.iter() {
+        assert_eq!(new_base_range.len(), other_range.len());
+        while let Some(UnchangedRange {
+            base_range,
+            offsets,
+        }) = current_ranges_iter.peek()
+        {
+            // No need to look further if we're past the new range.
+            if base_range.start >= new_base_range.end {
+                break;
+            }
+            // Discard any current unchanged regions that don't match between
+            // the base and the new input.
+            if base_range.end <= new_base_range.start {
+                current_ranges_iter.next();
+                continue;
+            }
+            let new_start = max(base_range.start, new_base_range.start);
+            let new_end = min(base_range.end, new_base_range.end);
+            let mut new_offsets = offsets.clone();
+            new_offsets
+                .push(other_range.start.wrapping_sub(new_base_range.start)
+                    as isize);
+            result.push(UnchangedRange {
+                base_range: new_start..new_end,
+                offsets: new_offsets,
+            });
+            if base_range.end >= new_base_range.end {
+                // Break without consuming the item; there may be other new
+                // ranges that overlap with it.
+                break;
+            }
+            current_ranges_iter.next();
+        }
+    }
+    result
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/merge-lists/tests/test-merge-lists.rs	Wed May 04 18:17:44 2022 +0200
@@ -0,0 +1,204 @@
+use similar::DiffableStr;
+use std::ffi::OsStr;
+use tempdir::TempDir;
+
+fn run_test(arg: &str, input: &str) -> String {
+    let mut cmd = assert_cmd::Command::cargo_bin("merge-lists").unwrap();
+    let temp_dir = TempDir::new("test").unwrap();
+    let base_path = temp_dir.path().join("base");
+    let local_path = temp_dir.path().join("local");
+    let other_path = temp_dir.path().join("other");
+
+    let rest = input.strip_prefix("\nbase:\n").unwrap();
+    let mut split = rest.split("\nlocal:\n");
+    std::fs::write(&base_path, split.next().unwrap()).unwrap();
+    let rest = split.next().unwrap();
+    let mut split = rest.split("\nother:\n");
+    std::fs::write(&local_path, split.next().unwrap()).unwrap();
+    std::fs::write(&other_path, split.next().unwrap()).unwrap();
+    cmd.args(&[
+        OsStr::new(arg),
+        local_path.as_os_str(),
+        base_path.as_os_str(),
+        other_path.as_os_str(),
+    ])
+    .assert()
+    .success();
+
+    let new_base_bytes = std::fs::read(&base_path).unwrap();
+    let new_local_bytes = std::fs::read(&local_path).unwrap();
+    let new_other_bytes = std::fs::read(&other_path).unwrap();
+    // No newline before "base:" because of https://github.com/mitsuhiko/insta/issues/117
+    format!(
+        "base:\n{}\nlocal:\n{}\nother:\n{}",
+        new_base_bytes.as_str().unwrap(),
+        new_local_bytes.as_str().unwrap(),
+        new_other_bytes.as_str().unwrap()
+    )
+}
+
+#[test]
+fn test_merge_lists_basic() {
+    let output = run_test(
+        "--python-imports",
+        r"
+base:
+import lib1
+import lib2
+
+local:
+import lib2
+import lib3
+
+other:
+import lib3
+import lib4
+",
+    );
+    insta::assert_snapshot!(output, @r###"
+    base:
+    import lib3
+    import lib4
+
+    local:
+    import lib3
+    import lib4
+
+    other:
+    import lib3
+    import lib4
+    "###);
+}
+
+#[test]
+fn test_merge_lists_from() {
+    // Test some "from x import y" statements and some non-import conflicts
+    // (unresolvable)
+    let output = run_test(
+        "--python-imports",
+        r"
+base:
+from . import x
+
+1+1
+
+local:
+from . import x
+from a import b
+
+2+2
+
+other:
+from a import c
+
+3+3
+",
+    );
+    insta::assert_snapshot!(output, @r###"
+    base:
+    from a import b
+    from a import c
+
+    1+1
+
+    local:
+    from a import b
+    from a import c
+
+    2+2
+
+    other:
+    from a import b
+    from a import c
+
+    3+3
+    "###);
+}
+
+#[test]
+fn test_merge_lists_not_sorted() {
+    // Test that nothing is done if the elements in the conflicting hunks are
+    // not sorted
+    let output = run_test(
+        "--python-imports",
+        r"
+base:
+import x
+
+1+1
+
+local:
+import a
+import x
+
+2+2
+
+other:
+import z
+import y
+
+3+3
+",
+    );
+    insta::assert_snapshot!(output, @r###"
+    base:
+    import x
+
+    1+1
+
+    local:
+    import a
+    import x
+
+    2+2
+
+    other:
+    import z
+    import y
+
+    3+3
+    "###);
+}
+
+#[test]
+fn test_custom_regex() {
+    // Test merging of all lines (by matching anything)
+    let output = run_test(
+        "--pattern=.*",
+        r"
+base:
+aardvark
+baboon
+camel
+
+local:
+aardvark
+camel
+eagle
+
+other:
+aardvark
+camel
+deer
+",
+    );
+    insta::assert_snapshot!(output, @r###"
+    base:
+    aardvark
+    camel
+    deer
+    eagle
+
+    local:
+    aardvark
+    camel
+    deer
+    eagle
+
+    other:
+    aardvark
+    camel
+    deer
+    eagle
+    "###);
+}
--- a/contrib/packaging/debian/rules	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/packaging/debian/rules	Wed May 04 18:17:44 2022 +0200
@@ -92,10 +92,8 @@
 	mkdir -p "$(CURDIR)"/debian/mercurial/etc/mercurial/hgrc.d/
 	cp contrib/packaging/debian/*.rc "$(CURDIR)"/debian/mercurial/etc/mercurial/hgrc.d/
 	# completions
-	mkdir -p "$(CURDIR)"/debian/mercurial/usr/share/bash-completion/completions
-	cp contrib/bash_completion "$(CURDIR)"/debian/mercurial/usr/share/bash-completion/completions/hg
 	mkdir -p "$(CURDIR)"/debian/mercurial/usr/share/zsh/vendor-completions
-	cp contrib/zsh_completion "$(CURDIR)"/debian/mercurial/usr/share/zsh/vendor-completions/_hg
+	mv "$(CURDIR)"/debian/mercurial/usr/share/zsh/site-functions/_hg "$(CURDIR)"/debian/mercurial/usr/share/zsh/vendor-completions/_hg
 	if [ "$(DEB_HG_CHG_BY_DEFAULT)" -eq 1 ]; then \
 		mkdir -p "$(CURDIR)"/debian/mercurial/usr/lib/mercurial; \
 		mv "$(CURDIR)"/debian/mercurial/usr/bin/hg "$(CURDIR)"/debian/mercurial/usr/lib/mercurial/hg; \
--- a/contrib/packaging/hgpackaging/cli.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/packaging/hgpackaging/cli.py	Wed May 04 18:17:44 2022 +0200
@@ -20,13 +20,7 @@
 SOURCE_DIR = HERE.parent.parent.parent
 
 
-def build_inno(pyoxidizer_target=None, python=None, iscc=None, version=None):
-    if not pyoxidizer_target and not python:
-        raise Exception("--python required unless building with PyOxidizer")
-
-    if python and not os.path.isabs(python):
-        raise Exception("--python arg must be an absolute path")
-
+def build_inno(pyoxidizer_target, iscc=None, version=None):
     if iscc:
         iscc = pathlib.Path(iscc)
     else:
@@ -38,59 +32,30 @@
 
     build_dir = SOURCE_DIR / "build"
 
-    if pyoxidizer_target:
-        inno.build_with_pyoxidizer(
-            SOURCE_DIR, build_dir, pyoxidizer_target, iscc, version=version
-        )
-    else:
-        inno.build_with_py2exe(
-            SOURCE_DIR,
-            build_dir,
-            pathlib.Path(python),
-            iscc,
-            version=version,
-        )
+    inno.build_with_pyoxidizer(
+        SOURCE_DIR, build_dir, pyoxidizer_target, iscc, version=version
+    )
 
 
 def build_wix(
+    pyoxidizer_target,
     name=None,
-    pyoxidizer_target=None,
-    python=None,
     version=None,
     sign_sn=None,
     sign_cert=None,
     sign_password=None,
     sign_timestamp_url=None,
-    extra_packages_script=None,
     extra_wxs=None,
     extra_features=None,
     extra_pyoxidizer_vars=None,
 ):
-    if not pyoxidizer_target and not python:
-        raise Exception("--python required unless building with PyOxidizer")
-
-    if python and not os.path.isabs(python):
-        raise Exception("--python arg must be an absolute path")
-
     kwargs = {
         "source_dir": SOURCE_DIR,
         "version": version,
+        "target_triple": pyoxidizer_target,
+        "extra_pyoxidizer_vars": extra_pyoxidizer_vars,
     }
 
-    if pyoxidizer_target:
-        fn = wix.build_installer_pyoxidizer
-        kwargs["target_triple"] = pyoxidizer_target
-        kwargs["extra_pyoxidizer_vars"] = extra_pyoxidizer_vars
-    else:
-        fn = wix.build_installer_py2exe
-        kwargs["python_exe"] = pathlib.Path(python)
-
-    if extra_packages_script:
-        if pyoxidizer_target:
-            raise Exception(
-                "pyoxidizer does not support --extra-packages-script"
-            )
-        kwargs["extra_packages_script"] = extra_packages_script
     if extra_wxs:
         kwargs["extra_wxs"] = dict(
             thing.split("=") for thing in extra_wxs.split(",")
@@ -107,7 +72,7 @@
             "timestamp_url": sign_timestamp_url,
         }
 
-    fn(**kwargs)
+    wix.build_installer_pyoxidizer(**kwargs)
 
 
 def get_parser():
@@ -119,14 +84,14 @@
     sp.add_argument(
         "--pyoxidizer-target",
         choices={"i686-pc-windows-msvc", "x86_64-pc-windows-msvc"},
+        required=True,
         help="Build with PyOxidizer targeting this host triple",
     )
-    sp.add_argument("--python", help="path to python.exe to use")
     sp.add_argument("--iscc", help="path to iscc.exe to use")
     sp.add_argument(
         "--version",
         help="Mercurial version string to use "
-        "(detected from __version__.py if not defined",
+        "(detected from __version__.py if not defined)",
     )
     sp.set_defaults(func=build_inno)
 
@@ -137,9 +102,9 @@
     sp.add_argument(
         "--pyoxidizer-target",
         choices={"i686-pc-windows-msvc", "x86_64-pc-windows-msvc"},
+        required=True,
         help="Build with PyOxidizer targeting this host triple",
     )
-    sp.add_argument("--python", help="Path to Python executable to use")
     sp.add_argument(
         "--sign-sn",
         help="Subject name (or fragment thereof) of certificate "
@@ -155,12 +120,6 @@
     )
     sp.add_argument("--version", help="Version string to use")
     sp.add_argument(
-        "--extra-packages-script",
-        help=(
-            "Script to execute to include extra packages in " "py2exe binary."
-        ),
-    )
-    sp.add_argument(
         "--extra-wxs", help="CSV of path_to_wxs_file=working_dir_for_wxs_file"
     )
     sp.add_argument(
--- a/contrib/packaging/hgpackaging/downloads.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/packaging/hgpackaging/downloads.py	Wed May 04 18:17:44 2022 +0200
@@ -10,6 +10,7 @@
 import gzip
 import hashlib
 import pathlib
+import typing
 import urllib.request
 
 
@@ -25,48 +26,6 @@
         'size': 715086,
         'sha256': '411f94974492fd2ecf52590cb05b1023530aec67e64154a88b1e4ebcd9c28588',
     },
-    'py2exe': {
-        'url': 'https://versaweb.dl.sourceforge.net/project/py2exe/py2exe/0.6.9/py2exe-0.6.9.zip',
-        'size': 149687,
-        'sha256': '6bd383312e7d33eef2e43a5f236f9445e4f3e0f6b16333c6f183ed445c44ddbd',
-        'version': '0.6.9',
-    },
-    # The VC9 CRT merge modules aren't readily available on most systems because
-    # they are only installed as part of a full Visual Studio 2008 install.
-    # While we could potentially extract them from a Visual Studio 2008
-    # installer, it is easier to just fetch them from a known URL.
-    'vc9-crt-x86-msm': {
-        'url': 'https://github.com/indygreg/vc90-merge-modules/raw/9232f8f0b2135df619bf7946eaa176b4ac35ccff/Microsoft_VC90_CRT_x86.msm',
-        'size': 615424,
-        'sha256': '837e887ef31b332feb58156f429389de345cb94504228bb9a523c25a9dd3d75e',
-    },
-    'vc9-crt-x86-msm-policy': {
-        'url': 'https://github.com/indygreg/vc90-merge-modules/raw/9232f8f0b2135df619bf7946eaa176b4ac35ccff/policy_9_0_Microsoft_VC90_CRT_x86.msm',
-        'size': 71168,
-        'sha256': '3fbcf92e3801a0757f36c5e8d304e134a68d5cafd197a6df7734ae3e8825c940',
-    },
-    'vc9-crt-x64-msm': {
-        'url': 'https://github.com/indygreg/vc90-merge-modules/raw/9232f8f0b2135df619bf7946eaa176b4ac35ccff/Microsoft_VC90_CRT_x86_x64.msm',
-        'size': 662528,
-        'sha256': '50d9639b5ad4844a2285269c7551bf5157ec636e32396ddcc6f7ec5bce487a7c',
-    },
-    'vc9-crt-x64-msm-policy': {
-        'url': 'https://github.com/indygreg/vc90-merge-modules/raw/9232f8f0b2135df619bf7946eaa176b4ac35ccff/policy_9_0_Microsoft_VC90_CRT_x86_x64.msm',
-        'size': 71168,
-        'sha256': '0550ea1929b21239134ad3a678c944ba0f05f11087117b6cf0833e7110686486',
-    },
-    'virtualenv': {
-        'url': 'https://files.pythonhosted.org/packages/37/db/89d6b043b22052109da35416abc3c397655e4bd3cff031446ba02b9654fa/virtualenv-16.4.3.tar.gz',
-        'size': 3713208,
-        'sha256': '984d7e607b0a5d1329425dd8845bd971b957424b5ba664729fab51ab8c11bc39',
-        'version': '16.4.3',
-    },
-    'wix': {
-        'url': 'https://github.com/wixtoolset/wix3/releases/download/wix3111rtm/wix311-binaries.zip',
-        'size': 34358269,
-        'sha256': '37f0a533b0978a454efb5dc3bd3598becf9660aaf4287e55bf68ca6b527d051d',
-        'version': '3.11.1',
-    },
 }
 
 
@@ -168,8 +127,8 @@
 
 
 def download_entry(
-    name: dict, dest_path: pathlib.Path, local_name=None
-) -> pathlib.Path:
+    name: str, dest_path: pathlib.Path, local_name=None
+) -> typing.Tuple[pathlib.Path, typing.Dict[str, typing.Union[str, int]]]:
     entry = DOWNLOADS[name]
 
     url = entry['url']
--- a/contrib/packaging/hgpackaging/inno.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/packaging/hgpackaging/inno.py	Wed May 04 18:17:44 2022 +0200
@@ -14,29 +14,13 @@
 
 import jinja2
 
-from .py2exe import (
-    build_py2exe,
-    stage_install,
-)
 from .pyoxidizer import create_pyoxidizer_install_layout
 from .util import (
-    find_legacy_vc_runtime_files,
     normalize_windows_version,
     process_install_rules,
     read_version_py,
 )
 
-EXTRA_PACKAGES = {
-    'dulwich',
-    'keyring',
-    'pygments',
-    'win32ctypes',
-}
-
-EXTRA_INCLUDES = {
-    '_curses',
-    '_curses_panel',
-}
 
 EXTRA_INSTALL_RULES = [
     ('contrib/win32/mercurial.ini', 'defaultrc/mercurial.rc'),
@@ -47,80 +31,6 @@
 }
 
 
-def build_with_py2exe(
-    source_dir: pathlib.Path,
-    build_dir: pathlib.Path,
-    python_exe: pathlib.Path,
-    iscc_exe: pathlib.Path,
-    version=None,
-):
-    """Build the Inno installer using py2exe.
-
-    Build files will be placed in ``build_dir``.
-
-    py2exe's setup.py doesn't use setuptools. It doesn't have modern logic
-    for finding the Python 2.7 toolchain. So, we require the environment
-    to already be configured with an active toolchain.
-    """
-    if not iscc_exe.exists():
-        raise Exception('%s does not exist' % iscc_exe)
-
-    vc_x64 = r'\x64' in os.environ.get('LIB', '')
-    arch = 'x64' if vc_x64 else 'x86'
-    inno_build_dir = build_dir / ('inno-py2exe-%s' % arch)
-    staging_dir = inno_build_dir / 'stage'
-
-    requirements_txt = (
-        source_dir / 'contrib' / 'packaging' / 'requirements-windows-py2.txt'
-    )
-
-    inno_build_dir.mkdir(parents=True, exist_ok=True)
-
-    build_py2exe(
-        source_dir,
-        build_dir,
-        python_exe,
-        'inno',
-        requirements_txt,
-        extra_packages=EXTRA_PACKAGES,
-        extra_includes=EXTRA_INCLUDES,
-    )
-
-    # Purge the staging directory for every build so packaging is
-    # pristine.
-    if staging_dir.exists():
-        print('purging %s' % staging_dir)
-        shutil.rmtree(staging_dir)
-
-    # Now assemble all the packaged files into the staging directory.
-    stage_install(source_dir, staging_dir)
-
-    # We also install some extra files.
-    process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
-
-    # hg.exe depends on VC9 runtime DLLs. Copy those into place.
-    for f in find_legacy_vc_runtime_files(vc_x64):
-        if f.name.endswith('.manifest'):
-            basename = 'Microsoft.VC90.CRT.manifest'
-        else:
-            basename = f.name
-
-        dest_path = staging_dir / basename
-
-        print('copying %s to %s' % (f, dest_path))
-        shutil.copyfile(f, dest_path)
-
-    build_installer(
-        source_dir,
-        inno_build_dir,
-        staging_dir,
-        iscc_exe,
-        version,
-        arch="x64" if vc_x64 else None,
-        suffix="-python2",
-    )
-
-
 def build_with_pyoxidizer(
     source_dir: pathlib.Path,
     build_dir: pathlib.Path,
--- a/contrib/packaging/hgpackaging/py2exe.py	Wed May 04 18:00:01 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,248 +0,0 @@
-# py2exe.py - Functionality for performing py2exe builds.
-#
-# Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
-#
-# This software may be used and distributed according to the terms of the
-# GNU General Public License version 2 or any later version.
-
-# no-check-code because Python 3 native.
-
-import os
-import pathlib
-import subprocess
-
-from .downloads import download_entry
-from .util import (
-    extract_tar_to_directory,
-    extract_zip_to_directory,
-    process_install_rules,
-    python_exe_info,
-)
-
-
-STAGING_RULES = [
-    ('contrib/bash_completion', 'contrib/'),
-    ('contrib/hgk', 'contrib/hgk.tcl'),
-    ('contrib/hgweb.fcgi', 'contrib/'),
-    ('contrib/hgweb.wsgi', 'contrib/'),
-    ('contrib/logo-droplets.svg', 'contrib/'),
-    ('contrib/mercurial.el', 'contrib/'),
-    ('contrib/mq.el', 'contrib/'),
-    ('contrib/tcsh_completion', 'contrib/'),
-    ('contrib/tcsh_completion_build.sh', 'contrib/'),
-    ('contrib/vim/*', 'contrib/vim/'),
-    ('contrib/win32/postinstall.txt', 'ReleaseNotes.txt'),
-    ('contrib/win32/ReadMe.html', 'ReadMe.html'),
-    ('contrib/xml.rnc', 'contrib/'),
-    ('contrib/zsh_completion', 'contrib/'),
-    ('dist/hg.exe', './'),
-    ('dist/lib/*.dll', 'lib/'),
-    ('dist/lib/*.pyd', 'lib/'),
-    ('dist/lib/library.zip', 'lib/'),
-    ('dist/Microsoft.VC*.CRT.manifest', './'),
-    ('dist/msvc*.dll', './'),
-    ('dist/python*.dll', './'),
-    ('doc/*.html', 'doc/'),
-    ('doc/style.css', 'doc/'),
-    ('mercurial/helptext/**/*.txt', 'helptext/'),
-    ('mercurial/defaultrc/*.rc', 'defaultrc/'),
-    ('mercurial/locale/**/*', 'locale/'),
-    ('mercurial/templates/**/*', 'templates/'),
-    ('COPYING', 'Copying.txt'),
-]
-
-# List of paths to exclude from the staging area.
-STAGING_EXCLUDES = [
-    'doc/hg-ssh.8.html',
-]
-
-
-def build_py2exe(
-    source_dir: pathlib.Path,
-    build_dir: pathlib.Path,
-    python_exe: pathlib.Path,
-    build_name: str,
-    venv_requirements_txt: pathlib.Path,
-    extra_packages=None,
-    extra_excludes=None,
-    extra_dll_excludes=None,
-    extra_packages_script=None,
-    extra_includes=None,
-):
-    """Build Mercurial with py2exe.
-
-    Build files will be placed in ``build_dir``.
-
-    py2exe's setup.py doesn't use setuptools. It doesn't have modern logic
-    for finding the Python 2.7 toolchain. So, we require the environment
-    to already be configured with an active toolchain.
-    """
-    if 'VCINSTALLDIR' not in os.environ:
-        raise Exception(
-            'not running from a Visual C++ build environment; '
-            'execute the "Visual C++ <version> Command Prompt" '
-            'application shortcut or a vcsvarsall.bat file'
-        )
-
-    # Identity x86/x64 and validate the environment matches the Python
-    # architecture.
-    vc_x64 = r'\x64' in os.environ['LIB']
-
-    py_info = python_exe_info(python_exe)
-
-    if vc_x64:
-        if py_info['arch'] != '64bit':
-            raise Exception(
-                'architecture mismatch: Visual C++ environment '
-                'is configured for 64-bit but Python is 32-bit'
-            )
-    else:
-        if py_info['arch'] != '32bit':
-            raise Exception(
-                'architecture mismatch: Visual C++ environment '
-                'is configured for 32-bit but Python is 64-bit'
-            )
-
-    if py_info['py3']:
-        raise Exception('Only Python 2 is currently supported')
-
-    build_dir.mkdir(exist_ok=True)
-
-    gettext_pkg, gettext_entry = download_entry('gettext', build_dir)
-    gettext_dep_pkg = download_entry('gettext-dep', build_dir)[0]
-    virtualenv_pkg, virtualenv_entry = download_entry('virtualenv', build_dir)
-    py2exe_pkg, py2exe_entry = download_entry('py2exe', build_dir)
-
-    venv_path = build_dir / (
-        'venv-%s-%s' % (build_name, 'x64' if vc_x64 else 'x86')
-    )
-
-    gettext_root = build_dir / ('gettext-win-%s' % gettext_entry['version'])
-
-    if not gettext_root.exists():
-        extract_zip_to_directory(gettext_pkg, gettext_root)
-        extract_zip_to_directory(gettext_dep_pkg, gettext_root)
-
-    # This assumes Python 2. We don't need virtualenv on Python 3.
-    virtualenv_src_path = build_dir / (
-        'virtualenv-%s' % virtualenv_entry['version']
-    )
-    virtualenv_py = virtualenv_src_path / 'virtualenv.py'
-
-    if not virtualenv_src_path.exists():
-        extract_tar_to_directory(virtualenv_pkg, build_dir)
-
-    py2exe_source_path = build_dir / ('py2exe-%s' % py2exe_entry['version'])
-
-    if not py2exe_source_path.exists():
-        extract_zip_to_directory(py2exe_pkg, build_dir)
-
-    if not venv_path.exists():
-        print('creating virtualenv with dependencies')
-        subprocess.run(
-            [str(python_exe), str(virtualenv_py), str(venv_path)], check=True
-        )
-
-    venv_python = venv_path / 'Scripts' / 'python.exe'
-    venv_pip = venv_path / 'Scripts' / 'pip.exe'
-
-    subprocess.run(
-        [str(venv_pip), 'install', '-r', str(venv_requirements_txt)], check=True
-    )
-
-    # Force distutils to use VC++ settings from environment, which was
-    # validated above.
-    env = dict(os.environ)
-    env['DISTUTILS_USE_SDK'] = '1'
-    env['MSSdk'] = '1'
-
-    if extra_packages_script:
-        more_packages = set(
-            subprocess.check_output(extra_packages_script, cwd=build_dir)
-            .split(b'\0')[-1]
-            .strip()
-            .decode('utf-8')
-            .splitlines()
-        )
-        if more_packages:
-            if not extra_packages:
-                extra_packages = more_packages
-            else:
-                extra_packages |= more_packages
-
-    if extra_packages:
-        env['HG_PY2EXE_EXTRA_PACKAGES'] = ' '.join(sorted(extra_packages))
-        hgext3rd_extras = sorted(
-            e for e in extra_packages if e.startswith('hgext3rd.')
-        )
-        if hgext3rd_extras:
-            env['HG_PY2EXE_EXTRA_INSTALL_PACKAGES'] = ' '.join(hgext3rd_extras)
-    if extra_includes:
-        env['HG_PY2EXE_EXTRA_INCLUDES'] = ' '.join(sorted(extra_includes))
-    if extra_excludes:
-        env['HG_PY2EXE_EXTRA_EXCLUDES'] = ' '.join(sorted(extra_excludes))
-    if extra_dll_excludes:
-        env['HG_PY2EXE_EXTRA_DLL_EXCLUDES'] = ' '.join(
-            sorted(extra_dll_excludes)
-        )
-
-    py2exe_py_path = venv_path / 'Lib' / 'site-packages' / 'py2exe'
-    if not py2exe_py_path.exists():
-        print('building py2exe')
-        subprocess.run(
-            [str(venv_python), 'setup.py', 'install'],
-            cwd=py2exe_source_path,
-            env=env,
-            check=True,
-        )
-
-    # Register location of msgfmt and other binaries.
-    env['PATH'] = '%s%s%s' % (
-        env['PATH'],
-        os.pathsep,
-        str(gettext_root / 'bin'),
-    )
-
-    print('building Mercurial')
-    subprocess.run(
-        [str(venv_python), 'setup.py', 'py2exe', 'build_doc', '--html'],
-        cwd=str(source_dir),
-        env=env,
-        check=True,
-    )
-
-
-def stage_install(
-    source_dir: pathlib.Path, staging_dir: pathlib.Path, lower_case=False
-):
-    """Copy all files to be installed to a directory.
-
-    This allows packaging to simply walk a directory tree to find source
-    files.
-    """
-    if lower_case:
-        rules = []
-        for source, dest in STAGING_RULES:
-            # Only lower directory names.
-            if '/' in dest:
-                parent, leaf = dest.rsplit('/', 1)
-                dest = '%s/%s' % (parent.lower(), leaf)
-            rules.append((source, dest))
-    else:
-        rules = STAGING_RULES
-
-    process_install_rules(rules, source_dir, staging_dir)
-
-    # Write out a default editor.rc file to configure notepad as the
-    # default editor.
-    with (staging_dir / 'defaultrc' / 'editor.rc').open(
-        'w', encoding='utf-8'
-    ) as fh:
-        fh.write('[ui]\neditor = notepad\n')
-
-    # Purge any files we don't want to be there.
-    for f in STAGING_EXCLUDES:
-        p = staging_dir / f
-        if p.exists():
-            print('removing %s' % p)
-            p.unlink()
--- a/contrib/packaging/hgpackaging/pyoxidizer.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/packaging/hgpackaging/pyoxidizer.py	Wed May 04 18:17:44 2022 +0200
@@ -23,7 +23,6 @@
 
 
 STAGING_RULES_WINDOWS = [
-    ('contrib/bash_completion', 'contrib/'),
     ('contrib/hgk', 'contrib/hgk.tcl'),
     ('contrib/hgweb.fcgi', 'contrib/'),
     ('contrib/hgweb.wsgi', 'contrib/'),
@@ -36,7 +35,6 @@
     ('contrib/win32/postinstall.txt', 'ReleaseNotes.txt'),
     ('contrib/win32/ReadMe.html', 'ReadMe.html'),
     ('contrib/xml.rnc', 'contrib/'),
-    ('contrib/zsh_completion', 'contrib/'),
     ('doc/*.html', 'doc/'),
     ('doc/style.css', 'doc/'),
     ('COPYING', 'Copying.txt'),
--- a/contrib/packaging/hgpackaging/util.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/packaging/hgpackaging/util.py	Wed May 04 18:17:44 2022 +0200
@@ -7,23 +7,15 @@
 
 # no-check-code because Python 3 native.
 
-import distutils.version
-import getpass
 import glob
 import os
 import pathlib
 import re
 import shutil
 import subprocess
-import tarfile
 import zipfile
 
 
-def extract_tar_to_directory(source: pathlib.Path, dest: pathlib.Path):
-    with tarfile.open(source, 'r') as tf:
-        tf.extractall(dest)
-
-
 def extract_zip_to_directory(source: pathlib.Path, dest: pathlib.Path):
     with zipfile.ZipFile(source, 'r') as zf:
         zf.extractall(dest)
@@ -81,59 +73,6 @@
     raise Exception("could not find vcruntime140.dll")
 
 
-def find_legacy_vc_runtime_files(x64=False):
-    """Finds Visual C++ Runtime DLLs to include in distribution."""
-    winsxs = pathlib.Path(os.environ['SYSTEMROOT']) / 'WinSxS'
-
-    prefix = 'amd64' if x64 else 'x86'
-
-    candidates = sorted(
-        p
-        for p in os.listdir(winsxs)
-        if p.lower().startswith('%s_microsoft.vc90.crt_' % prefix)
-    )
-
-    for p in candidates:
-        print('found candidate VC runtime: %s' % p)
-
-    # Take the newest version.
-    version = candidates[-1]
-
-    d = winsxs / version
-
-    return [
-        d / 'msvcm90.dll',
-        d / 'msvcp90.dll',
-        d / 'msvcr90.dll',
-        winsxs / 'Manifests' / ('%s.manifest' % version),
-    ]
-
-
-def windows_10_sdk_info():
-    """Resolves information about the Windows 10 SDK."""
-
-    base = pathlib.Path(os.environ['ProgramFiles(x86)']) / 'Windows Kits' / '10'
-
-    if not base.is_dir():
-        raise Exception('unable to find Windows 10 SDK at %s' % base)
-
-    # Find the latest version.
-    bin_base = base / 'bin'
-
-    versions = [v for v in os.listdir(bin_base) if v.startswith('10.')]
-    version = sorted(versions, reverse=True)[0]
-
-    bin_version = bin_base / version
-
-    return {
-        'root': base,
-        'version': version,
-        'bin_root': bin_version,
-        'bin_x86': bin_version / 'x86',
-        'bin_x64': bin_version / 'x64',
-    }
-
-
 def normalize_windows_version(version):
     """Normalize Mercurial version string so WiX/Inno accepts it.
 
@@ -194,93 +133,6 @@
     return '.'.join('%d' % x for x in versions[0:4])
 
 
-def find_signtool():
-    """Find signtool.exe from the Windows SDK."""
-    sdk = windows_10_sdk_info()
-
-    for key in ('bin_x64', 'bin_x86'):
-        p = sdk[key] / 'signtool.exe'
-
-        if p.exists():
-            return p
-
-    raise Exception('could not find signtool.exe in Windows 10 SDK')
-
-
-def sign_with_signtool(
-    file_path,
-    description,
-    subject_name=None,
-    cert_path=None,
-    cert_password=None,
-    timestamp_url=None,
-):
-    """Digitally sign a file with signtool.exe.
-
-    ``file_path`` is file to sign.
-    ``description`` is text that goes in the signature.
-
-    The signing certificate can be specified by ``cert_path`` or
-    ``subject_name``. These correspond to the ``/f`` and ``/n`` arguments
-    to signtool.exe, respectively.
-
-    The certificate password can be specified via ``cert_password``. If
-    not provided, you will be prompted for the password.
-
-    ``timestamp_url`` is the URL of a RFC 3161 timestamp server (``/tr``
-    argument to signtool.exe).
-    """
-    if cert_path and subject_name:
-        raise ValueError('cannot specify both cert_path and subject_name')
-
-    while cert_path and not cert_password:
-        cert_password = getpass.getpass('password for %s: ' % cert_path)
-
-    args = [
-        str(find_signtool()),
-        'sign',
-        '/v',
-        '/fd',
-        'sha256',
-        '/d',
-        description,
-    ]
-
-    if cert_path:
-        args.extend(['/f', str(cert_path), '/p', cert_password])
-    elif subject_name:
-        args.extend(['/n', subject_name])
-
-    if timestamp_url:
-        args.extend(['/tr', timestamp_url, '/td', 'sha256'])
-
-    args.append(str(file_path))
-
-    print('signing %s' % file_path)
-    subprocess.run(args, check=True)
-
-
-PRINT_PYTHON_INFO = '''
-import platform; print("%s:%s" % (platform.architecture()[0], platform.python_version()))
-'''.strip()
-
-
-def python_exe_info(python_exe: pathlib.Path):
-    """Obtain information about a Python executable."""
-
-    res = subprocess.check_output([str(python_exe), '-c', PRINT_PYTHON_INFO])
-
-    arch, version = res.decode('utf-8').split(':')
-
-    version = distutils.version.LooseVersion(version)
-
-    return {
-        'arch': arch,
-        'version': version,
-        'py3': version >= distutils.version.LooseVersion('3'),
-    }
-
-
 def process_install_rules(
     rules: list, source_dir: pathlib.Path, dest_dir: pathlib.Path
 ):
--- a/contrib/packaging/hgpackaging/wix.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/packaging/hgpackaging/wix.py	Wed May 04 18:17:44 2022 +0200
@@ -7,376 +7,16 @@
 
 # no-check-code because Python 3 native.
 
-import collections
 import json
 import os
 import pathlib
-import re
 import shutil
-import subprocess
 import typing
-import uuid
-import xml.dom.minidom
 
-from .downloads import download_entry
-from .py2exe import (
-    build_py2exe,
-    stage_install,
-)
 from .pyoxidizer import (
     build_docs_html,
-    create_pyoxidizer_install_layout,
     run_pyoxidizer,
 )
-from .util import (
-    extract_zip_to_directory,
-    normalize_windows_version,
-    process_install_rules,
-    sign_with_signtool,
-)
-
-
-EXTRA_PACKAGES = {
-    'dulwich',
-    'distutils',
-    'keyring',
-    'pygments',
-    'win32ctypes',
-}
-
-EXTRA_INCLUDES = {
-    '_curses',
-    '_curses_panel',
-}
-
-EXTRA_INSTALL_RULES = [
-    ('contrib/packaging/wix/COPYING.rtf', 'COPYING.rtf'),
-    ('contrib/win32/mercurial.ini', 'defaultrc/mercurial.rc'),
-]
-
-STAGING_REMOVE_FILES = [
-    # We use the RTF variant.
-    'copying.txt',
-]
-
-SHORTCUTS = {
-    # hg.1.html'
-    'hg.file.5d3e441c_28d9_5542_afd0_cdd4234f12d5': {
-        'Name': 'Mercurial Command Reference',
-    },
-    # hgignore.5.html
-    'hg.file.5757d8e0_f207_5e10_a2ec_3ba0a062f431': {
-        'Name': 'Mercurial Ignore Files',
-    },
-    # hgrc.5.html
-    'hg.file.92e605fd_1d1a_5dc6_9fc0_5d2998eb8f5e': {
-        'Name': 'Mercurial Configuration Files',
-    },
-}
-
-
-def find_version(source_dir: pathlib.Path):
-    version_py = source_dir / 'mercurial' / '__version__.py'
-
-    with version_py.open('r', encoding='utf-8') as fh:
-        source = fh.read().strip()
-
-    m = re.search('version = b"(.*)"', source)
-    return m.group(1)
-
-
-def ensure_vc90_merge_modules(build_dir):
-    x86 = (
-        download_entry(
-            'vc9-crt-x86-msm',
-            build_dir,
-            local_name='microsoft.vcxx.crt.x86_msm.msm',
-        )[0],
-        download_entry(
-            'vc9-crt-x86-msm-policy',
-            build_dir,
-            local_name='policy.x.xx.microsoft.vcxx.crt.x86_msm.msm',
-        )[0],
-    )
-
-    x64 = (
-        download_entry(
-            'vc9-crt-x64-msm',
-            build_dir,
-            local_name='microsoft.vcxx.crt.x64_msm.msm',
-        )[0],
-        download_entry(
-            'vc9-crt-x64-msm-policy',
-            build_dir,
-            local_name='policy.x.xx.microsoft.vcxx.crt.x64_msm.msm',
-        )[0],
-    )
-    return {
-        'x86': x86,
-        'x64': x64,
-    }
-
-
-def run_candle(wix, cwd, wxs, source_dir, defines=None):
-    args = [
-        str(wix / 'candle.exe'),
-        '-nologo',
-        str(wxs),
-        '-dSourceDir=%s' % source_dir,
-    ]
-
-    if defines:
-        args.extend('-d%s=%s' % define for define in sorted(defines.items()))
-
-    subprocess.run(args, cwd=str(cwd), check=True)
-
-
-def make_files_xml(staging_dir: pathlib.Path, is_x64) -> str:
-    """Create XML string listing every file to be installed."""
-
-    # We derive GUIDs from a deterministic file path identifier.
-    # We shoehorn the name into something that looks like a URL because
-    # the UUID namespaces are supposed to work that way (even though
-    # the input data probably is never validated).
-
-    doc = xml.dom.minidom.parseString(
-        '<?xml version="1.0" encoding="utf-8"?>'
-        '<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">'
-        '</Wix>'
-    )
-
-    # Assemble the install layout by directory. This makes it easier to
-    # emit XML, since each directory has separate entities.
-    manifest = collections.defaultdict(dict)
-
-    for root, dirs, files in os.walk(staging_dir):
-        dirs.sort()
-
-        root = pathlib.Path(root)
-        rel_dir = root.relative_to(staging_dir)
-
-        for i in range(len(rel_dir.parts)):
-            parent = '/'.join(rel_dir.parts[0 : i + 1])
-            manifest.setdefault(parent, {})
-
-        for f in sorted(files):
-            full = root / f
-            manifest[str(rel_dir).replace('\\', '/')][full.name] = full
-
-    component_groups = collections.defaultdict(list)
-
-    # Now emit a <Fragment> for each directory.
-    # Each directory is composed of a <DirectoryRef> pointing to its parent
-    # and defines child <Directory>'s and a <Component> with all the files.
-    for dir_name, entries in sorted(manifest.items()):
-        # The directory id is derived from the path. But the root directory
-        # is special.
-        if dir_name == '.':
-            parent_directory_id = 'INSTALLDIR'
-        else:
-            parent_directory_id = 'hg.dir.%s' % dir_name.replace(
-                '/', '.'
-            ).replace('-', '_')
-
-        fragment = doc.createElement('Fragment')
-        directory_ref = doc.createElement('DirectoryRef')
-        directory_ref.setAttribute('Id', parent_directory_id)
-
-        # Add <Directory> entries for immediate children directories.
-        for possible_child in sorted(manifest.keys()):
-            if (
-                dir_name == '.'
-                and '/' not in possible_child
-                and possible_child != '.'
-            ):
-                child_directory_id = ('hg.dir.%s' % possible_child).replace(
-                    '-', '_'
-                )
-                name = possible_child
-            else:
-                if not possible_child.startswith('%s/' % dir_name):
-                    continue
-                name = possible_child[len(dir_name) + 1 :]
-                if '/' in name:
-                    continue
-
-                child_directory_id = 'hg.dir.%s' % possible_child.replace(
-                    '/', '.'
-                ).replace('-', '_')
-
-            directory = doc.createElement('Directory')
-            directory.setAttribute('Id', child_directory_id)
-            directory.setAttribute('Name', name)
-            directory_ref.appendChild(directory)
-
-        # Add <Component>s for files in this directory.
-        for rel, source_path in sorted(entries.items()):
-            if dir_name == '.':
-                full_rel = rel
-            else:
-                full_rel = '%s/%s' % (dir_name, rel)
-
-            component_unique_id = (
-                'https://www.mercurial-scm.org/wix-installer/0/component/%s'
-                % full_rel
-            )
-            component_guid = uuid.uuid5(uuid.NAMESPACE_URL, component_unique_id)
-            component_id = 'hg.component.%s' % str(component_guid).replace(
-                '-', '_'
-            )
-
-            component = doc.createElement('Component')
-
-            component.setAttribute('Id', component_id)
-            component.setAttribute('Guid', str(component_guid).upper())
-            component.setAttribute('Win64', 'yes' if is_x64 else 'no')
-
-            # Assign this component to a top-level group.
-            if dir_name == '.':
-                component_groups['ROOT'].append(component_id)
-            elif '/' in dir_name:
-                component_groups[dir_name[0 : dir_name.index('/')]].append(
-                    component_id
-                )
-            else:
-                component_groups[dir_name].append(component_id)
-
-            unique_id = (
-                'https://www.mercurial-scm.org/wix-installer/0/%s' % full_rel
-            )
-            file_guid = uuid.uuid5(uuid.NAMESPACE_URL, unique_id)
-
-            # IDs have length limits. So use GUID to derive them.
-            file_guid_normalized = str(file_guid).replace('-', '_')
-            file_id = 'hg.file.%s' % file_guid_normalized
-
-            file_element = doc.createElement('File')
-            file_element.setAttribute('Id', file_id)
-            file_element.setAttribute('Source', str(source_path))
-            file_element.setAttribute('KeyPath', 'yes')
-            file_element.setAttribute('ReadOnly', 'yes')
-
-            component.appendChild(file_element)
-            directory_ref.appendChild(component)
-
-        fragment.appendChild(directory_ref)
-        doc.documentElement.appendChild(fragment)
-
-    for group, component_ids in sorted(component_groups.items()):
-        fragment = doc.createElement('Fragment')
-        component_group = doc.createElement('ComponentGroup')
-        component_group.setAttribute('Id', 'hg.group.%s' % group)
-
-        for component_id in component_ids:
-            component_ref = doc.createElement('ComponentRef')
-            component_ref.setAttribute('Id', component_id)
-            component_group.appendChild(component_ref)
-
-        fragment.appendChild(component_group)
-        doc.documentElement.appendChild(fragment)
-
-    # Add <Shortcut> to files that have it defined.
-    for file_id, metadata in sorted(SHORTCUTS.items()):
-        els = doc.getElementsByTagName('File')
-        els = [el for el in els if el.getAttribute('Id') == file_id]
-
-        if not els:
-            raise Exception('could not find File[Id=%s]' % file_id)
-
-        for el in els:
-            shortcut = doc.createElement('Shortcut')
-            shortcut.setAttribute('Id', 'hg.shortcut.%s' % file_id)
-            shortcut.setAttribute('Directory', 'ProgramMenuDir')
-            shortcut.setAttribute('Icon', 'hgIcon.ico')
-            shortcut.setAttribute('IconIndex', '0')
-            shortcut.setAttribute('Advertise', 'yes')
-            for k, v in sorted(metadata.items()):
-                shortcut.setAttribute(k, v)
-
-            el.appendChild(shortcut)
-
-    return doc.toprettyxml()
-
-
-def build_installer_py2exe(
-    source_dir: pathlib.Path,
-    python_exe: pathlib.Path,
-    msi_name='mercurial',
-    version=None,
-    extra_packages_script=None,
-    extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
-    extra_features: typing.Optional[typing.List[str]] = None,
-    signing_info: typing.Optional[typing.Dict[str, str]] = None,
-):
-    """Build a WiX MSI installer using py2exe.
-
-    ``source_dir`` is the path to the Mercurial source tree to use.
-    ``arch`` is the target architecture. either ``x86`` or ``x64``.
-    ``python_exe`` is the path to the Python executable to use/bundle.
-    ``version`` is the Mercurial version string. If not defined,
-    ``mercurial/__version__.py`` will be consulted.
-    ``extra_packages_script`` is a command to be run to inject extra packages
-    into the py2exe binary. It should stage packages into the virtualenv and
-    print a null byte followed by a newline-separated list of packages that
-    should be included in the exe.
-    ``extra_wxs`` is a dict of {wxs_name: working_dir_for_wxs_build}.
-    ``extra_features`` is a list of additional named Features to include in
-    the build. These must match Feature names in one of the wxs scripts.
-    """
-    arch = 'x64' if r'\x64' in os.environ.get('LIB', '') else 'x86'
-
-    hg_build_dir = source_dir / 'build'
-
-    requirements_txt = (
-        source_dir / 'contrib' / 'packaging' / 'requirements-windows-py2.txt'
-    )
-
-    build_py2exe(
-        source_dir,
-        hg_build_dir,
-        python_exe,
-        'wix',
-        requirements_txt,
-        extra_packages=EXTRA_PACKAGES,
-        extra_packages_script=extra_packages_script,
-        extra_includes=EXTRA_INCLUDES,
-    )
-
-    build_dir = hg_build_dir / ('wix-%s' % arch)
-    staging_dir = build_dir / 'stage'
-
-    build_dir.mkdir(exist_ok=True)
-
-    # Purge the staging directory for every build so packaging is pristine.
-    if staging_dir.exists():
-        print('purging %s' % staging_dir)
-        shutil.rmtree(staging_dir)
-
-    stage_install(source_dir, staging_dir, lower_case=True)
-
-    # We also install some extra files.
-    process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
-
-    # And remove some files we don't want.
-    for f in STAGING_REMOVE_FILES:
-        p = staging_dir / f
-        if p.exists():
-            print('removing %s' % p)
-            p.unlink()
-
-    return run_wix_packaging(
-        source_dir,
-        build_dir,
-        staging_dir,
-        arch,
-        version=version,
-        python2=True,
-        msi_name=msi_name,
-        suffix="-python2",
-        extra_wxs=extra_wxs,
-        extra_features=extra_features,
-        signing_info=signing_info,
-    )
 
 
 def build_installer_pyoxidizer(
@@ -454,133 +94,3 @@
     return {
         "msi_path": dist_path,
     }
-
-
-def run_wix_packaging(
-    source_dir: pathlib.Path,
-    build_dir: pathlib.Path,
-    staging_dir: pathlib.Path,
-    arch: str,
-    version: str,
-    python2: bool,
-    msi_name: typing.Optional[str] = "mercurial",
-    suffix: str = "",
-    extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
-    extra_features: typing.Optional[typing.List[str]] = None,
-    signing_info: typing.Optional[typing.Dict[str, str]] = None,
-):
-    """Invokes WiX to package up a built Mercurial.
-
-    ``signing_info`` is a dict defining properties to facilitate signing the
-    installer. Recognized keys include ``name``, ``subject_name``,
-    ``cert_path``, ``cert_password``, and ``timestamp_url``. If populated,
-    we will sign both the hg.exe and the .msi using the signing credentials
-    specified.
-    """
-
-    orig_version = version or find_version(source_dir)
-    version = normalize_windows_version(orig_version)
-    print('using version string: %s' % version)
-    if version != orig_version:
-        print('(normalized from: %s)' % orig_version)
-
-    if signing_info:
-        sign_with_signtool(
-            staging_dir / "hg.exe",
-            "%s %s" % (signing_info["name"], version),
-            subject_name=signing_info["subject_name"],
-            cert_path=signing_info["cert_path"],
-            cert_password=signing_info["cert_password"],
-            timestamp_url=signing_info["timestamp_url"],
-        )
-
-    wix_dir = source_dir / 'contrib' / 'packaging' / 'wix'
-
-    wix_pkg, wix_entry = download_entry('wix', build_dir)
-    wix_path = build_dir / ('wix-%s' % wix_entry['version'])
-
-    if not wix_path.exists():
-        extract_zip_to_directory(wix_pkg, wix_path)
-
-    if python2:
-        ensure_vc90_merge_modules(build_dir)
-
-    source_build_rel = pathlib.Path(os.path.relpath(source_dir, build_dir))
-
-    defines = {'Platform': arch}
-
-    # Derive a .wxs file with the staged files.
-    manifest_wxs = build_dir / 'stage.wxs'
-    with manifest_wxs.open('w', encoding='utf-8') as fh:
-        fh.write(make_files_xml(staging_dir, is_x64=arch == 'x64'))
-
-    run_candle(wix_path, build_dir, manifest_wxs, staging_dir, defines=defines)
-
-    for source, rel_path in sorted((extra_wxs or {}).items()):
-        run_candle(wix_path, build_dir, source, rel_path, defines=defines)
-
-    source = wix_dir / 'mercurial.wxs'
-    defines['Version'] = version
-    defines['Comments'] = 'Installs Mercurial version %s' % version
-
-    if python2:
-        defines["PythonVersion"] = "2"
-        defines['VCRedistSrcDir'] = str(build_dir)
-    else:
-        defines["PythonVersion"] = "3"
-
-    if (staging_dir / "lib").exists():
-        defines["MercurialHasLib"] = "1"
-
-    if extra_features:
-        assert all(';' not in f for f in extra_features)
-        defines['MercurialExtraFeatures'] = ';'.join(extra_features)
-
-    run_candle(wix_path, build_dir, source, source_build_rel, defines=defines)
-
-    msi_path = (
-        source_dir
-        / 'dist'
-        / ('%s-%s-%s%s.msi' % (msi_name, orig_version, arch, suffix))
-    )
-
-    args = [
-        str(wix_path / 'light.exe'),
-        '-nologo',
-        '-ext',
-        'WixUIExtension',
-        '-sw1076',
-        '-spdb',
-        '-o',
-        str(msi_path),
-    ]
-
-    for source, rel_path in sorted((extra_wxs or {}).items()):
-        assert source.endswith('.wxs')
-        source = os.path.basename(source)
-        args.append(str(build_dir / ('%s.wixobj' % source[:-4])))
-
-    args.extend(
-        [
-            str(build_dir / 'stage.wixobj'),
-            str(build_dir / 'mercurial.wixobj'),
-        ]
-    )
-
-    subprocess.run(args, cwd=str(source_dir), check=True)
-
-    print('%s created' % msi_path)
-
-    if signing_info:
-        sign_with_signtool(
-            msi_path,
-            "%s %s" % (signing_info["name"], version),
-            subject_name=signing_info["subject_name"],
-            cert_path=signing_info["cert_path"],
-            cert_password=signing_info["cert_password"],
-            timestamp_url=signing_info["timestamp_url"],
-        )
-
-    return {
-        'msi_path': msi_path,
-    }
--- a/contrib/packaging/inno/readme.rst	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/packaging/inno/readme.rst	Wed May 04 18:17:44 2022 +0200
@@ -5,52 +5,35 @@
 
 The following system dependencies must be installed:
 
-* Python 2.7 (download from https://www.python.org/downloads/)
-* Microsoft Visual C++ Compiler for Python 2.7
-  (https://www.microsoft.com/en-us/download/details.aspx?id=44266)
 * Inno Setup (http://jrsoftware.org/isdl.php) version 5.4 or newer.
   Be sure to install the optional Inno Setup Preprocessor feature,
   which is required.
-* Python 3.5+ (to run the ``packaging.py`` script)
+* Python 3.6+ (to run the ``packaging.py`` script)
 
 Building
 ========
 
-The ``packaging.py`` script automates the process of producing an
-Inno installer. It manages fetching and configuring the
-non-system dependencies (such as py2exe, gettext, and various
-Python packages).
-
-The script requires an activated ``Visual C++ 2008`` command prompt.
-A shortcut to such a prompt was installed with ``Microsoft Visual C++
-Compiler for Python 2.7``. From your Start Menu, look for
-``Microsoft Visual C++ Compiler Package for Python 2.7`` then launch
-either ``Visual C++ 2008 32-bit Command Prompt`` or
-``Visual C++ 2008 64-bit Command Prompt``.
+The ``packaging.py`` script automates the process of producing an Inno
+installer. It manages fetching and configuring non-system dependencies
+(such as gettext, and various Python packages).  It can be run from a
+basic cmd.exe Window (i.e. activating the MSBuildTools environment is
+not required).
 
 From the prompt, change to the Mercurial source directory. e.g.
 ``cd c:\src\hg``.
 
-Next, invoke ``packaging.py`` to produce an Inno installer. You will
-need to supply the path to the Python interpreter to use.::
+Next, invoke ``packaging.py`` to produce an Inno installer.::
 
    $ py -3 contrib\packaging\packaging.py \
-       inno --python c:\python27\python.exe
-
-.. note::
-
-   The script validates that the Visual C++ environment is
-   active and that the architecture of the specified Python
-   interpreter matches the Visual C++ environment and errors
-   if not.
+       inno --pyoxidizer-target x86_64-pc-windows-msvc
 
 If everything runs as intended, dependencies will be fetched and
 configured into the ``build`` sub-directory, Mercurial will be built,
-and an installer placed in the ``dist`` sub-directory. The final
-line of output should print the name of the generated installer.
+and an installer placed in the ``dist`` sub-directory. The final line
+of output should print the name of the generated installer.
 
-Additional options may be configured. Run
-``packaging.py inno --help`` to see a list of program flags.
+Additional options may be configured. Run ``packaging.py inno --help``
+to see a list of program flags.
 
 MinGW
 =====
--- a/contrib/packaging/mercurial.spec	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/packaging/mercurial.spec	Wed May 04 18:17:44 2022 +0200
@@ -126,14 +126,6 @@
 install -m 755 contrib/hgk $RPM_BUILD_ROOT%{_bindir}/
 install -m 755 contrib/hg-ssh $RPM_BUILD_ROOT%{_bindir}/
 
-bash_completion_dir=$RPM_BUILD_ROOT%{_sysconfdir}/bash_completion.d
-mkdir -p $bash_completion_dir
-install -m 644 contrib/bash_completion $bash_completion_dir/mercurial.sh
-
-zsh_completion_dir=$RPM_BUILD_ROOT%{_datadir}/zsh/site-functions
-mkdir -p $zsh_completion_dir
-install -m 644 contrib/zsh_completion $zsh_completion_dir/_mercurial
-
 mkdir -p $RPM_BUILD_ROOT%{emacs_lispdir}
 install -m 644 contrib/mercurial.el $RPM_BUILD_ROOT%{emacs_lispdir}/
 install -m 644 contrib/mq.el $RPM_BUILD_ROOT%{emacs_lispdir}/
@@ -148,9 +140,12 @@
 %doc CONTRIBUTORS COPYING doc/README doc/hg*.txt doc/hg*.html *.cgi contrib/*.fcgi contrib/*.wsgi
 %doc %attr(644,root,root) %{_mandir}/man?/hg*
 %doc %attr(644,root,root) contrib/*.svg
+%dir %{_datadir}/bash-completion/
+%dir %{_datadir}/bash-completion/completions
+%{_datadir}/bash-completion/completions/hg
 %dir %{_datadir}/zsh/
 %dir %{_datadir}/zsh/site-functions/
-%{_datadir}/zsh/site-functions/_mercurial
+%{_datadir}/zsh/site-functions/_hg
 %dir %{_datadir}/emacs/site-lisp/
 %{_datadir}/emacs/site-lisp/mercurial.el
 %{_datadir}/emacs/site-lisp/mq.el
@@ -158,8 +153,6 @@
 %{_bindir}/chg
 %{_bindir}/hgk
 %{_bindir}/hg-ssh
-%dir %{_sysconfdir}/bash_completion.d/
-%config(noreplace) %{_sysconfdir}/bash_completion.d/mercurial.sh
 %dir %{_sysconfdir}/mercurial
 %dir %{_sysconfdir}/mercurial/hgrc.d
 %if "%{?withpython}"
--- a/contrib/packaging/requirements-windows-py2.txt	Wed May 04 18:00:01 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,59 +0,0 @@
-#
-# This file is autogenerated by pip-compile
-# To update, run:
-#
-#    pip-compile --generate-hashes --output-file=contrib/packaging/requirements-windows-py2.txt contrib/packaging/requirements-windows.txt.in
-#
-certifi==2021.5.30 \
-    --hash=sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee \
-    --hash=sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8 \
-    # via dulwich
-configparser==4.0.2 \
-    --hash=sha256:254c1d9c79f60c45dfde850850883d5aaa7f19a23f13561243a050d5a7c3fe4c \
-    --hash=sha256:c7d282687a5308319bf3d2e7706e575c635b0a470342641c93bea0ea3b5331df \
-    # via entrypoints
-docutils==0.16 \
-    --hash=sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af \
-    --hash=sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc \
-    # via -r contrib/packaging/requirements-windows.txt.in
-dulwich==0.19.16 ; python_version <= "2.7" \
-    --hash=sha256:10699277c6268d0c16febe141a5b1c1a6e9744f3144c2d2de1706f4b1adafe63 \
-    --hash=sha256:267160904e9a1cb6c248c5efc53597a35d038ecc6f60bdc4546b3053bed11982 \
-    --hash=sha256:4e3aba5e4844e7c700721c1fc696987ea820ee3528a03604dc4e74eff4196826 \
-    --hash=sha256:60bb2c2c92f5025c1b53a556304008f0f624c98ae36f22d870e056b2d4236c11 \
-    --hash=sha256:dddae02d372fc3b5cfb0046d0f62246ef281fa0c088df7601ab5916607add94b \
-    --hash=sha256:f00d132082b8fcc2eb0d722abc773d4aeb5558c1475d7edd1f0f571146c29db9 \
-    --hash=sha256:f74561c448bfb6f04c07de731c1181ae4280017f759b0bb04fa5770aa84ca850 \
-    # via -r contrib/packaging/requirements-windows.txt.in
-entrypoints==0.3 \
-    --hash=sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19 \
-    --hash=sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451 \
-    # via keyring
-keyring==18.0.1 \
-    --hash=sha256:67d6cc0132bd77922725fae9f18366bb314fd8f95ff4d323a4df41890a96a838 \
-    --hash=sha256:7b29ebfcf8678c4da531b2478a912eea01e80007e5ddca9ee0c7038cb3489ec6 \
-    # via -r contrib/packaging/requirements-windows.txt.in
-pygments==2.5.2 \
-    --hash=sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b \
-    --hash=sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe \
-    # via -r contrib/packaging/requirements-windows.txt.in
-pywin32-ctypes==0.2.0 \
-    --hash=sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942 \
-    --hash=sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98 \
-    # via -r contrib/packaging/requirements-windows.txt.in, keyring
-urllib3==1.25.11 \
-    --hash=sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2 \
-    --hash=sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e \
-    # via dulwich
-windows-curses==2.1.0 \
-    --hash=sha256:261fde5680d1ce4ce116908996b9a3cfb0ffb03ea68d42240f62b56a9fa6af2c \
-    --hash=sha256:66034dc9a705d87308cc9ea90836f4ee60008a1d5e2c1d34ace627f60268158b \
-    --hash=sha256:669caad3ae16faf2d201d7ab3b8af418a2fd074d8a39d60ca26f3acb34b6afe5 \
-    --hash=sha256:73bd3eebccfda55330783f165151de115bfa238d1332f0b2e224b550d6187840 \
-    --hash=sha256:89a6d973f88cfe49b41ea80164dcbec209d296e0cec34a02002578b0bf464a64 \
-    --hash=sha256:8ba7c000d7ffa5452bbd0966b96e69261e4f117ebe510aeb8771a9650197b7f0 \
-    --hash=sha256:97084c6b37b1534f6a28a514d521dfae402f77dcbad42b14ee32e8d5bdc13648 \
-    --hash=sha256:9e474a181f96d60429a4766145628264e60b72e7715876f9135aeb2e842f9433 \
-    --hash=sha256:cfe64c30807c146ef8d094412f90f2a2c81ad6aefff3ebfe8e37aabe2f801303 \
-    --hash=sha256:ff8c67f74b88944d99fa9d22971c05c335bc74f149120f0a69340c2c3a595497 \
-    # via -r contrib/packaging/requirements-windows.txt.in
--- a/contrib/packaging/requirements-windows.txt.in	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/packaging/requirements-windows.txt.in	Wed May 04 18:17:44 2022 +0200
@@ -1,13 +1,11 @@
 docutils
-# Pinned to an old version because 0.20 drops Python 3 compatibility.
-dulwich < 0.20 ; python_version <= '2.7'
-dulwich ; python_version >= '3'
+dulwich
 
 # Needed by the release note tooling
 fuzzywuzzy
 
 keyring
-pygit2 ; python_version >= '3'
+pygit2
 pygments
 
 # Needed by the phabricator tests
--- a/contrib/packaging/wix/readme.rst	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/packaging/wix/readme.rst	Wed May 04 18:17:44 2022 +0200
@@ -12,50 +12,36 @@
 Requirements
 ============
 
-Building the WiX installers requires a Windows machine. The following
-dependencies must be installed:
+Building the WiX installer requires a Windows machine.
 
-* Python 2.7 (download from https://www.python.org/downloads/)
-* Microsoft Visual C++ Compiler for Python 2.7
-  (https://www.microsoft.com/en-us/download/details.aspx?id=44266)
-* Python 3.5+ (to run the ``packaging.py`` script)
+The following system dependencies must be installed:
+
+* Python 3.6+ (to run the ``packaging.py`` script)
 
 Building
 ========
 
 The ``packaging.py`` script automates the process of producing an MSI
 installer. It manages fetching and configuring non-system dependencies
-(such as py2exe, gettext, and various Python packages).
-
-The script requires an activated ``Visual C++ 2008`` command prompt.
-A shortcut to such a prompt was installed with ``Microsoft Visual
-C++ Compiler for Python 2.7``. From your Start Menu, look for
-``Microsoft Visual C++ Compiler Package for Python 2.7`` then
-launch either ``Visual C++ 2008 32-bit Command Prompt`` or
-``Visual C++ 2008 64-bit Command Prompt``.
+(such as gettext, and various Python packages).  It can be run from a
+basic cmd.exe Window (i.e. activating the MSBuildTools environment is
+not required).
 
 From the prompt, change to the Mercurial source directory. e.g.
 ``cd c:\src\hg``.
 
-Next, invoke ``packaging.py`` to produce an MSI installer. You will need
-to supply the path to the Python interpreter to use.::
+Next, invoke ``packaging.py`` to produce an MSI installer.::
 
    $ py -3 contrib\packaging\packaging.py \
-      wix --python c:\python27\python.exe
-
-.. note::
-
-   The script validates that the Visual C++ environment is active and
-   that the architecture of the specified Python interpreter matches the
-   Visual C++ environment. An error is raised otherwise.
+       wix --pyoxidizer-target x86_64-pc-windows-msvc
 
 If everything runs as intended, dependencies will be fetched and
 configured into the ``build`` sub-directory, Mercurial will be built,
 and an installer placed in the ``dist`` sub-directory. The final line
 of output should print the name of the generated installer.
 
-Additional options may be configured. Run ``packaging.py wix --help`` to
-see a list of program flags.
+Additional options may be configured. Run ``packaging.py wix --help``
+to see a list of program flags.
 
 Relationship to TortoiseHG
 ==========================
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/perf-utils/compare-discovery-case	Wed May 04 18:17:44 2022 +0200
@@ -0,0 +1,183 @@
+#!/usr/bin/env python3
+# compare various algorithm variants for a given case
+#
+#  search-discovery-case REPO LOCAL_CASE REMOTE_CASE
+#
+# The description for the case input uses the same format as the ouput of
+# search-discovery-case
+
+import json
+import os
+import subprocess
+import sys
+
+this_script = os.path.abspath(sys.argv[0])
+script_name = os.path.basename(this_script)
+this_dir = os.path.dirname(this_script)
+hg_dir = os.path.join(this_dir, '..', '..')
+HG_REPO = os.path.normpath(hg_dir)
+HG_BIN = os.path.join(HG_REPO, 'hg')
+
+
+SUBSET_PATH = os.path.join(HG_REPO, 'contrib', 'perf-utils', 'subsetmaker.py')
+
+CMD_BASE = (
+    HG_BIN,
+    'debugdiscovery',
+    '--template',
+    'json',
+    '--config',
+    'extensions.subset=%s' % SUBSET_PATH,
+)
+
+# --old
+# --nonheads
+#
+# devel.discovery.exchange-heads=True
+# devel.discovery.grow-sample=True
+# devel.discovery.grow-sample.dynamic=True
+
+VARIANTS = {
+    'tree-discovery': ('--old',),
+    'set-discovery-basic': (
+        '--config',
+        'devel.discovery.exchange-heads=no',
+        '--config',
+        'devel.discovery.grow-sample=no',
+        '--config',
+        'devel.discovery.grow-sample.dynamic=no',
+        '--config',
+        'devel.discovery.randomize=yes',
+    ),
+    'set-discovery-heads': (
+        '--config',
+        'devel.discovery.exchange-heads=yes',
+        '--config',
+        'devel.discovery.grow-sample=no',
+        '--config',
+        'devel.discovery.grow-sample.dynamic=no',
+        '--config',
+        'devel.discovery.randomize=yes',
+    ),
+    'set-discovery-grow-sample': (
+        '--config',
+        'devel.discovery.exchange-heads=yes',
+        '--config',
+        'devel.discovery.grow-sample=yes',
+        '--config',
+        'devel.discovery.grow-sample.dynamic=no',
+        '--config',
+        'devel.discovery.randomize=yes',
+    ),
+    'set-discovery-dynamic-sample': (
+        '--config',
+        'devel.discovery.exchange-heads=yes',
+        '--config',
+        'devel.discovery.grow-sample=yes',
+        '--config',
+        'devel.discovery.grow-sample.dynamic=yes',
+        '--config',
+        'devel.discovery.randomize=yes',
+    ),
+    'set-discovery-default': (
+        '--config',
+        'devel.discovery.randomize=yes',
+    ),
+}
+
+VARIANTS_KEYS = [
+    'tree-discovery',
+    'set-discovery-basic',
+    'set-discovery-heads',
+    'set-discovery-grow-sample',
+    'set-discovery-dynamic-sample',
+    'set-discovery-default',
+]
+
+assert set(VARIANTS.keys()) == set(VARIANTS_KEYS)
+
+
+def format_case(case):
+    return '-'.join(str(s) for s in case)
+
+
+def to_revsets(case):
+    t = case[0]
+    if t == 'scratch':
+        return 'not scratch(all(), %d, "%d")' % (case[1], case[2])
+    elif t == 'randomantichain':
+        return '::randomantichain(all(), "%d")' % case[1]
+    elif t == 'rev':
+        return '::%d' % case[1]
+    else:
+        assert False
+
+
+def compare(repo, local_case, remote_case):
+    case = (repo, local_case, remote_case)
+    for variant in VARIANTS_KEYS:
+        res = process(case, VARIANTS[variant])
+        revs = res["nb-revs"]
+        local_heads = res["nb-head-local"]
+        common_heads = res["nb-common-heads"]
+        roundtrips = res["total-roundtrips"]
+        queries = res["total-queries"]
+        if 'tree-discovery' in variant:
+            print(
+                repo,
+                format_case(local_case),
+                format_case(remote_case),
+                variant,
+                roundtrips,
+                queries,
+                revs,
+                local_heads,
+                common_heads,
+            )
+        else:
+            undecided_common = res["nb-ini_und-common"]
+            undecided_missing = res["nb-ini_und-missing"]
+            undecided = undecided_common + undecided_missing
+            print(
+                repo,
+                format_case(local_case),
+                format_case(remote_case),
+                variant,
+                roundtrips,
+                queries,
+                revs,
+                local_heads,
+                common_heads,
+                undecided,
+                undecided_common,
+                undecided_missing,
+            )
+    return 0
+
+
+def process(case, variant):
+    (repo, left, right) = case
+    cmd = list(CMD_BASE)
+    cmd.append('-R')
+    cmd.append(repo)
+    cmd.append('--local-as-revs')
+    cmd.append(to_revsets(left))
+    cmd.append('--remote-as-revs')
+    cmd.append(to_revsets(right))
+    cmd.extend(variant)
+    s = subprocess.Popen(cmd, stdout=subprocess.PIPE)
+    out, err = s.communicate()
+    return json.loads(out)[0]
+
+
+if __name__ == '__main__':
+    if len(sys.argv) != 4:
+        usage = f'USAGE: {script_name} REPO LOCAL_CASE REMOTE_CASE'
+        print(usage, file=sys.stderr)
+        sys.exit(128)
+    repo = sys.argv[1]
+    local_case = sys.argv[2].split('-')
+    local_case = (local_case[0],) + tuple(int(x) for x in local_case[1:])
+    remote_case = sys.argv[3].split('-')
+    remote_case = (remote_case[0],) + tuple(int(x) for x in remote_case[1:])
+    sys.exit(compare(repo, local_case, remote_case))
--- a/contrib/perf-utils/perf-revlog-write-plot.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/perf-utils/perf-revlog-write-plot.py	Wed May 04 18:17:44 2022 +0200
@@ -9,7 +9,6 @@
 # various plot related to write performance in a revlog
 #
 # usage: perf-revlog-write-plot.py details.json
-from __future__ import absolute_import, print_function
 import json
 import re
 
--- a/contrib/perf-utils/search-discovery-case	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/perf-utils/search-discovery-case	Wed May 04 18:17:44 2022 +0200
@@ -6,7 +6,6 @@
 # This use a subsetmaker extension (next to this script) to generate a steam of
 # random discovery instance. When interesting case are discovered, information
 # about them are print on the stdout.
-from __future__ import print_function
 
 import json
 import os
@@ -148,13 +147,23 @@
     roundtrips = res["total-roundtrips"]
     if roundtrips <= 1:
         return None
+    total_revs = res["nb-revs"]
+    common_revs = res["nb-revs-common"]
+    missing_revs = res["nb-revs-missing"]
     undecided_common = res["nb-ini_und-common"]
     undecided_missing = res["nb-ini_und-missing"]
     if undecided_common == 0:
         return None
     if undecided_missing == 0:
         return None
-    return (roundtrips, undecided_common, undecided_missing)
+    return (
+        roundtrips,
+        undecided_common,
+        undecided_missing,
+        total_revs,
+        common_revs,
+        missing_revs,
+    )
 
 
 def end(*args, **kwargs):
--- a/contrib/perf-utils/subsetmaker.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/perf-utils/subsetmaker.py	Wed May 04 18:17:44 2022 +0200
@@ -15,6 +15,10 @@
     smartset,
 )
 
+import sortedcontainers
+
+SortedSet = sortedcontainers.SortedSet
+
 revsetpredicate = registrar.revsetpredicate()
 
 
@@ -78,7 +82,7 @@
     n = revsetlang.getinteger(n, _(b"scratch expects a number"))
 
     selected = set()
-    heads = set()
+    heads = SortedSet()
     children_count = collections.defaultdict(lambda: 0)
     parents = repo.changelog._uncheckedparentrevs
 
@@ -102,7 +106,7 @@
     for x in range(n):
         if not heads:
             break
-        pick = rand.choice(list(heads))
+        pick = rand.choice(heads)
         heads.remove(pick)
         assert pick not in selected
         selected.add(pick)
@@ -155,16 +159,44 @@
     else:
         assert False
 
-    selected = set()
+    cl = repo.changelog
 
-    baseset = revset.getset(repo, smartset.fullreposet(repo), x)
-    undecided = baseset
+    # We already have cheap access to the parent mapping.
+    # However, we need to build a mapping of the children mapping
+    parents = repo.changelog._uncheckedparentrevs
+    children_map = collections.defaultdict(list)
+    for r in cl:
+        p1, p2 = parents(r)
+        if p1 >= 0:
+            children_map[p1].append(r)
+        if p2 >= 0:
+            children_map[p2].append(r)
+    children = children_map.__getitem__
+
+    selected = set()
+    undecided = SortedSet(cl)
 
     while undecided:
-        pick = rand.choice(list(undecided))
+        # while there is "undecided content", we pick a random changeset X
+        # and we remove anything in `::X + X::` from undecided content
+        pick = rand.choice(undecided)
         selected.add(pick)
-        undecided = repo.revs(
-            '%ld and not (::%ld or %ld::head())', baseset, selected, selected
-        )
+        undecided.remove(pick)
+
+        ancestors = set(p for p in parents(pick) if p in undecided)
+        descendants = set(c for c in children(pick) if c in undecided)
+
+        while ancestors:
+            current = ancestors.pop()
+            undecided.remove(current)
+            for p in parents(current):
+                if p in undecided:
+                    ancestors.add(p)
+        while descendants:
+            current = descendants.pop()
+            undecided.remove(current)
+            for p in children(current):
+                if p in undecided:
+                    ancestors.add(p)
 
     return smartset.baseset(selected) & subset
--- a/contrib/perf.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/perf.py	Wed May 04 18:17:44 2022 +0200
@@ -54,7 +54,6 @@
 # - make perf command for recent feature work correctly with early
 #   Mercurial
 
-from __future__ import absolute_import
 import contextlib
 import functools
 import gc
@@ -370,7 +369,7 @@
     return len
 
 
-class noop(object):
+class noop:
     """dummy context manager"""
 
     def __enter__(self):
@@ -414,7 +413,7 @@
         # available since 2.2 (or ae5f92e154d3)
         from mercurial import node
 
-        class defaultformatter(object):
+        class defaultformatter:
             """Minimized composition of baseformatter and plainformatter"""
 
             def __init__(self, ui, topic, opts):
@@ -653,7 +652,7 @@
 
     origvalue = getattr(obj, _sysstr(name))
 
-    class attrutil(object):
+    class attrutil:
         def set(self, newvalue):
             setattr(obj, _sysstr(name), newvalue)
 
@@ -2943,7 +2942,7 @@
     fm.end()
 
 
-class _faketr(object):
+class _faketr:
     def add(s, x, y, z=None):
         return None
 
--- a/contrib/phab-clean.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/phab-clean.py	Wed May 04 18:17:44 2022 +0200
@@ -3,7 +3,6 @@
 # A small script to automatically reject idle Diffs
 #
 # you need to set the PHABBOT_USER and PHABBOT_TOKEN environment variable for authentication
-from __future__ import absolute_import, print_function
 
 import datetime
 import os
--- a/contrib/python-hook-examples.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/python-hook-examples.py	Wed May 04 18:17:44 2022 +0200
@@ -1,7 +1,6 @@
 '''
 Examples of useful python hooks for Mercurial.
 '''
-from __future__ import absolute_import
 from mercurial import (
     patch,
     util,
--- a/contrib/python-zstandard/make_cffi.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/python-zstandard/make_cffi.py	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 # This software may be modified and distributed under the terms
 # of the BSD license. See the LICENSE file for details.
 
-from __future__ import absolute_import
 
 import cffi
 import distutils.ccompiler
--- a/contrib/python-zstandard/setup.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/python-zstandard/setup.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be modified and distributed under the terms
 # of the BSD license. See the LICENSE file for details.
 
-from __future__ import print_function
 
 from distutils.version import LooseVersion
 import os
--- a/contrib/python-zstandard/tests/test_module_attributes.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/python-zstandard/tests/test_module_attributes.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import unicode_literals
-
 import unittest
 
 import zstandard as zstd
--- a/contrib/python-zstandard/zstandard/__init__.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/python-zstandard/zstandard/__init__.py	Wed May 04 18:17:44 2022 +0200
@@ -6,7 +6,6 @@
 
 """Python interface to the Zstandard (zstd) compression library."""
 
-from __future__ import absolute_import, unicode_literals
 
 # This module serves 2 roles:
 #
--- a/contrib/python-zstandard/zstandard/cffi.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/python-zstandard/zstandard/cffi.py	Wed May 04 18:17:44 2022 +0200
@@ -6,7 +6,6 @@
 
 """Python interface to the Zstandard (zstd) compression library."""
 
-from __future__ import absolute_import, unicode_literals
 
 # This should match what the C extension exports.
 __all__ = [
--- a/contrib/python3-ratchet.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/python3-ratchet.py	Wed May 04 18:17:44 2022 +0200
@@ -15,8 +15,6 @@
   $ python3 ../contrib/python3-ratchet.py \
   >   --working-tests=../contrib/python3-whitelist
 """
-from __future__ import print_function
-from __future__ import absolute_import
 
 import argparse
 import json
--- a/contrib/revsetbenchmarks.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/revsetbenchmarks.py	Wed May 04 18:17:44 2022 +0200
@@ -8,7 +8,6 @@
 #
 # call with --help for details
 
-from __future__ import absolute_import, print_function
 import math
 import optparse  # cannot use argparse, python 2.7 only
 import os
--- a/contrib/showstack.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/showstack.py	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 r"""dump stack trace when receiving SIGQUIT (Ctrl-\) or SIGINFO (Ctrl-T on BSDs)
 """
 
-from __future__ import absolute_import, print_function
 import signal
 import sys
 import traceback
--- a/contrib/simplemerge	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/simplemerge	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,4 @@
 #!/usr/bin/env python3
-from __future__ import absolute_import
 
 import getopt
 import sys
--- a/contrib/synthrepo.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/synthrepo.py	Wed May 04 18:17:44 2022 +0200
@@ -36,7 +36,6 @@
 - Symlinks and binary files are ignored
 '''
 
-from __future__ import absolute_import
 import bisect
 import collections
 import itertools
@@ -213,7 +212,7 @@
             for filename, mar, lineadd, lineremove, isbin in parsegitdiff(diff):
                 if isbin:
                     continue
-                added = sum(pycompat.itervalues(lineadd), 0)
+                added = sum(lineadd.values(), 0)
                 if mar == 'm':
                     if added and lineremove:
                         lineschanged[
--- a/contrib/testparseutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/testparseutil.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import, print_function
 
 import abc
 import re
@@ -80,7 +79,7 @@
 ####################
 
 
-class embeddedmatcher(object):  # pytype: disable=ignored-metaclass
+class embeddedmatcher:  # pytype: disable=ignored-metaclass
     """Base class to detect embedded code fragments in *.t test script"""
 
     __metaclass__ = abc.ABCMeta
@@ -157,7 +156,7 @@
     :ends: line number (1-origin), at which embedded code ends (exclusive)
     :code: extracted embedded code, which is single-stringified
 
-    >>> class ambigmatcher(object):
+    >>> class ambigmatcher:
     ...     # mock matcher class to examine implementation of
     ...     # "ambiguous matching" corner case
     ...     def __init__(self, desc, matchfunc):
--- a/contrib/undumprevlog	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/undumprevlog	Wed May 04 18:17:44 2022 +0200
@@ -3,7 +3,6 @@
 # $ hg init
 # $ undumprevlog < repo.dump
 
-from __future__ import absolute_import, print_function
 
 import sys
 from mercurial.node import bin
--- a/contrib/win32/hgwebdir_wsgi.py	Wed May 04 18:00:01 2022 +0200
+++ b/contrib/win32/hgwebdir_wsgi.py	Wed May 04 18:17:44 2022 +0200
@@ -78,7 +78,6 @@
 # - Restart the web server and see if things are running.
 #
 
-from __future__ import absolute_import
 
 # Configuration file location
 hgweb_config = r'c:\your\directory\wsgi.config'
--- a/doc/check-seclevel.py	Wed May 04 18:00:01 2022 +0200
+++ b/doc/check-seclevel.py	Wed May 04 18:17:44 2022 +0200
@@ -2,7 +2,6 @@
 #
 # checkseclevel - checking section title levels in each online help document
 
-from __future__ import absolute_import
 
 import optparse
 import os
--- a/doc/docchecker	Wed May 04 18:00:01 2022 +0200
+++ b/doc/docchecker	Wed May 04 18:17:44 2022 +0200
@@ -7,7 +7,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import, print_function
 
 import os
 import re
--- a/doc/gendoc.py	Wed May 04 18:00:01 2022 +0200
+++ b/doc/gendoc.py	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 where DOC is the name of a document
 """
 
-from __future__ import absolute_import
 
 import os
 import sys
--- a/doc/hgmanpage.py	Wed May 04 18:00:01 2022 +0200
+++ b/doc/hgmanpage.py	Wed May 04 18:17:44 2022 +0200
@@ -41,7 +41,6 @@
 by the command whatis or apropos.
 
 """
-from __future__ import absolute_import
 
 __docformat__ = 'reStructuredText'
 
@@ -113,7 +112,7 @@
         self.output = visitor.astext()
 
 
-class Table(object):
+class Table:
     def __init__(self):
         self._rows = []
         self._options = ['center']
@@ -313,7 +312,7 @@
         pass
 
     def list_start(self, node):
-        class enum_char(object):
+        class enum_char:
             enum_style = {
                 'bullet': '\\(bu',
                 'emdash': '\\(em',
--- a/doc/runrst	Wed May 04 18:00:01 2022 +0200
+++ b/doc/runrst	Wed May 04 18:17:44 2022 +0200
@@ -12,7 +12,6 @@
 where WRITER is the name of a Docutils writer such as 'html' or 'manpage'
 """
 
-from __future__ import absolute_import
 
 import sys
 
--- a/hg	Wed May 04 18:00:01 2022 +0200
+++ b/hg	Wed May 04 18:17:44 2022 +0200
@@ -6,7 +6,6 @@
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-from __future__ import absolute_import
 
 import os
 import sys
@@ -44,10 +43,9 @@
 with tracing.log('hg script'):
     # enable importing on demand to reduce startup time
     try:
-        if sys.version_info[0] < 3 or sys.version_info >= (3, 6):
-            import hgdemandimport
+        import hgdemandimport
 
-            hgdemandimport.enable()
+        hgdemandimport.enable()
     except ImportError:
         sys.stderr.write(
             "abort: couldn't find mercurial libraries in [%s]\n"
--- a/hgdemandimport/__init__.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgdemandimport/__init__.py	Wed May 04 18:17:44 2022 +0200
@@ -11,15 +11,11 @@
 # demand loading is per-package. Keeping demandimport in the mercurial package
 # would disable demand loading for any modules in mercurial.
 
-from __future__ import absolute_import
 
 import os
 import sys
 
-if sys.version_info[0] >= 3:
-    from . import demandimportpy3 as demandimport
-else:
-    from . import demandimportpy2 as demandimport
+from . import demandimportpy3 as demandimport
 
 # Full module names which can't be lazy imported.
 # Extensions can add to this set.
--- a/hgdemandimport/demandimportpy2.py	Wed May 04 18:00:01 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,326 +0,0 @@
-# demandimport.py - global demand-loading of modules for Mercurial
-#
-# Copyright 2006, 2007 Olivia Mackall <olivia@selenic.com>
-#
-# This software may be used and distributed according to the terms of the
-# GNU General Public License version 2 or any later version.
-
-'''
-demandimport - automatic demandloading of modules
-
-To enable this module, do:
-
-  import demandimport; demandimport.enable()
-
-Imports of the following forms will be demand-loaded:
-
-  import a, b.c
-  import a.b as c
-  from a import b,c # a will be loaded immediately
-
-These imports will not be delayed:
-
-  from a import *
-  b = __import__(a)
-'''
-
-from __future__ import absolute_import
-
-import __builtin__ as builtins
-import contextlib
-import sys
-
-from . import tracing
-
-contextmanager = contextlib.contextmanager
-
-_origimport = __import__
-
-nothing = object()
-
-
-def _hgextimport(importfunc, name, globals, *args, **kwargs):
-    try:
-        return importfunc(name, globals, *args, **kwargs)
-    except ImportError:
-        if not globals:
-            raise
-        # extensions are loaded with "hgext_" prefix
-        hgextname = 'hgext_%s' % name
-        nameroot = hgextname.split('.', 1)[0]
-        contextroot = globals.get('__name__', '').split('.', 1)[0]
-        if nameroot != contextroot:
-            raise
-        # retry to import with "hgext_" prefix
-        return importfunc(hgextname, globals, *args, **kwargs)
-
-
-class _demandmod(object):
-    """module demand-loader and proxy
-
-    Specify 1 as 'level' argument at construction, to import module
-    relatively.
-    """
-
-    def __init__(self, name, globals, locals, level):
-        if '.' in name:
-            head, rest = name.split('.', 1)
-            after = [rest]
-        else:
-            head = name
-            after = []
-        object.__setattr__(
-            self, "_data", (head, globals, locals, after, level, set())
-        )
-        object.__setattr__(self, "_module", None)
-
-    def _extend(self, name):
-        """add to the list of submodules to load"""
-        self._data[3].append(name)
-
-    def _addref(self, name):
-        """Record that the named module ``name`` imports this module.
-
-        References to this proxy class having the name of this module will be
-        replaced at module load time. We assume the symbol inside the importing
-        module is identical to the "head" name of this module. We don't
-        actually know if "as X" syntax is being used to change the symbol name
-        because this information isn't exposed to __import__.
-        """
-        self._data[5].add(name)
-
-    def _load(self):
-        if not self._module:
-            with tracing.log('demandimport %s', self._data[0]):
-                head, globals, locals, after, level, modrefs = self._data
-                mod = _hgextimport(
-                    _origimport, head, globals, locals, None, level
-                )
-                if mod is self:
-                    # In this case, _hgextimport() above should imply
-                    # _demandimport(). Otherwise, _hgextimport() never
-                    # returns _demandmod. This isn't intentional behavior,
-                    # in fact. (see also issue5304 for detail)
-                    #
-                    # If self._module is already bound at this point, self
-                    # should be already _load()-ed while _hgextimport().
-                    # Otherwise, there is no way to import actual module
-                    # as expected, because (re-)invoking _hgextimport()
-                    # should cause same result.
-                    # This is reason why _load() returns without any more
-                    # setup but assumes self to be already bound.
-                    mod = self._module
-                    assert mod and mod is not self, "%s, %s" % (self, mod)
-                    return
-
-                # load submodules
-                def subload(mod, p):
-                    h, t = p, None
-                    if '.' in p:
-                        h, t = p.split('.', 1)
-                    if getattr(mod, h, nothing) is nothing:
-                        setattr(
-                            mod,
-                            h,
-                            _demandmod(p, mod.__dict__, mod.__dict__, level=1),
-                        )
-                    elif t:
-                        subload(getattr(mod, h), t)
-
-                for x in after:
-                    subload(mod, x)
-
-                # Replace references to this proxy instance with the
-                # actual module.
-                if locals:
-                    if locals.get(head) is self:
-                        locals[head] = mod
-                    elif locals.get(head + 'mod') is self:
-                        locals[head + 'mod'] = mod
-
-                for modname in modrefs:
-                    modref = sys.modules.get(modname, None)
-                    if modref and getattr(modref, head, None) is self:
-                        setattr(modref, head, mod)
-
-                object.__setattr__(self, "_module", mod)
-
-    def __repr__(self):
-        if self._module:
-            return "<proxied module '%s'>" % self._data[0]
-        return "<unloaded module '%s'>" % self._data[0]
-
-    def __call__(self, *args, **kwargs):
-        raise TypeError("%s object is not callable" % repr(self))
-
-    def __getattr__(self, attr):
-        self._load()
-        return getattr(self._module, attr)
-
-    def __setattr__(self, attr, val):
-        self._load()
-        setattr(self._module, attr, val)
-
-    @property
-    def __dict__(self):
-        self._load()
-        return self._module.__dict__
-
-    @property
-    def __doc__(self):
-        self._load()
-        return self._module.__doc__
-
-
-_pypy = '__pypy__' in sys.builtin_module_names
-
-
-def _demandimport(name, globals=None, locals=None, fromlist=None, level=-1):
-    if locals is None or name in ignores or fromlist == ('*',):
-        # these cases we can't really delay
-        return _hgextimport(_origimport, name, globals, locals, fromlist, level)
-    elif not fromlist:
-        # import a [as b]
-        if '.' in name:  # a.b
-            base, rest = name.split('.', 1)
-            # email.__init__ loading email.mime
-            if globals and globals.get('__name__', None) == base:
-                return _origimport(name, globals, locals, fromlist, level)
-            # if a is already demand-loaded, add b to its submodule list
-            if base in locals:
-                if isinstance(locals[base], _demandmod):
-                    locals[base]._extend(rest)
-                return locals[base]
-        return _demandmod(name, globals, locals, level)
-    else:
-        # There is a fromlist.
-        # from a import b,c,d
-        # from . import b,c,d
-        # from .a import b,c,d
-
-        # level == -1: relative and absolute attempted (Python 2 only).
-        # level >= 0: absolute only (Python 2 w/ absolute_import and Python 3).
-        # The modern Mercurial convention is to use absolute_import everywhere,
-        # so modern Mercurial code will have level >= 0.
-
-        # The name of the module the import statement is located in.
-        globalname = globals.get('__name__')
-
-        def processfromitem(mod, attr):
-            """Process an imported symbol in the import statement.
-
-            If the symbol doesn't exist in the parent module, and if the
-            parent module is a package, it must be a module. We set missing
-            modules up as _demandmod instances.
-            """
-            symbol = getattr(mod, attr, nothing)
-            nonpkg = getattr(mod, '__path__', nothing) is nothing
-            if symbol is nothing:
-                if nonpkg:
-                    # do not try relative import, which would raise ValueError,
-                    # and leave unknown attribute as the default __import__()
-                    # would do. the missing attribute will be detected later
-                    # while processing the import statement.
-                    return
-                mn = '%s.%s' % (mod.__name__, attr)
-                if mn in ignores:
-                    importfunc = _origimport
-                else:
-                    importfunc = _demandmod
-                symbol = importfunc(attr, mod.__dict__, locals, level=1)
-                setattr(mod, attr, symbol)
-
-            # Record the importing module references this symbol so we can
-            # replace the symbol with the actual module instance at load
-            # time.
-            if globalname and isinstance(symbol, _demandmod):
-                symbol._addref(globalname)
-
-        def chainmodules(rootmod, modname):
-            # recurse down the module chain, and return the leaf module
-            mod = rootmod
-            for comp in modname.split('.')[1:]:
-                obj = getattr(mod, comp, nothing)
-                if obj is nothing:
-                    obj = _demandmod(comp, mod.__dict__, mod.__dict__, level=1)
-                    setattr(mod, comp, obj)
-                elif mod.__name__ + '.' + comp in sys.modules:
-                    # prefer loaded module over attribute (issue5617)
-                    obj = sys.modules[mod.__name__ + '.' + comp]
-                mod = obj
-            return mod
-
-        if level >= 0:
-            if name:
-                # "from a import b" or "from .a import b" style
-                rootmod = _hgextimport(
-                    _origimport, name, globals, locals, level=level
-                )
-                mod = chainmodules(rootmod, name)
-            elif _pypy:
-                # PyPy's __import__ throws an exception if invoked
-                # with an empty name and no fromlist.  Recreate the
-                # desired behaviour by hand.
-                mn = globalname
-                mod = sys.modules[mn]
-                if getattr(mod, '__path__', nothing) is nothing:
-                    mn = mn.rsplit('.', 1)[0]
-                    mod = sys.modules[mn]
-                if level > 1:
-                    mn = mn.rsplit('.', level - 1)[0]
-                    mod = sys.modules[mn]
-            else:
-                mod = _hgextimport(
-                    _origimport, name, globals, locals, level=level
-                )
-
-            for x in fromlist:
-                processfromitem(mod, x)
-
-            return mod
-
-        # But, we still need to support lazy loading of standard library and 3rd
-        # party modules. So handle level == -1.
-        mod = _hgextimport(_origimport, name, globals, locals)
-        mod = chainmodules(mod, name)
-
-        for x in fromlist:
-            processfromitem(mod, x)
-
-        return mod
-
-
-ignores = set()
-
-
-def init(ignoreset):
-    global ignores
-    ignores = ignoreset
-
-
-def isenabled():
-    return builtins.__import__ == _demandimport
-
-
-def enable():
-    """enable global demand-loading of modules"""
-    builtins.__import__ = _demandimport
-
-
-def disable():
-    """disable global demand-loading of modules"""
-    builtins.__import__ = _origimport
-
-
-@contextmanager
-def deactivated():
-    """context manager for disabling demandimport in 'with' blocks"""
-    demandenabled = isenabled()
-    if demandenabled:
-        disable()
-
-    try:
-        yield
-    finally:
-        if demandenabled:
-            enable()
--- a/hgdemandimport/demandimportpy3.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgdemandimport/demandimportpy3.py	Wed May 04 18:17:44 2022 +0200
@@ -24,7 +24,6 @@
 """
 
 # This line is unnecessary, but it satisfies test-check-py3-compat.t.
-from __future__ import absolute_import
 
 import contextlib
 import importlib.util
@@ -34,12 +33,6 @@
 
 _deactivated = False
 
-# Python 3.5's LazyLoader doesn't work for some reason.
-# https://bugs.python.org/issue26186 is a known issue with extension
-# importing. But it appears to not have a meaningful effect with
-# Mercurial.
-_supported = sys.version_info[0:2] >= (3, 6)
-
 
 class _lazyloaderex(importlib.util.LazyLoader):
     """This is a LazyLoader except it also follows the _deactivated global and
@@ -55,7 +48,7 @@
                 super().exec_module(module)
 
 
-class LazyFinder(object):
+class LazyFinder:
     """A wrapper around a ``MetaPathFinder`` that makes loaders lazy.
 
     ``sys.meta_path`` finders have their ``find_spec()`` called to locate a
@@ -145,9 +138,6 @@
 
 
 def enable():
-    if not _supported:
-        return
-
     new_finders = []
     for finder in sys.meta_path:
         new_finders.append(
--- a/hgdemandimport/tracing.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgdemandimport/tracing.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-from __future__ import absolute_import
 
 import contextlib
 import os
--- a/hgext/__init__.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/__init__.py	Wed May 04 18:17:44 2022 +0200
@@ -1,4 +1,3 @@
-from __future__ import absolute_import
 import pkgutil
 
 __path__ = pkgutil.extend_path(__path__, __name__)
--- a/hgext/absorb.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/absorb.py	Wed May 04 18:17:44 2022 +0200
@@ -31,7 +31,6 @@
 #  * Converge getdraftstack() with other code in core
 #  * move many attributes on fixupstate to be private
 
-from __future__ import absolute_import
 
 import collections
 
@@ -84,7 +83,7 @@
 defaultdict = collections.defaultdict
 
 
-class nullui(object):
+class nullui:
     """blank ui object doing nothing"""
 
     debugflag = False
@@ -98,7 +97,7 @@
         return nullfunc
 
 
-class emptyfilecontext(object):
+class emptyfilecontext:
     """minimal filecontext representing an empty file"""
 
     def __init__(self, repo):
@@ -278,7 +277,7 @@
     )
 
 
-class filefixupstate(object):
+class filefixupstate:
     """state needed to apply fixups to a single file
 
     internally, it keeps file contents of several revisions and a linelog.
@@ -656,7 +655,7 @@
             )
 
 
-class fixupstate(object):
+class fixupstate:
     """state needed to run absorb
 
     internally, it keeps paths and filefixupstates.
@@ -734,7 +733,7 @@
 
     def apply(self):
         """apply fixups to individual filefixupstates"""
-        for path, state in pycompat.iteritems(self.fixupmap):
+        for path, state in self.fixupmap.items():
             if self.ui.debugflag:
                 self.ui.write(_(b'applying fixups to %s\n') % path)
             state.apply()
@@ -742,10 +741,7 @@
     @property
     def chunkstats(self):
         """-> {path: chunkstats}. collect chunkstats from filefixupstates"""
-        return {
-            path: state.chunkstats
-            for path, state in pycompat.iteritems(self.fixupmap)
-        }
+        return {path: state.chunkstats for path, state in self.fixupmap.items()}
 
     def commit(self):
         """commit changes. update self.finalnode, self.replacemap"""
@@ -763,7 +759,7 @@
         chunkstats = self.chunkstats
         if ui.verbose:
             # chunkstats for each file
-            for path, stat in pycompat.iteritems(chunkstats):
+            for path, stat in chunkstats.items():
                 if stat[0]:
                     ui.write(
                         _(b'%s: %d of %d chunk(s) applied\n')
@@ -846,7 +842,7 @@
         repo = self.repo
         needupdate = [
             (name, self.replacemap[hsh])
-            for name, hsh in pycompat.iteritems(repo._bookmarks)
+            for name, hsh in repo._bookmarks.items()
             if hsh in self.replacemap
         ]
         changes = []
@@ -909,7 +905,7 @@
         # ctx changes more files (not a subset of memworkingcopy)
         if not set(ctx.files()).issubset(set(memworkingcopy)):
             return False
-        for path, content in pycompat.iteritems(memworkingcopy):
+        for path, content in memworkingcopy.items():
             if path not in pctx or path not in ctx:
                 return False
             fctx = ctx[path]
@@ -952,7 +948,7 @@
     def _cleanupoldcommits(self):
         replacements = {
             k: ([v] if v is not None else [])
-            for k, v in pycompat.iteritems(self.replacemap)
+            for k, v in self.replacemap.items()
         }
         if replacements:
             scmutil.cleanupnodes(
@@ -1002,7 +998,7 @@
         if not path or not info:
             continue
         patchmap[path].append(info)
-    for path, patches in pycompat.iteritems(patchmap):
+    for path, patches in patchmap.items():
         if path not in ctx or not patches:
             continue
         patches.sort(reverse=True)
@@ -1049,6 +1045,10 @@
         origchunks = patch.parsepatch(diff)
         chunks = cmdutil.recordfilter(ui, origchunks, matcher)[0]
         targetctx = overlaydiffcontext(stack[-1], chunks)
+    if opts.get(b'edit_lines'):
+        # If we're going to open the editor, don't ask the user to confirm
+        # first
+        opts[b'apply_changes'] = True
     fm = None
     if opts.get(b'print_changes') or not opts.get(b'apply_changes'):
         fm = ui.formatter(b'absorb', opts)
@@ -1066,7 +1066,7 @@
             fm.context(ctx=ctx)
             fm.data(linetype=b'changeset')
             fm.write(b'node', b'%-7.7s ', ctx.hex(), label=b'absorb.node')
-            descfirstline = ctx.description().splitlines()[0]
+            descfirstline = stringutil.firstline(ctx.description())
             fm.write(
                 b'descfirstline',
                 b'%s\n',
--- a/hgext/acl.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/acl.py	Wed May 04 18:17:44 2022 +0200
@@ -213,7 +213,6 @@
 
 '''
 
-from __future__ import absolute_import
 
 from mercurial.i18n import _
 from mercurial import (
--- a/hgext/amend.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/amend.py	Wed May 04 18:17:44 2022 +0200
@@ -10,7 +10,6 @@
 ``commit --amend`` but does not prompt an editor.
 """
 
-from __future__ import absolute_import
 
 from mercurial.i18n import _
 from mercurial import (
--- a/hgext/automv.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/automv.py	Wed May 04 18:17:44 2022 +0200
@@ -24,7 +24,6 @@
 #
 # See http://markmail.org/thread/5pxnljesvufvom57 for context.
 
-from __future__ import absolute_import
 
 from mercurial.i18n import _
 from mercurial import (
--- a/hgext/beautifygraph.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/beautifygraph.py	Wed May 04 18:17:44 2022 +0200
@@ -11,7 +11,6 @@
    A terminal with UTF-8 support and monospace narrow text are required.
 '''
 
-from __future__ import absolute_import
 
 from mercurial.i18n import _
 from mercurial import (
--- a/hgext/blackbox.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/blackbox.py	Wed May 04 18:17:44 2022 +0200
@@ -42,7 +42,6 @@
 
 """
 
-from __future__ import absolute_import
 
 import re
 
@@ -106,7 +105,7 @@
 _lastlogger = loggingutil.proxylogger()
 
 
-class blackboxlogger(object):
+class blackboxlogger:
     def __init__(self, ui, repo):
         self._repo = repo
         self._trackedevents = set(ui.configlist(b'blackbox', b'track'))
--- a/hgext/bookflow.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/bookflow.py	Wed May 04 18:17:44 2022 +0200
@@ -13,7 +13,6 @@
     :hg up|co NAME: switch to bookmark
     :hg push -B .: push active bookmark
 """
-from __future__ import absolute_import
 
 from mercurial.i18n import _
 from mercurial import (
--- a/hgext/bugzilla.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/bugzilla.py	Wed May 04 18:17:44 2022 +0200
@@ -291,7 +291,6 @@
     Changeset commit comment. Bug 1234.
 '''
 
-from __future__ import absolute_import
 
 import json
 import re
@@ -435,7 +434,7 @@
 )
 
 
-class bzaccess(object):
+class bzaccess:
     '''Base class for access to Bugzilla.'''
 
     def __init__(self, ui):
@@ -691,7 +690,7 @@
 # Bugzilla via XMLRPC interface.
 
 
-class cookietransportrequest(object):
+class cookietransportrequest:
     """A Transport request method that retains cookies over its lifetime.
 
     The regular xmlrpclib transports ignore cookies. Which causes
@@ -1096,7 +1095,7 @@
         pass
 
 
-class bugzilla(object):
+class bugzilla:
     # supported versions of bugzilla. different versions have
     # different schemas.
     _versions = {
--- a/hgext/censor.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/censor.py	Wed May 04 18:17:44 2022 +0200
@@ -28,7 +28,6 @@
 ignore censored data and merely report that it was encountered.
 """
 
-from __future__ import absolute_import
 
 from mercurial.i18n import _
 from mercurial.node import short
--- a/hgext/children.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/children.py	Wed May 04 18:17:44 2022 +0200
@@ -14,7 +14,6 @@
 "children(REV)"` instead.
 '''
 
-from __future__ import absolute_import
 
 from mercurial.i18n import _
 from mercurial import (
--- a/hgext/churn.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/churn.py	Wed May 04 18:17:44 2022 +0200
@@ -8,7 +8,6 @@
 
 '''command to display statistics about repository history'''
 
-from __future__ import absolute_import, division
 
 import datetime
 import os
--- a/hgext/clonebundles.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/clonebundles.py	Wed May 04 18:17:44 2022 +0200
@@ -202,7 +202,6 @@
 Mercurial server when the bundle hosting service fails.
 """
 
-from __future__ import absolute_import
 
 from mercurial import (
     bundlecaches,
--- a/hgext/closehead.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/closehead.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 
 '''close arbitrary heads without checking them out first'''
 
-from __future__ import absolute_import
 
 from mercurial.i18n import _
 from mercurial import (
--- a/hgext/commitextras.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/commitextras.py	Wed May 04 18:17:44 2022 +0200
@@ -7,7 +7,6 @@
 
 '''adds a new flag extras to commit (ADVANCED)'''
 
-from __future__ import absolute_import
 
 import re
 
--- a/hgext/convert/__init__.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/convert/__init__.py	Wed May 04 18:17:44 2022 +0200
@@ -7,7 +7,6 @@
 
 '''import revisions from foreign VCS repositories into Mercurial'''
 
-from __future__ import absolute_import
 
 from mercurial.i18n import _
 from mercurial import registrar
--- a/hgext/convert/bzr.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/convert/bzr.py	Wed May 04 18:17:44 2022 +0200
@@ -8,7 +8,6 @@
 # This module is for handling Breezy imports or `brz`, but it's also compatible
 # with Bazaar or `bzr`, that was formerly known as Bazaar-NG;
 # it cannot access `bar` repositories, but they were never used very much.
-from __future__ import absolute_import
 
 import os
 
@@ -16,7 +15,6 @@
 from mercurial import (
     demandimport,
     error,
-    pycompat,
     util,
 )
 from . import common
@@ -210,7 +208,7 @@
             if not branch.supports_tags():
                 return {}
             tagdict = branch.tags.get_tag_dict()
-            for name, rev in pycompat.iteritems(tagdict):
+            for name, rev in tagdict.items():
                 bytetags[self.recode(name)] = rev
         return bytetags
 
--- a/hgext/convert/common.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/convert/common.py	Wed May 04 18:17:44 2022 +0200
@@ -4,12 +4,12 @@
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-from __future__ import absolute_import
 
 import base64
 import datetime
 import errno
 import os
+import pickle
 import re
 import shlex
 import subprocess
@@ -25,7 +25,6 @@
 )
 from mercurial.utils import procutil
 
-pickle = util.pickle
 propertycache = util.propertycache
 
 
@@ -35,7 +34,7 @@
     return d.encode('latin1')
 
 
-class _shlexpy3proxy(object):
+class _shlexpy3proxy:
     def __init__(self, l):
         self._l = l
 
@@ -128,7 +127,7 @@
 SKIPREV = b'SKIP'
 
 
-class commit(object):
+class commit:
     def __init__(
         self,
         author,
@@ -158,7 +157,7 @@
         self.ctx = ctx  # for hg to hg conversions
 
 
-class converter_source(object):
+class converter_source:
     """Conversion source interface"""
 
     def __init__(self, ui, repotype, path=None, revs=None):
@@ -247,7 +246,7 @@
         if not encoding:
             encoding = self.encoding or b'utf-8'
 
-        if isinstance(s, pycompat.unicode):
+        if isinstance(s, str):
             return s.encode("utf-8")
         try:
             return s.decode(pycompat.sysstr(encoding)).encode("utf-8")
@@ -308,7 +307,7 @@
         return True
 
 
-class converter_sink(object):
+class converter_sink:
     """Conversion sink (target) interface"""
 
     def __init__(self, ui, repotype, path):
@@ -404,7 +403,7 @@
         raise NotImplementedError
 
 
-class commandline(object):
+class commandline:
     def __init__(self, ui, command):
         self.ui = ui
         self.command = command
@@ -418,7 +417,7 @@
     def _cmdline(self, cmd, *args, **kwargs):
         kwargs = pycompat.byteskwargs(kwargs)
         cmdline = [self.command, cmd] + list(args)
-        for k, v in pycompat.iteritems(kwargs):
+        for k, v in kwargs.items():
             if len(k) == 1:
                 cmdline.append(b'-' + k)
             else:
@@ -553,7 +552,7 @@
             if err.errno != errno.ENOENT:
                 raise
             return
-        for i, line in enumerate(util.iterfile(fp)):
+        for i, line in enumerate(fp):
             line = line.splitlines()[0].rstrip()
             if not line:
                 # Ignore blank lines
--- a/hgext/convert/convcmd.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/convert/convcmd.py	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-from __future__ import absolute_import
 
 import collections
 import os
@@ -87,7 +86,7 @@
 
 
 def recode(s):
-    if isinstance(s, pycompat.unicode):
+    if isinstance(s, str):
         return s.encode(pycompat.sysstr(orig_encoding), 'replace')
     else:
         return s.decode('utf-8').encode(
@@ -177,7 +176,7 @@
     raise error.Abort(_(b'%s: unknown repository type') % path)
 
 
-class progresssource(object):
+class progresssource:
     def __init__(self, ui, source, filecount):
         self.ui = ui
         self.source = source
@@ -199,7 +198,7 @@
         self.progress.complete()
 
 
-class converter(object):
+class converter:
     def __init__(self, ui, source, dest, revmapfile, opts):
 
         self.source = source
@@ -243,7 +242,7 @@
         m = {}
         try:
             fp = open(path, b'rb')
-            for i, line in enumerate(util.iterfile(fp)):
+            for i, line in enumerate(fp):
                 line = line.splitlines()[0].rstrip()
                 if not line:
                     # Ignore blank lines
@@ -585,9 +584,7 @@
                         # write another hash correspondence to override the
                         # previous one so we don't end up with extra tag heads
                         tagsparents = [
-                            e
-                            for e in pycompat.iteritems(self.map)
-                            if e[1] == tagsparent
+                            e for e in self.map.items() if e[1] == tagsparent
                         ]
                         if tagsparents:
                             self.map[tagsparents[0][0]] = nrev
--- a/hgext/convert/cvs.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/convert/cvs.py	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-from __future__ import absolute_import
 
 import errno
 import os
@@ -19,7 +18,6 @@
 from mercurial import (
     encoding,
     error,
-    pycompat,
     util,
 )
 from mercurial.utils import (
@@ -317,7 +315,7 @@
         if full:
             raise error.Abort(_(b"convert from cvs does not support --full"))
         self._parse()
-        return sorted(pycompat.iteritems(self.files[rev])), {}, set()
+        return sorted(self.files[rev].items()), {}, set()
 
     def getcommit(self, rev):
         self._parse()
--- a/hgext/convert/cvsps.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/convert/cvsps.py	Wed May 04 18:17:44 2022 +0200
@@ -4,10 +4,10 @@
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-from __future__ import absolute_import
 
 import functools
 import os
+import pickle
 import re
 
 from mercurial.i18n import _
@@ -25,10 +25,8 @@
     stringutil,
 )
 
-pickle = util.pickle
 
-
-class logentry(object):
+class logentry:
     """Class logentry has the following attributes:
     .author    - author name as CVS knows it
     .branch    - name of branch this revision is on
@@ -468,7 +466,7 @@
 
             # find the branches starting from this revision
             branchpoints = set()
-            for branch, revision in pycompat.iteritems(branchmap):
+            for branch, revision in branchmap.items():
                 revparts = tuple([int(i) for i in revision.split(b'.')])
                 if len(revparts) < 2:  # bad tags
                     continue
@@ -579,7 +577,7 @@
     return log
 
 
-class changeset(object):
+class changeset:
     """Class changeset has the following attributes:
     .id        - integer identifying this changeset (list index)
     .author    - author name as CVS knows it
--- a/hgext/convert/darcs.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/convert/darcs.py	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-from __future__ import absolute_import
 
 import errno
 import os
@@ -114,7 +113,7 @@
         shutil.rmtree(self.tmppath, ignore_errors=True)
 
     def recode(self, s, encoding=None):
-        if isinstance(s, pycompat.unicode):
+        if isinstance(s, str):
             # XMLParser returns unicode objects for anything it can't
             # encode into ASCII. We convert them back to str to get
             # recode's normal conversion behavior.
--- a/hgext/convert/filemap.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/convert/filemap.py	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import, print_function
 
 import posixpath
 
@@ -42,7 +41,7 @@
     return posixpath.normpath(path)
 
 
-class filemapper(object):
+class filemapper:
     """Map and filter filenames when importing.
     A name can be mapped to itself, a new name, or None (omit from new
     repository)."""
@@ -126,7 +125,7 @@
         repo belong to the source repo and what parts don't."""
         if self.targetprefixes is None:
             self.targetprefixes = set()
-            for before, after in pycompat.iteritems(self.rename):
+            for before, after in self.rename.items():
                 self.targetprefixes.add(after)
 
         # If "." is a target, then all target files are considered from the
--- a/hgext/convert/git.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/convert/git.py	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-from __future__ import absolute_import
 
 import os
 
@@ -20,7 +19,7 @@
 from . import common
 
 
-class submodule(object):
+class submodule:
     def __init__(self, path, node, url):
         self.path = path
         self.node = node
--- a/hgext/convert/gnuarch.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/convert/gnuarch.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-from __future__ import absolute_import
 
 import os
 import shutil
@@ -28,7 +27,7 @@
 
 
 class gnuarch_source(common.converter_source, common.commandline):
-    class gnuarch_rev(object):
+    class gnuarch_rev:
         def __init__(self, rev):
             self.rev = rev
             self.summary = b''
--- a/hgext/convert/hg.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/convert/hg.py	Wed May 04 18:17:44 2022 +0200
@@ -16,7 +16,6 @@
 #   identifier to be stored in the converted revision. This will cause
 #   the converted revision to have a different identity than the
 #   source.
-from __future__ import absolute_import
 
 import os
 import re
@@ -40,7 +39,6 @@
     merge as mergemod,
     mergestate,
     phases,
-    pycompat,
     util,
 )
 from mercurial.utils import dateutil
@@ -139,7 +137,7 @@
 
         if missings:
             self.after()
-            for pbranch, heads in sorted(pycompat.iteritems(missings)):
+            for pbranch, heads in sorted(missings.items()):
                 pbranchpath = os.path.join(self.path, pbranch)
                 prepo = hg.peer(self.ui, {}, pbranchpath)
                 self.ui.note(
@@ -424,7 +422,7 @@
         tagparent = tagparent or self.repo.nullid
 
         oldlines = set()
-        for branch, heads in pycompat.iteritems(self.repo.branchmap()):
+        for branch, heads in self.repo.branchmap().items():
             for h in heads:
                 if b'.hgtags' in self.repo[h]:
                     oldlines.update(
@@ -596,7 +594,7 @@
         maappend = ma.append
         rappend = r.append
         d = ctx1.manifest().diff(ctx2.manifest())
-        for f, ((node1, flag1), (node2, flag2)) in pycompat.iteritems(d):
+        for f, ((node1, flag1), (node2, flag2)) in d.items():
             if node2 is None:
                 rappend(f)
             else:
@@ -622,7 +620,7 @@
         cleanp2 = set()
         if len(parents) == 2:
             d = parents[1].manifest().diff(ctx.manifest(), clean=True)
-            for f, value in pycompat.iteritems(d):
+            for f, value in d.items():
                 if value is None:
                     cleanp2.add(f)
         changes = [(f, rev) for f in files if f not in self.ignored]
--- a/hgext/convert/monotone.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/convert/monotone.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-from __future__ import absolute_import
 
 import os
 import re
@@ -103,7 +102,7 @@
         # Prepare the command in automate stdio format
         kwargs = pycompat.byteskwargs(kwargs)
         command = []
-        for k, v in pycompat.iteritems(kwargs):
+        for k, v in kwargs.items():
             command.append(b"%d:%s" % (len(k), k))
             if v:
                 command.append(b"%d:%s" % (len(v), v))
@@ -151,7 +150,7 @@
                 raise error.Abort(_(b'bad mtn packet - no end of packet size'))
             lengthstr += read
         try:
-            length = pycompat.long(lengthstr[:-1])
+            length = int(lengthstr[:-1])
         except TypeError:
             raise error.Abort(
                 _(b'bad mtn packet - bad packet size %s') % lengthstr
--- a/hgext/convert/p4.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/convert/p4.py	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-from __future__ import absolute_import
 
 import marshal
 import re
--- a/hgext/convert/subversion.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/convert/subversion.py	Wed May 04 18:17:44 2022 +0200
@@ -1,11 +1,11 @@
 # Subversion 1.4/1.5 Python API backend
 #
 # Copyright(C) 2007 Daniel Holth et al
-from __future__ import absolute_import
 
 import codecs
 import locale
 import os
+import pickle
 import re
 import xml.dom.minidom
 
@@ -26,7 +26,6 @@
 
 from . import common
 
-pickle = util.pickle
 stringio = util.stringio
 propertycache = util.propertycache
 urlerr = util.urlerr
@@ -181,7 +180,7 @@
     return optrev
 
 
-class changedpath(object):
+class changedpath:
     def __init__(self, p):
         self.copyfrom_path = p.copyfrom_path
         self.copyfrom_rev = p.copyfrom_rev
@@ -203,7 +202,7 @@
     def receiver(orig_paths, revnum, author, date, message, pool):
         paths = {}
         if orig_paths is not None:
-            for k, v in pycompat.iteritems(orig_paths):
+            for k, v in orig_paths.items():
                 paths[k] = changedpath(v)
         pickle.dump((paths, revnum, author, date, message), fp, protocol)
 
@@ -249,7 +248,7 @@
         get_log_child(ui.fout, *args)
 
 
-class logstream(object):
+class logstream:
     """Interruptible revision log iterator."""
 
     def __init__(self, stdout):
@@ -298,7 +297,7 @@
         def receiver(orig_paths, revnum, author, date, message, pool):
             paths = {}
             if orig_paths is not None:
-                for k, v in pycompat.iteritems(orig_paths):
+                for k, v in orig_paths.items():
                     paths[k] = changedpath(v)
             self.append((paths, revnum, author, date, message))
 
@@ -730,7 +729,7 @@
             )
             files = [
                 n
-                for n, e in pycompat.iteritems(entries)
+                for n, e in entries.items()
                 if e.kind == svn.core.svn_node_file
             ]
             self.removed = set()
@@ -820,7 +819,7 @@
                     origpaths = []
                 copies = [
                     (e.copyfrom_path, e.copyfrom_rev, p)
-                    for p, e in pycompat.iteritems(origpaths)
+                    for p, e in origpaths.items()
                     if e.copyfrom_path
                 ]
                 # Apply moves/copies from more specific to general
@@ -851,7 +850,7 @@
                 # be represented in mercurial.
                 addeds = {
                     p: e.copyfrom_path
-                    for p, e in pycompat.iteritems(origpaths)
+                    for p, e in origpaths.items()
                     if e.action == b'A' and e.copyfrom_path
                 }
                 badroots = set()
@@ -1140,7 +1139,7 @@
             parents = []
             # check whether this revision is the start of a branch or part
             # of a branch renaming
-            orig_paths = sorted(pycompat.iteritems(orig_paths))
+            orig_paths = sorted(orig_paths.items())
             root_paths = [
                 (p, e) for p, e in orig_paths if self.module.startswith(p)
             ]
@@ -1302,7 +1301,7 @@
             path += b'/'
         return (
             (path + p)
-            for p, e in pycompat.iteritems(entries)
+            for p, e in entries.items()
             if e.kind == svn.core.svn_node_file
         )
 
--- a/hgext/convert/transport.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/convert/transport.py	Wed May 04 18:17:44 2022 +0200
@@ -16,7 +16,6 @@
 
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, see <http://www.gnu.org/licenses/>.
-from __future__ import absolute_import
 
 import svn.client
 import svn.core
@@ -71,7 +70,7 @@
     pass
 
 
-class SvnRaTransport(object):
+class SvnRaTransport:
     """
     Open an ra connection to a Subversion repository.
     """
@@ -108,7 +107,7 @@
             self.ra = ra
             svn.ra.reparent(self.ra, self.svn_url.encode('utf8'))
 
-    class Reporter(object):
+    class Reporter:
         def __init__(self, reporter_data):
             self._reporter, self._baton = reporter_data
 
--- a/hgext/eol.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/eol.py	Wed May 04 18:17:44 2022 +0200
@@ -91,7 +91,6 @@
 used.
 """
 
-from __future__ import absolute_import
 
 import os
 import re
@@ -186,7 +185,7 @@
 }
 
 
-class eolfile(object):
+class eolfile:
     def __init__(self, ui, root, data):
         self._decode = {
             b'LF': b'to-lf',
@@ -379,7 +378,7 @@
 
     if not repo.local():
         return
-    for name, fn in pycompat.iteritems(filters):
+    for name, fn in filters.items():
         repo.adddatafilter(name, fn)
 
     ui.setconfig(b'patch', b'eol', b'auto', b'eol')
--- a/hgext/extdiff.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/extdiff.py	Wed May 04 18:17:44 2022 +0200
@@ -81,7 +81,6 @@
 pretty fast (at least faster than having to compare the entire tree).
 '''
 
-from __future__ import absolute_import
 
 import os
 import re
@@ -696,7 +695,7 @@
     return dodiff(ui, repo, cmdline, pats, opts)
 
 
-class savedcmd(object):
+class savedcmd:
     """use external program to diff repository (or selected files)
 
     Show differences between revisions for the specified files, using
--- a/hgext/factotum.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/factotum.py	Wed May 04 18:17:44 2022 +0200
@@ -45,7 +45,6 @@
 
 '''
 
-from __future__ import absolute_import
 
 import os
 from mercurial.i18n import _
--- a/hgext/fastannotate/__init__.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/fastannotate/__init__.py	Wed May 04 18:17:44 2022 +0200
@@ -101,7 +101,6 @@
 #
 # * format changes to the revmap file (maybe use length-encoding
 #   instead of null-terminated file paths at least?)
-from __future__ import absolute_import
 
 from mercurial.i18n import _
 from mercurial import (
--- a/hgext/fastannotate/commands.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/fastannotate/commands.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import os
 
--- a/hgext/fastannotate/context.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/fastannotate/context.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import collections
 import contextlib
@@ -160,7 +159,7 @@
 _defaultdiffopthash = hashdiffopts(mdiff.defaultopts)
 
 
-class annotateopts(object):
+class annotateopts:
     """like mercurial.mdiff.diffopts, but is for annotate
 
     followrename: follow renames, like "hg annotate -f"
@@ -175,7 +174,7 @@
 
     def __init__(self, **opts):
         opts = pycompat.byteskwargs(opts)
-        for k, v in pycompat.iteritems(self.defaults):
+        for k, v in self.defaults.items():
             setattr(self, k, opts.get(k, v))
 
     @util.propertycache
@@ -197,7 +196,7 @@
 defaultopts = annotateopts()
 
 
-class _annotatecontext(object):
+class _annotatecontext:
     """do not use this class directly as it does not use lock to protect
     writes. use "with annotatecontext(...)" instead.
     """
@@ -584,7 +583,7 @@
             # find an unresolved line and its linelog rev to annotate
             hsh = None
             try:
-                for (rev, _linenum), idxs in pycompat.iteritems(key2idxs):
+                for (rev, _linenum), idxs in key2idxs.items():
                     if revmap.rev2flag(rev) & revmapmod.sidebranchflag:
                         continue
                     hsh = annotateresult[idxs[0]][0]
@@ -595,7 +594,7 @@
                 # the remaining key2idxs are not in main branch, resolving them
                 # using the hard way...
                 revlines = {}
-                for (rev, linenum), idxs in pycompat.iteritems(key2idxs):
+                for (rev, linenum), idxs in key2idxs.items():
                     if rev not in revlines:
                         hsh = annotateresult[idxs[0]][0]
                         if self.ui.debugflag:
@@ -784,7 +783,7 @@
             pass
 
 
-class pathhelper(object):
+class pathhelper:
     """helper for getting paths for lockfile, linelog and revmap"""
 
     def __init__(self, repo, path, opts=defaultopts):
--- a/hgext/fastannotate/error.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/fastannotate/error.py	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-from __future__ import absolute_import
 
 
 class CorruptedFileError(Exception):
--- a/hgext/fastannotate/formatter.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/fastannotate/formatter.py	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-from __future__ import absolute_import
 
 from mercurial.node import (
     hex,
@@ -20,7 +19,7 @@
 
 # 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(object):
+class defaultformatter:
     """the default formatter that does leftpad and support some common flags"""
 
     def __init__(self, ui, repo, opts):
--- a/hgext/fastannotate/protocol.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/fastannotate/protocol.py	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-from __future__ import absolute_import
 
 import contextlib
 import os
@@ -15,7 +14,6 @@
     error,
     extensions,
     hg,
-    pycompat,
     util,
     wireprotov1peer,
     wireprotov1server,
@@ -190,7 +188,7 @@
         for result in results:
             r = result.result()
             # TODO: pconvert these paths on the server?
-            r = {util.pconvert(p): v for p, v in pycompat.iteritems(r)}
+            r = {util.pconvert(p): v for p, v in r.items()}
             for path in sorted(r):
                 # ignore malicious paths
                 if not path.startswith(b'fastannotate/') or b'/../' in (
--- a/hgext/fastannotate/revmap.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/fastannotate/revmap.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import bisect
 import io
@@ -49,7 +48,7 @@
 _hshlen = 20
 
 
-class revmap(object):
+class revmap:
     """trivial hg bin hash - linelog rev bidirectional map
 
     also stores a flag (uint8) for each revision, and track renames.
--- a/hgext/fastannotate/support.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/fastannotate/support.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from mercurial.pycompat import getattr
 from mercurial import (
@@ -23,7 +22,7 @@
 )
 
 
-class _lazyfctx(object):
+class _lazyfctx:
     """delegates to fctx but do not construct fctx when unnecessary"""
 
     def __init__(self, repo, node, path):
--- a/hgext/fastexport.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/fastexport.py	Wed May 04 18:17:44 2022 +0200
@@ -7,7 +7,6 @@
 # The format specification for fast-import streams can be found at
 # https://git-scm.com/docs/git-fast-import#_input_format
 
-from __future__ import absolute_import
 import re
 
 from mercurial.i18n import _
--- a/hgext/fetch.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/fetch.py	Wed May 04 18:17:44 2022 +0200
@@ -7,7 +7,6 @@
 
 '''pull, update and merge in one command (DEPRECATED)'''
 
-from __future__ import absolute_import
 
 from mercurial.i18n import _
 from mercurial.node import short
--- a/hgext/fix.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/fix.py	Wed May 04 18:17:44 2022 +0200
@@ -122,7 +122,6 @@
 file content back to stdout as documented above.
 """
 
-from __future__ import absolute_import
 
 import collections
 import itertools
@@ -378,9 +377,7 @@
     Useful as a hook point for extending "hg fix" with output summarizing the
     effects of the command, though we choose not to output anything here.
     """
-    replacements = {
-        prec: [succ] for prec, succ in pycompat.iteritems(replacements)
-    }
+    replacements = {prec: [succ] for prec, succ in replacements.items()}
     scmutil.cleanupnodes(repo, replacements, b'fix', fixphase=True)
 
 
@@ -693,7 +690,7 @@
     """
     metadata = {}
     newdata = fixctx[path].data()
-    for fixername, fixer in pycompat.iteritems(fixers):
+    for fixername, fixer in fixers.items():
         if fixer.affects(opts, fixctx, path):
             ranges = lineranges(
                 opts, path, basepaths, basectxs, fixctx, newdata
@@ -771,7 +768,7 @@
 
     Directly updates the dirstate for the affected files.
     """
-    for path, data in pycompat.iteritems(filedata):
+    for path, data in filedata.items():
         fctx = ctx[path]
         fctx.write(data, fctx.flags())
 
@@ -906,7 +903,7 @@
     return names
 
 
-class Fixer(object):
+class Fixer:
     """Wraps the raw config values for a fixer with methods"""
 
     def __init__(
--- a/hgext/fsmonitor/__init__.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/fsmonitor/__init__.py	Wed May 04 18:17:44 2022 +0200
@@ -107,7 +107,6 @@
 # The issues related to nested repos and subrepos are probably not fundamental
 # ones. Patches to fix them are welcome.
 
-from __future__ import absolute_import
 
 import codecs
 import os
@@ -336,7 +335,7 @@
     nonnormalset = {
         f
         for f, e in self._map.items()
-        if e.v1_state() != b"n" or e.v1_mtime() == -1
+        if e._v1_state() != b"n" or e._v1_mtime() == -1
     }
 
     copymap = self._map.copymap
@@ -502,15 +501,11 @@
             visit.update(f for f in copymap if f not in results and matchfn(f))
     else:
         if matchalways:
-            visit.update(
-                f for f, st in pycompat.iteritems(dmap) if f not in results
-            )
+            visit.update(f for f, st in dmap.items() if f not in results)
             visit.update(f for f in copymap if f not in results)
         else:
             visit.update(
-                f
-                for f, st in pycompat.iteritems(dmap)
-                if f not in results and matchfn(f)
+                f for f, st in dmap.items() if f not in results and matchfn(f)
             )
             visit.update(f for f in copymap if f not in results and matchfn(f))
 
@@ -686,7 +681,7 @@
     )
 
 
-class poststatus(object):
+class poststatus:
     def __init__(self, startclock):
         self._startclock = pycompat.sysbytes(startclock)
 
@@ -761,7 +756,7 @@
             pass
 
 
-class state_update(object):
+class state_update:
     """This context manager is responsible for dispatching the state-enter
     and state-leave signals to the watchman service. The enter and leave
     methods can be invoked manually (for scenarios where context manager
--- a/hgext/fsmonitor/pywatchman/__init__.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/fsmonitor/pywatchman/__init__.py	Wed May 04 18:17:44 2022 +0200
@@ -27,7 +27,6 @@
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 # no unicode literals
-from __future__ import absolute_import, division, print_function
 
 import inspect
 import math
@@ -302,7 +301,7 @@
         )
 
 
-class Transport(object):
+class Transport:
     """communication transport to the watchman server"""
 
     buf = None
@@ -347,7 +346,7 @@
             self.buf.append(b)
 
 
-class Codec(object):
+class Codec:
     """communication encoding for the watchman server"""
 
     transport = None
@@ -860,7 +859,7 @@
         self.transport.write(cmd + b"\n")
 
 
-class client(object):
+class client:
     """Handles the communication with the watchman service"""
 
     sockpath = None
--- a/hgext/fsmonitor/pywatchman/capabilities.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/fsmonitor/pywatchman/capabilities.py	Wed May 04 18:17:44 2022 +0200
@@ -27,7 +27,6 @@
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 # no unicode literals
-from __future__ import absolute_import, division, print_function
 
 
 def parse_version(vstr):
--- a/hgext/fsmonitor/pywatchman/compat.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/fsmonitor/pywatchman/compat.py	Wed May 04 18:17:44 2022 +0200
@@ -27,7 +27,6 @@
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 # no unicode literals
-from __future__ import absolute_import, division, print_function
 
 import sys
 
--- a/hgext/fsmonitor/pywatchman/encoding.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/fsmonitor/pywatchman/encoding.py	Wed May 04 18:17:44 2022 +0200
@@ -27,7 +27,6 @@
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 # no unicode literals
-from __future__ import absolute_import, division, print_function
 
 import sys
 
--- a/hgext/fsmonitor/pywatchman/load.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/fsmonitor/pywatchman/load.py	Wed May 04 18:17:44 2022 +0200
@@ -27,7 +27,6 @@
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 # no unicode literals
-from __future__ import absolute_import, division, print_function
 
 import ctypes
 
--- a/hgext/fsmonitor/pywatchman/pybser.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/fsmonitor/pywatchman/pybser.py	Wed May 04 18:17:44 2022 +0200
@@ -27,7 +27,6 @@
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 # no unicode literals
-from __future__ import absolute_import, division, print_function
 
 import binascii
 import collections
@@ -94,7 +93,7 @@
     return ret
 
 
-class _bser_buffer(object):
+class _bser_buffer:
     def __init__(self, version):
         self.bser_version = version
         self.buf = ctypes.create_string_buffer(8192)
@@ -325,7 +324,7 @@
 # This is a quack-alike with the bserObjectType in bser.c
 # It provides by getattr accessors and getitem for both index
 # and name.
-class _BunserDict(object):
+class _BunserDict:
     __slots__ = ("_keys", "_values")
 
     def __init__(self, keys, values):
@@ -351,7 +350,7 @@
         return len(self._keys)
 
 
-class Bunser(object):
+class Bunser:
     def __init__(self, mutable=True, value_encoding=None, value_errors=None):
         self.mutable = mutable
         self.value_encoding = value_encoding
--- a/hgext/fsmonitor/state.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/fsmonitor/state.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import errno
 import os
@@ -23,7 +22,7 @@
 _versionformat = b">I"
 
 
-class state(object):
+class state:
     def __init__(self, repo):
         self._vfs = repo.vfs
         self._ui = repo.ui
--- a/hgext/fsmonitor/watchmanclient.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/fsmonitor/watchmanclient.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import getpass
 
@@ -44,7 +43,7 @@
         super(WatchmanNoRoot, self).__init__(msg)
 
 
-class client(object):
+class client:
     def __init__(self, ui, root, timeout=1.0):
         err = None
         if not self._user:
--- a/hgext/git/__init__.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/git/__init__.py	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 firstborn a la Rumpelstiltskin, etc.
 """
 
-from __future__ import absolute_import
 
 import os
 
@@ -17,6 +16,7 @@
     localrepo,
     pycompat,
     registrar,
+    requirements as requirementsmod,
     scmutil,
     store,
     util,
@@ -48,7 +48,7 @@
 
 
 # TODO: extract an interface for this in core
-class gitstore(object):  # store.basicstore):
+class gitstore:  # store.basicstore):
     def __init__(self, path, vfstype):
         self.vfs = vfstype(path)
         self.opener = self.vfs
@@ -130,7 +130,7 @@
     return orig(requirements, storebasepath, vfstype)
 
 
-class gitfilestorage(object):
+class gitfilestorage:
     def file(self, path):
         if path[0:1] == b'/':
             path = path[1:]
@@ -162,7 +162,7 @@
 _BMS_PREFIX = 'refs/heads/'
 
 
-class gitbmstore(object):
+class gitbmstore:
     def __init__(self, gitrepo):
         self.gitrepo = gitrepo
         self._aclean = True
@@ -301,9 +301,15 @@
 
         class gitlocalrepo(orig):
             def _makedirstate(self):
+                v2_req = requirementsmod.DIRSTATE_V2_REQUIREMENT
+                use_dirstate_v2 = v2_req in self.requirements
+
                 # TODO narrow support here
                 return dirstate.gitdirstate(
-                    self.ui, self.vfs.base, self.store.git
+                    self.ui,
+                    self.vfs,
+                    self.store.git,
+                    use_dirstate_v2,
                 )
 
             def commit(self, *args, **kwargs):
--- a/hgext/git/dirstate.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/git/dirstate.py	Wed May 04 18:17:44 2022 +0200
@@ -1,11 +1,10 @@
-from __future__ import absolute_import
-
 import contextlib
 import errno
 import os
 
 from mercurial.node import sha1nodeconstants
 from mercurial import (
+    dirstatemap,
     error,
     extensions,
     match as matchmod,
@@ -13,6 +12,9 @@
     scmutil,
     util,
 )
+from mercurial.dirstateutils import (
+    timestamp,
+)
 from mercurial.interfaces import (
     dirstate as intdirstate,
     util as interfaceutil,
@@ -20,6 +22,9 @@
 
 from . import gitutil
 
+
+DirstateItem = dirstatemap.DirstateItem
+propertycache = util.propertycache
 pygit2 = gitutil.get_pygit2()
 
 
@@ -28,7 +33,7 @@
         return orig(filepath, warn, sourceinfo=False)
     result = []
     warnings = []
-    with open(filepath, b'rb') as fp:
+    with open(filepath, 'rb') as fp:
         for l in fp:
             l = l.strip()
             if not l or l.startswith(b'#'):
@@ -68,14 +73,29 @@
 
 
 @interfaceutil.implementer(intdirstate.idirstate)
-class gitdirstate(object):
-    def __init__(self, ui, root, gitrepo):
+class gitdirstate:
+    def __init__(self, ui, vfs, gitrepo, use_dirstate_v2):
         self._ui = ui
-        self._root = os.path.dirname(root)
+        self._root = os.path.dirname(vfs.base)
+        self._opener = vfs
         self.git = gitrepo
         self._plchangecallbacks = {}
         # TODO: context.poststatusfixup is bad and uses this attribute
         self._dirty = False
+        self._mapcls = dirstatemap.dirstatemap
+        self._use_dirstate_v2 = use_dirstate_v2
+
+    @propertycache
+    def _map(self):
+        """Return the dirstate contents (see documentation for dirstatemap)."""
+        self._map = self._mapcls(
+            self._ui,
+            self._opener,
+            self._root,
+            sha1nodeconstants,
+            self._use_dirstate_v2,
+        )
+        return self._map
 
     def p1(self):
         try:
@@ -144,6 +164,13 @@
             [],
             [],
         )
+
+        try:
+            mtime_boundary = timestamp.get_fs_now(self._opener)
+        except OSError:
+            # In largefiles or readonly context
+            mtime_boundary = None
+
         gstatus = self.git.status()
         for path, status in gstatus.items():
             path = pycompat.fsencode(path)
@@ -195,6 +222,7 @@
             scmutil.status(
                 modified, added, removed, deleted, unknown, ignored, clean
             ),
+            mtime_boundary,
         )
 
     def flagfunc(self, buildfallback):
@@ -207,6 +235,13 @@
             os.path.dirname(pycompat.fsencode(self.git.path))
         )
 
+    def get_entry(self, path):
+        """return a DirstateItem for the associated path"""
+        entry = self._map.get(path)
+        if entry is None:
+            return DirstateItem()
+        return entry
+
     def normalize(self, path):
         normed = util.normcase(path)
         assert normed == path, b"TODO handling of case folding: %s != %s" % (
--- a/hgext/git/gitlog.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/git/gitlog.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 from mercurial.i18n import _
 
 from mercurial.node import (
@@ -31,7 +29,7 @@
 pygit2 = gitutil.get_pygit2()
 
 
-class baselog(object):  # revlog.revlog):
+class baselog:  # revlog.revlog):
     """Common implementations between changelog and manifestlog."""
 
     def __init__(self, gr, db):
@@ -71,7 +69,7 @@
         return t is not None
 
 
-class baselogindex(object):
+class baselogindex:
     def __init__(self, log):
         self._log = log
 
--- a/hgext/git/gitutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/git/gitutil.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,4 @@
 """utilities to assist in working with pygit2"""
-from __future__ import absolute_import
 
 from mercurial.node import bin, hex, sha1nodeconstants
 
--- a/hgext/git/index.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/git/index.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import collections
 import os
 import sqlite3
--- a/hgext/git/manifest.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/git/manifest.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 from mercurial import (
     match as matchmod,
     pathutil,
@@ -17,7 +15,7 @@
 
 
 @interfaceutil.implementer(repository.imanifestdict)
-class gittreemanifest(object):
+class gittreemanifest:
     """Expose git trees (and optionally a builder's overlay) as a manifestdict.
 
     Very similar to mercurial.manifest.treemanifest.
@@ -260,7 +258,7 @@
 
 
 @interfaceutil.implementer(repository.imanifestrevisionstored)
-class gittreemanifestctx(object):
+class gittreemanifestctx:
     def __init__(self, repo, gittree):
         self._repo = repo
         self._tree = gittree
@@ -281,7 +279,7 @@
 
 
 @interfaceutil.implementer(repository.imanifestrevisionwritable)
-class memgittreemanifestctx(object):
+class memgittreemanifestctx:
     def __init__(self, repo, tree):
         self._repo = repo
         self._tree = tree
--- a/hgext/githelp.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/githelp.py	Wed May 04 18:17:44 2022 +0200
@@ -15,7 +15,6 @@
 produced.
 """
 
-from __future__ import absolute_import
 
 import getopt
 import re
@@ -116,14 +115,14 @@
     opts = dict(
         [
             (k, convert(v)) if isinstance(v, bytes) else (k, v)
-            for k, v in pycompat.iteritems(opts)
+            for k, v in opts.items()
         ]
     )
 
     return args, opts
 
 
-class Command(object):
+class Command:
     def __init__(self, name):
         self.name = name
         self.args = []
@@ -132,7 +131,7 @@
     def __bytes__(self):
         cmd = b"hg " + self.name
         if self.opts:
-            for k, values in sorted(pycompat.iteritems(self.opts)):
+            for k, values in sorted(self.opts.items()):
                 for v in values:
                     if v:
                         if isinstance(v, int):
@@ -164,7 +163,7 @@
         return AndCommand(self, other)
 
 
-class AndCommand(object):
+class AndCommand:
     def __init__(self, left, right):
         self.left = left
         self.right = right
--- a/hgext/gpg.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/gpg.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 
 '''commands to sign and verify changesets'''
 
-from __future__ import absolute_import
 
 import binascii
 import os
@@ -65,7 +64,7 @@
 help.CATEGORY_NAMES[_HELP_CATEGORY] = b'Signing changes (GPG)'
 
 
-class gpg(object):
+class gpg:
     def __init__(self, path, key=None):
         self.path = path
         self.key = (key and b" --local-user \"%s\"" % key) or b""
--- a/hgext/graphlog.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/graphlog.py	Wed May 04 18:17:44 2022 +0200
@@ -15,7 +15,6 @@
 revision graph is also shown.
 '''
 
-from __future__ import absolute_import
 
 from mercurial.i18n import _
 from mercurial import (
--- a/hgext/hgk.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/hgk.py	Wed May 04 18:17:44 2022 +0200
@@ -34,7 +34,6 @@
 vdiff on hovered and selected revisions.
 '''
 
-from __future__ import absolute_import
 
 import os
 
@@ -377,9 +376,7 @@
     """start interactive history viewer"""
     opts = pycompat.byteskwargs(opts)
     os.chdir(repo.root)
-    optstr = b' '.join(
-        [b'--%s %s' % (k, v) for k, v in pycompat.iteritems(opts) if v]
-    )
+    optstr = b' '.join([b'--%s %s' % (k, v) for k, v in opts.items() if v])
     if repo.filtername is None:
         optstr += b'--hidden'
 
--- a/hgext/highlight/__init__.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/highlight/__init__.py	Wed May 04 18:17:44 2022 +0200
@@ -26,7 +26,6 @@
 match (even matches with a low confidence score) will be used.
 """
 
-from __future__ import absolute_import
 
 from . import highlight
 from mercurial.hgweb import (
--- a/hgext/highlight/highlight.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/highlight/highlight.py	Wed May 04 18:17:44 2022 +0200
@@ -8,7 +8,6 @@
 # The original module was split in an interface and an implementation
 # file to defer pygments loading and speedup extension setup.
 
-from __future__ import absolute_import
 
 from mercurial import demandimport
 
--- a/hgext/histedit.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/histedit.py	Wed May 04 18:17:44 2022 +0200
@@ -190,7 +190,6 @@
 
 """
 
-from __future__ import absolute_import
 
 # chistedit dependencies that are not available everywhere
 try:
@@ -202,6 +201,7 @@
 
 import functools
 import os
+import pickle
 import struct
 
 from mercurial.i18n import _
@@ -245,7 +245,6 @@
     urlutil,
 )
 
-pickle = util.pickle
 cmdtable = {}
 command = registrar.command(cmdtable)
 
@@ -352,7 +351,7 @@
     return b''.join([b'# %s\n' % l if l else b'#\n' for l in lines])
 
 
-class histeditstate(object):
+class histeditstate:
     def __init__(self, repo):
         self.repo = repo
         self.actions = None
@@ -491,7 +490,7 @@
         return self.repo.vfs.exists(b'histedit-state')
 
 
-class histeditaction(object):
+class histeditaction:
     def __init__(self, state, node):
         self.state = state
         self.repo = state.repo
@@ -553,9 +552,7 @@
             summary = cmdutil.rendertemplate(
                 ctx, ui.config(b'histedit', b'summary-template')
             )
-        # Handle the fact that `''.splitlines() => []`
-        summary = summary.splitlines()[0] if summary else b''
-        line = b'%s %s %s' % (self.verb, ctx, summary)
+        line = b'%s %s %s' % (self.verb, ctx, stringutil.firstline(summary))
         # trim to 75 columns by default so it's not stupidly wide in my editor
         # (the 5 more are left for verb)
         maxlen = self.repo.ui.configint(b'histedit', b'linelen')
@@ -1143,7 +1140,7 @@
     return struct.unpack(b'hh', fcntl.ioctl(1, termios.TIOCGWINSZ, b'    '))
 
 
-class histeditrule(object):
+class histeditrule:
     def __init__(self, ui, ctx, pos, action=b'pick'):
         self.ui = ui
         self.ctx = ctx
@@ -1193,7 +1190,7 @@
         # This is split off from the prefix property so that we can
         # separately make the description for 'roll' red (since it
         # will get discarded).
-        return self.ctx.description().splitlines()[0].strip()
+        return stringutil.firstline(self.ctx.description())
 
     def checkconflicts(self, other):
         if other.pos > self.pos and other.origpos <= self.origpos:
@@ -1243,7 +1240,7 @@
     return line[: n - 2] + b' >'
 
 
-class _chistedit_state(object):
+class _chistedit_state:
     def __init__(
         self,
         repo,
@@ -1292,7 +1289,7 @@
         line = b"bookmark:  %s" % b' '.join(bms)
         win.addstr(3, 1, line[:length])
 
-        line = b"summary:   %s" % (ctx.description().splitlines()[0])
+        line = b"summary:   %s" % stringutil.firstline(ctx.description())
         win.addstr(4, 1, line[:length])
 
         line = b"files:     "
@@ -2102,7 +2099,7 @@
 
     mapping, tmpnodes, created, ntm = processreplacement(state)
     if mapping:
-        for prec, succs in pycompat.iteritems(mapping):
+        for prec, succs in mapping.items():
             if not succs:
                 ui.debug(b'histedit: %s is dropped\n' % short(prec))
             else:
@@ -2140,7 +2137,7 @@
     nodechanges = fd(
         {
             hf(oldn): fl([hf(n) for n in newn], name=b'node')
-            for oldn, newn in pycompat.iteritems(mapping)
+            for oldn, newn in mapping.items()
         },
         key=b"oldnode",
         value=b"newnodes",
@@ -2322,12 +2319,7 @@
 
 
 def _getsummary(ctx):
-    # a common pattern is to extract the summary but default to the empty
-    # string
-    summary = ctx.description() or b''
-    if summary:
-        summary = summary.splitlines()[0]
-    return summary
+    return stringutil.firstline(ctx.description())
 
 
 def bootstrapcontinue(ui, state, opts):
@@ -2388,7 +2380,7 @@
                     tsum = summary[len(fword) + 1 :].lstrip()
                     # safe but slow: reverse iterate over the actions so we
                     # don't clash on two commits having the same summary
-                    for na, l in reversed(list(pycompat.iteritems(newact))):
+                    for na, l in reversed(list(newact.items())):
                         actx = repo[na.node]
                         asum = _getsummary(actx)
                         if asum == tsum:
@@ -2401,7 +2393,7 @@
 
         # copy over and flatten the new list
         actions = []
-        for na, l in pycompat.iteritems(newact):
+        for na, l in newact.items():
             actions.append(na)
             actions += l
 
--- a/hgext/hooklib/__init__.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/hooklib/__init__.py	Wed May 04 18:17:44 2022 +0200
@@ -13,7 +13,6 @@
 extension as option. The functionality itself is planned to be supported
 long-term.
 """
-from __future__ import absolute_import
 from . import (
     changeset_obsoleted,
     changeset_published,
--- a/hgext/hooklib/changeset_obsoleted.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/hooklib/changeset_obsoleted.py	Wed May 04 18:17:44 2022 +0200
@@ -17,7 +17,6 @@
     python:hgext.hooklib.changeset_obsoleted.hook
 """
 
-from __future__ import absolute_import
 
 import email.errors as emailerrors
 import email.utils as emailutils
--- a/hgext/hooklib/changeset_published.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/hooklib/changeset_published.py	Wed May 04 18:17:44 2022 +0200
@@ -17,7 +17,6 @@
     python:hgext.hooklib.changeset_published.hook
 """
 
-from __future__ import absolute_import
 
 import email.errors as emailerrors
 import email.utils as emailutils
--- a/hgext/hooklib/enforce_draft_commits.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/hooklib/enforce_draft_commits.py	Wed May 04 18:17:44 2022 +0200
@@ -14,7 +14,6 @@
     python:hgext.hooklib.enforce_draft_commits.hook
 """
 
-from __future__ import absolute_import
 
 from mercurial.i18n import _
 from mercurial import (
--- a/hgext/hooklib/reject_merge_commits.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/hooklib/reject_merge_commits.py	Wed May 04 18:17:44 2022 +0200
@@ -14,7 +14,6 @@
     python:hgext.hooklib.reject_merge_commits.hook
 """
 
-from __future__ import absolute_import
 
 from mercurial.i18n import _
 from mercurial import (
--- a/hgext/hooklib/reject_new_heads.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/hooklib/reject_new_heads.py	Wed May 04 18:17:44 2022 +0200
@@ -14,7 +14,6 @@
     python:hgext.hooklib.reject_new_heads.hook
 """
 
-from __future__ import absolute_import
 
 from mercurial.i18n import _
 from mercurial import (
--- a/hgext/infinitepush/__init__.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/infinitepush/__init__.py	Wed May 04 18:17:44 2022 +0200
@@ -87,7 +87,6 @@
     bookmarks = True
 """
 
-from __future__ import absolute_import
 
 import collections
 import contextlib
@@ -287,7 +286,7 @@
     return remotebookmark
 
 
-class bundlestore(object):
+class bundlestore:
     def __init__(self, repo):
         self._repo = repo
         storetype = self._repo.ui.config(b'infinitepush', b'storetype')
@@ -406,7 +405,7 @@
 
 def wireprotolistkeyspatterns(repo, proto, namespace, patterns):
     patterns = wireprototypes.decodelist(patterns)
-    d = pycompat.iteritems(repo.listkeys(encoding.tolocal(namespace), patterns))
+    d = repo.listkeys(encoding.tolocal(namespace), patterns).items()
     return pushkey.encodekeys(d)
 
 
@@ -420,7 +419,7 @@
             if pattern.endswith(b'*'):
                 pattern = b're:^' + pattern[:-1] + b'.*'
             kind, pat, matcher = stringutil.stringmatcher(pattern)
-            for bookmark, node in pycompat.iteritems(bookmarks):
+            for bookmark, node in bookmarks.items():
                 if matcher(bookmark):
                     results[bookmark] = node
         return results
@@ -543,7 +542,7 @@
                     if part.type == b'changegroup':
                         haschangegroup = True
                     newpart = bundle2.bundlepart(part.type, data=part.read())
-                    for key, value in pycompat.iteritems(part.params):
+                    for key, value in part.params.items():
                         newpart.addparam(key, value)
                     parts.append(newpart)
 
@@ -795,7 +794,7 @@
             # saveremotenames expects 20 byte binary nodes for branches
             branches[rname].append(bin(hexnode))
 
-    for bookmark, hexnode in pycompat.iteritems(newbookmarks):
+    for bookmark, hexnode in newbookmarks.items():
         bookmarks[bookmark] = hexnode
     remotenamesext.saveremotenames(repo, remotepath, branches, bookmarks)
 
@@ -805,7 +804,7 @@
         return
     with repo.wlock(), repo.lock(), repo.transaction(b'bookmark') as tr:
         changes = []
-        for scratchbook, node in pycompat.iteritems(bookmarks):
+        for scratchbook, node in bookmarks.items():
             changectx = repo[node]
             changes.append((scratchbook, changectx.node()))
         repo._bookmarks.applychanges(repo, tr, changes)
@@ -1046,7 +1045,7 @@
                 bundle2._processpart(op, part)
             else:
                 bundlepart = bundle2.bundlepart(part.type, data=part.read())
-                for key, value in pycompat.iteritems(part.params):
+                for key, value in part.params.items():
                     bundlepart.addparam(key, value)
 
                 # Certain parts require a response
@@ -1138,7 +1137,7 @@
                     # differs from previous behavior, we need to put it behind a
                     # config flag for incremental rollout.
                     bundlepart = bundle2.bundlepart(part.type, data=part.read())
-                    for key, value in pycompat.iteritems(part.params):
+                    for key, value in part.params.items():
                         bundlepart.addparam(key, value)
 
                     # Certain parts require a response
@@ -1324,9 +1323,7 @@
                 b'new': newnode,
                 b'old': oldnode,
             }
-            op.reply.newpart(
-                b'pushkey', mandatoryparams=pycompat.iteritems(params)
-            )
+            op.reply.newpart(b'pushkey', mandatoryparams=params.items())
 
 
 def bundle2pushkey(orig, op, part):
--- a/hgext/infinitepush/bundleparts.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/infinitepush/bundleparts.py	Wed May 04 18:17:44 2022 +0200
@@ -3,7 +3,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from mercurial.i18n import _
 from mercurial.node import hex
@@ -13,7 +12,6 @@
     changegroup,
     error,
     extensions,
-    pycompat,
     revsetlang,
     util,
 )
@@ -68,7 +66,7 @@
     parts.append(
         bundle2.bundlepart(
             scratchbranchparttype.upper(),
-            advisoryparams=pycompat.iteritems(params),
+            advisoryparams=params.items(),
             data=cg,
         )
     )
@@ -103,7 +101,7 @@
         return
 
 
-class copiedpart(object):
+class copiedpart:
     """a copy of unbundlepart content that can be consumed later"""
 
     def __init__(self, part):
--- a/hgext/infinitepush/common.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/infinitepush/common.py	Wed May 04 18:17:44 2022 +0200
@@ -3,7 +3,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import os
 
--- a/hgext/infinitepush/fileindexapi.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/infinitepush/fileindexapi.py	Wed May 04 18:17:44 2022 +0200
@@ -11,7 +11,6 @@
     indexpath = PATH
 """
 
-from __future__ import absolute_import
 
 import os
 
--- a/hgext/infinitepush/indexapi.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/infinitepush/indexapi.py	Wed May 04 18:17:44 2022 +0200
@@ -5,10 +5,8 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
-
-class indexapi(object):
+class indexapi:
     """Class that manages access to infinitepush index.
 
     This class is a context manager and all write operations (like
--- a/hgext/infinitepush/sqlindexapi.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/infinitepush/sqlindexapi.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import logging
 import os
@@ -14,8 +13,6 @@
 import warnings
 import mysql.connector
 
-from mercurial import pycompat
-
 from . import indexapi
 
 
@@ -180,7 +177,7 @@
             self.sqlconnect()
         args = []
         values = []
-        for bookmark, node in pycompat.iteritems(bookmarks):
+        for bookmark, node in bookmarks.items():
             args.append(b'(%s, %s, %s)')
             values.extend((bookmark, node, self.reponame))
         args = b','.join(args)
--- a/hgext/infinitepush/store.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/infinitepush/store.py	Wed May 04 18:17:44 2022 +0200
@@ -3,7 +3,6 @@
 
 # based on bundleheads extension by Gregory Szorc <gps@mozilla.com>
 
-from __future__ import absolute_import
 
 import abc
 import os
@@ -26,7 +25,7 @@
     pass
 
 
-class abstractbundlestore(object):  # pytype: disable=ignored-metaclass
+class abstractbundlestore:  # pytype: disable=ignored-metaclass
     """Defines the interface for bundle stores.
 
     A bundle store is an entity that stores raw bundle data. It is a simple
@@ -57,7 +56,7 @@
         """
 
 
-class filebundlestore(object):
+class filebundlestore:
     """bundle store in filesystem
 
     meant for storing bundles somewhere on disk and on network filesystems
--- a/hgext/journal.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/journal.py	Wed May 04 18:17:44 2022 +0200
@@ -11,7 +11,6 @@
 
 """
 
-from __future__ import absolute_import
 
 import collections
 import errno
@@ -129,7 +128,7 @@
     repo = store._repo
     if util.safehasattr(repo, 'journal'):
         oldmarks = bookmarks.bmstore(repo)
-        for mark, value in pycompat.iteritems(store):
+        for mark, value in store.items():
             oldvalue = oldmarks.get(mark, repo.nullid)
             if value != oldvalue:
                 repo.journal.record(bookmarktype, mark, oldvalue, value)
@@ -167,7 +166,7 @@
             pass
 
     while iterable_map:
-        value, key, it = order(pycompat.itervalues(iterable_map))
+        value, key, it = order(iterable_map.values())
         yield value
         try:
             iterable_map[key][0] = next(it)
@@ -283,7 +282,7 @@
     __str__ = encoding.strmethod(__bytes__)
 
 
-class journalstorage(object):
+class journalstorage:
     """Storage for journal entries
 
     Entries are divided over two files; one with entries that pertain to the
--- a/hgext/keyword.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/keyword.py	Wed May 04 18:17:44 2022 +0200
@@ -83,8 +83,6 @@
 '''
 
 
-from __future__ import absolute_import
-
 import os
 import re
 import weakref
@@ -237,7 +235,7 @@
     return modified, added
 
 
-class kwtemplater(object):
+class kwtemplater:
     """
     Sets up keyword templates, corresponding keyword regex, and
     provides keyword substitution functions.
@@ -515,7 +513,7 @@
         kwmaps = _defaultkwmaps(ui)
         if uikwmaps:
             ui.status(_(b'\tdisabling current template maps\n'))
-            for k, v in pycompat.iteritems(kwmaps):
+            for k, v in kwmaps.items():
                 ui.setconfig(b'keywordmaps', k, v, b'keyword')
     else:
         ui.status(_(b'\n\tconfiguration using current keyword template maps\n'))
@@ -529,7 +527,7 @@
     ui.writenoi18n(b'[extensions]\nkeyword =\n')
     demoitems(b'keyword', ui.configitems(b'keyword'))
     demoitems(b'keywordset', ui.configitems(b'keywordset'))
-    demoitems(b'keywordmaps', pycompat.iteritems(kwmaps))
+    demoitems(b'keywordmaps', kwmaps.items())
     keywords = b'$' + b'$\n$'.join(sorted(kwmaps.keys())) + b'$\n'
     repo.wvfs.write(fn, keywords)
     repo[None].add([fn])
--- a/hgext/largefiles/__init__.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/largefiles/__init__.py	Wed May 04 18:17:44 2022 +0200
@@ -104,7 +104,6 @@
 explicitly do so with the --large flag passed to the :hg:`add`
 command.
 '''
-from __future__ import absolute_import
 
 from mercurial import (
     cmdutil,
--- a/hgext/largefiles/basestore.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/largefiles/basestore.py	Wed May 04 18:17:44 2022 +0200
@@ -7,7 +7,6 @@
 # GNU General Public License version 2 or any later version.
 
 '''base class for store implementations and store-related utility code'''
-from __future__ import absolute_import
 
 from mercurial.i18n import _
 
@@ -42,7 +41,7 @@
         return b"%s: %s" % (urlutil.hidepassword(self.url), self.detail)
 
 
-class basestore(object):
+class basestore:
     def __init__(self, ui, repo, url):
         self.ui = ui
         self.repo = repo
--- a/hgext/largefiles/lfcommands.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/largefiles/lfcommands.py	Wed May 04 18:17:44 2022 +0200
@@ -7,7 +7,6 @@
 # GNU General Public License version 2 or any later version.
 
 '''High-level command function for lfconvert, plus the cmdtable.'''
-from __future__ import absolute_import
 
 import errno
 import os
--- a/hgext/largefiles/lfutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/largefiles/lfutil.py	Wed May 04 18:17:44 2022 +0200
@@ -7,7 +7,6 @@
 # GNU General Public License version 2 or any later version.
 
 '''largefiles utility code: must not import other modules in this package.'''
-from __future__ import absolute_import
 
 import contextlib
 import copy
@@ -757,7 +756,7 @@
     return match
 
 
-class automatedcommithook(object):
+class automatedcommithook:
     """Stateful hook to update standins at the 1st commit of resuming
 
     For efficiency, updating standins in the working directory should
--- a/hgext/largefiles/localstore.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/largefiles/localstore.py	Wed May 04 18:17:44 2022 +0200
@@ -7,7 +7,6 @@
 # GNU General Public License version 2 or any later version.
 
 '''store class for local filesystem'''
-from __future__ import absolute_import
 
 from mercurial.i18n import _
 from mercurial.pycompat import open
--- a/hgext/largefiles/overrides.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/largefiles/overrides.py	Wed May 04 18:17:44 2022 +0200
@@ -7,7 +7,6 @@
 # GNU General Public License version 2 or any later version.
 
 '''Overridden Mercurial commands and functions for the largefiles extension'''
-from __future__ import absolute_import
 
 import copy
 import os
@@ -493,7 +492,7 @@
     large = opts.pop('large', False)
     if large:
 
-        class fakerepo(object):
+        class fakerepo:
             dirstate = lfutil.openlfdirstate(ui, repo)
 
         orig(ui, fakerepo, *pats, **opts)
@@ -714,7 +713,7 @@
     copies = orig(ctx1, ctx2, match=match)
     updated = {}
 
-    for k, v in pycompat.iteritems(copies):
+    for k, v in copies.items():
         updated[lfutil.splitstandin(k) or k] = lfutil.splitstandin(v) or v
 
     return updated
--- a/hgext/largefiles/proto.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/largefiles/proto.py	Wed May 04 18:17:44 2022 +0200
@@ -2,7 +2,6 @@
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-from __future__ import absolute_import
 
 import os
 
--- a/hgext/largefiles/remotestore.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/largefiles/remotestore.py	Wed May 04 18:17:44 2022 +0200
@@ -5,13 +5,11 @@
 # GNU General Public License version 2 or any later version.
 
 '''remote largefile store; the base class for wirestore'''
-from __future__ import absolute_import
 
 from mercurial.i18n import _
 
 from mercurial import (
     error,
-    pycompat,
     util,
 )
 
@@ -53,9 +51,8 @@
     def exists(self, hashes):
         return {
             h: s == 0
-            for (h, s) in pycompat.iteritems(
-                self._stat(hashes)
-            )  # dict-from-generator
+            for (h, s) in self._stat(hashes).items()
+            # dict-from-generator
         }
 
     def sendfile(self, filename, hash):
--- a/hgext/largefiles/reposetup.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/largefiles/reposetup.py	Wed May 04 18:17:44 2022 +0200
@@ -7,7 +7,6 @@
 # GNU General Public License version 2 or any later version.
 
 '''setup for largefiles repositories: reposetup'''
-from __future__ import absolute_import
 
 import copy
 
--- a/hgext/largefiles/storefactory.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/largefiles/storefactory.py	Wed May 04 18:17:44 2022 +0200
@@ -1,7 +1,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import re
 
--- a/hgext/largefiles/wirestore.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/largefiles/wirestore.py	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 # GNU General Public License version 2 or any later version.
 
 '''largefile store working over Mercurial's wire protocol'''
-from __future__ import absolute_import
 
 from . import (
     lfutil,
--- a/hgext/lfs/__init__.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/lfs/__init__.py	Wed May 04 18:17:44 2022 +0200
@@ -120,7 +120,6 @@
     usercache = /path/to/global/cache
 """
 
-from __future__ import absolute_import
 
 import sys
 
@@ -400,7 +399,7 @@
     def pointer(v):
         # In the file spec, version is first and the other keys are sorted.
         sortkeyfunc = lambda x: (x[0] != b'version', x)
-        items = sorted(pycompat.iteritems(pointers[v]), key=sortkeyfunc)
+        items = sorted(pointers[v].items(), key=sortkeyfunc)
         return util.sortdict(items)
 
     makemap = lambda v: {
--- a/hgext/lfs/blobstore.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/lfs/blobstore.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import contextlib
 import errno
@@ -109,7 +108,7 @@
         return None  # progress is handled by the worker client
 
 
-class local(object):
+class local:
     """Local blobstore for large file contents.
 
     This blobstore is used both as a cache and as a staging area for large blobs
@@ -274,7 +273,7 @@
         except (AttributeError, IndexError):
             # it might be anything, for example a string
             reason = inst.reason
-        if isinstance(reason, pycompat.unicode):
+        if isinstance(reason, str):
             # SSLError of Python 2.7.9 contains a unicode
             reason = encoding.unitolocal(reason)
         return reason
@@ -307,7 +306,7 @@
         return None
 
 
-class _gitlfsremote(object):
+class _gitlfsremote:
     def __init__(self, repo, url):
         ui = repo.ui
         self.ui = ui
@@ -407,7 +406,7 @@
             )
 
         def encodestr(x):
-            if isinstance(x, pycompat.unicode):
+            if isinstance(x, str):
                 return x.encode('utf-8')
             return x
 
@@ -643,7 +642,7 @@
                 getattr(h, "close_all", lambda: None)()
 
 
-class _dummyremote(object):
+class _dummyremote:
     """Dummy store storing blobs to temp directory."""
 
     def __init__(self, repo, url):
@@ -662,7 +661,7 @@
                 tostore.download(p.oid(), fp, None)
 
 
-class _nullremote(object):
+class _nullremote:
     """Null store storing blobs to /dev/null."""
 
     def __init__(self, repo, url):
@@ -675,7 +674,7 @@
         pass
 
 
-class _promptremote(object):
+class _promptremote:
     """Prompt user to set lfs.url when accessed."""
 
     def __init__(self, repo, url):
--- a/hgext/lfs/pointer.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/lfs/pointer.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import re
 
@@ -41,7 +40,7 @@
 
     def serialize(self):
         sortkeyfunc = lambda x: (x[0] != b'version', x)
-        items = sorted(pycompat.iteritems(self.validate()), key=sortkeyfunc)
+        items = sorted(self.validate().items(), key=sortkeyfunc)
         return b''.join(b'%s %s\n' % (k, v) for k, v in items)
 
     def oid(self):
@@ -63,7 +62,7 @@
     def validate(self):
         """raise InvalidPointer on error. return self if there is no error"""
         requiredcount = 0
-        for k, v in pycompat.iteritems(self):
+        for k, v in self.items():
             if k in self._requiredre:
                 if not self._requiredre[k].match(v):
                     raise InvalidPointer(
--- a/hgext/lfs/wireprotolfsserver.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/lfs/wireprotolfsserver.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import datetime
 import errno
--- a/hgext/lfs/wrapper.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/lfs/wrapper.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import hashlib
 
@@ -25,7 +24,6 @@
     exchange,
     exthelper,
     localrepo,
-    pycompat,
     revlog,
     scmutil,
     util,
@@ -143,7 +141,7 @@
 
     # translate hg filelog metadata to lfs metadata with "x-hg-" prefix
     if hgmeta is not None:
-        for k, v in pycompat.iteritems(hgmeta):
+        for k, v in hgmeta.items():
             metadata[b'x-hg-%s' % k] = v
 
     rawtext = metadata.serialize()
--- a/hgext/logtoprocess.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/logtoprocess.py	Wed May 04 18:17:44 2022 +0200
@@ -32,7 +32,6 @@
 
 """
 
-from __future__ import absolute_import
 
 import os
 
@@ -45,7 +44,7 @@
 testedwith = b'ships-with-hg-core'
 
 
-class processlogger(object):
+class processlogger:
     """Map log events to external commands
 
     Arguments are passed on as environment variables.
--- a/hgext/mq.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/mq.py	Wed May 04 18:17:44 2022 +0200
@@ -62,7 +62,6 @@
 in the strip extension.
 '''
 
-from __future__ import absolute_import, print_function
 
 import errno
 import os
@@ -151,7 +150,7 @@
 except KeyError:
     # note: load is lazy so we could avoid the try-except,
     # but I (marmoute) prefer this explicit code.
-    class dummyui(object):
+    class dummyui:
         def debug(self, msg):
             pass
 
@@ -184,7 +183,7 @@
 normname = util.normpath
 
 
-class statusentry(object):
+class statusentry:
     def __init__(self, node, name):
         self.node, self.name = node, name
 
@@ -294,7 +293,7 @@
     return lines
 
 
-class patchheader(object):
+class patchheader:
     def __init__(self, pf, plainmode=False):
         def eatdiff(lines):
             while lines:
@@ -496,7 +495,7 @@
     pass
 
 
-class queue(object):
+class queue:
     def __init__(self, ui, baseui, path, patchdir=None):
         self.basepath = path
         try:
@@ -2025,7 +2024,7 @@
                             # we can't copy a file created by the patch itself
                             if dst in copies:
                                 del copies[dst]
-                        for src, dsts in pycompat.iteritems(copies):
+                        for src, dsts in copies.items():
                             for dst in dsts:
                                 repo.dirstate.copy(src, dst)
                     else:
@@ -4288,7 +4287,7 @@
     entry[1].extend(mqopt)
 
     def dotable(cmdtable):
-        for cmd, entry in pycompat.iteritems(cmdtable):
+        for cmd, entry in cmdtable.items():
             cmd = cmdutil.parsealiases(cmd)[0]
             func = entry[0]
             if func.norepo:
--- a/hgext/narrow/__init__.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/narrow/__init__.py	Wed May 04 18:17:44 2022 +0200
@@ -6,7 +6,6 @@
 # GNU General Public License version 2 or any later version.
 '''create clones which fetch history data for subset of files (EXPERIMENTAL)'''
 
-from __future__ import absolute_import
 
 from mercurial import (
     localrepo,
--- a/hgext/narrow/narrowbundle2.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/narrow/narrowbundle2.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import errno
 import struct
--- a/hgext/narrow/narrowcommands.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/narrow/narrowcommands.py	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-from __future__ import absolute_import
 
 import itertools
 import os
--- a/hgext/narrow/narrowdirstate.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/narrow/narrowdirstate.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from mercurial.i18n import _
 from mercurial import error
--- a/hgext/narrow/narrowrepo.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/narrow/narrowrepo.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from mercurial import wireprototypes
 
--- a/hgext/narrow/narrowtemplates.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/narrow/narrowtemplates.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from mercurial import (
     registrar,
--- a/hgext/narrow/narrowwirepeer.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/narrow/narrowwirepeer.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from mercurial import (
     bundle2,
--- a/hgext/notify.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/notify.py	Wed May 04 18:17:44 2022 +0200
@@ -154,7 +154,6 @@
   references. See also ``notify.strip``.
 
 '''
-from __future__ import absolute_import
 
 import email.errors as emailerrors
 import email.utils as emailutils
@@ -315,7 +314,7 @@
 }
 
 
-class notifier(object):
+class notifier:
     '''email notification class.'''
 
     def __init__(self, ui, repo, hooktype):
--- a/hgext/pager.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/pager.py	Wed May 04 18:17:44 2022 +0200
@@ -21,7 +21,6 @@
   [pager]
   attend-cat = false
 '''
-from __future__ import absolute_import
 
 from mercurial import (
     cmdutil,
--- a/hgext/patchbomb.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/patchbomb.py	Wed May 04 18:17:44 2022 +0200
@@ -71,7 +71,6 @@
 You can set patchbomb to always ask for confirmation by setting
 ``patchbomb.confirm`` to true.
 '''
-from __future__ import absolute_import
 
 import email.encoders as emailencoders
 import email.mime.base as emimebase
--- a/hgext/phabricator.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/phabricator.py	Wed May 04 18:17:44 2022 +0200
@@ -57,11 +57,11 @@
     example.phabtoken = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
 """
 
-from __future__ import absolute_import
 
 import base64
 import contextlib
 import hashlib
+import io
 import itertools
 import json
 import mimetypes
@@ -219,9 +219,7 @@
         rawparams = encoding.unifromlocal(wdirvfs.read(b".arcconfig"))
         # json.loads only returns unicode strings
         arcconfig = pycompat.rapply(
-            lambda x: encoding.unitolocal(x)
-            if isinstance(x, pycompat.unicode)
-            else x,
+            lambda x: encoding.unitolocal(x) if isinstance(x, str) else x,
             pycompat.json_loads(rawparams),
         )
 
@@ -447,9 +445,7 @@
                 time.sleep(retry_interval)
     ui.debug(b'Conduit Response: %s\n' % body)
     parsed = pycompat.rapply(
-        lambda x: encoding.unitolocal(x)
-        if isinstance(x, pycompat.unicode)
-        else x,
+        lambda x: encoding.unitolocal(x) if isinstance(x, str) else x,
         # json.loads only accepts bytes from py3.6+
         pycompat.json_loads(encoding.unifromlocal(body)),
     )
@@ -473,9 +469,7 @@
     rawparams = encoding.unifromlocal(ui.fin.read())
     # json.loads only returns unicode strings
     params = pycompat.rapply(
-        lambda x: encoding.unitolocal(x)
-        if isinstance(x, pycompat.unicode)
-        else x,
+        lambda x: encoding.unitolocal(x) if isinstance(x, str) else x,
         pycompat.json_loads(rawparams),
     )
     # json.dumps only accepts unicode strings
@@ -674,7 +668,7 @@
     return output.getvalue()
 
 
-class DiffChangeType(object):
+class DiffChangeType:
     ADD = 1
     CHANGE = 2
     DELETE = 3
@@ -685,7 +679,7 @@
     MULTICOPY = 8
 
 
-class DiffFileType(object):
+class DiffFileType:
     TEXT = 1
     IMAGE = 2
     BINARY = 3
@@ -706,7 +700,7 @@
 
 
 @attr.s
-class phabchange(object):
+class phabchange:
     """Represents a Differential change, owns Differential hunks and owned by a
     Differential diff.  Each one represents one file in a diff.
     """
@@ -747,7 +741,7 @@
 
 
 @attr.s
-class phabdiff(object):
+class phabdiff:
     """Represents a Differential diff, owns Differential changes.  Corresponds
     to a commit.
     """
@@ -2200,7 +2194,7 @@
             for drev, contents in patches:
                 ui.status(_(b'applying patch from D%s\n') % drev)
 
-                with patch.extract(ui, pycompat.bytesio(contents)) as patchdata:
+                with patch.extract(ui, io.BytesIO(contents)) as patchdata:
                     msg, node, rej = cmdutil.tryimportone(
                         ui,
                         repo,
@@ -2279,7 +2273,7 @@
         drevmap = getdrevmap(repo, logcmdutil.revrange(repo, [revs]))
         specs = []
         unknown = []
-        for r, d in pycompat.iteritems(drevmap):
+        for r, d in drevmap.items():
             if d is None:
                 unknown.append(repo[r])
             else:
@@ -2364,7 +2358,7 @@
     revs = repo.revs('sort(_underway(), topo)')
     drevmap = getdrevmap(repo, revs)
     unknownrevs, drevids, revsbydrevid = [], set(), {}
-    for rev, drevid in pycompat.iteritems(drevmap):
+    for rev, drevid in drevmap.items():
         if drevid is not None:
             drevids.add(drevid)
             revsbydrevid.setdefault(drevid, set()).add(rev)
--- a/hgext/rebase.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/rebase.py	Wed May 04 18:17:44 2022 +0200
@@ -14,7 +14,6 @@
 https://mercurial-scm.org/wiki/RebaseExtension
 '''
 
-from __future__ import absolute_import
 
 import errno
 import os
@@ -160,7 +159,7 @@
     )
 
 
-class rebaseruntime(object):
+class rebaseruntime:
     """This class is a container for rebase runtime state"""
 
     def __init__(self, repo, ui, inmemory=False, dryrun=False, opts=None):
@@ -244,7 +243,7 @@
         f.write(b'%d\n' % int(self.keepbranchesf))
         f.write(b'%s\n' % (self.activebookmark or b''))
         destmap = self.destmap
-        for d, v in pycompat.iteritems(self.state):
+        for d, v in self.state.items():
             oldrev = repo[d].hex()
             if v >= 0:
                 newrev = repo[v].hex()
@@ -506,7 +505,7 @@
             # commits.
             self.storestatus(tr)
 
-        cands = [k for k, v in pycompat.iteritems(self.state) if v == revtodo]
+        cands = [k for k, v in self.state.items() if v == revtodo]
         p = repo.ui.makeprogress(
             _(b"rebasing"), unit=_(b'changesets'), total=len(cands)
         )
@@ -1337,7 +1336,7 @@
             # emulate the old behavior, showing "nothing to rebase" (a better
             # behavior may be abort with "cannot find branching point" error)
             bpbase.clear()
-        for bp, bs in pycompat.iteritems(bpbase):  # calculate roots
+        for bp, bs in bpbase.items():  # calculate roots
             roots += list(repo.revs(b'children(%d) & ancestors(%ld)', bp, bs))
 
         rebaseset = repo.revs(b'%ld::', roots)
@@ -2104,7 +2103,7 @@
         fl = fm.formatlist
         fd = fm.formatdict
         changes = {}
-        for oldns, newn in pycompat.iteritems(replacements):
+        for oldns, newn in replacements.items():
             for oldn in oldns:
                 changes[hf(oldn)] = fl([hf(n) for n in newn], name=b'node')
         nodechanges = fd(changes, key=b"oldnode", value=b"newnodes")
@@ -2258,7 +2257,7 @@
         msg = _(b'rebase: (use "hg rebase --abort" to clear broken state)\n')
         ui.write(msg)
         return
-    numrebased = len([i for i in pycompat.itervalues(state) if i >= 0])
+    numrebased = len([i for i in state.values() if i >= 0])
     # i18n: column positioning for "hg summary"
     ui.write(
         _(b'rebase: %s, %s (rebase --continue)\n')
--- a/hgext/record.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/record.py	Wed May 04 18:17:44 2022 +0200
@@ -10,7 +10,6 @@
 The feature provided by this extension has been moved into core Mercurial as
 :hg:`commit --interactive`.'''
 
-from __future__ import absolute_import
 
 from mercurial.i18n import _
 from mercurial import (
--- a/hgext/releasenotes.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/releasenotes.py	Wed May 04 18:17:44 2022 +0200
@@ -11,7 +11,6 @@
 process simpler by automating it.
 """
 
-from __future__ import absolute_import
 
 import difflib
 import errno
@@ -78,7 +77,7 @@
 BULLET_SECTION = _(b'Other Changes')
 
 
-class parsedreleasenotes(object):
+class parsedreleasenotes:
     def __init__(self):
         self.sections = {}
 
@@ -171,14 +170,14 @@
                 self.addnontitleditem(section, paragraphs)
 
 
-class releasenotessections(object):
+class releasenotessections:
     def __init__(self, ui, repo=None):
         if repo:
             sections = util.sortdict(DEFAULT_SECTIONS)
             custom_sections = getcustomadmonitions(repo)
             if custom_sections:
                 sections.update(custom_sections)
-            self._sections = list(pycompat.iteritems(sections))
+            self._sections = list(sections.items())
         else:
             self._sections = list(DEFAULT_SECTIONS)
 
--- a/hgext/relink.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/relink.py	Wed May 04 18:17:44 2022 +0200
@@ -6,7 +6,6 @@
 # GNU General Public License version 2 or any later version.
 
 """recreates hardlinks between repository clones"""
-from __future__ import absolute_import
 
 import os
 import stat
--- a/hgext/remotefilelog/__init__.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/remotefilelog/__init__.py	Wed May 04 18:17:44 2022 +0200
@@ -124,7 +124,6 @@
       corruption before returning metadata
 
 """
-from __future__ import absolute_import
 
 import os
 import time
--- a/hgext/remotefilelog/basepack.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/remotefilelog/basepack.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import collections
 import errno
 import mmap
@@ -65,7 +63,7 @@
     PACKOPENMODE = b'rb'
 
 
-class _cachebackedpacks(object):
+class _cachebackedpacks:
     def __init__(self, packs, cachesize):
         self._packs = set(packs)
         self._lrucache = util.lrucachedict(cachesize)
@@ -111,7 +109,7 @@
         self._lastpack = None
 
 
-class basepackstore(object):
+class basepackstore:
     # Default cache size limit for the pack files.
     DEFAULTCACHESIZE = 100
 
@@ -269,7 +267,7 @@
         return newpacks
 
 
-class versionmixin(object):
+class versionmixin:
     # Mix-in for classes with multiple supported versions
     VERSION = None
     SUPPORTED_VERSIONS = [2]
@@ -528,7 +526,7 @@
         self.idxfp.write(struct.pack(b'!BB', self.VERSION, config))
 
 
-class indexparams(object):
+class indexparams:
     __slots__ = (
         'fanoutprefix',
         'fanoutstruct',
--- a/hgext/remotefilelog/basestore.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/remotefilelog/basestore.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import errno
 import os
 import shutil
@@ -21,7 +19,7 @@
 )
 
 
-class basestore(object):
+class basestore:
     def __init__(self, repo, path, reponame, shared=False):
         """Creates a remotefilelog store object for the given repo name.
 
@@ -148,7 +146,7 @@
 
         filenamemap = self._resolvefilenames(existing.keys())
 
-        for filename, sha in pycompat.iteritems(filenamemap):
+        for filename, sha in filenamemap.items():
             yield (filename, existing[sha])
 
     def _resolvefilenames(self, hashes):
@@ -414,7 +412,7 @@
         )
 
 
-class baseunionstore(object):
+class baseunionstore:
     def __init__(self, *args, **kwargs):
         # If one of the functions that iterates all of the stores is about to
         # throw a KeyError, try this many times with a full refresh between
--- a/hgext/remotefilelog/connectionpool.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/remotefilelog/connectionpool.py	Wed May 04 18:17:44 2022 +0200
@@ -5,11 +5,9 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from mercurial import (
     hg,
-    pycompat,
     sshpeer,
     util,
 )
@@ -17,7 +15,7 @@
 _sshv1peer = sshpeer.sshv1peer
 
 
-class connectionpool(object):
+class connectionpool:
     def __init__(self, repo):
         self._repo = repo
         self._pool = dict()
@@ -61,13 +59,13 @@
         return conn
 
     def close(self):
-        for pathpool in pycompat.itervalues(self._pool):
+        for pathpool in self._pool.values():
             for conn in pathpool:
                 conn.close()
             del pathpool[:]
 
 
-class connection(object):
+class connection:
     def __init__(self, pool, peer):
         self._pool = pool
         self.peer = peer
--- a/hgext/remotefilelog/constants.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/remotefilelog/constants.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import struct
 
 from mercurial.i18n import _
--- a/hgext/remotefilelog/contentstore.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/remotefilelog/contentstore.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import threading
 
 from mercurial.node import (
@@ -19,7 +17,7 @@
 )
 
 
-class ChainIndicies(object):
+class ChainIndicies:
     """A static class for easy reference to the delta chain indicies."""
 
     # The filename of this revision delta
@@ -231,7 +229,7 @@
         self._threaddata.metacache = (node, meta)
 
 
-class remotecontentstore(object):
+class remotecontentstore:
     def __init__(self, ui, fileservice, shared):
         self._fileservice = fileservice
         # type(shared) is usually remotefilelogcontentstore
@@ -276,7 +274,7 @@
         pass
 
 
-class manifestrevlogstore(object):
+class manifestrevlogstore:
     def __init__(self, repo):
         self._store = repo.store
         self._svfs = repo.svfs
--- a/hgext/remotefilelog/datapack.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/remotefilelog/datapack.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import struct
 import zlib
 
@@ -455,7 +453,7 @@
 
     def createindex(self, nodelocations, indexoffset):
         entries = sorted(
-            (n, db, o, s) for n, (db, o, s) in pycompat.iteritems(self.entries)
+            (n, db, o, s) for n, (db, o, s) in self.entries.items()
         )
 
         rawindex = b''
--- a/hgext/remotefilelog/debugcommands.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/remotefilelog/debugcommands.py	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-from __future__ import absolute_import
 
 import os
 import zlib
@@ -82,7 +81,7 @@
         os.remove(temppath)
     r = filelog.filelog(repo.svfs, b'temprevlog')
 
-    class faket(object):
+    class faket:
         def add(self, a, b, c):
             pass
 
@@ -211,7 +210,7 @@
                 continue
             filepath = os.path.join(root, file)
             size, firstnode, mapping = parsefileblob(filepath, decompress)
-            for p1, p2, linknode, copyfrom in pycompat.itervalues(mapping):
+            for p1, p2, linknode, copyfrom in mapping.values():
                 if linknode == sha1nodeconstants.nullid:
                     actualpath = os.path.relpath(root, path)
                     key = fileserverclient.getcachekey(
--- a/hgext/remotefilelog/fileserverclient.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/remotefilelog/fileserverclient.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import io
 import os
@@ -140,7 +139,7 @@
     peer.__class__ = remotefilepeer
 
 
-class cacheconnection(object):
+class cacheconnection:
     """The connection for communicating with the remote cache. Performs
     gets and sets by communicating with an external process that has the
     cache-specific implementation.
@@ -303,7 +302,7 @@
     pipeo.flush()
 
 
-class fileserverclient(object):
+class fileserverclient:
     """A client for requesting files from the remote file server."""
 
     def __init__(self, repo):
@@ -518,7 +517,7 @@
             # returns cache misses.  This enables tests to run easily
             # and may eventually allow us to be a drop in replacement
             # for the largefiles extension.
-            class simplecache(object):
+            class simplecache:
                 def __init__(self):
                     self.missingids = []
                     self.connected = True
--- a/hgext/remotefilelog/historypack.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/remotefilelog/historypack.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import struct
 
 from mercurial.node import (
@@ -521,7 +519,7 @@
 
         files = (
             (hashutil.sha1(filename).digest(), filename, offset, size)
-            for filename, (offset, size) in pycompat.iteritems(self.files)
+            for filename, (offset, size) in self.files.items()
         )
         files = sorted(files)
 
@@ -557,7 +555,7 @@
             )
             nodeindexoffset += constants.FILENAMESIZE + len(filename)
 
-            for node, location in sorted(pycompat.iteritems(nodelocations)):
+            for node, location in sorted(nodelocations.items()):
                 nodeindexentries.append(
                     struct.pack(nodeindexformat, node, location)
                 )
--- a/hgext/remotefilelog/metadatastore.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/remotefilelog/metadatastore.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 from mercurial.node import (
     hex,
     sha1nodeconstants,
@@ -143,7 +141,7 @@
         )
 
 
-class remotemetadatastore(object):
+class remotemetadatastore:
     def __init__(self, ui, fileservice, shared):
         self._fileservice = fileservice
         self._shared = shared
--- a/hgext/remotefilelog/remotefilectx.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/remotefilelog/remotefilectx.py	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-from __future__ import absolute_import
 
 import collections
 import time
--- a/hgext/remotefilelog/remotefilelog.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/remotefilelog/remotefilelog.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-from __future__ import absolute_import
 
 import collections
 import os
@@ -16,7 +15,6 @@
     ancestor,
     error,
     mdiff,
-    pycompat,
     revlog,
 )
 from mercurial.utils import storageutil
@@ -29,7 +27,7 @@
 )
 
 
-class remotefilelognodemap(object):
+class remotefilelognodemap:
     def __init__(self, filename, store):
         self._filename = filename
         self._store = store
@@ -44,7 +42,7 @@
         return node
 
 
-class remotefilelog(object):
+class remotefilelog:
 
     _generaldelta = True
     _flagserrorclass = error.RevlogError
@@ -424,7 +422,7 @@
             return self.repo.nullid
 
         revmap, parentfunc = self._buildrevgraph(a, b)
-        nodemap = {v: k for (k, v) in pycompat.iteritems(revmap)}
+        nodemap = {v: k for (k, v) in revmap.items()}
 
         ancs = ancestor.ancestors(parentfunc, revmap[a], revmap[b])
         if ancs:
@@ -439,7 +437,7 @@
             return self.repo.nullid
 
         revmap, parentfunc = self._buildrevgraph(a, b)
-        nodemap = {v: k for (k, v) in pycompat.iteritems(revmap)}
+        nodemap = {v: k for (k, v) in revmap.items()}
 
         ancs = ancestor.commonancestorsheads(parentfunc, revmap[a], revmap[b])
         return map(nodemap.__getitem__, ancs)
@@ -455,7 +453,7 @@
         parentsmap = collections.defaultdict(list)
         allparents = set()
         for mapping in (amap, bmap):
-            for node, pdata in pycompat.iteritems(mapping):
+            for node, pdata in mapping.items():
                 parents = parentsmap[node]
                 p1, p2, linknode, copyfrom = pdata
                 # Don't follow renames (copyfrom).
--- a/hgext/remotefilelog/remotefilelogserver.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/remotefilelog/remotefilelogserver.py	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-from __future__ import absolute_import
 
 import errno
 import os
@@ -22,7 +21,6 @@
     error,
     extensions,
     match,
-    pycompat,
     scmutil,
     store,
     streamclone,
@@ -95,7 +93,7 @@
         b'x_rfl_getfile', b'file node', permission=b'pull'
     )(getfile)
 
-    class streamstate(object):
+    class streamstate:
         match = None
         shallowremote = False
         noflatmf = False
@@ -417,7 +415,7 @@
     cachepath = repo.vfs.join(b"remotefilelogcache")
     for head in heads:
         mf = repo[head].manifest()
-        for filename, filenode in pycompat.iteritems(mf):
+        for filename, filenode in mf.items():
             filecachepath = os.path.join(cachepath, filename, hex(filenode))
             neededfiles.add(filecachepath)
 
--- a/hgext/remotefilelog/repack.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/remotefilelog/repack.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import os
 import time
 
@@ -489,18 +487,18 @@
         if type(m) is dict:
             # m is a result of diff of two manifests and is a dictionary that
             # maps filename to ((newnode, newflag), (oldnode, oldflag)) tuple
-            for filename, diff in pycompat.iteritems(m):
+            for filename, diff in m.items():
                 if diff[0][0] is not None:
                     keepkeys.add(keyfn(filename, diff[0][0]))
         else:
             # m is a manifest object
-            for filename, filenode in pycompat.iteritems(m):
+            for filename, filenode in m.items():
                 keepkeys.add(keyfn(filename, filenode))
 
     return keepkeys
 
 
-class repacker(object):
+class repacker:
     """Class for orchestrating the repack of data and history information into a
     new format.
     """
@@ -596,7 +594,7 @@
         maxchainlen = ui.configint(b'packs', b'maxchainlen', 1000)
 
         byfile = {}
-        for entry in pycompat.itervalues(ledger.entries):
+        for entry in ledger.entries.values():
             if entry.datasource:
                 byfile.setdefault(entry.filename, {})[entry.node] = entry
 
@@ -604,7 +602,7 @@
         repackprogress = ui.makeprogress(
             _(b"repacking data"), unit=self.unit, total=len(byfile)
         )
-        for filename, entries in sorted(pycompat.iteritems(byfile)):
+        for filename, entries in sorted(byfile.items()):
             repackprogress.update(count)
 
             ancestors = {}
@@ -751,14 +749,14 @@
         ui = self.repo.ui
 
         byfile = {}
-        for entry in pycompat.itervalues(ledger.entries):
+        for entry in ledger.entries.values():
             if entry.historysource:
                 byfile.setdefault(entry.filename, {})[entry.node] = entry
 
         progress = ui.makeprogress(
             _(b"repacking history"), unit=self.unit, total=len(byfile)
         )
-        for filename, entries in sorted(pycompat.iteritems(byfile)):
+        for filename, entries in sorted(byfile.items()):
             ancestors = {}
             nodes = list(node for node in entries)
 
@@ -821,7 +819,7 @@
         return sortednodes
 
 
-class repackledger(object):
+class repackledger:
     """Storage for all the bookkeeping that happens during a repack. It contains
     the list of revisions being repacked, what happened to each revision, and
     which source store contained which revision originally (for later cleanup).
@@ -869,7 +867,7 @@
         self.created.add(value)
 
 
-class repackentry(object):
+class repackentry:
     """Simple class representing a single revision entry in the repackledger."""
 
     __slots__ = (
--- a/hgext/remotefilelog/shallowbundle.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/remotefilelog/shallowbundle.py	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-from __future__ import absolute_import
 
 from mercurial.i18n import _
 from mercurial.node import bin, hex
--- a/hgext/remotefilelog/shallowrepo.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/remotefilelog/shallowrepo.py	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-from __future__ import absolute_import
 
 import os
 
@@ -15,7 +14,6 @@
     error,
     localrepo,
     match,
-    pycompat,
     scmutil,
     sparse,
     util,
@@ -269,7 +267,7 @@
             mfrevlog = mfl.getstorage(b'')
             if base is not None:
                 mfdict = mfl[repo[base].manifestnode()].read()
-                skip = set(pycompat.iteritems(mfdict))
+                skip = set(mfdict.items())
             else:
                 skip = set()
 
@@ -299,7 +297,7 @@
                 else:
                     mfdict = mfl[mfnode].read()
 
-                diff = pycompat.iteritems(mfdict)
+                diff = mfdict.items()
                 if pats:
                     diff = (pf for pf in diff if m(pf[0]))
                 if sparsematch:
--- a/hgext/remotefilelog/shallowstore.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/remotefilelog/shallowstore.py	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-from __future__ import absolute_import
 
 
 def wrapstore(store):
--- a/hgext/remotefilelog/shallowutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/remotefilelog/shallowutil.py	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-from __future__ import absolute_import
 
 import collections
 import errno
@@ -103,7 +102,7 @@
     """
     result = collections.defaultdict(lambda: 0)
     for dict in dicts:
-        for k, v in pycompat.iteritems(dict):
+        for k, v in dict.items():
             result[k] += v
     return result
 
@@ -111,7 +110,7 @@
 def prefixkeys(dict, prefix):
     """Returns ``dict`` with ``prefix`` prepended to all its keys."""
     result = {}
-    for k, v in pycompat.iteritems(dict):
+    for k, v in dict.items():
         result[prefix + k] = v
     return result
 
@@ -160,7 +159,7 @@
     length limit is exceeded
     """
     metabuf = b''
-    for k, v in sorted(pycompat.iteritems((metadict or {}))):
+    for k, v in sorted((metadict or {}).items()):
         if len(k) != 1:
             raise error.ProgrammingError(b'packmeta: illegal key: %s' % k)
         if len(v) > 0xFFFE:
@@ -176,8 +175,8 @@
 
 
 _metaitemtypes = {
-    constants.METAKEYFLAG: (int, pycompat.long),
-    constants.METAKEYSIZE: (int, pycompat.long),
+    constants.METAKEYFLAG: (int, int),
+    constants.METAKEYSIZE: (int, int),
 }
 
 
@@ -188,7 +187,7 @@
     and METAKEYFLAG will be dropped if its value is 0.
     """
     newmeta = {}
-    for k, v in pycompat.iteritems(metadict or {}):
+    for k, v in (metadict or {}).items():
         expectedtype = _metaitemtypes.get(k, (bytes,))
         if not isinstance(v, expectedtype):
             raise error.ProgrammingError(b'packmeta: wrong type of key %s' % k)
@@ -209,7 +208,7 @@
     integers.
     """
     metadict = _parsepackmeta(metabuf)
-    for k, v in pycompat.iteritems(metadict):
+    for k, v in metadict.items():
         if k in _metaitemtypes and int in _metaitemtypes[k]:
             metadict[k] = bin2int(v)
     return metadict
--- a/hgext/remotefilelog/shallowverifier.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/remotefilelog/shallowverifier.py	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-from __future__ import absolute_import
 
 from mercurial.i18n import _
 from mercurial import verify
--- a/hgext/remotenames.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/remotenames.py	Wed May 04 18:17:44 2022 +0200
@@ -24,7 +24,6 @@
   namespace (default: 'default')
 """
 
-from __future__ import absolute_import
 
 from mercurial.i18n import _
 
@@ -171,13 +170,13 @@
         if not self.loaded:
             self._load()
 
-        for k, vtup in pycompat.iteritems(self.potentialentries):
+        for k, vtup in self.potentialentries.items():
             yield (k, [bin(vtup[0])])
 
     items = iteritems
 
 
-class remotenames(object):
+class remotenames:
     """
     This class encapsulates all the remotenames state. It also contains
     methods to access that state in convenient ways. Remotenames are lazy
@@ -208,7 +207,7 @@
         if not self._nodetobmarks:
             bmarktonodes = self.bmarktonodes()
             self._nodetobmarks = {}
-            for name, node in pycompat.iteritems(bmarktonodes):
+            for name, node in bmarktonodes.items():
                 self._nodetobmarks.setdefault(node[0], []).append(name)
         return self._nodetobmarks
 
@@ -219,7 +218,7 @@
         if not self._nodetobranch:
             branchtonodes = self.branchtonodes()
             self._nodetobranch = {}
-            for name, nodes in pycompat.iteritems(branchtonodes):
+            for name, nodes in branchtonodes.items():
                 for node in nodes:
                     self._nodetobranch.setdefault(node, []).append(name)
         return self._nodetobranch
@@ -229,7 +228,7 @@
             marktonodes = self.bmarktonodes()
             self._hoisttonodes = {}
             hoist += b'/'
-            for name, node in pycompat.iteritems(marktonodes):
+            for name, node in marktonodes.items():
                 if name.startswith(hoist):
                     name = name[len(hoist) :]
                     self._hoisttonodes[name] = node
@@ -240,7 +239,7 @@
             marktonodes = self.bmarktonodes()
             self._nodetohoists = {}
             hoist += b'/'
-            for name, node in pycompat.iteritems(marktonodes):
+            for name, node in marktonodes.items():
                 if name.startswith(hoist):
                     name = name[len(hoist) :]
                     self._nodetohoists.setdefault(node[0], []).append(name)
--- a/hgext/schemes.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/schemes.py	Wed May 04 18:17:44 2022 +0200
@@ -39,7 +39,6 @@
 You can override a predefined scheme by defining a new scheme with the
 same name.
 """
-from __future__ import absolute_import
 
 import os
 import re
@@ -68,7 +67,7 @@
 _partre = re.compile(br'{(\d+)\}')
 
 
-class ShortRepository(object):
+class ShortRepository:
     def __init__(self, url, scheme, templater):
         self.scheme = scheme
         self.templater = templater
--- a/hgext/share.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/share.py	Wed May 04 18:17:44 2022 +0200
@@ -65,7 +65,6 @@
     and there are no untracked files, delete that share and create a new share.
 '''
 
-from __future__ import absolute_import
 
 import errno
 from mercurial.i18n import _
--- a/hgext/show.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/show.py	Wed May 04 18:17:44 2022 +0200
@@ -25,7 +25,6 @@
    performed.
 """
 
-from __future__ import absolute_import
 
 from mercurial.i18n import _
 from mercurial.node import nullrev
--- a/hgext/sparse.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/sparse.py	Wed May 04 18:17:44 2022 +0200
@@ -71,7 +71,6 @@
   tools/tests/**
 """
 
-from __future__ import absolute_import
 
 from mercurial.i18n import _
 from mercurial.pycompat import setattr
--- a/hgext/split.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/split.py	Wed May 04 18:17:44 2022 +0200
@@ -7,7 +7,6 @@
 # GNU General Public License version 2 or any later version.
 """command to split a changeset into smaller ones (EXPERIMENTAL)"""
 
-from __future__ import absolute_import
 
 from mercurial.i18n import _
 
--- a/hgext/sqlitestore.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/sqlitestore.py	Wed May 04 18:17:44 2022 +0200
@@ -43,7 +43,6 @@
 #     --extra-config-opt extensions.sqlitestore= \
 #     --extra-config-opt storage.new-repo-backend=sqlite
 
-from __future__ import absolute_import
 
 import sqlite3
 import struct
@@ -265,7 +264,7 @@
 
 
 @attr.s
-class revisionentry(object):
+class revisionentry:
     rid = attr.ib()
     rev = attr.ib()
     node = attr.ib()
@@ -279,7 +278,7 @@
 
 @interfaceutil.implementer(repository.irevisiondelta)
 @attr.s(slots=True)
-class sqliterevisiondelta(object):
+class sqliterevisiondelta:
     node = attr.ib()
     p1node = attr.ib()
     p2node = attr.ib()
@@ -295,14 +294,14 @@
 
 @interfaceutil.implementer(repository.iverifyproblem)
 @attr.s(frozen=True)
-class sqliteproblem(object):
+class sqliteproblem:
     warning = attr.ib(default=None)
     error = attr.ib(default=None)
     node = attr.ib(default=None)
 
 
 @interfaceutil.implementer(repository.ifilestorage)
-class sqlitefilestore(object):
+class sqlitefilestore:
     """Implements storage for an individual tracked path."""
 
     def __init__(self, db, path, compression):
@@ -1250,7 +1249,7 @@
 
 
 @interfaceutil.implementer(repository.ilocalrepositoryfilestorage)
-class sqlitefilestorage(object):
+class sqlitefilestorage:
     """Repository file storage backed by SQLite."""
 
     def file(self, path):
--- a/hgext/strip.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/strip.py	Wed May 04 18:17:44 2022 +0200
@@ -6,7 +6,6 @@
 This extension allows you to strip changesets and all their descendants from the
 repository. See the command help for details.
 """
-from __future__ import absolute_import
 
 from mercurial import commands
 
--- a/hgext/transplant.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/transplant.py	Wed May 04 18:17:44 2022 +0200
@@ -13,7 +13,6 @@
 Transplanted patches are recorded in .hg/transplant/transplants, as a
 map from a changeset hash to its hash in the source repository.
 '''
-from __future__ import absolute_import
 
 import os
 
@@ -76,13 +75,13 @@
 )
 
 
-class transplantentry(object):
+class transplantentry:
     def __init__(self, lnode, rnode):
         self.lnode = lnode
         self.rnode = rnode
 
 
-class transplants(object):
+class transplants:
     def __init__(self, path=None, transplantfile=None, opener=None):
         self.path = path
         self.transplantfile = transplantfile
@@ -107,7 +106,7 @@
             if not os.path.isdir(self.path):
                 os.mkdir(self.path)
             fp = self.opener(self.transplantfile, b'w')
-            for list in pycompat.itervalues(self.transplants):
+            for list in self.transplants.values():
                 for t in list:
                     l, r = map(hex, (t.lnode, t.rnode))
                     fp.write(l + b':' + r + b'\n')
@@ -129,7 +128,7 @@
             self.dirty = True
 
 
-class transplanter(object):
+class transplanter:
     def __init__(self, ui, repo, opts):
         self.ui = ui
         self.repo = repo
--- a/hgext/uncommit.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/uncommit.py	Wed May 04 18:17:44 2022 +0200
@@ -17,7 +17,6 @@
 added and removed in the working directory.
 """
 
-from __future__ import absolute_import
 
 from mercurial.i18n import _
 
@@ -81,9 +80,7 @@
     files = initialfiles - exclude
     # Filter copies
     copied = copiesmod.pathcopies(base, ctx)
-    copied = {
-        dst: src for dst, src in pycompat.iteritems(copied) if dst in files
-    }
+    copied = {dst: src for dst, src in copied.items() if dst in files}
 
     def filectxfn(repo, memctx, path, contentctx=ctx, redirect=()):
         if path not in contentctx:
--- a/hgext/win32mbcs.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/win32mbcs.py	Wed May 04 18:17:44 2022 +0200
@@ -44,7 +44,6 @@
 
 It is useful for the users who want to commit with UTF-8 log message.
 '''
-from __future__ import absolute_import
 
 import os
 import sys
@@ -95,7 +94,7 @@
 
 
 def encode(arg):
-    if isinstance(arg, pycompat.unicode):
+    if isinstance(arg, str):
         return arg.encode(_encoding)
     elif isinstance(arg, tuple):
         return tuple(map(encode, arg))
@@ -136,7 +135,7 @@
 
 
 def wrapper(func, args, kwds):
-    return basewrapper(func, pycompat.unicode, encode, decode, args, kwds)
+    return basewrapper(func, str, encode, decode, args, kwds)
 
 
 def reversewrapper(func, args, kwds):
--- a/hgext/win32text.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/win32text.py	Wed May 04 18:17:44 2022 +0200
@@ -41,7 +41,6 @@
   # or pretxnchangegroup.cr = python:hgext.win32text.forbidcr
 '''
 
-from __future__ import absolute_import
 
 import re
 from mercurial.i18n import _
@@ -213,7 +212,7 @@
 def reposetup(ui, repo):
     if not repo.local():
         return
-    for name, fn in pycompat.iteritems(_filters):
+    for name, fn in _filters.items():
         repo.adddatafilter(name, fn)
 
 
--- a/hgext/zeroconf/Zeroconf.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/zeroconf/Zeroconf.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function
-
 """ Multicast DNS Service Discovery for Python, v0.12
     Copyright (C) 2003, Paul Scott-Murphy
 
@@ -233,7 +231,7 @@
 # implementation classes
 
 
-class DNSEntry(object):
+class DNSEntry:
     """A DNS entry"""
 
     def __init__(self, name, type, clazz):
@@ -508,7 +506,7 @@
         return self.toString(b"%s:%s" % (self.server, self.port))
 
 
-class DNSIncoming(object):
+class DNSIncoming:
     """Object representation of an incoming DNS packet"""
 
     def __init__(self, data):
@@ -704,7 +702,7 @@
         return result
 
 
-class DNSOutgoing(object):
+class DNSOutgoing:
     """Object representation of an outgoing packet"""
 
     def __init__(self, flags, multicast=1):
@@ -866,7 +864,7 @@
         return b''.join(self.data)
 
 
-class DNSCache(object):
+class DNSCache:
     """A cache of DNS entries"""
 
     def __init__(self):
@@ -984,7 +982,7 @@
         self.condition.release()
 
 
-class Listener(object):
+class Listener:
     """A Listener is used by this module to listen on the multicast
     group to which DNS messages are sent, allowing the implementation
     to cache information as it arrives.
@@ -1129,7 +1127,7 @@
                 event(self.zeroconf)
 
 
-class ServiceInfo(object):
+class ServiceInfo:
     """Service information"""
 
     def __init__(
@@ -1388,7 +1386,7 @@
         return result
 
 
-class Zeroconf(object):
+class Zeroconf:
     """Implementation of Zeroconf Multicast DNS Service Discovery
 
     Supports registration, unregistration, queries and browsing.
@@ -1461,7 +1459,7 @@
     def notifyAll(self):
         """Notifies all waiting threads"""
         self.condition.acquire()
-        self.condition.notifyAll()
+        self.condition.notify_all()
         self.condition.release()
 
     def getServiceInfo(self, type, name, timeout=3000):
--- a/hgext/zeroconf/__init__.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext/zeroconf/__init__.py	Wed May 04 18:17:44 2022 +0200
@@ -22,7 +22,6 @@
   $ hg paths
   zc-test = http://example.com:8000/test
 '''
-from __future__ import absolute_import
 
 import os
 import socket
@@ -159,7 +158,7 @@
 # listen
 
 
-class listener(object):
+class listener:
     def __init__(self):
         self.found = {}
 
--- a/hgext3rd/__init__.py	Wed May 04 18:00:01 2022 +0200
+++ b/hgext3rd/__init__.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,4 @@
 # name space package to host third party extensions
-from __future__ import absolute_import
 import pkgutil
 
 __path__ = pkgutil.extend_path(__path__, __name__)
--- a/i18n/check-translation.py	Wed May 04 18:00:01 2022 +0200
+++ b/i18n/check-translation.py	Wed May 04 18:17:44 2022 +0200
@@ -1,7 +1,6 @@
 #!/usr/bin/env python3
 #
 # check-translation.py - check Mercurial specific translation problems
-from __future__ import absolute_import
 
 import re
 
--- a/i18n/hggettext	Wed May 04 18:00:01 2022 +0200
+++ b/i18n/hggettext	Wed May 04 18:17:44 2022 +0200
@@ -20,7 +20,6 @@
 join the message cataloges to get the final catalog.
 """
 
-from __future__ import absolute_import, print_function
 
 import inspect
 import os
--- a/i18n/polib.py	Wed May 04 18:00:01 2022 +0200
+++ b/i18n/polib.py	Wed May 04 18:17:44 2022 +0200
@@ -13,7 +13,6 @@
 :func:`~polib.mofile` convenience functions.
 """
 
-from __future__ import absolute_import
 
 __author__ = 'David Jean Louis <izimobil@gmail.com>'
 __version__ = '1.0.7'
@@ -43,7 +42,7 @@
 except ImportError:
     # replacement of io.open() for python < 2.6
     # we use codecs instead
-    class io(object):
+    class io:
         @staticmethod
         def open(fpath, mode='r', encoding=None):
             return codecs.open(fpath, mode, encoding)
@@ -817,7 +816,7 @@
 # class _BaseEntry {{{
 
 
-class _BaseEntry(object):
+class _BaseEntry:
     """
     Base class for :class:`~polib.POEntry` and :class:`~polib.MOEntry` classes.
     This class should **not** be instanciated directly.
@@ -1228,7 +1227,7 @@
 # class _POFileParser {{{
 
 
-class _POFileParser(object):
+class _POFileParser:
     """
     A finite state machine to parse efficiently and correctly po
     file format.
@@ -1707,7 +1706,7 @@
 # class _MOFileParser {{{
 
 
-class _MOFileParser(object):
+class _MOFileParser:
     """
     A class to parse binary mo files.
     """
--- a/i18n/posplit	Wed May 04 18:00:01 2022 +0200
+++ b/i18n/posplit	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # license: MIT/X11/Expat
 #
 
-from __future__ import absolute_import, print_function
 
 import polib
 import re
--- a/mercurial/__init__.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/__init__.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 # Allow 'from mercurial import demandimport' to keep working.
 import hgdemandimport
--- a/mercurial/ancestor.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/ancestor.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import heapq
 
@@ -147,7 +146,7 @@
     return deepest(gca)
 
 
-class incrementalmissingancestors(object):
+class incrementalmissingancestors:
     """persistent state used to calculate missing ancestors incrementally
 
     Although similar in spirit to lazyancestors below, this is a separate class
@@ -317,7 +316,7 @@
             see(p2)
 
 
-class lazyancestors(object):
+class lazyancestors:
     def __init__(self, pfunc, revs, stoprev=0, inclusive=False):
         """Create a new object generating ancestors for the given revs. Does
         not generate revs lower than stoprev.
--- a/mercurial/archival.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/archival.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import gzip
 import os
@@ -76,7 +75,7 @@
 
 
 def guesskind(dest):
-    for kind, extensions in pycompat.iteritems(exts):
+    for kind, extensions in exts.items():
         if any(dest.endswith(ext) for ext in extensions):
             return kind
     return None
@@ -133,43 +132,10 @@
     return out.getvalue()
 
 
-class tarit(object):
+class tarit:
     """write archive to tar file or stream.  can write uncompressed,
     or compress with gzip or bzip2."""
 
-    if pycompat.ispy3:
-        GzipFileWithTime = gzip.GzipFile  # camelcase-required
-    else:
-
-        class GzipFileWithTime(gzip.GzipFile):
-            def __init__(self, *args, **kw):
-                timestamp = None
-                if 'mtime' in kw:
-                    timestamp = kw.pop('mtime')
-                if timestamp is None:
-                    self.timestamp = time.time()
-                else:
-                    self.timestamp = timestamp
-                gzip.GzipFile.__init__(self, *args, **kw)
-
-            def _write_gzip_header(self):
-                self.fileobj.write(b'\037\213')  # magic header
-                self.fileobj.write(b'\010')  # compression method
-                fname = self.name
-                if fname and fname.endswith(b'.gz'):
-                    fname = fname[:-3]
-                flags = 0
-                if fname:
-                    flags = gzip.FNAME  # pytype: disable=module-attr
-                self.fileobj.write(pycompat.bytechr(flags))
-                gzip.write32u(  # pytype: disable=module-attr
-                    self.fileobj, int(self.timestamp)
-                )
-                self.fileobj.write(b'\002')
-                self.fileobj.write(b'\377')
-                if fname:
-                    self.fileobj.write(fname + b'\000')
-
     def __init__(self, dest, mtime, kind=b''):
         self.mtime = mtime
         self.fileobj = None
@@ -179,7 +145,7 @@
                 mode = mode[0:1]
                 if not fileobj:
                     fileobj = open(name, mode + b'b')
-                gzfileobj = self.GzipFileWithTime(
+                gzfileobj = gzip.GzipFile(
                     name,
                     pycompat.sysstr(mode + b'b'),
                     zlib.Z_BEST_COMPRESSION,
@@ -227,7 +193,7 @@
             self.fileobj.close()
 
 
-class zipit(object):
+class zipit:
     """write archive to zip file or stream.  can write uncompressed,
     or compressed with deflate."""
 
@@ -274,7 +240,7 @@
         self.z.close()
 
 
-class fileit(object):
+class fileit:
     '''write archive as files in directory.'''
 
     def __init__(self, name, mtime):
@@ -339,9 +305,6 @@
     subrepos tells whether to include subrepos.
     """
 
-    if kind == b'txz' and not pycompat.ispy3:
-        raise error.Abort(_(b'xz compression is only available in Python 3'))
-
     if kind == b'files':
         if prefix:
             raise error.Abort(_(b'cannot give prefix when archiving to files'))
--- a/mercurial/bookmarks.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/bookmarks.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import errno
 import struct
@@ -28,6 +27,7 @@
     util,
 )
 from .utils import (
+    stringutil,
     urlutil,
 )
 
@@ -59,7 +59,7 @@
     return fp
 
 
-class bmstore(object):
+class bmstore:
     r"""Storage for bookmarks.
 
     This object should do all bookmark-related reads and writes, so
@@ -138,7 +138,7 @@
         return iter(self._refmap)
 
     def iteritems(self):
-        return pycompat.iteritems(self._refmap)
+        return self._refmap.items()
 
     def items(self):
         return self._refmap.items()
@@ -251,7 +251,7 @@
         self._aclean = True
 
     def _write(self, fp):
-        for name, node in sorted(pycompat.iteritems(self._refmap)):
+        for name, node in sorted(self._refmap.items()):
             fp.write(b"%s %s\n" % (hex(node), encoding.fromlocal(name)))
         self._clean = True
         self._repo.invalidatevolatilesets()
@@ -343,7 +343,7 @@
     # No readline() in osutil.posixfile, reading everything is
     # cheap.
     content = repo.vfs.tryread(b'bookmarks.current')
-    mark = encoding.tolocal((content.splitlines() or [b''])[0])
+    mark = encoding.tolocal(stringutil.firstline(content))
     if mark == b'' or mark not in marks:
         mark = None
     return mark
@@ -419,7 +419,7 @@
         )
     name = repo._activebookmark.split(b'@', 1)[0]
     heads = []
-    for mark, n in pycompat.iteritems(repo._bookmarks):
+    for mark, n in repo._bookmarks.items():
         if mark.split(b'@', 1)[0] == name:
             heads.append(n)
     return heads
@@ -477,7 +477,7 @@
     marks = getattr(repo, '_bookmarks', {})
 
     hasnode = repo.changelog.hasnode
-    for k, v in pycompat.iteritems(marks):
+    for k, v in marks.items():
         # don't expose local divergent bookmarks
         if hasnode(v) and not isdivergent(k):
             yield k, v
@@ -688,7 +688,7 @@
     remotemarks"""
     changed = []
     localmarks = repo._bookmarks
-    for (b, id) in pycompat.iteritems(remotemarks):
+    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:
@@ -1075,7 +1075,7 @@
     hexfn = fm.hexfunc
     if len(bmarks) == 0 and fm.isplain():
         ui.status(_(b"no bookmarks set\n"))
-    for bmark, (n, prefix, label) in sorted(pycompat.iteritems(bmarks)):
+    for bmark, (n, prefix, label) in sorted(bmarks.items()):
         fm.startitem()
         fm.context(repo=repo)
         if not ui.quiet:
--- a/mercurial/branchmap.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/branchmap.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import struct
 
@@ -63,7 +62,7 @@
 unpack_from = struct.unpack_from
 
 
-class BranchMapCache(object):
+class BranchMapCache:
     """mapping of filtered views of repo with their branchcache"""
 
     def __init__(self):
@@ -120,7 +119,7 @@
         clbranchinfo = cl.branchinfo
         rbheads = []
         closed = set()
-        for bheads in pycompat.itervalues(remotebranchmap):
+        for bheads in remotebranchmap.values():
             rbheads += bheads
             for h in bheads:
                 r = clrev(h)
@@ -170,7 +169,7 @@
         return b'branch cache'
 
 
-class branchcache(object):
+class branchcache:
     """A dict like object that hold branches heads cache.
 
     This cache is used to avoid costly computations to determine all the
@@ -271,7 +270,7 @@
         return key in self._entries
 
     def iteritems(self):
-        for k, v in pycompat.iteritems(self._entries):
+        for k, v in self._entries.items():
             self._verifybranch(k)
             yield k, v
 
@@ -401,13 +400,13 @@
         return heads
 
     def iterbranches(self):
-        for bn, heads in pycompat.iteritems(self):
+        for bn, heads in self.items():
             yield (bn, heads) + self._branchtip(heads)
 
     def iterheads(self):
         """returns all the heads"""
         self._verifyall()
-        return pycompat.itervalues(self._entries)
+        return self._entries.values()
 
     def copy(self):
         """return an deep copy of the branchcache object"""
@@ -435,7 +434,7 @@
                 cachekey.append(hex(self.filteredhash))
             f.write(b" ".join(cachekey) + b'\n')
             nodecount = 0
-            for label, nodes in sorted(pycompat.iteritems(self._entries)):
+            for label, nodes in sorted(self._entries.items()):
                 label = encoding.fromlocal(label)
                 for node in nodes:
                     nodecount += 1
@@ -491,7 +490,7 @@
         # Faster than using ctx.obsolete()
         obsrevs = obsolete.getrevs(repo, b'obsolete')
 
-        for branch, newheadrevs in pycompat.iteritems(newbranches):
+        for branch, newheadrevs in newbranches.items():
             # For every branch, compute the new branchheads.
             # A branchhead is a revision such that no descendant is on
             # the same branch.
@@ -632,7 +631,7 @@
 _rbccloseflag = 0x80000000
 
 
-class revbranchcache(object):
+class revbranchcache:
     """Persistent cache, mapping from revision number to branch name and close.
     This is a low level cache, independent of filtering.
 
--- a/mercurial/bundle2.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/bundle2.py	Wed May 04 18:17:44 2022 +0200
@@ -145,7 +145,6 @@
 preserve.
 """
 
-from __future__ import absolute_import, division
 
 import collections
 import errno
@@ -252,7 +251,7 @@
     return _decorator
 
 
-class unbundlerecords(object):
+class unbundlerecords:
     """keep record of what happens during and unbundle
 
     New records are added using `records.add('cat', obj)`. Where 'cat' is a
@@ -300,7 +299,7 @@
     __bool__ = __nonzero__
 
 
-class bundleoperation(object):
+class bundleoperation:
     """an object that represents a single bundling process
 
     Its purpose is to carry unbundle-related objects and states.
@@ -380,7 +379,7 @@
         return op
 
 
-class partiterator(object):
+class partiterator:
     def __init__(self, repo, op, unbundler):
         self.repo = repo
         self.op = op
@@ -627,7 +626,7 @@
 bundlepriority = [b'HG10GZ', b'HG10BZ', b'HG10UN']
 
 
-class bundle20(object):
+class bundle20:
     """represent an outgoing bundle2 container
 
     Use the `addparam` method to add stream level parameter. and `newpart` to
@@ -751,7 +750,7 @@
         return salvaged
 
 
-class unpackermixin(object):
+class unpackermixin:
     """A mixin to extract bytes and struct data from a stream"""
 
     def __init__(self, fp):
@@ -984,7 +983,7 @@
         unbundler._compressed = True
 
 
-class bundlepart(object):
+class bundlepart:
     """A bundle2 part contains application level payload
 
     The part `type` is used to route the part to the application level
@@ -1274,7 +1273,7 @@
         )
 
 
-class interruptoperation(object):
+class interruptoperation:
     """A limited operation to be use by part handler during interruption
 
     It only have access to an ui object.
@@ -2240,7 +2239,7 @@
         b'remote repository changed while pushing - please try again '
         b'(%s is %s expected %s)'
     )
-    for expectedphase, nodes in pycompat.iteritems(phasetonodes):
+    for expectedphase, nodes in phasetonodes.items():
         for n in nodes:
             actualphase = phasecache.phase(unfi, cl.rev(n))
             if actualphase != expectedphase:
--- a/mercurial/bundlecaches.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/bundlecaches.py	Wed May 04 18:17:44 2022 +0200
@@ -21,7 +21,7 @@
 
 
 @attr.s
-class bundlespec(object):
+class bundlespec:
     compression = attr.ib()
     wirecompression = attr.ib()
     version = attr.ib()
@@ -343,7 +343,7 @@
     return newentries
 
 
-class clonebundleentry(object):
+class clonebundleentry:
     """Represents an item in a clone bundles manifest.
 
     This rich class is needed to support sorting since sorted() in Python 3
--- a/mercurial/bundlerepo.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/bundlerepo.py	Wed May 04 18:17:44 2022 +0200
@@ -11,7 +11,6 @@
 were part of the actual repository.
 """
 
-from __future__ import absolute_import
 
 import os
 import shutil
@@ -271,7 +270,7 @@
     return filespos
 
 
-class bundlerepository(object):
+class bundlerepository:
     """A repository instance that is a union of a local repo and a bundle.
 
     Instances represent a read-only repository composed of a local repository
@@ -551,7 +550,7 @@
     return repo
 
 
-class bundletransactionmanager(object):
+class bundletransactionmanager:
     def transaction(self):
         return None
 
--- a/mercurial/cacheutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/cacheutil.py	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-from __future__ import absolute_import
 
 from . import repoview
 
--- a/mercurial/cext/base85.c	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/cext/base85.c	Wed May 04 18:17:44 2022 +0200
@@ -38,7 +38,7 @@
 	unsigned int acc, val, ch;
 	int pad = 0;
 
-	if (!PyArg_ParseTuple(args, PY23("s#|i", "y#|i"), &text, &len, &pad)) {
+	if (!PyArg_ParseTuple(args, "y#|i", &text, &len, &pad)) {
 		return NULL;
 	}
 
@@ -90,7 +90,7 @@
 	int c;
 	unsigned int acc;
 
-	if (!PyArg_ParseTuple(args, PY23("s#", "y#"), &text, &len)) {
+	if (!PyArg_ParseTuple(args, "y#", &text, &len)) {
 		return NULL;
 	}
 
@@ -177,7 +177,6 @@
 
 static const int version = 1;
 
-#ifdef IS_PY3K
 static struct PyModuleDef base85_module = {
     PyModuleDef_HEAD_INIT, "base85", base85_doc, -1, methods,
 };
@@ -191,13 +190,3 @@
 	PyModule_AddIntConstant(m, "version", version);
 	return m;
 }
-#else
-PyMODINIT_FUNC initbase85(void)
-{
-	PyObject *m;
-	m = Py_InitModule3("base85", methods, base85_doc);
-
-	b85prep();
-	PyModule_AddIntConstant(m, "version", version);
-}
-#endif
--- a/mercurial/cext/bdiff.c	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/cext/bdiff.c	Wed May 04 18:17:44 2022 +0200
@@ -76,8 +76,7 @@
 
 	l.next = NULL;
 
-	if (!PyArg_ParseTuple(args, PY23("s*s*:bdiff", "y*y*:bdiff"), &ba,
-	                      &bb)) {
+	if (!PyArg_ParseTuple(args, "y*y*:bdiff", &ba, &bb)) {
 		return NULL;
 	}
 
@@ -233,7 +232,7 @@
 	Py_ssize_t nelts = 0, size, i, start = 0;
 	PyObject *result = NULL;
 
-	if (!PyArg_ParseTuple(args, PY23("s#", "y#"), &text, &size)) {
+	if (!PyArg_ParseTuple(args, "y#", &text, &size)) {
 		goto abort;
 	}
 	if (!size) {
@@ -299,8 +298,7 @@
 	    NULL, /* priv */
 	};
 
-	if (!PyArg_ParseTuple(args, PY23("s#s#", "y#y#"), &a.ptr, &la, &b.ptr,
-	                      &lb)) {
+	if (!PyArg_ParseTuple(args, "y#y#", &a.ptr, &la, &b.ptr, &lb)) {
 		return NULL;
 	}
 
@@ -337,7 +335,6 @@
 
 static const int version = 3;
 
-#ifdef IS_PY3K
 static struct PyModuleDef bdiff_module = {
     PyModuleDef_HEAD_INIT, "bdiff", mdiff_doc, -1, methods,
 };
@@ -349,11 +346,3 @@
 	PyModule_AddIntConstant(m, "version", version);
 	return m;
 }
-#else
-PyMODINIT_FUNC initbdiff(void)
-{
-	PyObject *m;
-	m = Py_InitModule3("bdiff", methods, mdiff_doc);
-	PyModule_AddIntConstant(m, "version", version);
-}
-#endif
--- a/mercurial/cext/charencode.c	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/cext/charencode.c	Wed May 04 18:17:44 2022 +0200
@@ -15,14 +15,6 @@
 #include "compat.h"
 #include "util.h"
 
-#ifdef IS_PY3K
-/* The mapping of Python types is meant to be temporary to get Python
- * 3 to compile. We should remove this once Python 3 support is fully
- * supported and proper types are used in the extensions themselves. */
-#define PyInt_Type PyLong_Type
-#define PyInt_AS_LONG PyLong_AS_LONG
-#endif
-
 /* clang-format off */
 static const char lowertable[128] = {
 	'\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07',
@@ -133,8 +125,7 @@
 {
 	const char *buf;
 	Py_ssize_t i, len;
-	if (!PyArg_ParseTuple(args, PY23("s#:isasciistr", "y#:isasciistr"),
-	                      &buf, &len)) {
+	if (!PyArg_ParseTuple(args, "y#:isasciistr", &buf, &len)) {
 		return NULL;
 	}
 	i = 0;
@@ -228,12 +219,12 @@
 	const char *table;
 
 	if (!PyArg_ParseTuple(args, "O!O!O!:make_file_foldmap", &PyDict_Type,
-	                      &dmap, &PyInt_Type, &spec_obj, &PyFunction_Type,
+	                      &dmap, &PyLong_Type, &spec_obj, &PyFunction_Type,
 	                      &normcase_fallback)) {
 		goto quit;
 	}
 
-	spec = (int)PyInt_AS_LONG(spec_obj);
+	spec = (int)PyLong_AS_LONG(spec_obj);
 	switch (spec) {
 	case NORMCASE_LOWER:
 		table = lowertable;
--- a/mercurial/cext/dirs.c	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/cext/dirs.c	Wed May 04 18:17:44 2022 +0200
@@ -13,11 +13,7 @@
 
 #include "util.h"
 
-#ifdef IS_PY3K
 #define PYLONG_VALUE(o) ((PyLongObject *)o)->ob_digit[0]
-#else
-#define PYLONG_VALUE(o) PyInt_AS_LONG(o)
-#endif
 
 /*
  * This is a multiset of directory names, built from the files that
@@ -100,11 +96,7 @@
 		}
 
 		/* Force Python to not reuse a small shared int. */
-#ifdef IS_PY3K
 		val = PyLong_FromLong(0x1eadbeef);
-#else
-		val = PyInt_FromLong(0x1eadbeef);
-#endif
 
 		if (val == NULL)
 			goto bail;
--- a/mercurial/cext/manifest.c	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/cext/manifest.c	Wed May 04 18:17:44 2022 +0200
@@ -317,12 +317,7 @@
 	return ret;
 }
 
-#ifdef IS_PY3K
 #define LAZYMANIFESTENTRIESITERATOR_TPFLAGS Py_TPFLAGS_DEFAULT
-#else
-#define LAZYMANIFESTENTRIESITERATOR_TPFLAGS Py_TPFLAGS_DEFAULT \
-	| Py_TPFLAGS_HAVE_ITER
-#endif
 
 static PyTypeObject lazymanifestEntriesIterator = {
 	PyVarObject_HEAD_INIT(NULL, 0) /* header */
@@ -365,12 +360,7 @@
 	return PyBytes_FromStringAndSize(l->start, pl);
 }
 
-#ifdef IS_PY3K
 #define LAZYMANIFESTKEYSITERATOR_TPFLAGS Py_TPFLAGS_DEFAULT
-#else
-#define LAZYMANIFESTKEYSITERATOR_TPFLAGS Py_TPFLAGS_DEFAULT \
-	| Py_TPFLAGS_HAVE_ITER
-#endif
 
 static PyTypeObject lazymanifestKeysIterator = {
 	PyVarObject_HEAD_INIT(NULL, 0) /* header */
@@ -790,7 +780,7 @@
 	Py_INCREF(copy->pydata);
 	for (i = 0; i < self->numlines; i++) {
 		PyObject *arglist = NULL, *result = NULL;
-		arglist = Py_BuildValue(PY23("(s)", "(y)"),
+		arglist = Py_BuildValue("(y)",
 					self->lines[i].start);
 		if (!arglist) {
 			goto bail;
@@ -955,11 +945,7 @@
 	{NULL},
 };
 
-#ifdef IS_PY3K
 #define LAZYMANIFEST_TPFLAGS Py_TPFLAGS_DEFAULT
-#else
-#define LAZYMANIFEST_TPFLAGS Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_SEQUENCE_IN
-#endif
 
 static PyTypeObject lazymanifestType = {
 	PyVarObject_HEAD_INIT(NULL, 0) /* header */
--- a/mercurial/cext/mpatch.c	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/cext/mpatch.c	Wed May 04 18:17:44 2022 +0200
@@ -144,8 +144,7 @@
 	Py_ssize_t patchlen;
 	char *bin;
 
-	if (!PyArg_ParseTuple(args, PY23("ls#", "ly#"), &orig, &bin,
-	                      &patchlen)) {
+	if (!PyArg_ParseTuple(args, "ly#", &orig, &bin, &patchlen)) {
 		return NULL;
 	}
 
@@ -182,7 +181,6 @@
 
 static const int version = 1;
 
-#ifdef IS_PY3K
 static struct PyModuleDef mpatch_module = {
     PyModuleDef_HEAD_INIT, "mpatch", mpatch_doc, -1, methods,
 };
@@ -203,13 +201,3 @@
 
 	return m;
 }
-#else
-PyMODINIT_FUNC initmpatch(void)
-{
-	PyObject *m;
-	m = Py_InitModule3("mpatch", methods, mpatch_doc);
-	mpatch_Error =
-	    PyErr_NewException("mercurial.cext.mpatch.mpatchError", NULL, NULL);
-	PyModule_AddIntConstant(m, "version", version);
-}
-#endif
--- a/mercurial/cext/osutil.c	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/cext/osutil.c	Wed May 04 18:17:44 2022 +0200
@@ -73,19 +73,11 @@
 };
 #endif
 
-#ifdef IS_PY3K
 #define listdir_slot(name) \
 	static PyObject *listdir_stat_##name(PyObject *self, void *x) \
 	{ \
 		return PyLong_FromLong(((struct listdir_stat *)self)->st.name); \
 	}
-#else
-#define listdir_slot(name) \
-	static PyObject *listdir_stat_##name(PyObject *self, void *x) \
-	{ \
-		return PyInt_FromLong(((struct listdir_stat *)self)->st.name); \
-	}
-#endif
 
 listdir_slot(st_dev)
 listdir_slot(st_mode)
@@ -206,7 +198,7 @@
 		? _S_IFDIR : _S_IFREG;
 
 	if (!wantstat)
-		return Py_BuildValue(PY23("si", "yi"), fd->cFileName, kind);
+		return Py_BuildValue("yi", fd->cFileName, kind);
 
 	py_st = PyObject_CallObject((PyObject *)&listdir_stat_type, NULL);
 	if (!py_st)
@@ -224,7 +216,7 @@
 	if (kind == _S_IFREG)
 		stp->st_size = ((__int64)fd->nFileSizeHigh << 32)
 				+ fd->nFileSizeLow;
-	return Py_BuildValue(PY23("siN", "yiN"), fd->cFileName,
+	return Py_BuildValue("yiN", fd->cFileName,
 		kind, py_st);
 }
 
@@ -412,10 +404,10 @@
 			PyObject *stat = makestat(&st);
 			if (!stat)
 				goto error;
-			elem = Py_BuildValue(PY23("siN", "yiN"), ent->d_name,
+			elem = Py_BuildValue("yiN", ent->d_name,
 					     kind, stat);
 		} else
-			elem = Py_BuildValue(PY23("si", "yi"), ent->d_name,
+			elem = Py_BuildValue("yi", ent->d_name,
 					     kind);
 		if (!elem)
 			goto error;
@@ -593,10 +585,10 @@
 				stat = makestat(&st);
 				if (!stat)
 					goto error;
-				elem = Py_BuildValue(PY23("siN", "yiN"),
+				elem = Py_BuildValue("yiN",
 						     filename, kind, stat);
 			} else
-				elem = Py_BuildValue(PY23("si", "yi"),
+				elem = Py_BuildValue("yi",
 						     filename, kind);
 			if (!elem)
 				goto error;
@@ -767,10 +759,6 @@
 #if defined(HAVE_SETPROCTITLE)
 /* setproctitle is the first choice - available in FreeBSD */
 #define SETPROCNAME_USE_SETPROCTITLE
-#elif (defined(__linux__) || defined(__APPLE__)) && PY_MAJOR_VERSION == 2
-/* rewrite the argv buffer in place - works in Linux and OS X. Py_GetArgcArgv
- * in Python 3 returns the copied wchar_t **argv, thus unsupported. */
-#define SETPROCNAME_USE_ARGVREWRITE
 #else
 #define SETPROCNAME_USE_NONE
 #endif
@@ -780,49 +768,11 @@
 static PyObject *setprocname(PyObject *self, PyObject *args)
 {
 	const char *name = NULL;
-	if (!PyArg_ParseTuple(args, PY23("s", "y"), &name))
+	if (!PyArg_ParseTuple(args, "y", &name))
 		return NULL;
 
 #if defined(SETPROCNAME_USE_SETPROCTITLE)
 	setproctitle("%s", name);
-#elif defined(SETPROCNAME_USE_ARGVREWRITE)
-	{
-		static char *argvstart = NULL;
-		static size_t argvsize = 0;
-		if (argvstart == NULL) {
-			int argc = 0, i;
-			char **argv = NULL;
-			char *argvend;
-			extern void Py_GetArgcArgv(int *argc, char ***argv);
-			Py_GetArgcArgv(&argc, &argv);
-			/* Py_GetArgcArgv may not do much if a custom python
-			 * launcher is used that doesn't record the information
-			 * it needs. Let's handle this gracefully instead of
-			 * segfaulting. */
-			if (argv != NULL)
-				argvend = argvstart = argv[0];
-			else
-				argvend = argvstart = NULL;
-
-			/* Check the memory we can use. Typically, argv[i] and
-			 * argv[i + 1] are continuous. */
-			for (i = 0; i < argc; ++i) {
-				size_t len;
-				if (argv[i] > argvend || argv[i] < argvstart)
-					break; /* not continuous */
-				len = strlen(argv[i]);
-				argvend = argv[i] + len + 1 /* '\0' */;
-			}
-			if (argvend > argvstart) /* sanity check */
-				argvsize = argvend - argvstart;
-		}
-
-		if (argvstart && argvsize > 1) {
-			int n = snprintf(argvstart, argvsize, "%s", name);
-			if (n >= 0 && (size_t)n < argvsize)
-				memset(argvstart + n, 0, argvsize - n);
-		}
-	}
 #endif
 
 	Py_RETURN_NONE;
@@ -1135,14 +1085,14 @@
 	const char *path = NULL;
 	struct statfs buf;
 	int r;
-	if (!PyArg_ParseTuple(args, PY23("s", "y"), &path))
+	if (!PyArg_ParseTuple(args, "y", &path))
 		return NULL;
 
 	memset(&buf, 0, sizeof(buf));
 	r = statfs(path, &buf);
 	if (r != 0)
 		return PyErr_SetFromErrno(PyExc_OSError);
-	return Py_BuildValue(PY23("s", "y"), describefstype(&buf));
+	return Py_BuildValue("y", describefstype(&buf));
 }
 #endif /* defined(HAVE_LINUX_STATFS) || defined(HAVE_BSD_STATFS) */
 
@@ -1153,14 +1103,14 @@
 	const char *path = NULL;
 	struct statfs buf;
 	int r;
-	if (!PyArg_ParseTuple(args, PY23("s", "y"), &path))
+	if (!PyArg_ParseTuple(args, "y", &path))
 		return NULL;
 
 	memset(&buf, 0, sizeof(buf));
 	r = statfs(path, &buf);
 	if (r != 0)
 		return PyErr_SetFromErrno(PyExc_OSError);
-	return Py_BuildValue(PY23("s", "y"), buf.f_mntonname);
+	return Py_BuildValue("y", buf.f_mntonname);
 }
 #endif /* defined(HAVE_BSD_STATFS) */
 
@@ -1195,8 +1145,7 @@
 
 	static char *kwlist[] = {"path", "stat", "skip", NULL};
 
-	if (!PyArg_ParseTupleAndKeywords(args, kwargs, PY23("s#|OO:listdir",
-							    "y#|OO:listdir"),
+	if (!PyArg_ParseTupleAndKeywords(args, kwargs, "y#|OO:listdir",
 			kwlist, &path, &plen, &statobj, &skipobj))
 		return NULL;
 
@@ -1227,12 +1176,8 @@
 	char fpmode[4];
 	int fppos = 0;
 	int plus;
-#ifndef IS_PY3K
-	FILE *fp;
-#endif
 
-	if (!PyArg_ParseTupleAndKeywords(args, kwds, PY23("et|si:posixfile",
-							  "et|yi:posixfile"),
+	if (!PyArg_ParseTupleAndKeywords(args, kwds, "et|yi:posixfile",
 					 kwlist,
 					 Py_FileSystemDefaultEncoding,
 					 &name, &mode, &bufsize))
@@ -1302,26 +1247,9 @@
 		PyErr_SetFromErrnoWithFilename(PyExc_IOError, name);
 		goto bail;
 	}
-#ifndef IS_PY3K
-	fp = _fdopen(fd, fpmode);
-	if (fp == NULL) {
-		_close(fd);
-		PyErr_SetFromErrnoWithFilename(PyExc_IOError, name);
-		goto bail;
-	}
-
-	file_obj = PyFile_FromFile(fp, name, mode, fclose);
-	if (file_obj == NULL) {
-		fclose(fp);
-		goto bail;
-	}
-
-	PyFile_SetBufSize(file_obj, bufsize);
-#else
 	file_obj = PyFile_FromFd(fd, name, mode, bufsize, NULL, NULL, NULL, 1);
 	if (file_obj == NULL)
 		goto bail;
-#endif
 bail:
 	PyMem_Free(name);
 	return file_obj;
@@ -1387,7 +1315,6 @@
 
 static const int version = 4;
 
-#ifdef IS_PY3K
 static struct PyModuleDef osutil_module = {
 	PyModuleDef_HEAD_INIT,
 	"osutil",
@@ -1406,14 +1333,3 @@
 	PyModule_AddIntConstant(m, "version", version);
 	return m;
 }
-#else
-PyMODINIT_FUNC initosutil(void)
-{
-	PyObject *m;
-	if (PyType_Ready(&listdir_stat_type) == -1)
-		return;
-
-	m = Py_InitModule3("osutil", methods, osutil_doc);
-	PyModule_AddIntConstant(m, "version", version);
-}
-#endif
--- a/mercurial/cext/parsers.c	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/cext/parsers.c	Wed May 04 18:17:44 2022 +0200
@@ -17,22 +17,6 @@
 #include "charencode.h"
 #include "util.h"
 
-#ifdef IS_PY3K
-/* The mapping of Python types is meant to be temporary to get Python
- * 3 to compile. We should remove this once Python 3 support is fully
- * supported and proper types are used in the extensions themselves. */
-#define PyInt_Check PyLong_Check
-#define PyInt_FromLong PyLong_FromLong
-#define PyInt_FromSsize_t PyLong_FromSsize_t
-#define PyInt_AsLong PyLong_AsLong
-#else
-/* Windows on Python 2.7 doesn't define S_IFLNK. Python 3+ defines via
- * pyport.h. */
-#ifndef S_IFLNK
-#define S_IFLNK 0120000
-#endif
-#endif
-
 static const char *const versionerrortext = "Python minor version mismatch";
 
 static const int dirstate_v1_from_p2 = -2;
@@ -305,27 +289,6 @@
 	                     self->mtime_ns);
 };
 
-static PyObject *dirstate_item_v1_state(dirstateItemObject *self)
-{
-	char state = dirstate_item_c_v1_state(self);
-	return PyBytes_FromStringAndSize(&state, 1);
-};
-
-static PyObject *dirstate_item_v1_mode(dirstateItemObject *self)
-{
-	return PyInt_FromLong(dirstate_item_c_v1_mode(self));
-};
-
-static PyObject *dirstate_item_v1_size(dirstateItemObject *self)
-{
-	return PyInt_FromLong(dirstate_item_c_v1_size(self));
-};
-
-static PyObject *dirstate_item_v1_mtime(dirstateItemObject *self)
-{
-	return PyInt_FromLong(dirstate_item_c_v1_mtime(self));
-};
-
 static PyObject *dirstate_item_mtime_likely_equal_to(dirstateItemObject *self,
                                                      PyObject *other)
 {
@@ -419,20 +382,6 @@
 	return t;
 }
 
-/* This will never change since it's bound to V1, unlike `dirstate_item_new` */
-static PyObject *dirstate_item_from_v1_meth(PyTypeObject *subtype,
-                                            PyObject *args)
-{
-	/* We do all the initialization here and not a tp_init function because
-	 * dirstate_item is immutable. */
-	char state;
-	int size, mode, mtime;
-	if (!PyArg_ParseTuple(args, "ciii", &state, &mode, &size, &mtime)) {
-		return NULL;
-	}
-	return (PyObject *)dirstate_item_from_v1_data(state, mode, size, mtime);
-};
-
 static PyObject *dirstate_item_from_v2_meth(PyTypeObject *subtype,
                                             PyObject *args)
 {
@@ -542,18 +491,8 @@
 static PyMethodDef dirstate_item_methods[] = {
     {"v2_data", (PyCFunction)dirstate_item_v2_data, METH_NOARGS,
      "return data suitable for v2 serialization"},
-    {"v1_state", (PyCFunction)dirstate_item_v1_state, METH_NOARGS,
-     "return a \"state\" suitable for v1 serialization"},
-    {"v1_mode", (PyCFunction)dirstate_item_v1_mode, METH_NOARGS,
-     "return a \"mode\" suitable for v1 serialization"},
-    {"v1_size", (PyCFunction)dirstate_item_v1_size, METH_NOARGS,
-     "return a \"size\" suitable for v1 serialization"},
-    {"v1_mtime", (PyCFunction)dirstate_item_v1_mtime, METH_NOARGS,
-     "return a \"mtime\" suitable for v1 serialization"},
     {"mtime_likely_equal_to", (PyCFunction)dirstate_item_mtime_likely_equal_to,
      METH_O, "True if the stored mtime is likely equal to the given mtime"},
-    {"from_v1_data", (PyCFunction)dirstate_item_from_v1_meth,
-     METH_VARARGS | METH_CLASS, "build a new DirstateItem object from V1 data"},
     {"from_v2_data", (PyCFunction)dirstate_item_from_v2_meth,
      METH_VARARGS | METH_CLASS, "build a new DirstateItem object from V2 data"},
     {"set_possibly_dirty", (PyCFunction)dirstate_item_set_possibly_dirty,
@@ -571,17 +510,17 @@
 
 static PyObject *dirstate_item_get_mode(dirstateItemObject *self)
 {
-	return PyInt_FromLong(dirstate_item_c_v1_mode(self));
+	return PyLong_FromLong(dirstate_item_c_v1_mode(self));
 };
 
 static PyObject *dirstate_item_get_size(dirstateItemObject *self)
 {
-	return PyInt_FromLong(dirstate_item_c_v1_size(self));
+	return PyLong_FromLong(dirstate_item_c_v1_size(self));
 };
 
 static PyObject *dirstate_item_get_mtime(dirstateItemObject *self)
 {
-	return PyInt_FromLong(dirstate_item_c_v1_mtime(self));
+	return PyLong_FromLong(dirstate_item_c_v1_mtime(self));
 };
 
 static PyObject *dirstate_item_get_state(dirstateItemObject *self)
@@ -831,9 +770,8 @@
 	Py_ssize_t len = 40;
 	Py_ssize_t readlen;
 
-	if (!PyArg_ParseTuple(
-	        args, PY23("O!O!s#:parse_dirstate", "O!O!y#:parse_dirstate"),
-	        &PyDict_Type, &dmap, &PyDict_Type, &cmap, &str, &readlen)) {
+	if (!PyArg_ParseTuple(args, "O!O!y#:parse_dirstate", &PyDict_Type,
+	                      &dmap, &PyDict_Type, &cmap, &str, &readlen)) {
 		goto quit;
 	}
 
@@ -846,8 +784,8 @@
 		goto quit;
 	}
 
-	parents = Py_BuildValue(PY23("s#s#", "y#y#"), str, (Py_ssize_t)20,
-	                        str + 20, (Py_ssize_t)20);
+	parents = Py_BuildValue("y#y#", str, (Py_ssize_t)20, str + 20,
+	                        (Py_ssize_t)20);
 	if (!parents) {
 		goto quit;
 	}
@@ -1176,8 +1114,7 @@
 	Py_ssize_t datalen, offset, stop;
 	PyObject *markers = NULL;
 
-	if (!PyArg_ParseTuple(args, PY23("s#nn", "y#nn"), &data, &datalen,
-	                      &offset, &stop)) {
+	if (!PyArg_ParseTuple(args, "y#nn", &data, &datalen, &offset, &stop)) {
 		return NULL;
 	}
 	if (offset < 0) {
@@ -1289,7 +1226,7 @@
 	if (!ver) {
 		return -1;
 	}
-	hexversion = PyInt_AsLong(ver);
+	hexversion = PyLong_AsLong(ver);
 	Py_DECREF(ver);
 	/* sys.hexversion is a 32-bit number by default, so the -1 case
 	 * should only occur in unusual circumstances (e.g. if sys.hexversion
@@ -1309,7 +1246,6 @@
 	return 0;
 }
 
-#ifdef IS_PY3K
 static struct PyModuleDef parsers_module = {PyModuleDef_HEAD_INIT, "parsers",
                                             parsers_doc, -1, methods};
 
@@ -1323,15 +1259,3 @@
 	module_init(mod);
 	return mod;
 }
-#else
-PyMODINIT_FUNC initparsers(void)
-{
-	PyObject *mod;
-
-	if (check_python_version() == -1) {
-		return;
-	}
-	mod = Py_InitModule3("parsers", methods, parsers_doc);
-	module_init(mod);
-}
-#endif
--- a/mercurial/cext/pathencode.c	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/cext/pathencode.c	Wed May 04 18:17:44 2022 +0200
@@ -535,8 +535,7 @@
 	Py_ssize_t len, newlen;
 	PyObject *ret;
 
-	if (!PyArg_ParseTuple(args, PY23("s#:lowerencode", "y#:lowerencode"),
-	                      &path, &len)) {
+	if (!PyArg_ParseTuple(args, "y#:lowerencode", &path, &len)) {
 		return NULL;
 	}
 
@@ -711,7 +710,7 @@
 		}
 	}
 
-	shaobj = PyObject_CallFunction(shafunc, PY23("s#", "y#"), str, len);
+	shaobj = PyObject_CallFunction(shafunc, "y#", str, len);
 
 	if (shaobj == NULL) {
 		return -1;
--- a/mercurial/cext/revlog.c	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/cext/revlog.c	Wed May 04 18:17:44 2022 +0200
@@ -23,16 +23,6 @@
 #include "revlog.h"
 #include "util.h"
 
-#ifdef IS_PY3K
-/* The mapping of Python types is meant to be temporary to get Python
- * 3 to compile. We should remove this once Python 3 support is fully
- * supported and proper types are used in the extensions themselves. */
-#define PyInt_Check PyLong_Check
-#define PyInt_FromLong PyLong_FromLong
-#define PyInt_FromSsize_t PyLong_FromSsize_t
-#define PyInt_AsLong PyLong_AsLong
-#endif
-
 typedef struct indexObjectStruct indexObject;
 
 typedef struct {
@@ -43,6 +33,7 @@
 	int abi_version;
 	Py_ssize_t (*index_length)(const indexObject *);
 	const char *(*index_node)(indexObject *, Py_ssize_t);
+	int (*fast_rank)(indexObject *, Py_ssize_t);
 	int (*index_parents)(PyObject *, int, int *);
 } Revlog_CAPI;
 
@@ -119,11 +110,9 @@
 static int index_find_node(indexObject *self, const char *node);
 
 #if LONG_MAX == 0x7fffffffL
-static const char *const tuple_format =
-    PY23("Kiiiiiis#KiBBi", "Kiiiiiiy#KiBBi");
+static const char *const tuple_format = "Kiiiiiiy#KiBBi";
 #else
-static const char *const tuple_format =
-    PY23("kiiiiiis#kiBBi", "kiiiiiiy#kiBBi");
+static const char *const tuple_format = "kiiiiiiy#kiBBi";
 #endif
 
 /* A RevlogNG v1 index entry is 64 bytes long. */
@@ -576,6 +565,33 @@
 }
 
 /*
+ * Return the stored rank of a given revision if known, or rank_unknown
+ * otherwise.
+ *
+ * The rank of a revision is the size of the sub-graph it defines as a head.
+ * Equivalently, the rank of a revision `r` is the size of the set
+ * `ancestors(r)`, `r` included.
+ *
+ * This method returns the rank retrieved from the revlog in constant time. It
+ * makes no attempt at computing unknown values for versions of the revlog
+ * which do not persist the rank.
+ */
+static int index_fast_rank(indexObject *self, Py_ssize_t pos)
+{
+	Py_ssize_t length = index_length(self);
+
+	if (self->format_version != format_cl2 || pos >= length) {
+		return rank_unknown;
+	}
+
+	if (pos == nullrev) {
+		return 0; /* convention */
+	}
+
+	return getbe32(index_deref(self, pos) + entry_cl2_offset_rank);
+}
+
+/*
  * Return the hash of the node corresponding to the given rev. The
  * rev is assumed to be existing. If not, an exception is set.
  */
@@ -730,9 +746,9 @@
 	char comp_mode;
 	char *data;
 #if LONG_MAX == 0x7fffffffL
-	const char *const sidedata_format = PY23("nKiKB", "nKiKB");
+	const char *const sidedata_format = "nKiKB";
 #else
-	const char *const sidedata_format = PY23("nkikB", "nkikB");
+	const char *const sidedata_format = "nkikB";
 #endif
 
 	if (self->entry_size == v1_entry_size || self->inlined) {
@@ -802,7 +818,7 @@
 #define istat(__n, __d)                                                        \
 	do {                                                                   \
 		s = PyBytes_FromString(__d);                                   \
-		t = PyInt_FromSsize_t(self->__n);                              \
+		t = PyLong_FromSsize_t(self->__n);                             \
 		if (!s || !t)                                                  \
 			goto bail;                                             \
 		if (PyDict_SetItem(obj, s, t) == -1)                           \
@@ -953,7 +969,7 @@
 
 	l = PyList_GET_SIZE(roots);
 	for (i = 0; i < l; i++) {
-		revnum = PyInt_AsLong(PyList_GET_ITEM(roots, i));
+		revnum = PyLong_AsLong(PyList_GET_ITEM(roots, i));
 		if (revnum == -1 && PyErr_Occurred())
 			goto bail;
 		/* If root is out of range, e.g. wdir(), it must be unreachable
@@ -966,7 +982,7 @@
 	/* Populate tovisit with all the heads */
 	l = PyList_GET_SIZE(heads);
 	for (i = 0; i < l; i++) {
-		revnum = PyInt_AsLong(PyList_GET_ITEM(heads, i));
+		revnum = PyLong_AsLong(PyList_GET_ITEM(heads, i));
 		if (revnum == -1 && PyErr_Occurred())
 			goto bail;
 		if (revnum + 1 < 0 || revnum + 1 >= len + 1) {
@@ -986,7 +1002,7 @@
 		revnum = tovisit[k++];
 		if (revstates[revnum + 1] & RS_ROOT) {
 			revstates[revnum + 1] |= RS_REACHABLE;
-			val = PyInt_FromLong(revnum);
+			val = PyLong_FromLong(revnum);
 			if (val == NULL)
 				goto bail;
 			r = PyList_Append(reachable, val);
@@ -1031,7 +1047,7 @@
 			     RS_REACHABLE) &&
 			    !(revstates[i + 1] & RS_REACHABLE)) {
 				revstates[i + 1] |= RS_REACHABLE;
-				val = PyInt_FromSsize_t(i);
+				val = PyLong_FromSsize_t(i);
 				if (val == NULL)
 					goto bail;
 				r = PyList_Append(reachable, val);
@@ -1116,7 +1132,7 @@
 	}
 
 	for (i = 0; i < numphases; ++i) {
-		PyObject *pyphase = PyInt_FromLong(trackedphases[i]);
+		PyObject *pyphase = PyLong_FromLong(trackedphases[i]);
 		PyObject *phaseroots = NULL;
 		if (pyphase == NULL)
 			goto release;
@@ -1175,7 +1191,7 @@
 			                "bad phase number in internal list");
 			goto release;
 		}
-		pyrev = PyInt_FromLong(rev);
+		pyrev = PyLong_FromLong(rev);
 		if (pyrev == NULL)
 			goto release;
 		if (PySet_Add(pyphase, pyrev) == -1) {
@@ -1189,7 +1205,7 @@
 	if (phasesetsdict == NULL)
 		goto release;
 	for (i = 0; i < numphases; ++i) {
-		PyObject *pyphase = PyInt_FromLong(trackedphases[i]);
+		PyObject *pyphase = PyLong_FromLong(trackedphases[i]);
 		if (pyphase == NULL)
 			goto release;
 		if (PyDict_SetItem(phasesetsdict, pyphase, phasesets[i]) ==
@@ -1247,7 +1263,7 @@
 	if (heads == NULL)
 		goto bail;
 	if (len == 0) {
-		PyObject *nullid = PyInt_FromLong(-1);
+		PyObject *nullid = PyLong_FromLong(-1);
 		if (nullid == NULL || PyList_Append(heads, nullid) == -1) {
 			Py_XDECREF(nullid);
 			goto bail;
@@ -1296,7 +1312,7 @@
 
 		if (nothead[i])
 			continue;
-		head = PyInt_FromSsize_t(i);
+		head = PyLong_FromSsize_t(i);
 		if (head == NULL || PyList_Append(heads, head) == -1) {
 			Py_XDECREF(head);
 			goto bail;
@@ -1442,7 +1458,7 @@
 			assert(PyErr_Occurred());
 			goto bail;
 		}
-		key = PyInt_FromSsize_t(base);
+		key = PyLong_FromSsize_t(base);
 		allvalues = PyDict_GetItem(cache, key);
 		if (allvalues == NULL && PyErr_Occurred()) {
 			goto bail;
@@ -1459,7 +1475,7 @@
 				goto bail;
 			}
 		}
-		value = PyInt_FromSsize_t(rev);
+		value = PyLong_FromSsize_t(rev);
 		if (PyList_Append(allvalues, value)) {
 			goto bail;
 		}
@@ -1486,8 +1502,8 @@
 		return NULL;
 	}
 
-	if (PyInt_Check(stoparg)) {
-		stoprev = (int)PyInt_AsLong(stoparg);
+	if (PyLong_Check(stoparg)) {
+		stoprev = (int)PyLong_AsLong(stoparg);
 		if (stoprev == -1 && PyErr_Occurred()) {
 			return NULL;
 		}
@@ -1521,7 +1537,7 @@
 	iterrev = rev;
 
 	while (iterrev != baserev && iterrev != stoprev) {
-		PyObject *value = PyInt_FromLong(iterrev);
+		PyObject *value = PyLong_FromLong(iterrev);
 		if (value == NULL) {
 			goto bail;
 		}
@@ -1560,7 +1576,7 @@
 	if (iterrev == stoprev) {
 		stopped = 1;
 	} else {
-		PyObject *value = PyInt_FromLong(iterrev);
+		PyObject *value = PyLong_FromLong(iterrev);
 		if (value == NULL) {
 			goto bail;
 		}
@@ -1712,7 +1728,8 @@
 		goto bail;
 	}
 	for (i = 0; i < num_revs; i++) {
-		Py_ssize_t revnum = PyInt_AsLong(PyList_GET_ITEM(list_revs, i));
+		Py_ssize_t revnum =
+		    PyLong_AsLong(PyList_GET_ITEM(list_revs, i));
 		if (revnum == -1 && PyErr_Occurred()) {
 			goto bail;
 		}
@@ -2118,7 +2135,7 @@
 		raise_revlog_error();
 		return NULL;
 	}
-	return PyInt_FromLong(length);
+	return PyLong_FromLong(length);
 }
 
 static void nt_dealloc(nodetree *self)
@@ -2266,7 +2283,7 @@
 	char *node;
 	int rev;
 
-	if (PyInt_Check(value)) {
+	if (PyLong_Check(value)) {
 		long idx;
 		if (!pylong_to_long(value, &idx)) {
 			return NULL;
@@ -2278,7 +2295,7 @@
 		return NULL;
 	rev = index_find_node(self, node);
 	if (rev >= -1)
-		return PyInt_FromLong(rev);
+		return PyLong_FromLong(rev);
 	if (rev == -2)
 		raise_revlog_error();
 	return NULL;
@@ -2310,7 +2327,7 @@
 	char *node;
 	int rev, i;
 
-	if (!PyArg_ParseTuple(args, PY23("s#", "y#"), &node, &nodelen))
+	if (!PyArg_ParseTuple(args, "y#", &node, &nodelen))
 		return NULL;
 
 	if (nodelen < 1) {
@@ -2377,7 +2394,7 @@
 		raise_revlog_error();
 		return NULL;
 	}
-	return PyInt_FromLong(length);
+	return PyLong_FromLong(length);
 }
 
 static PyObject *index_m_get(indexObject *self, PyObject *args)
@@ -2395,14 +2412,14 @@
 		return NULL;
 	if (rev == -2)
 		Py_RETURN_NONE;
-	return PyInt_FromLong(rev);
+	return PyLong_FromLong(rev);
 }
 
 static int index_contains(indexObject *self, PyObject *value)
 {
 	char *node;
 
-	if (PyInt_Check(value)) {
+	if (PyLong_Check(value)) {
 		long rev;
 		if (!pylong_to_long(value, &rev)) {
 			return -1;
@@ -2440,7 +2457,7 @@
 		return NULL;
 	rev = index_find_node(self, node);
 	if (rev >= -1)
-		return PyInt_FromLong(rev);
+		return PyLong_FromLong(rev);
 	if (rev == -2)
 		raise_revlog_error();
 	return NULL;
@@ -2493,7 +2510,7 @@
 		if (sv < poison) {
 			interesting -= 1;
 			if (sv == allseen) {
-				PyObject *obj = PyInt_FromLong(v);
+				PyObject *obj = PyLong_FromLong(v);
 				if (obj == NULL)
 					goto bail;
 				if (PyList_Append(gca, obj) == -1) {
@@ -2561,7 +2578,7 @@
 	}
 
 	for (i = 0; i < revcount; i++) {
-		int n = (int)PyInt_AsLong(PyList_GET_ITEM(revs, i));
+		int n = (int)PyLong_AsLong(PyList_GET_ITEM(revs, i));
 		if (n > maxrev)
 			maxrev = n;
 	}
@@ -2586,7 +2603,7 @@
 		goto bail;
 
 	for (i = 0; i < revcount; i++) {
-		int n = (int)PyInt_AsLong(PyList_GET_ITEM(revs, i));
+		int n = (int)PyLong_AsLong(PyList_GET_ITEM(revs, i));
 		long b = 1l << i;
 		depth[n] = 1;
 		seen[n] = b;
@@ -2716,13 +2733,13 @@
 		bitmask x;
 		long val;
 
-		if (!PyInt_Check(obj)) {
+		if (!PyLong_Check(obj)) {
 			PyErr_SetString(PyExc_TypeError,
 			                "arguments must all be ints");
 			Py_DECREF(obj);
 			goto bail;
 		}
-		val = PyInt_AsLong(obj);
+		val = PyLong_AsLong(obj);
 		Py_DECREF(obj);
 		if (val == -1) {
 			ret = PyList_New(0);
@@ -2763,7 +2780,7 @@
 		ret = PyList_New(1);
 		if (ret == NULL)
 			goto bail;
-		obj = PyInt_FromLong(revs[0]);
+		obj = PyLong_FromLong(revs[0]);
 		if (obj == NULL)
 			goto bail;
 		PyList_SET_ITEM(ret, 0, obj);
@@ -2834,14 +2851,8 @@
 	Py_ssize_t length = index_length(self) + 1;
 	int ret = 0;
 
-/* Argument changed from PySliceObject* to PyObject* in Python 3. */
-#ifdef IS_PY3K
 	if (PySlice_GetIndicesEx(item, length, &start, &stop, &step,
 	                         &slicelength) < 0)
-#else
-	if (PySlice_GetIndicesEx((PySliceObject *)item, length, &start, &stop,
-	                         &step, &slicelength) < 0)
-#endif
 		return -1;
 
 	if (slicelength <= 0)
@@ -2925,7 +2936,7 @@
 	if (value == NULL)
 		return self->ntinitialized ? nt_delete_node(&self->nt, node)
 		                           : 0;
-	rev = PyInt_AsLong(value);
+	rev = PyLong_AsLong(value);
 	if (rev > INT_MAX || rev < 0) {
 		if (!PyErr_Occurred())
 			PyErr_SetString(PyExc_ValueError, "rev out of range");
@@ -3027,10 +3038,9 @@
 		self->entry_size = cl2_entry_size;
 	}
 
-	self->nullentry =
-	    Py_BuildValue(PY23("iiiiiiis#iiBBi", "iiiiiiiy#iiBBi"), 0, 0, 0, -1,
-	                  -1, -1, -1, nullid, self->nodelen, 0, 0,
-	                  comp_mode_inline, comp_mode_inline, rank_unknown);
+	self->nullentry = Py_BuildValue(
+	    "iiiiiiiy#iiBBi", 0, 0, 0, -1, -1, -1, -1, nullid, self->nodelen, 0,
+	    0, comp_mode_inline, comp_mode_inline, rank_unknown);
 
 	if (!self->nullentry)
 		return -1;
@@ -3266,10 +3276,7 @@
 static Revlog_CAPI CAPI = {
     /* increment the abi_version field upon each change in the Revlog_CAPI
        struct or in the ABI of the listed functions */
-    2,
-    index_length,
-    index_node,
-    HgRevlogIndex_GetParents,
+    3, index_length, index_node, index_fast_rank, HgRevlogIndex_GetParents,
 };
 
 void revlog_module_init(PyObject *mod)
--- a/mercurial/cext/util.h	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/cext/util.h	Wed May 04 18:17:44 2022 +0200
@@ -10,17 +10,6 @@
 
 #include "compat.h"
 
-#if PY_MAJOR_VERSION >= 3
-#define IS_PY3K
-#endif
-
-/* helper to switch things like string literal depending on Python version */
-#ifdef IS_PY3K
-#define PY23(py2, py3) py3
-#else
-#define PY23(py2, py3) py2
-#endif
-
 /* clang-format off */
 typedef struct {
 	PyObject_HEAD
--- a/mercurial/cffi/bdiff.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/cffi/bdiff.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import struct
 
--- a/mercurial/cffi/bdiffbuild.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/cffi/bdiffbuild.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import cffi
 import os
 
--- a/mercurial/cffi/mpatch.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/cffi/mpatch.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from ..pure.mpatch import *
 from ..pure.mpatch import mpatchError  # silence pyflakes
--- a/mercurial/cffi/mpatchbuild.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/cffi/mpatchbuild.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import cffi
 import os
 
--- a/mercurial/cffi/osutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/cffi/osutil.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import os
 import stat as statmod
@@ -34,7 +33,7 @@
     attrkinds[lib.VFIFO] = statmod.S_IFIFO
     attrkinds[lib.VSOCK] = statmod.S_IFSOCK
 
-    class stat_res(object):
+    class stat_res:
         def __init__(self, st_mode, st_mtime, st_size):
             self.st_mode = st_mode
             self.st_mtime = st_mtime
--- a/mercurial/cffi/osutilbuild.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/cffi/osutilbuild.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import cffi
 
 ffi = cffi.FFI()
--- a/mercurial/changegroup.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/changegroup.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import os
 import struct
@@ -106,7 +105,7 @@
                 os.unlink(cleanup)
 
 
-class cg1unpacker(object):
+class cg1unpacker:
     """Unpacker for cg1 changegroup streams.
 
     A changegroup unpacker handles the framing of the revision data in
@@ -425,7 +424,7 @@
                     mfnode = cl.changelogrevision(cset).manifest
                     mfest = ml[mfnode].readdelta()
                     # store file nodes we must see
-                    for f, n in pycompat.iteritems(mfest):
+                    for f, n in mfest.items():
                         needfiles.setdefault(f, set()).add(n)
 
             on_filelog_rev = None
@@ -692,7 +691,7 @@
         )
 
 
-class headerlessfixup(object):
+class headerlessfixup:
     def __init__(self, fh, h):
         self._h = h
         self._fh = fh
@@ -1004,7 +1003,7 @@
         progress.complete()
 
 
-class cgpacker(object):
+class cgpacker:
     def __init__(
         self,
         repo,
@@ -1967,7 +1966,7 @@
                 del needfiles[f]
     progress.complete()
 
-    for f, needs in pycompat.iteritems(needfiles):
+    for f, needs in needfiles.items():
         fl = repo.file(f)
         for n in needs:
             try:
--- a/mercurial/changelog.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/changelog.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from .i18n import _
 from .node import (
@@ -92,7 +91,7 @@
     return b'\n'.join([l.rstrip() for l in desc.splitlines()]).strip(b'\n')
 
 
-class appender(object):
+class appender:
     """the changelog index must be updated last on disk, so we use this class
     to delay writes to it"""
 
@@ -162,7 +161,7 @@
         return self.fp.__exit__(*args)
 
 
-class _divertopener(object):
+class _divertopener:
     def __init__(self, opener, target):
         self._opener = opener
         self._target = target
@@ -189,7 +188,7 @@
 
 
 @attr.s
-class _changelogrevision(object):
+class _changelogrevision:
     # Extensions might modify _defaultextra, so let the constructor below pass
     # it in
     extra = attr.ib()
@@ -205,7 +204,7 @@
     branchinfo = attr.ib(default=(_defaultextra[b'branch'], False))
 
 
-class changelogrevision(object):
+class changelogrevision:
     """Holds results of a parsed changelog revision.
 
     Changelog revisions consist of multiple pieces of data, including
--- a/mercurial/chgserver.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/chgserver.py	Wed May 04 18:17:44 2022 +0200
@@ -39,7 +39,6 @@
   skiphash = False
 """
 
-from __future__ import absolute_import
 
 import inspect
 import os
@@ -135,7 +134,7 @@
         ignored = set()
     envitems = [
         (k, v)
-        for k, v in pycompat.iteritems(encoding.environ)
+        for k, v in encoding.environ.items()
         if _envre.match(k) and k not in ignored
     ]
     envhash = _hashlist(sorted(envitems))
@@ -197,7 +196,7 @@
     return _hashlist(pycompat.maplist(trystat, paths))[:12]
 
 
-class hashstate(object):
+class hashstate:
     """a structure storing confighash, mtimehash, paths used for mtimehash"""
 
     def __init__(self, confighash, mtimehash, mtimepaths):
@@ -293,7 +292,7 @@
     return (newui, newlui)
 
 
-class channeledsystem(object):
+class channeledsystem:
     """Propagate ui.system() request in the following format:
 
     payload length (unsigned int),
@@ -321,7 +320,7 @@
 
     def __call__(self, cmd, environ, cwd=None, type=b'system', cmdtable=None):
         args = [type, cmd, util.abspath(cwd or b'.')]
-        args.extend(b'%s=%s' % (k, v) for k, v in pycompat.iteritems(environ))
+        args.extend(b'%s=%s' % (k, v) for k, v in environ.items())
         data = b'\0'.join(args)
         self.out.write(struct.pack(b'>cI', self.channel, len(data)))
         self.out.write(data)
@@ -409,22 +408,13 @@
             # be unbuffered no matter if it is a tty or not.
             if fn == b'ferr':
                 newfp = fp
-            elif pycompat.ispy3:
+            else:
                 # On Python 3, the standard library doesn't offer line-buffered
                 # binary streams, so wrap/unwrap it.
                 if fp.isatty():
                     newfp = procutil.make_line_buffered(fp)
                 else:
                     newfp = procutil.unwrap_line_buffered(fp)
-            else:
-                # Python 2 uses the I/O streams provided by the C library, so
-                # make it line-buffered explicitly. Otherwise the default would
-                # be decided on first write(), where fout could be a pager.
-                if fp.isatty():
-                    bufsize = 1  # line buffered
-                else:
-                    bufsize = -1  # system default
-                newfp = os.fdopen(fp.fileno(), mode, bufsize)
             if newfp is not fp:
                 setattr(ui, fn, newfp)
             setattr(self, cn, newfp)
@@ -448,17 +438,8 @@
         nullfd = os.open(os.devnull, os.O_WRONLY)
         ui = self.ui
         for (ch, fp, fd), (cn, fn, mode) in zip(self._oldios, _iochannels):
-            newfp = getattr(ui, fn)
-            # On Python 2, newfp and fp may be separate file objects associated
-            # with the same fd, so we must close newfp while it's associated
-            # with the client. Otherwise the new associated fd would be closed
-            # when newfp gets deleted. On Python 3, newfp is just a wrapper
-            # around fp even if newfp is not fp, so deleting newfp is safe.
-            if not (pycompat.ispy3 or newfp is fp):
-                newfp.close()
-            # restore original fd: fp is open again
             try:
-                if (pycompat.ispy3 or newfp is fp) and 'w' in mode:
+                if 'w' in mode:
                     # Discard buffered data which couldn't be flushed because
                     # of EPIPE. The data should belong to the current session
                     # and should never persist.
@@ -636,7 +617,7 @@
     return b'%s-%s' % (os.path.join(dirname, basename), hashstr)
 
 
-class chgunixservicehandler(object):
+class chgunixservicehandler:
     """Set of operations for chg services"""
 
     pollinterval = 1  # [sec]
--- a/mercurial/cmdutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/cmdutil.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import copy as copymod
 import errno
@@ -627,7 +626,7 @@
             # 5. finally restore backed-up files
             try:
                 dirstate = repo.dirstate
-                for realname, tmpname in pycompat.iteritems(backups):
+                for realname, tmpname in backups.items():
                     ui.debug(b'restoring %r to %r\n' % (tmpname, realname))
 
                     if dirstate.get_entry(realname).maybe_clean:
@@ -667,7 +666,7 @@
     return commit(ui, repo, recordinwlock, pats, opts)
 
 
-class dirnode(object):
+class dirnode:
     """
     Represent a directory in user working copy with information required for
     the purpose of tersing its status.
@@ -833,7 +832,7 @@
 
 
 @attr.s(frozen=True)
-class morestatus(object):
+class morestatus:
     reporoot = attr.ib()
     unfinishedop = attr.ib()
     unfinishedmsg = attr.ib()
@@ -1344,7 +1343,7 @@
     return not pat or pat == b'-'
 
 
-class _unclosablefile(object):
+class _unclosablefile:
     def __init__(self, fp):
         self._fp = fp
 
@@ -2934,16 +2933,15 @@
 
             def filectxfn(repo, ctx_, path):
                 try:
-                    # Return None for removed files.
-                    if path in wctx.removed() and path in filestoamend:
-                        return None
-
                     # If the file being considered is not amongst the files
                     # to be amended, we should use the file context from the
                     # old changeset. This avoids issues when only some files in
                     # the working copy are being amended but there are also
                     # changes to other files from the old changeset.
                     if path in filestoamend:
+                        # Return None for removed files.
+                        if path in wctx.removed():
+                            return None
                         fctx = wctx[path]
                     else:
                         fctx = old.filectx(path)
@@ -3750,10 +3748,18 @@
 
     for f in actions[b'add'][0]:
         # Don't checkout modified files, they are already created by the diff
-        if f not in newlyaddedandmodifiedfiles:
-            prntstatusmsg(b'add', f)
-            checkout(f)
-            repo.dirstate.set_tracked(f)
+        if f in newlyaddedandmodifiedfiles:
+            continue
+
+        if interactive:
+            choice = repo.ui.promptchoice(
+                _(b"add new file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
+            )
+            if choice != 0:
+                continue
+        prntstatusmsg(b'add', f)
+        checkout(f)
+        repo.dirstate.set_tracked(f)
 
     for f in actions[b'undelete'][0]:
         if interactive:
--- a/mercurial/color.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/color.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import re
 
--- a/mercurial/commands.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/commands.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import errno
 import os
@@ -2470,7 +2469,7 @@
 )
 def debugcommands(ui, cmd=b'', *args):
     """list all available commands and options"""
-    for cmd, vals in sorted(pycompat.iteritems(table)):
+    for cmd, vals in sorted(table.items()):
         cmd = cmd.split(b'|')[0]
         opts = b', '.join([i[1] for i in vals[1]])
         ui.write(b'%s: %s\n' % (cmd, opts))
@@ -3911,9 +3910,7 @@
                     hexremoterev = hex(remoterev)
                     bms = [
                         bm
-                        for bm, bmr in pycompat.iteritems(
-                            peer.listkeys(b'bookmarks')
-                        )
+                        for bm, bmr in peer.listkeys(b'bookmarks').items()
                         if bmr == hexremoterev
                     ]
 
@@ -7090,7 +7087,7 @@
 
     c = repo.dirstate.copies()
     copied, renamed = [], []
-    for d, s in pycompat.iteritems(c):
+    for d, s in c.items():
         if s in status.removed:
             status.removed.remove(s)
             renamed.append(d)
--- a/mercurial/commandserver.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/commandserver.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import errno
 import gc
@@ -40,7 +39,7 @@
 )
 
 
-class channeledoutput(object):
+class channeledoutput:
     """
     Write data to out in the following format:
 
@@ -69,7 +68,7 @@
         return getattr(self.out, attr)
 
 
-class channeledmessage(object):
+class channeledmessage:
     """
     Write encoded message and metadata to out in the following format:
 
@@ -98,7 +97,7 @@
         return getattr(self._cout, attr)
 
 
-class channeledinput(object):
+class channeledinput:
     """
     Read data from in_.
 
@@ -201,7 +200,7 @@
     )
 
 
-class server(object):
+class server:
     """
     Listens for commands on fin, runs them and writes the output on a channel
     based stream to fout.
@@ -451,7 +450,7 @@
         u.setlogger(b'cmdserver', logger)
 
 
-class pipeservice(object):
+class pipeservice:
     def __init__(self, ui, repo, opts):
         self.ui = ui
         self.repo = repo
@@ -526,7 +525,7 @@
                 raise
 
 
-class unixservicehandler(object):
+class unixservicehandler:
     """Set of pluggable operations for unix-mode services
 
     Almost all methods except for createcmdserver() are called in the main
@@ -560,7 +559,7 @@
         return server(self.ui, repo, fin, fout, prereposetups)
 
 
-class unixforkingservice(object):
+class unixforkingservice:
     """
     Listens on unix domain socket and forks server per connection
     """
--- a/mercurial/commit.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/commit.py	Wed May 04 18:17:44 2022 +0200
@@ -3,7 +3,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import errno
 
--- a/mercurial/config.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/config.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import errno
 import os
@@ -15,12 +14,11 @@
 from . import (
     encoding,
     error,
-    pycompat,
     util,
 )
 
 
-class config(object):
+class config:
     def __init__(self, data=None):
         self._current_source_level = 0
         self._data = {}
@@ -111,20 +109,19 @@
         return sorted(self._data.keys())
 
     def items(self, section):
-        items = pycompat.iteritems(self._data.get(section, {}))
+        items = self._data.get(section, {}).items()
         return [(k, v[0]) for (k, v) in items]
 
     def set(self, section, item, value, source=b""):
-        if pycompat.ispy3:
-            assert not isinstance(
-                section, str
-            ), b'config section may not be unicode strings on Python 3'
-            assert not isinstance(
-                item, str
-            ), b'config item may not be unicode strings on Python 3'
-            assert not isinstance(
-                value, str
-            ), b'config values may not be unicode strings on Python 3'
+        assert not isinstance(
+            section, str
+        ), b'config section may not be unicode strings on Python 3'
+        assert not isinstance(
+            item, str
+        ), b'config item may not be unicode strings on Python 3'
+        assert not isinstance(
+            value, str
+        ), b'config values may not be unicode strings on Python 3'
         if section not in self:
             self._data[section] = util.cowsortdict()
         else:
--- a/mercurial/configitems.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/configitems.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import functools
 import re
@@ -30,7 +29,7 @@
         knownitems.update(items)
 
 
-class configitem(object):
+class configitem:
     """represent a known config item
 
     :section: the official config section where to find this item,
@@ -1571,6 +1570,45 @@
     default=False,
 )
 coreconfigitem(
+    b'partial-merge-tools',
+    b'.*',
+    default=None,
+    generic=True,
+    experimental=True,
+)
+coreconfigitem(
+    b'partial-merge-tools',
+    br'.*\.patterns',
+    default=dynamicdefault,
+    generic=True,
+    priority=-1,
+    experimental=True,
+)
+coreconfigitem(
+    b'partial-merge-tools',
+    br'.*\.executable$',
+    default=dynamicdefault,
+    generic=True,
+    priority=-1,
+    experimental=True,
+)
+coreconfigitem(
+    b'partial-merge-tools',
+    br'.*\.order',
+    default=0,
+    generic=True,
+    priority=-1,
+    experimental=True,
+)
+coreconfigitem(
+    b'partial-merge-tools',
+    br'.*\.args',
+    default=b"$local $base $other",
+    generic=True,
+    priority=-1,
+    experimental=True,
+)
+coreconfigitem(
     b'merge-tools',
     b'.*',
     default=None,
@@ -1703,6 +1741,30 @@
     generic=True,
 )
 coreconfigitem(
+    b'paths',
+    b'.*:bookmarks.mode',
+    default='default',
+    generic=True,
+)
+coreconfigitem(
+    b'paths',
+    b'.*:multi-urls',
+    default=False,
+    generic=True,
+)
+coreconfigitem(
+    b'paths',
+    b'.*:pushrev',
+    default=None,
+    generic=True,
+)
+coreconfigitem(
+    b'paths',
+    b'.*:pushurl',
+    default=None,
+    generic=True,
+)
+coreconfigitem(
     b'phases',
     b'checksubrepos',
     default=b'follow',
--- a/mercurial/context.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/context.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import errno
 import filecmp
@@ -52,7 +51,7 @@
 propertycache = util.propertycache
 
 
-class basectx(object):
+class basectx:
     """A basectx object represents the common logic for its children:
     changectx: read-only context that is already present in the repo,
     workingctx: a context that represents the working directory and can
@@ -124,7 +123,7 @@
         deleted, unknown, ignored = s.deleted, s.unknown, s.ignored
         deletedset = set(deleted)
         d = mf1.diff(mf2, match=match, clean=listclean)
-        for fn, value in pycompat.iteritems(d):
+        for fn, value in d.items():
             if fn in deletedset:
                 continue
             if value is None:
@@ -797,7 +796,7 @@
         return self.walk(match)
 
 
-class basefilectx(object):
+class basefilectx:
     """A filecontext object represents the common logic for its children:
     filectx: read-only access to a filerevision that is already present
              in the repo,
@@ -993,6 +992,16 @@
             if self._repo._encodefilterpats:
                 # can't rely on size() because wdir content may be decoded
                 return self._filelog.cmp(self._filenode, fctx.data())
+            # filelog.size() has two special cases:
+            # - censored metadata
+            # - copy/rename tracking
+            # The first is detected by peaking into the delta,
+            # the second is detected by abusing parent order
+            # in the revlog index as flag bit. This leaves files using
+            # the dummy encoding and non-standard meta attributes.
+            # The following check is a special case for the empty
+            # metadata block used if the raw file content starts with '\1\n'.
+            # Cases of arbitrary metadata flags are currently mishandled.
             if self.size() - 4 == fctx.size():
                 # size() can match:
                 # if file data starts with '\1\n', empty metadata block is
@@ -3105,7 +3114,7 @@
         return scmutil.status(modified, added, removed, [], [], [], [])
 
 
-class arbitraryfilectx(object):
+class arbitraryfilectx:
     """Allows you to use filectx-like functions on a file in an arbitrary
     location on disk, possibly not in the working directory.
     """
--- a/mercurial/copies.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/copies.py	Wed May 04 18:17:44 2022 +0200
@@ -6,7 +6,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import collections
 import os
@@ -18,7 +17,6 @@
     match as matchmod,
     pathutil,
     policy,
-    pycompat,
     util,
 )
 
@@ -69,7 +67,7 @@
 def _chain(prefix, suffix):
     """chain two sets of copies 'prefix' and 'suffix'"""
     result = prefix.copy()
-    for key, value in pycompat.iteritems(suffix):
+    for key, value in suffix.items():
         result[key] = prefix.get(value, value)
     return result
 
@@ -409,7 +407,7 @@
 
                     if childcopies:
                         newcopies = copies.copy()
-                        for dest, source in pycompat.iteritems(childcopies):
+                        for dest, source in childcopies.items():
                             prev = copies.get(source)
                             if prev is not None and prev[1] is not None:
                                 source = prev[1]
@@ -624,7 +622,7 @@
             newcopies = copies
             if childcopies:
                 newcopies = copies.copy()
-                for dest, source in pycompat.iteritems(childcopies):
+                for dest, source in childcopies.items():
                     prev = copies.get(source)
                     if prev is not None and prev[1] is not None:
                         source = prev[1]
@@ -722,7 +720,7 @@
     # can still exist (e.g. hg cp a b; hg mv a c). In those cases we
     # arbitrarily pick one of the renames.
     r = {}
-    for k, v in sorted(pycompat.iteritems(copies)):
+    for k, v in sorted(copies.items()):
         if match and not match(v):
             continue
         # remove copies
@@ -889,7 +887,7 @@
             copy[dst] = src
 
 
-class branch_copies(object):
+class branch_copies:
     """Information about copies made on one side of a merge/graft.
 
     "copy" is a mapping from destination name -> source name,
@@ -1081,7 +1079,7 @@
 
     # examine each file copy for a potential directory move, which is
     # when all the files in a directory are moved to a new directory
-    for dst, src in pycompat.iteritems(fullcopy):
+    for dst, src in fullcopy.items():
         dsrc, ddst = pathutil.dirname(src), pathutil.dirname(dst)
         if dsrc in invalid:
             # already seen to be uninteresting
@@ -1104,7 +1102,7 @@
     if not dirmove:
         return {}, {}
 
-    dirmove = {k + b"/": v + b"/" for k, v in pycompat.iteritems(dirmove)}
+    dirmove = {k + b"/": v + b"/" for k, v in dirmove.items()}
 
     for d in dirmove:
         repo.ui.debug(
@@ -1187,7 +1185,7 @@
 
     copies2 = {}
     cp = _forwardcopies(base, c2)
-    for dst, src in pycompat.iteritems(cp):
+    for dst, src in cp.items():
         if src in m1:
             copies2[dst] = src
 
@@ -1305,5 +1303,5 @@
     for dest, __ in list(new_copies.items()):
         if dest in parent:
             del new_copies[dest]
-    for dst, src in pycompat.iteritems(new_copies):
+    for dst, src in new_copies.items():
         wctx[dst].markcopied(src)
--- a/mercurial/crecord.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/crecord.py	Wed May 04 18:17:44 2022 +0200
@@ -8,7 +8,6 @@
 # This code is based on the Mark Edgington's crecord extension.
 # (Itself based on Bryan O'Sullivan's record extension.)
 
-from __future__ import absolute_import
 
 import os
 import re
@@ -83,7 +82,7 @@
     return curses and ui.interface(b"chunkselector") == b"curses"
 
 
-class patchnode(object):
+class patchnode:
     """abstract class for patch graph nodes
     (i.e. patchroot, header, hunk, hunkline)
     """
@@ -506,7 +505,7 @@
             text = line.linetext
             if line.linetext == diffhelper.MISSING_NEWLINE_MARKER:
                 noeol = True
-                break
+                continue
             if line.applied:
                 if text.startswith(b'+'):
                     dels.append(text[1:])
@@ -602,7 +601,7 @@
     """
     chunkselector = curseschunkselector(headerlist, ui, operation)
 
-    class dummystdscr(object):
+    class dummystdscr:
         def clear(self):
             pass
 
@@ -629,7 +628,7 @@
 }
 
 
-class curseschunkselector(object):
+class curseschunkselector:
     def __init__(self, headerlist, ui, operation=None):
         # put the headers into a patch object
         self.headerlist = patch(headerlist)
--- a/mercurial/dagop.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/dagop.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import heapq
 
@@ -272,7 +271,7 @@
                 break
 
 
-class subsetparentswalker(object):
+class subsetparentswalker:
     r"""Scan adjacent ancestors in the graph given by the subset
 
     This computes parent-child relations in the sub graph filtered by
@@ -648,7 +647,7 @@
 
 
 @attr.s(slots=True, frozen=True)
-class annotateline(object):
+class annotateline:
     fctx = attr.ib()
     lineno = attr.ib()
     # Whether this annotation was the result of a skip-annotate.
@@ -657,7 +656,7 @@
 
 
 @attr.s(slots=True, frozen=True)
-class _annotatedfile(object):
+class _annotatedfile:
     # list indexed by lineno - 1
     fctxs = attr.ib()
     linenos = attr.ib()
--- a/mercurial/dagparser.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/dagparser.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import re
 import string
--- a/mercurial/debugcommands.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/debugcommands.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import binascii
 import codecs
@@ -494,7 +493,7 @@
         b2caps = bundle2.bundle2caps(peer)
         if b2caps:
             ui.writenoi18n(b'Bundle2 capabilities:\n')
-            for key, values in sorted(pycompat.iteritems(b2caps)):
+            for key, values in sorted(b2caps.items()):
                 ui.write(b'  %s\n' % key)
                 for v in values:
                     ui.write(b'    %s\n' % v)
@@ -1039,7 +1038,7 @@
             b'',
             b'remote-as-revs',
             b"",
-            b'use local as remote, with only these these revisions',
+            b'use local as remote, with only these revisions',
         ),
     ]
     + cmdutil.remoteopts
@@ -1240,6 +1239,7 @@
     # display discovery summary
     fm.plain(b"elapsed time:  %(elapsed)f seconds\n" % data)
     fm.plain(b"round-trips:           %(total-roundtrips)9d\n" % data)
+    fm.plain(b"queries:               %(total-queries)9d\n" % data)
     fm.plain(b"heads summary:\n")
     fm.plain(b"  total common heads:  %(nb-common-heads)9d\n" % data)
     fm.plain(b"    also local heads:  %(nb-common-heads-local)9d\n" % data)
@@ -2403,11 +2403,11 @@
     fm_files.end()
 
     fm_extras = fm.nested(b'extras')
-    for f, d in sorted(pycompat.iteritems(ms.allextras())):
+    for f, d in sorted(ms.allextras().items()):
         if f in ms:
             # If file is in mergestate, we have already processed it's extras
             continue
-        for k, v in pycompat.iteritems(d):
+        for k, v in d.items():
             fm_extras.startitem()
             fm_extras.data(file=f)
             fm_extras.data(key=k)
@@ -2424,7 +2424,7 @@
     names = set()
     # since we previously only listed open branches, we will handle that
     # specially (after this for loop)
-    for name, ns in pycompat.iteritems(repo.names):
+    for name, ns in repo.names.items():
         if name != b'branches':
             names.update(ns.listnames(repo))
     names.update(
@@ -2718,7 +2718,7 @@
         fullpaths = opts['full']
         files, dirs = set(), set()
         adddir, addfile = dirs.add, files.add
-        for f, st in pycompat.iteritems(dirstate):
+        for f, st in dirstate.items():
             if f.startswith(spec) and st.state in acceptable:
                 if fixpaths:
                     f = f.replace(b'/', pycompat.ossep)
@@ -2907,7 +2907,7 @@
             ui.status(pycompat.bytestr(r) + b'\n')
             return not r
         else:
-            for k, v in sorted(pycompat.iteritems(target.listkeys(namespace))):
+            for k, v in sorted(target.listkeys(namespace).items()):
                 ui.write(
                     b"%s\t%s\n"
                     % (stringutil.escapestr(k), stringutil.escapestr(v))
@@ -4289,7 +4289,7 @@
         for opt in cmdutil.remoteopts:
             del opts[opt[1]]
         args = {}
-        for k, v in pycompat.iteritems(opts):
+        for k, v in opts.items():
             if v:
                 args[k] = v
         args = pycompat.strkwargs(args)
--- a/mercurial/destutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/destutil.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from .i18n import _
 from . import bookmarks, error, obsutil, scmutil, stack
--- a/mercurial/diffhelper.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/diffhelper.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from .i18n import _
 
--- a/mercurial/diffutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/diffutil.py	Wed May 04 18:17:44 2022 +0200
@@ -7,7 +7,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from .i18n import _
 
--- a/mercurial/dirstate.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/dirstate.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import collections
 import contextlib
@@ -91,7 +90,7 @@
 
 
 @interfaceutil.implementer(intdirstate.idirstate)
-class dirstate(object):
+class dirstate:
     def __init__(
         self,
         opener,
@@ -343,7 +342,7 @@
         return iter(sorted(self._map))
 
     def items(self):
-        return pycompat.iteritems(self._map)
+        return self._map.items()
 
     iteritems = items
 
@@ -771,9 +770,7 @@
     def _writedirstate(self, tr, st):
         # notify callbacks about parents change
         if self._origpl is not None and self._origpl != self._pl:
-            for c, callback in sorted(
-                pycompat.iteritems(self._plchangecallbacks)
-            ):
+            for c, callback in sorted(self._plchangecallbacks.items()):
                 callback(self, self._origpl, self._pl)
             self._origpl = None
         self._map.write(tr, st)
@@ -936,7 +933,7 @@
         if match.isexact() and self._checkcase:
             normed = {}
 
-            for f, st in pycompat.iteritems(results):
+            for f, st in results.items():
                 if st is None:
                     continue
 
@@ -949,7 +946,7 @@
 
                 paths.add(f)
 
-            for norm, paths in pycompat.iteritems(normed):
+            for norm, paths in normed.items():
                 if len(paths) > 1:
                     for path in paths:
                         folded = self._discoverpath(
@@ -1311,9 +1308,9 @@
         # - match.traversedir does something, because match.traversedir should
         #   be called for every dir in the working dir
         full = listclean or match.traversedir is not None
-        for fn, st in pycompat.iteritems(
-            self.walk(match, subrepos, listunknown, listignored, full=full)
-        ):
+        for fn, st in self.walk(
+            match, subrepos, listunknown, listignored, full=full
+        ).items():
             if not dcontains(fn):
                 if (listignored or mexact(fn)) and dirignore(fn):
                     if listignored:
--- a/mercurial/dirstateguard.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/dirstateguard.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import os
 from .i18n import _
--- a/mercurial/dirstatemap.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/dirstatemap.py	Wed May 04 18:17:44 2022 +0200
@@ -3,7 +3,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import errno
 
@@ -13,7 +12,6 @@
     error,
     pathutil,
     policy,
-    pycompat,
     txnutil,
     util,
 )
@@ -36,7 +34,7 @@
 rangemask = 0x7FFFFFFF
 
 
-class _dirstatemapcommon(object):
+class _dirstatemapcommon:
     """
     Methods that are identical for both implementations of the dirstatemap
     class, with and without Rust extensions enabled.
@@ -81,134 +79,6 @@
     def __getitem__(self, item):
         return self._map[item]
 
-    ### sub-class utility method
-    #
-    # Use to allow for generic implementation of some method while still coping
-    # with minor difference between implementation.
-
-    def _dirs_incr(self, filename, old_entry=None):
-        """incremente the dirstate counter if applicable
-
-        This might be a no-op for some subclass who deal with directory
-        tracking in a different way.
-        """
-
-    def _dirs_decr(self, filename, old_entry=None, remove_variant=False):
-        """decremente the dirstate counter if applicable
-
-        This might be a no-op for some subclass who deal with directory
-        tracking in a different way.
-        """
-
-    def _refresh_entry(self, f, entry):
-        """record updated state of an entry"""
-
-    def _insert_entry(self, f, entry):
-        """add a new dirstate entry (or replace an unrelated one)
-
-        The fact it is actually new is the responsability of the caller
-        """
-
-    def _drop_entry(self, f):
-        """remove any entry for file f
-
-        This should also drop associated copy information
-
-        The fact we actually need to drop it is the responsability of the caller"""
-
-    ### method to manipulate the entries
-
-    def set_possibly_dirty(self, filename):
-        """record that the current state of the file on disk is unknown"""
-        entry = self[filename]
-        entry.set_possibly_dirty()
-        self._refresh_entry(filename, entry)
-
-    def set_clean(self, filename, mode, size, mtime):
-        """mark a file as back to a clean state"""
-        entry = self[filename]
-        size = size & rangemask
-        entry.set_clean(mode, size, mtime)
-        self._refresh_entry(filename, entry)
-        self.copymap.pop(filename, None)
-
-    def set_tracked(self, filename):
-        new = False
-        entry = self.get(filename)
-        if entry is None:
-            self._dirs_incr(filename)
-            entry = DirstateItem(
-                wc_tracked=True,
-            )
-
-            self._insert_entry(filename, entry)
-            new = True
-        elif not entry.tracked:
-            self._dirs_incr(filename, entry)
-            entry.set_tracked()
-            self._refresh_entry(filename, entry)
-            new = True
-        else:
-            # XXX This is probably overkill for more case, but we need this to
-            # fully replace the `normallookup` call with `set_tracked` one.
-            # Consider smoothing this in the future.
-            entry.set_possibly_dirty()
-            self._refresh_entry(filename, entry)
-        return new
-
-    def set_untracked(self, f):
-        """Mark a file as no longer tracked in the dirstate map"""
-        entry = self.get(f)
-        if entry is None:
-            return False
-        else:
-            self._dirs_decr(f, old_entry=entry, remove_variant=not entry.added)
-            if not entry.p2_info:
-                self.copymap.pop(f, None)
-            entry.set_untracked()
-            self._refresh_entry(f, entry)
-            return True
-
-    def reset_state(
-        self,
-        filename,
-        wc_tracked=False,
-        p1_tracked=False,
-        p2_info=False,
-        has_meaningful_mtime=True,
-        has_meaningful_data=True,
-        parentfiledata=None,
-    ):
-        """Set a entry to a given state, diregarding all previous state
-
-        This is to be used by the part of the dirstate API dedicated to
-        adjusting the dirstate after a update/merge.
-
-        note: calling this might result to no entry existing at all if the
-        dirstate map does not see any point at having one for this file
-        anymore.
-        """
-        # copy information are now outdated
-        # (maybe new information should be in directly passed to this function)
-        self.copymap.pop(filename, None)
-
-        if not (p1_tracked or p2_info or wc_tracked):
-            old_entry = self._map.get(filename)
-            self._drop_entry(filename)
-            self._dirs_decr(filename, old_entry=old_entry)
-            return
-
-        old_entry = self._map.get(filename)
-        self._dirs_incr(filename, old_entry)
-        entry = DirstateItem(
-            wc_tracked=wc_tracked,
-            p1_tracked=p1_tracked,
-            p2_info=p2_info,
-            has_meaningful_mtime=has_meaningful_mtime,
-            parentfiledata=parentfiledata,
-        )
-        self._insert_entry(filename, entry)
-
     ### disk interaction
 
     def _opendirstatefile(self):
@@ -355,7 +225,7 @@
         util.clearcachedproperty(self, b"dirfoldmap")
 
     def items(self):
-        return pycompat.iteritems(self._map)
+        return self._map.items()
 
     # forward for python2,3 compat
     iteritems = items
@@ -379,7 +249,7 @@
         self._dirtyparents = True
         copies = {}
         if fold_p2:
-            for f, s in pycompat.iteritems(self._map):
+            for f, s in self._map.items():
                 # Discard "merged" markers when moving away from a merge state
                 if s.p2_info:
                     source = self.copymap.pop(f, None)
@@ -465,7 +335,7 @@
     # (e.g. "has_dir")
 
     def _dirs_incr(self, filename, old_entry=None):
-        """incremente the dirstate counter if applicable"""
+        """increment the dirstate counter if applicable"""
         if (
             old_entry is None or old_entry.removed
         ) and "_dirs" in self.__dict__:
@@ -474,7 +344,7 @@
             self._alldirs.addpath(filename)
 
     def _dirs_decr(self, filename, old_entry=None, remove_variant=False):
-        """decremente the dirstate counter if applicable"""
+        """decrement the dirstate counter if applicable"""
         if old_entry is not None:
             if "_dirs" in self.__dict__ and not old_entry.removed:
                 self._dirs.delpath(filename)
@@ -502,7 +372,7 @@
 
         f = {}
         normcase = util.normcase
-        for name, s in pycompat.iteritems(self._map):
+        for name, s in self._map.items():
             if not s.removed:
                 f[normcase(name)] = name
         f[b'.'] = b'.'  # prevents useless util.fspath() invocation
@@ -540,14 +410,107 @@
 
     ### code related to manipulation of entries and copy-sources
 
+    def reset_state(
+        self,
+        filename,
+        wc_tracked=False,
+        p1_tracked=False,
+        p2_info=False,
+        has_meaningful_mtime=True,
+        parentfiledata=None,
+    ):
+        """Set a entry to a given state, diregarding all previous state
+
+        This is to be used by the part of the dirstate API dedicated to
+        adjusting the dirstate after a update/merge.
+
+        note: calling this might result to no entry existing at all if the
+        dirstate map does not see any point at having one for this file
+        anymore.
+        """
+        # copy information are now outdated
+        # (maybe new information should be in directly passed to this function)
+        self.copymap.pop(filename, None)
+
+        if not (p1_tracked or p2_info or wc_tracked):
+            old_entry = self._map.get(filename)
+            self._drop_entry(filename)
+            self._dirs_decr(filename, old_entry=old_entry)
+            return
+
+        old_entry = self._map.get(filename)
+        self._dirs_incr(filename, old_entry)
+        entry = DirstateItem(
+            wc_tracked=wc_tracked,
+            p1_tracked=p1_tracked,
+            p2_info=p2_info,
+            has_meaningful_mtime=has_meaningful_mtime,
+            parentfiledata=parentfiledata,
+        )
+        self._map[filename] = entry
+
+    def set_tracked(self, filename):
+        new = False
+        entry = self.get(filename)
+        if entry is None:
+            self._dirs_incr(filename)
+            entry = DirstateItem(
+                wc_tracked=True,
+            )
+
+            self._map[filename] = entry
+            new = True
+        elif not entry.tracked:
+            self._dirs_incr(filename, entry)
+            entry.set_tracked()
+            self._refresh_entry(filename, entry)
+            new = True
+        else:
+            # XXX This is probably overkill for more case, but we need this to
+            # fully replace the `normallookup` call with `set_tracked` one.
+            # Consider smoothing this in the future.
+            entry.set_possibly_dirty()
+            self._refresh_entry(filename, entry)
+        return new
+
+    def set_untracked(self, f):
+        """Mark a file as no longer tracked in the dirstate map"""
+        entry = self.get(f)
+        if entry is None:
+            return False
+        else:
+            self._dirs_decr(f, old_entry=entry, remove_variant=not entry.added)
+            if not entry.p2_info:
+                self.copymap.pop(f, None)
+            entry.set_untracked()
+            self._refresh_entry(f, entry)
+            return True
+
+    def set_clean(self, filename, mode, size, mtime):
+        """mark a file as back to a clean state"""
+        entry = self[filename]
+        size = size & rangemask
+        entry.set_clean(mode, size, mtime)
+        self._refresh_entry(filename, entry)
+        self.copymap.pop(filename, None)
+
+    def set_possibly_dirty(self, filename):
+        """record that the current state of the file on disk is unknown"""
+        entry = self[filename]
+        entry.set_possibly_dirty()
+        self._refresh_entry(filename, entry)
+
     def _refresh_entry(self, f, entry):
+        """record updated state of an entry"""
         if not entry.any_tracked:
             self._map.pop(f, None)
 
-    def _insert_entry(self, f, entry):
-        self._map[f] = entry
+    def _drop_entry(self, f):
+        """remove any entry for file f
 
-    def _drop_entry(self, f):
+        This should also drop associated copy information
+
+        The fact we actually need to drop it is the responsability of the caller"""
         self._map.pop(f, None)
         self.copymap.pop(f, None)
 
@@ -630,22 +593,7 @@
             self._dirtyparents = True
             copies = {}
             if fold_p2:
-                # Collect into an intermediate list to avoid a `RuntimeError`
-                # exception due to mutation during iteration.
-                # TODO: move this the whole loop to Rust where `iter_mut`
-                # enables in-place mutation of elements of a collection while
-                # iterating it, without mutating the collection itself.
-                files_with_p2_info = [
-                    f for f, s in self._map.items() if s.p2_info
-                ]
-                rust_map = self._map
-                for f in files_with_p2_info:
-                    e = rust_map.get(f)
-                    source = self.copymap.pop(f, None)
-                    if source:
-                        copies[f] = source
-                    e.drop_merge_data()
-                    rust_map.set_dirstate_item(f, e)
+                copies = self._map.setparents_fixup()
             return copies
 
         ### disk interaction
@@ -715,18 +663,32 @@
 
         ### code related to manipulation of entries and copy-sources
 
-        def _refresh_entry(self, f, entry):
-            if not entry.any_tracked:
-                self._map.drop_item_and_copy_source(f)
-            else:
-                self._map.addfile(f, entry)
+        def set_tracked(self, f):
+            return self._map.set_tracked(f)
+
+        def set_untracked(self, f):
+            return self._map.set_untracked(f)
+
+        def set_clean(self, filename, mode, size, mtime):
+            self._map.set_clean(filename, mode, size, mtime)
+
+        def set_possibly_dirty(self, f):
+            self._map.set_possibly_dirty(f)
 
-        def _insert_entry(self, f, entry):
-            self._map.addfile(f, entry)
-
-        def _drop_entry(self, f):
-            self._map.drop_item_and_copy_source(f)
-
-        def __setitem__(self, key, value):
-            assert isinstance(value, DirstateItem)
-            self._map.set_dirstate_item(key, value)
+        def reset_state(
+            self,
+            filename,
+            wc_tracked=False,
+            p1_tracked=False,
+            p2_info=False,
+            has_meaningful_mtime=True,
+            parentfiledata=None,
+        ):
+            return self._map.reset_state(
+                filename,
+                wc_tracked,
+                p1_tracked,
+                p2_info,
+                has_meaningful_mtime,
+                parentfiledata,
+            )
--- a/mercurial/dirstateutils/docket.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/dirstateutils/docket.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import struct
 
@@ -29,7 +28,7 @@
 )
 
 
-class DirstateDocket(object):
+class DirstateDocket:
     data_filename_pattern = b'dirstate.%s'
 
     def __init__(self, parents, data_size, tree_metadata, uuid):
--- a/mercurial/dirstateutils/timestamp.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/dirstateutils/timestamp.py	Wed May 04 18:17:44 2022 +0200
@@ -3,7 +3,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import functools
 import os
--- a/mercurial/dirstateutils/v2.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/dirstateutils/v2.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import struct
 
@@ -126,7 +125,7 @@
 
 
 @attr.s
-class Node(object):
+class Node:
     path = attr.ib()
     entry = attr.ib()
     parent = attr.ib(default=None)
--- a/mercurial/discovery.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/discovery.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import functools
 
@@ -74,7 +73,7 @@
     return (list(common), anyinc, heads or list(srvheads))
 
 
-class outgoing(object):
+class outgoing:
     """Represents the result of a findcommonoutgoing() call.
 
     Members:
@@ -238,7 +237,7 @@
 
     knownnode = cl.hasnode  # do not use nodemap until it is filtered
     # A. register remote heads of branches which are in outgoing set
-    for branch, heads in pycompat.iteritems(remotemap):
+    for branch, heads in remotemap.items():
         # don't add head info about branches which we don't have locally
         if branch not in branches:
             continue
@@ -262,14 +261,14 @@
         repo,
         (
             (branch, heads[1])
-            for branch, heads in pycompat.iteritems(headssum)
+            for branch, heads in headssum.items()
             if heads[0] is not None
         ),
     )
     newmap.update(repo, (ctx.rev() for ctx in missingctx))
-    for branch, newheads in pycompat.iteritems(newmap):
+    for branch, newheads in newmap.items():
         headssum[branch][1][:] = newheads
-    for branch, items in pycompat.iteritems(headssum):
+    for branch, items in headssum.items():
         for l in items:
             if l is not None:
                 l.sort()
@@ -380,9 +379,7 @@
         headssum = _oldheadssummary(repo, remoteheads, outgoing, inc)
     pushop.pushbranchmap = headssum
     newbranches = [
-        branch
-        for branch, heads in pycompat.iteritems(headssum)
-        if heads[0] is None
+        branch for branch, heads in headssum.items() if heads[0] is None
     ]
     # 1. Check for new branches on the remote.
     if newbranches and not newbranch:  # new branch requires --new-branch
--- a/mercurial/dispatch.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/dispatch.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import, print_function
 
 import errno
 import getopt
@@ -54,7 +53,7 @@
 )
 
 
-class request(object):
+class request:
     def __init__(
         self,
         args,
@@ -150,93 +149,76 @@
     sys.exit(status & 255)
 
 
-if pycompat.ispy3:
-
-    def initstdio():
-        # stdio streams on Python 3 are io.TextIOWrapper instances proxying another
-        # buffer. These streams will normalize \n to \r\n by default. Mercurial's
-        # preferred mechanism for writing output (ui.write()) uses io.BufferedWriter
-        # instances, which write to the underlying stdio file descriptor in binary
-        # mode. ui.write() uses \n for line endings and no line ending normalization
-        # is attempted through this interface. This "just works," even if the system
-        # preferred line ending is not \n.
-        #
-        # But some parts of Mercurial (e.g. hooks) can still send data to sys.stdout
-        # and sys.stderr. They will inherit the line ending normalization settings,
-        # potentially causing e.g. \r\n to be emitted. Since emitting \n should
-        # "just work," here we change the sys.* streams to disable line ending
-        # normalization, ensuring compatibility with our ui type.
-
-        if sys.stdout is not None:
-            # write_through is new in Python 3.7.
-            kwargs = {
-                "newline": "\n",
-                "line_buffering": sys.stdout.line_buffering,
-            }
-            if util.safehasattr(sys.stdout, "write_through"):
-                # pytype: disable=attribute-error
-                kwargs["write_through"] = sys.stdout.write_through
-                # pytype: enable=attribute-error
-            sys.stdout = io.TextIOWrapper(
-                sys.stdout.buffer,
-                sys.stdout.encoding,
-                sys.stdout.errors,
-                **kwargs
-            )
+def initstdio():
+    # stdio streams on Python 3 are io.TextIOWrapper instances proxying another
+    # buffer. These streams will normalize \n to \r\n by default. Mercurial's
+    # preferred mechanism for writing output (ui.write()) uses io.BufferedWriter
+    # instances, which write to the underlying stdio file descriptor in binary
+    # mode. ui.write() uses \n for line endings and no line ending normalization
+    # is attempted through this interface. This "just works," even if the system
+    # preferred line ending is not \n.
+    #
+    # But some parts of Mercurial (e.g. hooks) can still send data to sys.stdout
+    # and sys.stderr. They will inherit the line ending normalization settings,
+    # potentially causing e.g. \r\n to be emitted. Since emitting \n should
+    # "just work," here we change the sys.* streams to disable line ending
+    # normalization, ensuring compatibility with our ui type.
 
-        if sys.stderr is not None:
-            kwargs = {
-                "newline": "\n",
-                "line_buffering": sys.stderr.line_buffering,
-            }
-            if util.safehasattr(sys.stderr, "write_through"):
-                # pytype: disable=attribute-error
-                kwargs["write_through"] = sys.stderr.write_through
-                # pytype: enable=attribute-error
-            sys.stderr = io.TextIOWrapper(
-                sys.stderr.buffer,
-                sys.stderr.encoding,
-                sys.stderr.errors,
-                **kwargs
-            )
+    if sys.stdout is not None:
+        # write_through is new in Python 3.7.
+        kwargs = {
+            "newline": "\n",
+            "line_buffering": sys.stdout.line_buffering,
+        }
+        if util.safehasattr(sys.stdout, "write_through"):
+            # pytype: disable=attribute-error
+            kwargs["write_through"] = sys.stdout.write_through
+            # pytype: enable=attribute-error
+        sys.stdout = io.TextIOWrapper(
+            sys.stdout.buffer, sys.stdout.encoding, sys.stdout.errors, **kwargs
+        )
 
-        if sys.stdin is not None:
-            # No write_through on read-only stream.
-            sys.stdin = io.TextIOWrapper(
-                sys.stdin.buffer,
-                sys.stdin.encoding,
-                sys.stdin.errors,
-                # None is universal newlines mode.
-                newline=None,
-                line_buffering=sys.stdin.line_buffering,
-            )
+    if sys.stderr is not None:
+        kwargs = {
+            "newline": "\n",
+            "line_buffering": sys.stderr.line_buffering,
+        }
+        if util.safehasattr(sys.stderr, "write_through"):
+            # pytype: disable=attribute-error
+            kwargs["write_through"] = sys.stderr.write_through
+            # pytype: enable=attribute-error
+        sys.stderr = io.TextIOWrapper(
+            sys.stderr.buffer, sys.stderr.encoding, sys.stderr.errors, **kwargs
+        )
 
-    def _silencestdio():
-        for fp in (sys.stdout, sys.stderr):
-            if fp is None:
-                continue
-            # Check if the file is okay
-            try:
-                fp.flush()
-                continue
-            except IOError:
-                pass
-            # Otherwise mark it as closed to silence "Exception ignored in"
-            # message emitted by the interpreter finalizer.
-            try:
-                fp.close()
-            except IOError:
-                pass
+    if sys.stdin is not None:
+        # No write_through on read-only stream.
+        sys.stdin = io.TextIOWrapper(
+            sys.stdin.buffer,
+            sys.stdin.encoding,
+            sys.stdin.errors,
+            # None is universal newlines mode.
+            newline=None,
+            line_buffering=sys.stdin.line_buffering,
+        )
 
 
-else:
-
-    def initstdio():
-        for fp in (sys.stdin, sys.stdout, sys.stderr):
-            procutil.setbinary(fp)
-
-    def _silencestdio():
-        pass
+def _silencestdio():
+    for fp in (sys.stdout, sys.stderr):
+        if fp is None:
+            continue
+        # Check if the file is okay
+        try:
+            fp.flush()
+            continue
+        except IOError:
+            pass
+        # Otherwise mark it as closed to silence "Exception ignored in"
+        # message emitted by the interpreter finalizer.
+        try:
+            fp.close()
+        except IOError:
+            pass
 
 
 def _formatargs(args):
@@ -575,7 +557,7 @@
     return r.sub(lambda x: replacemap[x.group()], cmd)
 
 
-class cmdalias(object):
+class cmdalias:
     def __init__(self, ui, name, definition, cmdtable, source):
         self.name = self.cmd = name
         self.cmdname = b''
@@ -590,7 +572,7 @@
 
         try:
             aliases, entry = cmdutil.findcmd(self.name, cmdtable)
-            for alias, e in pycompat.iteritems(cmdtable):
+            for alias, e in cmdtable.items():
                 if e is entry:
                     self.cmd = alias
                     break
@@ -758,7 +740,7 @@
                 raise
 
 
-class lazyaliasentry(object):
+class lazyaliasentry:
     """like a typical command entry (func, opts, help), but is lazy"""
 
     def __init__(self, ui, name, definition, cmdtable, source):
--- a/mercurial/encoding.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/encoding.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import, print_function
 
 import locale
 import os
@@ -47,8 +46,7 @@
 
 _sysstr = pycompat.sysstr
 
-if pycompat.ispy3:
-    unichr = chr
+unichr = chr
 
 # These unicode characters are ignored by HFS+ (Apple Technote 1150,
 # "Unicode Subtleties"), so we need to ignore them in some places for
@@ -79,10 +77,8 @@
 
 # encoding.environ is provided read-only, which may not be used to modify
 # the process environment
-_nativeenviron = not pycompat.ispy3 or os.supports_bytes_environ
-if not pycompat.ispy3:
-    environ = os.environ  # re-exports
-elif _nativeenviron:
+_nativeenviron = os.supports_bytes_environ
+if _nativeenviron:
     environ = os.environb  # re-exports
 else:
     # preferred encoding isn't known yet; use utf-8 to avoid unicode error
@@ -99,7 +95,7 @@
 # cp65001 is a Windows variant of utf-8, which isn't supported on Python 2.
 # No idea if it should be rewritten to the canonical name 'utf-8' on Python 3.
 # https://bugs.python.org/issue13216
-if pycompat.iswindows and not pycompat.ispy3:
+if pycompat.iswindows:
     _encodingrewrites[b'cp65001'] = b'utf-8'
 
 try:
@@ -271,21 +267,9 @@
 # converter functions between native str and byte string. use these if the
 # character encoding is not aware (e.g. exception message) or is known to
 # be locale dependent (e.g. date formatting.)
-if pycompat.ispy3:
-    strtolocal = unitolocal
-    strfromlocal = unifromlocal
-    strmethod = unimethod
-else:
-
-    def strtolocal(s):
-        # type: (str) -> bytes
-        return s  # pytype: disable=bad-return-type
-
-    def strfromlocal(s):
-        # type: (bytes) -> str
-        return s  # pytype: disable=bad-return-type
-
-    strmethod = pycompat.identity
+strtolocal = unitolocal
+strfromlocal = unifromlocal
+strmethod = unimethod
 
 
 def lower(s):
@@ -345,7 +329,7 @@
 if not _nativeenviron:
     # now encoding and helper functions are available, recreate the environ
     # dict to be exported to other modules
-    if pycompat.iswindows and pycompat.ispy3:
+    if pycompat.iswindows:
 
         class WindowsEnviron(dict):
             """`os.environ` normalizes environment variables to uppercase on windows"""
@@ -361,36 +345,34 @@
 
 DRIVE_RE = re.compile(b'^[a-z]:')
 
-if pycompat.ispy3:
-    # os.getcwd() on Python 3 returns string, but it has os.getcwdb() which
-    # returns bytes.
-    if pycompat.iswindows:
-        # Python 3 on Windows issues a DeprecationWarning about using the bytes
-        # API when os.getcwdb() is called.
-        #
-        # Additionally, py3.8+ uppercases the drive letter when calling
-        # os.path.realpath(), which is used on ``repo.root``.  Since those
-        # strings are compared in various places as simple strings, also call
-        # realpath here.  See https://bugs.python.org/issue40368
-        #
-        # However this is not reliable, so lets explicitly make this drive
-        # letter upper case.
-        #
-        # note: we should consider dropping realpath here since it seems to
-        # change the semantic of `getcwd`.
+# os.getcwd() on Python 3 returns string, but it has os.getcwdb() which
+# returns bytes.
+if pycompat.iswindows:
+    # Python 3 on Windows issues a DeprecationWarning about using the bytes
+    # API when os.getcwdb() is called.
+    #
+    # Additionally, py3.8+ uppercases the drive letter when calling
+    # os.path.realpath(), which is used on ``repo.root``.  Since those
+    # strings are compared in various places as simple strings, also call
+    # realpath here.  See https://bugs.python.org/issue40368
+    #
+    # However this is not reliable, so lets explicitly make this drive
+    # letter upper case.
+    #
+    # note: we should consider dropping realpath here since it seems to
+    # change the semantic of `getcwd`.
 
-        def getcwd():
-            cwd = os.getcwd()  # re-exports
-            cwd = os.path.realpath(cwd)
-            cwd = strtolocal(cwd)
-            if DRIVE_RE.match(cwd):
-                cwd = cwd[0:1].upper() + cwd[1:]
-            return cwd
+    def getcwd():
+        cwd = os.getcwd()  # re-exports
+        cwd = os.path.realpath(cwd)
+        cwd = strtolocal(cwd)
+        if DRIVE_RE.match(cwd):
+            cwd = cwd[0:1].upper() + cwd[1:]
+        return cwd
 
-    else:
-        getcwd = os.getcwdb  # re-exports
+
 else:
-    getcwd = os.getcwd  # re-exports
+    getcwd = os.getcwdb  # re-exports
 
 # How to treat ambiguous-width characters. Set to 'wide' to treat as wide.
 _wide = _sysstr(
@@ -528,7 +510,7 @@
     return u + ellipsis
 
 
-class normcasespecs(object):
+class normcasespecs:
     """what a platform's normcase does to ASCII strings
 
     This is specified per platform, and should be consistent with what normcase
@@ -601,10 +583,7 @@
 
 # We need to decode/encode U+DCxx codes transparently since invalid UTF-8
 # bytes are mapped to that range.
-if pycompat.ispy3:
-    _utf8strict = r'surrogatepass'
-else:
-    _utf8strict = r'strict'
+_utf8strict = r'surrogatepass'
 
 _utf8len = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 3, 4]
 
--- a/mercurial/error.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/error.py	Wed May 04 18:17:44 2022 +0200
@@ -11,7 +11,6 @@
 imports.
 """
 
-from __future__ import absolute_import
 
 import difflib
 
@@ -40,7 +39,7 @@
     return b'(%s)' % b', '.join(b"'%s'" % pycompat.bytestr(a) for a in exc.args)
 
 
-class Hint(object):
+class Hint:
     """Mix-in to provide a hint of an error
 
     This should come first in the inheritance list to consume a hint and
@@ -69,14 +68,12 @@
     def __bytes__(self):
         return self.message
 
-    if pycompat.ispy3:
-
-        def __str__(self):
-            # type: () -> str
-            # the output would be unreadable if the message was translated,
-            # but do not replace it with encoding.strfromlocal(), which
-            # may raise another exception.
-            return pycompat.sysstr(self.__bytes__())
+    def __str__(self):
+        # type: () -> str
+        # the output would be unreadable if the message was translated,
+        # but do not replace it with encoding.strfromlocal(), which
+        # may raise another exception.
+        return pycompat.sysstr(self.__bytes__())
 
     def format(self):
         # type: () -> bytes
--- a/mercurial/exchange.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/exchange.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import collections
 import weakref
@@ -223,7 +222,7 @@
     return forcebundle1 or not op.remote.capable(b'bundle2')
 
 
-class pushoperation(object):
+class pushoperation:
     """A object that represent a single push operation
 
     Its purpose is to carry push related state and very common operations.
@@ -806,7 +805,7 @@
             bundler.newpart(b'check:heads', data=iter(pushop.remoteheads))
         else:
             affected = set()
-            for branch, heads in pycompat.iteritems(pushop.pushbranchmap):
+            for branch, heads in pushop.pushbranchmap.items():
                 remoteheads, newheads, unsyncedheads, discardedheads = heads
                 if remoteheads is not None:
                     remote = set(remoteheads)
@@ -855,7 +854,7 @@
         checks = {p: [] for p in phases.allphases}
         checks[phases.public].extend(pushop.remotephases.publicheads)
         checks[phases.draft].extend(pushop.remotephases.draftroots)
-        if any(pycompat.itervalues(checks)):
+        if any(checks.values()):
             for phase in checks:
                 checks[phase].sort()
             checkdata = phases.binaryencode(checks)
@@ -1117,7 +1116,7 @@
 
         part = bundler.newpart(b'pushvars')
 
-        for key, value in pycompat.iteritems(shellvars):
+        for key, value in shellvars.items():
             part.addparam(key, value, mandatory=False)
 
 
@@ -1372,7 +1371,7 @@
                 pushop.bkresult = 1
 
 
-class pulloperation(object):
+class pulloperation:
     """A object that represent a single pull operation
 
     It purpose is to carry pull related state and very common operation.
--- a/mercurial/extensions.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/extensions.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import ast
 import collections
@@ -74,7 +73,7 @@
     try:
         mod = _extensions[name]
     except KeyError:
-        for k, v in pycompat.iteritems(_extensions):
+        for k, v in _extensions.items():
             if k.endswith(b'.' + name) or k.endswith(b'/' + name):
                 mod = v
                 break
@@ -171,7 +170,7 @@
 
 def _validatecmdtable(ui, cmdtable):
     """Check if extension commands have required attributes"""
-    for c, e in pycompat.iteritems(cmdtable):
+    for c, e in cmdtable.items():
         f = e[0]
         missing = [a for a in _cmdfuncattrs if not util.safehasattr(f, a)]
         if not missing:
@@ -579,7 +578,7 @@
     '''
     assert callable(wrapper)
     aliases, entry = cmdutil.findcmd(command, table)
-    for alias, e in pycompat.iteritems(table):
+    for alias, e in table.items():
         if e is entry:
             key = alias
             break
@@ -622,7 +621,7 @@
         raise AttributeError("type '%s' has no property '%s'" % (cls, propname))
 
 
-class wrappedfunction(object):
+class wrappedfunction:
     '''context manager for temporarily wrapping a function'''
 
     def __init__(self, container, funcname, wrapper):
@@ -756,7 +755,7 @@
         if name in exts or name in _order or name == b'__init__':
             continue
         exts[name] = path
-    for name, path in pycompat.iteritems(_disabledextensions):
+    for name, path in _disabledextensions.items():
         # If no path was provided for a disabled extension (e.g. "color=!"),
         # don't replace the path we already found by the scan above.
         if path:
@@ -818,7 +817,7 @@
 
         return {
             name: gettext(desc)
-            for name, desc in pycompat.iteritems(__index__.docs)
+            for name, desc in __index__.docs.items()
             if name not in _order
         }
     except (ImportError, AttributeError):
@@ -829,10 +828,10 @@
         return {}
 
     exts = {}
-    for name, path in pycompat.iteritems(paths):
+    for name, path in paths.items():
         doc = _disabledhelp(path)
         if doc and name != b'__index__':
-            exts[name] = doc.splitlines()[0]
+            exts[name] = stringutil.firstline(doc)
 
     return exts
 
@@ -876,7 +875,7 @@
         a = node.args[0]
         if isinstance(a, ast.Str):
             name = pycompat.sysbytes(a.s)
-        elif pycompat.ispy3 and isinstance(a, ast.Bytes):
+        elif isinstance(a, ast.Bytes):
             name = a.s
         else:
             continue
@@ -918,7 +917,7 @@
         ext = _finddisabledcmd(ui, cmd, cmd, path, strict=strict)
     if not ext:
         # otherwise, interrogate each extension until there's a match
-        for name, path in pycompat.iteritems(paths):
+        for name, path in paths.items():
             ext = _finddisabledcmd(ui, cmd, name, path, strict=strict)
             if ext:
                 break
@@ -936,16 +935,14 @@
         assert doc is not None  # help pytype
         if shortname:
             ename = ename.split(b'.')[-1]
-        exts[ename] = doc.splitlines()[0].strip()
+        exts[ename] = stringutil.firstline(doc).strip()
 
     return exts
 
 
 def notloaded():
     '''return short names of extensions that failed to load'''
-    return [
-        name for name, mod in pycompat.iteritems(_extensions) if mod is None
-    ]
+    return [name for name, mod in _extensions.items() if mod is None]
 
 
 def moduleversion(module):
--- a/mercurial/exthelper.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/exthelper.py	Wed May 04 18:17:44 2022 +0200
@@ -9,20 +9,18 @@
 ### Extension helper                                              ###
 #####################################################################
 
-from __future__ import absolute_import
 
 from . import (
     commands,
     error,
     extensions,
-    pycompat,
     registrar,
 )
 
 from hgdemandimport import tracing
 
 
-class exthelper(object):
+class exthelper:
     """Helper for modular extension setup
 
     A single helper should be instantiated for each module of an
@@ -115,7 +113,7 @@
         self._extcommandwrappers.extend(other._extcommandwrappers)
         self._functionwrappers.extend(other._functionwrappers)
         self.cmdtable.update(other.cmdtable)
-        for section, items in pycompat.iteritems(other.configtable):
+        for section, items in other.configtable.items():
             if section in self.configtable:
                 self.configtable[section].update(items)
             else:
--- a/mercurial/fancyopts.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/fancyopts.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import abc
 import functools
@@ -205,7 +204,7 @@
     return parsedopts, parsedargs
 
 
-class customopt(object):  # pytype: disable=ignored-metaclass
+class customopt:  # pytype: disable=ignored-metaclass
     """Manage defaults and mutations for any type of opt."""
 
     __metaclass__ = abc.ABCMeta
--- a/mercurial/filelog.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/filelog.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from .i18n import _
 from .node import nullrev
@@ -25,7 +24,7 @@
 
 
 @interfaceutil.implementer(repository.ifilestorage)
-class filelog(object):
+class filelog:
     def __init__(self, opener, path):
         self._revlog = revlog.revlog(
             opener,
@@ -33,6 +32,7 @@
             target=(revlog_constants.KIND_FILELOG, path),
             radix=b'/'.join((b'data', path)),
             censorable=True,
+            canonical_parent_order=False,  # see comment in revlog.py
         )
         # Full name of the user visible file, relative to the repository root.
         # Used by LFS.
@@ -208,6 +208,7 @@
             return 0
 
         # XXX if self.read(node).startswith("\1\n"), this returns (size+4)
+        # XXX See also basefilectx.cmp.
         return self._revlog.size(rev)
 
     def cmp(self, node, text):
--- a/mercurial/filemerge.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/filemerge.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import contextlib
 import os
@@ -85,7 +84,7 @@
 )
 
 
-class absentfilectx(object):
+class absentfilectx:
     """Represents a file that's ostensibly in a context but is actually not
     present in it.
 
@@ -849,7 +848,7 @@
 
     props = {b'ctx': ctx}
     templateresult = template.renderdefault(props)
-    input.label_detail = templateresult.splitlines()[0]  # split for safety
+    input.label_detail = stringutil.firstline(templateresult)  # avoid '\n'
 
 
 def _populate_label_details(repo, inputs, tool=None):
@@ -1052,6 +1051,7 @@
             markerstyle = internalmarkerstyle
 
         if mergetype == fullmerge:
+            _run_partial_resolution_tools(repo, local, other, base)
             # conflict markers generated by premerge will use 'detailed'
             # settings if either ui.mergemarkers or the tool's mergemarkers
             # setting is 'detailed'. This way tools can have basic labels in
@@ -1116,6 +1116,73 @@
             backup.remove()
 
 
+def _run_partial_resolution_tools(repo, local, other, base):
+    """Runs partial-resolution tools on the three inputs and updates them."""
+    ui = repo.ui
+    # Tuples of (order, name, executable path, args)
+    tools = []
+    seen = set()
+    section = b"partial-merge-tools"
+    for k, v in ui.configitems(section):
+        name = k.split(b'.')[0]
+        if name in seen:
+            continue
+        patterns = ui.configlist(section, b'%s.patterns' % name, [])
+        is_match = True
+        if patterns:
+            m = match.match(repo.root, b'', patterns)
+            is_match = m(local.fctx.path())
+        if is_match:
+            order = ui.configint(section, b'%s.order' % name, 0)
+            executable = ui.config(section, b'%s.executable' % name, name)
+            args = ui.config(section, b'%s.args' % name)
+            tools.append((order, name, executable, args))
+
+    if not tools:
+        return
+    # Sort in configured order (first in tuple)
+    tools.sort()
+
+    files = [
+        (b"local", local.fctx.path(), local.text()),
+        (b"base", base.fctx.path(), base.text()),
+        (b"other", other.fctx.path(), other.text()),
+    ]
+
+    with _maketempfiles(files) as temppaths:
+        localpath, basepath, otherpath = temppaths
+
+        for order, name, executable, args in tools:
+            cmd = procutil.shellquote(executable)
+            replace = {
+                b'local': localpath,
+                b'base': basepath,
+                b'other': otherpath,
+            }
+            args = util.interpolate(
+                br'\$',
+                replace,
+                args,
+                lambda s: procutil.shellquote(util.localpath(s)),
+            )
+
+            cmd = b'%s %s' % (cmd, args)
+            r = ui.system(cmd, cwd=repo.root, blockedtag=b'partial-mergetool')
+            if r:
+                raise error.StateError(
+                    b'partial merge tool %s exited with code %d' % (name, r)
+                )
+            local_text = util.readfile(localpath)
+            other_text = util.readfile(otherpath)
+            if local_text == other_text:
+                # No need to run other tools if all conflicts have been resolved
+                break
+
+        local.set_text(local_text)
+        base.set_text(util.readfile(basepath))
+        other.set_text(other_text)
+
+
 def _haltmerge():
     msg = _(b'merge halted after failed merge (see hg resolve)')
     raise error.InterventionRequired(msg)
@@ -1199,7 +1266,7 @@
 
 def loadinternalmerge(ui, extname, registrarobj):
     """Load internal merge tool from specified registrarobj"""
-    for name, func in pycompat.iteritems(registrarobj._table):
+    for name, func in registrarobj._table.items():
         fullname = b':' + name
         internals[fullname] = func
         internals[b'internal:' + name] = func
--- a/mercurial/fileset.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/fileset.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import errno
 import re
@@ -504,7 +503,7 @@
 }
 
 
-class matchctx(object):
+class matchctx:
     def __init__(self, basectx, ctx, cwd, badfn=None):
         self._basectx = basectx
         self.ctx = ctx
@@ -614,7 +613,7 @@
 
 def loadpredicate(ui, extname, registrarobj):
     """Load fileset predicates from specified registrarobj"""
-    for name, func in pycompat.iteritems(registrarobj._table):
+    for name, func in registrarobj._table.items():
         symbols[name] = func
 
 
--- a/mercurial/filesetlang.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/filesetlang.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from .i18n import _
 from .pycompat import getattr
--- a/mercurial/formatter.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/formatter.py	Wed May 04 18:17:44 2022 +0200
@@ -105,11 +105,11 @@
 baz: foo, bar
 """
 
-from __future__ import absolute_import, print_function
 
 import contextlib
 import itertools
 import os
+import pickle
 
 from .i18n import _
 from .node import (
@@ -133,8 +133,6 @@
     stringutil,
 )
 
-pickle = util.pickle
-
 
 def isprintable(obj):
     """Check if the given object can be directly passed in to formatter's
@@ -143,10 +141,10 @@
     Returns False if the object is unsupported or must be pre-processed by
     formatdate(), formatdict(), or formatlist().
     """
-    return isinstance(obj, (type(None), bool, int, pycompat.long, float, bytes))
+    return isinstance(obj, (type(None), bool, int, int, float, bytes))
 
 
-class _nullconverter(object):
+class _nullconverter:
     '''convert non-primitive data types to be processed by formatter'''
 
     # set to True if context object should be stored as item
@@ -177,7 +175,7 @@
         return list(data)
 
 
-class baseformatter(object):
+class baseformatter:
 
     # set to True if the formater output a strict format that does not support
     # arbitrary output in the stream.
@@ -295,11 +293,11 @@
 def _iteritems(data):
     '''iterate key-value pairs in stable order'''
     if isinstance(data, dict):
-        return sorted(pycompat.iteritems(data))
+        return sorted(data.items())
     return data
 
 
-class _plainconverter(object):
+class _plainconverter:
     '''convert non-primitive data types to text'''
 
     storecontext = False
@@ -454,7 +452,7 @@
         self._out.write(b"\n]\n")
 
 
-class _templateconverter(object):
+class _templateconverter:
     '''convert non-primitive data types to be processed by templater'''
 
     storecontext = True
@@ -543,7 +541,7 @@
 
 
 @attr.s(frozen=True)
-class templatespec(object):
+class templatespec:
     ref = attr.ib()
     tmpl = attr.ib()
     mapfile = attr.ib()
@@ -560,8 +558,7 @@
 
 
 def literal_templatespec(tmpl):
-    if pycompat.ispy3:
-        assert not isinstance(tmpl, str), b'tmpl must not be a str'
+    assert not isinstance(tmpl, str), b'tmpl must not be a str'
     return templatespec(b'', tmpl, None)
 
 
--- a/mercurial/graphmod.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/graphmod.py	Wed May 04 18:17:44 2022 +0200
@@ -17,7 +17,6 @@
 Data depends on type.
 """
 
-from __future__ import absolute_import
 
 from .node import nullrev
 from .thirdparty import attr
@@ -359,7 +358,7 @@
 
 
 @attr.s
-class asciistate(object):
+class asciistate:
     """State of ascii() graph rendering"""
 
     seen = attr.ib(init=False, default=attr.Factory(list))
--- a/mercurial/grep.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/grep.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import difflib
 import errno
@@ -36,7 +35,7 @@
         yield linenum, mstart - lstart, mend - lstart, body[lstart:lend]
 
 
-class linestate(object):
+class linestate:
     def __init__(self, line, linenum, colstart, colend):
         self.line = line
         self.linenum = linenum
@@ -80,7 +79,7 @@
                 yield (b'+', b[i])
 
 
-class grepsearcher(object):
+class grepsearcher:
     """Search files and revisions for lines matching the given pattern
 
     Options:
--- a/mercurial/hbisect.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/hbisect.py	Wed May 04 18:17:44 2022 +0200
@@ -8,7 +8,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import collections
 import contextlib
--- a/mercurial/help.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/help.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import itertools
 import re
@@ -38,6 +37,7 @@
 from .utils import (
     compression,
     resourceutil,
+    stringutil,
 )
 
 _exclkeywords = {
@@ -126,7 +126,7 @@
     '''return a text listing of the given extensions'''
     rst = []
     if exts:
-        for name, desc in sorted(pycompat.iteritems(exts)):
+        for name, desc in sorted(exts.items()):
             if not showdeprecated and any(w in desc for w in _exclkeywords):
                 continue
             rst.append(b'%s:%s: %s\n' % (b' ' * indent, name, desc))
@@ -281,7 +281,7 @@
             name = names[0]
             if not filtertopic(ui, name):
                 results[b'topics'].append((names[0], header))
-    for cmd, entry in pycompat.iteritems(commands.table):
+    for cmd, entry in commands.table.items():
         if len(entry) == 3:
             summary = entry[2]
         else:
@@ -290,35 +290,34 @@
         func = entry[0]
         docs = _(pycompat.getdoc(func)) or b''
         if kw in cmd or lowercontains(summary) or lowercontains(docs):
-            doclines = docs.splitlines()
-            if doclines:
-                summary = doclines[0]
+            if docs:
+                summary = stringutil.firstline(docs)
             cmdname = cmdutil.parsealiases(cmd)[0]
             if filtercmd(ui, cmdname, func, kw, docs):
                 continue
             results[b'commands'].append((cmdname, summary))
     for name, docs in itertools.chain(
-        pycompat.iteritems(extensions.enabled(False)),
-        pycompat.iteritems(extensions.disabled()),
+        extensions.enabled(False).items(),
+        extensions.disabled().items(),
     ):
         if not docs:
             continue
         name = name.rpartition(b'.')[-1]
         if lowercontains(name) or lowercontains(docs):
             # extension docs are already translated
-            results[b'extensions'].append((name, docs.splitlines()[0]))
+            results[b'extensions'].append((name, stringutil.firstline(docs)))
         try:
             mod = extensions.load(ui, name, b'')
         except ImportError:
             # debug message would be printed in extensions.load()
             continue
-        for cmd, entry in pycompat.iteritems(getattr(mod, 'cmdtable', {})):
+        for cmd, entry in getattr(mod, 'cmdtable', {}).items():
             if kw in cmd or (len(entry) > 2 and lowercontains(entry[2])):
                 cmdname = cmdutil.parsealiases(cmd)[0]
                 func = entry[0]
                 cmddoc = pycompat.getdoc(func)
                 if cmddoc:
-                    cmddoc = gettext(cmddoc).splitlines()[0]
+                    cmddoc = stringutil.firstline(gettext(cmddoc))
                 else:
                     cmddoc = _(b'(no help text available)')
                 if filtercmd(ui, cmdname, func, kw, cmddoc):
@@ -608,7 +607,7 @@
             # Abuse latin1 to use textwrap.dedent() on bytes.
             text = textwrap.dedent(text.decode('latin1')).encode('latin1')
         lines = text.splitlines()
-        doclines = [(lines[0])]
+        doclines = [lines[0]]
         for l in lines[1:]:
             # Stop once we find some Python doctest
             if l.strip().startswith(b'>>>'):
@@ -665,7 +664,7 @@
     h = {}
     # Command -> string showing synonyms
     syns = {}
-    for c, e in pycompat.iteritems(cmdtable):
+    for c, e in cmdtable.items():
         fs = cmdutil.parsealiases(c)
         f = fs[0]
         syns[f] = fs
@@ -678,7 +677,7 @@
         doc = gettext(doc)
         if not doc:
             doc = _(b"(no help text available)")
-        h[f] = doc.splitlines()[0].rstrip()
+        h[f] = stringutil.firstline(doc).rstrip()
 
         cat = getattr(func, 'helpcategory', None) or (
             registrar.command.CATEGORY_NONE
@@ -1044,7 +1043,7 @@
         cmd, ext, doc = extensions.disabledcmd(
             ui, name, ui.configbool(b'ui', b'strict')
         )
-        doc = doc.splitlines()[0]
+        doc = stringutil.firstline(doc)
 
         rst = listexts(
             _(b"'%s' is provided by the following extension:") % cmd,
--- a/mercurial/hg.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/hg.py	Wed May 04 18:17:44 2022 +0200
@@ -6,7 +6,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import errno
 import os
@@ -1535,7 +1534,7 @@
 ]
 
 
-class cachedlocalrepo(object):
+class cachedlocalrepo:
     """Holds a localrepository that can be cached and reused."""
 
     def __init__(self, repo):
--- a/mercurial/hgweb/__init__.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/hgweb/__init__.py	Wed May 04 18:17:44 2022 +0200
@@ -6,7 +6,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import os
 
@@ -37,7 +36,7 @@
     - list of virtual:real tuples (multi-repo view)
     """
 
-    if isinstance(config, pycompat.unicode):
+    if isinstance(config, str):
         raise error.ProgrammingError(
             b'Mercurial only supports encoded strings: %r' % config
         )
@@ -55,7 +54,7 @@
     return hgwebdir_mod.hgwebdir(config, baseui=baseui)
 
 
-class httpservice(object):
+class httpservice:
     def __init__(self, ui, app, opts):
         self.ui = ui
         self.app = app
--- a/mercurial/hgweb/common.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/hgweb/common.py	Wed May 04 18:17:44 2022 +0200
@@ -6,7 +6,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import base64
 import errno
@@ -116,7 +115,7 @@
         self.message = message
 
 
-class continuereader(object):
+class continuereader:
     """File object wrapper to handle HTTP 100-continue.
 
     This is used by servers so they automatically handle Expect: 100-continue
--- a/mercurial/hgweb/hgweb_mod.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/hgweb/hgweb_mod.py	Wed May 04 18:17:44 2022 +0200
@@ -6,7 +6,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import contextlib
 import os
@@ -111,7 +110,7 @@
     return templateutil.mappinglist(reversed(breadcrumb))
 
 
-class requestcontext(object):
+class requestcontext:
     """Holds state/context for an individual request.
 
     Servers can be multi-threaded. Holding state on the WSGI application
@@ -236,7 +235,7 @@
         return self.res.sendresponse()
 
 
-class hgweb(object):
+class hgweb:
     """HTTP server for individual repositories.
 
     Instances of this class serve HTTP responses for a particular
@@ -413,7 +412,7 @@
 
             if cmd == b'archive':
                 fn = req.qsparams[b'node']
-                for type_, spec in pycompat.iteritems(webutil.archivespecs):
+                for type_, spec in webutil.archivespecs.items():
                     ext = spec[2]
                     if fn.endswith(ext):
                         req.qsparams[b'node'] = fn[: -len(ext)]
--- a/mercurial/hgweb/hgwebdir_mod.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/hgweb/hgwebdir_mod.py	Wed May 04 18:17:44 2022 +0200
@@ -6,7 +6,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import gc
 import os
@@ -269,7 +268,7 @@
     return templateutil.mappinggenerator(_indexentriesgen, args=args)
 
 
-class hgwebdir(object):
+class hgwebdir:
     """HTTP server for multiple repositories.
 
     Given a configuration, different repositories will be served depending
@@ -461,12 +460,9 @@
                 if real:
                     # Re-parse the WSGI environment to take into account our
                     # repository path component.
-                    uenv = req.rawenv
-                    if pycompat.ispy3:
-                        uenv = {
-                            k.decode('latin1'): v
-                            for k, v in pycompat.iteritems(uenv)
-                        }
+                    uenv = {
+                        k.decode('latin1'): v for k, v in req.rawenv.items()
+                    }
                     req = requestmod.parserequestfromenv(
                         uenv,
                         reponame=virtualrepo,
--- a/mercurial/hgweb/request.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/hgweb/request.py	Wed May 04 18:17:44 2022 +0200
@@ -6,7 +6,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 # import wsgiref.validate
 
@@ -22,7 +21,7 @@
 )
 
 
-class multidict(object):
+class multidict:
     """A dict like object that can store multiple values for a key.
 
     Used to store parsed request parameters.
@@ -78,11 +77,11 @@
         return vals[0]
 
     def asdictoflists(self):
-        return {k: list(v) for k, v in pycompat.iteritems(self._items)}
+        return {k: list(v) for k, v in self._items.items()}
 
 
 @attr.s(frozen=True)
-class parsedrequest(object):
+class parsedrequest:
     """Represents a parsed WSGI request.
 
     Contains both parsed parameters as well as a handle on the input stream.
@@ -161,24 +160,22 @@
     # TODO enable this once we fix internal violations.
     # wsgiref.validate.check_environ(env)
 
-    # PEP-0333 states that environment keys and values are native strings
-    # (bytes on Python 2 and str on Python 3). The code points for the Unicode
-    # strings on Python 3 must be between \00000-\000FF. We deal with bytes
-    # in Mercurial, so mass convert string keys and values to bytes.
-    if pycompat.ispy3:
+    # PEP-0333 states that environment keys and values are native strings.
+    # The code points for the Unicode strings on Python 3 must be between
+    # \00000-\000FF. We deal with bytes in Mercurial, so mass convert string
+    # keys and values to bytes.
+    def tobytes(s):
+        if not isinstance(s, str):
+            return s
+        if pycompat.iswindows:
+            # This is what mercurial.encoding does for os.environ on
+            # Windows.
+            return encoding.strtolocal(s)
+        else:
+            # This is what is documented to be used for os.environ on Unix.
+            return pycompat.fsencode(s)
 
-        def tobytes(s):
-            if not isinstance(s, str):
-                return s
-            if pycompat.iswindows:
-                # This is what mercurial.encoding does for os.environ on
-                # Windows.
-                return encoding.strtolocal(s)
-            else:
-                # This is what is documented to be used for os.environ on Unix.
-                return pycompat.fsencode(s)
-
-        env = {tobytes(k): tobytes(v) for k, v in pycompat.iteritems(env)}
+    env = {tobytes(k): tobytes(v) for k, v in env.items()}
 
     # Some hosting solutions are emulating hgwebdir, and dispatching directly
     # to an hgweb instance using this environment variable.  This was always
@@ -312,7 +309,7 @@
     # perform case normalization for us. We just rewrite underscore to dash
     # so keys match what likely went over the wire.
     headers = []
-    for k, v in pycompat.iteritems(env):
+    for k, v in env.items():
         if k.startswith(b'HTTP_'):
             headers.append((k[len(b'HTTP_') :].replace(b'_', b'-'), v))
 
@@ -358,7 +355,7 @@
     )
 
 
-class offsettrackingwriter(object):
+class offsettrackingwriter:
     """A file object like object that is append only and tracks write count.
 
     Instances are bound to a callable. This callable is called with data
@@ -391,7 +388,7 @@
         return self._offset
 
 
-class wsgiresponse(object):
+class wsgiresponse:
     """Represents a response to a WSGI request.
 
     A response consists of a status line, headers, and a body.
--- a/mercurial/hgweb/server.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/hgweb/server.py	Wed May 04 18:17:44 2022 +0200
@@ -6,7 +6,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import errno
 import importlib
@@ -53,7 +52,7 @@
     return urlreq.unquote(path), query
 
 
-class _error_logger(object):
+class _error_logger:
     def __init__(self, handler):
         self.handler = handler
 
@@ -186,18 +185,11 @@
         env['REMOTE_ADDR'] = self.client_address[0]
         env['QUERY_STRING'] = query or ''
 
-        if pycompat.ispy3:
-            if self.headers.get_content_type() is None:
-                env['CONTENT_TYPE'] = self.headers.get_default_type()
-            else:
-                env['CONTENT_TYPE'] = self.headers.get_content_type()
-            length = self.headers.get('content-length')
+        if self.headers.get_content_type() is None:
+            env['CONTENT_TYPE'] = self.headers.get_default_type()
         else:
-            if self.headers.typeheader is None:
-                env['CONTENT_TYPE'] = self.headers.type
-            else:
-                env['CONTENT_TYPE'] = self.headers.typeheader
-            length = self.headers.getheader('content-length')
+            env['CONTENT_TYPE'] = self.headers.get_content_type()
+        length = self.headers.get('content-length')
         if length:
             env['CONTENT_LENGTH'] = length
         for header in [
@@ -351,7 +343,7 @@
         _mixin = socketserver.ForkingMixIn
     else:
 
-        class _mixin(object):
+        class _mixin:
             pass
 
 
--- a/mercurial/hgweb/webcommands.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/hgweb/webcommands.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import copy
 import mimetypes
@@ -47,7 +46,7 @@
 commands = {}
 
 
-class webcommand(object):
+class webcommand:
     """Decorator used to register a web command handler.
 
     The decorator takes as its positional arguments the name/path the
@@ -564,7 +563,7 @@
     l = len(path)
     abspath = b"/" + path
 
-    for full, n in pycompat.iteritems(mf):
+    for full, n in mf.items():
         # the virtual path (working copy path) used for the full
         # (repository) path
         f = decodepath(full)
@@ -1521,7 +1520,7 @@
 
         early, other = [], []
         primary = lambda s: s.partition(b'|')[0]
-        for c, e in pycompat.iteritems(commands.table):
+        for c, e in commands.table.items():
             doc = _getdoc(e)
             if b'DEPRECATED' in doc or c.startswith(b'debug'):
                 continue
--- a/mercurial/hgweb/webutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/hgweb/webutil.py	Wed May 04 18:17:44 2022 +0200
@@ -6,7 +6,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import copy
 import difflib
@@ -57,7 +56,7 @@
     allowed = ui.configlist(b'web', b'allow-archive', untrusted=True)
     archives = []
 
-    for typ, spec in pycompat.iteritems(archivespecs):
+    for typ, spec in archivespecs.items():
         if typ in allowed or ui.configbool(
             b'web', b'allow' + typ, untrusted=True
         ):
@@ -100,7 +99,7 @@
         step *= 10
 
 
-class revnav(object):
+class revnav:
     def __init__(self, repo):
         """Navigation generation object
 
@@ -864,7 +863,7 @@
 
     def itermaps(self, context):
         separator = self._start
-        for key, value in sorted(pycompat.iteritems(self._vars)):
+        for key, value in sorted(self._vars.items()):
             yield {
                 b'name': key,
                 b'value': pycompat.bytestr(value),
--- a/mercurial/hgweb/wsgicgi.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/hgweb/wsgicgi.py	Wed May 04 18:17:44 2022 +0200
@@ -8,7 +8,6 @@
 # This was originally copied from the public domain code at
 # http://www.python.org/dev/peps/pep-0333/#the-server-gateway-side
 
-from __future__ import absolute_import
 
 import os
 
@@ -24,7 +23,7 @@
     procutil.setbinary(procutil.stdin)
     procutil.setbinary(procutil.stdout)
 
-    environ = dict(pycompat.iteritems(os.environ))  # re-exports
+    environ = dict(os.environ.items())  # re-exports
     environ.setdefault('PATH_INFO', '')
     if environ.get('SERVER_SOFTWARE', '').startswith('Microsoft-IIS'):
         # IIS includes script_name in PATH_INFO
--- a/mercurial/hgweb/wsgiheaders.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/hgweb/wsgiheaders.py	Wed May 04 18:17:44 2022 +0200
@@ -9,7 +9,6 @@
 
 # Regular expression that matches `special' characters in parameters, the
 # existence of which force quoting of the parameter value.
-from __future__ import absolute_import, print_function
 
 import re
 
@@ -30,7 +29,7 @@
         return param
 
 
-class Headers(object):
+class Headers:
     """Manage a collection of HTTP response headers"""
 
     def __init__(self, headers=None):
--- a/mercurial/hook.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/hook.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import contextlib
 import errno
@@ -167,7 +166,7 @@
     else:
         env[b'HGPLAIN'] = b''
 
-    for k, v in pycompat.iteritems(args):
+    for k, v in args.items():
         # transaction changes can accumulate MBs of data, so skip it
         # for external hooks
         if k == b'changes':
--- a/mercurial/httpconnection.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/httpconnection.py	Wed May 04 18:17:44 2022 +0200
@@ -8,7 +8,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import os
 
@@ -27,7 +26,7 @@
 urlreq = util.urlreq
 
 # moved here from url.py to avoid a cycle
-class httpsendfile(object):
+class httpsendfile:
     """This is a wrapper around the objects returned by python's "open".
 
     Its purpose is to send file-like objects via HTTP.
@@ -94,7 +93,7 @@
     bestuser = None
     bestlen = 0
     bestauth = None
-    for group, auth in pycompat.iteritems(groups):
+    for group, auth in groups.items():
         if user and user != auth.get(b'username', user):
             # If a username was set in the URI, the entry username
             # must either match it or be unset
--- a/mercurial/httppeer.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/httppeer.py	Wed May 04 18:17:44 2022 +0200
@@ -6,7 +6,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import errno
 import io
@@ -14,6 +13,7 @@
 import socket
 import struct
 
+from concurrent import futures
 from .i18n import _
 from .pycompat import getattr
 from . import (
@@ -62,7 +62,7 @@
     return result
 
 
-class _multifile(object):
+class _multifile:
     def __init__(self, *fileobjs):
         for f in fileobjs:
             if not util.safehasattr(f, b'length'):
@@ -231,15 +231,6 @@
     return req, cu, qs
 
 
-def _reqdata(req):
-    """Get request data, if any. If no data, returns None."""
-    if pycompat.ispy3:
-        return req.data
-    if not req.has_data():
-        return None
-    return req.get_data()
-
-
 def sendrequest(ui, opener, req):
     """Send a prepared HTTP request.
 
@@ -274,7 +265,7 @@
                 % b'  %d bytes of commands arguments in headers'
                 % hgargssize
             )
-        data = _reqdata(req)
+        data = req.data
         if data is not None:
             length = getattr(data, 'length', None)
             if length is None:
@@ -538,12 +529,12 @@
         raise exception
 
 
-class queuedcommandfuture(pycompat.futures.Future):
+class queuedcommandfuture(futures.Future):
     """Wraps result() on command futures to trigger submission on call."""
 
     def result(self, timeout=None):
         if self.done():
-            return pycompat.futures.Future.result(self, timeout)
+            return futures.Future.result(self, timeout)
 
         self._peerexecutor.sendcommands()
 
--- a/mercurial/i18n.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/i18n.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import gettext as gettextmod
 import locale
@@ -86,9 +85,9 @@
 
     cache = _msgcache.setdefault(encoding.encoding, {})
     if message not in cache:
-        if type(message) is pycompat.unicode:
+        if type(message) is str:
             # goofy unicode docstrings in test
-            paragraphs = message.split(u'\n\n')  # type: List[pycompat.unicode]
+            paragraphs = message.split(u'\n\n')  # type: List[str]
         else:
             # should be ascii, but we have unicode docstrings in test, which
             # are converted to utf-8 bytes on Python 3.
--- a/mercurial/interfaces/dirstate.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/interfaces/dirstate.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function
-
 import contextlib
 
 from . import util as interfaceutil
@@ -63,6 +61,9 @@
         used to get real file paths. Use vfs functions instead.
         """
 
+    def get_entry(path):
+        """return a DirstateItem for the associated path"""
+
     def pathto(f, cwd=None):
         pass
 
--- a/mercurial/interfaces/repository.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/interfaces/repository.py	Wed May 04 18:17:44 2022 +0200
@@ -6,7 +6,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from ..i18n import _
 from .. import error
@@ -389,7 +388,7 @@
 
 
 @interfaceutil.implementer(ipeerbase)
-class peer(object):
+class peer:
     """Base class for peer repositories."""
 
     limitedarguments = False
--- a/mercurial/interfaces/util.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/interfaces/util.py	Wed May 04 18:17:44 2022 +0200
@@ -9,7 +9,6 @@
 # bookkeeping for declaring interfaces. So, we use stubs for various
 # zope.interface primitives unless instructed otherwise.
 
-from __future__ import absolute_import
 
 from .. import encoding
 
@@ -21,11 +20,11 @@
     implementer = zi.implementer
 else:
 
-    class Attribute(object):
+    class Attribute:
         def __init__(self, __name__, __doc__=b''):
             pass
 
-    class Interface(object):
+    class Interface:
         def __init__(
             self, name, bases=(), attrs=None, __doc__=None, __module__=None
         ):
--- a/mercurial/keepalive.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/keepalive.py	Wed May 04 18:17:44 2022 +0200
@@ -82,7 +82,6 @@
 
 # $Id: keepalive.py,v 1.14 2006/04/04 21:00:32 mstenner Exp $
 
-from __future__ import absolute_import, print_function
 
 import collections
 import errno
@@ -108,7 +107,7 @@
 DEBUG = None
 
 
-class ConnectionManager(object):
+class ConnectionManager:
     """
     The connection manager must be able to:
       * keep track of all existing
@@ -171,7 +170,7 @@
             return dict(self._hostmap)
 
 
-class KeepAliveHandler(object):
+class KeepAliveHandler:
     def __init__(self, timeout=None):
         self._cm = ConnectionManager()
         self._timeout = timeout
@@ -194,7 +193,7 @@
 
     def close_all(self):
         """close all open connections"""
-        for host, conns in pycompat.iteritems(self._cm.get_all()):
+        for host, conns in self._cm.get_all().items():
             for h in conns:
                 self._cm.remove(h)
                 h.close()
@@ -399,12 +398,8 @@
     # modification from socket.py
 
     def __init__(self, sock, debuglevel=0, strict=0, method=None):
-        extrakw = {}
-        if not pycompat.ispy3:
-            extrakw['strict'] = True
-            extrakw['buffering'] = True
         httplib.HTTPResponse.__init__(
-            self, sock, debuglevel=debuglevel, method=method, **extrakw
+            self, sock, debuglevel=debuglevel, method=method
         )
         self.fileno = sock.fileno
         self.code = None
@@ -794,7 +789,7 @@
     global DEBUG
     dbbackup = DEBUG
 
-    class FakeLogger(object):
+    class FakeLogger:
         def debug(self, msg, *args):
             print(msg % args)
 
--- a/mercurial/linelog.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/linelog.py	Wed May 04 18:17:44 2022 +0200
@@ -18,7 +18,6 @@
 deletion is performed on the file, a jump instruction is used to patch
 in a new body of annotate information.
 """
-from __future__ import absolute_import, print_function
 
 import abc
 import struct
@@ -34,7 +33,7 @@
 
 
 @attr.s
-class lineinfo(object):
+class lineinfo:
     # Introducing revision of this line.
     rev = attr.ib()
     # Line number for this line in its introducing revision.
@@ -44,7 +43,7 @@
 
 
 @attr.s
-class annotateresult(object):
+class annotateresult:
     rev = attr.ib()
     lines = attr.ib()
     _eof = attr.ib()
@@ -53,7 +52,7 @@
         return iter(self.lines)
 
 
-class _llinstruction(object):  # pytype: disable=ignored-metaclass
+class _llinstruction:  # pytype: disable=ignored-metaclass
 
     __metaclass__ = abc.ABCMeta
 
@@ -234,7 +233,7 @@
     raise NotImplementedError(b'Unimplemented opcode %r' % opcode)
 
 
-class linelog(object):
+class linelog:
     """Efficient cache for per-line history information."""
 
     def __init__(self, program=None, maxrev=0):
--- a/mercurial/localrepo.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/localrepo.py	Wed May 04 18:17:44 2022 +0200
@@ -6,7 +6,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import errno
 import functools
@@ -16,6 +15,7 @@
 import time
 import weakref
 
+from concurrent import futures
 from .i18n import _
 from .node import (
     bin,
@@ -251,7 +251,7 @@
 
 
 @interfaceutil.implementer(repository.ipeercommandexecutor)
-class localcommandexecutor(object):
+class localcommandexecutor:
     def __init__(self, peer):
         self._peer = peer
         self._sent = False
@@ -278,7 +278,7 @@
         # method on the peer and return a resolved future.
         fn = getattr(self._peer, pycompat.sysstr(command))
 
-        f = pycompat.futures.Future()
+        f = futures.Future()
 
         try:
             result = fn(**pycompat.strkwargs(args))
@@ -1215,7 +1215,7 @@
 
 
 @interfaceutil.implementer(repository.ilocalrepositoryfilestorage)
-class revlogfilestorage(object):
+class revlogfilestorage:
     """File storage when using revlogs."""
 
     def file(self, path):
@@ -1226,7 +1226,7 @@
 
 
 @interfaceutil.implementer(repository.ilocalrepositoryfilestorage)
-class revlognarrowfilestorage(object):
+class revlognarrowfilestorage:
     """File storage when using revlogs and narrow files."""
 
     def file(self, path):
@@ -1259,7 +1259,7 @@
 
 
 @interfaceutil.implementer(repository.ilocalrepositorymain)
-class localrepository(object):
+class localrepository:
     """Main class for representing local repositories.
 
     All local repositories are instances of this class.
@@ -2044,7 +2044,7 @@
 
         # This simplifies its cache management by having one decorated
         # function (this one) and the rest simply fetch things from it.
-        class tagscache(object):
+        class tagscache:
             def __init__(self):
                 # These two define the set of tags for this repository. tags
                 # maps tag name to node; tagtypes maps tag name to 'global' or
@@ -2068,7 +2068,7 @@
         else:
             tags = self._tagscache.tags
         rev = self.changelog.rev
-        for k, v in pycompat.iteritems(tags):
+        for k, v in tags.items():
             try:
                 # ignore tags to unknown nodes
                 rev(v)
@@ -2103,13 +2103,12 @@
         # writing to the cache), but the rest of Mercurial wants them in
         # local encoding.
         tags = {}
-        for (name, (node, hist)) in pycompat.iteritems(alltags):
+        for (name, (node, hist)) in alltags.items():
             if node != self.nullid:
                 tags[encoding.tolocal(name)] = node
         tags[b'tip'] = self.changelog.tip()
         tagtypes = {
-            encoding.tolocal(name): value
-            for (name, value) in pycompat.iteritems(tagtypes)
+            encoding.tolocal(name): value for (name, value) in tagtypes.items()
         }
         return (tags, tagtypes)
 
@@ -2128,7 +2127,7 @@
         '''return a list of tags ordered by revision'''
         if not self._tagscache.tagslist:
             l = []
-            for t, n in pycompat.iteritems(self.tags()):
+            for t, n in self.tags().items():
                 l.append((self.changelog.rev(n), t, n))
             self._tagscache.tagslist = [(t, n) for r, t, n in sorted(l)]
 
@@ -2138,9 +2137,9 @@
         '''return the tags associated with a node'''
         if not self._tagscache.nodetagscache:
             nodetagscache = {}
-            for t, n in pycompat.iteritems(self._tagscache.tags):
+            for t, n in self._tagscache.tags.items():
                 nodetagscache.setdefault(n, []).append(t)
-            for tags in pycompat.itervalues(nodetagscache):
+            for tags in nodetagscache.values():
                 tags.sort()
             self._tagscache.nodetagscache = nodetagscache
         return self._tagscache.nodetagscache.get(node, [])
@@ -2256,7 +2255,7 @@
                 mf = matchmod.match(self.root, b'', [pat])
                 fn = None
                 params = cmd
-                for name, filterfn in pycompat.iteritems(self._datafilters):
+                for name, filterfn in self._datafilters.items():
                     if cmd.startswith(name):
                         fn = filterfn
                         params = cmd[len(name) :].lstrip()
@@ -3914,7 +3913,7 @@
     #
     # But we have to allow the close() method because some constructors
     # of repos call close() on repo references.
-    class poisonedrepository(object):
+    class poisonedrepository:
         def __getattribute__(self, item):
             if item == 'close':
                 return object.__getattribute__(self, item)
--- a/mercurial/lock.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/lock.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import contextlib
 import errno
@@ -174,7 +173,7 @@
     return l
 
 
-class lock(object):
+class lock:
     """An advisory lock held by one process to control access to a set
     of files.  Non-cooperating processes or incorrectly written scripts
     can ignore Mercurial's locking scheme and stomp all over the
--- a/mercurial/logcmdutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/logcmdutil.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import itertools
 import os
@@ -228,7 +227,7 @@
             )
 
 
-class changesetdiffer(object):
+class changesetdiffer:
     """Generate diff of changeset with pre-configured filtering functions"""
 
     def _makefilematcher(self, ctx):
@@ -262,7 +261,7 @@
     return b' '.join(labels)
 
 
-class changesetprinter(object):
+class changesetprinter:
     '''show changeset information when templating not requested.'''
 
     def __init__(self, ui, repo, differ=None, diffopts=None, buffered=False):
@@ -328,7 +327,7 @@
         if branch != b'default':
             self.ui.write(columns[b'branch'] % branch, label=b'log.branch')
 
-        for nsname, ns in pycompat.iteritems(self.repo.names):
+        for nsname, ns in self.repo.names.items():
             # branches has special logic already handled above, so here we just
             # skip it
             if nsname == b'branches':
@@ -417,7 +416,7 @@
                 self.ui.write(b"\n\n")
             else:
                 self.ui.write(
-                    columns[b'summary'] % description.splitlines()[0],
+                    columns[b'summary'] % stringutil.firstline(description),
                     label=b'log.summary',
                 )
         self.ui.write(b"\n")
@@ -707,7 +706,7 @@
 
 
 @attr.s
-class walkopts(object):
+class walkopts:
     """Options to configure a set of revisions and file matcher factory
     to scan revision/file history
     """
@@ -992,7 +991,7 @@
         opts[b'_patslog'] = list(wopts.pats)
 
     expr = []
-    for op, val in sorted(pycompat.iteritems(opts)):
+    for op, val in sorted(opts.items()):
         if not val:
             continue
         revop, listop = _opt2logrevset[op]
--- a/mercurial/logexchange.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/logexchange.py	Wed May 04 18:17:44 2022 +0200
@@ -6,12 +6,10 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from .node import hex
 
 from . import (
-    pycompat,
     util,
     vfs as vfsmod,
 )
@@ -78,7 +76,7 @@
         if oldpath != remotepath:
             f.write(b'%s\0%s\0%s\n' % (node, oldpath, rname))
 
-    for name, node in sorted(pycompat.iteritems(names)):
+    for name, node in sorted(names.items()):
         if nametype == b"branches":
             for n in node:
                 f.write(b'%s\0%s\0%s\n' % (n, remotepath, name))
@@ -160,7 +158,7 @@
     with remoterepo.commandexecutor() as e:
         branchmap = e.callcommand(b'branchmap', {}).result()
 
-    for branch, nodes in pycompat.iteritems(branchmap):
+    for branch, nodes in branchmap.items():
         bmap[branch] = []
         for node in nodes:
             if node in repo and not repo[node].obsolete():
--- a/mercurial/loggingutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/loggingutil.py	Wed May 04 18:17:44 2022 +0200
@@ -6,7 +6,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import errno
 
@@ -74,7 +73,7 @@
     return b'*' in tracked or event in tracked
 
 
-class filelogger(object):
+class filelogger:
     """Basic logger backed by physical file with optional rotation"""
 
     def __init__(self, vfs, name, tracked, maxfiles=0, maxsize=0):
@@ -105,7 +104,7 @@
             )
 
 
-class fileobjectlogger(object):
+class fileobjectlogger:
     """Basic logger backed by file-like object"""
 
     def __init__(self, fp, tracked):
@@ -130,7 +129,7 @@
             )
 
 
-class proxylogger(object):
+class proxylogger:
     """Forward log events to another logger to be set later"""
 
     def __init__(self):
--- a/mercurial/lsprof.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/lsprof.py	Wed May 04 18:17:44 2022 +0200
@@ -1,10 +1,7 @@
-from __future__ import absolute_import, print_function
-
 import _lsprof
 import sys
 
 from .pycompat import getattr
-from . import pycompat
 
 Profiler = _lsprof.Profiler
 
@@ -25,7 +22,7 @@
     return Stats(p.getstats())
 
 
-class Stats(object):
+class Stats:
     """XXX docstring"""
 
     def __init__(self, data):
@@ -120,13 +117,11 @@
 
 def label(code):
     if isinstance(code, str):
-        if sys.version_info.major >= 3:
-            code = code.encode('latin-1')
-        return code
+        return code.encode('latin-1')
     try:
         mname = _fn2mod[code.co_filename]
     except KeyError:
-        for k, v in list(pycompat.iteritems(sys.modules)):
+        for k, v in list(sys.modules.items()):
             if v is None:
                 continue
             if not isinstance(getattr(v, '__file__', None), str):
@@ -139,7 +134,4 @@
 
     res = '%s:%d(%s)' % (mname, code.co_firstlineno, code.co_name)
 
-    if sys.version_info.major >= 3:
-        res = res.encode('latin-1')
-
-    return res
+    return res.encode('latin-1')
--- a/mercurial/lsprofcalltree.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/lsprofcalltree.py	Wed May 04 18:17:44 2022 +0200
@@ -10,7 +10,6 @@
 of the GNU General Public License, incorporated herein by reference.
 """
 
-from __future__ import absolute_import
 
 from . import pycompat
 
@@ -27,7 +26,7 @@
         )
 
 
-class KCacheGrind(object):
+class KCacheGrind:
     def __init__(self, profiler):
         self.data = profiler.getstats()
         self.out_file = None
--- a/mercurial/mail.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/mail.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import email
 import email.charset
@@ -261,9 +260,11 @@
                 )
             )
     else:
-        if not procutil.findexe(method):
+        command = procutil.shellsplit(method)
+        command = command[0] if command else b''
+        if not (command and procutil.findexe(command)):
             raise error.Abort(
-                _(b'%r specified as email transport, but not in PATH') % method
+                _(b'%r specified as email transport, but not in PATH') % command
             )
 
 
@@ -468,43 +469,28 @@
     return mimetextqp(s, 'plain', cs)
 
 
-if pycompat.ispy3:
-
-    Generator = email.generator.BytesGenerator
-
-    def parse(fp):
-        # type: (Any) -> email.message.Message
-        ep = email.parser.Parser()
-        # disable the "universal newlines" mode, which isn't binary safe.
-        # I have no idea if ascii/surrogateescape is correct, but that's
-        # what the standard Python email parser does.
-        fp = io.TextIOWrapper(
-            fp, encoding='ascii', errors='surrogateescape', newline=chr(10)
-        )
-        try:
-            return ep.parse(fp)
-        finally:
-            fp.detach()
-
-    def parsebytes(data):
-        # type: (bytes) -> email.message.Message
-        ep = email.parser.BytesParser()
-        return ep.parsebytes(data)
+Generator = email.generator.BytesGenerator
 
 
-else:
-
-    Generator = email.generator.Generator
+def parse(fp):
+    # type: (Any) -> email.message.Message
+    ep = email.parser.Parser()
+    # disable the "universal newlines" mode, which isn't binary safe.
+    # I have no idea if ascii/surrogateescape is correct, but that's
+    # what the standard Python email parser does.
+    fp = io.TextIOWrapper(
+        fp, encoding='ascii', errors='surrogateescape', newline=chr(10)
+    )
+    try:
+        return ep.parse(fp)
+    finally:
+        fp.detach()
 
-    def parse(fp):
-        # type: (Any) -> email.message.Message
-        ep = email.parser.Parser()
-        return ep.parse(fp)
 
-    def parsebytes(data):
-        # type: (str) -> email.message.Message
-        ep = email.parser.Parser()
-        return ep.parsestr(data)
+def parsebytes(data):
+    # type: (bytes) -> email.message.Message
+    ep = email.parser.BytesParser()
+    return ep.parsebytes(data)
 
 
 def headdecode(s):
--- a/mercurial/manifest.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/manifest.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import heapq
 import itertools
@@ -85,7 +84,7 @@
     return b''.join(lines)
 
 
-class lazymanifestiter(object):
+class lazymanifestiter:
     def __init__(self, lm):
         self.pos = 0
         self.lm = lm
@@ -108,7 +107,7 @@
     __next__ = next
 
 
-class lazymanifestiterentries(object):
+class lazymanifestiterentries:
     def __init__(self, lm):
         self.lm = lm
         self.pos = 0
@@ -159,7 +158,7 @@
 _manifestflags = {b'', b'l', b't', b'x'}
 
 
-class _lazymanifest(object):
+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.
@@ -474,7 +473,7 @@
 
 
 @interfaceutil.implementer(repository.imanifestdict)
-class manifestdict(object):
+class manifestdict:
     def __init__(self, nodelen, data=b''):
         self._nodelen = nodelen
         self._lm = _lazymanifest(nodelen, data)
@@ -797,7 +796,7 @@
 
 
 @interfaceutil.implementer(repository.imanifestdict)
-class treemanifest(object):
+class treemanifest:
     def __init__(self, nodeconstants, dir=b'', text=b''):
         self._dir = dir
         self.nodeconstants = nodeconstants
@@ -827,9 +826,7 @@
     def _loadalllazy(self):
         selfdirs = self._dirs
         subpath = self._subpath
-        for d, (node, readsubtree, docopy) in pycompat.iteritems(
-            self._lazydirs
-        ):
+        for d, (node, readsubtree, docopy) in self._lazydirs.items():
             if docopy:
                 selfdirs[d] = readsubtree(subpath(d), node).copy()
             else:
@@ -868,11 +865,11 @@
           differs, load it in both
         """
         toloadlazy = []
-        for d, v1 in pycompat.iteritems(t1._lazydirs):
+        for d, v1 in t1._lazydirs.items():
             v2 = t2._lazydirs.get(d)
             if not v2 or v2[0] != v1[0]:
                 toloadlazy.append(d)
-        for d, v1 in pycompat.iteritems(t2._lazydirs):
+        for d, v1 in t2._lazydirs.items():
             if d not in t1._lazydirs:
                 toloadlazy.append(d)
 
@@ -954,7 +951,7 @@
             if p in self._files:
                 yield self._subpath(p), n
             else:
-                for f, sn in pycompat.iteritems(n):
+                for f, sn in n.items():
                     yield f, sn
 
     iteritems = items
@@ -1105,11 +1102,10 @@
             def _copyfunc(s):
                 self._load()
                 s._lazydirs = {
-                    d: (n, r, True)
-                    for d, (n, r, c) in pycompat.iteritems(self._lazydirs)
+                    d: (n, r, True) for d, (n, r, c) in self._lazydirs.items()
                 }
                 sdirs = s._dirs
-                for d, v in pycompat.iteritems(self._dirs):
+                for d, v in self._dirs.items():
                     sdirs[d] = v.copy()
                 s._files = dict.copy(self._files)
                 s._flags = dict.copy(self._flags)
@@ -1137,7 +1133,7 @@
             t1._load()
             t2._load()
             self._loaddifflazy(t1, t2)
-            for d, m1 in pycompat.iteritems(t1._dirs):
+            for d, m1 in t1._dirs.items():
                 if d in t2._dirs:
                     m2 = t2._dirs[d]
                     _filesnotin(m1, m2)
@@ -1250,7 +1246,7 @@
                 ret._flags[fn] = self._flags[fn]
 
         visit = self._loadchildrensetlazy(visit)
-        for dir, subm in pycompat.iteritems(self._dirs):
+        for dir, subm in self._dirs.items():
             if visit and dir[:-1] not in visit:
                 continue
             m = subm._matches_inner(match)
@@ -1295,15 +1291,15 @@
             t2._load()
             self._loaddifflazy(t1, t2)
 
-            for d, m1 in pycompat.iteritems(t1._dirs):
+            for d, m1 in t1._dirs.items():
                 m2 = t2._dirs.get(d, emptytree)
                 stack.append((m1, m2))
 
-            for d, m2 in pycompat.iteritems(t2._dirs):
+            for d, m2 in t2._dirs.items():
                 if d not in t1._dirs:
                     stack.append((emptytree, m2))
 
-            for fn, n1 in pycompat.iteritems(t1._files):
+            for fn, n1 in t1._files.items():
                 fl1 = t1._flags.get(fn, b'')
                 n2 = t2._files.get(fn, None)
                 fl2 = t2._flags.get(fn, b'')
@@ -1312,7 +1308,7 @@
                 elif clean:
                     result[t1._subpath(fn)] = None
 
-            for fn, n2 in pycompat.iteritems(t2._files):
+            for fn, n2 in t2._files.items():
                 if fn not in t1._files:
                     fl2 = t2._flags.get(fn, b'')
                     result[t2._subpath(fn)] = ((None, b''), (n2, fl2))
@@ -1362,9 +1358,7 @@
         """
         self._load()
         flags = self.flags
-        lazydirs = [
-            (d[:-1], v[0], b't') for d, v in pycompat.iteritems(self._lazydirs)
-        ]
+        lazydirs = [(d[:-1], v[0], b't') for d, v in self._lazydirs.items()]
         dirs = [(d[:-1], self._dirs[d]._node, b't') for d in self._dirs]
         files = [(f, self._files[f], flags(f)) for f in self._files]
         return _text(sorted(dirs + files + lazydirs))
@@ -1393,7 +1387,7 @@
         visit = self._loadchildrensetlazy(visit)
         if visit == b'this' or visit == b'all':
             visit = None
-        for d, subm in pycompat.iteritems(self._dirs):
+        for d, subm in self._dirs.items():
             if visit and d[:-1] not in visit:
                 continue
             subp1 = getnode(m1, d)
@@ -1416,7 +1410,7 @@
         self._load()
         # OPT: use visitchildrenset to avoid loading everything.
         self._loadalllazy()
-        for d, subm in pycompat.iteritems(self._dirs):
+        for d, subm in self._dirs.items():
             for subtree in subm.walksubtrees(matcher=matcher):
                 yield subtree
 
@@ -1556,7 +1550,7 @@
 
 
 @interfaceutil.implementer(repository.imanifeststorage)
-class manifestrevlog(object):
+class manifestrevlog:
     """A revlog that stores manifest texts. This is responsible for caching the
     full-text manifest contents.
     """
@@ -1914,7 +1908,7 @@
 
 
 @interfaceutil.implementer(repository.imanifestlog)
-class manifestlog(object):
+class manifestlog:
     """A collection class representing the collection of manifest snapshots
     referenced by commits in the repository.
 
@@ -2013,7 +2007,7 @@
 
 
 @interfaceutil.implementer(repository.imanifestrevisionwritable)
-class memmanifestctx(object):
+class memmanifestctx:
     def __init__(self, manifestlog):
         self._manifestlog = manifestlog
         self._manifestdict = manifestdict(manifestlog.nodeconstants.nodelen)
@@ -2043,7 +2037,7 @@
 
 
 @interfaceutil.implementer(repository.imanifestrevisionstored)
-class manifestctx(object):
+class manifestctx:
     """A class representing a single revision of a manifest, including its
     contents, its parent revs, and its linkrev.
     """
@@ -2123,7 +2117,7 @@
 
 
 @interfaceutil.implementer(repository.imanifestrevisionwritable)
-class memtreemanifestctx(object):
+class memtreemanifestctx:
     def __init__(self, manifestlog, dir=b''):
         self._manifestlog = manifestlog
         self._dir = dir
@@ -2158,7 +2152,7 @@
 
 
 @interfaceutil.implementer(repository.imanifestrevisionstored)
-class treemanifestctx(object):
+class treemanifestctx:
     def __init__(self, manifestlog, dir, node):
         self._manifestlog = manifestlog
         self._dir = dir
@@ -2249,7 +2243,7 @@
             m0 = self._manifestlog.get(self._dir, store.node(r0)).read()
             m1 = self.read()
             md = treemanifest(self._manifestlog.nodeconstants, dir=self._dir)
-            for f, ((n0, fl0), (n1, fl1)) in pycompat.iteritems(m0.diff(m1)):
+            for f, ((n0, fl0), (n1, fl1)) in m0.diff(m1).items():
                 if n1:
                     md[f] = n1
                     if fl1:
--- a/mercurial/match.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/match.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import, print_function
 
 import bisect
 import copy
@@ -383,7 +382,7 @@
     return kindpats
 
 
-class basematcher(object):
+class basematcher:
     def __init__(self, badfn=None):
         if badfn is not None:
             self.bad = badfn
@@ -584,10 +583,7 @@
     if b'' in prefix_set:
         return True
 
-    if pycompat.ispy3:
-        sl = ord(b'/')
-    else:
-        sl = '/'
+    sl = ord(b'/')
 
     # We already checked that path isn't in prefix_set exactly, so
     # `path[len(pf)] should never raise IndexError.
@@ -663,7 +659,7 @@
 # This is basically a reimplementation of pathutil.dirs that stores the
 # children instead of just a count of them, plus a small optional optimization
 # to avoid some directories we don't need.
-class _dirchildren(object):
+class _dirchildren:
     def __init__(self, paths, onlyinclude=None):
         self._dirs = {}
         self._onlyinclude = onlyinclude or []
@@ -1615,7 +1611,7 @@
     patterns = []
 
     fp = open(filepath, b'rb')
-    for lineno, line in enumerate(util.iterfile(fp), start=1):
+    for lineno, line in enumerate(fp, start=1):
         if b"#" in line:
             global _commentre
             if not _commentre:
@@ -1642,7 +1638,7 @@
             continue
 
         linesyntax = syntax
-        for s, rels in pycompat.iteritems(syntaxes):
+        for s, rels in syntaxes.items():
             if line.startswith(rels):
                 linesyntax = rels
                 line = line[len(rels) :]
--- a/mercurial/mdiff.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/mdiff.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import re
 import struct
@@ -38,7 +37,7 @@
 
 
 # TODO: this looks like it could be an attrs, which might help pytype
-class diffopts(object):
+class diffopts:
     """context is the number of context lines
     text treats all files as text
     showfunc enables diff -p output
--- a/mercurial/merge.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/merge.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import collections
 import errno
@@ -67,7 +66,7 @@
     )
 
 
-class _unknowndirschecker(object):
+class _unknowndirschecker:
     """
     Look for any unknown files or directories that may have a path conflict
     with a file.  If any path prefix of the file exists as a file or link,
@@ -538,7 +537,7 @@
             raise error.StateError(msg % f)
 
 
-class mergeresult(object):
+class mergeresult:
     """An object representing result of merging manifests.
 
     It has information about what actions need to be performed on dirstate
@@ -626,9 +625,7 @@
                     args, msg = self._actionmapping[a][f]
                     yield f, args, msg
             else:
-                for f, (args, msg) in pycompat.iteritems(
-                    self._actionmapping[a]
-                ):
+                for f, (args, msg) in self._actionmapping[a].items():
                     yield f, args, msg
 
     def len(self, actions=None):
@@ -644,10 +641,10 @@
 
     def filemap(self, sort=False):
         if sorted:
-            for key, val in sorted(pycompat.iteritems(self._filemapping)):
+            for key, val in sorted(self._filemapping.items()):
                 yield key, val
         else:
-            for key, val in pycompat.iteritems(self._filemapping):
+            for key, val in self._filemapping.items():
                 yield key, val
 
     def addcommitinfo(self, filename, key, value):
@@ -672,15 +669,15 @@
         """returns a dictionary of actions to be perfomed with action as key
         and a list of files and related arguments as values"""
         res = collections.defaultdict(list)
-        for a, d in pycompat.iteritems(self._actionmapping):
-            for f, (args, msg) in pycompat.iteritems(d):
+        for a, d in self._actionmapping.items():
+            for f, (args, msg) in d.items():
                 res[a].append((f, args, msg))
         return res
 
     def setactions(self, actions):
         self._filemapping = actions
         self._actionmapping = collections.defaultdict(dict)
-        for f, (act, data, msg) in pycompat.iteritems(self._filemapping):
+        for f, (act, data, msg) in self._filemapping.items():
             self._actionmapping[act][f] = data, msg
 
     def hasconflicts(self):
@@ -787,7 +784,7 @@
         relevantfiles = set(ma.diff(m2).keys())
 
         # For copied and moved files, we need to add the source file too.
-        for copykey, copyvalue in pycompat.iteritems(branch_copies1.copy):
+        for copykey, copyvalue in branch_copies1.copy.items():
             if copyvalue in relevantfiles:
                 relevantfiles.add(copykey)
         for movedirkey in branch_copies1.movewithdir:
@@ -797,7 +794,7 @@
 
     diff = m1.diff(m2, match=matcher)
 
-    for f, ((n1, fl1), (n2, fl2)) in pycompat.iteritems(diff):
+    for f, ((n1, fl1), (n2, fl2)) in diff.items():
         if n1 and n2:  # file exists on both local and remote side
             if f not in ma:
                 # TODO: what if they're renamed from different sources?
@@ -1470,7 +1467,7 @@
 
 
 @attr.s(frozen=True)
-class updateresult(object):
+class updateresult:
     updatedcount = attr.ib()
     mergedcount = attr.ib()
     removedcount = attr.ib()
@@ -1512,7 +1509,7 @@
     ms = wctx.mergestate(clean=True)
     ms.start(wctx.p1().node(), mctx.node(), labels)
 
-    for f, op in pycompat.iteritems(mresult.commitinfo):
+    for f, op in mresult.commitinfo.items():
         # the other side of filenode was choosen while merging, store this in
         # mergestate so that it can be reused on commit
         ms.addcommitinfo(f, op)
@@ -2073,7 +2070,7 @@
                 _checkcollision(repo, wc.manifest(), mresult)
 
         # divergent renames
-        for f, fl in sorted(pycompat.iteritems(mresult.diverge)):
+        for f, fl in sorted(mresult.diverge.items()):
             repo.ui.warn(
                 _(
                     b"note: possible conflict - %s was renamed "
@@ -2085,7 +2082,7 @@
                 repo.ui.warn(b" %s\n" % nf)
 
         # rename and delete
-        for f, fl in sorted(pycompat.iteritems(mresult.renamedelete)):
+        for f, fl in sorted(mresult.renamedelete.items()):
             repo.ui.warn(
                 _(
                     b"note: possible conflict - %s was deleted "
@@ -2125,7 +2122,7 @@
 
         if updatedirstate:
             if extraactions:
-                for k, acts in pycompat.iteritems(extraactions):
+                for k, acts in extraactions.items():
                     for a in acts:
                         mresult.addfile(a[0], k, *a[1:])
                     if k == mergestatemod.ACTION_GET and wantfiledata:
@@ -2196,10 +2193,10 @@
                         getfiledata = None
                     else:
                         now_sec = now[0]
-                        for f, m in pycompat.iteritems(getfiledata):
+                        for f, m in getfiledata.items():
                             if m is not None and m[2][0] >= now_sec:
                                 ambiguous_mtime[f] = (m[0], m[1], None)
-                        for f, m in pycompat.iteritems(ambiguous_mtime):
+                        for f, m in ambiguous_mtime.items():
                             getfiledata[f] = m
 
                 repo.setparents(fp1, fp2)
--- a/mercurial/mergestate.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/mergestate.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import collections
 import errno
 import shutil
@@ -15,7 +13,6 @@
 from . import (
     error,
     filemerge,
-    pycompat,
     util,
 )
 from .utils import hashutil
@@ -103,7 +100,7 @@
 CHANGE_MODIFIED = b'modified'
 
 
-class MergeAction(object):
+class MergeAction:
     """represent an "action" merge need to take for a given file
 
     Attributes:
@@ -197,7 +194,7 @@
 )
 
 
-class _mergestate_base(object):
+class _mergestate_base:
     """track 3-way merge state of individual files
 
     The merge state is stored on disk when needed. Two files are used: one with
@@ -365,7 +362,7 @@
     def unresolved(self):
         """Obtain the paths of unresolved files."""
 
-        for f, entry in pycompat.iteritems(self._state):
+        for f, entry in self._state.items():
             if entry[0] in (
                 MERGE_RECORD_UNRESOLVED,
                 MERGE_RECORD_UNRESOLVED_PATH,
@@ -469,7 +466,7 @@
         """return counts for updated, merged and removed files in this
         session"""
         updated, merged, removed = 0, 0, 0
-        for r, action in pycompat.itervalues(self._results):
+        for r, action in self._results.values():
             if r is None:
                 updated += 1
             elif r == 0:
@@ -492,7 +489,7 @@
             ACTION_ADD_MODIFIED: [],
             ACTION_GET: [],
         }
-        for f, (r, action) in pycompat.iteritems(self._results):
+        for f, (r, action) in self._results.items():
             if action is not None:
                 actions[action].append((f, None, b"merge result"))
         return actions
@@ -692,7 +689,7 @@
         # the type of state that is stored, and capital-letter records are used
         # to prevent older versions of Mercurial that do not support the feature
         # from loading them.
-        for filename, v in pycompat.iteritems(self._state):
+        for filename, v in self._state.items():
             if v[0] in (
                 MERGE_RECORD_UNRESOLVED_PATH,
                 MERGE_RECORD_RESOLVED_PATH,
@@ -716,9 +713,9 @@
             else:
                 # Normal files.  These are stored in 'F' records.
                 records.append((RECORD_MERGED, b'\0'.join([filename] + v)))
-        for filename, extras in sorted(pycompat.iteritems(self._stateextras)):
+        for filename, extras in sorted(self._stateextras.items()):
             rawextras = b'\0'.join(
-                b'%s\0%s' % (k, v) for k, v in pycompat.iteritems(extras)
+                b'%s\0%s' % (k, v) for k, v in extras.items()
             )
             records.append(
                 (RECORD_FILE_VALUES, b'%s\0%s' % (filename, rawextras))
--- a/mercurial/mergeutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/mergeutil.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from .i18n import _
 
--- a/mercurial/metadata.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/metadata.py	Wed May 04 18:17:44 2022 +0200
@@ -6,7 +6,6 @@
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-from __future__ import absolute_import, print_function
 
 import multiprocessing
 import struct
@@ -23,7 +22,7 @@
 )
 
 
-class ChangingFiles(object):
+class ChangingFiles:
     """A class recording the changes made to files by a changeset
 
     Actions performed on files are gathered into 3 sets:
--- a/mercurial/minifileset.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/minifileset.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from .i18n import _
 from . import (
--- a/mercurial/minirst.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/minirst.py	Wed May 04 18:17:44 2022 +0200
@@ -18,7 +18,6 @@
 when adding support for new constructs.
 """
 
-from __future__ import absolute_import
 
 import re
 
--- a/mercurial/namespaces.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/namespaces.py	Wed May 04 18:17:44 2022 +0200
@@ -1,8 +1,5 @@
-from __future__ import absolute_import
-
 from .i18n import _
 from . import (
-    pycompat,
     registrar,
     templatekw,
     util,
@@ -19,7 +16,7 @@
         return [val]
 
 
-class namespaces(object):
+class namespaces:
     """provides an interface to register and operate on multiple namespaces. See
     the namespace class below for details on the namespace object.
 
@@ -87,7 +84,7 @@
         return self._names.get(namespace, default)
 
     def items(self):
-        return pycompat.iteritems(self._names)
+        return self._names.items()
 
     iteritems = items
 
@@ -120,14 +117,14 @@
 
         Raises a KeyError if there is no such node.
         """
-        for ns, v in pycompat.iteritems(self._names):
+        for ns, v in self._names.items():
             n = v.singlenode(repo, name)
             if n:
                 return n
         raise KeyError(_(b'no such name: %s') % name)
 
 
-class namespace(object):
+class namespace:
     """provides an interface to a namespace
 
     Namespaces are basically generic many-to-many mapping between some
--- a/mercurial/narrowspec.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/narrowspec.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from .i18n import _
 from .pycompat import getattr
--- a/mercurial/node.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/node.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import binascii
 
@@ -32,7 +31,7 @@
 wdirrev = 0x7FFFFFFF
 
 
-class sha1nodeconstants(object):
+class sha1nodeconstants:
     nodelen = 20
 
     # In hex, this is '0000000000000000000000000000000000000000'
--- a/mercurial/obsolete.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/obsolete.py	Wed May 04 18:17:44 2022 +0200
@@ -67,7 +67,6 @@
 comment associated with each format for details.
 
 """
-from __future__ import absolute_import
 
 import errno
 import struct
@@ -249,7 +248,7 @@
                 # if content cannot be translated to nodeid drop the data.
                 parents = None
 
-        metadata = tuple(sorted(pycompat.iteritems(metadata)))
+        metadata = tuple(sorted(metadata.items()))
 
         yield (pre, sucs, flags, metadata, date, parents)
 
@@ -279,7 +278,7 @@
     """Return encoded metadata string to string mapping.
 
     Assume no ':' in key and no '\0' in both key and value."""
-    for key, value in pycompat.iteritems(meta):
+    for key, value in meta.items():
         if b':' in key or b'\0' in key:
             raise ValueError(b"':' and '\0' are forbidden in metadata key'")
         if b'\0' in value:
@@ -542,7 +541,7 @@
             )
 
 
-class obsstore(object):
+class obsstore:
     """Store obsolete markers
 
     Markers can be accessed with two mappings:
@@ -653,7 +652,7 @@
                 'in-marker cycle with %s' % pycompat.sysstr(hex(prec))
             )
 
-        metadata = tuple(sorted(pycompat.iteritems(metadata)))
+        metadata = tuple(sorted(metadata.items()))
         for k, v in metadata:
             try:
                 # might be better to reject non-ASCII keys
--- a/mercurial/obsutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/obsutil.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import re
 
@@ -19,7 +18,6 @@
     encoding,
     error,
     phases,
-    pycompat,
     util,
 )
 from .utils import dateutil
@@ -58,7 +56,7 @@
 usingsha256 = 2
 
 
-class marker(object):
+class marker:
     """Wrap obsolete marker raw data"""
 
     def __init__(self, repo, data):
@@ -998,7 +996,7 @@
             base[tuple(nsuccset)] = n
     return [
         {b'divergentnodes': divset, b'commonpredecessor': b}
-        for divset, b in pycompat.iteritems(base)
+        for divset, b in base.items()
     ]
 
 
--- a/mercurial/parser.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/parser.py	Wed May 04 18:17:44 2022 +0200
@@ -16,7 +16,6 @@
 # an action is a tree node name, a tree label, and an optional match
 # __call__(program) parses program into a labeled tree
 
-from __future__ import absolute_import, print_function
 
 from .i18n import _
 from . import (
@@ -26,7 +25,7 @@
 from .utils import stringutil
 
 
-class parser(object):
+class parser:
     def __init__(self, elements, methods=None):
         self._elements = elements
         self._methods = methods
@@ -416,7 +415,7 @@
         return inst.message
 
 
-class alias(object):
+class alias:
     """Parsed result of alias"""
 
     def __init__(self, name, args, err, replacement):
@@ -430,7 +429,7 @@
         self.warned = False
 
 
-class basealiasrules(object):
+class basealiasrules:
     """Parsing and expansion rule set of aliases
 
     This is a helper for fileset/revset/template aliases. A concrete rule set
--- a/mercurial/patch.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/patch.py	Wed May 04 18:17:44 2022 +0200
@@ -6,7 +6,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import, print_function
 
 import collections
 import contextlib
@@ -150,7 +149,7 @@
     def remainder(cur):
         yield chunk(cur)
 
-    class fiter(object):
+    class fiter:
         def __init__(self, fp):
             self.fp = fp
 
@@ -343,7 +342,7 @@
     return data
 
 
-class patchmeta(object):
+class patchmeta:
     """Patched file metadata
 
     'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
@@ -436,7 +435,7 @@
     return gitpatches
 
 
-class linereader(object):
+class linereader:
     # simple class to allow pushing lines back into the input stream
     def __init__(self, fp):
         self.fp = fp
@@ -457,7 +456,7 @@
         return iter(self.readline, b'')
 
 
-class abstractbackend(object):
+class abstractbackend:
     def __init__(self, ui):
         self.ui = ui
 
@@ -593,7 +592,7 @@
         return sorted(self.changed)
 
 
-class filestore(object):
+class filestore:
     def __init__(self, maxsize=None):
         self.opener = None
         self.files = {}
@@ -682,7 +681,7 @@
 eolmodes = [b'strict', b'crlf', b'lf', b'auto']
 
 
-class patchfile(object):
+class patchfile:
     def __init__(self, ui, gp, backend, store, eolmode=b'strict'):
         self.fname = gp.path
         self.eolmode = eolmode
@@ -915,7 +914,7 @@
         return len(self.rej)
 
 
-class header(object):
+class header:
     """patch header"""
 
     diffgit_re = re.compile(b'diff --git a/(.*) b/(.*)$')
@@ -995,7 +994,7 @@
         )
 
 
-class recordhunk(object):
+class recordhunk:
     """patch hunk
 
     XXX shouldn't we merge this with the other hunk class?
@@ -1260,7 +1259,7 @@
                     # Remove comment lines
                     patchfp = open(patchfn, 'rb')
                     ncpatchfp = stringio()
-                    for line in util.iterfile(patchfp):
+                    for line in patchfp:
                         line = util.fromnativeeol(line)
                         if not line.startswith(b'#'):
                             ncpatchfp.write(line)
@@ -1343,18 +1342,14 @@
                 fixoffset += chunk.removed - chunk.added
     return (
         sum(
-            [
-                h
-                for h in pycompat.itervalues(applied)
-                if h[0].special() or len(h) > 1
-            ],
+            [h for h in applied.values() if h[0].special() or len(h) > 1],
             [],
         ),
         {},
     )
 
 
-class hunk(object):
+class hunk:
     def __init__(self, desc, num, lr, context):
         self.number = num
         self.desc = desc
@@ -1582,7 +1577,7 @@
         return old, oldstart, new, newstart
 
 
-class binhunk(object):
+class binhunk:
     """A binary patch file."""
 
     def __init__(self, lr, fname):
@@ -1763,7 +1758,7 @@
     +9
     """
 
-    class parser(object):
+    class parser:
         """patch parsing state machine"""
 
         def __init__(self):
@@ -2348,7 +2343,7 @@
     ui.debug(b'Using external patch tool: %s\n' % cmd)
     fp = procutil.popen(cmd, b'rb')
     try:
-        for line in util.iterfile(fp):
+        for line in fp:
             line = line.rstrip()
             ui.note(line + b'\n')
             if line.startswith(b'patching file '):
@@ -2644,11 +2639,7 @@
     if copysourcematch:
         # filter out copies where source side isn't inside the matcher
         # (copies.pathcopies() already filtered out the destination)
-        copy = {
-            dst: src
-            for dst, src in pycompat.iteritems(copy)
-            if copysourcematch(src)
-        }
+        copy = {dst: src for dst, src in copy.items() if copysourcematch(src)}
 
     modifiedset = set(modified)
     addedset = set(added)
--- a/mercurial/pathutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/pathutil.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import contextlib
 import errno
 import os
@@ -33,7 +31,7 @@
     return encoding.hfsignoreclean(s.lower())
 
 
-class pathauditor(object):
+class pathauditor:
     """ensure that a filesystem path contains no banned components.
     the following properties of a path are checked:
 
@@ -316,7 +314,7 @@
     yield b''
 
 
-class dirs(object):
+class dirs:
     '''a multiset of directory names from a set of file paths'''
 
     def __init__(self, map, only_tracked=False):
@@ -326,7 +324,7 @@
         self._dirs = {}
         addpath = self.addpath
         if isinstance(map, dict) and only_tracked:
-            for f, s in pycompat.iteritems(map):
+            for f, s in map.items():
                 if s.state != b'r':
                     addpath(f)
         elif only_tracked:
--- a/mercurial/phases.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/phases.py	Wed May 04 18:17:44 2022 +0200
@@ -100,7 +100,6 @@
 
 """
 
-from __future__ import absolute_import
 
 import errno
 import struct
@@ -220,7 +219,7 @@
     The revision lists are encoded as (phase, root) pairs.
     """
     binarydata = []
-    for phase, nodes in pycompat.iteritems(phasemapping):
+    for phase, nodes in phasemapping.items():
         for head in nodes:
             binarydata.append(_fphasesentry.pack(phase, head))
     return b''.join(binarydata)
@@ -344,7 +343,7 @@
         data.insert(low + 1, (pycompat.xrange(rev, rev + 1), t))
 
 
-class phasecache(object):
+class phasecache:
     def __init__(self, repo, phasedefaults, _load=True):
         # type: (localrepo.localrepository, Optional[Phasedefaults], bool) -> None
         if _load:
@@ -364,9 +363,7 @@
             self.invalidate()
             self.loadphaserevs(repo)
         return any(
-            revs
-            for phase, revs in pycompat.iteritems(self.phaseroots)
-            if phase != public
+            revs for phase, revs in self.phaseroots.items() if phase != public
         )
 
     def nonpublicphaseroots(self, repo):
@@ -384,7 +381,7 @@
         return set().union(
             *[
                 revs
-                for phase, revs in pycompat.iteritems(self.phaseroots)
+                for phase, revs in self.phaseroots.items()
                 if phase != public
             ]
         )
@@ -529,7 +526,7 @@
             f.close()
 
     def _write(self, fp):
-        for phase, roots in pycompat.iteritems(self.phaseroots):
+        for phase, roots in self.phaseroots.items():
             for h in sorted(roots):
                 fp.write(b'%i %s\n' % (phase, hex(h)))
         self.dirty = False
@@ -613,7 +610,7 @@
     def retractboundary(self, repo, tr, targetphase, nodes):
         oldroots = {
             phase: revs
-            for phase, revs in pycompat.iteritems(self.phaseroots)
+            for phase, revs in self.phaseroots.items()
             if phase <= targetphase
         }
         if tr is None:
@@ -691,7 +688,7 @@
         """
         filtered = False
         has_node = repo.changelog.index.has_node  # to filter unknown nodes
-        for phase, nodes in pycompat.iteritems(self.phaseroots):
+        for phase, nodes in self.phaseroots.items():
             missing = sorted(node for node in nodes if not has_node(node))
             if missing:
                 for mnode in missing:
@@ -855,7 +852,7 @@
     # build list from dictionary
     draftroots = []
     has_node = repo.changelog.index.has_node  # to filter unknown nodes
-    for nhex, phase in pycompat.iteritems(roots):
+    for nhex, phase in roots.items():
         if nhex == b'publishing':  # ignore data related to publish option
             continue
         node = bin(nhex)
@@ -882,7 +879,7 @@
     return publicheads, draftroots
 
 
-class remotephasessummary(object):
+class remotephasessummary:
     """summarize phase information on the remote side
 
     :publishing: True is the remote is publishing
--- a/mercurial/policy.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/policy.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import os
 import sys
@@ -54,11 +53,8 @@
     policy = b'cffi'
 
 # Environment variable can always force settings.
-if sys.version_info[0] >= 3:
-    if 'HGMODULEPOLICY' in os.environ:
-        policy = os.environ['HGMODULEPOLICY'].encode('utf-8')
-else:
-    policy = os.environ.get('HGMODULEPOLICY', policy)
+if 'HGMODULEPOLICY' in os.environ:
+    policy = os.environ['HGMODULEPOLICY'].encode('utf-8')
 
 
 def _importfrom(pkgname, modname):
--- a/mercurial/posix.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/posix.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import errno
 import fcntl
@@ -60,21 +59,7 @@
 umask = os.umask(0)
 os.umask(umask)
 
-if not pycompat.ispy3:
-
-    def posixfile(name, mode='r', buffering=-1):
-        fp = open(name, mode=mode, buffering=buffering)
-        # The position when opening in append mode is implementation defined, so
-        # make it consistent by always seeking to the end.
-        if 'a' in mode:
-            fp.seek(0, os.SEEK_END)
-        return fp
-
-
-else:
-    # The underlying file object seeks as required in Python 3:
-    # https://github.com/python/cpython/blob/v3.7.3/Modules/_io/fileio.c#L474
-    posixfile = open
+posixfile = open
 
 
 def split(p):
@@ -679,7 +664,7 @@
     pass
 
 
-class cachestat(object):
+class cachestat:
     def __init__(self, path):
         self.stat = os.stat(path)
 
--- a/mercurial/profiling.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/profiling.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import, print_function
 
 import contextlib
 
@@ -174,7 +173,7 @@
         statprof.display(fp, data=data, format=displayformat, **kwargs)
 
 
-class profile(object):
+class profile:
     """Start profiling.
 
     Profiling is active when the context manager is active. When the context
@@ -232,7 +231,7 @@
                 self._fp = open(path, b'wb')
             elif pycompat.iswindows:
                 # parse escape sequence by win32print()
-                class uifp(object):
+                class uifp:
                     def __init__(self, ui):
                         self._ui = ui
 
--- a/mercurial/progress.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/progress.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import errno
 import threading
@@ -85,7 +84,7 @@
             raise
 
 
-class progbar(object):
+class progbar:
     def __init__(self, ui):
         self.ui = ui
         self._refreshlock = threading.Lock()
--- a/mercurial/pure/base85.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/pure/base85.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import struct
 
--- a/mercurial/pure/bdiff.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/pure/bdiff.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import difflib
 import re
--- a/mercurial/pure/charencode.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/pure/charencode.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import array
 
@@ -68,10 +67,7 @@
         raise ValueError
 
 
-if pycompat.ispy3:
-    _utf8strict = r'surrogatepass'
-else:
-    _utf8strict = r'strict'
+_utf8strict = r'surrogatepass'
 
 
 def jsonescapeu8fallback(u8chars, paranoid):
--- a/mercurial/pure/mpatch.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/pure/mpatch.py	Wed May 04 18:17:44 2022 +0200
@@ -5,13 +5,12 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
+import io
 import struct
 
-from .. import pycompat
 
-stringio = pycompat.bytesio
+stringio = io.BytesIO
 
 
 class mpatchError(Exception):
--- a/mercurial/pure/osutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/pure/osutil.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import, division
 
 import ctypes
 import ctypes.util
@@ -221,7 +220,7 @@
             err.errno, '%s: %s' % (encoding.strfromlocal(name), err.strerror)
         )
 
-    class posixfile(object):
+    class posixfile:
         """a file object aiming for POSIX-like semantics
 
         CPython's open() returns a file that was opened *without* setting the
--- a/mercurial/pure/parsers.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/pure/parsers.py	Wed May 04 18:17:44 2022 +0200
@@ -5,8 +5,8 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
+import io
 import stat
 import struct
 import zlib
@@ -18,7 +18,6 @@
 from ..thirdparty import attr
 from .. import (
     error,
-    pycompat,
     revlogutils,
     util,
 )
@@ -26,7 +25,7 @@
 from ..revlogutils import nodemap as nodemaputil
 from ..revlogutils import constants as revlog_constants
 
-stringio = pycompat.bytesio
+stringio = io.BytesIO
 
 
 _pack = struct.pack
@@ -64,7 +63,7 @@
 
 
 @attr.s(slots=True, init=False)
-class DirstateItem(object):
+class DirstateItem:
     """represent a dirstate entry
 
     It hold multiple attributes
@@ -279,7 +278,7 @@
         self._mtime_ns = None
 
     def drop_merge_data(self):
-        """remove all "merge-only" from a DirstateItem
+        """remove all "merge-only" information from a DirstateItem
 
         This is to be call by the dirstatemap code when the second parent is dropped
         """
@@ -292,15 +291,15 @@
 
     @property
     def mode(self):
-        return self.v1_mode()
+        return self._v1_mode()
 
     @property
     def size(self):
-        return self.v1_size()
+        return self._v1_size()
 
     @property
     def mtime(self):
-        return self.v1_mtime()
+        return self._v1_mtime()
 
     def mtime_likely_equal_to(self, other_mtime):
         self_sec = self._mtime_s
@@ -339,7 +338,7 @@
         """
         if not self.any_tracked:
             return b'?'
-        return self.v1_state()
+        return self._v1_state()
 
     @property
     def has_fallback_exec(self):
@@ -499,7 +498,7 @@
         # since we never set _DIRSTATE_V2_HAS_DIRCTORY_MTIME
         return (flags, self._size or 0, self._mtime_s or 0, self._mtime_ns or 0)
 
-    def v1_state(self):
+    def _v1_state(self):
         """return a "state" suitable for v1 serialization"""
         if not self.any_tracked:
             # the object has no state to record, this is -currently-
@@ -514,11 +513,11 @@
         else:
             return b'n'
 
-    def v1_mode(self):
+    def _v1_mode(self):
         """return a "mode" suitable for v1 serialization"""
         return self._mode if self._mode is not None else 0
 
-    def v1_size(self):
+    def _v1_size(self):
         """return a "size" suitable for v1 serialization"""
         if not self.any_tracked:
             # the object has no state to record, this is -currently-
@@ -537,7 +536,7 @@
         else:
             return self._size
 
-    def v1_mtime(self):
+    def _v1_mtime(self):
         """return a "mtime" suitable for v1 serialization"""
         if not self.any_tracked:
             # the object has no state to record, this is -currently-
@@ -561,7 +560,7 @@
     return int(q & 0xFFFF)
 
 
-class BaseIndexObject(object):
+class BaseIndexObject:
     # Can I be passed to an algorithme implemented in Rust ?
     rust_ext_compat = 0
     # Format of an index entry according to Python's `struct` language
@@ -959,15 +958,15 @@
     cs = stringio()
     write = cs.write
     write(b"".join(pl))
-    for f, e in pycompat.iteritems(dmap):
+    for f, e in dmap.items():
         if f in copymap:
             f = b"%s\0%s" % (f, copymap[f])
         e = _pack(
             b">cllll",
-            e.v1_state(),
-            e.v1_mode(),
-            e.v1_size(),
-            e.v1_mtime(),
+            e._v1_state(),
+            e._v1_mode(),
+            e._v1_size(),
+            e._v1_mtime(),
             len(f),
         )
         write(e)
--- a/mercurial/pushkey.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/pushkey.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from . import (
     bookmarks,
--- a/mercurial/pvec.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/pvec.py	Wed May 04 18:17:44 2022 +0200
@@ -48,7 +48,6 @@
   different branches
 '''
 
-from __future__ import absolute_import
 
 from .node import nullrev
 from . import (
@@ -181,7 +180,7 @@
     return pvec(util.b85encode(bs))
 
 
-class pvec(object):
+class pvec:
     def __init__(self, hashorctx):
         if isinstance(hashorctx, bytes):
             self._bs = hashorctx
--- a/mercurial/pycompat.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/pycompat.py	Wed May 04 18:17:44 2022 +0200
@@ -8,15 +8,26 @@
 This contains aliases to hide python version-specific details from the core.
 """
 
-from __future__ import absolute_import
 
+import builtins
+import codecs
+import concurrent.futures as futures
+import functools
 import getopt
+import http.client as httplib
+import http.cookiejar as cookielib
 import inspect
+import io
 import json
 import os
+import queue
 import shlex
+import socketserver
+import struct
 import sys
 import tempfile
+import xmlrpc.client as xmlrpclib
+
 
 ispy3 = sys.version_info[0] >= 3
 ispypy = '__pypy__' in sys.builtin_module_names
@@ -27,36 +38,12 @@
 
     TYPE_CHECKING = typing.TYPE_CHECKING
 
-if not ispy3:
-    import cookielib
-    import cPickle as pickle
-    import httplib
-    import Queue as queue
-    import SocketServer as socketserver
-    import xmlrpclib
-
-    from .thirdparty.concurrent import futures
-
-    def future_set_exception_info(f, exc_info):
-        f.set_exception_info(*exc_info)
 
-    # this is close enough for our usage
-    FileNotFoundError = OSError
+def future_set_exception_info(f, exc_info):
+    f.set_exception(exc_info[0])
 
-else:
-    import builtins
-    import concurrent.futures as futures
-    import http.cookiejar as cookielib
-    import http.client as httplib
-    import pickle
-    import queue as queue
-    import socketserver
-    import xmlrpc.client as xmlrpclib
 
-    def future_set_exception_info(f, exc_info):
-        f.set_exception(exc_info[0])
-
-    FileNotFoundError = builtins.FileNotFoundError
+FileNotFoundError = builtins.FileNotFoundError
 
 
 def identity(a):
@@ -98,402 +85,297 @@
     return _rapply(f, xs)
 
 
-if ispy3:
-    import builtins
-    import codecs
-    import functools
-    import io
-    import struct
-
-    if os.name == r'nt' and sys.version_info >= (3, 6):
-        # MBCS (or ANSI) filesystem encoding must be used as before.
-        # Otherwise non-ASCII filenames in existing repositories would be
-        # corrupted.
-        # This must be set once prior to any fsencode/fsdecode calls.
-        sys._enablelegacywindowsfsencoding()  # pytype: disable=module-attr
+if os.name == r'nt':
+    # MBCS (or ANSI) filesystem encoding must be used as before.
+    # Otherwise non-ASCII filenames in existing repositories would be
+    # corrupted.
+    # This must be set once prior to any fsencode/fsdecode calls.
+    sys._enablelegacywindowsfsencoding()  # pytype: disable=module-attr
 
-    fsencode = os.fsencode
-    fsdecode = os.fsdecode
-    oscurdir = os.curdir.encode('ascii')
-    oslinesep = os.linesep.encode('ascii')
-    osname = os.name.encode('ascii')
-    ospathsep = os.pathsep.encode('ascii')
-    ospardir = os.pardir.encode('ascii')
-    ossep = os.sep.encode('ascii')
-    osaltsep = os.altsep
-    if osaltsep:
-        osaltsep = osaltsep.encode('ascii')
-    osdevnull = os.devnull.encode('ascii')
+fsencode = os.fsencode
+fsdecode = os.fsdecode
+oscurdir = os.curdir.encode('ascii')
+oslinesep = os.linesep.encode('ascii')
+osname = os.name.encode('ascii')
+ospathsep = os.pathsep.encode('ascii')
+ospardir = os.pardir.encode('ascii')
+ossep = os.sep.encode('ascii')
+osaltsep = os.altsep
+if osaltsep:
+    osaltsep = osaltsep.encode('ascii')
+osdevnull = os.devnull.encode('ascii')
 
-    sysplatform = sys.platform.encode('ascii')
-    sysexecutable = sys.executable
-    if sysexecutable:
-        sysexecutable = os.fsencode(sysexecutable)
-    bytesio = io.BytesIO
-    # TODO deprecate stringio name, as it is a lie on Python 3.
-    stringio = bytesio
+sysplatform = sys.platform.encode('ascii')
+sysexecutable = sys.executable
+if sysexecutable:
+    sysexecutable = os.fsencode(sysexecutable)
+
 
-    def maplist(*args):
-        return list(map(*args))
+def maplist(*args):
+    return list(map(*args))
+
 
-    def rangelist(*args):
-        return list(range(*args))
+def rangelist(*args):
+    return list(range(*args))
+
 
-    def ziplist(*args):
-        return list(zip(*args))
+def ziplist(*args):
+    return list(zip(*args))
+
 
-    rawinput = input
-    getargspec = inspect.getfullargspec
+rawinput = input
+getargspec = inspect.getfullargspec
 
-    long = int
+long = int
 
-    if getattr(sys, 'argv', None) is not None:
-        # On POSIX, the char** argv array is converted to Python str using
-        # Py_DecodeLocale(). The inverse of this is Py_EncodeLocale(), which
-        # isn't directly callable from Python code. In practice, os.fsencode()
-        # can be used instead (this is recommended by Python's documentation
-        # for sys.argv).
-        #
-        # On Windows, the wchar_t **argv is passed into the interpreter as-is.
-        # Like POSIX, we need to emulate what Py_EncodeLocale() would do. But
-        # there's an additional wrinkle. What we really want to access is the
-        # ANSI codepage representation of the arguments, as this is what
-        # `int main()` would receive if Python 3 didn't define `int wmain()`
-        # (this is how Python 2 worked). To get that, we encode with the mbcs
-        # encoding, which will pass CP_ACP to the underlying Windows API to
-        # produce bytes.
-        if os.name == r'nt':
-            sysargv = [a.encode("mbcs", "ignore") for a in sys.argv]
-        else:
-            sysargv = [fsencode(a) for a in sys.argv]
+if getattr(sys, 'argv', None) is not None:
+    # On POSIX, the char** argv array is converted to Python str using
+    # Py_DecodeLocale(). The inverse of this is Py_EncodeLocale(), which
+    # isn't directly callable from Python code. In practice, os.fsencode()
+    # can be used instead (this is recommended by Python's documentation
+    # for sys.argv).
+    #
+    # On Windows, the wchar_t **argv is passed into the interpreter as-is.
+    # Like POSIX, we need to emulate what Py_EncodeLocale() would do. But
+    # there's an additional wrinkle. What we really want to access is the
+    # ANSI codepage representation of the arguments, as this is what
+    # `int main()` would receive if Python 3 didn't define `int wmain()`
+    # (this is how Python 2 worked). To get that, we encode with the mbcs
+    # encoding, which will pass CP_ACP to the underlying Windows API to
+    # produce bytes.
+    if os.name == r'nt':
+        sysargv = [a.encode("mbcs", "ignore") for a in sys.argv]
+    else:
+        sysargv = [fsencode(a) for a in sys.argv]
 
-    bytechr = struct.Struct('>B').pack
-    byterepr = b'%r'.__mod__
-
-    class bytestr(bytes):
-        """A bytes which mostly acts as a Python 2 str
+bytechr = struct.Struct('>B').pack
+byterepr = b'%r'.__mod__
 
-        >>> bytestr(), bytestr(bytearray(b'foo')), bytestr(u'ascii'), bytestr(1)
-        ('', 'foo', 'ascii', '1')
-        >>> s = bytestr(b'foo')
-        >>> assert s is bytestr(s)
 
-        __bytes__() should be called if provided:
+class bytestr(bytes):
+    """A bytes which mostly acts as a Python 2 str
 
-        >>> class bytesable(object):
-        ...     def __bytes__(self):
-        ...         return b'bytes'
-        >>> bytestr(bytesable())
-        'bytes'
+    >>> bytestr(), bytestr(bytearray(b'foo')), bytestr(u'ascii'), bytestr(1)
+    ('', 'foo', 'ascii', '1')
+    >>> s = bytestr(b'foo')
+    >>> assert s is bytestr(s)
+
+    __bytes__() should be called if provided:
 
-        There's no implicit conversion from non-ascii str as its encoding is
-        unknown:
+    >>> class bytesable:
+    ...     def __bytes__(self):
+    ...         return b'bytes'
+    >>> bytestr(bytesable())
+    'bytes'
 
-        >>> bytestr(chr(0x80)) # doctest: +ELLIPSIS
-        Traceback (most recent call last):
-          ...
-        UnicodeEncodeError: ...
-
-        Comparison between bytestr and bytes should work:
+    There's no implicit conversion from non-ascii str as its encoding is
+    unknown:
 
-        >>> assert bytestr(b'foo') == b'foo'
-        >>> assert b'foo' == bytestr(b'foo')
-        >>> assert b'f' in bytestr(b'foo')
-        >>> assert bytestr(b'f') in b'foo'
+    >>> bytestr(chr(0x80)) # doctest: +ELLIPSIS
+    Traceback (most recent call last):
+      ...
+    UnicodeEncodeError: ...
 
-        Sliced elements should be bytes, not integer:
+    Comparison between bytestr and bytes should work:
 
-        >>> s[1], s[:2]
-        (b'o', b'fo')
-        >>> list(s), list(reversed(s))
-        ([b'f', b'o', b'o'], [b'o', b'o', b'f'])
-
-        As bytestr type isn't propagated across operations, you need to cast
-        bytes to bytestr explicitly:
+    >>> assert bytestr(b'foo') == b'foo'
+    >>> assert b'foo' == bytestr(b'foo')
+    >>> assert b'f' in bytestr(b'foo')
+    >>> assert bytestr(b'f') in b'foo'
 
-        >>> s = bytestr(b'foo').upper()
-        >>> t = bytestr(s)
-        >>> s[0], t[0]
-        (70, b'F')
+    Sliced elements should be bytes, not integer:
 
-        Be careful to not pass a bytestr object to a function which expects
-        bytearray-like behavior.
+    >>> s[1], s[:2]
+    (b'o', b'fo')
+    >>> list(s), list(reversed(s))
+    ([b'f', b'o', b'o'], [b'o', b'o', b'f'])
 
-        >>> t = bytes(t)  # cast to bytes
-        >>> assert type(t) is bytes
-        """
+    As bytestr type isn't propagated across operations, you need to cast
+    bytes to bytestr explicitly:
 
-        # Trick pytype into not demanding Iterable[int] be passed to __new__(),
-        # since the appropriate bytes format is done internally.
-        #
-        # https://github.com/google/pytype/issues/500
-        if TYPE_CHECKING:
+    >>> s = bytestr(b'foo').upper()
+    >>> t = bytestr(s)
+    >>> s[0], t[0]
+    (70, b'F')
 
-            def __init__(self, s=b''):
-                pass
+    Be careful to not pass a bytestr object to a function which expects
+    bytearray-like behavior.
+
+    >>> t = bytes(t)  # cast to bytes
+    >>> assert type(t) is bytes
+    """
 
-        def __new__(cls, s=b''):
-            if isinstance(s, bytestr):
-                return s
-            if not isinstance(
-                s, (bytes, bytearray)
-            ) and not hasattr(  # hasattr-py3-only
-                s, u'__bytes__'
-            ):
-                s = str(s).encode('ascii')
-            return bytes.__new__(cls, s)
+    # Trick pytype into not demanding Iterable[int] be passed to __new__(),
+    # since the appropriate bytes format is done internally.
+    #
+    # https://github.com/google/pytype/issues/500
+    if TYPE_CHECKING:
 
-        def __getitem__(self, key):
-            s = bytes.__getitem__(self, key)
-            if not isinstance(s, bytes):
-                s = bytechr(s)
+        def __init__(self, s=b''):
+            pass
+
+    def __new__(cls, s=b''):
+        if isinstance(s, bytestr):
             return s
-
-        def __iter__(self):
-            return iterbytestr(bytes.__iter__(self))
-
-        def __repr__(self):
-            return bytes.__repr__(self)[1:]  # drop b''
+        if not isinstance(
+            s, (bytes, bytearray)
+        ) and not hasattr(  # hasattr-py3-only
+            s, u'__bytes__'
+        ):
+            s = str(s).encode('ascii')
+        return bytes.__new__(cls, s)
 
-    def iterbytestr(s):
-        """Iterate bytes as if it were a str object of Python 2"""
-        return map(bytechr, s)
-
-    def maybebytestr(s):
-        """Promote bytes to bytestr"""
-        if isinstance(s, bytes):
-            return bytestr(s)
+    def __getitem__(self, key):
+        s = bytes.__getitem__(self, key)
+        if not isinstance(s, bytes):
+            s = bytechr(s)
         return s
 
-    def sysbytes(s):
-        """Convert an internal str (e.g. keyword, __doc__) back to bytes
+    def __iter__(self):
+        return iterbytestr(bytes.__iter__(self))
 
-        This never raises UnicodeEncodeError, but only ASCII characters
-        can be round-trip by sysstr(sysbytes(s)).
-        """
-        if isinstance(s, bytes):
-            return s
-        return s.encode('utf-8')
+    def __repr__(self):
+        return bytes.__repr__(self)[1:]  # drop b''
 
-    def sysstr(s):
-        """Return a keyword str to be passed to Python functions such as
-        getattr() and str.encode()
 
-        This never raises UnicodeDecodeError. Non-ascii characters are
-        considered invalid and mapped to arbitrary but unique code points
-        such that 'sysstr(a) != sysstr(b)' for all 'a != b'.
-        """
-        if isinstance(s, builtins.str):
-            return s
-        return s.decode('latin-1')
+def iterbytestr(s):
+    """Iterate bytes as if it were a str object of Python 2"""
+    return map(bytechr, s)
+
 
-    def strurl(url):
-        """Converts a bytes url back to str"""
-        if isinstance(url, bytes):
-            return url.decode('ascii')
-        return url
+def maybebytestr(s):
+    """Promote bytes to bytestr"""
+    if isinstance(s, bytes):
+        return bytestr(s)
+    return s
 
-    def bytesurl(url):
-        """Converts a str url to bytes by encoding in ascii"""
-        if isinstance(url, str):
-            return url.encode('ascii')
-        return url
+
+def sysbytes(s):
+    """Convert an internal str (e.g. keyword, __doc__) back to bytes
 
-    def raisewithtb(exc, tb):
-        """Raise exception with the given traceback"""
-        raise exc.with_traceback(tb)
+    This never raises UnicodeEncodeError, but only ASCII characters
+    can be round-trip by sysstr(sysbytes(s)).
+    """
+    if isinstance(s, bytes):
+        return s
+    return s.encode('utf-8')
 
-    def getdoc(obj):
-        """Get docstring as bytes; may be None so gettext() won't confuse it
-        with _('')"""
-        doc = getattr(obj, '__doc__', None)
-        if doc is None:
-            return doc
-        return sysbytes(doc)
 
-    def _wrapattrfunc(f):
-        @functools.wraps(f)
-        def w(object, name, *args):
-            return f(object, sysstr(name), *args)
-
-        return w
+def sysstr(s):
+    """Return a keyword str to be passed to Python functions such as
+    getattr() and str.encode()
 
-    # these wrappers are automagically imported by hgloader
-    delattr = _wrapattrfunc(builtins.delattr)
-    getattr = _wrapattrfunc(builtins.getattr)
-    hasattr = _wrapattrfunc(builtins.hasattr)
-    setattr = _wrapattrfunc(builtins.setattr)
-    xrange = builtins.range
-    unicode = str
-
-    def open(name, mode=b'r', buffering=-1, encoding=None):
-        return builtins.open(name, sysstr(mode), buffering, encoding)
-
-    safehasattr = _wrapattrfunc(builtins.hasattr)
+    This never raises UnicodeDecodeError. Non-ascii characters are
+    considered invalid and mapped to arbitrary but unique code points
+    such that 'sysstr(a) != sysstr(b)' for all 'a != b'.
+    """
+    if isinstance(s, builtins.str):
+        return s
+    return s.decode('latin-1')
 
-    def _getoptbwrapper(orig, args, shortlist, namelist):
-        """
-        Takes bytes arguments, converts them to unicode, pass them to
-        getopt.getopt(), convert the returned values back to bytes and then
-        return them for Python 3 compatibility as getopt.getopt() don't accepts
-        bytes on Python 3.
-        """
-        args = [a.decode('latin-1') for a in args]
-        shortlist = shortlist.decode('latin-1')
-        namelist = [a.decode('latin-1') for a in namelist]
-        opts, args = orig(args, shortlist, namelist)
-        opts = [(a[0].encode('latin-1'), a[1].encode('latin-1')) for a in opts]
-        args = [a.encode('latin-1') for a in args]
-        return opts, args
+
+def strurl(url):
+    """Converts a bytes url back to str"""
+    if isinstance(url, bytes):
+        return url.decode('ascii')
+    return url
+
 
-    def strkwargs(dic):
-        """
-        Converts the keys of a python dictonary to str i.e. unicodes so that
-        they can be passed as keyword arguments as dictionaries with bytes keys
-        can't be passed as keyword arguments to functions on Python 3.
-        """
-        dic = {k.decode('latin-1'): v for k, v in dic.items()}
-        return dic
+def bytesurl(url):
+    """Converts a str url to bytes by encoding in ascii"""
+    if isinstance(url, str):
+        return url.encode('ascii')
+    return url
 
-    def byteskwargs(dic):
-        """
-        Converts keys of python dictionaries to bytes as they were converted to
-        str to pass that dictonary as a keyword argument on Python 3.
-        """
-        dic = {k.encode('latin-1'): v for k, v in dic.items()}
-        return dic
 
-    # TODO: handle shlex.shlex().
-    def shlexsplit(s, comments=False, posix=True):
-        """
-        Takes bytes argument, convert it to str i.e. unicodes, pass that into
-        shlex.split(), convert the returned value to bytes and return that for
-        Python 3 compatibility as shelx.split() don't accept bytes on Python 3.
-        """
-        ret = shlex.split(s.decode('latin-1'), comments, posix)
-        return [a.encode('latin-1') for a in ret]
+def raisewithtb(exc, tb):
+    """Raise exception with the given traceback"""
+    raise exc.with_traceback(tb)
+
 
-    iteritems = lambda x: x.items()
-    itervalues = lambda x: x.values()
+def getdoc(obj):
+    """Get docstring as bytes; may be None so gettext() won't confuse it
+    with _('')"""
+    doc = getattr(obj, '__doc__', None)
+    if doc is None:
+        return doc
+    return sysbytes(doc)
 
-    # Python 3.5's json.load and json.loads require str. We polyfill its
-    # code for detecting encoding from bytes.
-    if sys.version_info[0:2] < (3, 6):
 
-        def _detect_encoding(b):
-            bstartswith = b.startswith
-            if bstartswith((codecs.BOM_UTF32_BE, codecs.BOM_UTF32_LE)):
-                return 'utf-32'
-            if bstartswith((codecs.BOM_UTF16_BE, codecs.BOM_UTF16_LE)):
-                return 'utf-16'
-            if bstartswith(codecs.BOM_UTF8):
-                return 'utf-8-sig'
+def _wrapattrfunc(f):
+    @functools.wraps(f)
+    def w(object, name, *args):
+        return f(object, sysstr(name), *args)
+
+    return w
+
 
-            if len(b) >= 4:
-                if not b[0]:
-                    # 00 00 -- -- - utf-32-be
-                    # 00 XX -- -- - utf-16-be
-                    return 'utf-16-be' if b[1] else 'utf-32-be'
-                if not b[1]:
-                    # XX 00 00 00 - utf-32-le
-                    # XX 00 00 XX - utf-16-le
-                    # XX 00 XX -- - utf-16-le
-                    return 'utf-16-le' if b[2] or b[3] else 'utf-32-le'
-            elif len(b) == 2:
-                if not b[0]:
-                    # 00 XX - utf-16-be
-                    return 'utf-16-be'
-                if not b[1]:
-                    # XX 00 - utf-16-le
-                    return 'utf-16-le'
-            # default
-            return 'utf-8'
+# these wrappers are automagically imported by hgloader
+delattr = _wrapattrfunc(builtins.delattr)
+getattr = _wrapattrfunc(builtins.getattr)
+hasattr = _wrapattrfunc(builtins.hasattr)
+setattr = _wrapattrfunc(builtins.setattr)
+xrange = builtins.range
+unicode = str
 
-        def json_loads(s, *args, **kwargs):
-            if isinstance(s, (bytes, bytearray)):
-                s = s.decode(_detect_encoding(s), 'surrogatepass')
 
-            return json.loads(s, *args, **kwargs)
+def open(name, mode=b'r', buffering=-1, encoding=None):
+    return builtins.open(name, sysstr(mode), buffering, encoding)
 
-    else:
-        json_loads = json.loads
 
-else:
-    import cStringIO
+safehasattr = _wrapattrfunc(builtins.hasattr)
+
 
-    xrange = xrange
-    unicode = unicode
-    bytechr = chr
-    byterepr = repr
-    bytestr = str
-    iterbytestr = iter
-    maybebytestr = identity
-    sysbytes = identity
-    sysstr = identity
-    strurl = identity
-    bytesurl = identity
-    open = open
-    delattr = delattr
-    getattr = getattr
-    hasattr = hasattr
-    setattr = setattr
+def _getoptbwrapper(orig, args, shortlist, namelist):
+    """
+    Takes bytes arguments, converts them to unicode, pass them to
+    getopt.getopt(), convert the returned values back to bytes and then
+    return them for Python 3 compatibility as getopt.getopt() don't accepts
+    bytes on Python 3.
+    """
+    args = [a.decode('latin-1') for a in args]
+    shortlist = shortlist.decode('latin-1')
+    namelist = [a.decode('latin-1') for a in namelist]
+    opts, args = orig(args, shortlist, namelist)
+    opts = [(a[0].encode('latin-1'), a[1].encode('latin-1')) for a in opts]
+    args = [a.encode('latin-1') for a in args]
+    return opts, args
 
-    # this can't be parsed on Python 3
-    exec(b'def raisewithtb(exc, tb):\n    raise exc, None, tb\n')
 
-    def fsencode(filename):
-        """
-        Partial backport from os.py in Python 3, which only accepts bytes.
-        In Python 2, our paths should only ever be bytes, a unicode path
-        indicates a bug.
-        """
-        if isinstance(filename, str):
-            return filename
-        else:
-            raise TypeError("expect str, not %s" % type(filename).__name__)
-
-    # In Python 2, fsdecode() has a very chance to receive bytes. So it's
-    # better not to touch Python 2 part as it's already working fine.
-    fsdecode = identity
+def strkwargs(dic):
+    """
+    Converts the keys of a python dictonary to str i.e. unicodes so that
+    they can be passed as keyword arguments as dictionaries with bytes keys
+    can't be passed as keyword arguments to functions on Python 3.
+    """
+    dic = {k.decode('latin-1'): v for k, v in dic.items()}
+    return dic
 
-    def getdoc(obj):
-        return getattr(obj, '__doc__', None)
-
-    _notset = object()
 
-    def safehasattr(thing, attr):
-        return getattr(thing, attr, _notset) is not _notset
+def byteskwargs(dic):
+    """
+    Converts keys of python dictionaries to bytes as they were converted to
+    str to pass that dictonary as a keyword argument on Python 3.
+    """
+    dic = {k.encode('latin-1'): v for k, v in dic.items()}
+    return dic
 
-    def _getoptbwrapper(orig, args, shortlist, namelist):
-        return orig(args, shortlist, namelist)
-
-    strkwargs = identity
-    byteskwargs = identity
 
-    oscurdir = os.curdir
-    oslinesep = os.linesep
-    osname = os.name
-    ospathsep = os.pathsep
-    ospardir = os.pardir
-    ossep = os.sep
-    osaltsep = os.altsep
-    osdevnull = os.devnull
-    long = long
-    if getattr(sys, 'argv', None) is not None:
-        sysargv = sys.argv
-    sysplatform = sys.platform
-    sysexecutable = sys.executable
-    shlexsplit = shlex.split
-    bytesio = cStringIO.StringIO
-    stringio = bytesio
-    maplist = map
-    rangelist = range
-    ziplist = zip
-    rawinput = raw_input
-    getargspec = inspect.getargspec
-    iteritems = lambda x: x.iteritems()
-    itervalues = lambda x: x.itervalues()
-    json_loads = json.loads
+# TODO: handle shlex.shlex().
+def shlexsplit(s, comments=False, posix=True):
+    """
+    Takes bytes argument, convert it to str i.e. unicodes, pass that into
+    shlex.split(), convert the returned value to bytes and return that for
+    Python 3 compatibility as shelx.split() don't accept bytes on Python 3.
+    """
+    ret = shlex.split(s.decode('latin-1'), comments, posix)
+    return [a.encode('latin-1') for a in ret]
+
+
+iteritems = lambda x: x.items()
+itervalues = lambda x: x.values()
+
+json_loads = json.loads
 
 isjython = sysplatform.startswith(b'java')
 
--- a/mercurial/rcutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/rcutil.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import os
 
--- a/mercurial/registrar.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/registrar.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from . import (
     configitems,
@@ -22,7 +21,7 @@
 configitem = configitems.getitemregister
 
 
-class _funcregistrarbase(object):
+class _funcregistrarbase:
     """Base of decorator to register a function for specific purpose
 
     This decorator stores decorated functions into own dict 'table'.
--- a/mercurial/repair.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/repair.py	Wed May 04 18:17:44 2022 +0200
@@ -6,7 +6,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import errno
 
@@ -380,7 +379,7 @@
     return [c.node() for c in repo.set(b'roots(%ld)', tostrip)]
 
 
-class stripcallback(object):
+class stripcallback:
     """used as a transaction postclose callback"""
 
     def __init__(self, ui, repo, backup, topic):
--- a/mercurial/repocache.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/repocache.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import collections
 import gc
@@ -20,7 +19,7 @@
 )
 
 
-class repoloader(object):
+class repoloader:
     """Load repositories in background thread
 
     This is designed for a forking server. A cached repo cannot be obtained
--- a/mercurial/repoview.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/repoview.py	Wed May 04 18:17:44 2022 +0200
@@ -6,7 +6,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import copy
 import weakref
@@ -262,7 +261,7 @@
     return cl
 
 
-class filteredchangelogmixin(object):
+class filteredchangelogmixin:
     def tiprev(self):
         """filtered version of revlog.tiprev"""
         for i in pycompat.xrange(len(self) - 1, -2, -1):
@@ -362,7 +361,7 @@
         return super(filteredchangelogmixin, self).flags(rev)
 
 
-class repoview(object):
+class repoview:
     """Provide a read/write view of a repo through a filtered changelog
 
     This object is used to access a filtered version of a repository without
--- a/mercurial/requirements.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/requirements.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 # obsolete experimental requirements:
 #  - manifestv2: An experimental new manifest format that allowed
--- a/mercurial/revlog.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/revlog.py	Wed May 04 18:17:44 2022 +0200
@@ -12,7 +12,6 @@
 and O(changes) merge between branches.
 """
 
-from __future__ import absolute_import
 
 import binascii
 import collections
@@ -172,7 +171,7 @@
 
 @interfaceutil.implementer(repository.irevisiondelta)
 @attr.s(slots=True)
-class revlogrevisiondelta(object):
+class revlogrevisiondelta:
     node = attr.ib()
     p1node = attr.ib()
     p2node = attr.ib()
@@ -188,7 +187,7 @@
 
 @interfaceutil.implementer(repository.iverifyproblem)
 @attr.s(frozen=True)
-class revlogproblem(object):
+class revlogproblem:
     warning = attr.ib(default=None)
     error = attr.ib(default=None)
     node = attr.ib(default=None)
@@ -238,7 +237,7 @@
 )
 
 
-class revlog(object):
+class revlog:
     """
     the underlying revision storage object
 
@@ -299,6 +298,7 @@
         persistentnodemap=False,
         concurrencychecker=None,
         trypending=False,
+        canonical_parent_order=True,
     ):
         """
         create a revlog object
@@ -374,6 +374,13 @@
 
         self._concurrencychecker = concurrencychecker
 
+        # parent order is supposed to be semantically irrelevant, so we
+        # normally resort parents to ensure that the first parent is non-null,
+        # if there is a non-null parent at all.
+        # filelog abuses the parent order as flag to mark some instances of
+        # meta-encoded files, so allow it to disable this behavior.
+        self.canonical_parent_order = canonical_parent_order
+
     def _init_opts(self):
         """process options (from above/config) to setup associated default revlog mode
 
@@ -438,9 +445,7 @@
             self._flagprocessors[REVIDX_ELLIPSIS] = ellipsisprocessor
 
         # revlog v0 doesn't have flag processors
-        for flag, processor in pycompat.iteritems(
-            opts.get(b'flagprocessors', {})
-        ):
+        for flag, processor in opts.get(b'flagprocessors', {}).items():
             flagutil.insertflagprocessor(flag, processor, self._flagprocessors)
 
         if self._chunkcachesize <= 0:
@@ -869,8 +874,10 @@
         the revlog which do not persist the rank.
         """
         rank = self.index[rev][ENTRY_RANK]
-        if rank == RANK_UNKNOWN:
+        if self._format_version != CHANGELOGV2 or rank == RANK_UNKNOWN:
             return None
+        if rev == nullrev:
+            return 0  # convention
         return rank
 
     def chainbase(self, rev):
@@ -899,7 +906,10 @@
                 raise error.WdirUnsupported
             raise
 
-        return entry[5], entry[6]
+        if self.canonical_parent_order and entry[5] == nullrev:
+            return entry[6], entry[5]
+        else:
+            return entry[5], entry[6]
 
     # fast parentrevs(rev) where rev isn't filtered
     _uncheckedparentrevs = parentrevs
@@ -920,7 +930,11 @@
     def parents(self, node):
         i = self.index
         d = i[self.rev(node)]
-        return i[d[5]][7], i[d[6]][7]  # map revisions to nodes inline
+        # inline node() to avoid function call overhead
+        if self.canonical_parent_order and d[5] == self.nullid:
+            return i[d[6]][7], i[d[5]][7]
+        else:
+            return i[d[5]][7], i[d[6]][7]
 
     def chainlen(self, rev):
         return self._chaininfo(rev)[0]
@@ -1043,7 +1057,7 @@
         heads = [self.rev(n) for n in heads]
 
         # we want the ancestors, but inclusive
-        class lazyset(object):
+        class lazyset:
             def __init__(self, lazyvalues):
                 self.addedvalues = set()
                 self.lazyvalues = lazyvalues
@@ -1304,7 +1318,7 @@
                     # But, obviously its parents aren't.
                     for p in self.parents(n):
                         heads.pop(p, None)
-        heads = [head for head, flag in pycompat.iteritems(heads) if flag]
+        heads = [head for head, flag in heads.items() if flag]
         roots = list(roots)
         assert orderedout
         assert roots
@@ -2469,9 +2483,12 @@
             elif p1r == nullrev and p2r != nullrev:
                 rank = 1 + self.fast_rank(p2r)
             else:  # merge node
-                pmin, pmax = sorted((p1r, p2r))
-                rank = 1 + self.fast_rank(pmax)
-                rank += sum(1 for _ in self.findmissingrevs([pmax], [pmin]))
+                if rustdagop is not None and self.index.rust_ext_compat:
+                    rank = rustdagop.rank(self.index, p1r, p2r)
+                else:
+                    pmin, pmax = sorted((p1r, p2r))
+                    rank = 1 + self.fast_rank(pmax)
+                    rank += sum(1 for _ in self.findmissingrevs([pmax], [pmin]))
 
         e = revlogutils.entry(
             flags=flags,
--- a/mercurial/revlogutils/__init__.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/revlogutils/__init__.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from ..thirdparty import attr
 from ..interfaces import repository
@@ -63,7 +62,7 @@
 
 
 @attr.s(slots=True, frozen=True)
-class revisioninfo(object):
+class revisioninfo:
     """Information about a revision that allows building its fulltext
     node:       expected hash of the revision
     p1, p2:     parent revs of the revision
--- a/mercurial/revlogutils/constants.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/revlogutils/constants.py	Wed May 04 18:17:44 2022 +0200
@@ -7,7 +7,6 @@
 # GNU General Public License version 2 or any later version.
 """Helper class to compute deltas stored inside revlogs"""
 
-from __future__ import absolute_import
 
 import struct
 
--- a/mercurial/revlogutils/deltas.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/revlogutils/deltas.py	Wed May 04 18:17:44 2022 +0200
@@ -7,7 +7,6 @@
 # GNU General Public License version 2 or any later version.
 """Helper class to compute deltas stored inside revlogs"""
 
-from __future__ import absolute_import
 
 import collections
 import struct
@@ -39,7 +38,7 @@
 LIMIT_DELTA2TEXT = 2
 
 
-class _testrevlog(object):
+class _testrevlog:
     """minimalist fake revlog to use in doctests"""
 
     def __init__(self, data, density=0.5, mingap=0, snapshot=()):
@@ -545,7 +544,7 @@
 
 
 @attr.s(slots=True, frozen=True)
-class _deltainfo(object):
+class _deltainfo:
     distance = attr.ib()
     deltalen = attr.ib()
     data = attr.ib()
@@ -928,7 +927,7 @@
         yield (prev,)
 
 
-class deltacomputer(object):
+class deltacomputer:
     def __init__(self, revlog):
         self.revlog = revlog
 
--- a/mercurial/revlogutils/docket.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/revlogutils/docket.py	Wed May 04 18:17:44 2022 +0200
@@ -15,7 +15,6 @@
 #
 # * a data file, containing variable width data for these revisions,
 
-from __future__ import absolute_import
 
 import errno
 import os
@@ -26,7 +25,6 @@
     encoding,
     error,
     node,
-    pycompat,
     util,
 )
 
@@ -57,12 +55,7 @@
             if inst.errno != errno.ENOENT:
                 raise
             seed = b'04'  # chosen by a fair dice roll. garanteed to be random
-        if pycompat.ispy3:
-            iter_seed = iter(seed)
-        else:
-            # pytype: disable=wrong-arg-types
-            iter_seed = (ord(c) for c in seed)
-            # pytype: enable=wrong-arg-types
+        iter_seed = iter(seed)
         # some basic circular sum hashing on 64 bits
         int_seed = 0
         low_mask = int('1' * 35, 2)
@@ -71,10 +64,7 @@
             low_part = (int_seed & low_mask) << 28
             int_seed = high_part + low_part + i
         r = random.Random()
-        if pycompat.ispy3:
-            r.seed(int_seed, version=1)
-        else:
-            r.seed(int_seed)
+        r.seed(int_seed, version=1)
         # once we drop python 3.8 support we can simply use r.randbytes
         raw = r.getrandbits(id_size * 4)
         assert id_size == 8
@@ -109,7 +99,7 @@
 S_OLD_UID = struct.Struct('>BL')
 
 
-class RevlogDocket(object):
+class RevlogDocket:
     """metadata associated with revlog"""
 
     def __init__(
--- a/mercurial/revlogutils/flagutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/revlogutils/flagutil.py	Wed May 04 18:17:44 2022 +0200
@@ -6,7 +6,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from ..i18n import _
 
--- a/mercurial/revlogutils/nodemap.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/revlogutils/nodemap.py	Wed May 04 18:17:44 2022 +0200
@@ -6,7 +6,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import errno
 import re
@@ -114,7 +113,7 @@
     tr.addfinalize(callback_id, lambda tr: persist_nodemap(tr, revlog))
 
 
-class _NoTransaction(object):
+class _NoTransaction:
     """transaction like object to update the nodemap outside a transaction"""
 
     def __init__(self):
@@ -305,7 +304,7 @@
 S_HEADER = struct.Struct(">BQQQQ")
 
 
-class NodeMapDocket(object):
+class NodeMapDocket:
     """metadata associated with persistent nodemap data
 
     The persistent data may come from disk or be on their way to disk.
--- a/mercurial/revlogutils/randomaccessfile.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/revlogutils/randomaccessfile.py	Wed May 04 18:17:44 2022 +0200
@@ -23,7 +23,7 @@
     return (n & (n - 1) == 0) and n != 0
 
 
-class randomaccessfile(object):
+class randomaccessfile:
     """Accessing arbitrary chuncks of data within a file, with some caching"""
 
     def __init__(
--- a/mercurial/revlogutils/revlogv0.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/revlogutils/revlogv0.py	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-from __future__ import absolute_import
 
 
 from ..node import sha1nodeconstants
--- a/mercurial/revlogutils/sidedata.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/revlogutils/sidedata.py	Wed May 04 18:17:44 2022 +0200
@@ -30,7 +30,6 @@
 the concept.
 """
 
-from __future__ import absolute_import
 
 import collections
 import struct
--- a/mercurial/revset.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/revset.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import re
 
@@ -595,7 +594,7 @@
             bms.add(repo[bmrev].rev())
         else:
             matchrevs = set()
-            for name, bmrev in pycompat.iteritems(repo._bookmarks):
+            for name, bmrev in repo._bookmarks.items():
                 if matcher(name):
                     matchrevs.add(bmrev)
             for bmrev in matchrevs:
@@ -1707,7 +1706,7 @@
             )
         namespaces.add(repo.names[pattern])
     else:
-        for name, ns in pycompat.iteritems(repo.names):
+        for name, ns in repo.names.items():
             if matcher(name):
                 namespaces.add(ns)
 
@@ -2804,7 +2803,7 @@
 
 def loadpredicate(ui, extname, registrarobj):
     """Load revset predicates from specified registrarobj"""
-    for name, func in pycompat.iteritems(registrarobj._table):
+    for name, func in registrarobj._table.items():
         symbols[name] = func
         if func._safe:
             safesymbols.add(name)
--- a/mercurial/revsetlang.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/revsetlang.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import string
 
@@ -613,7 +612,7 @@
     tree = _aliasrules.expand(aliases, tree)
     # warn about problematic (but not referred) aliases
     if warn is not None:
-        for name, alias in sorted(pycompat.iteritems(aliases)):
+        for name, alias in sorted(aliases.items()):
             if alias.error and not alias.warned:
                 warn(_(b'warning: %s\n') % (alias.error))
                 alias.warned = True
--- a/mercurial/rewriteutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/rewriteutil.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import re
 
--- a/mercurial/scmposix.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/scmposix.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import array
 import errno
 import fcntl
--- a/mercurial/scmutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/scmutil.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import errno
 import glob
@@ -63,7 +62,7 @@
 
 
 @attr.s(slots=True, repr=False)
-class status(object):
+class status:
     """Struct with a list of files per status.
 
     The 'deleted', 'unknown' and 'ignored' properties are only
@@ -109,7 +108,7 @@
             del subpaths[subpath]
             missing.add(subpath)
 
-    for subpath, ctx in sorted(pycompat.iteritems(subpaths)):
+    for subpath, ctx in sorted(subpaths.items()):
         yield subpath, ctx.sub(subpath)
 
     # Yield an empty subrepo based on ctx1 for anything only in ctx2.  That way,
@@ -228,7 +227,7 @@
         except (AttributeError, IndexError):
             # it might be anything, for example a string
             reason = inst.reason
-        if isinstance(reason, pycompat.unicode):
+        if isinstance(reason, str):
             # SSLError of Python 2.7.9 contains a unicode
             reason = encoding.unitolocal(reason)
         ui.error(_(b"abort: error: %s\n") % stringutil.forcebytestr(reason))
@@ -324,7 +323,7 @@
     return abort, warn
 
 
-class casecollisionauditor(object):
+class casecollisionauditor:
     def __init__(self, ui, abort, dirstate):
         self._ui = ui
         self._abort = abort
@@ -1020,7 +1019,7 @@
     return origvfs.join(filepath)
 
 
-class _containsnode(object):
+class _containsnode:
     """proxy __contains__(node) to container.__contains__ which accepts revs"""
 
     def __init__(self, repo, revcontainer):
@@ -1337,7 +1336,7 @@
         ignored=False,
         full=False,
     )
-    for abs, st in pycompat.iteritems(walkresults):
+    for abs, st in walkresults.items():
         entry = dirstate.get_entry(abs)
         if (not entry.any_tracked) and audit_path.check(abs):
             unknown.append(abs)
@@ -1384,7 +1383,7 @@
     with repo.wlock():
         wctx.forget(deleted)
         wctx.add(unknown)
-        for new, old in pycompat.iteritems(renames):
+        for new, old in renames.items():
             wctx.copy(old, new)
 
 
@@ -1510,12 +1509,9 @@
     # Merge old parent and old working dir copies
     oldcopies = copiesmod.pathcopies(newctx, oldctx, match)
     oldcopies.update(copies)
-    copies = {
-        dst: oldcopies.get(src, src)
-        for dst, src in pycompat.iteritems(oldcopies)
-    }
+    copies = {dst: oldcopies.get(src, src) for dst, src in oldcopies.items()}
     # Adjust the dirstate copies
-    for dst, src in pycompat.iteritems(copies):
+    for dst, src in copies.items():
         if src not in newctx or dst in newctx or not ds.get_entry(dst).added:
             src = None
         ds.copy(src, dst)
@@ -1571,7 +1567,7 @@
             fp.write(b"%s\n" % r)
 
 
-class filecachesubentry(object):
+class filecachesubentry:
     def __init__(self, path, stat):
         self.path = path
         self.cachestat = None
@@ -1627,7 +1623,7 @@
                 raise
 
 
-class filecacheentry(object):
+class filecacheentry:
     def __init__(self, paths, stat=True):
         self._entries = []
         for path in paths:
@@ -1645,7 +1641,7 @@
             entry.refresh()
 
 
-class filecache(object):
+class filecache:
     """A property like decorator that tracks files under .hg/ for updates.
 
     On first access, the files defined as arguments are stat()ed and the
@@ -1802,7 +1798,7 @@
     return data
 
 
-class progress(object):
+class progress:
     def __init__(self, ui, updatebar, topic, unit=b"", total=None):
         self.ui = ui
         self.pos = 0
@@ -1867,7 +1863,7 @@
     return ui.configbool(b'format', b'generaldelta')
 
 
-class simplekeyvaluefile(object):
+class simplekeyvaluefile:
     """A simple file with key=value lines
 
     Keys must be alphanumerics and start with a letter, values must not
--- a/mercurial/scmwindows.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/scmwindows.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import os
 
 from . import (
@@ -54,7 +52,11 @@
 
     # next look for a system rcpath in the registry
     value = util.lookupreg(
-        b'SOFTWARE\\Mercurial', None, winreg.HKEY_LOCAL_MACHINE
+        # pytype: disable=module-attr
+        b'SOFTWARE\\Mercurial',
+        None,
+        winreg.HKEY_LOCAL_MACHINE
+        # pytype: enable=module-attr
     )
     if value and isinstance(value, bytes):
         value = util.localpath(value)
--- a/mercurial/server.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/server.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import os
 
--- a/mercurial/setdiscovery.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/setdiscovery.py	Wed May 04 18:17:44 2022 +0200
@@ -40,7 +40,6 @@
 classified with it (since all ancestors or descendants will be marked as well).
 """
 
-from __future__ import absolute_import
 
 import collections
 import random
@@ -107,7 +106,7 @@
     return set(sample[:desiredlen])
 
 
-class partialdiscovery(object):
+class partialdiscovery:
     """an object representing ongoing discovery
 
     Feed with data from the remote repository, this object keep track of the
@@ -299,6 +298,9 @@
 
     samplegrowth = float(ui.config(b'devel', b'discovery.grow-sample.rate'))
 
+    if audit is not None:
+        audit[b'total-queries'] = 0
+
     start = util.timer()
 
     roundtrips = 0
@@ -377,6 +379,8 @@
         roundtrips += 1
         with remote.commandexecutor() as e:
             fheads = e.callcommand(b'heads', {})
+            if audit is not None:
+                audit[b'total-queries'] += len(sample)
             fknown = e.callcommand(
                 b'known',
                 {
@@ -479,6 +483,8 @@
         sample = list(sample)
 
         with remote.commandexecutor() as e:
+            if audit is not None:
+                audit[b'total-queries'] += len(sample)
             yesno = e.callcommand(
                 b'known',
                 {
--- a/mercurial/shelve.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/shelve.py	Wed May 04 18:17:44 2022 +0200
@@ -20,7 +20,6 @@
 shelved change has a distinct name. For details, see the help for "hg
 shelve".
 """
-from __future__ import absolute_import
 
 import collections
 import errno
@@ -69,7 +68,7 @@
 shelveuser = b'shelve@localhost'
 
 
-class ShelfDir(object):
+class ShelfDir:
     def __init__(self, repo, for_backups=False):
         if for_backups:
             self.vfs = vfsmod.vfs(repo.vfs.join(backupdir))
@@ -102,7 +101,7 @@
         return sorted(info, reverse=True)
 
 
-class Shelf(object):
+class Shelf:
     """Represents a shelf, including possibly multiple files storing it.
 
     Old shelves will have a .patch and a .hg file. Newer shelves will
@@ -214,7 +213,7 @@
             self.vfs.tryunlink(self.name + b'.' + ext)
 
 
-class shelvedstate(object):
+class shelvedstate:
     """Handle persistence during unshelving operations.
 
     Handles saving and restoring a shelved state. Ensures that different
--- a/mercurial/similar.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/similar.py	Wed May 04 18:17:44 2022 +0200
@@ -5,12 +5,10 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from .i18n import _
 from . import (
     mdiff,
-    pycompat,
 )
 
 
@@ -98,7 +96,7 @@
                 copies[a] = (r, myscore)
     progress.complete()
 
-    for dest, v in pycompat.iteritems(copies):
+    for dest, v in copies.items():
         source, bscore = v
         yield source, dest, bscore
 
--- a/mercurial/simplemerge.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/simplemerge.py	Wed May 04 18:17:44 2022 +0200
@@ -16,7 +16,6 @@
 # mbp: "you know that thing where cvs gives you conflict markers?"
 # s: "i hate that."
 
-from __future__ import absolute_import
 
 from .i18n import _
 from . import (
@@ -63,7 +62,7 @@
         return True
 
 
-class Merge3Text(object):
+class Merge3Text:
     """3-way merge of texts.
 
     Given strings BASE, OTHER, THIS, tries to produce a combined text
@@ -469,7 +468,7 @@
     return lines
 
 
-class MergeInput(object):
+class MergeInput:
     def __init__(self, fctx, label=None, label_detail=None):
         self.fctx = fctx
         self.label = label
@@ -491,6 +490,9 @@
             self._text = self.fctx.decodeddata()
         return self._text
 
+    def set_text(self, text):
+        self._text = text
+
 
 def simplemerge(
     local,
--- a/mercurial/smartset.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/smartset.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from .pycompat import getattr
 from . import (
@@ -21,7 +20,7 @@
     return pycompat.sysbytes(type(o).__name__).lstrip(b'_')
 
 
-class abstractsmartset(object):
+class abstractsmartset:
     def __nonzero__(self):
         """True if the smartset is not empty"""
         raise NotImplementedError()
--- a/mercurial/sparse.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/sparse.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import os
 
@@ -555,7 +554,7 @@
         )
 
     # Check for files that were only in the dirstate.
-    for file, state in pycompat.iteritems(dirstate):
+    for file, state in dirstate.items():
         if not file in files:
             old = origsparsematch(file)
             new = sparsematch(file)
--- a/mercurial/sshpeer.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/sshpeer.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import re
 import uuid
@@ -48,7 +47,7 @@
                 display(_(b"remote: "), l, b'\n')
 
 
-class doublepipe(object):
+class doublepipe:
     """Operate a side-channel pipe in addition of a main one
 
     The side-channel pipe contains server output to be forwarded to the user
@@ -473,10 +472,10 @@
             else:
                 wireargs[k] = args[k]
                 del args[k]
-        for k, v in sorted(pycompat.iteritems(wireargs)):
+        for k, v in sorted(wireargs.items()):
             self._pipeo.write(b"%s %d\n" % (k, len(v)))
             if isinstance(v, dict):
-                for dk, dv in pycompat.iteritems(v):
+                for dk, dv in v.items():
                     self._pipeo.write(b"%s %d\n" % (dk, len(dv)))
                     self._pipeo.write(dv)
             else:
--- a/mercurial/sslutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/sslutil.py	Wed May 04 18:17:44 2022 +0200
@@ -7,12 +7,12 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import hashlib
 import os
 import re
 import ssl
+import warnings
 
 from .i18n import _
 from .pycompat import getattr
@@ -113,16 +113,18 @@
     minimumprotocol = ui.config(b'hostsecurity', key, minimumprotocol)
     validateprotocol(minimumprotocol, key)
 
+    ciphers = ui.config(b'hostsecurity', b'ciphers')
+    ciphers = ui.config(b'hostsecurity', b'%s:ciphers' % bhostname, ciphers)
+
     # If --insecure is used, we allow the use of TLS 1.0 despite config options.
     # We always print a "connection security to %s is disabled..." message when
     # --insecure is used. So no need to print anything more here.
     if ui.insecureconnections:
         minimumprotocol = b'tls1.0'
+        if not ciphers:
+            ciphers = b'DEFAULT'
 
     s[b'minimumprotocol'] = minimumprotocol
-
-    ciphers = ui.config(b'hostsecurity', b'ciphers')
-    ciphers = ui.config(b'hostsecurity', b'%s:ciphers' % bhostname, ciphers)
     s[b'ciphers'] = ciphers
 
     # Look for fingerprints in [hostsecurity] section. Value is a list
@@ -309,12 +311,43 @@
     # bundle with a specific CA cert removed. If the system/default CA bundle
     # is loaded and contains that removed CA, you've just undone the user's
     # choice.
-    #
-    # Despite its name, PROTOCOL_SSLv23 selects the highest protocol that both
-    # ends support, including TLS protocols. commonssloptions() restricts the
-    # set of allowed protocols.
-    sslcontext = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
-    sslcontext.options |= commonssloptions(settings[b'minimumprotocol'])
+
+    if util.safehasattr(ssl, 'PROTOCOL_TLS_CLIENT'):
+        # python 3.7+
+        sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+        minimumprotocol = settings[b'minimumprotocol']
+        if minimumprotocol == b'tls1.0':
+            with warnings.catch_warnings():
+                warnings.filterwarnings(
+                    'ignore',
+                    'ssl.TLSVersion.TLSv1 is deprecated',
+                    DeprecationWarning,
+                )
+                sslcontext.minimum_version = ssl.TLSVersion.TLSv1
+        elif minimumprotocol == b'tls1.1':
+            with warnings.catch_warnings():
+                warnings.filterwarnings(
+                    'ignore',
+                    'ssl.TLSVersion.TLSv1_1 is deprecated',
+                    DeprecationWarning,
+                )
+                sslcontext.minimum_version = ssl.TLSVersion.TLSv1_1
+        elif minimumprotocol == b'tls1.2':
+            sslcontext.minimum_version = ssl.TLSVersion.TLSv1_2
+        else:
+            raise error.Abort(_(b'this should not happen'))
+        # Prevent CRIME.
+        # There is no guarantee this attribute is defined on the module.
+        sslcontext.options |= getattr(ssl, 'OP_NO_COMPRESSION', 0)
+    else:
+        # Despite its name, PROTOCOL_SSLv23 selects the highest protocol that both
+        # ends support, including TLS protocols. commonssloptions() restricts the
+        # set of allowed protocols.
+        sslcontext = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
+        sslcontext.options |= commonssloptions(settings[b'minimumprotocol'])
+
+    # We check the hostname ourselves in _verifycert
+    sslcontext.check_hostname = False
     sslcontext.verify_mode = settings[b'verifymode']
 
     if settings[b'ciphers']:
@@ -392,7 +425,10 @@
             # outright. Hopefully the reason for this error is that we require
             # TLS 1.1+ and the server only supports TLS 1.0. Whatever the
             # reason, try to emit an actionable warning.
-            if e.reason == 'UNSUPPORTED_PROTOCOL':
+            if e.reason in (
+                'UNSUPPORTED_PROTOCOL',
+                'TLSV1_ALERT_PROTOCOL_VERSION',
+            ):
                 # We attempted TLS 1.0+.
                 if settings[b'minimumprotocol'] == b'tls1.0':
                     # We support more than just TLS 1.0+. If this happens,
@@ -510,44 +546,87 @@
                 _(b'referenced certificate file (%s) does not exist') % f
             )
 
-    # Despite its name, PROTOCOL_SSLv23 selects the highest protocol that both
-    # ends support, including TLS protocols. commonssloptions() restricts the
-    # set of allowed protocols.
-    protocol = ssl.PROTOCOL_SSLv23
-    options = commonssloptions(b'tls1.0')
+    if util.safehasattr(ssl, 'PROTOCOL_TLS_SERVER'):
+        # python 3.7+
+        sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+        sslcontext.options |= getattr(ssl, 'OP_NO_COMPRESSION', 0)
 
-    # This config option is intended for use in tests only. It is a giant
-    # footgun to kill security. Don't define it.
-    exactprotocol = ui.config(b'devel', b'serverexactprotocol')
-    if exactprotocol == b'tls1.0':
-        if b'tls1.0' not in supportedprotocols:
-            raise error.Abort(_(b'TLS 1.0 not supported by this Python'))
-        protocol = ssl.PROTOCOL_TLSv1
-    elif exactprotocol == b'tls1.1':
-        if b'tls1.1' not in supportedprotocols:
-            raise error.Abort(_(b'TLS 1.1 not supported by this Python'))
-        protocol = ssl.PROTOCOL_TLSv1_1
-    elif exactprotocol == b'tls1.2':
-        if b'tls1.2' not in supportedprotocols:
-            raise error.Abort(_(b'TLS 1.2 not supported by this Python'))
-        protocol = ssl.PROTOCOL_TLSv1_2
-    elif exactprotocol:
-        raise error.Abort(
-            _(b'invalid value for serverexactprotocol: %s') % exactprotocol
-        )
+        # This config option is intended for use in tests only. It is a giant
+        # footgun to kill security. Don't define it.
+        exactprotocol = ui.config(b'devel', b'serverexactprotocol')
+        if exactprotocol == b'tls1.0':
+            if b'tls1.0' not in supportedprotocols:
+                raise error.Abort(_(b'TLS 1.0 not supported by this Python'))
+            with warnings.catch_warnings():
+                warnings.filterwarnings(
+                    'ignore',
+                    'ssl.TLSVersion.TLSv1 is deprecated',
+                    DeprecationWarning,
+                )
+                sslcontext.minimum_version = ssl.TLSVersion.TLSv1
+                sslcontext.maximum_version = ssl.TLSVersion.TLSv1
+        elif exactprotocol == b'tls1.1':
+            if b'tls1.1' not in supportedprotocols:
+                raise error.Abort(_(b'TLS 1.1 not supported by this Python'))
+            with warnings.catch_warnings():
+                warnings.filterwarnings(
+                    'ignore',
+                    'ssl.TLSVersion.TLSv1_1 is deprecated',
+                    DeprecationWarning,
+                )
+                sslcontext.minimum_version = ssl.TLSVersion.TLSv1_1
+                sslcontext.maximum_version = ssl.TLSVersion.TLSv1_1
+        elif exactprotocol == b'tls1.2':
+            if b'tls1.2' not in supportedprotocols:
+                raise error.Abort(_(b'TLS 1.2 not supported by this Python'))
+            sslcontext.minimum_version = ssl.TLSVersion.TLSv1_2
+            sslcontext.maximum_version = ssl.TLSVersion.TLSv1_2
+        elif exactprotocol:
+            raise error.Abort(
+                _(b'invalid value for serverexactprotocol: %s') % exactprotocol
+            )
+    else:
+        # Despite its name, PROTOCOL_SSLv23 selects the highest protocol that both
+        # ends support, including TLS protocols. commonssloptions() restricts the
+        # set of allowed protocols.
+        protocol = ssl.PROTOCOL_SSLv23
+        options = commonssloptions(b'tls1.0')
 
-    # We /could/ use create_default_context() here since it doesn't load
-    # CAs when configured for client auth. However, it is hard-coded to
-    # use ssl.PROTOCOL_SSLv23 which may not be appropriate here.
-    sslcontext = ssl.SSLContext(protocol)
-    sslcontext.options |= options
+        # This config option is intended for use in tests only. It is a giant
+        # footgun to kill security. Don't define it.
+        exactprotocol = ui.config(b'devel', b'serverexactprotocol')
+        if exactprotocol == b'tls1.0':
+            if b'tls1.0' not in supportedprotocols:
+                raise error.Abort(_(b'TLS 1.0 not supported by this Python'))
+            protocol = ssl.PROTOCOL_TLSv1
+        elif exactprotocol == b'tls1.1':
+            if b'tls1.1' not in supportedprotocols:
+                raise error.Abort(_(b'TLS 1.1 not supported by this Python'))
+            protocol = ssl.PROTOCOL_TLSv1_1
+        elif exactprotocol == b'tls1.2':
+            if b'tls1.2' not in supportedprotocols:
+                raise error.Abort(_(b'TLS 1.2 not supported by this Python'))
+            protocol = ssl.PROTOCOL_TLSv1_2
+        elif exactprotocol:
+            raise error.Abort(
+                _(b'invalid value for serverexactprotocol: %s') % exactprotocol
+            )
+
+        # We /could/ use create_default_context() here since it doesn't load
+        # CAs when configured for client auth. However, it is hard-coded to
+        # use ssl.PROTOCOL_SSLv23 which may not be appropriate here.
+        sslcontext = ssl.SSLContext(protocol)
+        sslcontext.options |= options
 
     # Improve forward secrecy.
     sslcontext.options |= getattr(ssl, 'OP_SINGLE_DH_USE', 0)
     sslcontext.options |= getattr(ssl, 'OP_SINGLE_ECDH_USE', 0)
 
-    # Use the list of more secure ciphers if found in the ssl module.
-    if util.safehasattr(ssl, b'_RESTRICTED_SERVER_CIPHERS'):
+    # In tests, allow insecure ciphers
+    # Otherwise, use the list of more secure ciphers if found in the ssl module.
+    if exactprotocol:
+        sslcontext.set_ciphers('DEFAULT')
+    elif util.safehasattr(ssl, b'_RESTRICTED_SERVER_CIPHERS'):
         sslcontext.options |= getattr(ssl, 'OP_CIPHER_SERVER_PREFERENCE', 0)
         # pytype: disable=module-attr
         sslcontext.set_ciphers(ssl._RESTRICTED_SERVER_CIPHERS)
--- a/mercurial/stack.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/stack.py	Wed May 04 18:17:44 2022 +0200
@@ -5,8 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
-
 
 def getstack(repo, rev=None):
     """return a sorted smartrev of the stack containing either rev if it is
--- a/mercurial/state.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/state.py	Wed May 04 18:17:44 2022 +0200
@@ -17,7 +17,6 @@
 the data.
 """
 
-from __future__ import absolute_import
 
 import contextlib
 
@@ -40,7 +39,7 @@
         assert t
 
 
-class cmdstate(object):
+class cmdstate:
     """a wrapper class to store the state of commands like `rebase`, `graft`,
     `histedit`, `shelve` etc. Extensions can also use this to write state files.
 
@@ -103,7 +102,7 @@
         return self._repo.vfs.exists(self.fname)
 
 
-class _statecheck(object):
+class _statecheck:
     """a utility class that deals with multistep operations like graft,
     histedit, bisect, update etc and check whether such commands
     are in an unfinished conditition or not and return appropriate message
--- a/mercurial/statichttprepo.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/statichttprepo.py	Wed May 04 18:17:44 2022 +0200
@@ -7,7 +7,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import errno
 
@@ -35,7 +34,7 @@
 urlreq = util.urlreq
 
 
-class httprangereader(object):
+class httprangereader:
     def __init__(self, url, opener):
         # we assume opener has HTTPRangeHandler
         self.url = url
--- a/mercurial/statprof.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/statprof.py	Wed May 04 18:17:44 2022 +0200
@@ -101,7 +101,6 @@
 main thread's work patterns.
 """
 # no-check-code
-from __future__ import absolute_import, division, print_function
 
 import collections
 import contextlib
@@ -155,7 +154,7 @@
 ## Collection data structures
 
 
-class ProfileState(object):
+class ProfileState:
     def __init__(self, frequency=None):
         self.reset(frequency)
         self.track = b'cpu'
@@ -203,7 +202,7 @@
 state = ProfileState()
 
 
-class CodeSite(object):
+class CodeSite:
     cache = {}
 
     __slots__ = ('path', 'lineno', 'function', 'source')
@@ -261,7 +260,7 @@
         return '%s:%s' % (self.filename(), self.function)
 
 
-class Sample(object):
+class Sample:
     __slots__ = ('stack', 'time')
 
     def __init__(self, stack, time):
@@ -435,7 +434,7 @@
 ## Reporting API
 
 
-class SiteStats(object):
+class SiteStats:
     def __init__(self, site):
         self.site = site
         self.selfcount = 0
@@ -475,7 +474,7 @@
                 if i == 0:
                     sitestat.addself()
 
-        return [s for s in pycompat.itervalues(stats)]
+        return [s for s in stats.values()]
 
 
 class DisplayFormats:
@@ -574,7 +573,7 @@
 
     # compute sums for each function
     functiondata = []
-    for fname, sitestats in pycompat.iteritems(grouped):
+    for fname, sitestats in grouped.items():
         total_cum_sec = 0
         total_self_sec = 0
         total_percent = 0
@@ -608,9 +607,7 @@
             # only show line numbers for significant locations (>1% time spent)
             if stat.selfpercent() > 1:
                 source = stat.site.getsource(25)
-                if sys.version_info.major >= 3 and not isinstance(
-                    source, bytes
-                ):
+                if not isinstance(source, bytes):
                     source = pycompat.bytestr(source)
 
                 stattuple = (
@@ -653,7 +650,7 @@
                 else:
                     children[site] = 1
 
-    parents = [(parent, count) for parent, count in pycompat.iteritems(parents)]
+    parents = [(parent, count) for parent, count in parents.items()]
     parents.sort(reverse=True, key=lambda x: x[1])
     for parent, count in parents:
         fp.write(
@@ -697,7 +694,7 @@
         )
     )
 
-    children = [(child, count) for child, count in pycompat.iteritems(children)]
+    children = [(child, count) for child, count in children.items()]
     children.sort(reverse=True, key=lambda x: x[1])
     for child, count in children:
         fp.write(
@@ -711,7 +708,7 @@
 
 
 def display_hotpath(data, fp, limit=0.05, **kwargs):
-    class HotNode(object):
+    class HotNode:
         def __init__(self, site):
             self.site = site
             self.count = 0
@@ -746,9 +743,7 @@
     def _write(node, depth, multiple_siblings):
         site = node.site
         visiblechildren = [
-            c
-            for c in pycompat.itervalues(node.children)
-            if c.count >= (limit * root.count)
+            c for c in node.children.values() if c.count >= (limit * root.count)
         ]
         if site:
             indent = depth * 2 - 1
@@ -784,9 +779,7 @@
             )
 
             finalstring = liststring + codestring
-            childrensamples = sum(
-                [c.count for c in pycompat.itervalues(node.children)]
-            )
+            childrensamples = sum([c.count for c in node.children.values()])
             # Make frames that performed more than 10% of the operation red
             if node.count - childrensamples > (0.1 * root.count):
                 finalstring = b'\033[91m' + finalstring + b'\033[0m'
@@ -828,7 +821,7 @@
     fd, path = pycompat.mkstemp()
 
     with open(path, b"w+") as file:
-        for line, count in pycompat.iteritems(lines):
+        for line, count in lines.items():
             file.write(b"%s %d\n" % (line, count))
 
     if outputfile is None:
--- a/mercurial/store.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/store.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import errno
 import functools
@@ -145,7 +144,7 @@
         cmap[xchr(x)] = e + xchr(x).lower()
 
     dmap = {}
-    for k, v in pycompat.iteritems(cmap):
+    for k, v in cmap.items():
         dmap[v] = k
 
     def decode(s):
@@ -456,7 +455,7 @@
 FILETYPE_OTHER = FILEFLAGS_OTHER
 
 
-class basicstore(object):
+class basicstore:
     '''base class for local repository stores'''
 
     def __init__(self, path, vfstype):
@@ -602,7 +601,7 @@
         return [b'requires', b'00changelog.i'] + [b'store/' + f for f in _data]
 
 
-class fncache(object):
+class fncache:
     # the filename used to be partially encoded
     # hence the encodedir/decodedir dance
     def __init__(self, vfs):
@@ -662,7 +661,7 @@
         """make sure there is no empty string in entries"""
         if b'' in self.entries:
             fp.seek(0)
-            for n, line in enumerate(util.iterfile(fp)):
+            for n, line in enumerate(fp):
                 if not line.rstrip(b'\n'):
                     t = _(b'invalid entry in fncache, line %d') % (n + 1)
                     if warn:
--- a/mercurial/streamclone.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/streamclone.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import contextlib
 import errno
@@ -517,7 +516,7 @@
     nodemap.post_stream_cleanup(repo)
 
 
-class streamcloneapplier(object):
+class streamcloneapplier:
     """Class to manage applying streaming clone bundles.
 
     We need to wrap ``applybundlev1()`` in a dedicated type to enable bundle
--- a/mercurial/strip.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/strip.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 from .i18n import _
 from .pycompat import getattr
 from . import (
@@ -195,7 +193,7 @@
             # a revision we have to only delete the bookmark and not strip
             # anything. revsets cannot detect that case.
             nodetobookmarks = {}
-            for mark, node in pycompat.iteritems(repomarks):
+            for mark, node in repomarks.items():
                 nodetobookmarks.setdefault(node, []).append(mark)
             for marks in nodetobookmarks.values():
                 if bookmarks.issuperset(marks):
--- a/mercurial/subrepo.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/subrepo.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import copy
 import errno
@@ -221,7 +220,7 @@
 # subrepo classes need to implement the following abstract class:
 
 
-class abstractsubrepo(object):
+class abstractsubrepo:
     def __init__(self, ctx, path):
         """Initialize abstractsubrepo part
 
@@ -1771,7 +1770,7 @@
             for b in rev2branch[self._state[1]]:
                 if b.startswith(b'refs/remotes/origin/'):
                     return True
-        for b, revision in pycompat.iteritems(branch2rev):
+        for b, revision in branch2rev.items():
             if b.startswith(b'refs/remotes/origin/'):
                 if self._gitisancestor(self._state[1], revision):
                     return True
--- a/mercurial/subrepoutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/subrepoutil.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import errno
 import os
@@ -191,7 +190,7 @@
         repo.ui.debug(b"  subrepo %s: %s %s\n" % (s, msg, r))
 
     promptssrc = filemerge.partextras(labels)
-    for s, l in sorted(pycompat.iteritems(s1)):
+    for s, l in sorted(s1.items()):
         a = sa.get(s, nullstate)
         ld = l  # local state with possible dirty flag for compares
         if wctx.sub(s).dirty():
--- a/mercurial/tagmerge.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/tagmerge.py	Wed May 04 18:17:44 2022 +0200
@@ -71,7 +71,6 @@
 #         - put blocks whose nodes come all from p2 first
 #     - write the tag blocks in the sorted order
 
-from __future__ import absolute_import
 
 from .i18n import _
 from . import (
--- a/mercurial/tags.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/tags.py	Wed May 04 18:17:44 2022 +0200
@@ -10,7 +10,6 @@
 # Eventually, it could take care of updating (adding/removing/moving)
 # tags too.
 
-from __future__ import absolute_import
 
 import errno
 import io
@@ -26,7 +25,6 @@
     encoding,
     error,
     match as matchmod,
-    pycompat,
     scmutil,
     util,
 )
@@ -355,7 +353,7 @@
     if tagtype is None:
         assert tagtypes is None
 
-    for name, nodehist in pycompat.iteritems(filetags):
+    for name, nodehist in filetags.items():
         if name not in alltags:
             alltags[name] = nodehist
             if tagtype is not None:
@@ -508,7 +506,7 @@
 
     if unknown_entries:
         fixed_nodemap = fnodescache.refresh_invalid_nodes(unknown_entries)
-        for node, fnode in pycompat.iteritems(fixed_nodemap):
+        for node, fnode in fixed_nodemap.items():
             if fnode != repo.nullid:
                 cachefnode[node] = fnode
 
@@ -550,7 +548,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(pycompat.iteritems(cachetags)):
+    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))
@@ -686,7 +684,7 @@
 _fnodesmissingrec = b'\xff' * 24
 
 
-class hgtagsfnodescache(object):
+class hgtagsfnodescache:
     """Persistent cache mapping revisions to .hgtags filenodes.
 
     The cache is an array of records. Each item in the array corresponds to
--- a/mercurial/templatefilters.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/templatefilters.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import os
 import re
@@ -269,10 +268,7 @@
 @templatefilter(b'firstline', intype=bytes)
 def firstline(text):
     """Any text. Returns the first line of text."""
-    try:
-        return text.splitlines(True)[0].rstrip(b'\r\n')
-    except IndexError:
-        return b''
+    return stringutil.firstline(text)
 
 
 @templatefilter(b'hex', intype=bytes)
@@ -335,7 +331,7 @@
         return b'false'
     elif obj is True:
         return b'true'
-    elif isinstance(obj, (int, pycompat.long, float)):
+    elif isinstance(obj, (int, int, float)):
         return pycompat.bytestr(obj)
     elif isinstance(obj, bytes):
         return b'"%s"' % encoding.jsonescape(obj, paranoid=paranoid)
@@ -347,7 +343,7 @@
         out = [
             b'"%s": %s'
             % (encoding.jsonescape(k, paranoid=paranoid), json(v, paranoid))
-            for k, v in sorted(pycompat.iteritems(obj))
+            for k, v in sorted(obj.items())
         ]
         return b'{' + b', '.join(out) + b'}'
     elif util.safehasattr(obj, b'__iter__'):
@@ -373,9 +369,7 @@
     """Any text. Returns the input text rendered as a sequence of
     XML entities.
     """
-    text = pycompat.unicode(
-        text, pycompat.sysstr(encoding.encoding), r'replace'
-    )
+    text = str(text, pycompat.sysstr(encoding.encoding), r'replace')
     return b''.join([b'&#%d;' % ord(c) for c in text])
 
 
@@ -549,7 +543,7 @@
 
 def loadfilter(ui, extname, registrarobj):
     """Load template filter from specified registrarobj"""
-    for name, func in pycompat.iteritems(registrarobj._table):
+    for name, func in registrarobj._table.items():
         filters[name] = func
 
 
--- a/mercurial/templatefuncs.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/templatefuncs.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import re
 
@@ -89,7 +88,7 @@
 
     data.update(
         (k, evalfuncarg(context, mapping, v))
-        for k, v in pycompat.iteritems(args[b'kwargs'])
+        for k, v in args[b'kwargs'].items()
     )
     return templateutil.hybriddict(data)
 
@@ -911,7 +910,7 @@
 
 def loadfunction(ui, extname, registrarobj):
     """Load template function from specified registrarobj"""
-    for name, func in pycompat.iteritems(registrarobj._table):
+    for name, func in registrarobj._table.items():
         funcs[name] = func
 
 
--- a/mercurial/templatekw.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/templatekw.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from .i18n import _
 from .node import (
@@ -603,7 +602,7 @@
         # 'name' for iterating over namespaces, templatename for local reference
         return lambda v: {b'name': v, ns.templatename: v}
 
-    for k, ns in pycompat.iteritems(repo.names):
+    for k, ns in repo.names.items():
         names = ns.names(repo, ctx.node())
         f = _showcompatlist(context, mapping, b'name', names)
         namespaces[k] = _hybrid(f, names, makensmapfn(ns), pycompat.identity)
@@ -686,12 +685,12 @@
         d = {b'name': k}
         if len(ps) == 1:
             d[b'url'] = ps[0].rawloc
-            sub_opts = pycompat.iteritems(ps[0].suboptions)
+            sub_opts = ps[0].suboptions.items()
             sub_opts = util.sortdict(sorted(sub_opts))
             d.update(sub_opts)
         path_dict = util.sortdict()
         for p in ps:
-            sub_opts = util.sortdict(sorted(pycompat.iteritems(p.suboptions)))
+            sub_opts = util.sortdict(sorted(p.suboptions.items()))
             path_dict[b'url'] = p.rawloc
             path_dict.update(sub_opts)
             d[b'urls'] = [path_dict]
@@ -1024,7 +1023,7 @@
 
 def loadkeyword(ui, extname, registrarobj):
     """Load template keyword from specified registrarobj"""
-    for name, func in pycompat.iteritems(registrarobj._table):
+    for name, func in registrarobj._table.items():
         keywords[name] = func
 
 
--- a/mercurial/templater.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/templater.py	Wed May 04 18:17:44 2022 +0200
@@ -65,7 +65,6 @@
     operation.
 """
 
-from __future__ import absolute_import, print_function
 
 import abc
 import os
@@ -531,8 +530,7 @@
 
     def compiledict(xs):
         return util.sortdict(
-            (k, compileexp(x, context, curmethods))
-            for k, x in pycompat.iteritems(xs)
+            (k, compileexp(x, context, curmethods)) for k, x in xs.items()
         )
 
     def compilelist(xs):
@@ -628,7 +626,7 @@
     return s[1:-1]
 
 
-class resourcemapper(object):  # pytype: disable=ignored-metaclass
+class resourcemapper:  # pytype: disable=ignored-metaclass
     """Mapper of internal template resources"""
 
     __metaclass__ = abc.ABCMeta
@@ -665,7 +663,7 @@
         return {}
 
 
-class engine(object):
+class engine:
     """template expansion engine.
 
     template expansion works like this. a map file contains key=value
@@ -709,7 +707,7 @@
         newres = self._resources.availablekeys(newmapping)
         mapping = {
             k: v
-            for k, v in pycompat.iteritems(origmapping)
+            for k, v in origmapping.items()
             if (
                 k in knownres  # not a symbol per self.symbol()
                 or newres.isdisjoint(self._defaultrequires(k))
@@ -921,7 +919,7 @@
     return cache, tmap, aliases
 
 
-class loader(object):
+class loader:
     """Load template fragments optionally from a map file"""
 
     def __init__(self, cache, aliases):
@@ -996,7 +994,7 @@
         return syms
 
 
-class templater(object):
+class templater:
     def __init__(
         self,
         filters=None,
--- a/mercurial/templateutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/templateutil.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import abc
 import types
@@ -32,7 +31,7 @@
     pass
 
 
-class wrapped(object):  # pytype: disable=ignored-metaclass
+class wrapped:  # pytype: disable=ignored-metaclass
     """Object requiring extra conversion prior to displaying or processing
     as value
 
@@ -109,7 +108,7 @@
         """
 
 
-class mappable(object):  # pytype: disable=ignored-metaclass
+class mappable:  # pytype: disable=ignored-metaclass
     """Object which can be converted to a single template mapping"""
 
     __metaclass__ = abc.ABCMeta
@@ -311,7 +310,7 @@
         if util.safehasattr(self._values, b'get'):
             values = {
                 k: v
-                for k, v in pycompat.iteritems(self._values)
+                for k, v in self._values.items()
                 if select(self._wrapvalue(k, v))
             }
         else:
@@ -343,10 +342,7 @@
         # TODO: make it non-recursive for trivial lists/dicts
         xs = self._values
         if util.safehasattr(xs, b'get'):
-            return {
-                k: unwrapvalue(context, mapping, v)
-                for k, v in pycompat.iteritems(xs)
-            }
+            return {k: unwrapvalue(context, mapping, v) for k, v in xs.items()}
         return [unwrapvalue(context, mapping, x) for x in xs]
 
 
@@ -538,7 +534,7 @@
             items.append(
                 {
                     k: unwrapvalue(context, lm, v)
-                    for k, v in pycompat.iteritems(nm)
+                    for k, v in nm.items()
                     if k not in knownres
                 }
             )
@@ -716,7 +712,7 @@
     This exists for backward compatibility with the old-style template. Use
     hybriddict() for new template keywords.
     """
-    c = [{key: k, value: v} for k, v in pycompat.iteritems(data)]
+    c = [{key: k, value: v} for k, v in data.items()]
     f = _showcompatlist(context, mapping, name, c, plural, separator)
     return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
 
--- a/mercurial/testing/__init__.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/testing/__init__.py	Wed May 04 18:17:44 2022 +0200
@@ -1,8 +1,3 @@
-from __future__ import (
-    absolute_import,
-    division,
-)
-
 import os
 import time
 
--- a/mercurial/testing/revlog.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/testing/revlog.py	Wed May 04 18:17:44 2022 +0200
@@ -1,4 +1,3 @@
-from __future__ import absolute_import
 import unittest
 
 # picked from test-parse-index2, copied rather than imported
--- a/mercurial/testing/storage.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/testing/storage.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import unittest
 
--- a/mercurial/thirdparty/concurrent/LICENSE	Wed May 04 18:00:01 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,48 +0,0 @@
-PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
---------------------------------------------
-
-1. This LICENSE AGREEMENT is between the Python Software Foundation
-("PSF"), and the Individual or Organization ("Licensee") accessing and
-otherwise using this software ("Python") in source or binary form and
-its associated documentation.
-
-2. Subject to the terms and conditions of this License Agreement, PSF
-hereby grants Licensee a nonexclusive, royalty-free, world-wide
-license to reproduce, analyze, test, perform and/or display publicly,
-prepare derivative works, distribute, and otherwise use Python
-alone or in any derivative version, provided, however, that PSF's
-License Agreement and PSF's notice of copyright, i.e., "Copyright (c)
-2001, 2002, 2003, 2004, 2005, 2006 Python Software Foundation; All Rights
-Reserved" are retained in Python alone or in any derivative version 
-prepared by Licensee.
-
-3. In the event Licensee prepares a derivative work that is based on
-or incorporates Python or any part thereof, and wants to make
-the derivative work available to others as provided herein, then
-Licensee hereby agrees to include in any such work a brief summary of
-the changes made to Python.
-
-4. PSF is making Python available to Licensee on an "AS IS"
-basis.  PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
-IMPLIED.  BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
-DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
-FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
-INFRINGE ANY THIRD PARTY RIGHTS.
-
-5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
-FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
-A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
-OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
-
-6. This License Agreement will automatically terminate upon a material
-breach of its terms and conditions.
-
-7. Nothing in this License Agreement shall be deemed to create any
-relationship of agency, partnership, or joint venture between PSF and
-Licensee.  This License Agreement does not grant permission to use PSF
-trademarks or trade name in a trademark sense to endorse or promote
-products or services of Licensee, or any third party.
-
-8. By copying, installing or otherwise using Python, Licensee
-agrees to be bound by the terms and conditions of this License
-Agreement.
--- a/mercurial/thirdparty/concurrent/futures/__init__.py	Wed May 04 18:00:01 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,27 +0,0 @@
-# Copyright 2009 Brian Quinlan. All Rights Reserved.
-# Licensed to PSF under a Contributor Agreement.
-
-"""Execute computations asynchronously using threads or processes."""
-
-from __future__ import absolute_import
-
-__author__ = 'Brian Quinlan (brian@sweetapp.com)'
-
-from ._base import (
-    FIRST_COMPLETED,
-    FIRST_EXCEPTION,
-    ALL_COMPLETED,
-    CancelledError,
-    TimeoutError,
-    Future,
-    Executor,
-    wait,
-    as_completed,
-)
-from .thread import ThreadPoolExecutor
-
-try:
-    from .process import ProcessPoolExecutor
-except ImportError:
-    # some platforms don't have multiprocessing
-    pass
--- a/mercurial/thirdparty/concurrent/futures/_base.py	Wed May 04 18:00:01 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,669 +0,0 @@
-# Copyright 2009 Brian Quinlan. All Rights Reserved.
-# Licensed to PSF under a Contributor Agreement.
-
-from __future__ import absolute_import
-
-import collections
-import logging
-import threading
-import itertools
-import time
-import types
-
-__author__ = 'Brian Quinlan (brian@sweetapp.com)'
-
-FIRST_COMPLETED = 'FIRST_COMPLETED'
-FIRST_EXCEPTION = 'FIRST_EXCEPTION'
-ALL_COMPLETED = 'ALL_COMPLETED'
-_AS_COMPLETED = '_AS_COMPLETED'
-
-# Possible future states (for internal use by the futures package).
-PENDING = 'PENDING'
-RUNNING = 'RUNNING'
-# The future was cancelled by the user...
-CANCELLED = 'CANCELLED'
-# ...and _Waiter.add_cancelled() was called by a worker.
-CANCELLED_AND_NOTIFIED = 'CANCELLED_AND_NOTIFIED'
-FINISHED = 'FINISHED'
-
-_FUTURE_STATES = [
-    PENDING,
-    RUNNING,
-    CANCELLED,
-    CANCELLED_AND_NOTIFIED,
-    FINISHED
-]
-
-_STATE_TO_DESCRIPTION_MAP = {
-    PENDING: "pending",
-    RUNNING: "running",
-    CANCELLED: "cancelled",
-    CANCELLED_AND_NOTIFIED: "cancelled",
-    FINISHED: "finished"
-}
-
-# Logger for internal use by the futures package.
-LOGGER = logging.getLogger("concurrent.futures")
-
-class Error(Exception):
-    """Base class for all future-related exceptions."""
-    pass
-
-class CancelledError(Error):
-    """The Future was cancelled."""
-    pass
-
-class TimeoutError(Error):
-    """The operation exceeded the given deadline."""
-    pass
-
-class _Waiter(object):
-    """Provides the event that wait() and as_completed() block on."""
-    def __init__(self):
-        self.event = threading.Event()
-        self.finished_futures = []
-
-    def add_result(self, future):
-        self.finished_futures.append(future)
-
-    def add_exception(self, future):
-        self.finished_futures.append(future)
-
-    def add_cancelled(self, future):
-        self.finished_futures.append(future)
-
-class _AsCompletedWaiter(_Waiter):
-    """Used by as_completed()."""
-
-    def __init__(self):
-        super(_AsCompletedWaiter, self).__init__()
-        self.lock = threading.Lock()
-
-    def add_result(self, future):
-        with self.lock:
-            super(_AsCompletedWaiter, self).add_result(future)
-            self.event.set()
-
-    def add_exception(self, future):
-        with self.lock:
-            super(_AsCompletedWaiter, self).add_exception(future)
-            self.event.set()
-
-    def add_cancelled(self, future):
-        with self.lock:
-            super(_AsCompletedWaiter, self).add_cancelled(future)
-            self.event.set()
-
-class _FirstCompletedWaiter(_Waiter):
-    """Used by wait(return_when=FIRST_COMPLETED)."""
-
-    def add_result(self, future):
-        super(_FirstCompletedWaiter, self).add_result(future)
-        self.event.set()
-
-    def add_exception(self, future):
-        super(_FirstCompletedWaiter, self).add_exception(future)
-        self.event.set()
-
-    def add_cancelled(self, future):
-        super(_FirstCompletedWaiter, self).add_cancelled(future)
-        self.event.set()
-
-class _AllCompletedWaiter(_Waiter):
-    """Used by wait(return_when=FIRST_EXCEPTION and ALL_COMPLETED)."""
-
-    def __init__(self, num_pending_calls, stop_on_exception):
-        self.num_pending_calls = num_pending_calls
-        self.stop_on_exception = stop_on_exception
-        self.lock = threading.Lock()
-        super(_AllCompletedWaiter, self).__init__()
-
-    def _decrement_pending_calls(self):
-        with self.lock:
-            self.num_pending_calls -= 1
-            if not self.num_pending_calls:
-                self.event.set()
-
-    def add_result(self, future):
-        super(_AllCompletedWaiter, self).add_result(future)
-        self._decrement_pending_calls()
-
-    def add_exception(self, future):
-        super(_AllCompletedWaiter, self).add_exception(future)
-        if self.stop_on_exception:
-            self.event.set()
-        else:
-            self._decrement_pending_calls()
-
-    def add_cancelled(self, future):
-        super(_AllCompletedWaiter, self).add_cancelled(future)
-        self._decrement_pending_calls()
-
-class _AcquireFutures(object):
-    """A context manager that does an ordered acquire of Future conditions."""
-
-    def __init__(self, futures):
-        self.futures = sorted(futures, key=id)
-
-    def __enter__(self):
-        for future in self.futures:
-            future._condition.acquire()
-
-    def __exit__(self, *args):
-        for future in self.futures:
-            future._condition.release()
-
-def _create_and_install_waiters(fs, return_when):
-    if return_when == _AS_COMPLETED:
-        waiter = _AsCompletedWaiter()
-    elif return_when == FIRST_COMPLETED:
-        waiter = _FirstCompletedWaiter()
-    else:
-        pending_count = sum(
-                f._state not in [CANCELLED_AND_NOTIFIED, FINISHED] for f in fs)
-
-        if return_when == FIRST_EXCEPTION:
-            waiter = _AllCompletedWaiter(pending_count, stop_on_exception=True)
-        elif return_when == ALL_COMPLETED:
-            waiter = _AllCompletedWaiter(pending_count, stop_on_exception=False)
-        else:
-            raise ValueError("Invalid return condition: %r" % return_when)
-
-    for f in fs:
-        f._waiters.append(waiter)
-
-    return waiter
-
-
-def _yield_finished_futures(fs, waiter, ref_collect):
-    """
-    Iterate on the list *fs*, yielding finished futures one by one in
-    reverse order.
-    Before yielding a future, *waiter* is removed from its waiters
-    and the future is removed from each set in the collection of sets
-    *ref_collect*.
-
-    The aim of this function is to avoid keeping stale references after
-    the future is yielded and before the iterator resumes.
-    """
-    while fs:
-        f = fs[-1]
-        for futures_set in ref_collect:
-            futures_set.remove(f)
-        with f._condition:
-            f._waiters.remove(waiter)
-        del f
-        # Careful not to keep a reference to the popped value
-        yield fs.pop()
-
-
-def as_completed(fs, timeout=None):
-    """An iterator over the given futures that yields each as it completes.
-
-    Args:
-        fs: The sequence of Futures (possibly created by different Executors) to
-            iterate over.
-        timeout: The maximum number of seconds to wait. If None, then there
-            is no limit on the wait time.
-
-    Returns:
-        An iterator that yields the given Futures as they complete (finished or
-        cancelled). If any given Futures are duplicated, they will be returned
-        once.
-
-    Raises:
-        TimeoutError: If the entire result iterator could not be generated
-            before the given timeout.
-    """
-    if timeout is not None:
-        end_time = timeout + time.time()
-
-    fs = set(fs)
-    total_futures = len(fs)
-    with _AcquireFutures(fs):
-        finished = set(
-                f for f in fs
-                if f._state in [CANCELLED_AND_NOTIFIED, FINISHED])
-        pending = fs - finished
-        waiter = _create_and_install_waiters(fs, _AS_COMPLETED)
-    finished = list(finished)
-    try:
-        for f in _yield_finished_futures(finished, waiter,
-                                         ref_collect=(fs,)):
-            f = [f]
-            yield f.pop()
-
-        while pending:
-            if timeout is None:
-                wait_timeout = None
-            else:
-                wait_timeout = end_time - time.time()
-                if wait_timeout < 0:
-                    raise TimeoutError(
-                            '%d (of %d) futures unfinished' % (
-                            len(pending), total_futures))
-
-            waiter.event.wait(wait_timeout)
-
-            with waiter.lock:
-                finished = waiter.finished_futures
-                waiter.finished_futures = []
-                waiter.event.clear()
-
-            # reverse to keep finishing order
-            finished.reverse()
-            for f in _yield_finished_futures(finished, waiter,
-                                             ref_collect=(fs, pending)):
-                f = [f]
-                yield f.pop()
-
-    finally:
-        # Remove waiter from unfinished futures
-        for f in fs:
-            with f._condition:
-                f._waiters.remove(waiter)
-
-DoneAndNotDoneFutures = collections.namedtuple(
-        'DoneAndNotDoneFutures', 'done not_done')
-def wait(fs, timeout=None, return_when=ALL_COMPLETED):
-    """Wait for the futures in the given sequence to complete.
-
-    Args:
-        fs: The sequence of Futures (possibly created by different Executors) to
-            wait upon.
-        timeout: The maximum number of seconds to wait. If None, then there
-            is no limit on the wait time.
-        return_when: Indicates when this function should return. The options
-            are:
-
-            FIRST_COMPLETED - Return when any future finishes or is
-                              cancelled.
-            FIRST_EXCEPTION - Return when any future finishes by raising an
-                              exception. If no future raises an exception
-                              then it is equivalent to ALL_COMPLETED.
-            ALL_COMPLETED -   Return when all futures finish or are cancelled.
-
-    Returns:
-        A named 2-tuple of sets. The first set, named 'done', contains the
-        futures that completed (is finished or cancelled) before the wait
-        completed. The second set, named 'not_done', contains uncompleted
-        futures.
-    """
-    with _AcquireFutures(fs):
-        done = set(f for f in fs
-                   if f._state in [CANCELLED_AND_NOTIFIED, FINISHED])
-        not_done = set(fs) - done
-
-        if (return_when == FIRST_COMPLETED) and done:
-            return DoneAndNotDoneFutures(done, not_done)
-        elif (return_when == FIRST_EXCEPTION) and done:
-            if any(f for f in done
-                   if not f.cancelled() and f.exception() is not None):
-                return DoneAndNotDoneFutures(done, not_done)
-
-        if len(done) == len(fs):
-            return DoneAndNotDoneFutures(done, not_done)
-
-        waiter = _create_and_install_waiters(fs, return_when)
-
-    waiter.event.wait(timeout)
-    for f in fs:
-        with f._condition:
-            f._waiters.remove(waiter)
-
-    done.update(waiter.finished_futures)
-    return DoneAndNotDoneFutures(done, set(fs) - done)
-
-class Future(object):
-    """Represents the result of an asynchronous computation."""
-
-    def __init__(self):
-        """Initializes the future. Should not be called by clients."""
-        self._condition = threading.Condition()
-        self._state = PENDING
-        self._result = None
-        self._exception = None
-        self._traceback = None
-        self._waiters = []
-        self._done_callbacks = []
-
-    def _invoke_callbacks(self):
-        for callback in self._done_callbacks:
-            try:
-                callback(self)
-            except Exception:
-                LOGGER.exception('exception calling callback for %r', self)
-            except BaseException:
-                # Explicitly let all other new-style exceptions through so
-                # that we can catch all old-style exceptions with a simple
-                # "except:" clause below.
-                #
-                # All old-style exception objects are instances of
-                # types.InstanceType, but "except types.InstanceType:" does
-                # not catch old-style exceptions for some reason.  Thus, the
-                # only way to catch all old-style exceptions without catching
-                # any new-style exceptions is to filter out the new-style
-                # exceptions, which all derive from BaseException.
-                raise
-            except:
-                # Because of the BaseException clause above, this handler only
-                # executes for old-style exception objects.
-                LOGGER.exception('exception calling callback for %r', self)
-
-    def __repr__(self):
-        with self._condition:
-            if self._state == FINISHED:
-                if self._exception:
-                    return '<%s at %#x state=%s raised %s>' % (
-                        self.__class__.__name__,
-                        id(self),
-                        _STATE_TO_DESCRIPTION_MAP[self._state],
-                        self._exception.__class__.__name__)
-                else:
-                    return '<%s at %#x state=%s returned %s>' % (
-                        self.__class__.__name__,
-                        id(self),
-                        _STATE_TO_DESCRIPTION_MAP[self._state],
-                        self._result.__class__.__name__)
-            return '<%s at %#x state=%s>' % (
-                    self.__class__.__name__,
-                    id(self),
-                   _STATE_TO_DESCRIPTION_MAP[self._state])
-
-    def cancel(self):
-        """Cancel the future if possible.
-
-        Returns True if the future was cancelled, False otherwise. A future
-        cannot be cancelled if it is running or has already completed.
-        """
-        with self._condition:
-            if self._state in [RUNNING, FINISHED]:
-                return False
-
-            if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]:
-                return True
-
-            self._state = CANCELLED
-            self._condition.notify_all()
-
-        self._invoke_callbacks()
-        return True
-
-    def cancelled(self):
-        """Return True if the future was cancelled."""
-        with self._condition:
-            return self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]
-
-    def running(self):
-        """Return True if the future is currently executing."""
-        with self._condition:
-            return self._state == RUNNING
-
-    def done(self):
-        """Return True of the future was cancelled or finished executing."""
-        with self._condition:
-            return self._state in [CANCELLED, CANCELLED_AND_NOTIFIED, FINISHED]
-
-    def __get_result(self):
-        if self._exception:
-            if isinstance(self._exception, types.InstanceType):
-                # The exception is an instance of an old-style class, which
-                # means type(self._exception) returns types.ClassType instead
-                # of the exception's actual class type.
-                exception_type = self._exception.__class__
-            else:
-                exception_type = type(self._exception)
-            raise exception_type, self._exception, self._traceback
-        else:
-            return self._result
-
-    def add_done_callback(self, fn):
-        """Attaches a callable that will be called when the future finishes.
-
-        Args:
-            fn: A callable that will be called with this future as its only
-                argument when the future completes or is cancelled. The callable
-                will always be called by a thread in the same process in which
-                it was added. If the future has already completed or been
-                cancelled then the callable will be called immediately. These
-                callables are called in the order that they were added.
-        """
-        with self._condition:
-            if self._state not in [CANCELLED, CANCELLED_AND_NOTIFIED, FINISHED]:
-                self._done_callbacks.append(fn)
-                return
-        fn(self)
-
-    def result(self, timeout=None):
-        """Return the result of the call that the future represents.
-
-        Args:
-            timeout: The number of seconds to wait for the result if the future
-                isn't done. If None, then there is no limit on the wait time.
-
-        Returns:
-            The result of the call that the future represents.
-
-        Raises:
-            CancelledError: If the future was cancelled.
-            TimeoutError: If the future didn't finish executing before the given
-                timeout.
-            Exception: If the call raised then that exception will be raised.
-        """
-        with self._condition:
-            if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]:
-                raise CancelledError()
-            elif self._state == FINISHED:
-                return self.__get_result()
-
-            self._condition.wait(timeout)
-
-            if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]:
-                raise CancelledError()
-            elif self._state == FINISHED:
-                return self.__get_result()
-            else:
-                raise TimeoutError()
-
-    def exception_info(self, timeout=None):
-        """Return a tuple of (exception, traceback) raised by the call that the
-        future represents.
-
-        Args:
-            timeout: The number of seconds to wait for the exception if the
-                future isn't done. If None, then there is no limit on the wait
-                time.
-
-        Returns:
-            The exception raised by the call that the future represents or None
-            if the call completed without raising.
-
-        Raises:
-            CancelledError: If the future was cancelled.
-            TimeoutError: If the future didn't finish executing before the given
-                timeout.
-        """
-        with self._condition:
-            if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]:
-                raise CancelledError()
-            elif self._state == FINISHED:
-                return self._exception, self._traceback
-
-            self._condition.wait(timeout)
-
-            if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]:
-                raise CancelledError()
-            elif self._state == FINISHED:
-                return self._exception, self._traceback
-            else:
-                raise TimeoutError()
-
-    def exception(self, timeout=None):
-        """Return the exception raised by the call that the future represents.
-
-        Args:
-            timeout: The number of seconds to wait for the exception if the
-                future isn't done. If None, then there is no limit on the wait
-                time.
-
-        Returns:
-            The exception raised by the call that the future represents or None
-            if the call completed without raising.
-
-        Raises:
-            CancelledError: If the future was cancelled.
-            TimeoutError: If the future didn't finish executing before the given
-                timeout.
-        """
-        return self.exception_info(timeout)[0]
-
-    # The following methods should only be used by Executors and in tests.
-    def set_running_or_notify_cancel(self):
-        """Mark the future as running or process any cancel notifications.
-
-        Should only be used by Executor implementations and unit tests.
-
-        If the future has been cancelled (cancel() was called and returned
-        True) then any threads waiting on the future completing (though calls
-        to as_completed() or wait()) are notified and False is returned.
-
-        If the future was not cancelled then it is put in the running state
-        (future calls to running() will return True) and True is returned.
-
-        This method should be called by Executor implementations before
-        executing the work associated with this future. If this method returns
-        False then the work should not be executed.
-
-        Returns:
-            False if the Future was cancelled, True otherwise.
-
-        Raises:
-            RuntimeError: if this method was already called or if set_result()
-                or set_exception() was called.
-        """
-        with self._condition:
-            if self._state == CANCELLED:
-                self._state = CANCELLED_AND_NOTIFIED
-                for waiter in self._waiters:
-                    waiter.add_cancelled(self)
-                # self._condition.notify_all() is not necessary because
-                # self.cancel() triggers a notification.
-                return False
-            elif self._state == PENDING:
-                self._state = RUNNING
-                return True
-            else:
-                LOGGER.critical('Future %s in unexpected state: %s',
-                                id(self),
-                                self._state)
-                raise RuntimeError('Future in unexpected state')
-
-    def set_result(self, result):
-        """Sets the return value of work associated with the future.
-
-        Should only be used by Executor implementations and unit tests.
-        """
-        with self._condition:
-            self._result = result
-            self._state = FINISHED
-            for waiter in self._waiters:
-                waiter.add_result(self)
-            self._condition.notify_all()
-        self._invoke_callbacks()
-
-    def set_exception_info(self, exception, traceback):
-        """Sets the result of the future as being the given exception
-        and traceback.
-
-        Should only be used by Executor implementations and unit tests.
-        """
-        with self._condition:
-            self._exception = exception
-            self._traceback = traceback
-            self._state = FINISHED
-            for waiter in self._waiters:
-                waiter.add_exception(self)
-            self._condition.notify_all()
-        self._invoke_callbacks()
-
-    def set_exception(self, exception):
-        """Sets the result of the future as being the given exception.
-
-        Should only be used by Executor implementations and unit tests.
-        """
-        self.set_exception_info(exception, None)
-
-class Executor(object):
-    """This is an abstract base class for concrete asynchronous executors."""
-
-    def submit(self, fn, *args, **kwargs):
-        """Submits a callable to be executed with the given arguments.
-
-        Schedules the callable to be executed as fn(*args, **kwargs) and returns
-        a Future instance representing the execution of the callable.
-
-        Returns:
-            A Future representing the given call.
-        """
-        raise NotImplementedError()
-
-    def map(self, fn, *iterables, **kwargs):
-        """Returns an iterator equivalent to map(fn, iter).
-
-        Args:
-            fn: A callable that will take as many arguments as there are
-                passed iterables.
-            timeout: The maximum number of seconds to wait. If None, then there
-                is no limit on the wait time.
-
-        Returns:
-            An iterator equivalent to: map(func, *iterables) but the calls may
-            be evaluated out-of-order.
-
-        Raises:
-            TimeoutError: If the entire result iterator could not be generated
-                before the given timeout.
-            Exception: If fn(*args) raises for any values.
-        """
-        timeout = kwargs.get('timeout')
-        if timeout is not None:
-            end_time = timeout + time.time()
-
-        fs = [self.submit(fn, *args) for args in itertools.izip(*iterables)]
-
-        # Yield must be hidden in closure so that the futures are submitted
-        # before the first iterator value is required.
-        def result_iterator():
-            try:
-                # reverse to keep finishing order
-                fs.reverse()
-                while fs:
-                    # Careful not to keep a reference to the popped future
-                    if timeout is None:
-                        yield fs.pop().result()
-                    else:
-                        yield fs.pop().result(end_time - time.time())
-            finally:
-                for future in fs:
-                    future.cancel()
-        return result_iterator()
-
-    def shutdown(self, wait=True):
-        """Clean-up the resources associated with the Executor.
-
-        It is safe to call this method several times. Otherwise, no other
-        methods can be called after this one.
-
-        Args:
-            wait: If True then shutdown will not return until all running
-                futures have finished executing and the resources used by the
-                executor have been reclaimed.
-        """
-        pass
-
-    def __enter__(self):
-        return self
-
-    def __exit__(self, exc_type, exc_val, exc_tb):
-        self.shutdown(wait=True)
-        return False
--- a/mercurial/thirdparty/concurrent/futures/process.py	Wed May 04 18:00:01 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,365 +0,0 @@
-# Copyright 2009 Brian Quinlan. All Rights Reserved.
-# Licensed to PSF under a Contributor Agreement.
-
-"""Implements ProcessPoolExecutor.
-
-The follow diagram and text describe the data-flow through the system:
-
-|======================= In-process =====================|== Out-of-process ==|
-
-+----------+     +----------+       +--------+     +-----------+    +---------+
-|          |  => | Work Ids |    => |        |  => | Call Q    | => |         |
-|          |     +----------+       |        |     +-----------+    |         |
-|          |     | ...      |       |        |     | ...       |    |         |
-|          |     | 6        |       |        |     | 5, call() |    |         |
-|          |     | 7        |       |        |     | ...       |    |         |
-| Process  |     | ...      |       | Local  |     +-----------+    | Process |
-|  Pool    |     +----------+       | Worker |                      |  #1..n  |
-| Executor |                        | Thread |                      |         |
-|          |     +----------- +     |        |     +-----------+    |         |
-|          | <=> | Work Items | <=> |        | <=  | Result Q  | <= |         |
-|          |     +------------+     |        |     +-----------+    |         |
-|          |     | 6: call()  |     |        |     | ...       |    |         |
-|          |     |    future  |     |        |     | 4, result |    |         |
-|          |     | ...        |     |        |     | 3, except |    |         |
-+----------+     +------------+     +--------+     +-----------+    +---------+
-
-Executor.submit() called:
-- creates a uniquely numbered _WorkItem and adds it to the "Work Items" dict
-- adds the id of the _WorkItem to the "Work Ids" queue
-
-Local worker thread:
-- reads work ids from the "Work Ids" queue and looks up the corresponding
-  WorkItem from the "Work Items" dict: if the work item has been cancelled then
-  it is simply removed from the dict, otherwise it is repackaged as a
-  _CallItem and put in the "Call Q". New _CallItems are put in the "Call Q"
-  until "Call Q" is full. NOTE: the size of the "Call Q" is kept small because
-  calls placed in the "Call Q" can no longer be cancelled with Future.cancel().
-- reads _ResultItems from "Result Q", updates the future stored in the
-  "Work Items" dict and deletes the dict entry
-
-Process #1..n:
-- reads _CallItems from "Call Q", executes the calls, and puts the resulting
-  _ResultItems in "Request Q"
-"""
-
-from __future__ import absolute_import
-
-import atexit
-from . import _base
-import Queue as queue
-import multiprocessing
-import threading
-import weakref
-import sys
-
-__author__ = 'Brian Quinlan (brian@sweetapp.com)'
-
-# Workers are created as daemon threads and processes. This is done to allow the
-# interpreter to exit when there are still idle processes in a
-# ProcessPoolExecutor's process pool (i.e. shutdown() was not called). However,
-# allowing workers to die with the interpreter has two undesirable properties:
-#   - The workers would still be running during interpretor shutdown,
-#     meaning that they would fail in unpredictable ways.
-#   - The workers could be killed while evaluating a work item, which could
-#     be bad if the callable being evaluated has external side-effects e.g.
-#     writing to a file.
-#
-# To work around this problem, an exit handler is installed which tells the
-# workers to exit when their work queues are empty and then waits until the
-# threads/processes finish.
-
-_threads_queues = weakref.WeakKeyDictionary()
-_shutdown = False
-
-def _python_exit():
-    global _shutdown
-    _shutdown = True
-    items = list(_threads_queues.items()) if _threads_queues else ()
-    for t, q in items:
-        q.put(None)
-    for t, q in items:
-        t.join(sys.maxint)
-
-# Controls how many more calls than processes will be queued in the call queue.
-# A smaller number will mean that processes spend more time idle waiting for
-# work while a larger number will make Future.cancel() succeed less frequently
-# (Futures in the call queue cannot be cancelled).
-EXTRA_QUEUED_CALLS = 1
-
-class _WorkItem(object):
-    def __init__(self, future, fn, args, kwargs):
-        self.future = future
-        self.fn = fn
-        self.args = args
-        self.kwargs = kwargs
-
-class _ResultItem(object):
-    def __init__(self, work_id, exception=None, result=None):
-        self.work_id = work_id
-        self.exception = exception
-        self.result = result
-
-class _CallItem(object):
-    def __init__(self, work_id, fn, args, kwargs):
-        self.work_id = work_id
-        self.fn = fn
-        self.args = args
-        self.kwargs = kwargs
-
-def _process_worker(call_queue, result_queue):
-    """Evaluates calls from call_queue and places the results in result_queue.
-
-    This worker is run in a separate process.
-
-    Args:
-        call_queue: A multiprocessing.Queue of _CallItems that will be read and
-            evaluated by the worker.
-        result_queue: A multiprocessing.Queue of _ResultItems that will written
-            to by the worker.
-        shutdown: A multiprocessing.Event that will be set as a signal to the
-            worker that it should exit when call_queue is empty.
-    """
-    while True:
-        call_item = call_queue.get(block=True)
-        if call_item is None:
-            # Wake up queue management thread
-            result_queue.put(None)
-            return
-        try:
-            r = call_item.fn(*call_item.args, **call_item.kwargs)
-        except:
-            e = sys.exc_info()[1]
-            result_queue.put(_ResultItem(call_item.work_id,
-                                         exception=e))
-        else:
-            result_queue.put(_ResultItem(call_item.work_id,
-                                         result=r))
-
-def _add_call_item_to_queue(pending_work_items,
-                            work_ids,
-                            call_queue):
-    """Fills call_queue with _WorkItems from pending_work_items.
-
-    This function never blocks.
-
-    Args:
-        pending_work_items: A dict mapping work ids to _WorkItems e.g.
-            {5: <_WorkItem...>, 6: <_WorkItem...>, ...}
-        work_ids: A queue.Queue of work ids e.g. Queue([5, 6, ...]). Work ids
-            are consumed and the corresponding _WorkItems from
-            pending_work_items are transformed into _CallItems and put in
-            call_queue.
-        call_queue: A multiprocessing.Queue that will be filled with _CallItems
-            derived from _WorkItems.
-    """
-    while True:
-        if call_queue.full():
-            return
-        try:
-            work_id = work_ids.get(block=False)
-        except queue.Empty:
-            return
-        else:
-            work_item = pending_work_items[work_id]
-
-            if work_item.future.set_running_or_notify_cancel():
-                call_queue.put(_CallItem(work_id,
-                                         work_item.fn,
-                                         work_item.args,
-                                         work_item.kwargs),
-                               block=True)
-            else:
-                del pending_work_items[work_id]
-                continue
-
-def _queue_management_worker(executor_reference,
-                             processes,
-                             pending_work_items,
-                             work_ids_queue,
-                             call_queue,
-                             result_queue):
-    """Manages the communication between this process and the worker processes.
-
-    This function is run in a local thread.
-
-    Args:
-        executor_reference: A weakref.ref to the ProcessPoolExecutor that owns
-            this thread. Used to determine if the ProcessPoolExecutor has been
-            garbage collected and that this function can exit.
-        process: A list of the multiprocessing.Process instances used as
-            workers.
-        pending_work_items: A dict mapping work ids to _WorkItems e.g.
-            {5: <_WorkItem...>, 6: <_WorkItem...>, ...}
-        work_ids_queue: A queue.Queue of work ids e.g. Queue([5, 6, ...]).
-        call_queue: A multiprocessing.Queue that will be filled with _CallItems
-            derived from _WorkItems for processing by the process workers.
-        result_queue: A multiprocessing.Queue of _ResultItems generated by the
-            process workers.
-    """
-    nb_shutdown_processes = [0]
-    def shutdown_one_process():
-        """Tell a worker to terminate, which will in turn wake us again"""
-        call_queue.put(None)
-        nb_shutdown_processes[0] += 1
-    while True:
-        _add_call_item_to_queue(pending_work_items,
-                                work_ids_queue,
-                                call_queue)
-
-        result_item = result_queue.get(block=True)
-        if result_item is not None:
-            work_item = pending_work_items[result_item.work_id]
-            del pending_work_items[result_item.work_id]
-
-            if result_item.exception:
-                work_item.future.set_exception(result_item.exception)
-            else:
-                work_item.future.set_result(result_item.result)
-            # Delete references to object. See issue16284
-            del work_item
-        # Check whether we should start shutting down.
-        executor = executor_reference()
-        # No more work items can be added if:
-        #   - The interpreter is shutting down OR
-        #   - The executor that owns this worker has been collected OR
-        #   - The executor that owns this worker has been shutdown.
-        if _shutdown or executor is None or executor._shutdown_thread:
-            # Since no new work items can be added, it is safe to shutdown
-            # this thread if there are no pending work items.
-            if not pending_work_items:
-                while nb_shutdown_processes[0] < len(processes):
-                    shutdown_one_process()
-                # If .join() is not called on the created processes then
-                # some multiprocessing.Queue methods may deadlock on Mac OS
-                # X.
-                for p in processes:
-                    p.join()
-                call_queue.close()
-                return
-        del executor
-
-_system_limits_checked = False
-_system_limited = None
-def _check_system_limits():
-    global _system_limits_checked, _system_limited
-    if _system_limits_checked:
-        if _system_limited:
-            raise NotImplementedError(_system_limited)
-    _system_limits_checked = True
-    try:
-        import os
-        nsems_max = os.sysconf("SC_SEM_NSEMS_MAX")
-    except (AttributeError, ValueError):
-        # sysconf not available or setting not available
-        return
-    if nsems_max == -1:
-        # indetermine limit, assume that limit is determined
-        # by available memory only
-        return
-    if nsems_max >= 256:
-        # minimum number of semaphores available
-        # according to POSIX
-        return
-    _system_limited = "system provides too few semaphores (%d available, 256 necessary)" % nsems_max
-    raise NotImplementedError(_system_limited)
-
-
-class ProcessPoolExecutor(_base.Executor):
-    def __init__(self, max_workers=None):
-        """Initializes a new ProcessPoolExecutor instance.
-
-        Args:
-            max_workers: The maximum number of processes that can be used to
-                execute the given calls. If None or not given then as many
-                worker processes will be created as the machine has processors.
-        """
-        _check_system_limits()
-
-        if max_workers is None:
-            self._max_workers = multiprocessing.cpu_count()
-        else:
-            if max_workers <= 0:
-                raise ValueError("max_workers must be greater than 0")
-
-            self._max_workers = max_workers
-
-        # Make the call queue slightly larger than the number of processes to
-        # prevent the worker processes from idling. But don't make it too big
-        # because futures in the call queue cannot be cancelled.
-        self._call_queue = multiprocessing.Queue(self._max_workers +
-                                                 EXTRA_QUEUED_CALLS)
-        self._result_queue = multiprocessing.Queue()
-        self._work_ids = queue.Queue()
-        self._queue_management_thread = None
-        self._processes = set()
-
-        # Shutdown is a two-step process.
-        self._shutdown_thread = False
-        self._shutdown_lock = threading.Lock()
-        self._queue_count = 0
-        self._pending_work_items = {}
-
-    def _start_queue_management_thread(self):
-        # When the executor gets lost, the weakref callback will wake up
-        # the queue management thread.
-        def weakref_cb(_, q=self._result_queue):
-            q.put(None)
-        if self._queue_management_thread is None:
-            self._queue_management_thread = threading.Thread(
-                    target=_queue_management_worker,
-                    args=(weakref.ref(self, weakref_cb),
-                          self._processes,
-                          self._pending_work_items,
-                          self._work_ids,
-                          self._call_queue,
-                          self._result_queue))
-            self._queue_management_thread.daemon = True
-            self._queue_management_thread.start()
-            _threads_queues[self._queue_management_thread] = self._result_queue
-
-    def _adjust_process_count(self):
-        for _ in range(len(self._processes), self._max_workers):
-            p = multiprocessing.Process(
-                    target=_process_worker,
-                    args=(self._call_queue,
-                          self._result_queue))
-            p.start()
-            self._processes.add(p)
-
-    def submit(self, fn, *args, **kwargs):
-        with self._shutdown_lock:
-            if self._shutdown_thread:
-                raise RuntimeError('cannot schedule new futures after shutdown')
-
-            f = _base.Future()
-            w = _WorkItem(f, fn, args, kwargs)
-
-            self._pending_work_items[self._queue_count] = w
-            self._work_ids.put(self._queue_count)
-            self._queue_count += 1
-            # Wake up queue management thread
-            self._result_queue.put(None)
-
-            self._start_queue_management_thread()
-            self._adjust_process_count()
-            return f
-    submit.__doc__ = _base.Executor.submit.__doc__
-
-    def shutdown(self, wait=True):
-        with self._shutdown_lock:
-            self._shutdown_thread = True
-        if self._queue_management_thread:
-            # Wake up queue management thread
-            self._result_queue.put(None)
-            if wait:
-                self._queue_management_thread.join(sys.maxint)
-        # To reduce the risk of openning too many files, remove references to
-        # objects that use file descriptors.
-        self._queue_management_thread = None
-        self._call_queue = None
-        self._result_queue = None
-        self._processes = None
-    shutdown.__doc__ = _base.Executor.shutdown.__doc__
-
-atexit.register(_python_exit)
--- a/mercurial/thirdparty/concurrent/futures/thread.py	Wed May 04 18:00:01 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,162 +0,0 @@
-# Copyright 2009 Brian Quinlan. All Rights Reserved.
-# Licensed to PSF under a Contributor Agreement.
-
-"""Implements ThreadPoolExecutor."""
-
-from __future__ import absolute_import
-
-import atexit
-from . import _base
-import itertools
-import Queue as queue
-import threading
-import weakref
-import sys
-
-try:
-    from multiprocessing import cpu_count
-except ImportError:
-    # some platforms don't have multiprocessing
-    def cpu_count():
-        return None
-
-__author__ = 'Brian Quinlan (brian@sweetapp.com)'
-
-# Workers are created as daemon threads. This is done to allow the interpreter
-# to exit when there are still idle threads in a ThreadPoolExecutor's thread
-# pool (i.e. shutdown() was not called). However, allowing workers to die with
-# the interpreter has two undesirable properties:
-#   - The workers would still be running during interpretor shutdown,
-#     meaning that they would fail in unpredictable ways.
-#   - The workers could be killed while evaluating a work item, which could
-#     be bad if the callable being evaluated has external side-effects e.g.
-#     writing to a file.
-#
-# To work around this problem, an exit handler is installed which tells the
-# workers to exit when their work queues are empty and then waits until the
-# threads finish.
-
-_threads_queues = weakref.WeakKeyDictionary()
-_shutdown = False
-
-def _python_exit():
-    global _shutdown
-    _shutdown = True
-    items = list(_threads_queues.items()) if _threads_queues else ()
-    for t, q in items:
-        q.put(None)
-    for t, q in items:
-        t.join(sys.maxint)
-
-atexit.register(_python_exit)
-
-class _WorkItem(object):
-    def __init__(self, future, fn, args, kwargs):
-        self.future = future
-        self.fn = fn
-        self.args = args
-        self.kwargs = kwargs
-
-    def run(self):
-        if not self.future.set_running_or_notify_cancel():
-            return
-
-        try:
-            result = self.fn(*self.args, **self.kwargs)
-        except:
-            e, tb = sys.exc_info()[1:]
-            self.future.set_exception_info(e, tb)
-        else:
-            self.future.set_result(result)
-
-def _worker(executor_reference, work_queue):
-    try:
-        while True:
-            work_item = work_queue.get(block=True)
-            if work_item is not None:
-                work_item.run()
-                # Delete references to object. See issue16284
-                del work_item
-                continue
-            executor = executor_reference()
-            # Exit if:
-            #   - The interpreter is shutting down OR
-            #   - The executor that owns the worker has been collected OR
-            #   - The executor that owns the worker has been shutdown.
-            if _shutdown or executor is None or executor._shutdown:
-                # Notice other workers
-                work_queue.put(None)
-                return
-            del executor
-    except:
-        _base.LOGGER.critical('Exception in worker', exc_info=True)
-
-
-class ThreadPoolExecutor(_base.Executor):
-
-    # Used to assign unique thread names when thread_name_prefix is not supplied.
-    _counter = itertools.count().next
-
-    def __init__(self, max_workers=None, thread_name_prefix=''):
-        """Initializes a new ThreadPoolExecutor instance.
-
-        Args:
-            max_workers: The maximum number of threads that can be used to
-                execute the given calls.
-            thread_name_prefix: An optional name prefix to give our threads.
-        """
-        if max_workers is None:
-            # Use this number because ThreadPoolExecutor is often
-            # used to overlap I/O instead of CPU work.
-            max_workers = (cpu_count() or 1) * 5
-        if max_workers <= 0:
-            raise ValueError("max_workers must be greater than 0")
-
-        self._max_workers = max_workers
-        self._work_queue = queue.Queue()
-        self._threads = set()
-        self._shutdown = False
-        self._shutdown_lock = threading.Lock()
-        self._thread_name_prefix = (thread_name_prefix or
-                                    ("ThreadPoolExecutor-%d" % self._counter()))
-
-    def submit(self, fn, *args, **kwargs):
-        with self._shutdown_lock:
-            if self._shutdown:
-                raise RuntimeError('cannot schedule new futures after shutdown')
-
-            f = _base.Future()
-            w = _WorkItem(f, fn, args, kwargs)
-
-            self._work_queue.put(w)
-            self._adjust_thread_count()
-            return f
-    submit.__doc__ = _base.Executor.submit.__doc__
-
-    def _adjust_thread_count(self):
-        # When the executor gets lost, the weakref callback will wake up
-        # the worker threads.
-        def weakref_cb(_, q=self._work_queue):
-            q.put(None)
-        # TODO(bquinlan): Should avoid creating new threads if there are more
-        # idle threads than items in the work queue.
-        num_threads = len(self._threads)
-        if num_threads < self._max_workers:
-            thread_name = '%s_%d' % (self._thread_name_prefix or self,
-                                     num_threads)
-            t = threading.Thread(name=thread_name, target=_worker,
-                                 args=(weakref.ref(self, weakref_cb),
-                                       self._work_queue))
-            t.daemon = True
-            t.start()
-            self._threads.add(t)
-            _threads_queues[t] = self._work_queue
-
-    def shutdown(self, wait=True):
-        with self._shutdown_lock:
-            self._shutdown = True
-            self._work_queue.put(None)
-        if wait:
-            for t in self._threads:
-                t.join(sys.maxint)
-    shutdown.__doc__ = _base.Executor.shutdown.__doc__
--- a/mercurial/transaction.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/transaction.py	Wed May 04 18:17:44 2022 +0200
@@ -11,7 +11,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import errno
 
@@ -383,7 +382,7 @@
             skip_pre = group == GEN_GROUP_POST_FINALIZE
             skip_post = group == GEN_GROUP_PRE_FINALIZE
 
-        for id, entry in sorted(pycompat.iteritems(self._filegenerators)):
+        for id, entry in sorted(self._filegenerators.items()):
             any = True
             order, filenames, genfunc, location, post_finalize = entry
 
--- a/mercurial/treediscovery.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/treediscovery.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import collections
 
@@ -40,6 +39,7 @@
 
     if audit is not None:
         audit[b'total-roundtrips'] = 1
+        audit[b'total-queries'] = 0
 
     if repo.changelog.tip() == repo.nullid:
         base.add(repo.nullid)
@@ -70,6 +70,8 @@
     # head, root, first parent, second parent
     # (a branch always has two parents (or none) by definition)
     with remote.commandexecutor() as e:
+        if audit is not None:
+            audit[b'total-queries'] += len(unknown)
         branches = e.callcommand(b'branches', {b'nodes': unknown}).result()
 
     unknown = collections.deque(branches)
@@ -116,10 +118,13 @@
             )
             for p in pycompat.xrange(0, len(r), 10):
                 with remote.commandexecutor() as e:
+                    subset = r[p : p + 10]
+                    if audit is not None:
+                        audit[b'total-queries'] += len(subset)
                     branches = e.callcommand(
                         b'branches',
                         {
-                            b'nodes': r[p : p + 10],
+                            b'nodes': subset,
                         },
                     ).result()
 
@@ -136,6 +141,8 @@
         progress.increment()
 
         with remote.commandexecutor() as e:
+            if audit is not None:
+                audit[b'total-queries'] += len(search)
             between = e.callcommand(b'between', {b'pairs': search}).result()
 
         for n, l in zip(search, between):
--- a/mercurial/txnutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/txnutil.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import errno
 
--- a/mercurial/ui.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/ui.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import collections
 import contextlib
@@ -170,7 +169,7 @@
     return pycompat.rapply(pycompat.bytesurl, maybestr)
 
 
-class httppasswordmgrdbproxy(object):
+class httppasswordmgrdbproxy:
     """Delays loading urllib2 until it's needed."""
 
     def __init__(self):
@@ -208,7 +207,7 @@
 _reqexithandlers = []
 
 
-class ui(object):
+class ui:
     def __init__(self, src=None):
         """Create a fresh new ui object if no src given
 
@@ -1433,6 +1432,14 @@
             # HGPLAINEXCEPT=pager, and the user didn't specify --debug.
             return
 
+        # py2exe doesn't appear to be able to use legacy I/O, and nothing is
+        # output to the pager for paged commands.  Piping to `more` in cmd.exe
+        # works, but is easy to forget.  Just disable pager for py2exe, but
+        # leave it working for pyoxidizer and exewrapper builds.
+        if pycompat.iswindows and getattr(sys, "frozen", None) == "console_exe":
+            self.debug(b"pager is unavailable with py2exe packaging\n")
+            return
+
         pagercmd = self.config(b'pager', b'pager', rcutil.fallbackpager)
         if not pagercmd:
             return
@@ -1726,9 +1733,9 @@
             if usereadline:
                 self.flush()
                 prompt = encoding.strfromlocal(prompt)
-                line = encoding.strtolocal(pycompat.rawinput(prompt))
+                line = encoding.strtolocal(input(prompt))
                 # When stdin is in binary mode on Windows, it can cause
-                # raw_input() to emit an extra trailing carriage return
+                # input() to emit an extra trailing carriage return
                 if pycompat.oslinesep == b'\r\n' and line.endswith(b'\r'):
                     line = line[:-1]
             else:
@@ -2118,9 +2125,7 @@
         """
         if not self._loggers:
             return
-        activeloggers = [
-            l for l in pycompat.itervalues(self._loggers) if l.tracked(event)
-        ]
+        activeloggers = [l for l in self._loggers.values() if l.tracked(event)]
         if not activeloggers:
             return
         msg = msgfmt % msgargs
--- a/mercurial/unionrepo.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/unionrepo.py	Wed May 04 18:17:44 2022 +0200
@@ -11,7 +11,6 @@
 allowing operations like diff and log with revsets.
 """
 
-from __future__ import absolute_import
 
 from .i18n import _
 from .pycompat import getattr
@@ -210,7 +209,7 @@
         return False
 
 
-class unionrepository(object):
+class unionrepository:
     """Represents the union of data in 2 repositories.
 
     Instances are not usable if constructed directly. Use ``instance()``
--- a/mercurial/upgrade.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/upgrade.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from .i18n import _
 from . import (
--- a/mercurial/upgrade_utils/actions.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/upgrade_utils/actions.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from ..i18n import _
 from .. import (
@@ -38,6 +37,7 @@
 def preservedrequirements(repo):
     preserved = {
         requirements.SHARED_REQUIREMENT,
+        requirements.NARROW_REQUIREMENT,
     }
     return preserved & repo.requirements
 
@@ -46,7 +46,7 @@
 OPTIMISATION = b'optimization'
 
 
-class improvement(object):
+class improvement:
     """Represents an improvement that can be made as part of an upgrade."""
 
     ### The following attributes should be defined for each subclass:
@@ -685,7 +685,7 @@
     return newactions
 
 
-class UpgradeOperation(object):
+class UpgradeOperation:
     """represent the work to be done during an upgrade"""
 
     def __init__(
@@ -1005,7 +1005,7 @@
 def supporteddestrequirements(repo):
     """Obtain requirements that upgrade supports in the destination.
 
-    If the result of the upgrade would create requirements not in this set,
+    If the result of the upgrade would have requirements not in this set,
     the upgrade is disallowed.
 
     Extensions should monkeypatch this to add their custom requirements.
@@ -1025,6 +1025,7 @@
         requirements.SHARESAFE_REQUIREMENT,
         requirements.SPARSEREVLOG_REQUIREMENT,
         requirements.STORE_REQUIREMENT,
+        requirements.NARROW_REQUIREMENT,
     }
     for name in compression.compengines:
         engine = compression.compengines[name]
--- a/mercurial/upgrade_utils/engine.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/upgrade_utils/engine.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import errno
 import stat
--- a/mercurial/url.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/url.py	Wed May 04 18:17:44 2022 +0200
@@ -7,11 +7,9 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import base64
 import socket
-import sys
 
 from .i18n import _
 from .pycompat import getattr
@@ -52,7 +50,7 @@
     return s
 
 
-class passwordmgr(object):
+class passwordmgr:
     def __init__(self, ui, passwddb):
         self.ui = ui
         self.passwddb = passwddb
@@ -241,19 +239,15 @@
         if x.lower().startswith('proxy-')
     }
     self.send(b'CONNECT %s HTTP/1.0\r\n' % self.realhostport)
-    for header in pycompat.iteritems(proxyheaders):
+    for header in proxyheaders.items():
         self.send(b'%s: %s\r\n' % header)
     self.send(b'\r\n')
 
     # majority of the following code is duplicated from
     # httplib.HTTPConnection as there are no adequate places to
-    # override functions to provide the needed functionality
-    # strict was removed in Python 3.4.
-    kwargs = {}
-    if not pycompat.ispy3:
-        kwargs[b'strict'] = self.strict
+    # override functions to provide the needed functionality.
 
-    res = self.response_class(self.sock, method=self._method, **kwargs)
+    res = self.response_class(self.sock, method=self._method)
 
     while True:
         version, status, reason = res._read_status()
@@ -348,16 +342,6 @@
         keepalive.HTTPConnection.__init__(self, *args, **kwargs)
         self._create_connection = createconn
 
-    if sys.version_info < (2, 7, 7):
-        # copied from 2.7.14, since old implementations directly call
-        # socket.create_connection()
-        def connect(self):
-            self.sock = self._create_connection(
-                (self.host, self.port), self.timeout, self.source_address
-            )
-            if self._tunnel_host:
-                self._tunnel()
-
 
 class logginghttphandler(httphandler):
     """HTTP handler that logs socket I/O."""
--- a/mercurial/urllibcompat.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/urllibcompat.py	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,12 @@
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-from __future__ import absolute_import
+
+import http.server
+import urllib.error
+import urllib.parse
+import urllib.request
+import urllib.response
 
 from .pycompat import getattr
 from . import pycompat
@@ -12,7 +17,7 @@
 _sysstr = pycompat.sysstr
 
 
-class _pycompatstub(object):
+class _pycompatstub:
     def __init__(self):
         self._aliases = {}
 
@@ -40,199 +45,109 @@
 urlreq = _pycompatstub()
 urlerr = _pycompatstub()
 
-if pycompat.ispy3:
-    import urllib.parse
-
-    urlreq._registeraliases(
-        urllib.parse,
-        (
-            b"splitattr",
-            b"splitpasswd",
-            b"splitport",
-            b"splituser",
-            b"urlparse",
-            b"urlunparse",
-        ),
-    )
-    urlreq._registeralias(urllib.parse, b"parse_qs", b"parseqs")
-    urlreq._registeralias(urllib.parse, b"parse_qsl", b"parseqsl")
-    urlreq._registeralias(urllib.parse, b"unquote_to_bytes", b"unquote")
-    import urllib.request
-
-    urlreq._registeraliases(
-        urllib.request,
-        (
-            b"AbstractHTTPHandler",
-            b"BaseHandler",
-            b"build_opener",
-            b"FileHandler",
-            b"FTPHandler",
-            b"ftpwrapper",
-            b"HTTPHandler",
-            b"HTTPSHandler",
-            b"install_opener",
-            b"pathname2url",
-            b"HTTPBasicAuthHandler",
-            b"HTTPDigestAuthHandler",
-            b"HTTPPasswordMgrWithDefaultRealm",
-            b"ProxyHandler",
-            b"Request",
-            b"url2pathname",
-            b"urlopen",
-        ),
-    )
-    import urllib.response
-
-    urlreq._registeraliases(
-        urllib.response,
-        (
-            b"addclosehook",
-            b"addinfourl",
-        ),
-    )
-    import urllib.error
+urlreq._registeraliases(
+    urllib.parse,
+    (
+        b"splitattr",
+        b"splitpasswd",
+        b"splitport",
+        b"splituser",
+        b"urlparse",
+        b"urlunparse",
+    ),
+)
+urlreq._registeralias(urllib.parse, b"parse_qs", b"parseqs")
+urlreq._registeralias(urllib.parse, b"parse_qsl", b"parseqsl")
+urlreq._registeralias(urllib.parse, b"unquote_to_bytes", b"unquote")
 
-    urlerr._registeraliases(
-        urllib.error,
-        (
-            b"HTTPError",
-            b"URLError",
-        ),
-    )
-    import http.server
-
-    httpserver._registeraliases(
-        http.server,
-        (
-            b"HTTPServer",
-            b"BaseHTTPRequestHandler",
-            b"SimpleHTTPRequestHandler",
-            b"CGIHTTPRequestHandler",
-        ),
-    )
-
-    # 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.
-    def quote(s, safe='/'):
-        # bytestr has an __iter__ that emits characters. quote_from_bytes()
-        # does an iteration and expects ints. We coerce to bytes to appease it.
-        if isinstance(s, pycompat.bytestr):
-            s = bytes(s)
-        s = urllib.parse.quote_from_bytes(s, safe=safe)
-        return s.encode('ascii', 'strict')
-
-    # urllib.parse.urlencode() returns str. We use this function to make
-    # sure we return bytes.
-    def urlencode(query, doseq=False):
-        s = urllib.parse.urlencode(query, doseq=doseq)
-        return s.encode('ascii')
-
-    urlreq.quote = quote
-    urlreq.urlencode = urlencode
-
-    def getfullurl(req):
-        return req.full_url
-
-    def gethost(req):
-        return req.host
-
-    def getselector(req):
-        return req.selector
-
-    def getdata(req):
-        return req.data
-
-    def hasdata(req):
-        return req.data is not None
+urlreq._registeraliases(
+    urllib.request,
+    (
+        b"AbstractHTTPHandler",
+        b"BaseHandler",
+        b"build_opener",
+        b"FileHandler",
+        b"FTPHandler",
+        b"ftpwrapper",
+        b"HTTPHandler",
+        b"HTTPSHandler",
+        b"install_opener",
+        b"pathname2url",
+        b"HTTPBasicAuthHandler",
+        b"HTTPDigestAuthHandler",
+        b"HTTPPasswordMgrWithDefaultRealm",
+        b"ProxyHandler",
+        b"Request",
+        b"url2pathname",
+        b"urlopen",
+    ),
+)
 
 
-else:
-    # pytype: disable=import-error
-    import BaseHTTPServer
-    import CGIHTTPServer
-    import SimpleHTTPServer
-    import urllib2
-    import urllib
-    import urlparse
+urlreq._registeraliases(
+    urllib.response,
+    (
+        b"addclosehook",
+        b"addinfourl",
+    ),
+)
 
-    # pytype: enable=import-error
+urlerr._registeraliases(
+    urllib.error,
+    (
+        b"HTTPError",
+        b"URLError",
+    ),
+)
+
+httpserver._registeraliases(
+    http.server,
+    (
+        b"HTTPServer",
+        b"BaseHTTPRequestHandler",
+        b"SimpleHTTPRequestHandler",
+        b"CGIHTTPRequestHandler",
+    ),
+)
 
-    urlreq._registeraliases(
-        urllib,
-        (
-            b"addclosehook",
-            b"addinfourl",
-            b"ftpwrapper",
-            b"pathname2url",
-            b"quote",
-            b"splitattr",
-            b"splitpasswd",
-            b"splitport",
-            b"splituser",
-            b"unquote",
-            b"url2pathname",
-            b"urlencode",
-        ),
-    )
-    urlreq._registeraliases(
-        urllib2,
-        (
-            b"AbstractHTTPHandler",
-            b"BaseHandler",
-            b"build_opener",
-            b"FileHandler",
-            b"FTPHandler",
-            b"HTTPBasicAuthHandler",
-            b"HTTPDigestAuthHandler",
-            b"HTTPHandler",
-            b"HTTPPasswordMgrWithDefaultRealm",
-            b"HTTPSHandler",
-            b"install_opener",
-            b"ProxyHandler",
-            b"Request",
-            b"urlopen",
-        ),
-    )
-    urlreq._registeraliases(
-        urlparse,
-        (
-            b"urlparse",
-            b"urlunparse",
-        ),
-    )
-    urlreq._registeralias(urlparse, b"parse_qs", b"parseqs")
-    urlreq._registeralias(urlparse, b"parse_qsl", b"parseqsl")
-    urlerr._registeraliases(
-        urllib2,
-        (
-            b"HTTPError",
-            b"URLError",
-        ),
-    )
-    httpserver._registeraliases(
-        BaseHTTPServer,
-        (
-            b"HTTPServer",
-            b"BaseHTTPRequestHandler",
-        ),
-    )
-    httpserver._registeraliases(
-        SimpleHTTPServer, (b"SimpleHTTPRequestHandler",)
-    )
-    httpserver._registeraliases(CGIHTTPServer, (b"CGIHTTPRequestHandler",))
+# 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.
+def quote(s, safe='/'):
+    # bytestr has an __iter__ that emits characters. quote_from_bytes()
+    # does an iteration and expects ints. We coerce to bytes to appease it.
+    if isinstance(s, pycompat.bytestr):
+        s = bytes(s)
+    s = urllib.parse.quote_from_bytes(s, safe=safe)
+    return s.encode('ascii', 'strict')
+
+
+# urllib.parse.urlencode() returns str. We use this function to make
+# sure we return bytes.
+def urlencode(query, doseq=False):
+    s = urllib.parse.urlencode(query, doseq=doseq)
+    return s.encode('ascii')
+
 
-    def gethost(req):
-        return req.get_host()
+urlreq.quote = quote
+urlreq.urlencode = urlencode
+
 
-    def getselector(req):
-        return req.get_selector()
+def getfullurl(req):
+    return req.full_url
+
+
+def gethost(req):
+    return req.host
 
-    def getfullurl(req):
-        return req.get_full_url()
+
+def getselector(req):
+    return req.selector
+
 
-    def getdata(req):
-        return req.get_data()
+def getdata(req):
+    return req.data
 
-    def hasdata(req):
-        return req.has_data()
+
+def hasdata(req):
+    return req.data is not None
--- a/mercurial/util.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/util.py	Wed May 04 18:17:44 2022 +0200
@@ -13,7 +13,6 @@
 hide platform-specific details from the core.
 """
 
-from __future__ import absolute_import, print_function
 
 import abc
 import collections
@@ -21,11 +20,12 @@
 import errno
 import gc
 import hashlib
+import io
 import itertools
 import locale
 import mmap
 import os
-import platform as pyplatform
+import pickle  # provides util.pickle symbol
 import re as remod
 import shutil
 import stat
@@ -42,7 +42,6 @@
     open,
     setattr,
 )
-from .node import hex
 from hgdemandimport import tracing
 from . import (
     encoding,
@@ -76,10 +75,9 @@
 
 cookielib = pycompat.cookielib
 httplib = pycompat.httplib
-pickle = pycompat.pickle
 safehasattr = pycompat.safehasattr
 socketserver = pycompat.socketserver
-bytesio = pycompat.bytesio
+bytesio = io.BytesIO
 # TODO deprecate stringio name, as it is a lie on Python 3.
 stringio = bytesio
 xmlrpclib = pycompat.xmlrpclib
@@ -189,7 +187,7 @@
     warnings.filterwarnings('default', '', DeprecationWarning, 'mercurial')
     warnings.filterwarnings('default', '', DeprecationWarning, 'hgext')
     warnings.filterwarnings('default', '', DeprecationWarning, 'hgext3rd')
-if _dowarn and pycompat.ispy3:
+if _dowarn:
     # silence warning emitted by passing user string to re.sub()
     warnings.filterwarnings(
         'ignore', 'bad escape', DeprecationWarning, 'mercurial'
@@ -233,7 +231,7 @@
     assert k in DIGESTS
 
 
-class digester(object):
+class digester:
     """helper to compute digests.
 
     This helper can be used to compute one or more digests given their name.
@@ -281,7 +279,7 @@
         return None
 
 
-class digestchecker(object):
+class digestchecker:
     """file handle wrapper that additionally checks content against a given
     size and digests.
 
@@ -331,7 +329,7 @@
 _chunksize = 4096
 
 
-class bufferedinputpipe(object):
+class bufferedinputpipe:
     """a manually buffered input pipe
 
     Python will not let us use buffered IO and lazy reading with 'polling' at
@@ -459,7 +457,7 @@
         raise
 
 
-class fileobjectproxy(object):
+class fileobjectproxy:
     """A proxy around file objects that tells a watcher when events occur.
 
     This type is intended to only be used for testing purposes. Think hard
@@ -695,7 +693,7 @@
 }
 
 
-class socketproxy(object):
+class socketproxy:
     """A proxy around a socket that tells a watcher when events occur.
 
     This is like ``fileobjectproxy`` except for sockets.
@@ -818,7 +816,7 @@
         )
 
 
-class baseproxyobserver(object):
+class baseproxyobserver:
     def __init__(self, fh, name, logdata, logdataapis):
         self.fh = fh
         self.name = name
@@ -1258,7 +1256,7 @@
     return f
 
 
-class cow(object):
+class cow:
     """helper class to make copy-on-write easier
 
     Call preparewrite before doing any writes.
@@ -1302,7 +1300,7 @@
         # __setitem__() isn't called as of PyPy 5.8.0
         def update(self, src, **f):
             if isinstance(src, dict):
-                src = pycompat.iteritems(src)
+                src = src.items()
             for k, v in src:
                 self[k] = v
             for k in f:
@@ -1351,7 +1349,7 @@
     """
 
 
-class transactional(object):  # pytype: disable=ignored-metaclass
+class transactional:  # pytype: disable=ignored-metaclass
     """Base class for making a transactional type into a context manager."""
 
     __metaclass__ = abc.ABCMeta
@@ -1402,7 +1400,7 @@
     yield enter_result
 
 
-class _lrucachenode(object):
+class _lrucachenode:
     """A node in a doubly linked list.
 
     Holds a reference to nodes on either side as well as a key-value
@@ -1426,7 +1424,7 @@
         self.cost = 0
 
 
-class lrucachedict(object):
+class lrucachedict:
     """Dict that caches most recent accesses and sets.
 
     The dict consists of an actual backing dict - indexed by original
@@ -1757,7 +1755,7 @@
     return f
 
 
-class propertycache(object):
+class propertycache:
     def __init__(self, func):
         self.func = func
         self.name = func.__name__
@@ -2216,7 +2214,7 @@
     _re2 = False
 
 
-class _re(object):
+class _re:
     def _checkre2(self):
         global _re2
         global _re2_input
@@ -2418,7 +2416,7 @@
     return temp
 
 
-class filestat(object):
+class filestat:
     """help to exactly detect change of a file
 
     'stat' attribute is result of 'os.stat()' if specified 'path'
@@ -2524,7 +2522,7 @@
         return not self == other
 
 
-class atomictempfile(object):
+class atomictempfile:
     """writable file object that atomically updates a file
 
     All writes will go to a temporary copy of the original file. Call
@@ -2667,7 +2665,7 @@
         fp.write(text)
 
 
-class chunkbuffer(object):
+class chunkbuffer:
     """Allow arbitrary sized chunks of data to be efficiently read from an
     iterator over chunks of arbitrary size."""
 
@@ -2772,7 +2770,7 @@
         yield s
 
 
-class cappedreader(object):
+class cappedreader:
     """A file object proxy that allows reading up to N bytes.
 
     Given a source file object, instances of this type allow reading up to
@@ -2860,7 +2858,7 @@
 )
 
 
-class transformingwriter(object):
+class transformingwriter:
     """Writable file wrapper to transform data by function"""
 
     def __init__(self, fp, encode):
@@ -2906,50 +2904,10 @@
     fromnativeeol = pycompat.identity
     nativeeolwriter = pycompat.identity
 
-if pyplatform.python_implementation() == b'CPython' and sys.version_info < (
-    3,
-    0,
-):
-    # There is an issue in CPython that some IO methods do not handle EINTR
-    # correctly. The following table shows what CPython version (and functions)
-    # are affected (buggy: has the EINTR bug, okay: otherwise):
-    #
-    #                | < 2.7.4 | 2.7.4 to 2.7.12 | >= 3.0
-    #   --------------------------------------------------
-    #    fp.__iter__ | buggy   | buggy           | okay
-    #    fp.read*    | buggy   | okay [1]        | okay
-    #
-    # [1]: fixed by changeset 67dc99a989cd in the cpython hg repo.
-    #
-    # Here we workaround the EINTR issue for fileobj.__iter__. Other methods
-    # like "read*" work fine, as we do not support Python < 2.7.4.
-    #
-    # Although we can workaround the EINTR issue for fp.__iter__, it is slower:
-    # "for x in fp" is 4x faster than "for x in iter(fp.readline, '')" in
-    # CPython 2, because CPython 2 maintains an internal readahead buffer for
-    # fp.__iter__ but not other fp.read* methods.
-    #
-    # On modern systems like Linux, the "read" syscall cannot be interrupted
-    # when reading "fast" files like on-disk files. So the EINTR issue only
-    # affects things like pipes, sockets, ttys etc. We treat "normal" (S_ISREG)
-    # files approximately as "fast" files and use the fast (unsafe) code path,
-    # to minimize the performance impact.
-
-    def iterfile(fp):
-        fastpath = True
-        if type(fp) is file:
-            fastpath = stat.S_ISREG(os.fstat(fp.fileno()).st_mode)
-        if fastpath:
-            return fp
-        else:
-            # fp.readline deals with EINTR correctly, use it as a workaround.
-            return iter(fp.readline, b'')
-
-
-else:
-    # PyPy and CPython 3 do not have the EINTR issue thus no workaround needed.
-    def iterfile(fp):
-        return fp
+
+# TODO delete since workaround variant for Python 2 no longer needed.
+def iterfile(fp):
+    return fp
 
 
 def iterlines(iterator):
@@ -3008,7 +2966,7 @@
 
 
 @attr.s
-class timedcmstats(object):
+class timedcmstats:
     """Stats information produced by the timedcm context manager on entering."""
 
     # the starting value of the timer as a float (meaning and resulution is
@@ -3109,7 +3067,7 @@
         raise error.ParseError(_(b"couldn't parse size: %s") % s)
 
 
-class hooks(object):
+class hooks:
     """A collection of hook functions that can be used to extend a
     function's behavior. Hooks are called in lexicographic order,
     based on the names of their sources."""
--- a/mercurial/utils/cborutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/utils/cborutil.py	Wed May 04 18:17:44 2022 +0200
@@ -5,12 +5,9 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import struct
-import sys
 
-from .. import pycompat
 
 # Very short very of RFC 7049...
 #
@@ -175,9 +172,7 @@
     """
     yield encodelength(MAJOR_TYPE_MAP, len(d))
 
-    for key, value in sorted(
-        pycompat.iteritems(d), key=lambda x: _mixedtypesortkey(x[0])
-    ):
+    for key, value in sorted(d.items(), key=lambda x: _mixedtypesortkey(x[0])):
         for chunk in streamencode(key):
             yield chunk
         for chunk in streamencode(value):
@@ -210,7 +205,7 @@
 STREAM_ENCODERS = {
     bytes: streamencodebytestring,
     int: streamencodeint,
-    pycompat.long: streamencodeint,
+    int: streamencodeint,
     list: streamencodearray,
     tuple: streamencodearray,
     dict: streamencodemap,
@@ -250,16 +245,8 @@
     """Represents an error decoding CBOR."""
 
 
-if sys.version_info.major >= 3:
-
-    def _elementtointeger(b, i):
-        return b[i]
-
-
-else:
-
-    def _elementtointeger(b, i):
-        return ord(b[i])
+def _elementtointeger(b, i):
+    return b[i]
 
 
 STRUCT_BIG_UBYTE = struct.Struct('>B')
@@ -496,7 +483,7 @@
         return self
 
 
-class sansiodecoder(object):
+class sansiodecoder:
     """A CBOR decoder that doesn't perform its own I/O.
 
     To use, construct an instance and feed it segments containing
@@ -989,7 +976,7 @@
         return l
 
 
-class bufferingdecoder(object):
+class bufferingdecoder:
     """A CBOR decoder that buffers undecoded input.
 
     This is a glorified wrapper around ``sansiodecoder`` that adds a buffering
--- a/mercurial/utils/compression.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/utils/compression.py	Wed May 04 18:17:44 2022 +0200
@@ -4,8 +4,6 @@
 # GNU General Public License version 2 or any later version.
 
 
-from __future__ import absolute_import, print_function
-
 import bz2
 import collections
 import zlib
@@ -34,7 +32,7 @@
 )
 
 
-class propertycache(object):
+class propertycache:
     def __init__(self, func):
         self.func = func
         self.name = func.__name__
@@ -49,7 +47,7 @@
         obj.__dict__[self.name] = value
 
 
-class compressormanager(object):
+class compressormanager:
     """Holds registrations of various compression engines.
 
     This class essentially abstracts the differences between compression
@@ -221,7 +219,7 @@
 compengines = compressormanager()
 
 
-class compressionengine(object):
+class compressionengine:
     """Base class for compression engines.
 
     Compression engines must implement the interface defined by this class.
@@ -340,7 +338,7 @@
         raise NotImplementedError()
 
 
-class _CompressedStreamReader(object):
+class _CompressedStreamReader:
     def __init__(self, fh):
         if safehasattr(fh, 'unbufferedread'):
             self._reader = fh.unbufferedread
@@ -484,7 +482,7 @@
     def decompressorreader(self, fh):
         return _GzipCompressedStreamReader(fh)
 
-    class zlibrevlogcompressor(object):
+    class zlibrevlogcompressor:
         def __init__(self, level=None):
             self._level = level
 
@@ -628,7 +626,7 @@
     def decompressorreader(self, fh):
         return fh
 
-    class nooprevlogcompressor(object):
+    class nooprevlogcompressor:
         def compress(self, data):
             return None
 
@@ -700,7 +698,7 @@
     def decompressorreader(self, fh):
         return _ZstdCompressedStreamReader(fh, self._module)
 
-    class zstdrevlogcompressor(object):
+    class zstdrevlogcompressor:
         def __init__(self, zstd, level=3):
             # TODO consider omitting frame magic to save 4 bytes.
             # This writes content sizes into the frame header. That is
@@ -784,7 +782,7 @@
 
     # We need to format the docstring. So use a dummy object/type to hold it
     # rather than mutating the original.
-    class docobject(object):
+    class docobject:
         pass
 
     for name in compengines:
--- a/mercurial/utils/dateutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/utils/dateutil.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import, print_function
 
 import calendar
 import datetime
--- a/mercurial/utils/hashutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/utils/hashutil.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import hashlib
 
 try:
--- a/mercurial/utils/procutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/utils/procutil.py	Wed May 04 18:17:44 2022 +0200
@@ -7,7 +7,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import contextlib
 import errno
@@ -60,7 +59,7 @@
         raise IOError(errno.EBADF, 'Bad file descriptor')
 
 
-class LineBufferedWrapper(object):
+class LineBufferedWrapper:
     def __init__(self, orig):
         self.orig = orig
 
@@ -81,7 +80,7 @@
 
 
 def make_line_buffered(stream):
-    if pycompat.ispy3 and not isinstance(stream, io.BufferedIOBase):
+    if not isinstance(stream, io.BufferedIOBase):
         # On Python 3, buffered streams can be expected to subclass
         # BufferedIOBase. This is definitively the case for the streams
         # initialized by the interpreter. For unbuffered streams, we don't need
@@ -99,7 +98,7 @@
     return stream
 
 
-class WriteAllWrapper(object):
+class WriteAllWrapper:
     def __init__(self, orig):
         self.orig = orig
 
@@ -124,7 +123,6 @@
 
 
 def _make_write_all(stream):
-    assert pycompat.ispy3
     if isinstance(stream, WriteAllWrapper):
         return stream
     if isinstance(stream, io.BufferedIOBase):
@@ -136,52 +134,32 @@
     return WriteAllWrapper(stream)
 
 
-if pycompat.ispy3:
-    # Python 3 implements its own I/O streams. Unlike stdio of C library,
-    # sys.stdin/stdout/stderr may be None if underlying fd is closed.
-
-    # TODO: .buffer might not exist if std streams were replaced; we'll need
-    # a silly wrapper to make a bytes stream backed by a unicode one.
+# Python 3 implements its own I/O streams. Unlike stdio of C library,
+# sys.stdin/stdout/stderr may be None if underlying fd is closed.
 
-    if sys.stdin is None:
-        stdin = BadFile()
-    else:
-        stdin = sys.stdin.buffer
-    if sys.stdout is None:
-        stdout = BadFile()
-    else:
-        stdout = _make_write_all(sys.stdout.buffer)
-    if sys.stderr is None:
-        stderr = BadFile()
-    else:
-        stderr = _make_write_all(sys.stderr.buffer)
+# TODO: .buffer might not exist if std streams were replaced; we'll need
+# a silly wrapper to make a bytes stream backed by a unicode one.
 
-    if pycompat.iswindows:
-        # Work around Windows bugs.
-        stdout = platform.winstdout(stdout)  # pytype: disable=module-attr
-        stderr = platform.winstdout(stderr)  # pytype: disable=module-attr
-    if isatty(stdout):
-        # The standard library doesn't offer line-buffered binary streams.
-        stdout = make_line_buffered(stdout)
+if sys.stdin is None:
+    stdin = BadFile()
+else:
+    stdin = sys.stdin.buffer
+if sys.stdout is None:
+    stdout = BadFile()
 else:
-    # Python 2 uses the I/O streams provided by the C library.
-    stdin = sys.stdin
-    stdout = sys.stdout
-    stderr = sys.stderr
-    if pycompat.iswindows:
-        # Work around Windows bugs.
-        stdout = platform.winstdout(stdout)  # pytype: disable=module-attr
-        stderr = platform.winstdout(stderr)  # pytype: disable=module-attr
-    if isatty(stdout):
-        if pycompat.iswindows:
-            # The Windows C runtime library doesn't support line buffering.
-            stdout = make_line_buffered(stdout)
-        else:
-            # glibc determines buffering on first write to stdout - if we
-            # replace a TTY destined stdout with a pipe destined stdout (e.g.
-            # pager), we want line buffering.
-            stdout = os.fdopen(stdout.fileno(), 'wb', 1)
+    stdout = _make_write_all(sys.stdout.buffer)
+if sys.stderr is None:
+    stderr = BadFile()
+else:
+    stderr = _make_write_all(sys.stderr.buffer)
 
+if pycompat.iswindows:
+    # Work around Windows bugs.
+    stdout = platform.winstdout(stdout)  # pytype: disable=module-attr
+    stderr = platform.winstdout(stderr)  # pytype: disable=module-attr
+if isatty(stdout):
+    # The standard library doesn't offer line-buffered binary streams.
+    stdout = make_line_buffered(stdout)
 
 findexe = platform.findexe
 _gethgcmd = platform.gethgcmd
@@ -217,7 +195,7 @@
     return _(b"killed by signal %d") % -code
 
 
-class _pfile(object):
+class _pfile:
     """File-like wrapper for a stream opened by subprocess.Popen()"""
 
     def __init__(self, proc, fp):
@@ -366,7 +344,7 @@
 def filter(s, cmd):
     """filter a string through a command that transforms its input to its
     output"""
-    for name, fn in pycompat.iteritems(_filtertable):
+    for name, fn in _filtertable.items():
         if cmd.startswith(name):
             return fn(s, cmd[len(name) :].lstrip())
     return pipefilter(s, cmd)
@@ -472,7 +450,7 @@
 
     env = dict(encoding.environ)
     if environ:
-        env.update((k, py2shell(v)) for k, v in pycompat.iteritems(environ))
+        env.update((k, py2shell(v)) for k, v in environ.items())
     env[b'HG'] = hgexecutable()
     return env
 
@@ -707,7 +685,7 @@
 
 else:
 
-    def runbgcommandpy3(
+    def runbgcommand(
         cmd,
         env,
         shell=False,
@@ -790,128 +768,3 @@
             returncode = p.wait
             if record_wait is not None:
                 record_wait(returncode)
-
-    def runbgcommandpy2(
-        cmd,
-        env,
-        shell=False,
-        stdout=None,
-        stderr=None,
-        ensurestart=True,
-        record_wait=None,
-        stdin_bytes=None,
-    ):
-        """Spawn a command without waiting for it to finish.
-
-
-        When `record_wait` is not None, the spawned process will not be fully
-        detached and the `record_wait` argument will be called with a the
-        `Subprocess.wait` function for the spawned process.  This is mostly
-        useful for developers that need to make sure the spawned process
-        finished before a certain point. (eg: writing test)"""
-        if pycompat.isdarwin:
-            # avoid crash in CoreFoundation in case another thread
-            # calls gui() while we're calling fork().
-            gui()
-
-        # double-fork to completely detach from the parent process
-        # based on http://code.activestate.com/recipes/278731
-        if record_wait is None:
-            pid = os.fork()
-            if pid:
-                if not ensurestart:
-                    # Even though we're not waiting on the child process,
-                    # we still must call waitpid() on it at some point so
-                    # it's not a zombie/defunct. This is especially relevant for
-                    # chg since the parent process won't die anytime soon.
-                    # We use a thread to make the overhead tiny.
-                    def _do_wait():
-                        os.waitpid(pid, 0)
-
-                    t = threading.Thread(target=_do_wait)
-                    t.daemon = True
-                    t.start()
-                    return
-                # Parent process
-                (_pid, status) = os.waitpid(pid, 0)
-                if os.WIFEXITED(status):
-                    returncode = os.WEXITSTATUS(status)
-                else:
-                    returncode = -(os.WTERMSIG(status))
-                if returncode != 0:
-                    # The child process's return code is 0 on success, an errno
-                    # value on failure, or 255 if we don't have a valid errno
-                    # value.
-                    #
-                    # (It would be slightly nicer to return the full exception info
-                    # over a pipe as the subprocess module does.  For now it
-                    # doesn't seem worth adding that complexity here, though.)
-                    if returncode == 255:
-                        returncode = errno.EINVAL
-                    raise OSError(
-                        returncode,
-                        b'error running %r: %s'
-                        % (cmd, os.strerror(returncode)),
-                    )
-                return
-
-        returncode = 255
-        stdin = None
-
-        try:
-            if record_wait is None:
-                # Start a new session
-                os.setsid()
-            # connect stdin to devnull to make sure the subprocess can't
-            # muck up that stream for mercurial.
-            if stdin_bytes is None:
-                stdin = open(os.devnull, b'r')
-            else:
-                stdin = pycompat.unnamedtempfile()
-                stdin.write(stdin_bytes)
-                stdin.flush()
-                stdin.seek(0)
-
-            if stdout is None:
-                stdout = open(os.devnull, b'w')
-            if stderr is None:
-                stderr = open(os.devnull, b'w')
-
-            p = subprocess.Popen(
-                cmd,
-                shell=shell,
-                env=env,
-                close_fds=True,
-                stdin=stdin,
-                stdout=stdout,
-                stderr=stderr,
-            )
-            if record_wait is not None:
-                record_wait(p.wait)
-            returncode = 0
-        except EnvironmentError as ex:
-            returncode = ex.errno & 0xFF
-            if returncode == 0:
-                # This shouldn't happen, but just in case make sure the
-                # return code is never 0 here.
-                returncode = 255
-        except Exception:
-            returncode = 255
-        finally:
-            # mission accomplished, this child needs to exit and not
-            # continue the hg process here.
-            if stdin is not None:
-                stdin.close()
-            if record_wait is None:
-                os._exit(returncode)
-
-    if pycompat.ispy3:
-        # This branch is more robust, because it avoids running python
-        # code (hence gc finalizers, like sshpeer.__del__, which
-        # blocks).  But we can't easily do the equivalent in py2,
-        # because of the lack of start_new_session=True flag. Given
-        # that the py2 branch should die soon, the short-lived
-        # duplication seems acceptable.
-        runbgcommand = runbgcommandpy3
-    else:
-        runbgcommand = runbgcommandpy2
--- a/mercurial/utils/repoviewutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/utils/repoviewutil.py	Wed May 04 18:17:44 2022 +0200
@@ -6,7 +6,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 ### Nearest subset relation
 # Nearest subset of filter X is a filter Y so that:
--- a/mercurial/utils/resourceutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/utils/resourceutil.py	Wed May 04 18:17:44 2022 +0200
@@ -7,7 +7,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import imp
 import os
@@ -62,6 +61,10 @@
     # Force loading of the resources module
     resources.open_binary  # pytype: disable=module-attr
 
+    # py2exe raises an AssertionError if uses importlib.resources
+    if getattr(sys, "frozen", None) in ("console_exe", "windows_exe"):
+        raise ImportError
+
 except (ImportError, AttributeError):
     # importlib.resources was not found (almost definitely because we're on a
     # Python version before 3.7)
--- a/mercurial/utils/storageutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/utils/storageutil.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import re
 import struct
--- a/mercurial/utils/stringutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/utils/stringutil.py	Wed May 04 18:17:44 2022 +0200
@@ -7,7 +7,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import ast
 import codecs
@@ -497,7 +496,7 @@
 
 
 @attr.s(hash=True)
-class mailmapping(object):
+class mailmapping:
     """Represents a username/email key or value in
     a mailmap file"""
 
@@ -686,6 +685,18 @@
     return _correctauthorformat.match(author) is not None
 
 
+def firstline(text):
+    """Return the first line of the input"""
+    # Try to avoid running splitlines() on the whole string
+    i = text.find(b'\n')
+    if i != -1:
+        text = text[:i]
+    try:
+        return text.splitlines()[0]
+    except IndexError:
+        return b''
+
+
 def ellipsis(text, maxlength=400):
     """Trim string to at most maxlength (default: 400) columns in display."""
     return encoding.trim(text, maxlength, ellipsis=b'...')
@@ -965,6 +976,4 @@
 def evalpythonliteral(s):
     """Evaluate a string containing a Python literal expression"""
     # We could backport our tokenizer hack to rewrite '' to u'' if we want
-    if pycompat.ispy3:
-        return ast.literal_eval(s.decode('latin1'))
-    return ast.literal_eval(s)
+    return ast.literal_eval(s.decode('latin1'))
--- a/mercurial/utils/urlutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/utils/urlutil.py	Wed May 04 18:17:44 2022 +0200
@@ -54,7 +54,7 @@
         )
 
 
-class url(object):
+class url:
     r"""Reliable URL parser.
 
     This parses URLs and provides attributes for the following
@@ -453,7 +453,7 @@
     """list all the (name, paths) in the passed ui"""
     result = []
     if target_path is None:
-        for name, paths in sorted(pycompat.iteritems(ui.paths)):
+        for name, paths in sorted(ui.paths.items()):
             for p in paths:
                 result.append((name, p))
 
@@ -832,7 +832,7 @@
     return new_paths
 
 
-class path(object):
+class path:
     """Represents an individual path and its configuration."""
 
     def __init__(
@@ -919,7 +919,7 @@
         # Now process the sub-options. If a sub-option is registered, its
         # attribute will always be present. The value will be None if there
         # was no valid sub-option.
-        for suboption, (attr, func) in pycompat.iteritems(_pathsuboptions):
+        for suboption, (attr, func) in _pathsuboptions.items():
             if suboption not in sub_options:
                 setattr(self, attr, None)
                 continue
@@ -945,7 +945,7 @@
         This is intended to be used for presentation purposes.
         """
         d = {}
-        for subopt, (attr, _func) in pycompat.iteritems(_pathsuboptions):
+        for subopt, (attr, _func) in _pathsuboptions.items():
             value = getattr(self, attr)
             if value is not None:
                 d[subopt] = value
--- a/mercurial/verify.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/verify.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import os
 
@@ -55,7 +54,7 @@
 )
 
 
-class verifier(object):
+class verifier:
     def __init__(self, repo, level=None):
         self.repo = repo.unfiltered()
         self.ui = repo.ui
@@ -406,11 +405,11 @@
                 _(b'checking'), unit=_(b'manifests'), total=len(subdirs)
             )
 
-        for subdir, linkrevs in pycompat.iteritems(subdirnodes):
+        for subdir, linkrevs in subdirnodes.items():
             subdirfilenodes = self._verifymanifest(
                 linkrevs, subdir, storefiles, subdirprogress
             )
-            for f, onefilenodes in pycompat.iteritems(subdirfilenodes):
+            for f, onefilenodes in subdirfilenodes.items():
                 filenodes.setdefault(f, {}).update(onefilenodes)
 
         if not dir and subdirnodes:
@@ -575,7 +574,7 @@
 
             # cross-check
             if f in filenodes:
-                fns = [(v, k) for k, v in pycompat.iteritems(filenodes[f])]
+                fns = [(v, k) for k, v in filenodes[f].items()]
                 for lr, node in sorted(fns):
                     msg = _(b"manifest refers to unknown revision %s")
                     self._err(lr, msg % short(node), f)
--- a/mercurial/vfs.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/vfs.py	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-from __future__ import absolute_import
 
 import contextlib
 import errno
@@ -47,7 +46,7 @@
         checkandavoid()
 
 
-class abstractvfs(object):
+class abstractvfs:
     """Abstract base class; cannot be instantiated"""
 
     # default directory separator for vfs
@@ -607,7 +606,7 @@
         return self.vfs.join(path, *insidef)
 
 
-class closewrapbase(object):
+class closewrapbase:
     """Base class of wrapper, which hooks closing
 
     Do not instantiate outside of the vfs layer.
@@ -653,7 +652,7 @@
         self._closer.close(self._origfh)
 
 
-class backgroundfilecloser(object):
+class backgroundfilecloser:
     """Coordinates background closing of file handles on multiple threads."""
 
     def __init__(self, ui, expectedcount=-1):
--- a/mercurial/win32.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/win32.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import ctypes
 import ctypes.wintypes as wintypes
--- a/mercurial/windows.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/windows.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import errno
 import getpass
@@ -54,7 +53,7 @@
 umask = 0o022
 
 
-class mixedfilemodewrapper(object):
+class mixedfilemodewrapper:
     """Wraps a file handle when it is opened in read/write mode.
 
     fopen() and fdopen() on Windows have a specific-to-Windows requirement
@@ -131,7 +130,7 @@
         return self._fp.readlines(*args, **kwargs)
 
 
-class fdproxy(object):
+class fdproxy:
     """Wraps osutil.posixfile() to override the name attribute to reflect the
     underlying file name.
     """
@@ -163,8 +162,7 @@
 
         # PyFile_FromFd() ignores the name, and seems to report fp.name as the
         # underlying file descriptor.
-        if pycompat.ispy3:
-            fp = fdproxy(name, fp)
+        fp = fdproxy(name, fp)
 
         # The position when opening in append mode is implementation defined, so
         # make it consistent with other platforms, which position at EOF.
@@ -216,7 +214,7 @@
     return encoding.unitolocal(pw)
 
 
-class winstdout(object):
+class winstdout:
     """Some files on Windows misbehave.
 
     When writing to a broken pipe, EINVAL instead of EPIPE may be raised.
@@ -227,7 +225,6 @@
 
     def __init__(self, fp):
         self.fp = fp
-        self.throttle = not pycompat.ispy3 and _isatty(fp)
 
     def __getattr__(self, key):
         return getattr(self.fp, key)
@@ -240,17 +237,7 @@
 
     def write(self, s):
         try:
-            if not self.throttle:
-                return self.fp.write(s)
-            # This is workaround for "Not enough space" error on
-            # writing large size of data to console.
-            limit = 16000
-            l = len(s)
-            start = 0
-            while start < l:
-                end = start + limit
-                self.fp.write(s[start:end])
-                start = end
+            return self.fp.write(s)
         except IOError as inst:
             if inst.errno != 0 and not win32.lasterrorwaspipeerror(inst):
                 raise
@@ -671,7 +658,7 @@
     return False
 
 
-class cachestat(object):
+class cachestat:
     def __init__(self, path):
         pass
 
@@ -689,14 +676,21 @@
     LOCAL_MACHINE).
     """
     if scope is None:
+        # pytype: disable=module-attr
         scope = (winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE)
+        # pytype: enable=module-attr
     elif not isinstance(scope, (list, tuple)):
         scope = (scope,)
     for s in scope:
         try:
+            # pytype: disable=module-attr
             with winreg.OpenKey(s, encoding.strfromlocal(key)) as hkey:
+                # pytype: enable=module-attr
                 name = valname and encoding.strfromlocal(valname) or valname
+                # pytype: disable=module-attr
                 val = winreg.QueryValueEx(hkey, name)[0]
+                # pytype: enable=module-attr
+
                 # never let a Unicode string escape into the wild
                 return encoding.unitolocal(val)
         except EnvironmentError:
--- a/mercurial/wireprotoframing.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/wireprotoframing.py	Wed May 04 18:17:44 2022 +0200
@@ -9,7 +9,6 @@
 # protocol. For details about the protocol, see
 # `hg help internals.wireprotocol`.
 
-from __future__ import absolute_import
 
 import collections
 import struct
@@ -123,7 +122,7 @@
 
 def humanflags(mapping, value):
     """Convert a numeric flags value to a human value, using a mapping table."""
-    namemap = {v: k for k, v in pycompat.iteritems(mapping)}
+    namemap = {v: k for k, v in mapping.items()}
     flags = []
     val = 1
     while value >= val:
@@ -135,7 +134,7 @@
 
 
 @attr.s(slots=True)
-class frameheader(object):
+class frameheader:
     """Represents the data in a frame header."""
 
     length = attr.ib()
@@ -147,7 +146,7 @@
 
 
 @attr.s(slots=True, repr=False)
-class frame(object):
+class frame:
     """Represents a parsed frame."""
 
     requestid = attr.ib()
@@ -160,7 +159,7 @@
     @encoding.strmethod
     def __repr__(self):
         typename = b'<unknown 0x%02x>' % self.typeid
-        for name, value in pycompat.iteritems(FRAME_TYPES):
+        for name, value in FRAME_TYPES.items():
             if value == self.typeid:
                 typename = name
                 break
@@ -590,7 +589,7 @@
     )
 
 
-class bufferingcommandresponseemitter(object):
+class bufferingcommandresponseemitter:
     """Helper object to emit command response frames intelligently.
 
     Raw command response data is likely emitted in chunks much smaller
@@ -700,7 +699,7 @@
 # mechanism.
 
 
-class identityencoder(object):
+class identityencoder:
     """Encoder for the "identity" stream encoding profile."""
 
     def __init__(self, ui):
@@ -716,7 +715,7 @@
         return b''
 
 
-class identitydecoder(object):
+class identitydecoder:
     """Decoder for the "identity" stream encoding profile."""
 
     def __init__(self, ui, extraobjs):
@@ -729,7 +728,7 @@
         return data
 
 
-class zlibencoder(object):
+class zlibencoder:
     def __init__(self, ui):
         import zlib
 
@@ -750,7 +749,7 @@
         return res
 
 
-class zlibdecoder(object):
+class zlibdecoder:
     def __init__(self, ui, extraobjs):
         import zlib
 
@@ -762,15 +761,10 @@
         self._decompressor = zlib.decompressobj()
 
     def decode(self, data):
-        # Python 2's zlib module doesn't use the buffer protocol and can't
-        # handle all bytes-like types.
-        if not pycompat.ispy3 and isinstance(data, bytearray):
-            data = bytes(data)
-
         return self._decompressor.decompress(data)
 
 
-class zstdbaseencoder(object):
+class zstdbaseencoder:
     def __init__(self, level):
         from . import zstd
 
@@ -798,7 +792,7 @@
         super(zstd8mbencoder, self).__init__(3)
 
 
-class zstdbasedecoder(object):
+class zstdbasedecoder:
     def __init__(self, maxwindowsize):
         from . import zstd
 
@@ -848,7 +842,7 @@
     STREAM_ENCODERS_ORDER.append(b'identity')
 
 
-class stream(object):
+class stream:
     """Represents a logical unidirectional series of frames."""
 
     def __init__(self, streamid, active=False):
@@ -1001,7 +995,7 @@
 }
 
 
-class serverreactor(object):
+class serverreactor:
     """Holds state of a server handling frame-based protocol requests.
 
     This class is the "brain" of the unified frame-based protocol server
@@ -1689,7 +1683,7 @@
         return self._makeerrorresult(_(b'server already errored'))
 
 
-class commandrequest(object):
+class commandrequest:
     """Represents a request to run a command."""
 
     def __init__(self, requestid, name, args, datafh=None, redirect=None):
@@ -1701,7 +1695,7 @@
         self.state = b'pending'
 
 
-class clientreactor(object):
+class clientreactor:
     """Holds state of a client issuing frame-based protocol requests.
 
     This is like ``serverreactor`` but for client-side state.
--- a/mercurial/wireprotoserver.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/wireprotoserver.py	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import contextlib
 import struct
@@ -57,7 +56,7 @@
 
 
 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
-class httpv1protocolhandler(object):
+class httpv1protocolhandler:
     def __init__(self, req, ui, checkperm):
         self._req = req
         self._ui = ui
@@ -375,7 +374,7 @@
 
 
 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
-class sshv1protocolhandler(object):
+class sshv1protocolhandler:
     """Handler for requests services via version 1 of SSH protocol."""
 
     def __init__(self, ui, fin, fout):
@@ -521,7 +520,7 @@
             )
 
 
-class sshserver(object):
+class sshserver:
     def __init__(self, ui, repo, logfh=None):
         self._ui = ui
         self._repo = repo
--- a/mercurial/wireprototypes.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/wireprototypes.py	Wed May 04 18:17:44 2022 +0200
@@ -3,7 +3,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from .node import (
     bin,
@@ -40,14 +39,14 @@
 }
 
 
-class bytesresponse(object):
+class bytesresponse:
     """A wire protocol response consisting of raw bytes."""
 
     def __init__(self, data):
         self.data = data
 
 
-class ooberror(object):
+class ooberror:
     """wireproto reply: failure of a batch of operation
 
     Something failed during a batch call. The error message is stored in
@@ -58,7 +57,7 @@
         self.message = message
 
 
-class pushres(object):
+class pushres:
     """wireproto reply: success with simple integer return
 
     The call was successful and returned an integer contained in `self.res`.
@@ -69,7 +68,7 @@
         self.output = output
 
 
-class pusherr(object):
+class pusherr:
     """wireproto reply: failure
 
     The call failed. The `self.res` attribute contains the error message.
@@ -80,7 +79,7 @@
         self.output = output
 
 
-class streamres(object):
+class streamres:
     """wireproto reply: binary stream
 
     The call was successful and the result is a stream.
@@ -97,7 +96,7 @@
         self.prefer_uncompressed = prefer_uncompressed
 
 
-class streamreslegacy(object):
+class streamreslegacy:
     """wireproto reply: uncompressed binary stream
 
     The call was successful and the result is a stream.
@@ -244,7 +243,7 @@
         """
 
 
-class commandentry(object):
+class commandentry:
     """Represents a declared wire protocol command."""
 
     def __init__(
@@ -407,7 +406,7 @@
 
 
 @attr.s
-class encodedresponse(object):
+class encodedresponse:
     """Represents response data that is already content encoded.
 
     Wire protocol version 2 only.
@@ -421,7 +420,7 @@
 
 
 @attr.s
-class alternatelocationresponse(object):
+class alternatelocationresponse:
     """Represents a response available at an alternate location.
 
     Instances are sent in place of actual response objects when the server
@@ -440,7 +439,7 @@
 
 
 @attr.s
-class indefinitebytestringresponse(object):
+class indefinitebytestringresponse:
     """Represents an object to be encoded to an indefinite length bytestring.
 
     Instances are initialized from an iterable of chunks, with each chunk being
--- a/mercurial/wireprotov1peer.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/wireprotov1peer.py	Wed May 04 18:17:44 2022 +0200
@@ -5,11 +5,11 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import sys
 import weakref
 
+from concurrent import futures
 from .i18n import _
 from .node import bin
 from .pycompat import (
@@ -80,15 +80,14 @@
         assert all(escapearg(k) == k for k in argsdict)
 
         args = b','.join(
-            b'%s=%s' % (escapearg(k), escapearg(v))
-            for k, v in pycompat.iteritems(argsdict)
+            b'%s=%s' % (escapearg(k), escapearg(v)) for k, v in argsdict.items()
         )
         cmds.append(b'%s %s' % (op, args))
 
     return b';'.join(cmds)
 
 
-class unsentfuture(pycompat.futures.Future):
+class unsentfuture(futures.Future):
     """A Future variation to represent an unsent command.
 
     Because we buffer commands and don't submit them immediately, calling
@@ -99,7 +98,7 @@
 
     def result(self, timeout=None):
         if self.done():
-            return pycompat.futures.Future.result(self, timeout)
+            return futures.Future.result(self, timeout)
 
         self._peerexecutor.sendcommands()
 
@@ -110,7 +109,7 @@
 
 
 @interfaceutil.implementer(repository.ipeercommandexecutor)
-class peerexecutor(object):
+class peerexecutor:
     def __init__(self, peer):
         self._peer = peer
         self._sent = False
@@ -154,7 +153,7 @@
         # a batchable one and refuse to service it.
 
         def addcall():
-            f = pycompat.futures.Future()
+            f = futures.Future()
             self._futures.add(f)
             self._calls.append((command, args, fn, f))
             return f
@@ -194,7 +193,7 @@
         # cycle between us and futures.
         for f in self._futures:
             if isinstance(f, unsentfuture):
-                f.__class__ = pycompat.futures.Future
+                f.__class__ = futures.Future
                 f._peerexecutor = None
 
         calls = self._calls
@@ -258,7 +257,7 @@
         # hard and it is easy to encounter race conditions, deadlocks, etc.
         # concurrent.futures already solves these problems and its thread pool
         # executor has minimal overhead. So we use it.
-        self._responseexecutor = pycompat.futures.ThreadPoolExecutor(1)
+        self._responseexecutor = futures.ThreadPoolExecutor(1)
         self._responsef = self._responseexecutor.submit(
             self._readbatchresponse, states, wireresults
         )
@@ -438,7 +437,7 @@
         self.requirecap(b'getbundle', _(b'look up remote changes'))
         opts = {}
         bundlecaps = kwargs.get(b'bundlecaps') or set()
-        for key, value in pycompat.iteritems(kwargs):
+        for key, value in kwargs.items():
             if value is None:
                 continue
             keytype = wireprototypes.GETBUNDLE_ARGUMENTS.get(key)
--- a/mercurial/wireprotov1server.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/wireprotov1server.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import binascii
 import os
@@ -236,7 +235,7 @@
 def branchmap(repo, proto):
     branchmap = repo.branchmap()
     heads = []
-    for branch, nodes in pycompat.iteritems(branchmap):
+    for branch, nodes in branchmap.items():
         branchname = urlreq.quote(encoding.fromlocal(branch))
         branchnodes = wireprototypes.encodelist(nodes)
         heads.append(b'%s %s' % (branchname, branchnodes))
@@ -433,7 +432,7 @@
     opts = options(
         b'getbundle', wireprototypes.GETBUNDLE_ARGUMENTS.keys(), others
     )
-    for k, v in pycompat.iteritems(opts):
+    for k, v in opts.items():
         keytype = wireprototypes.GETBUNDLE_ARGUMENTS[k]
         if keytype == b'nodes':
             opts[k] = wireprototypes.decodelist(v)
--- a/mercurial/worker.py	Wed May 04 18:00:01 2022 +0200
+++ b/mercurial/worker.py	Wed May 04 18:17:44 2022 +0200
@@ -5,10 +5,10 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import errno
 import os
+import pickle
 import signal
 import sys
 import threading
@@ -27,7 +27,6 @@
     error,
     pycompat,
     scmutil,
-    util,
 )
 
 
@@ -65,51 +64,39 @@
     return min(max(countcpus(), 4), 32)
 
 
-if pycompat.ispy3:
-
-    def ismainthread():
-        return threading.current_thread() == threading.main_thread()
-
-    class _blockingreader(object):
-        def __init__(self, wrapped):
-            self._wrapped = wrapped
-
-        # Do NOT implement readinto() by making it delegate to
-        # _wrapped.readinto(), since that is unbuffered. The unpickler is fine
-        # with just read() and readline(), so we don't need to implement it.
-
-        def readline(self):
-            return self._wrapped.readline()
-
-        # issue multiple reads until size is fulfilled
-        def read(self, size=-1):
-            if size < 0:
-                return self._wrapped.readall()
-
-            buf = bytearray(size)
-            view = memoryview(buf)
-            pos = 0
-
-            while pos < size:
-                ret = self._wrapped.readinto(view[pos:])
-                if not ret:
-                    break
-                pos += ret
-
-            del view
-            del buf[pos:]
-            return bytes(buf)
+def ismainthread():
+    return threading.current_thread() == threading.main_thread()
 
 
-else:
+class _blockingreader:
+    def __init__(self, wrapped):
+        self._wrapped = wrapped
+
+    # Do NOT implement readinto() by making it delegate to
+    # _wrapped.readinto(), since that is unbuffered. The unpickler is fine
+    # with just read() and readline(), so we don't need to implement it.
+
+    def readline(self):
+        return self._wrapped.readline()
 
-    def ismainthread():
-        # pytype: disable=module-attr
-        return isinstance(threading.current_thread(), threading._MainThread)
-        # pytype: enable=module-attr
+    # issue multiple reads until size is fulfilled
+    def read(self, size=-1):
+        if size < 0:
+            return self._wrapped.readall()
+
+        buf = bytearray(size)
+        view = memoryview(buf)
+        pos = 0
 
-    def _blockingreader(wrapped):
-        return wrapped
+        while pos < size:
+            ret = self._wrapped.readinto(view[pos:])
+            if not ret:
+                break
+            pos += ret
+
+        del view
+        del buf[pos:]
+        return bytes(buf)
 
 
 if pycompat.isposix or pycompat.iswindows:
@@ -256,7 +243,7 @@
                         os.close(w)
                     os.close(rfd)
                     for result in func(*(staticargs + (pargs,))):
-                        os.write(wfd, util.pickle.dumps(result))
+                        os.write(wfd, pickle.dumps(result))
                     return 0
 
                 ret = scmutil.callcatch(ui, workerfunc)
@@ -292,7 +279,11 @@
         while openpipes > 0:
             for key, events in selector.select():
                 try:
-                    res = util.pickle.load(_blockingreader(key.fileobj))
+                    # The pytype error likely goes away on a modern version of
+                    # pytype having a modern typeshed snapshot.
+                    # pytype: disable=wrong-arg-types
+                    res = pickle.load(_blockingreader(key.fileobj))
+                    # pytype: enable=wrong-arg-types
                     if hasretval and res[0]:
                         retval.update(res[1])
                     else:
--- a/rust/Cargo.lock	Wed May 04 18:00:01 2022 +0200
+++ b/rust/Cargo.lock	Wed May 04 18:17:44 2022 +0200
@@ -15,10 +15,16 @@
 checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e"
 
 [[package]]
+name = "ahash"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e"
+
+[[package]]
 name = "aho-corasick"
-version = "0.7.15"
+version = "0.7.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5"
+checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
 dependencies = [
  "memchr",
 ]
@@ -31,9 +37,9 @@
 
 [[package]]
 name = "ansi_term"
-version = "0.11.0"
+version = "0.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
+checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
 dependencies = [
  "winapi",
 ]
@@ -57,9 +63,9 @@
 
 [[package]]
 name = "bitflags"
-version = "1.2.1"
+version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
 [[package]]
 name = "bitmaps"
@@ -80,10 +86,19 @@
 ]
 
 [[package]]
+name = "block-buffer"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
 name = "byteorder"
-version = "1.3.4"
+version = "1.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
 
 [[package]]
 name = "bytes-cast"
@@ -141,9 +156,9 @@
 
 [[package]]
 name = "clap"
-version = "2.33.3"
+version = "2.34.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
+checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
 dependencies = [
  "ansi_term",
  "atty",
@@ -161,6 +176,12 @@
 checksum = "cd51eab21ab4fd6a3bf889e2d0958c0a6e3a61ad04260325e919e652a2a62826"
 
 [[package]]
+name = "convert_case"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
+
+[[package]]
 name = "cpufeatures"
 version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -170,6 +191,15 @@
 ]
 
 [[package]]
+name = "cpufeatures"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469"
+dependencies = [
+ "libc",
+]
+
+[[package]]
 name = "cpython"
 version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -178,7 +208,6 @@
  "libc",
  "num-traits",
  "paste",
- "python27-sys",
  "python3-sys",
 ]
 
@@ -203,9 +232,9 @@
 
 [[package]]
 name = "crossbeam-channel"
-version = "0.5.0"
+version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775"
+checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa"
 dependencies = [
  "cfg-if 1.0.0",
  "crossbeam-utils 0.8.1",
@@ -259,6 +288,15 @@
 ]
 
 [[package]]
+name = "crypto-common"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4600d695eb3f6ce1cd44e6e291adceb2cc3ab12f20a33777ecd0bf6eba34e06"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
 name = "ctor"
 version = "0.1.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -270,20 +308,22 @@
 
 [[package]]
 name = "derive_more"
-version = "0.99.11"
+version = "0.99.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41cb0e6161ad61ed084a36ba71fbba9e3ac5aee3606fb607fe08da6acbcf3d8c"
+checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
 dependencies = [
+ "convert_case",
  "proc-macro2",
  "quote",
+ "rustc_version",
  "syn",
 ]
 
 [[package]]
-name = "difference"
-version = "2.0.0"
+name = "diff"
+version = "0.1.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
+checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499"
 
 [[package]]
 name = "digest"
@@ -295,6 +335,16 @@
 ]
 
 [[package]]
+name = "digest"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cb780dce4f9a8f5c087362b3a4595936b2019e7c8b30f2c3e9a7e94e6ae9837"
+dependencies = [
+ "block-buffer 0.10.2",
+ "crypto-common",
+]
+
+[[package]]
 name = "either"
 version = "1.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -302,9 +352,9 @@
 
 [[package]]
 name = "env_logger"
-version = "0.7.1"
+version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36"
+checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3"
 dependencies = [
  "atty",
  "humantime",
@@ -314,10 +364,19 @@
 ]
 
 [[package]]
+name = "fastrand"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
+dependencies = [
+ "instant",
+]
+
+[[package]]
 name = "flate2"
-version = "1.0.19"
+version = "1.0.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7411863d55df97a419aa64cb4d2f167103ea9d767e2c54a1868b7ac3f6b47129"
+checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f"
 dependencies = [
  "cfg-if 1.0.0",
  "crc32fast",
@@ -385,6 +444,16 @@
 checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
 
 [[package]]
+name = "hashbrown"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
+dependencies = [
+ "ahash",
+ "rayon",
+]
+
+[[package]]
 name = "hermit-abi"
 version = "0.1.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -411,23 +480,24 @@
  "derive_more",
  "flate2",
  "format-bytes",
+ "hashbrown",
  "home",
  "im-rc",
- "itertools",
+ "itertools 0.10.3",
  "lazy_static",
  "libc",
  "log",
  "memmap2",
- "micro-timer",
+ "micro-timer 0.3.1",
  "ouroboros",
  "pretty_assertions",
- "rand 0.8.4",
+ "rand 0.8.5",
  "rand_distr",
  "rand_pcg",
  "rayon",
  "regex",
  "same-file",
- "sha-1",
+ "sha-1 0.10.0",
  "tempfile",
  "twox-hash",
  "zstd",
@@ -438,7 +508,7 @@
 version = "0.1.0"
 dependencies = [
  "cpython",
- "crossbeam-channel 0.4.4",
+ "crossbeam-channel 0.5.2",
  "env_logger",
  "hg-core",
  "libc",
@@ -458,12 +528,9 @@
 
 [[package]]
 name = "humantime"
-version = "1.3.0"
+version = "2.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f"
-dependencies = [
- "quick-error",
-]
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
 
 [[package]]
 name = "im-rc"
@@ -480,6 +547,15 @@
 ]
 
 [[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
+[[package]]
 name = "itertools"
 version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -489,6 +565,15 @@
 ]
 
 [[package]]
+name = "itertools"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3"
+dependencies = [
+ "either",
+]
+
+[[package]]
 name = "jobserver"
 version = "0.1.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -528,11 +613,11 @@
 
 [[package]]
 name = "log"
-version = "0.4.11"
+version = "0.4.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
+checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
 dependencies = [
- "cfg-if 0.1.10",
+ "cfg-if 1.0.0",
 ]
 
 [[package]]
@@ -543,9 +628,9 @@
 
 [[package]]
 name = "memchr"
-version = "2.3.4"
+version = "2.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
+checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
 
 [[package]]
 name = "memmap2"
@@ -572,7 +657,17 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2620153e1d903d26b72b89f0e9c48d8c4756cba941c185461dddc234980c298c"
 dependencies = [
- "micro-timer-macros",
+ "micro-timer-macros 0.3.1",
+ "scopeguard",
+]
+
+[[package]]
+name = "micro-timer"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5de32cb59a062672560d6f0842c4aa7714727457b9fe2daf8987d995a176a405"
+dependencies = [
+ "micro-timer-macros 0.4.0",
  "scopeguard",
 ]
 
@@ -589,6 +684,18 @@
 ]
 
 [[package]]
+name = "micro-timer-macros"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cee948b94700125b52dfb68dd17c19f6326696c1df57f92c05ee857463c93ba1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "scopeguard",
+ "syn",
+]
+
+[[package]]
 name = "miniz_oxide"
 version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -687,13 +794,13 @@
 
 [[package]]
 name = "pretty_assertions"
-version = "0.6.1"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f81e1644e1b54f5a68959a29aa86cde704219254669da328ecfdf6a1f09d427"
+checksum = "76d5b548b725018ab5496482b45cb8bef21e9fed1858a6d674e3a8a0f0bb5d50"
 dependencies = [
  "ansi_term",
  "ctor",
- "difference",
+ "diff",
  "output_vt100",
 ]
 
@@ -731,16 +838,6 @@
 ]
 
 [[package]]
-name = "python27-sys"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94670354e264300dde81a5864cbb6bfc9d56ac3dcf3a278c32cb52f816f4dfd1"
-dependencies = [
- "libc",
- "regex",
-]
-
-[[package]]
 name = "python3-sys"
 version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -751,12 +848,6 @@
 ]
 
 [[package]]
-name = "quick-error"
-version = "1.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
-
-[[package]]
 name = "quote"
 version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -775,19 +866,18 @@
  "libc",
  "rand_chacha 0.2.2",
  "rand_core 0.5.1",
- "rand_hc 0.2.0",
+ "rand_hc",
 ]
 
 [[package]]
 name = "rand"
-version = "0.8.4"
+version = "0.8.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
 dependencies = [
  "libc",
  "rand_chacha 0.3.1",
  "rand_core 0.6.3",
- "rand_hc 0.3.1",
 ]
 
 [[package]]
@@ -830,12 +920,12 @@
 
 [[package]]
 name = "rand_distr"
-version = "0.4.2"
+version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "964d548f8e7d12e102ef183a0de7e98180c9f8729f555897a857b96e48122d2f"
+checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31"
 dependencies = [
  "num-traits",
- "rand 0.8.4",
+ "rand 0.8.5",
 ]
 
 [[package]]
@@ -848,15 +938,6 @@
 ]
 
 [[package]]
-name = "rand_hc"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
-dependencies = [
- "rand_core 0.6.3",
-]
-
-[[package]]
 name = "rand_pcg"
 version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -876,9 +957,9 @@
 
 [[package]]
 name = "rayon"
-version = "1.5.0"
+version = "1.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b0d8e0819fadc20c74ea8373106ead0600e3a67ef1fe8da56e39b9ae7275674"
+checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90"
 dependencies = [
  "autocfg",
  "crossbeam-deque",
@@ -888,11 +969,11 @@
 
 [[package]]
 name = "rayon-core"
-version = "1.9.0"
+version = "1.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ab346ac5921dc62ffa9f89b7a773907511cdfa5490c572ae9be1be33e8afa4a"
+checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e"
 dependencies = [
- "crossbeam-channel 0.5.0",
+ "crossbeam-channel 0.5.2",
  "crossbeam-deque",
  "crossbeam-utils 0.8.1",
  "lazy_static",
@@ -901,27 +982,29 @@
 
 [[package]]
 name = "redox_syscall"
-version = "0.1.57"
+version = "0.2.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
+checksum = "8380fe0152551244f0747b1bf41737e0f8a74f97a14ccefd1148187271634f3c"
+dependencies = [
+ "bitflags",
+]
 
 [[package]]
 name = "regex"
-version = "1.4.2"
+version = "1.5.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38cf2c13ed4745de91a5eb834e11c00bcc3709e773173b2ce4c56c9fbde04b9c"
+checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286"
 dependencies = [
  "aho-corasick",
  "memchr",
  "regex-syntax",
- "thread_local",
 ]
 
 [[package]]
 name = "regex-syntax"
-version = "0.6.21"
+version = "0.6.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189"
+checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
 
 [[package]]
 name = "remove_dir_all"
@@ -946,13 +1029,22 @@
  "home",
  "lazy_static",
  "log",
- "micro-timer",
+ "micro-timer 0.4.0",
  "regex",
  "users",
  "which",
 ]
 
 [[package]]
+name = "rustc_version"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
+dependencies = [
+ "semver",
+]
+
+[[package]]
 name = "same-file"
 version = "1.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -968,19 +1060,36 @@
 checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
 
 [[package]]
+name = "semver"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4a3381e03edd24287172047536f20cabde766e2cd3e65e6b00fb3af51c4f38d"
+
+[[package]]
 name = "sha-1"
 version = "0.9.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8c4cfa741c5832d0ef7fab46cabed29c2aae926db0b11bb2069edd8db5e64e16"
 dependencies = [
- "block-buffer",
+ "block-buffer 0.9.0",
  "cfg-if 1.0.0",
- "cpufeatures",
- "digest",
+ "cpufeatures 0.1.4",
+ "digest 0.9.0",
  "opaque-debug",
 ]
 
 [[package]]
+name = "sha-1"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f"
+dependencies = [
+ "cfg-if 1.0.0",
+ "cpufeatures 0.2.1",
+ "digest 0.10.2",
+]
+
+[[package]]
 name = "sized-chunks"
 version = "0.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1021,13 +1130,13 @@
 
 [[package]]
 name = "tempfile"
-version = "3.1.0"
+version = "3.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
+checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
 dependencies = [
- "cfg-if 0.1.10",
+ "cfg-if 1.0.0",
+ "fastrand",
  "libc",
- "rand 0.7.3",
  "redox_syscall",
  "remove_dir_all",
  "winapi",
@@ -1052,15 +1161,6 @@
 ]
 
 [[package]]
-name = "thread_local"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
-dependencies = [
- "lazy_static",
-]
-
-[[package]]
 name = "time"
 version = "0.1.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1073,12 +1173,12 @@
 
 [[package]]
 name = "twox-hash"
-version = "1.6.0"
+version = "1.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04f8ab788026715fa63b31960869617cba39117e520eb415b0139543e325ab59"
+checksum = "4ee73e6e4924fe940354b8d4d98cad5231175d615cd855b758adc658c0aac6a0"
 dependencies = [
- "cfg-if 0.1.10",
- "rand 0.7.3",
+ "cfg-if 1.0.0",
+ "rand 0.8.5",
  "static_assertions",
 ]
 
@@ -1090,9 +1190,9 @@
 
 [[package]]
 name = "unicode-width"
-version = "0.1.8"
+version = "0.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
+checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
 
 [[package]]
 name = "unicode-xid"
@@ -1124,7 +1224,7 @@
 dependencies = [
  "hex",
  "rand 0.7.3",
- "sha-1",
+ "sha-1 0.9.6",
 ]
 
 [[package]]
@@ -1195,18 +1295,18 @@
 
 [[package]]
 name = "zstd"
-version = "0.5.3+zstd.1.4.5"
+version = "0.5.4+zstd.1.4.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01b32eaf771efa709e8308605bbf9319bf485dc1503179ec0469b611937c0cd8"
+checksum = "69996ebdb1ba8b1517f61387a883857818a66c8a295f487b1ffd8fd9d2c82910"
 dependencies = [
  "zstd-safe",
 ]
 
 [[package]]
 name = "zstd-safe"
-version = "2.0.5+zstd.1.4.5"
+version = "2.0.6+zstd.1.4.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1cfb642e0d27f64729a639c52db457e0ae906e7bc6f5fe8f5c453230400f1055"
+checksum = "98aa931fb69ecee256d44589d19754e61851ae4769bf963b385119b1cc37a49e"
 dependencies = [
  "libc",
  "zstd-sys",
@@ -1214,12 +1314,12 @@
 
 [[package]]
 name = "zstd-sys"
-version = "1.4.17+zstd.1.4.5"
+version = "1.4.18+zstd.1.4.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b89249644df056b522696b1bb9e7c18c87e8ffa3e2f0dc3b0155875d6498f01b"
+checksum = "a1e6e8778706838f43f771d80d37787cb2fe06dafe89dd3aebaf6721b9eaec81"
 dependencies = [
  "cc",
  "glob",
- "itertools",
+ "itertools 0.9.0",
  "libc",
 ]
--- a/rust/README.rst	Wed May 04 18:00:01 2022 +0200
+++ b/rust/README.rst	Wed May 04 18:17:44 2022 +0200
@@ -40,8 +40,8 @@
 Special features
 ================
 
-You might want to check the `features` section in ``hg-cpython/Cargo.toml``.
-It may contain features that might be interesting to try out.
+In the future, compile-time opt-ins may be added
+to the `features` section in ``hg-cpython/Cargo.toml``.
 
 To use features from the Makefile, use the `HG_RUST_FEATURES` environment
 variable: for instance `HG_RUST_FEATURES="some-feature other-feature"`
--- a/rust/hg-core/Cargo.toml	Wed May 04 18:00:01 2022 +0200
+++ b/rust/hg-core/Cargo.toml	Wed May 04 18:17:44 2022 +0200
@@ -9,23 +9,24 @@
 name = "hg"
 
 [dependencies]
-bitflags = "1.2"
-bytes-cast = "0.2"
-byteorder = "1.3.4"
-derive_more = "0.99"
-home = "0.5"
-im-rc = "15.0.*"
-itertools = "0.9"
+bitflags = "1.3.2"
+bytes-cast = "0.2.0"
+byteorder = "1.4.3"
+derive_more = "0.99.17"
+hashbrown = { version = "0.9.1", features = ["rayon"] }
+home = "0.5.3"
+im-rc = "15.0.0"
+itertools = "0.10.3"
 lazy_static = "1.4.0"
 libc = "0.2"
 ouroboros = "0.15.0"
 rand = "0.8.4"
 rand_pcg = "0.3.1"
-rand_distr = "0.4.2"
-rayon = "1.3.0"
-regex = "1.3.9"
-sha-1 = "0.9.6"
-twox-hash = "1.5.0"
+rand_distr = "0.4.3"
+rayon = "1.5.1"
+regex = "1.5.5"
+sha-1 = "0.10.0"
+twox-hash = "1.6.2"
 same-file = "1.0.6"
 tempfile = "3.1.0"
 crossbeam-channel = "0.4"
@@ -38,10 +39,10 @@
 # We don't use the `miniz-oxide` backend to not change rhg benchmarks and until
 # we have a clearer view of which backend is the fastest.
 [dependencies.flate2]
-version = "1.0.16"
+version = "1.0.22"
 features = ["zlib"]
 default-features = false
 
 [dev-dependencies]
-clap = "*"
-pretty_assertions = "0.6.1"
+clap = "2.34.0"
+pretty_assertions = "1.1.0"
--- a/rust/hg-core/src/dirstate/dirs_multiset.rs	Wed May 04 18:00:01 2022 +0200
+++ b/rust/hg-core/src/dirstate/dirs_multiset.rs	Wed May 04 18:17:44 2022 +0200
@@ -10,7 +10,6 @@
 //! Used to counts the references to directories in a manifest or dirstate.
 use crate::dirstate_tree::on_disk::DirstateV2ParseError;
 use crate::{
-    dirstate::EntryState,
     utils::{
         files,
         hg_path::{HgPath, HgPathBuf, HgPathError},
@@ -49,7 +48,7 @@
             let filename = filename.as_ref();
             // This `if` is optimized out of the loop
             if only_tracked {
-                if entry.state() != EntryState::Removed {
+                if !entry.removed() {
                     multiset.add_path(filename)?;
                 }
             } else {
@@ -215,6 +214,8 @@
 
 #[cfg(test)]
 mod tests {
+    use crate::EntryState;
+
     use super::*;
 
     #[test]
--- a/rust/hg-core/src/dirstate/entry.rs	Wed May 04 18:00:01 2022 +0200
+++ b/rust/hg-core/src/dirstate/entry.rs	Wed May 04 18:17:44 2022 +0200
@@ -248,23 +248,41 @@
 /// dirstate v1 format.
 pub const SIZE_NON_NORMAL: i32 = -1;
 
+#[derive(Debug, Default, Copy, Clone)]
+pub struct DirstateV2Data {
+    pub wc_tracked: bool,
+    pub p1_tracked: bool,
+    pub p2_info: bool,
+    pub mode_size: Option<(u32, u32)>,
+    pub mtime: Option<TruncatedTimestamp>,
+    pub fallback_exec: Option<bool>,
+    pub fallback_symlink: Option<bool>,
+}
+
+#[derive(Debug, Default, Copy, Clone)]
+pub struct ParentFileData {
+    pub mode_size: Option<(u32, u32)>,
+    pub mtime: Option<TruncatedTimestamp>,
+}
+
 impl DirstateEntry {
-    pub fn from_v2_data(
-        wdir_tracked: bool,
-        p1_tracked: bool,
-        p2_info: bool,
-        mode_size: Option<(u32, u32)>,
-        mtime: Option<TruncatedTimestamp>,
-        fallback_exec: Option<bool>,
-        fallback_symlink: Option<bool>,
-    ) -> Self {
+    pub fn from_v2_data(v2_data: DirstateV2Data) -> Self {
+        let DirstateV2Data {
+            wc_tracked,
+            p1_tracked,
+            p2_info,
+            mode_size,
+            mtime,
+            fallback_exec,
+            fallback_symlink,
+        } = v2_data;
         if let Some((mode, size)) = mode_size {
             // TODO: return an error for out of range values?
             assert!(mode & !RANGE_MASK_31BIT == 0);
             assert!(size & !RANGE_MASK_31BIT == 0);
         }
         let mut flags = Flags::empty();
-        flags.set(Flags::WDIR_TRACKED, wdir_tracked);
+        flags.set(Flags::WDIR_TRACKED, wc_tracked);
         flags.set(Flags::P1_TRACKED, p1_tracked);
         flags.set(Flags::P2_INFO, p2_info);
         if let Some(exec) = fallback_exec {
@@ -367,6 +385,14 @@
         Self::from_v1_data(EntryState::Removed, 0, size, 0)
     }
 
+    pub fn new_tracked() -> Self {
+        let data = DirstateV2Data {
+            wc_tracked: true,
+            ..Default::default()
+        };
+        Self::from_v2_data(data)
+    }
+
     pub fn tracked(&self) -> bool {
         self.flags.contains(Flags::WDIR_TRACKED)
     }
@@ -391,6 +417,11 @@
         self.flags.contains(Flags::WDIR_TRACKED) && !self.in_either_parent()
     }
 
+    pub fn modified(&self) -> bool {
+        self.flags
+            .contains(Flags::WDIR_TRACKED | Flags::P1_TRACKED | Flags::P2_INFO)
+    }
+
     pub fn maybe_clean(&self) -> bool {
         if !self.flags.contains(Flags::WDIR_TRACKED) {
             false
@@ -409,36 +440,25 @@
         )
     }
 
-    /// Returns `(wdir_tracked, p1_tracked, p2_info, mode_size, mtime)`
-    pub(crate) fn v2_data(
-        &self,
-    ) -> (
-        bool,
-        bool,
-        bool,
-        Option<(u32, u32)>,
-        Option<TruncatedTimestamp>,
-        Option<bool>,
-        Option<bool>,
-    ) {
+    pub(crate) fn v2_data(&self) -> DirstateV2Data {
         if !self.any_tracked() {
             // TODO: return an Option instead?
-            panic!("Accessing v1_state of an untracked DirstateEntry")
+            panic!("Accessing v2_data of an untracked DirstateEntry")
         }
-        let wdir_tracked = self.flags.contains(Flags::WDIR_TRACKED);
+        let wc_tracked = self.flags.contains(Flags::WDIR_TRACKED);
         let p1_tracked = self.flags.contains(Flags::P1_TRACKED);
         let p2_info = self.flags.contains(Flags::P2_INFO);
         let mode_size = self.mode_size;
         let mtime = self.mtime;
-        (
-            wdir_tracked,
+        DirstateV2Data {
+            wc_tracked,
             p1_tracked,
             p2_info,
             mode_size,
             mtime,
-            self.get_fallback_exec(),
-            self.get_fallback_symlink(),
-        )
+            fallback_exec: self.get_fallback_exec(),
+            fallback_symlink: self.get_fallback_symlink(),
+        }
     }
 
     fn v1_state(&self) -> EntryState {
@@ -448,10 +468,7 @@
         }
         if self.removed() {
             EntryState::Removed
-        } else if self
-            .flags
-            .contains(Flags::WDIR_TRACKED | Flags::P1_TRACKED | Flags::P2_INFO)
-        {
+        } else if self.modified() {
             EntryState::Merged
         } else if self.added() {
             EntryState::Added
@@ -638,8 +655,7 @@
     }
 
     pub(crate) fn is_from_other_parent(&self) -> bool {
-        self.state() == EntryState::Normal
-            && self.size() == SIZE_FROM_OTHER_PARENT
+        self.flags.contains(Flags::WDIR_TRACKED | Flags::P2_INFO)
     }
 
     // TODO: other platforms
--- a/rust/hg-core/src/dirstate_tree/dirstate_map.rs	Wed May 04 18:00:01 2022 +0200
+++ b/rust/hg-core/src/dirstate_tree/dirstate_map.rs	Wed May 04 18:17:44 2022 +0200
@@ -11,18 +11,18 @@
 use crate::dirstate::parsers::packed_entry_size;
 use crate::dirstate::parsers::parse_dirstate_entries;
 use crate::dirstate::CopyMapIter;
+use crate::dirstate::DirstateV2Data;
+use crate::dirstate::ParentFileData;
 use crate::dirstate::StateMapIter;
 use crate::dirstate::TruncatedTimestamp;
-use crate::dirstate::SIZE_FROM_OTHER_PARENT;
-use crate::dirstate::SIZE_NON_NORMAL;
 use crate::matchers::Matcher;
 use crate::utils::hg_path::{HgPath, HgPathBuf};
 use crate::DirstateEntry;
 use crate::DirstateError;
+use crate::DirstateMapError;
 use crate::DirstateParents;
 use crate::DirstateStatus;
-use crate::EntryState;
-use crate::FastHashMap;
+use crate::FastHashbrownMap as FastHashMap;
 use crate::PatternFileWarning;
 use crate::StatusError;
 use crate::StatusOptions;
@@ -31,6 +31,7 @@
 /// anymore) is less than this fraction of the total amount of existing data.
 const ACCEPTABLE_UNREACHABLE_BYTES_RATIO: f32 = 0.5;
 
+#[derive(Debug)]
 pub struct DirstateMap<'on_disk> {
     /// Contents of the `.hg/dirstate` file
     pub(super) on_disk: &'on_disk [u8],
@@ -64,21 +65,25 @@
 
 /// Similar to `&'tree Cow<'on_disk, HgPath>`, but can also be returned
 /// for on-disk nodes that don’t actually have a `Cow` to borrow.
+#[derive(Debug)]
 pub(super) enum BorrowedPath<'tree, 'on_disk> {
     InMemory(&'tree HgPathBuf),
     OnDisk(&'on_disk HgPath),
 }
 
+#[derive(Debug)]
 pub(super) enum ChildNodes<'on_disk> {
     InMemory(FastHashMap<NodeKey<'on_disk>, Node<'on_disk>>),
     OnDisk(&'on_disk [on_disk::Node]),
 }
 
+#[derive(Debug)]
 pub(super) enum ChildNodesRef<'tree, 'on_disk> {
     InMemory(&'tree FastHashMap<NodeKey<'on_disk>, Node<'on_disk>>),
     OnDisk(&'on_disk [on_disk::Node]),
 }
 
+#[derive(Debug)]
 pub(super) enum NodeRef<'tree, 'on_disk> {
     InMemory(&'tree NodeKey<'on_disk>, &'tree Node<'on_disk>),
     OnDisk(&'on_disk on_disk::Node),
@@ -344,12 +349,6 @@
         }
     }
 
-    pub(super) fn state(
-        &self,
-    ) -> Result<Option<EntryState>, DirstateV2ParseError> {
-        Ok(self.entry()?.map(|e| e.state()))
-    }
-
     pub(super) fn cached_directory_mtime(
         &self,
     ) -> Result<Option<TruncatedTimestamp>, DirstateV2ParseError> {
@@ -380,7 +379,7 @@
 }
 
 /// Represents a file or a directory
-#[derive(Default)]
+#[derive(Default, Debug)]
 pub(super) struct Node<'on_disk> {
     pub(super) data: NodeData,
 
@@ -396,6 +395,7 @@
     pub(super) tracked_descendants_count: u32,
 }
 
+#[derive(Debug)]
 pub(super) enum NodeData {
     Entry(DirstateEntry),
     CachedDirectory { mtime: TruncatedTimestamp },
@@ -422,6 +422,13 @@
             _ => None,
         }
     }
+
+    fn as_entry_mut(&mut self) -> Option<&mut DirstateEntry> {
+        match self {
+            NodeData::Entry(entry) => Some(entry),
+            _ => None,
+        }
+    }
 }
 
 impl<'on_disk> DirstateMap<'on_disk> {
@@ -462,8 +469,8 @@
         let parents = parse_dirstate_entries(
             map.on_disk,
             |path, entry, copy_source| {
-                let tracked = entry.state().is_tracked();
-                let node = Self::get_or_insert_node(
+                let tracked = entry.tracked();
+                let node = Self::get_or_insert_node_inner(
                     map.on_disk,
                     &mut map.unreachable_bytes,
                     &mut map.root,
@@ -530,13 +537,37 @@
 
     /// Returns a mutable reference to the node at `path` if it exists
     ///
+    /// `each_ancestor` is a callback that is called for each ancestor node
+    /// when descending the tree. It is used to keep the different counters
+    /// of the `DirstateMap` up-to-date.
+    fn get_node_mut<'tree>(
+        &'tree mut self,
+        path: &HgPath,
+        each_ancestor: impl FnMut(&mut Node),
+    ) -> Result<Option<&'tree mut Node<'on_disk>>, DirstateV2ParseError> {
+        Self::get_node_mut_inner(
+            self.on_disk,
+            &mut self.unreachable_bytes,
+            &mut self.root,
+            path,
+            each_ancestor,
+        )
+    }
+
+    /// Lower-level version of `get_node_mut`.
+    ///
     /// This takes `root` instead of `&mut self` so that callers can mutate
-    /// other fields while the returned borrow is still valid
-    fn get_node_mut<'tree>(
+    /// other fields while the returned borrow is still valid.
+    ///
+    /// `each_ancestor` is a callback that is called for each ancestor node
+    /// when descending the tree. It is used to keep the different counters
+    /// of the `DirstateMap` up-to-date.
+    fn get_node_mut_inner<'tree>(
         on_disk: &'on_disk [u8],
         unreachable_bytes: &mut u32,
         root: &'tree mut ChildNodes<'on_disk>,
         path: &HgPath,
+        mut each_ancestor: impl FnMut(&mut Node),
     ) -> Result<Option<&'tree mut Node<'on_disk>>, DirstateV2ParseError> {
         let mut children = root;
         let mut components = path.components();
@@ -548,6 +579,7 @@
                 .get_mut(component)
             {
                 if let Some(next_component) = components.next() {
+                    each_ancestor(child);
                     component = next_component;
                     children = &mut child.children;
                 } else {
@@ -559,21 +591,30 @@
         }
     }
 
-    pub(super) fn get_or_insert<'tree, 'path>(
+    /// Get a mutable reference to the node at `path`, creating it if it does
+    /// not exist.
+    ///
+    /// `each_ancestor` is a callback that is called for each ancestor node
+    /// when descending the tree. It is used to keep the different counters
+    /// of the `DirstateMap` up-to-date.
+    fn get_or_insert_node<'tree, 'path>(
         &'tree mut self,
-        path: &HgPath,
+        path: &'path HgPath,
+        each_ancestor: impl FnMut(&mut Node),
     ) -> Result<&'tree mut Node<'on_disk>, DirstateV2ParseError> {
-        Self::get_or_insert_node(
+        Self::get_or_insert_node_inner(
             self.on_disk,
             &mut self.unreachable_bytes,
             &mut self.root,
             path,
             WithBasename::to_cow_owned,
-            |_| {},
+            each_ancestor,
         )
     }
 
-    fn get_or_insert_node<'tree, 'path>(
+    /// Lower-level version of `get_or_insert_node_inner`, which is used when
+    /// parsing disk data to remove allocations for new nodes.
+    fn get_or_insert_node_inner<'tree, 'path>(
         on_disk: &'on_disk [u8],
         unreachable_bytes: &mut u32,
         root: &'tree mut ChildNodes<'on_disk>,
@@ -590,13 +631,11 @@
             .next()
             .expect("expected at least one inclusive ancestor");
         loop {
-            // TODO: can we avoid allocating an owned key in cases where the
-            // map already contains that key, without introducing double
-            // lookup?
-            let child_node = child_nodes
+            let (_, child_node) = child_nodes
                 .make_mut(on_disk, unreachable_bytes)?
-                .entry(to_cow(ancestor_path))
-                .or_default();
+                .raw_entry_mut()
+                .from_key(ancestor_path.base_name())
+                .or_insert_with(|| (to_cow(ancestor_path), Node::default()));
             if let Some(next) = inclusive_ancestor_paths.next() {
                 each_ancestor(child_node);
                 ancestor_path = next;
@@ -607,46 +646,208 @@
         }
     }
 
-    fn add_or_remove_file(
+    fn reset_state(
+        &mut self,
+        filename: &HgPath,
+        old_entry_opt: Option<DirstateEntry>,
+        wc_tracked: bool,
+        p1_tracked: bool,
+        p2_info: bool,
+        has_meaningful_mtime: bool,
+        parent_file_data_opt: Option<ParentFileData>,
+    ) -> Result<(), DirstateError> {
+        let (had_entry, was_tracked) = match old_entry_opt {
+            Some(old_entry) => (true, old_entry.tracked()),
+            None => (false, false),
+        };
+        let node = self.get_or_insert_node(filename, |ancestor| {
+            if !had_entry {
+                ancestor.descendants_with_entry_count += 1;
+            }
+            if was_tracked {
+                if !wc_tracked {
+                    ancestor.tracked_descendants_count = ancestor
+                        .tracked_descendants_count
+                        .checked_sub(1)
+                        .expect("tracked count to be >= 0");
+                }
+            } else {
+                if wc_tracked {
+                    ancestor.tracked_descendants_count += 1;
+                }
+            }
+        })?;
+
+        let v2_data = if let Some(parent_file_data) = parent_file_data_opt {
+            DirstateV2Data {
+                wc_tracked,
+                p1_tracked,
+                p2_info,
+                mode_size: parent_file_data.mode_size,
+                mtime: if has_meaningful_mtime {
+                    parent_file_data.mtime
+                } else {
+                    None
+                },
+                ..Default::default()
+            }
+        } else {
+            DirstateV2Data {
+                wc_tracked,
+                p1_tracked,
+                p2_info,
+                ..Default::default()
+            }
+        };
+        node.data = NodeData::Entry(DirstateEntry::from_v2_data(v2_data));
+        if !had_entry {
+            self.nodes_with_entry_count += 1;
+        }
+        Ok(())
+    }
+
+    fn set_tracked(
+        &mut self,
+        filename: &HgPath,
+        old_entry_opt: Option<DirstateEntry>,
+    ) -> Result<bool, DirstateV2ParseError> {
+        let was_tracked = old_entry_opt.map_or(false, |e| e.tracked());
+        let had_entry = old_entry_opt.is_some();
+        let tracked_count_increment = if was_tracked { 0 } else { 1 };
+        let mut new = false;
+
+        let node = self.get_or_insert_node(filename, |ancestor| {
+            if !had_entry {
+                ancestor.descendants_with_entry_count += 1;
+            }
+
+            ancestor.tracked_descendants_count += tracked_count_increment;
+        })?;
+        if let Some(old_entry) = old_entry_opt {
+            let mut e = old_entry.clone();
+            if e.tracked() {
+                // XXX
+                // This is probably overkill for more case, but we need this to
+                // fully replace the `normallookup` call with `set_tracked`
+                // one. Consider smoothing this in the future.
+                e.set_possibly_dirty();
+            } else {
+                new = true;
+                e.set_tracked();
+            }
+            node.data = NodeData::Entry(e)
+        } else {
+            node.data = NodeData::Entry(DirstateEntry::new_tracked());
+            self.nodes_with_entry_count += 1;
+            new = true;
+        };
+        Ok(new)
+    }
+
+    /// Set a node as untracked in the dirstate.
+    ///
+    /// It is the responsibility of the caller to remove the copy source and/or
+    /// the entry itself if appropriate.
+    ///
+    /// # Panics
+    ///
+    /// Panics if the node does not exist.
+    fn set_untracked(
+        &mut self,
+        filename: &HgPath,
+        old_entry: DirstateEntry,
+    ) -> Result<(), DirstateV2ParseError> {
+        let node = self
+            .get_node_mut(filename, |ancestor| {
+                ancestor.tracked_descendants_count = ancestor
+                    .tracked_descendants_count
+                    .checked_sub(1)
+                    .expect("tracked_descendants_count should be >= 0");
+            })?
+            .expect("node should exist");
+        let mut new_entry = old_entry.clone();
+        new_entry.set_untracked();
+        node.data = NodeData::Entry(new_entry);
+        Ok(())
+    }
+
+    /// Set a node as clean in the dirstate.
+    ///
+    /// It is the responsibility of the caller to remove the copy source.
+    ///
+    /// # Panics
+    ///
+    /// Panics if the node does not exist.
+    fn set_clean(
+        &mut self,
+        filename: &HgPath,
+        old_entry: DirstateEntry,
+        mode: u32,
+        size: u32,
+        mtime: TruncatedTimestamp,
+    ) -> Result<(), DirstateError> {
+        let node = self
+            .get_node_mut(filename, |ancestor| {
+                if !old_entry.tracked() {
+                    ancestor.tracked_descendants_count += 1;
+                }
+            })?
+            .expect("node should exist");
+        let mut new_entry = old_entry.clone();
+        new_entry.set_clean(mode, size, mtime);
+        node.data = NodeData::Entry(new_entry);
+        Ok(())
+    }
+
+    /// Set a node as possibly dirty in the dirstate.
+    ///
+    /// # Panics
+    ///
+    /// Panics if the node does not exist.
+    fn set_possibly_dirty(
+        &mut self,
+        filename: &HgPath,
+    ) -> Result<(), DirstateError> {
+        let node = self
+            .get_node_mut(filename, |_ancestor| {})?
+            .expect("node should exist");
+        let entry = node.data.as_entry_mut().expect("entry should exist");
+        entry.set_possibly_dirty();
+        node.data = NodeData::Entry(*entry);
+        Ok(())
+    }
+
+    /// Clears the cached mtime for the (potential) folder at `path`.
+    pub(super) fn clear_cached_mtime(
         &mut self,
         path: &HgPath,
-        old_state: Option<EntryState>,
-        new_entry: DirstateEntry,
     ) -> Result<(), DirstateV2ParseError> {
-        let had_entry = old_state.is_some();
-        let was_tracked = old_state.map_or(false, |s| s.is_tracked());
-        let tracked_count_increment =
-            match (was_tracked, new_entry.state().is_tracked()) {
-                (false, true) => 1,
-                (true, false) => -1,
-                _ => 0,
-            };
+        let node = match self.get_node_mut(path, |_ancestor| {})? {
+            Some(node) => node,
+            None => return Ok(()),
+        };
+        if let NodeData::CachedDirectory { .. } = &node.data {
+            node.data = NodeData::None
+        }
+        Ok(())
+    }
 
-        let node = Self::get_or_insert_node(
-            self.on_disk,
-            &mut self.unreachable_bytes,
-            &mut self.root,
-            path,
-            WithBasename::to_cow_owned,
-            |ancestor| {
-                if !had_entry {
-                    ancestor.descendants_with_entry_count += 1;
-                }
-
-                // We can’t use `+= increment` because the counter is unsigned,
-                // and we want debug builds to detect accidental underflow
-                // through zero
-                match tracked_count_increment {
-                    1 => ancestor.tracked_descendants_count += 1,
-                    -1 => ancestor.tracked_descendants_count -= 1,
-                    _ => {}
-                }
-            },
-        )?;
-        if !had_entry {
-            self.nodes_with_entry_count += 1
+    /// Sets the cached mtime for the (potential) folder at `path`.
+    pub(super) fn set_cached_mtime(
+        &mut self,
+        path: &HgPath,
+        mtime: TruncatedTimestamp,
+    ) -> Result<(), DirstateV2ParseError> {
+        let node = match self.get_node_mut(path, |_ancestor| {})? {
+            Some(node) => node,
+            None => return Ok(()),
+        };
+        match &node.data {
+            NodeData::Entry(_) => {} // Don’t overwrite an entry
+            NodeData::CachedDirectory { .. } | NodeData::None => {
+                node.data = NodeData::CachedDirectory { mtime }
+            }
         }
-        node.data = NodeData::Entry(new_entry);
         Ok(())
     }
 
@@ -737,59 +938,103 @@
         });
     }
 
-    pub fn set_entry(
+    pub fn set_tracked(
+        &mut self,
+        filename: &HgPath,
+    ) -> Result<bool, DirstateV2ParseError> {
+        let old_entry_opt = self.get(filename)?;
+        self.with_dmap_mut(|map| map.set_tracked(filename, old_entry_opt))
+    }
+
+    pub fn set_untracked(
         &mut self,
         filename: &HgPath,
-        entry: DirstateEntry,
-    ) -> Result<(), DirstateV2ParseError> {
-        self.with_dmap_mut(|map| {
-            map.get_or_insert(&filename)?.data = NodeData::Entry(entry);
-            Ok(())
-        })
+    ) -> Result<bool, DirstateError> {
+        let old_entry_opt = self.get(filename)?;
+        match old_entry_opt {
+            None => Ok(false),
+            Some(old_entry) => {
+                if !old_entry.tracked() {
+                    // `DirstateMap::set_untracked` is not a noop if
+                    // already not tracked as it will decrement the
+                    // tracked counters while going down.
+                    return Ok(true);
+                }
+                if old_entry.added() {
+                    // Untracking an "added" entry will just result in a
+                    // worthless entry (and other parts of the code will
+                    // complain about it), just drop it entirely.
+                    self.drop_entry_and_copy_source(filename)?;
+                    return Ok(true);
+                }
+                if !old_entry.p2_info() {
+                    self.copy_map_remove(filename)?;
+                }
+
+                self.with_dmap_mut(|map| {
+                    map.set_untracked(filename, old_entry)?;
+                    Ok(true)
+                })
+            }
+        }
     }
 
-    pub fn add_file(
+    pub fn set_clean(
         &mut self,
         filename: &HgPath,
-        entry: DirstateEntry,
+        mode: u32,
+        size: u32,
+        mtime: TruncatedTimestamp,
     ) -> Result<(), DirstateError> {
-        let old_state = self.get(filename)?.map(|e| e.state());
+        let old_entry = match self.get(filename)? {
+            None => {
+                return Err(
+                    DirstateMapError::PathNotFound(filename.into()).into()
+                )
+            }
+            Some(e) => e,
+        };
+        self.copy_map_remove(filename)?;
         self.with_dmap_mut(|map| {
-            Ok(map.add_or_remove_file(filename, old_state, entry)?)
+            map.set_clean(filename, old_entry, mode, size, mtime)
         })
     }
 
-    pub fn remove_file(
+    pub fn set_possibly_dirty(
+        &mut self,
+        filename: &HgPath,
+    ) -> Result<(), DirstateError> {
+        if self.get(filename)?.is_none() {
+            return Err(DirstateMapError::PathNotFound(filename.into()).into());
+        }
+        self.with_dmap_mut(|map| map.set_possibly_dirty(filename))
+    }
+
+    pub fn reset_state(
         &mut self,
         filename: &HgPath,
-        in_merge: bool,
+        wc_tracked: bool,
+        p1_tracked: bool,
+        p2_info: bool,
+        has_meaningful_mtime: bool,
+        parent_file_data_opt: Option<ParentFileData>,
     ) -> Result<(), DirstateError> {
+        if !(p1_tracked || p2_info || wc_tracked) {
+            self.drop_entry_and_copy_source(filename)?;
+            return Ok(());
+        }
+        self.copy_map_remove(filename)?;
         let old_entry_opt = self.get(filename)?;
-        let old_state = old_entry_opt.map(|e| e.state());
-        let mut size = 0;
-        if in_merge {
-            // XXX we should not be able to have 'm' state and 'FROM_P2' if not
-            // during a merge. So I (marmoute) am not sure we need the
-            // conditionnal at all. Adding double checking this with assert
-            // would be nice.
-            if let Some(old_entry) = old_entry_opt {
-                // backup the previous state
-                if old_entry.state() == EntryState::Merged {
-                    size = SIZE_NON_NORMAL;
-                } else if old_entry.state() == EntryState::Normal
-                    && old_entry.size() == SIZE_FROM_OTHER_PARENT
-                {
-                    // other parent
-                    size = SIZE_FROM_OTHER_PARENT;
-                }
-            }
-        }
-        if size == 0 {
-            self.copy_map_remove(filename)?;
-        }
         self.with_dmap_mut(|map| {
-            let entry = DirstateEntry::new_removed(size);
-            Ok(map.add_or_remove_file(filename, old_state, entry)?)
+            map.reset_state(
+                filename,
+                old_entry_opt,
+                wc_tracked,
+                p1_tracked,
+                p2_info,
+                has_meaningful_mtime,
+                parent_file_data_opt,
+            )
         })
     }
 
@@ -797,9 +1042,7 @@
         &mut self,
         filename: &HgPath,
     ) -> Result<(), DirstateError> {
-        let was_tracked = self
-            .get(filename)?
-            .map_or(false, |e| e.state().is_tracked());
+        let was_tracked = self.get(filename)?.map_or(false, |e| e.tracked());
         struct Dropped {
             was_tracked: bool,
             had_entry: bool,
@@ -931,8 +1174,8 @@
             if let Some(node) = map.get_node(directory)? {
                 // A node without a `DirstateEntry` was created to hold child
                 // nodes, and is therefore a directory.
-                let state = node.state()?;
-                Ok(state.is_none() && node.tracked_descendants_count() > 0)
+                let is_dir = node.entry()?.is_none();
+                Ok(is_dir && node.tracked_descendants_count() > 0)
             } else {
                 Ok(false)
             }
@@ -947,8 +1190,8 @@
             if let Some(node) = map.get_node(directory)? {
                 // A node without a `DirstateEntry` was created to hold child
                 // nodes, and is therefore a directory.
-                let state = node.state()?;
-                Ok(state.is_none() && node.descendants_with_entry_count() > 0)
+                let is_dir = node.entry()?.is_none();
+                Ok(is_dir && node.descendants_with_entry_count() > 0)
             } else {
                 Ok(false)
             }
@@ -1078,15 +1321,18 @@
         self.with_dmap_mut(|map| {
             let count = &mut map.nodes_with_copy_source_count;
             let unreachable_bytes = &mut map.unreachable_bytes;
-            Ok(DirstateMap::get_node_mut(
+            Ok(DirstateMap::get_node_mut_inner(
                 map.on_disk,
                 unreachable_bytes,
                 &mut map.root,
                 key,
+                |_ancestor| {},
             )?
             .and_then(|node| {
                 if let Some(source) = &node.copy_source {
-                    *count -= 1;
+                    *count = count
+                        .checked_sub(1)
+                        .expect("nodes_with_copy_source_count should be >= 0");
                     DirstateMap::count_dropped_path(unreachable_bytes, source);
                 }
                 node.copy_source.take().map(Cow::into_owned)
@@ -1096,22 +1342,20 @@
 
     pub fn copy_map_insert(
         &mut self,
-        key: HgPathBuf,
-        value: HgPathBuf,
+        key: &HgPath,
+        value: &HgPath,
     ) -> Result<Option<HgPathBuf>, DirstateV2ParseError> {
         self.with_dmap_mut(|map| {
-            let node = DirstateMap::get_or_insert_node(
-                map.on_disk,
-                &mut map.unreachable_bytes,
-                &mut map.root,
-                &key,
-                WithBasename::to_cow_owned,
-                |_ancestor| {},
-            )?;
-            if node.copy_source.is_none() {
+            let node = map.get_or_insert_node(&key, |_ancestor| {})?;
+            let had_copy_source = node.copy_source.is_none();
+            let old = node
+                .copy_source
+                .replace(value.to_owned().into())
+                .map(Cow::into_owned);
+            if had_copy_source {
                 map.nodes_with_copy_source_count += 1
             }
-            Ok(node.copy_source.replace(value.into()).map(Cow::into_owned))
+            Ok(old)
         })
     }
 
@@ -1174,6 +1418,41 @@
         )))
     }
 
+    /// Only public because it needs to be exposed to the Python layer.
+    /// It is not the full `setparents` logic, only the parts that mutate the
+    /// entries.
+    pub fn setparents_fixup(
+        &mut self,
+    ) -> Result<Vec<(HgPathBuf, HgPathBuf)>, DirstateV2ParseError> {
+        // XXX
+        // All the copying and re-querying is quite inefficient, but this is
+        // still a lot better than doing it from Python.
+        //
+        // The better solution is to develop a mechanism for `iter_mut`,
+        // which will be a lot more involved: we're dealing with a lazy,
+        // append-mostly, tree-like data structure. This will do for now.
+        let mut copies = vec![];
+        let mut files_with_p2_info = vec![];
+        for res in self.iter() {
+            let (path, entry) = res?;
+            if entry.p2_info() {
+                files_with_p2_info.push(path.to_owned())
+            }
+        }
+        self.with_dmap_mut(|map| {
+            for path in files_with_p2_info.iter() {
+                let node = map.get_or_insert_node(path, |_| {})?;
+                let entry =
+                    node.data.as_entry_mut().expect("entry should exist");
+                entry.drop_merge_data();
+                if let Some(source) = node.copy_source.take().as_deref() {
+                    copies.push((path.to_owned(), source.to_owned()));
+                }
+            }
+            Ok(copies)
+        })
+    }
+
     pub fn debug_iter(
         &self,
         all: bool,
@@ -1201,3 +1480,418 @@
         }))
     }
 }
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    /// Shortcut to return tracked descendants of a path.
+    /// Panics if the path does not exist.
+    fn tracked_descendants(map: &OwningDirstateMap, path: &[u8]) -> u32 {
+        let path = dbg!(HgPath::new(path));
+        let node = map.get_map().get_node(path);
+        node.unwrap().unwrap().tracked_descendants_count()
+    }
+
+    /// Shortcut to return descendants with an entry.
+    /// Panics if the path does not exist.
+    fn descendants_with_an_entry(map: &OwningDirstateMap, path: &[u8]) -> u32 {
+        let path = dbg!(HgPath::new(path));
+        let node = map.get_map().get_node(path);
+        node.unwrap().unwrap().descendants_with_entry_count()
+    }
+
+    fn assert_does_not_exist(map: &OwningDirstateMap, path: &[u8]) {
+        let path = dbg!(HgPath::new(path));
+        let node = map.get_map().get_node(path);
+        assert!(node.unwrap().is_none());
+    }
+
+    /// Shortcut for path creation in tests
+    fn p(b: &[u8]) -> &HgPath {
+        HgPath::new(b)
+    }
+
+    /// Test the very simple case a single tracked file
+    #[test]
+    fn test_tracked_descendants_simple() -> Result<(), DirstateError> {
+        let mut map = OwningDirstateMap::new_empty(vec![]);
+        assert_eq!(map.len(), 0);
+
+        map.set_tracked(p(b"some/nested/path"))?;
+
+        assert_eq!(map.len(), 1);
+        assert_eq!(tracked_descendants(&map, b"some"), 1);
+        assert_eq!(tracked_descendants(&map, b"some/nested"), 1);
+        assert_eq!(tracked_descendants(&map, b"some/nested/path"), 0);
+
+        map.set_untracked(p(b"some/nested/path"))?;
+        assert_eq!(map.len(), 0);
+        assert!(map.get_map().get_node(p(b"some"))?.is_none());
+
+        Ok(())
+    }
+
+    /// Test the simple case of all tracked, but multiple files
+    #[test]
+    fn test_tracked_descendants_multiple() -> Result<(), DirstateError> {
+        let mut map = OwningDirstateMap::new_empty(vec![]);
+
+        map.set_tracked(p(b"some/nested/path"))?;
+        map.set_tracked(p(b"some/nested/file"))?;
+        // one layer without any files to test deletion cascade
+        map.set_tracked(p(b"some/other/nested/path"))?;
+        map.set_tracked(p(b"root_file"))?;
+        map.set_tracked(p(b"some/file"))?;
+        map.set_tracked(p(b"some/file2"))?;
+        map.set_tracked(p(b"some/file3"))?;
+
+        assert_eq!(map.len(), 7);
+        assert_eq!(tracked_descendants(&map, b"some"), 6);
+        assert_eq!(tracked_descendants(&map, b"some/nested"), 2);
+        assert_eq!(tracked_descendants(&map, b"some/other"), 1);
+        assert_eq!(tracked_descendants(&map, b"some/other/nested"), 1);
+        assert_eq!(tracked_descendants(&map, b"some/nested/path"), 0);
+
+        map.set_untracked(p(b"some/nested/path"))?;
+        assert_eq!(map.len(), 6);
+        assert_eq!(tracked_descendants(&map, b"some"), 5);
+        assert_eq!(tracked_descendants(&map, b"some/nested"), 1);
+        assert_eq!(tracked_descendants(&map, b"some/other"), 1);
+        assert_eq!(tracked_descendants(&map, b"some/other/nested"), 1);
+
+        map.set_untracked(p(b"some/nested/file"))?;
+        assert_eq!(map.len(), 5);
+        assert_eq!(tracked_descendants(&map, b"some"), 4);
+        assert_eq!(tracked_descendants(&map, b"some/other"), 1);
+        assert_eq!(tracked_descendants(&map, b"some/other/nested"), 1);
+        assert_does_not_exist(&map, b"some_nested");
+
+        map.set_untracked(p(b"some/other/nested/path"))?;
+        assert_eq!(map.len(), 4);
+        assert_eq!(tracked_descendants(&map, b"some"), 3);
+        assert_does_not_exist(&map, b"some/other");
+
+        map.set_untracked(p(b"root_file"))?;
+        assert_eq!(map.len(), 3);
+        assert_eq!(tracked_descendants(&map, b"some"), 3);
+        assert_does_not_exist(&map, b"root_file");
+
+        map.set_untracked(p(b"some/file"))?;
+        assert_eq!(map.len(), 2);
+        assert_eq!(tracked_descendants(&map, b"some"), 2);
+        assert_does_not_exist(&map, b"some/file");
+
+        map.set_untracked(p(b"some/file2"))?;
+        assert_eq!(map.len(), 1);
+        assert_eq!(tracked_descendants(&map, b"some"), 1);
+        assert_does_not_exist(&map, b"some/file2");
+
+        map.set_untracked(p(b"some/file3"))?;
+        assert_eq!(map.len(), 0);
+        assert_does_not_exist(&map, b"some/file3");
+
+        Ok(())
+    }
+
+    /// Check with a mix of tracked and non-tracked items
+    #[test]
+    fn test_tracked_descendants_different() -> Result<(), DirstateError> {
+        let mut map = OwningDirstateMap::new_empty(vec![]);
+
+        // A file that was just added
+        map.set_tracked(p(b"some/nested/path"))?;
+        // This has no information, the dirstate should ignore it
+        map.reset_state(p(b"some/file"), false, false, false, false, None)?;
+        assert_does_not_exist(&map, b"some/file");
+
+        // A file that was removed
+        map.reset_state(
+            p(b"some/nested/file"),
+            false,
+            true,
+            false,
+            false,
+            None,
+        )?;
+        assert!(!map.get(p(b"some/nested/file"))?.unwrap().tracked());
+        // Only present in p2
+        map.reset_state(p(b"some/file3"), false, false, true, false, None)?;
+        assert!(!map.get(p(b"some/file3"))?.unwrap().tracked());
+        // A file that was merged
+        map.reset_state(p(b"root_file"), true, true, true, false, None)?;
+        assert!(map.get(p(b"root_file"))?.unwrap().tracked());
+        // A file that is added, with info from p2
+        // XXX is that actually possible?
+        map.reset_state(p(b"some/file2"), true, false, true, false, None)?;
+        assert!(map.get(p(b"some/file2"))?.unwrap().tracked());
+        // A clean file
+        // One layer without any files to test deletion cascade
+        map.reset_state(
+            p(b"some/other/nested/path"),
+            true,
+            true,
+            false,
+            false,
+            None,
+        )?;
+        assert!(map.get(p(b"some/other/nested/path"))?.unwrap().tracked());
+
+        assert_eq!(map.len(), 6);
+        assert_eq!(tracked_descendants(&map, b"some"), 3);
+        assert_eq!(descendants_with_an_entry(&map, b"some"), 5);
+        assert_eq!(tracked_descendants(&map, b"some/other/nested"), 1);
+        assert_eq!(descendants_with_an_entry(&map, b"some/other/nested"), 1);
+        assert_eq!(tracked_descendants(&map, b"some/other/nested/path"), 0);
+        assert_eq!(
+            descendants_with_an_entry(&map, b"some/other/nested/path"),
+            0
+        );
+        assert_eq!(tracked_descendants(&map, b"some/nested"), 1);
+        assert_eq!(descendants_with_an_entry(&map, b"some/nested"), 2);
+
+        // might as well check this
+        map.set_untracked(p(b"path/does/not/exist"))?;
+        assert_eq!(map.len(), 6);
+
+        map.set_untracked(p(b"some/other/nested/path"))?;
+        // It is set untracked but not deleted since it held other information
+        assert_eq!(map.len(), 6);
+        assert_eq!(tracked_descendants(&map, b"some"), 2);
+        assert_eq!(descendants_with_an_entry(&map, b"some"), 5);
+        assert_eq!(descendants_with_an_entry(&map, b"some/other"), 1);
+        assert_eq!(descendants_with_an_entry(&map, b"some/other/nested"), 1);
+        assert_eq!(tracked_descendants(&map, b"some/nested"), 1);
+        assert_eq!(descendants_with_an_entry(&map, b"some/nested"), 2);
+
+        map.set_untracked(p(b"some/nested/path"))?;
+        // It is set untracked *and* deleted since it was only added
+        assert_eq!(map.len(), 5);
+        assert_eq!(tracked_descendants(&map, b"some"), 1);
+        assert_eq!(descendants_with_an_entry(&map, b"some"), 4);
+        assert_eq!(tracked_descendants(&map, b"some/nested"), 0);
+        assert_eq!(descendants_with_an_entry(&map, b"some/nested"), 1);
+        assert_does_not_exist(&map, b"some/nested/path");
+
+        map.set_untracked(p(b"root_file"))?;
+        // Untracked but not deleted
+        assert_eq!(map.len(), 5);
+        assert!(map.get(p(b"root_file"))?.is_some());
+
+        map.set_untracked(p(b"some/file2"))?;
+        assert_eq!(map.len(), 5);
+        assert_eq!(tracked_descendants(&map, b"some"), 0);
+        assert!(map.get(p(b"some/file2"))?.is_some());
+
+        map.set_untracked(p(b"some/file3"))?;
+        assert_eq!(map.len(), 5);
+        assert_eq!(tracked_descendants(&map, b"some"), 0);
+        assert!(map.get(p(b"some/file3"))?.is_some());
+
+        Ok(())
+    }
+
+    /// Check that copies counter is correctly updated
+    #[test]
+    fn test_copy_source() -> Result<(), DirstateError> {
+        let mut map = OwningDirstateMap::new_empty(vec![]);
+
+        // Clean file
+        map.reset_state(p(b"files/clean"), true, true, false, false, None)?;
+        // Merged file
+        map.reset_state(p(b"files/from_p2"), true, true, true, false, None)?;
+        // Removed file
+        map.reset_state(p(b"removed"), false, true, false, false, None)?;
+        // Added file
+        map.reset_state(p(b"files/added"), true, false, false, false, None)?;
+        // Add copy
+        map.copy_map_insert(p(b"files/clean"), p(b"clean_copy_source"))?;
+        assert_eq!(map.copy_map_len(), 1);
+
+        // Copy override
+        map.copy_map_insert(p(b"files/clean"), p(b"other_clean_copy_source"))?;
+        assert_eq!(map.copy_map_len(), 1);
+
+        // Multiple copies
+        map.copy_map_insert(p(b"removed"), p(b"removed_copy_source"))?;
+        assert_eq!(map.copy_map_len(), 2);
+
+        map.copy_map_insert(p(b"files/added"), p(b"added_copy_source"))?;
+        assert_eq!(map.copy_map_len(), 3);
+
+        // Added, so the entry is completely removed
+        map.set_untracked(p(b"files/added"))?;
+        assert_does_not_exist(&map, b"files/added");
+        assert_eq!(map.copy_map_len(), 2);
+
+        // Removed, so the entry is kept around, so is its copy
+        map.set_untracked(p(b"removed"))?;
+        assert!(map.get(p(b"removed"))?.is_some());
+        assert_eq!(map.copy_map_len(), 2);
+
+        // Clean, so the entry is kept around, but not its copy
+        map.set_untracked(p(b"files/clean"))?;
+        assert!(map.get(p(b"files/clean"))?.is_some());
+        assert_eq!(map.copy_map_len(), 1);
+
+        map.copy_map_insert(p(b"files/from_p2"), p(b"from_p2_copy_source"))?;
+        assert_eq!(map.copy_map_len(), 2);
+
+        // Info from p2, so its copy source info is kept around
+        map.set_untracked(p(b"files/from_p2"))?;
+        assert!(map.get(p(b"files/from_p2"))?.is_some());
+        assert_eq!(map.copy_map_len(), 2);
+
+        Ok(())
+    }
+
+    /// Test with "on disk" data. For the sake of this test, the "on disk" data
+    /// does not actually come from the disk, but it's opaque to the code being
+    /// tested.
+    #[test]
+    fn test_on_disk() -> Result<(), DirstateError> {
+        // First let's create some data to put "on disk"
+        let mut map = OwningDirstateMap::new_empty(vec![]);
+
+        // A file that was just added
+        map.set_tracked(p(b"some/nested/added"))?;
+        map.copy_map_insert(p(b"some/nested/added"), p(b"added_copy_source"))?;
+
+        // A file that was removed
+        map.reset_state(
+            p(b"some/nested/removed"),
+            false,
+            true,
+            false,
+            false,
+            None,
+        )?;
+        // Only present in p2
+        map.reset_state(
+            p(b"other/p2_info_only"),
+            false,
+            false,
+            true,
+            false,
+            None,
+        )?;
+        map.copy_map_insert(
+            p(b"other/p2_info_only"),
+            p(b"other/p2_info_copy_source"),
+        )?;
+        // A file that was merged
+        map.reset_state(p(b"merged"), true, true, true, false, None)?;
+        // A file that is added, with info from p2
+        // XXX is that actually possible?
+        map.reset_state(
+            p(b"other/added_with_p2"),
+            true,
+            false,
+            true,
+            false,
+            None,
+        )?;
+        // One layer without any files to test deletion cascade
+        // A clean file
+        map.reset_state(
+            p(b"some/other/nested/clean"),
+            true,
+            true,
+            false,
+            false,
+            None,
+        )?;
+
+        let (packed, metadata, _should_append, _old_data_size) =
+            map.pack_v2(false)?;
+        let packed_len = packed.len();
+        assert!(packed_len > 0);
+
+        // Recreate "from disk"
+        let mut map = OwningDirstateMap::new_v2(
+            packed,
+            packed_len,
+            metadata.as_bytes(),
+        )?;
+
+        // Check that everything is accounted for
+        assert!(map.contains_key(p(b"some/nested/added"))?);
+        assert!(map.contains_key(p(b"some/nested/removed"))?);
+        assert!(map.contains_key(p(b"merged"))?);
+        assert!(map.contains_key(p(b"other/p2_info_only"))?);
+        assert!(map.contains_key(p(b"other/added_with_p2"))?);
+        assert!(map.contains_key(p(b"some/other/nested/clean"))?);
+        assert_eq!(
+            map.copy_map_get(p(b"some/nested/added"))?,
+            Some(p(b"added_copy_source"))
+        );
+        assert_eq!(
+            map.copy_map_get(p(b"other/p2_info_only"))?,
+            Some(p(b"other/p2_info_copy_source"))
+        );
+        assert_eq!(tracked_descendants(&map, b"some"), 2);
+        assert_eq!(descendants_with_an_entry(&map, b"some"), 3);
+        assert_eq!(tracked_descendants(&map, b"other"), 1);
+        assert_eq!(descendants_with_an_entry(&map, b"other"), 2);
+        assert_eq!(tracked_descendants(&map, b"some/other"), 1);
+        assert_eq!(descendants_with_an_entry(&map, b"some/other"), 1);
+        assert_eq!(tracked_descendants(&map, b"some/other/nested"), 1);
+        assert_eq!(descendants_with_an_entry(&map, b"some/other/nested"), 1);
+        assert_eq!(tracked_descendants(&map, b"some/nested"), 1);
+        assert_eq!(descendants_with_an_entry(&map, b"some/nested"), 2);
+        assert_eq!(map.len(), 6);
+        assert_eq!(map.get_map().unreachable_bytes, 0);
+        assert_eq!(map.copy_map_len(), 2);
+
+        // Shouldn't change anything since it's already not tracked
+        map.set_untracked(p(b"some/nested/removed"))?;
+        assert_eq!(map.get_map().unreachable_bytes, 0);
+
+        match map.get_map().root {
+            ChildNodes::InMemory(_) => {
+                panic!("root should not have been mutated")
+            }
+            _ => (),
+        }
+        // We haven't mutated enough (nothing, actually), we should still be in
+        // the append strategy
+        assert!(map.get_map().write_should_append());
+
+        // But this mutates the structure, so there should be unreachable_bytes
+        assert!(map.set_untracked(p(b"some/nested/added"))?);
+        let unreachable_bytes = map.get_map().unreachable_bytes;
+        assert!(unreachable_bytes > 0);
+
+        match map.get_map().root {
+            ChildNodes::OnDisk(_) => panic!("root should have been mutated"),
+            _ => (),
+        }
+
+        // This should not mutate the structure either, since `root` has
+        // already been mutated along with its direct children.
+        map.set_untracked(p(b"merged"))?;
+        assert_eq!(map.get_map().unreachable_bytes, unreachable_bytes);
+
+        match map.get_map().get_node(p(b"other/added_with_p2"))?.unwrap() {
+            NodeRef::InMemory(_, _) => {
+                panic!("'other/added_with_p2' should not have been mutated")
+            }
+            _ => (),
+        }
+        // But this should, since it's in a different path
+        // than `<root>some/nested/add`
+        map.set_untracked(p(b"other/added_with_p2"))?;
+        assert!(map.get_map().unreachable_bytes > unreachable_bytes);
+
+        match map.get_map().get_node(p(b"other/added_with_p2"))?.unwrap() {
+            NodeRef::OnDisk(_) => {
+                panic!("'other/added_with_p2' should have been mutated")
+            }
+            _ => (),
+        }
+
+        // We have rewritten most of the tree, we should create a new file
+        assert!(!map.get_map().write_should_append());
+
+        Ok(())
+    }
+}
--- a/rust/hg-core/src/dirstate_tree/on_disk.rs	Wed May 04 18:00:01 2022 +0200
+++ b/rust/hg-core/src/dirstate_tree/on_disk.rs	Wed May 04 18:17:44 2022 +0200
@@ -2,7 +2,7 @@
 //!
 //! See `mercurial/helptext/internals/dirstate-v2.txt`
 
-use crate::dirstate::TruncatedTimestamp;
+use crate::dirstate::{DirstateV2Data, TruncatedTimestamp};
 use crate::dirstate_tree::dirstate_map::{self, DirstateMap, NodeRef};
 use crate::dirstate_tree::path_with_basename::WithBasename;
 use crate::errors::HgError;
@@ -84,7 +84,7 @@
 
 /// Fields are documented in the *The data file format*
 /// section of `mercurial/helptext/internals/dirstate-v2.txt`
-#[derive(BytesCast)]
+#[derive(BytesCast, Debug)]
 #[repr(C)]
 pub(super) struct Node {
     full_path: PathSlice,
@@ -124,7 +124,7 @@
 }
 
 /// Duration since the Unix epoch
-#[derive(BytesCast, Copy, Clone)]
+#[derive(BytesCast, Copy, Clone, Debug)]
 #[repr(C)]
 struct PackedTruncatedTimestamp {
     truncated_seconds: U32Be,
@@ -152,7 +152,7 @@
 /// Always sorted by ascending `full_path`, to allow binary search.
 /// Since nodes with the same parent nodes also have the same parent path,
 /// only the `base_name`s need to be compared during binary search.
-#[derive(BytesCast, Copy, Clone)]
+#[derive(BytesCast, Copy, Clone, Debug)]
 #[repr(C)]
 struct ChildNodes {
     start: Offset,
@@ -160,7 +160,7 @@
 }
 
 /// A `HgPath` of `len` bytes
-#[derive(BytesCast, Copy, Clone)]
+#[derive(BytesCast, Copy, Clone, Debug)]
 #[repr(C)]
 struct PathSlice {
     start: Offset,
@@ -413,7 +413,7 @@
 
     fn assume_entry(&self) -> Result<DirstateEntry, DirstateV2ParseError> {
         // TODO: convert through raw bits instead?
-        let wdir_tracked = self.flags().contains(Flags::WDIR_TRACKED);
+        let wc_tracked = self.flags().contains(Flags::WDIR_TRACKED);
         let p1_tracked = self.flags().contains(Flags::P1_TRACKED);
         let p2_info = self.flags().contains(Flags::P2_INFO);
         let mode_size = if self.flags().contains(Flags::HAS_MODE_AND_SIZE)
@@ -443,15 +443,15 @@
             } else {
                 None
             };
-        Ok(DirstateEntry::from_v2_data(
-            wdir_tracked,
+        Ok(DirstateEntry::from_v2_data(DirstateV2Data {
+            wc_tracked,
             p1_tracked,
             p2_info,
             mode_size,
             mtime,
             fallback_exec,
             fallback_symlink,
-        ))
+        }))
     }
 
     pub(super) fn entry(
@@ -491,18 +491,18 @@
     fn from_dirstate_entry(
         entry: &DirstateEntry,
     ) -> (Flags, U32Be, PackedTruncatedTimestamp) {
-        let (
-            wdir_tracked,
+        let DirstateV2Data {
+            wc_tracked,
             p1_tracked,
             p2_info,
-            mode_size_opt,
-            mtime_opt,
+            mode_size: mode_size_opt,
+            mtime: mtime_opt,
             fallback_exec,
             fallback_symlink,
-        ) = entry.v2_data();
-        // TODO: convert throug raw flag bits instead?
+        } = entry.v2_data();
+        // TODO: convert through raw flag bits instead?
         let mut flags = Flags::empty();
-        flags.set(Flags::WDIR_TRACKED, wdir_tracked);
+        flags.set(Flags::WDIR_TRACKED, wc_tracked);
         flags.set(Flags::P1_TRACKED, p1_tracked);
         flags.set(Flags::P2_INFO, p2_info);
         let size = if let Some((m, s)) = mode_size_opt {
@@ -588,7 +588,7 @@
     ) -> Result<(), DirstateV2ParseError> {
         for node in read_nodes(on_disk, nodes)? {
             if let Some(entry) = node.entry()? {
-                if entry.state().is_tracked() {
+                if entry.tracked() {
                     f(node.full_path(on_disk)?)
                 }
             }
--- a/rust/hg-core/src/dirstate_tree/status.rs	Wed May 04 18:00:01 2022 +0200
+++ b/rust/hg-core/src/dirstate_tree/status.rs	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 use crate::dirstate_tree::dirstate_map::BorrowedPath;
 use crate::dirstate_tree::dirstate_map::ChildNodesRef;
 use crate::dirstate_tree::dirstate_map::DirstateMap;
-use crate::dirstate_tree::dirstate_map::NodeData;
 use crate::dirstate_tree::dirstate_map::NodeRef;
 use crate::dirstate_tree::on_disk::DirstateV2ParseError;
 use crate::matchers::get_ignore_function;
@@ -14,7 +13,6 @@
 use crate::utils::hg_path::HgPath;
 use crate::BadMatch;
 use crate::DirstateStatus;
-use crate::EntryState;
 use crate::HgPathBuf;
 use crate::HgPathCow;
 use crate::PatternFileWarning;
@@ -140,19 +138,10 @@
     // Remove outdated mtimes before adding new mtimes, in case a given
     // directory is both
     for path in &outdated {
-        let node = dmap.get_or_insert(path)?;
-        if let NodeData::CachedDirectory { .. } = &node.data {
-            node.data = NodeData::None
-        }
+        dmap.clear_cached_mtime(path)?;
     }
     for (path, mtime) in &new_cachable {
-        let node = dmap.get_or_insert(path)?;
-        match &node.data {
-            NodeData::Entry(_) => {} // Don’t overwrite an entry
-            NodeData::CachedDirectory { .. } | NodeData::None => {
-                node.data = NodeData::CachedDirectory { mtime: *mtime }
-            }
-        }
+        dmap.set_cached_mtime(path, *mtime)?;
     }
 
     Ok((outcome, warnings))
@@ -469,17 +458,23 @@
             )?
         } else {
             if file_or_symlink && self.matcher.matches(hg_path) {
-                if let Some(state) = dirstate_node.state()? {
-                    match state {
-                        EntryState::Added => {
-                            self.push_outcome(Outcome::Added, &dirstate_node)?
-                        }
-                        EntryState::Removed => self
-                            .push_outcome(Outcome::Removed, &dirstate_node)?,
-                        EntryState::Merged => self
-                            .push_outcome(Outcome::Modified, &dirstate_node)?,
-                        EntryState::Normal => self
-                            .handle_normal_file(&dirstate_node, fs_metadata)?,
+                if let Some(entry) = dirstate_node.entry()? {
+                    if !entry.any_tracked() {
+                        // Forward-compat if we start tracking unknown/ignored
+                        // files for caching reasons
+                        self.mark_unknown_or_ignored(
+                            has_ignored_ancestor,
+                            hg_path,
+                        );
+                    }
+                    if entry.added() {
+                        self.push_outcome(Outcome::Added, &dirstate_node)?;
+                    } else if entry.removed() {
+                        self.push_outcome(Outcome::Removed, &dirstate_node)?;
+                    } else if entry.modified() {
+                        self.push_outcome(Outcome::Modified, &dirstate_node)?;
+                    } else {
+                        self.handle_normal_file(&dirstate_node, fs_metadata)?;
                     }
                 } else {
                     // `node.entry.is_none()` indicates a "directory"
@@ -589,8 +584,7 @@
         Ok(())
     }
 
-    /// A file with `EntryState::Normal` in the dirstate was found in the
-    /// filesystem
+    /// A file that is clean in the dirstate was found in the filesystem
     fn handle_normal_file(
         &self,
         dirstate_node: &NodeRef<'tree, 'on_disk>,
@@ -663,10 +657,15 @@
         &self,
         dirstate_node: &NodeRef<'tree, 'on_disk>,
     ) -> Result<(), DirstateV2ParseError> {
-        if let Some(state) = dirstate_node.state()? {
+        if let Some(entry) = dirstate_node.entry()? {
+            if !entry.any_tracked() {
+                // Future-compat for when we start storing ignored and unknown
+                // files for caching reasons
+                return Ok(());
+            }
             let path = dirstate_node.full_path(self.dmap.on_disk)?;
             if self.matcher.matches(path) {
-                if let EntryState::Removed = state {
+                if entry.removed() {
                     self.push_outcome(Outcome::Removed, dirstate_node)?
                 } else {
                     self.push_outcome(Outcome::Deleted, &dirstate_node)?
--- a/rust/hg-core/src/lib.rs	Wed May 04 18:00:01 2022 +0200
+++ b/rust/hg-core/src/lib.rs	Wed May 04 18:17:44 2022 +0200
@@ -56,6 +56,11 @@
 /// write access to your repository, you have other issues.
 pub type FastHashMap<K, V> = HashMap<K, V, RandomXxHashBuilder64>;
 
+// TODO: should this be the default `FastHashMap` for all of hg-core, not just
+// dirstate_tree? How does XxHash compare with AHash, hashbrown’s default?
+pub type FastHashbrownMap<K, V> =
+    hashbrown::HashMap<K, V, RandomXxHashBuilder64>;
+
 #[derive(Debug, PartialEq)]
 pub enum DirstateMapError {
     PathNotFound(HgPathBuf),
--- a/rust/hg-core/src/operations/debugdata.rs	Wed May 04 18:00:01 2022 +0200
+++ b/rust/hg-core/src/operations/debugdata.rs	Wed May 04 18:17:44 2022 +0200
@@ -6,6 +6,7 @@
 // GNU General Public License version 2 or any later version.
 
 use crate::repo::Repo;
+use crate::requirements;
 use crate::revlog::revlog::{Revlog, RevlogError};
 
 /// Kind of data to debug
@@ -25,7 +26,11 @@
         DebugDataKind::Changelog => "00changelog.i",
         DebugDataKind::Manifest => "00manifest.i",
     };
-    let revlog = Revlog::open(repo, index_file, None)?;
+    let use_nodemap = repo
+        .requirements()
+        .contains(requirements::NODEMAP_REQUIREMENT);
+    let revlog =
+        Revlog::open(&repo.store_vfs(), index_file, None, use_nodemap)?;
     let rev =
         crate::revset::resolve_rev_number_or_hex_prefix(revset, &revlog)?;
     let data = revlog.get_rev_data(rev)?;
--- a/rust/hg-core/src/operations/list_tracked_files.rs	Wed May 04 18:00:01 2022 +0200
+++ b/rust/hg-core/src/operations/list_tracked_files.rs	Wed May 04 18:17:44 2022 +0200
@@ -51,7 +51,7 @@
                 let _parents = parse_dirstate_entries(
                     &self.content,
                     |path, entry, _copy_source| {
-                        if entry.state().is_tracked() {
+                        if entry.tracked() {
                             files.push(path)
                         }
                         Ok(())
--- a/rust/hg-core/src/repo.rs	Wed May 04 18:00:01 2022 +0200
+++ b/rust/hg-core/src/repo.rs	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 use crate::dirstate_tree::owning::OwningDirstateMap;
 use crate::errors::HgResultExt;
 use crate::errors::{HgError, IoResultExt};
-use crate::exit_codes;
 use crate::lock::{try_with_lock_no_wait, LockError};
 use crate::manifest::{Manifest, Manifestlog};
 use crate::revlog::filelog::Filelog;
@@ -159,31 +158,8 @@
                 requirements::load(Vfs { base: &shared_path })?
                     .contains(requirements::SHARESAFE_REQUIREMENT);
 
-            if share_safe && !source_is_share_safe {
-                return Err(match config
-                    .get(b"share", b"safe-mismatch.source-not-safe")
-                {
-                    Some(b"abort") | None => HgError::abort(
-                        "abort: share source does not support share-safe requirement\n\
-                        (see `hg help config.format.use-share-safe` for more information)",
-                        exit_codes::ABORT,
-                    ),
-                    _ => HgError::unsupported("share-safe downgrade"),
-                }
-                .into());
-            } else if source_is_share_safe && !share_safe {
-                return Err(
-                    match config.get(b"share", b"safe-mismatch.source-safe") {
-                        Some(b"abort") | None => HgError::abort(
-                            "abort: version mismatch: source uses share-safe \
-                            functionality while the current share does not\n\
-                            (see `hg help config.format.use-share-safe` for more information)",
-                        exit_codes::ABORT,
-                        ),
-                        _ => HgError::unsupported("share-safe upgrade"),
-                    }
-                    .into(),
-                );
+            if share_safe != source_is_share_safe {
+                return Err(HgError::unsupported("share-safe mismatch").into());
             }
 
             if share_safe {
@@ -211,8 +187,8 @@
                 Self::read_dirstate_data_file_uuid,
             ),
             dirstate_map: LazyCell::new(Self::new_dirstate_map),
-            changelog: LazyCell::new(Changelog::open),
-            manifestlog: LazyCell::new(Manifestlog::open),
+            changelog: LazyCell::new(Self::new_changelog),
+            manifestlog: LazyCell::new(Self::new_manifestlog),
         };
 
         requirements::check(&repo)?;
@@ -270,6 +246,11 @@
         self.requirements.contains(requirements::NARROW_REQUIREMENT)
     }
 
+    pub fn has_nodemap(&self) -> bool {
+        self.requirements
+            .contains(requirements::NODEMAP_REQUIREMENT)
+    }
+
     fn dirstate_file_contents(&self) -> Result<Vec<u8>, HgError> {
         Ok(self
             .hg_vfs()
@@ -368,6 +349,10 @@
         self.dirstate_map.get_mut_or_init(self)
     }
 
+    fn new_changelog(&self) -> Result<Changelog, HgError> {
+        Changelog::open(&self.store_vfs(), self.has_nodemap())
+    }
+
     pub fn changelog(&self) -> Result<Ref<Changelog>, HgError> {
         self.changelog.get_or_init(self)
     }
@@ -376,6 +361,10 @@
         self.changelog.get_mut_or_init(self)
     }
 
+    fn new_manifestlog(&self) -> Result<Manifestlog, HgError> {
+        Manifestlog::open(&self.store_vfs(), self.has_nodemap())
+    }
+
     pub fn manifestlog(&self) -> Result<Ref<Manifestlog>, HgError> {
         self.manifestlog.get_or_init(self)
     }
@@ -412,7 +401,7 @@
 
     pub fn has_subrepos(&self) -> Result<bool, DirstateError> {
         if let Some(entry) = self.dirstate_map()?.get(HgPath::new(".hgsub"))? {
-            Ok(entry.state().is_tracked())
+            Ok(entry.tracked())
         } else {
             Ok(false)
         }
--- a/rust/hg-core/src/requirements.rs	Wed May 04 18:00:01 2022 +0200
+++ b/rust/hg-core/src/requirements.rs	Wed May 04 18:17:44 2022 +0200
@@ -92,6 +92,8 @@
     // not should opt out by checking `has_sparse` and `has_narrow`.
     SPARSE_REQUIREMENT,
     NARROW_REQUIREMENT,
+    // rhg doesn't care about bookmarks at all yet
+    BOOKMARKS_IN_STORE_REQUIREMENT,
 ];
 
 // Copied from mercurial/requirements.py:
@@ -103,6 +105,11 @@
 #[allow(unused)]
 pub(crate) const NARROW_REQUIREMENT: &str = "narrowhg-experimental";
 
+/// Bookmarks must be stored in the `store` part of the repository and will be
+/// share accross shares
+#[allow(unused)]
+pub(crate) const BOOKMARKS_IN_STORE_REQUIREMENT: &str = "bookmarksinstore";
+
 /// Enables sparse working directory usage
 #[allow(unused)]
 pub(crate) const SPARSE_REQUIREMENT: &str = "exp-sparse";
--- a/rust/hg-core/src/revlog/changelog.rs	Wed May 04 18:00:01 2022 +0200
+++ b/rust/hg-core/src/revlog/changelog.rs	Wed May 04 18:17:44 2022 +0200
@@ -1,9 +1,13 @@
 use crate::errors::HgError;
-use crate::repo::Repo;
-use crate::revlog::node::NULL_NODE;
-use crate::revlog::revlog::{Revlog, RevlogError};
+use crate::revlog::revlog::{Revlog, RevlogEntry, RevlogError};
 use crate::revlog::Revision;
 use crate::revlog::{Node, NodePrefix};
+use crate::utils::hg_path::HgPath;
+use crate::vfs::Vfs;
+use itertools::Itertools;
+use std::ascii::escape_default;
+use std::borrow::Cow;
+use std::fmt::{Debug, Formatter};
 
 /// A specialized `Revlog` to work with `changelog` data format.
 pub struct Changelog {
@@ -13,8 +17,9 @@
 
 impl Changelog {
     /// Open the `changelog` of a repository given by its root.
-    pub fn open(repo: &Repo) -> Result<Self, HgError> {
-        let revlog = Revlog::open(repo, "00changelog.i", None)?;
+    pub fn open(store_vfs: &Vfs, use_nodemap: bool) -> Result<Self, HgError> {
+        let revlog =
+            Revlog::open(store_vfs, "00changelog.i", None, use_nodemap)?;
         Ok(Self { revlog })
     }
 
@@ -27,41 +32,240 @@
         self.data_for_rev(rev)
     }
 
+    /// Return the `RevlogEntry` of the given revision number.
+    pub fn entry_for_rev(
+        &self,
+        rev: Revision,
+    ) -> Result<RevlogEntry, RevlogError> {
+        self.revlog.get_entry(rev)
+    }
+
     /// Return the `ChangelogEntry` of the given revision number.
     pub fn data_for_rev(
         &self,
         rev: Revision,
     ) -> Result<ChangelogRevisionData, RevlogError> {
-        let bytes = self.revlog.get_rev_data(rev)?.into_owned();
-        Ok(ChangelogRevisionData { bytes })
+        let bytes = self.revlog.get_rev_data(rev)?;
+        if bytes.is_empty() {
+            Ok(ChangelogRevisionData::null())
+        } else {
+            Ok(ChangelogRevisionData::new(bytes).map_err(|err| {
+                RevlogError::Other(HgError::CorruptedRepository(format!(
+                    "Invalid changelog data for revision {}: {:?}",
+                    rev, err
+                )))
+            })?)
+        }
     }
 
     pub fn node_from_rev(&self, rev: Revision) -> Option<&Node> {
         self.revlog.node_from_rev(rev)
     }
+
+    pub fn rev_from_node(
+        &self,
+        node: NodePrefix,
+    ) -> Result<Revision, RevlogError> {
+        self.revlog.rev_from_node(node)
+    }
 }
 
 /// `Changelog` entry which knows how to interpret the `changelog` data bytes.
-#[derive(Debug)]
-pub struct ChangelogRevisionData {
+#[derive(PartialEq)]
+pub struct ChangelogRevisionData<'changelog> {
     /// The data bytes of the `changelog` entry.
-    bytes: Vec<u8>,
+    bytes: Cow<'changelog, [u8]>,
+    /// The end offset for the hex manifest (not including the newline)
+    manifest_end: usize,
+    /// The end offset for the user+email (not including the newline)
+    user_end: usize,
+    /// The end offset for the timestamp+timezone+extras (not including the
+    /// newline)
+    timestamp_end: usize,
+    /// The end offset for the file list (not including the newline)
+    files_end: usize,
 }
 
-impl ChangelogRevisionData {
+impl<'changelog> ChangelogRevisionData<'changelog> {
+    fn new(bytes: Cow<'changelog, [u8]>) -> Result<Self, HgError> {
+        let mut line_iter = bytes.split(|b| b == &b'\n');
+        let manifest_end = line_iter
+            .next()
+            .expect("Empty iterator from split()?")
+            .len();
+        let user_slice = line_iter.next().ok_or_else(|| {
+            HgError::corrupted("Changeset data truncated after manifest line")
+        })?;
+        let user_end = manifest_end + 1 + user_slice.len();
+        let timestamp_slice = line_iter.next().ok_or_else(|| {
+            HgError::corrupted("Changeset data truncated after user line")
+        })?;
+        let timestamp_end = user_end + 1 + timestamp_slice.len();
+        let mut files_end = timestamp_end + 1;
+        loop {
+            let line = line_iter.next().ok_or_else(|| {
+                HgError::corrupted("Changeset data truncated in files list")
+            })?;
+            if line.is_empty() {
+                if files_end == bytes.len() {
+                    // The list of files ended with a single newline (there
+                    // should be two)
+                    return Err(HgError::corrupted(
+                        "Changeset data truncated after files list",
+                    ));
+                }
+                files_end -= 1;
+                break;
+            }
+            files_end += line.len() + 1;
+        }
+
+        Ok(Self {
+            bytes,
+            manifest_end,
+            user_end,
+            timestamp_end,
+            files_end,
+        })
+    }
+
+    fn null() -> Self {
+        Self::new(Cow::Borrowed(
+            b"0000000000000000000000000000000000000000\n\n0 0\n\n",
+        ))
+        .unwrap()
+    }
+
     /// Return an iterator over the lines of the entry.
     pub fn lines(&self) -> impl Iterator<Item = &[u8]> {
-        self.bytes
-            .split(|b| b == &b'\n')
-            .filter(|line| !line.is_empty())
+        self.bytes.split(|b| b == &b'\n')
     }
 
     /// Return the node id of the `manifest` referenced by this `changelog`
     /// entry.
     pub fn manifest_node(&self) -> Result<Node, HgError> {
-        match self.lines().next() {
-            None => Ok(NULL_NODE),
-            Some(x) => Node::from_hex_for_repo(x),
-        }
+        let manifest_node_hex = &self.bytes[..self.manifest_end];
+        Node::from_hex_for_repo(manifest_node_hex)
+    }
+
+    /// The full user string (usually a name followed by an email enclosed in
+    /// angle brackets)
+    pub fn user(&self) -> &[u8] {
+        &self.bytes[self.manifest_end + 1..self.user_end]
+    }
+
+    /// The full timestamp line (timestamp in seconds, offset in seconds, and
+    /// possibly extras)
+    // TODO: We should expose this in a more useful way
+    pub fn timestamp_line(&self) -> &[u8] {
+        &self.bytes[self.user_end + 1..self.timestamp_end]
+    }
+
+    /// The files changed in this revision.
+    pub fn files(&self) -> impl Iterator<Item = &HgPath> {
+        self.bytes[self.timestamp_end + 1..self.files_end]
+            .split(|b| b == &b'\n')
+            .map(|path| HgPath::new(path))
+    }
+
+    /// The change description.
+    pub fn description(&self) -> &[u8] {
+        &self.bytes[self.files_end + 2..]
+    }
+}
+
+impl Debug for ChangelogRevisionData<'_> {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("ChangelogRevisionData")
+            .field("bytes", &debug_bytes(&self.bytes))
+            .field("manifest", &debug_bytes(&self.bytes[..self.manifest_end]))
+            .field(
+                "user",
+                &debug_bytes(
+                    &self.bytes[self.manifest_end + 1..self.user_end],
+                ),
+            )
+            .field(
+                "timestamp",
+                &debug_bytes(
+                    &self.bytes[self.user_end + 1..self.timestamp_end],
+                ),
+            )
+            .field(
+                "files",
+                &debug_bytes(
+                    &self.bytes[self.timestamp_end + 1..self.files_end],
+                ),
+            )
+            .field(
+                "description",
+                &debug_bytes(&self.bytes[self.files_end + 2..]),
+            )
+            .finish()
     }
 }
+
+fn debug_bytes(bytes: &[u8]) -> String {
+    String::from_utf8_lossy(
+        &bytes.iter().flat_map(|b| escape_default(*b)).collect_vec(),
+    )
+    .to_string()
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use pretty_assertions::assert_eq;
+
+    #[test]
+    fn test_create_changelogrevisiondata_invalid() {
+        // Completely empty
+        assert!(ChangelogRevisionData::new(Cow::Borrowed(b"abcd")).is_err());
+        // No newline after manifest
+        assert!(ChangelogRevisionData::new(Cow::Borrowed(b"abcd")).is_err());
+        // No newline after user
+        assert!(ChangelogRevisionData::new(Cow::Borrowed(b"abcd\n")).is_err());
+        // No newline after timestamp
+        assert!(
+            ChangelogRevisionData::new(Cow::Borrowed(b"abcd\n\n0 0")).is_err()
+        );
+        // Missing newline after files
+        assert!(ChangelogRevisionData::new(Cow::Borrowed(
+            b"abcd\n\n0 0\nfile1\nfile2"
+        ))
+        .is_err(),);
+        // Only one newline after files
+        assert!(ChangelogRevisionData::new(Cow::Borrowed(
+            b"abcd\n\n0 0\nfile1\nfile2\n"
+        ))
+        .is_err(),);
+    }
+
+    #[test]
+    fn test_create_changelogrevisiondata() {
+        let data = ChangelogRevisionData::new(Cow::Borrowed(
+            b"0123456789abcdef0123456789abcdef01234567
+Some One <someone@example.com>
+0 0
+file1
+file2
+
+some
+commit
+message",
+        ))
+        .unwrap();
+        assert_eq!(
+            data.manifest_node().unwrap(),
+            Node::from_hex("0123456789abcdef0123456789abcdef01234567")
+                .unwrap()
+        );
+        assert_eq!(data.user(), b"Some One <someone@example.com>");
+        assert_eq!(data.timestamp_line(), b"0 0");
+        assert_eq!(
+            data.files().collect_vec(),
+            vec![HgPath::new("file1"), HgPath::new("file2")]
+        );
+        assert_eq!(data.description(), b"some\ncommit\nmessage");
+    }
+}
--- a/rust/hg-core/src/revlog/filelog.rs	Wed May 04 18:00:01 2022 +0200
+++ b/rust/hg-core/src/revlog/filelog.rs	Wed May 04 18:17:44 2022 +0200
@@ -20,7 +20,12 @@
     pub fn open(repo: &Repo, file_path: &HgPath) -> Result<Self, HgError> {
         let index_path = store_path(file_path, b".i");
         let data_path = store_path(file_path, b".d");
-        let revlog = Revlog::open(repo, index_path, Some(&data_path))?;
+        let revlog = Revlog::open(
+            &repo.store_vfs(),
+            index_path,
+            Some(&data_path),
+            false,
+        )?;
         Ok(Self { revlog })
     }
 
--- a/rust/hg-core/src/revlog/index.rs	Wed May 04 18:00:01 2022 +0200
+++ b/rust/hg-core/src/revlog/index.rs	Wed May 04 18:17:44 2022 +0200
@@ -282,6 +282,10 @@
         BigEndian::read_i32(&self.bytes[16..])
     }
 
+    pub fn link_revision(&self) -> Revision {
+        BigEndian::read_i32(&self.bytes[20..])
+    }
+
     pub fn p1(&self) -> Revision {
         BigEndian::read_i32(&self.bytes[24..])
     }
@@ -302,6 +306,7 @@
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::node::NULL_NODE;
 
     #[cfg(test)]
     #[derive(Debug, Copy, Clone)]
@@ -314,6 +319,10 @@
         compressed_len: usize,
         uncompressed_len: usize,
         base_revision_or_base_of_delta_chain: Revision,
+        link_revision: Revision,
+        p1: Revision,
+        p2: Revision,
+        node: Node,
     }
 
     #[cfg(test)]
@@ -323,11 +332,15 @@
                 is_first: false,
                 is_inline: false,
                 is_general_delta: true,
-                version: 2,
+                version: 1,
                 offset: 0,
                 compressed_len: 0,
                 uncompressed_len: 0,
                 base_revision_or_base_of_delta_chain: 0,
+                link_revision: 0,
+                p1: NULL_REVISION,
+                p2: NULL_REVISION,
+                node: NULL_NODE,
             }
         }
 
@@ -374,6 +387,26 @@
             self
         }
 
+        pub fn with_link_revision(&mut self, value: Revision) -> &mut Self {
+            self.link_revision = value;
+            self
+        }
+
+        pub fn with_p1(&mut self, value: Revision) -> &mut Self {
+            self.p1 = value;
+            self
+        }
+
+        pub fn with_p2(&mut self, value: Revision) -> &mut Self {
+            self.p2 = value;
+            self
+        }
+
+        pub fn with_node(&mut self, value: Node) -> &mut Self {
+            self.node = value;
+            self
+        }
+
         pub fn build(&self) -> Vec<u8> {
             let mut bytes = Vec::with_capacity(INDEX_ENTRY_SIZE);
             if self.is_first {
@@ -396,6 +429,11 @@
             bytes.extend(
                 &self.base_revision_or_base_of_delta_chain.to_be_bytes(),
             );
+            bytes.extend(&self.link_revision.to_be_bytes());
+            bytes.extend(&self.p1.to_be_bytes());
+            bytes.extend(&self.p2.to_be_bytes());
+            bytes.extend(self.node.as_bytes());
+            bytes.extend(vec![0u8; 12]);
             bytes
         }
     }
@@ -514,13 +552,63 @@
     }
 
     #[test]
+    fn link_revision_test() {
+        let bytes = IndexEntryBuilder::new().with_link_revision(123).build();
+
+        let entry = IndexEntry {
+            bytes: &bytes,
+            offset_override: None,
+        };
+
+        assert_eq!(entry.link_revision(), 123);
+    }
+
+    #[test]
+    fn p1_test() {
+        let bytes = IndexEntryBuilder::new().with_p1(123).build();
+
+        let entry = IndexEntry {
+            bytes: &bytes,
+            offset_override: None,
+        };
+
+        assert_eq!(entry.p1(), 123);
+    }
+
+    #[test]
+    fn p2_test() {
+        let bytes = IndexEntryBuilder::new().with_p2(123).build();
+
+        let entry = IndexEntry {
+            bytes: &bytes,
+            offset_override: None,
+        };
+
+        assert_eq!(entry.p2(), 123);
+    }
+
+    #[test]
+    fn node_test() {
+        let node = Node::from_hex("0123456789012345678901234567890123456789")
+            .unwrap();
+        let bytes = IndexEntryBuilder::new().with_node(node).build();
+
+        let entry = IndexEntry {
+            bytes: &bytes,
+            offset_override: None,
+        };
+
+        assert_eq!(*entry.hash(), node);
+    }
+
+    #[test]
     fn version_test() {
         let bytes = IndexEntryBuilder::new()
             .is_first(true)
-            .with_version(1)
+            .with_version(2)
             .build();
 
-        assert_eq!(get_version(&bytes), 1)
+        assert_eq!(get_version(&bytes), 2)
     }
 }
 
--- a/rust/hg-core/src/revlog/manifest.rs	Wed May 04 18:00:01 2022 +0200
+++ b/rust/hg-core/src/revlog/manifest.rs	Wed May 04 18:17:44 2022 +0200
@@ -1,10 +1,10 @@
 use crate::errors::HgError;
-use crate::repo::Repo;
 use crate::revlog::revlog::{Revlog, RevlogError};
 use crate::revlog::Revision;
 use crate::revlog::{Node, NodePrefix};
 use crate::utils::hg_path::HgPath;
 use crate::utils::SliceExt;
+use crate::vfs::Vfs;
 
 /// A specialized `Revlog` to work with `manifest` data format.
 pub struct Manifestlog {
@@ -14,8 +14,9 @@
 
 impl Manifestlog {
     /// Open the `manifest` of a repository given by its root.
-    pub fn open(repo: &Repo) -> Result<Self, HgError> {
-        let revlog = Revlog::open(repo, "00manifest.i", None)?;
+    pub fn open(store_vfs: &Vfs, use_nodemap: bool) -> Result<Self, HgError> {
+        let revlog =
+            Revlog::open(store_vfs, "00manifest.i", None, use_nodemap)?;
         Ok(Self { revlog })
     }
 
--- a/rust/hg-core/src/revlog/nodemap.rs	Wed May 04 18:00:01 2022 +0200
+++ b/rust/hg-core/src/revlog/nodemap.rs	Wed May 04 18:17:44 2022 +0200
@@ -403,7 +403,7 @@
         Err(NodeMapError::MultipleResults)
     }
 
-    fn visit<'n>(&'n self, prefix: NodePrefix) -> NodeTreeVisitor<'n> {
+    fn visit(&self, prefix: NodePrefix) -> NodeTreeVisitor {
         NodeTreeVisitor {
             nt: self,
             prefix,
--- a/rust/hg-core/src/revlog/nodemap_docket.rs	Wed May 04 18:00:01 2022 +0200
+++ b/rust/hg-core/src/revlog/nodemap_docket.rs	Wed May 04 18:17:44 2022 +0200
@@ -1,11 +1,10 @@
 use crate::errors::{HgError, HgResultExt};
-use crate::requirements;
 use bytes_cast::{unaligned, BytesCast};
 use memmap2::Mmap;
 use std::path::{Path, PathBuf};
 
-use crate::repo::Repo;
 use crate::utils::strip_suffix;
+use crate::vfs::Vfs;
 
 const ONDISK_VERSION: u8 = 1;
 
@@ -35,20 +34,12 @@
     /// * The docket file points to a missing (likely deleted) data file (this
     ///   can happen in a rare race condition).
     pub fn read_from_file(
-        repo: &Repo,
+        store_vfs: &Vfs,
         index_path: &Path,
     ) -> Result<Option<(Self, Mmap)>, HgError> {
-        if !repo
-            .requirements()
-            .contains(requirements::NODEMAP_REQUIREMENT)
-        {
-            // If .hg/requires does not opt it, don’t try to open a nodemap
-            return Ok(None);
-        }
-
         let docket_path = index_path.with_extension("n");
         let docket_bytes = if let Some(bytes) =
-            repo.store_vfs().read(&docket_path).io_not_found_as_none()?
+            store_vfs.read(&docket_path).io_not_found_as_none()?
         {
             bytes
         } else {
@@ -84,10 +75,8 @@
         let data_path = rawdata_path(&docket_path, uid);
         // TODO: use `vfs.read()` here when the `persistent-nodemap.mmap`
         // config is false?
-        if let Some(mmap) = repo
-            .store_vfs()
-            .mmap_open(&data_path)
-            .io_not_found_as_none()?
+        if let Some(mmap) =
+            store_vfs.mmap_open(&data_path).io_not_found_as_none()?
         {
             if mmap.len() >= data_length {
                 Ok(Some((docket, mmap)))
--- a/rust/hg-core/src/revlog/revlog.rs	Wed May 04 18:00:01 2022 +0200
+++ b/rust/hg-core/src/revlog/revlog.rs	Wed May 04 18:17:44 2022 +0200
@@ -16,8 +16,8 @@
 use super::nodemap_docket::NodeMapDocket;
 use super::patch;
 use crate::errors::HgError;
-use crate::repo::Repo;
 use crate::revlog::Revision;
+use crate::vfs::Vfs;
 use crate::{Node, NULL_REVISION};
 
 const REVISION_FLAG_CENSORED: u16 = 1 << 15;
@@ -32,7 +32,7 @@
     | REVISION_FLAG_EXTSTORED
     | REVISION_FLAG_HASCOPIESINFO;
 
-#[derive(derive_more::From)]
+#[derive(Debug, derive_more::From)]
 pub enum RevlogError {
     InvalidRevision,
     /// Working directory is not supported
@@ -81,13 +81,14 @@
     /// interleaved.
     #[timed]
     pub fn open(
-        repo: &Repo,
+        store_vfs: &Vfs,
         index_path: impl AsRef<Path>,
         data_path: Option<&Path>,
+        use_nodemap: bool,
     ) -> Result<Self, HgError> {
         let index_path = index_path.as_ref();
         let index = {
-            match repo.store_vfs().mmap_open_opt(&index_path)? {
+            match store_vfs.mmap_open_opt(&index_path)? {
                 None => Index::new(Box::new(vec![])),
                 Some(index_mmap) => {
                     let index = Index::new(Box::new(index_mmap))?;
@@ -105,14 +106,16 @@
                 None
             } else {
                 let data_path = data_path.unwrap_or(&default_data_path);
-                let data_mmap = repo.store_vfs().mmap_open(data_path)?;
+                let data_mmap = store_vfs.mmap_open(data_path)?;
                 Some(Box::new(data_mmap))
             };
 
         let nodemap = if index.is_inline() {
             None
+        } else if !use_nodemap {
+            None
         } else {
-            NodeMapDocket::read_from_file(repo, index_path)?.map(
+            NodeMapDocket::read_from_file(store_vfs, index_path)?.map(
                 |(docket, data)| {
                     nodemap::NodeTree::load_bytes(
                         Box::new(data),
@@ -331,6 +334,10 @@
         self.rev
     }
 
+    pub fn node(&self) -> &Node {
+        &self.hash
+    }
+
     pub fn uncompressed_len(&self) -> Option<u32> {
         u32::try_from(self.uncompressed_len).ok()
     }
@@ -339,6 +346,38 @@
         self.p1 != NULL_REVISION
     }
 
+    pub fn p1_entry(&self) -> Result<Option<RevlogEntry>, RevlogError> {
+        if self.p1 == NULL_REVISION {
+            Ok(None)
+        } else {
+            Ok(Some(self.revlog.get_entry(self.p1)?))
+        }
+    }
+
+    pub fn p2_entry(&self) -> Result<Option<RevlogEntry>, RevlogError> {
+        if self.p2 == NULL_REVISION {
+            Ok(None)
+        } else {
+            Ok(Some(self.revlog.get_entry(self.p2)?))
+        }
+    }
+
+    pub fn p1(&self) -> Option<Revision> {
+        if self.p1 == NULL_REVISION {
+            None
+        } else {
+            Some(self.p1)
+        }
+    }
+
+    pub fn p2(&self) -> Option<Revision> {
+        if self.p2 == NULL_REVISION {
+            None
+        } else {
+            Some(self.p2)
+        }
+    }
+
     pub fn is_cencored(&self) -> bool {
         (self.flags & REVISION_FLAG_CENSORED) != 0
     }
@@ -466,3 +505,92 @@
     hasher.update(data);
     *hasher.finalize().as_ref()
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::index::{IndexEntryBuilder, INDEX_ENTRY_SIZE};
+    use itertools::Itertools;
+
+    #[test]
+    fn test_empty() {
+        let temp = tempfile::tempdir().unwrap();
+        let vfs = Vfs { base: temp.path() };
+        std::fs::write(temp.path().join("foo.i"), b"").unwrap();
+        let revlog = Revlog::open(&vfs, "foo.i", None, false).unwrap();
+        assert!(revlog.is_empty());
+        assert_eq!(revlog.len(), 0);
+        assert!(revlog.get_entry(0).is_err());
+        assert!(!revlog.has_rev(0));
+    }
+
+    #[test]
+    fn test_inline() {
+        let temp = tempfile::tempdir().unwrap();
+        let vfs = Vfs { base: temp.path() };
+        let node0 = Node::from_hex("2ed2a3912a0b24502043eae84ee4b279c18b90dd")
+            .unwrap();
+        let node1 = Node::from_hex("b004912a8510032a0350a74daa2803dadfb00e12")
+            .unwrap();
+        let node2 = Node::from_hex("dd6ad206e907be60927b5a3117b97dffb2590582")
+            .unwrap();
+        let entry0_bytes = IndexEntryBuilder::new()
+            .is_first(true)
+            .with_version(1)
+            .with_inline(true)
+            .with_offset(INDEX_ENTRY_SIZE)
+            .with_node(node0)
+            .build();
+        let entry1_bytes = IndexEntryBuilder::new()
+            .with_offset(INDEX_ENTRY_SIZE)
+            .with_node(node1)
+            .build();
+        let entry2_bytes = IndexEntryBuilder::new()
+            .with_offset(INDEX_ENTRY_SIZE)
+            .with_p1(0)
+            .with_p2(1)
+            .with_node(node2)
+            .build();
+        let contents = vec![entry0_bytes, entry1_bytes, entry2_bytes]
+            .into_iter()
+            .flatten()
+            .collect_vec();
+        std::fs::write(temp.path().join("foo.i"), contents).unwrap();
+        let revlog = Revlog::open(&vfs, "foo.i", None, false).unwrap();
+
+        let entry0 = revlog.get_entry(0).ok().unwrap();
+        assert_eq!(entry0.revision(), 0);
+        assert_eq!(*entry0.node(), node0);
+        assert!(!entry0.has_p1());
+        assert_eq!(entry0.p1(), None);
+        assert_eq!(entry0.p2(), None);
+        let p1_entry = entry0.p1_entry().unwrap();
+        assert!(p1_entry.is_none());
+        let p2_entry = entry0.p2_entry().unwrap();
+        assert!(p2_entry.is_none());
+
+        let entry1 = revlog.get_entry(1).ok().unwrap();
+        assert_eq!(entry1.revision(), 1);
+        assert_eq!(*entry1.node(), node1);
+        assert!(!entry1.has_p1());
+        assert_eq!(entry1.p1(), None);
+        assert_eq!(entry1.p2(), None);
+        let p1_entry = entry1.p1_entry().unwrap();
+        assert!(p1_entry.is_none());
+        let p2_entry = entry1.p2_entry().unwrap();
+        assert!(p2_entry.is_none());
+
+        let entry2 = revlog.get_entry(2).ok().unwrap();
+        assert_eq!(entry2.revision(), 2);
+        assert_eq!(*entry2.node(), node2);
+        assert!(entry2.has_p1());
+        assert_eq!(entry2.p1(), Some(0));
+        assert_eq!(entry2.p2(), Some(1));
+        let p1_entry = entry2.p1_entry().unwrap();
+        assert!(p1_entry.is_some());
+        assert_eq!(p1_entry.unwrap().revision(), 0);
+        let p2_entry = entry2.p2_entry().unwrap();
+        assert!(p2_entry.is_some());
+        assert_eq!(p2_entry.unwrap().revision(), 1);
+    }
+}
--- a/rust/hg-cpython/Cargo.toml	Wed May 04 18:00:01 2022 +0200
+++ b/rust/hg-cpython/Cargo.toml	Wed May 04 18:17:44 2022 +0200
@@ -8,25 +8,12 @@
 name='rusthg'
 crate-type = ["cdylib"]
 
-[features]
-default = ["python3"]
-
-# Features to build an extension module:
-python27 = ["cpython/python27-sys", "cpython/extension-module-2-7"]
-python3 = ["cpython/python3-sys", "cpython/extension-module"]
-
-# Enable one of these features to build a test executable linked to libpython:
-# e.g. cargo test --no-default-features --features python27-bin
-python27-bin = ["cpython/python27-sys"]
-python3-bin = ["cpython/python3-sys"]
-
 [dependencies]
-cpython = { version = "0.7.0", default-features = false }
-crossbeam-channel = "0.4"
+cpython = { version = "0.7.0", features = ["extension-module"] }
+crossbeam-channel = "0.5.2"
 hg-core = { path = "../hg-core"}
-libc = "0.2"
-log = "0.4.8"
-env_logger = "0.7.1"
+libc = "0.2.119"
+log = "0.4.14"
+env_logger = "0.9.0"
 stable_deref_trait = "1.2.0"
 vcsgraph = "0.2.0"
-
--- a/rust/hg-cpython/src/cindex.rs	Wed May 04 18:00:01 2022 +0200
+++ b/rust/hg-cpython/src/cindex.rs	Wed May 04 18:17:44 2022 +0200
@@ -18,7 +18,7 @@
 use hg::{Graph, GraphError, Revision, WORKING_DIRECTORY_REVISION};
 use libc::{c_int, ssize_t};
 
-const REVLOG_CABI_VERSION: c_int = 2;
+const REVLOG_CABI_VERSION: c_int = 3;
 
 #[repr(C)]
 pub struct Revlog_CAPI {
@@ -29,6 +29,10 @@
         index: *mut revlog_capi::RawPyObject,
         rev: ssize_t,
     ) -> *const Node,
+    fast_rank: unsafe extern "C" fn(
+        index: *mut revlog_capi::RawPyObject,
+        rev: ssize_t,
+    ) -> ssize_t,
     index_parents: unsafe extern "C" fn(
         index: *mut revlog_capi::RawPyObject,
         rev: c_int,
@@ -173,6 +177,20 @@
     }
 }
 
+impl vcsgraph::graph::RankedGraph for Index {
+    fn rank(
+        &self,
+        rev: Revision,
+    ) -> Result<vcsgraph::graph::Rank, vcsgraph::graph::GraphReadError> {
+        match unsafe {
+            (self.capi.fast_rank)(self.index.as_ptr(), rev as ssize_t)
+        } {
+            -1 => Err(vcsgraph::graph::GraphReadError::InconsistentGraphData),
+            rank => Ok(rank as usize),
+        }
+    }
+}
+
 impl RevlogIndex for Index {
     /// Note C return type is Py_ssize_t (hence signed), but we shall
     /// force it to unsigned, because it's a length
--- a/rust/hg-cpython/src/dagops.rs	Wed May 04 18:00:01 2022 +0200
+++ b/rust/hg-cpython/src/dagops.rs	Wed May 04 18:17:44 2022 +0200
@@ -14,6 +14,8 @@
 use hg::dagops;
 use hg::Revision;
 use std::collections::HashSet;
+use vcsgraph::ancestors::node_rank;
+use vcsgraph::graph::{Parents, Rank};
 
 use crate::revlog::pyindex_to_graph;
 
@@ -31,6 +33,18 @@
     Ok(as_set)
 }
 
+/// Computes the rank, i.e. the number of ancestors including itself,
+/// of a node represented by its parents.
+pub fn rank(
+    py: Python,
+    index: PyObject,
+    p1r: Revision,
+    p2r: Revision,
+) -> PyResult<Rank> {
+    node_rank(&pyindex_to_graph(py, index)?, &Parents([p1r, p2r]))
+        .map_err(|e| GraphError::pynew_from_vcsgraph(py, e))
+}
+
 /// Create the module, with `__package__` given from parent
 pub fn init_module(py: Python, package: &str) -> PyResult<PyModule> {
     let dotted_name = &format!("{}.dagop", package);
@@ -42,6 +56,11 @@
         "headrevs",
         py_fn!(py, headrevs(index: PyObject, revs: PyObject)),
     )?;
+    m.add(
+        py,
+        "rank",
+        py_fn!(py, rank(index: PyObject, p1r: Revision, p2r: Revision)),
+    )?;
 
     let sys = PyModule::import(py, "sys")?;
     let sys_modules: PyDict = sys.get(py, "modules")?.extract(py)?;
--- a/rust/hg-cpython/src/dirstate/dirstate_map.rs	Wed May 04 18:00:01 2022 +0200
+++ b/rust/hg-cpython/src/dirstate/dirstate_map.rs	Wed May 04 18:17:44 2022 +0200
@@ -15,6 +15,7 @@
     exc, PyBool, PyBytes, PyClone, PyDict, PyErr, PyList, PyNone, PyObject,
     PyResult, Python, PythonObject, ToPyObject, UnsafePyLeaked,
 };
+use hg::dirstate::{ParentFileData, TruncatedTimestamp};
 
 use crate::{
     dirstate::copymap::{CopyMap, CopyMapItemsIterator, CopyMapKeysIterator},
@@ -22,13 +23,10 @@
     pybytes_deref::PyBytesDeref,
 };
 use hg::{
-    dirstate::StateMapIter,
-    dirstate_tree::on_disk::DirstateV2ParseError,
-    dirstate_tree::owning::OwningDirstateMap,
-    revlog::Node,
-    utils::files::normalize_case,
-    utils::hg_path::{HgPath, HgPathBuf},
-    DirstateEntry, DirstateError, DirstateParents, EntryState,
+    dirstate::StateMapIter, dirstate_tree::on_disk::DirstateV2ParseError,
+    dirstate_tree::owning::OwningDirstateMap, revlog::Node,
+    utils::files::normalize_case, utils::hg_path::HgPath, DirstateEntry,
+    DirstateError, DirstateParents,
 };
 
 // TODO
@@ -103,61 +101,104 @@
         }
     }
 
-    def set_dirstate_item(
-        &self,
-        path: PyObject,
-        item: DirstateItem
-    ) -> PyResult<PyObject> {
-        let f = path.extract::<PyBytes>(py)?;
-        let filename = HgPath::new(f.data(py));
-        self.inner(py)
-            .borrow_mut()
-            .set_entry(filename, item.get_entry(py))
-            .map_err(|e| v2_error(py, e))?;
-        Ok(py.None())
+    def set_tracked(&self, f: PyObject) -> PyResult<PyBool> {
+        let bytes = f.extract::<PyBytes>(py)?;
+        let path = HgPath::new(bytes.data(py));
+        let res = self.inner(py).borrow_mut().set_tracked(path);
+        let was_tracked = res.or_else(|_| {
+            Err(PyErr::new::<exc::OSError, _>(py, "Dirstate error".to_string()))
+        })?;
+        Ok(was_tracked.to_py_object(py))
+    }
+
+    def set_untracked(&self, f: PyObject) -> PyResult<PyBool> {
+        let bytes = f.extract::<PyBytes>(py)?;
+        let path = HgPath::new(bytes.data(py));
+        let res = self.inner(py).borrow_mut().set_untracked(path);
+        let was_tracked = res.or_else(|_| {
+            Err(PyErr::new::<exc::OSError, _>(py, "Dirstate error".to_string()))
+        })?;
+        Ok(was_tracked.to_py_object(py))
     }
 
-    def addfile(
+    def set_clean(
         &self,
-        f: PyBytes,
-        item: DirstateItem,
+        f: PyObject,
+        mode: u32,
+        size: u32,
+        mtime: (i64, u32, bool)
     ) -> PyResult<PyNone> {
-        let filename = HgPath::new(f.data(py));
-        let entry = item.get_entry(py);
-        self.inner(py)
-            .borrow_mut()
-            .add_file(filename, entry)
-            .map_err(|e |dirstate_error(py, e))?;
+        let (mtime_s, mtime_ns, second_ambiguous) = mtime;
+        let timestamp = TruncatedTimestamp::new_truncate(
+            mtime_s, mtime_ns, second_ambiguous
+        );
+        let bytes = f.extract::<PyBytes>(py)?;
+        let path = HgPath::new(bytes.data(py));
+        let res = self.inner(py).borrow_mut().set_clean(
+            path, mode, size, timestamp,
+        );
+        res.or_else(|_| {
+            Err(PyErr::new::<exc::OSError, _>(py, "Dirstate error".to_string()))
+        })?;
         Ok(PyNone)
     }
 
-    def removefile(
+    def set_possibly_dirty(&self, f: PyObject) -> PyResult<PyNone> {
+        let bytes = f.extract::<PyBytes>(py)?;
+        let path = HgPath::new(bytes.data(py));
+        let res = self.inner(py).borrow_mut().set_possibly_dirty(path);
+        res.or_else(|_| {
+            Err(PyErr::new::<exc::OSError, _>(py, "Dirstate error".to_string()))
+        })?;
+        Ok(PyNone)
+    }
+
+    def reset_state(
         &self,
         f: PyObject,
-        in_merge: PyObject
-    ) -> PyResult<PyObject> {
-        self.inner(py).borrow_mut()
-            .remove_file(
-                HgPath::new(f.extract::<PyBytes>(py)?.data(py)),
-                in_merge.extract::<PyBool>(py)?.is_true(),
-            )
-            .or_else(|_| {
-                Err(PyErr::new::<exc::OSError, _>(
-                    py,
-                    "Dirstate error".to_string(),
-                ))
-            })?;
-        Ok(py.None())
-    }
-
-    def drop_item_and_copy_source(
-        &self,
-        f: PyBytes,
+        wc_tracked: bool,
+        p1_tracked: bool,
+        p2_info: bool,
+        has_meaningful_mtime: bool,
+        parentfiledata: Option<(u32, u32, Option<(i64, u32, bool)>)>,
     ) -> PyResult<PyNone> {
-        self.inner(py)
-            .borrow_mut()
-            .drop_entry_and_copy_source(HgPath::new(f.data(py)))
-            .map_err(|e |dirstate_error(py, e))?;
+        let mut has_meaningful_mtime = has_meaningful_mtime;
+        let parent_file_data = match parentfiledata {
+            None => {
+                has_meaningful_mtime = false;
+                None
+            },
+            Some(data) => {
+                let (mode, size, mtime_info) = data;
+                let mtime = if let Some(mtime_info) = mtime_info {
+                    let (mtime_s, mtime_ns, second_ambiguous) = mtime_info;
+                    let timestamp = TruncatedTimestamp::new_truncate(
+                        mtime_s, mtime_ns, second_ambiguous
+                    );
+                    Some(timestamp)
+                } else {
+                    has_meaningful_mtime = false;
+                    None
+                };
+                Some(ParentFileData {
+                    mode_size: Some((mode, size)),
+                    mtime,
+                })
+            }
+        };
+        let bytes = f.extract::<PyBytes>(py)?;
+        let path = HgPath::new(bytes.data(py));
+        let res = self.inner(py).borrow_mut().reset_state(
+            path,
+            wc_tracked,
+            p1_tracked,
+            p2_info,
+            has_meaningful_mtime,
+            parent_file_data,
+        );
+        res.or_else(|_| {
+            Err(PyErr::new::<exc::OSError, _>(py, "Dirstate error".to_string()))
+        })?;
         Ok(PyNone)
     }
 
@@ -228,7 +269,7 @@
         let dict = PyDict::new(py);
         for item in self.inner(py).borrow_mut().iter() {
             let (path, entry) = item.map_err(|e| v2_error(py, e))?;
-            if entry.state() != EntryState::Removed {
+            if !entry.removed() {
                 let key = normalize_case(path);
                 let value = path;
                 dict.set_item(
@@ -367,8 +408,8 @@
         self.inner(py)
             .borrow_mut()
             .copy_map_insert(
-                HgPathBuf::from_bytes(key.data(py)),
-                HgPathBuf::from_bytes(value.data(py)),
+                HgPath::new(key.data(py)),
+                HgPath::new(value.data(py)),
             )
             .map_err(|e| v2_error(py, e))?;
         Ok(py.None())
@@ -420,6 +461,19 @@
         Ok(dirs)
     }
 
+    def setparents_fixup(&self) -> PyResult<PyDict> {
+        let dict = PyDict::new(py);
+        let copies = self.inner(py).borrow_mut().setparents_fixup();
+        for (key, value) in copies.map_err(|e| v2_error(py, e))? {
+            dict.set_item(
+                py,
+                PyBytes::new(py, key.as_bytes()),
+                PyBytes::new(py, value.as_bytes()),
+            )?;
+        }
+        Ok(dict)
+    }
+
     def debug_iter(&self, all: bool) -> PyResult<PyList> {
         let dirs = PyList::new(py, &[]);
         for item in self.inner(py).borrow().debug_iter(all) {
--- a/rust/hg-cpython/src/dirstate/item.rs	Wed May 04 18:00:01 2022 +0200
+++ b/rust/hg-cpython/src/dirstate/item.rs	Wed May 04 18:17:44 2022 +0200
@@ -8,10 +8,9 @@
 use cpython::Python;
 use cpython::PythonObject;
 use hg::dirstate::DirstateEntry;
-use hg::dirstate::EntryState;
+use hg::dirstate::DirstateV2Data;
 use hg::dirstate::TruncatedTimestamp;
 use std::cell::Cell;
-use std::convert::TryFrom;
 
 py_class!(pub class DirstateItem |py| {
     data entry: Cell<DirstateEntry>;
@@ -40,15 +39,15 @@
                 }
             }
         }
-        let entry = DirstateEntry::from_v2_data(
-            wc_tracked,
+        let entry = DirstateEntry::from_v2_data(DirstateV2Data {
+            wc_tracked: wc_tracked,
             p1_tracked,
             p2_info,
-            mode_size_opt,
-            mtime_opt,
+            mode_size: mode_size_opt,
+            mtime: mtime_opt,
             fallback_exec,
             fallback_symlink,
-        );
+        });
         DirstateItem::create_instance(py, Cell::new(entry))
     }
 
@@ -173,27 +172,6 @@
         Ok(self.entry(py).get().any_tracked())
     }
 
-    def v1_state(&self) -> PyResult<PyBytes> {
-        let (state, _mode, _size, _mtime) = self.entry(py).get().v1_data();
-        let state_byte: u8 = state.into();
-        Ok(PyBytes::new(py, &[state_byte]))
-    }
-
-    def v1_mode(&self) -> PyResult<i32> {
-        let (_state, mode, _size, _mtime) = self.entry(py).get().v1_data();
-        Ok(mode)
-    }
-
-    def v1_size(&self) -> PyResult<i32> {
-        let (_state, _mode, size, _mtime) = self.entry(py).get().v1_data();
-        Ok(size)
-    }
-
-    def v1_mtime(&self) -> PyResult<i32> {
-        let (_state, _mode, _size, mtime) = self.entry(py).get().v1_data();
-        Ok(mtime)
-    }
-
     def mtime_likely_equal_to(&self, other: (u32, u32, bool))
         -> PyResult<bool> {
         if let Some(mtime) = self.entry(py).get().truncated_mtime() {
@@ -203,22 +181,6 @@
         }
     }
 
-    @classmethod
-    def from_v1_data(
-        _cls,
-        state: PyBytes,
-        mode: i32,
-        size: i32,
-        mtime: i32,
-    ) -> PyResult<Self> {
-        let state = <[u8; 1]>::try_from(state.data(py))
-            .ok()
-            .and_then(|state| EntryState::try_from(state[0]).ok())
-            .ok_or_else(|| PyErr::new::<exc::ValueError, _>(py, "invalid state"))?;
-        let entry = DirstateEntry::from_v1_data(state, mode, size, mtime);
-        DirstateItem::create_instance(py, Cell::new(entry))
-    }
-
     def drop_merge_data(&self) -> PyResult<PyNone> {
         self.update(py, |entry| entry.drop_merge_data());
         Ok(PyNone)
--- a/rust/hg-cpython/src/lib.rs	Wed May 04 18:00:01 2022 +0200
+++ b/rust/hg-cpython/src/lib.rs	Wed May 04 18:17:44 2022 +0200
@@ -62,7 +62,7 @@
     Ok(())
 });
 
-#[cfg(not(any(feature = "python27-bin", feature = "python3-bin")))]
+#[cfg(not(feature = "python3-bin"))]
 #[test]
 #[ignore]
 fn libpython_must_be_linked_to_run_tests() {
--- a/rust/rhg/Cargo.toml	Wed May 04 18:00:01 2022 +0200
+++ b/rust/rhg/Cargo.toml	Wed May 04 18:17:44 2022 +0200
@@ -8,17 +8,17 @@
 edition = "2018"
 
 [dependencies]
-atty = "0.2"
+atty = "0.2.14"
 hg-core = { path = "../hg-core"}
 chrono = "0.4.19"
-clap = "2.33.1"
-derive_more = "0.99"
+clap = "2.34.0"
+derive_more = "0.99.17"
 home = "0.5.3"
 lazy_static = "1.4.0"
-log = "0.4.11"
-micro-timer = "0.3.1"
-regex = "1.3.9"
-env_logger = "0.7.1"
+log = "0.4.14"
+micro-timer = "0.4.0"
+regex = "1.5.5"
+env_logger = "0.9.0"
 format-bytes = "0.3.0"
 users = "0.11.0"
 which = "4.2.5"
--- a/rust/rhg/src/blackbox.rs	Wed May 04 18:00:01 2022 +0200
+++ b/rust/rhg/src/blackbox.rs	Wed May 04 18:17:44 2022 +0200
@@ -5,6 +5,7 @@
 use hg::errors::HgError;
 use hg::repo::Repo;
 use hg::utils::{files::get_bytes_from_os_str, shell_quote};
+use std::ffi::OsString;
 
 const ONE_MEBIBYTE: u64 = 1 << 20;
 
@@ -83,14 +84,21 @@
         })
     }
 
-    pub fn log_command_start(&self) {
+    pub fn log_command_start<'arg>(
+        &self,
+        argv: impl Iterator<Item = &'arg OsString>,
+    ) {
         if let Some(configured) = &self.configured {
-            let message = format_bytes!(b"(rust) {}", format_cli_args());
+            let message = format_bytes!(b"(rust) {}", format_cli_args(argv));
             configured.log(&self.process_start_time.calendar_based, &message);
         }
     }
 
-    pub fn log_command_end(&self, exit_code: i32) {
+    pub fn log_command_end<'arg>(
+        &self,
+        argv: impl Iterator<Item = &'arg OsString>,
+        exit_code: i32,
+    ) {
         if let Some(configured) = &self.configured {
             let now = chrono::Local::now();
             let duration = self
@@ -100,7 +108,7 @@
                 .as_secs_f64();
             let message = format_bytes!(
                 b"(rust) {} exited {} after {} seconds",
-                format_cli_args(),
+                format_cli_args(argv),
                 exit_code,
                 format_bytes::Utf8(format_args!("{:.03}", duration))
             );
@@ -147,8 +155,9 @@
     }
 }
 
-fn format_cli_args() -> Vec<u8> {
-    let mut args = std::env::args_os();
+fn format_cli_args<'a>(
+    mut args: impl Iterator<Item = &'a OsString>,
+) -> Vec<u8> {
     let _ = args.next(); // Skip the first (or zeroth) arg, the name of the `rhg` executable
     let mut args = args.map(|arg| shell_quote(&get_bytes_from_os_str(arg)));
     let mut formatted = Vec::new();
--- a/rust/rhg/src/commands/status.rs	Wed May 04 18:00:01 2022 +0200
+++ b/rust/rhg/src/commands/status.rs	Wed May 04 18:17:44 2022 +0200
@@ -15,7 +15,6 @@
 use hg::dirstate::has_exec_bit;
 use hg::dirstate::status::StatusPath;
 use hg::dirstate::TruncatedTimestamp;
-use hg::dirstate::RANGE_MASK_31BIT;
 use hg::errors::{HgError, IoResultExt};
 use hg::lock::LockError;
 use hg::manifest::Manifest;
@@ -390,12 +389,8 @@
                         .when_reading_file(&fs_path)?
                     {
                         let mode = fs_metadata.mode();
-                        let size = fs_metadata.len() as u32 & RANGE_MASK_31BIT;
-                        let mut entry = dmap
-                            .get(&hg_path)?
-                            .expect("ambiguous file not in dirstate");
-                        entry.set_clean(mode, size, mtime);
-                        dmap.add_file(&hg_path, entry)?;
+                        let size = fs_metadata.len();
+                        dmap.set_clean(&hg_path, mode, size as u32, mtime)?;
                         dirstate_write_needed = true
                     }
                 }
--- a/rust/rhg/src/main.rs	Wed May 04 18:00:01 2022 +0200
+++ b/rust/rhg/src/main.rs	Wed May 04 18:17:44 2022 +0200
@@ -26,6 +26,7 @@
 }
 
 fn main_with_result(
+    argv: Vec<OsString>,
     process_start_time: &blackbox::ProcessStartTime,
     ui: &ui::Ui,
     repo: Result<&Repo, &NoRepoInCwdError>,
@@ -79,7 +80,7 @@
         .version("0.0.1");
     let app = add_subcommand_args(app);
 
-    let matches = app.clone().get_matches_safe()?;
+    let matches = app.clone().get_matches_from_safe(argv.iter())?;
 
     let (subcommand_name, subcommand_matches) = matches.subcommand();
 
@@ -124,23 +125,26 @@
     if config.is_extension_enabled(b"blackbox") {
         let blackbox =
             blackbox::Blackbox::new(&invocation, process_start_time)?;
-        blackbox.log_command_start();
+        blackbox.log_command_start(argv.iter());
         let result = run(&invocation);
-        blackbox.log_command_end(exit_code(
-            &result,
-            // TODO: show a warning or combine with original error if
-            // `get_bool` returns an error
-            config
-                .get_bool(b"ui", b"detailed-exit-code")
-                .unwrap_or(false),
-        ));
+        blackbox.log_command_end(
+            argv.iter(),
+            exit_code(
+                &result,
+                // TODO: show a warning or combine with original error if
+                // `get_bool` returns an error
+                config
+                    .get_bool(b"ui", b"detailed-exit-code")
+                    .unwrap_or(false),
+            ),
+        );
         result
     } else {
         run(&invocation)
     }
 }
 
-fn main() {
+fn rhg_main(argv: Vec<OsString>) -> ! {
     // Run this first, before we find out if the blackbox extension is even
     // enabled, in order to include everything in-between in the duration
     // measurements. Reading config files can be slow if they’re on NFS.
@@ -148,7 +152,7 @@
 
     env_logger::init();
 
-    let early_args = EarlyArgs::parse(std::env::args_os());
+    let early_args = EarlyArgs::parse(&argv);
 
     let initial_current_dir = early_args.cwd.map(|cwd| {
         let cwd = get_path_from_bytes(&cwd);
@@ -159,6 +163,7 @@
             })
             .unwrap_or_else(|error| {
                 exit(
+                    &argv,
                     &None,
                     &Ui::new_infallible(&Config::empty()),
                     OnUnsupported::Abort,
@@ -180,6 +185,7 @@
             let on_unsupported = OnUnsupported::Abort;
 
             exit(
+                &argv,
                 &initial_current_dir,
                 &Ui::new_infallible(&Config::empty()),
                 on_unsupported,
@@ -192,6 +198,7 @@
         .load_cli_args(early_args.config, early_args.color)
         .unwrap_or_else(|error| {
             exit(
+                &argv,
                 &initial_current_dir,
                 &Ui::new_infallible(&non_repo_config),
                 OnUnsupported::from_config(&non_repo_config),
@@ -210,6 +217,7 @@
         }
         if SCHEME_RE.is_match(&repo_path_bytes) {
             exit(
+                &argv,
                 &initial_current_dir,
                 &Ui::new_infallible(&non_repo_config),
                 OnUnsupported::from_config(&non_repo_config),
@@ -300,6 +308,7 @@
             Err(NoRepoInCwdError { cwd: at })
         }
         Err(error) => exit(
+            &argv,
             &initial_current_dir,
             &Ui::new_infallible(&non_repo_config),
             OnUnsupported::from_config(&non_repo_config),
@@ -319,6 +328,7 @@
     };
     let ui = Ui::new(&config).unwrap_or_else(|error| {
         exit(
+            &argv,
             &initial_current_dir,
             &Ui::new_infallible(&config),
             OnUnsupported::from_config(&config),
@@ -331,12 +341,14 @@
     let on_unsupported = OnUnsupported::from_config(config);
 
     let result = main_with_result(
+        argv.iter().map(|s| s.to_owned()).collect(),
         &process_start_time,
         &ui,
         repo_result.as_ref(),
         config,
     );
     exit(
+        &argv,
         &initial_current_dir,
         &ui,
         on_unsupported,
@@ -349,6 +361,10 @@
     )
 }
 
+fn main() -> ! {
+    rhg_main(std::env::args_os().collect())
+}
+
 fn exit_code(
     result: &Result<(), CommandError>,
     use_detailed_exit_code: bool,
@@ -377,7 +393,8 @@
     }
 }
 
-fn exit(
+fn exit<'a>(
+    original_args: &'a [OsString],
     initial_current_dir: &Option<PathBuf>,
     ui: &Ui,
     mut on_unsupported: OnUnsupported,
@@ -389,7 +406,7 @@
         Err(CommandError::UnsupportedFeature { message }),
     ) = (&on_unsupported, &result)
     {
-        let mut args = std::env::args_os();
+        let mut args = original_args.iter();
         let executable = match executable {
             None => {
                 exit_no_fallback(
@@ -567,7 +584,7 @@
 }
 
 impl EarlyArgs {
-    fn parse(args: impl IntoIterator<Item = OsString>) -> Self {
+    fn parse<'a>(args: impl IntoIterator<Item = &'a OsString>) -> Self {
         let mut args = args.into_iter().map(get_bytes_from_os_str);
         let mut config = Vec::new();
         let mut color = None;
@@ -664,6 +681,11 @@
     &[b"blackbox", b"share", b"sparse", b"narrow", b"*"];
 
 fn check_extensions(config: &Config) -> Result<(), CommandError> {
+    if let Some(b"*") = config.get(b"rhg", b"ignored-extensions") {
+        // All extensions are to be ignored, nothing to do here
+        return Ok(());
+    }
+
     let enabled: HashSet<&[u8]> = config
         .get_section_keys(b"extensions")
         .into_iter()
@@ -690,6 +712,9 @@
     if unsupported.is_empty() {
         Ok(())
     } else {
+        let mut unsupported: Vec<_> = unsupported.into_iter().collect();
+        // Sort the extensions to get a stable output
+        unsupported.sort();
         Err(CommandError::UnsupportedFeature {
             message: format_bytes!(
                 b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)",
--- a/setup.py	Wed May 04 18:00:01 2022 +0200
+++ b/setup.py	Wed May 04 18:17:44 2022 +0200
@@ -5,99 +5,24 @@
 # 'python setup.py --help' for more options
 import os
 
-# Mercurial will never work on Python 3 before 3.5 due to a lack
-# of % formatting on bytestrings, and can't work on 3.6.0 or 3.6.1
-# due to a bug in % formatting in bytestrings.
-# We cannot support Python 3.5.0, 3.5.1, 3.5.2 because of bug in
-# codecs.escape_encode() where it raises SystemError on empty bytestring
-# bug link: https://bugs.python.org/issue25270
+# Mercurial can't work on 3.6.0 or 3.6.1 due to a bug in % formatting
+# in bytestrings.
 supportedpy = ','.join(
     [
-        '>=2.7.4',
-        '!=3.0.*',
-        '!=3.1.*',
-        '!=3.2.*',
-        '!=3.3.*',
-        '!=3.4.*',
-        '!=3.5.0',
-        '!=3.5.1',
-        '!=3.5.2',
-        '!=3.6.0',
-        '!=3.6.1',
+        '>=3.6.2',
     ]
 )
 
 import sys, platform
 import sysconfig
 
-if sys.version_info[0] >= 3:
-    printf = eval('print')
-    libdir_escape = 'unicode_escape'
 
-    def sysstr(s):
-        return s.decode('latin-1')
-
-
-else:
-    libdir_escape = 'string_escape'
-
-    def printf(*args, **kwargs):
-        f = kwargs.get('file', sys.stdout)
-        end = kwargs.get('end', '\n')
-        f.write(b' '.join(args) + end)
-
-    def sysstr(s):
-        return s
+def sysstr(s):
+    return s.decode('latin-1')
 
 
-# Attempt to guide users to a modern pip - this means that 2.6 users
-# should have a chance of getting a 4.2 release, and when we ratchet
-# the version requirement forward again hopefully everyone will get
-# something that works for them.
-if sys.version_info < (2, 7, 4, 'final'):
-    pip_message = (
-        'This may be due to an out of date pip. '
-        'Make sure you have pip >= 9.0.1.'
-    )
-    try:
-        import pip
-
-        pip_version = tuple([int(x) for x in pip.__version__.split('.')[:3]])
-        if pip_version < (9, 0, 1):
-            pip_message = (
-                'Your pip version is out of date, please install '
-                'pip >= 9.0.1. pip {} detected.'.format(pip.__version__)
-            )
-        else:
-            # pip is new enough - it must be something else
-            pip_message = ''
-    except Exception:
-        pass
-    error = """
-Mercurial does not support Python older than 2.7.4.
-Python {py} detected.
-{pip}
-""".format(
-        py=sys.version_info, pip=pip_message
-    )
-    printf(error, file=sys.stderr)
-    sys.exit(1)
-
 import ssl
 
-try:
-    ssl.SSLContext
-except AttributeError:
-    error = """
-The `ssl` module does not have the `SSLContext` class. This indicates an old
-Python version which does not support modern security features (which were
-added to Python 2.7 as part of "PEP 466"). Please make sure you have installed
-at least Python 2.7.9 or a Python version with backports of these security
-features.
-"""
-    printf(error, file=sys.stderr)
-    sys.exit(1)
-
 # ssl.HAS_TLSv1* are preferred to check support but they were added in Python
 # 3.7. Prior to CPython commit 6e8cda91d92da72800d891b2fc2073ecbc134d98
 # (backported to the 3.7 branch), ssl.PROTOCOL_TLSv1_1 / ssl.PROTOCOL_TLSv1_2
@@ -117,14 +42,10 @@
 version enabling these features (likely this requires the OpenSSL version to
 be at least 1.0.1).
 """
-    printf(error, file=sys.stderr)
+    print(error, file=sys.stderr)
     sys.exit(1)
 
-if sys.version_info[0] >= 3:
-    DYLIB_SUFFIX = sysconfig.get_config_vars()['EXT_SUFFIX']
-else:
-    # deprecated in Python 3
-    DYLIB_SUFFIX = sysconfig.get_config_vars()['SO']
+DYLIB_SUFFIX = sysconfig.get_config_vars()['EXT_SUFFIX']
 
 # Solaris Python packaging brain damage
 try:
@@ -276,7 +197,7 @@
 try:
     import py2exe
 
-    py2exe.Distribution  # silence unused import warning
+    py2exe.patch_distutils()
     py2exeloaded = True
     # import py2exe's patched Distribution class
     from distutils.core import Distribution
@@ -292,7 +213,7 @@
     return p.returncode, out, err
 
 
-class hgcommand(object):
+class hgcommand:
     def __init__(self, cmd, env):
         self.cmd = cmd
         self.env = env
@@ -302,8 +223,8 @@
         returncode, out, err = runcmd(cmd, self.env)
         err = filterhgerr(err)
         if err or returncode != 0:
-            printf("stderr from '%s':" % (' '.join(cmd)), file=sys.stderr)
-            printf(err, file=sys.stderr)
+            print("stderr from '%s':" % (' '.join(cmd)), file=sys.stderr)
+            print(err, file=sys.stderr)
             return b''
         return out
 
@@ -536,7 +457,7 @@
             if hgrustext != 'cpython' and hgrustext is not None:
                 if hgrustext:
                     msg = 'unknown HGWITHRUSTEXT value: %s' % hgrustext
-                    printf(msg, file=sys.stderr)
+                    print(msg, file=sys.stderr)
                 hgrustext = None
             self.rust = hgrustext is not None
             self.no_rust = not self.rust
@@ -810,12 +731,9 @@
 
                 # Copy the pythonXY.dll next to the binary so that it runs
                 # without tampering with PATH.
-                fsdecode = lambda x: x
-                if sys.version_info[0] >= 3:
-                    fsdecode = os.fsdecode
                 dest = os.path.join(
                     os.path.dirname(self.hgtarget),
-                    fsdecode(dllbasename),
+                    os.fsdecode(dllbasename),
                 )
 
                 if not os.path.exists(dest):
@@ -823,19 +741,18 @@
 
                 # Also overwrite python3.dll so that hgext.git is usable.
                 # TODO: also handle the MSYS flavor
-                if sys.version_info[0] >= 3:
-                    python_x = os.path.join(
-                        os.path.dirname(fsdecode(buf.value)),
-                        "python3.dll",
+                python_x = os.path.join(
+                    os.path.dirname(os.fsdecode(buf.value)),
+                    "python3.dll",
+                )
+
+                if os.path.exists(python_x):
+                    dest = os.path.join(
+                        os.path.dirname(self.hgtarget),
+                        os.path.basename(python_x),
                     )
 
-                    if os.path.exists(python_x):
-                        dest = os.path.join(
-                            os.path.dirname(self.hgtarget),
-                            os.path.basename(python_x),
-                        )
-
-                        shutil.copy(python_x, dest)
+                    shutil.copy(python_x, dest)
 
         if not pythonlib:
             log.warn(
@@ -850,14 +767,10 @@
             f.write(b'/* this file is autogenerated by setup.py */\n')
             f.write(b'#define HGPYTHONLIB "%s"\n' % pythonlib)
 
-        macros = None
-        if sys.version_info[0] >= 3:
-            macros = [('_UNICODE', None), ('UNICODE', None)]
-
         objects = self.compiler.compile(
             ['mercurial/exewrapper.c'],
             output_dir=self.build_temp,
-            macros=macros,
+            macros=[('_UNICODE', None), ('UNICODE', None)],
         )
         self.compiler.link_executable(
             objects, self.hgtarget, libraries=[], output_dir=self.build_temp
@@ -1069,6 +982,10 @@
         ),
     ]
 
+    sub_commands = install.sub_commands + [
+        ('install_completion', lambda self: True)
+    ]
+
     # Also helps setuptools not be sad while we refuse to create eggs.
     single_version_externally_managed = True
 
@@ -1183,11 +1100,43 @@
                 )
                 continue
 
-            data = data.replace(b'@LIBDIR@', libdir.encode(libdir_escape))
+            data = data.replace(b'@LIBDIR@', libdir.encode('unicode_escape'))
             with open(outfile, 'wb') as fp:
                 fp.write(data)
 
 
+class hginstallcompletion(Command):
+    description = 'Install shell completion'
+
+    def initialize_options(self):
+        self.install_dir = None
+        self.outputs = []
+
+    def finalize_options(self):
+        self.set_undefined_options(
+            'install_data', ('install_dir', 'install_dir')
+        )
+
+    def get_outputs(self):
+        return self.outputs
+
+    def run(self):
+        for src, dir_path, dest in (
+            (
+                'bash_completion',
+                ('share', 'bash-completion', 'completions'),
+                'hg',
+            ),
+            ('zsh_completion', ('share', 'zsh', 'site-functions'), '_hg'),
+        ):
+            dir = os.path.join(self.install_dir, *dir_path)
+            self.mkpath(dir)
+
+            dest = os.path.join(dir, dest)
+            self.outputs.append(dest)
+            self.copy_file(os.path.join('contrib', src), dest)
+
+
 # virtualenv installs custom distutils/__init__.py and
 # distutils/distutils.cfg files which essentially proxy back to the
 # "real" distutils in the main Python install. The presence of this
@@ -1278,6 +1227,7 @@
     'build_scripts': hgbuildscripts,
     'build_hgextindex': buildhgextindex,
     'install': hginstall,
+    'install_completion': hginstallcompletion,
     'install_lib': hginstalllib,
     'install_scripts': hginstallscripts,
     'build_hgexe': buildhgexe,
@@ -1324,27 +1274,12 @@
     'hgdemandimport',
 ]
 
-# The pygit2 dependency dropped py2 support with the 1.0 release in Dec 2019.
-# Prior releases do not build at all on Windows, because Visual Studio 2008
-# doesn't understand C 11.  Older Linux releases are buggy.
-if sys.version_info[0] == 2:
-    packages.remove('hgext.git')
-
-
 for name in os.listdir(os.path.join('mercurial', 'templates')):
     if name != '__pycache__' and os.path.isdir(
         os.path.join('mercurial', 'templates', name)
     ):
         packages.append('mercurial.templates.%s' % name)
 
-if sys.version_info[0] == 2:
-    packages.extend(
-        [
-            'mercurial.thirdparty.concurrent',
-            'mercurial.thirdparty.concurrent.futures',
-        ]
-    )
-
 if 'HG_PY2EXE_EXTRA_INSTALL_PACKAGES' in os.environ:
     # py2exe can't cope with namespace packages very well, so we have to
     # install any hgext3rd.* extensions that we want in the final py2exe
@@ -1476,19 +1411,9 @@
 
         cargocmd = ['cargo', 'rustc', '--release']
 
-        feature_flags = []
-
-        cargocmd.append('--no-default-features')
-        if sys.version_info[0] == 2:
-            feature_flags.append('python27')
-        elif sys.version_info[0] == 3:
-            feature_flags.append('python3')
-
         rust_features = env.get("HG_RUST_FEATURES")
         if rust_features:
-            feature_flags.append(rust_features)
-
-        cargocmd.extend(('--features', " ".join(feature_flags)))
+            cargocmd.extend(('--features', rust_features))
 
         cargocmd.append('--')
         if sys.platform == 'darwin':
@@ -1640,7 +1565,7 @@
     # the cygwinccompiler package is not available on some Python
     # distributions like the ones from the optware project for Synology
     # DiskStation boxes
-    class HackedMingw32CCompiler(object):
+    class HackedMingw32CCompiler:
         pass
 
 
@@ -1763,9 +1688,7 @@
 if sys.platform == 'darwin' and os.path.exists('/usr/bin/xcodebuild'):
     version = runcmd(['/usr/bin/xcodebuild', '-version'], {})[1].splitlines()
     if version:
-        version = version[0]
-        if sys.version_info[0] == 3:
-            version = version.decode('utf-8')
+        version = version[0].decode('utf-8')
         xcode4 = version.startswith('Xcode') and StrictVersion(
             version.split()[1]
         ) >= StrictVersion('4.0')
--- a/tests/artifacts/scripts/generate-churning-bundle.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/artifacts/scripts/generate-churning-bundle.py	Wed May 04 18:17:44 2022 +0200
@@ -17,7 +17,6 @@
 #
 # Running with `chg` in your path and `CHGHG` set is recommended for speed.
 
-from __future__ import absolute_import, print_function
 
 import hashlib
 import os
--- a/tests/autodiff.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/autodiff.py	Wed May 04 18:17:44 2022 +0200
@@ -1,6 +1,5 @@
 # Extension dedicated to test patch.diff() upgrade modes
 
-from __future__ import absolute_import
 
 from mercurial import (
     error,
--- a/tests/basic_test_result.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/basic_test_result.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function
-
 import sys
 import unittest
 
--- a/tests/blackbox-readonly-dispatch.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/blackbox-readonly-dispatch.py	Wed May 04 18:17:44 2022 +0200
@@ -1,4 +1,3 @@
-from __future__ import absolute_import
 import os
 from mercurial import (
     dispatch,
--- a/tests/bruterebase.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/bruterebase.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from mercurial import (
     error,
--- a/tests/check-perf-code.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/check-perf-code.py	Wed May 04 18:17:44 2022 +0200
@@ -2,7 +2,6 @@
 #
 # check-perf-code - (historical) portability checker for contrib/perf.py
 
-from __future__ import absolute_import
 
 import os
 import sys
--- a/tests/common-pattern.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/common-pattern.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,4 @@
 # common patterns in test at can safely be replaced
-from __future__ import absolute_import
 
 import os
 
--- a/tests/crashgetbundler.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/crashgetbundler.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 from mercurial.i18n import _
 from mercurial import changegroup, error, extensions
 
--- a/tests/drawdag.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/drawdag.py	Wed May 04 18:17:44 2022 +0200
@@ -80,7 +80,6 @@
       # split: A -> B, C           # 1 to many
       # prune: A, B, C             # many to nothing
 """
-from __future__ import absolute_import, print_function
 
 import collections
 import itertools
@@ -266,7 +265,7 @@
     return dict(edges)
 
 
-class simplefilectx(object):
+class simplefilectx:
     def __init__(self, path, data):
         self._data = data
         self._path = path
--- a/tests/dumbhttp.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/dumbhttp.py	Wed May 04 18:17:44 2022 +0200
@@ -1,6 +1,5 @@
 #!/usr/bin/env python
 
-from __future__ import absolute_import
 
 """
 Small and dumb HTTP server for use in tests.
@@ -38,7 +37,7 @@
         sys.stderr.flush()
 
 
-class simplehttpservice(object):
+class simplehttpservice:
     def __init__(self, host, port):
         self.address = (host, port)
 
--- a/tests/dummysmtpd.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/dummysmtpd.py	Wed May 04 18:17:44 2022 +0200
@@ -2,7 +2,6 @@
 
 """dummy SMTP server for use in tests"""
 
-from __future__ import absolute_import
 
 import asyncore
 import optparse
--- a/tests/dummyssh	Wed May 04 18:00:01 2022 +0200
+++ b/tests/dummyssh	Wed May 04 18:17:44 2022 +0200
@@ -1,6 +1,5 @@
 #!/usr/bin/env python3
 
-from __future__ import absolute_import
 
 import os
 import shlex
--- a/tests/f	Wed May 04 18:00:01 2022 +0200
+++ b/tests/f	Wed May 04 18:17:44 2022 +0200
@@ -23,7 +23,6 @@
   md5sum.py
 """
 
-from __future__ import absolute_import
 
 import binascii
 import glob
--- a/tests/failfilemerge.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/failfilemerge.py	Wed May 04 18:17:44 2022 +0200
@@ -1,6 +1,5 @@
 # extension to emulate interrupting filemerge._filemerge
 
-from __future__ import absolute_import
 
 from mercurial import (
     error,
--- a/tests/fakedirstatewritetime.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/fakedirstatewritetime.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 #   - 'workingctx._poststatusfixup()' (= 'repo.status()')
 #   - 'committablectx.markcommitted()'
 
-from __future__ import absolute_import
 
 from mercurial import (
     context,
--- a/tests/fakemergerecord.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/fakemergerecord.py	Wed May 04 18:17:44 2022 +0200
@@ -2,7 +2,6 @@
 #
 #
 
-from __future__ import absolute_import
 
 from mercurial import (
     mergestate as mergestatemod,
--- a/tests/fakepatchtime.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/fakepatchtime.py	Wed May 04 18:17:44 2022 +0200
@@ -1,7 +1,6 @@
 # extension to emulate invoking 'patch.internalpatch()' at the time
 # specified by '[fakepatchtime] fakenow'
 
-from __future__ import absolute_import
 
 from mercurial import (
     extensions,
--- a/tests/filterpyflakes.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/filterpyflakes.py	Wed May 04 18:17:44 2022 +0200
@@ -2,7 +2,6 @@
 
 # Filter output by pyflakes to control which warnings we check
 
-from __future__ import absolute_import, print_function
 
 import re
 import sys
--- a/tests/filtertraceback.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/filtertraceback.py	Wed May 04 18:17:44 2022 +0200
@@ -2,7 +2,6 @@
 
 # Filters traceback lines from stdin.
 
-from __future__ import absolute_import, print_function
 
 import io
 import sys
--- a/tests/flagprocessorext.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/flagprocessorext.py	Wed May 04 18:17:44 2022 +0200
@@ -1,6 +1,5 @@
 # coding=UTF-8
 
-from __future__ import absolute_import
 
 import base64
 import zlib
--- a/tests/fsmonitor-run-tests.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/fsmonitor-run-tests.py	Wed May 04 18:17:44 2022 +0200
@@ -11,8 +11,6 @@
 # Watchman and runs the Mercurial tests against it. This ensures that the global
 # version of Watchman isn't affected by anything this test does.
 
-from __future__ import absolute_import
-from __future__ import print_function
 
 import argparse
 import contextlib
--- a/tests/generate-working-copy-states.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/generate-working-copy-states.py	Wed May 04 18:17:44 2022 +0200
@@ -29,7 +29,6 @@
 # $ hg forget *_*_*-untracked
 # $ rm *_*_missing-*
 
-from __future__ import absolute_import, print_function
 
 import os
 import sys
--- a/tests/get-with-headers.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/get-with-headers.py	Wed May 04 18:17:44 2022 +0200
@@ -3,7 +3,6 @@
 """This does HTTP GET requests given a host:port and path and returns
 a subset of the headers plus the body of the result."""
 
-from __future__ import absolute_import
 
 import argparse
 import json
--- a/tests/heredoctest.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/heredoctest.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function
-
 import sys
 
 
--- a/tests/hghave	Wed May 04 18:00:01 2022 +0200
+++ b/tests/hghave	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 prefixed with "no-", the absence of feature is tested.
 """
 
-from __future__ import absolute_import, print_function
 
 import hghave
 import optparse
--- a/tests/hghave.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/hghave.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function
-
 import distutils.version
 import os
 import re
--- a/tests/hgweberror.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/hgweberror.py	Wed May 04 18:17:44 2022 +0200
@@ -1,6 +1,5 @@
 # A dummy extension that installs an hgweb command that throws an Exception.
 
-from __future__ import absolute_import
 
 from mercurial.hgweb import webcommands
 
--- a/tests/httpserverauth.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/httpserverauth.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import base64
 import hashlib
 
@@ -18,7 +16,7 @@
     return parsed
 
 
-class digestauthserver(object):
+class digestauthserver:
     def __init__(self):
         self._user_hashes = {}
 
--- a/tests/hypothesishelpers.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/hypothesishelpers.py	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 #
 # For details see http://hypothesis.readthedocs.org
 
-from __future__ import absolute_import, print_function
 import os
 import sys
 import traceback
--- a/tests/killdaemons.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/killdaemons.py	Wed May 04 18:17:44 2022 +0200
@@ -1,6 +1,5 @@
 #!/usr/bin/env python3
 
-from __future__ import absolute_import
 import errno
 import os
 import signal
--- a/tests/list-tree.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/list-tree.py	Wed May 04 18:17:44 2022 +0200
@@ -1,8 +1,3 @@
-from __future__ import (
-    absolute_import,
-    print_function,
-)
-
 import argparse
 import os
 
--- a/tests/lockdelay.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/lockdelay.py	Wed May 04 18:17:44 2022 +0200
@@ -2,7 +2,6 @@
 #
 # This extension can be used to test race conditions between lock acquisition.
 
-from __future__ import absolute_import
 
 import os
 import time
--- a/tests/logexceptions.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/logexceptions.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import inspect
 import os
--- a/tests/ls-l.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/ls-l.py	Wed May 04 18:17:44 2022 +0200
@@ -2,7 +2,6 @@
 
 # like ls -l, but do not print date, user, or non-common mode bit, to avoid
 # using globs in tests.
-from __future__ import absolute_import, print_function
 
 import os
 import stat
--- a/tests/md5sum.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/md5sum.py	Wed May 04 18:17:44 2022 +0200
@@ -6,7 +6,6 @@
 # of the PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2, which is
 # GPL-compatible.
 
-from __future__ import absolute_import
 
 import hashlib
 import os
--- a/tests/mockblackbox.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/mockblackbox.py	Wed May 04 18:17:44 2022 +0200
@@ -1,4 +1,3 @@
-from __future__ import absolute_import
 from mercurial.utils import procutil
 
 # XXX: we should probably offer a devel option to do this in blackbox directly
--- a/tests/mockmakedate.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/mockmakedate.py	Wed May 04 18:17:44 2022 +0200
@@ -1,6 +1,5 @@
 # mock out util.makedate() to supply testable values
 
-from __future__ import absolute_import
 
 import os
 
--- a/tests/mocktime.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/mocktime.py	Wed May 04 18:17:44 2022 +0200
@@ -1,10 +1,8 @@
-from __future__ import absolute_import
-
 import os
 import time
 
 
-class mocktime(object):
+class mocktime:
     def __init__(self, increment):
         self.time = 0
         self.increment = [float(s) for s in increment.split()]
--- a/tests/printenv.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/printenv.py	Wed May 04 18:17:44 2022 +0200
@@ -12,7 +12,6 @@
 #   - [output] is the name of the output file (default: use sys.stdout)
 #              the file will be opened in append mode.
 #
-from __future__ import absolute_import
 import argparse
 import os
 import sys
--- a/tests/printrevset.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/printrevset.py	Wed May 04 18:17:44 2022 +0200
@@ -1,4 +1,3 @@
-from __future__ import absolute_import
 from mercurial.thirdparty import attr
 from mercurial import (
     cmdutil,
--- a/tests/pullext.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/pullext.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from mercurial.i18n import _
 from mercurial import (
--- a/tests/readlink.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/readlink.py	Wed May 04 18:17:44 2022 +0200
@@ -1,6 +1,5 @@
 #!/usr/bin/env python3
 
-from __future__ import absolute_import, print_function
 
 import errno
 import os
--- a/tests/remotefilelog-getflogheads.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/remotefilelog-getflogheads.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 from mercurial.i18n import _
 from mercurial import (
     hg,
--- a/tests/revlog-formatv0.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/revlog-formatv0.py	Wed May 04 18:17:44 2022 +0200
@@ -17,7 +17,6 @@
 empty file
 """
 
-from __future__ import absolute_import
 import binascii
 import os
 import sys
--- a/tests/revnamesext.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/revnamesext.py	Wed May 04 18:17:44 2022 +0200
@@ -1,6 +1,5 @@
 # Dummy extension to define a namespace containing revision names
 
-from __future__ import absolute_import
 
 from mercurial import namespaces
 
--- a/tests/run-tests.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/run-tests.py	Wed May 04 18:17:44 2022 +0200
@@ -43,7 +43,6 @@
 # completes fairly quickly, includes both shell and Python scripts, and
 # includes some scripts that run daemon processes.)
 
-from __future__ import absolute_import, print_function
 
 import argparse
 import collections
@@ -51,12 +50,15 @@
 import difflib
 import distutils.version as version
 import errno
+import functools
 import json
 import multiprocessing
 import os
 import platform
+import queue
 import random
 import re
+import shlex
 import shutil
 import signal
 import socket
@@ -70,21 +72,15 @@
 import uuid
 import xml.dom.minidom as minidom
 
+if sys.version_info < (3, 5, 0):
+    print(
+        '%s is only supported on Python 3.5+, not %s'
+        % (sys.argv[0], '.'.join(str(v) for v in sys.version_info[:3]))
+    )
+    sys.exit(70)  # EX_SOFTWARE from `man 3 sysexit`
+
 WINDOWS = os.name == r'nt'
-
-try:
-    import Queue as queue
-except ImportError:
-    import queue
-
-try:
-    import shlex
-
-    shellquote = shlex.quote
-except (ImportError, AttributeError):
-    import pipes
-
-    shellquote = pipes.quote
+shellquote = shlex.quote
 
 
 processlock = threading.Lock()
@@ -155,80 +151,62 @@
 origenviron = os.environ.copy()
 
 
-if sys.version_info > (3, 5, 0):
-    PYTHON3 = True
-    xrange = range  # we use xrange in one place, and we'd rather not use range
-
-    def _sys2bytes(p):
-        if p is None:
-            return p
-        return p.encode('utf-8')
-
-    def _bytes2sys(p):
-        if p is None:
-            return p
-        return p.decode('utf-8')
-
-    osenvironb = getattr(os, 'environb', None)
-    if osenvironb is None:
-        # Windows lacks os.environb, for instance.  A proxy over the real thing
-        # instead of a copy allows the environment to be updated via bytes on
-        # all platforms.
-        class environbytes(object):
-            def __init__(self, strenv):
-                self.__len__ = strenv.__len__
-                self.clear = strenv.clear
-                self._strenv = strenv
-
-            def __getitem__(self, k):
-                v = self._strenv.__getitem__(_bytes2sys(k))
-                return _sys2bytes(v)
-
-            def __setitem__(self, k, v):
-                self._strenv.__setitem__(_bytes2sys(k), _bytes2sys(v))
-
-            def __delitem__(self, k):
-                self._strenv.__delitem__(_bytes2sys(k))
-
-            def __contains__(self, k):
-                return self._strenv.__contains__(_bytes2sys(k))
-
-            def __iter__(self):
-                return iter([_sys2bytes(k) for k in iter(self._strenv)])
-
-            def get(self, k, default=None):
-                v = self._strenv.get(_bytes2sys(k), _bytes2sys(default))
-                return _sys2bytes(v)
-
-            def pop(self, k, default=None):
-                v = self._strenv.pop(_bytes2sys(k), _bytes2sys(default))
-                return _sys2bytes(v)
-
-        osenvironb = environbytes(os.environ)
-
-    getcwdb = getattr(os, 'getcwdb')
-    if not getcwdb or WINDOWS:
-        getcwdb = lambda: _sys2bytes(os.getcwd())
-
-elif sys.version_info >= (3, 0, 0):
-    print(
-        '%s is only supported on Python 3.5+ and 2.7, not %s'
-        % (sys.argv[0], '.'.join(str(v) for v in sys.version_info[:3]))
-    )
-    sys.exit(70)  # EX_SOFTWARE from `man 3 sysexit`
-else:
-    PYTHON3 = False
-
-    # In python 2.x, path operations are generally done using
-    # bytestrings by default, so we don't have to do any extra
-    # fiddling there. We define the wrapper functions anyway just to
-    # help keep code consistent between platforms.
-    def _sys2bytes(p):
+xrange = range  # we use xrange in one place, and we'd rather not use range
+
+
+def _sys2bytes(p):
+    if p is None:
+        return p
+    return p.encode('utf-8')
+
+
+def _bytes2sys(p):
+    if p is None:
         return p
-
-    _bytes2sys = _sys2bytes
-    osenvironb = os.environ
-    getcwdb = os.getcwd
+    return p.decode('utf-8')
+
+
+osenvironb = getattr(os, 'environb', None)
+if osenvironb is None:
+    # Windows lacks os.environb, for instance.  A proxy over the real thing
+    # instead of a copy allows the environment to be updated via bytes on
+    # all platforms.
+    class environbytes:
+        def __init__(self, strenv):
+            self.__len__ = strenv.__len__
+            self.clear = strenv.clear
+            self._strenv = strenv
+
+        def __getitem__(self, k):
+            v = self._strenv.__getitem__(_bytes2sys(k))
+            return _sys2bytes(v)
+
+        def __setitem__(self, k, v):
+            self._strenv.__setitem__(_bytes2sys(k), _bytes2sys(v))
+
+        def __delitem__(self, k):
+            self._strenv.__delitem__(_bytes2sys(k))
+
+        def __contains__(self, k):
+            return self._strenv.__contains__(_bytes2sys(k))
+
+        def __iter__(self):
+            return iter([_sys2bytes(k) for k in iter(self._strenv)])
+
+        def get(self, k, default=None):
+            v = self._strenv.get(_bytes2sys(k), _bytes2sys(default))
+            return _sys2bytes(v)
+
+        def pop(self, k, default=None):
+            v = self._strenv.pop(_bytes2sys(k), _bytes2sys(default))
+            return _sys2bytes(v)
+
+    osenvironb = environbytes(os.environ)
+
+getcwdb = getattr(os, 'getcwdb')
+if not getcwdb or WINDOWS:
+    getcwdb = lambda: _sys2bytes(os.getcwd())
+
 
 if WINDOWS:
     _getcwdb = getcwdb
@@ -260,10 +238,14 @@
         s.bind(('localhost', port))
         s.close()
         return True
-    except socket.error as exc:
+    except (socket.error, OSError) as exc:
         if exc.errno == errno.EADDRINUSE:
             return True
-        elif exc.errno in (errno.EADDRNOTAVAIL, errno.EPROTONOSUPPORT):
+        elif exc.errno in (
+            errno.EADDRNOTAVAIL,
+            errno.EPROTONOSUPPORT,
+            errno.EAFNOSUPPORT,
+        ):
             return False
         else:
             raise
@@ -288,12 +270,11 @@
     except socket.error as exc:
         if WINDOWS and exc.errno == errno.WSAEACCES:
             return False
-        elif PYTHON3:
-            # TODO: make a proper exception handler after dropping py2.  This
-            #       works because socket.error is an alias for OSError on py3,
-            #       which is also the baseclass of PermissionError.
-            if isinstance(exc, PermissionError):
-                return False
+        # TODO: make a proper exception handler after dropping py2.  This
+        #       works because socket.error is an alias for OSError on py3,
+        #       which is also the baseclass of PermissionError.
+        elif isinstance(exc, PermissionError):
+            return False
         if exc.errno not in (
             errno.EADDRINUSE,
             errno.EADDRNOTAVAIL,
@@ -372,18 +353,10 @@
 
 
 def which(exe):
-    if PYTHON3:
-        # shutil.which only accept bytes from 3.8
-        cmd = _bytes2sys(exe)
-        real_exec = shutil.which(cmd)
-        return _sys2bytes(real_exec)
-    else:
-        # let us do the os work
-        for p in osenvironb[b'PATH'].split(os.pathsep):
-            f = os.path.join(p, exe)
-            if os.path.isfile(f):
-                return f
-        return None
+    # shutil.which only accept bytes from 3.8
+    cmd = _bytes2sys(exe)
+    real_exec = shutil.which(cmd)
+    return _sys2bytes(real_exec)
 
 
 def parselistfiles(files, listtype, warn=True):
@@ -898,11 +871,7 @@
                 pass
 
 
-_unified_diff = difflib.unified_diff
-if PYTHON3:
-    import functools
-
-    _unified_diff = functools.partial(difflib.diff_bytes, difflib.unified_diff)
+_unified_diff = functools.partial(difflib.diff_bytes, difflib.unified_diff)
 
 
 def getdiff(expected, output, ref, err):
@@ -1489,7 +1458,7 @@
         # This has the same effect as Py_LegacyWindowsStdioFlag in exewrapper.c,
         # but this is needed for testing python instances like dummyssh,
         # dummysmtpd.py, and dumbhttp.py.
-        if PYTHON3 and WINDOWS:
+        if WINDOWS:
             env['PYTHONLEGACYWINDOWSSTDIO'] = '1'
 
         # Modified HOME in test environment can confuse Rust tools. So set
@@ -1680,9 +1649,7 @@
     re.compile(br'.*\$LOCALIP.*$'),
 ]
 
-bchr = chr
-if PYTHON3:
-    bchr = lambda x: bytes([x])
+bchr = lambda x: bytes([x])
 
 WARN_UNDEFINED = 1
 WARN_YES = 2
@@ -1821,9 +1788,7 @@
                 script.append(b'echo %s %d $?\n' % (salt, line))
 
         activetrace = []
-        session = str(uuid.uuid4())
-        if PYTHON3:
-            session = session.encode('ascii')
+        session = str(uuid.uuid4()).encode('ascii')
         hgcatapult = os.getenv('HGTESTCATAPULTSERVERPIPE') or os.getenv(
             'HGCATAPULTSERVERPIPE'
         )
@@ -1877,11 +1842,8 @@
             script.append(b'alias pwd="pwd -W"\n')
 
         if hgcatapult and hgcatapult != os.devnull:
-            if PYTHON3:
-                hgcatapult = hgcatapult.encode('utf8')
-                cataname = self.name.encode('utf8')
-            else:
-                cataname = self.name
+            hgcatapult = hgcatapult.encode('utf8')
+            cataname = self.name.encode('utf8')
 
             # Kludge: use a while loop to keep the pipe from getting
             # closed by our echo commands. The still-running file gets
@@ -2186,11 +2148,8 @@
                     return "retry", False
 
         if el.endswith(b" (esc)\n"):
-            if PYTHON3:
-                el = el[:-7].decode('unicode_escape') + '\n'
-                el = el.encode('latin-1')
-            else:
-                el = el[:-7].decode('string-escape') + '\n'
+            el = el[:-7].decode('unicode_escape') + '\n'
+            el = el.encode('latin-1')
         if el == l or WINDOWS and el[:-1] + b'\r\n' == l:
             return True, True
         if el.endswith(b" (re)\n"):
@@ -2238,10 +2197,7 @@
 firstlock = threading.RLock()
 firsterror = False
 
-if PYTHON3:
-    base_class = unittest.TextTestResult
-else:
-    base_class = unittest._TextTestResult
+base_class = unittest.TextTestResult
 
 
 class TestResult(base_class):
@@ -2365,13 +2321,9 @@
                 self.stream.write('\n')
                 for line in lines:
                     line = highlightdiff(line, self.color)
-                    if PYTHON3:
-                        self.stream.flush()
-                        self.stream.buffer.write(line)
-                        self.stream.buffer.flush()
-                    else:
-                        self.stream.write(line)
-                        self.stream.flush()
+                    self.stream.flush()
+                    self.stream.buffer.write(line)
+                    self.stream.buffer.flush()
 
                 if servefail:
                     raise test.failureException(
@@ -3038,7 +2990,7 @@
     testdescs.sort(key=sortkey)
 
 
-class TestRunner(object):
+class TestRunner:
     """Holds context for executing tests.
 
     Tests rely on a lot of state. This object holds it for them.
@@ -3267,10 +3219,7 @@
         osenvironb[b'RUNTESTDIR_FORWARD_SLASH'] = runtestdir.replace(
             os.sep.encode('ascii'), b'/'
         )
-        if PYTHON3:
-            sepb = _sys2bytes(os.pathsep)
-        else:
-            sepb = os.pathsep
+        sepb = _sys2bytes(os.pathsep)
         path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
         if os.path.islink(__file__):
             # test helper will likely be at the end of the symlink
@@ -3466,7 +3415,7 @@
 
             failed = False
             kws = self.options.keywords
-            if kws is not None and PYTHON3:
+            if kws is not None:
                 kws = kws.encode('utf-8')
 
             suite = TestSuite(
@@ -3614,14 +3563,10 @@
     def _usecorrectpython(self):
         """Configure the environment to use the appropriate Python in tests."""
         # Tests must use the same interpreter as us or bad things will happen.
-        if WINDOWS and PYTHON3:
+        if WINDOWS:
             pyexe_names = [b'python', b'python3', b'python.exe']
-        elif WINDOWS:
-            pyexe_names = [b'python', b'python.exe']
-        elif PYTHON3:
+        else:
             pyexe_names = [b'python', b'python3']
-        else:
-            pyexe_names = [b'python', b'python2']
 
         # os.symlink() is a thing with py3 on Windows, but it requires
         # Administrator rights.
@@ -3664,14 +3609,6 @@
                     f.write(b'%s "$@"\n' % esc_executable)
 
             if WINDOWS:
-                if not PYTHON3:
-                    # lets try to build a valid python3 executable for the
-                    # scrip that requires it.
-                    py3exe_name = os.path.join(self._custom_bin_dir, b'python3')
-                    with open(py3exe_name, 'wb') as f:
-                        f.write(b'#!/bin/sh\n')
-                        f.write(b'py -3 "$@"\n')
-
                 # adjust the path to make sur the main python finds it own dll
                 path = os.environ['PATH'].split(os.pathsep)
                 main_exec_dir = os.path.dirname(sysexecutable)
@@ -3684,8 +3621,6 @@
                 if appdata is not None:
                     python_dir = 'Python%d%d' % (vi[0], vi[1])
                     scripts_path = [appdata, 'Python', python_dir, 'Scripts']
-                    if not PYTHON3:
-                        scripts_path = [appdata, 'Python', 'Scripts']
                     scripts_dir = os.path.join(*scripts_path)
                     extra_paths.append(scripts_dir)
 
@@ -3729,12 +3664,9 @@
             setup_opts = b"--no-rust"
 
         # Run installer in hg root
-        script = os.path.realpath(sys.argv[0])
-        exe = sysexecutable
-        if PYTHON3:
-            compiler = _sys2bytes(compiler)
-            script = _sys2bytes(script)
-            exe = _sys2bytes(exe)
+        compiler = _sys2bytes(compiler)
+        script = _sys2bytes(os.path.realpath(sys.argv[0]))
+        exe = _sys2bytes(sysexecutable)
         hgroot = os.path.dirname(os.path.dirname(script))
         self._hgroot = hgroot
         os.chdir(hgroot)
@@ -3786,10 +3718,7 @@
         else:
             with open(installerrs, 'rb') as f:
                 for line in f:
-                    if PYTHON3:
-                        sys.stdout.buffer.write(line)
-                    else:
-                        sys.stdout.write(line)
+                    sys.stdout.buffer.write(line)
             sys.exit(1)
         os.chdir(self._testdir)
 
@@ -3850,9 +3779,7 @@
             return self._hgpath
 
         cmd = b'"%s" -c "import mercurial; print (mercurial.__path__[0])"'
-        cmd = cmd % PYTHON
-        if PYTHON3:
-            cmd = _bytes2sys(cmd)
+        cmd = _bytes2sys(cmd % PYTHON)
 
         p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
         out, err = p.communicate()
@@ -3882,10 +3809,7 @@
         )
         out, _err = proc.communicate()
         if proc.returncode != 0:
-            if PYTHON3:
-                sys.stdout.buffer.write(out)
-            else:
-                sys.stdout.write(out)
+            sys.stdout.buffer.write(out)
             sys.exit(1)
 
     def _installrhg(self):
@@ -3909,10 +3833,7 @@
         )
         out, _err = proc.communicate()
         if proc.returncode != 0:
-            if PYTHON3:
-                sys.stdout.buffer.write(out)
-            else:
-                sys.stdout.write(out)
+            sys.stdout.buffer.write(out)
             sys.exit(1)
 
     def _build_pyoxidized(self):
@@ -3940,10 +3861,7 @@
         )
         out, _err = proc.communicate()
         if proc.returncode != 0:
-            if PYTHON3:
-                sys.stdout.buffer.write(out)
-            else:
-                sys.stdout.write(out)
+            sys.stdout.buffer.write(out)
             sys.exit(1)
 
     def _outputcoverage(self):
--- a/tests/seq.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/seq.py	Wed May 04 18:17:44 2022 +0200
@@ -7,7 +7,6 @@
 #   seq START STOP        [START, STOP] stepping by 1
 #   seq START STEP STOP   [START, STOP] stepping by STEP
 
-from __future__ import absolute_import, print_function
 import os
 import sys
 
--- a/tests/silenttestrunner.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/silenttestrunner.py	Wed May 04 18:17:44 2022 +0200
@@ -1,4 +1,3 @@
-from __future__ import absolute_import, print_function
 import os
 import sys
 import unittest
--- a/tests/simplestorerepo.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/simplestorerepo.py	Wed May 04 18:17:44 2022 +0200
@@ -10,7 +10,6 @@
 #   $ HGREPOFEATURES="simplestore" ./run-tests.py \
 #       --extra-config-opt extensions.simplestore=`pwd`/simplestorerepo.py
 
-from __future__ import absolute_import
 
 import stat
 
@@ -71,7 +70,7 @@
 
 @interfaceutil.implementer(repository.irevisiondelta)
 @attr.s(slots=True)
-class simplestorerevisiondelta(object):
+class simplestorerevisiondelta:
     node = attr.ib()
     p1node = attr.ib()
     p2node = attr.ib()
@@ -85,14 +84,14 @@
 
 @interfaceutil.implementer(repository.iverifyproblem)
 @attr.s(frozen=True)
-class simplefilestoreproblem(object):
+class simplefilestoreproblem:
     warning = attr.ib(default=None)
     error = attr.ib(default=None)
     node = attr.ib(default=None)
 
 
 @interfaceutil.implementer(repository.ifilestorage)
-class filestorage(object):
+class filestorage:
     """Implements storage for a tracked path.
 
     Data is stored in the VFS in a directory corresponding to the tracked
--- a/tests/sitecustomize.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/sitecustomize.py	Wed May 04 18:17:44 2022 +0200
@@ -1,4 +1,3 @@
-from __future__ import absolute_import
 import os
 
 if os.environ.get('COVERAGE_PROCESS_START'):
--- a/tests/sshprotoext.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/sshprotoext.py	Wed May 04 18:17:44 2022 +0200
@@ -8,7 +8,6 @@
 # This extension replaces the SSH server started via `hg serve --stdio`.
 # The server behaves differently depending on environment variables.
 
-from __future__ import absolute_import
 
 from mercurial import (
     error,
--- a/tests/svn-safe-append.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/svn-safe-append.py	Wed May 04 18:17:44 2022 +0200
@@ -1,6 +1,5 @@
 #!/usr/bin/env python3
 
-from __future__ import absolute_import
 
 __doc__ = """Same as `echo a >> b`, but ensures a changed mtime of b.
 Without this svn will not detect workspace changes."""
--- a/tests/svnurlof.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/svnurlof.py	Wed May 04 18:17:44 2022 +0200
@@ -1,4 +1,3 @@
-from __future__ import absolute_import, print_function
 import sys
 
 from mercurial import (
--- a/tests/svnxml.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/svnxml.py	Wed May 04 18:17:44 2022 +0200
@@ -1,7 +1,6 @@
 # Read the output of a "svn log --xml" command on stdin, parse it and
 # print a subset of attributes common to all svn versions tested by
 # hg.
-from __future__ import absolute_import
 import sys
 import xml.dom.minidom
 
--- a/tests/test-absorb-edit-lines.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-absorb-edit-lines.t	Wed May 04 18:17:44 2022 +0200
@@ -15,10 +15,10 @@
 
 absorb --edit-lines will run the editor if filename is provided:
 
-  $ hg absorb --edit-lines --apply-changes
+  $ hg absorb --edit-lines
   nothing applied
   [1]
-  $ HGEDITOR=cat hg absorb --edit-lines --apply-changes a
+  $ HGEDITOR=cat hg absorb --edit-lines a
   HG: editing a
   HG: "y" means the line to the right exists in the changeset to the top
   HG:
@@ -43,7 +43,7 @@
   >     y   : f
   >     yyy : g
   > EOF
-  $ HGEDITOR='cat editortext >' hg absorb -q --edit-lines --apply-changes a
+  $ HGEDITOR='cat editortext >' hg absorb -q --edit-lines a
   $ hg cat -r 0 a
   d  
   e
--- a/tests/test-absorb-filefixupstate.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-absorb-filefixupstate.py	Wed May 04 18:17:44 2022 +0200
@@ -1,11 +1,9 @@
-from __future__ import absolute_import, print_function
-
 import itertools
 from mercurial import pycompat
 from hgext import absorb
 
 
-class simplefctx(object):
+class simplefctx:
     def __init__(self, content):
         self.content = content
 
--- a/tests/test-ancestor.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-ancestor.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function
-
 import binascii
 import getopt
 import math
@@ -64,7 +62,7 @@
     return ancs
 
 
-class naiveincrementalmissingancestors(object):
+class naiveincrementalmissingancestors:
     def __init__(self, ancs, bases):
         self.ancs = ancs
         self.bases = set(bases)
--- a/tests/test-annotate.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-annotate.py	Wed May 04 18:17:44 2022 +0200
@@ -1,6 +1,3 @@
-from __future__ import absolute_import
-from __future__ import print_function
-
 import unittest
 
 from mercurial import (
--- a/tests/test-annotate.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-annotate.t	Wed May 04 18:17:44 2022 +0200
@@ -478,7 +478,6 @@
 and its ancestor by overriding "repo._filecommit".
 
   $ cat > ../legacyrepo.py <<EOF
-  > from __future__ import absolute_import
   > from mercurial import commit, error, extensions
   > def _filecommit(orig, repo, fctx, manifest1, manifest2,
   >                 linkrev, tr, includecopymeta, ms):
--- a/tests/test-arbitraryfilectx.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-arbitraryfilectx.t	Wed May 04 18:17:44 2022 +0200
@@ -1,6 +1,5 @@
 Setup:
   $ cat > eval.py <<EOF
-  > from __future__ import absolute_import
   > import filecmp
   > from mercurial import commands, context, pycompat, registrar
   > cmdtable = {}
--- a/tests/test-archive.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-archive.t	Wed May 04 18:17:44 2022 +0200
@@ -320,7 +320,6 @@
   $ TIP=`hg id -v | cut -f1 -d' '`
   $ QTIP=`hg id -q`
   $ cat > getarchive.py <<EOF
-  > from __future__ import absolute_import
   > import os
   > import sys
   > from mercurial import (
@@ -455,7 +454,6 @@
   > done
 
   $ cat > md5comp.py <<EOF
-  > from __future__ import absolute_import, print_function
   > import hashlib
   > import sys
   > f1, f2 = sys.argv[1:3]
@@ -582,16 +580,11 @@
   Strms  Blocks   Compressed Uncompressed  Ratio  Check   Filename (xz !)
   $ rm -f ../archive.txz
 #endif
-#if py3 no-lzma
+#if no-lzma
   $ hg archive ../archive.txz
   abort: lzma module is not available
   [255]
 #endif
-#if no-py3
-  $ hg archive ../archive.txz
-  abort: xz compression is only available in Python 3
-  [255]
-#endif
 
 show an error when a provided pattern matches no files
 
@@ -617,7 +610,6 @@
   $ hg -R repo add repo/a
   $ hg -R repo commit -m '#0' -d '456789012 21600'
   $ cat > show_mtime.py <<EOF
-  > from __future__ import absolute_import, print_function
   > import os
   > import sys
   > print(int(os.stat(sys.argv[1]).st_mtime))
--- a/tests/test-atomictempfile.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-atomictempfile.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import glob
 import os
 import shutil
--- a/tests/test-bad-extension.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-bad-extension.t	Wed May 04 18:17:44 2022 +0200
@@ -54,7 +54,6 @@
   $ hg -q help help 2>&1 |grep extension
   *** failed to import extension "badext" from $TESTTMP/badext.py: bit bucket overflow
   *** failed to import extension "badext2": No module named 'badext2' (py3 !)
-  *** failed to import extension "badext2": No module named badext2 (no-py3 !)
 
 show traceback
 
@@ -63,9 +62,7 @@
   Traceback (most recent call last):
   Exception: bit bucket overflow
   *** failed to import extension "badext2": No module named 'badext2' (py3 !)
-  *** failed to import extension "badext2": No module named badext2 (no-py3 !)
   Traceback (most recent call last):
-  ImportError: No module named badext2 (no-py3 !)
   ImportError: No module named 'hgext.badext2' (py3 no-py36 !)
   ModuleNotFoundError: No module named 'hgext.badext2' (py36 !)
   Traceback (most recent call last): (py3 !)
@@ -114,19 +111,16 @@
   YYYY/MM/DD HH:MM:SS (PID)>   - loading extension: badext2
   YYYY/MM/DD HH:MM:SS (PID)>     - could not import hgext.badext2 (No module named *badext2*): trying hgext3rd.badext2 (glob)
   Traceback (most recent call last):
-  ImportError: No module named badext2 (no-py3 !)
   ImportError: No module named 'hgext.badext2' (py3 no-py36 !)
   ModuleNotFoundError: No module named 'hgext.badext2' (py36 !)
   YYYY/MM/DD HH:MM:SS (PID)>     - could not import hgext3rd.badext2 (No module named *badext2*): trying badext2 (glob)
   Traceback (most recent call last):
-  ImportError: No module named badext2 (no-py3 !)
   ImportError: No module named 'hgext.badext2' (py3 no-py36 !)
   ModuleNotFoundError: No module named 'hgext.badext2' (py36 !)
   Traceback (most recent call last): (py3 !)
   ImportError: No module named 'hgext3rd.badext2' (py3 no-py36 !)
   ModuleNotFoundError: No module named 'hgext3rd.badext2' (py36 !)
   *** failed to import extension "badext2": No module named 'badext2' (py3 !)
-  *** failed to import extension "badext2": No module named badext2 (no-py3 !)
   Traceback (most recent call last):
   ImportError: No module named 'hgext.badext2' (py3 no-py36 !)
   ModuleNotFoundError: No module named 'hgext.badext2' (py36 !)
@@ -136,7 +130,6 @@
   Traceback (most recent call last): (py3 !)
   ModuleNotFoundError: No module named 'badext2' (py36 !)
   ImportError: No module named 'badext2' (py3 no-py36 !)
-  ImportError: No module named badext2 (no-py3 !)
   YYYY/MM/DD HH:MM:SS (PID)> > loaded 2 extensions, total time * (glob)
   YYYY/MM/DD HH:MM:SS (PID)> - loading configtable attributes
   YYYY/MM/DD HH:MM:SS (PID)> - executing uisetup hooks
@@ -165,7 +158,6 @@
   $ hg help --keyword baddocext
   *** failed to import extension "badext" from $TESTTMP/badext.py: bit bucket overflow
   *** failed to import extension "badext2": No module named 'badext2' (py3 !)
-  *** failed to import extension "badext2": No module named badext2 (no-py3 !)
   Topics:
   
    extensions Using Additional Features
--- a/tests/test-basic.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-basic.t	Wed May 04 18:17:44 2022 +0200
@@ -240,15 +240,16 @@
 Underlying message streams should be updated when ui.fout/ferr are set:
 
   $ cat <<'EOF' > capui.py
-  > from mercurial import pycompat, registrar
+  > import io
+  > from mercurial import registrar
   > cmdtable = {}
   > command = registrar.command(cmdtable)
   > @command(b'capui', norepo=True)
   > def capui(ui):
   >     out = ui.fout
-  >     ui.fout = pycompat.bytesio()
+  >     ui.fout = io.BytesIO()
   >     ui.status(b'status\n')
-  >     ui.ferr = pycompat.bytesio()
+  >     ui.ferr = io.BytesIO()
   >     ui.warn(b'warn\n')
   >     out.write(b'stdout: %s' % ui.fout.getvalue())
   >     out.write(b'stderr: %s' % ui.ferr.getvalue())
--- a/tests/test-batching.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-batching.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import, print_function
 
 import contextlib
 
@@ -21,7 +20,7 @@
 
 
 # equivalent of repo.repository
-class thing(object):
+class thing:
     def hello(self):
         return b"Ready."
 
@@ -108,7 +107,7 @@
 # server side
 
 # equivalent of wireproto's global functions
-class server(object):
+class server:
     def __init__(self, local):
         self.local = local
 
--- a/tests/test-bdiff.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-bdiff.py	Wed May 04 18:17:44 2022 +0200
@@ -1,4 +1,3 @@
-from __future__ import absolute_import, print_function
 import collections
 import struct
 import unittest
--- a/tests/test-bisect.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-bisect.t	Wed May 04 18:17:44 2022 +0200
@@ -462,7 +462,6 @@
 
   $ cat > script.py <<EOF
   > #!$PYTHON
-  > from __future__ import absolute_import
   > import sys
   > from mercurial import hg, ui as uimod
   > repo = hg.repository(uimod.ui.load(), b'.')
--- a/tests/test-blackbox.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-blackbox.t	Wed May 04 18:17:44 2022 +0200
@@ -403,7 +403,6 @@
 when using chg, blackbox.log should get rotated correctly
 
   $ cat > $TESTTMP/noop.py << EOF
-  > from __future__ import absolute_import
   > import time
   > from mercurial import registrar, scmutil
   > cmdtable = {}
@@ -463,7 +462,6 @@
 blackbox should work if repo.ui.log is not called (issue5518)
 
   $ cat > $TESTTMP/raise.py << EOF
-  > from __future__ import absolute_import
   > from mercurial import registrar, scmutil
   > cmdtable = {}
   > command = registrar.command(cmdtable)
--- a/tests/test-bookmarks.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-bookmarks.t	Wed May 04 18:17:44 2022 +0200
@@ -1069,7 +1069,6 @@
   $ echo a > a
 
   $ cat > $TESTTMP/pausefinalize.py <<EOF
-  > from __future__ import absolute_import
   > import os
   > import time
   > from mercurial import extensions, localrepo
--- a/tests/test-bugzilla.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-bugzilla.t	Wed May 04 18:17:44 2022 +0200
@@ -1,7 +1,6 @@
 mock bugzilla driver for testing template output:
 
   $ cat <<EOF > bzmock.py
-  > from __future__ import absolute_import
   > from mercurial import extensions
   > from mercurial import pycompat
   > from mercurial import registrar
--- a/tests/test-bundle.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-bundle.t	Wed May 04 18:17:44 2022 +0200
@@ -466,7 +466,6 @@
 transaction)
 
   $ cat > $TESTTMP/showtip.py <<EOF
-  > from __future__ import absolute_import
   > 
   > def showtip(ui, repo, hooktype, **kwargs):
   >     ui.warn(b'%s: %s\n' % (hooktype, repo[b'tip'].hex()[:12]))
--- a/tests/test-bundle2-pushback.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-bundle2-pushback.t	Wed May 04 18:17:44 2022 +0200
@@ -3,7 +3,6 @@
   > Current bundle2 implementation doesn't provide a way to generate those
   > parts, so they must be created by extensions.
   > """
-  > from __future__ import absolute_import
   > from mercurial import bundle2, exchange, pushkey, util
   > def _newhandlechangegroup(op, inpart):
   >     """This function wraps the changegroup part handler for getbundle.
--- a/tests/test-cappedreader.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-cappedreader.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function
-
 import io
 import unittest
 
--- a/tests/test-cbor.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-cbor.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import os
 import sys
 import unittest
--- a/tests/test-check-code.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-check-code.t	Wed May 04 18:17:44 2022 +0200
@@ -27,7 +27,6 @@
   Skipping contrib/packaging/hgpackaging/cli.py it has no-che?k-code (glob)
   Skipping contrib/packaging/hgpackaging/downloads.py it has no-che?k-code (glob)
   Skipping contrib/packaging/hgpackaging/inno.py it has no-che?k-code (glob)
-  Skipping contrib/packaging/hgpackaging/py2exe.py it has no-che?k-code (glob)
   Skipping contrib/packaging/hgpackaging/pyoxidizer.py it has no-che?k-code (glob)
   Skipping contrib/packaging/hgpackaging/util.py it has no-che?k-code (glob)
   Skipping contrib/packaging/hgpackaging/wix.py it has no-che?k-code (glob)
--- a/tests/test-check-help.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-check-help.t	Wed May 04 18:17:44 2022 +0200
@@ -3,7 +3,6 @@
   $ . "$TESTDIR/helpers-testrepo.sh"
 
   $ cat <<'EOF' > scanhelptopics.py
-  > from __future__ import absolute_import, print_function
   > import re
   > import sys
   > if sys.platform == "win32":
--- a/tests/test-check-interfaces.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-check-interfaces.py	Wed May 04 18:17:44 2022 +0200
@@ -1,6 +1,5 @@
 # Test that certain objects conform to well-defined interfaces.
 
-from __future__ import absolute_import, print_function
 
 from mercurial import encoding
 
@@ -81,7 +80,7 @@
 
 
 # Facilitates testing localpeer.
-class dummyrepo(object):
+class dummyrepo:
     def __init__(self):
         self.ui = uimod.ui()
         self._wanted_sidedata = set()
@@ -93,7 +92,7 @@
         pass
 
 
-class dummyopener(object):
+class dummyopener:
     handlers = []
 
 
@@ -109,7 +108,7 @@
         pass
 
 
-class dummypipe(object):
+class dummypipe:
     def close(self):
         pass
 
--- a/tests/test-check-py3-compat.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-check-py3-compat.t	Wed May 04 18:17:44 2022 +0200
@@ -3,35 +3,7 @@
   $ . "$TESTDIR/helpers-testrepo.sh"
   $ cd "$TESTDIR"/..
 
-#if no-py3
-  $ testrepohg files 'set:(**.py)' \
-  > -X contrib/automation/ \
-  > -X contrib/packaging/hgpackaging/ \
-  > -X contrib/packaging/inno/ \
-  > -X contrib/packaging/packaging.py \
-  > -X contrib/packaging/wix/ \
-  > -X hgdemandimport/demandimportpy2.py \
-  > -X mercurial/thirdparty/cbor \
-  > | sed 's|\\|/|g' | xargs "$PYTHON" contrib/check-py3-compat.py
-  contrib/python-zstandard/setup.py not using absolute_import
-  contrib/python-zstandard/setup_zstd.py not using absolute_import
-  contrib/python-zstandard/tests/common.py not using absolute_import
-  contrib/python-zstandard/tests/test_buffer_util.py not using absolute_import
-  contrib/python-zstandard/tests/test_compressor.py not using absolute_import
-  contrib/python-zstandard/tests/test_compressor_fuzzing.py not using absolute_import
-  contrib/python-zstandard/tests/test_data_structures.py not using absolute_import
-  contrib/python-zstandard/tests/test_data_structures_fuzzing.py not using absolute_import
-  contrib/python-zstandard/tests/test_decompressor.py not using absolute_import
-  contrib/python-zstandard/tests/test_decompressor_fuzzing.py not using absolute_import
-  contrib/python-zstandard/tests/test_estimate_sizes.py not using absolute_import
-  contrib/python-zstandard/tests/test_module_attributes.py not using absolute_import
-  contrib/python-zstandard/tests/test_train_dictionary.py not using absolute_import
-  setup.py not using absolute_import
-#endif
-
-#if py3
   $ testrepohg files 'set:(**.py) - grep(pygments)' \
-  > -X hgdemandimport/demandimportpy2.py \
   > -X hgext/fsmonitor/pywatchman \
   > -X mercurial/cffi \
   > -X mercurial/thirdparty \
@@ -44,9 +16,8 @@
   mercurial/windows.py: error importing: <*Error> No module named 'msvcrt' (error at windows.py:*) (glob) (no-windows !)
   mercurial/posix.py: error importing: <*Error> No module named 'fcntl' (error at posix.py:*) (glob) (windows !)
   mercurial/scmposix.py: error importing: <*Error> No module named 'fcntl' (error at scmposix.py:*) (glob) (windows !)
-#endif
 
-#if py3 pygments
+#if pygments
   $ testrepohg files 'set:(**.py) and grep(pygments)' | sed 's|\\|/|g' \
   > | xargs "$PYTHON" contrib/check-py3-compat.py \
   > | sed 's/[0-9][0-9]*)$/*)/'
--- a/tests/test-check-pyflakes.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-check-pyflakes.t	Wed May 04 18:17:44 2022 +0200
@@ -15,13 +15,19 @@
 
   $ testrepohg locate 'set:**.py or grep("^#!.*python")' \
   > -X hgext/fsmonitor/pywatchman \
-  > -X mercurial/pycompat.py -X contrib/python-zstandard \
+  > -X contrib/python-zstandard \
   > -X mercurial/thirdparty \
   > 2>/dev/null \
   > | xargs "$PYTHON" -m pyflakes 2>/dev/null | "$TESTDIR/filterpyflakes.py"
   contrib/perf.py:*:* undefined name 'xrange' (glob) (?)
   mercurial/hgweb/server.py:*:* undefined name 'reload' (glob) (?)
-  mercurial/util.py:*:* undefined name 'file' (glob) (?)
-  mercurial/encoding.py:*:* undefined name 'localstr' (glob) (?)
-  tests/run-tests.py:*:* undefined name 'PermissionError' (glob) (?)
+  mercurial/pycompat.py:*:* 'codecs' imported but unused (glob)
+  mercurial/pycompat.py:*:* 'concurrent.futures' imported but unused (glob)
+  mercurial/pycompat.py:*:* 'http.client as httplib' imported but unused (glob)
+  mercurial/pycompat.py:*:* 'http.cookiejar as cookielib' imported but unused (glob)
+  mercurial/pycompat.py:*:* 'io' imported but unused (glob)
+  mercurial/pycompat.py:*:* 'queue' imported but unused (glob)
+  mercurial/pycompat.py:*:* 'socketserver' imported but unused (glob)
+  mercurial/pycompat.py:*:* 'xmlrpc.client as xmlrpclib' imported but unused (glob)
+  mercurial/util.py:*:* 'pickle' imported but unused (glob)
   
--- a/tests/test-check-pytype.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-check-pytype.t	Wed May 04 18:17:44 2022 +0200
@@ -30,7 +30,6 @@
 mercurial/testing/storage.py  # tons of [attribute-error]
 mercurial/ui.py               # [attribute-error], [wrong-arg-types]
 mercurial/unionrepo.py        # ui, svfs, unfiltered [attribute-error]
-mercurial/utils/memorytop.py  # not 3.6 compatible
 mercurial/win32.py            # [not-callable]
 mercurial/wireprotoframing.py # [unsupported-operands], [attribute-error], [import-error]
 mercurial/wireprotov1peer.py  # [attribute-error]
@@ -39,7 +38,7 @@
 TODO: use --no-cache on test server?  Caching the files locally helps during
 development, but may be a hinderance for CI testing.
 
-  $ pytype -V 3.6 --keep-going --jobs auto mercurial \
+  $ pytype -V 3.7 --keep-going --jobs auto mercurial \
   >    -x mercurial/bundlerepo.py \
   >    -x mercurial/context.py \
   >    -x mercurial/crecord.py \
@@ -62,7 +61,6 @@
   >    -x mercurial/thirdparty \
   >    -x mercurial/ui.py \
   >    -x mercurial/unionrepo.py \
-  >    -x mercurial/utils/memorytop.py \
   >    -x mercurial/win32.py \
   >    -x mercurial/wireprotoframing.py \
   >    -x mercurial/wireprotov1peer.py \
--- a/tests/test-chg.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-chg.t	Wed May 04 18:17:44 2022 +0200
@@ -132,7 +132,6 @@
   > EOF
 
   $ cat > $TESTTMP/fakepager.py <<EOF
-  > from __future__ import absolute_import
   > import sys
   > import time
   > for line in iter(sys.stdin.readline, ''):
--- a/tests/test-commandserver.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-commandserver.t	Wed May 04 18:17:44 2022 +0200
@@ -23,7 +23,6 @@
   $ hg init repo
   $ cd repo
 
-  >>> from __future__ import absolute_import
   >>> import os
   >>> import sys
   >>> from hgclient import bprint, check, readchannel, runcommand
--- a/tests/test-commit-interactive.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-commit-interactive.t	Wed May 04 18:17:44 2022 +0200
@@ -938,7 +938,6 @@
   $ export LANGUAGE
 
   $ cat > $TESTTMP/escape.py <<EOF
-  > from __future__ import absolute_import
   > from mercurial import (
   >     pycompat,
   > )
--- a/tests/test-commit.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-commit.t	Wed May 04 18:17:44 2022 +0200
@@ -645,7 +645,6 @@
   
 verify pathauditor blocks evil filepaths
   $ cat > evil-commit.py <<EOF
-  > from __future__ import absolute_import
   > from mercurial import context, hg, ui as uimod
   > notrc = u".h\u200cg".encode('utf-8') + b'/hgrc'
   > u = uimod.ui.load()
@@ -671,7 +670,6 @@
   $ hg rollback -f
   repository tip rolled back to revision 2 (undo commit)
   $ cat > evil-commit.py <<EOF
-  > from __future__ import absolute_import
   > from mercurial import context, hg, ui as uimod
   > notrc = b"HG~1/hgrc"
   > u = uimod.ui.load()
@@ -691,7 +689,6 @@
   $ hg rollback -f
   repository tip rolled back to revision 2 (undo commit)
   $ cat > evil-commit.py <<EOF
-  > from __future__ import absolute_import
   > from mercurial import context, hg, ui as uimod
   > notrc = b"HG8B6C~2/hgrc"
   > u = uimod.ui.load()
--- a/tests/test-config-env.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-config-env.py	Wed May 04 18:17:44 2022 +0200
@@ -1,6 +1,5 @@
 # Test the config layer generated by environment variables
 
-from __future__ import absolute_import, print_function
 
 import os
 
--- a/tests/test-context-metadata.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-context-metadata.t	Wed May 04 18:17:44 2022 +0200
@@ -12,7 +12,6 @@
   $ hg commit -m 'Remove A'
 
   $ cat > metaedit.py <<EOF
-  > from __future__ import absolute_import
   > from mercurial import context, pycompat, registrar
   > cmdtable = {}
   > command = registrar.command(cmdtable)
--- a/tests/test-context.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-context.py	Wed May 04 18:17:44 2022 +0200
@@ -1,4 +1,3 @@
-from __future__ import absolute_import, print_function
 import os
 import stat
 import sys
--- a/tests/test-contrib-check-code.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-contrib-check-code.t	Wed May 04 18:17:44 2022 +0200
@@ -51,12 +51,6 @@
   ./quote.py:5:
    > '"""', 42+1, """and
    missing whitespace in expression
-  ./classstyle.py:4:
-   > class oldstyle_class:
-   old-style class, use class foo(object)
-  ./classstyle.py:7:
-   > class empty():
-   class foo() creates old style object, use class foo(object)
   [1]
   $ cat > python3-compat.py << NO_CHECK_EOF
   > foo <> bar
--- a/tests/test-contrib-perf.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-contrib-perf.t	Wed May 04 18:17:44 2022 +0200
@@ -301,7 +301,6 @@
   malformatted run limit entry, missing "-": 500
   ! wall * comb * user * sys * (best of 5) (glob)
   $ hg perfparents --config perf.stub=no --config perf.run-limits='aaa-12, 0.000000001-5'
-  malformatted run limit entry, could not convert string to float: aaa: aaa-12 (no-py3 !)
   malformatted run limit entry, could not convert string to float: 'aaa': aaa-12 (py3 !)
   ! wall * comb * user * sys * (best of 5) (glob)
   $ hg perfparents --config perf.stub=no --config perf.run-limits='12-aaaaaa, 0.000000001-5'
--- a/tests/test-convert-clonebranches.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-convert-clonebranches.t	Wed May 04 18:17:44 2022 +0200
@@ -31,7 +31,6 @@
 Miss perl... sometimes
 
   $ cat > filter.py <<EOF
-  > from __future__ import absolute_import
   > import re
   > import sys
   > 
--- a/tests/test-convert-git.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-convert-git.t	Wed May 04 18:17:44 2022 +0200
@@ -435,7 +435,7 @@
   $ cd git-repo3-hg
   $ hg up -C
   1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ "$PYTHON" -c 'from __future__ import print_function; print(len(open("b", "rb").read()))'
+  $ "$PYTHON" -c 'print(len(open("b", "rb").read()))'
   4096
   $ cd ..
 
--- a/tests/test-debugcommands.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-debugcommands.t	Wed May 04 18:17:44 2022 +0200
@@ -212,7 +212,6 @@
    {
     "chainid": 1,
     "chainlen": 1,
-    "chainratio": 1.02325581395, (no-py3 !)
     "chainratio": 1.0232558139534884, (py3 !)
     "chainsize": 44,
     "compsize": 44,
@@ -249,7 +248,6 @@
    {
     "chainid": 3,
     "chainlen": 1,
-    "chainratio": 1.02325581395, (no-py3 !)
     "chainratio": 1.0232558139534884, (py3 !)
     "chainsize": 44,
     "compsize": 44,
@@ -289,7 +287,6 @@
    {
     "chainid": 1,
     "chainlen": 1,
-    "chainratio": 1.02325581395, (no-py3 !)
     "chainratio": 1.0232558139534884, (py3 !)
     "chainsize": 44,
     "compsize": 44,
@@ -326,7 +323,6 @@
    {
     "chainid": 3,
     "chainlen": 1,
-    "chainratio": 1.02325581395, (no-py3 !)
     "chainratio": 1.0232558139534884, (py3 !)
     "chainsize": 44,
     "compsize": 44,
@@ -574,7 +570,6 @@
 Test internal debugstacktrace command
 
   $ cat > debugstacktrace.py << EOF
-  > from __future__ import absolute_import
   > from mercurial import (
   >     util,
   > )
@@ -593,15 +588,15 @@
   > EOF
   $ "$PYTHON" debugstacktrace.py
   stacktrace at:
-   *debugstacktrace.py:16 in * (glob)
-   *debugstacktrace.py:9  in f (glob)
+   *debugstacktrace.py:15 in * (glob)
+   *debugstacktrace.py:8  in f (glob)
   hello from g at:
-   *debugstacktrace.py:16 in * (glob)
-   *debugstacktrace.py:10 in f (glob)
+   *debugstacktrace.py:15 in * (glob)
+   *debugstacktrace.py:9  in f (glob)
   hi ...
   from h hidden in g at:
-   *debugstacktrace.py:10 in f (glob)
-   *debugstacktrace.py:13 in g (glob)
+   *debugstacktrace.py:9  in f (glob)
+   *debugstacktrace.py:12 in g (glob)
 
 Test debugcapabilities command:
 
--- a/tests/test-demandimport.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-demandimport.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function
-
 from mercurial import demandimport
 
 demandimport.enable()
--- a/tests/test-dirs.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-dirs.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import unittest
 
 import silenttestrunner
--- a/tests/test-dirstate.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-dirstate.t	Wed May 04 18:17:44 2022 +0200
@@ -77,7 +77,6 @@
 coherent (issue4353)
 
   $ cat > ../dirstateexception.py <<EOF
-  > from __future__ import absolute_import
   > from mercurial import (
   >   error,
   >   extensions,
--- a/tests/test-dispatch.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-dispatch.py	Wed May 04 18:17:44 2022 +0200
@@ -1,4 +1,3 @@
-from __future__ import absolute_import, print_function
 import os
 import sys
 from mercurial import dispatch
--- a/tests/test-doctest.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-doctest.py	Wed May 04 18:17:44 2022 +0200
@@ -1,7 +1,5 @@
 # this is hack to make sure no escape characters are inserted into the output
 
-from __future__ import absolute_import
-from __future__ import print_function
 
 import doctest
 import os
--- a/tests/test-duplicateoptions.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-duplicateoptions.py	Wed May 04 18:17:44 2022 +0200
@@ -1,4 +1,3 @@
-from __future__ import absolute_import, print_function
 import os
 from mercurial import (
     commands,
--- a/tests/test-encoding-func.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-encoding-func.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import unittest
 
 from mercurial import encoding
--- a/tests/test-eol.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-eol.t	Wed May 04 18:17:44 2022 +0200
@@ -8,7 +8,6 @@
 Set up helpers
 
   $ cat > switch-eol.py <<'EOF'
-  > from __future__ import absolute_import
   > import os
   > import sys
   > try:
--- a/tests/test-extension.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-extension.t	Wed May 04 18:17:44 2022 +0200
@@ -150,7 +150,6 @@
 Check that extensions are loaded in phases:
 
   $ cat > foo.py <<EOF
-  > from __future__ import print_function
   > import os
   > from mercurial import exthelper
   > from mercurial.utils import procutil
@@ -295,7 +294,6 @@
   $ echo "s = 'libroot/mod/ambig.py'" > $TESTTMP/libroot/mod/ambig.py
 
   $ cat > $TESTTMP/libroot/mod/ambigabs.py <<NO_CHECK_EOF
-  > from __future__ import absolute_import, print_function
   > import ambig # should load "libroot/ambig.py"
   > s = ambig.s
   > NO_CHECK_EOF
@@ -309,23 +307,6 @@
   ambigabs.s=libroot/ambig.py
   $TESTTMP/a
 
-#if no-py3
-  $ cat > $TESTTMP/libroot/mod/ambigrel.py <<NO_CHECK_EOF
-  > from __future__ import print_function
-  > import ambig # should load "libroot/mod/ambig.py"
-  > s = ambig.s
-  > NO_CHECK_EOF
-  $ cat > loadrel.py <<NO_CHECK_EOF
-  > import mod.ambigrel as ambigrel
-  > def extsetup(ui):
-  >     print('ambigrel.s=%s' % ambigrel.s, flush=True)
-  > NO_CHECK_EOF
-  $ "$PYTHON" $TESTTMP/unflush.py loadrel.py
-  $ (PYTHONPATH=${PYTHONPATH}${PATHSEP}${TESTTMP}/libroot; hg --config extensions.loadrel=loadrel.py root)
-  ambigrel.s=libroot/mod/ambig.py
-  $TESTTMP/a
-#endif
-
 Check absolute/relative import of extension specific modules
 
   $ mkdir $TESTTMP/extroot
@@ -340,7 +321,6 @@
   > s = b'this is extroot.sub1.baz'
   > NO_CHECK_EOF
   $ cat > $TESTTMP/extroot/__init__.py <<NO_CHECK_EOF
-  > from __future__ import absolute_import
   > s = b'this is extroot.__init__'
   > from . import foo
   > def extsetup(ui):
@@ -377,39 +357,6 @@
   (extroot) import extroot.bar in func(): this is extroot.bar
   $TESTTMP/a
 
-#if no-py3
-  $ rm "$TESTTMP"/extroot/foo.*
-  $ rm -Rf "$TESTTMP/extroot/__pycache__"
-  $ cat > $TESTTMP/extroot/foo.py <<NO_CHECK_EOF
-  > # test relative import
-  > buf = []
-  > def func():
-  >     # "not locals" case
-  >     import bar
-  >     buf.append('import bar in func(): %s' % bar.s)
-  >     return '\n(extroot) '.join(buf)
-  > # "fromlist == ('*',)" case
-  > from bar import *
-  > buf.append('from bar import *: %s' % s)
-  > # "not fromlist" and "if '.' in name" case
-  > import sub1.baz
-  > buf.append('import sub1.baz: %s' % sub1.baz.s)
-  > # "not fromlist" and NOT "if '.' in name" case
-  > import sub1
-  > buf.append('import sub1: %s' % sub1.s)
-  > # NOT "not fromlist" and NOT "level != -1" case
-  > from bar import s
-  > buf.append('from bar import s: %s' % s)
-  > NO_CHECK_EOF
-  $ hg --config extensions.extroot=$TESTTMP/extroot root
-  (extroot) from bar import *: this is extroot.bar
-  (extroot) import sub1.baz: this is extroot.sub1.baz
-  (extroot) import sub1: this is extroot.sub1.__init__
-  (extroot) from bar import s: this is extroot.bar
-  (extroot) import bar in func(): this is extroot.bar
-  $TESTTMP/a
-#endif
-
 #if demandimport
 
 Examine whether module loading is delayed until actual referring, even
@@ -453,7 +400,6 @@
   > detail = b"this is extlibroot.recursedown.abs.used"
   > NO_CHECK_EOF
   $ cat > $TESTTMP/extlibroot/recursedown/abs/__init__.py <<NO_CHECK_EOF
-  > from __future__ import absolute_import
   > from extlibroot.recursedown.abs.used import detail
   > NO_CHECK_EOF
 
@@ -467,7 +413,6 @@
   > NO_CHECK_EOF
 
   $ cat > $TESTTMP/extlibroot/recursedown/__init__.py <<NO_CHECK_EOF
-  > from __future__ import absolute_import
   > from extlibroot.recursedown.abs import detail as absdetail
   > from .legacy import detail as legacydetail
   > NO_CHECK_EOF
@@ -481,11 +426,9 @@
   > detail = b"this is extlibroot.shadowing.used"
   > NO_CHECK_EOF
   $ cat > $TESTTMP/extlibroot/shadowing/proxied.py <<NO_CHECK_EOF
-  > from __future__ import absolute_import
   > from extlibroot.shadowing.used import detail
   > NO_CHECK_EOF
   $ cat > $TESTTMP/extlibroot/shadowing/__init__.py <<NO_CHECK_EOF
-  > from __future__ import absolute_import
   > from .used import detail as used
   > NO_CHECK_EOF
 
@@ -514,7 +457,6 @@
   > detail = b"this is absextroot.relimportee"
   > NO_CHECK_EOF
   $ cat > $TESTTMP/absextroot/xsub1/xsub2/relimporter.py <<NO_CHECK_EOF
-  > from __future__ import absolute_import
   > from mercurial import pycompat
   > from ... import relimportee
   > detail = b"this relimporter imports %r" % (
@@ -525,7 +467,6 @@
 runtime.
 
   $ cat > $TESTTMP/absextroot/absolute.py << NO_CHECK_EOF
-  > from __future__ import absolute_import
   > 
   > # import extension local modules absolutely (level = 0)
   > from absextroot.xsub1.xsub2 import used, unused
@@ -539,7 +480,6 @@
   > NO_CHECK_EOF
 
   $ cat > $TESTTMP/absextroot/relative.py << NO_CHECK_EOF
-  > from __future__ import absolute_import
   > 
   > # import extension local modules relatively (level == 1)
   > from .xsub1.xsub2 import used, unused
@@ -559,7 +499,6 @@
 Setup main procedure of extension.
 
   $ cat > $TESTTMP/absextroot/__init__.py <<NO_CHECK_EOF
-  > from __future__ import absolute_import
   > from mercurial import registrar
   > cmdtable = {}
   > command = registrar.command(cmdtable)
@@ -1925,7 +1864,6 @@
   $ hg init $TESTTMP/opt-unicode-default
 
   $ cat > $TESTTMP/test_unicode_default_value.py << EOF
-  > from __future__ import print_function
   > from mercurial import registrar
   > cmdtable = {}
   > command = registrar.command(cmdtable)
@@ -1940,7 +1878,6 @@
   > EOF
   $ hg -R $TESTTMP/opt-unicode-default dummy
   *** failed to import extension "test_unicode_default_value" from $TESTTMP/test_unicode_default_value.py: unicode 'value' found in cmdtable.dummy (py3 !)
-  *** failed to import extension "test_unicode_default_value" from $TESTTMP/test_unicode_default_value.py: unicode u'value' found in cmdtable.dummy (no-py3 !)
   *** (use b'' to make it byte string)
   hg: unknown command 'dummy'
   (did you mean summary?)
--- a/tests/test-extensions-wrapfunction.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-extensions-wrapfunction.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function
-
 from mercurial import extensions
 
 
@@ -18,7 +16,7 @@
 wrappers = [genwrapper(i) for i in range(5)]
 
 
-class dummyclass(object):
+class dummyclass:
     def getstack(self):
         return ['orig']
 
@@ -69,7 +67,7 @@
 print('context manager', dummy.getstack())
 
 # Wrap callable object which has no __name__
-class callableobj(object):
+class callableobj:
     def __call__(self):
         return ['orig']
 
--- a/tests/test-fastannotate-hg.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-fastannotate-hg.t	Wed May 04 18:17:44 2022 +0200
@@ -481,7 +481,6 @@
 and its ancestor by overriding "repo._filecommit".
 
   $ cat > ../legacyrepo.py <<EOF
-  > from __future__ import absolute_import
   > from mercurial import commit, error, extensions
   > def _filecommit(orig, repo, fctx, manifest1, manifest2,
   >                 linkrev, tr, includecopymeta, ms):
--- a/tests/test-fastannotate-revmap.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-fastannotate-revmap.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function
-
 import os
 import tempfile
 
@@ -171,7 +169,7 @@
     os.unlink(path2)
 
 
-class fakefctx(object):
+class fakefctx:
     def __init__(self, node, path=None):
         self._node = node
         self._path = path
--- a/tests/test-filebranch.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-filebranch.t	Wed May 04 18:17:44 2022 +0200
@@ -2,7 +2,6 @@
 when we do a merge.
 
   $ cat <<EOF > merge
-  > from __future__ import print_function
   > import sys, os
   > print("merging for", os.path.basename(sys.argv[1]))
   > EOF
--- a/tests/test-filecache.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-filecache.py	Wed May 04 18:17:44 2022 +0200
@@ -1,4 +1,3 @@
-from __future__ import absolute_import, print_function
 import os
 import stat
 import subprocess
@@ -36,11 +35,11 @@
     xrange = range
 
 
-class fakerepo(object):
+class fakerepo:
     def __init__(self):
         self._filecache = {}
 
-    class fakevfs(object):
+    class fakevfs:
         def join(self, p):
             return p
 
--- a/tests/test-filelog.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-filelog.py	Wed May 04 18:17:44 2022 +0200
@@ -2,7 +2,6 @@
 """
 Tests the behavior of filelog w.r.t. data starting with '\1\n'
 """
-from __future__ import absolute_import, print_function
 
 from mercurial.node import hex
 from mercurial import (
--- a/tests/test-flagprocessor.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-flagprocessor.t	Wed May 04 18:17:44 2022 +0200
@@ -214,12 +214,10 @@
     File "mercurial.revlogutils.flagutil", line *, in insertflagprocessor (glob) (pyoxidizer !)
       raise error.Abort(msg)
   mercurial.error.Abort: cannot register multiple processors on flag '0x8'. (py3 !)
-  Abort: cannot register multiple processors on flag '0x8'. (no-py3 !)
   *** failed to set up extension duplicate: cannot register multiple processors on flag '0x8'.
   $ hg st 2>&1 | egrep 'cannot register multiple processors|flagprocessorext'
     File "*/tests/flagprocessorext.py", line *, in extsetup (glob)
   mercurial.error.Abort: cannot register multiple processors on flag '0x8'. (py3 !)
-  Abort: cannot register multiple processors on flag '0x8'. (no-py3 !)
   *** failed to set up extension duplicate: cannot register multiple processors on flag '0x8'.
     File "*/tests/flagprocessorext.py", line *, in b64decode (glob)
 
--- a/tests/test-fncache.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-fncache.t	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 does not break
 
   $ cat > chunksize.py <<EOF
-  > from __future__ import absolute_import
   > from mercurial import store
   > store.fncache_chunksize = 1
   > EOF
@@ -232,7 +231,6 @@
 Aborting lock does not prevent fncache writes
 
   $ cat > exceptionext.py <<EOF
-  > from __future__ import absolute_import
   > import os
   > from mercurial import commands, error, extensions
   > 
@@ -279,7 +277,6 @@
 Aborting transaction prevents fncache change
 
   $ cat > ../exceptionext.py <<EOF
-  > from __future__ import absolute_import
   > import os
   > from mercurial import commands, error, extensions, localrepo
   > 
@@ -315,7 +312,6 @@
 Aborted transactions can be recovered later
 
   $ cat > ../exceptionext.py <<EOF
-  > from __future__ import absolute_import
   > import os
   > from mercurial import (
   >   commands,
@@ -483,7 +479,6 @@
 changesets that only contain changes to existing files:
 
   $ cat > fncacheloadwarn.py << EOF
-  > from __future__ import absolute_import
   > from mercurial import extensions, localrepo
   > 
   > def extsetup(ui):
--- a/tests/test-generaldelta.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-generaldelta.t	Wed May 04 18:17:44 2022 +0200
@@ -25,7 +25,6 @@
   > done
 
   $ cd ..
-  >>> from __future__ import print_function
   >>> import os
   >>> regsize = os.stat("repo/.hg/store/00manifest.i").st_size
   >>> gdsize = os.stat("gdrepo/.hg/store/00manifest.i").st_size
--- a/tests/test-hardlinks.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-hardlinks.t	Wed May 04 18:17:44 2022 +0200
@@ -1,7 +1,6 @@
 #require hardlink reporevlogstore
 
   $ cat > nlinks.py <<EOF
-  > from __future__ import print_function
   > import sys
   > from mercurial import pycompat, util
   > for f in sorted(sys.stdin.readlines()):
@@ -17,7 +16,6 @@
 Some implementations of cp can't create hardlinks (replaces 'cp -al' on Linux):
 
   $ cat > linkcp.py <<EOF
-  > from __future__ import absolute_import
   > import sys
   > from mercurial import pycompat, util
   > util.copyfiles(pycompat.fsencode(sys.argv[1]),
--- a/tests/test-hashutil.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-hashutil.py	Wed May 04 18:17:44 2022 +0200
@@ -1,6 +1,5 @@
 # Tests to ensure that sha1dc.sha1 is exactly a drop-in for
 # hashlib.sha1 for our needs.
-from __future__ import absolute_import
 
 import hashlib
 import unittest
@@ -13,7 +12,7 @@
     sha1dc = None
 
 
-class hashertestsbase(object):
+class hashertestsbase:
     def test_basic_hash(self):
         h = self.hasher()
         h.update(b'foo')
--- a/tests/test-help.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-help.t	Wed May 04 18:17:44 2022 +0200
@@ -1790,7 +1790,6 @@
   > 
   > This paragraph is never omitted, too (for extension)
   > '''
-  > from __future__ import absolute_import
   > from mercurial import commands, help
   > testtopic = br"""This paragraph is never omitted (for topic).
   > 
--- a/tests/test-hg-parseurl.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-hg-parseurl.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function
-
 import unittest
 
 from mercurial.utils import urlutil
--- a/tests/test-hgrc.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-hgrc.t	Wed May 04 18:17:44 2022 +0200
@@ -71,7 +71,7 @@
   config error at $TESTTMP/hgrc:2: unexpected leading whitespace:   x = y
   [255]
 
-  $ "$PYTHON" -c "from __future__ import print_function; print('[foo]\nbar = a\n b\n c \n  de\n fg \nbaz = bif cb \n')" \
+  $ "$PYTHON" -c "print('[foo]\nbar = a\n b\n c \n  de\n fg \nbaz = bif cb \n')" \
   > > $HGRC
   $ hg showconfig foo
   foo.bar=a\nb\nc\nde\nfg
--- a/tests/test-hgweb-auth.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-hgweb-auth.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function
-
 from mercurial import demandimport
 
 demandimport.enable()
--- a/tests/test-hgweb-no-path-info.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-hgweb-no-path-info.t	Wed May 04 18:17:44 2022 +0200
@@ -15,7 +15,6 @@
   summary:     test
   
   $ cat > request.py <<EOF
-  > from __future__ import absolute_import
   > import os
   > import sys
   > from mercurial import (
--- a/tests/test-hgweb-no-request-uri.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-hgweb-no-request-uri.t	Wed May 04 18:17:44 2022 +0200
@@ -15,7 +15,6 @@
   summary:     test
   
   $ cat > request.py <<EOF
-  > from __future__ import absolute_import
   > import os
   > import sys
   > from mercurial import (
--- a/tests/test-hgweb-non-interactive.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-hgweb-non-interactive.t	Wed May 04 18:17:44 2022 +0200
@@ -7,7 +7,6 @@
   $ hg add bar
   $ hg commit -m "test"
   $ cat > request.py <<EOF
-  > from __future__ import absolute_import
   > import os
   > import sys
   > from mercurial import (
--- a/tests/test-hgweb.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-hgweb.t	Wed May 04 18:17:44 2022 +0200
@@ -329,7 +329,7 @@
 
 Test the access/error files are opened in append mode
 
-  $ "$PYTHON" -c "from __future__ import print_function; print(len(open('access.log', 'rb').readlines()), 'log lines written')"
+  $ "$PYTHON" -c "print(len(open('access.log', 'rb').readlines()), 'log lines written')"
   14 log lines written
 
 static file
--- a/tests/test-hgwebdir-gc.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-hgwebdir-gc.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import os
 from mercurial.hgweb import hgwebdir_mod
 
--- a/tests/test-hgwebdir-paths.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-hgwebdir-paths.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import os
 from mercurial import (
     hg,
--- a/tests/test-hook.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-hook.t	Wed May 04 18:17:44 2022 +0200
@@ -831,7 +831,6 @@
   $ cd "$TESTTMP/b"
 
   $ cat > hooktests.py <<EOF
-  > from __future__ import print_function
   > from mercurial import (
   >     error,
   >     pycompat,
@@ -979,7 +978,6 @@
   Traceback (most recent call last): (py3 !)
   SyntaxError: * (glob) (py3 !)
   Traceback (most recent call last):
-  ImportError: No module named hgext_syntaxerror (no-py3 !)
   ImportError: No module named 'hgext_syntaxerror' (py3 no-py36 !)
   ModuleNotFoundError: No module named 'hgext_syntaxerror' (py36 !)
   Traceback (most recent call last):
@@ -988,7 +986,6 @@
   ImportError: No module named 'hgext_syntaxerror' (py3 no-py36 !)
   ModuleNotFoundError: No module named 'hgext_syntaxerror' (py36 !)
   Traceback (most recent call last): (py3 !)
-  HookLoadError: preoutgoing.syntaxerror hook is invalid: import of "syntaxerror" failed (no-py3 !)
       raise error.HookLoadError( (py38 !)
   mercurial.error.HookLoadError: preoutgoing.syntaxerror hook is invalid: import of "syntaxerror" failed (py3 !)
   abort: preoutgoing.syntaxerror hook is invalid: import of "syntaxerror" failed
@@ -1123,7 +1120,6 @@
 
   $ hg id
   loading pre-identify.npmd hook failed:
-  abort: No module named repo (no-py3 !)
   abort: No module named 'repo' (py3 !)
   [255]
 
@@ -1144,7 +1140,6 @@
   $ hg --traceback commit -ma 2>&1 | egrep '^exception|ImportError|ModuleNotFoundError|Traceback|HookLoadError|abort'
   exception from first failed import attempt:
   Traceback (most recent call last):
-  ImportError: No module named somebogusmodule (no-py3 !)
   ImportError: No module named 'somebogusmodule' (py3 no-py36 !)
   ModuleNotFoundError: No module named 'somebogusmodule' (py36 !)
   exception from second failed import attempt:
@@ -1158,11 +1153,9 @@
   ImportError: No module named 'somebogusmodule' (py3 no-py36 !)
   ModuleNotFoundError: No module named 'somebogusmodule' (py36 !)
   Traceback (most recent call last):
-  ImportError: No module named hgext_importfail (no-py3 !)
   ImportError: No module named 'hgext_importfail' (py3 no-py36 !)
   ModuleNotFoundError: No module named 'hgext_importfail' (py36 !)
   Traceback (most recent call last):
-  HookLoadError: precommit.importfail hook is invalid: import of "importfail" failed (no-py3 !)
       raise error.HookLoadError( (py38 !)
   mercurial.error.HookLoadError: precommit.importfail hook is invalid: import of "importfail" failed (py3 !)
   abort: precommit.importfail hook is invalid: import of "importfail" failed
--- a/tests/test-http-bad-server.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-http-bad-server.t	Wed May 04 18:17:44 2022 +0200
@@ -134,13 +134,6 @@
   sendall(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob) (py36 !)
   write(160) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.1\r\nContent-Length: *\r\n\r\n (glob) (py3 no-py36 !)
   write(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob) (py3 no-py36 !)
-  write(36) -> HTTP/1.1 200 Script output follows\r\n (no-py3 !)
-  write(23) -> Server: badhttpserver\r\n (no-py3 !)
-  write(37) -> Date: $HTTP_DATE$\r\n (no-py3 !)
-  write(41) -> Content-Type: application/mercurial-0.1\r\n (no-py3 !)
-  write(21) -> Content-Length: *\r\n (glob) (no-py3 !)
-  write(2) -> \r\n (no-py3 !)
-  write(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob) (no-py3 !)
   readline(~) -> (26) GET /?cmd=batch HTTP/1.1\r\n (glob)
   readline(*) -> (1?) Accept-Encoding* (glob)
   read limit reached; closing socket
@@ -183,13 +176,6 @@
   sendall(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob) (py36 !)
   write(160) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.1\r\nContent-Length: *\r\n\r\n (glob) (py3 no-py36 !)
   write(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob) (py3 no-py36 !)
-  write(36) -> HTTP/1.1 200 Script output follows\r\n (no-py3 !)
-  write(23) -> Server: badhttpserver\r\n (no-py3 !)
-  write(37) -> Date: $HTTP_DATE$\r\n (no-py3 !)
-  write(41) -> Content-Type: application/mercurial-0.1\r\n (no-py3 !)
-  write(21) -> Content-Length: *\r\n (glob) (no-py3 !)
-  write(2) -> \r\n (no-py3 !)
-  write(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob) (no-py3 !)
   readline(~) -> (26) GET /?cmd=batch HTTP/1.1\r\n (glob)
   readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
   readline(*) -> (29) vary: X-HgArg-1,X-HgProto-1\r\n (glob)
@@ -209,13 +195,6 @@
   sendall(42) -> 96ee1d7354c4ad7372047672c36a1f561e3a6a4c\n; (py36 !)
   write(159) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.1\r\nContent-Length: 42\r\n\r\n (py3 no-py36 !)
   write(42) -> 96ee1d7354c4ad7372047672c36a1f561e3a6a4c\n; (py3 no-py36 !)
-  write(36) -> HTTP/1.1 200 Script output follows\r\n (no-py3 !)
-  write(23) -> Server: badhttpserver\r\n (no-py3 !)
-  write(37) -> Date: $HTTP_DATE$\r\n (no-py3 !)
-  write(41) -> Content-Type: application/mercurial-0.1\r\n (no-py3 !)
-  write(20) -> Content-Length: 42\r\n (no-py3 !)
-  write(2) -> \r\n (no-py3 !)
-  write(42) -> 96ee1d7354c4ad7372047672c36a1f561e3a6a4c\n; (no-py3 !)
   readline(24 from ~) -> (*) GET /?cmd=getbundle HTTP* (glob)
   read limit reached; closing socket
   readline(~) -> (30) GET /?cmd=getbundle HTTP/1.1\r\n
@@ -253,13 +232,6 @@
   sendall(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx httppostargs known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob) (py36 !)
   write(160) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.1\r\nContent-Length: *\r\n\r\n (glob) (py3 no-py36 !)
   write(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx httppostargs known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob) (py3 no-py36 !)
-  write(36) -> HTTP/1.1 200 Script output follows\r\n (no-py3 !)
-  write(23) -> Server: badhttpserver\r\n (no-py3 !)
-  write(37) -> Date: $HTTP_DATE$\r\n (no-py3 !)
-  write(41) -> Content-Type: application/mercurial-0.1\r\n (no-py3 !)
-  write(21) -> Content-Length: *\r\n (glob) (no-py3 !)
-  write(2) -> \r\n (no-py3 !)
-  write(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx httppostargs known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob) (no-py3 !)
   readline(~) -> (27) POST /?cmd=batch HTTP/1.1\r\n (glob)
   readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
   readline(*) -> (41) content-type: application/mercurial-0.1\r\n (glob)
@@ -312,7 +284,6 @@
   readline(*) -> (2) \r\n (glob)
   sendall(1 from 160) -> (0) H (py36 !)
   write(1 from 160) -> (0) H (py3 no-py36 !)
-  write(1 from 36) -> (0) H (no-py3 !)
   write limit reached; closing socket
   $LOCALIP - - [$ERRDATE$] Exception happened during processing request '/?cmd=capabilities': (glob)
   Traceback (most recent call last):
@@ -348,13 +319,6 @@
   sendall(20 from *) -> (0) batch branchmap bund (glob) (py36 !)
   write(160) -> (20) HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.1\r\nContent-Length: *\r\n\r\n (glob) (py3 no-py36 !)
   write(20 from *) -> (0) batch branchmap bund (glob) (py3 no-py36 !)
-  write(36) -> HTTP/1.1 200 Script output follows\r\n (no-py3 !)
-  write(23) -> Server: badhttpserver\r\n (no-py3 !)
-  write(37) -> Date: $HTTP_DATE$\r\n (no-py3 !)
-  write(41) -> Content-Type: application/mercurial-0.1\r\n (no-py3 !)
-  write(21) -> Content-Length: *\r\n (glob) (no-py3 !)
-  write(2) -> \r\n (no-py3 !)
-  write(20 from *) -> (0) batch branchmap bund (glob) (no-py3 !)
   write limit reached; closing socket
   $LOCALIP - - [$ERRDATE$] Exception happened during processing request '/?cmd=capabilities': (glob)
   Traceback (most recent call last):
@@ -394,13 +358,6 @@
   sendall(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob) (py36 !)
   write(160) -> (568) HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.1\r\nContent-Length: *\r\n\r\n (glob) (py3 no-py36 !)
   write(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob) (py3 no-py36 !)
-  write(36) -> HTTP/1.1 200 Script output follows\r\n (no-py3 !)
-  write(23) -> Server: badhttpserver\r\n (no-py3 !)
-  write(37) -> Date: $HTTP_DATE$\r\n (no-py3 !)
-  write(41) -> Content-Type: application/mercurial-0.1\r\n (no-py3 !)
-  write(21) -> Content-Length: *\r\n (glob) (no-py3 !)
-  write(2) -> \r\n (no-py3 !)
-  write(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob) (no-py3 !)
   readline(~) -> (26) GET /?cmd=batch HTTP/1.1\r\n
   readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
   readline(*) -> (29) vary: X-HgArg-1,X-HgProto-1\r\n (glob)
@@ -412,10 +369,6 @@
   readline(*) -> (2) \r\n (glob)
   sendall(118 from 159) -> (0) HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: applicat (py36 !)
   write(118 from 159) -> (0) HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: applicat (py3 no-py36 !)
-  write(36) -> HTTP/1.1 200 Script output follows\r\n (no-py3 !)
-  write(23) -> Server: badhttpserver\r\n (no-py3 !)
-  write(37) -> Date: $HTTP_DATE$\r\n (no-py3 !)
-  write(22 from 41) -> (0) Content-Type: applicat (no-py3 !)
   write limit reached; closing socket
   $LOCALIP - - [$ERRDATE$] Exception happened during processing request '/?cmd=batch': (glob)
   Traceback (most recent call last):
@@ -455,13 +408,6 @@
   sendall(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob) (py36 !)
   write(160) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.1\r\nContent-Length: *\r\n\r\n (glob) (py3 no-py36 !)
   write(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob) (py3 no-py36 !)
-  write(36) -> HTTP/1.1 200 Script output follows\r\n (no-py3 !)
-  write(23) -> Server: badhttpserver\r\n (no-py3 !)
-  write(37) -> Date: $HTTP_DATE$\r\n (no-py3 !)
-  write(41) -> Content-Type: application/mercurial-0.1\r\n (no-py3 !)
-  write(21) -> Content-Length: *\r\n (glob) (no-py3 !)
-  write(2) -> \r\n (no-py3 !)
-  write(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob) (no-py3 !)
   readline(~) -> (26) GET /?cmd=batch HTTP/1.1\r\n
   readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
   readline(*) -> (29) vary: X-HgArg-1,X-HgProto-1\r\n (glob)
@@ -475,13 +421,6 @@
   sendall(24 from 42) -> (0) 96ee1d7354c4ad7372047672 (py36 !)
   write(159) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.1\r\nContent-Length: 42\r\n\r\n (py3 no-py36 !)
   write(24 from 42) -> (0) 96ee1d7354c4ad7372047672 (py3 no-py36 !)
-  write(36) -> HTTP/1.1 200 Script output follows\r\n (no-py3 !)
-  write(23) -> Server: badhttpserver\r\n (no-py3 !)
-  write(37) -> Date: $HTTP_DATE$\r\n (no-py3 !)
-  write(41) -> Content-Type: application/mercurial-0.1\r\n (no-py3 !)
-  write(20) -> Content-Length: 42\r\n (no-py3 !)
-  write(2) -> \r\n (no-py3 !)
-  write(24 from 42) -> (0) 96ee1d7354c4ad7372047672 (no-py3 !)
   write limit reached; closing socket
   $LOCALIP - - [$ERRDATE$] Exception happened during processing request '/?cmd=batch': (glob)
   Traceback (most recent call last):
@@ -522,13 +461,6 @@
   sendall(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob) (py36 !)
   write(160) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.1\r\nContent-Length: *\r\n\r\n (glob) (py3 no-py36 !)
   write(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob) (py3 no-py36 !)
-  write(36) -> HTTP/1.1 200 Script output follows\r\n (no-py3 !)
-  write(23) -> Server: badhttpserver\r\n (no-py3 !)
-  write(37) -> Date: $HTTP_DATE$\r\n (no-py3 !)
-  write(41) -> Content-Type: application/mercurial-0.1\r\n (no-py3 !)
-  write(21) -> Content-Length: *\r\n (glob) (no-py3 !)
-  write(2) -> \r\n (no-py3 !)
-  write(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob) (no-py3 !)
   readline(~) -> (26) GET /?cmd=batch HTTP/1.1\r\n
   readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
   readline(*) -> (29) vary: X-HgArg-1,X-HgProto-1\r\n (glob)
@@ -542,13 +474,6 @@
   sendall(42) -> 96ee1d7354c4ad7372047672c36a1f561e3a6a4c\n; (py36 !)
   write(159) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.1\r\nContent-Length: 42\r\n\r\n (py3 no-py36 !)
   write(42) -> 96ee1d7354c4ad7372047672c36a1f561e3a6a4c\n; (py3 no-py36 !)
-  write(36) -> HTTP/1.1 200 Script output follows\r\n (no-py3 !)
-  write(23) -> Server: badhttpserver\r\n (no-py3 !)
-  write(37) -> Date: $HTTP_DATE$\r\n (no-py3 !)
-  write(41) -> Content-Type: application/mercurial-0.1\r\n (no-py3 !)
-  write(20) -> Content-Length: 42\r\n (no-py3 !)
-  write(2) -> \r\n (no-py3 !)
-  write(42) -> 96ee1d7354c4ad7372047672c36a1f561e3a6a4c\n; (no-py3 !)
   readline(~) -> (30) GET /?cmd=getbundle HTTP/1.1\r\n
   readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
   readline(*) -> (29) vary: X-HgArg-1,X-HgProto-1\r\n (glob)
@@ -560,10 +485,6 @@
   readline(*) -> (2) \r\n (glob)
   sendall(129 from 167) -> (0) HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercuri (py36 !)
   write(129 from 167) -> (0) HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercuri (py3 no-py36 !)
-  write(36) -> HTTP/1.1 200 Script output follows\r\n (no-py3 !)
-  write(23) -> Server: badhttpserver\r\n (no-py3 !)
-  write(37) -> Date: $HTTP_DATE$\r\n (no-py3 !)
-  write(33 from 41) -> (0) Content-Type: application/mercuri (no-py3 !)
   write limit reached; closing socket
   $LOCALIP - - [$ERRDATE$] Exception happened during processing request '/?cmd=getbundle': (glob)
   Traceback (most recent call last):
@@ -638,13 +559,6 @@
   sendall(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob) (py36 !)
   write(160) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.1\r\nContent-Length: *\r\n\r\n (glob) (py3 no-py36 !)
   write(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob) (py3 no-py36 !)
-  write(36) -> HTTP/1.1 200 Script output follows\r\n (no-py3 !)
-  write(23) -> Server: badhttpserver\r\n (no-py3 !)
-  write(37) -> Date: $HTTP_DATE$\r\n (no-py3 !)
-  write(41) -> Content-Type: application/mercurial-0.1\r\n (no-py3 !)
-  write(21) -> Content-Length: *\r\n (glob) (no-py3 !)
-  write(2) -> \r\n (no-py3 !)
-  write(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob) (no-py3 !)
   readline(~) -> (26) GET /?cmd=batch HTTP/1.1\r\n
   readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
   readline(*) -> (29) vary: X-HgArg-1,X-HgProto-1\r\n (glob)
@@ -658,13 +572,6 @@
   sendall(42) -> 96ee1d7354c4ad7372047672c36a1f561e3a6a4c\n; (py36 !)
   write(159) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.1\r\nContent-Length: 42\r\n\r\n (py3 no-py36 !)
   write(42) -> 96ee1d7354c4ad7372047672c36a1f561e3a6a4c\n; (py3 no-py36 !)
-  write(36) -> HTTP/1.1 200 Script output follows\r\n (no-py3 !)
-  write(23) -> Server: badhttpserver\r\n (no-py3 !)
-  write(37) -> Date: $HTTP_DATE$\r\n (no-py3 !)
-  write(41) -> Content-Type: application/mercurial-0.1\r\n (no-py3 !)
-  write(20) -> Content-Length: 42\r\n (no-py3 !)
-  write(2) -> \r\n (no-py3 !)
-  write(42) -> 96ee1d7354c4ad7372047672c36a1f561e3a6a4c\n; (no-py3 !)
   readline(~) -> (30) GET /?cmd=getbundle HTTP/1.1\r\n
   readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
   readline(*) -> (29) vary: X-HgArg-1,X-HgProto-1\r\n (glob)
@@ -676,12 +583,6 @@
   readline(*) -> (2) \r\n (glob)
   sendall(167 from 167) -> (0) HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.2\r\nTransfer-Encoding: chunked\r\n\r\n (py36 !)
   write(167 from 167) -> (0) HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.2\r\nTransfer-Encoding: chunked\r\n\r\n (py3 no-py36 !)
-  write(36) -> HTTP/1.1 200 Script output follows\r\n (no-py3 !)
-  write(23) -> Server: badhttpserver\r\n (no-py3 !)
-  write(37) -> Date: $HTTP_DATE$\r\n (no-py3 !)
-  write(41) -> Content-Type: application/mercurial-0.2\r\n (no-py3 !)
-  write(28) -> Transfer-Encoding: chunked\r\n (no-py3 !)
-  write(2 from 2) -> (0) \r\n (no-py3 !)
   write limit reached; closing socket
   $LOCALIP - - [$ERRDATE$] Exception happened during processing request '/?cmd=getbundle': (glob)
   Traceback (most recent call last):
@@ -718,13 +619,6 @@
   sendall(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob) (py36 !)
   write(160) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.1\r\nContent-Length: *\r\n\r\n (glob) (py3 no-py36 !)
   write(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob) (py3 no-py36 !)
-  write(36) -> HTTP/1.1 200 Script output follows\r\n (no-py3 !)
-  write(23) -> Server: badhttpserver\r\n (no-py3 !)
-  write(37) -> Date: $HTTP_DATE$\r\n (no-py3 !)
-  write(41) -> Content-Type: application/mercurial-0.1\r\n (no-py3 !)
-  write(21) -> Content-Length: *\r\n (glob) (no-py3 !)
-  write(2) -> \r\n (no-py3 !)
-  write(*) -> batch branchmap $USUAL_BUNDLE2_CAPS_NO_PHASES$ changegroupsubset compression=none getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=* unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (glob) (no-py3 !)
   readline(~) -> (26) GET /?cmd=batch HTTP/1.1\r\n
   readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
   readline(*) -> (29) vary: X-HgArg-1,X-HgProto-1\r\n (glob)
@@ -737,13 +631,6 @@
   sendall(159) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.1\r\nContent-Length: 42\r\n\r\n (py36 !)
   sendall(42) -> 96ee1d7354c4ad7372047672c36a1f561e3a6a4c\n; (py36 !)
   write(159) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.1\r\nContent-Length: 42\r\n\r\n (py3 no-py36 !)
-  write(36) -> HTTP/1.1 200 Script output follows\r\n (no-py3 !)
-  write(23) -> Server: badhttpserver\r\n (no-py3 !)
-  write(37) -> Date: $HTTP_DATE$\r\n (no-py3 !)
-  write(41) -> Content-Type: application/mercurial-0.1\r\n (no-py3 !)
-  write(20) -> Content-Length: 42\r\n (no-py3 !)
-  write(2) -> \r\n (no-py3 !)
-  write(42) -> 96ee1d7354c4ad7372047672c36a1f561e3a6a4c\n; (no-py3 !)
   readline(~) -> (30) GET /?cmd=getbundle HTTP/1.1\r\n
   readline(*) -> (27) Accept-Encoding: identity\r\n (glob)
   readline(*) -> (29) vary: X-HgArg-1,X-HgProto-1\r\n (glob)
@@ -758,15 +645,6 @@
   sendall(9) -> 4\r\nnone\r\n (py36 !)
   sendall(9 from 9) -> (0) 4\r\nHG20\r\n (py36 !)
   write(167) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.2\r\nTransfer-Encoding: chunked\r\n\r\n (py3 no-py36 !)
-  write(36) -> HTTP/1.1 200 Script output follows\r\n (no-py3 !)
-  write(23) -> Server: badhttpserver\r\n (no-py3 !)
-  write(37) -> Date: $HTTP_DATE$\r\n (no-py3 !)
-  write(41) -> Content-Type: application/mercurial-0.2\r\n (no-py3 !)
-  write(28) -> Transfer-Encoding: chunked\r\n (no-py3 !)
-  write(2) -> \r\n (no-py3 !)
-  write(6) -> 1\\r\\n\x04\\r\\n (esc) (no-py3 !)
-  write(9) -> 4\r\nnone\r\n (no-py3 !)
-  write(9 from 9) -> (0) 4\r\nHG20\r\n (no-py3 !)
   write limit reached; closing socket
   $LOCALIP - - [$ERRDATE$] Exception happened during processing request '/?cmd=getbundle': (glob)
   Traceback (most recent call last):
@@ -786,7 +664,6 @@
   $ hg clone http://localhost:$HGPORT/ clone
   requesting all changes
   abort: HTTP request error (incomplete response) (py3 !)
-  abort: HTTP request error (incomplete response; expected 4 bytes got 3) (no-py3 !)
   (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator)
   [255]
 
@@ -808,9 +685,6 @@
   $ "$PYTHON" $TESTDIR/filtertraceback.py < error.log | tail -11
   readline(~) -> (2) \r\n (py3 !)
   write(167) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.2\r\nTransfer-Encoding: chunked\r\n\r\n (py3 !)
-  write(41) -> Content-Type: application/mercurial-0.2\r\n (no-py3 !)
-  write(28) -> Transfer-Encoding: chunked\r\n (no-py3 !)
-  write(2) -> \r\n (no-py3 !)
   write(6) -> 1\\r\\n\x04\\r\\n (esc)
   write(9) -> 4\r\nnone\r\n
   write(6 from 9) -> (0) 4\r\nHG2
@@ -834,7 +708,6 @@
   $ hg clone http://localhost:$HGPORT/ clone
   requesting all changes
   abort: HTTP request error (incomplete response) (py3 !)
-  abort: HTTP request error (incomplete response; expected 4 bytes got 3) (no-py3 !)
   (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator)
   [255]
 
@@ -858,8 +731,6 @@
   readline(~) -> (2) \r\n (py3 !)
   write(167) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.2\r\nTransfer-Encoding: chunked\r\n\r\n (py3 !)
   write(41) -> Content-Type: application/mercurial-0.2\r\n
-  write(28) -> Transfer-Encoding: chunked\r\n (no-py3 !)
-  write(2) -> \r\n (no-py3 !)
   write(6) -> 1\\r\\n\x04\\r\\n (esc)
   write(9) -> 4\r\nnone\r\n
   write(9) -> 4\r\nHG20\r\n
@@ -907,8 +778,6 @@
   readline(~) -> (2) \r\n (py3 !)
   write(167) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.2\r\nTransfer-Encoding: chunked\r\n\r\n (py3 !)
   write(41) -> Content-Type: application/mercurial-0.2\r\n
-  write(28) -> Transfer-Encoding: chunked\r\n (no-py3 !)
-  write(2) -> \r\n (no-py3 !)
   write(6) -> 1\\r\\n\x04\\r\\n (esc)
   write(9) -> 4\r\nnone\r\n
   write(9) -> 4\r\nHG20\r\n
@@ -958,8 +827,6 @@
   readline(~) -> (2) \r\n (py3 !)
   write(167) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.2\r\nTransfer-Encoding: chunked\r\n\r\n (py3 !)
   write(41) -> Content-Type: application/mercurial-0.2\r\n
-  write(28) -> Transfer-Encoding: chunked\r\n (no-py3 !)
-  write(2) -> \r\n (no-py3 !)
   write(6) -> 1\\r\\n\x04\\r\\n (esc)
   write(9) -> 4\r\nnone\r\n
   write(9) -> 4\r\nHG20\r\n
@@ -1013,8 +880,6 @@
   readline(~) -> (2) \r\n (py3 !)
   write(167) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.2\r\nTransfer-Encoding: chunked\r\n\r\n (py3 !)
   write(41) -> Content-Type: application/mercurial-0.2\r\n
-  write(28) -> Transfer-Encoding: chunked\r\n (no-py3 !)
-  write(2) -> \r\n (no-py3 !)
   write(6) -> 1\\r\\n\x04\\r\\n (esc)
   write(9) -> 4\r\nnone\r\n
   write(9) -> 4\r\nHG20\r\n
@@ -1044,7 +909,6 @@
   transaction abort!
   rollback completed
   abort: HTTP request error (incomplete response) (py3 !)
-  abort: HTTP request error (incomplete response; expected 466 bytes got 7) (no-py3 !)
   (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator)
   [255]
 
@@ -1071,7 +935,6 @@
   $ "$PYTHON" $TESTDIR/filtertraceback.py < error.log | tail -15
   write(167) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.2\r\nTransfer-Encoding: chunked\r\n\r\n (py3 !)
   write(28) -> Transfer-Encoding: chunked\r\n
-  write(2) -> \r\n (no-py3 !)
   write(6) -> 1\\r\\n\x04\\r\\n (esc)
   write(9) -> 4\r\nnone\r\n
   write(9) -> 4\r\nHG20\r\n
@@ -1130,8 +993,6 @@
   readline(~) -> (2) \r\n (py3 !)
   write(167) -> HTTP/1.1 200 Script output follows\r\nServer: badhttpserver\r\nDate: $HTTP_DATE$\r\nContent-Type: application/mercurial-0.2\r\nTransfer-Encoding: chunked\r\n\r\n (py3 !)
   write(41) -> Content-Type: application/mercurial-0.2\r\n
-  write(28) -> Transfer-Encoding: chunked\r\n (no-py3 !)
-  write(2) -> \r\n (no-py3 !)
   write(6) -> 1\\r\\n\x04\\r\\n (esc)
   write(9) -> 4\r\nnone\r\n
   write(9) -> 4\r\nHG20\r\n
@@ -1165,7 +1026,6 @@
   transaction abort!
   rollback completed
   abort: HTTP request error (incomplete response) (py3 !)
-  abort: HTTP request error (incomplete response; expected 32 bytes got 9) (no-py3 !)
   (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator)
   [255]
 
--- a/tests/test-https.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-https.t	Wed May 04 18:17:44 2022 +0200
@@ -361,9 +361,9 @@
 
 Clients talking same TLS versions work
 
-  $ P="$CERTSDIR" hg --config hostsecurity.minimumprotocol=tls1.0 id https://localhost:$HGPORT/
+  $ P="$CERTSDIR" hg --config hostsecurity.minimumprotocol=tls1.0 --config hostsecurity.ciphers=DEFAULT id https://localhost:$HGPORT/
   5fed3813f7f5
-  $ P="$CERTSDIR" hg --config hostsecurity.minimumprotocol=tls1.1 id https://localhost:$HGPORT1/
+  $ P="$CERTSDIR" hg --config hostsecurity.minimumprotocol=tls1.1 --config hostsecurity.ciphers=DEFAULT id https://localhost:$HGPORT1/
   5fed3813f7f5
   $ P="$CERTSDIR" hg --config hostsecurity.minimumprotocol=tls1.2 id https://localhost:$HGPORT2/
   5fed3813f7f5
@@ -374,26 +374,26 @@
   (could not negotiate a common security protocol (tls1.1+) with localhost; the likely cause is Mercurial is configured to be more secure than the server can support)
   (consider contacting the operator of this server and ask them to support modern TLS protocol versions; or, set hostsecurity.localhost:minimumprotocol=tls1.0 to allow use of legacy, less secure protocols when communicating with this server)
   (see https://mercurial-scm.org/wiki/SecureConnections for more info)
-  abort: error: .*(unsupported protocol|wrong ssl version).* (re)
+  abort: error: .*(unsupported protocol|wrong ssl version|alert protocol version).* (re)
   [100]
 
   $ P="$CERTSDIR" hg --config hostsecurity.minimumprotocol=tls1.1 id https://localhost:$HGPORT/
   (could not negotiate a common security protocol (tls1.1+) with localhost; the likely cause is Mercurial is configured to be more secure than the server can support)
   (consider contacting the operator of this server and ask them to support modern TLS protocol versions; or, set hostsecurity.localhost:minimumprotocol=tls1.0 to allow use of legacy, less secure protocols when communicating with this server)
   (see https://mercurial-scm.org/wiki/SecureConnections for more info)
-  abort: error: .*(unsupported protocol|wrong ssl version).* (re)
+  abort: error: .*(unsupported protocol|wrong ssl version|alert protocol version).* (re)
   [100]
   $ P="$CERTSDIR" hg --config hostsecurity.minimumprotocol=tls1.2 id https://localhost:$HGPORT/
   (could not negotiate a common security protocol (tls1.2+) with localhost; the likely cause is Mercurial is configured to be more secure than the server can support)
   (consider contacting the operator of this server and ask them to support modern TLS protocol versions; or, set hostsecurity.localhost:minimumprotocol=tls1.0 to allow use of legacy, less secure protocols when communicating with this server)
   (see https://mercurial-scm.org/wiki/SecureConnections for more info)
-  abort: error: .*(unsupported protocol|wrong ssl version).* (re)
+  abort: error: .*(unsupported protocol|wrong ssl version|alert protocol version).* (re)
   [100]
   $ P="$CERTSDIR" hg --config hostsecurity.minimumprotocol=tls1.2 id https://localhost:$HGPORT1/
   (could not negotiate a common security protocol (tls1.2+) with localhost; the likely cause is Mercurial is configured to be more secure than the server can support)
   (consider contacting the operator of this server and ask them to support modern TLS protocol versions; or, set hostsecurity.localhost:minimumprotocol=tls1.0 to allow use of legacy, less secure protocols when communicating with this server)
   (see https://mercurial-scm.org/wiki/SecureConnections for more info)
-  abort: error: .*(unsupported protocol|wrong ssl version).* (re)
+  abort: error: .*(unsupported protocol|wrong ssl version|alert protocol version).* (re)
   [100]
 
 --insecure will allow TLS 1.0 connections and override configs
@@ -405,6 +405,7 @@
 The per-host config option overrides the default
 
   $ P="$CERTSDIR" hg id https://localhost:$HGPORT/ \
+  > --config hostsecurity.ciphers=DEFAULT \
   > --config hostsecurity.minimumprotocol=tls1.2 \
   > --config hostsecurity.localhost:minimumprotocol=tls1.0
   5fed3813f7f5
@@ -416,7 +417,7 @@
   (could not negotiate a common security protocol (tls1.2+) with localhost; the likely cause is Mercurial is configured to be more secure than the server can support)
   (consider contacting the operator of this server and ask them to support modern TLS protocol versions; or, set hostsecurity.localhost:minimumprotocol=tls1.0 to allow use of legacy, less secure protocols when communicating with this server)
   (see https://mercurial-scm.org/wiki/SecureConnections for more info)
-  abort: error: .*(unsupported protocol|wrong ssl version).* (re)
+  abort: error: .*(unsupported protocol|wrong ssl version|alert protocol version).* (re)
   [100]
 
 .hg/hgrc file [hostsecurity] settings are applied to remote ui instances (issue5305)
@@ -429,7 +430,7 @@
   (could not negotiate a common security protocol (tls1.2+) with localhost; the likely cause is Mercurial is configured to be more secure than the server can support)
   (consider contacting the operator of this server and ask them to support modern TLS protocol versions; or, set hostsecurity.localhost:minimumprotocol=tls1.0 to allow use of legacy, less secure protocols when communicating with this server)
   (see https://mercurial-scm.org/wiki/SecureConnections for more info)
-  abort: error: .*(unsupported protocol|wrong ssl version).* (re)
+  abort: error: .*(unsupported protocol|wrong ssl version|alert protocol version).* (re)
   [100]
 
   $ killdaemons.py hg0.pid
@@ -517,7 +518,7 @@
 without client certificate:
 
   $ P="$CERTSDIR" hg id https://localhost:$HGPORT/
-  abort: error: .*(\$ECONNRESET\$|certificate required|handshake failure).* (re)
+  abort: error: .*(\$ECONNRESET\$|certificate required|handshake failure|EOF occurred).* (re)
   [100]
 
 with client certificate:
--- a/tests/test-hybridencode.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-hybridencode.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function
-
 import unittest
 
 from mercurial import store
--- a/tests/test-impexp-branch.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-impexp-branch.t	Wed May 04 18:17:44 2022 +0200
@@ -2,7 +2,6 @@
   $ echo 'strip =' >> $HGRCPATH
 
   $ cat >findbranch.py <<EOF
-  > from __future__ import absolute_import
   > import re
   > import sys
   > 
--- a/tests/test-import.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-import.t	Wed May 04 18:17:44 2022 +0200
@@ -71,7 +71,6 @@
 regardless of the commit message in the patch)
 
   $ cat > dummypatch.py <<EOF
-  > from __future__ import print_function
   > print('patching file a')
   > open('a', 'wb').write(b'line2\n')
   > EOF
--- a/tests/test-imports-checker.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-imports-checker.t	Wed May 04 18:17:44 2022 +0200
@@ -16,74 +16,61 @@
   $ touch testpackage/__init__.py
 
   $ cat > testpackage/multiple.py << EOF
-  > from __future__ import absolute_import
   > import os, sys
   > EOF
 
   $ cat > testpackage/unsorted.py << EOF
-  > from __future__ import absolute_import
   > import sys
   > import os
   > EOF
 
   $ cat > testpackage/stdafterlocal.py << EOF
-  > from __future__ import absolute_import
   > from . import unsorted
   > import os
   > EOF
 
   $ cat > testpackage/requirerelative.py << EOF
-  > from __future__ import absolute_import
   > import testpackage.unsorted
   > EOF
 
   $ cat > testpackage/importalias.py << EOF
-  > from __future__ import absolute_import
   > import ui
   > EOF
 
   $ cat > testpackage/relativestdlib.py << EOF
-  > from __future__ import absolute_import
   > from .. import os
   > EOF
 
   $ cat > testpackage/stdlibfrom.py << EOF
-  > from __future__ import absolute_import
   > from collections import abc
   > EOF
 
   $ cat > testpackage/symbolimport.py << EOF
-  > from __future__ import absolute_import
   > from .unsorted import foo
   > EOF
 
   $ cat > testpackage/latesymbolimport.py << EOF
-  > from __future__ import absolute_import
   > from . import unsorted
   > from mercurial.node import hex
   > EOF
 
   $ cat > testpackage/multiplegroups.py << EOF
-  > from __future__ import absolute_import
   > from . import unsorted
   > from . import more
   > EOF
 
   $ mkdir testpackage/subpackage
   $ cat > testpackage/subpackage/levelpriority.py << EOF
-  > from __future__ import absolute_import
   > from . import foo
   > from .. import parent
   > EOF
 
   $ touch testpackage/subpackage/foo.py
   $ cat > testpackage/subpackage/__init__.py << EOF
-  > from __future__ import absolute_import
   > from . import levelpriority  # should not cause cycle
   > EOF
 
   $ cat > testpackage/subpackage/localimport.py << EOF
-  > from __future__ import absolute_import
   > from . import foo
   > def bar():
   >     # should not cause "higher-level import should come first"
@@ -94,17 +81,14 @@
   > EOF
 
   $ cat > testpackage/importmodulefromsub.py << EOF
-  > from __future__ import absolute_import
   > from .subpackage import foo  # not a "direct symbol import"
   > EOF
 
   $ cat > testpackage/importsymbolfromsub.py << EOF
-  > from __future__ import absolute_import
   > from .subpackage import foo, nonmodule
   > EOF
 
   $ cat > testpackage/sortedentries.py << EOF
-  > from __future__ import absolute_import
   > from . import (
   >     foo,
   >     bar,
@@ -112,12 +96,10 @@
   > EOF
 
   $ cat > testpackage/importfromalias.py << EOF
-  > from __future__ import absolute_import
   > from . import ui
   > EOF
 
   $ cat > testpackage/importfromrelative.py << EOF
-  > from __future__ import absolute_import
   > from testpackage.unsorted import foo
   > EOF
 
@@ -125,7 +107,6 @@
   $ touch testpackage2/__init__.py
 
   $ cat > testpackage2/latesymbolimport.py << EOF
-  > from __future__ import absolute_import
   > from testpackage import unsorted
   > from mercurial.node import hex
   > EOF
@@ -137,29 +118,28 @@
   $ touch email/__init__.py
   $ touch email/errors.py
   $ cat > email/utils.py << EOF
-  > from __future__ import absolute_import
   > from . import errors
   > EOF
 
   $ "$PYTHON" "$import_checker" testpackage*/*.py testpackage/subpackage/*.py \
   >   email/*.py
-  testpackage/importalias.py:2: ui module must be "as" aliased to uimod
-  testpackage/importfromalias.py:2: ui from testpackage must be "as" aliased to uimod
-  testpackage/importfromrelative.py:2: import should be relative: testpackage.unsorted
-  testpackage/importfromrelative.py:2: direct symbol import foo from testpackage.unsorted
-  testpackage/importsymbolfromsub.py:2: direct symbol import nonmodule from testpackage.subpackage
-  testpackage/latesymbolimport.py:3: symbol import follows non-symbol import: mercurial.node
-  testpackage/multiple.py:2: multiple imported names: os, sys
-  testpackage/multiplegroups.py:3: multiple "from . import" statements
-  testpackage/relativestdlib.py:2: relative import of stdlib module
-  testpackage/requirerelative.py:2: import should be relative: testpackage.unsorted
-  testpackage/sortedentries.py:2: imports from testpackage not lexically sorted: bar < foo
-  testpackage/stdafterlocal.py:3: stdlib import "os" follows local import: testpackage
-  testpackage/stdlibfrom.py:2: direct symbol import abc from collections
-  testpackage/subpackage/levelpriority.py:3: higher-level import should come first: testpackage
-  testpackage/subpackage/localimport.py:7: multiple "from .. import" statements
-  testpackage/subpackage/localimport.py:8: import should be relative: testpackage.subpackage.levelpriority
-  testpackage/symbolimport.py:2: direct symbol import foo from testpackage.unsorted
-  testpackage/unsorted.py:3: imports not lexically sorted: os < sys
-  testpackage2/latesymbolimport.py:3: symbol import follows non-symbol import: mercurial.node
+  testpackage/importalias.py:1: ui module must be "as" aliased to uimod
+  testpackage/importfromalias.py:1: ui from testpackage must be "as" aliased to uimod
+  testpackage/importfromrelative.py:1: import should be relative: testpackage.unsorted
+  testpackage/importfromrelative.py:1: direct symbol import foo from testpackage.unsorted
+  testpackage/importsymbolfromsub.py:1: direct symbol import nonmodule from testpackage.subpackage
+  testpackage/latesymbolimport.py:2: symbol import follows non-symbol import: mercurial.node
+  testpackage/multiple.py:1: multiple imported names: os, sys
+  testpackage/multiplegroups.py:2: multiple "from . import" statements
+  testpackage/relativestdlib.py:1: relative import of stdlib module
+  testpackage/requirerelative.py:1: import should be relative: testpackage.unsorted
+  testpackage/sortedentries.py:1: imports from testpackage not lexically sorted: bar < foo
+  testpackage/stdafterlocal.py:2: stdlib import "os" follows local import: testpackage
+  testpackage/stdlibfrom.py:1: direct symbol import abc from collections
+  testpackage/subpackage/levelpriority.py:2: higher-level import should come first: testpackage
+  testpackage/subpackage/localimport.py:6: multiple "from .. import" statements
+  testpackage/subpackage/localimport.py:7: import should be relative: testpackage.subpackage.levelpriority
+  testpackage/symbolimport.py:1: direct symbol import foo from testpackage.unsorted
+  testpackage/unsorted.py:2: imports not lexically sorted: os < sys
+  testpackage2/latesymbolimport.py:2: symbol import follows non-symbol import: mercurial.node
   [1]
--- a/tests/test-inherit-mode.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-inherit-mode.t	Wed May 04 18:17:44 2022 +0200
@@ -10,7 +10,6 @@
   $ cd dir
 
   $ cat >printmodes.py <<EOF
-  > from __future__ import absolute_import, print_function
   > import os
   > import sys
   > 
@@ -31,7 +30,6 @@
   > EOF
 
   $ cat >mode.py <<EOF
-  > from __future__ import absolute_import, print_function
   > import os
   > import sys
   > print('%05o' % os.lstat(sys.argv[1]).st_mode)
--- a/tests/test-install.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-install.t	Wed May 04 18:17:44 2022 +0200
@@ -3,7 +3,6 @@
   checking encoding (ascii)...
   checking Python executable (*) (glob)
   checking Python implementation (*) (glob)
-  checking Python version (2.*) (glob) (no-py3 !)
   checking Python version (3.*) (glob) (py3 !)
   checking Python lib (.*[Ll]ib.*)... (re) (no-pyoxidizer !)
   checking Python lib (.*pyoxidizer.*)... (re) (pyoxidizer !)
@@ -67,7 +66,6 @@
   checking encoding (ascii)...
   checking Python executable (*) (glob)
   checking Python implementation (*) (glob)
-  checking Python version (2.*) (glob) (no-py3 !)
   checking Python version (3.*) (glob) (py3 !)
   checking Python lib (.*[Ll]ib.*)... (re) (no-pyoxidizer !)
   checking Python lib (.*pyoxidizer.*)... (re) (pyoxidizer !)
@@ -117,7 +115,6 @@
   checking encoding (ascii)...
   checking Python executable (*) (glob)
   checking Python implementation (*) (glob)
-  checking Python version (2.*) (glob) (no-py3 !)
   checking Python version (3.*) (glob) (py3 !)
   checking Python lib (.*[Ll]ib.*)... (re) (no-pyoxidizer !)
   checking Python lib (.*pyoxidizer.*)... (re) (pyoxidizer !)
@@ -147,7 +144,6 @@
   checking encoding (ascii)...
   checking Python executable (*) (glob)
   checking Python implementation (*) (glob)
-  checking Python version (2.*) (glob) (no-py3 !)
   checking Python version (3.*) (glob) (py3 !)
   checking Python lib (.*[Ll]ib.*)... (re) (no-pyoxidizer !)
   checking Python lib (.*pyoxidizer.*)... (re) (pyoxidizer !)
--- a/tests/test-issue660.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-issue660.t	Wed May 04 18:17:44 2022 +0200
@@ -1,7 +1,19 @@
+#testcases dirstate-v1 dirstate-v2
+
+#if dirstate-v2
+  $ cat >> $HGRCPATH << EOF
+  > [format]
+  > use-dirstate-v2=1
+  > [storage]
+  > dirstate-v2.slow-path=allow
+  > EOF
+#endif
+
 https://bz.mercurial-scm.org/660 and:
 https://bz.mercurial-scm.org/322
 
-  $ hg init
+  $ hg init repo
+  $ cd repo
   $ echo a > a
   $ mkdir b
   $ echo b > b/b
--- a/tests/test-keyword.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-keyword.t	Wed May 04 18:17:44 2022 +0200
@@ -1412,7 +1412,6 @@
   $ grep -v '^promptecho ' < $HGRCPATH >> $HGRCPATH.new
   $ mv $HGRCPATH.new $HGRCPATH
 
-  >>> from __future__ import print_function
   >>> from hgclient import check, readchannel, runcommand
   >>> @check
   ... def check(server):
--- a/tests/test-largefiles-cache.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-largefiles-cache.t	Wed May 04 18:17:44 2022 +0200
@@ -96,7 +96,6 @@
 
   $ cat > ls-l.py <<EOF
   > #!$PYTHON
-  > from __future__ import absolute_import, print_function
   > import os
   > import sys
   > path = sys.argv[1]
--- a/tests/test-largefiles-small-disk.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-largefiles-small-disk.t	Wed May 04 18:17:44 2022 +0200
@@ -1,7 +1,6 @@
 Test how largefiles abort in case the disk runs full
 
   $ cat > criple.py <<EOF
-  > from __future__ import absolute_import
   > import errno
   > import os
   > import shutil
--- a/tests/test-lfs-pointer.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-lfs-pointer.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function
-
 # Import something from Mercurial, so the module loader gets initialized.
 from mercurial import pycompat
 
--- a/tests/test-lfs-serve-access.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-lfs-serve-access.t	Wed May 04 18:17:44 2022 +0200
@@ -355,7 +355,6 @@
   $LOCALIP - - [$ERRDATE$] HG error:      super(badstore, self).download(oid, src, contentlength)
   $LOCALIP - - [$ERRDATE$] HG error:      raise LfsCorruptionError( (glob) (py38 !)
   $LOCALIP - - [$ERRDATE$] HG error:      _(b'corrupt remote lfs object: %s') % oid (glob) (no-py38 !)
-  $LOCALIP - - [$ERRDATE$] HG error:  LfsCorruptionError: corrupt remote lfs object: b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c (no-py3 !)
   $LOCALIP - - [$ERRDATE$] HG error:  hgext.lfs.blobstore.LfsCorruptionError: corrupt remote lfs object: b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c (py3 !)
   $LOCALIP - - [$ERRDATE$] HG error:   (glob)
   $LOCALIP - - [$ERRDATE$] Exception happened during processing request '/.hg/lfs/objects/276f73cfd75f9fb519810df5f5d96d6594ca2521abd86cbcd92122f7d51a1f3d': (glob)
@@ -367,7 +366,6 @@
       handled = wireprotoserver.handlewsgirequest( (py38 !)
       return _processbasictransfer( (py38 !)
       rctx, req, res, self.check_perm (no-py38 !)
-      return func(*(args + a), **kw) (no-py3 !)
       rctx.repo, req, res, lambda perm: checkperm(rctx, req, perm) (no-py38 !)
       res.setbodybytes(localstore.read(oid))
       blob = self._read(self.vfs, oid, verify)
@@ -381,7 +379,6 @@
   $LOCALIP - - [$ERRDATE$] HG error:      blobstore._verify(oid, b'dummy content') (glob)
   $LOCALIP - - [$ERRDATE$] HG error:      raise LfsCorruptionError( (glob) (py38 !)
   $LOCALIP - - [$ERRDATE$] HG error:      hint=_(b'run hg verify'), (glob) (no-py38 !)
-  $LOCALIP - - [$ERRDATE$] HG error:  LfsCorruptionError: detected corrupt lfs object: 276f73cfd75f9fb519810df5f5d96d6594ca2521abd86cbcd92122f7d51a1f3d (no-py3 !)
   $LOCALIP - - [$ERRDATE$] HG error:  hgext.lfs.blobstore.LfsCorruptionError: detected corrupt lfs object: 276f73cfd75f9fb519810df5f5d96d6594ca2521abd86cbcd92122f7d51a1f3d (py3 !)
   $LOCALIP - - [$ERRDATE$] HG error:   (glob)
 
--- a/tests/test-lfs-serve.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-lfs-serve.t	Wed May 04 18:17:44 2022 +0200
@@ -107,7 +107,6 @@
 
   $ cd client
   $ echo 'non-lfs' > nonlfs.txt
-  >>> from __future__ import absolute_import
   >>> from hgclient import check, readchannel, runcommand
   >>> @check
   ... def diff(server):
@@ -240,7 +239,6 @@
 
   $ cd ../cmdserve_client3
 
-  >>> from __future__ import absolute_import
   >>> from hgclient import check, readchannel, runcommand
   >>> @check
   ... def addrequirement(server):
@@ -355,7 +353,6 @@
   $ mv $HGRCPATH $HGRCPATH.tmp
   $ cp $HGRCPATH.orig $HGRCPATH
 
-  >>> from __future__ import absolute_import
   >>> from hgclient import bprint, check, readchannel, runcommand, stdout
   >>> @check
   ... def checkflags(server):
@@ -404,7 +401,6 @@
   > lfs = !
   > EOF
 
-  >>> from __future__ import absolute_import, print_function
   >>> from hgclient import bprint, check, readchannel, runcommand, stdout
   >>> @check
   ... def checkflags2(server):
--- a/tests/test-linelog.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-linelog.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function
-
 import difflib
 import random
 import unittest
--- a/tests/test-linerange.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-linerange.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import unittest
 from mercurial import error, mdiff
 from mercurial.utils import stringutil
--- a/tests/test-lock.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-lock.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import copy
 import errno
 import tempfile
@@ -37,7 +35,7 @@
         return super(lockwrapper, self)._getpid() + self._pidoffset
 
 
-class teststate(object):
+class teststate:
     def __init__(self, testcase, dir, pidoffset=0):
         self._testcase = testcase
         self._acquirecalled = False
--- a/tests/test-log-exthook.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-log-exthook.t	Wed May 04 18:17:44 2022 +0200
@@ -2,7 +2,6 @@
 -------------------------------------------
 
   $ cat > $TESTTMP/logexthook.py <<EOF
-  > from __future__ import absolute_import
   > import codecs
   > from mercurial import (
   >   commands,
--- a/tests/test-log.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-log.t	Wed May 04 18:17:44 2022 +0200
@@ -2451,7 +2451,6 @@
 
   $ cat > ../names.py <<EOF
   > """A small extension to test adding arbitrary names to a repo"""
-  > from __future__ import absolute_import
   > from mercurial import namespaces
   > 
   > def reposetup(ui, repo):
--- a/tests/test-logtoprocess.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-logtoprocess.t	Wed May 04 18:17:44 2022 +0200
@@ -8,7 +8,6 @@
 
   $ hg init
   $ cat > $TESTTMP/foocommand.py << EOF
-  > from __future__ import absolute_import
   > from mercurial import registrar
   > cmdtable = {}
   > command = registrar.command(cmdtable)
--- a/tests/test-lrucachedict.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-lrucachedict.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function
-
 import unittest
 
 import silenttestrunner
--- a/tests/test-mac-packages.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-mac-packages.t	Wed May 04 18:17:44 2022 +0200
@@ -39,8 +39,8 @@
   ./Library/Python/2.7/site-packages/mercurial/pure/bdiff.pyo	100644	0/0
   $ grep zsh/site-functions/_hg boms.txt | cut -d '	' -f 1,2,3
   ./usr/local/share/zsh/site-functions/_hg	100644	0/0
-  $ grep hg-completion.bash boms.txt | cut -d '	' -f 1,2,3
-  ./usr/local/hg/contrib/hg-completion.bash	100644	0/0
+  $ grep bash-completion/completions/hg boms.txt | cut -d '	' -f 1,2,3
+  ./usr/local/share/bash-completion-completions/hg	100644	0/0
   $ egrep 'man[15]' boms.txt | cut -d '	' -f 1,2,3
   ./usr/local/share/man/man1	40755	0/0
   ./usr/local/share/man/man1/chg.1	100644	0/0
--- a/tests/test-manifest.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-manifest.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import binascii
 import itertools
 import silenttestrunner
@@ -76,7 +74,7 @@
 )
 
 
-class basemanifesttests(object):
+class basemanifesttests:
     def parsemanifest(self, text):
         raise NotImplementedError('parsemanifest not implemented by test case')
 
--- a/tests/test-match.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-match.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import unittest
 
 import silenttestrunner
--- a/tests/test-mdiff.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-mdiff.py	Wed May 04 18:17:44 2022 +0200
@@ -1,6 +1,3 @@
-from __future__ import absolute_import
-from __future__ import print_function
-
 import unittest
 
 from mercurial import mdiff
--- a/tests/test-merge-halt.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-merge-halt.t	Wed May 04 18:17:44 2022 +0200
@@ -210,6 +210,6 @@
   merge halted after failed merge (see hg resolve)
   [240]
   $ hg shelve --list
-  default         (* ago)    changes to: foo (glob)
+  default         (*s ago) * changes to: foo (glob)
   $ hg unshelve --abort
   unshelve of 'default' aborted
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-merge-partial-tool.t	Wed May 04 18:17:44 2022 +0200
@@ -0,0 +1,241 @@
+Test support for partial-resolution tools
+
+Create a tool that resolves conflicts after line 5 by simply dropping those
+lines (even if there are no conflicts there)
+  $ cat >> "$TESTTMP/head.sh" <<'EOF'
+  > #!/bin/sh
+  > for f in "$@"; do
+  >   head -5 $f > tmp
+  >   mv -f tmp $f
+  > done
+  > EOF
+  $ chmod +x "$TESTTMP/head.sh"
+...and another tool that keeps only the last 5 lines instead of the first 5.
+  $ cat >> "$TESTTMP/tail.sh" <<'EOF'
+  > #!/bin/sh
+  > for f in "$@"; do
+  >   tail -5 $f > tmp
+  >   mv -f tmp $f
+  > done
+  > EOF
+  $ chmod +x "$TESTTMP/tail.sh"
+
+Set up both tools to run on all patterns (the default), and let the `tail` tool
+run after the `head` tool, which means it will have no effect (we'll override it
+to test order later)
+  $ cat >> "$HGRCPATH" <<EOF
+  > [partial-merge-tools]
+  > head.executable=$TESTTMP/head.sh
+  > tail.executable=$TESTTMP/tail.sh
+  > tail.order=1
+  > EOF
+
+  $ make_commit() {
+  >   echo "$@" | xargs -n1 > file
+  >   hg add file 2> /dev/null
+  >   hg ci -m "$*"
+  > }
+
+
+Let a partial-resolution tool resolve some conflicts and leave other conflicts
+for the regular merge tool (:merge3 here)
+
+  $ hg init repo
+  $ cd repo
+  $ make_commit a b c d e f
+  $ make_commit a b2 c d e f2
+  $ hg up 0
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ make_commit a b3 c d e f3
+  created new head
+  $ hg merge 1 -t :merge3
+  merging file
+  warning: conflicts while merging file! (edit, then use 'hg resolve --mark')
+  0 files updated, 0 files merged, 0 files removed, 1 files unresolved
+  use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
+  [1]
+  $ cat file
+  a
+  <<<<<<< working copy:    e11a49d4b620 - test: a b3 c d e f3
+  b3
+  ||||||| common ancestor: 8ae8bb9cc43a - test: a b c d e f
+  b
+  =======
+  b2
+  >>>>>>> merge rev:       fbc096a40cc5 - test: a b2 c d e f2
+  c
+  d
+  e
+
+
+With premerge=keep, the partial-resolution tools runs before and doesn't see
+the conflict markers
+
+  $ hg up -C 2
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cat >> .hg/hgrc <<EOF
+  > [merge-tools]
+  > my-local.executable = cat
+  > my-local.args = $local
+  > my-local.premerge = keep-merge3
+  > EOF
+  $ hg merge 1 -t my-local
+  merging file
+  0 files updated, 1 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+  $ cat file
+  a
+  <<<<<<< working copy:    e11a49d4b620 - test: a b3 c d e f3
+  b3
+  ||||||| common ancestor: 8ae8bb9cc43a - test: a b c d e f
+  b
+  =======
+  b2
+  >>>>>>> merge rev:       fbc096a40cc5 - test: a b2 c d e f2
+  c
+  d
+  e
+
+
+When a partial-resolution tool resolves all conflicts, the resolution should
+be recorded and the regular merge tool should not be invoked for the file.
+
+  $ hg up -C 0
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ make_commit a b c d e f2
+  created new head
+  $ hg up 0
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ make_commit a b c d e f3
+  created new head
+  $ hg merge 3 -t false
+  merging file
+  0 files updated, 1 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+  $ cat file
+  a
+  b
+  c
+  d
+  e
+
+
+Only tools whose patterns match are run. We make `head` not match here, so
+only `tail` should run
+
+  $ hg up -C 4
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg merge 3 -t :merge3 --config partial-merge-tools.head.patterns=other
+  merging file
+  warning: conflicts while merging file! (edit, then use 'hg resolve --mark')
+  0 files updated, 0 files merged, 0 files removed, 1 files unresolved
+  use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
+  [1]
+  $ cat file
+  b
+  c
+  d
+  e
+  <<<<<<< working copy:    d57edaa6e21a - test: a b c d e f3
+  f3
+  ||||||| common ancestor: 8ae8bb9cc43a - test: a b c d e f
+  f
+  =======
+  f2
+  >>>>>>> merge rev:       8c217da987be - test: a b c d e f2
+
+
+If there are several matching tools, they are run in requested order. We move
+`head` after `tail` in order here so it has no effect (the conflict in "f" thus
+remains).
+
+  $ hg up -C 4
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg merge 3 -t :merge3 --config partial-merge-tools.head.order=2
+  merging file
+  warning: conflicts while merging file! (edit, then use 'hg resolve --mark')
+  0 files updated, 0 files merged, 0 files removed, 1 files unresolved
+  use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
+  [1]
+  $ cat file
+  b
+  c
+  d
+  e
+  <<<<<<< working copy:    d57edaa6e21a - test: a b c d e f3
+  f3
+  ||||||| common ancestor: 8ae8bb9cc43a - test: a b c d e f
+  f
+  =======
+  f2
+  >>>>>>> merge rev:       8c217da987be - test: a b c d e f2
+
+
+When using "nomerge" tools (e.g. `:other`), the partial-resolution tools
+should not be run.
+
+  $ hg up -C 4
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg merge 3 -t :other
+  0 files updated, 1 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+  $ cat file
+  a
+  b
+  c
+  d
+  e
+  f2
+
+
+If a partial-resolution tool resolved some conflict and simplemerge can
+merge the rest, then the regular merge tool should not be used. Here we merge
+"a b c d e3 f3" with "a b2 c d e f2". The `head` tool resolves the conflict in
+"f" and the internal simplemerge merges the remaining changes in "b" and "e".
+
+  $ hg up -C 0
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ make_commit a b c d e3 f3
+  created new head
+  $ hg merge 1 -t false
+  merging file
+  0 files updated, 1 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+  $ cat file
+  a
+  b2
+  c
+  d
+  e3
+
+Test that arguments get passed as expected.
+
+  $ cat >> "$TESTTMP/log-args.sh" <<'EOF'
+  > #!/bin/sh
+  > echo "$@" > args.log
+  > EOF
+  $ chmod +x "$TESTTMP/log-args.sh"
+  $ cat >> "$HGRCPATH" <<EOF
+  > [partial-merge-tools]
+  > log-args.executable=$TESTTMP/log-args.sh
+  > EOF
+  $ hg up -C 2
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg merge 1
+  merging file
+  warning: conflicts while merging file! (edit, then use 'hg resolve --mark')
+  0 files updated, 0 files merged, 0 files removed, 1 files unresolved
+  use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
+  [1]
+  $ cat args.log
+  */hgmerge-*/file~local */hgmerge-*/file~base */hgmerge-*/file~other (glob)
+  $ hg up -C 2
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg merge 1 --config partial-merge-tools.log-args.args='--other $other $base --foo --local $local --also-other $other'
+  merging file
+  warning: conflicts while merging file! (edit, then use 'hg resolve --mark')
+  0 files updated, 0 files merged, 0 files removed, 1 files unresolved
+  use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
+  [1]
+  $ cat args.log
+  --other */hgmerge-*/file~other */hgmerge-*/file~base --foo --local */hgmerge-*/file~local --also-other */hgmerge-*/file~other (glob)
--- a/tests/test-merge-symlinks.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-merge-symlinks.t	Wed May 04 18:17:44 2022 +0200
@@ -1,6 +1,5 @@
   $ cat > echo.py <<EOF
   > #!$PYTHON
-  > from __future__ import absolute_import, print_function
   > import os
   > import sys
   > try:
--- a/tests/test-merge1.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-merge1.t	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,4 @@
   $ cat <<EOF > merge
-  > from __future__ import print_function
   > import sys, os
   > 
   > try:
@@ -354,7 +353,6 @@
 trigger it. If you see flakyness here, there is a race.
 
   $ cat > $TESTTMP/abort.py <<EOF
-  > from __future__ import absolute_import
   > # emulate aborting before "recordupdates()". in this case, files
   > # are changed without updating dirstate
   > from mercurial import (
--- a/tests/test-minifileset.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-minifileset.py	Wed May 04 18:17:44 2022 +0200
@@ -1,6 +1,3 @@
-from __future__ import absolute_import
-from __future__ import print_function
-
 from mercurial import minifileset
 
 
--- a/tests/test-minirst.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-minirst.py	Wed May 04 18:17:44 2022 +0200
@@ -1,4 +1,3 @@
-from __future__ import absolute_import, print_function
 from mercurial import minirst
 from mercurial.utils import stringutil
 
--- a/tests/test-mq-missingfiles.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-mq-missingfiles.t	Wed May 04 18:17:44 2022 +0200
@@ -5,10 +5,7 @@
 
   $ cat > writelines.py <<EOF
   > import sys
-  > if sys.version_info[0] >= 3:
-  >     encode = lambda x: x.encode('utf-8').decode('unicode_escape').encode('utf-8')
-  > else:
-  >     encode = lambda x: x.decode('string_escape')
+  > encode = lambda x: x.encode('utf-8').decode('unicode_escape').encode('utf-8')
   > path = sys.argv[1]
   > args = sys.argv[2:]
   > assert (len(args) % 2) == 0
--- a/tests/test-mq-qimport.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-mq-qimport.t	Wed May 04 18:17:44 2022 +0200
@@ -1,9 +1,6 @@
   $ cat > writelines.py <<EOF
   > import sys
-  > if sys.version_info[0] >= 3:
-  >     encode = lambda x: x.encode('utf-8').decode('unicode_escape').encode('utf-8')
-  > else:
-  >     encode = lambda x: x.decode('string_escape')
+  > encode = lambda x: x.encode('utf-8').decode('unicode_escape').encode('utf-8')
   > path = sys.argv[1]
   > args = sys.argv[2:]
   > assert (len(args) % 2) == 0
--- a/tests/test-narrow-clone-non-narrow-server.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-narrow-clone-non-narrow-server.t	Wed May 04 18:17:44 2022 +0200
@@ -20,7 +20,6 @@
 Verify that narrow is advertised in the bundle2 capabilities:
 
   $ cat >> unquote.py <<EOF
-  > from __future__ import print_function
   > import sys
   > if sys.version[0] == '3':
   >     import urllib.parse as up
--- a/tests/test-narrow-shallow-merges.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-narrow-shallow-merges.t	Wed May 04 18:17:44 2022 +0200
@@ -179,7 +179,7 @@
   
 
   $ hg log -T '{if(ellipsis,"...")}{node|short} {p1node|short} {p2node|short} {desc}\n' | sort
-  ...2a20009de83e 000000000000 3ac1f5779de3 outside 10
+  ...2a20009de83e 3ac1f5779de3 000000000000 outside 10
   ...3ac1f5779de3 bb96a08b062a 465567bdfb2d merge a/b/c/d 9
   ...8d874d57adea 7ef88b4dd4fa 000000000000 outside 12
   ...b844052e7b3b 000000000000 000000000000 outside 2c
--- a/tests/test-narrow.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-narrow.t	Wed May 04 18:17:44 2022 +0200
@@ -71,6 +71,17 @@
   updating to branch default
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
 
+The "narrow" repo requirement is ignored by [debugupgraderepo]
+
+#if tree
+  $ (cd should-work; hg debugupgraderepo)
+  abort: cannot upgrade repository; unsupported source requirement: treemanifest
+  [255]
+#else
+  $ (cd should-work; hg debugupgraderepo | grep 'no format upgrades found in existing repository')
+  (no format upgrades found in existing repository)
+#endif
+
 Test repo with local changes
   $ hg clone --narrow ssh://user@dummy/master narrow-local-changes --include d0 --include d3 --include d6
   requesting all changes
--- a/tests/test-notify-changegroup.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-notify-changegroup.t	Wed May 04 18:17:44 2022 +0200
@@ -40,7 +40,7 @@
 
   $ hg --traceback --cwd b push ../a 2>&1 |
   >     "$PYTHON" $TESTDIR/unwrap-message-id.py | \
-  >     "$PYTHON" -c 'from __future__ import print_function ; import sys,re; print(re.sub("\n\t", " ", sys.stdin.read()), end="")'
+  >     "$PYTHON" -c 'import sys,re; print(re.sub("\n\t", " ", sys.stdin.read()), end="")'
   pushing to ../a
   searching for changes
   adding changesets
@@ -95,7 +95,7 @@
 
   $ hg --config notify.sources=unbundle --cwd a unbundle ../test.hg 2>&1 |
   >     "$PYTHON" $TESTDIR/unwrap-message-id.py | \
-  >     "$PYTHON" -c 'from __future__ import print_function ; import sys,re; print(re.sub("\n\t", " ", sys.stdin.read()), end="")'
+  >     "$PYTHON" -c 'import sys,re; print(re.sub("\n\t", " ", sys.stdin.read()), end="")'
   adding changesets
   adding manifests
   adding file changes
@@ -172,7 +172,7 @@
 
   $ hg --traceback --cwd b --config notify.fromauthor=True push ../a 2>&1 |
   >     "$PYTHON" $TESTDIR/unwrap-message-id.py | \
-  >     "$PYTHON" -c 'from __future__ import print_function ; import sys,re; print(re.sub("\n\t", " ", sys.stdin.read()), end="")'
+  >     "$PYTHON" -c 'import sys,re; print(re.sub("\n\t", " ", sys.stdin.read()), end="")'
   pushing to ../a
   searching for changes
   adding changesets
--- a/tests/test-notify.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-notify.t	Wed May 04 18:17:44 2022 +0200
@@ -1,16 +1,14 @@
   $ cat > $TESTTMP/filter.py <<EOF
-  > from __future__ import absolute_import, print_function
   > import io
   > import re
   > import sys
-  > if sys.version_info[0] >= 3:
-  >     sys.stdout = io.TextIOWrapper(
-  >         sys.stdout.buffer,
-  >         sys.stdout.encoding,
-  >         sys.stdout.errors,
-  >         newline="\n",
-  >         line_buffering=sys.stdout.line_buffering,
-  >     )
+  > sys.stdout = io.TextIOWrapper(
+  >     sys.stdout.buffer,
+  >     sys.stdout.encoding,
+  >     sys.stdout.errors,
+  >     newline="\n",
+  >     line_buffering=sys.stdout.line_buffering,
+  > )
   > print(re.sub("\n[ \t]", " ", sys.stdin.read()), end="")
   > EOF
 
@@ -469,7 +467,6 @@
   Content-Transfer-Encoding: 8bit
   X-Test: foo
   Date: * (glob)
-  Subject: \xc3\xa0... (esc) (no-py3 !)
   Subject: =?utf-8?b?w6AuLi4=?= (py3 !)
   From: test@test.com
   X-Hg-Notification: changeset 0f25f9c22b4c
--- a/tests/test-obsolete.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-obsolete.t	Wed May 04 18:17:44 2022 +0200
@@ -1465,7 +1465,6 @@
 Test heads computation on pending index changes with obsolescence markers
   $ cd ..
   $ cat >$TESTTMP/test_extension.py  << EOF
-  > from __future__ import absolute_import
   > from mercurial.i18n import _
   > from mercurial import cmdutil, pycompat, registrar
   > from mercurial.utils import stringutil
@@ -1499,7 +1498,6 @@
 bookmarks change
   $ cd ..
   $ cat >$TESTTMP/test_extension.py  << EOF
-  > from __future__ import absolute_import, print_function
   > import weakref
   > from mercurial import (
   >   bookmarks,
--- a/tests/test-pager.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-pager.t	Wed May 04 18:17:44 2022 +0200
@@ -411,7 +411,6 @@
 
 Environment variables like LESS and LV are set automatically:
   $ cat > $TESTTMP/printlesslv.py <<EOF
-  > from __future__ import absolute_import
   > import os
   > import sys
   > sys.stdin.read()
--- a/tests/test-parseindex.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-parseindex.t	Wed May 04 18:17:44 2022 +0200
@@ -26,7 +26,6 @@
   summary:     change foo
   
   $ cat >> test.py << EOF
-  > from __future__ import print_function
   > from mercurial import changelog, node, pycompat, vfs
   > 
   > class singlebyteread(object):
@@ -75,7 +74,6 @@
   $ cd a
 
   $ "$PYTHON" <<EOF
-  > from __future__ import print_function
   > from mercurial import changelog, vfs
   > cl = changelog.changelog(vfs.vfs(b'.hg/store'))
   > print('good heads:')
@@ -113,7 +111,7 @@
   10000: head out of range
   -2: head out of range
   -10000: head out of range
-  None: an integer is required( .got type NoneType.)? (re)
+  None: (an integer is required( .got type NoneType.)?|'NoneType' object cannot be interpreted as an integer) (re)
   good roots:
   0: [0]
   1: [1]
@@ -124,7 +122,7 @@
   -2: []
   -10000: []
   bad roots:
-  None: an integer is required( .got type NoneType.)? (re)
+  None: (an integer is required( .got type NoneType.)?|'NoneType' object cannot be interpreted as an integer) (re)
 
   $ cd ..
 
@@ -177,7 +175,6 @@
         1       2        1       -1    base         66         65         66   1.01538        66         0    0.00000
 
   $ cat <<EOF > test.py
-  > from __future__ import print_function
   > import sys
   > from mercurial import changelog, pycompat, vfs
   > cl = changelog.changelog(vfs.vfs(pycompat.fsencode(sys.argv[1])))
--- a/tests/test-parseindex2.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-parseindex2.py	Wed May 04 18:17:44 2022 +0200
@@ -3,7 +3,6 @@
 It also checks certain aspects of the parsers module as a whole.
 """
 
-from __future__ import absolute_import, print_function
 
 import os
 import struct
--- a/tests/test-patch.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-patch.t	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,4 @@
   $ cat > patchtool.py <<EOF
-  > from __future__ import absolute_import, print_function
   > import sys
   > print('Using custom patch')
   > if '--binary' in sys.argv:
--- a/tests/test-patchbomb.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-patchbomb.t	Wed May 04 18:17:44 2022 +0200
@@ -8,7 +8,6 @@
 --===+[0-9]+=+$ -> --===*= (glob)
 
   $ cat > prune-blank-after-boundary.py <<EOF
-  > from __future__ import absolute_import, print_function
   > import sys
   > skipblank = False
   > trim = lambda x: x.strip(' \r\n')
@@ -514,7 +513,6 @@
   X-Mercurial-Series-Id: <f81ef97829467e868fc4.240@test-hostname>
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Thu, 01 Jan 1970 00:04:00 +0000
-  From: Q <quux> (no-py3 !)
   From: =?iso-8859-1?q?Q?= <quux> (py3 !)
   To: foo
   Cc: bar
@@ -2400,9 +2398,6 @@
   User-Agent: Mercurial-patchbomb/* (glob)
   Date: Tue, 01 Jan 1980 00:01:00 +0000
   From: quux
-  To: spam <spam>, eggs, toast (no-py3 !)
-  Cc: foo, bar@example.com, "A, B <>" <a@example.com> (no-py3 !)
-  Bcc: "Quux, A." <quux> (no-py3 !)
   To: =?iso-8859-1?q?spam?= <spam>, eggs, toast (py3 !)
   Cc: foo, bar@example.com, =?iso-8859-1?q?A=2C_B_=3C=3E?= <a@example.com> (py3 !)
   Bcc: =?iso-8859-1?q?Quux=2C_A=2E?= <quux> (py3 !)
@@ -2722,7 +2717,6 @@
   MIME-Version: 1.0
   Content-Type: text/plain; charset="iso-8859-1"
   Content-Transfer-Encoding: quoted-printable
-  Subject: [PATCH 2 of 6] \xe7a (esc) (no-py3 !)
   Subject: =?utf-8?b?W1BBVENIIDIgb2YgNl0gw6dh?= (py3 !)
   X-Mercurial-Node: f81ef97829467e868fc405fccbcfa66217e4d3e6
   X-Mercurial-Series-Index: 2
--- a/tests/test-pathencode.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-pathencode.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # that have proven likely to expose bugs and divergent behavior in
 # different encoding implementations.
 
-from __future__ import absolute_import, print_function
 
 import binascii
 import collections
@@ -67,7 +66,7 @@
             counts[c] += 1
     for c in '\r/\n':
         counts.pop(c, None)
-    t = sum(pycompat.itervalues(counts)) / 100.0
+    t = sum(counts.values()) / 100.0
     fp.write('probtable = (')
     for i, (k, v) in enumerate(
         sorted(counts.items(), key=lambda x: x[1], reverse=True)
--- a/tests/test-profile.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-profile.t	Wed May 04 18:17:44 2022 +0200
@@ -132,7 +132,6 @@
 profiler extension could be loaded before other extensions
 
   $ cat > fooprof.py <<EOF
-  > from __future__ import absolute_import
   > import contextlib
   > import sys
   > @contextlib.contextmanager
@@ -147,7 +146,6 @@
   > EOF
 
   $ cat > otherextension.py <<EOF
-  > from __future__ import absolute_import
   > def extsetup(ui):
   >     ui.write(b'otherextension: loaded\n')
   > EOF
--- a/tests/test-progress.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-progress.t	Wed May 04 18:17:44 2022 +0200
@@ -1,6 +1,5 @@
 
   $ cat > loop.py <<EOF
-  > from __future__ import absolute_import
   > import time
   > from mercurial import commands, registrar
   > 
--- a/tests/test-propertycache.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-propertycache.py	Wed May 04 18:17:44 2022 +0200
@@ -4,7 +4,6 @@
 property cache of both localrepo and repoview to prevent
 regression."""
 
-from __future__ import absolute_import, print_function
 import os
 import subprocess
 
--- a/tests/test-pull-network.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-pull-network.t	Wed May 04 18:17:44 2022 +0200
@@ -90,12 +90,12 @@
 It's tricky to make file:// URLs working on every platform with
 regular shell commands.
 
-  $ URL=`"$PYTHON" -c "from __future__ import print_function; import os; print('file://foobar' + ('/' + os.getcwd().replace(os.sep, '/')).replace('//', '/') + '/../test')"`
+  $ URL=`"$PYTHON" -c "import os; print('file://foobar' + ('/' + os.getcwd().replace(os.sep, '/')).replace('//', '/') + '/../test')"`
   $ hg pull -q "$URL"
   abort: file:// URLs can only refer to localhost
   [255]
 
-  $ URL=`"$PYTHON" -c "from __future__ import print_function; import os; print('file://localhost' + ('/' + os.getcwd().replace(os.sep, '/')).replace('//', '/') + '/../test')"`
+  $ URL=`"$PYTHON" -c "import os; print('file://localhost' + ('/' + os.getcwd().replace(os.sep, '/')).replace('//', '/') + '/../test')"`
   $ hg pull -q "$URL"
 
 SEC: check for unsafe ssh url
--- a/tests/test-rebase-dest.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-rebase-dest.t	Wed May 04 18:17:44 2022 +0200
@@ -81,7 +81,6 @@
   $ cd $TESTTMP
 
   $ cat >> $TESTTMP/maprevset.py <<EOF
-  > from __future__ import absolute_import
   > from mercurial import registrar, revset, revsetlang, smartset
   > revsetpredicate = registrar.revsetpredicate()
   > cache = {}
--- a/tests/test-rebase-scenario-global.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-rebase-scenario-global.t	Wed May 04 18:17:44 2022 +0200
@@ -949,7 +949,6 @@
   $ hg init tr-state
   $ cd tr-state
   $ cat > $TESTTMP/wraprebase.py <<EOF
-  > from __future__ import absolute_import
   > from mercurial import extensions
   > def _rebase(orig, ui, repo, *args, **kwargs):
   >     with repo.wlock():
--- a/tests/test-relink.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-relink.t	Wed May 04 18:17:44 2022 +0200
@@ -8,7 +8,6 @@
   > }
 
   $ cat > arelinked.py <<EOF
-  > from __future__ import absolute_import, print_function
   > import os
   > import sys
   > from mercurial import (
--- a/tests/test-remotefilelog-corrupt-cache.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-remotefilelog-corrupt-cache.t	Wed May 04 18:17:44 2022 +0200
@@ -38,7 +38,6 @@
   $ chmod u+w $CACHEDIR/master/11/f6ad8ec52a2984abaafd7c3b516503785c2072/1406e74118627694268417491f018a4a883152f0
   $ echo x > $CACHEDIR/master/11/f6ad8ec52a2984abaafd7c3b516503785c2072/1406e74118627694268417491f018a4a883152f0
   $ hg up tip 2>&1 | egrep "^[^ ].*unexpected remotefilelog"
-  abort: unexpected remotefilelog header: illegal format (no-py3 !)
   hgext.remotefilelog.shallowutil.BadRemotefilelogHeader: unexpected remotefilelog header: illegal format (py3 !)
 
 Verify detection and remediation when remotefilelog.validatecachelog is set
--- a/tests/test-remotefilelog-datapack.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-remotefilelog-datapack.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,4 @@
 #!/usr/bin/env python
-from __future__ import absolute_import, print_function
 
 import hashlib
 import os
@@ -36,7 +35,7 @@
 )
 
 
-class datapacktestsbase(object):
+class datapacktestsbase:
     def __init__(self, datapackreader, paramsavailable):
         self.datapackreader = datapackreader
         self.paramsavailable = paramsavailable
--- a/tests/test-remotefilelog-histpack.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-remotefilelog-histpack.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,4 @@
 #!/usr/bin/env python
-from __future__ import absolute_import
 
 import hashlib
 import os
--- a/tests/test-rename.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-rename.t	Wed May 04 18:17:44 2022 +0200
@@ -682,7 +682,6 @@
 "hg cp" does not preserve the mtime, so it should be newer than the 2009
 timestamp.
   $ hg cp -q mtime mtime_cp
-  >>> from __future__ import print_function
   >>> import os
   >>> filename = "mtime_cp/f"
   >>> print(os.stat(filename).st_mtime < 1234567999)
@@ -691,7 +690,6 @@
 (modulo some fudge factor due to not every system supporting 1s-level
 precision).
   $ hg mv -q mtime mtime_mv
-  >>> from __future__ import print_function
   >>> import os
   >>> filename = "mtime_mv/f"
   >>> print(os.stat(filename).st_mtime < 1234567999)
--- a/tests/test-requires.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-requires.t	Wed May 04 18:17:44 2022 +0200
@@ -32,7 +32,6 @@
 
   $ echo 'featuresetup-test' >> supported/.hg/requires
   $ cat > $TESTTMP/supported-locally/supportlocally.py <<EOF
-  > from __future__ import absolute_import
   > from mercurial import extensions, localrepo
   > def featuresetup(ui, supported):
   >     for name, module in extensions.extensions(ui):
--- a/tests/test-revert-interactive-curses.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-revert-interactive-curses.t	Wed May 04 18:17:44 2022 +0200
@@ -1,4 +1,5 @@
 #require curses
+#testcases committed wdir
 
 Revert interactive tests with the Curses interface
 
@@ -12,6 +13,22 @@
 
 TODO: Make a curses version of the other tests from test-revert-interactive.t.
 
+#if committed
+  $ maybe_commit() {
+  >   hg ci "$@"
+  > }
+  $ do_revert() {
+  >   hg revert -ir'.^'
+  > }
+#else
+  $ maybe_commit() {
+  >   true
+  > }
+  $ do_revert() {
+  >   hg revert -i
+  > }
+#endif
+
 When a line without EOL is selected during "revert -i"
 
   $ hg init $TESTTMP/revert-i-curses-eol
@@ -19,7 +36,7 @@
   $ echo 0 > a
   $ hg ci -qAm 0
   $ printf 1 >> a
-  $ hg ci -qAm 1
+  $ maybe_commit -qAm 1
   $ cat a
   0
   1 (no-eol)
@@ -28,7 +45,7 @@
   > c
   > EOF
 
-  $ hg revert -ir'.^'
+  $ do_revert
   reverting a
   $ cat a
   0
@@ -40,7 +57,7 @@
   $ printf 0 > a
   $ hg ci -qAm 0
   $ echo 0 > a
-  $ hg ci -qAm 1
+  $ maybe_commit -qAm 1
   $ cat a
   0
 
@@ -48,7 +65,7 @@
   > c
   > EOF
 
-  $ hg revert -ir'.^'
+  $ do_revert
   reverting a
   $ cat a
   0 (no-eol)
--- a/tests/test-revert-interactive.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-revert-interactive.t	Wed May 04 18:17:44 2022 +0200
@@ -420,6 +420,19 @@
   forgetting newfile
   $ hg status
   ? newfile
+  $ rm newfile
+  $ hg up 0
+  1 files updated, 0 files merged, 4 files removed, 0 files unresolved
+  $ hg status
+  $ hg revert -r 2 -i <<EOF
+  > y
+  > n
+  > EOF
+  add new file folder1/g (Yn)? y
+  adding folder1/g
+  add new file folder2/h (Yn)? n
+  $ hg status
+  A folder1/g
 
 When a line without EOL is selected during "revert -i" (issue5651)
 
--- a/tests/test-revert.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-revert.t	Wed May 04 18:17:44 2022 +0200
@@ -550,7 +550,6 @@
 
   $ cat << EOF >> dircontent.py
   > # generate a simple text view of the directory for easy comparison
-  > from __future__ import print_function
   > import os
   > files = os.listdir('.')
   > files.sort()
--- a/tests/test-revlog-ancestry.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-revlog-ancestry.py	Wed May 04 18:17:44 2022 +0200
@@ -1,4 +1,3 @@
-from __future__ import absolute_import, print_function
 import os
 from mercurial import (
     hg,
--- a/tests/test-revlog-mmapindex.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-revlog-mmapindex.t	Wed May 04 18:17:44 2022 +0200
@@ -2,7 +2,6 @@
   $ cat << EOF > verbosemmap.py
   > # extension to make util.mmapread verbose
   > 
-  > from __future__ import absolute_import
   > 
   > from mercurial import (
   >     extensions,
--- a/tests/test-revlog-raw.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-revlog-raw.py	Wed May 04 18:17:44 2022 +0200
@@ -1,6 +1,5 @@
 # test revlog interaction about raw data (flagprocessor)
 
-from __future__ import absolute_import, print_function
 
 import collections
 import hashlib
@@ -20,7 +19,7 @@
 )
 
 
-class _NoTransaction(object):
+class _NoTransaction:
     """transaction like object to update the nodemap outside a transaction"""
 
     def __init__(self):
@@ -151,7 +150,7 @@
     code path, which is not covered by "appendrev" alone.
     """
 
-    class dummychangegroup(object):
+    class dummychangegroup:
         @staticmethod
         def deltachunk(pnode):
             pnode = pnode or rlog.nullid
--- a/tests/test-revset.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-revset.t	Wed May 04 18:17:44 2022 +0200
@@ -36,7 +36,6 @@
 these predicates use '\0' as a separator:
 
   $ cat <<EOF > debugrevlistspec.py
-  > from __future__ import absolute_import
   > from mercurial import (
   >     node as nodemod,
   >     registrar,
--- a/tests/test-rhg.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-rhg.t	Wed May 04 18:17:44 2022 +0200
@@ -384,3 +384,17 @@
   $ echo "*:required = yes" >> $HGRCPATH
   $ rhg files
   a
+
+We can ignore all extensions at once
+
+  $ echo "[extensions]" >> $HGRCPATH
+  $ echo "thisextensionbetternotexist=" >> $HGRCPATH
+  $ echo "thisextensionbetternotexisteither=" >> $HGRCPATH
+  $ $NO_FALLBACK rhg files
+  unsupported feature: extensions: thisextensionbetternotexist, thisextensionbetternotexisteither (consider adding them to 'rhg.ignored-extensions' config)
+  [252]
+
+  $ echo "[rhg]" >> $HGRCPATH
+  $ echo "ignored-extensions=*" >> $HGRCPATH
+  $ $NO_FALLBACK rhg files
+  a
--- a/tests/test-run-tests.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-run-tests.py	Wed May 04 18:17:44 2022 +0200
@@ -3,7 +3,6 @@
 run-test.t only checks positive matches and can not see warnings
 (both by design)
 """
-from __future__ import absolute_import, print_function
 
 import doctest
 import os
--- a/tests/test-run-tests.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-run-tests.t	Wed May 04 18:17:44 2022 +0200
@@ -2086,5 +2086,4 @@
   $ ./test-py3.py
   3.* (glob)
   $ ./test-py.py
-  2.* (glob) (no-py3 !)
   3.* (glob) (py3 !)
--- a/tests/test-rust-ancestor.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-rust-ancestor.py	Wed May 04 18:17:44 2022 +0200
@@ -1,4 +1,3 @@
-from __future__ import absolute_import
 import sys
 import unittest
 
--- a/tests/test-rust-discovery.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-rust-discovery.py	Wed May 04 18:17:44 2022 +0200
@@ -1,4 +1,3 @@
-from __future__ import absolute_import
 import unittest
 
 from mercurial import policy
@@ -32,12 +31,12 @@
 )
 
 
-class fakechangelog(object):
+class fakechangelog:
     def __init__(self, idx):
         self.index = idx
 
 
-class fakerepo(object):
+class fakerepo:
     def __init__(self, idx):
         """Just make so that self.changelog.index is the given idx."""
         self.changelog = fakechangelog(idx)
--- a/tests/test-rust-revlog.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-rust-revlog.py	Wed May 04 18:17:44 2022 +0200
@@ -1,4 +1,3 @@
-from __future__ import absolute_import
 import unittest
 
 try:
--- a/tests/test-setdiscovery.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-setdiscovery.t	Wed May 04 18:17:44 2022 +0200
@@ -45,6 +45,7 @@
   unpruned common: 01241442b3c2 66f7d451a68b b5714e113bc0
   elapsed time:  * seconds (glob)
   round-trips:                   2
+  queries:                       6
   heads summary:
     total common heads:          2
       also local heads:          2
@@ -77,6 +78,7 @@
   all local changesets known remotely
   elapsed time:  * seconds (glob)
   round-trips:                   1
+  queries:                       2
   heads summary:
     total common heads:          2
       also local heads:          2
@@ -109,6 +111,7 @@
   all local changesets known remotely
   elapsed time:  * seconds (glob)
   round-trips:                   1
+  queries:                       1
   heads summary:
     total common heads:          1
       also local heads:          1
@@ -140,6 +143,7 @@
   unpruned common: 01241442b3c2 b5714e113bc0
   elapsed time:  * seconds (glob)
   round-trips:                   1
+  queries:                       0
   heads summary:
     total common heads:          2
       also local heads:          1
@@ -172,6 +176,7 @@
   all remote heads known locally
   elapsed time:  * seconds (glob)
   round-trips:                   1
+  queries:                       3
   heads summary:
     total common heads:          2
       also local heads:          1
@@ -204,6 +209,7 @@
   all remote heads known locally
   elapsed time:  * seconds (glob)
   round-trips:                   1
+  queries:                       1
   heads summary:
     total common heads:          2
       also local heads:          1
@@ -242,6 +248,7 @@
   unpruned common: bebd167eb94d
   elapsed time:  * seconds (glob)
   round-trips:                   2
+  queries:                       3
   heads summary:
     total common heads:          1
       also local heads:          1
@@ -277,6 +284,7 @@
   2 total queries in *.????s (glob)
   elapsed time:  * seconds (glob)
   round-trips:                   2
+  queries:                      31
   heads summary:
     total common heads:          1
       also local heads:          1
@@ -312,6 +320,7 @@
   2 total queries in *.????s (glob)
   elapsed time:  * seconds (glob)
   round-trips:                   2
+  queries:                      32
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -343,6 +352,7 @@
   unpruned common: 66f7d451a68b bebd167eb94d
   elapsed time:  * seconds (glob)
   round-trips:                   4
+  queries:                       5
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -378,6 +388,7 @@
   2 total queries in *.????s (glob)
   elapsed time:  * seconds (glob)
   round-trips:                   2
+  queries:                       3
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -413,6 +424,7 @@
   2 total queries in *.????s (glob)
   elapsed time:  * seconds (glob)
   round-trips:                   2
+  queries:                       3
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -450,6 +462,7 @@
   unpruned common: 2dc09a01254d
   elapsed time:  * seconds (glob)
   round-trips:                   4
+  queries:                       5
   heads summary:
     total common heads:          1
       also local heads:          1
@@ -485,6 +498,7 @@
   2 total queries in *.????s (glob)
   elapsed time:  * seconds (glob)
   round-trips:                   2
+  queries:                      31
   heads summary:
     total common heads:          1
       also local heads:          1
@@ -520,6 +534,7 @@
   2 total queries in *.????s (glob)
   elapsed time:  * seconds (glob)
   round-trips:                   2
+  queries:                      32
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -551,6 +566,7 @@
   unpruned common: 2dc09a01254d 66f7d451a68b
   elapsed time:  * seconds (glob)
   round-trips:                   4
+  queries:                       5
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -586,6 +602,7 @@
   2 total queries in *.????s (glob)
   elapsed time:  * seconds (glob)
   round-trips:                   2
+  queries:                      30
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -621,6 +638,7 @@
   2 total queries in *.????s (glob)
   elapsed time:  * seconds (glob)
   round-trips:                   2
+  queries:                      30
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -659,6 +677,7 @@
   unpruned common: 66f7d451a68b
   elapsed time:  * seconds (glob)
   round-trips:                   4
+  queries:                       5
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -694,6 +713,7 @@
   2 total queries in *.????s (glob)
   elapsed time:  * seconds (glob)
   round-trips:                   2
+  queries:                      32
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -729,6 +749,7 @@
   2 total queries in *.????s (glob)
   elapsed time:  * seconds (glob)
   round-trips:                   2
+  queries:                      32
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -760,6 +781,7 @@
   unpruned common: 66f7d451a68b
   elapsed time:  * seconds (glob)
   round-trips:                   4
+  queries:                       5
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -795,6 +817,7 @@
   2 total queries in *.????s (glob)
   elapsed time:  * seconds (glob)
   round-trips:                   2
+  queries:                      32
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -830,6 +853,7 @@
   2 total queries in *.????s (glob)
   elapsed time:  * seconds (glob)
   round-trips:                   2
+  queries:                      32
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -868,6 +892,7 @@
   unpruned common: 66f7d451a68b
   elapsed time:  * seconds (glob)
   round-trips:                   4
+  queries:                       5
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -903,6 +928,7 @@
   2 total queries in *.????s (glob)
   elapsed time:  * seconds (glob)
   round-trips:                   2
+  queries:                      52
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -938,6 +964,7 @@
   2 total queries in *.????s (glob)
   elapsed time:  * seconds (glob)
   round-trips:                   2
+  queries:                      52
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -969,6 +996,7 @@
   unpruned common: 66f7d451a68b
   elapsed time:  * seconds (glob)
   round-trips:                   3
+  queries:                       4
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -1004,6 +1032,7 @@
   2 total queries in *.????s (glob)
   elapsed time:  * seconds (glob)
   round-trips:                   2
+  queries:                      32
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -1039,6 +1068,7 @@
   2 total queries in *.????s (glob)
   elapsed time:  * seconds (glob)
   round-trips:                   2
+  queries:                      32
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -1077,6 +1107,7 @@
   unpruned common: 7ead0cba2838
   elapsed time:  * seconds (glob)
   round-trips:                   4
+  queries:                       5
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -1115,6 +1146,7 @@
   3 total queries in *.????s (glob)
   elapsed time:  * seconds (glob)
   round-trips:                   3
+  queries:                      43
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -1153,6 +1185,7 @@
   3 total queries in *.????s (glob)
   elapsed time:  * seconds (glob)
   round-trips:                   3
+  queries:                      43
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -1184,6 +1217,7 @@
   unpruned common: 7ead0cba2838
   elapsed time:  * seconds (glob)
   round-trips:                   3
+  queries:                       4
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -1222,6 +1256,7 @@
   3 total queries in *.????s (glob)
   elapsed time:  * seconds (glob)
   round-trips:                   3
+  queries:                      27
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -1260,6 +1295,7 @@
   3 total queries in *.????s (glob)
   elapsed time:  * seconds (glob)
   round-trips:                   3
+  queries:                      27
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -1350,6 +1386,7 @@
   6 total queries in *.????s (glob)
   elapsed time:  * seconds (glob)
   round-trips:                   6
+  queries:                    1054
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -1387,6 +1424,7 @@
   3 total queries in *.????s (glob)
   elapsed time:  * seconds (glob)
   round-trips:                   3
+  queries:                      13
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -1436,6 +1474,7 @@
   9 total queries in *s (glob)
   elapsed time:  * seconds (glob)
   round-trips:                   9
+  queries:                     993
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -1564,6 +1603,7 @@
   searching for changes
   elapsed time:  * seconds (glob)
   round-trips:                   1
+  queries:                       1
   heads summary:
     total common heads:          1
       also local heads:          1
@@ -1610,6 +1650,7 @@
   all remote heads known locally
   elapsed time:  * seconds (glob)
   round-trips:                   1
+  queries:                     260
   heads summary:
     total common heads:         25
       also local heads:         25
@@ -1655,6 +1696,7 @@
   3 total queries *s (glob)
   elapsed time:  * seconds (glob)
   round-trips:                   3
+  queries:                     109
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -1700,6 +1742,7 @@
   3 total queries in *s (glob)
   elapsed time:  * seconds (glob)
   round-trips:                   3
+  queries:                     109
   heads summary:
     total common heads:          1
       also local heads:          0
@@ -1757,6 +1800,7 @@
     "nb-revs-common": 300,
     "nb-revs-missing": 100,
     "output": "query 1; heads\nsearching for changes\ntaking quick initial sample\nquery 2; still undecided: 375, sample size is: 81\nsampling from both directions\nquery 3; still undecided: 3, sample size is: 3\n3 total queries in *s\n", (glob)
+    "total-queries": 109,
     "total-roundtrips": 3
    }
   ]
--- a/tests/test-share-bookmarks.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-share-bookmarks.t	Wed May 04 18:17:44 2022 +0200
@@ -222,7 +222,6 @@
 
   $ cat > failpullbookmarks.py << EOF
   > """A small extension that makes bookmark pulls fail, for testing"""
-  > from __future__ import absolute_import
   > from mercurial import (
   >   error,
   >   exchange,
--- a/tests/test-shelve.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-shelve.t	Wed May 04 18:17:44 2022 +0200
@@ -976,7 +976,7 @@
 Test shelve --delete
 
   $ hg shelve --list
-  default         (*s ago)    changes to: create conflict (glob)
+  default         (*s ago) * changes to: create conflict (glob)
   $ hg shelve --delete doesnotexist
   abort: shelved change 'doesnotexist' not found
   [10]
@@ -1209,7 +1209,7 @@
   $ hg add e
   $ hg ci -m e
   $ hg shelve --patch
-  default         (*s ago)    changes to: b (glob)
+  default         (*s ago) * changes to: b (glob)
   
   diff --git a/c b/c
   new file mode 100644
@@ -1258,7 +1258,7 @@
   e
 -- shelve should not contain `c` now
   $ hg shelve --patch
-  default         (*s ago)    changes to: b (glob)
+  default         (*s ago) * changes to: b (glob)
   
   diff --git a/d b/d
   new file mode 100644
@@ -1357,7 +1357,7 @@
   A
   B
   $ hg shelve --patch
-  default         (*s ago)    changes to: add B to foo (glob)
+  default         (*s ago) * changes to: add B to foo (glob)
   
   diff --git a/foo b/foo
   --- a/foo
--- a/tests/test-simplekeyvaluefile.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-simplekeyvaluefile.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import unittest
 import silenttestrunner
 
@@ -9,7 +7,7 @@
 )
 
 
-class mockfile(object):
+class mockfile:
     def __init__(self, name, fs):
         self.name = name
         self.fs = fs
@@ -27,7 +25,7 @@
         return self.fs.contents[self.name]
 
 
-class mockvfs(object):
+class mockvfs:
     def __init__(self):
         self.contents = {}
 
--- a/tests/test-simplemerge.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-simplemerge.py	Wed May 04 18:17:44 2022 +0200
@@ -13,7 +13,6 @@
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, see <http://www.gnu.org/licenses/>.
 
-from __future__ import absolute_import
 
 import unittest
 from mercurial import (
--- a/tests/test-ssh-proto-unbundle.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-ssh-proto-unbundle.t	Wed May 04 18:17:44 2022 +0200
@@ -99,7 +99,6 @@
 Test pushing to a server that has a pretxnchangegroup Python hook that fails
 
   $ cat > $TESTTMP/failhook << EOF
-  > from __future__ import print_function
   > import sys
   > def hook1line(ui, repo, **kwargs):
   >     ui.write(b'ui.write 1 line\n')
--- a/tests/test-ssh.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-ssh.t	Wed May 04 18:17:44 2022 +0200
@@ -326,7 +326,6 @@
   remote: added 1 changesets with 1 changes to 1 files (py3 !)
   remote: KABOOM
   remote: KABOOM IN PROCESS
-  remote: added 1 changesets with 1 changes to 1 files (no-py3 !)
 
 #endif
 
--- a/tests/test-sshserver.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-sshserver.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function
-
 import io
 import unittest
 
@@ -40,12 +38,12 @@
     return wireprotoserver.sshserver(ui, repo)
 
 
-class mockrepo(object):
+class mockrepo:
     def __init__(self, ui):
         self.ui = ui
 
 
-class mockui(object):
+class mockui:
     def __init__(self, inbytes):
         self.fin = io.BytesIO(inbytes)
         self.fout = io.BytesIO()
--- a/tests/test-status-inprocess.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-status-inprocess.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,4 @@
 #!/usr/bin/env python
-from __future__ import absolute_import, print_function
 
 import sys
 
--- a/tests/test-status.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-status.t	Wed May 04 18:17:44 2022 +0200
@@ -315,9 +315,8 @@
   ]
 
   $ hg status -A -Tpickle > pickle
-  >>> from __future__ import print_function
+  >>> import pickle
   >>> from mercurial import util
-  >>> pickle = util.pickle
   >>> data = sorted((x[b'status'].decode(), x[b'path'].decode()) for x in pickle.load(open("pickle", r"rb")))
   >>> for s, p in data: print("%s %s" % (s, p))
   ! deleted
--- a/tests/test-stdio.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-stdio.py	Wed May 04 18:17:44 2022 +0200
@@ -2,11 +2,11 @@
 """
 Tests the buffering behavior of stdio streams in `mercurial.utils.procutil`.
 """
-from __future__ import absolute_import
 
 import contextlib
 import errno
 import os
+import pickle
 import signal
 import subprocess
 import sys
@@ -336,7 +336,7 @@
             proc.stdin.close()
 
         def post_child_check():
-            err = util.pickle.load(err_f)
+            err = pickle.load(err_f)
             self.assertEqual(err.errno, errno.EPIPE)
             self.assertEqual(err.strerror, "Broken pipe")
 
--- a/tests/test-storage.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-storage.py	Wed May 04 18:17:44 2022 +0200
@@ -1,6 +1,5 @@
 # This test verifies the conformance of various classes to various
 # storage interfaces.
-from __future__ import absolute_import
 
 import silenttestrunner
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-strip-branch-cache.t	Wed May 04 18:17:44 2022 +0200
@@ -0,0 +1,56 @@
+Define helpers.
+
+  $ hg_log () { hg log -G -T "{rev}:{node|short}"; }
+  $ commit () { echo "foo - ${2:-$1}" > $1; hg commit -Aqm "Edited $1"; }
+  $ strip() { hg --config extensions.strip= strip -q -r "$1" ; }
+
+Setup hg repo.
+
+  $ hg init repo
+  $ cd repo
+  $ touch x; hg add x; hg commit -m "initial"
+  $ hg clone -q . ../clone
+  $ commit a
+
+  $ cd ../clone
+
+  $ commit b
+
+  $ hg pull -q ../repo
+
+  $ cat .hg/cache/branch2-visible
+  222ae9789a75703f9836e44de7db179cbfd420ee 2
+  a3498d6e39376d2456425dd8c692367bdbf00fa2 o default
+  222ae9789a75703f9836e44de7db179cbfd420ee o default
+
+  $ hg_log
+  o  2:222ae9789a75
+  |
+  | @  1:a3498d6e3937
+  |/
+  o  0:7ab0a3bd758a
+  
+
+  $ strip '1:'
+
+The branchmap cache is not adjusted on strip.
+Now mentions a changelog entry that has been stripped.
+
+  $ cat .hg/cache/branch2-visible
+  222ae9789a75703f9836e44de7db179cbfd420ee 2
+  a3498d6e39376d2456425dd8c692367bdbf00fa2 o default
+  222ae9789a75703f9836e44de7db179cbfd420ee o default
+
+  $ commit c
+
+Not adjusted on commit, either.
+
+  $ cat .hg/cache/branch2-visible
+  222ae9789a75703f9836e44de7db179cbfd420ee 2
+  a3498d6e39376d2456425dd8c692367bdbf00fa2 o default
+  222ae9789a75703f9836e44de7db179cbfd420ee o default
+
+On pull we end up with the same tip, and so wrongly reuse the invalid cache and crash.
+
+  $ hg pull ../repo 2>&1 | grep 'ValueError:'
+  ValueError: node a3498d6e39376d2456425dd8c692367bdbf00fa2 does not exist (known-bad-output !)
--- a/tests/test-strip.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-strip.t	Wed May 04 18:17:44 2022 +0200
@@ -1290,7 +1290,6 @@
   2 files updated, 0 files merged, 0 files removed, 0 files unresolved
   $ echo 3 >> I
   $ cat > $TESTTMP/delayedstrip.py <<EOF
-  > from __future__ import absolute_import
   > from mercurial import commands, registrar, repair
   > cmdtable = {}
   > command = registrar.command(cmdtable)
--- a/tests/test-subrepo-git.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-subrepo-git.t	Wed May 04 18:17:44 2022 +0200
@@ -1,20 +1,5 @@
 #require git
 
-# XXX-CHG When running with python2 + chg this test tend to get stuck and end up
-# as a time-out error. My effort to reproduce this outside of the CI failed. The
-# test itself seems to pass fine, but never "complete". Debugging it is slow and
-# tedious. This as a bad impact on the development process as most CI run end up
-# wasting abotu 1h until that one fails.
-#
-# Pierre-Yves David, Augie Fackler and Raphaël Gomès all agreed to disable this
-# case in that specific case until we figure this out (or we drop python2 o:-) )
-
-#if no-py3 chg
-  $ echo 'skipped: this test get stuck on the CI with python2 + chg. investigation needed'
-  $ exit 80
-#endif
-
-
 make git commits repeatable
 
   $ cat >> $HGRCPATH <<EOF
--- a/tests/test-subrepo-svn.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-subrepo-svn.t	Wed May 04 18:17:44 2022 +0200
@@ -249,7 +249,7 @@
 
 verify subrepo is contained within the repo directory
 
-  $ "$PYTHON" -c "from __future__ import print_function; import os.path; print(os.path.exists('s'))"
+  $ "$PYTHON" -c "import os.path; print(os.path.exists('s'))"
   True
 
 update to nullrev (must delete the subrepo)
--- a/tests/test-symlink-os-yes-fs-no.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-symlink-os-yes-fs-no.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import os
 import sys
 import time
--- a/tests/test-template-functions.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-template-functions.t	Wed May 04 18:17:44 2022 +0200
@@ -192,7 +192,6 @@
   $ cd unstable-hash
   $ hg log --template '{date|age}\n' > /dev/null || exit 1
 
-  >>> from __future__ import absolute_import
   >>> import datetime
   >>> fp = open('a', 'wb')
   >>> n = datetime.datetime.now() + datetime.timedelta(366 * 7)
@@ -1572,7 +1571,6 @@
 Test cbor filter:
 
   $ cat <<'EOF' > "$TESTTMP/decodecbor.py"
-  > from __future__ import absolute_import
   > from mercurial import (
   >     dispatch,
   > )
--- a/tests/test-template-map.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-template-map.t	Wed May 04 18:17:44 2022 +0200
@@ -722,7 +722,6 @@
 test CBOR style:
 
   $ cat <<'EOF' > "$TESTTMP/decodecborarray.py"
-  > from __future__ import absolute_import
   > from mercurial import (
   >     dispatch,
   > )
--- a/tests/test-trusted.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-trusted.py	Wed May 04 18:17:44 2022 +0200
@@ -2,7 +2,6 @@
 # with files from different users/groups, we cheat a bit by
 # monkey-patching some functions in the util module
 
-from __future__ import absolute_import, print_function
 
 import os
 import sys
--- a/tests/test-ui-color.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-ui-color.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function
-
 import os
 from mercurial import (
     dispatch,
--- a/tests/test-ui-config.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-ui-config.py	Wed May 04 18:17:44 2022 +0200
@@ -1,4 +1,3 @@
-from __future__ import absolute_import, print_function
 from mercurial import (
     dispatch,
     error,
--- a/tests/test-ui-verbosity.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-ui-verbosity.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function
-
 import os
 from mercurial import (
     pycompat,
--- a/tests/test-unified-test.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-unified-test.t	Wed May 04 18:17:44 2022 +0200
@@ -26,7 +26,6 @@
 
 Doctest commands:
 
-  >>> from __future__ import print_function
   >>> print('foo')
   foo
   $ echo interleaved
--- a/tests/test-update-atomic.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-update-atomic.t	Wed May 04 18:17:44 2022 +0200
@@ -3,7 +3,6 @@
 Checking that experimental.atomic-file works.
 
   $ cat > $TESTTMP/show_mode.py <<EOF
-  > from __future__ import print_function
   > import os
   > import stat
   > import sys
@@ -20,7 +19,6 @@
   $ cd repo
 
   $ cat > .hg/showwrites.py <<EOF
-  > from __future__ import print_function
   > from mercurial import pycompat
   > from mercurial.utils import stringutil
   > def uisetup(ui):
--- a/tests/test-upgrade-repo.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-upgrade-repo.t	Wed May 04 18:17:44 2022 +0200
@@ -734,7 +734,6 @@
   $ touch FooBarDirectory.d/f1
   $ hg -q commit -A -m 'add f1'
   $ hg -q up -r 0
-  >>> from __future__ import absolute_import, print_function
   >>> import random
   >>> random.seed(0) # have a reproducible content
   >>> with open("f2", "wb") as f:
@@ -958,7 +957,6 @@
 Check that the repo still works fine
 
   $ hg log -G --stat
-  @  changeset:   2:76d4395f5413 (no-py3 !)
   @  changeset:   2:fca376863211 (py3 !)
   |  tag:         tip
   |  parent:      0:ba592bf28da2
--- a/tests/test-url.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-url.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,4 @@
 # coding=utf-8
-from __future__ import absolute_import, print_function
 
 import doctest
 import os
--- a/tests/test-util.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-util.py	Wed May 04 18:17:44 2022 +0200
@@ -1,7 +1,7 @@
 # unit tests for mercuril.util utilities
-from __future__ import absolute_import
 
 import contextlib
+import io
 import itertools
 import unittest
 
@@ -55,7 +55,7 @@
 
 @contextlib.contextmanager
 def capturestderr():
-    """Replace utils.procutil.stderr with a pycompat.bytesio instance
+    """Replace utils.procutil.stderr with an io.BytesIO instance
 
     The instance is made available as the return value of __enter__.
 
@@ -63,7 +63,7 @@
 
     """
     orig = utils.procutil.stderr
-    utils.procutil.stderr = pycompat.bytesio()
+    utils.procutil.stderr = io.BytesIO()
     try:
         yield utils.procutil.stderr
     finally:
--- a/tests/test-verify-repo-operations.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-verify-repo-operations.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import print_function, absolute_import
-
 """Fuzz testing for operations against a Mercurial repository
 
 This uses Hypothesis's stateful testing to generate random repository
--- a/tests/test-verify.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-verify.t	Wed May 04 18:17:44 2022 +0200
@@ -338,7 +338,6 @@
   checked 1 changesets with 1 changes to 1 files
 
   $ cat >> $TESTTMP/break-base64.py <<EOF
-  > from __future__ import absolute_import
   > import base64
   > base64.b64decode=lambda x: x
   > EOF
--- a/tests/test-walk.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-walk.t	Wed May 04 18:17:44 2022 +0200
@@ -640,7 +640,6 @@
   $ cd t
   $ echo fennel > overflow.list
   $ cat >> printnum.py <<EOF
-  > from __future__ import print_function
   > for i in range(20000 // 100):
   >   print('x' * 100)
   > EOF
--- a/tests/test-walkrepo.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-walkrepo.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function
-
 import os
 
 from mercurial import (
--- a/tests/test-wireproto-clientreactor.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-wireproto-clientreactor.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 import sys
 import unittest
 import zlib
--- a/tests/test-wireproto-framing.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-wireproto-framing.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function
-
 import unittest
 
 from mercurial import (
--- a/tests/test-wireproto-serverreactor.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-wireproto-serverreactor.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function
-
 import unittest
 
 from mercurial import (
--- a/tests/test-wireproto.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-wireproto.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function
-
 import sys
 
 from mercurial import (
@@ -16,7 +14,7 @@
 stringio = util.stringio
 
 
-class proto(object):
+class proto:
     def __init__(self, args):
         self.args = args
         self.name = 'dummyproto'
@@ -78,7 +76,7 @@
         return {b'name': mangle(name)}, unmangle
 
 
-class serverrepo(object):
+class serverrepo:
     def __init__(self, ui):
         self.ui = ui
 
--- a/tests/test-worker.t	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-worker.t	Wed May 04 18:17:44 2022 +0200
@@ -1,7 +1,6 @@
 Test UI worker interaction
 
   $ cat > t.py <<EOF
-  > from __future__ import absolute_import, print_function
   > import sys
   > import time
   > from mercurial import (
@@ -88,9 +87,7 @@
   > test 100000.0 abort --traceback 2>&1 | egrep '(WorkerError|Abort)'
       raise error.Abort(b'known exception')
   mercurial.error.Abort: known exception (py3 !)
-  Abort: known exception (no-py3 !)
       raise error.WorkerError(status)
-  WorkerError: 255 (no-py3 !)
   mercurial.error.WorkerError: 255 (py3 !)
 
 Traceback must be printed for unknown exceptions
@@ -102,7 +99,6 @@
 Workers should not do cleanups in all cases
 
   $ cat > $TESTTMP/detectcleanup.py <<EOF
-  > from __future__ import absolute_import
   > import atexit
   > import os
   > import sys
@@ -136,7 +132,6 @@
 Do not crash on partially read result
 
   $ cat > $TESTTMP/detecttruncated.py <<EOF
-  > from __future__ import absolute_import
   > import os
   > import sys
   > import time
--- a/tests/test-wsgirequest.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/test-wsgirequest.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function
-
 import unittest
 
 from mercurial.hgweb import request as requestmod
--- a/tests/testlib/badserverext.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/testlib/badserverext.py	Wed May 04 18:17:44 2022 +0200
@@ -44,7 +44,6 @@
    request)
 """
 
-from __future__ import absolute_import
 
 import re
 import socket
@@ -91,7 +90,7 @@
 )
 
 
-class ConditionTracker(object):
+class ConditionTracker:
     def __init__(
         self,
         close_after_recv_bytes,
@@ -257,7 +256,7 @@
 
 
 # We can't adjust __class__ on a socket instance. So we define a proxy type.
-class socketproxy(object):
+class socketproxy:
     __slots__ = ('_orig', '_logfp', '_cond')
 
     def __init__(self, obj, logfp, condition_tracked):
@@ -301,7 +300,7 @@
 
 
 # We can't adjust __class__ on socket._fileobject, so define a proxy.
-class fileobjectproxy(object):
+class fileobjectproxy:
     __slots__ = ('_orig', '_logfp', '_cond')
 
     def __init__(self, obj, logfp, condition_tracked):
--- a/tests/testlib/crash_transaction_late.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/testlib/crash_transaction_late.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from mercurial import (
     error,
--- a/tests/testlib/ext-phase-report.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/testlib/ext-phase-report.py	Wed May 04 18:17:44 2022 +0200
@@ -1,7 +1,5 @@
 # tiny extension to report phase changes during transaction
 
-from __future__ import absolute_import
-
 
 def reposetup(ui, repo):
     def reportphasemove(tr):
--- a/tests/testlib/ext-sidedata-2.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/testlib/ext-sidedata-2.py	Wed May 04 18:17:44 2022 +0200
@@ -8,7 +8,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import hashlib
 import struct
--- a/tests/testlib/ext-sidedata-3.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/testlib/ext-sidedata-3.py	Wed May 04 18:17:44 2022 +0200
@@ -9,7 +9,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import hashlib
 import struct
--- a/tests/testlib/ext-sidedata-4.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/testlib/ext-sidedata-4.py	Wed May 04 18:17:44 2022 +0200
@@ -9,7 +9,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 from mercurial.revlogutils import sidedata
 
--- a/tests/testlib/ext-sidedata-5.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/testlib/ext-sidedata-5.py	Wed May 04 18:17:44 2022 +0200
@@ -9,7 +9,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import hashlib
 import struct
--- a/tests/testlib/ext-sidedata.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/testlib/ext-sidedata.py	Wed May 04 18:17:44 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import hashlib
 import struct
--- a/tests/testlib/ext-stream-clone-steps.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/testlib/ext-stream-clone-steps.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 from mercurial import (
     encoding,
     extensions,
--- a/tests/testlib/persistent-nodemap-race-ext.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/testlib/persistent-nodemap-race-ext.py	Wed May 04 18:17:44 2022 +0200
@@ -35,7 +35,6 @@
      /!\ valid.
 """
 
-from __future__ import print_function
 
 import os
 
--- a/tests/testlib/sigpipe-remote.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/testlib/sigpipe-remote.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,4 @@
 #!/usr/bin/env python3
-from __future__ import print_function
 
 import io
 import os
@@ -7,14 +6,6 @@
 import sys
 import time
 
-# we cannot use mercurial.testing as long as python2 is not dropped as the test
-# will only install the mercurial module for python2 in python2 run
-if sys.version_info[0] < 3:
-    ver = '.'.join(str(x) for x in sys.version_info)
-    exe = sys.executable
-    print('SIGPIPE-HELPER: script should run with Python 3', file=sys.stderr)
-    print('SIGPIPE-HELPER:   %s is running %s' % (exe, ver), file=sys.stderr)
-    sys.exit(255)
 
 if isinstance(sys.stdout.buffer, io.BufferedWriter):
     print('SIGPIPE-HELPER: script need unbuffered output', file=sys.stderr)
--- a/tests/testlib/sigpipe-worker.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/testlib/sigpipe-worker.py	Wed May 04 18:17:44 2022 +0200
@@ -3,7 +3,6 @@
 # This is literally `cat` but in python, one char at a time.
 #
 # see sigpipe-remote.py for details.
-from __future__ import print_function
 
 import io
 import os
--- a/tests/tinyproxy.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/tinyproxy.py	Wed May 04 18:17:44 2022 +0200
@@ -1,6 +1,5 @@
 #!/usr/bin/env python
 
-from __future__ import absolute_import, print_function
 
 __doc__ = """Tiny HTTP Proxy.
 
--- a/tests/unwrap-message-id.py	Wed May 04 18:00:01 2022 +0200
+++ b/tests/unwrap-message-id.py	Wed May 04 18:17:44 2022 +0200
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function
-
 import sys
 
 for line in sys.stdin: