~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Robert Collins
  • Date: 2010-05-06 07:48:22 UTC
  • mto: This revision was merged to the branch mainline in revision 5223.
  • Revision ID: robertc@robertcollins.net-20100506074822-0bsgf2j4h8jx0xkk
Added ``bzrlib.tests.matchers`` as a place to put matchers, along with
our first in-tree matcher. See the module docstring for details.
(Robert Collins)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
 
1
# Copyright (C) 2005-2010 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
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
 
53
51
    )
54
52
from bzrlib.symbol_versioning import deprecated_in, deprecated_method
55
53
from bzrlib.trace import mutter
 
54
from bzrlib.static_tuple import StaticTuple
56
55
 
57
56
 
58
57
class InventoryEntry(object):
262
261
    def versionable_kind(kind):
263
262
        return (kind in ('file', 'directory', 'symlink', 'tree-reference'))
264
263
 
265
 
    def check(self, checker, rev_id, inv, tree):
 
264
    def check(self, checker, rev_id, inv):
266
265
        """Check this inventory entry is intact.
267
266
 
268
267
        This is a template method, override _check for kind specific
274
273
        :param rev_id: Revision id from which this InventoryEntry was loaded.
275
274
             Not necessarily the last-changed revision for this file.
276
275
        :param inv: Inventory from which the entry was loaded.
277
 
        :param tree: RevisionTree for this entry.
278
276
        """
279
277
        if self.parent_id is not None:
280
278
            if not inv.has_id(self.parent_id):
281
279
                raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
282
280
                        % (self.parent_id, rev_id))
283
 
        self._check(checker, rev_id, tree)
 
281
        checker._add_entry_to_text_key_references(inv, self)
 
282
        self._check(checker, rev_id)
284
283
 
285
 
    def _check(self, checker, rev_id, tree):
 
284
    def _check(self, checker, rev_id):
286
285
        """Check this inventory entry for kind specific errors."""
287
 
        raise BzrCheckError('unknown entry kind %r in revision {%s}' %
288
 
                            (self.kind, rev_id))
 
286
        checker._report_items.append(
 
287
            'unknown entry kind %r in revision {%s}' % (self.kind, rev_id))
289
288
 
290
289
    def copy(self):
291
290
        """Clone this inventory entry."""
404
403
                 'text_id', 'parent_id', 'children', 'executable',
405
404
                 'revision', 'symlink_target', 'reference_revision']
406
405
 
407
 
    def _check(self, checker, rev_id, tree):
 
406
    def _check(self, checker, rev_id):
408
407
        """See InventoryEntry._check"""
409
408
 
410
409
    def __init__(self, file_id):
433
432
                 'text_id', 'parent_id', 'children', 'executable',
434
433
                 'revision', 'symlink_target', 'reference_revision']
435
434
 
436
 
    def _check(self, checker, rev_id, tree):
 
435
    def _check(self, checker, rev_id):
437
436
        """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}'
 
437
        if (self.text_sha1 is not None or self.text_size is not None or
 
438
            self.text_id is not None):
 
439
            checker._report_items.append('directory {%s} has text in revision {%s}'
440
440
                                % (self.file_id, rev_id))
 
441
        # In non rich root repositories we do not expect a file graph for the
 
442
        # root.
 
443
        if self.name == '' and not checker.rich_roots:
 
444
            return
 
445
        # Directories are stored as an empty file, but the file should exist
 
446
        # to provide a per-fileid log. The hash of every directory content is
 
447
        # "da..." below (the sha1sum of '').
 
448
        checker.add_pending_item(rev_id,
 
449
            ('texts', self.file_id, self.revision), 'text',
 
450
             'da39a3ee5e6b4b0d3255bfef95601890afd80709')
441
451
 
442
452
    def copy(self):
443
453
        other = InventoryDirectory(self.file_id, self.name, self.parent_id)
476
486
                 'text_id', 'parent_id', 'children', 'executable',
477
487
                 'revision', 'symlink_target', 'reference_revision']
478
488
 
479
 
    def _check(self, checker, tree_revision_id, tree):
 
489
    def _check(self, checker, tree_revision_id):
480
490
        """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
 
491
        # TODO: check size too.
 
492
        checker.add_pending_item(tree_revision_id,
 
493
            ('texts', self.file_id, self.revision), 'text',
 
494
             self.text_sha1)
 
495
        if self.text_size is None:
 
496
            checker._report_items.append(
 
497
                'fileid {%s} in {%s} has None for text_size' % (self.file_id,
 
498
                tree_revision_id))
500
499
 
501
500
    def copy(self):
502
501
        other = InventoryFile(self.file_id, self.name, self.parent_id)
600
599
                 'text_id', 'parent_id', 'children', 'executable',
601
600
                 'revision', 'symlink_target', 'reference_revision']
602
601
 
603
 
    def _check(self, checker, rev_id, tree):
 
602
    def _check(self, checker, tree_revision_id):
604
603
        """See InventoryEntry._check"""
605
604
        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))
 
605
            checker._report_items.append(
 
606
               'symlink {%s} has text in revision {%s}'
 
607
                    % (self.file_id, tree_revision_id))
