← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/turnip:blob-api into turnip:master

 

Colin Watson has proposed merging ~cjwatson/turnip:blob-api into turnip:master.

Commit message:
Add an API endpoint for fetching blobs

/repo/NAME/blob/path/to/file?rev=master returns the contents of
path/to/file at the commit pointed to by master.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/turnip/+git/turnip/+merge/288416

Add an API endpoint for fetching blobs

/repo/NAME/blob/path/to/file?rev=master returns the contents of
path/to/file at the commit pointed to by master.

We'll use this for extracting snapcraft.yaml from branches used for snaps in order to find their snap name.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/turnip:blob-api into turnip:master.
diff --git a/turnip/api/store.py b/turnip/api/store.py
index 5728d0c..641702b 100644
--- a/turnip/api/store.py
+++ b/turnip/api/store.py
@@ -72,6 +72,17 @@ def format_signature(signature):
         }
 
 
+def format_blob(blob):
+    """Return a formatted blob dict."""
+    if blob.type != GIT_OBJ_BLOB:
+        raise GitError('Invalid type: object {} is not a blob.'.format(
+            blob.oid.hex))
+    return {
+        'size': blob.size,
+        'data': blob.data,
+        }
+
+
 def is_bare_repo(repo_path):
     return not os.path.exists(os.path.join(repo_path, '.git'))
 
@@ -453,3 +464,13 @@ def detect_merges(repo_store, repo_name, target_oid, source_oids):
             if not search_oids:
                 break
         return merge_info
+
+
+def get_blob(repo_store, repo_name, rev, filename):
+    """Return a blob from a revision and file name."""
+    with open_repo(repo_store, repo_name) as repo:
+        commit = repo.revparse_single(rev)
+        if commit.type != GIT_OBJ_COMMIT:
+            raise GitError('Invalid type: object {} is not a commit.'.format(
+                commit.oid.hex))
+        return format_blob(repo[commit.tree[filename].id])
diff --git a/turnip/api/tests/test_api.py b/turnip/api/tests/test_api.py
index 687e971..6bd79c0 100644
--- a/turnip/api/tests/test_api.py
+++ b/turnip/api/tests/test_api.py
@@ -645,6 +645,44 @@ class ApiTestCase(TestCase):
         self.assertEqual(200, resp.status_code)
         self.assertEqual({b.hex: c.hex, e.hex: g.hex}, resp.json)
 
+    def test_repo_blob(self):
+        """Getting an existing blob works."""
+        factory = RepoFactory(self.repo_store)
+        c1 = factory.add_commit('a\n', 'dir/file')
+        factory.add_commit('b\n', 'dir/file', parents=[c1])
+        resp = self.app.get('/repo/{}/blob/dir/file'.format(self.repo_path))
+        self.assertEqual({'size': 2, 'data': 'b\n'}, resp.json)
+        resp = self.app.get('/repo/{}/blob/dir/file?rev=master'.format(
+            self.repo_path))
+        self.assertEqual({'size': 2, 'data': 'b\n'}, resp.json)
+        resp = self.app.get('/repo/{}/blob/dir/file?rev={}'.format(
+            self.repo_path, c1.hex))
+        self.assertEqual({'size': 2, 'data': 'a\n'}, resp.json)
+
+    def test_repo_blob_missing_commit(self):
+        """Trying to get a blob from a non-existent commit returns HTTP 404."""
+        factory = RepoFactory(self.repo_store)
+        factory.add_commit('a\n', 'dir/file')
+        resp = self.app.get('/repo/{}/blob/dir/file?rev={}'.format(
+            self.repo_path, factory.nonexistent_oid()), expect_errors=True)
+        self.assertEqual(404, resp.status_code)
+
+    def test_repo_blob_missing_file(self):
+        """Trying to get a blob with a non-existent name returns HTTP 404."""
+        factory = RepoFactory(self.repo_store)
+        factory.add_commit('a\n', 'dir/file')
+        resp = self.app.get('/repo/{}/blob/nonexistent'.format(
+            self.repo_path), expect_errors=True)
+        self.assertEqual(404, resp.status_code)
+
+    def test_repo_blob_directory(self):
+        """Trying to get a blob referring to a directory returns HTTP 404."""
+        factory = RepoFactory(self.repo_store)
+        factory.add_commit('a\n', 'dir/file')
+        resp = self.app.get('/repo/{}/blob/dir'.format(
+            self.repo_path), expect_errors=True)
+        self.assertEqual(404, resp.status_code)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/turnip/api/views.py b/turnip/api/views.py
index db57e00..abdfaca 100644
--- a/turnip/api/views.py
+++ b/turnip/api/views.py
@@ -320,3 +320,27 @@ class DetectMergesAPI(BaseAPI):
         except GitError:
             return exc.HTTPNotFound()
         return merges
+
+
+@resource(path='/repo/{name}/blob/{filename:.*}')
+class BlobAPI(BaseAPI):
+    """Provides HTTP API for fetching blobs."""
+
+    def __init__(self, request):
+        super(BlobAPI, self).__init__()
+        self.request = request
+
+    @validate_path
+    def get(self, repo_store, repo_name):
+        """Get blob by file name.
+
+        If supplied, the 'rev' request parameter identifies the revision (in
+        gitrevisions(7) syntax) where the blob should be looked up.  It
+        defaults to 'HEAD'.
+        """
+        filename = self.request.matchdict['filename']
+        rev = self.request.params.get('rev', 'HEAD')
+        try:
+            return store.get_blob(repo_store, repo_name, rev, filename)
+        except (KeyError, GitError):
+            return exc.HTTPNotFound()

Follow ups