~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Joe Julian
  • Date: 2010-01-10 02:25:31 UTC
  • mto: (4634.119.7 2.0)
  • mto: This revision was merged to the branch mainline in revision 4959.
  • Revision ID: joe@julianfamily.org-20100110022531-wqk61rsagz8xsiga
Added MANIFEST.in to allow bdist_rpm to have all the required include files and tools. bdist_rpm will still fail to build correctly on some distributions due to a disttools bug http://bugs.python.org/issue644744

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
 
 
32
30
from bzrlib.lazy_import import lazy_import
33
31
lazy_import(globals(), """
34
32
import collections
 
33
import copy
35
34
import os
36
35
import re
37
36
import tarfile
43
42
    generate_ids,
44
43
    osutils,
45
44
    symbol_versioning,
46
 
    workingtree,
47
45
    )
48
46
""")
49
47
 
262
260
    def versionable_kind(kind):
263
261
        return (kind in ('file', 'directory', 'symlink', 'tree-reference'))
264
262
 
265
 
    def check(self, checker, rev_id, inv, tree):
 
263
    def check(self, checker, rev_id, inv):
266
264
        """Check this inventory entry is intact.
267
265
 
268
266
        This is a template method, override _check for kind specific
274
272
        :param rev_id: Revision id from which this InventoryEntry was loaded.
275
273
             Not necessarily the last-changed revision for this file.
276
274
        :param inv: Inventory from which the entry was loaded.
277
 
        :param tree: RevisionTree for this entry.
278
275
        """
279
276
        if self.parent_id is not None:
280
277
            if not inv.has_id(self.parent_id):
281
278
                raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
282
279
                        % (self.parent_id, rev_id))
283
 
        self._check(checker, rev_id, tree)
 
280
        checker._add_entry_to_text_key_references(inv, self)
 
281
        self._check(checker, rev_id)
284
282
 
285
 
    def _check(self, checker, rev_id, tree):
 
283
    def _check(self, checker, rev_id):
286
284
        """Check this inventory entry for kind specific errors."""
287
 
        raise BzrCheckError('unknown entry kind %r in revision {%s}' %
288
 
                            (self.kind, rev_id))
 
285
        checker._report_items.append(
 
286
            'unknown entry kind %r in revision {%s}' % (self.kind, rev_id))
289
287
 
290
288
    def copy(self):
291
289
        """Clone this inventory entry."""
404
402
                 'text_id', 'parent_id', 'children', 'executable',
405
403
                 'revision', 'symlink_target', 'reference_revision']
406
404
 
407
 
    def _check(self, checker, rev_id, tree):
 
405
    def _check(self, checker, rev_id):
408
406
        """See InventoryEntry._check"""
409
407
 
410
408
    def __init__(self, file_id):
433
431
                 'text_id', 'parent_id', 'children', 'executable',
434
432
                 'revision', 'symlink_target', 'reference_revision']
435
433
 
436
 
    def _check(self, checker, rev_id, tree):
 
434
    def _check(self, checker, rev_id):
437
435
        """See InventoryEntry._check"""
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}'
 
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}'
440
439
                                % (self.file_id, rev_id))
 
440
        # In non rich root repositories we do not expect a file graph for the
 
441
        # root.
 
442
        if self.name == '' and not checker.rich_roots:
 
443
            return
 
444
        # Directories are stored as an empty file, but the file should exist
 
445
        # to provide a per-fileid log. The hash of every directory content is
 
446
        # "da..." below (the sha1sum of '').
 
447
        checker.add_pending_item(rev_id,
 
448
            ('texts', self.file_id, self.revision), 'text',
 
449
             'da39a3ee5e6b4b0d3255bfef95601890afd80709')
441
450
 
442
451
    def copy(self):
443
452
        other = InventoryDirectory(self.file_id, self.name, self.parent_id)
476
485
                 'text_id', 'parent_id', 'children', 'executable',
477
486
                 'revision', 'symlink_target', 'reference_revision']
478
487
 
479
 
    def _check(self, checker, tree_revision_id, tree):
 
488
    def _check(self, checker, tree_revision_id):
480
489
        """See InventoryEntry._check"""
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
 
490
        # TODO: check size too.
 
491
        checker.add_pending_item(tree_revision_id,
 
492
            ('texts', self.file_id, self.revision), 'text',
 
493
             self.text_sha1)
 
494
        if self.text_size is None:
 
495
            checker._report_items.append(
 
496
                'fileid {%s} in {%s} has None for text_size' % (self.file_id,
 
497
                tree_revision_id))
500
498
 
501
499
    def copy(self):
502
500
        other = InventoryFile(self.file_id, self.name, self.parent_id)
600
598
                 'text_id', 'parent_id', 'children', 'executable',
601
599
                 'revision', 'symlink_target', 'reference_revision']
602
600
 
603
 
    def _check(self, checker, rev_id, tree):
 
601
    def _check(self, checker, tree_revision_id):
604
602
        """See InventoryEntry._check"""
605
603
        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))
 
604
            checker._report_items.append(
 
605
               'symlink {%s} has text in revision {%s}'
 
606
                    % (self.file_id, tree_revision_id))
608
607
        if self.symlink_target is None:
609
 
            raise BzrCheckError('symlink {%s} has no target in revision {%s}'
610
 
                    % (self.file_id, rev_id))
 
608
            checker._report_items.append(
 
609
                'symlink {%s} has no target in revision {%s}'
 
610
                    % (self.file_id, tree_revision_id))
 
611
        # Symlinks are stored as ''
 
612
        checker.add_pending_item(tree_revision_id,
 
613
            ('texts', self.file_id, self.revision), 'text',
 
614
             'da39a3ee5e6b4b0d3255bfef95601890afd80709')
611
615
 
612
616
    def copy(self):
613
617
        other = InventoryLink(self.file_id, self.name, self.parent_id)
714
718
 
715
719
 
716
720
class CommonInventory(object):
717
 
    """Basic inventory logic, defined in terms of primitives like has_id."""
 
721
    """Basic inventory logic, defined in terms of primitives like has_id.
 
722
 
 
723
    An inventory is the metadata about the contents of a tree.
 
724
 
 
725
    This is broadly a map from file_id to entries such as directories, files,
 
726
    symlinks and tree references. Each entry maintains its own metadata like
 
727
    SHA1 and length for files, or children for a directory.
 
728
 
 
729
    Entries can be looked up either by path or by file_id.
 
730
 
 
731
    InventoryEntry objects must not be modified after they are
 
732
    inserted, other than through the Inventory API.
 
733
    """
718
734
 
719
735
    def __contains__(self, file_id):
720
736
        """True if this entry contains a file with given id.
733
749
        """
734
750
        return self.has_id(file_id)
735
751
 
 
752
    def has_filename(self, filename):
 
753
        return bool(self.path2id(filename))
 
754
 
736
755
    def id2path(self, file_id):
737
756
        """Return as a string the path to file_id.
738
757
 
741
760
        >>> e = i.add(InventoryFile('foo-id', 'foo.c', parent_id='src-id'))
742
761
        >>> print i.id2path('foo-id')
743
762
        src/foo.c
 
763
 
 
764
        :raises NoSuchId: If file_id is not present in the inventory.
744
765
        """
745
766
        # get all names, skipping root
746
767
        return '/'.join(reversed(
747
768
            [parent.name for parent in
748
769
             self._iter_file_id_parents(file_id)][:-1]))
749
770
 
750
 
    def iter_entries(self, from_dir=None):
751
 
        """Return (path, entry) pairs, in order by name."""
 
771
    def iter_entries(self, from_dir=None, recursive=True):
 
772
        """Return (path, entry) pairs, in order by name.
 
773
        
 
774
        :param from_dir: if None, start from the root,
 
775
          otherwise start from this directory (either file-id or entry)
 
776
        :param recursive: recurse into directories or not
 
777
        """
752
778
        if from_dir is None:
753
779
            if self.root is None:
754
780
                return
761
787
        # 440ms/663ms (inline/total) to 116ms/116ms
762
788
        children = from_dir.children.items()
763
789
        children.sort()
 
790
        if not recursive:
 
791
            for name, ie in children:
 
792
                yield name, ie
 
793
            return
764
794
        children = collections.deque(children)
765
795
        stack = [(u'', children)]
766
796
        while stack:
1012
1042
 
1013
1043
 
1014
1044
class Inventory(CommonInventory):
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
 
1045
    """Mutable dict based in-memory inventory.
 
1046
 
 
1047
    We never store the full path to a file, because renaming a directory
 
1048
    implicitly moves all of its contents.  This class internally maintains a
1025
1049
    lookup tree that allows the children under a directory to be
1026
1050
    returned quickly.
1027
1051
 
1028
 
    InventoryEntry objects must not be modified after they are
1029
 
    inserted, other than through the Inventory API.
1030
 
 
1031
1052
    >>> inv = Inventory()
1032
1053
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
1033
1054
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1034
1055
    >>> inv['123-123'].name
1035
1056
    'hello.c'
1036
1057
 
1037
 
    May be treated as an iterator or set to look up file ids:
 
1058
    Id's may be looked up from paths:
1038
1059
 
1039
 
    >>> bool(inv.path2id('hello.c'))
1040
 
    True
 
1060
    >>> inv.path2id('hello.c')
 
1061
    '123-123'
1041
1062
    >>> '123-123' in inv
1042
1063
    True
1043
1064
 
1044
 
    May also look up by name:
 
1065
    There are iterators over the contents:
1045
1066
 
1046
 
    >>> [x[0] for x in inv.iter_entries()]
 
1067
    >>> [entry[0] for entry in inv.iter_entries()]
1047
1068
    ['', 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)
1054
1069
    """
 
1070
 
1055
1071
    def __init__(self, root_id=ROOT_ID, revision_id=None):
1056
1072
        """Create or read an inventory.
1057
1073
 
1081
1097
    def apply_delta(self, delta):
1082
1098
        """Apply a delta to this inventory.
1083
1099
 
 
1100
        See the inventory developers documentation for the theory behind
 
1101
        inventory deltas.
 
1102
 
 
1103
        If delta application fails the inventory is left in an indeterminate
 
1104
        state and must not be used.
 
1105
 
1084
1106
        :param delta: A list of changes to apply. After all the changes are
1085
1107
            applied the final inventory must be internally consistent, but it
1086
1108
            is ok to supply changes which, if only half-applied would have an
1117
1139
        """
1118
1140
        # Check that the delta is legal. It would be nice if this could be
1119
1141
        # done within the loops below but it's safer to validate the 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
 
1142
        # before starting to mutate the inventory, as there isn't a rollback
 
1143
        # facility.
 
1144
        list(_check_delta_unique_ids(_check_delta_unique_new_paths(
 
1145
            _check_delta_unique_old_paths(_check_delta_ids_match_entry(
 
1146
            _check_delta_ids_are_valid(
 
1147
            _check_delta_new_path_entry_both_or_None(
 
1148
            delta)))))))
1126
1149
 
1127
1150
        children = {}
1128
1151
        # Remove all affected items which were in the original inventory,
1131
1154
        # modified children remaining by the time we examine it.
1132
1155
        for old_path, file_id in sorted(((op, f) for op, np, f, e in delta
1133
1156
                                        if op is not None), reverse=True):
1134
 
            if file_id not in self:
1135
 
                # adds come later
1136
 
                continue
1137
1157
            # Preserve unaltered children of file_id for later reinsertion.
1138
1158
            file_id_children = getattr(self[file_id], 'children', {})
1139
1159
            if len(file_id_children):
1140
1160
                children[file_id] = file_id_children
 
1161
            if self.id2path(file_id) != old_path:
 
1162
                raise errors.InconsistentDelta(old_path, file_id,
 
1163
                    "Entry was at wrong other path %r." % self.id2path(file_id))
1141
1164
            # Remove file_id and the unaltered children. If file_id is not
1142
1165
            # being deleted it will be reinserted back later.
1143
1166
            self.remove_recursive_id(file_id)
1146
1169
        # longest, ensuring that items which were modified and whose parents in
1147
1170
        # the resulting inventory were also modified, are inserted after their
1148
1171
        # parents.
1149
 
        for new_path, new_entry in sorted((np, e) for op, np, f, e in
 
1172
        for new_path, f, new_entry in sorted((np, f, e) for op, np, f, e in
1150
1173
                                          delta if np is not None):
1151
1174
            if new_entry.kind == 'directory':
1152
1175
                # Pop the child which to allow detection of children whose
1157
1180
                replacement.revision = new_entry.revision
1158
1181
                replacement.children = children.pop(replacement.file_id, {})
1159
1182
                new_entry = replacement
1160
 
            self.add(new_entry)
 
1183
            try:
 
1184
                self.add(new_entry)
 
1185
            except errors.DuplicateFileId:
 
1186
                raise errors.InconsistentDelta(new_path, new_entry.file_id,
 
1187
                    "New id is already present in target.")
 
1188
            except AttributeError:
 
1189
                raise errors.InconsistentDelta(new_path, new_entry.file_id,
 
1190
                    "Parent is not a directory.")
 
1191
            if self.id2path(new_entry.file_id) != new_path:
 
1192
                raise errors.InconsistentDelta(new_path, new_entry.file_id,
 
1193
                    "New path is not consistent with parent path.")
1161
1194
        if len(children):
1162
1195
            # Get the parent id that was deleted
1163
1196
            parent_id, children = children.popitem()
1164
1197
            raise errors.InconsistentDelta("<deleted>", parent_id,
1165
1198
                "The file id was deleted but its children were not deleted.")
1166
1199
 
 
1200
    def create_by_apply_delta(self, inventory_delta, new_revision_id,
 
1201
                              propagate_caches=False):
 
1202
        """See CHKInventory.create_by_apply_delta()"""
 
1203
        new_inv = self.copy()
 
1204
        new_inv.apply_delta(inventory_delta)
 
1205
        new_inv.revision_id = new_revision_id
 
1206
        return new_inv
 
1207
 
1167
1208
    def _set_root(self, ie):
1168
1209
        self.root = ie
1169
1210
        self._byid = {self.root.file_id: self.root}
1183
1224
 
1184
1225
    def _get_mutable_inventory(self):
1185
1226
        """See CommonInventory._get_mutable_inventory."""
1186
 
        return deepcopy(self)
 
1227
        return copy.deepcopy(self)
1187
1228
 
1188
1229
    def __iter__(self):
1189
1230
        """Iterate over all file-ids."""
1243
1284
        To add  a file to a branch ready to be committed, use Branch.add,
1244
1285
        which calls this.
1245
1286
 
1246
 
        Returns the new entry object.
 
1287
        :return: entry
1247
1288
        """
1248
1289
        if entry.file_id in self._byid:
1249
1290
            raise errors.DuplicateFileId(entry.file_id,
1250
1291
                                         self._byid[entry.file_id])
1251
 
 
1252
1292
        if entry.parent_id is None:
1253
1293
            self.root = entry
1254
1294
        else:
1255
1295
            try:
1256
1296
                parent = self._byid[entry.parent_id]
1257
1297
            except KeyError:
1258
 
                raise BzrError("parent_id {%s} not in inventory" %
1259
 
                               entry.parent_id)
1260
 
 
 
1298
                raise errors.InconsistentDelta("<unknown>", entry.parent_id,
 
1299
                    "Parent not in inventory.")
1261
1300
            if entry.name in parent.children:
1262
 
                raise BzrError("%s is already versioned" %
1263
 
                        osutils.pathjoin(self.id2path(parent.file_id),
1264
 
                        entry.name).encode('utf-8'))
 
1301
                raise errors.InconsistentDelta(
 
1302
                    self.id2path(parent.children[entry.name].file_id),
 
1303
                    entry.file_id,
 
1304
                    "Path already versioned")
1265
1305
            parent.children[entry.name] = entry
1266
1306
        return self._add_child(entry)
1267
1307
 
1342
1382
            yield ie
1343
1383
            file_id = ie.parent_id
1344
1384
 
1345
 
    def has_filename(self, filename):
1346
 
        return bool(self.path2id(filename))
1347
 
 
1348
1385
    def has_id(self, file_id):
1349
1386
        return (file_id in self._byid)
1350
1387
 
1460
1497
        self._fileid_to_entry_cache = {}
1461
1498
        self._path_to_fileid_cache = {}
1462
1499
        self._search_key_name = search_key_name
 
1500
        self.root_id = None
 
1501
 
 
1502
    def __eq__(self, other):
 
1503
        """Compare two sets by comparing their contents."""
 
1504
        if not isinstance(other, CHKInventory):
 
1505
            return NotImplemented
 
1506
 
 
1507
        this_key = self.id_to_entry.key()
 
1508
        other_key = other.id_to_entry.key()
 
1509
        this_pid_key = self.parent_id_basename_to_file_id.key()
 
1510
        other_pid_key = other.parent_id_basename_to_file_id.key()
 
1511
        if None in (this_key, this_pid_key, other_key, other_pid_key):
 
1512
            return False
 
1513
        return this_key == other_key and this_pid_key == other_pid_key
1463
1514
 
1464
1515
    def _entry_to_bytes(self, entry):
1465
1516
        """Serialise entry as a single bytestring.
1503
1554
        else:
1504
1555
            raise ValueError("unknown kind %r" % entry.kind)
1505
1556
 
 
1557
    def _expand_fileids_to_parents_and_children(self, file_ids):
 
1558
        """Give a more wholistic view starting with the given file_ids.
 
1559
 
 
1560
        For any file_id which maps to a directory, we will include all children
 
1561
        of that directory. We will also include all directories which are
 
1562
        parents of the given file_ids, but we will not include their children.
 
1563
 
 
1564
        eg:
 
1565
          /     # TREE_ROOT
 
1566
          foo/  # foo-id
 
1567
            baz # baz-id
 
1568
            frob/ # frob-id
 
1569
              fringle # fringle-id
 
1570
          bar/  # bar-id
 
1571
            bing # bing-id
 
1572
 
 
1573
        if given [foo-id] we will include
 
1574
            TREE_ROOT as interesting parents
 
1575
        and 
 
1576
            foo-id, baz-id, frob-id, fringle-id
 
1577
        As interesting ids.
 
1578
        """
 
1579
        interesting = set()
 
1580
        # TODO: Pre-pass over the list of fileids to see if anything is already
 
1581
        #       deserialized in self._fileid_to_entry_cache
 
1582
 
 
1583
        directories_to_expand = set()
 
1584
        children_of_parent_id = {}
 
1585
        # It is okay if some of the fileids are missing
 
1586
        for entry in self._getitems(file_ids):
 
1587
            if entry.kind == 'directory':
 
1588
                directories_to_expand.add(entry.file_id)
 
1589
            interesting.add(entry.parent_id)
 
1590
            children_of_parent_id.setdefault(entry.parent_id, []
 
1591
                                             ).append(entry.file_id)
 
1592
 
 
1593
        # Now, interesting has all of the direct parents, but not the
 
1594
        # parents of those parents. It also may have some duplicates with
 
1595
        # specific_fileids
 
1596
        remaining_parents = interesting.difference(file_ids)
 
1597
        # When we hit the TREE_ROOT, we'll get an interesting parent of None,
 
1598
        # but we don't actually want to recurse into that
 
1599
        interesting.add(None) # this will auto-filter it in the loop
 
1600
        remaining_parents.discard(None) 
 
1601
        while remaining_parents:
 
1602
            if None in remaining_parents:
 
1603
                import pdb; pdb.set_trace()
 
1604
            next_parents = set()
 
1605
            for entry in self._getitems(remaining_parents):
 
1606
                next_parents.add(entry.parent_id)
 
1607
                children_of_parent_id.setdefault(entry.parent_id, []
 
1608
                                                 ).append(entry.file_id)
 
1609
            # Remove any search tips we've already processed
 
1610
            remaining_parents = next_parents.difference(interesting)
 
1611
            interesting.update(remaining_parents)
 
1612
            # We should probably also .difference(directories_to_expand)
 
1613
        interesting.update(file_ids)
 
1614
        interesting.discard(None)
 
1615
        while directories_to_expand:
 
1616
            # Expand directories by looking in the
 
1617
            # parent_id_basename_to_file_id map
 
1618
            keys = [(f,) for f in directories_to_expand]
 
1619
            directories_to_expand = set()
 
1620
            items = self.parent_id_basename_to_file_id.iteritems(keys)
 
1621
            next_file_ids = set([item[1] for item in items])
 
1622
            next_file_ids = next_file_ids.difference(interesting)
 
1623
            interesting.update(next_file_ids)
 
1624
            for entry in self._getitems(next_file_ids):
 
1625
                if entry.kind == 'directory':
 
1626
                    directories_to_expand.add(entry.file_id)
 
1627
                children_of_parent_id.setdefault(entry.parent_id, []
 
1628
                                                 ).append(entry.file_id)
 
1629
        return interesting, children_of_parent_id
 
1630
 
 
1631
    def filter(self, specific_fileids):
 
1632
        """Get an inventory view filtered against a set of file-ids.
 
