/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

First cut at pluralised VersionedFiles. Some rather massive API incompatabilities, primarily because of the difficulty of coherence among competing stores.

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
    def get_transaction(self):
 
713
        self._ensure_real()
 
714
        return self._real_repository.get_transaction()
 
715
 
 
716
    @needs_read_lock
 
717
    def clone(self, a_bzrdir, revision_id=None):
 
718
        self._ensure_real()
 
719
        return self._real_repository.clone(a_bzrdir, revision_id=revision_id)
 
720
 
 
721
    def make_working_trees(self):
 
722
        """See Repository.make_working_trees"""
 
723
        self._ensure_real()
 
724
        return self._real_repository.make_working_trees()
 
725
 
 
726
    def revision_ids_to_search_result(self, result_set):
 
727
        """Convert a set of revision ids to a graph SearchResult."""
 
728
        result_parents = set()
 
729
        for parents in self.get_graph().get_parent_map(
 
730
            result_set).itervalues():
 
731
            result_parents.update(parents)
 
732
        included_keys = result_set.intersection(result_parents)
 
733
        start_keys = result_set.difference(included_keys)
 
734
        exclude_keys = result_parents.difference(result_set)
 
735
        result = graph.SearchResult(start_keys, exclude_keys,
 
736
            len(result_set), result_set)
 
737
        return result
 
738
 
 
739
    @needs_read_lock
 
740
    def search_missing_revision_ids(self, other, revision_id=None, find_ghosts=True):
 
741
        """Return the revision ids that other has that this does not.
 
742
        
 
743
        These are returned in topological order.
 
744
 
 
745
        revision_id: only return revision ids included by revision_id.
 
746
        """
 
747
        return repository.InterRepository.get(
 
748
            other, self).search_missing_revision_ids(revision_id, find_ghosts)
 
749
 
 
750
    def fetch(self, source, revision_id=None, pb=None):
 
751
        if self.has_same_location(source):
 
752
            # check that last_revision is in 'from' and then return a
 
753
            # no-operation.
 
754
            if (revision_id is not None and
 
755
                not revision.is_null(revision_id)):
 
756
                self.get_revision(revision_id)
 
757
            return 0, []
 
758
        self._ensure_real()
 
759
        return self._real_repository.fetch(
 
760
            source, revision_id=revision_id, pb=pb)
 
761
 
 
762
    def create_bundle(self, target, base, fileobj, format=None):
 
763
        self._ensure_real()
 
764
        self._real_repository.create_bundle(target, base, fileobj, format)
 
765
 
 
766
    @needs_read_lock
 
767
    def get_ancestry(self, revision_id, topo_sorted=True):
 
768
        self._ensure_real()
 
769
        return self._real_repository.get_ancestry(revision_id, topo_sorted)
 
770
 
 
771
    @property
 
772
    def inventories(self):
 
773
        self._ensure_real()
 
774
        return self._real_repository.inventories
 
775
 
 
776
    def fileids_altered_by_revision_ids(self, revision_ids):
 
777
        self._ensure_real()
 
778
        return self._real_repository.fileids_altered_by_revision_ids(revision_ids)
 
779
 
 
780
    def _get_versioned_file_checker(self, revisions, revision_versions_cache):
 
781
        self._ensure_real()
 
782
        return self._real_repository._get_versioned_file_checker(
 
783
            revisions, revision_versions_cache)
 
784
        
 
785
    def iter_files_bytes(self, desired_files):
 
786
        """See Repository.iter_file_bytes.
 
787
        """
 
788
        self._ensure_real()
 
789
        return self._real_repository.iter_files_bytes(desired_files)
 
790
 
 
791
    def get_parent_map(self, keys):
 
792
        """See bzrlib.Graph.get_parent_map()."""
 
793
        # Hack to build up the caching logic.
 
794
        ancestry = self._parents_map
 
795
        if ancestry is None:
 
796
            # Repository is not locked, so there's no cache.
 
797
            missing_revisions = set(keys)
 
798
            ancestry = {}
 
799
        else:
 
800
            missing_revisions = set(key for key in keys if key not in ancestry)
 
801
        if missing_revisions:
 
802
            parent_map = self._get_parent_map(missing_revisions)
 
803
            if 'hpss' in debug.debug_flags:
 
804
                mutter('retransmitted revisions: %d of %d',
 
805
                        len(set(ancestry).intersection(parent_map)),
 
806
                        len(parent_map))
 
807
            ancestry.update(parent_map)
 
808
        present_keys = [k for k in keys if k in ancestry]
 
809
        if 'hpss' in debug.debug_flags:
 
810
            self._requested_parents.update(present_keys)
 
