/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

Merge Robert's status prefix changes to protocol 2.

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
 
 
22
from bzrlib import branch, errors, lockdir, repository
 
23
from bzrlib.branch import BranchReferenceFormat
 
24
from bzrlib.bzrdir import BzrDir, RemoteBzrDirFormat
 
25
from bzrlib.config import BranchConfig, TreeConfig
 
26
from bzrlib.decorators import needs_read_lock, needs_write_lock
 
27
from bzrlib.errors import NoSuchRevision
 
28
from bzrlib.lockable_files import LockableFiles
 
29
from bzrlib.revision import NULL_REVISION
 
30
from bzrlib.smart import client, vfs
 
31
 
 
32
# Note: RemoteBzrDirFormat is in bzrdir.py
 
33
 
 
34
class RemoteBzrDir(BzrDir):
 
35
    """Control directory on a remote server, accessed via bzr:// or similar."""
 
36
 
 
37
    def __init__(self, transport, _client=None):
 
38
        """Construct a RemoteBzrDir.
 
39
 
 
40
        :param _client: Private parameter for testing. Disables probing and the
 
41
            use of a real bzrdir.
 
42
        """
 
43
        BzrDir.__init__(self, transport, RemoteBzrDirFormat())
 
44
        # this object holds a delegated bzrdir that uses file-level operations
 
45
        # to talk to the other side
 
46
        self._real_bzrdir = None
 
47
 
 
48
        if _client is None:
 
49
            self._medium = transport.get_smart_client()
 
50
            self._client = client._SmartClient(self._medium)
 
51
        else:
 
52
            self._client = _client
 
53
            self._medium = None
 
54
            return
 
55
 
 
56
        path = self._path_for_remote_call(self._client)
 
57
        response = self._client.call('BzrDir.open', path)
 
58
        if response not in [('yes',), ('no',)]:
 
59
            raise errors.UnexpectedSmartServerResponse(response)
 
60
        if response == ('no',):
 
61
            raise errors.NotBranchError(path=transport.base)
 
62
 
 
63
    def _ensure_real(self):
 
64
        """Ensure that there is a _real_bzrdir set.
 
65
 
 
66
        Used before calls to self._real_bzrdir.
 
67
        """
 
68
        if not self._real_bzrdir:
 
69
            self._real_bzrdir = BzrDir.open_from_transport(
 
70
                self.root_transport, _server_formats=False)
 
71
 
 
72
    def create_repository(self, shared=False):
 
73
        self._ensure_real()
 
74
        self._real_bzrdir.create_repository(shared=shared)
 
75
        return self.open_repository()
 
76
 
 
77
    def create_branch(self):
 
78
        self._ensure_real()
 
79
        real_branch = self._real_bzrdir.create_branch()
 
80
        return RemoteBranch(self, self.find_repository(), real_branch)
 
81
 
 
82
    def create_workingtree(self, revision_id=None):
 
83
        raise errors.NotLocalUrl(self.transport.base)
 
84
 
 
85
    def find_branch_format(self):
 
86
        """Find the branch 'format' for this bzrdir.
 
87
 
 
88
        This might be a synthetic object for e.g. RemoteBranch and SVN.
 
89
        """
 
90
        b = self.open_branch()
 
91
        return b._format
 
92
 
 
93
    def get_branch_reference(self):
 
94
        """See BzrDir.get_branch_reference()."""
 
95
        path = self._path_for_remote_call(self._client)
 
96
        response = self._client.call('BzrDir.open_branch', path)
 
97
        if response[0] == 'ok':
 
98
            if response[1] == '':
 
99
                # branch at this location.
 
100
                return None
 
101
            else:
 
102
                # a branch reference, use the existing BranchReference logic.
 
103
                return response[1]
 
104
        elif response == ('nobranch',):
 
105
            raise errors.NotBranchError(path=self.root_transport.base)
 
106
        else:
 
107
            assert False, 'unexpected response code %r' % (response,)
 
108
 
 
109
    def open_branch(self, _unsupported=False):
 
110
        assert _unsupported == False, 'unsupported flag support not implemented yet.'
 
111
        reference_url = self.get_branch_reference()
 
112
        if reference_url is None:
 
113
            # branch at this location.
 
114
            return RemoteBranch(self, self.find_repository())
 
115
        else:
 
116
            # a branch reference, use the existing BranchReference logic.
 
117
            format = BranchReferenceFormat()
 
118
            return format.open(self, _found=True, location=reference_url)
 
119
                
 
120
    def open_repository(self):
 
121
        path = self._path_for_remote_call(self._client)
 
122
        response = self._client.call('BzrDir.find_repository', path)
 
123
        assert response[0] in ('ok', 'norepository'), \
 
124
            'unexpected response code %s' % (response,)
 
125
        if response[0] == 'norepository':
 
126
            raise errors.NoRepositoryPresent(self)
 
127
        assert len(response) == 4, 'incorrect response length %s' % (response,)
 
128
        if response[1] == '':
 
129
            format = RemoteRepositoryFormat()
 
130
            format.rich_root_data = (response[2] == 'yes')
 
131
            format.supports_tree_reference = (response[3] == 'yes')
 
132
            return RemoteRepository(self, format)
 
133
        else:
 
134
            raise errors.NoRepositoryPresent(self)
 
135
 
 
136
    def open_workingtree(self, recommend_upgrade=True):
 
137
        self._ensure_real()
 
138
        if self._real_bzrdir.has_workingtree():
 
139
            raise errors.NotLocalUrl(self.root_transport)
 
140
        else:
 
141
            raise errors.NoWorkingTree(self.root_transport.base)
 
142
 
 
143
    def _path_for_remote_call(self, client):
 
144
        """Return the path to be used for this bzrdir in a remote call."""
 
145
        return client.remote_path_from_transport(self.root_transport)
 
146
 
 
147
    def get_branch_transport(self, branch_format):
 
148
        self._ensure_real()
 
149
        return self._real_bzrdir.get_branch_transport(branch_format)
 
150
 
 
151
    def get_repository_transport(self, repository_format):
 
152
        self._ensure_real()
 
153
        return self._real_bzrdir.get_repository_transport(repository_format)
 
154
 
 
155
    def get_workingtree_transport(self, workingtree_format):
 
156
        self._ensure_real()
 
157
        return self._real_bzrdir.get_workingtree_transport(workingtree_format)
 
158
 
 
159
    def can_convert_format(self):
 
160
        """Upgrading of remote bzrdirs is not supported yet."""
 
161
        return False
 
162
 
 
163
    def needs_format_conversion(self, format=None):
 
164
        """Upgrading of remote bzrdirs is not supported yet."""
 
165
        return False
 
166
 
 
167
    def clone(self, url, revision_id=None, force_new_repo=False):
 
168
        self._ensure_real()
 
169
        return self._real_bzrdir.clone(url, revision_id=revision_id,
 
170
            force_new_repo=force_new_repo)
 
171
 
 
172
 
 
173
class RemoteRepositoryFormat(repository.RepositoryFormat):
 
174
    """Format for repositories accessed over a _SmartClient.
 
175
 
 
176
    Instances of this repository are represented by RemoteRepository
 
177
    instances.
 
178
 
 
179
    The RemoteRepositoryFormat is parameterised during construction
 
180
    to reflect the capabilities of the real, remote format. Specifically
 
181
    the attributes rich_root_data and supports_tree_reference are set
 
182
    on a per instance basis, and are not set (and should not be) at
 
183
    the class level.
 
184
    """
 
185
 
 
186
    _matchingbzrdir = RemoteBzrDirFormat
 
187
 
 
188
    def initialize(self, a_bzrdir, shared=False):
 
189
        assert isinstance(a_bzrdir, RemoteBzrDir), \
 
190
            '%r is not a RemoteBzrDir' % (a_bzrdir,)
 
191
        return a_bzrdir.create_repository(shared=shared)
 
192
    
 
193
    def open(self, a_bzrdir):
 
194
        assert isinstance(a_bzrdir, RemoteBzrDir)
 
195
        return a_bzrdir.open_repository()
 
196
 
 
197
    def get_format_description(self):
 
198
        return 'bzr remote repository'
 
199
 
 
200
    def __eq__(self, other):
 
201
        return self.__class__ == other.__class__
 
202
 
 
203
    def check_conversion_target(self, target_format):
 
204
        if self.rich_root_data and not target_format.rich_root_data:
 
205
            raise errors.BadConversionTarget(
 
206
                'Does not support rich root data.', target_format)
 
207
        if (self.supports_tree_reference and
 
208
            not getattr(target_format, 'supports_tree_reference', False)):
 
209
            raise errors.BadConversionTarget(
 
210
                'Does not support nested trees', target_format)
 
211
 
 
212
 
 
213
class RemoteRepository(object):
 
214
    """Repository accessed over rpc.
 
215
 
 
216
    For the moment most operations are performed using local transport-backed
 
217
    Repository objects.
 
218
    """
 
219
 
 
220
    def __init__(self, remote_bzrdir, format, real_repository=None, _client=None):
 
221
        """Create a RemoteRepository instance.
 
222
        
 
223
        :param remote_bzrdir: The bzrdir hosting this repository.
 
224
        :param format: The RemoteFormat object to use.
 
225
        :param real_repository: If not None, a local implementation of the
 
226
            repository logic for the repository, usually accessing the data
 
227
            via the VFS.
 
228
        :param _client: Private testing parameter - override the smart client
 
229
            to be used by the repository.
 
230
        """
 
231
        if real_repository:
 
232
            self._real_repository = real_repository
 
233
        else:
 
234
            self._real_repository = None
 
235
        self.bzrdir = remote_bzrdir
 
236
        if _client is None:
 
237
            self._client = client._SmartClient(self.bzrdir._medium)
 
238
        else:
 
239
            self._client = _client
 
240
        self._format = format
 
241
        self._lock_mode = None
 
242
        self._lock_token = None
 
243
        self._lock_count = 0
 
244
        self._leave_lock = False
 
245
 
 
246
    def _ensure_real(self):
 
247
        """Ensure that there is a _real_repository set.
 
248
 
 
249
        Used before calls to self._real_repository.
 
250
        """
 
251
        if not self._real_repository:
 
252
            self.bzrdir._ensure_real()
 
253
            #self._real_repository = self.bzrdir._real_bzrdir.open_repository()
 
254
            self._set_real_repository(self.bzrdir._real_bzrdir.open_repository())
 
255
 
 
256
    def get_revision_graph(self, revision_id=None):
 
257
        """See Repository.get_revision_graph()."""
 
258
        if revision_id is None:
 
259
            revision_id = ''
 
260
        elif revision_id == NULL_REVISION:
 
261
            return {}
 
262
 
 
263
        path = self.bzrdir._path_for_remote_call(self._client)
 
264
        assert type(revision_id) is str
 
265
        response = self._client.call_expecting_body(
 
266
            'Repository.get_revision_graph', path, revision_id)
 
267
        if response[0][0] not in ['ok', 'nosuchrevision']:
 
268
            raise errors.UnexpectedSmartServerResponse(response[0])
 
269
        if response[0][0] == 'ok':
 
270
            coded = response[1].read_body_bytes()
 
271
            if coded == '':
 
272
                # no revisions in this repository!
 
273
                return {}
 
274
            lines = coded.split('\n')
 
275
            revision_graph = {}
 
276
            for line in lines:
 
277
                d = list(line.split())
 
278
                revision_graph[d[0]] = d[1:]
 
279
                
 
280
            return revision_graph
 
281
        else:
 
282
            response_body = response[1].read_body_bytes()
 
283
            assert response_body == ''
 
284
            raise NoSuchRevision(self, revision_id)
 
285
 
 
286
    def has_revision(self, revision_id):
 
287
        """See Repository.has_revision()."""
 
288
        if revision_id is None:
 
289
            # The null revision is always present.
 
290
            return True
 
291
        path = self.bzrdir._path_for_remote_call(self._client)
 
292
        response = self._client.call('Repository.has_revision', path, revision_id)
 
293
        assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
 
294
        return response[0] == 'yes'
 
295
 
 
296
    def gather_stats(self, revid=None, committers=None):
 
297
        """See Repository.gather_stats()."""
 
298
        path = self.bzrdir._path_for_remote_call(self._client)
 
299
        if revid in (None, NULL_REVISION):
 
300
            fmt_revid = ''
 
301
        else:
 
302
            fmt_revid = revid
 
303
        if committers is None or not committers:
 
304
            fmt_committers = 'no'
 
305
        else:
 
306
            fmt_committers = 'yes'
 
307
        response = self._client.call_expecting_body(
 
308
            'Repository.gather_stats', path, fmt_revid, fmt_committers)
 
309
        assert response[0][0] == 'ok', \
 
310
            'unexpected response code %s' % (response[0],)
 
311
 
 
312
        body = response[1].read_body_bytes()
 
313
        result = {}
 
314
        for line in body.split('\n'):
 
315
            if not line:
 
316
                continue
 
317
            key, val_text = line.split(':')
 
318
            if key in ('revisions', 'size', 'committers'):
 
319
                result[key] = int(val_text)
 
320
            elif key in ('firstrev', 'latestrev'):
 
321
                values = val_text.split(' ')[1:]
 
322
                result[key] = (float(values[0]), long(values[1]))
 
323
 
 
324
        return result
 
325
 
 
326
    def get_physical_lock_status(self):
 
327
        """See Repository.get_physical_lock_status()."""
 
328
        return False
 
329
 
 
330
    def is_shared(self):
 
331
        """See Repository.is_shared()."""
 
332
        path = self.bzrdir._path_for_remote_call(self._client)
 
333
        response = self._client.call('Repository.is_shared', path)
 
334
        assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
 
335
        return response[0] == 'yes'
 
336
 
 
337
    def lock_read(self):
 
338
        # wrong eventually - want a local lock cache context
 
339
        if not self._lock_mode:
 
340
            self._lock_mode = 'r'
 
341
            self._lock_count = 1
 
342
            if self._real_repository is not None:
 
343
                self._real_repository.lock_read()
 
344
        else:
 
345
            self._lock_count += 1
 
346
 
 
347
    def _remote_lock_write(self, token):
 
348
        path = self.bzrdir._path_for_remote_call(self._client)
 
349
        if token is None:
 
350
            token = ''
 
351
        response = self._client.call('Repository.lock_write', path, token)
 
352
        if response[0] == 'ok':
 
353
            ok, token = response
 
354
            return token
 
355
        elif response[0] == 'LockContention':
 
356
            raise errors.LockContention('(remote lock)')
 
357
        elif response[0] == 'UnlockableTransport':
 
358
            raise errors.UnlockableTransport(self.bzrdir.root_transport)
 
359
        else:
 
360
            assert False, 'unexpected response code %s' % (response,)
 
361
 
 
362
    def lock_write(self, token=None):
 
363
        if not self._lock_mode:
 
364
            self._lock_token = self._remote_lock_write(token)
 
365
            assert self._lock_token, 'Remote server did not return a token!'
 
366
            if self._real_repository is not None:
 
367
                self._real_repository.lock_write(token=self._lock_token)
 
368
            if token is not None:
 
369
                self._leave_lock = True
 
370
            else:
 
371
                self._leave_lock = False
 
372
            self._lock_mode = 'w'
 
373
            self._lock_count = 1
 
374
        elif self._lock_mode == 'r':
 
375
            raise errors.ReadOnlyError(self)
 
376
        else:
 
377
            self._lock_count += 1
 
378
        return self._lock_token
 
379
 
 
380
    def leave_lock_in_place(self):
 
381
        self._leave_lock = True
 
382
 
 
383
    def dont_leave_lock_in_place(self):
 
384
        self._leave_lock = False
 
385
 
 
386
    def _set_real_repository(self, repository):
 
387
        """Set the _real_repository for this repository.
 
388
 
 
389
        :param repository: The repository to fallback to for non-hpss
 
390
            implemented operations.
 
391
        """
 
392
        assert not isinstance(repository, RemoteRepository)
 
393
        self._real_repository = repository
 
394
        if self._lock_mode == 'w':
 
395
            # if we are already locked, the real repository must be able to
 
396
            # acquire the lock with our token.
 
397
            self._real_repository.lock_write(self._lock_token)
 
398
        elif self._lock_mode == 'r':
 
399
            self._real_repository.lock_read()
 
400
 
 
401
    def _unlock(self, token):
 
402
        path = self.bzrdir._path_for_remote_call(self._client)
 
403
        response = self._client.call('Repository.unlock', path, token)
 
404
        if response == ('ok',):
 
405
            return
 
406
        elif response[0] == 'TokenMismatch':
 
407
            raise errors.TokenMismatch(token, '(remote token)')
 
408
        else:
 
409
            assert False, 'unexpected response code %s' % (response,)
 
410
 
 
411
    def unlock(self):
 
412
        self._lock_count -= 1
 
413
        if not self._lock_count:
 
414
            mode = self._lock_mode
 
415
            self._lock_mode = None
 
416
            if self._real_repository is not None:
 
417
                self._real_repository.unlock()
 
418
            if mode != 'w':
 
419
                # Only write-locked repositories need to make a remote method
 
420
                # call to perfom the unlock.
 
421
                return
 
422
            assert self._lock_token, 'Locked, but no token!'
 
423
            token = self._lock_token
 
424
            self._lock_token = None
 
425
            if not self._leave_lock:
 
426
                self._unlock(token)
 
427
 
 
428
    def break_lock(self):
 
429
        # should hand off to the network
 
430
        self._ensure_real()
 
431
        return self._real_repository.break_lock()
 
432
 
 
433
    ### These methods are just thin shims to the VFS object for now.
 
434
 
 
435
    def revision_tree(self, revision_id):
 
436
        self._ensure_real()
 
437
        return self._real_repository.revision_tree(revision_id)
 
438
 
 
439
    def get_commit_builder(self, branch, parents, config, timestamp=None,
 
440
                           timezone=None, committer=None, revprops=None,
 
441
                           revision_id=None):
 
442
        # FIXME: It ought to be possible to call this without immediately
 
443
        # triggering _ensure_real.  For now it's the easiest thing to do.
 
444
        self._ensure_real()
 
445
        builder = self._real_repository.get_commit_builder(branch, parents,
 
446
                config, timestamp=timestamp, timezone=timezone,
 
447
                committer=committer, revprops=revprops, revision_id=revision_id)
 
448
        # Make the builder use this RemoteRepository rather than the real one.
 
449
        builder.repository = self
 
450
        return builder
 
451
 
 
452
    @needs_write_lock
 
453
    def add_inventory(self, revid, inv, parents):
 
454
        self._ensure_real()
 
455
        return self._real_repository.add_inventory(revid, inv, parents)
 
456
 
 
457
    @needs_write_lock
 
458
    def add_revision(self, rev_id, rev, inv=None, config=None):
 
459
        self._ensure_real()
 
460
        return self._real_repository.add_revision(
 
461
            rev_id, rev, inv=inv, config=config)
 
462
 
 
463
    @needs_read_lock
 
464
    def get_inventory(self, revision_id):
 
465
        self._ensure_real()
 
466
        return self._real_repository.get_inventory(revision_id)
 
467
 
 
468
    @needs_read_lock
 
469
    def get_revision(self, revision_id):
 
470
        self._ensure_real()
 
471
        return self._real_repository.get_revision(revision_id)
 
472
 
 
473
    @property
 
474
    def weave_store(self):
 
475
        self._ensure_real()
 
476
        return self._real_repository.weave_store
 
477
 
 
478
    def get_transaction(self):
 
479
        self._ensure_real()
 
480
        return self._real_repository.get_transaction()
 
481
 
 
482
    @needs_read_lock
 
483
    def clone(self, a_bzrdir, revision_id=None):
 
484
        self._ensure_real()
 
485
        return self._real_repository.clone(a_bzrdir, revision_id=revision_id)
 
486
 
 
487
    def make_working_trees(self):
 
488
        """RemoteRepositories never create working trees by default."""
 
489
        return False
 
490
 
 
491
    def fetch(self, source, revision_id=None, pb=None):
 
492
        self._ensure_real()
 
493
        return self._real_repository.fetch(
 
494
            source, revision_id=revision_id, pb=pb)
 
495
 
 
496
    @property
 
497
    def control_weaves(self):
 
498
        self._ensure_real()
 
499
        return self._real_repository.control_weaves
 
500
 
 
501
    @needs_read_lock
 
502
    def get_ancestry(self, revision_id):
 
503
        self._ensure_real()
 
504
        return self._real_repository.get_ancestry(revision_id)
 
505
 
 
506
    @needs_read_lock
 
507
    def get_inventory_weave(self):
 
508
        self._ensure_real()
 
509
        return self._real_repository.get_inventory_weave()
 
