~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Benoît Pierre
  • Date: 2009-11-02 22:24:29 UTC
  • mto: (4634.96.1 integration-2.0)
  • mto: This revision was merged to the branch mainline in revision 4798.
  • Revision ID: benoit.pierre@gmail.com-20091102222429-xqdyo6n8odh3xbbd
Small fix for handling of short option names in shellcomplete_on_options.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2011 Canonical Ltd
 
1
# Copyright (C) 2005, 2006, 2007 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
31
31
lazy_import(globals(), """
32
32
import collections
33
33
import copy
 
34
import os
34
35
import re
35
36
import tarfile
36
37
 
 
38
import bzrlib
37
39
from bzrlib import (
38
40
    chk_map,
39
41
    errors,
40
42
    generate_ids,
41
43
    osutils,
 
44
    symbol_versioning,
42
45
    )
43
46
""")
44
47
 
45
 
from bzrlib import (
46
 
    lazy_regex,
47
 
    trace,
48
 
    )
49
 
 
50
 
from bzrlib.static_tuple import StaticTuple
51
 
from bzrlib.symbol_versioning import (
52
 
    deprecated_in,
53
 
    deprecated_method,
54
 
    )
 
48
from bzrlib.errors import (
 
49
    BzrCheckError,
 
50
    BzrError,
 
51
    )
 
52
from bzrlib.symbol_versioning import deprecated_in, deprecated_method
 
53
from bzrlib.trace import mutter
55
54
 
56
55
 
57
56
class InventoryEntry(object):
104
103
    InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
105
104
    >>> i.path2id('src/wibble')
106
105
    '2325'
 
106
    >>> '2325' in i
 
107
    True
107
108
    >>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
108
109
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None, revision=None)
109
110
    >>> i['2326']
129
130
    RENAMED = 'renamed'
130
131
    MODIFIED_AND_RENAMED = 'modified and renamed'
131
132
 
132
 
    __slots__ = ['file_id', 'revision', 'parent_id', 'name']
133
 
 
134
 
    # Attributes that all InventoryEntry instances are expected to have, but
135
 
    # that don't vary for all kinds of entry.  (e.g. symlink_target is only
136
 
    # relevant to InventoryLink, so there's no reason to make every
137
 
    # InventoryFile instance allocate space to hold a value for it.)
138
 
    # Attributes that only vary for files: executable, text_sha1, text_size,
139
 
    # text_id
140
 
    executable = False
141
 
    text_sha1 = None
142
 
    text_size = None
143
 
    text_id = None
144
 
    # Attributes that only vary for symlinks: symlink_target
145
 
    symlink_target = None
146
 
    # Attributes that only vary for tree-references: reference_revision
147
 
    reference_revision = None
148
 
 
 
133
    __slots__ = []
149
134
 
150
135
    def detect_changes(self, old_entry):
151
136
        """Return a (text_modified, meta_modified) from this to old_entry.
172
157
        candidates = {}
173
158
        # identify candidate head revision ids.
174
159
        for inv in previous_inventories:
175
 
            if inv.has_id(self.file_id):
 
160
            if self.file_id in inv:
176
161
                ie = inv[self.file_id]
177
162
                if ie.revision in candidates:
178
163
                    # same revision value in two different inventories:
190
175
                    candidates[ie.revision] = ie
191
176
        return candidates
192
177
 
 
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
 
193
188
    def has_text(self):
194
189
        """Return true if the object this entry represents has textual data.
195
190
 
201
196
        """
202
197
        return False
203
198
 
204
 
    def __init__(self, file_id, name, parent_id):
 
199
    def __init__(self, file_id, name, parent_id, text_id=None):
205
200
        """Create an InventoryEntry
206
201
 
207
202
        The filename must be a single component, relative to the
218
213
        """
219
214
        if '/' in name or '\\' in name:
220
215
            raise errors.InvalidEntryName(name=name)
 
216
        self.executable = False
 
217
        self.revision = None
 
218
        self.text_sha1 = None
 
219
        self.text_size = None
221
220
        self.file_id = file_id
222
 
        self.revision = None
223
221
        self.name = name
 
222
        self.text_id = text_id
224
223
        self.parent_id = parent_id
 
224
        self.symlink_target = None
 
225
        self.reference_revision = None
225
226
 
226
227
    def kind_character(self):
227
228
        """Return a short kind indicator useful for appending to names."""
228
 
        raise errors.BzrError('unknown kind %r' % self.kind)
 
229
        raise BzrError('unknown kind %r' % self.kind)
229
230
 
230
231
    known_kinds = ('file', 'directory', 'symlink')
231
232
 
 
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
 
232
256
    def sorted_children(self):
233
257
        return sorted(self.children.items())
234
258
 
251
275
        """
252
276
        if self.parent_id is not None:
253
277
            if not inv.has_id(self.parent_id):
254
 
                raise errors.BzrCheckError(
255
 
                    'missing parent {%s} in inventory for revision {%s}' % (
256
 
                        self.parent_id, rev_id))
 
278
                raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
 
279
                        % (self.parent_id, rev_id))