811
            mutter('Current RemoteRepository graph hit rate: %d%%',
 
812
                100.0 * len(self._requested_parents) / len(ancestry))
 
813
        return dict((k, ancestry[k]) for k in present_keys)
 
814
 
 
815
    def _get_parent_map(self, keys):
 
816
        """Helper for get_parent_map that performs the RPC."""
 
817
        medium = self._client._medium
 
818
        if not medium._remote_is_at_least_1_2:
 
819
            # We already found out that the server can't understand
 
820
            # Repository.get_parent_map requests, so just fetch the whole
 
821
            # graph.
 
822
            # XXX: Note that this will issue a deprecation warning. This is ok
 
823
            # :- its because we're working with a deprecated server anyway, and
 
824
            # the user will almost certainly have seen a warning about the
 
825
            # server version already.
 
826
            rg = self.get_revision_graph()
 
827
            # There is an api discrepency between get_parent_map and
 
828
            # get_revision_graph. Specifically, a "key:()" pair in
 
829
            # get_revision_graph just means a node has no parents. For
 
830
            # "get_parent_map" it means the node is a ghost. So fix up the
 
831
            # graph to correct this.
 
832
            #   https://bugs.launchpad.net/bzr/+bug/214894
 
833
            # There is one other "bug" which is that ghosts in
 
834
            # get_revision_graph() are not returned at all. But we won't worry
 
835
            # about that for now.
 
836
            for node_id, parent_ids in rg.iteritems():
 
837
                if parent_ids == ():
 
838
                    rg[node_id] = (NULL_REVISION,)
 
839
            rg[NULL_REVISION] = ()
 
840
            return rg
 
841
 
 
842
        keys = set(keys)
 
843
        if NULL_REVISION in keys:
 
844
            keys.discard(NULL_REVISION)
 
845
            found_parents = {NULL_REVISION:()}
 
846
            if not keys:
 
847
                return found_parents
 
848
        else:
 
849
            found_parents = {}
 
850
        # TODO(Needs analysis): We could assume that the keys being requested
 
851
        # from get_parent_map are in a breadth first search, so typically they
 
852
        # will all be depth N from some common parent, and we don't have to
 
853
        # have the server iterate from the root parent, but rather from the
 
854
        # keys we're searching; and just tell the server the keyspace we
 
855
        # already have; but this may be more traffic again.
 
856
 
 
857
        # Transform self._parents_map into a search request recipe.
 
858
        # TODO: Manage this incrementally to avoid covering the same path
 
859
        # repeatedly. (The server will have to on each request, but the less
 
860
        # work done the better).
 
861
        parents_map = self._parents_map
 
862
        if parents_map is None:
 
863
            # Repository is not locked, so there's no cache.
 
864
            parents_map = {}
 
865
        start_set = set(parents_map)
 
866
        result_parents = set()
 
867
        for parents in parents_map.itervalues():
 
868
            result_parents.update(parents)
 
869
        stop_keys = result_parents.difference(start_set)
 
870
        included_keys = start_set.intersection(result_parents)
 
871
        start_set.difference_update(included_keys)
 
872
        recipe = (start_set, stop_keys, len(parents_map))
 
873
        body = self._serialise_search_recipe(recipe)
 
874
        path = self.bzrdir._path_for_remote_call(self._client)
 
875
        for key in keys:
 
876
            if type(key) is not str:
 
877
                raise ValueError(
 
878
                    "key %r not a plain string" % (key,))
 
879
        verb = 'Repository.get_parent_map'
 
880
        args = (path,) + tuple(keys)
 
881
        try:
 
882
            response = self._client.call_with_body_bytes_expecting_body(
 
883
                verb, args, self._serialise_search_recipe(recipe))
 
884
        except errors.UnknownSmartMethod:
 
885
            # Server does not support this method, so get the whole graph.
 
886
            # Worse, we have to force a disconnection, because the server now
 
887
            # doesn't realise it has a body on the wire to consume, so the
 
888
            # only way to recover is to abandon the connection.
 
889
            warning(
 
890
                'Server is too old for fast get_parent_map, reconnecting.  '
 
891
                '(Upgrade the server to Bazaar 1.2 to avoid this)')
 
892
            medium.disconnect()
 
893
            # To avoid having to disconnect repeatedly, we keep track of the
 
894
            # fact the server doesn't understand remote methods added in 1.2.
 
895
            medium._remote_is_at_least_1_2 = False
 
896
            return self.get_revision_graph(None)
 
897
        if response[0][0] not in ['ok']:
 
898
            response[1].cancel_read_body()
 
899
            raise errors.UnexpectedSmartServerResponse(response[0])
 
900
        if response[0][0] == 'ok':
 
