launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #18027
[Merge] lp:~blr/turnip/api-diff into lp:turnip
Bayard 'kit' Randel has proposed merging lp:~blr/turnip/api-diff into lp:turnip.
Requested reviews:
William Grant (wgrant)
For more details, see:
https://code.launchpad.net/~blr/turnip/api-diff/+merge/251841
Initial diff api, with additional test helpers for index staging and creating commits from arbitrary blob data and paths.
--
Your team Launchpad code reviewers is subscribed to branch lp:turnip.
=== modified file 'turnip/api/store.py'
--- turnip/api/store.py 2015-02-26 21:34:02 +0000
+++ turnip/api/store.py 2015-03-04 22:21:18 +0000
@@ -3,7 +3,24 @@
import os
import shutil
-import pygit2
+from pygit2 import (
+ GitError,
+ GIT_OBJ_BLOB,
+ GIT_OBJ_COMMIT,
+ GIT_OBJ_TREE,
+ GIT_OBJ_TAG,
+ init_repository,
+ Repository,
+ )
+
+
+def get_ref_type_name(ref_type_code):
+ """Returns human readable ref type from ref type int."""
+ types = {GIT_OBJ_COMMIT: 'commit',
+ GIT_OBJ_TREE: 'tree',
+ GIT_OBJ_BLOB: 'blob',
+ GIT_OBJ_TAG: 'tag'}
+ return types.get(ref_type_code)
class Store(object):
@@ -11,21 +28,65 @@
@staticmethod
def init(repo, is_bare=True):
- """Initialise a git repo with pygit2."""
+ """Initialise a git repository."""
if os.path.exists(repo):
raise Exception("Repository '%s' already exists" % repo)
try:
- repo_path = pygit2.init_repository(repo, is_bare)
- except pygit2.GitError:
- print('Unable to create repository in {}.'.format(repo))
+ repo_path = init_repository(repo, is_bare)
+ except GitError:
raise
return repo_path
@staticmethod
+ def open_repo(repo_path):
+ """Open an existing git repository."""
+ try:
+ repo = Repository(repo_path)
+ except GitError:
+ raise
+ return repo
+
+ @staticmethod
def delete(repo):
"""Permanently delete a git repository from repo store."""
try:
shutil.rmtree(repo)
except (IOError, OSError):
- print('Unable to delete repository from {}.'.format(repo))
- raise
+ raise
+
+ @staticmethod
+ def get_refs(repo_path):
+ """Return all refs for a git repository."""
+ repo = Store.open_repo(repo_path)
+ refs = {}
+ for ref in repo.listall_references():
+ git_object = repo.lookup_reference(ref).get_object()
+ refs[ref] = {
+ "object": {'sha': git_object.oid.hex,
+ 'type': get_ref_type_name(git_object.type)}
+ }
+ return refs
+
+ @staticmethod
+ def get_ref(repo_path, ref):
+ """Return a specific ref for a git repository."""
+ repo = Store.open_repo(repo_path)
+ try:
+ git_object = repo.lookup_reference(ref).get_object()
+ except GitError:
+ raise
+ ref = {"ref": ref,
+ "object": {'sha': git_object.oid.hex,
+ 'type': get_ref_type_name(git_object.type)}}
+ return ref
+
+ @staticmethod
+ def get_diff(repo_path, hash1, hash2):
+ """Get diff of two commits."""
+ repo = Store.open_repo(repo_path)
+ shas = [hash1, hash2]
+ try:
+ commits = [repo.revparse_single(sha) for sha in shas]
+ except (TypeError, KeyError):
+ raise
+ return repo.diff(commits[0], commits[1]).patch
=== modified file 'turnip/api/tests/test_api.py'
--- turnip/api/tests/test_api.py 2015-02-25 07:28:44 +0000
+++ turnip/api/tests/test_api.py 2015-03-04 22:21:18 +0000
@@ -15,6 +15,7 @@
from webtest import TestApp
from turnip import api
+from turnip.api.tests.test_helpers import RepoFactory
class ApiTestCase(TestCase):
@@ -26,9 +27,17 @@
self.app = TestApp(api.main({}))
self.repo_path = str(uuid.uuid1())
self.repo_store = os.path.join(repo_store, self.repo_path)
-
- def test_repo_post(self):
- resp = self.app.post('/repo', json.dumps({'repo_path': self.repo_path}))
+ self.commit = {'ref': 'refs/heads/master', 'message': 'test commit.'}
+ self.tag = {'ref': 'refs/tags/tag0', 'message': 'tag message'}
+
+ def get_ref(self, ref):
+ resp = self.app.get('/repo/{}/{}'.format(
+ self.repo_path, ref))
+ return json.loads(resp.json_body)
+
+ def test_repo_create(self):
+ resp = self.app.post('/repo', json.dumps(
+ {'repo_path': self.repo_path}))
self.assertEqual(resp.status_code, 200)
def test_repo_delete(self):
@@ -37,6 +46,45 @@
self.assertEqual(resp.status_code, 200)
self.assertFalse(os.path.exists(self.repo_store))
+ def test_repo_get_refs(self):
+ """Ensure expected ref objects are returned and shas match."""
+ ref = self.commit.get('ref')
+ repo = RepoFactory(self.repo_store, num_commits=1, num_tags=1).build()
+ resp = self.app.get('/repo/{}/refs'.format(self.repo_path))
+ body = json.loads(resp.json_body)
+
+ self.assertTrue(ref in body)
+ self.assertTrue(self.tag.get('ref') in body)
+
+ oid = repo.head.get_object().oid.hex # git object sha
+ resp_sha = body[ref]['object'].get('sha')
+ self.assertEqual(oid, resp_sha)
+
+ def test_repo_get_ref(self):
+ RepoFactory(self.repo_store, num_commits=1).build()
+ ref = self.commit.get('ref')
+ resp = self.get_ref(ref)
+ self.assertEqual(ref, resp['ref'])
+
+ def test_repo_get_tag(self):
+ RepoFactory(self.repo_store, num_commits=1, num_tags=1).build()
+ tag = self.tag.get('ref')
+ resp = self.get_ref(tag)
+ self.assertEqual(tag, resp['ref'])
+
+ def test_repo_compare_commits(self):
+ repo = RepoFactory(self.repo_store)
+
+ c1_oid = repo.add_commit('foo', 'foobar.txt')
+ c2_oid = repo.add_commit('bar', 'foobar.txt', parents=[c1_oid])
+
+ path = '/repo/{}/compare/{}..{}'.format(self.repo_path, c1_oid, c2_oid)
+ resp = self.app.get(path)
+ resp_body = json.loads(resp.body)
+ self.assertTrue('-foo' in resp_body)
+ self.assertTrue('+bar' in resp_body)
+
if __name__ == '__main__':
+
unittest.main()
=== added file 'turnip/api/tests/test_helpers.py'
--- turnip/api/tests/test_helpers.py 1970-01-01 00:00:00 +0000
+++ turnip/api/tests/test_helpers.py 2015-03-04 22:21:18 +0000
@@ -0,0 +1,85 @@
+# Copyright 2015 Canonical Ltd. All rights reserved.
+
+import os
+
+from pygit2 import (
+ init_repository,
+ GIT_FILEMODE_BLOB,
+ GIT_OBJ_COMMIT,
+ GIT_SORT_TIME,
+ IndexEntry,
+ Signature,
+ )
+
+
+class RepoFactory():
+ """Builds a git repository in a user defined state."""
+
+ def __init__(self, repo_store=None, num_commits=None, num_tags=None):
+ self.author = Signature('Test Author', 'author@xxxxxxx')
+ self.committer = Signature('Test Commiter', 'committer@xxxxxxx')
+ self.num_commits = num_commits
+ self.num_tags = num_tags
+ self.repo_store = repo_store
+ self.repo = self.init_repo()
+
+ @property
+ def commits(self):
+ """Walk repo from HEAD and returns list of commit objects."""
+ last = self.repo[self.repo.head.target]
+ return list(self.repo.walk(last.id, GIT_SORT_TIME))
+
+ def add_commit(self, blob_content, file_path, parents=[],
+ ref='refs/heads/master'):
+ """Create a commit from blob_content and file_path."""
+ repo = self.repo
+
+ blob_oid = repo.create_blob(blob_content)
+ blob_entry = IndexEntry(file_path, blob_oid, GIT_FILEMODE_BLOB)
+ repo.index.add(blob_entry)
+ tree_id = repo.index.write_tree()
+ oid = repo.create_commit(ref,
+ self.author,
+ self.committer,
+ 'commit', tree_id, parents)
+ return oid
+
+ def stage(self, file_path):
+ """Stage a file and return a tree id."""
+ repo = self.repo
+ repo.index.add(file_path)
+ repo.index.write()
+ return repo.index.write_tree()
+
+ def generate_commits(self):
+ """Generate n number of commits."""
+ parents = []
+ for i in xrange(self.num_commits):
+ blob_content = b'commit {}'.format(i)
+ test_file = 'test.txt'
+ with open(os.path.join(self.repo_store, test_file), 'w') as f:
+ f.write(blob_content)
+
+ self.stage(test_file)
+
+ commit_oid = self.add_commit(blob_content, test_file, parents)
+ parents = [commit_oid]
+
+ def generate_tags(self):
+ """Generate n number of tags."""
+ repo = self.repo
+ oid = repo.head.get_object().oid
+ for i in xrange(self.num_tags):
+ repo.create_tag('tag{}'.format(i), oid, GIT_OBJ_COMMIT,
+ self.committer, 'tag message {}'.format(i))
+
+ def init_repo(self):
+ return init_repository(self.repo_store)
+
+ def build(self):
+ """Return a repo, optionally with generated commits and tags."""
+ if self.num_commits:
+ self.generate_commits()
+ if self.num_tags:
+ self.generate_tags()
+ return self.repo
=== modified file 'turnip/api/views.py'
--- turnip/api/views.py 2015-03-04 08:39:15 +0000
+++ turnip/api/views.py 2015-03-04 22:21:18 +0000
@@ -1,5 +1,6 @@
# Copyright 2015 Canonical Ltd. All rights reserved.
+import json
import os
from cornice.resource import resource
@@ -10,7 +11,19 @@
from turnip.api.store import Store
-@resource(collection_path='repo', path='/repo/{name}')
+def repo_path(func):
+ """Decorator builds repo path from request name and repo_store."""
+ def func_wrapper(self):
+ name = self.request.matchdict['name']
+ if not name:
+ self.request.errors.add('body', 'name', 'repo name is missing')
+ return
+ self.repo = os.path.join(self.repo_store, name)
+ return func(self)
+ return func_wrapper
+
+
+@resource(collection_path='/repo', path='/repo/{name}')
class RepoAPI(object):
"""Provides HTTP API for repository actions."""
@@ -32,14 +45,63 @@
except Exception:
return exc.HTTPConflict() # 409
+ @repo_path
def delete(self):
"""Delete an existing git repository."""
- name = self.request.matchdict['name']
- if not name:
- self.request.errors.add('body', 'name', 'repo name is missing')
- return
- repo = os.path.join(self.repo_store, name)
- try:
- Store.delete(repo)
- except Exception:
- return exc.HTTPNotFound() # 404
+ try:
+ Store.delete(self.repo)
+ except Exception:
+ return exc.HTTPNotFound() # 404
+
+
+@resource(collection_path='/repo/{name}/refs',
+ path='/repo/{name}/refs/{ref:.*}')
+class RefAPI(object):
+ """Provides HTTP API for git references."""
+
+ def __init__(self, request):
+ config = TurnipConfig()
+ self.request = request
+ self.repo_store = config.get('repo_store')
+
+ @repo_path
+ def collection_get(self):
+ try:
+ refs = Store.get_refs(self.repo)
+ except Exception:
+ return exc.HTTPNotFound() # 404
+ return json.dumps(refs)
+
+ @repo_path
+ def get(self):
+ ref = 'refs/' + self.request.matchdict['ref']
+ try:
+ ref = Store.get_ref(self.repo, ref)
+ except Exception:
+ return exc.HTTPNotFound()
+ return json.dumps(ref)
+
+
+@resource(path='/repo/{name}/compare/{c1}..{c2}')
+class DiffAPI(object):
+ """Provides HTTP API for git references."""
+
+ def __init__(self, request):
+ config = TurnipConfig()
+ self.request = request
+ self.repo_store = config.get('repo_store')
+
+ @repo_path
+ def get(self):
+ """Returns diff of two commits."""
+ c1 = self.request.matchdict['c1']
+ c2 = self.request.matchdict['c2']
+ for sha in self.request.matchdict.iteritems():
+ if 'c' in sha[0] and not 7 <= len(sha[1]) <= 40:
+ return exc.HTTPBadRequest(
+ comment='invalid sha1: {}'.format(sha))
+ try:
+ patch = Store.get_diff(self.repo, c1, c2)
+ except:
+ return exc.HTTPNotFound()
+ return json.dumps(patch)
Follow ups