608
608
        if self.symlink_target is None:
609
 
            raise BzrCheckError('symlink {%s} has no target in revision {%s}'
610
 
                    % (self.file_id, rev_id))
 
609
            checker._report_items.append(
 
610
                'symlink {%s} has no target in revision {%s}'
 
611
                    % (self.file_id, tree_revision_id))
 
612
        # Symlinks are stored as ''
 
613
        checker.add_pending_item(tree_revision_id,
 
614
            ('texts', self.file_id, self.revision), 'text',
 
615
             'da39a3ee5e6b4b0d3255bfef95601890afd80709')
611
616
 
612
617
    def copy(self):
613
618
        other = InventoryLink(self.file_id, self.name, self.parent_id)
714
719
 
715
720
 
716
721
class CommonInventory(object):
717
 
    """Basic inventory logic, defined in terms of primitives like has_id."""
 
722
    """Basic inventory logic, defined in terms of primitives like has_id.
 
723
 
 
724
    An inventory is the metadata about the contents of a tree.
 
725
 
 
726
    This is broadly a map from file_id to entries such as directories, files,
 
727
    symlinks and tree references. Each entry maintains its own metadata like
 
728
    SHA1 and length for files, or children for a directory.
 
729
 
 
730
    Entries can be looked up either by path or by file_id.
 
731
 
 
732
    InventoryEntry objects must not be modified after they are
 
733
    inserted, other than through the Inventory API.
 
734
    """
718
735
 
719
736
    def __contains__(self, file_id):
720
737
        """True if this entry contains a file with given id.
733
750
        """
734
751
        return self.has_id(file_id)
735
752
 
 
753
    def has_filename(self, filename):
 
754
        return bool(self.path2id(filename))
 
755
 
736
756
    def id2path(self, file_id):
737
757
        """Return as a string the path to file_id.
738
758
 
741
761
        >>> e = i.add(InventoryFile('foo-id', 'foo.c', parent_id='src-id'))
742
762
        >>> print i.id2path('foo-id')
743
763
        src/foo.c
 
764
 
 
765
        :raises NoSuchId: If file_id is not present in the inventory.
744
766
        """
745
767
        # get all names, skipping root
746
768
        return '/'.join(reversed(
747
769
            [parent.name for parent in
748
770
             self._iter_file_id_parents(file_id)][:-1]))
749
771
 
750
 
    def iter_entries(self, from_dir=None):
751
 
        """Return (path, entry) pairs, in order by name."""
 
772
    def iter_entries(self, from_dir=None, recursive=True):
 
773
        """Return (path, entry) pairs, in order by name.
 
774
        
 
775
        :param from_dir: if None, start from the root,
 
776
          otherwise start from this directory (either file-id or entry)
 
777
        :param recursive: recurse into directories or not
 
778
        """
752
779
        if from_dir is None:
753
780
            if self.root is None:
754
781
                return
761
788
        # 440ms/663ms (inline/total) to 116ms/116ms
762
789
        children = from_dir.children.items()
763
790
        children.sort()
 
791
        if not recursive:
 
792
            for name, ie in children:
 
793
                yield name, ie
 
794
            return
764
795
        children = collections.deque(children)
765
796
        stack = [(u'', children)]
766
797
        while stack:
928
959
        descend(self.root, u'')
929
960
        return accum
930
961
 
931
 
    def path2id(self, name):
 
962
    def path2id(self, relpath):
932
963
        """Walk down through directories to return entry of last component.
933
964
 
934
 
        names may be either a list of path components, or a single
935
 
        string, in which case it is automatically split.
 
965
        :param relpath: may be either a list of path components, or a single
 
966
            string, in which case it is automatically split.
936
967
 
937
968
        This returns the entry of the last component in the path,
938
969
        which may be either a file or a directory.
939
970
 
940
971
        Returns None IFF the path is not found.
941
972
        """
942
 
        if isinstance(name, basestring):
943
 
            name = osutils.splitpath(name)
944
 
 
945
 
        # mutter("lookup path %r" % name)
 
973
        if isinstance(relpath, basestring):
 
974
            names = osutils.splitpath(relpath)
 
975
        else:
 
976
            names = relpath
946
977
 
947
978
        try:
948
979
            parent = self.root
951
982
            return None
952
983
        if parent is None:
953
984
            return None
954
 
        for f in name:
 
985
        for f in names:
955
986
            try:
956
987
                children = getattr(parent, 'children', None)
957
988
                if children is None:
1012
1043
 
1013
1044
 
1014
1045
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
 
1046
    """Mutable dict based in-memory inventory.
 
1047
 
 
1048
    We never store the full path to a file, because renaming a directory
 
1049
    implicitly moves all of its contents.  This class internally maintains a
1025
1050
    lookup tree that allows the children under a directory to be
1026
1051
    returned quickly.
1027
1052
 
1028
 
    InventoryEntry objects must not be modified after they are
1029
 
    inserted, other than through the Inventory API.
1030
 
 
1031
1053
    >>> inv = Inventory()
1032
1054
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
1033
1055
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1034
1056
    >>> inv['123-123'].name
1035
1057
    'hello.c'
1036
1058
 
1037
 
    May be treated as an iterator or set to look up file ids:
 
1059
    Id's may be looked up from paths:
1038
1060
 
1039
 
    >>> bool(inv.path2id('hello.c'))
1040
 
    True
 
1061
    >>> inv.path2id('hello.c')
 
1062
    '123-123'
1041
1063
    >>> '123-123' in inv
1042
1064
    True
1043
1065
 
1044
 
    May also look up by name:
 
1066
    There are iterators over the contents:
1045
1067
 
1046
 
    >>> [x[0] for x in inv.iter_entries()]
 
1068
    >>> [entry[0] for entry in inv.iter_entries()]
1047
1069
    ['', 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
1070
    """
 
1071
 
1055
1072
    def __init__(self, root_id=ROOT_ID, revision_id=None):
1056
1073
        """Create or read an inventory.
1057
1074
 
1081
1098
    def apply_delta(self, delta):
1082
1099
        """Apply a delta to this inventory.
1083
1100
 
 
1101
        See the inventory developers documentation for the theory behind
 
1102
        inventory deltas.
 
1103
 
 
1104
        If delta application fails the inventory is left in an indeterminate
 
1105
        state and must not be used.
 
1106
 
1084
1107
        :param delta: A list of changes to apply. After all the changes are
1085
1108
            applied the final inventory must be internally consistent, but it
1086
1109
            is ok to supply changes which, if only half-applied would have an
1117
1140
        """
1118
1141
        # Check that the delta is legal. It would be nice if this could be
1119
1142
        # 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
 
1143
        # before starting to mutate the inventory, as there isn't a rollback
 
1144
        # facility.
 
1145
        list(_check_delta_unique_ids(_check_delta_unique_new_paths(
 
1146
            _check_delta_unique_old_paths(_check_delta_ids_match_entry(
 
1147
            _check_delta_ids_are_valid(
 
1148
            _check_delta_new_path_entry_both_or_None(
 
1149
            delta)))))))
1126
1150
 
1127
1151
        children = {}
1128
1152
        # Remove all affected items which were in the original inventory,
1131
1155
        # modified children remaining by the time we examine it.
1132
1156
        for old_path, file_id in sorted(((op, f) for op, np, f, e in delta
1133
1157
                                        if op is not None), reverse=True):
1134
 
            if file_id not in self:
1135
 
                # adds come later
1136
 
                continue
1137
1158
            # Preserve unaltered children of file_id for later reinsertion.
1138
1159
            file_id_children = getattr(self[file_id], 'children', {})
1139
1160
            if len(file_id_children):
1140
1161
                children[file_id] = file_id_children
 
1162
            if self.id2path(file_id) != old_path:
 
1163
                raise errors.InconsistentDelta(old_path, file_id,
 
1164
                    "Entry was at wrong other path %r." % self.id2path(file_id))
1141
1165
            # Remove file_id and the unaltered children. If file_id is not
1142
1166
            # being deleted it will be reinserted back later.
1143
1167
            self.remove_recursive_id(file_id)
1146
1170
        # longest, ensuring that items which were modified and whose parents in
1147
1171
        # the resulting inventory were also modified, are inserted after their
1148
1172
        # parents.
1149
 
        for new_path, new_entry in sorted((np, e) for op, np, f, e in
 
1173
        for new_path, f, new_entry in sorted((np, f, e) for op, np, f, e in
1150
1174
                                          delta if np is not None):
1151
1175
            if new_entry.kind == 'directory':
1152
1176
                # Pop the child which to allow detection of children whose
1157
1181
                replacement.revision = new_entry.revision
1158
1182
                replacement.children = children.pop(replacement.file_id, {})
1159
1183
                new_entry = replacement
1160
 
            self.add(new_entry)
 
1184
            try:
 
1185
                self.add(new_entry)
 
1186
            except errors.DuplicateFileId:
 
1187
                raise errors.InconsistentDelta(new_path, new_entry.file_id,
 
1188
                    "New id is already present in target.")
 
1189
            except AttributeError:
 
1190
                raise errors.InconsistentDelta(new_path, new_entry.file_id,
 
1191
                    "Parent is not a directory.")
 
1192
            if self.id2path(new_entry.file_id) != new_path:
 
1193
                raise errors.InconsistentDelta(new_path, new_entry.file_id,
 
1194
                    "New path is not consistent with parent path.")
1161
1195
        if len(children):
1162
1196
            # Get the parent id that was deleted
1163
1197
            parent_id, children = children.popitem()
1164
1198
            raise errors.InconsistentDelta("<deleted>", parent_id,
1165
1199
                "The file id was deleted but its children were not deleted.")
1166
1200
 
 
1201
    def create_by_apply_delta(self, inventory_delta, new_revision_id,
 
1202
                              propagate_caches=False):
 
1203
        """See CHKInventory.create_by_apply_delta()"""
 
1204
        new_inv = self.copy()
 
1205
        new_inv.apply_delta(inventory_delta)
 
1206
        new_inv.revision_id = new_revision_id
 
1207
        return new_inv
 
1208
 
1167
1209
    def _set_root(self, ie):
1168
1210
        self.root = ie
1169
1211
        self._byid = {self.root.file_id: self.root}
1183
1225
 
1184
1226
    def _get_mutable_inventory(self):
1185
1227
        """See CommonInventory._get_mutable_inventory."""
1186
 
        return deepcopy(self)
 
1228
        return copy.deepcopy(self)
1187
1229
 
1188
1230
    def __iter__(self):
1189
1231
        """Iterate over all file-ids."""
1243
1285
        To add  a file to a branch ready to be committed, use Branch.add,
1244
1286
        which calls this.
1245
1287
 
1246
 
        Returns the new entry object.
 
1288
        :return: entry
1247
1289
        """
