~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tree.py

  • Committer: Robert Collins
  • Date: 2009-07-29 04:57:50 UTC
  • mto: This revision was merged to the branch mainline in revision 4649.
  • Revision ID: robertc@robertcollins.net-20090729045750-qp1iw48rp7jyrfh2
Change the way iter_changes treats specific files to prevent InconsistentDeltas.

This is the internal core code that handles specific file operations like ``bzr st
FILENAME`` or ``bzr commit FILENAME``, and has been changed to include the
parent directories if they have altered, and when a directory stops being
a directory its children are always included.  This fixes a number of
causes for ``InconsistentDelta`` errors, and permits faster commit of
specific paths. (Robert Collins, #347649)

Show diffs side-by-side

added added

removed removed

Lines of Context:
170
170
        return self.bzrdir.is_control_filename(filename)
171
171
 
172
172
    @needs_read_lock
173
 
    def iter_entries_by_dir(self, specific_file_ids=None):
 
173
    def iter_entries_by_dir(self, specific_file_ids=None, yield_parents=False):
174
174
        """Walk the tree in 'by_dir' order.
175
175
 
176
176
        This will yield each entry in the tree as a (path, entry) tuple.
193
193
 
194
194
        The yield order (ignoring root) would be::
195
195
          a, f, a/b, a/d, a/b/c, a/d/e, f/g
 
196
 
 
197
        :param yield_parents: If True, yield the parents from the root leading
 
198
            down to specific_file_ids that have been requested. This has no
 
199
            impact if specific_file_ids is None.
196
200
        """
197
201
        return self.inventory.iter_entries_by_dir(
198
 
            specific_file_ids=specific_file_ids)
 
202
            specific_file_ids=specific_file_ids, yield_parents=yield_parents)
199
203
 
200
204
    def iter_references(self):
201
205
        if self.supports_tree_reference():
846
850
 
847
851
    _optimisers = []
848
852
 
 
853
    def _changes_from_entries(self, source_entry, target_entry,
 
854
        source_path=None, target_path=None):
 
855
        """Generate a iter_changes tuple between source_entry and target_entry.
 
856
 
 
857
        :param source_entry: An inventory entry from self.source, or None.
 
858
        :param target_entry: An inventory entry from self.target, or None.
 
859
        :param source_path: The path of source_entry, if known. If not known
 
860
            it will be looked up.
 
861
        :param target_path: The path of target_entry, if known. If not known
 
862
            it will be looked up.
 
863
        :return: A tuple, item 0 of which is an iter_changes result tuple, and
 
864
            item 1 is True if there are any changes in the result tuple.
 
865
        """
 
866
        if source_entry is None:
 
867
            if target_entry is None:
 
868
                return None
 
869
            file_id = target_entry.file_id
 
870
        else:
 
871
            file_id = source_entry.file_id
 
872
        if source_entry is not None:
 
873
            source_versioned = True
 
874
            source_name = source_entry.name
 
875
            source_parent = source_entry.parent_id
 
876
            if source_path is None:
 
877
                source_path = self.source.id2path(file_id)
 
878
            source_kind, source_executable, source_stat = \
 
879
                self.source._comparison_data(source_entry, source_path)
 
880
        else:
 
881
            source_versioned = False
 
882
            source_name = None
 
883
            source_parent = None
 
884
            source_kind = None
 
885
            source_executable = None
 
886
        if target_entry is not None:
 
887
            target_versioned = True
 
888
            target_name = target_entry.name
 
889
            target_parent = target_entry.parent_id
 
890
            if target_path is None:
 
891
                target_path = self.target.id2path(file_id)
 
892
            target_kind, target_executable, target_stat = \
 
893
                self.target._comparison_data(target_entry, target_path)
 
894
        else:
 
895
            target_versioned = False
 
896
            target_name = None
 
897
            target_parent = None
 
898
            target_kind = None
 
899
            target_executable = None
 
900
        versioned = (source_versioned, target_versioned)
 
901
        kind = (source_kind, target_kind)
 
902
        changed_content = False
 
903
        if source_kind != target_kind:
 
904
            changed_content = True
 
905
        elif source_kind == 'file':
 
906
            if (self.source.get_file_sha1(file_id, source_path, source_stat) !=
 
907
                self.target.get_file_sha1(file_id, target_path, target_stat)):
 
908
                changed_content = True
 
909
        elif source_kind == 'symlink':
 
910
            if (self.source.get_symlink_target(file_id) !=
 
911
                self.target.get_symlink_target(file_id)):
 
912
                changed_content = True
 
913
            # XXX: Yes, the indentation below is wrong. But fixing it broke
 
914
            # test_merge.TestMergerEntriesLCAOnDisk.
 
915
            # test_nested_tree_subtree_renamed_and_modified. We'll wait for
 
916
            # the fix from bzr.dev -- vila 2009026
 
917
            elif source_kind == 'tree-reference':
 
918
                if (self.source.get_reference_revision(file_id, source_path)
 
919
                    != self.target.get_reference_revision(file_id, target_path)):
 
920
                    changed_content = True
 
921
        parent = (source_parent, target_parent)
 
922
        name = (source_name, target_name)
 
923
        executable = (source_executable, target_executable)
 
924
        if (changed_content is not False or versioned[0] != versioned[1]
 
925
            or parent[0] != parent[1] or name[0] != name[1] or
 
926
            executable[0] != executable[1]):
 
927
            changes = True
 
928
        else:
 
929
            changes = False
 
930
        return (file_id, (source_path, target_path), changed_content,
 
931
                versioned, parent, name, kind, executable), changes
 
932
 
849
933
    @needs_read_lock
