Merge lp:~jelmer/brz/branch-reference-remote into lp:brz

Proposed by Jelmer Vernooij
Status: Merged
Approved by: Jelmer Vernooij
Approved revision: no longer in the source branch.
Merge reported by: The Breezy Bot
Merged at revision: not available
Proposed branch: lp:~jelmer/brz/branch-reference-remote
Merge into: lp:brz
Diff against target: 508 lines (+163/-64)
15 files modified
breezy/branch.py (+7/-3)
breezy/bzr/branch.py (+15/-25)
breezy/bzr/fullhistory.py (+2/-0)
breezy/bzr/remote.py (+50/-15)
breezy/bzr/smart/branch.py (+14/-0)
breezy/bzr/smart/request.py (+3/-0)
breezy/git/branch.py (+2/-0)
breezy/plugins/weave_fmt/branch.py (+2/-0)
breezy/tests/blackbox/test_branch.py (+8/-7)
breezy/tests/blackbox/test_checkout.py (+1/-1)
breezy/tests/blackbox/test_pull.py (+1/-1)
breezy/tests/per_workingtree/test_workingtree.py (+20/-12)
breezy/tests/test_remote.py (+20/-0)
breezy/tests/test_smart.py (+15/-0)
doc/en/release-notes/brz-3.1.txt (+3/-0)
To merge this branch: bzr merge lp:~jelmer/brz/branch-reference-remote
Reviewer Review Type Date Requested Status
Jelmer Vernooij Approve
Review via email: mp+377806@code.launchpad.net

Commit message

Support fetching / pushing reference information to remote branches.

Description of the change

Support fetching / pushing reference information to remote branches.

