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.
20
from cStringIO import StringIO
31
from bzrlib.branch import BranchReferenceFormat
32
from bzrlib.bzrdir import BzrDir, RemoteBzrDirFormat
33
from bzrlib.config import BranchConfig, TreeConfig
34
from bzrlib.decorators import needs_read_lock, needs_write_lock
35
from bzrlib.errors import NoSuchRevision
36
from bzrlib.lockable_files import LockableFiles
37
from bzrlib.pack import ContainerPushParser
38
from bzrlib.smart import client, vfs
39
from bzrlib.symbol_versioning import (
43
from bzrlib.revision import NULL_REVISION
44
from bzrlib.trace import mutter, note
46
# Note: RemoteBzrDirFormat is in bzrdir.py
48
class RemoteBzrDir(BzrDir):
49
"""Control directory on a remote server, accessed via bzr:// or similar."""
51
def __init__(self, transport, _client=None):
52
"""Construct a RemoteBzrDir.
54
:param _client: Private parameter for testing. Disables probing and the
57
BzrDir.__init__(self, transport, RemoteBzrDirFormat())
58
# this object holds a delegated bzrdir that uses file-level operations
59
# to talk to the other side
60
self._real_bzrdir = None
63
self._shared_medium = transport.get_shared_medium()
64
self._client = client._SmartClient(self._shared_medium)
66
self._client = _client
67
self._shared_medium = None
70
path = self._path_for_remote_call(self._client)
71
response = self._client.call('BzrDir.open', path)
72
if response not in [('yes',), ('no',)]:
73
raise errors.UnexpectedSmartServerResponse(response)
74
if response == ('no',):
75
raise errors.NotBranchError(path=transport.base)
77
def _ensure_real(self):
78
"""Ensure that there is a _real_bzrdir set.
80
Used before calls to self._real_bzrdir.
82
if not self._real_bzrdir:
83
self._real_bzrdir = BzrDir.open_from_transport(
84
self.root_transport, _server_formats=False)
86
def create_repository(self, shared=False):
88
self._real_bzrdir.create_repository(shared=shared)
89
return self.open_repository()
91
def destroy_repository(self):
92
"""See BzrDir.destroy_repository"""
94
self._real_bzrdir.destroy_repository()
96
def create_branch(self):
98
real_branch = self._real_bzrdir.create_branch()
99
return RemoteBranch(self, self.find_repository(), real_branch)
101
def destroy_branch(self):
102
"""See BzrDir.destroy_branch"""
104
self._real_bzrdir.destroy_branch()
106
def create_workingtree(self, revision_id=None, from_branch=None):
107
raise errors.NotLocalUrl(self.transport.base)
109
def find_branch_format(self):
110
"""Find the branch 'format' for this bzrdir.
112
This might be a synthetic object for e.g. RemoteBranch and SVN.
114
b = self.open_branch()
117
def get_branch_reference(self):
118
"""See BzrDir.get_branch_reference()."""
119
path = self._path_for_remote_call(self._client)
120
response = self._client.call('BzrDir.open_branch', path)
121
if response[0] == 'ok':
122
if response[1] == '':
123
# branch at this location.
126
# a branch reference, use the existing BranchReference logic.
128
elif response == ('nobranch',):
129
raise errors.NotBranchError(path=self.root_transport.base)
131
raise errors.UnexpectedSmartServerResponse(response)
133
def _get_tree_branch(self):
134
"""See BzrDir._get_tree_branch()."""
135
return None, self.open_branch()
137
def open_branch(self, _unsupported=False):
138
assert _unsupported == False, 'unsupported flag support not implemented yet.'
139
reference_url = self.get_branch_reference()
140
if reference_url is None:
141
# branch at this location.
142
return RemoteBranch(self, self.find_repository())
144
# a branch reference, use the existing BranchReference logic.
145
format = BranchReferenceFormat()
146
return format.open(self, _found=True, location=reference_url)
148
def open_repository(self):
149
path = self._path_for_remote_call(self._client)
150
response = self._client.call('BzrDir.find_repository', path)
151
assert response[0] in ('ok', 'norepository'), \
152
'unexpected response code %s' % (response,)
153
if response[0] == 'norepository':
154
raise errors.NoRepositoryPresent(self)
155
assert len(response) == 4, 'incorrect response length %s' % (response,)
156
if response[1] == '':
157
format = RemoteRepositoryFormat()
158
format.rich_root_data = (response[2] == 'yes')
159
format.supports_tree_reference = (response[3] == 'yes')
160
return RemoteRepository(self, format)
162
raise errors.NoRepositoryPresent(self)
164
def open_workingtree(self, recommend_upgrade=True):
166
if self._real_bzrdir.has_workingtree():
167
raise errors.NotLocalUrl(self.root_transport)
169
raise errors.NoWorkingTree(self.root_transport.base)
171
def _path_for_remote_call(self, client):
172
"""Return the path to be used for this bzrdir in a remote call."""
173
return client.remote_path_from_transport(self.root_transport)
175
def get_branch_transport(self, branch_format):
177
return self._real_bzrdir.get_branch_transport(branch_format)
179
def get_repository_transport(self, repository_format):
181
return self._real_bzrdir.get_repository_transport(repository_format)
183
def get_workingtree_transport(self, workingtree_format):
185
return self._real_bzrdir.get_workingtree_transport(workingtree_format)
187
def can_convert_format(self):
188
"""Upgrading of remote bzrdirs is not supported yet."""
191
def needs_format_conversion(self, format=None):
192
"""Upgrading of remote bzrdirs is not supported yet."""
195
def clone(self, url, revision_id=None, force_new_repo=False):
197
return self._real_bzrdir.clone(url, revision_id=revision_id,
198
force_new_repo=force_new_repo)
201
class RemoteRepositoryFormat(repository.RepositoryFormat):
202
"""Format for repositories accessed over a _SmartClient.
204
Instances of this repository are represented by RemoteRepository
207
The RemoteRepositoryFormat is parameterized during construction
208
to reflect the capabilities of the real, remote format. Specifically
209
the attributes rich_root_data and supports_tree_reference are set
210
on a per instance basis, and are not set (and should not be) at
214
_matchingbzrdir = RemoteBzrDirFormat
216
def initialize(self, a_bzrdir, shared=False):
217
assert isinstance(a_bzrdir, RemoteBzrDir), \
218
'%r is not a RemoteBzrDir' % (a_bzrdir,)
219
return a_bzrdir.create_repository(shared=shared)
221
def open(self, a_bzrdir):
222
assert isinstance(a_bzrdir, RemoteBzrDir)
223
return a_bzrdir.open_repository()
225
def get_format_description(self):
226
return 'bzr remote repository'
228
def __eq__(self, other):
229
return self.__class__ == other.__class__
231
def check_conversion_target(self, target_format):
232
if self.rich_root_data and not target_format.rich_root_data:
233
raise errors.BadConversionTarget(
234
'Does not support rich root data.', target_format)
235
if (self.supports_tree_reference and
236
not getattr(target_format, 'supports_tree_reference', False)):
237
raise errors.BadConversionTarget(
238
'Does not support nested trees', target_format)
241
class RemoteRepository(object):
242
"""Repository accessed over rpc.
244
For the moment most operations are performed using local transport-backed
248
def __init__(self, remote_bzrdir, format, real_repository=None, _client=None):
249
"""Create a RemoteRepository instance.
251
:param remote_bzrdir: The bzrdir hosting this repository.
252
:param format: The RemoteFormat object to use.
253
:param real_repository: If not None, a local implementation of the
254
repository logic for the repository, usually accessing the data
256
:param _client: Private testing parameter - override the smart client
257
to be used by the repository.
260
self._real_repository = real_repository
262
self._real_repository = None
263
self.bzrdir = remote_bzrdir
265
self._client = client._SmartClient(self.bzrdir._shared_medium)
267
self._client = _client
268
self._format = format
269
self._lock_mode = None
270
self._lock_token = None
272
self._leave_lock = False
273
# A cache of looked up revision parent data; reset at unlock time.
274
self._parents_map = None
276
# These depend on the actual remote format, so force them off for
277
# maximum compatibility. XXX: In future these should depend on the
278
# remote repository instance, but this is irrelevant until we perform
279
# reconcile via an RPC call.
280
self._reconcile_does_inventory_gc = False
281
self._reconcile_fixes_text_parents = False
282
self._reconcile_backsup_inventory = False
283
self.base = self.bzrdir.transport.base
286
return "%s(%s)" % (self.__class__.__name__, self.base)
290
def abort_write_group(self):
291
"""Complete a write group on the decorated repository.
293
Smart methods peform operations in a single step so this api
294
is not really applicable except as a compatibility thunk
295
for older plugins that don't use e.g. the CommitBuilder
299
return self._real_repository.abort_write_group()
301
def commit_write_group(self):
302
"""Complete a write group on the decorated repository.
304
Smart methods peform operations in a single step so this api
305
is not really applicable except as a compatibility thunk
306
for older plugins that don't use e.g. the CommitBuilder
310
return self._real_repository.commit_write_group()
312
def _ensure_real(self):
313
"""Ensure that there is a _real_repository set.
315
Used before calls to self._real_repository.
317
if not self._real_repository:
318
self.bzrdir._ensure_real()
319
#self._real_repository = self.bzrdir._real_bzrdir.open_repository()
320
self._set_real_repository(self.bzrdir._real_bzrdir.open_repository())
322
def find_text_key_references(self):
323
"""Find the text key references within the repository.
325
:return: a dictionary mapping (file_id, revision_id) tuples to altered file-ids to an iterable of
326
revision_ids. Each altered file-ids has the exact revision_ids that
327
altered it listed explicitly.
328
:return: A dictionary mapping text keys ((fileid, revision_id) tuples)
329
to whether they were referred to by the inventory of the
330
revision_id that they contain. The inventory texts from all present
331
revision ids are assessed to generate this report.
334
return self._real_repository.find_text_key_references()
336
def _generate_text_key_index(self):
337
"""Generate a new text key index for the repository.
339
This is an expensive function that will take considerable time to run.
341
:return: A dict mapping (file_id, revision_id) tuples to a list of
342
parents, also (file_id, revision_id) tuples.
345
return self._real_repository._generate_text_key_index()
347
def get_revision_graph(self, revision_id=None):
348
"""See Repository.get_revision_graph()."""
349
if revision_id is None:
351
elif revision.is_null(revision_id):
354
path = self.bzrdir._path_for_remote_call(self._client)
355
assert type(revision_id) is str
356
response = self._client.call_expecting_body(
357
'Repository.get_revision_graph', path, revision_id)
358
if response[0][0] not in ['ok', 'nosuchrevision']:
359
raise errors.UnexpectedSmartServerResponse(response[0])
360
if response[0][0] == 'ok':
361
coded = response[1].read_body_bytes()
363
# no revisions in this repository!
365
lines = coded.split('\n')
368
d = tuple(line.split())
369
revision_graph[d[0]] = d[1:]
371
return revision_graph
373
response_body = response[1].read_body_bytes()
374
assert response_body == ''
375
raise NoSuchRevision(self, revision_id)
377
def has_revision(self, revision_id):
378
"""See Repository.has_revision()."""
379
if revision_id == NULL_REVISION:
380
# The null revision is always present.
382
path = self.bzrdir._path_for_remote_call(self._client)
383
response = self._client.call('Repository.has_revision', path, revision_id)
384
assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
385
return response[0] == 'yes'
387
def has_revisions(self, revision_ids):
388
"""See Repository.has_revisions()."""
390
for revision_id in revision_ids:
391
if self.has_revision(revision_id):
392
result.add(revision_id)
395
def has_same_location(self, other):
396
return (self.__class__ == other.__class__ and
397
self.bzrdir.transport.base == other.bzrdir.transport.base)
399
def get_graph(self, other_repository=None):
400
"""Return the graph for this repository format"""
401
parents_provider = self
402
if (other_repository is not None and
403
other_repository.bzrdir.transport.base !=
404
self.bzrdir.transport.base):
405
parents_provider = graph._StackedParentsProvider(
406
[parents_provider, other_repository._make_parents_provider()])
407
return graph.Graph(parents_provider)
409
def gather_stats(self, revid=None, committers=None):
410
"""See Repository.gather_stats()."""
411
path = self.bzrdir._path_for_remote_call(self._client)
412
# revid can be None to indicate no revisions, not just NULL_REVISION
413
if revid is None or revision.is_null(revid):
417
if committers is None or not committers:
418
fmt_committers = 'no'
420
fmt_committers = 'yes'
421
response = self._client.call_expecting_body(
422
'Repository.gather_stats', path, fmt_revid, fmt_committers)
423
assert response[0][0] == 'ok', \
424
'unexpected response code %s' % (response[0],)
426
body = response[1].read_body_bytes()
428
for line in body.split('\n'):
431
key, val_text = line.split(':')
432
if key in ('revisions', 'size', 'committers'):
433
result[key] = int(val_text)
434
elif key in ('firstrev', 'latestrev'):
435
values = val_text.split(' ')[1:]
436
result[key] = (float(values[0]), long(values[1]))
440
def find_branches(self, using=False):
441
"""See Repository.find_branches()."""
442
# should be an API call to the server.
444
return self._real_repository.find_branches(using=using)
446
def get_physical_lock_status(self):
447
"""See Repository.get_physical_lock_status()."""
448
# should be an API call to the server.
450
return self._real_repository.get_physical_lock_status()
452
def is_in_write_group(self):
453
"""Return True if there is an open write group.
455
write groups are only applicable locally for the smart server..
457
if self._real_repository:
458
return self._real_repository.is_in_write_group()
461
return self._lock_count >= 1
464
"""See Repository.is_shared()."""
465
path = self.bzrdir._path_for_remote_call(self._client)
466
response = self._client.call('Repository.is_shared', path)
467
assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
468
return response[0] == 'yes'
470
def is_write_locked(self):
471
return self._lock_mode == 'w'
474
# wrong eventually - want a local lock cache context
475
if not self._lock_mode:
476
self._lock_mode = 'r'
478
self._parents_map = {}
479
if self._real_repository is not None:
480
self._real_repository.lock_read()
482
self._lock_count += 1
484
def _remote_lock_write(self, token):
485
path = self.bzrdir._path_for_remote_call(self._client)
488
response = self._client.call('Repository.lock_write', path, token)
489
if response[0] == 'ok':
492
elif response[0] == 'LockContention':
493
raise errors.LockContention('(remote lock)')
494
elif response[0] == 'UnlockableTransport':
495
raise errors.UnlockableTransport(self.bzrdir.root_transport)
496
elif response[0] == 'LockFailed':
497
raise errors.LockFailed(response[1], response[2])
499
raise errors.UnexpectedSmartServerResponse(response)
501
def lock_write(self, token=None):
502
if not self._lock_mode:
503
self._lock_token = self._remote_lock_write(token)
504
# if self._lock_token is None, then this is something like packs or
505
# svn where we don't get to lock the repo, or a weave style repository
506
# where we cannot lock it over the wire and attempts to do so will
508
if self._real_repository is not None:
509
self._real_repository.lock_write(token=self._lock_token)
510
if token is not None:
511
self._leave_lock = True
513
self._leave_lock = False
514
self._lock_mode = 'w'
516
self._parents_map = {}
517
elif self._lock_mode == 'r':
518
raise errors.ReadOnlyError(self)
520
self._lock_count += 1
521
return self._lock_token or None
523
def leave_lock_in_place(self):
524
if not self._lock_token:
525
raise NotImplementedError(self.leave_lock_in_place)
526
self._leave_lock = True
528
def dont_leave_lock_in_place(self):
529
if not self._lock_token:
530
raise NotImplementedError(self.dont_leave_lock_in_place)
531
self._leave_lock = False
533
def _set_real_repository(self, repository):
534
"""Set the _real_repository for this repository.
536
:param repository: The repository to fallback to for non-hpss
537
implemented operations.
539
assert not isinstance(repository, RemoteRepository)
540
self._real_repository = repository
541
if self._lock_mode == 'w':
542
# if we are already locked, the real repository must be able to
543
# acquire the lock with our token.
544
self._real_repository.lock_write(self._lock_token)
545
elif self._lock_mode == 'r':
546
self._real_repository.lock_read()
548
def start_write_group(self):
549
"""Start a write group on the decorated repository.
551
Smart methods peform operations in a single step so this api
552
is not really applicable except as a compatibility thunk
553
for older plugins that don't use e.g. the CommitBuilder
557
return self._real_repository.start_write_group()
559
def _unlock(self, token):
560
path = self.bzrdir._path_for_remote_call(self._client)
562
# with no token the remote repository is not persistently locked.
564
response = self._client.call('Repository.unlock', path, token)
565
if response == ('ok',):
567
elif response[0] == 'TokenMismatch':
568
raise errors.TokenMismatch(token, '(remote token)')
570
raise errors.UnexpectedSmartServerResponse(response)
573
self._lock_count -= 1
574
if self._lock_count > 0:
576
self._parents_map = None
577
old_mode = self._lock_mode
578
self._lock_mode = None
580
# The real repository is responsible at present for raising an
581
# exception if it's in an unfinished write group. However, it
582
# normally will *not* actually remove the lock from disk - that's
583
# done by the server on receiving the Repository.unlock call.
584
# This is just to let the _real_repository stay up to date.
585
if self._real_repository is not None:
586
self._real_repository.unlock()
588
# The rpc-level lock should be released even if there was a
589
# problem releasing the vfs-based lock.
591
# Only write-locked repositories need to make a remote method
592
# call to perfom the unlock.
593
old_token = self._lock_token
594
self._lock_token = None
595
if not self._leave_lock:
596
self._unlock(old_token)
598
def break_lock(self):
599
# should hand off to the network
601
return self._real_repository.break_lock()
603
def _get_tarball(self, compression):
604
"""Return a TemporaryFile containing a repository tarball.
606
Returns None if the server does not support sending tarballs.
609
path = self.bzrdir._path_for_remote_call(self._client)
610
response, protocol = self._client.call_expecting_body(
611
'Repository.tarball', path, compression)
612
if response[0] == 'ok':
613
# Extract the tarball and return it
614
t = tempfile.NamedTemporaryFile()
615
# TODO: rpc layer should read directly into it...
616
t.write(protocol.read_body_bytes())
619
if (response == ('error', "Generic bzr smart protocol error: "
620
"bad request 'Repository.tarball'") or
621
response == ('error', "Generic bzr smart protocol error: "
622
"bad request u'Repository.tarball'")):
623
protocol.cancel_read_body()
625
raise errors.UnexpectedSmartServerResponse(response)
627
def sprout(self, to_bzrdir, revision_id=None):
628
# TODO: Option to control what format is created?
630
dest_repo = self._real_repository._format.initialize(to_bzrdir,
632
dest_repo.fetch(self, revision_id=revision_id)
635
### These methods are just thin shims to the VFS object for now.
637
def revision_tree(self, revision_id):
639
return self._real_repository.revision_tree(revision_id)
641
def get_serializer_format(self):
643
return self._real_repository.get_serializer_format()
645
def get_commit_builder(self, branch, parents, config, timestamp=None,
646
timezone=None, committer=None, revprops=None,
648
# FIXME: It ought to be possible to call this without immediately
649
# triggering _ensure_real. For now it's the easiest thing to do.
651
builder = self._real_repository.get_commit_builder(branch, parents,
652
config, timestamp=timestamp, timezone=timezone,
653
committer=committer, revprops=revprops, revision_id=revision_id)
656
def add_inventory(self, revid, inv, parents):
658
return self._real_repository.add_inventory(revid, inv, parents)
660
def add_revision(self, rev_id, rev, inv=None, config=None):
662
return self._real_repository.add_revision(
663
rev_id, rev, inv=inv, config=config)
666
def get_inventory(self, revision_id):
668
return self._real_repository.get_inventory(revision_id)
670
def iter_inventories(self, revision_ids):
672
return self._real_repository.iter_inventories(revision_ids)
675
def get_revision(self, revision_id):
677
return self._real_repository.get_revision(revision_id)
680
def weave_store(self):
682
return self._real_repository.weave_store
684
def get_transaction(self):
686
return self._real_repository.get_transaction()
689
def clone(self, a_bzrdir, revision_id=None):
691
return self._real_repository.clone(a_bzrdir, revision_id=revision_id)
693
def make_working_trees(self):
694
"""RemoteRepositories never create working trees by default."""
697
def revision_ids_to_search_result(self, result_set):
698
"""Convert a set of revision ids to a graph SearchResult."""
699
result_parents = set()
700
for parents in self.get_graph().get_parent_map(
701
result_set).itervalues():
702
result_parents.update(parents)
703
included_keys = result_set.intersection(result_parents)
704
start_keys = result_set.difference(included_keys)
705
exclude_keys = result_parents.difference(result_set)
706
result = graph.SearchResult(start_keys, exclude_keys,
707
len(result_set), result_set)
711
def search_missing_revision_ids(self, other, revision_id=None, find_ghosts=True):
712
"""Return the revision ids that other has that this does not.
714
These are returned in topological order.
716
revision_id: only return revision ids included by revision_id.
718
return repository.InterRepository.get(
719
other, self).search_missing_revision_ids(revision_id, find_ghosts)
721
def fetch(self, source, revision_id=None, pb=None):
722
if self.has_same_location(source):
723
# check that last_revision is in 'from' and then return a
725
if (revision_id is not None and
726
not revision.is_null(revision_id)):
727
self.get_revision(revision_id)
730
return self._real_repository.fetch(
731
source, revision_id=revision_id, pb=pb)
733
def create_bundle(self, target, base, fileobj, format=None):
735
self._real_repository.create_bundle(target, base, fileobj, format)
738
def control_weaves(self):
740
return self._real_repository.control_weaves
743
def get_ancestry(self, revision_id, topo_sorted=True):
745
return self._real_repository.get_ancestry(revision_id, topo_sorted)
748
def get_inventory_weave(self):
750
return self._real_repository.get_inventory_weave()
752
def fileids_altered_by_revision_ids(self, revision_ids):
754
return self._real_repository.fileids_altered_by_revision_ids(revision_ids)
756
def _get_versioned_file_checker(self, revisions, revision_versions_cache):
758
return self._real_repository._get_versioned_file_checker(
759
revisions, revision_versions_cache)
761
def iter_files_bytes(self, desired_files):
762
"""See Repository.iter_file_bytes.
765
return self._real_repository.iter_files_bytes(desired_files)
767
def get_parent_map(self, keys):
768
"""See bzrlib.Graph.get_parent_map()."""
769
# Hack to build up the caching logic.
770
ancestry = self._parents_map
771
missing_revisions = set(key for key in keys if key not in ancestry)
772
if missing_revisions:
773
parent_map = self._get_parent_map(missing_revisions)
774
if 'hpss' in debug.debug_flags:
775
mutter('retransmitted revisions: %d of %d',
776
len(set(self._parents_map).intersection(parent_map)),
778
self._parents_map.update(parent_map)
779
return dict((k, ancestry[k]) for k in keys if k in ancestry)
781
def _response_is_unknown_method(self, response, verb):
782
"""Return True if response is an unknonwn method response to verb.
784
:param response: The response from a smart client call_expecting_body
786
:param verb: The verb used in that call.
787
:return: True if an unknown method was encountered.
789
# This might live better on
790
# bzrlib.smart.protocol.SmartClientRequestProtocolOne
791
if (response[0] == ('error', "Generic bzr smart protocol error: "
792
"bad request '%s'" % verb) or
793
response[0] == ('error', "Generic bzr smart protocol error: "
794
"bad request u'%s'" % verb)):
795
response[1].cancel_read_body()
799
def _get_parent_map(self, keys):
800
"""Helper for get_parent_map that performs the RPC."""
802
if NULL_REVISION in keys:
803
keys.discard(NULL_REVISION)
804
found_parents = {NULL_REVISION:()}
809
path = self.bzrdir._path_for_remote_call(self._client)
811
assert type(key) is str
812
verb = 'Repository.get_parent_map'
813
response = self._client.call_expecting_body(
815
if self._response_is_unknown_method(response, verb):
816
# Server that does not support this method, get the whole graph.
817
response = self._client.call_expecting_body(
818
'Repository.get_revision_graph', path, '')
819
if response[0][0] not in ['ok', 'nosuchrevision']:
820
reponse[1].cancel_read_body()
821
raise errors.UnexpectedSmartServerResponse(response[0])
822
elif response[0][0] not in ['ok']:
823
reponse[1].cancel_read_body()
824
raise errors.UnexpectedSmartServerResponse(response[0])
825
if response[0][0] == 'ok':
826
coded = response[1].read_body_bytes()
830
lines = coded.split('\n')
833
d = tuple(line.split())
835
revision_graph[d[0]] = d[1:]
837
# No parents - so give the Graph result (NULL_REVISION,).
838
revision_graph[d[0]] = (NULL_REVISION,)
839
return revision_graph
842
def get_signature_text(self, revision_id):
844
return self._real_repository.get_signature_text(revision_id)
847
def get_revision_graph_with_ghosts(self, revision_ids=None):
849
return self._real_repository.get_revision_graph_with_ghosts(
850
revision_ids=revision_ids)
853
def get_inventory_xml(self, revision_id):
855
return self._real_repository.get_inventory_xml(revision_id)
857
def deserialise_inventory(self, revision_id, xml):
859
return self._real_repository.deserialise_inventory(revision_id, xml)
861
def reconcile(self, other=None, thorough=False):
863
return self._real_repository.reconcile(other=other, thorough=thorough)
865
def all_revision_ids(self):
867
return self._real_repository.all_revision_ids()
870
def get_deltas_for_revisions(self, revisions):
872
return self._real_repository.get_deltas_for_revisions(revisions)
875
def get_revision_delta(self, revision_id):
877
return self._real_repository.get_revision_delta(revision_id)
880
def revision_trees(self, revision_ids):
882
return self._real_repository.revision_trees(revision_ids)
885
def get_revision_reconcile(self, revision_id):
887
return self._real_repository.get_revision_reconcile(revision_id)
890
def check(self, revision_ids=None):
892
return self._real_repository.check(revision_ids=revision_ids)
894
def copy_content_into(self, destination, revision_id=None):
896
return self._real_repository.copy_content_into(
897
destination, revision_id=revision_id)
899
def _copy_repository_tarball(self, to_bzrdir, revision_id=None):
900
# get a tarball of the remote repository, and copy from that into the
902
from bzrlib import osutils
905
# TODO: Maybe a progress bar while streaming the tarball?
906
note("Copying repository content as tarball...")
907
tar_file = self._get_tarball('bz2')
910
destination = to_bzrdir.create_repository()
912
tar = tarfile.open('repository', fileobj=tar_file,
914
tmpdir = tempfile.mkdtemp()
916
_extract_tar(tar, tmpdir)
917
tmp_bzrdir = BzrDir.open(tmpdir)
918
tmp_repo = tmp_bzrdir.open_repository()
919
tmp_repo.copy_content_into(destination, revision_id)
921
osutils.rmtree(tmpdir)
925
# TODO: Suggestion from john: using external tar is much faster than
926
# python's tarfile library, but it may not work on windows.
930
"""Compress the data within the repository.
932
This is not currently implemented within the smart server.
935
return self._real_repository.pack()
937
def set_make_working_trees(self, new_value):
938
raise NotImplementedError(self.set_make_working_trees)
941
def sign_revision(self, revision_id, gpg_strategy):
943
return self._real_repository.sign_revision(revision_id, gpg_strategy)
946
def get_revisions(self, revision_ids):
948
return self._real_repository.get_revisions(revision_ids)
950
def supports_rich_root(self):
952
return self._real_repository.supports_rich_root()
954
def iter_reverse_revision_history(self, revision_id):
956
return self._real_repository.iter_reverse_revision_history(revision_id)
959
def _serializer(self):
961
return self._real_repository._serializer
963
def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
965
return self._real_repository.store_revision_signature(
966
gpg_strategy, plaintext, revision_id)
968
def add_signature_text(self, revision_id, signature):
970
return self._real_repository.add_signature_text(revision_id, signature)
972
def has_signature_for_revision_id(self, revision_id):
974
return self._real_repository.has_signature_for_revision_id(revision_id)
976
def get_data_stream_for_search(self, search):
977
REQUEST_NAME = 'Repository.stream_revisions_chunked'
978
path = self.bzrdir._path_for_remote_call(self._client)
979
recipe = search.get_recipe()
980
start_keys = ' '.join(recipe[0])
981
stop_keys = ' '.join(recipe[1])
982
count = str(recipe[2])
983
body = '\n'.join((start_keys, stop_keys, count))
984
response, protocol = self._client.call_with_body_bytes_expecting_body(
985
REQUEST_NAME, (path,), body)
987
if response == ('ok',):
988
return self._deserialise_stream(protocol)
989
if response == ('NoSuchRevision', ):
990
# We cannot easily identify the revision that is missing in this
991
# situation without doing much more network IO. For now, bail.
992
raise NoSuchRevision(self, "unknown")
993
elif (response == ('error', "Generic bzr smart protocol error: "
994
"bad request '%s'" % REQUEST_NAME) or
995
response == ('error', "Generic bzr smart protocol error: "
996
"bad request u'%s'" % REQUEST_NAME)):
997
protocol.cancel_read_body()
999
return self._real_repository.get_data_stream_for_search(search)
1001
raise errors.UnexpectedSmartServerResponse(response)
1003
def _deserialise_stream(self, protocol):
1004
stream = protocol.read_streamed_body()
1005
container_parser = ContainerPushParser()
1006
for bytes in stream:
1007
container_parser.accept_bytes(bytes)
1008
records = container_parser.read_pending_records()
1009
for record_names, record_bytes in records:
1010
if len(record_names) != 1:
1011
# These records should have only one name, and that name
1012
# should be a one-element tuple.
1013
raise errors.SmartProtocolError(
1014
'Repository data stream had invalid record name %r'
1016
name_tuple = record_names[0]
1017
yield name_tuple, record_bytes
1019
def insert_data_stream(self, stream):
1021
self._real_repository.insert_data_stream(stream)
1023
def item_keys_introduced_by(self, revision_ids, _files_pb=None):
1025
return self._real_repository.item_keys_introduced_by(revision_ids,
1026
_files_pb=_files_pb)
1028
def revision_graph_can_have_wrong_parents(self):
1029
# The answer depends on the remote repo format.
1031
return self._real_repository.revision_graph_can_have_wrong_parents()
1033
def _find_inconsistent_revision_parents(self):
1035
return self._real_repository._find_inconsistent_revision_parents()
1037
def _check_for_inconsistent_revision_parents(self):
1039
return self._real_repository._check_for_inconsistent_revision_parents()
1041
def _make_parents_provider(self):
1045
class RemoteBranchLockableFiles(LockableFiles):
1046
"""A 'LockableFiles' implementation that talks to a smart server.
1048
This is not a public interface class.
1051
def __init__(self, bzrdir, _client):
1052
self.bzrdir = bzrdir
1053
self._client = _client
1054
self._need_find_modes = True
1055
LockableFiles.__init__(
1056
self, bzrdir.get_branch_transport(None),
1057
'lock', lockdir.LockDir)
1059
def _find_modes(self):
1060
# RemoteBranches don't let the client set the mode of control files.
1061
self._dir_mode = None
1062
self._file_mode = None
1064
def get(self, path):
1065
"""'get' a remote path as per the LockableFiles interface.
1067
:param path: the file to 'get'. If this is 'branch.conf', we do not
1068
just retrieve a file, instead we ask the smart server to generate
1069
a configuration for us - which is retrieved as an INI file.
1071
if path == 'branch.conf':
1072
path = self.bzrdir._path_for_remote_call(self._client)
1073
response = self._client.call_expecting_body(
1074
'Branch.get_config_file', path)
1075
assert response[0][0] == 'ok', \
1076
'unexpected response code %s' % (response[0],)
1077
return StringIO(response[1].read_body_bytes())
1080
return LockableFiles.get(self, path)
1083
class RemoteBranchFormat(branch.BranchFormat):
1085
def __eq__(self, other):
1086
return (isinstance(other, RemoteBranchFormat) and
1087
self.__dict__ == other.__dict__)
1089
def get_format_description(self):
1090
return 'Remote BZR Branch'
1092
def get_format_string(self):
1093
return 'Remote BZR Branch'
1095
def open(self, a_bzrdir):
1096
assert isinstance(a_bzrdir, RemoteBzrDir)
1097
return a_bzrdir.open_branch()
1099
def initialize(self, a_bzrdir):
1100
assert isinstance(a_bzrdir, RemoteBzrDir)
1101
return a_bzrdir.create_branch()
1103
def supports_tags(self):
1104
# Remote branches might support tags, but we won't know until we
1105
# access the real remote branch.
1109
class RemoteBranch(branch.Branch):
1110
"""Branch stored on a server accessed by HPSS RPC.
1112
At the moment most operations are mapped down to simple file operations.
1115
def __init__(self, remote_bzrdir, remote_repository, real_branch=None,
1117
"""Create a RemoteBranch instance.
1119
:param real_branch: An optional local implementation of the branch
1120
format, usually accessing the data via the VFS.
1121
:param _client: Private parameter for testing.
1123
# We intentionally don't call the parent class's __init__, because it
1124
# will try to assign to self.tags, which is a property in this subclass.
1125
# And the parent's __init__ doesn't do much anyway.
1126
self._revision_id_to_revno_cache = None
1127
self._revision_history_cache = None
1128
self.bzrdir = remote_bzrdir
1129
if _client is not None:
1130
self._client = _client
1132
self._client = client._SmartClient(self.bzrdir._shared_medium)
1133
self.repository = remote_repository
1134
if real_branch is not None:
1135
self._real_branch = real_branch
1136
# Give the remote repository the matching real repo.
1137
real_repo = self._real_branch.repository
1138
if isinstance(real_repo, RemoteRepository):
1139
real_repo._ensure_real()
1140
real_repo = real_repo._real_repository
1141
self.repository._set_real_repository(real_repo)
1142
# Give the branch the remote repository to let fast-pathing happen.
1143
self._real_branch.repository = self.repository
1145
self._real_branch = None
1146
# Fill out expected attributes of branch for bzrlib api users.
1147
self._format = RemoteBranchFormat()
1148
self.base = self.bzrdir.root_transport.base
1149
self._control_files = None
1150
self._lock_mode = None
1151
self._lock_token = None
1152
self._lock_count = 0
1153
self._leave_lock = False
1156
return "%s(%s)" % (self.__class__.__name__, self.base)
1160
def _ensure_real(self):
1161
"""Ensure that there is a _real_branch set.
1163
Used before calls to self._real_branch.
1165
if not self._real_branch:
1166
assert vfs.vfs_enabled()
1167
self.bzrdir._ensure_real()
1168
self._real_branch = self.bzrdir._real_bzrdir.open_branch()
1169
# Give the remote repository the matching real repo.
1170
real_repo = self._real_branch.repository
1171
if isinstance(real_repo, RemoteRepository):
1172
real_repo._ensure_real()
1173
real_repo = real_repo._real_repository
1174
self.repository._set_real_repository(real_repo)
1175
# Give the branch the remote repository to let fast-pathing happen.
1176
self._real_branch.repository = self.repository
1177
# XXX: deal with _lock_mode == 'w'
1178
if self._lock_mode == 'r':
1179
self._real_branch.lock_read()
1182
def control_files(self):
1183
# Defer actually creating RemoteBranchLockableFiles until its needed,
1184
# because it triggers an _ensure_real that we otherwise might not need.
1185
if self._control_files is None:
1186
self._control_files = RemoteBranchLockableFiles(
1187
self.bzrdir, self._client)
1188
return self._control_files
1190
def _get_checkout_format(self):
1192
return self._real_branch._get_checkout_format()
1194
def get_physical_lock_status(self):
1195
"""See Branch.get_physical_lock_status()."""
1196
# should be an API call to the server, as branches must be lockable.
1198
return self._real_branch.get_physical_lock_status()
1200
def lock_read(self):
1201
if not self._lock_mode:
1202
self._lock_mode = 'r'
1203
self._lock_count = 1
1204
if self._real_branch is not None:
1205
self._real_branch.lock_read()
1207
self._lock_count += 1
1209
def _remote_lock_write(self, token):
1211
branch_token = repo_token = ''
1213
branch_token = token
1214
repo_token = self.repository.lock_write()
1215
self.repository.unlock()
1216
path = self.bzrdir._path_for_remote_call(self._client)
1217
response = self._client.call('Branch.lock_write', path, branch_token,
1219
if response[0] == 'ok':
1220
ok, branch_token, repo_token = response
1221
return branch_token, repo_token
1222
elif response[0] == 'LockContention':
1223
raise errors.LockContention('(remote lock)')
1224
elif response[0] == 'TokenMismatch':
1225
raise errors.TokenMismatch(token, '(remote token)')
1226
elif response[0] == 'UnlockableTransport':
1227
raise errors.UnlockableTransport(self.bzrdir.root_transport)
1228
elif response[0] == 'ReadOnlyError':
1229
raise errors.ReadOnlyError(self)
1230
elif response[0] == 'LockFailed':
1231
raise errors.LockFailed(response[1], response[2])
1233
raise errors.UnexpectedSmartServerResponse(response)
1235
def lock_write(self, token=None):
1236
if not self._lock_mode:
1237
remote_tokens = self._remote_lock_write(token)
1238
self._lock_token, self._repo_lock_token = remote_tokens
1239
assert self._lock_token, 'Remote server did not return a token!'
1240
# TODO: We really, really, really don't want to call _ensure_real
1241
# here, but it's the easiest way to ensure coherency between the
1242
# state of the RemoteBranch and RemoteRepository objects and the
1243
# physical locks. If we don't materialise the real objects here,
1244
# then getting everything in the right state later is complex, so
1245
# for now we just do it the lazy way.
1246
# -- Andrew Bennetts, 2007-02-22.
1248
if self._real_branch is not None:
1249
self._real_branch.repository.lock_write(
1250
token=self._repo_lock_token)
1252
self._real_branch.lock_write(token=self._lock_token)
1254
self._real_branch.repository.unlock()
1255
if token is not None:
1256
self._leave_lock = True
1258
# XXX: this case seems to be unreachable; token cannot be None.
1259
self._leave_lock = False
1260
self._lock_mode = 'w'
1261
self._lock_count = 1
1262
elif self._lock_mode == 'r':
1263
raise errors.ReadOnlyTransaction
1265
if token is not None:
1266
# A token was given to lock_write, and we're relocking, so check
1267
# that the given token actually matches the one we already have.
1268
if token != self._lock_token:
1269
raise errors.TokenMismatch(token, self._lock_token)
1270
self._lock_count += 1
1271
return self._lock_token or None
1273
def _unlock(self, branch_token, repo_token):
1274
path = self.bzrdir._path_for_remote_call(self._client)
1275
response = self._client.call('Branch.unlock', path, branch_token,
1277
if response == ('ok',):
1279
elif response[0] == 'TokenMismatch':
1280
raise errors.TokenMismatch(
1281
str((branch_token, repo_token)), '(remote tokens)')
1283
raise errors.UnexpectedSmartServerResponse(response)
1286
self._lock_count -= 1
1287
if not self._lock_count:
1288
self._clear_cached_state()
1289
mode = self._lock_mode
1290
self._lock_mode = None
1291
if self._real_branch is not None:
1292
if (not self._leave_lock and mode == 'w' and
1293
self._repo_lock_token):
1294
# If this RemoteBranch will remove the physical lock for the
1295
# repository, make sure the _real_branch doesn't do it
1296
# first. (Because the _real_branch's repository is set to
1297
# be the RemoteRepository.)
1298
self._real_branch.repository.leave_lock_in_place()
1299
self._real_branch.unlock()
1301
# Only write-locked branched need to make a remote method call
1302
# to perfom the unlock.
1304
assert self._lock_token, 'Locked, but no token!'
1305
branch_token = self._lock_token
1306
repo_token = self._repo_lock_token
1307
self._lock_token = None
1308
self._repo_lock_token = None
1309
if not self._leave_lock:
1310
self._unlock(branch_token, repo_token)
1312
def break_lock(self):
1314
return self._real_branch.break_lock()
1316
def leave_lock_in_place(self):
1317
if not self._lock_token:
1318
raise NotImplementedError(self.leave_lock_in_place)
1319
self._leave_lock = True
1321
def dont_leave_lock_in_place(self):
1322
if not self._lock_token:
1323
raise NotImplementedError(self.dont_leave_lock_in_place)
1324
self._leave_lock = False
1326
def last_revision_info(self):
1327
"""See Branch.last_revision_info()."""
1328
path = self.bzrdir._path_for_remote_call(self._client)
1329
response = self._client.call('Branch.last_revision_info', path)
1330
assert response[0] == 'ok', 'unexpected response code %s' % (response,)
1331
revno = int(response[1])
1332
last_revision = response[2]
1333
return (revno, last_revision)
1335
def _gen_revision_history(self):
1336
"""See Branch._gen_revision_history()."""
1337
path = self.bzrdir._path_for_remote_call(self._client)
1338
response = self._client.call_expecting_body(
1339
'Branch.revision_history', path)
1340
assert response[0][0] == 'ok', ('unexpected response code %s'
1342
result = response[1].read_body_bytes().split('\x00')
1348
def set_revision_history(self, rev_history):
1349
# Send just the tip revision of the history; the server will generate
1350
# the full history from that. If the revision doesn't exist in this
1351
# branch, NoSuchRevision will be raised.
1352
path = self.bzrdir._path_for_remote_call(self._client)
1353
if rev_history == []:
1356
rev_id = rev_history[-1]
1357
self._clear_cached_state()
1358
response = self._client.call('Branch.set_last_revision',
1359
path, self._lock_token, self._repo_lock_token, rev_id)
1360
if response[0] == 'NoSuchRevision':
1361
raise NoSuchRevision(self, rev_id)
1363
assert response == ('ok',), (
1364
'unexpected response code %r' % (response,))
1365
self._cache_revision_history(rev_history)
1367
def get_parent(self):
1369
return self._real_branch.get_parent()
1371
def set_parent(self, url):
1373
return self._real_branch.set_parent(url)
1375
def get_config(self):
1376
return RemoteBranchConfig(self)
1378
def sprout(self, to_bzrdir, revision_id=None):
1379
# Like Branch.sprout, except that it sprouts a branch in the default
1380
# format, because RemoteBranches can't be created at arbitrary URLs.
1381
# XXX: if to_bzrdir is a RemoteBranch, this should perhaps do
1382
# to_bzrdir.create_branch...
1384
result = self._real_branch._format.initialize(to_bzrdir)
1385
self.copy_content_into(result, revision_id=revision_id)
1386
result.set_parent(self.bzrdir.root_transport.base)
1390
def pull(self, source, overwrite=False, stop_revision=None,
1392
# FIXME: This asks the real branch to run the hooks, which means
1393
# they're called with the wrong target branch parameter.
1394
# The test suite specifically allows this at present but it should be
1395
# fixed. It should get a _override_hook_target branch,
1396
# as push does. -- mbp 20070405
1398
self._real_branch.pull(
1399
source, overwrite=overwrite, stop_revision=stop_revision,
1403
def push(self, target, overwrite=False, stop_revision=None):
1405
return self._real_branch.push(
1406
target, overwrite=overwrite, stop_revision=stop_revision,
1407
_override_hook_source_branch=self)
1409
def is_locked(self):
1410
return self._lock_count >= 1
1412
def set_last_revision_info(self, revno, revision_id):
1414
self._clear_cached_state()
1415
return self._real_branch.set_last_revision_info(revno, revision_id)
1417
def generate_revision_history(self, revision_id, last_rev=None,
1420
return self._real_branch.generate_revision_history(
1421
revision_id, last_rev=last_rev, other_branch=other_branch)
1426
return self._real_branch.tags
1428
def set_push_location(self, location):
1430
return self._real_branch.set_push_location(location)
1432
def update_revisions(self, other, stop_revision=None, overwrite=False):
1434
return self._real_branch.update_revisions(
1435
other, stop_revision=stop_revision, overwrite=overwrite)
1438
class RemoteBranchConfig(BranchConfig):
1441
self.branch._ensure_real()
1442
return self.branch._real_branch.get_config().username()
1444
def _get_branch_data_config(self):
1445
self.branch._ensure_real()
1446
if self._branch_data_config is None:
1447
self._branch_data_config = TreeConfig(self.branch._real_branch)
1448
return self._branch_data_config
1451
def _extract_tar(tar, to_dir):
1452
"""Extract all the contents of a tarfile object.
1454
A replacement for extractall, which is not present in python2.4
1457
tar.extract(tarinfo, to_dir)