/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: 2019-03-05 07:32:38 UTC
  • mto: (7290.1.21 work)
  • mto: This revision was merged to the branch mainline in revision 7311.
  • Revision ID: jelmer@jelmer.uk-20190305073238-zlqn981opwnqsmzi
Add appveyor configuration.

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
 
20
42
from . import (
21
43
    errors,
22
44
    lock,
23
45
    osutils,
24
 
    revision as _mod_revision,
25
 
    trace,
26
46
    )
27
47
from .inter import InterObject
 
48
from .sixish import (
 
49
    text_type,
 
50
    viewvalues,
 
51
    )
28
52
 
29
53
 
30
54
class FileTimestampUnavailable(errors.BzrError):
37
61
        self.path = path
38
62
 
39
63
 
40
 
class MissingNestedTree(errors.BzrError):
41
 
 
42
 
    _fmt = "The nested tree for %(path)s can not be resolved."""
43
 
 
44
 
    def __init__(self, path):
45
 
        self.path = path
46
 
 
47
 
 
48
64
class TreeEntry(object):
49
65
    """An entry that implements the minimum interface used by commands.
50
66
    """
51
67
 
52
 
    __slots__ = []
53
 
 
54
68
    def __eq__(self, other):
55
69
        # yes, this is ugly, TODO: best practice __eq__ style.
56
70
        return (isinstance(other, TreeEntry)
61
75
    def kind_character(self):
62
76
        return "???"
63
77
 
64
 
    def is_unmodified(self, other):
65
 
        """Does this entry reference the same entry?
66
 
 
67
 
        This is mostly the same as __eq__, but returns False
68
 
        for entries without enough information (i.e. revision is None)
69
 
        """
70
 
        return False
71
 
 
72
78
 
73
79
class TreeDirectory(TreeEntry):
74
80
    """See TreeEntry. This is a directory in a working tree."""
75
81
 
76
 
    __slots__ = []
77
 
 
78
82
    kind = 'directory'
79
83
 
80
84
    def kind_character(self):
84
88
class TreeFile(TreeEntry):
85
89
    """See TreeEntry. This is a regular file in a working tree."""
86
90
 
87
 
    __slots__ = []
88
 
 
89
91
    kind = 'file'
90
92
 
91
93
    def kind_character(self):
95
97
class TreeLink(TreeEntry):
96
98
    """See TreeEntry. This is a symlink in a working tree."""
97
99
 
98
 
    __slots__ = []
99
 
 
100
100
    kind = 'symlink'
101
101
 
102
102
    def kind_character(self):
106
106
class TreeReference(TreeEntry):
107
107
    """See TreeEntry. This is a reference to a nested tree in a working tree."""
108
108
 
109
 
    __slots__ = []
110
 
 
111
109
    kind = 'tree-reference'
112
110
 
113
111
    def kind_character(self):
114
112
        return '+'
115
113
 
116
114
 
117
 
class TreeChange(object):
118
 
    """Describes the changes between the same item in two different trees."""
119
 
 
120
 
    __slots__ = ['path', 'changed_content', 'versioned',
121
 
                 'name', 'kind', 'executable', 'copied']
122
 
 
123
 
    def __init__(self, path, changed_content, versioned,
124
 
                 name, kind, executable, copied=False):
125
 
        self.path = path
126
 
        self.changed_content = changed_content
127
 
        self.versioned = versioned
128
 
        self.name = name
129
 
        self.kind = kind
130
 
        self.executable = executable
131
 
        self.copied = copied
132
 
 
133
 
    def __repr__(self):
134
 
        return "%s%r" % (self.__class__.__name__, self._as_tuple())
135
 
 
136
 
    def _as_tuple(self):
137
 
        return (self.path, self.changed_content, self.versioned,
138
 
                self.name, self.kind, self.executable, self.copied)
139
 
 
140
 
    def __eq__(self, other):
141
 
        if isinstance(other, TreeChange):
142
 
            return self._as_tuple() == other._as_tuple()
143
 
        if isinstance(other, tuple):
144
 
            return self._as_tuple() == other
145
 
        return False
146
 
 
147
 
    def __lt__(self, other):
148
 
        return self._as_tuple() < other._as_tuple()
149
 
 
150
 
    def meta_modified(self):
151
 
        if self.versioned == (True, True):
152
 
            return (self.executable[0] != self.executable[1])
153
 
        return False
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
 
 
162
 
    def is_reparented(self):
163
 
        return os.path.dirname(self.path[0]) != os.path.dirname(self.path[1])
164
 
 
165
 
    def discard_new(self):
166
 
        return self.__class__(
167
 
            (self.path[0], None), self.changed_content,
168
 
            (self.versioned[0], None),
169
 
            (self.name[0], None), (self.kind[0], None),
170
 
            (self.executable[0], None),
171
 
            copied=False)
172
 
 
173
 
 
174
115
class Tree(object):
175
116
    """Abstract file tree.
176
117
 
200
141
        """
201
142
        return True
202
143
 
203
 
    def supports_symlinks(self):
204
 
        """Does this tree support symbolic links?
205
 
        """
206
 
        return osutils.has_symlinks()
207
 
 
208
144
    def changes_from(self, other, want_unchanged=False, specific_files=None,
209
145
                     extra_trees=None, require_versioned=False, include_root=False,
210
146
                     want_unversioned=False):
245
181
        """See InterTree.iter_changes"""
246
182
        intertree = InterTree.get(from_tree, self)
247
183
        return intertree.iter_changes(include_unchanged, specific_files, pb,
248
 
                                      extra_trees, require_versioned,
249
 
                                      want_unversioned=want_unversioned)
 
184
                                      extra_trees, require_versioned, want_unversioned=want_unversioned)
250
185
 
251
186
    def conflicts(self):
252
187
        """Get a list of the conflicts in the tree.
253
188
 
254
189
        Each conflict is an instance of breezy.conflicts.Conflict.
255
190
        """
256
 
        from . import conflicts as _mod_conflicts
257
191
        return _mod_conflicts.ConflictList()
258
192
 
259
193
    def extras(self):
289
223
        """Iterate through all paths, including paths for missing files."""
290
224
        raise NotImplementedError(self.all_versioned_paths)
291
225
 
292
 
    def id2path(self, file_id, recurse='down'):
 
226
    def id2path(self, file_id):
293
227
        """Return the path for a file id.
294
228
 
295
229
        :raises NoSuchId:
296
230
        """
297
231
        raise NotImplementedError(self.id2path)
298
232
 
299
 
    def iter_entries_by_dir(self, specific_files=None, recurse_nested=False):
 
233
    def iter_entries_by_dir(self, specific_files=None):
300
234
        """Walk the tree in 'by_dir' order.
301
235
 
302
236
        This will yield each entry in the tree as a (path, entry) tuple.
320
254
        The yield order (ignoring root) would be::
321
255
 
322
256
          a, f, a/b, a/d, a/b/c, a/d/e, f/g
323
 
 
324
 
        If recurse_nested is enabled then nested trees are included as if
325
 
        they were a part of the tree. If is disabled then TreeReference
326
 
        objects (without any children) are yielded.
327
257
        """
328
258
        raise NotImplementedError(self.iter_entries_by_dir)
329
259
 
336
266
        """
337
267
        raise NotImplementedError(self.iter_child_entries)
338
268
 
339
 
    def list_files(self, include_root=False, from_dir=None, recursive=True,
340
 
                   recurse_nested=False):
 
269
    def list_files(self, include_root=False, from_dir=None, recursive=True):
341
270
        """List all files in this tree.
342
271
 
343
272
        :param include_root: Whether to include the entry for the tree root
344
273
        :param from_dir: Directory under which to list files
345
274
        :param recursive: Whether to list files recursively
346
 
        :param recurse_nested: enter nested trees
347
275
        :return: iterator over tuples of
348
276
            (path, versioned, kind, inventory entry)
349
277
        """
353
281
        if self.supports_tree_reference():
354
282
            for path, entry in self.iter_entries_by_dir():
355
283
                if entry.kind == 'tree-reference':
356
 
                    yield path
357
 
 
358
 
    def get_containing_nested_tree(self, path):
359
 
        """Find the nested tree that contains a path.
360
 
 
361
 
        :return: tuple with (nested tree and path inside the nested tree)
362
 
        """
363
 
        for nested_path in self.iter_references():
364
 
            nested_path += '/'
365
 
            if path.startswith(nested_path):
366
 
                nested_tree = self.get_nested_tree(nested_path)
367
 
                return nested_tree, path[len(nested_path):]
368
 
        else:
369
 
            return None, None
370
 
 
371
 
    def get_nested_tree(self, path):
372
 
        """Open the nested tree at the specified path.
373
 
 
374
 
        :param path: Path from which to resolve tree reference.
375
 
        :return: A Tree object for the nested tree
376
 
        :raise MissingNestedTree: If the nested tree can not be resolved
377
 
        """
378
 
        raise NotImplementedError(self.get_nested_tree)
 
284
                    yield path, entry.file_id
379
285
 
380
286
    def kind(self, path):
381
287
        raise NotImplementedError("Tree subclass %s must implement kind"
407
313
        """
408
314
        raise NotImplementedError(self.path_content_summary)
409
315
 
410
 
    def get_reference_revision(self, path, branch=None):
 
316
    def get_reference_revision(self, path):
411
317
        raise NotImplementedError("Tree subclass %s must implement "
412
318
                                  "get_reference_revision"
413
319
                                  % self.__class__.__name__)
540
446
        """
541
447
        raise NotImplementedError(self.get_symlink_target)
542
448
 
 
449
    def get_root_id(self):
 
450
        """Return the file_id for the root of this tree."""
 
451
        raise NotImplementedError(self.get_root_id)
 
452
 
543
453
    def annotate_iter(self, path,
544
454
                      default_revision=_mod_revision.CURRENT_REVISION):
545
455
        """Return an iterator of revision_id, line tuples.
554
464
        """
555
465
        raise NotImplementedError(self.annotate_iter)
556
466
 
 
467
    def _iter_parent_trees(self):
 
468
        """Iterate through parent trees, defaulting to Tree.revision_tree."""
 
469
        for revision_id in self.get_parent_ids():
 
470
            try:
 
471
                yield self.revision_tree(revision_id)
 
472
            except errors.NoSuchRevisionInTree:
 
473
                yield self.repository.revision_tree(revision_id)
 
474
 
557
475
    def path2id(self, path):
558
476
        """Return the id for path in this tree."""
559
477
        raise NotImplementedError(self.path2id)
634
552
        list to exclude some directories, they are then not descended into.
635
553
 
636
554
        The data yielded is of the form:
637
 
        (directory-relpath,
638
 
        [(relpath, basename, kind, lstat, path_from_tree_root,
 
555
        ((directory-relpath, directory-path-from-root, directory-fileid),
 
556
        [(relpath, basename, kind, lstat, path_from_tree_root, file_id,
639
557
          versioned_kind), ...]),
 
558
         - directory-relpath is the containing dirs relpath from prefix
640
559
         - directory-path-from-root is the containing dirs path from /
 
560
         - directory-fileid is the id of the directory if it is versioned.
641
561
         - relpath is the relative path within the subtree being walked.
642
562
         - basename is the basename
643
563
         - kind is the kind of the file now. If unknonwn then the file is not
645
565
           versioned_kind.
646
566
         - lstat is the stat data *if* the file was statted.
647
567
         - path_from_tree_root is the path from the root of the tree.
 
568
         - file_id is the file_id if the entry is versioned.
648
569
         - versioned_kind is the kind of the file as last recorded in the
649
570
           versioning system. If 'unknown' the file is not versioned.
650
571
        One of 'kind' and 'versioned_kind' must not be 'unknown'.
670
591
            or None if unknown
671
592
        :return: the list of filters - [] if there are none
672
593
        """
673
 
        from . import debug, filters
674
594
        filter_pref_names = filters._get_registered_names()
675
595
        if len(filter_pref_names) == 0:
676
596
            return []
677
597
        prefs = next(self.iter_search_rules([path], filter_pref_names))
678
598
        stk = filters._get_filter_stack_for(prefs)
679
599
        if 'filters' in debug.debug_flags:
680
 
            trace.note("*** {0} content-filter: {1} => {2!r}").format(path, prefs, stk)
 
600
            trace.note(
 
601
                gettext("*** {0} content-filter: {1} => {2!r}").format(path, prefs, stk))
681
602
        return stk
682
603
 
683
604
    def _content_filter_stack_provider(self):
689
610
        :return: None if content filtering is not supported by this tree.
690
611
        """
691
612
        if self.supports_content_filtering():
692
 
            return self._content_filter_stack
 
613
            return lambda path, file_id: \
 
614
                self._content_filter_stack(path)
693
615
        else:
694
616
            return None
695
617
 
704
626
        :return: an iterator of tuple sequences, one per path-name.
705
627
          See _RulesSearcher.get_items for details on the tuple sequence.
706
628
        """
707
 
        from . import rules
708
629
        if _default_searcher is None:
709
630
            _default_searcher = rules._per_user_searcher
710
631
        searcher = self._get_rules_searcher(_default_searcher)
741
662
        """Check if this tree support versioning a specific file kind."""
742
663
        return (kind in ('file', 'directory', 'symlink', 'tree-reference'))
743
664
 
744
 
    def preview_transform(self, pb=None):
745
 
        """Obtain a transform object."""
746
 
        raise NotImplementedError(self.preview_transform)
747
 
 
748
665
 
749
666
class InterTree(InterObject):
750
667
    """This class represents operations taking place between two Trees.
771
688
        # it works for all trees.
772
689
        return True
773
690
 
 
691
    def _changes_from_entries(self, source_entry, target_entry, source_path,
 
692
                              target_path):
 
693
        """Generate a iter_changes tuple between source_entry and target_entry.
 
694
 
 
695
        :param source_entry: An inventory entry from self.source, or None.
 
696
        :param target_entry: An inventory entry from self.target, or None.
 
697
        :param source_path: The path of source_entry.
 
698
        :param target_path: The path of target_entry.
 
699
        :return: A tuple, item 0 of which is an iter_changes result tuple, and
 
700
            item 1 is True if there are any changes in the result tuple.
 
701
        """
 
702
        if source_entry is None:
 
703
            if target_entry is None:
 
704
                return None
 
705
            file_id = target_entry.file_id
 
706
        else:
 
707
            file_id = source_entry.file_id
 
708
        if source_entry is not None:
 
709
            source_versioned = True
 
710
            source_name = source_entry.name
 
711
            source_parent = source_entry.parent_id
 
712
            source_kind, source_executable, source_stat = \
 
713
                self.source._comparison_data(source_entry, source_path)
 
714
        else:
 
715
            source_versioned = False
 
716
            source_name = None
 
717
            source_parent = None
 
718
            source_kind = None
 
719
            source_executable = None
 
720
        if target_entry is not None:
 
721
            target_versioned = True
 
722
            target_name = target_entry.name
 
723
            target_parent = target_entry.parent_id
 
724
            target_kind, target_executable, target_stat = \
 
725
                self.target._comparison_data(target_entry, target_path)
 
726
        else:
 
727
            target_versioned = False
 
728
            target_name = None
 
729
            target_parent = None
 
730
            target_kind = None
 
731
            target_executable = None
 
732
        versioned = (source_versioned, target_versioned)
 
733
        kind = (source_kind, target_kind)
 
734
        changed_content = False
 
735
        if source_kind != target_kind:
 
736
            changed_content = True
 
737
        elif source_kind == 'file':
 
738
            if not self.file_content_matches(
 
739
                    source_path, target_path,
 
740
                    source_stat, target_stat):
 
741
                changed_content = True
 
742
        elif source_kind == 'symlink':
 
743
            if (self.source.get_symlink_target(source_path) !=
 
744
                    self.target.get_symlink_target(target_path)):
 
745
                changed_content = True
 
746
        elif source_kind == 'tree-reference':
 
747
            if (self.source.get_reference_revision(source_path)
 
748
                    != self.target.get_reference_revision(target_path)):
 
749
                changed_content = True
 
750
        parent = (source_parent, target_parent)
 
751
        name = (source_name, target_name)
 
752
        executable = (source_executable, target_executable)
 
753
        if (changed_content is not False or versioned[0] != versioned[1] or
 
754
            parent[0] != parent[1] or name[0] != name[1] or
 
755
                executable[0] != executable[1]):
 
756
            changes = True
 
757
        else:
 
758
            changes = False
 
759
        return (file_id, (source_path, target_path), changed_content,
 
760
                versioned, parent, name, kind, executable), changes
 
761
 
774
762
    def compare(self, want_unchanged=False, specific_files=None,
775
763
                extra_trees=None, require_versioned=False, include_root=False,
776
764
                want_unversioned=False):
790
778
            a PathsNotVersionedError will be thrown.
791
779
        :param want_unversioned: Scan for unversioned paths.
792
780
        """
