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.
856
937
descend(self.root, u'')
859
def path2id(self, relpath):
940
def path2id(self, name):
860
941
"""Walk down through directories to return entry of last component.
862
:param relpath: may be either a list of path components, or a single
863
string, in which case it is automatically split.
943
names may be either a list of path components, or a single
944
string, in which case it is automatically split.
865
946
This returns the entry of the last component in the path,
866
947
which may be either a file or a directory.
868
949
Returns None IFF the path is not found.
870
if isinstance(relpath, basestring):
871
names = osutils.splitpath(relpath)
951
if isinstance(name, basestring):
952
name = osutils.splitpath(name)
954
# mutter("lookup path %r" % name)
876
957
parent = self.root
942
1023
class Inventory(CommonInventory):
943
"""Mutable dict based in-memory inventory.
945
We never store the full path to a file, because renaming a directory
946
implicitly moves all of its contents. This class internally maintains a
1024
"""Inventory of versioned files in a tree.
1026
This describes which file_id is present at each point in the tree,
1027
and possibly the SHA-1 or other information about the file.
1028
Entries can be looked up either by path or by file_id.
1030
The inventory represents a typical unix file tree, with
1031
directories containing files and subdirectories. We never store
1032
the full path to a file, because renaming a directory implicitly
1033
moves all of its contents. This class internally maintains a
947
1034
lookup tree that allows the children under a directory to be
948
1035
returned quickly.
1037
InventoryEntry objects must not be modified after they are
1038
inserted, other than through the Inventory API.
950
1040
>>> inv = Inventory()
951
1041
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
952
1042
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
953
1043
>>> inv['123-123'].name
956
Id's may be looked up from paths:
1046
May be treated as an iterator or set to look up file ids:
958
>>> inv.path2id('hello.c')
1048
>>> bool(inv.path2id('hello.c'))
960
1050
>>> '123-123' in inv
963
There are iterators over the contents:
1053
May also look up by name:
965
>>> [entry[0] for entry in inv.iter_entries()]
1055
>>> [x[0] for x in inv.iter_entries()]
966
1056
['', u'hello.c']
1057
>>> inv = Inventory('TREE_ROOT-12345678-12345678')
1058
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
1059
Traceback (most recent call last):
1060
BzrError: parent_id {TREE_ROOT} not in inventory
1061
>>> inv.add(InventoryFile('123-123', 'hello.c', 'TREE_ROOT-12345678-12345678'))
1062
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None, revision=None)
969
1064
def __init__(self, root_id=ROOT_ID, revision_id=None):
970
1065
"""Create or read an inventory.
1052
1140
# modified children remaining by the time we examine it.
1053
1141
for old_path, file_id in sorted(((op, f) for op, np, f, e in delta
1054
1142
if op is not None), reverse=True):
1143
if file_id not in self:
1055
1146
# Preserve unaltered children of file_id for later reinsertion.
1056
1147
file_id_children = getattr(self[file_id], 'children', {})
1057
1148
if len(file_id_children):
1058
1149
children[file_id] = file_id_children
1059
if self.id2path(file_id) != old_path:
1060
raise errors.InconsistentDelta(old_path, file_id,
1061
"Entry was at wrong other path %r." % self.id2path(file_id))
1062
1150
# Remove file_id and the unaltered children. If file_id is not
1063
1151
# being deleted it will be reinserted back later.
1064
1152
self.remove_recursive_id(file_id)
1078
1166
replacement.revision = new_entry.revision
1079
1167
replacement.children = children.pop(replacement.file_id, {})
1080
1168
new_entry = replacement
1083
except errors.DuplicateFileId:
1084
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1085
"New id is already present in target.")
1086
except AttributeError:
1087
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1088
"Parent is not a directory.")
1089
if self.id2path(new_entry.file_id) != new_path:
1090
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1091
"New path is not consistent with parent path.")
1092
1170
if len(children):
1093
1171
# Get the parent id that was deleted
1094
1172
parent_id, children = children.popitem()
1095
1173
raise errors.InconsistentDelta("<deleted>", parent_id,
1096
1174
"The file id was deleted but its children were not deleted.")
1098
def create_by_apply_delta(self, inventory_delta, new_revision_id,
1099
propagate_caches=False):
1100
"""See CHKInventory.create_by_apply_delta()"""
1101
new_inv = self.copy()
1102
new_inv.apply_delta(inventory_delta)
1103
new_inv.revision_id = new_revision_id
1106
1176
def _set_root(self, ie):
1108
1178
self._byid = {self.root.file_id: self.root}
1179
1249
def add(self, entry):
1180
1250
"""Add entry to inventory.
1252
To add a file to a branch ready to be committed, use Branch.add,
1255
Returns the new entry object.
1184
1257
if entry.file_id in self._byid:
1185
1258
raise errors.DuplicateFileId(entry.file_id,
1186
1259
self._byid[entry.file_id])
1187
1261
if entry.parent_id is None:
1188
1262
self.root = entry
1191
1265
parent = self._byid[entry.parent_id]
1192
1266
except KeyError:
1193
raise errors.InconsistentDelta("<unknown>", entry.parent_id,
1194
"Parent not in inventory.")
1267
raise BzrError("parent_id {%s} not in inventory" %
1195
1270
if entry.name in parent.children:
1196
raise errors.InconsistentDelta(
1197
self.id2path(parent.children[entry.name].file_id),
1199
"Path already versioned")
1271
raise BzrError("%s is already versioned" %
1272
osutils.pathjoin(self.id2path(parent.file_id),
1273
entry.name).encode('utf-8'))
1200
1274
parent.children[entry.name] = entry
1201
1275
return self._add_child(entry)
1392
1469
self._fileid_to_entry_cache = {}
1393
1470
self._path_to_fileid_cache = {}
1394
1471
self._search_key_name = search_key_name
1397
def __eq__(self, other):
1398
"""Compare two sets by comparing their contents."""
1399
if not isinstance(other, CHKInventory):
1400
return NotImplemented
1402
this_key = self.id_to_entry.key()
1403
other_key = other.id_to_entry.key()
1404
this_pid_key = self.parent_id_basename_to_file_id.key()
1405
other_pid_key = other.parent_id_basename_to_file_id.key()
1406
if None in (this_key, this_pid_key, other_key, other_pid_key):
1408
return this_key == other_key and this_pid_key == other_pid_key
1410
1473
def _entry_to_bytes(self, entry):
1411
1474
"""Serialise entry as a single bytestring.
1450
1513
raise ValueError("unknown kind %r" % entry.kind)
1452
def _expand_fileids_to_parents_and_children(self, file_ids):
1453
"""Give a more wholistic view starting with the given file_ids.
1455
For any file_id which maps to a directory, we will include all children
1456
of that directory. We will also include all directories which are
1457
parents of the given file_ids, but we will not include their children.
1464
fringle # fringle-id
1468
if given [foo-id] we will include
1469
TREE_ROOT as interesting parents
1471
foo-id, baz-id, frob-id, fringle-id
1475
# TODO: Pre-pass over the list of fileids to see if anything is already
1476
# deserialized in self._fileid_to_entry_cache
1478
directories_to_expand = set()
1479
children_of_parent_id = {}
1480
# It is okay if some of the fileids are missing
1481
for entry in self._getitems(file_ids):
1482
if entry.kind == 'directory':
1483
directories_to_expand.add(entry.file_id)
1484
interesting.add(entry.parent_id)
1485
children_of_parent_id.setdefault(entry.parent_id, []
1486
).append(entry.file_id)
1488
# Now, interesting has all of the direct parents, but not the
1489
# parents of those parents. It also may have some duplicates with
1491
remaining_parents = interesting.difference(file_ids)
1492
# When we hit the TREE_ROOT, we'll get an interesting parent of None,
1493
# but we don't actually want to recurse into that
1494
interesting.add(None) # this will auto-filter it in the loop
1495
remaining_parents.discard(None)
1496
while remaining_parents:
1497
next_parents = set()
1498
for entry in self._getitems(remaining_parents):
1499
next_parents.add(entry.parent_id)
1500
children_of_parent_id.setdefault(entry.parent_id, []
1501
).append(entry.file_id)
1502
# Remove any search tips we've already processed
1503
remaining_parents = next_parents.difference(interesting)
1504
interesting.update(remaining_parents)
1505
# We should probably also .difference(directories_to_expand)
1506
interesting.update(file_ids)
1507
interesting.discard(None)
1508
while directories_to_expand:
1509
# Expand directories by looking in the
1510
# parent_id_basename_to_file_id map
1511
keys = [StaticTuple(f,).intern() for f in directories_to_expand]
1512
directories_to_expand = set()
1513
items = self.parent_id_basename_to_file_id.iteritems(keys)
1514
next_file_ids = set([item[1] for item in items])
1515
next_file_ids = next_file_ids.difference(interesting)
1516
interesting.update(next_file_ids)
1517
for entry in self._getitems(next_file_ids):
1518
if entry.kind == 'directory':
1519
directories_to_expand.add(entry.file_id)
1520
children_of_parent_id.setdefault(entry.parent_id, []
1521
).append(entry.file_id)
1522
return interesting, children_of_parent_id
1524
def filter(self, specific_fileids):
1525
"""Get an inventory view filtered against a set of file-ids.
1527
Children of directories and parents are included.
1529
The result may or may not reference the underlying inventory
1530
so it should be treated as immutable.
1533
parent_to_children) = self._expand_fileids_to_parents_and_children(
1535
# There is some overlap here, but we assume that all interesting items
1536
# are in the _fileid_to_entry_cache because we had to read them to
1537
# determine if they were a dir we wanted to recurse, or just a file
1538
# This should give us all the entries we'll want to add, so start
1540
other = Inventory(self.root_id)
1541
other.root.revision = self.root.revision
1542
other.revision_id = self.revision_id
1543
if not interesting or not parent_to_children:
1544
# empty filter, or filtering entrys that don't exist
1545
# (if even 1 existed, then we would have populated
1546
# parent_to_children with at least the tree root.)
1548
cache = self._fileid_to_entry_cache
1549
remaining_children = collections.deque(parent_to_children[self.root_id])
1550
while remaining_children:
1551
file_id = remaining_children.popleft()
1553
if ie.kind == 'directory':
1554
ie = ie.copy() # We create a copy to depopulate the .children attribute
1555
# TODO: depending on the uses of 'other' we should probably alwyas
1556
# '.copy()' to prevent someone from mutating other and
1557
# invaliding our internal cache
1559
if file_id in parent_to_children:
1560
remaining_children.extend(parent_to_children[file_id])
1564
1516
def _bytes_to_utf8name_key(bytes):
1565
1517
"""Get the file_id, revision_id key out of bytes."""
1639
1586
search_key_func=search_key_func)
1640
1587
result.id_to_entry._ensure_root()
1641
1588
result.id_to_entry._root_node.set_maximum_size(maximum_size)
1642
# Change to apply to the parent_id_basename delta. The dict maps
1643
# (parent_id, basename) -> (old_key, new_value). We use a dict because
1644
# when a path has its id replaced (e.g. the root is changed, or someone
1645
# does bzr mv a b, bzr mv c a, we should output a single change to this
1646
# map rather than two.
1647
parent_id_basename_delta = {}
1589
parent_id_basename_delta = []
1648
1590
if self.parent_id_basename_to_file_id is not None:
1649
1591
result.parent_id_basename_to_file_id = chk_map.CHKMap(
1650
1592
self.parent_id_basename_to_file_id._store,
1660
1602
result.parent_id_basename_to_file_id = None
1661
1603
result.root_id = self.root_id
1662
1604
id_to_entry_delta = []
1663
# inventory_delta is only traversed once, so we just update the
1665
# Check for repeated file ids
1666
inventory_delta = _check_delta_unique_ids(inventory_delta)
1667
# Repeated old paths
1668
inventory_delta = _check_delta_unique_old_paths(inventory_delta)
1669
# Check for repeated new paths
1670
inventory_delta = _check_delta_unique_new_paths(inventory_delta)
1671
# Check for entries that don't match the fileid
1672
inventory_delta = _check_delta_ids_match_entry(inventory_delta)
1673
# Check for nonsense fileids
1674
inventory_delta = _check_delta_ids_are_valid(inventory_delta)
1675
# Check for new_path <-> entry consistency
1676
inventory_delta = _check_delta_new_path_entry_both_or_None(
1678
# All changed entries need to have their parents be directories and be
1679
# at the right path. This set contains (path, id) tuples.
1681
# When we delete an item, all the children of it must be either deleted
1682
# or altered in their own right. As we batch process the change via
1683
# CHKMap.apply_delta, we build a set of things to use to validate the
1687
1605
for old_path, new_path, file_id, entry in inventory_delta:
1688
1606
# file id changes
1689
1607
if new_path == '':
1698
1616
del result._path_to_fileid_cache[old_path]
1699
1617
except KeyError:
1701
deletes.add(file_id)
1703
new_key = StaticTuple(file_id,)
1620
new_key = (file_id,)
1704
1621
new_value = result._entry_to_bytes(entry)
1705
1622
# Update caches. It's worth doing this whether
1706
1623
# we're propagating the old caches or not.
1707
1624
result._path_to_fileid_cache[new_path] = file_id
1708
parents.add((split(new_path)[0], entry.parent_id))
1709
1625
if old_path is None:
1712
old_key = StaticTuple(file_id,)
1713
if self.id2path(file_id) != old_path:
1714
raise errors.InconsistentDelta(old_path, file_id,
1715
"Entry was at wrong other path %r." %
1716
self.id2path(file_id))
1717
altered.add(file_id)
1718
id_to_entry_delta.append(StaticTuple(old_key, new_key, new_value))
1628
old_key = (file_id,)
1629
id_to_entry_delta.append((old_key, new_key, new_value))
1719
1630
if result.parent_id_basename_to_file_id is not None:
1720
1631
# parent_id, basename changes
1721
1632
if old_path is None:
1730
1641
new_key = self._parent_id_basename_key(entry)
1731
1642
new_value = file_id
1732
# If the two keys are the same, the value will be unchanged
1733
# as its always the file id for this entry.
1734
1643
if old_key != new_key:
1735
# Transform a change into explicit delete/add preserving
1736
# a possible match on the key from a different file id.
1737
if old_key is not None:
1738
parent_id_basename_delta.setdefault(
1739
old_key, [None, None])[0] = old_key
1740
if new_key is not None:
1741
parent_id_basename_delta.setdefault(
1742
new_key, [None, None])[1] = new_value
1743
# validate that deletes are complete.
1744
for file_id in deletes:
1745
entry = self[file_id]
1746
if entry.kind != 'directory':
1748
# This loop could potentially be better by using the id_basename
1749
# map to just get the child file ids.
1750
for child in entry.children.values():
1751
if child.file_id not in altered:
1752
raise errors.InconsistentDelta(self.id2path(child.file_id),
1753
child.file_id, "Child not deleted or reparented when "
1644
# If the two keys are the same, the value will be unchanged
1645
# as its always the file id.
1646
parent_id_basename_delta.append((old_key, new_key, new_value))
1755
1647
result.id_to_entry.apply_delta(id_to_entry_delta)
1756
1648
if parent_id_basename_delta:
1757
# Transform the parent_id_basename delta data into a linear delta
1758
# with only one record for a given key. Optimally this would allow
1759
# re-keying, but its simpler to just output that as a delete+add
1760
# to spend less time calculating the delta.
1762
for key, (old_key, value) in parent_id_basename_delta.iteritems():
1763
if value is not None:
1764
delta_list.append((old_key, key, value))
1766
delta_list.append((old_key, None, None))
1767
result.parent_id_basename_to_file_id.apply_delta(delta_list)
1768
parents.discard(('', None))
1769
for parent_path, parent in parents:
1771
if result[parent].kind != 'directory':
1772
raise errors.InconsistentDelta(result.id2path(parent), parent,
1773
'Not a directory, but given children')
1774
except errors.NoSuchId:
1775
raise errors.InconsistentDelta("<unknown>", parent,
1776
"Parent is not present in resulting inventory.")
1777
if result.path2id(parent_path) != parent:
1778
raise errors.InconsistentDelta(parent_path, parent,
1779
"Parent has wrong path %r." % result.path2id(parent_path))
1649
result.parent_id_basename_to_file_id.apply_delta(parent_id_basename_delta)
1808
1678
raise errors.BzrError('Duplicate key in inventory: %r\n%r'
1809
1679
% (key, bytes))
1810
1680
info[key] = value
1811
revision_id = intern(info['revision_id'])
1812
root_id = intern(info['root_id'])
1813
search_key_name = intern(info.get('search_key_name', 'plain'))
1814
parent_id_basename_to_file_id = intern(info.get(
1815
'parent_id_basename_to_file_id', None))
1816
if not parent_id_basename_to_file_id.startswith('sha1:'):
1817
raise ValueError('parent_id_basename_to_file_id should be a sha1'
1818
' key not %r' % (parent_id_basename_to_file_id,))
1681
revision_id = info['revision_id']
1682
root_id = info['root_id']
1683
search_key_name = info.get('search_key_name', 'plain')
1684
parent_id_basename_to_file_id = info.get(
1685
'parent_id_basename_to_file_id', None)
1819
1686
id_to_entry = info['id_to_entry']
1820
if not id_to_entry.startswith('sha1:'):
1821
raise ValueError('id_to_entry should be a sha1'
1822
' key not %r' % (id_to_entry,))
1824
1688
result = CHKInventory(search_key_name)
1825
1689
result.revision_id = revision_id
1828
1692
result._search_key_name)
1829
1693
if parent_id_basename_to_file_id is not None:
1830
1694
result.parent_id_basename_to_file_id = chk_map.CHKMap(
1831
chk_store, StaticTuple(parent_id_basename_to_file_id,),
1695
chk_store, (parent_id_basename_to_file_id,),
1832
1696
search_key_func=search_key_func)
1834
1698
result.parent_id_basename_to_file_id = None
1836
result.id_to_entry = chk_map.CHKMap(chk_store,
1837
StaticTuple(id_to_entry,),
1700
result.id_to_entry = chk_map.CHKMap(chk_store, (id_to_entry,),
1838
1701
search_key_func=search_key_func)
1839
1702
if (result.revision_id,) != expected_revision_id:
1840
1703
raise ValueError("Mismatched revision id and expected: %r, %r" %
1853
1716
:param maximum_size: The CHKMap node size limit.
1854
1717
:param search_key_name: The identifier for the search key function
1856
result = klass(search_key_name)
1719
result = CHKInventory(search_key_name)
1857
1720
result.revision_id = inventory.revision_id
1858
1721
result.root_id = inventory.root.file_id
1860
entry_to_bytes = result._entry_to_bytes
1861
parent_id_basename_key = result._parent_id_basename_key
1862
id_to_entry_dict = {}
1863
parent_id_basename_dict = {}
1722
search_key_func = chk_map.search_key_registry.get(search_key_name)
1723
result.id_to_entry = chk_map.CHKMap(chk_store, None, search_key_func)
1724
result.id_to_entry._root_node.set_maximum_size(maximum_size)
1726
result.parent_id_basename_to_file_id = chk_map.CHKMap(chk_store,
1727
None, search_key_func)
1728
result.parent_id_basename_to_file_id._root_node.set_maximum_size(
1730
result.parent_id_basename_to_file_id._root_node._key_width = 2
1731
parent_id_delta = []
1864
1732
for path, entry in inventory.iter_entries():
1865
key = StaticTuple(entry.file_id,).intern()
1866
id_to_entry_dict[key] = entry_to_bytes(entry)
1867
p_id_key = parent_id_basename_key(entry)
1868
parent_id_basename_dict[p_id_key] = entry.file_id
1870
result._populate_from_dicts(chk_store, id_to_entry_dict,
1871
parent_id_basename_dict, maximum_size=maximum_size)
1733
file_id_delta.append((None, (entry.file_id,),
1734
result._entry_to_bytes(entry)))
1735
parent_id_delta.append(
1736
(None, result._parent_id_basename_key(entry),
1738
result.id_to_entry.apply_delta(file_id_delta)
1739
result.parent_id_basename_to_file_id.apply_delta(parent_id_delta)
1874
def _populate_from_dicts(self, chk_store, id_to_entry_dict,
1875
parent_id_basename_dict, maximum_size):
1876
search_key_func = chk_map.search_key_registry.get(self._search_key_name)
1877
root_key = chk_map.CHKMap.from_dict(chk_store, id_to_entry_dict,
1878
maximum_size=maximum_size, key_width=1,
1879
search_key_func=search_key_func)
1880
self.id_to_entry = chk_map.CHKMap(chk_store, root_key,
1882
root_key = chk_map.CHKMap.from_dict(chk_store,
1883
parent_id_basename_dict,
1884
maximum_size=maximum_size, key_width=2,
1885
search_key_func=search_key_func)
1886
self.parent_id_basename_to_file_id = chk_map.CHKMap(chk_store,
1887
root_key, search_key_func)
1889
1742
def _parent_id_basename_key(self, entry):
1890
1743
"""Create a key for a entry in a parent_id_basename_to_file_id index."""
1891
1744
if entry.parent_id is not None:
1892
1745
parent_id = entry.parent_id
1895
return StaticTuple(parent_id, entry.name.encode('utf8')).intern()
1748
return parent_id, entry.name.encode('utf8')
1897
1750
def __getitem__(self, file_id):
1898
1751
"""map a single file_id -> InventoryEntry."""
1905
1758
return self._bytes_to_entry(
1906
self.id_to_entry.iteritems([StaticTuple(file_id,)]).next()[1])
1759
self.id_to_entry.iteritems([(file_id,)]).next()[1])
1907
1760
except StopIteration:
1908
1761
# really we're passing an inventory, not a tree...
1909
1762
raise errors.NoSuchId(self, file_id)
1911
def _getitems(self, file_ids):
1912
"""Similar to __getitem__, but lets you query for multiple.
1914
The returned order is undefined. And currently if an item doesn't
1915
exist, it isn't included in the output.
1919
for file_id in file_ids:
1920
entry = self._fileid_to_entry_cache.get(file_id, None)
1922
remaining.append(file_id)
1924
result.append(entry)
1925
file_keys = [StaticTuple(f,).intern() for f in remaining]
1926
for file_key, value in self.id_to_entry.iteritems(file_keys):
1927
entry = self._bytes_to_entry(value)
1928
result.append(entry)
1929
self._fileid_to_entry_cache[entry.file_id] = entry
1932
1764
def has_id(self, file_id):
1933
1765
# Perhaps have an explicit 'contains' method on CHKMap ?
1934
1766
if self._fileid_to_entry_cache.get(file_id, None) is not None:
1937
self.id_to_entry.iteritems([StaticTuple(file_id,)]))) == 1
1768
return len(list(self.id_to_entry.iteritems([(file_id,)]))) == 1
1939
1770
def is_root(self, file_id):
1940
1771
return file_id == self.root_id
2069
1900
delta.append((old_path, new_path, file_id, entry))
2072
def path2id(self, relpath):
1903
def path2id(self, name):
2073
1904
"""See CommonInventory.path2id()."""
2074
# TODO: perhaps support negative hits?
2075
result = self._path_to_fileid_cache.get(relpath, None)
2076
if result is not None:
2078
if isinstance(relpath, basestring):
2079
names = osutils.splitpath(relpath)
2082
current_id = self.root_id
2083
if current_id is None:
2085
parent_id_index = self.parent_id_basename_to_file_id
2087
for basename in names:
2088
if cur_path is None:
2091
cur_path = cur_path + '/' + basename
2092
basename_utf8 = basename.encode('utf8')
2093
file_id = self._path_to_fileid_cache.get(cur_path, None)
2095
key_filter = [StaticTuple(current_id, basename_utf8)]
2096
items = parent_id_index.iteritems(key_filter)
2097
for (parent_id, name_utf8), file_id in items:
2098
if parent_id != current_id or name_utf8 != basename_utf8:
2099
raise errors.BzrError("corrupt inventory lookup! "
2100
"%r %r %r %r" % (parent_id, current_id, name_utf8,
2105
self._path_to_fileid_cache[cur_path] = file_id
2106
current_id = file_id
1905
result = self._path_to_fileid_cache.get(name, None)
1907
result = CommonInventory.path2id(self, name)
1908
self._path_to_fileid_cache[name] = result
2109
1911
def to_lines(self):
2110
1912
"""Serialise the inventory to lines."""
2114
1916
lines.append('search_key_name: %s\n' % (self._search_key_name,))
2115
1917
lines.append("root_id: %s\n" % self.root_id)
2116
1918
lines.append('parent_id_basename_to_file_id: %s\n' %
2117
(self.parent_id_basename_to_file_id.key()[0],))
1919
self.parent_id_basename_to_file_id.key())
2118
1920
lines.append("revision_id: %s\n" % self.revision_id)
2119
lines.append("id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
1921
lines.append("id_to_entry: %s\n" % self.id_to_entry.key())
2121
1923
lines.append("revision_id: %s\n" % self.revision_id)
2122
1924
lines.append("root_id: %s\n" % self.root_id)
2123
1925
if self.parent_id_basename_to_file_id is not None:
2124
1926
lines.append('parent_id_basename_to_file_id: %s\n' %
2125
(self.parent_id_basename_to_file_id.key()[0],))
2126
lines.append("id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
1927
self.parent_id_basename_to_file_id.key())
1928
lines.append("id_to_entry: %s\n" % self.id_to_entry.key())
2135
1937
class CHKInventoryDirectory(InventoryDirectory):
2136
1938
"""A directory in an inventory."""
2138
__slots__ = ['_children', '_chk_inventory']
1940
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
1941
'text_id', 'parent_id', '_children', 'executable',
1942
'revision', 'symlink_target', 'reference_revision',
2140
1945
def __init__(self, file_id, name, parent_id, chk_inventory):
2141
1946
# Don't call InventoryDirectory.__init__ - it isn't right for this
2143
1948
InventoryEntry.__init__(self, file_id, name, parent_id)
2144
1949
self._children = None
1950
self.kind = 'directory'
2145
1951
self._chk_inventory = chk_inventory
2240
2046
_NAME_RE = re.compile(r'^[^/\\]+$')
2242
2048
return bool(_NAME_RE.match(name))
2245
def _check_delta_unique_ids(delta):
2246
"""Decorate a delta and check that the file ids in it are unique.
2248
:return: A generator over delta.
2252
length = len(ids) + 1
2254
if len(ids) != length:
2255
raise errors.InconsistentDelta(item[0] or item[1], item[2],
2260
def _check_delta_unique_new_paths(delta):
2261
"""Decorate a delta and check that the new paths in it are unique.
2263
:return: A generator over delta.
2267
length = len(paths) + 1
2269
if path is not None:
2271
if len(paths) != length:
2272
raise errors.InconsistentDelta(path, item[2], "repeated path")
2276
def _check_delta_unique_old_paths(delta):
2277
"""Decorate a delta and check that the old paths in it are unique.
2279
:return: A generator over delta.
2283
length = len(paths) + 1
2285
if path is not None:
2287
if len(paths) != length:
2288
raise errors.InconsistentDelta(path, item[2], "repeated path")
2292
def _check_delta_ids_are_valid(delta):
2293
"""Decorate a delta and check that the ids in it are valid.
2295
:return: A generator over delta.
2300
raise errors.InconsistentDelta(item[0] or item[1], item[2],
2301
"entry with file_id None %r" % entry)
2302
if type(item[2]) != str:
2303
raise errors.InconsistentDelta(item[0] or item[1], item[2],
2304
"entry with non bytes file_id %r" % entry)
2308
def _check_delta_ids_match_entry(delta):
2309
"""Decorate a delta and check that the ids in it match the entry.file_id.
2311
:return: A generator over delta.
2315
if entry is not None:
2316
if entry.file_id != item[2]:
2317
raise errors.InconsistentDelta(item[0] or item[1], item[2],
2318
"mismatched id with %r" % entry)
2322
def _check_delta_new_path_entry_both_or_None(delta):
2323
"""Decorate a delta and check that the new_path and entry are paired.
2325
:return: A generator over delta.
2330
if new_path is None and entry is not None:
2331
raise errors.InconsistentDelta(item[0], item[1],
2332
"Entry with no new_path")
2333
if new_path is not None and entry is None:
2334
raise errors.InconsistentDelta(new_path, item[1],
2335
"new_path with no entry")