From a294655aa570e839bd686fde590a035f3c5a2a6d Mon Sep 17 00:00:00 2001 From: Nick Hynes Date: Mon, 27 Mar 2017 16:18:13 -0400 Subject: [PATCH 1/8] Add declarations for git-stash --- pygit2/decl.h | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/pygit2.c | 8 ++++++++ 2 files changed, 58 insertions(+) diff --git a/pygit2/decl.h b/pygit2/decl.h index 58676ac18..f3aec2916 100644 --- a/pygit2/decl.h +++ b/pygit2/decl.h @@ -803,6 +803,56 @@ int git_merge_trees(git_index **out, git_repository *repo, const git_tree *ances int git_merge_file_from_index(git_merge_file_result *out, git_repository *repo, const git_index_entry *ancestor, const git_index_entry *ours, const git_index_entry *theirs, const git_merge_file_options *opts); void git_merge_file_result_free(git_merge_file_result *result); +/* + * git_stash + */ + +typedef int (*git_stash_cb)( + size_t index, const char* message, const git_oid *stash_id, void *payload); + +typedef enum { + GIT_STASH_APPLY_PROGRESS_NONE = 0, + GIT_STASH_APPLY_PROGRESS_LOADING_STASH = 1, + GIT_STASH_APPLY_PROGRESS_ANALYZE_INDEX = 2, + GIT_STASH_APPLY_PROGRESS_ANALYZE_MODIFIED = 3, + GIT_STASH_APPLY_PROGRESS_ANALYZE_UNTRACKED = 4, + GIT_STASH_APPLY_PROGRESS_CHECKOUT_UNTRACKED = 5, + GIT_STASH_APPLY_PROGRESS_CHECKOUT_MODIFIED = 6, + GIT_STASH_APPLY_PROGRESS_DONE = 7, +} git_stash_apply_progress_t; + +typedef int (*git_stash_apply_progress_cb)( + git_stash_apply_progress_t progress, void *payload); + +typedef enum { + GIT_STASH_DEFAULT = 0, + GIT_STASH_KEEP_INDEX = 1, + GIT_STASH_INCLUDE_UNTRACKED = 2, + GIT_STASH_INCLUDE_IGNORED = 4, +} git_stash_flags; + +typedef enum { + GIT_STASH_APPLY_DEFAULT = 0, + GIT_STASH_APPLY_REINSTATE_INDEX = 1, +} git_stash_apply_flags; + +typedef struct git_stash_apply_options { + unsigned int version; + git_stash_apply_flags flags; + git_checkout_options checkout_options; + git_stash_apply_progress_cb progress_cb; + void *progress_payload; +} git_stash_apply_options; + +#define GIT_STASH_APPLY_OPTIONS_VERSION ... + +int git_stash_save(git_oid *out, git_repository *repo, const git_signature *stasher, const char *message, uint32_t flags); +int git_stash_apply_init_options(git_stash_apply_options *opts, unsigned int version); +int git_stash_apply(git_repository *repo, size_t index, const git_stash_apply_options *options); +int git_stash_foreach(git_repository *repo, git_stash_cb callback, void *payload); +int git_stash_drop(git_repository *repo, size_t index); +int git_stash_pop(git_repository *repo, size_t index, const git_stash_apply_options *options); + /* * Describe */ diff --git a/src/pygit2.c b/src/pygit2.c index f8fbe9a4a..eca5aa279 100644 --- a/src/pygit2.c +++ b/src/pygit2.c @@ -428,6 +428,14 @@ moduleinit(PyObject* m) ADD_CONSTANT_INT(m, GIT_DESCRIBE_TAGS); ADD_CONSTANT_INT(m, GIT_DESCRIBE_ALL); + /* Stash */ + ADD_CONSTANT_INT(m, GIT_STASH_DEFAULT); + ADD_CONSTANT_INT(m, GIT_STASH_KEEP_INDEX); + ADD_CONSTANT_INT(m, GIT_STASH_INCLUDE_UNTRACKED); + ADD_CONSTANT_INT(m, GIT_STASH_INCLUDE_IGNORED); + ADD_CONSTANT_INT(m, GIT_STASH_APPLY_DEFAULT); + ADD_CONSTANT_INT(m, GIT_STASH_APPLY_REINSTATE_INDEX); + /* Global initialization of libgit2 */ git_libgit2_init(); From cbbf9f1f8763c2fb1a21e68ce41ce75dba8ff059 Mon Sep 17 00:00:00 2001 From: Nick Hynes Date: Mon, 27 Mar 2017 16:18:21 -0400 Subject: [PATCH 2/8] Fix Signature._pointer --- src/signature.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/signature.c b/src/signature.c index 9990b1db9..536a9fc3f 100644 --- a/src/signature.c +++ b/src/signature.c @@ -96,10 +96,10 @@ Signature_dealloc(Signature *self) PyDoc_STRVAR(Signature__pointer__doc__, "Get the signature's pointer. For internal use only."); PyObject * -Signature__pointer__get__(Repository *self) +Signature__pointer__get__(Signature *self) { /* Bytes means a raw buffer */ - return PyBytes_FromStringAndSize((char *) &self->repo, sizeof(git_repository *)); + return PyBytes_FromStringAndSize((char *) &self->signature, sizeof(git_signature *)); } PyDoc_STRVAR(Signature__encoding__doc__, "Encoding."); From 1a842ff8bdf09125a456fa886e4b9e5c944364c2 Mon Sep 17 00:00:00 2001 From: Nick Hynes Date: Mon, 27 Mar 2017 16:20:33 -0400 Subject: [PATCH 3/8] Wrap git_stash_save --- pygit2/repository.py | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/pygit2/repository.py b/pygit2/repository.py index cd0316d6f..17eea0c8f 100644 --- a/pygit2/repository.py +++ b/pygit2/repository.py @@ -44,7 +44,7 @@ from _pygit2 import Oid, GIT_OID_HEXSZ, GIT_OID_MINPREFIXLEN from _pygit2 import GIT_CHECKOUT_SAFE, GIT_CHECKOUT_RECREATE_MISSING, GIT_DIFF_NORMAL from _pygit2 import GIT_FILEMODE_LINK -from _pygit2 import Reference, Tree, Commit, Blob +from _pygit2 import Reference, Tree, Commit, Blob, Signature from .config import Config from .errors import check_error @@ -735,6 +735,47 @@ def describe(self, committish=None, max_candidates_tags=None, C.git_buf_free(buf) finally: C.git_describe_result_free(result[0]) + # + # Stash + # + def stash(self, stasher, message=None, keep_index=False, + include_untracked=False, include_ignored=False): + """Save changes to the working directory to the stash. + + :param Signature stasher: The identity of the person doing the stashing. + :param str message: An optional description of stashed state. + :param bool keep_index: Leave changes already added to the index + in the working directory. + :param bool include_untracked: Also stash untracked files. + :param bool include_ignored: Also stash ignored files. + + :returns: The Oid of the stash merge commit. + :rtype: Oid + + Example:: + + >>> repo = pygit2.Repsitory('.') + >>> repo.stash(repo.default_signature(), 'WIP: stashing') + """ + + if message is not None: + stash_msg = ffi.new('char[]', to_bytes(message)) if message else ffi.NULL + else: + stash_msg = ffi.NULL + + flags = 0 + flags |= keep_index * C.GIT_STASH_KEEP_INDEX + flags |= include_untracked * C.GIT_STASH_INCLUDE_UNTRACKED + flags |= include_ignored * C.GIT_STASH_INCLUDE_IGNORED + + stasher_cptr = ffi.new('git_signature **') + ffi.buffer(stasher_cptr)[:] = stasher._pointer[:] + + coid = ffi.new('git_oid *') + err = C.git_stash_save(coid, self._repo, stasher_cptr[0], stash_msg, flags) + check_error(err) + + return Oid(raw=bytes(ffi.buffer(coid)[:])) # # Utility for writing a tree into an archive From da233b16c5b4cb5fdeef476725bee8fa731839f2 Mon Sep 17 00:00:00 2001 From: Nick Hynes Date: Mon, 27 Mar 2017 16:27:37 -0400 Subject: [PATCH 4/8] Wrap git_stash_apply --- pygit2/repository.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/pygit2/repository.py b/pygit2/repository.py index 17eea0c8f..6344fb93b 100644 --- a/pygit2/repository.py +++ b/pygit2/repository.py @@ -777,6 +777,34 @@ def stash(self, stasher, message=None, keep_index=False, return Oid(raw=bytes(ffi.buffer(coid)[:])) + def stash_apply(self, index=0, reinstate_index=False, **kwargs): + """Apply a stashed state in the stash list to the working directory. + + :param int index: The position within the stash list of the stash to apply. + 0 is the most recent stash. + :param bool reinstate_index: Try to reinstate stashed changes to the index. + + The checkout options may be customized using the same arguments taken by + Repository.checkout(). + + Example:: + + >>> repo = pygit2.Repsitory('.') + >>> repo.stash(repo.default_signature(), 'WIP: stashing') + >>> repo.stash_apply(strategy=GIT_CHECKOUT_ALLOW_CONFLICTS) + """ + + stash_opts = ffi.new('git_stash_apply_options *') + check_error(C.git_stash_apply_init_options(stash_opts, 1)) + + flags = reinstate_index * C.GIT_STASH_APPLY_REINSTATE_INDEX + stash_opts.flags = flags + + copts, refs = Repository._checkout_args_to_options(**kwargs) + stash_opts.checkout_options = copts[0] + + check_error(C.git_stash_apply(self._repo, index, stash_opts)) + # # Utility for writing a tree into an archive # From b31ac5021003cb678f89f5fc4a5c6594b12864df Mon Sep 17 00:00:00 2001 From: Nick Hynes Date: Mon, 27 Mar 2017 16:33:32 -0400 Subject: [PATCH 5/8] Wrap stash_drop --- pygit2/repository.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pygit2/repository.py b/pygit2/repository.py index 6344fb93b..d00c60e45 100644 --- a/pygit2/repository.py +++ b/pygit2/repository.py @@ -805,6 +805,14 @@ def stash_apply(self, index=0, reinstate_index=False, **kwargs): check_error(C.git_stash_apply(self._repo, index, stash_opts)) + def stash_drop(self, index=0): + """Remove a stashed state from the stash list. + + :param int index: The position within the stash list of the stash to remove. + 0 is the most recent stash. + """ + check_error(C.git_stash_drop(self._repo, index)) + # # Utility for writing a tree into an archive # From 9be907983f44f975f32edea9356b4fb083516896 Mon Sep 17 00:00:00 2001 From: Nick Hynes Date: Mon, 27 Mar 2017 16:39:58 -0400 Subject: [PATCH 6/8] Wrap stash_pop --- pygit2/repository.py | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/pygit2/repository.py b/pygit2/repository.py index d00c60e45..0de245cd1 100644 --- a/pygit2/repository.py +++ b/pygit2/repository.py @@ -777,7 +777,20 @@ def stash(self, stasher, message=None, keep_index=False, return Oid(raw=bytes(ffi.buffer(coid)[:])) - def stash_apply(self, index=0, reinstate_index=False, **kwargs): + @staticmethod + def _stash_args_to_options(reinstate_index=False, **kwargs): + stash_opts = ffi.new('git_stash_apply_options *') + check_error(C.git_stash_apply_init_options(stash_opts, 1)) + + flags = reinstate_index * C.GIT_STASH_APPLY_REINSTATE_INDEX + stash_opts.flags = flags + + copts, refs = Repository._checkout_args_to_options(**kwargs) + stash_opts.checkout_options = copts[0] + + return stash_opts + + def stash_apply(self, index=0, **kwargs): """Apply a stashed state in the stash list to the working directory. :param int index: The position within the stash list of the stash to apply. @@ -793,16 +806,7 @@ def stash_apply(self, index=0, reinstate_index=False, **kwargs): >>> repo.stash(repo.default_signature(), 'WIP: stashing') >>> repo.stash_apply(strategy=GIT_CHECKOUT_ALLOW_CONFLICTS) """ - - stash_opts = ffi.new('git_stash_apply_options *') - check_error(C.git_stash_apply_init_options(stash_opts, 1)) - - flags = reinstate_index * C.GIT_STASH_APPLY_REINSTATE_INDEX - stash_opts.flags = flags - - copts, refs = Repository._checkout_args_to_options(**kwargs) - stash_opts.checkout_options = copts[0] - + stash_opts = Repository._stash_args_to_options(**kwargs) check_error(C.git_stash_apply(self._repo, index, stash_opts)) def stash_drop(self, index=0): @@ -813,6 +817,15 @@ def stash_drop(self, index=0): """ check_error(C.git_stash_drop(self._repo, index)) + + def stash_pop(self, index=0, **kwargs): + """Apply a stashed state and remove it from the stash list. + + For arguments, see Repository.stash_apply(). + """ + stash_opts = Repository._stash_args_to_options(**kwargs) + check_error(C.git_stash_pop(self._repo, index, stash_opts)) + # # Utility for writing a tree into an archive # From 630d905e73f31ebb772df01eadbb3db4429725ed Mon Sep 17 00:00:00 2001 From: Nick Hynes Date: Mon, 27 Mar 2017 17:10:45 -0400 Subject: [PATCH 7/8] Add stash tests --- test/test_repository.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/test_repository.py b/test/test_repository.py index 0095fd9da..0969fc00f 100644 --- a/test/test_repository.py +++ b/test/test_repository.py @@ -422,6 +422,19 @@ def test_reset_mixed(self): self.assertTrue("hola mundo\n" in diff.patch) self.assertTrue("bonjour le monde\n" in diff.patch) + def test_stash(self): + # some changes to working dir + with open(os.path.join(self.repo.workdir, 'hello.txt'), 'w') as f: + f.write('new content') + + sig = pygit2.Signature('Stasher', 'stasher@example.com') + self.repo.stash(sig, include_untracked=True) + self.assertFalse('hello.txt' in self.repo.status()) + self.repo.stash_apply() + self.assertTrue('hello.txt' in self.repo.status()) + self.repo.stash_drop() + self.assertRaises(KeyError, self.repo.stash_pop) + class RepositorySignatureTest(utils.RepoTestCase): def test_default_signature(self): From 453fd8a9a3dbd5e9eb7f7bcbaba5ab2d81969ea7 Mon Sep 17 00:00:00 2001 From: Nick Hynes Date: Mon, 27 Mar 2017 17:10:51 -0400 Subject: [PATCH 8/8] Add docs --- docs/working-copy.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/working-copy.rst b/docs/working-copy.rst index 5e93be5d5..aff1a70ca 100644 --- a/docs/working-copy.rst +++ b/docs/working-copy.rst @@ -71,3 +71,11 @@ Lower level API: .. automethod:: pygit2.Repository.checkout_head .. automethod:: pygit2.Repository.checkout_tree .. automethod:: pygit2.Repository.checkout_index + +Stash +==================== + +.. automethod:: pygit2.Repository.stash +.. automethod:: pygit2.Repository.stash_apply +.. automethod:: pygit2.Repository.stash_drop +.. automethod:: pygit2.Repository.stash_pop