793
 
        from . import delta
794
781
        trees = (self.source,)
795
782
        if extra_trees is not None:
796
783
            trees = trees + tuple(extra_trees)
836
823
            output. An unversioned file is defined as one with (False, False)
837
824
            for the versioned pair.
838
825
        """
839
 
        raise NotImplementedError(self.iter_changes)
 
826
        if not extra_trees:
 
827
            extra_trees = []
 
828
        else:
 
829
            extra_trees = list(extra_trees)
 
830
        # The ids of items we need to examine to insure delta consistency.
 
831
        precise_file_ids = set()
 
832
        changed_file_ids = []
 
833
        if specific_files == []:
 
834
            target_specific_files = []
 
835
            source_specific_files = []
 
836
        else:
 
837
            target_specific_files = self.target.find_related_paths_across_trees(
 
838
                specific_files, [self.source] + extra_trees,
 
839
                require_versioned=require_versioned)
 
840
            source_specific_files = self.source.find_related_paths_across_trees(
 
841
                specific_files, [self.target] + extra_trees,
 
842
                require_versioned=require_versioned)
 
843
        if specific_files is not None:
 
844
            # reparented or added entries must have their parents included
 
845
            # so that valid deltas can be created. The seen_parents set
 
846
            # tracks the parents that we need to have.
 
847
            # The seen_dirs set tracks directory entries we've yielded.
 
848
            # After outputting version object in to_entries we set difference
 
849
            # the two seen sets and start checking parents.
 
850
            seen_parents = set()
 
851
            seen_dirs = set()
 
852
        if want_unversioned:
 
853
            all_unversioned = sorted([(p.split('/'), p) for p in
 
854
                                      self.target.extras()
 
855
                                      if specific_files is None or
 
856
                                      osutils.is_inside_any(specific_files, p)])
 
857
            all_unversioned = deque(all_unversioned)
 
858
        else:
 
859
            all_unversioned = deque()
 
860
        to_paths = {}
 
861
        from_entries_by_dir = list(self.source.iter_entries_by_dir(
 
862
            specific_files=source_specific_files))
 
863
        from_data = dict((e.file_id, (p, e)) for p, e in from_entries_by_dir)
 
864
        to_entries_by_dir = list(self.target.iter_entries_by_dir(
 
865
            specific_files=target_specific_files))
 
866
        num_entries = len(from_entries_by_dir) + len(to_entries_by_dir)
 
867
        entry_count = 0
 
868
        # the unversioned path lookup only occurs on real trees - where there
 
869
        # can be extras. So the fake_entry is solely used to look up
 
870
        # executable it values when execute is not supported.
 
871
        fake_entry = TreeFile()
 
872
        for target_path, target_entry in to_entries_by_dir:
 
873
            while (all_unversioned and
 
874
                   all_unversioned[0][0] < target_path.split('/')):
 
875
                unversioned_path = all_unversioned.popleft()
 
876
                target_kind, target_executable, target_stat = \
 
877
                    self.target._comparison_data(
 
878
                        fake_entry, unversioned_path[1])
 
879
                yield (None, (None, unversioned_path[1]), True, (False, False),
 
880
                       (None, None),
 
881
                       (None, unversioned_path[0][-1]),
 
882
                       (None, target_kind),
 
883
                       (None, target_executable))
 
884
            source_path, source_entry = from_data.get(target_entry.file_id,
 
885
                                                      (None, None))
 
886
            result, changes = self._changes_from_entries(source_entry,
 
887
                                                         target_entry, source_path=source_path, target_path=target_path)
 
888
            to_paths[result[0]] = result[1][1]
 
889
            entry_count += 1
 
890
            if result[3][0]:
 
891
                entry_count += 1
 
892
            if pb is not None:
 
893
                pb.update('comparing files', entry_count, num_entries)
 
894
            if changes or include_unchanged:
 
895
                if specific_files is not None:
 
896
                    new_parent_id = result[4][1]
 
897
                    precise_file_ids.add(new_parent_id)
 
898
                    changed_file_ids.append(result[0])
 
899
                yield result
 
900
            # Ensure correct behaviour for reparented/added specific files.
 
901
            if specific_files is not None:
 
902
                # Record output dirs
 
903
                if result[6][1] == 'directory':
 
904
                    seen_dirs.add(result[0])
 
905
                # Record parents of reparented/added entries.
 
906
                versioned = result[3]
 
907
                parents = result[4]
 
908
                if not versioned[0] or parents[0] != parents[1]:
 
909
                    seen_parents.add(parents[1])
 
910
        while all_unversioned:
 
911
            # yield any trailing unversioned paths
 
912
            unversioned_path = all_unversioned.popleft()
 
913
            to_kind, to_executable, to_stat = \
 
914
                self.target._comparison_data(fake_entry, unversioned_path[1])
 
915
            yield (None, (None, unversioned_path[1]), True, (False, False),
 
916
                   (None, None),
 
917
                   (None, unversioned_path[0][-1]),
 
918
                   (None, to_kind),
 
919
                   (None, to_executable))
 
920
        # Yield all remaining source paths
 
921
        for path, from_entry in from_entries_by_dir:
 
922
            file_id = from_entry.file_id
 
923
            if file_id in to_paths:
 
924
                # already returned
 
925
                continue
 
926
            to_path = find_previous_path(self.source, self.target, path)
 
927
            entry_count += 1
 
928
            if pb is not None:
 
929
                pb.update('comparing files', entry_count, num_entries)
 
930
            versioned = (True, False)
 
931
            parent = (from_entry.parent_id, None)
 
932
            name = (from_entry.name, None)
 
933
            from_kind, from_executable, stat_value = \
 
934
                self.source._comparison_data(from_entry, path)
 
935
            kind = (from_kind, None)
 
936
            executable = (from_executable, None)
 
937
            changed_content = from_kind is not None
 
938
            # the parent's path is necessarily known at this point.
 
939
            changed_file_ids.append(file_id)
 
940
            yield(file_id, (path, to_path), changed_content, versioned, parent,
 
941
                  name, kind, executable)
 
942
        changed_file_ids = set(changed_file_ids)
 
943
        if specific_files is not None:
 
944
            for result in self._handle_precise_ids(precise_file_ids,
 
945
                                                   changed_file_ids):
 
946
                yield result
 
947
 
 
948
    def _get_entry(self, tree, path):
 
949
        """Get an inventory entry from a tree, with missing entries as None.
 
