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:
