/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/remote.py

Implement RemoteRepository.lock_write/unlock to expect and send tokens over the
smart protocol as appropriate, so that locking operations on RemoteRepositories
work correctly.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006, 2007 Canonical Ltd
 
2
#
 
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.
 
7
#
 
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.
 
12
#
 
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
 
16
 
 
17
# TODO: At some point, handle upgrades by just passing the whole request
 
18
# across to run on the server.
 
19
 
 
20
from cStringIO import StringIO
 
21
from urlparse import urlparse
 
22
 
 
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
 
30
 
 
31
# Note: RemoteBzrDirFormat is in bzrdir.py
 
32
 
 
33
class RemoteBzrDir(BzrDir):
 
34
    """Control directory on a remote server, accessed by HPSS."""
 
35
 
 
36
    def __init__(self, transport, _client=None):
 
37
        """Construct a RemoteBzrDir.
 
38
 
 
39
        :param _client: Private parameter for testing. Disables probing and the
 
40
            use of a real bzrdir.
 
41
        """
 
42
        BzrDir.__init__(self, transport, RemoteBzrDirFormat())
 
43
        if _client is not None:
 
44
            self.client = _client
 
45
            return
 
46
 
 
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
 
53
        
 
54
        self._ensure_real()
 
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)
 
61
 
 
62
    def _ensure_real(self):
 
63
        """Ensure that there is a _real_bzrdir set.
 
64
 
 
65
        used before calls to self._real_bzrdir.
 
66
        """
 
67
        if not self._real_bzrdir:
 
68
            default_format = BzrDirFormat.get_default_format()
 
69
            self._real_bzrdir = default_format.open(self.root_transport,
 
70
                _found=True)
 
71
 
 
72
    def create_repository(self, shared=False):
 
73
        return RemoteRepository(
 
74
            self, self._real_bzrdir.create_repository(shared=shared))
 
75
 
 
76
    def create_branch(self):
 
77
        real_branch = self._real_bzrdir.create_branch()
 
78
        return RemoteBranch(self, self.find_repository(), real_branch)
 
79
 
 
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)
 
83
 
 
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)
 
93
        if response[1] == '':
 
94
            # branch at this location.
 
95
            return RemoteBranch(self, self.find_repository())
 
96
        else:
 
97
            # a branch reference, use the existing BranchReference logic.
 
98
            format = BranchReferenceFormat()
 
99
            return format.open(self, _found=True, location=response[1])
 
100
 
 
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)
 
111
        else:
 
112
            raise errors.NoRepositoryPresent(self)
 
113
 
 
114
    def open_workingtree(self):
 
115
        return RemoteWorkingTree(self, self._real_bzrdir.open_workingtree())
 
116
 
 
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)
 
120
 
 
121
    def get_branch_transport(self, branch_format):
 
122
        return self._real_bzrdir.get_branch_transport(branch_format)
 
123
 
 
124
    def get_repository_transport(self, repository_format):
 
125
        return self._real_bzrdir.get_repository_transport(repository_format)
 
126
 
 
127
    def get_workingtree_transport(self, workingtree_format):
 
128
        return self._real_bzrdir.get_workingtree_transport(workingtree_format)
 
129
 
 
130
    def can_convert_format(self):
 
131
        """Upgrading of remote bzrdirs is not supported yet."""
 
132
        return False
 
133
 
 
134
    def needs_format_conversion(self, format=None):
 
135
        """Upgrading of remote bzrdirs is not supported yet."""
 
136
        return False
 
137
 
 
138
 
 
139
class RemoteRepositoryFormat(repository.RepositoryFormat):
 
140
    """Format for repositories accessed over rpc.
 
141
 
 
142
    Instances of this repository are represented by RemoteRepository
 
143
    instances.
 
144
    """
 
145
 
 
146
    _matchingbzrdir = RemoteBzrDirFormat
 
147
 
 
148
    def initialize(self, a_bzrdir, shared=False):
 
149
        assert isinstance(a_bzrdir, RemoteBzrDir)
 
150
        return a_bzrdir.create_repository(shared=shared)
 
151
    
 
152
    def open(self, a_bzrdir):
 
153
        assert isinstance(a_bzrdir, RemoteBzrDir)
 
154
        return a_bzrdir.open_repository()
 
155
 
 
156
    def get_format_description(self):
 
157
        return 'bzr remote repository'
 
158
 
 
159
    def __eq__(self, other):
 
160
        return self.__class__ == other.__class__
 
161
 
 
162
    rich_root_data = False
 
