/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/tree.py

  • Committer: Jelmer Vernooij
  • Date: 2020-03-22 20:02:36 UTC
  • mto: (7490.7.7 work)
  • mto: This revision was merged to the branch mainline in revision 7501.
  • Revision ID: jelmer@jelmer.uk-20200322200236-fsbl91ktcn6fcbdd
Fix tests.

Show diffs side-by-side

added added

removed removed

Lines of Context:
17
17
"""Tree classes, representing directory at point in time.
18
18
"""
19
19
 
 
20
from __future__ import absolute_import
 
21
 
 
22
try:
 
23
    from collections.abc import deque
 
24
except ImportError:  # python < 3.7
 
25
    from collections import deque
 
26
 
20
27
from .lazy_import import lazy_import
21
28
lazy_import(globals(), """
 
29
 
 
30
from breezy import (
 
31
    conflicts as _mod_conflicts,
 
32
    debug,
 
33
    delta,
 
34
    filters,
 
35
    revision as _mod_revision,
 
36
    rules,
 
37
    trace,
 
38
    )
22
39
from breezy.i18n import gettext
23
40
""")
24
41
 
26
43
    errors,
27
44
    lock,
28
45
    osutils,
29
 
    revision as _mod_revision,
30
 
    trace,
31
46
    )
32
47
from .inter import InterObject
 
48
from .sixish import (
 
49
    text_type,
 
50
    viewvalues,
 
51
    )
33
52
 
34
53
 
35
54
class FileTimestampUnavailable(errors.BzrError):
256
275
 
257
276
        Each conflict is an instance of breezy.conflicts.Conflict.
258
277
        """
259
 
        from . import conflicts as _mod_conflicts
260
278
        return _mod_conflicts.ConflictList()
261
279
 
262
280
    def extras(self):
676
694
            or None if unknown
677
695
        :return: the list of filters - [] if there are none
678
696
        """
679
 
        from . import debug, filters
680
697
        filter_pref_names = filters._get_registered_names()
681
698
        if len(filter_pref_names) == 0:
682
699
            return []
711
728
        :return: an iterator of tuple sequences, one per path-name.
712
729
          See _RulesSearcher.get_items for details on the tuple sequence.
713
730
        """
714
 
        from . import rules
715
731
        if _default_searcher is None:
716
732
            _default_searcher = rules._per_user_searcher
717
733
        searcher = self._get_rules_searcher(_default_searcher)
748
764
        """Check if this tree support versioning a specific file kind."""
749
765
        return (kind in ('file', 'directory', 'symlink', 'tree-reference'))
750
766
 
751
 
    def preview_transform(self, pb=None):
752
 
        """Obtain a transform object."""
753
 
        raise NotImplementedError(self.preview_transform)
754
 
 
755
767
 
756
768
class InterTree(InterObject):
757
769
    """This class represents operations taking place between two Trees.
778
790
        # it works for all trees.
779
791
        return True
780
792
 
 
793
    def _changes_from_entries(self, source_entry, target_entry, source_path,
 
794
                              target_path):
 
795
        """Generate a iter_changes tuple between source_entry and target_entry.
 
796
 
 
797
        :param source_entry: An inventory entry from self.source, or None.
 
798
        :param target_entry: An inventory entry from self.target, or None.
 
799
        :param source_path: The path of source_entry.
 
800
        :param target_path: The path of target_entry.
 
801
        :return: A tuple, item 0 of which is an iter_changes result tuple, and
 
802
            item 1 is True if there are any changes in the result tuple.
 
803
        """
 
804
        if source_entry is None:
 
805
            if target_entry is None:
 
806
                return None
 
807
            file_id = target_entry.file_id
 
808
        else:
 
809
            file_id = source_entry.file_id
 
810
        if source_entry is not None:
 
811
            source_versioned = True
 
812
            source_name = source_entry.name
 
