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 2022-12-05 18:00:44 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-pygit2 (Old) and /work/SRC/openSUSE:Factory/.python-pygit2.new.1835 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-pygit2" Mon Dec 5 18:00:44 2022 rev:30 rq:1040007 version:1.11.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-pygit2/python-pygit2.changes 2022-09-20 19:24:10.482575754 +0200 +++ /work/SRC/openSUSE:Factory/.python-pygit2.new.1835/python-pygit2.changes 2022-12-05 18:00:45.832438707 +0100 @@ -1,0 +2,17 @@ +Sat Dec 3 20:06:29 UTC 2022 - Yogalakshmi Arunachalam <yarunacha...@suse.com> + +- Update to version 1.11.1 (2022-11-09) + * Fix Linux wheels, downgrade to manylinux 2_24 #1176 + * Windows wheels for Python 3.11 #1177 + * CI: Use 3.11 final release for testing #1178 + * Drop support for Python 3.7 + * Update Linux wheels to manylinux 2_28 #1136 + * Fix crash in signature representation #1162 + * Fix memory leak in Signature #1173 + * New optional argument raise_error in Repository.applies(...) #1166 + * New notify/progress callbacks for checkout and stash #1167 #1169 + * New Repository.remotes.names() #1159 + * Now refname argument in RemoteCallbacks.push_update_reference(...) is a string, not bytes #1168 + * Add missing newline at end of pygit2/decl/pack.h #1163 + +------------------------------------------------------------------- Old: ---- pygit2-1.10.1.tar.gz New: ---- pygit2-1.11.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-pygit2.spec ++++++ --- /var/tmp/diff_new_pack.0uSYyC/_old 2022-12-05 18:00:46.408441844 +0100 +++ /var/tmp/diff_new_pack.0uSYyC/_new 2022-12-05 18:00:46.412441866 +0100 @@ -20,7 +20,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} %define skip_python2 1 Name: python-pygit2 -Version: 1.10.1 +Version: 1.11.1 Release: 0 Summary: Python bindings for libgit2 License: GPL-2.0-only ++++++ pygit2-1.10.1.tar.gz -> pygit2-1.11.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pygit2-1.10.1/AUTHORS.rst new/pygit2-1.11.1/AUTHORS.rst --- old/pygit2-1.10.1/AUTHORS.rst 2022-08-28 11:14:16.406786400 +0200 +++ new/pygit2-1.11.1/AUTHORS.rst 2022-11-09 08:08:45.083533500 +0100 @@ -9,9 +9,9 @@ W. Trevor King Drew DeVault Dave Borowitz + Iliyas Jorio Brandon Milton Daniel RodriÌguez TroitinÌo - Iliyas Jorio Richo Healey Christian Boos Julien Miotte @@ -32,6 +32,7 @@ Tamir Bahar Valentin Haenel Michael Jones + Saugat Pachhai Bernardo Heynemann Brodie Rao John Szakmeister @@ -42,7 +43,6 @@ Lukas Fleischer Nicolas Dandrimont Raphael Medaer (Escaux) - Saugat Pachhai Anatoly Techtonik Andrew Olsen Dan Sully @@ -114,6 +114,7 @@ Vicent Marti Zbigniew JÄdrzejewski-Szmek Zoran Zaric + Adam Gausmann Adam Spiers Alexandru Fikl Andrew Chin @@ -168,6 +169,7 @@ Lance Eftink Legorooj Lukas Berk + Martin von Zweigbergk Mathieu Bridon Mathieu Parent Mathieu Pillard @@ -192,6 +194,7 @@ Saul Pwanson Shane Turner Sheeo + Simone Mosciatti Soasme Steven Winfield Tad Hardesty diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pygit2-1.10.1/CHANGELOG.rst new/pygit2-1.11.1/CHANGELOG.rst --- old/pygit2-1.10.1/CHANGELOG.rst 2022-08-28 11:13:13.705961500 +0200 +++ new/pygit2-1.11.1/CHANGELOG.rst 2022-11-09 08:06:54.571543000 +0100 @@ -1,3 +1,48 @@ +1.11.1 (2022-11-09) +------------------------- + +- Fix Linux wheels, downgrade to manylinux 2_24 + `#1176 <https://github.com/libgit2/pygit2/issues/1176>`_ + +- Windows wheels for Python 3.11 + `#1177 <https://github.com/libgit2/pygit2/pull/1177>`_ + +- CI: Use 3.11 final release for testing + `#1178 <https://github.com/libgit2/pygit2/pull/1178>`_ + + +1.11.0 (2022-11-06) +------------------------- + +- Drop support for Python 3.7 + +- Update Linux wheels to manylinux 2_28 + `#1136 <https://github.com/libgit2/pygit2/issues/1136>`_ + +- Fix crash in signature representation + `#1162 <https://github.com/libgit2/pygit2/pull/1162>`_ + +- Fix memory leak in ``Signature`` + `#1173 <https://github.com/libgit2/pygit2/pull/1173>`_ + +- New optional argument ``raise_error`` in ``Repository.applies(...)`` + `#1166 <https://github.com/libgit2/pygit2/pull/1166>`_ + +- New notify/progress callbacks for checkout and stash + `#1167 <https://github.com/libgit2/pygit2/pull/1167>`_ + `#1169 <https://github.com/libgit2/pygit2/pull/1169>`_ + +- New ``Repository.remotes.names()`` + `#1159 <https://github.com/libgit2/pygit2/pull/1159>`_ + +- Now ``refname`` argument in ``RemoteCallbacks.push_update_reference(...)`` is + a string, not bytes + `#1168 <https://github.com/libgit2/pygit2/pull/1168>`_ + +- Add missing newline at end of ``pygit2/decl/pack.h`` + `#1163 <https://github.com/libgit2/pygit2/pull/1163>`_ + + 1.10.1 (2022-08-28) ------------------------- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pygit2-1.10.1/PKG-INFO new/pygit2-1.11.1/PKG-INFO --- old/pygit2-1.10.1/PKG-INFO 2022-08-28 13:41:46.114741000 +0200 +++ new/pygit2-1.11.1/PKG-INFO 2022-11-09 10:53:56.286999700 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: pygit2 -Version: 1.10.1 +Version: 1.11.1 Summary: Python bindings for libgit2. Home-page: https://github.com/libgit2/pygit2 Maintainer: J. David Ibáñez @@ -14,14 +14,14 @@ Classifier: Intended Audience :: Developers Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Topic :: Software Development :: Version Control -Requires-Python: >=3.7 +Requires-Python: >=3.8 License-File: COPYING License-File: AUTHORS.rst @@ -30,7 +30,7 @@ ###################################################################### Bindings to the libgit2 shared library, implements Git plumbing. -Supports Python 3.7+ and PyPy3 7.3+ +Supports Python 3.8+ and PyPy3 7.3+ .. image:: https://github.com/libgit2/pygit2/actions/workflows/tests.yml/badge.svg :target: https://github.com/libgit2/pygit2/actions/workflows/tests.yml diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pygit2-1.10.1/README.rst new/pygit2-1.11.1/README.rst --- old/pygit2-1.10.1/README.rst 2021-12-06 09:47:08.838489800 +0100 +++ new/pygit2-1.11.1/README.rst 2022-10-23 11:28:21.044473400 +0200 @@ -3,7 +3,7 @@ ###################################################################### Bindings to the libgit2 shared library, implements Git plumbing. -Supports Python 3.7+ and PyPy3 7.3+ +Supports Python 3.8+ and PyPy3 7.3+ .. image:: https://github.com/libgit2/pygit2/actions/workflows/tests.yml/badge.svg :target: https://github.com/libgit2/pygit2/actions/workflows/tests.yml diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pygit2-1.10.1/build.sh new/pygit2-1.11.1/build.sh --- old/pygit2-1.10.1/build.sh 2022-08-28 11:11:54.262747800 +0200 +++ new/pygit2-1.11.1/build.sh 2022-11-08 13:48:28.755278000 +0100 @@ -68,8 +68,11 @@ rm -rf ci mkdir ci || true cd ci - if [ "$KERNEL" = "Linux" ]; then - yum install wget openssl-devel libssh2-devel zlib-devel -y + if [ -f /usr/bin/apt-get ]; then + apt-get update + apt-get install libssl-dev wget -y + elif [ -f /usr/bin/yum ]; then + yum install wget openssl-devel zlib-devel -y fi else # Create a virtual environment @@ -188,15 +191,13 @@ fi if [ "$CIBUILDWHEEL" = "1" ]; then - # This is gross. auditwheel/delocate-wheel are not so good - # at finding libraries in random places, so we have to - # put them in the loader path. if [ "$KERNEL" = "Darwin" ]; then + # Copy libraries where delocate-wheel can find them. + # In Linux we use LD_LIBRARY_PATH to avoid this, maybe + # DYLD_LIBRARY_PATH would work for macOS. cp -r $OPENSSL_PREFIX/*.dylib /usr/local/lib cp -r $LIBSSH2_PREFIX/lib/*.dylib /usr/local/lib cp -r $FILENAME/*.dylib /usr/local/lib - else - cp -r $PREFIX/lib64/*.so* /usr/local/lib fi # we're done building dependencies, cibuildwheel action will take over exit 0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pygit2-1.10.1/pygit2/__init__.py new/pygit2-1.11.1/pygit2/__init__.py --- old/pygit2-1.10.1/pygit2/__init__.py 2021-09-17 13:25:19.987411300 +0200 +++ new/pygit2-1.11.1/pygit2/__init__.py 2022-10-19 09:26:54.059615100 +0200 @@ -32,7 +32,7 @@ # High level API from .blame import Blame, BlameHunk from .callbacks import git_clone_options, git_fetch_options, get_credentials -from .callbacks import Payload, RemoteCallbacks +from .callbacks import Payload, RemoteCallbacks, CheckoutCallbacks, StashApplyCallbacks from .config import Config from .credentials import * from .errors import check_error, Passthrough @@ -84,6 +84,25 @@ GIT_FETCH_PRUNE = C.GIT_FETCH_PRUNE GIT_FETCH_NO_PRUNE = C.GIT_FETCH_NO_PRUNE +# GIT_CHECKOUT_NOTIFY_* +GIT_CHECKOUT_NOTIFY_NONE : int = C.GIT_CHECKOUT_NOTIFY_NONE +GIT_CHECKOUT_NOTIFY_CONFLICT : int = C.GIT_CHECKOUT_NOTIFY_CONFLICT +GIT_CHECKOUT_NOTIFY_DIRTY : int = C.GIT_CHECKOUT_NOTIFY_DIRTY +GIT_CHECKOUT_NOTIFY_UPDATED : int = C.GIT_CHECKOUT_NOTIFY_UPDATED +GIT_CHECKOUT_NOTIFY_UNTRACKED : int = C.GIT_CHECKOUT_NOTIFY_UNTRACKED +GIT_CHECKOUT_NOTIFY_IGNORED : int = C.GIT_CHECKOUT_NOTIFY_IGNORED +GIT_CHECKOUT_NOTIFY_ALL : int = C.GIT_CHECKOUT_NOTIFY_ALL + +# GIT_STASH_APPLY_PROGRESS_* +GIT_STASH_APPLY_PROGRESS_NONE : int = C.GIT_STASH_APPLY_PROGRESS_NONE +GIT_STASH_APPLY_PROGRESS_LOADING_STASH : int = C.GIT_STASH_APPLY_PROGRESS_LOADING_STASH +GIT_STASH_APPLY_PROGRESS_ANALYZE_INDEX : int = C.GIT_STASH_APPLY_PROGRESS_ANALYZE_INDEX +GIT_STASH_APPLY_PROGRESS_ANALYZE_MODIFIED : int = C.GIT_STASH_APPLY_PROGRESS_ANALYZE_MODIFIED +GIT_STASH_APPLY_PROGRESS_ANALYZE_UNTRACKED : int = C.GIT_STASH_APPLY_PROGRESS_ANALYZE_UNTRACKED +GIT_STASH_APPLY_PROGRESS_CHECKOUT_UNTRACKED : int = C.GIT_STASH_APPLY_PROGRESS_CHECKOUT_UNTRACKED +GIT_STASH_APPLY_PROGRESS_CHECKOUT_MODIFIED : int = C.GIT_STASH_APPLY_PROGRESS_CHECKOUT_MODIFIED +GIT_STASH_APPLY_PROGRESS_DONE : int = C.GIT_STASH_APPLY_PROGRESS_DONE + # libgit version tuple LIBGIT2_VER = (LIBGIT2_VER_MAJOR, LIBGIT2_VER_MINOR, LIBGIT2_VER_REVISION) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pygit2-1.10.1/pygit2/_build.py new/pygit2-1.11.1/pygit2/_build.py --- old/pygit2-1.10.1/pygit2/_build.py 2022-08-28 11:14:43.573393300 +0200 +++ new/pygit2-1.11.1/pygit2/_build.py 2022-11-09 08:09:05.476718700 +0100 @@ -35,7 +35,7 @@ # # The version number of pygit2 # -__version__ = '1.10.1' +__version__ = '1.11.1' # diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pygit2-1.10.1/pygit2/_pygit2.pyi new/pygit2-1.11.1/pygit2/_pygit2.pyi --- old/pygit2-1.10.1/pygit2/_pygit2.pyi 2022-08-24 10:35:14.088508000 +0200 +++ new/pygit2-1.11.1/pygit2/_pygit2.pyi 2022-10-19 09:26:54.059615100 +0200 @@ -300,6 +300,8 @@ path: str raw_path: bytes size: int + @staticmethod + def from_c(bytes) -> DiffFile: ... class DiffHunk: header: str @@ -449,7 +451,7 @@ def _disown(self, *args, **kwargs) -> None: ... def _from_c(self, *args, **kwargs) -> None: ... def add_worktree(self, name: str, path: str, ref: Reference = ...) -> Worktree: ... - def applies(self, diff: Diff, location: int = GIT_APPLY_LOCATION_INDEX) -> bool: ... + def applies(self, diff: Diff, location: int = GIT_APPLY_LOCATION_INDEX, raise_error: bool = False) -> bool: ... def apply(self, diff: Diff, location: int = GIT_APPLY_LOCATION_WORKDIR) -> None: ... def cherrypick(self, id: _OidArg) -> None: ... def compress_references(self) -> None: ... diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pygit2-1.10.1/pygit2/callbacks.py new/pygit2-1.11.1/pygit2/callbacks.py --- old/pygit2-1.10.1/pygit2/callbacks.py 2022-01-29 08:18:52.169225500 +0100 +++ new/pygit2-1.11.1/pygit2/callbacks.py 2022-11-06 12:28:05.708092000 +0100 @@ -65,12 +65,13 @@ # Standard Library from contextlib import contextmanager from functools import wraps +from typing import Optional # pygit2 -from ._pygit2 import Oid +from ._pygit2 import Oid, DiffFile, GIT_CHECKOUT_SAFE, GIT_CHECKOUT_RECREATE_MISSING from .errors import check_error, Passthrough from .ffi import ffi, C -from .utils import maybe_string, to_bytes +from .utils import maybe_string, to_bytes, ptr_to_bytes, StrArray # @@ -89,6 +90,10 @@ if error_code == C.GIT_EUSER: assert self._stored_exception is not None raise self._stored_exception + elif self._stored_exception is not None: + # A callback mapped to a C function returning void + # might still have raised an exception. + raise self._stored_exception check_error(error_code) @@ -210,6 +215,84 @@ """ +class CheckoutCallbacks(Payload): + """Base class for pygit2 checkout callbacks. + + Inherit from this class and override the callbacks that you want to use + in your class, which you can then pass to checkout operations. + """ + + def __init__(self): + super().__init__() + + def checkout_notify_flags(self) -> int: + """ + Returns a bit mask of the notifications to receive from a checkout + (GIT_CHECKOUT_NOTIFY values combined with bitwise OR). + + By default, if you override `checkout_notify`, all notifications will + be enabled. You can fine tune the notification types to enable by + overriding `checkout_notify_flags`. + + Please note that the flags are only sampled once when checkout begins. + You cannot change the flags while a checkout is in progress. + """ + if type(self).checkout_notify == CheckoutCallbacks.checkout_notify: + # If the user hasn't overridden the notify function, + # filter out all notifications. + return C.GIT_CHECKOUT_NOTIFY_NONE + else: + # If the user provides their own notify function, + # enable all notifications by default. + return C.GIT_CHECKOUT_NOTIFY_ALL + + def checkout_notify(self, + why: int, + path: str, + baseline: Optional[DiffFile], + target: Optional[DiffFile], + workdir: Optional[DiffFile] + ): + """ + Checkout will invoke an optional notification callback for + certain cases - you pick which ones via `checkout_notify_flags`. + + Raising an exception from this callback will cancel the checkout. + The exception will be propagated back and raised by the + Repository.checkout_... call. + + Notification callbacks are made prior to modifying any files on disk, + so canceling on any notification will still happen prior to any files + being modified. + """ + pass + + def checkout_progress(self, path: str, completed_steps: int, total_steps: int): + """ + Optional callback to notify the consumer of checkout progress. + """ + pass + + +class StashApplyCallbacks(CheckoutCallbacks): + """Base class for pygit2 stash apply callbacks. + + Inherit from this class and override the callbacks that you want to use + in your class, which you can then pass to stash apply or pop operations. + """ + + def stash_apply_progress(self, progress: int): + """ + Stash application progress notification function. + + `progress` is a GIT_STASH_APPLY_PROGRESS constant. + + Raising an exception from this callback will abort the stash + application. + """ + pass + + # # The context managers below wrap the calls to libgit2 functions, which them in # turn call to callbacks defined later in this module. These context managers @@ -334,7 +417,7 @@ # then libgit2 will behave as if there was no callback set in the # first place. return C.GIT_PASSTHROUGH - except Exception as e: + except BaseException as e: # Keep the exception to be re-raised later, and inform libgit2 that # the user defined callback has failed. data._stored_exception = e @@ -343,6 +426,26 @@ return ffi.def_extern()(wrapper) +def libgit2_callback_void(f): + @wraps(f) + def wrapper(*args): + data = ffi.from_handle(args[-1]) + args = args[:-1] + (data,) + try: + f(*args) + except Passthrough: + # A user defined callback can raise Passthrough to decline to act; + # then libgit2 will behave as if there was no callback set in the + # first place. + pass # Function returns void + except BaseException as e: + # Keep the exception to be re-raised later + data._stored_exception = e + pass # Function returns void, so we can't do much here. + + return ffi.def_extern()(wrapper) + + @libgit2_callback def _certificate_cb(cert_i, valid, host, data): # We want to simulate what should happen if libgit2 supported pass-through @@ -384,7 +487,7 @@ if not push_update_reference: return 0 - refname = ffi.string(ref) + refname = maybe_string(ref) message = maybe_string(msg) push_update_reference(refname, message) return 0 @@ -503,3 +606,148 @@ check_error(err) return ccred + + +# +# Checkout callbacks +# + +@libgit2_callback +def _checkout_notify_cb(why, path_cstr, baseline, target, workdir, data: CheckoutCallbacks): + pypath = maybe_string(path_cstr) + pybaseline = DiffFile.from_c(ptr_to_bytes(baseline)) + pytarget = DiffFile.from_c(ptr_to_bytes(target)) + pyworkdir = DiffFile.from_c(ptr_to_bytes(workdir)) + + try: + data.checkout_notify(why, pypath, pybaseline, pytarget, pyworkdir) + except Passthrough: + # Unlike most other operations with optional callbacks, checkout + # doesn't support the GIT_PASSTHROUGH return code, so we must bypass + # libgit2_callback's error handling and return 0 explicitly here. + pass + + # If the user's callback has raised any other exception type, + # it's caught by the libgit2_callback decorator by now. + # So, return success code to libgit2. + return 0 + + +@libgit2_callback_void +def _checkout_progress_cb(path, completed_steps, total_steps, data: CheckoutCallbacks): + data.checkout_progress(maybe_string(path), completed_steps, total_steps) + + +def _git_checkout_options(callbacks=None, strategy=None, directory=None, paths=None, c_checkout_options_ptr=None): + if callbacks is None: + payload = CheckoutCallbacks() + else: + payload = callbacks + + # Get handle to payload + handle = ffi.new_handle(payload) + + # Create the options struct to pass + if not c_checkout_options_ptr: + opts = ffi.new('git_checkout_options *') + else: + opts = c_checkout_options_ptr + check_error(C.git_checkout_init_options(opts, 1)) + + # References we need to keep to strings and so forth + refs = [handle] + + # pygit2's default is SAFE | RECREATE_MISSING + opts.checkout_strategy = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_RECREATE_MISSING + # and go through the arguments to see what the user wanted + if strategy: + opts.checkout_strategy = strategy + + if directory: + target_dir = ffi.new('char[]', to_bytes(directory)) + refs.append(target_dir) + opts.target_directory = target_dir + + if paths: + strarray = StrArray(paths) + refs.append(strarray) + opts.paths = strarray.array[0] + + # If we want to receive any notifications, set up notify_cb in the options + notify_flags = payload.checkout_notify_flags() + if notify_flags != C.GIT_CHECKOUT_NOTIFY_NONE: + opts.notify_cb = C._checkout_notify_cb + opts.notify_flags = notify_flags + opts.notify_payload = handle + + # Set up progress callback if the user has provided their own + if type(payload).checkout_progress != CheckoutCallbacks.checkout_progress: + opts.progress_cb = C._checkout_progress_cb + opts.progress_payload = handle + + # Give back control + payload.checkout_options = opts + payload._ffi_handle = handle + payload._refs = refs + payload._stored_exception = None + return payload + + +@contextmanager +def git_checkout_options(callbacks=None, strategy=None, directory=None, paths=None): + yield _git_checkout_options(callbacks=callbacks, strategy=strategy, directory=directory, paths=paths) + + +# +# Stash callbacks +# + +@libgit2_callback +def _stash_apply_progress_cb(progress: int, data: StashApplyCallbacks): + try: + data.stash_apply_progress(progress) + except Passthrough: + # Unlike most other operations with optional callbacks, stash apply + # doesn't support the GIT_PASSTHROUGH return code, so we must bypass + # libgit2_callback's error handling and return 0 explicitly here. + pass + + # If the user's callback has raised any other exception type, + # it's caught by the libgit2_callback decorator by now. + # So, return success code to libgit2. + return 0 + + +@contextmanager +def git_stash_apply_options(callbacks=None, reinstate_index=False, strategy=None, directory=None, paths=None): + if callbacks is None: + callbacks = StashApplyCallbacks() + + # Set up stash options + # TODO: git_stash_apply_init_options is deprecated (along with a bunch of other git_XXX_init_options functions) + stash_apply_options = ffi.new('git_stash_apply_options *') + check_error(C.git_stash_apply_init_options(stash_apply_options, 1)) + + flags = reinstate_index * C.GIT_STASH_APPLY_REINSTATE_INDEX + stash_apply_options.flags = flags + + # Now set up checkout options + c_checkout_options_ptr = ffi.addressof(stash_apply_options.checkout_options) + payload = _git_checkout_options( + callbacks=callbacks, + strategy=strategy, + directory=directory, + paths=paths, + c_checkout_options_ptr=c_checkout_options_ptr, + ) + assert payload == callbacks + assert payload.checkout_options == c_checkout_options_ptr + + # Set up stash progress callback if the user has provided their own + if type(callbacks).stash_apply_progress != StashApplyCallbacks.stash_apply_progress: + stash_apply_options.progress_cb = C._stash_apply_progress_cb + stash_apply_options.progress_payload = payload._ffi_handle + + # Give back control + payload.stash_apply_options = stash_apply_options + yield payload diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pygit2-1.10.1/pygit2/decl/callbacks.h new/pygit2-1.11.1/pygit2/decl/callbacks.h --- old/pygit2-1.10.1/pygit2/decl/callbacks.h 2021-06-19 11:40:24.646733300 +0200 +++ new/pygit2-1.11.1/pygit2/decl/callbacks.h 2022-10-19 09:26:54.059615100 +0200 @@ -43,3 +43,25 @@ const git_oid *a, const git_oid *b, void *data); + +/* Checkout */ + +extern "Python" int _checkout_notify_cb( + git_checkout_notify_t why, + const char *path, + const git_diff_file *baseline, + const git_diff_file *target, + const git_diff_file *workdir, + void *payload); + +extern "Python" void _checkout_progress_cb( + const char *path, + size_t completed_steps, + size_t total_steps, + void *payload); + +/* Stash */ + +extern "Python" int _stash_apply_progress_cb( + git_stash_apply_progress_t progress, + void *payload); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pygit2-1.10.1/pygit2/decl/pack.h new/pygit2-1.11.1/pygit2/decl/pack.h --- old/pygit2-1.10.1/pygit2/decl/pack.h 2021-06-19 11:40:24.646733300 +0200 +++ new/pygit2-1.11.1/pygit2/decl/pack.h 2022-10-06 18:14:22.057606500 +0200 @@ -15,4 +15,4 @@ int git_packbuilder_write(git_packbuilder *pb, const char *path, unsigned int mode, git_indexer_progress_cb progress_cb, void *progress_cb_payload); uint32_t git_packbuilder_written(git_packbuilder *pb); -unsigned int git_packbuilder_set_threads(git_packbuilder *pb, unsigned int n); \ No newline at end of file +unsigned int git_packbuilder_set_threads(git_packbuilder *pb, unsigned int n); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pygit2-1.10.1/pygit2/remote.py new/pygit2-1.11.1/pygit2/remote.py --- old/pygit2-1.10.1/pygit2/remote.py 2022-04-26 20:55:32.101869000 +0200 +++ new/pygit2-1.11.1/pygit2/remote.py 2022-11-06 12:28:47.396510000 +0100 @@ -295,20 +295,12 @@ C.git_strarray_free(names) def __iter__(self): - names = ffi.new('git_strarray *') - - try: - err = C.git_remote_list(names, self._repo._repo) + cremote = ffi.new('git_remote **') + for name in self._ffi_names(): + err = C.git_remote_lookup(cremote, self._repo._repo, name) check_error(err) - cremote = ffi.new('git_remote **') - for i in range(names.count): - err = C.git_remote_lookup(cremote, self._repo._repo, names.strings[i]) - check_error(err) - - yield Remote(self._repo, cremote[0]) - finally: - C.git_strarray_free(names) + yield Remote(self._repo, cremote[0]) def __getitem__(self, name): if isinstance(name, int): @@ -320,6 +312,23 @@ return Remote(self._repo, cremote[0]) + def _ffi_names(self): + names = ffi.new('git_strarray *') + + try: + err = C.git_remote_list(names, self._repo._repo) + check_error(err) + + for i in range(names.count): + yield names.strings[i] + finally: + C.git_strarray_free(names) + + def names(self): + """An iterator over the names of the available remotes.""" + for name in self._ffi_names(): + yield maybe_string(name) + def create(self, name, url, fetch=None): """Create a new remote with the given name and url. Returns a <Remote> object. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pygit2-1.10.1/pygit2/repository.py new/pygit2-1.11.1/pygit2/repository.py --- old/pygit2-1.10.1/pygit2/repository.py 2022-03-14 09:15:20.348721500 +0100 +++ new/pygit2-1.11.1/pygit2/repository.py 2022-10-23 20:23:35.574572000 +0200 @@ -40,7 +40,7 @@ from ._pygit2 import Reference, Tree, Commit, Blob, Signature from ._pygit2 import InvalidSpecError -from .callbacks import git_fetch_options +from .callbacks import git_checkout_options, git_fetch_options, git_stash_apply_options from .config import Config from .errors import check_error from .ffi import ffi, C @@ -318,59 +318,35 @@ # # Checkout # - @staticmethod - def _checkout_args_to_options(strategy=None, directory=None, paths=None): - # Create the options struct to pass - copts = ffi.new('git_checkout_options *') - check_error(C.git_checkout_init_options(copts, 1)) - - # References we need to keep to strings and so forth - refs = [] - - # pygit2's default is SAFE | RECREATE_MISSING - copts.checkout_strategy = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_RECREATE_MISSING - # and go through the arguments to see what the user wanted - if strategy: - copts.checkout_strategy = strategy - - if directory: - target_dir = ffi.new('char[]', to_bytes(directory)) - refs.append(target_dir) - copts.target_directory = target_dir - - if paths: - strarray = StrArray(paths) - refs.append(strarray) - copts.paths = strarray.array[0] - - return copts, refs def checkout_head(self, **kwargs): """Checkout HEAD For arguments, see Repository.checkout(). """ - copts, refs = Repository._checkout_args_to_options(**kwargs) - check_error(C.git_checkout_head(self._repo, copts)) + with git_checkout_options(**kwargs) as payload: + err = C.git_checkout_head(self._repo, payload.checkout_options) + payload.check_error(err) def checkout_index(self, index=None, **kwargs): """Checkout the given index or the repository's index For arguments, see Repository.checkout(). """ - copts, refs = Repository._checkout_args_to_options(**kwargs) - check_error(C.git_checkout_index(self._repo, index._index if index else ffi.NULL, copts)) + with git_checkout_options(**kwargs) as payload: + err = C.git_checkout_index(self._repo, index._index if index else ffi.NULL, payload.checkout_options) + payload.check_error(err) def checkout_tree(self, treeish, **kwargs): """Checkout the given treeish For arguments, see Repository.checkout(). """ - copts, refs = Repository._checkout_args_to_options(**kwargs) - cptr = ffi.new('git_object **') - ffi.buffer(cptr)[:] = treeish._pointer[:] - - check_error(C.git_checkout_tree(self._repo, cptr[0], copts)) + with git_checkout_options(**kwargs) as payload: + cptr = ffi.new('git_object **') + ffi.buffer(cptr)[:] = treeish._pointer[:] + err = C.git_checkout_tree(self._repo, cptr[0], payload.checkout_options) + payload.check_error(err) def checkout(self, refname=None, **kwargs): """ @@ -397,6 +373,15 @@ A list of files to checkout from the given reference. If paths is provided, HEAD will not be set to the reference. + callbacks : CheckoutCallbacks + Optional. Supply a `callbacks` object to get information about + conflicted files, updated files, etc. as the checkout is being + performed. The callbacks can also abort the checkout prematurely. + + The callbacks should be an object which inherits from + `pyclass:CheckoutCallbacks`. It should implement the callbacks + as overridden methods. + Examples: * To checkout from the HEAD, just pass 'HEAD':: @@ -1092,19 +1077,6 @@ return Oid(raw=bytes(ffi.buffer(coid)[:])) - @staticmethod - def _stash_args_to_options(reinstate_index=False, **kwargs): - stash_opts = ffi.new('git_stash_apply_options *') - check_error(C.git_stash_apply_init_options(stash_opts, 1)) - - flags = reinstate_index * C.GIT_STASH_APPLY_REINSTATE_INDEX - stash_opts.flags = flags - - copts, refs = Repository._checkout_args_to_options(**kwargs) - stash_opts.checkout_options = copts[0] - - return stash_opts - def stash_apply(self, index=0, **kwargs): """ Apply a stashed state in the stash list to the working directory. @@ -1118,6 +1090,18 @@ reinstate_index : bool Try to reinstate stashed changes to the index. + callbacks : StashApplyCallbacks + Optional. Supply a `callbacks` object to get information about + the progress of the stash application as it is being performed. + + The callbacks should be an object which inherits from + `pyclass:StashApplyCallbacks`. It should implement the callbacks + as overridden methods. + + Note that this class inherits from CheckoutCallbacks, so you can + also get information from the checkout part of the unstashing + process via the callbacks. + The checkout options may be customized using the same arguments taken by Repository.checkout(). @@ -1127,8 +1111,9 @@ >>> repo.stash(repo.default_signature(), 'WIP: stashing') >>> repo.stash_apply(strategy=GIT_CHECKOUT_ALLOW_CONFLICTS) """ - stash_opts = Repository._stash_args_to_options(**kwargs) - check_error(C.git_stash_apply(self._repo, index, stash_opts)) + with git_stash_apply_options(**kwargs) as payload: + err = C.git_stash_apply(self._repo, index, payload.stash_apply_options) + payload.check_error(err) def stash_drop(self, index=0): """ @@ -1147,8 +1132,9 @@ For arguments, see Repository.stash_apply(). """ - stash_opts = Repository._stash_args_to_options(**kwargs) - check_error(C.git_stash_pop(self._repo, index, stash_opts)) + with git_stash_apply_options(**kwargs) as payload: + err = C.git_stash_pop(self._repo, index, payload.stash_apply_options) + payload.check_error(err) # # Utility for writing a tree into an archive diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pygit2-1.10.1/pygit2/utils.py new/pygit2-1.11.1/pygit2/utils.py --- old/pygit2-1.10.1/pygit2/utils.py 2021-09-17 13:25:19.995411400 +0200 +++ new/pygit2-1.11.1/pygit2/utils.py 2022-10-19 09:26:54.059615100 +0200 @@ -62,6 +62,16 @@ raise TypeError('unexpected type "%s"' % repr(s)) +def ptr_to_bytes(ptr_cdata): + """ + Convert a pointer coming from C code (<cdata 'some_type *'>) + to a byte buffer containing the address that the pointer refers to. + """ + + pp = ffi.new('void **', ptr_cdata) + return bytes(ffi.buffer(pp)[:]) + + def strarray_to_strings(arr): l = [None] * arr.count for i in range(arr.count): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pygit2-1.10.1/requirements.txt new/pygit2-1.11.1/requirements.txt --- old/pygit2-1.10.1/requirements.txt 2022-02-23 13:39:10.615412500 +0100 +++ new/pygit2-1.11.1/requirements.txt 2022-10-23 11:28:21.044473400 +0200 @@ -1,2 +1 @@ -cached-property; python_version < "3.8" cffi>=1.9.1 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pygit2-1.10.1/setup.py new/pygit2-1.11.1/setup.py --- old/pygit2-1.10.1/setup.py 2022-03-20 21:36:06.725472500 +0100 +++ new/pygit2-1.11.1/setup.py 2022-10-23 11:28:21.044473400 +0200 @@ -75,10 +75,10 @@ "Intended Audience :: Developers", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Software Development :: Version Control"] @@ -152,7 +152,7 @@ cffi_modules=['pygit2/_run.py:ffi'], ext_modules=ext_modules, # Requirements - python_requires='>=3.7', + python_requires='>=3.8', setup_requires=['cffi>=1.9.1'], install_requires=install_requires, # URLs diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pygit2-1.10.1/src/diff.c new/pygit2-1.11.1/src/diff.c --- old/pygit2-1.10.1/src/diff.c 2022-03-14 09:11:42.426751900 +0100 +++ new/pygit2-1.11.1/src/diff.c 2022-10-19 09:26:54.059615100 +0200 @@ -179,6 +179,31 @@ PyObject_Del(self); } +PyDoc_STRVAR(DiffFile_from_c__doc__, "Method exposed for _checkout_notify_cb to hook into"); + +/* Expose wrap_diff_file to python so we can call it in callbacks.py. */ +PyObject * +DiffFile_from_c(DiffFile *dummy, PyObject *py_diff_file_ptr) +{ + const git_diff_file *diff_file; + char *buffer; + Py_ssize_t length; + + /* Here we need to do the opposite conversion from the _pointer getters */ + if (PyBytes_AsStringAndSize(py_diff_file_ptr, &buffer, &length)) + return NULL; + + if (length != sizeof(git_diff_file *)) { + PyErr_SetString(PyExc_TypeError, "passed value is not a pointer"); + return NULL; + } + + /* the "buffer" contains the pointer */ + diff_file = *((const git_diff_file **) buffer); + + return wrap_diff_file(diff_file); +} + PyMemberDef DiffFile_members[] = { MEMBER(DiffFile, id, T_OBJECT, "Oid of the item."), MEMBER(DiffFile, path, T_STRING, "Path to the entry."), @@ -189,6 +214,10 @@ {NULL} }; +PyMethodDef DiffFile_methods[] = { + METHOD(DiffFile, from_c, METH_STATIC | METH_O), + {NULL}, +}; PyDoc_STRVAR(DiffFile__doc__, "DiffFile object."); @@ -220,7 +249,7 @@ 0, /* tp_weaklistoffset */ 0, /* tp_iter */ 0, /* tp_iternext */ - 0, /* tp_methods */ + DiffFile_methods, /* tp_methods */ DiffFile_members, /* tp_members */ 0, /* tp_getset */ 0, /* tp_base */ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pygit2-1.10.1/src/repository.c new/pygit2-1.11.1/src/repository.c --- old/pygit2-1.10.1/src/repository.c 2022-07-25 09:42:29.865465900 +0200 +++ new/pygit2-1.11.1/src/repository.c 2022-10-10 08:59:10.300657700 +0200 @@ -2301,7 +2301,7 @@ } PyDoc_STRVAR(Repository_applies__doc__, - "applies(diff: Diff, location: int = GIT_APPLY_LOCATION_INDEX) -> bool\n" + "applies(diff: Diff, location: int = GIT_APPLY_LOCATION_INDEX, raise_error: bool = False) -> bool\n" "\n" "Tests if the given patch will apply to HEAD, without writing it.\n" "\n" @@ -2313,6 +2313,10 @@ "location\n" " The location to apply: GIT_APPLY_LOCATION_WORKDIR,\n" " GIT_APPLY_LOCATION_INDEX (default), or GIT_APPLY_LOCATION_BOTH.\n" + "\n" + "raise_error\n" + " If the patch doesn't apply, raise an exception containing more details\n" + " about the failure instead of returning False.\n" ); PyObject * @@ -2320,19 +2324,24 @@ { Diff *py_diff; int location = GIT_APPLY_LOCATION_INDEX; + int raise_error = 0; git_apply_options options = GIT_APPLY_OPTIONS_INIT; options.flags |= GIT_APPLY_CHECK; - char* keywords[] = {"diff", "location", NULL}; + char* keywords[] = {"diff", "location", "raise_error", NULL}; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|i", keywords, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|ii", keywords, &DiffType, &py_diff, - &location)) + &location, &raise_error)) return NULL; int err = git_apply(self->repo, ((Diff*)py_diff)->diff, location, &options); - if (err != 0) - Py_RETURN_FALSE; + if (err != 0) { + if (raise_error) + return Error_set(err); + else + Py_RETURN_FALSE; + } Py_RETURN_TRUE; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pygit2-1.10.1/src/signature.c new/pygit2-1.11.1/src/signature.c --- old/pygit2-1.10.1/src/signature.c 2022-08-23 11:36:41.305126400 +0200 +++ new/pygit2-1.11.1/src/signature.c 2022-10-30 15:53:06.262505000 +0100 @@ -81,14 +81,16 @@ void Signature_dealloc(Signature *self) { - /* self->obj is the owner of the git_signature, so we musn't free it */ + /* self->obj is the owner of the git_signature C structure, so we musn't free it */ if (self->obj) { Py_CLEAR(self->obj); } else { git_signature_free((git_signature *) self->signature); - free(self->encoding); } + /* we own self->encoding */ + free(self->encoding); + PyObject_Del(self); } @@ -246,6 +248,7 @@ encoding = to_unicode(self->encoding, self->encoding, NULL); } else { encoding = Py_None; + Py_INCREF(Py_None); } str = PyUnicode_FromFormat( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pygit2-1.10.1/test/test_apply_diff.py new/pygit2-1.11.1/test/test_apply_diff.py --- old/pygit2-1.10.1/test/test_apply_diff.py 2022-03-20 09:43:45.006981400 +0100 +++ new/pygit2-1.11.1/test/test_apply_diff.py 2022-10-10 08:59:10.300657700 +0200 @@ -59,6 +59,18 @@ # Return the diff return pygit2.Diff.parse_diff(patch) +@pytest.fixture +def foreign_patch_diff(): + patch_contents = """diff --git a/this_file_does_not_exist b/this_file_does_not_exist +index 7f129fd..af431f2 100644 +--- a/this_file_does_not_exist ++++ b/this_file_does_not_exist +@@ -1 +1 @@ +-a contents 2 ++a contents +""" + return pygit2.Diff.parse_diff(patch_contents) + def test_apply_type_error(testrepo): # Check apply type error @@ -127,3 +139,15 @@ assert not testrepo.applies(patch_diff, pygit2.GIT_APPLY_LOCATION_WORKDIR) assert not testrepo.applies(patch_diff, pygit2.GIT_APPLY_LOCATION_INDEX) +def test_applies_error(testrepo, old_content, patch_diff, foreign_patch_diff): + # Try to apply a "foreign" patch that affects files that aren't in the repo; + # ensure we get OSError about the missing file (due to raise_error) + with pytest.raises(OSError): + testrepo.applies(foreign_patch_diff, pygit2.GIT_APPLY_LOCATION_BOTH, raise_error=True) + + # Apply a valid patch + testrepo.apply(patch_diff, pygit2.GIT_APPLY_LOCATION_BOTH) + + # Ensure it can't be applied again and we get an exception about it (due to raise_error) + with pytest.raises(pygit2.GitError): + testrepo.applies(patch_diff, pygit2.GIT_APPLY_LOCATION_BOTH, raise_error=True) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pygit2-1.10.1/test/test_remote.py new/pygit2-1.11.1/test/test_remote.py --- old/pygit2-1.10.1/test/test_remote.py 2022-05-05 16:36:05.882153000 +0200 +++ new/pygit2-1.11.1/test/test_remote.py 2022-10-30 16:57:53.820178300 +0100 @@ -162,6 +162,7 @@ name = 'upstream' url = 'https://github.com/libgit2/pygit2.git' remote = testrepo.remotes.create(name, url) + assert remote.name in testrepo.remotes.names() assert remote.name in [x.name for x in testrepo.remotes] @utils.requires_network @@ -170,7 +171,6 @@ remote = testrepo.remotes[0] refs = remote.ls_remotes() - assert refs # Check that a known ref is returned. @@ -187,6 +187,7 @@ name = 'upstream' url = 'https://github.com/libgit2/pygit2.git' remote = testrepo.remotes.create(name, url) + assert remote.name in testrepo.remotes.names() assert remote.name in [x.name for x in testrepo.remotes] @utils.refcount diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pygit2-1.10.1/test/test_repository.py new/pygit2-1.11.1/test/test_repository.py --- old/pygit2-1.10.1/test/test_repository.py 2022-05-05 16:36:05.882153000 +0200 +++ new/pygit2-1.11.1/test/test_repository.py 2022-11-06 12:29:06.013696400 +0100 @@ -77,6 +77,75 @@ assert 'new' in head.tree assert 'bye.txt' not in testrepo.status() +def test_checkout_callbacks(testrepo): + ref_i18n = testrepo.lookup_reference('refs/heads/i18n') + + class MyCheckoutCallbacks(pygit2.CheckoutCallbacks): + def __init__(self): + super().__init__() + self.conflicting_paths = set() + self.updated_paths = set() + + def checkout_notify_flags(self) -> int: + return pygit2.GIT_CHECKOUT_NOTIFY_CONFLICT | pygit2.GIT_CHECKOUT_NOTIFY_UPDATED + + def checkout_notify(self, why, path, baseline, target, workdir): + if why == pygit2.GIT_CHECKOUT_NOTIFY_CONFLICT: + self.conflicting_paths.add(path) + elif why == pygit2.GIT_CHECKOUT_NOTIFY_UPDATED: + self.updated_paths.add(path) + + # 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 + + # checkout i18n with GIT_CHECKOUT_FORCE + head = testrepo.head + head = testrepo[head.target] + assert 'new' not in head.tree + callbacks = MyCheckoutCallbacks() + testrepo.checkout(ref_i18n, strategy=pygit2.GIT_CHECKOUT_FORCE, callbacks=callbacks) + # make sure the callbacks caught the files affected by the checkout + assert set() == callbacks.conflicting_paths + assert {'bye.txt', 'new'} == callbacks.updated_paths + +def test_checkout_aborted_from_callbacks(testrepo): + ref_i18n = testrepo.lookup_reference('refs/heads/i18n') + + def read_bye_txt(): + return testrepo[testrepo.create_blob_fromworkdir("bye.txt")].data + + s = testrepo.status() + assert s == {'bye.txt': pygit2.GIT_STATUS_WT_NEW} + + class MyCheckoutCallbacks(pygit2.CheckoutCallbacks): + def __init__(self): + super().__init__() + self.invoked_times = 0 + + def checkout_notify(self, why, path, baseline, target, workdir): + self.invoked_times += 1 + # skip one file so we're certain that NO files are affected, + # even if aborting the checkout from the second file + if self.invoked_times == 2: + raise InterruptedError("Stop the checkout!") + + head = testrepo.head + head = testrepo[head.target] + assert 'new' not in head.tree + assert b'bye world\n' == read_bye_txt() + callbacks = MyCheckoutCallbacks() + + # checkout i18n with GIT_CHECKOUT_FORCE - callbacks should prevent checkout from completing + with pytest.raises(InterruptedError): + testrepo.checkout(ref_i18n, strategy=pygit2.GIT_CHECKOUT_FORCE, callbacks=callbacks) + + assert callbacks.invoked_times == 2 + assert 'new' not in head.tree + assert b'bye world\n' == read_bye_txt() + def test_checkout_branch(testrepo): branch_i18n = testrepo.lookup_branch('i18n') @@ -279,6 +348,107 @@ with pytest.raises(KeyError): testrepo.stash_pop() +def test_stash_progress_callback(testrepo): + sig = pygit2.Signature(name='Stasher', email='stas...@example.com', time=1641000000, offset=0) + + # some changes to working dir + with (Path(testrepo.workdir) / 'hello.txt').open('w') as f: + f.write('new content') + + # create the stash + testrepo.stash(sig, include_untracked=True, message="custom stash message") + + progress_sequence = [] + + class MyStashApplyCallbacks(pygit2.StashApplyCallbacks): + def stash_apply_progress(self, progress: int): + progress_sequence.append(progress) + + # apply the stash + testrepo.stash_apply(callbacks=MyStashApplyCallbacks()) + + # make sure the callbacks were notified of all the steps + assert progress_sequence == [ + pygit2.GIT_STASH_APPLY_PROGRESS_LOADING_STASH, + pygit2.GIT_STASH_APPLY_PROGRESS_ANALYZE_INDEX, + pygit2.GIT_STASH_APPLY_PROGRESS_ANALYZE_MODIFIED, + pygit2.GIT_STASH_APPLY_PROGRESS_ANALYZE_UNTRACKED, + pygit2.GIT_STASH_APPLY_PROGRESS_CHECKOUT_UNTRACKED, + pygit2.GIT_STASH_APPLY_PROGRESS_CHECKOUT_MODIFIED, + pygit2.GIT_STASH_APPLY_PROGRESS_DONE, + ] + +def test_stash_aborted_from_callbacks(testrepo): + sig = pygit2.Signature(name='Stasher', email='stas...@example.com', time=1641000000, offset=0) + + # some changes to working dir + with (Path(testrepo.workdir) / 'hello.txt').open('w') as f: + f.write('new content') + with (Path(testrepo.workdir) / 'untracked.txt').open('w') as f: + f.write('yo') + + # create the stash + testrepo.stash(sig, include_untracked=True, message="custom stash message") + + # define callbacks that will abort the unstash process + # just as libgit2 is ready to write the files to disk + class MyStashApplyCallbacks(pygit2.StashApplyCallbacks): + def stash_apply_progress(self, progress: int): + if progress == pygit2.GIT_STASH_APPLY_PROGRESS_CHECKOUT_UNTRACKED: + raise InterruptedError("Stop applying the stash!") + + # attempt to apply and delete the stash; the callbacks will interrupt that + with pytest.raises(InterruptedError): + testrepo.stash_pop(callbacks=MyStashApplyCallbacks()) + + # we interrupted right before the checkout part of the unstashing process, + # so the untracked file shouldn't be here + assert not (Path(testrepo.workdir) / 'untracked.txt').exists() + + # and hello.txt should be as it is on master + with (Path(testrepo.workdir) / 'hello.txt').open('r') as f: + assert f.read() == 'hello world\nhola mundo\nbonjour le monde\n' + + # and since we didn't let stash_pop run to completion, the stash itself should still be here + repo_stashes = testrepo.listall_stashes() + assert 1 == len(repo_stashes) + assert repo_stashes[0].message == "On master: custom stash message" + +def test_stash_apply_checkout_options(testrepo): + sig = pygit2.Signature(name='Stasher', email='stas...@example.com', time=1641000000, offset=0) + + hello_txt = Path(testrepo.workdir) / 'hello.txt' + + # some changes to working dir + with hello_txt.open('w') as f: + f.write('stashed content') + + # create the stash + testrepo.stash(sig, include_untracked=True, message="custom stash message") + + # define callbacks that raise an InterruptedError when checkout detects a conflict + class MyStashApplyCallbacks(pygit2.StashApplyCallbacks): + def checkout_notify(self, why, path, baseline, target, workdir): + if why == pygit2.GIT_CHECKOUT_NOTIFY_CONFLICT: + raise InterruptedError("Applying the stash would create a conflict") + + # overwrite hello.txt so that applying the stash would create a conflict + with hello_txt.open('w') as f: + f.write('conflicting content') + + # apply the stash with the default (safe) strategy; + # the callbacks should detect a conflict on checkout + with pytest.raises(InterruptedError): + testrepo.stash_apply(strategy=pygit2.GIT_CHECKOUT_SAFE, callbacks=MyStashApplyCallbacks()) + + # hello.txt should be intact + with hello_txt.open('r') as f: assert f.read() == 'conflicting content' + + # force apply the stash; this should work + testrepo.stash_apply(strategy=pygit2.GIT_CHECKOUT_FORCE, callbacks=MyStashApplyCallbacks()) + with hello_txt.open('r') as f: assert f.read() == 'stashed content' + + def test_revert(testrepo): master = testrepo.head.peel() commit_to_revert = testrepo['4ec4389a8068641da2d6578db0419484972284c8'] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pygit2-1.10.1/test/utils.py new/pygit2-1.11.1/test/utils.py --- old/pygit2-1.10.1/test/utils.py 2022-04-23 18:14:30.410124800 +0200 +++ new/pygit2-1.11.1/test/utils.py 2022-11-04 09:27:27.316116800 +0100 @@ -30,7 +30,6 @@ import socket import stat import sys -import tarfile import zipfile # Requirements @@ -112,9 +111,6 @@ if path.suffix == '.zip': with zipfile.ZipFile(path) as zipf: zipf.extractall(self.tmp_path) - elif path.suffix == '.tar': - with tarfile.open(path) as tar: - tar.extractall(self.tmp_path) elif path.suffix == '.git': shutil.copytree(path, temp_repo_path) else: