/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 breezy/bzr/remote.py

  • Committer: Jelmer Vernooij
  • Date: 2020-07-18 23:14:00 UTC
  • mfrom: (7490.40.62 work)
  • mto: This revision was merged to the branch mainline in revision 7519.
  • Revision ID: jelmer@jelmer.uk-20200718231400-jaes9qltn8oi8xss
Merge lp:brz/3.1.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006-2012 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
import bz2
 
18
import os
 
19
import re
 
20
import sys
 
21
import zlib
 
22
 
 
23
from .. import (
 
24
    bencode,
 
25
    branch,
 
26
    bzr as _mod_bzr,
 
27
    config as _mod_config,
 
28
    controldir,
 
29
    debug,
 
30
    errors,
 
31
    gpg,
 
32
    graph,
 
33
    lock,
 
34
    lockdir,
 
35
    osutils,
 
36
    registry,
 
37
    repository as _mod_repository,
 
38
    revision as _mod_revision,
 
39
    urlutils,
 
40
    )
 
41
from . import (
 
42
    branch as bzrbranch,
 
43
    bzrdir as _mod_bzrdir,
 
44
    inventory_delta,
 
45
    testament as _mod_testament,
 
46
    vf_repository,
 
47
    vf_search,
 
48
    )
 
49
from .branch import BranchReferenceFormat
 
50
from ..branch import BranchWriteLockResult
 
51
from ..decorators import only_raises
 
52
from ..errors import (
 
53
    NoSuchRevision,
 
54
    SmartProtocolError,
 
55
    )
 
56
from ..i18n import gettext
 
57
from .inventory import Inventory
 
58
from .inventorytree import InventoryRevisionTree
 
59
from ..lockable_files import LockableFiles
 
60
from .smart import client, vfs, repository as smart_repo
 
61
from .smart.client import _SmartClient
 
62
from ..revision import NULL_REVISION
 
63
from ..repository import RepositoryWriteLockResult, _LazyListJoin
 
64
from .serializer import format_registry as serializer_format_registry
 
65
from ..trace import mutter, note, warning, log_exception_quietly
 
66
from .versionedfile import FulltextContentFactory
 
67
 
 
68
 
 
69
_DEFAULT_SEARCH_DEPTH = 100
 
70
 
 
71
 
 
72
class _RpcHelper(object):
 
73
    """Mixin class that helps with issuing RPCs."""
 
74
 
 
75
    def _call(self, method, *args, **err_context):
 
76
        try:
 
77
            return self._client.call(method, *args)
 
78
        except errors.ErrorFromSmartServer as err:
 
79
            self._translate_error(err, **err_context)
 
80
 
 
81
    def _call_expecting_body(self, method, *args, **err_context):
 
82
        try:
 
83
            return self._client.call_expecting_body(method, *args)
 
84
        except errors.ErrorFromSmartServer as err:
 
85
            self._translate_error(err, **err_context)
 
86
 
 
87
    def _call_with_body_bytes(self, method, args, body_bytes, **err_context):
 
88
        try:
 
89
            return self._client.call_with_body_bytes(method, args, body_bytes)
 
90
        except errors.ErrorFromSmartServer as err:
 
91
            self._translate_error(err, **err_context)
 
92
 
 
93
    def _call_with_body_bytes_expecting_body(self, method, args, body_bytes,
 
94
                                             **err_context):
 
95
        try:
 
96
            return self._client.call_with_body_bytes_expecting_body(
 
97
                method, args, body_bytes)
 
98
        except errors.ErrorFromSmartServer as err:
 
99
            self._translate_error(err, **err_context)
 
100
 
 
101
 
 
102
def response_tuple_to_repo_format(response):
 
103
    """Convert a response tuple describing a repository format to a format."""
 
104
    format = RemoteRepositoryFormat()
 
105
    format._rich_root_data = (response[0] == b'yes')
 
106
    format._supports_tree_reference = (response[1] == b'yes')
 
107
    format._supports_external_lookups = (response[2] == b'yes')
 
108
    format._network_name = response[3]
 
109
    return format
 
110
 
 
111
 
 
112
# Note that RemoteBzrDirProber lives in breezy.bzrdir so breezy.bzr.remote
 
113
# does not have to be imported unless a remote format is involved.
 
114
 
 
115
class RemoteBzrDirFormat(_mod_bzrdir.BzrDirMetaFormat1):
 
116
    """Format representing bzrdirs accessed via a smart server"""
 
117
 
 
118
    supports_workingtrees = False
 
119
 
 
120
    colocated_branches = False
 
121
 
 
122
    def __init__(self):
 
123
        _mod_bzrdir.BzrDirMetaFormat1.__init__(self)
 
124
        # XXX: It's a bit ugly that the network name is here, because we'd
 
125
        # like to believe that format objects are stateless or at least
 
126
        # immutable,  However, we do at least avoid mutating the name after
 
127
        # it's returned.  See <https://bugs.launchpad.net/bzr/+bug/504102>
 
128
        self._network_name = None
 
129
 
 
130
    def __repr__(self):
 
131
        return "%s(_network_name=%r)" % (self.__class__.__name__,
 
132
                                         self._network_name)
 
133
 
 
134
    def get_format_description(self):
 
135
        if self._network_name:
 
136
            try:
 
137
                real_format = controldir.network_format_registry.get(
 
138
                    self._network_name)
 
139
            except KeyError:
 
140
                pass
 
141
            else:
 
142
                return 'Remote: ' + real_format.get_format_description()
 
143
        return 'bzr remote bzrdir'
 
144
 
 
145
    def get_format_string(self):
 
146
        raise NotImplementedError(self.get_format_string)
 
147
 
 
148
    def network_name(self):
 
149
        if self._network_name:
 
150
            return self._network_name
 
151
        else:
 
152
            raise AssertionError("No network name set.")
 
153
 
 
154
    def initialize_on_transport(self, transport):
 
155
        try:
 
156
            # hand off the request to the smart server
 
157
            client_medium = transport.get_smart_medium()
 
158
        except errors.NoSmartMedium:
 
159
            # TODO: lookup the local format from a server hint.
 
160
            local_dir_format = _mod_bzrdir.BzrDirMetaFormat1()
 
161
            return local_dir_format.initialize_on_transport(transport)
 
162
        client = _SmartClient(client_medium)
 
163
        path = client.remote_path_from_transport(transport)
 
164
        try:
 
165
            response = client.call(b'BzrDirFormat.initialize', path)
 
166
        except errors.ErrorFromSmartServer as err:
 
167
            _translate_error(err, path=path)
 
168
        if response[0] != b'ok':
 
169
            raise errors.SmartProtocolError(
 
170
                'unexpected response code %s' % (response,))
 
171
        format = RemoteBzrDirFormat()
 
172
        self._supply_sub_formats_to(format)
 
173
        return RemoteBzrDir(transport, format)
 
174
 
 
175
    def parse_NoneTrueFalse(self, arg):
 
176
        if not arg:
 
177
            return None
 
178
        if arg == b'False':
 
179
            return False
 
180
        if arg == b'True':
 
181
            return True
 
182
        raise AssertionError("invalid arg %r" % arg)
 
183
 
 
184
    def _serialize_NoneTrueFalse(self, arg):
 
185
        if arg is False:
 
186
            return b'False'
 
187
        if arg:
 
188
            return b'True'
 
189
        return b''
 
190
 
 
191
    def _serialize_NoneString(self, arg):
 
192
        return arg or b''
 
193
 
 
194
    def initialize_on_transport_ex(self, transport, use_existing_dir=False,
 
195
                                   create_prefix=False, force_new_repo=False, stacked_on=None,
 
196
                                   stack_on_pwd=None, repo_format_name=None, make_working_trees=None,
 
197
                                   shared_repo=False):
 
198
        try:
 
199
            # hand off the request to the smart server
 
200
            client_medium = transport.get_smart_medium()
 
201
        except errors.NoSmartMedium:
 
202
            do_vfs = True
 
203
        else:
 
204
            # Decline to open it if the server doesn't support our required
 
205
            # version (3) so that the VFS-based transport will do it.
 
206
            if client_medium.should_probe():
 
207
                try:
 
208
                    server_version = client_medium.protocol_version()
 
209
                    if server_version != '2':
 
210
                        do_vfs = True
 
211
                    else:
 
212
                        do_vfs = False
 
213
                except errors.SmartProtocolError:
 
214
                    # Apparently there's no usable smart server there, even though
 
215
                    # the medium supports the smart protocol.
 
216
                    do_vfs = True
 
217
            else:
 
218
                do_vfs = False
 
219
        if not do_vfs:
 
220
            client = _SmartClient(client_medium)
 
221
            path = client.remote_path_from_transport(transport)
 
222
            if client_medium._is_remote_before((1, 16)):
 
223
                do_vfs = True
 
224
        if do_vfs:
 
225
            # TODO: lookup the local format from a server hint.
 
226
            local_dir_format = _mod_bzrdir.BzrDirMetaFormat1()
 
227
            self._supply_sub_formats_to(local_dir_format)
 
228
            return local_dir_format.initialize_on_transport_ex(transport,
 
229
                                                               use_existing_dir=use_existing_dir, create_prefix=create_prefix,
 
230
                                                               force_new_repo=force_new_repo, stacked_on=stacked_on,
 
231
                                                               stack_on_pwd=stack_on_pwd, repo_format_name=repo_format_name,
 
232
                                                               make_working_trees=make_working_trees, shared_repo=shared_repo,
 
233
                                                               vfs_only=True)
 
234
        return self._initialize_on_transport_ex_rpc(client, path, transport,
 
235
                                                    use_existing_dir, create_prefix, force_new_repo, stacked_on,
 
236
                                                    stack_on_pwd, repo_format_name, make_working_trees, shared_repo)
 
237
 
 
238
    def _initialize_on_transport_ex_rpc(self, client, path, transport,
 
239
                                        use_existing_dir, create_prefix, force_new_repo, stacked_on,
 
240
                                        stack_on_pwd, repo_format_name, make_working_trees, shared_repo):
 
241
        args = []
 
242
        args.append(self._serialize_NoneTrueFalse(use_existing_dir))
 
243
        args.append(self._serialize_NoneTrueFalse(create_prefix))
 
244
        args.append(self._serialize_NoneTrueFalse(force_new_repo))
 
245
        args.append(self._serialize_NoneString(stacked_on))
 
246
        # stack_on_pwd is often/usually our transport
 
247
        if stack_on_pwd:
 
248
            try:
 
249
                stack_on_pwd = transport.relpath(stack_on_pwd).encode('utf-8')
 
250
                if not stack_on_pwd:
 
251
                    stack_on_pwd = b'.'
 
252
            except errors.PathNotChild:
 
253
                pass
 
254
        args.append(self._serialize_NoneString(stack_on_pwd))
 
255
        args.append(self._serialize_NoneString(repo_format_name))
 
256
        args.append(self._serialize_NoneTrueFalse(make_working_trees))
 
257
        args.append(self._serialize_NoneTrueFalse(shared_repo))
 
258
        request_network_name = self._network_name or \
 
259
            _mod_bzrdir.BzrDirFormat.get_default_format().network_name()
 
260
        try:
 
261
            response = client.call(b'BzrDirFormat.initialize_ex_1.16',
 
262
                                   request_network_name, path, *args)
 
263
        except errors.UnknownSmartMethod:
 
264
            client._medium._remember_remote_is_before((1, 16))
 
265
            local_dir_format = _mod_bzrdir.BzrDirMetaFormat1()
 
266
            self._supply_sub_formats_to(local_dir_format)
 
267
            return local_dir_format.initialize_on_transport_ex(transport,
 
268
                                                               use_existing_dir=use_existing_dir, create_prefix=create_prefix,
 
269
                                                               force_new_repo=force_new_repo, stacked_on=stacked_on,
 
270
                                                               stack_on_pwd=stack_on_pwd, repo_format_name=repo_format_name,
 
271
                                                               make_working_trees=make_working_trees, shared_repo=shared_repo,
 
272
                                                               vfs_only=True)
 
273
        except errors.ErrorFromSmartServer as err:
 
274
            _translate_error(err, path=path.decode('utf-8'))
 
275
        repo_path = response[0]
 
276
        bzrdir_name = response[6]
 
277
        require_stacking = response[7]
 
278
        require_stacking = self.parse_NoneTrueFalse(require_stacking)
 
279
        format = RemoteBzrDirFormat()
 
280
        format._network_name = bzrdir_name
 
281
        self._supply_sub_formats_to(format)
 
282
        bzrdir = RemoteBzrDir(transport, format, _client=client)
 
283
        if repo_path:
 
284
            repo_format = response_tuple_to_repo_format(response[1:])
 
285
            if repo_path == b'.':
 
286
                repo_path = b''
 
287
            repo_path = repo_path.decode('utf-8')
 
288
            if repo_path:
 
289
                repo_bzrdir_format = RemoteBzrDirFormat()
 
290
                repo_bzrdir_format._network_name = response[5]
 
291
                repo_bzr = RemoteBzrDir(transport.clone(repo_path),
 
292
                                        repo_bzrdir_format)
 
293
            else:
 
294
                repo_bzr = bzrdir
 
295
            final_stack = response[8] or None
 
296
            if final_stack:
 
297
                final_stack = final_stack.decode('utf-8')
 
298
            final_stack_pwd = response[9] or None
 
299
            if final_stack_pwd:
 
300
                final_stack_pwd = urlutils.join(
 
301
                    transport.base, final_stack_pwd.decode('utf-8'))
 
302
            remote_repo = RemoteRepository(repo_bzr, repo_format)
 
303
            if len(response) > 10:
 
304
                # Updated server verb that locks remotely.
 
305
                repo_lock_token = response[10] or None
 
306
                remote_repo.lock_write(repo_lock_token, _skip_rpc=True)
 
307
                if repo_lock_token:
 
308
                    remote_repo.dont_leave_lock_in_place()
 
309
            else:
 
310
                remote_repo.lock_write()
 
311
            policy = _mod_bzrdir.UseExistingRepository(remote_repo,
 
312
                                                       final_stack, final_stack_pwd, require_stacking)
 
313
            policy.acquire_repository()
 
314
        else:
 
315
            remote_repo = None
 
316
            policy = None
 
317
        bzrdir._format.set_branch_format(self.get_branch_format())
 
318
        if require_stacking:
 
319
            # The repo has already been created, but we need to make sure that
 
320
            # we'll make a stackable branch.
 
321
            bzrdir._format.require_stacking(_skip_repo=True)
 
322
        return remote_repo, bzrdir, require_stacking, policy
 
323
 
 
324
    def _open(self, transport):
 
325
        return RemoteBzrDir(transport, self)
 
326
 
 
327
    def __eq__(self, other):
 
328
        if not isinstance(other, RemoteBzrDirFormat):
 
329
            return False
 
330
        return self.get_format_description() == other.get_format_description()
 
331
 
 
332
    def __return_repository_format(self):
 
333
        # Always return a RemoteRepositoryFormat object, but if a specific bzr
 
334
        # repository format has been asked for, tell the RemoteRepositoryFormat
 
335
        # that it should use that for init() etc.
 
336
        result = RemoteRepositoryFormat()
 
337
        custom_format = getattr(self, '_repository_format', None)
 
338
        if custom_format:
 
339
            if isinstance(custom_format, RemoteRepositoryFormat):
 
340
                return custom_format
 
341
            else:
 
342
                # We will use the custom format to create repositories over the
 
343
                # wire; expose its details like rich_root_data for code to
 
344
                # query
 
345
                result._custom_format = custom_format
 
346
        return result
 
347
 
 
348
    def get_branch_format(self):
 
349
        result = _mod_bzrdir.BzrDirMetaFormat1.get_branch_format(self)
 
350
        if not isinstance(result, RemoteBranchFormat):
 
351
            new_result = RemoteBranchFormat()
 
352
            new_result._custom_format = result
 
353
            # cache the result
 
354
            self.set_branch_format(new_result)
 
355
            result = new_result
 
356
        return result
 
357
 
 
358
    repository_format = property(__return_repository_format,
 
359
                                 _mod_bzrdir.BzrDirMetaFormat1._set_repository_format)  # .im_func)
 
360
 
 
361
 
 
362
class RemoteControlStore(_mod_config.IniFileStore):
 
363
    """Control store which attempts to use HPSS calls to retrieve control store.
 
364
 
 
365
    Note that this is specific to bzr-based formats.
 
366
    """
 
367
 
 
368
    def __init__(self, bzrdir):
 
369
        super(RemoteControlStore, self).__init__()
 
370
        self.controldir = bzrdir
 
371
        self._real_store = None
 
372
 
 
373
    def lock_write(self, token=None):
 
374
        self._ensure_real()
 
375
        return self._real_store.lock_write(token)
 
376
 
 
377
    def unlock(self):
 
378
        self._ensure_real()
 
379
        return self._real_store.unlock()
 
380
 
 
381
    def save(self):
 
382
        with self.lock_write():
 
383
            # We need to be able to override the undecorated implementation
 
384
            self.save_without_locking()
 
385
 
 
386
    def save_without_locking(self):
 
387
        super(RemoteControlStore, self).save()
 
388
 
 
389
    def _ensure_real(self):
 
390
        self.controldir._ensure_real()
 
391
        if self._real_store is None:
 
392
            self._real_store = _mod_config.ControlStore(self.controldir)
 
393
 
 
394
    def external_url(self):
 
395
        return urlutils.join(self.branch.user_url, 'control.conf')
 
396
 
 
397
    def _load_content(self):
 
398
        medium = self.controldir._client._medium
 
399
        path = self.controldir._path_for_remote_call(self.controldir._client)
 
400
        try:
 
401
            response, handler = self.controldir._call_expecting_body(
 
402
                b'BzrDir.get_config_file', path)
 
403
        except errors.UnknownSmartMethod:
 
404
            self._ensure_real()
 
405
            return self._real_store._load_content()
 
406
        if len(response) and response[0] != b'ok':
 
407
            raise errors.UnexpectedSmartServerResponse(response)
 
408
        return handler.read_body_bytes()
 
409
 
 
410
    def _save_content(self, content):
 
411
        # FIXME JRV 2011-11-22: Ideally this should use a
 
412
        # HPSS call too, but at the moment it is not possible
 
413
        # to write lock control directories.
 
414
        self._ensure_real()
 
415
        return self._real_store._save_content(content)
 
416
 
 
417
 
 
418
class RemoteBzrDir(_mod_bzrdir.BzrDir, _RpcHelper):
 
419
    """Control directory on a remote server, accessed via bzr:// or similar."""
 
420
 
 
421
    def __init__(self, transport, format, _client=None, _force_probe=False):
 
422
        """Construct a RemoteBzrDir.
 
423
 
 
424
        :param _client: Private parameter for testing. Disables probing and the
 
425
            use of a real bzrdir.
 
426
        """
 
427
        _mod_bzrdir.BzrDir.__init__(self, transport, format)
 
428
        # this object holds a delegated bzrdir that uses file-level operations
 
429
        # to talk to the other side
 
430
        self._real_bzrdir = None
 
431
        self._has_working_tree = None
 
432
        # 1-shot cache for the call pattern 'create_branch; open_branch' - see
 
433
        # create_branch for details.
 
434
        self._next_open_branch_result = None
 
435
 
 
436
        if _client is None:
 
437
            medium = transport.get_smart_medium()
 
438
            self._client = client._SmartClient(medium)
 
439
        else:
 
440
            self._client = _client
 
441
            if not _force_probe:
 
442
                return
 
443
 
 
444
        self._probe_bzrdir()
 
445
 
 
446
    def __repr__(self):
 
447
        return '%s(%r)' % (self.__class__.__name__, self._client)
 
448
 
 
449
    def _probe_bzrdir(self):
 
450
        medium = self._client._medium
 
451
        path = self._path_for_remote_call(self._client)
 
452
        if medium._is_remote_before((2, 1)):
 
453
            self._rpc_open(path)
 
454
            return
 
455
        try:
 
456
            self._rpc_open_2_1(path)
 
457
            return
 
458
        except errors.UnknownSmartMethod:
 
459
            medium._remember_remote_is_before((2, 1))
 
460
            self._rpc_open(path)
 
461
 
 
462
    def _rpc_open_2_1(self, path):
 
463
        response = self._call(b'BzrDir.open_2.1', path)
 
464
        if response == (b'no',):
 
465
            raise errors.NotBranchError(path=self.root_transport.base)
 
466
        elif response[0] == b'yes':
 
467
            if response[1] == b'yes':
 
468
                self._has_working_tree = True
 
469
            elif response[1] == b'no':
 
470
                self._has_working_tree = False
 
471
            else:
 
472
                raise errors.UnexpectedSmartServerResponse(response)
 
473
        else:
 
474
            raise errors.UnexpectedSmartServerResponse(response)
 
475
 
 
476
    def _rpc_open(self, path):
 
477
        response = self._call(b'BzrDir.open', path)
 
478
        if response not in [(b'yes',), (b'no',)]:
 
479
            raise errors.UnexpectedSmartServerResponse(response)
 
480
        if response == (b'no',):
 
481
            raise errors.NotBranchError(path=self.root_transport.base)
 
482
 
 
483
    def _ensure_real(self):
 
484
        """Ensure that there is a _real_bzrdir set.
 
485
 
 
486
        Used before calls to self._real_bzrdir.
 
487
        """
 
488
        if not self._real_bzrdir:
 
489
            if 'hpssvfs' in debug.debug_flags:
 
490
                import traceback
 
491
                warning('VFS BzrDir access triggered\n%s',
 
492
                        ''.join(traceback.format_stack()))
 
493
            self._real_bzrdir = _mod_bzrdir.BzrDir.open_from_transport(
 
494
                self.root_transport, probers=[_mod_bzr.BzrProber])
 
495
            self._format._network_name = \
 
496
                self._real_bzrdir._format.network_name()
 
497
 
 
498
    def _translate_error(self, err, **context):
 
499
        _translate_error(err, bzrdir=self, **context)
 
500
 
 
501
    def break_lock(self):
 
502
        # Prevent aliasing problems in the next_open_branch_result cache.
 
503
        # See create_branch for rationale.
 
504
        self._next_open_branch_result = None
 
505
        return _mod_bzrdir.BzrDir.break_lock(self)
 
506
 
 
507
    def _vfs_checkout_metadir(self):
 
508
        self._ensure_real()
 
509
        return self._real_bzrdir.checkout_metadir()
 
510
 
 
511
    def checkout_metadir(self):
 
512
        """Retrieve the controldir format to use for checkouts of this one.
 
513
        """
 
514
        medium = self._client._medium
 
515
        if medium._is_remote_before((2, 5)):
 
516
            return self._vfs_checkout_metadir()
 
517
        path = self._path_for_remote_call(self._client)
 
518
        try:
 
519
            response = self._client.call(b'BzrDir.checkout_metadir',
 
520
                                         path)
 
521
        except errors.UnknownSmartMethod:
 
522
            medium._remember_remote_is_before((2, 5))
 
523
            return self._vfs_checkout_metadir()
 
524
        if len(response) != 3:
 
525
            raise errors.UnexpectedSmartServerResponse(response)
 
526
        control_name, repo_name, branch_name = response
 
527
        try:
 
528
            format = controldir.network_format_registry.get(control_name)
 
529
        except KeyError:
 
530
            raise errors.UnknownFormatError(kind='control',
 
531
                                            format=control_name)
 
532
        if repo_name:
 
533
            try:
 
534
                repo_format = _mod_repository.network_format_registry.get(
 
535
                    repo_name)
 
536
            except KeyError:
 
537
                raise errors.UnknownFormatError(kind='repository',
 
538
                                                format=repo_name)
 
539
            format.repository_format = repo_format
 
540
        if branch_name:
 
541
            try:
 
542
                format.set_branch_format(
 
543
                    branch.network_format_registry.get(branch_name))
 
544
            except KeyError:
 
545
                raise errors.UnknownFormatError(kind='branch',
 
546
                                                format=branch_name)
 
547
        return format
 
548
 
 
549
    def _vfs_cloning_metadir(self, require_stacking=False):
 
550
        self._ensure_real()
 
551
        return self._real_bzrdir.cloning_metadir(
 
552
            require_stacking=require_stacking)
 
553
 
 
554
    def cloning_metadir(self, require_stacking=False):
 
555
        medium = self._client._medium
 
556
        if medium._is_remote_before((1, 13)):
 
557
            return self._vfs_cloning_metadir(require_stacking=require_stacking)
 
558
        verb = b'BzrDir.cloning_metadir'
 
559
        if require_stacking:
 
560
            stacking = b'True'
 
561
        else:
 
562
            stacking = b'False'
 
563
        path = self._path_for_remote_call(self._client)
 
564
        try:
 
565
            response = self._call(verb, path, stacking)
 
566
        except errors.UnknownSmartMethod:
 
567
            medium._remember_remote_is_before((1, 13))
 
568
            return self._vfs_cloning_metadir(require_stacking=require_stacking)
 
569
        except errors.UnknownErrorFromSmartServer as err:
 
570
            if err.error_tuple != (b'BranchReference',):
 
571
                raise
 
572
            # We need to resolve the branch reference to determine the
 
573
            # cloning_metadir.  This causes unnecessary RPCs to open the
 
574
            # referenced branch (and bzrdir, etc) but only when the caller
 
575
            # didn't already resolve the branch reference.
 
576
            referenced_branch = self.open_branch()
 
577
            return referenced_branch.controldir.cloning_metadir()
 
578
        if len(response) != 3:
 
579
            raise errors.UnexpectedSmartServerResponse(response)
 
580
        control_name, repo_name, branch_info = response
 
581
        if len(branch_info) != 2:
 
582
            raise errors.UnexpectedSmartServerResponse(response)
 
583
        branch_ref, branch_name = branch_info
 
584
        try:
 
585
            format = controldir.network_format_registry.get(control_name)
 
586
        except KeyError:
 
587
            raise errors.UnknownFormatError(
 
588
                kind='control', format=control_name)
 
589
 
 
590
        if repo_name:
 
591
            try:
 
592
                format.repository_format = _mod_repository.network_format_registry.get(
 
593
                    repo_name)
 
594
            except KeyError:
 
595
                raise errors.UnknownFormatError(kind='repository',
 
596
                                                format=repo_name)
 
597
        if branch_ref == b'ref':
 
598
            # XXX: we need possible_transports here to avoid reopening the
 
599
            # connection to the referenced location
 
600
            ref_bzrdir = _mod_bzrdir.BzrDir.open(branch_name)
 
601
            branch_format = ref_bzrdir.cloning_metadir().get_branch_format()
 
602
            format.set_branch_format(branch_format)
 
603
        elif branch_ref == b'branch':
 
604
            if branch_name:
 
605
                try:
 
606
                    branch_format = branch.network_format_registry.get(
 
607
                        branch_name)
 
