~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Vincent Ladeuil
  • Date: 2009-05-05 15:31:34 UTC
  • mto: (4343.1.1 integration)
  • mto: This revision was merged to the branch mainline in revision 4344.
  • Revision ID: v.ladeuil+lp@free.fr-20090505153134-q4bp4is9gywsmzrv
Clean up test for log formats.

* bzrlib/tests/blackbox/test_logformats.py:
Update tests to actual style.

Show diffs side-by-side

added added

removed removed

Lines of Context:
27
27
# created, but it's not for now.
28
28
ROOT_ID = "TREE_ROOT"
29
29
 
 
30
from copy import deepcopy
 
31
 
30
32
from bzrlib.lazy_import import lazy_import
31
33
lazy_import(globals(), """
32
34
import collections
33
 
import copy
34
35
import os
35
36
import re
36
37
import tarfile
42
43
    generate_ids,
43
44
    osutils,
44
45
    symbol_versioning,
 
46
    workingtree,
45
47
    )
46
48
""")
47
49
 
260
262
    def versionable_kind(kind):
261
263
        return (kind in ('file', 'directory', 'symlink', 'tree-reference'))
262
264
 
263
 
    def check(self, checker, rev_id, inv):
 
265
    def check(self, checker, rev_id, inv, tree):
264
266
        """Check this inventory entry is intact.
265
267
 
266
268
        This is a template method, override _check for kind specific
272
274
        :param rev_id: Revision id from which this InventoryEntry was loaded.
273
275
             Not necessarily the last-changed revision for this file.
274
276
        :param inv: Inventory from which the entry was loaded.
 
277
        :param tree: RevisionTree for this entry.
275
278
        """
276
279
        if self.parent_id is not None:
277
280
            if not inv.has_id(self.parent_id):
278
281
                raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
279
282
                        % (self.parent_id, rev_id))
280
 
        checker._add_entry_to_text_key_references(inv, self)
281
 
        self._check(checker, rev_id)
 
283
        self._check(checker, rev_id, tree)
282
284
 
283
 
    def _check(self, checker, rev_id):
 
285
    def _check(self, checker, rev_id, tree):
284
286
        """Check this inventory entry for kind specific errors."""
285
 
        checker._report_items.append(
286
 
            'unknown entry kind %r in revision {%s}' % (self.kind, rev_id))
 
287
        raise BzrCheckError('unknown entry kind %r in revision {%s}' %
 
288
                            (self.kind, rev_id))
287
289
 
288
290
    def copy(self):
289
291
        """Clone this inventory entry."""
402
404
                 'text_id', 'parent_id', 'children', 'executable',
403
405
                 'revision', 'symlink_target', 'reference_revision']
404
406
 
405
 
    def _check(self, checker, rev_id):
 
407
    def _check(self, checker, rev_id, tree):
406
408
        """See InventoryEntry._check"""
407
409
 
408
410
    def __init__(self, file_id):
431
433
                 'text_id', 'parent_id', 'children', 'executable',
432
434
                 'revision', 'symlink_target', 'reference_revision']
433
435
 
434
 
    def _check(self, checker, rev_id):
 
436
    def _check(self, checker, rev_id, tree):
435
437
        """See InventoryEntry._check"""
436
 
        if (self.text_sha1 is not None or self.text_size is not None or
437
 
            self.text_id is not None):
438
 
            checker._report_items.append('directory {%s} has text in revision {%s}'
 
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}'
439
440
                                % (self.file_id, rev_id))
440
 
        # Directories are stored as ''.
441
 
        checker.add_pending_item(rev_id,
442
 
            ('texts', self.file_id, self.revision), 'text',
443
 
             'da39a3ee5e6b4b0d3255bfef95601890afd80709')
444
441
 
445
442
    def copy(self):
446
443
        other = InventoryDirectory(self.file_id, self.name, self.parent_id)
479
476
                 'text_id', 'parent_id', 'children', 'executable',
480
477
                 'revision', 'symlink_target', 'reference_revision']
481
478
 
482
 
    def _check(self, checker, tree_revision_id):
 
479
    def _check(self, checker, tree_revision_id, tree):
483
480
        """See InventoryEntry._check"""
