← Back to team overview

launchpad-reviewers team mailing list archive

[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