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
29
from bzrlib.branch import BranchReferenceFormat
30
from bzrlib.bzrdir import BzrDir, RemoteBzrDirFormat
31
from bzrlib.config import BranchConfig, TreeConfig
32
from bzrlib.decorators import needs_read_lock, needs_write_lock
33
from bzrlib.errors import NoSuchRevision
34
from bzrlib.lockable_files import LockableFiles
35
from bzrlib.pack import ContainerPushParser
36
from bzrlib.smart import client, vfs
37
from bzrlib.symbol_versioning import (
41
from bzrlib.revision import NULL_REVISION
42
from bzrlib.trace import note
44
# Note: RemoteBzrDirFormat is in bzrdir.py
46
class RemoteBzrDir(BzrDir):
47
"""Control directory on a remote server, accessed via bzr:// or similar."""
49
def __init__(self, transport, _client=None):
50
"""Construct a RemoteBzrDir.
52
:param _client: Private parameter for testing. Disables probing and the
55
BzrDir.__init__(self, transport, RemoteBzrDirFormat())
56
# this object holds a delegated bzrdir that uses file-level operations
57
# to talk to the other side
58
self._real_bzrdir = None
61
self._shared_medium = transport.get_shared_medium()
62
self._client = client._SmartClient(self._shared_medium)
64
self._client = _client
65
self._shared_medium = None
68
path = self._path_for_remote_call(self._client)
69
response = self._client.call('BzrDir.open', path)
70
if response not in [('yes',), ('no',)]:
71
raise errors.UnexpectedSmartServerResponse(response)
72
if response == ('no',):
73
raise errors.NotBranchError(path=transport.base)
75
def _ensure_real(self):
76
"""Ensure that there is a _real_bzrdir set.
78
Used before calls to self._real_bzrdir.
80
if not self._real_bzrdir:
81
self._real_bzrdir = BzrDir.open_from_transport(
82
self.root_transport, _server_formats=False)
84
def create_repository(self, shared=False):
86
self._real_bzrdir.create_repository(shared=shared)
87
return self.open_repository()
89
def destroy_repository(self):
90
"""See BzrDir.destroy_repository"""
92
self._real_bzrdir.destroy_repository()
94
def create_branch(self):
96
real_branch = self._real_bzrdir.create_branch()
97
return RemoteBranch(self, self.find_repository(), real_branch)
99
def destroy_branch(self):
100
"""See BzrDir.destroy_branch"""
102
self._real_bzrdir.destroy_branch()
104
def create_workingtree(self, revision_id=None, from_branch=None):
105
raise errors.NotLocalUrl(self.transport.base)
107
def find_branch_format(self):
108
"""Find the branch 'format' for this bzrdir.
110
This might be a synthetic object for e.g. RemoteBranch and SVN.
112
b = self.open_branch()
115
def get_branch_reference(self):
116
"""See BzrDir.get_branch_reference()."""
117
path = self._path_for_remote_call(self._client)
118
response = self._client.call('BzrDir.open_branch', path)
119
if response[0] == 'ok':
120
if response[1] == '':
121
# branch at this location.
124
# a branch reference, use the existing BranchReference logic.
126
elif response == ('nobranch',):
127
raise errors.NotBranchError(path=self.root_transport.base)
129
raise errors.UnexpectedSmartServerResponse(response)
131
def open_branch(self, _unsupported=False):
132
assert _unsupported == False, 'unsupported flag support not implemented yet.'
133
reference_url = self.get_branch_reference()
134
if reference_url is None:
135
# branch at this location.
136
return RemoteBranch(self, self.find_repository())
138
# a branch reference, use the existing BranchReference logic.
139
format = BranchReferenceFormat()
140
return format.open(self, _found=True, location=reference_url)
142
def open_repository(self):
143
path = self._path_for_remote_call(self._client)
144
response = self._client.call('BzrDir.find_repository', path)
145
assert response[0] in ('ok', 'norepository'), \
146
'unexpected response code %s' % (response,)
147
if response[0] == 'norepository':
148
raise errors.NoRepositoryPresent(self)
149
assert len(response) == 4, 'incorrect response length %s' % (response,)
150
if response[1] == '':
151
format = RemoteRepositoryFormat()
152
format.rich_root_data = (response[2] == 'yes')
153
format.supports_tree_reference = (response[3] == 'yes')
154
return RemoteRepository(self, format)
156
raise errors.NoRepositoryPresent(self)
158
def open_workingtree(self, recommend_upgrade=True):
160
if self._real_bzrdir.has_workingtree():
161
raise errors.NotLocalUrl(self.root_transport)
163
raise errors.NoWorkingTree(self.root_transport.base)
165
def _path_for_remote_call(self, client):
166
"""Return the path to be used for this bzrdir in a remote call."""
167
return client.remote_path_from_transport(self.root_transport)
169
def get_branch_transport(self, branch_format):
171
return self._real_bzrdir.get_branch_transport(branch_format)
173
def get_repository_transport(self, repository_format):
175
return self._real_bzrdir.get_repository_transport(repository_format)
177
def get_workingtree_transport(self, workingtree_format):
179
return self._real_bzrdir.get_workingtree_transport(workingtree_format)
181
def can_convert_format(self):
182
"""Upgrading of remote bzrdirs is not supported yet."""
185
def needs_format_conversion(self, format=None):
186
"""Upgrading of remote bzrdirs is not supported yet."""
189
def clone(self, url, revision_id=None, force_new_repo=False):
191
return self._real_bzrdir.clone(url, revision_id=revision_id,
192
force_new_repo=force_new_repo)
195
class RemoteRepositoryFormat(repository.RepositoryFormat):
196
"""Format for repositories accessed over a _SmartClient.
198
Instances of this repository are represented by RemoteRepository
201
The RemoteRepositoryFormat is parameterized during construction
202
to reflect the capabilities of the real, remote format. Specifically
203
the attributes rich_root_data and supports_tree_reference are set
204
on a per instance basis, and are not set (and should not be) at
208
_matchingbzrdir = RemoteBzrDirFormat
210
def initialize(self, a_bzrdir, shared=False):
211
assert isinstance(a_bzrdir, RemoteBzrDir), \
212
'%r is not a RemoteBzrDir' % (a_bzrdir,)
213
return a_bzrdir.create_repository(shared=shared)
215
def open(self, a_bzrdir):
216
assert isinstance(a_bzrdir, RemoteBzrDir)
217
return a_bzrdir.open_repository()
219
def get_format_description(self):
220
return 'bzr remote repository'
222
def __eq__(self, other):
223
return self.__class__ == other.__class__
225
def check_conversion_target(self, target_format):
226
if self.rich_root_data and not target_format.rich_root_data:
227
raise errors.BadConversionTarget(
228
'Does not support rich root data.', target_format)
229
if (self.supports_tree_reference and
230
not getattr(target_format, 'supports_tree_reference', False)):
231
raise errors.BadConversionTarget(
232
'Does not support nested trees', target_format)
235
class RemoteRepository(object):
236
"""Repository accessed over rpc.
238
For the moment most operations are performed using local transport-backed
242
def __init__(self, remote_bzrdir, format, real_repository=None, _client=None):
243
"""Create a RemoteRepository instance.
245
:param remote_bzrdir: The bzrdir hosting this repository.
246
:param format: The RemoteFormat object to use.
247
:param real_repository: If not None, a local implementation of the
248
repository logic for the repository, usually accessing the data
250
:param _client: Private testing parameter - override the smart client
251
to be used by the repository.
254
self._real_repository = real_repository
256
self._real_repository = None
257
self.bzrdir = remote_bzrdir
259
self._client = client._SmartClient(self.bzrdir._shared_medium)
261
self._client = _client
262
self._format = format
263
self._lock_mode = None
264
self._lock_token = None
266
self._leave_lock = False
268
# These depend on the actual remote format, so force them off for
269
# maximum compatibility. XXX: In future these should depend on the
270
# remote repository instance, but this is irrelevant until we perform
271
# reconcile via an RPC call.
272
self._reconcile_does_inventory_gc = False
273
self._reconcile_fixes_text_parents = False
274
self._reconcile_backsup_inventory = False
275
self.base = self.bzrdir.transport.base
278
return "%s(%s)" % (self.__class__.__name__, self.base)
282
def abort_write_group(self):
283
"""Complete a write group on the decorated repository.
285
Smart methods peform operations in a single step so this api
286
is not really applicable except as a compatibility thunk
287
for older plugins that don't use e.g. the CommitBuilder
291
return self._real_repository.abort_write_group()
293
def commit_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.commit_write_group()
304
def _ensure_real(self):
305
"""Ensure that there is a _real_repository set.
307
Used before calls to self._real_repository.
309
if not self._real_repository:
310
self.bzrdir._ensure_real()
311
#self._real_repository = self.bzrdir._real_bzrdir.open_repository()
312
self._set_real_repository(self.bzrdir._real_bzrdir.open_repository())
314
def find_text_key_references(self):
315
"""Find the text key references within the repository.
317
:return: a dictionary mapping (file_id, revision_id) tuples to altered file-ids to an iterable of
318
revision_ids. Each altered file-ids has the exact revision_ids that
319
altered it listed explicitly.
320
:return: A dictionary mapping text keys ((fileid, revision_id) tuples)
321
to whether they were referred to by the inventory of the
322
revision_id that they contain. The inventory texts from all present
323
revision ids are assessed to generate this report.
326
return self._real_repository.find_text_key_references()
328
def _generate_text_key_index(self):
329
"""Generate a new text key index for the repository.
331
This is an expensive function that will take considerable time to run.
333
:return: A dict mapping (file_id, revision_id) tuples to a list of
334
parents, also (file_id, revision_id) tuples.
337
return self._real_repository._generate_text_key_index()
339
def get_revision_graph(self, revision_id=None):
340
"""See Repository.get_revision_graph()."""
341
if revision_id is None:
343
elif revision.is_null(revision_id):
346
path = self.bzrdir._path_for_remote_call(self._client)
347
assert type(revision_id) is str
348
response = self._client.call_expecting_body(
349
'Repository.get_revision_graph', path, revision_id)
350
if response[0][0] not in ['ok', 'nosuchrevision']:
351
raise errors.UnexpectedSmartServerResponse(response[0])
352
if response[0][0] == 'ok':
353
coded = response[1].read_body_bytes()
355
# no revisions in this repository!
357
lines = coded.split('\n')
360
d = tuple(line.split())
361
revision_graph[d[0]] = d[1:]
363
return revision_graph
365
response_body = response[1].read_body_bytes()
366
assert response_body == ''
367
raise NoSuchRevision(self, revision_id)
369
def has_revision(self, revision_id):
370
"""See Repository.has_revision()."""
371
if revision_id == NULL_REVISION:
372
# The null revision is always present.
374
path = self.bzrdir._path_for_remote_call(self._client)
375
response = self._client.call('Repository.has_revision', path, revision_id)
376
assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
377
return response[0] == 'yes'
379
def has_revisions(self, revision_ids):
380
"""See Repository.has_revisions()."""
382
for revision_id in revision_ids:
383
if self.has_revision(revision_id):
384
result.add(revision_id)
387
def has_same_location(self, other):
388
return (self.__class__ == other.__class__ and
389
self.bzrdir.transport.base == other.bzrdir.transport.base)
391
def get_graph(self, other_repository=None):
392
"""Return the graph for this repository format"""
394
return self._real_repository.get_graph(other_repository)
396
def gather_stats(self, revid=None, committers=None):
397
"""See Repository.gather_stats()."""
398
path = self.bzrdir._path_for_remote_call(self._client)
399
# revid can be None to indicate no revisions, not just NULL_REVISION
400
if revid is None or revision.is_null(revid):
404
if committers is None or not committers:
405
fmt_committers = 'no'
407
fmt_committers = 'yes'
408
response = self._client.call_expecting_body(
409
'Repository.gather_stats', path, fmt_revid, fmt_committers)
410
assert response[0][0] == 'ok', \
411
'unexpected response code %s' % (response[0],)
413
body = response[1].read_body_bytes()
415
for line in body.split('\n'):
418
key, val_text = line.split(':')
419
if key in ('revisions', 'size', 'committers'):
420
result[key] = int(val_text)
421
elif key in ('firstrev', 'latestrev'):
422
values = val_text.split(' ')[1:]
423
result[key] = (float(values[0]), long(values[1]))
427
def find_branches(self, using=False):
428
"""See Repository.find_branches()."""
429
# should be an API call to the server.
431
return self._real_repository.find_branches(using=using)
433
def get_physical_lock_status(self):
434
"""See Repository.get_physical_lock_status()."""
435
# should be an API call to the server.
437
return self._real_repository.get_physical_lock_status()
439
def is_in_write_group(self):
440
"""Return True if there is an open write group.
442
write groups are only applicable locally for the smart server..
444
if self._real_repository:
445
return self._real_repository.is_in_write_group()
448
return self._lock_count >= 1
451
"""See Repository.is_shared()."""
452
path = self.bzrdir._path_for_remote_call(self._client)
453
response = self._client.call('Repository.is_shared', path)
454
assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
455
return response[0] == 'yes'
457
def is_write_locked(self):
458
return self._lock_mode == 'w'
461
# wrong eventually - want a local lock cache context
462
if not self._lock_mode:
463
self._lock_mode = 'r'
465
if self._real_repository is not None:
466
self._real_repository.lock_read()
468
self._lock_count += 1
470
def _remote_lock_write(self, token):
471
path = self.bzrdir._path_for_remote_call(self._client)
474
response = self._client.call('Repository.lock_write', path, token)
475
if response[0] == 'ok':
478
elif response[0] == 'LockContention':
479
raise errors.LockContention('(remote lock)')
480
elif response[0] == 'UnlockableTransport':
481
raise errors.UnlockableTransport(self.bzrdir.root_transport)
482
elif response[0] == 'LockFailed':
483
raise errors.LockFailed(response[1], response[2])
485
raise errors.UnexpectedSmartServerResponse(response)
487
def lock_write(self, token=None):
488
if not self._lock_mode:
489
self._lock_token = self._remote_lock_write(token)
490
# if self._lock_token is None, then this is something like packs or
491
# svn where we don't get to lock the repo, or a weave style repository
492
# where we cannot lock it over the wire and attempts to do so will
494
if self._real_repository is not None:
495
self._real_repository.lock_write(token=self._lock_token)
496
if token is not None:
497
self._leave_lock = True
499
self._leave_lock = False
500
self._lock_mode = 'w'
502
elif self._lock_mode == 'r':
503
raise errors.ReadOnlyError(self)
505
self._lock_count += 1
506
return self._lock_token or None
508
def leave_lock_in_place(self):
509
if not self._lock_token:
510
raise NotImplementedError(self.leave_lock_in_place)
511
self._leave_lock = True
513
def dont_leave_lock_in_place(self):
514
if not self._lock_token:
515
raise NotImplementedError(self.dont_leave_lock_in_place)
516
self._leave_lock = False
518
def _set_real_repository(self, repository):
519
"""Set the _real_repository for this repository.
521
:param repository: The repository to fallback to for non-hpss
522
implemented operations.
524
assert not isinstance(repository, RemoteRepository)
525
self._real_repository = repository
526
if self._lock_mode == 'w':
527
# if we are already locked, the real repository must be able to
528
# acquire the lock with our token.
529
self._real_repository.lock_write(self._lock_token)
530
elif self._lock_mode == 'r':
531
self._real_repository.lock_read()
533
def start_write_group(self):
534
"""Start a write group on the decorated repository.
536
Smart methods peform operations in a single step so this api
537
is not really applicable except as a compatibility thunk
538
for older plugins that don't use e.g. the CommitBuilder
542
return self._real_repository.start_write_group()
544
def _unlock(self, token):
545
path = self.bzrdir._path_for_remote_call(self._client)
547
# with no token the remote repository is not persistently locked.
549
response = self._client.call('Repository.unlock', path, token)
550
if response == ('ok',):
552
elif response[0] == 'TokenMismatch':
553
raise errors.TokenMismatch(token, '(remote token)')
555
raise errors.UnexpectedSmartServerResponse(response)
558
self._lock_count -= 1
559
if self._lock_count > 0:
561
old_mode = self._lock_mode
562
self._lock_mode = None
564
# The real repository is responsible at present for raising an
565
# exception if it's in an unfinished write group. However, it
566
# normally will *not* actually remove the lock from disk - that's
567
# done by the server on receiving the Repository.unlock call.
568
# This is just to let the _real_repository stay up to date.
569
if self._real_repository is not None:
570
self._real_repository.unlock()
572
# The rpc-level lock should be released even if there was a
573
# problem releasing the vfs-based lock.
575
# Only write-locked repositories need to make a remote method
576
# call to perfom the unlock.
577
old_token = self._lock_token
578
self._lock_token = None
579
if not self._leave_lock:
580
self._unlock(old_token)
582
def break_lock(self):
583
# should hand off to the network
585
return self._real_repository.break_lock()
587
def _get_tarball(self, compression):
588
"""Return a TemporaryFile containing a repository tarball.
590
Returns None if the server does not support sending tarballs.
593
path = self.bzrdir._path_for_remote_call(self._client)
594
response, protocol = self._client.call_expecting_body(
595
'Repository.tarball', path, compression)
596
if response[0] == 'ok':
597
# Extract the tarball and return it
598
t = tempfile.NamedTemporaryFile()
599
# TODO: rpc layer should read directly into it...
600
t.write(protocol.read_body_bytes())
603
if (response == ('error', "Generic bzr smart protocol error: "
604
"bad request 'Repository.tarball'") or
605
response == ('error', "Generic bzr smart protocol error: "
606
"bad request u'Repository.tarball'")):
607
protocol.cancel_read_body()
609
raise errors.UnexpectedSmartServerResponse(response)
611
def sprout(self, to_bzrdir, revision_id=None):
612
# TODO: Option to control what format is created?
614
dest_repo = self._real_repository._format.initialize(to_bzrdir,
616
dest_repo.fetch(self, revision_id=revision_id)
619
### These methods are just thin shims to the VFS object for now.
621
def revision_tree(self, revision_id):
623
return self._real_repository.revision_tree(revision_id)
625
def get_serializer_format(self):
627
return self._real_repository.get_serializer_format()
629
def get_commit_builder(self, branch, parents, config, timestamp=None,
630
timezone=None, committer=None, revprops=None,
632
# FIXME: It ought to be possible to call this without immediately
633
# triggering _ensure_real. For now it's the easiest thing to do.
635
builder = self._real_repository.get_commit_builder(branch, parents,
636
config, timestamp=timestamp, timezone=timezone,
637
committer=committer, revprops=revprops, revision_id=revision_id)
641
def add_inventory(self, revid, inv, parents):
643
return self._real_repository.add_inventory(revid, inv, parents)
646
def add_revision(self, rev_id, rev, inv=None, config=None):
648
return self._real_repository.add_revision(
649
rev_id, rev, inv=inv, config=config)
652
def get_inventory(self, revision_id):
654
return self._real_repository.get_inventory(revision_id)
656
def iter_inventories(self, revision_ids):
658
return self._real_repository.iter_inventories(revision_ids)
661
def get_revision(self, revision_id):
663
return self._real_repository.get_revision(revision_id)
666
def weave_store(self):
668
return self._real_repository.weave_store
670
def get_transaction(self):
672
return self._real_repository.get_transaction()
675
def clone(self, a_bzrdir, revision_id=None):
677
return self._real_repository.clone(a_bzrdir, revision_id=revision_id)
679
def make_working_trees(self):
680
"""RemoteRepositories never create working trees by default."""
683
def fetch(self, source, revision_id=None, pb=None):
684
if self.has_same_location(source):
685
# check that last_revision is in 'from' and then return a
687
if (revision_id is not None and
688
not revision.is_null(revision_id)):
689
self.get_revision(revision_id)
692
return self._real_repository.fetch(
693
source, revision_id=revision_id, pb=pb)
695
def create_bundle(self, target, base, fileobj, format=None):
697
self._real_repository.create_bundle(target, base, fileobj, format)
700
def control_weaves(self):
702
return self._real_repository.control_weaves
705
def get_ancestry(self, revision_id, topo_sorted=True):
707
return self._real_repository.get_ancestry(revision_id, topo_sorted)
710
def get_inventory_weave(self):
712
return self._real_repository.get_inventory_weave()
714
def fileids_altered_by_revision_ids(self, revision_ids):
716
return self._real_repository.fileids_altered_by_revision_ids(revision_ids)
718
def _get_versioned_file_checker(self, revisions, revision_versions_cache):
720
return self._real_repository._get_versioned_file_checker(
721
revisions, revision_versions_cache)
723
def iter_files_bytes(self, desired_files):
724
"""See Repository.iter_file_bytes.
727
return self._real_repository.iter_files_bytes(desired_files)
730
def get_signature_text(self, revision_id):
732
return self._real_repository.get_signature_text(revision_id)
735
def get_revision_graph_with_ghosts(self, revision_ids=None):
737
return self._real_repository.get_revision_graph_with_ghosts(
738
revision_ids=revision_ids)
741
def get_inventory_xml(self, revision_id):
743
return self._real_repository.get_inventory_xml(revision_id)
745
def deserialise_inventory(self, revision_id, xml):
747
return self._real_repository.deserialise_inventory(revision_id, xml)
749
def reconcile(self, other=None, thorough=False):
751
return self._real_repository.reconcile(other=other, thorough=thorough)
753
def all_revision_ids(self):
755
return self._real_repository.all_revision_ids()
758
def get_deltas_for_revisions(self, revisions):
760
return self._real_repository.get_deltas_for_revisions(revisions)
763
def get_revision_delta(self, revision_id):
765
return self._real_repository.get_revision_delta(revision_id)
768
def revision_trees(self, revision_ids):
770
return self._real_repository.revision_trees(revision_ids)
773
def get_revision_reconcile(self, revision_id):
775
return self._real_repository.get_revision_reconcile(revision_id)
778
def check(self, revision_ids=None):
780
return self._real_repository.check(revision_ids=revision_ids)
782
def copy_content_into(self, destination, revision_id=None):
784
return self._real_repository.copy_content_into(
785
destination, revision_id=revision_id)
787
def _copy_repository_tarball(self, to_bzrdir, revision_id=None):
788
# get a tarball of the remote repository, and copy from that into the
790
from bzrlib import osutils
793
# TODO: Maybe a progress bar while streaming the tarball?
794
note("Copying repository content as tarball...")
795
tar_file = self._get_tarball('bz2')
798
destination = to_bzrdir.create_repository()
800
tar = tarfile.open('repository', fileobj=tar_file,
802
tmpdir = tempfile.mkdtemp()
804
_extract_tar(tar, tmpdir)
805
tmp_bzrdir = BzrDir.open(tmpdir)
806
tmp_repo = tmp_bzrdir.open_repository()
807
tmp_repo.copy_content_into(destination, revision_id)
809
osutils.rmtree(tmpdir)
813
# TODO: Suggestion from john: using external tar is much faster than
814
# python's tarfile library, but it may not work on windows.
818
"""Compress the data within the repository.
820
This is not currently implemented within the smart server.
823
return self._real_repository.pack()
825
def set_make_working_trees(self, new_value):
826
raise NotImplementedError(self.set_make_working_trees)
829
def sign_revision(self, revision_id, gpg_strategy):
831
return self._real_repository.sign_revision(revision_id, gpg_strategy)
834
def get_revisions(self, revision_ids):
836
return self._real_repository.get_revisions(revision_ids)
838
def supports_rich_root(self):
840
return self._real_repository.supports_rich_root()
842
def iter_reverse_revision_history(self, revision_id):
844
return self._real_repository.iter_reverse_revision_history(revision_id)
847
def _serializer(self):
849
return self._real_repository._serializer
851
def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
853
return self._real_repository.store_revision_signature(
854
gpg_strategy, plaintext, revision_id)
856
def add_signature_text(self, revision_id, signature):
858
return self._real_repository.add_signature_text(revision_id, signature)
860
def has_signature_for_revision_id(self, revision_id):
862
return self._real_repository.has_signature_for_revision_id(revision_id)
864
def get_data_stream(self, revision_ids):
865
REQUEST_NAME = 'Repository.stream_revisions_chunked'
866
path = self.bzrdir._path_for_remote_call(self._client)
867
response, protocol = self._client.call_expecting_body(
868
REQUEST_NAME, path, *revision_ids)
870
if response == ('ok',):
871
return self._deserialise_stream(protocol)
872
elif (response == ('error', "Generic bzr smart protocol error: "
873
"bad request '%s'" % REQUEST_NAME) or
874
response == ('error', "Generic bzr smart protocol error: "
875
"bad request u'%s'" % REQUEST_NAME)):
876
protocol.cancel_read_body()
878
return self._real_repository.get_data_stream(revision_ids)
880
raise errors.UnexpectedSmartServerResponse(response)
882
def _deserialise_stream(self, protocol):
883
stream = protocol.read_streamed_body()
884
container_parser = ContainerPushParser()
886
container_parser.accept_bytes(bytes)
887
records = container_parser.read_pending_records()
888
for record_names, record_bytes in records:
889
if len(record_names) != 1:
890
# These records should have only one name, and that name
891
# should be a one-element tuple.
892
raise errors.SmartProtocolError(
893
'Repository data stream had invalid record name %r'
895
name_tuple = record_names[0]
896
yield name_tuple, record_bytes
898
def insert_data_stream(self, stream):
900
self._real_repository.insert_data_stream(stream)
902
def item_keys_introduced_by(self, revision_ids, _files_pb=None):
904
return self._real_repository.item_keys_introduced_by(revision_ids,
907
def revision_graph_can_have_wrong_parents(self):
908
# The answer depends on the remote repo format.
910
return self._real_repository.revision_graph_can_have_wrong_parents()
912
def _find_inconsistent_revision_parents(self):
914
return self._real_repository._find_inconsistent_revision_parents()
916
def _check_for_inconsistent_revision_parents(self):
918
return self._real_repository._check_for_inconsistent_revision_parents()
920
def _make_parents_provider(self):
922
return self._real_repository._make_parents_provider()
925
class RemoteBranchLockableFiles(LockableFiles):
926
"""A 'LockableFiles' implementation that talks to a smart server.
928
This is not a public interface class.
931
def __init__(self, bzrdir, _client):
933
self._client = _client
934
self._need_find_modes = True
935
LockableFiles.__init__(
936
self, bzrdir.get_branch_transport(None),
937
'lock', lockdir.LockDir)
939
def _find_modes(self):
940
# RemoteBranches don't let the client set the mode of control files.
941
self._dir_mode = None
942
self._file_mode = None
945
"""'get' a remote path as per the LockableFiles interface.
947
:param path: the file to 'get'. If this is 'branch.conf', we do not
948
just retrieve a file, instead we ask the smart server to generate
949
a configuration for us - which is retrieved as an INI file.
951
if path == 'branch.conf':
952
path = self.bzrdir._path_for_remote_call(self._client)
953
response = self._client.call_expecting_body(
954
'Branch.get_config_file', path)
955
assert response[0][0] == 'ok', \
956
'unexpected response code %s' % (response[0],)
957
return StringIO(response[1].read_body_bytes())
960
return LockableFiles.get(self, path)
963
class RemoteBranchFormat(branch.BranchFormat):
965
def __eq__(self, other):
966
return (isinstance(other, RemoteBranchFormat) and
967
self.__dict__ == other.__dict__)
969
def get_format_description(self):
970
return 'Remote BZR Branch'
972
def get_format_string(self):
973
return 'Remote BZR Branch'
975
def open(self, a_bzrdir):
976
assert isinstance(a_bzrdir, RemoteBzrDir)
977
return a_bzrdir.open_branch()
979
def initialize(self, a_bzrdir):
980
assert isinstance(a_bzrdir, RemoteBzrDir)
981
return a_bzrdir.create_branch()
983
def supports_tags(self):
984
# Remote branches might support tags, but we won't know until we
985
# access the real remote branch.
989
class RemoteBranch(branch.Branch):
990
"""Branch stored on a server accessed by HPSS RPC.
992
At the moment most operations are mapped down to simple file operations.
995
def __init__(self, remote_bzrdir, remote_repository, real_branch=None,
997
"""Create a RemoteBranch instance.
999
:param real_branch: An optional local implementation of the branch
1000
format, usually accessing the data via the VFS.
1001
:param _client: Private parameter for testing.
1003
# We intentionally don't call the parent class's __init__, because it
1004
# will try to assign to self.tags, which is a property in this subclass.
1005
# And the parent's __init__ doesn't do much anyway.
1006
self._revision_id_to_revno_cache = None
1007
self._revision_history_cache = None
1008
self.bzrdir = remote_bzrdir
1009
if _client is not None:
1010
self._client = _client
1012
self._client = client._SmartClient(self.bzrdir._shared_medium)
1013
self.repository = remote_repository
1014
if real_branch is not None:
1015
self._real_branch = real_branch
1016
# Give the remote repository the matching real repo.
1017
real_repo = self._real_branch.repository
1018
if isinstance(real_repo, RemoteRepository):
1019
real_repo._ensure_real()
1020
real_repo = real_repo._real_repository
1021
self.repository._set_real_repository(real_repo)
1022
# Give the branch the remote repository to let fast-pathing happen.
1023
self._real_branch.repository = self.repository
1025
self._real_branch = None
1026
# Fill out expected attributes of branch for bzrlib api users.
1027
self._format = RemoteBranchFormat()
1028
self.base = self.bzrdir.root_transport.base
1029
self._control_files = None
1030
self._lock_mode = None
1031
self._lock_token = None
1032
self._lock_count = 0
1033
self._leave_lock = False
1036
return "%s(%s)" % (self.__class__.__name__, self.base)
1040
def _ensure_real(self):
1041
"""Ensure that there is a _real_branch set.
1043
Used before calls to self._real_branch.
1045
if not self._real_branch:
1046
assert vfs.vfs_enabled()
1047
self.bzrdir._ensure_real()
1048
self._real_branch = self.bzrdir._real_bzrdir.open_branch()
1049
# Give the remote repository the matching real repo.
1050
real_repo = self._real_branch.repository
1051
if isinstance(real_repo, RemoteRepository):
1052
real_repo._ensure_real()
1053
real_repo = real_repo._real_repository
1054
self.repository._set_real_repository(real_repo)
1055
# Give the branch the remote repository to let fast-pathing happen.
1056
self._real_branch.repository = self.repository
1057
# XXX: deal with _lock_mode == 'w'
1058
if self._lock_mode == 'r':
1059
self._real_branch.lock_read()
1062
def control_files(self):
1063
# Defer actually creating RemoteBranchLockableFiles until its needed,
1064
# because it triggers an _ensure_real that we otherwise might not need.
1065
if self._control_files is None:
1066
self._control_files = RemoteBranchLockableFiles(
1067
self.bzrdir, self._client)
1068
return self._control_files
1070
def _get_checkout_format(self):
1072
return self._real_branch._get_checkout_format()
1074
def get_physical_lock_status(self):
1075
"""See Branch.get_physical_lock_status()."""
1076
# should be an API call to the server, as branches must be lockable.
1078
return self._real_branch.get_physical_lock_status()
1080
def lock_read(self):
1081
if not self._lock_mode:
1082
self._lock_mode = 'r'
1083
self._lock_count = 1
1084
if self._real_branch is not None:
1085
self._real_branch.lock_read()
1087
self._lock_count += 1
1089
def _remote_lock_write(self, token):
1091
branch_token = repo_token = ''
1093
branch_token = token
1094
repo_token = self.repository.lock_write()
1095
self.repository.unlock()
1096
path = self.bzrdir._path_for_remote_call(self._client)
1097
response = self._client.call('Branch.lock_write', path, branch_token,
1099
if response[0] == 'ok':
1100
ok, branch_token, repo_token = response
1101
return branch_token, repo_token
1102
elif response[0] == 'LockContention':
1103
raise errors.LockContention('(remote lock)')
1104
elif response[0] == 'TokenMismatch':
1105
raise errors.TokenMismatch(token, '(remote token)')
1106
elif response[0] == 'UnlockableTransport':
1107
raise errors.UnlockableTransport(self.bzrdir.root_transport)
1108
elif response[0] == 'ReadOnlyError':
1109
raise errors.ReadOnlyError(self)
1110
elif response[0] == 'LockFailed':
1111
raise errors.LockFailed(response[1], response[2])
1113
raise errors.UnexpectedSmartServerResponse(response)
1115
def lock_write(self, token=None):
1116
if not self._lock_mode:
1117
remote_tokens = self._remote_lock_write(token)
1118
self._lock_token, self._repo_lock_token = remote_tokens
1119
assert self._lock_token, 'Remote server did not return a token!'
1120
# TODO: We really, really, really don't want to call _ensure_real
1121
# here, but it's the easiest way to ensure coherency between the
1122
# state of the RemoteBranch and RemoteRepository objects and the
1123
# physical locks. If we don't materialise the real objects here,
1124
# then getting everything in the right state later is complex, so
1125
# for now we just do it the lazy way.
1126
# -- Andrew Bennetts, 2007-02-22.
1128
if self._real_branch is not None:
1129
self._real_branch.repository.lock_write(
1130
token=self._repo_lock_token)
1132
self._real_branch.lock_write(token=self._lock_token)
1134
self._real_branch.repository.unlock()
1135
if token is not None:
1136
self._leave_lock = True
1138
# XXX: this case seems to be unreachable; token cannot be None.
1139
self._leave_lock = False
1140
self._lock_mode = 'w'
1141
self._lock_count = 1
1142
elif self._lock_mode == 'r':
1143
raise errors.ReadOnlyTransaction
1145
if token is not None:
1146
# A token was given to lock_write, and we're relocking, so check
1147
# that the given token actually matches the one we already have.
1148
if token != self._lock_token:
1149
raise errors.TokenMismatch(token, self._lock_token)
1150
self._lock_count += 1
1151
return self._lock_token or None
1153
def _unlock(self, branch_token, repo_token):
1154
path = self.bzrdir._path_for_remote_call(self._client)
1155
response = self._client.call('Branch.unlock', path, branch_token,
1157
if response == ('ok',):
1159
elif response[0] == 'TokenMismatch':
1160
raise errors.TokenMismatch(
1161
str((branch_token, repo_token)), '(remote tokens)')
1163
raise errors.UnexpectedSmartServerResponse(response)
1166
self._lock_count -= 1
1167
if not self._lock_count:
1168
self._clear_cached_state()
1169
mode = self._lock_mode
1170
self._lock_mode = None
1171
if self._real_branch is not None:
1172
if (not self._leave_lock and mode == 'w' and
1173
self._repo_lock_token):
1174
# If this RemoteBranch will remove the physical lock for the
1175
# repository, make sure the _real_branch doesn't do it
1176
# first. (Because the _real_branch's repository is set to
1177
# be the RemoteRepository.)
1178
self._real_branch.repository.leave_lock_in_place()
1179
self._real_branch.unlock()
1181
# Only write-locked branched need to make a remote method call
1182
# to perfom the unlock.
1184
assert self._lock_token, 'Locked, but no token!'
1185
branch_token = self._lock_token
1186
repo_token = self._repo_lock_token
1187
self._lock_token = None
1188
self._repo_lock_token = None
1189
if not self._leave_lock:
1190
self._unlock(branch_token, repo_token)
1192
def break_lock(self):
1194
return self._real_branch.break_lock()
1196
def leave_lock_in_place(self):
1197
if not self._lock_token:
1198
raise NotImplementedError(self.leave_lock_in_place)
1199
self._leave_lock = True
1201
def dont_leave_lock_in_place(self):
1202
if not self._lock_token:
1203
raise NotImplementedError(self.dont_leave_lock_in_place)
1204
self._leave_lock = False
1206
def last_revision_info(self):
1207
"""See Branch.last_revision_info()."""
1208
path = self.bzrdir._path_for_remote_call(self._client)
1209
response = self._client.call('Branch.last_revision_info', path)
1210
assert response[0] == 'ok', 'unexpected response code %s' % (response,)
1211
revno = int(response[1])
1212
last_revision = response[2]
1213
return (revno, last_revision)
1215
def _gen_revision_history(self):
1216
"""See Branch._gen_revision_history()."""
1217
path = self.bzrdir._path_for_remote_call(self._client)
1218
response = self._client.call_expecting_body(
1219
'Branch.revision_history', path)
1220
assert response[0][0] == 'ok', ('unexpected response code %s'
1222
result = response[1].read_body_bytes().split('\x00')
1228
def set_revision_history(self, rev_history):
1229
# Send just the tip revision of the history; the server will generate
1230
# the full history from that. If the revision doesn't exist in this
1231
# branch, NoSuchRevision will be raised.
1232
path = self.bzrdir._path_for_remote_call(self._client)
1233
if rev_history == []:
1236
rev_id = rev_history[-1]
1237
self._clear_cached_state()
1238
response = self._client.call('Branch.set_last_revision',
1239
path, self._lock_token, self._repo_lock_token, rev_id)
1240
if response[0] == 'NoSuchRevision':
1241
raise NoSuchRevision(self, rev_id)
1243
assert response == ('ok',), (
1244
'unexpected response code %r' % (response,))
1245
self._cache_revision_history(rev_history)
1247
def get_parent(self):
1249
return self._real_branch.get_parent()
1251
def set_parent(self, url):
1253
return self._real_branch.set_parent(url)
1255
def get_config(self):
1256
return RemoteBranchConfig(self)
1258
def sprout(self, to_bzrdir, revision_id=None):
1259
# Like Branch.sprout, except that it sprouts a branch in the default
1260
# format, because RemoteBranches can't be created at arbitrary URLs.
1261
# XXX: if to_bzrdir is a RemoteBranch, this should perhaps do
1262
# to_bzrdir.create_branch...
1264
result = self._real_branch._format.initialize(to_bzrdir)
1265
self.copy_content_into(result, revision_id=revision_id)
1266
result.set_parent(self.bzrdir.root_transport.base)
1270
def pull(self, source, overwrite=False, stop_revision=None,
1272
# FIXME: This asks the real branch to run the hooks, which means
1273
# they're called with the wrong target branch parameter.
1274
# The test suite specifically allows this at present but it should be
1275
# fixed. It should get a _override_hook_target branch,
1276
# as push does. -- mbp 20070405
1278
self._real_branch.pull(
1279
source, overwrite=overwrite, stop_revision=stop_revision,
1283
def push(self, target, overwrite=False, stop_revision=None):
1285
return self._real_branch.push(
1286
target, overwrite=overwrite, stop_revision=stop_revision,
1287
_override_hook_source_branch=self)
1289
def is_locked(self):
1290
return self._lock_count >= 1
1292
def set_last_revision_info(self, revno, revision_id):
1294
self._clear_cached_state()
1295
return self._real_branch.set_last_revision_info(revno, revision_id)
1297
def generate_revision_history(self, revision_id, last_rev=None,
1300
return self._real_branch.generate_revision_history(
1301
revision_id, last_rev=last_rev, other_branch=other_branch)
1306
return self._real_branch.tags
1308
def set_push_location(self, location):
1310
return self._real_branch.set_push_location(location)
1312
def update_revisions(self, other, stop_revision=None, overwrite=False):
1314
return self._real_branch.update_revisions(
1315
other, stop_revision=stop_revision, overwrite=overwrite)
1318
class RemoteBranchConfig(BranchConfig):
1321
self.branch._ensure_real()
1322
return self.branch._real_branch.get_config().username()
1324
def _get_branch_data_config(self):
1325
self.branch._ensure_real()
1326
if self._branch_data_config is None:
1327
self._branch_data_config = TreeConfig(self.branch._real_branch)
1328
return self._branch_data_config
1331
def _extract_tar(tar, to_dir):
1332
"""Extract all the contents of a tarfile object.
1334
A replacement for extractall, which is not present in python2.4
1337
tar.extract(tarinfo, to_dir)