~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/repository.py

  • Committer: Andrew Bennetts
  • Date: 2007-11-10 15:09:09 UTC
  • mfrom: (2916.2.17 streamable-containers)
  • mto: This revision was merged to the branch mainline in revision 3174.
  • Revision ID: andrew.bennetts@canonical.com-20071110150909-ik5254kgn930th10
Merge streamable-containers.

Show diffs side-by-side

added added

removed removed

Lines of Context:
116
116
            self._timezone = int(timezone)
117
117
 
118
118
        self._generate_revision_if_needed()
119
 
        self._repo_graph = repository.get_graph()
 
119
        self._heads = graph.HeadsCache(repository.get_graph()).heads
120
120
 
121
121
    def commit(self, message):
122
122
        """Make the actual commit.
285
285
        # XXX: Friction: parent_candidates should return a list not a dict
286
286
        #      so that we don't have to walk the inventories again.
287
287
        parent_candiate_entries = ie.parent_candidates(parent_invs)
288
 
        head_set = self._repo_graph.heads(parent_candiate_entries.keys())
 
288
        head_set = self._heads(parent_candiate_entries.keys())
289
289
        heads = []
290
290
        for inv in parent_invs:
291
291
            if ie.file_id in inv:
318
318
            if kind != parent_entry.kind:
319
319
                store = True
320
320
        if kind == 'file':
 
321
            assert content_summary[2] is not None, \
 
322
                "Files must not have executable = None"
321
323
            if not store:
322
324
                if (# if the file length changed we have to store:
323
325
                    parent_entry.text_size != content_summary[1] or
492
494
 
493
495
        returns the sha1 of the serialized inventory.
494
496
        """
 
497
        assert self.is_in_write_group()
495
498
        _mod_revision.check_not_reserved_id(revision_id)
496
499
        assert inv.revision_id is None or inv.revision_id == revision_id, \
497
500
            "Mismatch between inventory revision" \
613
616
        # for tests
614
617
        self._reconcile_does_inventory_gc = True
615
618
        self._reconcile_fixes_text_parents = False
 
619
        self._reconcile_backsup_inventory = True
616
620
        # not right yet - should be more semantically clear ? 
617
621
        # 
618
622
        self.control_store = control_store
756
760
        raise NotImplementedError(self.get_data_stream)
757
761
 
758
762
    def insert_data_stream(self, stream):
 
763
        """XXX What does this really do? 
 
764
        
 
765
        Is it a substitute for fetch? 
 
766
        Should it manage its own write group ?
 
767
        """
759
768
        for item_key, bytes in stream:
760
769
            if item_key[0] == 'file':
761
770
                (file_id,) = item_key[1:]
832
841
        extent possible considering file system caching etc).
833
842
        """
834
843
 
835
 
    def fetch(self, source, revision_id=None, pb=None):
 
844
    def fetch(self, source, revision_id=None, pb=None, find_ghosts=False):
836
845
        """Fetch the content required to construct revision_id from source.
837
846
 
838
847
        If revision_id is None all content is copied.
 
848
        :param find_ghosts: Find and copy revisions in the source that are
 
849
            ghosts in the target (and not reachable directly by walking out to
 
850
            the first-present revision in target from revision_id).
839
851
        """
840
852
        # fast path same-url fetch operations
841
853
        if self.has_same_location(source):
847
859
            return 0, []
848
860
        inter = InterRepository.get(source, self)
849
861
        try:
850
 
            return inter.fetch(revision_id=revision_id, pb=pb)
 
862
            return inter.fetch(revision_id=revision_id, pb=pb, find_ghosts=find_ghosts)
851
863
        except NotImplementedError:
852
864
            raise errors.IncompatibleRepositories(source, self)
853
865
 
877
889
        if (self.control_files._lock_count == 1 and
878
890
            self.control_files._lock_mode == 'w'):
879
891
            if self._write_group is not None:
 
892
                self.abort_write_group()
 
893
                self.control_files.unlock()
880
894
                raise errors.BzrError(
881
895
                    'Must end write groups before releasing write locks.')
882
896
        self.control_files.unlock()
1041
1055
                                                         signature,
1042
1056
                                                         self.get_transaction())
1043
1057
 
1044
 
    def fileids_altered_by_revision_ids(self, revision_ids):
1045
 
        """Find the file ids and versions affected by revisions.
1046
 
 
1047
 
        :param revisions: an iterable containing revision ids.
 
1058
    def _find_file_ids_from_xml_inventory_lines(self, line_iterator,
 
1059
        revision_ids):
 
1060
        """Helper routine for fileids_altered_by_revision_ids.
 
1061
 
 
1062
        This performs the translation of xml lines to revision ids.
 
1063
 
 
1064
        :param line_iterator: An iterator of lines
 
1065
        :param revision_ids: The revision ids to filter for. This should be a
 
1066
            set or other type which supports efficient __contains__ lookups, as
 
1067
            the revision id from each parsed line will be looked up in the
 
1068
            revision_ids filter.
1048
1069
        :return: a dictionary mapping altered file-ids to an iterable of
1049
1070
        revision_ids. Each altered file-ids has the exact revision_ids that
1050
1071
        altered it listed explicitly.
1051
1072
        """
1052
 
        assert self._serializer.support_altered_by_hack, \
1053
 
            ("fileids_altered_by_revision_ids only supported for branches " 
1054
 
             "which store inventory as unnested xml, not on %r" % self)
1055
 
        selected_revision_ids = set(revision_ids)
1056
 
        w = self.get_inventory_weave()
1057
1073
        result = {}
1058
1074
 
1059
1075
        # this code needs to read every new line in every inventory for the
1075
1091
        search = self._file_ids_altered_regex.search
1076
1092
        unescape = _unescape_xml
1077
1093
        setdefault = result.setdefault
 
1094
        for line in line_iterator:
 
1095
            match = search(line)
 
1096
            if match is None:
 
1097
                continue
 
1098
            # One call to match.group() returning multiple items is quite a
 
1099
            # bit faster than 2 calls to match.group() each returning 1
 
1100
            file_id, revision_id = match.group('file_id', 'revision_id')
 
1101
 
 
1102
            # Inlining the cache lookups helps a lot when you make 170,000
 
1103
            # lines and 350k ids, versus 8.4 unique ids.
 
1104
            # Using a cache helps in 2 ways:
 
1105
            #   1) Avoids unnecessary decoding calls
 
1106
            #   2) Re-uses cached strings, which helps in future set and
 
1107
            #      equality checks.
 
1108
            # (2) is enough that removing encoding entirely along with
 
1109
            # the cache (so we are using plain strings) results in no
 
1110
            # performance improvement.
 
1111
            try:
 
1112
                revision_id = unescape_revid_cache[revision_id]
 
1113
            except KeyError:
 
1114
                unescaped = unescape(revision_id)
 
1115
                unescape_revid_cache[revision_id] = unescaped
 
1116
                revision_id = unescaped
 
1117
 
 
1118
            if revision_id in revision_ids:
 
1119
                try:
 
1120
                    file_id = unescape_fileid_cache[file_id]
 
1121
                except KeyError:
 
1122
                    unescaped = unescape(file_id)
 
1123
                    unescape_fileid_cache[file_id] = unescaped
 
1124
                    file_id = unescaped
 
1125
                setdefault(file_id, set()).add(revision_id)
 
1126
        return result
 
1127
 
 
1128
    def fileids_altered_by_revision_ids(self, revision_ids):
 
1129
        """Find the file ids and versions affected by revisions.
 
1130
 
 
1131
        :param revisions: an iterable containing revision ids.
 
1132
        :return: a dictionary mapping altered file-ids to an iterable of
 
1133
        revision_ids. Each altered file-ids has the exact revision_ids that
 
1134
        altered it listed explicitly.
 
1135
        """
 
1136
        assert self._serializer.support_altered_by_hack, \
 
1137
            ("fileids_altered_by_revision_ids only supported for branches " 
 
1138
             "which store inventory as unnested xml, not on %r" % self)
 
