~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: 2009-06-22 17:11:20 UTC
  • mfrom: (4398.8.10 1.16-commit-fulltext)
  • Revision ID: pqm@pqm.ubuntu.com-20090622171120-fuxez9ylfqpxynqn
(jam) Add VF._add_text and reduce memory overhead during commit (see
        bug #109114)

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
27
27
# created, but it's not for now.
28
28
ROOT_ID = "TREE_ROOT"
29
29
 
 
30
from copy import deepcopy
 
31
 
30
32
from bzrlib.lazy_import import lazy_import
31
33
lazy_import(globals(), """
32
34
import collections
33
 
import copy
 
35
import os
34
36
import re
35
37
import tarfile
36
38
 
 
39
import bzrlib
37
40
from bzrlib import (
38
41
    chk_map,
39
42
    errors,
40
43
    generate_ids,
41
44
    osutils,
 
45
    symbol_versioning,
 
46
    workingtree,
42
47
    )
43
48
""")
44
49
 
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
 
    )
 
50
from bzrlib.errors import (
 
51
    BzrCheckError,
 
52
    BzrError,
 
53
    )
 
54
from bzrlib.symbol_versioning import deprecated_in, deprecated_method
 
55
from bzrlib.trace import mutter
55
56
 
56
57
 
57
58
class InventoryEntry(object):
104
105
    InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
105
106
    >>> i.path2id('src/wibble')
106
107
    '2325'
 
108
    >>> '2325' in i
 
109
    True
107
110
    >>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
108
111
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None, revision=None)
109
112
    >>> i['2326']
129
132
    RENAMED = 'renamed'
130
133
    MODIFIED_AND_RENAMED = 'modified and renamed'
131
134
 
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
 
 
 
135
    __slots__ = []
149
136
 
150
137
    def detect_changes(self, old_entry):
151
138
        """Return a (text_modified, meta_modified) from this to old_entry.
172
159
        candidates = {}
173
160
        # identify candidate head revision ids.
174
161
        for inv in previous_inventories:
175
 
            if inv.has_id(self.file_id):
 
162
            if self.file_id in inv:
176
163
                ie = inv[self.file_id]
177
164
                if ie.revision in candidates:
178
165
                    # same revision value in two different inventories:
190
177
                    candidates[ie.revision] = ie
191
178
        return candidates
192
179
 
 
180
    @deprecated_method(deprecated_in((1, 6, 0)))
 
181
    def get_tar_item(self, root, dp, now, tree):
 
182
        """Get a tarfile item and a file stream for its content."""
 
183
        item = tarfile.TarInfo(osutils.pathjoin(root, dp).encode('utf8'))
 
184
        # TODO: would be cool to actually set it to the timestamp of the
 
185
        # revision it was last changed
 
186
        item.mtime = now
 
187
        fileobj = self._put_in_tar(item, tree)
 
188
        return item, fileobj
 
189
 
193
190
    def has_text(self):
194
191
        """Return true if the object this entry represents has textual data.
195
192
 
201
198
        """
202
199
        return False
203
200
 
204
 
    def __init__(self, file_id, name, parent_id):
 
201
    def __init__(self, file_id, name, parent_id, text_id=None):
205
202
        """Create an InventoryEntry
206
203
 
207
204
        The filename must be a single component, relative to the
218
215
        """
219
216
        if '/' in name or '\\' in name:
220
217
            raise errors.InvalidEntryName(name=name)
 
218
        self.executable = False
 
219
        self.revision = None
 
220
        self.text_sha1 = None
 
221
        self.text_size = None
221
222
        self.file_id = file_id
222
 
        self.revision = None
223
223
        self.name = name
 
224
        self.text_id = text_id
224
225
        self.parent_id = parent_id
 
226
        self.symlink_target = None
 
227
        self.reference_revision = None
225
228
 
226
229
    def kind_character(self):
227
230
        """Return a short kind indicator useful for appending to names."""
228
 
        raise errors.BzrError('unknown kind %r' % self.kind)
 
231
        raise BzrError('unknown kind %r' % self.kind)
229
232
 
230
233
    known_kinds = ('file', 'directory', 'symlink')
231
234
 
 
235
    def _put_in_tar(self, item, tree):
 
236
        """populate item for stashing in a tar, and return the content stream.
 
237
 
 
238
        If no content is available, return None.
 
239
        """
 
240
        raise BzrError("don't know how to export {%s} of kind %r" %
 
241
                       (self.file_id, self.kind))
 
242
 
 
243
    @deprecated_method(deprecated_in((1, 6, 0)))
 
244
    def put_on_disk(self, dest, dp, tree):
 
245
        """Create a representation of self on disk in the prefix dest.
 
246
 
 
247
        This is a template method - implement _put_on_disk in subclasses.
 
248
        """
 
249
        fullpath = osutils.pathjoin(dest, dp)
 
250
        self._put_on_disk(fullpath, tree)
 
251
        # mutter("  export {%s} kind %s to %s", self.file_id,
 
252
        #         self.kind, fullpath)
 
253
 
 
254
    def _put_on_disk(self, fullpath, tree):
 
255
        """Put this entry onto disk at fullpath, from tree tree."""
 
256
        raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
 
257
 
232
258
    def sorted_children(self):
233
259
        return sorted(self.children.items())
234
260
 
236
262
    def versionable_kind(kind):
237
263
        return (kind in ('file', 'directory', 'symlink', 'tree-reference'))
238
264
 
239
 
    def check(self, checker, rev_id, inv):
 
265
    def check(self, checker, rev_id, inv, tree):
240
266
        """Check this inventory entry is intact.
241
267
 
242
268
        This is a template method, override _check for kind specific
248
274
        :param rev_id: Revision id from which this InventoryEntry was loaded.
249
275
             Not necessarily the last-changed revision for this file.
250
276
        :param inv: Inventory from which the entry was loaded.
 
277
        :param tree: RevisionTree for this entry.
251
278
        """
252
279
        if self.parent_id is not None:
253
280
            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))
257
 
        checker._add_entry_to_text_key_references(inv, self)
258
 
        self._check(checker, rev_id)
 
281
                raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
 
282
                        % (self.parent_id, rev_id))
 
283
        self._check(checker, rev_id, tree)
259
284
 
260
 
    def _check(self, checker, rev_id):
 
285
    def _check(self, checker, rev_id, tree):
261
286
        """Check this inventory entry for kind specific errors."""
262
 
        checker._report_items.append(
263
 
            'unknown entry kind %r in revision {%s}' % (self.kind, rev_id))
 
287
        raise BzrCheckError('unknown entry kind %r in revision {%s}' %
 
288
                            (self.kind, rev_id))
264
289
 
265
290
    def copy(self):
266
291
        """Clone this inventory entry."""
373
398
        pass
374
399
 
375
400
 
 
401
class RootEntry(InventoryEntry):
 
402
 
 
403
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
404
                 'text_id', 'parent_id', 'children', 'executable',
 
405
                 'revision', 'symlink_target', 'reference_revision']
 
406
 
 
407
    def _check(self, checker, rev_id, tree):
 
408
        """See InventoryEntry._check"""
 
409
 
 
410
    def __init__(self, file_id):
 
411
        self.file_id = file_id
 
412
        self.children = {}
 
413
        self.kind = 'directory'
 
414
        self.parent_id = None
 
415
        self.name = u''
 
416
        self.revision = None
 
417
        symbol_versioning.warn('RootEntry is deprecated as of bzr 0.10.'
 
418
                               '  Please use InventoryDirectory instead.',
 
419
                               DeprecationWarning, stacklevel=2)
 
420
 
 
421
    def __eq__(self, other):
 
422
        if not isinstance(other, RootEntry):
 
423
            return NotImplemented
 
424
 
 
425
        return (self.file_id == other.file_id) \
 
426
               and (self.children == other.children)
 
427
 
 
428
 
376
429
class InventoryDirectory(InventoryEntry):
377
430
    """A directory in an inventory."""
378
431
 
379
 
    __slots__ = ['children']
380
 
 
381
 
    kind = 'directory'
382
 
 
383
 
    def _check(self, checker, rev_id):
 
432
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
433
                 'text_id', 'parent_id', 'children', 'executable',
 
434
                 'revision', 'symlink_target', 'reference_revision']
 
435
 
 
436
    def _check(self, checker, rev_id, tree):
384
437
        """See InventoryEntry._check"""
385
 
        # In non rich root repositories we do not expect a file graph for the
386
 
        # root.
387
 
        if self.name == '' and not checker.rich_roots:
388
 
            return
389
 
        # Directories are stored as an empty file, but the file should exist
390
 
        # to provide a per-fileid log. The hash of every directory content is
391
 
        # "da..." below (the sha1sum of '').
392
 
        checker.add_pending_item(rev_id,
393
 
            ('texts', self.file_id, self.revision), 'text',
394
 
             'da39a3ee5e6b4b0d3255bfef95601890afd80709')
 
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}'
 
440
                                % (self.file_id, rev_id))
395
441
 
396
442
    def copy(self):
397
443
        other = InventoryDirectory(self.file_id, self.name, self.parent_id)
403
449
    def __init__(self, file_id, name, parent_id):
404
450
        super(InventoryDirectory, self).__init__(file_id, name, parent_id)
405
451
        self.children = {}
 
452
        self.kind = 'directory'
406
453
 
407
454
    def kind_character(self):
408
455
        """See InventoryEntry.kind_character."""
409
456
        return '/'
410
457
 
 
458
    def _put_in_tar(self, item, tree):
 
459
        """See InventoryEntry._put_in_tar."""
 
460
        item.type = tarfile.DIRTYPE
 
461
        fileobj = None
 
462
        item.name += '/'
 
463
        item.size = 0
 
464
        item.mode = 0755
 
465
        return fileobj
 
466
 
 
467
    def _put_on_disk(self, fullpath, tree):
 
468
        """See InventoryEntry._put_on_disk."""
 
469
        os.mkdir(fullpath)
 
470
 
411
471
 
412
472
class InventoryFile(InventoryEntry):
413
473
    """A file in an inventory."""
414
474
 
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
425
 
 
426
 
    def _check(self, checker, tree_revision_id):
 
475
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
476
                 'text_id', 'parent_id', 'children', 'executable',
 
477
                 'revision', 'symlink_target', 'reference_revision']
 
478
 
 
479
    def _check(self, checker, tree_revision_id, tree):
427
480
        """See InventoryEntry._check"""
428
 
        # TODO: check size too.
429
 
        checker.add_pending_item(tree_revision_id,
430
 
            ('texts', self.file_id, self.revision), 'text',
431
 
             self.text_sha1)
432
 
        if self.text_size is None:
433
 
            checker._report_items.append(
434
 
                'fileid {%s} in {%s} has None for text_size' % (self.file_id,
435
 
                tree_revision_id))
 
481
        key = (self.file_id, self.revision)
 
482
        if key in checker.checked_texts:
 
483
            prev_sha = checker.checked_texts[key]
 
484
            if prev_sha != self.text_sha1:
 
485
                raise BzrCheckError(
 
486
                    'mismatched sha1 on {%s} in {%s} (%s != %s) %r' %
 
487
                    (self.file_id, tree_revision_id, prev_sha, self.text_sha1,
 
488
                     t))
 
489
            else:
 
490
                checker.repeated_text_cnt += 1
 
491
                return
 
492
 
 
493
        checker.checked_text_cnt += 1
 
494
        # We can't check the length, because Weave doesn't store that
 
495
        # information, and the whole point of looking at the weave's
 
496
        # sha1sum is that we don't have to extract the text.
 
497
        if (self.text_sha1 != tree._repository.texts.get_sha1s([key])[key]):
 
498
            raise BzrCheckError('text {%s} version {%s} wrong sha1' % key)
 
499
        checker.checked_texts[key] = self.text_sha1
436
500
 
437
501
    def copy(self):
438
502
        other = InventoryFile(self.file_id, self.name, self.parent_id)
470
534
        """See InventoryEntry.has_text."""
471
535
        return True
472
536
 
 
537
    def __init__(self, file_id, name, parent_id):
 
538
        super(InventoryFile, self).__init__(file_id, name, parent_id)
 
539
        self.kind = 'file'
 
540
 
473
541
    def kind_character(self):
474
542
        """See InventoryEntry.kind_character."""
475
543
        return ''
476
544
 
 
545
    def _put_in_tar(self, item, tree):
 
546
        """See InventoryEntry._put_in_tar."""
 
547
        item.type = tarfile.REGTYPE
 
548
        fileobj = tree.get_file(self.file_id)
 
549
        item.size = self.text_size
 
550
        if tree.is_executable(self.file_id):
 
551
            item.mode = 0755
 
552
        else:
 
553
            item.mode = 0644
 
554
        return fileobj
 
555
 
 
556
    def _put_on_disk(self, fullpath, tree):
 
557
        """See InventoryEntry._put_on_disk."""
 
558
        osutils.pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
 
559
        if tree.is_executable(self.file_id):
 
560
            os.chmod(fullpath, 0755)
 
561
 
477
562
    def _read_tree_state(self, path, work_tree):
478
563
        """See InventoryEntry._read_tree_state."""
479
564
        self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
511
596
class InventoryLink(InventoryEntry):
512
597
    """A file in an inventory."""
513
598
 
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
521
 
 
522
 
    def _check(self, checker, tree_revision_id):
 
599
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
600
                 'text_id', 'parent_id', 'children', 'executable',
 
601
                 'revision', 'symlink_target', 'reference_revision']
 
602
 
 
603
    def _check(self, checker, rev_id, tree):
523
604
        """See InventoryEntry._check"""
 
605
        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))
524
608
        if self.symlink_target is None:
525
 
            checker._report_items.append(
526
 
                'symlink {%s} has no target in revision {%s}'
527
 
                    % (self.file_id, tree_revision_id))
528
 
        # Symlinks are stored as ''
529
 
        checker.add_pending_item(tree_revision_id,
530
 
            ('texts', self.file_id, self.revision), 'text',
531
 
             'da39a3ee5e6b4b0d3255bfef95601890afd80709')
 
609
            raise BzrCheckError('symlink {%s} has no target in revision {%s}'
 
610
                    % (self.file_id, rev_id))
532
611
 
533
612
    def copy(self):
534
613
        other = InventoryLink(self.file_id, self.name, self.parent_id)
541
620
        # FIXME: which _modified field should we use ? RBC 20051003
542
621
        text_modified = (self.symlink_target != old_entry.symlink_target)
543
622
        if text_modified:
544
 
            trace.mutter("    symlink target changed")
 
623
            mutter("    symlink target changed")
545
624
        meta_modified = False
546
625
        return text_modified, meta_modified
547
626
 
564
643
        differ = DiffSymlink(old_tree, new_tree, output_to)
565
644
        return differ.diff_symlink(old_target, new_target)
566
645
 
 
646
    def __init__(self, file_id, name, parent_id):
 
647
        super(InventoryLink, self).__init__(file_id, name, parent_id)
 
648
        self.kind = 'symlink'
 
649
 
567
650
    def kind_character(self):
568
651
        """See InventoryEntry.kind_character."""
569
652
        return ''
570
653
 
 
654
    def _put_in_tar(self, item, tree):
 
655
        """See InventoryEntry._put_in_tar."""
 
656
        item.type = tarfile.SYMTYPE
 
657
        fileobj = None
 
658
        item.size = 0
 
659
        item.mode = 0755
 
660
        item.linkname = self.symlink_target
 
661
        return fileobj
 
662
 
 
663
    def _put_on_disk(self, fullpath, tree):
 
664
        """See InventoryEntry._put_on_disk."""
 
665
        try:
 
666
            os.symlink(self.symlink_target, fullpath)
 
667
        except OSError,e:
 
668
            raise BzrError("Failed to create symlink %r -> %r, error: %s" % (fullpath, self.symlink_target, e))
 
669
 
571
670
    def _read_tree_state(self, path, work_tree):
572
671
        """See InventoryEntry._read_tree_state."""
573
672
        self.symlink_target = work_tree.get_symlink_target(self.file_id)
585
684
 
586
685
class TreeReference(InventoryEntry):
587
686
 
588
 
    __slots__ = ['reference_revision']
589
 
 
590
687
    kind = 'tree-reference'
591
688
 
592
689
    def __init__(self, file_id, name, parent_id, revision=None,
617
714
 
618
715
 
619
716
class CommonInventory(object):
620
 
    """Basic inventory logic, defined in terms of primitives like has_id.
621
 
 
622
 
    An inventory is the metadata about the contents of a tree.
623
 
 
624
 
    This is broadly a map from file_id to entries such as directories, files,
625
 
    symlinks and tree references. Each entry maintains its own metadata like
626
 
    SHA1 and length for files, or children for a directory.
627
 
 
628
 
    Entries can be looked up either by path or by file_id.
629
 
 
630
 
    InventoryEntry objects must not be modified after they are
631
 
    inserted, other than through the Inventory API.
632
 
    """
633
 
 
634
 
    @deprecated_method(deprecated_in((2, 4, 0)))
 
717
    """Basic inventory logic, defined in terms of primitives like has_id."""
 
718
 
635
719
    def __contains__(self, file_id):
636
720
        """True if this entry contains a file with given id.
637
721
 
638
722
        >>> inv = Inventory()
639
723
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
640
724
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
641
 
        >>> inv.has_id('123')
 
725
        >>> '123' in inv
642
726
        True
643
 
        >>> inv.has_id('456')
 
727
        >>> '456' in inv
644
728
        False
645
729
 
646
730
        Note that this method along with __iter__ are not encouraged for use as
649
733
        """
650
734
        return self.has_id(file_id)
651
735
 
652
 
    def has_filename(self, filename):
653
 
        return bool(self.path2id(filename))
654
 
 
655
736
    def id2path(self, file_id):
656
737
        """Return as a string the path to file_id.
657
738
 
660
741
        >>> e = i.add(InventoryFile('foo-id', 'foo.c', parent_id='src-id'))
661
742
        >>> print i.id2path('foo-id')
662
743
        src/foo.c
663
 
 
664
 
        :raises NoSuchId: If file_id is not present in the inventory.
665
744
        """
666
745
        # get all names, skipping root
667
746
        return '/'.join(reversed(
721
800
                # if we finished all children, pop it off the stack
722
801
                stack.pop()
723
802
 
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
803
    def iter_entries_by_dir(self, from_dir=None, specific_file_ids=None,
733
804
        yield_parents=False):
734
805
        """Iterate over the entries in a directory first order.
747
818
            specific_file_ids = set(specific_file_ids)
748
819
        # TODO? Perhaps this should return the from_dir so that the root is
749
820
        # 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
821
        if from_dir is None:
756
822
            if self.root is None:
757
823
                return
759
825
            if (not yield_parents and specific_file_ids is not None and
760
826
                len(specific_file_ids) == 1):
761
827
                file_id = list(specific_file_ids)[0]
762
 
                if self.has_id(file_id):
 
828
                if file_id in self:
763
829
                    yield self.id2path(file_id), self[file_id]
764
830
                return
765
831
            from_dir = self.root
775
841
            parents = set()
776
842
            byid = self
777
843
            def add_ancestors(file_id):
778
 
                if not byid.has_id(file_id):
 
844
                if file_id not in byid:
779
845
                    return
780
846
                parent_id = byid[file_id].parent_id
781
847
                if parent_id is None:
825
891
                    file_id, self[file_id]))
826
892
        return delta
827
893
 
 
894
    def _get_mutable_inventory(self):
 
895
        """Returns a mutable copy of the object.
 
896
 
 
897
        Some inventories are immutable, yet working trees, for example, needs
 
898
        to mutate exisiting inventories instead of creating a new one.
 
899
        """
 
900
        raise NotImplementedError(self._get_mutable_inventory)
 
901
 
828
902
    def make_entry(self, kind, name, parent_id, file_id=None):
829
903
        """Simple thunk to bzrlib.inventory.make_entry."""
830
904
        return make_entry(kind, name, parent_id, file_id)
844
918
                if ie.kind == 'directory':
845
919
                    descend(ie, child_path)
846
920
 
847
 
        if self.root is not None:
848
 
            descend(self.root, u'')
 
921
        descend(self.root, u'')
849
922
        return accum
850
923
 
851
924
    def directories(self):
864
937
        descend(self.root, u'')
865
938
        return accum
866
939
 
867
 
    def path2id(self, relpath):
 
940
    def path2id(self, name):
868
941
        """Walk down through directories to return entry of last component.
869
942
 
870
 
        :param relpath: may be either a list of path components, or a single
871
 
            string, in which case it is automatically split.
 
943
        names may be either a list of path components, or a single
 
944
        string, in which case it is automatically split.
872
945
 
873
946
        This returns the entry of the last component in the path,
874
947
        which may be either a file or a directory.
875
948
 
876
949
        Returns None IFF the path is not found.
877
950
        """
878
 
        if isinstance(relpath, basestring):
879
 
            names = osutils.splitpath(relpath)
880
 
        else:
881
 
            names = relpath
 
951
        if isinstance(name, basestring):
 
952
            name = osutils.splitpath(name)
 
953
 
 
954
        # mutter("lookup path %r" % name)
882
955
 
883
956
        try:
884
957
            parent = self.root
887
960
            return None
888
961
        if parent is None:
889
962
            return None
890
 
        for f in names:
 
963
        for f in name:
891
964
            try:
892
965
                children = getattr(parent, 'children', None)
893
966
                if children is None:
948
1021
 
949
1022
 
950
1023
class Inventory(CommonInventory):
951
 
    """Mutable dict based in-memory inventory.
952
 
 
953
 
    We never store the full path to a file, because renaming a directory
954
 
    implicitly moves all of its contents.  This class internally maintains a
 
1024
    """Inventory of versioned files in a tree.
 
1025
 
 
1026
    This describes which file_id is present at each point in the tree,
 
1027
    and possibly the SHA-1 or other information about the file.
 
1028
    Entries can be looked up either by path or by file_id.
 
1029
 
 
1030
    The inventory represents a typical unix file tree, with
 
1031
    directories containing files and subdirectories.  We never store
 
1032
    the full path to a file, because renaming a directory implicitly
 
1033
    moves all of its contents.  This class internally maintains a
955
1034
    lookup tree that allows the children under a directory to be
956
1035
    returned quickly.
957
1036
 
 
1037
    InventoryEntry objects must not be modified after they are
 
1038
    inserted, other than through the Inventory API.
 
1039
 
958
1040
    >>> inv = Inventory()
959
1041
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
960
1042
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
961
1043
    >>> inv['123-123'].name
962
1044
    'hello.c'
963
1045
 
964
 
    Id's may be looked up from paths:
965
 
 
966
 
    >>> inv.path2id('hello.c')
967
 
    '123-123'
968
 
    >>> inv.has_id('123-123')
969
 
    True
970
 
 
971
 
    There are iterators over the contents:
972
 
 
973
 
    >>> [entry[0] for entry in inv.iter_entries()]
 
1046
    May be treated as an iterator or set to look up file ids:
 
1047
 
 
1048
    >>> bool(inv.path2id('hello.c'))
 
1049
    True
 
1050
    >>> '123-123' in inv
 
1051
    True
 
1052
 
 
1053
    May also look up by name:
 
1054
 
 
1055
    >>> [x[0] for x in inv.iter_entries()]
974
1056
    ['', u'hello.c']
 
1057
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
 
1058
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
 
1059
    Traceback (most recent call last):
 
1060
    BzrError: parent_id {TREE_ROOT} not in inventory
 
1061
    >>> inv.add(InventoryFile('123-123', 'hello.c', 'TREE_ROOT-12345678-12345678'))
 
1062
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None, revision=None)
975
1063
    """
976
 
 
977
1064
    def __init__(self, root_id=ROOT_ID, revision_id=None):
978
1065
        """Create or read an inventory.
979
1066
 
1003
1090
    def apply_delta(self, delta):
1004
1091
        """Apply a delta to this inventory.
1005
1092
 
1006
 
        See the inventory developers documentation for the theory behind
1007
 
        inventory deltas.
1008
 
 
1009
 
        If delta application fails the inventory is left in an indeterminate
1010
 
        state and must not be used.
1011
 
 
1012
1093
        :param delta: A list of changes to apply. After all the changes are
1013
1094
            applied the final inventory must be internally consistent, but it
1014
1095
            is ok to supply changes which, if only half-applied would have an
1045
1126
        """
1046
1127
        # Check that the delta is legal. It would be nice if this could be
1047
1128
        # done within the loops below but it's safer to validate the delta
1048
 
        # before starting to mutate the inventory, as there isn't a rollback
1049
 
        # facility.
1050
 
        list(_check_delta_unique_ids(_check_delta_unique_new_paths(
1051
 
            _check_delta_unique_old_paths(_check_delta_ids_match_entry(
1052
 
            _check_delta_ids_are_valid(
1053
 
            _check_delta_new_path_entry_both_or_None(
1054
 
            delta)))))))
 
1129
        # before starting to mutate the inventory.
 
1130
        unique_file_ids = set([f for _, _, f, _ in delta])
 
1131
        if len(unique_file_ids) != len(delta):
 
1132
            raise AssertionError("a file-id appears multiple times in %r"
 
1133
                    % (delta,))
 
1134
        del unique_file_ids
1055
1135
 
1056
1136
        children = {}
1057
1137
        # Remove all affected items which were in the original inventory,
1060
1140
        # modified children remaining by the time we examine it.
1061
1141
        for old_path, file_id in sorted(((op, f) for op, np, f, e in delta
1062
1142
                                        if op is not None), reverse=True):
 
1143
            if file_id not in self:
 
1144
                # adds come later
 
1145
                continue
1063
1146
            # Preserve unaltered children of file_id for later reinsertion.
1064
1147
            file_id_children = getattr(self[file_id], 'children', {})
1065
1148
            if len(file_id_children):
1066
1149
                children[file_id] = file_id_children
1067
 
            if self.id2path(file_id) != old_path:
1068
 
                raise errors.InconsistentDelta(old_path, file_id,
1069
 
                    "Entry was at wrong other path %r." % self.id2path(file_id))
1070
1150
            # Remove file_id and the unaltered children. If file_id is not
1071
1151
            # being deleted it will be reinserted back later.
1072
1152
            self.remove_recursive_id(file_id)
1075
1155
        # longest, ensuring that items which were modified and whose parents in
1076
1156
        # the resulting inventory were also modified, are inserted after their
1077
1157
        # parents.
