~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2010-08-24 21:59:21 UTC
  • mfrom: (5363.2.22 controldir-1)
  • Revision ID: pqm@pqm.ubuntu.com-20100824215921-p4nheij9k4x6i1jw
(jelmer) Split generic interface code out of bzrlib.bzrdir.BzrDir into
 bzrlib.controldir.ControlDir. (Jelmer Vernooij)

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
35
35
import re
36
36
import tarfile
37
37
 
38
 
import bzrlib
39
38
from bzrlib import (
40
39
    chk_map,
41
40
    errors,
42
41
    generate_ids,
43
42
    osutils,
44
 
    symbol_versioning,
45
43
    )
46
44
""")
47
45
 
49
47
    BzrCheckError,
50
48
    BzrError,
51
49
    )
52
 
from bzrlib.symbol_versioning import deprecated_in, deprecated_method
53
50
from bzrlib.trace import mutter
 
51
from bzrlib.static_tuple import StaticTuple
54
52
 
55
53
 
56
54
class InventoryEntry(object):
130
128
    RENAMED = 'renamed'
131
129
    MODIFIED_AND_RENAMED = 'modified and renamed'
132
130
 
133
 
    __slots__ = []
 
131
    __slots__ = ['file_id', 'revision', 'parent_id', 'name']
 
132
 
 
133
    # Attributes that all InventoryEntry instances are expected to have, but
 
134
    # that don't vary for all kinds of entry.  (e.g. symlink_target is only
 
135
    # relevant to InventoryLink, so there's no reason to make every
 
136
    # InventoryFile instance allocate space to hold a value for it.)
 
137
    # Attributes that only vary for files: executable, text_sha1, text_size,
 
138
    # text_id
 
139
    executable = False
 
140
    text_sha1 = None
 
141
    text_size = None
 
142
    text_id = None
 
143
    # Attributes that only vary for symlinks: symlink_target
 
144
    symlink_target = None
 
145
    # Attributes that only vary for tree-references: reference_revision
 
146
    reference_revision = None
 
147
 
134
148
 
135
149
    def detect_changes(self, old_entry):
136
150
        """Return a (text_modified, meta_modified) from this to old_entry.
175
189
                    candidates[ie.revision] = ie
176
190
        return candidates
177
191
 
178
 
    @deprecated_method(deprecated_in((1, 6, 0)))
179
 
    def get_tar_item(self, root, dp, now, tree):
180
 
        """Get a tarfile item and a file stream for its content."""
181
 
        item = tarfile.TarInfo(osutils.pathjoin(root, dp).encode('utf8'))
182
 
        # TODO: would be cool to actually set it to the timestamp of the
183
 
        # revision it was last changed
184
 
        item.mtime = now
185
 
        fileobj = self._put_in_tar(item, tree)
186
 
        return item, fileobj
187
 
 
188
192
    def has_text(self):
189
193
        """Return true if the object this entry represents has textual data.
190
194
 
196
200
        """
197
201
        return False
198
202
 
199
 
    def __init__(self, file_id, name, parent_id, text_id=None):
 
203
    def __init__(self, file_id, name, parent_id):
200
204
        """Create an InventoryEntry
201
205
 
202
206
        The filename must be a single component, relative to the
213
217
        """
214
218
        if '/' in name or '\\' in name:
215
219
            raise errors.InvalidEntryName(name=name)
216
 
        self.executable = False
 
220
        self.file_id = file_id
217
221
        self.revision = None
218
 
        self.text_sha1 = None
219
 
        self.text_size = None
220
 
        self.file_id = file_id
221
222
        self.name = name
222
 
        self.text_id = text_id
223
223
        self.parent_id = parent_id
224
 
        self.symlink_target = None
225
 
        self.reference_revision = None
226
224
 
227
225
    def kind_character(self):
228
226
        """Return a short kind indicator useful for appending to names."""
230
228
 
231
229
    known_kinds = ('file', 'directory', 'symlink')
232
230
 
233
 
    def _put_in_tar(self, item, tree):
234
 
        """populate item for stashing in a tar, and return the content stream.
235
 
 
236
 
        If no content is available, return None.
237
 
        """
238
 
        raise BzrError("don't know how to export {%s} of kind %r" %
239
 
                       (self.file_id, self.kind))
240
 
 
241
 
    @deprecated_method(deprecated_in((1, 6, 0)))
242
 
    def put_on_disk(self, dest, dp, tree):
