64
class TreeEntry(object):
65
"""An entry that implements the minimum interface used by commands.
68
def __eq__(self, other):
69
# yes, this is ugly, TODO: best practice __eq__ style.
70
return (isinstance(other, TreeEntry)
71
and other.__class__ == self.__class__)
73
def kind_character(self):
77
class TreeDirectory(TreeEntry):
78
"""See TreeEntry. This is a directory in a working tree."""
80
def kind_character(self):
84
class TreeFile(TreeEntry):
85
"""See TreeEntry. This is a regular file in a working tree."""
87
def kind_character(self):
91
class TreeLink(TreeEntry):
92
"""See TreeEntry. This is a symlink in a working tree."""
94
def kind_character(self):
64
98
class Tree(object):
65
99
"""Abstract file tree.
399
438
this implementation, it is a tuple containing a single bytestring with
400
439
the complete text of the file.
402
:param desired_files: a list of (file_id, identifier) pairs
441
:param desired_files: a list of (path, identifier) pairs
404
for file_id, identifier in desired_files:
443
for path, identifier in desired_files:
405
444
# We wrap the string in a tuple so that we can return an iterable
406
445
# of bytestrings. (Technically, a bytestring is also an iterable
407
446
# of bytestrings, but iterating through each character is not
409
# TODO(jelmer): Pass paths into iter_files_bytes
410
path = self.id2path(file_id)
411
cur_file = (self.get_file_text(path, file_id),)
448
cur_file = (self.get_file_text(path),)
412
449
yield identifier, cur_file
414
451
def get_symlink_target(self, path, file_id=None):
443
480
raise NotImplementedError(self.annotate_iter)
445
def _get_plan_merge_data(self, file_id, other, base):
446
from .bzr import versionedfile
447
vf = versionedfile._PlanMergeVersionedFile(file_id)
448
last_revision_a = self._get_file_revision(
449
self.id2path(file_id), file_id, vf, 'this:')
450
last_revision_b = other._get_file_revision(
451
other.id2path(file_id), file_id, vf, 'other:')
453
last_revision_base = None
455
last_revision_base = base._get_file_revision(
456
base.id2path(file_id), file_id, vf, 'base:')
457
return vf, last_revision_a, last_revision_b, last_revision_base
459
def plan_file_merge(self, file_id, other, base=None):
460
"""Generate a merge plan based on annotations.
462
If the file contains uncommitted changes in this tree, they will be
463
attributed to the 'current:' pseudo-revision. If the file contains
464
uncommitted changes in the other tree, they will be assigned to the
465
'other:' pseudo-revision.
467
data = self._get_plan_merge_data(file_id, other, base)
468
vf, last_revision_a, last_revision_b, last_revision_base = data
469
return vf.plan_merge(last_revision_a, last_revision_b,
472
def plan_file_lca_merge(self, file_id, other, base=None):
473
"""Generate a merge plan based lca-newness.
475
If the file contains uncommitted changes in this tree, they will be
476
attributed to the 'current:' pseudo-revision. If the file contains
477
uncommitted changes in the other tree, they will be assigned to the
478
'other:' pseudo-revision.
480
data = self._get_plan_merge_data(file_id, other, base)
481
vf, last_revision_a, last_revision_b, last_revision_base = data
482
return vf.plan_lca_merge(last_revision_a, last_revision_b,
485
482
def _iter_parent_trees(self):
486
483
"""Iterate through parent trees, defaulting to Tree.revision_tree."""
487
484
for revision_id in self.get_parent_ids():
490
487
except errors.NoSuchRevisionInTree:
491
488
yield self.repository.revision_tree(revision_id)
493
def _get_file_revision(self, path, file_id, vf, tree_revision):
494
"""Ensure that file_id, tree_revision is in vf to plan the merge."""
495
if getattr(self, '_repository', None) is None:
496
last_revision = tree_revision
497
parent_keys = [(file_id, t.get_file_revision(path, file_id)) for t in
498
self._iter_parent_trees()]
499
vf.add_lines((file_id, last_revision), parent_keys,
500
self.get_file_lines(path, file_id))
501
repo = self.branch.repository
504
last_revision = self.get_file_revision(path, file_id)
505
base_vf = self._repository.texts
506
if base_vf not in vf.fallback_versionedfiles:
507
vf.fallback_versionedfiles.append(base_vf)
510
def _check_retrieved(self, ie, f):
513
fp = osutils.fingerprint_file(f)
516
if ie.text_size is not None:
517
if ie.text_size != fp['size']:
518
raise errors.BzrError(
519
"mismatched size for file %r in %r" %
520
(ie.file_id, self._store),
521
["inventory expects %d bytes" % ie.text_size,
522
"file is actually %d bytes" % fp['size'],
523
"store is probably damaged/corrupt"])
525
if ie.text_sha1 != fp['sha1']:
526
raise errors.BzrError("wrong SHA-1 for file %r in %r" %
527
(ie.file_id, self._store),
528
["inventory expects %s" % ie.text_sha1,
529
"file is actually %s" % fp['sha1'],
530
"store is probably damaged/corrupt"])
532
490
def path2id(self, path):
533
491
"""Return the id for path in this tree."""
534
492
raise NotImplementedError(self.path2id)
536
def paths2ids(self, paths, trees=[], require_versioned=True):
537
"""Return all the ids that can be reached by walking from paths.
539
Each path is looked up in this tree and any extras provided in
540
trees, and this is repeated recursively: the children in an extra tree
541
of a directory that has been renamed under a provided path in this tree
542
are all returned, even if none exist under a provided path in this
543
tree, and vice versa.
545
:param paths: An iterable of paths to start converting to ids from.
546
Alternatively, if paths is None, no ids should be calculated and None
547
will be returned. This is offered to make calling the api unconditional
548
for code that *might* take a list of files.
549
:param trees: Additional trees to consider.
550
:param require_versioned: If False, do not raise NotVersionedError if
551
an element of paths is not versioned in this tree and all of trees.
553
return find_ids_across_trees(paths, [self] + list(trees), require_versioned)
555
def iter_children(self, file_id):
556
"""Iterate over the file ids of the children of an entry.
558
:param file_id: File id of the entry
559
:return: Iterator over child file ids.
561
raise NotImplementedError(self.iter_children)
494
def is_versioned(self, path):
495
"""Check whether path is versioned.
497
:param path: Path to check
500
return self.path2id(path) is not None
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)
563
520
def lock_read(self):
564
521
"""Lock this tree for multiple read only operations.
703
def find_ids_across_trees(filenames, trees, require_versioned=True):
704
"""Find the ids corresponding to specified filenames.
706
All matches in all trees will be used, and all children of matched
707
directories will be used.
709
:param filenames: The filenames to find file_ids for (if None, returns
711
:param trees: The trees to find file_ids within
712
:param require_versioned: if true, all specified filenames must occur in
714
:return: a set of file ids for the specified filenames and their children.
718
specified_path_ids = _find_ids_across_trees(filenames, trees,
720
return _find_children_across_trees(specified_path_ids, trees)
723
def _find_ids_across_trees(filenames, trees, require_versioned):
724
"""Find the ids corresponding to specified filenames.
726
All matches in all trees will be used, but subdirectories are not scanned.
728
:param filenames: The filenames to find file_ids for
729
:param trees: The trees to find file_ids within
730
:param require_versioned: if true, all specified filenames must occur in
732
:return: a set of file ids for the specified filenames
735
interesting_ids = set()
736
for tree_path in filenames:
739
file_id = tree.path2id(tree_path)
740
if file_id is not None:
741
interesting_ids.add(file_id)
744
not_versioned.append(tree_path)
745
if len(not_versioned) > 0 and require_versioned:
746
raise errors.PathsNotVersionedError(not_versioned)
747
return interesting_ids
750
def _find_children_across_trees(specified_ids, trees):
751
"""Return a set including specified ids and their children.
753
All matches in all trees will be used.
755
:param trees: The trees to find file_ids within
756
:return: a set containing all specified ids and their children
758
interesting_ids = set(specified_ids)
759
pending = interesting_ids
760
# now handle children of interesting ids
761
# we loop so that we handle all children of each id in both trees
762
while len(pending) > 0:
764
for file_id in pending:
766
if not tree.has_or_had_id(file_id):
768
for child_id in tree.iter_children(file_id):
769
if child_id not in interesting_ids:
770
new_pending.add(child_id)
771
interesting_ids.update(new_pending)
772
pending = new_pending
773
return interesting_ids
776
657
class InterTree(InterObject):
777
658
"""This class represents operations taking place between two Trees.
798
679
# it works for all trees.
801
def _changes_from_entries(self, source_entry, target_entry,
802
source_path=None, target_path=None):
682
def _changes_from_entries(self, source_entry, target_entry, source_path,
803
684
"""Generate a iter_changes tuple between source_entry and target_entry.
805
686
:param source_entry: An inventory entry from self.source, or None.
806
687
:param target_entry: An inventory entry from self.target, or None.
807
:param source_path: The path of source_entry, if known. If not known
808
it will be looked up.
809
:param target_path: The path of target_entry, if known. If not known
810
it will be looked up.
688
:param source_path: The path of source_entry.
689
:param target_path: The path of target_entry.
811
690
:return: A tuple, item 0 of which is an iter_changes result tuple, and
812
691
item 1 is True if there are any changes in the result tuple.
897
773
if extra_trees is not None:
898
774
trees = trees + tuple(extra_trees)
899
775
with self.lock_read():
900
# target is usually the newer tree:
901
specific_file_ids = self.target.paths2ids(specific_files, trees,
902
require_versioned=require_versioned)
903
if specific_files and not specific_file_ids:
904
# All files are unversioned, so just return an empty delta
905
# _compare_trees would think we want a complete delta
906
result = delta.TreeDelta()
907
fake_entry = inventory.InventoryFile('unused', 'unused', 'unused')
908
result.unversioned = [(path, None,
909
self.target._comparison_data(fake_entry, path)[0]) for path in
912
776
return delta._compare_trees(self.source, self.target, want_unchanged,
913
777
specific_files, include_root, extra_trees=extra_trees,
914
778
require_versioned=require_versioned,
950
814
output. An unversioned file is defined as one with (False, False)
951
815
for the versioned pair.
953
lookup_trees = [self.source]
955
lookup_trees.extend(extra_trees)
820
extra_trees = list(extra_trees)
956
821
# The ids of items we need to examine to insure delta consistency.
957
822
precise_file_ids = set()
958
823
changed_file_ids = []
959
824
if specific_files == []:
960
specific_file_ids = []
825
target_specific_files = []
826
source_specific_files = []
962
specific_file_ids = self.target.paths2ids(specific_files,
963
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)
964
834
if specific_files is not None:
965
835
# reparented or added entries must have their parents included
966
836
# so that valid deltas can be created. The seen_parents set
1065
930
yield(file_id, (path, to_path), changed_content, versioned, parent,
1066
931
name, kind, executable)
1067
932
changed_file_ids = set(changed_file_ids)
1068
if specific_file_ids is not None:
933
if specific_files is not None:
1069
934
for result in self._handle_precise_ids(precise_file_ids,
1070
935
changed_file_ids):
1073
def _get_entry(self, tree, file_id):
938
def _get_entry(self, tree, path):
1074
939
"""Get an inventory entry from a tree, with missing entries as None.
1076
941
If the tree raises NotImplementedError on accessing .inventory, then
1080
945
:param tree: The tree to lookup the entry in.
1081
946
:param file_id: The file_id to lookup.
948
# No inventory available.
1084
inventory = tree.root_inventory
1085
except (AttributeError, NotImplementedError):
1086
# No inventory available.
1088
iterator = tree.iter_entries_by_dir(specific_file_ids=[file_id])
1089
return iterator.next()[1]
1090
except StopIteration:
1094
return inventory[file_id]
1095
except errors.NoSuchId:
950
iterator = tree.iter_entries_by_dir(specific_files=[path])
951
return iterator.next()[1]
952
except StopIteration:
1098
955
def _handle_precise_ids(self, precise_file_ids, changed_file_ids,
1099
956
discarded_changes=None):
1141
998
# Examine file_id
1142
999
if discarded_changes:
1143
1000
result = discarded_changes.get(file_id)
1147
1004
if result is None:
1148
old_entry = self._get_entry(self.source, file_id)
1149
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)
1150
1019
result, changes = self._changes_from_entries(
1151
old_entry, new_entry)
1020
source_entry, target_entry, source_path, target_path)
1154
1023
# Get this parents parent to examine.
1159
1028
result[6][1] != 'directory'):
1160
1029
# This stopped being a directory, the old children have
1161
1030
# to be included.
1162
if old_entry is None:
1031
if source_entry is None:
1163
1032
# Reusing a discarded change.
1164
old_entry = self._get_entry(self.source, file_id)
1033
source_entry = self._get_entry(self.source, result[1][0])
1165
1034
precise_file_ids.update(
1166
self.source.iter_children(file_id))
1036
for child in self.source.iter_child_entries(result[1][0]))
1167
1037
changed_file_ids.add(result[0])
1170
1040
def file_content_matches(
1171
self, source_file_id, target_file_id, source_path=None,
1172
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):
1173
1044
"""Check if two files are the same in the source and target trees.
1175
1046
This only checks that the contents of the files are the same,
1176
1047
it does not touch anything else.
1178
:param source_file_id: File id of the file in the source tree
1179
:param target_file_id: File id of the file in the target tree
1180
1049
:param source_path: Path of the file in the source tree
1181
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
1182
1053
:param source_stat: Optional stat value of the file in the source tree
1183
1054
:param target_stat: Optional stat value of the file in the target tree
1184
1055
:return: Boolean indicating whether the files have the same contents
1186
1057
with self.lock_read():
1187
if source_path is None:
1188
source_path = self.source.id2path(source_file_id)
1189
if target_path is None:
1190
target_path = self.target.id2path(target_file_id)
1191
1058
source_verifier_kind, source_verifier_data = (
1192
1059
self.source.get_file_verifier(
1193
1060
source_path, source_file_id, source_stat))
1443
1310
other_values.append(self._lookup_by_file_id(
1444
1311
alt_extra, alt_tree, file_id))
1445
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: