Merge ~racb/usd-importer:repo-builder into usd-importer:master

Proposed by Robie Basak on 2017-10-31
Status: Merged
Approved by: Robie Basak on 2017-11-13
Approved revision: 80b7dc41bcfab583a03367d02df113ac8fdcbf4e
Merged at revision: 80b7dc41bcfab583a03367d02df113ac8fdcbf4e
Proposed branch: ~racb/usd-importer:repo-builder
Merge into: usd-importer:master
Diff against target: 368 lines (+339/-5)
3 files modified
gitubuntu/git_repository.py (+4/-5)
gitubuntu/repo_builder.py (+250/-0)
gitubuntu/test_git_repository.py (+85/-0)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve on 2017-11-13
Nish Aravamudan 2017-10-31 Approve on 2017-11-10
Review via email: mp+333040@code.launchpad.net

Commit Message

Make jenkins happy.

To post a comment you must log in.
Robie Basak (racb) wrote :

Perhaps I should s/Link/Symlink/.

FAILED: Continuous integration, rev:91e2d9ffacb7ef0a3fc38965201630f5d9dab948
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https://code.launchpad.net/~racb/usd-importer/+git/usd-importer/+merge/333040/+edit-commit-message

https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/190/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Style Check
    SUCCESS: Unit Tests
    FAILED: Integration Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/190/rebuild

review: Needs Fixing (continuous-integration)
Nish Aravamudan (nacc) wrote :

This is not your fault, master is broken due to a typo on my part.

On Tue, Oct 31, 2017 at 11:23 AM, Server Team CI bot
<email address hidden> wrote:
> Review: Needs Fixing continuous-integration
>
> FAILED: Continuous integration, rev:91e2d9ffacb7ef0a3fc38965201630f5d9dab948
> No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
> https://code.launchpad.net/~racb/usd-importer/+git/usd-importer/+merge/333040/+edit-commit-message
>
> https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/190/
> Executed test runs:
> SUCCESS: Checkout
> SUCCESS: Style Check
> SUCCESS: Unit Tests
> FAILED: Integration Tests
>
> Click here to trigger a rebuild:
> https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/190/rebuild
>
> --
> https://code.launchpad.net/~racb/usd-importer/+git/usd-importer/+merge/333040
> Your team Ubuntu Server Dev import team is requested to review the proposed merge of ~racb/usd-importer:repo-builder into usd-importer:master.

Robie Basak (racb) wrote :

I intend to squash these two commits up before landing, but I've left them here to allow incremental review.

Nish Aravamudan (nacc) wrote :

On 31.10.2017 [17:30:59 -0000], Robie Basak wrote:
> Perhaps I should s/Link/Symlink/.

Link is missing some super() / **kwargs-foo.

Otherwise, the code seems really nice and clean. +1 from me on the
direction.

--
Nishanth Aravamudan
Ubuntu Server
Canonical Ltd

Nish Aravamudan (nacc) wrote :

Oh sorry Robie, I wasn't notified of the changes, just your comment. Weird.

If you rebase to master now, it hsould pass CI.

FAILED: Continuous integration, rev:3bb6b1ccbdb0093fa5e614126b9ec039ea09d248
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https://code.launchpad.net/~racb/usd-importer/+git/usd-importer/+merge/333040/+edit-commit-message

https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/191/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Style Check
    SUCCESS: Unit Tests
    FAILED: Integration Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/191/rebuild

review: Needs Fixing (continuous-integration)

FAILED: Continuous integration, rev:78b22fae3c034b83a35e47eeabe96ef5a95d85a7
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https://code.launchpad.net/~racb/usd-importer/+git/usd-importer/+merge/333040/+edit-commit-message

https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/192/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Style Check
    SUCCESS: Unit Tests
    SUCCESS: Integration Tests
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/192/rebuild

review: Needs Fixing (continuous-integration)

FAILED: Continuous integration, rev:78b22fae3c034b83a35e47eeabe96ef5a95d85a7
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https://code.launchpad.net/~racb/usd-importer/+git/usd-importer/+merge/333040/+edit-commit-message

https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/193/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Style Check
    SUCCESS: Unit Tests
    SUCCESS: Integration Tests
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/193/rebuild

review: Needs Fixing (continuous-integration)

FAILED: Continuous integration, rev:333f5306ad7249f8cc4062fb87671708f49f753b
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/199/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Style Check
    SUCCESS: Unit Tests
    FAILED: Integration Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/199/rebuild

review: Needs Fixing (continuous-integration)

PASSED: Continuous integration, rev:333f5306ad7249f8cc4062fb87671708f49f753b
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/200/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Style Check
    SUCCESS: Unit Tests
    SUCCESS: Integration Tests
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/200/rebuild

review: Approve (continuous-integration)
Nish Aravamudan (nacc) wrote :

Robie,

Do you want me to land this?

Nish Aravamudan (nacc) :
review: Approve

PASSED: Continuous integration, rev:80b7dc41bcfab583a03367d02df113ac8fdcbf4e
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/209/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Style Check
    SUCCESS: Unit Tests
    SUCCESS: Integration Tests
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/209/rebuild

review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/gitubuntu/git_repository.py b/gitubuntu/git_repository.py
2index 86d1a26..c598c10 100644
3--- a/gitubuntu/git_repository.py
4+++ b/gitubuntu/git_repository.py
5@@ -73,12 +73,11 @@ def _follow_symlinks_to_blob(repo, top_tree_object, search_path,
6 elif entry.type == 'blob' and entry.filemode == pygit2.GIT_FILEMODE_LINK:
7 # Found a symlink. Start again from the top with adjustment for symlink
8 # following
9+ target_tail = [decode_binary(repo.get(entry.id).data)]
10+ if tail is not None:
11+ target_tail.append(tail)
12 search_path = posixpath.normpath(
13- posixpath.join(
14- _rel_path,
15- decode_binary(repo.get(entry.id).data),
16- tail,
17- )
18+ posixpath.join(_rel_path, *target_tail)
19 )
20 return _follow_symlinks_to_blob(
21 repo=repo,
22diff --git a/gitubuntu/repo_builder.py b/gitubuntu/repo_builder.py
23new file mode 100644
24index 0000000..cb93278
25--- /dev/null
26+++ b/gitubuntu/repo_builder.py
27@@ -0,0 +1,250 @@
28+import functools
29+import tempfile
30+import unittest
31+
32+import pygit2
33+import pytest
34+
35+
36+"""Build test git repositories as data structures
37+
38+Represent a git repository as a data structure, nesting using Tree objects as
39+required. Name objects in a flat namespace using the name keyword object to
40+these constructors.
41+
42+Reference the same object with a Placeholder giving the name of the target to
43+its constructor. This allows a build in a single data structure instead of
44+having to use multiple statements to grab references to other parts of the same
45+structure.
46+
47+Write any object (including any children as necessary) using the write method,
48+which takes a single pygit2.Repository parameter. This returns the pygit2.Oid
49+of the top level written object.
50+
51+Objects must be treated as immutable. The result of mutating an object after it has been constructed is undefined.
52+"""
53+
54+
55+__all__ = [
56+ 'Blob',
57+ 'Commit',
58+ 'ExecutableBlob',
59+ 'Symlink',
60+ 'Placeholder',
61+ 'Tree',
62+]
63+
64+# General notes
65+
66+# Sometimes tests need to check an object hash matches something in a supplied
67+# builder repo tree. In this case, it seems redundant and messy to have to
68+# write that object again to get its hash. Perhaps objects should cache their
69+# hashes or something?
70+
71+
72+class NamedMixin:
73+ def __init__(self, name=None):
74+ self.name = name
75+
76+
77+class WriteMixin:
78+ def write(self, repo, record=None):
79+ record = record or dict()
80+ try:
81+ return record[self]
82+ except KeyError:
83+ record[self] = self._obj_to_oid(repo, record)
84+ return record[self]
85+
86+
87+class Placeholder:
88+ def __init__(self, target_name):
89+ self.target_name = target_name
90+
91+ def walk(self, parent=None):
92+ yield parent, self
93+
94+
95+class Blob(NamedMixin, WriteMixin):
96+ GIT_TREE_ATTR = pygit2.GIT_FILEMODE_BLOB
97+
98+ def __init__(self, content, **kwargs):
99+ """Construct a Blob
100+
101+ @param content: a bytes object of the desired blob contents
102+ """
103+ super().__init__(**kwargs)
104+ self.content = content
105+
106+ def walk(self, parent=None):
107+ yield parent, self
108+
109+ def _obj_to_oid(self, repo, record):
110+ return repo.create_blob(self.content)
111+
112+
113+class ExecutableBlob(Blob):
114+ GIT_TREE_ATTR = pygit2.GIT_FILEMODE_BLOB_EXECUTABLE
115+
116+
117+class Symlink(NamedMixin, WriteMixin):
118+ GIT_TREE_ATTR = pygit2.GIT_FILEMODE_LINK
119+
120+ def __init__(self, target, **kwargs):
121+ """Construct a symlink
122+
123+ @param target: a bytes object containing the target
124+
125+ target is not interpreted; it is exactly equivalent to the first
126+ parameter to ln(1).
127+ """
128+ super().__init__(**kwargs)
129+ self.target = target
130+
131+ def walk(self, parent=None):
132+ yield parent, self
133+
134+ def _obj_to_oid(self, repo, record):
135+ return repo.create_blob(self.target)
136+
137+
138+class Tree(NamedMixin, WriteMixin):
139+ GIT_TREE_ATTR = pygit2.GIT_FILEMODE_TREE
140+
141+ def __init__(self, entries, **kwargs):
142+ """Construct a tree
143+
144+ A tree is equivalent to a directory and contains zero or more entries
145+
146+ @param entries: a dictionary whose values are other objects from this
147+ module
148+ """
149+ super().__init__(**kwargs)
150+ self.entries = entries
151+
152+ def __getitem__(self, k):
153+ if k == self.name:
154+ return self
155+
156+ # XXX inefficient
157+ for entry in self.entries.values():
158+ if isinstance(entry, Placeholder):
159+ # As an exception, Placeholders don't have names so cannot be
160+ # located using this method.
161+ continue
162+ if entry.name == k:
163+ return entry
164+ elif isinstance(entry, type(self)):
165+ try:
166+ return entry[k]
167+ except KeyError:
168+ pass
169+ raise KeyError('Object named %r not found' % k)
170+
171+ def replace(self, old, new):
172+ # XXX inefficient
173+ for name, entry in self.entries.items():
174+ if entry is old:
175+ self.entries[name] = new
176+ return
177+ raise KeyError("Cannot find %r in entries" % old)
178+
179+ def walk(self, parent=None):
180+ for entry in self.entries.values():
181+ yield from entry.walk(parent=self)
182+ yield parent, self
183+
184+ def _obj_to_oid(self, repo, record):
185+ tree_builder = repo.TreeBuilder()
186+ for name, entry in self.entries.items():
187+ tree_builder.insert(
188+ name, # name
189+ entry.write(repo, record=record), # oid
190+ entry.GIT_TREE_ATTR, # attr
191+ )
192+ return tree_builder.write()
193+
194+
195+class Commit(NamedMixin, WriteMixin):
196+ def __init__(self, tree, **kwargs):
197+ super().__init__(**kwargs)
198+ self.tree = tree
199+
200+ def walk(self, parent=None):
201+ yield parent, self
202+
203+ def _obj_to_oid(self, repo, record=None):
204+ return repo.create_commit(
205+ None,
206+ pygit2.Signature(name='Test Builder', email='test@example.com'),
207+ pygit2.Signature(name='Test Builder', email='test@example.com'),
208+ 'Test commit',
209+ self.tree.write(repo),
210+ [],
211+ )
212+
213+ def __getitem__(self, k):
214+ return self if self.name == k else self.tree[k]
215+
216+
217+def replace_placeholders(top):
218+ for parent, obj in top.walk():
219+ if isinstance(obj, Placeholder):
220+ parent.replace(obj, top[obj.target_name])
221+
222+
223+class TestObjectCreation(unittest.TestCase):
224+ def setUp(self):
225+ self.git_dir = tempfile.TemporaryDirectory()
226+ self.repo = pygit2.init_repository(self.git_dir.name)
227+
228+ def tearDown(self):
229+ self.repo = None
230+ self.git_dir.cleanup()
231+
232+ def testBlobCreation(self):
233+ ref = Blob(b'foo').write(self.repo)
234+ assert self.repo.get(ref).data == b'foo'
235+
236+ def testExecutableBlobTreeCreation(self):
237+ tree_ref = Tree({'foo': ExecutableBlob(b'bar')}).write(self.repo)
238+ tree_entry = self.repo.get(tree_ref)['foo']
239+ assert self.repo.get(tree_entry.id).data == b'bar'
240+ assert tree_entry.filemode == pygit2.GIT_FILEMODE_BLOB_EXECUTABLE
241+
242+ def testSymlinkTreeCreation(self):
243+ tree_ref = Tree({'foo': Symlink(b'bar')}).write(self.repo)
244+ tree_entry = self.repo.get(tree_ref)['foo']
245+ assert tree_entry.filemode == pygit2.GIT_FILEMODE_LINK
246+
247+ def testCommitCreation(self):
248+ tree = Tree({'foo': Blob(b'bar')})
249+ tree_ref = tree.write(self.repo)
250+ commit_ref = Commit(tree).write(self.repo)
251+ commit = self.repo.get(commit_ref)
252+ assert commit.tree_id == tree_ref
253+
254+ def testNameSearch(self):
255+ inner_tree = Tree({'foo': Blob(b'bar', name='blob')}, name='inner')
256+ outer_tree = Tree({'baz': inner_tree}, name='outer')
257+ commit = Commit(outer_tree, name='commit')
258+ assert commit['commit'] is commit
259+ assert commit['inner'] is inner_tree
260+ assert commit['outer'] is outer_tree
261+ assert isinstance(commit['blob'], Blob)
262+ with pytest.raises(KeyError):
263+ commit['absent']
264+
265+
266+@pytest.mark.parametrize('cls', [
267+ functools.partial(Blob, b'qux'),
268+ functools.partial(ExecutableBlob, b'qux'),
269+ functools.partial(Symlink, 'target'),
270+ functools.partial(Tree, {}),
271+])
272+def test_placeholder(cls):
273+ common_blob = cls(name='name')
274+ top = Tree({'foo': common_blob, 'bar': Placeholder('name')})
275+ assert top.entries['foo'] is not top.entries['bar']
276+ replace_placeholders(top)
277+ assert top.entries['foo'] is top.entries['bar']
278diff --git a/gitubuntu/test_git_repository.py b/gitubuntu/test_git_repository.py
279new file mode 100644
280index 0000000..640dc16
281--- /dev/null
282+++ b/gitubuntu/test_git_repository.py
283@@ -0,0 +1,85 @@
284+import tempfile
285+import unittest
286+
287+import pygit2
288+import pytest
289+
290+from gitubuntu.git_repository import follow_symlinks_to_blob
291+from gitubuntu.repo_builder import (
292+ Blob,
293+ Symlink,
294+ Tree,
295+)
296+
297+
298+@pytest.fixture
299+def repo():
300+ """A empty pygit2 repository fixture in a temporary directory"""
301+ with tempfile.TemporaryDirectory() as tmp_dir:
302+ yield pygit2.init_repository(tmp_dir)
303+
304+
305+@pytest.mark.parametrize('tree_func', [
306+ # The tree_func parameter is a function that accepts a mock Blob that is to
307+ # represent the changelog blob itself and returns a mock Tree with the mock
308+ # Blob embedded somewhere within it. The test function can then ensure that
309+ # follow_symlinks_to_blob can correctly find the changelog Blob given the
310+ # Tree.
311+
312+ # Of course this is only expected to work if, after checking out the Tree,
313+ # "cat debian/changelog" would work. But this allows us to test the various
314+ # permutations of symlink following in Trees that _are_ valid.
315+
316+ # Simple case
317+ lambda b: Tree({
318+ 'debian': Tree({'changelog': b}),
319+ }),
320+
321+ # Symlink in debian/
322+ lambda b: Tree({
323+ 'debian': Tree({
324+ 'changelog.real': b,
325+ 'changelog': Symlink('changelog.real'),
326+ }),
327+ }),
328+
329+ # Symlink to parent directory
330+ lambda b: Tree({
331+ 'changelog': b,
332+ 'debian': Tree({
333+ 'changelog': Symlink('../changelog'),
334+ })
335+ }),
336+
337+ # Symlink to subdirectory
338+ lambda b: Tree({
339+ 'debian': Tree({
340+ 'changelog': Symlink('subdirectory/changelog'),
341+ 'subdirectory': Tree({'changelog': b}),
342+ })
343+ }),
344+
345+ # debian/ itself is a symlink to a different directory
346+ lambda b: Tree({
347+ 'pkg': Tree({'changelog': b}),
348+ 'debian': Symlink('pkg'),
349+ })
350+])
351+def test_follow_symlinks_to_blob(repo, tree_func):
352+ blob = Blob(b'')
353+ blob_id = blob.write(repo)
354+ tree = repo.get(tree_func(blob).write(repo))
355+ result_blob = follow_symlinks_to_blob(repo, tree, 'debian/changelog')
356+ assert result_blob.id == blob_id
357+
358+
359+@pytest.mark.parametrize('tree', [
360+ Tree({}),
361+ Tree({'debian': Tree({})}),
362+ Tree({'debian': Tree({'changelog': Symlink('other')})}),
363+ Tree({'debian': Tree({'changelog': Symlink('../other')})}),
364+])
365+def test_follow_symlinks_to_blob_not_found(repo, tree):
366+ pygit2_tree = repo.get(tree.write(repo))
367+ with pytest.raises(KeyError):
368+ follow_symlinks_to_blob(repo, pygit2_tree, 'debian/changelog')

Subscribers

People subscribed via source and target branches