901
            coded = bz2.decompress(response[1].read_body_bytes())
 
902
            if coded == '':
 
903
                # no revisions found
 
904
                return {}
 
905
            lines = coded.split('\n')
 
906
            revision_graph = {}
 
907
            for line in lines:
 
908
                d = tuple(line.split())
 
909
                if len(d) > 1:
 
910
                    revision_graph[d[0]] = d[1:]
 
911
                else:
 
912
                    # No parents - so give the Graph result (NULL_REVISION,).
 
913
                    revision_graph[d[0]] = (NULL_REVISION,)
 
914
            return revision_graph
 
915
 
 
916
    @needs_read_lock
 
917
    def get_signature_text(self, revision_id):
 
918
        self._ensure_real()
 
919
        return self._real_repository.get_signature_text(revision_id)
 
920
 
 
921
    @needs_read_lock
 
922
    @symbol_versioning.deprecated_method(symbol_versioning.one_three)
 
923
    def get_revision_graph_with_ghosts(self, revision_ids=None):
 
924
        self._ensure_real()
 
925
        return self._real_repository.get_revision_graph_with_ghosts(
 
926
            revision_ids=revision_ids)
 
927
 
 
928
    @needs_read_lock
 
929
    def get_inventory_xml(self, revision_id):
 
930
        self._ensure_real()
 
931
        return self._real_repository.get_inventory_xml(revision_id)
 
932
 
 
933
    def deserialise_inventory(self, revision_id, xml):
 
934
        self._ensure_real()
 
935
        return self._real_repository.deserialise_inventory(revision_id, xml)
 
936
 
 
937
    def reconcile(self, other=None, thorough=False):
 
938
        self._ensure_real()
 
939
        return self._real_repository.reconcile(other=other, thorough=thorough)
 
940
        
 
941
    def all_revision_ids(self):
 
942
        self._ensure_real()
 
943
        return self._real_repository.all_revision_ids()
 
944
    
 
945
    @needs_read_lock
 
946
    def get_deltas_for_revisions(self, revisions):
 
947
        self._ensure_real()
 
948
        return self._real_repository.get_deltas_for_revisions(revisions)
 
949
 
 
950
    @needs_read_lock
 
951
    def get_revision_delta(self, revision_id):
 
952
        self._ensure_real()
 
953
        return self._real_repository.get_revision_delta(revision_id)
 
954
 
 
955
    @needs_read_lock
 
956
    def revision_trees(self, revision_ids):
 
957
        self._ensure_real()
 
958
        return self._real_repository.revision_trees(revision_ids)
 
959
 
 
960
    @needs_read_lock
 
961
    def get_revision_reconcile(self, revision_id):
 
962
        self._ensure_real()
 
963
        return self._real_repository.get_revision_reconcile(revision_id)
 
964
 
 
965
    @needs_read_lock
 
966
    def check(self, revision_ids=None):
 
967
        self._ensure_real()
 
968
        return self._real_repository.check(revision_ids=revision_ids)
 
969
 
 
970
    def copy_content_into(self, destination, revision_id=None):
 
971
        self._ensure_real()
 
972
        return self._real_repository.copy_content_into(
 
973
            destination, revision_id=revision_id)
 
974
 
 
975
    def _copy_repository_tarball(self, to_bzrdir, revision_id=None):
 
976
        # get a tarball of the remote repository, and copy from that into the
 
977
        # destination
 
978
        from bzrlib import osutils
 
979
        import tarfile
 
980
        import tempfile
 
981
        # TODO: Maybe a progress bar while streaming the tarball?
 
982
        note("Copying repository content as tarball...")
 
983
        tar_file = self._get_tarball('bz2')
 
984
        if tar_file is None:
 
985
            return None
 
986
        destination = to_bzrdir.create_repository()
 
987
        try:
 
988
            tar = tarfile.open('repository', fileobj=tar_file,
 
989
                mode='r|bz2')
 
990
            tmpdir = tempfile.mkdtemp()
 
991
            try:
 
992
                _extract_tar(tar, tmpdir)
 
993
                tmp_bzrdir = BzrDir.open(tmpdir)
 
994
                tmp_repo = tmp_bzrdir.open_repository()
 
995
                tmp_repo.copy_content_into(destination, revision_id)
 
996
            finally:
 
997
                osutils.rmtree(tmpdir)
 
998
        finally:
 
999
            tar_file.close()
 
1000
        return destination
 
1001
        # TODO: Suggestion from john: using external tar is much faster than
 
1002
        # python's tarfile library, but it may not work on windows.
 
1003
 
 
1004
    @property
 
1005
    def inventories(self):
 
1006
        """Decorate the real repository for now.
 
1007
 
 
1008
        In the long term a full blown network facility is needed to
 
1009
        avoid creating a real repository object locally.
 
1010
        """
 
1011
        self._ensure_real()
 
1012
        return self._real_repository.inventories
 
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
    @property
 
1024
    def revisions(self):
 
1025
        """Decorate the real repository for now.
 
1026
 
 
1027
        In the short term this should become a real object to intercept graph
 
1028
        lookups.
 
1029
 
 
1030
        In the long term a full blown network facility is needed.
 
1031
        """
 
1032
        self._ensure_real()
 
1033
        return self._real_repository.revisions
 
1034
 
 
1035
    def set_make_working_trees(self, new_value):
 
1036
        self._ensure_real()
 
1037
        self._real_repository.set_make_working_trees(new_value)
 
1038
 
 
1039
    @property
 
1040
    def signatures(self):
 
1041
        """Decorate the real repository for now.
 
1042
 
 
1043
        In the long term a full blown network facility is needed to avoid
 
1044
        creating a real repository object locally.
 
1045
        """
 
1046
        self._ensure_real()
 
1047
        return self._real_repository.signatures
 
1048
 
 
1049
    @needs_write_lock
 
1050
    def sign_revision(self, revision_id, gpg_strategy):
 
1051
        self._ensure_real()
 
1052
        return self._real_repository.sign_revision(revision_id, gpg_strategy)
 
1053
 
 
1054
    @property
 
1055
    def texts(self):
 
1056
        """Decorate the real repository for now.
 
1057
 
 
1058
        In the long term a full blown network facility is needed to avoid
 
1059
        creating a real repository object locally.
 
1060
        """
 
1061
        self._ensure_real()
 
1062
        return self._real_repository.texts
 
1063
 
 
1064
    @needs_read_lock
 
1065
    def get_revisions(self, revision_ids):
 
1066
        self._ensure_real()
 
1067
        return self._real_repository.get_revisions(revision_ids)
 
1068
 
 
1069
    def supports_rich_root(self):
 
1070
        self._ensure_real()
 
1071
        return self._real_repository.supports_rich_root()
 
1072
 
 
1073
    def iter_reverse_revision_history(self, revision_id):
 
1074
        self._ensure_real()
 
1075
        return self._real_repository.iter_reverse_revision_history(revision_id)
 
1076
 
 
1077
    @property
 
1078
    def _serializer(self):
 
1079
        self._ensure_real()
 
1080
        return self._real_repository._serializer
 
1081
 
 
1082
    def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
 
1083
        self._ensure_real()
 
1084
        return self._real_repository.store_revision_signature(
 
1085
            gpg_strategy, plaintext, revision_id)
 
1086
 
 
1087
    def add_signature_text(self, revision_id, signature):
 
1088
        self._ensure_real()
 
1089
        return self._real_repository.add_signature_text(revision_id, signature)
 
1090
 
 
1091
    def has_signature_for_revision_id(self, revision_id):
 
1092
        self._ensure_real()
 
1093
        return self._real_repository.has_signature_for_revision_id(revision_id)
 
1094
 
 
1095
    def item_keys_introduced_by(self, revision_ids, _files_pb=None):
 
1096
        self._ensure_real()
 
1097
        return self._real_repository.item_keys_introduced_by(revision_ids,
 
1098
            _files_pb=_files_pb)
 
1099
 
 
1100
    def revision_graph_can_have_wrong_parents(self):
 
1101
        # The answer depends on the remote repo format.
 
1102
        self._ensure_real()
 
1103
        return self._real_repository.revision_graph_can_have_wrong_parents()
 
1104
 
 
1105
    def _find_inconsistent_revision_parents(self):
 
1106
        self._ensure_real()
 
1107
        return self._real_repository._find_inconsistent_revision_parents()
 
1108
 
 
1109
    def _check_for_inconsistent_revision_parents(self):
 
1110
        self._ensure_real()
 
1111
        return self._real_repository._check_for_inconsistent_revision_parents()
 
1112
 
 
1113
    def _make_parents_provider(self):
 
1114
        return self
 
1115
 
 
1116
    def _serialise_search_recipe(self, recipe):
 
1117
        """Serialise a graph search recipe.
 
1118
 
 
1119
        :param recipe: A search recipe (start, stop, count).
 
1120
        :return: Serialised bytes.
 
1121
        """
 
1122
        start_keys = ' '.join(recipe[0])
 
1123
        stop_keys = ' '.join(recipe[1])
 
1124
        count = str(recipe[2])
 
1125
        return '\n'.join((start_keys, stop_keys, count))
 
1126
 
 
1127
 
 
1128
class RemoteBranchLockableFiles(LockableFiles):
 