1078
 
        for new_path, f, new_entry in sorted((np, f, e) for op, np, f, e in
 
1158
        for new_path, new_entry in sorted((np, e) for op, np, f, e in
1079
1159
                                          delta if np is not None):
1080
1160
            if new_entry.kind == 'directory':
1081
1161
                # Pop the child which to allow detection of children whose
1086
1166
                replacement.revision = new_entry.revision
1087
1167
                replacement.children = children.pop(replacement.file_id, {})
1088
1168
                new_entry = replacement
1089
 
            try:
1090
 
                self.add(new_entry)
1091
 
            except errors.DuplicateFileId:
1092
 
                raise errors.InconsistentDelta(new_path, new_entry.file_id,
1093
 
                    "New id is already present in target.")
1094
 
            except AttributeError:
1095
 
                raise errors.InconsistentDelta(new_path, new_entry.file_id,
1096
 
                    "Parent is not a directory.")
1097
 
            if self.id2path(new_entry.file_id) != new_path:
1098
 
                raise errors.InconsistentDelta(new_path, new_entry.file_id,
1099
 
                    "New path is not consistent with parent path.")
 
1169
            self.add(new_entry)
1100
1170
        if len(children):
1101
1171
            # Get the parent id that was deleted
1102
1172
            parent_id, children = children.popitem()
1103
1173
            raise errors.InconsistentDelta("<deleted>", parent_id,
1104
1174
                "The file id was deleted but its children were not deleted.")
1105
1175
 
1106
 
    def create_by_apply_delta(self, inventory_delta, new_revision_id,
1107
 
                              propagate_caches=False):
1108
 
        """See CHKInventory.create_by_apply_delta()"""
1109
 
        new_inv = self.copy()
1110
 
        new_inv.apply_delta(inventory_delta)
1111
 
        new_inv.revision_id = new_revision_id
1112
 
        return new_inv
1113
 
 
1114
1176
    def _set_root(self, ie):
1115
1177
        self.root = ie
1116
1178
        self._byid = {self.root.file_id: self.root}
1128
1190
            other.add(entry.copy())
1129
1191
        return other
1130
1192
 
 
1193
    def _get_mutable_inventory(self):
 
1194
        """See CommonInventory._get_mutable_inventory."""
 
1195
        return deepcopy(self)
 
1196
 
1131
1197
    def __iter__(self):
1132
1198
        """Iterate over all file-ids."""
1133
1199
        return iter(self._byid)
1173
1239
    def _add_child(self, entry):
1174
1240
        """Add an entry to the inventory, without adding it to its parent"""
1175
1241
        if entry.file_id in self._byid:
1176
 
            raise errors.BzrError(
1177
 
                "inventory already contains entry with id {%s}" %
1178
 
                entry.file_id)
 
1242
            raise BzrError("inventory already contains entry with id {%s}" %
 
1243
                           entry.file_id)
1179
1244
        self._byid[entry.file_id] = entry
1180
1245
        for child in getattr(entry, 'children', {}).itervalues():
1181
1246
            self._add_child(child)
1184
1249
    def add(self, entry):
1185
1250
        """Add entry to inventory.
1186
1251
 
1187
 
        :return: entry
 
1252
        To add  a file to a branch ready to be committed, use Branch.add,
 
1253
        which calls this.
 
1254
 
 
1255
        Returns the new entry object.
1188
1256
        """
1189
1257
        if entry.file_id in self._byid:
1190
1258
            raise errors.DuplicateFileId(entry.file_id,
1191
1259
                                         self._byid[entry.file_id])
 
1260
 
1192
1261
        if entry.parent_id is None:
1193
1262
            self.root = entry
1194
1263
        else:
1195
1264
            try:
1196
1265
                parent = self._byid[entry.parent_id]
1197
1266
            except KeyError:
1198
 
                raise errors.InconsistentDelta("<unknown>", entry.parent_id,
1199
 
                    "Parent not in inventory.")
 
1267
                raise BzrError("parent_id {%s} not in inventory" %
 
1268
                               entry.parent_id)
 
1269
 
1200
1270
            if entry.name in parent.children:
1201
 
                raise errors.InconsistentDelta(
1202
 
                    self.id2path(parent.children[entry.name].file_id),
1203
 
                    entry.file_id,
1204
 
                    "Path already versioned")
 
1271
                raise BzrError("%s is already versioned" %
 
1272
                        osutils.pathjoin(self.id2path(parent.file_id),
 
1273
                        entry.name).encode('utf-8'))
1205
1274
            parent.children[entry.name] = entry
1206
1275
        return self._add_child(entry)
1207
1276
 
1234
1303
        >>> inv = Inventory()
1235
1304
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1236
1305
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1237
 
        >>> inv.has_id('123')
 
1306
        >>> '123' in inv
1238
1307
        True
1239
1308
        >>> del inv['123']
1240
 
        >>> inv.has_id('123')
 
1309
        >>> '123' in inv
1241
1310
        False
1242
1311
        """
1243
1312
        ie = self[file_id]
1282
1351
            yield ie
1283
1352
            file_id = ie.parent_id
1284
1353
 
 
1354
    def has_filename(self, filename):
 
1355
        return bool(self.path2id(filename))
 
1356
 
1285
1357
    def has_id(self, file_id):
1286
1358
        return (file_id in self._byid)
1287
1359
 
1345
1417
        """
1346
1418
        new_name = ensure_normalized_name(new_name)
1347
1419
        if not is_valid_name(new_name):
1348
 
            raise errors.BzrError("not an acceptable filename: %r" % new_name)
 
1420
            raise BzrError("not an acceptable filename: %r" % new_name)
1349
1421
 
1350
1422
        new_parent = self._byid[new_parent_id]
1351
1423
        if new_name in new_parent.children:
1352
 
            raise errors.BzrError("%r already exists in %r" %
1353
 
                (new_name, self.id2path(new_parent_id)))
 
1424
            raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
1354
1425
 
1355
1426
        new_parent_idpath = self.get_idpath(new_parent_id)
1356
1427
        if file_id in new_parent_idpath:
1357
 
            raise errors.BzrError(
1358
 
                "cannot move directory %r into a subdirectory of itself, %r"
 
1428
            raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
1359
1429
                    % (self.id2path(file_id), self.id2path(new_parent_id)))
1360
1430
 
1361
1431
        file_ie = self._byid[file_id]
1397
1467
    def __init__(self, search_key_name):
1398
1468
        CommonInventory.__init__(self)
1399
1469
        self._fileid_to_entry_cache = {}
1400
 
        self._fully_cached = False
1401
1470
        self._path_to_fileid_cache = {}
1402
1471
        self._search_key_name = search_key_name
1403
 
        self.root_id = None
1404
1472
 
1405
1473
    def __eq__(self, other):
1406
1474
        """Compare two sets by comparing their contents."""
1457
1525
        else:
1458
1526
            raise ValueError("unknown kind %r" % entry.kind)
1459
1527
 
1460
 
    def _expand_fileids_to_parents_and_children(self, file_ids):
1461
 
        """Give a more wholistic view starting with the given file_ids.
1462
 
 
1463
 
        For any file_id which maps to a directory, we will include all children
1464
 
        of that directory. We will also include all directories which are
1465
 
        parents of the given file_ids, but we will not include their children.
1466
 
 
1467
 
        eg:
1468
 
          /     # TREE_ROOT
1469
 
          foo/  # foo-id
1470
 
            baz # baz-id
1471
 
            frob/ # frob-id
1472
 
              fringle # fringle-id
1473
 
          bar/  # bar-id
1474
 
            bing # bing-id
1475
 
 
1476
 
        if given [foo-id] we will include
1477
 
            TREE_ROOT as interesting parents
1478
 
        and 
1479
 
            foo-id, baz-id, frob-id, fringle-id
1480
 
        As interesting ids.
1481
 
        """
1482
 
        interesting = set()
1483
 
        # TODO: Pre-pass over the list of fileids to see if anything is already
1484
 
        #       deserialized in self._fileid_to_entry_cache
1485
 
 
1486
 
        directories_to_expand = set()
1487
 
        children_of_parent_id = {}
1488
 
        # It is okay if some of the fileids are missing
1489
 
        for entry in self._getitems(file_ids):
1490
 
            if entry.kind == 'directory':
1491
 
                directories_to_expand.add(entry.file_id)
1492
 
            interesting.add(entry.parent_id)
1493
 
            children_of_parent_id.setdefault(entry.parent_id, set()
1494
 
                                             ).add(entry.file_id)
1495
 
 
1496
 
        # Now, interesting has all of the direct parents, but not the
1497
 
        # parents of those parents. It also may have some duplicates with
1498
 
        # specific_fileids
1499
 
        remaining_parents = interesting.difference(file_ids)
1500
 
        # When we hit the TREE_ROOT, we'll get an interesting parent of None,
1501
 
        # but we don't actually want to recurse into that
1502
 
        interesting.add(None) # this will auto-filter it in the loop
1503
 
        remaining_parents.discard(None) 
1504
 
        while remaining_parents:
1505
 
            next_parents = set()
1506
 
            for entry in self._getitems(remaining_parents):
1507
 
                next_parents.add(entry.parent_id)
1508
 
                children_of_parent_id.setdefault(entry.parent_id, set()
1509
 
                                                 ).add(entry.file_id)
1510
 
            # Remove any search tips we've already processed
1511
 
            remaining_parents = next_parents.difference(interesting)
1512
 
            interesting.update(remaining_parents)
1513
 
            # We should probably also .difference(directories_to_expand)
1514
 
        interesting.update(file_ids)
1515
 
        interesting.discard(None)
1516
 
        while directories_to_expand:
1517
 
            # Expand directories by looking in the
1518
 
            # parent_id_basename_to_file_id map
1519
 
            keys = [StaticTuple(f,).intern() for f in directories_to_expand]
1520
 
            directories_to_expand = set()
1521
 
            items = self.parent_id_basename_to_file_id.iteritems(keys)
1522
 
            next_file_ids = set([item[1] for item in items])
1523
 
            next_file_ids = next_file_ids.difference(interesting)
1524
 
            interesting.update(next_file_ids)
1525
 
            for entry in self._getitems(next_file_ids):
1526
 
                if entry.kind == 'directory':
1527
 
                    directories_to_expand.add(entry.file_id)
1528
 
                children_of_parent_id.setdefault(entry.parent_id, set()
1529
 
                                                 ).add(entry.file_id)
1530
 
        return interesting, children_of_parent_id
1531
 
 
1532
 
    def filter(self, specific_fileids):
1533
 
        """Get an inventory view filtered against a set of file-ids.
1534
 
 
1535
 
        Children of directories and parents are included.
1536
 
 
1537
 
        The result may or may not reference the underlying inventory
1538
 
        so it should be treated as immutable.
1539
 
        """
1540
 
        (interesting,
1541
 
         parent_to_children) = self._expand_fileids_to_parents_and_children(
1542
 
                                specific_fileids)
1543
 
        # There is some overlap here, but we assume that all interesting items
1544
 
        # are in the _fileid_to_entry_cache because we had to read them to
1545
 
        # determine if they were a dir we wanted to recurse, or just a file
1546
 
        # This should give us all the entries we'll want to add, so start
1547
 
        # adding
1548
 
        other = Inventory(self.root_id)
1549
 
        other.root.revision = self.root.revision
1550
 
        other.revision_id = self.revision_id
1551
 
        if not interesting or not parent_to_children:
1552
 
            # empty filter, or filtering entrys that don't exist
1553
 
            # (if even 1 existed, then we would have populated
1554
 
            # parent_to_children with at least the tree root.)
1555
 
            return other
1556
 
        cache = self._fileid_to_entry_cache
1557
 
        remaining_children = collections.deque(parent_to_children[self.root_id])
1558
 
        while remaining_children:
1559
 
            file_id = remaining_children.popleft()
1560
 
            ie = cache[file_id]
1561
 
            if ie.kind == 'directory':
1562
 
                ie = ie.copy() # We create a copy to depopulate the .children attribute
1563
 
            # TODO: depending on the uses of 'other' we should probably alwyas
1564
 
            #       '.copy()' to prevent someone from mutating other and
1565
 
            #       invaliding our internal cache
1566
 
            other.add(ie)
1567
 
            if file_id in parent_to_children:
1568
 
                remaining_children.extend(parent_to_children[file_id])
1569
 
        return other
1570
 
 
1571
1528
    @staticmethod
1572
1529
    def _bytes_to_utf8name_key(bytes):
1573
1530
        """Get the file_id, revision_id key out of bytes."""
1575
1532
        # to filter out empty names because of non rich-root...
1576
1533
        sections = bytes.split('\n')
1577
1534
        kind, file_id = sections[0].split(': ')
1578
 
        return (sections[2], intern(file_id), intern(sections[3]))
 
1535
        return (sections[2], file_id, sections[3])
1579
1536
 
1580
1537
    def _bytes_to_entry(self, bytes):
1581
1538
        """Deserialise a serialised entry."""
1603
1560
            result.reference_revision = sections[4]
1604
1561
        else:
1605
1562
            raise ValueError("Not a serialised entry %r" % bytes)
1606
 
        result.file_id = intern(result.file_id)
1607
 
        result.revision = intern(sections[3])
 
1563
        result.revision = sections[3]
1608
1564
        if result.parent_id == '':
1609
1565
            result.parent_id = None
1610
1566
        self._fileid_to_entry_cache[result.file_id] = result
1611
1567
        return result
1612
1568
 
 
1569
    def _get_mutable_inventory(self):
 
1570
        """See CommonInventory._get_mutable_inventory."""
 
1571
        entries = self.iter_entries()
 
1572
        inv = Inventory(None, self.revision_id)
 
1573
        for path, inv_entry in entries:
 
1574
            inv.add(inv_entry.copy())
 
1575
        return inv
 
1576
 
1613
1577
    def create_by_apply_delta(self, inventory_delta, new_revision_id,
1614
1578
        propagate_caches=False):
1615
1579
        """Create a new CHKInventory by applying inventory_delta to this one.
1616
1580
 
1617
 
        See the inventory developers documentation for the theory behind
1618
 
        inventory deltas.
1619
 
 
1620
1581
        :param inventory_delta: The inventory delta to apply. See
1621
1582
            Inventory.apply_delta for details.
1622
1583
        :param new_revision_id: The revision id of the resulting CHKInventory.
1624
1585
          copied to and updated for the result.
1625
1586
        :return: The new CHKInventory.
1626
1587
        """
1627
 
        split = osutils.split
1628
1588
        result = CHKInventory(self._search_key_name)
1629
1589
        if propagate_caches:
1630
1590
            # Just propagate the path-to-fileid cache for now
1639
1599
            search_key_func=search_key_func)