1633
 
 
1634
        Children of directories and parents are included.
 
1635
 
 
1636
        The result may or may not reference the underlying inventory
 
1637
        so it should be treated as immutable.
 
1638
        """
 
1639
        (interesting,
 
1640
         parent_to_children) = self._expand_fileids_to_parents_and_children(
 
1641
                                specific_fileids)
 
1642
        # There is some overlap here, but we assume that all interesting items
 
1643
        # are in the _fileid_to_entry_cache because we had to read them to
 
1644
        # determine if they were a dir we wanted to recurse, or just a file
 
1645
        # This should give us all the entries we'll want to add, so start
 
1646
        # adding
 
1647
        other = Inventory(self.root_id)
 
1648
        other.root.revision = self.root.revision
 
1649
        other.revision_id = self.revision_id
 
1650
        if not interesting or not parent_to_children:
 
1651
            # empty filter, or filtering entrys that don't exist
 
1652
            # (if even 1 existed, then we would have populated
 
1653
            # parent_to_children with at least the tree root.)
 
1654
            return other
 
1655
        cache = self._fileid_to_entry_cache
 
1656
        try:
 
1657
            remaining_children = collections.deque(parent_to_children[self.root_id])
 
1658
        except:
 
1659
            import pdb; pdb.set_trace()
 
1660
            raise
 
1661
        while remaining_children:
 
1662
            file_id = remaining_children.popleft()
 
1663
            ie = cache[file_id]
 
1664
            if ie.kind == 'directory':
 
1665
                ie = ie.copy() # We create a copy to depopulate the .children attribute
 
1666
            # TODO: depending on the uses of 'other' we should probably alwyas
 
1667
            #       '.copy()' to prevent someone from mutating other and
 
1668
            #       invaliding our internal cache
 
1669
            other.add(ie)
 
1670
            if file_id in parent_to_children:
 
1671
                remaining_children.extend(parent_to_children[file_id])
 
1672
        return other
 
1673
 
1506
1674
    @staticmethod
1507
1675
    def _bytes_to_utf8name_key(bytes):
1508
1676
        """Get the file_id, revision_id key out of bytes."""
1547
1715
    def _get_mutable_inventory(self):
1548
1716
        """See CommonInventory._get_mutable_inventory."""
1549
1717
        entries = self.iter_entries()
1550
 
        if self.root_id is not None:
1551
 
            entries.next()
1552
 
        inv = Inventory(self.root_id, self.revision_id)
 
1718
        inv = Inventory(None, self.revision_id)
1553
1719
        for path, inv_entry in entries:
1554
 
            inv.add(inv_entry)
 
1720
            inv.add(inv_entry.copy())
1555
1721
        return inv
1556
1722
 
1557
1723
    def create_by_apply_delta(self, inventory_delta, new_revision_id,
1558
1724
        propagate_caches=False):
1559
1725
        """Create a new CHKInventory by applying inventory_delta to this one.
1560
1726
 
 
1727
        See the inventory developers documentation for the theory behind
 
1728
        inventory deltas.
 
1729
 
1561
1730
        :param inventory_delta: The inventory delta to apply. See
1562
1731
            Inventory.apply_delta for details.
1563
1732
        :param new_revision_id: The revision id of the resulting CHKInventory.
1565
1734
          copied to and updated for the result.
1566
1735
        :return: The new CHKInventory.
