/brz/remove-bazaar

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

« back to all changes in this revision

Viewing changes to bzrlib/remote.py

  • Committer: John Arbash Meinel
  • Date: 2008-05-20 02:34:01 UTC
  • mto: This revision was merged to the branch mainline in revision 3441.
  • Revision ID: john@arbash-meinel.com-20080520023401-42mkw5g7dhq9f5bh
review feedback from Ian

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006, 2007, 2008 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
import bz2
 
21
from cStringIO import StringIO
 
22
 
 
23
from bzrlib import (
 
24
    branch,
 
25
    debug,
 
26
    errors,
 
27
    graph,
 
28
    lockdir,
 
29
    repository,
 
30
    revision,
 
31
    symbol_versioning,
 
32
)
 
33
from bzrlib.branch import BranchReferenceFormat
 
34
from bzrlib.bzrdir import BzrDir, RemoteBzrDirFormat
 
35
from bzrlib.config import BranchConfig, TreeConfig
 
36
from bzrlib.decorators import needs_read_lock, needs_write_lock
 
37
from bzrlib.errors import (
 
38
    NoSuchRevision,
 
39
    SmartProtocolError,
 
40
    )
 
41
from bzrlib.lockable_files import LockableFiles
 
42
from bzrlib.pack import ContainerPushParser
 
43
from bzrlib.smart import client, vfs
 
44
from bzrlib.symbol_versioning import (
 
45
    deprecated_method,
 
46
    )
 
47
from bzrlib.revision import ensure_null, NULL_REVISION
 
48
from bzrlib.trace import mutter, note, warning
 
49
 
 
50
# Note: RemoteBzrDirFormat is in bzrdir.py
 
51
 
 
52
class RemoteBzrDir(BzrDir):
 
53
    """Control directory on a remote server, accessed via bzr:// or similar."""
 
54
 
 
55
    def __init__(self, transport, _client=None):
 
56
        """Construct a RemoteBzrDir.
 
57
 
 
58
        :param _client: Private parameter for testing. Disables probing and the
 
59
            use of a real bzrdir.
 
60
        """
 
61
        BzrDir.__init__(self, transport, RemoteBzrDirFormat())
 
62
        # this object holds a delegated bzrdir that uses file-level operations
 
63
        # to talk to the other side
 
64
        self._real_bzrdir = None
 
65
 
 
66
        if _client is None:
 
67
            medium = transport.get_smart_medium()
 
68
            self._client = client._SmartClient(medium, transport.base)
 
69
        else:
 
70
            self._client = _client
 
71
            return
 
72
 
 
73
        path = self._path_for_remote_call(self._client)
 
74
        response = self._client.call('BzrDir.open', path)
 
75
        if response not in [('yes',), ('no',)]:
 
76
            raise errors.UnexpectedSmartServerResponse(response)
 
77
        if response == ('no',):
 
78
            raise errors.NotBranchError(path=transport.base)
 
79
 
 
80
    def _ensure_real(self):
 
81
        """Ensure that there is a _real_bzrdir set.
 
82
 
 
83
        Used before calls to self._real_bzrdir.
 
84
        """
 
85
        if not self._real_bzrdir:
 
86
            self._real_bzrdir = BzrDir.open_from_transport(
 
87
                self.root_transport, _server_formats=False)
 
88
 
 
89
    def create_repository(self, shared=False):
 
90
        self._ensure_real()
 
91
        self._real_bzrdir.create_repository(shared=shared)
 
92
        return self.open_repository()
 
93
 
 
94
    def destroy_repository(self):
 
95
        """See BzrDir.destroy_repository"""
 
96
        self._ensure_real()
 
97
        self._real_bzrdir.destroy_repository()
 
98
 
 
99
    def create_branch(self):
 
100
        self._ensure_real()
 
101
        real_branch = self._real_bzrdir.create_branch()
 
102
        return RemoteBranch(self, self.find_repository(), real_branch)
 
103
 
 
104
    def destroy_branch(self):
 
105
        """See BzrDir.destroy_branch"""
 
106
        self._ensure_real()
 
107
        self._real_bzrdir.destroy_branch()
 
108
 
 
109
    def create_workingtree(self, revision_id=None, from_branch=None):
 
110
        raise errors.NotLocalUrl(self.transport.base)
 
111
 
 
112
    def find_branch_format(self):
 
113
        """Find the branch 'format' for this bzrdir.
 
114
 
 
115
        This might be a synthetic object for e.g. RemoteBranch and SVN.
 
116
        """
 
117
        b = self.open_branch()
 
118
        return b._format
 
119
 
 
120
    def get_branch_reference(self):
 
121
        """See BzrDir.get_branch_reference()."""
 
122
        path = self._path_for_remote_call(self._client)
 
123
        response = self._client.call('BzrDir.open_branch', path)
 
124
        if response[0] == 'ok':
 
125
            if response[1] == '':
 
126
                # branch at this location.
 
127
                return None
 
128
            else:
 
129
                # a branch reference, use the existing BranchReference logic.
 
130
                return response[1]
 
131
        elif response == ('nobranch',):
 
132
            raise errors.NotBranchError(path=self.root_transport.base)
 
133
        else:
 
134
            raise errors.UnexpectedSmartServerResponse(response)
 
135
 
 
136
    def _get_tree_branch(self):
 
137
        """See BzrDir._get_tree_branch()."""
 
138
        return None, self.open_branch()
 
139
 
 
140
    def open_branch(self, _unsupported=False):
 
141
        if _unsupported:
 
142
            raise NotImplementedError('unsupported flag support not implemented yet.')
 
143
        reference_url = self.get_branch_reference()
 
144
        if reference_url is None:
 
145
            # branch at this location.
 
146
            return RemoteBranch(self, self.find_repository())
 
147
        else:
 
148
            # a branch reference, use the existing BranchReference logic.
 
149
            format = BranchReferenceFormat()
 
150
            return format.open(self, _found=True, location=reference_url)
 
151
                
 
152
    def open_repository(self):
 
153
        path = self._path_for_remote_call(self._client)
 
154
        verb = 'BzrDir.find_repositoryV2'
 
155
        try:
 
156
            response = self._client.call(verb, path)
 
157
        except errors.UnknownSmartMethod:
 
158
            verb = 'BzrDir.find_repository'
 
159
            response = self._client.call(verb, path)
 
160
        if response[0] == 'norepository':
 
161
            raise errors.NoRepositoryPresent(self)
 
162
        elif response[0] != 'ok':
 
163
            raise SmartProtocolError('unexpected response %r' 
 
164
                % response)
 
165
        if verb == 'BzrDir.find_repository':
 
166
            # servers that don't support the V2 method don't support external
 
167
            # references either.
 
168
            response = response + ('no', )
 
169
        if not (len(response) == 5):
 
170
            raise SmartProtocolError('incorrect response length %s' % (response,))
 
171
        if response[1] == '':
 
172
            format = RemoteRepositoryFormat()
 
173
            format.rich_root_data = (response[2] == 'yes')
 
174
            format.supports_tree_reference = (response[3] == 'yes')
 
175
            # No wire format to check this yet.
 
176
            format.supports_external_lookups = (response[4] == 'yes')
 
177
            return RemoteRepository(self, format)
 
178
        else:
 
179
            raise errors.NoRepositoryPresent(self)
 
180
 
 
181
    def open_workingtree(self, recommend_upgrade=True):
 
182
        self._ensure_real()
 
183
        if self._real_bzrdir.has_workingtree():
 
184
            raise errors.NotLocalUrl(self.root_transport)
 
185
        else:
 
186
            raise errors.NoWorkingTree(self.root_transport.base)
 
187
 
 
188
    def _path_for_remote_call(self, client):
 
189
        """Return the path to be used for this bzrdir in a remote call."""
 
190
        return client.remote_path_from_transport(self.root_transport)
 
191
 
 
192
    def get_branch_transport(self, branch_format):
 
193
        self._ensure_real()
 
194
        return self._real_bzrdir.get_branch_transport(branch_format)
 
195
 
 
196
    def get_repository_transport(self, repository_format):
 
197
        self._ensure_real()
 
198
        return self._real_bzrdir.get_repository_transport(repository_format)
 
199
 
 
200
    def get_workingtree_transport(self, workingtree_format):
 
201
        self._ensure_real()
 
202
        return self._real_bzrdir.get_workingtree_transport(workingtree_format)
 
203
 
 
204
    def can_convert_format(self):
 
205
        """Upgrading of remote bzrdirs is not supported yet."""
 
206
        return False
 
207
 
 
208
    def needs_format_conversion(self, format=None):
 
209
        """Upgrading of remote bzrdirs is not supported yet."""
 
210
        return False
 
211
 
 
212
    def clone(self, url, revision_id=None, force_new_repo=False):
 
213
        self._ensure_real()
 
214
        return self._real_bzrdir.clone(url, revision_id=revision_id,
 
215
            force_new_repo=force_new_repo)
 
216
 
 
217
 
 
218
class RemoteRepositoryFormat(repository.RepositoryFormat):
 
219
    """Format for repositories accessed over a _SmartClient.
 
220
 
 
221
    Instances of this repository are represented by RemoteRepository
 
222
    instances.
 
223
 
 
224
    The RemoteRepositoryFormat is parameterized during construction
 
225
    to reflect the capabilities of the real, remote format. Specifically
 
226
    the attributes rich_root_data and supports_tree_reference are set
 
227
    on a per instance basis, and are not set (and should not be) at
 
228
    the class level.
 
229
    """
 
230
 
 
231
    _matchingbzrdir = RemoteBzrDirFormat
 
232
 
 
233
    def initialize(self, a_bzrdir, shared=False):
 
234
        if not isinstance(a_bzrdir, RemoteBzrDir):
 
235
            raise AssertionError('%r is not a RemoteBzrDir' % (a_bzrdir,))
 
236
        return a_bzrdir.create_repository(shared=shared)
 
237
    
 
238
    def open(self, a_bzrdir):
 
239
        if not isinstance(a_bzrdir, RemoteBzrDir):
 
240
            raise AssertionError('%r is not a RemoteBzrDir' % (a_bzrdir,))
 
241
        return a_bzrdir.open_repository()
 
242
 
 
243
    def get_format_description(self):
 
244
        return 'bzr remote repository'
 
245
 
 
246
    def __eq__(self, other):
 
247
        return self.__class__ == other.__class__
 
248
 
 
249
    def check_conversion_target(self, target_format):
 
250
        if self.rich_root_data and not target_format.rich_root_data:
 
251
            raise errors.BadConversionTarget(
 
252
                'Does not support rich root data.', target_format)
 
253
        if (self.supports_tree_reference and
 
254
            not getattr(target_format, 'supports_tree_reference', False)):
 
255
            raise errors.BadConversionTarget(
 
256
                'Does not support nested trees', target_format)
 
257
 
 
258
 
 
259
class RemoteRepository(object):
 
260
    """Repository accessed over rpc.
 
261
 
 
262
    For the moment most operations are performed using local transport-backed
 
263
    Repository objects.
 
264
    """
 
265
 
 
266
    def __init__(self, remote_bzrdir, format, real_repository=None, _client=None):
 
267
        """Create a RemoteRepository instance.
 
268
        
 
269
        :param remote_bzrdir: The bzrdir hosting this repository.
 
270
        :param format: The RemoteFormat object to use.
 
271
        :param real_repository: If not None, a local implementation of the
 
272
            repository logic for the repository, usually accessing the data
 
273
            via the VFS.
 
274
        :param _client: Private testing parameter - override the smart client
 
275
            to be used by the repository.
 
276
        """
 
277
        if real_repository:
 
278
            self._real_repository = real_repository
 
279
        else:
 
280
            self._real_repository = None
 
281
        self.bzrdir = remote_bzrdir
 
282
        if _client is None:
 
283
            self._client = remote_bzrdir._client
 
284
        else:
 
285
            self._client = _client
 
286
        self._format = format
 
287
        self._lock_mode = None
 
288
        self._lock_token = None
 
289
        self._lock_count = 0
 
290
        self._leave_lock = False
 
291
        # A cache of looked up revision parent data; reset at unlock time.
 
292
        self._parents_map = None
 
293
        if 'hpss' in debug.debug_flags:
 
294
            self._requested_parents = None
 
295
        # For tests:
 
296
        # These depend on the actual remote format, so force them off for
 
297
        # maximum compatibility. XXX: In future these should depend on the
 
298
        # remote repository instance, but this is irrelevant until we perform
 
299
        # reconcile via an RPC call.
 
300
        self._reconcile_does_inventory_gc = False
 
301
        self._reconcile_fixes_text_parents = False
 
302
        self._reconcile_backsup_inventory = False
 
303
        self.base = self.bzrdir.transport.base
 
304
 
 
305
    def __str__(self):
 
306
        return "%s(%s)" % (self.__class__.__name__, self.base)
 
307
 
 
308
    __repr__ = __str__
 
309
 
 
310
    def abort_write_group(self):
 
311
        """Complete a write group on the decorated repository.
 
312
        
 
313
        Smart methods peform operations in a single step so this api
 
314
        is not really applicable except as a compatibility thunk
 
315
        for older plugins that don't use e.g. the CommitBuilder
 
316
        facility.
 
317
        """
 
318
        self._ensure_real()
 
319
        return self._real_repository.abort_write_group()
 
320
 
 
321
    def commit_write_group(self):
 
322
        """Complete a write group on the decorated repository.
 
323
        
 
324
        Smart methods peform operations in a single step so this api
 
325
        is not really applicable except as a compatibility thunk
 
326
        for older plugins that don't use e.g. the CommitBuilder
 
327
        facility.
 
328
        """
 
329
        self._ensure_real()
 
330
        return self._real_repository.commit_write_group()
 
331
 
 
332
    def _ensure_real(self):
 
333
        """Ensure that there is a _real_repository set.
 
334
 
 
335
        Used before calls to self._real_repository.
 
336
        """
 
337
        if not self._real_repository:
 
338
            self.bzrdir._ensure_real()
 
339
            #self._real_repository = self.bzrdir._real_bzrdir.open_repository()
 
340
            self._set_real_repository(self.bzrdir._real_bzrdir.open_repository())
 
341
 
 
342
    def find_text_key_references(self):
 
343
        """Find the text key references within the repository.
 
344
 
 
345
        :return: a dictionary mapping (file_id, revision_id) tuples to altered file-ids to an iterable of
 
346
        revision_ids. Each altered file-ids has the exact revision_ids that
 
347
        altered it listed explicitly.
 
348
        :return: A dictionary mapping text keys ((fileid, revision_id) tuples)
 
349
            to whether they were referred to by the inventory of the
 
350
            revision_id that they contain. The inventory texts from all present
 
351
            revision ids are assessed to generate this report.
 
352
        """
 
353
        self._ensure_real()
 
354
        return self._real_repository.find_text_key_references()
 
355
 
 
356
    def _generate_text_key_index(self):
 
357
        """Generate a new text key index for the repository.
 
358
 
 
359
        This is an expensive function that will take considerable time to run.
 
360
 
 
361
        :return: A dict mapping (file_id, revision_id) tuples to a list of
 
362
            parents, also (file_id, revision_id) tuples.
 
363
        """
 
364
        self._ensure_real()
 
365
        return self._real_repository._generate_text_key_index()
 
366
 
 
367
    @symbol_versioning.deprecated_method(symbol_versioning.one_four)
 
368
    def get_revision_graph(self, revision_id=None):
 
369
        """See Repository.get_revision_graph()."""
 
370
        return self._get_revision_graph(revision_id)
 
371
 
 
372
    def _get_revision_graph(self, revision_id):
 
373
        """Private method for using with old (< 1.2) servers to fallback."""
 
374
        if revision_id is None:
 
375
            revision_id = ''
 
376
        elif revision.is_null(revision_id):
 
377
            return {}
 
378
 
 
379
        path = self.bzrdir._path_for_remote_call(self._client)
 
380
        response = self._client.call_expecting_body(
 
381
            'Repository.get_revision_graph', path, revision_id)
 
382
        if response[0][0] not in ['ok', 'nosuchrevision']:
 
383
            raise errors.UnexpectedSmartServerResponse(response[0])
 
384
        if response[0][0] == 'ok':
 
385
            coded = response[1].read_body_bytes()
 
386
            if coded == '':
 
387
                # no revisions in this repository!
 
388
                return {}
 
389
            lines = coded.split('\n')
 
390
            revision_graph = {}
 
391
            for line in lines:
 
392
                d = tuple(line.split())
 
393
                revision_graph[d[0]] = d[1:]
 
394
                
 
395
            return revision_graph
 
396
        else:
 
397
            response_body = response[1].read_body_bytes()
 
398
            if response_body:
 
399
                raise SmartProtocolError('unexpected response body')
 
400
            raise NoSuchRevision(self, revision_id)
 
401
 
 
402
    def has_revision(self, revision_id):
 
403
        """See Repository.has_revision()."""
 
404
        if revision_id == NULL_REVISION:
 
405
            # The null revision is always present.
 
406
            return True
 
407
        path = self.bzrdir._path_for_remote_call(self._client)
 
408
        response = self._client.call('Repository.has_revision', path, revision_id)
 