1640
1600
        result.id_to_entry._ensure_root()
1641
1601
        result.id_to_entry._root_node.set_maximum_size(maximum_size)
1642
 
        # Change to apply to the parent_id_basename delta. The dict maps
1643
 
        # (parent_id, basename) -> (old_key, new_value). We use a dict because
1644
 
        # when a path has its id replaced (e.g. the root is changed, or someone
1645
 
        # does bzr mv a b, bzr mv c a, we should output a single change to this
1646
 
        # map rather than two.
1647
 
        parent_id_basename_delta = {}
 
1602
        parent_id_basename_delta = []
1648
1603
        if self.parent_id_basename_to_file_id is not None:
1649
1604
            result.parent_id_basename_to_file_id = chk_map.CHKMap(
1650
1605
                self.parent_id_basename_to_file_id._store,
1660
1615
            result.parent_id_basename_to_file_id = None
1661
1616
        result.root_id = self.root_id
1662
1617
        id_to_entry_delta = []
1663
 
        # inventory_delta is only traversed once, so we just update the
1664
 
        # variable.
1665
 
        # Check for repeated file ids
1666
 
        inventory_delta = _check_delta_unique_ids(inventory_delta)
1667
 
        # Repeated old paths
1668
 
        inventory_delta = _check_delta_unique_old_paths(inventory_delta)
1669
 
        # Check for repeated new paths
1670
 
        inventory_delta = _check_delta_unique_new_paths(inventory_delta)
1671
 
        # Check for entries that don't match the fileid
1672
 
        inventory_delta = _check_delta_ids_match_entry(inventory_delta)
1673
 
        # Check for nonsense fileids
1674
 
        inventory_delta = _check_delta_ids_are_valid(inventory_delta)
1675
 
        # Check for new_path <-> entry consistency
1676
 
        inventory_delta = _check_delta_new_path_entry_both_or_None(
1677
 
            inventory_delta)
1678
 
        # All changed entries need to have their parents be directories and be
1679
 
        # at the right path. This set contains (path, id) tuples.
1680
 
        parents = set()
1681
 
        # When we delete an item, all the children of it must be either deleted
1682
 
        # or altered in their own right. As we batch process the change via
1683
 
        # CHKMap.apply_delta, we build a set of things to use to validate the
1684
 
        # delta.
1685
 
        deletes = set()
1686
 
        altered = set()
1687
1618
        for old_path, new_path, file_id, entry in inventory_delta:
1688
1619
            # file id changes
1689
1620
            if new_path == '':
1698
1629
                        del result._path_to_fileid_cache[old_path]
1699
1630
                    except KeyError:
1700
1631
                        pass
1701
 
                deletes.add(file_id)
1702
1632
            else:
1703
 
                new_key = StaticTuple(file_id,)
 
1633
                new_key = (file_id,)
1704
1634
                new_value = result._entry_to_bytes(entry)
1705
1635
                # Update caches. It's worth doing this whether
1706
1636
                # we're propagating the old caches or not.
1707
1637
                result._path_to_fileid_cache[new_path] = file_id
1708
 
                parents.add((split(new_path)[0], entry.parent_id))
1709
1638
            if old_path is None:
1710
1639
                old_key = None
1711
1640
            else:
1712
 
                old_key = StaticTuple(file_id,)
1713
 
                if self.id2path(file_id) != old_path:
1714
 
                    raise errors.InconsistentDelta(old_path, file_id,
1715
 
                        "Entry was at wrong other path %r." %
1716
 
                        self.id2path(file_id))
1717
 
                altered.add(file_id)
1718
 
            id_to_entry_delta.append(StaticTuple(old_key, new_key, new_value))
 
1641
                old_key = (file_id,)
 
1642
            id_to_entry_delta.append((old_key, new_key, new_value))
1719
1643
            if result.parent_id_basename_to_file_id is not None:
1720
1644
                # parent_id, basename changes
1721
1645
                if old_path is None:
1729
1653
                else:
1730
1654
                    new_key = self._parent_id_basename_key(entry)
1731
1655
                    new_value = file_id
1732
 
                # If the two keys are the same, the value will be unchanged
1733
 
                # as its always the file id for this entry.
1734
1656
                if old_key != new_key:
1735
 
                    # Transform a change into explicit delete/add preserving
1736
 
                    # a possible match on the key from a different file id.
1737
 
                    if old_key is not None:
1738
 
                        parent_id_basename_delta.setdefault(
1739
 
                            old_key, [None, None])[0] = old_key
1740
 
                    if new_key is not None:
1741
 
                        parent_id_basename_delta.setdefault(
1742
 
                            new_key, [None, None])[1] = new_value
1743
 
        # validate that deletes are complete.
1744
 
        for file_id in deletes:
1745
 
            entry = self[file_id]
1746
 
            if entry.kind != 'directory':
1747
 
                continue
1748
 
            # This loop could potentially be better by using the id_basename
1749
 
            # map to just get the child file ids.
1750
 
            for child in entry.children.values():
1751
 
                if child.file_id not in altered:
1752
 
                    raise errors.InconsistentDelta(self.id2path(child.file_id),
1753
 
                        child.file_id, "Child not deleted or reparented when "
1754
 
                        "parent deleted.")
 
1657
                    # If the two keys are the same, the value will be unchanged
 
1658
                    # as its always the file id.
 
1659
                    parent_id_basename_delta.append((old_key, new_key, new_value))
1755
1660
        result.id_to_entry.apply_delta(id_to_entry_delta)
1756
1661
        if parent_id_basename_delta:
1757
 
            # Transform the parent_id_basename delta data into a linear delta
1758
 
            # with only one record for a given key. Optimally this would allow
1759
 
            # re-keying, but its simpler to just output that as a delete+add
1760
 
            # to spend less time calculating the delta.
1761
 
            delta_list = []
1762
 
            for key, (old_key, value) in parent_id_basename_delta.iteritems():
1763
 
                if value is not None:
1764
 
                    delta_list.append((old_key, key, value))
1765
 
                else:
1766
 
                    delta_list.append((old_key, None, None))
1767
 
            result.parent_id_basename_to_file_id.apply_delta(delta_list)
1768
 
        parents.discard(('', None))
1769
 
        for parent_path, parent in parents:
1770
 
            try:
1771
 
                if result[parent].kind != 'directory':
1772
 
                    raise errors.InconsistentDelta(result.id2path(parent), parent,
1773
 
                        'Not a directory, but given children')
1774
 
            except errors.NoSuchId:
1775
 
                raise errors.InconsistentDelta("<unknown>", parent,
1776
 
                    "Parent is not present in resulting inventory.")
1777
 
            if result.path2id(parent_path) != parent:
1778
 
                raise errors.InconsistentDelta(parent_path, parent,
1779
 
                    "Parent has wrong path %r." % result.path2id(parent_path))
 
1662
            result.parent_id_basename_to_file_id.apply_delta(parent_id_basename_delta)
1780
1663
        return result
1781
1664
 
1782
1665
    @classmethod
1808
1691
                raise errors.BzrError('Duplicate key in inventory: %r\n%r'
1809
1692
                                      % (key, bytes))
1810
1693
            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,))
 