608
                except KeyError:
 
609
                    raise errors.UnknownFormatError(kind='branch',
 
610
                                                    format=branch_name)
 
611
                format.set_branch_format(branch_format)
 
612
        else:
 
613
            raise errors.UnexpectedSmartServerResponse(response)
 
614
        return format
 
615
 
 
616
    def create_repository(self, shared=False):
 
617
        # as per meta1 formats - just delegate to the format object which may
 
618
        # be parameterised.
 
619
        result = self._format.repository_format.initialize(self, shared)
 
620
        if not isinstance(result, RemoteRepository):
 
621
            return self.open_repository()
 
622
        else:
 
623
            return result
 
624
 
 
625
    def destroy_repository(self):
 
626
        """See BzrDir.destroy_repository"""
 
627
        path = self._path_for_remote_call(self._client)
 
628
        try:
 
629
            response = self._call(b'BzrDir.destroy_repository', path)
 
630
        except errors.UnknownSmartMethod:
 
631
            self._ensure_real()
 
632
            self._real_bzrdir.destroy_repository()
 
633
            return
 
634
        if response[0] != b'ok':
 
635
            raise SmartProtocolError(
 
636
                'unexpected response code %s' % (response,))
 
637
 
 
638
    def create_branch(self, name=None, repository=None,
 
639
                      append_revisions_only=None):
 
640
        if name is None:
 
641
            name = self._get_selected_branch()
 
642
        if name != "":
 
643
            raise errors.NoColocatedBranchSupport(self)
 
644
        # as per meta1 formats - just delegate to the format object which may
 
645
        # be parameterised.
 
646
        real_branch = self._format.get_branch_format().initialize(self,
 
647
                                                                  name=name, repository=repository,
 
648
                                                                  append_revisions_only=append_revisions_only)
 
649
        if not isinstance(real_branch, RemoteBranch):
 
650
            if not isinstance(repository, RemoteRepository):
 
651
                raise AssertionError(
 
652
                    'need a RemoteRepository to use with RemoteBranch, got %r'
 
653
                    % (repository,))
 
654
            result = RemoteBranch(self, repository, real_branch, name=name)
 
655
        else:
 
656
            result = real_branch
 
657
        # BzrDir.clone_on_transport() uses the result of create_branch but does
 
658
        # not return it to its callers; we save approximately 8% of our round
 
659
        # trips by handing the branch we created back to the first caller to
 
660
        # open_branch rather than probing anew. Long term we need a API in
 
661
        # bzrdir that doesn't discard result objects (like result_branch).
 
662
        # RBC 20090225
 
663
        self._next_open_branch_result = result
 
664
        return result
 
665
 
 
666
    def destroy_branch(self, name=None):
 
667
        """See BzrDir.destroy_branch"""
 
668
        if name is None:
 
669
            name = self._get_selected_branch()
 
670
        if name != "":
 
671
            raise errors.NoColocatedBranchSupport(self)
 
672
        path = self._path_for_remote_call(self._client)
 
673
        try:
 
674
            if name != "":
 
675
                args = (name, )
 
676
            else:
 
677
                args = ()
 
678
            response = self._call(b'BzrDir.destroy_branch', path, *args)
 
679
        except errors.UnknownSmartMethod:
 
680
            self._ensure_real()
 
681
            self._real_bzrdir.destroy_branch(name=name)
 
682
            self._next_open_branch_result = None
 
683
            return
 
684
        self._next_open_branch_result = None
 
685
        if response[0] != b'ok':
 
686
            raise SmartProtocolError(
 
687
                'unexpected response code %s' % (response,))
 
688
 
 
689
    def create_workingtree(self, revision_id=None, from_branch=None,
 
690
                           accelerator_tree=None, hardlink=False):
 
691
        raise errors.NotLocalUrl(self.transport.base)
 
692
 
 
693
    def find_branch_format(self, name=None):
 
694
        """Find the branch 'format' for this bzrdir.
 
695
 
 
696
        This might be a synthetic object for e.g. RemoteBranch and SVN.
 
697
        """
 
698
        b = self.open_branch(name=name)
 
699
        return b._format
 
700
 
 
701
    def branch_names(self):
 
702
        path = self._path_for_remote_call(self._client)
 
703
        try:
 
704
            response, handler = self._call_expecting_body(
 
705
                b'BzrDir.get_branches', path)
 
706
        except errors.UnknownSmartMethod:
 
707
            self._ensure_real()
 
708
            return self._real_bzrdir.branch_names()
 
709
        if response[0] != b"success":
 
710
            raise errors.UnexpectedSmartServerResponse(response)
 
711
        body = bencode.bdecode(handler.read_body_bytes())
 
712
        ret = []
 
713
        for name, value in body.items():
 
714
            name = name.decode('utf-8')
 
715
            ret.append(name)
 
716
        return ret
 
717
 
 
718
    def get_branches(self, possible_transports=None, ignore_fallbacks=False):
 
719
        path = self._path_for_remote_call(self._client)
 
720
        try:
 
721
            response, handler = self._call_expecting_body(
 
722
                b'BzrDir.get_branches', path)
 
723
        except errors.UnknownSmartMethod:
 
724
            self._ensure_real()
 
725
            return self._real_bzrdir.get_branches()
 
726
        if response[0] != b"success":
 
727
            raise errors.UnexpectedSmartServerResponse(response)
 
728
        body = bencode.bdecode(handler.read_body_bytes())
 
729
        ret = {}
 
730
        for name, value in body.items():
 
731
            name = name.decode('utf-8')
 
732
            ret[name] = self._open_branch(
 
733
                name, value[0].decode('ascii'), value[1],
 
734
                possible_transports=possible_transports,
 
735
                ignore_fallbacks=ignore_fallbacks)
 
736
        return ret
 
737
 
 
738
    def set_branch_reference(self, target_branch, name=None):
 
739
        """See BzrDir.set_branch_reference()."""
 
740
        if name is None:
 
741
            name = self._get_selected_branch()
 
742
        if name != "":
 
743
            raise errors.NoColocatedBranchSupport(self)
 
744
        self._ensure_real()
 
745
        return self._real_bzrdir.set_branch_reference(target_branch, name=name)
 
746
 
 
747
    def get_branch_reference(self, name=None):
 
748
        """See BzrDir.get_branch_reference()."""
 
749
        if name is None:
 
750
            name = self._get_selected_branch()
 
751
        if name != "":
 
752
            raise errors.NoColocatedBranchSupport(self)
 
753
        response = self._get_branch_reference()
 
754
        if response[0] == 'ref':
 
755
            return response[1].decode('utf-8')
 
756
        else:
 
757
            return None
 
758
 
 
759
    def _get_branch_reference(self):
 
760
        """Get branch reference information
 
761
 
 
762
        :return: Tuple with (kind, location_or_format)
 
763
            if kind == 'ref', then location_or_format contains a location
 
764
            otherwise, it contains a format name
 
765
        """
 
766
        path = self._path_for_remote_call(self._client)
 
767
        medium = self._client._medium
 
768
        candidate_calls = [
 
769
            (b'BzrDir.open_branchV3', (2, 1)),
 
770
            (b'BzrDir.open_branchV2', (1, 13)),
 
771
            (b'BzrDir.open_branch', None),
 
772
            ]
 
773
        for verb, required_version in candidate_calls:
 
774
            if required_version and medium._is_remote_before(required_version):
 
775
                continue
 
776
            try:
 
777
                response = self._call(verb, path)
 
778
            except errors.UnknownSmartMethod:
 
779
                if required_version is None:
 
780
                    raise
 
781
                medium._remember_remote_is_before(required_version)
 
782
            else:
 
783
                break
 
784
        if verb == b'BzrDir.open_branch':
 
785
            if response[0] != b'ok':
 
786
                raise errors.UnexpectedSmartServerResponse(response)
 
787
            if response[1] != b'':
 
788
                return ('ref', response[1])
 
789
            else:
 
790
                return ('branch', b'')
 
791
        if response[0] not in (b'ref', b'branch'):
 
792
            raise errors.UnexpectedSmartServerResponse(response)
 
793
        return (response[0].decode('ascii'), response[1])
 
794
 
 
795
    def _get_tree_branch(self, name=None):
 
796
        """See BzrDir._get_tree_branch()."""
 
797
        return None, self.open_branch(name=name)
 
798
 
 
799
    def _open_branch(self, name, kind, location_or_format,
 
800
                     ignore_fallbacks=False, possible_transports=None):
 
801
        if kind == 'ref':
 
802
            # a branch reference, use the existing BranchReference logic.
 
803
            format = BranchReferenceFormat()
 
804
            ref_loc = urlutils.join(self.user_url, location_or_format.decode('utf-8'))
 
805
            return format.open(self, name=name, _found=True,
 
806
                               location=ref_loc,
 
807
                               ignore_fallbacks=ignore_fallbacks,
 
808
                               possible_transports=possible_transports)
 
809
        branch_format_name = location_or_format
 
810
        if not branch_format_name:
 
811
            branch_format_name = None
 
812
        format = RemoteBranchFormat(network_name=branch_format_name)
 
813
        return RemoteBranch(self, self.find_repository(), format=format,
 
814
                            setup_stacking=not ignore_fallbacks, name=name,
 
815
                            possible_transports=possible_transports)
 
816
 
 
817
    def open_branch(self, name=None, unsupported=False,
 
818
                    ignore_fallbacks=False, possible_transports=None):
 
819
        if name is None:
 
820
            name = self._get_selected_branch()
 
821
        if name != "":
 
822
            raise errors.NoColocatedBranchSupport(self)
 
823
        if unsupported:
 
824
            raise NotImplementedError(
 
825
                'unsupported flag support not implemented yet.')
 
826
        if self._next_open_branch_result is not None:
 
827
            # See create_branch for details.
 
828
            result = self._next_open_branch_result
 
829
            self._next_open_branch_result = None
 
830
            return result
 
831
        response = self._get_branch_reference()
 
832
        return self._open_branch(name, response[0], response[1],
 
833
                                 possible_transports=possible_transports,
 
834
                                 ignore_fallbacks=ignore_fallbacks)
 
835
 
 
836
    def _open_repo_v1(self, path):
 
837
        verb = b'BzrDir.find_repository'
 
838
        response = self._call(verb, path)
 
839
        if response[0] != b'ok':
 
840
            raise errors.UnexpectedSmartServerResponse(response)
 
841
        # servers that only support the v1 method don't support external
 
842
        # references either.
 
843
        self._ensure_real()
 
844
        repo = self._real_bzrdir.open_repository()
 
845
        response = response + (b'no', repo._format.network_name())
 
846
        return response, repo
 
847
 
 
848
    def _open_repo_v2(self, path):
 
849
        verb = b'BzrDir.find_repositoryV2'
 
850
        response = self._call(verb, path)
 
851
        if response[0] != b'ok':
 
852
            raise errors.UnexpectedSmartServerResponse(response)
 
853
        self._ensure_real()
 
854
        repo = self._real_bzrdir.open_repository()
 
855
        response = response + (repo._format.network_name(),)
 
856
        return response, repo
 
857
 
 
858
    def _open_repo_v3(self, path):
 
859
        verb = b'BzrDir.find_repositoryV3'
 
860
        medium = self._client._medium
 
861
        if medium._is_remote_before((1, 13)):
 
862
            raise errors.UnknownSmartMethod(verb)
 
863
        try:
 
864
            response = self._call(verb, path)
 
865
        except errors.UnknownSmartMethod:
 
866
            medium._remember_remote_is_before((1, 13))
 
867
            raise
 
868
        if response[0] != b'ok':
 
869
            raise errors.UnexpectedSmartServerResponse(response)
 
870
        return response, None
 
871
 
 
872
    def open_repository(self):
 
873
        path = self._path_for_remote_call(self._client)
 
874
        response = None
 
875
        for probe in [self._open_repo_v3, self._open_repo_v2,
 
876
                      self._open_repo_v1]:
 
877
            try:
 
878
                response, real_repo = probe(path)
 
879
                break
 
880
            except errors.UnknownSmartMethod:
 
881
                pass
 
882
        if response is None:
 
883
            raise errors.UnknownSmartMethod(b'BzrDir.find_repository{3,2,}')
 
884
        if response[0] != b'ok':
 
885
            raise errors.UnexpectedSmartServerResponse(response)
 
886
        if len(response) != 6:
 
887
            raise SmartProtocolError(
 
888
                'incorrect response length %s' % (response,))
 
889
        if response[1] == b'':
 
890
            # repo is at this dir.
 
891
            format = response_tuple_to_repo_format(response[2:])
 
892
            # Used to support creating a real format instance when needed.
 
893
            format._creating_bzrdir = self
 
894
            remote_repo = RemoteRepository(self, format)
 
895
            format._creating_repo = remote_repo
 
896
            if real_repo is not None:
 
897
                remote_repo._set_real_repository(real_repo)
 
898
            return remote_repo
 
899
        else:
 
900
            raise errors.NoRepositoryPresent(self)
 
901
 
 
902
    def has_workingtree(self):
 
903
        if self._has_working_tree is None:
 
904
            path = self._path_for_remote_call(self._client)
 
905
            try:
 
906
                response = self._call(b'BzrDir.has_workingtree', path)
 
907
            except errors.UnknownSmartMethod:
 
908
                self._ensure_real()
 
909
                self._has_working_tree = self._real_bzrdir.has_workingtree()
 
910
            else:
 
911
                if response[0] not in (b'yes', b'no'):
 
912
                    raise SmartProtocolError(
 
913
                        'unexpected response code %s' % (response,))
 
914
                self._has_working_tree = (response[0] == b'yes')
 
915
        return self._has_working_tree
 
916
 
 
917
    def open_workingtree(self, recommend_upgrade=True):
 
918
        if self.has_workingtree():
 
919
            raise errors.NotLocalUrl(self.root_transport)
 
920
        else:
 
921
            raise errors.NoWorkingTree(self.root_transport.base)
 
922
 
 
923
    def _path_for_remote_call(self, client):
 
924
        """Return the path to be used for this bzrdir in a remote call."""
 
925
        remote_path = client.remote_path_from_transport(self.root_transport)
 
926
        remote_path = remote_path.decode('utf-8')
 
927
        base_url, segment_parameters = urlutils.split_segment_parameters_raw(
 
928
            remote_path)
 
929
        base_url = base_url.encode('utf-8')
 
930
        return base_url
 
931
 
 
932
    def get_branch_transport(self, branch_format, name=None):
 
933
        self._ensure_real()
 
934
        return self._real_bzrdir.get_branch_transport(branch_format, name=name)
 
935
 
 
936
    def get_repository_transport(self, repository_format):
 
937
        self._ensure_real()
 
938
        return self._real_bzrdir.get_repository_transport(repository_format)
 
939
 
 
940
    def get_workingtree_transport(self, workingtree_format):
 
941
        self._ensure_real()
 
942
        return self._real_bzrdir.get_workingtree_transport(workingtree_format)
 
943
 
 
944
    def can_convert_format(self):
 
945
        """Upgrading of remote bzrdirs is not supported yet."""
 
946
        return False
 
947
 
 
948
    def needs_format_conversion(self, format):
 
949
        """Upgrading of remote bzrdirs is not supported yet."""
 
950
        return False
 
951
 
 
952
    def _get_config(self):
 
953
        return RemoteBzrDirConfig(self)
 
954
 
 
955
    def _get_config_store(self):
 
956
        return RemoteControlStore(self)
 
957
 
 
958
 
 
959
class RemoteInventoryTree(InventoryRevisionTree):
 
960
 
 
961
    def __init__(self, repository, inv, revision_id):
 
962
        super(RemoteInventoryTree, self).__init__(repository, inv, revision_id)
 
963
 
 
964
    def archive(self, format, name, root=None, subdir=None, force_mtime=None):
 
965
        ret = self._repository._revision_archive(
 
966
            self.get_revision_id(), format, name, root, subdir,
 
967
            force_mtime=force_mtime)
 
968
        if ret is None:
 
969
            return super(RemoteInventoryTree, self).archive(
 
970
                format, name, root, subdir, force_mtime=force_mtime)
 
971
        return ret
 
972
 
 
973
    def annotate_iter(self, path,
 
974
                      default_revision=_mod_revision.CURRENT_REVISION):
 
975
        """Return an iterator of revision_id, line tuples.
 
976
 
 
977
        For working trees (and mutable trees in general), the special
 
978
        revision_id 'current:' will be used for lines that are new in this
 
979
        tree, e.g. uncommitted changes.
 
980
        :param default_revision: For lines that don't match a basis, mark them
 
981
            with this revision id. Not all implementations will make use of
 
982
            this value.
 
983
        """
 
984
        ret = self._repository._annotate_file_revision(
 
985
            self.get_revision_id(), path, file_id=None,
 
986
            default_revision=default_revision)
 
987
        if ret is None:
 
988
            return super(RemoteInventoryTree, self).annotate_iter(
 
989
                path, default_revision=default_revision)
 
990
        return ret
 
991
 
 
992
 
 
993
class RemoteRepositoryFormat(vf_repository.VersionedFileRepositoryFormat):
 
994
    """Format for repositories accessed over a _SmartClient.
 
995
 
 
996
    Instances of this repository are represented by RemoteRepository
 
997
    instances.
 
998
 
 
999
    The RemoteRepositoryFormat is parameterized during construction
 
1000
    to reflect the capabilities of the real, remote format. Specifically
 
1001
    the attributes rich_root_data and supports_tree_reference are set
 
1002
    on a per instance basis, and are not set (and should not be) at
 
1003
    the class level.
 
1004
 
 
1005
    :ivar _custom_format: If set, a specific concrete repository format that
 
1006
        will be used when initializing a repository with this
 
1007
        RemoteRepositoryFormat.
 
1008
    :ivar _creating_repo: If set, the repository object that this
 
1009
        RemoteRepositoryFormat was created for: it can be called into
 
1010
        to obtain data like the network name.
 
1011
    """
 
1012
 
 
1013
    _matchingcontroldir = RemoteBzrDirFormat()
 
1014
    supports_full_versioned_files = True
 
1015
    supports_leaving_lock = True
 
1016
    supports_overriding_transport = False
 
1017
 
 
1018
    def __init__(self):
 
1019
        _mod_repository.RepositoryFormat.__init__(self)
 
1020
        self._custom_format = None
 
1021
        self._network_name = None
 
1022
        self._creating_bzrdir = None
 
1023
        self._revision_graph_can_have_wrong_parents = None
 
1024
        self._supports_chks = None
 
1025
        self._supports_external_lookups = None
 
1026
        self._supports_tree_reference = None
 
1027
        self._supports_funky_characters = None
 
1028
        self._supports_nesting_repositories = None
 
1029
        self._rich_root_data = None
 
1030
 
 
1031
    def __repr__(self):
 
1032
        return "%s(_network_name=%r)" % (self.__class__.__name__,
 
1033
                                         self._network_name)
 
1034
 
 
1035
    @property
 
1036
    def fast_deltas(self):
 
1037
        self._ensure_real()
 
1038
        return self._custom_format.fast_deltas
 
1039
 
 
1040
    @property
 
1041
    def rich_root_data(self):
 
1042
        if self._rich_root_data is None:
 
1043
            self._ensure_real()
 
1044
            self._rich_root_data = self._custom_format.rich_root_data
 
1045
        return self._rich_root_data
 
1046
 
 
1047
    @property
 
1048
    def supports_chks(self):
 
1049
        if self._supports_chks is None:
 
1050
            self._ensure_real()
 
1051
            self._supports_chks = self._custom_format.supports_chks
 
1052
        return self._supports_chks
 
1053
 
 
1054
    @property
 
1055
    def supports_external_lookups(self):
 
1056
        if self._supports_external_lookups is None:
 
1057
            self._ensure_real()
 
1058
            self._supports_external_lookups = \
 
1059
                self._custom_format.supports_external_lookups
 
1060
        return self._supports_external_lookups
 
1061
 
 
1062
    @property
 
1063
    def supports_funky_characters(self):
 
1064
        if self._supports_funky_characters is None:
 
1065
            self._ensure_real()
 
1066
            self._supports_funky_characters = \
 
1067
                self._custom_format.supports_funky_characters
 
1068
        return self._supports_funky_characters
 
1069
 
 
1070
    @property
 
1071
    def supports_nesting_repositories(self):
 
1072
        if self._supports_nesting_repositories is None:
 
1073
            self._ensure_real()
 
1074
            self._supports_nesting_repositories = \
 
1075
                self._custom_format.supports_nesting_repositories
 
1076
        return self._supports_nesting_repositories
 
1077
 
 
1078
    @property
 
1079
    def supports_tree_reference(self):
 
1080
        if self._supports_tree_reference is None:
 
1081
            self._ensure_real()
 
1082
            self._supports_tree_reference = \
 
1083
                self._custom_format.supports_tree_reference
 
1084
        return self._supports_tree_reference
 
1085
 
 
1086
    @property
 
1087
    def revision_graph_can_have_wrong_parents(self):
 
1088
        if self._revision_graph_can_have_wrong_parents is None:
 
1089
            self._ensure_real()
 
1090
            self._revision_graph_can_have_wrong_parents = \
 
1091
                self._custom_format.revision_graph_can_have_wrong_parents
 
1092
        return self._revision_graph_can_have_wrong_parents
 
1093
 
 
1094
    def _vfs_initialize(self, a_controldir, shared):
 
1095
        """Helper for common code in initialize."""
 
1096
        if self._custom_format:
 
1097
            # Custom format requested
 
1098
            result = self._custom_format.initialize(
 
1099
                a_controldir, shared=shared)
 
1100
        elif self._creating_bzrdir is not None:
 
1101
            # Use the format that the repository we were created to back
 
1102
            # has.
 
1103
            prior_repo = self._creating_bzrdir.open_repository()
 
1104
            prior_repo._ensure_real()
 
1105
            result = prior_repo._real_repository._format.initialize(
 
1106
                a_controldir, shared=shared)
 
1107
        else:
 
1108
            # assume that a_bzr is a RemoteBzrDir but the smart server didn't
 
1109
            # support remote initialization.
 
1110
            # We delegate to a real object at this point (as RemoteBzrDir
 
1111
            # delegate to the repository format which would lead to infinite
 
1112
            # recursion if we just called a_controldir.create_repository.
 
1113
            a_controldir._ensure_real()
 
1114
            result = a_controldir._real_bzrdir.create_repository(shared=shared)
 
1115
        if not isinstance(result, RemoteRepository):
 
1116
            return self.open(a_controldir)
 
1117
        else:
 
1118
            return result
 
1119
 
 
1120
    def initialize(self, a_controldir, shared=False):
 
1121
        # Being asked to create on a non RemoteBzrDir:
 
1122
        if not isinstance(a_controldir, RemoteBzrDir):
 
1123
            return self._vfs_initialize(a_controldir, shared)
 
1124
        medium = a_controldir._client._medium
 
1125
        if medium._is_remote_before((1, 13)):
 
1126
            return self._vfs_initialize(a_controldir, shared)
 
1127
        # Creating on a remote bzr dir.
 
1128
        # 1) get the network name to use.
 
1129
        if self._custom_format:
 
1130
            network_name = self._custom_format.network_name()
 
1131
        elif self._network_name:
 
1132
            network_name = self._network_name
 
1133
        else:
 
1134
            # Select the current breezy default and ask for that.
 
1135
            reference_bzrdir_format = controldir.format_registry.get(
 
1136
                'default')()
 
1137
            reference_format = reference_bzrdir_format.repository_format
 
1138
            network_name = reference_format.network_name()
 
1139
        # 2) try direct creation via RPC
 
1140
        path = a_controldir._path_for_remote_call(a_controldir._client)
 
1141
        verb = b'BzrDir.create_repository'
 
1142
        if shared:
 
1143
            shared_str = b'True'
 
1144
        else:
 
1145
            shared_str = b'False'
 
1146
        try:
 
1147
            response = a_controldir._call(verb, path, network_name, shared_str)
 
1148
        except errors.UnknownSmartMethod:
 
1149
            # Fallback - use vfs methods
 
1150
            medium._remember_remote_is_before((1, 13))
 
1151
            return self._vfs_initialize(a_controldir, shared)
 
1152
        else:
 
1153
            # Turn the response into a RemoteRepository object.
 
1154
            format = response_tuple_to_repo_format(response[1:])
 
1155
            # Used to support creating a real format instance when needed.
 
1156
            format._creating_bzrdir = a_controldir
 
1157
            remote_repo = RemoteRepository(a_controldir, format)
 
1158
            format._creating_repo = remote_repo
 
1159
            return remote_repo
 
1160
 
 
1161
    def open(self, a_controldir):
 
1162
        if not isinstance(a_controldir, RemoteBzrDir):
 
1163
            raise AssertionError('%r is not a RemoteBzrDir' % (a_controldir,))
 
1164
        return a_controldir.open_repository()
 
1165
 
 
1166
    def _ensure_real(self):
 
1167
        if self._custom_format is None:
 
1168
            try:
 
1169
                self._custom_format = _mod_repository.network_format_registry.get(
 
1170
                    self._network_name)
 
1171
            except KeyError:
 
1172
                raise errors.UnknownFormatError(kind='repository',
 
1173
                                                format=self._network_name)
 
1174
 
 
1175
    @property
 
1176
    def _fetch_order(self):
 
1177
        self._ensure_real()
 
1178
        return self._custom_format._fetch_order
 
1179
 
 
1180
    @property
 
1181
    def _fetch_uses_deltas(self):
 
1182
        self._ensure_real()
 
1183
        return self._custom_format._fetch_uses_deltas
 
1184
 
 
1185
    @property
 
1186
    def _fetch_reconcile(self):
 
1187
        self._ensure_real()
 
1188
        return self._custom_format._fetch_reconcile
 
1189
 
 
1190
    def get_format_description(self):
 
1191
        self._ensure_real()
 
1192
        return 'Remote: ' + self._custom_format.get_format_description()
 
1193
 
 
1194
    def __eq__(self, other):
 
1195
        return self.__class__ is other.__class__
 
1196
 
 
1197
    def network_name(self):
 
1198
        if self._network_name:
 
1199
            return self._network_name
 
1200
        self._creating_repo._ensure_real()
 
1201
        return self._creating_repo._real_repository._format.network_name()
 
1202
 
 
1203
    @property
 
1204
    def pack_compresses(self):
 
1205
        self._ensure_real()
 
1206
        return self._custom_format.pack_compresses
 
1207
 
 
1208
    @property
 
1209
    def _serializer(self):
 
1210
        self._ensure_real()
 
1211
        return self._custom_format._serializer
 
1212
 
 
1213
 
 
1214
class RemoteRepository(_mod_repository.Repository, _RpcHelper,
 
1215
                       lock._RelockDebugMixin):
 
1216
    """Repository accessed over rpc.
 
1217
 
 
1218
    For the moment most operations are performed using local transport-backed
 
1219
    Repository objects.
 
1220
    """
 
1221
 
 
1222
    def __init__(self, remote_bzrdir, format, real_repository=None, _client=None):
 
1223
        """Create a RemoteRepository instance.
 
1224
 
 
1225
        :param remote_bzrdir: The bzrdir hosting this repository.
 
1226
        :param format: The RemoteFormat object to use.
 
1227
        :param real_repository: If not None, a local implementation of the
 
1228
            repository logic for the repository, usually accessing the data
 
1229
            via the VFS.
 
1230
        :param _client: Private testing parameter - override the smart client
 
1231
            to be used by the repository.
 
1232
        """
 
1233
        if real_repository:
 
1234
            self._real_repository = real_repository
 
1235
        else:
 
1236
            self._real_repository = None
 
1237
        self.controldir = remote_bzrdir
 
1238
        if _client is None:
 
1239
            self._client = remote_bzrdir._client
 
1240
        else:
 
1241
            self._client = _client
 
1242
        self._format = format
 
1243
        self._lock_mode = None
 
1244
        self._lock_token = None
 
1245
        self._write_group_tokens = None
 
1246
        self._lock_count = 0
 
1247
        self._leave_lock = False
 
1248
        # Cache of revision parents; misses are cached during read locks, and
 
1249
        # write locks when no _real_repository has been set.
 
1250
        self._unstacked_provider = graph.CachingParentsProvider(
 
1251
            get_parent_map=self._get_parent_map_rpc)
 
1252
        self._unstacked_provider.disable_cache()
 
1253
        # For tests:
 
1254
        # These depend on the actual remote format, so force them off for
 
1255
        # maximum compatibility. XXX: In future these should depend on the
 
1256
        # remote repository instance, but this is irrelevant until we perform
 
1257
        # reconcile via an RPC call.
 
1258
        self._reconcile_does_inventory_gc = False
 
1259
        self._reconcile_fixes_text_parents = False
 
1260
        self._reconcile_backsup_inventory = False
 
1261
        self.base = self.controldir.transport.base
 
1262
        # Additional places to query for data.
 
1263
        self._fallback_repositories = []
 
1264
 
 
1265
    @property
 
1266
    def user_transport(self):
 
1267
        return self.controldir.user_transport
 
1268
 
 
1269
    @property
 
1270
    def control_transport(self):
 
1271
        # XXX: Normally you shouldn't directly get at the remote repository
 
1272
        # transport, but I'm not sure it's worth making this method
 
1273
        # optional -- mbp 2010-04-21
 
1274
        return self.controldir.get_repository_transport(None)
 
1275
 
 
1276
    def __str__(self):
 
1277
        return "%s(%s)" % (self.__class__.__name__, self.base)
 
1278
 
 
1279
    __repr__ = __str__
 
1280
 
 
1281
    def abort_write_group(self, suppress_errors=False):
 
1282
        """Complete a write group on the decorated repository.
 
1283
 
 
1284
        Smart methods perform operations in a single step so this API
 
1285
        is not really applicable except as a compatibility thunk
 
1286
        for older plugins that don't use e.g. the CommitBuilder
 
1287
        facility.
 
1288
 
 
1289
        :param suppress_errors: see Repository.abort_write_group.
 
1290
        """
 
1291
        if self._real_repository:
 
1292
            self._ensure_real()
 
1293
            return self._real_repository.abort_write_group(
 
1294
                suppress_errors=suppress_errors)
 
1295
        if not self.is_in_write_group():
 
1296
            if suppress_errors:
 
1297
                mutter('(suppressed) not in write group')
 
1298
                return
 
1299
            raise errors.BzrError("not in write group")
 
1300
        path = self.controldir._path_for_remote_call(self._client)
 
1301
        try:
 
1302
            response = self._call(b'Repository.abort_write_group', path,
 
1303
                                  self._lock_token,
 
1304
                                  [token.encode('utf-8') for token in self._write_group_tokens])
 
1305
        except Exception as exc:
 
1306
            self._write_group = None
 
1307
            if not suppress_errors:
 
1308
                raise
 
1309
            mutter('abort_write_group failed')
 
1310
            log_exception_quietly()
 
1311
            note(gettext('bzr: ERROR (ignored): %s'), exc)
 
1312
        else:
 
1313
            if response != (b'ok', ):
 
1314
                raise errors.UnexpectedSmartServerResponse(response)
 
1315
            self._write_group_tokens = None
 
1316
 
 
1317
    @property
 
1318
    def chk_bytes(self):
 
1319
        """Decorate the real repository for now.
 
1320
 
 
1321
        In the long term a full blown network facility is needed to avoid
 
1322
        creating a real repository object locally.
 
1323
        """
 
1324
        self._ensure_real()
 
1325
        return self._real_repository.chk_bytes
 
1326
 
 
1327
    def commit_write_group(self):
 
1328
        """Complete a write group on the decorated repository.
 
1329
 
 
1330
        Smart methods perform operations in a single step so this API
 
1331
        is not really applicable except as a compatibility thunk
 
1332
        for older plugins that don't use e.g. the CommitBuilder
 
1333
        facility.
 
1334
        """
 
1335
        if self._real_repository:
 
1336
            self._ensure_real()
 
1337
            return self._real_repository.commit_write_group()
 
1338
        if not self.is_in_write_group():
 
1339
            raise errors.BzrError("not in write group")
 
1340
        path = self.controldir._path_for_remote_call(self._client)
 
1341
        response = self._call(b'Repository.commit_write_group', path,
 
1342
                              self._lock_token, [token.encode('utf-8') for token in self._write_group_tokens])
 
1343
        if response != (b'ok', ):
 
1344
            raise errors.UnexpectedSmartServerResponse(response)
 
1345
        self._write_group_tokens = None
 
1346
        # Refresh data after writing to the repository.
 
1347
        self.refresh_data()
 
1348
 
 
1349
    def resume_write_group(self, tokens):
 
1350
        if self._real_repository:
 
1351
            return self._real_repository.resume_write_group(tokens)
 
1352
        path = self.controldir._path_for_remote_call(self._client)
 
1353
        try:
 
1354
            response = self._call(b'Repository.check_write_group', path,
 
1355
                                  self._lock_token, [token.encode('utf-8') for token in tokens])
 
1356
        except errors.UnknownSmartMethod:
 
1357
            self._ensure_real()
 
1358
            return self._real_repository.resume_write_group(tokens)
 
1359
        if response != (b'ok', ):
 
1360
            raise errors.UnexpectedSmartServerResponse(response)
 
1361
        self._write_group_tokens = tokens
 
1362
 
 
1363
    def suspend_write_group(self):
 
1364
        if self._real_repository:
 
1365
            return self._real_repository.suspend_write_group()
 
1366
        ret = self._write_group_tokens or []
 
1367
        self._write_group_tokens = None
 
1368
        return ret
 
1369
 
 
1370
    def get_missing_parent_inventories(self, check_for_missing_texts=True):
 
1371
        self._ensure_real()
 
1372
        return self._real_repository.get_missing_parent_inventories(
 
1373
            check_for_missing_texts=check_for_missing_texts)
 
1374
 
 
1375
    def _get_rev_id_for_revno_vfs(self, revno, known_pair):
 
1376
        self._ensure_real()
 
1377
        return self._real_repository.get_rev_id_for_revno(
 
1378
            revno, known_pair)
 
1379
 
 
1380
    def get_rev_id_for_revno(self, revno, known_pair):
 
1381
        """See Repository.get_rev_id_for_revno."""
 
1382
        path = self.controldir._path_for_remote_call(self._client)
 
1383
        try:
 
1384
            if self._client._medium._is_remote_before((1, 17)):
 
1385
                return self._get_rev_id_for_revno_vfs(revno, known_pair)
 
1386
            response = self._call(
 
1387
                b'Repository.get_rev_id_for_revno', path, revno, known_pair)
 
1388
        except errors.UnknownSmartMethod:
 
1389
            self._client._medium._remember_remote_is_before((1, 17))
 
1390
            return self._get_rev_id_for_revno_vfs(revno, known_pair)
 
1391
        except errors.UnknownErrorFromSmartServer as e:
 
1392
            # Older versions of Bazaar/Breezy (<< 3.0.0) would raise a
 
1393
            # ValueError instead of returning revno-outofbounds
 
1394
            if len(e.error_tuple) < 3:
 
1395
                raise
 
1396
            if e.error_tuple[:2] != (b'error', b'ValueError'):
 
1397
                raise
 
1398
            m = re.match(
 
1399
                br"requested revno \(([0-9]+)\) is later than given "
 
1400
                br"known revno \(([0-9]+)\)", e.error_tuple[2])
 
1401
            if not m:
 
1402
                raise
 
1403
            raise errors.RevnoOutOfBounds(
 
1404
                int(m.group(1)), (0, int(m.group(2))))
 
1405
        if response[0] == b'ok':
 
1406
            return True, response[1]
 
1407
        elif response[0] == b'history-incomplete':
 
1408
            known_pair = response[1:3]
 
1409
            for fallback in self._fallback_repositories:
 
1410
                found, result = fallback.get_rev_id_for_revno(
 
1411
                    revno, known_pair)
 
1412
                if found:
 
1413
                    return True, result
 
1414
                else:
 
1415
                    known_pair = result
 
1416
            # Not found in any fallbacks
 
1417
            return False, known_pair
 
1418
        else:
 
1419
            raise errors.UnexpectedSmartServerResponse(response)
 
1420
 
 
1421
    def _ensure_real(self):
 
1422
        """Ensure that there is a _real_repository set.
 
1423
 
 
1424
        Used before calls to self._real_repository.
 
1425
 
 
1426
        Note that _ensure_real causes many roundtrips to the server which are
 
1427
        not desirable, and prevents the use of smart one-roundtrip RPC's to
 
1428
        perform complex operations (such as accessing parent data, streaming
 
1429
        revisions etc). Adding calls to _ensure_real should only be done when
 
1430
        bringing up new functionality, adding fallbacks for smart methods that
 
1431
        require a fallback path, and never to replace an existing smart method
 
1432
        invocation. If in doubt chat to the bzr network team.
 
1433
        """
 
1434
        if self._real_repository is None:
 
1435
            if 'hpssvfs' in debug.debug_flags:
 
1436
                import traceback
 
1437
                warning('VFS Repository access triggered\n%s',
 
1438
                        ''.join(traceback.format_stack()))
 
1439
            self._unstacked_provider.missing_keys.clear()
 
1440
            self.controldir._ensure_real()
 
1441
            self._set_real_repository(
 
1442
                self.controldir._real_bzrdir.open_repository())
 
1443
 
 
1444
    def _translate_error(self, err, **context):
 
1445
        self.controldir._translate_error(err, repository=self, **context)
 
1446
 
 
1447
    def find_text_key_references(self):
 
1448
        """Find the text key references within the repository.
 
1449
 
 
1450
        :return: A dictionary mapping text keys ((fileid, revision_id) tuples)
 
1451
            to whether they were referred to by the inventory of the
 
1452
            revision_id that they contain. The inventory texts from all present
 
1453
            revision ids are assessed to generate this report.
 
1454
        """
 
1455
        self._ensure_real()
 
1456
        return self._real_repository.find_text_key_references()
 
1457
 
 
1458
    def _generate_text_key_index(self):
 
1459
        """Generate a new text key index for the repository.
 
1460
 
 
1461
        This is an expensive function that will take considerable time to run.
 
1462
 
 
1463
        :return: A dict mapping (file_id, revision_id) tuples to a list of
 
1464
            parents, also (file_id, revision_id) tuples.
 
1465
        """
 
1466
        self._ensure_real()
 
1467
        return self._real_repository._generate_text_key_index()
 
1468
 
 
1469
    def _get_revision_graph(self, revision_id):
 
1470
        """Private method for using with old (< 1.2) servers to fallback."""
 
1471
        if revision_id is None:
 
1472
            revision_id = b''
 
1473
        elif _mod_revision.is_null(revision_id):
 
1474
            return {}
 
1475
 
 
1476
        path = self.controldir._path_for_remote_call(self._client)
 
1477
        response = self._call_expecting_body(
 
1478
            b'Repository.get_revision_graph', path, revision_id)
 
1479
        response_tuple, response_handler = response
 
1480
        if response_tuple[0] != b'ok':
 
1481
            raise errors.UnexpectedSmartServerResponse(response_tuple)
 
1482
        coded = response_handler.read_body_bytes()
 
1483
        if coded == b'':
 
1484
            # no revisions in this repository!
 
1485
            return {}
 
1486
        lines = coded.split(b'\n')
 
1487
        revision_graph = {}
 
1488
        for line in lines:
 
1489
            d = tuple(line.split())
 
1490
            revision_graph[d[0]] = d[1:]
 
1491
 
 
1492
        return revision_graph
 
1493
 
 
1494
    def _get_sink(self):
 
1495
        """See Repository._get_sink()."""
 
1496
        return RemoteStreamSink(self)
 
1497
 
 
1498
    def _get_source(self, to_format):
 
1499
        """Return a source for streaming from this repository."""
 
1500
        return RemoteStreamSource(self, to_format)
 
1501
 
 
1502
    def get_file_graph(self):
 
1503
        with self.lock_read():
 
1504
            return graph.Graph(self.texts)
 
1505
 
 
1506
    def has_revision(self, revision_id):
 
1507
        """True if this repository has a copy of the revision."""
 
1508
        # Copy of breezy.repository.Repository.has_revision
 
1509
        with self.lock_read():
 
1510
            return revision_id in self.has_revisions((revision_id,))
 
1511
 
 
1512
    def has_revisions(self, revision_ids):
 
1513
        """Probe to find out the presence of multiple revisions.
 
1514
 
 
1515
        :param revision_ids: An iterable of revision_ids.
 
1516
        :return: A set of the revision_ids that were present.
 
1517
        """
 
1518
        with self.lock_read():
 
1519
            # Copy of breezy.repository.Repository.has_revisions
 
1520
            parent_map = self.get_parent_map(revision_ids)
 
1521
            result = set(parent_map)
 
1522
            if _mod_revision.NULL_REVISION in revision_ids:
 
1523
                result.add(_mod_revision.NULL_REVISION)
 
1524
            return result
 
1525
 
 
1526
    def _has_same_fallbacks(self, other_repo):
 
1527
        """Returns true if the repositories have the same fallbacks."""
 
1528
        # XXX: copied from Repository; it should be unified into a base class
 
1529
        # <https://bugs.launchpad.net/bzr/+bug/401622>
 
1530
        my_fb = self._fallback_repositories
 
1531
        other_fb = other_repo._fallback_repositories
 
1532
        if len(my_fb) != len(other_fb):
 
1533
            return False
 
1534
        for f, g in zip(my_fb, other_fb):
 
1535
            if not f.has_same_location(g):
 
1536
                return False
 
1537
        return True
 
1538
 
 
1539
    def has_same_location(self, other):
 
1540
        # TODO: Move to RepositoryBase and unify with the regular Repository
 
1541
        # one; unfortunately the tests rely on slightly different behaviour at
 
1542
        # present -- mbp 20090710
 
1543
        return (self.__class__ is other.__class__
 
1544
                and self.controldir.transport.base == other.controldir.transport.base)
 
1545
 
 
1546
    def get_graph(self, other_repository=None):
 
1547
        """Return the graph for this repository format"""
 
1548
        parents_provider = self._make_parents_provider(other_repository)
 
1549
        return graph.Graph(parents_provider)
 
1550
 
 
1551
    def get_known_graph_ancestry(self, revision_ids):
 
1552
        """Return the known graph for a set of revision ids and their ancestors.
 
1553
        """
 
1554
        with self.lock_read():
 
1555
            revision_graph = dict(((key, value) for key, value in
 
1556
                                   self.get_graph().iter_ancestry(revision_ids) if value is not None))
 
1557
            revision_graph = _mod_repository._strip_NULL_ghosts(revision_graph)
 
1558
            return graph.KnownGraph(revision_graph)
 
1559
 
 
1560
    def gather_stats(self, revid=None, committers=None):
 
1561
        """See Repository.gather_stats()."""
 
1562
        path = self.controldir._path_for_remote_call(self._client)
 
1563
        # revid can be None to indicate no revisions, not just NULL_REVISION
 
1564
        if revid is None or _mod_revision.is_null(revid):
 
1565
            fmt_revid = b''
 
1566
        else:
 
1567
            fmt_revid = revid
 
1568
        if committers is None or not committers:
 
1569
            fmt_committers = b'no'
 
1570
        else:
 
1571
            fmt_committers = b'yes'
 
1572
        response_tuple, response_handler = self._call_expecting_body(
 
1573
            b'Repository.gather_stats', path, fmt_revid, fmt_committers)
 
1574
        if response_tuple[0] != b'ok':
 
1575
            raise errors.UnexpectedSmartServerResponse(response_tuple)
 
1576
 
 
1577
        body = response_handler.read_body_bytes()
 
1578
        result = {}
 
1579
        for line in body.split(b'\n'):
 
1580
            if not line:
 
1581
                continue
 
1582
            key, val_text = line.split(b':')
 
1583
            key = key.decode('ascii')
 
1584
            if key in ('revisions', 'size', 'committers'):
 
1585
                result[key] = int(val_text)
 
1586
            elif key in ('firstrev', 'latestrev'):
 
1587
                values = val_text.split(b' ')[1:]
 
1588
                result[key] = (float(values[0]), int(values[1]))
 
1589
 
 
1590
        return result
 
1591
 
 
1592
    def find_branches(self, using=False):
 
1593
        """See Repository.find_branches()."""
 
1594
        # should be an API call to the server.
 
1595
        self._ensure_real()
 
1596
        return self._real_repository.find_branches(using=using)
 
1597
 
 
1598
    def get_physical_lock_status(self):
 
1599
        """See Repository.get_physical_lock_status()."""
 
1600
        path = self.controldir._path_for_remote_call(self._client)
 
1601
        try:
 
1602
            response = self._call(b'Repository.get_physical_lock_status', path)
 
1603
        except errors.UnknownSmartMethod:
 
1604
            self._ensure_real()
 
1605
            return self._real_repository.get_physical_lock_status()
 
1606
        if response[0] not in (b'yes', b'no'):
 
1607
            raise errors.UnexpectedSmartServerResponse(response)
 
1608
        return (response[0] == b'yes')
 
1609
 
 
1610
    def is_in_write_group(self):
 
1611
        """Return True if there is an open write group.
 
1612
 
 
1613
        write groups are only applicable locally for the smart server..
 
1614
        """
 
1615
        if self._write_group_tokens is not None:
 
1616
            return True
 
1617
        if self._real_repository:
 
1618
            return self._real_repository.is_in_write_group()
 
1619
 
 
1620
    def is_locked(self):
 
1621
        return self._lock_count >= 1
 
1622
 
 
1623
    def is_shared(self):
 
1624
        """See Repository.is_shared()."""
 
1625
        path = self.controldir._path_for_remote_call(self._client)
 
1626
        response = self._call(b'Repository.is_shared', path)
 
1627
        if response[0] not in (b'yes', b'no'):
 
1628
            raise SmartProtocolError(
 
1629
                'unexpected response code %s' % (response,))
 
1630
        return response[0] == b'yes'
 
1631
 
 
1632
    def is_write_locked(self):
 
1633
        return self._lock_mode == 'w'
 
1634
 
 
1635
    def _warn_if_deprecated(self, branch=None):
 
1636
        # If we have a real repository, the check will be done there, if we
 
1637
        # don't the check will be done remotely.
 
1638
        pass
 
1639
 
 
1640
    def lock_read(self):
 
1641
        """Lock the repository for read operations.
 
1642
 
 
1643
        :return: A breezy.lock.LogicalLockResult.
 
1644
        """
 
1645
        # wrong eventually - want a local lock cache context
 
1646
        if not self._lock_mode:
 
1647
            self._note_lock('r')
 
1648
            self._lock_mode = 'r'
 
1649
            self._lock_count = 1
 
1650
            self._unstacked_provider.enable_cache(cache_misses=True)
 
1651
            if self._real_repository is not None:
 
1652
                self._real_repository.lock_read()
 
1653
            for repo in self._fallback_repositories:
 
1654
                repo.lock_read()
 
1655
        else:
 
1656
            self._lock_count += 1
 
1657
        return lock.LogicalLockResult(self.unlock)
 
1658
 
 
1659
    def _remote_lock_write(self, token):
 
1660
        path = self.controldir._path_for_remote_call(self._client)
 
1661
        if token is None:
 
1662
            token = b''
 
1663
        err_context = {'token': token}
 
1664
        response = self._call(b'Repository.lock_write', path, token,
 
1665
                              **err_context)
 
1666
        if response[0] == b'ok':
 
1667
            ok, token = response
 
1668
            return token
 
1669
        else:
 
1670
            raise errors.UnexpectedSmartServerResponse(response)
 
1671
 
 
1672
    def lock_write(self, token=None, _skip_rpc=False):
 
1673
        if not self._lock_mode:
 
1674
            self._note_lock('w')
 
1675
            if _skip_rpc:
 
1676
                if self._lock_token is not None:
 
1677
                    if token != self._lock_token:
 
1678
                        raise errors.TokenMismatch(token, self._lock_token)
 
1679
                self._lock_token = token
 
1680
            else:
 
1681
                self._lock_token = self._remote_lock_write(token)
 
1682
            # if self._lock_token is None, then this is something like packs or
 
1683
            # svn where we don't get to lock the repo, or a weave style repository
 
1684
            # where we cannot lock it over the wire and attempts to do so will
 
1685
            # fail.
 
1686
            if self._real_repository is not None:
 
1687
                self._real_repository.lock_write(token=self._lock_token)
 
1688
            if token is not None:
 
1689
                self._leave_lock = True
 
1690
            else:
 
1691
                self._leave_lock = False
 
1692
            self._lock_mode = 'w'
 
1693
            self._lock_count = 1
 
1694
            cache_misses = self._real_repository is None
 
1695
            self._unstacked_provider.enable_cache(cache_misses=cache_misses)
 
1696
            for repo in self._fallback_repositories:
 
1697
                # Writes don't affect fallback repos
 
1698
                repo.lock_read()
 
1699
        elif self._lock_mode == 'r':
 
1700
            raise errors.ReadOnlyError(self)
 
1701
        else:
 
1702
            self._lock_count += 1
 
1703
        return RepositoryWriteLockResult(self.unlock, self._lock_token or None)
 
1704
 
 
1705
    def leave_lock_in_place(self):
 
1706
        if not self._lock_token:
 
1707
            raise NotImplementedError(self.leave_lock_in_place)
 
1708
        self._leave_lock = True
 
1709
 
 
1710
    def dont_leave_lock_in_place(self):
 
1711
        if not self._lock_token:
 
1712
            raise NotImplementedError(self.dont_leave_lock_in_place)
 
1713
        self._leave_lock = False
 
1714
 
 
1715
    def _set_real_repository(self, repository):
 
1716
        """Set the _real_repository for this repository.
 
1717
 
 
1718
        :param repository: The repository to fallback to for non-hpss
 
1719
            implemented operations.
 
1720
        """
 
1721
        if self._real_repository is not None:
 
1722
            # Replacing an already set real repository.
 
1723
            # We cannot do this [currently] if the repository is locked -
 
1724
            # synchronised state might be lost.
 
1725
            if self.is_locked():
 
1726
                raise AssertionError('_real_repository is already set')
 
1727
        if isinstance(repository, RemoteRepository):
 
1728
            raise AssertionError()
 
1729
        self._real_repository = repository
 
1730
        # three code paths happen here:
 
1731
        # 1) old servers, RemoteBranch.open() calls _ensure_real before setting
 
1732
        # up stacking. In this case self._fallback_repositories is [], and the
 
1733
        # real repo is already setup. Preserve the real repo and
 
1734
        # RemoteRepository.add_fallback_repository will avoid adding
 
1735
        # duplicates.
 
1736
        # 2) new servers, RemoteBranch.open() sets up stacking, and when
 
1737
        # ensure_real is triggered from a branch, the real repository to
 
1738
        # set already has a matching list with separate instances, but
 
1739
        # as they are also RemoteRepositories we don't worry about making the
 
1740
        # lists be identical.
 
1741
        # 3) new servers, RemoteRepository.ensure_real is triggered before
 
1742
        # RemoteBranch.ensure real, in this case we get a repo with no fallbacks
 
1743
        # and need to populate it.
 
1744
        if (self._fallback_repositories
 
1745
            and len(self._real_repository._fallback_repositories)
 
1746
                != len(self._fallback_repositories)):
 
1747
            if len(self._real_repository._fallback_repositories):
 
1748
                raise AssertionError(
 
1749
                    "cannot cleanly remove existing _fallback_repositories")
 
1750
        for fb in self._fallback_repositories:
 
1751
            self._real_repository.add_fallback_repository(fb)
 
1752
        if self._lock_mode == 'w':
 
1753
            # if we are already locked, the real repository must be able to
 
1754
            # acquire the lock with our token.
 
1755
            self._real_repository.lock_write(self._lock_token)
 
1756
        elif self._lock_mode == 'r':
 
1757
            self._real_repository.lock_read()
 
1758
        if self._write_group_tokens is not None:
 
1759
            # if we are already in a write group, resume it
 
1760
            self._real_repository.resume_write_group(self._write_group_tokens)
 
1761
            self._write_group_tokens = None
 
1762
 
 
1763
    def start_write_group(self):
 
1764
        """Start a write group on the decorated repository.
 
1765
 
 
1766
        Smart methods perform operations in a single step so this API
 
1767
        is not really applicable except as a compatibility thunk
 
1768
        for older plugins that don't use e.g. the CommitBuilder
 
1769
        facility.
 
1770
        """
 
1771
        if self._real_repository:
 
1772
            self._ensure_real()
 
1773
            return self._real_repository.start_write_group()
 
1774
        if not self.is_write_locked():
 
1775
            raise errors.NotWriteLocked(self)
 
1776
        if self._write_group_tokens is not None:
 
1777
            raise errors.BzrError('already in a write group')
 
1778
        path = self.controldir._path_for_remote_call(self._client)
 
1779
        try:
 
1780
            response = self._call(b'Repository.start_write_group', path,
 
1781
                                  self._lock_token)
 
1782
        except (errors.UnknownSmartMethod, errors.UnsuspendableWriteGroup):
 
1783
            self._ensure_real()
 
1784
            return self._real_repository.start_write_group()
 
1785
        if response[0] != b'ok':
 
1786
            raise errors.UnexpectedSmartServerResponse(response)
 
1787
        self._write_group_tokens = [
 
1788
            token.decode('utf-8') for token in response[1]]
 
1789
 
 
1790
    def _unlock(self, token):
 
1791
        path = self.controldir._path_for_remote_call(self._client)
 
1792
        if not token:
 
1793
            # with no token the remote repository is not persistently locked.
 
1794
            return
 
1795
        err_context = {'token': token}
 
1796
        response = self._call(b'Repository.unlock', path, token,
 
1797
                              **err_context)
 
1798
        if response == (b'ok',):
 
1799
            return
 
1800
        else:
 
1801
            raise errors.UnexpectedSmartServerResponse(response)
 
1802
 
 
1803
    @only_raises(errors.LockNotHeld, errors.LockBroken)
 
1804
    def unlock(self):
 
1805
        if not self._lock_count:
 
1806
            return lock.cant_unlock_not_held(self)
 
1807
        self._lock_count -= 1
 
1808
        if self._lock_count > 0:
 
1809
            return
 
1810
        self._unstacked_provider.disable_cache()
 
1811
        old_mode = self._lock_mode
 
1812
        self._lock_mode = None
 
1813
        try:
 
1814
            # The real repository is responsible at present for raising an
 
1815
            # exception if it's in an unfinished write group.  However, it
 
1816
            # normally will *not* actually remove the lock from disk - that's
 
1817
            # done by the server on receiving the Repository.unlock call.
 
1818
            # This is just to let the _real_repository stay up to date.
 
1819
            if self._real_repository is not None:
 
1820
                self._real_repository.unlock()
 
1821
            elif self._write_group_tokens is not None:
 
1822
                self.abort_write_group()
 
1823
        finally:
 
1824
            # The rpc-level lock should be released even if there was a
 
1825
            # problem releasing the vfs-based lock.
 
1826
            if old_mode == 'w':
 
1827
                # Only write-locked repositories need to make a remote method
 
1828
                # call to perform the unlock.
 
1829
                old_token = self._lock_token
 
1830
                self._lock_token = None
 
1831
                if not self._leave_lock:
 
1832
                    self._unlock(old_token)
 
1833
        # Fallbacks are always 'lock_read()' so we don't pay attention to
 
1834
        # self._leave_lock
 
1835
        for repo in self._fallback_repositories:
 
1836
            repo.unlock()
 
1837
 
 
1838
    def break_lock(self):
 
1839
        # should hand off to the network
 
1840
        path = self.controldir._path_for_remote_call(self._client)
 
1841
        try:
 
1842
            response = self._call(b"Repository.break_lock", path)
 
1843
        except errors.UnknownSmartMethod:
 
1844
            self._ensure_real()
 
1845
            return self._real_repository.break_lock()
 
1846
        if response != (b'ok',):
 
1847
            raise errors.UnexpectedSmartServerResponse(response)
 
1848
 
 
1849
    def _get_tarball(self, compression):
 
1850
        """Return a TemporaryFile containing a repository tarball.
 
1851
 
 
1852
        Returns None if the server does not support sending tarballs.
 
1853
        """
 
1854
        import tempfile
 
1855
        path = self.controldir._path_for_remote_call(self._client)
 
1856
        try:
 
1857
            response, protocol = self._call_expecting_body(
 
1858
                b'Repository.tarball', path, compression.encode('ascii'))
 
1859
        except errors.UnknownSmartMethod:
 
1860
            protocol.cancel_read_body()
 
1861
            return None
 
1862
        if response[0] == b'ok':
 
1863
            # Extract the tarball and return it
 
1864
            t = tempfile.NamedTemporaryFile()
 
1865
            # TODO: rpc layer should read directly into it...
 
1866
            t.write(protocol.read_body_bytes())
 
1867
            t.seek(0)
 
1868
            return t
 
1869
        raise errors.UnexpectedSmartServerResponse(response)
 
1870
 
 
1871
    def sprout(self, to_bzrdir, revision_id=None):
 
1872
        """Create a descendent repository for new development.
 
1873
 
 
1874
        Unlike clone, this does not copy the settings of the repository.
 
1875
        """
 
1876
        with self.lock_read():
 
1877
            dest_repo = self._create_sprouting_repo(to_bzrdir, shared=False)
 
1878
            dest_repo.fetch(self, revision_id=revision_id)
 
1879
            return dest_repo
 
1880
 
 
1881
    def _create_sprouting_repo(self, a_controldir, shared):
 
1882
        if not isinstance(a_controldir._format, self.controldir._format.__class__):
 
1883
            # use target default format.
 
1884
            dest_repo = a_controldir.create_repository()
 
1885
        else:
 
1886
            # Most control formats need the repository to be specifically
 
1887
            # created, but on some old all-in-one formats it's not needed
 
1888
            try:
 
1889
                dest_repo = self._format.initialize(
 
1890
                    a_controldir, shared=shared)
 
1891
            except errors.UninitializableFormat:
 
1892
                dest_repo = a_controldir.open_repository()
 
1893
        return dest_repo
 
1894
 
 
1895
    # These methods are just thin shims to the VFS object for now.
 
1896
 
 
1897
    def revision_tree(self, revision_id):
 
1898
        with self.lock_read():
 
1899
            revision_id = _mod_revision.ensure_null(revision_id)
 
1900
            if revision_id == _mod_revision.NULL_REVISION:
 
1901
                return InventoryRevisionTree(self,
 
1902
                                             Inventory(root_id=None), _mod_revision.NULL_REVISION)
 
1903
            else:
 
1904
                return list(self.revision_trees([revision_id]))[0]
 
1905
 
 
1906
    def get_serializer_format(self):
 
1907
        path = self.controldir._path_for_remote_call(self._client)
 
1908
        try:
 
1909
            response = self._call(b'VersionedFileRepository.get_serializer_format',
 
1910
                                  path)
 
1911
        except errors.UnknownSmartMethod:
 
1912
            self._ensure_real()
 
1913
            return self._real_repository.get_serializer_format()
 
1914
        if response[0] != b'ok':
 
1915
            raise errors.UnexpectedSmartServerResponse(response)
 
1916
        return response[1]
 
1917
 
 
1918
    def get_commit_builder(self, branch, parents, config, timestamp=None,
 
1919
                           timezone=None, committer=None, revprops=None,
 
1920
                           revision_id=None, lossy=False):
 
1921
        """Obtain a CommitBuilder for this repository.
 
1922
 
 
1923
        :param branch: Branch to commit to.
 
1924
        :param parents: Revision ids of the parents of the new revision.
 
1925
        :param config: Configuration to use.
 
1926
        :param timestamp: Optional timestamp recorded for commit.
 
1927
        :param timezone: Optional timezone for timestamp.
 
1928
        :param committer: Optional committer to set for commit.
 
1929
        :param revprops: Optional dictionary of revision properties.
 
1930
        :param revision_id: Optional revision id.
 
1931
        :param lossy: Whether to discard data that can not be natively
 
1932
            represented, when pushing to a foreign VCS
 
1933
        """
 
1934
        if self._fallback_repositories and not self._format.supports_chks:
 
1935
            raise errors.BzrError("Cannot commit directly to a stacked branch"
 
1936
                                  " in pre-2a formats. See "
 
1937
                                  "https://bugs.launchpad.net/bzr/+bug/375013 for details.")
 
1938
        commit_builder_kls = vf_repository.VersionedFileCommitBuilder
 
1939
        result = commit_builder_kls(self, parents, config,
 
1940
                                    timestamp, timezone, committer, revprops, revision_id,
 
1941
                                    lossy)
 
1942
        self.start_write_group()
 
1943
        return result
 
1944
 
 
1945
    def add_fallback_repository(self, repository):
 
1946
        """Add a repository to use for looking up data not held locally.
 
1947
 
 
1948
        :param repository: A repository.
 
1949
        """
 
1950
        if not self._format.supports_external_lookups:
 
1951
            raise errors.UnstackableRepositoryFormat(
 
1952
                self._format.network_name(), self.base)
 
1953
        # We need to accumulate additional repositories here, to pass them in
 
1954
        # on various RPC's.
 
1955
        #
 
1956
        # Make the check before we lock: this raises an exception.
 
1957
        self._check_fallback_repository(repository)
 
1958
        if self.is_locked():
 
1959
            # We will call fallback.unlock() when we transition to the unlocked
 
1960
            # state, so always add a lock here. If a caller passes us a locked
 
1961
            # repository, they are responsible for unlocking it later.
 
1962
            repository.lock_read()
 
1963
        self._fallback_repositories.append(repository)
 
1964
        # If self._real_repository was parameterised already (e.g. because a
 
1965
        # _real_branch had its get_stacked_on_url method called), then the
 
1966
        # repository to be added may already be in the _real_repositories list.
 
1967
        if self._real_repository is not None:
 
1968
            fallback_locations = [repo.user_url for repo in
 
1969
                                  self._real_repository._fallback_repositories]
 
1970
            if repository.user_url not in fallback_locations:
 
1971
                self._real_repository.add_fallback_repository(repository)
 
1972
 
 
1973
    def _check_fallback_repository(self, repository):
 
1974
        """Check that this repository can fallback to repository safely.
 
1975
 
 
1976
        Raise an error if not.
 
1977
 
 
1978
        :param repository: A repository to fallback to.
 
1979
        """
 
1980
        return _mod_repository.InterRepository._assert_same_model(
 
1981
            self, repository)
 
1982
 
 
1983
    def add_inventory(self, revid, inv, parents):
 
1984
        self._ensure_real()
 
1985
        return self._real_repository.add_inventory(revid, inv, parents)
 
1986
 
 
1987
    def add_inventory_by_delta(self, basis_revision_id, delta, new_revision_id,
 
1988
                               parents, basis_inv=None, propagate_caches=False):
 
1989
        self._ensure_real()
 
1990
        return self._real_repository.add_inventory_by_delta(basis_revision_id,
 
1991
                                                            delta, new_revision_id, parents, basis_inv=basis_inv,
 
1992
                                                            propagate_caches=propagate_caches)
 
1993
 
 
1994
    def add_revision(self, revision_id, rev, inv=None):
 
1995
        _mod_revision.check_not_reserved_id(revision_id)
 
1996
        key = (revision_id,)
 
1997
        # check inventory present
 
1998
        if not self.inventories.get_parent_map([key]):
 
1999
            if inv is None:
 
2000
                raise errors.WeaveRevisionNotPresent(revision_id,
 
2001
                                                     self.inventories)
 
2002
            else:
 
2003
                # yes, this is not suitable for adding with ghosts.
 
2004
                rev.inventory_sha1 = self.add_inventory(revision_id, inv,
 
2005
                                                        rev.parent_ids)
 
2006
        else:
 
2007
            rev.inventory_sha1 = self.inventories.get_sha1s([key])[key]
 
2008
        self._add_revision(rev)
 
2009
 
 
2010
    def _add_revision(self, rev):
 
2011
        if self._real_repository is not None:
 
2012
            return self._real_repository._add_revision(rev)
 
2013
        lines = self._serializer.write_revision_to_lines(rev)
 
2014
        key = (rev.revision_id,)
 
2015
        parents = tuple((parent,) for parent in rev.parent_ids)
 
2016
        self._write_group_tokens, missing_keys = self._get_sink().insert_stream(
 
2017
            [('revisions', [ChunkedContentFactory(key, parents, None, lines, chunks_are_lines=True)])],
 
2018
            self._format, self._write_group_tokens)
 
2019
 
 
2020
    def get_inventory(self, revision_id):
 
2021
        with self.lock_read():
 
2022
            return list(self.iter_inventories([revision_id]))[0]
 
2023
 
 
2024
    def _iter_inventories_rpc(self, revision_ids, ordering):
 
2025
        if ordering is None:
 
2026
            ordering = 'unordered'
 
2027
        path = self.controldir._path_for_remote_call(self._client)
 
2028
        body = b"\n".join(revision_ids)
 
2029
        response_tuple, response_handler = (
 
2030
            self._call_with_body_bytes_expecting_body(
 
2031
                b"VersionedFileRepository.get_inventories",
 
2032
                (path, ordering.encode('ascii')), body))
 
2033
        if response_tuple[0] != b"ok":
 
2034
            raise errors.UnexpectedSmartServerResponse(response_tuple)
 
2035
        deserializer = inventory_delta.InventoryDeltaDeserializer()
 
2036
        byte_stream = response_handler.read_streamed_body()
 
2037
        decoded = smart_repo._byte_stream_to_stream(byte_stream)
 
2038
        if decoded is None:
 
2039
            # no results whatsoever
 
2040
            return
 
2041
        src_format, stream = decoded
 
2042
        if src_format.network_name() != self._format.network_name():
 
2043
            raise AssertionError(
 
2044
                "Mismatched RemoteRepository and stream src %r, %r" % (
 
2045
                    src_format.network_name(), self._format.network_name()))
 
2046
        # ignore the src format, it's not really relevant
 
2047
        prev_inv = Inventory(root_id=None,
 
2048
                             revision_id=_mod_revision.NULL_REVISION)
 
2049
        # there should be just one substream, with inventory deltas
 
2050
        try:
 
2051
            substream_kind, substream = next(stream)
 
2052
        except StopIteration:
 
2053
            return
 
2054
        if substream_kind != "inventory-deltas":
 
2055
            raise AssertionError(
 
2056
                "Unexpected stream %r received" % substream_kind)
 
2057
        for record in substream:
 
2058
            (parent_id, new_id, versioned_root, tree_references, invdelta) = (
 
2059
                deserializer.parse_text_bytes(record.get_bytes_as("lines")))
 
2060
            if parent_id != prev_inv.revision_id:
 
2061
                raise AssertionError("invalid base %r != %r" % (parent_id,
 
2062
                                                                prev_inv.revision_id))
 
2063
            inv = prev_inv.create_by_apply_delta(invdelta, new_id)
 
2064
            yield inv, inv.revision_id
 
2065
            prev_inv = inv
 
2066
 
 
2067
    def _iter_inventories_vfs(self, revision_ids, ordering=None):
 
2068
        self._ensure_real()
 
2069
        return self._real_repository._iter_inventories(revision_ids, ordering)
 
2070
 
 
2071
    def iter_inventories(self, revision_ids, ordering=None):
 
2072
        """Get many inventories by revision_ids.
 
2073
 
 
2074
        This will buffer some or all of the texts used in constructing the
 
2075
        inventories in memory, but will only parse a single inventory at a
 
2076
        time.
 
2077
 
 
2078
        :param revision_ids: The expected revision ids of the inventories.
 
2079
        :param ordering: optional ordering, e.g. 'topological'.  If not
 
2080
            specified, the order of revision_ids will be preserved (by
 
2081
            buffering if necessary).
 
2082
        :return: An iterator of inventories.
 
2083
        """
 
2084
        if ((None in revision_ids) or
 
2085
                (_mod_revision.NULL_REVISION in revision_ids)):
 
2086
            raise ValueError('cannot get null revision inventory')
 
2087
        for inv, revid in self._iter_inventories(revision_ids, ordering):
 
2088
            if inv is None:
 
2089
                raise errors.NoSuchRevision(self, revid)
 
2090
            yield inv
 
2091
 
 
2092
    def _iter_inventories(self, revision_ids, ordering=None):
 
2093
        if len(revision_ids) == 0:
 
2094
            return
 
2095
        missing = set(revision_ids)
 
2096
        if ordering is None:
 
2097
            order_as_requested = True
 
2098
            invs = {}
 
2099
            order = list(revision_ids)
 
2100
            order.reverse()
 
2101
            next_revid = order.pop()
 
2102
        else:
 
2103
            order_as_requested = False
 
2104
            if ordering != 'unordered' and self._fallback_repositories:
 
2105
                raise ValueError('unsupported ordering %r' % ordering)
 
2106
        iter_inv_fns = [self._iter_inventories_rpc] + [
 
2107
            fallback._iter_inventories for fallback in
 
2108
            self._fallback_repositories]
 
2109
        try:
 
2110
            for iter_inv in iter_inv_fns:
 
2111
                request = [revid for revid in revision_ids if revid in missing]
 
2112
                for inv, revid in iter_inv(request, ordering):
 
2113
                    if inv is None:
 
2114
                        continue
 
2115
                    missing.remove(inv.revision_id)
 
2116
                    if ordering != 'unordered':
 
2117
                        invs[revid] = inv
 
2118
                    else:
 
2119
                        yield inv, revid
 
2120
                if order_as_requested:
 
2121
                    # Yield as many results as we can while preserving order.
 
2122
                    while next_revid in invs:
 
2123
                        inv = invs.pop(next_revid)
 
2124
                        yield inv, inv.revision_id
 
2125
                        try:
 
2126
                            next_revid = order.pop()
 
2127
                        except IndexError:
 
2128
                            # We still want to fully consume the stream, just
 
2129
                            # in case it is not actually finished at this point
 
2130
                            next_revid = None
 
2131
                            break
 
2132
        except errors.UnknownSmartMethod:
 
2133
            for inv, revid in self._iter_inventories_vfs(revision_ids, ordering):
 
2134
                yield inv, revid
 
2135
            return
 
2136
        # Report missing
 
2137
        if order_as_requested:
 
2138
            if next_revid is not None:
 
2139
                yield None, next_revid
 
2140
            while order:
 
2141
                revid = order.pop()
 
2142
                yield invs.get(revid), revid
 
2143
        else:
 
2144
            while missing:
 
2145
                yield None, missing.pop()
 
2146
 
 
2147
    def get_revision(self, revision_id):
 
2148
        with self.lock_read():
 
2149
            return self.get_revisions([revision_id])[0]
 
2150
 
 
2151
    def get_transaction(self):
 
2152
        self._ensure_real()
 
2153
        return self._real_repository.get_transaction()
 
2154
 
 
2155
    def clone(self, a_controldir, revision_id=None):
 
2156
        with self.lock_read():
 
2157
            dest_repo = self._create_sprouting_repo(
 
2158
                a_controldir, shared=self.is_shared())
 
2159
            self.copy_content_into(dest_repo, revision_id)
 
2160
            return dest_repo
 
2161
 
 
2162
    def make_working_trees(self):
 
2163
        """See Repository.make_working_trees"""
 
2164
        path = self.controldir._path_for_remote_call(self._client)
 
2165
        try:
 
2166
            response = self._call(b'Repository.make_working_trees', path)
 
2167
        except errors.UnknownSmartMethod:
 
2168
            self._ensure_real()
 
2169
            return self._real_repository.make_working_trees()
 
2170
        if response[0] not in (b'yes', b'no'):
 
2171
            raise SmartProtocolError(
 
2172
                'unexpected response code %s' % (response,))
 
2173
        return response[0] == b'yes'
 
2174
 
 
2175
    def refresh_data(self):
 
2176
        """Re-read any data needed to synchronise with disk.
 
2177
 
 
2178
        This method is intended to be called after another repository instance
 
2179
        (such as one used by a smart server) has inserted data into the
 
2180
        repository. On all repositories this will work outside of write groups.
 
2181
        Some repository formats (pack and newer for breezy native formats)
 
2182
        support refresh_data inside write groups. If called inside a write
 
2183
        group on a repository that does not support refreshing in a write group
 
2184
        IsInWriteGroupError will be raised.
 
2185
        """
 
2186
        if self._real_repository is not None:
 
2187
            self._real_repository.refresh_data()
 
2188
        # Refresh the parents cache for this object
 
2189
        self._unstacked_provider.disable_cache()
 
2190
        self._unstacked_provider.enable_cache()
 
2191
 
 
2192
    def revision_ids_to_search_result(self, result_set):
 
2193
        """Convert a set of revision ids to a graph SearchResult."""
 
2194
        result_parents = set()
 
2195
        for parents in self.get_graph().get_parent_map(result_set).values():
 
2196
            result_parents.update(parents)
 
2197
        included_keys = result_set.intersection(result_parents)
 
2198
        start_keys = result_set.difference(included_keys)
 
2199
        exclude_keys = result_parents.difference(result_set)
 
2200
        result = vf_search.SearchResult(start_keys, exclude_keys,
 
2201
                                        len(result_set), result_set)
 
2202
        return result
 
2203
 
 
2204
    def search_missing_revision_ids(self, other,
 
2205
                                    find_ghosts=True, revision_ids=None, if_present_ids=None,
 
2206
                                    limit=None):
 
2207
        """Return the revision ids that other has that this does not.
 
2208
 
 
2209
        These are returned in topological order.
 
2210
 
 
2211
        revision_id: only return revision ids included by revision_id.
 
2212
        """
 
2213
        with self.lock_read():
 
2214
            inter_repo = _mod_repository.InterRepository.get(other, self)
 
2215
            return inter_repo.search_missing_revision_ids(
 
2216
                find_ghosts=find_ghosts, revision_ids=revision_ids,
 
2217
                if_present_ids=if_present_ids, limit=limit)
 
2218
 
 
2219
    def fetch(self, source, revision_id=None, find_ghosts=False,
 
2220
              fetch_spec=None, lossy=False):
 
2221
        # No base implementation to use as RemoteRepository is not a subclass
 
2222
        # of Repository; so this is a copy of Repository.fetch().
 
2223
        if fetch_spec is not None and revision_id is not None:
 
2224
            raise AssertionError(
 
2225
                "fetch_spec and revision_id are mutually exclusive.")
 
2226
        if self.is_in_write_group():
 
2227
            raise errors.InternalBzrError(
 
2228
                "May not fetch while in a write group.")
 
2229
        # fast path same-url fetch operations
 
2230
        if (self.has_same_location(source) and
 
2231
            fetch_spec is None and
 
2232
                self._has_same_fallbacks(source)):
 
2233
            # check that last_revision is in 'from' and then return a
 
2234
            # no-operation.
 
2235
            if (revision_id is not None
 
2236
                    and not _mod_revision.is_null(revision_id)):
 
2237
                self.get_revision(revision_id)
 
2238
            return _mod_repository.FetchResult(0)
 
2239
        # if there is no specific appropriate InterRepository, this will get
 
2240
        # the InterRepository base class, which raises an
 
2241
        # IncompatibleRepositories when asked to fetch.
 
2242
        inter = _mod_repository.InterRepository.get(source, self)
 
2243
        if (fetch_spec is not None
 
2244
                and not getattr(inter, "supports_fetch_spec", False)):
 
2245
            raise errors.UnsupportedOperation(
 
2246
                "fetch_spec not supported for %r" % inter)
 
2247
        return inter.fetch(revision_id=revision_id,
 
2248
                           find_ghosts=find_ghosts, fetch_spec=fetch_spec,
 
2249
                           lossy=lossy)
 
2250
 
 
2251
    def create_bundle(self, target, base, fileobj, format=None):
 
2252
        self._ensure_real()
 
2253
        self._real_repository.create_bundle(target, base, fileobj, format)
 
2254
 
 
2255
    def fileids_altered_by_revision_ids(self, revision_ids):
 
2256
        self._ensure_real()
 
2257
        return self._real_repository.fileids_altered_by_revision_ids(revision_ids)
 
2258
 
 
2259
    def _get_versioned_file_checker(self, revisions, revision_versions_cache):
 
2260
        self._ensure_real()
 
2261
        return self._real_repository._get_versioned_file_checker(
 
2262
            revisions, revision_versions_cache)
 
2263
 
 
2264
    def _iter_files_bytes_rpc(self, desired_files, absent):
 
2265
        path = self.controldir._path_for_remote_call(self._client)
 
2266
        lines = []
 
2267
        identifiers = []
 
2268
        for (file_id, revid, identifier) in desired_files:
 
2269
            lines.append(b''.join([
 
2270
                file_id,
 
2271
                b'\0',
 
2272
                revid]))
 
2273
            identifiers.append(identifier)
 
2274
        (response_tuple, response_handler) = (
 
2275
            self._call_with_body_bytes_expecting_body(
 
2276
                b"Repository.iter_files_bytes", (path, ), b"\n".join(lines)))
 
2277
        if response_tuple != (b'ok', ):
 
2278
            response_handler.cancel_read_body()
 
2279
            raise errors.UnexpectedSmartServerResponse(response_tuple)
 
2280
        byte_stream = response_handler.read_streamed_body()
 
2281
 
 
2282
        def decompress_stream(start, byte_stream, unused):
 
2283
            decompressor = zlib.decompressobj()
 
2284
            yield decompressor.decompress(start)
 
2285
            while decompressor.unused_data == b"":
 
2286
                try:
 
2287
                    data = next(byte_stream)
 
2288
                except StopIteration:
 
2289
                    break
 
2290
                yield decompressor.decompress(data)
 
2291
            yield decompressor.flush()
 
2292
            unused.append(decompressor.unused_data)
 
2293
        unused = b""
 
2294
        while True:
 
2295
            while b"\n" not in unused:
 
2296
                try:
 
2297
                    unused += next(byte_stream)
 
2298
                except StopIteration:
 
2299
                    return
 
2300
            header, rest = unused.split(b"\n", 1)
 
2301
            args = header.split(b"\0")
 
2302
            if args[0] == b"absent":
 
2303
                absent[identifiers[int(args[3])]] = (args[1], args[2])
 
2304
                unused = rest
 
2305
                continue
 
2306
            elif args[0] == b"ok":
 
2307
                idx = int(args[1])
 
2308
            else:
 
2309
                raise errors.UnexpectedSmartServerResponse(args)
 
2310
            unused_chunks = []
 
2311
            yield (identifiers[idx],
 
2312
                   decompress_stream(rest, byte_stream, unused_chunks))
 
2313
            unused = b"".join(unused_chunks)
 
2314
 
 
2315
    def iter_files_bytes(self, desired_files):
 
2316
        """See Repository.iter_file_bytes.
 
2317
        """
 
2318
        try:
 
2319
            absent = {}
 
2320
            for (identifier, bytes_iterator) in self._iter_files_bytes_rpc(
 
2321
                    desired_files, absent):
 
2322
                yield identifier, bytes_iterator
 
2323
            for fallback in self._fallback_repositories:
 
2324
                if not absent:
 
2325
                    break
 
2326
                desired_files = [(key[0], key[1], identifier)
 
2327
                                 for identifier, key in absent.items()]
 
2328
                for (identifier, bytes_iterator) in fallback.iter_files_bytes(desired_files):
 
2329
                    del absent[identifier]
 
2330
                    yield identifier, bytes_iterator
 
2331
            if absent:
 
2332
                # There may be more missing items, but raise an exception
 
2333
                # for just one.
 
2334
                missing_identifier = next(iter(absent))
 
2335
                missing_key = absent[missing_identifier]
 
2336
                raise errors.RevisionNotPresent(revision_id=missing_key[1],
 
2337
                                                file_id=missing_key[0])
 
2338
        except errors.UnknownSmartMethod:
 
2339
            self._ensure_real()
 
2340
            for (identifier, bytes_iterator) in (
 
2341
                    self._real_repository.iter_files_bytes(desired_files)):
 
2342
                yield identifier, bytes_iterator
 
2343
 
 
2344
    def get_cached_parent_map(self, revision_ids):
 
2345
        """See breezy.CachingParentsProvider.get_cached_parent_map"""
 
2346
        return self._unstacked_provider.get_cached_parent_map(revision_ids)
 