243
 
        """Create a representation of self on disk in the prefix dest.
244
 
 
245
 
        This is a template method - implement _put_on_disk in subclasses.
246
 
        """
247
 
        fullpath = osutils.pathjoin(dest, dp)
248
 
        self._put_on_disk(fullpath, tree)
249
 
        # mutter("  export {%s} kind %s to %s", self.file_id,
250
 
        #         self.kind, fullpath)
251
 
 
252
 
    def _put_on_disk(self, fullpath, tree):
253
 
        """Put this entry onto disk at fullpath, from tree tree."""
254
 
        raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
255
 
 
256
231
    def sorted_children(self):
257
232
        return sorted(self.children.items())
258
233
 
396
371
        pass
397
372
 
398
373
 
399
 
class RootEntry(InventoryEntry):
400
 
 
401
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
402
 
                 'text_id', 'parent_id', 'children', 'executable',
403
 
                 'revision', 'symlink_target', 'reference_revision']
404
 
 
405
 
    def _check(self, checker, rev_id):
406
 
        """See InventoryEntry._check"""
407
 
 
408
 
    def __init__(self, file_id):
409
 
        self.file_id = file_id
410
 
        self.children = {}
411
 
        self.kind = 'directory'
412
 
        self.parent_id = None
413
 
        self.name = u''
414
 
        self.revision = None
415
 
        symbol_versioning.warn('RootEntry is deprecated as of bzr 0.10.'
416
 
                               '  Please use InventoryDirectory instead.',
417
 
                               DeprecationWarning, stacklevel=2)
418
 
 
419
 
    def __eq__(self, other):
420
 
        if not isinstance(other, RootEntry):
421
 
            return NotImplemented
422
 
 
423
 
        return (self.file_id == other.file_id) \
424
 
               and (self.children == other.children)
425
 
 
426
 
 
427
374
class InventoryDirectory(InventoryEntry):
428
375
    """A directory in an inventory."""
429
376
 
430
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
431
 
                 'text_id', 'parent_id', 'children', 'executable',
432
 
                 'revision', 'symlink_target', 'reference_revision']
 
377
    __slots__ = ['children']
 
378
 
 
379
    kind = 'directory'
433
380
 
434
381
    def _check(self, checker, rev_id):
435
382
        """See InventoryEntry._check"""
436
 
        if (self.text_sha1 is not None or self.text_size is not None or
437
 
            self.text_id is not None):
438
 
            checker._report_items.append('directory {%s} has text in revision {%s}'
439
 
                                % (self.file_id, rev_id))
440
 
        # Directories are stored as ''.
 
383
        # In non rich root repositories we do not expect a file graph for the
 
384
        # root.
 
385
        if self.name == '' and not checker.rich_roots:
 
386
            return
 
387
        # Directories are stored as an empty file, but the file should exist
 
388
        # to provide a per-fileid log. The hash of every directory content is
 
389
        # "da..." below (the sha1sum of '').
441
390
        checker.add_pending_item(rev_id,
442
391
            ('texts', self.file_id, self.revision), 'text',
443
392
             'da39a3ee5e6b4b0d3255bfef95601890afd80709')
452
401
    def __init__(self, file_id, name, parent_id):
453
402
        super(InventoryDirectory, self).__init__(file_id, name, parent_id)
454
403
        self.children = {}
455
 
        self.kind = 'directory'
456
404
 
457
405
    def kind_character(self):
458
406
        """See InventoryEntry.kind_character."""
459
407
        return '/'
460
408
 
461
 
    def _put_in_tar(self, item, tree):
462
 
        """See InventoryEntry._put_in_tar."""
463
 
        item.type = tarfile.DIRTYPE
464
 
        fileobj = None
465
 
        item.name += '/'
466
 
        item.size = 0
467
 
        item.mode = 0755
468
 
        return fileobj
469
 
 
470
 
    def _put_on_disk(self, fullpath, tree):
471
 
        """See InventoryEntry._put_on_disk."""
472
 
        os.mkdir(fullpath)
473
 
 
474
409
 
475
410
class InventoryFile(InventoryEntry):
476
411
    """A file in an inventory."""
477
412
 
478
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
479
 
                 'text_id', 'parent_id', 'children', 'executable',
480
 
                 'revision', 'symlink_target', 'reference_revision']
 
413
    __slots__ = ['text_sha1', 'text_size', 'text_id', 'executable']
 
414
 
 
415
    kind = 'file'
 
416
 
 
417
    def __init__(self, file_id, name, parent_id):
 
