← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~pappacena/turnip:copy-and-delete-ref-api into turnip:master

 

Thiago F. Pappacena has proposed merging ~pappacena/turnip:copy-and-delete-ref-api into turnip:master with ~pappacena/turnip:copy-ref-helper as a prerequisite.

Commit message:
API to copy refs between repositories and delete refs

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~pappacena/turnip/+git/turnip/+merge/390271
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~pappacena/turnip:copy-and-delete-ref-api into turnip:master.
diff --git a/turnip/api/tests/test_api.py b/turnip/api/tests/test_api.py
index f055b82..06f9eee 100644
--- a/turnip/api/tests/test_api.py
+++ b/turnip/api/tests/test_api.py
@@ -32,6 +32,7 @@ from turnip.api.tests.test_helpers import (
     open_repo,
     RepoFactory,
     )
+from turnip.tests.tasks import CeleryWorkerFixture
 
 
 class ApiTestCase(TestCase):
@@ -302,6 +303,106 @@ class ApiTestCase(TestCase):
         resp = self.get_ref(tag)
         self.assertTrue(tag in resp)
 
+    def test_delete_ref(self):
+        celery_fixture = CeleryWorkerFixture()
+        self.useFixture(celery_fixture)
+
+        repo = RepoFactory(
+            self.repo_store, num_branches=5, num_commits=1, num_tags=1).build()
+        self.assertEqual(7, len(repo.references.objects))
+
+        ref = 'refs/heads/branch-0'
+        url = '/repo/{}/{}'.format(self.repo_path, ref)
+        resp = self.app.delete(quote(url))
+
+        def branchDeleted():
+            refs = [i.name for i in repo.references.objects]
+            return b'refs/heads/branch-0' not in refs
+
+        celery_fixture.waitUntil(5, branchDeleted)
+
+        self.assertEqual(6, len(repo.references.objects))
+        self.assertEqual(202, resp.status_code)
+        self.assertEqual('', resp.body)
+
+    def test_delete_non_existing_ref(self):
+        celery_fixture = CeleryWorkerFixture()
+        self.useFixture(celery_fixture)
+
+        repo = RepoFactory(
+            self.repo_store, num_branches=5, num_commits=1, num_tags=1).build()
+        self.assertEqual(7, len(repo.references.objects))
+
+        ref = 'refs/heads/this-branch-doesnt-exist'
+        url = '/repo/{}/{}'.format(self.repo_path, ref)
+        resp = self.app.delete(quote(url), expect_errors=True)
+        self.assertEqual(404, resp.status_code)
+
+    def test_copy_ref_api(self):
+        celery_fixture = CeleryWorkerFixture()
+        self.useFixture(celery_fixture)
+        repo1_path = os.path.join(self.repo_root, 'repo1')
+        repo2_path = os.path.join(self.repo_root, 'repo2')
+        repo3_path = os.path.join(self.repo_root, 'repo3')
+
+        repo1_factory = RepoFactory(
+            repo1_path, num_branches=5, num_commits=1, num_tags=1)
+        repo1 = repo1_factory.build()
+        self.assertEqual(7, len(repo1.references.objects))
+
+        repo2_factory = RepoFactory(
+            repo2_path, num_branches=1, num_commits=1, num_tags=1)
+        repo2 = repo2_factory.build()
+        self.assertEqual(3, len(repo2.references.objects))
+
+        repo3_factory = RepoFactory(
+            repo3_path, num_branches=1, num_commits=1, num_tags=1)
+        repo3 = repo3_factory.build()
+        self.assertEqual(3, len(repo3.references.objects))
+
+        url = '/repo/repo1/refs-copy'
+        body = {
+            "operations": [
+                {
+                    b"from": b"refs/heads/branch-4",
+                    b"to": {b"repo": b'repo2', b"ref": b"refs/merge/123/head"}
+                }, {
+                    b"from": b"refs/heads/branch-4",
+                    b"to": {b"repo": b'repo3', b"ref": b"refs/merge/987/head"}
+                }]}
+        resp = self.app.post_json(quote(url), body)
+        self.assertEqual(202, resp.status_code)
+
+        def branchCreated():
+            repo2_refs = [i.name for i in repo2.references.objects]
+            repo3_refs = [i.name for i in repo3.references.objects]
+            return (b'refs/merge/123/head' in repo2_refs and
+                    b'refs/merge/987/head' in repo3_refs)
+
+        celery_fixture.waitUntil(5, branchCreated)
+        self.assertEqual(4, len(repo2.references.objects))
+        self.assertEqual(202, resp.status_code)
+        self.assertEqual('', resp.body)
+
+    def test_copy_non_existing_ref(self):
+        celery_fixture = CeleryWorkerFixture()
+        self.useFixture(celery_fixture)
+
+        repo_path = os.path.join(self.repo_root, 'repo1')
+        repo = RepoFactory(
+            repo_path, num_branches=5, num_commits=1, num_tags=1).build()
+        self.assertEqual(7, len(repo.references.objects))
+
+        body = {
+            "operations": [{
+                b"from": b"refs/heads/this-ref-doesnt-exist-at-all",
+                b"to": {b"repo": b'repo2', b"ref": b"refs/merge/123/head"}
+            }]}
+
+        url = '/repo/repo1/refs-copy'
+        resp = self.app.post_json(quote(url), body, expect_errors=True)
+        self.assertEqual(404, resp.status_code)
+
     def test_repo_compare_commits(self):
         """Ensure expected changes exist in diff patch."""
         repo = RepoFactory(self.repo_store)
diff --git a/turnip/api/views.py b/turnip/api/views.py
index 37e89ef..4b4ebff 100644
--- a/turnip/api/views.py
+++ b/turnip/api/views.py
@@ -9,6 +9,7 @@ from cornice.resource import resource
 from cornice.util import extract_json_data
 from pygit2 import GitError
 import pyramid.httpexceptions as exc
+from pyramid.response import Response
 
 from turnip.config import config
 from turnip.api import store
@@ -155,6 +156,49 @@ class RepackAPI(BaseAPI):
         return
 
 
+@resource(path='/repo/{name}/refs-copy')
+class RefCopyAPI(BaseAPI):
+    """Provides HTTP API for git references copy operations."""
+
+    def __init__(self, request, context=None):
+        super(RefCopyAPI, self).__init__()
+        self.request = request
+
+    def _validate_ref(self, repo_store, repo_name, ref_or_commit):
+        """Checks if a ref name or commit ID exists in repo. If not, raises
+        404 exception."""
+        # Checks if it's a commit.
+        try:
+            store.get_commit(repo_store, repo_name, ref_or_commit)
+            return
+        except GitError:
+            pass
+        # Checks if it's a ref name.
+        try:
+            store.get_ref(repo_store, repo_name, ref_or_commit)
+            return
+        except KeyError:
+            raise exc.HTTPNotFound()
+
+    @validate_path
+    def post(self, repo_store, repo_name):
+        orig_path = os.path.join(repo_store, repo_name)
+        copy_ref_calls = []
+        for operation in self.request.json.get('operations'):
+            source = operation["from"]
+            self._validate_ref(repo_store, repo_name, source)
+            dest = operation["to"]
+            dest_repo = dest.get('repo')
+            dest_ref_name = dest.get('ref')
+            dest_path = os.path.join(repo_store, dest_repo)
+            copy_ref_calls.append(
+                (orig_path, source, dest_path, dest_ref_name))
+
+        for args in copy_ref_calls:
+            store.copy_ref.apply_async(args)
+        return Response(status=202)
+
+
 @resource(collection_path='/repo/{name}/refs',
           path='/repo/{name}/refs/{ref:.*}')
 class RefAPI(BaseAPI):
@@ -181,6 +225,18 @@ class RefAPI(BaseAPI):
         except (KeyError, GitError):
             return exc.HTTPNotFound()
 
+    @validate_path
+    def delete(self, repo_store, repo_name):
+        ref = 'refs/' + self.request.matchdict['ref']
+        # Make sure the ref actually exists. Otherwise, raise a 404.
+        try:
+            store.get_ref(repo_store, repo_name, ref)
+        except (KeyError, GitError):
+            return exc.HTTPNotFound()
+        repo_path = os.path.join(repo_store, repo_name)
+        store.delete_ref.apply_async((repo_path, ref))
+        return Response(status=202)
+
 
 @resource(path='/repo/{name}/compare/{commits}')
 class DiffAPI(BaseAPI):