128
132
RENAMED = 'renamed'
129
133
MODIFIED_AND_RENAMED = 'modified and renamed'
131
__slots__ = ['file_id', 'revision', 'parent_id', 'name']
133
# Attributes that all InventoryEntry instances are expected to have, but
134
# that don't vary for all kinds of entry. (e.g. symlink_target is only
135
# relevant to InventoryLink, so there's no reason to make every
136
# InventoryFile instance allocate space to hold a value for it.)
137
# Attributes that only vary for files: executable, text_sha1, text_size,
143
# Attributes that only vary for symlinks: symlink_target
144
symlink_target = None
145
# Attributes that only vary for tree-references: reference_revision
146
reference_revision = None
149
137
def detect_changes(self, old_entry):
150
138
"""Return a (text_modified, meta_modified) from this to old_entry.
229
233
known_kinds = ('file', 'directory', 'symlink')
235
def _put_in_tar(self, item, tree):
236
"""populate item for stashing in a tar, and return the content stream.
238
If no content is available, return None.
240
raise BzrError("don't know how to export {%s} of kind %r" %
241
(self.file_id, self.kind))
243
@deprecated_method(deprecated_in((1, 6, 0)))
244
def put_on_disk(self, dest, dp, tree):
245
"""Create a representation of self on disk in the prefix dest.
247
This is a template method - implement _put_on_disk in subclasses.
249
fullpath = osutils.pathjoin(dest, dp)
250
self._put_on_disk(fullpath, tree)
251
# mutter(" export {%s} kind %s to %s", self.file_id,
252
# self.kind, fullpath)
254
def _put_on_disk(self, fullpath, tree):
255
"""Put this entry onto disk at fullpath, from tree tree."""
256
raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
231
258
def sorted_children(self):
232
259
return sorted(self.children.items())
247
274
:param rev_id: Revision id from which this InventoryEntry was loaded.
248
275
Not necessarily the last-changed revision for this file.
249
276
:param inv: Inventory from which the entry was loaded.
277
:param tree: RevisionTree for this entry.
251
279
if self.parent_id is not None:
252
280
if not inv.has_id(self.parent_id):
253
281
raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
254
282
% (self.parent_id, rev_id))
255
checker._add_entry_to_text_key_references(inv, self)
256
self._check(checker, rev_id)
283
self._check(checker, rev_id, tree)
258
def _check(self, checker, rev_id):
285
def _check(self, checker, rev_id, tree):
259
286
"""Check this inventory entry for kind specific errors."""
260
checker._report_items.append(
261
'unknown entry kind %r in revision {%s}' % (self.kind, rev_id))
287
raise BzrCheckError('unknown entry kind %r in revision {%s}' %
264
291
"""Clone this inventory entry."""
401
class RootEntry(InventoryEntry):
403
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
404
'text_id', 'parent_id', 'children', 'executable',
405
'revision', 'symlink_target', 'reference_revision']
407
def _check(self, checker, rev_id, tree):
408
"""See InventoryEntry._check"""
410
def __init__(self, file_id):
411
self.file_id = file_id
413
self.kind = 'directory'
414
self.parent_id = None
417
symbol_versioning.warn('RootEntry is deprecated as of bzr 0.10.'
418
' Please use InventoryDirectory instead.',
419
DeprecationWarning, stacklevel=2)
421
def __eq__(self, other):
422
if not isinstance(other, RootEntry):
423
return NotImplemented
425
return (self.file_id == other.file_id) \
426
and (self.children == other.children)
374
429
class InventoryDirectory(InventoryEntry):
375
430
"""A directory in an inventory."""
377
__slots__ = ['children']
381
def _check(self, checker, rev_id):
432
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
433
'text_id', 'parent_id', 'children', 'executable',
434
'revision', 'symlink_target', 'reference_revision']
436
def _check(self, checker, rev_id, tree):
382
437
"""See InventoryEntry._check"""
383
# In non rich root repositories we do not expect a file graph for the
385
if self.name == '' and not checker.rich_roots:
387
# Directories are stored as an empty file, but the file should exist
388
# to provide a per-fileid log. The hash of every directory content is
389
# "da..." below (the sha1sum of '').
390
checker.add_pending_item(rev_id,
391
('texts', self.file_id, self.revision), 'text',
392
'da39a3ee5e6b4b0d3255bfef95601890afd80709')
438
if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
439
raise BzrCheckError('directory {%s} has text in revision {%s}'
440
% (self.file_id, rev_id))
395
443
other = InventoryDirectory(self.file_id, self.name, self.parent_id)
401
449
def __init__(self, file_id, name, parent_id):
402
450
super(InventoryDirectory, self).__init__(file_id, name, parent_id)
403
451
self.children = {}
452
self.kind = 'directory'
405
454
def kind_character(self):
406
455
"""See InventoryEntry.kind_character."""
458
def _put_in_tar(self, item, tree):
459
"""See InventoryEntry._put_in_tar."""
460
item.type = tarfile.DIRTYPE
467
def _put_on_disk(self, fullpath, tree):
468
"""See InventoryEntry._put_on_disk."""
410
472
class InventoryFile(InventoryEntry):
411
473
"""A file in an inventory."""
413
__slots__ = ['text_sha1', 'text_size', 'text_id', 'executable']
417
def __init__(self, file_id, name, parent_id):
418
super(InventoryFile, self).__init__(file_id, name, parent_id)
419
self.text_sha1 = None
420
self.text_size = None
422
self.executable = False
424
def _check(self, checker, tree_revision_id):
475
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
476
'text_id', 'parent_id', 'children', 'executable',
477
'revision', 'symlink_target', 'reference_revision']
479
def _check(self, checker, tree_revision_id, tree):
425
480
"""See InventoryEntry._check"""
426
# TODO: check size too.
427
checker.add_pending_item(tree_revision_id,
428
('texts', self.file_id, self.revision), 'text',
430
if self.text_size is None:
431
checker._report_items.append(
432
'fileid {%s} in {%s} has None for text_size' % (self.file_id,
481
key = (self.file_id, self.revision)
482
if key in checker.checked_texts:
483
prev_sha = checker.checked_texts[key]
484
if prev_sha != self.text_sha1:
486
'mismatched sha1 on {%s} in {%s} (%s != %s) %r' %
487
(self.file_id, tree_revision_id, prev_sha, self.text_sha1,
490
checker.repeated_text_cnt += 1
493
checker.checked_text_cnt += 1
494
# We can't check the length, because Weave doesn't store that
495
# information, and the whole point of looking at the weave's
496
# sha1sum is that we don't have to extract the text.
497
if (self.text_sha1 != tree._repository.texts.get_sha1s([key])[key]):
498
raise BzrCheckError('text {%s} version {%s} wrong sha1' % key)
499
checker.checked_texts[key] = self.text_sha1
436
502
other = InventoryFile(self.file_id, self.name, self.parent_id)
468
534
"""See InventoryEntry.has_text."""
537
def __init__(self, file_id, name, parent_id):
538
super(InventoryFile, self).__init__(file_id, name, parent_id)
471
541
def kind_character(self):
472
542
"""See InventoryEntry.kind_character."""
545
def _put_in_tar(self, item, tree):
546
"""See InventoryEntry._put_in_tar."""
547
item.type = tarfile.REGTYPE
548
fileobj = tree.get_file(self.file_id)
549
item.size = self.text_size
550
if tree.is_executable(self.file_id):
556
def _put_on_disk(self, fullpath, tree):
557
"""See InventoryEntry._put_on_disk."""
558
osutils.pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
559
if tree.is_executable(self.file_id):
560
os.chmod(fullpath, 0755)
475
562
def _read_tree_state(self, path, work_tree):
476
563
"""See InventoryEntry._read_tree_state."""
477
564
self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
509
596
class InventoryLink(InventoryEntry):
510
597
"""A file in an inventory."""
512
__slots__ = ['symlink_target']
516
def __init__(self, file_id, name, parent_id):
517
super(InventoryLink, self).__init__(file_id, name, parent_id)
518
self.symlink_target = None
520
def _check(self, checker, tree_revision_id):
599
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
600
'text_id', 'parent_id', 'children', 'executable',
601
'revision', 'symlink_target', 'reference_revision']
603
def _check(self, checker, rev_id, tree):
521
604
"""See InventoryEntry._check"""
605
if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
606
raise BzrCheckError('symlink {%s} has text in revision {%s}'
607
% (self.file_id, rev_id))
522
608
if self.symlink_target is None:
523
checker._report_items.append(
524
'symlink {%s} has no target in revision {%s}'
525
% (self.file_id, tree_revision_id))
526
# Symlinks are stored as ''
527
checker.add_pending_item(tree_revision_id,
528
('texts', self.file_id, self.revision), 'text',
529
'da39a3ee5e6b4b0d3255bfef95601890afd80709')
609
raise BzrCheckError('symlink {%s} has no target in revision {%s}'
610
% (self.file_id, rev_id))
532
613
other = InventoryLink(self.file_id, self.name, self.parent_id)
562
643
differ = DiffSymlink(old_tree, new_tree, output_to)
563
644
return differ.diff_symlink(old_target, new_target)
646
def __init__(self, file_id, name, parent_id):
647
super(InventoryLink, self).__init__(file_id, name, parent_id)
648
self.kind = 'symlink'
565
650
def kind_character(self):
566
651
"""See InventoryEntry.kind_character."""
654
def _put_in_tar(self, item, tree):
655
"""See InventoryEntry._put_in_tar."""
656
item.type = tarfile.SYMTYPE
660
item.linkname = self.symlink_target
663
def _put_on_disk(self, fullpath, tree):
664
"""See InventoryEntry._put_on_disk."""
666
os.symlink(self.symlink_target, fullpath)
668
raise BzrError("Failed to create symlink %r -> %r, error: %s" % (fullpath, self.symlink_target, e))
569
670
def _read_tree_state(self, path, work_tree):
570
671
"""See InventoryEntry._read_tree_state."""
571
672
self.symlink_target = work_tree.get_symlink_target(self.file_id)
617
716
class CommonInventory(object):
618
"""Basic inventory logic, defined in terms of primitives like has_id.
620
An inventory is the metadata about the contents of a tree.
622
This is broadly a map from file_id to entries such as directories, files,
623
symlinks and tree references. Each entry maintains its own metadata like
624
SHA1 and length for files, or children for a directory.
626
Entries can be looked up either by path or by file_id.
628
InventoryEntry objects must not be modified after they are
629
inserted, other than through the Inventory API.
717
"""Basic inventory logic, defined in terms of primitives like has_id."""
632
719
def __contains__(self, file_id):
633
720
"""True if this entry contains a file with given id.
657
741
>>> e = i.add(InventoryFile('foo-id', 'foo.c', parent_id='src-id'))
658
742
>>> print i.id2path('foo-id')
661
:raises NoSuchId: If file_id is not present in the inventory.
663
745
# get all names, skipping root
664
746
return '/'.join(reversed(
665
747
[parent.name for parent in
666
748
self._iter_file_id_parents(file_id)][:-1]))
668
def iter_entries(self, from_dir=None, recursive=True):
669
"""Return (path, entry) pairs, in order by name.
671
:param from_dir: if None, start from the root,
672
otherwise start from this directory (either file-id or entry)
673
:param recursive: recurse into directories or not
750
def iter_entries(self, from_dir=None):
751
"""Return (path, entry) pairs, in order by name."""
675
752
if from_dir is None:
676
753
if self.root is None:
855
928
descend(self.root, u'')
858
def path2id(self, relpath):
931
def path2id(self, name):
859
932
"""Walk down through directories to return entry of last component.
861
:param relpath: may be either a list of path components, or a single
862
string, in which case it is automatically split.
934
names may be either a list of path components, or a single
935
string, in which case it is automatically split.
864
937
This returns the entry of the last component in the path,
865
938
which may be either a file or a directory.
867
940
Returns None IFF the path is not found.
869
if isinstance(relpath, basestring):
870
names = osutils.splitpath(relpath)
942
if isinstance(name, basestring):
943
name = osutils.splitpath(name)
945
# mutter("lookup path %r" % name)
875
948
parent = self.root
941
1014
class Inventory(CommonInventory):
942
"""Mutable dict based in-memory inventory.
944
We never store the full path to a file, because renaming a directory
945
implicitly moves all of its contents. This class internally maintains a
1015
"""Inventory of versioned files in a tree.
1017
This describes which file_id is present at each point in the tree,
1018
and possibly the SHA-1 or other information about the file.
1019
Entries can be looked up either by path or by file_id.
1021
The inventory represents a typical unix file tree, with
1022
directories containing files and subdirectories. We never store
1023
the full path to a file, because renaming a directory implicitly
1024
moves all of its contents. This class internally maintains a
946
1025
lookup tree that allows the children under a directory to be
947
1026
returned quickly.
1028
InventoryEntry objects must not be modified after they are
1029
inserted, other than through the Inventory API.
949
1031
>>> inv = Inventory()
950
1032
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
951
1033
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
952
1034
>>> inv['123-123'].name
955
Id's may be looked up from paths:
1037
May be treated as an iterator or set to look up file ids:
957
>>> inv.path2id('hello.c')
1039
>>> bool(inv.path2id('hello.c'))
959
1041
>>> '123-123' in inv
962
There are iterators over the contents:
1044
May also look up by name:
964
>>> [entry[0] for entry in inv.iter_entries()]
1046
>>> [x[0] for x in inv.iter_entries()]
965
1047
['', u'hello.c']
1048
>>> inv = Inventory('TREE_ROOT-12345678-12345678')
1049
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
1050
Traceback (most recent call last):
1051
BzrError: parent_id {TREE_ROOT} not in inventory
1052
>>> inv.add(InventoryFile('123-123', 'hello.c', 'TREE_ROOT-12345678-12345678'))
1053
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None, revision=None)
968
1055
def __init__(self, root_id=ROOT_ID, revision_id=None):
969
1056
"""Create or read an inventory.
1051
1131
# modified children remaining by the time we examine it.
1052
1132
for old_path, file_id in sorted(((op, f) for op, np, f, e in delta
1053
1133
if op is not None), reverse=True):
1134
if file_id not in self:
1054
1137
# Preserve unaltered children of file_id for later reinsertion.
1055
1138
file_id_children = getattr(self[file_id], 'children', {})
1056
1139
if len(file_id_children):
1057
1140
children[file_id] = file_id_children
1058
if self.id2path(file_id) != old_path:
1059
raise errors.InconsistentDelta(old_path, file_id,
1060
"Entry was at wrong other path %r." % self.id2path(file_id))
1061
1141
# Remove file_id and the unaltered children. If file_id is not
1062
1142
# being deleted it will be reinserted back later.
1063
1143
self.remove_recursive_id(file_id)
1077
1157
replacement.revision = new_entry.revision
1078
1158
replacement.children = children.pop(replacement.file_id, {})
1079
1159
new_entry = replacement
1082
except errors.DuplicateFileId:
1083
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1084
"New id is already present in target.")
1085
except AttributeError:
1086
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1087
"Parent is not a directory.")
1088
if self.id2path(new_entry.file_id) != new_path:
1089
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1090
"New path is not consistent with parent path.")
1091
1161
if len(children):
1092
1162
# Get the parent id that was deleted
1093
1163
parent_id, children = children.popitem()
1094
1164
raise errors.InconsistentDelta("<deleted>", parent_id,
1095
1165
"The file id was deleted but its children were not deleted.")
1097
def create_by_apply_delta(self, inventory_delta, new_revision_id,
1098
propagate_caches=False):
1099
"""See CHKInventory.create_by_apply_delta()"""
1100
new_inv = self.copy()
1101
new_inv.apply_delta(inventory_delta)
1102
new_inv.revision_id = new_revision_id
1105
1167
def _set_root(self, ie):
1107
1169
self._byid = {self.root.file_id: self.root}
1178
1240
def add(self, entry):
1179
1241
"""Add entry to inventory.
1243
To add a file to a branch ready to be committed, use Branch.add,
1246
Returns the new entry object.
1183
1248
if entry.file_id in self._byid:
1184
1249
raise errors.DuplicateFileId(entry.file_id,
1185
1250
self._byid[entry.file_id])
1186
1252
if entry.parent_id is None:
1187
1253
self.root = entry
1190
1256
parent = self._byid[entry.parent_id]
1191
1257
except KeyError:
1192
raise errors.InconsistentDelta("<unknown>", entry.parent_id,
1193
"Parent not in inventory.")
1258
raise BzrError("parent_id {%s} not in inventory" %
1194
1261
if entry.name in parent.children:
1195
raise errors.InconsistentDelta(
1196
self.id2path(parent.children[entry.name].file_id),
1198
"Path already versioned")
1262
raise BzrError("%s is already versioned" %
1263
osutils.pathjoin(self.id2path(parent.file_id),
1264
entry.name).encode('utf-8'))
1199
1265
parent.children[entry.name] = entry
1200
1266
return self._add_child(entry)
1391
1460
self._fileid_to_entry_cache = {}
1392
1461
self._path_to_fileid_cache = {}
1393
1462
self._search_key_name = search_key_name
1396
def __eq__(self, other):
1397
"""Compare two sets by comparing their contents."""
1398
if not isinstance(other, CHKInventory):
1399
return NotImplemented
1401
this_key = self.id_to_entry.key()
1402
other_key = other.id_to_entry.key()
1403
this_pid_key = self.parent_id_basename_to_file_id.key()
1404
other_pid_key = other.parent_id_basename_to_file_id.key()
1405
if None in (this_key, this_pid_key, other_key, other_pid_key):
1407
return this_key == other_key and this_pid_key == other_pid_key
1409
1464
def _entry_to_bytes(self, entry):
1410
1465
"""Serialise entry as a single bytestring.
1449
1504
raise ValueError("unknown kind %r" % entry.kind)
1451
def _expand_fileids_to_parents_and_children(self, file_ids):
1452
"""Give a more wholistic view starting with the given file_ids.
1454
For any file_id which maps to a directory, we will include all children
1455
of that directory. We will also include all directories which are
1456
parents of the given file_ids, but we will not include their children.
1463
fringle # fringle-id
1467
if given [foo-id] we will include
1468
TREE_ROOT as interesting parents
1470
foo-id, baz-id, frob-id, fringle-id
1474
# TODO: Pre-pass over the list of fileids to see if anything is already
1475
# deserialized in self._fileid_to_entry_cache
1477
directories_to_expand = set()
1478
children_of_parent_id = {}
1479
# It is okay if some of the fileids are missing
1480
for entry in self._getitems(file_ids):
1481
if entry.kind == 'directory':
1482
directories_to_expand.add(entry.file_id)
1483
interesting.add(entry.parent_id)
1484
children_of_parent_id.setdefault(entry.parent_id, []
1485
).append(entry.file_id)
1487
# Now, interesting has all of the direct parents, but not the
1488
# parents of those parents. It also may have some duplicates with
1490
remaining_parents = interesting.difference(file_ids)
1491
# When we hit the TREE_ROOT, we'll get an interesting parent of None,
1492
# but we don't actually want to recurse into that
1493
interesting.add(None) # this will auto-filter it in the loop
1494
remaining_parents.discard(None)
1495
while remaining_parents:
1496
next_parents = set()
1497
for entry in self._getitems(remaining_parents):
1498
next_parents.add(entry.parent_id)
1499
children_of_parent_id.setdefault(entry.parent_id, []
1500
).append(entry.file_id)
1501
# Remove any search tips we've already processed
1502
remaining_parents = next_parents.difference(interesting)
1503
interesting.update(remaining_parents)
1504
# We should probably also .difference(directories_to_expand)
1505
interesting.update(file_ids)
1506
interesting.discard(None)
1507
while directories_to_expand:
1508
# Expand directories by looking in the
1509
# parent_id_basename_to_file_id map
1510
keys = [StaticTuple(f,).intern() for f in directories_to_expand]
1511
directories_to_expand = set()
1512
items = self.parent_id_basename_to_file_id.iteritems(keys)
1513
next_file_ids = set([item[1] for item in items])
1514
next_file_ids = next_file_ids.difference(interesting)
1515
interesting.update(next_file_ids)
1516
for entry in self._getitems(next_file_ids):
1517
if entry.kind == 'directory':
1518
directories_to_expand.add(entry.file_id)
1519
children_of_parent_id.setdefault(entry.parent_id, []
1520
).append(entry.file_id)
1521
return interesting, children_of_parent_id
1523
def filter(self, specific_fileids):
1524
"""Get an inventory view filtered against a set of file-ids.
1526
Children of directories and parents are included.
1528
The result may or may not reference the underlying inventory
1529
so it should be treated as immutable.
1532
parent_to_children) = self._expand_fileids_to_parents_and_children(
1534
# There is some overlap here, but we assume that all interesting items
1535
# are in the _fileid_to_entry_cache because we had to read them to
1536
# determine if they were a dir we wanted to recurse, or just a file
1537
# This should give us all the entries we'll want to add, so start
1539
other = Inventory(self.root_id)
1540
other.root.revision = self.root.revision
1541
other.revision_id = self.revision_id
1542
if not interesting or not parent_to_children:
1543
# empty filter, or filtering entrys that don't exist
1544
# (if even 1 existed, then we would have populated
1545
# parent_to_children with at least the tree root.)
1547
cache = self._fileid_to_entry_cache
1548
remaining_children = collections.deque(parent_to_children[self.root_id])
1549
while remaining_children:
1550
file_id = remaining_children.popleft()
1552
if ie.kind == 'directory':
1553
ie = ie.copy() # We create a copy to depopulate the .children attribute
1554
# TODO: depending on the uses of 'other' we should probably alwyas
1555
# '.copy()' to prevent someone from mutating other and
1556
# invaliding our internal cache
1558
if file_id in parent_to_children:
1559
remaining_children.extend(parent_to_children[file_id])
1563
1507
def _bytes_to_utf8name_key(bytes):
1564
1508
"""Get the file_id, revision_id key out of bytes."""
1604
1547
def _get_mutable_inventory(self):
1605
1548
"""See CommonInventory._get_mutable_inventory."""
1606
1549
entries = self.iter_entries()
1607
inv = Inventory(None, self.revision_id)
1550
if self.root_id is not None:
1552
inv = Inventory(self.root_id, self.revision_id)
1608
1553
for path, inv_entry in entries:
1609
inv.add(inv_entry.copy())
1612
1557
def create_by_apply_delta(self, inventory_delta, new_revision_id,
1613
1558
propagate_caches=False):
1614
1559
"""Create a new CHKInventory by applying inventory_delta to this one.
1616
See the inventory developers documentation for the theory behind
1619
1561
:param inventory_delta: The inventory delta to apply. See
1620
1562
Inventory.apply_delta for details.
1621
1563
:param new_revision_id: The revision id of the resulting CHKInventory.
1638
1579
search_key_func=search_key_func)
1639
1580
result.id_to_entry._ensure_root()
1640
1581
result.id_to_entry._root_node.set_maximum_size(maximum_size)
1641
# Change to apply to the parent_id_basename delta. The dict maps
1642
# (parent_id, basename) -> (old_key, new_value). We use a dict because
1643
# when a path has its id replaced (e.g. the root is changed, or someone
1644
# does bzr mv a b, bzr mv c a, we should output a single change to this
1645
# map rather than two.
1646
parent_id_basename_delta = {}
1582
parent_id_basename_delta = []
1647
1583
if self.parent_id_basename_to_file_id is not None:
1648
1584
result.parent_id_basename_to_file_id = chk_map.CHKMap(
1649
1585
self.parent_id_basename_to_file_id._store,
1659
1595
result.parent_id_basename_to_file_id = None
1660
1596
result.root_id = self.root_id
1661
1597
id_to_entry_delta = []
1662
# inventory_delta is only traversed once, so we just update the
1664
# Check for repeated file ids
1665
inventory_delta = _check_delta_unique_ids(inventory_delta)
1666
# Repeated old paths
1667
inventory_delta = _check_delta_unique_old_paths(inventory_delta)
1668
# Check for repeated new paths
1669
inventory_delta = _check_delta_unique_new_paths(inventory_delta)
1670
# Check for entries that don't match the fileid
1671
inventory_delta = _check_delta_ids_match_entry(inventory_delta)
1672
# Check for nonsense fileids
1673
inventory_delta = _check_delta_ids_are_valid(inventory_delta)
1674
# Check for new_path <-> entry consistency
1675
inventory_delta = _check_delta_new_path_entry_both_or_None(
1677
# All changed entries need to have their parents be directories and be
1678
# at the right path. This set contains (path, id) tuples.
1680
# When we delete an item, all the children of it must be either deleted
1681
# or altered in their own right. As we batch process the change via
1682
# CHKMap.apply_delta, we build a set of things to use to validate the
1686
1598
for old_path, new_path, file_id, entry in inventory_delta:
1687
1599
# file id changes
1688
1600
if new_path == '':
1697
1609
del result._path_to_fileid_cache[old_path]
1698
1610
except KeyError:
1700
deletes.add(file_id)
1702
new_key = StaticTuple(file_id,)
1613
new_key = (file_id,)
1703
1614
new_value = result._entry_to_bytes(entry)
1704
1615
# Update caches. It's worth doing this whether
1705
1616
# we're propagating the old caches or not.
1706
1617
result._path_to_fileid_cache[new_path] = file_id
1707
parents.add((split(new_path)[0], entry.parent_id))
1708
1618
if old_path is None:
1711
old_key = StaticTuple(file_id,)
1712
if self.id2path(file_id) != old_path:
1713
raise errors.InconsistentDelta(old_path, file_id,
1714
"Entry was at wrong other path %r." %
1715
self.id2path(file_id))
1716
altered.add(file_id)
1717
id_to_entry_delta.append(StaticTuple(old_key, new_key, new_value))
1621
old_key = (file_id,)
1622
id_to_entry_delta.append((old_key, new_key, new_value))
1718
1623
if result.parent_id_basename_to_file_id is not None:
1719
1624
# parent_id, basename changes
1720
1625
if old_path is None:
1729
1634
new_key = self._parent_id_basename_key(entry)
1730
1635
new_value = file_id
1731
# If the two keys are the same, the value will be unchanged
1732
# as its always the file id for this entry.
1733
1636
if old_key != new_key:
1734
# Transform a change into explicit delete/add preserving
1735
# a possible match on the key from a different file id.
1736
if old_key is not None:
1737
parent_id_basename_delta.setdefault(
1738
old_key, [None, None])[0] = old_key
1739
if new_key is not None:
1740
parent_id_basename_delta.setdefault(
1741
new_key, [None, None])[1] = new_value
1742
# validate that deletes are complete.
1743
for file_id in deletes:
1744
entry = self[file_id]
1745
if entry.kind != 'directory':
1747
# This loop could potentially be better by using the id_basename
1748
# map to just get the child file ids.
1749
for child in entry.children.values():
1750
if child.file_id not in altered:
1751
raise errors.InconsistentDelta(self.id2path(child.file_id),
1752
child.file_id, "Child not deleted or reparented when "
1637
# If the two keys are the same, the value will be unchanged
1638
# as its always the file id.
1639
parent_id_basename_delta.append((old_key, new_key, new_value))
1754
1640
result.id_to_entry.apply_delta(id_to_entry_delta)
1755
1641
if parent_id_basename_delta:
1756
# Transform the parent_id_basename delta data into a linear delta
1757
# with only one record for a given key. Optimally this would allow
1758
# re-keying, but its simpler to just output that as a delete+add
1759
# to spend less time calculating the delta.
1761
for key, (old_key, value) in parent_id_basename_delta.iteritems():
1762
if value is not None:
1763
delta_list.append((old_key, key, value))
1765
delta_list.append((old_key, None, None))
1766
result.parent_id_basename_to_file_id.apply_delta(delta_list)
1767
parents.discard(('', None))
1768
for parent_path, parent in parents:
1770
if result[parent].kind != 'directory':
1771
raise errors.InconsistentDelta(result.id2path(parent), parent,
1772
'Not a directory, but given children')
1773
except errors.NoSuchId:
1774
raise errors.InconsistentDelta("<unknown>", parent,
1775
"Parent is not present in resulting inventory.")
1776
if result.path2id(parent_path) != parent:
1777
raise errors.InconsistentDelta(parent_path, parent,
1778
"Parent has wrong path %r." % result.path2id(parent_path))
1642
result.parent_id_basename_to_file_id.apply_delta(parent_id_basename_delta)
1807
1671
raise errors.BzrError('Duplicate key in inventory: %r\n%r'
1808
1672
% (key, bytes))
1809
1673
info[key] = value
1810
revision_id = intern(info['revision_id'])
1811
root_id = intern(info['root_id'])
1812
search_key_name = intern(info.get('search_key_name', 'plain'))
1813
parent_id_basename_to_file_id = intern(info.get(
1814
'parent_id_basename_to_file_id', None))
1815
if not parent_id_basename_to_file_id.startswith('sha1:'):
1816
raise ValueError('parent_id_basename_to_file_id should be a sha1'
1817
' key not %r' % (parent_id_basename_to_file_id,))
1674
revision_id = info['revision_id']
1675
root_id = info['root_id']
1676
search_key_name = info.get('search_key_name', 'plain')
1677
parent_id_basename_to_file_id = info.get(
1678
'parent_id_basename_to_file_id', None)
1818
1679
id_to_entry = info['id_to_entry']
1819
if not id_to_entry.startswith('sha1:'):
1820
raise ValueError('id_to_entry should be a sha1'
1821
' key not %r' % (id_to_entry,))
1823
1681
result = CHKInventory(search_key_name)
1824
1682
result.revision_id = revision_id
1827
1685
result._search_key_name)
1828
1686
if parent_id_basename_to_file_id is not None:
1829
1687
result.parent_id_basename_to_file_id = chk_map.CHKMap(
1830
chk_store, StaticTuple(parent_id_basename_to_file_id,),
1688
chk_store, (parent_id_basename_to_file_id,),
1831
1689
search_key_func=search_key_func)
1833
1691
result.parent_id_basename_to_file_id = None
1835
result.id_to_entry = chk_map.CHKMap(chk_store,
1836
StaticTuple(id_to_entry,),
1693
result.id_to_entry = chk_map.CHKMap(chk_store, (id_to_entry,),
1837
1694
search_key_func=search_key_func)
1838
1695
if (result.revision_id,) != expected_revision_id:
1839
1696
raise ValueError("Mismatched revision id and expected: %r, %r" %
1852
1709
:param maximum_size: The CHKMap node size limit.
1853
1710
:param search_key_name: The identifier for the search key function
1855
result = klass(search_key_name)
1712
result = CHKInventory(search_key_name)
1856
1713
result.revision_id = inventory.revision_id
1857
1714
result.root_id = inventory.root.file_id
1859
entry_to_bytes = result._entry_to_bytes
1860
parent_id_basename_key = result._parent_id_basename_key
1861
id_to_entry_dict = {}
1862
parent_id_basename_dict = {}
1715
search_key_func = chk_map.search_key_registry.get(search_key_name)
1716
result.id_to_entry = chk_map.CHKMap(chk_store, None, search_key_func)
1717
result.id_to_entry._root_node.set_maximum_size(maximum_size)
1719
result.parent_id_basename_to_file_id = chk_map.CHKMap(chk_store,
1720
None, search_key_func)
1721
result.parent_id_basename_to_file_id._root_node.set_maximum_size(
1723
result.parent_id_basename_to_file_id._root_node._key_width = 2
1724
parent_id_delta = []
1863
1725
for path, entry in inventory.iter_entries():
1864
key = StaticTuple(entry.file_id,).intern()
1865
id_to_entry_dict[key] = entry_to_bytes(entry)
1866
p_id_key = parent_id_basename_key(entry)
1867
parent_id_basename_dict[p_id_key] = entry.file_id
1869
result._populate_from_dicts(chk_store, id_to_entry_dict,
1870
parent_id_basename_dict, maximum_size=maximum_size)
1726
file_id_delta.append((None, (entry.file_id,),
1727
result._entry_to_bytes(entry)))
1728
parent_id_delta.append(
1729
(None, result._parent_id_basename_key(entry),
1731
result.id_to_entry.apply_delta(file_id_delta)
1732
result.parent_id_basename_to_file_id.apply_delta(parent_id_delta)
1873
def _populate_from_dicts(self, chk_store, id_to_entry_dict,
1874
parent_id_basename_dict, maximum_size):
1875
search_key_func = chk_map.search_key_registry.get(self._search_key_name)
1876
root_key = chk_map.CHKMap.from_dict(chk_store, id_to_entry_dict,
1877
maximum_size=maximum_size, key_width=1,
1878
search_key_func=search_key_func)
1879
self.id_to_entry = chk_map.CHKMap(chk_store, root_key,
1881
root_key = chk_map.CHKMap.from_dict(chk_store,
1882
parent_id_basename_dict,
1883
maximum_size=maximum_size, key_width=2,
1884
search_key_func=search_key_func)
1885
self.parent_id_basename_to_file_id = chk_map.CHKMap(chk_store,
1886
root_key, search_key_func)
1888
1735
def _parent_id_basename_key(self, entry):
1889
1736
"""Create a key for a entry in a parent_id_basename_to_file_id index."""
1890
1737
if entry.parent_id is not None:
1891
1738
parent_id = entry.parent_id
1894
return StaticTuple(parent_id, entry.name.encode('utf8')).intern()
1741
return parent_id, entry.name.encode('utf8')
1896
1743
def __getitem__(self, file_id):
1897
1744
"""map a single file_id -> InventoryEntry."""
1904
1751
return self._bytes_to_entry(
1905
self.id_to_entry.iteritems([StaticTuple(file_id,)]).next()[1])
1752
self.id_to_entry.iteritems([(file_id,)]).next()[1])
1906
1753
except StopIteration:
1907
1754
# really we're passing an inventory, not a tree...
1908
1755
raise errors.NoSuchId(self, file_id)
1910
def _getitems(self, file_ids):
1911
"""Similar to __getitem__, but lets you query for multiple.
1913
The returned order is undefined. And currently if an item doesn't
1914
exist, it isn't included in the output.
1918
for file_id in file_ids:
1919
entry = self._fileid_to_entry_cache.get(file_id, None)
1921
remaining.append(file_id)
1923
result.append(entry)
1924
file_keys = [StaticTuple(f,).intern() for f in remaining]
1925
for file_key, value in self.id_to_entry.iteritems(file_keys):
1926
entry = self._bytes_to_entry(value)
1927
result.append(entry)
1928
self._fileid_to_entry_cache[entry.file_id] = entry
1931
1757
def has_id(self, file_id):
1932
1758
# Perhaps have an explicit 'contains' method on CHKMap ?
1933
1759
if self._fileid_to_entry_cache.get(file_id, None) is not None:
1936
self.id_to_entry.iteritems([StaticTuple(file_id,)]))) == 1
1761
return len(list(self.id_to_entry.iteritems([(file_id,)]))) == 1
1938
1763
def is_root(self, file_id):
1939
1764
return file_id == self.root_id
2068
1893
delta.append((old_path, new_path, file_id, entry))
2071
def path2id(self, relpath):
1896
def path2id(self, name):
2072
1897
"""See CommonInventory.path2id()."""
2073
# TODO: perhaps support negative hits?
2074
result = self._path_to_fileid_cache.get(relpath, None)
2075
if result is not None:
2077
if isinstance(relpath, basestring):
2078
names = osutils.splitpath(relpath)
2081
current_id = self.root_id
2082
if current_id is None:
2084
parent_id_index = self.parent_id_basename_to_file_id
2086
for basename in names:
2087
if cur_path is None:
2090
cur_path = cur_path + '/' + basename
2091
basename_utf8 = basename.encode('utf8')
2092
file_id = self._path_to_fileid_cache.get(cur_path, None)
2094
key_filter = [StaticTuple(current_id, basename_utf8)]
2095
items = parent_id_index.iteritems(key_filter)
2096
for (parent_id, name_utf8), file_id in items:
2097
if parent_id != current_id or name_utf8 != basename_utf8:
2098
raise errors.BzrError("corrupt inventory lookup! "
2099
"%r %r %r %r" % (parent_id, current_id, name_utf8,
2104
self._path_to_fileid_cache[cur_path] = file_id
2105
current_id = file_id
1898
result = self._path_to_fileid_cache.get(name, None)
1900
result = CommonInventory.path2id(self, name)
1901
self._path_to_fileid_cache[name] = result
2108
1904
def to_lines(self):
2109
1905
"""Serialise the inventory to lines."""
2113
1909
lines.append('search_key_name: %s\n' % (self._search_key_name,))
2114
1910
lines.append("root_id: %s\n" % self.root_id)
2115
1911
lines.append('parent_id_basename_to_file_id: %s\n' %
2116
(self.parent_id_basename_to_file_id.key()[0],))
1912
self.parent_id_basename_to_file_id.key())
2117
1913
lines.append("revision_id: %s\n" % self.revision_id)
2118
lines.append("id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
1914
lines.append("id_to_entry: %s\n" % self.id_to_entry.key())
2120
1916
lines.append("revision_id: %s\n" % self.revision_id)
2121
1917
lines.append("root_id: %s\n" % self.root_id)
2122
1918
if self.parent_id_basename_to_file_id is not None:
2123
1919
lines.append('parent_id_basename_to_file_id: %s\n' %
2124
(self.parent_id_basename_to_file_id.key()[0],))
2125
lines.append("id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
1920
self.parent_id_basename_to_file_id.key())
1921
lines.append("id_to_entry: %s\n" % self.id_to_entry.key())
2134
1930
class CHKInventoryDirectory(InventoryDirectory):
2135
1931
"""A directory in an inventory."""
2137
__slots__ = ['_children', '_chk_inventory']
1933
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
1934
'text_id', 'parent_id', '_children', 'executable',
1935
'revision', 'symlink_target', 'reference_revision',
2139
1938
def __init__(self, file_id, name, parent_id, chk_inventory):
2140
1939
# Don't call InventoryDirectory.__init__ - it isn't right for this
2142
1941
InventoryEntry.__init__(self, file_id, name, parent_id)
2143
1942
self._children = None
1943
self.kind = 'directory'
2144
1944
self._chk_inventory = chk_inventory
2239
2039
_NAME_RE = re.compile(r'^[^/\\]+$')
2241
2041
return bool(_NAME_RE.match(name))
2244
def _check_delta_unique_ids(delta):
2245
"""Decorate a delta and check that the file ids in it are unique.
2247
:return: A generator over delta.
2251
length = len(ids) + 1
2253
if len(ids) != length:
2254
raise errors.InconsistentDelta(item[0] or item[1], item[2],
2259
def _check_delta_unique_new_paths(delta):
2260
"""Decorate a delta and check that the new paths in it are unique.
2262
:return: A generator over delta.
2266
length = len(paths) + 1
2268
if path is not None:
2270
if len(paths) != length:
2271
raise errors.InconsistentDelta(path, item[2], "repeated path")
2275
def _check_delta_unique_old_paths(delta):
2276
"""Decorate a delta and check that the old paths in it are unique.
2278
:return: A generator over delta.
2282
length = len(paths) + 1
2284
if path is not None:
2286
if len(paths) != length:
2287
raise errors.InconsistentDelta(path, item[2], "repeated path")
2291
def _check_delta_ids_are_valid(delta):
2292
"""Decorate a delta and check that the ids in it are valid.
2294
:return: A generator over delta.
2299
raise errors.InconsistentDelta(item[0] or item[1], item[2],
2300
"entry with file_id None %r" % entry)
2301
if type(item[2]) != str:
2302
raise errors.InconsistentDelta(item[0] or item[1], item[2],
2303
"entry with non bytes file_id %r" % entry)
2307
def _check_delta_ids_match_entry(delta):
2308
"""Decorate a delta and check that the ids in it match the entry.file_id.
2310
:return: A generator over delta.
2314
if entry is not None:
2315
if entry.file_id != item[2]:
2316
raise errors.InconsistentDelta(item[0] or item[1], item[2],
2317
"mismatched id with %r" % entry)
2321
def _check_delta_new_path_entry_both_or_None(delta):
2322
"""Decorate a delta and check that the new_path and entry are paired.
2324
:return: A generator over delta.
2329
if new_path is None and entry is not None:
2330
raise errors.InconsistentDelta(item[0], item[1],
2331
"Entry with no new_path")
2332
if new_path is not None and entry is None:
2333
raise errors.InconsistentDelta(new_path, item[1],
2334
"new_path with no entry")