257
280
        checker._add_entry_to_text_key_references(inv, self)
258
281
        self._check(checker, rev_id)
259
282
 
373
396
        pass
374
397
 
375
398
 
 
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
 
376
427
class InventoryDirectory(InventoryEntry):
377
428
    """A directory in an inventory."""
378
429
 
379
 
    __slots__ = ['children']
380
 
 
381
 
    kind = 'directory'
 
430
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
431
                 'text_id', 'parent_id', 'children', 'executable',
 
432
                 'revision', 'symlink_target', 'reference_revision']
382
433
 
383
434
    def _check(self, checker, rev_id):
384
435
        """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))
385
440
        # In non rich root repositories we do not expect a file graph for the
386
441
        # root.
387
442
        if self.name == '' and not checker.rich_roots:
403
458
    def __init__(self, file_id, name, parent_id):
404
459
        super(InventoryDirectory, self).__init__(file_id, name, parent_id)
405
460
        self.children = {}
 
461
        self.kind = 'directory'
406
462
 
407
463
    def kind_character(self):
408
464
        """See InventoryEntry.kind_character."""
409
465
        return '/'
410
466
 
 
467
    def _put_in_tar(self, item, tree):
 
468
        """See InventoryEntry._put_in_tar."""
 
469
        item.type = tarfile.DIRTYPE
 
470
        fileobj = None
 
471
        item.name += '/'
 
472
        item.size = 0
 
473
        item.mode = 0755
 
474
        return fileobj
 
475
 
 
476
    def _put_on_disk(self, fullpath, tree):
 
477
        """See InventoryEntry._put_on_disk."""
 
478
        os.mkdir(fullpath)
 
479
 
411
480
 
412
481
class InventoryFile(InventoryEntry):
413
482
    """A file in an inventory."""
414
483
 
415
 
    __slots__ = ['text_sha1', 'text_size', 'text_id', 'executable']
416
 
 
417
 
    kind = 'file'
418
 
 
419
 
    def __init__(self, file_id, name, parent_id):
420
 
        super(InventoryFile, self).__init__(file_id, name, parent_id)
421
 
        self.text_sha1 = None
422
 
        self.text_size = None
423
 
        self.text_id = None
424
 
        self.executable = False
 
484
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
485
                 'text_id', 'parent_id', 'children', 'executable',
 
486
                 'revision', 'symlink_target', 'reference_revision']
425
487
 
426
488
    def _check(self, checker, tree_revision_id):
427
489
        """See InventoryEntry._check"""
470
532
        """See InventoryEntry.has_text."""
471
533
        return True
472
534
 
 
535
    def __init__(self, file_id, name, parent_id):
 
536
        super(InventoryFile, self).__init__(file_id, name, parent_id)
 
537
        self.kind = 'file'
 
538
 
473
539
    def kind_character(self):
474
540
        """See InventoryEntry.kind_character."""
475
541
        return ''
476
542
 
 
543
    def _put_in_tar(self, item, tree):
 
544
        """See InventoryEntry._put_in_tar."""
 
545
        item.type = tarfile.REGTYPE
 
546
        fileobj = tree.get_file(self.file_id)
 
547
        item.size = self.text_size
 
548
        if tree.is_executable(self.file_id):
 
549
            item.mode = 0755
 
550
        else:
 
551
            item.mode = 0644
 
552
        return fileobj
 
553
 
 
554
    def _put_on_disk(self, fullpath, tree):
 
555
        """See InventoryEntry._put_on_disk."""
 
556
        osutils.pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
 
557
        if tree.is_executable(self.file_id):
 
558
            os.chmod(fullpath, 0755)
 
559
 
477
560
    def _read_tree_state(self, path, work_tree):
478
561
        """See InventoryEntry._read_tree_state."""
479
562
        self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
511
594
class InventoryLink(InventoryEntry):
512
595
    """A file in an inventory."""
513
596
 
514
 
    __slots__ = ['symlink_target']
515
 
 
516
 
    kind = 'symlink'
517
 
 
518
 
    def __init__(self, file_id, name, parent_id):
519
 
        super(InventoryLink, self).__init__(file_id, name, parent_id)
520
 
        self.symlink_target = None
 
597
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
598
                 'text_id', 'parent_id', 'children', 'executable',
 
599
                 'revision', 'symlink_target', 'reference_revision']
521
600
 
522
601
    def _check(self, checker, tree_revision_id):
523
602
        """See InventoryEntry._check"""
 
603
        if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
 
604
            checker._report_items.append(
 
605
               'symlink {%s} has text in revision {%s}'
 
606
                    % (self.file_id, tree_revision_id))
524
607
        if self.symlink_target is None:
525
608
            checker._report_items.append(
526
609
                'symlink {%s} has no target in revision {%s}'
541
624
        # FIXME: which _modified field should we use ? RBC 20051003
542
625
        text_modified = (self.symlink_target != old_entry.symlink_target)
543
626
        if text_modified:
544
 
            trace.mutter("    symlink target changed")
 
627
            mutter("    symlink target changed")
545
628
        meta_modified = False
546
629
        return text_modified, meta_modified
547
630
 
564
647
        differ = DiffSymlink(old_tree, new_tree, output_to)
565
648
        return differ.diff_symlink(old_target, new_target)
566
649
 
 
650
    def __init__(self, file_id, name, parent_id):
 
651
        super(InventoryLink, self).__init__(file_id, name, parent_id)
 
652
        self.kind = 'symlink'
 
653
 
567
654
    def kind_character(self):
568
655
        """See InventoryEntry.kind_character."""
569
656
        return ''
570
657
 
 
658
    def _put_in_tar(self, item, tree):
 
659
        """See InventoryEntry._put_in_tar."""
 
660
        item.type = tarfile.SYMTYPE
 
661
        fileobj = None
 
662
        item.size = 0
 
663
        item.mode = 0755
 
664
        item.linkname = self.symlink_target
 
665
        return fileobj
 
666
 
 
667
    def _put_on_disk(self, fullpath, tree):
 
668
        """See InventoryEntry._put_on_disk."""
 
669
        try:
 
670
            os.symlink(self.symlink_target, fullpath)
 
671
        except OSError,e:
 
672
            raise BzrError("Failed to create symlink %r -> %r, error: %s" % (fullpath, self.symlink_target, e))
 
673
 
571
674
    def _read_tree_state(self, path, work_tree):
572
675
        """See InventoryEntry._read_tree_state."""
573
676
        self.symlink_target = work_tree.get_symlink_target(self.file_id)
585
688
 
586
689
class TreeReference(InventoryEntry):
587
690
 
588
 
    __slots__ = ['reference_revision']
589
 
 
590
691
    kind = 'tree-reference'
591
692
 
592
693
    def __init__(self, file_id, name, parent_id, revision=None,
631
732
    inserted, other than through the Inventory API.
632
733
    """
