1
# Copyright (C) 2006, 2007 Canonical Ltd
 
 
3
# This program is free software; you can redistribute it and/or modify
 
 
4
# it under the terms of the GNU General Public License as published by
 
 
5
# the Free Software Foundation; either version 2 of the License, or
 
 
6
# (at your option) any later version.
 
 
8
# This program is distributed in the hope that it will be useful,
 
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
 
11
# GNU General Public License for more details.
 
 
13
# You should have received a copy of the GNU General Public License
 
 
14
# along with this program; if not, write to the Free Software
 
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
 
17
# TODO: At some point, handle upgrades by just passing the whole request
 
 
18
# across to run on the server.
 
 
20
from cStringIO import StringIO
 
 
28
from bzrlib.branch import Branch, BranchReferenceFormat
 
 
29
from bzrlib.bzrdir import BzrDir, RemoteBzrDirFormat
 
 
30
from bzrlib.config import BranchConfig, TreeConfig
 
 
31
from bzrlib.decorators import needs_read_lock, needs_write_lock
 
 
32
from bzrlib.errors import NoSuchRevision
 
 
33
from bzrlib.lockable_files import LockableFiles
 
 
34
from bzrlib.revision import NULL_REVISION
 
 
35
from bzrlib.smart import client, vfs
 
 
36
from bzrlib.symbol_versioning import (
 
 
40
from bzrlib.trace import note
 
 
42
# Note: RemoteBzrDirFormat is in bzrdir.py
 
 
44
class RemoteBzrDir(BzrDir):
 
 
45
    """Control directory on a remote server, accessed via bzr:// or similar."""
 
 
47
    def __init__(self, transport, _client=None):
 
 
48
        """Construct a RemoteBzrDir.
 
 
50
        :param _client: Private parameter for testing. Disables probing and the
 
 
53
        BzrDir.__init__(self, transport, RemoteBzrDirFormat())
 
 
54
        # this object holds a delegated bzrdir that uses file-level operations
 
 
55
        # to talk to the other side
 
 
56
        self._real_bzrdir = None
 
 
59
            self._shared_medium = transport.get_shared_medium()
 
 
60
            self._client = client._SmartClient(self._shared_medium)
 
 
62
            self._client = _client
 
 
63
            self._shared_medium = None
 
 
66
        path = self._path_for_remote_call(self._client)
 
 
67
        response = self._client.call('BzrDir.open', path)
 
 
68
        if response not in [('yes',), ('no',)]:
 
 
69
            raise errors.UnexpectedSmartServerResponse(response)
 
 
70
        if response == ('no',):
 
 
71
            raise errors.NotBranchError(path=transport.base)
 
 
73
    def _ensure_real(self):
 
 
74
        """Ensure that there is a _real_bzrdir set.
 
 
76
        Used before calls to self._real_bzrdir.
 
 
78
        if not self._real_bzrdir:
 
 
79
            self._real_bzrdir = BzrDir.open_from_transport(
 
 
80
                self.root_transport, _server_formats=False)
 
 
82
    def create_repository(self, shared=False):
 
 
84
        self._real_bzrdir.create_repository(shared=shared)
 
 
85
        return self.open_repository()
 
 
87
    def create_branch(self):
 
 
89
        real_branch = self._real_bzrdir.create_branch()
 
 
90
        return RemoteBranch(self, self.find_repository(), real_branch)
 
 
92
    def destroy_branch(self):
 
 
93
        """See BzrDir.destroy_branch"""
 
 
95
        self._real_bzrdir.destroy_branch()
 
 
97
    def create_workingtree(self, revision_id=None):
 
 
98
        raise errors.NotLocalUrl(self.transport.base)
 
 
100
    def find_branch_format(self):
 
 
101
        """Find the branch 'format' for this bzrdir.
 
 
103
        This might be a synthetic object for e.g. RemoteBranch and SVN.
 
 
105
        b = self.open_branch()
 
 
108
    def get_branch_reference(self):
 
 
109
        """See BzrDir.get_branch_reference()."""
 
 
110
        path = self._path_for_remote_call(self._client)
 
 
111
        response = self._client.call('BzrDir.open_branch', path)
 
 
112
        if response[0] == 'ok':
 
 
113
            if response[1] == '':
 
 
114
                # branch at this location.
 
 
117
                # a branch reference, use the existing BranchReference logic.
 
 
119
        elif response == ('nobranch',):
 
 
120
            raise errors.NotBranchError(path=self.root_transport.base)
 
 
122
            raise errors.UnexpectedSmartServerResponse(response)
 
 
124
    def open_branch(self, _unsupported=False):
 
 
125
        assert _unsupported == False, 'unsupported flag support not implemented yet.'
 
 
126
        reference_url = self.get_branch_reference()
 
 
127
        if reference_url is None:
 
 
128
            # branch at this location.
 
 
129
            return RemoteBranch(self, self.find_repository())
 
 
131
            # a branch reference, use the existing BranchReference logic.
 
 
132
            format = BranchReferenceFormat()
 
 
133
            return format.open(self, _found=True, location=reference_url)
 
 
135
    def open_repository(self):
 
 
136
        path = self._path_for_remote_call(self._client)
 
 
137
        response = self._client.call('BzrDir.find_repository', path)
 
 
138
        assert response[0] in ('ok', 'norepository'), \
 
 
139
            'unexpected response code %s' % (response,)
 
 
140
        if response[0] == 'norepository':
 
 
141
            raise errors.NoRepositoryPresent(self)
 
 
142
        assert len(response) == 4, 'incorrect response length %s' % (response,)
 
 
143
        if response[1] == '':
 
 
144
            format = RemoteRepositoryFormat()
 
 
145
            format.rich_root_data = (response[2] == 'yes')
 
 
146
            format.supports_tree_reference = (response[3] == 'yes')
 
 
147
            return RemoteRepository(self, format)
 
 
149
            raise errors.NoRepositoryPresent(self)
 
 
151
    def open_workingtree(self, recommend_upgrade=True):
 
 
153
        if self._real_bzrdir.has_workingtree():
 
 
154
            raise errors.NotLocalUrl(self.root_transport)
 
 
156
            raise errors.NoWorkingTree(self.root_transport.base)
 
 
158
    def _path_for_remote_call(self, client):
 
 
159
        """Return the path to be used for this bzrdir in a remote call."""
 
 
160
        return client.remote_path_from_transport(self.root_transport)
 
 
162
    def get_branch_transport(self, branch_format):
 
 
164
        return self._real_bzrdir.get_branch_transport(branch_format)
 
 
166
    def get_repository_transport(self, repository_format):
 
 
168
        return self._real_bzrdir.get_repository_transport(repository_format)
 
 
170
    def get_workingtree_transport(self, workingtree_format):
 
 
172
        return self._real_bzrdir.get_workingtree_transport(workingtree_format)
 
 
174
    def can_convert_format(self):
 
 
175
        """Upgrading of remote bzrdirs is not supported yet."""
 
 
178
    def needs_format_conversion(self, format=None):
 
 
179
        """Upgrading of remote bzrdirs is not supported yet."""
 
 
182
    def clone(self, url, revision_id=None, force_new_repo=False):
 
 
184
        return self._real_bzrdir.clone(url, revision_id=revision_id,
 
 
185
            force_new_repo=force_new_repo)
 
 
188
class RemoteRepositoryFormat(repository.RepositoryFormat):
 
 
189
    """Format for repositories accessed over a _SmartClient.
 
 
191
    Instances of this repository are represented by RemoteRepository
 
 
194
    The RemoteRepositoryFormat is parameterised during construction
 
 
195
    to reflect the capabilities of the real, remote format. Specifically
 
 
196
    the attributes rich_root_data and supports_tree_reference are set
 
 
197
    on a per instance basis, and are not set (and should not be) at
 
 
201
    _matchingbzrdir = RemoteBzrDirFormat
 
 
203
    def initialize(self, a_bzrdir, shared=False):
 
 
204
        assert isinstance(a_bzrdir, RemoteBzrDir), \
 
 
205
            '%r is not a RemoteBzrDir' % (a_bzrdir,)
 
 
206
        return a_bzrdir.create_repository(shared=shared)
 
 
208
    def open(self, a_bzrdir):
 
 
209
        assert isinstance(a_bzrdir, RemoteBzrDir)
 
 
210
        return a_bzrdir.open_repository()
 
 
212
    def get_format_description(self):
 
 
213
        return 'bzr remote repository'
 
 
215
    def __eq__(self, other):
 
 
216
        return self.__class__ == other.__class__
 
 
218
    def check_conversion_target(self, target_format):
 
 
219
        if self.rich_root_data and not target_format.rich_root_data:
 
 
220
            raise errors.BadConversionTarget(
 
 
221
                'Does not support rich root data.', target_format)
 
 
222
        if (self.supports_tree_reference and
 
 
223
            not getattr(target_format, 'supports_tree_reference', False)):
 
 
224
            raise errors.BadConversionTarget(
 
 
225
                'Does not support nested trees', target_format)
 
 
228
class RemoteRepository(object):
 
 
229
    """Repository accessed over rpc.
 
 
231
    For the moment most operations are performed using local transport-backed
 
 
235
    def __init__(self, remote_bzrdir, format, real_repository=None, _client=None):
 
 
236
        """Create a RemoteRepository instance.
 
 
238
        :param remote_bzrdir: The bzrdir hosting this repository.
 
 
239
        :param format: The RemoteFormat object to use.
 
 
240
        :param real_repository: If not None, a local implementation of the
 
 
241
            repository logic for the repository, usually accessing the data
 
 
243
        :param _client: Private testing parameter - override the smart client
 
 
244
            to be used by the repository.
 
 
247
            self._real_repository = real_repository
 
 
249
            self._real_repository = None
 
 
250
        self.bzrdir = remote_bzrdir
 
 
252
            self._client = client._SmartClient(self.bzrdir._shared_medium)
 
 
254
            self._client = _client
 
 
255
        self._format = format
 
 
256
        self._lock_mode = None
 
 
257
        self._lock_token = None
 
 
259
        self._leave_lock = False
 
 
261
        self._reconcile_does_inventory_gc = True
 
 
263
    def abort_write_group(self):
 
 
264
        """Complete a write group on the decorated repository.
 
 
266
        Smart methods peform operations in a single step so this api
 
 
267
        is not really applicable except as a compatibility thunk
 
 
268
        for older plugins that don't use e.g. the CommitBuilder
 
 
272
        return self._real_repository.abort_write_group()
 
 
274
    def commit_write_group(self):
 
 
275
        """Complete a write group on the decorated repository.
 
 
277
        Smart methods peform operations in a single step so this api
 
 
278
        is not really applicable except as a compatibility thunk
 
 
279
        for older plugins that don't use e.g. the CommitBuilder
 
 
283
        return self._real_repository.commit_write_group()
 
 
285
    def _ensure_real(self):
 
 
286
        """Ensure that there is a _real_repository set.
 
 
288
        Used before calls to self._real_repository.
 
 
290
        if not self._real_repository:
 
 
291
            self.bzrdir._ensure_real()
 
 
292
            #self._real_repository = self.bzrdir._real_bzrdir.open_repository()
 
 
293
            self._set_real_repository(self.bzrdir._real_bzrdir.open_repository())
 
 
295
    def get_revision_graph(self, revision_id=None):
 
 
296
        """See Repository.get_revision_graph()."""
 
 
297
        if revision_id is None:
 
 
299
        elif revision_id == NULL_REVISION:
 
 
302
        path = self.bzrdir._path_for_remote_call(self._client)
 
 
303
        assert type(revision_id) is str
 
 
304
        response = self._client.call_expecting_body(
 
 
305
            'Repository.get_revision_graph', path, revision_id)
 
 
306
        if response[0][0] not in ['ok', 'nosuchrevision']:
 
 
307
            raise errors.UnexpectedSmartServerResponse(response[0])
 
 
308
        if response[0][0] == 'ok':
 
 
309
            coded = response[1].read_body_bytes()
 
 
311
                # no revisions in this repository!
 
 
313
            lines = coded.split('\n')
 
 
316
                d = tuple(line.split())
 
 
317
                revision_graph[d[0]] = d[1:]
 
 
319
            return revision_graph
 
 
321
            response_body = response[1].read_body_bytes()
 
 
322
            assert response_body == ''
 
 
323
            raise NoSuchRevision(self, revision_id)
 
 
325
    def has_revision(self, revision_id):
 
 
326
        """See Repository.has_revision()."""
 
 
327
        if revision_id is None:
 
 
328
            # The null revision is always present.
 
 
330
        path = self.bzrdir._path_for_remote_call(self._client)
 
 
331
        response = self._client.call('Repository.has_revision', path, revision_id)
 
 
332
        assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
 
 
333
        return response[0] == 'yes'
 
 
335
    def has_same_location(self, other):
 
 
336
        return (self.__class__ == other.__class__ and
 
 
337
                self.bzrdir.transport.base == other.bzrdir.transport.base)
 
 
339
    def get_graph(self, other_repository=None):
 
 
340
        """Return the graph for this repository format"""
 
 
341
        return self._real_repository.get_graph(other_repository)
 
 
343
    def gather_stats(self, revid=None, committers=None):
 
 
344
        """See Repository.gather_stats()."""
 
 
345
        path = self.bzrdir._path_for_remote_call(self._client)
 
 
346
        if revid in (None, NULL_REVISION):
 
 
350
        if committers is None or not committers:
 
 
351
            fmt_committers = 'no'
 
 
353
            fmt_committers = 'yes'
 
 
354
        response = self._client.call_expecting_body(
 
 
355
            'Repository.gather_stats', path, fmt_revid, fmt_committers)
 
 
356
        assert response[0][0] == 'ok', \
 
 
357
            'unexpected response code %s' % (response[0],)
 
 
359
        body = response[1].read_body_bytes()
 
 
361
        for line in body.split('\n'):
 
 
364
            key, val_text = line.split(':')
 
 
365
            if key in ('revisions', 'size', 'committers'):
 
 
366
                result[key] = int(val_text)
 
 
367
            elif key in ('firstrev', 'latestrev'):
 
 
368
                values = val_text.split(' ')[1:]
 
 
369
                result[key] = (float(values[0]), long(values[1]))
 
 
373
    def get_physical_lock_status(self):
 
 
374
        """See Repository.get_physical_lock_status()."""
 
 
377
    def is_in_write_group(self):
 
 
378
        """Return True if there is an open write group.
 
 
380
        write groups are only applicable locally for the smart server..
 
 
382
        if self._real_repository:
 
 
383
            return self._real_repository.is_in_write_group()
 
 
386
        return self._lock_count >= 1
 
 
389
        """See Repository.is_shared()."""
 
 
390
        path = self.bzrdir._path_for_remote_call(self._client)
 
 
391
        response = self._client.call('Repository.is_shared', path)
 
 
392
        assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
 
 
393
        return response[0] == 'yes'
 
 
396
        # wrong eventually - want a local lock cache context
 
 
397
        if not self._lock_mode:
 
 
398
            self._lock_mode = 'r'
 
 
400
            if self._real_repository is not None:
 
 
401
                self._real_repository.lock_read()
 
 
403
            self._lock_count += 1
 
 
405
    def _remote_lock_write(self, token):
 
 
406
        path = self.bzrdir._path_for_remote_call(self._client)
 
 
409
        response = self._client.call('Repository.lock_write', path, token)
 
 
410
        if response[0] == 'ok':
 
 
413
        elif response[0] == 'LockContention':
 
 
414
            raise errors.LockContention('(remote lock)')
 
 
415
        elif response[0] == 'UnlockableTransport':
 
 
416
            raise errors.UnlockableTransport(self.bzrdir.root_transport)
 
 
418
            raise errors.UnexpectedSmartServerResponse(response)
 
 
420
    def lock_write(self, token=None):
 
 
421
        if not self._lock_mode:
 
 
422
            self._lock_token = self._remote_lock_write(token)
 
 
423
            assert self._lock_token, 'Remote server did not return a token!'
 
 
424
            if self._real_repository is not None:
 
 
425
                self._real_repository.lock_write(token=self._lock_token)
 
 
426
            if token is not None:
 
 
427
                self._leave_lock = True
 
 
429
                self._leave_lock = False
 
 
430
            self._lock_mode = 'w'
 
 
432
        elif self._lock_mode == 'r':
 
 
433
            raise errors.ReadOnlyError(self)
 
 
435
            self._lock_count += 1
 
 
436
        return self._lock_token
 
 
438
    def leave_lock_in_place(self):
 
 
439
        self._leave_lock = True
 
 
441
    def dont_leave_lock_in_place(self):
 
 
442
        self._leave_lock = False
 
 
444
    def _set_real_repository(self, repository):
 
 
445
        """Set the _real_repository for this repository.
 
 
447
        :param repository: The repository to fallback to for non-hpss
 
 
448
            implemented operations.
 
 
450
        assert not isinstance(repository, RemoteRepository)
 
 
451
        self._real_repository = repository
 
 
452
        if self._lock_mode == 'w':
 
 
453
            # if we are already locked, the real repository must be able to
 
 
454
            # acquire the lock with our token.
 
 
455
            self._real_repository.lock_write(self._lock_token)
 
 
456
        elif self._lock_mode == 'r':
 
 
457
            self._real_repository.lock_read()
 
 
459
    def start_write_group(self):
 
 
460
        """Start a write group on the decorated repository.
 
 
462
        Smart methods peform operations in a single step so this api
 
 
463
        is not really applicable except as a compatibility thunk
 
 
464
        for older plugins that don't use e.g. the CommitBuilder
 
 
468
        return self._real_repository.start_write_group()
 
 
470
    def _unlock(self, token):
 
 
471
        path = self.bzrdir._path_for_remote_call(self._client)
 
 
472
        response = self._client.call('Repository.unlock', path, token)
 
 
473
        if response == ('ok',):
 
 
475
        elif response[0] == 'TokenMismatch':
 
 
476
            raise errors.TokenMismatch(token, '(remote token)')
 
 
478
            raise errors.UnexpectedSmartServerResponse(response)
 
 
481
        if self._lock_count == 1 and self._lock_mode == 'w':
 
 
482
            # don't unlock if inside a write group.
 
 
483
            if self.is_in_write_group():
 
 
484
                raise errors.BzrError(
 
 
485
                    'Must end write groups before releasing write locks.')
 
 
486
        self._lock_count -= 1
 
 
487
        if not self._lock_count:
 
 
488
            mode = self._lock_mode
 
 
489
            self._lock_mode = None
 
 
490
            if self._real_repository is not None:
 
 
491
                self._real_repository.unlock()
 
 
493
                # Only write-locked repositories need to make a remote method
 
 
494
                # call to perfom the unlock.
 
 
496
            assert self._lock_token, 'Locked, but no token!'
 
 
497
            token = self._lock_token
 
 
498
            self._lock_token = None
 
 
499
            if not self._leave_lock:
 
 
502
    def break_lock(self):
 
 
503
        # should hand off to the network
 
 
505
        return self._real_repository.break_lock()
 
 
507
    def _get_tarball(self, compression):
 
 
508
        """Return a TemporaryFile containing a repository tarball"""
 
 
510
        path = self.bzrdir._path_for_remote_call(self._client)
 
 
511
        response, protocol = self._client.call_expecting_body(
 
 
512
            'Repository.tarball', path, compression)
 
 
513
        assert response[0] in ('ok', 'failure'), \
 
 
514
            'unexpected response code %s' % (response,)
 
 
515
        if response[0] == 'ok':
 
 
516
            # Extract the tarball and return it
 
 
517
            t = tempfile.NamedTemporaryFile()
 
 
518
            # TODO: rpc layer should read directly into it...
 
 
519
            t.write(protocol.read_body_bytes())
 
 
523
            raise errors.SmartServerError(error_code=response)
 
 
525
    def sprout(self, to_bzrdir, revision_id=None):
 
 
526
        # TODO: Option to control what format is created?
 
 
527
        to_repo = to_bzrdir.create_repository()
 
 
528
        self._copy_repository_tarball(to_repo, revision_id)
 
 
531
    ### These methods are just thin shims to the VFS object for now.
 
 
533
    def revision_tree(self, revision_id):
 
 
535
        return self._real_repository.revision_tree(revision_id)
 
 
537
    def get_serializer_format(self):
 
 
539
        return self._real_repository.get_serializer_format()
 
 
541
    def get_commit_builder(self, branch, parents, config, timestamp=None,
 
 
542
                           timezone=None, committer=None, revprops=None,
 
 
544
        # FIXME: It ought to be possible to call this without immediately
 
 
545
        # triggering _ensure_real.  For now it's the easiest thing to do.
 
 
547
        builder = self._real_repository.get_commit_builder(branch, parents,
 
 
548
                config, timestamp=timestamp, timezone=timezone,
 
 
549
                committer=committer, revprops=revprops, revision_id=revision_id)
 
 
550
        # Make the builder use this RemoteRepository rather than the real one.
 
 
551
        builder.repository = self
 
 
555
    def add_inventory(self, revid, inv, parents):
 
 
557
        return self._real_repository.add_inventory(revid, inv, parents)
 
 
560
    def add_revision(self, rev_id, rev, inv=None, config=None):
 
 
562
        return self._real_repository.add_revision(
 
 
563
            rev_id, rev, inv=inv, config=config)
 
 
566
    def get_inventory(self, revision_id):
 
 
568
        return self._real_repository.get_inventory(revision_id)
 
 
571
    def get_revision(self, revision_id):
 
 
573
        return self._real_repository.get_revision(revision_id)
 
 
576
    def weave_store(self):
 
 
578
        return self._real_repository.weave_store
 
 
580
    def get_transaction(self):
 
 
582
        return self._real_repository.get_transaction()
 
 
585
    def clone(self, a_bzrdir, revision_id=None):
 
 
587
        return self._real_repository.clone(a_bzrdir, revision_id=revision_id)
 
 
589
    def make_working_trees(self):
 
 
590
        """RemoteRepositories never create working trees by default."""
 
 
593
    def fetch(self, source, revision_id=None, pb=None):
 
 
595
        return self._real_repository.fetch(
 
 
596
            source, revision_id=revision_id, pb=pb)
 
 
598
    def create_bundle(self, target, base, fileobj, format=None):
 
 
600
        self._real_repository.create_bundle(target, base, fileobj, format)
 
 
603
    def control_weaves(self):
 
 
605
        return self._real_repository.control_weaves
 
 
608
    def get_ancestry(self, revision_id, topo_sorted=True):
 
 
610
        return self._real_repository.get_ancestry(revision_id, topo_sorted)
 
 
613
    def get_inventory_weave(self):
 
 
615
        return self._real_repository.get_inventory_weave()
 
 
617
    def fileids_altered_by_revision_ids(self, revision_ids):
 
 
619
        return self._real_repository.fileids_altered_by_revision_ids(revision_ids)
 
 
621
    def iter_files_bytes(self, desired_files):
 
 
622
        """See Repository.iter_file_bytes.
 
 
625
        return self._real_repository.iter_files_bytes(desired_files)
 
 
628
    def get_signature_text(self, revision_id):
 
 
630
        return self._real_repository.get_signature_text(revision_id)
 
 
633
    def get_revision_graph_with_ghosts(self, revision_ids=None):
 
 
635
        return self._real_repository.get_revision_graph_with_ghosts(
 
 
636
            revision_ids=revision_ids)
 
 
639
    def get_inventory_xml(self, revision_id):
 
 
641
        return self._real_repository.get_inventory_xml(revision_id)
 
 
643
    def deserialise_inventory(self, revision_id, xml):
 
 
645
        return self._real_repository.deserialise_inventory(revision_id, xml)
 
 
647
    def reconcile(self, other=None, thorough=False):
 
 
649
        return self._real_repository.reconcile(other=other, thorough=thorough)
 
 
651
    def all_revision_ids(self):
 
 
653
        return self._real_repository.all_revision_ids()
 
 
656
    def get_deltas_for_revisions(self, revisions):
 
 
658
        return self._real_repository.get_deltas_for_revisions(revisions)
 
 
661
    def get_revision_delta(self, revision_id):
 
 
663
        return self._real_repository.get_revision_delta(revision_id)
 
 
666
    def revision_trees(self, revision_ids):
 
 
668
        return self._real_repository.revision_trees(revision_ids)
 
 
671
    def get_revision_reconcile(self, revision_id):
 
 
673
        return self._real_repository.get_revision_reconcile(revision_id)
 
 
676
    def check(self, revision_ids):
 
 
678
        return self._real_repository.check(revision_ids)
 
 
680
    def copy_content_into(self, destination, revision_id=None):
 
 
682
        return self._real_repository.copy_content_into(
 
 
683
            destination, revision_id=revision_id)
 
 
685
    def _copy_repository_tarball(self, destination, revision_id=None):
 
 
686
        # get a tarball of the remote repository, and copy from that into the
 
 
688
        from bzrlib import osutils
 
 
691
        from StringIO import StringIO
 
 
692
        # TODO: Maybe a progress bar while streaming the tarball?
 
 
693
        note("Copying repository content as tarball...")
 
 
694
        tar_file = self._get_tarball('bz2')
 
 
696
            tar = tarfile.open('repository', fileobj=tar_file,
 
 
698
            tmpdir = tempfile.mkdtemp()
 
 
700
                _extract_tar(tar, tmpdir)
 
 
701
                tmp_bzrdir = BzrDir.open(tmpdir)
 
 
702
                tmp_repo = tmp_bzrdir.open_repository()
 
 
703
                tmp_repo.copy_content_into(destination, revision_id)
 
 
705
                osutils.rmtree(tmpdir)
 
 
708
        # TODO: if the server doesn't support this operation, maybe do it the
 
 
709
        # slow way using the _real_repository?
 
 
711
        # TODO: Suggestion from john: using external tar is much faster than
 
 
712
        # python's tarfile library, but it may not work on windows.
 
 
716
        """Compress the data within the repository.
 
 
718
        This is not currently implemented within the smart server.
 
 
721
        return self._real_repository.pack()
 
 
723
    def set_make_working_trees(self, new_value):
 
 
724
        raise NotImplementedError(self.set_make_working_trees)
 
 
727
    def sign_revision(self, revision_id, gpg_strategy):
 
 
729
        return self._real_repository.sign_revision(revision_id, gpg_strategy)
 
 
732
    def get_revisions(self, revision_ids):
 
 
734
        return self._real_repository.get_revisions(revision_ids)
 
 
736
    def supports_rich_root(self):
 
 
738
        return self._real_repository.supports_rich_root()
 
 
740
    def iter_reverse_revision_history(self, revision_id):
 
 
742
        return self._real_repository.iter_reverse_revision_history(revision_id)
 
 
745
    def _serializer(self):
 
 
747
        return self._real_repository._serializer
 
 
749
    def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
 
 
751
        return self._real_repository.store_revision_signature(
 
 
752
            gpg_strategy, plaintext, revision_id)
 
 
754
    def has_signature_for_revision_id(self, revision_id):
 
 
756
        return self._real_repository.has_signature_for_revision_id(revision_id)
 
 
759
class RemoteBranchLockableFiles(LockableFiles):
 
 
760
    """A 'LockableFiles' implementation that talks to a smart server.
 
 
762
    This is not a public interface class.
 
 
765
    def __init__(self, bzrdir, _client):
 
 
767
        self._client = _client
 
 
768
        self._need_find_modes = True
 
 
769
        LockableFiles.__init__(
 
 
770
            self, bzrdir.get_branch_transport(None),
 
 
771
            'lock', lockdir.LockDir)
 
 
773
    def _find_modes(self):
 
 
774
        # RemoteBranches don't let the client set the mode of control files.
 
 
775
        self._dir_mode = None
 
 
776
        self._file_mode = None
 
 
779
        """'get' a remote path as per the LockableFiles interface.
 
 
781
        :param path: the file to 'get'. If this is 'branch.conf', we do not
 
 
782
             just retrieve a file, instead we ask the smart server to generate
 
 
783
             a configuration for us - which is retrieved as an INI file.
 
 
785
        if path == 'branch.conf':
 
 
786
            path = self.bzrdir._path_for_remote_call(self._client)
 
 
787
            response = self._client.call_expecting_body(
 
 
788
                'Branch.get_config_file', path)
 
 
789
            assert response[0][0] == 'ok', \
 
 
790
                'unexpected response code %s' % (response[0],)
 
 
791
            return StringIO(response[1].read_body_bytes())
 
 
794
            return LockableFiles.get(self, path)
 
 
797
class RemoteBranchFormat(branch.BranchFormat):
 
 
799
    def __eq__(self, other):
 
 
800
        return (isinstance(other, RemoteBranchFormat) and 
 
 
801
            self.__dict__ == other.__dict__)
 
 
803
    def get_format_description(self):
 
 
804
        return 'Remote BZR Branch'
 
 
806
    def get_format_string(self):
 
 
807
        return 'Remote BZR Branch'
 
 
809
    def open(self, a_bzrdir):
 
 
810
        assert isinstance(a_bzrdir, RemoteBzrDir)
 
 
811
        return a_bzrdir.open_branch()
 
 
813
    def initialize(self, a_bzrdir):
 
 
814
        assert isinstance(a_bzrdir, RemoteBzrDir)
 
 
815
        return a_bzrdir.create_branch()
 
 
817
    def supports_tags(self):
 
 
818
        # Remote branches might support tags, but we won't know until we
 
 
819
        # access the real remote branch.
 
 
823
class RemoteBranch(branch.Branch):
 
 
824
    """Branch stored on a server accessed by HPSS RPC.
 
 
826
    At the moment most operations are mapped down to simple file operations.
 
 
829
    def __init__(self, remote_bzrdir, remote_repository, real_branch=None,
 
 
831
        """Create a RemoteBranch instance.
 
 
833
        :param real_branch: An optional local implementation of the branch
 
 
834
            format, usually accessing the data via the VFS.
 
 
835
        :param _client: Private parameter for testing.
 
 
837
        # We intentionally don't call the parent class's __init__, because it
 
 
838
        # will try to assign to self.tags, which is a property in this subclass.
 
 
839
        # And the parent's __init__ doesn't do much anyway.
 
 
840
        self._revision_history_cache = None
 
 
841
        self.bzrdir = remote_bzrdir
 
 
842
        if _client is not None:
 
 
843
            self._client = _client
 
 
845
            self._client = client._SmartClient(self.bzrdir._shared_medium)
 
 
846
        self.repository = remote_repository
 
 
847
        if real_branch is not None:
 
 
848
            self._real_branch = real_branch
 
 
849
            # Give the remote repository the matching real repo.
 
 
850
            real_repo = self._real_branch.repository
 
 
851
            if isinstance(real_repo, RemoteRepository):
 
 
852
                real_repo._ensure_real()
 
 
853
                real_repo = real_repo._real_repository
 
 
854
            self.repository._set_real_repository(real_repo)
 
 
855
            # Give the branch the remote repository to let fast-pathing happen.
 
 
856
            self._real_branch.repository = self.repository
 
 
858
            self._real_branch = None
 
 
859
        # Fill out expected attributes of branch for bzrlib api users.
 
 
860
        self._format = RemoteBranchFormat()
 
 
861
        self.base = self.bzrdir.root_transport.base
 
 
862
        self._control_files = None
 
 
863
        self._lock_mode = None
 
 
864
        self._lock_token = None
 
 
866
        self._leave_lock = False
 
 
869
        return "%s(%s)" % (self.__class__.__name__, self.base)
 
 
873
    def _ensure_real(self):
 
 
874
        """Ensure that there is a _real_branch set.
 
 
876
        Used before calls to self._real_branch.
 
 
878
        if not self._real_branch:
 
 
879
            assert vfs.vfs_enabled()
 
 
880
            self.bzrdir._ensure_real()
 
 
881
            self._real_branch = self.bzrdir._real_bzrdir.open_branch()
 
 
882
            # Give the remote repository the matching real repo.
 
 
883
            real_repo = self._real_branch.repository
 
 
884
            if isinstance(real_repo, RemoteRepository):
 
 
885
                real_repo._ensure_real()
 
 
886
                real_repo = real_repo._real_repository
 
 
887
            self.repository._set_real_repository(real_repo)
 
 
888
            # Give the branch the remote repository to let fast-pathing happen.
 
 
889
            self._real_branch.repository = self.repository
 
 
890
            # XXX: deal with _lock_mode == 'w'
 
 
891
            if self._lock_mode == 'r':
 
 
892
                self._real_branch.lock_read()
 
 
895
    def control_files(self):
 
 
896
        # Defer actually creating RemoteBranchLockableFiles until its needed,
 
 
897
        # because it triggers an _ensure_real that we otherwise might not need.
 
 
898
        if self._control_files is None:
 
 
899
            self._control_files = RemoteBranchLockableFiles(
 
 
900
                self.bzrdir, self._client)
 
 
901
        return self._control_files
 
 
903
    def _get_checkout_format(self):
 
 
905
        return self._real_branch._get_checkout_format()
 
 
907
    def get_physical_lock_status(self):
 
 
908
        """See Branch.get_physical_lock_status()."""
 
 
909
        # should be an API call to the server, as branches must be lockable.
 
 
911
        return self._real_branch.get_physical_lock_status()
 
 
914
        if not self._lock_mode:
 
 
915
            self._lock_mode = 'r'
 
 
917
            if self._real_branch is not None:
 
 
918
                self._real_branch.lock_read()
 
 
920
            self._lock_count += 1
 
 
922
    def _remote_lock_write(self, token):
 
 
924
            branch_token = repo_token = ''
 
 
927
            repo_token = self.repository.lock_write()
 
 
928
            self.repository.unlock()
 
 
929
        path = self.bzrdir._path_for_remote_call(self._client)
 
 
930
        response = self._client.call('Branch.lock_write', path, branch_token,
 
 
932
        if response[0] == 'ok':
 
 
933
            ok, branch_token, repo_token = response
 
 
934
            return branch_token, repo_token
 
 
935
        elif response[0] == 'LockContention':
 
 
936
            raise errors.LockContention('(remote lock)')
 
 
937
        elif response[0] == 'TokenMismatch':
 
 
938
            raise errors.TokenMismatch(token, '(remote token)')
 
 
939
        elif response[0] == 'UnlockableTransport':
 
 
940
            raise errors.UnlockableTransport(self.bzrdir.root_transport)
 
 
941
        elif response[0] == 'ReadOnlyError':
 
 
942
            raise errors.ReadOnlyError(self)
 
 
944
            raise errors.UnexpectedSmartServerResponse(response)
 
 
946
    def lock_write(self, token=None):
 
 
947
        if not self._lock_mode:
 
 
948
            remote_tokens = self._remote_lock_write(token)
 
 
949
            self._lock_token, self._repo_lock_token = remote_tokens
 
 
950
            assert self._lock_token, 'Remote server did not return a token!'
 
 
951
            # TODO: We really, really, really don't want to call _ensure_real
 
 
952
            # here, but it's the easiest way to ensure coherency between the
 
 
953
            # state of the RemoteBranch and RemoteRepository objects and the
 
 
954
            # physical locks.  If we don't materialise the real objects here,
 
 
955
            # then getting everything in the right state later is complex, so
 
 
956
            # for now we just do it the lazy way.
 
 
957
            #   -- Andrew Bennetts, 2007-02-22.
 
 
959
            if self._real_branch is not None:
 
 
960
                self._real_branch.repository.lock_write(
 
 
961
                    token=self._repo_lock_token)
 
 
963
                    self._real_branch.lock_write(token=self._lock_token)
 
 
965
                    self._real_branch.repository.unlock()
 
 
966
            if token is not None:
 
 
967
                self._leave_lock = True
 
 
969
                # XXX: this case seems to be unreachable; token cannot be None.
 
 
970
                self._leave_lock = False
 
 
971
            self._lock_mode = 'w'
 
 
973
        elif self._lock_mode == 'r':
 
 
974
            raise errors.ReadOnlyTransaction
 
 
976
            if token is not None:
 
 
977
                # A token was given to lock_write, and we're relocking, so check
 
 
978
                # that the given token actually matches the one we already have.
 
 
979
                if token != self._lock_token:
 
 
980
                    raise errors.TokenMismatch(token, self._lock_token)
 
 
981
            self._lock_count += 1
 
 
982
        return self._lock_token
 
 
984
    def _unlock(self, branch_token, repo_token):
 
 
985
        path = self.bzrdir._path_for_remote_call(self._client)
 
 
986
        response = self._client.call('Branch.unlock', path, branch_token,
 
 
988
        if response == ('ok',):
 
 
990
        elif response[0] == 'TokenMismatch':
 
 
991
            raise errors.TokenMismatch(
 
 
992
                str((branch_token, repo_token)), '(remote tokens)')
 
 
994
            raise errors.UnexpectedSmartServerResponse(response)
 
 
997
        self._lock_count -= 1
 
 
998
        if not self._lock_count:
 
 
999
            self._clear_cached_state()
 
 
1000
            mode = self._lock_mode
 
 
1001
            self._lock_mode = None
 
 
1002
            if self._real_branch is not None:
 
 
1003
                if not self._leave_lock:
 
 
1004
                    # If this RemoteBranch will remove the physical lock for the
 
 
1005
                    # repository, make sure the _real_branch doesn't do it
 
 
1006
                    # first.  (Because the _real_branch's repository is set to
 
 
1007
                    # be the RemoteRepository.)
 
 
1008
                    self._real_branch.repository.leave_lock_in_place()
 
 
1009
                self._real_branch.unlock()
 
 
1011
                # Only write-locked branched need to make a remote method call
 
 
1012
                # to perfom the unlock.
 
 
1014
            assert self._lock_token, 'Locked, but no token!'
 
 
1015
            branch_token = self._lock_token
 
 
1016
            repo_token = self._repo_lock_token
 
 
1017
            self._lock_token = None
 
 
1018
            self._repo_lock_token = None
 
 
1019
            if not self._leave_lock:
 
 
1020
                self._unlock(branch_token, repo_token)
 
 
1022
    def break_lock(self):
 
 
1024
        return self._real_branch.break_lock()
 
 
1026
    def leave_lock_in_place(self):
 
 
1027
        self._leave_lock = True
 
 
1029
    def dont_leave_lock_in_place(self):
 
 
1030
        self._leave_lock = False
 
 
1032
    def last_revision_info(self):
 
 
1033
        """See Branch.last_revision_info()."""
 
 
1034
        path = self.bzrdir._path_for_remote_call(self._client)
 
 
1035
        response = self._client.call('Branch.last_revision_info', path)
 
 
1036
        assert response[0] == 'ok', 'unexpected response code %s' % (response,)
 
 
1037
        revno = int(response[1])
 
 
1038
        last_revision = response[2]
 
 
1039
        return (revno, last_revision)
 
 
1041
    def _gen_revision_history(self):
 
 
1042
        """See Branch._gen_revision_history()."""
 
 
1043
        path = self.bzrdir._path_for_remote_call(self._client)
 
 
1044
        response = self._client.call_expecting_body(
 
 
1045
            'Branch.revision_history', path)
 
 
1046
        assert response[0][0] == 'ok', ('unexpected response code %s'
 
 
1048
        result = response[1].read_body_bytes().split('\x00')
 
 
1054
    def set_revision_history(self, rev_history):
 
 
1055
        # Send just the tip revision of the history; the server will generate
 
 
1056
        # the full history from that.  If the revision doesn't exist in this
 
 
1057
        # branch, NoSuchRevision will be raised.
 
 
1058
        path = self.bzrdir._path_for_remote_call(self._client)
 
 
1059
        if rev_history == []:
 
 
1062
            rev_id = rev_history[-1]
 
 
1063
        self._clear_cached_state()
 
 
1064
        response = self._client.call('Branch.set_last_revision',
 
 
1065
            path, self._lock_token, self._repo_lock_token, rev_id)
 
 
1066
        if response[0] == 'NoSuchRevision':
 
 
1067
            raise NoSuchRevision(self, rev_id)
 
 
1069
            assert response == ('ok',), (
 
 
1070
                'unexpected response code %r' % (response,))
 
 
1071
        self._cache_revision_history(rev_history)
 
 
1073
    def get_parent(self):
 
 
1075
        return self._real_branch.get_parent()
 
 
1077
    def set_parent(self, url):
 
 
1079
        return self._real_branch.set_parent(url)
 
 
1081
    def get_config(self):
 
 
1082
        return RemoteBranchConfig(self)
 
 
1084
    def sprout(self, to_bzrdir, revision_id=None):
 
 
1085
        # Like Branch.sprout, except that it sprouts a branch in the default
 
 
1086
        # format, because RemoteBranches can't be created at arbitrary URLs.
 
 
1087
        # XXX: if to_bzrdir is a RemoteBranch, this should perhaps do
 
 
1088
        # to_bzrdir.create_branch...
 
 
1089
        result = branch.BranchFormat.get_default_format().initialize(to_bzrdir)
 
 
1090
        self.copy_content_into(result, revision_id=revision_id)
 
 
1091
        result.set_parent(self.bzrdir.root_transport.base)
 
 
1095
    def pull(self, source, overwrite=False, stop_revision=None,
 
 
1097
        # FIXME: This asks the real branch to run the hooks, which means
 
 
1098
        # they're called with the wrong target branch parameter. 
 
 
1099
        # The test suite specifically allows this at present but it should be
 
 
1100
        # fixed.  It should get a _override_hook_target branch,
 
 
1101
        # as push does.  -- mbp 20070405
 
 
1103
        self._real_branch.pull(
 
 
1104
            source, overwrite=overwrite, stop_revision=stop_revision,
 
 
1108
    def push(self, target, overwrite=False, stop_revision=None):
 
 
1110
        return self._real_branch.push(
 
 
1111
            target, overwrite=overwrite, stop_revision=stop_revision,
 
 
1112
            _override_hook_source_branch=self)
 
 
1114
    def is_locked(self):
 
 
1115
        return self._lock_count >= 1
 
 
1117
    def set_last_revision_info(self, revno, revision_id):
 
 
1119
        self._clear_cached_state()
 
 
1120
        return self._real_branch.set_last_revision_info(revno, revision_id)
 
 
1122
    def generate_revision_history(self, revision_id, last_rev=None,
 
 
1125
        return self._real_branch.generate_revision_history(
 
 
1126
            revision_id, last_rev=last_rev, other_branch=other_branch)
 
 
1131
        return self._real_branch.tags
 
 
1133
    def set_push_location(self, location):
 
 
1135
        return self._real_branch.set_push_location(location)
 
 
1137
    def update_revisions(self, other, stop_revision=None):
 
 
1139
        return self._real_branch.update_revisions(
 
 
1140
            other, stop_revision=stop_revision)
 
 
1143
class RemoteBranchConfig(BranchConfig):
 
 
1146
        self.branch._ensure_real()
 
 
1147
        return self.branch._real_branch.get_config().username()
 
 
1149
    def _get_branch_data_config(self):
 
 
1150
        self.branch._ensure_real()
 
 
1151
        if self._branch_data_config is None:
 
 
1152
            self._branch_data_config = TreeConfig(self.branch._real_branch)
 
 
1153
        return self._branch_data_config
 
 
1156
def _extract_tar(tar, to_dir):
 
 
1157
    """Extract all the contents of a tarfile object.
 
 
1159
    A replacement for extractall, which is not present in python2.4
 
 
1162
        tar.extract(tarinfo, to_dir)