1129
    """A 'LockableFiles' implementation that talks to a smart server.
 
1130
    
 
1131
    This is not a public interface class.
 
1132
    """
 
1133
 
 
1134
    def __init__(self, bzrdir, _client):
 
1135
        self.bzrdir = bzrdir
 
1136
        self._client = _client
 
1137
        self._need_find_modes = True
 
1138
        LockableFiles.__init__(
 
1139
            self, bzrdir.get_branch_transport(None),
 
1140
            'lock', lockdir.LockDir)
 
1141
 
 
1142
    def _find_modes(self):
 
1143
        # RemoteBranches don't let the client set the mode of control files.
 
1144
        self._dir_mode = None
 
1145
        self._file_mode = None
 
1146
 
 
1147
 
 
1148
class RemoteBranchFormat(branch.BranchFormat):
 
1149
 
 
1150
    def __eq__(self, other):
 
1151
        return (isinstance(other, RemoteBranchFormat) and 
 
1152
            self.__dict__ == other.__dict__)
 
1153
 
 
1154
    def get_format_description(self):
 
1155
        return 'Remote BZR Branch'
 
1156
 
 
1157
    def get_format_string(self):
 
1158
        return 'Remote BZR Branch'
 
1159
 
 
1160
    def open(self, a_bzrdir):
 
1161
        return a_bzrdir.open_branch()
 
1162
 
 
1163
    def initialize(self, a_bzrdir):
 
1164
        return a_bzrdir.create_branch()
 
1165
 
 
1166
    def supports_tags(self):
 
1167
        # Remote branches might support tags, but we won't know until we
 
1168
        # access the real remote branch.
 
1169
        return True
 
1170
 
 
1171
 
 
1172
class RemoteBranch(branch.Branch):
 
1173
    """Branch stored on a server accessed by HPSS RPC.
 
1174
 
 
1175
    At the moment most operations are mapped down to simple file operations.
 
1176
    """
 
1177
 
 
1178
    def __init__(self, remote_bzrdir, remote_repository, real_branch=None,
 
1179
        _client=None):
 
1180
        """Create a RemoteBranch instance.
 
1181
 
 
1182
        :param real_branch: An optional local implementation of the branch
 
1183
            format, usually accessing the data via the VFS.
 
1184
        :param _client: Private parameter for testing.
 
1185
        """
 
1186
        # We intentionally don't call the parent class's __init__, because it
 
1187
        # will try to assign to self.tags, which is a property in this subclass.
 
1188
        # And the parent's __init__ doesn't do much anyway.
 
1189
        self._revision_id_to_revno_cache = None
 
1190
        self._revision_history_cache = None
 
1191
        self.bzrdir = remote_bzrdir
 
1192
        if _client is not None:
 
1193
            self._client = _client
 
1194
        else:
 
1195
            self._client = remote_bzrdir._client
 
1196
        self.repository = remote_repository
 
1197
        if real_branch is not None:
 
1198
            self._real_branch = real_branch
 
1199
            # Give the remote repository the matching real repo.
 
1200
            real_repo = self._real_branch.repository
 
1201
            if isinstance(real_repo, RemoteRepository):
 
1202
                real_repo._ensure_real()
 
1203
                real_repo = real_repo._real_repository
 
1204
            self.repository._set_real_repository(real_repo)
 
1205
            # Give the branch the remote repository to let fast-pathing happen.
 
1206
            self._real_branch.repository = self.repository
 
1207
        else:
 
1208
            self._real_branch = None
 
1209
        # Fill out expected attributes of branch for bzrlib api users.
 
1210
        self._format = RemoteBranchFormat()
 
1211
        self.base = self.bzrdir.root_transport.base
 
1212
        self._control_files = None
 
1213
        self._lock_mode = None
 
1214
        self._lock_token = None
 
1215
        self._repo_lock_token = None
 
1216
        self._lock_count = 0
 
1217
        self._leave_lock = False
 
1218
 
 
1219
    def __str__(self):
 
1220
        return "%s(%s)" % (self.__class__.__name__, self.base)
 
1221
 
 
1222
    __repr__ = __str__
 
1223
 
 
1224
    def _ensure_real(self):
 
1225
        """Ensure that there is a _real_branch set.
 
1226
 
 
1227
        Used before calls to self._real_branch.
 
1228
        """
 
1229
        if not self._real_branch:
 
1230
            if not vfs.vfs_enabled():
 
1231
                raise AssertionError('smart server vfs must be enabled '
 
1232
                    'to use vfs implementation')
 
1233
            self.bzrdir._ensure_real()
 
1234
            self._real_branch = self.bzrdir._real_bzrdir.open_branch()
 
1235
            # Give the remote repository the matching real repo.
 
