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
195
self._leave_lock = False
197
def _ensure_real(self):
198
"""Ensure that there is a _real_repository set.
200
used before calls to self._real_repository.
202
if not self._real_repository:
203
self.bzrdir._ensure_real()
204
self._real_repository = self.bzrdir._real_bzrdir.open_repository()
206
def get_revision_graph(self, revision_id=None):
207
"""See Repository.get_revision_graph()."""
208
if revision_id is None:
210
elif revision_id == NULL_REVISION:
213
path = self.bzrdir._path_for_remote_call(self._client)
214
response = self._client.call2('Repository.get_revision_graph', path, revision_id.encode('utf8'))
215
assert response[0][0] in ('ok', 'nosuchrevision'), 'unexpected response code %s' % (response[0],)
216
if response[0][0] == 'ok':
217
coded = response[1].read_body_bytes()
218
lines = coded.decode('utf8').split('\n')
222
d = list(line.split())
223
revision_graph[d[0]] = d[1:]
225
return revision_graph
227
assert response[1] != ''
228
raise NoSuchRevision(self, revision_id)
230
def has_revision(self, revision_id):
231
"""See Repository.has_revision()."""
232
path = self.bzrdir._path_for_remote_call(self._client)
233
response = self._client.call('Repository.has_revision', path, revision_id.encode('utf8'))
234
assert response[0] in ('ok', 'no'), 'unexpected response code %s' % (response,)
235
return response[0] == 'ok'
237
def gather_stats(self, revid, committers=None):
238
"""See Repository.gather_stats()."""
239
path = self.bzrdir._path_for_remote_call(self._client)
240
if revid in (None, NULL_REVISION):
243
fmt_revid = revid.encode('utf8')
244
if committers is None:
245
fmt_committers = 'no'
247
fmt_committers = 'yes'
248
response = self._client.call2('Repository.gather_stats', path,
249
fmt_revid, fmt_committers)
250
assert response[0][0] == 'ok', \
251
'unexpected response code %s' % (response[0],)
253
body = response[1].read_body_bytes()
255
for line in body.split('\n'):
258
key, val_text = line.split(':')
259
if key in ('revisions', 'size', 'committers'):
260
result[key] = int(val_text)
261
elif key in ('firstrev', 'latestrev'):
262
values = val_text.split(' ')[1:]
263
result[key] = (float(values[0]), long(values[1]))
267
def get_physical_lock_status(self):
268
"""See Repository.get_physical_lock_status()."""
272
"""See Repository.is_shared()."""
273
path = self.bzrdir._path_for_remote_call(self._client)
274
response = self._client.call('Repository.is_shared', path)
275
assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
276
return response[0] == 'yes'
279
# wrong eventually - want a local lock cache context
280
if not self._lock_mode:
281
self._lock_mode = 'r'
283
if self._real_repository is not None:
284
self._real_repository.lock_read()
286
self._lock_count += 1
288
def _lock_write(self, token):
289
path = self.bzrdir._path_for_remote_call(self._client)
292
response = self._client.call('Repository.lock_write', path, token)
293
if response[0] == 'ok':
296
elif response[0] == 'LockContention':
297
raise errors.LockContention('(remote lock)')
299
assert False, 'unexpected response code %s' % (response,)
301
def lock_write(self, token=None):
302
if not self._lock_mode:
303
self._lock_token = self._lock_write(token)
304
assert self._lock_token, 'Remote server did not return a token!'
305
if self._real_repository is not None:
306
self._real_repository.lock_write(token=self._lock_token)
307
if token is not None:
308
self._leave_lock = True
310
self._leave_lock = False
311
self._lock_mode = 'w'
313
elif self._lock_mode == 'r':
314
raise errors.ReadOnlyTransaction
316
self._lock_count += 1
317
return self._lock_token
319
def leave_lock_in_place(self):
320
self._leave_lock = True
322
def dont_leave_lock_in_place(self):
323
self._leave_lock = False
325
def _set_real_repository(self, repository):
326
"""Set the _real_repository for this repository.
328
:param repository: The repository to fallback to for non-hpss
329
implemented operations.
331
self._real_repository = repository
332
if self._lock_mode == 'w':
333
# if we are already locked, the real repository must be able to
334
# acquire the lock with our token.
335
self._real_repository.lock_write(self._lock_token)
337
def _unlock(self, token):
338
path = self.bzrdir._path_for_remote_call(self._client)
339
response = self._client.call('Repository.unlock', path, token)
340
if response == ('ok',):
342
elif response[0] == 'TokenMismatch':
343
raise errors.TokenMismatch(token, '(remote token)')
345
assert False, 'unexpected response code %s' % (response,)
348
self._lock_count -= 1
349
if not self._lock_count:
350
mode = self._lock_mode
351
self._lock_mode = None
352
if self._real_repository is not None:
353
self._real_repository.unlock()
356
assert self._lock_token, 'Locked, but no token!'
357
token = self._lock_token
358
self._lock_token = None
359
if not self._leave_lock:
362
def break_lock(self):
363
# should hand off to the network
365
return self._real_repository.break_lock()
368
class RemoteBranchLockableFiles(object):
369
"""A 'LockableFiles' implementation that talks to a smart server.
371
This is not a public interface class.
374
def __init__(self, bzrdir, _client):
376
self._client = _client
379
"""'get' a remote path as per the LockableFiles interface.
381
:param path: the file to 'get'. If this is 'branch.conf', we do not
382
just retrieve a file, instead we ask the smart server to generate
383
a configuration for us - which is retrieved as an INI file.
385
assert path == 'branch.conf'
386
path = self.bzrdir._path_for_remote_call(self._client)
387
response = self._client.call2('Branch.get_config_file', path)
388
assert response[0][0] == 'ok', \
389
'unexpected response code %s' % (response[0],)
390
return StringIO(response[1].read_body_bytes())
393
class RemoteBranchFormat(branch.BranchFormat):
395
def get_format_description(self):
396
return 'Remote BZR Branch'
398
def open(self, a_bzrdir):
399
assert isinstance(a_bzrdir, RemoteBzrDir)
400
return a_bzrdir.open_branch()
402
def initialize(self, a_bzrdir):
403
assert isinstance(a_bzrdir, RemoteBzrDir)
404
return a_bzrdir.create_branch()
407
class RemoteBranch(branch.Branch):
408
"""Branch stored on a server accessed by HPSS RPC.
410
At the moment most operations are mapped down to simple file operations.
413
def __init__(self, remote_bzrdir, remote_repository, real_branch=None,
415
"""Create a RemoteBranch instance.
417
:param real_branch: An optional local implementation of the branch
418
format, usually accessing the data via the VFS.
419
:param _client: Private parameter for testing.
421
self.bzrdir = remote_bzrdir
422
if _client is not None:
423
self._client = _client
425
self._client = client.SmartClient(self.bzrdir.client)
426
self.repository = remote_repository
427
if real_branch is not None:
428
self._real_branch = real_branch
430
self._real_branch = None
431
# Fill out expected attributes of branch for bzrlib api users.
432
self._format = RemoteBranchFormat()
433
self.base = self.bzrdir.root_transport.base
434
self.control_files = RemoteBranchLockableFiles(self.bzrdir, self._client)
436
def _ensure_real(self):
437
"""Ensure that there is a _real_branch set.
439
used before calls to self._real_branch.
441
if not self._real_branch:
442
assert vfs.vfs_enabled()
443
self.bzrdir._ensure_real()
444
self._real_branch = self.bzrdir._real_bzrdir.open_branch()
445
# Give the remote repository the matching real repo.
446
self.repository._set_real_repository(self._real_branch.repository)
447
# Give the branch the remote repository to let fast-pathing happen.
448
self._real_branch.repository = self.repository
450
def get_physical_lock_status(self):
451
"""See Branch.get_physical_lock_status()."""
452
# should be an API call to the server, as branches must be lockable.
454
return self._real_branch.get_physical_lock_status()
458
return self._real_branch.lock_read()
460
def lock_write(self, token=None):
462
return self._real_branch.lock_write(token=token)
466
return self._real_branch.unlock()
468
def break_lock(self):
470
return self._real_branch.break_lock()
472
def last_revision_info(self):
473
"""See Branch.last_revision_info()."""
474
path = self.bzrdir._path_for_remote_call(self._client)
475
response = self._client.call('Branch.last_revision_info', path)
476
assert response[0] == 'ok', 'unexpected response code %s' % (response,)
477
revno = int(response[1])
478
last_revision = response[2].decode('utf8')
479
if last_revision == '':
480
last_revision = NULL_REVISION
481
return (revno, last_revision)
483
def revision_history(self):
484
"""See Branch.revision_history()."""
485
# XXX: TODO: this does not cache the revision history for the duration
486
# of a lock, which is a bug - see the code for regular branches
488
path = self.bzrdir._path_for_remote_call(self._client)
489
response = self._client.call2('Branch.revision_history', path)
490
assert response[0][0] == 'ok', 'unexpected response code %s' % (response[0],)
491
result = response[1].read_body_bytes().decode('utf8').split('\x00')
496
def set_revision_history(self, rev_history):
497
# Send just the tip revision of the history; the server will generate
498
# the full history from that. If the revision doesn't exist in this
499
# branch, NoSuchRevision will be raised.
500
path = self.bzrdir._path_for_remote_call(self._client)
501
if rev_history == []:
504
rev_id = rev_history[-1]
505
response = self._client.call('Branch.set_last_revision', path, rev_id)
506
if response[0] == 'NoSuchRevision':
507
raise NoSuchRevision(self, rev_id)
509
assert response == ('ok',), (
510
'unexpected response code %r' % (response,))
512
def get_parent(self):
514
return self._real_branch.get_parent()
516
def set_parent(self, url):
518
return self._real_branch.set_parent(url)
521
class RemoteWorkingTree(object):
523
def __init__(self, remote_bzrdir, real_workingtree):
524
self.real_workingtree = real_workingtree
525
self.bzrdir = remote_bzrdir
527
def __getattr__(self, name):
528
# XXX: temporary way to lazily delegate everything to the real
530
return getattr(self.real_workingtree, name)