418
        super(InventoryFile, self).__init__(file_id, name, parent_id)
 
419
        self.text_sha1 = None
 
420
        self.text_size = None
 
421
        self.text_id = None
 
422
        self.executable = False
481
423
 
482
424
    def _check(self, checker, tree_revision_id):
483
425
        """See InventoryEntry._check"""
526
468
        """See InventoryEntry.has_text."""
527
469
        return True
528
470
 
529
 
    def __init__(self, file_id, name, parent_id):
530
 
        super(InventoryFile, self).__init__(file_id, name, parent_id)
531
 
        self.kind = 'file'
532
 
 
533
471
    def kind_character(self):
534
472
        """See InventoryEntry.kind_character."""
535
473
        return ''
536
474
 
537
 
    def _put_in_tar(self, item, tree):
538
 
        """See InventoryEntry._put_in_tar."""
539
 
        item.type = tarfile.REGTYPE
540
 
        fileobj = tree.get_file(self.file_id)
541
 
        item.size = self.text_size
542
 
        if tree.is_executable(self.file_id):
543
 
            item.mode = 0755
544
 
        else:
545
 
            item.mode = 0644
546
 
        return fileobj
547
 
 
548
 
    def _put_on_disk(self, fullpath, tree):
549
 
        """See InventoryEntry._put_on_disk."""
550
 
        osutils.pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
551
 
        if tree.is_executable(self.file_id):
552
 
            os.chmod(fullpath, 0755)
553
 
 
554
475
    def _read_tree_state(self, path, work_tree):
555
476
        """See InventoryEntry._read_tree_state."""
556
477
        self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
588
509
class InventoryLink(InventoryEntry):
589
510
    """A file in an inventory."""
590
511
 
591
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
592
 
                 'text_id', 'parent_id', 'children', 'executable',
593
 
                 'revision', 'symlink_target', 'reference_revision']
 
512
    __slots__ = ['symlink_target']
 
513
 
 
514
    kind = 'symlink'
 
515
 
 
516
    def __init__(self, file_id, name, parent_id):
 
517
        super(InventoryLink, self).__init__(file_id, name, parent_id)
 
518
        self.symlink_target = None
594
519
 
595
520
    def _check(self, checker, tree_revision_id):
596
521
        """See InventoryEntry._check"""
597
 
        if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
598
 
            checker._report_items.append(
599
 
               'symlink {%s} has text in revision {%s}'
600
 
                    % (self.file_id, tree_revision_id))
601
522
        if self.symlink_target is None:
602
523
            checker._report_items.append(
603
524
                'symlink {%s} has no target in revision {%s}'
641
562
        differ = DiffSymlink(old_tree, new_tree, output_to)
642
563
        return differ.diff_symlink(old_target, new_target)
643
564
 
644
 
    def __init__(self, file_id, name, parent_id):
645
 
        super(InventoryLink, self).__init__(file_id, name, parent_id)
646
 
        self.kind = 'symlink'
647
 
 
648
565
    def kind_character(self):
649
566
        """See InventoryEntry.kind_character."""
650
567
        return ''
651
568
 
652
 
    def _put_in_tar(self, item, tree):
653
 
        """See InventoryEntry._put_in_tar."""
654
 
        item.type = tarfile.SYMTYPE
655
 
        fileobj = None
656
 
        item.size = 0
657
 
        item.mode = 0755
658
 
        item.linkname = self.symlink_target
659
 
        return fileobj
660
 
 
661
 
    def _put_on_disk(self, fullpath, tree):
662
 
        """See InventoryEntry._put_on_disk."""
663
 
        try:
664
 
            os.symlink(self.symlink_target, fullpath)
665
 
        except OSError,e:
666
 
            raise BzrError("Failed to create symlink %r -> %r, error: %s" % (fullpath, self.symlink_target, e))
667
 
 
668
569
    def _read_tree_state(self, path, work_tree):
669
570
        """See InventoryEntry._read_tree_state."""
670
571
        self.symlink_target = work_tree.get_symlink_target(self.file_id)
682
583
 
683
584
class TreeReference(InventoryEntry):
684
585
 
 
586
    __slots__ = ['reference_revision']
 
587
 
685
588
    kind = 'tree-reference'
686
589
 
687
590
    def __init__(self, file_id, name, parent_id, revision=None,
743
646
        """
744
647
        return self.has_id(file_id)
745
648
 
 
649
    def has_filename(self, filename):
 
650
        return bool(self.path2id(filename))
 
651
 
746
652
    def id2path(self, file_id):
747
653
        """Return as a string the path to file_id.
748
654
 
751
657
        >>> e = i.add(InventoryFile('foo-id', 'foo.c', parent_id='src-id'))
752
658
        >>> print i.id2path('foo-id')
753
659
        src/foo.c
 
660
 
 
661
        :raises NoSuchId: If file_id is not present in the inventory.
754
662
        """
755
663
        # get all names, skipping root
756
664
        return '/'.join(reversed(
947
855
        descend(self.root, u'')
948
856
        return accum
949
857
 
950
 
    def path2id(self, name):
 
858
    def path2id(self, relpath):
951
859
        """Walk down through directories to return entry of last component.
952
860
 
953
 
        names may be either a list of path components, or a single
954
 
        string, in which case it is automatically split.
 
861
        :param relpath: may be either a list of path components, or a single
 
862
            string, in which case it is automatically split.
955
863
 
956
864
        This returns the entry of the last component in the path,
957
865
        which may be either a file or a directory.
958
866
 
959
867
        Returns None IFF the path is not found.
960
868
        """
961
 
        if isinstance(name, basestring):
962
 
            name = osutils.splitpath(name)
963
 
 
964
 
        # mutter("lookup path %r" % name)
 
869
        if isinstance(relpath, basestring):
 
870
            names = osutils.splitpath(relpath)
 
871
        else:
 
872
            names = relpath
965
873
 
966
874
        try:
967
875
            parent = self.root
970
878
            return None
971
879
        if parent is None:
972
880
            return None
973
 
        for f in name:
 
881
        for f in names:
974
882
            try:
975
883
                children = getattr(parent, 'children', None)
976
884
                if children is None:
1186
1094
            raise errors.InconsistentDelta("<deleted>", parent_id,
1187
1095
                "The file id was deleted but its children were not deleted.")
1188
1096
 
 
1097
    def create_by_apply_delta(self, inventory_delta, new_revision_id,
 
1098
                              propagate_caches=False):
 
1099
        """See CHKInventory.create_by_apply_delta()"""
 
1100
        new_inv = self.copy()
 
1101
        new_inv.apply_delta(inventory_delta)
 
1102
        new_inv.revision_id = new_revision_id
 
1103
        return new_inv
 
1104
 
1189
1105
    def _set_root(self, ie):
1190
1106
        self.root = ie
1191
1107
        self._byid = {self.root.file_id: self.root}
1262
1178
    def add(self, entry):
1263
1179
        """Add entry to inventory.
1264
1180
 
1265
 
        To add  a file to a branch ready to be committed, use Branch.add,
1266
 
        which calls this.
1267
 
 
1268
1181
        :return: entry
1269
1182
        """
1270
1183
        if entry.file_id in self._byid:
1363
1276
            yield ie
1364
1277
            file_id = ie.parent_id
1365
1278
 
1366
 
    def has_filename(self, filename):
1367
 
        return bool(self.path2id(filename))
1368
 
 
1369
1279
    def has_id(self, file_id):
1370
1280
        return (file_id in self._byid)
1371
1281
 
1538
1448
        else:
1539
1449
            raise ValueError("unknown kind %r" % entry.kind)
1540
1450
 
 
1451
    def _expand_fileids_to_parents_and_children(self, file_ids):
 
1452
        """Give a more wholistic view starting with the given file_ids.
 
1453
 
 
1454
        For any file_id which maps to a directory, we will include all children
 
1455
        of that directory. We will also include all directories which are
 
1456
        parents of the given file_ids, but we will not include their children.
 
1457
 
 
1458
        eg:
 
1459
          /     # TREE_ROOT
 
1460
          foo/  # foo-id
 
1461
            baz # baz-id
 
1462
            frob/ # frob-id
 
1463
              fringle # fringle-id
 
1464
          bar/  # bar-id
 
1465
            bing # bing-id
 
1466
 
 
1467
        if given [foo-id] we will include
 
1468
            TREE_ROOT as interesting parents
 
1469
        and 
 
1470
            foo-id, baz-id, frob-id, fringle-id
 
1471
        As interesting ids.
 
1472
        """
 
1473
        interesting = set()
 
1474
        # TODO: Pre-pass over the list of fileids to see if anything is already
 
1475
        #       deserialized in self._fileid_to_entry_cache
 
1476
 
 
1477
        directories_to_expand = set()
 
1478
        children_of_parent_id = {}
 
1479
        # It is okay if some of the fileids are missing
 
1480
        for entry in self._getitems(file_ids):
 
1481
            if entry.kind == 'directory':
 
1482
                directories_to_expand.add(entry.file_id)
 
1483
            interesting.add(entry.parent_id)
 
1484
            children_of_parent_id.setdefault(entry.parent_id, []
 
1485
                                             ).append(entry.file_id)
 
1486
 
 
1487
        # Now, interesting has all of the direct parents, but not the
 
1488
        # parents of those parents. It also may have some duplicates with
 
1489
        # specific_fileids
 
1490
        remaining_parents = interesting.difference(file_ids)
 
1491
        # When we hit the TREE_ROOT, we'll get an interesting parent of None,
 
1492
        # but we don't actually want to recurse into that
 
1493
        interesting.add(None) # this will auto-filter it in the loop
 
1494
        remaining_parents.discard(None) 
 
1495
        while remaining_parents:
 
1496
            next_parents = set()
 
1497
            for entry in self._getitems(remaining_parents):
 
1498
                next_parents.add(entry.parent_id)
 
1499
                children_of_parent_id.setdefault(entry.parent_id, []
 
1500
                                                 ).append(entry.file_id)
 
1501
            # Remove any search tips we've already processed
 
1502
            remaining_parents = next_parents.difference(interesting)
 
1503
            interesting.update(remaining_parents)
 
1504
            # We should probably also .difference(directories_to_expand)
 
1505
        interesting.update(file_ids)
 
1506
        interesting.discard(None)
 
1507
        while directories_to_expand:
 
1508
            # Expand directories by looking in the
 
1509
            # parent_id_basename_to_file_id map
 
1510
            keys = [StaticTuple(f,).intern() for f in directories_to_expand]
 
1511
            directories_to_expand = set()
 
1512
            items = self.parent_id_basename_to_file_id.iteritems(keys)
 
1513
            next_file_ids = set([item[1] for item in items])
 
1514
            next_file_ids = next_file_ids.difference(interesting)
 
1515
            interesting.update(next_file_ids)
 
1516
            for entry in self._getitems(next_file_ids):
 
1517
                if entry.kind == 'directory':
 
1518
                    directories_to_expand.add(entry.file_id)
 
1519
                children_of_parent_id.setdefault(entry.parent_id, []
 
1520
                                                 ).append(entry.file_id)
 
1521
        return interesting, children_of_parent_id
 
1522
 
 
1523
    def filter(self, specific_fileids):
 
1524
        """Get an inventory view filtered against a set of file-ids.
 
1525
 
 
1526
        Children of directories and parents are included.
 
1527
 
 
1528
        The result may or may not reference the underlying inventory
 
1529
        so it should be treated as immutable.
 
1530
        """
 
1531
        (interesting,
 
1532
         parent_to_children) = self._expand_fileids_to_parents_and_children(
 
1533
                                specific_fileids)
 
1534
        # There is some overlap here, but we assume that all interesting items
 
1535
        # are in the _fileid_to_entry_cache because we had to read them to
 
1536
        # determine if they were a dir we wanted to recurse, or just a file
 
1537
        # This should give us all the entries we'll want to add, so start
 
1538
        # adding
 
1539
        other = Inventory(self.root_id)
 
1540
        other.root.revision = self.root.revision
 
1541
        other.revision_id = self.revision_id
 
1542
        if not interesting or not parent_to_children:
 
1543
            # empty filter, or filtering entrys that don't exist
 
1544
            # (if even 1 existed, then we would have populated
 
1545
            # parent_to_children with at least the tree root.)
 
1546
            return other
 
1547
        cache = self._fileid_to_entry_cache
 
1548
        remaining_children = collections.deque(parent_to_children[self.root_id])
 
1549
        while remaining_children:
 
1550
            file_id = remaining_children.popleft()
 
1551
            ie = cache[file_id]
 
1552
            if ie.kind == 'directory':
 
1553
                ie = ie.copy() # We create a copy to depopulate the .children attribute
 
1554
            # TODO: depending on the uses of 'other' we should probably alwyas
 
1555
            #       '.copy()' to prevent someone from mutating other and
 
1556
            #       invaliding our internal cache
 
1557
            other.add(ie)
 
1558
            if file_id in parent_to_children:
 
1559
                remaining_children.extend(parent_to_children[file_id])
 
1560
        return other
 
1561
 
1541
1562
    @staticmethod
1542
1563
    def _bytes_to_utf8name_key(bytes):
1543
1564
        """Get the file_id, revision_id key out of bytes."""
1545
1566
        # to filter out empty names because of non rich-root...
1546
1567
        sections = bytes.split('\n')
1547
1568
        kind, file_id = sections[0].split(': ')
1548
 
        return (sections[2], file_id, sections[3])
 
1569
        return (sections[2], intern(file_id), intern(sections[3]))
1549
1570
 
1550
1571
    def _bytes_to_entry(self, bytes):
1551
1572
        """Deserialise a serialised entry."""
1573
1594
            result.reference_revision = sections[4]
1574
1595
        else:
1575
1596
            raise ValueError("Not a serialised entry %r" % bytes)
1576
 
        result.revision = sections[3]
 
1597
        result.file_id = intern(result.file_id)
 
1598
        result.revision = intern(sections[3])
1577
1599
        if result.parent_id == '':
1578
1600
            result.parent_id = None
1579
1601
        self._fileid_to_entry_cache[result.file_id] = result
1677
1699
                        pass
1678
1700
                deletes.add(file_id)
1679
1701
            else:
1680
 
                new_key = (file_id,)
 
1702
                new_key = StaticTuple(file_id,)
1681
1703
                new_value = result._entry_to_bytes(entry)
1682
1704
                # Update caches. It's worth doing this whether
1683
1705
                # we're propagating the old caches or not.
1686
1708
            if old_path is None:
1687
1709
                old_key = None
1688
1710
            else:
1689
 
                old_key = (file_id,)
 
1711
                old_key = StaticTuple(file_id,)
1690
1712
                if self.id2path(file_id) != old_path:
1691
1713
                    raise errors.InconsistentDelta(old_path, file_id,
1692
1714
                        "Entry was at wrong other path %r." %
1693
1715
                        self.id2path(file_id))
1694
1716
                altered.add(file_id)
1695
 
            id_to_entry_delta.append((old_key, new_key, new_value))
 
1717
            id_to_entry_delta.append(StaticTuple(old_key, new_key, new_value))
1696
1718
            if result.parent_id_basename_to_file_id is not None:
1697
1719
                # parent_id, basename changes
1698
1720
                if old_path is None:
1785
1807
                raise errors.BzrError('Duplicate key in inventory: %r\n%r'
1786
1808
                                      % (key, bytes))
1787
1809
            info[key] = value
1788
 
        revision_id = info['revision_id']
1789
 
        root_id = info['root_id']
1790
 
        search_key_name = info.get('search_key_name', 'plain')
1791
 
        parent_id_basename_to_file_id = info.get(
1792
 
            'parent_id_basename_to_file_id', None)
 
1810
        revision_id = intern(info['revision_id'])
 
1811
        root_id = intern(info['root_id'])
 
1812
        search_key_name = intern(info.get('search_key_name', 'plain'))
 
1813
        parent_id_basename_to_file_id = intern(info.get(
 
1814
            'parent_id_basename_to_file_id', None))
 
1815
        if not parent_id_basename_to_file_id.startswith('sha1:'):
 
1816
            raise ValueError('parent_id_basename_to_file_id should be a sha1'
 
1817
                             ' key not %r' % (parent_id_basename_to_file_id,))
1793
1818
        id_to_entry = info['id_to_entry']
 
1819
        if not id_to_entry.startswith('sha1:'):
 
1820
            raise ValueError('id_to_entry should be a sha1'
 
1821
                             ' key not %r' % (id_to_entry,))
1794
1822
 
1795
1823
        result = CHKInventory(search_key_name)
1796
1824
        result.revision_id = revision_id
1799
1827
                            result._search_key_name)
1800
1828
        if parent_id_basename_to_file_id is not None:
1801
1829
            result.parent_id_basename_to_file_id = chk_map.CHKMap(
1802
 
                chk_store, (parent_id_basename_to_file_id,),
 
1830
                chk_store, StaticTuple(parent_id_basename_to_file_id,),
1803
1831
                search_key_func=search_key_func)
1804
1832
        else:
1805
1833
            result.parent_id_basename_to_file_id = None
1806
1834
 
1807
 
        result.id_to_entry = chk_map.CHKMap(chk_store, (id_to_entry,),
 
1835
        result.id_to_entry = chk_map.CHKMap(chk_store,
 
1836
                                            StaticTuple(id_to_entry,),
1808
1837
                                            search_key_func=search_key_func)
1809
1838
        if (result.revision_id,) != expected_revision_id:
1810
1839
            raise ValueError("Mismatched revision id and expected: %r, %r" %
1832
1861
        id_to_entry_dict = {}
1833
1862
        parent_id_basename_dict = {}
1834
1863
        for path, entry in inventory.iter_entries():
1835
 
            id_to_entry_dict[(entry.file_id,)] = entry_to_bytes(entry)
 
1864
            key = StaticTuple(entry.file_id,).intern()
 
1865
            id_to_entry_dict[key] = entry_to_bytes(entry)
1836
1866
            p_id_key = parent_id_basename_key(entry)
1837
1867
            parent_id_basename_dict[p_id_key] = entry.file_id
1838
1868
 
1861
1891
            parent_id = entry.parent_id
1862
1892
        else:
1863
1893
            parent_id = ''
1864
 
        return parent_id, entry.name.encode('utf8')
 
1894
        return StaticTuple(parent_id, entry.name.encode('utf8')).intern()
1865
1895
 
1866
1896
    def __getitem__(self, file_id):
1867
1897
        """map a single file_id -> InventoryEntry."""
1872
1902
            return result
1873
1903
        try:
1874
1904
            return self._bytes_to_entry(
1875
 
                self.id_to_entry.iteritems([(file_id,)]).next()[1])
 
1905
                self.id_to_entry.iteritems([StaticTuple(file_id,)]).next()[1])
1876
1906
        except StopIteration:
1877
1907
            # really we're passing an inventory, not a tree...
1878
1908
            raise errors.NoSuchId(self, file_id)
1879
1909
 
 
1910
    def _getitems(self, file_ids):
 
1911
        """Similar to __getitem__, but lets you query for multiple.
 
1912
        
 
1913
        The returned order is undefined. And currently if an item doesn't
 
1914
        exist, it isn't included in the output.
 
1915
        """
 
1916
        result = []
 
1917
        remaining = []
 
1918
        for file_id in file_ids:
 
1919
            entry = self._fileid_to_entry_cache.get(file_id, None)
 
1920
            if entry is None:
 
1921
                remaining.append(file_id)
 
1922
            else:
 
1923
                result.append(entry)
 
1924
        file_keys = [StaticTuple(f,).intern() for f in remaining]
 
1925
        for file_key, value in self.id_to_entry.iteritems(file_keys):
 
1926
            entry = self._bytes_to_entry(value)
 
1927
            result.append(entry)
 
1928
            self._fileid_to_entry_cache[entry.file_id] = entry
 
1929
        return result
 
1930
 
1880
1931
    def has_id(self, file_id):
1881
1932
        # Perhaps have an explicit 'contains' method on CHKMap ?
1882
1933
        if self._fileid_to_entry_cache.get(file_id, None) is not None:
1883
1934
            return True
1884
 
        return len(list(self.id_to_entry.iteritems([(file_id,)]))) == 1
 
1935
        return len(list(
 
1936
            self.id_to_entry.iteritems([StaticTuple(file_id,)]))) == 1
1885
1937
 
1886
1938
    def is_root(self, file_id):
1887
1939
        return file_id == self.root_id
2016
2068
            delta.append((old_path, new_path, file_id, entry))
2017
2069
        return delta
2018
2070
 
2019
 
    def path2id(self, name):
 
2071
    def path2id(self, relpath):
2020
2072
        """See CommonInventory.path2id()."""
2021
2073
        # TODO: perhaps support negative hits?
2022
 
        result = self._path_to_fileid_cache.get(name, None)
 
2074
        result = self._path_to_fileid_cache.get(relpath, None)
2023
2075
        if result is not None:
2024
2076
            return result
2025
 
        if isinstance(name, basestring):
2026
 
            names = osutils.splitpath(name)
 
2077
        if isinstance(relpath, basestring):
 
2078
            names = osutils.splitpath(relpath)
2027
2079
        else:
2028
 
            names = name
 
2080
            names = relpath
2029
2081
        current_id = self.root_id
2030
2082
        if current_id is None:
2031
2083
            return None
2032
2084
        parent_id_index = self.parent_id_basename_to_file_id
 
2085
        cur_path = None
2033
2086
        for basename in names:
2034
 
            # TODO: Cache each path we figure out in this function.
 
2087
            if cur_path is None:
 
2088
                cur_path = basename
 
2089
            else:
 
2090
                cur_path = cur_path + '/' + basename
2035
2091
            basename_utf8 = basename.encode('utf8')
2036
 
            key_filter = [(current_id, basename_utf8)]
2037
 
            file_id = None
2038
 
            for (parent_id, name_utf8), file_id in parent_id_index.iteritems(
2039
 
                key_filter=key_filter):
2040
 
                if parent_id != current_id or name_utf8 != basename_utf8:
2041
 
                    raise errors.BzrError("corrupt inventory lookup! "
2042
 
                        "%r %r %r %r" % (parent_id, current_id, name_utf8,
2043
 
                        basename_utf8))
 
2092
            file_id = self._path_to_fileid_cache.get(cur_path, None)
2044
2093
            if file_id is None:
2045
 
                return None
 
2094
                key_filter = [StaticTuple(current_id, basename_utf8)]
 
2095
                items = parent_id_index.iteritems(key_filter)
 
2096
                for (parent_id, name_utf8), file_id in items:
 
2097
                    if parent_id != current_id or name_utf8 != basename_utf8:
 
2098
                        raise errors.BzrError("corrupt inventory lookup! "
 
2099
                            "%r %r %r %r" % (parent_id, current_id, name_utf8,
 
2100
                            basename_utf8))
 
2101
                if file_id is None:
 
2102
                    return None
 
2103
                else:
 
2104
                    self._path_to_fileid_cache[cur_path] = file_id
2046
2105
            current_id = file_id
2047
 
        self._path_to_fileid_cache[name] = current_id
2048
2106
        return current_id
2049
2107
 
2050
2108
    def to_lines(self):
2055
2113
            lines.append('search_key_name: %s\n' % (self._search_key_name,))
2056
2114
            lines.append("root_id: %s\n" % self.root_id)
2057
2115
            lines.append('parent_id_basename_to_file_id: %s\n' %
2058
 
                self.parent_id_basename_to_file_id.key())
 
2116
                (self.parent_id_basename_to_file_id.key()[0],))
2059
2117
            lines.append("revision_id: %s\n" % self.revision_id)
2060
 
            lines.append("id_to_entry: %s\n" % self.id_to_entry.key())
 
2118
            lines.append("id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
2061
2119
        else:
2062
2120
            lines.append("revision_id: %s\n" % self.revision_id)
2063
2121
            lines.append("root_id: %s\n" % self.root_id)
2064
2122
            if self.parent_id_basename_to_file_id is not None:
2065
2123
                lines.append('parent_id_basename_to_file_id: %s\n' %
2066
 
                    self.parent_id_basename_to_file_id.key())
2067
 
            lines.append("id_to_entry: %s\n" % self.id_to_entry.key())
 
2124
                    (self.parent_id_basename_to_file_id.key()[0],))
 
2125
            lines.append("id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
2068
2126
        return lines
2069
2127
 
2070
2128
    @property
2076
2134
class CHKInventoryDirectory(InventoryDirectory):
2077
2135
    """A directory in an inventory."""
2078
2136
 
2079
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
2080
 
                 'text_id', 'parent_id', '_children', 'executable',
2081
 
                 'revision', 'symlink_target', 'reference_revision',
2082
 
                 '_chk_inventory']
 
2137
    __slots__ = ['_children', '_chk_inventory']
2083
2138
 
2084
2139
    def __init__(self, file_id, name, parent_id, chk_inventory):
2085
2140
        # Don't call InventoryDirectory.__init__ - it isn't right for this
2086
2141
        # class.
2087
2142
        InventoryEntry.__init__(self, file_id, name, parent_id)
2088
2143
        self._children = None
2089
 
        self.kind = 'directory'
2090
2144
        self._chk_inventory = chk_inventory
2091
2145
 
2092
2146
    @property
2111
2165
        parent_id_index = self._chk_inventory.parent_id_basename_to_file_id
2112
2166
        child_keys = set()
2113
2167
        for (parent_id, name_utf8), file_id in parent_id_index.iteritems(
2114
 
            key_filter=[(self.file_id,)]):
2115
 
            child_keys.add((file_id,))
 
2168
            key_filter=[StaticTuple(self.file_id,)]):
 
2169
            child_keys.add(StaticTuple(file_id,))
2116
2170
        cached = set()
2117
2171
        for file_id_key in child_keys:
2118
2172
            entry = self._chk_inventory._fileid_to_entry_cache.get(
2151
2205
    try:
2152
2206
        factory = entry_factory[kind]
2153
2207
    except KeyError:
2154
 
        raise BzrError("unknown kind %r" % kind)
 
2208
        raise errors.BadFileKindError(name, kind)
2155
2209
    return factory(file_id, name, parent_id)
2156
2210
 
2157
2211