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()
192
self._lock_mode = None
193
self._lock_token = None
196
def _ensure_real(self):
197
"""Ensure that there is a _real_repository set.
199
used before calls to self._real_repository.
201
if not self._real_repository:
202
self.bzrdir._ensure_real()
203
self._real_repository = self.bzrdir._real_bzrdir.open_repository()
205
def get_revision_graph(self, revision_id=None):
206
"""See Repository.get_revision_graph()."""
207
if revision_id is None:
209
elif revision_id == NULL_REVISION:
212
path = self.bzrdir._path_for_remote_call(self._client)
213
response = self._client.call2('Repository.get_revision_graph', path, revision_id.encode('utf8'))
214
assert response[0][0] in ('ok', 'nosuchrevision'), 'unexpected response code %s' % (response[0],)
215
if response[0][0] == 'ok':
216
coded = response[1].read_body_bytes()
217
lines = coded.decode('utf8').split('\n')
221
d = list(line.split())
222
revision_graph[d[0]] = d[1:]
224
return revision_graph
226
assert response[1] != ''
227
raise NoSuchRevision(self, revision_id)
229
def has_revision(self, revision_id):
230
"""See Repository.has_revision()."""
231
path = self.bzrdir._path_for_remote_call(self._client)
232
response = self._client.call('Repository.has_revision', path, revision_id.encode('utf8'))
233
assert response[0] in ('ok', 'no'), 'unexpected response code %s' % (response,)
234
return response[0] == 'ok'
236
def gather_stats(self, revid, committers=None):
237
"""See Repository.gather_stats()."""
238
path = self.bzrdir._path_for_remote_call(self._client)
239
if revid in (None, NULL_REVISION):
242
fmt_revid = revid.encode('utf8')
243
if committers is None:
244
fmt_committers = 'no'
246
fmt_committers = 'yes'
247
response = self._client.call2('Repository.gather_stats', path,
248
fmt_revid, fmt_committers)
249
assert response[0][0] == 'ok', \
250
'unexpected response code %s' % (response[0],)
252
body = response[1].read_body_bytes()
254
for line in body.split('\n'):
257
key, val_text = line.split(':')
258
if key in ('revisions', 'size', 'committers'):
259
result[key] = int(val_text)
260
elif key in ('firstrev', 'latestrev'):
261
values = val_text.split(' ')[1:]
262
result[key] = (float(values[0]), long(values[1]))
266
def get_physical_lock_status(self):
267
"""See Repository.get_physical_lock_status()."""
271
"""See Repository.is_shared()."""
272
path = self.bzrdir._path_for_remote_call(self._client)
273
response = self._client.call('Repository.is_shared', path)
274
assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
275
return response[0] == 'yes'
278
# wrong eventually - want a local lock cache context
279
if not self._lock_mode:
280
self._lock_mode = 'r'
283
self._lock_count += 1
285
return self._real_repository.lock_read()
287
def lock_write(self, token=None):
288
# definately wrong: want to check if there is a real repo
289
# and not thunk through if not
290
if not self._lock_mode:
292
self._lock_token = self._real_repository.lock_write(token=token)
293
self._lock_mode = 'w'
295
elif self._lock_mode == 'r':
296
raise errors.ReadOnlyTransaction
298
self._lock_count += 1
299
return self._lock_token
301
def leave_lock_in_place(self):
302
self._real_repository.leave_lock_in_place()
304
def dont_leave_lock_in_place(self):
305
self._real_repository.dont_leave_lock_in_place()
307
def _set_real_repository(self, repository):
308
"""Set the _real_repository for this repository.
310
:param repository: The repository to fallback to for non-hpss
311
implemented operations.
313
self._real_repository = repository
314
if self._lock_mode == 'w':
315
# if we are already locked, the real repository must be able to
316
# acquire the lock with our token.
317
self._real_repository.lock_write(self._lock_token)
320
# should free cache context.
321
self._lock_count -= 1
322
if not self._lock_count:
323
self._lock_mode = None
324
self._lock_token = None
325
return self._real_repository.unlock()
327
def break_lock(self):
328
# should hand off to the network - or better yet, we should not
329
# allow stale network locks ?
331
return self._real_repository.break_lock()
334
class RemoteBranchLockableFiles(object):
335
"""A 'LockableFiles' implementation that talks to a smart server.
337
This is not a public interface class.
340
def __init__(self, bzrdir, _client):
342
self._client = _client
345
"""'get' a remote path as per the LockableFiles interface.
347
:param path: the file to 'get'. If this is 'branch.conf', we do not
348
just retrieve a file, instead we ask the smart server to generate
349
a configuration for us - which is retrieved as an INI file.
351
assert path == 'branch.conf'
352
path = self.bzrdir._path_for_remote_call(self._client)
353
response = self._client.call2('Branch.get_config_file', path)
354
assert response[0][0] == 'ok', \
355
'unexpected response code %s' % (response[0],)
356
return StringIO(response[1].read_body_bytes())
359
class RemoteBranchFormat(branch.BranchFormat):
361
def get_format_description(self):
362
return 'Remote BZR Branch'
364
def open(self, a_bzrdir):
365
assert isinstance(a_bzrdir, RemoteBzrDir)
366
return a_bzrdir.open_branch()
368
def initialize(self, a_bzrdir):
369
assert isinstance(a_bzrdir, RemoteBzrDir)
370
return a_bzrdir.create_branch()
373
class RemoteBranch(branch.Branch):
374
"""Branch stored on a server accessed by HPSS RPC.
376
At the moment most operations are mapped down to simple file operations.
379
def __init__(self, remote_bzrdir, remote_repository, real_branch=None,
381
"""Create a RemoteBranch instance.
383
:param real_branch: An optional local implementation of the branch
384
format, usually accessing the data via the VFS.
385
:param _client: Private parameter for testing.
387
self.bzrdir = remote_bzrdir
388
if _client is not None:
389
self._client = _client
391
self._client = client.SmartClient(self.bzrdir.client)
392
self.repository = remote_repository
393
if real_branch is not None:
394
self._real_branch = real_branch
396
self._real_branch = None
397
# Fill out expected attributes of branch for bzrlib api users.
398
self._format = RemoteBranchFormat()
399
self.base = self.bzrdir.root_transport.base
400
self.control_files = RemoteBranchLockableFiles(self.bzrdir, self._client)
402
def _ensure_real(self):
403
"""Ensure that there is a _real_branch set.
405
used before calls to self._real_branch.
407
if not self._real_branch:
408
assert vfs.vfs_enabled()
409
self.bzrdir._ensure_real()
410
self._real_branch = self.bzrdir._real_bzrdir.open_branch()
411
# Give the remote repository the matching real repo.
412
self.repository._set_real_repository(self._real_branch.repository)
413
# Give the branch the remote repository to let fast-pathing happen.
414
self._real_branch.repository = self.repository
416
def get_physical_lock_status(self):
417
"""See Branch.get_physical_lock_status()."""
418
# should be an API call to the server, as branches must be lockable.
420
return self._real_branch.get_physical_lock_status()
424
return self._real_branch.lock_read()
426
def lock_write(self, token=None):
428
return self._real_branch.lock_write(token=token)
432
return self._real_branch.unlock()
434
def break_lock(self):
436
return self._real_branch.break_lock()
438
def last_revision_info(self):
439
"""See Branch.last_revision_info()."""
440
path = self.bzrdir._path_for_remote_call(self._client)
441
response = self._client.call('Branch.last_revision_info', path)
442
assert response[0] == 'ok', 'unexpected response code %s' % (response,)
443
revno = int(response[1])
444
last_revision = response[2].decode('utf8')
445
if last_revision == '':
446
last_revision = NULL_REVISION
447
return (revno, last_revision)
449
def revision_history(self):
450
"""See Branch.revision_history()."""
451
# XXX: TODO: this does not cache the revision history for the duration
452
# of a lock, which is a bug - see the code for regular branches
454
path = self.bzrdir._path_for_remote_call(self._client)
455
response = self._client.call2('Branch.revision_history', path)
456
assert response[0][0] == 'ok', 'unexpected response code %s' % (response[0],)
457
result = response[1].read_body_bytes().decode('utf8').split('\x00')
462
def set_revision_history(self, rev_history):
463
# Send just the tip revision of the history; the server will generate
464
# the full history from that. If the revision doesn't exist in this
465
# branch, NoSuchRevision will be raised.
466
path = self.bzrdir._path_for_remote_call(self._client)
467
if rev_history == []:
470
rev_id = rev_history[-1]
471
response = self._client.call('Branch.set_last_revision', path, rev_id)
472
if response[0] == 'NoSuchRevision':
473
raise NoSuchRevision(self, rev_id)
475
assert response == ('ok',), (
476
'unexpected response code %r' % (response,))
478
def get_parent(self):
480
return self._real_branch.get_parent()
482
def set_parent(self, url):
484
return self._real_branch.set_parent(url)
487
class RemoteWorkingTree(object):
489
def __init__(self, remote_bzrdir, real_workingtree):
490
self.real_workingtree = real_workingtree
491
self.bzrdir = remote_bzrdir
493
def __getattr__(self, name):
494
# XXX: temporary way to lazily delegate everything to the real
496
return getattr(self.real_workingtree, name)