1
# Copyright (C) 2006, 2007, 2008 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
33
from bzrlib.branch import BranchReferenceFormat
34
from bzrlib.bzrdir import BzrDir, RemoteBzrDirFormat
35
from bzrlib.config import BranchConfig, TreeConfig
36
from bzrlib.decorators import needs_read_lock, needs_write_lock
37
from bzrlib.errors import (
41
from bzrlib.lockable_files import LockableFiles
42
from bzrlib.pack import ContainerPushParser
43
from bzrlib.smart import client, vfs
44
from bzrlib.symbol_versioning import (
47
from bzrlib.revision import ensure_null, NULL_REVISION
48
from bzrlib.trace import mutter, note, warning
50
# Note: RemoteBzrDirFormat is in bzrdir.py
52
class RemoteBzrDir(BzrDir):
53
"""Control directory on a remote server, accessed via bzr:// or similar."""
55
def __init__(self, transport, _client=None):
56
"""Construct a RemoteBzrDir.
58
:param _client: Private parameter for testing. Disables probing and the
61
BzrDir.__init__(self, transport, RemoteBzrDirFormat())
62
# this object holds a delegated bzrdir that uses file-level operations
63
# to talk to the other side
64
self._real_bzrdir = None
67
medium = transport.get_smart_medium()
68
self._client = client._SmartClient(medium, transport.base)
70
self._client = _client
73
path = self._path_for_remote_call(self._client)
74
response = self._client.call('BzrDir.open', path)
75
if response not in [('yes',), ('no',)]:
76
raise errors.UnexpectedSmartServerResponse(response)
77
if response == ('no',):
78
raise errors.NotBranchError(path=transport.base)
80
def _ensure_real(self):
81
"""Ensure that there is a _real_bzrdir set.
83
Used before calls to self._real_bzrdir.
85
if not self._real_bzrdir:
86
self._real_bzrdir = BzrDir.open_from_transport(
87
self.root_transport, _server_formats=False)
89
def create_repository(self, shared=False):
91
self._real_bzrdir.create_repository(shared=shared)
92
return self.open_repository()
94
def destroy_repository(self):
95
"""See BzrDir.destroy_repository"""
97
self._real_bzrdir.destroy_repository()
99
def create_branch(self):
101
real_branch = self._real_bzrdir.create_branch()
102
return RemoteBranch(self, self.find_repository(), real_branch)
104
def destroy_branch(self):
105
"""See BzrDir.destroy_branch"""
107
self._real_bzrdir.destroy_branch()
109
def create_workingtree(self, revision_id=None, from_branch=None):
110
raise errors.NotLocalUrl(self.transport.base)
112
def find_branch_format(self):
113
"""Find the branch 'format' for this bzrdir.
115
This might be a synthetic object for e.g. RemoteBranch and SVN.
117
b = self.open_branch()
120
def get_branch_reference(self):
121
"""See BzrDir.get_branch_reference()."""
122
path = self._path_for_remote_call(self._client)
123
response = self._client.call('BzrDir.open_branch', path)
124
if response[0] == 'ok':
125
if response[1] == '':
126
# branch at this location.
129
# a branch reference, use the existing BranchReference logic.
131
elif response == ('nobranch',):
132
raise errors.NotBranchError(path=self.root_transport.base)
134
raise errors.UnexpectedSmartServerResponse(response)
136
def _get_tree_branch(self):
137
"""See BzrDir._get_tree_branch()."""
138
return None, self.open_branch()
140
def open_branch(self, _unsupported=False):
142
raise NotImplementedError('unsupported flag support not implemented yet.')
143
reference_url = self.get_branch_reference()
144
if reference_url is None:
145
# branch at this location.
146
return RemoteBranch(self, self.find_repository())
148
# a branch reference, use the existing BranchReference logic.
149
format = BranchReferenceFormat()
150
return format.open(self, _found=True, location=reference_url)
152
def open_repository(self):
153
path = self._path_for_remote_call(self._client)
154
verb = 'BzrDir.find_repositoryV2'
156
response = self._client.call(verb, path)
157
except errors.UnknownSmartMethod:
158
verb = 'BzrDir.find_repository'
159
response = self._client.call(verb, path)
160
if response[0] == 'norepository':
161
raise errors.NoRepositoryPresent(self)
162
elif response[0] != 'ok':
163
raise SmartProtocolError('unexpected response %r'
165
if verb == 'BzrDir.find_repository':
166
# servers that don't support the V2 method don't support external
168
response = response + ('no', )
169
if not (len(response) == 5):
170
raise SmartProtocolError('incorrect response length %s' % (response,))
171
if response[1] == '':
172
format = RemoteRepositoryFormat()
173
format.rich_root_data = (response[2] == 'yes')
174
format.supports_tree_reference = (response[3] == 'yes')
175
# No wire format to check this yet.
176
format.supports_external_lookups = (response[4] == 'yes')
177
return RemoteRepository(self, format)
179
raise errors.NoRepositoryPresent(self)
181
def open_workingtree(self, recommend_upgrade=True):
183
if self._real_bzrdir.has_workingtree():
184
raise errors.NotLocalUrl(self.root_transport)
186
raise errors.NoWorkingTree(self.root_transport.base)
188
def _path_for_remote_call(self, client):
189
"""Return the path to be used for this bzrdir in a remote call."""
190
return client.remote_path_from_transport(self.root_transport)
192
def get_branch_transport(self, branch_format):
194
return self._real_bzrdir.get_branch_transport(branch_format)
196
def get_repository_transport(self, repository_format):
198
return self._real_bzrdir.get_repository_transport(repository_format)
200
def get_workingtree_transport(self, workingtree_format):
202
return self._real_bzrdir.get_workingtree_transport(workingtree_format)
204
def can_convert_format(self):
205
"""Upgrading of remote bzrdirs is not supported yet."""
208
def needs_format_conversion(self, format=None):
209
"""Upgrading of remote bzrdirs is not supported yet."""
212
def clone(self, url, revision_id=None, force_new_repo=False):
214
return self._real_bzrdir.clone(url, revision_id=revision_id,
215
force_new_repo=force_new_repo)
218
class RemoteRepositoryFormat(repository.RepositoryFormat):
219
"""Format for repositories accessed over a _SmartClient.
221
Instances of this repository are represented by RemoteRepository
224
The RemoteRepositoryFormat is parameterized during construction
225
to reflect the capabilities of the real, remote format. Specifically
226
the attributes rich_root_data and supports_tree_reference are set
227
on a per instance basis, and are not set (and should not be) at
231
_matchingbzrdir = RemoteBzrDirFormat
233
def initialize(self, a_bzrdir, shared=False):
234
if not isinstance(a_bzrdir, RemoteBzrDir):
235
raise AssertionError('%r is not a RemoteBzrDir' % (a_bzrdir,))
236
return a_bzrdir.create_repository(shared=shared)
238
def open(self, a_bzrdir):
239
if not isinstance(a_bzrdir, RemoteBzrDir):
240
raise AssertionError('%r is not a RemoteBzrDir' % (a_bzrdir,))
241
return a_bzrdir.open_repository()
243
def get_format_description(self):
244
return 'bzr remote repository'
246
def __eq__(self, other):
247
return self.__class__ == other.__class__
249
def check_conversion_target(self, target_format):
250
if self.rich_root_data and not target_format.rich_root_data:
251
raise errors.BadConversionTarget(
252
'Does not support rich root data.', target_format)
253
if (self.supports_tree_reference and
254
not getattr(target_format, 'supports_tree_reference', False)):
255
raise errors.BadConversionTarget(
256
'Does not support nested trees', target_format)
259
class RemoteRepository(object):
260
"""Repository accessed over rpc.
262
For the moment most operations are performed using local transport-backed
266
def __init__(self, remote_bzrdir, format, real_repository=None, _client=None):
267
"""Create a RemoteRepository instance.
269
:param remote_bzrdir: The bzrdir hosting this repository.
270
:param format: The RemoteFormat object to use.
271
:param real_repository: If not None, a local implementation of the
272
repository logic for the repository, usually accessing the data
274
:param _client: Private testing parameter - override the smart client
275
to be used by the repository.
278
self._real_repository = real_repository
280
self._real_repository = None
281
self.bzrdir = remote_bzrdir
283
self._client = remote_bzrdir._client
285
self._client = _client
286
self._format = format
287
self._lock_mode = None
288
self._lock_token = None
290
self._leave_lock = False
291
# A cache of looked up revision parent data; reset at unlock time.
292
self._parents_map = None
293
if 'hpss' in debug.debug_flags:
294
self._requested_parents = None
296
# These depend on the actual remote format, so force them off for
297
# maximum compatibility. XXX: In future these should depend on the
298
# remote repository instance, but this is irrelevant until we perform
299
# reconcile via an RPC call.
300
self._reconcile_does_inventory_gc = False
301
self._reconcile_fixes_text_parents = False
302
self._reconcile_backsup_inventory = False
303
self.base = self.bzrdir.transport.base
306
return "%s(%s)" % (self.__class__.__name__, self.base)
310
def abort_write_group(self):
311
"""Complete a write group on the decorated repository.
313
Smart methods peform operations in a single step so this api
314
is not really applicable except as a compatibility thunk
315
for older plugins that don't use e.g. the CommitBuilder
319
return self._real_repository.abort_write_group()
321
def commit_write_group(self):
322
"""Complete a write group on the decorated repository.
324
Smart methods peform operations in a single step so this api
325
is not really applicable except as a compatibility thunk
326
for older plugins that don't use e.g. the CommitBuilder
330
return self._real_repository.commit_write_group()
332
def _ensure_real(self):
333
"""Ensure that there is a _real_repository set.
335
Used before calls to self._real_repository.
337
if not self._real_repository:
338
self.bzrdir._ensure_real()
339
#self._real_repository = self.bzrdir._real_bzrdir.open_repository()
340
self._set_real_repository(self.bzrdir._real_bzrdir.open_repository())
342
def find_text_key_references(self):
343
"""Find the text key references within the repository.
345
:return: a dictionary mapping (file_id, revision_id) tuples to altered file-ids to an iterable of
346
revision_ids. Each altered file-ids has the exact revision_ids that
347
altered it listed explicitly.
348
:return: A dictionary mapping text keys ((fileid, revision_id) tuples)
349
to whether they were referred to by the inventory of the
350
revision_id that they contain. The inventory texts from all present
351
revision ids are assessed to generate this report.
354
return self._real_repository.find_text_key_references()
356
def _generate_text_key_index(self):
357
"""Generate a new text key index for the repository.
359
This is an expensive function that will take considerable time to run.
361
:return: A dict mapping (file_id, revision_id) tuples to a list of
362
parents, also (file_id, revision_id) tuples.
365
return self._real_repository._generate_text_key_index()
367
@symbol_versioning.deprecated_method(symbol_versioning.one_four)
368
def get_revision_graph(self, revision_id=None):
369
"""See Repository.get_revision_graph()."""
370
return self._get_revision_graph(revision_id)
372
def _get_revision_graph(self, revision_id):
373
"""Private method for using with old (< 1.2) servers to fallback."""
374
if revision_id is None:
376
elif revision.is_null(revision_id):
379
path = self.bzrdir._path_for_remote_call(self._client)
380
response = self._client.call_expecting_body(
381
'Repository.get_revision_graph', path, revision_id)
382
if response[0][0] not in ['ok', 'nosuchrevision']:
383
raise errors.UnexpectedSmartServerResponse(response[0])
384
if response[0][0] == 'ok':
385
coded = response[1].read_body_bytes()
387
# no revisions in this repository!
389
lines = coded.split('\n')
392
d = tuple(line.split())
393
revision_graph[d[0]] = d[1:]
395
return revision_graph
397
response_body = response[1].read_body_bytes()
399
raise SmartProtocolError('unexpected response body')
400
raise NoSuchRevision(self, revision_id)
402
def has_revision(self, revision_id):
403
"""See Repository.has_revision()."""
404
if revision_id == NULL_REVISION:
405
# The null revision is always present.
407
path = self.bzrdir._path_for_remote_call(self._client)
408
response = self._client.call('Repository.has_revision', path, revision_id)
409
if response[0] not in ('yes', 'no'):
410
raise SmartProtocolError('unexpected response code %s' % (response,))
411
return response[0] == 'yes'
413
def has_revisions(self, revision_ids):
414
"""See Repository.has_revisions()."""
416
for revision_id in revision_ids:
417
if self.has_revision(revision_id):
418
result.add(revision_id)
421
def has_same_location(self, other):
422
return (self.__class__ == other.__class__ and
423
self.bzrdir.transport.base == other.bzrdir.transport.base)
425
def get_graph(self, other_repository=None):
426
"""Return the graph for this repository format"""
427
parents_provider = self
428
if (other_repository is not None and
429
other_repository.bzrdir.transport.base !=
430
self.bzrdir.transport.base):
431
parents_provider = graph._StackedParentsProvider(
432
[parents_provider, other_repository._make_parents_provider()])
433
return graph.Graph(parents_provider)
435
def gather_stats(self, revid=None, committers=None):
436
"""See Repository.gather_stats()."""
437
path = self.bzrdir._path_for_remote_call(self._client)
438
# revid can be None to indicate no revisions, not just NULL_REVISION
439
if revid is None or revision.is_null(revid):
443
if committers is None or not committers:
444
fmt_committers = 'no'
446
fmt_committers = 'yes'
447
response = self._client.call_expecting_body(
448
'Repository.gather_stats', path, fmt_revid, fmt_committers)
449
if response[0][0] != 'ok':
450
raise SmartProtocolError('unexpected response code %s'
453
body = response[1].read_body_bytes()
455
for line in body.split('\n'):
458
key, val_text = line.split(':')
459
if key in ('revisions', 'size', 'committers'):
460
result[key] = int(val_text)
461
elif key in ('firstrev', 'latestrev'):
462
values = val_text.split(' ')[1:]
463
result[key] = (float(values[0]), long(values[1]))
467
def find_branches(self, using=False):
468
"""See Repository.find_branches()."""
469
# should be an API call to the server.
471
return self._real_repository.find_branches(using=using)
473
def get_physical_lock_status(self):
474
"""See Repository.get_physical_lock_status()."""
475
# should be an API call to the server.
477
return self._real_repository.get_physical_lock_status()
479
def is_in_write_group(self):
480
"""Return True if there is an open write group.
482
write groups are only applicable locally for the smart server..
484
if self._real_repository:
485
return self._real_repository.is_in_write_group()
488
return self._lock_count >= 1
491
"""See Repository.is_shared()."""
492
path = self.bzrdir._path_for_remote_call(self._client)
493
response = self._client.call('Repository.is_shared', path)
494
if response[0] not in ('yes', 'no'):
495
raise SmartProtocolError('unexpected response code %s' % (response,))
496
return response[0] == 'yes'
498
def is_write_locked(self):
499
return self._lock_mode == 'w'
502
# wrong eventually - want a local lock cache context
503
if not self._lock_mode:
504
self._lock_mode = 'r'
506
self._parents_map = {}
507
if 'hpss' in debug.debug_flags:
508
self._requested_parents = set()
509
if self._real_repository is not None:
510
self._real_repository.lock_read()
512
self._lock_count += 1
514
def _remote_lock_write(self, token):
515
path = self.bzrdir._path_for_remote_call(self._client)
518
response = self._client.call('Repository.lock_write', path, token)
519
if response[0] == 'ok':
522
elif response[0] == 'LockContention':
523
raise errors.LockContention('(remote lock)')
524
elif response[0] == 'UnlockableTransport':
525
raise errors.UnlockableTransport(self.bzrdir.root_transport)
526
elif response[0] == 'LockFailed':
527
raise errors.LockFailed(response[1], response[2])
529
raise errors.UnexpectedSmartServerResponse(response)
531
def lock_write(self, token=None):
532
if not self._lock_mode:
533
self._lock_token = self._remote_lock_write(token)
534
# if self._lock_token is None, then this is something like packs or
535
# svn where we don't get to lock the repo, or a weave style repository
536
# where we cannot lock it over the wire and attempts to do so will
538
if self._real_repository is not None:
539
self._real_repository.lock_write(token=self._lock_token)
540
if token is not None:
541
self._leave_lock = True
543
self._leave_lock = False
544
self._lock_mode = 'w'
546
self._parents_map = {}
547
if 'hpss' in debug.debug_flags:
548
self._requested_parents = set()
549
elif self._lock_mode == 'r':
550
raise errors.ReadOnlyError(self)
552
self._lock_count += 1
553
return self._lock_token or None
555
def leave_lock_in_place(self):
556
if not self._lock_token:
557
raise NotImplementedError(self.leave_lock_in_place)
558
self._leave_lock = True
560
def dont_leave_lock_in_place(self):
561
if not self._lock_token:
562
raise NotImplementedError(self.dont_leave_lock_in_place)
563
self._leave_lock = False
565
def _set_real_repository(self, repository):
566
"""Set the _real_repository for this repository.
568
:param repository: The repository to fallback to for non-hpss
569
implemented operations.
571
if isinstance(repository, RemoteRepository):
572
raise AssertionError()
573
self._real_repository = repository
574
if self._lock_mode == 'w':
575
# if we are already locked, the real repository must be able to
576
# acquire the lock with our token.
577
self._real_repository.lock_write(self._lock_token)
578
elif self._lock_mode == 'r':
579
self._real_repository.lock_read()
581
def start_write_group(self):
582
"""Start a write group on the decorated repository.
584
Smart methods peform operations in a single step so this api
585
is not really applicable except as a compatibility thunk
586
for older plugins that don't use e.g. the CommitBuilder
590
return self._real_repository.start_write_group()
592
def _unlock(self, token):
593
path = self.bzrdir._path_for_remote_call(self._client)
595
# with no token the remote repository is not persistently locked.
597
response = self._client.call('Repository.unlock', path, token)
598
if response == ('ok',):
600
elif response[0] == 'TokenMismatch':
601
raise errors.TokenMismatch(token, '(remote token)')
603
raise errors.UnexpectedSmartServerResponse(response)
606
self._lock_count -= 1
607
if self._lock_count > 0:
609
self._parents_map = None
610
if 'hpss' in debug.debug_flags:
611
self._requested_parents = None
612
old_mode = self._lock_mode
613
self._lock_mode = None
615
# The real repository is responsible at present for raising an
616
# exception if it's in an unfinished write group. However, it
617
# normally will *not* actually remove the lock from disk - that's
618
# done by the server on receiving the Repository.unlock call.
619
# This is just to let the _real_repository stay up to date.
620
if self._real_repository is not None:
621
self._real_repository.unlock()
623
# The rpc-level lock should be released even if there was a
624
# problem releasing the vfs-based lock.
626
# Only write-locked repositories need to make a remote method
627
# call to perfom the unlock.
628
old_token = self._lock_token
629
self._lock_token = None
630
if not self._leave_lock:
631
self._unlock(old_token)
633
def break_lock(self):
634
# should hand off to the network
636
return self._real_repository.break_lock()
638
def _get_tarball(self, compression):
639
"""Return a TemporaryFile containing a repository tarball.
641
Returns None if the server does not support sending tarballs.
644
path = self.bzrdir._path_for_remote_call(self._client)
646
response, protocol = self._client.call_expecting_body(
647
'Repository.tarball', path, compression)
648
except errors.UnknownSmartMethod:
649
protocol.cancel_read_body()
651
if response[0] == 'ok':
652
# Extract the tarball and return it
653
t = tempfile.NamedTemporaryFile()
654
# TODO: rpc layer should read directly into it...
655
t.write(protocol.read_body_bytes())
658
raise errors.UnexpectedSmartServerResponse(response)
660
def sprout(self, to_bzrdir, revision_id=None):
661
# TODO: Option to control what format is created?
663
dest_repo = self._real_repository._format.initialize(to_bzrdir,
665
dest_repo.fetch(self, revision_id=revision_id)
668
### These methods are just thin shims to the VFS object for now.
670
def revision_tree(self, revision_id):
672
return self._real_repository.revision_tree(revision_id)
674
def get_serializer_format(self):
676
return self._real_repository.get_serializer_format()
678
def get_commit_builder(self, branch, parents, config, timestamp=None,
679
timezone=None, committer=None, revprops=None,
681
# FIXME: It ought to be possible to call this without immediately
682
# triggering _ensure_real. For now it's the easiest thing to do.
684
builder = self._real_repository.get_commit_builder(branch, parents,
685
config, timestamp=timestamp, timezone=timezone,
686
committer=committer, revprops=revprops, revision_id=revision_id)
689
def add_inventory(self, revid, inv, parents):
691
return self._real_repository.add_inventory(revid, inv, parents)
693
def add_revision(self, rev_id, rev, inv=None, config=None):
695
return self._real_repository.add_revision(
696
rev_id, rev, inv=inv, config=config)
699
def get_inventory(self, revision_id):
701
return self._real_repository.get_inventory(revision_id)
703
def iter_inventories(self, revision_ids):
705
return self._real_repository.iter_inventories(revision_ids)
708
def get_revision(self, revision_id):
710
return self._real_repository.get_revision(revision_id)
713
def weave_store(self):
715
return self._real_repository.weave_store
717
def get_transaction(self):
719
return self._real_repository.get_transaction()
722
def clone(self, a_bzrdir, revision_id=None):
724
return self._real_repository.clone(a_bzrdir, revision_id=revision_id)
726
def make_working_trees(self):
727
"""See Repository.make_working_trees"""
729
return self._real_repository.make_working_trees()
731
def revision_ids_to_search_result(self, result_set):
732
"""Convert a set of revision ids to a graph SearchResult."""
733
result_parents = set()
734
for parents in self.get_graph().get_parent_map(
735
result_set).itervalues():
736
result_parents.update(parents)
737
included_keys = result_set.intersection(result_parents)
738
start_keys = result_set.difference(included_keys)
739
exclude_keys = result_parents.difference(result_set)
740
result = graph.SearchResult(start_keys, exclude_keys,
741
len(result_set), result_set)
745
def search_missing_revision_ids(self, other, revision_id=None, find_ghosts=True):
746
"""Return the revision ids that other has that this does not.
748
These are returned in topological order.
750
revision_id: only return revision ids included by revision_id.
752
return repository.InterRepository.get(
753
other, self).search_missing_revision_ids(revision_id, find_ghosts)
755
def fetch(self, source, revision_id=None, pb=None):
756
if self.has_same_location(source):
757
# check that last_revision is in 'from' and then return a
759
if (revision_id is not None and
760
not revision.is_null(revision_id)):
761
self.get_revision(revision_id)
764
return self._real_repository.fetch(
765
source, revision_id=revision_id, pb=pb)
767
def create_bundle(self, target, base, fileobj, format=None):
769
self._real_repository.create_bundle(target, base, fileobj, format)
772
def control_weaves(self):
774
return self._real_repository.control_weaves
777
def get_ancestry(self, revision_id, topo_sorted=True):
779
return self._real_repository.get_ancestry(revision_id, topo_sorted)
782
def get_inventory_weave(self):
784
return self._real_repository.get_inventory_weave()
786
def fileids_altered_by_revision_ids(self, revision_ids):
788
return self._real_repository.fileids_altered_by_revision_ids(revision_ids)
790
def _get_versioned_file_checker(self, revisions, revision_versions_cache):
792
return self._real_repository._get_versioned_file_checker(
793
revisions, revision_versions_cache)
795
def iter_files_bytes(self, desired_files):
796
"""See Repository.iter_file_bytes.
799
return self._real_repository.iter_files_bytes(desired_files)
801
def get_parent_map(self, keys):
802
"""See bzrlib.Graph.get_parent_map()."""
803
# Hack to build up the caching logic.
804
ancestry = self._parents_map
806
# Repository is not locked, so there's no cache.
807
missing_revisions = set(keys)
810
missing_revisions = set(key for key in keys if key not in ancestry)
811
if missing_revisions:
812
parent_map = self._get_parent_map(missing_revisions)
813
if 'hpss' in debug.debug_flags:
814
mutter('retransmitted revisions: %d of %d',
815
len(set(ancestry).intersection(parent_map)),
817
ancestry.update(parent_map)
818
present_keys = [k for k in keys if k in ancestry]
819
if 'hpss' in debug.debug_flags:
820
self._requested_parents.update(present_keys)
821
mutter('Current RemoteRepository graph hit rate: %d%%',
822
100.0 * len(self._requested_parents) / len(ancestry))
823
return dict((k, ancestry[k]) for k in present_keys)
825
def _get_parent_map(self, keys):
826
"""Helper for get_parent_map that performs the RPC."""
827
medium = self._client._medium
828
if not medium._remote_is_at_least_1_2:
829
# We already found out that the server can't understand
830
# Repository.get_parent_map requests, so just fetch the whole
832
# XXX: Note that this will issue a deprecation warning. This is ok
833
# :- its because we're working with a deprecated server anyway, and
834
# the user will almost certainly have seen a warning about the
835
# server version already.
836
rg = self.get_revision_graph()
837
# There is an api discrepency between get_parent_map and
838
# get_revision_graph. Specifically, a "key:()" pair in
839
# get_revision_graph just means a node has no parents. For
840
# "get_parent_map" it means the node is a ghost. So fix up the
841
# graph to correct this.
842
# https://bugs.launchpad.net/bzr/+bug/214894
843
# There is one other "bug" which is that ghosts in
844
# get_revision_graph() are not returned at all. But we won't worry
845
# about that for now.
846
for node_id, parent_ids in rg.iteritems():
848
rg[node_id] = (NULL_REVISION,)
849
rg[NULL_REVISION] = ()
853
if NULL_REVISION in keys:
854
keys.discard(NULL_REVISION)
855
found_parents = {NULL_REVISION:()}
860
# TODO(Needs analysis): We could assume that the keys being requested
861
# from get_parent_map are in a breadth first search, so typically they
862
# will all be depth N from some common parent, and we don't have to
863
# have the server iterate from the root parent, but rather from the
864
# keys we're searching; and just tell the server the keyspace we
865
# already have; but this may be more traffic again.
867
# Transform self._parents_map into a search request recipe.
868
# TODO: Manage this incrementally to avoid covering the same path
869
# repeatedly. (The server will have to on each request, but the less
870
# work done the better).
871
parents_map = self._parents_map
872
if parents_map is None:
873
# Repository is not locked, so there's no cache.
875
start_set = set(parents_map)
876
result_parents = set()
877
for parents in parents_map.itervalues():
878
result_parents.update(parents)
879
stop_keys = result_parents.difference(start_set)
880
included_keys = start_set.intersection(result_parents)
881
start_set.difference_update(included_keys)
882
recipe = (start_set, stop_keys, len(parents_map))
883
body = self._serialise_search_recipe(recipe)
884
path = self.bzrdir._path_for_remote_call(self._client)
886
if type(key) is not str:
888
"key %r not a plain string" % (key,))
889
verb = 'Repository.get_parent_map'
890
args = (path,) + tuple(keys)
892
response = self._client.call_with_body_bytes_expecting_body(
893
verb, args, self._serialise_search_recipe(recipe))
894
except errors.UnknownSmartMethod:
895
# Server does not support this method, so get the whole graph.
896
# Worse, we have to force a disconnection, because the server now
897
# doesn't realise it has a body on the wire to consume, so the
898
# only way to recover is to abandon the connection.
900
'Server is too old for fast get_parent_map, reconnecting. '
901
'(Upgrade the server to Bazaar 1.2 to avoid this)')
903
# To avoid having to disconnect repeatedly, we keep track of the
904
# fact the server doesn't understand remote methods added in 1.2.
905
medium._remote_is_at_least_1_2 = False
906
return self.get_revision_graph(None)
907
if response[0][0] not in ['ok']:
908
response[1].cancel_read_body()
909
raise errors.UnexpectedSmartServerResponse(response[0])
910
if response[0][0] == 'ok':
911
coded = bz2.decompress(response[1].read_body_bytes())
915
lines = coded.split('\n')
918
d = tuple(line.split())
920
revision_graph[d[0]] = d[1:]
922
# No parents - so give the Graph result (NULL_REVISION,).
923
revision_graph[d[0]] = (NULL_REVISION,)
924
return revision_graph
927
def get_signature_text(self, revision_id):
929
return self._real_repository.get_signature_text(revision_id)
932
@symbol_versioning.deprecated_method(symbol_versioning.one_three)
933
def get_revision_graph_with_ghosts(self, revision_ids=None):
935
return self._real_repository.get_revision_graph_with_ghosts(
936
revision_ids=revision_ids)
939
def get_inventory_xml(self, revision_id):
941
return self._real_repository.get_inventory_xml(revision_id)
943
def deserialise_inventory(self, revision_id, xml):
945
return self._real_repository.deserialise_inventory(revision_id, xml)
947
def reconcile(self, other=None, thorough=False):
949
return self._real_repository.reconcile(other=other, thorough=thorough)
951
def all_revision_ids(self):
953
return self._real_repository.all_revision_ids()
956
def get_deltas_for_revisions(self, revisions):
958
return self._real_repository.get_deltas_for_revisions(revisions)
961
def get_revision_delta(self, revision_id):
963
return self._real_repository.get_revision_delta(revision_id)
966
def revision_trees(self, revision_ids):
968
return self._real_repository.revision_trees(revision_ids)
971
def get_revision_reconcile(self, revision_id):
973
return self._real_repository.get_revision_reconcile(revision_id)
976
def check(self, revision_ids=None):
978
return self._real_repository.check(revision_ids=revision_ids)
980
def copy_content_into(self, destination, revision_id=None):
982
return self._real_repository.copy_content_into(
983
destination, revision_id=revision_id)
985
def _copy_repository_tarball(self, to_bzrdir, revision_id=None):
986
# get a tarball of the remote repository, and copy from that into the
988
from bzrlib import osutils
991
# TODO: Maybe a progress bar while streaming the tarball?
992
note("Copying repository content as tarball...")
993
tar_file = self._get_tarball('bz2')
996
destination = to_bzrdir.create_repository()
998
tar = tarfile.open('repository', fileobj=tar_file,
1000
tmpdir = tempfile.mkdtemp()
1002
_extract_tar(tar, tmpdir)
1003
tmp_bzrdir = BzrDir.open(tmpdir)
1004
tmp_repo = tmp_bzrdir.open_repository()
1005
tmp_repo.copy_content_into(destination, revision_id)
1007
osutils.rmtree(tmpdir)
1011
# TODO: Suggestion from john: using external tar is much faster than
1012
# python's tarfile library, but it may not work on windows.
1016
"""Compress the data within the repository.
1018
This is not currently implemented within the smart server.
1021
return self._real_repository.pack()
1023
def set_make_working_trees(self, new_value):
1025
self._real_repository.set_make_working_trees(new_value)
1028
def sign_revision(self, revision_id, gpg_strategy):
1030
return self._real_repository.sign_revision(revision_id, gpg_strategy)
1033
def get_revisions(self, revision_ids):
1035
return self._real_repository.get_revisions(revision_ids)
1037
def supports_rich_root(self):
1039
return self._real_repository.supports_rich_root()
1041
def iter_reverse_revision_history(self, revision_id):
1043
return self._real_repository.iter_reverse_revision_history(revision_id)
1046
def _serializer(self):
1048
return self._real_repository._serializer
1050
def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
1052
return self._real_repository.store_revision_signature(
1053
gpg_strategy, plaintext, revision_id)
1055
def add_signature_text(self, revision_id, signature):
1057
return self._real_repository.add_signature_text(revision_id, signature)
1059
def has_signature_for_revision_id(self, revision_id):
1061
return self._real_repository.has_signature_for_revision_id(revision_id)
1063
def get_data_stream_for_search(self, search):
1064
medium = self._client._medium
1065
if not medium._remote_is_at_least_1_2:
1067
return self._real_repository.get_data_stream_for_search(search)
1068
REQUEST_NAME = 'Repository.stream_revisions_chunked'
1069
path = self.bzrdir._path_for_remote_call(self._client)
1070
body = self._serialise_search_recipe(search.get_recipe())
1072
result = self._client.call_with_body_bytes_expecting_body(
1073
REQUEST_NAME, (path,), body)
1074
response, protocol = result
1075
except errors.UnknownSmartMethod:
1076
# Server does not support this method, so fall back to VFS.
1077
# Worse, we have to force a disconnection, because the server now
1078
# doesn't realise it has a body on the wire to consume, so the
1079
# only way to recover is to abandon the connection.
1081
'Server is too old for streaming pull, reconnecting. '
1082
'(Upgrade the server to Bazaar 1.2 to avoid this)')
1084
# To avoid having to disconnect repeatedly, we keep track of the
1085
# fact the server doesn't understand this remote method.
1086
medium._remote_is_at_least_1_2 = False
1088
return self._real_repository.get_data_stream_for_search(search)
1090
if response == ('ok',):
1091
return self._deserialise_stream(protocol)
1092
if response == ('NoSuchRevision', ):
1093
# We cannot easily identify the revision that is missing in this
1094
# situation without doing much more network IO. For now, bail.
1095
raise NoSuchRevision(self, "unknown")
1097
raise errors.UnexpectedSmartServerResponse(response)
1099
def _deserialise_stream(self, protocol):
1100
stream = protocol.read_streamed_body()
1101
container_parser = ContainerPushParser()
1102
for bytes in stream:
1103
container_parser.accept_bytes(bytes)
1104
records = container_parser.read_pending_records()
1105
for record_names, record_bytes in records:
1106
if len(record_names) != 1:
1107
# These records should have only one name, and that name
1108
# should be a one-element tuple.
1109
raise errors.SmartProtocolError(
1110
'Repository data stream had invalid record name %r'
1112
name_tuple = record_names[0]
1113
yield name_tuple, record_bytes
1115
def insert_data_stream(self, stream):
1117
self._real_repository.insert_data_stream(stream)
1119
def item_keys_introduced_by(self, revision_ids, _files_pb=None):
1121
return self._real_repository.item_keys_introduced_by(revision_ids,
1122
_files_pb=_files_pb)
1124
def revision_graph_can_have_wrong_parents(self):
1125
# The answer depends on the remote repo format.
1127
return self._real_repository.revision_graph_can_have_wrong_parents()
1129
def _find_inconsistent_revision_parents(self):
1131
return self._real_repository._find_inconsistent_revision_parents()
1133
def _check_for_inconsistent_revision_parents(self):
1135
return self._real_repository._check_for_inconsistent_revision_parents()
1137
def _make_parents_provider(self):
1140
def _serialise_search_recipe(self, recipe):
1141
"""Serialise a graph search recipe.
1143
:param recipe: A search recipe (start, stop, count).
1144
:return: Serialised bytes.
1146
start_keys = ' '.join(recipe[0])
1147
stop_keys = ' '.join(recipe[1])
1148
count = str(recipe[2])
1149
return '\n'.join((start_keys, stop_keys, count))
1152
class RemoteBranchLockableFiles(LockableFiles):
1153
"""A 'LockableFiles' implementation that talks to a smart server.
1155
This is not a public interface class.
1158
def __init__(self, bzrdir, _client):
1159
self.bzrdir = bzrdir
1160
self._client = _client
1161
self._need_find_modes = True
1162
LockableFiles.__init__(
1163
self, bzrdir.get_branch_transport(None),
1164
'lock', lockdir.LockDir)
1166
def _find_modes(self):
1167
# RemoteBranches don't let the client set the mode of control files.
1168
self._dir_mode = None
1169
self._file_mode = None
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
return a_bzrdir.open_branch()
1187
def initialize(self, a_bzrdir):
1188
return a_bzrdir.create_branch()
1190
def supports_tags(self):
1191
# Remote branches might support tags, but we won't know until we
1192
# access the real remote branch.
1196
class RemoteBranch(branch.Branch):
1197
"""Branch stored on a server accessed by HPSS RPC.
1199
At the moment most operations are mapped down to simple file operations.
1202
def __init__(self, remote_bzrdir, remote_repository, real_branch=None,
1204
"""Create a RemoteBranch instance.
1206
:param real_branch: An optional local implementation of the branch
1207
format, usually accessing the data via the VFS.
1208
:param _client: Private parameter for testing.
1210
# We intentionally don't call the parent class's __init__, because it
1211
# will try to assign to self.tags, which is a property in this subclass.
1212
# And the parent's __init__ doesn't do much anyway.
1213
self._revision_id_to_revno_cache = None
1214
self._revision_history_cache = None
1215
self.bzrdir = remote_bzrdir
1216
if _client is not None:
1217
self._client = _client
1219
self._client = remote_bzrdir._client
1220
self.repository = remote_repository
1221
if real_branch is not None:
1222
self._real_branch = real_branch
1223
# Give the remote repository the matching real repo.
1224
real_repo = self._real_branch.repository
1225
if isinstance(real_repo, RemoteRepository):
1226
real_repo._ensure_real()
1227
real_repo = real_repo._real_repository
1228
self.repository._set_real_repository(real_repo)
1229
# Give the branch the remote repository to let fast-pathing happen.
1230
self._real_branch.repository = self.repository
1232
self._real_branch = None
1233
# Fill out expected attributes of branch for bzrlib api users.
1234
self._format = RemoteBranchFormat()
1235
self.base = self.bzrdir.root_transport.base
1236
self._control_files = None
1237
self._lock_mode = None
1238
self._lock_token = None
1239
self._repo_lock_token = None
1240
self._lock_count = 0
1241
self._leave_lock = False
1244
return "%s(%s)" % (self.__class__.__name__, self.base)
1248
def _ensure_real(self):
1249
"""Ensure that there is a _real_branch set.
1251
Used before calls to self._real_branch.
1253
if not self._real_branch:
1254
if not vfs.vfs_enabled():
1255
raise AssertionError('smart server vfs must be enabled '
1256
'to use vfs implementation')
1257
self.bzrdir._ensure_real()
1258
self._real_branch = self.bzrdir._real_bzrdir.open_branch()
1259
# Give the remote repository the matching real repo.
1260
real_repo = self._real_branch.repository
1261
if isinstance(real_repo, RemoteRepository):
1262
real_repo._ensure_real()
1263
real_repo = real_repo._real_repository
1264
self.repository._set_real_repository(real_repo)
1265
# Give the branch the remote repository to let fast-pathing happen.
1266
self._real_branch.repository = self.repository
1267
# XXX: deal with _lock_mode == 'w'
1268
if self._lock_mode == 'r':
1269
self._real_branch.lock_read()
1272
def control_files(self):
1273
# Defer actually creating RemoteBranchLockableFiles until its needed,
1274
# because it triggers an _ensure_real that we otherwise might not need.
1275
if self._control_files is None:
1276
self._control_files = RemoteBranchLockableFiles(
1277
self.bzrdir, self._client)
1278
return self._control_files
1280
def _get_checkout_format(self):
1282
return self._real_branch._get_checkout_format()
1284
def get_physical_lock_status(self):
1285
"""See Branch.get_physical_lock_status()."""
1286
# should be an API call to the server, as branches must be lockable.
1288
return self._real_branch.get_physical_lock_status()
1290
def lock_read(self):
1291
if not self._lock_mode:
1292
self._lock_mode = 'r'
1293
self._lock_count = 1
1294
if self._real_branch is not None:
1295
self._real_branch.lock_read()
1297
self._lock_count += 1
1299
def _remote_lock_write(self, token):
1301
branch_token = repo_token = ''
1303
branch_token = token
1304
repo_token = self.repository.lock_write()
1305
self.repository.unlock()
1306
path = self.bzrdir._path_for_remote_call(self._client)
1307
response = self._client.call('Branch.lock_write', path, branch_token,
1309
if response[0] == 'ok':
1310
ok, branch_token, repo_token = response
1311
return branch_token, repo_token
1312
elif response[0] == 'LockContention':
1313
raise errors.LockContention('(remote lock)')
1314
elif response[0] == 'TokenMismatch':
1315
raise errors.TokenMismatch(token, '(remote token)')
1316
elif response[0] == 'UnlockableTransport':
1317
raise errors.UnlockableTransport(self.bzrdir.root_transport)
1318
elif response[0] == 'ReadOnlyError':
1319
raise errors.ReadOnlyError(self)
1320
elif response[0] == 'LockFailed':
1321
raise errors.LockFailed(response[1], response[2])
1323
raise errors.UnexpectedSmartServerResponse(response)
1325
def lock_write(self, token=None):
1326
if not self._lock_mode:
1327
remote_tokens = self._remote_lock_write(token)
1328
self._lock_token, self._repo_lock_token = remote_tokens
1329
if not self._lock_token:
1330
raise SmartProtocolError('Remote server did not return a token!')
1331
# TODO: We really, really, really don't want to call _ensure_real
1332
# here, but it's the easiest way to ensure coherency between the
1333
# state of the RemoteBranch and RemoteRepository objects and the
1334
# physical locks. If we don't materialise the real objects here,
1335
# then getting everything in the right state later is complex, so
1336
# for now we just do it the lazy way.
1337
# -- Andrew Bennetts, 2007-02-22.
1339
if self._real_branch is not None:
1340
self._real_branch.repository.lock_write(
1341
token=self._repo_lock_token)
1343
self._real_branch.lock_write(token=self._lock_token)
1345
self._real_branch.repository.unlock()
1346
if token is not None:
1347
self._leave_lock = True
1349
# XXX: this case seems to be unreachable; token cannot be None.
1350
self._leave_lock = False
1351
self._lock_mode = 'w'
1352
self._lock_count = 1
1353
elif self._lock_mode == 'r':
1354
raise errors.ReadOnlyTransaction
1356
if token is not None:
1357
# A token was given to lock_write, and we're relocking, so check
1358
# that the given token actually matches the one we already have.
1359
if token != self._lock_token:
1360
raise errors.TokenMismatch(token, self._lock_token)
1361
self._lock_count += 1
1362
return self._lock_token or None
1364
def _unlock(self, branch_token, repo_token):
1365
path = self.bzrdir._path_for_remote_call(self._client)
1366
response = self._client.call('Branch.unlock', path, branch_token,
1368
if response == ('ok',):
1370
elif response[0] == 'TokenMismatch':
1371
raise errors.TokenMismatch(
1372
str((branch_token, repo_token)), '(remote tokens)')
1374
raise errors.UnexpectedSmartServerResponse(response)
1377
self._lock_count -= 1
1378
if not self._lock_count:
1379
self._clear_cached_state()
1380
mode = self._lock_mode
1381
self._lock_mode = None
1382
if self._real_branch is not None:
1383
if (not self._leave_lock and mode == 'w' and
1384
self._repo_lock_token):
1385
# If this RemoteBranch will remove the physical lock for the
1386
# repository, make sure the _real_branch doesn't do it
1387
# first. (Because the _real_branch's repository is set to
1388
# be the RemoteRepository.)
1389
self._real_branch.repository.leave_lock_in_place()
1390
self._real_branch.unlock()
1392
# Only write-locked branched need to make a remote method call
1393
# to perfom the unlock.
1395
if not self._lock_token:
1396
raise AssertionError('Locked, but no token!')
1397
branch_token = self._lock_token
1398
repo_token = self._repo_lock_token
1399
self._lock_token = None
1400
self._repo_lock_token = None
1401
if not self._leave_lock:
1402
self._unlock(branch_token, repo_token)
1404
def break_lock(self):
1406
return self._real_branch.break_lock()
1408
def leave_lock_in_place(self):
1409
if not self._lock_token:
1410
raise NotImplementedError(self.leave_lock_in_place)
1411
self._leave_lock = True
1413
def dont_leave_lock_in_place(self):
1414
if not self._lock_token:
1415
raise NotImplementedError(self.dont_leave_lock_in_place)
1416
self._leave_lock = False
1418
def last_revision_info(self):
1419
"""See Branch.last_revision_info()."""
1420
path = self.bzrdir._path_for_remote_call(self._client)
1421
response = self._client.call('Branch.last_revision_info', path)
1422
if response[0] != 'ok':
1423
raise SmartProtocolError('unexpected response code %s' % (response,))
1424
revno = int(response[1])
1425
last_revision = response[2]
1426
return (revno, last_revision)
1428
def _gen_revision_history(self):
1429
"""See Branch._gen_revision_history()."""
1430
path = self.bzrdir._path_for_remote_call(self._client)
1431
response = self._client.call_expecting_body(
1432
'Branch.revision_history', path)
1433
if response[0][0] != 'ok':
1434
raise SmartProtocolError('unexpected response code %s' % (response,))
1435
result = response[1].read_body_bytes().split('\x00')
1441
def set_revision_history(self, rev_history):
1442
# Send just the tip revision of the history; the server will generate
1443
# the full history from that. If the revision doesn't exist in this
1444
# branch, NoSuchRevision will be raised.
1445
path = self.bzrdir._path_for_remote_call(self._client)
1446
if rev_history == []:
1449
rev_id = rev_history[-1]
1450
self._clear_cached_state()
1451
response = self._client.call('Branch.set_last_revision',
1452
path, self._lock_token, self._repo_lock_token, rev_id)
1453
if response[0] == 'NoSuchRevision':
1454
raise NoSuchRevision(self, rev_id)
1455
elif response[0] != 'ok':
1456
raise SmartProtocolError('unexpected response code %s' % (response,))
1457
self._cache_revision_history(rev_history)
1459
def get_parent(self):
1461
return self._real_branch.get_parent()
1463
def set_parent(self, url):
1465
return self._real_branch.set_parent(url)
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
1502
def set_last_revision_info(self, revno, revision_id):
1503
revision_id = ensure_null(revision_id)
1504
path = self.bzrdir._path_for_remote_call(self._client)
1506
response = self._client.call('Branch.set_last_revision_info',
1507
path, self._lock_token, self._repo_lock_token, str(revno), revision_id)
1508
except errors.UnknownSmartMethod:
1510
self._clear_cached_state()
1511
return self._real_branch.set_last_revision_info(revno, revision_id)
1512
if response == ('ok',):
1513
self._clear_cached_state()
1514
elif response[0] == 'NoSuchRevision':
1515
raise NoSuchRevision(self, response[1])
1517
raise errors.UnexpectedSmartServerResponse(response)
1519
def generate_revision_history(self, revision_id, last_rev=None,
1522
return self._real_branch.generate_revision_history(
1523
revision_id, last_rev=last_rev, other_branch=other_branch)
1528
return self._real_branch.tags
1530
def set_push_location(self, location):
1532
return self._real_branch.set_push_location(location)
1534
def update_revisions(self, other, stop_revision=None, overwrite=False):
1536
return self._real_branch.update_revisions(
1537
other, stop_revision=stop_revision, overwrite=overwrite)
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)