510
 
 
511
    def fileids_altered_by_revision_ids(self, revision_ids):
 
512
        self._ensure_real()
 
513
        return self._real_repository.fileids_altered_by_revision_ids(revision_ids)
 
514
 
 
515
    @needs_read_lock
 
516
    def get_signature_text(self, revision_id):
 
517
        self._ensure_real()
 
518
        return self._real_repository.get_signature_text(revision_id)
 
519
 
 
520
    @needs_read_lock
 
521
    def get_revision_graph_with_ghosts(self, revision_ids=None):
 
522
        self._ensure_real()
 
523
        return self._real_repository.get_revision_graph_with_ghosts(
 
524
            revision_ids=revision_ids)
 
525
 
 
526
    @needs_read_lock
 
527
    def get_inventory_xml(self, revision_id):
 
528
        self._ensure_real()
 
529
        return self._real_repository.get_inventory_xml(revision_id)
 
530
 
 
531
    def deserialise_inventory(self, revision_id, xml):
 
532
        self._ensure_real()
 
533
        return self._real_repository.deserialise_inventory(revision_id, xml)
 
534
 
 
535
    def reconcile(self, other=None, thorough=False):
 
536
        self._ensure_real()
 
537
        return self._real_repository.reconcile(other=other, thorough=thorough)
 
538
        
 
539
    def all_revision_ids(self):
 
540
        self._ensure_real()
 
541
        return self._real_repository.all_revision_ids()
 
542
    
 
543
    @needs_read_lock
 
544
    def get_deltas_for_revisions(self, revisions):
 
545
        self._ensure_real()
 
546
        return self._real_repository.get_deltas_for_revisions(revisions)
 
547
 
 
548
    @needs_read_lock
 
549
    def get_revision_delta(self, revision_id):
 
550
        self._ensure_real()
 
551
        return self._real_repository.get_revision_delta(revision_id)
 
552
 
 
553
    @needs_read_lock
 
554
    def revision_trees(self, revision_ids):
 
555
        self._ensure_real()
 
556
        return self._real_repository.revision_trees(revision_ids)
 
557
 
 
558
    @needs_read_lock
 
559
    def get_revision_reconcile(self, revision_id):
 
560
        self._ensure_real()
 
561
        return self._real_repository.get_revision_reconcile(revision_id)
 
562
 
 
563
    @needs_read_lock
 
564
    def check(self, revision_ids):
 
565
        self._ensure_real()
 
566
        return self._real_repository.check(revision_ids)
 
567
 
 
568
    def copy_content_into(self, destination, revision_id=None):
 
569
        self._ensure_real()
 
570
        return self._real_repository.copy_content_into(
 
571
            destination, revision_id=revision_id)
 
572
 
 
573
    def set_make_working_trees(self, new_value):
 
574
        raise NotImplementedError(self.set_make_working_trees)
 
575
 
 
576
    @needs_write_lock
 
577
    def sign_revision(self, revision_id, gpg_strategy):
 
578
        self._ensure_real()
 
579
        return self._real_repository.sign_revision(revision_id, gpg_strategy)
 
580
 
 
581
    @needs_read_lock
 
582
    def get_revisions(self, revision_ids):
 
583
        self._ensure_real()
 
584
        return self._real_repository.get_revisions(revision_ids)
 
585
 
 
586
    def supports_rich_root(self):
 
587
        self._ensure_real()
 
588
        return self._real_repository.supports_rich_root()
 
589
 
 
590
    def iter_reverse_revision_history(self, revision_id):
 
591
        self._ensure_real()
 
592
        return self._real_repository.iter_reverse_revision_history(revision_id)
 
593
 
 
594
    @property
 
595
    def _serializer(self):
 
596
        self._ensure_real()
 
597
        return self._real_repository._serializer
 
598
 
 
599
    def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
 
600
        self._ensure_real()
 
601
        return self._real_repository.store_revision_signature(
 
602
            gpg_strategy, plaintext, revision_id)
 
603
 
 
604
    def has_signature_for_revision_id(self, revision_id):
 
605
        self._ensure_real()
 
606
        return self._real_repository.has_signature_for_revision_id(revision_id)
 
607
 
 
608
 
 
609
class RemoteBranchLockableFiles(LockableFiles):
 
610
    """A 'LockableFiles' implementation that talks to a smart server.
 
611
    
 
612
    This is not a public interface class.
 
613
    """
 
614
 
 
615
    def __init__(self, bzrdir, _client):
 
616
        self.bzrdir = bzrdir
 
617
        self._client = _client
 
618
        self._need_find_modes = True
 
619
        LockableFiles.__init__(
 
620
            self, bzrdir.get_branch_transport(None),
 
621
            'lock', lockdir.LockDir)
 
622
 
 
623
    def _find_modes(self):
 
624
        # RemoteBranches don't let the client set the mode of control files.
 
625
        self._dir_mode = None
 
626
        self._file_mode = None
 
627
 
 
628
    def get(self, path):
 
629
        """'get' a remote path as per the LockableFiles interface.
 
630
 
 
631
        :param path: the file to 'get'. If this is 'branch.conf', we do not
 
632
             just retrieve a file, instead we ask the smart server to generate
 
633
             a configuration for us - which is retrieved as an INI file.
 
634
        """
 
635
        if path == 'branch.conf':
 
636
            path = self.bzrdir._path_for_remote_call(self._client)
 
637
            response = self._client.call_expecting_body(
 
638
                'Branch.get_config_file', path)
 
639
            assert response[0][0] == 'ok', \
 
640
                'unexpected response code %s' % (response[0],)
 
641
            return StringIO(response[1].read_body_bytes())
 
642
        else:
 
643
            # VFS fallback.
 
644
            return LockableFiles.get(self, path)
 
645
 
 
646
 
 
647
class RemoteBranchFormat(branch.BranchFormat):
 
648
 
 
649
    def __eq__(self, other):
 
650
        return (isinstance(other, RemoteBranchFormat) and 
 
651
            self.__dict__ == other.__dict__)
 
652
 
 
653
    def get_format_description(self):
 
654
        return 'Remote BZR Branch'
 
655
 
 
656
    def get_format_string(self):
 
657
        return 'Remote BZR Branch'
 
658
 
 
659
    def open(self, a_bzrdir):
 
660
        assert isinstance(a_bzrdir, RemoteBzrDir)
 
661
        return a_bzrdir.open_branch()
 
662
 
 
663
    def initialize(self, a_bzrdir):
 
664
        assert isinstance(a_bzrdir, RemoteBzrDir)
 
665
        return a_bzrdir.create_branch()
 
666
 
 
667
 
 
668
class RemoteBranch(branch.Branch):
 
669
    """Branch stored on a server accessed by HPSS RPC.
 
670
 
 
671
    At the moment most operations are mapped down to simple file operations.
 
672
    """
 
673
 
 
674
    def __init__(self, remote_bzrdir, remote_repository, real_branch=None,
 
675
        _client=None):
 
676
        """Create a RemoteBranch instance.
 
677
 
 
678
        :param real_branch: An optional local implementation of the branch
 
679
            format, usually accessing the data via the VFS.
 
680
        :param _client: Private parameter for testing.
 
681
        """
 
682
        # We intentionally don't call the parent class's __init__, because it
 
683
        # will try to assign to self.tags, which is a property in this subclass.
 
684
        # And the parent's __init__ doesn't do much anyway.
 
685
        self._revision_history_cache = None
 
686
        self.bzrdir = remote_bzrdir
 
687
        if _client is not None:
 
688
            self._client = _client
 
689
        else:
 
690
            self._client = client._SmartClient(self.bzrdir._medium)
 
691
        self.repository = remote_repository
 
692
        if real_branch is not None:
 
693
            self._real_branch = real_branch
 
694
            # Give the remote repository the matching real repo.
 
695
            real_repo = self._real_branch.repository
 
696
            if isinstance(real_repo, RemoteRepository):
 
697
                real_repo._ensure_real()
 
698
                real_repo = real_repo._real_repository
 
699
            self.repository._set_real_repository(real_repo)
 
700
            # Give the branch the remote repository to let fast-pathing happen.
 
701
            self._real_branch.repository = self.repository
 
702
        else:
 
703
            self._real_branch = None
 
704
        # Fill out expected attributes of branch for bzrlib api users.
 
705
        self._format = RemoteBranchFormat()
 
706
        self.base = self.bzrdir.root_transport.base
 
707
        self._control_files = None
 
708
        self._lock_mode = None
 
709
        self._lock_token = None
 
710
        self._lock_count = 0
 
711
        self._leave_lock = False
 
712
 
 
713
    def _ensure_real(self):
 
714
        """Ensure that there is a _real_branch set.
 
715
 
 
716
        Used before calls to self._real_branch.
 
717
        """
 
718
        if not self._real_branch:
 
719
            assert vfs.vfs_enabled()
 
720
            self.bzrdir._ensure_real()
 
721
            self._real_branch = self.bzrdir._real_bzrdir.open_branch()
 
722
            # Give the remote repository the matching real repo.
 
723
            real_repo = self._real_branch.repository
 
724
            if isinstance(real_repo, RemoteRepository):
 
725
                real_repo._ensure_real()
 
726
                real_repo = real_repo._real_repository
 
727
            self.repository._set_real_repository(real_repo)
 
728
            # Give the branch the remote repository to let fast-pathing happen.
 
729
            self._real_branch.repository = self.repository
 
730
            # XXX: deal with _lock_mode == 'w'
 
731
            if self._lock_mode == 'r':
 
732
                self._real_branch.lock_read()
 
733
 
 
734
    @property
 
735
    def control_files(self):
 
736
        # Defer actually creating RemoteBranchLockableFiles until its needed,
 
737
        # because it triggers an _ensure_real that we otherwise might not need.
 
738
        if self._control_files is None:
 
739
            self._control_files = RemoteBranchLockableFiles(
 
740
                self.bzrdir, self._client)
 
741
        return self._control_files
 
742
 
 
743
    def _get_checkout_format(self):
 
744
        self._ensure_real()
 
745
        return self._real_branch._get_checkout_format()
 
746
 
 
747
    def get_physical_lock_status(self):
 
748
        """See Branch.get_physical_lock_status()."""
 
749
        # should be an API call to the server, as branches must be lockable.
 
750
        self._ensure_real()
 
751
        return self._real_branch.get_physical_lock_status()
 
752
 
 
753
    def lock_read(self):
 
754
        if not self._lock_mode:
 
755
            self._lock_mode = 'r'
 
756
            self._lock_count = 1
 
757
            if self._real_branch is not None:
 
758
                self._real_branch.lock_read()
 
759
        else:
 
760
            self._lock_count += 1
 
761
 
 
762
    def _remote_lock_write(self, token):
 
763
        if token is None:
 
764
            branch_token = repo_token = ''
 
765
        else:
 
766
            branch_token = token
 
767
            repo_token = self.repository.lock_write()
 
768
            self.repository.unlock()
 
769
        path = self.bzrdir._path_for_remote_call(self._client)
 
770
        response = self._client.call('Branch.lock_write', path, branch_token,
 
771
                                     repo_token)
 
772
        if response[0] == 'ok':
 
773
            ok, branch_token, repo_token = response
 
774
            return branch_token, repo_token
 
775
        elif response[0] == 'LockContention':
 
776
            raise errors.LockContention('(remote lock)')
 
777
        elif response[0] == 'TokenMismatch':
 
778
            raise errors.TokenMismatch(token, '(remote token)')
 
779
        elif response[0] == 'UnlockableTransport':
 
780
            raise errors.UnlockableTransport(self.bzrdir.root_transport)
 
781
        elif response[0] == 'ReadOnlyError':
 
782
            raise errors.ReadOnlyError(self)
 
783
        else:
 
784
            assert False, 'unexpected response code %r' % (response,)
 
785
            
 
786
    def lock_write(self, token=None):
 
787
        if not self._lock_mode:
 
788
            remote_tokens = self._remote_lock_write(token)
 
789
            self._lock_token, self._repo_lock_token = remote_tokens
 
790
            assert self._lock_token, 'Remote server did not return a token!'
 
791
            # TODO: We really, really, really don't want to call _ensure_real
 
792
            # here, but it's the easiest way to ensure coherency between the
 
793
            # state of the RemoteBranch and RemoteRepository objects and the
 
794
            # physical locks.  If we don't materialise the real objects here,
 
795
            # then getting everything in the right state later is complex, so
 
796
            # for now we just do it the lazy way.
 
797
            #   -- Andrew Bennetts, 2007-02-22.
 
798
            self._ensure_real()
 
799
            if self._real_branch is not None:
 
800
                self._real_branch.repository.lock_write(
 
801
                    token=self._repo_lock_token)
 
802
                try:
 
803
                    self._real_branch.lock_write(token=self._lock_token)
 
804
                finally:
 
805
                    self._real_branch.repository.unlock()
 
806
            if token is not None:
 
807
                self._leave_lock = True
 
808
            else:
 
809
                # XXX: this case seems to be unreachable; token cannot be None.
 
810
                self._leave_lock = False
 
811
            self._lock_mode = 'w'
 
812
            self._lock_count = 1
 
813
        elif self._lock_mode == 'r':
 
814
            raise errors.ReadOnlyTransaction
 
815
        else:
 
816
            if token is not None:
 
817
                # A token was given to lock_write, and we're relocking, so check
 
818
                # that the given token actually matches the one we already have.
 
819
                if token != self._lock_token:
 
820
                    raise errors.TokenMismatch(token, self._lock_token)
 
821
            self._lock_count += 1
 
822
        return self._lock_token
 
823
 
 
824
    def _unlock(self, branch_token, repo_token):
 
825
        path = self.bzrdir._path_for_remote_call(self._client)
 
826
        response = self._client.call('Branch.unlock', path, branch_token,
 
827
                                     repo_token)
 
828
        if response == ('ok',):
 
829
            return
 
830
        elif response[0] == 'TokenMismatch':
 
831
            raise errors.TokenMismatch(
 
832
                str((branch_token, repo_token)), '(remote tokens)')
 
833
        else:
 
834
            assert False, 'unexpected response code %s' % (response,)
 
835
 
 
836
    def unlock(self):
 
837
        self._lock_count -= 1
 
838
        if not self._lock_count:
 
839
            self._clear_cached_state()
 
840
            mode = self._lock_mode
 
841
            self._lock_mode = None
 
842
            if self._real_branch is not None:
 
843
                if not self._leave_lock:
 
844
                    # If this RemoteBranch will remove the physical lock for the
 
845
                    # repository, make sure the _real_branch doesn't do it
 
846
                    # first.  (Because the _real_branch's repository is set to
 
847
                    # be the RemoteRepository.)
 
848
                    self._real_branch.repository.leave_lock_in_place()
 
849
                self._real_branch.unlock()
 
850
            if mode != 'w':
 
851
                # Only write-locked branched need to make a remote method call
 
852
                # to perfom the unlock.
 
853
                return
 
854
            assert self._lock_token, 'Locked, but no token!'
 
855
            branch_token = self._lock_token
 
856
            repo_token = self._repo_lock_token
 
857
            self._lock_token = None
 
858
            self._repo_lock_token = None
 
859
            if not self._leave_lock:
 
860
                self._unlock(branch_token, repo_token)
 
861
 
 
862
    def break_lock(self):
 
863
        self._ensure_real()
 
864
        return self._real_branch.break_lock()
 
865
 
 
866
    def leave_lock_in_place(self):
 
867
        self._leave_lock = True
 
868
 
 
869
    def dont_leave_lock_in_place(self):
 
870
        self._leave_lock = False
 
871
 
 
872
    def last_revision_info(self):
 