950
 
 
951
        If the tree raises NotImplementedError on accessing .inventory, then
 
952
        this is worked around using iter_entries_by_dir on just the file id
 
953
        desired.
 
954
 
 
955
        :param tree: The tree to lookup the entry in.
 
956
        :param path: The path to look up
 
957
        """
 
958
        # No inventory available.
 
959
        try:
 
960
            iterator = tree.iter_entries_by_dir(specific_files=[path])
 
961
            return next(iterator)[1]
 
962
        except StopIteration:
 
963
            return None
 
964
 
 
965
    def _handle_precise_ids(self, precise_file_ids, changed_file_ids,
 
966
                            discarded_changes=None):
 
967
        """Fill out a partial iter_changes to be consistent.
 
968
 
 
969
        :param precise_file_ids: The file ids of parents that were seen during
 
970
            the iter_changes.
 
971
        :param changed_file_ids: The file ids of already emitted items.
 
972
        :param discarded_changes: An optional dict of precalculated
 
973
            iter_changes items which the partial iter_changes had not output
 
974
            but had calculated.
 
975
        :return: A generator of iter_changes items to output.
 
976
        """
 
977
        # process parents of things that had changed under the users
 
978
        # requested paths to prevent incorrect paths or parent ids which
 
979
        # aren't in the tree.
 
980
        while precise_file_ids:
 
981
            precise_file_ids.discard(None)
 
982
            # Don't emit file_ids twice
 
983
            precise_file_ids.difference_update(changed_file_ids)
 
984
            if not precise_file_ids:
 
985
                break
 
986
            # If the there was something at a given output path in source, we
 
987
            # have to include the entry from source in the delta, or we would
 
988
            # be putting this entry into a used path.
 
989
            paths = []
 
990
            for parent_id in precise_file_ids:
 
991
                try:
 
992
                    paths.append(self.target.id2path(parent_id))
 
993
                except errors.NoSuchId:
 
994
                    # This id has been dragged in from the source by delta
 
995
                    # expansion and isn't present in target at all: we don't
 
996
                    # need to check for path collisions on it.
 
997
                    pass
 
998
            for path in paths:
 
999
                old_id = self.source.path2id(path)
 
1000
                precise_file_ids.add(old_id)
 
1001
            precise_file_ids.discard(None)
 
1002
            current_ids = precise_file_ids
 
1003
            precise_file_ids = set()
 
1004
            # We have to emit all of precise_file_ids that have been altered.
 
1005
            # We may have to output the children of some of those ids if any
 
1006
            # directories have stopped being directories.
 
1007
            for file_id in current_ids:
 
1008
                # Examine file_id
 
1009
                if discarded_changes:
 
1010
                    result = discarded_changes.get(file_id)
 
1011
                    source_entry = None
 
1012
                else:
 
1013
                    result = None
 
1014
                if result is None:
 
1015
                    try:
 
1016
                        source_path = self.source.id2path(file_id)
 
1017
                    except errors.NoSuchId:
 
1018
                        source_path = None
 
1019
                        source_entry = None
 
1020
                    else:
 
1021
                        source_entry = self._get_entry(
 
1022
                            self.source, source_path)
 
1023
                    try:
 
1024
                        target_path = self.target.id2path(file_id)
 
1025
                    except errors.NoSuchId:
 
1026
                        target_path = None
 
1027
                        target_entry = None
 
1028
                    else:
 
1029
                        target_entry = self._get_entry(
 
1030
                            self.target, target_path)
 
1031
                    result, changes = self._changes_from_entries(
 
1032
                        source_entry, target_entry, source_path, target_path)
 
1033
                else:
 
1034
                    changes = True
 
1035
                # Get this parents parent to examine.
 
1036
                new_parent_id = result[4][1]
 
1037
                precise_file_ids.add(new_parent_id)
 
1038
                if changes:
 
1039
                    if (result[6][0] == 'directory' and
 
1040
                            result[6][1] != 'directory'):
 
1041
                        # This stopped being a directory, the old children have
 
1042
                        # to be included.
 
1043
                        if source_entry is None:
 
1044
                            # Reusing a discarded change.
 
1045
                            source_entry = self._get_entry(
 
1046
                                self.source, result[1][0])
 
1047
                        precise_file_ids.update(
 
1048
                            child.file_id
 
1049
                            for child in self.source.iter_child_entries(result[1][0]))
 
1050
                    changed_file_ids.add(result[0])
 
1051
                    yield result
840
1052
 
841
1053
    def file_content_matches(
842
1054
            self, source_path, target_path,
848
1060
 
849
1061
        :param source_path: Path of the file in the source tree
850
1062
        :param target_path: Path of the file in the target tree
 
1063
        :param source_file_id: Optional file id of the file in the source tree
 
1064
        :param target_file_id: Optional file id of the file in the target tree
851
1065
        :param source_stat: Optional stat value of the file in the source tree
852
1066
        :param target_stat: Optional stat value of the file in the target tree
853
1067
        :return: Boolean indicating whether the files have the same contents
863
1077
            # Fall back to SHA1 for now
864
1078
            if source_verifier_kind != "SHA1":
865
1079
                source_sha1 = self.source.get_file_sha1(
866
 
                    source_path, source_stat)
 
1080
                    source_path, source_file_id, source_stat)
867
1081
            else:
868
1082
                source_sha1 = source_verifier_data
869
1083
            if target_verifier_kind != "SHA1":
870
1084
                target_sha1 = self.target.get_file_sha1(
871
 
                    target_path, target_stat)
 
1085
                    target_path, target_file_id, target_stat)
872
1086
            else:
873
1087
                target_sha1 = target_verifier_data
874
1088
            return (source_sha1 == target_sha1)
875
1089
 
876
 
    def find_target_path(self, path, recurse='none'):
877
 
        """Find target tree path.
