From 3a38f60db768e1904d4d6ea72fb36c0a717a2404 Mon Sep 17 00:00:00 2001 From: Robert Coup Date: Thu, 23 Jul 2020 09:14:07 +0100 Subject: [PATCH 1/3] Add crude repr() method implementation to Object and related classes >>> repr(a_commit) --- src/blob.c | 2 +- src/commit.c | 2 +- src/object.c | 10 +++++++++- src/object.h | 1 + src/tree.c | 2 +- test/test_object.py | 6 ++++++ 6 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/blob.c b/src/blob.c index 8ebf9da1e..0e426328b 100644 --- a/src/blob.c +++ b/src/blob.c @@ -208,7 +208,7 @@ PyTypeObject BlobType = { 0, /* tp_getattr */ 0, /* tp_setattr */ 0, /* tp_compare */ - 0, /* tp_repr */ + (reprfunc)Object_repr, /* tp_repr */ 0, /* tp_as_number */ 0, /* tp_as_sequence */ 0, /* tp_as_mapping */ diff --git a/src/commit.c b/src/commit.c index 3436bcf18..20a3dd0c7 100644 --- a/src/commit.c +++ b/src/commit.c @@ -286,7 +286,7 @@ PyTypeObject CommitType = { 0, /* tp_getattr */ 0, /* tp_setattr */ 0, /* tp_compare */ - 0, /* tp_repr */ + (reprfunc)Object_repr, /* tp_repr */ 0, /* tp_as_number */ 0, /* tp_as_sequence */ 0, /* tp_as_mapping */ diff --git a/src/object.c b/src/object.c index 3787aad46..6c5768bde 100644 --- a/src/object.c +++ b/src/object.c @@ -258,6 +258,14 @@ Object_hash(Object *self) return ret; } +PyObject * +Object_repr(Object *self) +{ + return PyUnicode_FromFormat("", + git_object_type2string(Object__type(self)), + Object_hex__get__(self)); +} + PyObject * Object_richcompare(PyObject *o1, PyObject *o2, int op) { @@ -325,7 +333,7 @@ PyTypeObject ObjectType = { 0, /* tp_getattr */ 0, /* tp_setattr */ 0, /* tp_compare */ - 0, /* tp_repr */ + (reprfunc)Object_repr, /* tp_repr */ 0, /* tp_as_number */ 0, /* tp_as_sequence */ 0, /* tp_as_mapping */ diff --git a/src/object.h b/src/object.h index 41c22c925..ebe793df9 100644 --- a/src/object.h +++ b/src/object.h @@ -40,6 +40,7 @@ PyObject* Object_get_oid(Object *self); PyObject* Object_get_hex(Object *self); PyObject* Object_get_type(Object *self); PyObject* Object_read_raw(Object *self); +PyObject* Object_repr(Object *self); PyObject* wrap_object(git_object *c_object, Repository *repo, const git_tree_entry *entry); #endif diff --git a/src/tree.c b/src/tree.c index 7d74abf0a..92977d8f3 100644 --- a/src/tree.c +++ b/src/tree.c @@ -455,7 +455,7 @@ PyTypeObject TreeType = { 0, /* tp_getattr */ 0, /* tp_setattr */ 0, /* tp_compare */ - 0, /* tp_repr */ + (reprfunc)Object_repr, /* tp_repr */ &Tree_as_number, /* tp_as_number */ &Tree_as_sequence, /* tp_as_sequence */ &Tree_as_mapping, /* tp_as_mapping */ diff --git a/test/test_object.py b/test/test_object.py index 488db4f0c..a679d0f55 100644 --- a/test/test_object.py +++ b/test/test_object.py @@ -128,3 +128,9 @@ def test_obj(obj, msg): test_obj(tree, "tree#"+tree.id.hex) for entry in tree: test_obj(testrepo[entry.hex], "entry="+entry.name+"#"+entry.hex) + + +def test_repr(testrepo): + commit_id = testrepo.lookup_reference('refs/heads/master').target + commit_a = testrepo[commit_id] + assert repr(commit_a) == "" % commit_id From 160e7ce999fe423da95663493c2fe03a645ccce0 Mon Sep 17 00:00:00 2001 From: Robert Coup Date: Thu, 23 Jul 2020 09:17:59 +0100 Subject: [PATCH 2/3] revparse: expand to Repository.revparse() method for full revparse support. This maps to git_revparse() and accepts full revspecs including ranges: https://git-scm.com/docs/git-rev-parse.html#_specifying_revisions Returns a RevSpec object which has from_object/to_object/flags properties. --- src/pygit2.c | 9 +++ src/repository.c | 28 ++++++++ src/revspec.c | 160 ++++++++++++++++++++++++++++++++++++++++++ src/revspec.h | 38 ++++++++++ src/types.h | 7 ++ test/test_revparse.py | 78 ++++++++++++++++++++ 6 files changed, 320 insertions(+) create mode 100644 src/revspec.c create mode 100644 src/revspec.h create mode 100644 test/test_revparse.py diff --git a/src/pygit2.c b/src/pygit2.c index 140aeafb6..d2d6fcbbb 100644 --- a/src/pygit2.c +++ b/src/pygit2.c @@ -67,6 +67,7 @@ extern PyTypeObject RefdbType; extern PyTypeObject RefdbBackendType; extern PyTypeObject RefdbFsBackendType; extern PyTypeObject ReferenceType; +extern PyTypeObject RevSpecType; extern PyTypeObject RefLogIterType; extern PyTypeObject RefLogEntryType; extern PyTypeObject BranchType; @@ -424,6 +425,14 @@ PyInit__pygit2(void) ADD_CONSTANT_INT(m, GIT_REF_SYMBOLIC) ADD_CONSTANT_INT(m, GIT_REF_LISTALL) + /* + * RevSpec + */ + INIT_TYPE(RevSpecType, NULL, NULL) + ADD_CONSTANT_INT(m, GIT_REVPARSE_SINGLE) + ADD_CONSTANT_INT(m, GIT_REVPARSE_RANGE) + ADD_CONSTANT_INT(m, GIT_REVPARSE_MERGE_BASE) + /* * Worktree */ diff --git a/src/repository.c b/src/repository.c index 8c8c8ead8..59041fda2 100755 --- a/src/repository.c +++ b/src/repository.c @@ -30,6 +30,7 @@ #include "error.h" #include "types.h" #include "reference.h" +#include "revspec.h" #include "utils.h" #include "odb.h" #include "object.h" @@ -57,6 +58,7 @@ extern PyTypeObject TreeBuilderType; extern PyTypeObject ConfigType; extern PyTypeObject DiffType; extern PyTypeObject ReferenceType; +extern PyTypeObject RevSpecType; extern PyTypeObject NoteType; extern PyTypeObject NoteIterType; @@ -364,6 +366,31 @@ Repository_revparse_single(Repository *self, PyObject *py_spec) } +PyDoc_STRVAR(Repository_revparse__doc__, + "revparse(revspec) -> RevSpec\n" + "\n" + "Parse a revision string for from, to, and intent. See `man gitrevisions`,\n" + "or the documentation for `git rev-parse` for information on the syntax\n" + "accepted."); + +PyObject * +Repository_revparse(Repository *self, PyObject *py_spec) +{ + /* Get the C revision spec */ + const char *c_spec = pgit_borrow(py_spec); + if (c_spec == NULL) + return NULL; + + /* Lookup */ + git_revspec revspec; + int err = git_revparse(&revspec, self->repo, c_spec); + if (err) { + return Error_set_str(err, c_spec); + } + return wrap_revspec(&revspec, self); +} + + PyDoc_STRVAR(Repository_path__doc__, "The normalized path to the git repository."); @@ -1989,6 +2016,7 @@ PyMethodDef Repository_methods[] = { METHOD(Repository, lookup_reference, METH_O), METHOD(Repository, lookup_reference_dwim, METH_O), METHOD(Repository, revparse_single, METH_O), + METHOD(Repository, revparse, METH_O), METHOD(Repository, status, METH_NOARGS), METHOD(Repository, status_file, METH_O), METHOD(Repository, notes, METH_VARARGS), diff --git a/src/revspec.c b/src/revspec.c new file mode 100644 index 000000000..6ab13ea8c --- /dev/null +++ b/src/revspec.c @@ -0,0 +1,160 @@ +/* + * Copyright 2010-2020 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 "object.h" +#include "types.h" +#include "utils.h" + +extern PyTypeObject RevSpecType; + +PyObject* +wrap_revspec(git_revspec *revspec, Repository *repo) +{ + RevSpec *py_revspec; + + py_revspec = PyObject_New(RevSpec, &RevSpecType); + if (py_revspec) { + py_revspec->flags = revspec->flags; + + if (revspec->from != NULL) { + py_revspec->from = wrap_object(revspec->from, repo, NULL); + } else { + py_revspec->from = NULL; + } + + if (revspec->to != NULL) { + py_revspec->to = wrap_object(revspec->to, repo, NULL); + } else { + py_revspec->to = NULL; + } + } + + return (PyObject*) py_revspec; +} + +PyDoc_STRVAR(RevSpec_from_object__doc__, "From revision"); + +PyObject * +RevSpec_from_object__get__(RevSpec *self) +{ + if (self->from == NULL) + Py_RETURN_NONE; + + Py_INCREF(self->from); + return self->from; +} + +PyDoc_STRVAR(RevSpec_to_object__doc__, "To revision"); + +PyObject * +RevSpec_to_object__get__(RevSpec *self) +{ + if (self->to == NULL) + Py_RETURN_NONE; + + Py_INCREF(self->to); + return self->to; +} + +PyDoc_STRVAR(RevSpec_flags__doc__, + "A combination of GIT_REVPARSE_ flags which indicate the\n" + "intended behavior of the spec passed to Repository.revparse()"); + +PyObject * +RevSpec_flags__get__(RevSpec *self) +{ + return PyLong_FromLong(self->flags); +} + +static PyObject * +RevSpec_repr(RevSpec *self) +{ + return PyUnicode_FromFormat("", + (self->from != NULL) ? self->from : Py_None, + (self->to != NULL) ? self->to : Py_None); +} + +static void +RevSpec_dealloc(RevSpec *self) +{ + Py_XDECREF(self->from); + Py_XDECREF(self->to); + PyObject_Del(self); +} + +PyGetSetDef RevSpec_getsetters[] = { + GETTER(RevSpec, from_object), + GETTER(RevSpec, to_object), + GETTER(RevSpec, flags), + {NULL} +}; + +PyDoc_STRVAR(RevSpec__doc__, "RevSpec object, output from Repository.revparse()."); + +PyTypeObject RevSpecType = { + PyVarObject_HEAD_INIT(NULL, 0) + "_pygit2.RevSpec", /* tp_name */ + sizeof(RevSpec), /* tp_basicsize */ + 0, /* tp_itemsize */ + (destructor)RevSpec_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + (reprfunc)RevSpec_repr, /* 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 */ + RevSpec__doc__, /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + 0, /* tp_methods */ + 0, /* tp_members */ + RevSpec_getsetters, /* 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/revspec.h b/src/revspec.h new file mode 100644 index 000000000..4c178a198 --- /dev/null +++ b/src/revspec.h @@ -0,0 +1,38 @@ +/* + * Copyright 2010-2020 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_revspec_h +#define INCLUDE_pygit2_revspec_h + +#define PY_SSIZE_T_CLEAN +#include +#include +#include "types.h" + +PyObject* wrap_revspec(git_revspec *revspec, Repository *repo); + +#endif diff --git a/src/types.h b/src/types.h index 1ff222c85..0f3c8cf99 100644 --- a/src/types.h +++ b/src/types.h @@ -236,6 +236,13 @@ typedef struct { size_t size; } RefLogIter; +/* git_revspec */ +typedef struct { + PyObject_HEAD + PyObject *from; + PyObject *to; + unsigned int flags; +} RevSpec; /* git_signature */ typedef struct { diff --git a/test/test_revparse.py b/test/test_revparse.py new file mode 100644 index 000000000..01a342996 --- /dev/null +++ b/test/test_revparse.py @@ -0,0 +1,78 @@ +# Copyright 2020 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. + +"""Tests for revision parsing.""" + +from pygit2 import GIT_REVPARSE_SINGLE, GIT_REVPARSE_RANGE, GIT_REVPARSE_MERGE_BASE, InvalidSpecError +from pytest import raises + +HEAD_SHA = '2be5719152d4f82c7302b1c0932d8e5f0a4a0e98' +PARENT_SHA = '5ebeeebb320790caf276b9fc8b24546d63316533' # HEAD^ + + +def test_revparse_single(testrepo): + o = testrepo.revparse_single('HEAD') + assert o.hex == HEAD_SHA + + o = testrepo.revparse_single('HEAD^') + assert o.hex == PARENT_SHA + + o = testrepo.revparse_single('@{-1}') + assert o.hex == '5470a671a80ac3789f1a6a8cefbcf43ce7af0563' + + +def test_revparse_1(testrepo): + s = testrepo.revparse('master') + assert s.from_object.hex == HEAD_SHA + assert s.to_object is None + assert s.flags == GIT_REVPARSE_SINGLE + + +def test_revparse_range_1(testrepo): + s = testrepo.revparse('HEAD^1..acecd5e') + assert s.from_object.hex == PARENT_SHA + assert s.to_object.hex.startswith('acecd5e') + assert s.flags == GIT_REVPARSE_RANGE + + +def test_revparse_range_2(testrepo): + s = testrepo.revparse('HEAD...i18n') + assert s.from_object.hex.startswith('2be5719') + assert s.to_object.hex.startswith('5470a67') + assert s.flags == GIT_REVPARSE_RANGE | GIT_REVPARSE_MERGE_BASE + assert testrepo.merge_base(s.from_object.id, s.to_object.id) is not None + + +def test_revparse_range_errors(testrepo): + with raises(KeyError): + testrepo.revparse('nope..2be571915') + + with raises(InvalidSpecError): + testrepo.revparse('master............2be571915') + + +def test_revparse_repr(testrepo): + s = testrepo.revparse('HEAD...i18n') + assert repr(s) == ",to=}>" From 083ecd8b1e53a5d772764c6e33b3cf84b461ae74 Mon Sep 17 00:00:00 2001 From: Robert Coup Date: Thu, 23 Jul 2020 09:21:42 +0100 Subject: [PATCH 3/3] revparse: add Repository.revparse_ext() Implements git_revparse_ext(). Takes the same revspec as Repository.revparse_single(), but returns a 2-tuple (Object, Reference). Where the revspec maps to a reference it is returned as well as the target object, otherwise None. --- src/repository.c | 39 +++++++++++++++++++++++++++++++++++++++ test/test_revparse.py | 14 ++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/src/repository.c b/src/repository.c index 59041fda2..93fa053cd 100755 --- a/src/repository.c +++ b/src/repository.c @@ -366,6 +366,44 @@ Repository_revparse_single(Repository *self, PyObject *py_spec) } +PyDoc_STRVAR(Repository_revparse_ext__doc__, + "revparse_ext(revision) -> (Object, Reference)\n" + "\n" + "Find a single object and intermediate reference, as specified by a revision\n" + "string. See `man gitrevisions`, or the documentation for `git rev-parse`\n" + "for information on the syntax accepted.\n" + "\n" + "In some cases (@{<-n>} or @{upstream}), the expression may\n" + "point to an intermediate reference, which is returned in the second element\n" + "of the result tuple."); + +PyObject * +Repository_revparse_ext(Repository *self, PyObject *py_spec) +{ + /* Get the C revision spec */ + const char *c_spec = pgit_borrow(py_spec); + if (c_spec == NULL) + return NULL; + + /* Lookup */ + git_object *c_obj = NULL; + git_reference *c_ref = NULL; + int err = git_revparse_ext(&c_obj, &c_ref, self->repo, c_spec); + if (err) + return Error_set_str(err, c_spec); + + PyObject *py_obj = wrap_object(c_obj, self, NULL); + PyObject *py_ref = NULL; + if (c_ref != NULL) { + py_ref = wrap_reference(c_ref, self); + } else { + py_ref = Py_None; + Py_INCREF(Py_None); + } + return Py_BuildValue("NN", py_obj, py_ref); +} + + PyDoc_STRVAR(Repository_revparse__doc__, "revparse(revspec) -> RevSpec\n" "\n" @@ -2016,6 +2054,7 @@ PyMethodDef Repository_methods[] = { METHOD(Repository, lookup_reference, METH_O), METHOD(Repository, lookup_reference_dwim, METH_O), METHOD(Repository, revparse_single, METH_O), + METHOD(Repository, revparse_ext, METH_O), METHOD(Repository, revparse, METH_O), METHOD(Repository, status, METH_NOARGS), METHOD(Repository, status_file, METH_O), diff --git a/test/test_revparse.py b/test/test_revparse.py index 01a342996..2d7ccbb30 100644 --- a/test/test_revparse.py +++ b/test/test_revparse.py @@ -43,6 +43,20 @@ def test_revparse_single(testrepo): assert o.hex == '5470a671a80ac3789f1a6a8cefbcf43ce7af0563' +def test_revparse_ext(testrepo): + o, r = testrepo.revparse_ext('master') + assert o.hex == HEAD_SHA + assert r == testrepo.references['refs/heads/master'] + + o, r = testrepo.revparse_ext('HEAD^') + assert o.hex == PARENT_SHA + assert r is None + + o, r = testrepo.revparse_ext('i18n') + assert o.hex.startswith('5470a67') + assert r == testrepo.references['refs/heads/i18n'] + + def test_revparse_1(testrepo): s = testrepo.revparse('master') assert s.from_object.hex == HEAD_SHA