changeset 52255:65d516db7309

branching: merge stable into default
author Raphaël Gomès <rgomes@octobus.net>
date Thu, 14 Nov 2024 16:45:23 +0100
parents 85bff84f0ad5 (current diff) d237fdd93eb9 (diff)
children 51a350a22d0c
files rust/Cargo.lock rust/hg-core/Cargo.toml rust/hg-core/src/errors.rs rust/hg-core/src/lib.rs rust/hg-core/src/update.rs
diffstat 33 files changed, 981 insertions(+), 212 deletions(-) [+]
line wrap: on
line diff
--- a/.hgsigs	Mon Oct 21 12:58:40 2024 +0200
+++ b/.hgsigs	Thu Nov 14 16:45:23 2024 +0100
@@ -267,3 +267,4 @@
 11a9e2fc0caf0800625612e374139e4d825349a6 0 iQJTBAABCgA9FiEE7SE+SGsjJJvcEHtZRcqpKnHKAnsFAmarnPwfHHBpZXJyZS15dmVzLmRhdmlkQGVucy1seW9uLm9yZwAKCRBFyqkqccoCe7S8EADa7zko/gg2lCWiCqj8FVKruUrcC8c807o0BQb5niPN4CMpG77BociIcbBV/ryKICR6jPR0RnG7I8K9EzNis6mMmwWweE5WkcEqsbuOmemAlRK74SZIWXW0D5Xp9iTIg1vcXd3jCmD77zxdbw6+aQNhkRddjZuWjA1iNKnuNWLwIpH3bbKsYhLK6lugvNIq1Vo3UEJTgFOX42u/WOskn4pFrqqNHH4cqFssWNNHNMpl7VJJxvGIWk7GzSAKQRIYJvgVSGjrBhg1PT/DlMo+3WwzmBnLPfDtWWRkCtRiGCg28caft00zEz+5K5VjSPO7JNquNxoLaKZ4HGgZZmTtf9M7g39Dsku02s7BM3iAfa9tkCxdZ2gVrVBj8d4mHr0VZZZb6bUzi3XOrMaEokpynQ+7PAHqx8o/gNo7M90MSbl6p0sqwZrScHOA/CkJRMbbjQrcSmIkoNwNjHgY88QaWUPExbmuyWYQ+u33usfSv2EIVGZiMb0AADAQw6TezWlkk3hWMYBuhFkSUs6KeNuLitUzSiMogg25ryblTYhMqeylTbbzD+OK/oyBKlC41qB88J/TQb8z1IAHM9WFIBhnCWTjvGGa7TKNQh0YE3tNH3E2FDEif07eDQggB1iJGJg+wtihyFaRK2EF36E7Sql1S+86WiPHUsqjYwxIpgq4R7xv3A==
 eae3ec345e5e62e653de32a87a70f6fa7241afde 0 iQHNBAABCgA3FiEEH2b4zfZU6QXBHaBhoR4BzQ4F2VYFAmcfahkZHGFscGhhcmVAcmFwaGFlbGdvbWVzLmRldgAKCRChHgHNDgXZVvTfC/4+EnMhYrYrPei1fdoVMLCFY9ibXL0ALdzH6HVFzQejc8yHQCMNbnuDXlaVuFpUSwRIt8BhZME5UXra9yVceGrnQO10I+Pz9IfT/Dz6gIE1TUHsmBlMszsTAg28GsD4/5pB9yHPNB/o3u4MoivVryKqBrRWFTF14Lb/vHAVKnupDY1T4gnrs5zGAH50WNBOJ3kOz6Joc62JlPkFrpfBLeisfB+motueCdqxwcb7Uf6jsWcsGK6tdtl1MBohKs8mHc4cYYvIczrP/P7XteX1HblpSSXe3ES61hih39n0Gjg+XCrgWVXMwRSnORkz0gylvk6woj801bifRyBJj5pOaFDjuxTu/fgPFyavtpfQs82bSAHgaHsou/3BUvKDSKxPPIckRIgFLb1Ut1r64Yl91yNsYtx6bcJbpZ03ltEkONxll9bQ0JyAEZyc7sB0tBV/LGHeJ91GIm/gDBpyfc+8Yqqco0Rkd6o+PV9PlH0GkqHNBNUB3rS1tWKq48Dq4gcOjDI=
 dc97e8670decc9925c2f570bdca636778184b199 0 iQHNBAABCgA3FiEEH2b4zfZU6QXBHaBhoR4BzQ4F2VYFAmcfrQsZHGFscGhhcmVAcmFwaGFlbGdvbWVzLmRldgAKCRChHgHNDgXZVp6WC/0cJriEsXt8UIogzNqAkBAotOAB/Py4ilRW2rENyfRBYikdI2aZ2GSPm+3oUHmBDUwtWDm4Ldr/MsW/cWn71nQqOCDtPRhnWfNiq+VqQOuMOB3A/KvPsRLnQKWmVyxYgaVAv+BJrJlJhINlRWxyozOZY+YXfSsmtJvrj4EfpZ0ieHevChitCoX0WGFbe31d++ZhfZJuWsweL2eO25fsyDJelGJzdZN6V/zPAAA2m2X3Qm415rRsvRwpkTJwwtx7m8c/bZ77EZB3OxrFWWWBmtB8WqcezPNosWJeM84OAEE8+9qAzJ0o1b7bo6upxiuKg612tUZvanLymzzcdfqeMcnoaX2Xxt6W4h7DNKth/8GXv1whDPn7LPKj8Jk2ZNTtRBQ5lTy/ytqrwKwNTree+PBlMA18BQ/vZAr1joeFfptNectxZMB0VgvOKgz/U/+BfPjFM1C3XMnVEWTBQlYSfbjKBYPuHGHuW3qVxKsT8yS08JrvFcNU9kAF8KBDFssif+w=
+31d45a1cbc479ac73fc8b355abe99e090eb6c747 0 iQHNBAABCgA3FiEEH2b4zfZU6QXBHaBhoR4BzQ4F2VYFAmc2E+wZHGFscGhhcmVAcmFwaGFlbGdvbWVzLmRldgAKCRChHgHNDgXZVgOeC/9kMZuDpTdSdGj2Fd8mTK8BLA+7PRvoUM4rbHlBDZjtCCcLkKTC1sB0FJzlbfNEYbFxwqnzCTFzwNBYwWYWW5of20EoMxl7KGFJDY4hZPhAe9uN346lnp3GkaIe9kI4B++NUrLuc3SfbSFo3cAQyBAmgwK0fAYec6TF+ZdkGrswgu6CMplckW35FkI24sNzYrjV5w0wUMhGQo2uT1g2XZFd2NsMaMrvCZIho916VLDumNglHAaxhoDbj7A9nQDesSlckSPDSu9Axu0NLoFWUndSheZQamoOJpJZ5IsyymsbZYGrrZeZREG/TeSSHV0WrvIfcLQQlJSKYrrakUSiqfXalwXrUS3fDdVymyEBy0q+cXkWyNMEqIYjH3coOof6F/9/DuVCsxDHJMJm5Bs4rLy2mHcMGXPSkWf75TwPHqPIsQm4WgaAaJNvEtc6XHMtw8Xu4z9wPywNeLBJITAipxI32xHHFW0yj2F//ttG47yM4FWPZJXgNAZlVK1WBtGCY6k=
--- a/.hgtags	Mon Oct 21 12:58:40 2024 +0200
+++ b/.hgtags	Thu Nov 14 16:45:23 2024 +0100
@@ -283,3 +283,4 @@
 11a9e2fc0caf0800625612e374139e4d825349a6 6.8.1
 eae3ec345e5e62e653de32a87a70f6fa7241afde 6.8.2
 dc97e8670decc9925c2f570bdca636778184b199 6.9rc0