1236
            real_repo = self._real_branch.repository
 
1237
            if isinstance(real_repo, RemoteRepository):
 
1238
                real_repo._ensure_real()
 
1239
                real_repo = real_repo._real_repository
 
1240
            self.repository._set_real_repository(real_repo)
 
1241
            # Give the branch the remote repository to let fast-pathing happen.
 
1242
            self._real_branch.repository = self.repository
 
1243
            # XXX: deal with _lock_mode == 'w'
 
1244
            if self._lock_mode == 'r':
 
1245
                self._real_branch.lock_read()
 
1246
 
 
1247
    @property
 
1248
    def control_files(self):
 
1249
        # Defer actually creating RemoteBranchLockableFiles until its needed,
 
1250
        # because it triggers an _ensure_real that we otherwise might not need.
 
1251
        if self._control_files is None:
 
1252
            self._control_files = RemoteBranchLockableFiles(
 
1253
                self.bzrdir, self._client)
 
1254
        return self._control_files
 
1255
 
 
1256
    def _get_checkout_format(self):
 
1257
        self._ensure_real()
 
1258
        return self._real_branch._get_checkout_format()
 
1259
 
 
1260
    def get_physical_lock_status(self):
 
1261
        """See Branch.get_physical_lock_status()."""
 
1262
        # should be an API call to the server, as branches must be lockable.
 
1263
        self._ensure_real()
 
1264
        return self._real_branch.get_physical_lock_status()
 
1265
 
 
1266
    def lock_read(self):
 
1267
        if not self._lock_mode:
 
1268
            self._lock_mode = 'r'
 
1269
            self._lock_count = 1
 
1270
            if self._real_branch is not None:
 
1271
                self._real_branch.lock_read()
 
1272
        else:
 
1273
            self._lock_count += 1
 
1274
 
 
1275
    def _remote_lock_write(self, token):
 
1276
        if token is None:
 
1277
            branch_token = repo_token = ''
 
1278
        else:
 
1279
            branch_token = token
 
1280
            repo_token = self.repository.lock_write()
 
1281
            self.repository.unlock()
 
1282
        path = self.bzrdir._path_for_remote_call(self._client)
 
1283
        response = self._client.call('Branch.lock_write', path, branch_token,
 
1284
                                     repo_token or '')
 
1285
        if response[0] == 'ok':
 
1286
            ok, branch_token, repo_token = response
 
1287
            return branch_token, repo_token
 
1288
        elif response[0] == 'LockContention':
 
1289
            raise errors.LockContention('(remote lock)')
 
1290
        elif response[0] == 'TokenMismatch':
 
1291
            raise errors.TokenMismatch(token, '(remote token)')
 
1292
        elif response[0] == 'UnlockableTransport':
 
1293
            raise errors.UnlockableTransport(self.bzrdir.root_transport)
 
1294
        elif response[0] == 'ReadOnlyError':
 
1295
            raise errors.ReadOnlyError(self)
 
1296
        elif response[0] == 'LockFailed':
 
1297
            raise errors.LockFailed(response[1], response[2])
 
1298
        else:
 
1299
            raise errors.UnexpectedSmartServerResponse(response)
 
1300
            
 
1301
    def lock_write(self, token=None):
 
1302
        if not self._lock_mode:
 
1303
            remote_tokens = self._remote_lock_write(token)
 
1304
            self._lock_token, self._repo_lock_token = remote_tokens
 
1305
            if not self._lock_token:
 
1306
                raise SmartProtocolError('Remote server did not return a token!')
 
1307
            # TODO: We really, really, really don't want to call _ensure_real
 
1308
            # here, but it's the easiest way to ensure coherency between the
 
1309
            # state of the RemoteBranch and RemoteRepository objects and the
 
1310
            # physical locks.  If we don't materialise the real objects here,
 
1311
            # then getting everything in the right state later is complex, so
 
1312
            # for now we just do it the lazy way.
 
1313
            #   -- Andrew Bennetts, 2007-02-22.
 
1314
            self._ensure_real()
 
1315
            if self._real_branch is not None:
 
1316
                self._real_branch.repository.lock_write(
 
1317
                    token=self._repo_lock_token)
 
1318
                try:
 
1319
                    self._real_branch.lock_write(token=self._lock_token)
 
1320
                finally:
 
1321
                    self._real_branch.repository.unlock()
 
1322
            if token is not None:
 
1323
                self._leave_lock = True
 
1324
            else:
 
1325
                # XXX: this case seems to be unreachable; token cannot be None.
 
1326
                self._leave_lock = False
 
1327
            self._lock_mode = 'w'
 
1328
            self._lock_count = 1
 
1329
        elif self._lock_mode == 'r':
 
