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 Branch, 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 ContainerReader
36
from bzrlib.smart import client, vfs
37
from bzrlib.symbol_versioning import (
41
from bzrlib.trace import note
43
# Note: RemoteBzrDirFormat is in bzrdir.py
45
class RemoteBzrDir(BzrDir):
46
"""Control directory on a remote server, accessed via bzr:// or similar."""
48
def __init__(self, transport, _client=None):
49
"""Construct a RemoteBzrDir.
51
:param _client: Private parameter for testing. Disables probing and the
54
BzrDir.__init__(self, transport, RemoteBzrDirFormat())
55
# this object holds a delegated bzrdir that uses file-level operations
56
# to talk to the other side
57
self._real_bzrdir = None
60
self._shared_medium = transport.get_shared_medium()
61
self._client = client._SmartClient(self._shared_medium)
63
self._client = _client
64
self._shared_medium = None
67
path = self._path_for_remote_call(self._client)
68
response = self._client.call('BzrDir.open', path)
69
if response not in [('yes',), ('no',)]:
70
raise errors.UnexpectedSmartServerResponse(response)
71
if response == ('no',):
72
raise errors.NotBranchError(path=transport.base)
74
def _ensure_real(self):
75
"""Ensure that there is a _real_bzrdir set.
77
Used before calls to self._real_bzrdir.
79
if not self._real_bzrdir:
80
self._real_bzrdir = BzrDir.open_from_transport(
81
self.root_transport, _server_formats=False)
83
def create_repository(self, shared=False):
85
self._real_bzrdir.create_repository(shared=shared)
86
return self.open_repository()
88
def destroy_repository(self):
89
"""See BzrDir.destroy_repository"""
91
self._real_bzrdir.destroy_repository()
93
def create_branch(self):
95
real_branch = self._real_bzrdir.create_branch()
96
return RemoteBranch(self, self.find_repository(), real_branch)
98
def destroy_branch(self):
99
"""See BzrDir.destroy_branch"""
101
self._real_bzrdir.destroy_branch()
103
def create_workingtree(self, revision_id=None, from_branch=None):
104
raise errors.NotLocalUrl(self.transport.base)
106
def find_branch_format(self):
107
"""Find the branch 'format' for this bzrdir.
109
This might be a synthetic object for e.g. RemoteBranch and SVN.
111
b = self.open_branch()
114
def get_branch_reference(self):
115
"""See BzrDir.get_branch_reference()."""
116
path = self._path_for_remote_call(self._client)
117
response = self._client.call('BzrDir.open_branch', path)
118
if response[0] == 'ok':
119
if response[1] == '':
120
# branch at this location.
123
# a branch reference, use the existing BranchReference logic.
125
elif response == ('nobranch',):
126
raise errors.NotBranchError(path=self.root_transport.base)
128
raise errors.UnexpectedSmartServerResponse(response)
130
def open_branch(self, _unsupported=False):
131
assert _unsupported == False, 'unsupported flag support not implemented yet.'
132
reference_url = self.get_branch_reference()
133
if reference_url is None:
134
# branch at this location.
135
return RemoteBranch(self, self.find_repository())
137
# a branch reference, use the existing BranchReference logic.
138
format = BranchReferenceFormat()
139
return format.open(self, _found=True, location=reference_url)
141
def open_repository(self):
142
path = self._path_for_remote_call(self._client)
143
response = self._client.call('BzrDir.find_repository', path)
144
assert response[0] in ('ok', 'norepository'), \
145
'unexpected response code %s' % (response,)
146
if response[0] == 'norepository':
147
raise errors.NoRepositoryPresent(self)
148
assert len(response) == 4, 'incorrect response length %s' % (response,)
149
if response[1] == '':
150
format = RemoteRepositoryFormat()
151
format.rich_root_data = (response[2] == 'yes')
152
format.supports_tree_reference = (response[3] == 'yes')
153
return RemoteRepository(self, format)
155
raise errors.NoRepositoryPresent(self)
157
def open_workingtree(self, recommend_upgrade=True):
159
if self._real_bzrdir.has_workingtree():
160
raise errors.NotLocalUrl(self.root_transport)
162
raise errors.NoWorkingTree(self.root_transport.base)
164
def _path_for_remote_call(self, client):
165
"""Return the path to be used for this bzrdir in a remote call."""
166
return client.remote_path_from_transport(self.root_transport)
168
def get_branch_transport(self, branch_format):
170
return self._real_bzrdir.get_branch_transport(branch_format)
172
def get_repository_transport(self, repository_format):
174
return self._real_bzrdir.get_repository_transport(repository_format)
176
def get_workingtree_transport(self, workingtree_format):
178
return self._real_bzrdir.get_workingtree_transport(workingtree_format)
180
def can_convert_format(self):
181
"""Upgrading of remote bzrdirs is not supported yet."""
184
def needs_format_conversion(self, format=None):
185
"""Upgrading of remote bzrdirs is not supported yet."""
188
def clone(self, url, revision_id=None, force_new_repo=False):
190
return self._real_bzrdir.clone(url, revision_id=revision_id,
191
force_new_repo=force_new_repo)
194
class RemoteRepositoryFormat(repository.RepositoryFormat):
195
"""Format for repositories accessed over a _SmartClient.
197
Instances of this repository are represented by RemoteRepository
200
The RemoteRepositoryFormat is parameterized during construction
201
to reflect the capabilities of the real, remote format. Specifically
202
the attributes rich_root_data and supports_tree_reference are set
203
on a per instance basis, and are not set (and should not be) at
207
_matchingbzrdir = RemoteBzrDirFormat
209
def initialize(self, a_bzrdir, shared=False):
210
assert isinstance(a_bzrdir, RemoteBzrDir), \
211
'%r is not a RemoteBzrDir' % (a_bzrdir,)
212
return a_bzrdir.create_repository(shared=shared)
214
def open(self, a_bzrdir):
215
assert isinstance(a_bzrdir, RemoteBzrDir)
216
return a_bzrdir.open_repository()
218
def get_format_description(self):
219
return 'bzr remote repository'
221
def __eq__(self, other):
222
return self.__class__ == other.__class__
224
def check_conversion_target(self, target_format):
225
if self.rich_root_data and not target_format.rich_root_data:
226
raise errors.BadConversionTarget(
227
'Does not support rich root data.', target_format)
228
if (self.supports_tree_reference and
229
not getattr(target_format, 'supports_tree_reference', False)):
230
raise errors.BadConversionTarget(
231
'Does not support nested trees', target_format)
234
class RemoteRepository(object):
235
"""Repository accessed over rpc.
237
For the moment most operations are performed using local transport-backed
241
def __init__(self, remote_bzrdir, format, real_repository=None, _client=None):
242
"""Create a RemoteRepository instance.
244
:param remote_bzrdir: The bzrdir hosting this repository.
245
:param format: The RemoteFormat object to use.
246
:param real_repository: If not None, a local implementation of the
247
repository logic for the repository, usually accessing the data
249
:param _client: Private testing parameter - override the smart client
250
to be used by the repository.
253
self._real_repository = real_repository
255
self._real_repository = None
256
self.bzrdir = remote_bzrdir
258
self._client = client._SmartClient(self.bzrdir._shared_medium)
260
self._client = _client
261
self._format = format
262
self._lock_mode = None
263
self._lock_token = None
265
self._leave_lock = False
267
# These depend on the actual remote format, so force them off for
268
# maximum compatibility. XXX: In future these should depend on the
269
# remote repository instance, but this is irrelevant until we perform
270
# reconcile via an RPC call.
271
self._reconcile_does_inventory_gc = False
272
self._reconcile_fixes_text_parents = False
273
self._reconcile_backsup_inventory = False
274
self.base = self.bzrdir.transport.base
277
return "%s(%s)" % (self.__class__.__name__, self.base)
281
def abort_write_group(self):
282
"""Complete a write group on the decorated repository.
284
Smart methods peform operations in a single step so this api
285
is not really applicable except as a compatibility thunk
286
for older plugins that don't use e.g. the CommitBuilder
290
return self._real_repository.abort_write_group()
292
def commit_write_group(self):
293
"""Complete a write group on the decorated repository.
295
Smart methods peform operations in a single step so this api
296
is not really applicable except as a compatibility thunk
297
for older plugins that don't use e.g. the CommitBuilder
301
return self._real_repository.commit_write_group()
303
def _ensure_real(self):
304
"""Ensure that there is a _real_repository set.
306
Used before calls to self._real_repository.
308
if not self._real_repository:
309
self.bzrdir._ensure_real()
310
#self._real_repository = self.bzrdir._real_bzrdir.open_repository()
311
self._set_real_repository(self.bzrdir._real_bzrdir.open_repository())
313
def find_text_key_references(self):
314
"""Find the text key references within the repository.
316
:return: a dictionary mapping (file_id, revision_id) tuples to altered file-ids to an iterable of
317
revision_ids. Each altered file-ids has the exact revision_ids that
318
altered it listed explicitly.
319
:return: A dictionary mapping text keys ((fileid, revision_id) tuples)
320
to whether they were referred to by the inventory of the
321
revision_id that they contain. The inventory texts from all present
322
revision ids are assessed to generate this report.
325
return self._real_repository.find_text_key_references()
327
def _generate_text_key_index(self):
328
"""Generate a new text key index for the repository.
330
This is an expensive function that will take considerable time to run.
332
:return: A dict mapping (file_id, revision_id) tuples to a list of
333
parents, also (file_id, revision_id) tuples.
336
return self._real_repository._generate_text_key_index()
338
def get_revision_graph(self, revision_id=None):
339
"""See Repository.get_revision_graph()."""
340
if revision_id is None:
342
elif revision.is_null(revision_id):
345
path = self.bzrdir._path_for_remote_call(self._client)
346
assert type(revision_id) is str
347
response = self._client.call_expecting_body(
348
'Repository.get_revision_graph', path, revision_id)
349
if response[0][0] not in ['ok', 'nosuchrevision']:
350
raise errors.UnexpectedSmartServerResponse(response[0])
351
if response[0][0] == 'ok':
352
coded = response[1].read_body_bytes()
354
# no revisions in this repository!
356
lines = coded.split('\n')
359
d = tuple(line.split())
360
revision_graph[d[0]] = d[1:]
362
return revision_graph
364
response_body = response[1].read_body_bytes()
365
assert response_body == ''
366
raise NoSuchRevision(self, revision_id)
368
def has_revision(self, revision_id):
369
"""See Repository.has_revision()."""
370
if revision_id is None:
371
# The null revision is always present.
373
path = self.bzrdir._path_for_remote_call(self._client)
374
response = self._client.call('Repository.has_revision', path, revision_id)
375
assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
376
return response[0] == 'yes'
378
def has_same_location(self, other):
379
return (self.__class__ == other.__class__ and
380
self.bzrdir.transport.base == other.bzrdir.transport.base)
382
def get_graph(self, other_repository=None):
383
"""Return the graph for this repository format"""
385
return self._real_repository.get_graph(other_repository)
387
def gather_stats(self, revid=None, committers=None):
388
"""See Repository.gather_stats()."""
389
path = self.bzrdir._path_for_remote_call(self._client)
390
# revid can be None to indicate no revisions, not just NULL_REVISION
391
if revid is None or revision.is_null(revid):
395
if committers is None or not committers:
396
fmt_committers = 'no'
398
fmt_committers = 'yes'
399
response = self._client.call_expecting_body(
400
'Repository.gather_stats', path, fmt_revid, fmt_committers)
401
assert response[0][0] == 'ok', \
402
'unexpected response code %s' % (response[0],)
404
body = response[1].read_body_bytes()
406
for line in body.split('\n'):
409
key, val_text = line.split(':')
410
if key in ('revisions', 'size', 'committers'):
411
result[key] = int(val_text)
412
elif key in ('firstrev', 'latestrev'):
413
values = val_text.split(' ')[1:]
414
result[key] = (float(values[0]), long(values[1]))
418
def get_physical_lock_status(self):
419
"""See Repository.get_physical_lock_status()."""
420
# should be an API call to the server.
422
return self._real_repository.get_physical_lock_status()
424
def is_in_write_group(self):
425
"""Return True if there is an open write group.
427
write groups are only applicable locally for the smart server..
429
if self._real_repository:
430
return self._real_repository.is_in_write_group()
433
return self._lock_count >= 1
436
"""See Repository.is_shared()."""
437
path = self.bzrdir._path_for_remote_call(self._client)
438
response = self._client.call('Repository.is_shared', path)
439
assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
440
return response[0] == 'yes'
442
def is_write_locked(self):
443
return self._lock_mode == 'w'
446
# wrong eventually - want a local lock cache context
447
if not self._lock_mode:
448
self._lock_mode = 'r'
450
if self._real_repository is not None:
451
self._real_repository.lock_read()
453
self._lock_count += 1
455
def _remote_lock_write(self, token):
456
path = self.bzrdir._path_for_remote_call(self._client)
459
response = self._client.call('Repository.lock_write', path, token)
460
if response[0] == 'ok':
463
elif response[0] == 'LockContention':
464
raise errors.LockContention('(remote lock)')
465
elif response[0] == 'UnlockableTransport':
466
raise errors.UnlockableTransport(self.bzrdir.root_transport)
467
elif response[0] == 'LockFailed':
468
raise errors.LockFailed(response[1], response[2])
470
raise errors.UnexpectedSmartServerResponse(response)
472
def lock_write(self, token=None):
473
if not self._lock_mode:
474
self._lock_token = self._remote_lock_write(token)
475
# if self._lock_token is None, then this is something like packs or
476
# svn where we don't get to lock the repo, or a weave style repository
477
# where we cannot lock it over the wire and attempts to do so will
479
if self._real_repository is not None:
480
self._real_repository.lock_write(token=self._lock_token)
481
if token is not None:
482
self._leave_lock = True
484
self._leave_lock = False
485
self._lock_mode = 'w'
487
elif self._lock_mode == 'r':
488
raise errors.ReadOnlyError(self)
490
self._lock_count += 1
491
return self._lock_token or None
493
def leave_lock_in_place(self):
494
if not self._lock_token:
495
raise NotImplementedError(self.leave_lock_in_place)
496
self._leave_lock = True
498
def dont_leave_lock_in_place(self):
499
if not self._lock_token:
500
raise NotImplementedError(self.dont_leave_lock_in_place)
501
self._leave_lock = False
503
def _set_real_repository(self, repository):
504
"""Set the _real_repository for this repository.
506
:param repository: The repository to fallback to for non-hpss
507
implemented operations.
509
assert not isinstance(repository, RemoteRepository)
510
self._real_repository = repository
511
if self._lock_mode == 'w':
512
# if we are already locked, the real repository must be able to
513
# acquire the lock with our token.
514
self._real_repository.lock_write(self._lock_token)
515
elif self._lock_mode == 'r':
516
self._real_repository.lock_read()
518
def start_write_group(self):
519
"""Start a write group on the decorated repository.
521
Smart methods peform operations in a single step so this api
522
is not really applicable except as a compatibility thunk
523
for older plugins that don't use e.g. the CommitBuilder
527
return self._real_repository.start_write_group()
529
def _unlock(self, token):
530
path = self.bzrdir._path_for_remote_call(self._client)
532
# with no token the remote repository is not persistently locked.
534
response = self._client.call('Repository.unlock', path, token)
535
if response == ('ok',):
537
elif response[0] == 'TokenMismatch':
538
raise errors.TokenMismatch(token, '(remote token)')
540
raise errors.UnexpectedSmartServerResponse(response)
543
self._lock_count -= 1
544
if self._lock_count > 0:
546
old_mode = self._lock_mode
547
self._lock_mode = None
549
# The real repository is responsible at present for raising an
550
# exception if it's in an unfinished write group. However, it
551
# normally will *not* actually remove the lock from disk - that's
552
# done by the server on receiving the Repository.unlock call.
553
# This is just to let the _real_repository stay up to date.
554
if self._real_repository is not None:
555
self._real_repository.unlock()
557
# The rpc-level lock should be released even if there was a
558
# problem releasing the vfs-based lock.
560
# Only write-locked repositories need to make a remote method
561
# call to perfom the unlock.
562
old_token = self._lock_token
563
self._lock_token = None
564
if not self._leave_lock:
565
self._unlock(old_token)
567
def break_lock(self):
568
# should hand off to the network
570
return self._real_repository.break_lock()
572
def _get_tarball(self, compression):
573
"""Return a TemporaryFile containing a repository tarball.
575
Returns None if the server does not support sending tarballs.
578
path = self.bzrdir._path_for_remote_call(self._client)
579
response, protocol = self._client.call_expecting_body(
580
'Repository.tarball', path, compression)
581
if response[0] == 'ok':
582
# Extract the tarball and return it
583
t = tempfile.NamedTemporaryFile()
584
# TODO: rpc layer should read directly into it...
585
t.write(protocol.read_body_bytes())
588
if (response == ('error', "Generic bzr smart protocol error: "
589
"bad request 'Repository.tarball'") or
590
response == ('error', "Generic bzr smart protocol error: "
591
"bad request u'Repository.tarball'")):
592
protocol.cancel_read_body()
594
raise errors.UnexpectedSmartServerResponse(response)
596
def sprout(self, to_bzrdir, revision_id=None):
597
# TODO: Option to control what format is created?
599
dest_repo = self._real_repository._format.initialize(to_bzrdir,
601
dest_repo.fetch(self, revision_id=revision_id)
604
### These methods are just thin shims to the VFS object for now.
606
def revision_tree(self, revision_id):
608
return self._real_repository.revision_tree(revision_id)
610
def get_serializer_format(self):
612
return self._real_repository.get_serializer_format()
614
def get_commit_builder(self, branch, parents, config, timestamp=None,
615
timezone=None, committer=None, revprops=None,
617
# FIXME: It ought to be possible to call this without immediately
618
# triggering _ensure_real. For now it's the easiest thing to do.
620
builder = self._real_repository.get_commit_builder(branch, parents,
621
config, timestamp=timestamp, timezone=timezone,
622
committer=committer, revprops=revprops, revision_id=revision_id)
626
def add_inventory(self, revid, inv, parents):
628
return self._real_repository.add_inventory(revid, inv, parents)
631
def add_revision(self, rev_id, rev, inv=None, config=None):
633
return self._real_repository.add_revision(
634
rev_id, rev, inv=inv, config=config)
637
def get_inventory(self, revision_id):
639
return self._real_repository.get_inventory(revision_id)
642
def get_revision(self, revision_id):
644
return self._real_repository.get_revision(revision_id)
647
def weave_store(self):
649
return self._real_repository.weave_store
651
def get_transaction(self):
653
return self._real_repository.get_transaction()
656
def clone(self, a_bzrdir, revision_id=None):
658
return self._real_repository.clone(a_bzrdir, revision_id=revision_id)
660
def make_working_trees(self):
661
"""RemoteRepositories never create working trees by default."""
664
def fetch(self, source, revision_id=None, pb=None):
665
if self.has_same_location(source):
666
# check that last_revision is in 'from' and then return a
668
if (revision_id is not None and
669
not revision.is_null(revision_id)):
670
self.get_revision(revision_id)
673
return self._real_repository.fetch(
674
source, revision_id=revision_id, pb=pb)
676
def create_bundle(self, target, base, fileobj, format=None):
678
self._real_repository.create_bundle(target, base, fileobj, format)
681
def control_weaves(self):
683
return self._real_repository.control_weaves
686
def get_ancestry(self, revision_id, topo_sorted=True):
688
return self._real_repository.get_ancestry(revision_id, topo_sorted)
691
def get_inventory_weave(self):
693
return self._real_repository.get_inventory_weave()
695
def fileids_altered_by_revision_ids(self, revision_ids):
697
return self._real_repository.fileids_altered_by_revision_ids(revision_ids)
699
def _get_versioned_file_checker(self, revisions, revision_versions_cache):
701
return self._real_repository._get_versioned_file_checker(
702
revisions, revision_versions_cache)
704
def iter_files_bytes(self, desired_files):
705
"""See Repository.iter_file_bytes.
708
return self._real_repository.iter_files_bytes(desired_files)
711
def get_signature_text(self, revision_id):
713
return self._real_repository.get_signature_text(revision_id)
716
def get_revision_graph_with_ghosts(self, revision_ids=None):
718
return self._real_repository.get_revision_graph_with_ghosts(
719
revision_ids=revision_ids)
722
def get_inventory_xml(self, revision_id):
724
return self._real_repository.get_inventory_xml(revision_id)
726
def deserialise_inventory(self, revision_id, xml):
728
return self._real_repository.deserialise_inventory(revision_id, xml)
730
def reconcile(self, other=None, thorough=False):
732
return self._real_repository.reconcile(other=other, thorough=thorough)
734
def all_revision_ids(self):
736
return self._real_repository.all_revision_ids()
739
def get_deltas_for_revisions(self, revisions):
741
return self._real_repository.get_deltas_for_revisions(revisions)
744
def get_revision_delta(self, revision_id):
746
return self._real_repository.get_revision_delta(revision_id)
749
def revision_trees(self, revision_ids):
751
return self._real_repository.revision_trees(revision_ids)
754
def get_revision_reconcile(self, revision_id):
756
return self._real_repository.get_revision_reconcile(revision_id)
759
def check(self, revision_ids=None):
761
return self._real_repository.check(revision_ids=revision_ids)
763
def copy_content_into(self, destination, revision_id=None):
765
return self._real_repository.copy_content_into(
766
destination, revision_id=revision_id)
768
def _copy_repository_tarball(self, to_bzrdir, revision_id=None):
769
# get a tarball of the remote repository, and copy from that into the
771
from bzrlib import osutils
774
from StringIO import StringIO
775
# TODO: Maybe a progress bar while streaming the tarball?
776
note("Copying repository content as tarball...")
777
tar_file = self._get_tarball('bz2')
780
destination = to_bzrdir.create_repository()
782
tar = tarfile.open('repository', fileobj=tar_file,
784
tmpdir = tempfile.mkdtemp()
786
_extract_tar(tar, tmpdir)
787
tmp_bzrdir = BzrDir.open(tmpdir)
788
tmp_repo = tmp_bzrdir.open_repository()
789
tmp_repo.copy_content_into(destination, revision_id)
791
osutils.rmtree(tmpdir)
795
# TODO: Suggestion from john: using external tar is much faster than
796
# python's tarfile library, but it may not work on windows.
800
"""Compress the data within the repository.
802
This is not currently implemented within the smart server.
805
return self._real_repository.pack()
807
def set_make_working_trees(self, new_value):
808
raise NotImplementedError(self.set_make_working_trees)
811
def sign_revision(self, revision_id, gpg_strategy):
813
return self._real_repository.sign_revision(revision_id, gpg_strategy)
816
def get_revisions(self, revision_ids):
818
return self._real_repository.get_revisions(revision_ids)
820
def supports_rich_root(self):
822
return self._real_repository.supports_rich_root()
824
def iter_reverse_revision_history(self, revision_id):
826
return self._real_repository.iter_reverse_revision_history(revision_id)
829
def _serializer(self):
831
return self._real_repository._serializer
833
def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
835
return self._real_repository.store_revision_signature(
836
gpg_strategy, plaintext, revision_id)
838
def add_signature_text(self, revision_id, signature):
840
return self._real_repository.add_signature_text(revision_id, signature)
842
def has_signature_for_revision_id(self, revision_id):
844
return self._real_repository.has_signature_for_revision_id(revision_id)
846
def get_data_stream(self, revision_ids):
847
path = self.bzrdir._path_for_remote_call(self._client)
848
response, protocol = self._client.call_expecting_body(
849
'Repository.stream_knit_data_for_revisions', path, *revision_ids)
850
if response == ('ok',):
851
return self._deserialise_stream(protocol)
852
elif (response == ('error', "Generic bzr smart protocol error: "
853
"bad request 'Repository.stream_knit_data_for_revisions'") or
854
response == ('error', "Generic bzr smart protocol error: "
855
"bad request u'Repository.stream_knit_data_for_revisions'")):
856
protocol.cancel_read_body()
858
return self._real_repository.get_data_stream(revision_ids)
860
raise errors.UnexpectedSmartServerResponse(response)
862
def _deserialise_stream(self, protocol):
863
buffer = StringIO(protocol.read_body_bytes())
864
reader = ContainerReader(buffer)
865
for record_names, read_bytes in reader.iter_records():
867
# These records should have only one name, and that name
868
# should be a one-element tuple.
869
[name_tuple] = record_names
871
raise errors.SmartProtocolError(
872
'Repository data stream had invalid record name %r'
874
yield name_tuple, read_bytes(None)
876
def insert_data_stream(self, stream):
878
self._real_repository.insert_data_stream(stream)
880
def item_keys_introduced_by(self, revision_ids, _files_pb=None):
882
return self._real_repository.item_keys_introduced_by(revision_ids,
885
def revision_graph_can_have_wrong_parents(self):
886
# The answer depends on the remote repo format.
888
return self._real_repository.revision_graph_can_have_wrong_parents()
890
def _find_inconsistent_revision_parents(self):
892
return self._real_repository._find_inconsistent_revision_parents()
894
def _check_for_inconsistent_revision_parents(self):
896
return self._real_repository._check_for_inconsistent_revision_parents()
898
def _make_parents_provider(self):
900
return self._real_repository._make_parents_provider()
903
class RemoteBranchLockableFiles(LockableFiles):
904
"""A 'LockableFiles' implementation that talks to a smart server.
906
This is not a public interface class.
909
def __init__(self, bzrdir, _client):
911
self._client = _client
912
self._need_find_modes = True
913
LockableFiles.__init__(
914
self, bzrdir.get_branch_transport(None),
915
'lock', lockdir.LockDir)
917
def _find_modes(self):
918
# RemoteBranches don't let the client set the mode of control files.
919
self._dir_mode = None
920
self._file_mode = None
923
"""'get' a remote path as per the LockableFiles interface.
925
:param path: the file to 'get'. If this is 'branch.conf', we do not
926
just retrieve a file, instead we ask the smart server to generate
927
a configuration for us - which is retrieved as an INI file.
929
if path == 'branch.conf':
930
path = self.bzrdir._path_for_remote_call(self._client)
931
response = self._client.call_expecting_body(
932
'Branch.get_config_file', path)
933
assert response[0][0] == 'ok', \
934
'unexpected response code %s' % (response[0],)
935
return StringIO(response[1].read_body_bytes())
938
return LockableFiles.get(self, path)
941
class RemoteBranchFormat(branch.BranchFormat):
943
def __eq__(self, other):
944
return (isinstance(other, RemoteBranchFormat) and
945
self.__dict__ == other.__dict__)
947
def get_format_description(self):
948
return 'Remote BZR Branch'
950
def get_format_string(self):
951
return 'Remote BZR Branch'
953
def open(self, a_bzrdir):
954
assert isinstance(a_bzrdir, RemoteBzrDir)
955
return a_bzrdir.open_branch()
957
def initialize(self, a_bzrdir):
958
assert isinstance(a_bzrdir, RemoteBzrDir)
959
return a_bzrdir.create_branch()
961
def supports_tags(self):
962
# Remote branches might support tags, but we won't know until we
963
# access the real remote branch.
967
class RemoteBranch(branch.Branch):
968
"""Branch stored on a server accessed by HPSS RPC.
970
At the moment most operations are mapped down to simple file operations.
973
def __init__(self, remote_bzrdir, remote_repository, real_branch=None,
975
"""Create a RemoteBranch instance.
977
:param real_branch: An optional local implementation of the branch
978
format, usually accessing the data via the VFS.
979
:param _client: Private parameter for testing.
981
# We intentionally don't call the parent class's __init__, because it
982
# will try to assign to self.tags, which is a property in this subclass.
983
# And the parent's __init__ doesn't do much anyway.
984
self._revision_id_to_revno_cache = None
985
self._revision_history_cache = None
986
self.bzrdir = remote_bzrdir
987
if _client is not None:
988
self._client = _client
990
self._client = client._SmartClient(self.bzrdir._shared_medium)
991
self.repository = remote_repository
992
if real_branch is not None:
993
self._real_branch = real_branch
994
# Give the remote repository the matching real repo.
995
real_repo = self._real_branch.repository
996
if isinstance(real_repo, RemoteRepository):
997
real_repo._ensure_real()
998
real_repo = real_repo._real_repository
999
self.repository._set_real_repository(real_repo)
1000
# Give the branch the remote repository to let fast-pathing happen.
1001
self._real_branch.repository = self.repository
1003
self._real_branch = None
1004
# Fill out expected attributes of branch for bzrlib api users.
1005
self._format = RemoteBranchFormat()
1006
self.base = self.bzrdir.root_transport.base
1007
self._control_files = None
1008
self._lock_mode = None
1009
self._lock_token = None
1010
self._lock_count = 0
1011
self._leave_lock = False
1014
return "%s(%s)" % (self.__class__.__name__, self.base)
1018
def _ensure_real(self):
1019
"""Ensure that there is a _real_branch set.
1021
Used before calls to self._real_branch.
1023
if not self._real_branch:
1024
assert vfs.vfs_enabled()
1025
self.bzrdir._ensure_real()
1026
self._real_branch = self.bzrdir._real_bzrdir.open_branch()
1027
# Give the remote repository the matching real repo.
1028
real_repo = self._real_branch.repository
1029
if isinstance(real_repo, RemoteRepository):
1030
real_repo._ensure_real()
1031
real_repo = real_repo._real_repository
1032
self.repository._set_real_repository(real_repo)
1033
# Give the branch the remote repository to let fast-pathing happen.
1034
self._real_branch.repository = self.repository
1035
# XXX: deal with _lock_mode == 'w'
1036
if self._lock_mode == 'r':
1037
self._real_branch.lock_read()
1040
def control_files(self):
1041
# Defer actually creating RemoteBranchLockableFiles until its needed,
1042
# because it triggers an _ensure_real that we otherwise might not need.
1043
if self._control_files is None:
1044
self._control_files = RemoteBranchLockableFiles(
1045
self.bzrdir, self._client)
1046
return self._control_files
1048
def _get_checkout_format(self):
1050
return self._real_branch._get_checkout_format()
1052
def get_physical_lock_status(self):
1053
"""See Branch.get_physical_lock_status()."""
1054
# should be an API call to the server, as branches must be lockable.
1056
return self._real_branch.get_physical_lock_status()
1058
def lock_read(self):
1059
if not self._lock_mode:
1060
self._lock_mode = 'r'
1061
self._lock_count = 1
1062
if self._real_branch is not None:
1063
self._real_branch.lock_read()
1065
self._lock_count += 1
1067
def _remote_lock_write(self, token):
1069
branch_token = repo_token = ''
1071
branch_token = token
1072
repo_token = self.repository.lock_write()
1073
self.repository.unlock()
1074
path = self.bzrdir._path_for_remote_call(self._client)
1075
response = self._client.call('Branch.lock_write', path, branch_token,
1077
if response[0] == 'ok':
1078
ok, branch_token, repo_token = response
1079
return branch_token, repo_token
1080
elif response[0] == 'LockContention':
1081
raise errors.LockContention('(remote lock)')
1082
elif response[0] == 'TokenMismatch':
1083
raise errors.TokenMismatch(token, '(remote token)')
1084
elif response[0] == 'UnlockableTransport':
1085
raise errors.UnlockableTransport(self.bzrdir.root_transport)
1086
elif response[0] == 'ReadOnlyError':
1087
raise errors.ReadOnlyError(self)
1088
elif response[0] == 'LockFailed':
1089
raise errors.LockFailed(response[1], response[2])
1091
raise errors.UnexpectedSmartServerResponse(response)
1093
def lock_write(self, token=None):
1094
if not self._lock_mode:
1095
remote_tokens = self._remote_lock_write(token)
1096
self._lock_token, self._repo_lock_token = remote_tokens
1097
assert self._lock_token, 'Remote server did not return a token!'
1098
# TODO: We really, really, really don't want to call _ensure_real
1099
# here, but it's the easiest way to ensure coherency between the
1100
# state of the RemoteBranch and RemoteRepository objects and the
1101
# physical locks. If we don't materialise the real objects here,
1102
# then getting everything in the right state later is complex, so
1103
# for now we just do it the lazy way.
1104
# -- Andrew Bennetts, 2007-02-22.
1106
if self._real_branch is not None:
1107
self._real_branch.repository.lock_write(
1108
token=self._repo_lock_token)
1110
self._real_branch.lock_write(token=self._lock_token)
1112
self._real_branch.repository.unlock()
1113
if token is not None:
1114
self._leave_lock = True
1116
# XXX: this case seems to be unreachable; token cannot be None.
1117
self._leave_lock = False
1118
self._lock_mode = 'w'
1119
self._lock_count = 1
1120
elif self._lock_mode == 'r':
1121
raise errors.ReadOnlyTransaction
1123
if token is not None:
1124
# A token was given to lock_write, and we're relocking, so check
1125
# that the given token actually matches the one we already have.
1126
if token != self._lock_token:
1127
raise errors.TokenMismatch(token, self._lock_token)
1128
self._lock_count += 1
1129
return self._lock_token or None
1131
def _unlock(self, branch_token, repo_token):
1132
path = self.bzrdir._path_for_remote_call(self._client)
1133
response = self._client.call('Branch.unlock', path, branch_token,
1135
if response == ('ok',):
1137
elif response[0] == 'TokenMismatch':
1138
raise errors.TokenMismatch(
1139
str((branch_token, repo_token)), '(remote tokens)')
1141
raise errors.UnexpectedSmartServerResponse(response)
1144
self._lock_count -= 1
1145
if not self._lock_count:
1146
self._clear_cached_state()
1147
mode = self._lock_mode
1148
self._lock_mode = None
1149
if self._real_branch is not None:
1150
if (not self._leave_lock and mode == 'w' and
1151
self._repo_lock_token):
1152
# If this RemoteBranch will remove the physical lock for the
1153
# repository, make sure the _real_branch doesn't do it
1154
# first. (Because the _real_branch's repository is set to
1155
# be the RemoteRepository.)
1156
self._real_branch.repository.leave_lock_in_place()
1157
self._real_branch.unlock()
1159
# Only write-locked branched need to make a remote method call
1160
# to perfom the unlock.
1162
assert self._lock_token, 'Locked, but no token!'
1163
branch_token = self._lock_token
1164
repo_token = self._repo_lock_token
1165
self._lock_token = None
1166
self._repo_lock_token = None
1167
if not self._leave_lock:
1168
self._unlock(branch_token, repo_token)
1170
def break_lock(self):
1172
return self._real_branch.break_lock()
1174
def leave_lock_in_place(self):
1175
if not self._lock_token:
1176
raise NotImplementedError(self.leave_lock_in_place)
1177
self._leave_lock = True
1179
def dont_leave_lock_in_place(self):
1180
if not self._lock_token:
1181
raise NotImplementedError(self.dont_leave_lock_in_place)
1182
self._leave_lock = False
1184
def last_revision_info(self):
1185
"""See Branch.last_revision_info()."""
1186
path = self.bzrdir._path_for_remote_call(self._client)
1187
response = self._client.call('Branch.last_revision_info', path)
1188
assert response[0] == 'ok', 'unexpected response code %s' % (response,)
1189
revno = int(response[1])
1190
last_revision = response[2]
1191
return (revno, last_revision)
1193
def _gen_revision_history(self):
1194
"""See Branch._gen_revision_history()."""
1195
path = self.bzrdir._path_for_remote_call(self._client)
1196
response = self._client.call_expecting_body(
1197
'Branch.revision_history', path)
1198
assert response[0][0] == 'ok', ('unexpected response code %s'
1200
result = response[1].read_body_bytes().split('\x00')
1206
def set_revision_history(self, rev_history):
1207
# Send just the tip revision of the history; the server will generate
1208
# the full history from that. If the revision doesn't exist in this
1209
# branch, NoSuchRevision will be raised.
1210
path = self.bzrdir._path_for_remote_call(self._client)
1211
if rev_history == []:
1214
rev_id = rev_history[-1]
1215
self._clear_cached_state()
1216
response = self._client.call('Branch.set_last_revision',
1217
path, self._lock_token, self._repo_lock_token, rev_id)
1218
if response[0] == 'NoSuchRevision':
1219
raise NoSuchRevision(self, rev_id)
1221
assert response == ('ok',), (
1222
'unexpected response code %r' % (response,))
1223
self._cache_revision_history(rev_history)
1225
def get_parent(self):
1227
return self._real_branch.get_parent()
1229
def set_parent(self, url):
1231
return self._real_branch.set_parent(url)
1233
def get_config(self):
1234
return RemoteBranchConfig(self)
1236
def sprout(self, to_bzrdir, revision_id=None):
1237
# Like Branch.sprout, except that it sprouts a branch in the default
1238
# format, because RemoteBranches can't be created at arbitrary URLs.
1239
# XXX: if to_bzrdir is a RemoteBranch, this should perhaps do
1240
# to_bzrdir.create_branch...
1242
result = self._real_branch._format.initialize(to_bzrdir)
1243
self.copy_content_into(result, revision_id=revision_id)
1244
result.set_parent(self.bzrdir.root_transport.base)
1248
def pull(self, source, overwrite=False, stop_revision=None,
1250
# FIXME: This asks the real branch to run the hooks, which means
1251
# they're called with the wrong target branch parameter.
1252
# The test suite specifically allows this at present but it should be
1253
# fixed. It should get a _override_hook_target branch,
1254
# as push does. -- mbp 20070405
1256
self._real_branch.pull(
1257
source, overwrite=overwrite, stop_revision=stop_revision,
1261
def push(self, target, overwrite=False, stop_revision=None):
1263
return self._real_branch.push(
1264
target, overwrite=overwrite, stop_revision=stop_revision,
1265
_override_hook_source_branch=self)
1267
def is_locked(self):
1268
return self._lock_count >= 1
1270
def set_last_revision_info(self, revno, revision_id):
1272
self._clear_cached_state()
1273
return self._real_branch.set_last_revision_info(revno, revision_id)
1275
def generate_revision_history(self, revision_id, last_rev=None,
1278
return self._real_branch.generate_revision_history(
1279
revision_id, last_rev=last_rev, other_branch=other_branch)
1284
return self._real_branch.tags
1286
def set_push_location(self, location):
1288
return self._real_branch.set_push_location(location)
1290
def update_revisions(self, other, stop_revision=None, overwrite=False):
1292
return self._real_branch.update_revisions(
1293
other, stop_revision=stop_revision, overwrite=overwrite)
1296
class RemoteBranchConfig(BranchConfig):
1299
self.branch._ensure_real()
1300
return self.branch._real_branch.get_config().username()
1302
def _get_branch_data_config(self):
1303
self.branch._ensure_real()
1304
if self._branch_data_config is None:
1305
self._branch_data_config = TreeConfig(self.branch._real_branch)
1306
return self._branch_data_config
1309
def _extract_tar(tar, to_dir):
1310
"""Extract all the contents of a tarfile object.
1312
A replacement for extractall, which is not present in python2.4
1315
tar.extract(tarinfo, to_dir)