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.trace import note
39
# Note: RemoteBzrDirFormat is in bzrdir.py
41
class RemoteBzrDir(BzrDir):
42
"""Control directory on a remote server, accessed via bzr:// or similar."""
44
def __init__(self, transport, _client=None):
45
"""Construct a RemoteBzrDir.
47
:param _client: Private parameter for testing. Disables probing and the
50
BzrDir.__init__(self, transport, RemoteBzrDirFormat())
51
# this object holds a delegated bzrdir that uses file-level operations
52
# to talk to the other side
53
self._real_bzrdir = None
56
self._shared_medium = transport.get_shared_medium()
57
self._client = client._SmartClient(self._shared_medium)
59
self._client = _client
60
self._shared_medium = None
63
path = self._path_for_remote_call(self._client)
64
response = self._client.call('BzrDir.open', path)
65
if response not in [('yes',), ('no',)]:
66
raise errors.UnexpectedSmartServerResponse(response)
67
if response == ('no',):
68
raise errors.NotBranchError(path=transport.base)
70
def _ensure_real(self):
71
"""Ensure that there is a _real_bzrdir set.
73
Used before calls to self._real_bzrdir.
75
if not self._real_bzrdir:
76
self._real_bzrdir = BzrDir.open_from_transport(
77
self.root_transport, _server_formats=False)
79
def create_repository(self, shared=False):
81
self._real_bzrdir.create_repository(shared=shared)
82
return self.open_repository()
84
def destroy_repository(self):
85
"""See BzrDir.destroy_repository"""
87
self._real_bzrdir.destroy_repository()
89
def create_branch(self):
91
real_branch = self._real_bzrdir.create_branch()
92
return RemoteBranch(self, self.find_repository(), real_branch)
94
def destroy_branch(self):
95
"""See BzrDir.destroy_branch"""
97
self._real_bzrdir.destroy_branch()
99
def create_workingtree(self, revision_id=None, from_branch=None):
100
raise errors.NotLocalUrl(self.transport.base)
102
def find_branch_format(self):
103
"""Find the branch 'format' for this bzrdir.
105
This might be a synthetic object for e.g. RemoteBranch and SVN.
107
b = self.open_branch()
110
def get_branch_reference(self):
111
"""See BzrDir.get_branch_reference()."""
112
path = self._path_for_remote_call(self._client)
113
response = self._client.call('BzrDir.open_branch', path)
114
if response[0] == 'ok':
115
if response[1] == '':
116
# branch at this location.
119
# a branch reference, use the existing BranchReference logic.
121
elif response == ('nobranch',):
122
raise errors.NotBranchError(path=self.root_transport.base)
124
raise errors.UnexpectedSmartServerResponse(response)
126
def open_branch(self, _unsupported=False):
127
assert _unsupported == False, 'unsupported flag support not implemented yet.'
128
reference_url = self.get_branch_reference()
129
if reference_url is None:
130
# branch at this location.
131
return RemoteBranch(self, self.find_repository())
133
# a branch reference, use the existing BranchReference logic.
134
format = BranchReferenceFormat()
135
return format.open(self, _found=True, location=reference_url)
137
def open_repository(self):
138
path = self._path_for_remote_call(self._client)
139
response = self._client.call('BzrDir.find_repository', path)
140
assert response[0] in ('ok', 'norepository'), \
141
'unexpected response code %s' % (response,)
142
if response[0] == 'norepository':
143
raise errors.NoRepositoryPresent(self)
144
assert len(response) == 4, 'incorrect response length %s' % (response,)
145
if response[1] == '':
146
format = RemoteRepositoryFormat()
147
format.rich_root_data = (response[2] == 'yes')
148
format.supports_tree_reference = (response[3] == 'yes')
149
return RemoteRepository(self, format)
151
raise errors.NoRepositoryPresent(self)
153
def open_workingtree(self, recommend_upgrade=True):
155
if self._real_bzrdir.has_workingtree():
156
raise errors.NotLocalUrl(self.root_transport)
158
raise errors.NoWorkingTree(self.root_transport.base)
160
def _path_for_remote_call(self, client):
161
"""Return the path to be used for this bzrdir in a remote call."""
162
return client.remote_path_from_transport(self.root_transport)
164
def get_branch_transport(self, branch_format):
166
return self._real_bzrdir.get_branch_transport(branch_format)
168
def get_repository_transport(self, repository_format):
170
return self._real_bzrdir.get_repository_transport(repository_format)
172
def get_workingtree_transport(self, workingtree_format):
174
return self._real_bzrdir.get_workingtree_transport(workingtree_format)
176
def can_convert_format(self):
177
"""Upgrading of remote bzrdirs is not supported yet."""
180
def needs_format_conversion(self, format=None):
181
"""Upgrading of remote bzrdirs is not supported yet."""
184
def clone(self, url, revision_id=None, force_new_repo=False):
186
return self._real_bzrdir.clone(url, revision_id=revision_id,
187
force_new_repo=force_new_repo)
190
class RemoteRepositoryFormat(repository.RepositoryFormat):
191
"""Format for repositories accessed over a _SmartClient.
193
Instances of this repository are represented by RemoteRepository
196
The RemoteRepositoryFormat is parameterized during construction
197
to reflect the capabilities of the real, remote format. Specifically
198
the attributes rich_root_data and supports_tree_reference are set
199
on a per instance basis, and are not set (and should not be) at
203
_matchingbzrdir = RemoteBzrDirFormat
205
def initialize(self, a_bzrdir, shared=False):
206
assert isinstance(a_bzrdir, RemoteBzrDir), \
207
'%r is not a RemoteBzrDir' % (a_bzrdir,)
208
return a_bzrdir.create_repository(shared=shared)
210
def open(self, a_bzrdir):
211
assert isinstance(a_bzrdir, RemoteBzrDir)
212
return a_bzrdir.open_repository()
214
def get_format_description(self):
215
return 'bzr remote repository'
217
def __eq__(self, other):
218
return self.__class__ == other.__class__
220
def check_conversion_target(self, target_format):
221
if self.rich_root_data and not target_format.rich_root_data:
222
raise errors.BadConversionTarget(
223
'Does not support rich root data.', target_format)
224
if (self.supports_tree_reference and
225
not getattr(target_format, 'supports_tree_reference', False)):
226
raise errors.BadConversionTarget(
227
'Does not support nested trees', target_format)
230
class RemoteRepository(object):
231
"""Repository accessed over rpc.
233
For the moment most operations are performed using local transport-backed
237
def __init__(self, remote_bzrdir, format, real_repository=None, _client=None):
238
"""Create a RemoteRepository instance.
240
:param remote_bzrdir: The bzrdir hosting this repository.
241
:param format: The RemoteFormat object to use.
242
:param real_repository: If not None, a local implementation of the
243
repository logic for the repository, usually accessing the data
245
:param _client: Private testing parameter - override the smart client
246
to be used by the repository.
249
self._real_repository = real_repository
251
self._real_repository = None
252
self.bzrdir = remote_bzrdir
254
self._client = client._SmartClient(self.bzrdir._shared_medium)
256
self._client = _client
257
self._format = format
258
self._lock_mode = None
259
self._lock_token = None
261
self._leave_lock = False
263
# These depend on the actual remote format, so force them off for
264
# maximum compatibility. XXX: In future these should depend on the
265
# remote repository instance, but this is irrelevant until we perform
266
# reconcile via an RPC call.
267
self._reconcile_does_inventory_gc = False
268
self._reconcile_fixes_text_parents = False
269
self._reconcile_backsup_inventory = False
270
self.base = self.bzrdir.transport.base
273
return "%s(%s)" % (self.__class__.__name__, self.base)
277
def abort_write_group(self):
278
"""Complete a write group on the decorated repository.
280
Smart methods peform operations in a single step so this api
281
is not really applicable except as a compatibility thunk
282
for older plugins that don't use e.g. the CommitBuilder
286
return self._real_repository.abort_write_group()
288
def commit_write_group(self):
289
"""Complete a write group on the decorated repository.
291
Smart methods peform operations in a single step so this api
292
is not really applicable except as a compatibility thunk
293
for older plugins that don't use e.g. the CommitBuilder
297
return self._real_repository.commit_write_group()
299
def _ensure_real(self):
300
"""Ensure that there is a _real_repository set.
302
Used before calls to self._real_repository.
304
if not self._real_repository:
305
self.bzrdir._ensure_real()
306
#self._real_repository = self.bzrdir._real_bzrdir.open_repository()
307
self._set_real_repository(self.bzrdir._real_bzrdir.open_repository())
309
def find_text_key_references(self):
310
"""Find the text key references within the repository.
312
:return: a dictionary mapping (file_id, revision_id) tuples to altered file-ids to an iterable of
313
revision_ids. Each altered file-ids has the exact revision_ids that
314
altered it listed explicitly.
315
:return: A dictionary mapping text keys ((fileid, revision_id) tuples)
316
to whether they were referred to by the inventory of the
317
revision_id that they contain. The inventory texts from all present
318
revision ids are assessed to generate this report.
321
return self._real_repository.find_text_key_references()
323
def _generate_text_key_index(self):
324
"""Generate a new text key index for the repository.
326
This is an expensive function that will take considerable time to run.
328
:return: A dict mapping (file_id, revision_id) tuples to a list of
329
parents, also (file_id, revision_id) tuples.
332
return self._real_repository._generate_text_key_index()
334
def get_revision_graph(self, revision_id=None):
335
"""See Repository.get_revision_graph()."""
336
if revision_id is None:
338
elif revision.is_null(revision_id):
341
path = self.bzrdir._path_for_remote_call(self._client)
342
assert type(revision_id) is str
343
response = self._client.call_expecting_body(
344
'Repository.get_revision_graph', path, revision_id)
345
if response[0][0] not in ['ok', 'nosuchrevision']:
346
raise errors.UnexpectedSmartServerResponse(response[0])
347
if response[0][0] == 'ok':
348
coded = response[1].read_body_bytes()
350
# no revisions in this repository!
352
lines = coded.split('\n')
355
d = tuple(line.split())
356
revision_graph[d[0]] = d[1:]
358
return revision_graph
360
response_body = response[1].read_body_bytes()
361
assert response_body == ''
362
raise NoSuchRevision(self, revision_id)
364
def has_revision(self, revision_id):
365
"""See Repository.has_revision()."""
366
if revision_id is None:
367
# The null revision is always present.
369
path = self.bzrdir._path_for_remote_call(self._client)
370
response = self._client.call('Repository.has_revision', path, revision_id)
371
assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
372
return response[0] == 'yes'
374
def has_same_location(self, other):
375
return (self.__class__ == other.__class__ and
376
self.bzrdir.transport.base == other.bzrdir.transport.base)
378
def get_graph(self, other_repository=None):
379
"""Return the graph for this repository format"""
381
return self._real_repository.get_graph(other_repository)
383
def gather_stats(self, revid=None, committers=None):
384
"""See Repository.gather_stats()."""
385
path = self.bzrdir._path_for_remote_call(self._client)
386
# revid can be None to indicate no revisions, not just NULL_REVISION
387
if revid is None or revision.is_null(revid):
391
if committers is None or not committers:
392
fmt_committers = 'no'
394
fmt_committers = 'yes'
395
response = self._client.call_expecting_body(
396
'Repository.gather_stats', path, fmt_revid, fmt_committers)
397
assert response[0][0] == 'ok', \
398
'unexpected response code %s' % (response[0],)
400
body = response[1].read_body_bytes()
402
for line in body.split('\n'):
405
key, val_text = line.split(':')
406
if key in ('revisions', 'size', 'committers'):
407
result[key] = int(val_text)
408
elif key in ('firstrev', 'latestrev'):
409
values = val_text.split(' ')[1:]
410
result[key] = (float(values[0]), long(values[1]))
414
def find_branches(self, using=False):
415
"""See Repository.find_branches()."""
416
# should be an API call to the server.
418
return self._real_repository.find_branches(using=using)
420
def get_physical_lock_status(self):
421
"""See Repository.get_physical_lock_status()."""
422
# should be an API call to the server.
424
return self._real_repository.get_physical_lock_status()
426
def is_in_write_group(self):
427
"""Return True if there is an open write group.
429
write groups are only applicable locally for the smart server..
431
if self._real_repository:
432
return self._real_repository.is_in_write_group()
435
return self._lock_count >= 1
438
"""See Repository.is_shared()."""
439
path = self.bzrdir._path_for_remote_call(self._client)
440
response = self._client.call('Repository.is_shared', path)
441
assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
442
return response[0] == 'yes'
444
def is_write_locked(self):
445
return self._lock_mode == 'w'
448
# wrong eventually - want a local lock cache context
449
if not self._lock_mode:
450
self._lock_mode = 'r'
452
if self._real_repository is not None:
453
self._real_repository.lock_read()
455
self._lock_count += 1
457
def _remote_lock_write(self, token):
458
path = self.bzrdir._path_for_remote_call(self._client)
461
response = self._client.call('Repository.lock_write', path, token)
462
if response[0] == 'ok':
465
elif response[0] == 'LockContention':
466
raise errors.LockContention('(remote lock)')
467
elif response[0] == 'UnlockableTransport':
468
raise errors.UnlockableTransport(self.bzrdir.root_transport)
469
elif response[0] == 'LockFailed':
470
raise errors.LockFailed(response[1], response[2])
472
raise errors.UnexpectedSmartServerResponse(response)
474
def lock_write(self, token=None):
475
if not self._lock_mode:
476
self._lock_token = self._remote_lock_write(token)
477
# if self._lock_token is None, then this is something like packs or
478
# svn where we don't get to lock the repo, or a weave style repository
479
# where we cannot lock it over the wire and attempts to do so will
481
if self._real_repository is not None:
482
self._real_repository.lock_write(token=self._lock_token)
483
if token is not None:
484
self._leave_lock = True
486
self._leave_lock = False
487
self._lock_mode = 'w'
489
elif self._lock_mode == 'r':
490
raise errors.ReadOnlyError(self)
492
self._lock_count += 1
493
return self._lock_token or None
495
def leave_lock_in_place(self):
496
if not self._lock_token:
497
raise NotImplementedError(self.leave_lock_in_place)
498
self._leave_lock = True
500
def dont_leave_lock_in_place(self):
501
if not self._lock_token:
502
raise NotImplementedError(self.dont_leave_lock_in_place)
503
self._leave_lock = False
505
def _set_real_repository(self, repository):
506
"""Set the _real_repository for this repository.
508
:param repository: The repository to fallback to for non-hpss
509
implemented operations.
511
assert not isinstance(repository, RemoteRepository)
512
self._real_repository = repository
513
if self._lock_mode == 'w':
514
# if we are already locked, the real repository must be able to
515
# acquire the lock with our token.
516
self._real_repository.lock_write(self._lock_token)
517
elif self._lock_mode == 'r':
518
self._real_repository.lock_read()
520
def start_write_group(self):
521
"""Start a write group on the decorated repository.
523
Smart methods peform operations in a single step so this api
524
is not really applicable except as a compatibility thunk
525
for older plugins that don't use e.g. the CommitBuilder
529
return self._real_repository.start_write_group()
531
def _unlock(self, token):
532
path = self.bzrdir._path_for_remote_call(self._client)
534
# with no token the remote repository is not persistently locked.
536
response = self._client.call('Repository.unlock', path, token)
537
if response == ('ok',):
539
elif response[0] == 'TokenMismatch':
540
raise errors.TokenMismatch(token, '(remote token)')
542
raise errors.UnexpectedSmartServerResponse(response)
545
self._lock_count -= 1
546
if self._lock_count > 0:
548
old_mode = self._lock_mode
549
self._lock_mode = None
551
# The real repository is responsible at present for raising an
552
# exception if it's in an unfinished write group. However, it
553
# normally will *not* actually remove the lock from disk - that's
554
# done by the server on receiving the Repository.unlock call.
555
# This is just to let the _real_repository stay up to date.
556
if self._real_repository is not None:
557
self._real_repository.unlock()
559
# The rpc-level lock should be released even if there was a
560
# problem releasing the vfs-based lock.
562
# Only write-locked repositories need to make a remote method
563
# call to perfom the unlock.
564
old_token = self._lock_token
565
self._lock_token = None
566
if not self._leave_lock:
567
self._unlock(old_token)
569
def break_lock(self):
570
# should hand off to the network
572
return self._real_repository.break_lock()
574
def _get_tarball(self, compression):
575
"""Return a TemporaryFile containing a repository tarball.
577
Returns None if the server does not support sending tarballs.
580
path = self.bzrdir._path_for_remote_call(self._client)
581
response, protocol = self._client.call_expecting_body(
582
'Repository.tarball', path, compression)
583
if response[0] == 'ok':
584
# Extract the tarball and return it
585
t = tempfile.NamedTemporaryFile()
586
# TODO: rpc layer should read directly into it...
587
t.write(protocol.read_body_bytes())
590
if (response == ('error', "Generic bzr smart protocol error: "
591
"bad request 'Repository.tarball'") or
592
response == ('error', "Generic bzr smart protocol error: "
593
"bad request u'Repository.tarball'")):
594
protocol.cancel_read_body()
596
raise errors.UnexpectedSmartServerResponse(response)
598
def sprout(self, to_bzrdir, revision_id=None):
599
# TODO: Option to control what format is created?
601
dest_repo = self._real_repository._format.initialize(to_bzrdir,
603
dest_repo.fetch(self, revision_id=revision_id)
606
### These methods are just thin shims to the VFS object for now.
608
def revision_tree(self, revision_id):
610
return self._real_repository.revision_tree(revision_id)
612
def get_serializer_format(self):
614
return self._real_repository.get_serializer_format()
616
def get_commit_builder(self, branch, parents, config, timestamp=None,
617
timezone=None, committer=None, revprops=None,
619
# FIXME: It ought to be possible to call this without immediately
620
# triggering _ensure_real. For now it's the easiest thing to do.
622
builder = self._real_repository.get_commit_builder(branch, parents,
623
config, timestamp=timestamp, timezone=timezone,
624
committer=committer, revprops=revprops, revision_id=revision_id)
628
def add_inventory(self, revid, inv, parents):
630
return self._real_repository.add_inventory(revid, inv, parents)
633
def add_revision(self, rev_id, rev, inv=None, config=None):
635
return self._real_repository.add_revision(
636
rev_id, rev, inv=inv, config=config)
639
def get_inventory(self, revision_id):
641
return self._real_repository.get_inventory(revision_id)
643
def iter_inventories(self, revision_ids):
645
return self._real_repository.iter_inventories(revision_ids)
648
def get_revision(self, revision_id):
650
return self._real_repository.get_revision(revision_id)
653
def weave_store(self):
655
return self._real_repository.weave_store
657
def get_transaction(self):
659
return self._real_repository.get_transaction()
662
def clone(self, a_bzrdir, revision_id=None):
664
return self._real_repository.clone(a_bzrdir, revision_id=revision_id)
666
def make_working_trees(self):
667
"""RemoteRepositories never create working trees by default."""
670
def fetch(self, source, revision_id=None, pb=None):
671
if self.has_same_location(source):
672
# check that last_revision is in 'from' and then return a
674
if (revision_id is not None and
675
not revision.is_null(revision_id)):
676
self.get_revision(revision_id)
679
return self._real_repository.fetch(
680
source, revision_id=revision_id, pb=pb)
682
def create_bundle(self, target, base, fileobj, format=None):
684
self._real_repository.create_bundle(target, base, fileobj, format)
687
def control_weaves(self):
689
return self._real_repository.control_weaves
692
def get_ancestry(self, revision_id, topo_sorted=True):
694
return self._real_repository.get_ancestry(revision_id, topo_sorted)
697
def get_inventory_weave(self):
699
return self._real_repository.get_inventory_weave()
701
def fileids_altered_by_revision_ids(self, revision_ids):
703
return self._real_repository.fileids_altered_by_revision_ids(revision_ids)
705
def _get_versioned_file_checker(self, revisions, revision_versions_cache):
707
return self._real_repository._get_versioned_file_checker(
708
revisions, revision_versions_cache)
710
def iter_files_bytes(self, desired_files):
711
"""See Repository.iter_file_bytes.
714
return self._real_repository.iter_files_bytes(desired_files)
717
def get_signature_text(self, revision_id):
719
return self._real_repository.get_signature_text(revision_id)
722
def get_revision_graph_with_ghosts(self, revision_ids=None):
724
return self._real_repository.get_revision_graph_with_ghosts(
725
revision_ids=revision_ids)
728
def get_inventory_xml(self, revision_id):
730
return self._real_repository.get_inventory_xml(revision_id)
732
def deserialise_inventory(self, revision_id, xml):
734
return self._real_repository.deserialise_inventory(revision_id, xml)
736
def reconcile(self, other=None, thorough=False):
738
return self._real_repository.reconcile(other=other, thorough=thorough)
740
def all_revision_ids(self):
742
return self._real_repository.all_revision_ids()
745
def get_deltas_for_revisions(self, revisions):
747
return self._real_repository.get_deltas_for_revisions(revisions)
750
def get_revision_delta(self, revision_id):
752
return self._real_repository.get_revision_delta(revision_id)
755
def revision_trees(self, revision_ids):
757
return self._real_repository.revision_trees(revision_ids)
760
def get_revision_reconcile(self, revision_id):
762
return self._real_repository.get_revision_reconcile(revision_id)
765
def check(self, revision_ids=None):
767
return self._real_repository.check(revision_ids=revision_ids)
769
def copy_content_into(self, destination, revision_id=None):
771
return self._real_repository.copy_content_into(
772
destination, revision_id=revision_id)
774
def _copy_repository_tarball(self, to_bzrdir, revision_id=None):
775
# get a tarball of the remote repository, and copy from that into the
777
from bzrlib import osutils
780
# TODO: Maybe a progress bar while streaming the tarball?
781
note("Copying repository content as tarball...")
782
tar_file = self._get_tarball('bz2')
785
destination = to_bzrdir.create_repository()
787
tar = tarfile.open('repository', fileobj=tar_file,
789
tmpdir = tempfile.mkdtemp()
791
_extract_tar(tar, tmpdir)
792
tmp_bzrdir = BzrDir.open(tmpdir)
793
tmp_repo = tmp_bzrdir.open_repository()
794
tmp_repo.copy_content_into(destination, revision_id)
796
osutils.rmtree(tmpdir)
800
# TODO: Suggestion from john: using external tar is much faster than
801
# python's tarfile library, but it may not work on windows.
805
"""Compress the data within the repository.
807
This is not currently implemented within the smart server.
810
return self._real_repository.pack()
812
def set_make_working_trees(self, new_value):
813
raise NotImplementedError(self.set_make_working_trees)
816
def sign_revision(self, revision_id, gpg_strategy):
818
return self._real_repository.sign_revision(revision_id, gpg_strategy)
821
def get_revisions(self, revision_ids):
823
return self._real_repository.get_revisions(revision_ids)
825
def supports_rich_root(self):
827
return self._real_repository.supports_rich_root()
829
def iter_reverse_revision_history(self, revision_id):
831
return self._real_repository.iter_reverse_revision_history(revision_id)
834
def _serializer(self):
836
return self._real_repository._serializer
838
def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
840
return self._real_repository.store_revision_signature(
841
gpg_strategy, plaintext, revision_id)
843
def add_signature_text(self, revision_id, signature):
845
return self._real_repository.add_signature_text(revision_id, signature)
847
def has_signature_for_revision_id(self, revision_id):
849
return self._real_repository.has_signature_for_revision_id(revision_id)
851
def get_data_stream(self, revision_ids):
852
REQUEST_NAME = 'Repository.stream_revisions_chunked'
853
path = self.bzrdir._path_for_remote_call(self._client)
854
response, protocol = self._client.call_expecting_body(
855
REQUEST_NAME, path, *revision_ids)
857
if response == ('ok',):
858
return self._deserialise_stream(protocol)
859
elif (response == ('error', "Generic bzr smart protocol error: "
860
"bad request '%s'" % REQUEST_NAME) or
861
response == ('error', "Generic bzr smart protocol error: "
862
"bad request u'%s'" % REQUEST_NAME)):
863
protocol.cancel_read_body()
865
return self._real_repository.get_data_stream(revision_ids)
867
raise errors.UnexpectedSmartServerResponse(response)
869
def _deserialise_stream(self, protocol):
870
stream = protocol.read_streamed_body()
871
container_parser = ContainerPushParser()
873
container_parser.accept_bytes(bytes)
874
records = container_parser.read_pending_records()
875
for record_names, record_bytes in records:
876
if len(record_names) != 1:
877
# These records should have only one name, and that name
878
# should be a one-element tuple.
879
raise errors.SmartProtocolError(
880
'Repository data stream had invalid record name %r'
882
name_tuple = record_names[0]
883
yield name_tuple, record_bytes
885
def insert_data_stream(self, stream):
887
self._real_repository.insert_data_stream(stream)
889
def item_keys_introduced_by(self, revision_ids, _files_pb=None):
891
return self._real_repository.item_keys_introduced_by(revision_ids,
894
def revision_graph_can_have_wrong_parents(self):
895
# The answer depends on the remote repo format.
897
return self._real_repository.revision_graph_can_have_wrong_parents()
899
def _find_inconsistent_revision_parents(self):
901
return self._real_repository._find_inconsistent_revision_parents()
903
def _check_for_inconsistent_revision_parents(self):
905
return self._real_repository._check_for_inconsistent_revision_parents()
907
def _make_parents_provider(self):
909
return self._real_repository._make_parents_provider()
912
class RemoteBranchLockableFiles(LockableFiles):
913
"""A 'LockableFiles' implementation that talks to a smart server.
915
This is not a public interface class.
918
def __init__(self, bzrdir, _client):
920
self._client = _client
921
self._need_find_modes = True
922
LockableFiles.__init__(
923
self, bzrdir.get_branch_transport(None),
924
'lock', lockdir.LockDir)
926
def _find_modes(self):
927
# RemoteBranches don't let the client set the mode of control files.
928
self._dir_mode = None
929
self._file_mode = None
932
"""'get' a remote path as per the LockableFiles interface.
934
:param path: the file to 'get'. If this is 'branch.conf', we do not
935
just retrieve a file, instead we ask the smart server to generate
936
a configuration for us - which is retrieved as an INI file.
938
if path == 'branch.conf':
939
path = self.bzrdir._path_for_remote_call(self._client)
940
response = self._client.call_expecting_body(
941
'Branch.get_config_file', path)
942
assert response[0][0] == 'ok', \
943
'unexpected response code %s' % (response[0],)
944
return StringIO(response[1].read_body_bytes())
947
return LockableFiles.get(self, path)
950
class RemoteBranchFormat(branch.BranchFormat):
952
def __eq__(self, other):
953
return (isinstance(other, RemoteBranchFormat) and
954
self.__dict__ == other.__dict__)
956
def get_format_description(self):
957
return 'Remote BZR Branch'
959
def get_format_string(self):
960
return 'Remote BZR Branch'
962
def open(self, a_bzrdir):
963
assert isinstance(a_bzrdir, RemoteBzrDir)
964
return a_bzrdir.open_branch()
966
def initialize(self, a_bzrdir):
967
assert isinstance(a_bzrdir, RemoteBzrDir)
968
return a_bzrdir.create_branch()
970
def supports_tags(self):
971
# Remote branches might support tags, but we won't know until we
972
# access the real remote branch.
976
class RemoteBranch(branch.Branch):
977
"""Branch stored on a server accessed by HPSS RPC.
979
At the moment most operations are mapped down to simple file operations.
982
def __init__(self, remote_bzrdir, remote_repository, real_branch=None,
984
"""Create a RemoteBranch instance.
986
:param real_branch: An optional local implementation of the branch
987
format, usually accessing the data via the VFS.
988
:param _client: Private parameter for testing.
990
# We intentionally don't call the parent class's __init__, because it
991
# will try to assign to self.tags, which is a property in this subclass.
992
# And the parent's __init__ doesn't do much anyway.
993
self._revision_id_to_revno_cache = None
994
self._revision_history_cache = None
995
self.bzrdir = remote_bzrdir
996
if _client is not None:
997
self._client = _client
999
self._client = client._SmartClient(self.bzrdir._shared_medium)
1000
self.repository = remote_repository
1001
if real_branch is not None:
1002
self._real_branch = real_branch
1003
# Give the remote repository the matching real repo.
1004
real_repo = self._real_branch.repository
1005
if isinstance(real_repo, RemoteRepository):
1006
real_repo._ensure_real()
1007
real_repo = real_repo._real_repository
1008
self.repository._set_real_repository(real_repo)
1009
# Give the branch the remote repository to let fast-pathing happen.
1010
self._real_branch.repository = self.repository
1012
self._real_branch = None
1013
# Fill out expected attributes of branch for bzrlib api users.
1014
self._format = RemoteBranchFormat()
1015
self.base = self.bzrdir.root_transport.base
1016
self._control_files = None
1017
self._lock_mode = None
1018
self._lock_token = None
1019
self._lock_count = 0
1020
self._leave_lock = False
1023
return "%s(%s)" % (self.__class__.__name__, self.base)
1027
def _ensure_real(self):
1028
"""Ensure that there is a _real_branch set.
1030
Used before calls to self._real_branch.
1032
if not self._real_branch:
1033
assert vfs.vfs_enabled()
1034
self.bzrdir._ensure_real()
1035
self._real_branch = self.bzrdir._real_bzrdir.open_branch()
1036
# Give the remote repository the matching real repo.
1037
real_repo = self._real_branch.repository
1038
if isinstance(real_repo, RemoteRepository):
1039
real_repo._ensure_real()
1040
real_repo = real_repo._real_repository
1041
self.repository._set_real_repository(real_repo)
1042
# Give the branch the remote repository to let fast-pathing happen.
1043
self._real_branch.repository = self.repository
1044
# XXX: deal with _lock_mode == 'w'
1045
if self._lock_mode == 'r':
1046
self._real_branch.lock_read()
1049
def control_files(self):
1050
# Defer actually creating RemoteBranchLockableFiles until its needed,
1051
# because it triggers an _ensure_real that we otherwise might not need.
1052
if self._control_files is None:
1053
self._control_files = RemoteBranchLockableFiles(
1054
self.bzrdir, self._client)
1055
return self._control_files
1057
def _get_checkout_format(self):
1059
return self._real_branch._get_checkout_format()
1061
def get_physical_lock_status(self):
1062
"""See Branch.get_physical_lock_status()."""
1063
# should be an API call to the server, as branches must be lockable.
1065
return self._real_branch.get_physical_lock_status()
1067
def lock_read(self):
1068
if not self._lock_mode:
1069
self._lock_mode = 'r'
1070
self._lock_count = 1
1071
if self._real_branch is not None:
1072
self._real_branch.lock_read()
1074
self._lock_count += 1
1076
def _remote_lock_write(self, token):
1078
branch_token = repo_token = ''
1080
branch_token = token
1081
repo_token = self.repository.lock_write()
1082
self.repository.unlock()
1083
path = self.bzrdir._path_for_remote_call(self._client)
1084
response = self._client.call('Branch.lock_write', path, branch_token,
1086
if response[0] == 'ok':
1087
ok, branch_token, repo_token = response
1088
return branch_token, repo_token
1089
elif response[0] == 'LockContention':
1090
raise errors.LockContention('(remote lock)')
1091
elif response[0] == 'TokenMismatch':
1092
raise errors.TokenMismatch(token, '(remote token)')
1093
elif response[0] == 'UnlockableTransport':
1094
raise errors.UnlockableTransport(self.bzrdir.root_transport)
1095
elif response[0] == 'ReadOnlyError':
1096
raise errors.ReadOnlyError(self)
1097
elif response[0] == 'LockFailed':
1098
raise errors.LockFailed(response[1], response[2])
1100
raise errors.UnexpectedSmartServerResponse(response)
1102
def lock_write(self, token=None):
1103
if not self._lock_mode:
1104
remote_tokens = self._remote_lock_write(token)
1105
self._lock_token, self._repo_lock_token = remote_tokens
1106
assert self._lock_token, 'Remote server did not return a token!'
1107
# TODO: We really, really, really don't want to call _ensure_real
1108
# here, but it's the easiest way to ensure coherency between the
1109
# state of the RemoteBranch and RemoteRepository objects and the
1110
# physical locks. If we don't materialise the real objects here,
1111
# then getting everything in the right state later is complex, so
1112
# for now we just do it the lazy way.
1113
# -- Andrew Bennetts, 2007-02-22.
1115
if self._real_branch is not None:
1116
self._real_branch.repository.lock_write(
1117
token=self._repo_lock_token)
1119
self._real_branch.lock_write(token=self._lock_token)
1121
self._real_branch.repository.unlock()
1122
if token is not None:
1123
self._leave_lock = True
1125
# XXX: this case seems to be unreachable; token cannot be None.
1126
self._leave_lock = False
1127
self._lock_mode = 'w'
1128
self._lock_count = 1
1129
elif self._lock_mode == 'r':
1130
raise errors.ReadOnlyTransaction
1132
if token is not None:
1133
# A token was given to lock_write, and we're relocking, so check
1134
# that the given token actually matches the one we already have.
1135
if token != self._lock_token:
1136
raise errors.TokenMismatch(token, self._lock_token)
1137
self._lock_count += 1
1138
return self._lock_token or None
1140
def _unlock(self, branch_token, repo_token):
1141
path = self.bzrdir._path_for_remote_call(self._client)
1142
response = self._client.call('Branch.unlock', path, branch_token,
1144
if response == ('ok',):
1146
elif response[0] == 'TokenMismatch':
1147
raise errors.TokenMismatch(
1148
str((branch_token, repo_token)), '(remote tokens)')
1150
raise errors.UnexpectedSmartServerResponse(response)
1153
self._lock_count -= 1
1154
if not self._lock_count:
1155
self._clear_cached_state()
1156
mode = self._lock_mode
1157
self._lock_mode = None
1158
if self._real_branch is not None:
1159
if (not self._leave_lock and mode == 'w' and
1160
self._repo_lock_token):
1161
# If this RemoteBranch will remove the physical lock for the
1162
# repository, make sure the _real_branch doesn't do it
1163
# first. (Because the _real_branch's repository is set to
1164
# be the RemoteRepository.)
1165
self._real_branch.repository.leave_lock_in_place()
1166
self._real_branch.unlock()
1168
# Only write-locked branched need to make a remote method call
1169
# to perfom the unlock.
1171
assert self._lock_token, 'Locked, but no token!'
1172
branch_token = self._lock_token
1173
repo_token = self._repo_lock_token
1174
self._lock_token = None
1175
self._repo_lock_token = None
1176
if not self._leave_lock:
1177
self._unlock(branch_token, repo_token)
1179
def break_lock(self):
1181
return self._real_branch.break_lock()
1183
def leave_lock_in_place(self):
1184
if not self._lock_token:
1185
raise NotImplementedError(self.leave_lock_in_place)
1186
self._leave_lock = True
1188
def dont_leave_lock_in_place(self):
1189
if not self._lock_token:
1190
raise NotImplementedError(self.dont_leave_lock_in_place)
1191
self._leave_lock = False
1193
def last_revision_info(self):
1194
"""See Branch.last_revision_info()."""
1195
path = self.bzrdir._path_for_remote_call(self._client)
1196
response = self._client.call('Branch.last_revision_info', path)
1197
assert response[0] == 'ok', 'unexpected response code %s' % (response,)
1198
revno = int(response[1])
1199
last_revision = response[2]
1200
return (revno, last_revision)
1202
def _gen_revision_history(self):
1203
"""See Branch._gen_revision_history()."""
1204
path = self.bzrdir._path_for_remote_call(self._client)
1205
response = self._client.call_expecting_body(
1206
'Branch.revision_history', path)
1207
assert response[0][0] == 'ok', ('unexpected response code %s'
1209
result = response[1].read_body_bytes().split('\x00')
1215
def set_revision_history(self, rev_history):
1216
# Send just the tip revision of the history; the server will generate
1217
# the full history from that. If the revision doesn't exist in this
1218
# branch, NoSuchRevision will be raised.
1219
path = self.bzrdir._path_for_remote_call(self._client)
1220
if rev_history == []:
1223
rev_id = rev_history[-1]
1224
self._clear_cached_state()
1225
response = self._client.call('Branch.set_last_revision',
1226
path, self._lock_token, self._repo_lock_token, rev_id)
1227
if response[0] == 'NoSuchRevision':
1228
raise NoSuchRevision(self, rev_id)
1230
assert response == ('ok',), (
1231
'unexpected response code %r' % (response,))
1232
self._cache_revision_history(rev_history)
1234
def get_parent(self):
1236
return self._real_branch.get_parent()
1238
def set_parent(self, url):
1240
return self._real_branch.set_parent(url)
1242
def get_config(self):
1243
return RemoteBranchConfig(self)
1245
def sprout(self, to_bzrdir, revision_id=None):
1246
# Like Branch.sprout, except that it sprouts a branch in the default
1247
# format, because RemoteBranches can't be created at arbitrary URLs.
1248
# XXX: if to_bzrdir is a RemoteBranch, this should perhaps do
1249
# to_bzrdir.create_branch...
1251
result = self._real_branch._format.initialize(to_bzrdir)
1252
self.copy_content_into(result, revision_id=revision_id)
1253
result.set_parent(self.bzrdir.root_transport.base)
1257
def pull(self, source, overwrite=False, stop_revision=None,
1259
# FIXME: This asks the real branch to run the hooks, which means
1260
# they're called with the wrong target branch parameter.
1261
# The test suite specifically allows this at present but it should be
1262
# fixed. It should get a _override_hook_target branch,
1263
# as push does. -- mbp 20070405
1265
self._real_branch.pull(
1266
source, overwrite=overwrite, stop_revision=stop_revision,
1270
def push(self, target, overwrite=False, stop_revision=None):
1272
return self._real_branch.push(
1273
target, overwrite=overwrite, stop_revision=stop_revision,
1274
_override_hook_source_branch=self)
1276
def is_locked(self):
1277
return self._lock_count >= 1
1279
def set_last_revision_info(self, revno, revision_id):
1281
self._clear_cached_state()
1282
return self._real_branch.set_last_revision_info(revno, revision_id)
1284
def generate_revision_history(self, revision_id, last_rev=None,
1287
return self._real_branch.generate_revision_history(
1288
revision_id, last_rev=last_rev, other_branch=other_branch)
1293
return self._real_branch.tags
1295
def set_push_location(self, location):
1297
return self._real_branch.set_push_location(location)
1299
def update_revisions(self, other, stop_revision=None, overwrite=False):
1301
return self._real_branch.update_revisions(
1302
other, stop_revision=stop_revision, overwrite=overwrite)
1305
class RemoteBranchConfig(BranchConfig):
1308
self.branch._ensure_real()
1309
return self.branch._real_branch.get_config().username()
1311
def _get_branch_data_config(self):
1312
self.branch._ensure_real()
1313
if self._branch_data_config is None:
1314
self._branch_data_config = TreeConfig(self.branch._real_branch)
1315
return self._branch_data_config
1318
def _extract_tar(tar, to_dir):
1319
"""Extract all the contents of a tarfile object.
1321
A replacement for extractall, which is not present in python2.4
1324
tar.extract(tarinfo, to_dir)