484
 
        # TODO: check size too.
485
 
        checker.add_pending_item(tree_revision_id,
486
 
            ('texts', self.file_id, self.revision), 'text',
487
 
             self.text_sha1)
488
 
        if self.text_size is None:
489
 
            checker._report_items.append(
490
 
                'fileid {%s} in {%s} has None for text_size' % (self.file_id,
491
 
                tree_revision_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:
 
485
                raise BzrCheckError(
 
486
                    'mismatched sha1 on {%s} in {%s} (%s != %s) %r' %
 
487
                    (self.file_id, tree_revision_id, prev_sha, self.text_sha1,
 
488
                     t))
 
489
            else:
 
490
                checker.repeated_text_cnt += 1
 
491
                return
 
492
 
 
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
492
500
 
493
501
    def copy(self):
494
502
        other = InventoryFile(self.file_id, self.name, self.parent_id)
592
600
                 'text_id', 'parent_id', 'children', 'executable',
593
601
                 'revision', 'symlink_target', 'reference_revision']
594
602
 
595
 
    def _check(self, checker, tree_revision_id):
 
603
    def _check(self, checker, rev_id, tree):
596
604
        """See InventoryEntry._check"""
597
605
        if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
598
 
            checker._report_items.append(
599
 
               'symlink {%s} has text in revision {%s}'
600
 
                    % (self.file_id, tree_revision_id))
 
606
            raise BzrCheckError('symlink {%s} has text in revision {%s}'
 
607
                    % (self.file_id, rev_id))
601
608
        if self.symlink_target is None:
602
 
            checker._report_items.append(
603
 
                'symlink {%s} has no target in revision {%s}'
604
 
                    % (self.file_id, tree_revision_id))
605
 
        # Symlinks are stored as ''
606
 
        checker.add_pending_item(tree_revision_id,
607
 
            ('texts', self.file_id, self.revision), 'text',
608
 
             'da39a3ee5e6b4b0d3255bfef95601890afd80709')
 
609
            raise BzrCheckError('symlink {%s} has no target in revision {%s}'
 
610
                    % (self.file_id, rev_id))
609
611
 
610
612
    def copy(self):
611
613
        other = InventoryLink(self.file_id, self.name, self.parent_id)
712
714
 
713
715
 
714
716
class CommonInventory(object):
715
 
    """Basic inventory logic, defined in terms of primitives like has_id.
716
 
 
717
 
    An inventory is the metadata about the contents of a tree.
718
 
 
719
 
    This is broadly a map from file_id to entries such as directories, files,
720
 
    symlinks and tree references. Each entry maintains its own metadata like
721
 
    SHA1 and length for files, or children for a directory.
722
 
 
723
 
    Entries can be looked up either by path or by file_id.
724
 
 
725
 
    InventoryEntry objects must not be modified after they are
726
 
    inserted, other than through the Inventory API.
727
 
    """
 
717
    """Basic inventory logic, defined in terms of primitives like has_id."""
728
718
 
729
719
    def __contains__(self, file_id):
730
720
        """True if this entry contains a file with given id.
757
747
            [parent.name for parent in
758
748
             self._iter_file_id_parents(file_id)][:-1]))
759
749
 
760
 
    def iter_entries(self, from_dir=None, recursive=True):
761
 
        """Return (path, entry) pairs, in order by name.
762
 
        
763
 
        :param from_dir: if None, start from the root,
764
 
          otherwise start from this directory (either file-id or entry)
765
 
        :param recursive: recurse into directories or not
766
 
        """
 
750
    def iter_entries(self, from_dir=None):
 
751
        """Return (path, entry) pairs, in order by name."""
767
752
        if from_dir is None:
768
753
            if self.root is None:
769
754
                return
776
761
        # 440ms/663ms (inline/total) to 116ms/116ms
777
762
        children = from_dir.children.items()
778
763
        children.sort()
779
 
        if not recursive:
780
 
            for name, ie in children:
781
 
                yield name, ie
782
 
            return
783
764
        children = collections.deque(children)
784
765
        stack = [(u'', children)]
785
766
        while stack:
1031
1012
 
1032
1013
 
1033
1014
class Inventory(CommonInventory):
1034
 
    """Mutable dict based in-memory inventory.
1035
 
 
1036
 
    We never store the full path to a file, because renaming a directory
1037
 
    implicitly moves all of its contents.  This class internally maintains a
 
1015
    """Inventory of versioned files in a tree.
 
1016
 
 
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.
 
1020
 
 
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
1038
1025
    lookup tree that allows the children under a directory to be
1039
1026
    returned quickly.
1040
1027
 
 
1028
    InventoryEntry objects must not be modified after they are
 
1029
    inserted, other than through the Inventory API.
 
1030
 
1041
1031
    >>> inv = Inventory()
1042
1032
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
1043
1033
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1044
1034
    >>> inv['123-123'].name
1045
1035
    'hello.c'
1046
1036
 
1047
 
    Id's may be looked up from paths:
 
1037
    May be treated as an iterator or set to look up file ids:
1048
1038
 
1049
 
    >>> inv.path2id('hello.c')
1050
 
    '123-123'
 
1039
    >>> bool(inv.path2id('hello.c'))
 
1040
    True
1051
1041
    >>> '123-123' in inv
1052
1042
    True
1053
1043
 
1054
 
    There are iterators over the contents:
 
1044
    May also look up by name:
1055
1045
 
1056
 
    >>> [entry[0] for entry in inv.iter_entries()]
 
1046
    >>> [x[0] for x in inv.iter_entries()]
1057
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)
1058
1054
    """
1059
 
 
1060
1055
    def __init__(self, root_id=ROOT_ID, revision_id=None):
1061
1056
        """Create or read an inventory.
1062
1057
 
1086
1081
    def apply_delta(self, delta):
1087
1082
        """Apply a delta to this inventory.
1088
1083
 
1089
 
        See the inventory developers documentation for the theory behind
1090
 
        inventory deltas.
1091
 
 
1092
 
        If delta application fails the inventory is left in an indeterminate
1093
 
        state and must not be used.
1094
 
 
1095
1084
        :param delta: A list of changes to apply. After all the changes are
1096
1085
            applied the final inventory must be internally consistent, but it
1097
1086
            is ok to supply changes which, if only half-applied would have an
1128
1117
        """
1129
1118
        # Check that the delta is legal. It would be nice if this could be
1130
1119
        # done within the loops below but it's safer to validate the delta
1131
 
        # before starting to mutate the inventory, as there isn't a rollback
1132
 
        # facility.
1133
 
        list(_check_delta_unique_ids(_check_delta_unique_new_paths(
1134
 
            _check_delta_unique_old_paths(_check_delta_ids_match_entry(
1135
 
            _check_delta_ids_are_valid(
1136
 
            _check_delta_new_path_entry_both_or_None(
1137
 
            delta)))))))
 
1120
        # before starting to mutate the inventory.
 
1121
        unique_file_ids = set([f for _, _, f, _ in delta])
 
1122
        if len(unique_file_ids) != len(delta):
 
1123
            raise AssertionError("a file-id appears multiple times in %r"
 
1124
                    % (delta,))
 
1125
        del unique_file_ids
1138
1126
 
1139
1127
        children = {}
1140
1128
        # Remove all affected items which were in the original inventory,
1143
1131
        # modified children remaining by the time we examine it.
1144
1132
        for old_path, file_id in sorted(((op, f) for op, np, f, e in delta
1145
1133
                                        if op is not None), reverse=True):
 
1134
            if file_id not in self:
 
1135
                # adds come later
 
1136
                continue
1146
1137
            # Preserve unaltered children of file_id for later reinsertion.
1147
1138
            file_id_children = getattr(self[file_id], 'children', {})
1148
1139
            if len(file_id_children):
1149
1140
                children[file_id] = file_id_children
1150
 
            if self.id2path(file_id) != old_path:
1151
 
                raise errors.InconsistentDelta(old_path, file_id,
1152
 
                    "Entry was at wrong other path %r." % self.id2path(file_id))
1153
1141
            # Remove file_id and the unaltered children. If file_id is not
1154
1142
            # being deleted it will be reinserted back later.
1155
1143
            self.remove_recursive_id(file_id)
1158
1146
        # longest, ensuring that items which were modified and whose parents in
1159
1147
        # the resulting inventory were also modified, are inserted after their
1160
1148
        # parents.
1161
 
        for new_path, f, new_entry in sorted((np, f, e) for op, np, f, e in
 
1149
        for new_path, new_entry in sorted((np, e) for op, np, f, e in
1162
1150
                                          delta if np is not None):
1163
1151
            if new_entry.kind == 'directory':
1164
1152
                # Pop the child which to allow detection of children whose
1169
1157
                replacement.revision = new_entry.revision
1170
1158
                replacement.children = children.pop(replacement.file_id, {})
1171
1159
                new_entry = replacement
1172
 
            try:
1173
 
                self.add(new_entry)
1174
 
            except errors.DuplicateFileId:
1175
 
                raise errors.InconsistentDelta(new_path, new_entry.file_id,
1176
 
                    "New id is already present in target.")
1177
 
            except AttributeError:
1178
 
                raise errors.InconsistentDelta(new_path, new_entry.file_id,
1179
 
                    "Parent is not a directory.")
1180
 
            if self.id2path(new_entry.file_id) != new_path:
1181
 
                raise errors.InconsistentDelta(new_path, new_entry.file_id,
1182
 
                    "New path is not consistent with parent path.")
 
1160
            self.add(new_entry)
1183
1161
        if len(children):
1184
1162
            # Get the parent id that was deleted
1185
1163
            parent_id, children = children.popitem()
1205
1183
 
1206
1184
    def _get_mutable_inventory(self):
1207
1185
        """See CommonInventory._get_mutable_inventory."""
1208
 
        return copy.deepcopy(self)
 
1186
        return deepcopy(self)
1209
1187
 
1210
1188
    def __iter__(self):
1211
1189
        """Iterate over all file-ids."""
1265
1243
        To add  a file to a branch ready to be committed, use Branch.add,
1266
1244
        which calls this.
1267
1245
 
1268
 
        :return: entry
 
1246
        Returns the new entry object.
1269
1247
        """
1270
1248
        if entry.file_id in self._byid:
1271
1249
            raise errors.DuplicateFileId(entry.file_id,
1272
1250
                                         self._byid[entry.file_id])
 
1251
 
1273
1252
        if entry.parent_id is None:
1274
1253
            self.root = entry
1275
1254
        else:
1276
1255
            try:
1277
1256
                parent = self._byid[entry.parent_id]
1278
1257
            except KeyError:
1279
 
                raise errors.InconsistentDelta("<unknown>", entry.parent_id,
1280
 
                    "Parent not in inventory.")
 
1258
                raise BzrError("parent_id {%s} not in inventory" %
 
1259
                               entry.parent_id)
 
1260
 
1281
1261
            if entry.name in parent.children:
1282
 
                raise errors.InconsistentDelta(
1283
 
                    self.id2path(parent.children[entry.name].file_id),
1284
 
                    entry.file_id,
1285
 
                    "Path already versioned")
 
1262
                raise BzrError("%s is already versioned" %
 
1263
                        osutils.pathjoin(self.id2path(parent.file_id),
 
1264
                        entry.name).encode('utf-8'))
1286
1265
            parent.children[entry.name] = entry
1287
1266
        return self._add_child(entry)
1288
1267
 
1481
1460
        self._fileid_to_entry_cache = {}
1482
1461
        self._path_to_fileid_cache = {}
1483
1462
        self._search_key_name = search_key_name
1484
 
        self.root_id = None
1485
 
 
1486
 
    def __eq__(self, other):
1487
 
        """Compare two sets by comparing their contents."""
1488
 
        if not isinstance(other, CHKInventory):
1489
 
            return NotImplemented
1490
 
 
1491
 
        this_key = self.id_to_entry.key()
1492
 
        other_key = other.id_to_entry.key()
1493
 
        this_pid_key = self.parent_id_basename_to_file_id.key()
1494
 
        other_pid_key = other.parent_id_basename_to_file_id.key()
1495
 
        if None in (this_key, this_pid_key, other_key, other_pid_key):
1496
 
            return False
1497
 
        return this_key == other_key and this_pid_key == other_pid_key
1498
1463
 
1499
1464
    def _entry_to_bytes(self, entry):
1500
1465
        """Serialise entry as a single bytestring.
1582
1547
    def _get_mutable_inventory(self):
1583
1548
        """See CommonInventory._get_mutable_inventory."""
1584
1549
        entries = self.iter_entries()
1585
 
        inv = Inventory(None, self.revision_id)
 
1550
        if self.root_id is not None:
 
1551
            entries.next()
 
1552
        inv = Inventory(self.root_id, self.revision_id)
1586
1553
        for path, inv_entry in entries:
1587
 
            inv.add(inv_entry.copy())
 
1554
            inv.add(inv_entry)
1588
1555
        return inv
1589
1556
 
1590
1557
    def create_by_apply_delta(self, inventory_delta, new_revision_id,
1591
1558
        propagate_caches=False):
1592
1559
        """Create a new CHKInventory by applying inventory_delta to this one.
1593
1560
 
1594
 
        See the inventory developers documentation for the theory behind
1595
 
        inventory deltas.
1596
 
 
1597
1561
        :param inventory_delta: The inventory delta to apply. See
1598
1562
            Inventory.apply_delta for details.
1599
1563
        :param new_revision_id: The revision id of the resulting CHKInventory.
1601
1565
          copied to and updated for the result.
1602
1566
        :return: The new CHKInventory.
1603
1567
        """
1604
 
        split = osutils.split
1605
1568
        result = CHKInventory(self._search_key_name)
1606
1569
        if propagate_caches:
1607
1570
            # Just propagate the path-to-fileid cache for now
1616
1579
            search_key_func=search_key_func)
1617
1580
        result.id_to_entry._ensure_root()
1618
1581
        result.id_to_entry._root_node.set_maximum_size(maximum_size)
1619
 
        # Change to apply to the parent_id_basename delta. The dict maps
1620
 
        # (parent_id, basename) -> (old_key, new_value). We use a dict because
1621
 
        # when a path has its id replaced (e.g. the root is changed, or someone
1622
 
        # does bzr mv a b, bzr mv c a, we should output a single change to this
1623
 
        # map rather than two.
1624
 
        parent_id_basename_delta = {}
 
1582
        parent_id_basename_delta = []
1625
1583
        if self.parent_id_basename_to_file_id is not None:
1626
1584
            result.parent_id_basename_to_file_id = chk_map.CHKMap(
1627
1585
                self.parent_id_basename_to_file_id._store,
1637
1595
            result.parent_id_basename_to_file_id = None
1638
1596
        result.root_id = self.root_id
1639
1597
        id_to_entry_delta = []
1640
 
        # inventory_delta is only traversed once, so we just update the
1641
 
        # variable.
1642
 
        # Check for repeated file ids
1643
 
        inventory_delta = _check_delta_unique_ids(inventory_delta)
1644
 
        # Repeated old paths
1645
 
        inventory_delta = _check_delta_unique_old_paths(inventory_delta)
1646
 
        # Check for repeated new paths
1647
 
        inventory_delta = _check_delta_unique_new_paths(inventory_delta)
1648
 
        # Check for entries that don't match the fileid
1649
 
        inventory_delta = _check_delta_ids_match_entry(inventory_delta)
1650
 
        # Check for nonsense fileids
1651
 
        inventory_delta = _check_delta_ids_are_valid(inventory_delta)
1652
 
        # Check for new_path <-> entry consistency
1653
 
        inventory_delta = _check_delta_new_path_entry_both_or_None(
1654
 
            inventory_delta)
1655
 
        # All changed entries need to have their parents be directories and be
1656
 
        # at the right path. This set contains (path, id) tuples.
1657
 
        parents = set()
1658
 
        # When we delete an item, all the children of it must be either deleted
1659
 
        # or altered in their own right. As we batch process the change via
1660
 
        # CHKMap.apply_delta, we build a set of things to use to validate the
1661
 
        # delta.
1662
 
        deletes = set()
1663
 
        altered = set()
1664
1598
        for old_path, new_path, file_id, entry in inventory_delta:
1665
1599
            # file id changes
1666
1600
            if new_path == '':
1675
1609
                        del result._path_to_fileid_cache[old_path]
1676
1610
                    except KeyError:
1677
1611
                        pass
1678
 
                deletes.add(file_id)
1679
1612
            else:
1680
1613
                new_key = (file_id,)
1681
1614
                new_value = result._entry_to_bytes(entry)
1682
1615
                # Update caches. It's worth doing this whether
1683
1616
                # we're propagating the old caches or not.
1684
1617
                result._path_to_fileid_cache[new_path] = file_id
1685
 
                parents.add((split(new_path)[0], entry.parent_id))
1686
1618
            if old_path is None:
1687
1619
                old_key = None
1688
1620
            else:
1689
1621
                old_key = (file_id,)
1690
 
                if self.id2path(file_id) != old_path:
1691
 
                    raise errors.InconsistentDelta(old_path, file_id,
1692
 
                        "Entry was at wrong other path %r." %
1693
 
                        self.id2path(file_id))
1694
 
                altered.add(file_id)
1695
1622
            id_to_entry_delta.append((old_key, new_key, new_value))
1696
1623
            if result.parent_id_basename_to_file_id is not None:
1697
1624
                # parent_id, basename changes
1706
1633
                else:
1707
1634
                    new_key = self._parent_id_basename_key(entry)
1708
1635
                    new_value = file_id
1709
 
                # If the two keys are the same, the value will be unchanged
1710
 
                # as its always the file id for this entry.
1711
1636
                if old_key != new_key:
1712
 
                    # Transform a change into explicit delete/add preserving
1713
 
                    # a possible match on the key from a different file id.
1714
 
                    if old_key is not None:
1715
 
                        parent_id_basename_delta.setdefault(
1716
 
                            old_key, [None, None])[0] = old_key
1717
 
                    if new_key is not None:
1718
 
                        parent_id_basename_delta.setdefault(
1719
 
                            new_key, [None, None])[1] = new_value
1720
 
        # validate that deletes are complete.
1721
 
        for file_id in deletes:
1722
 
            entry = self[file_id]
1723
 
            if entry.kind != 'directory':
1724
 
                continue
1725
 
            # This loop could potentially be better by using the id_basename
1726
 
            # map to just get the child file ids.
1727
 
            for child in entry.children.values():
1728
 
                if child.file_id not in altered:
1729
 
                    raise errors.InconsistentDelta(self.id2path(child.file_id),
1730
 
                        child.file_id, "Child not deleted or reparented when "
1731
 
                        "parent deleted.")
 
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))
1732
1640
        result.id_to_entry.apply_delta(id_to_entry_delta)
1733
1641
        if parent_id_basename_delta:
1734
 
            # Transform the parent_id_basename delta data into a linear delta
1735
 
            # with only one record for a given key. Optimally this would allow
1736
 
            # re-keying, but its simpler to just output that as a delete+add
1737
 
            # to spend less time calculating the delta.
1738
 
            delta_list = []
1739
 
            for key, (old_key, value) in parent_id_basename_delta.iteritems():
1740
 
                if value is not None:
1741
 
                    delta_list.append((old_key, key, value))
1742
 
                else:
1743
 
                    delta_list.append((old_key, None, None))
1744
 
            result.parent_id_basename_to_file_id.apply_delta(delta_list)
1745
 
        parents.discard(('', None))
1746
 
        for parent_path, parent in parents:
1747
 
            try:
1748
 
                if result[parent].kind != 'directory':
1749
 
                    raise errors.InconsistentDelta(result.id2path(parent), parent,
1750
 
                        'Not a directory, but given children')
1751
 
            except errors.NoSuchId:
1752
 
                raise errors.InconsistentDelta("<unknown>", parent,
1753
 
                    "Parent is not present in resulting inventory.")
1754
 
            if result.path2id(parent_path) != parent:
1755
 
                raise errors.InconsistentDelta(parent_path, parent,
1756
 
                    "Parent has wrong path %r." % result.path2id(parent_path))
 
1642
            result.parent_id_basename_to_file_id.apply_delta(parent_id_basename_delta)
1757
1643
        return result
1758
1644
 
1759
1645
    @classmethod
1823
1709
        :param maximum_size: The CHKMap node size limit.
1824
1710
        :param search_key_name: The identifier for the search key function
1825
1711
        """
1826
 
        result = klass(search_key_name)
 
1712
        result = CHKInventory(search_key_name)
1827
1713
        result.revision_id = inventory.revision_id
1828
1714
        result.root_id = inventory.root.file_id
1829
 
 
1830
 
        entry_to_bytes = result._entry_to_bytes
1831
 
        parent_id_basename_key = result._parent_id_basename_key
1832
 
        id_to_entry_dict = {}
1833
 
        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)
 
