96
79
sha-1 of the text of the file
99
82
size in bytes of the text of the file
101
84
(reading a version 4 tree created a text_id field.)
103
86
>>> i = Inventory()
106
>>> i.add(InventoryDirectory(b'123', 'src', ROOT_ID))
107
InventoryDirectory(b'123', 'src', parent_id=b'TREE_ROOT', revision=None)
108
>>> i.add(InventoryFile(b'2323', 'hello.c', parent_id=b'123'))
109
InventoryFile(b'2323', 'hello.c', parent_id=b'123', sha1=None, len=None, revision=None)
89
>>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
90
InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
91
>>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
92
InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
110
93
>>> shouldbe = {0: '', 1: 'src', 2: 'src/hello.c'}
111
94
>>> for ix, j in enumerate(i.iter_entries()):
112
... print(j[0] == shouldbe[ix], j[1])
114
True InventoryDirectory(b'TREE_ROOT', '', parent_id=None, revision=None)
115
True InventoryDirectory(b'123', 'src', parent_id=b'TREE_ROOT', revision=None)
116
True InventoryFile(b'2323', 'hello.c', parent_id=b'123', sha1=None, len=None, revision=None)
117
>>> i.add(InventoryFile(b'2324', 'bye.c', b'123'))
118
InventoryFile(b'2324', 'bye.c', parent_id=b'123', sha1=None, len=None, revision=None)
119
>>> i.add(InventoryDirectory(b'2325', 'wibble', b'123'))
120
InventoryDirectory(b'2325', 'wibble', parent_id=b'123', revision=None)
95
... print (j[0] == shouldbe[ix], j[1])
97
(True, InventoryDirectory('TREE_ROOT', u'', parent_id=None, revision=None))
98
(True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
99
(True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
100
>>> i.add(InventoryFile('2324', 'bye.c', '123'))
101
InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
102
>>> i.add(InventoryDirectory('2325', 'wibble', '123'))
103
InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
121
104
>>> i.path2id('src/wibble')
123
>>> i.add(InventoryFile(b'2326', 'wibble.c', b'2325'))
124
InventoryFile(b'2326', 'wibble.c', parent_id=b'2325', sha1=None, len=None, revision=None)
125
>>> i.get_entry(b'2326')
126
InventoryFile(b'2326', 'wibble.c', parent_id=b'2325', sha1=None, len=None, revision=None)
108
>>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
109
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
111
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
127
112
>>> for path, entry in i.iter_entries():
114
... assert i.path2id(path)
135
121
src/wibble/wibble.c
136
>>> i.id2path(b'2326')
122
>>> i.id2path('2326')
137
123
'src/wibble/wibble.c'
140
126
# Constants returned by describe_change()
142
# TODO: These should probably move to some kind of FileChangeDescription
143
# class; that's like what's inside a TreeDelta but we want to be able to
128
# TODO: These should probably move to some kind of FileChangeDescription
129
# class; that's like what's inside a TreeDelta but we want to be able to
144
130
# generate them just for one file at a time.
145
131
RENAMED = 'renamed'
146
132
MODIFIED_AND_RENAMED = 'modified and renamed'
148
__slots__ = ['file_id', 'revision', 'parent_id', 'name']
150
# Attributes that all InventoryEntry instances are expected to have, but
151
# that don't vary for all kinds of entry. (e.g. symlink_target is only
152
# relevant to InventoryLink, so there's no reason to make every
153
# InventoryFile instance allocate space to hold a value for it.)
154
# Attributes that only vary for files: executable, text_sha1, text_size,
160
# Attributes that only vary for symlinks: symlink_target
161
symlink_target = None
162
# Attributes that only vary for tree-references: reference_revision
163
reference_revision = None
165
136
def detect_changes(self, old_entry):
166
137
"""Return a (text_modified, meta_modified) from this to old_entry.
168
_read_tree_state must have been called on self and old_entry prior to
139
_read_tree_state must have been called on self and old_entry prior to
169
140
calling detect_changes.
171
142
return False, False
144
def diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
145
output_to, reverse=False):
146
"""Perform a diff from this to to_entry.
148
text_diff will be used for textual difference calculation.
149
This is a template method, override _diff in child classes.
151
self._read_tree_state(tree.id2path(self.file_id), tree)
153
# cannot diff from one kind to another - you must do a removal
154
# and an addif they do not match.
155
assert self.kind == to_entry.kind
156
to_entry._read_tree_state(to_tree.id2path(to_entry.file_id),
158
self._diff(text_diff, from_label, tree, to_label, to_entry, to_tree,
173
161
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
174
output_to, reverse=False):
162
output_to, reverse=False):
175
163
"""Perform a diff between two entries of the same kind."""
177
def parent_candidates(self, previous_inventories):
178
"""Find possible per-file graph parents.
180
This is currently defined by:
181
- Select the last changed revision in the parent inventory.
182
- Do deal with a short lived bug in bzr 0.8's development two entries
183
that have the same last changed but different 'x' bit settings are
165
def find_previous_heads(self, previous_inventories,
166
versioned_file_store,
169
"""Return the revisions and entries that directly precede this.
171
Returned as a map from revision to inventory entry.
173
This is a map containing the file revisions in all parents
174
for which the file exists, and its revision is not a parent of
175
any other. If the file is new, the set will be empty.
177
:param versioned_file_store: A store where ancestry data on this
178
file id can be queried.
179
:param transaction: The transaction that queries to the versioned
180
file store should be completed under.
181
:param entry_vf: The entry versioned file, if its already available.
183
def get_ancestors(weave, entry):
184
return set(weave.get_ancestry(entry.revision))
186
185
# revision:ie mapping for each ie found in previous_inventories.
187
# revision:ie mapping with one revision for each head.
189
# revision: ancestor list for each head
188
191
# identify candidate head revision ids.
189
192
for inv in previous_inventories:
191
ie = inv.get_entry(self.file_id)
192
except errors.NoSuchId:
193
if self.file_id in inv:
194
ie = inv[self.file_id]
195
assert ie.file_id == self.file_id
196
if ie.kind != self.kind:
197
# Can't be a candidate if the kind has changed.
195
199
if ie.revision in candidates:
196
200
# same revision value in two different inventories:
197
201
# correct possible inconsistencies:
198
# * there was a bug in revision updates with 'x' bit
202
# * there was a bug in revision updates with 'x' bit
201
205
if candidates[ie.revision].executable != ie.executable:
808
1028
add_ancestors(file_id)
812
1032
stack = [(u'', from_dir)]
814
1034
cur_relpath, cur_dir = stack.pop()
817
for child_name, child_ie in sorted(cur_dir.children.items()):
1037
for child_name, child_ie in sorted(cur_dir.children.iteritems()):
819
1039
child_relpath = cur_relpath + child_name
821
if (specific_file_ids is None
822
or child_ie.file_id in specific_file_ids):
1041
if (specific_file_ids is None or
1042
child_ie.file_id in specific_file_ids):
823
1043
yield child_relpath, child_ie
825
1045
if child_ie.kind == 'directory':
826
1046
if parents is None or child_ie.file_id in parents:
827
child_dirs.append((child_relpath + '/', child_ie))
1047
child_dirs.append((child_relpath+'/', child_ie))
828
1048
stack.extend(reversed(child_dirs))
830
def _make_delta(self, old):
831
"""Make an inventory delta from two inventories."""
832
old_ids = set(old.iter_all_ids())
833
new_ids = set(self.iter_all_ids())
834
adds = new_ids - old_ids
835
deletes = old_ids - new_ids
836
common = old_ids.intersection(new_ids)
838
for file_id in deletes:
839
delta.append((old.id2path(file_id), None, file_id, None))
841
delta.append((None, self.id2path(file_id),
842
file_id, self.get_entry(file_id)))
843
for file_id in common:
844
if old.get_entry(file_id) != self.get_entry(file_id):
845
delta.append((old.id2path(file_id), self.id2path(file_id),
846
file_id, self.get_entry(file_id)))
849
def make_entry(self, kind, name, parent_id, file_id=None):
850
"""Simple thunk to breezy.bzr.inventory.make_entry."""
851
return make_entry(kind, name, parent_id, file_id)
853
1050
def entries(self):
854
1051
"""Return list of (path, ie) for all entries except the root.
856
1053
This may be faster than iter_entries.
860
1056
def descend(dir_ie, dir_path):
861
kids = sorted(dir_ie.children.items())
1057
kids = dir_ie.children.items()
862
1059
for name, ie in kids:
863
1060
child_path = osutils.pathjoin(dir_path, name)
864
1061
accum.append((child_path, ie))
865
1062
if ie.kind == 'directory':
866
1063
descend(ie, child_path)
868
if self.root is not None:
869
descend(self.root, u'')
872
def get_entry_by_path_partial(self, relpath):
873
"""Like get_entry_by_path, but return TreeReference objects.
875
:param relpath: Path to resolve, either as string with / as separators,
876
or as list of elements.
877
:return: tuple with ie, resolved elements and elements left to resolve
879
if isinstance(relpath, str):
880
names = osutils.splitpath(relpath)
886
except errors.NoSuchId:
887
# root doesn't exist yet so nothing else can
888
return None, None, None
890
return None, None, None
891
for i, f in enumerate(names):
893
children = getattr(parent, 'children', None)
895
return None, None, None
897
if cie.kind == 'tree-reference':
898
return cie, names[:i + 1], names[i + 1:]
902
return None, None, None
903
return parent, names, []
905
def get_entry_by_path(self, relpath):
906
"""Return an inventory entry by path.
908
:param relpath: may be either a list of path components, or a single
909
string, in which case it is automatically split.
911
This returns the entry of the last component in the path,
912
which may be either a file or a directory.
914
Returns None IFF the path is not found.
916
if isinstance(relpath, str):
917
names = osutils.splitpath(relpath)
923
except errors.NoSuchId:
924
# root doesn't exist yet so nothing else can
930
children = getattr(parent, 'children', None)
940
def path2id(self, relpath):
941
"""Walk down through directories to return entry of last component.
943
:param relpath: may be either a list of path components, or a single
944
string, in which case it is automatically split.
946
This returns the entry of the last component in the path,
947
which may be either a file or a directory.
949
Returns None IFF the path is not found.
951
ie = self.get_entry_by_path(relpath)
956
def filter(self, specific_fileids):
957
"""Get an inventory view filtered against a set of file-ids.
959
Children of directories and parents are included.
961
The result may or may not reference the underlying inventory
962
so it should be treated as immutable.
964
interesting_parents = set()
965
for fileid in specific_fileids:
967
interesting_parents.update(self.get_idpath(fileid))
968
except errors.NoSuchId:
969
# This fileid is not in the inventory - that's ok
971
entries = self.iter_entries()
972
if self.root is None:
973
return Inventory(root_id=None)
974
other = Inventory(next(entries)[1].file_id)
975
other.root.revision = self.root.revision
976
other.revision_id = self.revision_id
977
directories_to_expand = set()
978
for path, entry in entries:
979
file_id = entry.file_id
980
if (file_id in specific_fileids or
981
entry.parent_id in directories_to_expand):
982
if entry.kind == 'directory':
983
directories_to_expand.add(file_id)
984
elif file_id not in interesting_parents:
986
other.add(entry.copy())
989
def get_idpath(self, file_id):
990
"""Return a list of file_ids for the path to an entry.
992
The list contains one element for each directory followed by
993
the id of the file itself. So the length of the returned list
994
is equal to the depth of the file in the tree, counting the
995
root directory as depth 1.
998
for parent in self._iter_file_id_parents(file_id):
999
p.insert(0, parent.file_id)
1003
class Inventory(CommonInventory):
1004
"""Mutable dict based in-memory inventory.
1006
We never store the full path to a file, because renaming a directory
1007
implicitly moves all of its contents. This class internally maintains a
1008
lookup tree that allows the children under a directory to be
1011
>>> inv = Inventory()
1012
>>> inv.add(InventoryFile(b'123-123', 'hello.c', ROOT_ID))
1013
InventoryFile(b'123-123', 'hello.c', parent_id=b'TREE_ROOT', sha1=None, len=None, revision=None)
1014
>>> inv.get_entry(b'123-123').name
1017
Id's may be looked up from paths:
1019
>>> inv.path2id('hello.c')
1021
>>> inv.has_id(b'123-123')
1024
There are iterators over the contents:
1026
>>> [entry[0] for entry in inv.iter_entries()]
1030
def __init__(self, root_id=ROOT_ID, revision_id=None):
1031
"""Create or read an inventory.
1033
If a working directory is specified, the inventory is read
1034
from there. If the file is specified, read from that. If not,
1035
the inventory is created empty.
1037
The inventory is created with a default root directory, with
1040
if root_id is not None:
1041
self._set_root(InventoryDirectory(root_id, u'', None))
1045
self.revision_id = revision_id
1048
# More than one page of ouput is not useful anymore to debug
1051
contents = repr(self._byid)
1052
if len(contents) > max_len:
1053
contents = contents[:(max_len - len(closing))] + closing
1054
return "<Inventory object at %x, contents=%r>" % (id(self), contents)
1056
def apply_delta(self, delta):
1057
"""Apply a delta to this inventory.
1059
See the inventory developers documentation for the theory behind
1062
If delta application fails the inventory is left in an indeterminate
1063
state and must not be used.
1065
:param delta: A list of changes to apply. After all the changes are
1066
applied the final inventory must be internally consistent, but it
1067
is ok to supply changes which, if only half-applied would have an
1068
invalid result - such as supplying two changes which rename two
1069
files, 'A' and 'B' with each other : [('A', 'B', b'A-id', a_entry),
1070
('B', 'A', b'B-id', b_entry)].
1072
Each change is a tuple, of the form (old_path, new_path, file_id,
1075
When new_path is None, the change indicates the removal of an entry
1076
from the inventory and new_entry will be ignored (using None is
1077
appropriate). If new_path is not None, then new_entry must be an
1078
InventoryEntry instance, which will be incorporated into the
1079
inventory (and replace any existing entry with the same file id).
1081
When old_path is None, the change indicates the addition of
1082
a new entry to the inventory.
1084
When neither new_path nor old_path are None, the change is a
1085
modification to an entry, such as a rename, reparent, kind change
1088
The children attribute of new_entry is ignored. This is because
1089
this method preserves children automatically across alterations to
1090
the parent of the children, and cases where the parent id of a
1091
child is changing require the child to be passed in as a separate
1092
change regardless. E.g. in the recursive deletion of a directory -
1093
the directory's children must be included in the delta, or the
1094
final inventory will be invalid.
1096
Note that a file_id must only appear once within a given delta.
1097
An AssertionError is raised otherwise.
1099
# Check that the delta is legal. It would be nice if this could be
1100
# done within the loops below but it's safer to validate the delta
1101
# before starting to mutate the inventory, as there isn't a rollback
1103
list(_check_delta_unique_ids(_check_delta_unique_new_paths(
1104
_check_delta_unique_old_paths(_check_delta_ids_match_entry(
1105
_check_delta_ids_are_valid(
1106
_check_delta_new_path_entry_both_or_None(
1110
# Remove all affected items which were in the original inventory,
1111
# starting with the longest paths, thus ensuring parents are examined
1112
# after their children, which means that everything we examine has no
1113
# modified children remaining by the time we examine it.
1114
for old_path, file_id in sorted(((op, f) for op, np, f, e in delta
1115
if op is not None), reverse=True):
1116
# Preserve unaltered children of file_id for later reinsertion.
1117
file_id_children = getattr(self.get_entry(file_id), 'children', {})
1118
if len(file_id_children):
1119
children[file_id] = file_id_children
1120
if self.id2path(file_id) != old_path:
1121
raise errors.InconsistentDelta(old_path, file_id,
1122
"Entry was at wrong other path %r." % self.id2path(file_id))
1123
# Remove file_id and the unaltered children. If file_id is not
1124
# being deleted it will be reinserted back later.
1125
self.remove_recursive_id(file_id)
1126
# Insert all affected which should be in the new inventory, reattaching
1127
# their children if they had any. This is done from shortest path to
1128
# longest, ensuring that items which were modified and whose parents in
1129
# the resulting inventory were also modified, are inserted after their
1131
for new_path, f, new_entry in sorted((np, f, e) for op, np, f, e in
1132
delta if np is not None):
1133
if new_entry.kind == 'directory':
1134
# Pop the child which to allow detection of children whose
1135
# parents were deleted and which were not reattached to a new
1137
replacement = InventoryDirectory(new_entry.file_id,
1138
new_entry.name, new_entry.parent_id)
1139
replacement.revision = new_entry.revision
1140
replacement.children = children.pop(replacement.file_id, {})
1141
new_entry = replacement
1144
except DuplicateFileId:
1145
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1146
"New id is already present in target.")
1147
except AttributeError:
1148
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1149
"Parent is not a directory.")
1150
if self.id2path(new_entry.file_id) != new_path:
1151
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1152
"New path is not consistent with parent path.")
1154
# Get the parent id that was deleted
1155
parent_id, children = children.popitem()
1156
raise errors.InconsistentDelta("<deleted>", parent_id,
1157
"The file id was deleted but its children were not deleted.")
1159
def create_by_apply_delta(self, inventory_delta, new_revision_id,
1160
propagate_caches=False):
1161
"""See CHKInventory.create_by_apply_delta()"""
1162
new_inv = self.copy()
1163
new_inv.apply_delta(inventory_delta)
1164
new_inv.revision_id = new_revision_id
1167
def _set_root(self, ie):
1169
self._byid = {self.root.file_id: self.root}
1172
# TODO: jam 20051218 Should copy also copy the revision_id?
1173
entries = self.iter_entries()
1174
if self.root is None:
1175
return Inventory(root_id=None)
1176
other = Inventory(next(entries)[1].file_id)
1177
other.root.revision = self.root.revision
1178
# copy recursively so we know directories will be added before
1179
# their children. There are more efficient ways than this...
1180
for path, entry in entries:
1181
other.add(entry.copy())
1184
def iter_all_ids(self):
1185
"""Iterate over all file-ids."""
1186
return iter(self._byid)
1188
def iter_just_entries(self):
1189
"""Iterate over all entries.
1191
Unlike iter_entries(), just the entries are returned (not (path, ie))
1192
and the order of entries is undefined.
1194
XXX: We may not want to merge this into bzr.dev.
1196
if self.root is None:
1198
return self._byid.values()
1201
"""Returns number of entries."""
1202
return len(self._byid)
1204
def get_entry(self, file_id):
1065
descend(self.root, u'')
1068
def directories(self):
1069
"""Return (path, entry) pairs for all directories, including the root.
1072
def descend(parent_ie, parent_path):
1073
accum.append((parent_path, parent_ie))
1075
kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
1078
for name, child_ie in kids:
1079
child_path = osutils.pathjoin(parent_path, name)
1080
descend(child_ie, child_path)
1081
descend(self.root, u'')
1084
def __contains__(self, file_id):
1085
"""True if this entry contains a file with given id.
1087
>>> inv = Inventory()
1088
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1089
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
1095
file_id = osutils.safe_file_id(file_id)
1096
return (file_id in self._byid)
1098
def __getitem__(self, file_id):
1205
1099
"""Return the entry for given file_id.
1207
1101
>>> inv = Inventory()
1208
>>> inv.add(InventoryFile(b'123123', 'hello.c', ROOT_ID))
1209
InventoryFile(b'123123', 'hello.c', parent_id=b'TREE_ROOT', sha1=None, len=None, revision=None)
1210
>>> inv.get_entry(b'123123').name
1102
>>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
1103
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
1104
>>> inv['123123'].name
1213
if not isinstance(file_id, bytes):
1214
raise TypeError(file_id)
1107
file_id = osutils.safe_file_id(file_id)
1216
1109
return self._byid[file_id]
1217
1110
except KeyError:
1422
1362
del old_parent.children[file_ie.name]
1423
1363
new_parent.children[new_name] = file_ie
1425
1365
file_ie.name = new_name
1426
1366
file_ie.parent_id = new_parent_id
1428
1368
def is_root(self, file_id):
1369
file_id = osutils.safe_file_id(file_id)
1429
1370
return self.root is not None and file_id == self.root.file_id
1432
class CHKInventory(CommonInventory):
1433
"""An inventory persisted in a CHK store.
1435
By design, a CHKInventory is immutable so many of the methods
1436
supported by Inventory - add, rename, apply_delta, etc - are *not*
1437
supported. To create a new CHKInventory, use create_by_apply_delta()
1438
or from_inventory(), say.
1440
Internally, a CHKInventory has one or two CHKMaps:
1442
* id_to_entry - a map from (file_id,) => InventoryEntry as bytes
1443
* parent_id_basename_to_file_id - a map from (parent_id, basename_utf8)
1446
The second map is optional and not present in early CHkRepository's.
1448
No caching is performed: every method call or item access will perform
1449
requests to the storage layer. As such, keep references to objects you
1453
def __init__(self, search_key_name):
1454
CommonInventory.__init__(self)
1455
self._fileid_to_entry_cache = {}
1456
self._fully_cached = False
1457
self._path_to_fileid_cache = {}
1458
self._search_key_name = search_key_name
1461
def __eq__(self, other):
1462
"""Compare two sets by comparing their contents."""
1463
if not isinstance(other, CHKInventory):
1464
return NotImplemented
1466
this_key = self.id_to_entry.key()
1467
other_key = other.id_to_entry.key()
1468
this_pid_key = self.parent_id_basename_to_file_id.key()
1469
other_pid_key = other.parent_id_basename_to_file_id.key()
1470
if None in (this_key, this_pid_key, other_key, other_pid_key):
1472
return this_key == other_key and this_pid_key == other_pid_key
1474
def _entry_to_bytes(self, entry):
1475
"""Serialise entry as a single bytestring.
1477
:param Entry: An inventory entry.
1478
:return: A bytestring for the entry.
1481
ENTRY ::= FILE | DIR | SYMLINK | TREE
1482
FILE ::= "file: " COMMON SEP SHA SEP SIZE SEP EXECUTABLE
1483
DIR ::= "dir: " COMMON
1484
SYMLINK ::= "symlink: " COMMON SEP TARGET_UTF8
1485
TREE ::= "tree: " COMMON REFERENCE_REVISION
1486
COMMON ::= FILE_ID SEP PARENT_ID SEP NAME_UTF8 SEP REVISION
1489
if entry.parent_id is not None:
1490
parent_str = entry.parent_id
1493
name_str = entry.name.encode("utf8")
1494
if entry.kind == 'file':
1495
if entry.executable:
1499
return b"file: %s\n%s\n%s\n%s\n%s\n%d\n%s" % (
1500
entry.file_id, parent_str, name_str, entry.revision,
1501
entry.text_sha1, entry.text_size, exec_str)
1502
elif entry.kind == 'directory':
1503
return b"dir: %s\n%s\n%s\n%s" % (
1504
entry.file_id, parent_str, name_str, entry.revision)
1505
elif entry.kind == 'symlink':
1506
return b"symlink: %s\n%s\n%s\n%s\n%s" % (
1507
entry.file_id, parent_str, name_str, entry.revision,
1508
entry.symlink_target.encode("utf8"))
1509
elif entry.kind == 'tree-reference':
1510
return b"tree: %s\n%s\n%s\n%s\n%s" % (
1511
entry.file_id, parent_str, name_str, entry.revision,
1512
entry.reference_revision)
1514
raise ValueError("unknown kind %r" % entry.kind)
1516
def _expand_fileids_to_parents_and_children(self, file_ids):
1517
"""Give a more wholistic view starting with the given file_ids.
1519
For any file_id which maps to a directory, we will include all children
1520
of that directory. We will also include all directories which are
1521
parents of the given file_ids, but we will not include their children.
1528
fringle # fringle-id
1532
if given [foo-id] we will include
1533
TREE_ROOT as interesting parents
1535
foo-id, baz-id, frob-id, fringle-id
1539
# TODO: Pre-pass over the list of fileids to see if anything is already
1540
# deserialized in self._fileid_to_entry_cache
1542
directories_to_expand = set()
1543
children_of_parent_id = {}
1544
# It is okay if some of the fileids are missing
1545
for entry in self._getitems(file_ids):
1546
if entry.kind == 'directory':
1547
directories_to_expand.add(entry.file_id)
1548
interesting.add(entry.parent_id)
1549
children_of_parent_id.setdefault(entry.parent_id, set()
1550
).add(entry.file_id)
1552
# Now, interesting has all of the direct parents, but not the
1553
# parents of those parents. It also may have some duplicates with
1555
remaining_parents = interesting.difference(file_ids)
1556
# When we hit the TREE_ROOT, we'll get an interesting parent of None,
1557
# but we don't actually want to recurse into that
1558
interesting.add(None) # this will auto-filter it in the loop
1559
remaining_parents.discard(None)
1560
while remaining_parents:
1561
next_parents = set()
1562
for entry in self._getitems(remaining_parents):
1563
next_parents.add(entry.parent_id)
1564
children_of_parent_id.setdefault(entry.parent_id, set()
1565
).add(entry.file_id)
1566
# Remove any search tips we've already processed
1567
remaining_parents = next_parents.difference(interesting)
1568
interesting.update(remaining_parents)
1569
# We should probably also .difference(directories_to_expand)
1570
interesting.update(file_ids)
1571
interesting.discard(None)
1572
while directories_to_expand:
1573
# Expand directories by looking in the
1574
# parent_id_basename_to_file_id map
1575
keys = [StaticTuple(f,).intern() for f in directories_to_expand]
1576
directories_to_expand = set()
1577
items = self.parent_id_basename_to_file_id.iteritems(keys)
1578
next_file_ids = {item[1] for item in items}
1579
next_file_ids = next_file_ids.difference(interesting)
1580
interesting.update(next_file_ids)
1581
for entry in self._getitems(next_file_ids):
1582
if entry.kind == 'directory':
1583
directories_to_expand.add(entry.file_id)
1584
children_of_parent_id.setdefault(entry.parent_id, set()
1585
).add(entry.file_id)
1586
return interesting, children_of_parent_id
1588
def filter(self, specific_fileids):
1589
"""Get an inventory view filtered against a set of file-ids.
1591
Children of directories and parents are included.
1593
The result may or may not reference the underlying inventory
1594
so it should be treated as immutable.
1597
parent_to_children) = self._expand_fileids_to_parents_and_children(
1599
# There is some overlap here, but we assume that all interesting items
1600
# are in the _fileid_to_entry_cache because we had to read them to
1601
# determine if they were a dir we wanted to recurse, or just a file
1602
# This should give us all the entries we'll want to add, so start
1604
other = Inventory(self.root_id)
1605
other.root.revision = self.root.revision
1606
other.revision_id = self.revision_id
1607
if not interesting or not parent_to_children:
1608
# empty filter, or filtering entrys that don't exist
1609
# (if even 1 existed, then we would have populated
1610
# parent_to_children with at least the tree root.)
1612
cache = self._fileid_to_entry_cache
1613
remaining_children = deque(
1614
parent_to_children[self.root_id])
1615
while remaining_children:
1616
file_id = remaining_children.popleft()
1618
if ie.kind == 'directory':
1619
ie = ie.copy() # We create a copy to depopulate the .children attribute
1620
# TODO: depending on the uses of 'other' we should probably alwyas
1621
# '.copy()' to prevent someone from mutating other and
1622
# invaliding our internal cache
1624
if file_id in parent_to_children:
1625
remaining_children.extend(parent_to_children[file_id])
1629
def _bytes_to_utf8name_key(data):
1630
"""Get the file_id, revision_id key out of data."""
1631
# We don't normally care about name, except for times when we want
1632
# to filter out empty names because of non rich-root...
1633
sections = data.split(b'\n')
1634
kind, file_id = sections[0].split(b': ')
1635
return (sections[2], file_id, sections[3])
1637
def _bytes_to_entry(self, bytes):
1638
"""Deserialise a serialised entry."""
1639
sections = bytes.split(b'\n')
1640
if sections[0].startswith(b"file: "):
1641
result = InventoryFile(sections[0][6:],
1642
sections[2].decode('utf8'),
1644
result.text_sha1 = sections[4]
1645
result.text_size = int(sections[5])
1646
result.executable = sections[6] == b"Y"
1647
elif sections[0].startswith(b"dir: "):
1648
result = CHKInventoryDirectory(sections[0][5:],
1649
sections[2].decode('utf8'),
1651
elif sections[0].startswith(b"symlink: "):
1652
result = InventoryLink(sections[0][9:],
1653
sections[2].decode('utf8'),
1655
result.symlink_target = sections[4].decode('utf8')
1656
elif sections[0].startswith(b"tree: "):
1657
result = TreeReference(sections[0][6:],
1658
sections[2].decode('utf8'),
1660
result.reference_revision = sections[4]
1662
raise ValueError("Not a serialised entry %r" % bytes)
1663
result.file_id = result.file_id
1664
result.revision = sections[3]
1665
if result.parent_id == b'':
1666
result.parent_id = None
1667
self._fileid_to_entry_cache[result.file_id] = result
1670
def create_by_apply_delta(self, inventory_delta, new_revision_id,
1671
propagate_caches=False):
1672
"""Create a new CHKInventory by applying inventory_delta to this one.
1674
See the inventory developers documentation for the theory behind
1677
:param inventory_delta: The inventory delta to apply. See
1678
Inventory.apply_delta for details.
1679
:param new_revision_id: The revision id of the resulting CHKInventory.
1680
:param propagate_caches: If True, the caches for this inventory are
1681
copied to and updated for the result.
1682
:return: The new CHKInventory.
1684
split = osutils.split
1685
result = CHKInventory(self._search_key_name)
1686
if propagate_caches:
1687
# Just propagate the path-to-fileid cache for now
1688
result._path_to_fileid_cache = self._path_to_fileid_cache.copy()
1689
search_key_func = chk_map.search_key_registry.get(
1690
self._search_key_name)
1691
self.id_to_entry._ensure_root()
1692
maximum_size = self.id_to_entry._root_node.maximum_size
1693
result.revision_id = new_revision_id
1694
result.id_to_entry = chk_map.CHKMap(
1695
self.id_to_entry._store,
1696
self.id_to_entry.key(),
1697
search_key_func=search_key_func)
1698
result.id_to_entry._ensure_root()
1699
result.id_to_entry._root_node.set_maximum_size(maximum_size)
1700
# Change to apply to the parent_id_basename delta. The dict maps
1701
# (parent_id, basename) -> (old_key, new_value). We use a dict because
1702
# when a path has its id replaced (e.g. the root is changed, or someone
1703
# does bzr mv a b, bzr mv c a, we should output a single change to this
1704
# map rather than two.
1705
parent_id_basename_delta = {}
1706
if self.parent_id_basename_to_file_id is not None:
1707
result.parent_id_basename_to_file_id = chk_map.CHKMap(
1708
self.parent_id_basename_to_file_id._store,
1709
self.parent_id_basename_to_file_id.key(),
1710
search_key_func=search_key_func)
1711
result.parent_id_basename_to_file_id._ensure_root()
1712
self.parent_id_basename_to_file_id._ensure_root()
1713
result_p_id_root = result.parent_id_basename_to_file_id._root_node
1714
p_id_root = self.parent_id_basename_to_file_id._root_node
1715
result_p_id_root.set_maximum_size(p_id_root.maximum_size)
1716
result_p_id_root._key_width = p_id_root._key_width
1718
result.parent_id_basename_to_file_id = None
1719
result.root_id = self.root_id
1720
id_to_entry_delta = []
1721
# inventory_delta is only traversed once, so we just update the
1723
# Check for repeated file ids
1724
inventory_delta = _check_delta_unique_ids(inventory_delta)
1725
# Repeated old paths
1726
inventory_delta = _check_delta_unique_old_paths(inventory_delta)
1727
# Check for repeated new paths
1728
inventory_delta = _check_delta_unique_new_paths(inventory_delta)
1729
# Check for entries that don't match the fileid
1730
inventory_delta = _check_delta_ids_match_entry(inventory_delta)
1731
# Check for nonsense fileids
1732
inventory_delta = _check_delta_ids_are_valid(inventory_delta)
1733
# Check for new_path <-> entry consistency
1734
inventory_delta = _check_delta_new_path_entry_both_or_None(
1736
# All changed entries need to have their parents be directories and be
1737
# at the right path. This set contains (path, id) tuples.
1739
# When we delete an item, all the children of it must be either deleted
1740
# or altered in their own right. As we batch process the change via
1741
# CHKMap.apply_delta, we build a set of things to use to validate the
1745
for old_path, new_path, file_id, entry in inventory_delta:
1748
result.root_id = file_id
1749
if new_path is None:
1754
if propagate_caches:
1756
del result._path_to_fileid_cache[old_path]
1759
deletes.add(file_id)
1761
new_key = StaticTuple(file_id,)
1762
new_value = result._entry_to_bytes(entry)
1763
# Update caches. It's worth doing this whether
1764
# we're propagating the old caches or not.
1765
result._path_to_fileid_cache[new_path] = file_id
1766
parents.add((split(new_path)[0], entry.parent_id))
1767
if old_path is None:
1770
old_key = StaticTuple(file_id,)
1771
if self.id2path(file_id) != old_path:
1772
raise errors.InconsistentDelta(old_path, file_id,
1773
"Entry was at wrong other path %r." %
1774
self.id2path(file_id))
1775
altered.add(file_id)
1776
id_to_entry_delta.append(StaticTuple(old_key, new_key, new_value))
1777
if result.parent_id_basename_to_file_id is not None:
1778
# parent_id, basename changes
1779
if old_path is None:
1782
old_entry = self.get_entry(file_id)
1783
old_key = self._parent_id_basename_key(old_entry)
1784
if new_path is None:
1788
new_key = self._parent_id_basename_key(entry)
1790
# If the two keys are the same, the value will be unchanged
1791
# as its always the file id for this entry.
1792
if old_key != new_key:
1793
# Transform a change into explicit delete/add preserving
1794
# a possible match on the key from a different file id.
1795
if old_key is not None:
1796
parent_id_basename_delta.setdefault(
1797
old_key, [None, None])[0] = old_key
1798
if new_key is not None:
1799
parent_id_basename_delta.setdefault(
1800
new_key, [None, None])[1] = new_value
1801
# validate that deletes are complete.
1802
for file_id in deletes:
1803
entry = self.get_entry(file_id)
1804
if entry.kind != 'directory':
1806
# This loop could potentially be better by using the id_basename
1807
# map to just get the child file ids.
1808
for child in entry.children.values():
1809
if child.file_id not in altered:
1810
raise errors.InconsistentDelta(self.id2path(child.file_id),
1811
child.file_id, "Child not deleted or reparented when "
1813
result.id_to_entry.apply_delta(id_to_entry_delta)
1814
if parent_id_basename_delta:
1815
# Transform the parent_id_basename delta data into a linear delta
1816
# with only one record for a given key. Optimally this would allow
1817
# re-keying, but its simpler to just output that as a delete+add
1818
# to spend less time calculating the delta.
1820
for key, (old_key, value) in parent_id_basename_delta.items():
1821
if value is not None:
1822
delta_list.append((old_key, key, value))
1824
delta_list.append((old_key, None, None))
1825
result.parent_id_basename_to_file_id.apply_delta(delta_list)
1826
parents.discard(('', None))
1827
for parent_path, parent in parents:
1829
if result.get_entry(parent).kind != 'directory':
1830
raise errors.InconsistentDelta(result.id2path(parent), parent,
1831
'Not a directory, but given children')
1832
except errors.NoSuchId:
1833
raise errors.InconsistentDelta("<unknown>", parent,
1834
"Parent is not present in resulting inventory.")
1835
if result.path2id(parent_path) != parent:
1836
raise errors.InconsistentDelta(parent_path, parent,
1837
"Parent has wrong path %r." % result.path2id(parent_path))
1841
def deserialise(klass, chk_store, lines, expected_revision_id):
1842
"""Deserialise a CHKInventory.
1844
:param chk_store: A CHK capable VersionedFiles instance.
1845
:param bytes: The serialised bytes.
1846
:param expected_revision_id: The revision ID we think this inventory is
1848
:return: A CHKInventory
1850
if not lines[-1].endswith(b'\n'):
1851
raise ValueError("last line should have trailing eol\n")
1852
if lines[0] != b'chkinventory:\n':
1853
raise ValueError("not a serialised CHKInventory: %r" % bytes)
1855
allowed_keys = frozenset((b'root_id', b'revision_id',
1856
b'parent_id_basename_to_file_id',
1857
b'search_key_name', b'id_to_entry'))
1858
for line in lines[1:]:
1859
key, value = line.rstrip(b'\n').split(b': ', 1)
1860
if key not in allowed_keys:
1861
raise errors.BzrError('Unknown key in inventory: %r\n%r'
1864
raise errors.BzrError('Duplicate key in inventory: %r\n%r'
1867
revision_id = info[b'revision_id']
1868
root_id = info[b'root_id']
1869
search_key_name = info.get(b'search_key_name', b'plain')
1870
parent_id_basename_to_file_id = info.get(
1871
b'parent_id_basename_to_file_id', None)
1872
if not parent_id_basename_to_file_id.startswith(b'sha1:'):
1873
raise ValueError('parent_id_basename_to_file_id should be a sha1'
1874
' key not %r' % (parent_id_basename_to_file_id,))
1875
id_to_entry = info[b'id_to_entry']
1876
if not id_to_entry.startswith(b'sha1:'):
1877
raise ValueError('id_to_entry should be a sha1'
1878
' key not %r' % (id_to_entry,))
1880
result = CHKInventory(search_key_name)
1881
result.revision_id = revision_id
1882
result.root_id = root_id
1883
search_key_func = chk_map.search_key_registry.get(
1884
result._search_key_name)
1885
if parent_id_basename_to_file_id is not None:
1886
result.parent_id_basename_to_file_id = chk_map.CHKMap(
1887
chk_store, StaticTuple(parent_id_basename_to_file_id,),
1888
search_key_func=search_key_func)
1890
result.parent_id_basename_to_file_id = None
1892
result.id_to_entry = chk_map.CHKMap(chk_store,
1893
StaticTuple(id_to_entry,),
1894
search_key_func=search_key_func)
1895
if (result.revision_id,) != expected_revision_id:
1896
raise ValueError("Mismatched revision id and expected: %r, %r" %
1897
(result.revision_id, expected_revision_id))
1901
def from_inventory(klass, chk_store, inventory, maximum_size=0, search_key_name=b'plain'):
1902
"""Create a CHKInventory from an existing inventory.
1904
The content of inventory is copied into the chk_store, and a
1905
CHKInventory referencing that is returned.
1907
:param chk_store: A CHK capable VersionedFiles instance.
1908
:param inventory: The inventory to copy.
1909
:param maximum_size: The CHKMap node size limit.
1910
:param search_key_name: The identifier for the search key function
1912
result = klass(search_key_name)
1913
result.revision_id = inventory.revision_id
1914
result.root_id = inventory.root.file_id
1916
entry_to_bytes = result._entry_to_bytes
1917
parent_id_basename_key = result._parent_id_basename_key
1918
id_to_entry_dict = {}
1919
parent_id_basename_dict = {}
1920
for path, entry in inventory.iter_entries():
1921
key = StaticTuple(entry.file_id,).intern()
1922
id_to_entry_dict[key] = entry_to_bytes(entry)
1923
p_id_key = parent_id_basename_key(entry)
1924
parent_id_basename_dict[p_id_key] = entry.file_id
1926
result._populate_from_dicts(chk_store, id_to_entry_dict,
1927
parent_id_basename_dict, maximum_size=maximum_size)
1930
def _populate_from_dicts(self, chk_store, id_to_entry_dict,
1931
parent_id_basename_dict, maximum_size):
1932
search_key_func = chk_map.search_key_registry.get(
1933
self._search_key_name)
1934
root_key = chk_map.CHKMap.from_dict(chk_store, id_to_entry_dict,
1935
maximum_size=maximum_size, key_width=1,
1936
search_key_func=search_key_func)
1937
self.id_to_entry = chk_map.CHKMap(chk_store, root_key,
1939
root_key = chk_map.CHKMap.from_dict(chk_store,
1940
parent_id_basename_dict,
1941
maximum_size=maximum_size, key_width=2,
1942
search_key_func=search_key_func)
1943
self.parent_id_basename_to_file_id = chk_map.CHKMap(chk_store,
1944
root_key, search_key_func)
1946
def _parent_id_basename_key(self, entry):
1947
"""Create a key for a entry in a parent_id_basename_to_file_id index."""
1948
if entry.parent_id is not None:
1949
parent_id = entry.parent_id
1952
return StaticTuple(parent_id, entry.name.encode('utf8')).intern()
1954
def get_entry(self, file_id):
1955
"""map a single file_id -> InventoryEntry."""
1957
raise errors.NoSuchId(self, file_id)
1958
result = self._fileid_to_entry_cache.get(file_id, None)
1959
if result is not None:
1962
return self._bytes_to_entry(
1963
next(self.id_to_entry.iteritems([StaticTuple(file_id,)]))[1])
1964
except StopIteration:
1965
# really we're passing an inventory, not a tree...
1966
raise errors.NoSuchId(self, file_id)
1968
def _getitems(self, file_ids):
1969
"""Similar to get_entry, but lets you query for multiple.
1971
The returned order is undefined. And currently if an item doesn't
1972
exist, it isn't included in the output.
1976
for file_id in file_ids:
1977
entry = self._fileid_to_entry_cache.get(file_id, None)
1979
remaining.append(file_id)
1981
result.append(entry)
1982
file_keys = [StaticTuple(f,).intern() for f in remaining]
1983
for file_key, value in self.id_to_entry.iteritems(file_keys):
1984
entry = self._bytes_to_entry(value)
1985
result.append(entry)
1986
self._fileid_to_entry_cache[entry.file_id] = entry
1989
def has_id(self, file_id):
1990
# Perhaps have an explicit 'contains' method on CHKMap ?
1991
if self._fileid_to_entry_cache.get(file_id, None) is not None:
1994
self.id_to_entry.iteritems([StaticTuple(file_id,)]))) == 1
1996
def is_root(self, file_id):
1997
return file_id == self.root_id
1999
def _iter_file_id_parents(self, file_id):
2000
"""Yield the parents of file_id up to the root."""
2001
while file_id is not None:
2003
ie = self.get_entry(file_id)
2005
raise errors.NoSuchId(tree=self, file_id=file_id)
2007
file_id = ie.parent_id
2009
def iter_all_ids(self):
2010
"""Iterate over all file-ids."""
2011
for key, _ in self.id_to_entry.iteritems():
2014
def iter_just_entries(self):
2015
"""Iterate over all entries.
2017
Unlike iter_entries(), just the entries are returned (not (path, ie))
2018
and the order of entries is undefined.
2020
XXX: We may not want to merge this into bzr.dev.
2022
for key, entry in self.id_to_entry.iteritems():
2024
ie = self._fileid_to_entry_cache.get(file_id, None)
2026
ie = self._bytes_to_entry(entry)
2027
self._fileid_to_entry_cache[file_id] = ie
2030
def _preload_cache(self):
2031
"""Make sure all file-ids are in _fileid_to_entry_cache"""
2032
if self._fully_cached:
2033
return # No need to do it again
2034
# The optimal sort order is to use iteritems() directly
2035
cache = self._fileid_to_entry_cache
2036
for key, entry in self.id_to_entry.iteritems():
2038
if file_id not in cache:
2039
ie = self._bytes_to_entry(entry)
2043
last_parent_id = last_parent_ie = None
2044
pid_items = self.parent_id_basename_to_file_id.iteritems()
2045
for key, child_file_id in pid_items:
2046
if key == (b'', b''): # This is the root
2047
if child_file_id != self.root_id:
2048
raise ValueError('Data inconsistency detected.'
2049
' We expected data with key ("","") to match'
2050
' the root id, but %s != %s'
2051
% (child_file_id, self.root_id))
2053
parent_id, basename = key
2054
ie = cache[child_file_id]
2055
if parent_id == last_parent_id:
2056
parent_ie = last_parent_ie
2058
parent_ie = cache[parent_id]
2059
if parent_ie.kind != 'directory':
2060
raise ValueError('Data inconsistency detected.'
2061
' An entry in the parent_id_basename_to_file_id map'
2062
' has parent_id {%s} but the kind of that object'
2063
' is %r not "directory"' % (parent_id, parent_ie.kind))
2064
if parent_ie._children is None:
2065
parent_ie._children = {}
2066
basename = basename.decode('utf-8')
2067
if basename in parent_ie._children:
2068
existing_ie = parent_ie._children[basename]
2069
if existing_ie != ie:
2070
raise ValueError('Data inconsistency detected.'
2071
' Two entries with basename %r were found'
2072
' in the parent entry {%s}'
2073
% (basename, parent_id))
2074
if basename != ie.name:
2075
raise ValueError('Data inconsistency detected.'
2076
' In the parent_id_basename_to_file_id map, file_id'
2077
' {%s} is listed as having basename %r, but in the'
2078
' id_to_entry map it is %r'
2079
% (child_file_id, basename, ie.name))
2080
parent_ie._children[basename] = ie
2081
self._fully_cached = True
2083
def iter_changes(self, basis):
2084
"""Generate a Tree.iter_changes change list between this and basis.
2086
:param basis: Another CHKInventory.
2087
:return: An iterator over the changes between self and basis, as per
2088
tree.iter_changes().
2090
# We want: (file_id, (path_in_source, path_in_target),
2091
# changed_content, versioned, parent, name, kind,
2093
for key, basis_value, self_value in \
2094
self.id_to_entry.iter_changes(basis.id_to_entry):
2096
if basis_value is not None:
2097
basis_entry = basis._bytes_to_entry(basis_value)
2098
path_in_source = basis.id2path(file_id)
2099
basis_parent = basis_entry.parent_id
2100
basis_name = basis_entry.name
2101
basis_executable = basis_entry.executable
2103
path_in_source = None
2106
basis_executable = None
2107
if self_value is not None:
2108
self_entry = self._bytes_to_entry(self_value)
2109
path_in_target = self.id2path(file_id)
2110
self_parent = self_entry.parent_id
2111
self_name = self_entry.name
2112
self_executable = self_entry.executable
2114
path_in_target = None
2117
self_executable = None
2118
if basis_value is None:
2120
kind = (None, self_entry.kind)
2121
versioned = (False, True)
2122
elif self_value is None:
2124
kind = (basis_entry.kind, None)
2125
versioned = (True, False)
2127
kind = (basis_entry.kind, self_entry.kind)
2128
versioned = (True, True)
2129
changed_content = False
2130
if kind[0] != kind[1]:
2131
changed_content = True
2132
elif kind[0] == 'file':
2133
if (self_entry.text_size != basis_entry.text_size
2134
or self_entry.text_sha1 != basis_entry.text_sha1):
2135
changed_content = True
2136
elif kind[0] == 'symlink':
2137
if self_entry.symlink_target != basis_entry.symlink_target:
2138
changed_content = True
2139
elif kind[0] == 'tree-reference':
2140
if (self_entry.reference_revision
2141
!= basis_entry.reference_revision):
2142
changed_content = True
2143
parent = (basis_parent, self_parent)
2144
name = (basis_name, self_name)
2145
executable = (basis_executable, self_executable)
2146
if (not changed_content and
2147
parent[0] == parent[1] and
2148
name[0] == name[1] and
2149
executable[0] == executable[1]):
2150
# Could happen when only the revision changed for a directory
2154
file_id, (path_in_source, path_in_target), changed_content,
2155
versioned, parent, name, kind, executable)
2158
"""Return the number of entries in the inventory."""
2159
return len(self.id_to_entry)
2161
def _make_delta(self, old):
2162
"""Make an inventory delta from two inventories."""
2163
if not isinstance(old, CHKInventory):
2164
return CommonInventory._make_delta(self, old)
2166
for key, old_value, self_value in \
2167
self.id_to_entry.iter_changes(old.id_to_entry):
2169
if old_value is not None:
2170
old_path = old.id2path(file_id)
2173
if self_value is not None:
2174
entry = self._bytes_to_entry(self_value)
2175
self._fileid_to_entry_cache[file_id] = entry
2176
new_path = self.id2path(file_id)
2180
delta.append((old_path, new_path, file_id, entry))
2183
def path2id(self, relpath):
2184
"""See CommonInventory.path2id()."""
2185
# TODO: perhaps support negative hits?
2186
if isinstance(relpath, str):
2187
names = osutils.splitpath(relpath)
2192
relpath = osutils.pathjoin(*relpath)
2193
result = self._path_to_fileid_cache.get(relpath, None)
2194
if result is not None:
2196
current_id = self.root_id
2197
if current_id is None:
2199
parent_id_index = self.parent_id_basename_to_file_id
2201
for basename in names:
2202
if cur_path is None:
2205
cur_path = cur_path + '/' + basename
2206
basename_utf8 = basename.encode('utf8')
2207
file_id = self._path_to_fileid_cache.get(cur_path, None)
2209
key_filter = [StaticTuple(current_id, basename_utf8)]
2210
items = parent_id_index.iteritems(key_filter)
2211
for (parent_id, name_utf8), file_id in items:
2212
if parent_id != current_id or name_utf8 != basename_utf8:
2213
raise errors.BzrError("corrupt inventory lookup! "
2214
"%r %r %r %r" % (parent_id, current_id, name_utf8,
2219
self._path_to_fileid_cache[cur_path] = file_id
2220
current_id = file_id
2224
"""Serialise the inventory to lines."""
2225
lines = [b"chkinventory:\n"]
2226
if self._search_key_name != b'plain':
2227
# custom ordering grouping things that don't change together
2228
lines.append(b'search_key_name: %s\n' % (
2229
self._search_key_name))
2230
lines.append(b"root_id: %s\n" % self.root_id)
2231
lines.append(b'parent_id_basename_to_file_id: %s\n' %
2232
(self.parent_id_basename_to_file_id.key()[0],))
2233
lines.append(b"revision_id: %s\n" % self.revision_id)
2234
lines.append(b"id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
2236
lines.append(b"revision_id: %s\n" % self.revision_id)
2237
lines.append(b"root_id: %s\n" % self.root_id)
2238
if self.parent_id_basename_to_file_id is not None:
2239
lines.append(b'parent_id_basename_to_file_id: %s\n' %
2240
(self.parent_id_basename_to_file_id.key()[0],))
2241
lines.append(b"id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
2246
"""Get the root entry."""
2247
return self.get_entry(self.root_id)
2250
class CHKInventoryDirectory(InventoryDirectory):
2251
"""A directory in an inventory."""
2253
__slots__ = ['_children', '_chk_inventory']
2255
def __init__(self, file_id, name, parent_id, chk_inventory):
2256
# Don't call InventoryDirectory.__init__ - it isn't right for this
2258
InventoryEntry.__init__(self, file_id, name, parent_id)
2259
self._children = None
2260
self._chk_inventory = chk_inventory
2264
"""Access the list of children of this directory.
2266
With a parent_id_basename_to_file_id index, loads all the children,
2267
without loads the entire index. Without is bad. A more sophisticated
2268
proxy object might be nice, to allow partial loading of children as
2269
well when specific names are accessed. (So path traversal can be
2270
written in the obvious way but not examine siblings.).
2272
if self._children is not None:
2273
return self._children
2274
# No longer supported
2275
if self._chk_inventory.parent_id_basename_to_file_id is None:
2276
raise AssertionError("Inventories without"
2277
" parent_id_basename_to_file_id are no longer supported")
2279
# XXX: Todo - use proxy objects for the children rather than loading
2280
# all when the attribute is referenced.
2281
parent_id_index = self._chk_inventory.parent_id_basename_to_file_id
2283
for (parent_id, name_utf8), file_id in parent_id_index.iteritems(
2284
key_filter=[StaticTuple(self.file_id,)]):
2285
child_keys.add(StaticTuple(file_id,))
2287
for file_id_key in child_keys:
2288
entry = self._chk_inventory._fileid_to_entry_cache.get(
2289
file_id_key[0], None)
2290
if entry is not None:
2291
result[entry.name] = entry
2292
cached.add(file_id_key)
2293
child_keys.difference_update(cached)
2294
# populate; todo: do by name
2295
id_to_entry = self._chk_inventory.id_to_entry
2296
for file_id_key, bytes in id_to_entry.iteritems(child_keys):
2297
entry = self._chk_inventory._bytes_to_entry(bytes)
2298
result[entry.name] = entry
2299
self._chk_inventory._fileid_to_entry_cache[file_id_key[0]] = entry
2300
self._children = result
2304
1373
entry_factory = {
2305
1374
'directory': InventoryDirectory,
2306
1375
'file': InventoryFile,