1567
1736
        """
 
1737
        split = osutils.split
1568
1738
        result = CHKInventory(self._search_key_name)
1569
1739
        if propagate_caches:
1570
1740
            # Just propagate the path-to-fileid cache for now
1579
1749
            search_key_func=search_key_func)
1580
1750
        result.id_to_entry._ensure_root()
1581
1751
        result.id_to_entry._root_node.set_maximum_size(maximum_size)
1582
 
        parent_id_basename_delta = []
 
1752
        # Change to apply to the parent_id_basename delta. The dict maps
 
1753
        # (parent_id, basename) -> (old_key, new_value). We use a dict because
 
1754
        # when a path has its id replaced (e.g. the root is changed, or someone
 
1755
        # does bzr mv a b, bzr mv c a, we should output a single change to this
 
1756
        # map rather than two.
 
1757
        parent_id_basename_delta = {}
1583
1758
        if self.parent_id_basename_to_file_id is not None:
1584
1759
            result.parent_id_basename_to_file_id = chk_map.CHKMap(
1585
1760
                self.parent_id_basename_to_file_id._store,
1595
1770
            result.parent_id_basename_to_file_id = None
1596
1771
        result.root_id = self.root_id
1597
1772
        id_to_entry_delta = []
 
1773
        # inventory_delta is only traversed once, so we just update the
 
1774
        # variable.
 
1775
        # Check for repeated file ids
 
1776
        inventory_delta = _check_delta_unique_ids(inventory_delta)
 
1777
        # Repeated old paths
 
1778
        inventory_delta = _check_delta_unique_old_paths(inventory_delta)
 
1779
        # Check for repeated new paths
 
1780
        inventory_delta = _check_delta_unique_new_paths(inventory_delta)
 
1781
        # Check for entries that don't match the fileid
 
1782
        inventory_delta = _check_delta_ids_match_entry(inventory_delta)
 
1783
        # Check for nonsense fileids
 
1784
        inventory_delta = _check_delta_ids_are_valid(inventory_delta)
 
1785
        # Check for new_path <-> entry consistency
 
1786
        inventory_delta = _check_delta_new_path_entry_both_or_None(
 
1787
            inventory_delta)
 
1788
        # All changed entries need to have their parents be directories and be
 
1789
        # at the right path. This set contains (path, id) tuples.
 
1790
        parents = set()
 
1791
        # When we delete an item, all the children of it must be either deleted
 
1792
        # or altered in their own right. As we batch process the change via
 
1793
        # CHKMap.apply_delta, we build a set of things to use to validate the
 
1794
        # delta.
 
1795
        deletes = set()
 
1796
        altered = set()
1598
1797
        for old_path, new_path, file_id, entry in inventory_delta:
1599
1798
            # file id changes
1600
1799
            if new_path == '':
1609
1808
                        del result._path_to_fileid_cache[old_path]
1610
1809
                    except KeyError:
1611
1810
                        pass
 
1811
                deletes.add(file_id)
1612
1812
            else:
1613
1813
                new_key = (file_id,)
1614
1814
                new_value = result._entry_to_bytes(entry)
1615
1815
                # Update caches. It's worth doing this whether
1616
1816
                # we're propagating the old caches or not.
1617
1817
                result._path_to_fileid_cache[new_path] = file_id
 
1818
                parents.add((split(new_path)[0], entry.parent_id))
1618
1819
            if old_path is None:
1619
1820
                old_key = None
1620
1821
            else:
1621
1822
                old_key = (file_id,)
 
1823
                if self.id2path(file_id) != old_path:
 
1824
                    raise errors.InconsistentDelta(old_path, file_id,
 
1825
                        "Entry was at wrong other path %r." %
 
1826
                        self.id2path(file_id))
 
1827
                altered.add(file_id)
1622
1828
            id_to_entry_delta.append((old_key, new_key, new_value))
1623
1829
            if result.parent_id_basename_to_file_id is not None:
1624
1830
                # parent_id, basename changes
1633
1839
                else:
1634
1840
                    new_key = self._parent_id_basename_key(entry)
1635
1841
                    new_value = file_id
 
1842
                # If the two keys are the same, the value will be unchanged
 
1843
                # as its always the file id for this entry.
1636
1844
                if old_key != new_key:
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))
 
1845
                    # Transform a change into explicit delete/add preserving
 
1846
                    # a possible match on the key from a different file id.
 
1847
                    if old_key is not None:
 
1848
                        parent_id_basename_delta.setdefault(
 
1849
                            old_key, [None, None])[0] = old_key
 
1850
                    if new_key is not None:
 
1851
                        parent_id_basename_delta.setdefault(
 
1852
                            new_key, [None, None])[1] = new_value
 
1853
        # validate that deletes are complete.
 
1854
        for file_id in deletes:
 
1855
            entry = self[file_id]
 
1856
            if entry.kind != 'directory':
 
1857
                continue
 
1858
            # This loop could potentially be better by using the id_basename
 
1859
            # map to just get the child file ids.
 
1860
            for child in entry.children.values():
 
1861
                if child.file_id not in altered:
 
1862
                    raise errors.InconsistentDelta(self.id2path(child.file_id),
 
1863
                        child.file_id, "Child not deleted or reparented when "
 
1864
                        "parent deleted.")
1640
1865
        result.id_to_entry.apply_delta(id_to_entry_delta)
1641
1866
        if parent_id_basename_delta:
1642
 
            result.parent_id_basename_to_file_id.apply_delta(parent_id_basename_delta)
 
1867
            # Transform the parent_id_basename delta data into a linear delta
 
1868
            # with only one record for a given key. Optimally this would allow
 
1869
            # re-keying, but its simpler to just output that as a delete+add
 
1870
            # to spend less time calculating the delta.
 
1871
            delta_list = []
 
1872
            for key, (old_key, value) in parent_id_basename_delta.iteritems():
 
1873
                if value is not None:
 
1874
                    delta_list.append((old_key, key, value))
 
1875
                else:
 
1876
                    delta_list.append((old_key, None, None))
 
1877
            result.parent_id_basename_to_file_id.apply_delta(delta_list)
 
1878
        parents.discard(('', None))
 
1879
        for parent_path, parent in parents:
 
1880
            try:
 
1881
                if result[parent].kind != 'directory':
 
1882
                    raise errors.InconsistentDelta(result.id2path(parent), parent,
 
1883
                        'Not a directory, but given children')
 
1884
            except errors.NoSuchId:
 
1885
                raise errors.InconsistentDelta("<unknown>", parent,
 
1886
                    "Parent is not present in resulting inventory.")
 
1887
            if result.path2id(parent_path) != parent:
 
1888
                raise errors.InconsistentDelta(parent_path, parent,
 
1889
                    "Parent has wrong path %r." % result.path2id(parent_path))
1643
1890
        return result
1644
1891
 
1645
1892
    @classmethod
1709
1956
        :param maximum_size: The CHKMap node size limit.
1710
1957
        :param search_key_name: The identifier for the search key function
1711
1958
        """
