/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: Gustav Hartvigsson
  • Date: 2021-01-09 21:36:27 UTC
  • Revision ID: gustav.hartvigsson@gmail.com-20210109213627-h1xwcutzy9m7a99b
Added 'Case Preserving Working Tree Use Cases' from Canonical Wiki

* Addod a page from the Canonical Bazaar wiki
  with information on the scmeatics of case
  perserving filesystems an a case insensitive
  filesystem works.
  
  * Needs re-work, but this will do as it is the
    same inforamoton as what was on the linked
    page in the currint documentation.

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
 
 
27
 
from .lazy_import import lazy_import
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
 
    )
39
 
from breezy.i18n import gettext
40
 
""")
41
 
 
42
20
from . import (
43
21
    errors,
44
22
    lock,
45
23
    osutils,
 
24
    revision as _mod_revision,
 
25
    trace,
46
26
    )
47
27
from .inter import InterObject
48
 
from .sixish import (
49
 
    text_type,
50
 
    viewvalues,
51
 
    )
52
28
 
53
29
 
54
30
class FileTimestampUnavailable(errors.BzrError):
141
117
class TreeChange(object):
142
118
    """Describes the changes between the same item in two different trees."""
143
119
 
144
 
    __slots__ = ['file_id', 'path', 'changed_content', 'versioned', 'parent_id',
 
120
    __slots__ = ['path', 'changed_content', 'versioned',
145
121
                 'name', 'kind', 'executable', 'copied']
146
122
 
147
 
    def __init__(self, file_id, path, changed_content, versioned, parent_id,
 
123
    def __init__(self, path, changed_content, versioned,
148
124
                 name, kind, executable, copied=False):
149
 
        self.file_id = file_id
150
125
        self.path = path
151
126
        self.changed_content = changed_content
152
127
        self.versioned = versioned
153
 
        self.parent_id = parent_id
154
128
        self.name = name
155
129
        self.kind = kind
156
130
        self.executable = executable
159
133
    def __repr__(self):
160
134
        return "%s%r" % (self.__class__.__name__, self._as_tuple())
161
135
 
162
 
    def __len__(self):
163
 
        return len(self.__slots__)
164
 
 
165
136
    def _as_tuple(self):
166
 
        return (self.file_id, self.path, self.changed_content, self.versioned,
167
 
                self.parent_id, self.name, self.kind, self.executable, self.copied)
 
137
        return (self.path, self.changed_content, self.versioned,
 
138
                self.name, self.kind, self.executable, self.copied)
168
139
 
169
140
    def __eq__(self, other):
170
141
        if isinstance(other, TreeChange):
181
152
            return (self.executable[0] != self.executable[1])
182
153
        return False
183
154
 
 
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
 
184
162
    def is_reparented(self):
185
 
        return self.parent_id[0] != self.parent_id[1]
 
163
        return os.path.dirname(self.path[0]) != os.path.dirname(self.path[1])
186
164
 
187
165
    def discard_new(self):
188
166
        return self.__class__(
189
 
            self.file_id, (self.path[0], None), self.changed_content,
190
 
            (self.versioned[0], None), (self.parent_id[0], None),
 
167
            (self.path[0], None), self.changed_content,
 
168
            (self.versioned[0], None),
191
169
            (self.name[0], None), (self.kind[0], None),
192
170
            (self.executable[0], None),
193
171
            copied=False)
275
253
 
276
254
        Each conflict is an instance of breezy.conflicts.Conflict.
277
255
        """
 
256
        from . import conflicts as _mod_conflicts
278
257
        return _mod_conflicts.ConflictList()
279
258
 
280
259
    def extras(self):
655
634
        list to exclude some directories, they are then not descended into.
656
635
 
657
636
        The data yielded is of the form:
