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.errors import NoSuchRevision
27
from bzrlib.revision import NULL_REVISION
28
from bzrlib.smart import client, vfs
29
from bzrlib.urlutils import unescape
31
# Note: RemoteBzrDirFormat is in bzrdir.py
33
class RemoteBzrDir(BzrDir):
34
"""Control directory on a remote server, accessed by HPSS."""
36
def __init__(self, transport, _client=None):
37
"""Construct a RemoteBzrDir.
39
:param _client: Private parameter for testing. Disables probing and the
42
BzrDir.__init__(self, transport, RemoteBzrDirFormat())
43
if _client is not None:
47
self.client = transport.get_smart_client()
48
# this object holds a delegated bzrdir that uses file-level operations
49
# to talk to the other side
50
# XXX: We should go into find_format, but not allow it to find
51
# RemoteBzrDirFormat and make sure it finds the real underlying format.
53
# THIS IS A COMPLETE AND UTTER LIE.
54
# XXX: XXX: XXX: must be removed before merging to mainline
55
# SMART_SERVER_MERGE_BLOCKER
56
default_format = BzrDirFormat.get_default_format()
57
self._real_bzrdir = default_format.open(transport, _found=True)
58
smartclient = client.SmartClient(self.client)
59
path = self._path_for_remote_call(smartclient)
60
#self._real_bzrdir._format.probe_transport(transport)
61
response = smartclient.call('probe_dont_use', path)
62
if response == ('no',):
63
raise errors.NotBranchError(path=transport.base)
65
def create_repository(self, shared=False):
66
return RemoteRepository(
67
self, self._real_bzrdir.create_repository(shared=shared))
69
def create_branch(self):
70
real_branch = self._real_bzrdir.create_branch()
71
real_repository = real_branch.repository
72
remote_repository = RemoteRepository(self, real_repository)
73
return RemoteBranch(self, remote_repository, real_branch)
75
def create_workingtree(self, revision_id=None):
76
real_workingtree = self._real_bzrdir.create_workingtree(revision_id=revision_id)
77
return RemoteWorkingTree(self, real_workingtree)
79
def open_branch(self, _unsupported=False):
80
assert _unsupported == False, 'unsupported flag support not implemented yet.'
81
smartclient = client.SmartClient(self.client)
82
path = self._path_for_remote_call(smartclient)
83
response = smartclient.call('BzrDir.open_branch', path)
84
assert response[0] == 'ok', 'unexpected response code %s' % (response,)
85
if response[0] != 'ok':
86
# this should probably be a regular translate no ?
87
raise errors.NotBranchError(path=self.root_transport.base)
89
# branch at this location.
91
# if the VFS is enabled, create a local object using the VFS.
92
real_branch = self._real_bzrdir.open_branch(unsupported=_unsupported)
93
# This branch accessed through the smart server, so wrap the
95
real_repository = real_branch.repository
96
remote_repository = self.find_repository()
97
remote_repository._real_repository = real_repository
98
return RemoteBranch(self, remote_repository, real_branch)
100
# otherwise just create a proxy for the branch.
101
return RemoteBranch(self, self.find_repository())
103
# a branch reference, use the existing BranchReference logic.
104
format = BranchReferenceFormat()
105
return format.open(self, _found=True, location=response[1])
107
def open_repository(self):
108
smartclient = client.SmartClient(self.client)
109
path = self._path_for_remote_call(smartclient)
110
response = smartclient.call('BzrDir.find_repository', path)
111
assert response[0] in ('ok', 'norepository'), \
112
'unexpected response code %s' % (response,)
113
if response[0] == 'norepository':
114
raise errors.NoRepositoryPresent(self)
115
if response[1] == '':
116
if vfs.vfs_enabled():
117
return RemoteRepository(self, self._real_bzrdir.open_repository())
119
return RemoteRepository(self)
121
raise errors.NoRepositoryPresent(self)
123
def open_workingtree(self):
124
return RemoteWorkingTree(self, self._real_bzrdir.open_workingtree())
126
def _path_for_remote_call(self, client):
127
"""Return the path to be used for this bzrdir in a remote call."""
128
return client.remote_path_from_transport(self.root_transport)
130
def get_branch_transport(self, branch_format):
131
return self._real_bzrdir.get_branch_transport(branch_format)
133
def get_repository_transport(self, repository_format):
134
return self._real_bzrdir.get_repository_transport(repository_format)
136
def get_workingtree_transport(self, workingtree_format):
137
return self._real_bzrdir.get_workingtree_transport(workingtree_format)
139
def can_convert_format(self):
140
"""Upgrading of remote bzrdirs is not supported yet."""
143
def needs_format_conversion(self, format=None):
144
"""Upgrading of remote bzrdirs is not supported yet."""
148
class RemoteRepositoryFormat(repository.RepositoryFormat):
149
"""Format for repositories accessed over rpc.
151
Instances of this repository are represented by RemoteRepository
155
_matchingbzrdir = RemoteBzrDirFormat
157
def initialize(self, a_bzrdir, shared=False):
158
assert isinstance(a_bzrdir, RemoteBzrDir)
159
return a_bzrdir.create_repository(shared=shared)
161
def open(self, a_bzrdir):
162
assert isinstance(a_bzrdir, RemoteBzrDir)
163
return a_bzrdir.open_repository()
165
def get_format_description(self):
166
return 'bzr remote repository'
168
def __eq__(self, other):
169
return self.__class__ == other.__class__
171
rich_root_data = False
174
class RemoteRepository(object):
175
"""Repository accessed over rpc.
177
For the moment everything is delegated to IO-like operations over
181
def __init__(self, remote_bzrdir, real_repository=None, _client=None):
182
"""Create a RemoteRepository instance.
184
:param remote_bzrdir: The bzrdir hosting this repository.
185
:param real_repository: If not None, a local implementation of the
186
repository logic for the repository, usually accessing the data
188
:param _client: Private testing parameter - override the smart client
189
to be used by the repository.
192
self._real_repository = real_repository
193
self.bzrdir = remote_bzrdir
195
self._client = client.SmartClient(self.bzrdir.client)
197
self._client = _client
198
self._format = RemoteRepositoryFormat()
200
def get_revision_graph(self, revision_id=None):
201
"""See Repository.get_revision_graph()."""
202
if revision_id is None:
204
elif revision_id == NULL_REVISION:
207
path = self.bzrdir._path_for_remote_call(self._client)
208
response = self._client.call2('Repository.get_revision_graph', path, revision_id.encode('utf8'))
209
assert response[0][0] in ('ok', 'nosuchrevision'), 'unexpected response code %s' % (response[0],)
210
if response[0][0] == 'ok':
211
coded = response[1].read_body_bytes()
212
lines = coded.decode('utf8').split('\n')
216
d = list(line.split())
217
revision_graph[d[0]] = d[1:]
219
return revision_graph
221
assert response[1] != ''
222
raise NoSuchRevision(self, revision_id)
224
def has_revision(self, revision_id):
225
"""See Repository.has_revision()."""
226
path = self.bzrdir._path_for_remote_call(self._client)
227
response = self._client.call('Repository.has_revision', path, revision_id.encode('utf8'))
228
assert response[0] in ('ok', 'no'), 'unexpected response code %s' % (response,)
229
return response[0] == 'ok'
231
def gather_stats(self, revid, committers=None):
232
"""See Repository.gather_stats()."""
233
path = self.bzrdir._path_for_remote_call(self._client)
234
if revid in (None, NULL_REVISION):
237
fmt_revid = revid.encode('utf8')
238
if committers is None:
239
fmt_committers = 'no'
241
fmt_committers = 'yes'
242
response = self._client.call2('Repository.gather_stats', path,
243
fmt_revid, fmt_committers)
244
assert response[0][0] == 'ok', \
245
'unexpected response code %s' % (response[0],)
247
body = response[1].read_body_bytes()
249
for line in body.split('\n'):
252
key, val_text = line.split(':')
253
if key in ('revisions', 'size', 'committers'):
254
result[key] = int(val_text)
255
elif key in ('firstrev', 'latestrev'):
256
values = val_text.split(' ')[1:]
257
result[key] = (float(values[0]), long(values[1]))
261
def get_physical_lock_status(self):
262
"""See Repository.get_physical_lock_status()."""
266
"""See Repository.is_shared()."""
267
path = self.bzrdir._path_for_remote_call(self._client)
268
response = self._client.call('Repository.is_shared', path)
269
assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
270
return response[0] == 'yes'
273
# wrong eventually - want a local lock cache context
274
return self._real_repository.lock_read()
276
def lock_write(self):
277
# definately wrong: want to check if there is a real repo
278
# and not thunk through if not
279
return self._real_repository.lock_write()
282
# should free cache context.
283
return self._real_repository.unlock()
285
def break_lock(self):
286
# should hand off to the network - or better yet, we should not
287
# allow stale network locks ?
288
return self._real_repository.break_lock()
291
class RemoteBranchLockableFiles(object):
292
"""A 'LockableFiles' implementation that talks to a smart server.
294
This is not a public interface class.
297
def __init__(self, bzrdir, _client):
299
self._client = _client
302
"""'get' a remote path as per the LockableFiles interface.
304
:param path: the file to 'get'. If this is 'branch.conf', we do not
305
just retrieve a file, instead we ask the smart server to generate
306
a configuration for us - which is retrieved as an INI file.
308
assert path == 'branch.conf'
309
path = self.bzrdir._path_for_remote_call(self._client)
310
response = self._client.call2('Branch.get_config_file', path)
311
assert response[0][0] == 'ok', \
312
'unexpected response code %s' % (response[0],)
313
return StringIO(response[1].read_body_bytes())
316
class RemoteBranchFormat(branch.BranchFormat):
318
def get_format_description(self):
319
return 'Remote BZR Branch'
321
def open(self, a_bzrdir):
322
assert isinstance(a_bzrdir, RemoteBzrDir)
323
return a_bzrdir.open_branch()
325
def initialize(self, a_bzrdir):
326
assert isinstance(a_bzrdir, RemoteBzrDir)
327
return a_bzrdir.create_branch()
330
class RemoteBranch(branch.Branch):
331
"""Branch stored on a server accessed by HPSS RPC.
333
At the moment most operations are mapped down to simple file operations.
336
def __init__(self, remote_bzrdir, remote_repository, real_branch=None,
338
"""Create a RemoteBranch instance.
340
:param real_branch: An optional local implementation of the branch
341
format, usually accessing the data via the VFS.
342
:param _client: Private parameter for testing.
344
self.bzrdir = remote_bzrdir
345
if _client is not None:
346
self._client = _client
348
self._client = client.SmartClient(self.bzrdir.client)
349
self.repository = remote_repository
350
if real_branch is not None:
351
self._real_branch = real_branch
352
# Fill out expected attributes of branch for bzrlib api users.
353
self._format = RemoteBranchFormat()
354
self.base = self.bzrdir.root_transport.base
355
self.control_files = RemoteBranchLockableFiles(self.bzrdir, self._client)
357
def get_physical_lock_status(self):
358
"""See Branch.get_physical_lock_status()."""
359
# should be an API call to the server, as branches must be lockable.
360
return self._real_branch.get_physical_lock_status()
363
return self._real_branch.lock_read()
365
def lock_write(self):
366
return self._real_branch.lock_write()
369
return self._real_branch.unlock()
371
def break_lock(self):
372
return self._real_branch.break_lock()
374
def last_revision_info(self):
375
"""See Branch.last_revision_info()."""
376
path = self.bzrdir._path_for_remote_call(self._client)
377
response = self._client.call('Branch.last_revision_info', path)
378
assert response[0] == 'ok', 'unexpected response code %s' % (response,)
379
revno = int(response[1])
380
last_revision = response[2].decode('utf8')
381
if last_revision == '':
382
last_revision = NULL_REVISION
383
return (revno, last_revision)
385
def revision_history(self):
386
"""See Branch.revision_history()."""
387
# XXX: TODO: this does not cache the revision history for the duration
388
# of a lock, which is a bug - see the code for regular branches
390
path = self.bzrdir._path_for_remote_call(self._client)
391
response = self._client.call2('Branch.revision_history', path)
392
assert response[0][0] == 'ok', 'unexpected response code %s' % (response[0],)
393
result = response[1].read_body_bytes().decode('utf8').split('\x00')
398
def set_revision_history(self, rev_history):
399
return self._real_branch.set_revision_history(rev_history)
401
def get_parent(self):
402
return self._real_branch.get_parent()
404
def set_parent(self, url):
405
return self._real_branch.set_parent(url)
408
class RemoteWorkingTree(object):
410
def __init__(self, remote_bzrdir, real_workingtree):
411
self.real_workingtree = real_workingtree
412
self.bzrdir = remote_bzrdir
414
def __getattr__(self, name):
415
# XXX: temporary way to lazily delegate everything to the real
417
return getattr(self.real_workingtree, name)