2347
 
 
2348
    def get_parent_map(self, revision_ids):
 
2349
        """See breezy.Graph.get_parent_map()."""
 
2350
        return self._make_parents_provider().get_parent_map(revision_ids)
 
2351
 
 
2352
    def _get_parent_map_rpc(self, keys):
 
2353
        """Helper for get_parent_map that performs the RPC."""
 
2354
        medium = self._client._medium
 
2355
        if medium._is_remote_before((1, 2)):
 
2356
            # We already found out that the server can't understand
 
2357
            # Repository.get_parent_map requests, so just fetch the whole
 
2358
            # graph.
 
2359
            #
 
2360
            # Note that this reads the whole graph, when only some keys are
 
2361
            # wanted.  On this old server there's no way (?) to get them all
 
2362
            # in one go, and the user probably will have seen a warning about
 
2363
            # the server being old anyhow.
 
2364
            rg = self._get_revision_graph(None)
 
2365
            # There is an API discrepancy between get_parent_map and
 
2366
            # get_revision_graph. Specifically, a "key:()" pair in
 
2367
            # get_revision_graph just means a node has no parents. For
 
2368
            # "get_parent_map" it means the node is a ghost. So fix up the
 
2369
            # graph to correct this.
 
2370
            #   https://bugs.launchpad.net/bzr/+bug/214894
 
2371
            # There is one other "bug" which is that ghosts in
 
2372
            # get_revision_graph() are not returned at all. But we won't worry
 
2373
            # about that for now.
 
2374
            for node_id, parent_ids in rg.items():
 
2375
                if parent_ids == ():
 
2376
                    rg[node_id] = (NULL_REVISION,)
 
2377
            rg[NULL_REVISION] = ()
 
2378
            return rg
 
2379
 
 
2380
        keys = set(keys)
 
2381
        if None in keys:
 
2382
            raise ValueError('get_parent_map(None) is not valid')
 
2383
        if NULL_REVISION in keys:
 
2384
            keys.discard(NULL_REVISION)
 
2385
            found_parents = {NULL_REVISION: ()}
 
2386
            if not keys:
 
2387
                return found_parents
 
2388
        else:
 
2389
            found_parents = {}
 
2390
        # TODO(Needs analysis): We could assume that the keys being requested
 
2391
        # from get_parent_map are in a breadth first search, so typically they
 
2392
        # will all be depth N from some common parent, and we don't have to
 
2393
        # have the server iterate from the root parent, but rather from the
 
2394
        # keys we're searching; and just tell the server the keyspace we
 
2395
        # already have; but this may be more traffic again.
 
2396
 
 
2397
        # Transform self._parents_map into a search request recipe.
 
2398
        # TODO: Manage this incrementally to avoid covering the same path
 
2399
        # repeatedly. (The server will have to on each request, but the less
 
2400
        # work done the better).
 
2401
        #
 
2402
        # Negative caching notes:
 
2403
        # new server sends missing when a request including the revid
 
2404
        # 'include-missing:' is present in the request.
 
2405
        # missing keys are serialised as missing:X, and we then call
 
2406
        # provider.note_missing(X) for-all X
 
2407
        parents_map = self._unstacked_provider.get_cached_map()
 
2408
        if parents_map is None:
 
2409
            # Repository is not locked, so there's no cache.
 
2410
            parents_map = {}
 
2411
        if _DEFAULT_SEARCH_DEPTH <= 0:
 
2412
            (start_set, stop_keys,
 
2413
             key_count) = vf_search.search_result_from_parent_map(
 
2414
                parents_map, self._unstacked_provider.missing_keys)
 
2415
        else:
 
2416
            (start_set, stop_keys,
 
2417
             key_count) = vf_search.limited_search_result_from_parent_map(
 
2418
                parents_map, self._unstacked_provider.missing_keys,
 
2419
                keys, depth=_DEFAULT_SEARCH_DEPTH)
 
2420
        recipe = ('manual', start_set, stop_keys, key_count)
 
2421
        body = self._serialise_search_recipe(recipe)
 
2422
        path = self.controldir._path_for_remote_call(self._client)
 
2423
        for key in keys:
 
2424
            if not isinstance(key, bytes):
 
2425
                raise ValueError(
 
2426
                    "key %r not a bytes string" % (key,))
 
2427
        verb = b'Repository.get_parent_map'
 
2428
        args = (path, b'include-missing:') + tuple(keys)
 
2429
        try:
 
2430
            response = self._call_with_body_bytes_expecting_body(
 
2431
                verb, args, body)
 
2432
        except errors.UnknownSmartMethod:
 
2433
            # Server does not support this method, so get the whole graph.
 
2434
            # Worse, we have to force a disconnection, because the server now
 
2435
            # doesn't realise it has a body on the wire to consume, so the
 
2436
            # only way to recover is to abandon the connection.
 
2437
            warning(
 
2438
                'Server is too old for fast get_parent_map, reconnecting.  '
 
2439
                '(Upgrade the server to Bazaar 1.2 to avoid this)')
 
2440
            medium.disconnect()
 
2441
            # To avoid having to disconnect repeatedly, we keep track of the
 
2442
            # fact the server doesn't understand remote methods added in 1.2.
 
2443
            medium._remember_remote_is_before((1, 2))
 
2444
            # Recurse just once and we should use the fallback code.
 
2445
            return self._get_parent_map_rpc(keys)
 
2446
        response_tuple, response_handler = response
 
2447
        if response_tuple[0] not in [b'ok']:
 
2448
            response_handler.cancel_read_body()
 
2449
            raise errors.UnexpectedSmartServerResponse(response_tuple)
 
2450
        if response_tuple[0] == b'ok':
 
2451
            coded = bz2.decompress(response_handler.read_body_bytes())
 
2452
            if coded == b'':
 
2453
                # no revisions found
 
2454
                return {}
 
2455
            lines = coded.split(b'\n')
 
2456
            revision_graph = {}
 
2457
            for line in lines:
 
2458
                d = tuple(line.split())
 
2459
                if len(d) > 1:
 
2460
                    revision_graph[d[0]] = d[1:]
 
2461
                else:
 
2462
                    # No parents:
 
2463
                    if d[0].startswith(b'missing:'):
 
2464
                        revid = d[0][8:]
 
2465
                        self._unstacked_provider.note_missing_key(revid)
 
2466
                    else:
 
2467
                        # no parents - so give the Graph result
 
2468
                        # (NULL_REVISION,).
 
2469
                        revision_graph[d[0]] = (NULL_REVISION,)
 
2470
            return revision_graph
 
2471
 
 
2472
    def get_signature_text(self, revision_id):
 
2473
        with self.lock_read():
 
2474
            path = self.controldir._path_for_remote_call(self._client)
 
2475
            try:
 
2476
                response_tuple, response_handler = self._call_expecting_body(
 
2477
                    b'Repository.get_revision_signature_text', path, revision_id)
 
2478
            except errors.UnknownSmartMethod:
 
2479
                self._ensure_real()
 
2480
                return self._real_repository.get_signature_text(revision_id)
 
2481
            except errors.NoSuchRevision as err:
 
2482
                for fallback in self._fallback_repositories:
 
2483
                    try:
 
2484
                        return fallback.get_signature_text(revision_id)
 
2485
                    except errors.NoSuchRevision:
 
2486
                        pass
 
2487
                raise err
 
2488
            else:
 
2489
                if response_tuple[0] != b'ok':
 
2490
                    raise errors.UnexpectedSmartServerResponse(response_tuple)
 
2491
                return response_handler.read_body_bytes()
 
2492
 
 
2493
    def _get_inventory_xml(self, revision_id):
 
2494
        with self.lock_read():
 
2495
            # This call is used by older working tree formats,
 
2496
            # which stored a serialized basis inventory.
 
2497
            self._ensure_real()
 
2498
            return self._real_repository._get_inventory_xml(revision_id)
 
2499
 
 
2500
    def reconcile(self, other=None, thorough=False):
 
2501
        from ..reconcile import ReconcileResult
 
2502
        with self.lock_write():
 
2503
            path = self.controldir._path_for_remote_call(self._client)
 
2504
            try:
 
2505
                response, handler = self._call_expecting_body(
 
2506
                    b'Repository.reconcile', path, self._lock_token)
 
2507
            except (errors.UnknownSmartMethod, errors.TokenLockingNotSupported):
 
2508
                self._ensure_real()
 
2509
                return self._real_repository.reconcile(other=other, thorough=thorough)
 
2510
            if response != (b'ok', ):
 
2511
                raise errors.UnexpectedSmartServerResponse(response)
 
2512
            body = handler.read_body_bytes()
 
2513
            result = ReconcileResult()
 
2514
            result.garbage_inventories = None
 
2515
            result.inconsistent_parents = None
 
2516
            result.aborted = None
 
2517
            for line in body.split(b'\n'):
 
2518
                if not line:
 
2519
                    continue
 
2520
                key, val_text = line.split(b':')
 
2521
                if key == b"garbage_inventories":
 
2522
                    result.garbage_inventories = int(val_text)
 
2523
                elif key == b"inconsistent_parents":
 
2524
                    result.inconsistent_parents = int(val_text)
 
2525
                else:
 
2526
                    mutter("unknown reconcile key %r" % key)
 
2527
            return result
 
2528
 
 
2529
    def all_revision_ids(self):
 
2530
        path = self.controldir._path_for_remote_call(self._client)
 
2531
        try:
 
2532
            response_tuple, response_handler = self._call_expecting_body(
 
2533
                b"Repository.all_revision_ids", path)
 
2534
        except errors.UnknownSmartMethod:
 
2535
            self._ensure_real()
 
2536
            return self._real_repository.all_revision_ids()
 
2537
        if response_tuple != (b"ok", ):
 
2538
            raise errors.UnexpectedSmartServerResponse(response_tuple)
 
2539
        revids = set(response_handler.read_body_bytes().splitlines())
 
2540
        for fallback in self._fallback_repositories:
 
2541
            revids.update(set(fallback.all_revision_ids()))
 
2542
        return list(revids)
 
2543
 
 
2544
    def _filtered_revision_trees(self, revision_ids, file_ids):
 
2545
        """Return Tree for a revision on this branch with only some files.
 
2546
 
 
2547
        :param revision_ids: a sequence of revision-ids;
 
2548
          a revision-id may not be None or b'null:'
 
2549
        :param file_ids: if not None, the result is filtered
 
2550
          so that only those file-ids, their parents and their
 
2551
          children are included.
 
2552
        """
 
2553
        inventories = self.iter_inventories(revision_ids)
 
2554
        for inv in inventories:
 
2555
            # Should we introduce a FilteredRevisionTree class rather
 
2556
            # than pre-filter the inventory here?
 
2557
            filtered_inv = inv.filter(file_ids)
 
2558
            yield InventoryRevisionTree(self, filtered_inv, filtered_inv.revision_id)
 
2559
 
 
2560
    def get_revision_delta(self, revision_id):
 
2561
        with self.lock_read():
 
2562
            r = self.get_revision(revision_id)
 
2563
            return list(self.get_revision_deltas([r]))[0]
 
2564
 
 
2565
    def revision_trees(self, revision_ids):
 
2566
        with self.lock_read():
 
2567
            inventories = self.iter_inventories(revision_ids)
 
2568
            for inv in inventories:
 
2569
                yield RemoteInventoryTree(self, inv, inv.revision_id)
 
2570
 
 
2571
    def get_revision_reconcile(self, revision_id):
 
2572
        with self.lock_read():
 
2573
            self._ensure_real()
 
2574
            return self._real_repository.get_revision_reconcile(revision_id)
 
2575
 
 
2576
    def check(self, revision_ids=None, callback_refs=None, check_repo=True):
 
2577
        with self.lock_read():
 
2578
            self._ensure_real()
 
2579
            return self._real_repository.check(revision_ids=revision_ids,
 
2580
                                               callback_refs=callback_refs, check_repo=check_repo)
 
2581
 
 
2582
    def copy_content_into(self, destination, revision_id=None):
 
2583
        """Make a complete copy of the content in self into destination.
 
2584
 
 
2585
        This is a destructive operation! Do not use it on existing
 
2586
        repositories.
 
2587
        """
 
2588
        interrepo = _mod_repository.InterRepository.get(self, destination)
 
2589
        return interrepo.copy_content(revision_id)
 
2590
 
 
2591
    def _copy_repository_tarball(self, to_bzrdir, revision_id=None):
 
2592
        # get a tarball of the remote repository, and copy from that into the
 
2593
        # destination
 
2594
        import tarfile
 
2595
        # TODO: Maybe a progress bar while streaming the tarball?
 
2596
        note(gettext("Copying repository content as tarball..."))
 
2597
        tar_file = self._get_tarball('bz2')
 
2598
        if tar_file is None:
 
2599
            return None
 
2600
        destination = to_bzrdir.create_repository()
 
2601
        try:
 
2602
            tar = tarfile.open('repository', fileobj=tar_file,
 
2603
                               mode='r|bz2')
 
2604
            tmpdir = osutils.mkdtemp()
 
2605
            try:
 
2606
                tar.extractall(tmpdir)
 
2607
                tmp_bzrdir = _mod_bzrdir.BzrDir.open(tmpdir)
 
2608
                tmp_repo = tmp_bzrdir.open_repository()
 
2609
                tmp_repo.copy_content_into(destination, revision_id)
 
2610
            finally:
 
2611
                osutils.rmtree(tmpdir)
 
2612
        finally:
 
2613
            tar_file.close()
 
2614
        return destination
 
2615
        # TODO: Suggestion from john: using external tar is much faster than
 
2616
        # python's tarfile library, but it may not work on windows.
 
2617
 
 
2618
    @property
 
2619
    def inventories(self):
 
2620
        """Decorate the real repository for now.
 
2621
 
 
2622
        In the long term a full blown network facility is needed to
 
2623
        avoid creating a real repository object locally.
 
2624
        """
 
2625
        self._ensure_real()
 
2626
        return self._real_repository.inventories
 
2627
 
 
2628
    def pack(self, hint=None, clean_obsolete_packs=False):
 
2629
        """Compress the data within the repository.
 
2630
        """
 
2631
        if hint is None:
 
2632
            body = b""
 
2633
        else:
 
2634
            body = b"".join([l.encode('ascii') + b"\n" for l in hint])
 
2635
        with self.lock_write():
 
2636
            path = self.controldir._path_for_remote_call(self._client)
 
2637
            try:
 
2638
                response, handler = self._call_with_body_bytes_expecting_body(
 
2639
                    b'Repository.pack', (path, self._lock_token,
 
2640
                                         str(clean_obsolete_packs).encode('ascii')), body)
 
2641
            except errors.UnknownSmartMethod:
 
2642
                self._ensure_real()
 
2643
                return self._real_repository.pack(hint=hint,
 
2644
                                                  clean_obsolete_packs=clean_obsolete_packs)
 
2645
            handler.cancel_read_body()
 
2646
            if response != (b'ok', ):
 
2647
                raise errors.UnexpectedSmartServerResponse(response)
 
2648
 
 
2649
    @property
 
2650
    def revisions(self):
 
2651
        """Decorate the real repository for now.
 
2652
 
 
2653
        In the long term a full blown network facility is needed.
 
2654
        """
 
2655
        self._ensure_real()
 
2656
        return self._real_repository.revisions
 
2657
 
 
2658
    def set_make_working_trees(self, new_value):
 
2659
        if new_value:
 
2660
            new_value_str = b"True"
 
2661
        else:
 
2662
            new_value_str = b"False"
 
2663
        path = self.controldir._path_for_remote_call(self._client)
 
2664
        try:
 
2665
            response = self._call(
 
2666
                b'Repository.set_make_working_trees', path, new_value_str)
 
2667
        except errors.UnknownSmartMethod:
 
2668
            self._ensure_real()
 
2669
            self._real_repository.set_make_working_trees(new_value)
 
2670
        else:
 
2671
            if response[0] != b'ok':
 
2672
                raise errors.UnexpectedSmartServerResponse(response)
 
2673
 
 
2674
    @property
 
2675
    def signatures(self):
 
2676
        """Decorate the real repository for now.
 
2677
 
 
2678
        In the long term a full blown network facility is needed to avoid
 
2679
        creating a real repository object locally.
 
2680
        """
 
2681
        self._ensure_real()
 
2682
        return self._real_repository.signatures
 
2683
 
 
2684
    def sign_revision(self, revision_id, gpg_strategy):
 
2685
        with self.lock_write():
 
2686
            testament = _mod_testament.Testament.from_revision(
 
2687
                self, revision_id)
 
2688
            plaintext = testament.as_short_text()
 
2689
            self.store_revision_signature(gpg_strategy, plaintext, revision_id)
 
2690
 
 
2691
    @property
 
2692
    def texts(self):
 
2693
        """Decorate the real repository for now.
 
2694
 
 
2695
        In the long term a full blown network facility is needed to avoid
 
2696
        creating a real repository object locally.
 
2697
        """
 
2698
        self._ensure_real()
 
2699
        return self._real_repository.texts
 
2700
 
 
2701
    def _iter_revisions_rpc(self, revision_ids):
 
2702
        body = b"\n".join(revision_ids)
 
2703
        path = self.controldir._path_for_remote_call(self._client)
 
2704
        response_tuple, response_handler = (
 
2705
            self._call_with_body_bytes_expecting_body(
 
2706
                b"Repository.iter_revisions", (path, ), body))
 
2707
        if response_tuple[0] != b"ok":
 
2708
            raise errors.UnexpectedSmartServerResponse(response_tuple)
 
2709
        serializer_format = response_tuple[1].decode('ascii')
 
2710
        serializer = serializer_format_registry.get(serializer_format)
 
2711
        byte_stream = response_handler.read_streamed_body()
 
2712
        decompressor = zlib.decompressobj()
 
2713
        chunks = []
 
2714
        for bytes in byte_stream:
 
2715
            chunks.append(decompressor.decompress(bytes))
 
2716
            if decompressor.unused_data != b"":
 
2717
                chunks.append(decompressor.flush())
 
2718
                yield serializer.read_revision_from_string(b"".join(chunks))
 
2719
                unused = decompressor.unused_data
 
2720
                decompressor = zlib.decompressobj()
 
2721
                chunks = [decompressor.decompress(unused)]
 
2722
        chunks.append(decompressor.flush())
 
2723
        text = b"".join(chunks)
 
2724
        if text != b"":
 
2725
            yield serializer.read_revision_from_string(b"".join(chunks))
 
2726
 
 
2727
    def iter_revisions(self, revision_ids):
 
2728
        for rev_id in revision_ids:
 
2729
            if not rev_id or not isinstance(rev_id, bytes):
 
2730
                raise errors.InvalidRevisionId(
 
2731
                    revision_id=rev_id, branch=self)
 
2732
        with self.lock_read():
 
2733
            try:
 
2734
                missing = set(revision_ids)
 
2735
                for rev in self._iter_revisions_rpc(revision_ids):
 
2736
                    missing.remove(rev.revision_id)
 
2737
                    yield (rev.revision_id, rev)
 
2738
                for fallback in self._fallback_repositories:
 
2739
                    if not missing:
 
2740
                        break
 
2741
                    for (revid, rev) in fallback.iter_revisions(missing):
 
2742
                        if rev is not None:
 
2743
                            yield (revid, rev)
 
2744
                            missing.remove(revid)
 
2745
                for revid in missing:
 
2746
                    yield (revid, None)
 
2747
            except errors.UnknownSmartMethod:
 
2748
                self._ensure_real()
 
2749
                for entry in self._real_repository.iter_revisions(revision_ids):
 
2750
                    yield entry
 
2751
 
 
2752
    def supports_rich_root(self):
 
2753
        return self._format.rich_root_data
 
2754
 
 
2755
    @property
 
2756
    def _serializer(self):
 
2757
        return self._format._serializer
 
2758
 
 
2759
    def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
 
2760
        with self.lock_write():
 
2761
            signature = gpg_strategy.sign(plaintext, gpg.MODE_CLEAR)
 
2762
            self.add_signature_text(revision_id, signature)
 
2763
 
 
2764
    def add_signature_text(self, revision_id, signature):
 
2765
        if self._real_repository:
 
2766
            # If there is a real repository the write group will
 
2767
            # be in the real repository as well, so use that:
 
2768
            self._ensure_real()
 
2769
            return self._real_repository.add_signature_text(
 
2770
                revision_id, signature)
 
2771
        path = self.controldir._path_for_remote_call(self._client)
 
2772
        response, handler = self._call_with_body_bytes_expecting_body(
 
2773
            b'Repository.add_signature_text', (path, self._lock_token,
 
2774
                                               revision_id) +
 
2775
            tuple([token.encode('utf-8')
 
2776
                   for token in self._write_group_tokens]),
 
2777
            signature)
 
2778
        handler.cancel_read_body()
 
2779
        self.refresh_data()
 
2780
        if response[0] != b'ok':
 
2781
            raise errors.UnexpectedSmartServerResponse(response)
 
2782
        self._write_group_tokens = [token.decode(
 
2783
            'utf-8') for token in response[1:]]
 
2784
 
 
2785
    def has_signature_for_revision_id(self, revision_id):
 
2786
        path = self.controldir._path_for_remote_call(self._client)
 
2787
        try:
 
2788
            response = self._call(b'Repository.has_signature_for_revision_id',
 
2789
                                  path, revision_id)
 
2790
        except errors.UnknownSmartMethod:
 
2791
            self._ensure_real()
 
2792
            return self._real_repository.has_signature_for_revision_id(
 
2793
                revision_id)
 
2794
        if response[0] not in (b'yes', b'no'):
 
2795
            raise SmartProtocolError(
 
2796
                'unexpected response code %s' % (response,))
 
2797
        if response[0] == b'yes':
 
2798
            return True
 
2799
        for fallback in self._fallback_repositories:
 
2800
            if fallback.has_signature_for_revision_id(revision_id):
 
2801
                return True
 
2802
        return False
 
2803
 
 
2804
    def verify_revision_signature(self, revision_id, gpg_strategy):
 
2805
        with self.lock_read():
 
2806
            if not self.has_signature_for_revision_id(revision_id):
 
2807
                return gpg.SIGNATURE_NOT_SIGNED, None
 
2808
            signature = self.get_signature_text(revision_id)
 
2809
 
 
2810
            testament = _mod_testament.Testament.from_revision(
 
2811
                self, revision_id)
 
2812
 
 
2813
            (status, key, signed_plaintext) = gpg_strategy.verify(signature)
 
2814
            if testament.as_short_text() != signed_plaintext:
 
2815
                return gpg.SIGNATURE_NOT_VALID, None
 
2816
            return (status, key)
 
2817
 
 
2818
    def item_keys_introduced_by(self, revision_ids, _files_pb=None):
 
2819
        self._ensure_real()
 
2820
        return self._real_repository.item_keys_introduced_by(revision_ids,
 
2821
                                                             _files_pb=_files_pb)
 
2822
 
 
2823
    def _find_inconsistent_revision_parents(self, revisions_iterator=None):
 
2824
        self._ensure_real()
 
2825
        return self._real_repository._find_inconsistent_revision_parents(
 
2826
            revisions_iterator)
 
2827
 
 
2828
    def _check_for_inconsistent_revision_parents(self):
 
2829
        self._ensure_real()
 
2830
        return self._real_repository._check_for_inconsistent_revision_parents()
 
2831
 
 
2832
    def _make_parents_provider(self, other=None):
 
2833
        providers = [self._unstacked_provider]
 
2834
        if other is not None:
 
2835
            providers.insert(0, other)
 
2836
        return graph.StackedParentsProvider(_LazyListJoin(
 
2837
            providers, self._fallback_repositories))
 
2838
 
 
2839
    def _serialise_search_recipe(self, recipe):
 
2840
        """Serialise a graph search recipe.
 
2841
 
 
2842
        :param recipe: A search recipe (start, stop, count).
 
2843
        :return: Serialised bytes.
 
2844
        """
 
2845
        start_keys = b' '.join(recipe[1])
 
2846
        stop_keys = b' '.join(recipe[2])
 
2847
        count = str(recipe[3]).encode('ascii')
 
2848
        return b'\n'.join((start_keys, stop_keys, count))
 
2849
 
 
2850
    def _serialise_search_result(self, search_result):
 
2851
        parts = search_result.get_network_struct()
 
2852
        return b'\n'.join(parts)
 
2853
 
 
2854
    def autopack(self):
 
2855
        path = self.controldir._path_for_remote_call(self._client)
 
2856
        try:
 
2857
            response = self._call(b'PackRepository.autopack', path)
 
2858
        except errors.UnknownSmartMethod:
 
2859
            self._ensure_real()
 
2860
            self._real_repository._pack_collection.autopack()
 
2861
            return
 
2862
        self.refresh_data()
 
2863
        if response[0] != b'ok':
 
2864
            raise errors.UnexpectedSmartServerResponse(response)
 
2865
 
 
2866
    def _revision_archive(self, revision_id, format, name, root, subdir,
 
2867
                          force_mtime=None):
 
2868
        path = self.controldir._path_for_remote_call(self._client)
 
2869
        format = format or ''
 
2870
        root = root or ''
 
2871
        subdir = subdir or ''
 
2872
        force_mtime = int(force_mtime) if force_mtime is not None else None
 
2873
        try:
 
2874
            response, protocol = self._call_expecting_body(
 
2875
                b'Repository.revision_archive', path,
 
2876
                revision_id,
 
2877
                format.encode('ascii'),
 
2878
                os.path.basename(name).encode('utf-8'),
 
2879
                root.encode('utf-8'),
 
2880
                subdir.encode('utf-8'),
 
2881
                force_mtime)
 
2882
        except errors.UnknownSmartMethod:
 
2883
            return None
 
2884
        if response[0] == b'ok':
 
2885
            return iter([protocol.read_body_bytes()])
 
2886
        raise errors.UnexpectedSmartServerResponse(response)
 
2887
 
 
2888
    def _annotate_file_revision(self, revid, tree_path, file_id, default_revision):
 
2889
        path = self.controldir._path_for_remote_call(self._client)
 
2890
        tree_path = tree_path.encode('utf-8')
 
2891
        file_id = file_id or b''
 
2892
        default_revision = default_revision or b''
 
2893
        try:
 
2894
            response, handler = self._call_expecting_body(
 
2895
                b'Repository.annotate_file_revision', path,
 
2896
                revid, tree_path, file_id, default_revision)
 
2897
        except errors.UnknownSmartMethod:
 
2898
            return None
 
2899
        if response[0] != b'ok':
 
2900
            raise errors.UnexpectedSmartServerResponse(response)
 
2901
        return map(tuple, bencode.bdecode(handler.read_body_bytes()))
 
2902
 
 
2903
 
 
2904
class RemoteStreamSink(vf_repository.StreamSink):
 