813
            source_parent = source_entry.parent_id
 
814
            source_kind, source_executable, source_stat = \
 
815
                self.source._comparison_data(source_entry, source_path)
 
816
        else:
 
817
            source_versioned = False
 
818
            source_name = None
 
819
            source_parent = None
 
820
            source_kind = None
 
821
            source_executable = None
 
822
        if target_entry is not None:
 
823
            target_versioned = True
 
824
            target_name = target_entry.name
 
825
            target_parent = target_entry.parent_id
 
826
            target_kind, target_executable, target_stat = \
 
827
                self.target._comparison_data(target_entry, target_path)
 
828
        else:
 
829
            target_versioned = False
 
830
            target_name = None
 
831
            target_parent = None
 
832
            target_kind = None
 
833
            target_executable = None
 
834
        versioned = (source_versioned, target_versioned)
 
835
        kind = (source_kind, target_kind)
 
836
        changed_content = False
 
837
        if source_kind != target_kind:
 
838
            changed_content = True
 
839
        elif source_kind == 'file':
 
840
            if not self.file_content_matches(
 
841
                    source_path, target_path,
 
842
                    source_stat, target_stat):
 
843
                changed_content = True
 
844
        elif source_kind == 'symlink':
 
845
            if (self.source.get_symlink_target(source_path) !=
 
846
                    self.target.get_symlink_target(target_path)):
 
847
                changed_content = True
 
848
        elif source_kind == 'tree-reference':
 
849
            if (self.source.get_reference_revision(source_path)
 
850
                    != self.target.get_reference_revision(target_path)):
 
851
                changed_content = True
 
852
        parent = (source_parent, target_parent)
 
853
        name = (source_name, target_name)
 
854
        executable = (source_executable, target_executable)
 
855
        if (changed_content is not False or versioned[0] != versioned[1] or
 
856
            parent[0] != parent[1] or name[0] != name[1] or
 
857
                executable[0] != executable[1]):
 
858
            changes = True
 
859
        else:
 
860
            changes = False
 
861
        return TreeChange(
 
862
            file_id, (source_path, target_path), changed_content,
 
863
            versioned, parent, name, kind, executable), changes
 
864
 
781
865
    def compare(self, want_unchanged=False, specific_files=None,
782
866
                extra_trees=None, require_versioned=False, include_root=False,
783
867
                want_unversioned=False):
797
881
            a PathsNotVersionedError will be thrown.
798
882
        :param want_unversioned: Scan for unversioned paths.
799
883
        """
800
 
        from . import delta
801
884
        trees = (self.source,)
802
885
        if extra_trees is not None:
803
886
            trees = trees + tuple(extra_trees)
843
926
            output. An unversioned file is defined as one with (False, False)
844
927
            for the versioned pair.
845
928
        """
846
 
        raise NotImplementedError(self.iter_changes)
 
929
        if not extra_trees:
 
930
            extra_trees = []
 
931
        else:
 
932
            extra_trees = list(extra_trees)
 
933
        # The ids of items we need to examine to insure delta consistency.
 
934
        precise_file_ids = set()
 
935
        changed_file_ids = []
 
936
        if specific_files == []:
 
937
            target_specific_files = []
 
938
            source_specific_files = []
 
939
        else:
 
940
            target_specific_files = self.target.find_related_paths_across_trees(
 
941
                specific_files, [self.source] + extra_trees,
 
942
                require_versioned=require_versioned)
 
943
            source_specific_files = self.source.find_related_paths_across_trees(
 
944
                specific_files, [self.target] + extra_trees,
 
945
                require_versioned=require_versioned)
 
946
        if specific_files is not None:
 
947
            # reparented or added entries must have their parents included
 
948
            # so that valid deltas can be created. The seen_parents set
 
949
            # tracks the parents that we need to have.
 
950
            # The seen_dirs set tracks directory entries we've yielded.
 
951
            # After outputting version object in to_entries we set difference
 
