1
# Copyright (C) 2006, 2007 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
# TODO: At some point, handle upgrades by just passing the whole request
18
# across to run on the server.
21
from cStringIO import StringIO
32
from bzrlib.branch import BranchReferenceFormat
33
from bzrlib.bzrdir import BzrDir, RemoteBzrDirFormat
34
from bzrlib.config import BranchConfig, TreeConfig
35
from bzrlib.decorators import needs_read_lock, needs_write_lock
36
from bzrlib.errors import NoSuchRevision
37
from bzrlib.lockable_files import LockableFiles
38
from bzrlib.pack import ContainerPushParser
39
from bzrlib.smart import client, vfs
40
from bzrlib.symbol_versioning import (
44
from bzrlib.revision import NULL_REVISION
45
from bzrlib.trace import mutter, note, warning
47
# Note: RemoteBzrDirFormat is in bzrdir.py
49
class RemoteBzrDir(BzrDir):
50
"""Control directory on a remote server, accessed via bzr:// or similar."""
52
def __init__(self, transport, _client=None):
53
"""Construct a RemoteBzrDir.
55
:param _client: Private parameter for testing. Disables probing and the
58
BzrDir.__init__(self, transport, RemoteBzrDirFormat())
59
# this object holds a delegated bzrdir that uses file-level operations
60
# to talk to the other side
61
self._real_bzrdir = None
64
self._shared_medium = transport.get_shared_medium()
65
self._client = client._SmartClient(self._shared_medium)
67
self._client = _client
68
self._shared_medium = None
71
path = self._path_for_remote_call(self._client)
72
response = self._client.call('BzrDir.open', path)
73
if response not in [('yes',), ('no',)]:
74
raise errors.UnexpectedSmartServerResponse(response)
75
if response == ('no',):
76
raise errors.NotBranchError(path=transport.base)
78
def _ensure_real(self):
79
"""Ensure that there is a _real_bzrdir set.
81
Used before calls to self._real_bzrdir.
83
if not self._real_bzrdir:
84
self._real_bzrdir = BzrDir.open_from_transport(
85
self.root_transport, _server_formats=False)
87
def create_repository(self, shared=False):
89
self._real_bzrdir.create_repository(shared=shared)
90
return self.open_repository()
92
def destroy_repository(self):
93
"""See BzrDir.destroy_repository"""
95
self._real_bzrdir.destroy_repository()
97
def create_branch(self):
99
real_branch = self._real_bzrdir.create_branch()
100
return RemoteBranch(self, self.find_repository(), real_branch)
102
def destroy_branch(self):
103
"""See BzrDir.destroy_branch"""
105
self._real_bzrdir.destroy_branch()
107
def create_workingtree(self, revision_id=None, from_branch=None):
108
raise errors.NotLocalUrl(self.transport.base)
110
def find_branch_format(self):
111
"""Find the branch 'format' for this bzrdir.
113
This might be a synthetic object for e.g. RemoteBranch and SVN.
115
b = self.open_branch()
118
def get_branch_reference(self):
119
"""See BzrDir.get_branch_reference()."""
120
path = self._path_for_remote_call(self._client)
121
response = self._client.call('BzrDir.open_branch', path)
122
if response[0] == 'ok':
123
if response[1] == '':
124
# branch at this location.
127
# a branch reference, use the existing BranchReference logic.
129
elif response == ('nobranch',):
130
raise errors.NotBranchError(path=self.root_transport.base)
132
raise errors.UnexpectedSmartServerResponse(response)
134
def _get_tree_branch(self):
135
"""See BzrDir._get_tree_branch()."""
136
return None, self.open_branch()
138
def open_branch(self, _unsupported=False):
139
assert _unsupported == False, 'unsupported flag support not implemented yet.'
140
reference_url = self.get_branch_reference()
141
if reference_url is None:
142
# branch at this location.
143
return RemoteBranch(self, self.find_repository())
145
# a branch reference, use the existing BranchReference logic.
146
format = BranchReferenceFormat()
147
return format.open(self, _found=True, location=reference_url)
149
def open_repository(self):
150
path = self._path_for_remote_call(self._client)
151
response = self._client.call('BzrDir.find_repository', path)
152
assert response[0] in ('ok', 'norepository'), \
153
'unexpected response code %s' % (response,)
154
if response[0] == 'norepository':
155
raise errors.NoRepositoryPresent(self)
156
assert len(response) == 4, 'incorrect response length %s' % (response,)
157
if response[1] == '':
158
format = RemoteRepositoryFormat()
159
format.rich_root_data = (response[2] == 'yes')
160
format.supports_tree_reference = (response[3] == 'yes')
161
return RemoteRepository(self, format)
163
raise errors.NoRepositoryPresent(self)
165
def open_workingtree(self, recommend_upgrade=True):
167
if self._real_bzrdir.has_workingtree():
168
raise errors.NotLocalUrl(self.root_transport)
170
raise errors.NoWorkingTree(self.root_transport.base)
172
def _path_for_remote_call(self, client):
173
"""Return the path to be used for this bzrdir in a remote call."""
174
return client.remote_path_from_transport(self.root_transport)
176
def get_branch_transport(self, branch_format):
178
return self._real_bzrdir.get_branch_transport(branch_format)
180
def get_repository_transport(self, repository_format):
182
return self._real_bzrdir.get_repository_transport(repository_format)
184
def get_workingtree_transport(self, workingtree_format):
186
return self._real_bzrdir.get_workingtree_transport(workingtree_format)
188
def can_convert_format(self):
189
"""Upgrading of remote bzrdirs is not supported yet."""
192
def needs_format_conversion(self, format=None):
193
"""Upgrading of remote bzrdirs is not supported yet."""
196
def clone(self, url, revision_id=None, force_new_repo=False):
198
return self._real_bzrdir.clone(url, revision_id=revision_id,
199
force_new_repo=force_new_repo)
202
class RemoteRepositoryFormat(repository.RepositoryFormat):
203
"""Format for repositories accessed over a _SmartClient.
205
Instances of this repository are represented by RemoteRepository
208
The RemoteRepositoryFormat is parameterized during construction
209
to reflect the capabilities of the real, remote format. Specifically
210
the attributes rich_root_data and supports_tree_reference are set
211
on a per instance basis, and are not set (and should not be) at
215
_matchingbzrdir = RemoteBzrDirFormat
217
def initialize(self, a_bzrdir, shared=False):
218
assert isinstance(a_bzrdir, RemoteBzrDir), \
219
'%r is not a RemoteBzrDir' % (a_bzrdir,)
220
return a_bzrdir.create_repository(shared=shared)
222
def open(self, a_bzrdir):
223
assert isinstance(a_bzrdir, RemoteBzrDir)
224
return a_bzrdir.open_repository()
226
def get_format_description(self):
227
return 'bzr remote repository'
229
def __eq__(self, other):
230
return self.__class__ == other.__class__
232
def check_conversion_target(self, target_format):
233
if self.rich_root_data and not target_format.rich_root_data:
234
raise errors.BadConversionTarget(
235
'Does not support rich root data.', target_format)
236
if (self.supports_tree_reference and
237
not getattr(target_format, 'supports_tree_reference', False)):
238
raise errors.BadConversionTarget(
239
'Does not support nested trees', target_format)
242
class RemoteRepository(object):
243
"""Repository accessed over rpc.
245
For the moment most operations are performed using local transport-backed
249
def __init__(self, remote_bzrdir, format, real_repository=None, _client=None):
250
"""Create a RemoteRepository instance.
252
:param remote_bzrdir: The bzrdir hosting this repository.
253
:param format: The RemoteFormat object to use.
254
:param real_repository: If not None, a local implementation of the
255
repository logic for the repository, usually accessing the data
257
:param _client: Private testing parameter - override the smart client
258
to be used by the repository.
261
self._real_repository = real_repository
263
self._real_repository = None
264
self.bzrdir = remote_bzrdir
266
self._client = client._SmartClient(self.bzrdir._shared_medium)
268
self._client = _client
269
self._format = format
270
self._lock_mode = None
271
self._lock_token = None
273
self._leave_lock = False
274
# A cache of looked up revision parent data; reset at unlock time.
275
self._parents_map = None
276
if 'hpss' in debug.debug_flags:
277
self._requested_parents = None
279
# These depend on the actual remote format, so force them off for
280
# maximum compatibility. XXX: In future these should depend on the
281
# remote repository instance, but this is irrelevant until we perform
282
# reconcile via an RPC call.
283
self._reconcile_does_inventory_gc = False
284
self._reconcile_fixes_text_parents = False
285
self._reconcile_backsup_inventory = False
286
self.base = self.bzrdir.transport.base
289
return "%s(%s)" % (self.__class__.__name__, self.base)
293
def abort_write_group(self):
294
"""Complete a write group on the decorated repository.
296
Smart methods peform operations in a single step so this api
297
is not really applicable except as a compatibility thunk
298
for older plugins that don't use e.g. the CommitBuilder
302
return self._real_repository.abort_write_group()
304
def commit_write_group(self):
305
"""Complete a write group on the decorated repository.
307
Smart methods peform operations in a single step so this api
308
is not really applicable except as a compatibility thunk
309
for older plugins that don't use e.g. the CommitBuilder
313
return self._real_repository.commit_write_group()
315
def _ensure_real(self):
316
"""Ensure that there is a _real_repository set.
318
Used before calls to self._real_repository.
320
if not self._real_repository:
321
self.bzrdir._ensure_real()
322
#self._real_repository = self.bzrdir._real_bzrdir.open_repository()
323
self._set_real_repository(self.bzrdir._real_bzrdir.open_repository())
325
def find_text_key_references(self):
326
"""Find the text key references within the repository.
328
:return: a dictionary mapping (file_id, revision_id) tuples to altered file-ids to an iterable of
329
revision_ids. Each altered file-ids has the exact revision_ids that
330
altered it listed explicitly.
331
:return: A dictionary mapping text keys ((fileid, revision_id) tuples)
332
to whether they were referred to by the inventory of the
333
revision_id that they contain. The inventory texts from all present
334
revision ids are assessed to generate this report.
337
return self._real_repository.find_text_key_references()
339
def _generate_text_key_index(self):
340
"""Generate a new text key index for the repository.
342
This is an expensive function that will take considerable time to run.
344
:return: A dict mapping (file_id, revision_id) tuples to a list of
345
parents, also (file_id, revision_id) tuples.
348
return self._real_repository._generate_text_key_index()
350
def get_revision_graph(self, revision_id=None):
351
"""See Repository.get_revision_graph()."""
352
if revision_id is None:
354
elif revision.is_null(revision_id):
357
path = self.bzrdir._path_for_remote_call(self._client)
358
assert type(revision_id) is str
359
response = self._client.call_expecting_body(
360
'Repository.get_revision_graph', path, revision_id)
361
if response[0][0] not in ['ok', 'nosuchrevision']:
362
raise errors.UnexpectedSmartServerResponse(response[0])
363
if response[0][0] == 'ok':
364
coded = response[1].read_body_bytes()
366
# no revisions in this repository!
368
lines = coded.split('\n')
371
d = tuple(line.split())
372
revision_graph[d[0]] = d[1:]
374
return revision_graph
376
response_body = response[1].read_body_bytes()
377
assert response_body == ''
378
raise NoSuchRevision(self, revision_id)
380
def has_revision(self, revision_id):
381
"""See Repository.has_revision()."""
382
if revision_id == NULL_REVISION:
383
# The null revision is always present.
385
path = self.bzrdir._path_for_remote_call(self._client)
386
response = self._client.call('Repository.has_revision', path, revision_id)
387
assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
388
return response[0] == 'yes'
390
def has_revisions(self, revision_ids):
391
"""See Repository.has_revisions()."""
393
for revision_id in revision_ids:
394
if self.has_revision(revision_id):
395
result.add(revision_id)
398
def has_same_location(self, other):
399
return (self.__class__ == other.__class__ and
400
self.bzrdir.transport.base == other.bzrdir.transport.base)
402
def get_graph(self, other_repository=None):
403
"""Return the graph for this repository format"""
404
parents_provider = self
405
if (other_repository is not None and
406
other_repository.bzrdir.transport.base !=
407
self.bzrdir.transport.base):
408
parents_provider = graph._StackedParentsProvider(
409
[parents_provider, other_repository._make_parents_provider()])
410
return graph.Graph(parents_provider)
412
def gather_stats(self, revid=None, committers=None):
413
"""See Repository.gather_stats()."""
414
path = self.bzrdir._path_for_remote_call(self._client)
415
# revid can be None to indicate no revisions, not just NULL_REVISION
416
if revid is None or revision.is_null(revid):
420
if committers is None or not committers:
421
fmt_committers = 'no'
423
fmt_committers = 'yes'
424
response = self._client.call_expecting_body(
425
'Repository.gather_stats', path, fmt_revid, fmt_committers)
426
assert response[0][0] == 'ok', \
427
'unexpected response code %s' % (response[0],)
429
body = response[1].read_body_bytes()
431
for line in body.split('\n'):
434
key, val_text = line.split(':')
435
if key in ('revisions', 'size', 'committers'):
436
result[key] = int(val_text)
437
elif key in ('firstrev', 'latestrev'):
438
values = val_text.split(' ')[1:]
439
result[key] = (float(values[0]), long(values[1]))
443
def find_branches(self, using=False):
444
"""See Repository.find_branches()."""
445
# should be an API call to the server.
447
return self._real_repository.find_branches(using=using)
449
def get_physical_lock_status(self):
450
"""See Repository.get_physical_lock_status()."""
451
# should be an API call to the server.
453
return self._real_repository.get_physical_lock_status()
455
def is_in_write_group(self):
456
"""Return True if there is an open write group.
458
write groups are only applicable locally for the smart server..
460
if self._real_repository:
461
return self._real_repository.is_in_write_group()
464
return self._lock_count >= 1
467
"""See Repository.is_shared()."""
468
path = self.bzrdir._path_for_remote_call(self._client)
469
response = self._client.call('Repository.is_shared', path)
470
assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
471
return response[0] == 'yes'
473
def is_write_locked(self):
474
return self._lock_mode == 'w'
477
# wrong eventually - want a local lock cache context
478
if not self._lock_mode:
479
self._lock_mode = 'r'
481
self._parents_map = {}
482
if 'hpss' in debug.debug_flags:
483
self._requested_parents = set()
484
if self._real_repository is not None:
485
self._real_repository.lock_read()
487
self._lock_count += 1
489
def _remote_lock_write(self, token):
490
path = self.bzrdir._path_for_remote_call(self._client)
493
response = self._client.call('Repository.lock_write', path, token)
494
if response[0] == 'ok':
497
elif response[0] == 'LockContention':
498
raise errors.LockContention('(remote lock)')
499
elif response[0] == 'UnlockableTransport':
500
raise errors.UnlockableTransport(self.bzrdir.root_transport)
501
elif response[0] == 'LockFailed':
502
raise errors.LockFailed(response[1], response[2])
504
raise errors.UnexpectedSmartServerResponse(response)
506
def lock_write(self, token=None):
507
if not self._lock_mode:
508
self._lock_token = self._remote_lock_write(token)
509
# if self._lock_token is None, then this is something like packs or
510
# svn where we don't get to lock the repo, or a weave style repository
511
# where we cannot lock it over the wire and attempts to do so will
513
if self._real_repository is not None:
514
self._real_repository.lock_write(token=self._lock_token)
515
if token is not None:
516
self._leave_lock = True
518
self._leave_lock = False
519
self._lock_mode = 'w'
521
self._parents_map = {}
522
if 'hpss' in debug.debug_flags:
523
self._requested_parents = set()
524
elif self._lock_mode == 'r':
525
raise errors.ReadOnlyError(self)
527
self._lock_count += 1
528
return self._lock_token or None
530
def leave_lock_in_place(self):
531
if not self._lock_token:
532
raise NotImplementedError(self.leave_lock_in_place)
533
self._leave_lock = True
535
def dont_leave_lock_in_place(self):
536
if not self._lock_token:
537
raise NotImplementedError(self.dont_leave_lock_in_place)
538
self._leave_lock = False
540
def _set_real_repository(self, repository):
541
"""Set the _real_repository for this repository.
543
:param repository: The repository to fallback to for non-hpss
544
implemented operations.
546
assert not isinstance(repository, RemoteRepository)
547
self._real_repository = repository
548
if self._lock_mode == 'w':
549
# if we are already locked, the real repository must be able to
550
# acquire the lock with our token.
551
self._real_repository.lock_write(self._lock_token)
552
elif self._lock_mode == 'r':
553
self._real_repository.lock_read()
555
def start_write_group(self):
556
"""Start a write group on the decorated repository.
558
Smart methods peform operations in a single step so this api
559
is not really applicable except as a compatibility thunk
560
for older plugins that don't use e.g. the CommitBuilder
564
return self._real_repository.start_write_group()
566
def _unlock(self, token):
567
path = self.bzrdir._path_for_remote_call(self._client)
569
# with no token the remote repository is not persistently locked.
571
response = self._client.call('Repository.unlock', path, token)
572
if response == ('ok',):
574
elif response[0] == 'TokenMismatch':
575
raise errors.TokenMismatch(token, '(remote token)')
577
raise errors.UnexpectedSmartServerResponse(response)
580
self._lock_count -= 1
581
if self._lock_count > 0:
583
self._parents_map = None
584
if 'hpss' in debug.debug_flags:
585
self._requested_parents = None
586
old_mode = self._lock_mode
587
self._lock_mode = None
589
# The real repository is responsible at present for raising an
590
# exception if it's in an unfinished write group. However, it
591
# normally will *not* actually remove the lock from disk - that's
592
# done by the server on receiving the Repository.unlock call.
593
# This is just to let the _real_repository stay up to date.
594
if self._real_repository is not None:
595
self._real_repository.unlock()
597
# The rpc-level lock should be released even if there was a
598
# problem releasing the vfs-based lock.
600
# Only write-locked repositories need to make a remote method
601
# call to perfom the unlock.
602
old_token = self._lock_token
603
self._lock_token = None
604
if not self._leave_lock:
605
self._unlock(old_token)
607
def break_lock(self):
608
# should hand off to the network
610
return self._real_repository.break_lock()
612
def _get_tarball(self, compression):
613
"""Return a TemporaryFile containing a repository tarball.
615
Returns None if the server does not support sending tarballs.
618
path = self.bzrdir._path_for_remote_call(self._client)
619
response, protocol = self._client.call_expecting_body(
620
'Repository.tarball', path, compression)
621
if response[0] == 'ok':
622
# Extract the tarball and return it
623
t = tempfile.NamedTemporaryFile()
624
# TODO: rpc layer should read directly into it...
625
t.write(protocol.read_body_bytes())
628
if (response == ('error', "Generic bzr smart protocol error: "
629
"bad request 'Repository.tarball'") or
630
response == ('error', "Generic bzr smart protocol error: "
631
"bad request u'Repository.tarball'")):
632
protocol.cancel_read_body()
634
raise errors.UnexpectedSmartServerResponse(response)
636
def sprout(self, to_bzrdir, revision_id=None):
637
# TODO: Option to control what format is created?
639
dest_repo = self._real_repository._format.initialize(to_bzrdir,
641
dest_repo.fetch(self, revision_id=revision_id)
644
### These methods are just thin shims to the VFS object for now.
646
def revision_tree(self, revision_id):
648
return self._real_repository.revision_tree(revision_id)
650
def get_serializer_format(self):
652
return self._real_repository.get_serializer_format()
654
def get_commit_builder(self, branch, parents, config, timestamp=None,
655
timezone=None, committer=None, revprops=None,
657
# FIXME: It ought to be possible to call this without immediately
658
# triggering _ensure_real. For now it's the easiest thing to do.
660
builder = self._real_repository.get_commit_builder(branch, parents,
661
config, timestamp=timestamp, timezone=timezone,
662
committer=committer, revprops=revprops, revision_id=revision_id)
665
def add_inventory(self, revid, inv, parents):
667
return self._real_repository.add_inventory(revid, inv, parents)
669
def add_revision(self, rev_id, rev, inv=None, config=None):
671
return self._real_repository.add_revision(
672
rev_id, rev, inv=inv, config=config)
675
def get_inventory(self, revision_id):
677
return self._real_repository.get_inventory(revision_id)
679
def iter_inventories(self, revision_ids):
681
return self._real_repository.iter_inventories(revision_ids)
684
def get_revision(self, revision_id):
686
return self._real_repository.get_revision(revision_id)
689
def weave_store(self):
691
return self._real_repository.weave_store
693
def get_transaction(self):
695
return self._real_repository.get_transaction()
698
def clone(self, a_bzrdir, revision_id=None):
700
return self._real_repository.clone(a_bzrdir, revision_id=revision_id)
702
def make_working_trees(self):
703
"""RemoteRepositories never create working trees by default."""
706
def revision_ids_to_search_result(self, result_set):
707
"""Convert a set of revision ids to a graph SearchResult."""
708
result_parents = set()
709
for parents in self.get_graph().get_parent_map(
710
result_set).itervalues():
711
result_parents.update(parents)
712
included_keys = result_set.intersection(result_parents)
713
start_keys = result_set.difference(included_keys)
714
exclude_keys = result_parents.difference(result_set)
715
result = graph.SearchResult(start_keys, exclude_keys,
716
len(result_set), result_set)
720
def search_missing_revision_ids(self, other, revision_id=None, find_ghosts=True):
721
"""Return the revision ids that other has that this does not.
723
These are returned in topological order.
725
revision_id: only return revision ids included by revision_id.
727
return repository.InterRepository.get(
728
other, self).search_missing_revision_ids(revision_id, find_ghosts)
730
def fetch(self, source, revision_id=None, pb=None):
731
if self.has_same_location(source):
732
# check that last_revision is in 'from' and then return a
734
if (revision_id is not None and
735
not revision.is_null(revision_id)):
736
self.get_revision(revision_id)
739
return self._real_repository.fetch(
740
source, revision_id=revision_id, pb=pb)
742
def create_bundle(self, target, base, fileobj, format=None):
744
self._real_repository.create_bundle(target, base, fileobj, format)
747
def control_weaves(self):
749
return self._real_repository.control_weaves
752
def get_ancestry(self, revision_id, topo_sorted=True):
754
return self._real_repository.get_ancestry(revision_id, topo_sorted)
757
def get_inventory_weave(self):
759
return self._real_repository.get_inventory_weave()
761
def fileids_altered_by_revision_ids(self, revision_ids):
763
return self._real_repository.fileids_altered_by_revision_ids(revision_ids)
765
def _get_versioned_file_checker(self, revisions, revision_versions_cache):
767
return self._real_repository._get_versioned_file_checker(
768
revisions, revision_versions_cache)
770
def iter_files_bytes(self, desired_files):
771
"""See Repository.iter_file_bytes.
774
return self._real_repository.iter_files_bytes(desired_files)
776
def get_parent_map(self, keys):
777
"""See bzrlib.Graph.get_parent_map()."""
778
# Hack to build up the caching logic.
779
ancestry = self._parents_map
781
# Repository is not locked, so there's no cache.
782
missing_revisions = set(keys)
785
missing_revisions = set(key for key in keys if key not in ancestry)
786
if missing_revisions:
787
parent_map = self._get_parent_map(missing_revisions)
788
if 'hpss' in debug.debug_flags:
789
mutter('retransmitted revisions: %d of %d',
790
len(set(ancestry).intersection(parent_map)),
792
ancestry.update(parent_map)
793
present_keys = [k for k in keys if k in ancestry]
794
if 'hpss' in debug.debug_flags:
795
self._requested_parents.update(present_keys)
796
mutter('Current RemoteRepository graph hit rate: %d%%',
797
100.0 * len(self._requested_parents) / len(ancestry))
798
return dict((k, ancestry[k]) for k in present_keys)
800
def _response_is_unknown_method(self, response, verb):
801
"""Return True if response is an unknonwn method response to verb.
803
:param response: The response from a smart client call_expecting_body
805
:param verb: The verb used in that call.
806
:return: True if an unknown method was encountered.
808
# This might live better on
809
# bzrlib.smart.protocol.SmartClientRequestProtocolOne
810
if (response[0] == ('error', "Generic bzr smart protocol error: "
811
"bad request '%s'" % verb) or
812
response[0] == ('error', "Generic bzr smart protocol error: "
813
"bad request u'%s'" % verb)):
814
response[1].cancel_read_body()
818
def _get_parent_map(self, keys):
819
"""Helper for get_parent_map that performs the RPC."""
820
medium = self._client.get_smart_medium()
821
if not medium._remote_is_at_least_1_2:
822
# We already found out that the server can't understand
823
# Repository.get_parent_map requests, so just fetch the whole
825
return self.get_revision_graph()
828
if NULL_REVISION in keys:
829
keys.discard(NULL_REVISION)
830
found_parents = {NULL_REVISION:()}
835
# TODO(Needs analysis): We could assume that the keys being requested
836
# from get_parent_map are in a breadth first search, so typically they
837
# will all be depth N from some common parent, and we don't have to
838
# have the server iterate from the root parent, but rather from the
839
# keys we're searching; and just tell the server the keyspace we
840
# already have; but this may be more traffic again.
842
# Transform self._parents_map into a search request recipe.
843
# TODO: Manage this incrementally to avoid covering the same path
844
# repeatedly. (The server will have to on each request, but the less
845
# work done the better).
846
parents_map = self._parents_map
847
if parents_map is None:
848
# Repository is not locked, so there's no cache.
850
start_set = set(parents_map)
851
result_parents = set()
852
for parents in parents_map.itervalues():
853
result_parents.update(parents)
854
stop_keys = result_parents.difference(start_set)
855
included_keys = start_set.intersection(result_parents)
856
start_set.difference_update(included_keys)
857
recipe = (start_set, stop_keys, len(parents_map))
858
body = self._serialise_search_recipe(recipe)
859
path = self.bzrdir._path_for_remote_call(self._client)
861
assert type(key) is str
862
verb = 'Repository.get_parent_map'
863
args = (path,) + tuple(keys)
864
response = self._client.call_with_body_bytes_expecting_body(
865
verb, args, self._serialise_search_recipe(recipe))
866
if self._response_is_unknown_method(response, verb):
867
# Server does not support this method, so get the whole graph.
868
# Worse, we have to force a disconnection, because the server now
869
# doesn't realise it has a body on the wire to consume, so the
870
# only way to recover is to abandon the connection.
872
'Server is too old for fast get_parent_map, reconnecting. '
873
'(Upgrade the server to Bazaar 1.2 to avoid this)')
875
# To avoid having to disconnect repeatedly, we keep track of the
876
# fact the server doesn't understand remote methods added in 1.2.
877
medium._remote_is_at_least_1_2 = False
878
return self.get_revision_graph()
879
elif response[0][0] not in ['ok']:
880
reponse[1].cancel_read_body()
881
raise errors.UnexpectedSmartServerResponse(response[0])
882
if response[0][0] == 'ok':
883
coded = bz2.decompress(response[1].read_body_bytes())
887
lines = coded.split('\n')
890
d = tuple(line.split())
892
revision_graph[d[0]] = d[1:]
894
# No parents - so give the Graph result (NULL_REVISION,).
895
revision_graph[d[0]] = (NULL_REVISION,)
896
return revision_graph
899
def get_signature_text(self, revision_id):
901
return self._real_repository.get_signature_text(revision_id)
904
def get_revision_graph_with_ghosts(self, revision_ids=None):
906
return self._real_repository.get_revision_graph_with_ghosts(
907
revision_ids=revision_ids)
910
def get_inventory_xml(self, revision_id):
912
return self._real_repository.get_inventory_xml(revision_id)
914
def deserialise_inventory(self, revision_id, xml):
916
return self._real_repository.deserialise_inventory(revision_id, xml)
918
def reconcile(self, other=None, thorough=False):
920
return self._real_repository.reconcile(other=other, thorough=thorough)
922
def all_revision_ids(self):
924
return self._real_repository.all_revision_ids()
927
def get_deltas_for_revisions(self, revisions):
929
return self._real_repository.get_deltas_for_revisions(revisions)
932
def get_revision_delta(self, revision_id):
934
return self._real_repository.get_revision_delta(revision_id)
937
def revision_trees(self, revision_ids):
939
return self._real_repository.revision_trees(revision_ids)
942
def get_revision_reconcile(self, revision_id):
944
return self._real_repository.get_revision_reconcile(revision_id)
947
def check(self, revision_ids=None):
949
return self._real_repository.check(revision_ids=revision_ids)
951
def copy_content_into(self, destination, revision_id=None):
953
return self._real_repository.copy_content_into(
954
destination, revision_id=revision_id)
956
def _copy_repository_tarball(self, to_bzrdir, revision_id=None):
957
# get a tarball of the remote repository, and copy from that into the
959
from bzrlib import osutils
962
# TODO: Maybe a progress bar while streaming the tarball?
963
note("Copying repository content as tarball...")
964
tar_file = self._get_tarball('bz2')
967
destination = to_bzrdir.create_repository()
969
tar = tarfile.open('repository', fileobj=tar_file,
971
tmpdir = tempfile.mkdtemp()
973
_extract_tar(tar, tmpdir)
974
tmp_bzrdir = BzrDir.open(tmpdir)
975
tmp_repo = tmp_bzrdir.open_repository()
976
tmp_repo.copy_content_into(destination, revision_id)
978
osutils.rmtree(tmpdir)
982
# TODO: Suggestion from john: using external tar is much faster than
983
# python's tarfile library, but it may not work on windows.
987
"""Compress the data within the repository.
989
This is not currently implemented within the smart server.
992
return self._real_repository.pack()
994
def set_make_working_trees(self, new_value):
995
raise NotImplementedError(self.set_make_working_trees)
998
def sign_revision(self, revision_id, gpg_strategy):
1000
return self._real_repository.sign_revision(revision_id, gpg_strategy)
1003
def get_revisions(self, revision_ids):
1005
return self._real_repository.get_revisions(revision_ids)
1007
def supports_rich_root(self):
1009
return self._real_repository.supports_rich_root()
1011
def iter_reverse_revision_history(self, revision_id):
1013
return self._real_repository.iter_reverse_revision_history(revision_id)
1016
def _serializer(self):
1018
return self._real_repository._serializer
1020
def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
1022
return self._real_repository.store_revision_signature(
1023
gpg_strategy, plaintext, revision_id)
1025
def add_signature_text(self, revision_id, signature):
1027
return self._real_repository.add_signature_text(revision_id, signature)
1029
def has_signature_for_revision_id(self, revision_id):
1031
return self._real_repository.has_signature_for_revision_id(revision_id)
1033
def get_data_stream_for_search(self, search):
1034
medium = self._client.get_smart_medium()
1035
if not medium._remote_is_at_least_1_2:
1037
return self._real_repository.get_data_stream_for_search(search)
1038
REQUEST_NAME = 'Repository.stream_revisions_chunked'
1039
path = self.bzrdir._path_for_remote_call(self._client)
1040
body = self._serialise_search_recipe(search.get_recipe())
1041
response, protocol = self._client.call_with_body_bytes_expecting_body(
1042
REQUEST_NAME, (path,), body)
1044
if self._response_is_unknown_method((response, protocol), REQUEST_NAME):
1045
# Server does not support this method, so fall back to VFS.
1046
# Worse, we have to force a disconnection, because the server now
1047
# doesn't realise it has a body on the wire to consume, so the
1048
# only way to recover is to abandon the connection.
1050
'Server is too old for streaming pull, reconnecting. '
1051
'(Upgrade the server to Bazaar 1.2 to avoid this)')
1053
# To avoid having to disconnect repeatedly, we keep track of the
1054
# fact the server doesn't understand this remote method.
1055
medium._remote_is_at_least_1_2 = False
1057
return self._real_repository.get_data_stream_for_search(search)
1059
if response == ('ok',):
1060
return self._deserialise_stream(protocol)
1061
if response == ('NoSuchRevision', ):
1062
# We cannot easily identify the revision that is missing in this
1063
# situation without doing much more network IO. For now, bail.
1064
raise NoSuchRevision(self, "unknown")
1066
raise errors.UnexpectedSmartServerResponse(response)
1068
def _deserialise_stream(self, protocol):
1069
stream = protocol.read_streamed_body()
1070
container_parser = ContainerPushParser()
1071
for bytes in stream:
1072
container_parser.accept_bytes(bytes)
1073
records = container_parser.read_pending_records()
1074
for record_names, record_bytes in records:
1075
if len(record_names) != 1:
1076
# These records should have only one name, and that name
1077
# should be a one-element tuple.
1078
raise errors.SmartProtocolError(
1079
'Repository data stream had invalid record name %r'
1081
name_tuple = record_names[0]
1082
yield name_tuple, record_bytes
1084
def insert_data_stream(self, stream):
1086
self._real_repository.insert_data_stream(stream)
1088
def item_keys_introduced_by(self, revision_ids, _files_pb=None):
1090
return self._real_repository.item_keys_introduced_by(revision_ids,
1091
_files_pb=_files_pb)
1093
def revision_graph_can_have_wrong_parents(self):
1094
# The answer depends on the remote repo format.
1096
return self._real_repository.revision_graph_can_have_wrong_parents()
1098
def _find_inconsistent_revision_parents(self):
1100
return self._real_repository._find_inconsistent_revision_parents()
1102
def _check_for_inconsistent_revision_parents(self):
1104
return self._real_repository._check_for_inconsistent_revision_parents()
1106
def _make_parents_provider(self):
1109
def _serialise_search_recipe(self, recipe):
1110
"""Serialise a graph search recipe.
1112
:param recipe: A search recipe (start, stop, count).
1113
:return: Serialised bytes.
1115
start_keys = ' '.join(recipe[0])
1116
stop_keys = ' '.join(recipe[1])
1117
count = str(recipe[2])
1118
return '\n'.join((start_keys, stop_keys, count))
1121
class RemoteBranchLockableFiles(LockableFiles):
1122
"""A 'LockableFiles' implementation that talks to a smart server.
1124
This is not a public interface class.
1127
def __init__(self, bzrdir, _client):
1128
self.bzrdir = bzrdir
1129
self._client = _client
1130
self._need_find_modes = True
1131
LockableFiles.__init__(
1132
self, bzrdir.get_branch_transport(None),
1133
'lock', lockdir.LockDir)
1135
def _find_modes(self):
1136
# RemoteBranches don't let the client set the mode of control files.
1137
self._dir_mode = None
1138
self._file_mode = None
1140
def get(self, path):
1141
"""'get' a remote path as per the LockableFiles interface.
1143
:param path: the file to 'get'. If this is 'branch.conf', we do not
1144
just retrieve a file, instead we ask the smart server to generate
1145
a configuration for us - which is retrieved as an INI file.
1147
if path == 'branch.conf':
1148
path = self.bzrdir._path_for_remote_call(self._client)
1149
response = self._client.call_expecting_body(
1150
'Branch.get_config_file', path)
1151
assert response[0][0] == 'ok', \
1152
'unexpected response code %s' % (response[0],)
1153
return StringIO(response[1].read_body_bytes())
1156
return LockableFiles.get(self, path)
1159
class RemoteBranchFormat(branch.BranchFormat):
1161
def __eq__(self, other):
1162
return (isinstance(other, RemoteBranchFormat) and
1163
self.__dict__ == other.__dict__)
1165
def get_format_description(self):
1166
return 'Remote BZR Branch'
1168
def get_format_string(self):
1169
return 'Remote BZR Branch'
1171
def open(self, a_bzrdir):
1172
assert isinstance(a_bzrdir, RemoteBzrDir)
1173
return a_bzrdir.open_branch()
1175
def initialize(self, a_bzrdir):
1176
assert isinstance(a_bzrdir, RemoteBzrDir)
1177
return a_bzrdir.create_branch()
1179
def supports_tags(self):
1180
# Remote branches might support tags, but we won't know until we
1181
# access the real remote branch.
1185
class RemoteBranch(branch.Branch):
1186
"""Branch stored on a server accessed by HPSS RPC.
1188
At the moment most operations are mapped down to simple file operations.
1191
def __init__(self, remote_bzrdir, remote_repository, real_branch=None,
1193
"""Create a RemoteBranch instance.
1195
:param real_branch: An optional local implementation of the branch
1196
format, usually accessing the data via the VFS.
1197
:param _client: Private parameter for testing.
1199
# We intentionally don't call the parent class's __init__, because it
1200
# will try to assign to self.tags, which is a property in this subclass.
1201
# And the parent's __init__ doesn't do much anyway.
1202
self._revision_id_to_revno_cache = None
1203
self._revision_history_cache = None
1204
self.bzrdir = remote_bzrdir
1205
if _client is not None:
1206
self._client = _client
1208
self._client = client._SmartClient(self.bzrdir._shared_medium)
1209
self.repository = remote_repository
1210
if real_branch is not None:
1211
self._real_branch = real_branch
1212
# Give the remote repository the matching real repo.
1213
real_repo = self._real_branch.repository
1214
if isinstance(real_repo, RemoteRepository):
1215
real_repo._ensure_real()
1216
real_repo = real_repo._real_repository
1217
self.repository._set_real_repository(real_repo)
1218
# Give the branch the remote repository to let fast-pathing happen.
1219
self._real_branch.repository = self.repository
1221
self._real_branch = None
1222
# Fill out expected attributes of branch for bzrlib api users.
1223
self._format = RemoteBranchFormat()
1224
self.base = self.bzrdir.root_transport.base
1225
self._control_files = None
1226
self._lock_mode = None
1227
self._lock_token = None
1228
self._lock_count = 0
1229
self._leave_lock = False
1232
return "%s(%s)" % (self.__class__.__name__, self.base)
1236
def _ensure_real(self):
1237
"""Ensure that there is a _real_branch set.
1239
Used before calls to self._real_branch.
1241
if not self._real_branch:
1242
assert vfs.vfs_enabled()
1243
self.bzrdir._ensure_real()
1244
self._real_branch = self.bzrdir._real_bzrdir.open_branch()
1245
# Give the remote repository the matching real repo.
1246
real_repo = self._real_branch.repository
1247
if isinstance(real_repo, RemoteRepository):
1248
real_repo._ensure_real()
1249
real_repo = real_repo._real_repository
1250
self.repository._set_real_repository(real_repo)
1251
# Give the branch the remote repository to let fast-pathing happen.
1252
self._real_branch.repository = self.repository
1253
# XXX: deal with _lock_mode == 'w'
1254
if self._lock_mode == 'r':
1255
self._real_branch.lock_read()
1258
def control_files(self):
1259
# Defer actually creating RemoteBranchLockableFiles until its needed,
1260
# because it triggers an _ensure_real that we otherwise might not need.
1261
if self._control_files is None:
1262
self._control_files = RemoteBranchLockableFiles(
1263
self.bzrdir, self._client)
1264
return self._control_files
1266
def _get_checkout_format(self):
1268
return self._real_branch._get_checkout_format()
1270
def get_physical_lock_status(self):
1271
"""See Branch.get_physical_lock_status()."""
1272
# should be an API call to the server, as branches must be lockable.
1274
return self._real_branch.get_physical_lock_status()
1276
def lock_read(self):
1277
if not self._lock_mode:
1278
self._lock_mode = 'r'
1279
self._lock_count = 1
1280
if self._real_branch is not None:
1281
self._real_branch.lock_read()
1283
self._lock_count += 1
1285
def _remote_lock_write(self, token):
1287
branch_token = repo_token = ''
1289
branch_token = token
1290
repo_token = self.repository.lock_write()
1291
self.repository.unlock()
1292
path = self.bzrdir._path_for_remote_call(self._client)
1293
response = self._client.call('Branch.lock_write', path, branch_token,
1295
if response[0] == 'ok':
1296
ok, branch_token, repo_token = response
1297
return branch_token, repo_token
1298
elif response[0] == 'LockContention':
1299
raise errors.LockContention('(remote lock)')
1300
elif response[0] == 'TokenMismatch':
1301
raise errors.TokenMismatch(token, '(remote token)')
1302
elif response[0] == 'UnlockableTransport':
1303
raise errors.UnlockableTransport(self.bzrdir.root_transport)
1304
elif response[0] == 'ReadOnlyError':
1305
raise errors.ReadOnlyError(self)
1306
elif response[0] == 'LockFailed':
1307
raise errors.LockFailed(response[1], response[2])
1309
raise errors.UnexpectedSmartServerResponse(response)
1311
def lock_write(self, token=None):
1312
if not self._lock_mode:
1313
remote_tokens = self._remote_lock_write(token)
1314
self._lock_token, self._repo_lock_token = remote_tokens
1315
assert self._lock_token, 'Remote server did not return a token!'
1316
# TODO: We really, really, really don't want to call _ensure_real
1317
# here, but it's the easiest way to ensure coherency between the
1318
# state of the RemoteBranch and RemoteRepository objects and the
1319
# physical locks. If we don't materialise the real objects here,
1320
# then getting everything in the right state later is complex, so
1321
# for now we just do it the lazy way.
1322
# -- Andrew Bennetts, 2007-02-22.
1324
if self._real_branch is not None:
1325
self._real_branch.repository.lock_write(
1326
token=self._repo_lock_token)
1328
self._real_branch.lock_write(token=self._lock_token)
1330
self._real_branch.repository.unlock()
1331
if token is not None:
1332
self._leave_lock = True
1334
# XXX: this case seems to be unreachable; token cannot be None.
1335
self._leave_lock = False
1336
self._lock_mode = 'w'
1337
self._lock_count = 1
1338
elif self._lock_mode == 'r':
1339
raise errors.ReadOnlyTransaction
1341
if token is not None:
1342
# A token was given to lock_write, and we're relocking, so check
1343
# that the given token actually matches the one we already have.
1344
if token != self._lock_token:
1345
raise errors.TokenMismatch(token, self._lock_token)
1346
self._lock_count += 1
1347
return self._lock_token or None
1349
def _unlock(self, branch_token, repo_token):
1350
path = self.bzrdir._path_for_remote_call(self._client)
1351
response = self._client.call('Branch.unlock', path, branch_token,
1353
if response == ('ok',):
1355
elif response[0] == 'TokenMismatch':
1356
raise errors.TokenMismatch(
1357
str((branch_token, repo_token)), '(remote tokens)')
1359
raise errors.UnexpectedSmartServerResponse(response)
1362
self._lock_count -= 1
1363
if not self._lock_count:
1364
self._clear_cached_state()
1365
mode = self._lock_mode
1366
self._lock_mode = None
1367
if self._real_branch is not None:
1368
if (not self._leave_lock and mode == 'w' and
1369
self._repo_lock_token):
1370
# If this RemoteBranch will remove the physical lock for the
1371
# repository, make sure the _real_branch doesn't do it
1372
# first. (Because the _real_branch's repository is set to
1373
# be the RemoteRepository.)
1374
self._real_branch.repository.leave_lock_in_place()
1375
self._real_branch.unlock()
1377
# Only write-locked branched need to make a remote method call
1378
# to perfom the unlock.
1380
assert self._lock_token, 'Locked, but no token!'
1381
branch_token = self._lock_token
1382
repo_token = self._repo_lock_token
1383
self._lock_token = None
1384
self._repo_lock_token = None
1385
if not self._leave_lock:
1386
self._unlock(branch_token, repo_token)
1388
def break_lock(self):
1390
return self._real_branch.break_lock()
1392
def leave_lock_in_place(self):
1393
if not self._lock_token:
1394
raise NotImplementedError(self.leave_lock_in_place)
1395
self._leave_lock = True
1397
def dont_leave_lock_in_place(self):
1398
if not self._lock_token:
1399
raise NotImplementedError(self.dont_leave_lock_in_place)
1400
self._leave_lock = False
1402
def last_revision_info(self):
1403
"""See Branch.last_revision_info()."""
1404
path = self.bzrdir._path_for_remote_call(self._client)
1405
response = self._client.call('Branch.last_revision_info', path)
1406
assert response[0] == 'ok', 'unexpected response code %s' % (response,)
1407
revno = int(response[1])
1408
last_revision = response[2]
1409
return (revno, last_revision)
1411
def _gen_revision_history(self):
1412
"""See Branch._gen_revision_history()."""
1413
path = self.bzrdir._path_for_remote_call(self._client)
1414
response = self._client.call_expecting_body(
1415
'Branch.revision_history', path)
1416
assert response[0][0] == 'ok', ('unexpected response code %s'
1418
result = response[1].read_body_bytes().split('\x00')
1424
def set_revision_history(self, rev_history):
1425
# Send just the tip revision of the history; the server will generate
1426
# the full history from that. If the revision doesn't exist in this
1427
# branch, NoSuchRevision will be raised.
1428
path = self.bzrdir._path_for_remote_call(self._client)
1429
if rev_history == []:
1432
rev_id = rev_history[-1]
1433
self._clear_cached_state()
1434
response = self._client.call('Branch.set_last_revision',
1435
path, self._lock_token, self._repo_lock_token, rev_id)
1436
if response[0] == 'NoSuchRevision':
1437
raise NoSuchRevision(self, rev_id)
1439
assert response == ('ok',), (
1440
'unexpected response code %r' % (response,))
1441
self._cache_revision_history(rev_history)
1443
def get_parent(self):
1445
return self._real_branch.get_parent()
1447
def set_parent(self, url):
1449
return self._real_branch.set_parent(url)
1451
def get_config(self):
1452
return RemoteBranchConfig(self)
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
1488
def set_last_revision_info(self, revno, revision_id):
1490
self._clear_cached_state()
1491
return self._real_branch.set_last_revision_info(revno, revision_id)
1493
def generate_revision_history(self, revision_id, last_rev=None,
1496
return self._real_branch.generate_revision_history(
1497
revision_id, last_rev=last_rev, other_branch=other_branch)
1502
return self._real_branch.tags
1504
def set_push_location(self, location):
1506
return self._real_branch.set_push_location(location)
1508
def update_revisions(self, other, stop_revision=None, overwrite=False):
1510
return self._real_branch.update_revisions(
1511
other, stop_revision=stop_revision, overwrite=overwrite)
1514
class RemoteBranchConfig(BranchConfig):
1517
self.branch._ensure_real()
1518
return self.branch._real_branch.get_config().username()
1520
def _get_branch_data_config(self):
1521
self.branch._ensure_real()
1522
if self._branch_data_config is None:
1523
self._branch_data_config = TreeConfig(self.branch._real_branch)
1524
return self._branch_data_config
1527
def _extract_tar(tar, to_dir):
1528
"""Extract all the contents of a tarfile object.
1530
A replacement for extractall, which is not present in python2.4
1533
tar.extract(tarinfo, to_dir)