Merge ~pappacena/turnip:copy-ref-helper into turnip:master

Proposed by Thiago F. Pappacena
Status: Merged
Approved by: Thiago F. Pappacena
Approved revision: 618b35c3f6ecb9883397ff4abaceee114c732046
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~pappacena/turnip:copy-ref-helper
Merge into: turnip:master
Prerequisite: ~pappacena/turnip:celery-test-fixture
Diff against target: 188 lines (+146/-2)
2 files modified
turnip/api/store.py (+54/-0)
turnip/api/tests/test_store.py (+92/-2)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+390205@code.launchpad.net

Commit message

Celery task to copy refs between repositories and delete them

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

Pushed the requested changes.

Revision history for this message
Colin Watson (cjwatson) :
review: Approve
Revision history for this message
Thiago F. Pappacena (pappacena) :
Revision history for this message
Otto Co-Pilot (otto-copilot) wrote :

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 f7909c7..d3e3985 100644
3--- a/turnip/api/store.py
4+++ b/turnip/api/store.py
5@@ -9,6 +9,7 @@ import re
6 import shutil
7 import subprocess
8 import uuid
9+from collections import defaultdict
10
11 from contextlib2 import (
12 contextmanager,
13@@ -120,6 +121,59 @@ def write_alternates(repo_path, alternate_repo_paths):
14 object_dir_re = re.compile(r'\A[0-9a-f][0-9a-f]\Z')
15
16
17+@app.task
18+def fetch_refs(operations):
19+ """Copy a set of refs from one git repository to another.
20+
21+ This is implemented now using git client's "git fetch" command,
22+ since it's way easier than trying to copy the refs, commits and objects
23+ manually using pygit.
24+
25+ :param operations: A list of tuples describing the copy operations,
26+ in the format (from_root, from_ref, to_root, to_ref). If "to_ref" is
27+ None, the target ref will have the same name as the source ref.
28+ """
29+ # Group copy operations by source/dest repositories pairs.
30+ grouped_refs = defaultdict(set)
31+ for from_root, from_ref, to_root, to_ref in operations:
32+ grouped_refs[(from_root, to_root)].add((from_ref, to_ref))
33+
34+ # A pair of (cmd, stderr) errors happened during the copy.
35+ errors = []
36+ for repo_pair, refs_pairs in grouped_refs.items():
37+ from_root, to_root = repo_pair
38+ cmd = [b'git', b'fetch', b'--no-tags', from_root]
39+ cmd += [b"%s:%s" % (a, b if b else a) for a, b in refs_pairs]
40+
41+ # XXX pappacena: On Python3, this could be replaced with
42+ # stdout=subprocess.DEVNULL.
43+ with open(os.devnull, 'wb') as devnull:
44+ proc = subprocess.Popen(
45+ cmd, cwd=to_root,
46+ stdout=devnull, stderr=subprocess.PIPE)
47+ _, stderr = proc.communicate()
48+ if proc.returncode != 0:
49+ errors.append(cmd, stderr)
50+
51+ if errors:
52+ details = b"\n ".join(b"%s = %s" % (cmd, err) for cmd, err in errors)
53+ raise GitError(b"Error copying refs: %s" % details)
54+
55+
56+@app.task
57+def delete_refs(operations):
58+ """Remove refs from repositories.
59+
60+ :param operations: A list of tuples (repo_root, ref_name) to be deleted.
61+ """
62+ repos = {}
63+ for repo_root, ref_name in operations:
64+ if repo_root not in repos:
65+ repos[repo_root] = Repository(repo_root)
66+ repo = repos[repo_root]
67+ repo.references[ref_name].delete()
68+
69+
70 def copy_refs(from_root, to_root):
71 """Copy refs from one .git directory to another.
72
73diff --git a/turnip/api/tests/test_store.py b/turnip/api/tests/test_store.py
74index 02ed3fd..d12216b 100644
75--- a/turnip/api/tests/test_store.py
76+++ b/turnip/api/tests/test_store.py
77@@ -26,6 +26,7 @@ from turnip.api.tests.test_helpers import (
78 open_repo,
79 RepoFactory,
80 )
81+from turnip.tests.tasks import CeleryWorkerFixture
82
83
84 class InitTestCase(TestCase):
85@@ -95,8 +96,9 @@ class InitTestCase(TestCase):
86
87 def makeOrig(self):
88 self.orig_path = os.path.join(self.repo_store, 'orig/')
89- orig = RepoFactory(
90- self.orig_path, num_branches=3, num_commits=2, num_tags=2).build()
91+ self.orig_factory = RepoFactory(
92+ self.orig_path, num_branches=3, num_commits=2, num_tags=2)
93+ orig = self.orig_factory.build()
94 self.orig_refs = {}
95 for ref in orig.references.objects:
96 obj = orig[ref.target]
97@@ -356,3 +358,91 @@ class InitTestCase(TestCase):
98 self.orig_refs, os.path.join(to_path, 'turnip-subordinate'))
99 self.assertPackedRefs(
100 packed_refs, os.path.join(too_path, 'turnip-subordinate'))
101+
102+ def test_fetch_refs(self):
103+ celery_fixture = CeleryWorkerFixture()
104+ self.useFixture(celery_fixture)
105+
106+ self.makeOrig()
107+ # Creates a new branch in the orig repository.
108+ orig_path = self.orig_path
109+ orig = self.orig_factory.repo
110+ master_tip = orig.references[b'refs/heads/master'].target.hex
111+
112+ orig_branch_name = b'new-branch'
113+ orig_ref_name = b'refs/heads/new-branch'
114+ orig.create_branch(orig_branch_name, orig[master_tip])
115+ orig_commit_oid = self.orig_factory.add_commit(
116+ b'foobar file content', 'foobar.txt', parents=[master_tip],
117+ ref=orig_ref_name)
118+ orig_blob_id = orig[orig_commit_oid].tree[0].id
119+
120+ dest_path = os.path.join(self.repo_store, 'to/')
121+ store.init_repo(dest_path, clone_from=self.orig_path)
122+
123+ dest = pygit2.Repository(dest_path)
124+ self.assertEqual([], dest.references.objects)
125+
126+ dest_ref_name = b'refs/merge/123'
127+ store.fetch_refs.apply_async(args=([
128+ (orig_path, orig_commit_oid.hex, dest_path, dest_ref_name)], ))
129+ celery_fixture.waitUntil(5, lambda: len(dest.references.objects) == 1)
130+
131+ self.assertEqual(1, len(dest.references.objects))
132+ copied_ref = dest.references.objects[0]
133+ self.assertEqual(dest_ref_name, copied_ref.name)
134+ self.assertEqual(
135+ orig.references[orig_ref_name].target,
136+ dest.references[dest_ref_name].target)
137+ self.assertEqual(b'foobar file content', dest[orig_blob_id].data)
138+
139+ # Updating and copying again should work too, and it should be
140+ # compatible with using the ref name instead of the commit ID too.
141+ orig_commit_oid = self.orig_factory.add_commit(
142+ b'changed foobar content', 'foobar.txt', parents=[orig_commit_oid],
143+ ref=orig_ref_name)
144+ orig_blob_id = orig[orig_commit_oid].tree[0].id
145+
146+ store.fetch_refs.apply_async(args=([
147+ (orig_path, orig_ref_name, dest_path, dest_ref_name)], ))
148+
149+ def waitForNewCommit():
150+ try:
151+ return dest[orig_blob_id].data == b'changed foobar content'
152+ except KeyError:
153+ return False
154+ celery_fixture.waitUntil(5, waitForNewCommit)
155+
156+ self.assertEqual(1, len(dest.references.objects))
157+ copied_ref = dest.references.objects[0]
158+ self.assertEqual(dest_ref_name, copied_ref.name)
159+ self.assertEqual(
160+ orig.references[orig_ref_name].target,
161+ dest.references[dest_ref_name].target)
162+ self.assertEqual(b'changed foobar content', dest[orig_blob_id].data)
163+
164+ def test_delete_ref(self):
165+ celery_fixture = CeleryWorkerFixture()
166+ self.useFixture(celery_fixture)
167+
168+ self.makeOrig()
169+ orig_path = self.orig_path
170+ orig = self.orig_factory.repo
171+
172+ master_tip = orig.references[b'refs/heads/master'].target.hex
173+ new_branch_name = b'new-branch'
174+ new_ref_name = b'refs/heads/new-branch'
175+ orig.create_branch(new_branch_name, orig[master_tip])
176+ self.orig_factory.add_commit(
177+ b'foobar file content', 'foobar.txt', parents=[master_tip],
178+ ref=new_ref_name)
179+
180+ before_refs_len = len(orig.references.objects)
181+ operations = [(orig_path, new_ref_name)]
182+ store.delete_refs.apply_async((operations, ))
183+ celery_fixture.waitUntil(
184+ 5, lambda: len(orig.references.objects) < before_refs_len)
185+
186+ self.assertEqual(before_refs_len - 1, len(orig.references.objects))
187+ self.assertNotIn(
188+ new_branch_name, [i.name for i in orig.references.objects])

Subscribers

People subscribed via source and target branches