2905
 
 
2906
    def _insert_real(self, stream, src_format, resume_tokens):
 
2907
        self.target_repo._ensure_real()
 
2908
        sink = self.target_repo._real_repository._get_sink()
 
2909
        result = sink.insert_stream(stream, src_format, resume_tokens)
 
2910
        if not result:
 
2911
            self.target_repo.autopack()
 
2912
        return result
 
2913
 
 
2914
    def insert_missing_keys(self, source, missing_keys):
 
2915
        if (isinstance(source, RemoteStreamSource)
 
2916
                and source.from_repository._client._medium == self.target_repo._client._medium):
 
2917
            # Streaming from and to the same medium is tricky, since we don't support
 
2918
            # more than one concurrent request. For now, just force VFS.
 
2919
            stream = source._get_real_stream_for_missing_keys(missing_keys)
 
2920
        else:
 
2921
            stream = source.get_stream_for_missing_keys(missing_keys)
 
2922
        return self.insert_stream_without_locking(stream,
 
2923
                                                  self.target_repo._format)
 
2924
 
 
2925
    def insert_stream(self, stream, src_format, resume_tokens):
 
2926
        target = self.target_repo
 
2927
        target._unstacked_provider.missing_keys.clear()
 
2928
        candidate_calls = [(b'Repository.insert_stream_1.19', (1, 19))]
 
2929
        if target._lock_token:
 
2930
            candidate_calls.append(
 
2931
                (b'Repository.insert_stream_locked', (1, 14)))
 
2932
            lock_args = (target._lock_token or b'',)
 
2933
        else:
 
2934
            candidate_calls.append((b'Repository.insert_stream', (1, 13)))
 
2935
            lock_args = ()
 
2936
        client = target._client
 
2937
        medium = client._medium
 
2938
        path = target.controldir._path_for_remote_call(client)
 
2939
        # Probe for the verb to use with an empty stream before sending the
 
2940
        # real stream to it.  We do this both to avoid the risk of sending a
 
2941
        # large request that is then rejected, and because we don't want to
 
2942
        # implement a way to buffer, rewind, or restart the stream.
 
2943
        found_verb = False
 
2944
        for verb, required_version in candidate_calls:
 
2945
            if medium._is_remote_before(required_version):
 
2946
                continue
 
2947
            if resume_tokens:
 
2948
                # We've already done the probing (and set _is_remote_before) on
 
2949
                # a previous insert.
 
2950
                found_verb = True
 
2951
                break
 
2952
            byte_stream = smart_repo._stream_to_byte_stream([], src_format)
 
2953
            try:
 
2954
                response = client.call_with_body_stream(
 
2955
                    (verb, path, b'') + lock_args, byte_stream)
 
2956
            except errors.UnknownSmartMethod:
 
2957
                medium._remember_remote_is_before(required_version)
 
2958
            else:
 
2959
                found_verb = True
 
2960
                break
 
2961
        if not found_verb:
 
2962
            # Have to use VFS.
 
2963
            return self._insert_real(stream, src_format, resume_tokens)
 
2964
        self._last_inv_record = None
 
2965
        self._last_substream = None
 
2966
        if required_version < (1, 19):
 
2967
            # Remote side doesn't support inventory deltas.  Wrap the stream to
 
2968
            # make sure we don't send any.  If the stream contains inventory
 
2969
            # deltas we'll interrupt the smart insert_stream request and
 
2970
            # fallback to VFS.
 
2971
            stream = self._stop_stream_if_inventory_delta(stream)
 
2972
        byte_stream = smart_repo._stream_to_byte_stream(
 
2973
            stream, src_format)
 
2974
        resume_tokens = b' '.join([token.encode('utf-8')
 
2975
                                   for token in resume_tokens])
 
2976
        response = client.call_with_body_stream(
 
2977
            (verb, path, resume_tokens) + lock_args, byte_stream)
 
2978
        if response[0][0] not in (b'ok', b'missing-basis'):
 
2979
            raise errors.UnexpectedSmartServerResponse(response)
 
2980
        if self._last_substream is not None:
 
2981
            # The stream included an inventory-delta record, but the remote
 
2982
            # side isn't new enough to support them.  So we need to send the
 
2983
            # rest of the stream via VFS.
 
2984
            self.target_repo.refresh_data()
 
2985
            return self._resume_stream_with_vfs(response, src_format)
 
2986
        if response[0][0] == b'missing-basis':
 
2987
            tokens, missing_keys = bencode.bdecode_as_tuple(response[0][1])
 
2988
            resume_tokens = [token.decode('utf-8') for token in tokens]
 
2989
            return resume_tokens, set((entry[0].decode('utf-8'), ) + entry[1:] for entry in missing_keys)
 
2990
        else:
 
2991
            self.target_repo.refresh_data()
 
2992
            return [], set()
 
2993
 
 
2994
    def _resume_stream_with_vfs(self, response, src_format):
 
2995
        """Resume sending a stream via VFS, first resending the record and
 
2996
        substream that couldn't be sent via an insert_stream verb.
 
2997
        """
 
2998
        if response[0][0] == b'missing-basis':
 
2999
            tokens, missing_keys = bencode.bdecode_as_tuple(response[0][1])
 
3000
            tokens = [token.decode('utf-8') for token in tokens]
 
3001
            # Ignore missing_keys, we haven't finished inserting yet
 
3002
        else:
 
3003
            tokens = []
 
3004
 
 
3005
        def resume_substream():
 
3006
            # Yield the substream that was interrupted.
 
3007
            for record in self._last_substream:
 
3008
                yield record
 
3009
            self._last_substream = None
 
3010
 
 
3011
        def resume_stream():
 
3012
            # Finish sending the interrupted substream
 
3013
            yield ('inventory-deltas', resume_substream())
 
3014
            # Then simply continue sending the rest of the stream.
 
3015
            for substream_kind, substream in self._last_stream:
 
3016
                yield substream_kind, substream
 
3017
        return self._insert_real(resume_stream(), src_format, tokens)
 
3018
 
 
3019
    def _stop_stream_if_inventory_delta(self, stream):
 
3020
        """Normally this just lets the original stream pass-through unchanged.
 
3021
 
 
3022
        However if any 'inventory-deltas' substream occurs it will stop
 
3023
        streaming, and store the interrupted substream and stream in
 
3024
        self._last_substream and self._last_stream so that the stream can be
 
3025
        resumed by _resume_stream_with_vfs.
 
3026
        """
 
3027
 
 
3028
        stream_iter = iter(stream)
 
3029
        for substream_kind, substream in stream_iter:
 
3030
            if substream_kind == 'inventory-deltas':
 
3031
                self._last_substream = substream
 
3032
                self._last_stream = stream_iter
 
3033
                return
 
3034
            else:
 
3035
                yield substream_kind, substream
 
3036
 
 
3037
 
 
3038
class RemoteStreamSource(vf_repository.StreamSource):
 
3039
    """Stream data from a remote server."""
 
3040
 
 
3041
    def get_stream(self, search):
 
3042
        if (self.from_repository._fallback_repositories
 
3043
                and self.to_format._fetch_order == 'topological'):
 
3044
            return self._real_stream(self.from_repository, search)
 
3045
        sources = []
 
3046
        seen = set()
 
3047
        repos = [self.from_repository]
 
3048
        while repos:
 
3049
            repo = repos.pop(0)
 
3050
            if repo in seen:
 
3051
                continue
 
3052
            seen.add(repo)
 
3053
            repos.extend(repo._fallback_repositories)
 
3054
            sources.append(repo)
 
3055
        return self.missing_parents_chain(search, sources)
 
3056
 
 
3057
    def _get_real_stream_for_missing_keys(self, missing_keys):
 
3058
        self.from_repository._ensure_real()
 
3059
        real_repo = self.from_repository._real_repository
 
3060
        real_source = real_repo._get_source(self.to_format)
 
3061
        return real_source.get_stream_for_missing_keys(missing_keys)
 
3062
 
 
3063
    def get_stream_for_missing_keys(self, missing_keys):
 
3064
        if not isinstance(self.from_repository, RemoteRepository):
 
3065
            return self._get_real_stream_for_missing_keys(missing_keys)
 
3066
        client = self.from_repository._client
 
3067
        medium = client._medium
 
3068
        if medium._is_remote_before((3, 0)):
 
3069
            return self._get_real_stream_for_missing_keys(missing_keys)
 
3070
        path = self.from_repository.controldir._path_for_remote_call(client)
 
3071
        args = (path, self.to_format.network_name())
 
3072
        search_bytes = b'\n'.join(
 
3073
            [b'%s\t%s' % (key[0].encode('utf-8'), key[1]) for key in missing_keys])
 
3074
        try:
 
3075
            response, handler = self.from_repository._call_with_body_bytes_expecting_body(
 
3076
                b'Repository.get_stream_for_missing_keys', args, search_bytes)
 
3077
        except (errors.UnknownSmartMethod, errors.UnknownFormatError):
 
3078
            return self._get_real_stream_for_missing_keys(missing_keys)
 
3079
        if response[0] != b'ok':
 
3080
            raise errors.UnexpectedSmartServerResponse(response)
 
3081
        byte_stream = handler.read_streamed_body()
 
3082
        src_format, stream = smart_repo._byte_stream_to_stream(byte_stream,
 
3083
                                                               self._record_counter)
 
3084
        if src_format.network_name() != self.from_repository._format.network_name():
 
3085
            raise AssertionError(
 
3086
                "Mismatched RemoteRepository and stream src %r, %r" % (
 
3087
                    src_format.network_name(), repo._format.network_name()))
 
3088
        return stream
 
3089
 
 
3090
    def _real_stream(self, repo, search):
 
3091
        """Get a stream for search from repo.
 
3092
 
 
3093
        This never called RemoteStreamSource.get_stream, and is a helper
 
3094
        for RemoteStreamSource._get_stream to allow getting a stream
 
3095
        reliably whether fallback back because of old servers or trying
 
3096
        to stream from a non-RemoteRepository (which the stacked support
 
3097
        code will do).
 
3098
        """
 
3099
        source = repo._get_source(self.to_format)
 
3100
        if isinstance(source, RemoteStreamSource):
 
3101
            repo._ensure_real()
 
3102
            source = repo._real_repository._get_source(self.to_format)
 
3103
        return source.get_stream(search)
 
3104
 
 
3105
    def _get_stream(self, repo, search):
 
3106
        """Core worker to get a stream from repo for search.
 
3107
 
 
3108
        This is used by both get_stream and the stacking support logic. It
 
3109
        deliberately gets a stream for repo which does not need to be
 
3110
        self.from_repository. In the event that repo is not Remote, or
 
3111
        cannot do a smart stream, a fallback is made to the generic
 
3112
        repository._get_stream() interface, via self._real_stream.
 
3113
 
 
3114
        In the event of stacking, streams from _get_stream will not
 
3115
        contain all the data for search - this is normal (see get_stream).
 
3116
 
 
3117
        :param repo: A repository.
 
3118
        :param search: A search.
 
3119
        """
 
3120
        # Fallbacks may be non-smart
 
3121
        if not isinstance(repo, RemoteRepository):
 
3122
            return self._real_stream(repo, search)
 
3123
        client = repo._client
 
3124
        medium = client._medium
 
3125
        path = repo.controldir._path_for_remote_call(client)
 
3126
        search_bytes = repo._serialise_search_result(search)
 
3127
        args = (path, self.to_format.network_name())
 
3128
        candidate_verbs = [
 
3129
            (b'Repository.get_stream_1.19', (1, 19)),
 
3130
            (b'Repository.get_stream', (1, 13))]
 
3131
 
 
3132
        found_verb = False
 
3133
        for verb, version in candidate_verbs:
 
3134
            if medium._is_remote_before(version):
 
3135
                continue
 
3136
            try:
 
3137
                response = repo._call_with_body_bytes_expecting_body(
 
3138
                    verb, args, search_bytes)
 
3139
            except errors.UnknownSmartMethod:
 
3140
                medium._remember_remote_is_before(version)
 
3141
            except errors.UnknownErrorFromSmartServer as e:
 
3142
                if isinstance(search, vf_search.EverythingResult):
 
3143
                    error_verb = e.error_from_smart_server.error_verb
 
3144
                    if error_verb == b'BadSearch':
 
3145
                        # Pre-2.4 servers don't support this sort of search.
 
3146
                        # XXX: perhaps falling back to VFS on BadSearch is a
 
3147
                        # good idea in general?  It might provide a little bit
 
3148
                        # of protection against client-side bugs.
 
3149
                        medium._remember_remote_is_before((2, 4))
 
3150
                        break
 
3151
                raise
 
3152
            else:
 
3153
                response_tuple, response_handler = response
 
3154
                found_verb = True
 
3155
                break
 
3156
        if not found_verb:
 
3157
            return self._real_stream(repo, search)
 
3158
        if response_tuple[0] != b'ok':
 
3159
            raise errors.UnexpectedSmartServerResponse(response_tuple)
 
3160
        byte_stream = response_handler.read_streamed_body()
 
3161
        src_format, stream = smart_repo._byte_stream_to_stream(byte_stream,
 
3162
                                                               self._record_counter)
 
3163
        if src_format.network_name() != repo._format.network_name():
 
3164
            raise AssertionError(
 
3165
                "Mismatched RemoteRepository and stream src %r, %r" % (
 
3166
                    src_format.network_name(), repo._format.network_name()))
 
3167
        return stream
 
3168
 
 
3169
    def missing_parents_chain(self, search, sources):
 
3170
        """Chain multiple streams together to handle stacking.
 
3171
 
 
3172
        :param search: The overall search to satisfy with streams.
 
3173
        :param sources: A list of Repository objects to query.
 
3174
        """
 
3175
        self.from_serialiser = self.from_repository._format._serializer
 
3176
        self.seen_revs = set()
 
3177
        self.referenced_revs = set()
 
3178
        # If there are heads in the search, or the key count is > 0, we are not
 
3179
        # done.
 
3180
        while not search.is_empty() and len(sources) > 1:
 
3181
            source = sources.pop(0)
 
3182
            stream = self._get_stream(source, search)
 
3183
            for kind, substream in stream:
 
3184
                if kind != 'revisions':
 
3185
                    yield kind, substream
 
3186
                else:
 
3187
                    yield kind, self.missing_parents_rev_handler(substream)
 
3188
            search = search.refine(self.seen_revs, self.referenced_revs)
 
3189
            self.seen_revs = set()
 
3190
            self.referenced_revs = set()
 
3191
        if not search.is_empty():
 
3192
            for kind, stream in self._get_stream(sources[0], search):
 
3193
                yield kind, stream
 
3194
 
 
3195
    def missing_parents_rev_handler(self, substream):
 
3196
        for content in substream:
 
3197
            revision_bytes = content.get_bytes_as('fulltext')
 
3198
            revision = self.from_serialiser.read_revision_from_string(
 
3199
                revision_bytes)
 
3200
            self.seen_revs.add(content.key[-1])
 
3201
            self.referenced_revs.update(revision.parent_ids)
 
3202
            yield content
 
3203
 
 
3204
 
 
3205
class RemoteBranchLockableFiles(LockableFiles):
 
3206
    """A 'LockableFiles' implementation that talks to a smart server.
 
3207
 
 
3208
    This is not a public interface class.
 
3209
    """
 
3210
 
 
3211
    def __init__(self, bzrdir, _client):
 
3212
        self.controldir = bzrdir
 
3213
        self._client = _client
 
3214
        self._need_find_modes = True
 
3215
        LockableFiles.__init__(
 
3216
            self, bzrdir.get_branch_transport(None),
 
3217
            'lock', lockdir.LockDir)
 
3218
 
 
3219
    def _find_modes(self):
 
3220
        # RemoteBranches don't let the client set the mode of control files.
 
3221
        self._dir_mode = None
 
3222
        self._file_mode = None
 
3223
 
 
3224
 
 
3225
class RemoteBranchFormat(branch.BranchFormat):
 
3226
 
 
3227
    def __init__(self, network_name=None):
 
3228
        super(RemoteBranchFormat, self).__init__()
 
3229
        self._matchingcontroldir = RemoteBzrDirFormat()
 
3230
        self._matchingcontroldir.set_branch_format(self)
 
3231
        self._custom_format = None
 
3232
        self._network_name = network_name
 
3233
 
 
3234
    def __eq__(self, other):
 
3235
        return (isinstance(other, RemoteBranchFormat)
 
3236
                and self.__dict__ == other.__dict__)
 
3237
 
 
3238
    def _ensure_real(self):
 
3239
        if self._custom_format is None:
 
3240
            try:
 
3241
                self._custom_format = branch.network_format_registry.get(
 
3242
                    self._network_name)
 
3243
            except KeyError:
 
3244
                raise errors.UnknownFormatError(kind='branch',
 
3245
                                                format=self._network_name)
 
3246
 
 
3247
    def get_format_description(self):
 
3248
        self._ensure_real()
 
3249
        return 'Remote: ' + self._custom_format.get_format_description()
 
3250
 
 
3251
    def network_name(self):
 
3252
        return self._network_name
 
3253
 
 
3254
    def open(self, a_controldir, name=None, ignore_fallbacks=False):
 
3255
        return a_controldir.open_branch(name=name,
 
3256
                                        ignore_fallbacks=ignore_fallbacks)
 
3257
 
 
3258
    def _vfs_initialize(self, a_controldir, name, append_revisions_only,
 
3259
                        repository=None):
 
3260
        # Initialisation when using a local bzrdir object, or a non-vfs init
 
3261
        # method is not available on the server.
 
3262
        # self._custom_format is always set - the start of initialize ensures
 
3263
        # that.
 
3264
        if isinstance(a_controldir, RemoteBzrDir):
 
3265
            a_controldir._ensure_real()
 
3266
            result = self._custom_format.initialize(a_controldir._real_bzrdir,
 
3267
                                                    name=name, append_revisions_only=append_revisions_only,
 
3268
                                                    repository=repository)
 
3269
        else:
 
3270
            # We assume the bzrdir is parameterised; it may not be.
 
3271
            result = self._custom_format.initialize(a_controldir, name=name,
 
3272
                                                    append_revisions_only=append_revisions_only,
 
3273
                                                    repository=repository)
 
3274
        if (isinstance(a_controldir, RemoteBzrDir)
 
3275
                and not isinstance(result, RemoteBranch)):
 
3276
            result = RemoteBranch(a_controldir, a_controldir.find_repository(), result,
 
3277
                                  name=name)
 
3278
        return result
 
3279
 
 
3280
    def initialize(self, a_controldir, name=None, repository=None,
 
3281
                   append_revisions_only=None):
 
3282
        if name is None:
 
3283
            name = a_controldir._get_selected_branch()
 
3284
        # 1) get the network name to use.
 
3285
        if self._custom_format:
 
3286
            network_name = self._custom_format.network_name()
 
3287
        else:
 
3288
            # Select the current breezy default and ask for that.
 
3289
            reference_bzrdir_format = controldir.format_registry.get(
 
3290
                'default')()
 
3291
            reference_format = reference_bzrdir_format.get_branch_format()
 
3292
            self._custom_format = reference_format
 
3293
            network_name = reference_format.network_name()
 
3294
        # Being asked to create on a non RemoteBzrDir:
 
3295
        if not isinstance(a_controldir, RemoteBzrDir):
 
3296
            return self._vfs_initialize(a_controldir, name=name,
 
3297
                                        append_revisions_only=append_revisions_only,
 
3298
                                        repository=repository)
 
3299
        medium = a_controldir._client._medium
 
3300
        if medium._is_remote_before((1, 13)):
 
3301
            return self._vfs_initialize(a_controldir, name=name,
 
3302
                                        append_revisions_only=append_revisions_only,
 
3303
                                        repository=repository)
 
3304
        # Creating on a remote bzr dir.
 
3305
        # 2) try direct creation via RPC
 
3306
        path = a_controldir._path_for_remote_call(a_controldir._client)
 
3307
        if name != "":
 
3308
            # XXX JRV20100304: Support creating colocated branches
 
3309
            raise errors.NoColocatedBranchSupport(self)
 
3310
        verb = b'BzrDir.create_branch'
 
3311
        try:
 
3312
            response = a_controldir._call(verb, path, network_name)
 
3313
        except errors.UnknownSmartMethod:
 
3314
            # Fallback - use vfs methods
 
3315
            medium._remember_remote_is_before((1, 13))
 
3316
            return self._vfs_initialize(a_controldir, name=name,
 
3317
                                        append_revisions_only=append_revisions_only,
 
3318
                                        repository=repository)
 
3319
        if response[0] != b'ok':
 
3320
            raise errors.UnexpectedSmartServerResponse(response)
 
3321
        # Turn the response into a RemoteRepository object.
 
3322
        format = RemoteBranchFormat(network_name=response[1])
 
3323
        repo_format = response_tuple_to_repo_format(response[3:])
 
3324
        repo_path = response[2].decode('utf-8')
 
3325
        if repository is not None:
 
3326
            remote_repo_url = urlutils.join(a_controldir.user_url, repo_path)
 
3327
            url_diff = urlutils.relative_url(repository.user_url,
 
3328
                                             remote_repo_url)
 
3329
            if url_diff != '.':
 
3330
                raise AssertionError(
 
3331
                    'repository.user_url %r does not match URL from server '
 
3332
                    'response (%r + %r)'
 
3333
                    % (repository.user_url, a_controldir.user_url, repo_path))
 
3334
            remote_repo = repository
 
3335
        else:
 
3336
            if repo_path == '':
 
3337
                repo_bzrdir = a_controldir
 
3338
            else:
 
3339
                repo_bzrdir = RemoteBzrDir(
 
3340
                    a_controldir.root_transport.clone(
 
3341
                        repo_path), a_controldir._format,
 
3342
                    a_controldir._client)
 
3343
            remote_repo = RemoteRepository(repo_bzrdir, repo_format)
 
3344
        remote_branch = RemoteBranch(a_controldir, remote_repo,
 
3345
                                     format=format, setup_stacking=False, name=name)
 
3346
        if append_revisions_only:
 
3347
            remote_branch.set_append_revisions_only(append_revisions_only)
 
3348
        # XXX: We know this is a new branch, so it must have revno 0, revid
 
3349
        # NULL_REVISION. Creating the branch locked would make this be unable
 
3350
        # to be wrong; here its simply very unlikely to be wrong. RBC 20090225
 
3351
        remote_branch._last_revision_info_cache = 0, NULL_REVISION
 
3352
        return remote_branch
 
3353
 
 
3354
    def make_tags(self, branch):
 
3355
        self._ensure_real()
 
3356
        return self._custom_format.make_tags(branch)
 
3357
 
 
3358
    def supports_tags(self):
 
3359
        # Remote branches might support tags, but we won't know until we
 
3360
        # access the real remote branch.
 
3361
        self._ensure_real()
 
3362
        return self._custom_format.supports_tags()
 
3363
 
 
3364
    def supports_stacking(self):
 
3365
        self._ensure_real()
 
3366
        return self._custom_format.supports_stacking()
 
3367
 
 
3368
    def supports_set_append_revisions_only(self):
 
3369
        self._ensure_real()
 
3370
        return self._custom_format.supports_set_append_revisions_only()
 
3371
 
 
3372
    @property
 
3373
    def supports_reference_locations(self):
 
3374
        self._ensure_real()
 
3375
        return self._custom_format.supports_reference_locations
 
3376
 
 
3377
    def stores_revno(self):
 
3378
        return True
 
3379
 
 
3380
    def _use_default_local_heads_to_fetch(self):
 
3381
        # If the branch format is a metadir format *and* its heads_to_fetch
 
3382
        # implementation is not overridden vs the base class, we can use the
 
3383
        # base class logic rather than use the heads_to_fetch RPC.  This is
 
3384
        # usually cheaper in terms of net round trips, as the last-revision and
 
3385
        # tags info fetched is cached and would be fetched anyway.
 
3386
        self._ensure_real()
 
3387
        if isinstance(self._custom_format, bzrbranch.BranchFormatMetadir):
 
3388
            branch_class = self._custom_format._branch_class()
 
3389
            heads_to_fetch_impl = branch_class.heads_to_fetch
 
3390
            if heads_to_fetch_impl is branch.Branch.heads_to_fetch:
 
3391
                return True
 
3392
        return False
 
3393
 
 
3394
 
 
3395
class RemoteBranchStore(_mod_config.IniFileStore):
 
3396
    """Branch store which attempts to use HPSS calls to retrieve branch store.
 
3397
 
 
3398
    Note that this is specific to bzr-based formats.
 
3399
    """
 
3400
 
 
3401
    def __init__(self, branch):
 
3402
        super(RemoteBranchStore, self).__init__()
 
3403
        self.branch = branch
 
3404
        self.id = "branch"
 
3405
        self._real_store = None
 
3406
 
 
3407
    def external_url(self):
 
3408
        return urlutils.join(self.branch.user_url, 'branch.conf')
 
3409
 
 
3410
    def _load_content(self):
 
3411
        path = self.branch._remote_path()
 
3412
        try:
 
3413
            response, handler = self.branch._call_expecting_body(
 
3414
                b'Branch.get_config_file', path)
 
3415
        except errors.UnknownSmartMethod:
 
3416
            self._ensure_real()
 
3417
            return self._real_store._load_content()
 
3418
        if len(response) and response[0] != b'ok':
 
3419
            raise errors.UnexpectedSmartServerResponse(response)
 
3420
        return handler.read_body_bytes()
 
3421
 
 
3422
    def _save_content(self, content):
 
3423
        path = self.branch._remote_path()
 
3424
        try:
 
3425
            response, handler = self.branch._call_with_body_bytes_expecting_body(
 
3426
                b'Branch.put_config_file', (path,
 
3427
                                            self.branch._lock_token, self.branch._repo_lock_token),
 
3428
                content)
 
3429
        except errors.UnknownSmartMethod:
 
3430
            self._ensure_real()
 
3431
            return self._real_store._save_content(content)
 
3432
        handler.cancel_read_body()
 
3433
        if response != (b'ok', ):
 
3434
            raise errors.UnexpectedSmartServerResponse(response)
 
3435
 
 
3436
    def _ensure_real(self):
 
3437
        self.branch._ensure_real()
 
3438
        if self._real_store is None:
 
3439
            self._real_store = _mod_config.BranchStore(self.branch)
 
3440
 
 
3441
 
 
3442
class RemoteBranch(branch.Branch, _RpcHelper, lock._RelockDebugMixin):
 
3443
    """Branch stored on a server accessed by HPSS RPC.
 
3444
 
 
3445
    At the moment most operations are mapped down to simple file operations.
 
3446
    """
 
3447
 
 
3448
    def __init__(self, remote_bzrdir, remote_repository, real_branch=None,
 
3449
                 _client=None, format=None, setup_stacking=True, name=None,
 
3450
                 possible_transports=None):
 
