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 create_workingtree(self, revision_id=None):
94
raise errors.NotLocalUrl(self.transport.base)
96
def find_branch_format(self):
97
"""Find the branch 'format' for this bzrdir.
99
This might be a synthetic object for e.g. RemoteBranch and SVN.
101
b = self.open_branch()
104
def get_branch_reference(self):
105
"""See BzrDir.get_branch_reference()."""
106
path = self._path_for_remote_call(self._client)
107
response = self._client.call('BzrDir.open_branch', path)
108
if response[0] == 'ok':
109
if response[1] == '':
110
# branch at this location.
113
# a branch reference, use the existing BranchReference logic.
115
elif response == ('nobranch',):
116
raise errors.NotBranchError(path=self.root_transport.base)
118
raise errors.UnexpectedSmartServerResponse(response)
120
def open_branch(self, _unsupported=False):
121
assert _unsupported == False, 'unsupported flag support not implemented yet.'
122
reference_url = self.get_branch_reference()
123
if reference_url is None:
124
# branch at this location.
125
return RemoteBranch(self, self.find_repository())
127
# a branch reference, use the existing BranchReference logic.
128
format = BranchReferenceFormat()
129
return format.open(self, _found=True, location=reference_url)
131
def open_repository(self):
132
path = self._path_for_remote_call(self._client)
133
response = self._client.call('BzrDir.find_repository', path)
134
assert response[0] in ('ok', 'norepository'), \
135
'unexpected response code %s' % (response,)
136
if response[0] == 'norepository':
137
raise errors.NoRepositoryPresent(self)
138
assert len(response) == 4, 'incorrect response length %s' % (response,)
139
if response[1] == '':
140
format = RemoteRepositoryFormat()
141
format.rich_root_data = (response[2] == 'yes')
142
format.supports_tree_reference = (response[3] == 'yes')
143
return RemoteRepository(self, format)
145
raise errors.NoRepositoryPresent(self)
147
def open_workingtree(self, recommend_upgrade=True):
149
if self._real_bzrdir.has_workingtree():
150
raise errors.NotLocalUrl(self.root_transport)
152
raise errors.NoWorkingTree(self.root_transport.base)
154
def _path_for_remote_call(self, client):
155
"""Return the path to be used for this bzrdir in a remote call."""
156
return client.remote_path_from_transport(self.root_transport)
158
def get_branch_transport(self, branch_format):
160
return self._real_bzrdir.get_branch_transport(branch_format)
162
def get_repository_transport(self, repository_format):
164
return self._real_bzrdir.get_repository_transport(repository_format)
166
def get_workingtree_transport(self, workingtree_format):
168
return self._real_bzrdir.get_workingtree_transport(workingtree_format)
170
def can_convert_format(self):
171
"""Upgrading of remote bzrdirs is not supported yet."""
174
def needs_format_conversion(self, format=None):
175
"""Upgrading of remote bzrdirs is not supported yet."""
178
def clone(self, url, revision_id=None, force_new_repo=False):
180
return self._real_bzrdir.clone(url, revision_id=revision_id,
181
force_new_repo=force_new_repo)
184
class RemoteRepositoryFormat(repository.RepositoryFormat):
185
"""Format for repositories accessed over a _SmartClient.
187
Instances of this repository are represented by RemoteRepository
190
The RemoteRepositoryFormat is parameterised during construction
191
to reflect the capabilities of the real, remote format. Specifically
192
the attributes rich_root_data and supports_tree_reference are set
193
on a per instance basis, and are not set (and should not be) at
197
_matchingbzrdir = RemoteBzrDirFormat
199
def initialize(self, a_bzrdir, shared=False):
200
assert isinstance(a_bzrdir, RemoteBzrDir), \
201
'%r is not a RemoteBzrDir' % (a_bzrdir,)
202
return a_bzrdir.create_repository(shared=shared)
204
def open(self, a_bzrdir):
205
assert isinstance(a_bzrdir, RemoteBzrDir)
206
return a_bzrdir.open_repository()
208
def get_format_description(self):
209
return 'bzr remote repository'
211
def __eq__(self, other):
212
return self.__class__ == other.__class__
214
def check_conversion_target(self, target_format):
215
if self.rich_root_data and not target_format.rich_root_data:
216
raise errors.BadConversionTarget(
217
'Does not support rich root data.', target_format)
218
if (self.supports_tree_reference and
219
not getattr(target_format, 'supports_tree_reference', False)):
220
raise errors.BadConversionTarget(
221
'Does not support nested trees', target_format)
224
class RemoteRepository(object):
225
"""Repository accessed over rpc.
227
For the moment most operations are performed using local transport-backed
231
def __init__(self, remote_bzrdir, format, real_repository=None, _client=None):
232
"""Create a RemoteRepository instance.
234
:param remote_bzrdir: The bzrdir hosting this repository.
235
:param format: The RemoteFormat object to use.
236
:param real_repository: If not None, a local implementation of the
237
repository logic for the repository, usually accessing the data
239
:param _client: Private testing parameter - override the smart client
240
to be used by the repository.
243
self._real_repository = real_repository
245
self._real_repository = None
246
self.bzrdir = remote_bzrdir
248
self._client = client._SmartClient(self.bzrdir._shared_medium)
250
self._client = _client
251
self._format = format
252
self._lock_mode = None
253
self._lock_token = None
255
self._leave_lock = False
257
self._reconcile_does_inventory_gc = True
259
def abort_write_group(self):
260
"""Complete a write group on the decorated repository.
262
Smart methods peform operations in a single step so this api
263
is not really applicable except as a compatibility thunk
264
for older plugins that don't use e.g. the CommitBuilder
268
return self._real_repository.abort_write_group()
270
def commit_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.commit_write_group()
281
def _ensure_real(self):
282
"""Ensure that there is a _real_repository set.
284
Used before calls to self._real_repository.
286
if not self._real_repository:
287
self.bzrdir._ensure_real()
288
#self._real_repository = self.bzrdir._real_bzrdir.open_repository()
289
self._set_real_repository(self.bzrdir._real_bzrdir.open_repository())
291
def get_revision_graph(self, revision_id=None):
292
"""See Repository.get_revision_graph()."""
293
if revision_id is None:
295
elif revision_id == NULL_REVISION:
298
path = self.bzrdir._path_for_remote_call(self._client)
299
assert type(revision_id) is str
300
response = self._client.call_expecting_body(
301
'Repository.get_revision_graph', path, revision_id)
302
if response[0][0] not in ['ok', 'nosuchrevision']:
303
raise errors.UnexpectedSmartServerResponse(response[0])
304
if response[0][0] == 'ok':
305
coded = response[1].read_body_bytes()
307
# no revisions in this repository!
309
lines = coded.split('\n')
312
d = tuple(line.split())
313
revision_graph[d[0]] = d[1:]
315
return revision_graph
317
response_body = response[1].read_body_bytes()
318
assert response_body == ''
319
raise NoSuchRevision(self, revision_id)
321
def has_revision(self, revision_id):
322
"""See Repository.has_revision()."""
323
if revision_id is None:
324
# The null revision is always present.
326
path = self.bzrdir._path_for_remote_call(self._client)
327
response = self._client.call('Repository.has_revision', path, revision_id)
328
assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
329
return response[0] == 'yes'
331
def has_same_location(self, other):
332
return (self.__class__ == other.__class__ and
333
self.bzrdir.transport.base == other.bzrdir.transport.base)
335
def get_graph(self, other_repository=None):
336
"""Return the graph for this repository format"""
337
return self._real_repository.get_graph(other_repository)
339
def gather_stats(self, revid=None, committers=None):
340
"""See Repository.gather_stats()."""
341
path = self.bzrdir._path_for_remote_call(self._client)
342
if revid in (None, NULL_REVISION):
346
if committers is None or not committers:
347
fmt_committers = 'no'
349
fmt_committers = 'yes'
350
response = self._client.call_expecting_body(
351
'Repository.gather_stats', path, fmt_revid, fmt_committers)
352
assert response[0][0] == 'ok', \
353
'unexpected response code %s' % (response[0],)
355
body = response[1].read_body_bytes()
357
for line in body.split('\n'):
360
key, val_text = line.split(':')
361
if key in ('revisions', 'size', 'committers'):
362
result[key] = int(val_text)
363
elif key in ('firstrev', 'latestrev'):
364
values = val_text.split(' ')[1:]
365
result[key] = (float(values[0]), long(values[1]))
369
def get_physical_lock_status(self):
370
"""See Repository.get_physical_lock_status()."""
373
def is_in_write_group(self):
374
"""Return True if there is an open write group.
376
write groups are only applicable locally for the smart server..
378
if self._real_repository:
379
return self._real_repository.is_in_write_group()
382
return self._lock_count >= 1
385
"""See Repository.is_shared()."""
386
path = self.bzrdir._path_for_remote_call(self._client)
387
response = self._client.call('Repository.is_shared', path)
388
assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
389
return response[0] == 'yes'
392
# wrong eventually - want a local lock cache context
393
if not self._lock_mode:
394
self._lock_mode = 'r'
396
if self._real_repository is not None:
397
self._real_repository.lock_read()
399
self._lock_count += 1
401
def _remote_lock_write(self, token):
402
path = self.bzrdir._path_for_remote_call(self._client)
405
response = self._client.call('Repository.lock_write', path, token)
406
if response[0] == 'ok':
409
elif response[0] == 'LockContention':
410
raise errors.LockContention('(remote lock)')
411
elif response[0] == 'UnlockableTransport':
412
raise errors.UnlockableTransport(self.bzrdir.root_transport)
414
raise errors.UnexpectedSmartServerResponse(response)
416
def lock_write(self, token=None):
417
if not self._lock_mode:
418
self._lock_token = self._remote_lock_write(token)
419
assert self._lock_token, 'Remote server did not return a token!'
420
if self._real_repository is not None:
421
self._real_repository.lock_write(token=self._lock_token)
422
if token is not None:
423
self._leave_lock = True
425
self._leave_lock = False
426
self._lock_mode = 'w'
428
elif self._lock_mode == 'r':
429
raise errors.ReadOnlyError(self)
431
self._lock_count += 1
432
return self._lock_token
434
def leave_lock_in_place(self):
435
self._leave_lock = True
437
def dont_leave_lock_in_place(self):
438
self._leave_lock = False
440
def _set_real_repository(self, repository):
441
"""Set the _real_repository for this repository.
443
:param repository: The repository to fallback to for non-hpss
444
implemented operations.
446
assert not isinstance(repository, RemoteRepository)
447
self._real_repository = repository
448
if self._lock_mode == 'w':
449
# if we are already locked, the real repository must be able to
450
# acquire the lock with our token.
451
self._real_repository.lock_write(self._lock_token)
452
elif self._lock_mode == 'r':
453
self._real_repository.lock_read()
455
def start_write_group(self):
456
"""Start a write group on the decorated repository.
458
Smart methods peform operations in a single step so this api
459
is not really applicable except as a compatibility thunk
460
for older plugins that don't use e.g. the CommitBuilder
464
return self._real_repository.start_write_group()
466
def _unlock(self, token):
467
path = self.bzrdir._path_for_remote_call(self._client)
468
response = self._client.call('Repository.unlock', path, token)
469
if response == ('ok',):
471
elif response[0] == 'TokenMismatch':
472
raise errors.TokenMismatch(token, '(remote token)')
474
raise errors.UnexpectedSmartServerResponse(response)
477
if self._lock_count == 1 and self._lock_mode == 'w':
478
# don't unlock if inside a write group.
479
if self.is_in_write_group():
480
raise errors.BzrError(
481
'Must end write groups before releasing write locks.')
482
self._lock_count -= 1
483
if not self._lock_count:
484
mode = self._lock_mode
485
self._lock_mode = None
486
if self._real_repository is not None:
487
self._real_repository.unlock()
489
# Only write-locked repositories need to make a remote method
490
# call to perfom the unlock.
492
assert self._lock_token, 'Locked, but no token!'
493
token = self._lock_token
494
self._lock_token = None
495
if not self._leave_lock:
498
def break_lock(self):
499
# should hand off to the network
501
return self._real_repository.break_lock()
503
def _get_tarball(self, compression):
504
"""Return a TemporaryFile containing a repository tarball"""
506
path = self.bzrdir._path_for_remote_call(self._client)
507
response, protocol = self._client.call_expecting_body(
508
'Repository.tarball', path, compression)
509
assert response[0] in ('ok', 'failure'), \
510
'unexpected response code %s' % (response,)
511
if response[0] == 'ok':
512
# Extract the tarball and return it
513
t = tempfile.NamedTemporaryFile()
514
# TODO: rpc layer should read directly into it...
515
t.write(protocol.read_body_bytes())
519
raise errors.SmartServerError(error_code=response)
521
def sprout(self, to_bzrdir, revision_id=None):
522
# TODO: Option to control what format is created?
523
dest_repo = to_bzrdir.create_repository()
524
dest_repo.fetch(self, revision_id=revision_id)
527
### These methods are just thin shims to the VFS object for now.
529
def revision_tree(self, revision_id):
531
return self._real_repository.revision_tree(revision_id)
533
def get_serializer_format(self):
535
return self._real_repository.get_serializer_format()
537
def get_commit_builder(self, branch, parents, config, timestamp=None,
538
timezone=None, committer=None, revprops=None,
540
# FIXME: It ought to be possible to call this without immediately
541
# triggering _ensure_real. For now it's the easiest thing to do.
543
builder = self._real_repository.get_commit_builder(branch, parents,
544
config, timestamp=timestamp, timezone=timezone,
545
committer=committer, revprops=revprops, revision_id=revision_id)
546
# Make the builder use this RemoteRepository rather than the real one.
547
builder.repository = self
551
def add_inventory(self, revid, inv, parents):
553
return self._real_repository.add_inventory(revid, inv, parents)
556
def add_revision(self, rev_id, rev, inv=None, config=None):
558
return self._real_repository.add_revision(
559
rev_id, rev, inv=inv, config=config)
562
def get_inventory(self, revision_id):
564
return self._real_repository.get_inventory(revision_id)
567
def get_revision(self, revision_id):
569
return self._real_repository.get_revision(revision_id)
572
def weave_store(self):
574
return self._real_repository.weave_store
576
def get_transaction(self):
578
return self._real_repository.get_transaction()
581
def clone(self, a_bzrdir, revision_id=None):
583
return self._real_repository.clone(a_bzrdir, revision_id=revision_id)
585
def make_working_trees(self):
586
"""RemoteRepositories never create working trees by default."""
589
def fetch(self, source, revision_id=None, pb=None):
591
return self._real_repository.fetch(
592
source, revision_id=revision_id, pb=pb)
594
def create_bundle(self, target, base, fileobj, format=None):
596
self._real_repository.create_bundle(target, base, fileobj, format)
599
def control_weaves(self):
601
return self._real_repository.control_weaves
604
def get_ancestry(self, revision_id, topo_sorted=True):
606
return self._real_repository.get_ancestry(revision_id, topo_sorted)
609
def get_inventory_weave(self):
611
return self._real_repository.get_inventory_weave()
613
def fileids_altered_by_revision_ids(self, revision_ids):
615
return self._real_repository.fileids_altered_by_revision_ids(revision_ids)
617
def iter_files_bytes(self, desired_files):
618
"""See Repository.iter_file_bytes.
621
return self._real_repository.iter_files_bytes(desired_files)
624
def get_signature_text(self, revision_id):
626
return self._real_repository.get_signature_text(revision_id)
629
def get_revision_graph_with_ghosts(self, revision_ids=None):
631
return self._real_repository.get_revision_graph_with_ghosts(
632
revision_ids=revision_ids)
635
def get_inventory_xml(self, revision_id):
637
return self._real_repository.get_inventory_xml(revision_id)
639
def deserialise_inventory(self, revision_id, xml):
641
return self._real_repository.deserialise_inventory(revision_id, xml)
643
def reconcile(self, other=None, thorough=False):
645
return self._real_repository.reconcile(other=other, thorough=thorough)
647
def all_revision_ids(self):
649
return self._real_repository.all_revision_ids()
652
def get_deltas_for_revisions(self, revisions):
654
return self._real_repository.get_deltas_for_revisions(revisions)
657
def get_revision_delta(self, revision_id):
659
return self._real_repository.get_revision_delta(revision_id)
662
def revision_trees(self, revision_ids):
664
return self._real_repository.revision_trees(revision_ids)
667
def get_revision_reconcile(self, revision_id):
669
return self._real_repository.get_revision_reconcile(revision_id)
672
def check(self, revision_ids):
674
return self._real_repository.check(revision_ids)
676
def copy_content_into(self, destination, revision_id=None):
678
return self._real_repository.copy_content_into(
679
destination, revision_id=revision_id)
681
def _copy_repository_tarball(self, destination, revision_id=None):
682
# get a tarball of the remote repository, and copy from that into the
684
from bzrlib import osutils
687
from StringIO import StringIO
688
# TODO: Maybe a progress bar while streaming the tarball?
689
note("Copying repository content as tarball...")
690
tar_file = self._get_tarball('bz2')
692
tar = tarfile.open('repository', fileobj=tar_file,
694
tmpdir = tempfile.mkdtemp()
696
_extract_tar(tar, tmpdir)
697
tmp_bzrdir = BzrDir.open(tmpdir)
698
tmp_repo = tmp_bzrdir.open_repository()
699
tmp_repo.copy_content_into(destination, revision_id)
701
osutils.rmtree(tmpdir)
704
# TODO: if the server doesn't support this operation, maybe do it the
705
# slow way using the _real_repository?
707
# TODO: Suggestion from john: using external tar is much faster than
708
# python's tarfile library, but it may not work on windows.
712
"""Compress the data within the repository.
714
This is not currently implemented within the smart server.
717
return self._real_repository.pack()
719
def set_make_working_trees(self, new_value):
720
raise NotImplementedError(self.set_make_working_trees)
723
def sign_revision(self, revision_id, gpg_strategy):
725
return self._real_repository.sign_revision(revision_id, gpg_strategy)
728
def get_revisions(self, revision_ids):
730
return self._real_repository.get_revisions(revision_ids)
732
def supports_rich_root(self):
734
return self._real_repository.supports_rich_root()
736
def iter_reverse_revision_history(self, revision_id):
738
return self._real_repository.iter_reverse_revision_history(revision_id)
741
def _serializer(self):
743
return self._real_repository._serializer
745
def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
747
return self._real_repository.store_revision_signature(
748
gpg_strategy, plaintext, revision_id)
750
def has_signature_for_revision_id(self, revision_id):
752
return self._real_repository.has_signature_for_revision_id(revision_id)
754
def get_data_stream(self, revision_ids):
755
path = self.bzrdir._path_for_remote_call(self._client)
756
response, protocol = self._client.call_expecting_body(
757
'Repository.stream_knit_data_for_revisions', path, *revision_ids)
758
if response == ('ok',):
759
buffer = StringIO(protocol.read_body_bytes())
760
reader = ContainerReader(buffer)
761
for record_names, read_bytes in reader.iter_records():
763
# These records should have only one name, and that name
764
# should be a one-element tuple.
765
[name_tuple] = record_names
767
raise errors.SmartProtocolError(
768
'Repository data stream had invalid record name %r'
770
yield name_tuple, read_bytes(None)
772
raise errors.UnexpectedSmartServerResponse(response)
774
def insert_data_stream(self, stream):
776
self._real_repository.insert_data_stream(stream)
778
def item_keys_introduced_by(self, revision_ids, _files_pb=None):
780
return self._real_repository.item_keys_introduced_by(revision_ids,
784
class RemoteBranchLockableFiles(LockableFiles):
785
"""A 'LockableFiles' implementation that talks to a smart server.
787
This is not a public interface class.
790
def __init__(self, bzrdir, _client):
792
self._client = _client
793
self._need_find_modes = True
794
LockableFiles.__init__(
795
self, bzrdir.get_branch_transport(None),
796
'lock', lockdir.LockDir)
798
def _find_modes(self):
799
# RemoteBranches don't let the client set the mode of control files.
800
self._dir_mode = None
801
self._file_mode = None
804
"""'get' a remote path as per the LockableFiles interface.
806
:param path: the file to 'get'. If this is 'branch.conf', we do not
807
just retrieve a file, instead we ask the smart server to generate
808
a configuration for us - which is retrieved as an INI file.
810
if path == 'branch.conf':
811
path = self.bzrdir._path_for_remote_call(self._client)
812
response = self._client.call_expecting_body(
813
'Branch.get_config_file', path)
814
assert response[0][0] == 'ok', \
815
'unexpected response code %s' % (response[0],)
816
return StringIO(response[1].read_body_bytes())
819
return LockableFiles.get(self, path)
822
class RemoteBranchFormat(branch.BranchFormat):
824
def __eq__(self, other):
825
return (isinstance(other, RemoteBranchFormat) and
826
self.__dict__ == other.__dict__)
828
def get_format_description(self):
829
return 'Remote BZR Branch'
831
def get_format_string(self):
832
return 'Remote BZR Branch'
834
def open(self, a_bzrdir):
835
assert isinstance(a_bzrdir, RemoteBzrDir)
836
return a_bzrdir.open_branch()
838
def initialize(self, a_bzrdir):
839
assert isinstance(a_bzrdir, RemoteBzrDir)
840
return a_bzrdir.create_branch()
842
def supports_tags(self):
843
# Remote branches might support tags, but we won't know until we
844
# access the real remote branch.
848
class RemoteBranch(branch.Branch):
849
"""Branch stored on a server accessed by HPSS RPC.
851
At the moment most operations are mapped down to simple file operations.
854
def __init__(self, remote_bzrdir, remote_repository, real_branch=None,
856
"""Create a RemoteBranch instance.
858
:param real_branch: An optional local implementation of the branch
859
format, usually accessing the data via the VFS.
860
:param _client: Private parameter for testing.
862
# We intentionally don't call the parent class's __init__, because it
863
# will try to assign to self.tags, which is a property in this subclass.
864
# And the parent's __init__ doesn't do much anyway.
865
self._revision_history_cache = None
866
self.bzrdir = remote_bzrdir
867
if _client is not None:
868
self._client = _client
870
self._client = client._SmartClient(self.bzrdir._shared_medium)
871
self.repository = remote_repository
872
if real_branch is not None:
873
self._real_branch = real_branch
874
# Give the remote repository the matching real repo.
875
real_repo = self._real_branch.repository
876
if isinstance(real_repo, RemoteRepository):
877
real_repo._ensure_real()
878
real_repo = real_repo._real_repository
879
self.repository._set_real_repository(real_repo)
880
# Give the branch the remote repository to let fast-pathing happen.
881
self._real_branch.repository = self.repository
883
self._real_branch = None
884
# Fill out expected attributes of branch for bzrlib api users.
885
self._format = RemoteBranchFormat()
886
self.base = self.bzrdir.root_transport.base
887
self._control_files = None
888
self._lock_mode = None
889
self._lock_token = None
891
self._leave_lock = False
894
return "%s(%s)" % (self.__class__.__name__, self.base)
898
def _ensure_real(self):
899
"""Ensure that there is a _real_branch set.
901
Used before calls to self._real_branch.
903
if not self._real_branch:
904
assert vfs.vfs_enabled()
905
self.bzrdir._ensure_real()
906
self._real_branch = self.bzrdir._real_bzrdir.open_branch()
907
# Give the remote repository the matching real repo.
908
real_repo = self._real_branch.repository
909
if isinstance(real_repo, RemoteRepository):
910
real_repo._ensure_real()
911
real_repo = real_repo._real_repository
912
self.repository._set_real_repository(real_repo)
913
# Give the branch the remote repository to let fast-pathing happen.
914
self._real_branch.repository = self.repository
915
# XXX: deal with _lock_mode == 'w'
916
if self._lock_mode == 'r':
917
self._real_branch.lock_read()
920
def control_files(self):
921
# Defer actually creating RemoteBranchLockableFiles until its needed,
922
# because it triggers an _ensure_real that we otherwise might not need.
923
if self._control_files is None:
924
self._control_files = RemoteBranchLockableFiles(
925
self.bzrdir, self._client)
926
return self._control_files
928
def _get_checkout_format(self):
930
return self._real_branch._get_checkout_format()
932
def get_physical_lock_status(self):
933
"""See Branch.get_physical_lock_status()."""
934
# should be an API call to the server, as branches must be lockable.
936
return self._real_branch.get_physical_lock_status()
939
if not self._lock_mode:
940
self._lock_mode = 'r'
942
if self._real_branch is not None:
943
self._real_branch.lock_read()
945
self._lock_count += 1
947
def _remote_lock_write(self, token):
949
branch_token = repo_token = ''
952
repo_token = self.repository.lock_write()
953
self.repository.unlock()
954
path = self.bzrdir._path_for_remote_call(self._client)
955
response = self._client.call('Branch.lock_write', path, branch_token,
957
if response[0] == 'ok':
958
ok, branch_token, repo_token = response
959
return branch_token, repo_token
960
elif response[0] == 'LockContention':
961
raise errors.LockContention('(remote lock)')
962
elif response[0] == 'TokenMismatch':
963
raise errors.TokenMismatch(token, '(remote token)')
964
elif response[0] == 'UnlockableTransport':
965
raise errors.UnlockableTransport(self.bzrdir.root_transport)
966
elif response[0] == 'ReadOnlyError':
967
raise errors.ReadOnlyError(self)
969
raise errors.UnexpectedSmartServerResponse(response)
971
def lock_write(self, token=None):
972
if not self._lock_mode:
973
remote_tokens = self._remote_lock_write(token)
974
self._lock_token, self._repo_lock_token = remote_tokens
975
assert self._lock_token, 'Remote server did not return a token!'
976
# TODO: We really, really, really don't want to call _ensure_real
977
# here, but it's the easiest way to ensure coherency between the
978
# state of the RemoteBranch and RemoteRepository objects and the
979
# physical locks. If we don't materialise the real objects here,
980
# then getting everything in the right state later is complex, so
981
# for now we just do it the lazy way.
982
# -- Andrew Bennetts, 2007-02-22.
984
if self._real_branch is not None:
985
self._real_branch.repository.lock_write(
986
token=self._repo_lock_token)
988
self._real_branch.lock_write(token=self._lock_token)
990
self._real_branch.repository.unlock()
991
if token is not None:
992
self._leave_lock = True
994
# XXX: this case seems to be unreachable; token cannot be None.
995
self._leave_lock = False
996
self._lock_mode = 'w'
998
elif self._lock_mode == 'r':
999
raise errors.ReadOnlyTransaction
1001
if token is not None:
1002
# A token was given to lock_write, and we're relocking, so check
1003
# that the given token actually matches the one we already have.
1004
if token != self._lock_token:
1005
raise errors.TokenMismatch(token, self._lock_token)
1006
self._lock_count += 1
1007
return self._lock_token
1009
def _unlock(self, branch_token, repo_token):
1010
path = self.bzrdir._path_for_remote_call(self._client)
1011
response = self._client.call('Branch.unlock', path, branch_token,
1013
if response == ('ok',):
1015
elif response[0] == 'TokenMismatch':
1016
raise errors.TokenMismatch(
1017
str((branch_token, repo_token)), '(remote tokens)')
1019
raise errors.UnexpectedSmartServerResponse(response)
1022
self._lock_count -= 1
1023
if not self._lock_count:
1024
self._clear_cached_state()
1025
mode = self._lock_mode
1026
self._lock_mode = None
1027
if self._real_branch is not None:
1028
if not self._leave_lock:
1029
# If this RemoteBranch will remove the physical lock for the
1030
# repository, make sure the _real_branch doesn't do it
1031
# first. (Because the _real_branch's repository is set to
1032
# be the RemoteRepository.)
1033
self._real_branch.repository.leave_lock_in_place()
1034
self._real_branch.unlock()
1036
# Only write-locked branched need to make a remote method call
1037
# to perfom the unlock.
1039
assert self._lock_token, 'Locked, but no token!'
1040
branch_token = self._lock_token
1041
repo_token = self._repo_lock_token
1042
self._lock_token = None
1043
self._repo_lock_token = None
1044
if not self._leave_lock:
1045
self._unlock(branch_token, repo_token)
1047
def break_lock(self):
1049
return self._real_branch.break_lock()
1051
def leave_lock_in_place(self):
1052
self._leave_lock = True
1054
def dont_leave_lock_in_place(self):
1055
self._leave_lock = False
1057
def last_revision_info(self):
1058
"""See Branch.last_revision_info()."""
1059
path = self.bzrdir._path_for_remote_call(self._client)
1060
response = self._client.call('Branch.last_revision_info', path)
1061
assert response[0] == 'ok', 'unexpected response code %s' % (response,)
1062
revno = int(response[1])
1063
last_revision = response[2]
1064
return (revno, last_revision)
1066
def _gen_revision_history(self):
1067
"""See Branch._gen_revision_history()."""
1068
path = self.bzrdir._path_for_remote_call(self._client)
1069
response = self._client.call_expecting_body(
1070
'Branch.revision_history', path)
1071
assert response[0][0] == 'ok', ('unexpected response code %s'
1073
result = response[1].read_body_bytes().split('\x00')
1079
def set_revision_history(self, rev_history):
1080
# Send just the tip revision of the history; the server will generate
1081
# the full history from that. If the revision doesn't exist in this
1082
# branch, NoSuchRevision will be raised.
1083
path = self.bzrdir._path_for_remote_call(self._client)
1084
if rev_history == []:
1087
rev_id = rev_history[-1]
1088
self._clear_cached_state()
1089
response = self._client.call('Branch.set_last_revision',
1090
path, self._lock_token, self._repo_lock_token, rev_id)
1091
if response[0] == 'NoSuchRevision':
1092
raise NoSuchRevision(self, rev_id)
1094
assert response == ('ok',), (
1095
'unexpected response code %r' % (response,))
1096
self._cache_revision_history(rev_history)
1098
def get_parent(self):
1100
return self._real_branch.get_parent()
1102
def set_parent(self, url):
1104
return self._real_branch.set_parent(url)
1106
def get_config(self):
1107
return RemoteBranchConfig(self)
1109
def sprout(self, to_bzrdir, revision_id=None):
1110
# Like Branch.sprout, except that it sprouts a branch in the default
1111
# format, because RemoteBranches can't be created at arbitrary URLs.
1112
# XXX: if to_bzrdir is a RemoteBranch, this should perhaps do
1113
# to_bzrdir.create_branch...
1114
result = branch.BranchFormat.get_default_format().initialize(to_bzrdir)
1115
self.copy_content_into(result, revision_id=revision_id)
1116
result.set_parent(self.bzrdir.root_transport.base)
1120
def pull(self, source, overwrite=False, stop_revision=None,
1122
# FIXME: This asks the real branch to run the hooks, which means
1123
# they're called with the wrong target branch parameter.
1124
# The test suite specifically allows this at present but it should be
1125
# fixed. It should get a _override_hook_target branch,
1126
# as push does. -- mbp 20070405
1128
self._real_branch.pull(
1129
source, overwrite=overwrite, stop_revision=stop_revision,
1133
def push(self, target, overwrite=False, stop_revision=None):
1135
return self._real_branch.push(
1136
target, overwrite=overwrite, stop_revision=stop_revision,
1137
_override_hook_source_branch=self)
1139
def is_locked(self):
1140
return self._lock_count >= 1
1142
def set_last_revision_info(self, revno, revision_id):
1144
self._clear_cached_state()
1145
return self._real_branch.set_last_revision_info(revno, revision_id)
1147
def generate_revision_history(self, revision_id, last_rev=None,
1150
return self._real_branch.generate_revision_history(
1151
revision_id, last_rev=last_rev, other_branch=other_branch)
1156
return self._real_branch.tags
1158
def set_push_location(self, location):
1160
return self._real_branch.set_push_location(location)
1162
def update_revisions(self, other, stop_revision=None):
1164
return self._real_branch.update_revisions(
1165
other, stop_revision=stop_revision)
1168
class RemoteBranchConfig(BranchConfig):
1171
self.branch._ensure_real()
1172
return self.branch._real_branch.get_config().username()
1174
def _get_branch_data_config(self):
1175
self.branch._ensure_real()
1176
if self._branch_data_config is None:
1177
self._branch_data_config = TreeConfig(self.branch._real_branch)
1178
return self._branch_data_config
1181
def _extract_tar(tar, to_dir):
1182
"""Extract all the contents of a tarfile object.
1184
A replacement for extractall, which is not present in python2.4
1187
tar.extract(tarinfo, to_dir)