409
        if response[0] not in ('yes', 'no'):
 
410
            raise SmartProtocolError('unexpected response code %s' % (response,))
 
411
        return response[0] == 'yes'
 
412
 
 
413
    def has_revisions(self, revision_ids):
 
414
        """See Repository.has_revisions()."""
 
415
        result = set()
 
416
        for revision_id in revision_ids:
 
417
            if self.has_revision(revision_id):
 
418
                result.add(revision_id)
 
419
        return result
 
420
 
 
421
    def has_same_location(self, other):
 
422
        return (self.__class__ == other.__class__ and
 
423
                self.bzrdir.transport.base == other.bzrdir.transport.base)
 
424
        
 
425
    def get_graph(self, other_repository=None):
 
426
        """Return the graph for this repository format"""
 
427
        parents_provider = self
 
428
        if (other_repository is not None and
 
429
            other_repository.bzrdir.transport.base !=
 
430
            self.bzrdir.transport.base):
 
431
            parents_provider = graph._StackedParentsProvider(
 
432
                [parents_provider, other_repository._make_parents_provider()])
 
433
        return graph.Graph(parents_provider)
 
434
 
 
435
    def gather_stats(self, revid=None, committers=None):
 
436
        """See Repository.gather_stats()."""
 
437
        path = self.bzrdir._path_for_remote_call(self._client)
 
438
        # revid can be None to indicate no revisions, not just NULL_REVISION
 
439
        if revid is None or revision.is_null(revid):
 
440
            fmt_revid = ''
 
441
        else:
 
442
            fmt_revid = revid
 
443
        if committers is None or not committers:
 
444
            fmt_committers = 'no'
 
445
        else:
 
446
            fmt_committers = 'yes'
 
447
        response = self._client.call_expecting_body(
 
448
            'Repository.gather_stats', path, fmt_revid, fmt_committers)
 
449
        if response[0][0] != 'ok':
 
450
            raise SmartProtocolError('unexpected response code %s'
 
451
                % (response[0],))
 
452
 
 
453
        body = response[1].read_body_bytes()
 
454
        result = {}
 
455
        for line in body.split('\n'):
 
456
            if not line:
 
457
                continue
 
458
            key, val_text = line.split(':')
 
459
            if key in ('revisions', 'size', 'committers'):
 
460
                result[key] = int(val_text)
 
461
            elif key in ('firstrev', 'latestrev'):
 
462
                values = val_text.split(' ')[1:]
 
463
                result[key] = (float(values[0]), long(values[1]))
 
464
 
 
465
        return result
 
466
 
 
467
    def find_branches(self, using=False):
 
468
        """See Repository.find_branches()."""
 
469
        # should be an API call to the server.
 
470
        self._ensure_real()
 
471
        return self._real_repository.find_branches(using=using)
 
472
 
 
473
    def get_physical_lock_status(self):
 
474
        """See Repository.get_physical_lock_status()."""
 
475
        # should be an API call to the server.
 
476
        self._ensure_real()
 
477
        return self._real_repository.get_physical_lock_status()
 
478
 
 
479
    def is_in_write_group(self):
 
480
        """Return True if there is an open write group.
 
481
 
 
482
        write groups are only applicable locally for the smart server..
 
483
        """
 
484
        if self._real_repository:
 
485
            return self._real_repository.is_in_write_group()
 
486
 
 
487
    def is_locked(self):
 
488
        return self._lock_count >= 1
 
489
 
 
490
    def is_shared(self):
 
491
        """See Repository.is_shared()."""
 
492
        path = self.bzrdir._path_for_remote_call(self._client)
 
493
        response = self._client.call('Repository.is_shared', path)
 
494
        if response[0] not in ('yes', 'no'):
 
495
            raise SmartProtocolError('unexpected response code %s' % (response,))
 
496
        return response[0] == 'yes'
 
497
 
 
498
    def is_write_locked(self):
 
499
        return self._lock_mode == 'w'
 
500
 
 
501
    def lock_read(self):
 
502
        # wrong eventually - want a local lock cache context
 
503
        if not self._lock_mode:
 
504
            self._lock_mode = 'r'
 
505
            self._lock_count = 1
 
506
            self._parents_map = {}
 
507
            if 'hpss' in debug.debug_flags:
 
508
                self._requested_parents = set()
 
509
            if self._real_repository is not None:
 
510
                self._real_repository.lock_read()
 
511
        else:
 
512
            self._lock_count += 1
 
513
 
 
514
    def _remote_lock_write(self, token):
 
515
        path = self.bzrdir._path_for_remote_call(self._client)
 
516
        if token is None:
 
517
            token = ''
 
518
        response = self._client.call('Repository.lock_write', path, token)
 
519
        if response[0] == 'ok':
 
520
            ok, token = response
 
521
            return token
 
522
        elif response[0] == 'LockContention':
 
523
            raise errors.LockContention('(remote lock)')
 
524
        elif response[0] == 'UnlockableTransport':
 
525
            raise errors.UnlockableTransport(self.bzrdir.root_transport)
 
526
        elif response[0] == 'LockFailed':
 
527
            raise errors.LockFailed(response[1], response[2])
 
528
        else:
 
529
            raise errors.UnexpectedSmartServerResponse(response)
 
530
 
 
531
    def lock_write(self, token=None):
 
532
        if not self._lock_mode:
 
533
            self._lock_token = self._remote_lock_write(token)
 
534
            # if self._lock_token is None, then this is something like packs or
 
535
            # svn where we don't get to lock the repo, or a weave style repository
 
536
            # where we cannot lock it over the wire and attempts to do so will
 
537
            # fail.
 
538
            if self._real_repository is not None:
 
539
                self._real_repository.lock_write(token=self._lock_token)
 
540
            if token is not None:
 
541
                self._leave_lock = True
 
542
            else:
 
543
                self._leave_lock = False
 
544
            self._lock_mode = 'w'
 
545
            self._lock_count = 1
 
546
            self._parents_map = {}
 
547
            if 'hpss' in debug.debug_flags:
 
548
                self._requested_parents = set()
 
549
        elif self._lock_mode == 'r':
 
550
            raise errors.ReadOnlyError(self)
 
551
        else:
 
552
            self._lock_count += 1
 
553
        return self._lock_token or None
 
554
 
 
555
    def leave_lock_in_place(self):
 
556
        if not self._lock_token:
 
557
            raise NotImplementedError(self.leave_lock_in_place)
 
558
        self._leave_lock = True
 
559
 
 
560
    def dont_leave_lock_in_place(self):
 
561
        if not self._lock_token:
 
562
            raise NotImplementedError(self.dont_leave_lock_in_place)
 
563
        self._leave_lock = False
 
564
 
 
565
    def _set_real_repository(self, repository):
 
566
        """Set the _real_repository for this repository.
 
567
 
 
568
        :param repository: The repository to fallback to for non-hpss
 
569
            implemented operations.
 
570
        """
 
571
        if isinstance(repository, RemoteRepository):
 
572
            raise AssertionError()
 
573
        self._real_repository = repository
 
574
        if self._lock_mode == 'w':
 
575
            # if we are already locked, the real repository must be able to
 
576
            # acquire the lock with our token.
 
577
            self._real_repository.lock_write(self._lock_token)
 
578
        elif self._lock_mode == 'r':
 
579
            self._real_repository.lock_read()
 
580
 
 
581
    def start_write_group(self):
 
582
        """Start a write group on the decorated repository.
 
583
        
 
584
        Smart methods peform operations in a single step so this api
 
585
        is not really applicable except as a compatibility thunk
 
586
        for older plugins that don't use e.g. the CommitBuilder
 
587
        facility.
 
588
        """
 
589
        self._ensure_real()
 
590
        return self._real_repository.start_write_group()
 
591
 
 
592
    def _unlock(self, token):
 
593
        path = self.bzrdir._path_for_remote_call(self._client)
 
594
        if not token:
 
595
            # with no token the remote repository is not persistently locked.
 
596
            return
 
597
        response = self._client.call('Repository.unlock', path, token)
 
598
        if response == ('ok',):
 
599
            return
 
600
        elif response[0] == 'TokenMismatch':
 
601
            raise errors.TokenMismatch(token, '(remote token)')
 
602
        else:
 
603
            raise errors.UnexpectedSmartServerResponse(response)
 
604
 
 
605
    def unlock(self):
 
606
        self._lock_count -= 1
 
607
        if self._lock_count > 0:
 
608
            return
 
609
        self._parents_map = None
 
610
        if 'hpss' in debug.debug_flags:
 
611
            self._requested_parents = None
 
612
        old_mode = self._lock_mode
 
613
        self._lock_mode = None
 
