diff -pruN 1.17.0-2/.github/workflows/codespell.yml 1.18.1-1/.github/workflows/codespell.yml
--- 1.17.0-2/.github/workflows/codespell.yml	1970-01-01 00:00:00.000000000 +0000
+++ 1.18.1-1/.github/workflows/codespell.yml	2025-07-26 10:03:19.000000000 +0000
@@ -0,0 +1,23 @@
+# Codespell configuration is within pyproject.toml
+---
+name: Codespell
+
+on:
+  pull_request:
+  push:
+
+permissions:
+  contents: read
+
+jobs:
+  codespell:
+    name: Check for spelling errors
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+      - name: Annotate locations with typos
+        uses: codespell-project/codespell-problem-matcher@v1
+      - name: Codespell
+        uses: codespell-project/actions-codespell@v2
diff -pruN 1.17.0-2/.github/workflows/lint.yml 1.18.1-1/.github/workflows/lint.yml
--- 1.17.0-2/.github/workflows/lint.yml	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/.github/workflows/lint.yml	2025-07-26 10:03:19.000000000 +0000
@@ -17,10 +17,13 @@ jobs:
     - name: Set up Python
       uses: actions/setup-python@v5
       with:
-        python-version: '3.12'
+        python-version: '3.13'
 
     - name: Install ruff
       run: pip install ruff
 
     - name: Check code style with ruff
       run: ruff format --diff
+
+    - name: Check typing with mypy
+      run: LIBSSH2_VERSION=1.11.1 LIBGIT2_VERSION=1.9.1 /bin/sh build.sh mypy
diff -pruN 1.17.0-2/.github/workflows/tests.yml 1.18.1-1/.github/workflows/tests.yml
--- 1.17.0-2/.github/workflows/tests.yml	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/.github/workflows/tests.yml	2025-07-26 10:03:19.000000000 +0000
@@ -7,7 +7,7 @@ on:
     - '**.rst'
 
 jobs:
-  linux-x86_64:
+  linux:
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
@@ -18,6 +18,8 @@ jobs:
           python-version: '3.13'
         - os: ubuntu-24.04
           python-version: 'pypy3.10'
+        - os: ubuntu-24.04-arm
+          python-version: '3.13'
 
     steps:
     - name: Checkout pygit2
@@ -31,24 +33,7 @@ jobs:
     - name: Linux
       run: |
         sudo apt install tinyproxy
-        LIBSSH2_VERSION=1.11.1 LIBGIT2_VERSION=1.9.0 /bin/sh build.sh test
-
-  linux-arm64:
-    runs-on: ubuntu-24.04
-    steps:
-    - name: Checkout
-      uses: actions/checkout@v4
-
-    - name: Build & test
-      uses: uraimo/run-on-arch-action@v2
-      with:
-        arch: aarch64
-        distro: ubuntu22.04
-        install: |
-          apt-get update -q -y
-          apt-get install -q -y cmake libssl-dev python3-dev python3-venv wget
-        run: |
-          LIBSSH2_VERSION=1.11.1 LIBGIT2_VERSION=1.9.0 /bin/sh build.sh test
+        LIBSSH2_VERSION=1.11.1 LIBGIT2_VERSION=1.9.1 /bin/sh build.sh test
 
   linux-s390x:
     runs-on: ubuntu-24.04
@@ -58,7 +43,7 @@ jobs:
       uses: actions/checkout@v4
 
     - name: Build & test
-      uses: uraimo/run-on-arch-action@v2
+      uses: uraimo/run-on-arch-action@v3
       with:
         arch: s390x
         distro: ubuntu22.04
@@ -66,7 +51,7 @@ jobs:
           apt-get update -q -y
           apt-get install -q -y cmake libssl-dev python3-dev python3-venv wget
         run: |
-          LIBSSH2_VERSION=1.11.1 LIBGIT2_VERSION=1.9.0 /bin/sh build.sh test
+          LIBSSH2_VERSION=1.11.1 LIBGIT2_VERSION=1.9.1 /bin/sh build.sh test
       continue-on-error: true # Tests are expected to fail, see issue #812
 
   macos-arm64:
@@ -83,4 +68,4 @@ jobs:
     - name: macOS
       run: |
         export OPENSSL_PREFIX=`brew --prefix openssl@3`
-        LIBSSH2_VERSION=1.11.1 LIBGIT2_VERSION=1.9.0 /bin/sh build.sh test
+        LIBSSH2_VERSION=1.11.1 LIBGIT2_VERSION=1.9.1 /bin/sh build.sh test
diff -pruN 1.17.0-2/.github/workflows/wheels.yml 1.18.1-1/.github/workflows/wheels.yml
--- 1.17.0-2/.github/workflows/wheels.yml	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/.github/workflows/wheels.yml	2025-07-26 10:03:19.000000000 +0000
@@ -4,18 +4,21 @@ on:
   push:
     branches:
     - master
+    - wheels-*
     tags:
     - 'v*'
 
 jobs:
   build_wheels:
-    name: Build wheels on ${{ matrix.os }}
+    name: Wheels for ${{ matrix.name }}
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
         include:
-        - name: linux
+        - name: linux-amd
           os: ubuntu-24.04
+        - name: linux-arm
+          os: ubuntu-24.04-arm
         - name: macos
           os: macos-13
 
@@ -24,27 +27,51 @@ jobs:
 
       - uses: actions/setup-python@v5
         with:
