diff --git a/docs/index_file.rst b/docs/index_file.rst index 58fd35a2b..68170b5b8 100644 --- a/docs/index_file.rst +++ b/docs/index_file.rst @@ -51,6 +51,12 @@ The IndexEntry type .. automethod:: __repr__ .. automethod:: __str__ +The Stash type +==================== + +.. autoclass:: pygit2.Stash + :members: commit_id, message + Status ==================== @@ -111,3 +117,4 @@ Stash .. automethod:: pygit2.Repository.stash_apply .. automethod:: pygit2.Repository.stash_drop .. automethod:: pygit2.Repository.stash_pop +.. automethod:: pygit2.Repository.listall_stashes diff --git a/src/pygit2.c b/src/pygit2.c index 2c3e74f41..6785d29f1 100644 --- a/src/pygit2.c +++ b/src/pygit2.c @@ -78,6 +78,7 @@ extern PyTypeObject NoteType; extern PyTypeObject NoteIterType; extern PyTypeObject WorktreeType; extern PyTypeObject MailmapType; +extern PyTypeObject StashType; PyDoc_STRVAR(discover_repository__doc__, @@ -629,6 +630,10 @@ PyInit__pygit2(void) INIT_TYPE(MailmapType, NULL, PyType_GenericNew) ADD_TYPE(m, Mailmap) + /* Stash */ + INIT_TYPE(StashType, NULL, NULL) + ADD_TYPE(m, Stash) + /* Global initialization of libgit2 */ git_libgit2_init(); diff --git a/src/repository.c b/src/repository.c index 0768c390b..b98da6f3d 100755 --- a/src/repository.c +++ b/src/repository.c @@ -61,6 +61,7 @@ extern PyTypeObject ReferenceType; extern PyTypeObject RevSpecType; extern PyTypeObject NoteType; extern PyTypeObject NoteIterType; +extern PyTypeObject StashType; /* forward-declaration for Repsository._from_c() */ PyTypeObject RepositoryType; @@ -2211,6 +2212,63 @@ Repository_set_refdb(Repository *self, Refdb *refdb) Py_RETURN_NONE; } +static int foreach_stash_cb(size_t index, const char *message, const git_oid *stash_id, void *payload) +{ + int err; + Stash *py_stash; + + py_stash = PyObject_New(Stash, &StashType); + if (py_stash == NULL) + return GIT_EUSER; + + assert(message != NULL); + assert(stash_id != NULL); + + py_stash->commit_id = git_oid_to_python(stash_id); + if (py_stash->commit_id == NULL) + return GIT_EUSER; + + py_stash->message = strdup(message); + if (py_stash->message == NULL) { + PyErr_NoMemory(); + return GIT_EUSER; + } + + PyObject* list = (PyObject*) payload; + err = PyList_Append(list, (PyObject*) py_stash); + Py_DECREF(py_stash); + if (err < 0) + return GIT_EUSER; + + return 0; +} + +PyDoc_STRVAR(Repository_listall_stashes__doc__, + "listall_stashes() -> [Stash, ...]\n" + "\n" + "Return a list with all stashed commits in the repository.\n"); + +PyObject * +Repository_listall_stashes(Repository *self, PyObject *args) +{ + int err; + + PyObject *list = PyList_New(0); + if (list == NULL) + return NULL; + + err = git_stash_foreach(self->repo, foreach_stash_cb, (void*)list); + + if (err == 0) { + return list; + } else { + Py_CLEAR(list); + if (PyErr_Occurred()) + return NULL; + return Error_set(err); + } +} + PyMethodDef Repository_methods[] = { METHOD(Repository, create_blob, METH_VARARGS), METHOD(Repository, create_blob_fromworkdir, METH_O), @@ -2263,6 +2321,7 @@ PyMethodDef Repository_methods[] = { METHOD(Repository, _disown, METH_NOARGS), METHOD(Repository, set_odb, METH_O), METHOD(Repository, set_refdb, METH_O), + METHOD(Repository, listall_stashes, METH_NOARGS), {NULL} }; diff --git a/src/stash.c b/src/stash.c new file mode 100644 index 000000000..faf276d99 --- /dev/null +++ b/src/stash.c @@ -0,0 +1,175 @@ +/* + * Copyright 2010-2022 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 "object.h" +#include "error.h" +#include "types.h" +#include "utils.h" +#include "oid.h" + +PyTypeObject StashType; + + +PyDoc_STRVAR(Stash_commit_id__doc__, "The commit id of the stashed state."); + +PyObject * +Stash_commit_id__get__(Stash *self) +{ + Py_INCREF(self->commit_id); + return self->commit_id; +} + + +PyDoc_STRVAR(Stash_message__doc__, "Stash message."); + +PyObject * +Stash_message__get__(Stash *self) +{ + return to_unicode(self->message, "utf-8", "strict"); +} + + +PyDoc_STRVAR(Stash_raw_message__doc__, "Stash message (bytes)."); + +PyObject * +Stash_raw_message__get__(Stash *self) +{ + return PyBytes_FromString(self->message); +} + + +static void +Stash_dealloc(Stash *self) +{ + Py_CLEAR(self->commit_id); + free(self->message); + PyObject_Del(self); +} + + +static PyObject * +Stash_repr(Stash *self) +{ + return PyUnicode_FromFormat("", self->commit_id); +} + + +PyObject * +Stash_richcompare(PyObject *o1, PyObject *o2, int op) +{ + int eq = 0; + Stash *s1, *s2; + git_oid *oid1, *oid2; + + /* We only support comparing to another stash */ + if (!PyObject_TypeCheck(o2, &StashType)) { + Py_INCREF(Py_NotImplemented); + return Py_NotImplemented; + } + + s1 = (Stash *)o1; + s2 = (Stash *)o2; + + oid1 = &((Oid *)s1->commit_id)->oid; + oid2 = &((Oid *)s2->commit_id)->oid; + + eq = git_oid_equal(oid1, oid2) && + (0 == strcmp(s1->message, s2->message)); + + switch (op) { + case Py_EQ: + if (eq) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + case Py_NE: + if (eq) { + Py_RETURN_FALSE; + } else { + Py_RETURN_TRUE; + } + default: + Py_INCREF(Py_NotImplemented); + return Py_NotImplemented; + } +} + + +PyGetSetDef Stash_getseters[] = { + GETTER(Stash, commit_id), + GETTER(Stash, message), + GETTER(Stash, raw_message), + {NULL} +}; + + +PyDoc_STRVAR(Stash__doc__, "Stashed state."); + +PyTypeObject StashType = { + PyVarObject_HEAD_INIT(NULL, 0) + "_pygit2.Stash", /* tp_name */ + sizeof(Stash), /* tp_basicsize */ + 0, /* tp_itemsize */ + (destructor)Stash_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + (reprfunc)Stash_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, /* tp_flags */ + Stash__doc__, /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + Stash_richcompare, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + 0, /* tp_methods */ + 0, /* tp_members */ + Stash_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/types.h b/src/types.h index d3d9f9610..9b6edf5ac 100644 --- a/src/types.h +++ b/src/types.h @@ -256,4 +256,10 @@ typedef struct { git_mailmap *mailmap; } Mailmap; +typedef struct { + PyObject_HEAD + PyObject *commit_id; + char *message; +} Stash; + #endif diff --git a/test/test_repository.py b/test/test_repository.py index 300f1c587..5360f18a7 100644 --- a/test/test_repository.py +++ b/test/test_repository.py @@ -251,16 +251,37 @@ def test_reset_mixed(testrepo): assert "bonjour le monde\n" in diff.patch def test_stash(testrepo): + stash_hash = "6aab5192f88018cb98a7ede99c242f43add5a2fd" + stash_message = "custom stash message" + sig = pygit2.Signature( + name='Stasher', + email='stasher@example.com', + time=1641000000, # fixed time so the oid is stable + offset=0) + + # make sure we're starting with no stashes + assert [] == testrepo.listall_stashes() + # some changes to working dir with open(os.path.join(testrepo.workdir, 'hello.txt'), 'w') as f: f.write('new content') - sig = pygit2.Signature('Stasher', 'stasher@example.com') - testrepo.stash(sig, include_untracked=True) + testrepo.stash(sig, include_untracked=True, message=stash_message) assert 'hello.txt' not in testrepo.status() + + repo_stashes = testrepo.listall_stashes() + assert 1 == len(repo_stashes) + assert repr(repo_stashes[0]) == f"" + assert repo_stashes[0].commit_id.hex == stash_hash + assert repo_stashes[0].message == "On master: " + stash_message + testrepo.stash_apply() assert 'hello.txt' in testrepo.status() + assert repo_stashes == testrepo.listall_stashes() # still the same stashes + testrepo.stash_drop() + assert [] == testrepo.listall_stashes() + with pytest.raises(KeyError): testrepo.stash_pop() def test_revert(testrepo):