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
31
from bzrlib.branch import BranchReferenceFormat
32
from bzrlib.bzrdir import BzrDir, RemoteBzrDirFormat
33
from bzrlib.config import BranchConfig, TreeConfig
34
from bzrlib.decorators import needs_read_lock, needs_write_lock
35
from bzrlib.errors import NoSuchRevision
36
from bzrlib.lockable_files import LockableFiles
37
from bzrlib.pack import ContainerPushParser
38
from bzrlib.smart import client, vfs
39
from bzrlib.symbol_versioning import (
43
from bzrlib.revision import NULL_REVISION
44
from bzrlib.trace import mutter, note
45
from bzrlib.tuned_gzip import GzipFile
47
# Note: RemoteBzrDirFormat is in bzrdir.py
49
class RemoteBzrDir(BzrDir):
50
"""Control directory on a remote server, accessed via bzr:// or similar."""
52
def __init__(self, transport, _client=None):
53
"""Construct a RemoteBzrDir.
55
:param _client: Private parameter for testing. Disables probing and the
58
BzrDir.__init__(self, transport, RemoteBzrDirFormat())
59
# this object holds a delegated bzrdir that uses file-level operations
60
# to talk to the other side
61
self._real_bzrdir = None
64
self._shared_medium = transport.get_shared_medium()
65
self._client = client._SmartClient(self._shared_medium)
67
self._client = _client
68
self._shared_medium = None
71
path = self._path_for_remote_call(self._client)
72
response = self._client.call('BzrDir.open', path)
73
if response not in [('yes',), ('no',)]:
74
raise errors.UnexpectedSmartServerResponse(response)
75
if response == ('no',):
76
raise errors.NotBranchError(path=transport.base)
78
def _ensure_real(self):
79
"""Ensure that there is a _real_bzrdir set.
81
Used before calls to self._real_bzrdir.
83
if not self._real_bzrdir:
84
self._real_bzrdir = BzrDir.open_from_transport(
85
self.root_transport, _server_formats=False)
87
def create_repository(self, shared=False):
89
self._real_bzrdir.create_repository(shared=shared)
90
return self.open_repository()
92
def destroy_repository(self):
93
"""See BzrDir.destroy_repository"""
95
self._real_bzrdir.destroy_repository()
97
def create_branch(self):
99
real_branch = self._real_bzrdir.create_branch()
100
return RemoteBranch(self, self.find_repository(), real_branch)
102
def destroy_branch(self):
103
"""See BzrDir.destroy_branch"""
105
self._real_bzrdir.destroy_branch()
107
def create_workingtree(self, revision_id=None, from_branch=None):
108
raise errors.NotLocalUrl(self.transport.base)
110
def find_branch_format(self):
111
"""Find the branch 'format' for this bzrdir.
113
This might be a synthetic object for e.g. RemoteBranch and SVN.
115
b = self.open_branch()
118
def get_branch_reference(self):
119
"""See BzrDir.get_branch_reference()."""
120
path = self._path_for_remote_call(self._client)
121
response = self._client.call('BzrDir.open_branch', path)
122
if response[0] == 'ok':
123
if response[1] == '':
124
# branch at this location.
127
# a branch reference, use the existing BranchReference logic.
129
elif response == ('nobranch',):
130
raise errors.NotBranchError(path=self.root_transport.base)
132
raise errors.UnexpectedSmartServerResponse(response)
134
def open_branch(self, _unsupported=False):
135
assert _unsupported == False, 'unsupported flag support not implemented yet.'
136
reference_url = self.get_branch_reference()
137
if reference_url is None:
138
# branch at this location.
139
return RemoteBranch(self, self.find_repository())
141
# a branch reference, use the existing BranchReference logic.
142
format = BranchReferenceFormat()
143
return format.open(self, _found=True, location=reference_url)
145
def open_repository(self):
146
path = self._path_for_remote_call(self._client)
147
response = self._client.call('BzrDir.find_repository', path)
148
assert response[0] in ('ok', 'norepository'), \
149
'unexpected response code %s' % (response,)
150
if response[0] == 'norepository':
151
raise errors.NoRepositoryPresent(self)
152
assert len(response) == 4, 'incorrect response length %s' % (response,)
153
if response[1] == '':
154
format = RemoteRepositoryFormat()
155
format.rich_root_data = (response[2] == 'yes')
156
format.supports_tree_reference = (response[3] == 'yes')
157
return RemoteRepository(self, format)
159
raise errors.NoRepositoryPresent(self)
161
def open_workingtree(self, recommend_upgrade=True):
163
if self._real_bzrdir.has_workingtree():
164
raise errors.NotLocalUrl(self.root_transport)
166
raise errors.NoWorkingTree(self.root_transport.base)
168
def _path_for_remote_call(self, client):
169
"""Return the path to be used for this bzrdir in a remote call."""
170
return client.remote_path_from_transport(self.root_transport)
172
def get_branch_transport(self, branch_format):
174
return self._real_bzrdir.get_branch_transport(branch_format)
176
def get_repository_transport(self, repository_format):
178
return self._real_bzrdir.get_repository_transport(repository_format)
180
def get_workingtree_transport(self, workingtree_format):
182
return self._real_bzrdir.get_workingtree_transport(workingtree_format)
184
def can_convert_format(self):
185
"""Upgrading of remote bzrdirs is not supported yet."""
188
def needs_format_conversion(self, format=None):
189
"""Upgrading of remote bzrdirs is not supported yet."""
192
def clone(self, url, revision_id=None, force_new_repo=False):
194
return self._real_bzrdir.clone(url, revision_id=revision_id,
195
force_new_repo=force_new_repo)
198
class RemoteRepositoryFormat(repository.RepositoryFormat):
199
"""Format for repositories accessed over a _SmartClient.
201
Instances of this repository are represented by RemoteRepository
204
The RemoteRepositoryFormat is parameterized during construction
205
to reflect the capabilities of the real, remote format. Specifically
206
the attributes rich_root_data and supports_tree_reference are set
207
on a per instance basis, and are not set (and should not be) at
211
_matchingbzrdir = RemoteBzrDirFormat
213
def initialize(self, a_bzrdir, shared=False):
214
assert isinstance(a_bzrdir, RemoteBzrDir), \
215
'%r is not a RemoteBzrDir' % (a_bzrdir,)
216
return a_bzrdir.create_repository(shared=shared)
218
def open(self, a_bzrdir):
219
assert isinstance(a_bzrdir, RemoteBzrDir)
220
return a_bzrdir.open_repository()
222
def get_format_description(self):
223
return 'bzr remote repository'
225
def __eq__(self, other):
226
return self.__class__ == other.__class__
228
def check_conversion_target(self, target_format):
229
if self.rich_root_data and not target_format.rich_root_data:
230
raise errors.BadConversionTarget(
231
'Does not support rich root data.', target_format)
232
if (self.supports_tree_reference and
233
not getattr(target_format, 'supports_tree_reference', False)):
234
raise errors.BadConversionTarget(
235
'Does not support nested trees', target_format)
238
class RemoteRepository(object):
239
"""Repository accessed over rpc.
241
For the moment most operations are performed using local transport-backed
245
def __init__(self, remote_bzrdir, format, real_repository=None, _client=None):
246
"""Create a RemoteRepository instance.
248
:param remote_bzrdir: The bzrdir hosting this repository.
249
:param format: The RemoteFormat object to use.
250
:param real_repository: If not None, a local implementation of the
251
repository logic for the repository, usually accessing the data
253
:param _client: Private testing parameter - override the smart client
254
to be used by the repository.
257
self._real_repository = real_repository
259
self._real_repository = None
260
self.bzrdir = remote_bzrdir
262
self._client = client._SmartClient(self.bzrdir._shared_medium)
264
self._client = _client
265
self._format = format
266
self._lock_mode = None
267
self._lock_token = None
269
self._leave_lock = False
270
# A cache of looked up revision parent data; reset at unlock time.
271
self._parents_map = None
272
if 'hpss' in debug.debug_flags:
273
self._requested_parents = None
275
# These depend on the actual remote format, so force them off for
276
# maximum compatibility. XXX: In future these should depend on the
277
# remote repository instance, but this is irrelevant until we perform
278
# reconcile via an RPC call.
279
self._reconcile_does_inventory_gc = False
280
self._reconcile_fixes_text_parents = False
281
self._reconcile_backsup_inventory = False
282
self.base = self.bzrdir.transport.base
285
return "%s(%s)" % (self.__class__.__name__, self.base)
289
def abort_write_group(self):
290
"""Complete a write group on the decorated repository.
292
Smart methods peform operations in a single step so this api
293
is not really applicable except as a compatibility thunk
294
for older plugins that don't use e.g. the CommitBuilder
298
return self._real_repository.abort_write_group()
300
def commit_write_group(self):
301
"""Complete a write group on the decorated repository.
303
Smart methods peform operations in a single step so this api
304
is not really applicable except as a compatibility thunk
305
for older plugins that don't use e.g. the CommitBuilder
309
return self._real_repository.commit_write_group()
311
def _ensure_real(self):
312
"""Ensure that there is a _real_repository set.
314
Used before calls to self._real_repository.
316
if not self._real_repository:
317
self.bzrdir._ensure_real()
318
#self._real_repository = self.bzrdir._real_bzrdir.open_repository()
319
self._set_real_repository(self.bzrdir._real_bzrdir.open_repository())
321
def find_text_key_references(self):
322
"""Find the text key references within the repository.
324
:return: a dictionary mapping (file_id, revision_id) tuples to altered file-ids to an iterable of
325
revision_ids. Each altered file-ids has the exact revision_ids that
326
altered it listed explicitly.
327
:return: A dictionary mapping text keys ((fileid, revision_id) tuples)
328
to whether they were referred to by the inventory of the
329
revision_id that they contain. The inventory texts from all present
330
revision ids are assessed to generate this report.
333
return self._real_repository.find_text_key_references()
335
def _generate_text_key_index(self):
336
"""Generate a new text key index for the repository.
338
This is an expensive function that will take considerable time to run.
340
:return: A dict mapping (file_id, revision_id) tuples to a list of
341
parents, also (file_id, revision_id) tuples.
344
return self._real_repository._generate_text_key_index()
346
def get_revision_graph(self, revision_id=None):
347
"""See Repository.get_revision_graph()."""
348
if revision_id is None:
350
elif revision.is_null(revision_id):
353
path = self.bzrdir._path_for_remote_call(self._client)
354
assert type(revision_id) is str
355
response = self._client.call_expecting_body(
356
'Repository.get_revision_graph', path, revision_id)
357
if response[0][0] not in ['ok', 'nosuchrevision']:
358
raise errors.UnexpectedSmartServerResponse(response[0])
359
if response[0][0] == 'ok':
360
coded = response[1].read_body_bytes()
362
# no revisions in this repository!
364
lines = coded.split('\n')
367
d = tuple(line.split())
368
revision_graph[d[0]] = d[1:]
370
return revision_graph
372
response_body = response[1].read_body_bytes()
373
assert response_body == ''
374
raise NoSuchRevision(self, revision_id)
376
def has_revision(self, revision_id):
377
"""See Repository.has_revision()."""
378
if revision_id == NULL_REVISION:
379
# The null revision is always present.
381
path = self.bzrdir._path_for_remote_call(self._client)
382
response = self._client.call('Repository.has_revision', path, revision_id)
383
assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
384
return response[0] == 'yes'
386
def has_revisions(self, revision_ids):
387
"""See Repository.has_revisions()."""
389
for revision_id in revision_ids:
390
if self.has_revision(revision_id):
391
result.add(revision_id)
394
def has_same_location(self, other):
395
return (self.__class__ == other.__class__ and
396
self.bzrdir.transport.base == other.bzrdir.transport.base)
398
def get_graph(self, other_repository=None):
399
"""Return the graph for this repository format"""
400
parents_provider = self
401
if (other_repository is not None and
402
other_repository.bzrdir.transport.base !=
403
self.bzrdir.transport.base):
404
parents_provider = graph._StackedParentsProvider(
405
[parents_provider, other_repository._make_parents_provider()])
406
return graph.Graph(parents_provider)
408
def gather_stats(self, revid=None, committers=None):
409
"""See Repository.gather_stats()."""
410
path = self.bzrdir._path_for_remote_call(self._client)
411
# revid can be None to indicate no revisions, not just NULL_REVISION
412
if revid is None or revision.is_null(revid):
416
if committers is None or not committers:
417
fmt_committers = 'no'
419
fmt_committers = 'yes'
420
response = self._client.call_expecting_body(
421
'Repository.gather_stats', path, fmt_revid, fmt_committers)
422
assert response[0][0] == 'ok', \
423
'unexpected response code %s' % (response[0],)
425
body = response[1].read_body_bytes()
427
for line in body.split('\n'):
430
key, val_text = line.split(':')
431
if key in ('revisions', 'size', 'committers'):
432
result[key] = int(val_text)
433
elif key in ('firstrev', 'latestrev'):
434
values = val_text.split(' ')[1:]
435
result[key] = (float(values[0]), long(values[1]))
439
def find_branches(self, using=False):
440
"""See Repository.find_branches()."""
441
# should be an API call to the server.
443
return self._real_repository.find_branches(using=using)
445
def get_physical_lock_status(self):
446
"""See Repository.get_physical_lock_status()."""
447
# should be an API call to the server.
449
return self._real_repository.get_physical_lock_status()
451
def is_in_write_group(self):
452
"""Return True if there is an open write group.
454
write groups are only applicable locally for the smart server..
456
if self._real_repository:
457
return self._real_repository.is_in_write_group()
460
return self._lock_count >= 1
463
"""See Repository.is_shared()."""
464
path = self.bzrdir._path_for_remote_call(self._client)
465
response = self._client.call('Repository.is_shared', path)
466
assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
467
return response[0] == 'yes'
469
def is_write_locked(self):
470
return self._lock_mode == 'w'
473
# wrong eventually - want a local lock cache context
474
if not self._lock_mode:
475
self._lock_mode = 'r'
477
self._parents_map = {}
478
if 'hpss' in debug.debug_flags:
479
self._requested_parents = set()
480
if self._real_repository is not None:
481
self._real_repository.lock_read()
483
self._lock_count += 1
485
def _remote_lock_write(self, token):
486
path = self.bzrdir._path_for_remote_call(self._client)
489
response = self._client.call('Repository.lock_write', path, token)
490
if response[0] == 'ok':
493
elif response[0] == 'LockContention':
494
raise errors.LockContention('(remote lock)')
495
elif response[0] == 'UnlockableTransport':
496
raise errors.UnlockableTransport(self.bzrdir.root_transport)
497
elif response[0] == 'LockFailed':
498
raise errors.LockFailed(response[1], response[2])
500
raise errors.UnexpectedSmartServerResponse(response)
502
def lock_write(self, token=None):
503
if not self._lock_mode:
504
self._lock_token = self._remote_lock_write(token)
505
# if self._lock_token is None, then this is something like packs or
506
# svn where we don't get to lock the repo, or a weave style repository
507
# where we cannot lock it over the wire and attempts to do so will
509
if self._real_repository is not None:
510
self._real_repository.lock_write(token=self._lock_token)
511
if token is not None:
512
self._leave_lock = True
514
self._leave_lock = False
515
self._lock_mode = 'w'
517
self._parents_map = {}
518
if 'hpss' in debug.debug_flags:
519
self._requested_parents = set()
520
elif self._lock_mode == 'r':
521
raise errors.ReadOnlyError(self)
523
self._lock_count += 1
524
return self._lock_token or None
526
def leave_lock_in_place(self):
527
if not self._lock_token:
528
raise NotImplementedError(self.leave_lock_in_place)
529
self._leave_lock = True
531
def dont_leave_lock_in_place(self):
532
if not self._lock_token:
533
raise NotImplementedError(self.dont_leave_lock_in_place)
534
self._leave_lock = False
536
def _set_real_repository(self, repository):
537
"""Set the _real_repository for this repository.
539
:param repository: The repository to fallback to for non-hpss
540
implemented operations.
542
assert not isinstance(repository, RemoteRepository)
543
self._real_repository = repository
544
if self._lock_mode == 'w':
545
# if we are already locked, the real repository must be able to
546
# acquire the lock with our token.
547
self._real_repository.lock_write(self._lock_token)
548
elif self._lock_mode == 'r':
549
self._real_repository.lock_read()
551
def start_write_group(self):
552
"""Start a write group on the decorated repository.
554
Smart methods peform operations in a single step so this api
555
is not really applicable except as a compatibility thunk
556
for older plugins that don't use e.g. the CommitBuilder
560
return self._real_repository.start_write_group()
562
def _unlock(self, token):
563
path = self.bzrdir._path_for_remote_call(self._client)
565
# with no token the remote repository is not persistently locked.
567
response = self._client.call('Repository.unlock', path, token)
568
if response == ('ok',):
570
elif response[0] == 'TokenMismatch':
571
raise errors.TokenMismatch(token, '(remote token)')
573
raise errors.UnexpectedSmartServerResponse(response)
576
self._lock_count -= 1
577
if self._lock_count > 0:
579
self._parents_map = None
580
if 'hpss' in debug.debug_flags:
581
self._requested_parents = None
582
old_mode = self._lock_mode
583
self._lock_mode = None
585
# The real repository is responsible at present for raising an
586
# exception if it's in an unfinished write group. However, it
587
# normally will *not* actually remove the lock from disk - that's
588
# done by the server on receiving the Repository.unlock call.
589
# This is just to let the _real_repository stay up to date.
590
if self._real_repository is not None:
591
self._real_repository.unlock()
593
# The rpc-level lock should be released even if there was a
594
# problem releasing the vfs-based lock.
596
# Only write-locked repositories need to make a remote method
597
# call to perfom the unlock.
598
old_token = self._lock_token
599
self._lock_token = None
600
if not self._leave_lock:
601
self._unlock(old_token)
603
def break_lock(self):
604
# should hand off to the network
606
return self._real_repository.break_lock()
608
def _get_tarball(self, compression):
609
"""Return a TemporaryFile containing a repository tarball.
611
Returns None if the server does not support sending tarballs.
614
path = self.bzrdir._path_for_remote_call(self._client)
615
response, protocol = self._client.call_expecting_body(
616
'Repository.tarball', path, compression)
617
if response[0] == 'ok':
618
# Extract the tarball and return it
619
t = tempfile.NamedTemporaryFile()
620
# TODO: rpc layer should read directly into it...
621
t.write(protocol.read_body_bytes())
624
if (response == ('error', "Generic bzr smart protocol error: "
625
"bad request 'Repository.tarball'") or
626
response == ('error', "Generic bzr smart protocol error: "
627
"bad request u'Repository.tarball'")):
628
protocol.cancel_read_body()
630
raise errors.UnexpectedSmartServerResponse(response)
632
def sprout(self, to_bzrdir, revision_id=None):
633
# TODO: Option to control what format is created?
635
dest_repo = self._real_repository._format.initialize(to_bzrdir,
637
dest_repo.fetch(self, revision_id=revision_id)
640
### These methods are just thin shims to the VFS object for now.
642
def revision_tree(self, revision_id):
644
return self._real_repository.revision_tree(revision_id)
646
def get_serializer_format(self):
648
return self._real_repository.get_serializer_format()
650
def get_commit_builder(self, branch, parents, config, timestamp=None,
651
timezone=None, committer=None, revprops=None,
653
# FIXME: It ought to be possible to call this without immediately
654
# triggering _ensure_real. For now it's the easiest thing to do.
656
builder = self._real_repository.get_commit_builder(branch, parents,
657
config, timestamp=timestamp, timezone=timezone,
658
committer=committer, revprops=revprops, revision_id=revision_id)
661
def add_inventory(self, revid, inv, parents):
663
return self._real_repository.add_inventory(revid, inv, parents)
665
def add_revision(self, rev_id, rev, inv=None, config=None):
667
return self._real_repository.add_revision(
668
rev_id, rev, inv=inv, config=config)
671
def get_inventory(self, revision_id):
673
return self._real_repository.get_inventory(revision_id)
675
def iter_inventories(self, revision_ids):
677
return self._real_repository.iter_inventories(revision_ids)
680
def get_revision(self, revision_id):
682
return self._real_repository.get_revision(revision_id)
685
def weave_store(self):
687
return self._real_repository.weave_store
689
def get_transaction(self):
691
return self._real_repository.get_transaction()
694
def clone(self, a_bzrdir, revision_id=None):
696
return self._real_repository.clone(a_bzrdir, revision_id=revision_id)
698
def make_working_trees(self):
699
"""RemoteRepositories never create working trees by default."""
702
def revision_ids_to_search_result(self, result_set):
703
"""Convert a set of revision ids to a graph SearchResult."""
704
result_parents = set()
705
for parents in self.get_graph().get_parent_map(
706
result_set).itervalues():
707
result_parents.update(parents)
708
included_keys = result_set.intersection(result_parents)
709
start_keys = result_set.difference(included_keys)
710
exclude_keys = result_parents.difference(result_set)
711
result = graph.SearchResult(start_keys, exclude_keys,
712
len(result_set), result_set)
716
def search_missing_revision_ids(self, other, revision_id=None, find_ghosts=True):
717
"""Return the revision ids that other has that this does not.
719
These are returned in topological order.
721
revision_id: only return revision ids included by revision_id.
723
return repository.InterRepository.get(
724
other, self).search_missing_revision_ids(revision_id, find_ghosts)
726
def fetch(self, source, revision_id=None, pb=None):
727
if self.has_same_location(source):
728
# check that last_revision is in 'from' and then return a
730
if (revision_id is not None and
731
not revision.is_null(revision_id)):
732
self.get_revision(revision_id)
735
return self._real_repository.fetch(
736
source, revision_id=revision_id, pb=pb)
738
def create_bundle(self, target, base, fileobj, format=None):
740
self._real_repository.create_bundle(target, base, fileobj, format)
743
def control_weaves(self):
745
return self._real_repository.control_weaves
748
def get_ancestry(self, revision_id, topo_sorted=True):
750
return self._real_repository.get_ancestry(revision_id, topo_sorted)
753
def get_inventory_weave(self):
755
return self._real_repository.get_inventory_weave()
757
def fileids_altered_by_revision_ids(self, revision_ids):
759
return self._real_repository.fileids_altered_by_revision_ids(revision_ids)
761
def _get_versioned_file_checker(self, revisions, revision_versions_cache):
763
return self._real_repository._get_versioned_file_checker(
764
revisions, revision_versions_cache)
766
def iter_files_bytes(self, desired_files):
767
"""See Repository.iter_file_bytes.
770
return self._real_repository.iter_files_bytes(desired_files)
772
def get_parent_map(self, keys):
773
"""See bzrlib.Graph.get_parent_map()."""
774
# Hack to build up the caching logic.
775
ancestry = self._parents_map
776
missing_revisions = set(key for key in keys if key not in ancestry)
777
if missing_revisions:
778
parent_map = self._get_parent_map(missing_revisions)
779
if 'hpss' in debug.debug_flags:
780
mutter('retransmitted revisions: %d of %d',
781
len(set(self._parents_map).intersection(parent_map)),
783
self._parents_map.update(parent_map)
784
present_keys = [k for k in keys if k in ancestry]
785
if 'hpss' in debug.debug_flags:
786
self._requested_parents.update(present_keys)
787
mutter('Current RemoteRepository graph hit rate: %d%%',
788
100.0 * len(self._requested_parents) / len(self._parents_map))
789
return dict((k, ancestry[k]) for k in present_keys)
791
def _response_is_unknown_method(self, response, verb):
792
"""Return True if response is an unknonwn method response to verb.
794
:param response: The response from a smart client call_expecting_body
796
:param verb: The verb used in that call.
797
:return: True if an unknown method was encountered.
799
# This might live better on
800
# bzrlib.smart.protocol.SmartClientRequestProtocolOne
801
if (response[0] == ('error', "Generic bzr smart protocol error: "
802
"bad request '%s'" % verb) or
803
response[0] == ('error', "Generic bzr smart protocol error: "
804
"bad request u'%s'" % verb)):
805
response[1].cancel_read_body()
809
def _get_parent_map(self, keys):
810
"""Helper for get_parent_map that performs the RPC."""
812
if NULL_REVISION in keys:
813
keys.discard(NULL_REVISION)
814
found_parents = {NULL_REVISION:()}
819
# TODO(Needs analysis): We could assume that the keys being requested
820
# from get_parent_map are in a breadth first search, so typically they
821
# will all be depth N from some common parent, and we don't have to
822
# have the server iterate from the root parent, but rather from the
823
# keys we're searching; and just tell the server the keyspace we
824
# already have; but this may be more traffic again.
826
# Transform self._parents_map into a search request recipe.
827
# TODO: Manage this incrementally to avoid covering the same path
828
# repeatedly. (The server will have to on each request, but the less
829
# work done the better).
830
start_set = set(self._parents_map)
831
result_parents = set()
832
for parents in self._parents_map.itervalues():
833
result_parents.update(parents)
834
stop_keys = result_parents.difference(start_set)
835
included_keys = start_set.intersection(result_parents)
836
start_set.difference_update(included_keys)
837
recipe = (start_set, stop_keys, len(self._parents_map))
838
body = self._serialise_search_recipe(recipe)
839
path = self.bzrdir._path_for_remote_call(self._client)
841
assert type(key) is str
842
verb = 'Repository.get_parent_map'
843
args = (path,) + tuple(keys)
844
response = self._client.call_with_body_bytes_expecting_body(
845
verb, args, self._serialise_search_recipe(recipe))
846
if self._response_is_unknown_method(response, verb):
847
# Server that does not support this method, get the whole graph.
848
response = self._client.call_expecting_body(
849
'Repository.get_revision_graph', path, '')
850
if response[0][0] not in ['ok', 'nosuchrevision']:
851
reponse[1].cancel_read_body()
852
raise errors.UnexpectedSmartServerResponse(response[0])
853
elif response[0][0] not in ['ok']:
854
reponse[1].cancel_read_body()
855
raise errors.UnexpectedSmartServerResponse(response[0])
856
if response[0][0] == 'ok':
857
coded = GzipFile(mode='rb',
858
fileobj=StringIO(response[1].read_body_bytes())).read()
862
lines = coded.split('\n')
865
d = tuple(line.split())
867
revision_graph[d[0]] = d[1:]
869
# No parents - so give the Graph result (NULL_REVISION,).
870
revision_graph[d[0]] = (NULL_REVISION,)
871
return revision_graph
874
def get_signature_text(self, revision_id):
876
return self._real_repository.get_signature_text(revision_id)
879
def get_revision_graph_with_ghosts(self, revision_ids=None):
881
return self._real_repository.get_revision_graph_with_ghosts(
882
revision_ids=revision_ids)
885
def get_inventory_xml(self, revision_id):
887
return self._real_repository.get_inventory_xml(revision_id)
889
def deserialise_inventory(self, revision_id, xml):
891
return self._real_repository.deserialise_inventory(revision_id, xml)
893
def reconcile(self, other=None, thorough=False):
895
return self._real_repository.reconcile(other=other, thorough=thorough)
897
def all_revision_ids(self):
899
return self._real_repository.all_revision_ids()
902
def get_deltas_for_revisions(self, revisions):
904
return self._real_repository.get_deltas_for_revisions(revisions)
907
def get_revision_delta(self, revision_id):
909
return self._real_repository.get_revision_delta(revision_id)
912
def revision_trees(self, revision_ids):
914
return self._real_repository.revision_trees(revision_ids)
917
def get_revision_reconcile(self, revision_id):
919
return self._real_repository.get_revision_reconcile(revision_id)
922
def check(self, revision_ids=None):
924
return self._real_repository.check(revision_ids=revision_ids)
926
def copy_content_into(self, destination, revision_id=None):
928
return self._real_repository.copy_content_into(
929
destination, revision_id=revision_id)
931
def _copy_repository_tarball(self, to_bzrdir, revision_id=None):
932
# get a tarball of the remote repository, and copy from that into the
934
from bzrlib import osutils
937
# TODO: Maybe a progress bar while streaming the tarball?
938
note("Copying repository content as tarball...")
939
tar_file = self._get_tarball('bz2')
942
destination = to_bzrdir.create_repository()
944
tar = tarfile.open('repository', fileobj=tar_file,
946
tmpdir = tempfile.mkdtemp()
948
_extract_tar(tar, tmpdir)
949
tmp_bzrdir = BzrDir.open(tmpdir)
950
tmp_repo = tmp_bzrdir.open_repository()
951
tmp_repo.copy_content_into(destination, revision_id)
953
osutils.rmtree(tmpdir)
957
# TODO: Suggestion from john: using external tar is much faster than
958
# python's tarfile library, but it may not work on windows.
962
"""Compress the data within the repository.
964
This is not currently implemented within the smart server.
967
return self._real_repository.pack()
969
def set_make_working_trees(self, new_value):
970
raise NotImplementedError(self.set_make_working_trees)
973
def sign_revision(self, revision_id, gpg_strategy):
975
return self._real_repository.sign_revision(revision_id, gpg_strategy)
978
def get_revisions(self, revision_ids):
980
return self._real_repository.get_revisions(revision_ids)
982
def supports_rich_root(self):
984
return self._real_repository.supports_rich_root()
986
def iter_reverse_revision_history(self, revision_id):
988
return self._real_repository.iter_reverse_revision_history(revision_id)
991
def _serializer(self):
993
return self._real_repository._serializer
995
def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
997
return self._real_repository.store_revision_signature(
998
gpg_strategy, plaintext, revision_id)
1000
def add_signature_text(self, revision_id, signature):
1002
return self._real_repository.add_signature_text(revision_id, signature)
1004
def has_signature_for_revision_id(self, revision_id):
1006
return self._real_repository.has_signature_for_revision_id(revision_id)
1008
def get_data_stream_for_search(self, search):
1009
REQUEST_NAME = 'Repository.stream_revisions_chunked'
1010
path = self.bzrdir._path_for_remote_call(self._client)
1011
body = self._serialise_search_recipe(search.get_recipe())
1012
response, protocol = self._client.call_with_body_bytes_expecting_body(
1013
REQUEST_NAME, (path,), body)
1015
if response == ('ok',):
1016
return self._deserialise_stream(protocol)
1017
if response == ('NoSuchRevision', ):
1018
# We cannot easily identify the revision that is missing in this
1019
# situation without doing much more network IO. For now, bail.
1020
raise NoSuchRevision(self, "unknown")
1021
elif (response == ('error', "Generic bzr smart protocol error: "
1022
"bad request '%s'" % REQUEST_NAME) or
1023
response == ('error', "Generic bzr smart protocol error: "
1024
"bad request u'%s'" % REQUEST_NAME)):
1025
protocol.cancel_read_body()
1027
return self._real_repository.get_data_stream_for_search(search)
1029
raise errors.UnexpectedSmartServerResponse(response)
1031
def _deserialise_stream(self, protocol):
1032
stream = protocol.read_streamed_body()
1033
container_parser = ContainerPushParser()
1034
for bytes in stream:
1035
container_parser.accept_bytes(bytes)
1036
records = container_parser.read_pending_records()
1037
for record_names, record_bytes in records:
1038
if len(record_names) != 1:
1039
# These records should have only one name, and that name
1040
# should be a one-element tuple.
1041
raise errors.SmartProtocolError(
1042
'Repository data stream had invalid record name %r'
1044
name_tuple = record_names[0]
1045
yield name_tuple, record_bytes
1047
def insert_data_stream(self, stream):
1049
self._real_repository.insert_data_stream(stream)
1051
def item_keys_introduced_by(self, revision_ids, _files_pb=None):
1053
return self._real_repository.item_keys_introduced_by(revision_ids,
1054
_files_pb=_files_pb)
1056
def revision_graph_can_have_wrong_parents(self):
1057
# The answer depends on the remote repo format.
1059
return self._real_repository.revision_graph_can_have_wrong_parents()
1061
def _find_inconsistent_revision_parents(self):
1063
return self._real_repository._find_inconsistent_revision_parents()
1065
def _check_for_inconsistent_revision_parents(self):
1067
return self._real_repository._check_for_inconsistent_revision_parents()
1069
def _make_parents_provider(self):
1072
def _serialise_search_recipe(self, recipe):
1073
"""Serialise a graph search recipe.
1075
:param recipe: A search recipe (start, stop, count).
1076
:return: Serialised bytes.
1078
start_keys = ' '.join(recipe[0])
1079
stop_keys = ' '.join(recipe[1])
1080
count = str(recipe[2])
1081
return '\n'.join((start_keys, stop_keys, count))
1084
class RemoteBranchLockableFiles(LockableFiles):
1085
"""A 'LockableFiles' implementation that talks to a smart server.
1087
This is not a public interface class.
1090
def __init__(self, bzrdir, _client):
1091
self.bzrdir = bzrdir
1092
self._client = _client
1093
self._need_find_modes = True
1094
LockableFiles.__init__(
1095
self, bzrdir.get_branch_transport(None),
1096
'lock', lockdir.LockDir)
1098
def _find_modes(self):
1099
# RemoteBranches don't let the client set the mode of control files.
1100
self._dir_mode = None
1101
self._file_mode = None
1103
def get(self, path):
1104
"""'get' a remote path as per the LockableFiles interface.
1106
:param path: the file to 'get'. If this is 'branch.conf', we do not
1107
just retrieve a file, instead we ask the smart server to generate
1108
a configuration for us - which is retrieved as an INI file.
1110
if path == 'branch.conf':
1111
path = self.bzrdir._path_for_remote_call(self._client)
1112
response = self._client.call_expecting_body(
1113
'Branch.get_config_file', path)
1114
assert response[0][0] == 'ok', \
1115
'unexpected response code %s' % (response[0],)
1116
return StringIO(response[1].read_body_bytes())
1119
return LockableFiles.get(self, path)
1122
class RemoteBranchFormat(branch.BranchFormat):
1124
def __eq__(self, other):
1125
return (isinstance(other, RemoteBranchFormat) and
1126
self.__dict__ == other.__dict__)
1128
def get_format_description(self):
1129
return 'Remote BZR Branch'
1131
def get_format_string(self):
1132
return 'Remote BZR Branch'
1134
def open(self, a_bzrdir):
1135
assert isinstance(a_bzrdir, RemoteBzrDir)
1136
return a_bzrdir.open_branch()
1138
def initialize(self, a_bzrdir):
1139
assert isinstance(a_bzrdir, RemoteBzrDir)
1140
return a_bzrdir.create_branch()
1142
def supports_tags(self):
1143
# Remote branches might support tags, but we won't know until we
1144
# access the real remote branch.
1148
class RemoteBranch(branch.Branch):
1149
"""Branch stored on a server accessed by HPSS RPC.
1151
At the moment most operations are mapped down to simple file operations.
1154
def __init__(self, remote_bzrdir, remote_repository, real_branch=None,
1156
"""Create a RemoteBranch instance.
1158
:param real_branch: An optional local implementation of the branch
1159
format, usually accessing the data via the VFS.
1160
:param _client: Private parameter for testing.
1162
# We intentionally don't call the parent class's __init__, because it
1163
# will try to assign to self.tags, which is a property in this subclass.
1164
# And the parent's __init__ doesn't do much anyway.
1165
self._revision_id_to_revno_cache = None
1166
self._revision_history_cache = None
1167
self.bzrdir = remote_bzrdir
1168
if _client is not None:
1169
self._client = _client
1171
self._client = client._SmartClient(self.bzrdir._shared_medium)
1172
self.repository = remote_repository
1173
if real_branch is not None:
1174
self._real_branch = real_branch
1175
# Give the remote repository the matching real repo.
1176
real_repo = self._real_branch.repository
1177
if isinstance(real_repo, RemoteRepository):
1178
real_repo._ensure_real()
1179
real_repo = real_repo._real_repository
1180
self.repository._set_real_repository(real_repo)
1181
# Give the branch the remote repository to let fast-pathing happen.
1182
self._real_branch.repository = self.repository
1184
self._real_branch = None
1185
# Fill out expected attributes of branch for bzrlib api users.
1186
self._format = RemoteBranchFormat()
1187
self.base = self.bzrdir.root_transport.base
1188
self._control_files = None
1189
self._lock_mode = None
1190
self._lock_token = None
1191
self._lock_count = 0
1192
self._leave_lock = False
1195
return "%s(%s)" % (self.__class__.__name__, self.base)
1199
def _ensure_real(self):
1200
"""Ensure that there is a _real_branch set.
1202
Used before calls to self._real_branch.
1204
if not self._real_branch:
1205
assert vfs.vfs_enabled()
1206
self.bzrdir._ensure_real()
1207
self._real_branch = self.bzrdir._real_bzrdir.open_branch()
1208
# Give the remote repository the matching real repo.
1209
real_repo = self._real_branch.repository
1210
if isinstance(real_repo, RemoteRepository):
1211
real_repo._ensure_real()
1212
real_repo = real_repo._real_repository
1213
self.repository._set_real_repository(real_repo)
1214
# Give the branch the remote repository to let fast-pathing happen.
1215
self._real_branch.repository = self.repository
1216
# XXX: deal with _lock_mode == 'w'
1217
if self._lock_mode == 'r':
1218
self._real_branch.lock_read()
1221
def control_files(self):
1222
# Defer actually creating RemoteBranchLockableFiles until its needed,
1223
# because it triggers an _ensure_real that we otherwise might not need.
1224
if self._control_files is None:
1225
self._control_files = RemoteBranchLockableFiles(
1226
self.bzrdir, self._client)
1227
return self._control_files
1229
def _get_checkout_format(self):
1231
return self._real_branch._get_checkout_format()
1233
def get_physical_lock_status(self):
1234
"""See Branch.get_physical_lock_status()."""
1235
# should be an API call to the server, as branches must be lockable.
1237
return self._real_branch.get_physical_lock_status()
1239
def lock_read(self):
1240
if not self._lock_mode:
1241
self._lock_mode = 'r'
1242
self._lock_count = 1
1243
if self._real_branch is not None:
1244
self._real_branch.lock_read()
1246
self._lock_count += 1
1248
def _remote_lock_write(self, token):
1250
branch_token = repo_token = ''
1252
branch_token = token
1253
repo_token = self.repository.lock_write()
1254
self.repository.unlock()
1255
path = self.bzrdir._path_for_remote_call(self._client)
1256
response = self._client.call('Branch.lock_write', path, branch_token,
1258
if response[0] == 'ok':
1259
ok, branch_token, repo_token = response
1260
return branch_token, repo_token
1261
elif response[0] == 'LockContention':
1262
raise errors.LockContention('(remote lock)')
1263
elif response[0] == 'TokenMismatch':
1264
raise errors.TokenMismatch(token, '(remote token)')
1265
elif response[0] == 'UnlockableTransport':
1266
raise errors.UnlockableTransport(self.bzrdir.root_transport)
1267
elif response[0] == 'ReadOnlyError':
1268
raise errors.ReadOnlyError(self)
1269
elif response[0] == 'LockFailed':
1270
raise errors.LockFailed(response[1], response[2])
1272
raise errors.UnexpectedSmartServerResponse(response)
1274
def lock_write(self, token=None):
1275
if not self._lock_mode:
1276
remote_tokens = self._remote_lock_write(token)
1277
self._lock_token, self._repo_lock_token = remote_tokens
1278
assert self._lock_token, 'Remote server did not return a token!'
1279
# TODO: We really, really, really don't want to call _ensure_real
1280
# here, but it's the easiest way to ensure coherency between the
1281
# state of the RemoteBranch and RemoteRepository objects and the
1282
# physical locks. If we don't materialise the real objects here,
1283
# then getting everything in the right state later is complex, so
1284
# for now we just do it the lazy way.
1285
# -- Andrew Bennetts, 2007-02-22.
1287
if self._real_branch is not None:
1288
self._real_branch.repository.lock_write(
1289
token=self._repo_lock_token)
1291
self._real_branch.lock_write(token=self._lock_token)
1293
self._real_branch.repository.unlock()
1294
if token is not None:
1295
self._leave_lock = True
1297
# XXX: this case seems to be unreachable; token cannot be None.
1298
self._leave_lock = False
1299
self._lock_mode = 'w'
1300
self._lock_count = 1
1301
elif self._lock_mode == 'r':
1302
raise errors.ReadOnlyTransaction
1304
if token is not None:
1305
# A token was given to lock_write, and we're relocking, so check
1306
# that the given token actually matches the one we already have.
1307
if token != self._lock_token:
1308
raise errors.TokenMismatch(token, self._lock_token)
1309
self._lock_count += 1
1310
return self._lock_token or None
1312
def _unlock(self, branch_token, repo_token):
1313
path = self.bzrdir._path_for_remote_call(self._client)
1314
response = self._client.call('Branch.unlock', path, branch_token,
1316
if response == ('ok',):
1318
elif response[0] == 'TokenMismatch':
1319
raise errors.TokenMismatch(
1320
str((branch_token, repo_token)), '(remote tokens)')
1322
raise errors.UnexpectedSmartServerResponse(response)
1325
self._lock_count -= 1
1326
if not self._lock_count:
1327
self._clear_cached_state()
1328
mode = self._lock_mode
1329
self._lock_mode = None
1330
if self._real_branch is not None:
1331
if (not self._leave_lock and mode == 'w' and
1332
self._repo_lock_token):
1333
# If this RemoteBranch will remove the physical lock for the
1334
# repository, make sure the _real_branch doesn't do it
1335
# first. (Because the _real_branch's repository is set to
1336
# be the RemoteRepository.)
1337
self._real_branch.repository.leave_lock_in_place()
1338
self._real_branch.unlock()
1340
# Only write-locked branched need to make a remote method call
1341
# to perfom the unlock.
1343
assert self._lock_token, 'Locked, but no token!'
1344
branch_token = self._lock_token
1345
repo_token = self._repo_lock_token
1346
self._lock_token = None
1347
self._repo_lock_token = None
1348
if not self._leave_lock:
1349
self._unlock(branch_token, repo_token)
1351
def break_lock(self):
1353
return self._real_branch.break_lock()
1355
def leave_lock_in_place(self):
1356
if not self._lock_token:
1357
raise NotImplementedError(self.leave_lock_in_place)
1358
self._leave_lock = True
1360
def dont_leave_lock_in_place(self):
1361
if not self._lock_token:
1362
raise NotImplementedError(self.dont_leave_lock_in_place)
1363
self._leave_lock = False
1365
def last_revision_info(self):
1366
"""See Branch.last_revision_info()."""
1367
path = self.bzrdir._path_for_remote_call(self._client)
1368
response = self._client.call('Branch.last_revision_info', path)
1369
assert response[0] == 'ok', 'unexpected response code %s' % (response,)
1370
revno = int(response[1])
1371
last_revision = response[2]
1372
return (revno, last_revision)
1374
def _gen_revision_history(self):
1375
"""See Branch._gen_revision_history()."""
1376
path = self.bzrdir._path_for_remote_call(self._client)
1377
response = self._client.call_expecting_body(
1378
'Branch.revision_history', path)
1379
assert response[0][0] == 'ok', ('unexpected response code %s'
1381
result = response[1].read_body_bytes().split('\x00')
1387
def set_revision_history(self, rev_history):
1388
# Send just the tip revision of the history; the server will generate
1389
# the full history from that. If the revision doesn't exist in this
1390
# branch, NoSuchRevision will be raised.
1391
path = self.bzrdir._path_for_remote_call(self._client)
1392
if rev_history == []:
1395
rev_id = rev_history[-1]
1396
self._clear_cached_state()
1397
response = self._client.call('Branch.set_last_revision',
1398
path, self._lock_token, self._repo_lock_token, rev_id)
1399
if response[0] == 'NoSuchRevision':
1400
raise NoSuchRevision(self, rev_id)
1402
assert response == ('ok',), (
1403
'unexpected response code %r' % (response,))
1404
self._cache_revision_history(rev_history)
1406
def get_parent(self):
1408
return self._real_branch.get_parent()
1410
def set_parent(self, url):
1412
return self._real_branch.set_parent(url)
1414
def get_config(self):
1415
return RemoteBranchConfig(self)
1417
def sprout(self, to_bzrdir, revision_id=None):
1418
# Like Branch.sprout, except that it sprouts a branch in the default
1419
# format, because RemoteBranches can't be created at arbitrary URLs.
1420
# XXX: if to_bzrdir is a RemoteBranch, this should perhaps do
1421
# to_bzrdir.create_branch...
1423
result = self._real_branch._format.initialize(to_bzrdir)
1424
self.copy_content_into(result, revision_id=revision_id)
1425
result.set_parent(self.bzrdir.root_transport.base)
1429
def pull(self, source, overwrite=False, stop_revision=None,
1431
# FIXME: This asks the real branch to run the hooks, which means
1432
# they're called with the wrong target branch parameter.
1433
# The test suite specifically allows this at present but it should be
1434
# fixed. It should get a _override_hook_target branch,
1435
# as push does. -- mbp 20070405
1437
self._real_branch.pull(
1438
source, overwrite=overwrite, stop_revision=stop_revision,
1442
def push(self, target, overwrite=False, stop_revision=None):
1444
return self._real_branch.push(
1445
target, overwrite=overwrite, stop_revision=stop_revision,
1446
_override_hook_source_branch=self)
1448
def is_locked(self):
1449
return self._lock_count >= 1
1451
def set_last_revision_info(self, revno, revision_id):
1453
self._clear_cached_state()
1454
return self._real_branch.set_last_revision_info(revno, revision_id)
1456
def generate_revision_history(self, revision_id, last_rev=None,
1459
return self._real_branch.generate_revision_history(
1460
revision_id, last_rev=last_rev, other_branch=other_branch)
1465
return self._real_branch.tags
1467
def set_push_location(self, location):
1469
return self._real_branch.set_push_location(location)
1471
def update_revisions(self, other, stop_revision=None, overwrite=False):
1473
return self._real_branch.update_revisions(
1474
other, stop_revision=stop_revision, overwrite=overwrite)
1477
class RemoteBranchConfig(BranchConfig):
1480
self.branch._ensure_real()
1481
return self.branch._real_branch.get_config().username()
1483
def _get_branch_data_config(self):
1484
self.branch._ensure_real()
1485
if self._branch_data_config is None:
1486
self._branch_data_config = TreeConfig(self.branch._real_branch)
1487
return self._branch_data_config
1490
def _extract_tar(tar, to_dir):
1491
"""Extract all the contents of a tarfile object.
1493
A replacement for extractall, which is not present in python2.4
1496
tar.extract(tarinfo, to_dir)