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 NoSuchRevision
38
from bzrlib.lockable_files import LockableFiles
39
from bzrlib.pack import ContainerPushParser
40
from bzrlib.smart import client, vfs
41
from bzrlib.symbol_versioning import (
44
from bzrlib.revision import ensure_null, 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
medium = transport.get_smart_medium()
65
self._client = client._SmartClient(medium, transport.base)
67
self._client = _client
70
path = self._path_for_remote_call(self._client)
71
response = self._client.call('BzrDir.open', path)
72
if response not in [('yes',), ('no',)]:
73
raise errors.UnexpectedSmartServerResponse(response)
74
if response == ('no',):
75
raise errors.NotBranchError(path=transport.base)
77
def _ensure_real(self):
78
"""Ensure that there is a _real_bzrdir set.
80
Used before calls to self._real_bzrdir.
82
if not self._real_bzrdir:
83
self._real_bzrdir = BzrDir.open_from_transport(
84
self.root_transport, _server_formats=False)
86
def create_repository(self, shared=False):
88
self._real_bzrdir.create_repository(shared=shared)
89
return self.open_repository()
91
def destroy_repository(self):
92
"""See BzrDir.destroy_repository"""
94
self._real_bzrdir.destroy_repository()
96
def create_branch(self):
98
real_branch = self._real_bzrdir.create_branch()
99
return RemoteBranch(self, self.find_repository(), real_branch)
101
def destroy_branch(self):
102
"""See BzrDir.destroy_branch"""
104
self._real_bzrdir.destroy_branch()
106
def create_workingtree(self, revision_id=None, from_branch=None):
107
raise errors.NotLocalUrl(self.transport.base)
109
def find_branch_format(self):
110
"""Find the branch 'format' for this bzrdir.
112
This might be a synthetic object for e.g. RemoteBranch and SVN.
114
b = self.open_branch()
117
def get_branch_reference(self):
118
"""See BzrDir.get_branch_reference()."""
119
path = self._path_for_remote_call(self._client)
120
response = self._client.call('BzrDir.open_branch', path)
121
if response[0] == 'ok':
122
if response[1] == '':
123
# branch at this location.
126
# a branch reference, use the existing BranchReference logic.
128
elif response == ('nobranch',):
129
raise errors.NotBranchError(path=self.root_transport.base)
131
raise errors.UnexpectedSmartServerResponse(response)
133
def _get_tree_branch(self):
134
"""See BzrDir._get_tree_branch()."""
135
return None, self.open_branch()
137
def open_branch(self, _unsupported=False):
138
assert _unsupported == False, 'unsupported flag support not implemented yet.'
139
reference_url = self.get_branch_reference()
140
if reference_url is None:
141
# branch at this location.
142
return RemoteBranch(self, self.find_repository())
144
# a branch reference, use the existing BranchReference logic.
145
format = BranchReferenceFormat()
146
return format.open(self, _found=True, location=reference_url)
148
def open_repository(self):
149
path = self._path_for_remote_call(self._client)
150
verb = 'BzrDir.find_repositoryV2'
152
response = self._client.call(verb, path)
153
except errors.UnknownSmartMethod:
154
verb = 'BzrDir.find_repository'
155
response = self._client.call(verb, path)
156
assert response[0] in ('ok', 'norepository'), \
157
'unexpected response code %s' % (response,)
158
if response[0] == 'norepository':
159
raise errors.NoRepositoryPresent(self)
160
if verb == 'BzrDir.find_repository':
161
# servers that don't support the V2 method don't support external
163
response = response + ('no', )
164
assert len(response) == 5, 'incorrect response length %s' % (response,)
165
if response[1] == '':
166
format = RemoteRepositoryFormat()
167
format.rich_root_data = (response[2] == 'yes')
168
format.supports_tree_reference = (response[3] == 'yes')
169
# No wire format to check this yet.
170
format.supports_external_lookups = (response[4] == 'yes')
171
return RemoteRepository(self, format)
173
raise errors.NoRepositoryPresent(self)
175
def open_workingtree(self, recommend_upgrade=True):
177
if self._real_bzrdir.has_workingtree():
178
raise errors.NotLocalUrl(self.root_transport)
180
raise errors.NoWorkingTree(self.root_transport.base)
182
def _path_for_remote_call(self, client):
183
"""Return the path to be used for this bzrdir in a remote call."""
184
return client.remote_path_from_transport(self.root_transport)
186
def get_branch_transport(self, branch_format):
188
return self._real_bzrdir.get_branch_transport(branch_format)
190
def get_repository_transport(self, repository_format):
192
return self._real_bzrdir.get_repository_transport(repository_format)
194
def get_workingtree_transport(self, workingtree_format):
196
return self._real_bzrdir.get_workingtree_transport(workingtree_format)
198
def can_convert_format(self):
199
"""Upgrading of remote bzrdirs is not supported yet."""
202
def needs_format_conversion(self, format=None):
203
"""Upgrading of remote bzrdirs is not supported yet."""
206
def clone(self, url, revision_id=None, force_new_repo=False):
208
return self._real_bzrdir.clone(url, revision_id=revision_id,
209
force_new_repo=force_new_repo)
212
class RemoteRepositoryFormat(repository.RepositoryFormat):
213
"""Format for repositories accessed over a _SmartClient.
215
Instances of this repository are represented by RemoteRepository
218
The RemoteRepositoryFormat is parameterized during construction
219
to reflect the capabilities of the real, remote format. Specifically
220
the attributes rich_root_data and supports_tree_reference are set
221
on a per instance basis, and are not set (and should not be) at
225
_matchingbzrdir = RemoteBzrDirFormat
227
def initialize(self, a_bzrdir, shared=False):
228
assert isinstance(a_bzrdir, RemoteBzrDir), \
229
'%r is not a RemoteBzrDir' % (a_bzrdir,)
230
return a_bzrdir.create_repository(shared=shared)
232
def open(self, a_bzrdir):
233
assert isinstance(a_bzrdir, RemoteBzrDir)
234
return a_bzrdir.open_repository()
236
def get_format_description(self):
237
return 'bzr remote repository'
239
def __eq__(self, other):
240
return self.__class__ == other.__class__
242
def check_conversion_target(self, target_format):
243
if self.rich_root_data and not target_format.rich_root_data:
244
raise errors.BadConversionTarget(
245
'Does not support rich root data.', target_format)
246
if (self.supports_tree_reference and
247
not getattr(target_format, 'supports_tree_reference', False)):
248
raise errors.BadConversionTarget(
249
'Does not support nested trees', target_format)
252
class RemoteRepository(object):
253
"""Repository accessed over rpc.
255
For the moment most operations are performed using local transport-backed
259
def __init__(self, remote_bzrdir, format, real_repository=None, _client=None):
260
"""Create a RemoteRepository instance.
262
:param remote_bzrdir: The bzrdir hosting this repository.
263
:param format: The RemoteFormat object to use.
264
:param real_repository: If not None, a local implementation of the
265
repository logic for the repository, usually accessing the data
267
:param _client: Private testing parameter - override the smart client
268
to be used by the repository.
271
self._real_repository = real_repository
273
self._real_repository = None
274
self.bzrdir = remote_bzrdir
276
self._client = remote_bzrdir._client
278
self._client = _client
279
self._format = format
280
self._lock_mode = None
281
self._lock_token = None
283
self._leave_lock = False
284
# A cache of looked up revision parent data; reset at unlock time.
285
self._parents_map = None
286
if 'hpss' in debug.debug_flags:
287
self._requested_parents = None
289
# These depend on the actual remote format, so force them off for
290
# maximum compatibility. XXX: In future these should depend on the
291
# remote repository instance, but this is irrelevant until we perform
292
# reconcile via an RPC call.
293
self._reconcile_does_inventory_gc = False
294
self._reconcile_fixes_text_parents = False
295
self._reconcile_backsup_inventory = False
296
self.base = self.bzrdir.transport.base
299
return "%s(%s)" % (self.__class__.__name__, self.base)
303
def abort_write_group(self):
304
"""Complete a write group on the decorated repository.
306
Smart methods peform operations in a single step so this api
307
is not really applicable except as a compatibility thunk
308
for older plugins that don't use e.g. the CommitBuilder
312
return self._real_repository.abort_write_group()
314
def commit_write_group(self):
315
"""Complete a write group on the decorated repository.
317
Smart methods peform operations in a single step so this api
318
is not really applicable except as a compatibility thunk
319
for older plugins that don't use e.g. the CommitBuilder
323
return self._real_repository.commit_write_group()
325
def _ensure_real(self):
326
"""Ensure that there is a _real_repository set.
328
Used before calls to self._real_repository.
330
if not self._real_repository:
331
self.bzrdir._ensure_real()
332
#self._real_repository = self.bzrdir._real_bzrdir.open_repository()
333
self._set_real_repository(self.bzrdir._real_bzrdir.open_repository())
335
def find_text_key_references(self):
336
"""Find the text key references within the repository.
338
:return: a dictionary mapping (file_id, revision_id) tuples to altered file-ids to an iterable of
339
revision_ids. Each altered file-ids has the exact revision_ids that
340
altered it listed explicitly.
341
:return: A dictionary mapping text keys ((fileid, revision_id) tuples)
342
to whether they were referred to by the inventory of the
343
revision_id that they contain. The inventory texts from all present
344
revision ids are assessed to generate this report.
347
return self._real_repository.find_text_key_references()
349
def _generate_text_key_index(self):
350
"""Generate a new text key index for the repository.
352
This is an expensive function that will take considerable time to run.
354
:return: A dict mapping (file_id, revision_id) tuples to a list of
355
parents, also (file_id, revision_id) tuples.
358
return self._real_repository._generate_text_key_index()
360
@symbol_versioning.deprecated_method(symbol_versioning.one_four)
361
def get_revision_graph(self, revision_id=None):
362
"""See Repository.get_revision_graph()."""
363
return self._get_revision_graph(revision_id)
365
def _get_revision_graph(self, revision_id):
366
"""Private method for using with old (< 1.2) servers to fallback."""
367
if revision_id is None:
369
elif revision.is_null(revision_id):
372
path = self.bzrdir._path_for_remote_call(self._client)
373
assert type(revision_id) is str
374
response = self._client.call_expecting_body(
375
'Repository.get_revision_graph', path, revision_id)
376
if response[0][0] not in ['ok', 'nosuchrevision']:
377
raise errors.UnexpectedSmartServerResponse(response[0])
378
if response[0][0] == 'ok':
379
coded = response[1].read_body_bytes()
381
# no revisions in this repository!
383
lines = coded.split('\n')
386
d = tuple(line.split())
387
revision_graph[d[0]] = d[1:]
389
return revision_graph
391
response_body = response[1].read_body_bytes()
392
assert response_body == ''
393
raise NoSuchRevision(self, revision_id)
395
def has_revision(self, revision_id):
396
"""See Repository.has_revision()."""
397
if revision_id == NULL_REVISION:
398
# The null revision is always present.
400
path = self.bzrdir._path_for_remote_call(self._client)
401
response = self._client.call('Repository.has_revision', path, revision_id)
402
assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
403
return response[0] == 'yes'
405
def has_revisions(self, revision_ids):
406
"""See Repository.has_revisions()."""
408
for revision_id in revision_ids:
409
if self.has_revision(revision_id):
410
result.add(revision_id)
413
def has_same_location(self, other):
414
return (self.__class__ == other.__class__ and
415
self.bzrdir.transport.base == other.bzrdir.transport.base)
417
def get_graph(self, other_repository=None):
418
"""Return the graph for this repository format"""
419
parents_provider = self
420
if (other_repository is not None and
421
other_repository.bzrdir.transport.base !=
422
self.bzrdir.transport.base):
423
parents_provider = graph._StackedParentsProvider(
424
[parents_provider, other_repository._make_parents_provider()])
425
return graph.Graph(parents_provider)
427
def gather_stats(self, revid=None, committers=None):
428
"""See Repository.gather_stats()."""
429
path = self.bzrdir._path_for_remote_call(self._client)
430
# revid can be None to indicate no revisions, not just NULL_REVISION
431
if revid is None or revision.is_null(revid):
435
if committers is None or not committers:
436
fmt_committers = 'no'
438
fmt_committers = 'yes'
439
response = self._client.call_expecting_body(
440
'Repository.gather_stats', path, fmt_revid, fmt_committers)
441
assert response[0][0] == 'ok', \
442
'unexpected response code %s' % (response[0],)
444
body = response[1].read_body_bytes()
446
for line in body.split('\n'):
449
key, val_text = line.split(':')
450
if key in ('revisions', 'size', 'committers'):
451
result[key] = int(val_text)
452
elif key in ('firstrev', 'latestrev'):
453
values = val_text.split(' ')[1:]
454
result[key] = (float(values[0]), long(values[1]))
458
def find_branches(self, using=False):
459
"""See Repository.find_branches()."""
460
# should be an API call to the server.
462
return self._real_repository.find_branches(using=using)
464
def get_physical_lock_status(self):
465
"""See Repository.get_physical_lock_status()."""
466
# should be an API call to the server.
468
return self._real_repository.get_physical_lock_status()
470
def is_in_write_group(self):
471
"""Return True if there is an open write group.
473
write groups are only applicable locally for the smart server..
475
if self._real_repository:
476
return self._real_repository.is_in_write_group()
479
return self._lock_count >= 1
482
"""See Repository.is_shared()."""
483
path = self.bzrdir._path_for_remote_call(self._client)
484
response = self._client.call('Repository.is_shared', path)
485
assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
486
return response[0] == 'yes'
488
def is_write_locked(self):
489
return self._lock_mode == 'w'
492
# wrong eventually - want a local lock cache context
493
if not self._lock_mode:
494
self._lock_mode = 'r'
496
self._parents_map = {}
497
if 'hpss' in debug.debug_flags:
498
self._requested_parents = set()
499
if self._real_repository is not None:
500
self._real_repository.lock_read()
502
self._lock_count += 1
504
def _remote_lock_write(self, token):
505
path = self.bzrdir._path_for_remote_call(self._client)
508
response = self._client.call('Repository.lock_write', path, token)
509
if response[0] == 'ok':
512
elif response[0] == 'LockContention':
513
raise errors.LockContention('(remote lock)')
514
elif response[0] == 'UnlockableTransport':
515
raise errors.UnlockableTransport(self.bzrdir.root_transport)
516
elif response[0] == 'LockFailed':
517
raise errors.LockFailed(response[1], response[2])
519
raise errors.UnexpectedSmartServerResponse(response)
521
def lock_write(self, token=None):
522
if not self._lock_mode:
523
self._lock_token = self._remote_lock_write(token)
524
# if self._lock_token is None, then this is something like packs or
525
# svn where we don't get to lock the repo, or a weave style repository
526
# where we cannot lock it over the wire and attempts to do so will
528
if self._real_repository is not None:
529
self._real_repository.lock_write(token=self._lock_token)
530
if token is not None:
531
self._leave_lock = True
533
self._leave_lock = False
534
self._lock_mode = 'w'
536
self._parents_map = {}
537
if 'hpss' in debug.debug_flags:
538
self._requested_parents = set()
539
elif self._lock_mode == 'r':
540
raise errors.ReadOnlyError(self)
542
self._lock_count += 1
543
return self._lock_token or None
545
def leave_lock_in_place(self):
546
if not self._lock_token:
547
raise NotImplementedError(self.leave_lock_in_place)
548
self._leave_lock = True
550
def dont_leave_lock_in_place(self):
551
if not self._lock_token:
552
raise NotImplementedError(self.dont_leave_lock_in_place)
553
self._leave_lock = False
555
def _set_real_repository(self, repository):
556
"""Set the _real_repository for this repository.
558
:param repository: The repository to fallback to for non-hpss
559
implemented operations.
561
assert not isinstance(repository, RemoteRepository)
562
self._real_repository = repository
563
if self._lock_mode == 'w':
564
# if we are already locked, the real repository must be able to
565
# acquire the lock with our token.
566
self._real_repository.lock_write(self._lock_token)
567
elif self._lock_mode == 'r':
568
self._real_repository.lock_read()
570
def start_write_group(self):
571
"""Start a write group on the decorated repository.
573
Smart methods peform operations in a single step so this api
574
is not really applicable except as a compatibility thunk
575
for older plugins that don't use e.g. the CommitBuilder
579
return self._real_repository.start_write_group()
581
def _unlock(self, token):
582
path = self.bzrdir._path_for_remote_call(self._client)
584
# with no token the remote repository is not persistently locked.
586
response = self._client.call('Repository.unlock', path, token)
587
if response == ('ok',):
589
elif response[0] == 'TokenMismatch':
590
raise errors.TokenMismatch(token, '(remote token)')
592
raise errors.UnexpectedSmartServerResponse(response)
595
self._lock_count -= 1
596
if self._lock_count > 0:
598
self._parents_map = None
599
if 'hpss' in debug.debug_flags:
600
self._requested_parents = None
601
old_mode = self._lock_mode
602
self._lock_mode = None
604
# The real repository is responsible at present for raising an
605
# exception if it's in an unfinished write group. However, it
606
# normally will *not* actually remove the lock from disk - that's
607
# done by the server on receiving the Repository.unlock call.
608
# This is just to let the _real_repository stay up to date.
609
if self._real_repository is not None:
610
self._real_repository.unlock()
612
# The rpc-level lock should be released even if there was a
613
# problem releasing the vfs-based lock.
615
# Only write-locked repositories need to make a remote method
616
# call to perfom the unlock.
617
old_token = self._lock_token
618
self._lock_token = None
619
if not self._leave_lock:
620
self._unlock(old_token)
622
def break_lock(self):
623
# should hand off to the network
625
return self._real_repository.break_lock()
627
def _get_tarball(self, compression):
628
"""Return a TemporaryFile containing a repository tarball.
630
Returns None if the server does not support sending tarballs.
633
path = self.bzrdir._path_for_remote_call(self._client)
635
response, protocol = self._client.call_expecting_body(
636
'Repository.tarball', path, compression)
637
except errors.UnknownSmartMethod:
638
protocol.cancel_read_body()
640
if response[0] == 'ok':
641
# Extract the tarball and return it
642
t = tempfile.NamedTemporaryFile()
643
# TODO: rpc layer should read directly into it...
644
t.write(protocol.read_body_bytes())
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
"""See Repository.make_working_trees"""
718
return self._real_repository.make_working_trees()
720
def revision_ids_to_search_result(self, result_set):
721
"""Convert a set of revision ids to a graph SearchResult."""
722
result_parents = set()
723
for parents in self.get_graph().get_parent_map(
724
result_set).itervalues():
725
result_parents.update(parents)
726
included_keys = result_set.intersection(result_parents)
727
start_keys = result_set.difference(included_keys)
728
exclude_keys = result_parents.difference(result_set)
729
result = graph.SearchResult(start_keys, exclude_keys,
730
len(result_set), result_set)
734
def search_missing_revision_ids(self, other, revision_id=None, find_ghosts=True):
735
"""Return the revision ids that other has that this does not.
737
These are returned in topological order.
739
revision_id: only return revision ids included by revision_id.
741
return repository.InterRepository.get(
742
other, self).search_missing_revision_ids(revision_id, find_ghosts)
744
def fetch(self, source, revision_id=None, pb=None):
745
if self.has_same_location(source):
746
# check that last_revision is in 'from' and then return a
748
if (revision_id is not None and
749
not revision.is_null(revision_id)):
750
self.get_revision(revision_id)
753
return self._real_repository.fetch(
754
source, revision_id=revision_id, pb=pb)
756
def create_bundle(self, target, base, fileobj, format=None):
758
self._real_repository.create_bundle(target, base, fileobj, format)
761
def control_weaves(self):
763
return self._real_repository.control_weaves
766
def get_ancestry(self, revision_id, topo_sorted=True):
768
return self._real_repository.get_ancestry(revision_id, topo_sorted)
771
def get_inventory_weave(self):
773
return self._real_repository.get_inventory_weave()
775
def fileids_altered_by_revision_ids(self, revision_ids):
777
return self._real_repository.fileids_altered_by_revision_ids(revision_ids)
779
def _get_versioned_file_checker(self, revisions, revision_versions_cache):
781
return self._real_repository._get_versioned_file_checker(
782
revisions, revision_versions_cache)
784
def iter_files_bytes(self, desired_files):
785
"""See Repository.iter_file_bytes.
788
return self._real_repository.iter_files_bytes(desired_files)
790
def get_parent_map(self, keys):
791
"""See bzrlib.Graph.get_parent_map()."""
792
# Hack to build up the caching logic.
793
ancestry = self._parents_map
795
# Repository is not locked, so there's no cache.
796
missing_revisions = set(keys)
799
missing_revisions = set(key for key in keys if key not in ancestry)
800
if missing_revisions:
801
parent_map = self._get_parent_map(missing_revisions)
802
if 'hpss' in debug.debug_flags:
803
mutter('retransmitted revisions: %d of %d',
804
len(set(ancestry).intersection(parent_map)),
806
ancestry.update(parent_map)
807
present_keys = [k for k in keys if k in ancestry]
808
if 'hpss' in debug.debug_flags:
809
self._requested_parents.update(present_keys)
810
mutter('Current RemoteRepository graph hit rate: %d%%',
811
100.0 * len(self._requested_parents) / len(ancestry))
812
return dict((k, ancestry[k]) for k in present_keys)
814
def _get_parent_map(self, keys):
815
"""Helper for get_parent_map that performs the RPC."""
816
medium = self._client._medium
817
if not medium._remote_is_at_least_1_2:
818
# We already found out that the server can't understand
819
# Repository.get_parent_map requests, so just fetch the whole
821
# XXX: Note that this will issue a deprecation warning. This is ok
822
# :- its because we're working with a deprecated server anyway, and
823
# the user will almost certainly have seen a warning about the
824
# server version already.
825
rg = self.get_revision_graph()
826
# There is an api discrepency between get_parent_map and
827
# get_revision_graph. Specifically, a "key:()" pair in
828
# get_revision_graph just means a node has no parents. For
829
# "get_parent_map" it means the node is a ghost. So fix up the
830
# graph to correct this.
831
# https://bugs.launchpad.net/bzr/+bug/214894
832
# There is one other "bug" which is that ghosts in
833
# get_revision_graph() are not returned at all. But we won't worry
834
# about that for now.
835
for node_id, parent_ids in rg.iteritems():
837
rg[node_id] = (NULL_REVISION,)
838
rg[NULL_REVISION] = ()
842
if NULL_REVISION in keys:
843
keys.discard(NULL_REVISION)
844
found_parents = {NULL_REVISION:()}
849
# TODO(Needs analysis): We could assume that the keys being requested
850
# from get_parent_map are in a breadth first search, so typically they
851
# will all be depth N from some common parent, and we don't have to
852
# have the server iterate from the root parent, but rather from the
853
# keys we're searching; and just tell the server the keyspace we
854
# already have; but this may be more traffic again.
856
# Transform self._parents_map into a search request recipe.
857
# TODO: Manage this incrementally to avoid covering the same path
858
# repeatedly. (The server will have to on each request, but the less
859
# work done the better).
860
parents_map = self._parents_map
861
if parents_map is None:
862
# Repository is not locked, so there's no cache.
864
start_set = set(parents_map)
865
result_parents = set()
866
for parents in parents_map.itervalues():
867
result_parents.update(parents)
868
stop_keys = result_parents.difference(start_set)
869
included_keys = start_set.intersection(result_parents)
870
start_set.difference_update(included_keys)
871
recipe = (start_set, stop_keys, len(parents_map))
872
body = self._serialise_search_recipe(recipe)
873
path = self.bzrdir._path_for_remote_call(self._client)
875
if type(key) is not str:
877
"key %r not a plain string" % (key,))
878
verb = 'Repository.get_parent_map'
879
args = (path,) + tuple(keys)
881
response = self._client.call_with_body_bytes_expecting_body(
882
verb, args, self._serialise_search_recipe(recipe))
883
except errors.UnknownSmartMethod:
884
# Server does not support this method, so get the whole graph.
885
# Worse, we have to force a disconnection, because the server now
886
# doesn't realise it has a body on the wire to consume, so the
887
# only way to recover is to abandon the connection.
889
'Server is too old for fast get_parent_map, reconnecting. '
890
'(Upgrade the server to Bazaar 1.2 to avoid this)')
892
# To avoid having to disconnect repeatedly, we keep track of the
893
# fact the server doesn't understand remote methods added in 1.2.
894
medium._remote_is_at_least_1_2 = False
895
return self.get_revision_graph(None)
896
if response[0][0] not in ['ok']:
897
response[1].cancel_read_body()
898
raise errors.UnexpectedSmartServerResponse(response[0])
899
if response[0][0] == 'ok':
900
coded = bz2.decompress(response[1].read_body_bytes())
904
lines = coded.split('\n')
907
d = tuple(line.split())
909
revision_graph[d[0]] = d[1:]
911
# No parents - so give the Graph result (NULL_REVISION,).
912
revision_graph[d[0]] = (NULL_REVISION,)
913
return revision_graph
916
def get_signature_text(self, revision_id):
918
return self._real_repository.get_signature_text(revision_id)
921
@symbol_versioning.deprecated_method(symbol_versioning.one_three)
922
def get_revision_graph_with_ghosts(self, revision_ids=None):
924
return self._real_repository.get_revision_graph_with_ghosts(
925
revision_ids=revision_ids)
928
def get_inventory_xml(self, revision_id):
930
return self._real_repository.get_inventory_xml(revision_id)
932
def deserialise_inventory(self, revision_id, xml):
934
return self._real_repository.deserialise_inventory(revision_id, xml)
936
def reconcile(self, other=None, thorough=False):
938
return self._real_repository.reconcile(other=other, thorough=thorough)
940
def all_revision_ids(self):
942
return self._real_repository.all_revision_ids()
945
def get_deltas_for_revisions(self, revisions):
947
return self._real_repository.get_deltas_for_revisions(revisions)
950
def get_revision_delta(self, revision_id):
952
return self._real_repository.get_revision_delta(revision_id)
955
def revision_trees(self, revision_ids):
957
return self._real_repository.revision_trees(revision_ids)
960
def get_revision_reconcile(self, revision_id):
962
return self._real_repository.get_revision_reconcile(revision_id)
965
def check(self, revision_ids=None):
967
return self._real_repository.check(revision_ids=revision_ids)
969
def copy_content_into(self, destination, revision_id=None):
971
return self._real_repository.copy_content_into(
972
destination, revision_id=revision_id)
974
def _copy_repository_tarball(self, to_bzrdir, revision_id=None):
975
# get a tarball of the remote repository, and copy from that into the
977
from bzrlib import osutils
980
# TODO: Maybe a progress bar while streaming the tarball?
981
note("Copying repository content as tarball...")
982
tar_file = self._get_tarball('bz2')
985
destination = to_bzrdir.create_repository()
987
tar = tarfile.open('repository', fileobj=tar_file,
989
tmpdir = tempfile.mkdtemp()
991
_extract_tar(tar, tmpdir)
992
tmp_bzrdir = BzrDir.open(tmpdir)
993
tmp_repo = tmp_bzrdir.open_repository()
994
tmp_repo.copy_content_into(destination, revision_id)
996
osutils.rmtree(tmpdir)
1000
# TODO: Suggestion from john: using external tar is much faster than
1001
# python's tarfile library, but it may not work on windows.
1005
"""Compress the data within the repository.
1007
This is not currently implemented within the smart server.
1010
return self._real_repository.pack()
1012
def set_make_working_trees(self, new_value):
1014
self._real_repository.set_make_working_trees(new_value)
1017
def sign_revision(self, revision_id, gpg_strategy):
1019
return self._real_repository.sign_revision(revision_id, gpg_strategy)
1022
def get_revisions(self, revision_ids):
1024
return self._real_repository.get_revisions(revision_ids)
1026
def supports_rich_root(self):
1028
return self._real_repository.supports_rich_root()
1030
def iter_reverse_revision_history(self, revision_id):
1032
return self._real_repository.iter_reverse_revision_history(revision_id)
1035
def _serializer(self):
1037
return self._real_repository._serializer
1039
def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
1041
return self._real_repository.store_revision_signature(
1042
gpg_strategy, plaintext, revision_id)
1044
def add_signature_text(self, revision_id, signature):
1046
return self._real_repository.add_signature_text(revision_id, signature)
1048
def has_signature_for_revision_id(self, revision_id):
1050
return self._real_repository.has_signature_for_revision_id(revision_id)
1052
def get_data_stream_for_search(self, search):
1053
medium = self._client._medium
1054
if not medium._remote_is_at_least_1_2:
1056
return self._real_repository.get_data_stream_for_search(search)
1057
REQUEST_NAME = 'Repository.stream_revisions_chunked'
1058
path = self.bzrdir._path_for_remote_call(self._client)
1059
body = self._serialise_search_recipe(search.get_recipe())
1061
result = self._client.call_with_body_bytes_expecting_body(
1062
REQUEST_NAME, (path,), body)
1063
response, protocol = result
1064
except errors.UnknownSmartMethod:
1065
# Server does not support this method, so fall back to VFS.
1066
# Worse, we have to force a disconnection, because the server now
1067
# doesn't realise it has a body on the wire to consume, so the
1068
# only way to recover is to abandon the connection.
1070
'Server is too old for streaming pull, reconnecting. '
1071
'(Upgrade the server to Bazaar 1.2 to avoid this)')
1073
# To avoid having to disconnect repeatedly, we keep track of the
1074
# fact the server doesn't understand this remote method.
1075
medium._remote_is_at_least_1_2 = False
1077
return self._real_repository.get_data_stream_for_search(search)
1079
if response == ('ok',):
1080
return self._deserialise_stream(protocol)
1081
if response == ('NoSuchRevision', ):
1082
# We cannot easily identify the revision that is missing in this
1083
# situation without doing much more network IO. For now, bail.
1084
raise NoSuchRevision(self, "unknown")
1086
raise errors.UnexpectedSmartServerResponse(response)
1088
def _deserialise_stream(self, protocol):
1089
stream = protocol.read_streamed_body()
1090
container_parser = ContainerPushParser()
1091
for bytes in stream:
1092
container_parser.accept_bytes(bytes)
1093
records = container_parser.read_pending_records()
1094
for record_names, record_bytes in records:
1095
if len(record_names) != 1:
1096
# These records should have only one name, and that name
1097
# should be a one-element tuple.
1098
raise errors.SmartProtocolError(
1099
'Repository data stream had invalid record name %r'
1101
name_tuple = record_names[0]
1102
yield name_tuple, record_bytes
1104
def insert_data_stream(self, stream):
1106
self._real_repository.insert_data_stream(stream)
1108
def item_keys_introduced_by(self, revision_ids, _files_pb=None):
1110
return self._real_repository.item_keys_introduced_by(revision_ids,
1111
_files_pb=_files_pb)
1113
def revision_graph_can_have_wrong_parents(self):
1114
# The answer depends on the remote repo format.
1116
return self._real_repository.revision_graph_can_have_wrong_parents()
1118
def _find_inconsistent_revision_parents(self):
1120
return self._real_repository._find_inconsistent_revision_parents()
1122
def _check_for_inconsistent_revision_parents(self):
1124
return self._real_repository._check_for_inconsistent_revision_parents()
1126
def _make_parents_provider(self):
1129
def _serialise_search_recipe(self, recipe):
1130
"""Serialise a graph search recipe.
1132
:param recipe: A search recipe (start, stop, count).
1133
:return: Serialised bytes.
1135
start_keys = ' '.join(recipe[0])
1136
stop_keys = ' '.join(recipe[1])
1137
count = str(recipe[2])
1138
return '\n'.join((start_keys, stop_keys, count))
1141
class RemoteBranchLockableFiles(LockableFiles):
1142
"""A 'LockableFiles' implementation that talks to a smart server.
1144
This is not a public interface class.
1147
def __init__(self, bzrdir, _client):
1148
self.bzrdir = bzrdir
1149
self._client = _client
1150
self._need_find_modes = True
1151
LockableFiles.__init__(
1152
self, bzrdir.get_branch_transport(None),
1153
'lock', lockdir.LockDir)
1155
def _find_modes(self):
1156
# RemoteBranches don't let the client set the mode of control files.
1157
self._dir_mode = None
1158
self._file_mode = None
1161
class RemoteBranchFormat(branch.BranchFormat):
1163
def __eq__(self, other):
1164
return (isinstance(other, RemoteBranchFormat) and
1165
self.__dict__ == other.__dict__)
1167
def get_format_description(self):
1168
return 'Remote BZR Branch'
1170
def get_format_string(self):
1171
return 'Remote BZR Branch'
1173
def open(self, a_bzrdir):
1174
assert isinstance(a_bzrdir, RemoteBzrDir)
1175
return a_bzrdir.open_branch()
1177
def initialize(self, a_bzrdir):
1178
assert isinstance(a_bzrdir, RemoteBzrDir)
1179
return a_bzrdir.create_branch()
1181
def supports_tags(self):
1182
# Remote branches might support tags, but we won't know until we
1183
# access the real remote branch.
1187
class RemoteBranch(branch.Branch):
1188
"""Branch stored on a server accessed by HPSS RPC.
1190
At the moment most operations are mapped down to simple file operations.
1193
def __init__(self, remote_bzrdir, remote_repository, real_branch=None,
1195
"""Create a RemoteBranch instance.
1197
:param real_branch: An optional local implementation of the branch
1198
format, usually accessing the data via the VFS.
1199
:param _client: Private parameter for testing.
1201
# We intentionally don't call the parent class's __init__, because it
1202
# will try to assign to self.tags, which is a property in this subclass.
1203
# And the parent's __init__ doesn't do much anyway.
1204
self._revision_id_to_revno_cache = None
1205
self._revision_history_cache = None
1206
self.bzrdir = remote_bzrdir
1207
if _client is not None:
1208
self._client = _client
1210
self._client = remote_bzrdir._client
1211
self.repository = remote_repository
1212
if real_branch is not None:
1213
self._real_branch = real_branch
1214
# Give the remote repository the matching real repo.
1215
real_repo = self._real_branch.repository
1216
if isinstance(real_repo, RemoteRepository):
1217
real_repo._ensure_real()
1218
real_repo = real_repo._real_repository
1219
self.repository._set_real_repository(real_repo)
1220
# Give the branch the remote repository to let fast-pathing happen.
1221
self._real_branch.repository = self.repository
1223
self._real_branch = None
1224
# Fill out expected attributes of branch for bzrlib api users.
1225
self._format = RemoteBranchFormat()
1226
self.base = self.bzrdir.root_transport.base
1227
self._control_files = None
1228
self._lock_mode = None
1229
self._lock_token = None
1230
self._repo_lock_token = None
1231
self._lock_count = 0
1232
self._leave_lock = False
1235
return "%s(%s)" % (self.__class__.__name__, self.base)
1239
def _ensure_real(self):
1240
"""Ensure that there is a _real_branch set.
1242
Used before calls to self._real_branch.
1244
if not self._real_branch:
1245
assert vfs.vfs_enabled()
1246
self.bzrdir._ensure_real()
1247
self._real_branch = self.bzrdir._real_bzrdir.open_branch()
1248
# Give the remote repository the matching real repo.
1249
real_repo = self._real_branch.repository
1250
if isinstance(real_repo, RemoteRepository):
1251
real_repo._ensure_real()
1252
real_repo = real_repo._real_repository
1253
self.repository._set_real_repository(real_repo)
1254
# Give the branch the remote repository to let fast-pathing happen.
1255
self._real_branch.repository = self.repository
1256
# XXX: deal with _lock_mode == 'w'
1257
if self._lock_mode == 'r':
1258
self._real_branch.lock_read()
1261
def control_files(self):
1262
# Defer actually creating RemoteBranchLockableFiles until its needed,
1263
# because it triggers an _ensure_real that we otherwise might not need.
1264
if self._control_files is None:
1265
self._control_files = RemoteBranchLockableFiles(
1266
self.bzrdir, self._client)
1267
return self._control_files
1269
def _get_checkout_format(self):
1271
return self._real_branch._get_checkout_format()
1273
def get_physical_lock_status(self):
1274
"""See Branch.get_physical_lock_status()."""
1275
# should be an API call to the server, as branches must be lockable.
1277
return self._real_branch.get_physical_lock_status()
1279
def lock_read(self):
1280
if not self._lock_mode:
1281
self._lock_mode = 'r'
1282
self._lock_count = 1
1283
if self._real_branch is not None:
1284
self._real_branch.lock_read()
1286
self._lock_count += 1
1288
def _remote_lock_write(self, token):
1290
branch_token = repo_token = ''
1292
branch_token = token
1293
repo_token = self.repository.lock_write()
1294
self.repository.unlock()
1295
path = self.bzrdir._path_for_remote_call(self._client)
1296
response = self._client.call('Branch.lock_write', path, branch_token,
1298
if response[0] == 'ok':
1299
ok, branch_token, repo_token = response
1300
return branch_token, repo_token
1301
elif response[0] == 'LockContention':
1302
raise errors.LockContention('(remote lock)')
1303
elif response[0] == 'TokenMismatch':
1304
raise errors.TokenMismatch(token, '(remote token)')
1305
elif response[0] == 'UnlockableTransport':
1306
raise errors.UnlockableTransport(self.bzrdir.root_transport)
1307
elif response[0] == 'ReadOnlyError':
1308
raise errors.ReadOnlyError(self)
1309
elif response[0] == 'LockFailed':
1310
raise errors.LockFailed(response[1], response[2])
1312
raise errors.UnexpectedSmartServerResponse(response)
1314
def lock_write(self, token=None):
1315
if not self._lock_mode:
1316
remote_tokens = self._remote_lock_write(token)
1317
self._lock_token, self._repo_lock_token = remote_tokens
1318
assert self._lock_token, 'Remote server did not return a token!'
1319
# TODO: We really, really, really don't want to call _ensure_real
1320
# here, but it's the easiest way to ensure coherency between the
1321
# state of the RemoteBranch and RemoteRepository objects and the
1322
# physical locks. If we don't materialise the real objects here,
1323
# then getting everything in the right state later is complex, so
1324
# for now we just do it the lazy way.
1325
# -- Andrew Bennetts, 2007-02-22.
1327
if self._real_branch is not None:
1328
self._real_branch.repository.lock_write(
1329
token=self._repo_lock_token)
1331
self._real_branch.lock_write(token=self._lock_token)
1333
self._real_branch.repository.unlock()
1334
if token is not None:
1335
self._leave_lock = True
1337
# XXX: this case seems to be unreachable; token cannot be None.
1338
self._leave_lock = False
1339
self._lock_mode = 'w'
1340
self._lock_count = 1
1341
elif self._lock_mode == 'r':
1342
raise errors.ReadOnlyTransaction
1344
if token is not None:
1345
# A token was given to lock_write, and we're relocking, so check
1346
# that the given token actually matches the one we already have.
1347
if token != self._lock_token:
1348
raise errors.TokenMismatch(token, self._lock_token)
1349
self._lock_count += 1
1350
return self._lock_token or None
1352
def _unlock(self, branch_token, repo_token):
1353
path = self.bzrdir._path_for_remote_call(self._client)
1354
response = self._client.call('Branch.unlock', path, branch_token,
1356
if response == ('ok',):
1358
elif response[0] == 'TokenMismatch':
1359
raise errors.TokenMismatch(
1360
str((branch_token, repo_token)), '(remote tokens)')
1362
raise errors.UnexpectedSmartServerResponse(response)
1365
self._lock_count -= 1
1366
if not self._lock_count:
1367
self._clear_cached_state()
1368
mode = self._lock_mode
1369
self._lock_mode = None
1370
if self._real_branch is not None:
1371
if (not self._leave_lock and mode == 'w' and
1372
self._repo_lock_token):
1373
# If this RemoteBranch will remove the physical lock for the
1374
# repository, make sure the _real_branch doesn't do it
1375
# first. (Because the _real_branch's repository is set to
1376
# be the RemoteRepository.)
1377
self._real_branch.repository.leave_lock_in_place()
1378
self._real_branch.unlock()
1380
# Only write-locked branched need to make a remote method call
1381
# to perfom the unlock.
1383
assert self._lock_token, 'Locked, but no token!'
1384
branch_token = self._lock_token
1385
repo_token = self._repo_lock_token
1386
self._lock_token = None
1387
self._repo_lock_token = None
1388
if not self._leave_lock:
1389
self._unlock(branch_token, repo_token)
1391
def break_lock(self):
1393
return self._real_branch.break_lock()
1395
def leave_lock_in_place(self):
1396
if not self._lock_token:
1397
raise NotImplementedError(self.leave_lock_in_place)
1398
self._leave_lock = True
1400
def dont_leave_lock_in_place(self):
1401
if not self._lock_token:
1402
raise NotImplementedError(self.dont_leave_lock_in_place)
1403
self._leave_lock = False
1405
def last_revision_info(self):
1406
"""See Branch.last_revision_info()."""
1407
path = self.bzrdir._path_for_remote_call(self._client)
1408
response = self._client.call('Branch.last_revision_info', path)
1409
assert response[0] == 'ok', 'unexpected response code %s' % (response,)
1410
revno = int(response[1])
1411
last_revision = response[2]
1412
return (revno, last_revision)
1414
def _gen_revision_history(self):
1415
"""See Branch._gen_revision_history()."""
1416
path = self.bzrdir._path_for_remote_call(self._client)
1417
response = self._client.call_expecting_body(
1418
'Branch.revision_history', path)
1419
assert response[0][0] == 'ok', ('unexpected response code %s'
1421
result = response[1].read_body_bytes().split('\x00')
1427
def set_revision_history(self, rev_history):
1428
# Send just the tip revision of the history; the server will generate
1429
# the full history from that. If the revision doesn't exist in this
1430
# branch, NoSuchRevision will be raised.
1431
path = self.bzrdir._path_for_remote_call(self._client)
1432
if rev_history == []:
1435
rev_id = rev_history[-1]
1436
self._clear_cached_state()
1437
response = self._client.call('Branch.set_last_revision',
1438
path, self._lock_token, self._repo_lock_token, rev_id)
1439
if response[0] == 'NoSuchRevision':
1440
raise NoSuchRevision(self, rev_id)
1442
assert response == ('ok',), (
1443
'unexpected response code %r' % (response,))
1444
self._cache_revision_history(rev_history)
1446
def get_parent(self):
1448
return self._real_branch.get_parent()
1450
def set_parent(self, url):
1452
return self._real_branch.set_parent(url)
1454
def sprout(self, to_bzrdir, revision_id=None):
1455
# Like Branch.sprout, except that it sprouts a branch in the default
1456
# format, because RemoteBranches can't be created at arbitrary URLs.
1457
# XXX: if to_bzrdir is a RemoteBranch, this should perhaps do
1458
# to_bzrdir.create_branch...
1460
result = self._real_branch._format.initialize(to_bzrdir)
1461
self.copy_content_into(result, revision_id=revision_id)
1462
result.set_parent(self.bzrdir.root_transport.base)
1466
def pull(self, source, overwrite=False, stop_revision=None,
1468
# FIXME: This asks the real branch to run the hooks, which means
1469
# they're called with the wrong target branch parameter.
1470
# The test suite specifically allows this at present but it should be
1471
# fixed. It should get a _override_hook_target branch,
1472
# as push does. -- mbp 20070405
1474
self._real_branch.pull(
1475
source, overwrite=overwrite, stop_revision=stop_revision,
1479
def push(self, target, overwrite=False, stop_revision=None):
1481
return self._real_branch.push(
1482
target, overwrite=overwrite, stop_revision=stop_revision,
1483
_override_hook_source_branch=self)
1485
def is_locked(self):
1486
return self._lock_count >= 1
1489
def set_last_revision_info(self, revno, revision_id):
1490
assert type(revno) is int
1491
revision_id = ensure_null(revision_id)
1492
path = self.bzrdir._path_for_remote_call(self._client)
1494
response = self._client.call('Branch.set_last_revision_info',
1495
path, self._lock_token, self._repo_lock_token, str(revno), revision_id)
1496
except errors.UnknownSmartMethod:
1498
self._clear_cached_state()
1499
return self._real_branch.set_last_revision_info(revno, revision_id)
1500
if response == ('ok',):
1501
self._clear_cached_state()
1502
elif response[0] == 'NoSuchRevision':
1503
raise NoSuchRevision(self, response[1])
1505
raise errors.UnexpectedSmartServerResponse(response)
1507
def generate_revision_history(self, revision_id, last_rev=None,
1510
return self._real_branch.generate_revision_history(
1511
revision_id, last_rev=last_rev, other_branch=other_branch)
1516
return self._real_branch.tags
1518
def set_push_location(self, location):
1520
return self._real_branch.set_push_location(location)
1522
def update_revisions(self, other, stop_revision=None, overwrite=False):
1524
return self._real_branch.update_revisions(
1525
other, stop_revision=stop_revision, overwrite=overwrite)
1528
def _extract_tar(tar, to_dir):
1529
"""Extract all the contents of a tarfile object.
1531
A replacement for extractall, which is not present in python2.4
1534
tar.extract(tarinfo, to_dir)