952
            # the two seen sets and start checking parents.
 
953
            seen_parents = set()
 
954
            seen_dirs = set()
 
955
        if want_unversioned:
 
956
            all_unversioned = sorted([(p.split('/'), p) for p in
 
957
                                      self.target.extras()
 
958
                                      if specific_files is None or
 
959
                                      osutils.is_inside_any(specific_files, p)])
 
960
            all_unversioned = deque(all_unversioned)
 
961
        else:
 
962
            all_unversioned = deque()
 
963
        to_paths = {}
 
964
        from_entries_by_dir = list(self.source.iter_entries_by_dir(
 
965
            specific_files=source_specific_files))
 
966
        from_data = dict(from_entries_by_dir)
 
967
        to_entries_by_dir = list(self.target.iter_entries_by_dir(
 
968
            specific_files=target_specific_files))
 
969
        path_equivs = self.find_source_paths([p for p, e in to_entries_by_dir])
 
970
        num_entries = len(from_entries_by_dir) + len(to_entries_by_dir)
 
971
        entry_count = 0
 
972
        # the unversioned path lookup only occurs on real trees - where there
 
973
        # can be extras. So the fake_entry is solely used to look up
 
974
        # executable it values when execute is not supported.
 
975
        fake_entry = TreeFile()
 
976
        for target_path, target_entry in to_entries_by_dir:
 
977
            while (all_unversioned and
 
978
                   all_unversioned[0][0] < target_path.split('/')):
 
979
                unversioned_path = all_unversioned.popleft()
 
980
                target_kind, target_executable, target_stat = \
 
981
                    self.target._comparison_data(
 
982
                        fake_entry, unversioned_path[1])
 
983
                yield TreeChange(
 
984
                    None, (None, unversioned_path[1]), True, (False, False),
 
985
                    (None, None),
 
986
                    (None, unversioned_path[0][-1]),
 
987
                    (None, target_kind),
 
988
                    (None, target_executable))
 
989
            source_path = path_equivs[target_path]
 
990
            if source_path is not None:
 
991
                source_entry = from_data.get(source_path)
 
992
            else:
 
993
                source_entry = None
 
994
            result, changes = self._changes_from_entries(
 
995
                source_entry, target_entry, source_path=source_path, target_path=target_path)
 
996
            to_paths[result.file_id] = result.path[1]
 
997
            entry_count += 1
 
998
            if result.versioned[0]:
 
999
                entry_count += 1
 
1000
            if pb is not None:
 
1001
                pb.update('comparing files', entry_count, num_entries)
 
1002
            if changes or include_unchanged:
 
1003
                if specific_files is not None:
 
1004
                    precise_file_ids.add(result.parent_id[1])
 
1005
                    changed_file_ids.append(result.file_id)
 
1006
                yield result
 
1007
            # Ensure correct behaviour for reparented/added specific files.
 
1008
            if specific_files is not None:
 
1009
                # Record output dirs
 
1010
                if result.kind[1] == 'directory':
 
1011
                    seen_dirs.add(result.file_id)
 
1012
                # Record parents of reparented/added entries.
 
1013
                if not result.versioned[0] or result.is_reparented():
 
1014
                    seen_parents.add(result.parent_id[1])
 
1015
        while all_unversioned:
 
1016
            # yield any trailing unversioned paths
 
1017
            unversioned_path = all_unversioned.popleft()
 
1018
            to_kind, to_executable, to_stat = \
 
1019
                self.target._comparison_data(fake_entry, unversioned_path[1])
 
1020
            yield TreeChange(
 
1021
                None, (None, unversioned_path[1]), True, (False, False),
 
1022
                (None, None),
 
1023
                (None, unversioned_path[0][-1]),
 
1024
                (None, to_kind),
 
1025
                (None, to_executable))
 
1026
        # Yield all remaining source paths
 
1027
        for path, from_entry in from_entries_by_dir:
 
1028
            file_id = from_entry.file_id
 
1029
            if file_id in to_paths:
 
1030
                # already returned
 
1031
                continue
 
1032
            to_path = self.find_target_path(path)
 
1033
            entry_count += 1
 
1034
            if pb is not None:
 
1035
                pb.update('comparing files', entry_count, num_entries)
 
1036
            versioned = (True, False)
 
1037
            parent = (from_entry.parent_id, None)
 
1038
            name = (from_entry.name, None)
 
1039
            from_kind, from_executable, stat_value = \
 
1040
                self.source._comparison_data(from_entry, path)
 
1041
            kind = (from_kind, None)
 
1042
            executable = (from_executable, None)
 
1043
            changed_content = from_kind is not None
 
1044
            # the parent's path is necessarily known at this point.
 
1045
            changed_file_ids.append(file_id)
 
1046
            yield TreeChange(
 
1047
                file_id, (path, to_path), changed_content, versioned, parent,
 
1048
                name, kind, executable)
 
1049
        changed_file_ids = set(changed_file_ids)
 
1050
        if specific_files is not None:
 
1051
            for result in self._handle_precise_ids(precise_file_ids,
 
1052
                                                   changed_file_ids):
 
1053
                yield result
 
1054
 
 
1055
    @staticmethod
 
1056
    def _get_entry(tree, path):
 
1057
        """Get an inventory entry from a tree, with missing entries as None.
 
1058
 
 
1059
        If the tree raises NotImplementedError on accessing .inventory, then
 
1060
        this is worked around using iter_entries_by_dir on just the file id
 
1061
        desired.
 
1062
 
 
1063
        :param tree: The tree to lookup the entry in.
 
1064
        :param path: The path to look up
 
1065
        """
 
1066
        # No inventory available.
 
1067
        try:
 
1068
            iterator = tree.iter_entries_by_dir(specific_files=[path])
 
1069
            return next(iterator)[1]
 
1070
        except StopIteration:
 
1071
            return None
 
1072
 
 
1073
    def _handle_precise_ids(self, precise_file_ids, changed_file_ids,
 
1074
                            discarded_changes=None):
 
1075
        """Fill out a partial iter_changes to be consistent.
 
1076
 
 
1077
        :param precise_file_ids: The file ids of parents that were seen during
 
1078
            the iter_changes.
 
1079
        :param changed_file_ids: The file ids of already emitted items.
 
1080
        :param discarded_changes: An optional dict of precalculated
 
1081
            iter_changes items which the partial iter_changes had not output
 
1082
            but had calculated.
 
1083
        :return: A generator of iter_changes items to output.
 
1084
        """
 
1085
        # process parents of things that had changed under the users
 
1086
        # requested paths to prevent incorrect paths or parent ids which
 
1087
        # aren't in the tree.
 
1088
        while precise_file_ids:
 
1089
            precise_file_ids.discard(None)
 
1090
            # Don't emit file_ids twice
 
1091
            precise_file_ids.difference_update(changed_file_ids)
 
1092
            if not precise_file_ids:
 
1093
                break
 
1094
            # If the there was something at a given output path in source, we
 
1095
            # have to include the entry from source in the delta, or we would
 
1096
            # be putting this entry into a used path.
 
1097
            paths = []
 
1098
            for parent_id in precise_file_ids:
 
1099
                try:
 
1100
                    paths.append(self.target.id2path(parent_id))
 
1101
                except errors.NoSuchId:
 
1102
                    # This id has been dragged in from the source by delta
 
1103
                    # expansion and isn't present in target at all: we don't
 
1104
                    # need to check for path collisions on it.
 
1105
                    pass
 
1106
            for path in paths:
 
1107
                old_id = self.source.path2id(path)
 
1108
                precise_file_ids.add(old_id)
 
1109
            precise_file_ids.discard(None)
 
1110
            current_ids = precise_file_ids
 
1111
            precise_file_ids = set()
 
1112
            # We have to emit all of precise_file_ids that have been altered.
 
1113
            # We may have to output the children of some of those ids if any
 
1114
            # directories have stopped being directories.
 
1115
            for file_id in current_ids:
 
1116
                # Examine file_id
 
1117
                if discarded_changes:
 
1118
                    result = discarded_changes.get(file_id)
 
1119
                    source_entry = None
 
1120
                else:
 
1121
                    result = None
 
1122
                if result is None:
 
1123
                    try:
 
1124
                        source_path = self.source.id2path(file_id)
 
1125
                    except errors.NoSuchId:
 
1126
                        source_path = None
 
1127
                        source_entry = None
 
1128
                    else:
 
1129
                        source_entry = self._get_entry(
 
1130
                            self.source, source_path)
 
1131
                    try:
 
1132
                        target_path = self.target.id2path(file_id)
 
1133
                    except errors.NoSuchId:
 
1134
                        target_path = None
 
1135
                        target_entry = None
 
1136
                    else:
 
1137
                        target_entry = self._get_entry(
 
1138
                            self.target, target_path)
 
1139
                    result, changes = self._changes_from_entries(
 
1140
                        source_entry, target_entry, source_path, target_path)
 
1141
                else:
 
1142
                    changes = True
 
1143
                # Get this parents parent to examine.
 
1144
                new_parent_id = result.parent_id[1]
 
1145
                precise_file_ids.add(new_parent_id)
 
1146
                if changes:
 
1147
                    if (result.kind[0] == 'directory' and
 
1148
                            result.kind[1] != 'directory'):
 
1149
                        # This stopped being a directory, the old children have
 
1150
                        # to be included.
 
1151
                        if source_entry is None:
 
1152
                            # Reusing a discarded change.
 
1153
                            source_entry = self._get_entry(
 
1154
                                self.source, result.path[0])
 
1155
                        precise_file_ids.update(
 
1156
                            child.file_id
 
1157
                            for child in self.source.iter_child_entries(result.path[0]))
 
1158
                    changed_file_ids.add(result.file_id)
 
1159
                    yield result
847
1160
 
848
1161
    def file_content_matches(
849
1162
            self, source_path, target_path,
887
1200
        :return: path in target, or None if there is no equivalent path.
888
1201
        :raise NoSuchFile: If the path doesn't exist in source
889
1202
        """
890
 
        raise NotImplementedError(self.find_target_path)
 
1203
        file_id = self.source.path2id(path)
 
1204
        if file_id is None:
 
1205
            raise errors.NoSuchFile(path)
 
1206
        try:
 
1207
            return self.target.id2path(file_id, recurse=recurse)
 
1208
        except errors.NoSuchId:
 
1209
            return None
891
1210
 
892
1211
    def find_source_path(self, path, recurse='none'):
893
1212
        """Find the source tree path.
896
1215
        :return: path in source, or None if there is no equivalent path.
897
1216
        :raise NoSuchFile: if the path doesn't exist in target
898
1217
        """
899
 
        raise NotImplementedError(self.find_source_path)
 
1218
        file_id = self.target.path2id(path)
 
1219
        if file_id is None:
 
1220
            raise errors.NoSuchFile(path)
 
1221
        try:
 
1222
            return self.source.id2path(file_id, recurse=recurse)
 
1223
        except errors.NoSuchId:
 
1224
            return None
900
1225
 
901
1226
    def find_target_paths(self, paths, recurse='none'):
902
1227
        """Find target tree paths.
923
1248
        return ret
924
1249
 
925
1250
 
 
1251
InterTree.register_optimiser(InterTree)
 
1252
 
 
1253
 
926
1254
def find_previous_paths(from_tree, to_tree, paths, recurse='none'):
927
1255
    """Find previous tree paths.
928
1256