614
        try:
 
615
            # The real repository is responsible at present for raising an
 
616
            # exception if it's in an unfinished write group.  However, it
 
617
            # normally will *not* actually remove the lock from disk - that's
 
618
            # done by the server on receiving the Repository.unlock call.
 
619
            # This is just to let the _real_repository stay up to date.
 
620
            if self._real_repository is not None:
 
621
                self._real_repository.unlock()
 
622
        finally:
 
623
            # The rpc-level lock should be released even if there was a
 
624
            # problem releasing the vfs-based lock.
 
625
            if old_mode == 'w':
 
626
                # Only write-locked repositories need to make a remote method
 
627
                # call to perfom the unlock.
 
628
                old_token = self._lock_token
 
629
                self._lock_token = None
 
630
                if not self._leave_lock:
 
631
                    self._unlock(old_token)
 
632
 
 
633
    def break_lock(self):
 
634
        # should hand off to the network
 
635
        self._ensure_real()
 
636
        return self._real_repository.break_lock()
 
637
 
 
638
    def _get_tarball(self, compression):
 
639
        """Return a TemporaryFile containing a repository tarball.
 
640
        
 
641
        Returns None if the server does not support sending tarballs.
 
642
        """
 
643
        import tempfile
 
644
        path = self.bzrdir._path_for_remote_call(self._client)
 
645
        try:
 
646
            response, protocol = self._client.call_expecting_body(
 
647
                'Repository.tarball', path, compression)
 
648
        except errors.UnknownSmartMethod:
 
649
            protocol.cancel_read_body()
 
650
            return None
 
651
        if response[0] == 'ok':
 
652
            # Extract the tarball and return it
 
653
            t = tempfile.NamedTemporaryFile()
 
654
            # TODO: rpc layer should read directly into it...
 
655
            t.write(protocol.read_body_bytes())
 
656
            t.seek(0)
 
657
            return t
 
658
        raise errors.UnexpectedSmartServerResponse(response)
 
659
 
 
660
    def sprout(self, to_bzrdir, revision_id=None):
 
661
        # TODO: Option to control what format is created?
 
662
        self._ensure_real()
 
663
        dest_repo = self._real_repository._format.initialize(to_bzrdir,
 
664
                                                             shared=False)
 
665
        dest_repo.fetch(self, revision_id=revision_id)
 
666
        return dest_repo
 
667
 
 
668
    ### These methods are just thin shims to the VFS object for now.
 
669
 
 
670
    def revision_tree(self, revision_id):
 
671
        self._ensure_real()
 
672
        return self._real_repository.revision_tree(revision_id)
 
673
 
 
674
    def get_serializer_format(self):
 
675
        self._ensure_real()
 
676
        return self._real_repository.get_serializer_format()
 
677
 
 
678
    def get_commit_builder(self, branch, parents, config, timestamp=None,
 
679
                           timezone=None, committer=None, revprops=None,
 
680
                           revision_id=None):
 
681
        # FIXME: It ought to be possible to call this without immediately
 
682
        # triggering _ensure_real.  For now it's the easiest thing to do.
 
683
        self._ensure_real()
 
684
        builder = self._real_repository.get_commit_builder(branch, parents,
 
685
                config, timestamp=timestamp, timezone=timezone,
 
686
                committer=committer, revprops=revprops, revision_id=revision_id)
 
687
        return builder
 
688
 
 
689
    def add_inventory(self, revid, inv, parents):
 
690
        self._ensure_real()
 
691
        return self._real_repository.add_inventory(revid, inv, parents)
 
692
 
 
693
    def add_revision(self, rev_id, rev, inv=None, config=None):
 
694
        self._ensure_real()
 
695
        return self._real_repository.add_revision(
 
696
            rev_id, rev, inv=inv, config=config)
 
697
 
 
698
    @needs_read_lock
 
699
    def get_inventory(self, revision_id):
 
700
        self._ensure_real()
 
701
        return self._real_repository.get_inventory(revision_id)
 
702
 
 
703
    def iter_inventories(self, revision_ids):
 
704
        self._ensure_real()
 
705
        return self._real_repository.iter_inventories(revision_ids)
 
706
 
 
707
    @needs_read_lock
 
708
    def get_revision(self, revision_id):
 
709
        self._ensure_real()
 
710
        return self._real_repository.get_revision(revision_id)
 
711
 
 
712
    @property
 
713
    def weave_store(self):
 
714
        self._ensure_real()
 
715
        return self._real_repository.weave_store
 
716
 
 
717
    def get_transaction(self):
 
718
        self._ensure_real()
 
719
        return self._real_repository.get_transaction()
 
720
 
 
721
    @needs_read_lock
 
722
    def clone(self, a_bzrdir, revision_id=None):
 
723
        self._ensure_real()
 
724
        return self._real_repository.clone(a_bzrdir, revision_id=revision_id)
 
725
 
 
726
    def make_working_trees(self):
 
727
        """See Repository.make_working_trees"""
 
728
        self._ensure_real()
 
729
        return self._real_repository.make_working_trees()
 
730
 
 
731
    def revision_ids_to_search_result(self, result_set):
 
732
        """Convert a set of revision ids to a graph SearchResult."""
 
733
        result_parents = set()
 
734
        for parents in self.get_graph().get_parent_map(
 
735
            result_set).itervalues():
 
736
            result_parents.update(parents)
 
737
        included_keys = result_set.intersection(result_parents)
 
738
        start_keys = result_set.difference(included_keys)
 
739
        exclude_keys = result_parents.difference(result_set)
 
740
        result = graph.SearchResult(start_keys, exclude_keys,
 
741
            len(result_set), result_set)
 
742
        return result
 
743
 
 
744
    @needs_read_lock
 
745
    def search_missing_revision_ids(self, other, revision_id=None, find_ghosts=True):
 
746
        """Return the revision ids that other has that this does not.
 
747
        
 
748
        These are returned in topological order.
 
749
 
 
750
        revision_id: only return revision ids included by revision_id.
 
751
        """
 
752
        return repository.InterRepository.get(
 
753
            other, self).search_missing_revision_ids(revision_id, find_ghosts)
 
754
 
 
755
    def fetch(self, source, revision_id=None, pb=None):
 
756
        if self.has_same_location(source):
 
757
            # check that last_revision is in 'from' and then return a
 
758
            # no-operation.
 
759
            if (revision_id is not None and
 
760
                not revision.is_null(revision_id)):
 
761
                self.get_revision(revision_id)
 
762
            return 0, []
 
763
        self._ensure_real()
 
764
        return self._real_repository.fetch(
 
765
            source, revision_id=revision_id, pb=pb)
 
766
 
 
767
    def create_bundle(self, target, base, fileobj, format=None):
 
768
        self._ensure_real()
 
769
        self._real_repository.create_bundle(target, base, fileobj, format)
 
770
 
 
771
    @property
 
772
    def control_weaves(self):
 
773
        self._ensure_real()
 
774
        return self._real_repository.control_weaves
 
775
 
 
776
    @needs_read_lock
 
777
    def get_ancestry(self, revision_id, topo_sorted=True):
 
778
        self._ensure_real()
 
779
        return self._real_repository.get_ancestry(revision_id, topo_sorted)
 
780
 
 
781
    @needs_read_lock
 
782
    def get_inventory_weave(self):
 
783
        self._ensure_real()
 
784
        return self._real_repository.get_inventory_weave()
 
785
 
 
786
    def fileids_altered_by_revision_ids(self, revision_ids):
 
787
        self._ensure_real()
 
788
        return self._real_repository.fileids_altered_by_revision_ids(revision_ids)
 
789
 
 
790
    def _get_versioned_file_checker(self, revisions, revision_versions_cache):
 
791
        self._ensure_real()
 
792
        return self._real_repository._get_versioned_file_checker(
 
793
            revisions, revision_versions_cache)
 
794
        
 
795
    def iter_files_bytes(self, desired_files):
 
796
        """See Repository.iter_file_bytes.
 
797
        """
 
798
        self._ensure_real()
 
799
        return self._real_repository.iter_files_bytes(desired_files)
 
800
 
 
801
    def get_parent_map(self, keys):
 
802
        """See bzrlib.Graph.get_parent_map()."""
 
803
        # Hack to build up the caching logic.
 
804
        ancestry = self._parents_map
 
805
        if ancestry is None:
 
806
            # Repository is not locked, so there's no cache.
 
807
            missing_revisions = set(keys)
 
808
            ancestry = {}
 
809
        else:
 
810
            missing_revisions = set(key for key in keys if key not in ancestry)
 
811
        if missing_revisions:
 
812
            parent_map = self._get_parent_map(missing_revisions)
 
