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
21
from urlparse import urlparse
23
from bzrlib import branch, errors, repository
24
from bzrlib.branch import BranchReferenceFormat
25
from bzrlib.bzrdir import BzrDir, BzrDirFormat, RemoteBzrDirFormat
26
from bzrlib.config import BranchConfig
27
from bzrlib.decorators import needs_read_lock, needs_write_lock
28
from bzrlib.errors import NoSuchRevision
29
from bzrlib.revision import NULL_REVISION
30
from bzrlib.smart import client, vfs
31
from bzrlib.urlutils import unescape
33
# Note: RemoteBzrDirFormat is in bzrdir.py
35
class RemoteBzrDir(BzrDir):
36
"""Control directory on a remote server, accessed by HPSS."""
38
def __init__(self, transport, _client=None):
39
"""Construct a RemoteBzrDir.
41
:param _client: Private parameter for testing. Disables probing and the
44
BzrDir.__init__(self, transport, RemoteBzrDirFormat())
45
# this object holds a delegated bzrdir that uses file-level operations
46
# to talk to the other side
47
# XXX: We should go into find_format, but not allow it to find
48
# RemoteBzrDirFormat and make sure it finds the real underlying format.
49
self._real_bzrdir = None
52
self._medium = transport.get_smart_client()
53
self._client = client.SmartClient(self._medium)
55
self._client = _client
60
path = self._path_for_remote_call(self._client)
61
#self._real_bzrdir._format.probe_transport(transport)
62
response = self._client.call('probe_dont_use', path)
63
if response == ('no',):
64
raise errors.NotBranchError(path=transport.base)
66
def _ensure_real(self):
67
"""Ensure that there is a _real_bzrdir set.
69
used before calls to self._real_bzrdir.
71
if not self._real_bzrdir:
72
default_format = BzrDirFormat.get_default_format()
73
self._real_bzrdir = default_format.open(self.root_transport,
76
def create_repository(self, shared=False):
77
return RemoteRepository(
78
self, self._real_bzrdir.create_repository(shared=shared))
80
def create_branch(self):
81
real_branch = self._real_bzrdir.create_branch()
82
return RemoteBranch(self, self.find_repository(), real_branch)
84
def create_workingtree(self, revision_id=None):
85
real_workingtree = self._real_bzrdir.create_workingtree(revision_id=revision_id)
86
return RemoteWorkingTree(self, real_workingtree)
88
def open_branch(self, _unsupported=False):
89
assert _unsupported == False, 'unsupported flag support not implemented yet.'
90
path = self._path_for_remote_call(self._client)
91
response = self._client.call('BzrDir.open_branch', path)
92
if response[0] == 'ok':
94
# branch at this location.
95
return RemoteBranch(self, self.find_repository())
97
# a branch reference, use the existing BranchReference logic.
98
format = BranchReferenceFormat()
99
return format.open(self, _found=True, location=response[1])
100
elif response == ('nobranch',):
101
raise errors.NotBranchError(path=self.root_transport.base)
103
assert False, 'unexpected response code %r' % (response,)
105
def open_repository(self):
106
path = self._path_for_remote_call(self._client)
107
response = self._client.call('BzrDir.find_repository', path)
108
assert response[0] in ('ok', 'norepository'), \
109
'unexpected response code %s' % (response,)
110
if response[0] == 'norepository':
111
raise errors.NoRepositoryPresent(self)
112
if response[1] == '':
113
return RemoteRepository(self)
115
raise errors.NoRepositoryPresent(self)
117
def open_workingtree(self):
118
return RemoteWorkingTree(self, self._real_bzrdir.open_workingtree())
120
def _path_for_remote_call(self, client):
121
"""Return the path to be used for this bzrdir in a remote call."""
122
return client.remote_path_from_transport(self.root_transport)
124
def get_branch_transport(self, branch_format):
125
return self._real_bzrdir.get_branch_transport(branch_format)
127
def get_repository_transport(self, repository_format):
128
return self._real_bzrdir.get_repository_transport(repository_format)
130
def get_workingtree_transport(self, workingtree_format):
131
return self._real_bzrdir.get_workingtree_transport(workingtree_format)
133
def can_convert_format(self):
134
"""Upgrading of remote bzrdirs is not supported yet."""
137
def needs_format_conversion(self, format=None):
138
"""Upgrading of remote bzrdirs is not supported yet."""
142
class RemoteRepositoryFormat(repository.RepositoryFormat):
143
"""Format for repositories accessed over rpc.
145
Instances of this repository are represented by RemoteRepository
149
_matchingbzrdir = RemoteBzrDirFormat
151
def initialize(self, a_bzrdir, shared=False):
152
assert isinstance(a_bzrdir, RemoteBzrDir)
153
return a_bzrdir.create_repository(shared=shared)
155
def open(self, a_bzrdir):
156
assert isinstance(a_bzrdir, RemoteBzrDir)
157
return a_bzrdir.open_repository()
159
def get_format_description(self):
160
return 'bzr remote repository'
162
def __eq__(self, other):
163
return self.__class__ == other.__class__
165
rich_root_data = False
168
class RemoteRepository(object):
169
"""Repository accessed over rpc.
171
For the moment everything is delegated to IO-like operations over
175
def __init__(self, remote_bzrdir, real_repository=None, _client=None):
176
"""Create a RemoteRepository instance.
178
:param remote_bzrdir: The bzrdir hosting this repository.
179
:param real_repository: If not None, a local implementation of the
180
repository logic for the repository, usually accessing the data
182
:param _client: Private testing parameter - override the smart client
183
to be used by the repository.
186
self._real_repository = real_repository
188
self._real_repository = None
189
self.bzrdir = remote_bzrdir
191
self._client = client.SmartClient(self.bzrdir._medium)
193
self._client = _client
194
self._format = RemoteRepositoryFormat()
195
self._lock_mode = None
196
self._lock_token = None
198
self._leave_lock = False
200
def _ensure_real(self):
201
"""Ensure that there is a _real_repository set.
203
used before calls to self._real_repository.
205
if not self._real_repository:
206
self.bzrdir._ensure_real()
207
#self._real_repository = self.bzrdir._real_bzrdir.open_repository()
208
self._set_real_repository(self.bzrdir._real_bzrdir.open_repository())
210
def get_revision_graph(self, revision_id=None):
211
"""See Repository.get_revision_graph()."""
212
if revision_id is None:
214
elif revision_id == NULL_REVISION:
217
path = self.bzrdir._path_for_remote_call(self._client)
218
response = self._client.call2('Repository.get_revision_graph', path, revision_id.encode('utf8'))
219
assert response[0][0] in ('ok', 'nosuchrevision'), 'unexpected response code %s' % (response[0],)
220
if response[0][0] == 'ok':
221
coded = response[1].read_body_bytes()
222
lines = coded.decode('utf8').split('\n')
226
d = list(line.split())
227
revision_graph[d[0]] = d[1:]
229
return revision_graph
231
response_body = response[1].read_body_bytes()
232
assert response_body == ''
233
raise NoSuchRevision(self, revision_id)
235
def has_revision(self, revision_id):
236
"""See Repository.has_revision()."""
237
if revision_id is None:
238
# The null revision is always present.
240
path = self.bzrdir._path_for_remote_call(self._client)
241
response = self._client.call('Repository.has_revision', path, revision_id.encode('utf8'))
242
assert response[0] in ('ok', 'no'), 'unexpected response code %s' % (response,)
243
return response[0] == 'ok'
245
def gather_stats(self, revid=None, committers=None):
246
"""See Repository.gather_stats()."""
247
path = self.bzrdir._path_for_remote_call(self._client)
248
if revid in (None, NULL_REVISION):
251
fmt_revid = revid.encode('utf8')
252
if committers is None or not committers:
253
fmt_committers = 'no'
255
fmt_committers = 'yes'
256
response = self._client.call2('Repository.gather_stats', path,
257
fmt_revid, fmt_committers)
258
assert response[0][0] == 'ok', \
259
'unexpected response code %s' % (response[0],)
261
body = response[1].read_body_bytes()
263
for line in body.split('\n'):
266
key, val_text = line.split(':')
267
if key in ('revisions', 'size', 'committers'):
268
result[key] = int(val_text)
269
elif key in ('firstrev', 'latestrev'):
270
values = val_text.split(' ')[1:]
271
result[key] = (float(values[0]), long(values[1]))
275
def get_physical_lock_status(self):
276
"""See Repository.get_physical_lock_status()."""
280
"""See Repository.is_shared()."""
281
path = self.bzrdir._path_for_remote_call(self._client)
282
response = self._client.call('Repository.is_shared', path)
283
assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
284
return response[0] == 'yes'
287
# wrong eventually - want a local lock cache context
288
if not self._lock_mode:
289
self._lock_mode = 'r'
291
if self._real_repository is not None:
292
self._real_repository.lock_read()
294
self._lock_count += 1
296
def _remote_lock_write(self, token):
297
path = self.bzrdir._path_for_remote_call(self._client)
300
response = self._client.call('Repository.lock_write', path, token)
301
if response[0] == 'ok':
304
elif response[0] == 'LockContention':
305
raise errors.LockContention('(remote lock)')
307
assert False, 'unexpected response code %s' % (response,)
309
def lock_write(self, token=None):
310
if not self._lock_mode:
311
self._lock_token = self._remote_lock_write(token)
312
assert self._lock_token, 'Remote server did not return a token!'
313
if self._real_repository is not None:
314
self._real_repository.lock_write(token=self._lock_token)
315
if token is not None:
316
self._leave_lock = True
318
self._leave_lock = False
319
self._lock_mode = 'w'
321
elif self._lock_mode == 'r':
322
raise errors.ReadOnlyError(self)
324
self._lock_count += 1
325
return self._lock_token
327
def leave_lock_in_place(self):
328
self._leave_lock = True
330
def dont_leave_lock_in_place(self):
331
self._leave_lock = False
333
def _set_real_repository(self, repository):
334
"""Set the _real_repository for this repository.
336
:param repository: The repository to fallback to for non-hpss
337
implemented operations.
339
self._real_repository = repository
340
if self._lock_mode == 'w':
341
# if we are already locked, the real repository must be able to
342
# acquire the lock with our token.
343
self._real_repository.lock_write(self._lock_token)
344
elif self._lock_mode == 'r':
345
self._real_repository.lock_read()
347
def _unlock(self, token):
348
path = self.bzrdir._path_for_remote_call(self._client)
349
response = self._client.call('Repository.unlock', path, token)
350
if response == ('ok',):
352
elif response[0] == 'TokenMismatch':
353
raise errors.TokenMismatch(token, '(remote token)')
355
assert False, 'unexpected response code %s' % (response,)
358
self._lock_count -= 1
359
if not self._lock_count:
360
mode = self._lock_mode
361
self._lock_mode = None
362
if self._real_repository is not None:
363
self._real_repository.unlock()
366
assert self._lock_token, 'Locked, but no token!'
367
token = self._lock_token
368
self._lock_token = None
369
if not self._leave_lock:
372
def break_lock(self):
373
# should hand off to the network
375
return self._real_repository.break_lock()
377
### These methods are just thin shims to the VFS object for now.
379
def revision_tree(self, revision_id):
381
return self._real_repository.revision_tree(revision_id)
383
def get_commit_builder(self, branch, parents, config, timestamp=None,
384
timezone=None, committer=None, revprops=None,
386
# FIXME: It ought to be possible to call this without immediately
387
# triggering _ensure_real. For now it's the easiest thing to do.
389
builder = self._real_repository.get_commit_builder(branch, parents,
390
config, timestamp=timestamp, timezone=timezone,
391
committer=committer, revprops=revprops, revision_id=revision_id)
392
# Make the builder use this RemoteRepository rather than the real one.
393
builder.repository = self
397
def add_inventory(self, revid, inv, parents):
399
return self._real_repository.add_inventory(revid, inv, parents)
402
def add_revision(self, rev_id, rev, inv=None, config=None):
404
return self._real_repository.add_revision(
405
rev_id, rev, inv=inv, config=config)
408
def get_inventory(self, revision_id):
410
return self._real_repository.get_inventory(revision_id)
413
def get_revision(self, revision_id):
415
return self._real_repository.get_revision(revision_id)
418
def weave_store(self):
420
return self._real_repository.weave_store
422
def get_transaction(self):
424
return self._real_repository.get_transaction()
427
def clone(self, a_bzrdir, revision_id=None, basis=None):
429
return self._real_repository.clone(
430
a_bzrdir, revision_id=revision_id, basis=basis)
432
def make_working_trees(self):
435
def fetch(self, source, revision_id=None, pb=None):
437
return self._real_repository.fetch(
438
source, revision_id=revision_id, pb=pb)
441
def control_weaves(self):
443
return self._real_repository.control_weaves
446
def get_ancestry(self, revision_id):
448
return self._real_repository.get_ancestry(revision_id)
451
def get_inventory_weave(self):
453
return self._real_repository.get_inventory_weave()
455
def fileids_altered_by_revision_ids(self, revision_ids):
457
return self._real_repository.fileids_altered_by_revision_ids(revision_ids)
460
def get_signature_text(self, revision_id):
462
return self._real_repository.get_signature_text(revision_id)
465
def get_revision_graph_with_ghosts(self, revision_ids=None):
467
return self._real_repository.get_revision_graph_with_ghosts(
468
revision_ids=revision_ids)
471
def get_inventory_xml(self, revision_id):
473
return self._real_repository.get_inventory_xml(revision_id)
475
def deserialise_inventory(self, revision_id, xml):
477
return self._real_repository.deserialise_inventory(revision_id, xml)
479
def reconcile(self, other=None, thorough=False):
481
return self._real_repository.reconcile(other=other, thorough=thorough)
483
def all_revision_ids(self):
485
return self._real_repository.all_revision_ids()
488
def get_deltas_for_revisions(self, revisions):
490
return self._real_repository.get_deltas_for_revisions(revisions)
493
def get_revision_delta(self, revision_id):
495
return self._real_repository.get_revision_delta(revision_id)
498
def revision_trees(self, revision_ids):
500
return self._real_repository.revision_trees(revision_ids)
503
def get_revision_reconcile(self, revision_id):
505
return self._real_repository.get_revision_reconcile(revision_id)
508
def check(self, revision_ids):
510
return self._real_repository.check(revision_ids)
512
def copy_content_into(self, destination, revision_id=None, basis=None):
514
return self._real_repository.copy_content_into(
515
destination, revision_id=revision_id, basis=basis)
517
def set_make_working_trees(self, new_value):
518
raise NotImplementedError(self.set_make_working_trees)
521
def sign_revision(self, revision_id, gpg_strategy):
523
return self._real_repository.sign_revision(revision_id, gpg_strategy)
526
def get_revisions(self, revision_ids):
528
return self._real_repository.get_revisions(revision_ids)
530
def supports_rich_root(self):
531
# Perhaps this should return True depending on the real repository, but
532
# for now we just take the easy option and assume we can't handle rich
537
class RemoteBranchLockableFiles(object):
538
"""A 'LockableFiles' implementation that talks to a smart server.
540
This is not a public interface class.
543
def __init__(self, bzrdir, _client):
545
self._client = _client
548
"""'get' a remote path as per the LockableFiles interface.
550
:param path: the file to 'get'. If this is 'branch.conf', we do not
551
just retrieve a file, instead we ask the smart server to generate
552
a configuration for us - which is retrieved as an INI file.
554
assert path == 'branch.conf'
555
path = self.bzrdir._path_for_remote_call(self._client)
556
response = self._client.call2('Branch.get_config_file', path)
557
assert response[0][0] == 'ok', \
558
'unexpected response code %s' % (response[0],)
559
return StringIO(response[1].read_body_bytes())
562
class RemoteBranchFormat(branch.BranchFormat):
564
def get_format_description(self):
565
return 'Remote BZR Branch'
567
def get_format_string(self):
568
return 'Remote BZR Branch'
570
def open(self, a_bzrdir):
571
assert isinstance(a_bzrdir, RemoteBzrDir)
572
return a_bzrdir.open_branch()
574
def initialize(self, a_bzrdir):
575
assert isinstance(a_bzrdir, RemoteBzrDir)
576
return a_bzrdir.create_branch()
579
class RemoteBranch(branch.Branch):
580
"""Branch stored on a server accessed by HPSS RPC.
582
At the moment most operations are mapped down to simple file operations.
585
def __init__(self, remote_bzrdir, remote_repository, real_branch=None,
587
"""Create a RemoteBranch instance.
589
:param real_branch: An optional local implementation of the branch
590
format, usually accessing the data via the VFS.
591
:param _client: Private parameter for testing.
593
self.bzrdir = remote_bzrdir
594
if _client is not None:
595
self._client = _client
597
self._client = client.SmartClient(self.bzrdir._medium)
598
self.repository = remote_repository
599
if real_branch is not None:
600
self._real_branch = real_branch
601
# Give the remote repository the matching real repo.
602
self.repository._set_real_repository(self._real_branch.repository)
603
# Give the branch the remote repository to let fast-pathing happen.
604
self._real_branch.repository = self.repository
606
self._real_branch = None
607
# Fill out expected attributes of branch for bzrlib api users.
608
self._format = RemoteBranchFormat()
609
self.base = self.bzrdir.root_transport.base
610
self.control_files = RemoteBranchLockableFiles(self.bzrdir, self._client)
611
self._lock_mode = None
612
self._lock_token = None
614
self._leave_lock = False
616
def _ensure_real(self):
617
"""Ensure that there is a _real_branch set.
619
used before calls to self._real_branch.
621
if not self._real_branch:
622
assert vfs.vfs_enabled()
623
self.bzrdir._ensure_real()
624
self._real_branch = self.bzrdir._real_bzrdir.open_branch()
625
# Give the remote repository the matching real repo.
626
self.repository._set_real_repository(self._real_branch.repository)
627
# Give the branch the remote repository to let fast-pathing happen.
628
self._real_branch.repository = self.repository
629
# XXX: deal with _lock_mode == 'w'
630
if self._lock_mode == 'r':
631
self._real_branch.lock_read()
633
def get_physical_lock_status(self):
634
"""See Branch.get_physical_lock_status()."""
635
# should be an API call to the server, as branches must be lockable.
637
return self._real_branch.get_physical_lock_status()
640
if not self._lock_mode:
641
self._lock_mode = 'r'
643
if self._real_branch is not None:
644
self._real_branch.lock_read()
646
self._lock_count += 1
648
def _remote_lock_write(self, tokens):
650
branch_token = repo_token = ''
652
branch_token, repo_token = tokens
653
path = self.bzrdir._path_for_remote_call(self._client)
654
response = self._client.call('Branch.lock_write', path, branch_token,
656
if response[0] == 'ok':
657
ok, branch_token, repo_token = response
658
return branch_token, repo_token
659
elif response[0] == 'LockContention':
660
raise errors.LockContention('(remote lock)')
661
elif response[0] == 'TokenMismatch':
662
raise errors.TokenMismatch(tokens, '(remote tokens)')
664
assert False, 'unexpected response code %r' % (response,)
666
def lock_write(self, tokens=None):
667
if not self._lock_mode:
668
remote_tokens = self._remote_lock_write(tokens)
669
self._lock_token, self._repo_lock_token = remote_tokens
670
assert self._lock_token, 'Remote server did not return a token!'
671
# TODO: We really, really, really don't want to call _ensure_real
672
# here, but it's the easiest way to ensure coherency between the
673
# state of the RemoteBranch and RemoteRepository objects and the
674
# physical locks. If we don't materialise the real objects here,
675
# then getting everything in the right state later is complex, so
676
# for now we just do it the lazy way.
677
# -- Andrew Bennetts, 2007-02-22.
679
if self._real_branch is not None:
680
self._real_branch.lock_write(tokens=remote_tokens)
681
if tokens is not None:
682
self._leave_lock = True
684
# XXX: this case seems to be unreachable; tokens cannot be None.
685
self._leave_lock = False
686
self._lock_mode = 'w'
688
elif self._lock_mode == 'r':
689
raise errors.ReadOnlyTransaction
691
if tokens is not None:
692
# Tokens were given to lock_write, and we're relocking, so check
693
# that the given tokens actually match the ones we already have.
694
held_tokens = (self._lock_token, self._repo_lock_token)
695
if tokens != held_tokens:
696
raise errors.TokenMismatch(str(tokens), str(held_tokens))
697
self._lock_count += 1
698
return self._lock_token, self._repo_lock_token
700
def _unlock(self, branch_token, repo_token):
701
path = self.bzrdir._path_for_remote_call(self._client)
702
response = self._client.call('Branch.unlock', path, branch_token,
704
if response == ('ok',):
706
elif response[0] == 'TokenMismatch':
707
raise errors.TokenMismatch(
708
str((branch_token, repo_token)), '(remote tokens)')
710
assert False, 'unexpected response code %s' % (response,)
713
self._lock_count -= 1
714
if not self._lock_count:
715
mode = self._lock_mode
716
self._lock_mode = None
717
if self._real_branch is not None:
718
self._real_branch.unlock()
721
assert self._lock_token, 'Locked, but no token!'
722
branch_token = self._lock_token
723
repo_token = self._repo_lock_token
724
self._lock_token = None
725
self._repo_lock_token = None
726
if not self._leave_lock:
727
self._unlock(branch_token, repo_token)
729
def break_lock(self):
731
return self._real_branch.break_lock()
733
def leave_lock_in_place(self):
734
self._leave_lock = True
736
def dont_leave_lock_in_place(self):
737
self._leave_lock = False
739
def last_revision_info(self):
740
"""See Branch.last_revision_info()."""
741
path = self.bzrdir._path_for_remote_call(self._client)
742
response = self._client.call('Branch.last_revision_info', path)
743
assert response[0] == 'ok', 'unexpected response code %s' % (response,)
744
revno = int(response[1])
745
last_revision = response[2].decode('utf8')
746
if last_revision == '':
747
last_revision = NULL_REVISION
748
return (revno, last_revision)
750
def revision_history(self):
751
"""See Branch.revision_history()."""
752
# XXX: TODO: this does not cache the revision history for the duration
753
# of a lock, which is a bug - see the code for regular branches
755
path = self.bzrdir._path_for_remote_call(self._client)
756
response = self._client.call2('Branch.revision_history', path)
757
assert response[0][0] == 'ok', 'unexpected response code %s' % (response[0],)
758
result = response[1].read_body_bytes().decode('utf8').split('\x00')
764
def set_revision_history(self, rev_history):
765
# Send just the tip revision of the history; the server will generate
766
# the full history from that. If the revision doesn't exist in this
767
# branch, NoSuchRevision will be raised.
768
path = self.bzrdir._path_for_remote_call(self._client)
769
if rev_history == []:
772
rev_id = rev_history[-1]
773
response = self._client.call('Branch.set_last_revision',
774
path, self._lock_token, self._repo_lock_token, rev_id)
775
if response[0] == 'NoSuchRevision':
776
raise NoSuchRevision(self, rev_id)
778
assert response == ('ok',), (
779
'unexpected response code %r' % (response,))
781
def get_parent(self):
783
return self._real_branch.get_parent()
785
def set_parent(self, url):
787
return self._real_branch.set_parent(url)
789
def get_config(self):
790
return RemoteBranchConfig(self)
793
def append_revision(self, *revision_ids):
795
return self._real_branch.append_revision(*revision_ids)
798
def pull(self, source, overwrite=False, stop_revision=None):
800
self._real_branch.pull(
801
source, overwrite=overwrite, stop_revision=stop_revision)
804
class RemoteWorkingTree(object):
806
def __init__(self, remote_bzrdir, real_workingtree):
807
self.real_workingtree = real_workingtree
808
self.bzrdir = remote_bzrdir
810
def __getattr__(self, name):
811
# XXX: temporary way to lazily delegate everything to the real
813
return getattr(self.real_workingtree, name)
816
class RemoteBranchConfig(BranchConfig):
819
self.branch._ensure_real()
820
return self.branch._real_branch.get_config().username()