163
 
 
164
 
 
165
class RemoteRepository(object):
 
166
    """Repository accessed over rpc.
 
167
 
 
168
    For the moment everything is delegated to IO-like operations over
 
169
    the transport.
 
170
    """
 
171
 
 
172
    def __init__(self, remote_bzrdir, real_repository=None, _client=None):
 
173
        """Create a RemoteRepository instance.
 
174
        
 
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
 
178
            via the VFS.
 
179
        :param _client: Private testing parameter - override the smart client
 
180
            to be used by the repository.
 
181
        """
 
182
        if real_repository:
 
183
            self._real_repository = real_repository
 
184
        else:
 
185
            self._real_repository = None
 
186
        self.bzrdir = remote_bzrdir
 
187
        if _client is None:
 
188
            self._client = client.SmartClient(self.bzrdir.client)
 
189
        else:
 
190
            self._client = _client
 
191
        self._format = RemoteRepositoryFormat()
 
192
        self._lock_mode = None
 
193
        self._lock_token = None
 
194
        self._lock_count = 0
 
195
        self._leave_lock = False
 
196
 
 
197
    def _ensure_real(self):
 
198
        """Ensure that there is a _real_repository set.
 
199
 
 
200
        used before calls to self._real_repository.
 
201
        """
 
202
        if not self._real_repository:
 
203
            self.bzrdir._ensure_real()
 
204
            self._real_repository = self.bzrdir._real_bzrdir.open_repository()
 
205
 
 
206
    def get_revision_graph(self, revision_id=None):
 
207
        """See Repository.get_revision_graph()."""
 
208
        if revision_id is None:
 
209
            revision_id = ''
 
210
        elif revision_id == NULL_REVISION:
 
211
            return {}
 
212
 
 
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')
 
219
            revision_graph = {}
 
220
            # FIXME
 
221
            for line in lines:
 
222
                d = list(line.split())
 
223
                revision_graph[d[0]] = d[1:]
 
224
                
 
225
            return revision_graph
 
226
        else:
 
227
            assert response[1] != ''
 
228
            raise NoSuchRevision(self, revision_id)
 
229
 
 
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'
 
236
 
 
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):
 
241
            fmt_revid = ''
 
242
        else:
 
243
            fmt_revid = revid.encode('utf8')
 
244
        if committers is None:
 
245
            fmt_committers = 'no'
 
246
        else:
 
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],)
 
252
 
 
253
        body = response[1].read_body_bytes()
 
254
        result = {}
 
255
        for line in body.split('\n'):
 
256
            if not line:
 
257
                continue
 
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]))
 
264
 
 
265
        return result
 
266
 
 
267
    def get_physical_lock_status(self):
 
268
        """See Repository.get_physical_lock_status()."""
 
269
        return False
 
270
 
 
271
    def is_shared(self):
 
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'
 
277
 
 
278
    def lock_read(self):
 
279
        # wrong eventually - want a local lock cache context
 
280
        if not self._lock_mode:
 
281
            self._lock_mode = 'r'
 
282
            self._lock_count = 1
 
283
            if self._real_repository is not None:
 
284
                self._real_repository.lock_read()
 
285
        else:
 
286
            self._lock_count += 1
 
287
 
 
288
    def _lock_write(self, token):
 
289
        path = self.bzrdir._path_for_remote_call(self._client)
 
290
        if token is None:
 
291
            token = ''
 
292
        response = self._client.call('Repository.lock_write', path, token)
 
293
        if response[0] == 'ok':
 
294
            ok, token = response
 
295
            return token
 
296
        elif response[0] == 'LockContention':
 
297
            raise errors.LockContention('(remote lock)')
 
298
        else:
 
299
            assert False, 'unexpected response code %s' % (response,)
 
300
 
 
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
 
309
            else:
 
310
                self._leave_lock = False
 
311
            self._lock_mode = 'w'
 
312
            self._lock_count = 1
 
313
        elif self._lock_mode == 'r':
 
314
            raise errors.ReadOnlyTransaction
 
315
        else:
 
316
            self._lock_count += 1
 
317
        return self._lock_token
 
318
 
 
319
    def leave_lock_in_place(self):
 
320
        self._leave_lock = True
 
321
 
 
322
    def dont_leave_lock_in_place(self):
 
323
        self._leave_lock = False
 
324
 
 
325
    def _set_real_repository(self, repository):
 
326
        """Set the _real_repository for this repository.
 
327
 
 
328
        :param repository: The repository to fallback to for non-hpss
 
329
            implemented operations.
 
330
        """
 
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)
 
