From 74b1628e5170a0406db62d726b61933de4825b56 Mon Sep 17 00:00:00 2001 From: Fraser Tweedale Date: Thu, 29 Aug 2013 23:22:36 +1000 Subject: [PATCH 1/2] test utils: implement a repo context manager The current hierarchy of test cases that create and blow away test repositories in the ``setUp`` and ``tearDown`` methods is inadequate for testing scenarios where multiple repositories must be edited, e.g. testing push. Extract the temporary repository behaviour into a context manager class ``TemporaryRepository`` which can be used via the ``with`` keyword. The context manager's ``__enter__` method returns creates the specified repository in a temporary directory and returns its path. The temporary directory is removed in ``__exit__``. Update the existing test case base classes to use the context manager and invoke ``__enter__`` and ``__exit__`` in the ``setUp`` and ``tearDown`` methods respectively. Finally, make some small tweaks to a handful of tests to get them working under this new system. --- test/test_blob.py | 5 ++-- test/test_repository.py | 6 ++-- test/utils.py | 65 +++++++++++++++++++++++------------------ 3 files changed, 42 insertions(+), 34 deletions(-) diff --git a/test/test_blob.py b/test/test_blob.py index 8105455f7..2f5259649 100644 --- a/test/test_blob.py +++ b/test/test_blob.py @@ -93,13 +93,12 @@ def test_create_blob_fromworkdir(self): def test_create_blob_outside_workdir(self): - path = join(dirname(__file__), 'data', self.repo_dir + '.tar') + path = __file__ self.assertRaises(KeyError, self.repo.create_blob_fromworkdir, path) def test_create_blob_fromdisk(self): - path = join(dirname(__file__), 'data', self.repo_dir + '.tar') - blob_oid = self.repo.create_blob_fromdisk(path) + blob_oid = self.repo.create_blob_fromdisk(__file__) blob = self.repo[blob_oid] self.assertTrue(isinstance(blob, pygit2.Blob)) diff --git a/test/test_repository.py b/test/test_repository.py index 87c9fc704..ab0b22c89 100644 --- a/test/test_repository.py +++ b/test/test_repository.py @@ -143,7 +143,7 @@ def test_lookup_commit_prefix(self): def test_get_path(self): directory = realpath(self.repo.path) - expected = realpath(join(self._temp_dir, 'testrepo.git')) + expected = realpath(self.repo_path) self.assertEqual(directory, expected) def test_get_workdir(self): @@ -179,12 +179,12 @@ def test_is_bare(self): def test_get_path(self): directory = realpath(self.repo.path) - expected = realpath(join(self._temp_dir, 'testrepo', '.git')) + expected = realpath(join(self.repo_path, '.git')) self.assertEqual(directory, expected) def test_get_workdir(self): directory = realpath(self.repo.workdir) - expected = realpath(join(self._temp_dir, 'testrepo')) + expected = realpath(self.repo_path) self.assertEqual(directory, expected) def test_checkout_ref(self): diff --git a/test/utils.py b/test/utils.py index 10a4e1719..fb7ca58f9 100644 --- a/test/utils.py +++ b/test/utils.py @@ -65,6 +65,27 @@ def rmtree(path): shutil.rmtree(path, onerror=onerror) +class TemporaryRepository(object): + def __init__(self, repo_spec): + self.repo_spec = repo_spec + + def __enter__(self): + container, name = self.repo_spec + repo_path = os.path.join(os.path.dirname(__file__), 'data', name) + self.temp_dir = tempfile.mkdtemp() + temp_repo_path = os.path.join(self.temp_dir, name) + if container == 'tar': + tar = tarfile.open('.'.join((repo_path, 'tar'))) + tar.extractall(self.temp_dir) + tar.close() + else: + shutil.copytree(repo_path, temp_repo_path) + return temp_repo_path + + def __exit__(self, exc_type, exc_value, traceback): + rmtree(self.temp_dir) + + class NoRepoTestCase(unittest.TestCase): def setUp(self): @@ -103,45 +124,33 @@ def assertEqualSignature(self, a, b): self.assertEqual(a.offset, b.offset) -class BareRepoTestCase(NoRepoTestCase): - - repo_dir = 'testrepo.git' - +class AutoRepoTestCase(NoRepoTestCase): def setUp(self): - super(BareRepoTestCase, self).setUp() - - repo_dir = self.repo_dir - repo_path = os.path.join(os.path.dirname(__file__), 'data', repo_dir) - temp_repo_path = os.path.join(self._temp_dir, repo_dir) - - shutil.copytree(repo_path, temp_repo_path) - - self.repo = pygit2.Repository(temp_repo_path) + super(AutoRepoTestCase, self).setUp() + self.repo_ctxtmgr = TemporaryRepository(self.repo_spec) + self.repo_path = self.repo_ctxtmgr.__enter__() + self.repo = pygit2.Repository(self.repo_path) + def tearDown(self): + self.repo_ctxtmgr.__exit__(None, None, None) + super(AutoRepoTestCase, self).tearDown() -class RepoTestCase(NoRepoTestCase): - repo_dir = 'testrepo' +class BareRepoTestCase(AutoRepoTestCase): - def setUp(self): - super(RepoTestCase, self).setUp() + repo_spec = 'git', 'testrepo.git' - repo_dir = self.repo_dir - repo_path = os.path.join(os.path.dirname(__file__), 'data', repo_dir) - temp_repo_path = os.path.join(self._temp_dir, repo_dir, '.git') - tar = tarfile.open(repo_path + '.tar') - tar.extractall(self._temp_dir) - tar.close() +class RepoTestCase(AutoRepoTestCase): - self.repo = pygit2.Repository(temp_repo_path) + repo_spec = 'tar', 'testrepo' -class DirtyRepoTestCase(RepoTestCase): +class DirtyRepoTestCase(AutoRepoTestCase): - repo_dir = 'dirtyrepo' + repo_spec = 'tar', 'dirtyrepo' -class EmptyRepoTestCase(RepoTestCase): +class EmptyRepoTestCase(AutoRepoTestCase): - repo_dir = 'emptyrepo' + repo_spec = 'tar', 'emptyrepo' From 134d87ab2a06459cb6c273cdbc82cc5a0aa5aa3c Mon Sep 17 00:00:00 2001 From: Fraser Tweedale Date: Sun, 25 Aug 2013 19:51:20 +1000 Subject: [PATCH 2/2] implement push support Implement push support via Remote.push which is called with a single refspec and raises GitError (with an appropriate message where possible) if the push fails. Note that local push to non-bare repository is currently not supported by libgit2. --- docs/remotes.rst | 1 + src/remote.c | 66 +++++++++++++++++++++++++++++++++++++++++++++ test/test_remote.py | 40 +++++++++++++++++++++++++++ 3 files changed, 107 insertions(+) diff --git a/docs/remotes.rst b/docs/remotes.rst index 91a69d055..4b9692fcd 100644 --- a/docs/remotes.rst +++ b/docs/remotes.rst @@ -15,4 +15,5 @@ The Remote type .. autoattribute:: pygit2.Remote.refspec_count .. automethod:: pygit2.Remote.get_refspec .. automethod:: pygit2.Remote.fetch +.. automethod:: pygit2.Remote.push .. automethod:: pygit2.Remote.save diff --git a/src/remote.c b/src/remote.c index 9faf11435..e482d8b1c 100644 --- a/src/remote.c +++ b/src/remote.c @@ -218,10 +218,76 @@ Remote_save(Remote *self, PyObject *args) } +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); +} + + PyMethodDef Remote_methods[] = { METHOD(Remote, fetch, METH_NOARGS), METHOD(Remote, save, METH_NOARGS), METHOD(Remote, get_refspec, METH_O), + METHOD(Remote, push, METH_VARARGS), {NULL} }; diff --git a/test/test_remote.py b/test/test_remote.py index a4a125966..b6a9edc5d 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -124,5 +124,45 @@ def test_fetch(self): self.assertEqual(stats['received_objects'], REMOTE_REPO_OBJECTS) +class PushTestCase(unittest.TestCase): + def setUp(self): + self.origin_ctxtmgr = utils.TemporaryRepository(('git', 'testrepo.git')) + self.clone_ctxtmgr = utils.TemporaryRepository(('git', 'testrepo.git')) + self.origin = pygit2.Repository(self.origin_ctxtmgr.__enter__()) + self.clone = pygit2.Repository(self.clone_ctxtmgr.__enter__()) + self.remote = self.clone.create_remote('origin', self.origin.path) + + def tearDown(self): + self.origin_ctxtmgr.__exit__(None, None, None) + self.clone_ctxtmgr.__exit__(None, None, None) + + def test_push_fast_forward_commits_to_remote_succeeds(self): + tip = self.clone[self.clone.head.target] + oid = self.clone.create_commit( + 'refs/heads/master', tip.author, tip.author, 'empty commit', + tip.tree.oid, [tip.oid] + ) + self.remote.push('refs/heads/master') + self.assertEqual(self.origin[self.origin.head.target].oid, oid) + + def test_push_when_up_to_date_succeeds(self): + self.remote.push('refs/heads/master') + origin_tip = self.origin[self.origin.head.target].oid + clone_tip = self.clone[self.clone.head.target].oid + self.assertEqual(origin_tip, clone_tip) + + def test_push_non_fast_forward_commits_to_remote_fails(self): + tip = self.origin[self.origin.head.target] + oid = self.origin.create_commit( + 'refs/heads/master', tip.author, tip.author, 'some commit', + tip.tree.oid, [tip.oid] + ) + tip = self.clone[self.clone.head.target] + oid = self.clone.create_commit( + 'refs/heads/master', tip.author, tip.author, 'other commit', + tip.tree.oid, [tip.oid] + ) + self.assertRaises(pygit2.GitError, self.remote.push, 'refs/heads/master') + if __name__ == '__main__': unittest.main()