633
734
 
634
 
    @deprecated_method(deprecated_in((2, 4, 0)))
635
735
    def __contains__(self, file_id):
636
736
        """True if this entry contains a file with given id.
637
737
 
638
738
        >>> inv = Inventory()
639
739
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
640
740
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
641
 
        >>> inv.has_id('123')
 
741
        >>> '123' in inv
642
742
        True
643
 
        >>> inv.has_id('456')
 
743
        >>> '456' in inv
644
744
        False
645
745
 
646
746
        Note that this method along with __iter__ are not encouraged for use as
721
821
                # if we finished all children, pop it off the stack
722
822
                stack.pop()
723
823
 
724
 
    def _preload_cache(self):
725
 
        """Populate any caches, we are about to access all items.
726
 
        
727
 
        The default implementation does nothing, because CommonInventory doesn't
728
 
        have a cache.
729
 
        """
730
 
        pass
731
 
    
732
824
    def iter_entries_by_dir(self, from_dir=None, specific_file_ids=None,
733
825
        yield_parents=False):
734
826
        """Iterate over the entries in a directory first order.
747
839
            specific_file_ids = set(specific_file_ids)
748
840
        # TODO? Perhaps this should return the from_dir so that the root is
749
841
        # yielded? or maybe an option?
750
 
        if from_dir is None and specific_file_ids is None:
751
 
            # They are iterating from the root, and have not specified any
752
 
            # specific entries to look at. All current callers fully consume the
753
 
            # iterator, so we can safely assume we are accessing all entries
754
 
            self._preload_cache()
755
842
        if from_dir is None:
756
843
            if self.root is None:
757
844
                return
759
846
            if (not yield_parents and specific_file_ids is not None and
760
847
                len(specific_file_ids) == 1):
761
848
                file_id = list(specific_file_ids)[0]
762
 
                if self.has_id(file_id):
 
849
                if file_id in self:
763
850
                    yield self.id2path(file_id), self[file_id]
764
851
                return
765
852
            from_dir = self.root
775
862
            parents = set()
776
863
            byid = self
777
864
            def add_ancestors(file_id):
778
 
                if not byid.has_id(file_id):
 
865
                if file_id not in byid:
779
866
                    return
780
867
                parent_id = byid[file_id].parent_id
781
868
                if parent_id is None:
825
912
                    file_id, self[file_id]))
826
913
        return delta
827
914
 
 
915
    def _get_mutable_inventory(self):
 
916
        """Returns a mutable copy of the object.
 
917
 
 
918
        Some inventories are immutable, yet working trees, for example, needs
 
919
        to mutate exisiting inventories instead of creating a new one.
 
920
        """
 
921
        raise NotImplementedError(self._get_mutable_inventory)
 
922
 
828
923
    def make_entry(self, kind, name, parent_id, file_id=None):
829
924
        """Simple thunk to bzrlib.inventory.make_entry."""
830
925
        return make_entry(kind, name, parent_id, file_id)
844
939
                if ie.kind == 'directory':
845
940
                    descend(ie, child_path)
846
941
 
847
 
        if self.root is not None:
848
 
            descend(self.root, u'')
 
942
        descend(self.root, u'')
849
943
        return accum
850
944
 
851
945
    def directories(self):