-          python-version: '3.11'
+          python-version: '3.13'
+
+      - name: Install cibuildwheel
+        run: python -m pip install cibuildwheel==3.0.0
+
+      - name: Build wheels
+        run: python -m cibuildwheel --output-dir wheelhouse
+
+      - uses: actions/upload-artifact@v4
+        with:
+          name: wheels-${{ matrix.name }}
+          path: ./wheelhouse/*.whl
+
+  build_wheels_ppc:
+    name: Wheels for linux-ppc
+    runs-on: ubuntu-24.04
+
+    steps:
+      - uses: actions/checkout@v4
+
+      - uses: actions/setup-python@v5
+        with:
+          python-version: '3.13'
 
       - uses: docker/setup-qemu-action@v3
-        if: runner.os == 'Linux'
         with:
-          platforms: all
+          platforms: linux/ppc64le
 
       - name: Install cibuildwheel
-        run: python -m pip install cibuildwheel==2.22.0
+        run: python -m pip install cibuildwheel==3.0.0
 
       - name: Build wheels
         run: python -m cibuildwheel --output-dir wheelhouse
+        env:
+          CIBW_ARCHS: ppc64le
+          CIBW_ENVIRONMENT: LIBSSH2_VERSION=1.11.1 LIBGIT2_VERSION=1.9.1 LIBGIT2=/project/ci
 
       - uses: actions/upload-artifact@v4
         with:
-          name: wheels-${{ matrix.name }}
+          name: wheels-linux-ppc
           path: ./wheelhouse/*.whl
 
   pypi:
     if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
-    needs: [build_wheels]
+    needs: [build_wheels, build_wheels_ppc]
     runs-on: ubuntu-24.04
 
     steps:
diff -pruN 1.17.0-2/.mailmap 1.18.1-1/.mailmap
--- 1.17.0-2/.mailmap	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/.mailmap	2025-07-26 10:03:19.000000000 +0000
@@ -3,6 +3,7 @@ Alexander Linne <alexander.linne@outlook
 Anatoly Techtonik <techtonik@gmail.com>
 Bob Carroll <bob.carroll@alum.rit.edu> <bobc@qti.qualcomm.com>
 Brandon Milton <bmilton@fb.com> <brandon.milton21@gmail.com>
+CJ Steiner <47841949+clintonsteiner@users.noreply.github.com>
 Carlos Martín Nieto <cmn@dwim.me> <carlos@cmartin.tk>
 Christian Boos <cboos@edgewall.org> <cboos@bct-technology.com>
 Grégory Herrero <gregory.herrero@oracle.com>
@@ -12,6 +13,7 @@ J. David Ibáñez <jdavid.ibp@gmail.com>
 Jeremy Westwood <jeremy.westwood@hexagon.com>
 Jose Plana <jplana@tuenti.com> <jplana@gmail.com>
 Kaarel Kitsemets <kitsemets@gmail.com>
+Karl Malmros <itkalle@outlook.com> <44969574+ktpa@users.noreply.github.com>
 Lukas Fleischer <lfleischer@lfos.de> <info@cryptocrack.de>
 Martin Lenders <mlenders@elegosoft.com> <authmill@datalove.me>
 Matthew Duggan <mduggan@qti.qualcomm.com> <mgithub@guarana.org>
@@ -29,9 +31,10 @@ Sriram Raghu <imbuedhope@gmail.com> <imb
 Sukhman Bhuller <sbhuller@atlassian.com>
 Tamir Bahar <tamir@north-bit.com> <tmr232@github>
 Tamir Bahar <tamir@north-bit.com> <tmr232@users.noreply.github.com>
-Victor Garcia <bravejolie@gmail.com> <victor@tuenti.com>
 Victor Florea <victor@engineeredarts.co.uk>
+Victor Garcia <bravejolie@gmail.com> <victor@tuenti.com>
 Vlad Temian <vladtemian@gmail.com>
+William Schueller <william.schueller@gmail.com> <wschuell@users.noreply.github.com>
 Wim Jeantine-Glenn <hey@wimglenn.com>
 Xavier Delannoy <xavier.delannoy@gmail.com>
 Xu Tao <xutao881001@gmail.com>
diff -pruN 1.17.0-2/AUTHORS.md 1.18.1-1/AUTHORS.md
--- 1.17.0-2/AUTHORS.md	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/AUTHORS.md	2025-07-26 10:03:19.000000000 +0000
@@ -16,6 +16,7 @@ Authors:
     Richo Healey
     Christian Boos
     Julien Miotte
+    Benedikt Seidl
     Nick Hynes
     Richard Möhn
     Xu Tao
@@ -34,6 +35,7 @@ Authors:
     Xavier Delannoy
     Michael Jones
     Saugat Pachhai
+    Andrej730
     Bernardo Heynemann
     John Szakmeister
     Nabijacz Leweli
@@ -43,14 +45,16 @@ Authors:
     Chad Dombrova
     Lukas Fleischer
     Mathias Leppich
+    Mathieu Parent
+    Michał Kępień
     Nicolas Dandrimont
     Raphael Medaer (Escaux)
+    Yaroslav Halchenko
     Anatoly Techtonik
     Andrew Olsen
     Dan Sully
     David Versmisse
     Grégory Herrero
-    Michał Kępień
     Mikhail Yushkovskiy
     Robin Stocker
     Rohit Sanjay
@@ -64,6 +68,7 @@ Authors:
     Assaf Nativ
     Bob Carroll
     Christian Häggström
+    Edmundo Carmona Antoranz
     Erik Johnson
     Filip Rindler
     Fraser Tweedale
@@ -89,6 +94,7 @@ Authors:
     Andrey Devyatkin
     Arno van Lumig
     Ben Davis
+    CJ Steiner
     Colin Watson
     Dan Yeaw
     Dustin Raimondi
@@ -114,7 +120,6 @@ Authors:
     Kyle Gottfried
     Marcel Waldvogel
     Masud Rahman
-    Mathieu Parent
     Michael Sondergaard
     Natanael Arndt
     Ondřej Nový
@@ -127,9 +132,11 @@ Authors:
     nikitalita
     Adam Gausmann
     Adam Spiers
+    Adrien Nader
     Albin Söderström
     Alexandru Fikl
     Andrew Chin
+    Andrew McNulty
     Andrey Trubachev
     András Veres-Szentkirályi
     Ash Berlin
@@ -180,6 +187,7 @@ Authors:
     Josh Bleecher Snyder
     Julia Evans
     Justin Clift
+    Karl Malmros
     Kevin Valk
     Konstantinos Smanis
     Kyriakos Oikonomakos
@@ -215,6 +223,7 @@ Authors:
     Rui Chen
     Sandro Jäckel
     Saul Pwanson
+    Sebastian Hamann
     Shane Turner
     Sheeo
     Simone Mosciatti
@@ -224,6 +233,7 @@ Authors:
     Timo Röhling
     Victor Florea
     Vladimir Rutsky
+    William Schueller
     Wim Jeantine-Glenn
     Yu Jianjian
     buhl
diff -pruN 1.17.0-2/CHANGELOG.md 1.18.1-1/CHANGELOG.md
--- 1.17.0-2/CHANGELOG.md	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/CHANGELOG.md	2025-07-26 10:03:19.000000000 +0000
@@ -1,3 +1,97 @@
+# 1.18.1 (2025-07-26)
+
+- Update wheels to libgit2 1.9.1 and OpenSSL 3.3
+
+- New `Index.remove_directory(...)`
+  [#1377](https://github.com/libgit2/pygit2/pull/1377)
+
+- New `Index.add_conflict(...)`
+  [#1382](https://github.com/libgit2/pygit2/pull/1382)
+
+- Now `Repository.merge_file_from_index(...)` returns a `MergeFileResult` object when
+  called with `use_deprecated=False`
+  [#1376](https://github.com/libgit2/pygit2/pull/1376)
+
+- Typing improvements
+  [#1369](https://github.com/libgit2/pygit2/pull/1369)
+  [#1370](https://github.com/libgit2/pygit2/pull/1370)
+  [#1371](https://github.com/libgit2/pygit2/pull/1371)
+  [#1373](https://github.com/libgit2/pygit2/pull/1373)
+  [#1384](https://github.com/libgit2/pygit2/pull/1384)
+  [#1386](https://github.com/libgit2/pygit2/pull/1386)
+
+Deprecations:
+
+- Update your code:
+
+      # Before
+      contents = Repository.merge_file_from_index(...)
+
+      # Now
+      result = Repository.merge_file_from_index(..., use_deprecated=False)
+      contents = result.contents
+
+  At some point in the future `use_deprecated=False` will be the default.
+
+
+# 1.18.0 (2025-04-24)
+
+- Upgrade Linux Glibc wheels to `manylinux_2_28`
+
+- Add `RemoteCallbacks.push_transfer_progress(...)` callback
+  [#1345](https://github.com/libgit2/pygit2/pull/1345)
+
+- New `bool(oid)`
+  [#1347](https://github.com/libgit2/pygit2/pull/1347)
+
+- Now `Repository.merge(...)` accepts a commit or reference object
+  [#1348](https://github.com/libgit2/pygit2/pull/1348)
+
+- New `threads` optional argument in `Remote.push(...)`
+  [#1352](https://github.com/libgit2/pygit2/pull/1352)
+
+- New `proxy` optional argument in `clone_repository(...)`
+  [#1354](https://github.com/libgit2/pygit2/pull/1354)
+
+- New optional arguments `context_lines` and `interhunk_lines` in `Blob.diff(...)` ; and
+  now `Repository.diff(...)` honors these two arguments when the objects diffed are blobs.
+  [#1360](https://github.com/libgit2/pygit2/pull/1360)
+
+- Now `Tree.diff_to_workdir(...)` accepts keyword arguments, not just positional.
+
+- Fix when a reference name has non UTF-8 chars
+  [#1329](https://github.com/libgit2/pygit2/pull/1329)
+
+- Fix condition check in `Repository.remotes.rename(...)`
+  [#1342](https://github.com/libgit2/pygit2/pull/1342)
+
+- Add codespell workflow, fix a number of typos
+  [#1344](https://github.com/libgit2/pygit2/pull/1344)
+
+- Documentation and typing
+  [#1343](https://github.com/libgit2/pygit2/pull/1343)
+  [#1347](https://github.com/libgit2/pygit2/pull/1347)
+  [#1356](https://github.com/libgit2/pygit2/pull/1356)
+
+- CI: Use ARM runner for tests and wheels
+  [#1346](https://github.com/libgit2/pygit2/pull/1346)
+
+- Build and CI updates
+  [#1363](https://github.com/libgit2/pygit2/pull/1363)
+  [#1365](https://github.com/libgit2/pygit2/pull/1365)
+
+Deprecations:
+
+- Passing str to `Repository.merge(...)` is deprecated,
+  instead pass an oid object (or a commit, or a reference)
+  [#1349](https://github.com/libgit2/pygit2/pull/1349)
+
+Breaking changes:
+
+- Keyword argument `flag` has been renamed to `flags` in `Blob.diff(...)` and
+  `Blob.diff_to_buffer(...)`
+
+
 # 1.17.0 (2025-01-08)
 
 - Upgrade to libgit2 1.9
@@ -259,7 +353,7 @@ Deprecations:
 -   New `keep_all` and `paths` optional arguments for
     `Repository.stash(...)`
     [#1202](https://github.com/libgit2/pygit2/pull/1202)
--   New `Respository.state()`
+-   New `Repository.state()`
     [#1204](https://github.com/libgit2/pygit2/pull/1204)
 -   Improve `Repository.write_archive(...)` performance
     [#1183](https://github.com/libgit2/pygit2/pull/1183)
@@ -439,7 +533,7 @@ Breaking changes:
 
 Breaking changes:
 
--   Remove deprecated `GIT_CREDTYPE_XXX` contants, use
+-   Remove deprecated `GIT_CREDTYPE_XXX` constants, use
     `GIT_CREDENTIAL_XXX` instead.
 -   Remove deprecated `Patch.patch` getter, use `Patch.text` instead.
 
@@ -536,7 +630,7 @@ Deprecations:
 
 -   Deprecate `Repository.create_remote(...)`, use instead
     `Repository.remotes.create(...)`
--   Deprecate `GIT_CREDTYPE_XXX` contants, use `GIT_CREDENTIAL_XXX`
+-   Deprecate `GIT_CREDTYPE_XXX` constants, use `GIT_CREDENTIAL_XXX`
     instead.
 
 # 1.2.0 (2020-04-05)
@@ -657,7 +751,7 @@ Breaking changes:
 
 Breaking changes:
 
--   Now the Repository has a new attribue `odb` for object database:
+-   Now the Repository has a new attribute `odb` for object database:
 
         # Before
         repository.read(...)
@@ -862,7 +956,7 @@ Other changes:
     [#610](https://github.com/libgit2/pygit2/issues/610)
 -   Fix tests failing in some cases
     [#795](https://github.com/libgit2/pygit2/issues/795)
--   Automatize wheels upload to pypi
+-   Automate wheels upload to pypi
     [#563](https://github.com/libgit2/pygit2/issues/563)
 
 # 0.27.0 (2018-03-30)
diff -pruN 1.17.0-2/Makefile 1.18.1-1/Makefile
--- 1.17.0-2/Makefile	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/Makefile	2025-07-26 10:03:19.000000000 +0000
@@ -1,7 +1,7 @@
 .PHONY: build html
 
 build:
-	OPENSSL_VERSION=3.2.3 LIBSSH2_VERSION=1.11.1 LIBGIT2_VERSION=1.9.0 sh build.sh
+	OPENSSL_VERSION=3.3.3 LIBSSH2_VERSION=1.11.1 LIBGIT2_VERSION=1.9.1 sh build.sh
 
 html: build
 	make -C docs html
diff -pruN 1.17.0-2/appveyor.yml 1.18.1-1/appveyor.yml
--- 1.17.0-2/appveyor.yml	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/appveyor.yml	2025-07-26 10:03:19.000000000 +0000
@@ -1,4 +1,4 @@
-version: 1.17.{build}
+version: 1.18.{build}
 image: Visual Studio 2019
 configuration: Release
 environment:
@@ -35,7 +35,7 @@ build_script:
 # Clone, build and install libgit2
 - cmd: |
     set LIBGIT2=%APPVEYOR_BUILD_FOLDER%\venv
-    git clone --depth=1 -b v1.9.0 https://github.com/libgit2/libgit2.git libgit2
+    git clone --depth=1 -b v1.9.1 https://github.com/libgit2/libgit2.git libgit2
     cd libgit2
     cmake . -DBUILD_TESTS=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="%LIBGIT2%" -G "%GENERATOR%"
     cmake --build . --target install
diff -pruN 1.17.0-2/build.sh 1.18.1-1/build.sh
--- 1.17.0-2/build.sh	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/build.sh	2025-07-26 10:03:19.000000000 +0000
@@ -69,6 +69,7 @@ if [ "$CIBUILDWHEEL" = "1" ]; then
             yum install openssl-devel -y
         else
             yum install perl-IPC-Cmd -y
+            yum install perl-Pod-Html -y
         fi
     elif [ -f /sbin/apk ]; then
         apk add wget
@@ -261,6 +262,16 @@ if [ "$1" = "test" ]; then
     $PREFIX/bin/pytest --cov=pygit2
 fi
 
+# Type checking
+if [ "$1" = "mypy" ]; then
+    shift
+    if [ -n "$WHEELDIR" ]; then
+        $PREFIX/bin/pip install $WHEELDIR/pygit2*-$PYTHON_TAG-*.whl
+    fi
+    $PREFIX/bin/pip install -r requirements-test.txt
+    $PREFIX/bin/mypy pygit2
+fi
+
 # Test .pyi stub file
 if [ "$1" = "stubtest" ]; then
     shift
diff -pruN 1.17.0-2/build_tag.py 1.18.1-1/build_tag.py
--- 1.17.0-2/build_tag.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/build_tag.py	2025-07-26 10:03:19.000000000 +0000
@@ -1,4 +1,5 @@
-import platform, sys
+import platform
+import sys
 
 py = {'CPython': 'cp', 'PyPy': 'pp'}[platform.python_implementation()]
 print(f'{py}{sys.version_info.major}{sys.version_info.minor}')
diff -pruN 1.17.0-2/debian/changelog 1.18.1-1/debian/changelog
--- 1.17.0-2/debian/changelog	2025-04-03 15:39:34.000000000 +0000
+++ 1.18.1-1/debian/changelog	2025-08-13 12:32:28.000000000 +0000
@@ -1,10 +1,17 @@
+python-pygit2 (1.18.1-1) unstable; urgency=medium
+
+  * New upstream version 1.18.1
+  * Refresh patches (no functional changes)
+
+ -- Timo Röhling <roehling@debian.org>  Wed, 13 Aug 2025 14:32:28 +0200
+
 python-pygit2 (1.17.0-2) unstable; urgency=medium
 
   * Upload to unstable.
   * Bump Standards-Version to 4.7.2
   * Drop old FSF snail mail address from d/copyright
 
- -- Timo Röhling <roehling@debian.org>  Thu, 03 Apr 2025 17:39:34 +0200
+ -- Timo Röhling <roehling@debian.org>  Wed, 13 Aug 2025 14:32:23 +0200
 
 python-pygit2 (1.17.0-1) experimental; urgency=medium
 
diff -pruN 1.17.0-2/debian/patches/0001-Remove-privacy-breach.patch 1.18.1-1/debian/patches/0001-Remove-privacy-breach.patch
--- 1.17.0-2/debian/patches/0001-Remove-privacy-breach.patch	2025-04-03 15:39:34.000000000 +0000
+++ 1.18.1-1/debian/patches/0001-Remove-privacy-breach.patch	2025-08-13 12:32:28.000000000 +0000
@@ -7,7 +7,7 @@ Subject: Remove privacy breach
  1 file changed, 9 deletions(-)
 
 diff --git a/docs/development.rst b/docs/development.rst
-index 771a708..19c3743 100644
+index b9422bf..8585a2b 100644
 --- a/docs/development.rst
 +++ b/docs/development.rst
 @@ -2,15 +2,6 @@
diff -pruN 1.17.0-2/debian/patches/0002-Don-t-access-internet-during-build.patch 1.18.1-1/debian/patches/0002-Don-t-access-internet-during-build.patch
--- 1.17.0-2/debian/patches/0002-Don-t-access-internet-during-build.patch	2025-04-03 15:39:34.000000000 +0000
+++ 1.18.1-1/debian/patches/0002-Don-t-access-internet-during-build.patch	2025-08-13 12:32:28.000000000 +0000
@@ -7,10 +7,10 @@ Subject: Don't access internet during bu
  1 file changed, 1 insertion(+), 5 deletions(-)
 
 diff --git a/test/utils.py b/test/utils.py
-index 3f1fefc..a91d12c 100644
+index 7c840f1..6a4c9a1 100644
 --- a/test/utils.py
 +++ b/test/utils.py
-@@ -44,11 +44,7 @@ requires_future_libgit2 = pytest.mark.skipif(
+@@ -44,11 +44,7 @@ requires_future_libgit2 = pytest.mark.xfail(
      reason='This test may work with a future version of libgit2',
  )
  
diff -pruN 1.17.0-2/debian/patches/0004-use-python3-sphinx-rtd-theme.patch 1.18.1-1/debian/patches/0004-use-python3-sphinx-rtd-theme.patch
--- 1.17.0-2/debian/patches/0004-use-python3-sphinx-rtd-theme.patch	2025-04-03 15:39:34.000000000 +0000
+++ 1.18.1-1/debian/patches/0004-use-python3-sphinx-rtd-theme.patch	2025-08-13 12:32:28.000000000 +0000
@@ -8,10 +8,10 @@ Forwarded: not-needed
  1 file changed, 1 deletion(-)
 
 diff --git a/docs/conf.py b/docs/conf.py
-index b3c6c1c..771ced4 100644
+index 6978c4e..b9ebf12 100644
 --- a/docs/conf.py
 +++ b/docs/conf.py
-@@ -49,7 +49,6 @@ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+@@ -50,7 +50,6 @@ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
  # a list of builtin themes.
  #
  html_theme = 'sphinx_rtd_theme'
diff -pruN 1.17.0-2/debian/patches/0005-Do-not-insert-source-dir-in-sphinx.patch 1.18.1-1/debian/patches/0005-Do-not-insert-source-dir-in-sphinx.patch
--- 1.17.0-2/debian/patches/0005-Do-not-insert-source-dir-in-sphinx.patch	2025-04-03 15:39:34.000000000 +0000
+++ 1.18.1-1/debian/patches/0005-Do-not-insert-source-dir-in-sphinx.patch	2025-08-13 12:32:28.000000000 +0000
@@ -9,10 +9,10 @@ must be built in the binary package dire
  1 file changed, 1 insertion(+), 1 deletion(-)
 
 diff --git a/docs/conf.py b/docs/conf.py
-index 771ced4..1d5dce5 100644
+index b9ebf12..f38fbe8 100644
 --- a/docs/conf.py
 +++ b/docs/conf.py
-@@ -12,7 +12,7 @@ import os, sys
+@@ -13,7 +13,7 @@ import sys
  # add these directories to sys.path here. If the directory is relative to the
  # documentation root, use os.path.abspath to make it absolute, like shown here.
  #
diff -pruN 1.17.0-2/debian/patches/0006-Fix-GenericIterator-class-interface.patch 1.18.1-1/debian/patches/0006-Fix-GenericIterator-class-interface.patch
--- 1.17.0-2/debian/patches/0006-Fix-GenericIterator-class-interface.patch	2025-04-03 15:39:34.000000000 +0000
+++ 1.18.1-1/debian/patches/0006-Fix-GenericIterator-class-interface.patch	2025-08-13 12:32:28.000000000 +0000
@@ -7,7 +7,7 @@ Subject: Fix GenericIterator class inter
  1 file changed, 3 insertions(+)
 
 diff --git a/pygit2/utils.py b/pygit2/utils.py
-index 1f112b2..d3bc51f 100644
+index 1139b23..2c0af78 100644
 --- a/pygit2/utils.py
 +++ b/pygit2/utils.py
 @@ -168,6 +168,9 @@ class GenericIterator:
diff -pruN 1.17.0-2/docs/conf.py 1.18.1-1/docs/conf.py
--- 1.17.0-2/docs/conf.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/docs/conf.py	2025-07-26 10:03:19.000000000 +0000
@@ -4,7 +4,8 @@
 # list see the documentation:
 # http://www.sphinx-doc.org/en/master/config
 
-import os, sys
+import os
+import sys
 
 # -- Path setup --------------------------------------------------------------
 
@@ -22,7 +23,7 @@ copyright = '2010-2025 The pygit2 contri
 # author = ''
 
 # The full version, including alpha/beta/rc tags
-release = '1.17.0'
+release = '1.18.1'
 
 
 # -- General configuration ---------------------------------------------------
diff -pruN 1.17.0-2/docs/development.rst 1.18.1-1/docs/development.rst
--- 1.17.0-2/docs/development.rst	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/docs/development.rst	2025-07-26 10:03:19.000000000 +0000
@@ -82,7 +82,7 @@ Step 3. Build pygit2 with debug symbols:
 Step 4. Install requirements::
 
   $ $PYTHONBIN/python3 setup.py install
-  $ pip insall pytest
+  $ pip install pytest
 
 Step 4. Run valgrind::
 
diff -pruN 1.17.0-2/docs/index_file.rst 1.18.1-1/docs/index_file.rst
--- 1.17.0-2/docs/index_file.rst	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/docs/index_file.rst	2025-07-26 10:03:19.000000000 +0000
@@ -18,9 +18,10 @@ Iterate over all entries of the index::
 
 Index write::
 
-    >>> index.add('path/to/file')          # git add
-    >>> index.remove('path/to/file')       # git rm
-    >>> index.write()                      # don't forget to save the changes
+    >>> index.add('path/to/file')                    # git add
+    >>> index.remove('path/to/file')                 # git rm
+    >>> index.remove_directory('path/to/directory/') # git rm -r
+    >>> index.write()                                # don't forget to save the changes
 
 Custom entries::
    >>> entry = pygit2.IndexEntry('README.md', blob_id, blob_filemode)
diff -pruN 1.17.0-2/docs/install.rst 1.18.1-1/docs/install.rst
--- 1.17.0-2/docs/install.rst	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/docs/install.rst	2025-07-26 10:03:19.000000000 +0000
@@ -60,7 +60,7 @@ Python requirements (these are specified
 Libgit2 **v1.9.x**; binary wheels already include libgit2, so you only need to
 worry about this if you install the source package.
 
-Optional libgit2 dependecies to support ssh and https:
+Optional libgit2 dependencies to support ssh and https:
 
 - https: WinHTTP (Windows), SecureTransport (OS X) or OpenSSL.
 - ssh: libssh2 1.9.0 or later, pkg-config
@@ -83,39 +83,39 @@ The version number of pygit2 is composed
 The table below summarizes the latest pygit2 versions with the supported versions
 of Python and the required libgit2 version.
 
-+-----------+----------------+------------+
-| pygit2    | Python         | libgit2    |
-+-----------+----------------+------------+
-| 1.17      | 3.10 - 3.13    | 1.9        |
-+-----------+----------------+------------+
-| 1.16      | 3.10 - 3.13    | 1.8        |
-+-----------+----------------+------------+
-| 1.15      | 3.9 - 3.12     | 1.8        |
-+-----------+----------------+------------+
-| 1.14      | 3.9 - 3.12     | 1.7        |
-+-----------+----------------+------------+
-| 1.13      | 3.8 - 3.12     | 1.7        |
-+-----------+----------------+------------+
-| 1.12      | 3.8 - 3.11     | 1.6        |
-+-----------+----------------+------------+
-| 1.11      | 3.8 - 3.11     | 1.5        |
-+-----------+----------------+------------+
-| 1.10      | 3.7 - 3.10     | 1.5        |
-+-----------+----------------+------------+
-| 1.9       | 3.7 - 3.10     | 1.4        |
-+-----------+----------------+------------+
-| 1.7 - 1.8 | 3.7 - 3.10     | 1.3        |
-+-----------+----------------+------------+
-| 1.4 - 1.6 | 3.6 - 3.9      | 1.1        |
-+-----------+----------------+------------+
-| 1.2 - 1.3 | 3.6 - 3.8      | 1.0        |
-+-----------+----------------+------------+
-| 1.1       | 3.5 - 3.8      | 0.99 - 1.0 |
-+-----------+----------------+------------+
-| 1.0       | 3.5 - 3.8      | 0.28       |
-+-----------+----------------+------------+
-| 0.28.2    | 2.7, 3.4 - 3.7 | 0.28       |
-+-----------+----------------+------------+
++-------------+----------------+------------+
+| pygit2      | Python         | libgit2    |
++-------------+----------------+------------+
+| 1.17 - 1.18 | 3.10 - 3.13    | 1.9        |
++-------------+----------------+------------+
+| 1.16        | 3.10 - 3.13    | 1.8        |
++-------------+----------------+------------+
+| 1.15        | 3.9 - 3.12     | 1.8        |
++-------------+----------------+------------+
+| 1.14        | 3.9 - 3.12     | 1.7        |
++-------------+----------------+------------+
+| 1.13        | 3.8 - 3.12     | 1.7        |
++-------------+----------------+------------+
+| 1.12        | 3.8 - 3.11     | 1.6        |
++-------------+----------------+------------+
+| 1.11        | 3.8 - 3.11     | 1.5        |
++-------------+----------------+------------+
+| 1.10        | 3.7 - 3.10     | 1.5        |
++-------------+----------------+------------+
+| 1.9         | 3.7 - 3.10     | 1.4        |
++-------------+----------------+------------+
+| 1.7 - 1.8   | 3.7 - 3.10     | 1.3        |
++-------------+----------------+------------+
+| 1.4 - 1.6   | 3.6 - 3.9      | 1.1        |
++-------------+----------------+------------+
+| 1.2 - 1.3   | 3.6 - 3.8      | 1.0        |
++-------------+----------------+------------+
+| 1.1         | 3.5 - 3.8      | 0.99 - 1.0 |
++-------------+----------------+------------+
+| 1.0         | 3.5 - 3.8      | 0.28       |
++-------------+----------------+------------+
+| 0.28.2      | 2.7, 3.4 - 3.7 | 0.28       |
++-------------+----------------+------------+
 
 .. warning::
 
@@ -214,7 +214,7 @@ libgit2 within a virtual environment
 
 This is how to install both libgit2 and pygit2 within a virtual environment.
 
-This is useful if you don't have root acces to install libgit2 system wide.
+This is useful if you don't have root access to install libgit2 system wide.
 Or if you wish to have different versions of libgit2/pygit2 installed in
 different virtual environments, isolated from each other.
 
@@ -265,7 +265,7 @@ So you need to either set ``LD_LIBRARY_P
 Or, like we have done in the instructions above, use the `rpath
 <http://en.wikipedia.org/wiki/Rpath>`_, it hard-codes extra search paths within
 the pygit2 extension modules, so you don't need to set ``LD_LIBRARY_PATH``
-everytime. Verify yourself if curious:
+every time. Verify yourself if curious:
 
 .. code-block:: sh
 
@@ -317,7 +317,7 @@ source package.
 
 The easiest way is to first install libgit2 with the `Homebrew <http://brew.sh>`_
 package manager and then use pip3 for pygit2. The following example assumes that
-XCode and Hombrew are already installed.
+XCode and Homebrew are already installed.
 
 .. code-block:: sh
 
diff -pruN 1.17.0-2/docs/merge.rst 1.18.1-1/docs/merge.rst
--- 1.17.0-2/docs/merge.rst	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/docs/merge.rst	2025-07-26 10:03:19.000000000 +0000
@@ -66,7 +66,7 @@ The following methods perform the calcul
 .. automethod:: pygit2.Repository.merge_base_many
 .. automethod:: pygit2.Repository.merge_base_octopus
 
-With this base at hand one can do repeated invokations of
+With this base at hand one can do repeated invocations of
 :py:meth:`.Repository.merge_commits` and :py:meth:`.Repository.merge_trees`
 to perform the actual merge into one tree (and deal with conflicts along the
 way).
\ No newline at end of file
diff -pruN 1.17.0-2/docs/objects.rst 1.18.1-1/docs/objects.rst
--- 1.17.0-2/docs/objects.rst	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/docs/objects.rst	2025-07-26 10:03:19.000000000 +0000
@@ -16,7 +16,7 @@ Object lookup
 
 In the previous chapter we learnt about Object IDs. With an Oid we can ask the
 repository to get the associated object. To do that the ``Repository`` class
-implementes a subset of the mapping interface.
+implements a subset of the mapping interface.
 
 .. autoclass:: pygit2.Repository
    :noindex:
@@ -33,7 +33,7 @@ implementes a subset of the mapping inte
         >>> repo = Repository('path/to/pygit2')
         >>> obj = repo.get("101715bf37440d32291bde4f58c3142bcf7d8adb")
         >>> obj
-        <_pygit2.Commit object at 0x7ff27a6b60f0>
+        <pygit2.Object{commit:101715bf37440d32291bde4f58c3142bcf7d8adb}>
 
    .. method:: Repository.__getitem__(id)
 
@@ -90,7 +90,7 @@ Blobs
 =================
 
 A blob is just a raw byte string. They are the Git equivalent to files in
-a filesytem.
+a filesystem.
 
 This is their API:
 
@@ -221,7 +221,7 @@ Creating trees
 Commits
 =================
 
-A commit is a snapshot of the working dir with meta informations like author,
+A commit is a snapshot of the working dir with meta information like author,
 committer and others.
 
 .. autoclass:: pygit2.Commit
diff -pruN 1.17.0-2/docs/oid.rst 1.18.1-1/docs/oid.rst
--- 1.17.0-2/docs/oid.rst	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/docs/oid.rst	2025-07-26 10:03:19.000000000 +0000
@@ -56,9 +56,9 @@ The Oid type
      >>> raw = unhexlify("cff3ceaefc955f0dbe1957017db181bc49913781")
      >>> oid2 = Oid(raw=raw)
 
-And the other way around, from an Oid object we can get the hexadecimal and raw
-forms. You can use the built-in `str()` (or `unicode()` in python 2) to get the
-hexadecimal representation of the Oid.
+And the other way around, from an Oid object we can get the raw form via
+`oid.raw`. You can use `str(oid)` to get the hexadecimal representation of the
+Oid.
 
 .. method:: Oid.__str__()
 .. autoattribute:: pygit2.Oid.raw
@@ -68,8 +68,11 @@ The Oid type supports:
 - rich comparisons, not just for equality, also: lesser-than, lesser-or-equal,
   etc.
 
-- hashing, so Oid objects can be used as keys in a dictionary.
+- `hash(oid)`, so Oid objects can be used as keys in a dictionary.
 
+- `bool(oid)`, returning False if the Oid is a null SHA-1 (all zeros).
+
+- `str(oid)`, returning the hexadecimal representation of the Oid.
 
 Constants
 =========
diff -pruN 1.17.0-2/docs/repository.rst 1.18.1-1/docs/repository.rst
--- 1.17.0-2/docs/repository.rst	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/docs/repository.rst	2025-07-26 10:03:19.000000000 +0000
@@ -29,6 +29,7 @@ Functions
      >>> repo_path = '/path/to/create/repository'
      >>> repo = clone_repository(repo_url, repo_path) # Clones a non-bare repository
      >>> repo = clone_repository(repo_url, repo_path, bare=True) # Clones a bare repository
+     >>> repo = clone_repository(repo_url, repo_path, proxy=True) # Enable automatic proxy detection
 
 
 .. autofunction:: pygit2.discover_repository
diff -pruN 1.17.0-2/pygit2/__init__.py 1.18.1-1/pygit2/__init__.py
--- 1.17.0-2/pygit2/__init__.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/pygit2/__init__.py	2025-07-26 10:03:19.000000000 +0000
@@ -23,9 +23,11 @@
 # the Free Software Foundation, 51 Franklin Street, Fifth Floor,
 # Boston, MA 02110-1301, USA.
 
+# ruff: noqa: F401 F403 F405
+
 # Standard Library
 import functools
-from os import PathLike
+import os
 import typing
 
 # Low level API
@@ -38,7 +40,12 @@ from ._build import __version__
 from .blame import Blame, BlameHunk
 from .blob import BlobIO
 from .callbacks import Payload, RemoteCallbacks, CheckoutCallbacks, StashApplyCallbacks
-from .callbacks import git_clone_options, git_fetch_options, get_credentials
+from .callbacks import (
+    git_clone_options,
+    git_fetch_options,
+    git_proxy_options,
+    get_credentials,
+)
 from .config import Config
 from .credentials import *
 from .errors import check_error, Passthrough
@@ -66,7 +73,7 @@ _cache_enums()
 
 
 def init_repository(
-    path: typing.Union[str, bytes, PathLike, None],
+    path: typing.Union[str, bytes, os.PathLike, None],
     bare: bool = False,
     flags: enums.RepositoryInitFlag = enums.RepositoryInitFlag.MKPATH,
     mode: typing.Union[
@@ -86,7 +93,7 @@ def init_repository(
 
     The *flags* may be a combination of enums.RepositoryInitFlag constants:
 
-    - BARE (overriden by the *bare* parameter)
+    - BARE (overridden by the *bare* parameter)
     - NO_REINIT
     - NO_DOTGIT_DIR
     - MKDIR
@@ -99,6 +106,10 @@ def init_repository(
     The *workdir_path*, *description*, *template_path*, *initial_head* and
     *origin_url* are all strings.
 
+    If a repository already exists at *path*, it may be opened successfully but
+    you must not rely on that behavior and should use the Repository
+    constructor directly instead.
+
     See libgit2's documentation on git_repository_init_ext for further details.
     """
     # Pre-process input parameters
@@ -144,14 +155,15 @@ def init_repository(
 
 
 def clone_repository(
-    url,
-    path,
-    bare=False,
-    repository=None,
-    remote=None,
-    checkout_branch=None,
-    callbacks=None,
-    depth=0,
+    url: str | bytes | os.PathLike,
+    path: str | bytes | os.PathLike,
+    bare: bool = False,
+    repository: typing.Callable | None = None,
+    remote: typing.Callable | None = None,
+    checkout_branch: str | bytes | None = None,
+    callbacks: RemoteCallbacks | None = None,
+    depth: int = 0,
+    proxy: None | bool | str = None,
 ):
     """
     Clones a new Git repository from *url* in the given *path*.
@@ -160,9 +172,9 @@ def clone_repository(
 
     Parameters:
 
-    url : str
+    url : str or bytes or pathlike object
         URL of the repository to clone.
-    path : str
+    path : str or bytes or pathlike object
         Local path to clone into.
     bare : bool
         Whether the local repository should be bare.
@@ -178,7 +190,7 @@ def clone_repository(
         The repository callback has `(path, bare) -> Repository` as a
         signature. The Repository it returns will be used instead of creating a
         new one.
-    checkout_branch : str
+    checkout_branch : str or bytes
         Branch to checkout after the clone. The default is to use the remote's
         default branch.
     callbacks : RemoteCallbacks
@@ -192,6 +204,12 @@ def clone_repository(
         If greater than 0, creates a shallow clone with a history truncated to
         the specified number of commits.
         The default is 0 (full commit history).
+    proxy : None or True or str
+        Proxy configuration. Can be one of:
+
+        * `None` (the default) to disable proxy usage
+        * `True` to enable automatic proxy detection
+        * an url to a proxy (`http://proxy.example.org:3128/`)
     """
 
     if callbacks is None:
@@ -212,9 +230,10 @@ def clone_repository(
             opts.checkout_branch = checkout_branch_ref
 
         with git_fetch_options(payload, opts=opts.fetch_opts):
-            crepo = ffi.new('git_repository **')
-            err = C.git_clone(crepo, to_bytes(url), to_bytes(path), opts)
-            payload.check_error(err)
+            with git_proxy_options(payload, opts.fetch_opts.proxy_opts, proxy):
+                crepo = ffi.new('git_repository **')
+                err = C.git_clone(crepo, to_bytes(url), to_bytes(path), opts)
+                payload.check_error(err)
 
     # Ok
     return Repository._from_c(crepo[0], owned=True)
@@ -223,3 +242,423 @@ def clone_repository(
 tree_entry_key = functools.cmp_to_key(tree_entry_cmp)
 
 settings = Settings()
+
+__all__ = (
+    # Standard Library
+    'functools',
+    'os',
+    'typing',
+    # Standard Library symbols
+    'TYPE_CHECKING',
+    'annotations',
+    # Low level API
+    'GIT_OID_HEX_ZERO',
+    'GIT_OID_HEXSZ',
+    'GIT_OID_MINPREFIXLEN',
+    'GIT_OID_RAWSZ',
+    'LIBGIT2_VER_MAJOR',
+    'LIBGIT2_VER_MINOR',
+    'LIBGIT2_VER_REVISION',
+    'LIBGIT2_VERSION',
+    'Object',
+    'Reference',
+    'AlreadyExistsError',
+    'Blob',
+    'Branch',
+    'Commit',
+    'Diff',
+    'DiffDelta',
+    'DiffFile',
+    'DiffHunk',
+    'DiffLine',
+    'DiffStats',
+    'GitError',
+    'InvalidSpecError',
+    'Mailmap',
+    'Note',
+    'Odb',
+    'OdbBackend',
+    'OdbBackendLoose',
+    'OdbBackendPack',
+    'Oid',
+    'Patch',
+    'RefLogEntry',
+    'Refdb',
+    'RefdbBackend',
+    'RefdbFsBackend',
+    'RevSpec',
+    'Signature',
+    'Stash',
+    'Tag',
+    'Tree',
+    'TreeBuilder',
+    'Walker',
+    'Worktree',
+    'discover_repository',
+    'hash',
+    'hashfile',
+    'init_file_backend',
+    'option',
+    'reference_is_valid_name',
+    'tree_entry_cmp',
+    # Low Level API (not present in .pyi)
+    'FilterSource',
+    'filter_register',
+    'filter_unregister',
+    'GIT_APPLY_LOCATION_BOTH',
+    'GIT_APPLY_LOCATION_INDEX',
+    'GIT_APPLY_LOCATION_WORKDIR',
+    'GIT_BLAME_FIRST_PARENT',
+    'GIT_BLAME_IGNORE_WHITESPACE',
+    'GIT_BLAME_NORMAL',
+    'GIT_BLAME_TRACK_COPIES_ANY_COMMIT_COPIES',
+    'GIT_BLAME_TRACK_COPIES_SAME_COMMIT_COPIES',
+    'GIT_BLAME_TRACK_COPIES_SAME_COMMIT_MOVES',
+    'GIT_BLAME_TRACK_COPIES_SAME_FILE',
+    'GIT_BLAME_USE_MAILMAP',
+    'GIT_BLOB_FILTER_ATTRIBUTES_FROM_COMMIT',
+    'GIT_BLOB_FILTER_ATTRIBUTES_FROM_HEAD',
+    'GIT_BLOB_FILTER_CHECK_FOR_BINARY',
+    'GIT_BLOB_FILTER_NO_SYSTEM_ATTRIBUTES',
+    'GIT_BRANCH_ALL',
+    'GIT_BRANCH_LOCAL',
+    'GIT_BRANCH_REMOTE',
+    'GIT_CHECKOUT_ALLOW_CONFLICTS',
+    'GIT_CHECKOUT_CONFLICT_STYLE_DIFF3',
+    'GIT_CHECKOUT_CONFLICT_STYLE_MERGE',
+    'GIT_CHECKOUT_CONFLICT_STYLE_ZDIFF3',
+    'GIT_CHECKOUT_DISABLE_PATHSPEC_MATCH',
+    'GIT_CHECKOUT_DONT_OVERWRITE_IGNORED',
+    'GIT_CHECKOUT_DONT_REMOVE_EXISTING',
+    'GIT_CHECKOUT_DONT_UPDATE_INDEX',
+    'GIT_CHECKOUT_DONT_WRITE_INDEX',
+    'GIT_CHECKOUT_DRY_RUN',
+    'GIT_CHECKOUT_FORCE',
+    'GIT_CHECKOUT_NO_REFRESH',
+    'GIT_CHECKOUT_NONE',
+    'GIT_CHECKOUT_RECREATE_MISSING',
+    'GIT_CHECKOUT_REMOVE_IGNORED',
+    'GIT_CHECKOUT_REMOVE_UNTRACKED',
+    'GIT_CHECKOUT_SAFE',
+    'GIT_CHECKOUT_SKIP_LOCKED_DIRECTORIES',
+    'GIT_CHECKOUT_SKIP_UNMERGED',
+    'GIT_CHECKOUT_UPDATE_ONLY',
+    'GIT_CHECKOUT_USE_OURS',
+    'GIT_CHECKOUT_USE_THEIRS',
+    'GIT_CONFIG_HIGHEST_LEVEL',
+    'GIT_CONFIG_LEVEL_APP',
+    'GIT_CONFIG_LEVEL_GLOBAL',
+    'GIT_CONFIG_LEVEL_LOCAL',
+    'GIT_CONFIG_LEVEL_PROGRAMDATA',
+    'GIT_CONFIG_LEVEL_SYSTEM',
+    'GIT_CONFIG_LEVEL_WORKTREE',
+    'GIT_CONFIG_LEVEL_XDG',
+    'GIT_DELTA_ADDED',
+    'GIT_DELTA_CONFLICTED',
+    'GIT_DELTA_COPIED',
+    'GIT_DELTA_DELETED',
+    'GIT_DELTA_IGNORED',
+    'GIT_DELTA_MODIFIED',
+    'GIT_DELTA_RENAMED',
+    'GIT_DELTA_TYPECHANGE',
+    'GIT_DELTA_UNMODIFIED',
+    'GIT_DELTA_UNREADABLE',
+    'GIT_DELTA_UNTRACKED',
+    'GIT_DESCRIBE_ALL',
+    'GIT_DESCRIBE_DEFAULT',
+    'GIT_DESCRIBE_TAGS',
+    'GIT_DIFF_BREAK_REWRITES_FOR_RENAMES_ONLY',
+    'GIT_DIFF_BREAK_REWRITES',
+    'GIT_DIFF_DISABLE_PATHSPEC_MATCH',
+    'GIT_DIFF_ENABLE_FAST_UNTRACKED_DIRS',
+    'GIT_DIFF_FIND_ALL',
+    'GIT_DIFF_FIND_AND_BREAK_REWRITES',
+    'GIT_DIFF_FIND_BY_CONFIG',
+    'GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED',
+    'GIT_DIFF_FIND_COPIES',
+    'GIT_DIFF_FIND_DONT_IGNORE_WHITESPACE',
+    'GIT_DIFF_FIND_EXACT_MATCH_ONLY',
+    'GIT_DIFF_FIND_FOR_UNTRACKED',
+    'GIT_DIFF_FIND_IGNORE_LEADING_WHITESPACE',
+    'GIT_DIFF_FIND_IGNORE_WHITESPACE',
+    'GIT_DIFF_FIND_REMOVE_UNMODIFIED',
+    'GIT_DIFF_FIND_RENAMES_FROM_REWRITES',
+    'GIT_DIFF_FIND_RENAMES',
+    'GIT_DIFF_FIND_REWRITES',
+    'GIT_DIFF_FLAG_BINARY',
+    'GIT_DIFF_FLAG_EXISTS',
+    'GIT_DIFF_FLAG_NOT_BINARY',
+    'GIT_DIFF_FLAG_VALID_ID',
+    'GIT_DIFF_FLAG_VALID_SIZE',
+    'GIT_DIFF_FORCE_BINARY',
+    'GIT_DIFF_FORCE_TEXT',
+    'GIT_DIFF_IGNORE_BLANK_LINES',
+    'GIT_DIFF_IGNORE_CASE',
+    'GIT_DIFF_IGNORE_FILEMODE',
+    'GIT_DIFF_IGNORE_SUBMODULES',
+    'GIT_DIFF_IGNORE_WHITESPACE_CHANGE',
+    'GIT_DIFF_IGNORE_WHITESPACE_EOL',
+    'GIT_DIFF_IGNORE_WHITESPACE',
+    'GIT_DIFF_INCLUDE_CASECHANGE',
+    'GIT_DIFF_INCLUDE_IGNORED',
+    'GIT_DIFF_INCLUDE_TYPECHANGE_TREES',
+    'GIT_DIFF_INCLUDE_TYPECHANGE',
+    'GIT_DIFF_INCLUDE_UNMODIFIED',
+    'GIT_DIFF_INCLUDE_UNREADABLE_AS_UNTRACKED',
+    'GIT_DIFF_INCLUDE_UNREADABLE',
+    'GIT_DIFF_INCLUDE_UNTRACKED',
+    'GIT_DIFF_INDENT_HEURISTIC',
+    'GIT_DIFF_MINIMAL',
+    'GIT_DIFF_NORMAL',
+    'GIT_DIFF_PATIENCE',
+    'GIT_DIFF_RECURSE_IGNORED_DIRS',
+    'GIT_DIFF_RECURSE_UNTRACKED_DIRS',
+    'GIT_DIFF_REVERSE',
+    'GIT_DIFF_SHOW_BINARY',
+    'GIT_DIFF_SHOW_UNMODIFIED',
+    'GIT_DIFF_SHOW_UNTRACKED_CONTENT',
+    'GIT_DIFF_SKIP_BINARY_CHECK',
+    'GIT_DIFF_STATS_FULL',
+    'GIT_DIFF_STATS_INCLUDE_SUMMARY',
+    'GIT_DIFF_STATS_NONE',
+    'GIT_DIFF_STATS_NUMBER',
+    'GIT_DIFF_STATS_SHORT',
+    'GIT_DIFF_UPDATE_INDEX',
+    'GIT_FILEMODE_BLOB_EXECUTABLE',
+    'GIT_FILEMODE_BLOB',
+    'GIT_FILEMODE_COMMIT',
+    'GIT_FILEMODE_LINK',
+    'GIT_FILEMODE_TREE',
+    'GIT_FILEMODE_UNREADABLE',
+    'GIT_FILTER_ALLOW_UNSAFE',
+    'GIT_FILTER_ATTRIBUTES_FROM_COMMIT',
+    'GIT_FILTER_ATTRIBUTES_FROM_HEAD',
+    'GIT_FILTER_CLEAN',
+    'GIT_FILTER_DEFAULT',
+    'GIT_FILTER_DRIVER_PRIORITY',
+    'GIT_FILTER_NO_SYSTEM_ATTRIBUTES',
+    'GIT_FILTER_SMUDGE',
+    'GIT_FILTER_TO_ODB',
+    'GIT_FILTER_TO_WORKTREE',
+    'GIT_MERGE_ANALYSIS_FASTFORWARD',
+    'GIT_MERGE_ANALYSIS_NONE',
+    'GIT_MERGE_ANALYSIS_NORMAL',
+    'GIT_MERGE_ANALYSIS_UNBORN',
+    'GIT_MERGE_ANALYSIS_UP_TO_DATE',
+    'GIT_MERGE_PREFERENCE_FASTFORWARD_ONLY',
+    'GIT_MERGE_PREFERENCE_NO_FASTFORWARD',
+    'GIT_MERGE_PREFERENCE_NONE',
+    'GIT_OBJECT_ANY',
+    'GIT_OBJECT_BLOB',
+    'GIT_OBJECT_COMMIT',
+    'GIT_OBJECT_INVALID',
+    'GIT_OBJECT_OFS_DELTA',
+    'GIT_OBJECT_REF_DELTA',
+    'GIT_OBJECT_TAG',
+    'GIT_OBJECT_TREE',
+    'GIT_OPT_DISABLE_PACK_KEEP_FILE_CHECKS',
+    'GIT_OPT_ENABLE_CACHING',
+    'GIT_OPT_ENABLE_FSYNC_GITDIR',
+    'GIT_OPT_ENABLE_OFS_DELTA',
+    'GIT_OPT_ENABLE_STRICT_HASH_VERIFICATION',
+    'GIT_OPT_ENABLE_STRICT_OBJECT_CREATION',
+    'GIT_OPT_ENABLE_STRICT_SYMBOLIC_REF_CREATION',
+    'GIT_OPT_ENABLE_UNSAVED_INDEX_SAFETY',
+    'GIT_OPT_GET_CACHED_MEMORY',
+    'GIT_OPT_GET_MWINDOW_FILE_LIMIT',
+    'GIT_OPT_GET_MWINDOW_MAPPED_LIMIT',
+    'GIT_OPT_GET_MWINDOW_SIZE',
+    'GIT_OPT_GET_OWNER_VALIDATION',
+    'GIT_OPT_GET_PACK_MAX_OBJECTS',
+    'GIT_OPT_GET_SEARCH_PATH',
+    'GIT_OPT_GET_TEMPLATE_PATH',
+    'GIT_OPT_GET_USER_AGENT',
+    'GIT_OPT_GET_WINDOWS_SHAREMODE',
+    'GIT_OPT_SET_ALLOCATOR',
+    'GIT_OPT_SET_CACHE_MAX_SIZE',
+    'GIT_OPT_SET_CACHE_OBJECT_LIMIT',
+    'GIT_OPT_SET_MWINDOW_FILE_LIMIT',
+    'GIT_OPT_SET_MWINDOW_MAPPED_LIMIT',
+    'GIT_OPT_SET_MWINDOW_SIZE',
+    'GIT_OPT_SET_OWNER_VALIDATION',
+    'GIT_OPT_SET_PACK_MAX_OBJECTS',
+    'GIT_OPT_SET_SEARCH_PATH',
+    'GIT_OPT_SET_SSL_CERT_LOCATIONS',
+    'GIT_OPT_SET_SSL_CIPHERS',
+    'GIT_OPT_SET_TEMPLATE_PATH',
+    'GIT_OPT_SET_USER_AGENT',
+    'GIT_OPT_SET_WINDOWS_SHAREMODE',
+    'GIT_REFERENCES_ALL',
+    'GIT_REFERENCES_BRANCHES',
+    'GIT_REFERENCES_TAGS',
+    'GIT_RESET_HARD',
+    'GIT_RESET_MIXED',
+    'GIT_RESET_SOFT',
+    'GIT_REVSPEC_MERGE_BASE',
+    'GIT_REVSPEC_RANGE',
+    'GIT_REVSPEC_SINGLE',
+    'GIT_SORT_NONE',
+    'GIT_SORT_REVERSE',
+    'GIT_SORT_TIME',
+    'GIT_SORT_TOPOLOGICAL',
+    'GIT_STASH_APPLY_DEFAULT',
+    'GIT_STASH_APPLY_REINSTATE_INDEX',
+    'GIT_STASH_DEFAULT',
+    'GIT_STASH_INCLUDE_IGNORED',
+    'GIT_STASH_INCLUDE_UNTRACKED',
+    'GIT_STASH_KEEP_ALL',
+    'GIT_STASH_KEEP_INDEX',
+    'GIT_STATUS_CONFLICTED',
+    'GIT_STATUS_CURRENT',
+    'GIT_STATUS_IGNORED',
+    'GIT_STATUS_INDEX_DELETED',
+    'GIT_STATUS_INDEX_MODIFIED',
+    'GIT_STATUS_INDEX_NEW',
+    'GIT_STATUS_INDEX_RENAMED',
+    'GIT_STATUS_INDEX_TYPECHANGE',
+    'GIT_STATUS_WT_DELETED',
+    'GIT_STATUS_WT_MODIFIED',
+    'GIT_STATUS_WT_NEW',
+    'GIT_STATUS_WT_RENAMED',
+    'GIT_STATUS_WT_TYPECHANGE',
+    'GIT_STATUS_WT_UNREADABLE',
+    'GIT_SUBMODULE_IGNORE_ALL',
+    'GIT_SUBMODULE_IGNORE_DIRTY',
+    'GIT_SUBMODULE_IGNORE_NONE',
+    'GIT_SUBMODULE_IGNORE_UNSPECIFIED',
+    'GIT_SUBMODULE_IGNORE_UNTRACKED',
+    'GIT_SUBMODULE_STATUS_IN_CONFIG',
+    'GIT_SUBMODULE_STATUS_IN_HEAD',
+    'GIT_SUBMODULE_STATUS_IN_INDEX',
+    'GIT_SUBMODULE_STATUS_IN_WD',
+    'GIT_SUBMODULE_STATUS_INDEX_ADDED',
+    'GIT_SUBMODULE_STATUS_INDEX_DELETED',
+    'GIT_SUBMODULE_STATUS_INDEX_MODIFIED',
+    'GIT_SUBMODULE_STATUS_WD_ADDED',
+    'GIT_SUBMODULE_STATUS_WD_DELETED',
+    'GIT_SUBMODULE_STATUS_WD_INDEX_MODIFIED',
+    'GIT_SUBMODULE_STATUS_WD_MODIFIED',
+    'GIT_SUBMODULE_STATUS_WD_UNINITIALIZED',
+    'GIT_SUBMODULE_STATUS_WD_UNTRACKED',
+    'GIT_SUBMODULE_STATUS_WD_WD_MODIFIED',
+    # High level API.
+    'enums',
+    'blame',
+    'Blame',
+    'BlameHunk',
+    'blob',
+    'BlobIO',
+    'callbacks',
+    'Payload',
+    'RemoteCallbacks',
+    'CheckoutCallbacks',
+    'StashApplyCallbacks',
+    'git_clone_options',
+    'git_fetch_options',
+    'git_proxy_options',
+    'get_credentials',
+    'config',
+    'Config',
+    'credentials',
+    'CredentialType',
+    'Username',
+    'UserPass',
+    'Keypair',
+    'KeypairFromAgent',
+    'KeypairFromMemory',
+    'errors',
+    'check_error',
+    'Passthrough',
+    'ffi',
+    'C',
+    'filter',
+    'Filter',
+    'index',
+    'Index',
+    'IndexEntry',
+    'legacyenums',
+    'GIT_FEATURE_THREADS',
+    'GIT_FEATURE_HTTPS',
+    'GIT_FEATURE_SSH',
+    'GIT_FEATURE_NSEC',
+    'GIT_REPOSITORY_INIT_BARE',
+    'GIT_REPOSITORY_INIT_NO_REINIT',
+    'GIT_REPOSITORY_INIT_NO_DOTGIT_DIR',
+    'GIT_REPOSITORY_INIT_MKDIR',
+    'GIT_REPOSITORY_INIT_MKPATH',
+    'GIT_REPOSITORY_INIT_EXTERNAL_TEMPLATE',
+    'GIT_REPOSITORY_INIT_RELATIVE_GITLINK',
+    'GIT_REPOSITORY_INIT_SHARED_UMASK',
+    'GIT_REPOSITORY_INIT_SHARED_GROUP',
+    'GIT_REPOSITORY_INIT_SHARED_ALL',
+    'GIT_REPOSITORY_OPEN_NO_SEARCH',
+    'GIT_REPOSITORY_OPEN_CROSS_FS',
+    'GIT_REPOSITORY_OPEN_BARE',
+    'GIT_REPOSITORY_OPEN_NO_DOTGIT',
+    'GIT_REPOSITORY_OPEN_FROM_ENV',
+    'GIT_REPOSITORY_STATE_NONE',
+    'GIT_REPOSITORY_STATE_MERGE',
+    'GIT_REPOSITORY_STATE_REVERT',
+    'GIT_REPOSITORY_STATE_REVERT_SEQUENCE',
+    'GIT_REPOSITORY_STATE_CHERRYPICK',
+    'GIT_REPOSITORY_STATE_CHERRYPICK_SEQUENCE',
+    'GIT_REPOSITORY_STATE_BISECT',
+    'GIT_REPOSITORY_STATE_REBASE',
+    'GIT_REPOSITORY_STATE_REBASE_INTERACTIVE',
+    'GIT_REPOSITORY_STATE_REBASE_MERGE',
+    'GIT_REPOSITORY_STATE_APPLY_MAILBOX',
+    'GIT_REPOSITORY_STATE_APPLY_MAILBOX_OR_REBASE',
+    'GIT_ATTR_CHECK_FILE_THEN_INDEX',
+    'GIT_ATTR_CHECK_INDEX_THEN_FILE',
+    'GIT_ATTR_CHECK_INDEX_ONLY',
+    'GIT_ATTR_CHECK_NO_SYSTEM',
+    'GIT_ATTR_CHECK_INCLUDE_HEAD',
+    'GIT_ATTR_CHECK_INCLUDE_COMMIT',
+    'GIT_FETCH_PRUNE_UNSPECIFIED',
+    'GIT_FETCH_PRUNE',
+    'GIT_FETCH_NO_PRUNE',
+    'GIT_CHECKOUT_NOTIFY_NONE',
+    'GIT_CHECKOUT_NOTIFY_CONFLICT',
+    'GIT_CHECKOUT_NOTIFY_DIRTY',
+    'GIT_CHECKOUT_NOTIFY_UPDATED',
+    'GIT_CHECKOUT_NOTIFY_UNTRACKED',
+    'GIT_CHECKOUT_NOTIFY_IGNORED',
+    'GIT_CHECKOUT_NOTIFY_ALL',
+    'GIT_STASH_APPLY_PROGRESS_NONE',
+    'GIT_STASH_APPLY_PROGRESS_LOADING_STASH',
+    'GIT_STASH_APPLY_PROGRESS_ANALYZE_INDEX',
+    'GIT_STASH_APPLY_PROGRESS_ANALYZE_MODIFIED',
+    'GIT_STASH_APPLY_PROGRESS_ANALYZE_UNTRACKED',
+    'GIT_STASH_APPLY_PROGRESS_CHECKOUT_UNTRACKED',
+    'GIT_STASH_APPLY_PROGRESS_CHECKOUT_MODIFIED',
+    'GIT_STASH_APPLY_PROGRESS_DONE',
+    'GIT_CREDENTIAL_USERPASS_PLAINTEXT',
+    'GIT_CREDENTIAL_SSH_KEY',
+    'GIT_CREDENTIAL_SSH_CUSTOM',
+    'GIT_CREDENTIAL_DEFAULT',
+    'GIT_CREDENTIAL_SSH_INTERACTIVE',
+    'GIT_CREDENTIAL_USERNAME',
+    'GIT_CREDENTIAL_SSH_MEMORY',
+    'packbuilder',
+    'PackBuilder',
+    'refspec',
+    'remotes',
+    'Remote',
+    'repository',
+    'Repository',
+    'branches',
+    'references',
+    'settings',
+    'Settings',
+    'submodules',
+    'Submodule',
+    'utils',
+    'to_bytes',
+    'to_str',
+    # __init__ module defined symbols
+    'features',
+    'LIBGIT2_VER',
+    'init_repository',
+    'clone_repository',
+    'tree_entry_key',
+)
diff -pruN 1.17.0-2/pygit2/_build.py 1.18.1-1/pygit2/_build.py
--- 1.17.0-2/pygit2/_build.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/pygit2/_build.py	2025-07-26 10:03:19.000000000 +0000
@@ -34,11 +34,11 @@ from pathlib import Path
 #
 # The version number of pygit2
 #
-__version__ = '1.17.0'
+__version__ = '1.18.1'
 
 
 #
-# Utility functions to get the paths required for bulding extensions
+# Utility functions to get the paths required for building extensions
 #
 def _get_libgit2_path():
     # LIBGIT2 environment variable takes precedence
diff -pruN 1.17.0-2/pygit2/_pygit2.pyi 1.18.1-1/pygit2/_pygit2.pyi
--- 1.17.0-2/pygit2/_pygit2.pyi	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/pygit2/_pygit2.pyi	2025-07-26 10:03:19.000000000 +0000
@@ -1,9 +1,12 @@
-from typing import Iterator, Literal, Optional, overload
-from io import IOBase
+from typing import Iterator, Literal, Optional, overload, Type, TypedDict
+from io import IOBase, DEFAULT_BUFFER_SIZE
+from queue import Queue
+from threading import Event
 from . import Index
 from .enums import (
     ApplyLocation,
     BranchType,
+    BlobFilter,
     DeltaStatus,
     DiffFind,
     DiffFlag,
@@ -19,41 +22,282 @@ from .enums import (
     ResetMode,
     SortMode,
 )
+from collections.abc import Generator
+
+from .repository import BaseRepository
+from .remotes import Remote
+
+GIT_OBJ_BLOB = Literal[3]
+GIT_OBJ_COMMIT = Literal[1]
+GIT_OBJ_TAG = Literal[4]
+GIT_OBJ_TREE = Literal[2]
 
-GIT_OBJ_BLOB: Literal[3]
-GIT_OBJ_COMMIT: Literal[1]
-GIT_OBJ_TAG: Literal[4]
-GIT_OBJ_TREE: Literal[2]
-GIT_OID_HEXSZ: int
-GIT_OID_HEX_ZERO: str
-GIT_OID_MINPREFIXLEN: int
-GIT_OID_RAWSZ: int
-LIBGIT2_VERSION: str
 LIBGIT2_VER_MAJOR: int
 LIBGIT2_VER_MINOR: int
 LIBGIT2_VER_REVISION: int
+LIBGIT2_VERSION: str
+GIT_OPT_GET_MWINDOW_SIZE: int
+GIT_OPT_SET_MWINDOW_SIZE: int
+GIT_OPT_GET_MWINDOW_MAPPED_LIMIT: int
+GIT_OPT_SET_MWINDOW_MAPPED_LIMIT: int
+GIT_OPT_GET_SEARCH_PATH: int
+GIT_OPT_SET_SEARCH_PATH: int
+GIT_OPT_SET_CACHE_OBJECT_LIMIT: int
+GIT_OPT_SET_CACHE_MAX_SIZE: int
+GIT_OPT_ENABLE_CACHING: int
+GIT_OPT_GET_CACHED_MEMORY: int
+GIT_OPT_GET_TEMPLATE_PATH: int
+GIT_OPT_SET_TEMPLATE_PATH: int
+GIT_OPT_SET_SSL_CERT_LOCATIONS: int
+GIT_OPT_SET_USER_AGENT: int
+GIT_OPT_ENABLE_STRICT_OBJECT_CREATION: int
+GIT_OPT_ENABLE_STRICT_SYMBOLIC_REF_CREATION: int
+GIT_OPT_SET_SSL_CIPHERS: int
+GIT_OPT_GET_USER_AGENT: int
+GIT_OPT_ENABLE_OFS_DELTA: int
+GIT_OPT_ENABLE_FSYNC_GITDIR: int
+GIT_OPT_GET_WINDOWS_SHAREMODE: int
+GIT_OPT_SET_WINDOWS_SHAREMODE: int
+GIT_OPT_ENABLE_STRICT_HASH_VERIFICATION: int
+GIT_OPT_SET_ALLOCATOR: int
+GIT_OPT_ENABLE_UNSAVED_INDEX_SAFETY: int
+GIT_OPT_GET_PACK_MAX_OBJECTS: int
+GIT_OPT_SET_PACK_MAX_OBJECTS: int
+GIT_OPT_DISABLE_PACK_KEEP_FILE_CHECKS: int
+GIT_OPT_GET_OWNER_VALIDATION: int
+GIT_OPT_SET_OWNER_VALIDATION: int
+GIT_OPT_GET_MWINDOW_FILE_LIMIT: int
+GIT_OPT_SET_MWINDOW_FILE_LIMIT: int
+GIT_OID_RAWSZ: int
+GIT_OID_HEXSZ: int
+GIT_OID_HEX_ZERO: str
+GIT_OID_MINPREFIXLEN: int
+GIT_OBJECT_ANY: int
+GIT_OBJECT_INVALID: int
+GIT_OBJECT_COMMIT: int
+GIT_OBJECT_TREE: int
+GIT_OBJECT_BLOB: int
+GIT_OBJECT_TAG: int
+GIT_OBJECT_OFS_DELTA: int
+GIT_OBJECT_REF_DELTA: int
+GIT_FILEMODE_UNREADABLE: int
+GIT_FILEMODE_TREE: int
+GIT_FILEMODE_BLOB: int
+GIT_FILEMODE_BLOB_EXECUTABLE: int
+GIT_FILEMODE_LINK: int
+GIT_FILEMODE_COMMIT: int
+GIT_SORT_NONE: int
+GIT_SORT_TOPOLOGICAL: int
+GIT_SORT_TIME: int
+GIT_SORT_REVERSE: int
+GIT_RESET_SOFT: int
+GIT_RESET_MIXED: int
+GIT_RESET_HARD: int
+GIT_REFERENCES_ALL: int
+GIT_REFERENCES_BRANCHES: int
+GIT_REFERENCES_TAGS: int
+GIT_REVSPEC_SINGLE: int
+GIT_REVSPEC_RANGE: int
+GIT_REVSPEC_MERGE_BASE: int
+GIT_BRANCH_LOCAL: int
+GIT_BRANCH_REMOTE: int
+GIT_BRANCH_ALL: int
+GIT_STATUS_CURRENT: int
+GIT_STATUS_INDEX_NEW: int
+GIT_STATUS_INDEX_MODIFIED: int
+GIT_STATUS_INDEX_DELETED: int
+GIT_STATUS_INDEX_RENAMED: int
+GIT_STATUS_INDEX_TYPECHANGE: int
+GIT_STATUS_WT_NEW: int
+GIT_STATUS_WT_MODIFIED: int
+GIT_STATUS_WT_DELETED: int
+GIT_STATUS_WT_TYPECHANGE: int
+GIT_STATUS_WT_RENAMED: int
+GIT_STATUS_WT_UNREADABLE: int
+GIT_STATUS_IGNORED: int
+GIT_STATUS_CONFLICTED: int
+GIT_CHECKOUT_NONE: int
+GIT_CHECKOUT_SAFE: int
+GIT_CHECKOUT_FORCE: int
+GIT_CHECKOUT_RECREATE_MISSING: int
+GIT_CHECKOUT_ALLOW_CONFLICTS: int
+GIT_CHECKOUT_REMOVE_UNTRACKED: int
+GIT_CHECKOUT_REMOVE_IGNORED: int
+GIT_CHECKOUT_UPDATE_ONLY: int
+GIT_CHECKOUT_DONT_UPDATE_INDEX: int
+GIT_CHECKOUT_NO_REFRESH: int
+GIT_CHECKOUT_SKIP_UNMERGED: int
+GIT_CHECKOUT_USE_OURS: int
+GIT_CHECKOUT_USE_THEIRS: int
+GIT_CHECKOUT_DISABLE_PATHSPEC_MATCH: int
+GIT_CHECKOUT_SKIP_LOCKED_DIRECTORIES: int
+GIT_CHECKOUT_DONT_OVERWRITE_IGNORED: int
+GIT_CHECKOUT_CONFLICT_STYLE_MERGE: int
+GIT_CHECKOUT_CONFLICT_STYLE_DIFF3: int
+GIT_CHECKOUT_DONT_REMOVE_EXISTING: int
+GIT_CHECKOUT_DONT_WRITE_INDEX: int
+GIT_CHECKOUT_DRY_RUN: int
+GIT_CHECKOUT_CONFLICT_STYLE_ZDIFF3: int
+GIT_DIFF_NORMAL: int
+GIT_DIFF_REVERSE: int
+GIT_DIFF_INCLUDE_IGNORED: int
+GIT_DIFF_RECURSE_IGNORED_DIRS: int
+GIT_DIFF_INCLUDE_UNTRACKED: int
+GIT_DIFF_RECURSE_UNTRACKED_DIRS: int
+GIT_DIFF_INCLUDE_UNMODIFIED: int
+GIT_DIFF_INCLUDE_TYPECHANGE: int
+GIT_DIFF_INCLUDE_TYPECHANGE_TREES: int
+GIT_DIFF_IGNORE_FILEMODE: int
+GIT_DIFF_IGNORE_SUBMODULES: int
+GIT_DIFF_IGNORE_CASE: int
+GIT_DIFF_INCLUDE_CASECHANGE: int
+GIT_DIFF_DISABLE_PATHSPEC_MATCH: int
+GIT_DIFF_SKIP_BINARY_CHECK: int
+GIT_DIFF_ENABLE_FAST_UNTRACKED_DIRS: int
+GIT_DIFF_UPDATE_INDEX: int
+GIT_DIFF_INCLUDE_UNREADABLE: int
+GIT_DIFF_INCLUDE_UNREADABLE_AS_UNTRACKED: int
+GIT_DIFF_INDENT_HEURISTIC: int
+GIT_DIFF_IGNORE_BLANK_LINES: int
+GIT_DIFF_FORCE_TEXT: int
+GIT_DIFF_FORCE_BINARY: int
+GIT_DIFF_IGNORE_WHITESPACE: int
+GIT_DIFF_IGNORE_WHITESPACE_CHANGE: int
+GIT_DIFF_IGNORE_WHITESPACE_EOL: int
+GIT_DIFF_SHOW_UNTRACKED_CONTENT: int
+GIT_DIFF_SHOW_UNMODIFIED: int
+GIT_DIFF_PATIENCE: int
+GIT_DIFF_MINIMAL: int
+GIT_DIFF_SHOW_BINARY: int
+GIT_DIFF_STATS_NONE: int
+GIT_DIFF_STATS_FULL: int
+GIT_DIFF_STATS_SHORT: int
+GIT_DIFF_STATS_NUMBER: int
+GIT_DIFF_STATS_INCLUDE_SUMMARY: int
+GIT_DIFF_FIND_BY_CONFIG: int
+GIT_DIFF_FIND_RENAMES: int
+GIT_DIFF_FIND_RENAMES_FROM_REWRITES: int
+GIT_DIFF_FIND_COPIES: int
+GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED: int
+GIT_DIFF_FIND_REWRITES: int
+GIT_DIFF_BREAK_REWRITES: int
+GIT_DIFF_FIND_AND_BREAK_REWRITES: int
+GIT_DIFF_FIND_FOR_UNTRACKED: int
+GIT_DIFF_FIND_ALL: int
+GIT_DIFF_FIND_IGNORE_LEADING_WHITESPACE: int
+GIT_DIFF_FIND_IGNORE_WHITESPACE: int
+GIT_DIFF_FIND_DONT_IGNORE_WHITESPACE: int
+GIT_DIFF_FIND_EXACT_MATCH_ONLY: int
+GIT_DIFF_BREAK_REWRITES_FOR_RENAMES_ONLY: int
+GIT_DIFF_FIND_REMOVE_UNMODIFIED: int
+GIT_DIFF_FLAG_BINARY: int
+GIT_DIFF_FLAG_NOT_BINARY: int
+GIT_DIFF_FLAG_VALID_ID: int
+GIT_DIFF_FLAG_EXISTS: int
+GIT_DIFF_FLAG_VALID_SIZE: int
+GIT_DELTA_UNMODIFIED: int
+GIT_DELTA_ADDED: int
+GIT_DELTA_DELETED: int
+GIT_DELTA_MODIFIED: int
+GIT_DELTA_RENAMED: int
+GIT_DELTA_COPIED: int
+GIT_DELTA_IGNORED: int
+GIT_DELTA_UNTRACKED: int
+GIT_DELTA_TYPECHANGE: int
+GIT_DELTA_UNREADABLE: int
+GIT_DELTA_CONFLICTED: int
+GIT_CONFIG_LEVEL_PROGRAMDATA: int
+GIT_CONFIG_LEVEL_SYSTEM: int
+GIT_CONFIG_LEVEL_XDG: int
+GIT_CONFIG_LEVEL_GLOBAL: int
+GIT_CONFIG_LEVEL_LOCAL: int
+GIT_CONFIG_LEVEL_WORKTREE: int
+GIT_CONFIG_LEVEL_APP: int
+GIT_CONFIG_HIGHEST_LEVEL: int
+GIT_BLAME_NORMAL: int
+GIT_BLAME_TRACK_COPIES_SAME_FILE: int
+GIT_BLAME_TRACK_COPIES_SAME_COMMIT_MOVES: int
+GIT_BLAME_TRACK_COPIES_SAME_COMMIT_COPIES: int
+GIT_BLAME_TRACK_COPIES_ANY_COMMIT_COPIES: int
+GIT_BLAME_FIRST_PARENT: int
+GIT_BLAME_USE_MAILMAP: int
+GIT_BLAME_IGNORE_WHITESPACE: int
+GIT_MERGE_ANALYSIS_NONE: int
+GIT_MERGE_ANALYSIS_NORMAL: int
+GIT_MERGE_ANALYSIS_UP_TO_DATE: int
+GIT_MERGE_ANALYSIS_FASTFORWARD: int
+GIT_MERGE_ANALYSIS_UNBORN: int
+GIT_MERGE_PREFERENCE_NONE: int
+GIT_MERGE_PREFERENCE_NO_FASTFORWARD: int
+GIT_MERGE_PREFERENCE_FASTFORWARD_ONLY: int
+GIT_DESCRIBE_DEFAULT: int
+GIT_DESCRIBE_TAGS: int
+GIT_DESCRIBE_ALL: int
+GIT_STASH_DEFAULT: int
+GIT_STASH_KEEP_INDEX: int
+GIT_STASH_INCLUDE_UNTRACKED: int
+GIT_STASH_INCLUDE_IGNORED: int
+GIT_STASH_KEEP_ALL: int
+GIT_STASH_APPLY_DEFAULT: int
+GIT_STASH_APPLY_REINSTATE_INDEX: int
+GIT_APPLY_LOCATION_WORKDIR: int
+GIT_APPLY_LOCATION_INDEX: int
+GIT_APPLY_LOCATION_BOTH: int
+GIT_SUBMODULE_IGNORE_UNSPECIFIED: int
+GIT_SUBMODULE_IGNORE_NONE: int
+GIT_SUBMODULE_IGNORE_UNTRACKED: int
+GIT_SUBMODULE_IGNORE_DIRTY: int
+GIT_SUBMODULE_IGNORE_ALL: int
+GIT_SUBMODULE_STATUS_IN_HEAD: int
+GIT_SUBMODULE_STATUS_IN_INDEX: int
+GIT_SUBMODULE_STATUS_IN_CONFIG: int
+GIT_SUBMODULE_STATUS_IN_WD: int
+GIT_SUBMODULE_STATUS_INDEX_ADDED: int
+GIT_SUBMODULE_STATUS_INDEX_DELETED: int
+GIT_SUBMODULE_STATUS_INDEX_MODIFIED: int
+GIT_SUBMODULE_STATUS_WD_UNINITIALIZED: int
+GIT_SUBMODULE_STATUS_WD_ADDED: int
+GIT_SUBMODULE_STATUS_WD_DELETED: int
+GIT_SUBMODULE_STATUS_WD_MODIFIED: int
+GIT_SUBMODULE_STATUS_WD_INDEX_MODIFIED: int
+GIT_SUBMODULE_STATUS_WD_WD_MODIFIED: int
+GIT_SUBMODULE_STATUS_WD_UNTRACKED: int
+GIT_BLOB_FILTER_CHECK_FOR_BINARY: int
+GIT_BLOB_FILTER_NO_SYSTEM_ATTRIBUTES: int
+GIT_BLOB_FILTER_ATTRIBUTES_FROM_HEAD: int
+GIT_BLOB_FILTER_ATTRIBUTES_FROM_COMMIT: int
+GIT_FILTER_DRIVER_PRIORITY: int
+GIT_FILTER_TO_WORKTREE: int
+GIT_FILTER_SMUDGE: int
+GIT_FILTER_TO_ODB: int
+GIT_FILTER_CLEAN: int
+GIT_FILTER_DEFAULT: int
+GIT_FILTER_ALLOW_UNSAFE: int
+GIT_FILTER_NO_SYSTEM_ATTRIBUTES: int
+GIT_FILTER_ATTRIBUTES_FROM_HEAD: int
+GIT_FILTER_ATTRIBUTES_FROM_COMMIT: int
 
 class Object:
     _pointer: bytes
     filemode: FileMode
-    hex: str
     id: Oid
     name: str | None
-    oid: Oid
     raw_name: bytes | None
     short_id: str
     type: 'Literal[GIT_OBJ_COMMIT] | Literal[GIT_OBJ_TREE] | Literal[GIT_OBJ_TAG] | Literal[GIT_OBJ_BLOB]'
     type_str: "Literal['commit'] | Literal['tree'] | Literal['tag'] | Literal['blob']"
     @overload
-    def peel(self, target_type: 'Literal[GIT_OBJ_COMMIT]') -> 'Commit': ...
+    def peel(
+        self, target_type: 'Literal[GIT_OBJ_COMMIT] | Type[Commit]'
+    ) -> 'Commit': ...
     @overload
-    def peel(self, target_type: 'Literal[GIT_OBJ_TREE]') -> 'Tree': ...
+    def peel(self, target_type: 'Literal[GIT_OBJ_TREE] | Type[Tree]') -> 'Tree': ...
     @overload
-    def peel(self, target_type: 'Literal[GIT_OBJ_TAG]') -> 'Tag': ...
+    def peel(self, target_type: 'Literal[GIT_OBJ_TAG] | Type[Tag]') -> 'Tag': ...
     @overload
-    def peel(self, target_type: 'Literal[GIT_OBJ_BLOB]') -> 'Blob': ...
+    def peel(self, target_type: 'Literal[GIT_OBJ_BLOB] | Type[Blob]') -> 'Blob': ...
     @overload
-    def peel(self, target_type: 'None') -> 'Commit|Tree|Blob': ...
+    def peel(self, target_type: 'None') -> 'Commit|Tree|Tag|Blob': ...
     def read_raw(self) -> bytes: ...
     def __eq__(self, other) -> bool: ...
     def __ge__(self, other) -> bool: ...
@@ -75,15 +319,15 @@ class Reference:
     def delete(self) -> None: ...
     def log(self) -> Iterator[RefLogEntry]: ...
     @overload
-    def peel(self, type: 'Literal[GIT_OBJ_COMMIT]') -> 'Commit': ...
+    def peel(self, type: 'Literal[GIT_OBJ_COMMIT] | Type[Commit]') -> 'Commit': ...
     @overload
-    def peel(self, type: 'Literal[GIT_OBJ_TREE]') -> 'Tree': ...
+    def peel(self, type: 'Literal[GIT_OBJ_TREE] | Type[Tree]') -> 'Tree': ...
     @overload
-    def peel(self, type: 'Literal[GIT_OBJ_TAG]') -> 'Tag': ...
+    def peel(self, type: 'Literal[GIT_OBJ_TAG] | Type[Tag]') -> 'Tag': ...
     @overload
-    def peel(self, type: 'Literal[GIT_OBJ_BLOB]') -> 'Blob': ...
+    def peel(self, type: 'Literal[GIT_OBJ_BLOB] | Type[Blob]') -> 'Blob': ...
     @overload
-    def peel(self, type: 'None') -> 'Commit|Tree|Blob': ...
+    def peel(self, type: 'None' = None) -> 'Commit|Tree|Tag|Blob': ...
     def rename(self, new_name: str) -> None: ...
     def resolve(self) -> Reference: ...
     def set_target(self, target: _OidArg, message: str = ...) -> None: ...
@@ -114,6 +358,15 @@ class Blob(Object):
         old_as_path: str = ...,
         buffer_as_path: str = ...,
     ) -> Patch: ...
+    def _write_to_queue(
+        self,
+        queue: Queue,
+        closed: Event,
+        chunk_size: int = DEFAULT_BUFFER_SIZE,
+        as_path: Optional[str] = None,
+        flags: BlobFilter = BlobFilter.CHECK_FOR_BINARY,
+        commit_id: Optional[Oid] = None,
+    ) -> None: ...
 
 class Branch(Reference):
     branch_name: str
@@ -124,7 +377,25 @@ class Branch(Reference):
     def delete(self) -> None: ...
     def is_checked_out(self) -> bool: ...
     def is_head(self) -> bool: ...
-    def rename(self, name: str, force: bool = False) -> None: ...
+    def rename(self, name: str, force: bool = False) -> 'Branch': ...  # type: ignore[override]
+
+class FetchOptions:
+    # incomplete
+    depth: int
+    proxy_opts: ProxyOpts
+
+class CloneOptions:
+    # incomplete
+    version: int
+    checkout_opts: object
+    fetch_opts: FetchOptions
+    bare: int
+    local: object
+    checkout_branch: object
+    repository_cb: object
+    repository_cb_payload: object
+    remote_cb: object
+    remote_cb_payload: object
 
 class Commit(Object):
     author: Signature
@@ -207,6 +478,10 @@ class DiffStats:
     insertions: int
     def format(self, format: DiffStatsFormat, width: int) -> str: ...
 
+class FilterSource:
+    # probably incomplete
+    pass
+
 class GitError(Exception): ...
 class InvalidSpecError(ValueError): ...
 
@@ -262,7 +537,6 @@ class OdbBackendPack(OdbBackend):
     def __init__(self, *args, **kwargs) -> None: ...
 
 class Oid:
-    hex: str
     raw: bytes
     def __init__(self, raw: bytes = ..., hex: str = ...) -> None: ...
     def __eq__(self, other) -> bool: ...
@@ -272,6 +546,7 @@ class Oid:
     def __le__(self, other) -> bool: ...
     def __lt__(self, other) -> bool: ...
     def __ne__(self, other) -> bool: ...
+    def __bool__(self) -> bool: ...
 
 class Patch:
     data: bytes
@@ -331,6 +606,80 @@ class RefdbBackend:
 class RefdbFsBackend(RefdbBackend):
     def __init__(self, *args, **kwargs) -> None: ...
 
+class References:
+    def __init__(self, repository: BaseRepository) -> None: ...
+    def __getitem__(self, name: str) -> Reference: ...
+    def get(self, key: str) -> Reference: ...
+    def __iter__(self) -> Iterator[str]: ...
+    def iterator(
+        self, references_return_type: ReferenceFilter = ...
+    ) -> Iterator[Reference]: ...
+    def create(self, name: str, target: _OidArg, force: bool = False) -> Reference: ...
+    def delete(self, name: str) -> None: ...
+    def __contains__(self, name: str) -> bool: ...
+    @property
+    def objects(self) -> list[Reference]: ...
+    def compress(self) -> None: ...
+
+_Proxy = None | Literal[True] | str
+
+class _StrArray:
+    # incomplete
+    count: int
+
+class ProxyOpts:
+    # incomplete
+    type: object
+    url: str
+
+class PushOptions:
+    version: int
+    pb_parallelism: int
+    callbacks: object  # TODO
+    proxy_opts: ProxyOpts
+    follow_redirects: object  # TODO
+    custom_headers: _StrArray
+    remote_push_options: _StrArray
+
+class _LsRemotesDict(TypedDict):
+    local: bool
+    loid: Oid | None
+    name: str | None
+    symref_target: str | None
+    oid: Oid
+
+class RemoteCollection:
+    def __init__(self, repo: BaseRepository) -> None: ...
+    def __len__(self) -> int: ...
+    def __iter__(self): ...
+    def __getitem__(self, name: str | int) -> Remote: ...
+    def names(self) -> Generator[str, None, None]: ...
+    def create(self, name: str, url: str, fetch: str | None = None) -> Remote: ...
+    def create_anonymous(self, url: str) -> Remote: ...
+    def rename(self, name: str, new_name: str) -> list[str]: ...
+    def delete(self, name: str) -> None: ...
+    def set_url(self, name: str, url: str) -> None: ...
+    def set_push_url(self, name: str, url: str) -> None: ...
+    def add_fetch(self, name: str, refspec: str) -> None: ...
+    def add_push(self, name: str, refspec: str) -> None: ...
+
+class Branches:
+    local: 'Branches'
+    remote: 'Branches'
+    def __init__(
+        self,
+        repository: BaseRepository,
+        flag: BranchType = ...,
+        commit: Commit | _OidArg | None = None,
+    ) -> None: ...
+    def __getitem__(self, name: str) -> Branch: ...
+    def get(self, key: str) -> Branch: ...
+    def __iter__(self) -> Iterator[str]: ...
+    def create(self, name: str, commit: Commit, force: bool = False) -> Branch: ...
+    def delete(self, name: str) -> None: ...
+    def with_commit(self, commit: Commit | _OidArg | None) -> 'Branches': ...
+    def __contains__(self, name: _OidArg) -> bool: ...
+
 class Repository:
     _pointer: bytes
     default_signature: Signature
@@ -344,10 +693,14 @@ class Repository:
     path: str
     refdb: Refdb
     workdir: str
+    references: References
+    remotes: RemoteCollection
+    branches: Branches
     def __init__(self, *args, **kwargs) -> None: ...
     def TreeBuilder(self, src: Tree | _OidArg = ...) -> TreeBuilder: ...
     def _disown(self, *args, **kwargs) -> None: ...
     def _from_c(self, *args, **kwargs) -> None: ...
+    def __getitem__(self, key: str | Oid) -> Object: ...
     def add_worktree(self, name: str, path: str, ref: Reference = ...) -> Worktree: ...
     def applies(
         self,
@@ -396,6 +749,9 @@ class Repository:
         ref: str = 'refs/notes/commits',
         force: bool = False,
     ) -> Oid: ...
+    def create_reference(
+        self, name: str, target: _OidArg, force: bool = False
+    ) -> Reference: ...
     def create_reference_direct(
         self, name: str, target: _OidArg, force: bool, message: Optional[str] = None
     ) -> Reference: ...
@@ -445,6 +801,7 @@ class Repository:
     def revparse(self, revspec: str) -> RevSpec: ...
     def revparse_ext(self, revision: str) -> tuple[Object, Reference]: ...
     def revparse_single(self, revision: str) -> Object: ...
+    def set_ident(self, name: str, email: str) -> None: ...
     def set_odb(self, odb: Odb) -> None: ...
     def set_refdb(self, refdb: Refdb) -> None: ...
     def status(
@@ -565,5 +922,6 @@ def init_file_backend(path: str, flags:
 def option(opt: Option, *args) -> None: ...
 def reference_is_valid_name(refname: str) -> bool: ...
 def tree_entry_cmp(a: Object, b: Object) -> int: ...
+def _cache_enums() -> None: ...
 
 _OidArg = str | Oid
diff -pruN 1.17.0-2/pygit2/_run.py 1.18.1-1/pygit2/_run.py
--- 1.17.0-2/pygit2/_run.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/pygit2/_run.py	2025-07-26 10:03:19.000000000 +0000
@@ -33,11 +33,11 @@ from pathlib import Path
 import sys
 
 # Import from cffi
-from cffi import FFI
+from cffi import FFI  # type: ignore
 
 # Import from pygit2
 try:
-    from _build import get_libgit2_paths
+    from _build import get_libgit2_paths  # type: ignore
 except ImportError:
     from ._build import get_libgit2_paths
 
@@ -85,7 +85,7 @@ h_files = [
 ]
 h_source = []
 for h_file in h_files:
-    h_file = dir_path / 'decl' / h_file
+    h_file = dir_path / 'decl' / h_file  # type: ignore
     with codecs.open(h_file, 'r', 'utf-8') as f:
         h_source.append(f.read())
 
diff -pruN 1.17.0-2/pygit2/blob.py 1.18.1-1/pygit2/blob.py
--- 1.17.0-2/pygit2/blob.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/pygit2/blob.py	2025-07-26 10:03:19.000000000 +0000
@@ -26,7 +26,7 @@ class _BlobIO(io.RawIOBase):
     ):
         super().__init__()
         self._blob = blob
-        self._queue = Queue(maxsize=1)
+        self._queue: Optional[Queue] = Queue(maxsize=1)
         self._ready = threading.Event()
         self._writer_closed = threading.Event()
         self._chunk: Optional[bytes] = None
@@ -45,7 +45,7 @@ class _BlobIO(io.RawIOBase):
     def __exit__(self, exc_type, exc_value, traceback):
         self.close()
 
-    def isatty():
+    def isatty(self):
         return False
 
     def readable(self):
diff -pruN 1.17.0-2/pygit2/callbacks.py 1.18.1-1/pygit2/callbacks.py
--- 1.17.0-2/pygit2/callbacks.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/pygit2/callbacks.py	2025-07-26 10:03:19.000000000 +0000
@@ -65,7 +65,7 @@ API.
 # Standard Library
 from contextlib import contextmanager
 from functools import wraps
-from typing import Optional, Union
+from typing import Optional, Union, TYPE_CHECKING, Callable, Generator
 
 # pygit2
 from ._pygit2 import Oid, DiffFile
@@ -73,8 +73,13 @@ from .enums import CheckoutNotify, Check
 from .errors import check_error, Passthrough
 from .ffi import ffi, C
 from .utils import maybe_string, to_bytes, ptr_to_bytes, StrArray
+from .credentials import Username, UserPass, Keypair
 
+_Credentials = Username | UserPass | Keypair
 
+if TYPE_CHECKING:
+    from .remotes import TransferProgress
+    from ._pygit2 import ProxyOpts, PushOptions, CloneOptions
 #
 # The payload is the way to pass information from the pygit2 API, through
 # libgit2, to the Python callbacks. And back.
@@ -82,12 +87,16 @@ from .utils import maybe_string, to_byte
 
 
 class Payload:
-    def __init__(self, **kw):
+    repository: Callable | None
+    remote: Callable | None
+    clone_options: 'CloneOptions'
+
+    def __init__(self, **kw: object) -> None:
         for key, value in kw.items():
             setattr(self, key, value)
         self._stored_exception = None
 
-    def check_error(self, error_code):
+    def check_error(self, error_code: int) -> None:
         if error_code == C.GIT_EUSER:
             assert self._stored_exception is not None
             raise self._stored_exception
@@ -113,14 +122,20 @@ class RemoteCallbacks(Payload):
     RemoteCallbacks(certificate=certificate).
     """
 
-    def __init__(self, credentials=None, certificate_check=None):
+    push_options: 'PushOptions'
+
+    def __init__(
+        self,
+        credentials: _Credentials | None = None,
+        certificate_check: Callable[[None, bool, bytes], bool] | None = None,
+    ) -> None:
         super().__init__()
         if credentials is not None:
-            self.credentials = credentials
+            self.credentials = credentials  # type: ignore[method-assign, assignment]
         if certificate_check is not None:
-            self.certificate_check = certificate_check
+            self.certificate_check = certificate_check  # type: ignore[method-assign, assignment]
 
-    def sideband_progress(self, string):
+    def sideband_progress(self, string: str) -> None:
         """
         Progress output callback.  Override this function with your own
         progress reporting function
@@ -136,7 +151,7 @@ class RemoteCallbacks(Payload):
         url: str,
         username_from_url: Union[str, None],
         allowed_types: CredentialType,
-    ):
+    ) -> _Credentials:
         """
         Credentials callback.  If the remote server requires authentication,
         this function will be called and its return value used for
@@ -159,7 +174,7 @@ class RemoteCallbacks(Payload):
         """
         raise Passthrough
 
-    def certificate_check(self, certificate, valid, host):
+    def certificate_check(self, certificate: None, valid: bool, host: bytes) -> bool:
         """
         Certificate callback. Override with your own function to determine
         whether to accept the server's certificate.
@@ -181,10 +196,12 @@ class RemoteCallbacks(Payload):
 
         raise Passthrough
 
-    def transfer_progress(self, stats):
+    def transfer_progress(self, stats: 'TransferProgress') -> None:
         """
-        Transfer progress callback. Override with your own function to report
-        transfer progress.
+        During the download of new data, this will be regularly called with
+        the indexer's progress.
+
+        Override with your own function to report transfer progress.
 
         Parameters:
 
@@ -192,7 +209,20 @@ class RemoteCallbacks(Payload):
             The progress up to now.
         """
 
-    def update_tips(self, refname, old, new):
+    def push_transfer_progress(
+        self, objects_pushed: int, total_objects: int, bytes_pushed: int
+    ) -> None:
+        """
+        During the upload portion of a push, this will be regularly called
+        with progress information.
+
+        Be aware that this is called inline with pack building operations,
+        so performance may be affected.
+
+        Override with your own function to report push transfer progress.
+        """
+
+    def update_tips(self, refname: str, old: Oid, new: Oid) -> None:
         """
         Update tips callback. Override with your own function to report
         reference updates.
@@ -209,7 +239,7 @@ class RemoteCallbacks(Payload):
             The reference's new value.
         """
 
-    def push_update_reference(self, refname, message):
+    def push_update_reference(self, refname: str, message: str) -> None:
         """
         Push update reference callback. Override with your own function to
         report the remote's acceptance or rejection of reference updates.
@@ -229,7 +259,7 @@ class CheckoutCallbacks(Payload):
     in your class, which you can then pass to checkout operations.
     """
 
-    def __init__(self):
+    def __init__(self) -> None:
         super().__init__()
 
     def checkout_notify_flags(self) -> CheckoutNotify:
@@ -260,7 +290,7 @@ class CheckoutCallbacks(Payload):
         baseline: Optional[DiffFile],
         target: Optional[DiffFile],
         workdir: Optional[DiffFile],
-    ):
+    ) -> None:
         """
         Checkout will invoke an optional notification callback for
         certain cases - you pick which ones via `checkout_notify_flags`.
@@ -275,7 +305,9 @@ class CheckoutCallbacks(Payload):
         """
         pass
 
-    def checkout_progress(self, path: str, completed_steps: int, total_steps: int):
+    def checkout_progress(
+        self, path: str, completed_steps: int, total_steps: int
+    ) -> None:
         """
         Optional callback to notify the consumer of checkout progress.
         """
@@ -289,7 +321,7 @@ class StashApplyCallbacks(CheckoutCallba
     in your class, which you can then pass to stash apply or pop operations.
     """
 
-    def stash_apply_progress(self, progress: StashApplyProgress):
+    def stash_apply_progress(self, progress: StashApplyProgress) -> None:
         """
         Stash application progress notification function.
 
@@ -356,6 +388,29 @@ def git_fetch_options(payload, opts=None
 
 
 @contextmanager
+def git_proxy_options(
+    payload: object,
+    opts: Optional['ProxyOpts'] = None,
+    proxy: None | bool | str = None,
+) -> Generator['ProxyOpts', None, None]:
+    if opts is None:
+        opts = ffi.new('git_proxy_options *')
+        C.git_proxy_options_init(opts, C.GIT_PROXY_OPTIONS_VERSION)
+    if proxy is None:
+        opts.type = C.GIT_PROXY_NONE
+    elif proxy is True:
+        opts.type = C.GIT_PROXY_AUTO
+    elif type(proxy) is str:
+        opts.type = C.GIT_PROXY_SPECIFIED
+        # Keep url in memory, otherwise memory is freed and bad things happen
+        payload.__proxy_url = ffi.new('char[]', to_bytes(proxy))  # type: ignore[attr-defined, no-untyped-call]
+        opts.url = payload.__proxy_url  # type: ignore[attr-defined]
+    else:
+        raise TypeError('Proxy must be None, True, or a string')
+    yield opts
+
+
+@contextmanager
 def git_push_options(payload, opts=None):
     if payload is None:
         payload = RemoteCallbacks()
@@ -370,6 +425,13 @@ def git_push_options(payload, opts=None)
     opts.callbacks.credentials = C._credentials_cb
     opts.callbacks.certificate_check = C._certificate_check_cb
     opts.callbacks.push_update_reference = C._push_update_reference_cb
+    # Per libgit2 sources, push_transfer_progress may incur a performance hit.
+    # So, set it only if the user has overridden the no-op stub.
+    if (
+        type(payload).push_transfer_progress
+        is not RemoteCallbacks.push_transfer_progress
+    ):
+        opts.callbacks.push_transfer_progress = C._push_transfer_progress_cb
     # Payload
     handle = ffi.new_handle(payload)
     opts.callbacks.payload = handle
@@ -405,10 +467,10 @@ def git_remote_callbacks(payload):
 #
 # C callbacks
 #
-# These functions are called by libgit2. They cannot raise execptions, since
+# These functions are called by libgit2. They cannot raise exceptions, since
 # they return to libgit2, they can only send back error codes.
 #
-# They cannot be overriden, but sometimes the only thing these functions do is
+# They cannot be overridden, but sometimes the only thing these functions do is
 # to proxy the call to a user defined function. If user defined functions
 # raises an exception, the callback must store it somewhere and return
 # GIT_EUSER to libgit2, then the outer Python code will be able to reraise the
@@ -554,6 +616,16 @@ def _transfer_progress_cb(stats_ptr, dat
     return 0
 
 
+@libgit2_callback
+def _push_transfer_progress_cb(current, total, bytes_pushed, payload):
+    push_transfer_progress = getattr(payload, 'push_transfer_progress', None)
+    if not push_transfer_progress:
+        return 0
+
+    push_transfer_progress(current, total, bytes_pushed)
+    return 0
+
+
 @libgit2_callback
 def _update_tips_cb(refname, a, b, data):
     update_tips = getattr(data, 'update_tips', None)
diff -pruN 1.17.0-2/pygit2/config.py 1.18.1-1/pygit2/config.py
--- 1.17.0-2/pygit2/config.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/pygit2/config.py	2025-07-26 10:03:19.000000000 +0000
@@ -26,7 +26,7 @@
 try:
     from functools import cached_property
 except ImportError:
-    from cached_property import cached_property
+    from cached_property import cached_property  # type: ignore
 
 # Import from pygit2
 from .errors import check_error
@@ -304,7 +304,7 @@ class Config:
 
 
 class ConfigEntry:
-    """An entry in a configuation object."""
+    """An entry in a configuration object."""
 
     @classmethod
     def _from_c(cls, ptr, iterator=None):
@@ -321,7 +321,7 @@ class ConfigEntry:
         # git_config_iterator_free when we've deleted all ConfigEntry objects.
         # But it's not, to reproduce the error comment the lines below and run
         # the script in https://github.com/libgit2/pygit2/issues/970
-        # So instead we load the Python object immmediately. Ideally we should
+        # So instead we load the Python object immediately. Ideally we should
         # investigate libgit2 source code.
         if iterator is not None:
             entry.raw_name = entry.raw_name
diff -pruN 1.17.0-2/pygit2/credentials.py 1.18.1-1/pygit2/credentials.py
--- 1.17.0-2/pygit2/credentials.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/pygit2/credentials.py	2025-07-26 10:03:19.000000000 +0000
@@ -23,11 +23,17 @@
 # the Free Software Foundation, 51 Franklin Street, Fifth Floor,
 # Boston, MA 02110-1301, USA.
 
-from .ffi import C
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
 
 from .enums import CredentialType
 
 
+if TYPE_CHECKING:
+    from pathlib import Path
+
+
 class Username:
     """Username credentials
 
@@ -35,7 +41,7 @@ class Username:
     callback and for returning from said callback.
     """
 
-    def __init__(self, username):
+    def __init__(self, username: str):
         self._username = username
 
     @property
@@ -43,10 +49,12 @@ class Username:
         return CredentialType.USERNAME
 
     @property
-    def credential_tuple(self):
+    def credential_tuple(self) -> tuple[str]:
         return (self._username,)
 
-    def __call__(self, _url, _username, _allowed):
+    def __call__(
+        self, _url: str, _username: str | None, _allowed: CredentialType
+    ) -> Username:
         return self
 
 
@@ -57,7 +65,7 @@ class UserPass:
     callback and for returning from said callback.
     """
 
-    def __init__(self, username, password):
+    def __init__(self, username: str, password: str):
         self._username = username
         self._password = password
 
@@ -66,10 +74,12 @@ class UserPass:
         return CredentialType.USERPASS_PLAINTEXT
 
     @property
-    def credential_tuple(self):
+    def credential_tuple(self) -> tuple[str, str]:
         return (self._username, self._password)
 
-    def __call__(self, _url, _username, _allowed):
+    def __call__(
+        self, _url: str, _username: str | None, _allowed: CredentialType
+    ) -> UserPass:
         return self
 
 
@@ -96,7 +106,13 @@ class Keypair:
         no passphrase is required.
     """
 
-    def __init__(self, username, pubkey, privkey, passphrase):
+    def __init__(
+        self,
+        username: str,
+        pubkey: str | Path | None,
+        privkey: str | Path | None,
+        passphrase: str | None,
+    ):
         self._username = username
         self._pubkey = pubkey
         self._privkey = privkey
@@ -107,15 +123,19 @@ class Keypair:
         return CredentialType.SSH_KEY
 
     @property
-    def credential_tuple(self):
+    def credential_tuple(
+        self,
+    ) -> tuple[str, str | Path | None, str | Path | None, str | None]:
         return (self._username, self._pubkey, self._privkey, self._passphrase)
 
-    def __call__(self, _url, _username, _allowed):
+    def __call__(
+        self, _url: str, _username: str | None, _allowed: CredentialType
+    ) -> Keypair:
         return self
 
 
 class KeypairFromAgent(Keypair):
-    def __init__(self, username):
+    def __init__(self, username: str):
         super().__init__(username, None, None, None)
 
 
diff -pruN 1.17.0-2/pygit2/decl/callbacks.h 1.18.1-1/pygit2/decl/callbacks.h
--- 1.17.0-2/pygit2/decl/callbacks.h	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/pygit2/decl/callbacks.h	2025-07-26 10:03:19.000000000 +0000
@@ -38,6 +38,12 @@ extern "Python" int _transfer_progress_c
     const git_indexer_progress *stats,
     void *payload);
 
+extern "Python" int _push_transfer_progress_cb(
+    unsigned int objects_pushed,
+    unsigned int total_objects,
+    size_t bytes_pushed,
+    void *payload);
+
 extern "Python" int _update_tips_cb(
 	const char *refname,
 	const git_oid *a,
diff -pruN 1.17.0-2/pygit2/decl/commit.h 1.18.1-1/pygit2/decl/commit.h
--- 1.17.0-2/pygit2/decl/commit.h	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/pygit2/decl/commit.h	2025-07-26 10:03:19.000000000 +0000
@@ -13,4 +13,9 @@ int git_annotated_commit_lookup(
 	git_repository *repo,
 	const git_oid *id);
 
+int git_annotated_commit_from_ref(
+	git_annotated_commit **out,
+	git_repository *repo,
+	const struct git_reference *ref);
+
 void git_annotated_commit_free(git_annotated_commit *commit);
diff -pruN 1.17.0-2/pygit2/decl/index.h 1.18.1-1/pygit2/decl/index.h
--- 1.17.0-2/pygit2/decl/index.h	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/pygit2/decl/index.h	2025-07-26 10:03:19.000000000 +0000
@@ -34,6 +34,7 @@ int git_index_find(size_t *at_pos, git_i
 int git_index_add_bypath(git_index *index, const char *path);
 int git_index_add(git_index *index, const git_index_entry *source_entry);
 int git_index_remove(git_index *index, const char *path, int stage);
+int git_index_remove_directory(git_index *index, const char *path, int stage);
 int git_index_read_tree(git_index *index, const git_tree *tree);
 int git_index_clear(git_index *index);
 int git_index_write_tree(git_oid *out, git_index *index);
@@ -59,6 +60,11 @@ void git_index_conflict_iterator_free(
 int git_index_conflict_iterator_new(
 	git_index_conflict_iterator **iterator_out,
 	git_index *index);
+int git_index_conflict_add(
+    git_index *index,
+    const git_index_entry *ancestor_entry,
+    const git_index_entry *our_entry,
+    const git_index_entry *their_entry);
 int git_index_conflict_get(
 	const git_index_entry **ancestor_out,
 	const git_index_entry **our_out,
diff -pruN 1.17.0-2/pygit2/decl/repository.h 1.18.1-1/pygit2/decl/repository.h
--- 1.17.0-2/pygit2/decl/repository.h	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/pygit2/decl/repository.h	2025-07-26 10:03:19.000000000 +0000
@@ -81,7 +81,7 @@ int git_repository_set_head(
 
 int git_repository_set_head_detached(
 	git_repository* repo,
-	const git_oid* commitish);
+	const git_oid* committish);
 
 int git_repository_hashfile(git_oid *out, git_repository *repo, const char *path, git_object_t type, const char *as_path);
 int git_repository_ident(const char **name, const char **email, const git_repository *repo);
diff -pruN 1.17.0-2/pygit2/errors.py 1.18.1-1/pygit2/errors.py
--- 1.17.0-2/pygit2/errors.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/pygit2/errors.py	2025-07-26 10:03:19.000000000 +0000
@@ -42,7 +42,7 @@ def check_error(err, io=False):
     # Error message
     giterr = C.git_error_last()
     if giterr != ffi.NULL:
-        message = ffi.string(giterr.message).decode('utf8')
+        message = ffi.string(giterr.message).decode('utf8', errors='surrogateescape')
     else:
         message = f'err {err} (no message provided)'
 
diff -pruN 1.17.0-2/pygit2/ffi.py 1.18.1-1/pygit2/ffi.py
--- 1.17.0-2/pygit2/ffi.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/pygit2/ffi.py	2025-07-26 10:03:19.000000000 +0000
@@ -24,4 +24,4 @@
 # Boston, MA 02110-1301, USA.
 
 # Import from pygit2
-from ._libgit2 import ffi, lib as C
+from ._libgit2 import ffi, lib as C  # type: ignore # noqa: F401
diff -pruN 1.17.0-2/pygit2/index.py 1.18.1-1/pygit2/index.py
--- 1.17.0-2/pygit2/index.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/pygit2/index.py	2025-07-26 10:03:19.000000000 +0000
@@ -23,8 +23,9 @@
 # the Free Software Foundation, 51 Franklin Street, Fifth Floor,
 # Boston, MA 02110-1301, USA.
 
+import typing
 import warnings
-import weakref
+from dataclasses import dataclass
 
 # Import from pygit2
 from ._pygit2 import Oid, Tree, Diff
@@ -177,6 +178,11 @@ class Index:
         err = C.git_index_remove(self._index, to_bytes(path), level)
         check_error(err, io=True)
 
+    def remove_directory(self, path, level=0):
+        """Remove a directory from the Index."""
+        err = C.git_index_remove_directory(self._index, to_bytes(path), level)
+        check_error(err, io=True)
+
     def remove_all(self, pathspecs):
         """Remove all index entries matching pathspecs."""
         with StrArray(pathspecs) as arr:
@@ -216,6 +222,41 @@ class Index:
 
         check_error(err, io=True)
 
+    def add_conflict(self, ancestor, ours, theirs):
+        """
+        Add or update index entries to represent a conflict. Any staged entries that
+        exist at the given paths will be removed.
+
+        Parameters:
+
+        ancestor
+            ancestor of the conflict
+        ours
+            ours side of the conflict
+        theirs
+            their side of the conflict
+        """
+
+        if ancestor and not isinstance(ancestor, IndexEntry):
+            raise TypeError('ancestor has to be an instance of IndexEntry or None')
+        if ours and not isinstance(ours, IndexEntry):
+            raise TypeError('ours has to be an instance of IndexEntry or None')
+        if theirs and not isinstance(theirs, IndexEntry):
+            raise TypeError('theirs has to be an instance of IndexEntry or None')
+
+        centry_ancestor = centry_ours = centry_theirs = ffi.NULL
+        if ancestor is not None:
+            centry_ancestor, _ = ancestor._to_c()
+        if ours is not None:
+            centry_ours, _ = ours._to_c()
+        if theirs is not None:
+            centry_theirs, _ = theirs._to_c()
+        err = C.git_index_conflict_add(
+            self._index, centry_ancestor, centry_ours, centry_theirs
+        )
+
+        check_error(err, io=True)
+
     def diff_to_workdir(
         self,
         flags: DiffOption = DiffOption.NORMAL,
@@ -311,7 +352,6 @@ class Index:
     #
     # Conflicts
     #
-    _conflicts = None
 
     @property
     def conflicts(self):
@@ -333,15 +373,49 @@ class Index:
         the particular conflict.
         """
         if not C.git_index_has_conflicts(self._index):
-            self._conflicts = None
             return None
 
-        if self._conflicts is None or self._conflicts() is None:
-            conflicts = ConflictCollection(self)
-            self._conflicts = weakref.ref(conflicts)
-            return conflicts
+        return ConflictCollection(self)
+
+
+@dataclass
+class MergeFileResult:
+    automergeable: bool
+    'True if the output was automerged, false if the output contains conflict markers'
+
+    path: typing.Union[str, None]
+    'The path that the resultant merge file should use, or None if a filename conflict would occur'
+
+    mode: FileMode
+    'The mode that the resultant merge file should use'
+
+    contents: str
+    'Contents of the file, which might include conflict markers'
+
+    def __repr__(self):
+        t = type(self)
+        contents = (
+            self.contents if len(self.contents) <= 20 else f'{self.contents[:20]}...'
+        )
+        return (
+            f'<{t.__module__}.{t.__qualname__} "'
+            f'automergeable={self.automergeable} "'
+            f'path={self.path} '
+            f'mode={self.mode} '
+            f'contents={contents}>'
+        )
+
+    @classmethod
+    def _from_c(cls, centry):
+        if centry == ffi.NULL:
+            return None
+
+        automergeable = centry.automergeable != 0
+        path = to_str(ffi.string(centry.path)) if centry.path else None
+        mode = FileMode(centry.mode)
+        contents = ffi.string(centry.ptr, centry.len).decode('utf-8')
 
-        return self._conflicts()
+        return MergeFileResult(automergeable, path, mode, contents)
 
 
 class IndexEntry:
diff -pruN 1.17.0-2/pygit2/refspec.py 1.18.1-1/pygit2/refspec.py
--- 1.17.0-2/pygit2/refspec.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/pygit2/refspec.py	2025-07-26 10:03:19.000000000 +0000
@@ -43,7 +43,7 @@ class Refspec:
 
     @property
     def dst(self):
-        """Destinaton or rhs of the refspec"""
+        """Destination or rhs of the refspec"""
         return ffi.string(C.git_refspec_dst(self._refspec)).decode('utf-8')
 
     @property
diff -pruN 1.17.0-2/pygit2/remotes.py 1.18.1-1/pygit2/remotes.py
--- 1.17.0-2/pygit2/remotes.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/pygit2/remotes.py	2025-07-26 10:03:19.000000000 +0000
@@ -24,11 +24,16 @@
 # Boston, MA 02110-1301, USA.
 
 from __future__ import annotations
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Any
 
 # Import from pygit2
 from ._pygit2 import Oid
-from .callbacks import git_fetch_options, git_push_options, git_remote_callbacks
+from .callbacks import (
+    git_fetch_options,
+    git_push_options,
+    git_proxy_options,
+    git_remote_callbacks,
+)
 from .enums import FetchPrune
 from .errors import check_error
 from .ffi import ffi, C
@@ -44,7 +49,15 @@ if TYPE_CHECKING:
 class TransferProgress:
     """Progress downloading and indexing data during a fetch."""
 
-    def __init__(self, tp):
+    total_objects: int
+    indexed_objects: int
+    received_objects: int
+    local_objects: int
+    total_deltas: int
+    indexed_deltas: int
+    received_bytes: int
+
+    def __init__(self, tp: Any) -> None:
         self.total_objects = tp.total_objects
         """Total number of objects to download"""
 
@@ -107,14 +120,16 @@ class Remote:
             * `True` to enable automatic proxy detection
             * an url to a proxy (`http://proxy.example.org:3128/`)
         """
-        proxy_opts = ffi.new('git_proxy_options *')
-        C.git_proxy_options_init(proxy_opts, C.GIT_PROXY_OPTIONS_VERSION)
-        self.__set_proxy(proxy_opts, proxy)
-        with git_remote_callbacks(callbacks) as payload:
-            err = C.git_remote_connect(
-                self._remote, direction, payload.remote_callbacks, proxy_opts, ffi.NULL
-            )
-            payload.check_error(err)
+        with git_proxy_options(self, proxy=proxy) as proxy_opts:
+            with git_remote_callbacks(callbacks) as payload:
+                err = C.git_remote_connect(
+                    self._remote,
+                    direction,
+                    payload.remote_callbacks,
+                    proxy_opts,
+                    ffi.NULL,
+                )
+                payload.check_error(err)
 
     def fetch(
         self,
@@ -154,10 +169,12 @@ class Remote:
             opts = payload.fetch_options
             opts.prune = prune
             opts.depth = depth
-            self.__set_proxy(opts.proxy_opts, proxy)
-            with StrArray(refspecs) as arr:
-                err = C.git_remote_fetch(self._remote, arr.ptr, opts, to_bytes(message))
-                payload.check_error(err)
+            with git_proxy_options(self, payload.fetch_options.proxy_opts, proxy):
+                with StrArray(refspecs) as arr:
+                    err = C.git_remote_fetch(
+                        self._remote, arr.ptr, opts, to_bytes(message)
+                    )
+                    payload.check_error(err)
 
         return TransferProgress(C.git_remote_stats(self._remote))
 
@@ -237,7 +254,7 @@ class Remote:
         check_error(err)
         return strarray_to_strings(specs)
 
-    def push(self, specs, callbacks=None, proxy=None, push_options=None):
+    def push(self, specs, callbacks=None, proxy=None, push_options=None, threads=1):
         """
         Push the given refspec to the remote. Raises ``GitError`` on protocol
         error or unpack failure.
@@ -246,7 +263,7 @@ class Remote:
         function will return successfully. Thus it is strongly recommended to
         install a callback, that implements
         :py:meth:`RemoteCallbacks.push_update_reference` and check the passed
-        parameters for successfull operations.
+        parameters for successful operations.
 
         Parameters:
 
@@ -263,27 +280,24 @@ class Remote:
         push_options : [str]
             Push options to send to the server, which passes them to the
             pre-receive as well as the post-receive hook.
+
+        threads : int
+            If the transport being used to push to the remote requires the
+            creation of a pack file, this controls the number of worker threads
+            used by the packbuilder when creating that pack file to be sent to
+            the remote.
+
+            If set to 0, the packbuilder will auto-detect the number of threads
+            to create. The default value is 1.
         """
         with git_push_options(callbacks) as payload:
             opts = payload.push_options
-            self.__set_proxy(opts.proxy_opts, proxy)
-            with StrArray(specs) as refspecs, StrArray(push_options) as pushopts:
-                pushopts.assign_to(opts.remote_push_options)
-                err = C.git_remote_push(self._remote, refspecs.ptr, opts)
-                payload.check_error(err)
-
-    def __set_proxy(self, proxy_opts, proxy):
-        if proxy is None:
-            proxy_opts.type = C.GIT_PROXY_NONE
-        elif proxy is True:
-            proxy_opts.type = C.GIT_PROXY_AUTO
-        elif type(proxy) is str:
-            proxy_opts.type = C.GIT_PROXY_SPECIFIED
-            # Keep url in memory, otherwise memory is freed and bad things happen
-            self.__url = ffi.new('char[]', to_bytes(proxy))
-            proxy_opts.url = self.__url
-        else:
-            raise TypeError('Proxy must be None, True, or a string')
+            opts.pb_parallelism = threads
+            with git_proxy_options(self, payload.push_options.proxy_opts, proxy):
+                with StrArray(specs) as refspecs, StrArray(push_options) as pushopts:
+                    pushopts.assign_to(opts.remote_push_options)
+                    err = C.git_remote_push(self._remote, refspecs.ptr, opts)
+                    payload.check_error(err)
 
 
 class RemoteCollection:
@@ -335,7 +349,7 @@ class RemoteCollection:
         for name in self._ffi_names():
             yield maybe_string(name)
 
-    def create(self, name, url, fetch=None):
+    def create(self, name, url, fetch=None) -> Remote:
         """Create a new remote with the given name and url. Returns a <Remote>
         object.
 
@@ -376,7 +390,7 @@ class RemoteCollection:
         the standard format and thus could not be remapped.
         """
 
-        if not new_name:
+        if not name:
             raise ValueError('Current remote name must be a non-empty string')
 
         if not new_name:
diff -pruN 1.17.0-2/pygit2/repository.py 1.18.1-1/pygit2/repository.py
--- 1.17.0-2/pygit2/repository.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/pygit2/repository.py	2025-07-26 10:03:19.000000000 +0000
@@ -22,17 +22,18 @@
 # along with this program; see the file COPYING.  If not, write to
 # the Free Software Foundation, 51 Franklin Street, Fifth Floor,
 # Boston, MA 02110-1301, USA.
-
+import warnings
 from io import BytesIO
 from os import PathLike
 from string import hexdigits
 from time import time
 import tarfile
 import typing
+from typing import Optional
 
 # Import from pygit2
 from ._pygit2 import Repository as _Repository, init_file_backend
-from ._pygit2 import Oid, GIT_OID_HEXSZ, GIT_OID_MINPREFIXLEN
+from ._pygit2 import Oid, GIT_OID_HEXSZ, GIT_OID_MINPREFIXLEN, Object
 from ._pygit2 import Reference, Tree, Commit, Blob, Signature
 from ._pygit2 import InvalidSpecError
 
@@ -43,7 +44,6 @@ from .config import Config
 from .enums import (
     AttrCheck,
     BlameFlag,
-    BranchType,
     CheckoutStrategy,
     DescribeStrategy,
     DiffOption,
@@ -57,7 +57,7 @@ from .enums import (
 )
 from .errors import check_error
 from .ffi import ffi, C
-from .index import Index, IndexEntry
+from .index import Index, IndexEntry, MergeFileResult
 from .packbuilder import PackBuilder
 from .references import References
 from .remotes import RemoteCollection
@@ -188,20 +188,20 @@ class BaseRepository(_Repository):
     #
     # Mapping interface
     #
-    def get(self, key, default=None):
+    def get(self, key: str, default: Optional[Commit] = None) -> Object:
         value = self.git_object_lookup_prefix(key)
         return value if (value is not None) else default
 
-    def __getitem__(self, key):
+    def __getitem__(self, key: str | Oid) -> Object:
         value = self.git_object_lookup_prefix(key)
         if value is None:
             raise KeyError(key)
         return value
 
-    def __contains__(self, key):
+    def __contains__(self, key: str | Oid) -> bool:
         return self.git_object_lookup_prefix(key) is not None
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return f'pygit2.Repository({repr(self.path)})'
 
     #
@@ -522,27 +522,30 @@ class BaseRepository(_Repository):
         a = self.__whatever_to_tree_or_blob(a)
         b = self.__whatever_to_tree_or_blob(b)
 
-        opt_keys = ['flags', 'context_lines', 'interhunk_lines']
-        opt_values = [int(flags), context_lines, interhunk_lines]
+        options = {
+            'flags': int(flags),
+            'context_lines': context_lines,
+            'interhunk_lines': interhunk_lines,
+        }
 
         # Case 1: Diff tree to tree
         if isinstance(a, Tree) and isinstance(b, Tree):
-            return a.diff_to_tree(b, **dict(zip(opt_keys, opt_values)))
+            return a.diff_to_tree(b, **options)  # type: ignore[arg-type]
 
         # Case 2: Index to workdir
         elif a is None and b is None:
-            return self.index.diff_to_workdir(*opt_values)
+            return self.index.diff_to_workdir(**options)  # type: ignore[arg-type]
 
         # Case 3: Diff tree to index or workdir
         elif isinstance(a, Tree) and b is None:
             if cached:
-                return a.diff_to_index(self.index, *opt_values)
+                return a.diff_to_index(self.index, **options)  # type: ignore[arg-type]
             else:
-                return a.diff_to_workdir(*opt_values)
+                return a.diff_to_workdir(**options)  # type: ignore[arg-type]
 
         # Case 4: Diff blob to blob
         if isinstance(a, Blob) and isinstance(b, Blob):
-            return a.diff(b)
+            return a.diff(b, **options)  # type: ignore[arg-type]
 
         raise ValueError('Only blobs and treeish can be diffed')
 
@@ -557,7 +560,7 @@ class BaseRepository(_Repository):
             return RepositoryState(cstate)
         except ValueError:
             # Some value not in the IntEnum - newer libgit2 version?
-            return cstate
+            return cstate  # type: ignore[return-value]
 
     def state_cleanup(self):
         """Remove all the metadata associated with an ongoing command like
@@ -680,9 +683,13 @@ class BaseRepository(_Repository):
         ancestor: typing.Union[None, IndexEntry],
         ours: typing.Union[None, IndexEntry],
         theirs: typing.Union[None, IndexEntry],
-    ) -> str:
-        """Merge files from index. Return a string with the merge result
-        containing possible conflicts.
+        use_deprecated: bool = True,
+    ) -> typing.Union[str, typing.Union[MergeFileResult, None]]:
+        """Merge files from index.
+
+        Returns: A string with the content of the file containing
+        possible conflicts if use_deprecated==True.
+        If use_deprecated==False then it returns an instance of MergeFileResult.
 
         ancestor
             The index entry which will be used as a common
@@ -691,6 +698,10 @@ class BaseRepository(_Repository):
             The index entry to take as "ours" or base.
         theirs
             The index entry which will be merged into "ours"
+        use_deprecated
+            This controls what will be returned. If use_deprecated==True (default),
+            a string with the contents of the file will be returned.
+            An instance of MergeFileResult will be returned otherwise.
         """
         cmergeresult = ffi.new('git_merge_file_result *')
 
@@ -707,10 +718,19 @@ class BaseRepository(_Repository):
         )
         check_error(err)
 
-        ret = ffi.string(cmergeresult.ptr, cmergeresult.len).decode('utf-8')
+        mergeFileResult = MergeFileResult._from_c(cmergeresult)
         C.git_merge_file_result_free(cmergeresult)
 
-        return ret
+        if use_deprecated:
+            warnings.warn(
+                'Getting an str from Repository.merge_file_from_index is deprecated. '
+                'The method will later return an instance of MergeFileResult by default, instead. '
+                'Check parameter use_deprecated.',
+                DeprecationWarning,
+            )
+            return mergeFileResult.contents if mergeFileResult else ''
+
+        return mergeFileResult
 
     def merge_commits(
         self,
@@ -751,9 +771,15 @@ class BaseRepository(_Repository):
         cindex = ffi.new('git_index **')
 
         if isinstance(ours, (str, Oid)):
-            ours = self[ours]
+            ours_object = self[ours]
+            if not isinstance(ours_object, Commit):
+                raise TypeError(f'expected Commit, got {type(ours_object)}')
+            ours = ours_object
         if isinstance(theirs, (str, Oid)):
-            theirs = self[theirs]
+            theirs_object = self[theirs]
+            if not isinstance(theirs_object, Commit):
+                raise TypeError(f'expected Commit, got {type(theirs_object)}')
+            theirs = theirs_object
 
         ours = ours.peel(Commit)
         theirs = theirs.peel(Commit)
@@ -808,16 +834,9 @@ class BaseRepository(_Repository):
         theirs_ptr = ffi.new('git_tree **')
         cindex = ffi.new('git_index **')
 
-        if isinstance(ancestor, (str, Oid)):
-            ancestor = self[ancestor]
-        if isinstance(ours, (str, Oid)):
-            ours = self[ours]
-        if isinstance(theirs, (str, Oid)):
-            theirs = self[theirs]
-
-        ancestor = ancestor.peel(Tree)
-        ours = ours.peel(Tree)
-        theirs = theirs.peel(Tree)
+        ancestor = self.__ensure_tree(ancestor)
+        ours = self.__ensure_tree(ours)
+        theirs = self.__ensure_tree(theirs)
 
         opts = self._merge_options(favor, flags, file_flags)
 
@@ -834,23 +853,28 @@ class BaseRepository(_Repository):
 
     def merge(
         self,
-        id: typing.Union[Oid, str],
+        source: typing.Union[Reference, Commit, Oid, str],
         favor=MergeFavor.NORMAL,
         flags=MergeFlag.FIND_RENAMES,
         file_flags=MergeFileFlag.DEFAULT,
     ):
         """
-        Merges the given id into HEAD.
+        Merges the given Reference or Commit into HEAD.
 
-        Merges the given commit(s) into HEAD, writing the results into the working directory.
+        Merges the given commit into HEAD, writing the results into the working directory.
         Any changes are staged for commit and any conflicts are written to the index.
         Callers should inspect the repository's index after this completes,
         resolve any conflicts and prepare a commit.
 
         Parameters:
 
-        id
-            The id to merge into HEAD
+        source
+            The Reference, Commit, or commit Oid to merge into HEAD.
+            It is preferable to pass in a Reference, because this enriches the
+            merge with additional information (for example, Repository.message will
+            specify the name of the branch being merged).
+            Previous versions of pygit2 allowed passing in a partial commit
+            hash as a string; this is deprecated.
 
         favor
             An enums.MergeFavor constant specifying how to deal with file-level conflicts.
@@ -862,12 +886,35 @@ class BaseRepository(_Repository):
         file_flags
             A combination of enums.MergeFileFlag constants.
         """
-        if not isinstance(id, (str, Oid)):
-            raise TypeError(f'expected oid (string or <Oid>) got {type(id)}')
 
-        id = self[id].id
-        c_id = ffi.new('git_oid *')
-        ffi.buffer(c_id)[:] = id.raw[:]
+        if isinstance(source, Reference):
+            # Annotated commit from ref
+            cptr = ffi.new('struct git_reference **')
+            ffi.buffer(cptr)[:] = source._pointer[:]  # type: ignore[attr-defined]
+            commit_ptr = ffi.new('git_annotated_commit **')
+            err = C.git_annotated_commit_from_ref(commit_ptr, self._repo, cptr[0])
+            check_error(err)
+        else:
+            # Annotated commit from commit id
+            if isinstance(source, str):
+                # For backwards compatibility, parse a string as a partial commit hash
+                warnings.warn(
+                    'Passing str to Repository.merge is deprecated. '
+                    'Pass Commit, Oid, or a Reference (such as a Branch) instead.',
+                    DeprecationWarning,
+                )
+                oid = self[source].peel(Commit).id
+            elif isinstance(source, Commit):
+                oid = source.id
+            elif isinstance(source, Oid):
+                oid = source
+            else:
+                raise TypeError('expected Reference, Commit, or Oid')
+            c_id = ffi.new('git_oid *')
+            ffi.buffer(c_id)[:] = oid.raw[:]
+            commit_ptr = ffi.new('git_annotated_commit **')
+            err = C.git_annotated_commit_lookup(commit_ptr, self._repo, c_id)
+            check_error(err)
 
         merge_opts = self._merge_options(favor, flags, file_flags)
 
@@ -877,10 +924,6 @@ class BaseRepository(_Repository):
             CheckoutStrategy.SAFE | CheckoutStrategy.RECREATE_MISSING
         )
 
-        commit_ptr = ffi.new('git_annotated_commit **')
-        err = C.git_annotated_commit_lookup(commit_ptr, self._repo, c_id)
-        check_error(err)
-
         err = C.git_merge(self._repo, commit_ptr, 1, merge_opts, checkout_opts)
         C.git_annotated_commit_free(commit_ptr[0])
         check_error(err)
@@ -987,7 +1030,7 @@ class BaseRepository(_Repository):
 
         always_use_long_format : bool
             Always output the long format (the nearest tag, the number of
-            commits, and the abbrevated commit name) even when the committish
+            commits, and the abbreviated commit name) even when the committish
             matches a tag.
 
         dirty_suffix : str
@@ -1572,6 +1615,11 @@ class BaseRepository(_Repository):
 
         return Oid(raw=bytes(ffi.buffer(coid)[:]))
 
+    def __ensure_tree(self, maybe_tree: str | Oid | Tree) -> Tree:
+        if isinstance(maybe_tree, Tree):
+            return maybe_tree
+        return self[maybe_tree].peel(Tree)
+
 
 class Repository(BaseRepository):
     def __init__(
diff -pruN 1.17.0-2/pygit2/submodules.py 1.18.1-1/pygit2/submodules.py
--- 1.17.0-2/pygit2/submodules.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/pygit2/submodules.py	2025-07-26 10:03:19.000000000 +0000
@@ -39,6 +39,9 @@ if TYPE_CHECKING:
 
 
 class Submodule:
+    _repo: BaseRepository
+    _subm: object
+
     @classmethod
     def _from_c(cls, repo: BaseRepository, cptr):
         subm = cls.__new__(cls)
@@ -74,7 +77,10 @@ class Submodule:
         check_error(err)
 
     def update(
-        self, init: bool = False, callbacks: RemoteCallbacks = None, depth: int = 0
+        self,
+        init: bool = False,
+        callbacks: Optional[RemoteCallbacks] = None,
+        depth: int = 0,
     ):
         """
         Update a submodule. This will clone a missing submodule and checkout
diff -pruN 1.17.0-2/pygit2/utils.py 1.18.1-1/pygit2/utils.py
--- 1.17.0-2/pygit2/utils.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/pygit2/utils.py	2025-07-26 10:03:19.000000000 +0000
@@ -34,7 +34,7 @@ def maybe_string(ptr):
     if not ptr:
         return None
 
-    return ffi.string(ptr).decode('utf8')
+    return ffi.string(ptr).decode('utf8', errors='surrogateescape')
 
 
 def to_bytes(s, encoding='utf-8', errors='strict'):
@@ -113,18 +113,18 @@ class StrArray:
     contents of 'struct' only remain valid within the StrArray context.
     """
 
-    def __init__(self, l):
+    def __init__(self, lst):
         # Allow passing in None as lg2 typically considers them the same as empty
-        if l is None:
+        if lst is None:
             self.__array = ffi.NULL
             return
 
-        if not isinstance(l, (list, tuple)):
+        if not isinstance(lst, (list, tuple)):
             raise TypeError('Value must be a list')
 
-        strings = [None] * len(l)
-        for i in range(len(l)):
-            li = l[i]
+        strings = [None] * len(lst)
+        for i in range(len(lst)):
+            li = lst[i]
             if not isinstance(li, str) and not hasattr(li, '__fspath__'):
                 raise TypeError('Value must be a string or PathLike object')
 
diff -pruN 1.17.0-2/pyproject.toml 1.18.1-1/pyproject.toml
--- 1.17.0-2/pyproject.toml	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/pyproject.toml	2025-07-26 10:03:19.000000000 +0000
@@ -5,16 +5,15 @@ requires = ["setuptools", "wheel"]
 enable = ["pypy"]
 skip = "*musllinux_aarch64 *musllinux_ppc64le"
 
-archs = ["auto"]
+archs = ["native"]
 build-frontend = "default"
 dependency-versions = "pinned"
-environment = {LIBGIT2_VERSION="1.9.0", LIBSSH2_VERSION="1.11.1", OPENSSL_VERSION="3.2.3", LIBGIT2="/project/ci"}
+environment = {LIBGIT2_VERSION="1.9.1", LIBSSH2_VERSION="1.11.1", OPENSSL_VERSION="3.3.3", LIBGIT2="/project/ci"}
 
 before-all = "sh build.sh"
 
 [tool.cibuildwheel.linux]
 repair-wheel-command = "LD_LIBRARY_PATH=/project/ci/lib64 auditwheel repair -w {dest_dir} {wheel}"
-archs = ["x86_64", "aarch64", "ppc64le"]
 
 [[tool.cibuildwheel.overrides]]
 select = "*-musllinux*"
@@ -22,18 +21,19 @@ repair-wheel-command = "LD_LIBRARY_PATH=
 
 [tool.cibuildwheel.macos]
 archs = ["universal2"]
-environment = {LIBGIT2_VERSION="1.9.0", LIBSSH2_VERSION="1.11.1", OPENSSL_VERSION="3.2.3", LIBGIT2="/Users/runner/work/pygit2/pygit2/ci"}
+environment = {LIBGIT2_VERSION="1.9.1", LIBSSH2_VERSION="1.11.1", OPENSSL_VERSION="3.3.3", LIBGIT2="/Users/runner/work/pygit2/pygit2/ci"}
 repair-wheel-command = "DYLD_LIBRARY_PATH=/Users/runner/work/pygit2/pygit2/ci/lib delocate-wheel --require-archs {delocate_archs} -w {dest_dir} {wheel}"
 
 [tool.ruff]
+extend-exclude = [ ".cache", ".coverage", "build", "site-packages", "venv*"]
 target-version = "py310"  # oldest supported Python version
-fix = true
-extend-exclude = [
-    ".cache",
-    ".coverage",
-    "build",
-    "venv*",
-]
 
 [tool.ruff.format]
 quote-style = "single"
+
+[tool.codespell]
+# Ref: https://github.com/codespell-project/codespell#using-a-config-file
+skip = '.git*'
+check-hidden = true
+# ignore-regex = ''
+ignore-words-list = 'devault,claus'
diff -pruN 1.17.0-2/requirements-test.txt 1.18.1-1/requirements-test.txt
--- 1.17.0-2/requirements-test.txt	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/requirements-test.txt	2025-07-26 10:03:19.000000000 +0000
@@ -1,2 +1,3 @@
 pytest
 pytest-cov
+mypy
diff -pruN 1.17.0-2/src/blob.c 1.18.1-1/src/blob.c
--- 1.17.0-2/src/blob.c	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/src/blob.c	2025-07-26 10:03:19.000000000 +0000
@@ -41,7 +41,7 @@ extern PyObject *GitError;
 extern PyTypeObject BlobType;
 
 PyDoc_STRVAR(Blob_diff__doc__,
-  "diff([blob: Blob, flag: int = GIT_DIFF_NORMAL, old_as_path: str, new_as_path: str]) -> Patch\n"
+  "diff([blob: Blob, flags: int = GIT_DIFF_NORMAL, old_as_path: str, new_as_path: str]) -> Patch\n"
   "\n"
   "Directly generate a :py:class:`pygit2.Patch` from the difference\n"
   "between two blobs.\n"
@@ -53,14 +53,22 @@ PyDoc_STRVAR(Blob_diff__doc__,
   "blob : Blob\n"
   "    The :py:class:`~pygit2.Blob` to diff.\n"
   "\n"
-  "flag\n"
-  "    A GIT_DIFF_* constant.\n"
+  "flags\n"
+  "    A combination of GIT_DIFF_* constant.\n"
   "\n"
   "old_as_path : str\n"
   "    Treat old blob as if it had this filename.\n"
   "\n"
   "new_as_path : str\n"
-  "    Treat new blob as if it had this filename.\n");
+  "    Treat new blob as if it had this filename.\n"
+  "\n"
+  "context_lines: int\n"
+  "    Number of unchanged lines that define the boundary of a hunk\n"
+  "    (and to display before and after).\n"
+  "\n"
+  "interhunk_lines: int\n"
+  "    Maximum number of unchanged lines between hunk boundaries\n"
+  "    before the hunks will be merged into one.\n");
 
 PyObject *
 Blob_diff(Blob *self, PyObject *args, PyObject *kwds)
@@ -70,11 +78,12 @@ Blob_diff(Blob *self, PyObject *args, Py
     char *old_as_path = NULL, *new_as_path = NULL;
     Blob *other = NULL;
     int err;
-    char *keywords[] = {"blob", "flag", "old_as_path", "new_as_path", NULL};
+    char *keywords[] = {"blob", "flags", "old_as_path", "new_as_path", "context_lines", "interhunk_lines", NULL};
 
-    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O!Iss", keywords,
+    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O!IssHH", keywords,
                                      &BlobType, &other, &opts.flags,
-                                     &old_as_path, &new_as_path))
+                                     &old_as_path, &new_as_path,
+                                     &opts.context_lines, &opts.interhunk_lines))
         return NULL;
 
     if (Object__load((Object*)self) == NULL) { return NULL; } // Lazy load
@@ -91,7 +100,7 @@ Blob_diff(Blob *self, PyObject *args, Py
 
 
 PyDoc_STRVAR(Blob_diff_to_buffer__doc__,
-  "diff_to_buffer(buffer: bytes = None, flag: int = GIT_DIFF_NORMAL[, old_as_path: str, buffer_as_path: str]) -> Patch\n"
+  "diff_to_buffer(buffer: bytes = None, flags: int = GIT_DIFF_NORMAL[, old_as_path: str, buffer_as_path: str]) -> Patch\n"
   "\n"
   "Directly generate a :py:class:`~pygit2.Patch` from the difference\n"
   "between a blob and a buffer.\n"
@@ -103,8 +112,8 @@ PyDoc_STRVAR(Blob_diff_to_buffer__doc__,
   "buffer : bytes\n"
   "    Raw data for new side of diff.\n"
   "\n"
-  "flag\n"
-  "    A GIT_DIFF_* constant.\n"
+  "flags\n"
+  "    A combination of GIT_DIFF_* constants.\n"
   "\n"
   "old_as_path : str\n"
   "    Treat old blob as if it had this filename.\n"
@@ -121,8 +130,7 @@ Blob_diff_to_buffer(Blob *self, PyObject
     const char *buffer = NULL;
     Py_ssize_t buffer_len;
     int err;
-    char *keywords[] = {"buffer", "flag", "old_as_path", "buffer_as_path",
-                        NULL};
+    char *keywords[] = {"buffer", "flags", "old_as_path", "buffer_as_path", NULL};
 
     if (!PyArg_ParseTupleAndKeywords(args, kwds, "|z#Iss", keywords,
                                      &buffer, &buffer_len, &opts.flags,
diff -pruN 1.17.0-2/src/odb_backend.c 1.18.1-1/src/odb_backend.c
--- 1.17.0-2/src/odb_backend.c	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/src/odb_backend.c	2025-07-26 10:03:19.000000000 +0000
@@ -103,7 +103,7 @@ pgit_odb_backend_read_prefix(git_oid *oi
     if (result == NULL)
         return git_error_for_exc();
 
-    // Parse output from calback
+    // Parse output from callback
     PyObject *py_oid_out;
     Py_ssize_t type_value;
     const char *bytes;
diff -pruN 1.17.0-2/src/oid.c 1.18.1-1/src/oid.c
--- 1.17.0-2/src/oid.c	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/src/oid.c	2025-07-26 10:03:19.000000000 +0000
@@ -34,6 +34,8 @@
 
 PyTypeObject OidType;
 
+static const git_oid oid_zero = GIT_OID_SHA1_ZERO;
+
 
 PyObject *
 git_oid_to_python(const git_oid *oid)
@@ -255,6 +257,13 @@ Oid__str__(Oid *self)
     return git_oid_to_py_str(&self->oid);
 }
 
+int
+Oid__bool(PyObject *self)
+{
+    git_oid *oid = &((Oid*)self)->oid;
+    return !git_oid_equal(oid, &oid_zero);
+}
+
 PyDoc_STRVAR(Oid_raw__doc__, "Raw oid, a 20 bytes string.");
 
 PyObject *
@@ -269,6 +278,45 @@ PyGetSetDef Oid_getseters[] = {
     {NULL},
 };
 
+PyNumberMethods Oid_as_number = {
+     0,                          /* nb_add */
+     0,                          /* nb_subtract */
+     0,                          /* nb_multiply */
+     0,                          /* nb_remainder */
+     0,                          /* nb_divmod */
+     0,                          /* nb_power */
+     0,                          /* nb_negative */
+     0,                          /* nb_positive */
+     0,                          /* nb_absolute */
+     Oid__bool,                  /* nb_bool */
+     0,                          /* nb_invert */
+     0,                          /* nb_lshift */
+     0,                          /* nb_rshift */
+     0,                          /* nb_and */
+     0,                          /* nb_xor */
+     0,                          /* nb_or */
+     0,                          /* nb_int */
+     0,                          /* nb_reserved */
+     0,                          /* nb_float */
+     0,                          /* nb_inplace_add */
+     0,                          /* nb_inplace_subtract */
+     0,                          /* nb_inplace_multiply */
+     0,                          /* nb_inplace_remainder */
+     0,                          /* nb_inplace_power */
+     0,                          /* nb_inplace_lshift */
+     0,                          /* nb_inplace_rshift */
+     0,                          /* nb_inplace_and */
+     0,                          /* nb_inplace_xor */
+     0,                          /* nb_inplace_or */
+     0,                          /* nb_floor_divide */
+     0,                          /* nb_true_divide */
+     0,                          /* nb_inplace_floor_divide */
+     0,                          /* nb_inplace_true_divide */
+     0,                          /* nb_index */
+     0,                          /* nb_matrix_multiply */
+     0,                          /* nb_inplace_matrix_multiply */
+};
+
 PyDoc_STRVAR(Oid__doc__, "Object id.");
 
 PyTypeObject OidType = {
@@ -282,7 +330,7 @@ PyTypeObject OidType = {
     0,                                         /* tp_setattr        */
     0,                                         /* tp_compare        */
     (reprfunc)Oid__str__,                      /* tp_repr           */
-    0,                                         /* tp_as_number      */
+    &Oid_as_number,                            /* tp_as_number      */
     0,                                         /* tp_as_sequence    */
     0,                                         /* tp_as_mapping     */
     (hashfunc)Oid_hash,                        /* tp_hash           */
diff -pruN 1.17.0-2/src/pygit2.c 1.18.1-1/src/pygit2.c
--- 1.17.0-2/src/pygit2.c	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/src/pygit2.c	2025-07-26 10:03:19.000000000 +0000
@@ -290,7 +290,7 @@ PyDoc_STRVAR(filter_register__doc__,
     "\n"
     "`priority` defaults to GIT_FILTER_DRIVER_PRIORITY which imitates a core\n"
     "Git filter driver that will be run last on checkout (smudge) and first \n"
-    "on checkin (clean).\n"
+    "on check-in (clean).\n"
     "\n"
     "Note that the filter registry is not thread safe. Any registering or\n"
     "deregistering of filters should be done outside of any possible usage\n"
diff -pruN 1.17.0-2/src/reference.c 1.18.1-1/src/reference.c
--- 1.17.0-2/src/reference.c	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/src/reference.c	2025-07-26 10:03:19.000000000 +0000
@@ -230,7 +230,7 @@ Reference_rename(Reference *self, PyObje
     if (err)
         return Error_set(err);
 
-    // Upadate reference
+    // Update reference
     git_reference_free(self->reference);
     self->reference = new_reference;
 
@@ -440,6 +440,14 @@ Reference_type__get__(Reference *self)
     return pygit2_enum(ReferenceTypeEnum, c_type);
 }
 
+PyDoc_STRVAR(Reference__pointer__doc__, "Get the reference's pointer. For internal use only.");
+
+PyObject *
+Reference__pointer__get__(Reference *self)
+{
+    /* Bytes means a raw buffer */
+    return PyBytes_FromStringAndSize((char *) &self->reference, sizeof(git_reference *));
+}
 
 PyDoc_STRVAR(Reference_log__doc__,
   "log() -> RefLogIter\n"
@@ -668,6 +676,7 @@ PyGetSetDef Reference_getseters[] = {
     GETTER(Reference, target),
     GETTER(Reference, raw_target),
     GETTER(Reference, type),
+    GETTER(Reference, _pointer),
     {NULL}
 };
 
diff -pruN 1.17.0-2/src/repository.c 1.18.1-1/src/repository.c
--- 1.17.0-2/src/repository.c	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/src/repository.c	2025-07-26 10:03:19.000000000 +0000
@@ -69,7 +69,7 @@ extern PyObject *FileStatusEnum;
 extern PyObject *MergeAnalysisEnum;
 extern PyObject *MergePreferenceEnum;
 
-/* forward-declaration for Repsository._from_c() */
+/* forward-declaration for Repository._from_c() */
 PyTypeObject RepositoryType;
 
 PyObject *
@@ -2078,7 +2078,7 @@ Repository_free(Repository *self)
 PyDoc_STRVAR(Repository_expand_id__doc__,
     "expand_id(hex: str) -> Oid\n"
     "\n"
-    "Expand a string into a full Oid according to the objects in this repsitory.\n");
+    "Expand a string into a full Oid according to the objects in this repository.\n");
 
 PyObject *
 Repository_expand_id(Repository *self, PyObject *py_hex)
diff -pruN 1.17.0-2/src/signature.c 1.18.1-1/src/signature.c
--- 1.17.0-2/src/signature.c	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/src/signature.c	2025-07-26 10:03:19.000000000 +0000
@@ -81,7 +81,7 @@ Signature_init(Signature *self, PyObject
 void
 Signature_dealloc(Signature *self)
 {
-    /* self->obj is the owner of the git_signature C structure, so we musn't free it */
+    /* self->obj is the owner of the git_signature C structure, so we mustn't free it */
     if (self->obj) {
         Py_CLEAR(self->obj);
     } else {
diff -pruN 1.17.0-2/src/tree.c 1.18.1-1/src/tree.c
--- 1.17.0-2/src/tree.c	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/src/tree.c	2025-07-26 10:03:19.000000000 +0000
@@ -222,14 +222,16 @@ PyDoc_STRVAR(Tree_diff_to_workdir__doc__
   "    the hunks will be merged into a one.\n");
 
 PyObject *
-Tree_diff_to_workdir(Tree *self, PyObject *args)
+Tree_diff_to_workdir(Tree *self, PyObject *args, PyObject *kwds)
 {
     git_diff_options opts = GIT_DIFF_OPTIONS_INIT;
     git_diff *diff;
     int err;
 
-    if (!PyArg_ParseTuple(args, "|IHH", &opts.flags, &opts.context_lines,
-                                        &opts.interhunk_lines))
+    char *keywords[] = {"flags", "context_lines", "interhunk_lines", NULL};
+
+    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|IHH", keywords, &opts.flags,
+                                     &opts.context_lines, &opts.interhunk_lines))
         return NULL;
 
     if (Object__load((Object*)self) == NULL) { return NULL; } // Lazy load
@@ -354,8 +356,7 @@ Tree_diff_to_tree(Tree *self, PyObject *
     git_diff *diff;
     git_tree *from, *to = NULL, *tmp;
     int err, swap = 0;
-    char *keywords[] = {"obj", "flags", "context_lines", "interhunk_lines",
-                        "swap", NULL};
+    char *keywords[] = {"obj", "flags", "context_lines", "interhunk_lines", "swap", NULL};
 
     Tree *other = NULL;
 
@@ -406,7 +407,7 @@ PyMappingMethods Tree_as_mapping = {
 
 PyMethodDef Tree_methods[] = {
     METHOD(Tree, diff_to_tree, METH_VARARGS | METH_KEYWORDS),
-    METHOD(Tree, diff_to_workdir, METH_VARARGS),
+    METHOD(Tree, diff_to_workdir, METH_VARARGS | METH_KEYWORDS),
     METHOD(Tree, diff_to_index, METH_VARARGS | METH_KEYWORDS),
     {NULL}
 };
diff -pruN 1.17.0-2/test/test_branch.py 1.18.1-1/test/test_branch.py
--- 1.17.0-2/test/test_branch.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/test/test_branch.py	2025-07-26 10:03:19.000000000 +0000
@@ -29,6 +29,7 @@ import pygit2
 import pytest
 import os
 from pygit2.enums import BranchType
+from pygit2 import Repository
 
 
 LAST_COMMIT = '2be5719152d4f82c7302b1c0932d8e5f0a4a0e98'
@@ -38,7 +39,7 @@ EXCLUSIVE_MASTER_COMMIT = '5ebeeebb32079
 SHARED_COMMIT = '4ec4389a8068641da2d6578db0419484972284c8'
 
 
-def test_branches_getitem(testrepo):
+def test_branches_getitem(testrepo: Repository) -> None:
     branch = testrepo.branches['master']
     assert branch.target == LAST_COMMIT
 
@@ -49,12 +50,12 @@ def test_branches_getitem(testrepo):
         testrepo.branches['not-exists']
 
 
-def test_branches(testrepo):
+def test_branches(testrepo: Repository) -> None:
     branches = sorted(testrepo.branches)
     assert branches == ['i18n', 'master']
 
 
-def test_branches_create(testrepo):
+def test_branches_create(testrepo: Repository) -> None:
     commit = testrepo[LAST_COMMIT]
     reference = testrepo.branches.create('version1', commit)
     assert 'version1' in testrepo.branches
@@ -70,27 +71,27 @@ def test_branches_create(testrepo):
     assert reference.target == LAST_COMMIT
 
 
-def test_branches_delete(testrepo):
+def test_branches_delete(testrepo: Repository) -> None:
     testrepo.branches.delete('i18n')
     assert testrepo.branches.get('i18n') is None
 
 
-def test_branches_delete_error(testrepo):
+def test_branches_delete_error(testrepo: Repository) -> None:
     with pytest.raises(pygit2.GitError):
         testrepo.branches.delete('master')
 
 
-def test_branches_is_head(testrepo):
+def test_branches_is_head(testrepo: Repository) -> None:
     branch = testrepo.branches.get('master')
     assert branch.is_head()
 
 
-def test_branches_is_not_head(testrepo):
+def test_branches_is_not_head(testrepo: Repository) -> None:
     branch = testrepo.branches.get('i18n')
     assert not branch.is_head()
 
 
-def test_branches_rename(testrepo):
+def test_branches_rename(testrepo: Repository) -> None:
     new_branch = testrepo.branches['i18n'].rename('new-branch')
     assert new_branch.target == I18N_LAST_COMMIT
 
@@ -98,25 +99,25 @@ def test_branches_rename(testrepo):
     assert new_branch_2.target == I18N_LAST_COMMIT
 
 
-def test_branches_rename_error(testrepo):
+def test_branches_rename_error(testrepo: Repository) -> None:
     original_branch = testrepo.branches.get('i18n')
     with pytest.raises(ValueError):
         original_branch.rename('master')
 
 
-def test_branches_rename_force(testrepo):
+def test_branches_rename_force(testrepo: Repository) -> None:
     original_branch = testrepo.branches.get('master')
     new_branch = original_branch.rename('i18n', True)
     assert new_branch.target == LAST_COMMIT
 
 
-def test_branches_rename_invalid(testrepo):
+def test_branches_rename_invalid(testrepo: Repository) -> None:
     original_branch = testrepo.branches.get('i18n')
     with pytest.raises(ValueError):
         original_branch.rename('abc@{123')
 
 
-def test_branches_name(testrepo):
+def test_branches_name(testrepo: Repository) -> None:
     branch = testrepo.branches.get('master')
     assert branch.branch_name == 'master'
     assert branch.name == 'refs/heads/master'
@@ -128,7 +129,7 @@ def test_branches_name(testrepo):
     assert branch.raw_branch_name == branch.branch_name.encode('utf-8')
 
 
-def test_branches_with_commit(testrepo):
+def test_branches_with_commit(testrepo: Repository) -> None:
     branches = testrepo.branches.with_commit(EXCLUSIVE_MASTER_COMMIT)
     assert sorted(branches) == ['master']
     assert branches.get('i18n') is None
diff -pruN 1.17.0-2/test/test_commit.py 1.18.1-1/test/test_commit.py
--- 1.17.0-2/test/test_commit.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/test/test_commit.py	2025-07-26 10:03:19.000000000 +0000
@@ -40,7 +40,7 @@ COMMIT_SHA_TO_AMEND = (
 )
 
 
-@utils.refcount
+@utils.requires_refcount
 def test_commit_refcount(barerepo):
     commit = barerepo[COMMIT_SHA]
     start = sys.getrefcount(commit)
@@ -58,7 +58,7 @@ def test_read_commit(barerepo):
     assert 'c2792cfa289ae6321ecf2cd5806c2194b0fd070c' == parents[0].id
     assert commit.message_encoding is None
     assert commit.message == (
-        'Second test data commit.\n\n' 'This commit has some additional text.\n'
+        'Second test data commit.\n\nThis commit has some additional text.\n'
     )
     commit_time = 1288481576
     assert commit_time == commit.commit_time
diff -pruN 1.17.0-2/test/test_credentials.py 1.18.1-1/test/test_credentials.py
--- 1.17.0-2/test/test_credentials.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/test/test_credentials.py	2025-07-26 10:03:19.000000000 +0000
@@ -23,8 +23,6 @@
 # the Free Software Foundation, 51 Franklin Street, Fifth Floor,
 # Boston, MA 02110-1301, USA.
 
-"""Tests for credentials"""
-
 from pathlib import Path
 import platform
 
diff -pruN 1.17.0-2/test/test_diff.py 1.18.1-1/test/test_diff.py
--- 1.17.0-2/test/test_diff.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/test/test_diff.py	2025-07-26 10:03:19.000000000 +0000
@@ -107,6 +107,68 @@ STATS_EXPECTED = """ a   | 2 +-
  delete mode 100644 c/d
 """
 
+TEXT_BLOB1 = """Common header of the file
+Blob 1 line 1
+Common middle line 1
+Common middle line 2
+Common middle line 3
+Blob 1 line 2
+Common footer of the file
+"""
+
+TEXT_BLOB2 = """Common header of the file
+Blob 2 line 1
+Common middle line 1
+Common middle line 2
+Common middle line 3
+Blob 2 line 2
+Common footer of the file
+"""
+
+PATCH_BLOBS_DEFAULT = """diff --git a/file b/file
+index 0b5ac93..ddfdbcc 100644
+--- a/file
++++ b/file
+@@ -1,7 +1,7 @@
+ Common header of the file
+-Blob 1 line 1
++Blob 2 line 1
+ Common middle line 1
+ Common middle line 2
+ Common middle line 3
+-Blob 1 line 2
++Blob 2 line 2
+ Common footer of the file
+"""
+
+PATCH_BLOBS_NO_LEEWAY = """diff --git a/file b/file
+index 0b5ac93..ddfdbcc 100644
+--- a/file
++++ b/file
+@@ -2 +2 @@ Common header of the file
+-Blob 1 line 1
++Blob 2 line 1
+@@ -6 +6 @@ Common middle line 3
+-Blob 1 line 2
++Blob 2 line 2
+"""
+
+PATCH_BLOBS_ONE_CONTEXT_LINE = """diff --git a/file b/file
+index 0b5ac93..ddfdbcc 100644
+--- a/file
++++ b/file
+@@ -1,3 +1,3 @@
+ Common header of the file
+-Blob 1 line 1
++Blob 2 line 1
+ Common middle line 1
+@@ -5,3 +5,3 @@ Common middle line 2
+ Common middle line 3
+-Blob 1 line 2
++Blob 2 line 2
+ Common footer of the file
+"""
+
 
 def test_diff_empty_index(dirtyrepo):
     repo = dirtyrepo
@@ -220,7 +282,7 @@ def test_diff_empty_tree(barerepo):
 
 def test_diff_revparse(barerepo):
     diff = barerepo.diff('HEAD', 'HEAD~6')
-    assert type(diff) == pygit2.Diff
+    assert type(diff) is pygit2.Diff
 
 
 def test_diff_tree_opts(barerepo):
@@ -382,3 +444,17 @@ def test_parse_diff_bad():
     )
     with pytest.raises(pygit2.GitError):
         pygit2.Diff.parse_diff(diff)
+
+
+def test_diff_blobs(emptyrepo):
+    repo = emptyrepo
+    blob1 = repo.create_blob(TEXT_BLOB1.encode())
+    blob2 = repo.create_blob(TEXT_BLOB2.encode())
+    diff_default = repo.diff(blob1, blob2)
+    assert diff_default.text == PATCH_BLOBS_DEFAULT
+    diff_no_leeway = repo.diff(blob1, blob2, context_lines=0)
+    assert diff_no_leeway.text == PATCH_BLOBS_NO_LEEWAY
+    diff_one_context_line = repo.diff(blob1, blob2, context_lines=1)
+    assert diff_one_context_line.text == PATCH_BLOBS_ONE_CONTEXT_LINE
+    diff_all_together = repo.diff(blob1, blob2, context_lines=1, interhunk_lines=1)
+    assert diff_all_together.text == PATCH_BLOBS_DEFAULT
diff -pruN 1.17.0-2/test/test_index.py 1.18.1-1/test/test_index.py
--- 1.17.0-2/test/test_index.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/test/test_index.py	2025-07-26 10:03:19.000000000 +0000
@@ -30,7 +30,7 @@ from pathlib import Path
 import pytest
 
 import pygit2
-from pygit2 import Repository, Index, Oid
+from pygit2 import Repository, Index, Oid, IndexEntry
 from pygit2.enums import FileMode
 from . import utils
 
@@ -192,6 +192,13 @@ def test_remove(testrepo):
     assert 'hello.txt' not in index
 
 
+def test_remove_directory(dirtyrepo):
+    index = dirtyrepo.index
+    assert 'subdir/current_file' in index
+    index.remove_directory('subdir')
+    assert 'subdir/current_file' not in index
+
+
 def test_remove_all(testrepo):
     index = testrepo.index
     assert 'hello.txt' in index
@@ -208,6 +215,13 @@ def test_remove_aspath(testrepo):
     assert 'hello.txt' not in index
 
 
+def test_remove_directory_aspath(dirtyrepo):
+    index = dirtyrepo.index
+    assert 'subdir/current_file' in index
+    index.remove_directory(Path('subdir'))
+    assert 'subdir/current_file' not in index
+
+
 def test_remove_all_aspath(testrepo):
     index = testrepo.index
     assert 'hello.txt' in index
@@ -297,3 +311,26 @@ def test_create_empty_read_tree_as_strin
 def test_create_empty_read_tree(testrepo):
     index = Index()
     index.read_tree(testrepo['fd937514cb799514d4b81bb24c5fcfeb6472b245'])
+
+
+@utils.fails_in_macos
+def test_add_conflict(testrepo):
+    ancestor_blob_id = testrepo.create_blob('ancestor')
+    ancestor = IndexEntry('conflict.txt', ancestor_blob_id, FileMode.BLOB_EXECUTABLE)
+
+    ours_blob_id = testrepo.create_blob('ours')
+    ours = IndexEntry('conflict.txt', ours_blob_id, FileMode.BLOB)
+
+    index = Index()
+    assert index.conflicts is None
+
+    index.add_conflict(ancestor, ours, None)
+
+    assert index.conflicts is not None
+    assert 'conflict.txt' in index.conflicts
+    conflict_ancestor, conflict_ours, conflict_theirs = index.conflicts['conflict.txt']
+    assert conflict_ancestor.id == ancestor_blob_id
+    assert conflict_ancestor.mode == FileMode.BLOB_EXECUTABLE
+    assert conflict_ours.id == ours_blob_id
+    assert conflict_ours.mode == FileMode.BLOB
+    assert conflict_theirs is None
diff -pruN 1.17.0-2/test/test_merge.py 1.18.1-1/test/test_merge.py
--- 1.17.0-2/test/test_merge.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/test/test_merge.py	2025-07-26 10:03:19.000000000 +0000
@@ -39,6 +39,16 @@ def test_merge_invalid_type(mergerepo, i
         mergerepo.merge(id)
 
 
+# TODO: Once Repository.merge drops support for str arguments,
+#       add an extra parameter to test_merge_invalid_type above
+#       to make sure we cover legacy code.
+def test_merge_string_argument_deprecated(mergerepo):
+    branch_head_hex = '5ebeeebb320790caf276b9fc8b24546d63316533'
+
+    with pytest.warns(DeprecationWarning, match=r'Pass Commit.+instead'):
+        mergerepo.merge(branch_head_hex)
+
+
 def test_merge_analysis_uptodate(mergerepo):
     branch_head_hex = '5ebeeebb320790caf276b9fc8b24546d63316533'
     branch_id = mergerepo.get(branch_head_hex).id
@@ -82,7 +92,10 @@ def test_merge_no_fastforward_no_conflic
 
 def test_merge_invalid_hex(mergerepo):
     branch_head_hex = '12345678'
-    with pytest.raises(KeyError):
+    with (
+        pytest.raises(KeyError),
+        pytest.warns(DeprecationWarning, match=r'Pass Commit.+instead'),
+    ):
         mergerepo.merge(branch_head_hex)
 
 
@@ -132,7 +145,7 @@ def test_merge_no_fastforward_conflicts(
 
 
 def test_merge_remove_conflicts(mergerepo):
-    other_branch_tip = '1b2bae55ac95a4be3f8983b86cd579226d0eb247'
+    other_branch_tip = pygit2.Oid(hex='1b2bae55ac95a4be3f8983b86cd579226d0eb247')
     mergerepo.merge(other_branch_tip)
     idx = mergerepo.index
     conflicts = idx.conflicts
@@ -158,30 +171,29 @@ def test_merge_remove_conflicts(mergerep
     ],
 )
 def test_merge_favor(mergerepo, favor):
-    branch_head_hex = '1b2bae55ac95a4be3f8983b86cd579226d0eb247'
-    mergerepo.merge(branch_head_hex, favor=favor)
+    branch_head = pygit2.Oid(hex='1b2bae55ac95a4be3f8983b86cd579226d0eb247')
+    mergerepo.merge(branch_head, favor=favor)
 
     assert mergerepo.index.conflicts is None
 
 
 def test_merge_fail_on_conflict(mergerepo):
-    branch_head_hex = '1b2bae55ac95a4be3f8983b86cd579226d0eb247'
+    branch_head = pygit2.Oid(hex='1b2bae55ac95a4be3f8983b86cd579226d0eb247')
 
-    with pytest.raises(pygit2.GitError):
+    with pytest.raises(pygit2.GitError, match=r'merge conflicts exist'):
         mergerepo.merge(
-            branch_head_hex, flags=MergeFlag.FIND_RENAMES | MergeFlag.FAIL_ON_CONFLICT
+            branch_head, flags=MergeFlag.FIND_RENAMES | MergeFlag.FAIL_ON_CONFLICT
         )
 
 
 def test_merge_commits(mergerepo):
-    branch_head_hex = '03490f16b15a09913edb3a067a3dc67fbb8d41f1'
-    branch_id = mergerepo.get(branch_head_hex).id
+    branch_head = pygit2.Oid(hex='03490f16b15a09913edb3a067a3dc67fbb8d41f1')
 
-    merge_index = mergerepo.merge_commits(mergerepo.head.target, branch_head_hex)
+    merge_index = mergerepo.merge_commits(mergerepo.head.target, branch_head)
     assert merge_index.conflicts is None
     merge_commits_tree = merge_index.write_tree(mergerepo)
 
-    mergerepo.merge(branch_id)
+    mergerepo.merge(branch_head)
     index = mergerepo.index
     assert index.conflicts is None
     merge_tree = index.write_tree()
@@ -190,26 +202,23 @@ def test_merge_commits(mergerepo):
 
 
 def test_merge_commits_favor(mergerepo):
-    branch_head_hex = '1b2bae55ac95a4be3f8983b86cd579226d0eb247'
+    branch_head = pygit2.Oid(hex='1b2bae55ac95a4be3f8983b86cd579226d0eb247')
 
     merge_index = mergerepo.merge_commits(
-        mergerepo.head.target, branch_head_hex, favor=MergeFavor.OURS
+        mergerepo.head.target, branch_head, favor=MergeFavor.OURS
     )
     assert merge_index.conflicts is None
 
     # Incorrect favor value
-    with pytest.raises(TypeError):
-        mergerepo.merge_commits(mergerepo.head.target, branch_head_hex, favor='foo')
+    with pytest.raises(TypeError, match=r'favor argument must be MergeFavor'):
+        mergerepo.merge_commits(mergerepo.head.target, branch_head, favor='foo')
 
 
 def test_merge_trees(mergerepo):
-    branch_head_hex = '03490f16b15a09913edb3a067a3dc67fbb8d41f1'
-    branch_id = mergerepo.get(branch_head_hex).id
+    branch_id = pygit2.Oid(hex='03490f16b15a09913edb3a067a3dc67fbb8d41f1')
     ancestor_id = mergerepo.merge_base(mergerepo.head.target, branch_id)
 
-    merge_index = mergerepo.merge_trees(
-        ancestor_id, mergerepo.head.target, branch_head_hex
-    )
+    merge_index = mergerepo.merge_trees(ancestor_id, mergerepo.head.target, branch_id)
     assert merge_index.conflicts is None
     merge_commits_tree = merge_index.write_tree(mergerepo)
 
@@ -312,25 +321,25 @@ def test_merge_octopus(mergerepo):
 def test_merge_mergeheads(mergerepo):
     assert mergerepo.listall_mergeheads() == []
 
-    branch_head_hex = '1b2bae55ac95a4be3f8983b86cd579226d0eb247'
-    mergerepo.merge(branch_head_hex)
+    branch_head = pygit2.Oid(hex='1b2bae55ac95a4be3f8983b86cd579226d0eb247')
+    mergerepo.merge(branch_head)
 
-    assert mergerepo.listall_mergeheads() == [pygit2.Oid(hex=branch_head_hex)]
+    assert mergerepo.listall_mergeheads() == [branch_head]
 
     mergerepo.state_cleanup()
-    assert (
-        mergerepo.listall_mergeheads() == []
-    ), 'state_cleanup() should wipe the mergeheads'
+    assert mergerepo.listall_mergeheads() == [], (
+        'state_cleanup() should wipe the mergeheads'
+    )
 
 
 def test_merge_message(mergerepo):
     assert not mergerepo.message
     assert not mergerepo.raw_message
 
-    branch_head_hex = '1b2bae55ac95a4be3f8983b86cd579226d0eb247'
-    mergerepo.merge(branch_head_hex)
+    branch_head = pygit2.Oid(hex='1b2bae55ac95a4be3f8983b86cd579226d0eb247')
+    mergerepo.merge(branch_head)
 
-    assert mergerepo.message.startswith(f"Merge commit '{branch_head_hex}'")
+    assert mergerepo.message.startswith(f"Merge commit '{branch_head}'")
     assert mergerepo.message.encode('utf-8') == mergerepo.raw_message
 
     mergerepo.state_cleanup()
@@ -338,9 +347,27 @@ def test_merge_message(mergerepo):
 
 
 def test_merge_remove_message(mergerepo):
-    branch_head_hex = '1b2bae55ac95a4be3f8983b86cd579226d0eb247'
-    mergerepo.merge(branch_head_hex)
+    branch_head = pygit2.Oid(hex='1b2bae55ac95a4be3f8983b86cd579226d0eb247')
+    mergerepo.merge(branch_head)
 
-    assert mergerepo.message.startswith(f"Merge commit '{branch_head_hex}'")
+    assert mergerepo.message.startswith(f"Merge commit '{branch_head}'")
     mergerepo.remove_message()
     assert not mergerepo.message
+
+
+def test_merge_commit(mergerepo):
+    commit = mergerepo['1b2bae55ac95a4be3f8983b86cd579226d0eb247']
+    assert isinstance(commit, pygit2.Commit)
+    mergerepo.merge(commit)
+
+    assert mergerepo.message.startswith(f"Merge commit '{str(commit.id)}'")
+    assert mergerepo.listall_mergeheads() == [commit.id]
+
+
+def test_merge_reference(mergerepo):
+    branch = mergerepo.branches.local['branch-conflicts']
+    branch_head_hex = '1b2bae55ac95a4be3f8983b86cd579226d0eb247'
+    mergerepo.merge(branch)
+
+    assert mergerepo.message.startswith("Merge branch 'branch-conflicts'")
+    assert mergerepo.listall_mergeheads() == [pygit2.Oid(hex=branch_head_hex)]
diff -pruN 1.17.0-2/test/test_nonunicode.py 1.18.1-1/test/test_nonunicode.py
--- 1.17.0-2/test/test_nonunicode.py	1970-01-01 00:00:00.000000000 +0000
+++ 1.18.1-1/test/test_nonunicode.py	2025-07-26 10:03:19.000000000 +0000
@@ -0,0 +1,58 @@
+# Copyright 2010-2024 The pygit2 contributors
+#
+# This file is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License, version 2,
+# as published by the Free Software Foundation.
+#
+# In addition to the permissions in the GNU General Public License,
+# the authors give you unlimited permission to link the compiled
+# version of this file into combinations with other programs,
+# and to distribute those combinations without any restriction
+# coming from the use of this file.  (The General Public License
+# restrictions do apply in other respects; for example, they cover
+# modification of the file, and distribution when not linked into
+# a combined executable.)
+#
+# This file is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; see the file COPYING.  If not, write to
+# the Free Software Foundation, 51 Franklin Street, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+
+"""Tests for non unicode byte strings"""
+
+import os
+import shutil
+import sys
+
+import pytest
+
+import pygit2
+from . import utils
+
+
+# FIXME Detect the filesystem rather than the operating system
+works_in_linux = pytest.mark.xfail(
+    sys.platform != 'linux',
+    reason='fails in macOS/Windows, and also in Linux with the FAT filesystem',
+)
+
+
+@utils.requires_network
+@works_in_linux
+def test_nonunicode_branchname(testrepo):
+    folderpath = 'temp_repo_nonutf'
+    if os.path.exists(folderpath):
+        shutil.rmtree(folderpath)
+    newrepo = pygit2.clone_repository(
+        path=folderpath, url='https://github.com/pygit2/test_branch_notutf.git'
+    )
+    bstring = b'\xc3master'
+    assert bstring in [
+        (ref.split('/')[-1]).encode('utf8', 'surrogateescape')
+        for ref in newrepo.listall_references()
+    ]  # Remote branch among references: 'refs/remotes/origin/\udcc3master'
diff -pruN 1.17.0-2/test/test_object.py 1.18.1-1/test/test_object.py
--- 1.17.0-2/test/test_object.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/test/test_object.py	2025-07-26 10:03:19.000000000 +0000
@@ -88,7 +88,7 @@ def test_peel_commit(testrepo):
     # and peel to the tree
     tree = commit.peel(ObjectType.TREE)
 
-    assert type(tree) == Tree
+    assert type(tree) is Tree
     assert tree.id == 'fd937514cb799514d4b81bb24c5fcfeb6472b245'
 
 
@@ -97,7 +97,7 @@ def test_peel_commit_type(testrepo):
     commit = testrepo[commit_id]
     tree = commit.peel(Tree)
 
-    assert type(tree) == Tree
+    assert type(tree) is Tree
     assert tree.id == 'fd937514cb799514d4b81bb24c5fcfeb6472b245'
 
 
diff -pruN 1.17.0-2/test/test_odb.py 1.18.1-1/test/test_odb.py
--- 1.17.0-2/test/test_odb.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/test/test_odb.py	2025-07-26 10:03:19.000000000 +0000
@@ -91,4 +91,4 @@ def test_write(odb):
         odb.write(ObjectType.ANY, data)
 
     oid = odb.write(ObjectType.BLOB, data)
-    assert type(oid) == Oid
+    assert type(oid) is Oid
diff -pruN 1.17.0-2/test/test_oid.py 1.18.1-1/test/test_oid.py
--- 1.17.0-2/test/test_oid.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/test/test_oid.py	2025-07-26 10:03:19.000000000 +0000
@@ -99,3 +99,10 @@ def test_hash():
     s.add(Oid(hex='0000000000000000000000000000000000000000'))
     s.add(Oid(hex='0000000000000000000000000000000000000001'))
     assert len(s) == 3
+
+
+def test_bool():
+    assert Oid(raw=RAW)
+    assert Oid(hex=HEX)
+    assert not Oid(raw=b'')
+    assert not Oid(hex='0000000000000000000000000000000000000000')
diff -pruN 1.17.0-2/test/test_refs.py 1.18.1-1/test/test_refs.py
--- 1.17.0-2/test/test_refs.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/test/test_refs.py	2025-07-26 10:03:19.000000000 +0000
@@ -29,7 +29,7 @@ from pathlib import Path
 
 import pytest
 
-from pygit2 import Commit, Signature, Tree, reference_is_valid_name
+from pygit2 import Commit, Signature, Tree, reference_is_valid_name, Repository
 from pygit2 import AlreadyExistsError, GitError, InvalidSpecError
 from pygit2.enums import ReferenceType
 
@@ -45,7 +45,7 @@ def test_refs_list_objects(testrepo):
     ]
 
 
-def test_refs_list(testrepo):
+def test_refs_list(testrepo: Repository) -> None:
     # Without argument
     assert sorted(testrepo.references) == ['refs/heads/i18n', 'refs/heads/master']
 
@@ -58,13 +58,13 @@ def test_refs_list(testrepo):
     ]
 
 
-def test_head(testrepo):
+def test_head(testrepo: Repository) -> None:
     head = testrepo.head
     assert LAST_COMMIT == testrepo[head.target].id
     assert LAST_COMMIT == testrepo[head.raw_target].id
 
 
-def test_refs_getitem(testrepo):
+def test_refs_getitem(testrepo: Repository) -> None:
     refname = 'refs/foo'
     # Raise KeyError ?
     with pytest.raises(KeyError):
@@ -78,37 +78,37 @@ def test_refs_getitem(testrepo):
     assert reference.name == 'refs/heads/master'
 
 
-def test_refs_get_sha(testrepo):
+def test_refs_get_sha(testrepo: Repository) -> None:
     reference = testrepo.references['refs/heads/master']
     assert reference.target == LAST_COMMIT
 
 
-def test_refs_set_sha(testrepo):
+def test_refs_set_sha(testrepo: Repository) -> None:
     NEW_COMMIT = '5ebeeebb320790caf276b9fc8b24546d63316533'
     reference = testrepo.references.get('refs/heads/master')
     reference.set_target(NEW_COMMIT)
     assert reference.target == NEW_COMMIT
 
 
-def test_refs_set_sha_prefix(testrepo):
+def test_refs_set_sha_prefix(testrepo: Repository) -> None:
     NEW_COMMIT = '5ebeeebb320790caf276b9fc8b24546d63316533'
     reference = testrepo.references.get('refs/heads/master')
     reference.set_target(NEW_COMMIT[0:6])
     assert reference.target == NEW_COMMIT
 
 
-def test_refs_get_type(testrepo):
+def test_refs_get_type(testrepo: Repository) -> None:
     reference = testrepo.references.get('refs/heads/master')
     assert reference.type == ReferenceType.DIRECT
 
 
-def test_refs_get_target(testrepo):
+def test_refs_get_target(testrepo: Repository) -> None:
     reference = testrepo.references.get('HEAD')
     assert reference.target == 'refs/heads/master'
     assert reference.raw_target == b'refs/heads/master'
 
 
-def test_refs_set_target(testrepo):
+def test_refs_set_target(testrepo: Repository) -> None:
     reference = testrepo.references.get('HEAD')
     assert reference.target == 'refs/heads/master'
     assert reference.raw_target == b'refs/heads/master'
@@ -117,14 +117,14 @@ def test_refs_set_target(testrepo):
     assert reference.raw_target == b'refs/heads/i18n'
 
 
-def test_refs_get_shorthand(testrepo):
+def test_refs_get_shorthand(testrepo: Repository) -> None:
     reference = testrepo.references.get('refs/heads/master')
     assert reference.shorthand == 'master'
     reference = testrepo.references.create('refs/remotes/origin/master', LAST_COMMIT)
     assert reference.shorthand == 'origin/master'
 
 
-def test_refs_set_target_with_message(testrepo):
+def test_refs_set_target_with_message(testrepo: Repository) -> None:
     reference = testrepo.references.get('HEAD')
     assert reference.target == 'refs/heads/master'
     assert reference.raw_target == b'refs/heads/master'
@@ -139,7 +139,7 @@ def test_refs_set_target_with_message(te
     assert first.committer == sig
 
 
-def test_refs_delete(testrepo):
+def test_refs_delete(testrepo: Repository) -> None:
     # We add a tag as a new reference that points to "origin/master"
     reference = testrepo.references.create('refs/tags/version1', LAST_COMMIT)
     assert 'refs/tags/version1' in testrepo.references
@@ -163,7 +163,7 @@ def test_refs_delete(testrepo):
         reference.rename('refs/tags/version2')
 
 
-def test_refs_rename(testrepo):
+def test_refs_rename(testrepo: Repository) -> None:
     # We add a tag as a new reference that points to "origin/master"
     reference = testrepo.references.create('refs/tags/version1', LAST_COMMIT)
     assert reference.name == 'refs/tags/version1'
@@ -177,7 +177,7 @@ def test_refs_rename(testrepo):
         reference.rename('b1')
 
 
-# def test_reload(testrepo):
+# def test_reload(testrepo: Repository) -> None:
 #    name = 'refs/tags/version1'
 #    ref = testrepo.create_reference(name, "refs/heads/master", symbolic=True)
 #    ref2 = testrepo.lookup_reference(name)
@@ -187,7 +187,7 @@ def test_refs_rename(testrepo):
 #    with pytest.raises(GitError): getattr(ref2, 'name')
 
 
-def test_refs_resolve(testrepo):
+def test_refs_resolve(testrepo: Repository) -> None:
     reference = testrepo.references.get('HEAD')
     assert reference.type == ReferenceType.SYMBOLIC
     reference = reference.resolve()
@@ -195,13 +195,13 @@ def test_refs_resolve(testrepo):
     assert reference.target == LAST_COMMIT
 
 
-def test_refs_resolve_identity(testrepo):
+def test_refs_resolve_identity(testrepo: Repository) -> None:
     head = testrepo.references.get('HEAD')
     ref = head.resolve()
     assert ref.resolve() is ref
 
 
-def test_refs_create(testrepo):
+def test_refs_create(testrepo: Repository) -> None:
     # We add a tag as a new reference that points to "origin/master"
     reference = testrepo.references.create('refs/tags/version1', LAST_COMMIT)
     refs = testrepo.references
@@ -220,7 +220,7 @@ def test_refs_create(testrepo):
     assert reference.target == LAST_COMMIT
 
 
-def test_refs_create_symbolic(testrepo):
+def test_refs_create_symbolic(testrepo: Repository) -> None:
     # We add a tag as a new symbolic reference that always points to
     # "refs/heads/master"
     reference = testrepo.references.create('refs/tags/beta', 'refs/heads/master')
@@ -241,11 +241,11 @@ def test_refs_create_symbolic(testrepo):
     assert reference.raw_target == b'refs/heads/master'
 
 
-# def test_packall_references(testrepo):
+# def test_packall_references(testrepo: Repository) -> None:
 #    testrepo.packall_references()
 
 
-def test_refs_peel(testrepo):
+def test_refs_peel(testrepo: Repository) -> None:
     ref = testrepo.references.get('refs/heads/master')
     assert testrepo[ref.target].id == ref.peel().id
     assert testrepo[ref.raw_target].id == ref.peel().id
@@ -254,7 +254,7 @@ def test_refs_peel(testrepo):
     assert commit.tree.id == ref.peel(Tree).id
 
 
-def test_refs_equality(testrepo):
+def test_refs_equality(testrepo: Repository) -> None:
     ref1 = testrepo.references.get('refs/heads/master')
     ref2 = testrepo.references.get('refs/heads/master')
     ref3 = testrepo.references.get('refs/heads/i18n')
@@ -267,7 +267,7 @@ def test_refs_equality(testrepo):
     assert not ref1 == ref3
 
 
-def test_refs_compress(testrepo):
+def test_refs_compress(testrepo: Repository) -> None:
     packed_refs_file = Path(testrepo.path) / 'packed-refs'
     assert not packed_refs_file.exists()
     old_refs = [(ref.name, ref.target) for ref in testrepo.references.objects]
diff -pruN 1.17.0-2/test/test_remote.py 1.18.1-1/test/test_remote.py
--- 1.17.0-2/test/test_remote.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/test/test_remote.py	2025-07-26 10:03:19.000000000 +0000
@@ -23,16 +23,15 @@
 # the Free Software Foundation, 51 Franklin Street, Fifth Floor,
 # Boston, MA 02110-1301, USA.
 
-"""Tests for Remote objects."""
-
-from unittest.mock import patch
 import sys
+from pathlib import Path
+from collections.abc import Generator
 
 import pytest
 
 import pygit2
-from pygit2 import Oid
-from pygit2.ffi import ffi
+from pygit2 import Repository, Remote
+from pygit2.remotes import TransferProgress
 from . import utils
 
 
@@ -48,13 +47,13 @@ REMOTE_REPO_FETCH_HEAD_COMMIT_OBJECTS =
 ORIGIN_REFSPEC = '+refs/heads/*:refs/remotes/origin/*'
 
 
-def test_remote_create(testrepo):
+def test_remote_create(testrepo: Repository) -> None:
     name = 'upstream'
     url = 'https://github.com/libgit2/pygit2.git'
 
     remote = testrepo.remotes.create(name, url)
 
-    assert type(remote) == pygit2.Remote
+    assert type(remote) is pygit2.Remote
     assert name == remote.name
     assert url == remote.url
     assert remote.push_url is None
@@ -63,21 +62,21 @@ def test_remote_create(testrepo):
         testrepo.remotes.create(*(name, url))
 
 
-def test_remote_create_with_refspec(testrepo):
+def test_remote_create_with_refspec(testrepo: Repository) -> None:
     name = 'upstream'
     url = 'https://github.com/libgit2/pygit2.git'
     fetch = '+refs/*:refs/*'
 
     remote = testrepo.remotes.create(name, url, fetch)
 
-    assert type(remote) == pygit2.Remote
+    assert type(remote) is pygit2.Remote
     assert name == remote.name
     assert url == remote.url
     assert [fetch] == remote.fetch_refspecs
     assert remote.push_url is None
 
 
-def test_remote_create_anonymous(testrepo):
+def test_remote_create_anonymous(testrepo: Repository) -> None:
     url = 'https://github.com/libgit2/pygit2.git'
 
     remote = testrepo.remotes.create_anonymous(url)
@@ -88,7 +87,7 @@ def test_remote_create_anonymous(testrep
     assert [] == remote.push_refspecs
 
 
-def test_remote_delete(testrepo):
+def test_remote_delete(testrepo: Repository) -> None:
     name = 'upstream'
     url = 'https://github.com/libgit2/pygit2.git'
 
@@ -101,7 +100,7 @@ def test_remote_delete(testrepo):
     assert 1 == len(testrepo.remotes)
 
 
-def test_remote_rename(testrepo):
+def test_remote_rename(testrepo: Repository) -> None:
     remote = testrepo.remotes[0]
 
     assert REMOTE_NAME == remote.name
@@ -112,10 +111,10 @@ def test_remote_rename(testrepo):
     with pytest.raises(ValueError):
         testrepo.remotes.rename('', '')
     with pytest.raises(ValueError):
-        testrepo.remotes.rename(None, None)
+        testrepo.remotes.rename(None, None)  # type: ignore
 
 
-def test_remote_set_url(testrepo):
+def test_remote_set_url(testrepo: Repository) -> None:
     remote = testrepo.remotes['origin']
     assert REMOTE_URL == remote.url
 
@@ -134,7 +133,7 @@ def test_remote_set_url(testrepo):
         testrepo.remotes.set_push_url('origin', '')
 
 
-def test_refspec(testrepo):
+def test_refspec(testrepo: Repository) -> None:
     remote = testrepo.remotes['origin']
 
     assert remote.refspec_count == 1
@@ -144,7 +143,7 @@ def test_refspec(testrepo):
     assert refspec.force is True
     assert ORIGIN_REFSPEC == refspec.string
 
-    assert list == type(remote.fetch_refspecs)
+    assert list is type(remote.fetch_refspecs)
     assert 1 == len(remote.fetch_refspecs)
     assert ORIGIN_REFSPEC == remote.fetch_refspecs[0]
 
@@ -153,18 +152,18 @@ def test_refspec(testrepo):
     assert 'refs/remotes/origin/master' == refspec.transform('refs/heads/master')
     assert 'refs/heads/master' == refspec.rtransform('refs/remotes/origin/master')
 
-    assert list == type(remote.push_refspecs)
+    assert list is type(remote.push_refspecs)
     assert 0 == len(remote.push_refspecs)
 
     push_specs = remote.push_refspecs
-    assert list == type(push_specs)
+    assert list is type(push_specs)
     assert 0 == len(push_specs)
 
     testrepo.remotes.add_fetch('origin', '+refs/test/*:refs/test/remotes/*')
     remote = testrepo.remotes['origin']
 
     fetch_specs = remote.fetch_refspecs
-    assert list == type(fetch_specs)
+    assert list is type(fetch_specs)
     assert 2 == len(fetch_specs)
     assert [
         '+refs/heads/*:refs/remotes/origin/*',
@@ -174,13 +173,13 @@ def test_refspec(testrepo):
     testrepo.remotes.add_push('origin', '+refs/test/*:refs/test/remotes/*')
 
     with pytest.raises(TypeError):
-        testrepo.remotes.add_fetch(['+refs/*:refs/*', 5])
+        testrepo.remotes.add_fetch(['+refs/*:refs/*', 5])  # type: ignore
 
     remote = testrepo.remotes['origin']
     assert ['+refs/test/*:refs/test/remotes/*'] == remote.push_refspecs
 
 
-def test_remote_list(testrepo):
+def test_remote_list(testrepo: Repository) -> None:
     assert 1 == len(testrepo.remotes)
     remote = testrepo.remotes[0]
     assert REMOTE_NAME == remote.name
@@ -194,7 +193,7 @@ def test_remote_list(testrepo):
 
 
 @utils.requires_network
-def test_ls_remotes(testrepo):
+def test_ls_remotes(testrepo: Repository) -> None:
     assert 1 == len(testrepo.remotes)
     remote = testrepo.remotes[0]
 
@@ -205,7 +204,7 @@ def test_ls_remotes(testrepo):
     assert next(iter(r for r in refs if r['name'] == 'refs/tags/v0.28.2'))
 
 
-def test_remote_collection(testrepo):
+def test_remote_collection(testrepo: Repository) -> None:
     remote = testrepo.remotes['origin']
     assert REMOTE_NAME == remote.name
     assert REMOTE_URL == remote.url
@@ -220,8 +219,8 @@ def test_remote_collection(testrepo):
     assert remote.name in [x.name for x in testrepo.remotes]
 
 
-@utils.refcount
-def test_remote_refcount(testrepo):
+@utils.requires_refcount
+def test_remote_refcount(testrepo: Repository) -> None:
     start = sys.getrefcount(testrepo)
     remote = testrepo.remotes[0]
     del remote
@@ -229,7 +228,7 @@ def test_remote_refcount(testrepo):
     assert start == end
 
 
-def test_fetch(emptyrepo):
+def test_fetch(emptyrepo: Repository) -> None:
     remote = emptyrepo.remotes[0]
     stats = remote.fetch()
     assert stats.received_bytes > 2700
@@ -239,7 +238,7 @@ def test_fetch(emptyrepo):
 
 
 @utils.requires_network
-def test_fetch_depth_zero(testrepo):
+def test_fetch_depth_zero(testrepo: Repository) -> None:
     remote = testrepo.remotes[0]
     stats = remote.fetch(REMOTE_FETCHTEST_FETCHSPECS, depth=0)
     assert stats.indexed_objects == REMOTE_REPO_FETCH_ALL_OBJECTS
@@ -247,17 +246,17 @@ def test_fetch_depth_zero(testrepo):
 
 
 @utils.requires_network
-def test_fetch_depth_one(testrepo):
+def test_fetch_depth_one(testrepo: Repository) -> None:
     remote = testrepo.remotes[0]
     stats = remote.fetch(REMOTE_FETCHTEST_FETCHSPECS, depth=1)
     assert stats.indexed_objects == REMOTE_REPO_FETCH_HEAD_COMMIT_OBJECTS
     assert stats.received_objects == REMOTE_REPO_FETCH_HEAD_COMMIT_OBJECTS
 
 
-def test_transfer_progress(emptyrepo):
+def test_transfer_progress(emptyrepo: Repository) -> None:
     class MyCallbacks(pygit2.RemoteCallbacks):
-        def transfer_progress(emptyrepo, stats):
-            emptyrepo.tp = stats
+        def transfer_progress(self, stats: TransferProgress) -> None:
+            self.tp = stats
 
     callbacks = MyCallbacks()
     remote = emptyrepo.remotes[0]
@@ -267,18 +266,18 @@ def test_transfer_progress(emptyrepo):
     assert stats.received_objects == callbacks.tp.received_objects
 
 
-def test_update_tips(emptyrepo):
+def test_update_tips(emptyrepo: Repository) -> None:
     remote = emptyrepo.remotes[0]
     tips = [
         (
             'refs/remotes/origin/master',
-            Oid(hex='0' * 40),
-            Oid(hex='784855caf26449a1914d2cf62d12b9374d76ae78'),
+            pygit2.Oid(hex='0' * 40),
+            pygit2.Oid(hex='784855caf26449a1914d2cf62d12b9374d76ae78'),
         ),
         (
             'refs/tags/root',
-            Oid(hex='0' * 40),
-            Oid(hex='3d2962987c695a29f1f80b6c3aa4ec046ef44369'),
+            pygit2.Oid(hex='0' * 40),
+            pygit2.Oid(hex='3d2962987c695a29f1f80b6c3aa4ec046ef44369'),
         ),
     ]
 
@@ -297,14 +296,16 @@ def test_update_tips(emptyrepo):
 
 
 @utils.requires_network
-def test_ls_remotes_certificate_check():
+def test_ls_remotes_certificate_check() -> None:
     url = 'https://github.com/pygit2/empty.git'
 
     class MyCallbacks(pygit2.RemoteCallbacks):
-        def __init__(self):
+        def __init__(self) -> None:
             self.i = 0
 
-        def certificate_check(self, certificate, valid, host):
+        def certificate_check(
+            self, certificate: None, valid: bool, host: str | bytes
+        ) -> bool:
             self.i += 1
 
             assert certificate is None
@@ -327,13 +328,13 @@ def test_ls_remotes_certificate_check():
 
 
 @pytest.fixture
-def origin(tmp_path):
+def origin(tmp_path: Path) -> Generator[Repository, None, None]:
     with utils.TemporaryRepository('barerepo.zip', tmp_path) as path:
         yield pygit2.Repository(path)
 
 
 @pytest.fixture
-def clone(tmp_path):
+def clone(tmp_path: Path) -> Generator[Repository, None, None]:
     clone = tmp_path / 'clone'
     clone.mkdir()
     with utils.TemporaryRepository('barerepo.zip', clone) as path:
@@ -345,7 +346,9 @@ def remote(origin, clone):
     yield clone.remotes.create('origin', origin.path)
 
 
-def test_push_fast_forward_commits_to_remote_succeeds(origin, clone, remote):
+def test_push_fast_forward_commits_to_remote_succeeds(
+    origin: Repository, clone: Repository, remote: Remote
+) -> None:
     tip = clone[clone.head.target]
     oid = clone.create_commit(
         'refs/heads/master',
@@ -359,14 +362,79 @@ def test_push_fast_forward_commits_to_re
     assert origin[origin.head.target].id == oid
 
 
-def test_push_when_up_to_date_succeeds(origin, clone, remote):
+def test_push_when_up_to_date_succeeds(
+    origin: Repository, clone: Repository, remote: Remote
+) -> None:
     remote.push(['refs/heads/master'])
     origin_tip = origin[origin.head.target].id
     clone_tip = clone[clone.head.target].id
     assert origin_tip == clone_tip
 
 
-def test_push_non_fast_forward_commits_to_remote_fails(origin, clone, remote):
+def test_push_transfer_progress(
+    origin: Repository, clone: Repository, remote: Remote
+) -> None:
+    tip = clone[clone.head.target]
+    new_tip_id = clone.create_commit(
+        'refs/heads/master',
+        tip.author,
+        tip.author,
+        'empty commit',
+        tip.tree.id,
+        [tip.id],
+    )
+
+    # NOTE: We're currently not testing bytes_pushed due to a bug in libgit2
+    # 1.9.0: it passes a junk value for bytes_pushed when pushing to a remote
+    # on the local filesystem, as is the case in this unit test. (When pushing
+    # to a remote over the network, the value is correct.)
+    class MyCallbacks(pygit2.RemoteCallbacks):
+        def push_transfer_progress(
+            self, objects_pushed: int, total_objects: int, bytes_pushed: int
+        ) -> None:
+            self.objects_pushed = objects_pushed
+            self.total_objects = total_objects
+
+    assert origin.branches['master'].target == tip.id
+
+    callbacks = MyCallbacks()
+    remote.push(['refs/heads/master'], callbacks=callbacks)
+    assert callbacks.objects_pushed == 1
+    assert callbacks.total_objects == 1
+    assert origin.branches['master'].target == new_tip_id
+
+
+def test_push_interrupted_from_callbacks(
+    origin: Repository, clone: Repository, remote: Remote
+) -> None:
+    tip = clone[clone.head.target]
+    clone.create_commit(
+        'refs/heads/master',
+        tip.author,
+        tip.author,
+        'empty commit',
+        tip.tree.id,
+        [tip.id],
+    )
+
+    class MyCallbacks(pygit2.RemoteCallbacks):
+        def push_transfer_progress(
+            self, objects_pushed: int, total_objects: int, bytes_pushed: int
+        ) -> None:
+            raise InterruptedError('retreat! retreat!')
+
+    assert origin.branches['master'].target == tip.id
+
+    callbacks = MyCallbacks()
+    with pytest.raises(InterruptedError, match='retreat! retreat!'):
+        remote.push(['refs/heads/master'], callbacks=callbacks)
+
+    assert origin.branches['master'].target == tip.id
+
+
+def test_push_non_fast_forward_commits_to_remote_fails(
+    origin: Repository, clone: Repository, remote: Remote
+) -> None:
     tip = origin[origin.head.target]
     origin.create_commit(
         'refs/heads/master',
@@ -390,22 +458,47 @@ def test_push_non_fast_forward_commits_t
         remote.push(['refs/heads/master'])
 
 
-@patch.object(pygit2.callbacks, 'RemoteCallbacks')
-def test_push_options(mock_callbacks, origin, clone, remote):
-    remote.push(['refs/heads/master'])
-    remote_push_options = mock_callbacks.return_value.push_options.remote_push_options
+def test_push_options(origin: Repository, clone: Repository, remote: Remote) -> None:
+    from pygit2 import RemoteCallbacks
+
+    callbacks = RemoteCallbacks()
+    remote.push(['refs/heads/master'], callbacks)
+    remote_push_options = callbacks.push_options.remote_push_options
     assert remote_push_options.count == 0
 
-    remote.push(['refs/heads/master'], push_options=[])
-    remote_push_options = mock_callbacks.return_value.push_options.remote_push_options
+    callbacks = RemoteCallbacks()
+    remote.push(['refs/heads/master'], callbacks, push_options=[])
+    remote_push_options = callbacks.push_options.remote_push_options
     assert remote_push_options.count == 0
 
-    remote.push(['refs/heads/master'], push_options=['foo'])
-    remote_push_options = mock_callbacks.return_value.push_options.remote_push_options
+    callbacks = RemoteCallbacks()
+    # Local remotes don't support push_options, so pushing will raise an error.
+    # However, push_options should still be set in RemoteCallbacks.
+    with pytest.raises(pygit2.GitError, match='push-options not supported by remote'):
+        remote.push(['refs/heads/master'], callbacks, push_options=['foo'])
+    remote_push_options = callbacks.push_options.remote_push_options
     assert remote_push_options.count == 1
     # strings pointed to by remote_push_options.strings[] are already freed
 
-    remote.push(['refs/heads/master'], push_options=['Option A', 'Option B'])
-    remote_push_options = mock_callbacks.return_value.push_options.remote_push_options
+    callbacks = RemoteCallbacks()
+    with pytest.raises(pygit2.GitError, match='push-options not supported by remote'):
+        remote.push(['refs/heads/master'], callbacks, push_options=['Opt A', 'Opt B'])
+    remote_push_options = callbacks.push_options.remote_push_options
     assert remote_push_options.count == 2
     # strings pointed to by remote_push_options.strings[] are already freed
+
+
+def test_push_threads(origin: Repository, clone: Repository, remote: Remote) -> None:
+    from pygit2 import RemoteCallbacks
+
+    callbacks = RemoteCallbacks()
+    remote.push(['refs/heads/master'], callbacks)
+    assert callbacks.push_options.pb_parallelism == 1
+
+    callbacks = RemoteCallbacks()
+    remote.push(['refs/heads/master'], callbacks, threads=0)
+    assert callbacks.push_options.pb_parallelism == 0
+
+    callbacks = RemoteCallbacks()
+    remote.push(['refs/heads/master'], callbacks, threads=1)
+    assert callbacks.push_options.pb_parallelism == 1
diff -pruN 1.17.0-2/test/test_repository.py 1.18.1-1/test/test_repository.py
--- 1.17.0-2/test/test_repository.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/test/test_repository.py	2025-07-26 10:03:19.000000000 +0000
@@ -31,7 +31,7 @@ import pytest
 
 # pygit2
 import pygit2
-from pygit2 import init_repository, clone_repository, discover_repository
+from pygit2 import init_repository, clone_repository, discover_repository, IndexEntry
 from pygit2 import Oid
 from pygit2.enums import (
     CheckoutNotify,
@@ -42,7 +42,9 @@ from pygit2.enums import (
     RepositoryState,
     ResetMode,
     StashApplyProgress,
+    FileMode,
 )
+from pygit2.index import MergeFileResult
 from . import utils
 
 
@@ -99,6 +101,8 @@ def test_checkout_callbacks(testrepo):
             super().__init__()
             self.conflicting_paths = set()
             self.updated_paths = set()
+            self.completed_steps = -1
+            self.total_steps = -1
 
         def checkout_notify_flags(self) -> CheckoutNotify:
             return CheckoutNotify.CONFLICT | CheckoutNotify.UPDATED
@@ -109,12 +113,17 @@ def test_checkout_callbacks(testrepo):
             elif why == CheckoutNotify.UPDATED:
                 self.updated_paths.add(path)
 
+        def checkout_progress(self, path: str, completed_steps: int, total_steps: int):
+            self.completed_steps = completed_steps
+            self.total_steps = total_steps
+
     # checkout i18n with conflicts and default strategy should not be possible
     callbacks = MyCheckoutCallbacks()
     with pytest.raises(pygit2.GitError):
         testrepo.checkout(ref_i18n, callbacks=callbacks)
     # make sure the callbacks caught that
     assert {'bye.txt'} == callbacks.conflicting_paths
+    assert -1 == callbacks.completed_steps  # shouldn't have done anything
 
     # checkout i18n with GIT_CHECKOUT_FORCE
     head = testrepo.head
@@ -125,6 +134,8 @@ def test_checkout_callbacks(testrepo):
     # make sure the callbacks caught the files affected by the checkout
     assert set() == callbacks.conflicting_paths
     assert {'bye.txt', 'new'} == callbacks.updated_paths
+    assert callbacks.completed_steps > 0
+    assert callbacks.completed_steps == callbacks.total_steps
 
 
 def test_checkout_aborted_from_callbacks(testrepo):
@@ -587,7 +598,7 @@ def test_new_repo(tmp_path):
     repo = init_repository(tmp_path, False)
 
     oid = repo.write(ObjectType.BLOB, 'Test')
-    assert type(oid) == Oid
+    assert type(oid) is Oid
 
     assert (tmp_path / '.git').exists()
 
@@ -629,7 +640,6 @@ def test_discover_repo(tmp_path):
     assert repo.path == discover_repository(str(subdir))
 
 
-@utils.fspath
 def test_discover_repo_aspath(tmp_path):
     repo = init_repository(Path(tmp_path), False)
     subdir = Path(tmp_path) / 'test1' / 'test2'
@@ -728,6 +738,18 @@ def test_clone_with_checkout_branch(bare
     assert repo.lookup_reference('HEAD').target == 'refs/heads/test'
 
 
+@utils.requires_proxy
+@utils.requires_network
+def test_clone_with_proxy(tmp_path):
+    url = 'https://github.com/libgit2/TestGitRepository'
+    repo = clone_repository(
+        url,
+        tmp_path / 'testrepo-orig.git',
+        proxy=True,
+    )
+    assert not repo.is_empty
+
+
 # FIXME The tests below are commented because they are broken:
 #
 # - test_clone_push_url: Passes, but does nothing useful.
@@ -811,7 +833,6 @@ def test_worktree(testrepo):
     assert testrepo.list_worktrees() == []
 
 
-@utils.fspath
 def test_worktree_aspath(testrepo):
     worktree_name = 'foo'
     worktree_dir = Path(tempfile.mkdtemp())
@@ -966,3 +987,144 @@ def test_repository_hashfile_filter(test
     testrepo.config['core.safecrlf'] = 'fail'
     with pytest.raises(pygit2.GitError):
         h = testrepo.hashfile('hello.txt')
+
+
+def test_merge_file_from_index_deprecated(testrepo):
+    hello_txt = testrepo.index['hello.txt']
+    hello_txt_executable = IndexEntry(
+        hello_txt.path, hello_txt.id, FileMode.BLOB_EXECUTABLE
+    )
+    hello_world = IndexEntry('hello_world.txt', hello_txt.id, hello_txt.mode)
+
+    # no change
+    res = testrepo.merge_file_from_index(hello_txt, hello_txt, hello_txt)
+    assert res == testrepo.get(hello_txt.id).data.decode()
+
+    # executable switch on ours
+    res = testrepo.merge_file_from_index(hello_txt, hello_txt_executable, hello_txt)
+    assert res == testrepo.get(hello_txt.id).data.decode()
+
+    # executable switch on theirs
+    res = testrepo.merge_file_from_index(hello_txt, hello_txt, hello_txt_executable)
+    assert res == testrepo.get(hello_txt.id).data.decode()
+
+    # executable switch on both
+    res = testrepo.merge_file_from_index(
+        hello_txt, hello_txt_executable, hello_txt_executable
+    )
+    assert res == testrepo.get(hello_txt.id).data.decode()
+
+    # path switch on ours
+    res = testrepo.merge_file_from_index(hello_txt, hello_world, hello_txt)
+    assert res == testrepo.get(hello_txt.id).data.decode()
+
+    # path switch on theirs
+    res = testrepo.merge_file_from_index(hello_txt, hello_txt, hello_world)
+    assert res == testrepo.get(hello_txt.id).data.decode()
+
+    # path switch on both
+    res = testrepo.merge_file_from_index(hello_txt, hello_world, hello_world)
+    assert res == testrepo.get(hello_txt.id).data.decode()
+
+    # path switch on ours, executable flag switch on theirs
+    res = testrepo.merge_file_from_index(hello_txt, hello_world, hello_txt_executable)
+    assert res == testrepo.get(hello_txt.id).data.decode()
+
+    # path switch on theirs, executable flag switch on ours
+    res = testrepo.merge_file_from_index(hello_txt, hello_txt_executable, hello_world)
+    assert res == testrepo.get(hello_txt.id).data.decode()
+
+
+def test_merge_file_from_index_non_deprecated(testrepo):
+    hello_txt = testrepo.index['hello.txt']
+    hello_txt_executable = IndexEntry(
+        hello_txt.path, hello_txt.id, FileMode.BLOB_EXECUTABLE
+    )
+    hello_world = IndexEntry('hello_world.txt', hello_txt.id, hello_txt.mode)
+
+    # no change
+    res = testrepo.merge_file_from_index(
+        hello_txt, hello_txt, hello_txt, use_deprecated=False
+    )
+    assert res == MergeFileResult(
+        True, hello_txt.path, hello_txt.mode, testrepo.get(hello_txt.id).data.decode()
+    )
+
+    # executable switch on ours
+    res = testrepo.merge_file_from_index(
+        hello_txt, hello_txt_executable, hello_txt, use_deprecated=False
+    )
+    assert res == MergeFileResult(
+        True,
+        hello_txt.path,
+        hello_txt_executable.mode,
+        testrepo.get(hello_txt.id).data.decode(),
+    )
+
+    # executable switch on theirs
+    res = testrepo.merge_file_from_index(
+        hello_txt, hello_txt, hello_txt_executable, use_deprecated=False
+    )
+    assert res == MergeFileResult(
+        True,
+        hello_txt.path,
+        hello_txt_executable.mode,
+        testrepo.get(hello_txt.id).data.decode(),
+    )
+
+    # executable switch on both
+    res = testrepo.merge_file_from_index(
+        hello_txt, hello_txt_executable, hello_txt_executable, use_deprecated=False
+    )
+    assert res == MergeFileResult(
+        True,
+        hello_txt.path,
+        hello_txt_executable.mode,
+        testrepo.get(hello_txt.id).data.decode(),
+    )
+
+    # path switch on ours
+    res = testrepo.merge_file_from_index(
+        hello_txt, hello_world, hello_txt, use_deprecated=False
+    )
+    assert res == MergeFileResult(
+        True, hello_world.path, hello_txt.mode, testrepo.get(hello_txt.id).data.decode()
+    )
+
+    # path switch on theirs
+    res = testrepo.merge_file_from_index(
+        hello_txt, hello_txt, hello_world, use_deprecated=False
+    )
+    assert res == MergeFileResult(
+        True, hello_world.path, hello_txt.mode, testrepo.get(hello_txt.id).data.decode()
+    )
+
+    # path switch on both
+    res = testrepo.merge_file_from_index(
+        hello_txt, hello_world, hello_world, use_deprecated=False
+    )
+    assert res == MergeFileResult(
+        True, None, hello_txt.mode, testrepo.get(hello_txt.id).data.decode()
+    )
+
+    # path switch on ours, executable flag switch on theirs
+    res = testrepo.merge_file_from_index(
+        hello_txt, hello_world, hello_txt_executable, use_deprecated=False
+    )
+    assert res == MergeFileResult(
+        True,
+        hello_world.path,
+        hello_txt_executable.mode,
+        testrepo.get(hello_txt.id).data.decode(),
+    )
+
+    # path switch on theirs, executable flag switch on ours
+    res = testrepo.merge_file_from_index(
+        hello_txt, hello_txt_executable, hello_world, use_deprecated=False
+    )
+    assert res == MergeFileResult(
+        True,
+        hello_world.path,
+        hello_txt_executable.mode,
+        testrepo.get(hello_txt.id).data.decode(),
+    )
diff -pruN 1.17.0-2/test/test_repository_bare.py 1.18.1-1/test/test_repository_bare.py
--- 1.17.0-2/test/test_repository_bare.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/test/test_repository_bare.py	2025-07-26 10:03:19.000000000 +0000
@@ -23,12 +23,12 @@
 # the Free Software Foundation, 51 Franklin Street, Fifth Floor,
 # Boston, MA 02110-1301, USA.
 
-# Standard Library
 import binascii
 import os
-from pathlib import Path
+import pathlib
 import sys
 import tempfile
+
 import pytest
 
 import pygit2
@@ -54,13 +54,13 @@ def test_is_bare(barerepo):
 def test_head(barerepo):
     head = barerepo.head
     assert HEAD_SHA == head.target
-    assert type(head) == pygit2.Reference
+    assert type(head) is pygit2.Reference
     assert not barerepo.head_is_unborn
     assert not barerepo.head_is_detached
 
 
 def test_set_head(barerepo):
-    # Test setting a detatched HEAD.
+    # Test setting a detached HEAD.
     barerepo.set_head(pygit2.Oid(hex=PARENT_SHA))
     assert barerepo.head.target == PARENT_SHA
     # And test setting a normal HEAD.
@@ -94,7 +94,7 @@ def test_write(barerepo):
         barerepo.write(ObjectType.ANY, data)
 
     oid = barerepo.write(ObjectType.BLOB, data)
-    assert type(oid) == pygit2.Oid
+    assert type(oid) is pygit2.Oid
 
 
 def test_contains(barerepo):
@@ -135,7 +135,7 @@ def test_lookup_commit(barerepo):
     assert commit_sha == commit.id
     assert ObjectType.COMMIT == commit.type
     assert commit.message == (
-        'Second test data commit.\n\n' 'This commit has some additional text.\n'
+        'Second test data commit.\n\nThis commit has some additional text.\n'
     )
 
 
@@ -160,7 +160,7 @@ def test_expand_id(barerepo):
     assert commit_sha == expanded
 
 
-@utils.refcount
+@utils.requires_refcount
 def test_lookup_commit_refcount(barerepo):
     start = sys.getrefcount(barerepo)
     commit_sha = '5fe808e8953c12735680c257f56600cb0de44b10'
@@ -173,7 +173,7 @@ def test_lookup_commit_refcount(barerepo
 def test_get_path(barerepo_path):
     barerepo, path = barerepo_path
 
-    directory = Path(barerepo.path).resolve()
+    directory = pathlib.Path(barerepo.path).resolve()
     assert directory == path.resolve()
 
 
@@ -199,7 +199,7 @@ def test_hashfile(barerepo):
     with os.fdopen(handle, 'w') as fh:
         fh.write(data)
     hashed_sha1 = pygit2.hashfile(tempfile_path)
-    Path(tempfile_path).unlink()
+    pathlib.Path(tempfile_path).unlink()
     written_sha1 = barerepo.create_blob(data)
     assert hashed_sha1 == written_sha1
 
diff -pruN 1.17.0-2/test/test_tree.py 1.18.1-1/test/test_tree.py
--- 1.17.0-2/test/test_tree.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/test/test_tree.py	2025-07-26 10:03:19.000000000 +0000
@@ -169,7 +169,7 @@ def test_modify_tree(barerepo):
 def test_iterate_tree(barerepo):
     """
     Testing that we're able to iterate of a Tree object and that the
-    resulting sha strings are consitent with the sha strings we could
+    resulting sha strings are consistent with the sha strings we could
     get with other Tree access methods.
     """
     tree = barerepo[TREE_SHA]
diff -pruN 1.17.0-2/test/utils.py 1.18.1-1/test/utils.py
--- 1.17.0-2/test/utils.py	2025-01-08 09:08:49.000000000 +0000
+++ 1.18.1-1/test/utils.py	2025-07-26 10:03:19.000000000 +0000
@@ -39,7 +39,7 @@ import pytest
 import pygit2
 
 
-requires_future_libgit2 = pytest.mark.skipif(
+requires_future_libgit2 = pytest.mark.xfail(
     pygit2.LIBGIT2_VER < (2, 0, 0),
     reason='This test may work with a future version of libgit2',
 )
@@ -64,12 +64,11 @@ requires_ssh = pytest.mark.skipif(
 
 is_pypy = '__pypy__' in sys.builtin_module_names
 
-fspath = pytest.mark.skipif(
-    is_pypy,
-    reason="PyPy doesn't fully support fspath, see https://foss.heptapod.net/pypy/pypy/-/issues/3168",
-)
+requires_refcount = pytest.mark.skipif(is_pypy, reason='skip refcounts checks in pypy')
 
-refcount = pytest.mark.skipif(is_pypy, reason='skip refcounts checks in pypy')
+fails_in_macos = pytest.mark.xfail(
+    sys.platform == 'darwin', reason='fails in macOS for an unknown reason'
+)
 
 
 def gen_blob_sha1(data):
