23
23
# But those depend on its position within a particular inventory, and
24
24
# it would be nice not to need to hold the backpointer here.
26
from __future__ import absolute_import
26
28
# This should really be an id randomly assigned when the tree is
27
29
# created, but it's not for now.
28
ROOT_ID = b"TREE_ROOT"
31
from collections.abc import deque
32
except ImportError: # python < 3.7
33
from collections import deque
36
from ..lazy_import import lazy_import
32
from bzrlib.lazy_import import lazy_import
37
33
lazy_import(globals(), """
39
from breezy.bzr import (
51
from ..static_tuple import StaticTuple
54
class InvalidEntryName(errors.InternalBzrError):
56
_fmt = "Invalid entry name: %(name)s"
58
def __init__(self, name):
59
errors.BzrError.__init__(self)
63
class DuplicateFileId(errors.BzrError):
65
_fmt = "File id {%(file_id)s} already exists in inventory as %(entry)s"
67
def __init__(self, file_id, entry):
68
errors.BzrError.__init__(self)
69
self.file_id = file_id
52
from bzrlib.static_tuple import StaticTuple
53
from bzrlib.symbol_versioning import (
73
59
class InventoryEntry(object):
103
89
>>> 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)
92
>>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
93
InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
94
>>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
95
InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None, revision=None)
110
96
>>> shouldbe = {0: '', 1: 'src', 2: 'src/hello.c'}
111
97
>>> for ix, j in enumerate(i.iter_entries()):
112
... print(j[0] == shouldbe[ix], j[1])
98
... 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)
100
(True, InventoryDirectory('TREE_ROOT', u'', parent_id=None, revision=None))
101
(True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
102
(True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None, revision=None))
103
>>> i.add(InventoryFile('2324', 'bye.c', '123'))
104
InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None, revision=None)
105
>>> i.add(InventoryDirectory('2325', 'wibble', '123'))
106
InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
121
107
>>> 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)
109
>>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
110
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None, revision=None)
112
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None, revision=None)
127
113
>>> for path, entry in i.iter_entries():
225
209
The filename must be a single component, relative to the
226
210
parent directory; it cannot be a whole path or relative name.
228
>>> e = InventoryFile(b'123', 'hello.c', ROOT_ID)
212
>>> e = InventoryFile('123', 'hello.c', ROOT_ID)
233
>>> e = InventoryFile(b'123', 'src/hello.c', ROOT_ID)
217
>>> e = InventoryFile('123', 'src/hello.c', ROOT_ID)
234
218
Traceback (most recent call last):
235
breezy.bzr.inventory.InvalidEntryName: Invalid entry name: src/hello.c
219
InvalidEntryName: Invalid entry name: src/hello.c
238
raise InvalidEntryName(name=name)
239
if not isinstance(file_id, bytes):
240
raise TypeError(file_id)
221
if '/' in name or '\\' in name:
222
raise errors.InvalidEntryName(name=name)
241
223
self.file_id = file_id
242
224
self.revision = None
346
325
if not isinstance(other, InventoryEntry):
347
326
return NotImplemented
349
return ((self.file_id == other.file_id) and
350
(self.name == other.name) and
351
(other.symlink_target == self.symlink_target) and
352
(self.text_sha1 == other.text_sha1) and
353
(self.text_size == other.text_size) and
354
(self.text_id == other.text_id) and
355
(self.parent_id == other.parent_id) and
356
(self.kind == other.kind) and
357
(self.revision == other.revision) and
358
(self.executable == other.executable) and
359
(self.reference_revision == other.reference_revision)
328
return ((self.file_id == other.file_id)
329
and (self.name == other.name)
330
and (other.symlink_target == self.symlink_target)
331
and (self.text_sha1 == other.text_sha1)
332
and (self.text_size == other.text_size)
333
and (self.text_id == other.text_id)
334
and (self.parent_id == other.parent_id)
335
and (self.kind == other.kind)
336
and (self.revision == other.revision)
337
and (self.executable == other.executable)
338
and (self.reference_revision == other.reference_revision)
362
341
def __ne__(self, other):
413
392
# to provide a per-fileid log. The hash of every directory content is
414
393
# "da..." below (the sha1sum of '').
415
394
checker.add_pending_item(rev_id,
416
('texts', self.file_id, self.revision), b'text',
417
b'da39a3ee5e6b4b0d3255bfef95601890afd80709')
395
('texts', self.file_id, self.revision), 'text',
396
'da39a3ee5e6b4b0d3255bfef95601890afd80709')
420
399
other = InventoryDirectory(self.file_id, self.name, self.parent_id)
453
429
"""See InventoryEntry._check"""
454
430
# TODO: check size too.
455
431
checker.add_pending_item(tree_revision_id,
456
('texts', self.file_id, self.revision), b'text',
432
('texts', self.file_id, self.revision), 'text',
458
434
if self.text_size is None:
459
435
checker._report_items.append(
460
436
'fileid {%s} in {%s} has None for text_size' % (self.file_id,
464
440
other = InventoryFile(self.file_id, self.name, self.parent_id)
476
452
return text_modified, meta_modified
478
454
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
479
output_to, reverse=False):
455
output_to, reverse=False):
480
456
"""See InventoryEntry._diff."""
481
from breezy.diff import DiffText
457
from bzrlib.diff import DiffText
482
458
from_file_id = self.file_id
484
460
to_file_id = to_entry.file_id
485
to_path = to_tree.id2path(to_file_id)
487
462
to_file_id = None
489
if from_file_id is not None:
490
from_path = tree.id2path(from_file_id)
494
464
to_file_id, from_file_id = from_file_id, to_file_id
495
465
tree, to_tree = to_tree, tree
496
466
from_label, to_label = to_label, from_label
497
467
differ = DiffText(tree, to_tree, output_to, 'utf-8', '', '',
499
return differ.diff_text(from_path, to_path, from_label, to_label,
500
from_file_id, to_file_id)
469
return differ.diff_text(from_file_id, to_file_id, from_label, to_label)
502
471
def has_text(self):
503
472
"""See InventoryEntry.has_text."""
557
526
if self.symlink_target is None:
558
527
checker._report_items.append(
559
528
'symlink {%s} has no target in revision {%s}'
560
% (self.file_id, tree_revision_id))
529
% (self.file_id, tree_revision_id))
561
530
# Symlinks are stored as ''
562
531
checker.add_pending_item(tree_revision_id,
563
('texts', self.file_id, self.revision), b'text',
564
b'da39a3ee5e6b4b0d3255bfef95601890afd80709')
532
('texts', self.file_id, self.revision), 'text',
533
'da39a3ee5e6b4b0d3255bfef95601890afd80709')
567
536
other = InventoryLink(self.file_id, self.name, self.parent_id)
579
548
return text_modified, meta_modified
581
550
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
582
output_to, reverse=False):
551
output_to, reverse=False):
583
552
"""See InventoryEntry._diff."""
584
from breezy.diff import DiffSymlink
553
from bzrlib.diff import DiffSymlink
585
554
old_target = self.symlink_target
586
555
if to_entry is not None:
587
556
new_target = to_entry.symlink_target
701
665
from_dir = self.root
702
666
yield '', self.root
703
elif isinstance(from_dir, bytes):
704
from_dir = self.get_entry(from_dir)
667
elif isinstance(from_dir, basestring):
668
from_dir = self[from_dir]
706
670
# unrolling the recursive called changed the time from
707
671
# 440ms/663ms (inline/total) to 116ms/116ms
708
children = sorted(from_dir.children.items())
672
children = from_dir.children.items()
709
674
if not recursive:
675
for name, ie in children:
712
children = deque(children)
678
children = collections.deque(children)
713
679
stack = [(u'', children)]
715
681
from_dir_relpath, children = stack[-1]
769
740
if self.root is None:
771
742
# Optimize a common case
772
if (specific_file_ids is not None
773
and len(specific_file_ids) == 1):
743
if (not yield_parents and specific_file_ids is not None and
744
len(specific_file_ids) == 1):
774
745
file_id = list(specific_file_ids)[0]
775
if file_id is not None:
777
path = self.id2path(file_id)
778
except errors.NoSuchId:
781
yield path, self.get_entry(file_id)
746
if self.has_id(file_id):
747
yield self.id2path(file_id), self[file_id]
783
749
from_dir = self.root
784
if (specific_file_ids is None
785
or self.root.file_id in specific_file_ids):
750
if (specific_file_ids is None or yield_parents or
751
self.root.file_id in specific_file_ids):
786
752
yield u'', self.root
787
elif isinstance(from_dir, bytes):
788
from_dir = self.get_entry(from_dir)
790
raise TypeError(from_dir)
753
elif isinstance(from_dir, basestring):
754
from_dir = self[from_dir]
792
756
if specific_file_ids is not None:
793
757
# TODO: jam 20070302 This could really be done as a loop rather
794
758
# than a bunch of recursive calls.
798
761
def add_ancestors(file_id):
799
762
if not byid.has_id(file_id):
801
parent_id = byid.get_entry(file_id).parent_id
764
parent_id = byid[file_id].parent_id
802
765
if parent_id is None:
804
767
if parent_id not in parents:
814
777
cur_relpath, cur_dir = stack.pop()
817
for child_name, child_ie in sorted(cur_dir.children.items()):
780
for child_name, child_ie in sorted(cur_dir.children.iteritems()):
819
782
child_relpath = cur_relpath + child_name
821
if (specific_file_ids is None
822
or child_ie.file_id in specific_file_ids):
784
if (specific_file_ids is None or
785
child_ie.file_id in specific_file_ids or
786
(yield_parents and child_ie.file_id in parents)):
823
787
yield child_relpath, child_ie
825
789
if child_ie.kind == 'directory':
826
790
if parents is None or child_ie.file_id in parents:
827
child_dirs.append((child_relpath + '/', child_ie))
791
child_dirs.append((child_relpath+'/', child_ie))
828
792
stack.extend(reversed(child_dirs))
830
794
def _make_delta(self, old):
831
795
"""Make an inventory delta from two inventories."""
832
old_ids = set(old.iter_all_ids())
833
new_ids = set(self.iter_all_ids())
834
798
adds = new_ids - old_ids
835
799
deletes = old_ids - new_ids
836
800
common = old_ids.intersection(new_ids)
838
802
for file_id in deletes:
839
803
delta.append((old.id2path(file_id), None, file_id, None))
840
804
for file_id in adds:
841
delta.append((None, self.id2path(file_id),
842
file_id, self.get_entry(file_id)))
805
delta.append((None, self.id2path(file_id), file_id, self[file_id]))
843
806
for file_id in common:
844
if old.get_entry(file_id) != self.get_entry(file_id):
807
if old[file_id] != self[file_id]:
845
808
delta.append((old.id2path(file_id), self.id2path(file_id),
846
file_id, self.get_entry(file_id)))
809
file_id, self[file_id]))
849
812
def make_entry(self, kind, name, parent_id, file_id=None):
850
"""Simple thunk to breezy.bzr.inventory.make_entry."""
813
"""Simple thunk to bzrlib.inventory.make_entry."""
851
814
return make_entry(kind, name, parent_id, file_id)
853
816
def entries(self):
869
832
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.
835
def path2id(self, relpath):
836
"""Walk down through directories to return entry of last component.
908
838
:param relpath: may be either a list of path components, or a single
909
839
string, in which case it is automatically split.
971
886
entries = self.iter_entries()
972
887
if self.root is None:
973
888
return Inventory(root_id=None)
974
other = Inventory(next(entries)[1].file_id)
889
other = Inventory(entries.next()[1].file_id)
975
890
other.root.revision = self.root.revision
976
891
other.revision_id = self.revision_id
977
892
directories_to_expand = set()
978
893
for path, entry in entries:
979
894
file_id = entry.file_id
980
if (file_id in specific_fileids or
981
entry.parent_id in directories_to_expand):
895
if (file_id in specific_fileids
896
or entry.parent_id in directories_to_expand):
982
897
if entry.kind == 'directory':
983
898
directories_to_expand.add(file_id)
984
899
elif file_id not in interesting_parents:
1009
924
returned quickly.
1011
926
>>> 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
927
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
928
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
929
>>> inv['123-123'].name
1017
932
Id's may be looked up from paths:
1019
934
>>> inv.path2id('hello.c')
1021
>>> inv.has_id(b'123-123')
936
>>> inv.has_id('123-123')
1024
939
There are iterators over the contents:
1026
941
>>> [entry[0] for entry in inv.iter_entries()]
1030
945
def __init__(self, root_id=ROOT_ID, revision_id=None):
1066
981
applied the final inventory must be internally consistent, but it
1067
982
is ok to supply changes which, if only half-applied would have an
1068
983
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)].
984
files, 'A' and 'B' with each other : [('A', 'B', 'A-id', a_entry),
985
('B', 'A', 'B-id', b_entry)].
1072
987
Each change is a tuple, of the form (old_path, new_path, file_id,
1112
1027
# after their children, which means that everything we examine has no
1113
1028
# modified children remaining by the time we examine it.
1114
1029
for old_path, file_id in sorted(((op, f) for op, np, f, e in delta
1115
if op is not None), reverse=True):
1030
if op is not None), reverse=True):
1116
1031
# Preserve unaltered children of file_id for later reinsertion.
1117
file_id_children = getattr(self.get_entry(file_id), 'children', {})
1032
file_id_children = getattr(self[file_id], 'children', {})
1118
1033
if len(file_id_children):
1119
1034
children[file_id] = file_id_children
1120
1035
if self.id2path(file_id) != old_path:
1121
1036
raise errors.InconsistentDelta(old_path, file_id,
1122
"Entry was at wrong other path %r." % self.id2path(file_id))
1037
"Entry was at wrong other path %r." % self.id2path(file_id))
1123
1038
# Remove file_id and the unaltered children. If file_id is not
1124
1039
# being deleted it will be reinserted back later.
1125
1040
self.remove_recursive_id(file_id)
1129
1044
# the resulting inventory were also modified, are inserted after their
1131
1046
for new_path, f, new_entry in sorted((np, f, e) for op, np, f, e in
1132
delta if np is not None):
1047
delta if np is not None):
1133
1048
if new_entry.kind == 'directory':
1134
1049
# Pop the child which to allow detection of children whose
1135
1050
# parents were deleted and which were not reattached to a new
1137
1052
replacement = InventoryDirectory(new_entry.file_id,
1138
new_entry.name, new_entry.parent_id)
1053
new_entry.name, new_entry.parent_id)
1139
1054
replacement.revision = new_entry.revision
1140
1055
replacement.children = children.pop(replacement.file_id, {})
1141
1056
new_entry = replacement
1143
1058
self.add(new_entry)
1144
except DuplicateFileId:
1059
except errors.DuplicateFileId:
1145
1060
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1146
"New id is already present in target.")
1061
"New id is already present in target.")
1147
1062
except AttributeError:
1148
1063
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1149
"Parent is not a directory.")
1064
"Parent is not a directory.")
1150
1065
if self.id2path(new_entry.file_id) != new_path:
1151
1066
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1152
"New path is not consistent with parent path.")
1067
"New path is not consistent with parent path.")
1153
1068
if len(children):
1154
1069
# Get the parent id that was deleted
1155
1070
parent_id, children = children.popitem()
1156
1071
raise errors.InconsistentDelta("<deleted>", parent_id,
1157
"The file id was deleted but its children were not deleted.")
1072
"The file id was deleted but its children were not deleted.")
1159
1074
def create_by_apply_delta(self, inventory_delta, new_revision_id,
1160
1075
propagate_caches=False):
1181
1096
other.add(entry.copy())
1184
def iter_all_ids(self):
1185
1100
"""Iterate over all file-ids."""
1186
1101
return iter(self._byid)
1188
1103
def iter_just_entries(self):
1189
1104
"""Iterate over all entries.
1191
1106
Unlike iter_entries(), just the entries are returned (not (path, ie))
1192
1107
and the order of entries is undefined.
1194
1109
XXX: We may not want to merge this into bzr.dev.
1196
1111
if self.root is None:
1198
return self._byid.values()
1113
for _, ie in self._byid.iteritems():
1200
1116
def __len__(self):
1201
1117
"""Returns number of entries."""
1202
1118
return len(self._byid)
1204
def get_entry(self, file_id):
1120
def __getitem__(self, file_id):
1205
1121
"""Return the entry for given file_id.
1207
1123
>>> 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
1124
>>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
1125
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1126
>>> inv['123123'].name
1213
if not isinstance(file_id, bytes):
1214
raise TypeError(file_id)
1216
1130
return self._byid[file_id]
1217
1131
except KeyError:
1283
1196
ie = make_entry(kind, parts[-1], parent_id, file_id)
1284
1197
return self.add(ie)
1286
def delete(self, file_id):
1199
def __delitem__(self, file_id):
1287
1200
"""Remove entry by id.
1289
1202
>>> inv = Inventory()
1290
>>> inv.add(InventoryFile(b'123', 'foo.c', ROOT_ID))
1291
InventoryFile(b'123', 'foo.c', parent_id=b'TREE_ROOT', sha1=None, len=None, revision=None)
1292
>>> inv.has_id(b'123')
1203
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1204
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1205
>>> inv.has_id('123')
1294
>>> inv.delete(b'123')
1295
>>> inv.has_id(b'123')
1208
>>> inv.has_id('123')
1298
ie = self.get_entry(file_id)
1299
1212
del self._byid[file_id]
1300
1213
if ie.parent_id is not None:
1301
del self.get_entry(ie.parent_id).children[ie.name]
1214
del self[ie.parent_id].children[ie.name]
1303
1216
def __eq__(self, other):
1304
1217
"""Compare two sets by comparing their contents.
1307
1220
>>> i2 = Inventory()
1310
>>> i1.add(InventoryFile(b'123', 'foo', ROOT_ID))
1311
InventoryFile(b'123', 'foo', parent_id=b'TREE_ROOT', sha1=None, len=None, revision=None)
1223
>>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
1224
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1314
>>> i2.add(InventoryFile(b'123', 'foo', ROOT_ID))
1315
InventoryFile(b'123', 'foo', parent_id=b'TREE_ROOT', sha1=None, len=None, revision=None)
1227
>>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
1228
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1356
1269
for file_id in deletes:
1357
1270
delta.append((old.id2path(file_id), None, file_id, None))
1358
1271
for file_id in adds:
1359
delta.append((None, self.id2path(file_id),
1360
file_id, self.get_entry(file_id)))
1272
delta.append((None, self.id2path(file_id), file_id, self[file_id]))
1361
1273
for file_id in common:
1362
new_ie = new_getter(file_id)
1363
old_ie = old_getter(file_id)
1274
new_ie = new_getter[file_id]
1275
old_ie = old_getter[file_id]
1364
1276
# If xml_serializer returns the cached InventoryEntries (rather
1365
1277
# than always doing .copy()), inlining the 'is' check saves 2.7M
1366
1278
# calls to __eq__. Under lsprof this saves 20s => 6s.
1406
1318
new_parent = self._byid[new_parent_id]
1407
1319
if new_name in new_parent.children:
1408
1320
raise errors.BzrError("%r already exists in %r" %
1409
(new_name, self.id2path(new_parent_id)))
1321
(new_name, self.id2path(new_parent_id)))
1411
1323
new_parent_idpath = self.get_idpath(new_parent_id)
1412
1324
if file_id in new_parent_idpath:
1413
1325
raise errors.BzrError(
1414
1326
"cannot move directory %r into a subdirectory of itself, %r"
1415
% (self.id2path(file_id), self.id2path(new_parent_id)))
1327
% (self.id2path(file_id), self.id2path(new_parent_id)))
1417
1329
file_ie = self._byid[file_id]
1418
1330
old_parent = self._byid[file_ie.parent_id]
1489
1401
if entry.parent_id is not None:
1490
1402
parent_str = entry.parent_id
1493
1405
name_str = entry.name.encode("utf8")
1494
1406
if entry.kind == 'file':
1495
1407
if entry.executable:
1499
return b"file: %s\n%s\n%s\n%s\n%s\n%d\n%s" % (
1411
return "file: %s\n%s\n%s\n%s\n%s\n%d\n%s" % (
1500
1412
entry.file_id, parent_str, name_str, entry.revision,
1501
1413
entry.text_sha1, entry.text_size, exec_str)
1502
1414
elif entry.kind == 'directory':
1503
return b"dir: %s\n%s\n%s\n%s" % (
1415
return "dir: %s\n%s\n%s\n%s" % (
1504
1416
entry.file_id, parent_str, name_str, entry.revision)
1505
1417
elif entry.kind == 'symlink':
1506
return b"symlink: %s\n%s\n%s\n%s\n%s" % (
1418
return "symlink: %s\n%s\n%s\n%s\n%s" % (
1507
1419
entry.file_id, parent_str, name_str, entry.revision,
1508
1420
entry.symlink_target.encode("utf8"))
1509
1421
elif entry.kind == 'tree-reference':
1510
return b"tree: %s\n%s\n%s\n%s\n%s" % (
1422
return "tree: %s\n%s\n%s\n%s\n%s" % (
1511
1423
entry.file_id, parent_str, name_str, entry.revision,
1512
1424
entry.reference_revision)
1610
1522
# parent_to_children with at least the tree root.)
1612
1524
cache = self._fileid_to_entry_cache
1613
remaining_children = deque(
1614
parent_to_children[self.root_id])
1525
remaining_children = collections.deque(parent_to_children[self.root_id])
1615
1526
while remaining_children:
1616
1527
file_id = remaining_children.popleft()
1617
1528
ie = cache[file_id]
1618
1529
if ie.kind == 'directory':
1619
ie = ie.copy() # We create a copy to depopulate the .children attribute
1530
ie = ie.copy() # We create a copy to depopulate the .children attribute
1620
1531
# TODO: depending on the uses of 'other' we should probably alwyas
1621
1532
# '.copy()' to prevent someone from mutating other and
1622
1533
# invaliding our internal cache
1629
def _bytes_to_utf8name_key(data):
1630
"""Get the file_id, revision_id key out of data."""
1540
def _bytes_to_utf8name_key(bytes):
1541
"""Get the file_id, revision_id key out of bytes."""
1631
1542
# We don't normally care about name, except for times when we want
1632
1543
# 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])
1544
sections = bytes.split('\n')
1545
kind, file_id = sections[0].split(': ')
1546
return (sections[2], intern(file_id), intern(sections[3]))
1637
1548
def _bytes_to_entry(self, bytes):
1638
1549
"""Deserialise a serialised entry."""
1639
sections = bytes.split(b'\n')
1640
if sections[0].startswith(b"file: "):
1550
sections = bytes.split('\n')
1551
if sections[0].startswith("file: "):
1641
1552
result = InventoryFile(sections[0][6:],
1642
sections[2].decode('utf8'),
1553
sections[2].decode('utf8'),
1644
1555
result.text_sha1 = sections[4]
1645
1556
result.text_size = int(sections[5])
1646
result.executable = sections[6] == b"Y"
1647
elif sections[0].startswith(b"dir: "):
1557
result.executable = sections[6] == "Y"
1558
elif sections[0].startswith("dir: "):
1648
1559
result = CHKInventoryDirectory(sections[0][5:],
1649
sections[2].decode('utf8'),
1651
elif sections[0].startswith(b"symlink: "):
1560
sections[2].decode('utf8'),
1562
elif sections[0].startswith("symlink: "):
1652
1563
result = InventoryLink(sections[0][9:],
1653
sections[2].decode('utf8'),
1564
sections[2].decode('utf8'),
1655
1566
result.symlink_target = sections[4].decode('utf8')
1656
elif sections[0].startswith(b"tree: "):
1567
elif sections[0].startswith("tree: "):
1657
1568
result = TreeReference(sections[0][6:],
1658
sections[2].decode('utf8'),
1569
sections[2].decode('utf8'),
1660
1571
result.reference_revision = sections[4]
1662
1573
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'':
1574
result.file_id = intern(result.file_id)
1575
result.revision = intern(sections[3])
1576
if result.parent_id == '':
1666
1577
result.parent_id = None
1667
1578
self._fileid_to_entry_cache[result.file_id] = result
1670
1581
def create_by_apply_delta(self, inventory_delta, new_revision_id,
1671
propagate_caches=False):
1582
propagate_caches=False):
1672
1583
"""Create a new CHKInventory by applying inventory_delta to this one.
1674
1585
See the inventory developers documentation for the theory behind
1826
1736
parents.discard(('', None))
1827
1737
for parent_path, parent in parents:
1829
if result.get_entry(parent).kind != 'directory':
1739
if result[parent].kind != 'directory':
1830
1740
raise errors.InconsistentDelta(result.id2path(parent), parent,
1831
'Not a directory, but given children')
1741
'Not a directory, but given children')
1832
1742
except errors.NoSuchId:
1833
1743
raise errors.InconsistentDelta("<unknown>", parent,
1834
"Parent is not present in resulting inventory.")
1744
"Parent is not present in resulting inventory.")
1835
1745
if result.path2id(parent_path) != parent:
1836
1746
raise errors.InconsistentDelta(parent_path, parent,
1837
"Parent has wrong path %r." % result.path2id(parent_path))
1747
"Parent has wrong path %r." % result.path2id(parent_path))
1841
def deserialise(klass, chk_store, lines, expected_revision_id):
1751
def deserialise(klass, chk_store, bytes, expected_revision_id):
1842
1752
"""Deserialise a CHKInventory.
1844
1754
:param chk_store: A CHK capable VersionedFiles instance.
1848
1758
: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':
1760
lines = bytes.split('\n')
1762
raise AssertionError('bytes to deserialize must end with an eol')
1764
if lines[0] != 'chkinventory:':
1853
1765
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'))
1767
allowed_keys = frozenset(['root_id', 'revision_id', 'search_key_name',
1768
'parent_id_basename_to_file_id',
1858
1770
for line in lines[1:]:
1859
key, value = line.rstrip(b'\n').split(b': ', 1)
1771
key, value = line.split(': ', 1)
1860
1772
if key not in allowed_keys:
1861
1773
raise errors.BzrError('Unknown key in inventory: %r\n%r'
1862
1774
% (key, bytes))
1864
1776
raise errors.BzrError('Duplicate key in inventory: %r\n%r'
1865
1777
% (key, bytes))
1866
1778
info[key] = value
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:'):
1779
revision_id = intern(info['revision_id'])
1780
root_id = intern(info['root_id'])
1781
search_key_name = intern(info.get('search_key_name', 'plain'))
1782
parent_id_basename_to_file_id = intern(info.get(
1783
'parent_id_basename_to_file_id', None))
1784
if not parent_id_basename_to_file_id.startswith('sha1:'):
1873
1785
raise ValueError('parent_id_basename_to_file_id should be a sha1'
1874
1786
' 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:'):
1787
id_to_entry = info['id_to_entry']
1788
if not id_to_entry.startswith('sha1:'):
1877
1789
raise ValueError('id_to_entry should be a sha1'
1878
1790
' key not %r' % (id_to_entry,))
1894
1806
search_key_func=search_key_func)
1895
1807
if (result.revision_id,) != expected_revision_id:
1896
1808
raise ValueError("Mismatched revision id and expected: %r, %r" %
1897
(result.revision_id, expected_revision_id))
1809
(result.revision_id, expected_revision_id))
1901
def from_inventory(klass, chk_store, inventory, maximum_size=0, search_key_name=b'plain'):
1813
def from_inventory(klass, chk_store, inventory, maximum_size=0, search_key_name='plain'):
1902
1814
"""Create a CHKInventory from an existing inventory.
1904
1816
The content of inventory is copied into the chk_store, and a
1924
1836
parent_id_basename_dict[p_id_key] = entry.file_id
1926
1838
result._populate_from_dicts(chk_store, id_to_entry_dict,
1927
parent_id_basename_dict, maximum_size=maximum_size)
1839
parent_id_basename_dict, maximum_size=maximum_size)
1930
1842
def _populate_from_dicts(self, chk_store, id_to_entry_dict,
1931
1843
parent_id_basename_dict, maximum_size):
1932
search_key_func = chk_map.search_key_registry.get(
1933
self._search_key_name)
1844
search_key_func = chk_map.search_key_registry.get(self._search_key_name)
1934
1845
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)
1846
maximum_size=maximum_size, key_width=1,
1847
search_key_func=search_key_func)
1937
1848
self.id_to_entry = chk_map.CHKMap(chk_store, root_key,
1938
1849
search_key_func)
1939
1850
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)
1851
parent_id_basename_dict,
1852
maximum_size=maximum_size, key_width=2,
1853
search_key_func=search_key_func)
1943
1854
self.parent_id_basename_to_file_id = chk_map.CHKMap(chk_store,
1944
root_key, search_key_func)
1855
root_key, search_key_func)
1946
1857
def _parent_id_basename_key(self, entry):
1947
1858
"""Create a key for a entry in a parent_id_basename_to_file_id index."""
1948
1859
if entry.parent_id is not None:
1949
1860
parent_id = entry.parent_id
1952
1863
return StaticTuple(parent_id, entry.name.encode('utf8')).intern()
1954
def get_entry(self, file_id):
1865
def __getitem__(self, file_id):
1955
1866
"""map a single file_id -> InventoryEntry."""
1956
1867
if file_id is None:
1957
1868
raise errors.NoSuchId(self, file_id)
1962
1873
return self._bytes_to_entry(
1963
next(self.id_to_entry.iteritems([StaticTuple(file_id,)]))[1])
1874
self.id_to_entry.iteritems([StaticTuple(file_id,)]).next()[1])
1964
1875
except StopIteration:
1965
1876
# really we're passing an inventory, not a tree...
1966
1877
raise errors.NoSuchId(self, file_id)
1968
1879
def _getitems(self, file_ids):
1969
"""Similar to get_entry, but lets you query for multiple.
1880
"""Similar to __getitem__, but lets you query for multiple.
1971
1882
The returned order is undefined. And currently if an item doesn't
1972
1883
exist, it isn't included in the output.
2043
1954
last_parent_id = last_parent_ie = None
2044
1955
pid_items = self.parent_id_basename_to_file_id.iteritems()
2045
1956
for key, child_file_id in pid_items:
2046
if key == (b'', b''): # This is the root
1957
if key == ('', ''): # This is the root
2047
1958
if child_file_id != self.root_id:
2048
1959
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))
1960
' We expected data with key ("","") to match'
1961
' the root id, but %s != %s'
1962
% (child_file_id, self.root_id))
2053
1964
parent_id, basename = key
2054
1965
ie = cache[child_file_id]
2068
1979
existing_ie = parent_ie._children[basename]
2069
1980
if existing_ie != ie:
2070
1981
raise ValueError('Data inconsistency detected.'
2071
' Two entries with basename %r were found'
2072
' in the parent entry {%s}'
2073
% (basename, parent_id))
1982
' Two entries with basename %r were found'
1983
' in the parent entry {%s}'
1984
% (basename, parent_id))
2074
1985
if basename != ie.name:
2075
1986
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))
1987
' In the parent_id_basename_to_file_id map, file_id'
1988
' {%s} is listed as having basename %r, but in the'
1989
' id_to_entry map it is %r'
1990
% (child_file_id, basename, ie.name))
2080
1991
parent_ie._children[basename] = ie
2081
1992
self._fully_cached = True
2130
2041
if kind[0] != kind[1]:
2131
2042
changed_content = True
2132
2043
elif kind[0] == 'file':
2133
if (self_entry.text_size != basis_entry.text_size
2134
or self_entry.text_sha1 != basis_entry.text_sha1):
2044
if (self_entry.text_size != basis_entry.text_size or
2045
self_entry.text_sha1 != basis_entry.text_sha1):
2135
2046
changed_content = True
2136
2047
elif kind[0] == 'symlink':
2137
2048
if self_entry.symlink_target != basis_entry.symlink_target:
2138
2049
changed_content = True
2139
2050
elif kind[0] == 'tree-reference':
2140
if (self_entry.reference_revision
2141
!= basis_entry.reference_revision):
2051
if (self_entry.reference_revision !=
2052
basis_entry.reference_revision):
2142
2053
changed_content = True
2143
2054
parent = (basis_parent, self_parent)
2144
2055
name = (basis_name, self_name)
2145
2056
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]):
2057
if (not changed_content
2058
and parent[0] == parent[1]
2059
and name[0] == name[1]
2060
and executable[0] == executable[1]):
2150
2061
# Could happen when only the revision changed for a directory
2151
2062
# for instance.
2154
file_id, (path_in_source, path_in_target), changed_content,
2064
yield (file_id, (path_in_source, path_in_target), changed_content,
2155
2065
versioned, parent, name, kind, executable)
2157
2067
def __len__(self):
2223
2133
def to_lines(self):
2224
2134
"""Serialise the inventory to lines."""
2225
lines = [b"chkinventory:\n"]
2226
if self._search_key_name != b'plain':
2135
lines = ["chkinventory:\n"]
2136
if self._search_key_name != 'plain':
2227
2137
# 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],))
2138
lines.append('search_key_name: %s\n' % (self._search_key_name,))
2139
lines.append("root_id: %s\n" % self.root_id)
2140
lines.append('parent_id_basename_to_file_id: %s\n' %
2141
(self.parent_id_basename_to_file_id.key()[0],))
2142
lines.append("revision_id: %s\n" % self.revision_id)
2143
lines.append("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)
2145
lines.append("revision_id: %s\n" % self.revision_id)
2146
lines.append("root_id: %s\n" % self.root_id)
2238
2147
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],))
2148
lines.append('parent_id_basename_to_file_id: %s\n' %
2149
(self.parent_id_basename_to_file_id.key()[0],))
2150
lines.append("id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
2245
2154
def root(self):
2246
2155
"""Get the root entry."""
2247
return self.get_entry(self.root_id)
2156
return self[self.root_id]
2250
2159
class CHKInventoryDirectory(InventoryDirectory):
2274
2183
# No longer supported
2275
2184
if self._chk_inventory.parent_id_basename_to_file_id is None:
2276
2185
raise AssertionError("Inventories without"
2277
" parent_id_basename_to_file_id are no longer supported")
2186
" parent_id_basename_to_file_id are no longer supported")
2279
2188
# XXX: Todo - use proxy objects for the children rather than loading
2280
2189
# all when the attribute is referenced.
2281
2190
parent_id_index = self._chk_inventory.parent_id_basename_to_file_id
2282
2191
child_keys = set()
2283
2192
for (parent_id, name_utf8), file_id in parent_id_index.iteritems(
2284
key_filter=[StaticTuple(self.file_id,)]):
2193
key_filter=[StaticTuple(self.file_id,)]):
2285
2194
child_keys.add(StaticTuple(file_id,))
2287
2196
for file_id_key in child_keys: