/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: Breezy landing bot
  • Author(s): Jelmer Vernooij
  • Date: 2020-06-01 21:57:00 UTC
  • mfrom: (7490.39.3 move-launchpad)
  • Revision ID: breezy.the.bot@gmail.com-20200601215700-joxuzo6w172gq74v
Move launchpad hoster support to the launchpad plugin.

Merged from https://code.launchpad.net/~jelmer/brz/move-launchpad/+merge/384931

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
try:
 
21
    from collections.abc import deque
 
22
except ImportError:  # python < 3.7
 
23
    from collections import deque
 
24
 
 
25
from .lazy_import import lazy_import
 
26
lazy_import(globals(), """
 
27
 
 
28
from breezy import (
 
29
    conflicts as _mod_conflicts,
 
30
    debug,
 
31
    delta,
 
32
    filters,
 
33
    revision as _mod_revision,
 
34
    rules,
 
35
    trace,
 
36
    )
 
37
from breezy.i18n import gettext
 
38
""")
 
39
 
20
40
from . import (
21
41
    errors,
22
42
    lock,
23
43
    osutils,
24
 
    revision as _mod_revision,
25
 
    trace,
26
44
    )
27
45
from .inter import InterObject
28
46
 
117
135
class TreeChange(object):
118
136
    """Describes the changes between the same item in two different trees."""
119
137
 
120
 
    __slots__ = ['path', 'changed_content', 'versioned',
 
138
    __slots__ = ['file_id', 'path', 'changed_content', 'versioned', 'parent_id',
121
139
                 'name', 'kind', 'executable', 'copied']
122
140
 
123
 
    def __init__(self, path, changed_content, versioned,
 
141
    def __init__(self, file_id, path, changed_content, versioned, parent_id,
124
142
                 name, kind, executable, copied=False):
 
143
        self.file_id = file_id
125
144
        self.path = path
126
145
        self.changed_content = changed_content
127
146
        self.versioned = versioned
 
147
        self.parent_id = parent_id
128
148
        self.name = name
129
149
        self.kind = kind
130
150
        self.executable = executable
133
153
    def __repr__(self):
134
154
        return "%s%r" % (self.__class__.__name__, self._as_tuple())
135
155
 
 
156
    def __len__(self):
 
157
        return len(self.__slots__)
 
158
 
136
159
    def _as_tuple(self):
137
 
        return (self.path, self.changed_content, self.versioned,
138
 
                self.name, self.kind, self.executable, self.copied)
 
160
        return (self.file_id, self.path, self.changed_content, self.versioned,
 
161
                self.parent_id, self.name, self.kind, self.executable, self.copied)
139
162
 
140
163
    def __eq__(self, other):
141
164
        if isinstance(other, TreeChange):
152
175
            return (self.executable[0] != self.executable[1])
153
176
        return False
154
177
 
155
 
    @property
156
 
    def renamed(self):
157
 
        return (
158
 
            not self.copied and
159
 
            None not in self.name and
160
 
            self.path[0] != self.path[1])
161
 
 
162
178
    def is_reparented(self):
163
 
        return os.path.dirname(self.path[0]) != os.path.dirname(self.path[1])
 
179
        return self.parent_id[0] != self.parent_id[1]
164
180
 
165
181
    def discard_new(self):
166
182
        return self.__class__(
167
 
            (self.path[0], None), self.changed_content,
168
 
            (self.versioned[0], None),
 
183
            self.file_id, (self.path[0], None), self.changed_content,
 
184
            (self.versioned[0], None), (self.parent_id[0], None),
169
185
            (self.name[0], None), (self.kind[0], None),
170
186
            (self.executable[0], None),
171
187
            copied=False)
253
269
 
254
270
        Each conflict is an instance of breezy.conflicts.Conflict.
255
271
        """
256
 
        from . import conflicts as _mod_conflicts
257
272
        return _mod_conflicts.ConflictList()
258
273
 
259
274
    def extras(self):
634
649
        list to exclude some directories, they are then not descended into.
635
650
 
636
651
        The data yielded is of the form:
637
 
        (directory-relpath,
638
 
        [(relpath, basename, kind, lstat, path_from_tree_root,
 
652
        ((directory-relpath, directory-path-from-root, directory-fileid),
 
653
        [(relpath, basename, kind, lstat, path_from_tree_root, file_id,
639
654
          versioned_kind), ...]),
 
655
         - directory-relpath is the containing dirs relpath from prefix
640
656
         - directory-path-from-root is the containing dirs path from /
 
657
         - directory-fileid is the id of the directory if it is versioned.
641
658
         - relpath is the relative path within the subtree being walked.
642
659
         - basename is the basename
643
660
         - kind is the kind of the file now. If unknonwn then the file is not
645
662
           versioned_kind.
646
663
         - lstat is the stat data *if* the file was statted.
647
664
         - path_from_tree_root is the path from the root of the tree.
 
665
         - file_id is the file_id if the entry is versioned.
648
666
         - versioned_kind is the kind of the file as last recorded in the
649
667
           versioning system. If 'unknown' the file is not versioned.
650
668
        One of 'kind' and 'versioned_kind' must not be 'unknown'.
670
688
            or None if unknown
671
689
        :return: the list of filters - [] if there are none
672
690
        """
673
 
        from . import debug, filters
674
691
        filter_pref_names = filters._get_registered_names()
675
692
        if len(filter_pref_names) == 0:
676
693
            return []
677
694
        prefs = next(self.iter_search_rules([path], filter_pref_names))
678
695
        stk = filters._get_filter_stack_for(prefs)
679
696
        if 'filters' in debug.debug_flags:
680
 
            trace.note("*** {0} content-filter: {1} => {2!r}").format(path, prefs, stk)
 
697
            trace.note(
 
698
                gettext("*** {0} content-filter: {1} => {2!r}").format(path, prefs, stk))
681
699
        return stk
682
700
 
683
701
    def _content_filter_stack_provider(self):
704
722
        :return: an iterator of tuple sequences, one per path-name.
705
723
          See _RulesSearcher.get_items for details on the tuple sequence.
706
724
        """
707
 
        from . import rules
708
725
        if _default_searcher is None:
709
726
            _default_searcher = rules._per_user_searcher
710
727
        searcher = self._get_rules_searcher(_default_searcher)
741
758
        """Check if this tree support versioning a specific file kind."""
742
759
        return (kind in ('file', 'directory', 'symlink', 'tree-reference'))
743
760
 
744
 
    def preview_transform(self, pb=None):
745
 
        """Obtain a transform object."""
746
 
        raise NotImplementedError(self.preview_transform)
747
 
 
748
761
 
749
762
class InterTree(InterObject):
750
763
    """This class represents operations taking place between two Trees.
771
784
        # it works for all trees.
772
785
        return True
773
786
 
 
787
    def _changes_from_entries(self, source_entry, target_entry, source_path,
 
788
                              target_path):
 
789
        """Generate a iter_changes tuple between source_entry and target_entry.
 
790
 
 
791
        :param source_entry: An inventory entry from self.source, or None.
 
792
        :param target_entry: An inventory entry from self.target, or None.
 
793
        :param source_path: The path of source_entry.
 
794
        :param target_path: The path of target_entry.
 
795
        :return: A tuple, item 0 of which is an iter_changes result tuple, and
 
796
            item 1 is True if there are any changes in the result tuple.
 
797
        """
 
798
        if source_entry is None:
 
799
            if target_entry is None:
 
800
                return None
 
801
            file_id = target_entry.file_id
 
802
        else:
 
803
            file_id = source_entry.file_id
 
804
        if source_entry is not None:
 
805
            source_versioned = True
 
806
            source_name = source_entry.name
 
807
            source_parent = source_entry.parent_id
 
808
            source_kind, source_executable, source_stat = \
 
809
                self.source._comparison_data(source_entry, source_path)
 
810
        else:
 
811
            source_versioned = False
 
812
            source_name = None
 
813
            source_parent = None
 
814
            source_kind = None
 
815
            source_executable = None
 
816
        if target_entry is not None:
 
817
            target_versioned = True
 
818
            target_name = target_entry.name
 
819
            target_parent = target_entry.parent_id
 
820
            target_kind, target_executable, target_stat = \
 
821
                self.target._comparison_data(target_entry, target_path)
 
822
        else:
 
823
            target_versioned = False
 
824
            target_name = None
 
825
            target_parent = None
 
826
            target_kind = None
 
827
            target_executable = None
 
828
        versioned = (source_versioned, target_versioned)
 
829
        kind = (source_kind, target_kind)
 
830
        changed_content = False
 
831
        if source_kind != target_kind:
 
832
            changed_content = True
 
833
        elif source_kind == 'file':
 
834
            if not self.file_content_matches(
 
835
                    source_path, target_path,
 
836
                    source_stat, target_stat):
 
837
                changed_content = True
 
838
        elif source_kind == 'symlink':
 
839
            if (self.source.get_symlink_target(source_path) !=
 
840
                    self.target.get_symlink_target(target_path)):
 
841
                changed_content = True
 
842
        elif source_kind == 'tree-reference':
 
843
            if (self.source.get_reference_revision(source_path)
 
844
                    != self.target.get_reference_revision(target_path)):
 
845
                changed_content = True
 
846
        parent = (source_parent, target_parent)
 
847
        name = (source_name, target_name)
 
848
        executable = (source_executable, target_executable)
 
849
        if (changed_content is not False or versioned[0] != versioned[1] or
 
850
            parent[0] != parent[1] or name[0] != name[1] or
 
851
                executable[0] != executable[1]):
 
852
            changes = True
 
853
        else:
 
854
            changes = False
 
855
        return TreeChange(
 
856
            file_id, (source_path, target_path), changed_content,
 
857
            versioned, parent, name, kind, executable), changes
 
858
 
774
859
    def compare(self, want_unchanged=False, specific_files=None,
775
860
                extra_trees=None, require_versioned=False, include_root=False,
776
861
                want_unversioned=False):
790
875
            a PathsNotVersionedError will be thrown.
791
876
        :param want_unversioned: Scan for unversioned paths.
792
877
        """
793
 
        from . import delta
794
878
        trees = (self.source,)
795
879
        if extra_trees is not None:
796
880
            trees = trees + tuple(extra_trees)
836
920
            output. An unversioned file is defined as one with (False, False)
837
921
            for the versioned pair.
838
922
        """
839
 
        raise NotImplementedError(self.iter_changes)
 
923
        if not extra_trees:
 
924
            extra_trees = []
 
925
        else:
 
926
            extra_trees = list(extra_trees)
 
927
        # The ids of items we need to examine to insure delta consistency.
 
928
        precise_file_ids = set()
 
929
        changed_file_ids = []
 
930
        if specific_files == []:
 
931
            target_specific_files = []
 
932
            source_specific_files = []
 
933
        else:
 
934
            target_specific_files = self.target.find_related_paths_across_trees(
 
935
                specific_files, [self.source] + extra_trees,
 
936
                require_versioned=require_versioned)
 
937
            source_specific_files = self.source.find_related_paths_across_trees(
 
938
                specific_files, [self.target] + extra_trees,
 
939
                require_versioned=require_versioned)
 
940
        if specific_files is not None:
 
941
            # reparented or added entries must have their parents included
 
942
            # so that valid deltas can be created. The seen_parents set
 
943
            # tracks the parents that we need to have.
 
944
            # The seen_dirs set tracks directory entries we've yielded.
 
945
            # After outputting version object in to_entries we set difference
 
946
            # the two seen sets and start checking parents.
 
947
            seen_parents = set()
 
948
            seen_dirs = set()
 
949
        if want_unversioned:
 
950
            all_unversioned = sorted([(p.split('/'), p) for p in
 
951
                                      self.target.extras()
 
952
                                      if specific_files is None or
 
953
                                      osutils.is_inside_any(specific_files, p)])
 
954
            all_unversioned = deque(all_unversioned)
 
955
        else:
 
956
            all_unversioned = deque()
 
957
        to_paths = {}
 
958
        from_entries_by_dir = list(self.source.iter_entries_by_dir(
 
959
            specific_files=source_specific_files))
 
960
        from_data = dict(from_entries_by_dir)
 
961
        to_entries_by_dir = list(self.target.iter_entries_by_dir(
 
962
            specific_files=target_specific_files))
 
963
        path_equivs = self.find_source_paths([p for p, e in to_entries_by_dir])
 
964
        num_entries = len(from_entries_by_dir) + len(to_entries_by_dir)
 
965
        entry_count = 0
 
966
        # the unversioned path lookup only occurs on real trees - where there
 
967
        # can be extras. So the fake_entry is solely used to look up
 
968
        # executable it values when execute is not supported.
 
969
        fake_entry = TreeFile()
 
970
        for target_path, target_entry in to_entries_by_dir:
 
971
            while (all_unversioned and
 
972
                   all_unversioned[0][0] < target_path.split('/')):
 
973
                unversioned_path = all_unversioned.popleft()
 
974
                target_kind, target_executable, target_stat = \
 
975
                    self.target._comparison_data(
 
976
                        fake_entry, unversioned_path[1])
 
977
                yield TreeChange(
 
978
                    None, (None, unversioned_path[1]), True, (False, False),
 
979
                    (None, None),
 
980
                    (None, unversioned_path[0][-1]),
 
981
                    (None, target_kind),
 
982
                    (None, target_executable))
 
983
            source_path = path_equivs[target_path]
 
984
            if source_path is not None:
 
985
                source_entry = from_data.get(source_path)
 
986
            else:
 
987
                source_entry = None
 
988
            result, changes = self._changes_from_entries(
 
989
                source_entry, target_entry, source_path=source_path, target_path=target_path)
 
990
            to_paths[result.file_id] = result.path[1]
 
991
            entry_count += 1
 
992
            if result.versioned[0]:
 
993
                entry_count += 1
 
994
            if pb is not None:
 
995
                pb.update('comparing files', entry_count, num_entries)
 
996
            if changes or include_unchanged:
 
997
                if specific_files is not None:
 
998
                    precise_file_ids.add(result.parent_id[1])
 
999
                    changed_file_ids.append(result.file_id)
 
1000
                yield result
 
1001
            # Ensure correct behaviour for reparented/added specific files.
 
1002
            if specific_files is not None:
 
1003
                # Record output dirs
 
1004
                if result.kind[1] == 'directory':
 
1005
                    seen_dirs.add(result.file_id)
 
1006
                # Record parents of reparented/added entries.
 
1007
                if not result.versioned[0] or result.is_reparented():
 
1008
                    seen_parents.add(result.parent_id[1])
 
1009
        while all_unversioned:
 
1010
            # yield any trailing unversioned paths
 
1011
            unversioned_path = all_unversioned.popleft()
 
1012
            to_kind, to_executable, to_stat = \
 
1013
                self.target._comparison_data(fake_entry, unversioned_path[1])
 
1014
            yield TreeChange(
 
1015
                None, (None, unversioned_path[1]), True, (False, False),
 
1016
                (None, None),
 
1017
                (None, unversioned_path[0][-1]),
 
1018
                (None, to_kind),
 
1019
                (None, to_executable))
 
1020
        # Yield all remaining source paths
 
1021
        for path, from_entry in from_entries_by_dir:
 
1022
            file_id = from_entry.file_id
 
1023
            if file_id in to_paths:
 
1024
                # already returned
 
1025
                continue
 
1026
            to_path = self.find_target_path(path)
 
1027
            entry_count += 1
 
1028
            if pb is not None:
 
1029
                pb.update('comparing files', entry_count, num_entries)
 
1030
            versioned = (True, False)
 
1031
            parent = (from_entry.parent_id, None)
 
1032
            name = (from_entry.name, None)
 
1033
            from_kind, from_executable, stat_value = \
 
1034
                self.source._comparison_data(from_entry, path)
 
1035
            kind = (from_kind, None)
 
1036
            executable = (from_executable, None)
 
1037
            changed_content = from_kind is not None
 
1038
            # the parent's path is necessarily known at this point.
 
1039
            changed_file_ids.append(file_id)
 
1040
            yield TreeChange(
 
1041
                file_id, (path, to_path), changed_content, versioned, parent,
 
1042
                name, kind, executable)
 
1043
        changed_file_ids = set(changed_file_ids)
 
1044
        if specific_files is not None:
 
1045
            for result in self._handle_precise_ids(precise_file_ids,
 
1046
                                                   changed_file_ids):
 
1047
                yield result
 
1048
 
 
1049
    @staticmethod
 
1050
    def _get_entry(tree, path):
 
1051
        """Get an inventory entry from a tree, with missing entries as None.
 
1052
 
 
1053
        If the tree raises NotImplementedError on accessing .inventory, then
 
1054
        this is worked around using iter_entries_by_dir on just the file id
 
1055
        desired.
 
1056
 
 
1057
        :param tree: The tree to lookup the entry in.
 
1058
        :param path: The path to look up
 
1059
        """
 
1060
        # No inventory available.
 
1061
        try:
 
1062
            iterator = tree.iter_entries_by_dir(specific_files=[path])
 
1063
            return next(iterator)[1]
 
1064
        except StopIteration:
 
1065
            return None
 
1066
 
 
1067
    def _handle_precise_ids(self, precise_file_ids, changed_file_ids,
 
1068
                            discarded_changes=None):
 
1069
        """Fill out a partial iter_changes to be consistent.
 
1070
 
 
1071
        :param precise_file_ids: The file ids of parents that were seen during
 
1072
            the iter_changes.
 
1073
        :param changed_file_ids: The file ids of already emitted items.
 
1074
        :param discarded_changes: An optional dict of precalculated
 
1075
            iter_changes items which the partial iter_changes had not output
 
1076
            but had calculated.
 
1077
        :return: A generator of iter_changes items to output.
 
1078
        """
 
1079
        # process parents of things that had changed under the users
 
1080
        # requested paths to prevent incorrect paths or parent ids which
 
1081
        # aren't in the tree.
 
1082
        while precise_file_ids:
 
1083
            precise_file_ids.discard(None)
 
1084
            # Don't emit file_ids twice
 
1085
            precise_file_ids.difference_update(changed_file_ids)
 
1086
            if not precise_file_ids:
 
1087
                break
 
1088
            # If the there was something at a given output path in source, we
 
1089
            # have to include the entry from source in the delta, or we would
 
1090
            # be putting this entry into a used path.
 
1091
            paths = []
 
1092
            for parent_id in precise_file_ids:
 
1093
                try:
 
1094
                    paths.append(self.target.id2path(parent_id))
 
1095
                except errors.NoSuchId:
 
1096
                    # This id has been dragged in from the source by delta
 
1097
                    # expansion and isn't present in target at all: we don't
 
1098
                    # need to check for path collisions on it.
 
1099
                    pass
 
1100
            for path in paths:
 
1101
                old_id = self.source.path2id(path)
 
1102
                precise_file_ids.add(old_id)
 
1103
            precise_file_ids.discard(None)
 
1104
            current_ids = precise_file_ids
 
1105
            precise_file_ids = set()
 
1106
            # We have to emit all of precise_file_ids that have been altered.
 
1107
            # We may have to output the children of some of those ids if any
 
1108
            # directories have stopped being directories.
 
1109
            for file_id in current_ids:
 
1110
                # Examine file_id
 
1111
                if discarded_changes:
 
1112
                    result = discarded_changes.get(file_id)
 
1113
                    source_entry = None
 
1114
                else:
 
1115
                    result = None
 
1116
                if result is None:
 
1117
                    try:
 
1118
                        source_path = self.source.id2path(file_id)
 
1119
                    except errors.NoSuchId:
 
1120
                        source_path = None
 
1121
                        source_entry = None
 
1122
                    else:
 
1123
                        source_entry = self._get_entry(
 
1124
                            self.source, source_path)
 
1125
                    try:
 
1126
                        target_path = self.target.id2path(file_id)
 
1127
                    except errors.NoSuchId:
 
1128
                        target_path = None
 
1129
                        target_entry = None
 
1130
                    else:
 
1131
                        target_entry = self._get_entry(
 
1132
                            self.target, target_path)
 
1133
                    result, changes = self._changes_from_entries(
 
1134
                        source_entry, target_entry, source_path, target_path)
 
1135
                else:
 
1136
                    changes = True
 
1137
                # Get this parents parent to examine.
 
1138
                new_parent_id = result.parent_id[1]
 
1139
                precise_file_ids.add(new_parent_id)
 
1140
                if changes:
 
1141
                    if (result.kind[0] == 'directory' and
 
1142
                            result.kind[1] != 'directory'):
 
1143
                        # This stopped being a directory, the old children have
 
1144
                        # to be included.
 
1145
                        if source_entry is None:
 
1146
                            # Reusing a discarded change.
 
1147
                            source_entry = self._get_entry(
 
1148
                                self.source, result.path[0])
 
1149
                        precise_file_ids.update(
 
1150
                            child.file_id
 
1151
                            for child in self.source.iter_child_entries(result.path[0]))
 
1152
                    changed_file_ids.add(result.file_id)
 
1153
                    yield result
840
1154
 
841
1155
    def file_content_matches(
842
1156
            self, source_path, target_path,
880
1194
        :return: path in target, or None if there is no equivalent path.
881
1195
        :raise NoSuchFile: If the path doesn't exist in source
882
1196
        """
883
 
        raise NotImplementedError(self.find_target_path)
 
1197
        file_id = self.source.path2id(path)
 
1198
        if file_id is None:
 
1199
            raise errors.NoSuchFile(path)
 
1200
        try:
 
1201
            return self.target.id2path(file_id, recurse=recurse)
 
1202
        except errors.NoSuchId:
 
1203
            return None
884
1204
 
885
1205
    def find_source_path(self, path, recurse='none'):
886
1206
        """Find the source tree path.
889
1209
        :return: path in source, or None if there is no equivalent path.
890
1210
        :raise NoSuchFile: if the path doesn't exist in target
891
1211
        """
892
 
        raise NotImplementedError(self.find_source_path)
 
1212
        file_id = self.target.path2id(path)
 
1213
        if file_id is None:
 
1214
            raise errors.NoSuchFile(path)
 
1215
        try:
 
1216
            return self.source.id2path(file_id, recurse=recurse)
 
1217
        except errors.NoSuchId:
 
1218
            return None
893
1219
 
894
1220
    def find_target_paths(self, paths, recurse='none'):
895
1221
        """Find target tree paths.
916
1242
        return ret
917
1243
 
918
1244
 
 
1245
InterTree.register_optimiser(InterTree)
 
1246
 
 
1247
 
919
1248
def find_previous_paths(from_tree, to_tree, paths, recurse='none'):
920
1249
    """Find previous tree paths.
921
1250