31
30
revision as _mod_revision,
35
33
from bzrlib.decorators import needs_read_lock
36
from bzrlib.errors import BzrError, BzrCheckError, NoSuchId
34
from bzrlib.errors import BzrError, NoSuchId
37
35
from bzrlib import errors
38
from bzrlib.inventory import Inventory, InventoryFile
36
from bzrlib.inventory import InventoryFile
39
37
from bzrlib.inter import InterObject
40
38
from bzrlib.osutils import fingerprint_file
41
39
import bzrlib.revision
42
40
from bzrlib.symbol_versioning import deprecated_function, deprecated_in
43
from bzrlib.trace import mutter, note
41
from bzrlib.trace import note
46
44
class Tree(object):
222
222
def path_content_summary(self, path):
223
223
"""Get a summary of the information about path.
225
All the attributes returned are for the canonical form, not the
226
convenient form (if content filters are in use.)
225
228
:param path: A relative path within the tree.
226
229
:return: A tuple containing kind, size, exec, sha1-or-link.
227
230
Kind is always present (see tree.kind()).
228
size is present if kind is file, None otherwise.
231
size is present if kind is file and the size of the
232
canonical form can be cheaply determined, None otherwise.
229
233
exec is None unless kind is file and the platform supports the 'x'
231
235
sha1-or-link is the link target if kind is symlink, or the sha1 if
857
def _changes_from_entries(self, source_entry, target_entry,
858
source_path=None, target_path=None):
859
"""Generate a iter_changes tuple between source_entry and target_entry.
861
:param source_entry: An inventory entry from self.source, or None.
862
:param target_entry: An inventory entry from self.target, or None.
863
:param source_path: The path of source_entry, if known. If not known
864
it will be looked up.
865
:param target_path: The path of target_entry, if known. If not known
866
it will be looked up.
867
:return: A tuple, item 0 of which is an iter_changes result tuple, and
868
item 1 is True if there are any changes in the result tuple.
870
if source_entry is None:
871
if target_entry is None:
873
file_id = target_entry.file_id
875
file_id = source_entry.file_id
876
if source_entry is not None:
877
source_versioned = True
878
source_name = source_entry.name
879
source_parent = source_entry.parent_id
880
if source_path is None:
881
source_path = self.source.id2path(file_id)
882
source_kind, source_executable, source_stat = \
883
self.source._comparison_data(source_entry, source_path)
885
source_versioned = False
889
source_executable = None
890
if target_entry is not None:
891
target_versioned = True
892
target_name = target_entry.name
893
target_parent = target_entry.parent_id
894
if target_path is None:
895
target_path = self.target.id2path(file_id)
896
target_kind, target_executable, target_stat = \
897
self.target._comparison_data(target_entry, target_path)
899
target_versioned = False
903
target_executable = None
904
versioned = (source_versioned, target_versioned)
905
kind = (source_kind, target_kind)
906
changed_content = False
907
if source_kind != target_kind:
908
changed_content = True
909
elif source_kind == 'file':
910
if (self.source.get_file_sha1(file_id, source_path, source_stat) !=
911
self.target.get_file_sha1(file_id, target_path, target_stat)):
912
changed_content = True
913
elif source_kind == 'symlink':
914
if (self.source.get_symlink_target(file_id) !=
915
self.target.get_symlink_target(file_id)):
916
changed_content = True
917
# XXX: Yes, the indentation below is wrong. But fixing it broke
918
# test_merge.TestMergerEntriesLCAOnDisk.
919
# test_nested_tree_subtree_renamed_and_modified. We'll wait for
920
# the fix from bzr.dev -- vila 2009026
921
elif source_kind == 'tree-reference':
922
if (self.source.get_reference_revision(file_id, source_path)
923
!= self.target.get_reference_revision(file_id, target_path)):
924
changed_content = True
925
parent = (source_parent, target_parent)
926
name = (source_name, target_name)
927
executable = (source_executable, target_executable)
928
if (changed_content is not False or versioned[0] != versioned[1]
929
or parent[0] != parent[1] or name[0] != name[1] or
930
executable[0] != executable[1]):
934
return (file_id, (source_path, target_path), changed_content,
935
versioned, parent, name, kind, executable), changes
854
938
def compare(self, want_unchanged=False, specific_files=None,
855
939
extra_trees=None, require_versioned=False, include_root=False,
950
1052
# can be extras. So the fake_entry is solely used to look up
951
1053
# executable it values when execute is not supported.
952
1054
fake_entry = InventoryFile('unused', 'unused', 'unused')
953
for to_path, to_entry in to_entries_by_dir:
954
while all_unversioned and all_unversioned[0][0] < to_path.split('/'):
1055
for target_path, target_entry in to_entries_by_dir:
1056
while (all_unversioned and
1057
all_unversioned[0][0] < target_path.split('/')):
955
1058
unversioned_path = all_unversioned.popleft()
956
to_kind, to_executable, to_stat = \
1059
target_kind, target_executable, target_stat = \
957
1060
self.target._comparison_data(fake_entry, unversioned_path[1])
958
1061
yield (None, (None, unversioned_path[1]), True, (False, False),
960
1063
(None, unversioned_path[0][-1]),
962
(None, to_executable))
963
file_id = to_entry.file_id
964
to_paths[file_id] = to_path
1064
(None, target_kind),
1065
(None, target_executable))
1066
source_path, source_entry = from_data.get(target_entry.file_id,
1068
result, changes = self._changes_from_entries(source_entry,
1069
target_entry, source_path=source_path, target_path=target_path)
1070
to_paths[result[0]] = result[1][1]
965
1071
entry_count += 1
966
changed_content = False
967
from_path, from_entry = from_data.get(file_id, (None, None))
968
from_versioned = (from_entry is not None)
969
if from_entry is not None:
970
from_versioned = True
971
from_name = from_entry.name
972
from_parent = from_entry.parent_id
973
from_kind, from_executable, from_stat = \
974
self.source._comparison_data(from_entry, from_path)
975
1073
entry_count += 1
977
from_versioned = False
981
from_executable = None
982
versioned = (from_versioned, True)
983
to_kind, to_executable, to_stat = \
984
self.target._comparison_data(to_entry, to_path)
985
kind = (from_kind, to_kind)
986
if kind[0] != kind[1]:
987
changed_content = True
988
elif from_kind == 'file':
989
if (self.source.get_file_sha1(file_id, from_path, from_stat) !=
990
self.target.get_file_sha1(file_id, to_path, to_stat)):
991
changed_content = True
992
elif from_kind == 'symlink':
993
if (self.source.get_symlink_target(file_id) !=
994
self.target.get_symlink_target(file_id)):
995
changed_content = True
996
# XXX: Yes, the indentation below is wrong. But fixing it broke
997
# test_merge.TestMergerEntriesLCAOnDisk.
998
# test_nested_tree_subtree_renamed_and_modified. We'll wait for
999
# the fix from bzr.dev -- vila 2009026
1000
elif from_kind == 'tree-reference':
1001
if (self.source.get_reference_revision(file_id, from_path)
1002
!= self.target.get_reference_revision(file_id, to_path)):
1003
changed_content = True
1004
parent = (from_parent, to_entry.parent_id)
1005
name = (from_name, to_entry.name)
1006
executable = (from_executable, to_executable)
1007
1074
if pb is not None:
1008
1075
pb.update('comparing files', entry_count, num_entries)
1009
if (changed_content is not False or versioned[0] != versioned[1]
1010
or parent[0] != parent[1] or name[0] != name[1] or
1011
executable[0] != executable[1] or include_unchanged):
1012
yield (file_id, (from_path, to_path), changed_content,
1013
versioned, parent, name, kind, executable)
1076
if changes or include_unchanged:
1077
if specific_file_ids is not None:
1078
new_parent_id = result[4][1]
1079
precise_file_ids.add(new_parent_id)
1080
changed_file_ids.append(result[0])
1082
# Ensure correct behaviour for reparented/added specific files.
1083
if specific_files is not None:
1084
# Record output dirs
1085
if result[6][1] == 'directory':
1086
seen_dirs.add(result[0])
1087
# Record parents of reparented/added entries.
1088
versioned = result[3]
1090
if not versioned[0] or parents[0] != parents[1]:
1091
seen_parents.add(parents[1])
1015
1092
while all_unversioned:
1016
1093
# yield any trailing unversioned paths
1017
1094
unversioned_path = all_unversioned.popleft()
1022
1099
(None, unversioned_path[0][-1]),
1023
1100
(None, to_kind),
1024
1101
(None, to_executable))
1026
def get_to_path(to_entry):
1027
if to_entry.parent_id is None:
1028
to_path = '' # the root
1030
if to_entry.parent_id not in to_paths:
1032
return get_to_path(self.target.inventory[to_entry.parent_id])
1033
to_path = osutils.pathjoin(to_paths[to_entry.parent_id],
1035
to_paths[to_entry.file_id] = to_path
1102
# Yield all remaining source paths
1038
1103
for path, from_entry in from_entries_by_dir:
1039
1104
file_id = from_entry.file_id
1040
1105
if file_id in to_paths:
1041
1106
# already returned
1043
if not file_id in self.target.all_file_ids():
1108
if file_id not in self.target.all_file_ids():
1044
1109
# common case - paths we have not emitted are not present in
1048
to_path = get_to_path(self.target.inventory[file_id])
1113
to_path = self.target.id2path(file_id)
1049
1114
entry_count += 1
1050
1115
if pb is not None:
1051
1116
pb.update('comparing files', entry_count, num_entries)
1058
1123
executable = (from_executable, None)
1059
1124
changed_content = from_kind is not None
1060
1125
# the parent's path is necessarily known at this point.
1126
changed_file_ids.append(file_id)
1061
1127
yield(file_id, (path, to_path), changed_content, versioned, parent,
1062
1128
name, kind, executable)
1129
changed_file_ids = set(changed_file_ids)
1130
if specific_file_ids is not None:
1131
for result in self._handle_precise_ids(precise_file_ids,
1135
def _get_entry(self, tree, file_id):
1136
"""Get an inventory entry from a tree, with missing entries as None.
1138
If the tree raises NotImplementedError on accessing .inventory, then
1139
this is worked around using iter_entries_by_dir on just the file id
1142
:param tree: The tree to lookup the entry in.
1143
:param file_id: The file_id to lookup.
1146
inventory = tree.inventory
1147
except NotImplementedError:
1148
# No inventory available.
1150
iterator = tree.iter_entries_by_dir(specific_file_ids=[file_id])
1151
return iterator.next()[1]
1152
except StopIteration:
1156
return inventory[file_id]
1157
except errors.NoSuchId:
1160
def _handle_precise_ids(self, precise_file_ids, changed_file_ids,
1161
discarded_changes=None):
1162
"""Fill out a partial iter_changes to be consistent.
1164
:param precise_file_ids: The file ids of parents that were seen during
1166
:param changed_file_ids: The file ids of already emitted items.
1167
:param discarded_changes: An optional dict of precalculated
1168
iter_changes items which the partial iter_changes had not output
1170
:return: A generator of iter_changes items to output.
1172
# process parents of things that had changed under the users
1173
# requested paths to prevent incorrect paths or parent ids which
1174
# aren't in the tree.
1175
while precise_file_ids:
1176
precise_file_ids.discard(None)
1177
# Don't emit file_ids twice
1178
precise_file_ids.difference_update(changed_file_ids)
1179
if not precise_file_ids:
1181
# If the there was something at a given output path in source, we
1182
# have to include the entry from source in the delta, or we would
1183
# be putting this entry into a used path.
1185
for parent_id in precise_file_ids:
1187
paths.append(self.target.id2path(parent_id))
1188
except errors.NoSuchId:
1189
# This id has been dragged in from the source by delta
1190
# expansion and isn't present in target at all: we don't
1191
# need to check for path collisions on it.
1194
old_id = self.source.path2id(path)
1195
precise_file_ids.add(old_id)
1196
precise_file_ids.discard(None)
1197
current_ids = precise_file_ids
1198
precise_file_ids = set()
1199
# We have to emit all of precise_file_ids that have been altered.
1200
# We may have to output the children of some of those ids if any
1201
# directories have stopped being directories.
1202
for file_id in current_ids:
1204
if discarded_changes:
1205
result = discarded_changes.get(file_id)
1210
old_entry = self._get_entry(self.source, file_id)
1211
new_entry = self._get_entry(self.target, file_id)
1212
result, changes = self._changes_from_entries(
1213
old_entry, new_entry)
1216
# Get this parents parent to examine.
1217
new_parent_id = result[4][1]
1218
precise_file_ids.add(new_parent_id)
1220
if (result[6][0] == 'directory' and
1221
result[6][1] != 'directory'):
1222
# This stopped being a directory, the old children have
1224
if old_entry is None:
1225
# Reusing a discarded change.
1226
old_entry = self._get_entry(self.source, file_id)
1227
for child in old_entry.children.values():
1228
precise_file_ids.add(child.file_id)
1229
changed_file_ids.add(result[0])
1065
1233
class MultiWalker(object):