Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-pygit2 for openSUSE:Factory 
checked in at 2026-04-09 16:09:31
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-pygit2 (Old)
 and      /work/SRC/openSUSE:Factory/.python-pygit2.new.21863 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-pygit2"

Thu Apr  9 16:09:31 2026 rev:45 rq:1345300 version:1.19.2

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-pygit2/python-pygit2.changes      
2026-03-04 21:08:52.389511767 +0100
+++ /work/SRC/openSUSE:Factory/.python-pygit2.new.21863/python-pygit2.changes   
2026-04-09 16:22:32.561078818 +0200
@@ -1,0 +2,11 @@
+Wed Apr  8 22:26:30 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 1.19.2:
+  * Fix refcount and error handling issues in
+    `filter_register(...)`
+  * Fix config with valueless keys
+  * New `Repository.load_filter_list(...)` and `FilterList`
+  * New `Odb.read_header(...)` and now `Odb.read(...)` returns
+    `enums.ObjectType` instead of int
+
+-------------------------------------------------------------------

Old:
----
  pygit2-1.19.1.tar.gz

New:
----
  pygit2-1.19.2.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-pygit2.spec ++++++
--- /var/tmp/diff_new_pack.VuKpc5/_old  2026-04-09 16:22:33.353111317 +0200
+++ /var/tmp/diff_new_pack.VuKpc5/_new  2026-04-09 16:22:33.353111317 +0200
@@ -19,7 +19,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-pygit2
-Version:        1.19.1
+Version:        1.19.2
 Release:        0
 Summary:        Python bindings for libgit2
 License:        GPL-2.0-only

++++++ pygit2-1.19.1.tar.gz -> pygit2-1.19.2.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/AUTHORS.md new/pygit2-1.19.2/AUTHORS.md
--- old/pygit2-1.19.1/AUTHORS.md        2025-12-29 12:17:27.251583000 +0100
+++ new/pygit2-1.19.2/AUTHORS.md        2026-03-29 15:47:01.114112000 +0200
@@ -138,6 +138,7 @@
     Adam Spiers
     Adrien Nader
     Albin Söderström
+    Alexander Shadchin
     Alexandru Fikl
     Andrew Chin
     Andrew McNulty
@@ -182,6 +183,7 @@
     Hugh Cole-Baker
     Isabella Stephens
     Jacob Swanson
+    Jah-yee
     Jasper Lievisse Adriaanse
     Jimisola Laursen
     Jiri Benc
@@ -237,6 +239,7 @@
     Timo Röhling
     Victor Florea
     Vladimir Rutsky
+    Vruyr Gyolchanyan
     William Schueller
     Wim Jeantine-Glenn
     Yu Jianjian
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/CHANGELOG.md 
new/pygit2-1.19.2/CHANGELOG.md
--- old/pygit2-1.19.1/CHANGELOG.md      2025-12-29 12:17:27.251583000 +0100
+++ new/pygit2-1.19.2/CHANGELOG.md      2026-03-29 15:47:01.114112000 +0200
@@ -1,3 +1,22 @@
+# 1.19.2 (2026-03-29)
+
+- Fix refcount and error handling issues in `filter_register(...)`
+
+- Fix config with valueless keys
+  [#1457](https://github.com/libgit2/pygit2/pull/1457)
+
+- New `Repository.load_filter_list(...)` and `FilterList`
+  [#1444](https://github.com/libgit2/pygit2/pull/1444)
+
+- New `Odb.read_header(...)` and now `Odb.read(...)` returns 
`enums.ObjectType` instead of int
+  [#1450](https://github.com/libgit2/pygit2/pull/1450)
+
+- Build and CI fixes
+  [#1446](https://github.com/libgit2/pygit2/pull/1446)
+  [#1448](https://github.com/libgit2/pygit2/pull/1448)
+  [#1455](https://github.com/libgit2/pygit2/pull/1455)
+
+
 # 1.19.1 (2025-12-29)
 
 - Update wheels to libgit2 1.9.2 and OpenSSL 3.5
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/PKG-INFO new/pygit2-1.19.2/PKG-INFO
--- old/pygit2-1.19.1/PKG-INFO  2025-12-29 12:17:33.921532600 +0100
+++ new/pygit2-1.19.2/PKG-INFO  2026-03-29 15:47:05.747294000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: pygit2
-Version: 1.19.1
+Version: 1.19.2
 Summary: Python bindings for libgit2.
 Home-page: https://github.com/libgit2/pygit2
 Maintainer: J. David Ibáñez
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/pygit2/__init__.py 
new/pygit2-1.19.2/pygit2/__init__.py
--- old/pygit2-1.19.1/pygit2/__init__.py        2025-12-29 12:17:27.253583000 
+0100
+++ new/pygit2-1.19.2/pygit2/__init__.py        2026-03-29 15:47:01.115543000 
+0200
@@ -286,7 +286,6 @@
     _cache_enums,
     discover_repository,
     filter_register,
-    filter_unregister,
     hash,
     hashfile,
     init_file_backend,
@@ -545,6 +544,27 @@
     return Repository._from_c(crepo[0], owned=True)
 
 
+def filter_unregister(name: str) -> None:
+    """
+    Unregister the given filter.
+
+    Note that the filter registry is not thread safe. Any registering or
+    deregistering of filters should be done outside of any possible usage
+    of the filters.
+
+    In particular, any FilterLists that use the filter must have been garbage
+    collected before you can unregister the filter.
+    """
+    from .filter import FilterList
+
+    if FilterList._is_filter_in_use(name):
+        raise RuntimeError(f"filter still in use: '{name}'")
+
+    c_name = to_bytes(name)
+    err = C.git_filter_unregister(c_name)
+    check_error(err)
+
+
 tree_entry_key = functools.cmp_to_key(tree_entry_cmp)
 
 settings = Settings()
@@ -610,7 +630,6 @@
     # Low Level API (not present in .pyi)
     'FilterSource',
     'filter_register',
-    'filter_unregister',
     'GIT_APPLY_LOCATION_BOTH',
     'GIT_APPLY_LOCATION_INDEX',
     'GIT_APPLY_LOCATION_WORKDIR',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/pygit2/_build.py 
new/pygit2-1.19.2/pygit2/_build.py
--- old/pygit2-1.19.1/pygit2/_build.py  2025-12-29 12:17:27.253583000 +0100
+++ new/pygit2-1.19.2/pygit2/_build.py  2026-03-29 15:47:01.116144400 +0200
@@ -34,7 +34,7 @@
 #
 # The version number of pygit2
 #
-__version__ = '1.19.1'
+__version__ = '1.19.2'
 
 
 #
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/pygit2/_libgit2/ffi.pyi 
new/pygit2-1.19.2/pygit2/_libgit2/ffi.pyi
--- old/pygit2-1.19.1/pygit2/_libgit2/ffi.pyi   2025-12-29 12:17:27.253583000 
+0100
+++ new/pygit2-1.19.2/pygit2/_libgit2/ffi.pyi   2026-03-29 15:47:01.116144400 
+0200
@@ -123,6 +123,10 @@
     # incomplete
     pass
 
+class GitBlobC:
+    # incomplete
+    pass
+
 class GitMergeOptionsC:
     file_favor: int
     flags: int
@@ -177,6 +181,10 @@
 class GitDescribeResultC:
     pass
 
+class GitFilterListC:
+    # opaque struct
+    pass
+
 class GitIndexC:
     pass
 
@@ -264,6 +272,8 @@
 @overload
 def new(a: Literal['git_blame **']) -> _Pointer[GitBlameC]: ...
 @overload
+def new(a: Literal['git_blob **']) -> _Pointer[GitBlobC]: ...
+@overload
 def new(a: Literal['git_clone_options *']) -> GitCloneOptionsC: ...
 @overload
 def new(a: Literal['git_merge_options *']) -> GitMergeOptionsC: ...
@@ -318,6 +328,8 @@
 @overload
 def new(a: Literal['git_signature **']) -> _Pointer[GitSignatureC]: ...
 @overload
+def new(a: Literal['git_filter_list **']) -> _Pointer[GitFilterListC]: ...
+@overload
 def new(a: Literal['int *']) -> int_c: ...
 @overload
 def new(a: Literal['int64_t *']) -> int64_t: ...
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/pygit2/_pygit2.pyi 
new/pygit2-1.19.2/pygit2/_pygit2.pyi
--- old/pygit2-1.19.1/pygit2/_pygit2.pyi        2025-12-29 12:17:27.253583000 
+0100
+++ new/pygit2-1.19.2/pygit2/_pygit2.pyi        2026-03-29 15:47:01.116144400 
+0200
@@ -518,7 +518,8 @@
     def add_backend(self, backend: OdbBackend, priority: int) -> None: ...
     def add_disk_alternate(self, path: str | Path) -> None: ...
     def exists(self, oid: _OidArg) -> bool: ...
-    def read(self, oid: _OidArg) -> tuple[int, bytes]: ...
+    def read(self, oid: _OidArg) -> tuple[ObjectType, bytes]: ...
+    def read_header(self, oid: _OidArg) -> tuple[ObjectType, int]: ...
     def write(self, type: int, data: bytes | str) -> Oid: ...
     def __contains__(self, other: _OidArg) -> bool: ...
     def __iter__(self) -> Iterator[Oid]: ...  # Odb_as_iter
@@ -857,6 +858,5 @@
 def tree_entry_cmp(a: Object, b: Object) -> int: ...
 def _cache_enums() -> None: ...
 def filter_register(name: str, filter: type[Filter]) -> None: ...
-def filter_unregister(name: str) -> None: ...
 
 _OidArg = str | Oid
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/pygit2/_run.py 
new/pygit2-1.19.2/pygit2/_run.py
--- old/pygit2-1.19.1/pygit2/_run.py    2025-12-29 12:17:27.253583000 +0100
+++ new/pygit2-1.19.2/pygit2/_run.py    2026-03-29 15:47:01.116144400 +0200
@@ -77,6 +77,7 @@
     'net.h',
     'refspec.h',
     'repository.h',
+    'filter.h',
     'commit.h',
     'revert.h',
     'stash.h',
@@ -96,6 +97,7 @@
 C_PREAMBLE = """\
 #include <git2.h>
 #include <git2/sys/repository.h>
+#include <git2/sys/filter.h>
 """
 
 # ffi
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/pygit2/config.py 
new/pygit2-1.19.2/pygit2/config.py
--- old/pygit2-1.19.1/pygit2/config.py  2025-12-29 12:17:27.254583100 +0100
+++ new/pygit2-1.19.2/pygit2/config.py  2026-03-29 15:47:01.116144400 +0200
@@ -73,7 +73,7 @@
 
 
 class ConfigMultivarIterator(ConfigIterator):
-    def __next__(self) -> str:  # type: ignore[override]
+    def __next__(self) -> str | None:  # type: ignore[override]
         entry = self._next_entry()
         return entry.value
 
@@ -137,7 +137,7 @@
 
         return True
 
-    def __getitem__(self, key: str | bytes) -> str:
+    def __getitem__(self, key: str | bytes) -> str | None:
         """
         When using the mapping interface, the value is returned as a string. In
         order to apply the git-config parsing rules, you can use
@@ -365,8 +365,8 @@
         return ffi.string(self._entry.name)
 
     @cached_property
-    def raw_value(self) -> bytes:
-        return ffi.string(self.c_value)
+    def raw_value(self) -> bytes | None:
+        return ffi.string(self.c_value) if self.c_value != ffi.NULL else None
 
     @cached_property
     def level(self) -> int:
@@ -379,6 +379,6 @@
         return self.raw_name.decode('utf-8')
 
     @property
-    def value(self) -> str:
+    def value(self) -> str | None:
         """The entry's value as a string."""
-        return self.raw_value.decode('utf-8')
+        return self.raw_value.decode('utf-8') if self.raw_value is not None 
else None
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/pygit2/decl/filter.h 
new/pygit2-1.19.2/pygit2/decl/filter.h
--- old/pygit2-1.19.1/pygit2/decl/filter.h      1970-01-01 01:00:00.000000000 
+0100
+++ new/pygit2-1.19.2/pygit2/decl/filter.h      2026-03-29 15:47:01.116711900 
+0200
@@ -0,0 +1,49 @@
+typedef enum {
+       GIT_FILTER_TO_WORKTREE = ...,
+       GIT_FILTER_TO_ODB = ...,
+} git_filter_mode_t;
+
+typedef enum {
+       GIT_FILTER_DEFAULT = ...,
+       GIT_FILTER_ALLOW_UNSAFE = ...,
+       GIT_FILTER_NO_SYSTEM_ATTRIBUTES = ...,
+       GIT_FILTER_ATTRIBUTES_FROM_HEAD = ...,
+       GIT_FILTER_ATTRIBUTES_FROM_COMMIT = ...,
+} git_filter_flag_t;
+
+int git_filter_unregister(
+       const char *name);
+
+int git_filter_list_load(
+       git_filter_list **filters,
+       git_repository *repo,
+       git_blob *blob,
+       const char *path,
+       git_filter_mode_t mode,
+       uint32_t flags);
+
+int git_filter_list_contains(
+       git_filter_list *filters,
+       const char *name);
+
+int git_filter_list_apply_to_buffer(
+       git_buf *out,
+       git_filter_list *filters,
+       const char* in,
+       size_t in_len);
+
+int git_filter_list_apply_to_file(
+       git_buf *out,
+       git_filter_list *filters,
+       git_repository *repo,
+       const char *path);
+
+int git_filter_list_apply_to_blob(
+       git_buf *out,
+       git_filter_list *filters,
+       git_blob *blob);
+
+size_t git_filter_list_length(
+       const git_filter_list *fl);
+
+void git_filter_list_free(git_filter_list *filters);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/pygit2/decl/types.h 
new/pygit2-1.19.2/pygit2/decl/types.h
--- old/pygit2-1.19.1/pygit2/decl/types.h       2025-12-29 12:17:27.255583000 
+0100
+++ new/pygit2-1.19.2/pygit2/decl/types.h       2026-03-29 15:47:01.116711900 
+0200
@@ -1,6 +1,8 @@
+typedef struct git_blob git_blob;
 typedef struct git_commit git_commit;
 typedef struct git_annotated_commit git_annotated_commit;
 typedef struct git_config git_config;
+typedef struct git_filter_list git_filter_list;
 typedef struct git_index git_index;
 typedef struct git_index_conflict_iterator git_index_conflict_iterator;
 typedef struct git_object git_object;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/pygit2/filter.py 
new/pygit2-1.19.2/pygit2/filter.py
--- old/pygit2-1.19.1/pygit2/filter.py  2025-12-29 12:17:27.255583000 +0100
+++ new/pygit2-1.19.2/pygit2/filter.py  2026-03-29 15:47:01.116711900 +0200
@@ -23,9 +23,20 @@
 # the Free Software Foundation, 51 Franklin Street, Fifth Floor,
 # Boston, MA 02110-1301, USA.
 
+from __future__ import annotations
+
+import weakref
 from collections.abc import Callable
+from typing import TYPE_CHECKING
 
-from ._pygit2 import FilterSource
+from ._pygit2 import Blob, FilterSource
+from .errors import check_error
+from .ffi import C, ffi
+from .utils import to_bytes
+
+if TYPE_CHECKING:
+    from ._libgit2.ffi import GitFilterListC
+    from .repository import BaseRepository
 
 
 class Filter:
@@ -107,3 +118,90 @@
                 Any remaining filtered output data must be written to
                 `write_next` before returning.
         """
+
+
+class FilterList:
+    _all_filter_lists: set[weakref.ReferenceType[FilterList]] = set()
+
+    _pointer: GitFilterListC
+
+    @classmethod
+    def _from_c(cls, ptr: GitFilterListC):
+        if ptr == ffi.NULL:
+            return None
+
+        fl = cls.__new__(cls)
+        fl._pointer = ptr
+
+        # Keep track of this FilterList until it's garbage collected. This lets
+        # `filter_unregister` ensure the user isn't trying to delete a filter
+        # that's still in use.
+        ref = weakref.ref(fl, FilterList._all_filter_lists.remove)
+        FilterList._all_filter_lists.add(ref)
+
+        return fl
+
+    @classmethod
+    def _is_filter_in_use(cls, name: str) -> bool:
+        for ref in cls._all_filter_lists:
+            fl = ref()
+            if fl is not None and name in fl:
+                return True
+        return False
+
+    def __contains__(self, name: str) -> bool:
+        if not isinstance(name, str):
+            raise TypeError('argument must be str')
+        c_name = to_bytes(name)
+        result = C.git_filter_list_contains(self._pointer, c_name)
+        return bool(result)
+
+    def __len__(self) -> int:
+        return C.git_filter_list_length(self._pointer)
+
+    def apply_to_buffer(self, data: bytes) -> bytes:
+        """
+        Apply a filter list to a data buffer.
+        Return the filtered contents.
+        """
+        buf = ffi.new('git_buf *')
+        err = C.git_filter_list_apply_to_buffer(buf, self._pointer, data, 
len(data))
+        check_error(err)
+        try:
+            return ffi.string(buf.ptr)
+        finally:
+            C.git_buf_dispose(buf)
+
+    def apply_to_file(self, repo: BaseRepository, path: str) -> bytes:
+        """
+        Apply a filter list to the contents of a file on disk.
+        Return the filtered contents.
+        """
+        buf = ffi.new('git_buf *')
+        c_path = to_bytes(path)
+        err = C.git_filter_list_apply_to_file(buf, self._pointer, repo._repo, 
c_path)
+        check_error(err)
+        try:
+            return ffi.string(buf.ptr)
+        finally:
+            C.git_buf_dispose(buf)
+
+    def apply_to_blob(self, blob: Blob) -> bytes:
+        """
+        Apply a filter list to a data buffer.
+        Return the filtered contents.
+        """
+        buf = ffi.new('git_buf *')
+
+        c_blob = ffi.new('git_blob **')
+        ffi.buffer(c_blob)[:] = blob._pointer[:]
+
+        err = C.git_filter_list_apply_to_blob(buf, self._pointer, c_blob[0])
+        check_error(err)
+        try:
+            return ffi.string(buf.ptr)
+        finally:
+            C.git_buf_dispose(buf)
+
+    def __del__(self):
+        C.git_filter_list_free(self._pointer)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/pygit2/repository.py 
new/pygit2-1.19.2/pygit2/repository.py
--- old/pygit2-1.19.1/pygit2/repository.py      2025-12-29 12:17:27.256583000 
+0100
+++ new/pygit2-1.19.2/pygit2/repository.py      2026-03-29 15:47:01.116937600 
+0200
@@ -64,6 +64,7 @@
     DescribeStrategy,
     DiffOption,
     FileMode,
+    FilterMode,
     MergeFavor,
     MergeFileFlag,
     MergeFlag,
@@ -73,6 +74,7 @@
 )
 from .errors import check_error
 from .ffi import C, ffi
+from .filter import FilterList
 from .index import Index, IndexEntry, MergeFileResult
 from .packbuilder import PackBuilder
 from .references import References
@@ -235,6 +237,31 @@
         oid = Oid(raw=bytes(ffi.buffer(c_oid.id)[:]))
         return oid
 
+    def load_filter_list(
+        self, path: str, mode: FilterMode = FilterMode.TO_ODB
+    ) -> FilterList | None:
+        """
+        Load the filter list for a given path.
+        May return None if there are no filters to apply to this path.
+
+        Parameters:
+
+        path
+            Relative path of the file to be filtered
+
+        mode
+            Filtering direction: ODB to worktree (SMUDGE), or worktree to ODB
+            (CLEAN).
+        """
+        c_filters = ffi.new('git_filter_list **')
+        c_path = to_bytes(path)
+        c_mode = int(mode)
+
+        err = C.git_filter_list_load(c_filters, self._repo, ffi.NULL, c_path, 
c_mode, 0)
+        check_error(err)
+        fl = FilterList._from_c(c_filters[0])
+        return fl
+
     def __iter__(self) -> Iterator[Oid]:
         return iter(self.odb)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/pyproject.toml 
new/pygit2-1.19.2/pyproject.toml
--- old/pygit2-1.19.1/pyproject.toml    2025-12-29 12:17:27.256583000 +0100
+++ new/pygit2-1.19.2/pyproject.toml    2026-03-29 15:47:01.116937600 +0200
@@ -1,5 +1,6 @@
 [build-system]
-requires = ["setuptools", "wheel"]
+requires = ["setuptools"]
+build-backend = "setuptools.build_meta"
 
 [tool.cibuildwheel]
 enable = ["pypy"]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/src/branch.c 
new/pygit2-1.19.2/src/branch.c
--- old/pygit2-1.19.1/src/branch.c      2025-12-29 12:17:27.257583000 +0100
+++ new/pygit2-1.19.2/src/branch.c      2026-03-29 15:47:01.117937600 +0200
@@ -275,7 +275,7 @@
 }
 
 
-PyMethodDef Branch_methods[] = {
+static PyMethodDef Branch_methods[] = {
     METHOD(Branch, delete, METH_NOARGS),
     METHOD(Branch, is_head, METH_NOARGS),
     METHOD(Branch, is_checked_out, METH_NOARGS),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/src/diff.c new/pygit2-1.19.2/src/diff.c
--- old/pygit2-1.19.1/src/diff.c        2025-12-29 12:17:27.257583000 +0100
+++ new/pygit2-1.19.2/src/diff.c        2026-03-29 15:47:01.117937600 +0200
@@ -236,7 +236,7 @@
     {NULL}
 };
 
-PyMethodDef DiffFile_methods[] = {
+static PyMethodDef DiffFile_methods[] = {
     METHOD(DiffFile, from_c, METH_STATIC | METH_O),
     {NULL},
 };
@@ -914,7 +914,7 @@
     PyObject_Del(self);
 }
 
-PyMethodDef DiffStats_methods[] = {
+static PyMethodDef DiffStats_methods[] = {
     METHOD(DiffStats, format, METH_VARARGS | METH_KEYWORDS),
     {NULL}
 };
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/src/filter.c 
new/pygit2-1.19.2/src/filter.c
--- old/pygit2-1.19.1/src/filter.c      2025-12-29 12:17:27.257583000 +0100
+++ new/pygit2-1.19.2/src/filter.c      2026-03-29 15:47:01.117937600 +0200
@@ -551,7 +551,9 @@
 void pygit2_filter_shutdown(git_filter *self)
 {
     pygit2_filter *filter = (pygit2_filter *)self;
+
     PyGILState_STATE gil = PyGILState_Ensure();
+    free((void*)filter->filter.attributes);
     Py_DECREF(filter->py_filter_cls);
     free(filter);
     PyGILState_Release(gil);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/src/mailmap.c 
new/pygit2-1.19.2/src/mailmap.c
--- old/pygit2-1.19.1/src/mailmap.c     2025-12-29 12:17:27.258583000 +0100
+++ new/pygit2-1.19.2/src/mailmap.c     2026-03-29 15:47:01.117937600 +0200
@@ -188,7 +188,7 @@
 }
 
 
-PyMethodDef Mailmap_methods[] = {
+static PyMethodDef Mailmap_methods[] = {
     METHOD(Mailmap, add_entry, METH_VARARGS | METH_KEYWORDS),
     METHOD(Mailmap, resolve, METH_VARARGS),
     METHOD(Mailmap, resolve_signature, METH_VARARGS),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/src/note.c new/pygit2-1.19.2/src/note.c
--- old/pygit2-1.19.1/src/note.c        2025-12-29 12:17:27.258583000 +0100
+++ new/pygit2-1.19.2/src/note.c        2026-03-29 15:47:01.117937600 +0200
@@ -99,7 +99,7 @@
 }
 
 
-PyMethodDef Note_methods[] = {
+static PyMethodDef Note_methods[] = {
     METHOD(Note, remove, METH_VARARGS),
     {NULL}
 };
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/src/object.c 
new/pygit2-1.19.2/src/object.c
--- old/pygit2-1.19.1/src/object.c      2025-12-29 12:17:27.258583000 +0100
+++ new/pygit2-1.19.2/src/object.c      2026-03-29 15:47:01.117937600 +0200
@@ -302,7 +302,7 @@
     {NULL}
 };
 
-PyMethodDef Object_methods[] = {
+static PyMethodDef Object_methods[] = {
     METHOD(Object, read_raw, METH_NOARGS),
     METHOD(Object, peel, METH_O),
     {NULL}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/src/odb.c new/pygit2-1.19.2/src/odb.c
--- old/pygit2-1.19.1/src/odb.c 2025-12-29 12:17:27.258583000 +0100
+++ new/pygit2-1.19.2/src/odb.c 2026-03-29 15:47:01.117937600 +0200
@@ -37,6 +37,8 @@
 
 extern PyTypeObject OdbBackendType;
 
+extern PyObject *ObjectTypeEnum;
+
 static git_otype
 int_to_loose_object_type(int type_id)
 {
@@ -170,7 +172,7 @@
 }
 
 PyDoc_STRVAR(Odb_read__doc__,
-  "read(oid) -> type, data, size\n"
+  "read(oid: Oid) -> tuple[enums.ObjectType, bytes]\n"
   "\n"
   "Read raw object data from the object db.");
 
@@ -180,6 +182,8 @@
     git_oid oid;
     git_odb_object *obj;
     size_t len;
+    git_object_t type;
+    PyObject* type_enum;
     PyObject* tuple;
 
     len = py_oid_to_git_oid(py_hex, &oid);
@@ -190,9 +194,13 @@
     if (obj == NULL)
         return NULL;
 
+    // Convert type to ObjectType enum
+    type = git_odb_object_type(obj);
+    type_enum = pygit2_enum(ObjectTypeEnum, type);
+
     tuple = Py_BuildValue(
-        "(ny#)",
-        git_odb_object_type(obj),
+        "(Oy#)",
+        type_enum,
         git_odb_object_data(obj),
         git_odb_object_size(obj));
 
@@ -200,6 +208,44 @@
     return tuple;
 }
 
+PyDoc_STRVAR(Odb_read_header__doc__,
+    "read_header(oid: Oid) -> tuple[enums.ObjectType, size\n"
+    "\n"
+    "Read the header of an object from the database, without reading its 
full\n"
+    "contents.\n"
+    "\n"
+    "The header includes the type and the length of an object.\n"
+    "\n"
+    "Note that most backends do not support reading only the header of an 
object,\n"
+    "so the whole object may be read and then the header will be returned.");
+
+PyObject *
+Odb_read_header(Odb *self, PyObject *py_hex)
+{
+    git_oid oid;
+    int err;
+    size_t len;
+    git_object_t type;
+    PyObject* type_enum;
+    PyObject* tuple;
+
+    len = py_oid_to_git_oid(py_hex, &oid);
+    if (len == 0)
+        return NULL;
+
+    err = git_odb_read_header(&len, &type, self->odb, &oid);
+    if (err != 0) {
+        Error_set_oid(err, &oid, len);
+        return NULL;
+    }
+
+    // Convert type to ObjectType enum
+    type_enum = pygit2_enum(ObjectTypeEnum, type);
+
+    tuple = Py_BuildValue("(On)", type_enum, len);
+    return tuple;
+}
+
 PyDoc_STRVAR(Odb_write__doc__,
     "write(type: int, data: bytes) -> Oid\n"
     "\n"
@@ -298,9 +344,10 @@
 }
 
 
-PyMethodDef Odb_methods[] = {
+static PyMethodDef Odb_methods[] = {
     METHOD(Odb, add_disk_alternate, METH_O),
     METHOD(Odb, read, METH_O),
+    METHOD(Odb, read_header, METH_O),
     METHOD(Odb, write, METH_VARARGS),
     METHOD(Odb, exists, METH_O),
     METHOD(Odb, add_backend, METH_VARARGS),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/src/odb_backend.c 
new/pygit2-1.19.2/src/odb_backend.c
--- old/pygit2-1.19.1/src/odb_backend.c 2025-12-29 12:17:27.258583000 +0100
+++ new/pygit2-1.19.2/src/odb_backend.c 2026-03-29 15:47:01.118937500 +0200
@@ -518,7 +518,7 @@
  * - readstream
  * - freshen
  */
-PyMethodDef OdbBackend_methods[] = {
+static PyMethodDef OdbBackend_methods[] = {
     METHOD(OdbBackend, read, METH_O),
     METHOD(OdbBackend, read_prefix, METH_O),
     METHOD(OdbBackend, read_header, METH_O),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/src/patch.c 
new/pygit2-1.19.2/src/patch.c
--- old/pygit2-1.19.1/src/patch.c       2025-12-29 12:17:27.258583000 +0100
+++ new/pygit2-1.19.2/src/patch.c       2026-03-29 15:47:01.118937500 +0200
@@ -235,7 +235,7 @@
 }
 
 
-PyMethodDef Patch_methods[] = {
+static PyMethodDef Patch_methods[] = {
     {"create_from", (PyCFunction) Patch_create_from,
       METH_KEYWORDS | METH_VARARGS | METH_STATIC, Patch_create_from__doc__},
     {NULL}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/src/pygit2.c 
new/pygit2-1.19.2/src/pygit2.c
--- old/pygit2-1.19.1/src/pygit2.c      2025-12-29 12:17:27.258583000 +0100
+++ new/pygit2-1.19.2/src/pygit2.c      2026-03-29 15:47:01.118937500 +0200
@@ -47,6 +47,7 @@
 PyObject *FileStatusEnum;
 PyObject *MergeAnalysisEnum;
 PyObject *MergePreferenceEnum;
+PyObject *ObjectTypeEnum;
 PyObject *ReferenceTypeEnum;
 
 extern PyTypeObject RepositoryType;
@@ -304,72 +305,53 @@
     int priority = GIT_FILTER_DRIVER_PRIORITY;
     char *keywords[] = {"name", "filter_cls", "priority", NULL};
     pygit2_filter *filter;
-    PyObject *py_attrs;
-    PyObject *result = Py_None;
     int err;
 
     if (!PyArg_ParseTupleAndKeywords(args, kwds, "s#O|i", keywords,
                                      &name, &size, &py_filter_cls, &priority))
         return NULL;
 
-    py_attrs = PyObject_GetAttrString(py_filter_cls, "attributes");
-    if (py_attrs == NULL)
+    /* py_attrs = py_filter_cls.attributes */
+    PyObject* py_attrs = PyObject_GetAttrString(py_filter_cls, "attributes");
+    if (py_attrs == NULL) {
         return NULL;
+    }
+    char* attrs = pgit_strdup(py_attrs);
+    Py_DECREF(py_attrs);
+    if (attrs == NULL) {
+        return NULL;
+    }
 
+    /* allocate memory */
     filter = malloc(sizeof(pygit2_filter));
-    if (filter == NULL)
-    {
-        return PyExc_MemoryError;
+    if (filter == NULL) {
+        free(attrs);
+        return PyErr_NoMemory();
     }
     memset(filter, 0, sizeof(pygit2_filter));
-    git_filter_init(&filter->filter, GIT_FILTER_VERSION);
 
-    filter->filter.attributes = PyUnicode_AsUTF8(py_attrs);
+    /* initialize git_filter */
+    git_filter_init(&filter->filter, GIT_FILTER_VERSION);
+    filter->filter.attributes = attrs;
     filter->filter.shutdown = pygit2_filter_shutdown;
     filter->filter.check = pygit2_filter_check;
     filter->filter.stream = pygit2_filter_stream;
     filter->filter.cleanup = pygit2_filter_cleanup;
-    filter->py_filter_cls = py_filter_cls;
-    Py_INCREF(py_filter_cls);
-
-    if ((err = git_filter_register(name, &filter->filter, priority)) < 0)
-        goto error;
-
-    goto done;
-
-error:
-    Py_DECREF(py_filter_cls);
-    free(filter);
-done:
-    Py_DECREF(py_attrs);
-    return result;
-}
-
-PyDoc_STRVAR(filter_unregister__doc__,
-    "filter_unregister(name: str) -> None\n"
-    "\n"
-    "Unregister the given filter.\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"
-    "of the filters.\n");
 
-PyObject *
-filter_unregister(PyObject *self, PyObject *args)
-{
-    const char *name;
-    Py_ssize_t size;
-    int err;
+    /* keep reference to Python filter */
+    filter->py_filter_cls = py_filter_cls;
 
-    if (!PyArg_ParseTuple(args, "s#", &name, &size))
-        return NULL;
-    if ((err = git_filter_unregister(name)) < 0)
+    /* git register filter */
+    if ((err = git_filter_register(name, &filter->filter, priority)) < 0) {
+        free(attrs);
+        free(filter);
         return Error_set(err);
+    }
 
+    Py_INCREF(py_filter_cls);  /* libgit2 now owns this reference, will decref 
in shutdown */
     Py_RETURN_NONE;
 }
 
-
 static void
 forget_enums(void)
 {
@@ -379,6 +361,7 @@
     Py_CLEAR(FileStatusEnum);
     Py_CLEAR(MergeAnalysisEnum);
     Py_CLEAR(MergePreferenceEnum);
+    Py_CLEAR(ObjectTypeEnum);
     Py_CLEAR(ReferenceTypeEnum);
 }
 
@@ -414,6 +397,7 @@
     CACHE_PYGIT2_ENUM(FileStatus);
     CACHE_PYGIT2_ENUM(MergeAnalysis);
     CACHE_PYGIT2_ENUM(MergePreference);
+    CACHE_PYGIT2_ENUM(ObjectType);
     CACHE_PYGIT2_ENUM(ReferenceType);
 
 #undef CACHE_PYGIT2_ENUM
@@ -433,7 +417,7 @@
 }
 
 
-PyMethodDef module_methods[] = {
+static PyMethodDef module_methods[] = {
     {"discover_repository", discover_repository, METH_VARARGS, 
discover_repository__doc__},
     {"hash", hash, METH_VARARGS, hash__doc__},
     {"hashfile", hashfile, METH_VARARGS, hashfile__doc__},
@@ -441,7 +425,6 @@
     {"reference_is_valid_name", reference_is_valid_name, METH_O, 
reference_is_valid_name__doc__},
     {"tree_entry_cmp", tree_entry_cmp, METH_VARARGS, tree_entry_cmp__doc__},
     {"filter_register", (PyCFunction)filter_register, METH_VARARGS | 
METH_KEYWORDS, filter_register__doc__},
-    {"filter_unregister", filter_unregister, METH_VARARGS, 
filter_unregister__doc__},
     {"_cache_enums", _cache_enums, METH_NOARGS, _cache_enums__doc__},
     {NULL}
 };
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/src/refdb.c 
new/pygit2-1.19.2/src/refdb.c
--- old/pygit2-1.19.1/src/refdb.c       2025-12-29 12:17:27.258583000 +0100
+++ new/pygit2-1.19.2/src/refdb.c       2026-03-29 15:47:01.118937500 +0200
@@ -125,13 +125,11 @@
     return wrap_refdb(refdb);
 }
 
-PyMethodDef Refdb_methods[] = {
+static PyMethodDef Refdb_methods[] = {
     METHOD(Refdb, compress, METH_NOARGS),
     METHOD(Refdb, set_backend, METH_O),
-    {"new", (PyCFunction) Refdb_new,
-      METH_O | METH_STATIC, Refdb_new__doc__},
-    {"open", (PyCFunction) Refdb_open,
-      METH_O | METH_STATIC, Refdb_open__doc__},
+    {"new", (PyCFunction) Refdb_new, METH_O | METH_STATIC, Refdb_new__doc__},
+    {"open", (PyCFunction) Refdb_open, METH_O | METH_STATIC, 
Refdb_open__doc__},
     {NULL}
 };
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/src/refdb_backend.c 
new/pygit2-1.19.2/src/refdb_backend.c
--- old/pygit2-1.19.1/src/refdb_backend.c       2025-12-29 12:17:27.259583000 
+0100
+++ new/pygit2-1.19.2/src/refdb_backend.c       2026-03-29 15:47:01.118937500 
+0200
@@ -772,7 +772,7 @@
     }
 }
 
-PyMethodDef RefdbBackend_methods[] = {
+static PyMethodDef RefdbBackend_methods[] = {
     METHOD(RefdbBackend, exists, METH_O),
     METHOD(RefdbBackend, lookup, METH_O),
     METHOD(RefdbBackend, write, METH_VARARGS),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/src/reference.c 
new/pygit2-1.19.2/src/reference.c
--- old/pygit2-1.19.1/src/reference.c   2025-12-29 12:17:27.259583000 +0100
+++ new/pygit2-1.19.2/src/reference.c   2026-03-29 15:47:01.118937500 +0200
@@ -658,7 +658,7 @@
     0,                                         /* tp_new            */
 };
 
-PyMethodDef Reference_methods[] = {
+static PyMethodDef Reference_methods[] = {
     METHOD(Reference, delete, METH_NOARGS),
     METHOD(Reference, rename, METH_O),
     METHOD(Reference, resolve, METH_NOARGS),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/src/repository.c 
new/pygit2-1.19.2/src/repository.c
--- old/pygit2-1.19.1/src/repository.c  2025-12-29 12:17:27.259583000 +0100
+++ new/pygit2-1.19.2/src/repository.c  2026-03-29 15:47:01.118937500 +0200
@@ -2423,7 +2423,7 @@
     }
 }
 
-PyMethodDef Repository_methods[] = {
+static PyMethodDef Repository_methods[] = {
     METHOD(Repository, create_blob, METH_VARARGS),
     METHOD(Repository, create_blob_fromworkdir, METH_O),
     METHOD(Repository, create_blob_fromdisk, METH_O),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/src/tag.c new/pygit2-1.19.2/src/tag.c
--- old/pygit2-1.19.1/src/tag.c 2025-12-29 12:17:27.259583000 +0100
+++ new/pygit2-1.19.2/src/tag.c 2026-03-29 15:47:01.118937500 +0200
@@ -142,7 +142,7 @@
     return PyBytes_FromString(message);
 }
 
-PyMethodDef Tag_methods[] = {
+static PyMethodDef Tag_methods[] = {
     METHOD(Tag, get_object, METH_NOARGS),
     {NULL}
 };
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/src/tree.c new/pygit2-1.19.2/src/tree.c
--- old/pygit2-1.19.1/src/tree.c        2025-12-29 12:17:27.259583000 +0100
+++ new/pygit2-1.19.2/src/tree.c        2026-03-29 15:47:01.119937700 +0200
@@ -405,7 +405,7 @@
     0,                            /* mp_ass_subscript */
 };
 
-PyMethodDef Tree_methods[] = {
+static PyMethodDef Tree_methods[] = {
     METHOD(Tree, diff_to_tree, METH_VARARGS | METH_KEYWORDS),
     METHOD(Tree, diff_to_workdir, METH_VARARGS | METH_KEYWORDS),
     METHOD(Tree, diff_to_index, METH_VARARGS | METH_KEYWORDS),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/src/treebuilder.c 
new/pygit2-1.19.2/src/treebuilder.c
--- old/pygit2-1.19.1/src/treebuilder.c 2025-12-29 12:17:27.260583000 +0100
+++ new/pygit2-1.19.2/src/treebuilder.c 2026-03-29 15:47:01.119937700 +0200
@@ -160,7 +160,7 @@
     Py_RETURN_NONE;
 }
 
-PyMethodDef TreeBuilder_methods[] = {
+static PyMethodDef TreeBuilder_methods[] = {
     METHOD(TreeBuilder, clear, METH_NOARGS),
     METHOD(TreeBuilder, get, METH_O),
     METHOD(TreeBuilder, insert, METH_VARARGS),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/src/utils.c 
new/pygit2-1.19.2/src/utils.c
--- old/pygit2-1.19.1/src/utils.c       2025-12-29 12:17:27.260583000 +0100
+++ new/pygit2-1.19.2/src/utils.c       2026-03-29 15:47:01.119937700 +0200
@@ -151,6 +151,35 @@
 }
 
 
+char*
+pgit_strdup(PyObject *value)
+{
+    const char *str;
+    char *copy;
+    size_t len;
+
+    if (PyUnicode_Check(value)) {
+        str = PyUnicode_AsUTF8(value);
+        if (str == NULL)
+            return NULL;
+        len = strlen(str);
+    }
+    else {
+        Error_type_error("unexpected %.200s", value);
+        return NULL;
+    }
+
+    copy = malloc(len + 1);
+    if (copy == NULL) {
+        PyErr_NoMemory();
+        return NULL;
+    }
+
+    memcpy(copy, str, len + 1);
+    return copy;
+}
+
+
 static git_otype
 py_type_to_git_type(PyTypeObject *py_type)
 {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/src/utils.h 
new/pygit2-1.19.2/src/utils.h
--- old/pygit2-1.19.1/src/utils.h       2025-12-29 12:17:27.260583000 +0100
+++ new/pygit2-1.19.2/src/utils.h       2026-03-29 15:47:01.119937700 +0200
@@ -90,6 +90,7 @@
 const char* pgit_borrow(PyObject *value);
 const char* pgit_borrow_encoding(PyObject *value, const char *encoding, const 
char *errors, PyObject **tvalue);
 char* pgit_borrow_fsdefault(PyObject *value, PyObject **tvalue);
+char* pgit_strdup(PyObject *value);
 
 
 //PyObject * get_pylist_from_git_strarray(git_strarray *strarray);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/src/walker.c 
new/pygit2-1.19.2/src/walker.c
--- old/pygit2-1.19.1/src/walker.c      2025-12-29 12:17:27.260583000 +0100
+++ new/pygit2-1.19.2/src/walker.c      2026-03-29 15:47:01.119937700 +0200
@@ -163,7 +163,7 @@
     return wrap_object((git_object*)commit, self->repo, NULL);
 }
 
-PyMethodDef Walker_methods[] = {
+static PyMethodDef Walker_methods[] = {
     METHOD(Walker, hide, METH_O),
     METHOD(Walker, push, METH_O),
     METHOD(Walker, reset, METH_NOARGS),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/src/worktree.c 
new/pygit2-1.19.2/src/worktree.c
--- old/pygit2-1.19.1/src/worktree.c    2025-12-29 12:17:27.260583000 +0100
+++ new/pygit2-1.19.2/src/worktree.c    2026-03-29 15:47:01.119937700 +0200
@@ -93,7 +93,7 @@
 }
 
 
-PyMethodDef Worktree_methods[] = {
+static PyMethodDef Worktree_methods[] = {
     METHOD(Worktree, prune, METH_VARARGS),
     {NULL}
 };
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/test/test_config.py 
new/pygit2-1.19.2/test/test_config.py
--- old/pygit2-1.19.1/test/test_config.py       2025-12-29 12:17:27.263583000 
+0100
+++ new/pygit2-1.19.2/test/test_config.py       2026-03-29 15:47:01.123324200 
+0200
@@ -185,6 +185,35 @@
     assert lst['core.bare']
 
 
+def test_valueless_key_iteration() -> None:
+    # A valueless key (no `= value`) has a NULL value pointer in libgit2.
+    # Iterating over such entries must not raise a RuntimeError.
+    with open(CONFIG_FILENAME, 'w') as new_file:
+        new_file.write('[section]\n\tvaluelesskey\n\tnormalkey = somevalue\n')
+
+    config = Config()
+    config.add_file(CONFIG_FILENAME, 6)
+
+    entries = {entry.name: entry for entry in config}
+    assert 'section.valuelesskey' in entries
+    assert 'section.normalkey' in entries
+
+
+def test_valueless_key_value() -> None:
+    # A valueless key must expose value=None and raw_value=None.
+    with open(CONFIG_FILENAME, 'w') as new_file:
+        new_file.write('[section]\n\tvaluelesskey\n\tnormalkey = somevalue\n')
+
+    config = Config()
+    config.add_file(CONFIG_FILENAME, 6)
+
+    entries = {entry.name: entry for entry in config}
+    assert entries['section.valuelesskey'].raw_value is None
+    assert entries['section.valuelesskey'].value is None
+    assert entries['section.normalkey'].raw_value == b'somevalue'
+    assert entries['section.normalkey'].value == 'somevalue'
+
+
 def test_parsing() -> None:
     assert Config.parse_bool('on')
     assert Config.parse_bool('1')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/test/test_filter.py 
new/pygit2-1.19.2/test/test_filter.py
--- old/pygit2-1.19.1/test/test_filter.py       2025-12-29 12:17:27.263583000 
+0100
+++ new/pygit2-1.19.2/test/test_filter.py       2026-03-29 15:47:01.123324200 
+0200
@@ -1,4 +1,5 @@
 import codecs
+import gc
 from collections.abc import Callable, Generator
 from io import BytesIO
 
@@ -6,7 +7,7 @@
 
 import pygit2
 from pygit2 import Blob, Filter, FilterSource, Repository
-from pygit2.enums import BlobFilter
+from pygit2.enums import BlobFilter, FilterMode
 from pygit2.errors import Passthrough
 
 
@@ -56,32 +57,34 @@
     attributes = 'filter=rot13'
 
 
+def _filter_fixture(name: str, filter: type[Filter]) -> Generator[None, None, 
None]:
+    pygit2.filter_register(name, filter)
+    yield
+
+    # Collect any FilterLists that may use this filter before unregistering it
+    gc.collect()
+
+    pygit2.filter_unregister(name)
+
+
 @pytest.fixture
 def rot13_filter() -> Generator[None, None, None]:
-    pygit2.filter_register('rot13', _Rot13Filter)
-    yield
-    pygit2.filter_unregister('rot13')
+    yield from _filter_fixture('rot13', _Rot13Filter)
 
 
 @pytest.fixture
 def passthrough_filter() -> Generator[None, None, None]:
-    pygit2.filter_register('passthrough-rot13', _PassthroughFilter)
-    yield
-    pygit2.filter_unregister('passthrough-rot13')
+    yield from _filter_fixture('passthrough-rot13', _PassthroughFilter)
 
 
 @pytest.fixture
 def buffered_filter() -> Generator[None, None, None]:
-    pygit2.filter_register('buffered-rot13', _BufferedFilter)
-    yield
-    pygit2.filter_unregister('buffered-rot13')
+    yield from _filter_fixture('buffered-rot13', _BufferedFilter)
 
 
 @pytest.fixture
 def unmatched_filter() -> Generator[None, None, None]:
-    pygit2.filter_register('unmatched-rot13', _UnmatchedFilter)
-    yield
-    pygit2.filter_unregister('unmatched-rot13')
+    yield from _filter_fixture('unmatched-rot13', _UnmatchedFilter)
 
 
 def test_filter(testrepo: Repository, rot13_filter: Filter) -> None:
@@ -136,3 +139,94 @@
     # Indirectly test that pygit2_filter_cleanup has the GIL
     # before calling pygit2_filter_payload_free.
     dirtyrepo.diff()
+
+
+def test_filterlist_none(testrepo: Repository) -> None:
+    fl = testrepo.load_filter_list('hello.txt')
+    assert fl is None
+
+
+def test_filterlist_apply_to_buffer_crlf_clean(testrepo: Repository) -> None:
+    testrepo.config['core.autocrlf'] = True
+
+    fl = testrepo.load_filter_list('whatever.txt', mode=FilterMode.CLEAN)
+    assert fl is not None
+    assert len(fl) == 1
+    assert 'crlf' in fl
+    assert 'bogus_filter_name' not in fl
+    with pytest.raises(TypeError):
+        1234 in fl  # type: ignore
+
+    filtered = fl.apply_to_buffer(b'hello\r\nworld\r\n')
+    assert filtered == b'hello\nworld\n'
+
+
+def test_filterlist_apply_to_buffer_crlf_smudge(testrepo: Repository) -> None:
+    testrepo.config['core.autocrlf'] = True
+
+    fl = testrepo.load_filter_list('whatever.txt', mode=FilterMode.SMUDGE)
+    assert fl is not None
+    assert len(fl) == 1
+    assert 'crlf' in fl
+
+    filtered = fl.apply_to_buffer(b'hello\nworld\n')
+    assert filtered == b'hello\r\nworld\r\n'
+
+
+def test_filterlist_dangerous_unregister(testrepo: Repository) -> None:
+    pygit2.filter_register('rot13', _Rot13Filter)
+
+    fl = testrepo.load_filter_list('hello.txt')
+    assert fl is not None
+    assert len(fl) == 1
+    assert 'rot13' in fl
+
+    # Unregistering a filter that's still in use in a FilterList is dangerous!
+    # Our built-in check (that raises RuntimeError) may avert a segfault.
+    with pytest.raises(RuntimeError):
+        pygit2.filter_unregister('rot13')
+
+    # Delete any FilterLists that use the filter, and only then is it safe
+    # to unregister the filter.
+    del fl
+    gc.collect()
+    pygit2.filter_unregister('rot13')
+
+
+def test_filterlist_apply_to_file(testrepo: Repository, rot13_filter: Filter) 
-> None:
+    fl = testrepo.load_filter_list('bye.txt')
+    assert fl is not None
+    assert len(fl) == 1
+    assert 'rot13' in fl
+
+    filtered = fl.apply_to_file(testrepo, 'bye.txt')
+    assert filtered == b'olr jbeyq\n'
+
+
+def test_filterlist_apply_to_blob(testrepo: Repository, rot13_filter: Filter) 
-> None:
+    fl = testrepo.load_filter_list('whatever.txt')
+    assert fl is not None
+    assert len(fl) == 1
+    assert 'rot13' in fl
+
+    blob_oid = testrepo.create_blob(b'bye world\n')
+    blob = testrepo[blob_oid]
+    assert isinstance(blob, Blob)
+
+    filtered = fl.apply_to_blob(blob)
+    assert filtered == b'olr jbeyq\n'
+
+
+def test_filterlist_apply_to_buffer_multiple(
+    testrepo: Repository, rot13_filter: Filter
+) -> None:
+    testrepo.config['core.autocrlf'] = True
+
+    fl = testrepo.load_filter_list('whatever.txt')
+    assert fl is not None
+    assert len(fl) == 2
+    assert 'crlf' in fl
+    assert 'rot13' in fl
+
+    filtered = fl.apply_to_buffer(b'bye\r\nworld\r\n')
+    assert filtered == b'olr\njbeyq\n'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pygit2-1.19.1/test/test_odb.py 
new/pygit2-1.19.2/test/test_odb.py
--- old/pygit2-1.19.1/test/test_odb.py  2025-12-29 12:17:27.264582900 +0100
+++ new/pygit2-1.19.2/test/test_odb.py  2026-03-29 15:47:01.123324200 +0200
@@ -41,6 +41,7 @@
 BLOB_HEX = 'af431f20fc541ed6d5afede3e2dc7160f6f01f16'
 BLOB_RAW = binascii.unhexlify(BLOB_HEX.encode('ascii'))
 BLOB_OID = Oid(raw=BLOB_RAW)
+BLOB_CONTENTS = b'a contents\n'
 
 
 def test_emptyodb(barerepo: Repository) -> None:
@@ -75,14 +76,29 @@
     ab = odb.read(BLOB_OID)
     a = odb.read(BLOB_HEX)
     assert ab == a
-    assert (ObjectType.BLOB, b'a contents\n') == a
+    assert (ObjectType.BLOB, BLOB_CONTENTS) == a
+    assert isinstance(a[0], ObjectType)
 
     a2 = odb.read('7f129fd57e31e935c6d60a0c794efe4e6927664b')
     assert (ObjectType.BLOB, b'a contents 2\n') == a2
+    assert isinstance(a2[0], ObjectType)
 
     a_hex_prefix = BLOB_HEX[:4]
     a3 = odb.read(a_hex_prefix)
-    assert (ObjectType.BLOB, b'a contents\n') == a3
+    assert (ObjectType.BLOB, BLOB_CONTENTS) == a3
+    assert isinstance(a3[0], ObjectType)
+
+
+def test_read_header(odb: Odb) -> None:
+    with pytest.raises(TypeError):
+        odb.read_header(123)  # type: ignore
+    utils.assertRaisesWithArg(KeyError, '1' * 40, odb.read_header, '1' * 40)
+
+    ab = odb.read_header(BLOB_OID)
+    a = odb.read_header(BLOB_HEX)
+    assert ab == a
+    assert (ObjectType.BLOB, len(BLOB_CONTENTS)) == a
+    assert isinstance(a[0], ObjectType)
 
 
 def test_write(odb: Odb) -> None:

Reply via email to