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)
433
438
this implementation, it is a tuple containing a single bytestring with
434
439
the complete text of the file.
436
:param desired_files: a list of (file_id, identifier) pairs
441
:param desired_files: a list of (path, identifier) pairs
438
for file_id, identifier in desired_files:
443
for path, identifier in desired_files:
439
444
# We wrap the string in a tuple so that we can return an iterable
440
445
# of bytestrings. (Technically, a bytestring is also an iterable
441
446
# of bytestrings, but iterating through each character is not
443
# TODO(jelmer): Pass paths into iter_files_bytes
444
path = self.id2path(file_id)
445
cur_file = (self.get_file_text(path, file_id),)
448
cur_file = (self.get_file_text(path),)
446
449
yield identifier, cur_file
448
451
def get_symlink_target(self, path, file_id=None):
477
480
raise NotImplementedError(self.annotate_iter)
479
def _get_plan_merge_data(self, file_id, other, base):
480
from .bzr import versionedfile
481
vf = versionedfile._PlanMergeVersionedFile(file_id)
482
last_revision_a = self._get_file_revision(
483
self.id2path(file_id), file_id, vf, 'this:')
484
last_revision_b = other._get_file_revision(
485
other.id2path(file_id), file_id, vf, 'other:')
487
last_revision_base = None
489
last_revision_base = base._get_file_revision(
490
base.id2path(file_id), file_id, vf, 'base:')
491
return vf, last_revision_a, last_revision_b, last_revision_base
493
def plan_file_merge(self, file_id, other, base=None):
494
"""Generate a merge plan based on annotations.
496
If the file contains uncommitted changes in this tree, they will be
497
attributed to the 'current:' pseudo-revision. If the file contains
498
uncommitted changes in the other tree, they will be assigned to the
499
'other:' pseudo-revision.
501
data = self._get_plan_merge_data(file_id, other, base)
502
vf, last_revision_a, last_revision_b, last_revision_base = data
503
return vf.plan_merge(last_revision_a, last_revision_b,
506
def plan_file_lca_merge(self, file_id, other, base=None):
507
"""Generate a merge plan based lca-newness.
509
If the file contains uncommitted changes in this tree, they will be
510
attributed to the 'current:' pseudo-revision. If the file contains
511
uncommitted changes in the other tree, they will be assigned to the
512
'other:' pseudo-revision.
514
data = self._get_plan_merge_data(file_id, other, base)
515
vf, last_revision_a, last_revision_b, last_revision_base = data
516
return vf.plan_lca_merge(last_revision_a, last_revision_b,
519
482
def _iter_parent_trees(self):
520
483
"""Iterate through parent trees, defaulting to Tree.revision_tree."""
521
484
for revision_id in self.get_parent_ids():
524
487
except errors.NoSuchRevisionInTree:
525
488
yield self.repository.revision_tree(revision_id)
527
def _get_file_revision(self, path, file_id, vf, tree_revision):
528
"""Ensure that file_id, tree_revision is in vf to plan the merge."""
529
if getattr(self, '_repository', None) is None:
530
last_revision = tree_revision
531
parent_keys = [(file_id, t.get_file_revision(path, file_id)) for t in
532
self._iter_parent_trees()]
533
vf.add_lines((file_id, last_revision), parent_keys,
534
self.get_file_lines(path, file_id))
535
repo = self.branch.repository
538
last_revision = self.get_file_revision(path, file_id)
539
base_vf = self._repository.texts
540
if base_vf not in vf.fallback_versionedfiles:
541
vf.fallback_versionedfiles.append(base_vf)
544
def _check_retrieved(self, ie, f):
547
fp = osutils.fingerprint_file(f)
550
if ie.text_size is not None:
551
if ie.text_size != fp['size']:
552
raise errors.BzrError(
553
"mismatched size for file %r in %r" %
554
(ie.file_id, self._store),
555
["inventory expects %d bytes" % ie.text_size,
556
"file is actually %d bytes" % fp['size'],
557
"store is probably damaged/corrupt"])
559
if ie.text_sha1 != fp['sha1']:
560
raise errors.BzrError("wrong SHA-1 for file %r in %r" %
561
(ie.file_id, self._store),
562
["inventory expects %s" % ie.text_sha1,
563
"file is actually %s" % fp['sha1'],
564
"store is probably damaged/corrupt"])
566
490
def path2id(self, path):
567
491
"""Return the id for path in this tree."""
568
492
raise NotImplementedError(self.path2id)
576
500
return self.path2id(path) is not None
578
def paths2ids(self, paths, trees=[], require_versioned=True):
579
"""Return all the ids that can be reached by walking from paths.
581
Each path is looked up in this tree and any extras provided in
582
trees, and this is repeated recursively: the children in an extra tree
583
of a directory that has been renamed under a provided path in this tree
584
are all returned, even if none exist under a provided path in this
585
tree, and vice versa.
587
:param paths: An iterable of paths to start converting to ids from.
588
Alternatively, if paths is None, no ids should be calculated and None
589
will be returned. This is offered to make calling the api unconditional
590
for code that *might* take a list of files.
591
:param trees: Additional trees to consider.
592
:param require_versioned: If False, do not raise NotVersionedError if
593
an element of paths is not versioned in this tree and all of trees.
595
return find_ids_across_trees(paths, [self] + list(trees), require_versioned)
597
def iter_children(self, file_id):
598
"""Iterate over the file ids of the children of an entry.
600
:param file_id: File id of the entry
601
:return: Iterator over child file ids.
603
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)
605
520
def lock_read(self):
606
521
"""Lock this tree for multiple read only operations.
687
602
:param path: path relative to the root of the tree
688
603
or None if unknown
689
:param file_id: file_id or None if unknown
690
604
:return: the list of filters - [] if there are none
692
606
filter_pref_names = filters._get_registered_names()
693
607
if len(filter_pref_names) == 0:
696
path = self.id2path(file_id)
697
609
prefs = next(self.iter_search_rules([path], filter_pref_names))
698
610
stk = filters._get_filter_stack_for(prefs)
699
611
if 'filters' in debug.debug_flags:
745
def find_ids_across_trees(filenames, trees, require_versioned=True):
746
"""Find the ids corresponding to specified filenames.
748
All matches in all trees will be used, and all children of matched
749
directories will be used.
751
:param filenames: The filenames to find file_ids for (if None, returns
753
:param trees: The trees to find file_ids within
754
:param require_versioned: if true, all specified filenames must occur in
756
:return: a set of file ids for the specified filenames and their children.
760
specified_path_ids = _find_ids_across_trees(filenames, trees,
762
return _find_children_across_trees(specified_path_ids, trees)
765
def _find_ids_across_trees(filenames, trees, require_versioned):
766
"""Find the ids corresponding to specified filenames.
768
All matches in all trees will be used, but subdirectories are not scanned.
770
:param filenames: The filenames to find file_ids for
771
:param trees: The trees to find file_ids within
772
:param require_versioned: if true, all specified filenames must occur in
774
:return: a set of file ids for the specified filenames
777
interesting_ids = set()
778
for tree_path in filenames:
781
file_id = tree.path2id(tree_path)
782
if file_id is not None:
783
interesting_ids.add(file_id)
786
not_versioned.append(tree_path)
787
if len(not_versioned) > 0 and require_versioned:
788
raise errors.PathsNotVersionedError(not_versioned)
789
return interesting_ids
792
def _find_children_across_trees(specified_ids, trees):
793
"""Return a set including specified ids and their children.
795
All matches in all trees will be used.
797
:param trees: The trees to find file_ids within
798
:return: a set containing all specified ids and their children
800
interesting_ids = set(specified_ids)
801
pending = interesting_ids
802
# now handle children of interesting ids
803
# we loop so that we handle all children of each id in both trees
804
while len(pending) > 0:
806
for file_id in pending:
808
if not tree.has_or_had_id(file_id):
810
for child_id in tree.iter_children(file_id):
811
if child_id not in interesting_ids:
812
new_pending.add(child_id)
813
interesting_ids.update(new_pending)
814
pending = new_pending
815
return interesting_ids
818
657
class InterTree(InterObject):
819
658
"""This class represents operations taking place between two Trees.
840
679
# it works for all trees.
843
def _changes_from_entries(self, source_entry, target_entry,
844
source_path=None, target_path=None):
682
def _changes_from_entries(self, source_entry, target_entry, source_path,
845
684
"""Generate a iter_changes tuple between source_entry and target_entry.
847
686
:param source_entry: An inventory entry from self.source, or None.
848
687
:param target_entry: An inventory entry from self.target, or None.
849
:param source_path: The path of source_entry, if known. If not known
850
it will be looked up.
851
:param target_path: The path of target_entry, if known. If not known
852
it will be looked up.
688
:param source_path: The path of source_entry.
689
:param target_path: The path of target_entry.
853
690
:return: A tuple, item 0 of which is an iter_changes result tuple, and
854
691
item 1 is True if there are any changes in the result tuple.
893
726
if source_kind != target_kind:
894
727
changed_content = True
895
728
elif source_kind == 'file':
896
if not self.file_content_matches(file_id, file_id, source_path,
897
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):
898
732
changed_content = True
899
733
elif source_kind == 'symlink':
900
734
if (self.source.get_symlink_target(source_path, file_id) !=
939
773
if extra_trees is not None:
940
774
trees = trees + tuple(extra_trees)
941
775
with self.lock_read():
942
# target is usually the newer tree:
943
specific_file_ids = self.target.paths2ids(specific_files, trees,
944
require_versioned=require_versioned)
945
if specific_files and not specific_file_ids:
946
# All files are unversioned, so just return an empty delta
947
# _compare_trees would think we want a complete delta
948
result = delta.TreeDelta()
949
fake_entry = inventory.InventoryFile('unused', 'unused', 'unused')
950
result.unversioned = [(path, None,
951
self.target._comparison_data(fake_entry, path)[0]) for path in
954
776
return delta._compare_trees(self.source, self.target, want_unchanged,
955
777
specific_files, include_root, extra_trees=extra_trees,
956
778
require_versioned=require_versioned,
992
814
output. An unversioned file is defined as one with (False, False)
993
815
for the versioned pair.
995
lookup_trees = [self.source]
997
lookup_trees.extend(extra_trees)
820
extra_trees = list(extra_trees)
998
821
# The ids of items we need to examine to insure delta consistency.
999
822
precise_file_ids = set()
1000
823
changed_file_ids = []
1001
824
if specific_files == []:
1002
specific_file_ids = []
825
target_specific_files = []
826
source_specific_files = []
1004
specific_file_ids = self.target.paths2ids(specific_files,
1005
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)
1006
834
if specific_files is not None:
1007
835
# reparented or added entries must have their parents included
1008
836
# so that valid deltas can be created. The seen_parents set
1022
850
all_unversioned = collections.deque()
1024
852
from_entries_by_dir = list(self.source.iter_entries_by_dir(
1025
specific_file_ids=specific_file_ids))
853
specific_files=source_specific_files))
1026
854
from_data = dict((e.file_id, (p, e)) for p, e in from_entries_by_dir)
1027
855
to_entries_by_dir = list(self.target.iter_entries_by_dir(
1028
specific_file_ids=specific_file_ids))
856
specific_files=target_specific_files))
1029
857
num_entries = len(from_entries_by_dir) + len(to_entries_by_dir)
1031
859
# the unversioned path lookup only occurs on real trees - where there
1054
882
if pb is not None:
1055
883
pb.update('comparing files', entry_count, num_entries)
1056
884
if changes or include_unchanged:
1057
if specific_file_ids is not None:
885
if specific_files is not None:
1058
886
new_parent_id = result[4][1]
1059
887
precise_file_ids.add(new_parent_id)
1060
888
changed_file_ids.append(result[0])
1107
930
yield(file_id, (path, to_path), changed_content, versioned, parent,
1108
931
name, kind, executable)
1109
932
changed_file_ids = set(changed_file_ids)
1110
if specific_file_ids is not None:
933
if specific_files is not None:
1111
934
for result in self._handle_precise_ids(precise_file_ids,
1112
935
changed_file_ids):
1115
def _get_entry(self, tree, file_id):
938
def _get_entry(self, tree, path):
1116
939
"""Get an inventory entry from a tree, with missing entries as None.
1118
941
If the tree raises NotImplementedError on accessing .inventory, then
1122
945
:param tree: The tree to lookup the entry in.
1123
946
:param file_id: The file_id to lookup.
948
# No inventory available.
1126
inventory = tree.root_inventory
1127
except (AttributeError, NotImplementedError):
1128
# No inventory available.
1130
iterator = tree.iter_entries_by_dir(specific_file_ids=[file_id])
1131
return iterator.next()[1]
1132
except StopIteration:
1136
return inventory[file_id]
1137
except errors.NoSuchId:
950
iterator = tree.iter_entries_by_dir(specific_files=[path])
951
return iterator.next()[1]
952
except StopIteration:
1140
955
def _handle_precise_ids(self, precise_file_ids, changed_file_ids,
1141
956
discarded_changes=None):
1183
998
# Examine file_id
1184
999
if discarded_changes:
1185
1000
result = discarded_changes.get(file_id)
1189
1004
if result is None:
1190
old_entry = self._get_entry(self.source, file_id)
1191
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)
1192
1019
result, changes = self._changes_from_entries(
1193
old_entry, new_entry)
1020
source_entry, target_entry, source_path, target_path)
1196
1023
# Get this parents parent to examine.
1201
1028
result[6][1] != 'directory'):
1202
1029
# This stopped being a directory, the old children have
1203
1030
# to be included.
1204
if old_entry is None:
1031
if source_entry is None:
1205
1032
# Reusing a discarded change.
1206
old_entry = self._get_entry(self.source, file_id)
1033
source_entry = self._get_entry(self.source, result[1][0])
1207
1034
precise_file_ids.update(
1208
self.source.iter_children(file_id))
1036
for child in self.source.iter_child_entries(result[1][0]))
1209
1037
changed_file_ids.add(result[0])
1212
1040
def file_content_matches(
1213
self, source_file_id, target_file_id, source_path=None,
1214
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):
1215
1044
"""Check if two files are the same in the source and target trees.
1217
1046
This only checks that the contents of the files are the same,
1218
1047
it does not touch anything else.
1220
:param source_file_id: File id of the file in the source tree
1221
:param target_file_id: File id of the file in the target tree
1222
1049
:param source_path: Path of the file in the source tree
1223
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
1224
1053
:param source_stat: Optional stat value of the file in the source tree
1225
1054
:param target_stat: Optional stat value of the file in the target tree
1226
1055
:return: Boolean indicating whether the files have the same contents
1228
1057
with self.lock_read():
1229
if source_path is None:
1230
source_path = self.source.id2path(source_file_id)
1231
if target_path is None:
1232
target_path = self.target.id2path(target_file_id)
1233
1058
source_verifier_kind, source_verifier_data = (
1234
1059
self.source.get_file_verifier(
1235
1060
source_path, source_file_id, source_stat))
1485
1310
other_values.append(self._lookup_by_file_id(
1486
1311
alt_extra, alt_tree, file_id))
1487
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: