~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/remote.py

Merge bzr.dev.

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