From 687dc5388e7f75ee71ea683006ddd122d0fd2647 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Thu, 27 Mar 2014 16:53:16 +0100 Subject: [PATCH 01/28] config: make type conversion explicit The type of a config value depends on the tool that interprets it. Parsing eagerly can lead to a situation where we return a bool instead of a string or a number. Let the user specify the type themselves by passing in a (str, type) tuple into the mapping interface. --- docs/config.rst | 16 +++++++++++++++ src/config.c | 49 +++++++++++++++++++++++++++++++++++---------- src/utils.h | 2 ++ test/test_config.py | 12 +++++------ 4 files changed, 62 insertions(+), 17 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index 348c347..4bdaaa1 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -23,3 +23,19 @@ The Config type set multiple times in the configuration files. The :class:`Config` Mapping interface. + +Parsing the values +=================== + +Instead of a string, a tuple of `(str,type)` can be used to look up a +key and parse it through the Git rules. E.g. + + config['core.bare',bool] + +will return True if 'core.bare' is truthy. + +Truty values are: 'true', 1, 'on' or 'yes'. Falsy values are: 'false', +0, 'off' and 'no'. + +Available types are `bool` and `int`. Not specifying a type returns a +string. diff --git a/src/config.c b/src/config.c index 9b2625d..58fdeaa 100644 --- a/src/config.c +++ b/src/config.c @@ -170,29 +170,56 @@ Config_contains(Config *self, PyObject *py_key) { PyObject * -Config_getitem(Config *self, PyObject *py_key) +Config_getitem(Config *self, PyObject *py_input_key) { - int64_t value_int; - int err, value_bool; + int err; const char *value_str; const char *key; - PyObject* py_value, *tmp; + PyObject *py_key, *py_value, *tkey, *tmp_type = NULL; + PyTypeObject *py_type = NULL; - key = py_str_borrow_c_str(&tmp, py_key, NULL); + if (PyTuple_Check(py_input_key) && PyTuple_Size(py_input_key) == 2) { + py_key = PyTuple_GetItem(py_input_key, 0); + tmp_type = PyTuple_GetItem(py_input_key, 1); + } else { + py_key = py_input_key; + } + + /* If passed a tuple, make sure the second item is a type */ + if (tmp_type) { + if (!PyType_Check(tmp_type)) + return NULL; + else + py_type = (PyTypeObject *) tmp_type; + } + + key = py_str_borrow_c_str(&tkey, py_key, NULL); if (key == NULL) return NULL; err = git_config_get_string(&value_str, self->config, key); - Py_CLEAR(tmp); + Py_CLEAR(tkey); if (err < 0) goto cleanup; - if (git_config_parse_int64(&value_int, value_str) == 0) - py_value = PyLong_FromLongLong(value_int); - else if(git_config_parse_bool(&value_bool, value_str) == 0) - py_value = PyBool_FromLong(value_bool); - else + /* If the user specified a type, let's parse it */ + if (py_type) { + if (py_type == &PyBool_Type) { + int value; + if ((err = git_config_parse_bool(&value, value_str)) < 0) + goto cleanup; + + py_value = PyBool_FromLong(value); + } else if (py_type == &PyInteger_Type) { + int64_t value; + if ((err = git_config_parse_int64(&value, value_str)) < 0) + goto cleanup; + + py_value = PyLong_FromLongLong(value); + } + } else { py_value = to_unicode(value_str, NULL, NULL); + } cleanup: if (err < 0) { diff --git a/src/utils.h b/src/utils.h index 81744f6..e321ea1 100644 --- a/src/utils.h +++ b/src/utils.h @@ -47,6 +47,7 @@ #undef PyLong_Check #define PyLong_Check PyInt_Check #define PyLong_FromLong PyInt_FromLong + #define PyInteger_Type PyInt_Type #define PyBytes_AS_STRING PyString_AS_STRING #define PyBytes_AsString PyString_AsString #define PyBytes_AsStringAndSize PyString_AsStringAndSize @@ -57,6 +58,7 @@ #define to_path(x) to_bytes(x) #define to_encoding(x) to_bytes(x) #else + #define PyInteger_Type PyLong_Type #define to_path(x) to_unicode(x, Py_FileSystemDefaultEncoding, "strict") #define to_encoding(x) PyUnicode_DecodeASCII(x, strlen(x), "strict") #endif diff --git a/test/test_config.py b/test/test_config.py index 9f4d460..f29c703 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -74,7 +74,7 @@ class ConfigTest(utils.RepoTestCase): config_read = Config(CONFIG_FILENAME) self.assertTrue('core.bare' in config_read) - self.assertFalse(config_read['core.bare']) + self.assertFalse(config_read['core.bare',bool]) self.assertTrue('core.editor' in config_read) self.assertEqual(config_read['core.editor'], 'ed') @@ -88,9 +88,9 @@ class ConfigTest(utils.RepoTestCase): config.add_file(CONFIG_FILENAME, 0) self.assertTrue('this.that' in config) - self.assertTrue(config['this.that']) + self.assertTrue(config['this.that',bool]) self.assertTrue('something.other.here' in config) - self.assertFalse(config['something.other.here']) + self.assertFalse(config['something.other.here',bool]) def test_read(self): config = self.repo.config @@ -103,11 +103,11 @@ class ConfigTest(utils.RepoTestCase): lambda: config['abc.def']) self.assertTrue('core.bare' in config) - self.assertFalse(config['core.bare']) + self.assertFalse(config['core.bare',bool]) self.assertTrue('core.editor' in config) self.assertEqual(config['core.editor'], 'ed') self.assertTrue('core.repositoryformatversion' in config) - self.assertEqual(config['core.repositoryformatversion'], 0) + self.assertEqual(config['core.repositoryformatversion',int], 0) new_file = open(CONFIG_FILENAME, "w") new_file.write("[this]\n\tthat = foobar\n\tthat = foobeer\n") @@ -129,7 +129,7 @@ class ConfigTest(utils.RepoTestCase): self.assertFalse('core.dummy1' in config) config['core.dummy1'] = 42 self.assertTrue('core.dummy1' in config) - self.assertEqual(config['core.dummy1'], 42) + self.assertEqual(config['core.dummy1',int], 42) self.assertFalse('core.dummy2' in config) config['core.dummy2'] = 'foobar' From d882af8f52fadfdc35a98daaac8bf0a2fb8e155f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20David=20Ib=C3=A1=C3=B1ez?= Date: Wed, 2 Apr 2014 22:28:18 +0200 Subject: [PATCH 02/28] Get ready to release 0.20.3 --- README.rst | 44 +++++++++++++++++++++++--------------------- docs/conf.py | 2 +- pygit2/version.py | 2 +- 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/README.rst b/README.rst index 272b149..04c4366 100644 --- a/README.rst +++ b/README.rst @@ -44,33 +44,35 @@ for the topic), send a pull request. Authors ============== -57 developers have contributed at least 1 commit to pygit2:: +62 developers have contributed at least 1 commit to pygit2:: - J. David Ibáñez Brodie Rao Adam Spiers - Nico von Geyso David Versmisse Alexander Bayandin - Carlos Martín Nieto Rémi Duraffort Andrew Chin - W. Trevor King Sebastian Thiel András Veres-Szentkirályi - Dave Borowitz Fraser Tweedale Benjamin Kircher - Daniel Rodríguez Troitiño Han-Wen Nienhuys Benjamin Pollack - Richo Healey Petr Viktorin Bryan O'Sullivan - Christian Boos Alex Chamberlain David Fischer - Julien Miotte Amit Bakshi David Sanders - Xu Tao Andrey Devyatkin Eric Davis - Jose Plana Ben Davis Erik van Zijst - Martin Lenders Eric Schrijver Ferengee - Petr Hosek Hervé Cauwelier Gustavo Di Pietro - Victor Garcia Huang Huang Hugh Cole-Baker - Xavier Delannoy Jared Flatow Josh Bleecher Snyder - Yonggang Luo Jiunn Haur Lim Jun Omae - Valentin Haenel Sarath Lakshman Óscar San José - Bernardo Heynemann Vicent Marti Ridge Kennedy - John Szakmeister Zoran Zaric Rui Abreu Ferreira + J. David Ibáñez Rémi Duraffort András Veres-Szentkirályi + Nico von Geyso Sebastian Thiel Benjamin Kircher + Carlos Martín Nieto Fraser Tweedale Benjamin Pollack + W. Trevor King Han-Wen Nienhuys Bryan O'Sullivan + Dave Borowitz Leonardo Rhodes David Fischer + Daniel Rodríguez Troitiño Petr Viktorin David Sanders + Richo Healey Alex Chamberlain Devaev Maxim + Christian Boos Amit Bakshi Eric Davis + Julien Miotte Andrey Devyatkin Erik Meusel + Xu Tao Ben Davis Erik van Zijst + Jose Plana Eric Schrijver Ferengee + Martin Lenders Hervé Cauwelier Gustavo Di Pietro + Petr Hosek Huang Huang Hugh Cole-Baker + Victor Garcia Jared Flatow Josh Bleecher Snyder + Xavier Delannoy Jiunn Haur Lim Jun Omae + Yonggang Luo Sarath Lakshman Óscar San José + Valentin Haenel Vicent Marti Ridge Kennedy + Bernardo Heynemann Zoran Zaric Rui Abreu Ferreira + John Szakmeister Adam Spiers Thomas Kluyver + Brodie Rao Alexander Bayandin earl + David Versmisse Andrew Chin Changelog ============== -0.20.3 (2014-04-XX) +0.20.3 (2014-04-02) ------------------- - A number of memory issues fixed diff --git a/docs/conf.py b/docs/conf.py index d7f2799..c432057 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -50,7 +50,7 @@ copyright = u'2010-2014 The pygit2 contributors' # The short X.Y version. version = '0.20' # The full version, including alpha/beta/rc tags. -release = '0.20.2' +release = '0.20.3' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/pygit2/version.py b/pygit2/version.py index af8676f..e68ec8d 100644 --- a/pygit2/version.py +++ b/pygit2/version.py @@ -23,4 +23,4 @@ # the Free Software Foundation, 51 Franklin Street, Fifth Floor, # Boston, MA 02110-1301, USA. -__version__ = '0.20.2' +__version__ = '0.20.3' From c76c3f0195aa8c3437db959e0780157931f006a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Fri, 11 Apr 2014 23:54:31 +0200 Subject: [PATCH 03/28] Start implementing remotes with CFFI This moves enough code into python with CFFI to pass the test_remotes unit tests. There is no credentials support yet. There is a small change in the return value of Remote.fetch() in that we now return a TransferProgress object instead of extracting a few values into a dictionary. --- .gitignore | 1 + pygit2/__init__.py | 1 + pygit2/decl.h | 127 ++++++++++++++++++ pygit2/errors.py | 55 ++++++++ pygit2/ffi.py | 106 +++++++++++++++ pygit2/refspec.py | 110 ++++++++++++++++ pygit2/remote.py | 207 ++++++++++++++++++++++++++++++ pygit2/repository.py | 48 +++++++ src/refspec.c | 297 ------------------------------------------- src/repository.c | 77 ++--------- test/test_remote.py | 12 +- 11 files changed, 675 insertions(+), 366 deletions(-) create mode 100644 pygit2/decl.h create mode 100644 pygit2/errors.py create mode 100644 pygit2/ffi.py create mode 100644 pygit2/refspec.py create mode 100644 pygit2/remote.py delete mode 100644 src/refspec.c diff --git a/.gitignore b/.gitignore index d802242..79889fe 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ pygit2/__pycache__ *.egg-info *.swp docs/_build +__pycache__ diff --git a/pygit2/__init__.py b/pygit2/__init__.py index fc2e693..396af19 100644 --- a/pygit2/__init__.py +++ b/pygit2/__init__.py @@ -37,6 +37,7 @@ from .repository import Repository from .version import __version__ from .settings import Settings from .credentials import * +from .remote import Remote def init_repository(path, bare=False): """ diff --git a/pygit2/decl.h b/pygit2/decl.h new file mode 100644 index 0000000..e0ad826 --- /dev/null +++ b/pygit2/decl.h @@ -0,0 +1,127 @@ +typedef ... git_repository; +typedef ... git_remote; +typedef ... git_refspec; +typedef ... git_push; +typedef ... git_cred; +typedef ... git_oid; + +typedef struct git_strarray { + char **strings; + size_t count; +} git_strarray; + +typedef enum { + GIT_OK = 0, + GIT_ERROR = -1, + GIT_ENOTFOUND = -3, + GIT_EEXISTS = -4, + GIT_EAMBIGUOUS = -5, + GIT_EBUFS = -6, + GIT_EUSER = -7, + GIT_EBAREREPO = -8, + GIT_EUNBORNBRANCH = -9, + GIT_EUNMERGED = -10, + GIT_ENONFASTFORWARD = -11, + GIT_EINVALIDSPEC = -12, + GIT_EMERGECONFLICT = -13, + GIT_ELOCKED = -14, + + GIT_PASSTHROUGH = -30, + GIT_ITEROVER = -31, +} git_error_code; + +typedef struct { + char *message; + int klass; +} git_error; + +const git_error * giterr_last(void); + +void git_strarray_free(git_strarray *array); + +typedef struct git_transfer_progress { + unsigned int total_objects; + unsigned int indexed_objects; + unsigned int received_objects; + unsigned int local_objects; + unsigned int total_deltas; + unsigned int indexed_deltas; + size_t received_bytes; +} git_transfer_progress; + +typedef enum git_remote_completion_type { + GIT_REMOTE_COMPLETION_DOWNLOAD, + GIT_REMOTE_COMPLETION_INDEXING, + GIT_REMOTE_COMPLETION_ERROR, +} git_remote_completion_type; + +typedef enum { + GIT_DIRECTION_FETCH = 0, + GIT_DIRECTION_PUSH = 1 +} git_direction; + +typedef struct git_remote_callbacks { + unsigned int version; + int (*progress)(const char *str, int len, void *data); + int (*completion)(git_remote_completion_type type, void *data); + int (*credentials)(git_cred **cred, const char *url, const char *username_from_url, unsigned int allowed_types, void *data); + int (*transfer_progress)(const git_transfer_progress *stats, void *data); + int (*update_tips)(const char *refname, const git_oid *a, const git_oid *b, void *data); + void *payload; +} git_remote_callbacks ; + +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_create(git_remote **out, + git_repository *repo, + const char *name, + const char *url); +const char * git_remote_name(const git_remote *remote); +typedef int (*git_remote_rename_problem_cb)(const char *problematic_refspec, void *payload); +int git_remote_rename(git_remote *remote, + const char *new_name, + git_remote_rename_problem_cb callback, + void *payload); +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_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); +int git_remote_save(const git_remote *remote); +int git_remote_set_callbacks(git_remote *remote, const git_remote_callbacks *callbacks); +size_t git_remote_refspec_count(git_remote *remote); +const git_refspec * git_remote_get_refspec(git_remote *remote, size_t n); + +int git_remote_get_fetch_refspecs(git_strarray *array, git_remote *remote); +int git_remote_set_fetch_refspecs(git_remote *remote, git_strarray *array); +int git_remote_get_push_refspecs(git_strarray *array, git_remote *remote); +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); +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); +const char * git_refspec_string(const git_refspec *refspec); +git_direction git_refspec_direction(const git_refspec *spec); + +int git_refspec_src_matches(const git_refspec *refspec, const char *refname); +int git_refspec_dst_matches(const git_refspec *refspec, const char *refname); + +int git_refspec_transform(char *out, size_t outlen, const git_refspec *spec, const char *name); +int git_refspec_rtransform(char *out, size_t outlen, const git_refspec *spec, const char *name); diff --git a/pygit2/errors.py b/pygit2/errors.py new file mode 100644 index 0000000..b9f6c9b --- /dev/null +++ b/pygit2/errors.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2010-2014 The pygit2 contributors +# +# This file is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2, +# as published by the Free Software Foundation. +# +# In addition to the permissions in the GNU General Public License, +# the authors give you unlimited permission to link the compiled +# version of this file into combinations with other programs, +# and to distribute those combinations without any restriction +# coming from the use of this file. (The General Public License +# restrictions do apply in other respects; for example, they cover +# modification of the file, and distribution when not linked into +# a combined executable.) +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; see the file COPYING. If not, write to +# the Free Software Foundation, 51 Franklin Street, Fifth Floor, +# Boston, MA 02110-1301, USA. + +# Import from the Standard Library +from string import hexdigits + +# ffi +from .ffi import ffi, C + +from _pygit2 import GitError + +def check_error(err): + if err >= 0: + return + + message = "(no message provided)" + giterr = C.giterr_last() + if giterr != ffi.NULL: + message = ffi.string(giterr.message).decode() + + if err in [C.GIT_EEXISTS, C.GIT_EINVALIDSPEC, C.GIT_EEXISTS, C.GIT_EAMBIGUOUS]: + raise ValueError(message) + elif err == C.GIT_ENOTFOUND: + raise KeyError(message) + elif err == C.GIT_EINVALIDSPEC: + raise ValueError(message) + elif err == C.GIT_ITEROVER: + raise StopIteration() + + raise GitError(message) + diff --git a/pygit2/ffi.py b/pygit2/ffi.py new file mode 100644 index 0000000..ce25ff0 --- /dev/null +++ b/pygit2/ffi.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2010-2014 The pygit2 contributors +# +# This file is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2, +# as published by the Free Software Foundation. +# +# In addition to the permissions in the GNU General Public License, +# the authors give you unlimited permission to link the compiled +# version of this file into combinations with other programs, +# and to distribute those combinations without any restriction +# coming from the use of this file. (The General Public License +# restrictions do apply in other respects; for example, they cover +# modification of the file, and distribution when not linked into +# a combined executable.) +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; see the file COPYING. If not, write to +# the Free Software Foundation, 51 Franklin Street, Fifth Floor, +# Boston, MA 02110-1301, USA. + +# Import from the future +from __future__ import absolute_import + +import inspect +from os import path +from cffi import FFI +import sys + +if sys.version_info.major < 3: + def to_str(s, encoding='utf-8', errors='strict'): + if s == ffi.NULL: + return ffi.NULL + encoding = encoding or 'utf-8' + if isinstance(s, unicode): + return s.encode(encoding, errors) + + return s +else: + def to_str(s, encoding='utf-8', errors='strict'): + if isinstance(s, bytes): + return s + else: + return bytes(s, encoding, errors) + +if sys.version_info.major < 3: + def is_string(s): + return isinstance(s, str) +else: + def is_string(s): + return isinstance(s, basestring) + +ffi = FFI() + +def strarray_to_strings(arr): + l = [None] * arr.count + for i in range(arr.count): + l[i] = ffi.string(arr.strings[i]).decode() + + return l + +def strings_to_strarray(l): + """Convert a list of strings to a git_strarray + + We return first the git_strarray* you can pass to libgit2 and a + list of references to the memory, which we must keep around for as + long as the git_strarray must live. + """ + + if not isinstance(l, list): + raise TypeError("Value must be a list") + + arr = ffi.new('git_strarray *') + strings = ffi.new('char *[]', len(l)) + + # We need refs in order to keep a reference to the value returned + # by the ffi.new(). Otherwise, they will be freed and the memory + # re-used, with less than great consequences. + refs = [None] * len(l) + + for i in range(len(l)): + if not is_string(l[i]): + raise TypeError("Value must be a string") + + s = ffi.new('char []', l[i]) + refs[i] = s + strings[i] = s + + arr.strings = strings + arr.count = len(l) + + return arr, refs + +dir_path = path.dirname(path.abspath(inspect.getfile(inspect.currentframe()))) + +decl_path = path.join(dir_path, 'decl.h') +with open(decl_path, 'rb') as f: + ffi.cdef(f.read()) + +C = ffi.verify("#include ", libraries=["git2"]) diff --git a/pygit2/refspec.py b/pygit2/refspec.py new file mode 100644 index 0000000..0097449 --- /dev/null +++ b/pygit2/refspec.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2010-2014 The pygit2 contributors +# +# This file is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2, +# as published by the Free Software Foundation. +# +# In addition to the permissions in the GNU General Public License, +# the authors give you unlimited permission to link the compiled +# version of this file into combinations with other programs, +# and to distribute those combinations without any restriction +# coming from the use of this file. (The General Public License +# restrictions do apply in other respects; for example, they cover +# modification of the file, and distribution when not linked into +# a combined executable.) +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; see the file COPYING. If not, write to +# the Free Software Foundation, 51 Franklin Street, Fifth Floor, +# Boston, MA 02110-1301, USA. + +# Import from the future +from __future__ import absolute_import + +from .ffi import ffi, C, to_str +from .errors import check_error + +class Refspec(object): + def __init__(self, owner, ptr): + self._owner = owner + self._refspec = ptr + + @property + def src(self): + """Source or lhs of the refspec""" + return ffi.string(C.git_refspec_src(self._refspec)).decode() + + @property + def dst(self): + """Destinaton or rhs of the refspec""" + return ffi.string(C.git_refspec_dst(self._refspec)).decode() + + @property + def force(self): + """Whether this refspeca llows non-fast-forward updates""" + return bool(C.git_refspec_force(self._refspec)) + + @property + def string(self): + """String which was used to create this refspec""" + return ffi.string(C.git_refspec_string(self._refspec)).decode() + + @property + def direction(self): + """Direction of this refspec (fetch or push)""" + return C.git_refspec_direction(self._refspec) + + def src_matches(self, ref): + """src_matches(str) -> Bool + + Returns whether the given string matches the source of this refspec""" + return bool(C.git_refspec_src_matches(self._refspec, to_str(ref))) + + def dst_matches(self, ref): + """dst_matches(str) -> Bool + + Returns whether the given string matches the destination of this refspec""" + return bool(C.git_refspec_dst_matches(self._refspec, to_str(ref))) + + def transform(self, ref): + """transform(str) -> str + + Transform a reference name according to this refspec from the lhs to the rhs.""" + alen = len(ref) + err = C.GIT_EBUFS + ptr = None + ref_str = to_str(ref) + + while err == C.GIT_EBUFS: + alen *= 2 + ptr = ffi.new('char []', alen) + + err = C.git_refspec_transform(ptr, alen, self._refspec, ref_str) + + check_error(err) + return ffi.string(ptr).decode() + + def rtransform(self, ref): + """transform(str) -> str + + Transform a reference name according to this refspec from the lhs to the rhs""" + alen = len(ref) + err = C.GIT_EBUFS + ptr = None + ref_str = to_str(ref) + + while err == C.GIT_EBUFS: + alen *= 2 + ptr = ffi.new('char []', alen) + + err = C.git_refspec_rtransform(ptr, alen, self._refspec, ref_str) + + check_error(err) + return ffi.string(ptr).decode() diff --git a/pygit2/remote.py b/pygit2/remote.py new file mode 100644 index 0000000..28a4860 --- /dev/null +++ b/pygit2/remote.py @@ -0,0 +1,207 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2010-2014 The pygit2 contributors +# +# This file is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2, +# as published by the Free Software Foundation. +# +# In addition to the permissions in the GNU General Public License, +# the authors give you unlimited permission to link the compiled +# version of this file into combinations with other programs, +# and to distribute those combinations without any restriction +# coming from the use of this file. (The General Public License +# restrictions do apply in other respects; for example, they cover +# modification of the file, and distribution when not linked into +# a combined executable.) +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; see the file COPYING. If not, write to +# the Free Software Foundation, 51 Franklin Street, Fifth Floor, +# Boston, MA 02110-1301, USA. + +# Import from the future +from __future__ import absolute_import + +from .ffi import ffi, C, to_str, strarray_to_strings, strings_to_strarray +from .errors import check_error, GitError +from .refspec import Refspec + +def maybe_string(ptr): + if not ptr: + return None + + return ffi.string(ptr).decode() + + +class TransferProgress(object): + """Progress downloading and indexing data during a fetch""" + + def __init__(self, tp): + self.total_objects = tp.total_objects + self.indexed_objects = tp.indexed_objects + self.received_objects = tp.received_objects + self.local_objects = tp.local_objects + self.total_deltas = tp.total_deltas + self.indexed_deltas = tp.indexed_deltas + self.received_bytes = tp.received_bytes + +class Remote(object): + def __init__(self, repo, ptr): + """The constructor is for internal use only""" + + self._repo = repo + self._remote = ptr + + # Build the callback structure + callbacks = ffi.new('git_remote_callbacks *') + callbacks.version = 1 + callbacks.transfer_progress = self._transfer_progress_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) + check_error(err) + + def __del__(self): + C.git_remote_free(self._remote) + + @property + def name(self): + return maybe_string(C.git_remote_name(self._remote)) + + @name.setter + def name(self, value): + err = C.git_remote_rename(self._remote, to_str(value), ffi.NULL, ffi.NULL) + check_error(err) + + @property + def url(self): + return maybe_string(C.git_remote_url(self._remote)) + + @url.setter + def url(self, value): + err = C.git_remote_set_url(self._remote, to_str(value)) + + @property + def push_url(self): + return maybe_string(C.git_remote_pushurl(self._remote)) + + @push_url.setter + def push_url(self, value): + err = C.git_remote_set_pushurl(self._remote, to_str(value)) + check_error(err) + + def save(self): + err = C.git_remote_save(self._remote) + check_error(err) + + def fetch(self): + err = C.git_remote_fetch(self._remote) + if err == C.GIT_EUSER: + raise self._stored_exception + + check_error(err) + + return TransferProgress(C.git_remote_stats(self._remote)) + + @property + def refspec_count(self): + return C.git_remote_refspec_count(self._remote) + + def get_refspec(self, n): + spec = C.git_remote_get_refspec(self._remote, n) + return Refspec(self, spec) + + @property + def fetch_refspecs(self): + specs = ffi.new('git_strarray *') + err = C.git_remote_get_fetch_refspecs(specs, self._remote) + check_error(err) + + return strarray_to_strings(specs) + + @fetch_refspecs.setter + def fetch_refspecs(self, l): + arr, refs = strings_to_strarray(l) + err = C.git_remote_set_fetch_refspecs(self._remote, arr) + check_error(err) + + @property + def push_refspecs(self): + specs = ffi.new('git_strarray *') + err = C.git_remote_get_push_refspecs(specs, self._remote) + check_error(err) + + return strarray_to_strings(specs) + + @push_refspecs.setter + def push_refspecs(self, l): + arr, refs = strings_to_strarray(l) + err = C.git_remote_set_push_refspecs(self._remote, arr) + check_error(err) + + def add_fetch(self, spec): + err = C.git_remote_add_fetch(self._remote, to_str(spec)) + + def add_push(self, spec): + err = C.git_remote_add_push(self._remote, to_str(spec)) + + @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, spec): + 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_str(spec)) + 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) + + err = C.git_push_update_tips(push) + check_error(err) + + finally: + 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 + + @ffi.callback('int (*transfer_progress)(const git_transfer_progress *stats, void *data)') + def _transfer_progress_cb(stats_ptr, data): + self = ffi.from_handle(data) + if not hasattr(self, 'transfer_progress'): + return 0 + + try: + self.transfer_progress(TransferProgress(stats_ptr)) + except Exception, e: + self._stored_exception = e + return C.GIT_EUSER + + return 0 diff --git a/pygit2/repository.py b/pygit2/repository.py index e6c750f..a22c820 100644 --- a/pygit2/repository.py +++ b/pygit2/repository.py @@ -35,6 +35,9 @@ from _pygit2 import Oid, GIT_OID_HEXSZ, GIT_OID_MINPREFIXLEN from _pygit2 import GIT_CHECKOUT_SAFE_CREATE, GIT_DIFF_NORMAL from _pygit2 import Reference, Tree, Commit, Blob +from .ffi import ffi, C, to_str +from .errors import check_error +from .remote import Remote class Repository(_Repository): @@ -59,6 +62,51 @@ class Repository(_Repository): def __repr__(self): return "pygit2.Repository(%r)" % self.path + + # + # Remotes + # + def create_remote(self, name, url): + """create_remote(name, url) -> Remote + + Creates a new remote. + """ + + repo_cptr = ffi.new('git_repository **') + repo_cptr[0] = ffi.cast('git_repository *', self._pointer) + cremote = ffi.new('git_remote **') + + repo = repo_cptr[0] + err = C.git_remote_create(cremote, repo, to_str(name), to_str(url)) + check_error(err) + + return Remote(repo, cremote[0]) + + @property + def remotes(self): + """Returns all configured remotes""" + + repo_cptr = ffi.new('git_repository **') + repo_cptr[0] = ffi.cast('git_repository *', self._pointer) + names = ffi.new('git_strarray *') + + repo = repo_cptr[0] + try: + err = C.git_remote_list(names, 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, repo, names.strings[i]) + check_error(err) + + l[i] = Remote(repo, cremote[0]) + return l + finally: + C.git_strarray_free(names) + + # # References # diff --git a/src/refspec.c b/src/refspec.c deleted file mode 100644 index 3053fe6..0000000 --- a/src/refspec.c +++ /dev/null @@ -1,297 +0,0 @@ -/* - * Copyright 2010-2014 The pygit2 contributors - * - * This file is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License, version 2, - * as published by the Free Software Foundation. - * - * In addition to the permissions in the GNU General Public License, - * the authors give you unlimited permission to link the compiled - * version of this file into combinations with other programs, - * and to distribute those combinations without any restriction - * coming from the use of this file. (The General Public License - * restrictions do apply in other respects; for example, they cover - * modification of the file, and distribution when not linked into - * a combined executable.) - * - * This file is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; see the file COPYING. If not, write to - * the Free Software Foundation, 51 Franklin Street, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#define PY_SSIZE_T_CLEAN -#include -#include -#include "error.h" -#include "types.h" -#include "utils.h" -#include "refspec.h" - - -extern PyTypeObject RefspecType; - -Refspec * -wrap_refspec(const Remote *owner, const git_refspec *refspec) -{ - Refspec *spec; - - spec = PyObject_New(Refspec, &RefspecType); - if (!spec) - return NULL; - - Py_INCREF(owner); - spec->owner = owner; - spec->refspec = refspec; - - return spec; -} - -PyDoc_STRVAR(Refspec_direction__doc__, - "The direction of this refspec (fetch or push)"); - -PyObject * -Refspec_direction__get__(Refspec *self) -{ - return Py_BuildValue("i", git_refspec_direction(self->refspec)); -} - -PyDoc_STRVAR(Refspec_src__doc__, "Source or lhs of the refspec"); - -PyObject * -Refspec_src__get__(Refspec *self) -{ - return to_unicode(git_refspec_src(self->refspec), NULL, NULL); -} - -PyDoc_STRVAR(Refspec_dst__doc__, "Destination or rhs of the refspec"); - -PyObject * -Refspec_dst__get__(Refspec *self) -{ - return to_unicode(git_refspec_dst(self->refspec), NULL, NULL); -} - -PyDoc_STRVAR(Refspec_string__doc__, "String used to create this refspec"); - -PyObject * -Refspec_string__get__(Refspec *self) -{ - return to_unicode(git_refspec_string(self->refspec), NULL, NULL); -} - -PyDoc_STRVAR(Refspec_force__doc__, - "Whether this refspec allows non-fast-forward updates"); - -PyObject * -Refspec_force__get__(Refspec *self) -{ - if (git_refspec_force(self->refspec)) - Py_RETURN_TRUE; - - Py_RETURN_FALSE; -} - -PyDoc_STRVAR(Refspec_src_matches__doc__, - "src_matches(str) -> Bool\n" - "\n" - "Returns whether the string matches the source refspec\n"); - -PyObject * -Refspec_src_matches(Refspec *self, PyObject *py_str) -{ - const char *str; - PyObject *tstr; - int res; - - str = py_str_borrow_c_str(&tstr, py_str, NULL); - if (!str) - return NULL; - - res = git_refspec_src_matches(self->refspec, str); - Py_DECREF(tstr); - - if (res) - Py_RETURN_TRUE; - - Py_RETURN_FALSE; -} - -PyDoc_STRVAR(Refspec_dst_matches__doc__, - "dst_matches(str) -> Bool\n" - "\n" - "Returns whether the string matches the destination refspec\n"); - -PyObject * -Refspec_dst_matches(Refspec *self, PyObject *py_str) -{ - const char *str; - PyObject *tstr; - int res; - - str = py_str_borrow_c_str(&tstr, py_str, NULL); - if (!str) - return NULL; - - res = git_refspec_dst_matches(self->refspec, str); - Py_DECREF(tstr); - - if (res) - Py_RETURN_TRUE; - - Py_RETURN_FALSE; -} - -PyDoc_STRVAR(Refspec_transform__doc__, - "transform(str) -> str\n" - "\n" - "Transform a reference according to the refspec\n"); - -PyObject * -Refspec_transform(Refspec *self, PyObject *py_str) -{ - const char *str; - char *trans; - int err, len, alen; - PyObject *py_trans, *tstr; - - str = py_str_borrow_c_str(&tstr, py_str, NULL); - alen = len = strlen(str); - - do { - alen *= alen; - trans = malloc(alen); - if (!trans) { - Py_DECREF(tstr); - return PyErr_NoMemory(); - } - - err = git_refspec_transform(trans, alen, self->refspec, str); - } while(err == GIT_EBUFS); - Py_DECREF(tstr); - - if (err < 0) { - free(trans); - Error_set(err); - return NULL; - } - - py_trans = to_unicode(trans, NULL, NULL); - - free(trans); - - return py_trans; -} - -PyDoc_STRVAR(Refspec_rtransform__doc__, - "rtransform(str) -> str\n" - "\n" - "Transform a reference according to the refspec in reverse\n"); - -PyObject * -Refspec_rtransform(Refspec *self, PyObject *py_str) -{ - const char *str; - char *trans; - int err, len, alen; - PyObject *py_trans, *tstr; - - str = py_str_borrow_c_str(&tstr, py_str, NULL); - alen = len = strlen(str); - - do { - alen *= alen; - trans = malloc(alen); - if (!trans) { - Py_DECREF(tstr); - return PyErr_NoMemory(); - } - - err = git_refspec_rtransform(trans, alen, self->refspec, str); - } while(err == GIT_EBUFS); - Py_DECREF(tstr); - - if (err < 0) { - free(trans); - Error_set(err); - return NULL; - } - - py_trans = to_unicode(trans, NULL, NULL); - - free(trans); - - return py_trans; -} - -PyMethodDef Refspec_methods[] = { - METHOD(Refspec, src_matches, METH_O), - METHOD(Refspec, dst_matches, METH_O), - METHOD(Refspec, transform, METH_O), - METHOD(Refspec, rtransform, METH_O), - {NULL} -}; - -PyGetSetDef Refspec_getseters[] = { - GETTER(Refspec, direction), - GETTER(Refspec, src), - GETTER(Refspec, dst), - GETTER(Refspec, string), - GETTER(Refspec, force), - {NULL} -}; - -static void -Refspec_dealloc(Refspec *self) -{ - Py_CLEAR(self->owner); - PyObject_Del(self); -} - -PyDoc_STRVAR(Refspec__doc__, "Refspec object."); - -PyTypeObject RefspecType = { - PyVarObject_HEAD_INIT(NULL, 0) - "_pygit2.Refspec", /* tp_name */ - sizeof(Refspec), /* tp_basicsize */ - 0, /* tp_itemsize */ - (destructor)Refspec_dealloc, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_compare */ - 0, /* tp_repr */ - 0, /* tp_as_number */ - 0, /* tp_as_sequence */ - 0, /* tp_as_mapping */ - 0, /* tp_hash */ - 0, /* tp_call */ - 0, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ - Refspec__doc__, /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - Refspec_methods, /* tp_methods */ - 0, /* tp_members */ - Refspec_getseters, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - 0, /* tp_init */ - 0, /* tp_alloc */ - 0, /* tp_new */ -}; diff --git a/src/repository.c b/src/repository.c index bb2a11b..14e07a1 100644 --- a/src/repository.c +++ b/src/repository.c @@ -1277,67 +1277,6 @@ Repository_TreeBuilder(Repository *self, PyObject *args) return (PyObject*)builder; } - -PyDoc_STRVAR(Repository_create_remote__doc__, - "create_remote(name, url) -> Remote\n" - "\n" - "Creates a new remote."); - -PyObject * -Repository_create_remote(Repository *self, PyObject *args) -{ - git_remote *remote; - char *name = NULL, *url = NULL; - int err; - - if (!PyArg_ParseTuple(args, "ss", &name, &url)) - return NULL; - - err = git_remote_create(&remote, self->repo, name, url); - if (err < 0) - return Error_set(err); - - return (PyObject*) wrap_remote(remote, self); -} - - -PyDoc_STRVAR(Repository_remotes__doc__, "Returns all configured remotes."); - -PyObject * -Repository_remotes__get__(Repository *self) -{ - git_strarray remotes; - git_remote *remote = NULL; - PyObject *py_list = NULL; - PyObject *py_remote = NULL; - size_t i; - int err; - - git_remote_list(&remotes, self->repo); - - py_list = PyList_New(remotes.count); - for (i=0; i < remotes.count; ++i) { - err = git_remote_load(&remote, self->repo, remotes.strings[i]); - if (err < 0) - goto cleanup; - py_remote = wrap_remote(remote, self); - if (py_remote == NULL) - goto cleanup; - PyList_SetItem(py_list, i, py_remote); - } - - git_strarray_free(&remotes); - return (PyObject*) py_list; - -cleanup: - git_strarray_free(&remotes); - if (py_list) - Py_DECREF(py_list); - if (err < 0) - return Error_set(err); - return NULL; -} - PyDoc_STRVAR(Repository_default_signature__doc__, "Return the signature according to the repository's configuration"); PyObject * @@ -1352,6 +1291,19 @@ Repository_default_signature__get__(Repository *self) return build_signature(NULL, sig, "utf-8"); } +PyDoc_STRVAR(Repository__pointer__doc__, "Get the repo's pointer. For internal use only."); +PyObject * +Repository__pointer__get__(Repository *self) +{ + /* + * This is pretty bad. We shouldn't be casting a pointer into an + * integer, but we can't access the contents of a PyCapsule from + * python code, which we need to do in order to get a type that + * cffi likes. + */ + return PyLong_FromLongLong((long long) self->repo); +} + PyDoc_STRVAR(Repository_checkout_head__doc__, "checkout_head(strategy)\n" "\n" @@ -1633,7 +1585,6 @@ PyMethodDef Repository_methods[] = { METHOD(Repository, revparse_single, METH_O), METHOD(Repository, status, METH_NOARGS), METHOD(Repository, status_file, METH_O), - METHOD(Repository, create_remote, METH_VARARGS), METHOD(Repository, checkout_head, METH_VARARGS), METHOD(Repository, checkout_index, METH_VARARGS), METHOD(Repository, checkout_tree, METH_VARARGS), @@ -1659,8 +1610,8 @@ PyGetSetDef Repository_getseters[] = { GETTER(Repository, is_bare), GETTER(Repository, config), GETTER(Repository, workdir), - GETTER(Repository, remotes), GETTER(Repository, default_signature), + GETTER(Repository, _pointer), {NULL} }; diff --git a/test/test_remote.py b/test/test_remote.py index 797474c..54ba27c 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -189,9 +189,9 @@ class EmptyRepositoryTest(utils.EmptyRepoTestCase): def test_fetch(self): remote = self.repo.remotes[0] stats = remote.fetch() - self.assertEqual(stats['received_bytes'], REMOTE_REPO_BYTES) - self.assertEqual(stats['indexed_objects'], REMOTE_REPO_OBJECTS) - self.assertEqual(stats['received_objects'], REMOTE_REPO_OBJECTS) + self.assertEqual(stats.received_bytes, REMOTE_REPO_BYTES) + self.assertEqual(stats.indexed_objects, REMOTE_REPO_OBJECTS) + self.assertEqual(stats.received_objects, REMOTE_REPO_OBJECTS) def test_transfer_progress(self): self.tp = None @@ -201,9 +201,9 @@ class EmptyRepositoryTest(utils.EmptyRepoTestCase): remote = self.repo.remotes[0] remote.transfer_progress = tp_cb stats = remote.fetch() - self.assertEqual(stats['received_bytes'], self.tp.received_bytes) - self.assertEqual(stats['indexed_objects'], self.tp.indexed_objects) - self.assertEqual(stats['received_objects'], self.tp.received_objects) + self.assertEqual(stats.received_bytes, self.tp.received_bytes) + self.assertEqual(stats.indexed_objects, self.tp.indexed_objects) + self.assertEqual(stats.received_objects, self.tp.received_objects) def test_update_tips(self): remote = self.repo.remotes[0] From 4ef3be18cce6b0645a4bb8d00c4976f00c6f3af3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Sat, 12 Apr 2014 17:13:15 +0200 Subject: [PATCH 04/28] Remote: support credentials via CFFI --- pygit2/decl.h | 18 ++++++++++++++++++ pygit2/remote.py | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/pygit2/decl.h b/pygit2/decl.h index e0ad826..9c4e052 100644 --- a/pygit2/decl.h +++ b/pygit2/decl.h @@ -60,6 +60,13 @@ typedef enum { GIT_DIRECTION_PUSH = 1 } git_direction; +typedef enum { + GIT_CREDTYPE_USERPASS_PLAINTEXT = ..., + GIT_CREDTYPE_SSH_KEY = ..., + GIT_CREDTYPE_SSH_CUSTOM = ..., + GIT_CREDTYPE_DEFAULT = ..., +} git_credtype_t; + typedef struct git_remote_callbacks { unsigned int version; int (*progress)(const char *str, int len, void *data); @@ -125,3 +132,14 @@ int git_refspec_dst_matches(const git_refspec *refspec, const char *refname); int git_refspec_transform(char *out, size_t outlen, const git_refspec *spec, const char *name); int git_refspec_rtransform(char *out, size_t outlen, const git_refspec *spec, const char *name); + +int git_cred_userpass_plaintext_new( + git_cred **out, + const char *username, + const char *password); +int git_cred_ssh_key_new( + git_cred **out, + const char *username, + const char *publickey, + const char *privatekey, + const char *passphrase); diff --git a/pygit2/remote.py b/pygit2/remote.py index 28a4860..eca3dfc 100644 --- a/pygit2/remote.py +++ b/pygit2/remote.py @@ -57,11 +57,13 @@ class Remote(object): self._repo = repo self._remote = ptr + self._stored_exception = None # Build the callback structure callbacks = ffi.new('git_remote_callbacks *') callbacks.version = 1 callbacks.transfer_progress = self._transfer_progress_cb + callbacks.credentials = self._credentials_cb # We need to make sure that this handle stays alive self._self_handle = ffi.new_handle(self) callbacks.payload = self._self_handle @@ -103,8 +105,9 @@ class Remote(object): check_error(err) def fetch(self): + self._stored_exception = None err = C.git_remote_fetch(self._remote) - if err == C.GIT_EUSER: + if self._stored_exception: raise self._stored_exception check_error(err) @@ -205,3 +208,46 @@ class Remote(object): 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)') + def _credentials_cb(cred_out, url, username, allowed, data): + self = ffi.from_handle(data) + + if not hasattr(self, 'credentials'): + return 0 + + try: + url_str = maybe_string(url) + username_str = maybe_string(username) + + creds = self.credentials(url_str, username_str, allowed) + + if not hasattr(creds, 'credential_type') or not hasattr(creds, 'credential_tuple'): + raise TypeError("credential does not implement interface") + + cred_type = creds.credential_type + + if not (allowed & cred_type): + raise TypeError("invalid credential type") + + ccred = ffi.new('git_cred **') + if cred_type == C.GIT_CREDTYPE_USERPASS_PLAINTEXT: + name, passwd = creds.credential_tuple + err = C.git_cred_userpass_plaintext_new(ccred, to_str(name), to_str(passwd)) + + elif cred_type == C.GIT_CREDTYPE_SSH_KEY: + name, pubkey, privkey, passphrase = creds.credential_tuple + err = C.git_cred_ssh_key_new(ccred, to_str(name),to_str(pubkey), + to_str(privkey), to_str(passphrase)) + + else: + raise TypeError("unsupported credential type") + + check_error(err) + cred_out[0] = ccred[0] + + except Exception, e: + self._stored_exception = e + return C.GIT_EUSER + + return 0 From 2d1615dd29f496588fb894418d8a0ea3fd1cadb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Sat, 12 Apr 2014 17:33:15 +0200 Subject: [PATCH 05/28] Remote: add support for transfer and update_tips though CFFI --- pygit2/decl.h | 7 ++++++- pygit2/remote.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/pygit2/decl.h b/pygit2/decl.h index 9c4e052..40680ce 100644 --- a/pygit2/decl.h +++ b/pygit2/decl.h @@ -3,7 +3,12 @@ typedef ... git_remote; typedef ... git_refspec; typedef ... git_push; typedef ... git_cred; -typedef ... git_oid; + +#define GIT_OID_RAWSZ ... + +typedef struct git_oid { + unsigned char id[20]; +} git_oid; typedef struct git_strarray { char **strings; diff --git a/pygit2/remote.py b/pygit2/remote.py index eca3dfc..d68c884 100644 --- a/pygit2/remote.py +++ b/pygit2/remote.py @@ -28,6 +28,8 @@ # Import from the future from __future__ import absolute_import +from _pygit2 import Oid + from .ffi import ffi, C, to_str, strarray_to_strings, strings_to_strarray from .errors import check_error, GitError from .refspec import Refspec @@ -62,7 +64,9 @@ class Remote(object): # Build the callback structure callbacks = ffi.new('git_remote_callbacks *') callbacks.version = 1 + callbacks.progress = self._progress_cb callbacks.transfer_progress = self._transfer_progress_cb + callbacks.update_tips = self._update_tips_cb callbacks.credentials = self._credentials_cb # We need to make sure that this handle stays alive self._self_handle = ffi.new_handle(self) @@ -209,6 +213,41 @@ class Remote(object): return 0 + @ffi.callback('int (*progress)(const char *str, int len, void *data)') + def _progress_cb(string, length, data): + self = ffi.from_handle(data) + + if not hasattr(self, 'progress'): + return 0 + + try: + s = ffi.string(string, length).decode() + self.progress(s) + except Exception, e: + self._stored_exception = e + return C.GIT_EUSER + + return 0 + + @ffi.callback('int (*update_tips)(const char *refname, const git_oid *a, const git_oid *b, void *data)') + def _update_tips_cb(refname, a, b, data): + self = ffi.from_handle(data) + + if not hasattr(self, 'update_tips'): + return 0 + + try: + s = maybe_string(refname) + a = Oid(raw=bytes(ffi.buffer(a))) + b = Oid(raw=bytes(ffi.buffer(b))) + + self.update_tips(s, a, b) + except Exception, 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)') def _credentials_cb(cred_out, url, username, allowed, data): self = ffi.from_handle(data) From cf2703998e42c36c65dc2078377b693a8b41cd01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Sat, 12 Apr 2014 18:18:50 +0200 Subject: [PATCH 06/28] Remote: add documentation strings Now that it has the features of the old implementation, let's add documentation on how to use it. --- docs/remotes.rst | 6 +-- pygit2/remote.py | 104 +++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 103 insertions(+), 7 deletions(-) diff --git a/docs/remotes.rst b/docs/remotes.rst index 58ab8ef..445bef2 100644 --- a/docs/remotes.rst +++ b/docs/remotes.rst @@ -16,9 +16,9 @@ The Remote type .. autoattribute:: pygit2.Remote.refspec_count .. autoattribute:: pygit2.Remote.push_refspecs .. autoattribute:: pygit2.Remote.fetch_refspecs -.. autoattribute:: pygit2.Remote.progress -.. autoattribute:: pygit2.Remote.transfer_progress -.. autoattribute:: pygit2.Remote.update_tips +.. automethod:: pygit2.Remote.progress +.. automethod:: pygit2.Remote.transfer_progress +.. automethod:: pygit2.Remote.update_tips .. automethod:: pygit2.Remote.get_refspec .. automethod:: pygit2.Remote.fetch .. automethod:: pygit2.Remote.push diff --git a/pygit2/remote.py b/pygit2/remote.py index d68c884..c7f122c 100644 --- a/pygit2/remote.py +++ b/pygit2/remote.py @@ -45,15 +45,73 @@ class TransferProgress(object): """Progress downloading and indexing data during a fetch""" def __init__(self, tp): + self.total_objects = tp.total_objects + """Total number objects to download""" + self.indexed_objects = tp.indexed_objects + """Objects which have been indexed""" + self.received_objects = tp.received_objects + """Objects which have been received up to now""" + self.local_objects = tp.local_objects + """Local objects which were used to fix the thin pack""" + self.total_deltas = tp.total_deltas + """Total number of deltas in the pack""" + self.indexed_deltas = tp.indexed_deltas + """Deltas which have been indexed""" + self.received_bytes = tp.received_bytes + """"Number of bytes received up to now""" class Remote(object): + + def progress(self, string): + """Progress output callback + + Override this function with your own progress reporting function + + :param str string: Progress otuput from the remote + """ + pass + + def credentials(self, url, username_from_url, allowed_types): + """Credentials callback + + If the remote server requires authentication, this function will + be called and its return value used for authentication. Override + it if you want to be able to perform authentication. + + :param str url: The url of the remote + :param username_from_url: Username extracted from the url, if any + :type username_from_url: str or None + :param int allowed_types: credential types supported by the remote + :rtype: credential + """ + pass + + def transfer_progress(self, stats): + """Transfer progress callback + + Override with your own function to report transfer progress. + + :param TransferProgress stats: The progress up to now + """ + pass + + def update_tips(self, refname, old, new): + """Update tips callabck + + Override with your own function to report reference updates + + :param str refname: the name of the reference that's being updated + :param Oid old: the reference's old value + :param Oid new: the reference's new value + """ + def __init__(self, repo, ptr): """The constructor is for internal use only""" @@ -80,6 +138,8 @@ class Remote(object): @property def name(self): + """Name of the remote""" + return maybe_string(C.git_remote_name(self._remote)) @name.setter @@ -89,6 +149,8 @@ class Remote(object): @property def url(self): + """Url of the remote""" + return maybe_string(C.git_remote_url(self._remote)) @url.setter @@ -97,6 +159,8 @@ class Remote(object): @property def push_url(self): + """Push url of the remote""" + return maybe_string(C.git_remote_pushurl(self._remote)) @push_url.setter @@ -105,10 +169,19 @@ class Remote(object): check_error(err) def save(self): + """save() + + Save a remote to its repository's configuration""" + err = C.git_remote_save(self._remote) check_error(err) def fetch(self): + """fetch() -> TransferProgress + + Perform a fetch against this remote. + """ + self._stored_exception = None err = C.git_remote_fetch(self._remote) if self._stored_exception: @@ -120,14 +193,22 @@ class Remote(object): @property def refspec_count(self): + """Total number of refspecs in this remote""" + return C.git_remote_refspec_count(self._remote) def get_refspec(self, n): + """get_refspec(n) -> Refspec + + Return the refspec at the given position + """ spec = C.git_remote_get_refspec(self._remote, n) return Refspec(self, spec) @property def fetch_refspecs(self): + """Refspecs that will be used for fetching""" + specs = ffi.new('git_strarray *') err = C.git_remote_get_fetch_refspecs(specs, self._remote) check_error(err) @@ -142,6 +223,8 @@ class Remote(object): @property def push_refspecs(self): + """Refspecs that will be used for pushing""" + specs = ffi.new('git_strarray *') err = C.git_remote_get_push_refspecs(specs, self._remote) check_error(err) @@ -155,9 +238,17 @@ class Remote(object): check_error(err) def add_fetch(self, spec): + """add_fetch(refspec) + + Add a fetch refspec to the remote""" + err = C.git_remote_add_fetch(self._remote, to_str(spec)) def add_push(self, spec): + """add_push(refspec) + + Add a push refspec to the remote""" + err = C.git_remote_add_push(self._remote, to_str(spec)) @ffi.callback("int (*cb)(const char *ref, const char *msg, void *data)") @@ -168,6 +259,10 @@ class Remote(object): return 0 def push(self, spec): + """push(refspec) + + Push the given refspec to the remote. Raises ``GitError`` on error""" + cpush = ffi.new('git_push **') err = C.git_push_new(cpush, self._remote) check_error(err) @@ -202,7 +297,8 @@ class Remote(object): @ffi.callback('int (*transfer_progress)(const git_transfer_progress *stats, void *data)') def _transfer_progress_cb(stats_ptr, data): self = ffi.from_handle(data) - if not hasattr(self, 'transfer_progress'): + + if not hasattr(self, 'transfer_progress') or not self.transfer_progress: return 0 try: @@ -217,7 +313,7 @@ class Remote(object): def _progress_cb(string, length, data): self = ffi.from_handle(data) - if not hasattr(self, 'progress'): + if not hasattr(self, 'progress') or not self.progress: return 0 try: @@ -233,7 +329,7 @@ class Remote(object): def _update_tips_cb(refname, a, b, data): self = ffi.from_handle(data) - if not hasattr(self, 'update_tips'): + if not hasattr(self, 'update_tips') or not self.update_tips: return 0 try: @@ -252,7 +348,7 @@ class Remote(object): def _credentials_cb(cred_out, url, username, allowed, data): self = ffi.from_handle(data) - if not hasattr(self, 'credentials'): + if not hasattr(self, 'credentials') or not self.credentials: return 0 try: From db218fae3f262fdb4367434bd7e02ecb30e77239 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Sat, 12 Apr 2014 18:25:40 +0200 Subject: [PATCH 07/28] Remote: get rid of the C code This code has been obsoleted by the CFFI-using code. Some credentials code remains in C due to the clone functionality making use of it. --- pygit2/credentials.py | 6 +- src/pygit2.c | 15 - src/remote.c | 728 ------------------------------------------ src/remote.h | 39 --- src/repository.c | 2 - src/types.h | 32 -- 6 files changed, 4 insertions(+), 818 deletions(-) delete mode 100644 src/remote.c delete mode 100644 src/remote.h diff --git a/pygit2/credentials.py b/pygit2/credentials.py index cad215a..9f060e2 100644 --- a/pygit2/credentials.py +++ b/pygit2/credentials.py @@ -25,8 +25,10 @@ # the Free Software Foundation, 51 Franklin Street, Fifth Floor, # Boston, MA 02110-1301, USA. -# Import from pygit2 -from _pygit2 import GIT_CREDTYPE_USERPASS_PLAINTEXT, GIT_CREDTYPE_SSH_KEY +from .ffi import ffi, C + +GIT_CREDTYPE_USERPASS_PLAINTEXT = C.GIT_CREDTYPE_USERPASS_PLAINTEXT +GIT_CREDTYPE_SSH_KEY = C.GIT_CREDTYPE_SSH_KEY class UserPass(object): """Username/Password credentials diff --git a/src/pygit2.c b/src/pygit2.c index bda9545..06e4f17 100644 --- a/src/pygit2.c +++ b/src/pygit2.c @@ -75,7 +75,6 @@ extern PyTypeObject BranchType; extern PyTypeObject SignatureType; extern PyTypeObject RemoteType; extern PyTypeObject RefspecType; -extern PyTypeObject TransferProgressType; extern PyTypeObject NoteType; extern PyTypeObject NoteIterType; extern PyTypeObject BlameType; @@ -456,20 +455,6 @@ moduleinit(PyObject* m) ADD_TYPE(m, Config) ADD_TYPE(m, ConfigIter) - /* Remotes */ - INIT_TYPE(RemoteType, NULL, NULL) - INIT_TYPE(RefspecType, NULL, NULL) - INIT_TYPE(TransferProgressType, NULL, NULL) - ADD_TYPE(m, Remote) - ADD_TYPE(m, Refspec) - ADD_TYPE(m, TransferProgress) - /* Direction for the refspec */ - ADD_CONSTANT_INT(m, GIT_DIRECTION_FETCH) - ADD_CONSTANT_INT(m, GIT_DIRECTION_PUSH) - /* Credential types */ - ADD_CONSTANT_INT(m, GIT_CREDTYPE_USERPASS_PLAINTEXT) - ADD_CONSTANT_INT(m, GIT_CREDTYPE_SSH_KEY) - /* Blame */ INIT_TYPE(BlameType, NULL, NULL) INIT_TYPE(BlameIterType, NULL, NULL) diff --git a/src/remote.c b/src/remote.c deleted file mode 100644 index dee9b5a..0000000 --- a/src/remote.c +++ /dev/null @@ -1,728 +0,0 @@ -/* - * Copyright 2010-2014 The pygit2 contributors - * - * This file is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License, version 2, - * as published by the Free Software Foundation. - * - * In addition to the permissions in the GNU General Public License, - * the authors give you unlimited permission to link the compiled - * version of this file into combinations with other programs, - * and to distribute those combinations without any restriction - * coming from the use of this file. (The General Public License - * restrictions do apply in other respects; for example, they cover - * modification of the file, and distribution when not linked into - * a combined executable.) - * - * This file is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; see the file COPYING. If not, write to - * the Free Software Foundation, 51 Franklin Street, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#define PY_SSIZE_T_CLEAN -#include -#include -#include "error.h" -#include "types.h" -#include "utils.h" -#include "oid.h" -#include "refspec.h" -#include "remote.h" - - -extern PyObject *GitError; -extern PyTypeObject RepositoryType; -extern PyTypeObject TransferProgressType; - -PyObject * -wrap_transfer_progress(const git_transfer_progress *stats) -{ - TransferProgress *py_stats; - - py_stats = PyObject_New(TransferProgress, &TransferProgressType); - if (!py_stats) - return NULL; - - py_stats->total_objects = stats->total_objects; - py_stats->indexed_objects = stats->indexed_objects; - py_stats->received_objects = stats->received_objects; - py_stats->local_objects = stats->local_objects; - py_stats->total_deltas = stats->total_deltas; - py_stats->indexed_deltas = stats->indexed_deltas; - py_stats->received_bytes = stats->received_bytes; - - return (PyObject *) py_stats; -} - -void -TransferProgress_dealloc(TransferProgress *self) -{ - PyObject_Del(self); -} - -PyMemberDef TransferProgress_members[] = { - RMEMBER(TransferProgress, total_objects, T_UINT, - "Total number objects to download"), - RMEMBER(TransferProgress, indexed_objects, T_UINT, - "Objects which have been indexed"), - RMEMBER(TransferProgress, received_objects, T_UINT, - "Objects which have been received up to now"), - RMEMBER(TransferProgress, local_objects, T_UINT, - "Local objects which were used to fix the thin pack"), - RMEMBER(TransferProgress, total_deltas, T_UINT, - "Total number of deltas in the pack"), - RMEMBER(TransferProgress, indexed_deltas, T_UINT, - "Deltas which have been indexed"), - /* FIXME: technically this is unsigned, but there's no value for size_t - * here. */ - RMEMBER(TransferProgress, received_bytes, T_PYSSIZET, - "Number of bytes received up to now"), - {NULL}, -}; - -PyDoc_STRVAR(TransferProgress__doc__, - "Progress downloading and indexing data during a fetch"); - -PyTypeObject TransferProgressType = { - PyVarObject_HEAD_INIT(NULL, 0) - "_pygit2.TransferProgress", /* tp_name */ - sizeof(TransferProgress), /* tp_basicsize */ - 0, /* tp_itemsize */ - (destructor)TransferProgress_dealloc, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_compare */ - 0, /* tp_repr */ - 0, /* tp_as_number */ - 0, /* tp_as_sequence */ - 0, /* tp_as_mapping */ - 0, /* tp_hash */ - 0, /* tp_call */ - 0, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT, /* tp_flags */ - TransferProgress__doc__, /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - 0, /* tp_methods */ - TransferProgress_members, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - 0, /* tp_init */ - 0, /* tp_alloc */ - 0, /* tp_new */ -}; - -static int -progress_cb(const char *str, int len, void *data) -{ - Remote *remote = (Remote *) data; - PyObject *arglist, *ret; - - if (remote->progress == NULL) - return 0; - - if (!PyCallable_Check(remote->progress)) { - PyErr_SetString(PyExc_TypeError, "progress callback is not callable"); - return -1; - } - - arglist = Py_BuildValue("(s#)", str, len); - ret = PyObject_CallObject(remote->progress, arglist); - Py_DECREF(arglist); - - if (!ret) - return -1; - - Py_DECREF(ret); - - return 0; -} - -static int -credentials_cb(git_cred **out, const char *url, const char *username_from_url, unsigned int allowed_types, void *data) -{ - Remote *remote = (Remote *) data; - - return callable_to_credentials(out, url, username_from_url, allowed_types, remote->credentials); -} - -static int -transfer_progress_cb(const git_transfer_progress *stats, void *data) -{ - Remote *remote = (Remote *) data; - PyObject *py_stats, *ret; - - if (remote->transfer_progress == NULL) - return 0; - - if (!PyCallable_Check(remote->transfer_progress)) { - PyErr_SetString(PyExc_TypeError, "transfer progress callback is not callable"); - return -1; - } - - py_stats = wrap_transfer_progress(stats); - if (!py_stats) - return -1; - - ret = PyObject_CallFunctionObjArgs(remote->transfer_progress, py_stats, NULL); - Py_DECREF(py_stats); - if (!ret) - return -1; - - Py_DECREF(ret); - - return 0; -} - -static int -update_tips_cb(const char *refname, const git_oid *a, const git_oid *b, void *data) -{ - Remote *remote = (Remote *) data; - PyObject *ret; - PyObject *old, *new; - - if (remote->update_tips == NULL) - return 0; - - if (!PyCallable_Check(remote->update_tips)) { - PyErr_SetString(PyExc_TypeError, "update tips callback is not callable"); - return -1; - } - - old = git_oid_to_python(a); - new = git_oid_to_python(b); - - ret = PyObject_CallFunction(remote->update_tips, "(s,O,O)", refname, old ,new); - - Py_DECREF(old); - Py_DECREF(new); - - if (!ret) - return -1; - - Py_DECREF(ret); - - return 0; -} - -static void -Remote_dealloc(Remote *self) -{ - Py_CLEAR(self->repo); - Py_CLEAR(self->progress); - git_remote_free(self->remote); - PyObject_Del(self); -} - -PyDoc_STRVAR(Remote_name__doc__, "Name of the remote refspec"); - -PyObject * -Remote_name__get__(Remote *self) -{ - return to_unicode(git_remote_name(self->remote), NULL, NULL); -} - -int -Remote_name__set__(Remote *self, PyObject* py_name) -{ - int err; - const char* name; - PyObject *tname; - - name = py_str_borrow_c_str(&tname, py_name, NULL); - if (name != NULL) { - err = git_remote_rename(self->remote, name, NULL, NULL); - Py_DECREF(tname); - - if (err == GIT_OK) - return 0; - - Error_set(err); - } - - return -1; -} - -PyDoc_STRVAR(Remote_fetch_refspecs__doc__, "Fetch refspecs"); - -PyObject * -Remote_fetch_refspecs__get__(Remote *self) -{ - int err; - git_strarray refspecs; - PyObject *new_list; - - err = git_remote_get_fetch_refspecs(&refspecs, self->remote); - if (err != GIT_OK) - return Error_set(err); - - new_list = get_pylist_from_git_strarray(&refspecs); - - git_strarray_free(&refspecs); - return new_list; -} - -int -Remote_fetch_refspecs__set__(Remote *self, PyObject *py_list) -{ - int err; - git_strarray fetch_refspecs; - - if (get_strarraygit_from_pylist(&fetch_refspecs, py_list) < 0) - return -1; - - err = git_remote_set_fetch_refspecs(self->remote, &fetch_refspecs); - git_strarray_free(&fetch_refspecs); - - if (err < 0) { - Error_set(err); - return -1; - } - - return 0; -} - -PyDoc_STRVAR(Remote_push_refspecs__doc__, "Push refspecs"); - -PyObject * -Remote_push_refspecs__get__(Remote *self) -{ - int err; - git_strarray refspecs; - PyObject *new_list; - - err = git_remote_get_push_refspecs(&refspecs, self->remote); - if (err != GIT_OK) - return Error_set(err); - - new_list = get_pylist_from_git_strarray(&refspecs); - - git_strarray_free(&refspecs); - return new_list; -} - -int -Remote_push_refspecs__set__(Remote *self, PyObject *py_list) -{ - int err; - git_strarray push_refspecs; - - if (get_strarraygit_from_pylist(&push_refspecs, py_list) != 0) - return -1; - - err = git_remote_set_push_refspecs(self->remote, &push_refspecs); - git_strarray_free(&push_refspecs); - - if (err < 0) { - Error_set(err); - return -1; - } - - return 0; -} - - -PyDoc_STRVAR(Remote_url__doc__, "Url of the remote"); - - -PyObject * -Remote_url__get__(Remote *self) -{ - const char *url; - - url = git_remote_url(self->remote); - if (!url) - Py_RETURN_NONE; - - return to_unicode(url, NULL, NULL); -} - - -int -Remote_url__set__(Remote *self, PyObject* py_url) -{ - int err; - const char* url = NULL; - PyObject *turl; - - url = py_str_borrow_c_str(&turl, py_url, NULL); - if (url != NULL) { - err = git_remote_set_url(self->remote, url); - Py_DECREF(turl); - - if (err == GIT_OK) - return 0; - - Error_set(err); - } - - return -1; -} - -PyDoc_STRVAR(Remote_push_url__doc__, "Push url of the remote"); - - -PyObject * -Remote_push_url__get__(Remote *self) -{ - const char *url; - - url = git_remote_pushurl(self->remote); - if (!url) - Py_RETURN_NONE; - - return to_unicode(url, NULL, NULL); -} - - -int -Remote_push_url__set__(Remote *self, PyObject* py_url) -{ - int err; - const char* url = NULL; - PyObject *turl; - - url = py_str_borrow_c_str(&turl, py_url, NULL); - if (url != NULL) { - err = git_remote_set_pushurl(self->remote, url); - Py_DECREF(turl); - - if (err == GIT_OK) - return 0; - - Error_set(err); - } - - return -1; -} - - -PyDoc_STRVAR(Remote_refspec_count__doc__, "Number of refspecs."); - -PyObject * -Remote_refspec_count__get__(Remote *self) -{ - size_t count; - - count = git_remote_refspec_count(self->remote); - return PyLong_FromSize_t(count); -} - - -PyDoc_STRVAR(Remote_get_refspec__doc__, - "get_refspec(n) -> (str, str)\n" - "\n" - "Return the refspec at the given position."); - -PyObject * -Remote_get_refspec(Remote *self, PyObject *value) -{ - size_t n; - const git_refspec *refspec; - - n = PyLong_AsSize_t(value); - if (PyErr_Occurred()) - return NULL; - - refspec = git_remote_get_refspec(self->remote, n); - if (refspec == NULL) { - PyErr_SetObject(PyExc_IndexError, value); - return NULL; - } - - return (PyObject*) wrap_refspec(self, refspec); -} - - -PyDoc_STRVAR(Remote_fetch__doc__, - "fetch() -> {'indexed_objects': int, 'received_objects' : int," - " 'received_bytesa' : int}\n" - "\n" - "Negotiate what objects should be downloaded and download the\n" - "packfile with those objects"); - -PyObject * -Remote_fetch(Remote *self, PyObject *args) -{ - PyObject* py_stats = NULL; - const git_transfer_progress *stats; - int err; - - PyErr_Clear(); - err = git_remote_fetch(self->remote); - /* - * XXX: We should be checking for GIT_EUSER, but on v0.20, this does not - * make it all the way to us for update_tips - */ - if (err < 0 && PyErr_Occurred()) - return NULL; - if (err < 0) - return Error_set(err); - - stats = git_remote_stats(self->remote); - py_stats = Py_BuildValue("{s:I,s:I,s:n}", - "indexed_objects", stats->indexed_objects, - "received_objects", stats->received_objects, - "received_bytes", stats->received_bytes); - - return (PyObject*) py_stats; -} - - -PyDoc_STRVAR(Remote_save__doc__, - "save()\n\n" - "Save a remote to its repository configuration."); - -PyObject * -Remote_save(Remote *self, PyObject *args) -{ - int err; - - err = git_remote_save(self->remote); - if (err == GIT_OK) { - Py_RETURN_NONE; - } - else { - return Error_set(err); - } -} - - -int -push_status_foreach_callback(const char *ref, const char *msg, void *data) -{ - const char **msg_dst = (const char **)data; - if (msg != NULL && *msg_dst == NULL) - *msg_dst = msg; - return 0; -} - -PyDoc_STRVAR(Remote_push__doc__, - "push(refspec)\n" - "\n" - "Push the given refspec to the remote. Raises ``GitError`` on error."); - -PyObject * -Remote_push(Remote *self, PyObject *args) -{ - git_push *push = NULL; - const char *refspec = NULL; - const char *msg = NULL; - int err; - - if (!PyArg_ParseTuple(args, "s", &refspec)) - return NULL; - - err = git_push_new(&push, self->remote); - if (err < 0) - return Error_set(err); - - err = git_push_add_refspec(push, refspec); - if (err < 0) - goto error; - - err = git_push_finish(push); - if (err < 0) - goto error; - - if (!git_push_unpack_ok(push)) { - git_push_free(push); - PyErr_SetString(GitError, "Remote failed to unpack objects"); - return NULL; - } - - err = git_push_status_foreach(push, push_status_foreach_callback, &msg); - if (err < 0) - goto error; - if (msg != NULL) { - git_push_free(push); - PyErr_SetString(GitError, msg); - return NULL; - } - - err = git_push_update_tips(push); - if (err < 0) - goto error; - - git_push_free(push); - Py_RETURN_NONE; - -error: - git_push_free(push); - return Error_set(err); -} - - -PyDoc_STRVAR(Remote_add_push__doc__, - "add_push(refspec)\n" - "\n" - "Add a push refspec to the remote."); - -PyObject * -Remote_add_push(Remote *self, PyObject *args) -{ - git_remote *remote; - char *refspec = NULL; - int err = 0; - - if (!PyArg_ParseTuple(args, "s", &refspec)) - return NULL; - - remote = self->remote; - err = git_remote_add_push(remote, refspec); - if (err < 0) - return Error_set(err); - - Py_RETURN_NONE; -} - - -PyDoc_STRVAR(Remote_add_fetch__doc__, - "add_fetch(refspec)\n" - "\n" - "Add a fetch refspec to the remote."); - -PyObject * -Remote_add_fetch(Remote *self, PyObject *args) -{ - git_remote *remote; - char *refspec = NULL; - int err = 0; - - if (!PyArg_ParseTuple(args, "s", &refspec)) - return NULL; - - remote = self->remote; - err = git_remote_add_fetch(remote, refspec); - if (err < 0) - return Error_set(err); - - Py_RETURN_NONE; -} - -PyMethodDef Remote_methods[] = { - METHOD(Remote, fetch, METH_NOARGS), - METHOD(Remote, save, METH_NOARGS), - METHOD(Remote, get_refspec, METH_O), - METHOD(Remote, push, METH_VARARGS), - METHOD(Remote, add_push, METH_VARARGS), - METHOD(Remote, add_fetch, METH_VARARGS), - {NULL} -}; - -PyGetSetDef Remote_getseters[] = { - GETSET(Remote, name), - GETSET(Remote, url), - GETSET(Remote, push_url), - GETTER(Remote, refspec_count), - GETSET(Remote, fetch_refspecs), - GETSET(Remote, push_refspecs), - {NULL} -}; - -PyMemberDef Remote_members[] = { - MEMBER(Remote, progress, T_OBJECT_EX, "Progress output callback"), - MEMBER(Remote, credentials, T_OBJECT_EX, - "credentials(url, username_from_url, allowed_types) -> credential\n" - "\n" - "Credentials callback\n" - "\n" - "If the remote server requires authentication, this function will\n" - "be called and its return value used for authentication.\n" - "\n" - ":param str url: The url of the remote\n" - ":param username_from_url: Username extracted from the url, if any\n" - ":type username_from_url: str or None\n" - ":param int allowed_types: credential types supported by the remote "), - MEMBER(Remote, transfer_progress, T_OBJECT_EX, "Transfer progress callback"), - MEMBER(Remote, update_tips, T_OBJECT_EX, "update tips callback"), - {NULL}, -}; - -PyDoc_STRVAR(Remote__doc__, "Remote object."); - -PyTypeObject RemoteType = { - PyVarObject_HEAD_INIT(NULL, 0) - "_pygit2.Remote", /* tp_name */ - sizeof(Remote), /* tp_basicsize */ - 0, /* tp_itemsize */ - (destructor)Remote_dealloc, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_compare */ - 0, /* tp_repr */ - 0, /* tp_as_number */ - 0, /* tp_as_sequence */ - 0, /* tp_as_mapping */ - 0, /* tp_hash */ - 0, /* tp_call */ - 0, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT, /* tp_flags */ - Remote__doc__, /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - Remote_methods, /* tp_methods */ - Remote_members, /* tp_members */ - Remote_getseters, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - 0, /* tp_init */ - 0, /* tp_alloc */ - 0, /* tp_new */ -}; - -PyObject * -wrap_remote(git_remote *c_remote, Repository *repo) -{ - Remote *py_remote = NULL; - git_remote_callbacks callbacks = GIT_REMOTE_CALLBACKS_INIT; - - py_remote = PyObject_New(Remote, &RemoteType); - if (py_remote) { - Py_INCREF(repo); - py_remote->repo = repo; - py_remote->remote = c_remote; - py_remote->progress = NULL; - py_remote->credentials = NULL; - py_remote->transfer_progress = NULL; - py_remote->update_tips = NULL; - - callbacks.progress = progress_cb; - callbacks.credentials = credentials_cb; - callbacks.transfer_progress = transfer_progress_cb; - callbacks.update_tips = update_tips_cb; - callbacks.payload = py_remote; - git_remote_set_callbacks(c_remote, &callbacks); - } - - return (PyObject *)py_remote; -} diff --git a/src/remote.h b/src/remote.h deleted file mode 100644 index ce6ee47..0000000 --- a/src/remote.h +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2010-2014 The pygit2 contributors - * - * This file is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License, version 2, - * as published by the Free Software Foundation. - * - * In addition to the permissions in the GNU General Public License, - * the authors give you unlimited permission to link the compiled - * version of this file into combinations with other programs, - * and to distribute those combinations without any restriction - * coming from the use of this file. (The General Public License - * restrictions do apply in other respects; for example, they cover - * modification of the file, and distribution when not linked into - * a combined executable.) - * - * This file is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; see the file COPYING. If not, write to - * the Free Software Foundation, 51 Franklin Street, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#ifndef INCLUDE_pygit2_remote_h -#define INCLUDE_pygit2_remote_h - -#define PY_SSIZE_T_CLEAN -#include -#include -#include - -PyObject* Remote_fetch(Remote *self, PyObject *args); -PyObject* wrap_remote(git_remote *c_remote, Repository *repo); - -#endif diff --git a/src/repository.c b/src/repository.c index 14e07a1..e8e38f9 100644 --- a/src/repository.c +++ b/src/repository.c @@ -35,7 +35,6 @@ #include "oid.h" #include "note.h" #include "repository.h" -#include "remote.h" #include "branch.h" #include "blame.h" #include "mergeresult.h" @@ -54,7 +53,6 @@ extern PyTypeObject TreeType; extern PyTypeObject TreeBuilderType; extern PyTypeObject ConfigType; extern PyTypeObject DiffType; -extern PyTypeObject RemoteType; extern PyTypeObject ReferenceType; extern PyTypeObject NoteType; extern PyTypeObject NoteIterType; diff --git a/src/types.h b/src/types.h index cfd0009..e50f834 100644 --- a/src/types.h +++ b/src/types.h @@ -194,38 +194,6 @@ typedef struct { char *encoding; } Signature; - -/* git_remote */ -typedef struct { - PyObject_HEAD - Repository *repo; - git_remote *remote; - /* Callbacks for network events */ - PyObject *progress; - PyObject *credentials; - PyObject *transfer_progress; - PyObject *update_tips; -} Remote; - -/* git_refspec */ -typedef struct { - PyObject_HEAD - const Remote *owner; - const git_refspec *refspec; -} Refspec; - -/* git_transfer_progress */ -typedef struct { - PyObject_HEAD - unsigned int total_objects; - unsigned int indexed_objects; - unsigned int received_objects; - unsigned int local_objects; - unsigned int total_deltas; - unsigned int indexed_deltas; - size_t received_bytes; -} TransferProgress; - /* git_blame */ SIMPLE_TYPE(Blame, git_blame, blame) From 072b0382104f432721dbe9e31395d224f0b751b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Sat, 12 Apr 2014 19:10:50 +0200 Subject: [PATCH 08/28] Implement clone via CFFI as well This lets us get rid of the last piece of C for anything related to remotes and credentials. --- pygit2/__init__.py | 55 ++++++++++++++++++++++++++-- pygit2/decl.h | 64 +++++++++++++++++++++++++++++++++ pygit2/remote.py | 61 +++++++++++++++++-------------- src/pygit2.c | 65 --------------------------------- src/utils.c | 89 ---------------------------------------------- src/utils.h | 2 -- 6 files changed, 150 insertions(+), 186 deletions(-) diff --git a/pygit2/__init__.py b/pygit2/__init__.py index 396af19..df3a898 100644 --- a/pygit2/__init__.py +++ b/pygit2/__init__.py @@ -37,7 +37,9 @@ from .repository import Repository from .version import __version__ from .settings import Settings from .credentials import * -from .remote import Remote +from .remote import Remote, get_credentials +from .errors import check_error +from .ffi import ffi, C, to_str def init_repository(path, bare=False): """ @@ -50,6 +52,19 @@ def init_repository(path, bare=False): return Repository(path) +@ffi.callback('int (*credentials)(git_cred **cred, const char *url, const char *username_from_url, unsigned int allowed_types, void *data)') +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) + cred_out[0] = ccred[0] + except Exception, 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): @@ -75,8 +90,42 @@ def clone_repository( """ - _pygit2.clone_repository( - url, path, bare, ignore_cert_errors, remote_name, checkout_branch, credentials) + opts = ffi.new('git_clone_options *') + crepo = ffi.new('git_repository **') + + branch = checkout_branch or None + + # Data, let's use a dict as we don't really want much more + d = {} + d['callback'] = credentials + d_handle = ffi.new_handle(d) + + # We need to keep the ref alive ourselves + checkout_branch_ref = None + if branch: + checkout_branch_ref = ffi.new('char []', branch) + opts.checkout_branch = checkout_branch_ref + + remote_name_ref = ffi.new('char []', to_str(remote_name)) + opts.remote_name = remote_name_ref + + opts.version = 1 + opts.ignore_cert_errors = ignore_cert_errors + opts.bare = bare + opts.remote_callbacks.version = 1 + opts.checkout_opts.version = 1 + if credentials: + opts.remote_callbacks.credentials = _credentials_cb + opts.remote_callbacks.payload = d_handle + + err = C.git_clone(crepo, to_str(url), to_str(path), opts) + C.git_repository_free(crepo[0]) + + if 'exception' in d: + raise d['exception'] + + check_error(err) + return Repository(path) settings = Settings() diff --git a/pygit2/decl.h b/pygit2/decl.h index 40680ce..a64c32f 100644 --- a/pygit2/decl.h +++ b/pygit2/decl.h @@ -3,6 +3,8 @@ typedef ... git_remote; typedef ... git_refspec; typedef ... git_push; typedef ... git_cred; +typedef ... git_diff_file; +typedef ... git_tree; #define GIT_OID_RAWSZ ... @@ -43,6 +45,7 @@ typedef struct { const git_error * giterr_last(void); void git_strarray_free(git_strarray *array); +void git_repository_free(git_repository *repo); typedef struct git_transfer_progress { unsigned int total_objects; @@ -148,3 +151,64 @@ int git_cred_ssh_key_new( const char *publickey, const char *privatekey, const char *passphrase); + +typedef enum { ... } git_checkout_notify_t; + +typedef int (*git_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); + +typedef void (*git_checkout_progress_cb)( + const char *path, + size_t completed_steps, + size_t total_steps, + void *payload); + +typedef struct git_checkout_opts { + unsigned int version; + + unsigned int checkout_strategy; + + int disable_filters; + unsigned int dir_mode; + unsigned int file_mode; + int file_open_flags; + + unsigned int notify_flags; + git_checkout_notify_cb notify_cb; + void *notify_payload; + + git_checkout_progress_cb progress_cb; + void *progress_payload; + + git_strarray paths; + + git_tree *baseline; + + const char *target_directory; + + const char *our_label; + const char *their_label; +} git_checkout_opts; + + +typedef struct git_clone_options { + unsigned int version; + + git_checkout_opts checkout_opts; + git_remote_callbacks remote_callbacks; + + int bare; + int ignore_cert_errors; + const char *remote_name; + const char* checkout_branch; +} git_clone_options; + +int git_clone(git_repository **out, + const char *url, + const char *local_path, + const git_clone_options *options); diff --git a/pygit2/remote.py b/pygit2/remote.py index c7f122c..40e6dda 100644 --- a/pygit2/remote.py +++ b/pygit2/remote.py @@ -352,33 +352,7 @@ class Remote(object): return 0 try: - url_str = maybe_string(url) - username_str = maybe_string(username) - - creds = self.credentials(url_str, username_str, allowed) - - if not hasattr(creds, 'credential_type') or not hasattr(creds, 'credential_tuple'): - raise TypeError("credential does not implement interface") - - cred_type = creds.credential_type - - if not (allowed & cred_type): - raise TypeError("invalid credential type") - - ccred = ffi.new('git_cred **') - if cred_type == C.GIT_CREDTYPE_USERPASS_PLAINTEXT: - name, passwd = creds.credential_tuple - err = C.git_cred_userpass_plaintext_new(ccred, to_str(name), to_str(passwd)) - - elif cred_type == C.GIT_CREDTYPE_SSH_KEY: - name, pubkey, privkey, passphrase = creds.credential_tuple - err = C.git_cred_ssh_key_new(ccred, to_str(name),to_str(pubkey), - to_str(privkey), to_str(passphrase)) - - else: - raise TypeError("unsupported credential type") - - check_error(err) + ccred = get_credentials(self.credentials, url, username, allowed) cred_out[0] = ccred[0] except Exception, e: @@ -386,3 +360,36 @@ class Remote(object): return C.GIT_EUSER return 0 + +def get_credentials(fn, url, username, allowed): + """Call fn and return the credentials object""" + + url_str = maybe_string(url) + username_str = maybe_string(username) + + creds = fn(url_str, username_str, allowed) + + if not hasattr(creds, 'credential_type') or not hasattr(creds, 'credential_tuple'): + raise TypeError("credential does not implement interface") + + cred_type = creds.credential_type + + if not (allowed & cred_type): + raise TypeError("invalid credential type") + + ccred = ffi.new('git_cred **') + if cred_type == C.GIT_CREDTYPE_USERPASS_PLAINTEXT: + name, passwd = creds.credential_tuple + err = C.git_cred_userpass_plaintext_new(ccred, to_str(name), to_str(passwd)) + + elif cred_type == C.GIT_CREDTYPE_SSH_KEY: + name, pubkey, privkey, passphrase = creds.credential_tuple + err = C.git_cred_ssh_key_new(ccred, to_str(name),to_str(pubkey), + to_str(privkey), to_str(passphrase)) + + else: + raise TypeError("unsupported credential type") + + check_error(err) + + return ccred diff --git a/src/pygit2.c b/src/pygit2.c index 06e4f17..d88c0e4 100644 --- a/src/pygit2.c +++ b/src/pygit2.c @@ -115,69 +115,6 @@ init_repository(PyObject *self, PyObject *args) { Py_RETURN_NONE; }; -static int -credentials_cb(git_cred **out, const char *url, const char *username_from_url, unsigned int allowed_types, void *data) -{ - PyObject *credentials = (PyObject *) data; - - return callable_to_credentials(out, url, username_from_url, allowed_types, credentials); -} - -PyDoc_STRVAR(clone_repository__doc__, - "clone_repository(url, path, bare, remote_name, checkout_branch)\n" - "\n" - "Clones a Git repository in the given url to the given path " - "with the specified options.\n" - "\n" - "Arguments:\n" - "\n" - "url\n" - " Git repository remote url.\n" - "path\n" - " Path where to create the repository.\n" - "bare\n" - " If 'bare' is not 0, then a bare git repository will be created.\n" - "remote_name\n" - " The name given to the 'origin' remote. The default is 'origin'.\n" - "checkout_branch\n" - " The name of the branch to checkout. None means use the remote's " - "HEAD.\n"); - - -PyObject * -clone_repository(PyObject *self, PyObject *args) { - git_repository *repo; - const char *url; - const char *path; - unsigned int bare, ignore_cert_errors; - const char *remote_name, *checkout_branch; - PyObject *credentials = NULL; - int err; - git_clone_options opts = GIT_CLONE_OPTIONS_INIT; - - if (!PyArg_ParseTuple(args, "zzIIzzO", - &url, &path, &bare, &ignore_cert_errors, &remote_name, &checkout_branch, &credentials)) - return NULL; - - opts.bare = bare; - opts.ignore_cert_errors = ignore_cert_errors; - opts.remote_name = remote_name; - opts.checkout_branch = checkout_branch; - - if (credentials != Py_None) { - opts.remote_callbacks.credentials = credentials_cb; - opts.remote_callbacks.payload = credentials; - } - - err = git_clone(&repo, url, path, &opts); - if (err < 0) - return Error_set(err); - - git_repository_free(repo); - Py_RETURN_NONE; -}; - - PyDoc_STRVAR(discover_repository__doc__, "discover_repository(path[, across_fs[, ceiling_dirs]]) -> str\n" "\n" @@ -252,8 +189,6 @@ hash(PyObject *self, PyObject *args) PyMethodDef module_methods[] = { {"init_repository", init_repository, METH_VARARGS, init_repository__doc__}, - {"clone_repository", clone_repository, METH_VARARGS, - clone_repository__doc__}, {"discover_repository", discover_repository, METH_VARARGS, discover_repository__doc__}, {"hashfile", hashfile, METH_VARARGS, hashfile__doc__}, diff --git a/src/utils.c b/src/utils.c index 24b6bbd..44acf5c 100644 --- a/src/utils.c +++ b/src/utils.c @@ -153,92 +153,3 @@ on_error: return -1; } - -static int -py_cred_to_git_cred(git_cred **out, PyObject *py_cred, unsigned int allowed) -{ - PyObject *py_type, *py_tuple; - long type; - int err = -1; - - py_type = PyObject_GetAttrString(py_cred, "credential_type"); - py_tuple = PyObject_GetAttrString(py_cred, "credential_tuple"); - - if (!py_type || !py_tuple) { - printf("py_type %p, py_tuple %p\n", py_type, py_tuple); - PyErr_SetString(PyExc_TypeError, "credential doesn't implement the interface"); - goto cleanup; - } - - if (!PyLong_Check(py_type)) { - PyErr_SetString(PyExc_TypeError, "credential type is not a long"); - goto cleanup; - } - - type = PyLong_AsLong(py_type); - - /* Sanity check, make sure we're given credentials we can use */ - if (!(allowed & type)) { - PyErr_SetString(PyExc_TypeError, "invalid credential type"); - goto cleanup; - } - - switch (type) { - case GIT_CREDTYPE_USERPASS_PLAINTEXT: - { - const char *username, *password; - - if (!PyArg_ParseTuple(py_tuple, "ss", &username, &password)) - goto cleanup; - - err = git_cred_userpass_plaintext_new(out, username, password); - break; - } - case GIT_CREDTYPE_SSH_KEY: - { - const char *username, *pubkey, *privkey, *passphrase; - - if (!PyArg_ParseTuple(py_tuple, "ssss", &username, &pubkey, &privkey, &passphrase)) - goto cleanup; - - err = git_cred_ssh_key_new(out, username, pubkey, privkey, passphrase); - break; - } - default: - PyErr_SetString(PyExc_TypeError, "unsupported credential type"); - break; - } - -cleanup: - Py_XDECREF(py_type); - Py_XDECREF(py_tuple); - - return err; -} - -int -callable_to_credentials(git_cred **out, const char *url, const char *username_from_url, unsigned int allowed_types, PyObject *credentials) -{ - int err; - PyObject *py_cred = NULL, *arglist = NULL; - - if (credentials == NULL || credentials == Py_None) - return 0; - - if (!PyCallable_Check(credentials)) { - PyErr_SetString(PyExc_TypeError, "credentials callback is not callable"); - return -1; - } - - arglist = Py_BuildValue("(szI)", url, username_from_url, allowed_types); - py_cred = PyObject_CallObject(credentials, arglist); - Py_DECREF(arglist); - - if (!py_cred) - return -1; - - err = py_cred_to_git_cred(out, py_cred, allowed_types); - Py_DECREF(py_cred); - - return err; -} diff --git a/src/utils.h b/src/utils.h index 7f95d73..81744f6 100644 --- a/src/utils.h +++ b/src/utils.h @@ -117,8 +117,6 @@ const char *py_str_borrow_c_str(PyObject **tvaue, PyObject *value, const char *e PyObject * get_pylist_from_git_strarray(git_strarray *strarray); int get_strarraygit_from_pylist(git_strarray *array, PyObject *pylist); -int callable_to_credentials(git_cred **out, const char *url, const char *username_from_url, unsigned int allowed_types, PyObject *credentials); - #define py_path_to_c_str(py_path) \ py_str_to_c_str(py_path, Py_FileSystemDefaultEncoding) From 73e9e58fa4941a6b5bcd14537848abbd1ab0000e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Mon, 14 Apr 2014 19:37:44 +0200 Subject: [PATCH 09/28] Config: make bool and int parsing explicit via functions Passing a tuple to the mapping interface isn't the best of interfaces, as the key is only the string. Instead, expose `Config.get_bool()` and `Config.get_int()` methods to parse the values as per the git-config rules before returning the appropriate type to the user. The mapping interface itself returns a string. --- docs/config.rst | 19 ++----- src/config.c | 126 ++++++++++++++++++++++++++------------------ test/test_config.py | 12 ++--- 3 files changed, 87 insertions(+), 70 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index 4bdaaa1..881fc1d 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -24,18 +24,9 @@ The Config type The :class:`Config` Mapping interface. -Parsing the values -=================== +When using the mapping interface, the value is returned as a +string. In order to apply the git-config parsing rules, you can use +:method:`Config.get_bool` or :method:`Config.get_int`. -Instead of a string, a tuple of `(str,type)` can be used to look up a -key and parse it through the Git rules. E.g. - - config['core.bare',bool] - -will return True if 'core.bare' is truthy. - -Truty values are: 'true', 1, 'on' or 'yes'. Falsy values are: 'false', -0, 'off' and 'no'. - -Available types are `bool` and `int`. Not specifying a type returns a -string. +.. automethod:: pygit2.Config.get_bool +.. automethod:: pygit2.Config.get_int diff --git a/src/config.c b/src/config.c index 58fdeaa..07dbb40 100644 --- a/src/config.c +++ b/src/config.c @@ -168,70 +168,94 @@ Config_contains(Config *self, PyObject *py_key) { return 1; } - -PyObject * -Config_getitem(Config *self, PyObject *py_input_key) +/* Get the C string value given a python string as key */ +static int +get_string(const char **key_out, Config *self, PyObject *py_key) { - int err; - const char *value_str; + PyObject *tkey; const char *key; - PyObject *py_key, *py_value, *tkey, *tmp_type = NULL; - PyTypeObject *py_type = NULL; - - if (PyTuple_Check(py_input_key) && PyTuple_Size(py_input_key) == 2) { - py_key = PyTuple_GetItem(py_input_key, 0); - tmp_type = PyTuple_GetItem(py_input_key, 1); - } else { - py_key = py_input_key; - } - - /* If passed a tuple, make sure the second item is a type */ - if (tmp_type) { - if (!PyType_Check(tmp_type)) - return NULL; - else - py_type = (PyTypeObject *) tmp_type; - } + int err; key = py_str_borrow_c_str(&tkey, py_key, NULL); if (key == NULL) + return -1; + + err = git_config_get_string(key_out, self->config, key); + Py_CLEAR(tkey); + + if (err == GIT_ENOTFOUND) { + PyErr_SetObject(PyExc_KeyError, py_key); + return -1; + } + + if (err < 0) { + Error_set(err); + return -1; + } + + return 0; +} + +PyObject * +Config_getitem(Config *self, PyObject *py_key) +{ + int err; + const char *value_str; + + err = get_string(&value_str, self, py_key); + if (err < 0) return NULL; - err = git_config_get_string(&value_str, self->config, key); - Py_CLEAR(tkey); + return to_unicode(value_str, NULL, NULL); +} + +PyDoc_STRVAR(Config_get_bool__doc__, + "get_bool(key) -> Bool\n" + "\n" + "Look up *key* and parse its value as a boolean as per the git-config rules\n" + "\n" + "Truthy values are: 'true', 1, 'on' or 'yes'. Falsy values are: 'false',\n" + "0, 'off' and 'no'"); + +PyObject * +Config_get_bool(Config *self, PyObject *key) +{ + int err, value; + const char *value_str; + + err = get_string(&value_str, self, key); if (err < 0) - goto cleanup; + return NULL; - /* If the user specified a type, let's parse it */ - if (py_type) { - if (py_type == &PyBool_Type) { - int value; - if ((err = git_config_parse_bool(&value, value_str)) < 0) - goto cleanup; + if ((err = git_config_parse_bool(&value, value_str)) < 0) + return NULL; - py_value = PyBool_FromLong(value); - } else if (py_type == &PyInteger_Type) { - int64_t value; - if ((err = git_config_parse_int64(&value, value_str)) < 0) - goto cleanup; + return PyBool_FromLong(value); +} - py_value = PyLong_FromLongLong(value); - } - } else { - py_value = to_unicode(value_str, NULL, NULL); - } +PyDoc_STRVAR(Config_get_int__doc__, + "get_int(key) -> int\n" + "\n" + "Look up *key* and parse its value as an integer as per the git-config rules\n" + "\n" + "A value can have a suffix 'k', 'm' or 'g' which stand for 'kilo', 'mega' and\n" + "'giga' respectively"); -cleanup: - if (err < 0) { - if (err == GIT_ENOTFOUND) { - PyErr_SetObject(PyExc_KeyError, py_key); - return NULL; - } +PyObject * +Config_get_int(Config *self, PyObject *key) +{ + int err; + int64_t value; + const char *value_str; - return Error_set(err); - } + err = get_string(&value_str, self, key); + if (err < 0) + return NULL; - return py_value; + if ((err = git_config_parse_int64(&value, value_str)) < 0) + return NULL; + + return PyLong_FromLongLong(value); } int @@ -396,6 +420,8 @@ PyMethodDef Config_methods[] = { METHOD(Config, add_file, METH_VARARGS | METH_KEYWORDS), METHOD(Config, get_multivar, METH_VARARGS), METHOD(Config, set_multivar, METH_VARARGS), + METHOD(Config, get_bool, METH_O), + METHOD(Config, get_int, METH_O), {NULL} }; diff --git a/test/test_config.py b/test/test_config.py index f29c703..937ed1b 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -74,7 +74,7 @@ class ConfigTest(utils.RepoTestCase): config_read = Config(CONFIG_FILENAME) self.assertTrue('core.bare' in config_read) - self.assertFalse(config_read['core.bare',bool]) + self.assertFalse(config_read.get_bool('core.bare')) self.assertTrue('core.editor' in config_read) self.assertEqual(config_read['core.editor'], 'ed') @@ -88,9 +88,9 @@ class ConfigTest(utils.RepoTestCase): config.add_file(CONFIG_FILENAME, 0) self.assertTrue('this.that' in config) - self.assertTrue(config['this.that',bool]) + self.assertTrue(config.get_bool('this.that')) self.assertTrue('something.other.here' in config) - self.assertFalse(config['something.other.here',bool]) + self.assertFalse(config.get_bool('something.other.here')) def test_read(self): config = self.repo.config @@ -103,11 +103,11 @@ class ConfigTest(utils.RepoTestCase): lambda: config['abc.def']) self.assertTrue('core.bare' in config) - self.assertFalse(config['core.bare',bool]) + self.assertFalse(config.get_bool('core.bare')) self.assertTrue('core.editor' in config) self.assertEqual(config['core.editor'], 'ed') self.assertTrue('core.repositoryformatversion' in config) - self.assertEqual(config['core.repositoryformatversion',int], 0) + self.assertEqual(config.get_int('core.repositoryformatversion'), 0) new_file = open(CONFIG_FILENAME, "w") new_file.write("[this]\n\tthat = foobar\n\tthat = foobeer\n") @@ -129,7 +129,7 @@ class ConfigTest(utils.RepoTestCase): self.assertFalse('core.dummy1' in config) config['core.dummy1'] = 42 self.assertTrue('core.dummy1' in config) - self.assertEqual(config['core.dummy1',int], 42) + self.assertEqual(config.get_int('core.dummy1'), 42) self.assertFalse('core.dummy2' in config) config['core.dummy2'] = 'foobar' From 674546bbb527106273e670b1dd6a4bf84194e4ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Sat, 12 Apr 2014 23:30:00 +0200 Subject: [PATCH 10/28] Some python3 fixes --- pygit2/__init__.py | 2 +- pygit2/ffi.py | 13 +++++++------ pygit2/remote.py | 8 ++++---- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/pygit2/__init__.py b/pygit2/__init__.py index df3a898..2dbda72 100644 --- a/pygit2/__init__.py +++ b/pygit2/__init__.py @@ -59,7 +59,7 @@ def _credentials_cb(cred_out, url, username_from_url, allowed, data): try: ccred = get_credentials(d['callback'], url, username_from_url, allowed) cred_out[0] = ccred[0] - except Exception, e: + except Exception as e: d['exception'] = e return C.GIT_EUSER diff --git a/pygit2/ffi.py b/pygit2/ffi.py index ce25ff0..10762b6 100644 --- a/pygit2/ffi.py +++ b/pygit2/ffi.py @@ -29,7 +29,8 @@ from __future__ import absolute_import import inspect -from os import path +import codecs +from os import path, getenv from cffi import FFI import sys @@ -51,10 +52,10 @@ else: if sys.version_info.major < 3: def is_string(s): - return isinstance(s, str) + return isinstance(s, basestring) else: def is_string(s): - return isinstance(s, basestring) + return isinstance(s, str) ffi = FFI() @@ -88,7 +89,7 @@ def strings_to_strarray(l): if not is_string(l[i]): raise TypeError("Value must be a string") - s = ffi.new('char []', l[i]) + s = ffi.new('char []', to_str(l[i])) refs[i] = s strings[i] = s @@ -100,7 +101,7 @@ def strings_to_strarray(l): dir_path = path.dirname(path.abspath(inspect.getfile(inspect.currentframe()))) decl_path = path.join(dir_path, 'decl.h') -with open(decl_path, 'rb') as f: - ffi.cdef(f.read()) +with codecs.open(decl_path, 'r', 'utf-8') as header: + ffi.cdef(header.read()) C = ffi.verify("#include ", libraries=["git2"]) diff --git a/pygit2/remote.py b/pygit2/remote.py index 40e6dda..07614d3 100644 --- a/pygit2/remote.py +++ b/pygit2/remote.py @@ -303,7 +303,7 @@ class Remote(object): try: self.transfer_progress(TransferProgress(stats_ptr)) - except Exception, e: + except Exception as e: self._stored_exception = e return C.GIT_EUSER @@ -319,7 +319,7 @@ class Remote(object): try: s = ffi.string(string, length).decode() self.progress(s) - except Exception, e: + except Exception as e: self._stored_exception = e return C.GIT_EUSER @@ -338,7 +338,7 @@ class Remote(object): b = Oid(raw=bytes(ffi.buffer(b))) self.update_tips(s, a, b) - except Exception, e: + except Exception as e: self._stored_exception = e return C.GIT_EUSER @@ -355,7 +355,7 @@ class Remote(object): ccred = get_credentials(self.credentials, url, username, allowed) cred_out[0] = ccred[0] - except Exception, e: + except Exception as e: self._stored_exception = e return C.GIT_EUSER From d51174d34ffd898e2922b952da12451c1f74c4d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20David=20Ib=C3=A1=C3=B1ez?= Date: Sun, 13 Apr 2014 17:07:13 +0200 Subject: [PATCH 11/28] Install the "decl.h" file --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index eff54e1..a52fc44 100644 --- a/setup.py +++ b/setup.py @@ -184,6 +184,7 @@ setup(name='pygit2', maintainer_email='jdavid.ibp@gmail.com', long_description=long_description, packages=['pygit2'], + package_data={'pygit2': ['decl.h']}, ext_modules=[ Extension('_pygit2', pygit2_exts, include_dirs=[libgit2_include, 'include'], From e56ab370a7803067b2f230fceb53ebf4d46a580f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Sun, 13 Apr 2014 18:57:03 +0200 Subject: [PATCH 12/28] Remote: make sure to take the contents of the id Pass the contents of the buffer containing the git_oid to bytes() so build a raw representation of its contents. Using bytes(ffi.buffer(...)) returns the representation of the buffer. --- pygit2/remote.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pygit2/remote.py b/pygit2/remote.py index 07614d3..6a2a7b9 100644 --- a/pygit2/remote.py +++ b/pygit2/remote.py @@ -334,8 +334,8 @@ class Remote(object): try: s = maybe_string(refname) - a = Oid(raw=bytes(ffi.buffer(a))) - b = Oid(raw=bytes(ffi.buffer(b))) + a = Oid(raw=bytes(ffi.buffer(a)[:])) + b = Oid(raw=bytes(ffi.buffer(b)[:])) self.update_tips(s, a, b) except Exception as e: From b4bc2b6295e32fe8a4a46d5007ec1209e6af5bce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Sun, 13 Apr 2014 19:20:45 +0200 Subject: [PATCH 13/28] Depend on the cffi package Let both pip and Travis know what we need. --- .travis.yml | 1 + setup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index f2ffa47..4fbf403 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ env: LIBGIT2=~/libgit2/_install/ LD_LIBRARY_PATH=~/libgit2/_install/lib before_install: - sudo apt-get install cmake + - pip install cffi - "./.travis.sh" script: diff --git a/setup.py b/setup.py index a52fc44..914cbed 100644 --- a/setup.py +++ b/setup.py @@ -185,6 +185,7 @@ setup(name='pygit2', long_description=long_description, packages=['pygit2'], package_data={'pygit2': ['decl.h']}, + install_requires=['cffi'], ext_modules=[ Extension('_pygit2', pygit2_exts, include_dirs=[libgit2_include, 'include'], From f3bb8a45567b5244b231e164266972f89f8f7d72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Sun, 13 Apr 2014 20:37:58 +0200 Subject: [PATCH 14/28] Fix installation-time cffi compilation The documentation recommends adding the ffi code as an extension so it gets built at the right time. Make use of the LIBGIT2 environment variable to build and link the ffi module the same way the C extension does so the user doesn't have to export CFLAGS. --- pygit2/ffi.py | 10 +++++++++- setup.py | 6 ++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/pygit2/ffi.py b/pygit2/ffi.py index 10762b6..0f225c8 100644 --- a/pygit2/ffi.py +++ b/pygit2/ffi.py @@ -104,4 +104,12 @@ decl_path = path.join(dir_path, 'decl.h') with codecs.open(decl_path, 'r', 'utf-8') as header: ffi.cdef(header.read()) -C = ffi.verify("#include ", libraries=["git2"]) +# if LIBGIT2 exists, set build and link against that version +libgit2_path = getenv('LIBGIT2') +include_dirs = [] +library_dirs = [] +if libgit2_path: + include_dirs = [path.join(libgit2_path, 'include')] + library_dirs = [path.join(libgit2_path, 'lib')] + +C = ffi.verify("#include ", libraries=["git2"], include_dirs=include_dirs, library_dirs=library_dirs) diff --git a/setup.py b/setup.py index 914cbed..27733f4 100644 --- a/setup.py +++ b/setup.py @@ -173,6 +173,11 @@ classifiers = [ with codecs.open('README.rst', 'r', 'utf-8') as readme: long_description = readme.read() +# This ffi is pygit2.ffi due to the path trick used in the beginning +# of the file +from ffi import ffi +ffi_ext = ffi.verifier.get_extension() + setup(name='pygit2', description='Python bindings for libgit2.', keywords='git', @@ -191,5 +196,6 @@ setup(name='pygit2', include_dirs=[libgit2_include, 'include'], library_dirs=[libgit2_lib], libraries=['git2']), + ffi_ext, ], cmdclass=cmdclass) From 5a20510f8a73ec1603aac33652b64016b0502748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Mon, 14 Apr 2014 02:13:36 +0200 Subject: [PATCH 15/28] Safer repository pointer extraction Casting a pointer to a non-pointer type is something which you should never do. Instead, do something a bit more convoluted, but which is guaranteed to give us the right pointer, as well as making sure that the memory we exchange between python/cffi and the C extension is of the right pointer size. While we're touching this code, fix which object we pass to the Remote constructor to keep alive. We need to pass the Repository object to stop it from becoming unreferenced (and thus freeing memory the remote needs) instead of the git_repository pointer. --- pygit2/repository.py | 25 ++++++++++++++----------- src/repository.c | 9 ++------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/pygit2/repository.py b/pygit2/repository.py index a22c820..107db91 100644 --- a/pygit2/repository.py +++ b/pygit2/repository.py @@ -41,6 +41,15 @@ from .remote import Remote class Repository(_Repository): + def __init__(self, *args, **kwargs): + super(Repository, self).__init__(*args, **kwargs) + + # Get the pointer as the contents of a buffer and store it for + # later access + repo_cptr = ffi.new('git_repository **') + ffi.buffer(repo_cptr)[:] = self._pointer[:] + self._repo = repo_cptr[0] + # # Mapping interface # @@ -72,36 +81,30 @@ class Repository(_Repository): Creates a new remote. """ - repo_cptr = ffi.new('git_repository **') - repo_cptr[0] = ffi.cast('git_repository *', self._pointer) cremote = ffi.new('git_remote **') - repo = repo_cptr[0] - err = C.git_remote_create(cremote, repo, to_str(name), to_str(url)) + err = C.git_remote_create(cremote, self._repo, to_str(name), to_str(url)) check_error(err) - return Remote(repo, cremote[0]) + return Remote(self, cremote[0]) @property def remotes(self): """Returns all configured remotes""" - repo_cptr = ffi.new('git_repository **') - repo_cptr[0] = ffi.cast('git_repository *', self._pointer) names = ffi.new('git_strarray *') - repo = repo_cptr[0] try: - err = C.git_remote_list(names, repo) + 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, repo, names.strings[i]) + err = C.git_remote_load(cremote, self._repo, names.strings[i]) check_error(err) - l[i] = Remote(repo, cremote[0]) + l[i] = Remote(self, cremote[0]) return l finally: C.git_strarray_free(names) diff --git a/src/repository.c b/src/repository.c index e8e38f9..1713136 100644 --- a/src/repository.c +++ b/src/repository.c @@ -1293,13 +1293,8 @@ PyDoc_STRVAR(Repository__pointer__doc__, "Get the repo's pointer. For internal u PyObject * Repository__pointer__get__(Repository *self) { - /* - * This is pretty bad. We shouldn't be casting a pointer into an - * integer, but we can't access the contents of a PyCapsule from - * python code, which we need to do in order to get a type that - * cffi likes. - */ - return PyLong_FromLongLong((long long) self->repo); + /* Bytes means a raw buffer */ + return PyBytes_FromStringAndSize((char *) &self->repo, sizeof(git_repository *)); } PyDoc_STRVAR(Repository_checkout_head__doc__, From d7d0eb37c35607d217819d5b2ae5783a875cb023 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Mon, 14 Apr 2014 16:41:30 +0200 Subject: [PATCH 16/28] ffi: style changes Make to_str() accept None as well as ffi.NULL to return as a negative value, and grab the version in a more compatible way. --- pygit2/ffi.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/pygit2/ffi.py b/pygit2/ffi.py index 0f225c8..468b9bc 100644 --- a/pygit2/ffi.py +++ b/pygit2/ffi.py @@ -34,23 +34,29 @@ from os import path, getenv from cffi import FFI import sys -if sys.version_info.major < 3: +(major_version, _, _, _, _) = sys.version_info + +if major_version < 3: def to_str(s, encoding='utf-8', errors='strict'): - if s == ffi.NULL: + if s == ffi.NULL or s == None: return ffi.NULL - encoding = encoding or 'utf-8' + if isinstance(s, unicode): + encoding = encoding or 'utf-8' return s.encode(encoding, errors) return s else: def to_str(s, encoding='utf-8', errors='strict'): + if s == ffi.NULL or s == None: + return ffi.NULL + if isinstance(s, bytes): return s - else: - return bytes(s, encoding, errors) -if sys.version_info.major < 3: + return s.encode(encoding, errors) + +if major_version < 3: def is_string(s): return isinstance(s, basestring) else: From 37c01d79c5afc20377328fcbebf45abfffbbf61e Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Thu, 17 Apr 2014 10:13:26 -0700 Subject: [PATCH 17/28] Update some docstrings which had got out of date. --- pygit2/repository.py | 2 +- src/repository.c | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pygit2/repository.py b/pygit2/repository.py index e6c750f..7565070 100644 --- a/pygit2/repository.py +++ b/pygit2/repository.py @@ -78,7 +78,7 @@ class Repository(_Repository): Examples:: - repo.create_reference('refs/heads/foo', repo.head.hex) + repo.create_reference('refs/heads/foo', repo.head.get_object().hex) repo.create_reference('refs/tags/foo', 'refs/heads/master') repo.create_reference('refs/tags/foo', 'bbb78a9cec580') """ diff --git a/src/repository.c b/src/repository.c index bb2a11b..2a606e1 100644 --- a/src/repository.c +++ b/src/repository.c @@ -916,7 +916,7 @@ PyDoc_STRVAR(Repository_create_branch__doc__, "\n" "Examples::\n" "\n" - " repo.create_branch('foo', repo.head.hex, force=False)"); + " repo.create_branch('foo', repo.head.get_object(), force=False)"); PyObject * Repository_create_branch(Repository *self, PyObject *args) @@ -1065,7 +1065,7 @@ Repository_lookup_reference(Repository *self, PyObject *py_name) } PyDoc_STRVAR(Repository_create_reference_direct__doc__, - "git_reference_create(name, target, force) -> Reference\n" + "create_reference_direct(name, target, force) -> Reference\n" "\n" "Create a new reference \"name\" which points to an object.\n" "\n" @@ -1077,7 +1077,8 @@ PyDoc_STRVAR(Repository_create_reference_direct__doc__, "\n" "Examples::\n" "\n" - " repo.git_reference_create('refs/heads/foo', repo.head.hex, False)"); + " repo.create_reference_direct('refs/heads/foo',\n" + " repo.head.get_object().hex, False)"); PyObject * Repository_create_reference_direct(Repository *self, PyObject *args, @@ -1104,7 +1105,7 @@ Repository_create_reference_direct(Repository *self, PyObject *args, } PyDoc_STRVAR(Repository_create_reference_symbolic__doc__, - "git_reference_symbolic_create(name, source, force) -> Reference\n" + "create_reference_symbolic(name, source, force) -> Reference\n" "\n" "Create a new reference \"name\" which points to another reference.\n" "\n" @@ -1116,7 +1117,7 @@ PyDoc_STRVAR(Repository_create_reference_symbolic__doc__, "\n" "Examples::\n" "\n" - " repo.git_reference_symbolic_create('refs/tags/foo', 'refs/heads/master', False)"); + " repo.create_reference_symbolic('refs/tags/foo', 'refs/heads/master', False)"); PyObject * Repository_create_reference_symbolic(Repository *self, PyObject *args, From 7ed89b0aabcc2c551807f368faf21310ca30ccec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Mon, 14 Apr 2014 17:19:36 +0200 Subject: [PATCH 18/28] Remote: protect against invalid input on rename The C API expects a non-NULL new name and raises an assertion if we don't protect against such input, so let's guard against falsy values, which also takes care of the empty string, which is also not valid input. --- pygit2/remote.py | 3 +++ test/test_remote.py | 1 + 2 files changed, 4 insertions(+) diff --git a/pygit2/remote.py b/pygit2/remote.py index 6a2a7b9..11bc86c 100644 --- a/pygit2/remote.py +++ b/pygit2/remote.py @@ -144,6 +144,9 @@ class Remote(object): @name.setter def name(self, value): + if not value: + raise ValueError("New remote name must be a non-empty string") + err = C.git_remote_rename(self._remote, to_str(value), ffi.NULL, ffi.NULL) check_error(err) diff --git a/test/test_remote.py b/test/test_remote.py index 54ba27c..9c95f80 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -65,6 +65,7 @@ class RepositoryTest(utils.RepoTestCase): self.assertEqual('new', remote.name) self.assertRaisesAssign(ValueError, remote, 'name', '') + self.assertRaisesAssign(ValueError, remote, 'name', None) def test_remote_set_url(self): From 4c4968a2fb63dd7a147bcbef4830ac9afaad4b3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Fri, 18 Apr 2014 12:17:54 +0200 Subject: [PATCH 19/28] Fix config documentation keyword The keyword for linking to a mehtod is 'meth', not 'method'. Setting the 'currentmodule' allows us to link without the 'pygit2' prefix in the link text. --- docs/config.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/config.rst b/docs/config.rst index 881fc1d..5c091e7 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -22,11 +22,13 @@ The Config type aware that this may return multiple versions of each entry if they are set multiple times in the configuration files. +.. currentmodule:: pygit2 + The :class:`Config` Mapping interface. When using the mapping interface, the value is returned as a string. In order to apply the git-config parsing rules, you can use -:method:`Config.get_bool` or :method:`Config.get_int`. +:meth:`Config.get_bool` or :meth:`Config.get_int`. .. automethod:: pygit2.Config.get_bool .. automethod:: pygit2.Config.get_int From 569d396f0d5a99215b1fd5b058c7ab3646fac3a5 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Fri, 18 Apr 2014 13:44:25 -0700 Subject: [PATCH 20/28] Use repo.head.target instead of repo.head.get_object().hex --- pygit2/repository.py | 2 +- src/repository.c | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pygit2/repository.py b/pygit2/repository.py index 7565070..e184ec2 100644 --- a/pygit2/repository.py +++ b/pygit2/repository.py @@ -78,7 +78,7 @@ class Repository(_Repository): Examples:: - repo.create_reference('refs/heads/foo', repo.head.get_object().hex) + repo.create_reference('refs/heads/foo', repo.head.target) repo.create_reference('refs/tags/foo', 'refs/heads/master') repo.create_reference('refs/tags/foo', 'bbb78a9cec580') """ diff --git a/src/repository.c b/src/repository.c index 2a606e1..8912076 100644 --- a/src/repository.c +++ b/src/repository.c @@ -1077,8 +1077,7 @@ PyDoc_STRVAR(Repository_create_reference_direct__doc__, "\n" "Examples::\n" "\n" - " repo.create_reference_direct('refs/heads/foo',\n" - " repo.head.get_object().hex, False)"); + " repo.create_reference_direct('refs/heads/foo', repo.head.target, False)"); PyObject * Repository_create_reference_direct(Repository *self, PyObject *args, From c41c1a1c97f5dae70160e96f77ad10d259fe8ec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Sun, 20 Apr 2014 18:18:25 +0200 Subject: [PATCH 21/28] Config: move to cffi This halves the amount of code we have to take into account for dealing with the config. There is a slight change in the API. Config.get_multivar() returns an iterator instead of a list, which lets us reuse code from the general iterator and is closer to libgit2's API. --- pygit2/__init__.py | 1 + pygit2/config.py | 259 +++++++++++++++++++++ pygit2/decl.h | 49 ++++ pygit2/errors.py | 7 +- pygit2/repository.py | 18 ++ src/config.c | 540 ------------------------------------------- src/config.h | 45 ---- src/pygit2.c | 7 - src/repository.c | 40 ---- src/types.h | 13 -- test/test_config.py | 13 +- 11 files changed, 339 insertions(+), 653 deletions(-) create mode 100644 pygit2/config.py delete mode 100644 src/config.c delete mode 100644 src/config.h diff --git a/pygit2/__init__.py b/pygit2/__init__.py index 2dbda72..62954f6 100644 --- a/pygit2/__init__.py +++ b/pygit2/__init__.py @@ -38,6 +38,7 @@ from .version import __version__ from .settings import Settings from .credentials import * from .remote import Remote, get_credentials +from .config import Config from .errors import check_error from .ffi import ffi, C, to_str diff --git a/pygit2/config.py b/pygit2/config.py new file mode 100644 index 0000000..a6f2388 --- /dev/null +++ b/pygit2/config.py @@ -0,0 +1,259 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2010-2014 The pygit2 contributors +# +# This file is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2, +# as published by the Free Software Foundation. +# +# In addition to the permissions in the GNU General Public License, +# the authors give you unlimited permission to link the compiled +# version of this file into combinations with other programs, +# and to distribute those combinations without any restriction +# coming from the use of this file. (The General Public License +# restrictions do apply in other respects; for example, they cover +# modification of the file, and distribution when not linked into +# a combined executable.) +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; see the file COPYING. If not, write to +# the Free Software Foundation, 51 Franklin Street, Fifth Floor, +# Boston, MA 02110-1301, USA. + +# Import from the future +from __future__ import absolute_import, unicode_literals + +from _pygit2 import Oid + +from .ffi import ffi, C, to_str, is_string +from .errors import check_error, GitError +from .refspec import Refspec + +def assert_string(v, desc): + if not is_string(v): + raise TypeError("%s must be a string" % desc) + +class ConfigIterator(object): + + def __init__(self, config, ptr): + self._iter = ptr + self._config = config + + def __del__(self): + C.git_config_iterator_free(self._iter) + + def __iter__(self): + return self + + def _next_entry(self): + centry = ffi.new('git_config_entry **') + err = C.git_config_next(centry, self._iter) + check_error(err) + + return centry[0] + + def next(self): + return self.__next__() + + def __next__(self): + entry = self._next_entry() + name = ffi.string(entry.name).decode('utf-8') + value = ffi.string(entry.value).decode('utf-8') + + return name, value + +class ConfigMultivarIterator(ConfigIterator): + def __next__(self): + entry = self._next_entry() + + return ffi.string(entry.value).decode('utf-8') + +class Config(object): + """Git configuration management""" + + def __init__(self, path=None): + cconfig = ffi.new('git_config **') + + if not path: + err = C.git_config_new(cconfig) + else: + assert_string(path, "path") + err = C.git_config_open_ondisk(cconfig, to_str(path)) + + check_error(err, True) + self._config = cconfig[0] + + @classmethod + def from_c(cls, repo, ptr): + config = cls.__new__(cls) + config._repo = repo + config._config = ptr + + return config + + def __del__(self): + C.git_config_free(self._config) + + def _get(self, key): + assert_string(key, "key") + + cstr = ffi.new('char **') + err = C.git_config_get_string(cstr, self._config, to_str(key)) + + return err, cstr + + def _get_string(self, key): + err, cstr = self._get(key) + + if err == C.GIT_ENOTFOUND: + raise KeyError(key) + + check_error(err) + return cstr[0] + + def __contains__(self, key): + err, cstr = self._get(key) + + if err == C.GIT_ENOTFOUND: + return False + + check_error(err) + + return True + + def __getitem__(self, key): + val = self._get_string(key) + + return ffi.string(val).decode() + + def __setitem__(self, key, value): + assert_string(key, "key") + + err = 0 + if isinstance(value, bool): + err = C.git_config_set_bool(self._config, to_str(key), value) + elif isinstance(value, int): + err = C.git_config_set_int64(self._config, to_str(key), value) + else: + err = C.git_config_set_string(self._config, to_str(key), to_str(value)) + + check_error(err) + + def __delitem__(self, key): + assert_string(key, "key") + + err = C.git_config_delete_entry(self._config, to_str(key)) + check_error(err) + + def __iter__(self): + citer = ffi.new('git_config_iterator **') + err = C.git_config_iterator_new(citer, self._config) + check_error(err) + + return ConfigIterator(self, citer[0]) + + def get_multivar(self, name, regex=None): + """get_multivar(name[, regex]) -> [str, ...] + + Get each value of a multivar ''name'' as a list. The optional ''regex'' + parameter is expected to be a regular expression to filter the variables + we're interested in.""" + + assert_string(name, "name") + + citer = ffi.new('git_config_iterator **') + err = C.git_config_multivar_iterator_new(citer, self._config, to_str(name), to_str(regex)) + check_error(err) + + return ConfigMultivarIterator(self, citer[0]) + + def set_multivar(self, name, regex, value): + """set_multivar(name, regex, value) + + Set a multivar ''name'' to ''value''. ''regexp'' is a regular expression + to indicate which values to replace""" + + assert_string(name, "name") + assert_string(regex, "regex") + assert_string(value, "value") + + err = C.git_config_set_multivar(self._config, to_str(name), to_str(regex), to_str(value)) + check_error(err) + + def get_bool(self, key): + """get_bool(key) -> Bool + + Look up *key* and parse its value as a boolean as per the git-config rules + + Truthy values are: 'true', 1, 'on' or 'yes'. Falsy values are: 'false', + 0, 'off' and 'no'""" + + val = self._get_string(key) + res = ffi.new('int *') + err = C.git_config_parse_bool(res, val) + check_error(err) + + return res[0] != 0 + + def get_int(self, key): + """get_int(key) -> int + + Look up *key* and parse its value as an integer as per the git-config rules. + + A value can have a suffix 'k', 'm' or 'g' which stand for 'kilo', 'mega' and + 'giga' respectively""" + + val = self._get_string(key) + res = ffi.new('int64_t *') + err = C.git_config_parse_int64(res, val) + check_error(err) + + return res[0] + + def add_file(self, path, level=0, force=0): + """add_file(path, level=0, force=0) + + Add a config file instance to an existing config.""" + + err = C.git_config_add_file_ondisk(self._config, to_str(path), level, force) + check_error(err) + + # + # Static methods to get specialized version of the config + # + + @staticmethod + def _from_found_config(fn): + buf = ffi.new('char []', C.GIT_PATH_MAX) + err = fn(buf, C.GIT_PATH_MAX) + check_error(err, True) + return Config(ffi.string(buf).decode()) + + @staticmethod + def get_system_config(): + """get_system_config() -> Config + + Return an object representing the system configuration file.""" + + return Config._from_found_config(C.git_config_find_system) + + @staticmethod + def get_global_config(): + """get_global_config() -> Config + + Return an object representing the global configuration file.""" + + return Config._from_found_config(C.git_config_find_global) + + @staticmethod + def get_xdg_config(): + """get_xdg_config() -> Config + + Return an object representing the global configuration file.""" + + return Config._from_found_config(C.git_config_find_xdg) diff --git a/pygit2/decl.h b/pygit2/decl.h index a64c32f..29d4e1c 100644 --- a/pygit2/decl.h +++ b/pygit2/decl.h @@ -5,8 +5,11 @@ typedef ... git_push; typedef ... git_cred; typedef ... git_diff_file; typedef ... git_tree; +typedef ... git_config; +typedef ... git_config_iterator; #define GIT_OID_RAWSZ ... +#define GIT_PATH_MAX ... typedef struct git_oid { unsigned char id[20]; @@ -212,3 +215,49 @@ int git_clone(git_repository **out, const char *url, const char *local_path, const git_clone_options *options); + + +typedef enum { + GIT_CONFIG_LEVEL_SYSTEM = 1, + GIT_CONFIG_LEVEL_XDG = 2, + GIT_CONFIG_LEVEL_GLOBAL = 3, + GIT_CONFIG_LEVEL_LOCAL = 4, + GIT_CONFIG_LEVEL_APP = 5, + GIT_CONFIG_HIGHEST_LEVEL = -1, +} git_config_level_t; + +typedef struct { + const char *name; + const char *value; + git_config_level_t level; +} git_config_entry; + +int git_repository_config(git_config **out, git_repository *repo); +void git_config_free(git_config *cfg); + +int git_config_get_string(const char **out, const git_config *cfg, const char *name); +int git_config_set_string(git_config *cfg, const char *name, const char *value); +int git_config_set_bool(git_config *cfg, const char *name, int value); +int git_config_set_int64(git_config *cfg, const char *name, int64_t value); + +int git_config_parse_bool(int *out, const char *value); +int git_config_parse_int64(int64_t *out, const char *value); + +int git_config_delete_entry(git_config *cfg, const char *name); +int git_config_add_file_ondisk(git_config *cfg, + const char *path, + git_config_level_t level, + int force); + +int git_config_iterator_new(git_config_iterator **out, const git_config *cfg); +int git_config_next(git_config_entry **entry, git_config_iterator *iter); +void git_config_iterator_free(git_config_iterator *iter); + +int git_config_multivar_iterator_new(git_config_iterator **out, const git_config *cfg, const char *name, const char *regexp); +int git_config_set_multivar(git_config *cfg, const char *name, const char *regexp, const char *value); + +int git_config_new(git_config **out); +int git_config_open_ondisk(git_config **out, const char *path); +int git_config_find_system(char *out, size_t length); +int git_config_find_global(char *out, size_t length); +int git_config_find_xdg(char *out, size_t length); diff --git a/pygit2/errors.py b/pygit2/errors.py index b9f6c9b..398d08c 100644 --- a/pygit2/errors.py +++ b/pygit2/errors.py @@ -33,7 +33,7 @@ from .ffi import ffi, C from _pygit2 import GitError -def check_error(err): +def check_error(err, io=False): if err >= 0: return @@ -45,7 +45,10 @@ def check_error(err): if err in [C.GIT_EEXISTS, C.GIT_EINVALIDSPEC, C.GIT_EEXISTS, C.GIT_EAMBIGUOUS]: raise ValueError(message) elif err == C.GIT_ENOTFOUND: - raise KeyError(message) + if io: + raise IOError(message) + else: + raise KeyError(message) elif err == C.GIT_EINVALIDSPEC: raise ValueError(message) elif err == C.GIT_ITEROVER: diff --git a/pygit2/repository.py b/pygit2/repository.py index 107db91..2827c28 100644 --- a/pygit2/repository.py +++ b/pygit2/repository.py @@ -38,6 +38,7 @@ from _pygit2 import Reference, Tree, Commit, Blob from .ffi import ffi, C, to_str from .errors import check_error from .remote import Remote +from .config import Config class Repository(_Repository): @@ -110,6 +111,23 @@ class Repository(_Repository): C.git_strarray_free(names) + # + # Configuration + # + @property + def config(self): + """The configuration file for this repository + + If a the configuration hasn't been set yet, the default config for + repository will be returned, including global and system configurations + (if they are available).""" + + cconfig = ffi.new('git_config **') + err = C.git_repository_config(cconfig, self._repo) + check_error(err) + + return Config.from_c(self, cconfig[0]) + # # References # diff --git a/src/config.c b/src/config.c deleted file mode 100644 index 07dbb40..0000000 --- a/src/config.c +++ /dev/null @@ -1,540 +0,0 @@ -/* - * Copyright 2010-2014 The pygit2 contributors - * - * This file is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License, version 2, - * as published by the Free Software Foundation. - * - * In addition to the permissions in the GNU General Public License, - * the authors give you unlimited permission to link the compiled - * version of this file into combinations with other programs, - * and to distribute those combinations without any restriction - * coming from the use of this file. (The General Public License - * restrictions do apply in other respects; for example, they cover - * modification of the file, and distribution when not linked into - * a combined executable.) - * - * This file is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; see the file COPYING. If not, write to - * the Free Software Foundation, 51 Franklin Street, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#define PY_SSIZE_T_CLEAN -#include -#include "error.h" -#include "types.h" -#include "utils.h" -#include "config.h" - -extern PyTypeObject ConfigType; -extern PyTypeObject ConfigIterType; - - -PyObject * -wrap_config(char *c_path) { - int err; - PyObject *py_path; - Config *py_config; - - py_path = Py_BuildValue("(s)", c_path); - py_config = PyObject_New(Config, &ConfigType); - - err = Config_init(py_config, py_path, NULL); - if (err < 0) - return NULL; - - return (PyObject*) py_config; -} - - -int -Config_init(Config *self, PyObject *args, PyObject *kwds) -{ - char *path = NULL; - int err; - - if (kwds && PyDict_Size(kwds) > 0) { - PyErr_SetString(PyExc_TypeError, - "Config takes no keyword arguments"); - return -1; - } - - if (!PyArg_ParseTuple(args, "|s", &path)) - return -1; - - if (path == NULL) - err = git_config_new(&self->config); - else - err = git_config_open_ondisk(&self->config, path); - - if (err < 0) { - git_config_free(self->config); - - if (err == GIT_ENOTFOUND) - Error_set_exc(PyExc_IOError); - else - Error_set(err); - - return -1; - } - - return 0; -} - - -void -Config_dealloc(Config *self) -{ - git_config_free(self->config); - Py_TYPE(self)->tp_free(self); -} - -PyDoc_STRVAR(Config_get_global_config__doc__, - "get_global_config() -> Config\n" - "\n" - "Return an object representing the global configuration file."); - -PyObject * -Config_get_global_config(void) -{ - char path[GIT_PATH_MAX]; - int err; - - err = git_config_find_global(path, GIT_PATH_MAX); - if (err < 0) { - if (err == GIT_ENOTFOUND) { - PyErr_SetString(PyExc_IOError, "Global config file not found."); - return NULL; - } - - return Error_set(err); - } - - return wrap_config(path); -} - - -PyDoc_STRVAR(Config_get_system_config__doc__, - "get_system_config() -> Config\n" - "\n" - "Return an object representing the system configuration file."); - -PyObject * -Config_get_system_config(void) -{ - char path[GIT_PATH_MAX]; - int err; - - err = git_config_find_system(path, GIT_PATH_MAX); - if (err < 0) { - if (err == GIT_ENOTFOUND) { - PyErr_SetString(PyExc_IOError, "System config file not found."); - return NULL; - } - return Error_set(err); - } - - return wrap_config(path); -} - - -int -Config_contains(Config *self, PyObject *py_key) { - int err; - const char *c_value, *c_key; - PyObject *tkey; - - c_key = py_str_borrow_c_str(&tkey, py_key, NULL); - if (c_key == NULL) - return -1; - - err = git_config_get_string(&c_value, self->config, c_key); - Py_DECREF(tkey); - - if (err < 0) { - if (err == GIT_ENOTFOUND) - return 0; - - Error_set(err); - return -1; - } - - return 1; -} - -/* Get the C string value given a python string as key */ -static int -get_string(const char **key_out, Config *self, PyObject *py_key) -{ - PyObject *tkey; - const char *key; - int err; - - key = py_str_borrow_c_str(&tkey, py_key, NULL); - if (key == NULL) - return -1; - - err = git_config_get_string(key_out, self->config, key); - Py_CLEAR(tkey); - - if (err == GIT_ENOTFOUND) { - PyErr_SetObject(PyExc_KeyError, py_key); - return -1; - } - - if (err < 0) { - Error_set(err); - return -1; - } - - return 0; -} - -PyObject * -Config_getitem(Config *self, PyObject *py_key) -{ - int err; - const char *value_str; - - err = get_string(&value_str, self, py_key); - if (err < 0) - return NULL; - - return to_unicode(value_str, NULL, NULL); -} - -PyDoc_STRVAR(Config_get_bool__doc__, - "get_bool(key) -> Bool\n" - "\n" - "Look up *key* and parse its value as a boolean as per the git-config rules\n" - "\n" - "Truthy values are: 'true', 1, 'on' or 'yes'. Falsy values are: 'false',\n" - "0, 'off' and 'no'"); - -PyObject * -Config_get_bool(Config *self, PyObject *key) -{ - int err, value; - const char *value_str; - - err = get_string(&value_str, self, key); - if (err < 0) - return NULL; - - if ((err = git_config_parse_bool(&value, value_str)) < 0) - return NULL; - - return PyBool_FromLong(value); -} - -PyDoc_STRVAR(Config_get_int__doc__, - "get_int(key) -> int\n" - "\n" - "Look up *key* and parse its value as an integer as per the git-config rules\n" - "\n" - "A value can have a suffix 'k', 'm' or 'g' which stand for 'kilo', 'mega' and\n" - "'giga' respectively"); - -PyObject * -Config_get_int(Config *self, PyObject *key) -{ - int err; - int64_t value; - const char *value_str; - - err = get_string(&value_str, self, key); - if (err < 0) - return NULL; - - if ((err = git_config_parse_int64(&value, value_str)) < 0) - return NULL; - - return PyLong_FromLongLong(value); -} - -int -Config_setitem(Config *self, PyObject *py_key, PyObject *py_value) -{ - int err; - const char *key, *value; - PyObject *tkey, *tvalue; - - key = py_str_borrow_c_str(&tkey, py_key, NULL); - if (key == NULL) - return -1; - - if (py_value == NULL) - err = git_config_delete_entry(self->config, key); - else if (PyBool_Check(py_value)) { - err = git_config_set_bool(self->config, key, - (int)PyObject_IsTrue(py_value)); - } else if (PyLong_Check(py_value)) { - err = git_config_set_int64(self->config, key, - (int64_t)PyLong_AsLong(py_value)); - } else { - value = py_str_borrow_c_str(&tvalue, py_value, NULL); - err = git_config_set_string(self->config, key, value); - Py_DECREF(tvalue); - } - - Py_DECREF(tkey); - if (err < 0) { - Error_set(err); - return -1; - } - return 0; -} - -PyDoc_STRVAR(Config_add_file__doc__, - "add_file(path, level=0, force=0)\n" - "\n" - "Add a config file instance to an existing config."); - -PyObject * -Config_add_file(Config *self, PyObject *args, PyObject *kwds) -{ - char *keywords[] = {"path", "level", "force", NULL}; - int err; - char *path; - unsigned int level = 0; - int force = 0; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|Ii", keywords, - &path, &level, &force)) - return NULL; - - err = git_config_add_file_ondisk(self->config, path, level, force); - if (err < 0) - return Error_set_str(err, path); - - Py_RETURN_NONE; -} - - -PyDoc_STRVAR(Config_get_multivar__doc__, - "get_multivar(name[, regex]) -> [str, ...]\n" - "\n" - "Get each value of a multivar ''name'' as a list. The optional ''regex''\n" - "parameter is expected to be a regular expression to filter the variables\n" - "we're interested in."); - -PyObject * -Config_get_multivar(Config *self, PyObject *args) -{ - int err; - PyObject *list; - const char *name = NULL; - const char *regex = NULL; - git_config_iterator *iter; - git_config_entry *entry; - - if (!PyArg_ParseTuple(args, "s|s", &name, ®ex)) - return NULL; - - list = PyList_New(0); - err = git_config_multivar_iterator_new(&iter, self->config, name, regex); - if (err < 0) - return Error_set(err); - - while ((err = git_config_next(&entry, iter)) == 0) { - PyObject *item; - - item = to_unicode(entry->value, NULL, NULL); - if (item == NULL) { - git_config_iterator_free(iter); - return NULL; - } - - PyList_Append(list, item); - Py_CLEAR(item); - } - - git_config_iterator_free(iter); - if (err == GIT_ITEROVER) - err = 0; - - if (err < 0) - return Error_set(err); - - return list; -} - - -PyDoc_STRVAR(Config_set_multivar__doc__, - "set_multivar(name, regex, value)\n" - "\n" - "Set a multivar ''name'' to ''value''. ''regexp'' is a regular expression\n" - "to indicate which values to replace"); - -PyObject * -Config_set_multivar(Config *self, PyObject *args) -{ - int err; - const char *name = NULL; - const char *regex = NULL; - const char *value = NULL; - - if (!PyArg_ParseTuple(args, "sss", &name, ®ex, &value)) - return NULL; - - err = git_config_set_multivar(self->config, name, regex, value); - if (err < 0) { - if (err == GIT_ENOTFOUND) - Error_set(err); - else - PyErr_SetNone(PyExc_TypeError); - return NULL; - } - - Py_RETURN_NONE; -} - -PyObject * -Config_iter(Config *self) -{ - ConfigIter *iter; - int err; - - iter = PyObject_New(ConfigIter, &ConfigIterType); - if (!iter) - return NULL; - - if ((err = git_config_iterator_new(&iter->iter, self->config)) < 0) - return Error_set(err); - - Py_INCREF(self); - iter->owner = self; - - return (PyObject*)iter; -} - -PyMethodDef Config_methods[] = { - METHOD(Config, get_system_config, METH_NOARGS | METH_STATIC), - METHOD(Config, get_global_config, METH_NOARGS | METH_STATIC), - METHOD(Config, add_file, METH_VARARGS | METH_KEYWORDS), - METHOD(Config, get_multivar, METH_VARARGS), - METHOD(Config, set_multivar, METH_VARARGS), - METHOD(Config, get_bool, METH_O), - METHOD(Config, get_int, METH_O), - {NULL} -}; - -PySequenceMethods Config_as_sequence = { - 0, /* sq_length */ - 0, /* sq_concat */ - 0, /* sq_repeat */ - 0, /* sq_item */ - 0, /* sq_slice */ - 0, /* sq_ass_item */ - 0, /* sq_ass_slice */ - (objobjproc)Config_contains, /* sq_contains */ -}; - -PyMappingMethods Config_as_mapping = { - 0, /* mp_length */ - (binaryfunc)Config_getitem, /* mp_subscript */ - (objobjargproc)Config_setitem, /* mp_ass_subscript */ -}; - - -PyDoc_STRVAR(Config__doc__, "Configuration management."); - -PyTypeObject ConfigType = { - PyVarObject_HEAD_INIT(NULL, 0) - "_pygit2.Config", /* tp_name */ - sizeof(Config), /* tp_basicsize */ - 0, /* tp_itemsize */ - (destructor)Config_dealloc, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_compare */ - 0, /* tp_repr */ - 0, /* tp_as_number */ - &Config_as_sequence, /* tp_as_sequence */ - &Config_as_mapping, /* tp_as_mapping */ - 0, /* tp_hash */ - 0, /* tp_call */ - 0, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT, /* tp_flags */ - Config__doc__, /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - (getiterfunc)Config_iter, /* tp_iter */ - 0, /* tp_iternext */ - Config_methods, /* tp_methods */ - 0, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - (initproc)Config_init, /* tp_init */ - 0, /* tp_alloc */ - 0, /* tp_new */ -}; - -void -ConfigIter_dealloc(ConfigIter *self) -{ - Py_CLEAR(self->owner); - git_config_iterator_free(self->iter); - PyObject_Del(self); -} - -PyObject * -ConfigIter_iternext(ConfigIter *self) -{ - int err; - git_config_entry *entry; - - if ((err = git_config_next(&entry, self->iter)) < 0) - return Error_set(err); - - return Py_BuildValue("ss", entry->name, entry->value); -} - -PyDoc_STRVAR(ConfigIter__doc__, "Configuration iterator."); - -PyTypeObject ConfigIterType = { - PyVarObject_HEAD_INIT(NULL, 0) - "_pygit2.ConfigIter", /* tp_name */ - sizeof(ConfigIter), /* tp_basicsize */ - 0, /* tp_itemsize */ - (destructor)ConfigIter_dealloc , /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_compare */ - 0, /* tp_repr */ - 0, /* tp_as_number */ - 0, /* tp_as_sequence */ - 0, /* tp_as_mapping */ - 0, /* tp_hash */ - 0, /* tp_call */ - 0, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ - ConfigIter__doc__, /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - PyObject_SelfIter, /* tp_iter */ - (iternextfunc)ConfigIter_iternext, /* tp_iternext */ - -}; diff --git a/src/config.h b/src/config.h deleted file mode 100644 index ffa0733..0000000 --- a/src/config.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2010-2014 The pygit2 contributors - * - * This file is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License, version 2, - * as published by the Free Software Foundation. - * - * In addition to the permissions in the GNU General Public License, - * the authors give you unlimited permission to link the compiled - * version of this file into combinations with other programs, - * and to distribute those combinations without any restriction - * coming from the use of this file. (The General Public License - * restrictions do apply in other respects; for example, they cover - * modification of the file, and distribution when not linked into - * a combined executable.) - * - * This file is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; see the file COPYING. If not, write to - * the Free Software Foundation, 51 Franklin Street, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#ifndef INCLUDE_pygit2_config_h -#define INCLUDE_pygit2_config_h - -#define PY_SSIZE_T_CLEAN -#include -#include - -PyObject* wrap_config(char *c_path); -PyObject* Config_get_global_config(void); -PyObject* Config_get_system_config(void); -PyObject* Config_add_file(Config *self, PyObject *args, PyObject *kwds); -PyObject* Config_getitem(Config *self, PyObject *key); -PyObject* Config_foreach(Config *self, PyObject *args); -PyObject* Config_get_multivar(Config *self, PyObject *args); -PyObject* Config_set_multivar(Config *self, PyObject *args); -int Config_init(Config *self, PyObject *args, PyObject *kwds); -int Config_setitem(Config *self, PyObject *key, PyObject *value); -#endif diff --git a/src/pygit2.c b/src/pygit2.c index d88c0e4..2ed0808 100644 --- a/src/pygit2.c +++ b/src/pygit2.c @@ -66,8 +66,6 @@ extern PyTypeObject IndexType; extern PyTypeObject IndexEntryType; extern PyTypeObject IndexIterType; extern PyTypeObject WalkerType; -extern PyTypeObject ConfigType; -extern PyTypeObject ConfigIterType; extern PyTypeObject ReferenceType; extern PyTypeObject RefLogIterType; extern PyTypeObject RefLogEntryType; @@ -385,11 +383,6 @@ moduleinit(PyObject* m) ADD_CONSTANT_INT(m, GIT_CONFIG_LEVEL_XDG); ADD_CONSTANT_INT(m, GIT_CONFIG_LEVEL_SYSTEM); - INIT_TYPE(ConfigType, NULL, PyType_GenericNew) - INIT_TYPE(ConfigIterType, NULL, NULL) - ADD_TYPE(m, Config) - ADD_TYPE(m, ConfigIter) - /* Blame */ INIT_TYPE(BlameType, NULL, NULL) INIT_TYPE(BlameIterType, NULL, NULL) diff --git a/src/repository.c b/src/repository.c index 1713136..acbe1d3 100644 --- a/src/repository.c +++ b/src/repository.c @@ -513,45 +513,6 @@ Repository_workdir__get__(Repository *self, void *closure) return to_path(c_path); } - -PyDoc_STRVAR(Repository_config__doc__, - "Get the configuration file for this repository.\n" - "\n" - "If a configuration file has not been set, the default config set for the\n" - "repository will be returned, including global and system configurations\n" - "(if they are available)."); - -PyObject * -Repository_config__get__(Repository *self) -{ - int err; - git_config *config; - Config *py_config; - - assert(self->repo); - - if (self->config == NULL) { - err = git_repository_config(&config, self->repo); - if (err < 0) - return Error_set(err); - - py_config = PyObject_New(Config, &ConfigType); - if (py_config == NULL) { - git_config_free(config); - return NULL; - } - - py_config->config = config; - self->config = (PyObject*)py_config; - /* We need 2 refs here. One is returned, one is kept internally. */ - Py_INCREF(self->config); - } else { - Py_INCREF(self->config); - } - - return self->config; -} - PyDoc_STRVAR(Repository_merge_base__doc__, "merge_base(oid, oid) -> Oid\n" "\n" @@ -1601,7 +1562,6 @@ PyGetSetDef Repository_getseters[] = { GETTER(Repository, head_is_unborn), GETTER(Repository, is_empty), GETTER(Repository, is_bare), - GETTER(Repository, config), GETTER(Repository, workdir), GETTER(Repository, default_signature), GETTER(Repository, _pointer), diff --git a/src/types.h b/src/types.h index e50f834..c39ade9 100644 --- a/src/types.h +++ b/src/types.h @@ -70,19 +70,6 @@ SIMPLE_TYPE(Tree, git_tree, tree) SIMPLE_TYPE(Blob, git_blob, blob) SIMPLE_TYPE(Tag, git_tag, tag) - -/* git_config */ -typedef struct { - PyObject_HEAD - git_config* config; -} Config; - -typedef struct { - PyObject_HEAD - Config *owner; - git_config_iterator *iter; -} ConfigIter; - /* git_note */ typedef struct { PyObject_HEAD diff --git a/test/test_config.py b/test/test_config.py index 937ed1b..5598c05 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -115,9 +115,10 @@ class ConfigTest(utils.RepoTestCase): config.add_file(CONFIG_FILENAME, 0) self.assertTrue('this.that' in config) - self.assertEqual(len(config.get_multivar('this.that')), 2) - l = config.get_multivar('this.that', 'bar') - self.assertEqual(len(l), 1) + + self.assertEqual(2, len(list(config.get_multivar('this.that')))) + l = list(config.get_multivar('this.that', 'bar')) + self.assertEqual(1, len(l)) self.assertEqual(l[0], 'foobar') def test_write(self): @@ -155,16 +156,16 @@ class ConfigTest(utils.RepoTestCase): config.add_file(CONFIG_FILENAME, 5) self.assertTrue('this.that' in config) l = config.get_multivar('this.that', 'foo.*') - self.assertEqual(len(l), 2) + self.assertEqual(2, len(list(l))) config.set_multivar('this.that', '^.*beer', 'fool') - l = config.get_multivar('this.that', 'fool') + l = list(config.get_multivar('this.that', 'fool')) self.assertEqual(len(l), 1) self.assertEqual(l[0], 'fool') config.set_multivar('this.that', 'foo.*', 'foo-123456') l = config.get_multivar('this.that', 'foo.*') - self.assertEqual(len(l), 2) + self.assertEqual(2, len(list(l))) for i in l: self.assertEqual(i, 'foo-123456') From 1e13a1094915a48ff2a7a2552068986deaa88098 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Sun, 20 Apr 2014 18:37:08 +0200 Subject: [PATCH 22/28] Config: expose config rules parsing Expose Config.parse_bool() and Config.parse_int() to parse text according to git-config rules. --- pygit2/config.py | 19 +++++++++++++++++++ test/test_config.py | 7 +++++++ 2 files changed, 26 insertions(+) diff --git a/pygit2/config.py b/pygit2/config.py index a6f2388..80f38dc 100644 --- a/pygit2/config.py +++ b/pygit2/config.py @@ -223,6 +223,25 @@ class Config(object): err = C.git_config_add_file_ondisk(self._config, to_str(path), level, force) check_error(err) + # + # Methods to parse a string according to the git-config rules + # + + @staticmethod + def parse_bool(text): + res = ffi.new('int *') + err = C.git_config_parse_bool(res, to_str(text)) + + return res[0] != 0 + + @staticmethod + def parse_int(text): + res = ffi.new('int64_t *') + err = C.git_config_parse_int64(res, to_str(text)) + check_error(err) + + return res[0] + # # Static methods to get specialized version of the config # diff --git a/test/test_config.py b/test/test_config.py index 5598c05..9787c3e 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -179,5 +179,12 @@ class ConfigTest(utils.RepoTestCase): self.assertTrue('core.bare' in lst) self.assertTrue(lst['core.bare']) + def test_parsing(self): + self.assertTrue(Config.parse_bool("on")) + self.assertTrue(Config.parse_bool("1")) + + self.assertEqual(5, Config.parse_int("5")) + self.assertEqual(1024, Config.parse_int("1k")) + if __name__ == '__main__': unittest.main() From 1c76d5667a9a9f77c84805f1cbf41f5396b1b07d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Wed, 23 Apr 2014 12:25:23 +0200 Subject: [PATCH 23/28] Blob: implement the memory buffer interface This allows us to expose access to the blob's data without the need to copy it into new buffer. --- src/blob.c | 60 +++++++++++++++++++++++++++++++++++++++++++++-- test/test_blob.py | 7 ++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/blob.c b/src/blob.c index 133d494..de0141b 100644 --- a/src/blob.c +++ b/src/blob.c @@ -158,8 +158,58 @@ PyGetSetDef Blob_getseters[] = { {NULL} }; +static int +Blob_getbuffer(Blob *self, Py_buffer *view, int flags) +{ + return PyBuffer_FillInfo(view, (PyObject *) self, + (void *) git_blob_rawcontent(self->blob), + git_blob_rawsize(self->blob), 1, flags); +} -PyDoc_STRVAR(Blob__doc__, "Blob objects."); +#if PY_MAJOR_VERSION == 2 + +static Py_ssize_t +Blob_getreadbuffer(Blob *self, Py_ssize_t index, const void **ptr) +{ + if (index != 0) { + PyErr_SetString(PyExc_SystemError, + "accessing non-existent blob segment"); + return -1; + } + *ptr = (void *) git_blob_rawcontent(self->blob); + return git_blob_rawsize(self->blob); +} + +static Py_ssize_t +Blob_getsegcount(Blob *self, Py_ssize_t *lenp) +{ + if (lenp) + *lenp = git_blob_rawsize(self->blob); + + return 1; +} + +static PyBufferProcs Blob_as_buffer = { + (readbufferproc)Blob_getreadbuffer, + NULL, /* bf_getwritebuffer */ + (segcountproc)Blob_getsegcount, + NULL, /* charbufferproc */ + (getbufferproc)Blob_getbuffer, +}; + +#else + +static PyBufferProcs Blob_as_buffer = { + (getbufferproc)Blob_getbuffer, +}; + +#endif /* python 2 vs python 3 buffers */ + +PyDoc_STRVAR(Blob__doc__, "Blob object.\n" + "\n" + "Blobs implement the buffer interface, which means you can get access\n" + "to its data via `memoryview(blob)` without the need to create a copy." +); PyTypeObject BlobType = { PyVarObject_HEAD_INIT(NULL, 0) @@ -180,8 +230,14 @@ PyTypeObject BlobType = { 0, /* tp_str */ 0, /* tp_getattro */ 0, /* tp_setattro */ - 0, /* tp_as_buffer */ + &Blob_as_buffer, /* tp_as_buffer */ +#if PY_MAJOR_VERSION == 2 + Py_TPFLAGS_DEFAULT | /* tp_flags */ + Py_TPFLAGS_HAVE_GETCHARBUFFER | + Py_TPFLAGS_HAVE_NEWBUFFER, +#else Py_TPFLAGS_DEFAULT, /* tp_flags */ +#endif Blob__doc__, /* tp_doc */ 0, /* tp_traverse */ 0, /* tp_clear */ diff --git a/test/test_blob.py b/test/test_blob.py index da3db3e..9fd1d4f 100644 --- a/test/test_blob.py +++ b/test/test_blob.py @@ -74,6 +74,13 @@ class BlobTest(utils.RepoTestCase): self.assertEqual(BLOB_NEW_CONTENT, blob.data) self.assertEqual(len(BLOB_NEW_CONTENT), blob.size) self.assertEqual(BLOB_NEW_CONTENT, blob.read_raw()) + blob_buffer = memoryview(blob) + self.assertEqual(len(BLOB_NEW_CONTENT), len(blob_buffer)) + self.assertEqual(BLOB_NEW_CONTENT, blob_buffer) + def set_content(): + blob_buffer[:2] = b'hi' + + self.assertRaises(TypeError, set_content) def test_create_blob_fromworkdir(self): From 17ba85831bb9ab6ed28e0565d64db097b873968f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20David=20Ib=C3=A1=C3=B1ez?= Date: Wed, 23 Apr 2014 14:01:09 +0200 Subject: [PATCH 24/28] Drop official support for Python 2.6 --- .travis.yml | 1 - README.rst | 8 ++++++-- docs/index.rst | 7 +++++-- docs/install.rst | 2 +- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4fbf403..8440098 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: python python: - - "2.6" - "2.7" - "3.2" - "3.3" diff --git a/README.rst b/README.rst index 04c4366..48ffc06 100644 --- a/README.rst +++ b/README.rst @@ -6,8 +6,12 @@ pygit2 - libgit2 bindings in Python :target: http://travis-ci.org/libgit2/pygit2 Pygit2 is a set of Python bindings to the libgit2 shared library, libgit2 -implements the core of Git. Pygit2 works with Python 2.6, 2.7, 3.1, 3.2 and -3.3 +implements the core of Git. Pygit2 works with Python 2.7, 3.2, 3.3, 3.4 and +pypy. + +It is likely to work with Python 2.6 and 3.1, but these versions are not +officially supported. + Pygit2 links: diff --git a/docs/index.rst b/docs/index.rst index 6eccbd5..62f112f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,8 +10,11 @@ Welcome to pygit2's documentation! :target: http://travis-ci.org/libgit2/pygit2 Pygit2 is a set of Python bindings to the libgit2 shared library, libgit2 -implements the core of Git. Pygit2 works with Python 2.6, 2.7, 3.1, 3.2 and -3.3 +implements the core of Git. Pygit2 works with Python 2.7, 3.2, 3.3, 3.4 and +pypy. + +It is likely to work with Python 2.6 and 3.1, but these versions are not +officially supported. Pygit2 links: diff --git a/docs/install.rst b/docs/install.rst index f74e2d7..5f058f2 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -13,7 +13,7 @@ website: http://libgit2.github.com -Also, make sure you have Python 2.6+ installed together with the Python +Also, make sure you have Python 2.7 or 3.2+ installed together with the Python development headers. When those are installed, you can install pygit2: From c68de8e2b8a0c95b90f033dd7883460e39d1f112 Mon Sep 17 00:00:00 2001 From: Jun Omae Date: Mon, 28 Apr 2014 16:24:49 +0900 Subject: [PATCH 25/28] make build options.c with VS2008 compiler --- src/options.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/options.c b/src/options.c index abfa465..072a5ae 100644 --- a/src/options.c +++ b/src/options.c @@ -41,10 +41,11 @@ get_search_path(long level) size_t len = 64; PyObject *py_path; int error; + char *tmp; do { len *= 2; - char *tmp = realloc(buf, len); + tmp = realloc(buf, len); if (!tmp) { free(buf); PyErr_NoMemory(); From 1fbe52c0f753d36ad5f71b9dd1be75c21532b223 Mon Sep 17 00:00:00 2001 From: Daniel Bruce Date: Tue, 29 Apr 2014 12:38:30 +0200 Subject: [PATCH 26/28] Add version check to C code --- src/types.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/types.h b/src/types.h index c39ade9..15f7bfe 100644 --- a/src/types.h +++ b/src/types.h @@ -32,6 +32,10 @@ #include #include +#if !(LIBGIT2_VER_MAJOR == 0 && LIBGIT2_VER_MINOR == 20) +#error You need a compatible libgit2 version (v0.20.x) +#endif + /* * Python objects * From 0c62c83135a799535cda5ce0e33a3f72afc634ca Mon Sep 17 00:00:00 2001 From: "Ian P. McCullough" Date: Thu, 8 May 2014 08:50:21 -0400 Subject: [PATCH 27/28] Fix format string for Blob.diff(); Format string items out of order relative to docstring and outargs. --- src/blob.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blob.c b/src/blob.c index 133d494..d1f5f3b 100644 --- a/src/blob.c +++ b/src/blob.c @@ -63,7 +63,7 @@ Blob_diff(Blob *self, PyObject *args, PyObject *kwds) int err; char *keywords[] = {"blob", "flag", "old_as_path", "new_as_path", NULL}; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O!ssI", keywords, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O!Iss", keywords, &BlobType, &py_blob, &opts.flags, &old_as_path, &new_as_path)) return NULL; From 6b3f9e92f7a67cdb14f86ef972bd41311361afec Mon Sep 17 00:00:00 2001 From: "Ian P. McCullough" Date: Thu, 8 May 2014 09:01:06 -0400 Subject: [PATCH 28/28] And on diff_to_buffer too. --- src/blob.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blob.c b/src/blob.c index d1f5f3b..076827b 100644 --- a/src/blob.c +++ b/src/blob.c @@ -106,7 +106,7 @@ Blob_diff_to_buffer(Blob *self, PyObject *args, PyObject *kwds) char *keywords[] = {"buffer", "flag", "old_as_path", "buffer_as_path", NULL}; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|s#ssI", keywords, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|s#Iss", keywords, &buffer, &buffer_len, &opts.flags, &old_as_path, &buffer_as_path)) return NULL;