878
 
 
879
 
        :param path: Path to search for (exists in source)
880
 
        :return: path in target, or None if there is no equivalent path.
881
 
        :raise NoSuchFile: If the path doesn't exist in source
882
 
        """
883
 
        raise NotImplementedError(self.find_target_path)
884
 
 
885
 
    def find_source_path(self, path, recurse='none'):
886
 
        """Find the source tree path.
887
 
 
888
 
        :param path: Path to search for (exists in target)
889
 
        :return: path in source, or None if there is no equivalent path.
890
 
        :raise NoSuchFile: if the path doesn't exist in target
891
 
        """
892
 
        raise NotImplementedError(self.find_source_path)
893
 
 
894
 
    def find_target_paths(self, paths, recurse='none'):
895
 
        """Find target tree paths.
896
 
 
897
 
        :param paths: Iterable over paths in target to search for
898
 
        :return: Dictionary mapping from source paths to paths in target , or
899
 
            None if there is no equivalent path.
900
 
        """
901
 
        ret = {}
902
 
        for path in paths:
903
 
            ret[path] = self.find_target_path(path, recurse=recurse)
904
 
        return ret
905
 
 
906
 
    def find_source_paths(self, paths, recurse='none'):
907
 
        """Find source tree paths.
908
 
 
909
 
        :param paths: Iterable over paths in target to search for
910
 
        :return: Dictionary mapping from target paths to paths in source, or
911
 
            None if there is no equivalent path.
912
 
        """
913
 
        ret = {}
914
 
        for path in paths:
915
 
            ret[path] = self.find_source_path(path, recurse=recurse)
916
 
        return ret
917
 
 
918
 
 
919
 
def find_previous_paths(from_tree, to_tree, paths, recurse='none'):
 
1090
 
 
1091
InterTree.register_optimiser(InterTree)
 
1092
 
 
1093
 
 
1094
class MultiWalker(object):
 
1095
    """Walk multiple trees simultaneously, getting combined results."""
 
1096
 
 
1097
    # Note: This could be written to not assume you can do out-of-order
 
1098
    #       lookups. Instead any nodes that don't match in all trees could be
 
1099
    #       marked as 'deferred', and then returned in the final cleanup loop.
 
1100
    #       For now, I think it is "nicer" to return things as close to the
 
1101
    #       "master_tree" order as we can.
 
1102
 
 
1103
    def __init__(self, master_tree, other_trees):
 
1104
        """Create a new MultiWalker.
 
1105
 
 
1106
        All trees being walked must implement "iter_entries_by_dir()", such
 
1107
        that they yield (path, object) tuples, where that object will have a
 
1108
        '.file_id' member, that can be used to check equality.
 
1109
 
 
1110
        :param master_tree: All trees will be 'slaved' to the master_tree such
 
1111
            that nodes in master_tree will be used as 'first-pass' sync points.
 
1112
            Any nodes that aren't in master_tree will be merged in a second
 
1113
            pass.
 
1114
        :param other_trees: A list of other trees to walk simultaneously.
 
1115
        """
 
1116
        self._master_tree = master_tree
 
1117
        self._other_trees = other_trees
 
1118
 
 
1119
        # Keep track of any nodes that were properly processed just out of
 
1120
        # order, that way we don't return them at the end, we don't have to
 
1121
        # track *all* processed file_ids, just the out-of-order ones
 
1122
        self._out_of_order_processed = set()
 
1123
 
 
1124
    @staticmethod
 
1125
    def _step_one(iterator):
 
1126
        """Step an iter_entries_by_dir iterator.
 
1127
 
 
1128
        :return: (has_more, path, ie)
 
1129
            If has_more is False, path and ie will be None.
 
1130
        """
 
1131
        try:
 
1132
            path, ie = next(iterator)
 
1133
        except StopIteration:
 
1134
            return False, None, None
 
1135
        else:
 
1136
            return True, path, ie
 
1137
 
 
1138
    @staticmethod
 
1139
    def _lt_path_by_dirblock(path1, path2):
 
1140
        """Compare two paths based on what directory they are in.
 
1141
 
 
1142
        This generates a sort order, such that all children of a directory are
 
1143
        sorted together, and grandchildren are in the same order as the
 
1144
        children appear. But all grandchildren come after all children.
 
1145
 
 
1146
        :param path1: first path
 
1147
        :param path2: the second path
 
1148
        :return: negative number if ``path1`` comes first,
 
1149
            0 if paths are equal
 
1150
            and a positive number if ``path2`` sorts first
 
1151
        """
 
1152
        # Shortcut this special case
 
1153
        if path1 == path2:
 
1154
            return False
 
1155
        # This is stolen from _dirstate_helpers_py.py, only switching it to
 
1156
        # Unicode objects. Consider using encode_utf8() and then using the
 
1157
        # optimized versions, or maybe writing optimized unicode versions.
 
1158
        if not isinstance(path1, text_type):
 
1159
            raise TypeError("'path1' must be a unicode string, not %s: %r"
 
1160
                            % (type(path1), path1))
 
1161
        if not isinstance(path2, text_type):
 
1162
            raise TypeError("'path2' must be a unicode string, not %s: %r"
 
1163
                            % (type(path2), path2))
 
1164
        return (MultiWalker._path_to_key(path1) <
 
1165
                MultiWalker._path_to_key(path2))
 
1166
 
 
1167
    @staticmethod
 
1168
    def _path_to_key(path):
 
1169
        dirname, basename = osutils.split(path)
 
1170
        return (dirname.split(u'/'), basename)
 
1171
 
 
1172
    def _lookup_by_file_id(self, extra_entries, other_tree, file_id):
 
1173
        """Lookup an inventory entry by file_id.
 
1174
 
 
1175
        This is called when an entry is missing in the normal order.
 
1176
        Generally this is because a file was either renamed, or it was
 
1177
        deleted/added. If the entry was found in the inventory and not in
 
1178
        extra_entries, it will be added to self._out_of_order_processed
 
1179
 
 
1180
        :param extra_entries: A dictionary of {file_id: (path, ie)}.  This
 
1181
            should be filled with entries that were found before they were
 
1182
            used. If file_id is present, it will be removed from the
 
1183
            dictionary.
 
1184
        :param other_tree: The Tree to search, in case we didn't find the entry
 
1185
            yet.
 
1186
        :param file_id: The file_id to look for
 
1187
        :return: (path, ie) if found or (None, None) if not present.
 
1188
        """
 
1189
        if file_id in extra_entries:
 
1190
            return extra_entries.pop(file_id)
 
1191
        # TODO: Is id2path better as the first call, or is
 
1192
        #       inventory[file_id] better as a first check?
 
1193
        try:
 
1194
            cur_path = other_tree.id2path(file_id)
 
1195
        except errors.NoSuchId:
 
1196
            cur_path = None
 
1197
        if cur_path is None:
 
1198
            return (None, None)
 
1199
        else:
 
1200
            self._out_of_order_processed.add(file_id)
 
1201
            cur_ie = other_tree.root_inventory.get_entry(file_id)
 
1202
            return (cur_path, cur_ie)
 
1203
 
 
1204
    def iter_all(self):
 
1205
        """Match up the values in the different trees."""
 
1206
        for result in self._walk_master_tree():
 
1207
            yield result
 
1208
        self._finish_others()
 
1209
        for result in self._walk_others():
 
1210
            yield result
 
1211
 
 
1212
    def _walk_master_tree(self):
 
1213
        """First pass, walk all trees in lock-step.
 
1214
 
 
1215
        When we are done, all nodes in the master_tree will have been
 
1216
        processed. _other_walkers, _other_entries, and _others_extra will be
 
1217
        set on 'self' for future processing.
 