813
            if 'hpss' in debug.debug_flags:
 
814
                mutter('retransmitted revisions: %d of %d',
 
815
                        len(set(ancestry).intersection(parent_map)),
 
816
                        len(parent_map))
 
817
            ancestry.update(parent_map)
 
818
        present_keys = [k for k in keys if k in ancestry]
 
819
        if 'hpss' in debug.debug_flags:
 
820
            self._requested_parents.update(present_keys)
 
821
            mutter('Current RemoteRepository graph hit rate: %d%%',
 
822
                100.0 * len(self._requested_parents) / len(ancestry))
 
823
        return dict((k, ancestry[k]) for k in present_keys)
 
824
 
 
825
    def _get_parent_map(self, keys):
 
826
        """Helper for get_parent_map that performs the RPC."""
 
827
        medium = self._client._medium
 
828
        if not medium._remote_is_at_least_1_2:
 
829
            # We already found out that the server can't understand
 
830
            # Repository.get_parent_map requests, so just fetch the whole
 
831
            # graph.
 
832
            # XXX: Note that this will issue a deprecation warning. This is ok
 
833
            # :- its because we're working with a deprecated server anyway, and
 
834
            # the user will almost certainly have seen a warning about the
 
835
            # server version already.
 
836
            rg = self.get_revision_graph()
 
837
            # There is an api discrepency between get_parent_map and
 
838
            # get_revision_graph. Specifically, a "key:()" pair in
 
839
            # get_revision_graph just means a node has no parents. For
 
840
            # "get_parent_map" it means the node is a ghost. So fix up the
 
841
            # graph to correct this.
 
842
            #   https://bugs.launchpad.net/bzr/+bug/214894
 
843
            # There is one other "bug" which is that ghosts in
 
844
            # get_revision_graph() are not returned at all. But we won't worry
 
845
            # about that for now.
 
846
            for node_id, parent_ids in rg.iteritems():
 
847
                if parent_ids == ():
 
848
                    rg[node_id] = (NULL_REVISION,)
 
849
            rg[NULL_REVISION] = ()
 
850
            return rg
 
851
 
 
852
        keys = set(keys)
 
853
        if NULL_REVISION in keys:
 
854
            keys.discard(NULL_REVISION)
 
855
            found_parents = {NULL_REVISION:()}
 
856
            if not keys:
 
857
                return found_parents
 
858
        else:
 
859
            found_parents = {}
 
860
        # TODO(Needs analysis): We could assume that the keys being requested
 
861
        # from get_parent_map are in a breadth first search, so typically they
 
862
        # will all be depth N from some common parent, and we don't have to
 
863
        # have the server iterate from the root parent, but rather from the
 
864
        # keys we're searching; and just tell the server the keyspace we
 
865
        # already have; but this may be more traffic again.
 
866
 
 
867
        # Transform self._parents_map into a search request recipe.
 
868
        # TODO: Manage this incrementally to avoid covering the same path
 
869
        # repeatedly. (The server will have to on each request, but the less
 
870
        # work done the better).
 
871
        parents_map = self._parents_map
 
872
        if parents_map is None:
 
873
            # Repository is not locked, so there's no cache.
 
874
            parents_map = {}
 
875
        start_set = set(parents_map)
 
876
        result_parents = set()
 
877
        for parents in parents_map.itervalues():
 
878
            result_parents.update(parents)
 
879
        stop_keys = result_parents.difference(start_set)
 
880
        included_keys = start_set.intersection(result_parents)
 
881
        start_set.difference_update(included_keys)
 
882
        recipe = (start_set, stop_keys, len(parents_map))
 
883
        body = self._serialise_search_recipe(recipe)
 
884
        path = self.bzrdir._path_for_remote_call(self._client)
 
885
        for key in keys:
 
886
            if type(key) is not str:
 
887
                raise ValueError(
 
888
                    "key %r not a plain string" % (key,))
 
889
        verb = 'Repository.get_parent_map'
 
890
        args = (path,) + tuple(keys)
 
891
        try:
 
892
            response = self._client.call_with_body_bytes_expecting_body(
 
893
                verb, args, self._serialise_search_recipe(recipe))
 
894
        except errors.UnknownSmartMethod:
 
895
            # Server does not support this method, so get the whole graph.
 
896
            # Worse, we have to force a disconnection, because the server now
 
897
            # doesn't realise it has a body on the wire to consume, so the
 
898
            # only way to recover is to abandon the connection.
 
899
            warning(
 
900
                'Server is too old for fast get_parent_map, reconnecting.  '
 
901
                '(Upgrade the server to Bazaar 1.2 to avoid this)')
 
902
            medium.disconnect()
 
903
            # To avoid having to disconnect repeatedly, we keep track of the
 
904
            # fact the server doesn't understand remote methods added in 1.2.
 
905
            medium._remote_is_at_least_1_2 = False
 
906
            return self.get_revision_graph(None)
 
907
        if response[0][0] not in ['ok']:
 
908
            response[1].cancel_read_body()
 
909
            raise errors.UnexpectedSmartServerResponse(response[0])
 
910
        if response[0][0] == 'ok':
 
911
            coded = bz2.decompress(response[1].read_body_bytes())
 
912
            if coded == '':
 
913
                # no revisions found
 
914
                return {}
 
915
            lines = coded.split('\n')
 
916
            revision_graph = {}
 
917
            for line in lines:
 
918
                d = tuple(line.split())
 
919
                if len(d) > 1:
 
920
                    revision_graph[d[0]] = d[1:]
 
921
                else:
 
922
                    # No parents - so give the Graph result (NULL_REVISION,).
 
923
                    revision_graph[d[0]] = (NULL_REVISION,)
 
924
            return revision_graph
 
925
 
 
926
    @needs_read_lock
 
927
    def get_signature_text(self, revision_id):
 
928
        self._ensure_real()
 
929
        return self._real_repository.get_signature_text(revision_id)
 
930
 
 
931
    @needs_read_lock
 
932
    @symbol_versioning.deprecated_method(symbol_versioning.one_three)
 
933
    def get_revision_graph_with_ghosts(self, revision_ids=None):
 
934
        self._ensure_real()
 
935
        return self._real_repository.get_revision_graph_with_ghosts(
 
936
            revision_ids=revision_ids)
 
937
 
 
938
    @needs_read_lock
 
939
    def get_inventory_xml(self, revision_id):
 
940
        self._ensure_real()
 
941
        return self._real_repository.get_inventory_xml(revision_id)
 
942
 
 
943
    def deserialise_inventory(self, revision_id, xml):
 
944
        self._ensure_real()
 
945
        return self._real_repository.deserialise_inventory(revision_id, xml)
 
946
 
 
947
    def reconcile(self, other=None, thorough=False):
 
948
        self._ensure_real()
 
949
        return self._real_repository.reconcile(other=other, thorough=thorough)
 
950
        
 
951
    def all_revision_ids(self):
 
952
        self._ensure_real()
 
953
        return self._real_repository.all_revision_ids()
 
954
    
 
955
    @needs_read_lock
 
956
    def get_deltas_for_revisions(self, revisions):
 
957
        self._ensure_real()
 
958
        return self._real_repository.get_deltas_for_revisions(revisions)
 
959
 
 
960
    @needs_read_lock
 
961
    def get_revision_delta(self, revision_id):
 
962
        self._ensure_real()
 
963
        return self._real_repository.get_revision_delta(revision_id)
 
964
 
 
965
    @needs_read_lock
 
966
    def revision_trees(self, revision_ids):
 
967
        self._ensure_real()
 
968
        return self._real_repository.revision_trees(revision_ids)
 
969
 
 
970
    @needs_read_lock
 
971
    def get_revision_reconcile(self, revision_id):
 
972
        self._ensure_real()
 
973
        return self._real_repository.get_revision_reconcile(revision_id)
 
974
 
 
975
    @needs_read_lock
 
976
    def check(self, revision_ids=None):
 
977
        self._ensure_real()
 
978
        return self._real_repository.check(revision_ids=revision_ids)
 
979
 
 
980
    def copy_content_into(self, destination, revision_id=None):
 
981
        self._ensure_real()
 
982
        return self._real_repository.copy_content_into(
 
983
            destination, revision_id=revision_id)
 
984
 
 
985
    def _copy_repository_tarball(self, to_bzrdir, revision_id=None):
 
986
        # get a tarball of the remote repository, and copy from that into the
 
987
        # destination
 
988
        from bzrlib import osutils
 
989
        import tarfile
 
990
        import tempfile
 
991
        # TODO: Maybe a progress bar while streaming the tarball?
 
992
        note("Copying repository content as tarball...")
 
993
        tar_file = self._get_tarball('bz2')
 
