diff --git a/.travis.sh b/.travis.sh index 317d318..54bfd01 100755 --- a/.travis.sh +++ b/.travis.sh @@ -2,7 +2,7 @@ cd ~ -git clone --depth=1 -b v0.21.2 https://github.com/libgit2/libgit2.git +git clone --depth=1 -b maint/v0.22 https://github.com/libgit2/libgit2.git cd libgit2/ mkdir build && cd build diff --git a/README.rst b/README.rst index 9febfa5..6110310 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ pygit2 - libgit2 bindings in Python ###################################################################### -.. image:: https://secure.travis-ci.org/libgit2/pygit2.png +.. image:: https://secure.travis-ci.org/libgit2/pygit2.svg :target: http://travis-ci.org/libgit2/pygit2 Pygit2 is a set of Python bindings to the libgit2 shared library, libgit2 @@ -25,6 +25,62 @@ How to install Changelog ============== +0.22.0 (2015-01-16) +------------------- + +New: + +- Update to libgit2 v0.22 + `#459 `_ + +- Add support for libgit2 feature detection + (new ``pygit2.features`` and ``pygit2.GIT_FEATURE_*``) + `#475 `_ + +- New ``Repository.remotes`` (``RemoteCollection``) + `#447 `_ + +API Changes: + +- Prototype of ``clone_repository`` changed, check documentation + +- Removed ``clone_into``, use ``clone_repository`` with callbacks instead + +- Use ``Repository.remotes.rename(name, new_name)`` instead of + ``Remote.rename(new_name)`` + +- Use ``Repository.remotes.delete(name)`` instead of ``Remote.delete()`` + +- Now ``Remote.push(...)`` takes a list of refspecs instead of just one + +- Change ``Patch.old_id``, ``Patch.new_id``, ``Note.annotated_id``, + ``RefLogEntry.oid_old`` and ``RefLogEntry.oid_new`` to be ``Oid`` objects + instead of strings + `#449 `_ + +Other: + +- Fix ``init_repository`` when passing optional parameters ``workdir_path``, + ``description``, ``template_path``, ``initial_head`` or ``origin_url`` + `#466 `_ + `#471 `_ + +- Fix use-after-free when patch outlives diff + `#457 `_ + `#461 `_ + `#474 `_ + +- Documentation improvements + `#456 `_ + `#462 `_ + `#465 `_ + `#472 `_ + `#473 `_ + +- Make the GPL exception explicit in setup.py + `#450 `_ + + 0.21.4 (2014-11-04) ------------------- @@ -446,7 +502,7 @@ Other: `#331 `_ Authors ============== -77 developers have contributed at least 1 commit to pygit2:: +83 developers have contributed at least 1 commit to pygit2:: J. David Ibáñez Sebastian Thiel András Veres-Szentkirályi Carlos Martín Nieto Fraser Tweedale Ash Berlin diff --git a/docs/conf.py b/docs/conf.py index f9f7fdc..0b3b646 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -50,9 +50,9 @@ copyright = u'2010-2014 The pygit2 contributors' # built documents. # # The short X.Y version. -version = '0.21' +version = '0.22' # The full version, including alpha/beta/rc tags. -release = '0.21.4' +release = '0.22.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/features.rst b/docs/features.rst new file mode 100644 index 0000000..2b26fde --- /dev/null +++ b/docs/features.rst @@ -0,0 +1,8 @@ +********************************************************************** +Feature detection +********************************************************************** + +.. py:data:: pygit2.features + + This variable contains a combination of `GIT_FEATURE_*` flags, + indicating which features a particular build of libgit2 supports. diff --git a/docs/general.rst b/docs/general.rst index 37bc32e..1518972 100644 --- a/docs/general.rst +++ b/docs/general.rst @@ -18,7 +18,7 @@ library that has been built against. The version number has a .. py:data:: LIBGIT2_VER_MAJOR Integer value of the major version number. For example, for the version - ``0.21.2``:: + ``0.22.0``:: >>> print LIBGIT2_VER_MAJOR 0 @@ -26,25 +26,25 @@ library that has been built against. The version number has a .. py:data:: LIBGIT2_VER_MINOR Integer value of the minor version number. For example, for the version - ``0.21.2``:: + ``0.22.0``:: >>> print LIBGIT2_VER_MINOR - 21 + 22 .. py:data:: LIBGIT2_VER_REVISION Integer value of the revision version number. For example, for the version - ``0.21.2``:: + ``0.22.0``:: >>> print LIBGIT2_VER_REVISION - 1 + 0 .. py:data:: LIBGIT2_VERSION The libgit2 version number as a string:: >>> print LIBGIT2_VERSION - '0.21.2' + '0.22.0' Errors ====== diff --git a/docs/index.rst b/docs/index.rst index fbdfd40..156f0ac 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -47,6 +47,7 @@ Usage guide: remotes blame settings + features Indices and tables diff --git a/docs/install.rst b/docs/install.rst index c63b254..88af0b4 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -14,8 +14,9 @@ Requirements ============ - Python 2.7, 3.2+ or pypy (including the development headers) -- Libgit2 v0.21.1+ +- Libgit2 v0.22.x - cffi 0.8.1+ +- Libssh2, optional, used for SSH network operations. .. warning:: @@ -33,11 +34,11 @@ while the last number |lq| *.micro* |rq| auto-increments independently. As illustration see this table of compatible releases: -+-----------+---------------------------------------+------------------------------+--------------+ -|**libgit2**|0.21.1, 0.21.2 |0.20.0 |0.19.0 | -+-----------+---------------------------------------+------------------------------+--------------+ -|**pygit2** |0.21.0, 0.21.1, 0.21.2, 0.21.3, 0.21.4 |0.20.0, 0.20.1, 0.20.2, 0.20.3|0.19.0, 0.19.1| -+-----------+---------------------------------------+------------------------------+--------------+ ++-----------+--------+----------------------------------------+-------------------------------+ +|**libgit2**| 0.22.0 | 0.21.1, 0.21.2 |0.20.0 | ++-----------+--------+----------------------------------------+-------------------------------+ +|**pygit2** | 0.22.0 | 0.21.0, 0.21.1, 0.21.2, 0.21.3, 0.21.4 | 0.20.0, 0.20.1, 0.20.2, 0.20.3| ++-----------+--------+----------------------------------------+-------------------------------+ .. warning:: @@ -54,9 +55,9 @@ directory, do: .. code-block:: sh - $ wget https://github.com/libgit2/libgit2/archive/v0.21.2.tar.gz - $ tar xzf v0.21.2.tar.gz - $ cd libgit2-0.21.2/ + $ wget https://github.com/libgit2/libgit2/archive/v0.22.0.tar.gz + $ tar xzf v0.22.0.tar.gz + $ cd libgit2-0.22.0/ $ cmake . $ make $ sudo make install @@ -138,9 +139,9 @@ Install libgit2 (see we define the installation prefix): .. code-block:: sh - $ wget https://github.com/libgit2/libgit2/archive/v0.21.2.tar.gz - $ tar xzf v0.21.2.tar.gz - $ cd libgit2-0.21.2/ + $ wget https://github.com/libgit2/libgit2/archive/v0.22.0.tar.gz + $ tar xzf v0.22.0.tar.gz + $ cd libgit2-0.22.0/ $ cmake . -DCMAKE_INSTALL_PREFIX=$LIBGIT2 $ make $ make install @@ -193,9 +194,9 @@ from a bash shell: .. code-block:: sh $ export LIBGIT2=C:/Dev/libgit2 - $ wget https://github.com/libgit2/libgit2/archive/v0.21.2.tar.gz - $ tar xzf v0.21.2.tar.gz - $ cd libgit2-0.21.2/ + $ wget https://github.com/libgit2/libgit2/archive/v0.22.0.tar.gz + $ tar xzf v0.22.0.tar.gz + $ cd libgit2-0.22.0/ $ cmake . -DSTDCALL=OFF -DCMAKE_INSTALL_PREFIX=$LIBGIT2 -G "Visual Studio 9 2008" $ cmake --build . --config release --target install $ ctest -v diff --git a/docs/recipes.rst b/docs/recipes.rst index 019f863..14f31c0 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -17,7 +17,6 @@ Main porcelain commands .. toctree:: :maxdepth: 1 - git-branch (List, create, or delete branches.) git-init (Create an empty git repository or reinitialize an existing one.) git-log (Show commit logs.) git-show (Show various types of objects.) diff --git a/docs/recipes/git-branch.rst b/docs/recipes/git-branch.rst deleted file mode 100644 index 5db822b..0000000 --- a/docs/recipes/git-branch.rst +++ /dev/null @@ -1,30 +0,0 @@ -********************************************************************** -git-branch -********************************************************************** - ----------------------------------------------------------------------- -Listing branches ----------------------------------------------------------------------- - -====================================================================== -List all branches -====================================================================== - -.. code-block:: bash - - $> git branch - -.. code-block:: python - - >>> regex = re.compile('^refs/heads/') - >>> branches = filter(lambda r: regex.match(r), repo.listall_references()) - -`Note that the next release will probably allow` ``repo.listall_branches()``. - ----------------------------------------------------------------------- -References ----------------------------------------------------------------------- - -- git-branch_. - -.. _git-branch: https://www.kernel.org/pub/software/scm/git/docs/git-branch.html diff --git a/docs/remotes.rst b/docs/remotes.rst index dfa8804..387e9ac 100644 --- a/docs/remotes.rst +++ b/docs/remotes.rst @@ -2,10 +2,18 @@ Remotes ********************************************************************** +.. py:attribute:: Repository.remotes + + The collection of configured remotes, an instance of + :py:class:`pygit2.remote.RemoteCollection` -.. autoattribute:: pygit2.Repository.remotes .. automethod:: pygit2.Repository.create_remote +The remote collection +========================== + +.. autoclass:: pygit2.remote.RemoteCollection + :members: The Remote type ==================== @@ -24,6 +32,10 @@ This class contains the data which is available to us during a fetch. The Refspec type =================== +Refspecs objects are not constructed directly, but returned by +:meth:`pygit2.Remote.get_refspec`. To create a new a refspec on a Remote, use +:meth:`pygit2.Remote.add_fetch` or :meth:`pygit2.Remote.add_push`. + .. autoclass:: pygit2.refspec.Refspec :members: diff --git a/pygit2/__init__.py b/pygit2/__init__.py index 2674063..601a7b9 100644 --- a/pygit2/__init__.py +++ b/pygit2/__init__.py @@ -41,7 +41,7 @@ from .index import Index, IndexEntry from .remote import Remote, get_credentials from .repository import Repository from .settings import Settings -from .utils import to_bytes +from .utils import to_bytes, to_str from ._utils import __version__ @@ -86,11 +86,26 @@ def init_repository(path, bare=False, C.git_repository_init_init_options(options, C.GIT_REPOSITORY_INIT_OPTIONS_VERSION) options.flags = flags options.mode = mode - options.workdir_path = to_bytes(workdir_path) - options.description = to_bytes(description) - options.template_path = to_bytes(template_path) - options.initial_head = to_bytes(initial_head) - options.origin_url = to_bytes(origin_url) + + if workdir_path: + workdir_path_ref = ffi.new('char []', to_bytes(workdir_path)) + options.workdir_path = workdir_path_ref + + if description: + description_ref = ffi.new('char []', to_bytes(description)) + options.description = description_ref + + if template_path: + template_path_ref = ffi.new('char []', to_bytes(template_path)) + options.template_path = template_path_ref + + if initial_head: + initial_head_ref = ffi.new('char []', to_bytes(initial_head)) + options.initial_head = initial_head_ref + + if origin_url: + origin_url_ref = ffi.new('char []', to_bytes(origin_url)) + options.origin_url = origin_url_ref # Call crepository = ffi.new('git_repository **') @@ -98,7 +113,7 @@ def init_repository(path, bare=False, check_error(err) # Ok - return Repository(path) + return Repository(to_str(path)) @ffi.callback('int (*credentials)(git_cred **cred, const char *url,' @@ -108,7 +123,7 @@ def _credentials_cb(cred_out, url, username_from_url, allowed, data): d = ffi.from_handle(data) try: - ccred = get_credentials(d['callback'], url, username_from_url, allowed) + ccred = get_credentials(d['credentials_cb'], url, username_from_url, allowed) cred_out[0] = ccred[0] except Exception as e: d['exception'] = e @@ -116,10 +131,54 @@ def _credentials_cb(cred_out, url, username_from_url, allowed, data): return 0 +@ffi.callback('int (*git_repository_create_cb)(git_repository **out,' + 'const char *path, int bare, void *payload)') +def _repository_create_cb(repo_out, path, bare, data): + d = ffi.from_handle(data) + try: + repository = d['repository_cb'](ffi.string(path), bare != 0) + # we no longer own the C object + repository._disown() + repo_out[0] = repository._repo + except Exception as e: + d['exception'] = e + return C.GIT_EUSER + + return 0 + +@ffi.callback('int (*git_remote_create_cb)(git_remote **out, git_repository *repo,' + 'const char *name, const char *url, void *payload)') +def _remote_create_cb(remote_out, repo, name, url, data): + d = ffi.from_handle(data) + try: + remote = d['remote_cb'](Repository._from_c(repo, False), ffi.string(name), ffi.string(url)) + remote_out[0] = remote._remote + # we no longer own the C object + remote._remote = ffi.NULL + except Exception as e: + d['exception'] = e + return C.GIT_EUSER + + return 0 + +@ffi.callback('int (*git_transport_certificate_check_cb)' + '(git_cert *cert, int valid, const char *host, void *payload)') +def _certificate_cb(cert_i, valid, host, data): + d = ffi.from_handle(data) + try: + # python's parting is deep in the libraries and assumes an OpenSSL-owned cert + val = d['certificate_cb'](None, bool(valid), ffi.string(host)) + if not val: + return C.GIT_ECERTIFICATE + except Exception as e: + d['exception'] = e + return C.GIT_EUSER + + return 0 def clone_repository( - url, path, bare=False, ignore_cert_errors=False, - remote_name="origin", checkout_branch=None, credentials=None): + url, path, bare=False, repository=None, remote=None, + checkout_branch=None, credentials=None, certificate=None): """Clones a new Git repository from *url* in the given *path*. Returns a Repository class pointing to the newly cloned repository. @@ -130,7 +189,9 @@ def clone_repository( :param bool bare: Whether the local repository should be bare - :param str remote_name: Name to give the remote at *url*. + :param callable remote: Callback for the remote to use. + + :param callable repository: Callback for the repository to use. :param str checkout_branch: Branch to checkout after the clone. The default is to use the remote's default branch. @@ -138,8 +199,22 @@ def clone_repository( :param callable credentials: authentication to use if the remote requires it + :param callable certificate: callback to verify the host's + certificate or fingerprint. + :rtype: Repository + The repository callback has `(path, bare) -> Repository` as a + signature. The Repository it returns will be used instead of + creating a new one. + + The remote callback has `(Repository, name, url) -> Remote` as a + signature. The Remote it returns will be used instead of the default + one. + + The certificate callback has `(cert, valid, hostname) -> bool` as + a signature. Return True to accept the connection, False to abort. + """ opts = ffi.new('git_clone_options *') @@ -149,7 +224,10 @@ def clone_repository( # Data, let's use a dict as we don't really want much more d = {} - d['callback'] = credentials + d['credentials_cb'] = credentials + d['repository_cb'] = repository + d['remote_cb'] = remote + d['certificate_cb'] = certificate d_handle = ffi.new_handle(d) # Perform the initialization with the version we compiled @@ -161,48 +239,36 @@ def clone_repository( checkout_branch_ref = ffi.new('char []', to_bytes(branch)) opts.checkout_branch = checkout_branch_ref - remote_name_ref = ffi.new('char []', to_bytes(remote_name)) - opts.remote_name = remote_name_ref + if repository: + opts.repository_cb = _repository_create_cb + opts.repository_cb_payload = d_handle + + if remote: + opts.remote_cb = _remote_create_cb + opts.remote_cb_payload = d_handle + - opts.ignore_cert_errors = ignore_cert_errors opts.bare = bare if credentials: opts.remote_callbacks.credentials = _credentials_cb opts.remote_callbacks.payload = d_handle + if certificate: + opts.remote_callbacks.certificate_check = _certificate_cb + opts.remote_callbacks.payload = d_handle + err = C.git_clone(crepo, to_bytes(url), to_bytes(path), opts) - C.git_repository_free(crepo[0]) if 'exception' in d: raise d['exception'] check_error(err) - return Repository(path) - - -def clone_into(repo, remote, branch=None): - """Clone into an empty repository from the specified remote - - :param Repository repo: The empty repository into which to clone - - :param Remote remote: The remote from which to clone - - :param str branch: Branch to checkout after the clone. Pass None - to use the remotes's default branch. - - This allows you specify arbitrary repository and remote configurations - before performing the clone step itself. E.g. you can replicate git-clone's - '--mirror' option by setting a refspec of '+refs/*:refs/*', 'core.mirror' - to true and calling this function. - """ - - err = C.git_clone_into(repo._repo, remote._remote, ffi.NULL, - to_bytes(branch), ffi.NULL) - - if remote._stored_exception: - raise remote._stored_exception - - check_error(err) + return Repository._from_c(crepo[0], owned=True) settings = Settings() + +features = C.git_libgit2_features() +GIT_FEATURE_THREADS = C.GIT_FEATURE_THREADS +GIT_FEATURE_HTTPS = C.GIT_FEATURE_HTTPS +GIT_FEATURE_SSH = C.GIT_FEATURE_SSH diff --git a/pygit2/_utils.py b/pygit2/_utils.py index 9ecb35b..9c13d9b 100644 --- a/pygit2/_utils.py +++ b/pygit2/_utils.py @@ -43,7 +43,7 @@ import sys # # The version number of pygit2 # -__version__ = '0.21.4' +__version__ = '0.22.0' # diff --git a/pygit2/credentials.py b/pygit2/credentials.py index 39d86ee..adefc6c 100644 --- a/pygit2/credentials.py +++ b/pygit2/credentials.py @@ -65,7 +65,8 @@ class Keypair(object): remote server :param str pubkey: the path to the user's public key file :param str privkey: the path to the user's private key file - :param str passphrase: the password used to decrypt the private key file + :param str passphrase: the password used to decrypt the private key file, + or empty string if no passphrase is required. """ diff --git a/pygit2/decl.h b/pygit2/decl.h index 5c4d9a6..e89f60f 100644 --- a/pygit2/decl.h +++ b/pygit2/decl.h @@ -1,7 +1,6 @@ typedef ... git_repository; typedef ... git_remote; typedef ... git_refspec; -typedef ... git_push; typedef ... git_cred; typedef ... git_object; typedef ... git_tree; @@ -74,6 +73,12 @@ typedef struct git_signature { git_time when; } git_signature; +#define GIT_FEATURE_THREADS ... +#define GIT_FEATURE_HTTPS ... +#define GIT_FEATURE_SSH ... + +int git_libgit2_features(void); + const git_error * giterr_last(void); void git_strarray_free(git_strarray *array); @@ -106,9 +111,38 @@ typedef enum { GIT_CREDTYPE_SSH_KEY, GIT_CREDTYPE_SSH_CUSTOM, GIT_CREDTYPE_DEFAULT, + GIT_CREDTYPE_SSH_INTERACTIVE, + GIT_CREDTYPE_USERNAME, ... } git_credtype_t; +typedef enum git_cert_t { + GIT_CERT_X509, + GIT_CERT_HOSTKEY_LIBSSH2, +} git_cert_t; + +typedef enum { + GIT_CERT_SSH_MD5 = 1, + GIT_CERT_SSH_SHA1 = 2, +} git_cert_ssh_t; + +typedef struct { + git_cert_t cert_type; + git_cert_ssh_t type; + unsigned char hash_md5[16]; + unsigned char hash_sha1[20]; +} git_cert_hostkey; + +typedef struct { + git_cert_t cert_type; + void *data; + size_t len; +} git_cert_x509; + +typedef struct { + git_cert_t cert_type; +} git_cert; + typedef int (*git_transport_message_cb)(const char *str, int len, void *data); typedef int (*git_cred_acquire_cb)( git_cred **cred, @@ -117,40 +151,59 @@ typedef int (*git_cred_acquire_cb)( unsigned int allowed_types, void *payload); typedef int (*git_transfer_progress_cb)(const git_transfer_progress *stats, void *payload); +typedef int (*git_transport_certificate_check_cb)(git_cert *cert, int valid, const char *host, void *payload); + +typedef int (*git_packbuilder_progress)( + int stage, + unsigned int current, + unsigned int total, + void *payload); +typedef int (*git_push_transfer_progress)( + unsigned int current, + unsigned int total, + size_t bytes, + void* payload); struct git_remote_callbacks { unsigned int version; git_transport_message_cb sideband_progress; int (*completion)(git_remote_completion_type type, void *data); git_cred_acquire_cb credentials; + git_transport_certificate_check_cb certificate_check; git_transfer_progress_cb transfer_progress; int (*update_tips)(const char *refname, const git_oid *a, const git_oid *b, void *data); + git_packbuilder_progress pack_progress; + git_push_transfer_progress push_transfer_progress; + int (*push_update_reference)(const char *refname, const char *status, void *data); void *payload; }; typedef struct git_remote_callbacks git_remote_callbacks; +typedef struct { + unsigned int version; + unsigned int pb_parallelism; +} git_push_options; + int git_remote_list(git_strarray *out, git_repository *repo); -int git_remote_load(git_remote **out, git_repository *repo, const char *name); +int git_remote_lookup(git_remote **out, git_repository *repo, const char *name); int git_remote_create( git_remote **out, git_repository *repo, const char *name, const char *url); -int git_remote_delete(git_remote *remote); +int git_remote_delete(git_repository *repo, const char *name); int git_repository_state_cleanup(git_repository *repo); const char * git_remote_name(const git_remote *remote); -int git_remote_rename( - git_strarray *problems, - git_remote *remote, - const char *new_name); +int git_remote_rename(git_strarray *problems, git_repository *repo, const char *name, const char *new_name); const char * git_remote_url(const git_remote *remote); int git_remote_set_url(git_remote *remote, const char* url); const char * git_remote_pushurl(const git_remote *remote); int git_remote_set_pushurl(git_remote *remote, const char* url); -int git_remote_fetch(git_remote *remote, const git_signature *signature, const char *reflog_message); +int git_remote_fetch(git_remote *remote, const git_strarray *refspecs, const git_signature *signature, const char *reflog_message); +int git_remote_push(git_remote *remote, git_strarray *refspecs, const git_push_options *opts, const git_signature *signature, const char *reflog_message); const git_transfer_progress * git_remote_stats(git_remote *remote); int git_remote_add_push(git_remote *remote, const char *refspec); int git_remote_add_fetch(git_remote *remote, const char *refspec); @@ -167,22 +220,6 @@ int git_remote_set_push_refspecs(git_remote *remote, git_strarray *array); void git_remote_free(git_remote *remote); -int git_push_new(git_push **push, git_remote *remote); -int git_push_add_refspec(git_push *push, const char *refspec); -int git_push_finish(git_push *push); -int git_push_unpack_ok(git_push *push); - -int git_push_status_foreach( - git_push *push, - int (*cb)(const char *ref, const char *msg, void *data), - void *data); - -int git_push_update_tips( - git_push *push, - const git_signature *signature, - const char *reflog_message); -void git_push_free(git_push *push); - const char * git_refspec_src(const git_refspec *refspec); const char * git_refspec_dst(const git_refspec *refspec); int git_refspec_force(const git_refspec *refspec); @@ -261,14 +298,12 @@ typedef int (*git_diff_notify_cb)( typedef struct { unsigned int version; uint32_t flags; - git_submodule_ignore_t ignore_submodules; git_strarray pathspec; git_diff_notify_cb notify_cb; void *notify_payload; - - uint16_t context_lines; - uint16_t interhunk_lines; + uint32_t context_lines; + uint32_t interhunk_lines; uint16_t id_abbrev; git_off_t max_size; const char *old_prefix; @@ -339,13 +374,6 @@ typedef struct git_checkout_options { const char *their_label; } git_checkout_options; -typedef enum { - GIT_CLONE_LOCAL_AUTO, - GIT_CLONE_LOCAL, - GIT_CLONE_NO_LOCAL, - GIT_CLONE_LOCAL_NO_LINKS, -} git_clone_local_t; - int git_checkout_init_options(git_checkout_options *opts, unsigned int version); int git_checkout_tree(git_repository *repo, const git_object *treeish, const git_checkout_options *opts); int git_checkout_head(git_repository *repo, const git_checkout_options *opts); @@ -355,18 +383,38 @@ int git_checkout_index(git_repository *repo, git_index *index, const git_checkou * git_clone */ +typedef int (*git_remote_create_cb)( + git_remote **out, + git_repository *repo, + const char *name, + const char *url, + void *payload); + +typedef int (*git_repository_create_cb)( + git_repository **out, + const char *path, + int bare, + void *payload); + +typedef enum { + GIT_CLONE_LOCAL_AUTO, + GIT_CLONE_LOCAL, + GIT_CLONE_NO_LOCAL, + GIT_CLONE_LOCAL_NO_LINKS, +} git_clone_local_t; + typedef struct git_clone_options { unsigned int version; - git_checkout_options checkout_opts; git_remote_callbacks remote_callbacks; - int bare; - int ignore_cert_errors; git_clone_local_t local; - const char *remote_name; const char* checkout_branch; git_signature *signature; + git_repository_create_cb repository_cb; + void *repository_cb_payload; + git_remote_create_cb remote_cb; + void *remote_cb_payload; } git_clone_options; #define GIT_CLONE_OPTIONS_VERSION ... @@ -377,13 +425,6 @@ int git_clone(git_repository **out, const char *local_path, const git_clone_options *options); -int git_clone_into( - git_repository *repo, - git_remote *remote, - const git_checkout_options *co_opts, - const char *branch, - const git_signature *signature); - /* * git_config */ diff --git a/pygit2/refspec.py b/pygit2/refspec.py index 423b72b..e551710 100644 --- a/pygit2/refspec.py +++ b/pygit2/refspec.py @@ -35,6 +35,7 @@ from .utils import to_bytes class Refspec(object): + """The constructor is for internal use only""" def __init__(self, owner, ptr): self._owner = owner self._refspec = ptr @@ -95,7 +96,7 @@ class Refspec(object): return self._transform(ref, C.git_refspec_transform) def rtransform(self, ref): - """transform(str) -> str + """rtransform(str) -> str Transform a reference name according to this refspec from the lhs to the rhs""" diff --git a/pygit2/remote.py b/pygit2/remote.py index c69578c..c4a195f 100644 --- a/pygit2/remote.py +++ b/pygit2/remote.py @@ -121,6 +121,16 @@ class Remote(object): :param Oid new: the reference's new value """ + def push_update_reference(self, refname, message): + """Push update reference callback + + Override with your own function to report the remote's + acceptace or rejection of reference updates. + + :param str refname: the name of the reference (on the remote) + :param str messsage: rejection message from the remote. If None, the update was accepted. + """ + def __init__(self, repo, ptr): """The constructor is for internal use only""" @@ -137,25 +147,6 @@ class Remote(object): return maybe_string(C.git_remote_name(self._remote)) - def rename(self, new_name): - """Rename this remote - - Returns a list of fetch refspecs which were not in the standard format - and thus could not be remapped - """ - - if not new_name: - raise ValueError("New remote name must be a non-empty string") - - problems = ffi.new('git_strarray *') - err = C.git_remote_rename(problems, self._remote, to_bytes(new_name)) - check_error(err) - - ret = strarray_to_strings(problems) - C.git_strarray_free(problems) - - return ret - @property def url(self): """Url of the remote""" @@ -178,14 +169,6 @@ class Remote(object): err = C.git_remote_set_pushurl(self._remote, to_bytes(value)) check_error(err) - def delete(self): - """Remove this remote - - All remote-tracking branches and configuration settings for the remote will be removed. - """ - err = C.git_remote_delete(self._remote) - check_error(err) - def save(self): """save() @@ -231,7 +214,7 @@ class Remote(object): self._stored_exception = None try: - err = C.git_remote_fetch(self._remote, ptr, to_bytes(message)) + err = C.git_remote_fetch(self._remote, ffi.NULL, ptr, to_bytes(message)) if self._stored_exception: raise self._stored_exception @@ -293,7 +276,7 @@ class Remote(object): def add_fetch(self, spec): """add_fetch(refspec) - Add a fetch refspec to the remote""" + Add a fetch refspec (str) to the remote""" err = C.git_remote_add_fetch(self._remote, to_bytes(spec)) check_error(err) @@ -301,32 +284,35 @@ class Remote(object): def add_push(self, spec): """add_push(refspec) - Add a push refspec to the remote""" + Add a push refspec (str) to the remote""" err = C.git_remote_add_push(self._remote, to_bytes(spec)) check_error(err) - @ffi.callback("int (*cb)(const char *ref, const char *msg, void *data)") - def _push_cb(ref, msg, data): - self = ffi.from_handle(data) - if msg: - self._bad_message = ffi.string(msg).decode() - return 0 + def push(self, specs, signature=None, message=None): + """push(specs, signature, message) - def push(self, spec, signature=None, message=None): - """push(refspec, signature, message) + Push the given refspec to the remote. Raises ``GitError`` on + protocol error or unpack failure. - Push the given refspec to the remote. Raises ``GitError`` on error. - - :param str spec: push refspec to use + :param [str] specs: push refspecs to use :param Signature signature: signature to use when updating the tips :param str message: message to use when updating the tips + """ # Get the default callbacks first defaultcallbacks = ffi.new('git_remote_callbacks *') err = C.git_remote_init_callbacks(defaultcallbacks, 1) check_error(err) + refspecs, refspecs_refs = strings_to_strarray(specs) + if signature: + sig_cptr = ffi.new('git_signature **') + ffi.buffer(sig_cptr)[:] = signature._pointer[:] + sig_ptr = sig_cptr[0] + else: + sig_ptr = ffi.NULL + # Build custom callback structure callbacks = ffi.new('git_remote_callbacks *') callbacks.version = 1 @@ -334,53 +320,23 @@ class Remote(object): callbacks.transfer_progress = self._transfer_progress_cb callbacks.update_tips = self._update_tips_cb callbacks.credentials = self._credentials_cb + callbacks.push_update_reference = self._push_update_reference_cb # We need to make sure that this handle stays alive self._self_handle = ffi.new_handle(self) callbacks.payload = self._self_handle - err = C.git_remote_set_callbacks(self._remote, callbacks) - try: + err = C.git_remote_set_callbacks(self._remote, callbacks) check_error(err) except: self._self_handle = None raise - - cpush = ffi.new('git_push **') - err = C.git_push_new(cpush, self._remote) - check_error(err) - - push = cpush[0] - try: - err = C.git_push_add_refspec(push, to_bytes(spec)) + err = C.git_remote_push(self._remote, refspecs, ffi.NULL, sig_ptr, to_bytes(message)) check_error(err) - - err = C.git_push_finish(push) - check_error(err) - - if not C.git_push_unpack_ok(push): - raise GitError("remote failed to unpack objects") - - err = C.git_push_status_foreach(push, self._push_cb, - ffi.new_handle(self)) - check_error(err) - - if hasattr(self, '_bad_message'): - raise GitError(self._bad_message) - - if signature: - ptr = signature._pointer[:] - else: - ptr = ffi.NULL - - err = C.git_push_update_tips(push, ptr, to_bytes(message)) - check_error(err) - finally: self._self_handle = None - C.git_push_free(push) # These functions exist to be called by the git_remote as # callbacks. They proxy the call to whatever the user set @@ -437,6 +393,23 @@ class Remote(object): return 0 + @ffi.callback("int (*push_update_reference)(const char *ref, const char *msg, void *data)") + def _push_update_reference_cb(ref, msg, data): + self = ffi.from_handle(data) + + if not hasattr(self, 'push_update_reference') or not self.push_update_reference: + return 0 + + try: + refname = ffi.string(ref) + message = maybe_string(msg) + self.push_update_reference(refname, message) + except Exception as e: + self._stored_exception = e + return C.GIT_EUSER + + return 0 + @ffi.callback('int (*credentials)(git_cred **cred, const char *url,' 'const char *username_from_url, unsigned int allowed_types,' 'void *data)') @@ -494,3 +467,99 @@ def get_credentials(fn, url, username, allowed): check_error(err) return ccred + +class RemoteCollection(object): + """Collection of configured remotes + + You can use this class to look up and manage the remotes configured + in a repository. You can access repositories using index + access. E.g. to look up the "origin" remote, you can use + + >>> repo.remotes["origin"] + """ + + def __init__(self, repo): + self._repo = repo; + + def __len__(self): + names = ffi.new('git_strarray *') + + try: + err = C.git_remote_list(names, self._repo._repo) + check_error(err) + + return names.count + finally: + C.git_strarray_free(names) + + def __iter__(self): + names = ffi.new('git_strarray *') + + try: + err = C.git_remote_list(names, self._repo._repo) + 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) + + def __getitem__(self, name): + if isinstance(name, int): + return list(self)[name] + + cremote = ffi.new('git_remote **') + err = C.git_remote_lookup(cremote, self._repo._repo, to_bytes(name)) + check_error(err) + + return Remote(self._repo, cremote[0]) + + def create(self, name, url): + """create(name, url) -> Remote + + Create a new remote with the given name and url. + """ + + cremote = ffi.new('git_remote **') + + err = C.git_remote_create(cremote, self._repo._repo, to_bytes(name), to_bytes(url)) + check_error(err) + + return Remote(self._repo, cremote[0]) + + def rename(self, name, new_name): + """rename(name, new_name) -> [str] + + Rename a remote in the configuration. The refspecs in strandard + format will be renamed. + + Returns a list of fetch refspecs which were not in the standard format + and thus could not be remapped + """ + + if not new_name: + raise ValueError("Current remote name must be a non-empty string") + + if not new_name: + raise ValueError("New remote name must be a non-empty string") + + problems = ffi.new('git_strarray *') + err = C.git_remote_rename(problems, self._repo._repo, to_bytes(name), to_bytes(new_name)) + check_error(err) + + ret = strarray_to_strings(problems) + C.git_strarray_free(problems) + + return ret + + def delete(self, name): + """Remove a remote from the configuration + + All remote-tracking branches and configuration settings for the remote will be removed. + """ + err = C.git_remote_delete(self._repo._repo, to_bytes(name)) + check_error(err) diff --git a/pygit2/repository.py b/pygit2/repository.py index d9c1eed..3dd14f1 100644 --- a/pygit2/repository.py +++ b/pygit2/repository.py @@ -48,7 +48,7 @@ from .config import Config from .errors import check_error from .ffi import ffi, C from .index import Index -from .remote import Remote +from .remote import RemoteCollection from .blame import Blame from .utils import to_bytes, is_string @@ -57,6 +57,19 @@ class Repository(_Repository): def __init__(self, *args, **kwargs): super(Repository, self).__init__(*args, **kwargs) + self._common_init() + + @classmethod + def _from_c(cls, ptr, owned): + cptr = ffi.new('git_repository **') + cptr[0] = ptr + repo = cls.__new__(cls) + super(cls, repo)._from_c(bytes(ffi.buffer(cptr)[:]), owned) + repo._common_init() + return repo + + def _common_init(self): + self.remotes = RemoteCollection(self) # Get the pointer as the contents of a buffer and store it for # later access @@ -90,36 +103,11 @@ class Repository(_Repository): """create_remote(name, url) -> Remote Creates a new remote. + + This method is deprecated, please use Remote.remotes.create() """ - cremote = ffi.new('git_remote **') - - err = C.git_remote_create(cremote, self._repo, to_bytes(name), - to_bytes(url)) - check_error(err) - - return Remote(self, cremote[0]) - - @property - def remotes(self): - """Returns all configured remotes""" - - names = ffi.new('git_strarray *') - - try: - err = C.git_remote_list(names, self._repo) - check_error(err) - - l = [None] * names.count - cremote = ffi.new('git_remote **') - for i in range(names.count): - err = C.git_remote_load(cremote, self._repo, names.strings[i]) - check_error(err) - - l[i] = Remote(self, cremote[0]) - return l - finally: - C.git_strarray_free(names) + return self.remotes.create(name, url) # # Configuration diff --git a/setup.py b/setup.py index ee8973f..92b5280 100644 --- a/setup.py +++ b/setup.py @@ -37,6 +37,8 @@ from distutils.command.build import build from distutils.command.sdist import sdist from distutils import log import os +from os import getenv, listdir, pathsep +from os.path import abspath, isfile from setuptools import setup, Extension, Command import shlex from subprocess import Popen, PIPE @@ -58,7 +60,7 @@ else: libgit2_bin, libgit2_include, libgit2_lib = get_libgit2_paths() -pygit2_exts = [os.path.join('src', name) for name in os.listdir('src') +pygit2_exts = [os.path.join('src', name) for name in listdir('src') if name.endswith('.c')] @@ -71,7 +73,6 @@ class TestCommand(Command): def initialize_options(self): self.args = '' - pass def finalize_options(self): pass @@ -80,7 +81,7 @@ class TestCommand(Command): self.run_command('build') bld = self.distribution.get_command_obj('build') # Add build_lib in to sys.path so that unittest can found DLLs and libs - sys.path = [os.path.abspath(bld.build_lib)] + sys.path + sys.path = [abspath(bld.build_lib)] + sys.path test_argv0 = [sys.argv[0] + ' test --args='] # For transfering args to unittest, we have to split args by ourself, @@ -93,6 +94,7 @@ class TestCommand(Command): test_argv = test_argv0 + shlex.split(self.args) unittest.main(None, defaultTest='test.test_suite', argv=test_argv) + class CFFIBuild(build): """Hack to combat the chicken and egg problem that we need cffi to add cffi as an extension. @@ -116,12 +118,12 @@ class BuildWithDLLs(CFFIBuild): libgit2_dlls.append('git2.dll') elif compiler_type == 'mingw32': libgit2_dlls.append('libgit2.dll') - look_dirs = [libgit2_bin] + os.getenv("PATH", "").split(os.pathsep) - target = os.path.abspath(self.build_lib) + look_dirs = [libgit2_bin] + getenv("PATH", "").split(pathsep) + target = abspath(self.build_lib) for bin in libgit2_dlls: for look in look_dirs: f = os.path.join(look, bin) - if os.path.isfile(f): + if isfile(f): ret.append((f, target)) break else: @@ -131,10 +133,9 @@ class BuildWithDLLs(CFFIBuild): def run(self): build.run(self) - if os.name == 'nt': - # On Windows we package up the dlls with the plugin. - for s, d in self._get_dlls(): - self.copy_file(s, d) + # On Windows we package up the dlls with the plugin. + for s, d in self._get_dlls(): + self.copy_file(s, d) class sdist_files_from_git(sdist): @@ -167,16 +168,10 @@ with codecs.open('README.rst', 'r', 'utf-8') as readme: cmdclass = { + 'build': BuildWithDLLs if os.name == 'nt' else CFFIBuild, 'test': TestCommand, - 'sdist': sdist_files_from_git} - -if os.name == 'nt': - # BuildWithDLLs can copy external DLLs into source directory. - cmdclass['build'] = BuildWithDLLs -else: - # Build cffi - cmdclass['build'] = CFFIBuild - + 'sdist': sdist_files_from_git, + } setup(name='pygit2', description='Python bindings for libgit2.', @@ -184,7 +179,7 @@ setup(name='pygit2', version=__version__, url='http://github.com/libgit2/pygit2', classifiers=classifiers, - license='GPLv2', + license='GPLv2 with linking exception', maintainer=u('J. David Ibáñez'), maintainer_email='jdavid.ibp@gmail.com', long_description=long_description, diff --git a/src/diff.c b/src/diff.c index 0f7bfb3..1874c50 100644 --- a/src/diff.c +++ b/src/diff.c @@ -31,6 +31,7 @@ #include "error.h" #include "types.h" #include "utils.h" +#include "oid.h" #include "diff.h" extern PyObject *GitError; @@ -43,7 +44,7 @@ extern PyTypeObject RepositoryType; PyTypeObject PatchType; -PyObject* +PyObject * wrap_diff(git_diff *diff, Repository *repo) { Diff *py_diff; @@ -52,7 +53,7 @@ wrap_diff(git_diff *diff, Repository *repo) if (py_diff) { Py_INCREF(repo); py_diff->repo = repo; - py_diff->list = diff; + py_diff->diff = diff; } return (PyObject*) py_diff; @@ -76,13 +77,13 @@ wrap_patch(git_patch *patch) delta = git_patch_get_delta(patch); - py_patch->old_file_path = delta->old_file.path; - py_patch->new_file_path = delta->new_file.path; + py_patch->old_file_path = strdup(delta->old_file.path); + py_patch->new_file_path = strdup(delta->new_file.path); py_patch->status = git_diff_status_char(delta->status); py_patch->similarity = delta->similarity; py_patch->flags = delta->flags; - py_patch->old_id = git_oid_allocfmt(&delta->old_file.id); - py_patch->new_id = git_oid_allocfmt(&delta->new_file.id); + py_patch->old_id = git_oid_to_python(&delta->old_file.id); + py_patch->new_id = git_oid_to_python(&delta->new_file.id); git_patch_line_stats(NULL, &additions, &deletions, patch); py_patch->additions = additions; @@ -133,7 +134,7 @@ wrap_patch(git_patch *patch) return (PyObject*) py_patch; } -PyObject* +PyObject * diff_get_patch_byindex(git_diff *diff, size_t idx) { git_patch *patch = NULL; @@ -150,18 +151,18 @@ static void Patch_dealloc(Patch *self) { Py_CLEAR(self->hunks); - free(self->old_id); - free(self->new_id); - /* We do not have to free old_file_path and new_file_path, they will - * be freed by git_diff_list_free in Diff_dealloc */ + Py_CLEAR(self->old_id); + Py_CLEAR(self->new_id); + free(self->old_file_path); + free(self->new_file_path); PyObject_Del(self); } PyMemberDef Patch_members[] = { MEMBER(Patch, old_file_path, T_STRING, "old file path"), MEMBER(Patch, new_file_path, T_STRING, "new file path"), - MEMBER(Patch, old_id, T_STRING, "old oid"), - MEMBER(Patch, new_id, T_STRING, "new oid"), + MEMBER(Patch, old_id, T_OBJECT, "old oid"), + MEMBER(Patch, new_id, T_OBJECT, "new oid"), MEMBER(Patch, status, T_CHAR, "status"), MEMBER(Patch, similarity, T_INT, "similarity"), MEMBER(Patch, hunks, T_OBJECT, "hunks"), @@ -234,7 +235,7 @@ PyObject * DiffIter_iternext(DiffIter *self) { if (self->i < self->n) - return diff_get_patch_byindex(self->diff->list, self->i++); + return diff_get_patch_byindex(self->diff->diff, self->i++); PyErr_SetNone(PyExc_StopIteration); return NULL; @@ -283,8 +284,8 @@ PyTypeObject DiffIterType = { Py_ssize_t Diff_len(Diff *self) { - assert(self->list); - return (Py_ssize_t)git_diff_num_deltas(self->list); + assert(self->diff); + return (Py_ssize_t)git_diff_num_deltas(self->diff); } PyDoc_STRVAR(Diff_patch__doc__, "Patch diff string."); @@ -295,15 +296,15 @@ Diff_patch__get__(Diff *self) git_patch* patch; git_buf buf = {NULL}; int err = GIT_ERROR; - size_t i, len, num; + size_t i, num; PyObject *py_patch = NULL; - num = git_diff_num_deltas(self->list); + num = git_diff_num_deltas(self->diff); if (num == 0) Py_RETURN_NONE; - for (i = 0, len = 1; i < num ; ++i) { - err = git_patch_from_diff(&patch, self->list, i); + for (i = 0; i < num ; ++i) { + err = git_patch_from_diff(&patch, self->diff, i); if (err < 0) goto cleanup; @@ -429,7 +430,7 @@ Diff_merge(Diff *self, PyObject *args) if (py_diff->repo->repo != self->repo->repo) return Error_set(GIT_ERROR); - err = git_diff_merge(self->list, py_diff->list); + err = git_diff_merge(self->diff, py_diff->diff); if (err < 0) return Error_set(err); @@ -454,7 +455,7 @@ Diff_find_similar(Diff *self, PyObject *args, PyObject *kwds) &opts.flags, &opts.rename_threshold, &opts.copy_threshold, &opts.rename_from_rewrite_threshold, &opts.break_rewrite_threshold, &opts.rename_limit)) return NULL; - err = git_diff_find_similar(self->list, &opts); + err = git_diff_find_similar(self->diff, &opts); if (err < 0) return Error_set(err); @@ -471,7 +472,7 @@ Diff_iter(Diff *self) Py_INCREF(self); iter->diff = self; iter->i = 0; - iter->n = git_diff_num_deltas(self->list); + iter->n = git_diff_num_deltas(self->diff); } return (PyObject*)iter; } @@ -486,14 +487,14 @@ Diff_getitem(Diff *self, PyObject *value) i = PyLong_AsUnsignedLong(value); - return diff_get_patch_byindex(self->list, i); + return diff_get_patch_byindex(self->diff, i); } static void Diff_dealloc(Diff *self) { - git_diff_free(self->list); + git_diff_free(self->diff); Py_CLEAR(self->repo); PyObject_Del(self); } diff --git a/src/note.c b/src/note.c index 9762ef6..6a7a437 100644 --- a/src/note.c +++ b/src/note.c @@ -39,13 +39,13 @@ extern PyTypeObject SignatureType; PyDoc_STRVAR(Note_remove__doc__, "Removes a note for an annotated object"); -PyObject* +PyObject * Note_remove(Note *self, PyObject* args) { char *ref = "refs/notes/commits"; int err = GIT_ERROR; - git_oid annotated_id; Signature *py_author, *py_committer; + Oid *id; if (!PyArg_ParseTuple(args, "O!O!|s", &SignatureType, &py_author, @@ -53,12 +53,9 @@ Note_remove(Note *self, PyObject* args) &ref)) return NULL; - err = git_oid_fromstr(&annotated_id, self->annotated_id); - if (err < 0) - return Error_set(err); - + id = (Oid *) self->annotated_id; err = git_note_remove(self->repo->repo, ref, py_author->signature, - py_committer->signature, &annotated_id); + py_committer->signature, &id->oid); if (err < 0) return Error_set(err); @@ -90,7 +87,7 @@ static void Note_dealloc(Note *self) { Py_CLEAR(self->repo); - free(self->annotated_id); + Py_CLEAR(self->annotated_id); git_note_free(self->note); PyObject_Del(self); } @@ -102,7 +99,7 @@ PyMethodDef Note_methods[] = { }; PyMemberDef Note_members[] = { - MEMBER(Note, annotated_id, T_STRING, "id of the annotated object."), + MEMBER(Note, annotated_id, T_OBJECT, "id of the annotated object."), {NULL} }; @@ -211,7 +208,7 @@ PyTypeObject NoteIterType = { }; -PyObject* +PyObject * wrap_note(Repository* repo, git_oid* annotated_id, const char* ref) { Note* py_note = NULL; @@ -229,7 +226,7 @@ wrap_note(Repository* repo, git_oid* annotated_id, const char* ref) py_note->repo = repo; Py_INCREF(repo); - py_note->annotated_id = git_oid_allocfmt(annotated_id); + py_note->annotated_id = git_oid_to_python(annotated_id); return (PyObject*) py_note; } diff --git a/src/pygit2.c b/src/pygit2.c index 767ae2e..cf2decd 100644 --- a/src/pygit2.c +++ b/src/pygit2.c @@ -149,7 +149,7 @@ PyMethodDef module_methods[] = { {NULL} }; -PyObject* +PyObject * moduleinit(PyObject* m) { if (m == NULL) @@ -210,7 +210,6 @@ moduleinit(PyObject* m) ADD_CONSTANT_INT(m, GIT_OBJ_BLOB) ADD_CONSTANT_INT(m, GIT_OBJ_TAG) /* Valid modes for index and tree entries. */ - ADD_CONSTANT_INT(m, GIT_FILEMODE_NEW) ADD_CONSTANT_INT(m, GIT_FILEMODE_TREE) ADD_CONSTANT_INT(m, GIT_FILEMODE_BLOB) ADD_CONSTANT_INT(m, GIT_FILEMODE_BLOB_EXECUTABLE) @@ -347,7 +346,7 @@ moduleinit(PyObject* m) ADD_CONSTANT_INT(m, GIT_MERGE_ANALYSIS_UNBORN) /* Global initialization of libgit2 */ - git_threads_init(); + git_libgit2_init(); return m; } diff --git a/src/reference.c b/src/reference.c index 28c1bbd..4d8e56e 100644 --- a/src/reference.c +++ b/src/reference.c @@ -60,8 +60,8 @@ RefLogIter_iternext(RefLogIter *self) entry = git_reflog_entry_byindex(self->reflog, self->i); py_entry = PyObject_New(RefLogEntry, &RefLogEntryType); - py_entry->oid_old = git_oid_allocfmt(git_reflog_entry_id_old(entry)); - py_entry->oid_new = git_oid_allocfmt(git_reflog_entry_id_new(entry)); + py_entry->oid_old = git_oid_to_python(git_reflog_entry_id_old(entry)); + py_entry->oid_new = git_oid_to_python(git_reflog_entry_id_new(entry)); py_entry->message = strdup(git_reflog_entry_message(entry)); err = git_signature_dup(&py_entry->signature, git_reflog_entry_committer(entry)); @@ -431,16 +431,16 @@ RefLogEntry_init(RefLogEntry *self, PyObject *args, PyObject *kwds) static void RefLogEntry_dealloc(RefLogEntry *self) { - free(self->oid_old); - free(self->oid_new); + Py_CLEAR(self->oid_old); + Py_CLEAR(self->oid_new); free(self->message); git_signature_free(self->signature); PyObject_Del(self); } PyMemberDef RefLogEntry_members[] = { - MEMBER(RefLogEntry, oid_new, T_STRING, "New oid."), - MEMBER(RefLogEntry, oid_old, T_STRING, "Old oid."), + MEMBER(RefLogEntry, oid_new, T_OBJECT, "New oid."), + MEMBER(RefLogEntry, oid_old, T_OBJECT, "Old oid."), MEMBER(RefLogEntry, message, T_STRING, "Message."), {NULL} }; diff --git a/src/repository.c b/src/repository.c index 31075c2..9eabbb9 100644 --- a/src/repository.c +++ b/src/repository.c @@ -55,6 +55,9 @@ extern PyTypeObject ReferenceType; extern PyTypeObject NoteType; extern PyTypeObject NoteIterType; +/* forward-declaration for Repsository._from_c() */ +PyTypeObject RepositoryType; + git_otype int_to_loose_object_type(int type_id) { @@ -88,19 +91,62 @@ Repository_init(Repository *self, PyObject *args, PyObject *kwds) return -1; } + self->owned = 1; self->config = NULL; self->index = NULL; return 0; } +PyDoc_STRVAR(Repository__from_c__doc__, "Init a Repository from a pointer. For internal use only."); +PyObject * +Repository__from_c(Repository *py_repo, PyObject *args) +{ + PyObject *py_pointer, *py_free; + char *buffer; + Py_ssize_t len; + int err; + + py_repo->repo = NULL; + py_repo->config = NULL; + py_repo->index = NULL; + + if (!PyArg_ParseTuple(args, "OO!", &py_pointer, &PyBool_Type, &py_free)) + return NULL; + + err = PyBytes_AsStringAndSize(py_pointer, &buffer, &len); + if (err < 0) + return NULL; + + if (len != sizeof(git_repository *)) { + PyErr_SetString(PyExc_TypeError, "invalid pointer length"); + return NULL; + } + + py_repo->repo = *((git_repository **) buffer); + py_repo->owned = py_free == Py_True; + + Py_RETURN_NONE; +} + +PyDoc_STRVAR(Repository__disown__doc__, "Mark the object as not-owned by us. For internal use only."); +PyObject * +Repository__disown(Repository *py_repo) +{ + py_repo->owned = 0; + Py_RETURN_NONE; +} + void Repository_dealloc(Repository *self) { PyObject_GC_UnTrack(self); Py_CLEAR(self->index); Py_CLEAR(self->config); - git_repository_free(self->repo); + + if (self->owned) + git_repository_free(self->repo); + Py_TYPE(self)->tp_free(self); } @@ -526,7 +572,7 @@ Repository_merge_analysis(Repository *self, PyObject *py_id) int err; size_t len; git_oid id; - git_merge_head *merge_head; + git_annotated_commit *commit; git_merge_analysis_t analysis; git_merge_preference_t preference; @@ -534,12 +580,12 @@ Repository_merge_analysis(Repository *self, PyObject *py_id) if (len == 0) return NULL; - err = git_merge_head_from_id(&merge_head, self->repo, &id); + err = git_annotated_commit_lookup(&commit, self->repo, &id); if (err < 0) return Error_set(err); - err = git_merge_analysis(&analysis, &preference, self->repo, (const git_merge_head **) &merge_head, 1); - git_merge_head_free(merge_head); + err = git_merge_analysis(&analysis, &preference, self->repo, (const git_annotated_commit **) &commit, 1); + git_annotated_commit_free(commit); if (err < 0) return Error_set(err); @@ -561,7 +607,7 @@ PyDoc_STRVAR(Repository_merge__doc__, PyObject * Repository_merge(Repository *self, PyObject *py_oid) { - git_merge_head *oid_merge_head; + git_annotated_commit *commit; git_oid oid; int err; size_t len; @@ -572,16 +618,16 @@ Repository_merge(Repository *self, PyObject *py_oid) if (len == 0) return NULL; - err = git_merge_head_from_id(&oid_merge_head, self->repo, &oid); + err = git_annotated_commit_lookup(&commit, self->repo, &oid); if (err < 0) return Error_set(err); checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; err = git_merge(self->repo, - (const git_merge_head **)&oid_merge_head, 1, + (const git_annotated_commit **)&commit, 1, &merge_opts, &checkout_opts); - git_merge_head_free(oid_merge_head); + git_annotated_commit_free(commit); if (err < 0) return Error_set(err); @@ -936,10 +982,15 @@ out: PyDoc_STRVAR(Repository_listall_branches__doc__, - "listall_branches([flags]) -> [str, ...]\n" + "listall_branches([flag]) -> [str, ...]\n" "\n" - "Return a tuple with all the branches in the repository.\n" - "By default, it returns all local branches."); + "Return a list with all the branches in the repository.\n" + "\n" + "The *flag* may be:\n" + "\n" + "- GIT_BRANCH_LOCAL - return all local branches (set by default)\n" + "- GIT_BRANCH_REMOTE - return all remote-tracking branches\n" + "- GIT_BRANCH_ALL - return local branches and remote-tracking branches"); PyObject * Repository_listall_branches(Repository *self, PyObject *args) @@ -1220,7 +1271,7 @@ Repository_TreeBuilder(Repository *self, PyObject *args) } } - err = git_treebuilder_create(&bld, tree); + err = git_treebuilder_new(&bld, self->repo, tree); if (must_free != NULL) git_tree_free(must_free); @@ -1313,8 +1364,8 @@ Repository_create_note(Repository *self, PyObject* args) if (err < 0) return Error_set(err); - err = git_note_create(¬e_id, self->repo, py_author->signature, - py_committer->signature, ref, + err = git_note_create(¬e_id, self->repo, ref, py_author->signature, + py_committer->signature, &annotated_id, message, force); if (err < 0) return Error_set(err); @@ -1375,7 +1426,7 @@ Repository_reset(Repository *self, PyObject* args) err = git_object_lookup_prefix(&target, self->repo, &oid, len, GIT_OBJ_ANY); - err = err < 0 ? err : git_reset(self->repo, target, reset_type, NULL, NULL); + err = err < 0 ? err : git_reset(self->repo, target, reset_type, NULL, NULL, NULL); git_object_free(target); if (err < 0) return Error_set_oid(err, &oid, len); @@ -1410,6 +1461,8 @@ PyMethodDef Repository_methods[] = { METHOD(Repository, listall_branches, METH_VARARGS), METHOD(Repository, create_branch, METH_VARARGS), METHOD(Repository, reset, METH_VARARGS), + METHOD(Repository, _from_c, METH_VARARGS), + METHOD(Repository, _disown, METH_NOARGS), {NULL} }; diff --git a/src/treebuilder.c b/src/treebuilder.c index 5957040..dbec825 100644 --- a/src/treebuilder.c +++ b/src/treebuilder.c @@ -88,7 +88,7 @@ TreeBuilder_write(TreeBuilder *self) int err; git_oid oid; - err = git_treebuilder_write(&oid, self->repo->repo, self->bld); + err = git_treebuilder_write(&oid, self->bld); if (err < 0) return Error_set(err); diff --git a/src/types.h b/src/types.h index 58e9e99..4b9c2a1 100644 --- a/src/types.h +++ b/src/types.h @@ -32,8 +32,8 @@ #include #include -#if !(LIBGIT2_VER_MAJOR == 0 && LIBGIT2_VER_MINOR == 21) -#error You need a compatible libgit2 version (v0.21.x) +#if !(LIBGIT2_VER_MAJOR == 0 && LIBGIT2_VER_MINOR == 22) +#error You need a compatible libgit2 version (v0.22.x) #endif /* @@ -47,6 +47,7 @@ typedef struct { git_repository *repo; PyObject *index; /* It will be None for a bare repository */ PyObject *config; /* It will be None for a bare repository */ + int owned; /* _from_c() sometimes means we don't own the C pointer */ } Repository; @@ -79,7 +80,7 @@ typedef struct { PyObject_HEAD Repository *repo; git_note *note; - char* annotated_id; + PyObject* annotated_id; } Note; typedef struct { @@ -90,12 +91,12 @@ typedef struct { } NoteIter; -/* git _diff */ -SIMPLE_TYPE(Diff, git_diff, list) +/* git_diff */ +SIMPLE_TYPE(Diff, git_diff, diff) typedef struct { PyObject_HEAD - Diff* diff; + Diff *diff; size_t i; size_t n; } DiffIter; @@ -103,10 +104,10 @@ typedef struct { typedef struct { PyObject_HEAD PyObject* hunks; - const char * old_file_path; - const char * new_file_path; - char* old_id; - char* new_id; + char * old_file_path; + char * new_file_path; + PyObject* old_id; + PyObject* new_id; char status; unsigned similarity; unsigned additions; @@ -164,8 +165,8 @@ typedef Reference Branch; typedef struct { PyObject_HEAD git_signature *signature; - char *oid_old; - char *oid_new; + PyObject *oid_old; + PyObject *oid_new; char *message; } RefLogEntry; diff --git a/test/test_diff.py b/test/test_diff.py index 2f4314d..6fd4fbb 100644 --- a/test/test_diff.py +++ b/test/test_diff.py @@ -261,9 +261,9 @@ class DiffTest(utils.BareRepoTestCase): commit_a = self.repo[COMMIT_SHA1_1] commit_b = self.repo[COMMIT_SHA1_2] patch = commit_a.tree.diff_to_tree(commit_b.tree)[0] - self.assertEqual(patch.old_id, + self.assertEqual(patch.old_id.hex, '7f129fd57e31e935c6d60a0c794efe4e6927664b') - self.assertEqual(patch.new_id, + self.assertEqual(patch.new_id.hex, 'af431f20fc541ed6d5afede3e2dc7160f6f01f16') def test_hunk_content(self): diff --git a/test/test_note.py b/test/test_note.py index 4fcead5..5dce89c 100644 --- a/test/test_note.py +++ b/test/test_note.py @@ -70,7 +70,7 @@ class NotesTest(utils.BareRepoTestCase): def test_iterate_notes(self): for i, note in enumerate(self.repo.notes()): - entry = (note.id.hex, note.message, note.annotated_id) + entry = (note.id.hex, note.message, note.annotated_id.hex) self.assertEqual(NOTES[i], entry) def test_iterate_non_existing_ref(self): diff --git a/test/test_remote.py b/test/test_remote.py index e0b9fae..f825cd6 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -71,19 +71,19 @@ class RepositoryTest(utils.RepoTestCase): remote = self.repo.remotes[1] self.assertEqual(name, remote.name) - remote.delete() + self.repo.remotes.delete(remote.name) self.assertEqual(1, len(self.repo.remotes)) def test_remote_rename(self): remote = self.repo.remotes[0] self.assertEqual(REMOTE_NAME, remote.name) - problems = remote.rename('new') + problems = self.repo.remotes.rename(remote.name, "new") self.assertEqual([], problems) - self.assertEqual('new', remote.name) + self.assertNotEqual('new', remote.name) - self.assertRaises(ValueError, remote.rename, '') - self.assertRaises(ValueError, remote.rename, None) + self.assertRaises(ValueError, self.repo.remotes.rename, '', '') + self.assertRaises(ValueError, self.repo.remotes.rename, None, None) def test_remote_set_url(self): @@ -167,16 +167,25 @@ class RepositoryTest(utils.RepoTestCase): remote = self.repo.create_remote(name, url) self.assertTrue(remote.name in [x.name for x in self.repo.remotes]) + def test_remote_collection(self): + remote = self.repo.remotes['origin'] + self.assertEqual(REMOTE_NAME, remote.name) + self.assertEqual(REMOTE_URL, remote.url) + + with self.assertRaises(KeyError): + self.repo.remotes['upstream'] + + name = 'upstream' + url = 'git://github.com/libgit2/pygit2.git' + remote = self.repo.remotes.create(name, url) + self.assertTrue(remote.name in [x.name for x in self.repo.remotes]) + def test_remote_save(self): remote = self.repo.remotes[0] - - remote.rename('new-name') remote.url = 'http://example.com/test.git' - remote.save() - self.assertEqual('new-name', self.repo.remotes[0].name) self.assertEqual('http://example.com/test.git', self.repo.remotes[0].url) @@ -265,11 +274,11 @@ class PushTestCase(unittest.TestCase): 'refs/heads/master', tip.author, tip.author, 'empty commit', tip.tree.id, [tip.id] ) - self.remote.push('refs/heads/master') + self.remote.push(['refs/heads/master']) self.assertEqual(self.origin[self.origin.head.target].id, oid) def test_push_when_up_to_date_succeeds(self): - self.remote.push('refs/heads/master') + self.remote.push(['refs/heads/master']) origin_tip = self.origin[self.origin.head.target].id clone_tip = self.clone[self.clone.head.target].id self.assertEqual(origin_tip, clone_tip) @@ -285,7 +294,8 @@ class PushTestCase(unittest.TestCase): 'refs/heads/master', tip.author, tip.author, 'other commit', tip.tree.id, [tip.id] ) - self.assertRaises(pygit2.GitError, self.remote.push, 'refs/heads/master') + + self.assertRaises(pygit2.GitError, self.remote.push, ['refs/heads/master']) if __name__ == '__main__': unittest.main() diff --git a/test/test_repository.py b/test/test_repository.py index 72a5e64..7640f39 100644 --- a/test/test_repository.py +++ b/test/test_repository.py @@ -41,7 +41,7 @@ import sys # Import from pygit2 from pygit2 import GIT_OBJ_ANY, GIT_OBJ_BLOB, GIT_OBJ_COMMIT -from pygit2 import init_repository, clone_repository, clone_into, discover_repository +from pygit2 import init_repository, clone_repository, discover_repository from pygit2 import Oid, Reference, hashfile import pygit2 from . import utils @@ -440,19 +440,22 @@ class CloneRepositoryTest(utils.NoRepoTestCase): self.assertFalse(repo.is_empty) self.assertTrue(repo.is_bare) - def test_clone_remote_name(self): - repo_path = "./test/data/testrepo.git/" - repo = clone_repository( - repo_path, self._temp_dir, remote_name="custom_remote") - self.assertFalse(repo.is_empty) - self.assertEqual(repo.remotes[0].name, "custom_remote") + def test_clone_repository_and_remote_callbacks(self): + src_repo_relpath = "./test/data/testrepo.git/" + repo_path = os.path.join(self._temp_dir, "clone-into") + url = 'file://' + os.path.realpath(src_repo_relpath) - def test_clone_into(self): - repo_path = "./test/data/testrepo.git/" - repo = init_repository(os.path.join(self._temp_dir, "clone-into")) - remote = repo.create_remote("origin", 'file://' + os.path.realpath(repo_path)) - clone_into(repo, remote) - self.assertTrue('refs/remotes/origin/master' in repo.listall_references()) + def create_repository(path, bare): + return init_repository(path, bare) + + # here we override the name + def create_remote(repo, name, url): + return repo.remotes.create("custom_remote", url) + + repo = clone_repository(url, repo_path, repository=create_repository, remote=create_remote) + self.assertFalse(repo.is_empty) + self.assertTrue('refs/remotes/custom_remote/master' in repo.listall_references()) + self.assertIsNotNone(repo.remotes["custom_remote"]) def test_clone_with_credentials(self): credentials = pygit2.UserPass("libgit2", "libgit2")