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, warning
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 _get_tree_branch(self):
135
"""See BzrDir._get_tree_branch()."""
136
return None, self.open_branch()
138
def open_branch(self, _unsupported=False):
139
assert _unsupported == False, 'unsupported flag support not implemented yet.'
140
reference_url = self.get_branch_reference()
141
if reference_url is None:
142
# branch at this location.
143
return RemoteBranch(self, self.find_repository())
145
# a branch reference, use the existing BranchReference logic.
146
format = BranchReferenceFormat()
147
return format.open(self, _found=True, location=reference_url)
149
def open_repository(self):
150
path = self._path_for_remote_call(self._client)
151
verb = 'BzrDir.find_repositoryV2'
152
response = self._client.call(verb, path)
153
if (response == ('error', "Generic bzr smart protocol error: "
154
"bad request '%s'" % verb) or
155
response == ('error', "Generic bzr smart protocol error: "
156
"bad request u'%s'" % verb)):
157
verb = 'BzrDir.find_repository'
158
response = self._client.call(verb, path)
159
assert response[0] in ('ok', 'norepository'), \
160
'unexpected response code %s' % (response,)
161
if response[0] == 'norepository':
162
raise errors.NoRepositoryPresent(self)
163
if verb == 'BzrDir.find_repository':
164
# servers that don't support the V2 method don't support external
166
response = response + ('no', )
167
assert len(response) == 5, 'incorrect response length %s' % (response,)
168
if response[1] == '':
169
format = RemoteRepositoryFormat()
170
format.rich_root_data = (response[2] == 'yes')
171
format.supports_tree_reference = (response[3] == 'yes')
172
# No wire format to check this yet.
173
format.supports_external_lookups = (response[4] == 'yes')
174
return RemoteRepository(self, format)
176
raise errors.NoRepositoryPresent(self)
178
def open_workingtree(self, recommend_upgrade=True):
180
if self._real_bzrdir.has_workingtree():
181
raise errors.NotLocalUrl(self.root_transport)
183
raise errors.NoWorkingTree(self.root_transport.base)
185
def _path_for_remote_call(self, client):
186
"""Return the path to be used for this bzrdir in a remote call."""
187
return client.remote_path_from_transport(self.root_transport)
189
def get_branch_transport(self, branch_format):
191
return self._real_bzrdir.get_branch_transport(branch_format)
193
def get_repository_transport(self, repository_format):
195
return self._real_bzrdir.get_repository_transport(repository_format)
197
def get_workingtree_transport(self, workingtree_format):
199
return self._real_bzrdir.get_workingtree_transport(workingtree_format)
201
def can_convert_format(self):
202
"""Upgrading of remote bzrdirs is not supported yet."""
205
def needs_format_conversion(self, format=None):
206
"""Upgrading of remote bzrdirs is not supported yet."""
209
def clone(self, url, revision_id=None, force_new_repo=False):
211
return self._real_bzrdir.clone(url, revision_id=revision_id,
212
force_new_repo=force_new_repo)
215
class RemoteRepositoryFormat(repository.RepositoryFormat):
216
"""Format for repositories accessed over a _SmartClient.
218
Instances of this repository are represented by RemoteRepository
221
The RemoteRepositoryFormat is parameterized during construction
222
to reflect the capabilities of the real, remote format. Specifically
223
the attributes rich_root_data and supports_tree_reference are set
224
on a per instance basis, and are not set (and should not be) at
228
_matchingbzrdir = RemoteBzrDirFormat
230
def initialize(self, a_bzrdir, shared=False):
231
assert isinstance(a_bzrdir, RemoteBzrDir), \
232
'%r is not a RemoteBzrDir' % (a_bzrdir,)
233
return a_bzrdir.create_repository(shared=shared)
235
def open(self, a_bzrdir):
236
assert isinstance(a_bzrdir, RemoteBzrDir)
237
return a_bzrdir.open_repository()
239
def get_format_description(self):
240
return 'bzr remote repository'
242
def __eq__(self, other):
243
return self.__class__ == other.__class__
245
def check_conversion_target(self, target_format):
246
if self.rich_root_data and not target_format.rich_root_data:
247
raise errors.BadConversionTarget(
248
'Does not support rich root data.', target_format)
249
if (self.supports_tree_reference and
250
not getattr(target_format, 'supports_tree_reference', False)):
251
raise errors.BadConversionTarget(
252
'Does not support nested trees', target_format)
255
class RemoteRepository(object):
256
"""Repository accessed over rpc.
258
For the moment most operations are performed using local transport-backed
262
def __init__(self, remote_bzrdir, format, real_repository=None, _client=None):
263
"""Create a RemoteRepository instance.
265
:param remote_bzrdir: The bzrdir hosting this repository.
266
:param format: The RemoteFormat object to use.
267
:param real_repository: If not None, a local implementation of the
268
repository logic for the repository, usually accessing the data
270
:param _client: Private testing parameter - override the smart client
271
to be used by the repository.
274
self._real_repository = real_repository
276
self._real_repository = None
277
self.bzrdir = remote_bzrdir
279
self._client = client._SmartClient(self.bzrdir._shared_medium)
281
self._client = _client
282
self._format = format
283
self._lock_mode = None
284
self._lock_token = None
286
self._leave_lock = False
287
# A cache of looked up revision parent data; reset at unlock time.
288
self._parents_map = None
289
if 'hpss' in debug.debug_flags:
290
self._requested_parents = None
292
# These depend on the actual remote format, so force them off for
293
# maximum compatibility. XXX: In future these should depend on the
294
# remote repository instance, but this is irrelevant until we perform
295
# reconcile via an RPC call.
296
self._reconcile_does_inventory_gc = False
297
self._reconcile_fixes_text_parents = False
298
self._reconcile_backsup_inventory = False
299
self.base = self.bzrdir.transport.base
302
return "%s(%s)" % (self.__class__.__name__, self.base)
306
def abort_write_group(self):
307
"""Complete a write group on the decorated repository.
309
Smart methods peform operations in a single step so this api
310
is not really applicable except as a compatibility thunk
311
for older plugins that don't use e.g. the CommitBuilder
315
return self._real_repository.abort_write_group()
317
def commit_write_group(self):
318
"""Complete a write group on the decorated repository.
320
Smart methods peform operations in a single step so this api
321
is not really applicable except as a compatibility thunk
322
for older plugins that don't use e.g. the CommitBuilder
326
return self._real_repository.commit_write_group()
328
def _ensure_real(self):
329
"""Ensure that there is a _real_repository set.
331
Used before calls to self._real_repository.
333
if not self._real_repository:
334
self.bzrdir._ensure_real()
335
#self._real_repository = self.bzrdir._real_bzrdir.open_repository()
336
self._set_real_repository(self.bzrdir._real_bzrdir.open_repository())
338
def find_text_key_references(self):
339
"""Find the text key references within the repository.
341
:return: a dictionary mapping (file_id, revision_id) tuples to altered file-ids to an iterable of
342
revision_ids. Each altered file-ids has the exact revision_ids that
343
altered it listed explicitly.
344
:return: A dictionary mapping text keys ((fileid, revision_id) tuples)
345
to whether they were referred to by the inventory of the
346
revision_id that they contain. The inventory texts from all present
347
revision ids are assessed to generate this report.
350
return self._real_repository.find_text_key_references()
352
def _generate_text_key_index(self):
353
"""Generate a new text key index for the repository.
355
This is an expensive function that will take considerable time to run.
357
:return: A dict mapping (file_id, revision_id) tuples to a list of
358
parents, also (file_id, revision_id) tuples.
361
return self._real_repository._generate_text_key_index()
363
def get_revision_graph(self, revision_id=None):
364
"""See Repository.get_revision_graph()."""
365
if revision_id is None:
367
elif revision.is_null(revision_id):
370
path = self.bzrdir._path_for_remote_call(self._client)
371
assert type(revision_id) is str
372
response = self._client.call_expecting_body(
373
'Repository.get_revision_graph', path, revision_id)
374
if response[0][0] not in ['ok', 'nosuchrevision']:
375
raise errors.UnexpectedSmartServerResponse(response[0])
376
if response[0][0] == 'ok':
377
coded = response[1].read_body_bytes()
379
# no revisions in this repository!
381
lines = coded.split('\n')
384
d = tuple(line.split())
385
revision_graph[d[0]] = d[1:]
387
return revision_graph
389
response_body = response[1].read_body_bytes()
390
assert response_body == ''
391
raise NoSuchRevision(self, revision_id)
393
def has_revision(self, revision_id):
394
"""See Repository.has_revision()."""
395
if revision_id == NULL_REVISION:
396
# The null revision is always present.
398
path = self.bzrdir._path_for_remote_call(self._client)
399
response = self._client.call('Repository.has_revision', path, revision_id)
400
assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
401
return response[0] == 'yes'
403
def has_revisions(self, revision_ids):
404
"""See Repository.has_revisions()."""
406
for revision_id in revision_ids:
407
if self.has_revision(revision_id):
408
result.add(revision_id)
411
def has_same_location(self, other):
412
return (self.__class__ == other.__class__ and
413
self.bzrdir.transport.base == other.bzrdir.transport.base)
415
def get_graph(self, other_repository=None):
416
"""Return the graph for this repository format"""
417
parents_provider = self
418
if (other_repository is not None and
419
other_repository.bzrdir.transport.base !=
420
self.bzrdir.transport.base):
421
parents_provider = graph._StackedParentsProvider(
422
[parents_provider, other_repository._make_parents_provider()])
423
return graph.Graph(parents_provider)
425
def gather_stats(self, revid=None, committers=None):
426
"""See Repository.gather_stats()."""
427
path = self.bzrdir._path_for_remote_call(self._client)
428
# revid can be None to indicate no revisions, not just NULL_REVISION
429
if revid is None or revision.is_null(revid):
433
if committers is None or not committers:
434
fmt_committers = 'no'
436
fmt_committers = 'yes'
437
response = self._client.call_expecting_body(
438
'Repository.gather_stats', path, fmt_revid, fmt_committers)
439
assert response[0][0] == 'ok', \
440
'unexpected response code %s' % (response[0],)
442
body = response[1].read_body_bytes()
444
for line in body.split('\n'):
447
key, val_text = line.split(':')
448
if key in ('revisions', 'size', 'committers'):
449
result[key] = int(val_text)
450
elif key in ('firstrev', 'latestrev'):
451
values = val_text.split(' ')[1:]
452
result[key] = (float(values[0]), long(values[1]))
456
def find_branches(self, using=False):
457
"""See Repository.find_branches()."""
458
# should be an API call to the server.
460
return self._real_repository.find_branches(using=using)
462
def get_physical_lock_status(self):
463
"""See Repository.get_physical_lock_status()."""
464
# should be an API call to the server.
466
return self._real_repository.get_physical_lock_status()
468
def is_in_write_group(self):
469
"""Return True if there is an open write group.
471
write groups are only applicable locally for the smart server..
473
if self._real_repository:
474
return self._real_repository.is_in_write_group()
477
return self._lock_count >= 1
480
"""See Repository.is_shared()."""
481
path = self.bzrdir._path_for_remote_call(self._client)
482
response = self._client.call('Repository.is_shared', path)
483
assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
484
return response[0] == 'yes'
486
def is_write_locked(self):
487
return self._lock_mode == 'w'
490
# wrong eventually - want a local lock cache context
491
if not self._lock_mode:
492
self._lock_mode = 'r'
494
self._parents_map = {}
495
if 'hpss' in debug.debug_flags:
496
self._requested_parents = set()
497
if self._real_repository is not None:
498
self._real_repository.lock_read()
500
self._lock_count += 1
502
def _remote_lock_write(self, token):
503
path = self.bzrdir._path_for_remote_call(self._client)
506
response = self._client.call('Repository.lock_write', path, token)
507
if response[0] == 'ok':
510
elif response[0] == 'LockContention':
511
raise errors.LockContention('(remote lock)')
512
elif response[0] == 'UnlockableTransport':
513
raise errors.UnlockableTransport(self.bzrdir.root_transport)
514
elif response[0] == 'LockFailed':
515
raise errors.LockFailed(response[1], response[2])
517
raise errors.UnexpectedSmartServerResponse(response)
519
def lock_write(self, token=None):
520
if not self._lock_mode:
521
self._lock_token = self._remote_lock_write(token)
522
# if self._lock_token is None, then this is something like packs or
523
# svn where we don't get to lock the repo, or a weave style repository
524
# where we cannot lock it over the wire and attempts to do so will
526
if self._real_repository is not None:
527
self._real_repository.lock_write(token=self._lock_token)
528
if token is not None:
529
self._leave_lock = True
531
self._leave_lock = False
532
self._lock_mode = 'w'
534
self._parents_map = {}
535
if 'hpss' in debug.debug_flags:
536
self._requested_parents = set()
537
elif self._lock_mode == 'r':
538
raise errors.ReadOnlyError(self)
540
self._lock_count += 1
541
return self._lock_token or None
543
def leave_lock_in_place(self):
544
if not self._lock_token:
545
raise NotImplementedError(self.leave_lock_in_place)
546
self._leave_lock = True
548
def dont_leave_lock_in_place(self):
549
if not self._lock_token:
550
raise NotImplementedError(self.dont_leave_lock_in_place)
551
self._leave_lock = False
553
def _set_real_repository(self, repository):
554
"""Set the _real_repository for this repository.
556
:param repository: The repository to fallback to for non-hpss
557
implemented operations.
559
assert not isinstance(repository, RemoteRepository)
560
self._real_repository = repository
561
if self._lock_mode == 'w':
562
# if we are already locked, the real repository must be able to
563
# acquire the lock with our token.
564
self._real_repository.lock_write(self._lock_token)
565
elif self._lock_mode == 'r':
566
self._real_repository.lock_read()
568
def start_write_group(self):
569
"""Start a write group on the decorated repository.
571
Smart methods peform operations in a single step so this api
572
is not really applicable except as a compatibility thunk
573
for older plugins that don't use e.g. the CommitBuilder
577
return self._real_repository.start_write_group()
579
def _unlock(self, token):
580
path = self.bzrdir._path_for_remote_call(self._client)
582
# with no token the remote repository is not persistently locked.
584
response = self._client.call('Repository.unlock', path, token)
585
if response == ('ok',):
587
elif response[0] == 'TokenMismatch':
588
raise errors.TokenMismatch(token, '(remote token)')
590
raise errors.UnexpectedSmartServerResponse(response)
593
self._lock_count -= 1
594
if self._lock_count > 0:
596
self._parents_map = None
597
if 'hpss' in debug.debug_flags:
598
self._requested_parents = None
599
old_mode = self._lock_mode
600
self._lock_mode = None
602
# The real repository is responsible at present for raising an
603
# exception if it's in an unfinished write group. However, it
604
# normally will *not* actually remove the lock from disk - that's
605
# done by the server on receiving the Repository.unlock call.
606
# This is just to let the _real_repository stay up to date.
607
if self._real_repository is not None:
608
self._real_repository.unlock()
610
# The rpc-level lock should be released even if there was a
611
# problem releasing the vfs-based lock.
613
# Only write-locked repositories need to make a remote method
614
# call to perfom the unlock.
615
old_token = self._lock_token
616
self._lock_token = None
617
if not self._leave_lock:
618
self._unlock(old_token)
620
def break_lock(self):
621
# should hand off to the network
623
return self._real_repository.break_lock()
625
def _get_tarball(self, compression):
626
"""Return a TemporaryFile containing a repository tarball.
628
Returns None if the server does not support sending tarballs.
631
path = self.bzrdir._path_for_remote_call(self._client)
632
response, protocol = self._client.call_expecting_body(
633
'Repository.tarball', path, compression)
634
if response[0] == 'ok':
635
# Extract the tarball and return it
636
t = tempfile.NamedTemporaryFile()
637
# TODO: rpc layer should read directly into it...
638
t.write(protocol.read_body_bytes())
641
if (response == ('error', "Generic bzr smart protocol error: "
642
"bad request 'Repository.tarball'") or
643
response == ('error', "Generic bzr smart protocol error: "
644
"bad request u'Repository.tarball'")):
645
protocol.cancel_read_body()
647
raise errors.UnexpectedSmartServerResponse(response)
649
def sprout(self, to_bzrdir, revision_id=None):
650
# TODO: Option to control what format is created?
652
dest_repo = self._real_repository._format.initialize(to_bzrdir,
654
dest_repo.fetch(self, revision_id=revision_id)
657
### These methods are just thin shims to the VFS object for now.
659
def revision_tree(self, revision_id):
661
return self._real_repository.revision_tree(revision_id)
663
def get_serializer_format(self):
665
return self._real_repository.get_serializer_format()
667
def get_commit_builder(self, branch, parents, config, timestamp=None,
668
timezone=None, committer=None, revprops=None,
670
# FIXME: It ought to be possible to call this without immediately
671
# triggering _ensure_real. For now it's the easiest thing to do.
673
builder = self._real_repository.get_commit_builder(branch, parents,
674
config, timestamp=timestamp, timezone=timezone,
675
committer=committer, revprops=revprops, revision_id=revision_id)
678
def add_inventory(self, revid, inv, parents):
680
return self._real_repository.add_inventory(revid, inv, parents)
682
def add_revision(self, rev_id, rev, inv=None, config=None):
684
return self._real_repository.add_revision(
685
rev_id, rev, inv=inv, config=config)
688
def get_inventory(self, revision_id):
690
return self._real_repository.get_inventory(revision_id)
692
def iter_inventories(self, revision_ids):
694
return self._real_repository.iter_inventories(revision_ids)
697
def get_revision(self, revision_id):
699
return self._real_repository.get_revision(revision_id)
702
def weave_store(self):
704
return self._real_repository.weave_store
706
def get_transaction(self):
708
return self._real_repository.get_transaction()
711
def clone(self, a_bzrdir, revision_id=None):
713
return self._real_repository.clone(a_bzrdir, revision_id=revision_id)
715
def make_working_trees(self):
716
"""RemoteRepositories never create working trees by default."""
719
def revision_ids_to_search_result(self, result_set):
720
"""Convert a set of revision ids to a graph SearchResult."""
721
result_parents = set()
722
for parents in self.get_graph().get_parent_map(
723
result_set).itervalues():
724
result_parents.update(parents)
725
included_keys = result_set.intersection(result_parents)
726
start_keys = result_set.difference(included_keys)
727
exclude_keys = result_parents.difference(result_set)
728
result = graph.SearchResult(start_keys, exclude_keys,
729
len(result_set), result_set)
733
def search_missing_revision_ids(self, other, revision_id=None, find_ghosts=True):
734
"""Return the revision ids that other has that this does not.
736
These are returned in topological order.
738
revision_id: only return revision ids included by revision_id.
740
return repository.InterRepository.get(
741
other, self).search_missing_revision_ids(revision_id, find_ghosts)
743
def fetch(self, source, revision_id=None, pb=None):
744
if self.has_same_location(source):
745
# check that last_revision is in 'from' and then return a
747
if (revision_id is not None and
748
not revision.is_null(revision_id)):
749
self.get_revision(revision_id)
752
return self._real_repository.fetch(
753
source, revision_id=revision_id, pb=pb)
755
def create_bundle(self, target, base, fileobj, format=None):
757
self._real_repository.create_bundle(target, base, fileobj, format)
760
def control_weaves(self):
762
return self._real_repository.control_weaves
765
def get_ancestry(self, revision_id, topo_sorted=True):
767
return self._real_repository.get_ancestry(revision_id, topo_sorted)
770
def get_inventory_weave(self):
772
return self._real_repository.get_inventory_weave()
774
def fileids_altered_by_revision_ids(self, revision_ids):
776
return self._real_repository.fileids_altered_by_revision_ids(revision_ids)
778
def _get_versioned_file_checker(self, revisions, revision_versions_cache):
780
return self._real_repository._get_versioned_file_checker(
781
revisions, revision_versions_cache)
783
def iter_files_bytes(self, desired_files):
784
"""See Repository.iter_file_bytes.
787
return self._real_repository.iter_files_bytes(desired_files)
789
def get_parent_map(self, keys):
790
"""See bzrlib.Graph.get_parent_map()."""
791
# Hack to build up the caching logic.
792
ancestry = self._parents_map
794
# Repository is not locked, so there's no cache.
795
missing_revisions = set(keys)
798
missing_revisions = set(key for key in keys if key not in ancestry)
799
if missing_revisions:
800
parent_map = self._get_parent_map(missing_revisions)
801
if 'hpss' in debug.debug_flags:
802
mutter('retransmitted revisions: %d of %d',
803
len(set(ancestry).intersection(parent_map)),
805
ancestry.update(parent_map)
806
present_keys = [k for k in keys if k in ancestry]
807
if 'hpss' in debug.debug_flags:
808
self._requested_parents.update(present_keys)
809
mutter('Current RemoteRepository graph hit rate: %d%%',
810
100.0 * len(self._requested_parents) / len(ancestry))
811
return dict((k, ancestry[k]) for k in present_keys)
813
def _response_is_unknown_method(self, response, verb):
814
"""Return True if response is an unknonwn method response to verb.
816
:param response: The response from a smart client call_expecting_body
818
:param verb: The verb used in that call.
819
:return: True if an unknown method was encountered.
821
# This might live better on
822
# bzrlib.smart.protocol.SmartClientRequestProtocolOne
823
if (response[0] == ('error', "Generic bzr smart protocol error: "
824
"bad request '%s'" % verb) or
825
response[0] == ('error', "Generic bzr smart protocol error: "
826
"bad request u'%s'" % verb)):
827
response[1].cancel_read_body()
831
def _get_parent_map(self, keys):
832
"""Helper for get_parent_map that performs the RPC."""
833
medium = self._client.get_smart_medium()
834
if not medium._remote_is_at_least_1_2:
835
# We already found out that the server can't understand
836
# Repository.get_parent_map requests, so just fetch the whole
838
return self.get_revision_graph()
841
if NULL_REVISION in keys:
842
keys.discard(NULL_REVISION)
843
found_parents = {NULL_REVISION:()}
848
# TODO(Needs analysis): We could assume that the keys being requested
849
# from get_parent_map are in a breadth first search, so typically they
850
# will all be depth N from some common parent, and we don't have to
851
# have the server iterate from the root parent, but rather from the
852
# keys we're searching; and just tell the server the keyspace we
853
# already have; but this may be more traffic again.
855
# Transform self._parents_map into a search request recipe.
856
# TODO: Manage this incrementally to avoid covering the same path
857
# repeatedly. (The server will have to on each request, but the less
858
# work done the better).
859
parents_map = self._parents_map
860
if parents_map is None:
861
# Repository is not locked, so there's no cache.
863
start_set = set(parents_map)
864
result_parents = set()
865
for parents in parents_map.itervalues():
866
result_parents.update(parents)
867
stop_keys = result_parents.difference(start_set)
868
included_keys = start_set.intersection(result_parents)
869
start_set.difference_update(included_keys)
870
recipe = (start_set, stop_keys, len(parents_map))
871
body = self._serialise_search_recipe(recipe)
872
path = self.bzrdir._path_for_remote_call(self._client)
874
assert type(key) is str
875
verb = 'Repository.get_parent_map'
876
args = (path,) + tuple(keys)
877
response = self._client.call_with_body_bytes_expecting_body(
878
verb, args, self._serialise_search_recipe(recipe))
879
if self._response_is_unknown_method(response, verb):
880
# Server does not support this method, so get the whole graph.
881
# Worse, we have to force a disconnection, because the server now
882
# doesn't realise it has a body on the wire to consume, so the
883
# only way to recover is to abandon the connection.
885
'Server is too old for fast get_parent_map, reconnecting. '
886
'(Upgrade the server to Bazaar 1.2 to avoid this)')
888
# To avoid having to disconnect repeatedly, we keep track of the
889
# fact the server doesn't understand remote methods added in 1.2.
890
medium._remote_is_at_least_1_2 = False
891
return self.get_revision_graph()
892
elif response[0][0] not in ['ok']:
893
reponse[1].cancel_read_body()
894
raise errors.UnexpectedSmartServerResponse(response[0])
895
if response[0][0] == 'ok':
896
coded = bz2.decompress(response[1].read_body_bytes())
900
lines = coded.split('\n')
903
d = tuple(line.split())
905
revision_graph[d[0]] = d[1:]
907
# No parents - so give the Graph result (NULL_REVISION,).
908
revision_graph[d[0]] = (NULL_REVISION,)
909
return revision_graph
912
def get_signature_text(self, revision_id):
914
return self._real_repository.get_signature_text(revision_id)
917
def get_revision_graph_with_ghosts(self, revision_ids=None):
919
return self._real_repository.get_revision_graph_with_ghosts(
920
revision_ids=revision_ids)
923
def get_inventory_xml(self, revision_id):
925
return self._real_repository.get_inventory_xml(revision_id)
927
def deserialise_inventory(self, revision_id, xml):
929
return self._real_repository.deserialise_inventory(revision_id, xml)
931
def reconcile(self, other=None, thorough=False):
933
return self._real_repository.reconcile(other=other, thorough=thorough)
935
def all_revision_ids(self):
937
return self._real_repository.all_revision_ids()
940
def get_deltas_for_revisions(self, revisions):
942
return self._real_repository.get_deltas_for_revisions(revisions)
945
def get_revision_delta(self, revision_id):
947
return self._real_repository.get_revision_delta(revision_id)
950
def revision_trees(self, revision_ids):
952
return self._real_repository.revision_trees(revision_ids)
955
def get_revision_reconcile(self, revision_id):
957
return self._real_repository.get_revision_reconcile(revision_id)
960
def check(self, revision_ids=None):
962
return self._real_repository.check(revision_ids=revision_ids)
964
def copy_content_into(self, destination, revision_id=None):
966
return self._real_repository.copy_content_into(
967
destination, revision_id=revision_id)
969
def _copy_repository_tarball(self, to_bzrdir, revision_id=None):
970
# get a tarball of the remote repository, and copy from that into the
972
from bzrlib import osutils
975
# TODO: Maybe a progress bar while streaming the tarball?
976
note("Copying repository content as tarball...")
977
tar_file = self._get_tarball('bz2')
980
destination = to_bzrdir.create_repository()
982
tar = tarfile.open('repository', fileobj=tar_file,
984
tmpdir = tempfile.mkdtemp()
986
_extract_tar(tar, tmpdir)
987
tmp_bzrdir = BzrDir.open(tmpdir)
988
tmp_repo = tmp_bzrdir.open_repository()
989
tmp_repo.copy_content_into(destination, revision_id)
991
osutils.rmtree(tmpdir)
995
# TODO: Suggestion from john: using external tar is much faster than
996
# python's tarfile library, but it may not work on windows.
1000
"""Compress the data within the repository.
1002
This is not currently implemented within the smart server.
1005
return self._real_repository.pack()
1007
def set_make_working_trees(self, new_value):
1008
raise NotImplementedError(self.set_make_working_trees)
1011
def sign_revision(self, revision_id, gpg_strategy):
1013
return self._real_repository.sign_revision(revision_id, gpg_strategy)
1016
def get_revisions(self, revision_ids):
1018
return self._real_repository.get_revisions(revision_ids)
1020
def supports_rich_root(self):
1022
return self._real_repository.supports_rich_root()
1024
def iter_reverse_revision_history(self, revision_id):
1026
return self._real_repository.iter_reverse_revision_history(revision_id)
1029
def _serializer(self):
1031
return self._real_repository._serializer
1033
def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
1035
return self._real_repository.store_revision_signature(
1036
gpg_strategy, plaintext, revision_id)
1038
def add_signature_text(self, revision_id, signature):
1040
return self._real_repository.add_signature_text(revision_id, signature)
1042
def has_signature_for_revision_id(self, revision_id):
1044
return self._real_repository.has_signature_for_revision_id(revision_id)
1046
def get_data_stream_for_search(self, search):
1047
medium = self._client.get_smart_medium()
1048
if not medium._remote_is_at_least_1_2:
1050
return self._real_repository.get_data_stream_for_search(search)
1051
REQUEST_NAME = 'Repository.stream_revisions_chunked'
1052
path = self.bzrdir._path_for_remote_call(self._client)
1053
body = self._serialise_search_recipe(search.get_recipe())
1054
response, protocol = self._client.call_with_body_bytes_expecting_body(
1055
REQUEST_NAME, (path,), body)
1057
if self._response_is_unknown_method((response, protocol), REQUEST_NAME):
1058
# Server does not support this method, so fall back to VFS.
1059
# Worse, we have to force a disconnection, because the server now
1060
# doesn't realise it has a body on the wire to consume, so the
1061
# only way to recover is to abandon the connection.
1063
'Server is too old for streaming pull, reconnecting. '
1064
'(Upgrade the server to Bazaar 1.2 to avoid this)')
1066
# To avoid having to disconnect repeatedly, we keep track of the
1067
# fact the server doesn't understand this remote method.
1068
medium._remote_is_at_least_1_2 = False
1070
return self._real_repository.get_data_stream_for_search(search)
1072
if response == ('ok',):
1073
return self._deserialise_stream(protocol)
1074
if response == ('NoSuchRevision', ):
1075
# We cannot easily identify the revision that is missing in this
1076
# situation without doing much more network IO. For now, bail.
1077
raise NoSuchRevision(self, "unknown")
1079
raise errors.UnexpectedSmartServerResponse(response)
1081
def _deserialise_stream(self, protocol):
1082
stream = protocol.read_streamed_body()
1083
container_parser = ContainerPushParser()
1084
for bytes in stream:
1085
container_parser.accept_bytes(bytes)
1086
records = container_parser.read_pending_records()
1087
for record_names, record_bytes in records:
1088
if len(record_names) != 1:
1089
# These records should have only one name, and that name
1090
# should be a one-element tuple.
1091
raise errors.SmartProtocolError(
1092
'Repository data stream had invalid record name %r'
1094
name_tuple = record_names[0]
1095
yield name_tuple, record_bytes
1097
def insert_data_stream(self, stream):
1099
self._real_repository.insert_data_stream(stream)
1101
def item_keys_introduced_by(self, revision_ids, _files_pb=None):
1103
return self._real_repository.item_keys_introduced_by(revision_ids,
1104
_files_pb=_files_pb)
1106
def revision_graph_can_have_wrong_parents(self):
1107
# The answer depends on the remote repo format.
1109
return self._real_repository.revision_graph_can_have_wrong_parents()
1111
def _find_inconsistent_revision_parents(self):
1113
return self._real_repository._find_inconsistent_revision_parents()
1115
def _check_for_inconsistent_revision_parents(self):
1117
return self._real_repository._check_for_inconsistent_revision_parents()
1119
def _make_parents_provider(self):
1122
def _serialise_search_recipe(self, recipe):
1123
"""Serialise a graph search recipe.
1125
:param recipe: A search recipe (start, stop, count).
1126
:return: Serialised bytes.
1128
start_keys = ' '.join(recipe[0])
1129
stop_keys = ' '.join(recipe[1])
1130
count = str(recipe[2])
1131
return '\n'.join((start_keys, stop_keys, count))
1134
class RemoteBranchLockableFiles(LockableFiles):
1135
"""A 'LockableFiles' implementation that talks to a smart server.
1137
This is not a public interface class.
1140
def __init__(self, bzrdir, _client):
1141
self.bzrdir = bzrdir
1142
self._client = _client
1143
self._need_find_modes = True
1144
LockableFiles.__init__(
1145
self, bzrdir.get_branch_transport(None),
1146
'lock', lockdir.LockDir)
1148
def _find_modes(self):
1149
# RemoteBranches don't let the client set the mode of control files.
1150
self._dir_mode = None
1151
self._file_mode = None
1153
def get(self, path):
1154
"""'get' a remote path as per the LockableFiles interface.
1156
:param path: the file to 'get'. If this is 'branch.conf', we do not
1157
just retrieve a file, instead we ask the smart server to generate
1158
a configuration for us - which is retrieved as an INI file.
1160
if path == 'branch.conf':
1161
path = self.bzrdir._path_for_remote_call(self._client)
1162
response = self._client.call_expecting_body(
1163
'Branch.get_config_file', path)
1164
assert response[0][0] == 'ok', \
1165
'unexpected response code %s' % (response[0],)
1166
return StringIO(response[1].read_body_bytes())
1169
return LockableFiles.get(self, path)
1172
class RemoteBranchFormat(branch.BranchFormat):
1174
def __eq__(self, other):
1175
return (isinstance(other, RemoteBranchFormat) and
1176
self.__dict__ == other.__dict__)
1178
def get_format_description(self):
1179
return 'Remote BZR Branch'
1181
def get_format_string(self):
1182
return 'Remote BZR Branch'
1184
def open(self, a_bzrdir):
1185
assert isinstance(a_bzrdir, RemoteBzrDir)
1186
return a_bzrdir.open_branch()
1188
def initialize(self, a_bzrdir):
1189
assert isinstance(a_bzrdir, RemoteBzrDir)
1190
return a_bzrdir.create_branch()
1192
def supports_tags(self):
1193
# Remote branches might support tags, but we won't know until we
1194
# access the real remote branch.
1198
class RemoteBranch(branch.Branch):
1199
"""Branch stored on a server accessed by HPSS RPC.
1201
At the moment most operations are mapped down to simple file operations.
1204
def __init__(self, remote_bzrdir, remote_repository, real_branch=None,
1206
"""Create a RemoteBranch instance.
1208
:param real_branch: An optional local implementation of the branch
1209
format, usually accessing the data via the VFS.
1210
:param _client: Private parameter for testing.
1212
# We intentionally don't call the parent class's __init__, because it
1213
# will try to assign to self.tags, which is a property in this subclass.
1214
# And the parent's __init__ doesn't do much anyway.
1215
self._revision_id_to_revno_cache = None
1216
self._revision_history_cache = None
1217
self.bzrdir = remote_bzrdir
1218
if _client is not None:
1219
self._client = _client
1221
self._client = client._SmartClient(self.bzrdir._shared_medium)
1222
self.repository = remote_repository
1223
if real_branch is not None:
1224
self._real_branch = real_branch
1225
# Give the remote repository the matching real repo.
1226
real_repo = self._real_branch.repository
1227
if isinstance(real_repo, RemoteRepository):
1228
real_repo._ensure_real()
1229
real_repo = real_repo._real_repository
1230
self.repository._set_real_repository(real_repo)
1231
# Give the branch the remote repository to let fast-pathing happen.
1232
self._real_branch.repository = self.repository
1234
self._real_branch = None
1235
# Fill out expected attributes of branch for bzrlib api users.
1236
self._format = RemoteBranchFormat()
1237
self.base = self.bzrdir.root_transport.base
1238
self._control_files = None
1239
self._lock_mode = None
1240
self._lock_token = None
1241
self._lock_count = 0
1242
self._leave_lock = False
1245
return "%s(%s)" % (self.__class__.__name__, self.base)
1249
def _ensure_real(self):
1250
"""Ensure that there is a _real_branch set.
1252
Used before calls to self._real_branch.
1254
if not self._real_branch:
1255
assert vfs.vfs_enabled()
1256
self.bzrdir._ensure_real()
1257
self._real_branch = self.bzrdir._real_bzrdir.open_branch()
1258
# Give the remote repository the matching real repo.
1259
real_repo = self._real_branch.repository
1260
if isinstance(real_repo, RemoteRepository):
1261
real_repo._ensure_real()
1262
real_repo = real_repo._real_repository
1263
self.repository._set_real_repository(real_repo)
1264
# Give the branch the remote repository to let fast-pathing happen.
1265
self._real_branch.repository = self.repository
1266
# XXX: deal with _lock_mode == 'w'
1267
if self._lock_mode == 'r':
1268
self._real_branch.lock_read()
1271
def control_files(self):
1272
# Defer actually creating RemoteBranchLockableFiles until its needed,
1273
# because it triggers an _ensure_real that we otherwise might not need.
1274
if self._control_files is None:
1275
self._control_files = RemoteBranchLockableFiles(
1276
self.bzrdir, self._client)
1277
return self._control_files
1279
def _get_checkout_format(self):
1281
return self._real_branch._get_checkout_format()
1283
def get_physical_lock_status(self):
1284
"""See Branch.get_physical_lock_status()."""
1285
# should be an API call to the server, as branches must be lockable.
1287
return self._real_branch.get_physical_lock_status()
1289
def lock_read(self):
1290
if not self._lock_mode:
1291
self._lock_mode = 'r'
1292
self._lock_count = 1
1293
if self._real_branch is not None:
1294
self._real_branch.lock_read()
1296
self._lock_count += 1
1298
def _remote_lock_write(self, token):
1300
branch_token = repo_token = ''
1302
branch_token = token
1303
repo_token = self.repository.lock_write()
1304
self.repository.unlock()
1305
path = self.bzrdir._path_for_remote_call(self._client)
1306
response = self._client.call('Branch.lock_write', path, branch_token,
1308
if response[0] == 'ok':
1309
ok, branch_token, repo_token = response
1310
return branch_token, repo_token
1311
elif response[0] == 'LockContention':
1312
raise errors.LockContention('(remote lock)')
1313
elif response[0] == 'TokenMismatch':
1314
raise errors.TokenMismatch(token, '(remote token)')
1315
elif response[0] == 'UnlockableTransport':
1316
raise errors.UnlockableTransport(self.bzrdir.root_transport)
1317
elif response[0] == 'ReadOnlyError':
1318
raise errors.ReadOnlyError(self)
1319
elif response[0] == 'LockFailed':
1320
raise errors.LockFailed(response[1], response[2])
1322
raise errors.UnexpectedSmartServerResponse(response)
1324
def lock_write(self, token=None):
1325
if not self._lock_mode:
1326
remote_tokens = self._remote_lock_write(token)
1327
self._lock_token, self._repo_lock_token = remote_tokens
1328
assert self._lock_token, 'Remote server did not return a token!'
1329
# TODO: We really, really, really don't want to call _ensure_real
1330
# here, but it's the easiest way to ensure coherency between the
1331
# state of the RemoteBranch and RemoteRepository objects and the
1332
# physical locks. If we don't materialise the real objects here,
1333
# then getting everything in the right state later is complex, so
1334
# for now we just do it the lazy way.
1335
# -- Andrew Bennetts, 2007-02-22.
1337
if self._real_branch is not None:
1338
self._real_branch.repository.lock_write(
1339
token=self._repo_lock_token)
1341
self._real_branch.lock_write(token=self._lock_token)
1343
self._real_branch.repository.unlock()
1344
if token is not None:
1345
self._leave_lock = True
1347
# XXX: this case seems to be unreachable; token cannot be None.
1348
self._leave_lock = False
1349
self._lock_mode = 'w'
1350
self._lock_count = 1
1351
elif self._lock_mode == 'r':
1352
raise errors.ReadOnlyTransaction
1354
if token is not None:
1355
# A token was given to lock_write, and we're relocking, so check
1356
# that the given token actually matches the one we already have.
1357
if token != self._lock_token:
1358
raise errors.TokenMismatch(token, self._lock_token)
1359
self._lock_count += 1
1360
return self._lock_token or None
1362
def _unlock(self, branch_token, repo_token):
1363
path = self.bzrdir._path_for_remote_call(self._client)
1364
response = self._client.call('Branch.unlock', path, branch_token,
1366
if response == ('ok',):
1368
elif response[0] == 'TokenMismatch':
1369
raise errors.TokenMismatch(
1370
str((branch_token, repo_token)), '(remote tokens)')
1372
raise errors.UnexpectedSmartServerResponse(response)
1375
self._lock_count -= 1
1376
if not self._lock_count:
1377
self._clear_cached_state()
1378
mode = self._lock_mode
1379
self._lock_mode = None
1380
if self._real_branch is not None:
1381
if (not self._leave_lock and mode == 'w' and
1382
self._repo_lock_token):
1383
# If this RemoteBranch will remove the physical lock for the
1384
# repository, make sure the _real_branch doesn't do it
1385
# first. (Because the _real_branch's repository is set to
1386
# be the RemoteRepository.)
1387
self._real_branch.repository.leave_lock_in_place()
1388
self._real_branch.unlock()
1390
# Only write-locked branched need to make a remote method call
1391
# to perfom the unlock.
1393
assert self._lock_token, 'Locked, but no token!'
1394
branch_token = self._lock_token
1395
repo_token = self._repo_lock_token
1396
self._lock_token = None
1397
self._repo_lock_token = None
1398
if not self._leave_lock:
1399
self._unlock(branch_token, repo_token)
1401
def break_lock(self):
1403
return self._real_branch.break_lock()
1405
def leave_lock_in_place(self):
1406
if not self._lock_token:
1407
raise NotImplementedError(self.leave_lock_in_place)
1408
self._leave_lock = True
1410
def dont_leave_lock_in_place(self):
1411
if not self._lock_token:
1412
raise NotImplementedError(self.dont_leave_lock_in_place)
1413
self._leave_lock = False
1415
def last_revision_info(self):
1416
"""See Branch.last_revision_info()."""
1417
path = self.bzrdir._path_for_remote_call(self._client)
1418
response = self._client.call('Branch.last_revision_info', path)
1419
assert response[0] == 'ok', 'unexpected response code %s' % (response,)
1420
revno = int(response[1])
1421
last_revision = response[2]
1422
return (revno, last_revision)
1424
def _gen_revision_history(self):
1425
"""See Branch._gen_revision_history()."""
1426
path = self.bzrdir._path_for_remote_call(self._client)
1427
response = self._client.call_expecting_body(
1428
'Branch.revision_history', path)
1429
assert response[0][0] == 'ok', ('unexpected response code %s'
1431
result = response[1].read_body_bytes().split('\x00')
1437
def set_revision_history(self, rev_history):
1438
# Send just the tip revision of the history; the server will generate
1439
# the full history from that. If the revision doesn't exist in this
1440
# branch, NoSuchRevision will be raised.
1441
path = self.bzrdir._path_for_remote_call(self._client)
1442
if rev_history == []:
1445
rev_id = rev_history[-1]
1446
self._clear_cached_state()
1447
response = self._client.call('Branch.set_last_revision',
1448
path, self._lock_token, self._repo_lock_token, rev_id)
1449
if response[0] == 'NoSuchRevision':
1450
raise NoSuchRevision(self, rev_id)
1452
assert response == ('ok',), (
1453
'unexpected response code %r' % (response,))
1454
self._cache_revision_history(rev_history)
1456
def get_parent(self):
1458
return self._real_branch.get_parent()
1460
def set_parent(self, url):
1462
return self._real_branch.set_parent(url)
1464
def get_config(self):
1465
return RemoteBranchConfig(self)
1467
def sprout(self, to_bzrdir, revision_id=None):
1468
# Like Branch.sprout, except that it sprouts a branch in the default
1469
# format, because RemoteBranches can't be created at arbitrary URLs.
1470
# XXX: if to_bzrdir is a RemoteBranch, this should perhaps do
1471
# to_bzrdir.create_branch...
1473
result = self._real_branch._format.initialize(to_bzrdir)
1474
self.copy_content_into(result, revision_id=revision_id)
1475
result.set_parent(self.bzrdir.root_transport.base)
1479
def pull(self, source, overwrite=False, stop_revision=None,
1481
# FIXME: This asks the real branch to run the hooks, which means
1482
# they're called with the wrong target branch parameter.
1483
# The test suite specifically allows this at present but it should be
1484
# fixed. It should get a _override_hook_target branch,
1485
# as push does. -- mbp 20070405
1487
self._real_branch.pull(
1488
source, overwrite=overwrite, stop_revision=stop_revision,
1492
def push(self, target, overwrite=False, stop_revision=None):
1494
return self._real_branch.push(
1495
target, overwrite=overwrite, stop_revision=stop_revision,
1496
_override_hook_source_branch=self)
1498
def is_locked(self):
1499
return self._lock_count >= 1
1501
def set_last_revision_info(self, revno, revision_id):
1503
self._clear_cached_state()
1504
return self._real_branch.set_last_revision_info(revno, revision_id)
1506
def generate_revision_history(self, revision_id, last_rev=None,
1509
return self._real_branch.generate_revision_history(
1510
revision_id, last_rev=last_rev, other_branch=other_branch)
1515
return self._real_branch.tags
1517
def set_push_location(self, location):
1519
return self._real_branch.set_push_location(location)
1521
def update_revisions(self, other, stop_revision=None, overwrite=False):
1523
return self._real_branch.update_revisions(
1524
other, stop_revision=stop_revision, overwrite=overwrite)
1527
class RemoteBranchConfig(BranchConfig):
1530
self.branch._ensure_real()
1531
return self.branch._real_branch.get_config().username()
1533
def _get_branch_data_config(self):
1534
self.branch._ensure_real()
1535
if self._branch_data_config is None:
1536
self._branch_data_config = TreeConfig(self.branch._real_branch)
1537
return self._branch_data_config
1540
def _extract_tar(tar, to_dir):
1541
"""Extract all the contents of a tarfile object.
1543
A replacement for extractall, which is not present in python2.4
1546
tar.extract(tarinfo, to_dir)