994
        if tar_file is None:
 
995
            return None
 
996
        destination = to_bzrdir.create_repository()
 
997
        try:
 
998
            tar = tarfile.open('repository', fileobj=tar_file,
 
999
                mode='r|bz2')
 
1000
            tmpdir = tempfile.mkdtemp()
 
1001
            try:
 
1002
                _extract_tar(tar, tmpdir)
 
1003
                tmp_bzrdir = BzrDir.open(tmpdir)
 
1004
                tmp_repo = tmp_bzrdir.open_repository()
 
1005
                tmp_repo.copy_content_into(destination, revision_id)
 
1006
            finally:
 
1007
                osutils.rmtree(tmpdir)
 
1008
        finally:
 
1009
            tar_file.close()
 
1010
        return destination
 
1011
        # TODO: Suggestion from john: using external tar is much faster than
 
1012
        # python's tarfile library, but it may not work on windows.
 
1013
 
 
1014
    @needs_write_lock
 
1015
    def pack(self):
 
1016
        """Compress the data within the repository.
 
1017
 
 
1018
        This is not currently implemented within the smart server.
 
1019
        """
 
1020
        self._ensure_real()
 
1021
        return self._real_repository.pack()
 
1022
 
 
1023
    def set_make_working_trees(self, new_value):
 
1024
        self._ensure_real()
 
1025
        self._real_repository.set_make_working_trees(new_value)
 
1026
 
 
1027
    @needs_write_lock
 
1028
    def sign_revision(self, revision_id, gpg_strategy):
 
1029
        self._ensure_real()
 
1030
        return self._real_repository.sign_revision(revision_id, gpg_strategy)
 
1031
 
 
1032
    @needs_read_lock
 
1033
    def get_revisions(self, revision_ids):
 
1034
        self._ensure_real()
 
1035
        return self._real_repository.get_revisions(revision_ids)
 
1036
 
 
1037
    def supports_rich_root(self):
 
1038
        self._ensure_real()
 
1039
        return self._real_repository.supports_rich_root()
 
1040
 
 
1041
    def iter_reverse_revision_history(self, revision_id):
 
1042
        self._ensure_real()
 
1043
        return self._real_repository.iter_reverse_revision_history(revision_id)
 
1044
 
 
1045
    @property
 
1046
    def _serializer(self):
 
1047
        self._ensure_real()
 
1048
        return self._real_repository._serializer
 
1049
 
 
1050
    def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
 
1051
        self._ensure_real()
 
1052
        return self._real_repository.store_revision_signature(
 
1053
            gpg_strategy, plaintext, revision_id)
 
1054
 
 
1055
    def add_signature_text(self, revision_id, signature):
 
1056
        self._ensure_real()
 
1057
        return self._real_repository.add_signature_text(revision_id, signature)
 
1058
 
 
1059
    def has_signature_for_revision_id(self, revision_id):
 
1060
        self._ensure_real()
 
1061
        return self._real_repository.has_signature_for_revision_id(revision_id)
 
1062
 
 
1063
    def get_data_stream_for_search(self, search):
 
1064
        medium = self._client._medium
 
1065
        if not medium._remote_is_at_least_1_2:
 
1066
            self._ensure_real()
 
1067
            return self._real_repository.get_data_stream_for_search(search)
 
1068
        REQUEST_NAME = 'Repository.stream_revisions_chunked'
 
1069
        path = self.bzrdir._path_for_remote_call(self._client)
 
1070
        body = self._serialise_search_recipe(search.get_recipe())
 
1071
        try:
 
1072
            result = self._client.call_with_body_bytes_expecting_body(
 
1073
                REQUEST_NAME, (path,), body)
 
1074
            response, protocol = result
 
1075
        except errors.UnknownSmartMethod:
 
1076
            # Server does not support this method, so fall back to VFS.
 
1077
            # Worse, we have to force a disconnection, because the server now
 
1078
            # doesn't realise it has a body on the wire to consume, so the
 
1079
            # only way to recover is to abandon the connection.
 
1080
            warning(
 
1081
                'Server is too old for streaming pull, reconnecting.  '
 
1082
                '(Upgrade the server to Bazaar 1.2 to avoid this)')
 
1083
            medium.disconnect()
 
1084
            # To avoid having to disconnect repeatedly, we keep track of the
 
1085
            # fact the server doesn't understand this remote method.
 
1086
            medium._remote_is_at_least_1_2 = False
 
1087
            self._ensure_real()
 
1088
            return self._real_repository.get_data_stream_for_search(search)
 
1089
 
 
1090
        if response == ('ok',):
 
1091
            return self._deserialise_stream(protocol)
 
1092
        if response == ('NoSuchRevision', ):
 
1093
            # We cannot easily identify the revision that is missing in this
 
1094
            # situation without doing much more network IO. For now, bail.
 
1095
            raise NoSuchRevision(self, "unknown")
 
1096
        else:
 
1097
            raise errors.UnexpectedSmartServerResponse(response)
 
1098
 
 
1099
    def _deserialise_stream(self, protocol):
 
1100
        stream = protocol.read_streamed_body()
 
1101
        container_parser = ContainerPushParser()
 
1102
        for bytes in stream:
 
1103
            container_parser.accept_bytes(bytes)
 
1104
            records = container_parser.read_pending_records()
 
1105
            for record_names, record_bytes in records:
 
1106
                if len(record_names) != 1:
 
1107
                    # These records should have only one name, and that name
 
1108
                    # should be a one-element tuple.
 
1109
                    raise errors.SmartProtocolError(
 
1110
                        'Repository data stream had invalid record name %r'
 
1111
                        % (record_names,))
 
1112
                name_tuple = record_names[0]
 
1113
                yield name_tuple, record_bytes
 
1114
 
 
1115
    def insert_data_stream(self, stream):
 
1116
        self._ensure_real()
 
1117
        self._real_repository.insert_data_stream(stream)
 
1118
 
 
1119
    def item_keys_introduced_by(self, revision_ids, _files_pb=None):
 
1120
        self._ensure_real()
 
1121
        return self._real_repository.item_keys_introduced_by(revision_ids,
 
1122
            _files_pb=_files_pb)
 
1123
 
 
1124
    def revision_graph_can_have_wrong_parents(self):
 
1125
        # The answer depends on the remote repo format.
 
1126
        self._ensure_real()
 
1127
        return self._real_repository.revision_graph_can_have_wrong_parents()
 
1128
 
 
1129
    def _find_inconsistent_revision_parents(self):
 
1130
        self._ensure_real()
 
1131
        return self._real_repository._find_inconsistent_revision_parents()
 
1132
 
 
1133
    def _check_for_inconsistent_revision_parents(self):
 
1134
        self._ensure_real()
 
1135
        return self._real_repository._check_for_inconsistent_revision_parents()
 
1136
 
 
1137
    def _make_parents_provider(self):
 
1138
        return self
 
1139
 
 
1140
    def _serialise_search_recipe(self, recipe):
 
1141
        """Serialise a graph search recipe.
 
1142
 
 
1143
        :param recipe: A search recipe (start, stop, count).
 
1144
        :return: Serialised bytes.
 
1145
        """
 
1146
        start_keys = ' '.join(recipe[0])
 
1147
        stop_keys = ' '.join(recipe[1])
 
1148
        count = str(recipe[2])
 
1149
        return '\n'.join((start_keys, stop_keys, count))
 
1150
 
 
1151
 
 
1152
class RemoteBranchLockableFiles(LockableFiles):
 
1153
    """A 'LockableFiles' implementation that talks to a smart server.
 
1154
    
 
1155
    This is not a public interface class.
 
1156
    """
 
1157
 
 
1158
    def __init__(self, bzrdir, _client):
 
1159
        self.bzrdir = bzrdir
 
1160
        self._client = _client
 
1161
        self._need_find_modes = True
 
1162
        LockableFiles.__init__(
 
1163
            self, bzrdir.get_branch_transport(None),
 
1164
            'lock', lockdir.LockDir)
 
1165
 
 
1166
    def _find_modes(self):
 
1167
        # RemoteBranches don't let the client set the mode of control files.
 
1168
        self._dir_mode = None
 
1169
        self._file_mode = None
 
1170
 
 
1171
 
 
1172
class RemoteBranchFormat(branch.BranchFormat):
 
1173
 
 
1174
    def __eq__(self, other):
 
1175
        return (isinstance(other, RemoteBranchFormat) and 
 
1176
            self.__dict__ == other.__dict__)
 
1177
 
 
1178
    def get_format_description(self):
 
1179
        return 'Remote BZR Branch'
 
1180
 
 
1181
    def get_format_string(self):
 
1182
        return 'Remote BZR Branch'
 