336
 
 
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',):
 
341
            return
 
342
        elif response[0] == 'TokenMismatch':
 
343
            raise errors.TokenMismatch(token, '(remote token)')
 
344
        else:
 
345
            assert False, 'unexpected response code %s' % (response,)
 
346
 
 
347
    def unlock(self):
 
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()
 
354
            if mode != 'w':
 
355
                return
 
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:
 
360
                self._unlock(token)
 
361
 
 
362
    def break_lock(self):
 
363
        # should hand off to the network
 
364
        self._ensure_real()
 
365
        return self._real_repository.break_lock()
 
366
 
 
367
 
 
368
class RemoteBranchLockableFiles(object):
 
369
    """A 'LockableFiles' implementation that talks to a smart server.
 
370
    
 
371
    This is not a public interface class.
 
372
    """
 
373
 
 
374
    def __init__(self, bzrdir, _client):
 
375
        self.bzrdir = bzrdir
 
376
        self._client = _client
 
377
 
 
378
    def get(self, path):
 
379
        """'get' a remote path as per the LockableFiles interface.
 
380
 
 
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.
 
384
        """
 
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())
 
391
 
 
392
 
 
393
class RemoteBranchFormat(branch.BranchFormat):
 
394
 
 
395
    def get_format_description(self):
 
396
        return 'Remote BZR Branch'
 
397
 
 
398
    def open(self, a_bzrdir):
 
399
        assert isinstance(a_bzrdir, RemoteBzrDir)
 
400
        return a_bzrdir.open_branch()
 
401
 
 
402
    def initialize(self, a_bzrdir):
 
403
        assert isinstance(a_bzrdir, RemoteBzrDir)
 
404
        return a_bzrdir.create_branch()
 
405
 
 
406
 
 
407
class RemoteBranch(branch.Branch):
 
408
    """Branch stored on a server accessed by HPSS RPC.
 
409
 
 
410
    At the moment most operations are mapped down to simple file operations.
 
411
    """
 
412
 
 
413
    def __init__(self, remote_bzrdir, remote_repository, real_branch=None,
 
414
        _client=None):
 
415
        """Create a RemoteBranch instance.
 
416
 
 
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.
 
420
        """
 
421
        self.bzrdir = remote_bzrdir
 
422
        if _client is not None:
 
423
            self._client = _client
 
424
        else:
 
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
 
429
        else:
 
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)
 
435
 
 
436
    def _ensure_real(self):
 
437
        """Ensure that there is a _real_branch set.
 
438
 
 
439
        used before calls to self._real_branch.
 
440
        """
 
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
 
449
 
 
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.
 
453
        self._ensure_real()
 
454
        return self._real_branch.get_physical_lock_status()
 
455
 
 
456
    def lock_read(self):
 
457
        self._ensure_real()
 
458
        return self._real_branch.lock_read()
 
459
 
 
460
    def lock_write(self, token=None):
 
461
        self._ensure_real()
 
462
        return self._real_branch.lock_write(token=token)
 
463
 
 
464
    def unlock(self):
 
465
        self._ensure_real()
 
466
        return self._real_branch.unlock()
 
467
 
 
468
    def break_lock(self):
 
469
        self._ensure_real()
 
470
        return self._real_branch.break_lock()
 
471
 
 
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)
 
482
 
 
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
 
487
        # for details.
 
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')
 
492
        if result == ['']:
 
493
            return []
 
494
        return result
 
495
 
 
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 == []:
 
502
            rev_id = ''
 
503
        else:
 
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)
 
508
        else:
 
509
            assert response == ('ok',), (
 
510
                'unexpected response code %r' % (response,))
 
511
 
 
512
    def get_parent(self):
 
513
        self._ensure_real()
 
514
        return self._real_branch.get_parent()
 
515
        
 
516
    def set_parent(self, url):
 
517
        self._ensure_real()
 
518
        return self._real_branch.set_parent(url)
 
519
        
 
520
 
 
521
class RemoteWorkingTree(object):
 
522
 
 
523
    def __init__(self, remote_bzrdir, real_workingtree):
 
524
        self.real_workingtree = real_workingtree
 
525
        self.bzrdir = remote_bzrdir
 
526
 
 
527
    def __getattr__(self, name):
 
528
        # XXX: temporary way to lazily delegate everything to the real
 
529
        # workingtree
 
530
        return getattr(self.real_workingtree, name)
 
531
 
 
532