873
        """See Branch.last_revision_info()."""
 
874
        path = self.bzrdir._path_for_remote_call(self._client)
 
875
        response = self._client.call('Branch.last_revision_info', path)
 
876
        assert response[0] == 'ok', 'unexpected response code %s' % (response,)
 
877
        revno = int(response[1])
 
878
        last_revision = response[2]
 
879
        return (revno, last_revision)
 
880
 
 
881
    def _gen_revision_history(self):
 
882
        """See Branch._gen_revision_history()."""
 
883
        path = self.bzrdir._path_for_remote_call(self._client)
 
884
        response = self._client.call_expecting_body(
 
885
            'Branch.revision_history', path)
 
886
        assert response[0][0] == 'ok', ('unexpected response code %s'
 
887
                                        % (response[0],))
 
888
        result = response[1].read_body_bytes().split('\x00')
 
889
        if result == ['']:
 
890
            return []
 
891
        return result
 
892
 
 
893
    @needs_write_lock
 
894
    def set_revision_history(self, rev_history):
 
895
        # Send just the tip revision of the history; the server will generate
 
896
        # the full history from that.  If the revision doesn't exist in this
 
897
        # branch, NoSuchRevision will be raised.
 
898
        path = self.bzrdir._path_for_remote_call(self._client)
 
899
        if rev_history == []:
 
900
            rev_id = 'null:'
 
901
        else:
 
902
            rev_id = rev_history[-1]
 
903
        self._clear_cached_state()
 
904
        response = self._client.call('Branch.set_last_revision',
 
905
            path, self._lock_token, self._repo_lock_token, rev_id)
 
906
        if response[0] == 'NoSuchRevision':
 
907
            raise NoSuchRevision(self, rev_id)
 
908
        else:
 
909
            assert response == ('ok',), (
 
910
                'unexpected response code %r' % (response,))
 
911
        self._cache_revision_history(rev_history)
 
912
 
 
913
    def get_parent(self):
 
914
        self._ensure_real()
 
915
        return self._real_branch.get_parent()
 
916
        
 
917
    def set_parent(self, url):
 
918
        self._ensure_real()
 
919
        return self._real_branch.set_parent(url)
 
920
        
 
921
    def get_config(self):
 
922
        return RemoteBranchConfig(self)
 
923
 
 
924
    def sprout(self, to_bzrdir, revision_id=None):
 
925
        # Like Branch.sprout, except that it sprouts a branch in the default
 
926
        # format, because RemoteBranches can't be created at arbitrary URLs.
 
927
        # XXX: if to_bzrdir is a RemoteBranch, this should perhaps do
 
928
        # to_bzrdir.create_branch...
 
929
        self._ensure_real()
 
930
        result = branch.BranchFormat.get_default_format().initialize(to_bzrdir)
 
931
        self._real_branch.copy_content_into(result, revision_id=revision_id)
 
932
        result.set_parent(self.bzrdir.root_transport.base)
 
933
        return result
 
934
 
 
935
    @needs_write_lock
 
936
    def append_revision(self, *revision_ids):
 
937
        self._ensure_real()
 
938
        return self._real_branch.append_revision(*revision_ids)
 
939
 
 
940
    @needs_write_lock
 
941
    def pull(self, source, overwrite=False, stop_revision=None):
 
942
        self._ensure_real()
 
943
        self._real_branch.pull(
 
944
            source, overwrite=overwrite, stop_revision=stop_revision)
 
945
 
 
946
    @needs_read_lock
 
947
    def push(self, target, overwrite=False, stop_revision=None):
 
948
        self._ensure_real()
 
949
        return self._real_branch.push(
 
950
            target, overwrite=overwrite, stop_revision=stop_revision)
 
951
 
 
952
    def is_locked(self):
 
953
        return self._lock_count >= 1
 
954
 
 
955
    def set_last_revision_info(self, revno, revision_id):
 
956
        self._ensure_real()
 
957
        self._clear_cached_state()
 
958
        return self._real_branch.set_last_revision_info(revno, revision_id)
 
959
 
 
960
    def generate_revision_history(self, revision_id, last_rev=None,
 
961
                                  other_branch=None):
 
962
        self._ensure_real()
 
963
        return self._real_branch.generate_revision_history(
 
964
            revision_id, last_rev=last_rev, other_branch=other_branch)
 
965
 
 
966
    @property
 
967
    def tags(self):
 
968
        self._ensure_real()
 
969
        return self._real_branch.tags
 
970
 
 
971
    def set_push_location(self, location):
 
972
        self._ensure_real()
 
973
        return self._real_branch.set_push_location(location)
 
974
 
 
975
    def update_revisions(self, other, stop_revision=None):
 
976
        self._ensure_real()
 
977
        return self._real_branch.update_revisions(
 
978
            other, stop_revision=stop_revision)
 
979
 
 
980
 
 
981
class RemoteBranchConfig(BranchConfig):
 
982
 
 
983
    def username(self):
 
984
        self.branch._ensure_real()
 
985
        return self.branch._real_branch.get_config().username()
 
986
 
 
987
    def _get_branch_data_config(self):
 
988
        self.branch._ensure_real()
 
989
        if self._branch_data_config is None:
 
990
            self._branch_data_config = TreeConfig(self.branch._real_branch)
 
991
        return self._branch_data_config
 
992