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 create_workingtree(self, revision_id=None):
 
 
93
        raise errors.NotLocalUrl(self.transport.base)
 
 
95
    def find_branch_format(self):
 
 
96
        """Find the branch 'format' for this bzrdir.
 
 
98
        This might be a synthetic object for e.g. RemoteBranch and SVN.
 
 
100
        b = self.open_branch()
 
 
103
    def get_branch_reference(self):
 
 
104
        """See BzrDir.get_branch_reference()."""
 
 
105
        path = self._path_for_remote_call(self._client)
 
 
106
        response = self._client.call('BzrDir.open_branch', path)
 
 
107
        if response[0] == 'ok':
 
 
108
            if response[1] == '':
 
 
109
                # branch at this location.
 
 
112
                # a branch reference, use the existing BranchReference logic.
 
 
114
        elif response == ('nobranch',):
 
 
115
            raise errors.NotBranchError(path=self.root_transport.base)
 
 
117
            raise errors.UnexpectedSmartServerResponse(response)
 
 
119
    def open_branch(self, _unsupported=False):
 
 
120
        assert _unsupported == False, 'unsupported flag support not implemented yet.'
 
 
121
        reference_url = self.get_branch_reference()
 
 
122
        if reference_url is None:
 
 
123
            # branch at this location.
 
 
124
            return RemoteBranch(self, self.find_repository())
 
 
126
            # a branch reference, use the existing BranchReference logic.
 
 
127
            format = BranchReferenceFormat()
 
 
128
            return format.open(self, _found=True, location=reference_url)
 
 
130
    def open_repository(self):
 
 
131
        path = self._path_for_remote_call(self._client)
 
 
132
        response = self._client.call('BzrDir.find_repository', path)
 
 
133
        assert response[0] in ('ok', 'norepository'), \
 
 
134
            'unexpected response code %s' % (response,)
 
 
135
        if response[0] == 'norepository':
 
 
136
            raise errors.NoRepositoryPresent(self)
 
 
137
        assert len(response) == 4, 'incorrect response length %s' % (response,)
 
 
138
        if response[1] == '':
 
 
139
            format = RemoteRepositoryFormat()
 
 
140
            format.rich_root_data = (response[2] == 'yes')
 
 
141
            format.supports_tree_reference = (response[3] == 'yes')
 
 
142
            return RemoteRepository(self, format)
 
 
144
            raise errors.NoRepositoryPresent(self)
 
 
146
    def open_workingtree(self, recommend_upgrade=True):
 
 
148
        if self._real_bzrdir.has_workingtree():
 
 
149
            raise errors.NotLocalUrl(self.root_transport)
 
 
151
            raise errors.NoWorkingTree(self.root_transport.base)
 
 
153
    def _path_for_remote_call(self, client):
 
 
154
        """Return the path to be used for this bzrdir in a remote call."""
 
 
155
        return client.remote_path_from_transport(self.root_transport)
 
 
157
    def get_branch_transport(self, branch_format):
 
 
159
        return self._real_bzrdir.get_branch_transport(branch_format)
 
 
161
    def get_repository_transport(self, repository_format):
 
 
163
        return self._real_bzrdir.get_repository_transport(repository_format)
 
 
165
    def get_workingtree_transport(self, workingtree_format):
 
 
167
        return self._real_bzrdir.get_workingtree_transport(workingtree_format)
 
 
169
    def can_convert_format(self):
 
 
170
        """Upgrading of remote bzrdirs is not supported yet."""
 
 
173
    def needs_format_conversion(self, format=None):
 
 
174
        """Upgrading of remote bzrdirs is not supported yet."""
 
 
177
    def clone(self, url, revision_id=None, force_new_repo=False):
 
 
179
        return self._real_bzrdir.clone(url, revision_id=revision_id,
 
 
180
            force_new_repo=force_new_repo)
 
 
183
class RemoteRepositoryFormat(repository.RepositoryFormat):
 
 
184
    """Format for repositories accessed over a _SmartClient.
 
 
186
    Instances of this repository are represented by RemoteRepository
 
 
189
    The RemoteRepositoryFormat is parameterised during construction
 
 
190
    to reflect the capabilities of the real, remote format. Specifically
 
 
191
    the attributes rich_root_data and supports_tree_reference are set
 
 
192
    on a per instance basis, and are not set (and should not be) at
 
 
196
    _matchingbzrdir = RemoteBzrDirFormat
 
 
198
    def initialize(self, a_bzrdir, shared=False):
 
 
199
        assert isinstance(a_bzrdir, RemoteBzrDir), \
 
 
200
            '%r is not a RemoteBzrDir' % (a_bzrdir,)
 
 
201
        return a_bzrdir.create_repository(shared=shared)
 
 
203
    def open(self, a_bzrdir):
 
 
204
        assert isinstance(a_bzrdir, RemoteBzrDir)
 
 
205
        return a_bzrdir.open_repository()
 
 
207
    def get_format_description(self):
 
 
208
        return 'bzr remote repository'
 
 
210
    def __eq__(self, other):
 
 
211
        return self.__class__ == other.__class__
 
 
213
    def check_conversion_target(self, target_format):
 
 
214
        if self.rich_root_data and not target_format.rich_root_data:
 
 
215
            raise errors.BadConversionTarget(
 
 
216
                'Does not support rich root data.', target_format)
 
 
217
        if (self.supports_tree_reference and
 
 
218
            not getattr(target_format, 'supports_tree_reference', False)):
 
 
219
            raise errors.BadConversionTarget(
 
 
220
                'Does not support nested trees', target_format)
 
 
223
class RemoteRepository(object):
 
 
224
    """Repository accessed over rpc.
 
 
226
    For the moment most operations are performed using local transport-backed
 
 
230
    def __init__(self, remote_bzrdir, format, real_repository=None, _client=None):
 
 
231
        """Create a RemoteRepository instance.
 
 
233
        :param remote_bzrdir: The bzrdir hosting this repository.
 
 
234
        :param format: The RemoteFormat object to use.
 
 
235
        :param real_repository: If not None, a local implementation of the
 
 
236
            repository logic for the repository, usually accessing the data
 
 
238
        :param _client: Private testing parameter - override the smart client
 
 
239
            to be used by the repository.
 
 
242
            self._real_repository = real_repository
 
 
244
            self._real_repository = None
 
 
245
        self.bzrdir = remote_bzrdir
 
 
247
            self._client = client._SmartClient(self.bzrdir._shared_medium)
 
 
249
            self._client = _client
 
 
250
        self._format = format
 
 
251
        self._lock_mode = None
 
 
252
        self._lock_token = None
 
 
254
        self._leave_lock = False
 
 
256
        self._reconcile_does_inventory_gc = True
 
 
258
    def abort_write_group(self):
 
 
259
        """Complete a write group on the decorated repository.
 
 
261
        Smart methods peform operations in a single step so this api
 
 
262
        is not really applicable except as a compatibility thunk
 
 
263
        for older plugins that don't use e.g. the CommitBuilder
 
 
267
        return self._real_repository.abort_write_group()
 
 
269
    def commit_write_group(self):
 
 
270
        """Complete a write group on the decorated repository.
 
 
272
        Smart methods peform operations in a single step so this api
 
 
273
        is not really applicable except as a compatibility thunk
 
 
274
        for older plugins that don't use e.g. the CommitBuilder
 
 
278
        return self._real_repository.commit_write_group()
 
 
280
    def _ensure_real(self):
 
 
281
        """Ensure that there is a _real_repository set.
 
 
283
        Used before calls to self._real_repository.
 
 
285
        if not self._real_repository:
 
 
286
            self.bzrdir._ensure_real()
 
 
287
            #self._real_repository = self.bzrdir._real_bzrdir.open_repository()
 
 
288
            self._set_real_repository(self.bzrdir._real_bzrdir.open_repository())
 
 
290
    def get_revision_graph(self, revision_id=None):
 
 
291
        """See Repository.get_revision_graph()."""
 
 
292
        if revision_id is None:
 
 
294
        elif revision_id == NULL_REVISION:
 
 
297
        path = self.bzrdir._path_for_remote_call(self._client)
 
 
298
        assert type(revision_id) is str
 
 
299
        response = self._client.call_expecting_body(
 
 
300
            'Repository.get_revision_graph', path, revision_id)
 
 
301
        if response[0][0] not in ['ok', 'nosuchrevision']:
 
 
302
            raise errors.UnexpectedSmartServerResponse(response[0])
 
 
303
        if response[0][0] == 'ok':
 
 
304
            coded = response[1].read_body_bytes()
 
 
306
                # no revisions in this repository!
 
 
308
            lines = coded.split('\n')
 
 
311
                d = tuple(line.split())
 
 
312
                revision_graph[d[0]] = d[1:]
 
 
314
            return revision_graph
 
 
316
            response_body = response[1].read_body_bytes()
 
 
317
            assert response_body == ''
 
 
318
            raise NoSuchRevision(self, revision_id)
 
 
320
    def has_revision(self, revision_id):
 
 
321
        """See Repository.has_revision()."""
 
 
322
        if revision_id is None:
 
 
323
            # The null revision is always present.
 
 
325
        path = self.bzrdir._path_for_remote_call(self._client)
 
 
326
        response = self._client.call('Repository.has_revision', path, revision_id)
 
 
327
        assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
 
 
328
        return response[0] == 'yes'
 
 
330
    def has_same_location(self, other):
 
 
331
        return (self.__class__ == other.__class__ and
 
 
332
                self.bzrdir.transport.base == other.bzrdir.transport.base)
 
 
334
    def get_graph(self, other_repository=None):
 
 
335
        """Return the graph for this repository format"""
 
 
336
        return self._real_repository.get_graph(other_repository)
 
 
338
    def gather_stats(self, revid=None, committers=None):
 
 
339
        """See Repository.gather_stats()."""
 
 
340
        path = self.bzrdir._path_for_remote_call(self._client)
 
 
341
        if revid in (None, NULL_REVISION):
 
 
345
        if committers is None or not committers:
 
 
346
            fmt_committers = 'no'
 
 
348
            fmt_committers = 'yes'
 
 
349
        response = self._client.call_expecting_body(
 
 
350
            'Repository.gather_stats', path, fmt_revid, fmt_committers)
 
 
351
        assert response[0][0] == 'ok', \
 
 
352
            'unexpected response code %s' % (response[0],)
 
 
354
        body = response[1].read_body_bytes()
 
 
356
        for line in body.split('\n'):
 
 
359
            key, val_text = line.split(':')
 
 
360
            if key in ('revisions', 'size', 'committers'):
 
 
361
                result[key] = int(val_text)
 
 
362
            elif key in ('firstrev', 'latestrev'):
 
 
363
                values = val_text.split(' ')[1:]
 
 
364
                result[key] = (float(values[0]), long(values[1]))
 
 
368
    def get_physical_lock_status(self):
 
 
369
        """See Repository.get_physical_lock_status()."""
 
 
372
    def is_in_write_group(self):
 
 
373
        """Return True if there is an open write group.
 
 
375
        write groups are only applicable locally for the smart server..
 
 
377
        if self._real_repository:
 
 
378
            return self._real_repository.is_in_write_group()
 
 
381
        return self._lock_count >= 1
 
 
384
        """See Repository.is_shared()."""
 
 
385
        path = self.bzrdir._path_for_remote_call(self._client)
 
 
386
        response = self._client.call('Repository.is_shared', path)
 
 
387
        assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
 
 
388
        return response[0] == 'yes'
 
 
391
        # wrong eventually - want a local lock cache context
 
 
392
        if not self._lock_mode:
 
 
393
            self._lock_mode = 'r'
 
 
395
            if self._real_repository is not None:
 
 
396
                self._real_repository.lock_read()
 
 
398
            self._lock_count += 1
 
 
400
    def _remote_lock_write(self, token):
 
 
401
        path = self.bzrdir._path_for_remote_call(self._client)
 
 
404
        response = self._client.call('Repository.lock_write', path, token)
 
 
405
        if response[0] == 'ok':
 
 
408
        elif response[0] == 'LockContention':
 
 
409
            raise errors.LockContention('(remote lock)')
 
 
410
        elif response[0] == 'UnlockableTransport':
 
 
411
            raise errors.UnlockableTransport(self.bzrdir.root_transport)
 
 
413
            raise errors.UnexpectedSmartServerResponse(response)
 
 
415
    def lock_write(self, token=None):
 
 
416
        if not self._lock_mode:
 
 
417
            self._lock_token = self._remote_lock_write(token)
 
 
418
            assert self._lock_token, 'Remote server did not return a token!'
 
 
419
            if self._real_repository is not None:
 
 
420
                self._real_repository.lock_write(token=self._lock_token)
 
 
421
            if token is not None:
 
 
422
                self._leave_lock = True
 
 
424
                self._leave_lock = False
 
 
425
            self._lock_mode = 'w'
 
 
427
        elif self._lock_mode == 'r':
 
 
428
            raise errors.ReadOnlyError(self)
 
 
430
            self._lock_count += 1
 
 
431
        return self._lock_token
 
 
433
    def leave_lock_in_place(self):
 
 
434
        self._leave_lock = True
 
 
436
    def dont_leave_lock_in_place(self):
 
 
437
        self._leave_lock = False
 
 
439
    def _set_real_repository(self, repository):
 
 
440
        """Set the _real_repository for this repository.
 
 
442
        :param repository: The repository to fallback to for non-hpss
 
 
443
            implemented operations.
 
 
445
        assert not isinstance(repository, RemoteRepository)
 
 
446
        self._real_repository = repository
 
 
447
        if self._lock_mode == 'w':
 
 
448
            # if we are already locked, the real repository must be able to
 
 
449
            # acquire the lock with our token.
 
 
450
            self._real_repository.lock_write(self._lock_token)
 
 
451
        elif self._lock_mode == 'r':
 
 
452
            self._real_repository.lock_read()
 
 
454
    def start_write_group(self):
 
 
455
        """Start a write group on the decorated repository.
 
 
457
        Smart methods peform operations in a single step so this api
 
 
458
        is not really applicable except as a compatibility thunk
 
 
459
        for older plugins that don't use e.g. the CommitBuilder
 
 
463
        return self._real_repository.start_write_group()
 
 
465
    def _unlock(self, token):
 
 
466
        path = self.bzrdir._path_for_remote_call(self._client)
 
 
467
        response = self._client.call('Repository.unlock', path, token)
 
 
468
        if response == ('ok',):
 
 
470
        elif response[0] == 'TokenMismatch':
 
 
471
            raise errors.TokenMismatch(token, '(remote token)')
 
 
473
            raise errors.UnexpectedSmartServerResponse(response)
 
 
476
        if self._lock_count == 1 and self._lock_mode == 'w':
 
 
477
            # don't unlock if inside a write group.
 
 
478
            if self.is_in_write_group():
 
 
479
                raise errors.BzrError(
 
 
480
                    'Must end write groups before releasing write locks.')
 
 
481
        self._lock_count -= 1
 
 
482
        if not self._lock_count:
 
 
483
            mode = self._lock_mode
 
 
484
            self._lock_mode = None
 
 
485
            if self._real_repository is not None:
 
 
486
                self._real_repository.unlock()
 
 
488
                # Only write-locked repositories need to make a remote method
 
 
489
                # call to perfom the unlock.
 
 
491
            assert self._lock_token, 'Locked, but no token!'
 
 
492
            token = self._lock_token
 
 
493
            self._lock_token = None
 
 
494
            if not self._leave_lock:
 
 
497
    def break_lock(self):
 
 
498
        # should hand off to the network
 
 
500
        return self._real_repository.break_lock()
 
 
502
    def _get_tarball(self, compression):
 
 
503
        """Return a TemporaryFile containing a repository tarball"""
 
 
505
        path = self.bzrdir._path_for_remote_call(self._client)
 
 
506
        response, protocol = self._client.call_expecting_body(
 
 
507
            'Repository.tarball', path, compression)
 
 
508
        assert response[0] in ('ok', 'failure'), \
 
 
509
            'unexpected response code %s' % (response,)
 
 
510
        if response[0] == 'ok':
 
 
511
            # Extract the tarball and return it
 
 
512
            t = tempfile.NamedTemporaryFile()
 
 
513
            # TODO: rpc layer should read directly into it...
 
 
514
            t.write(protocol.read_body_bytes())
 
 
518
            raise errors.SmartServerError(error_code=response)
 
 
520
    def sprout(self, to_bzrdir, revision_id=None):
 
 
521
        # TODO: Option to control what format is created?
 
 
522
        to_repo = to_bzrdir.create_repository()
 
 
523
        self._copy_repository_tarball(to_repo, revision_id)
 
 
526
    ### These methods are just thin shims to the VFS object for now.
 
 
528
    def revision_tree(self, revision_id):
 
 
530
        return self._real_repository.revision_tree(revision_id)
 
 
532
    def get_serializer_format(self):
 
 
534
        return self._real_repository.get_serializer_format()
 
 
536
    def get_commit_builder(self, branch, parents, config, timestamp=None,
 
 
537
                           timezone=None, committer=None, revprops=None,
 
 
539
        # FIXME: It ought to be possible to call this without immediately
 
 
540
        # triggering _ensure_real.  For now it's the easiest thing to do.
 
 
542
        builder = self._real_repository.get_commit_builder(branch, parents,
 
 
543
                config, timestamp=timestamp, timezone=timezone,
 
 
544
                committer=committer, revprops=revprops, revision_id=revision_id)
 
 
545
        # Make the builder use this RemoteRepository rather than the real one.
 
 
546
        builder.repository = self
 
 
550
    def add_inventory(self, revid, inv, parents):
 
 
552
        return self._real_repository.add_inventory(revid, inv, parents)
 
 
555
    def add_revision(self, rev_id, rev, inv=None, config=None):
 
 
557
        return self._real_repository.add_revision(
 
 
558
            rev_id, rev, inv=inv, config=config)
 
 
561
    def get_inventory(self, revision_id):
 
 
563
        return self._real_repository.get_inventory(revision_id)
 
 
566
    def get_revision(self, revision_id):
 
 
568
        return self._real_repository.get_revision(revision_id)
 
 
571
    def weave_store(self):
 
 
573
        return self._real_repository.weave_store
 
 
575
    def get_transaction(self):
 
 
577
        return self._real_repository.get_transaction()
 
 
580
    def clone(self, a_bzrdir, revision_id=None):
 
 
582
        return self._real_repository.clone(a_bzrdir, revision_id=revision_id)
 
 
584
    def make_working_trees(self):
 
 
585
        """RemoteRepositories never create working trees by default."""
 
 
588
    def fetch(self, source, revision_id=None, pb=None):
 
 
590
        return self._real_repository.fetch(
 
 
591
            source, revision_id=revision_id, pb=pb)
 
 
593
    def create_bundle(self, target, base, fileobj, format=None):
 
 
595
        self._real_repository.create_bundle(target, base, fileobj, format)
 
 
598
    def control_weaves(self):
 
 
600
        return self._real_repository.control_weaves
 
 
603
    def get_ancestry(self, revision_id, topo_sorted=True):
 
 
605
        return self._real_repository.get_ancestry(revision_id, topo_sorted)
 
 
608
    def get_inventory_weave(self):
 
 
610
        return self._real_repository.get_inventory_weave()
 
 
612
    def fileids_altered_by_revision_ids(self, revision_ids):
 
 
614
        return self._real_repository.fileids_altered_by_revision_ids(revision_ids)
 
 
616
    def iter_files_bytes(self, desired_files):
 
 
617
        """See Repository.iter_file_bytes.
 
 
620
        return self._real_repository.iter_files_bytes(desired_files)
 
 
623
    def get_signature_text(self, revision_id):
 
 
625
        return self._real_repository.get_signature_text(revision_id)
 
 
628
    def get_revision_graph_with_ghosts(self, revision_ids=None):
 
 
630
        return self._real_repository.get_revision_graph_with_ghosts(
 
 
631
            revision_ids=revision_ids)
 
 
634
    def get_inventory_xml(self, revision_id):
 
 
636
        return self._real_repository.get_inventory_xml(revision_id)
 
 
638
    def deserialise_inventory(self, revision_id, xml):
 
 
640
        return self._real_repository.deserialise_inventory(revision_id, xml)
 
 
642
    def reconcile(self, other=None, thorough=False):
 
 
644
        return self._real_repository.reconcile(other=other, thorough=thorough)
 
 
646
    def all_revision_ids(self):
 
 
648
        return self._real_repository.all_revision_ids()
 
 
651
    def get_deltas_for_revisions(self, revisions):
 
 
653
        return self._real_repository.get_deltas_for_revisions(revisions)
 
 
656
    def get_revision_delta(self, revision_id):
 
 
658
        return self._real_repository.get_revision_delta(revision_id)
 
 
661
    def revision_trees(self, revision_ids):
 
 
663
        return self._real_repository.revision_trees(revision_ids)
 
 
666
    def get_revision_reconcile(self, revision_id):
 
 
668
        return self._real_repository.get_revision_reconcile(revision_id)
 
 
671
    def check(self, revision_ids):
 
 
673
        return self._real_repository.check(revision_ids)
 
 
675
    def copy_content_into(self, destination, revision_id=None):
 
 
677
        return self._real_repository.copy_content_into(
 
 
678
            destination, revision_id=revision_id)
 
 
680
    def _copy_repository_tarball(self, destination, revision_id=None):
 
 
681
        # get a tarball of the remote repository, and copy from that into the
 
 
683
        from bzrlib import osutils
 
 
686
        from StringIO import StringIO
 
 
687
        # TODO: Maybe a progress bar while streaming the tarball?
 
 
688
        note("Copying repository content as tarball...")
 
 
689
        tar_file = self._get_tarball('bz2')
 
 
691
            tar = tarfile.open('repository', fileobj=tar_file,
 
 
693
            tmpdir = tempfile.mkdtemp()
 
 
695
                _extract_tar(tar, tmpdir)
 
 
696
                tmp_bzrdir = BzrDir.open(tmpdir)
 
 
697
                tmp_repo = tmp_bzrdir.open_repository()
 
 
698
                tmp_repo.copy_content_into(destination, revision_id)
 
 
700
                osutils.rmtree(tmpdir)
 
 
703
        # TODO: if the server doesn't support this operation, maybe do it the
 
 
704
        # slow way using the _real_repository?
 
 
706
        # TODO: Suggestion from john: using external tar is much faster than
 
 
707
        # python's tarfile library, but it may not work on windows.
 
 
711
        """Compress the data within the repository.
 
 
713
        This is not currently implemented within the smart server.
 
 
716
        return self._real_repository.pack()
 
 
718
    def set_make_working_trees(self, new_value):
 
 
719
        raise NotImplementedError(self.set_make_working_trees)
 
 
722
    def sign_revision(self, revision_id, gpg_strategy):
 
 
724
        return self._real_repository.sign_revision(revision_id, gpg_strategy)
 
 
727
    def get_revisions(self, revision_ids):
 
 
729
        return self._real_repository.get_revisions(revision_ids)
 
 
731
    def supports_rich_root(self):
 
 
733
        return self._real_repository.supports_rich_root()
 
 
735
    def iter_reverse_revision_history(self, revision_id):
 
 
737
        return self._real_repository.iter_reverse_revision_history(revision_id)
 
 
740
    def _serializer(self):
 
 
742
        return self._real_repository._serializer
 
 
744
    def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
 
 
746
        return self._real_repository.store_revision_signature(
 
 
747
            gpg_strategy, plaintext, revision_id)
 
 
749
    def has_signature_for_revision_id(self, revision_id):
 
 
751
        return self._real_repository.has_signature_for_revision_id(revision_id)
 
 
754
class RemoteBranchLockableFiles(LockableFiles):
 
 
755
    """A 'LockableFiles' implementation that talks to a smart server.
 
 
757
    This is not a public interface class.
 
 
760
    def __init__(self, bzrdir, _client):
 
 
762
        self._client = _client
 
 
763
        self._need_find_modes = True
 
 
764
        LockableFiles.__init__(
 
 
765
            self, bzrdir.get_branch_transport(None),
 
 
766
            'lock', lockdir.LockDir)
 
 
768
    def _find_modes(self):
 
 
769
        # RemoteBranches don't let the client set the mode of control files.
 
 
770
        self._dir_mode = None
 
 
771
        self._file_mode = None
 
 
774
        """'get' a remote path as per the LockableFiles interface.
 
 
776
        :param path: the file to 'get'. If this is 'branch.conf', we do not
 
 
777
             just retrieve a file, instead we ask the smart server to generate
 
 
778
             a configuration for us - which is retrieved as an INI file.
 
 
780
        if path == 'branch.conf':
 
 
781
            path = self.bzrdir._path_for_remote_call(self._client)
 
 
782
            response = self._client.call_expecting_body(
 
 
783
                'Branch.get_config_file', path)
 
 
784
            assert response[0][0] == 'ok', \
 
 
785
                'unexpected response code %s' % (response[0],)
 
 
786
            return StringIO(response[1].read_body_bytes())
 
 
789
            return LockableFiles.get(self, path)
 
 
792
class RemoteBranchFormat(branch.BranchFormat):
 
 
794
    def __eq__(self, other):
 
 
795
        return (isinstance(other, RemoteBranchFormat) and 
 
 
796
            self.__dict__ == other.__dict__)
 
 
798
    def get_format_description(self):
 
 
799
        return 'Remote BZR Branch'
 
 
801
    def get_format_string(self):
 
 
802
        return 'Remote BZR Branch'
 
 
804
    def open(self, a_bzrdir):
 
 
805
        assert isinstance(a_bzrdir, RemoteBzrDir)
 
 
806
        return a_bzrdir.open_branch()
 
 
808
    def initialize(self, a_bzrdir):
 
 
809
        assert isinstance(a_bzrdir, RemoteBzrDir)
 
 
810
        return a_bzrdir.create_branch()
 
 
812
    def supports_tags(self):
 
 
813
        # Remote branches might support tags, but we won't know until we
 
 
814
        # access the real remote branch.
 
 
818
class RemoteBranch(branch.Branch):
 
 
819
    """Branch stored on a server accessed by HPSS RPC.
 
 
821
    At the moment most operations are mapped down to simple file operations.
 
 
824
    def __init__(self, remote_bzrdir, remote_repository, real_branch=None,
 
 
826
        """Create a RemoteBranch instance.
 
 
828
        :param real_branch: An optional local implementation of the branch
 
 
829
            format, usually accessing the data via the VFS.
 
 
830
        :param _client: Private parameter for testing.
 
 
832
        # We intentionally don't call the parent class's __init__, because it
 
 
833
        # will try to assign to self.tags, which is a property in this subclass.
 
 
834
        # And the parent's __init__ doesn't do much anyway.
 
 
835
        self._revision_history_cache = None
 
 
836
        self.bzrdir = remote_bzrdir
 
 
837
        if _client is not None:
 
 
838
            self._client = _client
 
 
840
            self._client = client._SmartClient(self.bzrdir._shared_medium)
 
 
841
        self.repository = remote_repository
 
 
842
        if real_branch is not None:
 
 
843
            self._real_branch = real_branch
 
 
844
            # Give the remote repository the matching real repo.
 
 
845
            real_repo = self._real_branch.repository
 
 
846
            if isinstance(real_repo, RemoteRepository):
 
 
847
                real_repo._ensure_real()
 
 
848
                real_repo = real_repo._real_repository
 
 
849
            self.repository._set_real_repository(real_repo)
 
 
850
            # Give the branch the remote repository to let fast-pathing happen.
 
 
851
            self._real_branch.repository = self.repository
 
 
853
            self._real_branch = None
 
 
854
        # Fill out expected attributes of branch for bzrlib api users.
 
 
855
        self._format = RemoteBranchFormat()
 
 
856
        self.base = self.bzrdir.root_transport.base
 
 
857
        self._control_files = None
 
 
858
        self._lock_mode = None
 
 
859
        self._lock_token = None
 
 
861
        self._leave_lock = False
 
 
864
        return "%s(%s)" % (self.__class__.__name__, self.base)
 
 
868
    def _ensure_real(self):
 
 
869
        """Ensure that there is a _real_branch set.
 
 
871
        Used before calls to self._real_branch.
 
 
873
        if not self._real_branch:
 
 
874
            assert vfs.vfs_enabled()
 
 
875
            self.bzrdir._ensure_real()
 
 
876
            self._real_branch = self.bzrdir._real_bzrdir.open_branch()
 
 
877
            # Give the remote repository the matching real repo.
 
 
878
            real_repo = self._real_branch.repository
 
 
879
            if isinstance(real_repo, RemoteRepository):
 
 
880
                real_repo._ensure_real()
 
 
881
                real_repo = real_repo._real_repository
 
 
882
            self.repository._set_real_repository(real_repo)
 
 
883
            # Give the branch the remote repository to let fast-pathing happen.
 
 
884
            self._real_branch.repository = self.repository
 
 
885
            # XXX: deal with _lock_mode == 'w'
 
 
886
            if self._lock_mode == 'r':
 
 
887
                self._real_branch.lock_read()
 
 
890
    def control_files(self):
 
 
891
        # Defer actually creating RemoteBranchLockableFiles until its needed,
 
 
892
        # because it triggers an _ensure_real that we otherwise might not need.
 
 
893
        if self._control_files is None:
 
 
894
            self._control_files = RemoteBranchLockableFiles(
 
 
895
                self.bzrdir, self._client)
 
 
896
        return self._control_files
 
 
898
    def _get_checkout_format(self):
 
 
900
        return self._real_branch._get_checkout_format()
 
 
902
    def get_physical_lock_status(self):
 
 
903
        """See Branch.get_physical_lock_status()."""
 
 
904
        # should be an API call to the server, as branches must be lockable.
 
 
906
        return self._real_branch.get_physical_lock_status()
 
 
909
        if not self._lock_mode:
 
 
910
            self._lock_mode = 'r'
 
 
912
            if self._real_branch is not None:
 
 
913
                self._real_branch.lock_read()
 
 
915
            self._lock_count += 1
 
 
917
    def _remote_lock_write(self, token):
 
 
919
            branch_token = repo_token = ''
 
 
922
            repo_token = self.repository.lock_write()
 
 
923
            self.repository.unlock()
 
 
924
        path = self.bzrdir._path_for_remote_call(self._client)
 
 
925
        response = self._client.call('Branch.lock_write', path, branch_token,
 
 
927
        if response[0] == 'ok':
 
 
928
            ok, branch_token, repo_token = response
 
 
929
            return branch_token, repo_token
 
 
930
        elif response[0] == 'LockContention':
 
 
931
            raise errors.LockContention('(remote lock)')
 
 
932
        elif response[0] == 'TokenMismatch':
 
 
933
            raise errors.TokenMismatch(token, '(remote token)')
 
 
934
        elif response[0] == 'UnlockableTransport':
 
 
935
            raise errors.UnlockableTransport(self.bzrdir.root_transport)
 
 
936
        elif response[0] == 'ReadOnlyError':
 
 
937
            raise errors.ReadOnlyError(self)
 
 
939
            raise errors.UnexpectedSmartServerResponse(response)
 
 
941
    def lock_write(self, token=None):
 
 
942
        if not self._lock_mode:
 
 
943
            remote_tokens = self._remote_lock_write(token)
 
 
944
            self._lock_token, self._repo_lock_token = remote_tokens
 
 
945
            assert self._lock_token, 'Remote server did not return a token!'
 
 
946
            # TODO: We really, really, really don't want to call _ensure_real
 
 
947
            # here, but it's the easiest way to ensure coherency between the
 
 
948
            # state of the RemoteBranch and RemoteRepository objects and the
 
 
949
            # physical locks.  If we don't materialise the real objects here,
 
 
950
            # then getting everything in the right state later is complex, so
 
 
951
            # for now we just do it the lazy way.
 
 
952
            #   -- Andrew Bennetts, 2007-02-22.
 
 
954
            if self._real_branch is not None:
 
 
955
                self._real_branch.repository.lock_write(
 
 
956
                    token=self._repo_lock_token)
 
 
958
                    self._real_branch.lock_write(token=self._lock_token)
 
 
960
                    self._real_branch.repository.unlock()
 
 
961
            if token is not None:
 
 
962
                self._leave_lock = True
 
 
964
                # XXX: this case seems to be unreachable; token cannot be None.
 
 
965
                self._leave_lock = False
 
 
966
            self._lock_mode = 'w'
 
 
968
        elif self._lock_mode == 'r':
 
 
969
            raise errors.ReadOnlyTransaction
 
 
971
            if token is not None:
 
 
972
                # A token was given to lock_write, and we're relocking, so check
 
 
973
                # that the given token actually matches the one we already have.
 
 
974
                if token != self._lock_token:
 
 
975
                    raise errors.TokenMismatch(token, self._lock_token)
 
 
976
            self._lock_count += 1
 
 
977
        return self._lock_token
 
 
979
    def _unlock(self, branch_token, repo_token):
 
 
980
        path = self.bzrdir._path_for_remote_call(self._client)
 
 
981
        response = self._client.call('Branch.unlock', path, branch_token,
 
 
983
        if response == ('ok',):
 
 
985
        elif response[0] == 'TokenMismatch':
 
 
986
            raise errors.TokenMismatch(
 
 
987
                str((branch_token, repo_token)), '(remote tokens)')
 
 
989
            raise errors.UnexpectedSmartServerResponse(response)
 
 
992
        self._lock_count -= 1
 
 
993
        if not self._lock_count:
 
 
994
            self._clear_cached_state()
 
 
995
            mode = self._lock_mode
 
 
996
            self._lock_mode = None
 
 
997
            if self._real_branch is not None:
 
 
998
                if not self._leave_lock:
 
 
999
                    # If this RemoteBranch will remove the physical lock for the
 
 
1000
                    # repository, make sure the _real_branch doesn't do it
 
 
1001
                    # first.  (Because the _real_branch's repository is set to
 
 
1002
                    # be the RemoteRepository.)
 
 
1003
                    self._real_branch.repository.leave_lock_in_place()
 
 
1004
                self._real_branch.unlock()
 
 
1006
                # Only write-locked branched need to make a remote method call
 
 
1007
                # to perfom the unlock.
 
 
1009
            assert self._lock_token, 'Locked, but no token!'
 
 
1010
            branch_token = self._lock_token
 
 
1011
            repo_token = self._repo_lock_token
 
 
1012
            self._lock_token = None
 
 
1013
            self._repo_lock_token = None
 
 
1014
            if not self._leave_lock:
 
 
1015
                self._unlock(branch_token, repo_token)
 
 
1017
    def break_lock(self):
 
 
1019
        return self._real_branch.break_lock()
 
 
1021
    def leave_lock_in_place(self):
 
 
1022
        self._leave_lock = True
 
 
1024
    def dont_leave_lock_in_place(self):
 
 
1025
        self._leave_lock = False
 
 
1027
    def last_revision_info(self):
 
 
1028
        """See Branch.last_revision_info()."""
 
 
1029
        path = self.bzrdir._path_for_remote_call(self._client)
 
 
1030
        response = self._client.call('Branch.last_revision_info', path)
 
 
1031
        assert response[0] == 'ok', 'unexpected response code %s' % (response,)
 
 
1032
        revno = int(response[1])
 
 
1033
        last_revision = response[2]
 
 
1034
        return (revno, last_revision)
 
 
1036
    def _gen_revision_history(self):
 
 
1037
        """See Branch._gen_revision_history()."""
 
 
1038
        path = self.bzrdir._path_for_remote_call(self._client)
 
 
1039
        response = self._client.call_expecting_body(
 
 
1040
            'Branch.revision_history', path)
 
 
1041
        assert response[0][0] == 'ok', ('unexpected response code %s'
 
 
1043
        result = response[1].read_body_bytes().split('\x00')
 
 
1049
    def set_revision_history(self, rev_history):
 
 
1050
        # Send just the tip revision of the history; the server will generate
 
 
1051
        # the full history from that.  If the revision doesn't exist in this
 
 
1052
        # branch, NoSuchRevision will be raised.
 
 
1053
        path = self.bzrdir._path_for_remote_call(self._client)
 
 
1054
        if rev_history == []:
 
 
1057
            rev_id = rev_history[-1]
 
 
1058
        self._clear_cached_state()
 
 
1059
        response = self._client.call('Branch.set_last_revision',
 
 
1060
            path, self._lock_token, self._repo_lock_token, rev_id)
 
 
1061
        if response[0] == 'NoSuchRevision':
 
 
1062
            raise NoSuchRevision(self, rev_id)
 
 
1064
            assert response == ('ok',), (
 
 
1065
                'unexpected response code %r' % (response,))
 
 
1066
        self._cache_revision_history(rev_history)
 
 
1068
    def get_parent(self):
 
 
1070
        return self._real_branch.get_parent()
 
 
1072
    def set_parent(self, url):
 
 
1074
        return self._real_branch.set_parent(url)
 
 
1076
    def get_config(self):
 
 
1077
        return RemoteBranchConfig(self)
 
 
1079
    def sprout(self, to_bzrdir, revision_id=None):
 
 
1080
        # Like Branch.sprout, except that it sprouts a branch in the default
 
 
1081
        # format, because RemoteBranches can't be created at arbitrary URLs.
 
 
1082
        # XXX: if to_bzrdir is a RemoteBranch, this should perhaps do
 
 
1083
        # to_bzrdir.create_branch...
 
 
1084
        result = branch.BranchFormat.get_default_format().initialize(to_bzrdir)
 
 
1085
        self.copy_content_into(result, revision_id=revision_id)
 
 
1086
        result.set_parent(self.bzrdir.root_transport.base)
 
 
1090
    def pull(self, source, overwrite=False, stop_revision=None,
 
 
1092
        # FIXME: This asks the real branch to run the hooks, which means
 
 
1093
        # they're called with the wrong target branch parameter. 
 
 
1094
        # The test suite specifically allows this at present but it should be
 
 
1095
        # fixed.  It should get a _override_hook_target branch,
 
 
1096
        # as push does.  -- mbp 20070405
 
 
1098
        self._real_branch.pull(
 
 
1099
            source, overwrite=overwrite, stop_revision=stop_revision,
 
 
1103
    def push(self, target, overwrite=False, stop_revision=None):
 
 
1105
        return self._real_branch.push(
 
 
1106
            target, overwrite=overwrite, stop_revision=stop_revision,
 
 
1107
            _override_hook_source_branch=self)
 
 
1109
    def is_locked(self):
 
 
1110
        return self._lock_count >= 1
 
 
1112
    def set_last_revision_info(self, revno, revision_id):
 
 
1114
        self._clear_cached_state()
 
 
1115
        return self._real_branch.set_last_revision_info(revno, revision_id)
 
 
1117
    def generate_revision_history(self, revision_id, last_rev=None,
 
 
1120
        return self._real_branch.generate_revision_history(
 
 
1121
            revision_id, last_rev=last_rev, other_branch=other_branch)
 
 
1126
        return self._real_branch.tags
 
 
1128
    def set_push_location(self, location):
 
 
1130
        return self._real_branch.set_push_location(location)
 
 
1132
    def update_revisions(self, other, stop_revision=None):
 
 
1134
        return self._real_branch.update_revisions(
 
 
1135
            other, stop_revision=stop_revision)
 
 
1138
class RemoteBranchConfig(BranchConfig):
 
 
1141
        self.branch._ensure_real()
 
 
1142
        return self.branch._real_branch.get_config().username()
 
 
1144
    def _get_branch_data_config(self):
 
 
1145
        self.branch._ensure_real()
 
 
1146
        if self._branch_data_config is None:
 
 
1147
            self._branch_data_config = TreeConfig(self.branch._real_branch)
 
 
1148
        return self._branch_data_config
 
 
1151
def _extract_tar(tar, to_dir):
 
 
1152
    """Extract all the contents of a tarfile object.
 
 
1154
    A replacement for extractall, which is not present in python2.4
 
 
1157
        tar.extract(tarinfo, to_dir)