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)
712
def get_transaction(self):
714
return self._real_repository.get_transaction()
717
def clone(self, a_bzrdir, revision_id=None):
719
return self._real_repository.clone(a_bzrdir, revision_id=revision_id)
721
def make_working_trees(self):
722
"""See Repository.make_working_trees"""
724
return self._real_repository.make_working_trees()
726
def revision_ids_to_search_result(self, result_set):
727
"""Convert a set of revision ids to a graph SearchResult."""
728
result_parents = set()
729
for parents in self.get_graph().get_parent_map(
730
result_set).itervalues():
731
result_parents.update(parents)
732
included_keys = result_set.intersection(result_parents)
733
start_keys = result_set.difference(included_keys)
734
exclude_keys = result_parents.difference(result_set)
735
result = graph.SearchResult(start_keys, exclude_keys,
736
len(result_set), result_set)
740
def search_missing_revision_ids(self, other, revision_id=None, find_ghosts=True):
741
"""Return the revision ids that other has that this does not.
743
These are returned in topological order.
745
revision_id: only return revision ids included by revision_id.
747
return repository.InterRepository.get(
748
other, self).search_missing_revision_ids(revision_id, find_ghosts)
750
def fetch(self, source, revision_id=None, pb=None):
751
if self.has_same_location(source):
752
# check that last_revision is in 'from' and then return a
754
if (revision_id is not None and
755
not revision.is_null(revision_id)):
756
self.get_revision(revision_id)
759
return self._real_repository.fetch(
760
source, revision_id=revision_id, pb=pb)
762
def create_bundle(self, target, base, fileobj, format=None):
764
self._real_repository.create_bundle(target, base, fileobj, format)
767
def get_ancestry(self, revision_id, topo_sorted=True):
769
return self._real_repository.get_ancestry(revision_id, topo_sorted)
772
def inventories(self):
774
return self._real_repository.inventories
776
def fileids_altered_by_revision_ids(self, revision_ids):
778
return self._real_repository.fileids_altered_by_revision_ids(revision_ids)
780
def _get_versioned_file_checker(self, revisions, revision_versions_cache):
782
return self._real_repository._get_versioned_file_checker(
783
revisions, revision_versions_cache)
785
def iter_files_bytes(self, desired_files):
786
"""See Repository.iter_file_bytes.
789
return self._real_repository.iter_files_bytes(desired_files)
791
def get_parent_map(self, keys):
792
"""See bzrlib.Graph.get_parent_map()."""
793
# Hack to build up the caching logic.
794
ancestry = self._parents_map
796
# Repository is not locked, so there's no cache.
797
missing_revisions = set(keys)
800
missing_revisions = set(key for key in keys if key not in ancestry)
801
if missing_revisions:
802
parent_map = self._get_parent_map(missing_revisions)
803
if 'hpss' in debug.debug_flags:
804
mutter('retransmitted revisions: %d of %d',
805
len(set(ancestry).intersection(parent_map)),
807
ancestry.update(parent_map)
808
present_keys = [k for k in keys if k in ancestry]
809
if 'hpss' in debug.debug_flags:
810
self._requested_parents.update(present_keys)
811
mutter('Current RemoteRepository graph hit rate: %d%%',
812
100.0 * len(self._requested_parents) / len(ancestry))
813
return dict((k, ancestry[k]) for k in present_keys)
815
def _get_parent_map(self, keys):
816
"""Helper for get_parent_map that performs the RPC."""
817
medium = self._client._medium
818
if not medium._remote_is_at_least_1_2:
819
# We already found out that the server can't understand
820
# Repository.get_parent_map requests, so just fetch the whole
822
# XXX: Note that this will issue a deprecation warning. This is ok
823
# :- its because we're working with a deprecated server anyway, and
824
# the user will almost certainly have seen a warning about the
825
# server version already.
826
rg = self.get_revision_graph()
827
# There is an api discrepency between get_parent_map and
828
# get_revision_graph. Specifically, a "key:()" pair in
829
# get_revision_graph just means a node has no parents. For
830
# "get_parent_map" it means the node is a ghost. So fix up the
831
# graph to correct this.
832
# https://bugs.launchpad.net/bzr/+bug/214894
833
# There is one other "bug" which is that ghosts in
834
# get_revision_graph() are not returned at all. But we won't worry
835
# about that for now.
836
for node_id, parent_ids in rg.iteritems():
838
rg[node_id] = (NULL_REVISION,)
839
rg[NULL_REVISION] = ()
843
if NULL_REVISION in keys:
844
keys.discard(NULL_REVISION)
845
found_parents = {NULL_REVISION:()}
850
# TODO(Needs analysis): We could assume that the keys being requested
851
# from get_parent_map are in a breadth first search, so typically they
852
# will all be depth N from some common parent, and we don't have to
853
# have the server iterate from the root parent, but rather from the
854
# keys we're searching; and just tell the server the keyspace we
855
# already have; but this may be more traffic again.
857
# Transform self._parents_map into a search request recipe.
858
# TODO: Manage this incrementally to avoid covering the same path
859
# repeatedly. (The server will have to on each request, but the less
860
# work done the better).
861
parents_map = self._parents_map
862
if parents_map is None:
863
# Repository is not locked, so there's no cache.
865
start_set = set(parents_map)
866
result_parents = set()
867
for parents in parents_map.itervalues():
868
result_parents.update(parents)
869
stop_keys = result_parents.difference(start_set)
870
included_keys = start_set.intersection(result_parents)
871
start_set.difference_update(included_keys)
872
recipe = (start_set, stop_keys, len(parents_map))
873
body = self._serialise_search_recipe(recipe)
874
path = self.bzrdir._path_for_remote_call(self._client)
876
if type(key) is not str:
878
"key %r not a plain string" % (key,))
879
verb = 'Repository.get_parent_map'
880
args = (path,) + tuple(keys)
882
response = self._client.call_with_body_bytes_expecting_body(
883
verb, args, self._serialise_search_recipe(recipe))
884
except errors.UnknownSmartMethod:
885
# Server does not support this method, so get the whole graph.
886
# Worse, we have to force a disconnection, because the server now
887
# doesn't realise it has a body on the wire to consume, so the
888
# only way to recover is to abandon the connection.
890
'Server is too old for fast get_parent_map, reconnecting. '
891
'(Upgrade the server to Bazaar 1.2 to avoid this)')
893
# To avoid having to disconnect repeatedly, we keep track of the
894
# fact the server doesn't understand remote methods added in 1.2.
895
medium._remote_is_at_least_1_2 = False
896
return self.get_revision_graph(None)
897
if response[0][0] not in ['ok']:
898
response[1].cancel_read_body()
899
raise errors.UnexpectedSmartServerResponse(response[0])
900
if response[0][0] == 'ok':
901
coded = bz2.decompress(response[1].read_body_bytes())
905
lines = coded.split('\n')
908
d = tuple(line.split())
910
revision_graph[d[0]] = d[1:]
912
# No parents - so give the Graph result (NULL_REVISION,).
913
revision_graph[d[0]] = (NULL_REVISION,)
914
return revision_graph
917
def get_signature_text(self, revision_id):
919
return self._real_repository.get_signature_text(revision_id)
922
@symbol_versioning.deprecated_method(symbol_versioning.one_three)
923
def get_revision_graph_with_ghosts(self, revision_ids=None):
925
return self._real_repository.get_revision_graph_with_ghosts(
926
revision_ids=revision_ids)
929
def get_inventory_xml(self, revision_id):
931
return self._real_repository.get_inventory_xml(revision_id)
933
def deserialise_inventory(self, revision_id, xml):
935
return self._real_repository.deserialise_inventory(revision_id, xml)
937
def reconcile(self, other=None, thorough=False):
939
return self._real_repository.reconcile(other=other, thorough=thorough)
941
def all_revision_ids(self):
943
return self._real_repository.all_revision_ids()
946
def get_deltas_for_revisions(self, revisions):
948
return self._real_repository.get_deltas_for_revisions(revisions)
951
def get_revision_delta(self, revision_id):
953
return self._real_repository.get_revision_delta(revision_id)
956
def revision_trees(self, revision_ids):
958
return self._real_repository.revision_trees(revision_ids)
961
def get_revision_reconcile(self, revision_id):
963
return self._real_repository.get_revision_reconcile(revision_id)
966
def check(self, revision_ids=None):
968
return self._real_repository.check(revision_ids=revision_ids)
970
def copy_content_into(self, destination, revision_id=None):
972
return self._real_repository.copy_content_into(
973
destination, revision_id=revision_id)
975
def _copy_repository_tarball(self, to_bzrdir, revision_id=None):
976
# get a tarball of the remote repository, and copy from that into the
978
from bzrlib import osutils
981
# TODO: Maybe a progress bar while streaming the tarball?
982
note("Copying repository content as tarball...")
983
tar_file = self._get_tarball('bz2')
986
destination = to_bzrdir.create_repository()
988
tar = tarfile.open('repository', fileobj=tar_file,
990
tmpdir = tempfile.mkdtemp()
992
_extract_tar(tar, tmpdir)
993
tmp_bzrdir = BzrDir.open(tmpdir)
994
tmp_repo = tmp_bzrdir.open_repository()
995
tmp_repo.copy_content_into(destination, revision_id)
997
osutils.rmtree(tmpdir)
1001
# TODO: Suggestion from john: using external tar is much faster than
1002
# python's tarfile library, but it may not work on windows.
1005
def inventories(self):
1006
"""Decorate the real repository for now.
1008
In the long term a full blown network facility is needed to
1009
avoid creating a real repository object locally.
1012
return self._real_repository.inventories
1016
"""Compress the data within the repository.
1018
This is not currently implemented within the smart server.
1021
return self._real_repository.pack()
1024
def revisions(self):
1025
"""Decorate the real repository for now.
1027
In the short term this should become a real object to intercept graph
1030
In the long term a full blown network facility is needed.
1033
return self._real_repository.revisions
1035
def set_make_working_trees(self, new_value):
1037
self._real_repository.set_make_working_trees(new_value)
1040
def signatures(self):
1041
"""Decorate the real repository for now.
1043
In the long term a full blown network facility is needed to avoid
1044
creating a real repository object locally.
1047
return self._real_repository.signatures
1050
def sign_revision(self, revision_id, gpg_strategy):
1052
return self._real_repository.sign_revision(revision_id, gpg_strategy)
1056
"""Decorate the real repository for now.
1058
In the long term a full blown network facility is needed to avoid
1059
creating a real repository object locally.
1062
return self._real_repository.texts
1065
def get_revisions(self, revision_ids):
1067
return self._real_repository.get_revisions(revision_ids)
1069
def supports_rich_root(self):
1071
return self._real_repository.supports_rich_root()
1073
def iter_reverse_revision_history(self, revision_id):
1075
return self._real_repository.iter_reverse_revision_history(revision_id)
1078
def _serializer(self):
1080
return self._real_repository._serializer
1082
def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
1084
return self._real_repository.store_revision_signature(
1085
gpg_strategy, plaintext, revision_id)
1087
def add_signature_text(self, revision_id, signature):
1089
return self._real_repository.add_signature_text(revision_id, signature)
1091
def has_signature_for_revision_id(self, revision_id):
1093
return self._real_repository.has_signature_for_revision_id(revision_id)
1095
def item_keys_introduced_by(self, revision_ids, _files_pb=None):
1097
return self._real_repository.item_keys_introduced_by(revision_ids,
1098
_files_pb=_files_pb)
1100
def revision_graph_can_have_wrong_parents(self):
1101
# The answer depends on the remote repo format.
1103
return self._real_repository.revision_graph_can_have_wrong_parents()
1105
def _find_inconsistent_revision_parents(self):
1107
return self._real_repository._find_inconsistent_revision_parents()
1109
def _check_for_inconsistent_revision_parents(self):
1111
return self._real_repository._check_for_inconsistent_revision_parents()
1113
def _make_parents_provider(self):
1116
def _serialise_search_recipe(self, recipe):
1117
"""Serialise a graph search recipe.
1119
:param recipe: A search recipe (start, stop, count).
1120
:return: Serialised bytes.
1122
start_keys = ' '.join(recipe[0])
1123
stop_keys = ' '.join(recipe[1])
1124
count = str(recipe[2])
1125
return '\n'.join((start_keys, stop_keys, count))
1128
class RemoteBranchLockableFiles(LockableFiles):
1129
"""A 'LockableFiles' implementation that talks to a smart server.
1131
This is not a public interface class.
1134
def __init__(self, bzrdir, _client):
1135
self.bzrdir = bzrdir
1136
self._client = _client
1137
self._need_find_modes = True
1138
LockableFiles.__init__(
1139
self, bzrdir.get_branch_transport(None),
1140
'lock', lockdir.LockDir)
1142
def _find_modes(self):
1143
# RemoteBranches don't let the client set the mode of control files.
1144
self._dir_mode = None
1145
self._file_mode = None
1148
class RemoteBranchFormat(branch.BranchFormat):
1150
def __eq__(self, other):
1151
return (isinstance(other, RemoteBranchFormat) and
1152
self.__dict__ == other.__dict__)
1154
def get_format_description(self):
1155
return 'Remote BZR Branch'
1157
def get_format_string(self):
1158
return 'Remote BZR Branch'
1160
def open(self, a_bzrdir):
1161
return a_bzrdir.open_branch()
1163
def initialize(self, a_bzrdir):
1164
return a_bzrdir.create_branch()
1166
def supports_tags(self):
1167
# Remote branches might support tags, but we won't know until we
1168
# access the real remote branch.
1172
class RemoteBranch(branch.Branch):
1173
"""Branch stored on a server accessed by HPSS RPC.
1175
At the moment most operations are mapped down to simple file operations.
1178
def __init__(self, remote_bzrdir, remote_repository, real_branch=None,
1180
"""Create a RemoteBranch instance.
1182
:param real_branch: An optional local implementation of the branch
1183
format, usually accessing the data via the VFS.
1184
:param _client: Private parameter for testing.
1186
# We intentionally don't call the parent class's __init__, because it
1187
# will try to assign to self.tags, which is a property in this subclass.
1188
# And the parent's __init__ doesn't do much anyway.
1189
self._revision_id_to_revno_cache = None
1190
self._revision_history_cache = None
1191
self.bzrdir = remote_bzrdir
1192
if _client is not None:
1193
self._client = _client
1195
self._client = remote_bzrdir._client
1196
self.repository = remote_repository
1197
if real_branch is not None:
1198
self._real_branch = real_branch
1199
# Give the remote repository the matching real repo.
1200
real_repo = self._real_branch.repository
1201
if isinstance(real_repo, RemoteRepository):
1202
real_repo._ensure_real()
1203
real_repo = real_repo._real_repository
1204
self.repository._set_real_repository(real_repo)
1205
# Give the branch the remote repository to let fast-pathing happen.
1206
self._real_branch.repository = self.repository
1208
self._real_branch = None
1209
# Fill out expected attributes of branch for bzrlib api users.
1210
self._format = RemoteBranchFormat()
1211
self.base = self.bzrdir.root_transport.base
1212
self._control_files = None
1213
self._lock_mode = None
1214
self._lock_token = None
1215
self._repo_lock_token = None
1216
self._lock_count = 0
1217
self._leave_lock = False
1220
return "%s(%s)" % (self.__class__.__name__, self.base)
1224
def _ensure_real(self):
1225
"""Ensure that there is a _real_branch set.
1227
Used before calls to self._real_branch.
1229
if not self._real_branch:
1230
if not vfs.vfs_enabled():
1231
raise AssertionError('smart server vfs must be enabled '
1232
'to use vfs implementation')
1233
self.bzrdir._ensure_real()
1234
self._real_branch = self.bzrdir._real_bzrdir.open_branch()
1235
# Give the remote repository the matching real repo.
1236
real_repo = self._real_branch.repository
1237
if isinstance(real_repo, RemoteRepository):
1238
real_repo._ensure_real()
1239
real_repo = real_repo._real_repository
1240
self.repository._set_real_repository(real_repo)
1241
# Give the branch the remote repository to let fast-pathing happen.
1242
self._real_branch.repository = self.repository
1243
# XXX: deal with _lock_mode == 'w'
1244
if self._lock_mode == 'r':
1245
self._real_branch.lock_read()
1248
def control_files(self):
1249
# Defer actually creating RemoteBranchLockableFiles until its needed,
1250
# because it triggers an _ensure_real that we otherwise might not need.
1251
if self._control_files is None:
1252
self._control_files = RemoteBranchLockableFiles(
1253
self.bzrdir, self._client)
1254
return self._control_files
1256
def _get_checkout_format(self):
1258
return self._real_branch._get_checkout_format()
1260
def get_physical_lock_status(self):
1261
"""See Branch.get_physical_lock_status()."""
1262
# should be an API call to the server, as branches must be lockable.
1264
return self._real_branch.get_physical_lock_status()
1266
def lock_read(self):
1267
if not self._lock_mode:
1268
self._lock_mode = 'r'
1269
self._lock_count = 1
1270
if self._real_branch is not None:
1271
self._real_branch.lock_read()
1273
self._lock_count += 1
1275
def _remote_lock_write(self, token):
1277
branch_token = repo_token = ''
1279
branch_token = token
1280
repo_token = self.repository.lock_write()
1281
self.repository.unlock()
1282
path = self.bzrdir._path_for_remote_call(self._client)
1283
response = self._client.call('Branch.lock_write', path, branch_token,
1285
if response[0] == 'ok':
1286
ok, branch_token, repo_token = response
1287
return branch_token, repo_token
1288
elif response[0] == 'LockContention':
1289
raise errors.LockContention('(remote lock)')
1290
elif response[0] == 'TokenMismatch':
1291
raise errors.TokenMismatch(token, '(remote token)')
1292
elif response[0] == 'UnlockableTransport':
1293
raise errors.UnlockableTransport(self.bzrdir.root_transport)
1294
elif response[0] == 'ReadOnlyError':
1295
raise errors.ReadOnlyError(self)
1296
elif response[0] == 'LockFailed':
1297
raise errors.LockFailed(response[1], response[2])
1299
raise errors.UnexpectedSmartServerResponse(response)
1301
def lock_write(self, token=None):
1302
if not self._lock_mode:
1303
remote_tokens = self._remote_lock_write(token)
1304
self._lock_token, self._repo_lock_token = remote_tokens
1305
if not self._lock_token:
1306
raise SmartProtocolError('Remote server did not return a token!')
1307
# TODO: We really, really, really don't want to call _ensure_real
1308
# here, but it's the easiest way to ensure coherency between the
1309
# state of the RemoteBranch and RemoteRepository objects and the
1310
# physical locks. If we don't materialise the real objects here,
1311
# then getting everything in the right state later is complex, so
1312
# for now we just do it the lazy way.
1313
# -- Andrew Bennetts, 2007-02-22.
1315
if self._real_branch is not None:
1316
self._real_branch.repository.lock_write(
1317
token=self._repo_lock_token)
1319
self._real_branch.lock_write(token=self._lock_token)
1321
self._real_branch.repository.unlock()
1322
if token is not None:
1323
self._leave_lock = True
1325
# XXX: this case seems to be unreachable; token cannot be None.
1326
self._leave_lock = False
1327
self._lock_mode = 'w'
1328
self._lock_count = 1
1329
elif self._lock_mode == 'r':
1330
raise errors.ReadOnlyTransaction
1332
if token is not None:
1333
# A token was given to lock_write, and we're relocking, so check
1334
# that the given token actually matches the one we already have.
1335
if token != self._lock_token:
1336
raise errors.TokenMismatch(token, self._lock_token)
1337
self._lock_count += 1
1338
return self._lock_token or None
1340
def _unlock(self, branch_token, repo_token):
1341
path = self.bzrdir._path_for_remote_call(self._client)
1342
response = self._client.call('Branch.unlock', path, branch_token,
1344
if response == ('ok',):
1346
elif response[0] == 'TokenMismatch':
1347
raise errors.TokenMismatch(
1348
str((branch_token, repo_token)), '(remote tokens)')
1350
raise errors.UnexpectedSmartServerResponse(response)
1353
self._lock_count -= 1
1354
if not self._lock_count:
1355
self._clear_cached_state()
1356
mode = self._lock_mode
1357
self._lock_mode = None
1358
if self._real_branch is not None:
1359
if (not self._leave_lock and mode == 'w' and
1360
self._repo_lock_token):
1361
# If this RemoteBranch will remove the physical lock for the
1362
# repository, make sure the _real_branch doesn't do it
1363
# first. (Because the _real_branch's repository is set to
1364
# be the RemoteRepository.)
1365
self._real_branch.repository.leave_lock_in_place()
1366
self._real_branch.unlock()
1368
# Only write-locked branched need to make a remote method call
1369
# to perfom the unlock.
1371
if not self._lock_token:
1372
raise AssertionError('Locked, but no token!')
1373
branch_token = self._lock_token
1374
repo_token = self._repo_lock_token
1375
self._lock_token = None
1376
self._repo_lock_token = None
1377
if not self._leave_lock:
1378
self._unlock(branch_token, repo_token)
1380
def break_lock(self):
1382
return self._real_branch.break_lock()
1384
def leave_lock_in_place(self):
1385
if not self._lock_token:
1386
raise NotImplementedError(self.leave_lock_in_place)
1387
self._leave_lock = True
1389
def dont_leave_lock_in_place(self):
1390
if not self._lock_token:
1391
raise NotImplementedError(self.dont_leave_lock_in_place)
1392
self._leave_lock = False
1394
def last_revision_info(self):
1395
"""See Branch.last_revision_info()."""
1396
path = self.bzrdir._path_for_remote_call(self._client)
1397
response = self._client.call('Branch.last_revision_info', path)
1398
if response[0] != 'ok':
1399
raise SmartProtocolError('unexpected response code %s' % (response,))
1400
revno = int(response[1])
1401
last_revision = response[2]
1402
return (revno, last_revision)
1404
def _gen_revision_history(self):
1405
"""See Branch._gen_revision_history()."""
1406
path = self.bzrdir._path_for_remote_call(self._client)
1407
response = self._client.call_expecting_body(
1408
'Branch.revision_history', path)
1409
if response[0][0] != 'ok':
1410
raise SmartProtocolError('unexpected response code %s' % (response,))
1411
result = response[1].read_body_bytes().split('\x00')
1417
def set_revision_history(self, rev_history):
1418
# Send just the tip revision of the history; the server will generate
1419
# the full history from that. If the revision doesn't exist in this
1420
# branch, NoSuchRevision will be raised.
1421
path = self.bzrdir._path_for_remote_call(self._client)
1422
if rev_history == []:
1425
rev_id = rev_history[-1]
1426
self._clear_cached_state()
1427
response = self._client.call('Branch.set_last_revision',
1428
path, self._lock_token, self._repo_lock_token, rev_id)
1429
if response[0] == 'NoSuchRevision':
1430
raise NoSuchRevision(self, rev_id)
1431
elif response[0] != 'ok':
1432
raise SmartProtocolError('unexpected response code %s' % (response,))
1433
self._cache_revision_history(rev_history)
1435
def get_parent(self):
1437
return self._real_branch.get_parent()
1439
def set_parent(self, url):
1441
return self._real_branch.set_parent(url)
1443
def sprout(self, to_bzrdir, revision_id=None):
1444
# Like Branch.sprout, except that it sprouts a branch in the default
1445
# format, because RemoteBranches can't be created at arbitrary URLs.
1446
# XXX: if to_bzrdir is a RemoteBranch, this should perhaps do
1447
# to_bzrdir.create_branch...
1449
result = self._real_branch._format.initialize(to_bzrdir)
1450
self.copy_content_into(result, revision_id=revision_id)
1451
result.set_parent(self.bzrdir.root_transport.base)
1455
def pull(self, source, overwrite=False, stop_revision=None,
1457
# FIXME: This asks the real branch to run the hooks, which means
1458
# they're called with the wrong target branch parameter.
1459
# The test suite specifically allows this at present but it should be
1460
# fixed. It should get a _override_hook_target branch,
1461
# as push does. -- mbp 20070405
1463
self._real_branch.pull(
1464
source, overwrite=overwrite, stop_revision=stop_revision,
1468
def push(self, target, overwrite=False, stop_revision=None):
1470
return self._real_branch.push(
1471
target, overwrite=overwrite, stop_revision=stop_revision,
1472
_override_hook_source_branch=self)
1474
def is_locked(self):
1475
return self._lock_count >= 1
1478
def set_last_revision_info(self, revno, revision_id):
1479
revision_id = ensure_null(revision_id)
1480
path = self.bzrdir._path_for_remote_call(self._client)
1482
response = self._client.call('Branch.set_last_revision_info',
1483
path, self._lock_token, self._repo_lock_token, str(revno), revision_id)
1484
except errors.UnknownSmartMethod:
1486
self._clear_cached_state()
1487
return self._real_branch.set_last_revision_info(revno, revision_id)
1488
if response == ('ok',):
1489
self._clear_cached_state()
1490
elif response[0] == 'NoSuchRevision':
1491
raise NoSuchRevision(self, response[1])
1493
raise errors.UnexpectedSmartServerResponse(response)
1495
def generate_revision_history(self, revision_id, last_rev=None,
1498
return self._real_branch.generate_revision_history(
1499
revision_id, last_rev=last_rev, other_branch=other_branch)
1504
return self._real_branch.tags
1506
def set_push_location(self, location):
1508
return self._real_branch.set_push_location(location)
1510
def update_revisions(self, other, stop_revision=None, overwrite=False):
1512
return self._real_branch.update_revisions(
1513
other, stop_revision=stop_revision, overwrite=overwrite)
1516
def _extract_tar(tar, to_dir):
1517
"""Extract all the contents of a tarfile object.
1519
A replacement for extractall, which is not present in python2.4
1522
tar.extract(tarinfo, to_dir)