1694
        revision_id = info['revision_id']
 
1695
        root_id = info['root_id']
 
1696
        search_key_name = info.get('search_key_name', 'plain')
 
1697
        parent_id_basename_to_file_id = info.get(
 
1698
            'parent_id_basename_to_file_id', None)
1819
1699
        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
1700
 
1824
1701
        result = CHKInventory(search_key_name)
1825
1702
        result.revision_id = revision_id
1828
1705
                            result._search_key_name)
1829
1706
        if parent_id_basename_to_file_id is not None:
1830
1707
            result.parent_id_basename_to_file_id = chk_map.CHKMap(
1831
 
                chk_store, StaticTuple(parent_id_basename_to_file_id,),
 
1708
                chk_store, (parent_id_basename_to_file_id,),
1832
1709
                search_key_func=search_key_func)
1833
1710
        else:
1834
1711
            result.parent_id_basename_to_file_id = None
1835
1712
 
1836
 
        result.id_to_entry = chk_map.CHKMap(chk_store,
1837
 
                                            StaticTuple(id_to_entry,),
 
1713
        result.id_to_entry = chk_map.CHKMap(chk_store, (id_to_entry,),
1838
1714
                                            search_key_func=search_key_func)
1839
1715
        if (result.revision_id,) != expected_revision_id:
1840
1716
            raise ValueError("Mismatched revision id and expected: %r, %r" %
1862
1738
        id_to_entry_dict = {}
1863
1739
        parent_id_basename_dict = {}
1864
1740
        for path, entry in inventory.iter_entries():
1865
 
            key = StaticTuple(entry.file_id,).intern()
1866
 
            id_to_entry_dict[key] = entry_to_bytes(entry)
 
1741
            id_to_entry_dict[(entry.file_id,)] = entry_to_bytes(entry)
1867
1742
            p_id_key = parent_id_basename_key(entry)
1868
1743
            parent_id_basename_dict[p_id_key] = entry.file_id
1869
1744
 
1892
1767
            parent_id = entry.parent_id
1893
1768
        else:
1894
1769
            parent_id = ''
1895
 
        return StaticTuple(parent_id, entry.name.encode('utf8')).intern()
 
1770
        return parent_id, entry.name.encode('utf8')
1896
1771
 
1897
1772
    def __getitem__(self, file_id):
1898
1773
        """map a single file_id -> InventoryEntry."""
1903
1778
            return result
1904
1779
        try:
1905
1780
            return self._bytes_to_entry(
1906
 
                self.id_to_entry.iteritems([StaticTuple(file_id,)]).next()[1])
 
1781
                self.id_to_entry.iteritems([(file_id,)]).next()[1])
1907
1782
        except StopIteration:
1908
1783
            # really we're passing an inventory, not a tree...
1909
1784
            raise errors.NoSuchId(self, file_id)
1910
1785
 
1911
 
    def _getitems(self, file_ids):
1912
 
        """Similar to __getitem__, but lets you query for multiple.
1913
 
        
1914
 
        The returned order is undefined. And currently if an item doesn't
1915
 
        exist, it isn't included in the output.
1916
 
        """
1917
 
        result = []
1918
 
        remaining = []
1919
 
        for file_id in file_ids:
1920
 
            entry = self._fileid_to_entry_cache.get(file_id, None)
1921
 
            if entry is None:
1922
 
                remaining.append(file_id)
1923
 
            else:
1924
 
                result.append(entry)
1925
 
        file_keys = [StaticTuple(f,).intern() for f in remaining]
1926
 
        for file_key, value in self.id_to_entry.iteritems(file_keys):
1927
 
            entry = self._bytes_to_entry(value)
1928
 
            result.append(entry)
1929
 
            self._fileid_to_entry_cache[entry.file_id] = entry
1930
 
        return result
1931
 
 
1932
1786
    def has_id(self, file_id):
1933
1787
        # Perhaps have an explicit 'contains' method on CHKMap ?
1934
1788
        if self._fileid_to_entry_cache.get(file_id, None) is not None:
1935
1789
            return True
1936
 
        return len(list(
1937
 
            self.id_to_entry.iteritems([StaticTuple(file_id,)]))) == 1
 
1790
        return len(list(self.id_to_entry.iteritems([(file_id,)]))) == 1
1938
1791
 
1939
1792
    def is_root(self, file_id):
1940
1793
        return file_id == self.root_id
1956
1809
 
1957
1810
    def iter_just_entries(self):
1958
1811
        """Iterate over all entries.
1959
 
 
 
1812
        
1960
1813
        Unlike iter_entries(), just the entries are returned (not (path, ie))
1961
1814
        and the order of entries is undefined.
1962
1815
 
1970
1823
                self._fileid_to_entry_cache[file_id] = ie
1971
1824
            yield ie
1972
1825
 
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
1826
    def iter_changes(self, basis):
2027
1827
        """Generate a Tree.iter_changes change list between this and basis.
2028
1828
 
2122
1922
            delta.append((old_path, new_path, file_id, entry))
2123
1923
        return delta
2124
1924
 
2125
 
    def path2id(self, relpath):
 
1925
    def path2id(self, name):
2126
1926
        """See CommonInventory.path2id()."""
2127
 
        # TODO: perhaps support negative hits?
2128
 
        result = self._path_to_fileid_cache.get(relpath, None)
2129
 
        if result is not None:
2130
 
            return result
2131
 
        if isinstance(relpath, basestring):
2132
 
            names = osutils.splitpath(relpath)
2133
 
        else:
2134
 
            names = relpath
2135
 
        current_id = self.root_id
2136
 
        if current_id is None:
2137
 
            return None
2138
 
        parent_id_index = self.parent_id_basename_to_file_id
2139
 
        cur_path = None
2140
 
        for basename in names:
2141
 
            if cur_path is None:
2142
 
                cur_path = basename
2143
 
            else:
2144
 
                cur_path = cur_path + '/' + basename
2145
 
            basename_utf8 = basename.encode('utf8')
2146
 
            file_id = self._path_to_fileid_cache.get(cur_path, None)
2147
 
            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
2159
 
            current_id = file_id
2160
 
        return current_id
 
1927
        result = self._path_to_fileid_cache.get(name, None)
 
1928
        if result is None:
 
1929
            result = CommonInventory.path2id(self, name)
 
1930
            self._path_to_fileid_cache[name] = result
 
1931
        return result
2161
1932
 
2162
1933
    def to_lines(self):
2163
1934
        """Serialise the inventory to lines."""
2167
1938
            lines.append('search_key_name: %s\n' % (self._search_key_name,))
2168
1939
            lines.append("root_id: %s\n" % self.root_id)
2169
1940
            lines.append('parent_id_basename_to_file_id: %s\n' %
2170
 
                (self.parent_id_basename_to_file_id.key()[0],))
 
1941
                self.parent_id_basename_to_file_id.key())
2171
1942
            lines.append("revision_id: %s\n" % self.revision_id)
2172
 
            lines.append("id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
 
1943
            lines.append("id_to_entry: %s\n" % self.id_to_entry.key())
2173
1944
        else:
2174
1945
            lines.append("revision_id: %s\n" % self.revision_id)
2175
1946
            lines.append("root_id: %s\n" % self.root_id)
2176
1947
            if self.parent_id_basename_to_file_id is not None:
2177
1948
                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],))
 
1949
                    self.parent_id_basename_to_file_id.key())
 
1950
            lines.append("id_to_entry: %s\n" % self.id_to_entry.key())
2180
1951
        return lines
2181
1952
 
2182
1953
    @property
2188
1959
class CHKInventoryDirectory(InventoryDirectory):
2189
1960
    """A directory in an inventory."""
2190
1961
 
2191
 
    __slots__ = ['_children', '_chk_inventory']
 
1962
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
1963
                 'text_id', 'parent_id', '_children', 'executable',
 
1964
                 'revision', 'symlink_target', 'reference_revision',
 
1965
                 '_chk_inventory']
2192
1966
 
2193
1967
    def __init__(self, file_id, name, parent_id, chk_inventory):
2194
1968
        # Don't call InventoryDirectory.__init__ - it isn't right for this
2195
1969
        # class.
2196
1970
        InventoryEntry.__init__(self, file_id, name, parent_id)
2197
1971
        self._children = None
 
1972
        self.kind = 'directory'
2198
1973
        self._chk_inventory = chk_inventory
2199
1974
 
2200
1975
    @property
2219
1994
        parent_id_index = self._chk_inventory.parent_id_basename_to_file_id
2220
1995
        child_keys = set()
2221
1996
        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,))
 
1997
            key_filter=[(self.file_id,)]):
 
1998
            child_keys.add((file_id,))
2224
1999
        cached = set()
2225
2000
        for file_id_key in child_keys:
2226
2001
            entry = self._chk_inventory._fileid_to_entry_cache.get(
2259
2034
    try:
2260
2035
        factory = entry_factory[kind]
2261
2036
    except KeyError:
2262
 
        raise errors.BadFileKindError(name, kind)
 
2037
        raise BzrError("unknown kind %r" % kind)
2263
2038
    return factory(file_id, name, parent_id)
2264
2039
 
2265
2040
 
2285
2060
    return name
2286
2061
 
2287
2062
 
2288
 
_NAME_RE = lazy_regex.lazy_compile(r'^[^/\\]+$')
 
2063
_NAME_RE = None
2289
2064
 
2290
2065
def is_valid_name(name):
 
2066
    global _NAME_RE
 
2067
    if _NAME_RE is None:
 
2068
        _NAME_RE = re.compile(r'^[^/\\]+$')
 
2069
 
2291
2070
    return bool(_NAME_RE.match(name))
2292
 
 
2293
 
 
2294
 
def _check_delta_unique_ids(delta):
2295
 
    """Decorate a delta and check that the file ids in it are unique.
2296
 
 
2297
 
    :return: A generator over delta.
2298
 
    """
2299
 
    ids = set()
2300
 
    for item in delta:
2301
 
        length = len(ids) + 1
2302
 
        ids.add(item[2])
2303
 
        if len(ids) != length:
2304
 
            raise errors.InconsistentDelta(item[0] or item[1], item[2],
2305
 
                "repeated file_id")
2306
 
        yield item
2307
 
 
2308
 
 
2309
 
def _check_delta_unique_new_paths(delta):
2310
 
    """Decorate a delta and check that the new paths in it are unique.
2311
 
 
2312
 
    :return: A generator over delta.
2313
 
    """
2314
 
    paths = set()
2315
 
    for item in delta:
2316
 
        length = len(paths) + 1
2317
 
        path = item[1]
2318
 
        if path is not None:
2319
 
            paths.add(path)
2320
 
            if len(paths) != length:
2321
 
                raise errors.InconsistentDelta(path, item[2], "repeated path")
2322
 
        yield item
2323
 
 
2324
 
 
2325
 
def _check_delta_unique_old_paths(delta):
2326
 
    """Decorate a delta and check that the old paths in it are unique.
2327
 
 
2328
 
    :return: A generator over delta.
2329
 
    """
2330
 
    paths = set()
2331
 
    for item in delta:
2332
 
        length = len(paths) + 1
2333
 
        path = item[0]
2334
 
        if path is not None:
2335
 
            paths.add(path)
2336
 
            if len(paths) != length:
2337
 
                raise errors.InconsistentDelta(path, item[2], "repeated path")
2338
 
        yield item
2339
 
 
2340
 
 
2341
 
def _check_delta_ids_are_valid(delta):
2342
 
    """Decorate a delta and check that the ids in it are valid.
2343
 
 
2344
 
    :return: A generator over delta.
2345
 
    """
2346
 
    for item in delta:
2347
 
        entry = item[3]
2348
 
        if item[2] is None:
2349
 
            raise errors.InconsistentDelta(item[0] or item[1], item[2],
2350
 
                "entry with file_id None %r" % entry)
2351
 
        if type(item[2]) != str:
2352
 
            raise errors.InconsistentDelta(item[0] or item[1], item[2],
2353
 
                "entry with non bytes file_id %r" % entry)
2354
 
        yield item
2355
 
 
2356
 
 
2357
 
def _check_delta_ids_match_entry(delta):
2358
 
    """Decorate a delta and check that the ids in it match the entry.file_id.
2359
 
 
2360
 
    :return: A generator over delta.
2361
 
    """
2362
 
    for item in delta:
2363
 
        entry = item[3]
2364
 
        if entry is not None:
2365
 
            if entry.file_id != item[2]:
2366
 
                raise errors.InconsistentDelta(item[0] or item[1], item[2],
2367
 
                    "mismatched id with %r" % entry)
2368
 
        yield item
2369
 
 
2370
 
 
2371
 
def _check_delta_new_path_entry_both_or_None(delta):
2372
 
    """Decorate a delta and check that the new_path and entry are paired.
2373
 
 
2374
 
    :return: A generator over delta.
2375
 
    """
2376
 
    for item in delta:
2377
 
        new_path = item[1]
2378
 
        entry = item[3]
2379
 
        if new_path is None and entry is not None:
2380
 
            raise errors.InconsistentDelta(item[0], item[1],
2381
 
                "Entry with no new_path")
2382
 
        if new_path is not None and entry is None:
2383
 
            raise errors.InconsistentDelta(new_path, item[1],
2384
 
                "new_path with no entry")
2385
 
        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