Merge ~pappacena/turnip:async-repo-creation into turnip:master

Proposed by Thiago F. Pappacena
Status: Merged
Approved by: Thiago F. Pappacena
Approved revision: f71c5aa4de8ee0da622776706f724d0fc37ed114
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~pappacena/turnip:async-repo-creation
Merge into: turnip:master
Diff against target: 176 lines (+68/-5)
4 files modified
turnip/api/store.py (+29/-0)
turnip/api/tests/test_api.py (+6/-2)
turnip/api/tests/test_store.py (+28/-0)
turnip/api/views.py (+5/-3)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+386461@code.launchpad.net

Commit message

Adding option on GET /repo/<repository-path> API call to check if a repository finished creation.

Description of the change

GET /repo/<repository-path> now returns a new key called "is_available", that should be False while the repository is being created. This could be used, for example, at Launchpad's garbo to make sure a repository creation actually failed before deleting it.

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

Changed the behavior of the status file as requested, and added another MP to add celery (https://code.launchpad.net/~pappacena/turnip/+git/turnip/+merge/387252).

The separated MP is just to make the review a bit easier, since they should be landed at once.

Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

Removed the code to run async repository creation from this MP. Now, this only adds the "is_available" return value to GET /repo/<repo-path>.

Revision history for this message
Colin Watson (cjwatson) :
review: Approve
Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

Pushed the requested change. I'll land this MP.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/turnip/api/store.py b/turnip/api/store.py
2index 5723048..f4df7e9 100644
3--- a/turnip/api/store.py
4+++ b/turnip/api/store.py
5@@ -38,6 +38,10 @@ REF_TYPE_NAME = {
6 }
7
8
9+# Where to store repository status information inside a repository directory.
10+REPOSITORY_CREATING_FILE_NAME = '.turnip-creating'
11+
12+
13 def format_ref(ref, git_object):
14 """Return a formatted object dict from a ref."""
15 return {
16@@ -209,6 +213,7 @@ def init_repo(repo_path, clone_from=None, clone_refs=False,
17 if os.path.exists(repo_path):
18 raise AlreadyExistsError(repo_path)
19 init_repository(repo_path, is_bare)
20+ set_repository_creating(repo_path, True)
21
22 if clone_from:
23 # The clone_from's objects and refs are in fact cloned into a
24@@ -240,6 +245,7 @@ def init_repo(repo_path, clone_from=None, clone_refs=False,
25 write_packed_refs(repo_path, packable_refs)
26
27 ensure_config(repo_path) # set repository configuration defaults
28+ set_repository_creating(repo_path, False)
29
30
31 @contextmanager
32@@ -287,6 +293,29 @@ def get_default_branch(repo_path):
33 return repo.references['HEAD'].target
34
35
36+def set_repository_creating(repo_path, is_creating):
37+ if not os.path.exists(repo_path):
38+ raise ValueError("Repository %s does not exist." % repo_path)
39+ file_path = os.path.join(repo_path, REPOSITORY_CREATING_FILE_NAME)
40+ if is_creating:
41+ open(file_path, 'a').close()
42+ else:
43+ try:
44+ os.unlink(file_path)
45+ except OSError:
46+ pass
47+
48+
49+def is_repository_available(repo_path):
50+ """Checks if the repository is available (that is, if it is not in the
51+ middle of a clone or init operation)."""
52+ if not os.path.exists(repo_path):
53+ return False
54+
55+ status_file_path = os.path.join(repo_path, REPOSITORY_CREATING_FILE_NAME)
56+ return not os.path.exists(status_file_path)
57+
58+
59 def set_default_branch(repo_path, target):
60 repo = Repository(repo_path)
61 repo.set_head(target)
62diff --git a/turnip/api/tests/test_api.py b/turnip/api/tests/test_api.py
63index 396369b..bcf13f9 100644
64--- a/turnip/api/tests/test_api.py
65+++ b/turnip/api/tests/test_api.py
66@@ -98,7 +98,9 @@ class ApiTestCase(TestCase):
67
68 resp = self.app.get('/repo/{}'.format(self.repo_path))
69 self.assertEqual(200, resp.status_code)
70- self.assertEqual({'default_branch': 'refs/heads/branch-0'}, resp.json)
71+ self.assertEqual({
72+ 'default_branch': 'refs/heads/branch-0',
73+ 'is_available': True}, resp.json)
74
75 def test_repo_get_default_branch_missing(self):
76 """default_branch is returned even if that branch has been deleted."""
77@@ -109,7 +111,9 @@ class ApiTestCase(TestCase):
78
79 resp = self.app.get('/repo/{}'.format(self.repo_path))
80 self.assertEqual(200, resp.status_code)
81- self.assertEqual({'default_branch': 'refs/heads/branch-0'}, resp.json)
82+ self.assertEqual({
83+ 'default_branch': 'refs/heads/branch-0',
84+ 'is_available': True}, resp.json)
85
86 def test_repo_patch_default_branch(self):
87 """A repository's default branch ("HEAD") can be changed."""
88diff --git a/turnip/api/tests/test_store.py b/turnip/api/tests/test_store.py
89index 9276303..02ed3fd 100644
90--- a/turnip/api/tests/test_store.py
91+++ b/turnip/api/tests/test_store.py
92@@ -125,6 +125,25 @@ class InitTestCase(TestCase):
93 self.assertEqual(str(yaml_config['pack.depth']),
94 repo_config['pack.depth'])
95
96+ def test_is_repository_available(self):
97+ repo_path = os.path.join(self.repo_store, 'repo/')
98+
99+ # Fail to set status if repository directory doesn't exist.
100+ self.assertRaises(
101+ ValueError, store.set_repository_creating, repo_path, False)
102+
103+ store.init_repository(repo_path, True)
104+ store.set_repository_creating(repo_path, True)
105+ self.assertFalse(store.is_repository_available(repo_path))
106+
107+ store.set_repository_creating(repo_path, False)
108+ self.assertTrue(store.is_repository_available(repo_path))
109+
110+ # Duplicate call to set_repository_creating(False) should ignore
111+ # eventually missing ".turnip-creating" file.
112+ store.set_repository_creating(repo_path, False)
113+ self.assertTrue(store.is_repository_available(repo_path))
114+
115 def test_open_ephemeral_repo(self):
116 """Opening a repo where a repo name contains ':' should return
117 a new ephemeral repo.
118@@ -224,7 +243,10 @@ class InitTestCase(TestCase):
119 # repo with the same set of refs. And the objects are copied
120 # too.
121 to_path = os.path.join(self.repo_store, 'to/')
122+ self.assertFalse(store.is_repository_available(to_path))
123 store.init_repo(to_path, clone_from=self.orig_path, clone_refs=True)
124+ self.assertTrue(store.is_repository_available(to_path))
125+
126 to = pygit2.Repository(to_path)
127 self.assertIsNot(None, to[self.master_oid])
128 self.assertEqual(
129@@ -260,7 +282,10 @@ class InitTestCase(TestCase):
130 # init_repo with clone_from=orig and clone_refs=False creates a
131 # repo without any refs, but the objects are copied.
132 to_path = os.path.join(self.repo_store, 'to/')
133+ self.assertFalse(store.is_repository_available(to_path))
134 store.init_repo(to_path, clone_from=self.orig_path, clone_refs=False)
135+ self.assertTrue(store.is_repository_available(to_path))
136+
137 to = pygit2.Repository(to_path)
138 self.assertIsNot(None, to[self.master_oid])
139 self.assertEqual([], to.listall_references())
140@@ -286,7 +311,10 @@ class InitTestCase(TestCase):
141
142 self.assertAllLinkCounts(1, self.orig_objs)
143 to_path = os.path.join(self.repo_store, 'to/')
144+ self.assertFalse(store.is_repository_available(to_path))
145 store.init_repo(to_path, clone_from=self.orig_path)
146+ self.assertTrue(store.is_repository_available(to_path))
147+
148 self.assertAllLinkCounts(2, self.orig_objs)
149 to = pygit2.Repository(to_path)
150 to_blob = to.create_blob(b'to')
151diff --git a/turnip/api/views.py b/turnip/api/views.py
152index 2e8a082..37e89ef 100644
153--- a/turnip/api/views.py
154+++ b/turnip/api/views.py
155@@ -53,9 +53,10 @@ class RepoAPI(BaseAPI):
156
157 def collection_post(self):
158 """Initialise a new git repository, or clone from an existing repo."""
159- repo_path = extract_json_data(self.request).get('repo_path')
160- clone_path = extract_json_data(self.request).get('clone_from')
161- clone_refs = extract_json_data(self.request).get('clone_refs', False)
162+ json_data = extract_json_data(self.request)
163+ repo_path = json_data.get('repo_path')
164+ clone_path = json_data.get('clone_from')
165+ clone_refs = json_data.get('clone_refs', False)
166
167 if not repo_path:
168 self.request.errors.add('body', 'repo_path',
169@@ -88,6 +89,7 @@ class RepoAPI(BaseAPI):
170 raise exc.HTTPNotFound()
171 return {
172 'default_branch': store.get_default_branch(repo_path),
173+ 'is_available': store.is_repository_available(repo_path)
174 }
175
176 def _patch_default_branch(self, repo_path, value):

Subscribers

People subscribed via source and target branches