1139
        selected_revision_ids = set(revision_ids)
 
1140
        w = self.get_inventory_weave()
1078
1141
        pb = ui.ui_factory.nested_progress_bar()
1079
1142
        try:
1080
 
            for line in w.iter_lines_added_or_present_in_versions(
1081
 
                                        selected_revision_ids, pb=pb):
1082
 
                match = search(line)
1083
 
                if match is None:
1084
 
                    continue
1085
 
                # One call to match.group() returning multiple items is quite a
1086
 
                # bit faster than 2 calls to match.group() each returning 1
1087
 
                file_id, revision_id = match.group('file_id', 'revision_id')
1088
 
 
1089
 
                # Inlining the cache lookups helps a lot when you make 170,000
1090
 
                # lines and 350k ids, versus 8.4 unique ids.
1091
 
                # Using a cache helps in 2 ways:
1092
 
                #   1) Avoids unnecessary decoding calls
1093
 
                #   2) Re-uses cached strings, which helps in future set and
1094
 
                #      equality checks.
1095
 
                # (2) is enough that removing encoding entirely along with
1096
 
                # the cache (so we are using plain strings) results in no
1097
 
                # performance improvement.
1098
 
                try:
1099
 
                    revision_id = unescape_revid_cache[revision_id]
1100
 
                except KeyError:
1101
 
                    unescaped = unescape(revision_id)
1102
 
                    unescape_revid_cache[revision_id] = unescaped
1103
 
                    revision_id = unescaped
1104
 
 
1105
 
                if revision_id in selected_revision_ids:
1106
 
                    try:
1107
 
                        file_id = unescape_fileid_cache[file_id]
1108
 
                    except KeyError:
1109
 
                        unescaped = unescape(file_id)
1110
 
                        unescape_fileid_cache[file_id] = unescaped
1111
 
                        file_id = unescaped
1112
 
                    setdefault(file_id, set()).add(revision_id)
 
1143
            return self._find_file_ids_from_xml_inventory_lines(
 
1144
                w.iter_lines_added_or_present_in_versions(
 
1145
                    selected_revision_ids, pb=pb),
 
1146
                selected_revision_ids)
1113
1147
        finally:
1114
1148
            pb.finished()
1115
 
        return result
1116
1149
 
1117
1150
    def iter_files_bytes(self, desired_files):
1118
1151
        """Iterate through file versions.
1593
1626
 
1594
1627
def install_revision(repository, rev, revision_tree):
1595
1628
    """Install all revision data into a repository."""
 
1629
    repository.start_write_group()
 
1630
    try:
 
1631
        _install_revision(repository, rev, revision_tree)
 
1632
    except:
 
1633
        repository.abort_write_group()
 
1634
        raise
 
1635
    else:
 
1636
        repository.commit_write_group()
 
1637
 
 
1638
 
 
1639
def _install_revision(repository, rev, revision_tree):
 
1640
    """Install all revision data into a repository."""
1596
1641
    present_parents = []
1597
1642
    parent_trees = {}
1598
1643
    for p_id in rev.parent_ids:
1730
1775
    parameterisation.
1731
1776
    """
1732
1777
 
 
1778
    # Set to True or False in derived classes. True indicates that the format
 
1779
    # supports ghosts gracefully.
 
1780
    supports_ghosts = None
 
1781
 
1733
1782
    def __str__(self):
1734
1783
        return "<%s>" % self.__class__.__name__
1735
1784
 
1932
1981
    'RepositoryFormatKnit3',
1933
1982
    )
1934
1983
 
 
1984
# Pack-based formats. There is one format for pre-subtrees, and one for
 
1985
# post-subtrees to allow ease of testing.
 
1986
# NOTE: These are experimental in 0.92.
 
1987
format_registry.register_lazy(
 
1988
    'Bazaar pack repository format 1 (needs bzr 0.92)\n',
 
1989
    'bzrlib.repofmt.pack_repo',
 
1990
    'RepositoryFormatKnitPack1',
 
1991
    )
 
1992
format_registry.register_lazy(
 
1993
    'Bazaar pack repository format 1 with subtree support (needs bzr 0.92)\n',
 
1994
    'bzrlib.repofmt.pack_repo',
 
1995
    'RepositoryFormatKnitPack3',
 
1996
    )
 
1997
 
1935
1998
 
1936
1999
class InterRepository(InterObject):
1937
2000
    """This class represents operations taking place between two repositories.
1951
2014
    def copy_content(self, revision_id=None):
1952
2015
        raise NotImplementedError(self.copy_content)
1953
2016
 
1954
 
    def fetch(self, revision_id=None, pb=None):
 
2017
    def fetch(self, revision_id=None, pb=None, find_ghosts=False):
1955
2018
        """Fetch the content required to construct revision_id.
1956
2019
 
1957
2020
        The content is copied from self.source to self.target.
2043
2106
        self.target.fetch(self.source, revision_id=revision_id)
2044
2107
 
2045
2108
    @needs_write_lock
2046
 
    def fetch(self, revision_id=None, pb=None):
 
2109
    def fetch(self, revision_id=None, pb=None, find_ghosts=False):
2047
2110
        """See InterRepository.fetch()."""
2048
2111
        from bzrlib.fetch import GenericRepoFetcher
2049
2112
        mutter("Using fetch logic to copy between %s(%s) and %s(%s)",
2122
2185
            self.target.fetch(self.source, revision_id=revision_id)
2123
2186
 
2124
2187
    @needs_write_lock
2125
 
    def fetch(self, revision_id=None, pb=None):
 
2188
    def fetch(self, revision_id=None, pb=None, find_ghosts=False):
2126
2189
        """See InterRepository.fetch()."""
2127
2190
        from bzrlib.fetch import GenericRepoFetcher
2128
2191
        mutter("Using fetch logic to copy between %s(%s) and %s(%s)",
2200
2263
        return are_knits and InterRepository._same_model(source, target)
2201
2264
 
2202
2265
    @needs_write_lock
2203
 
    def fetch(self, revision_id=None, pb=None):
 
2266
    def fetch(self, revision_id=None, pb=None, find_ghosts=False):
2204
2267
        """See InterRepository.fetch()."""
2205
2268
        from bzrlib.fetch import KnitRepoFetcher
2206
2269
        mutter("Using fetch logic to copy between %s(%s) and %s(%s)",
2242
2305
            return self.source._eliminate_revisions_not_present(required_topo_revisions)
2243
2306
 
2244
2307
 
 
2308
class InterPackRepo(InterSameDataRepository):
 
2309
    """Optimised code paths between Pack based repositories."""
 
2310
 
 
2311
    @classmethod
 
2312
    def _get_repo_format_to_test(self):
 
2313
        from bzrlib.repofmt import pack_repo
 
2314
        return pack_repo.RepositoryFormatKnitPack1()
 
2315
 
 
2316
    @staticmethod
 
2317
    def is_compatible(source, target):
 
2318
        """Be compatible with known Pack formats.
 
2319
        
 
2320
        We don't test for the stores being of specific types because that
 
2321
        could lead to confusing results, and there is no need to be 
 
2322
        overly general.
 
2323
        """
 
2324
        from bzrlib.repofmt.pack_repo import RepositoryFormatPack
 
2325
        try:
 
2326
            are_packs = (isinstance(source._format, RepositoryFormatPack) and
 
2327
                isinstance(target._format, RepositoryFormatPack))
 
2328
        except AttributeError:
 
2329
            return False
 
2330
        return are_packs and InterRepository._same_model(source, target)
 
2331
 
 
2332
    @needs_write_lock
 
2333
    def fetch(self, revision_id=None, pb=None, find_ghosts=False):
 
2334
        """See InterRepository.fetch()."""
 
2335
        from bzrlib.repofmt.pack_repo import Packer
 
2336
        mutter("Using fetch logic to copy between %s(%s) and %s(%s)",
 
2337
               self.source, self.source._format, self.target, self.target._format)
 
2338
        self.count_copied = 0
 
2339
        if revision_id is None:
 
2340
            # TODO:
 
2341
            # everything to do - use pack logic
 
2342
            # to fetch from all packs to one without
 
2343
            # inventory parsing etc, IFF nothing to be copied is in the target.
 
2344
            # till then:
 
2345
            revision_ids = self.source.all_revision_ids()
 
2346
            # implementing the TODO will involve:
 
2347
            # - detecting when all of a pack is selected
 
2348
            # - avoiding as much as possible pre-selection, so the
 
2349
            # more-core routines such as create_pack_from_packs can filter in
 
2350
            # a just-in-time fashion. (though having a HEADS list on a
 
2351
            # repository might make this a lot easier, because we could
 
2352
            # sensibly detect 'new revisions' without doing a full index scan.
 
2353
        elif _mod_revision.is_null(revision_id):
 
2354
            # nothing to do:
 
2355
            return
 
2356
        else:
 
2357
            try:
 
2358
                revision_ids = self.missing_revision_ids(revision_id,
 
2359
                    find_ghosts=find_ghosts)
 
2360
            except errors.NoSuchRevision:
 
2361
                raise errors.InstallFailed([revision_id])
 
2362
        packs = self.source._pack_collection.all_packs()
 
2363
        pack = Packer(self.target._pack_collection, packs, '.fetch',
 
2364
            revision_ids).pack()
 
2365
        if pack is not None:
 
2366
            self.target._pack_collection._save_pack_names()
 
2367
            # Trigger an autopack. This may duplicate effort as we've just done
 
2368
            # a pack creation, but for now it is simpler to think about as
 
2369
            # 'upload data, then repack if needed'.
 
2370
            self.target._pack_collection.autopack()
 
2371
            return pack.get_revision_count()
 
2372
        else:
 
2373
            return 0
 
2374
 
 
2375
    @needs_read_lock
 
2376
    def missing_revision_ids(self, revision_id=None, find_ghosts=True):
 
2377
        """See InterRepository.missing_revision_ids().
 
2378
        
 
2379
        :param find_ghosts: Find ghosts throughough the ancestry of
 
2380
            revision_id.
 
2381
        """
 
2382
        if not find_ghosts and revision_id is not None:
 
2383
            graph = self.source.get_graph()
 
2384
            missing_revs = set()
 
2385
            searcher = graph._make_breadth_first_searcher([revision_id])
 
2386
            target_index = \
 
2387
                self.target._pack_collection.revision_index.combined_index
 
2388
            null_set = frozenset([_mod_revision.NULL_REVISION])
 
2389
            while True:
 
2390
                try:
 
2391
                    next_revs = set(searcher.next())
 
2392
                except StopIteration:
 
2393
                    break
 
2394
                next_revs.difference_update(null_set)
 
2395
                target_keys = [(key,) for key in next_revs]
 
2396
                have_revs = frozenset(node[1][0] for node in
 
2397
                    target_index.iter_entries(target_keys))
 
2398
                missing_revs.update(next_revs - have_revs)
 
2399
                searcher.stop_searching_any(have_revs)
 
2400
            return missing_revs
 
2401
        elif revision_id is not None:
 
2402
            source_ids = self.source.get_ancestry(revision_id)
 
2403
            assert source_ids[0] is None
 
2404
            source_ids.pop(0)
 
2405
        else:
 
2406
            source_ids = self.source.all_revision_ids()
 
2407
        # source_ids is the worst possible case we may need to pull.
 
2408
        # now we want to filter source_ids against what we actually
 
2409
        # have in target, but don't try to check for existence where we know
 
2410
        # we do not have a revision as that would be pointless.
 
2411
        target_ids = set(self.target.all_revision_ids())
 
2412
        return [r for r in source_ids if (r not in target_ids)]
 
2413
 
 
2414
 
2245
2415
class InterModel1and2(InterRepository):
2246
2416
 
2247
2417
    @classmethod
2256
2426
            return False
2257
2427
 
2258
2428
    @needs_write_lock
2259
 
    def fetch(self, revision_id=None, pb=None):
 
2429
    def fetch(self, revision_id=None, pb=None, find_ghosts=False):
2260
2430
        """See InterRepository.fetch()."""
2261
2431
        from bzrlib.fetch import Model1toKnit2Fetcher
2262
2432
        f = Model1toKnit2Fetcher(to_repository=self.target,
2297
2467
        """Be compatible with Knit1 source and Knit3 target"""
2298
2468
        from bzrlib.repofmt.knitrepo import RepositoryFormatKnit3
2299
2469
        try:
2300
 
            from bzrlib.repofmt.knitrepo import RepositoryFormatKnit1, \
2301
 
                    RepositoryFormatKnit3
2302
 
            return (isinstance(source._format, (RepositoryFormatKnit1)) and
2303
 
                    isinstance(target._format, (RepositoryFormatKnit3)))
 
2470
            from bzrlib.repofmt.knitrepo import (RepositoryFormatKnit1,
 
2471
                RepositoryFormatKnit3)
 
2472
            from bzrlib.repofmt.pack_repo import (RepositoryFormatKnitPack1,
 
2473
                RepositoryFormatKnitPack3)
 
2474
            return (isinstance(source._format,
 
2475
                    (RepositoryFormatKnit1, RepositoryFormatKnitPack1)) and
 
2476
                isinstance(target._format,
 
2477
                    (RepositoryFormatKnit3, RepositoryFormatKnitPack3))
 
2478
                )
2304
2479
        except AttributeError:
2305
2480
            return False
2306
2481
 
2307
2482
    @needs_write_lock
2308
 
    def fetch(self, revision_id=None, pb=None):
 
2483
    def fetch(self, revision_id=None, pb=None, find_ghosts=False):
2309
2484
        """See InterRepository.fetch()."""
2310
2485
        from bzrlib.fetch import Knit1to2Fetcher
2311
2486
        mutter("Using fetch logic to copy between %s(%s) and %s(%s)",
2338
2513
        return real_source._format == target._format
2339
2514
 
2340
2515
    @needs_write_lock
2341
 
    def fetch(self, revision_id=None, pb=None):
 
2516
    def fetch(self, revision_id=None, pb=None, find_ghosts=False):
2342
2517
        """See InterRepository.fetch()."""
2343
2518
        from bzrlib.fetch import RemoteToOtherFetcher
2344
2519
        mutter("Using fetch logic to copy between %s(remote) and %s(%s)",
2378
2553
        self._ensure_real_inter()
2379
2554
        self._real_inter.copy_content(revision_id=revision_id)
2380
2555
 
2381
 
    def fetch(self, revision_id=None, pb=None):
 
2556
    def fetch(self, revision_id=None, pb=None, find_ghosts=False):
2382
2557
        self._ensure_real_inter()
2383
2558
        self._real_inter.fetch(revision_id=revision_id, pb=pb)
2384
2559
 
2392
2567
InterRepository.register_optimiser(InterKnitRepo)
2393
2568
InterRepository.register_optimiser(InterModel1and2)
2394
2569
InterRepository.register_optimiser(InterKnit1and2)
 
2570
InterRepository.register_optimiser(InterPackRepo)
2395
2571
InterRepository.register_optimiser(InterRemoteToOther)
2396
2572
InterRepository.register_optimiser(InterOtherToRemote)
2397
2573
 
2486
2662
        self.revision_versions = {}
2487
2663
        self.revision_parents = {}
2488
2664
        self.repo_graph = self.repository.get_graph()
2489
 
        self.rev_heads = {}
 
2665
        # XXX: RBC: I haven't tracked down what uses this, but it would be
 
2666
        # better to use the headscache directly I think.
 
2667
        self.heads = graph.HeadsCache(self.repo_graph).heads
2490
2668
 
2491
2669
    def add_revision_text_versions(self, tree):
2492
2670
        """Cache text version data from the supplied revision tree"""
2517
2695
        # XXX: this loop is very similar to
2518
2696
        # bzrlib.fetch.Inter1and2Helper.iter_rev_trees.
2519
2697
        while revs:
 
2698
            mutter('%d revisions left to prepopulate', len(revs))
2520
2699
            for tree in self.repository.revision_trees(revs[:100]):
2521
2700
                if tree.inventory.revision_id is None:
2522
2701
                    tree.inventory.revision_id = tree.get_revision_id()
2531
2710
            self.revision_parents[revision_id] = parents
2532
2711
            return parents
2533
2712
 
2534
 
    def heads(self, revision_ids):
2535
 
        revision_ids = tuple(revision_ids)
2536
 
        try:
2537
 
            return self.rev_heads[revision_ids]
2538
 
        except KeyError:
2539
 
            heads = self.repo_graph.heads(revision_ids)
2540
 
            self.rev_heads[revision_ids] = heads
2541
 
            return heads
 
2713
    def used_file_versions(self):
 
2714
        """Return a set of (revision_id, file_id) pairs for each file version
 
2715
        referenced by any inventory cached by this _RevisionTextVersionCache.
 
2716
 
 
2717
        If the entire repository has been cached, this can be used to find all
 
2718
        file versions that are actually referenced by inventories.  Thus any
 
2719
        other file version is completely unused and can be removed safely.
 
2720
        """
 
2721
        result = set()
 
2722
        for inventory_summary in self.revision_versions.itervalues():
 
2723
            result.update(inventory_summary.items())
 
2724
        return result
 
2725
 
2542
2726
 
2543
2727
class VersionedFileChecker(object):
2544
2728
 
2548
2732
        self.repository = repository
2549
2733
    
2550
2734
    def calculate_file_version_parents(self, revision_id, file_id):
 
2735
        """Calculate the correct parents for a file version according to
 
2736
        the inventories.
 
2737
        """
2551
2738
        text_revision = self.revision_versions.get_text_version(
2552
2739
            file_id, revision_id)
2553
2740
        if text_revision is None:
2567
2754
        for parent in parents_from_inventories:
2568
2755
            if parent in heads and parent not in new_parents:
2569
2756
                new_parents.append(parent)
2570
 
        return new_parents
 
2757
        return tuple(new_parents)
2571
2758
 
2572
2759
    def check_file_version_parents(self, weave, file_id):
2573
 
        result = {}
 
2760
        """Check the parents stored in a versioned file are correct.
 
2761
 
 
2762
        It also detects file versions that are not referenced by their
 
2763
        corresponding revision's inventory.
 
2764
 
 
2765
        :returns: A tuple of (wrong_parents, dangling_file_versions).
 
2766
            wrong_parents is a dict mapping {revision_id: (stored_parents,
 
2767
            correct_parents)} for each revision_id where the stored parents
 
2768
            are not correct.  dangling_file_versions is a set of (file_id,
 
2769
            revision_id) tuples for versions that are present in this versioned
 
2770
            file, but not used by the corresponding inventory.
 
2771
        """
 
2772
        wrong_parents = {}
 
2773
        dangling_file_versions = set()
2574
2774
        for num, revision_id in enumerate(self.planned_revisions):
2575
2775
            correct_parents = self.calculate_file_version_parents(
2576
2776
                revision_id, file_id)
2578
2778
                continue
2579
2779
            text_revision = self.revision_versions.get_text_version(
2580
2780
                file_id, revision_id)
2581
 
            knit_parents = weave.get_parents(text_revision)
 
2781
            try:
 
2782
                knit_parents = tuple(weave.get_parents(revision_id))
 
2783
            except errors.RevisionNotPresent:
 
2784
                knit_parents = None
 
2785
            if text_revision != revision_id:
 
2786
                # This file version is not referenced by its corresponding
 
2787
                # inventory!
 
2788
                dangling_file_versions.add((file_id, revision_id))
2582
2789
            if correct_parents != knit_parents:
2583
 
                result[revision_id] = (knit_parents, correct_parents)
2584
 
        return result
 
2790
                wrong_parents[revision_id] = (knit_parents, correct_parents)
 
2791
        return wrong_parents, dangling_file_versions