launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #25258
[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):