1718
        file_id_delta = []
 
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(
 
1722
            maximum_size)
 
1723
        result.parent_id_basename_to_file_id._root_node._key_width = 2
 
1724
        parent_id_delta = []
1834
1725
        for path, entry in inventory.iter_entries():
1835
 
            id_to_entry_dict[(entry.file_id,)] = entry_to_bytes(entry)
1836
 
            p_id_key = parent_id_basename_key(entry)
1837
 
            parent_id_basename_dict[p_id_key] = entry.file_id
1838
 
 
1839
 
        result._populate_from_dicts(chk_store, id_to_entry_dict,
1840
 
            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),
 
1730
                 entry.file_id))
 
1731
        result.id_to_entry.apply_delta(file_id_delta)
 
1732
        result.parent_id_basename_to_file_id.apply_delta(parent_id_delta)
1841
1733
        return result
1842
1734
 
1843
 
    def _populate_from_dicts(self, chk_store, id_to_entry_dict,
1844
 
                             parent_id_basename_dict, maximum_size):
1845
 
        search_key_func = chk_map.search_key_registry.get(self._search_key_name)
1846
 
        root_key = chk_map.CHKMap.from_dict(chk_store, id_to_entry_dict,
1847
 
                   maximum_size=maximum_size, key_width=1,
1848
 
                   search_key_func=search_key_func)
1849
 
        self.id_to_entry = chk_map.CHKMap(chk_store, root_key,
1850
 
                                          search_key_func)
1851
 
        root_key = chk_map.CHKMap.from_dict(chk_store,
1852
 
                   parent_id_basename_dict,
1853
 
                   maximum_size=maximum_size, key_width=2,
1854
 
                   search_key_func=search_key_func)
1855
 
        self.parent_id_basename_to_file_id = chk_map.CHKMap(chk_store,
1856
 
                                                    root_key, search_key_func)
1857
 
 
1858
1735
    def _parent_id_basename_key(self, entry):
1859
1736
        """Create a key for a entry in a parent_id_basename_to_file_id index."""
1860
1737
        if entry.parent_id is not None:
2018
1895
 
2019
1896
    def path2id(self, name):
2020
1897
        """See CommonInventory.path2id()."""
2021
 
        # TODO: perhaps support negative hits?
2022
1898
        result = self._path_to_fileid_cache.get(name, None)
2023
 
        if result is not None:
2024
 
            return result
2025
 
        if isinstance(name, basestring):
2026
 
            names = osutils.splitpath(name)
2027
 
        else:
2028
 
            names = name
2029
 
        current_id = self.root_id
2030
 
        if current_id is None:
2031
 
            return None
2032
 
        parent_id_index = self.parent_id_basename_to_file_id
2033
 
        for basename in names:
2034
 
            # TODO: Cache each path we figure out in this function.
2035
 
            basename_utf8 = basename.encode('utf8')
2036
 
            key_filter = [(current_id, basename_utf8)]
2037
 
            file_id = None
2038
 
            for (parent_id, name_utf8), file_id in parent_id_index.iteritems(
2039
 
                key_filter=key_filter):
2040
 
                if parent_id != current_id or name_utf8 != basename_utf8:
2041
 
                    raise errors.BzrError("corrupt inventory lookup! "
2042
 
                        "%r %r %r %r" % (parent_id, current_id, name_utf8,
2043
 
                        basename_utf8))
2044
 
            if file_id is None:
2045
 
                return None
2046
 
            current_id = file_id
2047
 
        self._path_to_fileid_cache[name] = current_id
2048
 
        return current_id
 
1899
        if result is None:
 
1900
            result = CommonInventory.path2id(self, name)
 
1901
            self._path_to_fileid_cache[name] = result
 
1902
        return result
2049
1903
 
2050
1904
    def to_lines(self):
2051
1905
        """Serialise the inventory to lines."""
2185
2039
        _NAME_RE = re.compile(r'^[^/\\]+$')
2186
2040
 
2187
2041
    return bool(_NAME_RE.match(name))
2188
 
 
2189
 
 
2190
 
def _check_delta_unique_ids(delta):
2191
 
    """Decorate a delta and check that the file ids in it are unique.
2192
 
 
2193
 
    :return: A generator over delta.
2194
 
    """
2195
 
    ids = set()
2196
 
    for item in delta:
2197
 
        length = len(ids) + 1
2198
 
        ids.add(item[2])
2199
 
        if len(ids) != length:
2200
 
            raise errors.InconsistentDelta(item[0] or item[1], item[2],
2201
 
                "repeated file_id")
2202
 
        yield item
2203
 
 
2204
 
 
2205
 
def _check_delta_unique_new_paths(delta):
2206
 
    """Decorate a delta and check that the new paths in it are unique.
2207
 
 
2208
 
    :return: A generator over delta.
2209
 
    """
2210
 
    paths = set()
2211
 
    for item in delta:
2212
 
        length = len(paths) + 1
2213
 
        path = item[1]
2214
 
        if path is not None:
2215
 
            paths.add(path)
2216
 
            if len(paths) != length:
2217
 
                raise errors.InconsistentDelta(path, item[2], "repeated path")
2218
 
        yield item
2219
 
 
2220
 
 
2221
 
def _check_delta_unique_old_paths(delta):
2222
 
    """Decorate a delta and check that the old paths in it are unique.
2223
 
 
2224
 
    :return: A generator over delta.
2225
 
    """
2226
 
    paths = set()
2227
 
    for item in delta:
2228
 
        length = len(paths) + 1
2229
 
        path = item[0]
2230
 
        if path is not None:
2231
 
            paths.add(path)
2232
 
            if len(paths) != length:
2233
 
                raise errors.InconsistentDelta(path, item[2], "repeated path")
2234
 
        yield item
2235
 
 
2236
 
 
2237
 
def _check_delta_ids_are_valid(delta):
2238
 
    """Decorate a delta and check that the ids in it are valid.
2239
 
 
2240
 
    :return: A generator over delta.
2241
 
    """
2242
 
    for item in delta:
2243
 
        entry = item[3]
2244
 
        if item[2] is None:
2245
 
            raise errors.InconsistentDelta(item[0] or item[1], item[2],
2246
 
                "entry with file_id None %r" % entry)
2247
 
        if type(item[2]) != str:
2248
 
            raise errors.InconsistentDelta(item[0] or item[1], item[2],
2249
 
                "entry with non bytes file_id %r" % entry)
2250
 
        yield item
2251
 
 
2252
 
 
2253
 
def _check_delta_ids_match_entry(delta):
2254
 
    """Decorate a delta and check that the ids in it match the entry.file_id.
2255
 
 
2256
 
    :return: A generator over delta.
2257
 
    """
2258
 
    for item in delta:
2259
 
        entry = item[3]
2260
 
        if entry is not None:
2261
 
            if entry.file_id != item[2]:
2262
 
                raise errors.InconsistentDelta(item[0] or item[1], item[2],
2263
 
                    "mismatched id with %r" % entry)
2264
 
        yield item
2265
 
 
2266
 
 
2267
 
def _check_delta_new_path_entry_both_or_None(delta):
2268
 
    """Decorate a delta and check that the new_path and entry are paired.
2269
 
 
2270
 
    :return: A generator over delta.
2271
 
    """
2272
 
    for item in delta:
2273
 
        new_path = item[1]
2274
 
        entry = item[3]
2275
 
        if new_path is None and entry is not None:
2276
 
            raise errors.InconsistentDelta(item[0], item[1],
2277
 
                "Entry with no new_path")
2278
 
        if new_path is not None and entry is None:
2279
 
            raise errors.InconsistentDelta(new_path, item[1],
2280
 
                "new_path with no entry")
2281
 
        yield item