1712
 
        result = CHKInventory(search_key_name)
 
1959
        result = klass(search_key_name)
1713
1960
        result.revision_id = inventory.revision_id
1714
1961
        result.root_id = inventory.root.file_id
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 = []
 
1962
 
 
1963
        entry_to_bytes = result._entry_to_bytes
 
1964
        parent_id_basename_key = result._parent_id_basename_key
 
1965
        id_to_entry_dict = {}
 
1966
        parent_id_basename_dict = {}
1725
1967
        for path, entry in inventory.iter_entries():
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)
 
1968
            id_to_entry_dict[(entry.file_id,)] = entry_to_bytes(entry)
 
1969
            p_id_key = parent_id_basename_key(entry)
 
1970
            parent_id_basename_dict[p_id_key] = entry.file_id
 
1971
 
 
1972
        result._populate_from_dicts(chk_store, id_to_entry_dict,
 
1973
            parent_id_basename_dict, maximum_size=maximum_size)
1733
1974
        return result
1734
1975
 
 
1976
    def _populate_from_dicts(self, chk_store, id_to_entry_dict,
 
1977
                             parent_id_basename_dict, maximum_size):
 
1978
        search_key_func = chk_map.search_key_registry.get(self._search_key_name)
 
1979
        root_key = chk_map.CHKMap.from_dict(chk_store, id_to_entry_dict,
 
1980
                   maximum_size=maximum_size, key_width=1,
 
1981
                   search_key_func=search_key_func)
 
1982
        self.id_to_entry = chk_map.CHKMap(chk_store, root_key,
 
1983
                                          search_key_func)
 
1984
        root_key = chk_map.CHKMap.from_dict(chk_store,
 
1985
                   parent_id_basename_dict,
 
1986
                   maximum_size=maximum_size, key_width=2,
 
1987
                   search_key_func=search_key_func)
 
1988
        self.parent_id_basename_to_file_id = chk_map.CHKMap(chk_store,
 
1989
                                                    root_key, search_key_func)
 
1990
 
1735
1991
    def _parent_id_basename_key(self, entry):
1736
1992
        """Create a key for a entry in a parent_id_basename_to_file_id index."""
1737
1993
        if entry.parent_id is not None:
1754
2010
            # really we're passing an inventory, not a tree...
1755
2011
            raise errors.NoSuchId(self, file_id)
1756
2012
 
 
2013
    def _getitems(self, file_ids):
 
2014
        """Similar to __getitem__, but lets you query for multiple.
 
2015
        
 
2016
        The returned order is undefined. And currently if an item doesn't
 
2017
        exist, it isn't included in the output.
 
2018
        """
 
2019
        result = []
 
2020
        remaining = []
 
2021
        for file_id in file_ids:
 
2022
            entry = self._fileid_to_entry_cache.get(file_id, None)
 
2023
            if entry is None:
 
2024
                remaining.append(file_id)
 
2025
            else:
 
2026
                result.append(entry)
 
2027
        file_keys = [(f,) for f in remaining]
 
2028
        for file_key, value in self.id_to_entry.iteritems(file_keys):
 
2029
            entry = self._bytes_to_entry(value)
 
2030
            result.append(entry)
 
2031
            self._fileid_to_entry_cache[entry.file_id] = entry
 
2032
        return result
 
2033
 
1757
2034
    def has_id(self, file_id):
1758
2035
        # Perhaps have an explicit 'contains' method on CHKMap ?
1759
2036
        if self._fileid_to_entry_cache.get(file_id, None) is not None:
1895
2172
 
1896
2173
    def path2id(self, name):
1897
2174
        """See CommonInventory.path2id()."""
 
2175
        # TODO: perhaps support negative hits?
1898
2176
        result = self._path_to_fileid_cache.get(name, None)
1899
 
        if result is None:
1900
 
            result = CommonInventory.path2id(self, name)
1901
 
            self._path_to_fileid_cache[name] = result
1902
 
        return result
 
2177
        if result is not None:
 
2178
            return result
 
2179
        if isinstance(name, basestring):
 
2180
            names = osutils.splitpath(name)
 
2181
        else:
 
2182
            names = name
 
2183
        current_id = self.root_id
 
2184
        if current_id is None:
 
2185
            return None
 
2186
        parent_id_index = self.parent_id_basename_to_file_id
 
2187
        for basename in names:
 