850
934
    def compare(self, want_unchanged=False, specific_files=None,
851
935
        extra_trees=None, require_versioned=False, include_root=False,
914
998
        :param require_versioned: Raise errors.PathsNotVersionedError if a
915
999
            path in the specific_files list is not versioned in one of
916
1000
            source, target or extra_trees.
 
1001
        :param specific_files: An optional list of file paths to restrict the
 
1002
            comparison to. When mapping filenames to ids, all matches in all
 
1003
            trees (including optional extra_trees) are used, and all children
 
1004
            of matched directories are included. The parents in the target tree
 
1005
            of the specific files up to and including the root of the tree are
 
1006
            always evaluated for changes too.
917
1007
        :param want_unversioned: Should unversioned files be returned in the
918
1008
            output. An unversioned file is defined as one with (False, False)
919
1009
            for the versioned pair.
921
1011
        lookup_trees = [self.source]
922
1012
        if extra_trees:
923
1013
             lookup_trees.extend(extra_trees)
 
1014
        # The ids of items we need to examine to insure delta consistency.
 
1015
        precise_file_ids = set()
 
1016
        changed_file_ids = []
924
1017
        if specific_files == []:
925
1018
            specific_file_ids = []
926
1019
        else:
927
1020
            specific_file_ids = self.target.paths2ids(specific_files,
928
1021
                lookup_trees, require_versioned=require_versioned)
 
1022
        if specific_files is not None:
 
1023
            # reparented or added entries must have their parents included
 
1024
            # so that valid deltas can be created. The seen_parents set
 
1025
            # tracks the parents that we need to have.
 
1026
            # The seen_dirs set tracks directory entries we've yielded.
 
1027
            # After outputting version object in to_entries we set difference
 
1028
            # the two seen sets and start checking parents.
 
1029
            seen_parents = set()
 
1030
            seen_dirs = set()
929
1031
        if want_unversioned:
930
1032
            all_unversioned = sorted([(p.split('/'), p) for p in
931
1033
                                     self.target.extras()
946
1048
        # can be extras. So the fake_entry is solely used to look up
947
1049
        # executable it values when execute is not supported.
948
1050
        fake_entry = InventoryFile('unused', 'unused', 'unused')
949
 
        for to_path, to_entry in to_entries_by_dir:
950
 
            while all_unversioned and all_unversioned[0][0] < to_path.split('/'):
 
1051
        for target_path, target_entry in to_entries_by_dir:
 
1052
            while (all_unversioned and
 
1053
                all_unversioned[0][0] < target_path.split('/')):
951
1054
                unversioned_path = all_unversioned.popleft()
952
 
                to_kind, to_executable, to_stat = \
 
1055
                target_kind, target_executable, target_stat = \
953
1056
                    self.target._comparison_data(fake_entry, unversioned_path[1])
954
1057
                yield (None, (None, unversioned_path[1]), True, (False, False),
955
1058
                    (None, None),
956
1059
                    (None, unversioned_path[0][-1]),
957
 
                    (None, to_kind),
958
 
                    (None, to_executable))
959
 
            file_id = to_entry.file_id
960
 
            to_paths[file_id] = to_path
 
1060
                    (None, target_kind),
 
1061
                    (None, target_executable))
 
1062
            source_path, source_entry = from_data.get(target_entry.file_id,
 
1063
                (None, None))
 
1064
            result, changes = self._changes_from_entries(source_entry,
 
1065
                target_entry, source_path=source_path, target_path=target_path)
 
1066
            to_paths[result[0]] = result[1][1]
961
1067
            entry_count += 1
962
 
            changed_content = False
963
 
            from_path, from_entry = from_data.get(file_id, (None, None))
964
 
            from_versioned = (from_entry is not None)
965
 
            if from_entry is not None:
966
 
                from_versioned = True
967
 
                from_name = from_entry.name
968
 
                from_parent = from_entry.parent_id
969
 
                from_kind, from_executable, from_stat = \
970
 
                    self.source._comparison_data(from_entry, from_path)
 
1068
            if result[3][0]:
971
1069
                entry_count += 1
972
 
            else:
973
 
                from_versioned = False
974
 
                from_kind = None
975
 
                from_parent = None
976
 
                from_name = None
977
 
                from_executable = None
978
 
            versioned = (from_versioned, True)
979
 
            to_kind, to_executable, to_stat = \
980
 
                self.target._comparison_data(to_entry, to_path)
981
 
            kind = (from_kind, to_kind)
982
 
            if kind[0] != kind[1]:
983
 
                changed_content = True
984
 
            elif from_kind == 'file':
985
 
                if (self.source.get_file_sha1(file_id, from_path, from_stat) !=
986
 
                    self.target.get_file_sha1(file_id, to_path, to_stat)):
987
 
                    changed_content = True
988
 
            elif from_kind == 'symlink':
989
 
                if (self.source.get_symlink_target(file_id) !=
990
 
                    self.target.get_symlink_target(file_id)):
991
 
                    changed_content = True
992
 
                # XXX: Yes, the indentation below is wrong. But fixing it broke
993
 
                # test_merge.TestMergerEntriesLCAOnDisk.
994
 
                # test_nested_tree_subtree_renamed_and_modified. We'll wait for
995
 
                # the fix from bzr.dev -- vila 2009026
996
 
                elif from_kind == 'tree-reference':
997
 
                    if (self.source.get_reference_revision(file_id, from_path)
998
 
                        != self.target.get_reference_revision(file_id, to_path)):
999
 
                        changed_content = True
1000
 
            parent = (from_parent, to_entry.parent_id)
1001
 
            name = (from_name, to_entry.name)
1002
 
            executable = (from_executable, to_executable)
1003
1070
            if pb is not None:
1004
1071
                pb.update('comparing files', entry_count, num_entries)
1005
 
            if (changed_content is not False or versioned[0] != versioned[1]
1006
 
                or parent[0] != parent[1] or name[0] != name[1] or
1007
 
                executable[0] != executable[1] or include_unchanged):
1008
 
                yield (file_id, (from_path, to_path), changed_content,
1009
 
                    versioned, parent, name, kind, executable)
1010
 
 
 
1072
            if changes or include_unchanged:
 
1073
                if specific_file_ids is not None:
 
1074
                    new_parent_id = result[4][1]
 
1075
                    precise_file_ids.add(new_parent_id)
 
1076
                    changed_file_ids.append(result[0])
 
1077
                yield result
 
1078
            # Ensure correct behaviour for reparented/added specific files.
 
1079
            if specific_files is not None:
 
1080
                # Record output dirs
 
1081
                if result[6][1] == 'directory':
 
1082
                    seen_dirs.add(result[0])
 
1083
                # Record parents of reparented/added entries.
 
1084
                versioned = result[3]
 
1085
                parents = result[4]
 
1086
                if not versioned[0] or parents[0] != parents[1]:
 
1087
                    seen_parents.add(parents[1])
1011
1088
        while all_unversioned:
1012
1089
            # yield any trailing unversioned paths
1013
1090
            unversioned_path = all_unversioned.popleft()
1018
1095
                (None, unversioned_path[0][-1]),
1019
1096
                (None, to_kind),
1020
1097
                (None, to_executable))
1021
 
 
1022
 
        def get_to_path(to_entry):
1023
 
            if to_entry.parent_id is None:
1024
 
                to_path = '' # the root
1025
 
            else:
1026
 
                if to_entry.parent_id not in to_paths:
1027
 
                    # recurse up
1028
 
                    return get_to_path(self.target.inventory[to_entry.parent_id])
1029
 
                to_path = osutils.pathjoin(to_paths[to_entry.parent_id],
1030
 
                                           to_entry.name)
1031
 
            to_paths[to_entry.file_id] = to_path
1032
 
            return to_path
1033
 
 
 
1098
        # Yield all remaining source paths
1034
1099
        for path, from_entry in from_entries_by_dir:
1035
1100
            file_id = from_entry.file_id
1036
1101
            if file_id in to_paths:
1037
1102
                # already returned
1038
1103
                continue
1039
 
            if not file_id in self.target.all_file_ids():
 
1104
            if file_id not in self.target.all_file_ids():
1040
1105
                # common case - paths we have not emitted are not present in
1041
1106
                # target.
1042
1107
                to_path = None
1043
1108
            else:
1044
 
                to_path = get_to_path(self.target.inventory[file_id])
 
1109
                to_path = self.target.id2path(file_id)
1045
1110
            entry_count += 1
1046
1111
            if pb is not None:
1047
1112
                pb.update('comparing files', entry_count, num_entries)
1054
1119
            executable = (from_executable, None)
1055
1120
            changed_content = from_kind is not None
1056
1121
            # the parent's path is necessarily known at this point.
 
1122
            changed_file_ids.append(file_id)
1057
1123
            yield(file_id, (path, to_path), changed_content, versioned, parent,
1058
1124
                  name, kind, executable)
 
1125
        changed_file_ids = set(changed_file_ids)
 
1126
        if specific_file_ids is not None:
 
1127
            for result in self._handle_precise_ids(precise_file_ids,
 
1128
                changed_file_ids):
 
1129
                yield result
 
1130
 
 
1131
    def _get_entry(self, tree, file_id):
 
1132
        """Get an inventory entry from a tree, with missing entries as None.
 
1133
 
 
1134
        If the tree raises NotImplementedError on accessing .inventory, then
 
1135
        this is worked around using iter_entries_by_dir on just the file id
 
1136
        desired.
 
1137
 
 
1138
        :param tree: The tree to lookup the entry in.
 
1139
        :param file_id: The file_id to lookup.
 
1140
        """
 
1141
        try:
 
1142
            inventory = tree.inventory
 
1143
        except NotImplementedError:
 
1144
            # No inventory available.
 
1145
            try:
 
1146
                iterator = tree.iter_entries_by_dir(specific_file_ids=[file_id])
 
1147
                return iterator.next()[1]
 
1148
            except StopIteration:
 
1149
                return None
 
1150
        else:
 
1151
            try:
 
1152
                return inventory[file_id]
 
1153
            except errors.NoSuchId:
 
1154
                return None
 
1155
 
 
1156
    def _handle_precise_ids(self, precise_file_ids, changed_file_ids,
 
1157
        discarded_changes=None):
 
1158
        """Fill out a partial iter_changes to be consistent.
 
1159
 
 
1160
        :param precise_file_ids: The file ids of parents that were seen during
 
1161
            the iter_changes.
 
1162
        :param changed_file_ids: The file ids of already emitted items.
 
1163
        :param discarded_changes: An optional dict of precalculated
 
1164
            iter_changes items which the partial iter_changes had not output
 
1165
            but had calculated.
 
1166
        :return: A generator of iter_changes items to output.
 
1167
        """
 
1168
        # process parents of things that had changed under the users
 
1169
        # requested paths to prevent incorrect paths or parent ids which
 
1170
        # aren't in the tree.
 
1171
        while precise_file_ids:
 
1172
            precise_file_ids.discard(None)
 
1173
            # Don't emit file_ids twice
 
1174
            precise_file_ids.difference_update(changed_file_ids)
 
1175
            if not precise_file_ids:
 
1176
                break
 
1177
            # If the there was something at a given output path in source, we
 
1178
            # have to include the entry from source in the delta, or we would
 
1179
            # be putting this entry into a used path.
 
1180
            paths = []
 
1181
            for parent_id in precise_file_ids:
 
1182
                try:
 
1183
                    paths.append(self.target.id2path(parent_id))
 
1184
                except errors.NoSuchId:
 
1185
                    # This id has been dragged in from the source by delta
 
1186
                    # expansion and isn't present in target at all: we don't
 
1187
                    # need to check for path collisions on it.
 
1188
                    pass
 
1189
            for path in paths:
 
1190
                old_id = self.source.path2id(path)
 
1191
                precise_file_ids.add(old_id)
 
1192
            precise_file_ids.discard(None)
 
1193
            current_ids = precise_file_ids
 
1194
            precise_file_ids = set()
 
1195
            # We have to emit all of precise_file_ids that have been altered.
 
1196
            # We may have to output the children of some of those ids if any
 
1197
            # directories have stopped being directories.
 
1198
            for file_id in current_ids:
 
1199
                # Examine file_id
 
1200
                if discarded_changes:
 
1201
                    result = discarded_changes.get(file_id)
 
1202
                    old_entry = None
 
1203
                else:
 
1204
                    result = None
 
1205
                if result is None:
 
1206
                    old_entry = self._get_entry(self.source, file_id)
 
1207
                    new_entry = self._get_entry(self.target, file_id)
 
1208
                    result, changes = self._changes_from_entries(
 
1209
                        old_entry, new_entry)
 
1210
                else:
 
1211
                    changes = True
 
1212
                if changes:
 
1213
                    # Get this parents parent.
 
1214
                    new_parent_id = result[4][1]
 
1215
                    precise_file_ids.add(new_parent_id)
 
1216
                    if (result[6][0] == 'directory' and
 
1217
                        result[6][1] != 'directory'):
 
1218
                        # This stopped being a directory, the old children have
 
1219
                        # to be included.
 
1220
                        if old_entry is None:
 
1221
                            # Reusing a discarded change.
 
1222
                            old_entry = self._get_entry(self.source, file_id)
 
1223
                        for child in old_entry.children.values():
 
1224
                            precise_file_ids.add(child.file_id)
 
1225
                    changed_file_ids.add(result[0])
 
1226
                    yield result
1059
1227
 
1060
1228
 
1061
1229
class MultiWalker(object):