To post a comment you must log in.
Revision history for this message
Jelmer Vernooij (jelmer) :
review: Approve
Revision history for this message
The Breezy Bot (the-breezy-bot) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'breezy/branch.py'
2--- breezy/branch.py 2020-01-19 03:22:04 +0000
3+++ breezy/branch.py 2020-01-19 15:24:24 +0000
4@@ -1271,7 +1271,7 @@
5 revision_id=revision_id)
6
7 def update_references(self, target):
8- if not getattr(self._format, 'supports_reference_locations', False):
9+ if not self._format.supports_reference_locations:
10 return
11 return InterBranch.get(self, target).update_references()
12
13@@ -2391,8 +2391,12 @@
14 new_base = self.target.base
15 target_reference_dict = self.target._get_all_reference_info()
16 for tree_path, (branch_location, file_id) in viewitems(reference_dict):
17- branch_location = urlutils.rebase_url(branch_location,
18- old_base, new_base)
19+ try:
20+ branch_location = urlutils.rebase_url(branch_location,
21+ old_base, new_base)
22+ except urlutils.InvalidRebaseURLs:
23+ # Fall back to absolute URL
24+ branch_location = urlutils.join(old_base, branch_location)
25 target_reference_dict.setdefault(
26 tree_path, (branch_location, file_id))
27 self.target._set_all_reference_info(target_reference_dict)
28
29=== modified file 'breezy/bzr/branch.py'
30--- breezy/bzr/branch.py 2020-01-19 03:22:04 +0000
31+++ breezy/bzr/branch.py 2020-01-19 15:24:24 +0000
32@@ -465,11 +465,20 @@
33 :return: A branch associated with the nested tree
34 """
35 try:
36- return Branch.open_from_transport(
37- self.controldir.root_transport.clone(path),
38- possible_transports=possible_transports)
39- except errors.NotBranchError:
40- return None
41+ branch_location = self.get_reference_info(file_id)[0]
42+ except errors.UnsupportedOperation:
43+ branch_location = None
44+ if branch_location is None:
45+ try:
46+ return Branch.open_from_transport(
47+ self.controldir.root_transport.clone(path),
48+ possible_transports=possible_transports)
49+ except errors.NotBranchError:
50+ return None
51+ return Branch.open(
52+ urlutils.join(
53+ urlutils.strip_segment_parameters(self.user_url), branch_location),
54+ possible_transports=possible_transports)
55
56
57 class BzrBranch8(BzrBranch):
58@@ -565,7 +574,7 @@
59 with self._transport.get('references') as rio_file:
60 stanzas = rio.read_stanzas(rio_file)
61 info_dict = {
62- s['file_id'].encode('ascii'): (
63+ s['file_id'].encode('utf-8'): (
64 s['branch_location'],
65 s['tree_path'] if 'tree_path' in s else None)
66 for s in stanzas}
67@@ -595,25 +604,6 @@
68 """
69 return self._get_all_reference_info().get(file_id, (None, None))
70
71- def reference_parent(self, file_id, path, possible_transports=None):
72- """Return the parent branch for a tree-reference.
73-
74- :param path: The path of the nested tree in the tree
75- :return: A branch associated with the nested tree
76- """
77- branch_location = self.get_reference_info(file_id)[0]
78- if branch_location is None:
79- try:
80- return Branch.open_from_transport(
81- self.controldir.root_transport.clone(path),
82- possible_transports=possible_transports)
83- except errors.NotBranchError:
84- return None
85- else:
86- branch_location = urlutils.join(self.user_url, branch_location)
87- return Branch.open(
88- branch_location, possible_transports=possible_transports)
89-
90 def set_push_location(self, location):
91 """See Branch.set_push_location."""
92 self._set_config_location('push_location', location)
93
94=== modified file 'breezy/bzr/fullhistory.py'
95--- breezy/bzr/fullhistory.py 2018-11-11 04:08:32 +0000
96+++ breezy/bzr/fullhistory.py 2020-01-19 15:24:24 +0000
97@@ -173,3 +173,5 @@
98
99 def supports_tags(self):
100 return False
101+
102+ supports_reference_locations = False
103
104=== modified file 'breezy/bzr/remote.py'
105--- breezy/bzr/remote.py 2020-01-18 22:02:05 +0000
106+++ breezy/bzr/remote.py 2020-01-19 15:24:24 +0000
107@@ -3397,6 +3397,11 @@
108 self._ensure_real()
109 return self._custom_format.supports_set_append_revisions_only()
110
111+ @property
112+ def supports_reference_locations(self):
113+ self._ensure_real()
114+ return self._custom_format.supports_reference_locations
115+
116 def stores_revno(self):
117 return True
118
119@@ -3415,8 +3420,6 @@
120 return True
121 return False
122
123- supports_reference_locations = False
124-
125
126 class RemoteBranchStore(_mod_config.IniFileStore):
127 """Branch store which attempts to use HPSS calls to retrieve branch store.
128@@ -4179,28 +4182,60 @@
129 reconciler = BranchReconciler(self, thorough=thorough)
130 return reconciler.reconcile()
131
132- def set_reference_info(self, tree_path, branch_location, file_id=None):
133- raise errors.UnsupportedOperation(self.set_reference_info, self)
134-
135- def get_reference_info(self, tree_path):
136- raise errors.UnsupportedOperation(self.get_reference_info, self)
137+ def get_reference_info(self, file_id):
138+ """Get the tree_path and branch_location for a tree reference."""
139+ if not self._format.supports_reference_locations:
140+ raise errors.UnsupportedOperation(self.get_reference_info, self)
141+ return self._get_all_reference_info().get(file_id, (None, None))
142+
143+ def set_reference_info(self, file_id, branch_location, tree_path=None):
144+ """Set the branch location to use for a tree reference."""
145+ if not self._format.supports_reference_locations:
146+ raise errors.UnsupportedOperation(self.set_reference_info, self)
147+ self._ensure_real()
148+ self._real_branch.set_reference_info(
149+ file_id, branch_location, tree_path)
150+
151+ def _set_all_reference_info(self, reference_info):
152+ if not self._format.supports_reference_locations:
153+ raise errors.UnsupportedOperation(self.set_reference_info, self)
154+ self._ensure_real()
155+ self._real_branch._set_all_reference_info(reference_info)
156
157 def _get_all_reference_info(self):
158- self._ensure_real()
159- return self._real_branch._get_all_reference_info()
160+ if not self._format.supports_reference_locations:
161+ return {}
162+ try:
163+ response, handler = self._call_expecting_body(
164+ b'Branch.get_all_reference_info', self._remote_path())
165+ except errors.UnknownSmartMethod:
166+ self._ensure_real()
167+ return self._real_branch._get_all_reference_info()
168+ if len(response) and response[0] != b'ok':
169+ raise errors.UnexpectedSmartServerResponse(response)
170+ ret = {}
171+ for (f, u, p) in bencode.bdecode(handler.read_body_bytes()):
172+ ret[f] = (u.decode('utf-8'), p.decode('utf-8') if p else None)
173+ return ret
174
175- def reference_parent(self, path, possible_transports=None):
176+ def reference_parent(self, file_id, path, possible_transports=None):
177 """Return the parent branch for a tree-reference.
178
179 :param path: The path of the nested tree in the tree
180 :return: A branch associated with the nested tree
181 """
182- branch_location = self.get_reference_info(path)[0]
183+ branch_location = self.get_reference_info(file_id)[0]
184 if branch_location is None:
185- return BzrBranch.reference_parent(self, path, possible_transports)
186- branch_location = urlutils.join(self.user_url, branch_location)
187- return Branch.open(branch_location,
188- possible_transports=possible_transports)
189+ try:
190+ return branch.Branch.open_from_transport(
191+ self.controldir.root_transport.clone(path),
192+ possible_transports=possible_transports)
193+ except errors.NotBranchError:
194+ return None
195+ return branch.Branch.open(
196+ urlutils.join(
197+ urlutils.strip_segment_parameters(self.user_url), branch_location),
198+ possible_transports=possible_transports)
199
200
201 class RemoteConfig(object):
202
203=== modified file 'breezy/bzr/smart/branch.py'
204--- breezy/bzr/smart/branch.py 2019-03-06 21:27:22 +0000
205+++ breezy/bzr/smart/branch.py 2020-01-19 15:24:24 +0000
206@@ -439,3 +439,17 @@
207 return SuccessfulSmartServerResponse((b'yes',))
208 else:
209 return SuccessfulSmartServerResponse((b'no',))
210+
211+
212+class SmartServerBranchRequestGetAllReferenceInfo(SmartServerBranchRequest):
213+ """Get the reference information.
214+
215+ New in 3.1.
216+ """
217+
218+ def do_with_branch(self, branch):
219+ all_reference_info = branch._get_all_reference_info()
220+ content = bencode.bencode([
221+ (key, value[0].encode('utf-8'), value[1].encode('utf-8') if value[1] else b'')
222+ for (key, value) in all_reference_info.items()])
223+ return SuccessfulSmartServerResponse((b'ok', ), content)
224
225=== modified file 'breezy/bzr/smart/request.py'
226--- breezy/bzr/smart/request.py 2019-06-30 10:50:40 +0000
227+++ breezy/bzr/smart/request.py 2020-01-19 15:24:24 +0000
228@@ -609,6 +609,9 @@
229 b'Branch.revision_id_to_revno', 'breezy.bzr.smart.branch',
230 'SmartServerBranchRequestRevisionIdToRevno', info='read')
231 request_handlers.register_lazy(
232+ b'Branch.get_all_reference_info', 'breezy.bzr.smart.branch',
233+ 'SmartServerBranchRequestGetAllReferenceInfo', info='read')
234+request_handlers.register_lazy(
235 b'BzrDir.checkout_metadir', 'breezy.bzr.smart.bzrdir',
236 'SmartServerBzrDirRequestCheckoutMetaDir', info='read')
237 request_handlers.register_lazy(
238
239=== modified file 'breezy/git/branch.py'
240--- breezy/git/branch.py 2020-01-18 16:15:37 +0000
241+++ breezy/git/branch.py 2020-01-19 15:24:24 +0000
242@@ -348,6 +348,8 @@
243 """True if this branch format store revision numbers."""
244 return False
245
246+ supports_reference_locations = False
247+
248
249 class LocalGitBranchFormat(GitBranchFormat):
250
251
252=== modified file 'breezy/plugins/weave_fmt/branch.py'
253--- breezy/plugins/weave_fmt/branch.py 2018-11-11 04:08:32 +0000
254+++ breezy/plugins/weave_fmt/branch.py 2020-01-19 15:24:24 +0000
255@@ -215,3 +215,5 @@
256
257 def supports_leaving_lock(self):
258 return False
259+
260+ supports_reference_locations = False
261
262=== modified file 'breezy/tests/blackbox/test_branch.py'
263--- breezy/tests/blackbox/test_branch.py 2020-01-19 03:22:04 +0000
264+++ breezy/tests/blackbox/test_branch.py 2020-01-19 15:24:24 +0000
265@@ -557,9 +557,10 @@
266 # become necessary for this use case. Please do not adjust this number
267 # upwards without agreement from bzr's network support maintainers.
268 self.assertLength(2, self.hpss_connections)
269- self.assertLength(33, self.hpss_calls)
270- self.expectFailure("branching to the same branch requires VFS access",
271- self.assertThat, self.hpss_calls, ContainsNoVfsCalls)
272+ self.assertLength(34, self.hpss_calls)
273+ self.expectFailure(
274+ "branching to the same branch requires VFS access",
275+ self.assertThat, self.hpss_calls, ContainsNoVfsCalls)
276
277 def test_branch_from_trivial_branch_streaming_acceptance(self):
278 self.setup_smart_server_with_call_log()
279@@ -575,7 +576,7 @@
280 # become necessary for this use case. Please do not adjust this number
281 # upwards without agreement from bzr's network support maintainers.
282 self.assertThat(self.hpss_calls, ContainsNoVfsCalls)
283- self.assertLength(10, self.hpss_calls)
284+ self.assertLength(11, self.hpss_calls)
285 self.assertLength(1, self.hpss_connections)
286
287 def test_branch_from_trivial_stacked_branch_streaming_acceptance(self):
288@@ -597,7 +598,7 @@
289 # being too low. If rpc_count increases, more network roundtrips have
290 # become necessary for this use case. Please do not adjust this number
291 # upwards without agreement from bzr's network support maintainers.
292- self.assertLength(15, self.hpss_calls)
293+ self.assertLength(16, self.hpss_calls)
294 self.assertLength(1, self.hpss_connections)
295 self.assertThat(self.hpss_calls, ContainsNoVfsCalls)
296
297@@ -617,7 +618,7 @@
298 # being too low. If rpc_count increases, more network roundtrips have
299 # become necessary for this use case. Please do not adjust this number
300 # upwards without agreement from bzr's network support maintainers.
301- self.assertLength(10, self.hpss_calls)
302+ self.assertLength(11, self.hpss_calls)
303 self.assertThat(self.hpss_calls, ContainsNoVfsCalls)
304 self.assertLength(1, self.hpss_connections)
305
306@@ -659,7 +660,7 @@
307 # become necessary for this use case. Please do not adjust this number
308 # upwards without agreement from bzr's network support maintainers.
309 self.assertThat(self.hpss_calls, ContainsNoVfsCalls)
310- self.assertLength(11, self.hpss_calls)
311+ self.assertLength(12, self.hpss_calls)
312 self.assertLength(1, self.hpss_connections)
313
314
315
316=== modified file 'breezy/tests/blackbox/test_checkout.py'
317--- breezy/tests/blackbox/test_checkout.py 2018-11-11 04:08:32 +0000
318+++ breezy/tests/blackbox/test_checkout.py 2020-01-19 15:24:24 +0000
319@@ -211,7 +211,7 @@
320 # being too low. If rpc_count increases, more network roundtrips have
321 # become necessary for this use case. Please do not adjust this number
322 # upwards without agreement from bzr's network support maintainers.
323- self.assertLength(10, self.hpss_calls)
324+ self.assertLength(11, self.hpss_calls)
325 self.assertLength(1, self.hpss_connections)
326 self.assertThat(self.hpss_calls, ContainsNoVfsCalls)
327
328
329=== modified file 'breezy/tests/blackbox/test_pull.py'
330--- breezy/tests/blackbox/test_pull.py 2019-02-09 02:59:15 +0000
331+++ breezy/tests/blackbox/test_pull.py 2020-01-19 15:24:24 +0000
332@@ -413,7 +413,7 @@
333 # being too low. If rpc_count increases, more network roundtrips have
334 # become necessary for this use case. Please do not adjust this number
335 # upwards without agreement from bzr's network support maintainers.
336- self.assertLength(19, self.hpss_calls)
337+ self.assertLength(20, self.hpss_calls)
338 self.assertLength(1, self.hpss_connections)
339 remote = branch.Branch.open('stacked')
340 self.assertEndsWith(remote.get_stacked_on_url(), '/parent')
341
342=== modified file 'breezy/tests/per_workingtree/test_workingtree.py'
343--- breezy/tests/per_workingtree/test_workingtree.py 2020-01-19 03:22:04 +0000
344+++ breezy/tests/per_workingtree/test_workingtree.py 2020-01-19 15:24:24 +0000
345@@ -1385,8 +1385,8 @@
346 tree = WorkingTree.open('branch')
347 branch_location = tree.get_reference_info('path/to/file')
348 self.assertEqual(
349- urlutils.local_path_to_url('branch/path/to/location'),
350- urlutils.join(tree.branch.user_url, branch_location))
351+ urlutils.join(urlutils.strip_segment_parameters(tree.branch.user_url), 'path/to/location'),
352+ urlutils.join(urlutils.strip_segment_parameters(tree.branch.user_url), branch_location))
353
354 def test_set_null_reference_info(self):
355 tree = self.make_branch_and_tree('branch')
356@@ -1443,7 +1443,8 @@
357 tree = self.make_tree_with_reference('branch', '../reference')
358 new_tree = tree.branch.controldir.sprout('new-branch').open_workingtree()
359 self.assertEqual(
360- urlutils.local_path_to_url('reference'),
361+ urlutils.join(urlutils.strip_segment_parameters(tree.branch.user_url),
362+ '../reference'),
363 urlutils.join(urlutils.strip_segment_parameters(new_tree.branch.user_url),
364 new_tree.get_reference_info('path/to/file')))
365
366@@ -1451,7 +1452,7 @@
367 tree = self.make_tree_with_reference('branch', '../reference')
368 new_tree = tree.controldir.clone('new-branch').open_workingtree()
369 self.assertEqual(
370- urlutils.local_path_to_url('reference'),
371+ urlutils.join(urlutils.strip_segment_parameters(tree.branch.user_url), '../reference'),
372 urlutils.join(urlutils.strip_segment_parameters(new_tree.branch.user_url),
373 new_tree.get_reference_info('path/to/file')))
374
375@@ -1460,7 +1461,8 @@
376 new_tree = tree.controldir.sprout(
377 'branch/new-branch').open_workingtree()
378 self.assertEqual(
379- urlutils.local_path_to_url('branch/reference'),
380+ urlutils.join(urlutils.strip_segment_parameters(tree.branch.user_url),
381+ 'reference'),
382 urlutils.join(urlutils.strip_segment_parameters(new_tree.branch.user_url),
383 new_tree.get_reference_info('path/to/file')))
384
385@@ -1470,7 +1472,9 @@
386 'new_branch', 'reference2')
387 new_tree.branch.update_references(tree.branch)
388 self.assertEqual(
389- urlutils.local_path_to_url('branch/reference'),
390+ urlutils.join(
391+ urlutils.strip_segment_parameters(tree.branch.user_url),
392+ 'reference'),
393 urlutils.join(
394 urlutils.strip_segment_parameters(tree.branch.user_url),
395 tree.get_reference_info('path/to/file')))
396@@ -1481,7 +1485,9 @@
397 'new_branch', 'reference2')
398 new_tree.branch.update_references(tree.branch)
399 self.assertEqual(
400- urlutils.local_path_to_url('branch/reference'),
401+ urlutils.join(
402+ urlutils.strip_segment_parameters(tree.branch.user_url),
403+ 'reference'),
404 urlutils.join(
405 urlutils.strip_segment_parameters(tree.branch.user_url),
406 tree.get_reference_info('path/to/file')))
407@@ -1495,7 +1501,9 @@
408 new_tree.set_reference_info('foo', '../foo')
409 new_tree.branch.update_references(tree.branch)
410 self.assertEqual(
411- urlutils.local_path_to_url('branch/reference'),
412+ urlutils.join(
413+ urlutils.strip_segment_parameters(tree.branch.user_url),
414+ 'reference'),
415 urlutils.join(
416 urlutils.strip_segment_parameters(tree.branch.user_url),
417 tree.get_reference_info('path/to/file')))
418@@ -1510,7 +1518,7 @@
419 new_tree.commit('set reference')
420 tree.pull(new_tree.branch)
421 self.assertEqual(
422- urlutils.local_path_to_url('branch/foo'),
423+ urlutils.join(urlutils.strip_segment_parameters(new_tree.branch.user_url), '../foo'),
424 urlutils.join(tree.branch.user_url, tree.get_reference_info('foo')))
425
426 def test_push_updates_references(self):
427@@ -1524,8 +1532,8 @@
428 tree.pull(new_tree.branch)
429 tree.update()
430 self.assertEqual(
431- urlutils.local_path_to_url('branch/foo'),
432- urlutils.join(tree.branch.user_url, tree.get_reference_info('foo')))
433+ urlutils.join(urlutils.strip_segment_parameters(new_tree.branch.user_url), '../foo'),
434+ urlutils.join(urlutils.strip_segment_parameters(tree.branch.user_url), tree.get_reference_info('foo')))
435
436 def test_merge_updates_references(self):
437 orig_tree = self.make_tree_with_reference('branch', 'reference')
438@@ -1542,5 +1550,5 @@
439 merger.merge_type = merge.Merge3Merger
440 merger.do_merge()
441 self.assertEqual(
442- urlutils.local_path_to_url('branch/reference'),
443+ urlutils.join(urlutils.strip_segment_parameters(orig_tree.branch.user_url), 'reference'),
444 urlutils.join(urlutils.strip_segment_parameters(tree.branch.user_url), tree.get_reference_info('path/to/file')))
445
446=== modified file 'breezy/tests/test_remote.py'
447--- breezy/tests/test_remote.py 2019-11-06 01:31:41 +0000
448+++ breezy/tests/test_remote.py 2020-01-19 15:24:24 +0000
449@@ -4529,3 +4529,23 @@
450 (b'baserevid', b'line 1\n'),
451 (b'somerevid', b'line2\n')],
452 list(tree.annotate_iter('filename')))
453+
454+
455+class TestBranchGetAllReferenceInfo(RemoteBranchTestCase):
456+
457+ def test_get_all_reference_info(self):
458+ transport = MemoryTransport()
459+ client = FakeClient(transport.base)
460+ client.add_expected_call(
461+ b'Branch.get_stacked_on_url', (b'quack/',),
462+ b'error', (b'NotStacked',))
463+ client.add_expected_call(
464+ b'Branch.get_all_reference_info', (b'quack/',),
465+ b'success', (b'ok',), bencode.bencode([
466+ (b'file-id', b'https://www.example.com/', b'')]))
467+ transport.mkdir('quack')
468+ transport = transport.clone('quack')
469+ branch = self.make_remote_branch(transport, client)
470+ result = branch._get_all_reference_info()
471+ self.assertFinished(client)
472+ self.assertEqual({b'file-id': ('https://www.example.com/', None)}, result)
473
474=== modified file 'breezy/tests/test_smart.py'
475--- breezy/tests/test_smart.py 2019-12-23 03:04:00 +0000
476+++ breezy/tests/test_smart.py 2020-01-19 15:24:24 +0000
477@@ -2819,3 +2819,18 @@
478 self.assertEqual(
479 [[b'somerev', b'somecontents\n'], [b'somerev', b'morecontents\n']],
480 bencode.bdecode(response.body))
481+
482+
483+class TestSmartServerBranchRequestGetAllReferenceInfo(TestLockedBranch):
484+
485+ def test_get_some(self):
486+ backing = self.get_transport()
487+ request = smart_branch.SmartServerBranchRequestGetAllReferenceInfo(backing)
488+ branch = self.make_branch('.')
489+ branch.set_reference_info('some/path', 'http://www.example.com/')
490+ response = request.execute(b'')
491+ self.assertTrue(response.is_successful())
492+ self.assertEqual(response.args, (b"ok", ))
493+ self.assertEqual(
494+ [[b'some/path', b'http://www.example.com/', b'']],
495+ bencode.bdecode(response.body))
496
497=== modified file 'doc/en/release-notes/brz-3.1.txt'
498--- doc/en/release-notes/brz-3.1.txt 2020-01-12 23:41:10 +0000
499+++ doc/en/release-notes/brz-3.1.txt 2020-01-19 15:24:24 +0000
500@@ -46,6 +46,9 @@
501 have installed and speeds up import time since psutil brings in
502 various other modules. (Jelmer Vernooij)
503
504+ * Information about tree references can now be updated on remote
505+ branches. (Jelmer Vernooij)
506+
507 * Warn the user when they attempt to use Breezy in a Subversion
508 working copy. (Jelmer Vernooij)
509

Subscribers

People subscribed via source and target branches