1330
            raise errors.ReadOnlyTransaction
 
1331
        else:
 
1332
            if token is not None:
 
1333
                # A token was given to lock_write, and we're relocking, so check
 
1334
                # that the given token actually matches the one we already have.
 
1335
                if token != self._lock_token:
 
1336
                    raise errors.TokenMismatch(token, self._lock_token)
 
1337
            self._lock_count += 1
 
1338
        return self._lock_token or None
 
1339
 
 
1340
    def _unlock(self, branch_token, repo_token):
 
1341
        path = self.bzrdir._path_for_remote_call(self._client)
 
1342
        response = self._client.call('Branch.unlock', path, branch_token,
 
1343
                                     repo_token or '')
 
1344
        if response == ('ok',):
 
1345
            return
 
1346
        elif response[0] == 'TokenMismatch':
 
1347
            raise errors.TokenMismatch(
 
1348
                str((branch_token, repo_token)), '(remote tokens)')
 
1349
        else:
 
1350
            raise errors.UnexpectedSmartServerResponse(response)
 
1351
 
 
1352
    def unlock(self):
 
1353
        self._lock_count -= 1
 
1354
        if not self._lock_count:
 
1355
            self._clear_cached_state()
 
1356
            mode = self._lock_mode
 
1357
            self._lock_mode = None
 
1358
            if self._real_branch is not None:
 
1359
                if (not self._leave_lock and mode == 'w' and
 
1360
                    self._repo_lock_token):
 
1361
                    # If this RemoteBranch will remove the physical lock for the
 
1362
                    # repository, make sure the _real_branch doesn't do it
 
1363
                    # first.  (Because the _real_branch's repository is set to
 
1364
                    # be the RemoteRepository.)
 
1365
                    self._real_branch.repository.leave_lock_in_place()
 
1366
                self._real_branch.unlock()
 
1367
            if mode != 'w':
 
1368
                # Only write-locked branched need to make a remote method call
 
1369
                # to perfom the unlock.
 
1370
                return
 
1371
            if not self._lock_token:
 
1372
                raise AssertionError('Locked, but no token!')
 
1373
            branch_token = self._lock_token
 
1374
            repo_token = self._repo_lock_token
 
1375
            self._lock_token = None
 
1376
            self._repo_lock_token = None
 
1377
            if not self._leave_lock:
 
1378
                self._unlock(branch_token, repo_token)
 
1379
 
 
1380
    def break_lock(self):
 
1381
        self._ensure_real()
 
1382
        return self._real_branch.break_lock()
 
1383
 
 
1384
    def leave_lock_in_place(self):
 
1385
        if not self._lock_token:
 
1386
            raise NotImplementedError(self.leave_lock_in_place)
 
1387
        self._leave_lock = True
 
1388
 
 
1389
    def dont_leave_lock_in_place(self):
 
1390
        if not self._lock_token:
 
1391
            raise NotImplementedError(self.dont_leave_lock_in_place)
 
1392
        self._leave_lock = False
 
1393
 
 
1394
    def last_revision_info(self):
 
1395
        """See Branch.last_revision_info()."""
 
1396
        path = self.bzrdir._path_for_remote_call(self._client)
 
1397
        response = self._client.call('Branch.last_revision_info', path)
 
1398
        if response[0] != 'ok':
 
1399
            raise SmartProtocolError('unexpected response code %s' % (response,))
 
1400
        revno = int(response[1])
 
1401
        last_revision = response[2]
 
1402
        return (revno, last_revision)
 
1403
 
 
1404
    def _gen_revision_history(self):
 
1405
        """See Branch._gen_revision_history()."""
 
1406
        path = self.bzrdir._path_for_remote_call(self._client)
 
1407
        response = self._client.call_expecting_body(
 
1408
            'Branch.revision_history', path)
 
1409
        if response[0][0] != 'ok':
 
1410
            raise SmartProtocolError('unexpected response code %s' % (response,))
 
1411
        result = response[1].read_body_bytes().split('\x00')
 
1412
        if result == ['']:
 
1413
            return []
 
1414
        return result
 
1415
 
 
1416
    @needs_write_lock
 
1417
    def set_revision_history(self, rev_history):
 
1418
        # Send just the tip revision of the history; the server will generate
 
1419
        # the full history from that.  If the revision doesn't exist in this
 
1420
        # branch, NoSuchRevision will be raised.
 
1421
        path = self.bzrdir._path_for_remote_call(self._client)
 
1422
        if rev_history == []:
 
1423
            rev_id = 'null:'
 
1424
        else:
 
1425
            rev_id = rev_history[-1]
 
1426
        self._clear_cached_state()
 
