108
108
trees or versioned trees.
111
def supports_rename_tracking(self):
112
"""Whether this tree supports rename tracking.
114
This defaults to True, but some implementations may want to override
111
119
def has_versioned_directories(self):
112
120
"""Whether this tree can contain explicitly versioned directories.
206
214
raise NotImplementedError(self.id2path)
208
def iter_entries_by_dir(self, specific_file_ids=None, yield_parents=False):
216
def iter_entries_by_dir(self, specific_files=None, yield_parents=False):
209
217
"""Walk the tree in 'by_dir' order.
211
219
This will yield each entry in the tree as a (path, entry) tuple.
231
239
a, f, a/b, a/d, a/b/c, a/d/e, f/g
233
241
:param yield_parents: If True, yield the parents from the root leading
234
down to specific_file_ids that have been requested. This has no
235
impact if specific_file_ids is None.
242
down to specific_files that have been requested. This has no
243
impact if specific_files is None.
237
245
raise NotImplementedError(self.iter_entries_by_dir)
310
318
raise NotImplementedError(self._comparison_data)
312
def _file_size(self, entry, stat_value):
313
raise NotImplementedError(self._file_size)
315
320
def get_file(self, path, file_id=None):
316
321
"""Return a file object for the file file_id in the tree.
482
487
except errors.NoSuchRevisionInTree:
483
488
yield self.repository.revision_tree(revision_id)
485
def _check_retrieved(self, ie, f):
488
fp = osutils.fingerprint_file(f)
491
if ie.text_size is not None:
492
if ie.text_size != fp['size']:
493
raise errors.BzrError(
494
"mismatched size for file %r in %r" %
495
(ie.file_id, self._store),
496
["inventory expects %d bytes" % ie.text_size,
497
"file is actually %d bytes" % fp['size'],
498
"store is probably damaged/corrupt"])
500
if ie.text_sha1 != fp['sha1']:
501
raise errors.BzrError("wrong SHA-1 for file %r in %r" %
502
(ie.file_id, self._store),
503
["inventory expects %s" % ie.text_sha1,
504
"file is actually %s" % fp['sha1'],
505
"store is probably damaged/corrupt"])
507
490
def path2id(self, path):
508
491
"""Return the id for path in this tree."""
509
492
raise NotImplementedError(self.path2id)
517
500
return self.path2id(path) is not None
519
def paths2ids(self, paths, trees=[], require_versioned=True):
520
"""Return all the ids that can be reached by walking from paths.
522
Each path is looked up in this tree and any extras provided in
523
trees, and this is repeated recursively: the children in an extra tree
524
of a directory that has been renamed under a provided path in this tree
525
are all returned, even if none exist under a provided path in this
526
tree, and vice versa.
528
:param paths: An iterable of paths to start converting to ids from.
529
Alternatively, if paths is None, no ids should be calculated and None
530
will be returned. This is offered to make calling the api unconditional
531
for code that *might* take a list of files.
532
:param trees: Additional trees to consider.
533
:param require_versioned: If False, do not raise NotVersionedError if
534
an element of paths is not versioned in this tree and all of trees.
536
return find_ids_across_trees(paths, [self] + list(trees), require_versioned)
538
def iter_children(self, file_id):
539
"""Iterate over the file ids of the children of an entry.
541
:param file_id: File id of the entry
542
:return: Iterator over child file ids.
544
raise NotImplementedError(self.iter_children)
502
def find_related_paths_across_trees(self, paths, trees=[],
503
require_versioned=True):
504
"""Find related paths in tree corresponding to specified filenames in any
507
All matches in all trees will be used, and all children of matched
508
directories will be used.
510
:param paths: The filenames to find related paths for (if None, returns
512
:param trees: The trees to find file_ids within
513
:param require_versioned: if true, all specified filenames must occur in
515
:return: a set of paths for the specified filenames and their children
518
raise NotImplementedError(self.find_related_paths_across_trees)
546
520
def lock_read(self):
547
521
"""Lock this tree for multiple read only operations.
618
592
def supports_content_filtering(self):
621
def _content_filter_stack(self, path=None, file_id=None):
595
def _content_filter_stack(self, path=None):
622
596
"""The stack of content filters for a path if filtering is supported.
624
598
Readers will be applied in first-to-last order.
628
602
:param path: path relative to the root of the tree
629
603
or None if unknown
630
:param file_id: file_id or None if unknown
631
604
:return: the list of filters - [] if there are none
633
606
filter_pref_names = filters._get_registered_names()
634
607
if len(filter_pref_names) == 0:
637
path = self.id2path(file_id)
638
609
prefs = next(self.iter_search_rules([path], filter_pref_names))
639
610
stk = filters._get_filter_stack_for(prefs)
640
611
if 'filters' in debug.debug_flags:
686
def find_ids_across_trees(filenames, trees, require_versioned=True):
687
"""Find the ids corresponding to specified filenames.
689
All matches in all trees will be used, and all children of matched
690
directories will be used.
692
:param filenames: The filenames to find file_ids for (if None, returns
694
:param trees: The trees to find file_ids within
695
:param require_versioned: if true, all specified filenames must occur in
697
:return: a set of file ids for the specified filenames and their children.
701
specified_path_ids = _find_ids_across_trees(filenames, trees,
703
return _find_children_across_trees(specified_path_ids, trees)
706
def _find_ids_across_trees(filenames, trees, require_versioned):
707
"""Find the ids corresponding to specified filenames.
709
All matches in all trees will be used, but subdirectories are not scanned.
711
:param filenames: The filenames to find file_ids for
712
:param trees: The trees to find file_ids within
713
:param require_versioned: if true, all specified filenames must occur in
715
:return: a set of file ids for the specified filenames
718
interesting_ids = set()
719
for tree_path in filenames:
722
file_id = tree.path2id(tree_path)
723
if file_id is not None:
724
interesting_ids.add(file_id)
727
not_versioned.append(tree_path)
728
if len(not_versioned) > 0 and require_versioned:
729
raise errors.PathsNotVersionedError(not_versioned)
730
return interesting_ids
733
def _find_children_across_trees(specified_ids, trees):
734
"""Return a set including specified ids and their children.
736
All matches in all trees will be used.
738
:param trees: The trees to find file_ids within
739
:return: a set containing all specified ids and their children
741
interesting_ids = set(specified_ids)
742
pending = interesting_ids
743
# now handle children of interesting ids
744
# we loop so that we handle all children of each id in both trees
745
while len(pending) > 0:
747
for file_id in pending:
749
if not tree.has_or_had_id(file_id):
751
for child_id in tree.iter_children(file_id):
752
if child_id not in interesting_ids:
753
new_pending.add(child_id)
754
interesting_ids.update(new_pending)
755
pending = new_pending
756
return interesting_ids
759
657
class InterTree(InterObject):
760
658
"""This class represents operations taking place between two Trees.
781
679
# it works for all trees.
784
def _changes_from_entries(self, source_entry, target_entry,
785
source_path=None, target_path=None):
682
def _changes_from_entries(self, source_entry, target_entry, source_path,
786
684
"""Generate a iter_changes tuple between source_entry and target_entry.
788
686
:param source_entry: An inventory entry from self.source, or None.
789
687
:param target_entry: An inventory entry from self.target, or None.
790
:param source_path: The path of source_entry, if known. If not known
791
it will be looked up.
792
:param target_path: The path of target_entry, if known. If not known
793
it will be looked up.
688
:param source_path: The path of source_entry.
689
:param target_path: The path of target_entry.
794
690
:return: A tuple, item 0 of which is an iter_changes result tuple, and
795
691
item 1 is True if there are any changes in the result tuple.
804
700
source_versioned = True
805
701
source_name = source_entry.name
806
702
source_parent = source_entry.parent_id
807
if source_path is None:
808
source_path = self.source.id2path(file_id)
809
703
source_kind, source_executable, source_stat = \
810
704
self.source._comparison_data(source_entry, source_path)
818
712
target_versioned = True
819
713
target_name = target_entry.name
820
714
target_parent = target_entry.parent_id
821
if target_path is None:
822
target_path = self.target.id2path(file_id)
823
715
target_kind, target_executable, target_stat = \
824
716
self.target._comparison_data(target_entry, target_path)
834
726
if source_kind != target_kind:
835
727
changed_content = True
836
728
elif source_kind == 'file':
837
if not self.file_content_matches(file_id, file_id, source_path,
838
target_path, source_stat, target_stat):
729
if not self.file_content_matches(
730
source_path, target_path,
731
file_id, file_id, source_stat, target_stat):
839
732
changed_content = True
840
733
elif source_kind == 'symlink':
841
734
if (self.source.get_symlink_target(source_path, file_id) !=
880
773
if extra_trees is not None:
881
774
trees = trees + tuple(extra_trees)
882
775
with self.lock_read():
883
# target is usually the newer tree:
884
specific_file_ids = self.target.paths2ids(specific_files, trees,
885
require_versioned=require_versioned)
886
if specific_files and not specific_file_ids:
887
# All files are unversioned, so just return an empty delta
888
# _compare_trees would think we want a complete delta
889
result = delta.TreeDelta()
890
fake_entry = inventory.InventoryFile('unused', 'unused', 'unused')
891
result.unversioned = [(path, None,
892
self.target._comparison_data(fake_entry, path)[0]) for path in
895
776
return delta._compare_trees(self.source, self.target, want_unchanged,
896
777
specific_files, include_root, extra_trees=extra_trees,
897
778
require_versioned=require_versioned,
933
814
output. An unversioned file is defined as one with (False, False)
934
815
for the versioned pair.
936
lookup_trees = [self.source]
938
lookup_trees.extend(extra_trees)
820
extra_trees = list(extra_trees)
939
821
# The ids of items we need to examine to insure delta consistency.
940
822
precise_file_ids = set()
941
823
changed_file_ids = []
942
824
if specific_files == []:
943
specific_file_ids = []
825
target_specific_files = []
826
source_specific_files = []
945
specific_file_ids = self.target.paths2ids(specific_files,
946
lookup_trees, require_versioned=require_versioned)
828
target_specific_files = self.target.find_related_paths_across_trees(
829
specific_files, [self.source] + extra_trees,
830
require_versioned=require_versioned)
831
source_specific_files = self.source.find_related_paths_across_trees(
832
specific_files, [self.target] + extra_trees,
833
require_versioned=require_versioned)
947
834
if specific_files is not None:
948
835
# reparented or added entries must have their parents included
949
836
# so that valid deltas can be created. The seen_parents set
963
850
all_unversioned = collections.deque()
965
852
from_entries_by_dir = list(self.source.iter_entries_by_dir(
966
specific_file_ids=specific_file_ids))
853
specific_files=source_specific_files))
967
854
from_data = dict((e.file_id, (p, e)) for p, e in from_entries_by_dir)
968
855
to_entries_by_dir = list(self.target.iter_entries_by_dir(
969
specific_file_ids=specific_file_ids))
856
specific_files=target_specific_files))
970
857
num_entries = len(from_entries_by_dir) + len(to_entries_by_dir)
972
859
# the unversioned path lookup only occurs on real trees - where there
995
882
if pb is not None:
996
883
pb.update('comparing files', entry_count, num_entries)
997
884
if changes or include_unchanged:
998
if specific_file_ids is not None:
885
if specific_files is not None:
999
886
new_parent_id = result[4][1]
1000
887
precise_file_ids.add(new_parent_id)
1001
888
changed_file_ids.append(result[0])
1026
913
if file_id in to_paths:
1027
914
# already returned
1029
if not self.target.has_id(file_id):
1030
# common case - paths we have not emitted are not present in
1034
to_path = self.target.id2path(file_id)
916
to_path = find_previous_path(self.source, self.target, path)
1035
917
entry_count += 1
1036
918
if pb is not None:
1037
919
pb.update('comparing files', entry_count, num_entries)
1048
930
yield(file_id, (path, to_path), changed_content, versioned, parent,
1049
931
name, kind, executable)
1050
932
changed_file_ids = set(changed_file_ids)
1051
if specific_file_ids is not None:
933
if specific_files is not None:
1052
934
for result in self._handle_precise_ids(precise_file_ids,
1053
935
changed_file_ids):
1056
def _get_entry(self, tree, file_id):
938
def _get_entry(self, tree, path):
1057
939
"""Get an inventory entry from a tree, with missing entries as None.
1059
941
If the tree raises NotImplementedError on accessing .inventory, then
1063
945
:param tree: The tree to lookup the entry in.
1064
946
:param file_id: The file_id to lookup.
948
# No inventory available.
1067
inventory = tree.root_inventory
1068
except (AttributeError, NotImplementedError):
1069
# No inventory available.
1071
iterator = tree.iter_entries_by_dir(specific_file_ids=[file_id])
1072
return iterator.next()[1]
1073
except StopIteration:
1077
return inventory[file_id]
1078
except errors.NoSuchId:
950
iterator = tree.iter_entries_by_dir(specific_files=[path])
951
return iterator.next()[1]
952
except StopIteration:
1081
955
def _handle_precise_ids(self, precise_file_ids, changed_file_ids,
1082
956
discarded_changes=None):
1124
998
# Examine file_id
1125
999
if discarded_changes:
1126
1000
result = discarded_changes.get(file_id)
1130
1004
if result is None:
1131
old_entry = self._get_entry(self.source, file_id)
1132
new_entry = self._get_entry(self.target, file_id)
1006
source_path = self.source.id2path(file_id)
1007
except errors.NoSuchId:
1011
source_entry = self._get_entry(self.source, source_path)
1013
target_path = self.target.id2path(file_id)
1014
except errors.NoSuchId:
1018
target_entry = self._get_entry(self.target, target_path)
1133
1019
result, changes = self._changes_from_entries(
1134
old_entry, new_entry)
1020
source_entry, target_entry, source_path, target_path)
1137
1023
# Get this parents parent to examine.
1142
1028
result[6][1] != 'directory'):
1143
1029
# This stopped being a directory, the old children have
1144
1030
# to be included.
1145
if old_entry is None:
1031
if source_entry is None:
1146
1032
# Reusing a discarded change.
1147
old_entry = self._get_entry(self.source, file_id)
1033
source_entry = self._get_entry(self.source, result[1][0])
1148
1034
precise_file_ids.update(
1149
self.source.iter_children(file_id))
1036
for child in self.source.iter_child_entries(result[1][0]))
1150
1037
changed_file_ids.add(result[0])
1153
1040
def file_content_matches(
1154
self, source_file_id, target_file_id, source_path=None,
1155
target_path=None, source_stat=None, target_stat=None):
1041
self, source_path, target_path,
1042
source_file_id=None, target_file_id=None,
1043
source_stat=None, target_stat=None):
1156
1044
"""Check if two files are the same in the source and target trees.
1158
1046
This only checks that the contents of the files are the same,
1159
1047
it does not touch anything else.
1161
:param source_file_id: File id of the file in the source tree
1162
:param target_file_id: File id of the file in the target tree
1163
1049
:param source_path: Path of the file in the source tree
1164
1050
:param target_path: Path of the file in the target tree
1051
:param source_file_id: Optional file id of the file in the source tree
1052
:param target_file_id: Optional file id of the file in the target tree
1165
1053
:param source_stat: Optional stat value of the file in the source tree
1166
1054
:param target_stat: Optional stat value of the file in the target tree
1167
1055
:return: Boolean indicating whether the files have the same contents
1169
1057
with self.lock_read():
1170
if source_path is None:
1171
source_path = self.source.id2path(source_file_id)
1172
if target_path is None:
1173
target_path = self.target.id2path(target_file_id)
1174
1058
source_verifier_kind, source_verifier_data = (
1175
1059
self.source.get_file_verifier(
1176
1060
source_path, source_file_id, source_stat))
1426
1310
other_values.append(self._lookup_by_file_id(
1427
1311
alt_extra, alt_tree, file_id))
1428
1312
yield other_path, file_id, None, other_values
1315
def find_previous_paths(from_tree, to_tree, paths):
1316
"""Find previous tree paths.
1318
:param from_tree: From tree
1319
:param to_tree: To tree
1320
:param paths: Iterable over paths to search for
1321
:return: Dictionary mapping from from_tree paths to paths in to_tree, or
1322
None if there is no equivalent path.
1326
ret[path] = find_previous_path(from_tree, to_tree, path)
1330
def find_previous_path(from_tree, to_tree, path, file_id=None):
1331
"""Find previous tree path.
1333
:param from_tree: From tree
1334
:param to_tree: To tree
1335
:param path: Path to search for
1336
:return: path in to_tree, or None if there is no equivalent path.
1339
file_id = from_tree.path2id(path)
1341
raise errors.NoSuchFile(path)
1343
return to_tree.id2path(file_id)
1344
except errors.NoSuchId: