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.
52
self._real_bzrdir = None
55
smartclient = client.SmartClient(self.client)
56
path = self._path_for_remote_call(smartclient)
57
#self._real_bzrdir._format.probe_transport(transport)
58
response = smartclient.call('probe_dont_use', path)
59
if response == ('no',):
60
raise errors.NotBranchError(path=transport.base)
62
def _ensure_real(self):
63
"""Ensure that there is a _real_bzrdir set.
65
used before calls to self._real_bzrdir.
67
if not self._real_bzrdir:
68
default_format = BzrDirFormat.get_default_format()
69
self._real_bzrdir = default_format.open(self.root_transport,
72
def create_repository(self, shared=False):
73
return RemoteRepository(
74
self, self._real_bzrdir.create_repository(shared=shared))
76
def create_branch(self):
77
real_branch = self._real_bzrdir.create_branch()
78
return RemoteBranch(self, self.find_repository(), real_branch)
80
def create_workingtree(self, revision_id=None):
81
real_workingtree = self._real_bzrdir.create_workingtree(revision_id=revision_id)
82
return RemoteWorkingTree(self, real_workingtree)
84
def open_branch(self, _unsupported=False):
85
assert _unsupported == False, 'unsupported flag support not implemented yet.'
86
smartclient = client.SmartClient(self.client)
87
path = self._path_for_remote_call(smartclient)
88
response = smartclient.call('BzrDir.open_branch', path)
89
assert response[0] == 'ok', 'unexpected response code %s' % (response,)
90
if response[0] != 'ok':
91
# this should probably be a regular translate no ?
92
raise errors.NotBranchError(path=self.root_transport.base)
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])
101
def open_repository(self):
102
smartclient = client.SmartClient(self.client)
103
path = self._path_for_remote_call(smartclient)
104
response = smartclient.call('BzrDir.find_repository', path)
105
assert response[0] in ('ok', 'norepository'), \
106
'unexpected response code %s' % (response,)
107
if response[0] == 'norepository':
108
raise errors.NoRepositoryPresent(self)
109
if response[1] == '':
110
return RemoteRepository(self)
112
raise errors.NoRepositoryPresent(self)
114
def open_workingtree(self):
115
return RemoteWorkingTree(self, self._real_bzrdir.open_workingtree())
117
def _path_for_remote_call(self, client):
118
"""Return the path to be used for this bzrdir in a remote call."""
119
return client.remote_path_from_transport(self.root_transport)
121
def get_branch_transport(self, branch_format):
122
return self._real_bzrdir.get_branch_transport(branch_format)
124
def get_repository_transport(self, repository_format):
125
return self._real_bzrdir.get_repository_transport(repository_format)
127
def get_workingtree_transport(self, workingtree_format):
128
return self._real_bzrdir.get_workingtree_transport(workingtree_format)
130
def can_convert_format(self):
131
"""Upgrading of remote bzrdirs is not supported yet."""
134
def needs_format_conversion(self, format=None):
135
"""Upgrading of remote bzrdirs is not supported yet."""
139
class RemoteRepositoryFormat(repository.RepositoryFormat):
140
"""Format for repositories accessed over rpc.
142
Instances of this repository are represented by RemoteRepository
146
_matchingbzrdir = RemoteBzrDirFormat
148
def initialize(self, a_bzrdir, shared=False):
149
assert isinstance(a_bzrdir, RemoteBzrDir)
150
return a_bzrdir.create_repository(shared=shared)
152
def open(self, a_bzrdir):
153
assert isinstance(a_bzrdir, RemoteBzrDir)
154
return a_bzrdir.open_repository()
156
def get_format_description(self):
157
return 'bzr remote repository'
159
def __eq__(self, other):
160
return self.__class__ == other.__class__
162
rich_root_data = False
165
class RemoteRepository(object):
166
"""Repository accessed over rpc.
168
For the moment everything is delegated to IO-like operations over
172
def __init__(self, remote_bzrdir, real_repository=None, _client=None):
173
"""Create a RemoteRepository instance.
175
:param remote_bzrdir: The bzrdir hosting this repository.
176
:param real_repository: If not None, a local implementation of the
177
repository logic for the repository, usually accessing the data
179
:param _client: Private testing parameter - override the smart client
180
to be used by the repository.
183
self._real_repository = real_repository
185
self._real_repository = None
186
self.bzrdir = remote_bzrdir
188
self._client = client.SmartClient(self.bzrdir.client)
190
self._client = _client
191
self._format = RemoteRepositoryFormat()
193
def _ensure_real(self):
194
"""Ensure that there is a _real_repository set.
196
used before calls to self._real_repository.
198
if not self._real_repository:
199
self.bzrdir._ensure_real()
200
self._real_repository = self.bzrdir._real_bzrdir.open_repository()
202
def get_revision_graph(self, revision_id=None):
203
"""See Repository.get_revision_graph()."""
204
if revision_id is None:
206
elif revision_id == NULL_REVISION:
209
path = self.bzrdir._path_for_remote_call(self._client)
210
response = self._client.call2('Repository.get_revision_graph', path, revision_id.encode('utf8'))
211
assert response[0][0] in ('ok', 'nosuchrevision'), 'unexpected response code %s' % (response[0],)
212
if response[0][0] == 'ok':
213
coded = response[1].read_body_bytes()
214
lines = coded.decode('utf8').split('\n')
218
d = list(line.split())
219
revision_graph[d[0]] = d[1:]
221
return revision_graph
223
assert response[1] != ''
224
raise NoSuchRevision(self, revision_id)
226
def has_revision(self, revision_id):
227
"""See Repository.has_revision()."""
228
path = self.bzrdir._path_for_remote_call(self._client)
229
response = self._client.call('Repository.has_revision', path, revision_id.encode('utf8'))
230
assert response[0] in ('ok', 'no'), 'unexpected response code %s' % (response,)
231
return response[0] == 'ok'
233
def gather_stats(self, revid, committers=None):
234
"""See Repository.gather_stats()."""
235
path = self.bzrdir._path_for_remote_call(self._client)
236
if revid in (None, NULL_REVISION):
239
fmt_revid = revid.encode('utf8')
240
if committers is None:
241
fmt_committers = 'no'
243
fmt_committers = 'yes'
244
response = self._client.call2('Repository.gather_stats', path,
245
fmt_revid, fmt_committers)
246
assert response[0][0] == 'ok', \
247
'unexpected response code %s' % (response[0],)
249
body = response[1].read_body_bytes()
251
for line in body.split('\n'):
254
key, val_text = line.split(':')
255
if key in ('revisions', 'size', 'committers'):
256
result[key] = int(val_text)
257
elif key in ('firstrev', 'latestrev'):
258
values = val_text.split(' ')[1:]
259
result[key] = (float(values[0]), long(values[1]))
263
def get_physical_lock_status(self):
264
"""See Repository.get_physical_lock_status()."""
268
"""See Repository.is_shared()."""
269
path = self.bzrdir._path_for_remote_call(self._client)
270
response = self._client.call('Repository.is_shared', path)
271
assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
272
return response[0] == 'yes'
275
# wrong eventually - want a local lock cache context
277
return self._real_repository.lock_read()
279
def lock_write(self):
280
# definately wrong: want to check if there is a real repo
281
# and not thunk through if not
283
return self._real_repository.lock_write()
286
# should free cache context.
287
return self._real_repository.unlock()
289
def break_lock(self):
290
# should hand off to the network - or better yet, we should not
291
# allow stale network locks ?
293
return self._real_repository.break_lock()
296
class RemoteBranchLockableFiles(object):
297
"""A 'LockableFiles' implementation that talks to a smart server.
299
This is not a public interface class.
302
def __init__(self, bzrdir, _client):
304
self._client = _client
307
"""'get' a remote path as per the LockableFiles interface.
309
:param path: the file to 'get'. If this is 'branch.conf', we do not
310
just retrieve a file, instead we ask the smart server to generate
311
a configuration for us - which is retrieved as an INI file.
313
assert path == 'branch.conf'
314
path = self.bzrdir._path_for_remote_call(self._client)
315
response = self._client.call2('Branch.get_config_file', path)
316
assert response[0][0] == 'ok', \
317
'unexpected response code %s' % (response[0],)
318
return StringIO(response[1].read_body_bytes())
321
class RemoteBranchFormat(branch.BranchFormat):
323
def get_format_description(self):
324
return 'Remote BZR Branch'
326
def open(self, a_bzrdir):
327
assert isinstance(a_bzrdir, RemoteBzrDir)
328
return a_bzrdir.open_branch()
330
def initialize(self, a_bzrdir):
331
assert isinstance(a_bzrdir, RemoteBzrDir)
332
return a_bzrdir.create_branch()
335
class RemoteBranch(branch.Branch):
336
"""Branch stored on a server accessed by HPSS RPC.
338
At the moment most operations are mapped down to simple file operations.
341
def __init__(self, remote_bzrdir, remote_repository, real_branch=None,
343
"""Create a RemoteBranch instance.
345
:param real_branch: An optional local implementation of the branch
346
format, usually accessing the data via the VFS.
347
:param _client: Private parameter for testing.
349
self.bzrdir = remote_bzrdir
350
if _client is not None:
351
self._client = _client
353
self._client = client.SmartClient(self.bzrdir.client)
354
self.repository = remote_repository
355
if real_branch is not None:
356
self._real_branch = real_branch
358
self._real_branch = None
359
# Fill out expected attributes of branch for bzrlib api users.
360
self._format = RemoteBranchFormat()
361
self.base = self.bzrdir.root_transport.base
362
self.control_files = RemoteBranchLockableFiles(self.bzrdir, self._client)
364
def _ensure_real(self):
365
"""Ensure that there is a _real_branch set.
367
used before calls to self._real_branch.
369
if not self._real_branch:
370
assert vfs.vfs_enabled()
371
self.bzrdir._ensure_real()
372
self._real_branch = self.bzrdir._real_bzrdir.open_branch()
373
# give the repository the matching file level repo.
374
self.repository._real_repository = self._real_branch.repository
375
# give the branch the remote repository to let fast-pathing happen
376
self._real_branch.repository = self.repository
378
def get_physical_lock_status(self):
379
"""See Branch.get_physical_lock_status()."""
380
# should be an API call to the server, as branches must be lockable.
382
return self._real_branch.get_physical_lock_status()
386
return self._real_branch.lock_read()
388
def lock_write(self):
390
return self._real_branch.lock_write()
394
return self._real_branch.unlock()
396
def break_lock(self):
398
return self._real_branch.break_lock()
400
def last_revision_info(self):
401
"""See Branch.last_revision_info()."""
402
path = self.bzrdir._path_for_remote_call(self._client)
403
response = self._client.call('Branch.last_revision_info', path)
404
assert response[0] == 'ok', 'unexpected response code %s' % (response,)
405
revno = int(response[1])
406
last_revision = response[2].decode('utf8')
407
if last_revision == '':
408
last_revision = NULL_REVISION
409
return (revno, last_revision)
411
def revision_history(self):
412
"""See Branch.revision_history()."""
413
# XXX: TODO: this does not cache the revision history for the duration
414
# of a lock, which is a bug - see the code for regular branches
416
path = self.bzrdir._path_for_remote_call(self._client)
417
response = self._client.call2('Branch.revision_history', path)
418
assert response[0][0] == 'ok', 'unexpected response code %s' % (response[0],)
419
result = response[1].read_body_bytes().decode('utf8').split('\x00')
424
def set_revision_history(self, rev_history):
426
return self._real_branch.set_revision_history(rev_history)
428
def get_parent(self):
430
return self._real_branch.get_parent()
432
def set_parent(self, url):
434
return self._real_branch.set_parent(url)
437
class RemoteWorkingTree(object):
439
def __init__(self, remote_bzrdir, real_workingtree):
440
self.real_workingtree = real_workingtree
441
self.bzrdir = remote_bzrdir
443
def __getattr__(self, name):
444
# XXX: temporary way to lazily delegate everything to the real
446
return getattr(self.real_workingtree, name)