1218
        """
 
1219
        # This iterator has the most "inlining" done, because it tends to touch
 
1220
        # every file in the tree, while the others only hit nodes that don't
 
1221
        # match.
 
1222
        master_iterator = self._master_tree.iter_entries_by_dir()
 
1223
 
 
1224
        other_walkers = [other.iter_entries_by_dir()
 
1225
                         for other in self._other_trees]
 
1226
        other_entries = [self._step_one(walker) for walker in other_walkers]
 
1227
        # Track extra nodes in the other trees
 
1228
        others_extra = [{} for _ in range(len(self._other_trees))]
 
1229
 
 
1230
        master_has_more = True
 
1231
        step_one = self._step_one
 
1232
        lookup_by_file_id = self._lookup_by_file_id
 
1233
        out_of_order_processed = self._out_of_order_processed
 
1234
 
 
1235
        while master_has_more:
 
1236
            (master_has_more, path, master_ie) = step_one(master_iterator)
 
1237
            if not master_has_more:
 
1238
                break
 
1239
 
 
1240
            file_id = master_ie.file_id
 
1241
            other_values = []
 
1242
            other_values_append = other_values.append
 
1243
            next_other_entries = []
 
1244
            next_other_entries_append = next_other_entries.append
 
1245
            for idx, (other_has_more, other_path, other_ie) in enumerate(other_entries):
 
1246
                if not other_has_more:
 
1247
                    other_values_append(lookup_by_file_id(
 
1248
                        others_extra[idx], self._other_trees[idx], file_id))
 
1249
                    next_other_entries_append((False, None, None))
 
1250
                elif file_id == other_ie.file_id:
 
1251
                    # This is the critical code path, as most of the entries
 
1252
                    # should match between most trees.
 
1253
                    other_values_append((other_path, other_ie))
 
1254
                    next_other_entries_append(step_one(other_walkers[idx]))
 
1255
                else:
 
1256
                    # This walker did not match, step it until it either
 
1257
                    # matches, or we know we are past the current walker.
 
1258
                    other_walker = other_walkers[idx]
 
1259
                    other_extra = others_extra[idx]
 
1260
                    while (other_has_more and
 
1261
                           self._lt_path_by_dirblock(other_path, path)):
 
1262
                        other_file_id = other_ie.file_id
 
1263
                        if other_file_id not in out_of_order_processed:
 
1264
                            other_extra[other_file_id] = (other_path, other_ie)
 
1265
                        other_has_more, other_path, other_ie = \
 
1266
                            step_one(other_walker)
 
1267
                    if other_has_more and other_ie.file_id == file_id:
 
1268
                        # We ended up walking to this point, match and step
 
1269
                        # again
 
1270
                        other_values_append((other_path, other_ie))
 
1271
                        other_has_more, other_path, other_ie = \
 
1272
                            step_one(other_walker)
 
1273
                    else:
 
1274
                        # This record isn't in the normal order, see if it
 
1275
                        # exists at all.
 
1276
                        other_values_append(lookup_by_file_id(
 
1277
                            other_extra, self._other_trees[idx], file_id))
 
1278
                    next_other_entries_append((other_has_more, other_path,
 
1279
                                               other_ie))
 
1280
            other_entries = next_other_entries
 
1281
 
 
1282
            # We've matched all the walkers, yield this datapoint
 
1283
            yield path, file_id, master_ie, other_values
 
1284
        self._other_walkers = other_walkers
 
1285
        self._other_entries = other_entries
 
1286
        self._others_extra = others_extra
 
1287
 
 
1288
    def _finish_others(self):
 
1289
        """Finish walking the other iterators, so we get all entries."""
 
1290
        for idx, info in enumerate(self._other_entries):
 
1291
            other_extra = self._others_extra[idx]
 
1292
            (other_has_more, other_path, other_ie) = info
 
1293
            while other_has_more:
 
1294
                other_file_id = other_ie.file_id
 
1295
                if other_file_id not in self._out_of_order_processed:
 
1296
                    other_extra[other_file_id] = (other_path, other_ie)
 
1297
                other_has_more, other_path, other_ie = \
 
1298
                    self._step_one(self._other_walkers[idx])
 
1299
        del self._other_entries
 
1300
 
 
1301
    def _walk_others(self):
 
1302
        """Finish up by walking all the 'deferred' nodes."""
 
1303
        # TODO: One alternative would be to grab all possible unprocessed
 
1304
        #       file_ids, and then sort by path, and then yield them. That
 
1305
        #       might ensure better ordering, in case a caller strictly
 
1306
        #       requires parents before children.
 
1307
        for idx, other_extra in enumerate(self._others_extra):
 
1308
            others = sorted(viewvalues(other_extra),
 
1309
                            key=lambda x: self._path_to_key(x[0]))
 
1310
            for other_path, other_ie in others:
 
1311
                file_id = other_ie.file_id
 
1312
                # We don't need to check out_of_order_processed here, because
 
1313
                # the lookup_by_file_id will be removing anything processed
 
1314
                # from the extras cache
 
1315
                other_extra.pop(file_id)
 
1316
                other_values = [(None, None)] * idx
 
1317
                other_values.append((other_path, other_ie))
 
1318
                for alt_idx, alt_extra in enumerate(self._others_extra[idx + 1:]):
 
1319
                    alt_idx = alt_idx + idx + 1
 
1320
                    alt_extra = self._others_extra[alt_idx]
 
1321
                    alt_tree = self._other_trees[alt_idx]
 
1322
                    other_values.append(self._lookup_by_file_id(
 
1323
                        alt_extra, alt_tree, file_id))
 
1324
                yield other_path, file_id, None, other_values
 
1325
 
 
1326
 
 
1327
def find_previous_paths(from_tree, to_tree, paths):
920
1328
    """Find previous tree paths.
921
1329
 
922
1330
    :param from_tree: From tree
923
1331
    :param to_tree: To tree
924
 
    :param paths: Iterable over paths in from_tree to search for
 
1332
    :param paths: Iterable over paths to search for
925
1333
    :return: Dictionary mapping from from_tree paths to paths in to_tree, or
926
1334
        None if there is no equivalent path.
927
1335
    """
928
 
    return InterTree.get(to_tree, from_tree).find_source_paths(paths, recurse=recurse)
929
 
 
930
 
 
931
 
def find_previous_path(from_tree, to_tree, path, recurse='none'):
 
1336
    ret = {}
 
1337
    for path in paths:
 
1338
        ret[path] = find_previous_path(from_tree, to_tree, path)
 
1339
    return ret
 
1340
 
 
1341
 
 
1342
def find_previous_path(from_tree, to_tree, path, file_id=None):
932
1343
    """Find previous tree path.
933
1344
 
934
1345
    :param from_tree: From tree
935
1346
    :param to_tree: To tree
936
 
    :param path: Path to search for (exists in from_tree)
 
1347
    :param path: Path to search for
937
1348
    :return: path in to_tree, or None if there is no equivalent path.
938
 
    :raise NoSuchFile: If the path doesn't exist in from_tree
939
1349
    """
940
 
    return InterTree.get(to_tree, from_tree).find_source_path(
941
 
        path, recurse=recurse)
 
1350
    if file_id is None:
 
1351
        file_id = from_tree.path2id(path)
 
1352
    if file_id is None:
 
1353
        raise errors.NoSuchFile(path)
 
1354
    try:
 
1355
        return to_tree.id2path(file_id)
 
1356
    except errors.NoSuchId:
 
1357
        return None
942
1358
 
943
1359
 
944
1360
def get_canonical_path(tree, path, normalize):