658
 
        ((directory-relpath, directory-path-from-root, directory-fileid),
659
 
        [(relpath, basename, kind, lstat, path_from_tree_root, file_id,
 
637
        (directory-relpath,
 
638
        [(relpath, basename, kind, lstat, path_from_tree_root,
660
639
          versioned_kind), ...]),
661
 
         - directory-relpath is the containing dirs relpath from prefix
662
640
         - directory-path-from-root is the containing dirs path from /
663
 
         - directory-fileid is the id of the directory if it is versioned.
664
641
         - relpath is the relative path within the subtree being walked.
665
642
         - basename is the basename
666
643
         - kind is the kind of the file now. If unknonwn then the file is not
668
645
           versioned_kind.
669
646
         - lstat is the stat data *if* the file was statted.
670
647
         - path_from_tree_root is the path from the root of the tree.
671
 
         - file_id is the file_id if the entry is versioned.
672
648
         - versioned_kind is the kind of the file as last recorded in the
673
649
           versioning system. If 'unknown' the file is not versioned.
674
650
        One of 'kind' and 'versioned_kind' must not be 'unknown'.
694
670
            or None if unknown
695
671
        :return: the list of filters - [] if there are none
696
672
        """
 
673
        from . import debug, filters
697
674
        filter_pref_names = filters._get_registered_names()
698
675
        if len(filter_pref_names) == 0:
699
676
            return []
700
677
        prefs = next(self.iter_search_rules([path], filter_pref_names))
701
678
        stk = filters._get_filter_stack_for(prefs)
702
679
        if 'filters' in debug.debug_flags:
703
 
            trace.note(
704
 
                gettext("*** {0} content-filter: {1} => {2!r}").format(path, prefs, stk))
 
680
            trace.note("*** {0} content-filter: {1} => {2!r}").format(path, prefs, stk)
705
681
        return stk
706
682
 
707
683
    def _content_filter_stack_provider(self):
728
704
        :return: an iterator of tuple sequences, one per path-name.
729
705
          See _RulesSearcher.get_items for details on the tuple sequence.
730
706
        """
 
707
        from . import rules
731
708
        if _default_searcher is None:
732
709
            _default_searcher = rules._per_user_searcher
733
710
        searcher = self._get_rules_searcher(_default_searcher)
764
741
        """Check if this tree support versioning a specific file kind."""
765
742
        return (kind in ('file', 'directory', 'symlink', 'tree-reference'))
766
743
 
 
744
    def preview_transform(self, pb=None):
 
745
        """Obtain a transform object."""
 
746
        raise NotImplementedError(self.preview_transform)
 
747
 
767
748
 
768
749
class InterTree(InterObject):
769
750
    """This class represents operations taking place between two Trees.
790
771
        # it works for all trees.
791
772
        return True
792
773
 
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
 
 
865
774
    def compare(self, want_unchanged=False, specific_files=None,
866
775
                extra_trees=None, require_versioned=False, include_root=False,
867
776
                want_unversioned=False):
881
790
            a PathsNotVersionedError will be thrown.
882
791
        :param want_unversioned: Scan for unversioned paths.
883
792
        """
 
793
        from . import delta
884
794
        trees = (self.source,)
885
795
        if extra_trees is not None:
886
796
            trees = trees + tuple(extra_trees)
926
836
            output. An unversioned file is defined as one with (False, False)
927
837
            for the versioned pair.
928
838
        """
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
 
839
        raise NotImplementedError(self.iter_changes)
1160
840
 
1161
841
    def file_content_matches(
1162
842
            self, source_path, target_path,
1200
880
        :return: path in target, or None if there is no equivalent path.
1201
881
        :raise NoSuchFile: If the path doesn't exist in source
1202
882
        """
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
 
883
        raise NotImplementedError(self.find_target_path)
1210
884
 
1211
885
    def find_source_path(self, path, recurse='none'):
1212
886
        """Find the source tree path.
1215
889
        :return: path in source, or None if there is no equivalent path.
1216
890
        :raise NoSuchFile: if the path doesn't exist in target
1217
891
        """
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
 
892
        raise NotImplementedError(self.find_source_path)
1225
893
 
1226
894
    def find_target_paths(self, paths, recurse='none'):
1227
895
        """Find target tree paths.
1248
916
        return ret
1249
917
 
1250
918
 
1251
 
InterTree.register_optimiser(InterTree)
1252
 
 
1253
 
 
1254
919
def find_previous_paths(from_tree, to_tree, paths, recurse='none'):
1255
920
    """Find previous tree paths.
1256
921