1
# Copyright (C) 2006, 2007 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
# TODO: At some point, handle upgrades by just passing the whole request
18
# across to run on the server.
21
from cStringIO import StringIO
32
from bzrlib.branch import BranchReferenceFormat
33
from bzrlib.bzrdir import BzrDir, RemoteBzrDirFormat
34
from bzrlib.config import BranchConfig, TreeConfig
35
from bzrlib.decorators import needs_read_lock, needs_write_lock
36
from bzrlib.errors import NoSuchRevision
37
from bzrlib.lockable_files import LockableFiles
38
from bzrlib.pack import ContainerPushParser
39
from bzrlib.smart import client, vfs
40
from bzrlib.symbol_versioning import (
44
from bzrlib.revision import NULL_REVISION
45
from bzrlib.trace import mutter, note
47
# Note: RemoteBzrDirFormat is in bzrdir.py
49
class RemoteBzrDir(BzrDir):
50
"""Control directory on a remote server, accessed via bzr:// or similar."""
52
def __init__(self, transport, _client=None):
53
"""Construct a RemoteBzrDir.
55
:param _client: Private parameter for testing. Disables probing and the
58
BzrDir.__init__(self, transport, RemoteBzrDirFormat())
59
# this object holds a delegated bzrdir that uses file-level operations
60
# to talk to the other side
61
self._real_bzrdir = None
64
self._shared_medium = transport.get_shared_medium()
65
self._client = client._SmartClient(self._shared_medium)
67
self._client = _client
68
self._shared_medium = None
71
path = self._path_for_remote_call(self._client)
72
response = self._client.call('BzrDir.open', path)
73
if response not in [('yes',), ('no',)]:
74
raise errors.UnexpectedSmartServerResponse(response)
75
if response == ('no',):
76
raise errors.NotBranchError(path=transport.base)
78
def _ensure_real(self):
79
"""Ensure that there is a _real_bzrdir set.
81
Used before calls to self._real_bzrdir.
83
if not self._real_bzrdir:
84
self._real_bzrdir = BzrDir.open_from_transport(
85
self.root_transport, _server_formats=False)
87
def create_repository(self, shared=False):
89
self._real_bzrdir.create_repository(shared=shared)
90
return self.open_repository()
92
def destroy_repository(self):
93
"""See BzrDir.destroy_repository"""
95
self._real_bzrdir.destroy_repository()
97
def create_branch(self):
99
real_branch = self._real_bzrdir.create_branch()
100
return RemoteBranch(self, self.find_repository(), real_branch)
102
def destroy_branch(self):
103
"""See BzrDir.destroy_branch"""
105
self._real_bzrdir.destroy_branch()
107
def create_workingtree(self, revision_id=None, from_branch=None):
108
raise errors.NotLocalUrl(self.transport.base)
110
def find_branch_format(self):
111
"""Find the branch 'format' for this bzrdir.
113
This might be a synthetic object for e.g. RemoteBranch and SVN.
115
b = self.open_branch()
118
def get_branch_reference(self):
119
"""See BzrDir.get_branch_reference()."""
120
path = self._path_for_remote_call(self._client)
121
response = self._client.call('BzrDir.open_branch', path)
122
if response[0] == 'ok':
123
if response[1] == '':
124
# branch at this location.
127
# a branch reference, use the existing BranchReference logic.
129
elif response == ('nobranch',):
130
raise errors.NotBranchError(path=self.root_transport.base)
132
raise errors.UnexpectedSmartServerResponse(response)
134
def open_branch(self, _unsupported=False):
135
assert _unsupported == False, 'unsupported flag support not implemented yet.'
136
reference_url = self.get_branch_reference()
137
if reference_url is None:
138
# branch at this location.
139
return RemoteBranch(self, self.find_repository())
141
# a branch reference, use the existing BranchReference logic.
142
format = BranchReferenceFormat()
143
return format.open(self, _found=True, location=reference_url)
145
def open_repository(self):
146
path = self._path_for_remote_call(self._client)
147
response = self._client.call('BzrDir.find_repository', path)
148
assert response[0] in ('ok', 'norepository'), \
149
'unexpected response code %s' % (response,)
150
if response[0] == 'norepository':
151
raise errors.NoRepositoryPresent(self)
152
assert len(response) == 4, 'incorrect response length %s' % (response,)
153
if response[1] == '':
154
format = RemoteRepositoryFormat()
155
format.rich_root_data = (response[2] == 'yes')
156
format.supports_tree_reference = (response[3] == 'yes')
157
return RemoteRepository(self, format)
159
raise errors.NoRepositoryPresent(self)
161
def open_workingtree(self, recommend_upgrade=True):
163
if self._real_bzrdir.has_workingtree():
164
raise errors.NotLocalUrl(self.root_transport)
166
raise errors.NoWorkingTree(self.root_transport.base)
168
def _path_for_remote_call(self, client):
169
"""Return the path to be used for this bzrdir in a remote call."""
170
return client.remote_path_from_transport(self.root_transport)
172
def get_branch_transport(self, branch_format):
174
return self._real_bzrdir.get_branch_transport(branch_format)
176
def get_repository_transport(self, repository_format):
178
return self._real_bzrdir.get_repository_transport(repository_format)
180
def get_workingtree_transport(self, workingtree_format):
182
return self._real_bzrdir.get_workingtree_transport(workingtree_format)
184
def can_convert_format(self):
185
"""Upgrading of remote bzrdirs is not supported yet."""
188
def needs_format_conversion(self, format=None):
189
"""Upgrading of remote bzrdirs is not supported yet."""
192
def clone(self, url, revision_id=None, force_new_repo=False):
194
return self._real_bzrdir.clone(url, revision_id=revision_id,
195
force_new_repo=force_new_repo)
198
class RemoteRepositoryFormat(repository.RepositoryFormat):
199
"""Format for repositories accessed over a _SmartClient.
201
Instances of this repository are represented by RemoteRepository
204
The RemoteRepositoryFormat is parameterized during construction
205
to reflect the capabilities of the real, remote format. Specifically
206
the attributes rich_root_data and supports_tree_reference are set
207
on a per instance basis, and are not set (and should not be) at
211
_matchingbzrdir = RemoteBzrDirFormat
213
def initialize(self, a_bzrdir, shared=False):
214
assert isinstance(a_bzrdir, RemoteBzrDir), \
215
'%r is not a RemoteBzrDir' % (a_bzrdir,)
216
return a_bzrdir.create_repository(shared=shared)
218
def open(self, a_bzrdir):
219
assert isinstance(a_bzrdir, RemoteBzrDir)
220
return a_bzrdir.open_repository()
222
def get_format_description(self):
223
return 'bzr remote repository'
225
def __eq__(self, other):
226
return self.__class__ == other.__class__
228
def check_conversion_target(self, target_format):
229
if self.rich_root_data and not target_format.rich_root_data:
230
raise errors.BadConversionTarget(
231
'Does not support rich root data.', target_format)
232
if (self.supports_tree_reference and
233
not getattr(target_format, 'supports_tree_reference', False)):
234
raise errors.BadConversionTarget(
235
'Does not support nested trees', target_format)
238
class RemoteRepository(object):
239
"""Repository accessed over rpc.
241
For the moment most operations are performed using local transport-backed
245
def __init__(self, remote_bzrdir, format, real_repository=None, _client=None):
246
"""Create a RemoteRepository instance.
248
:param remote_bzrdir: The bzrdir hosting this repository.
249
:param format: The RemoteFormat object to use.
250
:param real_repository: If not None, a local implementation of the
251
repository logic for the repository, usually accessing the data
253
:param _client: Private testing parameter - override the smart client
254
to be used by the repository.
257
self._real_repository = real_repository
259
self._real_repository = None
260
self.bzrdir = remote_bzrdir
262
self._client = client._SmartClient(self.bzrdir._shared_medium)
264
self._client = _client
265
self._format = format
266
self._lock_mode = None
267
self._lock_token = None
269
self._leave_lock = False
270
# A cache of looked up revision parent data; reset at unlock time.
271
self._parents_map = None
272
if 'hpss' in debug.debug_flags:
273
self._requested_parents = None
275
# These depend on the actual remote format, so force them off for
276
# maximum compatibility. XXX: In future these should depend on the
277
# remote repository instance, but this is irrelevant until we perform
278
# reconcile via an RPC call.
279
self._reconcile_does_inventory_gc = False
280
self._reconcile_fixes_text_parents = False
281
self._reconcile_backsup_inventory = False
282
self.base = self.bzrdir.transport.base
285
return "%s(%s)" % (self.__class__.__name__, self.base)
289
def abort_write_group(self):
290
"""Complete a write group on the decorated repository.
292
Smart methods peform operations in a single step so this api
293
is not really applicable except as a compatibility thunk
294
for older plugins that don't use e.g. the CommitBuilder
298
return self._real_repository.abort_write_group()
300
def commit_write_group(self):
301
"""Complete a write group on the decorated repository.
303
Smart methods peform operations in a single step so this api
304
is not really applicable except as a compatibility thunk
305
for older plugins that don't use e.g. the CommitBuilder
309
return self._real_repository.commit_write_group()
311
def _ensure_real(self):
312
"""Ensure that there is a _real_repository set.
314
Used before calls to self._real_repository.
316
if not self._real_repository:
317
self.bzrdir._ensure_real()
318
#self._real_repository = self.bzrdir._real_bzrdir.open_repository()
319
self._set_real_repository(self.bzrdir._real_bzrdir.open_repository())
321
def find_text_key_references(self):
322
"""Find the text key references within the repository.
324
:return: a dictionary mapping (file_id, revision_id) tuples to altered file-ids to an iterable of
325
revision_ids. Each altered file-ids has the exact revision_ids that
326
altered it listed explicitly.
327
:return: A dictionary mapping text keys ((fileid, revision_id) tuples)
328
to whether they were referred to by the inventory of the
329
revision_id that they contain. The inventory texts from all present
330
revision ids are assessed to generate this report.
333
return self._real_repository.find_text_key_references()
335
def _generate_text_key_index(self):
336
"""Generate a new text key index for the repository.
338
This is an expensive function that will take considerable time to run.
340
:return: A dict mapping (file_id, revision_id) tuples to a list of
341
parents, also (file_id, revision_id) tuples.
344
return self._real_repository._generate_text_key_index()
346
def get_revision_graph(self, revision_id=None):
347
"""See Repository.get_revision_graph()."""
348
if revision_id is None:
350
elif revision.is_null(revision_id):
353
path = self.bzrdir._path_for_remote_call(self._client)
354
assert type(revision_id) is str
355
response = self._client.call_expecting_body(
356
'Repository.get_revision_graph', path, revision_id)
357
if response[0][0] not in ['ok', 'nosuchrevision']:
358
raise errors.UnexpectedSmartServerResponse(response[0])
359
if response[0][0] == 'ok':
360
coded = response[1].read_body_bytes()
362
# no revisions in this repository!
364
lines = coded.split('\n')
367
d = tuple(line.split())
368
revision_graph[d[0]] = d[1:]
370
return revision_graph
372
response_body = response[1].read_body_bytes()
373
assert response_body == ''
374
raise NoSuchRevision(self, revision_id)
376
def has_revision(self, revision_id):
377
"""See Repository.has_revision()."""
378
if revision_id == NULL_REVISION:
379
# The null revision is always present.
381
path = self.bzrdir._path_for_remote_call(self._client)
382
response = self._client.call('Repository.has_revision', path, revision_id)
383
assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
384
return response[0] == 'yes'
386
def has_revisions(self, revision_ids):
387
"""See Repository.has_revisions()."""
389
for revision_id in revision_ids:
390
if self.has_revision(revision_id):
391
result.add(revision_id)
394
def has_same_location(self, other):
395
return (self.__class__ == other.__class__ and
396
self.bzrdir.transport.base == other.bzrdir.transport.base)
398
def get_graph(self, other_repository=None):
399
"""Return the graph for this repository format"""
400
parents_provider = self
401
if (other_repository is not None and
402
other_repository.bzrdir.transport.base !=
403
self.bzrdir.transport.base):
404
parents_provider = graph._StackedParentsProvider(
405
[parents_provider, other_repository._make_parents_provider()])
406
return graph.Graph(parents_provider)
408
def gather_stats(self, revid=None, committers=None):
409
"""See Repository.gather_stats()."""
410
path = self.bzrdir._path_for_remote_call(self._client)
411
# revid can be None to indicate no revisions, not just NULL_REVISION
412
if revid is None or revision.is_null(revid):
416
if committers is None or not committers:
417
fmt_committers = 'no'
419
fmt_committers = 'yes'
420
response = self._client.call_expecting_body(
421
'Repository.gather_stats', path, fmt_revid, fmt_committers)
422
assert response[0][0] == 'ok', \
423
'unexpected response code %s' % (response[0],)
425
body = response[1].read_body_bytes()
427
for line in body.split('\n'):
430
key, val_text = line.split(':')
431
if key in ('revisions', 'size', 'committers'):
432
result[key] = int(val_text)
433
elif key in ('firstrev', 'latestrev'):
434
values = val_text.split(' ')[1:]
435
result[key] = (float(values[0]), long(values[1]))
439
def find_branches(self, using=False):
440
"""See Repository.find_branches()."""
441
# should be an API call to the server.
443
return self._real_repository.find_branches(using=using)
445
def get_physical_lock_status(self):
446
"""See Repository.get_physical_lock_status()."""
447
# should be an API call to the server.
449
return self._real_repository.get_physical_lock_status()
451
def is_in_write_group(self):
452
"""Return True if there is an open write group.
454
write groups are only applicable locally for the smart server..
456
if self._real_repository:
457
return self._real_repository.is_in_write_group()
460
return self._lock_count >= 1
463
"""See Repository.is_shared()."""
464
path = self.bzrdir._path_for_remote_call(self._client)
465
response = self._client.call('Repository.is_shared', path)
466
assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
467
return response[0] == 'yes'
469
def is_write_locked(self):
470
return self._lock_mode == 'w'
473
# wrong eventually - want a local lock cache context
474
if not self._lock_mode:
475
self._lock_mode = 'r'
477
self._parents_map = {}
478
if 'hpss' in debug.debug_flags:
479
self._requested_parents = set()
480
if self._real_repository is not None:
481
self._real_repository.lock_read()
483
self._lock_count += 1
485
def _remote_lock_write(self, token):
486
path = self.bzrdir._path_for_remote_call(self._client)
489
response = self._client.call('Repository.lock_write', path, token)
490
if response[0] == 'ok':
493
elif response[0] == 'LockContention':
494
raise errors.LockContention('(remote lock)')
495
elif response[0] == 'UnlockableTransport':
496
raise errors.UnlockableTransport(self.bzrdir.root_transport)
497
elif response[0] == 'LockFailed':
498
raise errors.LockFailed(response[1], response[2])
500
raise errors.UnexpectedSmartServerResponse(response)
502
def lock_write(self, token=None):
503
if not self._lock_mode:
504
self._lock_token = self._remote_lock_write(token)
505
# if self._lock_token is None, then this is something like packs or
506
# svn where we don't get to lock the repo, or a weave style repository
507
# where we cannot lock it over the wire and attempts to do so will
509
if self._real_repository is not None:
510
self._real_repository.lock_write(token=self._lock_token)
511
if token is not None:
512
self._leave_lock = True
514
self._leave_lock = False
515
self._lock_mode = 'w'
517
self._parents_map = {}
518
if 'hpss' in debug.debug_flags:
519
self._requested_parents = set()
520
elif self._lock_mode == 'r':
521
raise errors.ReadOnlyError(self)
523
self._lock_count += 1
524
return self._lock_token or None
526
def leave_lock_in_place(self):
527
if not self._lock_token:
528
raise NotImplementedError(self.leave_lock_in_place)
529
self._leave_lock = True
531
def dont_leave_lock_in_place(self):
532
if not self._lock_token:
533
raise NotImplementedError(self.dont_leave_lock_in_place)
534
self._leave_lock = False
536
def _set_real_repository(self, repository):
537
"""Set the _real_repository for this repository.
539
:param repository: The repository to fallback to for non-hpss
540
implemented operations.
542
assert not isinstance(repository, RemoteRepository)
543
self._real_repository = repository
544
if self._lock_mode == 'w':
545
# if we are already locked, the real repository must be able to
546
# acquire the lock with our token.
547
self._real_repository.lock_write(self._lock_token)
548
elif self._lock_mode == 'r':
549
self._real_repository.lock_read()
551
def start_write_group(self):
552
"""Start a write group on the decorated repository.
554
Smart methods peform operations in a single step so this api
555
is not really applicable except as a compatibility thunk
556
for older plugins that don't use e.g. the CommitBuilder
560
return self._real_repository.start_write_group()
562
def _unlock(self, token):
563
path = self.bzrdir._path_for_remote_call(self._client)
565
# with no token the remote repository is not persistently locked.
567
response = self._client.call('Repository.unlock', path, token)
568
if response == ('ok',):
570
elif response[0] == 'TokenMismatch':
571
raise errors.TokenMismatch(token, '(remote token)')
573
raise errors.UnexpectedSmartServerResponse(response)
576
self._lock_count -= 1
577
if self._lock_count > 0:
579
self._parents_map = None
580
if 'hpss' in debug.debug_flags:
581
self._requested_parents = None
582
old_mode = self._lock_mode
583
self._lock_mode = None
585
# The real repository is responsible at present for raising an
586
# exception if it's in an unfinished write group. However, it
587
# normally will *not* actually remove the lock from disk - that's
588
# done by the server on receiving the Repository.unlock call.
589
# This is just to let the _real_repository stay up to date.
590
if self._real_repository is not None:
591
self._real_repository.unlock()
593
# The rpc-level lock should be released even if there was a
594
# problem releasing the vfs-based lock.
596
# Only write-locked repositories need to make a remote method
597
# call to perfom the unlock.
598
old_token = self._lock_token
599
self._lock_token = None
600
if not self._leave_lock:
601
self._unlock(old_token)
603
def break_lock(self):
604
# should hand off to the network
606
return self._real_repository.break_lock()
608
def _get_tarball(self, compression):
609
"""Return a TemporaryFile containing a repository tarball.
611
Returns None if the server does not support sending tarballs.
614
path = self.bzrdir._path_for_remote_call(self._client)
615
response, protocol = self._client.call_expecting_body(
616
'Repository.tarball', path, compression)
617
if response[0] == 'ok':
618
# Extract the tarball and return it
619
t = tempfile.NamedTemporaryFile()
620
# TODO: rpc layer should read directly into it...
621
t.write(protocol.read_body_bytes())
624
if (response == ('error', "Generic bzr smart protocol error: "
625
"bad request 'Repository.tarball'") or
626
response == ('error', "Generic bzr smart protocol error: "
627
"bad request u'Repository.tarball'")):
628
protocol.cancel_read_body()
630
raise errors.UnexpectedSmartServerResponse(response)
632
def sprout(self, to_bzrdir, revision_id=None):
633
# TODO: Option to control what format is created?
635
dest_repo = self._real_repository._format.initialize(to_bzrdir,
637
dest_repo.fetch(self, revision_id=revision_id)
640
### These methods are just thin shims to the VFS object for now.
642
def revision_tree(self, revision_id):
644
return self._real_repository.revision_tree(revision_id)
646
def get_serializer_format(self):
648
return self._real_repository.get_serializer_format()
650
def get_commit_builder(self, branch, parents, config, timestamp=None,
651
timezone=None, committer=None, revprops=None,
653
# FIXME: It ought to be possible to call this without immediately
654
# triggering _ensure_real. For now it's the easiest thing to do.
656
builder = self._real_repository.get_commit_builder(branch, parents,
657
config, timestamp=timestamp, timezone=timezone,
658
committer=committer, revprops=revprops, revision_id=revision_id)
661
def add_inventory(self, revid, inv, parents):
663
return self._real_repository.add_inventory(revid, inv, parents)
665
def add_revision(self, rev_id, rev, inv=None, config=None):
667
return self._real_repository.add_revision(
668
rev_id, rev, inv=inv, config=config)
671
def get_inventory(self, revision_id):
673
return self._real_repository.get_inventory(revision_id)
675
def iter_inventories(self, revision_ids):
677
return self._real_repository.iter_inventories(revision_ids)
680
def get_revision(self, revision_id):
682
return self._real_repository.get_revision(revision_id)
685
def weave_store(self):
687
return self._real_repository.weave_store
689
def get_transaction(self):
691
return self._real_repository.get_transaction()
694
def clone(self, a_bzrdir, revision_id=None):
696
return self._real_repository.clone(a_bzrdir, revision_id=revision_id)
698
def make_working_trees(self):
699
"""RemoteRepositories never create working trees by default."""
702
def revision_ids_to_search_result(self, result_set):
703
"""Convert a set of revision ids to a graph SearchResult."""
704
result_parents = set()
705
for parents in self.get_graph().get_parent_map(
706
result_set).itervalues():
707
result_parents.update(parents)
708
included_keys = result_set.intersection(result_parents)
709
start_keys = result_set.difference(included_keys)
710
exclude_keys = result_parents.difference(result_set)
711
result = graph.SearchResult(start_keys, exclude_keys,
712
len(result_set), result_set)
716
def search_missing_revision_ids(self, other, revision_id=None, find_ghosts=True):
717
"""Return the revision ids that other has that this does not.
719
These are returned in topological order.
721
revision_id: only return revision ids included by revision_id.
723
return repository.InterRepository.get(
724
other, self).search_missing_revision_ids(revision_id, find_ghosts)
726
def fetch(self, source, revision_id=None, pb=None):
727
if self.has_same_location(source):
728
# check that last_revision is in 'from' and then return a
730
if (revision_id is not None and
731
not revision.is_null(revision_id)):
732
self.get_revision(revision_id)
735
return self._real_repository.fetch(
736
source, revision_id=revision_id, pb=pb)
738
def create_bundle(self, target, base, fileobj, format=None):
740
self._real_repository.create_bundle(target, base, fileobj, format)
743
def control_weaves(self):
745
return self._real_repository.control_weaves
748
def get_ancestry(self, revision_id, topo_sorted=True):
750
return self._real_repository.get_ancestry(revision_id, topo_sorted)
753
def get_inventory_weave(self):
755
return self._real_repository.get_inventory_weave()
757
def fileids_altered_by_revision_ids(self, revision_ids):
759
return self._real_repository.fileids_altered_by_revision_ids(revision_ids)
761
def _get_versioned_file_checker(self, revisions, revision_versions_cache):
763
return self._real_repository._get_versioned_file_checker(
764
revisions, revision_versions_cache)
766
def iter_files_bytes(self, desired_files):
767
"""See Repository.iter_file_bytes.
770
return self._real_repository.iter_files_bytes(desired_files)
772
def get_parent_map(self, keys):
773
"""See bzrlib.Graph.get_parent_map()."""
774
# Hack to build up the caching logic.
775
ancestry = self._parents_map
776
missing_revisions = set(key for key in keys if key not in ancestry)
777
if missing_revisions:
778
parent_map = self._get_parent_map(missing_revisions)
779
if 'hpss' in debug.debug_flags:
780
mutter('retransmitted revisions: %d of %d',
781
len(set(self._parents_map).intersection(parent_map)),
783
self._parents_map.update(parent_map)
784
present_keys = [k for k in keys if k in ancestry]
785
if 'hpss' in debug.debug_flags:
786
self._requested_parents.update(present_keys)
787
mutter('Current RemoteRepository graph hit rate: %d%%',
788
100.0 * len(self._requested_parents) / len(self._parents_map))
789
return dict((k, ancestry[k]) for k in present_keys)
791
def _response_is_unknown_method(self, response, verb):
792
"""Return True if response is an unknonwn method response to verb.
794
:param response: The response from a smart client call_expecting_body
796
:param verb: The verb used in that call.
797
:return: True if an unknown method was encountered.
799
# This might live better on
800
# bzrlib.smart.protocol.SmartClientRequestProtocolOne
801
if (response[0] == ('error', "Generic bzr smart protocol error: "
802
"bad request '%s'" % verb) or
803
response[0] == ('error', "Generic bzr smart protocol error: "
804
"bad request u'%s'" % verb)):
805
response[1].cancel_read_body()
809
def _get_parent_map(self, keys):
810
"""Helper for get_parent_map that performs the RPC."""
812
if NULL_REVISION in keys:
813
keys.discard(NULL_REVISION)
814
found_parents = {NULL_REVISION:()}
819
# TODO(Needs analysis): We could assume that the keys being requested
820
# from get_parent_map are in a breadth first search, so typically they
821
# will all be depth N from some common parent, and we don't have to
822
# have the server iterate from the root parent, but rather from the
823
# keys we're searching; and just tell the server the keyspace we
824
# already have; but this may be more traffic again.
826
# Transform self._parents_map into a search request recipe.
827
# TODO: Manage this incrementally to avoid covering the same path
828
# repeatedly. (The server will have to on each request, but the less
829
# work done the better).
830
start_set = set(self._parents_map)
831
result_parents = set()
832
for parents in self._parents_map.itervalues():
833
result_parents.update(parents)
834
stop_keys = result_parents.difference(start_set)
835
included_keys = start_set.intersection(result_parents)
836
start_set.difference_update(included_keys)
837
recipe = (start_set, stop_keys, len(self._parents_map))
838
body = self._serialise_search_recipe(recipe)
839
path = self.bzrdir._path_for_remote_call(self._client)
841
assert type(key) is str
842
verb = 'Repository.get_parent_map'
843
args = (path,) + tuple(keys)
844
response = self._client.call_with_body_bytes_expecting_body(
845
verb, args, self._serialise_search_recipe(recipe))
846
if self._response_is_unknown_method(response, verb):
847
# Server that does not support this method, get the whole graph.
848
response = self._client.call_expecting_body(
849
'Repository.get_revision_graph', path, '')
850
if response[0][0] not in ['ok', 'nosuchrevision']:
851
reponse[1].cancel_read_body()
852
raise errors.UnexpectedSmartServerResponse(response[0])
853
elif response[0][0] not in ['ok']:
854
reponse[1].cancel_read_body()
855
raise errors.UnexpectedSmartServerResponse(response[0])
856
if response[0][0] == 'ok':
857
coded = bz2.decompress(response[1].read_body_bytes())
861
lines = coded.split('\n')
864
d = tuple(line.split())
866
revision_graph[d[0]] = d[1:]
868
# No parents - so give the Graph result (NULL_REVISION,).
869
revision_graph[d[0]] = (NULL_REVISION,)
870
return revision_graph
873
def get_signature_text(self, revision_id):
875
return self._real_repository.get_signature_text(revision_id)
878
def get_revision_graph_with_ghosts(self, revision_ids=None):
880
return self._real_repository.get_revision_graph_with_ghosts(
881
revision_ids=revision_ids)
884
def get_inventory_xml(self, revision_id):
886
return self._real_repository.get_inventory_xml(revision_id)
888
def deserialise_inventory(self, revision_id, xml):
890
return self._real_repository.deserialise_inventory(revision_id, xml)
892
def reconcile(self, other=None, thorough=False):
894
return self._real_repository.reconcile(other=other, thorough=thorough)
896
def all_revision_ids(self):
898
return self._real_repository.all_revision_ids()
901
def get_deltas_for_revisions(self, revisions):
903
return self._real_repository.get_deltas_for_revisions(revisions)
906
def get_revision_delta(self, revision_id):
908
return self._real_repository.get_revision_delta(revision_id)
911
def revision_trees(self, revision_ids):
913
return self._real_repository.revision_trees(revision_ids)
916
def get_revision_reconcile(self, revision_id):
918
return self._real_repository.get_revision_reconcile(revision_id)
921
def check(self, revision_ids=None):
923
return self._real_repository.check(revision_ids=revision_ids)
925
def copy_content_into(self, destination, revision_id=None):
927
return self._real_repository.copy_content_into(
928
destination, revision_id=revision_id)
930
def _copy_repository_tarball(self, to_bzrdir, revision_id=None):
931
# get a tarball of the remote repository, and copy from that into the
933
from bzrlib import osutils
936
# TODO: Maybe a progress bar while streaming the tarball?
937
note("Copying repository content as tarball...")
938
tar_file = self._get_tarball('bz2')
941
destination = to_bzrdir.create_repository()
943
tar = tarfile.open('repository', fileobj=tar_file,
945
tmpdir = tempfile.mkdtemp()
947
_extract_tar(tar, tmpdir)
948
tmp_bzrdir = BzrDir.open(tmpdir)
949
tmp_repo = tmp_bzrdir.open_repository()
950
tmp_repo.copy_content_into(destination, revision_id)
952
osutils.rmtree(tmpdir)
956
# TODO: Suggestion from john: using external tar is much faster than
957
# python's tarfile library, but it may not work on windows.
961
"""Compress the data within the repository.
963
This is not currently implemented within the smart server.
966
return self._real_repository.pack()
968
def set_make_working_trees(self, new_value):
969
raise NotImplementedError(self.set_make_working_trees)
972
def sign_revision(self, revision_id, gpg_strategy):
974
return self._real_repository.sign_revision(revision_id, gpg_strategy)
977
def get_revisions(self, revision_ids):
979
return self._real_repository.get_revisions(revision_ids)
981
def supports_rich_root(self):
983
return self._real_repository.supports_rich_root()
985
def iter_reverse_revision_history(self, revision_id):
987
return self._real_repository.iter_reverse_revision_history(revision_id)
990
def _serializer(self):
992
return self._real_repository._serializer
994
def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
996
return self._real_repository.store_revision_signature(
997
gpg_strategy, plaintext, revision_id)
999
def add_signature_text(self, revision_id, signature):
1001
return self._real_repository.add_signature_text(revision_id, signature)
1003
def has_signature_for_revision_id(self, revision_id):
1005
return self._real_repository.has_signature_for_revision_id(revision_id)
1007
def get_data_stream_for_search(self, search):
1008
REQUEST_NAME = 'Repository.stream_revisions_chunked'
1009
path = self.bzrdir._path_for_remote_call(self._client)
1010
body = self._serialise_search_recipe(search.get_recipe())
1011
response, protocol = self._client.call_with_body_bytes_expecting_body(
1012
REQUEST_NAME, (path,), body)
1014
if response == ('ok',):
1015
return self._deserialise_stream(protocol)
1016
if response == ('NoSuchRevision', ):
1017
# We cannot easily identify the revision that is missing in this
1018
# situation without doing much more network IO. For now, bail.
1019
raise NoSuchRevision(self, "unknown")
1020
elif (response == ('error', "Generic bzr smart protocol error: "
1021
"bad request '%s'" % REQUEST_NAME) or
1022
response == ('error', "Generic bzr smart protocol error: "
1023
"bad request u'%s'" % REQUEST_NAME)):
1024
protocol.cancel_read_body()
1026
return self._real_repository.get_data_stream_for_search(search)
1028
raise errors.UnexpectedSmartServerResponse(response)
1030
def _deserialise_stream(self, protocol):
1031
stream = protocol.read_streamed_body()
1032
container_parser = ContainerPushParser()
1033
for bytes in stream:
1034
container_parser.accept_bytes(bytes)
1035
records = container_parser.read_pending_records()
1036
for record_names, record_bytes in records:
1037
if len(record_names) != 1:
1038
# These records should have only one name, and that name
1039
# should be a one-element tuple.
1040
raise errors.SmartProtocolError(
1041
'Repository data stream had invalid record name %r'
1043
name_tuple = record_names[0]
1044
yield name_tuple, record_bytes
1046
def insert_data_stream(self, stream):
1048
self._real_repository.insert_data_stream(stream)
1050
def item_keys_introduced_by(self, revision_ids, _files_pb=None):
1052
return self._real_repository.item_keys_introduced_by(revision_ids,
1053
_files_pb=_files_pb)
1055
def revision_graph_can_have_wrong_parents(self):
1056
# The answer depends on the remote repo format.
1058
return self._real_repository.revision_graph_can_have_wrong_parents()
1060
def _find_inconsistent_revision_parents(self):
1062
return self._real_repository._find_inconsistent_revision_parents()
1064
def _check_for_inconsistent_revision_parents(self):
1066
return self._real_repository._check_for_inconsistent_revision_parents()
1068
def _make_parents_provider(self):
1071
def _serialise_search_recipe(self, recipe):
1072
"""Serialise a graph search recipe.
1074
:param recipe: A search recipe (start, stop, count).
1075
:return: Serialised bytes.
1077
start_keys = ' '.join(recipe[0])
1078
stop_keys = ' '.join(recipe[1])
1079
count = str(recipe[2])
1080
return '\n'.join((start_keys, stop_keys, count))
1083
class RemoteBranchLockableFiles(LockableFiles):
1084
"""A 'LockableFiles' implementation that talks to a smart server.
1086
This is not a public interface class.
1089
def __init__(self, bzrdir, _client):
1090
self.bzrdir = bzrdir
1091
self._client = _client
1092
self._need_find_modes = True
1093
LockableFiles.__init__(
1094
self, bzrdir.get_branch_transport(None),
1095
'lock', lockdir.LockDir)
1097
def _find_modes(self):
1098
# RemoteBranches don't let the client set the mode of control files.
1099
self._dir_mode = None
1100
self._file_mode = None
1102
def get(self, path):
1103
"""'get' a remote path as per the LockableFiles interface.
1105
:param path: the file to 'get'. If this is 'branch.conf', we do not
1106
just retrieve a file, instead we ask the smart server to generate
1107
a configuration for us - which is retrieved as an INI file.
1109
if path == 'branch.conf':
1110
path = self.bzrdir._path_for_remote_call(self._client)
1111
response = self._client.call_expecting_body(
1112
'Branch.get_config_file', path)
1113
assert response[0][0] == 'ok', \
1114
'unexpected response code %s' % (response[0],)
1115
return StringIO(response[1].read_body_bytes())
1118
return LockableFiles.get(self, path)
1121
class RemoteBranchFormat(branch.BranchFormat):
1123
def __eq__(self, other):
1124
return (isinstance(other, RemoteBranchFormat) and
1125
self.__dict__ == other.__dict__)
1127
def get_format_description(self):
1128
return 'Remote BZR Branch'
1130
def get_format_string(self):
1131
return 'Remote BZR Branch'
1133
def open(self, a_bzrdir):
1134
assert isinstance(a_bzrdir, RemoteBzrDir)
1135
return a_bzrdir.open_branch()
1137
def initialize(self, a_bzrdir):
1138
assert isinstance(a_bzrdir, RemoteBzrDir)
1139
return a_bzrdir.create_branch()
1141
def supports_tags(self):
1142
# Remote branches might support tags, but we won't know until we
1143
# access the real remote branch.
1147
class RemoteBranch(branch.Branch):
1148
"""Branch stored on a server accessed by HPSS RPC.
1150
At the moment most operations are mapped down to simple file operations.
1153
def __init__(self, remote_bzrdir, remote_repository, real_branch=None,
1155
"""Create a RemoteBranch instance.
1157
:param real_branch: An optional local implementation of the branch
1158
format, usually accessing the data via the VFS.
1159
:param _client: Private parameter for testing.
1161
# We intentionally don't call the parent class's __init__, because it
1162
# will try to assign to self.tags, which is a property in this subclass.
1163
# And the parent's __init__ doesn't do much anyway.
1164
self._revision_id_to_revno_cache = None
1165
self._revision_history_cache = None
1166
self.bzrdir = remote_bzrdir
1167
if _client is not None:
1168
self._client = _client
1170
self._client = client._SmartClient(self.bzrdir._shared_medium)
1171
self.repository = remote_repository
1172
if real_branch is not None:
1173
self._real_branch = real_branch
1174
# Give the remote repository the matching real repo.
1175
real_repo = self._real_branch.repository
1176
if isinstance(real_repo, RemoteRepository):
1177
real_repo._ensure_real()
1178
real_repo = real_repo._real_repository
1179
self.repository._set_real_repository(real_repo)
1180
# Give the branch the remote repository to let fast-pathing happen.
1181
self._real_branch.repository = self.repository
1183
self._real_branch = None
1184
# Fill out expected attributes of branch for bzrlib api users.
1185
self._format = RemoteBranchFormat()
1186
self.base = self.bzrdir.root_transport.base
1187
self._control_files = None
1188
self._lock_mode = None
1189
self._lock_token = None
1190
self._lock_count = 0
1191
self._leave_lock = False
1194
return "%s(%s)" % (self.__class__.__name__, self.base)
1198
def _ensure_real(self):
1199
"""Ensure that there is a _real_branch set.
1201
Used before calls to self._real_branch.
1203
if not self._real_branch:
1204
assert vfs.vfs_enabled()
1205
self.bzrdir._ensure_real()
1206
self._real_branch = self.bzrdir._real_bzrdir.open_branch()
1207
# Give the remote repository the matching real repo.
1208
real_repo = self._real_branch.repository
1209
if isinstance(real_repo, RemoteRepository):
1210
real_repo._ensure_real()
1211
real_repo = real_repo._real_repository
1212
self.repository._set_real_repository(real_repo)
1213
# Give the branch the remote repository to let fast-pathing happen.
1214
self._real_branch.repository = self.repository
1215
# XXX: deal with _lock_mode == 'w'
1216
if self._lock_mode == 'r':
1217
self._real_branch.lock_read()
1220
def control_files(self):
1221
# Defer actually creating RemoteBranchLockableFiles until its needed,
1222
# because it triggers an _ensure_real that we otherwise might not need.
1223
if self._control_files is None:
1224
self._control_files = RemoteBranchLockableFiles(
1225
self.bzrdir, self._client)
1226
return self._control_files
1228
def _get_checkout_format(self):
1230
return self._real_branch._get_checkout_format()
1232
def get_physical_lock_status(self):
1233
"""See Branch.get_physical_lock_status()."""
1234
# should be an API call to the server, as branches must be lockable.
1236
return self._real_branch.get_physical_lock_status()
1238
def lock_read(self):
1239
if not self._lock_mode:
1240
self._lock_mode = 'r'
1241
self._lock_count = 1
1242
if self._real_branch is not None:
1243
self._real_branch.lock_read()
1245
self._lock_count += 1
1247
def _remote_lock_write(self, token):
1249
branch_token = repo_token = ''
1251
branch_token = token
1252
repo_token = self.repository.lock_write()
1253
self.repository.unlock()
1254
path = self.bzrdir._path_for_remote_call(self._client)
1255
response = self._client.call('Branch.lock_write', path, branch_token,
1257
if response[0] == 'ok':
1258
ok, branch_token, repo_token = response
1259
return branch_token, repo_token
1260
elif response[0] == 'LockContention':
1261
raise errors.LockContention('(remote lock)')
1262
elif response[0] == 'TokenMismatch':
1263
raise errors.TokenMismatch(token, '(remote token)')
1264
elif response[0] == 'UnlockableTransport':
1265
raise errors.UnlockableTransport(self.bzrdir.root_transport)
1266
elif response[0] == 'ReadOnlyError':
1267
raise errors.ReadOnlyError(self)
1268
elif response[0] == 'LockFailed':
1269
raise errors.LockFailed(response[1], response[2])
1271
raise errors.UnexpectedSmartServerResponse(response)
1273
def lock_write(self, token=None):
1274
if not self._lock_mode:
1275
remote_tokens = self._remote_lock_write(token)
1276
self._lock_token, self._repo_lock_token = remote_tokens
1277
assert self._lock_token, 'Remote server did not return a token!'
1278
# TODO: We really, really, really don't want to call _ensure_real
1279
# here, but it's the easiest way to ensure coherency between the
1280
# state of the RemoteBranch and RemoteRepository objects and the
1281
# physical locks. If we don't materialise the real objects here,
1282
# then getting everything in the right state later is complex, so
1283
# for now we just do it the lazy way.
1284
# -- Andrew Bennetts, 2007-02-22.
1286
if self._real_branch is not None:
1287
self._real_branch.repository.lock_write(
1288
token=self._repo_lock_token)
1290
self._real_branch.lock_write(token=self._lock_token)
1292
self._real_branch.repository.unlock()
1293
if token is not None:
1294
self._leave_lock = True
1296
# XXX: this case seems to be unreachable; token cannot be None.
1297
self._leave_lock = False
1298
self._lock_mode = 'w'
1299
self._lock_count = 1
1300
elif self._lock_mode == 'r':
1301
raise errors.ReadOnlyTransaction
1303
if token is not None:
1304
# A token was given to lock_write, and we're relocking, so check
1305
# that the given token actually matches the one we already have.
1306
if token != self._lock_token:
1307
raise errors.TokenMismatch(token, self._lock_token)
1308
self._lock_count += 1
1309
return self._lock_token or None
1311
def _unlock(self, branch_token, repo_token):
1312
path = self.bzrdir._path_for_remote_call(self._client)
1313
response = self._client.call('Branch.unlock', path, branch_token,
1315
if response == ('ok',):
1317
elif response[0] == 'TokenMismatch':
1318
raise errors.TokenMismatch(
1319
str((branch_token, repo_token)), '(remote tokens)')
1321
raise errors.UnexpectedSmartServerResponse(response)
1324
self._lock_count -= 1
1325
if not self._lock_count:
1326
self._clear_cached_state()
1327
mode = self._lock_mode
1328
self._lock_mode = None
1329
if self._real_branch is not None:
1330
if (not self._leave_lock and mode == 'w' and
1331
self._repo_lock_token):
1332
# If this RemoteBranch will remove the physical lock for the
1333
# repository, make sure the _real_branch doesn't do it
1334
# first. (Because the _real_branch's repository is set to
1335
# be the RemoteRepository.)
1336
self._real_branch.repository.leave_lock_in_place()
1337
self._real_branch.unlock()
1339
# Only write-locked branched need to make a remote method call
1340
# to perfom the unlock.
1342
assert self._lock_token, 'Locked, but no token!'
1343
branch_token = self._lock_token
1344
repo_token = self._repo_lock_token
1345
self._lock_token = None
1346
self._repo_lock_token = None
1347
if not self._leave_lock:
1348
self._unlock(branch_token, repo_token)
1350
def break_lock(self):
1352
return self._real_branch.break_lock()
1354
def leave_lock_in_place(self):
1355
if not self._lock_token:
1356
raise NotImplementedError(self.leave_lock_in_place)
1357
self._leave_lock = True
1359
def dont_leave_lock_in_place(self):
1360
if not self._lock_token:
1361
raise NotImplementedError(self.dont_leave_lock_in_place)
1362
self._leave_lock = False
1364
def last_revision_info(self):
1365
"""See Branch.last_revision_info()."""
1366
path = self.bzrdir._path_for_remote_call(self._client)
1367
response = self._client.call('Branch.last_revision_info', path)
1368
assert response[0] == 'ok', 'unexpected response code %s' % (response,)
1369
revno = int(response[1])
1370
last_revision = response[2]
1371
return (revno, last_revision)
1373
def _gen_revision_history(self):
1374
"""See Branch._gen_revision_history()."""
1375
path = self.bzrdir._path_for_remote_call(self._client)
1376
response = self._client.call_expecting_body(
1377
'Branch.revision_history', path)
1378
assert response[0][0] == 'ok', ('unexpected response code %s'
1380
result = response[1].read_body_bytes().split('\x00')
1386
def set_revision_history(self, rev_history):
1387
# Send just the tip revision of the history; the server will generate
1388
# the full history from that. If the revision doesn't exist in this
1389
# branch, NoSuchRevision will be raised.
1390
path = self.bzrdir._path_for_remote_call(self._client)
1391
if rev_history == []:
1394
rev_id = rev_history[-1]
1395
self._clear_cached_state()
1396
response = self._client.call('Branch.set_last_revision',
1397
path, self._lock_token, self._repo_lock_token, rev_id)
1398
if response[0] == 'NoSuchRevision':
1399
raise NoSuchRevision(self, rev_id)
1401
assert response == ('ok',), (
1402
'unexpected response code %r' % (response,))
1403
self._cache_revision_history(rev_history)
1405
def get_parent(self):
1407
return self._real_branch.get_parent()
1409
def set_parent(self, url):
1411
return self._real_branch.set_parent(url)
1413
def get_config(self):
1414
return RemoteBranchConfig(self)
1416
def sprout(self, to_bzrdir, revision_id=None):
1417
# Like Branch.sprout, except that it sprouts a branch in the default
1418
# format, because RemoteBranches can't be created at arbitrary URLs.
1419
# XXX: if to_bzrdir is a RemoteBranch, this should perhaps do
1420
# to_bzrdir.create_branch...
1422
result = self._real_branch._format.initialize(to_bzrdir)
1423
self.copy_content_into(result, revision_id=revision_id)
1424
result.set_parent(self.bzrdir.root_transport.base)
1428
def pull(self, source, overwrite=False, stop_revision=None,
1430
# FIXME: This asks the real branch to run the hooks, which means
1431
# they're called with the wrong target branch parameter.
1432
# The test suite specifically allows this at present but it should be
1433
# fixed. It should get a _override_hook_target branch,
1434
# as push does. -- mbp 20070405
1436
self._real_branch.pull(
1437
source, overwrite=overwrite, stop_revision=stop_revision,
1441
def push(self, target, overwrite=False, stop_revision=None):
1443
return self._real_branch.push(
1444
target, overwrite=overwrite, stop_revision=stop_revision,
1445
_override_hook_source_branch=self)
1447
def is_locked(self):
1448
return self._lock_count >= 1
1450
def set_last_revision_info(self, revno, revision_id):
1452
self._clear_cached_state()
1453
return self._real_branch.set_last_revision_info(revno, revision_id)
1455
def generate_revision_history(self, revision_id, last_rev=None,
1458
return self._real_branch.generate_revision_history(
1459
revision_id, last_rev=last_rev, other_branch=other_branch)
1464
return self._real_branch.tags
1466
def set_push_location(self, location):
1468
return self._real_branch.set_push_location(location)
1470
def update_revisions(self, other, stop_revision=None, overwrite=False):
1472
return self._real_branch.update_revisions(
1473
other, stop_revision=stop_revision, overwrite=overwrite)
1476
class RemoteBranchConfig(BranchConfig):
1479
self.branch._ensure_real()
1480
return self.branch._real_branch.get_config().username()
1482
def _get_branch_data_config(self):
1483
self.branch._ensure_real()
1484
if self._branch_data_config is None:
1485
self._branch_data_config = TreeConfig(self.branch._real_branch)
1486
return self._branch_data_config
1489
def _extract_tar(tar, to_dir):
1490
"""Extract all the contents of a tarfile object.
1492
A replacement for extractall, which is not present in python2.4
1495
tar.extract(tarinfo, to_dir)