/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: 2018-11-16 18:15:40 UTC
  • mto: (7143.16.20 even-more-cleanups)
  • mto: This revision was merged to the branch mainline in revision 7175.
  • Revision ID: jelmer@jelmer.uk-20181116181540-7y2wbhqzjk067mqy
Fix repo acquisition.

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