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
28
from bzrlib.branch import Branch, BranchReferenceFormat
29
from bzrlib.bzrdir import BzrDir, RemoteBzrDirFormat
30
from bzrlib.config import BranchConfig, TreeConfig
31
from bzrlib.decorators import needs_read_lock, needs_write_lock
32
from bzrlib.errors import NoSuchRevision
33
from bzrlib.lockable_files import LockableFiles
34
from bzrlib.pack import ContainerReader
35
from bzrlib.revision import NULL_REVISION
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 create_branch(self):
90
real_branch = self._real_bzrdir.create_branch()
91
return RemoteBranch(self, self.find_repository(), real_branch)
93
def destroy_branch(self):
94
"""See BzrDir.destroy_branch"""
96
self._real_bzrdir.destroy_branch()
98
def create_workingtree(self, revision_id=None):
99
raise errors.NotLocalUrl(self.transport.base)
101
def find_branch_format(self):
102
"""Find the branch 'format' for this bzrdir.
104
This might be a synthetic object for e.g. RemoteBranch and SVN.
106
b = self.open_branch()
109
def get_branch_reference(self):
110
"""See BzrDir.get_branch_reference()."""
111
path = self._path_for_remote_call(self._client)
112
response = self._client.call('BzrDir.open_branch', path)
113
if response[0] == 'ok':
114
if response[1] == '':
115
# branch at this location.
118
# a branch reference, use the existing BranchReference logic.
120
elif response == ('nobranch',):
121
raise errors.NotBranchError(path=self.root_transport.base)
123
raise errors.UnexpectedSmartServerResponse(response)
125
def open_branch(self, _unsupported=False):
126
assert _unsupported == False, 'unsupported flag support not implemented yet.'
127
reference_url = self.get_branch_reference()
128
if reference_url is None:
129
# branch at this location.
130
return RemoteBranch(self, self.find_repository())
132
# a branch reference, use the existing BranchReference logic.
133
format = BranchReferenceFormat()
134
return format.open(self, _found=True, location=reference_url)
136
def open_repository(self):
137
path = self._path_for_remote_call(self._client)
138
response = self._client.call('BzrDir.find_repository', path)
139
assert response[0] in ('ok', 'norepository'), \
140
'unexpected response code %s' % (response,)
141
if response[0] == 'norepository':
142
raise errors.NoRepositoryPresent(self)
143
assert len(response) == 4, 'incorrect response length %s' % (response,)
144
if response[1] == '':
145
format = RemoteRepositoryFormat()
146
format.rich_root_data = (response[2] == 'yes')
147
format.supports_tree_reference = (response[3] == 'yes')
148
return RemoteRepository(self, format)
150
raise errors.NoRepositoryPresent(self)
152
def open_workingtree(self, recommend_upgrade=True):
154
if self._real_bzrdir.has_workingtree():
155
raise errors.NotLocalUrl(self.root_transport)
157
raise errors.NoWorkingTree(self.root_transport.base)
159
def _path_for_remote_call(self, client):
160
"""Return the path to be used for this bzrdir in a remote call."""
161
return client.remote_path_from_transport(self.root_transport)
163
def get_branch_transport(self, branch_format):
165
return self._real_bzrdir.get_branch_transport(branch_format)
167
def get_repository_transport(self, repository_format):
169
return self._real_bzrdir.get_repository_transport(repository_format)
171
def get_workingtree_transport(self, workingtree_format):
173
return self._real_bzrdir.get_workingtree_transport(workingtree_format)
175
def can_convert_format(self):
176
"""Upgrading of remote bzrdirs is not supported yet."""
179
def needs_format_conversion(self, format=None):
180
"""Upgrading of remote bzrdirs is not supported yet."""
183
def clone(self, url, revision_id=None, force_new_repo=False):
185
return self._real_bzrdir.clone(url, revision_id=revision_id,
186
force_new_repo=force_new_repo)
189
class RemoteRepositoryFormat(repository.RepositoryFormat):
190
"""Format for repositories accessed over a _SmartClient.
192
Instances of this repository are represented by RemoteRepository
195
The RemoteRepositoryFormat is parameterised during construction
196
to reflect the capabilities of the real, remote format. Specifically
197
the attributes rich_root_data and supports_tree_reference are set
198
on a per instance basis, and are not set (and should not be) at
202
_matchingbzrdir = RemoteBzrDirFormat
204
def initialize(self, a_bzrdir, shared=False):
205
assert isinstance(a_bzrdir, RemoteBzrDir), \
206
'%r is not a RemoteBzrDir' % (a_bzrdir,)
207
return a_bzrdir.create_repository(shared=shared)
209
def open(self, a_bzrdir):
210
assert isinstance(a_bzrdir, RemoteBzrDir)
211
return a_bzrdir.open_repository()
213
def get_format_description(self):
214
return 'bzr remote repository'
216
def __eq__(self, other):
217
return self.__class__ == other.__class__
219
def check_conversion_target(self, target_format):
220
if self.rich_root_data and not target_format.rich_root_data:
221
raise errors.BadConversionTarget(
222
'Does not support rich root data.', target_format)
223
if (self.supports_tree_reference and
224
not getattr(target_format, 'supports_tree_reference', False)):
225
raise errors.BadConversionTarget(
226
'Does not support nested trees', target_format)
229
class RemoteRepository(object):
230
"""Repository accessed over rpc.
232
For the moment most operations are performed using local transport-backed
236
def __init__(self, remote_bzrdir, format, real_repository=None, _client=None):
237
"""Create a RemoteRepository instance.
239
:param remote_bzrdir: The bzrdir hosting this repository.
240
:param format: The RemoteFormat object to use.
241
:param real_repository: If not None, a local implementation of the
242
repository logic for the repository, usually accessing the data
244
:param _client: Private testing parameter - override the smart client
245
to be used by the repository.
248
self._real_repository = real_repository
250
self._real_repository = None
251
self.bzrdir = remote_bzrdir
253
self._client = client._SmartClient(self.bzrdir._shared_medium)
255
self._client = _client
256
self._format = format
257
self._lock_mode = None
258
self._lock_token = None
260
self._leave_lock = False
262
self._reconcile_does_inventory_gc = True
263
self.base = self.bzrdir.transport.base
266
return "%s(%s)" % (self.__class__.__name__, self.base)
270
def abort_write_group(self):
271
"""Complete a write group on the decorated repository.
273
Smart methods peform operations in a single step so this api
274
is not really applicable except as a compatibility thunk
275
for older plugins that don't use e.g. the CommitBuilder
279
return self._real_repository.abort_write_group()
281
def commit_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.commit_write_group()
292
def _ensure_real(self):
293
"""Ensure that there is a _real_repository set.
295
Used before calls to self._real_repository.
297
if not self._real_repository:
298
self.bzrdir._ensure_real()
299
#self._real_repository = self.bzrdir._real_bzrdir.open_repository()
300
self._set_real_repository(self.bzrdir._real_bzrdir.open_repository())
302
def get_revision_graph(self, revision_id=None):
303
"""See Repository.get_revision_graph()."""
304
if revision_id is None:
306
elif revision_id == NULL_REVISION:
309
path = self.bzrdir._path_for_remote_call(self._client)
310
assert type(revision_id) is str
311
response = self._client.call_expecting_body(
312
'Repository.get_revision_graph', path, revision_id)
313
if response[0][0] not in ['ok', 'nosuchrevision']:
314
raise errors.UnexpectedSmartServerResponse(response[0])
315
if response[0][0] == 'ok':
316
coded = response[1].read_body_bytes()
318
# no revisions in this repository!
320
lines = coded.split('\n')
323
d = tuple(line.split())
324
revision_graph[d[0]] = d[1:]
326
return revision_graph
328
response_body = response[1].read_body_bytes()
329
assert response_body == ''
330
raise NoSuchRevision(self, revision_id)
332
def has_revision(self, revision_id):
333
"""See Repository.has_revision()."""
334
if revision_id is None:
335
# The null revision is always present.
337
path = self.bzrdir._path_for_remote_call(self._client)
338
response = self._client.call('Repository.has_revision', path, revision_id)
339
assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
340
return response[0] == 'yes'
342
def has_same_location(self, other):
343
return (self.__class__ == other.__class__ and
344
self.bzrdir.transport.base == other.bzrdir.transport.base)
346
def get_graph(self, other_repository=None):
347
"""Return the graph for this repository format"""
348
return self._real_repository.get_graph(other_repository)
350
def gather_stats(self, revid=None, committers=None):
351
"""See Repository.gather_stats()."""
352
path = self.bzrdir._path_for_remote_call(self._client)
353
if revid in (None, NULL_REVISION):
357
if committers is None or not committers:
358
fmt_committers = 'no'
360
fmt_committers = 'yes'
361
response = self._client.call_expecting_body(
362
'Repository.gather_stats', path, fmt_revid, fmt_committers)
363
assert response[0][0] == 'ok', \
364
'unexpected response code %s' % (response[0],)
366
body = response[1].read_body_bytes()
368
for line in body.split('\n'):
371
key, val_text = line.split(':')
372
if key in ('revisions', 'size', 'committers'):
373
result[key] = int(val_text)
374
elif key in ('firstrev', 'latestrev'):
375
values = val_text.split(' ')[1:]
376
result[key] = (float(values[0]), long(values[1]))
380
def get_physical_lock_status(self):
381
"""See Repository.get_physical_lock_status()."""
384
def is_in_write_group(self):
385
"""Return True if there is an open write group.
387
write groups are only applicable locally for the smart server..
389
if self._real_repository:
390
return self._real_repository.is_in_write_group()
393
return self._lock_count >= 1
396
"""See Repository.is_shared()."""
397
path = self.bzrdir._path_for_remote_call(self._client)
398
response = self._client.call('Repository.is_shared', path)
399
assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
400
return response[0] == 'yes'
403
# wrong eventually - want a local lock cache context
404
if not self._lock_mode:
405
self._lock_mode = 'r'
407
if self._real_repository is not None:
408
self._real_repository.lock_read()
410
self._lock_count += 1
412
def _remote_lock_write(self, token):
413
path = self.bzrdir._path_for_remote_call(self._client)
416
response = self._client.call('Repository.lock_write', path, token)
417
if response[0] == 'ok':
420
elif response[0] == 'LockContention':
421
raise errors.LockContention('(remote lock)')
422
elif response[0] == 'UnlockableTransport':
423
raise errors.UnlockableTransport(self.bzrdir.root_transport)
425
raise errors.UnexpectedSmartServerResponse(response)
427
def lock_write(self, token=None):
428
if not self._lock_mode:
429
self._lock_token = self._remote_lock_write(token)
430
assert self._lock_token, 'Remote server did not return a token!'
431
if self._real_repository is not None:
432
self._real_repository.lock_write(token=self._lock_token)
433
if token is not None:
434
self._leave_lock = True
436
self._leave_lock = False
437
self._lock_mode = 'w'
439
elif self._lock_mode == 'r':
440
raise errors.ReadOnlyError(self)
442
self._lock_count += 1
443
return self._lock_token
445
def leave_lock_in_place(self):
446
self._leave_lock = True
448
def dont_leave_lock_in_place(self):
449
self._leave_lock = False
451
def _set_real_repository(self, repository):
452
"""Set the _real_repository for this repository.
454
:param repository: The repository to fallback to for non-hpss
455
implemented operations.
457
assert not isinstance(repository, RemoteRepository)
458
self._real_repository = repository
459
if self._lock_mode == 'w':
460
# if we are already locked, the real repository must be able to
461
# acquire the lock with our token.
462
self._real_repository.lock_write(self._lock_token)
463
elif self._lock_mode == 'r':
464
self._real_repository.lock_read()
466
def start_write_group(self):
467
"""Start a write group on the decorated repository.
469
Smart methods peform operations in a single step so this api
470
is not really applicable except as a compatibility thunk
471
for older plugins that don't use e.g. the CommitBuilder
475
return self._real_repository.start_write_group()
477
def _unlock(self, token):
478
path = self.bzrdir._path_for_remote_call(self._client)
479
response = self._client.call('Repository.unlock', path, token)
480
if response == ('ok',):
482
elif response[0] == 'TokenMismatch':
483
raise errors.TokenMismatch(token, '(remote token)')
485
raise errors.UnexpectedSmartServerResponse(response)
488
if self._lock_count == 1 and self._lock_mode == 'w':
489
# don't unlock if inside a write group.
490
if self.is_in_write_group():
491
raise errors.BzrError(
492
'Must end write groups before releasing write locks.')
493
self._lock_count -= 1
494
if not self._lock_count:
495
mode = self._lock_mode
496
self._lock_mode = None
497
if self._real_repository is not None:
498
self._real_repository.unlock()
500
# Only write-locked repositories need to make a remote method
501
# call to perfom the unlock.
503
assert self._lock_token, 'Locked, but no token!'
504
token = self._lock_token
505
self._lock_token = None
506
if not self._leave_lock:
509
def break_lock(self):
510
# should hand off to the network
512
return self._real_repository.break_lock()
514
def _get_tarball(self, compression):
515
"""Return a TemporaryFile containing a repository tarball.
517
Returns None if the server does not support sending tarballs.
520
path = self.bzrdir._path_for_remote_call(self._client)
521
response, protocol = self._client.call_expecting_body(
522
'Repository.tarball', path, compression)
523
if response[0] == 'ok':
524
# Extract the tarball and return it
525
t = tempfile.NamedTemporaryFile()
526
# TODO: rpc layer should read directly into it...
527
t.write(protocol.read_body_bytes())
530
if (response == ('error', "Generic bzr smart protocol error: "
531
"bad request 'Repository.tarball'") or
532
response == ('error', "Generic bzr smart protocol error: "
533
"bad request u'Repository.tarball'")):
534
protocol.cancel_read_body()
536
raise errors.UnexpectedSmartServerResponse(response)
538
def sprout(self, to_bzrdir, revision_id=None):
539
# TODO: Option to control what format is created?
540
dest_repo = to_bzrdir.create_repository()
541
dest_repo.fetch(self, revision_id=revision_id)
544
### These methods are just thin shims to the VFS object for now.
546
def revision_tree(self, revision_id):
548
return self._real_repository.revision_tree(revision_id)
550
def get_serializer_format(self):
552
return self._real_repository.get_serializer_format()
554
def get_commit_builder(self, branch, parents, config, timestamp=None,
555
timezone=None, committer=None, revprops=None,
557
# FIXME: It ought to be possible to call this without immediately
558
# triggering _ensure_real. For now it's the easiest thing to do.
560
builder = self._real_repository.get_commit_builder(branch, parents,
561
config, timestamp=timestamp, timezone=timezone,
562
committer=committer, revprops=revprops, revision_id=revision_id)
563
# Make the builder use this RemoteRepository rather than the real one.
564
builder.repository = self
568
def add_inventory(self, revid, inv, parents):
570
return self._real_repository.add_inventory(revid, inv, parents)
573
def add_revision(self, rev_id, rev, inv=None, config=None):
575
return self._real_repository.add_revision(
576
rev_id, rev, inv=inv, config=config)
579
def get_inventory(self, revision_id):
581
return self._real_repository.get_inventory(revision_id)
584
def get_revision(self, revision_id):
586
return self._real_repository.get_revision(revision_id)
589
def weave_store(self):
591
return self._real_repository.weave_store
593
def get_transaction(self):
595
return self._real_repository.get_transaction()
598
def clone(self, a_bzrdir, revision_id=None):
600
return self._real_repository.clone(a_bzrdir, revision_id=revision_id)
602
def make_working_trees(self):
603
"""RemoteRepositories never create working trees by default."""
606
def fetch(self, source, revision_id=None, pb=None):
607
if self.has_same_location(source):
608
# check that last_revision is in 'from' and then return a
610
if (revision_id is not None and
611
not _mod_revision.is_null(revision_id)):
612
self.get_revision(revision_id)
615
return self._real_repository.fetch(
616
source, revision_id=revision_id, pb=pb)
618
def create_bundle(self, target, base, fileobj, format=None):
620
self._real_repository.create_bundle(target, base, fileobj, format)
623
def control_weaves(self):
625
return self._real_repository.control_weaves
628
def get_ancestry(self, revision_id, topo_sorted=True):
630
return self._real_repository.get_ancestry(revision_id, topo_sorted)
633
def get_inventory_weave(self):
635
return self._real_repository.get_inventory_weave()
637
def fileids_altered_by_revision_ids(self, revision_ids):
639
return self._real_repository.fileids_altered_by_revision_ids(revision_ids)
641
def iter_files_bytes(self, desired_files):
642
"""See Repository.iter_file_bytes.
645
return self._real_repository.iter_files_bytes(desired_files)
648
def get_signature_text(self, revision_id):
650
return self._real_repository.get_signature_text(revision_id)
653
def get_revision_graph_with_ghosts(self, revision_ids=None):
655
return self._real_repository.get_revision_graph_with_ghosts(
656
revision_ids=revision_ids)
659
def get_inventory_xml(self, revision_id):
661
return self._real_repository.get_inventory_xml(revision_id)
663
def deserialise_inventory(self, revision_id, xml):
665
return self._real_repository.deserialise_inventory(revision_id, xml)
667
def reconcile(self, other=None, thorough=False):
669
return self._real_repository.reconcile(other=other, thorough=thorough)
671
def all_revision_ids(self):
673
return self._real_repository.all_revision_ids()
676
def get_deltas_for_revisions(self, revisions):
678
return self._real_repository.get_deltas_for_revisions(revisions)
681
def get_revision_delta(self, revision_id):
683
return self._real_repository.get_revision_delta(revision_id)
686
def revision_trees(self, revision_ids):
688
return self._real_repository.revision_trees(revision_ids)
691
def get_revision_reconcile(self, revision_id):
693
return self._real_repository.get_revision_reconcile(revision_id)
696
def check(self, revision_ids):
698
return self._real_repository.check(revision_ids)
700
def copy_content_into(self, destination, revision_id=None):
702
return self._real_repository.copy_content_into(
703
destination, revision_id=revision_id)
705
def _copy_repository_tarball(self, to_bzrdir, revision_id=None):
706
# get a tarball of the remote repository, and copy from that into the
708
from bzrlib import osutils
711
from StringIO import StringIO
712
# TODO: Maybe a progress bar while streaming the tarball?
713
note("Copying repository content as tarball...")
714
tar_file = self._get_tarball('bz2')
717
destination = to_bzrdir.create_repository()
719
tar = tarfile.open('repository', fileobj=tar_file,
721
tmpdir = tempfile.mkdtemp()
723
_extract_tar(tar, tmpdir)
724
tmp_bzrdir = BzrDir.open(tmpdir)
725
tmp_repo = tmp_bzrdir.open_repository()
726
tmp_repo.copy_content_into(destination, revision_id)
728
osutils.rmtree(tmpdir)
732
# TODO: Suggestion from john: using external tar is much faster than
733
# python's tarfile library, but it may not work on windows.
737
"""Compress the data within the repository.
739
This is not currently implemented within the smart server.
742
return self._real_repository.pack()
744
def set_make_working_trees(self, new_value):
745
raise NotImplementedError(self.set_make_working_trees)
748
def sign_revision(self, revision_id, gpg_strategy):
750
return self._real_repository.sign_revision(revision_id, gpg_strategy)
753
def get_revisions(self, revision_ids):
755
return self._real_repository.get_revisions(revision_ids)
757
def supports_rich_root(self):
759
return self._real_repository.supports_rich_root()
761
def iter_reverse_revision_history(self, revision_id):
763
return self._real_repository.iter_reverse_revision_history(revision_id)
766
def _serializer(self):
768
return self._real_repository._serializer
770
def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
772
return self._real_repository.store_revision_signature(
773
gpg_strategy, plaintext, revision_id)
775
def has_signature_for_revision_id(self, revision_id):
777
return self._real_repository.has_signature_for_revision_id(revision_id)
779
def get_data_stream(self, revision_ids):
780
path = self.bzrdir._path_for_remote_call(self._client)
781
response, protocol = self._client.call_expecting_body(
782
'Repository.stream_knit_data_for_revisions', path, *revision_ids)
783
if response == ('ok',):
784
return self._deserialise_stream(protocol)
785
elif (response == ('error', "Generic bzr smart protocol error: "
786
"bad request 'Repository.stream_knit_data_for_revisions'") or
787
response == ('error', "Generic bzr smart protocol error: "
788
"bad request u'Repository.stream_knit_data_for_revisions'")):
789
protocol.cancel_read_body()
791
return self._real_repository.get_data_stream(revision_ids)
793
raise errors.UnexpectedSmartServerResponse(response)
795
def _deserialise_stream(self, protocol):
796
buffer = StringIO(protocol.read_body_bytes())
797
reader = ContainerReader(buffer)
798
for record_names, read_bytes in reader.iter_records():
800
# These records should have only one name, and that name
801
# should be a one-element tuple.
802
[name_tuple] = record_names
804
raise errors.SmartProtocolError(
805
'Repository data stream had invalid record name %r'
807
yield name_tuple, read_bytes(None)
809
def insert_data_stream(self, stream):
811
self._real_repository.insert_data_stream(stream)
813
def item_keys_introduced_by(self, revision_ids, _files_pb=None):
815
return self._real_repository.item_keys_introduced_by(revision_ids,
819
class RemoteBranchLockableFiles(LockableFiles):
820
"""A 'LockableFiles' implementation that talks to a smart server.
822
This is not a public interface class.
825
def __init__(self, bzrdir, _client):
827
self._client = _client
828
self._need_find_modes = True
829
LockableFiles.__init__(
830
self, bzrdir.get_branch_transport(None),
831
'lock', lockdir.LockDir)
833
def _find_modes(self):
834
# RemoteBranches don't let the client set the mode of control files.
835
self._dir_mode = None
836
self._file_mode = None
839
"""'get' a remote path as per the LockableFiles interface.
841
:param path: the file to 'get'. If this is 'branch.conf', we do not
842
just retrieve a file, instead we ask the smart server to generate
843
a configuration for us - which is retrieved as an INI file.
845
if path == 'branch.conf':
846
path = self.bzrdir._path_for_remote_call(self._client)
847
response = self._client.call_expecting_body(
848
'Branch.get_config_file', path)
849
assert response[0][0] == 'ok', \
850
'unexpected response code %s' % (response[0],)
851
return StringIO(response[1].read_body_bytes())
854
return LockableFiles.get(self, path)
857
class RemoteBranchFormat(branch.BranchFormat):
859
def __eq__(self, other):
860
return (isinstance(other, RemoteBranchFormat) and
861
self.__dict__ == other.__dict__)
863
def get_format_description(self):
864
return 'Remote BZR Branch'
866
def get_format_string(self):
867
return 'Remote BZR Branch'
869
def open(self, a_bzrdir):
870
assert isinstance(a_bzrdir, RemoteBzrDir)
871
return a_bzrdir.open_branch()
873
def initialize(self, a_bzrdir):
874
assert isinstance(a_bzrdir, RemoteBzrDir)
875
return a_bzrdir.create_branch()
877
def supports_tags(self):
878
# Remote branches might support tags, but we won't know until we
879
# access the real remote branch.
883
class RemoteBranch(branch.Branch):
884
"""Branch stored on a server accessed by HPSS RPC.
886
At the moment most operations are mapped down to simple file operations.
889
def __init__(self, remote_bzrdir, remote_repository, real_branch=None,
891
"""Create a RemoteBranch instance.
893
:param real_branch: An optional local implementation of the branch
894
format, usually accessing the data via the VFS.
895
:param _client: Private parameter for testing.
897
# We intentionally don't call the parent class's __init__, because it
898
# will try to assign to self.tags, which is a property in this subclass.
899
# And the parent's __init__ doesn't do much anyway.
900
self._revision_history_cache = None
901
self.bzrdir = remote_bzrdir
902
if _client is not None:
903
self._client = _client
905
self._client = client._SmartClient(self.bzrdir._shared_medium)
906
self.repository = remote_repository
907
if real_branch is not None:
908
self._real_branch = real_branch
909
# Give the remote repository the matching real repo.
910
real_repo = self._real_branch.repository
911
if isinstance(real_repo, RemoteRepository):
912
real_repo._ensure_real()
913
real_repo = real_repo._real_repository
914
self.repository._set_real_repository(real_repo)
915
# Give the branch the remote repository to let fast-pathing happen.
916
self._real_branch.repository = self.repository
918
self._real_branch = None
919
# Fill out expected attributes of branch for bzrlib api users.
920
self._format = RemoteBranchFormat()
921
self.base = self.bzrdir.root_transport.base
922
self._control_files = None
923
self._lock_mode = None
924
self._lock_token = None
926
self._leave_lock = False
929
return "%s(%s)" % (self.__class__.__name__, self.base)
933
def _ensure_real(self):
934
"""Ensure that there is a _real_branch set.
936
Used before calls to self._real_branch.
938
if not self._real_branch:
939
assert vfs.vfs_enabled()
940
self.bzrdir._ensure_real()
941
self._real_branch = self.bzrdir._real_bzrdir.open_branch()
942
# Give the remote repository the matching real repo.
943
real_repo = self._real_branch.repository
944
if isinstance(real_repo, RemoteRepository):
945
real_repo._ensure_real()
946
real_repo = real_repo._real_repository
947
self.repository._set_real_repository(real_repo)
948
# Give the branch the remote repository to let fast-pathing happen.
949
self._real_branch.repository = self.repository
950
# XXX: deal with _lock_mode == 'w'
951
if self._lock_mode == 'r':
952
self._real_branch.lock_read()
955
def control_files(self):
956
# Defer actually creating RemoteBranchLockableFiles until its needed,
957
# because it triggers an _ensure_real that we otherwise might not need.
958
if self._control_files is None:
959
self._control_files = RemoteBranchLockableFiles(
960
self.bzrdir, self._client)
961
return self._control_files
963
def _get_checkout_format(self):
965
return self._real_branch._get_checkout_format()
967
def get_physical_lock_status(self):
968
"""See Branch.get_physical_lock_status()."""
969
# should be an API call to the server, as branches must be lockable.
971
return self._real_branch.get_physical_lock_status()
974
if not self._lock_mode:
975
self._lock_mode = 'r'
977
if self._real_branch is not None:
978
self._real_branch.lock_read()
980
self._lock_count += 1
982
def _remote_lock_write(self, token):
984
branch_token = repo_token = ''
987
repo_token = self.repository.lock_write()
988
self.repository.unlock()
989
path = self.bzrdir._path_for_remote_call(self._client)
990
response = self._client.call('Branch.lock_write', path, branch_token,
992
if response[0] == 'ok':
993
ok, branch_token, repo_token = response
994
return branch_token, repo_token
995
elif response[0] == 'LockContention':
996
raise errors.LockContention('(remote lock)')
997
elif response[0] == 'TokenMismatch':
998
raise errors.TokenMismatch(token, '(remote token)')
999
elif response[0] == 'UnlockableTransport':
1000
raise errors.UnlockableTransport(self.bzrdir.root_transport)
1001
elif response[0] == 'ReadOnlyError':
1002
raise errors.ReadOnlyError(self)
1004
raise errors.UnexpectedSmartServerResponse(response)
1006
def lock_write(self, token=None):
1007
if not self._lock_mode:
1008
remote_tokens = self._remote_lock_write(token)
1009
self._lock_token, self._repo_lock_token = remote_tokens
1010
assert self._lock_token, 'Remote server did not return a token!'
1011
# TODO: We really, really, really don't want to call _ensure_real
1012
# here, but it's the easiest way to ensure coherency between the
1013
# state of the RemoteBranch and RemoteRepository objects and the
1014
# physical locks. If we don't materialise the real objects here,
1015
# then getting everything in the right state later is complex, so
1016
# for now we just do it the lazy way.
1017
# -- Andrew Bennetts, 2007-02-22.
1019
if self._real_branch is not None:
1020
self._real_branch.repository.lock_write(
1021
token=self._repo_lock_token)
1023
self._real_branch.lock_write(token=self._lock_token)
1025
self._real_branch.repository.unlock()
1026
if token is not None:
1027
self._leave_lock = True
1029
# XXX: this case seems to be unreachable; token cannot be None.
1030
self._leave_lock = False
1031
self._lock_mode = 'w'
1032
self._lock_count = 1
1033
elif self._lock_mode == 'r':
1034
raise errors.ReadOnlyTransaction
1036
if token is not None:
1037
# A token was given to lock_write, and we're relocking, so check
1038
# that the given token actually matches the one we already have.
1039
if token != self._lock_token:
1040
raise errors.TokenMismatch(token, self._lock_token)
1041
self._lock_count += 1
1042
return self._lock_token
1044
def _unlock(self, branch_token, repo_token):
1045
path = self.bzrdir._path_for_remote_call(self._client)
1046
response = self._client.call('Branch.unlock', path, branch_token,
1048
if response == ('ok',):
1050
elif response[0] == 'TokenMismatch':
1051
raise errors.TokenMismatch(
1052
str((branch_token, repo_token)), '(remote tokens)')
1054
raise errors.UnexpectedSmartServerResponse(response)
1057
self._lock_count -= 1
1058
if not self._lock_count:
1059
self._clear_cached_state()
1060
mode = self._lock_mode
1061
self._lock_mode = None
1062
if self._real_branch is not None:
1063
if not self._leave_lock:
1064
# If this RemoteBranch will remove the physical lock for the
1065
# repository, make sure the _real_branch doesn't do it
1066
# first. (Because the _real_branch's repository is set to
1067
# be the RemoteRepository.)
1068
self._real_branch.repository.leave_lock_in_place()
1069
self._real_branch.unlock()
1071
# Only write-locked branched need to make a remote method call
1072
# to perfom the unlock.
1074
assert self._lock_token, 'Locked, but no token!'
1075
branch_token = self._lock_token
1076
repo_token = self._repo_lock_token
1077
self._lock_token = None
1078
self._repo_lock_token = None
1079
if not self._leave_lock:
1080
self._unlock(branch_token, repo_token)
1082
def break_lock(self):
1084
return self._real_branch.break_lock()
1086
def leave_lock_in_place(self):
1087
self._leave_lock = True
1089
def dont_leave_lock_in_place(self):
1090
self._leave_lock = False
1092
def last_revision_info(self):
1093
"""See Branch.last_revision_info()."""
1094
path = self.bzrdir._path_for_remote_call(self._client)
1095
response = self._client.call('Branch.last_revision_info', path)
1096
assert response[0] == 'ok', 'unexpected response code %s' % (response,)
1097
revno = int(response[1])
1098
last_revision = response[2]
1099
return (revno, last_revision)
1101
def _gen_revision_history(self):
1102
"""See Branch._gen_revision_history()."""
1103
path = self.bzrdir._path_for_remote_call(self._client)
1104
response = self._client.call_expecting_body(
1105
'Branch.revision_history', path)
1106
assert response[0][0] == 'ok', ('unexpected response code %s'
1108
result = response[1].read_body_bytes().split('\x00')
1114
def set_revision_history(self, rev_history):
1115
# Send just the tip revision of the history; the server will generate
1116
# the full history from that. If the revision doesn't exist in this
1117
# branch, NoSuchRevision will be raised.
1118
path = self.bzrdir._path_for_remote_call(self._client)
1119
if rev_history == []:
1122
rev_id = rev_history[-1]
1123
self._clear_cached_state()
1124
response = self._client.call('Branch.set_last_revision',
1125
path, self._lock_token, self._repo_lock_token, rev_id)
1126
if response[0] == 'NoSuchRevision':
1127
raise NoSuchRevision(self, rev_id)
1129
assert response == ('ok',), (
1130
'unexpected response code %r' % (response,))
1131
self._cache_revision_history(rev_history)
1133
def get_parent(self):
1135
return self._real_branch.get_parent()
1137
def set_parent(self, url):
1139
return self._real_branch.set_parent(url)
1141
def get_config(self):
1142
return RemoteBranchConfig(self)
1144
def sprout(self, to_bzrdir, revision_id=None):
1145
# Like Branch.sprout, except that it sprouts a branch in the default
1146
# format, because RemoteBranches can't be created at arbitrary URLs.
1147
# XXX: if to_bzrdir is a RemoteBranch, this should perhaps do
1148
# to_bzrdir.create_branch...
1149
result = branch.BranchFormat.get_default_format().initialize(to_bzrdir)
1150
self.copy_content_into(result, revision_id=revision_id)
1151
result.set_parent(self.bzrdir.root_transport.base)
1155
def pull(self, source, overwrite=False, stop_revision=None,
1157
# FIXME: This asks the real branch to run the hooks, which means
1158
# they're called with the wrong target branch parameter.
1159
# The test suite specifically allows this at present but it should be
1160
# fixed. It should get a _override_hook_target branch,
1161
# as push does. -- mbp 20070405
1163
self._real_branch.pull(
1164
source, overwrite=overwrite, stop_revision=stop_revision,
1168
def push(self, target, overwrite=False, stop_revision=None):
1170
return self._real_branch.push(
1171
target, overwrite=overwrite, stop_revision=stop_revision,
1172
_override_hook_source_branch=self)
1174
def is_locked(self):
1175
return self._lock_count >= 1
1177
def set_last_revision_info(self, revno, revision_id):
1179
self._clear_cached_state()
1180
return self._real_branch.set_last_revision_info(revno, revision_id)
1182
def generate_revision_history(self, revision_id, last_rev=None,
1185
return self._real_branch.generate_revision_history(
1186
revision_id, last_rev=last_rev, other_branch=other_branch)
1191
return self._real_branch.tags
1193
def set_push_location(self, location):
1195
return self._real_branch.set_push_location(location)
1197
def update_revisions(self, other, stop_revision=None):
1199
return self._real_branch.update_revisions(
1200
other, stop_revision=stop_revision)
1203
class RemoteBranchConfig(BranchConfig):
1206
self.branch._ensure_real()
1207
return self.branch._real_branch.get_config().username()
1209
def _get_branch_data_config(self):
1210
self.branch._ensure_real()
1211
if self._branch_data_config is None:
1212
self._branch_data_config = TreeConfig(self.branch._real_branch)
1213
return self._branch_data_config
1216
def _extract_tar(tar, to_dir):
1217
"""Extract all the contents of a tarfile object.
1219
A replacement for extractall, which is not present in python2.4
1222
tar.extract(tarinfo, to_dir)