1248
1290
        if entry.file_id in self._byid:
1249
1291
            raise errors.DuplicateFileId(entry.file_id,
1250
1292
                                         self._byid[entry.file_id])
1251
 
 
1252
1293
        if entry.parent_id is None:
1253
1294
            self.root = entry
1254
1295
        else:
1255
1296
            try:
1256
1297
                parent = self._byid[entry.parent_id]
1257
1298
            except KeyError:
1258
 
                raise BzrError("parent_id {%s} not in inventory" %
1259
 
                               entry.parent_id)
1260
 
 
 
1299
                raise errors.InconsistentDelta("<unknown>", entry.parent_id,
 
1300
                    "Parent not in inventory.")
1261
1301
            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'))
 
1302
                raise errors.InconsistentDelta(
 
1303
                    self.id2path(parent.children[entry.name].file_id),
 
1304
                    entry.file_id,
 
1305
                    "Path already versioned")
1265
1306
            parent.children[entry.name] = entry
1266
1307
        return self._add_child(entry)
1267
1308
 
1342
1383
            yield ie
1343
1384
            file_id = ie.parent_id
1344
1385
 
1345
 
    def has_filename(self, filename):
1346
 
        return bool(self.path2id(filename))
1347
 
 
1348
1386
    def has_id(self, file_id):
1349
1387
        return (file_id in self._byid)
1350
1388
 
1460
1498
        self._fileid_to_entry_cache = {}
1461
1499
        self._path_to_fileid_cache = {}
1462
1500
        self._search_key_name = search_key_name
 
1501
        self.root_id = None
 
1502
 
 
1503
    def __eq__(self, other):
 
1504
        """Compare two sets by comparing their contents."""
 
1505
        if not isinstance(other, CHKInventory):
 
1506
            return NotImplemented
 
1507
 
 
1508
        this_key = self.id_to_entry.key()
 
1509
        other_key = other.id_to_entry.key()
 
1510
        this_pid_key = self.parent_id_basename_to_file_id.key()
 
1511
        other_pid_key = other.parent_id_basename_to_file_id.key()
 
1512
        if None in (this_key, this_pid_key, other_key, other_pid_key):
 
1513
            return False
 
1514
        return this_key == other_key and this_pid_key == other_pid_key
1463
1515
 
1464
1516
    def _entry_to_bytes(self, entry):
1465
1517
        """Serialise entry as a single bytestring.
1503
1555
        else:
1504
1556
            raise ValueError("unknown kind %r" % entry.kind)
1505
1557
 
 
1558
    def _expand_fileids_to_parents_and_children(self, file_ids):
 
1559
        """Give a more wholistic view starting with the given file_ids.
 
1560
 
 
1561
        For any file_id which maps to a directory, we will include all children
 
1562
        of that directory. We will also include all directories which are
 
1563
        parents of the given file_ids, but we will not include their children.
 
1564
 
 
1565
        eg:
 
1566
          /     # TREE_ROOT
 
1567
          foo/  # foo-id
 
1568
            baz # baz-id
 
1569
            frob/ # frob-id
 
1570
              fringle # fringle-id
 
1571
          bar/  # bar-id
 
1572
            bing # bing-id
 
1573
 
 
1574
        if given [foo-id] we will include
 
1575
            TREE_ROOT as interesting parents
 
1576
        and 
 
1577
            foo-id, baz-id, frob-id, fringle-id
 
1578
        As interesting ids.
 
1579
        """
 
1580
        interesting = set()
 
1581
        # TODO: Pre-pass over the list of fileids to see if anything is already
 
1582
        #       deserialized in self._fileid_to_entry_cache
 
1583
 
 
1584
        directories_to_expand = set()
 
1585
        children_of_parent_id = {}
 
1586
        # It is okay if some of the fileids are missing
 
1587
        for entry in self._getitems(file_ids):
 
1588
            if entry.kind == 'directory':
 
1589
                directories_to_expand.add(entry.file_id)
 
1590
            interesting.add(entry.parent_id)
 
1591
            children_of_parent_id.setdefault(entry.parent_id, []
 
1592
                                             ).append(entry.file_id)
 
1593
 
 
1594
        # Now, interesting has all of the direct parents, but not the
 
1595
        # parents of those parents. It also may have some duplicates with
 
1596
        # specific_fileids
 
1597
        remaining_parents = interesting.difference(file_ids)
 
1598
        # When we hit the TREE_ROOT, we'll get an interesting parent of None,
 
1599
        # but we don't actually want to recurse into that
 
1600
        interesting.add(None) # this will auto-filter it in the loop
 
1601
        remaining_parents.discard(None) 
 
1602
        while remaining_parents:
 
1603
            next_parents = set()
 
1604
            for entry in self._getitems(remaining_parents):
 
1605
                next_parents.add(entry.parent_id)
 
1606
                children_of_parent_id.setdefault(entry.parent_id, []
 
1607
                                                 ).append(entry.file_id)
 
1608
            # Remove any search tips we've already processed
 
1609
            remaining_parents = next_parents.difference(interesting)
 
1610
            interesting.update(remaining_parents)
 
1611
            # We should probably also .difference(directories_to_expand)
 
1612
        interesting.update(file_ids)
 
1613
        interesting.discard(None)
 
1614
        while directories_to_expand:
 
1615
            # Expand directories by looking in the
 
1616
            # parent_id_basename_to_file_id map
 
1617
            keys = [StaticTuple(f,).intern() for f in directories_to_expand]
 
1618
            directories_to_expand = set()
 
1619
            items = self.parent_id_basename_to_file_id.iteritems(keys)
 
1620
            next_file_ids = set([item[1] for item in items])
 
1621
            next_file_ids = next_file_ids.difference(interesting)
 
1622
            interesting.update(next_file_ids)
 
1623
            for entry in self._getitems(next_file_ids):
 
1624
                if entry.kind == 'directory':
 
1625
                    directories_to_expand.add(entry.file_id)
 
1626
                children_of_parent_id.setdefault(entry.parent_id, []
 
1627
                                                 ).append(entry.file_id)
 
1628
        return interesting, children_of_parent_id
 
1629
 
 
1630
    def filter(self, specific_fileids):
 
1631
        """Get an inventory view filtered against a set of file-ids.
 
1632
 
 
1633
        Children of directories and parents are included.
 
1634
 
 
1635
        The result may or may not reference the underlying inventory
 
1636
        so it should be treated as immutable.
 
1637
        """
 
1638
        (interesting,
 
1639
         parent_to_children) = self._expand_fileids_to_parents_and_children(
 
1640
                                specific_fileids)
 
1641
        # There is some overlap here, but we assume that all interesting items
 
1642
        # are in the _fileid_to_entry_cache because we had to read them to
 
1643
        # determine if they were a dir we wanted to recurse, or just a file
 
1644
        # This should give us all the entries we'll want to add, so start
 
1645
        # adding
 
1646
        other = Inventory(self.root_id)
 
1647
        other.root.revision = self.root.revision
 
1648
        other.revision_id = self.revision_id
 
1649
        if not interesting or not parent_to_children:
 
1650
            # empty filter, or filtering entrys that don't exist
 
1651
            # (if even 1 existed, then we would have populated
 
1652
            # parent_to_children with at least the tree root.)
 
1653
            return other
 
1654
        cache = self._fileid_to_entry_cache
 
1655
        try:
 
1656
            remaining_children = collections.deque(parent_to_children[self.root_id])
 
1657
        except:
 
1658
            import pdb; pdb.set_trace()
 
1659
            raise
 
1660
        while remaining_children:
 
1661
            file_id = remaining_children.popleft()
 
1662
            ie = cache[file_id]
 
1663
            if ie.kind == 'directory':
 
1664
                ie = ie.copy() # We create a copy to depopulate the .children attribute
 
1665
            # TODO: depending on the uses of 'other' we should probably alwyas
 
1666
            #       '.copy()' to prevent someone from mutating other and
 
1667
            #       invaliding our internal cache
 
1668
            other.add(ie)
 
1669
            if file_id in parent_to_children:
 
1670
                remaining_children.extend(parent_to_children[file_id])
 
1671
        return other
 
1672
 
1506
1673
    @staticmethod
1507
1674
    def _bytes_to_utf8name_key(bytes):
1508
1675
        """Get the file_id, revision_id key out of bytes."""
1510
1677
        # to filter out empty names because of non rich-root...
1511
1678
        sections = bytes.split('\n')
1512
1679
        kind, file_id = sections[0].split(': ')
1513
 
        return (sections[2], file_id, sections[3])
 
1680
        return (sections[2], intern(file_id), intern(sections[3]))
1514
1681
 
1515
1682
    def _bytes_to_entry(self, bytes):
1516
1683
        """Deserialise a serialised entry."""
1538
1705
            result.reference_revision = sections[4]
1539
1706
        else:
1540
1707
            raise ValueError("Not a serialised entry %r" % bytes)
1541
 
        result.revision = sections[3]
 
1708
        result.file_id = intern(result.file_id)
 
1709
        result.revision = intern(sections[3])
1542
1710
        if result.parent_id == '':
1543
1711
            result.parent_id = None
1544
1712
        self._fileid_to_entry_cache[result.file_id] = result
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
 
                new_key = (file_id,)
 
1813
                new_key = StaticTuple(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
 
                old_key = (file_id,)
1622
 
            id_to_entry_delta.append((old_key, new_key, new_value))
 
1822
                old_key = StaticTuple(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)
 
1828
            id_to_entry_delta.append(StaticTuple(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
1625
1831
                if old_path is None:
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
1671
1918
                raise errors.BzrError('Duplicate key in inventory: %r\n%r'
1672
1919
                                      % (key, bytes))
1673
1920
            info[key] = value
1674
 
        revision_id = info['revision_id']
1675
 
        root_id = info['root_id']
1676
 
        search_key_name = info.get('search_key_name', 'plain')
1677
 
        parent_id_basename_to_file_id = info.get(
1678
 
            'parent_id_basename_to_file_id', None)
 
1921
        revision_id = intern(info['revision_id'])
 
1922
        root_id = intern(info['root_id'])
 
1923
        search_key_name = intern(info.get('search_key_name', 'plain'))
 
1924
        parent_id_basename_to_file_id = intern(info.get(
 
1925
            'parent_id_basename_to_file_id', None))
 
1926
        if not parent_id_basename_to_file_id.startswith('sha1:'):
 
1927
            raise ValueError('parent_id_basename_to_file_id should be a sha1'
 
1928
                             ' key not %r' % (parent_id_basename_to_file_id,))
1679
1929
        id_to_entry = info['id_to_entry']
 
1930
        if not id_to_entry.startswith('sha1:'):
 
1931
            raise ValueError('id_to_entry should be a sha1'
 
1932
                             ' key not %r' % (id_to_entry,))
1680
1933
 
1681
1934
        result = CHKInventory(search_key_name)
1682
1935
        result.revision_id = revision_id
1685
1938
                            result._search_key_name)
1686
1939
        if parent_id_basename_to_file_id is not None:
1687
1940
            result.parent_id_basename_to_file_id = chk_map.CHKMap(
1688
 
                chk_store, (parent_id_basename_to_file_id,),
 
1941
                chk_store, StaticTuple(parent_id_basename_to_file_id,),
1689
1942
                search_key_func=search_key_func)
1690
1943
        else:
1691
1944
            result.parent_id_basename_to_file_id = None
1692
1945
 
1693
 
        result.id_to_entry = chk_map.CHKMap(chk_store, (id_to_entry,),
 
1946
        result.id_to_entry = chk_map.CHKMap(chk_store,
 
1947
                                            StaticTuple(id_to_entry,),
1694
1948
                                            search_key_func=search_key_func)
1695
1949
        if (result.revision_id,) != expected_revision_id:
1696
1950
            raise ValueError("Mismatched revision id and expected: %r, %r" %
1709
1963
        :param maximum_size: The CHKMap node size limit.
1710
1964
        :param search_key_name: The identifier for the search key function
1711
1965
        """
1712
 
        result = CHKInventory(search_key_name)
 
1966
        result = klass(search_key_name)
1713
1967
        result.revision_id = inventory.revision_id
1714
1968
        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 = []
 
1969
 
 
1970
        entry_to_bytes = result._entry_to_bytes
 
1971
        parent_id_basename_key = result._parent_id_basename_key
 
1972
        id_to_entry_dict = {}
 
1973
        parent_id_basename_dict = {}
1725
1974
        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)
 
1975
            key = StaticTuple(entry.file_id,).intern()
 
1976
            id_to_entry_dict[key] = entry_to_bytes(entry)
 
1977
            p_id_key = parent_id_basename_key(entry)
 
1978
            parent_id_basename_dict[p_id_key] = entry.file_id
 
1979
 
 
1980
        result._populate_from_dicts(chk_store, id_to_entry_dict,
 
1981
            parent_id_basename_dict, maximum_size=maximum_size)
1733
1982
        return result
1734
1983
 
 
1984
    def _populate_from_dicts(self, chk_store, id_to_entry_dict,
 
1985
                             parent_id_basename_dict, maximum_size):
 
1986
        search_key_func = chk_map.search_key_registry.get(self._search_key_name)
 
1987
        root_key = chk_map.CHKMap.from_dict(chk_store, id_to_entry_dict,
 
1988
                   maximum_size=maximum_size, key_width=1,
 
1989
                   search_key_func=search_key_func)
 
1990
        self.id_to_entry = chk_map.CHKMap(chk_store, root_key,
 
1991
                                          search_key_func)
 
1992
        root_key = chk_map.CHKMap.from_dict(chk_store,
 
1993
                   parent_id_basename_dict,
 
1994
                   maximum_size=maximum_size, key_width=2,
 
1995
                   search_key_func=search_key_func)
 
1996
        self.parent_id_basename_to_file_id = chk_map.CHKMap(chk_store,
 
1997
                                                    root_key, search_key_func)
 
1998
 
1735
1999
    def _parent_id_basename_key(self, entry):
1736
2000
        """Create a key for a entry in a parent_id_basename_to_file_id index."""
1737
2001
        if entry.parent_id is not None:
1738
2002
            parent_id = entry.parent_id
1739
2003
        else:
1740
2004
            parent_id = ''
1741
 
        return parent_id, entry.name.encode('utf8')
 
2005
        return StaticTuple(parent_id, entry.name.encode('utf8')).intern()
1742
2006
 
1743
2007
    def __getitem__(self, file_id):
1744
2008
        """map a single file_id -> InventoryEntry."""
1749
2013
            return result
1750
2014
        try:
1751
2015
            return self._bytes_to_entry(
1752
 
                self.id_to_entry.iteritems([(file_id,)]).next()[1])
 
2016
                self.id_to_entry.iteritems([StaticTuple(file_id,)]).next()[1])
1753
2017
        except StopIteration:
1754
2018
            # really we're passing an inventory, not a tree...
1755
2019
            raise errors.NoSuchId(self, file_id)
1756
2020
 
 
2021
    def _getitems(self, file_ids):
 
2022
        """Similar to __getitem__, but lets you query for multiple.
 
2023
        
 
2024
        The returned order is undefined. And currently if an item doesn't
 
2025
        exist, it isn't included in the output.
 
2026
        """
 
2027
        result = []
 
2028
        remaining = []
 
2029
        for file_id in file_ids:
 
2030
            entry = self._fileid_to_entry_cache.get(file_id, None)
 
2031
            if entry is None:
 
2032
                remaining.append(file_id)
 
2033
            else:
 
2034
                result.append(entry)
 
2035
        file_keys = [StaticTuple(f,).intern() for f in remaining]
 
2036
        for file_key, value in self.id_to_entry.iteritems(file_keys):
 
2037
            entry = self._bytes_to_entry(value)
 
2038
            result.append(entry)
 
2039
            self._fileid_to_entry_cache[entry.file_id] = entry
 
2040
        return result
 
2041
 
1757
2042
    def has_id(self, file_id):
1758
2043
        # Perhaps have an explicit 'contains' method on CHKMap ?
1759
2044
        if self._fileid_to_entry_cache.get(file_id, None) is not None:
1760
2045
            return True
1761
 
        return len(list(self.id_to_entry.iteritems([(file_id,)]))) == 1
 
2046
        return len(list(
 
2047
            self.id_to_entry.iteritems([StaticTuple(file_id,)]))) == 1
1762
2048
 
1763
2049
    def is_root(self, file_id):
1764
2050
        return file_id == self.root_id
1893
2179
            delta.append((old_path, new_path, file_id, entry))
1894
2180
        return delta
1895
2181
 
1896
 
    def path2id(self, name):
 
2182
    def path2id(self, relpath):
1897
2183
        """See CommonInventory.path2id()."""
1898
 
        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
 
2184
        # TODO: perhaps support negative hits?
 
2185
        result = self._path_to_fileid_cache.get(relpath, None)
 
2186
        if result is not None:
 
2187
            return result
 
2188
        if isinstance(relpath, basestring):
 
2189
            names = osutils.splitpath(relpath)
 
2190
        else:
 
2191
            names = relpath
 
2192
        current_id = self.root_id
 
2193
        if current_id is None:
 
2194
            return None
 
2195
        parent_id_index = self.parent_id_basename_to_file_id
 
2196
        cur_path = None
 
2197
        for basename in names:
 
2198
            if cur_path is None:
 
2199
                cur_path = basename
 
2200
            else:
 
2201
                cur_path = cur_path + '/' + basename
 
2202
            basename_utf8 = basename.encode('utf8')
 
2203
            file_id = self._path_to_fileid_cache.get(cur_path, None)
 
2204
            if file_id is None:
 
2205
                key_filter = [StaticTuple(current_id, basename_utf8)]
 
2206
                items = parent_id_index.iteritems(key_filter)
 
2207
                for (parent_id, name_utf8), file_id in items:
 
2208
                    if parent_id != current_id or name_utf8 != basename_utf8:
 
2209
                        raise errors.BzrError("corrupt inventory lookup! "
 
2210
                            "%r %r %r %r" % (parent_id, current_id, name_utf8,
 
2211
                            basename_utf8))
 
2212
                if file_id is None:
 
2213
                    return None
 
2214
                else:
 
2215
                    self._path_to_fileid_cache[cur_path] = file_id
 
2216
            current_id = file_id
 
2217
        return current_id
1903
2218
 
1904
2219
    def to_lines(self):
1905
2220
        """Serialise the inventory to lines."""
1909
2224
            lines.append('search_key_name: %s\n' % (self._search_key_name,))
1910
2225
            lines.append("root_id: %s\n" % self.root_id)
1911
2226
            lines.append('parent_id_basename_to_file_id: %s\n' %
1912
 
                self.parent_id_basename_to_file_id.key())
 
2227
                (self.parent_id_basename_to_file_id.key()[0],))
1913
2228
            lines.append("revision_id: %s\n" % self.revision_id)
1914
 
            lines.append("id_to_entry: %s\n" % self.id_to_entry.key())
 
2229
            lines.append("id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
1915
2230
        else:
1916
2231
            lines.append("revision_id: %s\n" % self.revision_id)
1917
2232
            lines.append("root_id: %s\n" % self.root_id)
1918
2233
            if self.parent_id_basename_to_file_id is not None:
1919
2234
                lines.append('parent_id_basename_to_file_id: %s\n' %
1920
 
                    self.parent_id_basename_to_file_id.key())
1921
 
            lines.append("id_to_entry: %s\n" % self.id_to_entry.key())
 
2235
                    (self.parent_id_basename_to_file_id.key()[0],))
 
