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

Proposed by Thiago F. Pappacena
Status: Merged
Approved by: Thiago F. Pappacena
Approved revision: 7c3c5f2d182f59a02a34001c77c2a72e0745407a
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~pappacena/turnip:copy-and-delete-ref-api
Merge into: turnip:master
Prerequisite: ~pappacena/turnip:copy-ref-helper
Diff against target: 244 lines (+197/-4)
2 files modified
turnip/api/tests/test_api.py (+122/-4)
turnip/api/views.py (+75/-0)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+390271@code.launchpad.net

Commit message

API to copy refs between repositories and delete refs

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) :
review: Approve
Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

Pushed the requested changes.

Revision history for this message
Colin Watson (cjwatson) :
Revision history for this message
Otto Co-Pilot (otto-copilot) wrote :
Revision history for this message
Otto Co-Pilot (otto-copilot) wrote :
7c3c5f2... by Thiago F. Pappacena

Test fix: assertRepositoryCreatedAsynchronously should retry even if API crashes

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/turnip/api/tests/test_api.py b/turnip/api/tests/test_api.py
2index dbdf77a..15c366d 100644
3--- a/turnip/api/tests/test_api.py
4+++ b/turnip/api/tests/test_api.py
5@@ -316,6 +316,120 @@ class ApiTestCase(TestCase, ApiRepoStoreMixin):
6 resp = self.get_ref(tag)
7 self.assertTrue(tag in resp)
8
9+ def test_delete_ref(self):
10+ celery_fixture = CeleryWorkerFixture()
11+ self.useFixture(celery_fixture)
12+
13+ repo = RepoFactory(
14+ self.repo_store, num_branches=5, num_commits=1, num_tags=1).build()
15+ self.assertEqual(7, len(repo.references.objects))
16+
17+ ref = 'refs/heads/branch-0'
18+ url = '/repo/{}/{}'.format(self.repo_path, ref)
19+ resp = self.app.delete(quote(url))
20+
21+ self.assertEqual(6, len(repo.references.objects))
22+ self.assertEqual(200, resp.status_code)
23+ self.assertEqual('', resp.body)
24+
25+ def test_delete_non_existing_ref(self):
26+ celery_fixture = CeleryWorkerFixture()
27+ self.useFixture(celery_fixture)
28+
29+ repo = RepoFactory(
30+ self.repo_store, num_branches=5, num_commits=1, num_tags=1).build()
31+ self.assertEqual(7, len(repo.references.objects))
32+
33+ ref = 'refs/heads/fake-branch'
34+ url = '/repo/{}/{}'.format(self.repo_path, ref)
35+ resp = self.app.delete(quote(url), expect_errors=True)
36+ self.assertEqual(404, resp.status_code)
37+ self.assertEqual({
38+ 'status': 'error',
39+ 'errors': [{
40+ 'description': 'Ref refs/heads/fake-branch does not exist.',
41+ 'location': 'body',
42+ 'name': u'operations'
43+ }]}, resp.json)
44+
45+ def test_copy_ref_api(self):
46+ celery_fixture = CeleryWorkerFixture()
47+ self.useFixture(celery_fixture)
48+ repo1_path = os.path.join(self.repo_root, 'repo1')
49+ repo2_path = os.path.join(self.repo_root, 'repo2')
50+ repo3_path = os.path.join(self.repo_root, 'repo3')
51+
52+ repo1_factory = RepoFactory(
53+ repo1_path, num_branches=5, num_commits=1, num_tags=1)
54+ repo1 = repo1_factory.build()
55+ self.assertEqual(7, len(repo1.references.objects))
56+
57+ repo2_factory = RepoFactory(
58+ repo2_path, num_branches=1, num_commits=1, num_tags=1)
59+ repo2 = repo2_factory.build()
60+ self.assertEqual(3, len(repo2.references.objects))
61+
62+ repo3_factory = RepoFactory(
63+ repo3_path, num_branches=1, num_commits=1, num_tags=1)
64+ repo3 = repo3_factory.build()
65+ self.assertEqual(3, len(repo3.references.objects))
66+
67+ url = '/repo/repo1/refs-copy'
68+ body = {
69+ "operations": [
70+ {
71+ b"from": b"refs/heads/branch-4",
72+ b"to": {b"repo": b'repo2', b"ref": b"refs/merge/123/head"}
73+ }, {
74+ b"from": b"refs/heads/branch-4",
75+ b"to": {b"repo": b'repo3', b"ref": b"refs/merge/987/head"}
76+ }]}
77+ resp = self.app.post_json(quote(url), body)
78+ self.assertEqual(202, resp.status_code)
79+
80+ def branchCreated():
81+ repo2_refs = [i.name for i in repo2.references.objects]
82+ repo3_refs = [i.name for i in repo3.references.objects]
83+ return (b'refs/merge/123/head' in repo2_refs and
84+ b'refs/merge/987/head' in repo3_refs)
85+
86+ celery_fixture.waitUntil(5, branchCreated)
87+ self.assertEqual(4, len(repo2.references.objects))
88+ self.assertEqual(202, resp.status_code)
89+ self.assertEqual('', resp.body)
90+
91+ def test_copy_non_existing_ref(self):
92+ celery_fixture = CeleryWorkerFixture()
93+ self.useFixture(celery_fixture)
94+
95+ repo_path = os.path.join(self.repo_root, 'repo1')
96+ repo = RepoFactory(
97+ repo_path, num_branches=5, num_commits=1, num_tags=1).build()
98+ self.assertEqual(7, len(repo.references.objects))
99+
100+ body = {
101+ "operations": [{
102+ b"from": b"refs/heads/nope",
103+ b"to": {b"repo": b'repo2', b"ref": b"refs/merge/123/head"}
104+ }, {
105+ b"from": b"refs/heads/no-ref",
106+ b"to": {b"repo": b'repo2', b"ref": b"refs/merge/123/head"}
107+ }]}
108+
109+ url = '/repo/repo1/refs-copy'
110+ resp = self.app.post_json(quote(url), body, expect_errors=True)
111+ self.assertEqual(404, resp.status_code)
112+ self.assertEqual({
113+ 'status': 'error',
114+ 'errors': [{
115+ 'description': 'Ref refs/heads/nope does not exist.',
116+ 'location': 'body',
117+ 'name': u'operations'
118+ }, {
119+ 'description': 'Ref refs/heads/no-ref does not exist.',
120+ 'location': 'body', 'name': u'operations'
121+ }]}, resp.json)
122+
123 def test_repo_compare_commits(self):
124 """Ensure expected changes exist in diff patch."""
125 repo = RepoFactory(self.repo_store)
126@@ -929,10 +1043,14 @@ class AsyncRepoCreationAPI(TestCase, ApiRepoStoreMixin):
127 start = datetime.now()
128 while datetime.now() <= (start + timeout):
129 self._doReactorIteration()
130- resp = self.app.get('/repo/{}'.format(repo_path),
131- expect_errors=True)
132- if resp.status_code == 200 and resp.json['is_available']:
133- return
134+ try:
135+ resp = self.app.get('/repo/{}'.format(repo_path),
136+ expect_errors=True)
137+ if resp.status_code == 200 and resp.json['is_available']:
138+ return
139+ except Exception:
140+ # If we have any unexpected error, wait a bit and retry.
141+ pass
142 time.sleep(0.1)
143 self.fail(
144 "Repository %s was not created after %s secs"
145diff --git a/turnip/api/views.py b/turnip/api/views.py
146index de6bb4e..049f3ee 100644
147--- a/turnip/api/views.py
148+++ b/turnip/api/views.py
149@@ -9,6 +9,7 @@ from cornice.resource import resource
150 from cornice.util import extract_json_data
151 from pygit2 import GitError
152 import pyramid.httpexceptions as exc
153+from pyramid.response import Response
154
155 from turnip.config import config
156 from turnip.api import store
157@@ -162,6 +163,65 @@ class RepackAPI(BaseAPI):
158 return
159
160
161+@resource(path='/repo/{name}/refs-copy')
162+class RefCopyAPI(BaseAPI):
163+ """Provides HTTP API for git references copy operations."""
164+
165+ def __init__(self, request, context=None):
166+ super(RefCopyAPI, self).__init__()
167+ self.request = request
168+
169+ def _validate_refs(self, repo_store, repo_name, refs_or_commits):
170+ """Checks if a given list of ref names or commits ID exists in
171+ repo. If not, raises 404 exception.
172+
173+ Note that the API copy runs async, in a celery job. So,
174+ this validation does not guarantee that the copy operation will
175+ actually be done since someone could delete the ref_or_commit
176+ between the check and the actual execution of the copy task.
177+ """
178+ for ref_or_commit in refs_or_commits:
179+ # Checks if it's a commit.
180+ try:
181+ store.get_commit(repo_store, repo_name, ref_or_commit)
182+ return
183+ except GitError:
184+ pass
185+ # Checks if it's a ref name.
186+ try:
187+ store.get_ref(repo_store, repo_name, ref_or_commit)
188+ return
189+ except KeyError:
190+ self.request.errors.add(
191+ 'body', 'operations',
192+ 'Ref %s does not exist.' % ref_or_commit)
193+
194+ if len(self.request.errors):
195+ self.request.errors.status = 404
196+
197+ @validate_path
198+ def post(self, repo_store, repo_name):
199+ orig_path = os.path.join(repo_store, repo_name)
200+ copy_refs_args = []
201+ operations = self.request.json.get('operations', [])
202+ self._validate_refs(
203+ repo_store, repo_name, [i["from"] for i in operations])
204+ if len(self.request.errors):
205+ return
206+
207+ for operation in operations:
208+ source = operation["from"]
209+ dest = operation["to"]
210+ dest_repo = dest.get('repo')
211+ dest_ref_name = dest.get('ref')
212+ dest_path = os.path.join(repo_store, dest_repo)
213+ copy_refs_args.append(
214+ (orig_path, source, dest_path, dest_ref_name))
215+
216+ store.fetch_refs.apply_async(args=(copy_refs_args, ))
217+ return Response(status=202)
218+
219+
220 @resource(collection_path='/repo/{name}/refs',
221 path='/repo/{name}/refs/{ref:.*}')
222 class RefAPI(BaseAPI):
223@@ -188,6 +248,21 @@ class RefAPI(BaseAPI):
224 except (KeyError, GitError):
225 return exc.HTTPNotFound()
226
227+ @validate_path
228+ def delete(self, repo_store, repo_name):
229+ ref = 'refs/' + self.request.matchdict['ref']
230+ # Make sure the ref actually exists. Otherwise, raise a 404.
231+ try:
232+ store.get_ref(repo_store, repo_name, ref)
233+ except (KeyError, GitError):
234+ self.request.errors.add(
235+ 'body', 'operations', 'Ref %s does not exist.' % ref)
236+ self.request.errors.status = 404
237+ return
238+ repo_path = os.path.join(repo_store, repo_name)
239+ store.delete_refs([(repo_path, ref)])
240+ return Response(status=200)
241+
242
243 @resource(path='/repo/{name}/compare/{commits}')
244 class DiffAPI(BaseAPI):

Subscribers

People subscribed via source and target branches