864
958
        descend(self.root, u'')
865
959
        return accum
866
960
 
867
 
    def path2id(self, relpath):
 
961
    def path2id(self, name):
868
962
        """Walk down through directories to return entry of last component.
869
963
 
870
 
        :param relpath: may be either a list of path components, or a single
871
 
            string, in which case it is automatically split.
 
964
        names may be either a list of path components, or a single
 
965
        string, in which case it is automatically split.
872
966
 
873
967
        This returns the entry of the last component in the path,
874
968
        which may be either a file or a directory.
875
969
 
876
970
        Returns None IFF the path is not found.
877
971
        """
878
 
        if isinstance(relpath, basestring):
879
 
            names = osutils.splitpath(relpath)
880
 
        else:
881
 
            names = relpath
 
972
        if isinstance(name, basestring):
 
973
            name = osutils.splitpath(name)
 
974
 
 
975
        # mutter("lookup path %r" % name)
882
976
 
883
977
        try:
884
978
            parent = self.root
887
981
            return None
888
982
        if parent is None:
889
983
            return None
890
 
        for f in names:
 
984
        for f in name:
891
985
            try:
892
986
                children = getattr(parent, 'children', None)
893
987
                if children is None:
965
1059
 
966
1060
    >>> inv.path2id('hello.c')
967
1061
    '123-123'
968
 
    >>> inv.has_id('123-123')
 
1062
    >>> '123-123' in inv
969
1063
    True
970
1064
 
971
1065
    There are iterators over the contents:
1128
1222
            other.add(entry.copy())
1129
1223
        return other
1130
1224
 
 
1225
    def _get_mutable_inventory(self):
 
1226
        """See CommonInventory._get_mutable_inventory."""
 
1227
        return copy.deepcopy(self)
 
1228
 
1131
1229
    def __iter__(self):
1132
1230
        """Iterate over all file-ids."""
1133
1231
        return iter(self._byid)
1173
1271
    def _add_child(self, entry):
1174
1272
        """Add an entry to the inventory, without adding it to its parent"""
1175
1273
        if entry.file_id in self._byid:
1176
 
            raise errors.BzrError(
1177
 
                "inventory already contains entry with id {%s}" %
1178
 
                entry.file_id)
 
1274
            raise BzrError("inventory already contains entry with id {%s}" %
 
1275
                           entry.file_id)
1179
1276
        self._byid[entry.file_id] = entry
1180
1277
        for child in getattr(entry, 'children', {}).itervalues():
1181
1278
            self._add_child(child)
1184
1281
    def add(self, entry):
1185
1282
        """Add entry to inventory.
1186
1283
 
 
1284
        To add  a file to a branch ready to be committed, use Branch.add,
 
1285
        which calls this.
 
1286
 
1187
1287
        :return: entry
1188
1288
        """
1189
1289
        if entry.file_id in self._byid:
1234
1334
        >>> inv = Inventory()
1235
1335
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1236
1336
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1237
 
        >>> inv.has_id('123')
 
1337
        >>> '123' in inv
1238
1338
        True
1239
1339
        >>> del inv['123']
1240
 
        >>> inv.has_id('123')
 
1340
        >>> '123' in inv
1241
1341
        False
1242
1342
        """
1243
1343
        ie = self[file_id]
1345
1445
        """
1346
1446
        new_name = ensure_normalized_name(new_name)
1347
1447
        if not is_valid_name(new_name):
1348
 
            raise errors.BzrError("not an acceptable filename: %r" % new_name)
 
1448
            raise BzrError("not an acceptable filename: %r" % new_name)
1349
1449
 
1350
1450
        new_parent = self._byid[new_parent_id]
1351
1451
        if new_name in new_parent.children:
1352
 
            raise errors.BzrError("%r already exists in %r" %
1353
 
                (new_name, self.id2path(new_parent_id)))
 
1452
            raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
1354
1453
 
1355
1454
        new_parent_idpath = self.get_idpath(new_parent_id)
1356
1455
        if file_id in new_parent_idpath:
1357
 
            raise errors.BzrError(
1358
 
                "cannot move directory %r into a subdirectory of itself, %r"
 
1456
            raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
1359
1457
                    % (self.id2path(file_id), self.id2path(new_parent_id)))
1360
1458
 
1361
1459
        file_ie = self._byid[file_id]
1397
1495
    def __init__(self, search_key_name):
1398
1496
        CommonInventory.__init__(self)
1399
1497
        self._fileid_to_entry_cache = {}
1400
 
        self._fully_cached = False
1401
1498
        self._path_to_fileid_cache = {}
1402
1499
        self._search_key_name = search_key_name
1403
1500
        self.root_id = None
1490
1587
            if entry.kind == 'directory':
1491
1588
                directories_to_expand.add(entry.file_id)
1492
1589
            interesting.add(entry.parent_id)
1493
 
            children_of_parent_id.setdefault(entry.parent_id, set()
1494
 
                                             ).add(entry.file_id)
 
1590
            children_of_parent_id.setdefault(entry.parent_id, []
 
1591
                                             ).append(entry.file_id)
1495
1592
 
1496
1593
        # Now, interesting has all of the direct parents, but not the
1497
1594
        # parents of those parents. It also may have some duplicates with
1502
1599
        interesting.add(None) # this will auto-filter it in the loop
1503
1600
        remaining_parents.discard(None) 
1504
1601
        while remaining_parents:
 
1602
            if None in remaining_parents:
 
1603
                import pdb; pdb.set_trace()
1505
1604
            next_parents = set()
1506
1605
            for entry in self._getitems(remaining_parents):
1507
1606
                next_parents.add(entry.parent_id)
1508
 
                children_of_parent_id.setdefault(entry.parent_id, set()
1509
 
                                                 ).add(entry.file_id)
 
1607
                children_of_parent_id.setdefault(entry.parent_id, []
 
1608
                                                 ).append(entry.file_id)
1510
1609
            # Remove any search tips we've already processed
1511
1610
            remaining_parents = next_parents.difference(interesting)
1512
1611
            interesting.update(remaining_parents)
1516
1615
        while directories_to_expand:
1517
1616
            # Expand directories by looking in the
1518
1617
            # parent_id_basename_to_file_id map
1519
 
            keys = [StaticTuple(f,).intern() for f in directories_to_expand]
 
1618
            keys = [(f,) for f in directories_to_expand]
1520
1619
            directories_to_expand = set()
1521
1620
            items = self.parent_id_basename_to_file_id.iteritems(keys)
1522
1621
            next_file_ids = set([item[1] for item in items])
1525
1624
            for entry in self._getitems(next_file_ids):
1526
1625
                if entry.kind == 'directory':
1527
1626
                    directories_to_expand.add(entry.file_id)
1528
 
                children_of_parent_id.setdefault(entry.parent_id, set()
1529
 
                                                 ).add(entry.file_id)
 
1627
                children_of_parent_id.setdefault(entry.parent_id, []
 
1628
                                                 ).append(entry.file_id)
1530
1629
        return interesting, children_of_parent_id
1531
1630
 
1532
1631
    def filter(self, specific_fileids):
1554
1653
            # parent_to_children with at least the tree root.)
1555
1654
            return other
1556
1655
        cache = self._fileid_to_entry_cache
1557
 
        remaining_children = collections.deque(parent_to_children[self.root_id])
 
1656
        try:
 
1657
            remaining_children = collections.deque(parent_to_children[self.root_id])
 
1658
        except:
 
1659
            import pdb; pdb.set_trace()
 
1660
            raise
1558
1661
        while remaining_children:
1559
1662
            file_id = remaining_children.popleft()
1560
1663
            ie = cache[file_id]
1575
1678
        # to filter out empty names because of non rich-root...
1576
1679
        sections = bytes.split('\n')
1577
1680
        kind, file_id = sections[0].split(': ')
1578
 
        return (sections[2], intern(file_id), intern(sections[3]))
 
1681
        return (sections[2], file_id, sections[3])
1579
1682
 
1580
1683
    def _bytes_to_entry(self, bytes):
1581
1684
        """Deserialise a serialised entry."""
1603
1706
            result.reference_revision = sections[4]
1604
1707
        else:
1605
1708
            raise ValueError("Not a serialised entry %r" % bytes)
1606
 
        result.file_id = intern(result.file_id)
1607
 
        result.revision = intern(sections[3])
 
1709
        result.revision = sections[3]
1608
1710
        if result.parent_id == '':
1609
1711
            result.parent_id = None
1610
1712
        self._fileid_to_entry_cache[result.file_id] = result
1611
1713
        return result
1612
1714
 
 
1715
    def _get_mutable_inventory(self):
 
1716
        """See CommonInventory._get_mutable_inventory."""
 
1717
        entries = self.iter_entries()
 
1718
        inv = Inventory(None, self.revision_id)
 
1719
        for path, inv_entry in entries:
 
1720
            inv.add(inv_entry.copy())
 
1721
        return inv
 
1722
 
1613
1723
    def create_by_apply_delta(self, inventory_delta, new_revision_id,
1614
1724
        propagate_caches=False):
1615
1725
        """Create a new CHKInventory by applying inventory_delta to this one.
1700
1810
                        pass
1701
1811
                deletes.add(file_id)
1702
1812
            else:
1703
 
                new_key = StaticTuple(file_id,)
 
1813
                new_key = (file_id,)
1704
1814
                new_value = result._entry_to_bytes(entry)
1705
1815
                # Update caches. It's worth doing this whether
1706
1816
                # we're propagating the old caches or not.
1709
1819
            if old_path is None:
1710
1820
                old_key = None
1711
1821
            else:
1712
 
                old_key = StaticTuple(file_id,)
 
1822
                old_key = (file_id,)
1713
1823
                if self.id2path(file_id) != old_path:
1714
1824
                    raise errors.InconsistentDelta(old_path, file_id,
1715
1825
                        "Entry was at wrong other path %r." %
1716
1826
                        self.id2path(file_id))
1717
1827
                altered.add(file_id)
1718
 
            id_to_entry_delta.append(StaticTuple(old_key, new_key, new_value))
 
1828
            id_to_entry_delta.append((old_key, new_key, new_value))
1719
1829
            if result.parent_id_basename_to_file_id is not None:
1720
1830
                # parent_id, basename changes
1721
1831
                if old_path is None:
1808
1918
                raise errors.BzrError('Duplicate key in inventory: %r\n%r'
1809
1919
                                      % (key, bytes))
1810
1920
            info[key] = value
1811
 
        revision_id = intern(info['revision_id'])
1812
 
        root_id = intern(info['root_id'])
1813
 
        search_key_name = intern(info.get('search_key_name', 'plain'))
1814
 
        parent_id_basename_to_file_id = intern(info.get(
1815
 
            'parent_id_basename_to_file_id', None))
1816
 
        if not parent_id_basename_to_file_id.startswith('sha1:'):
1817
 
            raise ValueError('parent_id_basename_to_file_id should be a sha1'
1818
 
                             ' key not %r' % (parent_id_basename_to_file_id,))
 
1921
        revision_id = info['revision_id']
 
1922
        root_id = info['root_id']
 
1923
        search_key_name = info.get('search_key_name', 'plain')
 
1924
        parent_id_basename_to_file_id = info.get(
 
1925
            'parent_id_basename_to_file_id', None)
1819
1926
        id_to_entry = info['id_to_entry']
1820
 
        if not id_to_entry.startswith('sha1:'):
1821
 
            raise ValueError('id_to_entry should be a sha1'
1822
 
                             ' key not %r' % (id_to_entry,))
1823
1927
 
1824
1928
        result = CHKInventory(search_key_name)
1825
1929
        result.revision_id = revision_id
1828
1932
                            result._search_key_name)
1829
1933
        if parent_id_basename_to_file_id is not None:
1830
1934
            result.parent_id_basename_to_file_id = chk_map.CHKMap(
1831
 
                chk_store, StaticTuple(parent_id_basename_to_file_id,),
 
1935
                chk_store, (parent_id_basename_to_file_id,),
1832
1936
                search_key_func=search_key_func)
1833
1937
        else:
1834
1938
            result.parent_id_basename_to_file_id = None
1835
1939
 
1836
 
        result.id_to_entry = chk_map.CHKMap(chk_store,
1837
 
                                            StaticTuple(id_to_entry,),
 
1940
        result.id_to_entry = chk_map.CHKMap(chk_store, (id_to_entry,),
1838
1941
                                            search_key_func=search_key_func)
1839
1942
        if (result.revision_id,) != expected_revision_id:
1840
1943
            raise ValueError("Mismatched revision id and expected: %r, %r" %
1862
1965
        id_to_entry_dict = {}
1863
1966
        parent_id_basename_dict = {}
1864
1967
        for path, entry in inventory.iter_entries():
1865
 
            key = StaticTuple(entry.file_id,).intern()
1866
 
            id_to_entry_dict[key] = entry_to_bytes(entry)
 
1968
            id_to_entry_dict[(entry.file_id,)] = entry_to_bytes(entry)
1867
1969
            p_id_key = parent_id_basename_key(entry)
1868
1970
            parent_id_basename_dict[p_id_key] = entry.file_id
1869
1971
 
1892
1994
            parent_id = entry.parent_id
1893
1995
        else:
1894
1996
            parent_id = ''
1895
 
        return StaticTuple(parent_id, entry.name.encode('utf8')).intern()
 
1997
        return parent_id, entry.name.encode('utf8')
1896
1998
 
1897
1999
    def __getitem__(self, file_id):
1898
2000
        """map a single file_id -> InventoryEntry."""
1903
2005
            return result
1904
2006
        try:
1905
2007
            return self._bytes_to_entry(
1906
 
                self.id_to_entry.iteritems([StaticTuple(file_id,)]).next()[1])
 
2008
                self.id_to_entry.iteritems([(file_id,)]).next()[1])
1907
2009
        except StopIteration:
1908
2010
            # really we're passing an inventory, not a tree...
1909
2011
            raise errors.NoSuchId(self, file_id)
1922
2024
                remaining.append(file_id)
1923
2025
            else:
1924
2026
                result.append(entry)
1925
 
        file_keys = [StaticTuple(f,).intern() for f in remaining]
 
2027
        file_keys = [(f,) for f in remaining]
1926
2028
        for file_key, value in self.id_to_entry.iteritems(file_keys):
1927
2029
            entry = self._bytes_to_entry(value)
1928
2030
            result.append(entry)
1933
2035
        # Perhaps have an explicit 'contains' method on CHKMap ?
1934
2036
        if self._fileid_to_entry_cache.get(file_id, None) is not None:
1935
2037
            return True
1936
 
        return len(list(
1937
 
            self.id_to_entry.iteritems([StaticTuple(file_id,)]))) == 1
 
2038
        return len(list(self.id_to_entry.iteritems([(file_id,)]))) == 1
1938
2039
 
1939
2040
    def is_root(self, file_id):
1940
2041
        return file_id == self.root_id
1956
2057
 
1957
2058
    def iter_just_entries(self):
1958
2059
        """Iterate over all entries.
1959
 
 
 
2060
        
1960
2061
        Unlike iter_entries(), just the entries are returned (not (path, ie))
1961
2062
        and the order of entries is undefined.
1962
2063
 
1970
2071
                self._fileid_to_entry_cache[file_id] = ie
1971
2072
            yield ie
1972
2073
 
1973
 
    def _preload_cache(self):
1974
 
        """Make sure all file-ids are in _fileid_to_entry_cache"""
1975
 
        if self._fully_cached:
1976
 
            return # No need to do it again
1977
 
        # The optimal sort order is to use iteritems() directly
1978
 
        cache = self._fileid_to_entry_cache
1979
 
        for key, entry in self.id_to_entry.iteritems():
1980
 
            file_id = key[0]
1981
 
            if file_id not in cache:
1982
 
                ie = self._bytes_to_entry(entry)
1983
 
                cache[file_id] = ie
1984
 
            else:
1985
 
                ie = cache[file_id]
1986
 
        last_parent_id = last_parent_ie = None
1987
 
        pid_items = self.parent_id_basename_to_file_id.iteritems()
1988
 
        for key, child_file_id in pid_items:
1989
 
            if key == ('', ''): # This is the root
1990
 
                if child_file_id != self.root_id:
1991
 
                    raise ValueError('Data inconsistency detected.'
1992
 
                        ' We expected data with key ("","") to match'
1993
 
                        ' the root id, but %s != %s'
1994
 
                        % (child_file_id, self.root_id))
1995
 
                continue
1996
 
            parent_id, basename = key
1997
 
            ie = cache[child_file_id]
1998
 
            if parent_id == last_parent_id:
1999
 
                parent_ie = last_parent_ie
2000
 
            else:
2001
 
                parent_ie = cache[parent_id]
2002
 
            if parent_ie.kind != 'directory':
2003
 
                raise ValueError('Data inconsistency detected.'
2004
 
                    ' An entry in the parent_id_basename_to_file_id map'
2005
 
                    ' has parent_id {%s} but the kind of that object'
2006
 
                    ' is %r not "directory"' % (parent_id, parent_ie.kind))
2007
 
            if parent_ie._children is None:
2008
 
                parent_ie._children = {}
2009
 
            basename = basename.decode('utf-8')
2010
 
            if basename in parent_ie._children:
2011
 
                existing_ie = parent_ie._children[basename]
2012
 
                if existing_ie != ie:
2013
 
                    raise ValueError('Data inconsistency detected.'
2014
 
                        ' Two entries with basename %r were found'
2015
 
                        ' in the parent entry {%s}'
2016
 
                        % (basename, parent_id))
2017
 
            if basename != ie.name:
2018
 
                raise ValueError('Data inconsistency detected.'
2019
 
                    ' In the parent_id_basename_to_file_id map, file_id'
2020
 
                    ' {%s} is listed as having basename %r, but in the'
2021
 
                    ' id_to_entry map it is %r'
2022
 
                    % (child_file_id, basename, ie.name))
2023
 
            parent_ie._children[basename] = ie
2024
 
        self._fully_cached = True
2025
 
 
2026
2074
    def iter_changes(self, basis):
2027
2075
        """Generate a Tree.iter_changes change list between this and basis.
2028
2076
 
2122
2170
            delta.append((old_path, new_path, file_id, entry))
2123
2171
        return delta
2124
2172
 
2125
 
    def path2id(self, relpath):
 
2173
    def path2id(self, name):
2126
2174
        """See CommonInventory.path2id()."""
2127
2175
        # TODO: perhaps support negative hits?
2128
 
        result = self._path_to_fileid_cache.get(relpath, None)
 
2176
        result = self._path_to_fileid_cache.get(name, None)
2129
2177
        if result is not None:
2130
2178
            return result
2131
 
        if isinstance(relpath, basestring):
2132
 
            names = osutils.splitpath(relpath)
 
2179
        if isinstance(name, basestring):
 
2180
            names = osutils.splitpath(name)
2133
2181
        else:
2134
 
            names = relpath
 
2182
            names = name
2135
2183
        current_id = self.root_id
2136
2184
        if current_id is None:
2137
2185
            return None
2138
2186
        parent_id_index = self.parent_id_basename_to_file_id
2139
 
        cur_path = None
2140
2187
        for basename in names:
2141
 
            if cur_path is None:
2142
 
                cur_path = basename
2143
 
            else:
2144
 
                cur_path = cur_path + '/' + basename
 
2188
            # TODO: Cache each path we figure out in this function.
2145
2189
            basename_utf8 = basename.encode('utf8')
2146
 
            file_id = self._path_to_fileid_cache.get(cur_path, None)
 
2190
            key_filter = [(current_id, basename_utf8)]
 
2191
            file_id = None
 
2192
            for (parent_id, name_utf8), file_id in parent_id_index.iteritems(
 
2193
                key_filter=key_filter):
 
2194
                if parent_id != current_id or name_utf8 != basename_utf8:
 
2195
                    raise errors.BzrError("corrupt inventory lookup! "
 
2196
                        "%r %r %r %r" % (parent_id, current_id, name_utf8,
 
2197
                        basename_utf8))
2147
2198
            if file_id is None:
2148
 
                key_filter = [StaticTuple(current_id, basename_utf8)]
2149
 
                items = parent_id_index.iteritems(key_filter)
2150
 
                for (parent_id, name_utf8), file_id in items:
2151
 
                    if parent_id != current_id or name_utf8 != basename_utf8:
2152
 
                        raise errors.BzrError("corrupt inventory lookup! "
2153
 
                            "%r %r %r %r" % (parent_id, current_id, name_utf8,
2154
 
                            basename_utf8))
2155
 
                if file_id is None:
2156
 
                    return None
2157
 
                else:
2158
 
                    self._path_to_fileid_cache[cur_path] = file_id
 
2199
                return None
2159
2200
            current_id = file_id
 
2201
        self._path_to_fileid_cache[name] = current_id
2160
2202
        return current_id
2161
2203
 
2162
2204
    def to_lines(self):
2167
2209
            lines.append('search_key_name: %s\n' % (self._search_key_name,))
2168
2210
            lines.append("root_id: %s\n" % self.root_id)
2169
2211
            lines.append('parent_id_basename_to_file_id: %s\n' %
2170
 
                (self.parent_id_basename_to_file_id.key()[0],))
 
2212
                self.parent_id_basename_to_file_id.key())
2171
2213
            lines.append("revision_id: %s\n" % self.revision_id)
2172
 
            lines.append("id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
 
2214
            lines.append("id_to_entry: %s\n" % self.id_to_entry.key())
2173
2215
        else:
2174
2216
            lines.append("revision_id: %s\n" % self.revision_id)
2175
2217
            lines.append("root_id: %s\n" % self.root_id)
2176
2218
            if self.parent_id_basename_to_file_id is not None:
2177
2219
                lines.append('parent_id_basename_to_file_id: %s\n' %
2178
 
                    (self.parent_id_basename_to_file_id.key()[0],))
2179
 
            lines.append("id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
 
2220
                    self.parent_id_basename_to_file_id.key())
 
2221
            lines.append("id_to_entry: %s\n" % self.id_to_entry.key())
2180
2222
        return lines
2181
2223
 
2182
2224
    @property
2188
2230
class CHKInventoryDirectory(InventoryDirectory):
2189
2231
    """A directory in an inventory."""
2190
2232
 
2191
 
    __slots__ = ['_children', '_chk_inventory']
 
2233
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
2234
                 'text_id', 'parent_id', '_children', 'executable',
 
2235
                 'revision', 'symlink_target', 'reference_revision',
 
2236
                 '_chk_inventory']
2192
2237
 
2193
2238
    def __init__(self, file_id, name, parent_id, chk_inventory):
2194
2239
        # Don't call InventoryDirectory.__init__ - it isn't right for this
2195
2240
        # class.
2196
2241
        InventoryEntry.__init__(self, file_id, name, parent_id)
2197
2242
        self._children = None
 
2243
        self.kind = 'directory'
2198
2244
        self._chk_inventory = chk_inventory
2199
2245
 
2200
2246
    @property
2219
2265
        parent_id_index = self._chk_inventory.parent_id_basename_to_file_id
2220
2266
        child_keys = set()
2221
2267
        for (parent_id, name_utf8), file_id in parent_id_index.iteritems(
2222
 
            key_filter=[StaticTuple(self.file_id,)]):
2223
 
            child_keys.add(StaticTuple(file_id,))
 
2268
            key_filter=[(self.file_id,)]):
 
2269
            child_keys.add((file_id,))
2224
2270
        cached = set()
2225
2271
        for file_id_key in child_keys:
2226
2272
            entry = self._chk_inventory._fileid_to_entry_cache.get(
2285
2331
    return name
2286
2332
 
2287
2333
 
2288
 
_NAME_RE = lazy_regex.lazy_compile(r'^[^/\\]+$')
 
2334
_NAME_RE = None
2289
2335
 
2290
2336
def is_valid_name(name):
 
2337
    global _NAME_RE
 
2338
    if _NAME_RE is None:
 
2339
        _NAME_RE = re.compile(r'^[^/\\]+$')
 
2340
 
2291
2341
    return bool(_NAME_RE.match(name))
2292
2342
 
2293
2343
 
2383
2433
            raise errors.InconsistentDelta(new_path, item[1],
2384
2434
                "new_path with no entry")
2385
2435
        yield item
2386
 
 
2387
 
 
2388
 
def mutable_inventory_from_tree(tree):
2389
 
    """Create a new inventory that has the same contents as a specified tree.
2390
 
 
2391
 
    :param tree: Revision tree to create inventory from
2392
 
    """
2393
 
    entries = tree.iter_entries_by_dir()
2394
 
    inv = Inventory(None, tree.get_revision_id())
2395
 
    for path, inv_entry in entries:
2396
 
        inv.add(inv_entry.copy())
2397
 
    return inv