2236
            lines.append("id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
1922
2237
        return lines
1923
2238
 
1924
2239
    @property
1965
2280
        parent_id_index = self._chk_inventory.parent_id_basename_to_file_id
1966
2281
        child_keys = set()
1967
2282
        for (parent_id, name_utf8), file_id in parent_id_index.iteritems(
1968
 
            key_filter=[(self.file_id,)]):
1969
 
            child_keys.add((file_id,))
 
2283
            key_filter=[StaticTuple(self.file_id,)]):
 
2284
            child_keys.add(StaticTuple(file_id,))
1970
2285
        cached = set()
1971
2286
        for file_id_key in child_keys:
1972
2287
            entry = self._chk_inventory._fileid_to_entry_cache.get(
2005
2320
    try:
2006
2321
        factory = entry_factory[kind]
2007
2322
    except KeyError:
2008
 
        raise BzrError("unknown kind %r" % kind)
 
2323
        raise errors.BadFileKindError(name, kind)
2009
2324
    return factory(file_id, name, parent_id)
2010
2325
 
2011
2326
 
2039
2354
        _NAME_RE = re.compile(r'^[^/\\]+$')
2040
2355
 
2041
2356
    return bool(_NAME_RE.match(name))
 
2357
 
 
2358
 
 
2359
def _check_delta_unique_ids(delta):
 
2360
    """Decorate a delta and check that the file ids in it are unique.
 
2361
 
 
2362
    :return: A generator over delta.
 
2363
    """
 
2364
    ids = set()
 
2365
    for item in delta:
 
2366
        length = len(ids) + 1
 
2367
        ids.add(item[2])
 
2368
        if len(ids) != length:
 
2369
            raise errors.InconsistentDelta(item[0] or item[1], item[2],
 
2370
                "repeated file_id")
 
2371
        yield item
 
2372
 
 
2373
 
 
2374
def _check_delta_unique_new_paths(delta):
 
2375
    """Decorate a delta and check that the new paths in it are unique.
 
2376
 
 
2377
    :return: A generator over delta.
 
2378
    """
 
2379
    paths = set()
 
2380
    for item in delta:
 
2381
        length = len(paths) + 1
 
2382
        path = item[1]
 
2383
        if path is not None:
 
2384
            paths.add(path)
 
2385
            if len(paths) != length:
 
2386
                raise errors.InconsistentDelta(path, item[2], "repeated path")
 
2387
        yield item
 
2388
 
 
2389
 
 
2390
def _check_delta_unique_old_paths(delta):
 
2391
    """Decorate a delta and check that the old paths in it are unique.
 
2392
 
 
2393
    :return: A generator over delta.
 
2394
    """
 
2395
    paths = set()
 
2396
    for item in delta:
 
2397
        length = len(paths) + 1
 
2398
        path = item[0]
 
2399
        if path is not None:
 
2400
            paths.add(path)
 
2401
            if len(paths) != length:
 
2402
                raise errors.InconsistentDelta(path, item[2], "repeated path")
 
2403
        yield item
 
2404
 
 
2405
 
 
2406
def _check_delta_ids_are_valid(delta):
 
2407
    """Decorate a delta and check that the ids in it are valid.
 
2408
 
 
2409
    :return: A generator over delta.
 
2410
    """
 
2411
    for item in delta:
 
2412
        entry = item[3]
 
2413
        if item[2] is None:
 
2414
            raise errors.InconsistentDelta(item[0] or item[1], item[2],
 
2415
                "entry with file_id None %r" % entry)
 
2416
        if type(item[2]) != str:
 
2417
            raise errors.InconsistentDelta(item[0] or item[1], item[2],
 
2418
                "entry with non bytes file_id %r" % entry)
 
2419
        yield item
 
2420
 
 
2421
 
 
2422
def _check_delta_ids_match_entry(delta):
 
2423
    """Decorate a delta and check that the ids in it match the entry.file_id.
 
2424
 
 
2425
    :return: A generator over delta.
 
2426
    """
 
2427
    for item in delta:
 
2428
        entry = item[3]
 
2429
        if entry is not None:
 
2430
            if entry.file_id != item[2]:
 
2431
                raise errors.InconsistentDelta(item[0] or item[1], item[2],
 
2432
                    "mismatched id with %r" % entry)
 
2433
        yield item
 
2434
 
 
2435
 
 
2436
def _check_delta_new_path_entry_both_or_None(delta):
 
2437
    """Decorate a delta and check that the new_path and entry are paired.
 
2438
 
 
2439
    :return: A generator over delta.
 
2440
    """
 
2441
    for item in delta:
 
2442
        new_path = item[1]
 
2443
        entry = item[3]
 
2444
        if new_path is None and entry is not None:
 
2445
            raise errors.InconsistentDelta(item[0], item[1],
 
2446
                "Entry with no new_path")
 
2447
        if new_path is not None and entry is None:
 
2448
            raise errors.InconsistentDelta(new_path, item[1],
 
2449
                "new_path with no entry")
 
2450
        yield item