2188
            # TODO: Cache each path we figure out in this function.
 
2189
            basename_utf8 = basename.encode('utf8')
 
2190
            key_filter = [(current_id, basename_utf8)]
 
2191
            file_id = None
 
2192
            for (parent_id, name_utf8), file_id in parent_id_index.iteritems(
 
2193
                key_filter=key_filter):
 
2194
                if parent_id != current_id or name_utf8 != basename_utf8:
 
2195
                    raise errors.BzrError("corrupt inventory lookup! "
 
2196
                        "%r %r %r %r" % (parent_id, current_id, name_utf8,
 
2197
                        basename_utf8))
 
2198
            if file_id is None:
 
2199
                return None
 
2200
            current_id = file_id
 
2201
        self._path_to_fileid_cache[name] = current_id
 
2202
        return current_id
1903
2203
 
1904
2204
    def to_lines(self):
1905
2205
        """Serialise the inventory to lines."""
2005
2305
    try:
2006
2306
        factory = entry_factory[kind]
2007
2307
    except KeyError:
2008
 
        raise BzrError("unknown kind %r" % kind)
 
2308
        raise errors.BadFileKindError(name, kind)
2009
2309
    return factory(file_id, name, parent_id)
2010
2310
 
2011
2311
 
2039
2339
        _NAME_RE = re.compile(r'^[^/\\]+$')
2040
2340
 
2041
2341
    return bool(_NAME_RE.match(name))
 
2342
 
 
2343
 
 
2344
def _check_delta_unique_ids(delta):
 
2345
    """Decorate a delta and check that the file ids in it are unique.
 
2346
 
 
2347
    :return: A generator over delta.
 
2348
    """
 
2349
    ids = set()
 
2350
    for item in delta:
 
2351
        length = len(ids) + 1
 
2352
        ids.add(item[2])
 
2353
        if len(ids) != length:
 
2354
            raise errors.InconsistentDelta(item[0] or item[1], item[2],
 
2355
                "repeated file_id")
 
2356
        yield item
 
2357
 
 
2358
 
 
2359
def _check_delta_unique_new_paths(delta):
 
2360
    """Decorate a delta and check that the new paths in it are unique.
 
2361
 
 
2362
    :return: A generator over delta.
 
2363
    """
 
2364
    paths = set()
 
2365
    for item in delta:
 
2366
        length = len(paths) + 1
 
2367
        path = item[1]
 
2368
        if path is not None:
 
2369
            paths.add(path)
 
2370
            if len(paths) != length:
 
2371
                raise errors.InconsistentDelta(path, item[2], "repeated path")
 
2372
        yield item
 
2373
 
 
2374
 
 
2375
def _check_delta_unique_old_paths(delta):
 
2376
    """Decorate a delta and check that the old paths in it are unique.
 
2377
 
 
2378
    :return: A generator over delta.
 
2379
    """
 
2380
    paths = set()
 
2381
    for item in delta:
 
2382
        length = len(paths) + 1
 
2383
        path = item[0]
 
2384
        if path is not None:
 
2385
            paths.add(path)
 
2386
            if len(paths) != length:
 
2387
                raise errors.InconsistentDelta(path, item[2], "repeated path")
 
2388
        yield item
 
2389
 
 
2390
 
 
2391
def _check_delta_ids_are_valid(delta):
 
2392
    """Decorate a delta and check that the ids in it are valid.
 
2393
 
 
2394
    :return: A generator over delta.
 
2395
    """
 
2396
    for item in delta:
 
2397
        entry = item[3]
 
2398
        if item[2] is None:
 
2399
            raise errors.InconsistentDelta(item[0] or item[1], item[2],
 
2400
                "entry with file_id None %r" % entry)
 
2401
        if type(item[2]) != str:
 
2402
            raise errors.InconsistentDelta(item[0] or item[1], item[2],
 
2403
                "entry with non bytes file_id %r" % entry)
 
2404
        yield item
 
2405
 
 
2406
 
 
2407
def _check_delta_ids_match_entry(delta):
 
2408
    """Decorate a delta and check that the ids in it match the entry.file_id.
 
2409
 
 
2410
    :return: A generator over delta.
 
2411
    """
 
2412
    for item in delta:
 
2413
        entry = item[3]
 
2414
        if entry is not None:
 
2415
            if entry.file_id != item[2]:
 
2416
                raise errors.InconsistentDelta(item[0] or item[1], item[2],
 
2417
                    "mismatched id with %r" % entry)
 
2418
        yield item
 
2419
 
 
2420
 
 
2421
def _check_delta_new_path_entry_both_or_None(delta):
 
2422
    """Decorate a delta and check that the new_path and entry are paired.
 
2423
 
 
2424
    :return: A generator over delta.
 
2425
    """
 
2426
    for item in delta:
 
2427
        new_path = item[1]
 
2428
        entry = item[3]
 
2429
        if new_path is None and entry is not None:
 
2430
            raise errors.InconsistentDelta(item[0], item[1],
 
2431
                "Entry with no new_path")
 
2432
        if new_path is not None and entry is None:
 
2433
            raise errors.InconsistentDelta(new_path, item[1],
 
2434
                "new_path with no entry")
 
2435
        yield item