3451
        """Create a RemoteBranch instance.
 
3452
 
 
3453
        :param real_branch: An optional local implementation of the branch
 
3454
            format, usually accessing the data via the VFS.
 
3455
        :param _client: Private parameter for testing.
 
3456
        :param format: A RemoteBranchFormat object, None to create one
 
3457
            automatically. If supplied it should have a network_name already
 
3458
            supplied.
 
3459
        :param setup_stacking: If True make an RPC call to determine the
 
3460
            stacked (or not) status of the branch. If False assume the branch
 
3461
            is not stacked.
 
3462
        :param name: Colocated branch name
 
3463
        """
 
3464
        # We intentionally don't call the parent class's __init__, because it
 
3465
        # will try to assign to self.tags, which is a property in this subclass.
 
3466
        # And the parent's __init__ doesn't do much anyway.
 
3467
        self.controldir = remote_bzrdir
 
3468
        self.name = name
 
3469
        if _client is not None:
 
3470
            self._client = _client
 
3471
        else:
 
3472
            self._client = remote_bzrdir._client
 
3473
        self.repository = remote_repository
 
3474
        if real_branch is not None:
 
3475
            self._real_branch = real_branch
 
3476
            # Give the remote repository the matching real repo.
 
3477
            real_repo = self._real_branch.repository
 
3478
            if isinstance(real_repo, RemoteRepository):
 
3479
                real_repo._ensure_real()
 
3480
                real_repo = real_repo._real_repository
 
3481
            self.repository._set_real_repository(real_repo)
 
3482
            # Give the branch the remote repository to let fast-pathing happen.
 
3483
            self._real_branch.repository = self.repository
 
3484
        else:
 
3485
            self._real_branch = None
 
3486
        # Fill out expected attributes of branch for breezy API users.
 
3487
        self._clear_cached_state()
 
3488
        # TODO: deprecate self.base in favor of user_url
 
3489
        self.base = self.controldir.user_url
 
3490
        self._name = name
 
3491
        self._control_files = None
 
3492
        self._lock_mode = None
 
3493
        self._lock_token = None
 
3494
        self._repo_lock_token = None
 
3495
        self._lock_count = 0
 
3496
        self._leave_lock = False
 
3497
        self.conf_store = None
 
3498
        # Setup a format: note that we cannot call _ensure_real until all the
 
3499
        # attributes above are set: This code cannot be moved higher up in this
 
3500
        # function.
 
3501
        if format is None:
 
3502
            self._format = RemoteBranchFormat()
 
3503
            if real_branch is not None:
 
3504
                self._format._network_name = \
 
3505
                    self._real_branch._format.network_name()
 
3506
        else:
 
3507
            self._format = format
 
3508
        # when we do _ensure_real we may need to pass ignore_fallbacks to the
 
3509
        # branch.open_branch method.
 
3510
        self._real_ignore_fallbacks = not setup_stacking
 
3511
        if not self._format._network_name:
 
3512
            # Did not get from open_branchV2 - old server.
 
3513
            self._ensure_real()
 
3514
            self._format._network_name = \
 
3515
                self._real_branch._format.network_name()
 
3516
        self.tags = self._format.make_tags(self)
 
3517
        # The base class init is not called, so we duplicate this:
 
3518
        hooks = branch.Branch.hooks['open']
 
3519
        for hook in hooks:
 
3520
            hook(self)
 
3521
        self._is_stacked = False
 
3522
        if setup_stacking:
 
3523
            self._setup_stacking(possible_transports)
 
3524
 
 
3525
    def _setup_stacking(self, possible_transports):
 
3526
        # configure stacking into the remote repository, by reading it from
 
3527
        # the vfs branch.
 
3528
        try:
 
3529
            fallback_url = self.get_stacked_on_url()
 
3530
        except (errors.NotStacked, branch.UnstackableBranchFormat,
 
3531
                errors.UnstackableRepositoryFormat) as e:
 
3532
            return
 
3533
        self._is_stacked = True
 
3534
        if possible_transports is None:
 
3535
            possible_transports = []
 
3536
        else:
 
3537
            possible_transports = list(possible_transports)
 
3538
        possible_transports.append(self.controldir.root_transport)
 
3539
        self._activate_fallback_location(fallback_url,
 
3540
                                         possible_transports=possible_transports)
 
3541
 
 
3542
    def _get_config(self):
 
3543
        return RemoteBranchConfig(self)
 
3544
 
 
3545
    def _get_config_store(self):
 
3546
        if self.conf_store is None:
 
3547
            self.conf_store = RemoteBranchStore(self)
 
3548
        return self.conf_store
 
3549
 
 
3550
    def store_uncommitted(self, creator):
 
3551
        self._ensure_real()
 
3552
        return self._real_branch.store_uncommitted(creator)
 
3553
 
 
3554
    def get_unshelver(self, tree):
 
3555
        self._ensure_real()
 
3556
        return self._real_branch.get_unshelver(tree)
 
3557
 
 
3558
    def _get_real_transport(self):
 
3559
        # if we try vfs access, return the real branch's vfs transport
 
3560
        self._ensure_real()
 
3561
        return self._real_branch._transport
 
3562
 
 
3563
    _transport = property(_get_real_transport)
 
3564
 
 
3565
    def __str__(self):
 
3566
        return "%s(%s)" % (self.__class__.__name__, self.base)
 
3567
 
 
3568
    __repr__ = __str__
 
3569
 
 
3570
    def _ensure_real(self):
 
3571
        """Ensure that there is a _real_branch set.
 
3572
 
 
3573
        Used before calls to self._real_branch.
 
3574
        """
 
3575
        if self._real_branch is None:
 
3576
            if not vfs.vfs_enabled():
 
3577
                raise AssertionError('smart server vfs must be enabled '
 
3578
                                     'to use vfs implementation')
 
3579
            self.controldir._ensure_real()
 
3580
            self._real_branch = self.controldir._real_bzrdir.open_branch(
 
3581
                ignore_fallbacks=self._real_ignore_fallbacks, name=self._name)
 
3582
            # The remote branch and the real branch shares the same store. If
 
3583
            # we don't, there will always be cases where one of the stores
 
3584
            # doesn't see an update made on the other.
 
3585
            self._real_branch.conf_store = self.conf_store
 
3586
            if self.repository._real_repository is None:
 
3587
                # Give the remote repository the matching real repo.
 
3588
                real_repo = self._real_branch.repository
 
3589
                if isinstance(real_repo, RemoteRepository):
 
3590
                    real_repo._ensure_real()
 
3591
                    real_repo = real_repo._real_repository
 
3592
                self.repository._set_real_repository(real_repo)
 
3593
            # Give the real branch the remote repository to let fast-pathing
 
3594
            # happen.
 
3595
            self._real_branch.repository = self.repository
 
3596
            if self._lock_mode == 'r':
 
3597
                self._real_branch.lock_read()
 
3598
            elif self._lock_mode == 'w':
 
3599
                self._real_branch.lock_write(token=self._lock_token)
 
3600
 
 
3601
    def _translate_error(self, err, **context):
 
3602
        self.repository._translate_error(err, branch=self, **context)
 
3603
 
 
3604
    def _clear_cached_state(self):
 
3605
        super(RemoteBranch, self)._clear_cached_state()
 
3606
        self._tags_bytes = None
 
3607
        if self._real_branch is not None:
 
3608
            self._real_branch._clear_cached_state()
 
3609
 
 
3610
    def _clear_cached_state_of_remote_branch_only(self):
 
3611
        """Like _clear_cached_state, but doesn't clear the cache of
 
3612
        self._real_branch.
 
3613
 
 
3614
        This is useful when falling back to calling a method of
 
3615
        self._real_branch that changes state.  In that case the underlying
 
3616
        branch changes, so we need to invalidate this RemoteBranch's cache of
 
3617
        it.  However, there's no need to invalidate the _real_branch's cache
 
3618
        too, in fact doing so might harm performance.
 
3619
        """
 
3620
        super(RemoteBranch, self)._clear_cached_state()
 
3621
 
 
3622
    @property
 
3623
    def control_files(self):
 
3624
        # Defer actually creating RemoteBranchLockableFiles until its needed,
 
3625
        # because it triggers an _ensure_real that we otherwise might not need.
 
3626
        if self._control_files is None:
 
3627
            self._control_files = RemoteBranchLockableFiles(
 
3628
                self.controldir, self._client)
 
3629
        return self._control_files
 
3630
 
 
3631
    def get_physical_lock_status(self):
 
3632
        """See Branch.get_physical_lock_status()."""
 
3633
        try:
 
3634
            response = self._client.call(b'Branch.get_physical_lock_status',
 
3635
                                         self._remote_path())
 
3636
        except errors.UnknownSmartMethod:
 
3637
            self._ensure_real()
 
3638
            return self._real_branch.get_physical_lock_status()
 
3639
        if response[0] not in (b'yes', b'no'):
 
3640
            raise errors.UnexpectedSmartServerResponse(response)
 
3641
        return (response[0] == b'yes')
 
3642
 
 
3643
    def get_stacked_on_url(self):
 
3644
        """Get the URL this branch is stacked against.
 
3645
 
 
3646
        :raises NotStacked: If the branch is not stacked.
 
3647
        :raises UnstackableBranchFormat: If the branch does not support
 
3648
            stacking.
 
3649
        :raises UnstackableRepositoryFormat: If the repository does not support
 
3650
            stacking.
 
3651
        """
 
3652
        try:
 
3653
            # there may not be a repository yet, so we can't use
 
3654
            # self._translate_error, so we can't use self._call either.
 
3655
            response = self._client.call(b'Branch.get_stacked_on_url',
 
3656
                                         self._remote_path())
 
3657
        except errors.ErrorFromSmartServer as err:
 
3658
            # there may not be a repository yet, so we can't call through
 
3659
            # its _translate_error
 
3660
            _translate_error(err, branch=self)
 
3661
        except errors.UnknownSmartMethod as err:
 
3662
            self._ensure_real()
 
3663
            return self._real_branch.get_stacked_on_url()
 
3664
        if response[0] != b'ok':
 
3665
            raise errors.UnexpectedSmartServerResponse(response)
 
3666
        return response[1].decode('utf-8')
 
3667
 
 
3668
    def set_stacked_on_url(self, url):
 
3669
        branch.Branch.set_stacked_on_url(self, url)
 
3670
        # We need the stacked_on_url to be visible both locally (to not query
 
3671
        # it repeatedly) and remotely (so smart verbs can get it server side)
 
3672
        # Without the following line,
 
3673
        # breezy.tests.per_branch.test_create_clone.TestCreateClone
 
3674
        # .test_create_clone_on_transport_stacked_hooks_get_stacked_branch
 
3675
        # fails for remote branches -- vila 2012-01-04
 
3676
        self.conf_store.save_changes()
 
3677
        if not url:
 
3678
            self._is_stacked = False
 
3679
        else:
 
3680
            self._is_stacked = True
 
3681
 
 
3682
    def _vfs_get_tags_bytes(self):
 
3683
        self._ensure_real()
 
3684
        return self._real_branch._get_tags_bytes()
 
3685
 
 
3686
    def _get_tags_bytes(self):
 
3687
        with self.lock_read():
 
3688
            if self._tags_bytes is None:
 
3689
                self._tags_bytes = self._get_tags_bytes_via_hpss()
 
3690
            return self._tags_bytes
 
3691
 
 
3692
    def _get_tags_bytes_via_hpss(self):
 
3693
        medium = self._client._medium
 
3694
        if medium._is_remote_before((1, 13)):
 
3695
            return self._vfs_get_tags_bytes()
 
3696
        try:
 
3697
            response = self._call(
 
3698
                b'Branch.get_tags_bytes', self._remote_path())
 
3699
        except errors.UnknownSmartMethod:
 
3700
            medium._remember_remote_is_before((1, 13))
 
3701
            return self._vfs_get_tags_bytes()
 
3702
        return response[0]
 
3703
 
 
3704
    def _vfs_set_tags_bytes(self, bytes):
 
3705
        self._ensure_real()
 
3706
        return self._real_branch._set_tags_bytes(bytes)
 
3707
 
 
3708
    def _set_tags_bytes(self, bytes):
 
3709
        if self.is_locked():
 
3710
            self._tags_bytes = bytes
 
3711
        medium = self._client._medium
 
3712
        if medium._is_remote_before((1, 18)):
 
3713
            self._vfs_set_tags_bytes(bytes)
 
3714
            return
 
3715
        try:
 
3716
            args = (
 
3717
                self._remote_path(), self._lock_token, self._repo_lock_token)
 
3718
            response = self._call_with_body_bytes(
 
3719
                b'Branch.set_tags_bytes', args, bytes)
 
3720
        except errors.UnknownSmartMethod:
 
3721
            medium._remember_remote_is_before((1, 18))
 
3722
            self._vfs_set_tags_bytes(bytes)
 
3723
 
 
3724
    def lock_read(self):
 
3725
        """Lock the branch for read operations.
 
3726
 
 
3727
        :return: A breezy.lock.LogicalLockResult.
 
3728
        """
 
3729
        self.repository.lock_read()
 
3730
        if not self._lock_mode:
 
3731
            self._note_lock('r')
 
3732
            self._lock_mode = 'r'
 
3733
            self._lock_count = 1
 
3734
            if self._real_branch is not None:
 
3735
                self._real_branch.lock_read()
 
3736
        else:
 
3737
            self._lock_count += 1
 
3738
        return lock.LogicalLockResult(self.unlock)
 
3739
 
 
3740
    def _remote_lock_write(self, token):
 
3741
        if token is None:
 
3742
            branch_token = repo_token = b''
 
3743
        else:
 
3744
            branch_token = token
 
3745
            repo_token = self.repository.lock_write().repository_token
 
3746
            self.repository.unlock()
 
3747
        err_context = {'token': token}
 
3748
        try:
 
3749
            response = self._call(
 
3750
                b'Branch.lock_write', self._remote_path(), branch_token,
 
3751
                repo_token or b'', **err_context)
 
3752
        except errors.LockContention as e:
 
3753
            # The LockContention from the server doesn't have any
 
3754
            # information about the lock_url. We re-raise LockContention
 
3755
            # with valid lock_url.
 
3756
            raise errors.LockContention('(remote lock)',
 
3757
                                        self.repository.base.split('.bzr/')[0])
 
3758
        if response[0] != b'ok':
 
3759
            raise errors.UnexpectedSmartServerResponse(response)
 
3760
        ok, branch_token, repo_token = response
 
3761
        return branch_token, repo_token
 
3762
 
 
3763
    def lock_write(self, token=None):
 
3764
        if not self._lock_mode:
 
3765
            self._note_lock('w')
 
3766
            # Lock the branch and repo in one remote call.
 
3767
            remote_tokens = self._remote_lock_write(token)
 
3768
            self._lock_token, self._repo_lock_token = remote_tokens
 
3769
            if not self._lock_token:
 
3770
                raise SmartProtocolError(
 
3771
                    'Remote server did not return a token!')
 
3772
            # Tell the self.repository object that it is locked.
 
3773
            self.repository.lock_write(
 
3774
                self._repo_lock_token, _skip_rpc=True)
 
3775
 
 
3776
            if self._real_branch is not None:
 
3777
                self._real_branch.lock_write(token=self._lock_token)
 
3778
            if token is not None:
 
3779
                self._leave_lock = True
 
3780
            else:
 
3781
                self._leave_lock = False
 
3782
            self._lock_mode = 'w'
 
3783
            self._lock_count = 1
 
3784
        elif self._lock_mode == 'r':
 
3785
            raise errors.ReadOnlyError(self)
 
3786
        else:
 
3787
            if token is not None:
 
3788
                # A token was given to lock_write, and we're relocking, so
 
3789
                # check that the given token actually matches the one we
 
3790
                # already have.
 
3791
                if token != self._lock_token:
 
3792
                    raise errors.TokenMismatch(token, self._lock_token)
 
3793
            self._lock_count += 1
 
3794
            # Re-lock the repository too.
 
3795
            self.repository.lock_write(self._repo_lock_token)
 
3796
        return BranchWriteLockResult(self.unlock, self._lock_token or None)
 
3797
 
 
3798
    def _unlock(self, branch_token, repo_token):
 
3799
        err_context = {'token': str((branch_token, repo_token))}
 
3800
        response = self._call(
 
3801
            b'Branch.unlock', self._remote_path(), branch_token,
 
3802
            repo_token or b'', **err_context)
 
3803
        if response == (b'ok',):
 
3804
            return
 
3805
        raise errors.UnexpectedSmartServerResponse(response)
 
3806
 
 
3807
    @only_raises(errors.LockNotHeld, errors.LockBroken)
 
3808
    def unlock(self):
 
3809
        try:
 
3810
            self._lock_count -= 1
 
3811
            if not self._lock_count:
 
3812
                if self.conf_store is not None:
 
3813
                    self.conf_store.save_changes()
 
3814
                self._clear_cached_state()
 
3815
                mode = self._lock_mode
 
3816
                self._lock_mode = None
 
3817
                if self._real_branch is not None:
 
3818
                    if (not self._leave_lock and mode == 'w'
 
3819
                            and self._repo_lock_token):
 
3820
                        # If this RemoteBranch will remove the physical lock
 
3821
                        # for the repository, make sure the _real_branch
 
3822
                        # doesn't do it first.  (Because the _real_branch's
 
3823
                        # repository is set to be the RemoteRepository.)
 
3824
                        self._real_branch.repository.leave_lock_in_place()
 
3825
                    self._real_branch.unlock()
 
3826
                if mode != 'w':
 
3827
                    # Only write-locked branched need to make a remote method
 
3828
                    # call to perform the unlock.
 
3829
                    return
 
3830
                if not self._lock_token:
 
3831
                    raise AssertionError('Locked, but no token!')
 
3832
                branch_token = self._lock_token
 
3833
                repo_token = self._repo_lock_token
 
3834
                self._lock_token = None
 
3835
                self._repo_lock_token = None
 
3836
                if not self._leave_lock:
 
3837
                    self._unlock(branch_token, repo_token)
 
3838
        finally:
 
3839
            self.repository.unlock()
 
3840
 
 
3841
    def break_lock(self):
 
3842
        try:
 
3843
            response = self._call(
 
3844
                b'Branch.break_lock', self._remote_path())
 
3845
        except errors.UnknownSmartMethod:
 
3846
            self._ensure_real()
 
3847
            return self._real_branch.break_lock()
 
3848
        if response != (b'ok',):
 
3849
            raise errors.UnexpectedSmartServerResponse(response)
 
3850
 
 
3851
    def leave_lock_in_place(self):
 
3852
        if not self._lock_token:
 
3853
            raise NotImplementedError(self.leave_lock_in_place)
 
3854
        self._leave_lock = True
 
3855
 
 
3856
    def dont_leave_lock_in_place(self):
 
3857
        if not self._lock_token:
 
3858
            raise NotImplementedError(self.dont_leave_lock_in_place)
 
3859
        self._leave_lock = False
 
3860
 
 
3861
    def get_rev_id(self, revno, history=None):
 
3862
        if revno == 0:
 
3863
            return _mod_revision.NULL_REVISION
 
3864
        with self.lock_read():
 
3865
            last_revision_info = self.last_revision_info()
 
3866
            if revno < 0:
 
3867
                raise errors.RevnoOutOfBounds(
 
3868
                    revno, (0, last_revision_info[0]))
 
3869
            ok, result = self.repository.get_rev_id_for_revno(
 
3870
                revno, last_revision_info)
 
3871
            if ok:
 
3872
                return result
 
3873
            missing_parent = result[1]
 
3874
            # Either the revision named by the server is missing, or its parent
 
3875
            # is.  Call get_parent_map to determine which, so that we report a
 
3876
            # useful error.
 
3877
            parent_map = self.repository.get_parent_map([missing_parent])
 
3878
            if missing_parent in parent_map:
 
3879
                missing_parent = parent_map[missing_parent]
 
3880
            raise errors.NoSuchRevision(self, missing_parent)
 
3881
 
 
3882
    def _read_last_revision_info(self):
 
3883
        response = self._call(
 
3884
            b'Branch.last_revision_info', self._remote_path())
 
3885
        if response[0] != b'ok':
 
3886
            raise SmartProtocolError(
 
3887
                'unexpected response code %s' % (response,))
 
3888
        revno = int(response[1])
 
3889
        last_revision = response[2]
 
3890
        return (revno, last_revision)
 
3891
 
 
3892
    def _gen_revision_history(self):
 
3893
        """See Branch._gen_revision_history()."""
 
3894
        if self._is_stacked:
 
3895
            self._ensure_real()
 
3896
            return self._real_branch._gen_revision_history()
 
3897
        response_tuple, response_handler = self._call_expecting_body(
 
3898
            b'Branch.revision_history', self._remote_path())
 
3899
        if response_tuple[0] != b'ok':
 
3900
            raise errors.UnexpectedSmartServerResponse(response_tuple)
 
3901
        result = response_handler.read_body_bytes().split(b'\x00')
 
3902
        if result == ['']:
 
3903
            return []
 
3904
        return result
 
3905
 
 
3906
    def _remote_path(self):
 
3907
        return self.controldir._path_for_remote_call(self._client)
 
3908
 
 
3909
    def _set_last_revision_descendant(self, revision_id, other_branch,
 
3910
                                      allow_diverged=False, allow_overwrite_descendant=False):
 
3911
        # This performs additional work to meet the hook contract; while its
 
3912
        # undesirable, we have to synthesise the revno to call the hook, and
 
3913
        # not calling the hook is worse as it means changes can't be prevented.
 
3914
        # Having calculated this though, we can't just call into
 
3915
        # set_last_revision_info as a simple call, because there is a set_rh
 
3916
        # hook that some folk may still be using.
 
3917
        old_revno, old_revid = self.last_revision_info()
 
3918
        history = self._lefthand_history(revision_id)
 
3919
        self._run_pre_change_branch_tip_hooks(len(history), revision_id)
 
3920
        err_context = {'other_branch': other_branch}
 
3921
        response = self._call(b'Branch.set_last_revision_ex',
 
3922
                              self._remote_path(), self._lock_token, self._repo_lock_token,
 
3923
                              revision_id, int(allow_diverged), int(
 
3924
                                  allow_overwrite_descendant),
 
3925
                              **err_context)
 
3926
        self._clear_cached_state()
 
3927
        if len(response) != 3 and response[0] != b'ok':
 
3928
            raise errors.UnexpectedSmartServerResponse(response)
 
3929
        new_revno, new_revision_id = response[1:]
 
3930
        self._last_revision_info_cache = new_revno, new_revision_id
 
3931
        self._run_post_change_branch_tip_hooks(old_revno, old_revid)
 
3932
        if self._real_branch is not None:
 
3933
            cache = new_revno, new_revision_id
 
3934
            self._real_branch._last_revision_info_cache = cache
 
3935
 
 
3936
    def _set_last_revision(self, revision_id):
 
3937
        old_revno, old_revid = self.last_revision_info()
 
3938
        # This performs additional work to meet the hook contract; while its
 
3939
        # undesirable, we have to synthesise the revno to call the hook, and
 
3940
        # not calling the hook is worse as it means changes can't be prevented.
 
3941
        # Having calculated this though, we can't just call into
 
3942
        # set_last_revision_info as a simple call, because there is a set_rh
 
3943
        # hook that some folk may still be using.
 
3944
        history = self._lefthand_history(revision_id)
 
3945
        self._run_pre_change_branch_tip_hooks(len(history), revision_id)
 
3946
        self._clear_cached_state()
 
3947
        response = self._call(b'Branch.set_last_revision',
 
3948
                              self._remote_path(), self._lock_token, self._repo_lock_token,
 
3949
                              revision_id)
 
3950
        if response != (b'ok',):
 
3951
            raise errors.UnexpectedSmartServerResponse(response)
 
3952
        self._run_post_change_branch_tip_hooks(old_revno, old_revid)
 
3953
 
 
3954
    def _get_parent_location(self):
 
3955
        medium = self._client._medium
 
3956
        if medium._is_remote_before((1, 13)):
 
3957
            return self._vfs_get_parent_location()
 
3958
        try:
 
3959
            response = self._call(b'Branch.get_parent', self._remote_path())
 
3960
        except errors.UnknownSmartMethod:
 
3961
            medium._remember_remote_is_before((1, 13))
 
3962
            return self._vfs_get_parent_location()
 
3963
        if len(response) != 1:
 
3964
            raise errors.UnexpectedSmartServerResponse(response)
 
3965
        parent_location = response[0]
 
3966
        if parent_location == b'':
 
3967
            return None
 
3968
        return parent_location.decode('utf-8')
 
3969
 
 
3970
    def _vfs_get_parent_location(self):
 
3971
        self._ensure_real()
 
3972
        return self._real_branch._get_parent_location()
 
3973
 
 
3974
    def _set_parent_location(self, url):
 
3975
        medium = self._client._medium
 
3976
        if medium._is_remote_before((1, 15)):
 
3977
            return self._vfs_set_parent_location(url)
 
3978
        try:
 
3979
            call_url = url or u''
 
3980
            if isinstance(call_url, str):
 
3981
                call_url = call_url.encode('utf-8')
 
3982
            response = self._call(b'Branch.set_parent_location',
 
3983
                                  self._remote_path(), self._lock_token, self._repo_lock_token,
 
3984
                                  call_url)
 
3985
        except errors.UnknownSmartMethod:
 
3986
            medium._remember_remote_is_before((1, 15))
 
3987
            return self._vfs_set_parent_location(url)
 
3988
        if response != ():
 
3989
            raise errors.UnexpectedSmartServerResponse(response)
 
3990
 
 
3991
    def _vfs_set_parent_location(self, url):
 
3992
        self._ensure_real()
 
3993
        return self._real_branch._set_parent_location(url)
 
3994
 
 
3995
    def pull(self, source, overwrite=False, stop_revision=None,
 
3996
             **kwargs):
 
3997
        with self.lock_write():
 
3998
            self._clear_cached_state_of_remote_branch_only()
 
3999
            self._ensure_real()
 
4000
            return self._real_branch.pull(
 
4001
                source, overwrite=overwrite, stop_revision=stop_revision,
 
4002
                _override_hook_target=self, **kwargs)
 
4003
 
 
4004
    def push(self, target, overwrite=False, stop_revision=None, lossy=False, tag_selector=None):
 
4005
        with self.lock_read():
 
4006
            self._ensure_real()
 
4007
            return self._real_branch.push(
 
4008
                target, overwrite=overwrite, stop_revision=stop_revision, lossy=lossy,
 
4009
                _override_hook_source_branch=self, tag_selector=tag_selector)
 
4010
 
 
4011
    def peek_lock_mode(self):
 
4012
        return self._lock_mode
 
4013
 
 
4014
    def is_locked(self):
 
4015
        return self._lock_count >= 1
 
4016
 
 
4017
    def revision_id_to_dotted_revno(self, revision_id):
 
