~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/remote.py

  • Committer: Aaron Bentley
  • Date: 2008-02-24 16:42:13 UTC
  • mfrom: (3234 +trunk)
  • mto: This revision was merged to the branch mainline in revision 3235.
  • Revision ID: aaron@aaronbentley.com-20080224164213-eza1lzru5bwuwmmj
Merge with bzr.dev

Show diffs side-by-side

added added

removed removed

Lines of Context:
17
17
# TODO: At some point, handle upgrades by just passing the whole request
18
18
# across to run on the server.
19
19
 
 
20
import bz2
20
21
from cStringIO import StringIO
21
22
 
22
23
from bzrlib import (
23
24
    branch,
 
25
    debug,
24
26
    errors,
 
27
    graph,
25
28
    lockdir,
26
29
    repository,
27
30
    revision,
28
31
)
29
 
from bzrlib.branch import Branch, BranchReferenceFormat
 
32
from bzrlib.branch import BranchReferenceFormat
30
33
from bzrlib.bzrdir import BzrDir, RemoteBzrDirFormat
31
34
from bzrlib.config import BranchConfig, TreeConfig
32
35
from bzrlib.decorators import needs_read_lock, needs_write_lock
33
36
from bzrlib.errors import NoSuchRevision
34
37
from bzrlib.lockable_files import LockableFiles
35
 
from bzrlib.pack import ContainerReader
 
38
from bzrlib.pack import ContainerPushParser
36
39
from bzrlib.smart import client, vfs
37
40
from bzrlib.symbol_versioning import (
38
41
    deprecated_method,
39
42
    zero_ninetyone,
40
43
    )
41
 
from bzrlib.trace import note
 
44
from bzrlib.revision import NULL_REVISION
 
45
from bzrlib.trace import mutter, note, warning
42
46
 
43
47
# Note: RemoteBzrDirFormat is in bzrdir.py
44
48
 
127
131
        else:
128
132
            raise errors.UnexpectedSmartServerResponse(response)
129
133
 
 
134
    def _get_tree_branch(self):
 
135
        """See BzrDir._get_tree_branch()."""
 
136
        return None, self.open_branch()
 
137
 
130
138
    def open_branch(self, _unsupported=False):
131
139
        assert _unsupported == False, 'unsupported flag support not implemented yet.'
132
140
        reference_url = self.get_branch_reference()
140
148
                
141
149
    def open_repository(self):
142
150
        path = self._path_for_remote_call(self._client)
143
 
        response = self._client.call('BzrDir.find_repository', path)
 
151
        verb = 'BzrDir.find_repositoryV2'
 
152
        response = self._client.call(verb, path)
 
153
        if (response == ('error', "Generic bzr smart protocol error: "
 
154
                "bad request '%s'" % verb) or
 
155
              response == ('error', "Generic bzr smart protocol error: "
 
156
                "bad request u'%s'" % verb)):
 
157
            verb = 'BzrDir.find_repository'
 
158
            response = self._client.call(verb, path)
144
159
        assert response[0] in ('ok', 'norepository'), \
145
160
            'unexpected response code %s' % (response,)
146
161
        if response[0] == 'norepository':
147
162
            raise errors.NoRepositoryPresent(self)
148
 
        assert len(response) == 4, 'incorrect response length %s' % (response,)
 
163
        if verb == 'BzrDir.find_repository':
 
164
            # servers that don't support the V2 method don't support external
 
165
            # references either.
 
166
            response = response + ('no', )
 
167
        assert len(response) == 5, 'incorrect response length %s' % (response,)
149
168
        if response[1] == '':
150
169
            format = RemoteRepositoryFormat()
151
170
            format.rich_root_data = (response[2] == 'yes')
152
171
            format.supports_tree_reference = (response[3] == 'yes')
 
172
            # No wire format to check this yet.
 
173
            format.supports_external_lookups = (response[4] == 'yes')
153
174
            return RemoteRepository(self, format)
154
175
        else:
155
176
            raise errors.NoRepositoryPresent(self)
263
284
        self._lock_token = None
264
285
        self._lock_count = 0
265
286
        self._leave_lock = False
 
287
        # A cache of looked up revision parent data; reset at unlock time.
 
288
        self._parents_map = None
 
289
        if 'hpss' in debug.debug_flags:
 
290
            self._requested_parents = None
266
291
        # For tests:
267
292
        # These depend on the actual remote format, so force them off for
268
293
        # maximum compatibility. XXX: In future these should depend on the
272
297
        self._reconcile_fixes_text_parents = False
273
298
        self._reconcile_backsup_inventory = False
274
299
        self.base = self.bzrdir.transport.base
 
300
        # Can this repository be given external locations to lookup additional
 
301
        # data.
 
302
        self.supports_external_lookups = False
275
303
 
276
304
    def __str__(self):
277
305
        return "%s(%s)" % (self.__class__.__name__, self.base)
367
395
 
368
396
    def has_revision(self, revision_id):
369
397
        """See Repository.has_revision()."""
370
 
        if revision_id is None:
 
398
        if revision_id == NULL_REVISION:
371
399
            # The null revision is always present.
372
400
            return True
373
401
        path = self.bzrdir._path_for_remote_call(self._client)
375
403
        assert response[0] in ('yes', 'no'), 'unexpected response code %s' % (response,)
376
404
        return response[0] == 'yes'
377
405
 
 
406
    def has_revisions(self, revision_ids):
 
407
        """See Repository.has_revisions()."""
 
408
        result = set()
 
409
        for revision_id in revision_ids:
 
410
            if self.has_revision(revision_id):
 
411
                result.add(revision_id)
 
412
        return result
 
413
 
378
414
    def has_same_location(self, other):
379
415
        return (self.__class__ == other.__class__ and
380
416
                self.bzrdir.transport.base == other.bzrdir.transport.base)
381
417
        
382
418
    def get_graph(self, other_repository=None):
383
419
        """Return the graph for this repository format"""
384
 
        self._ensure_real()
385
 
        return self._real_repository.get_graph(other_repository)
 
420
        parents_provider = self
 
421
        if (other_repository is not None and
 
422
            other_repository.bzrdir.transport.base !=
 
423
            self.bzrdir.transport.base):
 
424
            parents_provider = graph._StackedParentsProvider(
 
425
                [parents_provider, other_repository._make_parents_provider()])
 
426
        return graph.Graph(parents_provider)
386
427
 
387
428
    def gather_stats(self, revid=None, committers=None):
388
429
        """See Repository.gather_stats()."""
415
456
 
416
457
        return result
417
458
 
 
459
    def find_branches(self, using=False):
 
460
        """See Repository.find_branches()."""
 
461
        # should be an API call to the server.
 
462
        self._ensure_real()
 
463
        return self._real_repository.find_branches(using=using)
 
464
 
418
465
    def get_physical_lock_status(self):
419
466
        """See Repository.get_physical_lock_status()."""
420
467
        # should be an API call to the server.
447
494
        if not self._lock_mode:
448
495
            self._lock_mode = 'r'
449
496
            self._lock_count = 1
 
497
            self._parents_map = {}
 
498
            if 'hpss' in debug.debug_flags:
 
499
                self._requested_parents = set()
450
500
            if self._real_repository is not None:
451
501
                self._real_repository.lock_read()
452
502
        else:
484
534
                self._leave_lock = False
485
535
            self._lock_mode = 'w'
486
536
            self._lock_count = 1
 
537
            self._parents_map = {}
 
538
            if 'hpss' in debug.debug_flags:
 
539
                self._requested_parents = set()
487
540
        elif self._lock_mode == 'r':
488
541
            raise errors.ReadOnlyError(self)
489
542
        else:
543
596
        self._lock_count -= 1
544
597
        if self._lock_count > 0:
545
598
            return
 
599
        self._parents_map = None
 
600
        if 'hpss' in debug.debug_flags:
 
601
            self._requested_parents = None
546
602
        old_mode = self._lock_mode
547
603
        self._lock_mode = None
548
604
        try:
622
678
                committer=committer, revprops=revprops, revision_id=revision_id)
623
679
        return builder
624
680
 
625
 
    @needs_write_lock
626
681
    def add_inventory(self, revid, inv, parents):
627
682
        self._ensure_real()
628
683
        return self._real_repository.add_inventory(revid, inv, parents)
629
684
 
630
 
    @needs_write_lock
631
685
    def add_revision(self, rev_id, rev, inv=None, config=None):
632
686
        self._ensure_real()
633
687
        return self._real_repository.add_revision(
638
692
        self._ensure_real()
639
693
        return self._real_repository.get_inventory(revision_id)
640
694
 
 
695
    def iter_inventories(self, revision_ids):
 
696
        self._ensure_real()
 
697
        return self._real_repository.iter_inventories(revision_ids)
 
698
 
641
699
    @needs_read_lock
642
700
    def get_revision(self, revision_id):
643
701
        self._ensure_real()
661
719
        """RemoteRepositories never create working trees by default."""
662
720
        return False
663
721
 
 
722
    def revision_ids_to_search_result(self, result_set):
 
723
        """Convert a set of revision ids to a graph SearchResult."""
 
724
        result_parents = set()
 
725
        for parents in self.get_graph().get_parent_map(
 
726
            result_set).itervalues():
 
727
            result_parents.update(parents)
 
728
        included_keys = result_set.intersection(result_parents)
 
729
        start_keys = result_set.difference(included_keys)
 
730
        exclude_keys = result_parents.difference(result_set)
 
731
        result = graph.SearchResult(start_keys, exclude_keys,
 
732
            len(result_set), result_set)
 
733
        return result
 
734
 
 
735
    @needs_read_lock
 
736
    def search_missing_revision_ids(self, other, revision_id=None, find_ghosts=True):
 
737
        """Return the revision ids that other has that this does not.
 
738
        
 
739
        These are returned in topological order.
 
740
 
 
741
        revision_id: only return revision ids included by revision_id.
 
742
        """
 
743
        return repository.InterRepository.get(
 
744
            other, self).search_missing_revision_ids(revision_id, find_ghosts)
 
745
 
664
746
    def fetch(self, source, revision_id=None, pb=None):
665
747
        if self.has_same_location(source):
666
748
            # check that last_revision is in 'from' and then return a
707
789
        self._ensure_real()
708
790
        return self._real_repository.iter_files_bytes(desired_files)
709
791
 
 
792
    def get_parent_map(self, keys):
 
793
        """See bzrlib.Graph.get_parent_map()."""
 
794
        # Hack to build up the caching logic.
 
795
        ancestry = self._parents_map
 
796
        if ancestry is None:
 
797
            # Repository is not locked, so there's no cache.
 
798
            missing_revisions = set(keys)
 
799
            ancestry = {}
 
800
        else:
 
801
            missing_revisions = set(key for key in keys if key not in ancestry)
 
802
        if missing_revisions:
 
803
            parent_map = self._get_parent_map(missing_revisions)
 
804
            if 'hpss' in debug.debug_flags:
 
805
                mutter('retransmitted revisions: %d of %d',
 
806
                        len(set(ancestry).intersection(parent_map)),
 
807
                        len(parent_map))
 
808
            ancestry.update(parent_map)
 
809
        present_keys = [k for k in keys if k in ancestry]
 
810
        if 'hpss' in debug.debug_flags:
 
811
            self._requested_parents.update(present_keys)
 
812
            mutter('Current RemoteRepository graph hit rate: %d%%',
 
813
                100.0 * len(self._requested_parents) / len(ancestry))
 
814
        return dict((k, ancestry[k]) for k in present_keys)
 
815
 
 
816
    def _response_is_unknown_method(self, response, verb):
 
817
        """Return True if response is an unknonwn method response to verb.
 
818
        
 
819
        :param response: The response from a smart client call_expecting_body
 
820
            call.
 
821
        :param verb: The verb used in that call.
 
822
        :return: True if an unknown method was encountered.
 
823
        """
 
824
        # This might live better on
 
825
        # bzrlib.smart.protocol.SmartClientRequestProtocolOne
 
826
        if (response[0] == ('error', "Generic bzr smart protocol error: "
 
827
                "bad request '%s'" % verb) or
 
828
              response[0] == ('error', "Generic bzr smart protocol error: "
 
829
                "bad request u'%s'" % verb)):
 
830
           response[1].cancel_read_body()
 
831
           return True
 
832
        return False
 
833
 
 
834
    def _get_parent_map(self, keys):
 
835
        """Helper for get_parent_map that performs the RPC."""
 
836
        medium = self._client.get_smart_medium()
 
837
        if not medium._remote_is_at_least_1_2:
 
838
            # We already found out that the server can't understand
 
839
            # Repository.get_parent_map requests, so just fetch the whole
 
840
            # graph.
 
841
            return self.get_revision_graph()
 
842
 
 
843
        keys = set(keys)
 
844
        if NULL_REVISION in keys:
 
845
            keys.discard(NULL_REVISION)
 
846
            found_parents = {NULL_REVISION:()}
 
847
            if not keys:
 
848
                return found_parents
 
849
        else:
 
850
            found_parents = {}
 
851
        # TODO(Needs analysis): We could assume that the keys being requested
 
852
        # from get_parent_map are in a breadth first search, so typically they
 
853
        # will all be depth N from some common parent, and we don't have to
 
854
        # have the server iterate from the root parent, but rather from the
 
855
        # keys we're searching; and just tell the server the keyspace we
 
856
        # already have; but this may be more traffic again.
 
857
 
 
858
        # Transform self._parents_map into a search request recipe.
 
859
        # TODO: Manage this incrementally to avoid covering the same path
 
860
        # repeatedly. (The server will have to on each request, but the less
 
861
        # work done the better).
 
862
        parents_map = self._parents_map
 
863
        if parents_map is None:
 
864
            # Repository is not locked, so there's no cache.
 
865
            parents_map = {}
 
866
        start_set = set(parents_map)
 
867
        result_parents = set()
 
868
        for parents in parents_map.itervalues():
 
869
            result_parents.update(parents)
 
870
        stop_keys = result_parents.difference(start_set)
 
871
        included_keys = start_set.intersection(result_parents)
 
872
        start_set.difference_update(included_keys)
 
873
        recipe = (start_set, stop_keys, len(parents_map))
 
874
        body = self._serialise_search_recipe(recipe)
 
875
        path = self.bzrdir._path_for_remote_call(self._client)
 
876
        for key in keys:
 
877
            assert type(key) is str
 
878
        verb = 'Repository.get_parent_map'
 
879
        args = (path,) + tuple(keys)
 
880
        response = self._client.call_with_body_bytes_expecting_body(
 
881
            verb, args, self._serialise_search_recipe(recipe))
 
882
        if self._response_is_unknown_method(response, verb):
 
883
            # Server does not support this method, so get the whole graph.
 
884
            # Worse, we have to force a disconnection, because the server now
 
885
            # doesn't realise it has a body on the wire to consume, so the
 
886
            # only way to recover is to abandon the connection.
 
887
            warning(
 
888
                'Server is too old for fast get_parent_map, reconnecting.  '
 
889
                '(Upgrade the server to Bazaar 1.2 to avoid this)')
 
890
            medium.disconnect()
 
891
            # To avoid having to disconnect repeatedly, we keep track of the
 
892
            # fact the server doesn't understand remote methods added in 1.2.
 
893
            medium._remote_is_at_least_1_2 = False
 
894
            return self.get_revision_graph()
 
895
        elif response[0][0] not in ['ok']:
 
896
            reponse[1].cancel_read_body()
 
897
            raise errors.UnexpectedSmartServerResponse(response[0])
 
898
        if response[0][0] == 'ok':
 
899
            coded = bz2.decompress(response[1].read_body_bytes())
 
900
            if coded == '':
 
901
                # no revisions found
 
902
                return {}
 
903
            lines = coded.split('\n')
 
904
            revision_graph = {}
 
905
            for line in lines:
 
906
                d = tuple(line.split())
 
907
                if len(d) > 1:
 
908
                    revision_graph[d[0]] = d[1:]
 
909
                else:
 
910
                    # No parents - so give the Graph result (NULL_REVISION,).
 
911
                    revision_graph[d[0]] = (NULL_REVISION,)
 
912
            return revision_graph
 
913
 
710
914
    @needs_read_lock
711
915
    def get_signature_text(self, revision_id):
712
916
        self._ensure_real()
771
975
        from bzrlib import osutils
772
976
        import tarfile
773
977
        import tempfile
774
 
        from StringIO import StringIO
775
978
        # TODO: Maybe a progress bar while streaming the tarball?
776
979
        note("Copying repository content as tarball...")
777
980
        tar_file = self._get_tarball('bz2')
843
1046
        self._ensure_real()
844
1047
        return self._real_repository.has_signature_for_revision_id(revision_id)
845
1048
 
846
 
    def get_data_stream(self, revision_ids):
 
1049
    def get_data_stream_for_search(self, search):
 
1050
        medium = self._client.get_smart_medium()
 
1051
        if not medium._remote_is_at_least_1_2:
 
1052
            self._ensure_real()
 
1053
            return self._real_repository.get_data_stream_for_search(search)
 
1054
        REQUEST_NAME = 'Repository.stream_revisions_chunked'
847
1055
        path = self.bzrdir._path_for_remote_call(self._client)
848
 
        response, protocol = self._client.call_expecting_body(
849
 
            'Repository.stream_knit_data_for_revisions', path, *revision_ids)
 
1056
        body = self._serialise_search_recipe(search.get_recipe())
 
1057
        response, protocol = self._client.call_with_body_bytes_expecting_body(
 
1058
            REQUEST_NAME, (path,), body)
 
1059
 
 
1060
        if self._response_is_unknown_method((response, protocol), REQUEST_NAME):
 
1061
            # Server does not support this method, so fall back to VFS.
 
1062
            # Worse, we have to force a disconnection, because the server now
 
1063
            # doesn't realise it has a body on the wire to consume, so the
 
1064
            # only way to recover is to abandon the connection.
 
1065
            warning(
 
1066
                'Server is too old for streaming pull, reconnecting.  '
 
1067
                '(Upgrade the server to Bazaar 1.2 to avoid this)')
 
1068
            medium.disconnect()
 
1069
            # To avoid having to disconnect repeatedly, we keep track of the
 
1070
            # fact the server doesn't understand this remote method.
 
1071
            medium._remote_is_at_least_1_2 = False
 
1072
            self._ensure_real()
 
1073
            return self._real_repository.get_data_stream_for_search(search)
 
1074
 
850
1075
        if response == ('ok',):
851
1076
            return self._deserialise_stream(protocol)
852
 
        elif (response == ('error', "Generic bzr smart protocol error: "
853
 
                "bad request 'Repository.stream_knit_data_for_revisions'") or
854
 
              response == ('error', "Generic bzr smart protocol error: "
855
 
                "bad request u'Repository.stream_knit_data_for_revisions'")):
856
 
            protocol.cancel_read_body()
857
 
            self._ensure_real()
858
 
            return self._real_repository.get_data_stream(revision_ids)
 
1077
        if response == ('NoSuchRevision', ):
 
1078
            # We cannot easily identify the revision that is missing in this
 
1079
            # situation without doing much more network IO. For now, bail.
 
1080
            raise NoSuchRevision(self, "unknown")
859
1081
        else:
860
1082
            raise errors.UnexpectedSmartServerResponse(response)
861
1083
 
862
1084
    def _deserialise_stream(self, protocol):
863
 
        buffer = StringIO(protocol.read_body_bytes())
864
 
        reader = ContainerReader(buffer)
865
 
        for record_names, read_bytes in reader.iter_records():
866
 
            try:
867
 
                # These records should have only one name, and that name
868
 
                # should be a one-element tuple.
869
 
                [name_tuple] = record_names
870
 
            except ValueError:
871
 
                raise errors.SmartProtocolError(
872
 
                    'Repository data stream had invalid record name %r'
873
 
                    % (record_names,))
874
 
            yield name_tuple, read_bytes(None)
 
1085
        stream = protocol.read_streamed_body()
 
1086
        container_parser = ContainerPushParser()
 
1087
        for bytes in stream:
 
1088
            container_parser.accept_bytes(bytes)
 
1089
            records = container_parser.read_pending_records()
 
1090
            for record_names, record_bytes in records:
 
1091
                if len(record_names) != 1:
 
1092
                    # These records should have only one name, and that name
 
1093
                    # should be a one-element tuple.
 
1094
                    raise errors.SmartProtocolError(
 
1095
                        'Repository data stream had invalid record name %r'
 
1096
                        % (record_names,))
 
1097
                name_tuple = record_names[0]
 
1098
                yield name_tuple, record_bytes
875
1099
 
876
1100
    def insert_data_stream(self, stream):
877
1101
        self._ensure_real()
896
1120
        return self._real_repository._check_for_inconsistent_revision_parents()
897
1121
 
898
1122
    def _make_parents_provider(self):
899
 
        self._ensure_real()
900
 
        return self._real_repository._make_parents_provider()
 
1123
        return self
 
1124
 
 
1125
    def _serialise_search_recipe(self, recipe):
 
1126
        """Serialise a graph search recipe.
 
1127
 
 
1128
        :param recipe: A search recipe (start, stop, count).
 
1129
        :return: Serialised bytes.
 
1130
        """
 
1131
        start_keys = ' '.join(recipe[0])
 
1132
        stop_keys = ' '.join(recipe[1])
 
1133
        count = str(recipe[2])
 
1134
        return '\n'.join((start_keys, stop_keys, count))
901
1135
 
902
1136
 
903
1137
class RemoteBranchLockableFiles(LockableFiles):