From 3dd998c061f769081e385ecc4065e05b1d362cd3 Mon Sep 17 00:00:00 2001 From: Bernardo Heynemann Date: Wed, 15 May 2013 18:15:00 -0300 Subject: [PATCH 1/6] Supporting clone in pygit2 --- pygit2/__init__.py | 19 ++++++++++++++ src/pygit2.c | 56 +++++++++++++++++++++++++++++++++++++++++ test/test_repository.py | 42 ++++++++++++++++++++++++++++++- 3 files changed, 116 insertions(+), 1 deletion(-) diff --git a/pygit2/__init__.py b/pygit2/__init__.py index aeb7fe5..44b31e7 100644 --- a/pygit2/__init__.py +++ b/pygit2/__init__.py @@ -47,3 +47,22 @@ def init_repository(path, bare=False): """ _pygit2.init_repository(path, bare) return Repository(path) + + +def clone_repository( + url, path, bare=False, remote_name="origin", push_url=None, fetch_spec=None, + push_spec=None, checkout_branch=None): + """ + Clones a new Git repository from *url* in the given *path*. + + Parameters: + * If 'bare' is True, then a bare git repository will be created. + * 'remote_name' is the name given to the "origin" remote. The default is "origin". + * 'push_url' is a URL to be used for pushing. None means use the fetch url. + * 'fetch_spec' is the fetch specification to be used for fetching. None results in the same behavior as GIT_REMOTE_DEFAULT_FETCH. + * 'push_spec' is the fetch specification to be used for pushing. None means use the same spec as for 'fetch_spec'. + * 'checkout_branch' gives the name of the branch to checkout. None means use the remote's HEAD + """ + + _pygit2.clone_repository(url, path, bare, remote_name, push_url, fetch_spec, push_spec, checkout_branch) + return Repository(path) diff --git a/src/pygit2.c b/src/pygit2.c index c68e530..ed1ad9d 100644 --- a/src/pygit2.c +++ b/src/pygit2.c @@ -97,6 +97,61 @@ init_repository(PyObject *self, PyObject *args) { Py_RETURN_NONE; }; +PyDoc_STRVAR(clone_repository__doc__, + "clone_repository(url, path, bare, remote_name, push_url, fetch_spec, push_spec, 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" + "push_url\n" + " URL to be used for pushing.\n" + "fetch_spec\n" + " The fetch specification to be used for fetching. None results in the same behavior as GIT_REMOTE_DEFAULT_FETCH.\n" + "push_spec\n" + " The fetch specification to be used for pushing. None means use the same spec as for 'fetch_spec'\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; + const char *remote_name, *push_url, *fetch_spec, *push_spec, *checkout_branch; + int err; + + if (!PyArg_ParseTuple(args, "zzIzzzzz", &url, &path, &bare, &remote_name, &push_url, &fetch_spec, &push_spec, &checkout_branch)) + return NULL; + + git_clone_options opts = { + .version=1, + .bare=bare, + .remote_name=remote_name, + .pushurl=push_url, + .fetch_spec=fetch_spec, + .push_spec=push_spec, + .checkout_branch=checkout_branch + }; + + err = git_clone(&repo, url, path, &opts); + if (err < 0) + return Error_set_str(err, path); + + git_repository_free(repo); + Py_RETURN_NONE; +}; + PyDoc_STRVAR(discover_repository__doc__, "discover_repository(path[, across_fs[, ceiling_dirs]]) -> str\n" @@ -172,6 +227,7 @@ 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/test/test_repository.py b/test/test_repository.py index b11c804..f1771e9 100644 --- a/test/test_repository.py +++ b/test/test_repository.py @@ -40,7 +40,7 @@ from os.path import join, realpath # Import from pygit2 from pygit2 import GIT_OBJ_ANY, GIT_OBJ_BLOB, GIT_OBJ_COMMIT -from pygit2 import init_repository, discover_repository, Reference, hashfile +from pygit2 import init_repository, clone_repository, discover_repository, Reference, hashfile from pygit2 import Oid import pygit2 from . import utils @@ -291,6 +291,46 @@ class EmptyRepositoryTest(utils.EmptyRepoTestCase): self.assertTrue(self.repo.head_is_orphaned) self.assertFalse(self.repo.head_is_detached) +class CloneRepositoryTest(utils.NoRepoTestCase): + def test_clone_repository(self): + repo = clone_repository("./test/data/testrepo.git/", self._temp_dir) + self.assertFalse(repo.is_empty) + self.assertFalse(repo.is_bare) + + def test_clone_bare_repository(self): + repo = clone_repository("./test/data/testrepo.git/", self._temp_dir, bare=True) + self.assertFalse(repo.is_empty) + self.assertTrue(repo.is_bare) + + def test_clone_remote_name(self): + repo = clone_repository("./test/data/testrepo.git/", self._temp_dir, remote_name="custom_remote") + self.assertFalse(repo.is_empty) + self.assertEqual(repo.remotes[0].name, "custom_remote") + + def test_clone_push_url(self): + repo = clone_repository("./test/data/testrepo.git/", self._temp_dir, push_url="custom_push_url") + self.assertFalse(repo.is_empty) + # not sure how to test this... couldn't find pushurl + # self.assertEqual(repo.remotes[0].pushurl, "custom_push_url") + + def test_clone_fetch_spec(self): + repo = clone_repository("./test/data/testrepo.git/", self._temp_dir, fetch_spec="refs/heads/test") + self.assertFalse(repo.is_empty) + # not sure how to test this either... fetchspec seems to be going through, but repo is not getting it. + # self.assertEqual(repo.remotes[0].fetchspec, "refs/heads/test") + + def test_clone_push_spec(self): + repo = clone_repository("./test/data/testrepo.git/", self._temp_dir, push_spec="refs/heads/test") + self.assertFalse(repo.is_empty) + # not sure how to test this either... couldn't find pushspec + # self.assertEqual(repo.remotes[0].fetchspec, "refs/heads/test") + + def test_clone_checkout_branch(self): + repo = clone_repository("./test/data/testrepo.git/", self._temp_dir, checkout_branch="test") + self.assertFalse(repo.is_empty) + # not sure how to test this either... couldn't find current branch + # self.assertEqual(repo.remotes[0].current_branch, "test") + if __name__ == '__main__': unittest.main() From a831b8b6f6c588a0a7e172e681523aad3f9a6cf8 Mon Sep 17 00:00:00 2001 From: Bernardo Heynemann Date: Thu, 16 May 2013 10:04:17 -0300 Subject: [PATCH 2/6] Since I was PEP8-ing my tests, I decided to do it for the whole file. --- test/test_repository.py | 64 ++++++++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 17 deletions(-) diff --git a/test/test_repository.py b/test/test_repository.py index f1771e9..d6e762e 100644 --- a/test/test_repository.py +++ b/test/test_repository.py @@ -40,7 +40,10 @@ from os.path import join, realpath # Import from pygit2 from pygit2 import GIT_OBJ_ANY, GIT_OBJ_BLOB, GIT_OBJ_COMMIT -from pygit2 import init_repository, clone_repository, discover_repository, Reference, hashfile +from pygit2 import ( + init_repository, clone_repository, discover_repository, + Reference, hashfile +) from pygit2 import Oid import pygit2 from . import utils @@ -101,7 +104,7 @@ class RepositoryTest(utils.BareRepoTestCase): self.assertFalse('a' * 20 in self.repo) def test_iterable(self): - l = [ obj for obj in self.repo ] + l = [obj for obj in self.repo] oid = Oid(hex=BLOB_HEX) self.assertTrue(oid in l) @@ -135,9 +138,10 @@ class RepositoryTest(utils.BareRepoTestCase): commit = self.repo[commit_sha_prefix] self.assertEqual(commit_sha, commit.hex) self.assertEqual(GIT_OBJ_COMMIT, commit.type) - self.assertEqual(('Second test data commit.\n\n' - 'This commit has some additional text.\n'), - commit.message) + self.assertEqual( + ('Second test data commit.\n\n' + 'This commit has some additional text.\n'), + commit.message) self.assertRaises(ValueError, self.repo.__getitem__, too_short_prefix) def test_get_path(self): @@ -208,7 +212,7 @@ class RepositoryTest_II(utils.RepoTestCase): def test_checkout_index(self): # some changes to working dir with open(os.path.join(self.repo.workdir, 'hello.txt'), 'w') as f: - f.write('new content') + f.write('new content') # checkout index self.assertTrue('hello.txt' in self.repo.status()) @@ -218,7 +222,7 @@ class RepositoryTest_II(utils.RepoTestCase): def test_checkout_head(self): # some changes to the index with open(os.path.join(self.repo.workdir, 'bye.txt'), 'w') as f: - f.write('new content') + f.write('new content') self.repo.index.add('bye.txt') # checkout from index should not change anything @@ -272,6 +276,7 @@ class InitRepositoryTest(utils.NoRepoTestCase): repo = init_repository(self._temp_dir, bare=True) self.assertTrue(repo.is_bare) + class DiscoverRepositoryTest(utils.NoRepoTestCase): def test_discover_repo(self): repo = init_repository(self._temp_dir, False) @@ -279,6 +284,7 @@ class DiscoverRepositoryTest(utils.NoRepoTestCase): os.makedirs(subdir) self.assertEqual(repo.path, discover_repository(subdir)) + class EmptyRepositoryTest(utils.EmptyRepoTestCase): def test_is_empty(self): @@ -291,44 +297,68 @@ class EmptyRepositoryTest(utils.EmptyRepoTestCase): self.assertTrue(self.repo.head_is_orphaned) self.assertFalse(self.repo.head_is_detached) + class CloneRepositoryTest(utils.NoRepoTestCase): def test_clone_repository(self): - repo = clone_repository("./test/data/testrepo.git/", self._temp_dir) + repo_path = "./test/data/testrepo.git/" + repo = clone_repository(repo_path, self._temp_dir) self.assertFalse(repo.is_empty) self.assertFalse(repo.is_bare) def test_clone_bare_repository(self): - repo = clone_repository("./test/data/testrepo.git/", self._temp_dir, bare=True) + repo_path = "./test/data/testrepo.git/" + repo = clone_repository(repo_path, self._temp_dir, bare=True) self.assertFalse(repo.is_empty) self.assertTrue(repo.is_bare) def test_clone_remote_name(self): - repo = clone_repository("./test/data/testrepo.git/", self._temp_dir, remote_name="custom_remote") + repo_path = "./test/data/testrepo.git/" + repo = clone_repository( + repo_path, self._temp_dir, remote_name="custom_remote" + ) self.assertFalse(repo.is_empty) self.assertEqual(repo.remotes[0].name, "custom_remote") def test_clone_push_url(self): - repo = clone_repository("./test/data/testrepo.git/", self._temp_dir, push_url="custom_push_url") + repo_path = "./test/data/testrepo.git/" + repo = clone_repository( + repo_path, self._temp_dir, push_url="custom_push_url" + ) self.assertFalse(repo.is_empty) - # not sure how to test this... couldn't find pushurl + # FIXME: When pygit2 supports retrieving the pushurl parameter, + # enable this test # self.assertEqual(repo.remotes[0].pushurl, "custom_push_url") def test_clone_fetch_spec(self): - repo = clone_repository("./test/data/testrepo.git/", self._temp_dir, fetch_spec="refs/heads/test") + repo_path = "./test/data/testrepo.git/" + repo = clone_repository( + repo_path, self._temp_dir, fetch_spec="refs/heads/test" + ) self.assertFalse(repo.is_empty) - # not sure how to test this either... fetchspec seems to be going through, but repo is not getting it. + # FIXME: When pygit2 retrieve the fetchspec we passed to git clone. + # fetchspec seems to be going through, but the Repository class is + # not getting it. # self.assertEqual(repo.remotes[0].fetchspec, "refs/heads/test") def test_clone_push_spec(self): - repo = clone_repository("./test/data/testrepo.git/", self._temp_dir, push_spec="refs/heads/test") + repo_path = "./test/data/testrepo.git/" + repo = clone_repository( + repo_path, self._temp_dir, push_spec="refs/heads/test" + ) self.assertFalse(repo.is_empty) + # FIXME: When pygit2 supports retrieving the pushspec parameter, + # enable this test # not sure how to test this either... couldn't find pushspec # self.assertEqual(repo.remotes[0].fetchspec, "refs/heads/test") def test_clone_checkout_branch(self): - repo = clone_repository("./test/data/testrepo.git/", self._temp_dir, checkout_branch="test") + repo_path = "./test/data/testrepo.git/" + repo = clone_repository( + repo_path, self._temp_dir, checkout_branch="test" + ) self.assertFalse(repo.is_empty) - # not sure how to test this either... couldn't find current branch + # FIXME: When pygit2 supports retrieving the current branch, + # enable this test # self.assertEqual(repo.remotes[0].current_branch, "test") From 57ea19e90589f506243a5e9128ca37a063b33a4f Mon Sep 17 00:00:00 2001 From: Bernardo Heynemann Date: Thu, 16 May 2013 10:06:46 -0300 Subject: [PATCH 3/6] Now __init__ is pep8-compliant --- pygit2/__init__.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/pygit2/__init__.py b/pygit2/__init__.py index 44b31e7..babce1a 100644 --- a/pygit2/__init__.py +++ b/pygit2/__init__.py @@ -50,19 +50,27 @@ def init_repository(path, bare=False): def clone_repository( - url, path, bare=False, remote_name="origin", push_url=None, fetch_spec=None, + url, path, bare=False, remote_name="origin", + push_url=None, fetch_spec=None, push_spec=None, checkout_branch=None): """ Clones a new Git repository from *url* in the given *path*. Parameters: - * If 'bare' is True, then a bare git repository will be created. - * 'remote_name' is the name given to the "origin" remote. The default is "origin". - * 'push_url' is a URL to be used for pushing. None means use the fetch url. - * 'fetch_spec' is the fetch specification to be used for fetching. None results in the same behavior as GIT_REMOTE_DEFAULT_FETCH. - * 'push_spec' is the fetch specification to be used for pushing. None means use the same spec as for 'fetch_spec'. - * 'checkout_branch' gives the name of the branch to checkout. None means use the remote's HEAD + * 'bare' indicates whether a bare git repository should be created. + * 'remote_name' is the name given to the "origin" remote. + The default is "origin". + * 'push_url' is a URL to be used for pushing. + None means use the fetch url. + * 'fetch_spec' is the fetch specification to be used for fetching. + None results in the same behavior as GIT_REMOTE_DEFAULT_FETCH. + * 'push_spec' is the fetch specification to be used for pushing. + None means use the same spec as for 'fetch_spec'. + * 'checkout_branch' gives the name of the branch to checkout. + None means use the remote's HEAD """ - _pygit2.clone_repository(url, path, bare, remote_name, push_url, fetch_spec, push_spec, checkout_branch) + _pygit2.clone_repository( + url, path, bare, remote_name, push_url, + fetch_spec, push_spec, checkout_branch) return Repository(path) From a377b32b603b82aaa9878679b9ae0ac583ecce34 Mon Sep 17 00:00:00 2001 From: Bernardo Heynemann Date: Thu, 16 May 2013 10:25:47 -0300 Subject: [PATCH 4/6] Documentation for the new clone method. Even though I am aware that the comment lines in pygit2/__init__.py are longer than 79 characters, there's a reason for that. If I break them, they'll show poorly in the documentation, and in my opinion better docs trump the 80 char requirement. If you think it's still better to have less than 80 characters in the comments, I'll gladly resubmit the changes at the expense of the documentation. --- docs/repository.rst | 11 +++++++++++ pygit2/__init__.py | 26 ++++++++++++++------------ 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/docs/repository.rst b/docs/repository.rst index 24f0674..034ed56 100644 --- a/docs/repository.rst +++ b/docs/repository.rst @@ -20,6 +20,17 @@ Functions >>> repo = init_repository('test') # Creates a non-bare repository >>> repo = init_repository('test', bare=True) # Creates a bare repository +.. autofunction:: pygit2.clone_repository + + Example:: + + >>> from pygit2 import clone_repository + >>> repo_url = 'git://github.com/libgit2/pygit2.git' + >>> repo_path = '/path/to/create/repository' + >>> repo = clone_repository(repo_url, repo_path) # Clones a non-bare repository + >>> repo = clone_repository(repo_url, repo_path, bare=True) # Clones a bare repository + + .. autofunction:: pygit2.discover_repository diff --git a/pygit2/__init__.py b/pygit2/__init__.py index babce1a..be87a49 100644 --- a/pygit2/__init__.py +++ b/pygit2/__init__.py @@ -56,18 +56,20 @@ def clone_repository( """ Clones a new Git repository from *url* in the given *path*. - Parameters: - * 'bare' indicates whether a bare git repository should be created. - * 'remote_name' is the name given to the "origin" remote. - The default is "origin". - * 'push_url' is a URL to be used for pushing. - None means use the fetch url. - * 'fetch_spec' is the fetch specification to be used for fetching. - None results in the same behavior as GIT_REMOTE_DEFAULT_FETCH. - * 'push_spec' is the fetch specification to be used for pushing. - None means use the same spec as for 'fetch_spec'. - * 'checkout_branch' gives the name of the branch to checkout. - None means use the remote's HEAD + * **bare** indicates whether a bare git repository should be created. + * **remote_name** is the name given to the "origin" remote. The default is "origin". + * **push_url** is a URL to be used for pushing. None means use the *url* parameter. + * **fetch_spec** defines the the default fetch spec. None results in the same behavior as *GIT_REMOTE_DEFAULT_FETCH*. + * **push_spec** is the fetch specification to be used for pushing. None means use the same spec as for *fetch_spec*. + * **checkout_branch** gives the name of the branch to checkout. None means use the remote's *HEAD*. + + Returns a Repository class pointing to the newly cloned repository. + + If you wish to use the repo, you need to do a checkout for one of the available branches, like this: + + >>> repo = repo.clone_repository("url", "path") + >>> repo.checkout(branch) # i.e.: refs/heads/master + """ _pygit2.clone_repository( From 032faf7017d9b5ac60c37f126697539e0dc8f63b Mon Sep 17 00:00:00 2001 From: Bernardo Heynemann Date: Thu, 16 May 2013 16:09:39 -0300 Subject: [PATCH 5/6] Fixing 80-column width for both the python and C code. --- pygit2/__init__.py | 25 ++++++++++++++++++------- src/pygit2.c | 19 +++++++++++++------ 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/pygit2/__init__.py b/pygit2/__init__.py index be87a49..e246ff8 100644 --- a/pygit2/__init__.py +++ b/pygit2/__init__.py @@ -56,16 +56,27 @@ def clone_repository( """ Clones a new Git repository from *url* in the given *path*. - * **bare** indicates whether a bare git repository should be created. - * **remote_name** is the name given to the "origin" remote. The default is "origin". - * **push_url** is a URL to be used for pushing. None means use the *url* parameter. - * **fetch_spec** defines the the default fetch spec. None results in the same behavior as *GIT_REMOTE_DEFAULT_FETCH*. - * **push_spec** is the fetch specification to be used for pushing. None means use the same spec as for *fetch_spec*. - * **checkout_branch** gives the name of the branch to checkout. None means use the remote's *HEAD*. + **bare** indicates whether a bare git repository should be created. + + **remote_name** is the name given to the "origin" remote. + The default is "origin". + + **push_url** is a URL to be used for pushing. + None means use the *url* parameter. + + **fetch_spec** defines the the default fetch spec. + None results in the same behavior as *GIT_REMOTE_DEFAULT_FETCH*. + + **push_spec** is the fetch specification to be used for pushing. + None means use the same spec as for *fetch_spec*. + + **checkout_branch** gives the name of the branch to checkout. + None means use the remote's *HEAD*. Returns a Repository class pointing to the newly cloned repository. - If you wish to use the repo, you need to do a checkout for one of the available branches, like this: + If you wish to use the repo, you need to do a checkout for one of + the available branches, like this: >>> repo = repo.clone_repository("url", "path") >>> repo.checkout(branch) # i.e.: refs/heads/master diff --git a/src/pygit2.c b/src/pygit2.c index ed1ad9d..d062a9e 100644 --- a/src/pygit2.c +++ b/src/pygit2.c @@ -98,9 +98,11 @@ init_repository(PyObject *self, PyObject *args) { }; PyDoc_STRVAR(clone_repository__doc__, - "clone_repository(url, path, bare, remote_name, push_url, fetch_spec, push_spec, checkout_branch)\n" + "clone_repository(url, path, bare, remote_name, push_url," + "fetch_spec, push_spec, checkout_branch)\n" "\n" - "Clones a Git repository in the given url to the given path with the specified options.\n" + "Clones a Git repository in the given url to the given path " + "with the specified options.\n" "\n" "Arguments:\n" "\n" @@ -115,11 +117,14 @@ PyDoc_STRVAR(clone_repository__doc__, "push_url\n" " URL to be used for pushing.\n" "fetch_spec\n" - " The fetch specification to be used for fetching. None results in the same behavior as GIT_REMOTE_DEFAULT_FETCH.\n" + " The fetch specification to be used for fetching. None results in " + "the same behavior as GIT_REMOTE_DEFAULT_FETCH.\n" "push_spec\n" - " The fetch specification to be used for pushing. None means use the same spec as for 'fetch_spec'\n" + " The fetch specification to be used for pushing. None means use the " + "same spec as for 'fetch_spec'\n" "checkout_branch\n" - " The name of the branch to checkout. None means use the remote's HEAD.\n"); + " The name of the branch to checkout. None means use the remote's " + "HEAD.\n"); PyObject * @@ -131,7 +136,9 @@ clone_repository(PyObject *self, PyObject *args) { const char *remote_name, *push_url, *fetch_spec, *push_spec, *checkout_branch; int err; - if (!PyArg_ParseTuple(args, "zzIzzzzz", &url, &path, &bare, &remote_name, &push_url, &fetch_spec, &push_spec, &checkout_branch)) + if (!PyArg_ParseTuple(args, "zzIzzzzz", + &url, &path, &bare, &remote_name, &push_url, + &fetch_spec, &push_spec, &checkout_branch)) return NULL; git_clone_options opts = { From 68e3e06a88bc7b781bc029f7241cd9125a370326 Mon Sep 17 00:00:00 2001 From: Bernardo Heynemann Date: Thu, 16 May 2013 16:13:23 -0300 Subject: [PATCH 6/6] Missed a couple lines in the C code --- src/pygit2.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pygit2.c b/src/pygit2.c index d062a9e..8362891 100644 --- a/src/pygit2.c +++ b/src/pygit2.c @@ -133,7 +133,8 @@ clone_repository(PyObject *self, PyObject *args) { const char *url; const char *path; unsigned int bare; - const char *remote_name, *push_url, *fetch_spec, *push_spec, *checkout_branch; + const char *remote_name, *push_url, *fetch_spec; + const char *push_spec, *checkout_branch; int err; if (!PyArg_ParseTuple(args, "zzIzzzzz", @@ -234,7 +235,8 @@ 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__}, + {"clone_repository", clone_repository, METH_VARARGS, + clone_repository__doc__}, {"discover_repository", discover_repository, METH_VARARGS, discover_repository__doc__}, {"hashfile", hashfile, METH_VARARGS, hashfile__doc__},