4018
        """Given a revision id, return its dotted revno.
 
4019
 
 
4020
        :return: a tuple like (1,) or (400,1,3).
 
4021
        """
 
4022
        with self.lock_read():
 
4023
            try:
 
4024
                response = self._call(b'Branch.revision_id_to_revno',
 
4025
                                      self._remote_path(), revision_id)
 
4026
            except errors.UnknownSmartMethod:
 
4027
                self._ensure_real()
 
4028
                return self._real_branch.revision_id_to_dotted_revno(revision_id)
 
4029
            except errors.UnknownErrorFromSmartServer as e:
 
4030
                # Deal with older versions of bzr/brz that didn't explicitly
 
4031
                # wrap GhostRevisionsHaveNoRevno.
 
4032
                if e.error_tuple[1] == b'GhostRevisionsHaveNoRevno':
 
4033
                    (revid, ghost_revid) = re.findall(b"{([^}]+)}", e.error_tuple[2])
 
4034
                    raise errors.GhostRevisionsHaveNoRevno(
 
4035
                        revid, ghost_revid)
 
4036
                raise
 
4037
            if response[0] == b'ok':
 
4038
                return tuple([int(x) for x in response[1:]])
 
4039
            else:
 
4040
                raise errors.UnexpectedSmartServerResponse(response)
 
4041
 
 
4042
    def revision_id_to_revno(self, revision_id):
 
4043
        """Given a revision id on the branch mainline, return its revno.
 
4044
 
 
4045
        :return: an integer
 
4046
        """
 
4047
        with self.lock_read():
 
4048
            try:
 
4049
                response = self._call(b'Branch.revision_id_to_revno',
 
4050
                                      self._remote_path(), revision_id)
 
4051
            except errors.UnknownSmartMethod:
 
4052
                self._ensure_real()
 
4053
                return self._real_branch.revision_id_to_revno(revision_id)
 
4054
            if response[0] == b'ok':
 
4055
                if len(response) == 2:
 
4056
                    return int(response[1])
 
4057
                raise NoSuchRevision(self, revision_id)
 
4058
            else:
 
4059
                raise errors.UnexpectedSmartServerResponse(response)
 
4060
 
 
4061
    def set_last_revision_info(self, revno, revision_id):
 
4062
        with self.lock_write():
 
4063
            # XXX: These should be returned by the set_last_revision_info verb
 
4064
            old_revno, old_revid = self.last_revision_info()
 
4065
            self._run_pre_change_branch_tip_hooks(revno, revision_id)
 
4066
            if not revision_id or not isinstance(revision_id, bytes):
 
4067
                raise errors.InvalidRevisionId(
 
4068
                    revision_id=revision_id, branch=self)
 
4069
            try:
 
4070
                response = self._call(b'Branch.set_last_revision_info',
 
4071
                                      self._remote_path(), self._lock_token, self._repo_lock_token,
 
4072
                                      str(revno).encode('ascii'), revision_id)
 
4073
            except errors.UnknownSmartMethod:
 
4074
                self._ensure_real()
 
4075
                self._clear_cached_state_of_remote_branch_only()
 
4076
                self._real_branch.set_last_revision_info(revno, revision_id)
 
4077
                self._last_revision_info_cache = revno, revision_id
 
4078
                return
 
4079
            if response == (b'ok',):
 
4080
                self._clear_cached_state()
 
4081
                self._last_revision_info_cache = revno, revision_id
 
4082
                self._run_post_change_branch_tip_hooks(old_revno, old_revid)
 
4083
                # Update the _real_branch's cache too.
 
4084
                if self._real_branch is not None:
 
4085
                    cache = self._last_revision_info_cache
 
4086
                    self._real_branch._last_revision_info_cache = cache
 
4087
            else:
 
4088
                raise errors.UnexpectedSmartServerResponse(response)
 
4089
 
 
4090
    def generate_revision_history(self, revision_id, last_rev=None,
 
4091
                                  other_branch=None):
 
4092
        with self.lock_write():
 
4093
            medium = self._client._medium
 
4094
            if not medium._is_remote_before((1, 6)):
 
4095
                # Use a smart method for 1.6 and above servers
 
4096
                try:
 
4097
                    self._set_last_revision_descendant(revision_id, other_branch,
 
4098
                                                       allow_diverged=True, allow_overwrite_descendant=True)
 
4099
                    return
 
4100
                except errors.UnknownSmartMethod:
 
4101
                    medium._remember_remote_is_before((1, 6))
 
4102
            self._clear_cached_state_of_remote_branch_only()
 
4103
            graph = self.repository.get_graph()
 
4104
            (last_revno, last_revid) = self.last_revision_info()
 
4105
            known_revision_ids = [
 
4106
                (last_revid, last_revno),
 
4107
                (_mod_revision.NULL_REVISION, 0),
 
4108
                ]
 
4109
            if last_rev is not None:
 
4110
                if not graph.is_ancestor(last_rev, revision_id):
 
4111
                    # our previous tip is not merged into stop_revision
 
4112
                    raise errors.DivergedBranches(self, other_branch)
 
4113
            revno = graph.find_distance_to_null(
 
4114
                revision_id, known_revision_ids)
 
4115
            self.set_last_revision_info(revno, revision_id)
 
4116
 
 
4117
    def set_push_location(self, location):
 
4118
        self._set_config_location('push_location', location)
 
4119
 
 
4120
    def heads_to_fetch(self):
 
4121
        if self._format._use_default_local_heads_to_fetch():
 
4122
            # We recognise this format, and its heads-to-fetch implementation
 
4123
            # is the default one (tip + tags).  In this case it's cheaper to
 
4124
            # just use the default implementation rather than a special RPC as
 
4125
            # the tip and tags data is cached.
 
4126
            return branch.Branch.heads_to_fetch(self)
 
4127
        medium = self._client._medium
 
4128
        if medium._is_remote_before((2, 4)):
 
4129
            return self._vfs_heads_to_fetch()
 
4130
        try:
 
4131
            return self._rpc_heads_to_fetch()
 
4132
        except errors.UnknownSmartMethod:
 
4133
            medium._remember_remote_is_before((2, 4))
 
4134
            return self._vfs_heads_to_fetch()
 
4135
 
 
4136
    def _rpc_heads_to_fetch(self):
 
4137
        response = self._call(b'Branch.heads_to_fetch', self._remote_path())
 
4138
        if len(response) != 2:
 
4139
            raise errors.UnexpectedSmartServerResponse(response)
 
4140
        must_fetch, if_present_fetch = response
 
4141
        return set(must_fetch), set(if_present_fetch)
 
4142
 
 
4143
    def _vfs_heads_to_fetch(self):
 
4144
        self._ensure_real()
 
4145
        return self._real_branch.heads_to_fetch()
 
4146
 
 
4147
    def reconcile(self, thorough=True):
 
4148
        """Make sure the data stored in this branch is consistent."""
 
4149
        from .reconcile import BranchReconciler
 
4150
        with self.lock_write():
 
4151
            reconciler = BranchReconciler(self, thorough=thorough)
 
4152
            return reconciler.reconcile()
 
4153
 
 
4154
    def get_reference_info(self, file_id):
 
4155
        """Get the tree_path and branch_location for a tree reference."""
 
4156
        if not self._format.supports_reference_locations:
 
4157
            raise errors.UnsupportedOperation(self.get_reference_info, self)
 
4158
        return self._get_all_reference_info().get(file_id, (None, None))
 
4159
 
 
4160
    def set_reference_info(self, file_id, branch_location, tree_path=None):
 
4161
        """Set the branch location to use for a tree reference."""
 
4162
        if not self._format.supports_reference_locations:
 
4163
            raise errors.UnsupportedOperation(self.set_reference_info, self)
 
4164
        self._ensure_real()
 
4165
        self._real_branch.set_reference_info(
 
4166
            file_id, branch_location, tree_path)
 
4167
 
 
4168
    def _set_all_reference_info(self, reference_info):
 
4169
        if not self._format.supports_reference_locations:
 
4170
            raise errors.UnsupportedOperation(self.set_reference_info, self)
 
4171
        self._ensure_real()
 
4172
        self._real_branch._set_all_reference_info(reference_info)
 
4173
 
 
4174
    def _get_all_reference_info(self):
 
4175
        if not self._format.supports_reference_locations:
 
4176
            return {}
 
4177
        try:
 
4178
            response, handler = self._call_expecting_body(
 
4179
                b'Branch.get_all_reference_info', self._remote_path())
 
4180
        except errors.UnknownSmartMethod:
 
4181
            self._ensure_real()
 
4182
            return self._real_branch._get_all_reference_info()
 
4183
        if len(response) and response[0] != b'ok':
 
4184
            raise errors.UnexpectedSmartServerResponse(response)
 
4185
        ret = {}
 
4186
        for (f, u, p) in bencode.bdecode(handler.read_body_bytes()):
 
4187
            ret[f] = (u.decode('utf-8'), p.decode('utf-8') if p else None)
 
4188
        return ret
 
4189
 
 
4190
    def reference_parent(self, file_id, path, possible_transports=None):
 
4191
        """Return the parent branch for a tree-reference.
 
4192
 
 
4193
        :param path: The path of the nested tree in the tree
 
4194
        :return: A branch associated with the nested tree
 
4195
        """
 
4196
        branch_location = self.get_reference_info(file_id)[0]
 
4197
        if branch_location is None:
 
4198
            try:
 
4199
                return branch.Branch.open_from_transport(
 
4200
                    self.controldir.root_transport.clone(path),
 
4201
                    possible_transports=possible_transports)
 
4202
            except errors.NotBranchError:
 
4203
                return None
 
4204
        return branch.Branch.open(
 
4205
            urlutils.join(
 
4206
                urlutils.strip_segment_parameters(self.user_url), branch_location),
 
4207
            possible_transports=possible_transports)
 
4208
 
 
4209
 
 
4210
class RemoteConfig(object):
 
4211
    """A Config that reads and writes from smart verbs.
 
4212
 
 
4213
    It is a low-level object that considers config data to be name/value pairs
 
4214
    that may be associated with a section. Assigning meaning to the these
 
4215
    values is done at higher levels like breezy.config.TreeConfig.
 
4216
    """
 
4217
 
 
4218
    def get_option(self, name, section=None, default=None):
 
4219
        """Return the value associated with a named option.
 
4220
 
 
4221
        :param name: The name of the value
 
4222
        :param section: The section the option is in (if any)
 
4223
        :param default: The value to return if the value is not set
 
4224
        :return: The value or default value
 
4225
        """
 
4226
        try:
 
4227
            configobj = self._get_configobj()
 
4228
            section_obj = None
 
4229
            if section is None:
 
4230
                section_obj = configobj
 
4231
            else:
 
4232
                try:
 
4233
                    section_obj = configobj[section]
 
4234
                except KeyError:
 
4235
                    pass
 
4236
            if section_obj is None:
 
4237
                value = default
 
4238
            else:
 
4239
                value = section_obj.get(name, default)
 
4240
        except errors.UnknownSmartMethod:
 
4241
            value = self._vfs_get_option(name, section, default)
 
4242
        for hook in _mod_config.OldConfigHooks['get']:
 
4243
            hook(self, name, value)
 
4244
        return value
 
4245
 
 
4246
    def _response_to_configobj(self, response):
 
4247
        if len(response[0]) and response[0][0] != b'ok':
 
4248
            raise errors.UnexpectedSmartServerResponse(response)
 
4249
        lines = response[1].read_body_bytes().splitlines()
 
4250
        conf = _mod_config.ConfigObj(lines, encoding='utf-8')
 
4251
        for hook in _mod_config.OldConfigHooks['load']:
 
4252
            hook(self)
 
4253
        return conf
 
4254
 
 
4255
 
 
4256
class RemoteBranchConfig(RemoteConfig):
 
4257
    """A RemoteConfig for Branches."""
 
4258
 
 
4259
    def __init__(self, branch):
 
4260
        self._branch = branch
 
4261
 
 
4262
    def _get_configobj(self):
 
4263
        path = self._branch._remote_path()
 
4264
        response = self._branch._client.call_expecting_body(
 
4265
            b'Branch.get_config_file', path)
 
4266
        return self._response_to_configobj(response)
 
4267
 
 
4268
    def set_option(self, value, name, section=None):
 
4269
        """Set the value associated with a named option.
 
4270
 
 
4271
        :param value: The value to set
 
4272
        :param name: The name of the value to set
 
4273
        :param section: The section the option is in (if any)
 
4274
        """
 
4275
        medium = self._branch._client._medium
 
4276
        if medium._is_remote_before((1, 14)):
 
4277
            return self._vfs_set_option(value, name, section)
 
4278
        if isinstance(value, dict):
 
4279
            if medium._is_remote_before((2, 2)):
 
4280
                return self._vfs_set_option(value, name, section)
 
4281
            return self._set_config_option_dict(value, name, section)
 
4282
        else:
 
4283
            return self._set_config_option(value, name, section)
 
4284
 
 
4285
    def _set_config_option(self, value, name, section):
 
4286
        if isinstance(value, (bool, int)):
 
4287
            value = str(value)
 
4288
        elif isinstance(value, str):
 
4289
            pass
 
4290
        else:
 
4291
            raise TypeError(value)
 
4292
        try:
 
4293
            path = self._branch._remote_path()
 
4294
            response = self._branch._client.call(b'Branch.set_config_option',
 
4295
                                                 path, self._branch._lock_token, self._branch._repo_lock_token,
 
4296
                                                 value.encode('utf-8'), name.encode('utf-8'),
 
4297
                                                 (section or '').encode('utf-8'))
 
4298
        except errors.UnknownSmartMethod:
 
4299
            medium = self._branch._client._medium
 
4300
            medium._remember_remote_is_before((1, 14))
 
4301
            return self._vfs_set_option(value, name, section)
 
4302
        if response != ():
 
4303
            raise errors.UnexpectedSmartServerResponse(response)
 
4304
 
 
4305
    def _serialize_option_dict(self, option_dict):
 
4306
        utf8_dict = {}
 
4307
        for key, value in option_dict.items():
 
4308
            if isinstance(key, str):
 
4309
                key = key.encode('utf8')
 
4310
            if isinstance(value, str):
 
4311
                value = value.encode('utf8')
 
4312
            utf8_dict[key] = value
 
4313
        return bencode.bencode(utf8_dict)
 
4314
 
 
4315
    def _set_config_option_dict(self, value, name, section):
 
4316
        try:
 
4317
            path = self._branch._remote_path()
 
4318
            serialised_dict = self._serialize_option_dict(value)
 
4319
            response = self._branch._client.call(
 
4320
                b'Branch.set_config_option_dict',
 
4321
                path, self._branch._lock_token, self._branch._repo_lock_token,
 
4322
                serialised_dict, name.encode('utf-8'), (section or '').encode('utf-8'))
 
4323
        except errors.UnknownSmartMethod:
 
4324
            medium = self._branch._client._medium
 
4325
            medium._remember_remote_is_before((2, 2))
 
4326
            return self._vfs_set_option(value, name, section)
 
4327
        if response != ():
 
4328
            raise errors.UnexpectedSmartServerResponse(response)
 
4329
 
 
4330
    def _real_object(self):
 
4331
        self._branch._ensure_real()
 
4332
        return self._branch._real_branch
 
4333
 
 
4334
    def _vfs_set_option(self, value, name, section=None):
 
4335
        return self._real_object()._get_config().set_option(
 
4336
            value, name, section)
 
4337
 
 
4338
 
 
4339
class RemoteBzrDirConfig(RemoteConfig):
 
4340
    """A RemoteConfig for BzrDirs."""
 
4341
 
 
4342
    def __init__(self, bzrdir):
 
4343
        self._bzrdir = bzrdir
 
4344
 
 
4345
    def _get_configobj(self):
 
4346
        medium = self._bzrdir._client._medium
 
4347
        verb = b'BzrDir.get_config_file'
 
4348
        if medium._is_remote_before((1, 15)):
 
4349
            raise errors.UnknownSmartMethod(verb)
 
4350
        path = self._bzrdir._path_for_remote_call(self._bzrdir._client)
 
4351
        response = self._bzrdir._call_expecting_body(
 
4352
            verb, path)
 
4353
        return self._response_to_configobj(response)
 
4354
 
 
4355
    def _vfs_get_option(self, name, section, default):
 
4356
        return self._real_object()._get_config().get_option(
 
4357
            name, section, default)
 
4358
 
 
4359
    def set_option(self, value, name, section=None):
 
4360
        """Set the value associated with a named option.
 
4361
 
 
4362
        :param value: The value to set
 
4363
        :param name: The name of the value to set
 
4364
        :param section: The section the option is in (if any)
 
4365
        """
 
4366
        return self._real_object()._get_config().set_option(
 
4367
            value, name, section)
 
4368
 
 
4369
    def _real_object(self):
 
4370
        self._bzrdir._ensure_real()
 
4371
        return self._bzrdir._real_bzrdir
 
4372
 
 
4373
 
 
4374
error_translators = registry.Registry()
 
4375
no_context_error_translators = registry.Registry()
 
4376
 
 
4377
 
 
4378
def _translate_error(err, **context):
 
4379
    """Translate an ErrorFromSmartServer into a more useful error.
 
4380
 
 
4381
    Possible context keys:
 
4382
      - branch
 
4383
      - repository
 
4384
      - bzrdir
 
4385
      - token
 
4386
      - other_branch
 
4387
      - path
 
4388
 
 
4389
    If the error from the server doesn't match a known pattern, then
 
4390
    UnknownErrorFromSmartServer is raised.
 
4391
    """
 
4392
    def find(name):
 
4393
        try:
 
4394
            return context[name]
 
4395
        except KeyError:
 
4396
            mutter('Missing key \'%s\' in context %r', name, context)
 
4397
            raise err
 
4398
 
 
4399
    def get_path():
 
4400
        """Get the path from the context if present, otherwise use first error
 
4401
        arg.
 
4402
        """
 
4403
        try:
 
4404
            return context['path']
 
4405
        except KeyError:
 
4406
            try:
 
4407
                return err.error_args[0].decode('utf-8')
 
4408
            except IndexError:
 
4409
                mutter('Missing key \'path\' in context %r', context)
 
4410
                raise err
 
4411
    if not isinstance(err.error_verb, bytes):
 
4412
        raise TypeError(err.error_verb)
 
4413
    try:
 
4414
        translator = error_translators.get(err.error_verb)
 
4415
    except KeyError:
 
4416
        pass
 
4417
    else:
 
4418
        raise translator(err, find, get_path)
 
4419
    try:
 
4420
        translator = no_context_error_translators.get(err.error_verb)
 
4421
    except KeyError:
 
4422
        raise errors.UnknownErrorFromSmartServer(err)
 
4423
    else:
 
4424
        raise translator(err)
 
4425
 
 
4426
 
 
4427
error_translators.register(b'NoSuchRevision',
 
4428
                           lambda err, find, get_path: NoSuchRevision(
 
4429
                               find('branch'), err.error_args[0]))
 
4430
error_translators.register(b'nosuchrevision',
 
4431
                           lambda err, find, get_path: NoSuchRevision(
 
4432
                               find('repository'), err.error_args[0]))
 
4433
error_translators.register(
 
4434
    b'revno-outofbounds',
 
4435
    lambda err, find, get_path: errors.RevnoOutOfBounds(
 
4436
        err.error_args[0], (err.error_args[1], err.error_args[2])))
 
4437
 
 
4438
 
 
4439
def _translate_nobranch_error(err, find, get_path):
 
4440
    if len(err.error_args) >= 1:
 
4441
        extra = err.error_args[0].decode('utf-8')
 
4442
    else:
 
4443
        extra = None
 
4444
    return errors.NotBranchError(path=find('bzrdir').root_transport.base,
 
4445
                                 detail=extra)
 
4446
 
 
4447
 
 
4448
error_translators.register(b'nobranch', _translate_nobranch_error)
 
4449
error_translators.register(b'norepository',
 
4450
                           lambda err, find, get_path: errors.NoRepositoryPresent(
 
4451
                               find('bzrdir')))
 
4452
error_translators.register(b'UnlockableTransport',
 
4453
                           lambda err, find, get_path: errors.UnlockableTransport(
 
4454
                               find('bzrdir').root_transport))
 
4455
error_translators.register(b'TokenMismatch',
 
4456
                           lambda err, find, get_path: errors.TokenMismatch(
 
4457
                               find('token'), '(remote token)'))
 
4458
error_translators.register(b'Diverged',
 
4459
                           lambda err, find, get_path: errors.DivergedBranches(
 
4460
                               find('branch'), find('other_branch')))
 
4461
error_translators.register(b'NotStacked',
 
4462
                           lambda err, find, get_path: errors.NotStacked(branch=find('branch')))
 
4463
 
 
4464
 
 
4465
def _translate_PermissionDenied(err, find, get_path):
 
4466
    path = get_path()
 
4467
    if len(err.error_args) >= 2:
 
4468
        extra = err.error_args[1].decode('utf-8')
 
4469
    else:
 
4470
        extra = None
 
4471
    return errors.PermissionDenied(path, extra=extra)
 
4472
 
 
4473
 
 
4474
error_translators.register(b'PermissionDenied', _translate_PermissionDenied)
 
4475
error_translators.register(b'ReadError',
 
4476
                           lambda err, find, get_path: errors.ReadError(get_path()))
 
4477
error_translators.register(b'NoSuchFile',
 
4478
                           lambda err, find, get_path: errors.NoSuchFile(get_path()))
 
4479
error_translators.register(b'TokenLockingNotSupported',
 
4480
                           lambda err, find, get_path: errors.TokenLockingNotSupported(
 
4481
                               find('repository')))
 
4482
error_translators.register(b'UnsuspendableWriteGroup',
 
4483
                           lambda err, find, get_path: errors.UnsuspendableWriteGroup(
 
4484
                               repository=find('repository')))
 
4485
error_translators.register(b'UnresumableWriteGroup',
 
4486
                           lambda err, find, get_path: errors.UnresumableWriteGroup(
 
4487
                               repository=find('repository'), write_groups=err.error_args[0],
 
4488
                               reason=err.error_args[1]))
 
4489
error_translators.register(b'AlreadyControlDir',
 
4490
                           lambda err, find, get_path: errors.AlreadyControlDirError(get_path()))
 
4491
 
 
4492
no_context_error_translators.register(b'GhostRevisionsHaveNoRevno',
 
4493
                                      lambda err: errors.GhostRevisionsHaveNoRevno(*err.error_args))
 
4494
no_context_error_translators.register(b'IncompatibleRepositories',
 
4495
                                      lambda err: errors.IncompatibleRepositories(
 
4496
                                          err.error_args[0].decode('utf-8'), err.error_args[1].decode('utf-8'), err.error_args[2].decode('utf-8')))
 
4497
no_context_error_translators.register(b'LockContention',
 
4498
                                      lambda err: errors.LockContention('(remote lock)'))
 
4499
no_context_error_translators.register(b'LockFailed',
 
4500
                                      lambda err: errors.LockFailed(err.error_args[0].decode('utf-8'), err.error_args[1].decode('utf-8')))
 
4501
no_context_error_translators.register(b'TipChangeRejected',
 
4502
                                      lambda err: errors.TipChangeRejected(err.error_args[0].decode('utf8')))
 
4503
no_context_error_translators.register(b'UnstackableBranchFormat',
 
4504
                                      lambda err: branch.UnstackableBranchFormat(*err.error_args))
 
4505
no_context_error_translators.register(b'UnstackableRepositoryFormat',
 
4506
                                      lambda err: errors.UnstackableRepositoryFormat(*err.error_args))
 
4507
no_context_error_translators.register(b'FileExists',
 
4508
                                      lambda err: errors.FileExists(err.error_args[0].decode('utf-8')))
 
4509
no_context_error_translators.register(b'DirectoryNotEmpty',
 
4510
                                      lambda err: errors.DirectoryNotEmpty(err.error_args[0].decode('utf-8')))
 
4511
no_context_error_translators.register(b'UnknownFormat',
 
4512
                                      lambda err: errors.UnknownFormatError(
 
4513
                                          err.error_args[0].decode('ascii'), err.error_args[0].decode('ascii')))
 
4514
no_context_error_translators.register(b'InvalidURL',
 
4515
                                      lambda err: urlutils.InvalidURL(
 
4516
                                          err.error_args[0].decode('utf-8'), err.error_args[1].decode('ascii')))
 
4517
 
 
4518
 
 
4519
def _translate_short_readv_error(err):
 
4520
    args = err.error_args
 
4521
    return errors.ShortReadvError(
 
4522
        args[0].decode('utf-8'),
 
4523
        int(args[1].decode('ascii')), int(args[2].decode('ascii')),
 
4524
        int(args[3].decode('ascii')))
 
4525
 
 
4526
 
 
4527
no_context_error_translators.register(b'ShortReadvError',
 
4528
                                      _translate_short_readv_error)
 
4529
 
 
4530
 
 
4531
def _translate_unicode_error(err):
 
4532
    encoding = err.error_args[0].decode('ascii')
 
4533
    val = err.error_args[1].decode('utf-8')
 
4534
    start = int(err.error_args[2].decode('ascii'))
 
4535
    end = int(err.error_args[3].decode('ascii'))
 
4536
    reason = err.error_args[4].decode('utf-8')
 
4537
    if val.startswith('u:'):
 
4538
        val = val[2:].decode('utf-8')
 
4539
    elif val.startswith('s:'):
 
4540
        val = val[2:].decode('base64')
 
4541
    if err.error_verb == 'UnicodeDecodeError':
 
4542
        raise UnicodeDecodeError(encoding, val, start, end, reason)
 
4543
    elif err.error_verb == 'UnicodeEncodeError':
 
4544
        raise UnicodeEncodeError(encoding, val, start, end, reason)
 
4545
 
 
4546
 
 
4547
no_context_error_translators.register(b'UnicodeEncodeError',
 
4548
                                      _translate_unicode_error)
 
4549
no_context_error_translators.register(b'UnicodeDecodeError',
 
4550
                                      _translate_unicode_error)
 
4551
no_context_error_translators.register(b'ReadOnlyError',
 
4552
                                      lambda err: errors.TransportNotPossible('readonly transport'))
 
4553
no_context_error_translators.register(b'MemoryError',
 
4554
                                      lambda err: errors.BzrError("remote server out of memory\n"
 
4555
                                                                  "Retry non-remotely, or contact the server admin for details."))
 
4556
no_context_error_translators.register(b'RevisionNotPresent',
 
4557
                                      lambda err: errors.RevisionNotPresent(err.error_args[0].decode('utf-8'), err.error_args[1].decode('utf-8')))
 
4558
 
 
4559
no_context_error_translators.register(b'BzrCheckError',
 
4560
                                      lambda err: errors.BzrCheckError(msg=err.error_args[0].decode('utf-8')))