1183
 
 
1184
    def open(self, a_bzrdir):
 
1185
        return a_bzrdir.open_branch()
 
1186
 
 
1187
    def initialize(self, a_bzrdir):
 
1188
        return a_bzrdir.create_branch()
 
1189
 
 
1190
    def supports_tags(self):
 
1191
        # Remote branches might support tags, but we won't know until we
 
1192
        # access the real remote branch.
 
1193
        return True
 
1194
 
 
1195
 
 
1196
class RemoteBranch(branch.Branch):
 
1197
    """Branch stored on a server accessed by HPSS RPC.
 
1198
 
 
1199
    At the moment most operations are mapped down to simple file operations.
 
1200
    """
 
1201
 
 
1202
    def __init__(self, remote_bzrdir, remote_repository, real_branch=None,
 
1203
        _client=None):
 
1204
        """Create a RemoteBranch instance.
 
1205
 
 
1206
        :param real_branch: An optional local implementation of the branch
 
1207
            format, usually accessing the data via the VFS.
 
1208
        :param _client: Private parameter for testing.
 
1209
        """
 
1210
        # We intentionally don't call the parent class's __init__, because it
 
1211
        # will try to assign to self.tags, which is a property in this subclass.
 
1212
        # And the parent's __init__ doesn't do much anyway.
 
1213
        self._revision_id_to_revno_cache = None
 
1214
        self._revision_history_cache = None
 
1215
        self.bzrdir = remote_bzrdir
 
1216
        if _client is not None:
 
1217
            self._client = _client
 
1218
        else:
 
1219
            self._client = remote_bzrdir._client
 
1220
        self.repository = remote_repository
 
1221
        if real_branch is not None:
 
1222
            self._real_branch = real_branch
 
1223
            # Give the remote repository the matching real repo.
 
1224
            real_repo = self._real_branch.repository
 
1225
            if isinstance(real_repo, RemoteRepository):
 
1226
                real_repo._ensure_real()
 
1227
                real_repo = real_repo._real_repository
 
1228
            self.repository._set_real_repository(real_repo)
 
1229
            # Give the branch the remote repository to let fast-pathing happen.
 
1230
            self._real_branch.repository = self.repository
 
1231
        else:
 
1232
            self._real_branch = None
 
1233
        # Fill out expected attributes of branch for bzrlib api users.
 
1234
        self._format = RemoteBranchFormat()
 
1235
        self.base = self.bzrdir.root_transport.base
 
1236
        self._control_files = None
 
1237
        self._lock_mode = None
 
1238
        self._lock_token = None
 
1239
        self._repo_lock_token = None
 
1240
        self._lock_count = 0
 
1241
        self._leave_lock = False
 
1242
 
 
1243
    def __str__(self):
 
1244
        return "%s(%s)" % (self.__class__.__name__, self.base)
 
1245
 
 
1246
    __repr__ = __str__
 
1247
 
 
1248
    def _ensure_real(self):
 
1249
        """Ensure that there is a _real_branch set.
 
1250
 
 
1251
        Used before calls to self._real_branch.
 
1252
        """
 
1253
        if not self._real_branch:
 
1254
            if not vfs.vfs_enabled():
 
1255
                raise AssertionError('smart server vfs must be enabled '
 
1256
                    'to use vfs implementation')
 
1257
            self.bzrdir._ensure_real()
 
1258
            self._real_branch = self.bzrdir._real_bzrdir.open_branch()
 
1259
            # Give the remote repository the matching real repo.
 
1260
            real_repo = self._real_branch.repository
 
1261
            if isinstance(real_repo, RemoteRepository):
 
1262
                real_repo._ensure_real()
 
1263
                real_repo = real_repo._real_repository
 
1264
            self.repository._set_real_repository(real_repo)
 
1265
            # Give the branch the remote repository to let fast-pathing happen.
 
1266
            self._real_branch.repository = self.repository
 
1267
            # XXX: deal with _lock_mode == 'w'
 
1268
            if self._lock_mode == 'r':
 
1269
                self._real_branch.lock_read()
 
1270
 
 
1271
    @property
 
1272
    def control_files(self):
 
1273
        # Defer actually creating RemoteBranchLockableFiles until its needed,
 
1274
        # because it triggers an _ensure_real that we otherwise might not need.
 
1275
        if self._control_files is None:
 
1276
            self._control_files = RemoteBranchLockableFiles(
 
1277
                self.bzrdir, self._client)
 
1278
        return self._control_files
 
1279
 
 
1280
    def _get_checkout_format(self):
 
1281
        self._ensure_real()
 
1282
        return self._real_branch._get_checkout_format()
 
1283
 
 
1284
    def get_physical_lock_status(self):
 
1285
        """See Branch.get_physical_lock_status()."""
 
1286
        # should be an API call to the server, as branches must be lockable.
 
1287
        self._ensure_real()
 
1288
        return self._real_branch.get_physical_lock_status()
 
1289
 
 
1290
    def lock_read(self):
 
1291
        if not self._lock_mode:
 
1292
            self._lock_mode = 'r'
 
1293
            self._lock_count = 1
 
1294
            if self._real_branch is not None:
 
1295
                self._real_branch.lock_read()
 
1296
        else:
 
1297
            self._lock_count += 1
 
1298
 
 
1299
    def _remote_lock_write(self, token):
 
1300
        if token is None:
 
1301
            branch_token = repo_token = ''
 
1302
        else:
 
1303
            branch_token = token
 
1304
            repo_token = self.repository.lock_write()
 
1305
            self.repository.unlock()
 
1306
        path = self.bzrdir._path_for_remote_call(self._client)
 
1307
        response = self._client.call('Branch.lock_write', path, branch_token,
 
1308
                                     repo_token or '')
 
1309
        if response[0] == 'ok':
 
1310
            ok, branch_token, repo_token = response
 
1311
            return branch_token, repo_token
 
1312
        elif response[0] == 'LockContention':
 
1313
            raise errors.LockContention('(remote lock)')
 
1314
        elif response[0] == 'TokenMismatch':
 
1315
            raise errors.TokenMismatch(token, '(remote token)')
 
1316
        elif response[0] == 'UnlockableTransport':
 
1317
            raise errors.UnlockableTransport(self.bzrdir.root_transport)
 
1318
        elif response[0] == 'ReadOnlyError':
 
1319
            raise errors.ReadOnlyError(self)
 
1320
        elif response[0] == 'LockFailed':
 
1321
            raise errors.LockFailed(response[1], response[2])
 
1322
        else:
 
1323
            raise errors.UnexpectedSmartServerResponse(response)
 
1324
            
 
1325
    def lock_write(self, token=None):
 
1326
        if not self._lock_mode:
 
1327
            remote_tokens = self._remote_lock_write(token)
 
1328
            self._lock_token, self._repo_lock_token = remote_tokens
 
1329
            if not self._lock_token:
 
1330
                raise SmartProtocolError('Remote server did not return a token!')
 
1331
            # TODO: We really, really, really don't want to call _ensure_real
 
1332
            # here, but it's the easiest way to ensure coherency between the
 
1333
            # state of the RemoteBranch and RemoteRepository objects and the
 
1334
            # physical locks.  If we don't materialise the real objects here,
 
1335
            # then getting everything in the right state later is complex, so
 
1336
            # for now we just do it the lazy way.
 
1337
            #   -- Andrew Bennetts, 2007-02-22.
 
1338
            self._ensure_real()
 
1339
            if self._real_branch is not None:
 
1340
                self._real_branch.repository.lock_write(
 
1341
                    token=self._repo_lock_token)
 
1342
                try:
 
1343
                    self._real_branch.lock_write(token=self._lock_token)
 
1344
                finally:
 
1345
                    self._real_branch.repository.unlock()
 
1346
            if token is not None:
 
1347
                self._leave_lock = True
 
1348
            else:
 
1349
                # XXX: this case seems to be unreachable; token cannot be None.
 
1350
                self._leave_lock = False
 
1351
            self._lock_mode = 'w'
 
1352
            self._lock_count = 1
 
1353
        elif self._lock_mode == 'r':
 
1354
            raise errors.ReadOnlyTransaction
 
1355
        else:
 
1356
            if token is not None:
 
1357
                # A token was given to lock_write, and we're relocking, so check
 
1358
                # that the given token actually matches the one we already have.
 
1359
                if token != self._lock_token:
 
1360
                    raise errors.TokenMismatch(token, self._lock_token)
 
1361
            self._lock_count += 1
 
1362
        return self._lock_token or None
 
1363
 
 
1364
    def _unlock(self, branch_token, repo_token):
 
1365
        path = self.bzrdir._path_for_remote_call(self._client)
 
1366
        response = self._client.call('Branch.unlock', path, branch_token,
 
1367
                                     repo_token or '')
 
1368
        if response == ('ok',):
 
1369
            return
 
1370
        elif response[0] == 'TokenMismatch':
 
1371
            raise errors.TokenMismatch(
 
1372
                str((branch_token, repo_token)), '(remote tokens)')
 
1373
        else:
 
1374
            raise errors.UnexpectedSmartServerResponse(response)
 
1375
 
 
1376
    def unlock(self):
 
1377
        self._lock_count -= 1
 
1378
        if not self._lock_count:
 
1379
            self._clear_cached_state()
 
1380
            mode = self._lock_mode
 
1381
            self._lock_mode = None
 
1382
            if self._real_branch is not None:
 
1383
                if (not self._leave_lock and mode == 'w' and
 
1384
                    self._repo_lock_token):
 
1385
                    # If this RemoteBranch will remove the physical lock for the
 
1386
                    # repository, make sure the _real_branch doesn't do it
 
1387
                    # first.  (Because the _real_branch's repository is set to
 
1388
                    # be the RemoteRepository.)
 
1389
                    self._real_branch.repository.leave_lock_in_place()
 
1390
                self._real_branch.unlock()
 
1391
            if mode != 'w':
 
1392
                # Only write-locked branched need to make a remote method call
 
1393
                # to perfom the unlock.
 
1394
                return
 
1395
            if not self._lock_token:
 
1396
                raise AssertionError('Locked, but no token!')
 
1397
            branch_token = self._lock_token
 
1398
            repo_token = self._repo_lock_token
 
1399
            self._lock_token = None
 
1400
            self._repo_lock_token = None
 
1401
            if not self._leave_lock:
 
1402
                self._unlock(branch_token, repo_token)
 
1403
 
 
1404
    def break_lock(self):
 
1405
        self._ensure_real()
 
1406
        return self._real_branch.break_lock()
 
1407
 
 
1408
    def leave_lock_in_place(self):
 
1409
        if not self._lock_token:
 
1410
            raise NotImplementedError(self.leave_lock_in_place)
 
1411
        self._leave_lock = True
 
1412
 
 
1413
    def dont_leave_lock_in_place(self):
 
1414
        if not self._lock_token:
 
1415
            raise NotImplementedError(self.dont_leave_lock_in_place)
 
1416
        self._leave_lock = False
 
1417
 
 
1418
    def last_revision_info(self):
 
1419
        """See Branch.last_revision_info()."""
 
1420
        path = self.bzrdir._path_for_remote_call(self._client)
 
1421
        response = self._client.call('Branch.last_revision_info', path)
 
1422
        if response[0] != 'ok':
 
1423
            raise SmartProtocolError('unexpected response code %s' % (response,))
 
1424
        revno = int(response[1])
 
1425
        last_revision = response[2]
 
1426
        return (revno, last_revision)
 
1427
 
 
1428
    def _gen_revision_history(self):
 
1429
        """See Branch._gen_revision_history()."""
 
1430
        path = self.bzrdir._path_for_remote_call(self._client)
 
1431
        response = self._client.call_expecting_body(
 
1432
            'Branch.revision_history', path)
 
1433
        if response[0][0] != 'ok':
 
1434
            raise SmartProtocolError('unexpected response code %s' % (response,))
 
1435
        result = response[1].read_body_bytes().split('\x00')
 
1436
        if result == ['']:
 
1437
            return []
 
1438
        return result
 
1439
 
 
1440
    @needs_write_lock
 
1441
    def set_revision_history(self, rev_history):
 
1442
        # Send just the tip revision of the history; the server will generate
 
1443
        # the full history from that.  If the revision doesn't exist in this
 
1444
        # branch, NoSuchRevision will be raised.
 
1445
        path = self.bzrdir._path_for_remote_call(self._client)
 
1446
        if rev_history == []:
 
1447
            rev_id = 'null:'
 
1448
        else:
 
1449
            rev_id = rev_history[-1]
 
1450
        self._clear_cached_state()
 
1451
        response = self._client.call('Branch.set_last_revision',
 
1452
            path, self._lock_token, self._repo_lock_token, rev_id)
 
1453
        if response[0] == 'NoSuchRevision':
 
1454
            raise NoSuchRevision(self, rev_id)
 
1455
        elif response[0] != 'ok':
 
1456
            raise SmartProtocolError('unexpected response code %s' % (response,))
 
1457
        self._cache_revision_history(rev_history)
 
1458
 
 
1459
    def get_parent(self):
 
1460
        self._ensure_real()
 
1461
        return self._real_branch.get_parent()
 
1462
        
 
1463
    def set_parent(self, url):
 
1464
        self._ensure_real()
 
1465
        return self._real_branch.set_parent(url)
 
1466
        
 
1467
    def sprout(self, to_bzrdir, revision_id=None):
 
1468
        # Like Branch.sprout, except that it sprouts a branch in the default
 
1469
        # format, because RemoteBranches can't be created at arbitrary URLs.
 
1470
        # XXX: if to_bzrdir is a RemoteBranch, this should perhaps do
 
1471
        # to_bzrdir.create_branch...
 
1472
        self._ensure_real()
 
1473
        result = self._real_branch._format.initialize(to_bzrdir)
 
1474
        self.copy_content_into(result, revision_id=revision_id)
 
1475
        result.set_parent(self.bzrdir.root_transport.base)
 
1476
        return result
 
1477
 
 
1478
    @needs_write_lock
 
1479
    def pull(self, source, overwrite=False, stop_revision=None,
 
1480
             **kwargs):
 
1481
        # FIXME: This asks the real branch to run the hooks, which means
 
1482
        # they're called with the wrong target branch parameter. 
 
1483
        # The test suite specifically allows this at present but it should be
 
1484
        # fixed.  It should get a _override_hook_target branch,
 
1485
        # as push does.  -- mbp 20070405
 
1486
        self._ensure_real()
 
1487
        self._real_branch.pull(
 
1488
            source, overwrite=overwrite, stop_revision=stop_revision,
 
1489
            **kwargs)
 
1490
 
 
1491
    @needs_read_lock
 
1492
    def push(self, target, overwrite=False, stop_revision=None):
 
1493
        self._ensure_real()
 
1494
        return self._real_branch.push(
 
1495
            target, overwrite=overwrite, stop_revision=stop_revision,
 
1496
            _override_hook_source_branch=self)
 
1497
 
 
1498
    def is_locked(self):
 
1499
        return self._lock_count >= 1
 
1500
 
 
1501
    @needs_write_lock
 
1502
    def set_last_revision_info(self, revno, revision_id):
 
1503
        revision_id = ensure_null(revision_id)
 
1504
        path = self.bzrdir._path_for_remote_call(self._client)
 
1505
        try:
 
1506
            response = self._client.call('Branch.set_last_revision_info',
 
1507
                path, self._lock_token, self._repo_lock_token, str(revno), revision_id)
 
1508
        except errors.UnknownSmartMethod:
 
1509
            self._ensure_real()
 
1510
            self._clear_cached_state()
 
1511
            return self._real_branch.set_last_revision_info(revno, revision_id)
 
1512
        if response == ('ok',):
 
1513
            self._clear_cached_state()
 
1514
        elif response[0] == 'NoSuchRevision':
 
1515
            raise NoSuchRevision(self, response[1])
 
1516
        else:
 
1517
            raise errors.UnexpectedSmartServerResponse(response)
 
1518
 
 
1519
    def generate_revision_history(self, revision_id, last_rev=None,
 
1520
                                  other_branch=None):
 
1521
        self._ensure_real()
 
1522
        return self._real_branch.generate_revision_history(
 
1523
            revision_id, last_rev=last_rev, other_branch=other_branch)
 
1524
 
 
1525
    @property
 
1526
    def tags(self):
 
1527
        self._ensure_real()
 
1528
        return self._real_branch.tags
 
1529
 
 
1530
    def set_push_location(self, location):
 
1531
        self._ensure_real()
 
1532
        return self._real_branch.set_push_location(location)
 
1533
 
 
1534
    def update_revisions(self, other, stop_revision=None, overwrite=False):
 
1535
        self._ensure_real()
 
1536
        return self._real_branch.update_revisions(
 
1537
            other, stop_revision=stop_revision, overwrite=overwrite)
 
1538
 
 
1539
 
 
1540
def _extract_tar(tar, to_dir):
 
1541
    """Extract all the contents of a tarfile object.
 
1542
 
 
1543
    A replacement for extractall, which is not present in python2.4
 
1544
    """
 
1545
    for tarinfo in tar:
 
1546
        tar.extract(tarinfo, to_dir)