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
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
780
missing_revisions = set(key for key in keys if key not in ancestry)
781
if missing_revisions:
782
parent_map = self._get_parent_map(missing_revisions)
783
if 'hpss' in debug.debug_flags:
784
mutter('retransmitted revisions: %d of %d',
785
len(set(self._parents_map).intersection(parent_map)),
787
self._parents_map.update(parent_map)
788
present_keys = [k for k in keys if k in ancestry]
789
if 'hpss' in debug.debug_flags:
790
self._requested_parents.update(present_keys)
791
mutter('Current RemoteRepository graph hit rate: %d%%',
792
100.0 * len(self._requested_parents) / len(self._parents_map))
793
return dict((k, ancestry[k]) for k in present_keys)
795
def _response_is_unknown_method(self, response, verb):
796
"""Return True if response is an unknonwn method response to verb.
798
:param response: The response from a smart client call_expecting_body
800
:param verb: The verb used in that call.
801
:return: True if an unknown method was encountered.
803
# This might live better on
804
# bzrlib.smart.protocol.SmartClientRequestProtocolOne
805
if (response[0] == ('error', "Generic bzr smart protocol error: "
806
"bad request '%s'" % verb) or
807
response[0] == ('error', "Generic bzr smart protocol error: "
808
"bad request u'%s'" % verb)):
809
response[1].cancel_read_body()
813
def _get_parent_map(self, keys):
814
"""Helper for get_parent_map that performs the RPC."""
816
if NULL_REVISION in keys:
817
keys.discard(NULL_REVISION)
818
found_parents = {NULL_REVISION:()}
823
# TODO(Needs analysis): We could assume that the keys being requested
824
# from get_parent_map are in a breadth first search, so typically they
825
# will all be depth N from some common parent, and we don't have to
826
# have the server iterate from the root parent, but rather from the
827
# keys we're searching; and just tell the server the keyspace we
828
# already have; but this may be more traffic again.
830
# Transform self._parents_map into a search request recipe.
831
# TODO: Manage this incrementally to avoid covering the same path
832
# repeatedly. (The server will have to on each request, but the less
833
# work done the better).
834
start_set = set(self._parents_map)
835
result_parents = set()
836
for parents in self._parents_map.itervalues():
837
result_parents.update(parents)
838
stop_keys = result_parents.difference(start_set)
839
included_keys = start_set.intersection(result_parents)
840
start_set.difference_update(included_keys)
841
recipe = (start_set, stop_keys, len(self._parents_map))
842
body = self._serialise_search_recipe(recipe)
843
path = self.bzrdir._path_for_remote_call(self._client)
845
assert type(key) is str
846
verb = 'Repository.get_parent_map'
847
args = (path,) + tuple(keys)
848
response = self._client.call_with_body_bytes_expecting_body(
849
verb, args, self._serialise_search_recipe(recipe))
850
if self._response_is_unknown_method(response, verb):
851
# Server that does not support this method, get the whole graph.
852
response = self._client.call_expecting_body(
853
'Repository.get_revision_graph', path, '')
854
if response[0][0] not in ['ok', 'nosuchrevision']:
855
reponse[1].cancel_read_body()
856
raise errors.UnexpectedSmartServerResponse(response[0])
857
elif response[0][0] not in ['ok']:
858
reponse[1].cancel_read_body()
859
raise errors.UnexpectedSmartServerResponse(response[0])
860
if response[0][0] == 'ok':
861
coded = bz2.decompress(response[1].read_body_bytes())
865
lines = coded.split('\n')
868
d = tuple(line.split())
870
revision_graph[d[0]] = d[1:]
872
# No parents - so give the Graph result (NULL_REVISION,).
873
revision_graph[d[0]] = (NULL_REVISION,)
874
return revision_graph
877
def get_signature_text(self, revision_id):
879
return self._real_repository.get_signature_text(revision_id)
882
def get_revision_graph_with_ghosts(self, revision_ids=None):
884
return self._real_repository.get_revision_graph_with_ghosts(
885
revision_ids=revision_ids)
888
def get_inventory_xml(self, revision_id):
890
return self._real_repository.get_inventory_xml(revision_id)
892
def deserialise_inventory(self, revision_id, xml):
894
return self._real_repository.deserialise_inventory(revision_id, xml)
896
def reconcile(self, other=None, thorough=False):
898
return self._real_repository.reconcile(other=other, thorough=thorough)
900
def all_revision_ids(self):
902
return self._real_repository.all_revision_ids()
905
def get_deltas_for_revisions(self, revisions):
907
return self._real_repository.get_deltas_for_revisions(revisions)
910
def get_revision_delta(self, revision_id):
912
return self._real_repository.get_revision_delta(revision_id)
915
def revision_trees(self, revision_ids):
917
return self._real_repository.revision_trees(revision_ids)
920
def get_revision_reconcile(self, revision_id):
922
return self._real_repository.get_revision_reconcile(revision_id)
925
def check(self, revision_ids=None):
927
return self._real_repository.check(revision_ids=revision_ids)
929
def copy_content_into(self, destination, revision_id=None):
931
return self._real_repository.copy_content_into(
932
destination, revision_id=revision_id)
934
def _copy_repository_tarball(self, to_bzrdir, revision_id=None):
935
# get a tarball of the remote repository, and copy from that into the
937
from bzrlib import osutils
940
# TODO: Maybe a progress bar while streaming the tarball?
941
note("Copying repository content as tarball...")
942
tar_file = self._get_tarball('bz2')
945
destination = to_bzrdir.create_repository()
947
tar = tarfile.open('repository', fileobj=tar_file,
949
tmpdir = tempfile.mkdtemp()
951
_extract_tar(tar, tmpdir)
952
tmp_bzrdir = BzrDir.open(tmpdir)
953
tmp_repo = tmp_bzrdir.open_repository()
954
tmp_repo.copy_content_into(destination, revision_id)
956
osutils.rmtree(tmpdir)
960
# TODO: Suggestion from john: using external tar is much faster than
961
# python's tarfile library, but it may not work on windows.
965
"""Compress the data within the repository.
967
This is not currently implemented within the smart server.
970
return self._real_repository.pack()
972
def set_make_working_trees(self, new_value):
973
raise NotImplementedError(self.set_make_working_trees)
976
def sign_revision(self, revision_id, gpg_strategy):
978
return self._real_repository.sign_revision(revision_id, gpg_strategy)
981
def get_revisions(self, revision_ids):
983
return self._real_repository.get_revisions(revision_ids)
985
def supports_rich_root(self):
987
return self._real_repository.supports_rich_root()
989
def iter_reverse_revision_history(self, revision_id):
991
return self._real_repository.iter_reverse_revision_history(revision_id)
994
def _serializer(self):
996
return self._real_repository._serializer
998
def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
1000
return self._real_repository.store_revision_signature(
1001
gpg_strategy, plaintext, revision_id)
1003
def add_signature_text(self, revision_id, signature):
1005
return self._real_repository.add_signature_text(revision_id, signature)
1007
def has_signature_for_revision_id(self, revision_id):
1009
return self._real_repository.has_signature_for_revision_id(revision_id)
1011
def get_data_stream_for_search(self, search):
1012
REQUEST_NAME = 'Repository.stream_revisions_chunked'
1013
path = self.bzrdir._path_for_remote_call(self._client)
1014
body = self._serialise_search_recipe(search.get_recipe())
1015
response, protocol = self._client.call_with_body_bytes_expecting_body(
1016
REQUEST_NAME, (path,), body)
1018
if response == ('ok',):
1019
return self._deserialise_stream(protocol)
1020
if response == ('NoSuchRevision', ):
1021
# We cannot easily identify the revision that is missing in this
1022
# situation without doing much more network IO. For now, bail.
1023
raise NoSuchRevision(self, "unknown")
1024
elif (response == ('error', "Generic bzr smart protocol error: "
1025
"bad request '%s'" % REQUEST_NAME) or
1026
response == ('error', "Generic bzr smart protocol error: "
1027
"bad request u'%s'" % REQUEST_NAME)):
1028
protocol.cancel_read_body()
1030
return self._real_repository.get_data_stream_for_search(search)
1032
raise errors.UnexpectedSmartServerResponse(response)
1034
def _deserialise_stream(self, protocol):
1035
stream = protocol.read_streamed_body()
1036
container_parser = ContainerPushParser()
1037
for bytes in stream:
1038
container_parser.accept_bytes(bytes)
1039
records = container_parser.read_pending_records()
1040
for record_names, record_bytes in records:
1041
if len(record_names) != 1:
1042
# These records should have only one name, and that name
1043
# should be a one-element tuple.
1044
raise errors.SmartProtocolError(
1045
'Repository data stream had invalid record name %r'
1047
name_tuple = record_names[0]
1048
yield name_tuple, record_bytes
1050
def insert_data_stream(self, stream):
1052
self._real_repository.insert_data_stream(stream)
1054
def item_keys_introduced_by(self, revision_ids, _files_pb=None):
1056
return self._real_repository.item_keys_introduced_by(revision_ids,
1057
_files_pb=_files_pb)
1059
def revision_graph_can_have_wrong_parents(self):
1060
# The answer depends on the remote repo format.
1062
return self._real_repository.revision_graph_can_have_wrong_parents()
1064
def _find_inconsistent_revision_parents(self):
1066
return self._real_repository._find_inconsistent_revision_parents()
1068
def _check_for_inconsistent_revision_parents(self):
1070
return self._real_repository._check_for_inconsistent_revision_parents()
1072
def _make_parents_provider(self):
1075
def _serialise_search_recipe(self, recipe):
1076
"""Serialise a graph search recipe.
1078
:param recipe: A search recipe (start, stop, count).
1079
:return: Serialised bytes.
1081
start_keys = ' '.join(recipe[0])
1082
stop_keys = ' '.join(recipe[1])
1083
count = str(recipe[2])
1084
return '\n'.join((start_keys, stop_keys, count))
1087
class RemoteBranchLockableFiles(LockableFiles):
1088
"""A 'LockableFiles' implementation that talks to a smart server.
1090
This is not a public interface class.
1093
def __init__(self, bzrdir, _client):
1094
self.bzrdir = bzrdir
1095
self._client = _client
1096
self._need_find_modes = True
1097
LockableFiles.__init__(
1098
self, bzrdir.get_branch_transport(None),
1099
'lock', lockdir.LockDir)
1101
def _find_modes(self):
1102
# RemoteBranches don't let the client set the mode of control files.
1103
self._dir_mode = None
1104
self._file_mode = None
1106
def get(self, path):
1107
"""'get' a remote path as per the LockableFiles interface.
1109
:param path: the file to 'get'. If this is 'branch.conf', we do not
1110
just retrieve a file, instead we ask the smart server to generate
1111
a configuration for us - which is retrieved as an INI file.
1113
if path == 'branch.conf':
1114
path = self.bzrdir._path_for_remote_call(self._client)
1115
response = self._client.call_expecting_body(
1116
'Branch.get_config_file', path)
1117
assert response[0][0] == 'ok', \
1118
'unexpected response code %s' % (response[0],)
1119
return StringIO(response[1].read_body_bytes())
1122
return LockableFiles.get(self, path)
1125
class RemoteBranchFormat(branch.BranchFormat):
1127
def __eq__(self, other):
1128
return (isinstance(other, RemoteBranchFormat) and
1129
self.__dict__ == other.__dict__)
1131
def get_format_description(self):
1132
return 'Remote BZR Branch'
1134
def get_format_string(self):
1135
return 'Remote BZR Branch'
1137
def open(self, a_bzrdir):
1138
assert isinstance(a_bzrdir, RemoteBzrDir)
1139
return a_bzrdir.open_branch()
1141
def initialize(self, a_bzrdir):
1142
assert isinstance(a_bzrdir, RemoteBzrDir)
1143
return a_bzrdir.create_branch()
1145
def supports_tags(self):
1146
# Remote branches might support tags, but we won't know until we
1147
# access the real remote branch.
1151
class RemoteBranch(branch.Branch):
1152
"""Branch stored on a server accessed by HPSS RPC.
1154
At the moment most operations are mapped down to simple file operations.
1157
def __init__(self, remote_bzrdir, remote_repository, real_branch=None,
1159
"""Create a RemoteBranch instance.
1161
:param real_branch: An optional local implementation of the branch
1162
format, usually accessing the data via the VFS.
1163
:param _client: Private parameter for testing.
1165
# We intentionally don't call the parent class's __init__, because it
1166
# will try to assign to self.tags, which is a property in this subclass.
1167
# And the parent's __init__ doesn't do much anyway.
1168
self._revision_id_to_revno_cache = None
1169
self._revision_history_cache = None
1170
self.bzrdir = remote_bzrdir
1171
if _client is not None:
1172
self._client = _client
1174
self._client = client._SmartClient(self.bzrdir._shared_medium)
1175
self.repository = remote_repository
1176
if real_branch is not None:
1177
self._real_branch = real_branch
1178
# Give the remote repository the matching real repo.
1179
real_repo = self._real_branch.repository
1180
if isinstance(real_repo, RemoteRepository):
1181
real_repo._ensure_real()
1182
real_repo = real_repo._real_repository
1183
self.repository._set_real_repository(real_repo)
1184
# Give the branch the remote repository to let fast-pathing happen.
1185
self._real_branch.repository = self.repository
1187
self._real_branch = None
1188
# Fill out expected attributes of branch for bzrlib api users.
1189
self._format = RemoteBranchFormat()
1190
self.base = self.bzrdir.root_transport.base
1191
self._control_files = None
1192
self._lock_mode = None
1193
self._lock_token = None
1194
self._lock_count = 0
1195
self._leave_lock = False
1198
return "%s(%s)" % (self.__class__.__name__, self.base)
1202
def _ensure_real(self):
1203
"""Ensure that there is a _real_branch set.
1205
Used before calls to self._real_branch.
1207
if not self._real_branch:
1208
assert vfs.vfs_enabled()
1209
self.bzrdir._ensure_real()
1210
self._real_branch = self.bzrdir._real_bzrdir.open_branch()
1211
# Give the remote repository the matching real repo.
1212
real_repo = self._real_branch.repository
1213
if isinstance(real_repo, RemoteRepository):
1214
real_repo._ensure_real()
1215
real_repo = real_repo._real_repository
1216
self.repository._set_real_repository(real_repo)
1217
# Give the branch the remote repository to let fast-pathing happen.
1218
self._real_branch.repository = self.repository
1219
# XXX: deal with _lock_mode == 'w'
1220
if self._lock_mode == 'r':
1221
self._real_branch.lock_read()
1224
def control_files(self):
1225
# Defer actually creating RemoteBranchLockableFiles until its needed,
1226
# because it triggers an _ensure_real that we otherwise might not need.
1227
if self._control_files is None:
1228
self._control_files = RemoteBranchLockableFiles(
1229
self.bzrdir, self._client)
1230
return self._control_files
1232
def _get_checkout_format(self):
1234
return self._real_branch._get_checkout_format()
1236
def get_physical_lock_status(self):
1237
"""See Branch.get_physical_lock_status()."""
1238
# should be an API call to the server, as branches must be lockable.
1240
return self._real_branch.get_physical_lock_status()
1242
def lock_read(self):
1243
if not self._lock_mode:
1244
self._lock_mode = 'r'
1245
self._lock_count = 1
1246
if self._real_branch is not None:
1247
self._real_branch.lock_read()
1249
self._lock_count += 1
1251
def _remote_lock_write(self, token):
1253
branch_token = repo_token = ''
1255
branch_token = token
1256
repo_token = self.repository.lock_write()
1257
self.repository.unlock()
1258
path = self.bzrdir._path_for_remote_call(self._client)
1259
response = self._client.call('Branch.lock_write', path, branch_token,
1261
if response[0] == 'ok':
1262
ok, branch_token, repo_token = response
1263
return branch_token, repo_token
1264
elif response[0] == 'LockContention':
1265
raise errors.LockContention('(remote lock)')
1266
elif response[0] == 'TokenMismatch':
1267
raise errors.TokenMismatch(token, '(remote token)')
1268
elif response[0] == 'UnlockableTransport':
1269
raise errors.UnlockableTransport(self.bzrdir.root_transport)
1270
elif response[0] == 'ReadOnlyError':
1271
raise errors.ReadOnlyError(self)
1272
elif response[0] == 'LockFailed':
1273
raise errors.LockFailed(response[1], response[2])
1275
raise errors.UnexpectedSmartServerResponse(response)
1277
def lock_write(self, token=None):
1278
if not self._lock_mode:
1279
remote_tokens = self._remote_lock_write(token)
1280
self._lock_token, self._repo_lock_token = remote_tokens
1281
assert self._lock_token, 'Remote server did not return a token!'
1282
# TODO: We really, really, really don't want to call _ensure_real
1283
# here, but it's the easiest way to ensure coherency between the
1284
# state of the RemoteBranch and RemoteRepository objects and the
1285
# physical locks. If we don't materialise the real objects here,
1286
# then getting everything in the right state later is complex, so
1287
# for now we just do it the lazy way.
1288
# -- Andrew Bennetts, 2007-02-22.
1290
if self._real_branch is not None:
1291
self._real_branch.repository.lock_write(
1292
token=self._repo_lock_token)
1294
self._real_branch.lock_write(token=self._lock_token)
1296
self._real_branch.repository.unlock()
1297
if token is not None:
1298
self._leave_lock = True
1300
# XXX: this case seems to be unreachable; token cannot be None.
1301
self._leave_lock = False
1302
self._lock_mode = 'w'
1303
self._lock_count = 1
1304
elif self._lock_mode == 'r':
1305
raise errors.ReadOnlyTransaction
1307
if token is not None:
1308
# A token was given to lock_write, and we're relocking, so check
1309
# that the given token actually matches the one we already have.
1310
if token != self._lock_token:
1311
raise errors.TokenMismatch(token, self._lock_token)
1312
self._lock_count += 1
1313
return self._lock_token or None
1315
def _unlock(self, branch_token, repo_token):
1316
path = self.bzrdir._path_for_remote_call(self._client)
1317
response = self._client.call('Branch.unlock', path, branch_token,
1319
if response == ('ok',):
1321
elif response[0] == 'TokenMismatch':
1322
raise errors.TokenMismatch(
1323
str((branch_token, repo_token)), '(remote tokens)')
1325
raise errors.UnexpectedSmartServerResponse(response)
1328
self._lock_count -= 1
1329
if not self._lock_count:
1330
self._clear_cached_state()
1331
mode = self._lock_mode
1332
self._lock_mode = None
1333
if self._real_branch is not None:
1334
if (not self._leave_lock and mode == 'w' and
1335
self._repo_lock_token):
1336
# If this RemoteBranch will remove the physical lock for the
1337
# repository, make sure the _real_branch doesn't do it
1338
# first. (Because the _real_branch's repository is set to
1339
# be the RemoteRepository.)
1340
self._real_branch.repository.leave_lock_in_place()
1341
self._real_branch.unlock()
1343
# Only write-locked branched need to make a remote method call
1344
# to perfom the unlock.
1346
assert self._lock_token, 'Locked, but no token!'
1347
branch_token = self._lock_token
1348
repo_token = self._repo_lock_token
1349
self._lock_token = None
1350
self._repo_lock_token = None
1351
if not self._leave_lock:
1352
self._unlock(branch_token, repo_token)
1354
def break_lock(self):
1356
return self._real_branch.break_lock()
1358
def leave_lock_in_place(self):
1359
if not self._lock_token:
1360
raise NotImplementedError(self.leave_lock_in_place)
1361
self._leave_lock = True
1363
def dont_leave_lock_in_place(self):
1364
if not self._lock_token:
1365
raise NotImplementedError(self.dont_leave_lock_in_place)
1366
self._leave_lock = False
1368
def last_revision_info(self):
1369
"""See Branch.last_revision_info()."""
1370
path = self.bzrdir._path_for_remote_call(self._client)
1371
response = self._client.call('Branch.last_revision_info', path)
1372
assert response[0] == 'ok', 'unexpected response code %s' % (response,)
1373
revno = int(response[1])
1374
last_revision = response[2]
1375
return (revno, last_revision)
1377
def _gen_revision_history(self):
1378
"""See Branch._gen_revision_history()."""
1379
path = self.bzrdir._path_for_remote_call(self._client)
1380
response = self._client.call_expecting_body(
1381
'Branch.revision_history', path)
1382
assert response[0][0] == 'ok', ('unexpected response code %s'
1384
result = response[1].read_body_bytes().split('\x00')
1390
def set_revision_history(self, rev_history):
1391
# Send just the tip revision of the history; the server will generate
1392
# the full history from that. If the revision doesn't exist in this
1393
# branch, NoSuchRevision will be raised.
1394
path = self.bzrdir._path_for_remote_call(self._client)
1395
if rev_history == []:
1398
rev_id = rev_history[-1]
1399
self._clear_cached_state()
1400
response = self._client.call('Branch.set_last_revision',
1401
path, self._lock_token, self._repo_lock_token, rev_id)
1402
if response[0] == 'NoSuchRevision':
1403
raise NoSuchRevision(self, rev_id)
1405
assert response == ('ok',), (
1406
'unexpected response code %r' % (response,))
1407
self._cache_revision_history(rev_history)
1409
def get_parent(self):
1411
return self._real_branch.get_parent()
1413
def set_parent(self, url):
1415
return self._real_branch.set_parent(url)
1417
def get_config(self):
1418
return RemoteBranchConfig(self)
1420
def sprout(self, to_bzrdir, revision_id=None):
1421
# Like Branch.sprout, except that it sprouts a branch in the default
1422
# format, because RemoteBranches can't be created at arbitrary URLs.
1423
# XXX: if to_bzrdir is a RemoteBranch, this should perhaps do
1424
# to_bzrdir.create_branch...
1426
result = self._real_branch._format.initialize(to_bzrdir)
1427
self.copy_content_into(result, revision_id=revision_id)
1428
result.set_parent(self.bzrdir.root_transport.base)
1432
def pull(self, source, overwrite=False, stop_revision=None,
1434
# FIXME: This asks the real branch to run the hooks, which means
1435
# they're called with the wrong target branch parameter.
1436
# The test suite specifically allows this at present but it should be
1437
# fixed. It should get a _override_hook_target branch,
1438
# as push does. -- mbp 20070405
1440
self._real_branch.pull(
1441
source, overwrite=overwrite, stop_revision=stop_revision,
1445
def push(self, target, overwrite=False, stop_revision=None):
1447
return self._real_branch.push(
1448
target, overwrite=overwrite, stop_revision=stop_revision,
1449
_override_hook_source_branch=self)
1451
def is_locked(self):
1452
return self._lock_count >= 1
1454
def set_last_revision_info(self, revno, revision_id):
1456
self._clear_cached_state()
1457
return self._real_branch.set_last_revision_info(revno, revision_id)
1459
def generate_revision_history(self, revision_id, last_rev=None,
1462
return self._real_branch.generate_revision_history(
1463
revision_id, last_rev=last_rev, other_branch=other_branch)
1468
return self._real_branch.tags
1470
def set_push_location(self, location):
1472
return self._real_branch.set_push_location(location)
1474
def update_revisions(self, other, stop_revision=None, overwrite=False):
1476
return self._real_branch.update_revisions(
1477
other, stop_revision=stop_revision, overwrite=overwrite)
1480
class RemoteBranchConfig(BranchConfig):
1483
self.branch._ensure_real()
1484
return self.branch._real_branch.get_config().username()
1486
def _get_branch_data_config(self):
1487
self.branch._ensure_real()
1488
if self._branch_data_config is None:
1489
self._branch_data_config = TreeConfig(self.branch._real_branch)
1490
return self._branch_data_config
1493
def _extract_tar(tar, to_dir):
1494
"""Extract all the contents of a tarfile object.
1496
A replacement for extractall, which is not present in python2.4
1499
tar.extract(tarinfo, to_dir)