1427
        response = self._client.call('Branch.set_last_revision',
 
1428
            path, self._lock_token, self._repo_lock_token, rev_id)
 
1429
        if response[0] == 'NoSuchRevision':
 
1430
            raise NoSuchRevision(self, rev_id)
 
1431
        elif response[0] != 'ok':
 
1432
            raise SmartProtocolError('unexpected response code %s' % (response,))
 
1433
        self._cache_revision_history(rev_history)
 
1434
 
 
1435
    def get_parent(self):
 
1436
        self._ensure_real()
 
1437
        return self._real_branch.get_parent()
 
1438
        
 
1439
    def set_parent(self, url):
 
1440
        self._ensure_real()
 
1441
        return self._real_branch.set_parent(url)
 
1442
        
 
1443
    def sprout(self, to_bzrdir, revision_id=None):
 
1444
        # Like Branch.sprout, except that it sprouts a branch in the default
 
1445
        # format, because RemoteBranches can't be created at arbitrary URLs.
 
1446
        # XXX: if to_bzrdir is a RemoteBranch, this should perhaps do
 
1447
        # to_bzrdir.create_branch...
 
1448
        self._ensure_real()
 
1449
        result = self._real_branch._format.initialize(to_bzrdir)
 
1450
        self.copy_content_into(result, revision_id=revision_id)
 
1451
        result.set_parent(self.bzrdir.root_transport.base)
 
1452
        return result
 
1453
 
 
1454
    @needs_write_lock
 
1455
    def pull(self, source, overwrite=False, stop_revision=None,
 
1456
             **kwargs):
 
1457
        # FIXME: This asks the real branch to run the hooks, which means
 
1458
        # they're called with the wrong target branch parameter. 
 
1459
        # The test suite specifically allows this at present but it should be
 
1460
        # fixed.  It should get a _override_hook_target branch,
 
1461
        # as push does.  -- mbp 20070405
 
1462
        self._ensure_real()
 
1463
        self._real_branch.pull(
 
1464
            source, overwrite=overwrite, stop_revision=stop_revision,
 
1465
            **kwargs)
 
1466
 
 
1467
    @needs_read_lock
 
1468
    def push(self, target, overwrite=False, stop_revision=None):
 
1469
        self._ensure_real()
 
1470
        return self._real_branch.push(
 
1471
            target, overwrite=overwrite, stop_revision=stop_revision,
 
1472
            _override_hook_source_branch=self)
 
1473
 
 
1474
    def is_locked(self):
 
1475
        return self._lock_count >= 1
 
1476
 
 
1477
    @needs_write_lock
 
1478
    def set_last_revision_info(self, revno, revision_id):
 
1479
        revision_id = ensure_null(revision_id)
 
1480
        path = self.bzrdir._path_for_remote_call(self._client)
 
1481
        try:
 
1482
            response = self._client.call('Branch.set_last_revision_info',
 
1483
                path, self._lock_token, self._repo_lock_token, str(revno), revision_id)
 
1484
        except errors.UnknownSmartMethod:
 
1485
            self._ensure_real()
 
1486
            self._clear_cached_state()
 
1487
            return self._real_branch.set_last_revision_info(revno, revision_id)
 
1488
        if response == ('ok',):
 
1489
            self._clear_cached_state()
 
1490
        elif response[0] == 'NoSuchRevision':
 
1491
            raise NoSuchRevision(self, response[1])
 
1492
        else:
 
1493
            raise errors.UnexpectedSmartServerResponse(response)
 
1494
 
 
1495
    def generate_revision_history(self, revision_id, last_rev=None,
 
1496
                                  other_branch=None):
 
1497
        self._ensure_real()
 
1498
        return self._real_branch.generate_revision_history(
 
1499
            revision_id, last_rev=last_rev, other_branch=other_branch)
 
1500
 
 
1501
    @property
 
1502
    def tags(self):
 
1503
        self._ensure_real()
 
1504
        return self._real_branch.tags
 
1505
 
 
1506
    def set_push_location(self, location):
 
1507
        self._ensure_real()
 
1508
        return self._real_branch.set_push_location(location)
 
1509
 
 
1510
    def update_revisions(self, other, stop_revision=None, overwrite=False):
 
1511
        self._ensure_real()
 
1512
        return self._real_branch.update_revisions(
 
1513
            other, stop_revision=stop_revision, overwrite=overwrite)
 
1514
 
 
1515
 
 
1516
def _extract_tar(tar, to_dir):
 
1517
    """Extract all the contents of a tarfile object.
 
1518
 
 
1519
    A replacement for extractall, which is not present in python2.4
 
1520
    """
 
1521
    for tarinfo in tar:
 
1522
        tar.extract(tarinfo, to_dir)