Merge lp:~jelmer/brz/hpss-working-tree-update into lp:brz

Proposed by Jelmer Vernooij
Status: Work in progress
Proposed branch: lp:~jelmer/brz/hpss-working-tree-update
Merge into: lp:brz
Diff against target: 555 lines (+373/-11)
11 files modified
breezy/bzr/bzrdir.py (+2/-2)
breezy/bzr/remote.py (+168/-2)
breezy/bzr/smart/branch.py (+1/-1)
breezy/bzr/smart/request.py (+6/-0)
breezy/bzr/smart/workingtree.py (+115/-0)
breezy/bzr/workingtree_4.py (+2/-2)
breezy/controldir.py (+3/-2)
breezy/tests/blackbox/test_push.py (+17/-0)
breezy/tests/test_remote.py (+54/-0)
breezy/workingtree.py (+2/-2)
doc/en/release-notes/brz-3.0.txt (+3/-0)
To merge this branch: bzr merge lp:~jelmer/brz/hpss-working-tree-update
Reviewer Review Type Date Requested Status
Breezy developers Pending
Review via email: mp+345970@code.launchpad.net

Commit message

Add a HPSS call for updating the remote working tree.

Description of the change

Add a HPSS call for updating the remote working tree.

To post a comment you must log in.
Revision history for this message
Jelmer Vernooij (jelmer) wrote :

All the plumbing for this is in place, but the trouble is that this requires breaking out of the chroot since WorkingTree requires a filesystem path.

Unmerged revisions

6324. By Jelmer Vernooij

Pass along remote argument.

6323. By Jelmer Vernooij

Fix some tests.

6322. By Jelmer Vernooij

merge trunk.

6321. By Jelmer Vernooij

Update NEWS.

6320. By Jelmer Vernooij

Add test for push updating remote working tree.

6319. By Jelmer Vernooij

Add server side.

6318. By Jelmer Vernooij

Merge hpss-working-tree.

6317. By Jelmer Vernooij

Add basic RemoteWorkingTree.update.

6316. By Jelmer Vernooij