+31d45a1cbc479ac73fc8b355abe99e090eb6c747 6.9rc1
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/build-one-linux-wheel.sh	Thu Nov 14 16:45:23 2024 +0100
@@ -0,0 +1,28 @@
+#!/bin/bash
+# build a single linux wheel within a prepared imaged based on manylinux images
+#
+#
+#
+set -eu
+
+# enforce that the translation are built
+export MERCURIAL_SETUP_FORCE_TRANSLATIONS=1
+
+if [ $# -lt 2 ]; then
+    echo "usage $0 PYTHONTAG DEST_DIR" >&2
+    echo "" >&2
+    echo 'PYTHONTAG should be of the form "cp310-cp310"' >&2
+    exit 64
+fi
+py_tag=$1
+destination_directory=$2
+
+
+tmp_wheel_dir=./tmp-wheelhouse
+
+if [ -e $tmp_wheel_dir ]; then
+    rm -rf $tmp_wheel_dir
+fi
+/opt/python/$py_tag/bin/python setup.py bdist_wheel --dist-dir $tmp_wheel_dir
+# adjust it to make it universal
+auditwheel repair $tmp_wheel_dir/*.whl -w $destination_directory
--- a/contrib/check-pytype.sh	Mon Oct 21 12:58:40 2024 +0200
+++ b/contrib/check-pytype.sh	Thu Nov 14 16:45:23 2024 +0100
@@ -68,6 +68,13 @@
 
 # TODO: include hgext and hgext3rd
 
+# use ts to produce some timing if available
+if ! command -v ts; then
+    ts() {
+        cat
+    }
+fi
+
 pytype --keep-going --jobs auto \
     doc/check-seclevel.py hgdemandimport hgext mercurial \
     -x hgext/absorb.py \
@@ -116,7 +123,8 @@
     -x mercurial/testing/storage.py \
     -x mercurial/thirdparty \
     -x mercurial/win32.py \
-    -x mercurial/wireprotov1server.py
+    -x mercurial/wireprotov1server.py \
+    | ts -i "(%.s)" | ts -s "%.s"
 
 if find .pytype/pyi -name '*.pyi' | xargs grep -ql '# Caught error'; then
     echo 'pytype crashed while generating the following type stubs:'
--- a/contrib/heptapod-ci.yml	Mon Oct 21 12:58:40 2024 +0200
+++ b/contrib/heptapod-ci.yml	Thu Nov 14 16:45:23 2024 +0100
@@ -25,6 +25,7 @@
   - tests
   - platform-compat
   - py-version-compat
+  - upload
 
 
 image: registry.heptapod.net/mercurial/ci-images/mercurial-core:$HG_CI_IMAGE_TAG
@@ -32,81 +33,144 @@
 variables:
     PYTHON: python
     HG_CI_IMAGE_TAG: "v2.1"
-    TEST_HGTESTS_ALLOW_NETIO: "0"
-    SHOW_VERSION_OF: "$PYTHON"
+    # a directory dedicated to creating files and temporary clone
+    # with shell runner, its content is not cleaned from one call to the next,
+    # so plan for it.
+    TMP_WORK_DIR: "${CI_PROJECT_DIR}/../.."
+    # we use CIBW_SKIP="pp*" to prevent the building of pypy wheel that are neither
+    # needed nor working.
+    CIBW_SKIP: "pp*"
 
-.all_template: &all
+.all:
+  # help changing all job at once when debugging
   when: on_success
+  # make sure jobs from later steps does not wait for anything implicit before
+  # starting.
   needs: []
 
-# TODO: we should use an image based on manylinux instead "all-in-one" image
-# used for all test so far.
-.build-wheel: &wheel
-    <<: *all
-    stage: build
-    variables:
-      WHEEL_TYPE: ""
-      FLAVOR: ""
-    before_script:
-      - echo "python used, $PYTHON"
-      - $PYTHON --version
-      - echo $WHEEL_TYPE
-      - test -n "$WHEEL_TYPE"
-      - echo $FLAVOR
-      - mkdir -p wheels/$WHEEL_TYPE
-    script:
-      - $PYTHON setup.py bdist_wheel $FLAVOR --dist-dir wheels/$WHEEL_TYPE
-    artifacts:
-      paths:
-        - wheels/$WHEEL_TYPE
-      expire_in: 1 week
+# a dummy job that only serve to trigger others
+#
+# This is useful for two reasons:
+# - the UX around parallel jobs is awful so manually starting them is unpractical
+# - manual starting job cannot make the pipeline "fails" and block a merge,
+#   while "on_success" job depending on manual trigger works fine in that regard.
+.trigger:
+  extends: .all
+  # smallest I know of
+  image: busybox
+  when: manual
+  variables:
+    GIT_STRATEGY: none
+    CI_CLEVER_CLOUD_FLAVOR: "XS"
+  script:
+    - echo 'nothing to see here'
+
+.build-wheel:
+  extends: .all
+  image: "registry.heptapod.net/mercurial/ci-images/core-wheel-x86_64-c:v3.0"
+  stage: build
+  variables:
+    WHEEL_TYPE: ""
+    FLAVOR: ""
+    MERCURIAL_SETUP_FORCE_TRANSLATIONS: "1"
+    CI_CLEVER_CLOUD_FLAVOR: "XS"
+  script:
+    - PLATFORM=`/opt/python/cp313-cp313/bin/python -c 'import sys; print(sys.platform)'`
+    - echo $WHEEL_TYPE
+    - test -n "$WHEEL_TYPE"
+    - echo $FLAVOR
+    - mkdir -p wheels/$PLATFORM/$WHEEL_TYPE/$BUILD_PY_ID
+    - contrib/build-one-linux-wheel.sh $BUILD_PY_ID wheels/$PLATFORM/$WHEEL_TYPE/$BUILD_PY_ID
+  artifacts:
+    paths:
+      - wheels/
+    expire_in: 1 week
 
 build-c-wheel:
-    <<: *wheel
-    variables:
-      WHEEL_TYPE: "c"
+  extends: .build-wheel
+  variables:
+    WHEEL_TYPE: "c"
+  parallel:
+    matrix:
+      - BUILD_PY_ID:
+          - cp38-cp38
+          - cp39-cp39
+          - cp310-cp310
+          - cp311-cp311
+          - cp312-cp312
+          - cp313-cp313
 
-# TODO: We should select the wheel compatible with the python (and plateform)
-# we use. This is necessary to build multiple wheel.
-.runtests_template: &runtests
-    <<: *all
+.runtests:
+    extends: .all
     stage: tests
+    variables:
+      SHOW_VERSION_OF: "$PYTHON"
+      TEST_HGTESTS_ALLOW_NETIO: "0"
+      FILTER: ""
+      FLAVOR: ""
+      RUNTEST_ARGS: ""
     # The runner made a clone as root.
     # We make a new clone owned by user used to run the step.
     before_script:
       - echo "python used, $PYTHON"
       - for tool in $SHOW_VERSION_OF ; do echo '#' version of $tool; $tool --version; done
-      - rm -rf /tmp/mercurial-ci/  # Clean slate if not using containers
-      - hg clone . /tmp/mercurial-ci/ --noupdate --config phases.publish=no
-      - hg -R /tmp/mercurial-ci/ update `hg log --rev '.' --template '{node}'`
-      - cd /tmp/mercurial-ci/
-      - ls -1 tests/test-check-*.* > /tmp/check-tests.txt
+      - rm -rf "${TMP_WORK_DIR}"/mercurial-ci/  # Clean slate if not using containers
+      - hg clone . "${TMP_WORK_DIR}"/mercurial-ci/ --noupdate --config phases.publish=no
+      - hg -R "${TMP_WORK_DIR}"/mercurial-ci/ update `hg log --rev '.' --template '{node}'`
+      - cd "${TMP_WORK_DIR}"/mercurial-ci/
+      - ls -1 tests/test-check-*.* > "${TMP_WORK_DIR}"/check-tests.txt
     script:
+        - echo "$TEST_HGTESTS_ALLOW_NETIO"
         - echo "$RUNTEST_ARGS"
+        - echo "$FILTER"
+        - echo "$FLAVOR"
         - echo "$WHEEL_TYPE"
-        - WHEEL=""
+        - PORT_START=`expr 19051 + 1009 '*' $CI_CONCURRENT_ID`
+        - PORT_ARG="--port $PORT_START"
+        - echo $PORT_ARG
+        - PLATFORM=`$PYTHON -c 'import sys; print(sys.platform)'`
+        - echo $PLATFORM
+        - WHEEL_ARG=""
+        - SHARDING_ARGS=""
         - if test -n "$WHEEL_TYPE"; then
-             WHEEL="`ls -1 $CI_PROJECT_DIR/wheels/$WHEEL_TYPE/*.whl`";
+             PY_TAG=`$PYTHON -c 'import sys; v=sys.version_info; t=f"cp{v.major}{v.minor}"; print(f"{t}-{t}")'`;
+             echo "$PY_TAG";
+             test -n "PY_TAG";
+             WHEEL="`ls -1 $CI_PROJECT_DIR/wheels/$PLATFORM/$WHEEL_TYPE/$PY_TAG/*.whl`";
              test -n "$WHEEL";
-          fi
-        - if test -n "$WHEEL"; then
-            echo installing from $WHEEL;
-            HGTESTS_ALLOW_NETIO="$TEST_HGTESTS_ALLOW_NETIO" "$PYTHON" tests/run-tests.py --hg-wheel $WHEEL --color=always $RUNTEST_ARGS;
+             echo installing from $WHEEL;
+             WHEEL_ARG="--hg-wheel $WHEEL";
+             echo disabling flavor as this is currently incompatible with '"--hg-wheel"';
+             FLAVOR="";
           else
             echo installing from source;
-            HGTESTS_ALLOW_NETIO="$TEST_HGTESTS_ALLOW_NETIO" "$PYTHON" tests/run-tests.py --color=always $RUNTEST_ARGS;
+          fi;
+        - if [ -n "$CI_NODE_INDEX" ]; then 
+            echo "Running the test in multiple shard - [$CI_NODE_INDEX/$CI_NODE_TOTAL]";
+            SHARDING_ARGS="--shard-index $CI_NODE_INDEX --shard-total $CI_NODE_TOTAL";
+            echo "sharding... $SHARDING_ARGS";
           fi
+        - HGTESTS_ALLOW_NETIO="$TEST_HGTESTS_ALLOW_NETIO"
+          "$PYTHON" tests/run-tests.py
+            --color=always
+            $PORT_ARG
+            $WHEEL_ARG
+            $FLAVOR
+            $SHARDING_ARGS
+            $FILTER
+            $RUNTEST_ARGS;
 
 checks:
-    <<: *runtests
+    extends: .runtests
     stage: checks
     variables:
         SHOW_VERSION_OF: "$PYTHON black clang-format"
-        RUNTEST_ARGS: "--time --test-list /tmp/check-tests.txt"
+        RUNTEST_ARGS: "--time"
+        FILTER: "--test-list ${TMP_WORK_DIR}/check-tests.txt"
         CI_CLEVER_CLOUD_FLAVOR: S
 
 rust-cargo-test:
-    <<: *all
+    extends: .all
     stage: checks
     script:
         - make rust-tests
@@ -114,89 +178,120 @@
     variables:
         CI_CLEVER_CLOUD_FLAVOR: S
 
-.test-c: &test_c
-    <<: *runtests
+.runtests-no-check:
+  extends: .runtests
+  variables:
+      FILTER: "--blacklist ${TMP_WORK_DIR}/check-tests.txt"
+      TEST_HGTESTS_ALLOW_NETIO: "1"
+
+.test-c:
+    extends: .runtests-no-check
     variables:
-        RUNTEST_ARGS: " --no-rust --blacklist /tmp/check-tests.txt"
-        TEST_HGTESTS_ALLOW_NETIO: "1"
+        FLAVOR: "--no-rust"
 
 test-c:
-    <<: *test_c
-    needs: [build-c-wheel]
+    extends: .test-c
+    needs:
+      - job: build-c-wheel
+        parallel:
+          matrix:
+            - BUILD_PY_ID: "cp311-cp311"
     variables:
         WHEEL_TYPE: "c"
-        RUNTEST_ARGS: "--blacklist /tmp/check-tests.txt"
-        TEST_HGTESTS_ALLOW_NETIO: "1"
 
 test-pure:
-    <<: *runtests
+    extends: .runtests-no-check
     variables:
-        RUNTEST_ARGS: "--pure --blacklist /tmp/check-tests.txt"
+        FLAVOR: "--pure"
 
-test-rust: &test_rust
-    <<: *runtests
+test-rust:
+    extends: .runtests-no-check
     variables:
-        HGWITHRUSTEXT: cpython
-        RUNTEST_ARGS: "--rust --blacklist /tmp/check-tests.txt"
+        HGWITHRUSTEXT: "cpython"
+        FLAVOR: "--rust"
 
 test-rhg:
-    <<: *runtests
+    extends: .runtests-no-check
     variables:
-        HGWITHRUSTEXT: cpython
-        RUNTEST_ARGS: "--rust --rhg --blacklist /tmp/check-tests.txt"
+        HGWITHRUSTEXT: "cpython"
+        FLAVOR: "--rust --rhg"
 
 test-chg:
-    <<: *runtests
+    extends: .runtests-no-check
     variables:
-        RUNTEST_ARGS: "--blacklist /tmp/check-tests.txt --chg"
+        FLAVOR: "--chg"
+
+
+trigger-pycompat:
+  extends: .trigger
+  stage: py-version-compat
+
+.test-c-pycompat:
+    extends: .test-c
+    stage: py-version-compat
+    needs:
+      - trigger-pycompat
+    variables:
+        WHEEL_TYPE: "c"
 
 # note: we should probably get a full matrix for flavor × py-version, but this
 # is a simple start to be able to check if we break the lowest supported
 # version (and 3.12 have been giving us various troubles)
 test-3.8-c:
-    <<: *test_c
-    stage: py-version-compat
-    when: manual  # avoid overloading the CI by default
+    extends: .test-c-pycompat
     variables:
         PYTHON: python3.8
+    needs:
+      - job: build-c-wheel
+        parallel:
+          matrix:
+            - BUILD_PY_ID: "cp38-cp38"
 
 test-3.12-c:
-    <<: *test_c
-    stage: py-version-compat
-    when: manual  # avoid overloading the CI by default
+    extends: .test-c-pycompat
     variables:
         PYTHON: python3.12
+    needs:
+      - job: build-c-wheel
+        parallel:
+          matrix:
+            - BUILD_PY_ID: "cp312-cp312"
 
 test-3.12-rust:
-    <<: *test_rust
+    extends: test-rust
     stage: py-version-compat
-    when: manual  # avoid overloading the CI by default
+    needs:
+      - trigger-pycompat
     variables:
         PYTHON: python3.12
 
 test-3.13-c:
-    <<: *test_c
-    stage: py-version-compat
-    when: manual  # avoid overloading the CI by default
+    extends: .test-c-pycompat
     variables:
         PYTHON: python3.13
+    needs:
+      - job: build-c-wheel
+        parallel:
+          matrix:
+            - BUILD_PY_ID: "cp313-cp313"
 
 test-3.13-rust:
-    <<: *test_rust
+    extends: test-rust
     stage: py-version-compat
-    when: manual  # avoid overloading the CI by default
+    needs:
+      - trigger-pycompat
     variables:
         PYTHON: python3.13
 
 check-pytype:
-    <<: *test_rust
+    extends: test-rust
     stage: checks
     before_script:
       - export PATH="/home/ci-runner/vendor/pyenv/pyenv-2.4.7-adf3c2bccf09cdb81febcfd15b186711a33ac7a8/shims:/home/ci-runner/vendor/pyenv/pyenv-2.4.7-adf3c2bccf09cdb81febcfd15b186711a33ac7a8/bin:$PATH"
       - echo "PATH, $PATH"
-      - hg clone . /tmp/mercurial-ci/ --noupdate --config phases.publish=no
-      - hg -R /tmp/mercurial-ci/ update `hg log --rev '.' --template '{node}'`
-      - cd /tmp/mercurial-ci/
+      - hg clone . "${TMP_WORK_DIR}"/mercurial-ci/ --noupdate --config phases.publish=no
+      - hg -R "${TMP_WORK_DIR}"/mercurial-ci/ update `hg log --rev '.' --template '{node}'`
+      - cd "${TMP_WORK_DIR}"/mercurial-ci/
       - make local PYTHON=$PYTHON
       - ./contrib/setup-pytype.sh
     script:
@@ -208,44 +303,217 @@
 # is stored in OLDPWD.  Of the added variables, MSYSTEM is crucial to running
 # run-tests.py- it is needed to make run-tests.py generate a `python3` script
 # that satisfies the various shebang lines and delegates to `py -3`.
-.window_runtests_template: &windows_runtests
-    <<: *all
+
+.windows:
+    extends: .all
     when: manual  # we don't have any Windows runners anymore at the moment
+    tags:
+      - windows
+    before_script:
+      - C:/hgdev/MinGW/msys/1.0/bin/sh.exe --login -c 'cd "$OLDPWD" && ls -1 tests/test-check-*.* > "${TMP_WORK_DIR}"/check-tests.txt'
+      # TODO: find/install cvs, bzr, perforce, gpg, sqlite3
+    variables:
+        PYTHON: C:/hgdev/venvs/python39-x64/Scripts/python.exe
+
+# a dummy job that only serve to trigger the wider windows build
+trigger-wheel-windows:
+  extends: .trigger
+  stage: build
+
+build-c-wheel-windows:
+    extends: .windows
+    stage: build
+    # wait for someone to click on "trigger-wheel-windows"
+    when: on_success
+    needs:
+      - "trigger-wheel-windows"
+    variables:
+      MERCURIAL_SETUP_FORCE_TRANSLATIONS: "1"
+    script:
+        - echo "Entering script section"
+        - echo "python used, $Env:PYTHON"
+        - Invoke-Expression "$Env:PYTHON -V"
+        - echo "$Env:RUNTEST_ARGS"
+        - echo "$Env:TMP"
+        - echo "$Env:TEMP"
+        - "C:/hgdev/venvs/python39-x64/Scripts/python.exe -m cibuildwheel --output-dir wheels/win32"
+    artifacts:
+      paths:
+        - wheels
+      expire_in: 1 week
+    parallel:
+      matrix:
+        # "cp39" is first as it unlock the tests
+        - CIBW_BUILD:
+          - "cp39-*"
+          - "cp38-*"
+          - "cp310-*"
+          - "cp311-*"
+          - "cp312-*"
+          - "cp313-*"
+          CIBW_ARCHS:
+          - "AMD64"
+          - "x86"
+        - CIBW_BUILD:
+          - "cp311-*"
+          - "cp312-*"
+          - "cp313-*"
+          CIBW_ARCHS:
+          - "ARM64"
+
+
+.windows-runtests:
+    extends: .windows
     stage: platform-compat
-    before_script:
-      - C:/MinGW/msys/1.0/bin/sh.exe --login -c 'cd "$OLDPWD" && ls -1 tests/test-check-*.* > C:/Temp/check-tests.txt'
-      # TODO: find/install cvs, bzr, perforce, gpg, sqlite3
-
+    # the UX for manual parallel jobs is quite awful, and the job que depends
+    # upon are manual anyway, so we can make this start automatically once the
+    # associated wheel is ready.
+    when: on_success
+    parallel: 20
     script:
         - echo "Entering script section"
         - echo "python used, $Env:PYTHON"
         - Invoke-Expression "$Env:PYTHON -V"
-        - Invoke-Expression "$Env:PYTHON -m black --version"
+        - echo "$Env:HGTESTS_ALLOW_NETIO"
+        - echo "$Env:WHEEL_ARG"
+        - echo "$Env:FLAVOR"
+        - echo "$Env:FILTER"
         - echo "$Env:RUNTEST_ARGS"
         - echo "$Env:TMP"
         - echo "$Env:TEMP"
+        # This test is hanging the worker and not that important, so lets skip
+        # it for now
+        - C:/hgdev/MinGW/msys/1.0/bin/sh.exe -c 'cd "$OLDPWD" && echo tests/test-clonebundles-autogen.t > $TMP_WORK_DIR/windows-skip.txt'
 
-        - C:/MinGW/msys/1.0/bin/sh.exe --login -c 'cd "$OLDPWD" && HGTESTS_ALLOW_NETIO="$TEST_HGTESTS_ALLOW_NETIO" $PYTHON tests/run-tests.py --color=always $RUNTEST_ARGS'
+        - C:/hgdev/MinGW/msys/1.0/bin/sh.exe
+          --login -c 'cd "$OLDPWD"
+            && HGTESTS_ALLOW_NETIO="$TEST_HGTESTS_ALLOW_NETIO"
+               $PYTHON tests/run-tests.py
+               --color=always
+               $WHEEL_ARG
+               $FLAVOR
+               --port `expr 19051 + 1009 "*" $CI_CONCURRENT_ID`
+               --shard-index $CI_NODE_INDEX --shard-total $CI_NODE_TOTAL
+               $FILTER
+               $RUNTEST_ARGS;
+          '
+    variables:
+      WHEEL_ARG: ""
+      RUNTEST_ARGS: ""
+      FLAVOR: ""
+      FILTER: "--blacklist ${TMP_WORK_DIR}/check-tests.txt --blacklist ${TMP_WORK_DIR}/windows-skip.txt"
 
 windows:
-    <<: *windows_runtests
-    tags:
-      - windows
+    extends: .windows-runtests
     variables:
-        RUNTEST_ARGS: "--blacklist C:/Temp/check-tests.txt"
-        PYTHON: py -3
+        RUNTEST_ARGS: ""
+        WHEEL_ARG: "--hg-wheel wheels/win32/mercurial-*-cp39-cp39-win_amd64.whl"
+    needs:
+      - job: build-c-wheel-windows
+        parallel:
+          matrix:
+            - CIBW_BUILD: "cp39-*"
+              CIBW_ARCHS: "AMD64"
 
 windows-pyox:
-    <<: *windows_runtests
-    tags:
-      - windows
+    extends: .windows-runtests
+    when: manual  # pyoxidizer builds seem broken with --no-use-pep517
     variables:
-        RUNTEST_ARGS: "--blacklist C:/Temp/check-tests.txt --pyoxidized"
-        PYTHON: py -3
+        FLAVOR: "--pyoxidized"
 
 macos:
-    <<: *test_c
+    extends: .test-c
     stage: platform-compat
+    # run the test in multiple shard to help spread the load between concurrent
+    # MR as the macos runner is a shell runner there is not startup overhead
+    # for tests.
+    parallel: 10
+    tags:
+      - macos
+    variables:
+        WHEEL_TYPE: "c"
+    needs:
+      - build-c-wheel-macos
+
+# We could use CIBW_BUILD="cp310-*" to only build the Python 3.10 wheel for now as
+# this is the only one we need to test. However testing that build work on all
+# version is useful and match what we do with Linux.
+#
+# CIBW_SKIP is set globally at the start of the file. See comment there.
+#
+# The weird directory structure match the one we use for Linux to deal with the
+# multiple jobs. (all this might be unnecessary)
+build-c-wheel-macos:
     when: manual  # avoid overloading the CI by default
+    stage: build
     tags:
       - macos
+    variables:
+      MERCURIAL_SETUP_FORCE_TRANSLATIONS: "1"
+    script:
+      - PLATFORM=`$PYTHON -c 'import sys; print(sys.platform)'`
+      - rm -rf tmp-wheels
+      - cibuildwheel --output-dir tmp-wheels/
+      - for py_version in cp38-cp38 cp39-cp39 cp310-cp310 cp311-cp311 cp312-cp312 cp313-cp313; do
+          mkdir -p wheels/$PLATFORM/c/$py_version/;
+          mv tmp-wheels/*$py_version*.whl wheels/$PLATFORM/c/$py_version/;
+        done
+      - rm -rf tmp-wheels
+    artifacts:
+      paths:
+        - wheels
+      expire_in: 1 week
+
+# Upload nightly build wheel on the heptapod registry on test success
+#
+# At the time this task is added, since the mac wheels are built on shell
+# runner, those nightly are not be considered fully secured.
+#
+# In addition, since any job can upload package, pretty much anyone with CI
+# access can upload anything pretending to be any version. To fix it we would
+# have to prevent the CI token to upload to the registry and have dedicated
+# credential accessible only from protected branches.
+upload-wheel-nightly:
+  extends: .all
+  image: "registry.heptapod.net/mercurial/ci-images/twine:v3.0"
+  stage: upload
+  # because we don't want to upload only half of a wheel
+  interruptible: false
+  rules:
+    - if: $CI_COMMIT_BRANCH =~ /^branch\/.*/
+      # note that at the time of writing this, this job depends on multiple
+      # manual one. So it will not run by default, but will automatically run
+      # if the manual jobs are triggered.
+      #
+      # Also beware that "on_success" will ignore failure of manual test we
+      # directly depends on. This currently relevant for the "test-3.x-c"
+      # tests.
+      when: on_success
+    - if: $CI_COMMIT_BRANCH =~ /^topic\/.*/
+      when: never
+      # if you need to test this, make it
+      # when: manual
+      # allow_failure: true
+  needs:
+    - build-c-wheel
+    - build-c-wheel-macos
+    - build-c-wheel-windows
+    - test-c
+    - macos
+    # if we also requires windows to be happy, reach the "50 needed jobs" limit.
+    # So we need some intermediate job to reduce the number.
+    # - windows
+    - test-3.8-c
+    - test-3.12-c
+    - test-3.13-c
+  # It would be nice to be able to restrict that a bit to protected branch only
+  variables:
+    TWINE_USERNAME: gitlab-ci-token
+    TWINE_PASSWORD: $CI_JOB_TOKEN
+  script:
+    - twine
+      upload
+      --verbose
+      --repository-url ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi
+      wheels/*/*/*/*.whl
+      wheels/*/*.whl
--- a/contrib/import-checker.py	Mon Oct 21 12:58:40 2024 +0200
+++ b/contrib/import-checker.py	Thu Nov 14 16:45:23 2024 +0100
@@ -231,7 +231,7 @@
     yield 'importlib.machinery'  # python3 only
     yield 'importlib.util'  # python3 only
     yield 'packaging.version'
-    for m in 'fcntl', 'grp', 'pwd', 'termios':  # Unix only
+    for m in 'fcntl', 'grp', 'pwd', 'select', 'termios':  # Unix only
         yield m
     for m in 'cPickle', 'datetime':  # in Python (not C) on PyPy
         yield m
--- a/contrib/packaging/Makefile	Mon Oct 21 12:58:40 2024 +0200
+++ b/contrib/packaging/Makefile	Thu Nov 14 16:45:23 2024 +0100
@@ -129,7 +129,7 @@
 
 .PHONY: linux-wheels-x86_64
 linux-wheels-x86_64:
-	docker run -e "HGTEST_JOBS=$(shell nproc)" --rm -ti -v `pwd`/../..:/src quay.io/pypa/manylinux1_x86_64 /src/contrib/packaging/build-linux-wheels.sh
+	docker run --rm -ti -v `pwd`/../..:/src registry.heptapod.net/mercurial/ci-images/core-wheel-x86_64-c:v3.0 /src/contrib/packaging/build-linux-wheels.sh
 
 .PHONY: linux-wheels-i686
 linux-wheels-i686:
--- a/contrib/packaging/build-linux-wheels.sh	Mon Oct 21 12:58:40 2024 +0200
+++ b/contrib/packaging/build-linux-wheels.sh	Thu Nov 14 16:45:23 2024 +0100
@@ -1,34 +1,44 @@
 #!/bin/bash
-# This file is directly inspired by
-# https://github.com/pypa/python-manylinux-demo/blob/master/travis/build-wheels.sh
+
+# Intended to run within docker using image:
+#
+#  registry.heptapod.net/mercurial/ci-images/core-wheel-x86_64-c:v3.0
+#
+# we might want to factor most of this with the associated mercurial-core CI
+# definition. (i.e. move this script into a place where the CI can directly call it for its purpose)
+
 set -e -x
 
-PYTHON_TARGETS=$(ls -d /opt/python/cp27*/bin)
+PYTHON_TARGETS="cp38-cp38 cp39-cp39 cp310-cp310 cp311-cp311 cp312-cp312 cp313-cp313"
+
+export MERCURIAL_SETUP_FORCE_TRANSLATIONS=1
 
-# Create an user for the tests
-useradd hgbuilder
+# We need to copy the repository to ensure:
+# (1) we don't wrongly write roots files in the repository (or any other wrong
+#     users)
+# (2) we don't reuse pre-compiled extension built outside for manylinux and
+#     therefor not compatible.
+cp -r /src/ /tmp/src/
+cd /tmp/src/
+hg purge --all --no-confirm
 
-# Bypass uid/gid problems
-cp -R /src /io && chown -R hgbuilder:hgbuilder /io
+export HGRCPATH=/tmp/build-config.rc
+cat << EOF > $HGRCPATH
+[trusted]
+users=*
+groups=*
+EOF
 
-# Compile wheels for Python 2.X
-for PYBIN in $PYTHON_TARGETS; do
-    "${PYBIN}/pip" wheel /io/ -w wheelhouse/
+for py in $PYTHON_TARGETS; do
+    echo 'build wheel for' $py
+    # cleanup any previous wheel
+    tmp_wd="/tmp/wheels/$py/repaired"
+    rm -rf $tmp_wd
+    mkdir -p $tmp_wd
+    # build a new wheel
+    contrib/build-one-linux-wheel.sh $py $tmp_wd
+    # fix the owner back to the repository owner
+    chown `stat /src/ -c %u:%g` $tmp_wd/*.whl
+    mv $tmp_wd/*.whl /src/dist
 done
 
-# Bundle external shared libraries into the wheels with
-# auditwheel (https://github.com/pypa/auditwheel) repair.
-# It also fix the ABI tag on the wheel making it pip installable.
-for whl in wheelhouse/*.whl; do
-    auditwheel repair "$whl" -w /src/wheelhouse/
-done
-
-# Install packages and run the tests for all Python versions
-cd /io/tests/
-
-for PYBIN in $PYTHON_TARGETS; do
-    # Install mercurial wheel as root
-    "${PYBIN}/pip" install mercurial --no-index -f /src/wheelhouse
-    # But run tests as hgbuilder user (non-root)
-    su hgbuilder -c "\"${PYBIN}/python\" /io/tests/run-tests.py --with-hg=\"${PYBIN}/hg\" --blacklist=/io/contrib/packaging/linux-wheel-centos5-blacklist"
-done
--- a/mercurial/hgweb/server.py	Mon Oct 21 12:58:40 2024 +0200
+++ b/mercurial/hgweb/server.py	Thu Nov 14 16:45:23 2024 +0100
@@ -8,7 +8,6 @@
 
 from __future__ import annotations
 
-import errno
 import os
 import socket
 import sys
@@ -124,8 +123,7 @@
             # I/O below could raise another exception. So log the original
             # exception first to ensure it is recorded.
             if not (
-                isinstance(e, (OSError, socket.error))
-                and e.errno == errno.ECONNRESET
+                isinstance(e, (ConnectionResetError, ConnectionAbortedError))
             ):
                 tb = "".join(traceback.format_exception(*sys.exc_info()))
                 # We need a native-string newline to poke in the log
--- a/mercurial/interfaces/repository.py	Mon Oct 21 12:58:40 2024 +0200
+++ b/mercurial/interfaces/repository.py	Thu Nov 14 16:45:23 2024 +0100
@@ -95,6 +95,7 @@
 # (this is a mutable set to let extension update it)
 CACHES_POST_CLONE = CACHES_ALL.copy()
 CACHES_POST_CLONE.discard(CACHE_FILE_NODE_TAGS)
+CACHES_POST_CLONE.discard(CACHE_REV_BRANCH)
 
 
 class ipeerconnection(interfaceutil.Interface):
--- a/mercurial/merge.py	Mon Oct 21 12:58:40 2024 +0200
+++ b/mercurial/merge.py	Thu Nov 14 16:45:23 2024 +0100
@@ -2054,9 +2054,14 @@
             repo.hook(b'preupdate', throw=True, parent1=xp1, parent2=xp2)
             # note that we're in the middle of an update
             repo.vfs.write(b'updatestate', p2.hex())
+            num_cpus = (
+                repo.ui.configint(b"worker", b"numcpus", None)
+                if repo.ui.configbool(b"worker", b"enabled")
+                else 1
+            )
             try:
                 updated_count = rust_update_mod.update_from_null(
-                    repo.root, p2.rev()
+                    repo.root, p2.rev(), num_cpus
                 )
             except rust_update_mod.FallbackError:
                 update_from_null_fallback = True
--- a/mercurial/streamclone.py	Mon Oct 21 12:58:40 2024 +0200
+++ b/mercurial/streamclone.py	Thu Nov 14 16:45:23 2024 +0100
@@ -582,8 +582,10 @@
     """
 
     # arbitrarily picked as "it seemed fine" and much higher than the current
-    # usage.
-    MAX_OPEN = 100
+    # usage.  The Windows value of 2 is actually 1 file open at a time, due to
+    # the `flush_count = self.MAX_OPEN // 2` and `self.MAX_OPEN - 1` threshold
+    # for flushing to disk in __call__().
+    MAX_OPEN = 2 if pycompat.iswindows else 100
 
     def __init__(self):
         self._counter = 0
--- a/relnotes/6.9	Mon Oct 21 12:58:40 2024 +0200
+++ b/relnotes/6.9	Thu Nov 14 16:45:23 2024 +0100
@@ -1,3 +1,86 @@
+= Mercurial 6.9rc1 =
+
+/!\ These are release notes for a release candidate version. Any and all points can be reverted before the final release.
+
+ * streamclone: disable the volatile file open handle optimization on Windows
+ * rust-update: make `update_from_null` respect `worker.numcpu` config option
+ * rust-update: handle SIGINT from long-running update threads
+ * rust-cpython: add a TODO about repo reuse
+ * pytype: add relative timestamp to the output if `ts` is available
+ * hgweb: skip logging ConnectionAbortedError
+
+Below are many, many changes that have to do with building/testing wheels,
+adding some sharding to the CI and MacOS + Windows compatibility work:
+
+ * run-tests: don't use shell call for subprocess
+ * run-tests: add a --hg-wheel options to test a pre-built wheel
+ * ci: unify the way `check-pytype` inherit the common setting
+ * ci: split the jobs on more stage
+ * ci: build a wheel and use it to run c tests
+ * tests: stabilize `test-extdiff.t` on macOS
+ * tests: disable `test-git-interop.t` with a requirements directive
+ * tests: disable a section of `test-hgrc.t` that may hit a zeroconf bug
+ * ci: add a runner for Windows 10
+ * tests: treat `select` as a built-in module on Windows
+ * tests: disable a section of `test-paths.t` that may hit a zeroconf bug
+ * tests: conditionalize missing output in test-racy-mutations.t on Windows
+ * tests: add a "missing" tests for manifest content in test-racy-mutations.t
+ * tests: bump the wait timeouts in test-racy-mutations.t
+ * test-install: use the global hg for the install step
+ * test-install: glob instance of "python" in warning
+ * ci: pre-adjust some identation
+ * setup: add a way to force the setup to translate (or fail)
+ * ci: use smaller VM to build wheel
+ * ci: use a pre-setup many-linux image to build wheel
+ * ci: build (and use) wheel for all supported version
+ * ci: automatically compute the python tag we use to identify tag
+ * run-tests: install wheel using --prefix instead of --user
+ * pycompat: drop some now useless workaround for makedirs
+ * wheel: build mac os wheel through the CI
+ * ci: use the macos wheel to run tests
+ * ci: use extends instead of <<: *x
+ * ci: move some variables closer to their usage
+ * ci: rationalize variable usage
+ * ci: abstract the of absolute /tmp/ path
+ * ci: move the "tempory work dir" to "concurrency-safe" location
+ * ci: adjust the starting port range to runner concurrency
+ * ci: have the mac test run if you trigger building the mac wheel
+ * run-tests: implement crude sharding support
+ * ci: shard the test run on mac os X
+ * dev-version: change the scheme of non tagged version
+ * wheel: enforce that translation being build for macos wheel
+ * run-tests: focus on listing the selected test for the shard tests
+ * run-tests: cleanup the "output" directory after the related tests
+ * tests: drop PYTHONPATH manipulation in test-pushvars.t
+ * windows: work around argument size limitation in test-bookmarks-pushpull.t
+ * windows: adjust PYTHONPATH update in test-status-color.t
+ * ci: use a concurrency safe TMP dir on Windows
+ * ci: again common element into a `.windows` template
+ * ci: split the windows runtest invocation into more granular variables
+ * windows: skip test-clonebundles-autogen.t in the CI
+ * ci: adjust port range on windows too
+ * windows: simply rely on the PATH adjustment to find python.exe in tests
+ * wheel: assign CIBW_SKIP globally
+ * wheel: make --hg-wheel works on Windows
+ * wheel: build Windows wheels too
+ * wheel: explicitly list built architecture
+ * wheel: test the built wheel in the windows tests
+ * ci: shard the tests on windows too
+ * wheel: enforce that translation being build for windows wheel
+ * setup: remote a debug statement that slipped through
+ * setup: factor version computation in a function
+ * setup: use the same code to compute tag from archive
+ * wheel: add a platform level to the wheel directories
+ * wheel: add a job uploading nightly build
+ * wheels: factor the core of Linux wheel building into a script
+ * wheels: update the Linux wheels make target
+ * clone: properly exclude rev-branch-cache from post clone cache warming
+ * setup: make sure Rust build its extension for the right python
+ * setup: preserve version part after the "+" on Windows
+ * wheel: build windows wheel for ARM64 too
+ * ci: adds a trigger for all pycompat jobs
+ * ci: let the Windows runner decide how many job they want to run
+
 = Mercurial 6.9rc0 =
 
 /!\ These are release notes for a release candidate version. Any and all points can be reverted before the final release.
--- a/rust/Cargo.lock	Mon Oct 21 12:58:40 2024 +0200
+++ b/rust/Cargo.lock	Thu Nov 14 16:45:23 2024 +0100
@@ -171,6 +171,12 @@
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
 [[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
 name = "chrono"
 version = "0.4.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -350,6 +356,16 @@
 ]
 
 [[package]]
+name = "ctrlc"
+version = "3.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3"
+dependencies = [
+ "nix",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
 name = "cxx"
 version = "1.0.81"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -621,6 +637,7 @@
  "chrono",
  "clap",
  "crossbeam-channel",
+ "ctrlc",
  "derive_more",
  "dyn-clone",
  "filetime",
@@ -898,6 +915,18 @@
 ]
 
 [[package]]
+name = "nix"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
+dependencies = [
+ "bitflags 2.6.0",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+]
+
+[[package]]
 name = "nom8"
 version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1723,6 +1752,15 @@
 ]
 
 [[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
 name = "windows-targets"
 version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
--- a/rust/hg-core/Cargo.toml	Mon Oct 21 12:58:40 2024 +0200
+++ b/rust/hg-core/Cargo.toml	Thu Nov 14 16:45:23 2024 +0100
@@ -12,6 +12,7 @@
 bitflags = "1.3.2"
 bytes-cast = "0.3.0"
 byteorder = "1.4.3"
+ctrlc = "3.4"
 derive_more = "0.99.17"
 hashbrown = { version = "0.13.1", features = ["rayon"] }
 home = "0.5.4"
--- a/rust/hg-core/src/errors.rs	Mon Oct 21 12:58:40 2024 +0200
+++ b/rust/hg-core/src/errors.rs	Thu Nov 14 16:45:23 2024 +0100
@@ -52,6 +52,8 @@
     RaceDetected(String),
     /// An invalid path was found
     Path(HgPathError),
+    /// An interrupt was received and we need to stop whatever we're doing
+    InterruptReceived,
 }
 
 /// Details about where an I/O error happened
@@ -125,6 +127,7 @@
                 write!(f, "encountered a race condition {context}")
             }
             HgError::Path(hg_path_error) => write!(f, "{}", hg_path_error),
+            HgError::InterruptReceived => write!(f, "interrupt received"),
         }
     }
 }
--- a/rust/hg-core/src/lib.rs	Mon Oct 21 12:58:40 2024 +0200
+++ b/rust/hg-core/src/lib.rs	Thu Nov 14 16:45:23 2024 +0100
@@ -53,10 +53,14 @@
     parse_pattern_syntax_kind, read_pattern_file, IgnorePattern,
     PatternFileWarning, PatternSyntax,
 };
-use std::collections::HashMap;
 use std::fmt;
+use std::{collections::HashMap, sync::atomic::AtomicBool};
 use twox_hash::RandomXxHashBuilder64;
 
+/// Used to communicate with threads spawned from code within this crate that
+/// they should stop their work (SIGINT was received).
+pub static INTERRUPT_RECEIVED: AtomicBool = AtomicBool::new(false);
+
 pub type LineNumber = usize;
 
 /// Rust's default hasher is too slow because it tries to prevent collision
--- a/rust/hg-core/src/update.rs	Mon Oct 21 12:58:40 2024 +0200
+++ b/rust/hg-core/src/update.rs	Thu Nov 14 16:45:23 2024 +0100
@@ -5,6 +5,7 @@
     io::Write,
     os::unix::fs::{MetadataExt, PermissionsExt},
     path::Path,
+    sync::atomic::Ordering,
     time::Duration,
 };
 
@@ -24,12 +25,13 @@
     revlog::RevlogError,
     sparse,
     utils::{
+        cap_default_rayon_threads,
         files::{filesystem_now, get_path_from_bytes},
         hg_path::{hg_path_to_path_buf, HgPath, HgPathError},
         path_auditor::PathAuditor,
     },
     vfs::{is_on_nfs_mount, VfsImpl},
-    DirstateParents, UncheckedRevision,
+    DirstateParents, UncheckedRevision, INTERRUPT_RECEIVED,
 };
 use crossbeam_channel::{Receiver, Sender};
 use rayon::prelude::*;
@@ -50,6 +52,7 @@
     repo: &Repo,
     to: UncheckedRevision,
     progress: &dyn Progress,
+    workers: Option<usize>,
 ) -> Result<usize, HgError> {
     // Ignore the warnings, they've been displayed by Python already
     // TODO non-Python clients: display narrow warnings
@@ -103,6 +106,15 @@
     let chunks = chunk_tracked_files(tracked_files);
     progress.update(0, Some(files_count as u64));
 
+    // TODO find a way (with `nix` or `signal-hook`?) of resetting the
+    // previous signal handler directly after. Currently, this is Python's
+    // job, but:
+    //     - it introduces a (small) race between catching and resetting
+    //     - it would break signal handlers in other contexts like `rhg``
+    let _ = ctrlc::set_handler(|| {
+        INTERRUPT_RECEIVED.store(true, Ordering::Relaxed)
+    });
+
     create_working_copy(
         chunks,
         working_directory_path,
@@ -111,8 +123,15 @@
         files_sender,
         errors_sender,
         progress,
+        workers,
     );
 
+    // Reset the global interrupt now that we're done
+    if INTERRUPT_RECEIVED.swap(false, Ordering::Relaxed) {
+        // The threads have all exited early, let's re-raise
+        return Err(HgError::InterruptReceived);
+    }
+
     let errors: Vec<HgError> = errors_receiver.iter().collect();
     if !errors.is_empty() {
         log::debug!("{} errors during update (see trace logs)", errors.len());
@@ -182,6 +201,7 @@
 }
 
 #[logging_timer::time("trace")]
+#[allow(clippy::too_many_arguments)]
 fn create_working_copy<'a: 'b, 'b>(
     chunks: Vec<(&HgPath, Vec<ExpandedManifestEntry<'a>>)>,
     working_directory_path: &Path,
@@ -190,9 +210,11 @@
     files_sender: Sender<(&'b HgPath, u32, usize, TruncatedTimestamp)>,
     error_sender: Sender<HgError>,
     progress: &dyn Progress,
+    workers: Option<usize>,
 ) {
     let auditor = PathAuditor::new(working_directory_path);
-    chunks.into_par_iter().for_each(|(dir_path, chunk)| {
+
+    let work_closure = |(dir_path, chunk)| -> Result<(), HgError> {
         if let Err(e) = working_copy_worker(
             dir_path,
             chunk,
@@ -207,7 +229,37 @@
                 .send(e)
                 .expect("channel should not be disconnected")
         }
-    });
+        Ok(())
+    };
+    if let Some(workers) = workers {
+        if workers > 1 {
+            // Work in parallel, potentially restricting the number of threads
+            match rayon::ThreadPoolBuilder::new().num_threads(workers).build()
+            {
+                Err(error) => error_sender
+                    .send(HgError::abort(
+                        error.to_string(),
+                        exit_codes::ABORT,
+                        None,
+                    ))
+                    .expect("channel should not be disconnected"),
+                Ok(pool) => {
+                    log::trace!("restricting update to {} threads", workers);
+                    pool.install(|| {
+                        let _ =
+                            chunks.into_par_iter().try_for_each(work_closure);
+                    });
+                }
+            }
+        } else {
+            // Work sequentially, don't even invoke rayon
+            let _ = chunks.into_iter().try_for_each(work_closure);
+        }
+    } else {
+        // Work in parallel by default in the global threadpool
+        let _ = cap_default_rayon_threads();
+        let _ = chunks.into_par_iter().try_for_each(work_closure);
+    }
 }
 
 /// Represents a work unit for a single thread, responsible for this set of
@@ -228,6 +280,11 @@
     let dir_path = working_directory_path.join(dir_path);
     std::fs::create_dir_all(&dir_path).when_writing_file(&dir_path)?;
 
+    if INTERRUPT_RECEIVED.load(Ordering::Relaxed) {
+        // Stop working, the user has requested that we stop
+        return Err(HgError::InterruptReceived);
+    }
+
     for (file, file_node, flags) in chunk {
         auditor.audit_path(file)?;
         let flags = flags.map(|f| f.into());
--- a/rust/hg-core/src/utils.rs	Mon Oct 21 12:58:40 2024 +0200
+++ b/rust/hg-core/src/utils.rs	Thu Nov 14 16:45:23 2024 +0100
@@ -542,7 +542,7 @@
 /// Force the global rayon threadpool to not exceed 16 concurrent threads
 /// unless the user has specified a value.
 /// This is a stop-gap measure until we figure out why using more than 16
-/// threads makes `status` slower for each additional thread.
+/// threads makes `status` and `update` slower for each additional thread.
 ///
 /// TODO find the underlying cause and fix it, then remove this.
 ///
--- a/rust/hg-cpython/src/update.rs	Mon Oct 21 12:58:40 2024 +0200
+++ b/rust/hg-cpython/src/update.rs	Thu Nov 14 16:45:23 2024 +0100
@@ -15,18 +15,24 @@
 
 use crate::{
     exceptions::FallbackError,
-    utils::{hgerror_to_pyerr, repo_from_path},
+    utils::{hgerror_to_pyerr, repo_from_path, with_sigint_wrapper},
 };
 
 pub fn update_from_null_fast_path(
     py: Python,
     repo_path: PyObject,
     to: BaseRevision,
+    num_cpus: Option<usize>,
 ) -> PyResult<usize> {
     log::trace!("Using update from null fastpath");
     let repo = repo_from_path(py, repo_path)?;
     let progress: &dyn Progress = &HgProgressBar::new("updating");
-    hgerror_to_pyerr(py, update_from_null(&repo, to.into(), progress))
+
+    let res = with_sigint_wrapper(py, || {
+        update_from_null(&repo, to.into(), progress, num_cpus)
+    })?;
+
+    hgerror_to_pyerr(py, res)
 }
 
 pub fn init_module(py: Python, package: &str) -> PyResult<PyModule> {
@@ -41,7 +47,11 @@
         "update_from_null",
         py_fn!(
             py,
-            update_from_null_fast_path(repo_path: PyObject, to: BaseRevision,)
+            update_from_null_fast_path(
+                repo_path: PyObject,
+                to: BaseRevision,
+                num_cpus: Option<usize>
+            )
         ),
     )?;
 
--- a/rust/hg-cpython/src/utils.rs	Mon Oct 21 12:58:40 2024 +0200
+++ b/rust/hg-cpython/src/utils.rs	Thu Nov 14 16:45:23 2024 +0100
@@ -1,7 +1,7 @@
-use cpython::exc::ValueError;
+use cpython::exc::{KeyboardInterrupt, ValueError};
 use cpython::{
-    ObjectProtocol, PyBytes, PyDict, PyErr, PyObject, PyResult, PyTuple,
-    Python, ToPyObject,
+    ObjectProtocol, PyBytes, PyClone, PyDict, PyErr, PyObject, PyResult,
+    PyTuple, Python, ToPyObject,
 };
 use hg::config::Config;
 use hg::errors::HgError;
@@ -50,6 +50,9 @@
                 cls.call(py, (msg,), None).ok().into_py_object(py),
             )
         }
+        HgError::InterruptReceived => {
+            PyErr::new::<KeyboardInterrupt, _>(py, "")
+        }
         e => PyErr::new::<cpython::exc::RuntimeError, _>(py, e.to_string()),
     })
 }
@@ -64,6 +67,8 @@
 /// Get a repository from a given [`PyObject`] path, and bubble up any error
 /// that comes up.
 pub fn repo_from_path(py: Python, repo_path: PyObject) -> Result<Repo, PyErr> {
+    // TODO make the Config a Python class and downcast it here, otherwise we
+    // lose CLI args and runtime overrides done in Python.
     let config =
         hgerror_to_pyerr(py, Config::load_non_repo().map_err(HgError::from))?;
     let py_bytes = &repo_path.extract::<PyBytes>(py)?;
@@ -102,3 +107,38 @@
         })
         .map(Into::into)
 }
+
+/// Wrap a call to `func` so that Python's `SIGINT` handler is first stored,
+/// then restored after the call to `func` and finally raised if
+/// `func` returns a [`HgError::InterruptReceived`]
+pub fn with_sigint_wrapper<R>(
+    py: Python,
+    func: impl Fn() -> Result<R, HgError>,
+) -> PyResult<Result<R, HgError>> {
+    let signal_py_mod = py.import("signal")?;
+    let sigint_py_const = signal_py_mod.get(py, "SIGINT")?;
+    let old_handler = signal_py_mod.call(
+        py,
+        "getsignal",
+        PyTuple::new(py, &[sigint_py_const.clone_ref(py)]),
+        None,
+    )?;
+    let res = func();
+    // Reset the old signal handler in Python because we've may have changed it
+    signal_py_mod.call(
+        py,
+        "signal",
+        PyTuple::new(py, &[sigint_py_const.clone_ref(py), old_handler]),
+        None,
+    )?;
+    if let Err(HgError::InterruptReceived) = res {
+        // Trigger the signal in Python
+        signal_py_mod.call(
+            py,
+            "raise_signal",
+            PyTuple::new(py, &[sigint_py_const]),
+            None,
+        )?;
+    }
+    Ok(res)
+}
--- a/setup.py	Mon Oct 21 12:58:40 2024 +0200
+++ b/setup.py	Thu Nov 14 16:45:23 2024 +0100
@@ -375,9 +375,7 @@
         eprint(r"/!\ Failed to retrieve current revision tags")
         return ''
     if numerictags:  # tag(s) found
-        version = numerictags[-1]
-        if hgid.endswith('+'):  # propagate the dirty status to the tag
-            version += '+'
+        return _version(tag=numerictags[-1], dirty=hgid.endswith('+'))
     else:  # no tag found on the checked out revision
         ltagcmd = ['log', '--rev', 'wdir()', '--template', '{latesttag}']
         ltag = sysstr(hg.run(ltagcmd))
@@ -396,9 +394,59 @@
             "only(parents(),'%s')" % ltag,
         ]
         changessince = len(hg.run(changessincecmd).splitlines())
-        version = '%s+hg%s.%s' % (ltag, changessince, hgid)
-    if version.endswith('+'):
-        version = version[:-1] + 'local' + time.strftime('%Y%m%d')
+        branch = hg.run(["branch"]).strip()
+        return _version(
+            tag=ltag,
+            branch=branch,
+            hgid=hgid.rstrip('+'),
+            changes_since=changessince,
+            dirty=hgid.endswith('+'),
+        )
+
+
+def _version(
+    tag: str,
+    branch: str = '',
+    hgid: str = '',
+    changes_since: int = 0,
+    dirty: bool = False,
+):
+    """compute a version number from available information"""
+    version = tag
+    if changes_since > 0:
+        assert branch
+        if branch == b'stable':
+            post_nb = 0
+        elif branch == b'default':
+            # we use 1 here to be greater than 0 to make sure change from
+            # default are considered newer than change on stable
+            post_nb = 1
+        else:
+            # what is this branch ? probably a local variant ?
+            post_nb = 2
+
+        assert hgid
+
+        # logic of the scheme
+        # - '.postX' to mark the version as "above" the tagged version
+        #   X is 0 for stable, 1 for default, 2 for anything else
+        # - use '.devY'
+        #   Y is the number of extra revision compared to the tag. So that
+        #   revision with more change are "above" previous ones.
+        # - '+hg.NODEID.local.DATE' if there is any uncommitted changes.
+        version += '.post%d.dev%d+hg.%s' % (post_nb, changes_since, hgid)
+    if dirty:
+        version = version[:-1] + '.local.' + time.strftime('%Y%m%d')
+    # try to give warning early about bad version if possible
+    try:
+        from packaging.version import Version
+
+        Version(version)
+    except ImportError:
+        pass
+    except ValueError as exc:
+        eprint(r"/!\ generated version is invalid")
+        eprint(r"/!\ error: %s" % exc)
     return version
 
 
@@ -409,16 +457,23 @@
         [[t.strip() for t in l.split(':', 1)] for l in open('.hg_archival.txt')]
     )
     if 'tag' in kw:
-        version = kw['tag']
+        version = _version(tag=kw['tag'])
     elif 'latesttag' in kw:
-        if 'changessincelatesttag' in kw:
-            version = (
-                '%(latesttag)s+hg%(changessincelatesttag)s.%(node).12s' % kw
-            )
-        else:
-            version = '%(latesttag)s+hg%(latesttagdistance)s.%(node).12s' % kw
+        distance = int(kw.get('changessincelatesttag', kw['latesttagdistance']))
+        version = _version(
+            tag=kw['latesttag'],
+            branch=kw['branch'],
+            changes_since=distance,
+            hgid=kw['node'][:12],
+        )
     else:
-        version = '0+hg' + kw.get('node', '')[:12]
+        version = _version(
+            tag='0',
+            branch='unknown-source',
+            changes_since=1,
+            hgid=kw.get('node', 'unknownid')[:12],
+            dirty=True,
+        )
 elif os.path.exists('mercurial/__version__.py'):
     with open('mercurial/__version__.py') as f:
         data = f.read()
@@ -464,6 +519,14 @@
     description = "build translations (.mo files)"
 
     def run(self):
+        result = self._run()
+        if (
+            not result
+            and os.environ.get('MERCURIAL_SETUP_FORCE_TRANSLATIONS') == '1'
+        ):
+            raise DistutilsExecError("failed to build translations")
+
+    def _run(self):
         try:
             from shutil import which as find_executable
         except ImportError:
@@ -475,12 +538,12 @@
                 "could not find msgfmt executable, no translations "
                 "will be built"
             )
-            return
+            return False
 
         podir = 'i18n'
         if not os.path.isdir(podir):
             self.warn("could not find %s/ directory" % podir)
-            return
+            return False
 
         join = os.path.join
         for po in os.listdir(podir):
@@ -496,6 +559,7 @@
                 cmd.append('-c')
             self.mkpath(join('mercurial', modir))
             self.make_file([pofile], mobuildfile, spawn, (cmd,))
+        return True
 
 
 class hgdist(Distribution):
@@ -1502,6 +1566,15 @@
 
             env['HOME'] = pwd.getpwuid(os.getuid()).pw_dir
 
+        # Wildy shooting in the dark to make sure rust-cpython use the right
+        # python
+        if not sys.executable:
+            msg = "Cannot determine which Python to compile Rust for"
+            raise RustCompilationError(msg)
+        env['PYTHON_SYS_EXECUTABLE'] = sys.executable
+        env['PYTHONEXECUTABLE'] = sys.executable
+        env['PYTHON'] = sys.executable
+
         cargocmd = ['cargo', 'rustc', '--release']
 
         rust_features = env.get("HG_RUST_FEATURES")
@@ -1760,11 +1833,6 @@
 if os.environ.get('PYOXIDIZER'):
     hgbuild.sub_commands.insert(0, ('build_hgextindex', None))
 
-if os.name == 'nt':
-    # Windows binary file versions for exe/dll files must have the
-    # form W.X.Y.Z, where W,X,Y,Z are numbers in the range 0..65535
-    setupversion = setupversion.split(r'+', 1)[0]
-
 setup(
     version=setupversion,
     long_description=(
--- a/tests/run-tests.py	Mon Oct 21 12:58:40 2024 +0200
+++ b/tests/run-tests.py	Thu Nov 14 16:45:23 2024 +0100
@@ -61,7 +61,6 @@
 import shlex
 import shutil
 import signal
-import site
 import socket
 import subprocess
 import sys
@@ -448,6 +447,18 @@
         help="skip tests listed in the specified blacklist file",
     )
     selection.add_argument(
+        "--shard-total",
+        type=int,
+        default=None,
+        help="total number of shard to use (enable sharding)",
+    )
+    selection.add_argument(
+        "--shard-index",
+        type=int,
+        default=None,
+        help="index of this shard [1-N]",
+    )
+    selection.add_argument(
         "--changed",
         help="run tests that are changed in parent rev or working directory",
     )
@@ -886,6 +897,32 @@
     if options.showchannels:
         options.nodiff = True
 
+    if options.shard_total is not None:
+        if options.shard_index is None:
+            parser.error("--shard-total requires --shard-index to be set")
+
+    if options.shard_index is not None:
+        if options.shard_total is None:
+            parser.error("--shard-index requires --shard-total to be set")
+        elif options.shard_index <= 0:
+            msg = "--shard-index must be > 0 (%d)"
+            msg %= options.shard_index
+            parser.error(msg)
+        elif options.shard_index > options.shard_total:
+            msg = (
+                "--shard-index must be <= than --shard-total (%d not in [1,%d])"
+            )
+            msg %= (options.shard_index, options.shard_total)
+            parser.error(msg)
+
+    if options.shard_total is not None and options.order_by_runtime:
+        msg = "cannot use --order-by-runtime when sharding"
+        parser.error(msg)
+
+    if options.shard_total is not None and options.random:
+        msg = "cannot use --random when sharding"
+        parser.error(msg)
+
     return options
 
 
@@ -3158,7 +3195,11 @@
                 import statprof
 
                 statprof.start()
-            result = self._run(testdescs)
+            result = self._run(
+                testdescs,
+                shard_index=options.shard_index,
+                shard_total=options.shard_total,
+            )
             if options.profile_runner:
                 statprof.stop()
                 statprof.display()
@@ -3167,7 +3208,7 @@
         finally:
             os.umask(oldmask)
 
-    def _run(self, testdescs):
+    def _run(self, testdescs, shard_index=None, shard_total=None):
         testdir = getcwdb()
         # assume all tests in same folder for now
         if testdescs:
@@ -3268,11 +3309,34 @@
 
         else:
             self._installdir = os.path.join(self._hgtmp, b"install")
-            self._bindir = os.path.join(self._installdir, b"bin")
+            if WINDOWS:
+                # The wheel variant will install things in "Scripts".
+                # So we can as well always install things here.
+                self._bindir = os.path.join(self._installdir, b"Scripts")
+            else:
+                self._bindir = os.path.join(self._installdir, b"bin")
             self._hgcommand = b'hg'
 
-            if self.options.wheel:
-                suffix = _sys2bytes(site.USER_SITE[len(site.USER_BASE) + 1 :])
+            if self.options.wheel and not WINDOWS:
+                # pip installing a wheel does not have an --install-lib flag
+                # so we have to guess where the file will be installed.
+                #
+                # In addition, that location is not really stable, so we are
+                # using awful symlink trrick later in `_installhg`
+                v_info = sys.version_info
+                suffix = os.path.join(
+                    b"lib",
+                    b"python%d.%d" % (v_info.major, v_info.minor),
+                    b"site-packages",
+                )
+            elif self.options.wheel and WINDOWS:
+                # for some reason, Windows use an even different scheme:
+                #
+                # <prefix>/lib/site-packages/
+                suffix = os.path.join(
+                    b"lib",
+                    b"site-packages",
+                )
             else:
                 suffix = os.path.join(b"lib", b"python")
             self._pythondir = os.path.join(self._installdir, suffix)
@@ -3281,7 +3345,13 @@
         # a python script and feed it to python.exe.  Legacy stdio is force
         # enabled by hg.exe, and this is a more realistic way to launch hg
         # anyway.
-        if WINDOWS and not self._hgcommand.endswith(b'.exe'):
+        #
+        # We do not do it when using wheels and they do not install a .exe.
+        if (
+            WINDOWS
+            and not self.options.wheel
+            and not self._hgcommand.endswith(b'.exe')
+        ):
             self._hgcommand += b'.exe'
 
         real_hg = os.path.join(self._bindir, self._hgcommand)
@@ -3445,6 +3515,14 @@
         )
         vlog("# Writing to directory", _bytes2sys(self._outputdir))
 
+        if shard_total is not None:
+            slot = shard_index - 1
+            testdescs = [
+                t
+                for (idx, t) in enumerate(testdescs)
+                if (idx % shard_total == slot)
+            ]
+
         try:
             return self._runtests(testdescs) or 0
         finally:
@@ -3720,10 +3798,7 @@
     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:
-            pyexe_names = [b'python', b'python3', b'python.exe']
-        else:
-            pyexe_names = [b'python', b'python3']
+        pyexe_names = [b'python', b'python3']
 
         # os.symlink() is a thing with py3 on Windows, but it requires
         # Administrator rights.
@@ -3764,7 +3839,8 @@
                     f.write(b'%s "$@"\n' % esc_executable)
 
             if WINDOWS:
-                # adjust the path to make sur the main python finds it own dll
+                # adjust the path to make sur the main python finds itself and
+                # its own dll
                 path = os.environ['PATH'].split(os.pathsep)
                 main_exec_dir = os.path.dirname(sysexecutable)
                 extra_paths = [_bytes2sys(self._custom_bin_dir), main_exec_dir]
@@ -3826,16 +3902,12 @@
             wheel_path,
             b"--force",
             b"--ignore-installed",
-            b"--user",
-            b"--break-system-packages",
+            b"--prefix",
+            self._installdir,
         ]
         if not WINDOWS:
-            # The --home="" trick works only on OS where os.sep == '/'
-            # because of a distutils convert_path() fast-path. Avoid it at
-            # least on Windows for now, deal with .pydistutils.cfg bugs
-            # when they happen.
-            # cmd.append(b"--global-option=--home=")
-            pass
+            # windows does not have this flag apparently.
+            cmd.append(b"--break-system-packages")
 
         return cmd
 
@@ -3909,14 +3981,18 @@
             install_env.pop('HGWITHRUSTEXT', None)
 
         # setuptools requires install directories to exist.
-        def makedirs(p):
-            try:
-                os.makedirs(p)
-            except FileExistsError:
-                pass
-
-        makedirs(self._pythondir)
-        makedirs(self._bindir)
+        os.makedirs(self._pythondir, exist_ok=True)
+        os.makedirs(self._bindir, exist_ok=True)
+        if self.options.wheel is not None and not WINDOWS:
+            # the wheel instalation location is not stable, so try to deal with
+            # that to funnel it back where we need its.
+            #
+            # (mostly deals with Debian shenanigans)
+            assert self._pythondir.endswith(b'site-packages')
+            lib_dir = os.path.dirname(self._pythondir)
+            dist_dir = os.path.join(lib_dir, b'dist-packages')
+            os.symlink(b'./site-packages', dist_dir)
+            os.symlink(b'.', os.path.join(self._installdir, b'local'))
 
         vlog("# Running", cmd)
         with open(installerrs, "wb") as logfile:
--- a/tests/test-bookmarks-pushpull.t	Mon Oct 21 12:58:40 2024 +0200
+++ b/tests/test-bookmarks-pushpull.t	Thu Nov 14 16:45:23 2024 +0100
@@ -1155,20 +1155,21 @@
 ===============================================
 
 #if b2-binary
+(use `sh -c` as Windows struggle with the long argument)
   >>> with open('longname', 'w') as f:
   ...     f.write('wat' * 100) and None
-  $ hg book `cat longname`
-  $ hg push -B `cat longname` ../unchanged-b
+  $ sh -c "hg book `cat longname`"
+  $ sh -c "hg push -B `cat longname` ../unchanged-b"
   pushing to ../unchanged-b
   searching for changes
   no changes found
   exporting bookmark (wat){100} (re)
   [1]
-  $ hg -R ../unchanged-b book --delete `cat longname`
+  $ sh -c "hg -R ../unchanged-b book --delete `cat longname`"
 
 Test again but forcing bundle2 exchange to make sure that doesn't regress.
 
-  $ hg push -B `cat longname` ../unchanged-b --config devel.legacy.exchange=bundle1
+  $ sh -c "hg push -B `cat longname` ../unchanged-b --config devel.legacy.exchange=bundle1"
   pushing to ../unchanged-b
   searching for changes
   no changes found
--- a/tests/test-extdiff.t	Mon Oct 21 12:58:40 2024 +0200
+++ b/tests/test-extdiff.t	Thu Nov 14 16:45:23 2024 +0100
@@ -167,6 +167,7 @@
   [255]
 #endif
 
+#if gui
 Test --per-file option for gui tool:
 
   $ DISPLAY=fake hg --config extdiff.gui.alabalaf=True alabalaf -c 6 --per-file --debug
@@ -198,6 +199,7 @@
   running '* diffing * *' in * (backgrounded) (glob)
   cleaning up temp directory
   [1]
+#endif
 
 Test --per-file and --confirm options:
 
--- a/tests/test-git-interop.t	Mon Oct 21 12:58:40 2024 +0200
+++ b/tests/test-git-interop.t	Thu Nov 14 16:45:23 2024 +0100
@@ -1,4 +1,4 @@
-#require pygit2 no-windows
+#require pygit2 no-windows missing-correct-output
 
 Setup:
   $ GIT_AUTHOR_NAME='test'; export GIT_AUTHOR_NAME
--- a/tests/test-hgrc.t	Mon Oct 21 12:58:40 2024 +0200
+++ b/tests/test-hgrc.t	Thu Nov 14 16:45:23 2024 +0100
@@ -304,6 +304,10 @@
   config error at $TESTTMP/.hg/hgrc:3: [broken
   [255]
 
+XXX: This occasionally crashes with a bytes vs str problem when processing a
+packet response, so disable it for now.
+
+#if missing-correct-output
   $ HGRCSKIPREPO=1 hg paths --config extensions.zeroconf=
   foo = $TESTTMP/bar
-
+#endif
--- a/tests/test-install.t	Mon Oct 21 12:58:40 2024 +0200
+++ b/tests/test-install.t	Thu Nov 14 16:45:23 2024 +0100
@@ -1,3 +1,5 @@
+  $ . "$RUNTESTDIR/helpers-testrepo.sh"
+
 hg debuginstall
   $ hg debuginstall
   checking encoding (ascii)...
@@ -212,13 +214,13 @@
 
 Note: we use this weird path to run pip and hg to avoid platform differences,
 since it's bin on most platforms but Scripts on Windows.
-  $ ./installenv/*/pip install $TESTDIR/.. >> pip.log
+  $ (syshgenv; ./installenv/*/pip install $TESTDIR/.. >> pip.log)
     Failed building wheel for mercurial (?)
   WARNING: You are using pip version *; however, version * is available. (glob) (?)
   You should consider upgrading via the '$TESTTMP/installenv/bin/python* -m pip install --upgrade pip' command. (glob) (?)
    (?)
   [notice] A new release of pip is available: * -> * (glob) (?)
-  [notice] To update, run: python -m pip install --upgrade pip (?)
+  [notice] To update, run: * -m pip install --upgrade pip (glob) (?)
   $ ./installenv/*/hg debuginstall || cat pip.log
   checking encoding (ascii)...
   checking Python executable (*) (glob)
--- a/tests/test-paths.t	Mon Oct 21 12:58:40 2024 +0200
+++ b/tests/test-paths.t	Thu Nov 14 16:45:23 2024 +0100
@@ -140,11 +140,16 @@
 
 zeroconf wraps ui.configitems(), which shouldn't crash at least:
 
+XXX: This occasionally crashes with 'TypeError: ord() expected string of length
+1, but int found' when processing a packet response, so disable it for now.
+
+#if missing-correct-output
   $ hg paths --config extensions.zeroconf=
   dupe = $TESTTMP/b#tip
   dupe:pushurl = https://example.com/dupe
   expand = $TESTTMP/a/$SOMETHING/bar
   insecure = http://foo:***@example.com/
+#endif
 
 
   $ cd ..
--- a/tests/test-pushvars.t	Mon Oct 21 12:58:40 2024 +0200
+++ b/tests/test-pushvars.t	Thu Nov 14 16:45:23 2024 +0100
@@ -1,8 +1,5 @@
 Setup
 
-  $ PYTHONPATH=$TESTDIR/..:$PYTHONPATH
-  $ export PYTHONPATH
-
   $ cat > $TESTTMP/pretxnchangegroup.sh << EOF
   > #!/bin/sh
   > env | grep -E "^HG_USERVAR_(DEBUG|BYPASS_REVIEW)" | sort
--- a/tests/test-racy-mutations.t	Mon Oct 21 12:58:40 2024 +0200
+++ b/tests/test-racy-mutations.t	Thu Nov 14 16:45:23 2024 +0100
@@ -17,7 +17,7 @@
   >     [ -n "\${WAITLOCK_ANNOUNCE:-}" ] && touch "\${WAITLOCK_ANNOUNCE}"
   >     f="\${WAITLOCK_FILE}"
   >     start=\`date +%s\`
-  >     timeout=5
+  >     timeout=20
   >     "$RUNTESTDIR_FORWARD_SLASH/testlib/wait-on-file" "\$timeout" "\$f"
   >     if [ \$# -gt 1 ]; then
   >         cat "\$@"
@@ -65,19 +65,23 @@
   > ) &
 
 Wait for the "editor" to actually start
-  $ sh "$RUNTESTDIR_FORWARD_SLASH/testlib/wait-on-file" 5 "${EDITOR_STARTED}"
-
+  $ sh "$RUNTESTDIR_FORWARD_SLASH/testlib/wait-on-file" 20 "${EDITOR_STARTED}"
 
 Do a concurrent edition
   $ cd ../racing-client
   $ touch ../pre-race
-  $ sleep 1
+  $ sleep 10
   $ echo bar > bar
   $ hg --repository ../racing-client commit -qAm 'r2 (bar)' bar
   $ hg --repository ../racing-client debugrevlogindex -c
      rev linkrev nodeid       p1           p2
        0       0 222799e2f90b 000000000000 000000000000
        1       1 6f124f6007a0 222799e2f90b 000000000000
+  $ hg --repository ../racing-client debugrevlogindex -m
+     rev linkrev nodeid       p1           p2
+       0       0 7b7020262a56 000000000000 000000000000
+       1       1 ad3fe36d86d9 7b7020262a56 000000000000
+
 
 We simulate an network FS race by overwriting raced repo content with the new
 content of the files changed in the racing repository
@@ -102,6 +106,15 @@
        0       0 222799e2f90b 000000000000 000000000000
        1       1 6f124f6007a0 222799e2f90b 000000000000
        2       1 ac80e6205bb2 222799e2f90b 000000000000
+
+TODO: Figure out why the middle entry is missing on Windows.
+  $ hg debugrevlogindex -m
+     rev linkrev nodeid       p1           p2
+       0       0 7b7020262a56 000000000000 000000000000
+       1       1 ad3fe36d86d9 7b7020262a56 000000000000 (no-windows !)
+       2       1 d93163bb8ce3 7b7020262a56 000000000000 (no-windows !)
+       1       1 d93163bb8ce3 7b7020262a56 000000000000 (windows !)
+
 #endif
 
 #if fail-if-detected
@@ -116,10 +129,14 @@
      rev linkrev nodeid       p1           p2
        0       0 222799e2f90b 000000000000 000000000000
        1       1 6f124f6007a0 222799e2f90b 000000000000 (missing-correct-output !)
+
 And, because of transactions, there's none in the manifestlog either.
+
+TODO: Figure out why this is different on Windows.
   $ hg debugrevlogindex -m
      rev linkrev nodeid       p1           p2
        0       0 7b7020262a56 000000000000 000000000000
-       1       1 ad3fe36d86d9 7b7020262a56 000000000000
+       1       1 ad3fe36d86d9 7b7020262a56 000000000000 (no-windows !)
+       1       1 ad3fe36d86d9 7b7020262a56 000000000000 (missing-correct-output windows !)
 #endif
 
--- a/tests/test-run-tests.t	Mon Oct 21 12:58:40 2024 +0200
+++ b/tests/test-run-tests.t	Thu Nov 14 16:45:23 2024 +0100
@@ -1762,6 +1762,7 @@
   # Ran 2 tests, 0 skipped, 1 failed.
   python hash seed: * (glob)
   [1]
+  $ rm output/*
 
 Test TESTCASE variable
 
@@ -2087,3 +2088,34 @@
   3.* (glob)
   $ ./test-py.py
   3.* (glob)
+
+Test sharding
+=============
+
+  $ rt --shard-index 1
+  usage: run-tests.py [options] [tests]
+  run-tests.py: error: --shard-index requires --shard-total to be set
+  [2]
+  $ rt --shard-total 15
+  usage: run-tests.py [options] [tests]
+  run-tests.py: error: --shard-total requires --shard-index to be set
+  [2]
+  $ rt --shard-index -2 --shard-total 15
+  usage: run-tests.py [options] [tests]
+  run-tests.py: error: --shard-index must be > 0 (-2)
+  [2]
+  $ rt --shard-index 10 --shard-total 5
+  usage: run-tests.py [options] [tests]
+  run-tests.py: error: --shard-index must be <= than --shard-total (10 not in [1,5])
+  [2]
+  $ rt --list-tests --shard-index 1 --shard-total 5
+  test-cases-abc.t#A
+  test-cases-advanced-cases.t#casewith_-.chars
+  test-config-opt.t
+  $ rt --shard-index 6 --shard-total 5
+  usage: run-tests.py [options] [tests]
+  run-tests.py: error: --shard-index must be <= than --shard-total (6 not in [1,5])
+  [2]
+  $ rt --list-tests --shard-index 5 --shard-total 5
+  test-cases-advanced-cases.t#case-with-dashes
+  test-conditional-matching.t#foo
--- a/tests/test-status-color.t	Mon Oct 21 12:58:40 2024 +0200
+++ b/tests/test-status-color.t	Thu Nov 14 16:45:23 2024 +0100
@@ -399,9 +399,16 @@
 color coding of error message without curses
 
   $ echo 'raise ImportError' > curses.py
+#if windows
+  $ PYTHONPATH="`pwd`;$PYTHONPATH" hg unknowncommand > /dev/null
+  hg: unknown command 'unknowncommand'
+  (use 'hg help' for a list of commands)
+  [10]
+#else
   $ PYTHONPATH=`pwd`:$PYTHONPATH hg unknowncommand > /dev/null
   hg: unknown command 'unknowncommand'
   (use 'hg help' for a list of commands)
   [10]
+#endif
 
   $ cd ..