Merge lp:~blr/turnip/repack-api into lp:turnip

Proposed by Kit Randel on 2015-04-24
Status: Merged
Approved by: Kit Randel on 2015-05-26
Approved revision: 169
Merged at revision: 160
Proposed branch: lp:~blr/turnip/repack-api
Merge into: lp:turnip
Prerequisite: lp:~blr/turnip/api-init-with-alternates
Diff against target: 380 lines (+159/-15)
8 files modified
.bzrignore (+6/-6)
git.config.yaml (+8/-0)
turnip/api/store.py (+34/-0)
turnip/api/tests/test_api.py (+36/-5)
turnip/api/tests/test_helpers.py (+27/-1)
turnip/api/tests/test_store.py (+12/-0)
turnip/api/views.py (+31/-0)
turnip/pack/helpers.py (+5/-3)
To merge this branch: bzr merge lp:~blr/turnip/repack-api
Reviewer Review Type Date Requested Status
William Grant code 2015-04-24 Approve on 2015-05-21
Review via email: mp+257312@code.launchpad.net

Commit Message

Provides API for git repack, and per-repository configuration.

Description of the Change

Provides API for git repack, and per-repository configuration.

Per-repository config defaults are read from git.config.yaml.

To post a comment you must log in.
Kit Randel (blr) wrote :

Provides API for git repack.

Once support for per-repository configuration lands, this will potentially need to be refactored.

William Grant (wgrant) wrote :

git repack -l prevents inclusion of data from alternates. We also may want -f to force deltas to be recalculated.

lp:~blr/turnip/repack-api updated on 2015-05-21
158. By Kit Randel on 2015-05-21

Add git.config.yaml and fix imports.

William Grant (wgrant) wrote :

Repack can be a lengthy operation, so running it synchronously within an HTTP request is less than ideal, but it'll do until we have a job system of some kind. I'd like to see how it handles long requests, though -- if the connection dies, does the repack continue?

review: Approve (code)
lp:~blr/turnip/repack-api updated on 2015-05-26
159. By Kit Randel on 2015-05-21

Only extract json once is view.

160. By Kit Randel on 2015-05-21

Add -q to suppress output from repack.

161. By Kit Randel on 2015-05-21

* Add repack depth and window.
* ensure_config before repack.

162. By Kit Randel on 2015-05-26

* Add test to verify commits in separate packs exist in repacked pack.
* Set head/master to oid of commit in factory.add_commit().

163. By Kit Randel on 2015-05-26

Call pack-objects once.

164. By Kit Randel on 2015-05-26

Add inline comment on git gc.

165. By Kit Randel on 2015-05-26

Pass revlist to stdout.

166. By Kit Randel on 2015-05-26

Use subprocess.communicate rather than writing revlist.

167. By Kit Randel on 2015-05-26

Add test_helper factory.packs property.

168. By Kit Randel on 2015-05-26

Inline comment.

169. By Kit Randel on 2015-05-26

Assert repack results in 1 pack.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2015-02-17 21:32:29 +0000
3+++ .bzrignore 2015-05-26 07:47:04 +0000
4@@ -1,13 +1,13 @@
5 bin
6+build
7 develop-eggs
8+dist
9 download-cache
10 eggs
11+*.egg*
12+*.egg-info
13 .installed.cfg
14+*.log
15 parts
16-*.egg-info
17 tags
18-TAGS
19-build
20-*.egg
21-dist
22-*.log
23\ No newline at end of file
24+TAGS
25\ No newline at end of file
26
27=== added file 'git.config.yaml'
28--- git.config.yaml 1970-01-01 00:00:00 +0000
29+++ git.config.yaml 2015-05-26 07:47:04 +0000
30@@ -0,0 +1,8 @@
31+# Per-repository configuration
32+# See http://git-scm.com/docs/git-config.html for more info
33+
34+core.logallrefupdates: True
35+pack.depth: 100
36+pack.window: 100
37+pack.windowMemory: 2g
38+repack.writeBitmaps: True
39
40=== modified file 'turnip/api/store.py'
41--- turnip/api/store.py 2015-05-19 12:55:00 +0000
42+++ turnip/api/store.py 2015-05-26 07:47:04 +0000
43@@ -7,6 +7,7 @@
44 import itertools
45 import os
46 import shutil
47+import subprocess
48 import uuid
49
50 from pygit2 import (
51@@ -22,6 +23,8 @@
52 Repository,
53 )
54
55+from turnip.pack.helpers import ensure_config
56+
57
58 REF_TYPE_NAME = {
59 GIT_OBJ_COMMIT: 'commit',
60@@ -127,6 +130,7 @@
61
62 if alternate_repo_paths:
63 write_alternates(repo_path, alternate_repo_paths)
64+ ensure_config(repo_path) # set repository configuration defaults
65 return repo_path
66
67
68@@ -172,6 +176,36 @@
69 shutil.rmtree(repo_path)
70
71
72+def repack(repo_path, ignore_alternates=False, single=False,
73+ prune=False, no_reuse_delta=False, window=None, depth=None):
74+ """Repack a repository with git-repack.
75+
76+ :param ignore_alternates: Only repack local refs (git repack --local).
77+ :param single: Create a single packfile (git repack -a).
78+ :param prune: Remove redundant packs. (git repack -d)
79+ :param no_reuse_delta: Force delta recalculation.
80+ """
81+ ensure_config(repo_path)
82+
83+ repack_args = ['git', 'repack', '-q']
84+ if ignore_alternates:
85+ repack_args.append('-l')
86+ if no_reuse_delta:
87+ repack_args.append('-f')
88+ if prune:
89+ repack_args.append('-d')
90+ if single:
91+ repack_args.append('-a')
92+ if window:
93+ repack_args.append('--window', window)
94+ if depth:
95+ repack_args.append('--depth', depth)
96+
97+ return subprocess.check_call(
98+ repack_args, cwd=repo_path,
99+ stderr=subprocess.PIPE, stdout=subprocess.PIPE)
100+
101+
102 def get_refs(repo_store, repo_name):
103 """Return all refs for a git repository."""
104 with open_repo(repo_store, repo_name) as repo:
105
106=== modified file 'turnip/api/tests/test_api.py'
107--- turnip/api/tests/test_api.py 2015-05-19 12:55:00 +0000
108+++ turnip/api/tests/test_api.py 2015-05-26 07:47:04 +0000
109@@ -3,6 +3,7 @@
110 from __future__ import print_function
111
112 import os
113+import subprocess
114 from textwrap import dedent
115 import unittest
116 import uuid
117@@ -16,6 +17,7 @@
118
119 from turnip import api
120 from turnip.api.tests.test_helpers import (
121+ chdir,
122 get_revlist,
123 open_repo,
124 RepoFactory,
125@@ -117,7 +119,6 @@
126 """Merge diff can be requested across 2 repositories."""
127 factory = RepoFactory(self.repo_store)
128 c1 = factory.add_commit('foo', 'foobar.txt')
129- factory.set_head(c1)
130
131 repo2_name = uuid.uuid4().hex
132 factory2 = RepoFactory(
133@@ -133,7 +134,6 @@
134 """Diff can be requested across 2 repositories."""
135 factory = RepoFactory(self.repo_store)
136 c1 = factory.add_commit('foo', 'foobar.txt')
137- factory.set_head(c1)
138
139 repo2_name = uuid.uuid4().hex
140 factory2 = RepoFactory(
141@@ -155,7 +155,6 @@
142 """Cross repo diff with an invalid commit returns HTTP 404."""
143 factory = RepoFactory(self.repo_store)
144 c1 = factory.add_commit('foo', 'foobar.txt')
145- factory.set_head(c1)
146
147 repo2_name = uuid.uuid4().hex
148 RepoFactory(
149@@ -201,7 +200,7 @@
150
151 resp = self.app.get('/repo/{}/refs'.format(self.repo_path))
152 refs = resp.json
153- self.assertEqual(0, len(refs.keys()))
154+ self.assertEqual(1, len(refs.keys()))
155
156 def test_allow_unicode_refs(self):
157 """Ensure unicode refs are included in ref collection."""
158@@ -213,7 +212,7 @@
159
160 resp = self.app.get('/repo/{}/refs'.format(self.repo_path))
161 refs = resp.json
162- self.assertEqual(1, len(refs.keys()))
163+ self.assertEqual(2, len(refs.keys()))
164
165 def test_repo_get_ref(self):
166 RepoFactory(self.repo_store, num_commits=1).build()
167@@ -510,6 +509,38 @@
168 self.assertEqual(5, len(resp.json))
169 self.assertNotIn(excluded_commit, resp.json)
170
171+ def test_repo_repack_verify_pack(self):
172+ """Ensure commit exists in pack."""
173+ factory = RepoFactory(self.repo_store)
174+ oid = factory.add_commit('foo', 'foobar.txt')
175+ resp = self.app.post_json('/repo/{}/repack'.format(self.repo_path),
176+ {'prune': True, 'single': True})
177+ for filename in factory.packs:
178+ pack = os.path.join(factory.pack_dir, filename)
179+ out = subprocess.check_output(['git', 'verify-pack', pack, '-v'])
180+ self.assertEqual(200, resp.status_code)
181+ self.assertIn(oid.hex, out)
182+
183+ def test_repo_repack_verify_commits_to_pack(self):
184+ """Ensure commits in different packs exist in merged pack."""
185+ factory = RepoFactory(self.repo_store)
186+ oid = factory.add_commit('foo', 'foobar.txt')
187+ with chdir(factory.pack_dir):
188+ subprocess.call(['git', 'gc', '-q']) # pack first commit
189+ oid2 = factory.add_commit('bar', 'foobar.txt', [oid])
190+ p = subprocess.Popen(['git', 'pack-objects', '-q', 'pack2'],
191+ stdin=subprocess.PIPE, stdout=subprocess.PIPE)
192+ p.communicate(input=oid2.hex)
193+ self.assertEqual(2, len(factory.packs)) # ensure 2 packs exist
194+ self.app.post_json('/repo/{}/repack'.format(self.repo_path),
195+ {'prune': True, 'single': True})
196+ self.assertEqual(1, len(factory.packs))
197+ repacked_pack = os.path.join(factory.pack_dir, factory.packs[0])
198+ out = subprocess.check_output(['git', 'verify-pack',
199+ repacked_pack, '-v'])
200+ self.assertIn(oid.hex, out)
201+ self.assertIn(oid2.hex, out)
202+
203
204 if __name__ == '__main__':
205 unittest.main()
206
207=== modified file 'turnip/api/tests/test_helpers.py'
208--- turnip/api/tests/test_helpers.py 2015-04-22 21:41:44 +0000
209+++ turnip/api/tests/test_helpers.py 2015-05-26 07:47:04 +0000
210@@ -1,5 +1,7 @@
211 # Copyright 2015 Canonical Ltd. All rights reserved.
212
213+import contextlib
214+import fnmatch
215 import itertools
216 import os
217 import urllib
218@@ -28,6 +30,17 @@
219 return Repository(repo_path)
220
221
222+@contextlib.contextmanager
223+def chdir(dirname=None):
224+ curdir = os.getcwd()
225+ try:
226+ if dirname is not None:
227+ os.chdir(dirname)
228+ yield
229+ finally:
230+ os.chdir(curdir)
231+
232+
233 class RepoFactory():
234 """Builds a git repository in a user defined state."""
235
236@@ -41,6 +54,7 @@
237 self.num_commits = num_commits
238 self.num_tags = num_tags
239 self.repo_path = repo_path
240+ self.pack_dir = os.path.join(repo_path, '.git', 'objects', 'pack')
241 if clone_from:
242 self.repo = self.clone_repo(clone_from)
243 else:
244@@ -52,6 +66,12 @@
245 last = self.repo[self.repo.head.target]
246 return list(self.repo.walk(last.id, GIT_SORT_TIME))
247
248+ @property
249+ def packs(self):
250+ """Return list of pack files."""
251+ return [filename for filename in fnmatch.filter(
252+ os.listdir(self.pack_dir), '*.pack')]
253+
254 def add_commit(self, blob_content, file_path, parents=[],
255 ref=None, author=None, committer=None):
256 """Create a commit from blob_content and file_path."""
257@@ -66,10 +86,16 @@
258 tree_id = repo.index.write_tree()
259 oid = repo.create_commit(ref, author, committer,
260 blob_content, tree_id, parents)
261+ self.set_head(oid) # set master
262 return oid
263
264 def set_head(self, oid):
265- self.repo.create_reference('refs/heads/master', oid)
266+ try:
267+ master_ref = self.repo.lookup_reference('refs/heads/master')
268+ except KeyError:
269+ master_ref = self.repo.create_reference('refs/heads/master', oid)
270+ finally:
271+ master_ref.set_target(oid)
272
273 def add_branch(self, name, oid):
274 commit = self.repo.get(oid)
275
276=== modified file 'turnip/api/tests/test_store.py'
277--- turnip/api/tests/test_store.py 2015-04-29 02:43:28 +0000
278+++ turnip/api/tests/test_store.py 2015-05-26 07:47:04 +0000
279@@ -16,6 +16,7 @@
280 )
281 import pygit2
282 from testtools import TestCase
283+import yaml
284
285 from turnip.api import store
286 from turnip.api.tests.test_helpers import (
287@@ -74,6 +75,17 @@
288 r = pygit2.Repository(path)
289 self.assertEqual([], r.listall_references())
290
291+ def test_repo_config(self):
292+ """Assert repository is initialised with correct config defaults."""
293+ repo_path = store.init_repo(self.repo_path)
294+ repo_config = pygit2.Repository(repo_path).config
295+ yaml_config = yaml.load(open('git.config.yaml'))
296+
297+ self.assertEqual(bool(yaml_config['core.logallrefupdates']),
298+ bool(repo_config['core.logallrefupdates']))
299+ self.assertEqual(str(yaml_config['pack.depth']),
300+ repo_config['pack.depth'])
301+
302 def test_open_ephemeral_repo(self):
303 """Opening a repo where a repo name contains ':' should return
304 a new ephemeral repo.
305
306=== modified file 'turnip/api/views.py'
307--- turnip/api/views.py 2015-05-19 12:55:00 +0000
308+++ turnip/api/views.py 2015-05-26 07:47:04 +0000
309@@ -2,6 +2,7 @@
310
311 import os
312 import re
313+from subprocess import CalledProcessError
314
315 from cornice.resource import resource
316 from cornice.util import extract_json_data
317@@ -123,6 +124,36 @@
318 return exc.HTTPNotFound() # 404
319
320
321+@resource(path='/repo/{name}/repack')
322+class RepackAPI(BaseAPI):
323+ """Provides HTTP API for repository repacking."""
324+
325+ def __init__(self, request):
326+ super(RepackAPI, self).__init__()
327+ self.request = request
328+
329+ @validate_path
330+ def post(self, repo_store, repo_name):
331+ repo_path = os.path.join(repo_store, repo_name)
332+
333+ data = extract_json_data(self.request)
334+ ignore_alternates = data.get('ignore_alternates')
335+ no_reuse_delta = data.get('no_reuse_delta')
336+ prune = data.get('prune')
337+ single = data.get('single')
338+ window = data.get('window')
339+ depth = data.get('depth')
340+
341+ try:
342+ store.repack(repo_path, single=single, prune=prune,
343+ no_reuse_delta=no_reuse_delta,
344+ ignore_alternates=ignore_alternates,
345+ window=window, depth=depth)
346+ except (CalledProcessError):
347+ return exc.HTTPInternalServerError()
348+ return
349+
350+
351 @resource(collection_path='/repo/{name}/refs',
352 path='/repo/{name}/refs/{ref:.*}')
353 class RefAPI(BaseAPI):
354
355=== modified file 'turnip/pack/helpers.py'
356--- turnip/pack/helpers.py 2015-05-06 11:46:43 +0000
357+++ turnip/pack/helpers.py 2015-05-26 07:47:04 +0000
358@@ -14,6 +14,7 @@
359 )
360
361 from pygit2 import Repository
362+import yaml
363
364 import turnip.pack.hooks
365
366@@ -104,10 +105,11 @@
367 pygit2.Config handles locking itself, so we don't need to think too hard
368 about concurrency.
369 """
370-
371+ config_file = open('git.config.yaml')
372+ git_config_defaults = yaml.load(config_file)
373 config = Repository(repo_root).config
374- config['core.logallrefupdates'] = True
375- config['repack.writeBitmaps'] = True
376+ for key, val in git_config_defaults.iteritems():
377+ config[key] = val
378
379
380 def ensure_hooks(repo_root):

Subscribers

People subscribed via source and target branches