Support opening remote working trees.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'breezy/bzr/bzrdir.py'
2--- breezy/bzr/bzrdir.py 2018-05-07 14:35:05 +0000
3+++ breezy/bzr/bzrdir.py 2018-05-19 02:48:08 +0000
4@@ -1103,13 +1103,13 @@
5 return format.open(self, _found=True)
6
7 def open_workingtree(self, unsupported=False,
8- recommend_upgrade=True):
9+ recommend_upgrade=True, remote=False):
10 """See BzrDir.open_workingtree."""
11 from .workingtree import WorkingTreeFormatMetaDir
12 format = WorkingTreeFormatMetaDir.find_format(self)
13 format.check_support_status(unsupported, recommend_upgrade,
14 basedir=self.root_transport.base)
15- return format.open(self, _found=True)
16+ return format.open(self, remote=remote, _found=True)
17
18 def _get_config(self):
19 return config.TransportConfig(self.transport, 'control.conf')
20
21=== modified file 'breezy/bzr/remote.py'
22--- breezy/bzr/remote.py 2018-05-13 16:23:17 +0000
23+++ breezy/bzr/remote.py 2018-05-19 02:48:08 +0000
24@@ -45,6 +45,7 @@
25 inventory_delta,
26 vf_repository,
27 vf_search,
28+ workingtree,
29 )
30 from .branch import BranchReferenceFormat
31 from ..branch import BranchWriteLockResult
32@@ -883,9 +884,12 @@
33 self._has_working_tree = (response[0] == b'yes')
34 return self._has_working_tree
35
36- def open_workingtree(self, recommend_upgrade=True):
37+ def open_workingtree(self, recommend_upgrade=True, remote=False):
38 if self.has_workingtree():
39- raise errors.NotLocalUrl(self.root_transport)
40+ if remote:
41+ return RemoteWorkingTree(self, self.open_branch())
42+ else:
43+ raise errors.NotLocalUrl(self.root_transport.base)
44 else:
45 raise errors.NoWorkingTree(self.root_transport.base)
46
47@@ -4127,6 +4131,168 @@
48 return self._bzrdir._real_bzrdir
49
50
51+class RemoteWorkingTreeFormat(workingtree.WorkingTreeFormat):
52+
53+ def __init__(self):
54+ super(RemoteWorkingTreeFormat, self).__init__()
55+
56+ def get_format_description(self):
57+ return "Remote Working Tree"
58+
59+ def __eq__(self, other):
60+ return (isinstance(other, RemoteWorkingTreeFormat) and
61+ self.__dict__ == other.__dict__)
62+
63+
64+class RemoteWorkingTree(workingtree.WorkingTree, _RpcHelper, lock._RelockDebugMixin):
65+ """Working tree stored on a server accessed by HPSS RPC.
66+
67+ At the moment this only implements .update(). All other methods
68+ raise _ensure_real().
69+ """
70+
71+ def __init__(self, remote_controldir, remote_branch, _client=None):
72+ self.controldir = remote_controldir
73+ self._branch = remote_branch
74+ self._format = RemoteWorkingTreeFormat()
75+ self._real_workingtree = None
76+ self._lock_mode = None
77+ self._lock_count = 0
78+ if _client is None:
79+ self._client = remote_controldir._client
80+ else:
81+ self._client = _client
82+
83+ @property
84+ def user_transport(self):
85+ return self.controldir.user_transport
86+
87+ @property
88+ def control_transport(self):
89+ # XXX: Normally you shouldn't directly get at the remote repository
90+ # transport, but I'm not sure it's worth making this method
91+ # optional -- mbp 2010-04-21
92+ return self.controldir.get_repository_transport(None)
93+
94+ def _translate_error(self, err, **context):
95+ _translate_error(err, workingtree=self, **context)
96+
97+ def _ensure_real(self):
98+ # Most working tree operations require local access at the moment.
99+ # When Bazaar working trees can be accessed over
100+ # transports, set self._real_workingtree here.
101+ raise errors.NotLocalUrl(self.controldir.root_transport)
102+
103+ def is_locked(self):
104+ return self._lock_count >= 1
105+
106+ def lock_read(self):
107+ """Lock the working tree for read operations.
108+
109+ :return: A bzrlib.lock.LogicalLockResult.
110+ """
111+ # wrong eventually - want a local lock cache context
112+ if not self._lock_mode:
113+ self._note_lock('r')
114+ self._lock_mode = 'r'
115+ self._lock_count = 1
116+ if self._real_workingtree is not None:
117+ self._real_workingtree.lock_read()
118+ self.branch.lock_read()
119+ else:
120+ self._lock_count += 1
121+ return lock.LogicalLockResult(self.unlock)
122+
123+ def lock_write(self):
124+ if not self._lock_mode:
125+ self._note_lock('w')
126+ if self._real_workingtree is not None:
127+ self._real_workingtree.lock_write()
128+ self._lock_mode = 'w'
129+ self._lock_count = 1
130+ self.branch.lock_write()
131+ elif self._lock_mode == 'r':
132+ raise errors.ReadOnlyError(self)
133+ else:
134+ self._lock_count += 1
135+ return lock.LogicalLockResult(
136+ lambda: self.unlock())
137+
138+ @only_raises(errors.LockNotHeld, errors.LockBroken)
139+ def unlock(self):
140+ if not self._lock_count:
141+ return lock.cant_unlock_not_held(self)
142+ self._lock_count -= 1
143+ if self._lock_count > 0:
144+ return
145+ old_mode = self._lock_mode
146+ self._lock_mode = None
147+ try:
148+ if self._real_workingtree is not None:
149+ self._real_workingtree.unlock()
150+ finally:
151+ self.branch.unlock()
152+
153+ def get_physical_lock_status(self):
154+ """See WorkingTree.get_physical_lock_status()."""
155+ path = self.controldir._path_for_remote_call(self._client)
156+ try:
157+ response = self._call(b'WorkingTree.get_physical_lock_status', path)
158+ except errors.UnknownSmartMethod:
159+ self._ensure_real()
160+ return self._real_workingtree.get_physical_lock_status()
161+ if response[0] not in (b'yes', b'no'):
162+ raise errors.UnexpectedSmartServerResponse(response)
163+ return (response[0] == b'yes')
164+
165+ def _update_rpc(self, old_tip=None, change_reporter=None,
166+ possible_transports=None, revision=None, show_base=False):
167+ medium = self.controldir._client._medium
168+ path = self.controldir._path_for_remote_call(self.controldir._client)
169+ if revision is None:
170+ revision = ''
171+ if old_tip is None:
172+ old_tip = ''
173+ response, response_handler = self._call_expecting_body(
174+ 'WorkingTree.update', path, revision, old_tip, show_base)
175+ if response[0] != 'ok':
176+ raise errors.UnexpectedSmartServerResponse(response)
177+ if change_reporter is None:
178+ response_handler.cancel_read_body()
179+ else:
180+ byte_stream = response_handler.read_streamed_body()
181+ data = ""
182+ for bytes in byte_stream:
183+ data += bytes
184+ lines = bytes.split("\n")
185+ data = lines.pop()
186+ for line in lines:
187+ if not line.startswith("change: "):
188+ raise errors.UnexpectedSmartServerResponse(response)
189+ change = bencode.bdecode_as_tuple(line[8:])
190+ # FIXME: More post-processing
191+ change_reporter.report(*change)
192+ return int(response[1])
193+
194+ def update(self, old_tip=None, change_reporter=None,
195+ possible_transports=None, revision=None, show_base=False):
196+ try:
197+ return self._update_rpc(old_tip=old_tip,
198+ change_reporter=change_reporter,
199+ possible_transports=possible_transports, revision=revision,
200+ show_base=show_base)
201+ except errors.UnknownSmartMethod:
202+ self._ensure_real()
203+ return self._real_workingtree.update(old_tip=old_tip,
204+ change_reporter=change_reporter,
205+ possible_transports=possible_transports,
206+ revision=revision, show_base=show_base)
207+
208+ def get_parent_ids(self):
209+ self._ensure_real()
210+ return self._real_workingtree.get_parent_ids()
211+
212+
213 error_translators = registry.Registry()
214 no_context_error_translators = registry.Registry()
215
216
217=== modified file 'breezy/bzr/smart/branch.py'
218--- breezy/bzr/smart/branch.py 2018-05-13 02:18:13 +0000
219+++ breezy/bzr/smart/branch.py 2018-05-19 02:48:08 +0000
220@@ -14,7 +14,7 @@
221 # along with this program; if not, write to the Free Software
222 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
223
224-"""Server-side branch related request implmentations."""
225+"""Server-side branch related request implementations."""
226
227 from __future__ import absolute_import
228
229
230=== modified file 'breezy/bzr/smart/request.py'
231--- breezy/bzr/smart/request.py 2018-05-13 02:18:13 +0000
232+++ breezy/bzr/smart/request.py 2018-05-19 02:48:08 +0000
233@@ -791,3 +791,9 @@
234 request_handlers.register_lazy(
235 b'Transport.is_readonly', 'breezy.bzr.smart.request',
236 'SmartServerIsReadonly', info='read')
237+request_handlers.register_lazy(
238+ b'WorkingTree.update', 'breezy.bzr.smart.workingtree',
239+ 'SmartServerWorkingTreeUpdate', info='mutate')
240+request_handlers.register_lazy(
241+ b'WorkingTree.get_physical_lock_status', 'breezy.bzr.smart.workingtree',
242+ 'SmartServerWorkingTreeGetPhysicalLockStatus', info='read')
243
244=== added file 'breezy/bzr/smart/workingtree.py'
245--- breezy/bzr/smart/workingtree.py 1970-01-01 00:00:00 +0000
246+++ breezy/bzr/smart/workingtree.py 2018-05-19 02:48:08 +0000
247@@ -0,0 +1,115 @@
248+# Copyright (C) 2011 Canonical Ltd
249+#
250+# This program is free software; you can redistribute it and/or modify
251+# it under the terms of the GNU General Public License as published by
252+# the Free Software Foundation; either version 2 of the License, or
253+# (at your option) any later version.
254+#
255+# This program is distributed in the hope that it will be useful,
256+# but WITHOUT ANY WARRANTY; without even the implied warranty of
257+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
258+# GNU General Public License for more details.
259+#
260+# You should have received a copy of the GNU General Public License
261+# along with this program; if not, write to the Free Software
262+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
263+
264+"""Server-side working tree related request implementations."""
265+
266+from __future__ import absolute_import
267+
268+
269+from breezy import (
270+ bencode,
271+ delta as _mod_delta,
272+ errors,
273+ revision as _mod_revision,
274+ )
275+from breezy.controldir import ControlDir
276+from breezy.bzr.smart.request import (
277+ FailedSmartServerResponse,
278+ SmartServerRequest,
279+ SuccessfulSmartServerResponse,
280+ )
281+
282+
283+class SmartServerWorkingTreeRequest(SmartServerRequest):
284+ """Base class for handling common working tree request logic.
285+ """
286+
287+ def do(self, path, *args):
288+ """Execute a request for a working tree at path.
289+
290+ All working tree requests take a path to the tree as their first
291+ argument.
292+
293+ :param path: The path for the repository as received from the
294+ client.
295+ :return: A SmartServerResponse from self.do_with_workingtree().
296+ """
297+ transport = self.transport_from_client_path(path)
298+ controldir = ControlDir.open_from_transport(transport)
299+ workingtree = controldir.open_workingtree(remote=True)
300+ return self.do_with_workingtree(workingtree, *args)
301+
302+ def do_with_workingtree(self, workingtree, *args):
303+ raise NotImplementedError(self.do_with_workingtree)
304+
305+
306+class SerializingChangeReporter(_mod_delta._ChangeReporter):
307+ """Change reporter that serializes."""
308+
309+ def __init__(self):
310+ self.chunks = []
311+
312+ def _format_path(self, path):
313+ if path is None:
314+ return ""
315+ elif path == "":
316+ return "."
317+ else:
318+ return path.encode("utf-8")
319+
320+ def _format_kind(self, kind):
321+ if kind is None:
322+ return ""
323+ return kind
324+
325+ def report(self, file_id, paths, versioned, renamed, modified, exe_change,
326+ kind):
327+ self.chunks.append("report: %s\n" % bencode.bencode((file_id,
328+ self._format_path(paths[0]), self._format_path(paths[1]),
329+ versioned, renamed, modified, exe_change,
330+ self._format_kind(kind[0]), self._format_kind(kind[1]))))
331+
332+
333+class SmartServerWorkingTreeUpdate(SmartServerWorkingTreeRequest):
334+ """Update a working tree.
335+
336+ New in 3.0.
337+ """
338+
339+ def do_with_workingtree(self, workingtree, revision, old_tip, show_base):
340+ if revision == '':
341+ revision = None
342+ if old_tip == '':
343+ old_tip = None
344+ reporter = SerializingChangeReporter()
345+ with workingtree.lock_write(): # TODO(jelmer): Accept token
346+ nconflicts = workingtree.update(change_reporter=reporter,
347+ revision=revision, old_tip=old_tip, show_base=show_base)
348+ return SuccessfulSmartServerResponse(('ok', nconflicts),
349+ body_stream=iter(reporter.chunks))
350+
351+
352+class SmartServerWorkingTreeGetPhysicalLockStatus(SmartServerWorkingTreeRequest):
353+ """Get the physical lock status for a working tree.
354+
355+ New in 3.0.
356+ """
357+
358+ def do_with_workingtree(self, workingtree):
359+ if workingtree.get_physical_lock_status():
360+ return SuccessfulSmartServerResponse((b'yes', ))
361+ else:
362+ return SuccessfulSmartServerResponse((b'no', ))
363
364=== modified file 'breezy/bzr/workingtree_4.py'
365--- breezy/bzr/workingtree_4.py 2018-03-28 01:17:12 +0000
366+++ breezy/bzr/workingtree_4.py 2018-05-19 02:48:08 +0000
367@@ -1567,7 +1567,7 @@
368 :param wt: the WorkingTree object
369 """
370
371- def open(self, a_controldir, _found=False):
372+ def open(self, a_controldir, remote=False, _found=False):
373 """Return the WorkingTree object for a_controldir
374
375 _found is a private parameter, do not use it. It is used to indicate
376@@ -1576,7 +1576,7 @@
377 if not _found:
378 # we are being called directly and must probe.
379 raise NotImplementedError
380- if not isinstance(a_controldir.transport, LocalTransport):
381+ if not remote and not isinstance(a_controldir.transport, LocalTransport):
382 raise errors.NotLocalUrl(a_controldir.transport.base)
383 wt = self._open(a_controldir, self._open_control_files(a_controldir))
384 return wt
385
386=== modified file 'breezy/controldir.py'
387--- breezy/controldir.py 2018-05-07 14:35:05 +0000
388+++ breezy/controldir.py 2018-05-19 02:48:08 +0000
389@@ -306,6 +306,7 @@
390 (but still fully supported).
391 :param from_branch: override controldir branch (for lightweight
392 checkouts)
393+ :param remote: open working tree even if it's remote
394 """
395 raise NotImplementedError(self.open_workingtree)
396
397@@ -444,9 +445,9 @@
398 # FIXME: Should be done only if we succeed ? -- vila 2012-01-18
399 source.set_push_location(br_to.base)
400 try:
401- tree_to = self.open_workingtree()
402+ tree_to = self.open_workingtree(remote=True)
403 except errors.NotLocalUrl:
404- push_result.branch_push_result = source.push(br_to,
405+ push_result.branch_push_result = source.push(br_to,
406 overwrite, stop_revision=revision_id, lossy=lossy)
407 push_result.workingtree_updated = False
408 except errors.NoWorkingTree:
409
410=== modified file 'breezy/tests/blackbox/test_push.py'
411--- breezy/tests/blackbox/test_push.py 2018-03-26 00:54:10 +0000
412+++ breezy/tests/blackbox/test_push.py 2018-05-19 02:48:08 +0000
413@@ -266,6 +266,23 @@
414 # ancestry
415 self.assertEqual([('A',), ('C',)], sorted(target_repo.revisions.keys()))
416
417+ def test_push_smart_updates_tree(self):
418+ self.setup_smart_server_with_call_log()
419+ t = self.make_branch_and_tree('from')
420+ self.build_tree(['from/afile'])
421+ t.add(['afile'])
422+ self.make_branch_and_tree('to')
423+ t.commit(allow_pointless=True, message='first commit')
424+ self.reset_smart_call_log()
425+ self.run_bzr(['push', self.get_url('to')], working_dir='from')
426+ # This figure represent the amount of work to perform this use case. It
427+ # is entirely ok to reduce this number if a test fails due to rpc_count
428+ # being too low. If rpc_count increases, more network roundtrips have
429+ # become necessary for this use case. Please do not adjust this number
430+ # upwards without agreement from bzr's network support maintainers.
431+ self.assertLength(11, self.hpss_calls)
432+ self.assertPathExists('to/afile')
433+
434 def test_push_smart_non_stacked_streaming_acceptance(self):
435 self.setup_smart_server_with_call_log()
436 t = self.make_branch_and_tree('from')
437
438=== modified file 'breezy/tests/test_remote.py'
439--- breezy/tests/test_remote.py 2018-05-13 02:45:58 +0000
440+++ breezy/tests/test_remote.py 2018-05-19 02:48:08 +0000
441@@ -60,6 +60,7 @@
442 RemoteBranchFormat,
443 RemoteBzrDir,
444 RemoteBzrDirFormat,
445+ RemoteWorkingTree,
446 RemoteRepository,
447 RemoteRepositoryFormat,
448 )
449@@ -659,11 +660,24 @@
450 client, transport = self.make_fake_client_and_transport()
451 client.add_expected_call(
452 b'BzrDir.open_2.1', (b'quack/',), b'success', (b'yes', b'yes'))
453+ branch_network_name = self.get_branch_format().network_name()
454+ repo_network_name = self.get_repo_format().network_name()
455+ client.add_expected_call(
456+ 'BzrDir.open_branchV3', ('quack/',),
457+ 'success', ('branch', branch_network_name))
458+ client.add_expected_call(
459+ 'BzrDir.find_repositoryV3', ('quack/',),
460+ 'success', ('ok', '', 'no', 'no', 'no', repo_network_name))
461+ client.add_expected_call(
462+ 'Branch.get_stacked_on_url', ('quack/',),
463+ 'error', ('NotStacked',))
464 bd = RemoteBzrDir(transport, RemoteBzrDirFormat(),
465 _client=client, _force_probe=True)
466 self.assertIsInstance(bd, RemoteBzrDir)
467 self.assertTrue(bd.has_workingtree())
468 self.assertRaises(errors.NotLocalUrl, bd.open_workingtree)
469+ tree = bd.open_workingtree(remote=True)
470+ self.assertIsInstance(tree, RemoteWorkingTree)
471 self.assertFinished(client)
472
473 def test_backwards_compat(self):
474@@ -4261,6 +4275,46 @@
475 repo.pack([b'hinta', b'hintb'])
476
477
478+class RemoteWorkingTreeTestCase(RemoteBranchTestCase):
479+
480+ def make_remote_working_tree(self, transport, client):
481+ """Make a RemoteWorkingTree using 'client' as its _SmartClient.
482+ """
483+ bzrdir = self.make_remote_bzrdir(transport, client)
484+ return RemoteWorkingTree(bzrdir, bzrdir.open_branch())
485+
486+
487+class TestWorkingTreeLocking(RemoteWorkingTreeTestCase):
488+
489+ def test_lock(self):
490+ transport = MemoryTransport()
491+ client = FakeClient(transport.base)
492+ branch_network_name = self.get_branch_format().network_name()
493+ repo_network_name = self.get_repo_format().network_name()
494+ client.add_expected_call(
495+ 'BzrDir.open_branchV3', ('.',),
496+ 'success', ('branch', branch_network_name))
497+ client.add_expected_call(
498+ 'BzrDir.find_repositoryV3', ('.',),
499+ 'success', ('ok', '', 'no', 'no', 'no', repo_network_name))
500+ client.add_expected_call(
501+ 'Branch.get_stacked_on_url', ('.',),
502+ 'error', ('NotStacked',))
503+ client.add_expected_call(
504+ 'Branch.lock_write', ('.', '', ''),
505+ 'success', ('ok', 'repo token', 'branch token'))
506+ client.add_expected_call(
507+ 'Branch.unlock', ('.', 'repo token', 'branch token'),
508+ 'success', ('ok', ))
509+ tree = self.make_remote_working_tree(transport, client)
510+ self.assertFalse(tree.is_locked())
511+ tree.lock_write()
512+ self.assertTrue(tree.is_locked())
513+ tree.unlock()
514+ self.assertFalse(tree.is_locked())
515+ self.assertFinished(client)
516+
517+
518 class TestRepositoryIterInventories(TestRemoteRepository):
519 """Test Repository.iter_inventories."""
520
521
522=== modified file 'breezy/workingtree.py'
523--- breezy/workingtree.py 2018-03-25 00:39:16 +0000
524+++ breezy/workingtree.py 2018-05-19 02:48:08 +0000
525@@ -406,7 +406,7 @@
526 else:
527 parents = [last_rev]
528 try:
529- merges_bytes = self._transport.get_bytes('pending-merges')
530+ merges_bytes = self.control_transport.get_bytes('pending-merges')
531 except errors.NoSuchFile:
532 pass
533 else:
534@@ -584,7 +584,7 @@
535
536 def _set_merges_from_parent_ids(self, parent_ids):
537 merges = parent_ids[1:]
538- self._transport.put_bytes('pending-merges', b'\n'.join(merges),
539+ self.control_transport.put_bytes('pending-merges', b'\n'.join(merges),
540 mode=self.controldir._get_file_mode())
541
542 def _filter_parent_ids_by_ancestry(self, revision_ids):
543
544=== modified file 'doc/en/release-notes/brz-3.0.txt'
545--- doc/en/release-notes/brz-3.0.txt 2018-03-24 03:03:03 +0000
546+++ doc/en/release-notes/brz-3.0.txt 2018-05-19 02:48:08 +0000
547@@ -126,6 +126,9 @@
548 * New ``bzr cp`` command which copies files (but does not currently track
549 history). (Jelmer Vernooij, start towards #269095)
550
551+* ``bzr push`` will now update the remote working tree when it pushes to
552+ a smart server running Breezy 3.0 or later. (Jelmer Vernooij, #325355)
553+
554 Bug Fixes
555 *********
556

Subscribers

People subscribed via source and target branches