~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Wouter van Heyst
  • Date: 2006-06-07 16:05:27 UTC
  • mto: This revision was merged to the branch mainline in revision 1752.
  • Revision ID: larstiq@larstiq.dyndns.org-20060607160527-2b3649154d0e2e84
more code cleanup

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
 
1
# Copyright (C) 2005, 2006 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
 
import os
 
30
 
 
31
import collections
 
32
import os.path
31
33
import re
32
34
import sys
33
 
 
34
 
from bzrlib.lazy_import import lazy_import
35
 
lazy_import(globals(), """
36
 
import collections
37
35
import tarfile
 
36
import types
38
37
 
39
38
import bzrlib
40
 
from bzrlib import (
41
 
    errors,
42
 
    generate_ids,
43
 
    osutils,
44
 
    symbol_versioning,
45
 
    workingtree,
46
 
    )
47
 
""")
48
 
 
49
 
from bzrlib.errors import (
50
 
    BzrCheckError,
51
 
    BzrError,
52
 
    )
 
39
from bzrlib.osutils import (pumpfile, quotefn, splitpath, joinpath,
 
40
                            pathjoin, sha_strings)
 
41
from bzrlib.errors import (NotVersionedError, InvalidEntryName,
 
42
                           BzrError, BzrCheckError, BinaryFile)
53
43
from bzrlib.trace import mutter
54
44
 
55
45
 
87
77
    >>> i.path2id('')
88
78
    'TREE_ROOT'
89
79
    >>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
90
 
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
 
80
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
91
81
    >>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
92
 
    InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
93
 
    >>> shouldbe = {0: '', 1: 'src', 2: 'src/hello.c'}
 
82
    InventoryFile('2323', 'hello.c', parent_id='123')
 
83
    >>> shouldbe = {0: 'src', 1: pathjoin('src','hello.c')}
94
84
    >>> for ix, j in enumerate(i.iter_entries()):
95
85
    ...   print (j[0] == shouldbe[ix], j[1])
96
86
    ... 
97
 
    (True, InventoryDirectory('TREE_ROOT', u'', parent_id=None, revision=None))
98
 
    (True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
99
 
    (True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
 
87
    (True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
 
88
    (True, InventoryFile('2323', 'hello.c', parent_id='123'))
 
89
    >>> i.add(InventoryFile('2323', 'bye.c', '123'))
 
90
    Traceback (most recent call last):
 
91
    ...
 
92
    BzrError: inventory already contains entry with id {2323}
100
93
    >>> i.add(InventoryFile('2324', 'bye.c', '123'))
101
 
    InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
 
94
    InventoryFile('2324', 'bye.c', parent_id='123')
102
95
    >>> i.add(InventoryDirectory('2325', 'wibble', '123'))
103
 
    InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
 
96
    InventoryDirectory('2325', 'wibble', parent_id='123')
104
97
    >>> i.path2id('src/wibble')
105
98
    '2325'
106
99
    >>> '2325' in i
107
100
    True
108
101
    >>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
109
 
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
 
102
    InventoryFile('2326', 'wibble.c', parent_id='2325')
110
103
    >>> i['2326']
111
 
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
 
104
    InventoryFile('2326', 'wibble.c', parent_id='2325')
112
105
    >>> for path, entry in i.iter_entries():
113
106
    ...     print path
114
107
    ...     assert i.path2id(path)
115
108
    ... 
116
 
    <BLANKLINE>
117
109
    src
118
110
    src/bye.c
119
111
    src/hello.c
131
123
    RENAMED = 'renamed'
132
124
    MODIFIED_AND_RENAMED = 'modified and renamed'
133
125
    
134
 
    __slots__ = []
 
126
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
127
                 'text_id', 'parent_id', 'children', 'executable', 
 
128
                 'revision']
 
129
 
 
130
    def _add_text_to_weave(self, new_lines, parents, weave_store, transaction):
 
131
        versionedfile = weave_store.get_weave_or_empty(self.file_id,
 
132
                                                       transaction)
 
133
        versionedfile.add_lines(self.revision, parents, new_lines)
 
134
        versionedfile.clear_cache()
135
135
 
136
136
    def detect_changes(self, old_entry):
137
137
        """Return a (text_modified, meta_modified) from this to old_entry.
166
166
                            versioned_file_store,
167
167
                            transaction,
168
168
                            entry_vf=None):
169
 
        """Return the revisions and entries that directly precede this.
 
169
        """Return the revisions and entries that directly preceed this.
170
170
 
171
171
        Returned as a map from revision to inventory entry.
172
172
 
181
181
        :param entry_vf: The entry versioned file, if its already available.
182
182
        """
183
183
        def get_ancestors(weave, entry):
184
 
            return set(weave.get_ancestry(entry.revision, topo_sorted=False))
 
184
            return set(weave.get_ancestry(entry.revision))
185
185
        # revision:ie mapping for each ie found in previous_inventories.
186
186
        candidates = {}
187
187
        # revision:ie mapping with one revision for each head.
193
193
            if self.file_id in inv:
194
194
                ie = inv[self.file_id]
195
195
                assert ie.file_id == self.file_id
196
 
                if ie.kind != self.kind:
197
 
                    # Can't be a candidate if the kind has changed.
198
 
                    continue
199
196
                if ie.revision in candidates:
200
197
                    # same revision value in two different inventories:
201
198
                    # correct possible inconsistencies:
253
250
 
254
251
    def get_tar_item(self, root, dp, now, tree):
255
252
        """Get a tarfile item and a file stream for its content."""
256
 
        item = tarfile.TarInfo(osutils.pathjoin(root, dp).encode('utf8'))
 
253
        item = tarfile.TarInfo(pathjoin(root, dp))
257
254
        # TODO: would be cool to actually set it to the timestamp of the
258
255
        # revision it was last changed
259
256
        item.mtime = now
288
285
        """
289
286
        assert isinstance(name, basestring), name
290
287
        if '/' in name or '\\' in name:
291
 
            raise errors.InvalidEntryName(name=name)
 
288
            raise InvalidEntryName(name=name)
292
289
        self.executable = False
293
290
        self.revision = None
294
291
        self.text_sha1 = None
295
292
        self.text_size = None
296
293
        self.file_id = file_id
297
 
        assert isinstance(file_id, (str, None.__class__)), \
298
 
            'bad type %r for %r' % (type(file_id), file_id)
299
294
        self.name = name
300
295
        self.text_id = text_id
301
296
        self.parent_id = parent_id
302
297
        self.symlink_target = None
303
 
        self.reference_revision = None
304
298
 
305
299
    def kind_character(self):
306
300
        """Return a short kind indicator useful for appending to names."""
307
301
        raise BzrError('unknown kind %r' % self.kind)
308
302
 
309
 
    known_kinds = ('file', 'directory', 'symlink')
 
303
    known_kinds = ('file', 'directory', 'symlink', 'root_directory')
310
304
 
311
305
    def _put_in_tar(self, item, tree):
312
306
        """populate item for stashing in a tar, and return the content stream.
321
315
        
322
316
        This is a template method - implement _put_on_disk in subclasses.
323
317
        """
324
 
        fullpath = osutils.pathjoin(dest, dp)
 
318
        fullpath = pathjoin(dest, dp)
325
319
        self._put_on_disk(fullpath, tree)
326
 
        # mutter("  export {%s} kind %s to %s", self.file_id,
327
 
        #         self.kind, fullpath)
 
320
        mutter("  export {%s} kind %s to %s", self.file_id,
 
321
                self.kind, fullpath)
328
322
 
329
323
    def _put_on_disk(self, fullpath, tree):
330
324
        """Put this entry onto disk at fullpath, from tree tree."""
331
325
        raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
332
326
 
333
327
    def sorted_children(self):
334
 
        return sorted(self.children.items())
 
328
        l = self.children.items()
 
329
        l.sort()
 
330
        return l
335
331
 
336
332
    @staticmethod
337
333
    def versionable_kind(kind):
338
 
        return (kind in ('file', 'directory', 'symlink', 'tree-reference'))
 
334
        return kind in ('file', 'directory', 'symlink')
339
335
 
340
336
    def check(self, checker, rev_id, inv, tree):
341
337
        """Check this inventory entry is intact.
351
347
        :param inv: Inventory from which the entry was loaded.
352
348
        :param tree: RevisionTree for this entry.
353
349
        """
354
 
        if self.parent_id is not None:
 
350
        if self.parent_id != None:
355
351
            if not inv.has_id(self.parent_id):
356
352
                raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
357
353
                        % (self.parent_id, rev_id))
386
382
            return 'added'
387
383
        elif new_entry is None:
388
384
            return 'removed'
389
 
        if old_entry.kind != new_entry.kind:
390
 
            return 'modified'
391
385
        text_modified, meta_modified = new_entry.detect_changes(old_entry)
392
386
        if text_modified or meta_modified:
393
387
            modified = True
409
403
        return 'unchanged'
410
404
 
411
405
    def __repr__(self):
412
 
        return ("%s(%r, %r, parent_id=%r, revision=%r)"
 
406
        return ("%s(%r, %r, parent_id=%r)"
413
407
                % (self.__class__.__name__,
414
408
                   self.file_id,
415
409
                   self.name,
416
 
                   self.parent_id,
417
 
                   self.revision))
 
410
                   self.parent_id))
418
411
 
419
412
    def snapshot(self, revision, path, previous_entries,
420
 
                 work_tree, commit_builder):
 
413
                 work_tree, weave_store, transaction):
421
414
        """Make a snapshot of this entry which may or may not have changed.
422
415
        
423
416
        This means that all its fields are populated, that it has its
424
417
        text stored in the text store or weave.
425
418
        """
426
 
        # mutter('new parents of %s are %r', path, previous_entries)
 
419
        mutter('new parents of %s are %r', path, previous_entries)
427
420
        self._read_tree_state(path, work_tree)
428
 
        # TODO: Where should we determine whether to reuse a
429
 
        # previous revision id or create a new revision? 20060606
430
421
        if len(previous_entries) == 1:
431
422
            # cannot be unchanged unless there is only one parent file rev.
432
423
            parent_ie = previous_entries.values()[0]
433
424
            if self._unchanged(parent_ie):
434
 
                # mutter("found unchanged entry")
 
425
                mutter("found unchanged entry")
435
426
                self.revision = parent_ie.revision
436
427
                return "unchanged"
437
428
        return self._snapshot_into_revision(revision, previous_entries, 
438
 
                                            work_tree, commit_builder)
 
429
                                            work_tree, weave_store, transaction)
439
430
 
440
431
    def _snapshot_into_revision(self, revision, previous_entries, work_tree,
441
 
                                commit_builder):
 
432
                                weave_store, transaction):
442
433
        """Record this revision unconditionally into a store.
443
434
 
444
435
        The entry's last-changed revision property (`revision`) is updated to 
448
439
 
449
440
        :returns: String description of the commit (e.g. "merged", "modified"), etc.
450
441
        """
451
 
        # mutter('new revision {%s} for {%s}', revision, self.file_id)
 
442
        mutter('new revision {%s} for {%s}', revision, self.file_id)
452
443
        self.revision = revision
453
 
        self._snapshot_text(previous_entries, work_tree, commit_builder)
 
444
        self._snapshot_text(previous_entries, work_tree, weave_store,
 
445
                            transaction)
454
446
 
455
 
    def _snapshot_text(self, file_parents, work_tree, commit_builder): 
 
447
    def _snapshot_text(self, file_parents, work_tree, weave_store, transaction): 
456
448
        """Record the 'text' of this entry, whatever form that takes.
457
449
        
458
450
        This default implementation simply adds an empty text.
459
451
        """
460
 
        raise NotImplementedError(self._snapshot_text)
 
452
        mutter('storing file {%s} in revision {%s}',
 
453
               self.file_id, self.revision)
 
454
        self._add_text_to_weave([], file_parents.keys(), weave_store, transaction)
461
455
 
462
456
    def __eq__(self, other):
463
457
        if not isinstance(other, InventoryEntry):
473
467
                and (self.kind == other.kind)
474
468
                and (self.revision == other.revision)
475
469
                and (self.executable == other.executable)
476
 
                and (self.reference_revision == other.reference_revision)
477
470
                )
478
471
 
479
472
    def __ne__(self, other):
485
478
    def _unchanged(self, previous_ie):
486
479
        """Has this entry changed relative to previous_ie.
487
480
 
488
 
        This method should be overridden in child classes.
 
481
        This method should be overriden in child classes.
489
482
        """
490
483
        compatible = True
491
484
        # different inv parent
494
487
        # renamed
495
488
        elif previous_ie.name != self.name:
496
489
            compatible = False
497
 
        elif previous_ie.kind != self.kind:
498
 
            compatible = False
499
490
        return compatible
500
491
 
501
492
    def _read_tree_state(self, path, work_tree):
515
506
 
516
507
class RootEntry(InventoryEntry):
517
508
 
518
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
519
 
                 'text_id', 'parent_id', 'children', 'executable',
520
 
                 'revision', 'symlink_target', 'reference_revision']
521
 
 
522
509
    def _check(self, checker, rev_id, tree):
523
510
        """See InventoryEntry._check"""
524
511
 
525
512
    def __init__(self, file_id):
526
513
        self.file_id = file_id
527
514
        self.children = {}
528
 
        self.kind = 'directory'
 
515
        self.kind = 'root_directory'
529
516
        self.parent_id = None
530
517
        self.name = u''
531
 
        self.revision = None
532
 
        symbol_versioning.warn('RootEntry is deprecated as of bzr 0.10.'
533
 
                               '  Please use InventoryDirectory instead.',
534
 
                               DeprecationWarning, stacklevel=2)
535
518
 
536
519
    def __eq__(self, other):
537
520
        if not isinstance(other, RootEntry):
544
527
class InventoryDirectory(InventoryEntry):
545
528
    """A directory in an inventory."""
546
529
 
547
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
548
 
                 'text_id', 'parent_id', 'children', 'executable',
549
 
                 'revision', 'symlink_target', 'reference_revision']
550
 
 
551
530
    def _check(self, checker, rev_id, tree):
552
531
        """See InventoryEntry._check"""
553
 
        if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
 
532
        if self.text_sha1 != None or self.text_size != None or self.text_id != None:
554
533
            raise BzrCheckError('directory {%s} has text in revision {%s}'
555
534
                                % (self.file_id, rev_id))
556
535
 
583
562
        """See InventoryEntry._put_on_disk."""
584
563
        os.mkdir(fullpath)
585
564
 
586
 
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
587
 
        """See InventoryEntry._snapshot_text."""
588
 
        commit_builder.modified_directory(self.file_id, file_parents)
589
 
 
590
565
 
591
566
class InventoryFile(InventoryEntry):
592
567
    """A file in an inventory."""
593
568
 
594
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
595
 
                 'text_id', 'parent_id', 'children', 'executable',
596
 
                 'revision', 'symlink_target', 'reference_revision']
597
 
 
598
569
    def _check(self, checker, tree_revision_id, tree):
599
570
        """See InventoryEntry._check"""
600
571
        t = (self.file_id, self.revision)
639
610
 
640
611
    def detect_changes(self, old_entry):
641
612
        """See InventoryEntry.detect_changes."""
642
 
        assert self.text_sha1 is not None
643
 
        assert old_entry.text_sha1 is not None
 
613
        assert self.text_sha1 != None
 
614
        assert old_entry.text_sha1 != None
644
615
        text_modified = (self.text_sha1 != old_entry.text_sha1)
645
616
        meta_modified = (self.executable != old_entry.executable)
646
617
        return text_modified, meta_modified
660
631
            else:
661
632
                text_diff(to_label, to_text,
662
633
                          from_label, from_text, output_to)
663
 
        except errors.BinaryFile:
 
634
        except BinaryFile:
664
635
            if reverse:
665
636
                label_pair = (to_label, from_label)
666
637
            else:
692
663
 
693
664
    def _put_on_disk(self, fullpath, tree):
694
665
        """See InventoryEntry._put_on_disk."""
695
 
        osutils.pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
 
666
        pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
696
667
        if tree.is_executable(self.file_id):
697
668
            os.chmod(fullpath, 0755)
698
669
 
699
670
    def _read_tree_state(self, path, work_tree):
700
671
        """See InventoryEntry._read_tree_state."""
701
672
        self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
702
 
        # FIXME: 20050930 probe for the text size when getting sha1
703
 
        # in _read_tree_state
704
673
        self.executable = work_tree.is_executable(self.file_id, path=path)
705
674
 
706
 
    def __repr__(self):
707
 
        return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s)"
708
 
                % (self.__class__.__name__,
709
 
                   self.file_id,
710
 
                   self.name,
711
 
                   self.parent_id,
712
 
                   self.text_sha1,
713
 
                   self.text_size))
714
 
 
715
675
    def _forget_tree_state(self):
716
676
        self.text_sha1 = None
 
677
        self.executable = None
717
678
 
718
 
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
 
679
    def _snapshot_text(self, file_parents, work_tree, versionedfile_store, transaction):
719
680
        """See InventoryEntry._snapshot_text."""
720
 
        def get_content_byte_lines():
721
 
            return work_tree.get_file(self.file_id).readlines()
722
 
        self.text_sha1, self.text_size = commit_builder.modified_file_text(
723
 
            self.file_id, file_parents, get_content_byte_lines, self.text_sha1, self.text_size)
 
681
        mutter('storing text of file {%s} in revision {%s} into %r',
 
682
               self.file_id, self.revision, versionedfile_store)
 
683
        # special case to avoid diffing on renames or 
 
684
        # reparenting
 
685
        if (len(file_parents) == 1
 
686
            and self.text_sha1 == file_parents.values()[0].text_sha1
 
687
            and self.text_size == file_parents.values()[0].text_size):
 
688
            previous_ie = file_parents.values()[0]
 
689
            versionedfile = versionedfile_store.get_weave(self.file_id, transaction)
 
690
            versionedfile.clone_text(self.revision, previous_ie.revision, file_parents.keys())
 
691
        else:
 
692
            new_lines = work_tree.get_file(self.file_id).readlines()
 
693
            self._add_text_to_weave(new_lines, file_parents.keys(), versionedfile_store,
 
694
                                    transaction)
 
695
            self.text_sha1 = sha_strings(new_lines)
 
696
            self.text_size = sum(map(len, new_lines))
 
697
 
724
698
 
725
699
    def _unchanged(self, previous_ie):
726
700
        """See InventoryEntry._unchanged."""
739
713
class InventoryLink(InventoryEntry):
740
714
    """A file in an inventory."""
741
715
 
742
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
743
 
                 'text_id', 'parent_id', 'children', 'executable',
744
 
                 'revision', 'symlink_target', 'reference_revision']
 
716
    __slots__ = ['symlink_target']
745
717
 
746
718
    def _check(self, checker, rev_id, tree):
747
719
        """See InventoryEntry._check"""
748
 
        if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
 
720
        if self.text_sha1 != None or self.text_size != None or self.text_id != None:
749
721
            raise BzrCheckError('symlink {%s} has text in revision {%s}'
750
722
                    % (self.file_id, rev_id))
751
 
        if self.symlink_target is None:
 
723
        if self.symlink_target == None:
752
724
            raise BzrCheckError('symlink {%s} has no target in revision {%s}'
753
725
                    % (self.file_id, rev_id))
754
726
 
822
794
            compatible = False
823
795
        return compatible
824
796
 
825
 
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
826
 
        """See InventoryEntry._snapshot_text."""
827
 
        commit_builder.modified_link(
828
 
            self.file_id, file_parents, self.symlink_target)
829
 
 
830
 
 
831
 
class TreeReference(InventoryEntry):
832
 
    
833
 
    kind = 'tree-reference'
834
 
    
835
 
    def __init__(self, file_id, name, parent_id, revision=None,
836
 
                 reference_revision=None):
837
 
        InventoryEntry.__init__(self, file_id, name, parent_id)
838
 
        self.revision = revision
839
 
        self.reference_revision = reference_revision
840
 
 
841
 
    def copy(self):
842
 
        return TreeReference(self.file_id, self.name, self.parent_id,
843
 
                             self.revision, self.reference_revision)
844
 
 
845
 
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
846
 
        commit_builder.modified_reference(self.file_id, file_parents)
847
 
 
848
 
    def _read_tree_state(self, path, work_tree):
849
 
        """Populate fields in the inventory entry from the given tree.
850
 
        """
851
 
        self.reference_revision = work_tree.get_reference_revision(
852
 
            self.file_id, path)
853
 
 
854
 
    def _forget_tree_state(self):
855
 
        self.reference_revision = None 
856
 
 
857
797
 
858
798
class Inventory(object):
859
799
    """Inventory of versioned files in a tree.
874
814
 
875
815
    >>> inv = Inventory()
876
816
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
877
 
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
 
817
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT')
878
818
    >>> inv['123-123'].name
879
819
    'hello.c'
880
820
 
888
828
    May also look up by name:
889
829
 
890
830
    >>> [x[0] for x in inv.iter_entries()]
891
 
    ['', u'hello.c']
 
831
    [u'hello.c']
892
832
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
893
833
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
894
 
    Traceback (most recent call last):
895
 
    BzrError: parent_id {TREE_ROOT} not in inventory
896
 
    >>> inv.add(InventoryFile('123-123', 'hello.c', 'TREE_ROOT-12345678-12345678'))
897
 
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None)
 
834
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
898
835
    """
899
836
    def __init__(self, root_id=ROOT_ID, revision_id=None):
900
837
        """Create or read an inventory.
906
843
        The inventory is created with a default root directory, with
907
844
        an id of None.
908
845
        """
909
 
        if root_id is not None:
910
 
            assert root_id.__class__ == str
911
 
            self._set_root(InventoryDirectory(root_id, u'', None))
912
 
        else:
913
 
            self.root = None
914
 
            self._byid = {}
 
846
        # We are letting Branch.create() create a unique inventory
 
847
        # root id. Rather than generating a random one here.
 
848
        #if root_id is None:
 
849
        #    root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
 
850
        self.root = RootEntry(root_id)
915
851
        self.revision_id = revision_id
916
 
 
917
 
    def _set_root(self, ie):
918
 
        self.root = ie
919
852
        self._byid = {self.root.file_id: self.root}
920
853
 
 
854
 
921
855
    def copy(self):
922
856
        # TODO: jam 20051218 Should copy also copy the revision_id?
923
 
        entries = self.iter_entries()
924
 
        other = Inventory(entries.next()[1].file_id)
 
857
        other = Inventory(self.root.file_id)
925
858
        # copy recursively so we know directories will be added before
926
859
        # their children.  There are more efficient ways than this...
927
 
        for path, entry in entries():
 
860
        for path, entry in self.iter_entries():
 
861
            if entry == self.root:
 
862
                continue
928
863
            other.add(entry.copy())
929
864
        return other
930
865
 
 
866
 
931
867
    def __iter__(self):
932
868
        return iter(self._byid)
933
869
 
 
870
 
934
871
    def __len__(self):
935
872
        """Returns number of entries."""
936
873
        return len(self._byid)
937
874
 
 
875
 
938
876
    def iter_entries(self, from_dir=None):
939
877
        """Return (path, entry) pairs, in order by name."""
940
 
        if from_dir is None:
941
 
            if self.root is None:
942
 
                return
 
878
        if from_dir == None:
 
879
            assert self.root
943
880
            from_dir = self.root
944
 
            yield '', self.root
945
881
        elif isinstance(from_dir, basestring):
946
882
            from_dir = self._byid[from_dir]
947
883
            
979
915
                # if we finished all children, pop it off the stack
980
916
                stack.pop()
981
917
 
982
 
    def iter_entries_by_dir(self, from_dir=None, specific_file_ids=None):
983
 
        """Iterate over the entries in a directory first order.
984
 
 
985
 
        This returns all entries for a directory before returning
986
 
        the entries for children of a directory. This is not
987
 
        lexicographically sorted order, and is a hybrid between
988
 
        depth-first and breadth-first.
989
 
 
990
 
        :return: This yields (path, entry) pairs
991
 
        """
992
 
        if specific_file_ids:
993
 
            safe = osutils.safe_file_id
994
 
            specific_file_ids = set(safe(fid) for fid in specific_file_ids)
995
 
        # TODO? Perhaps this should return the from_dir so that the root is
996
 
        # yielded? or maybe an option?
997
 
        if from_dir is None:
998
 
            if self.root is None:
999
 
                return
1000
 
            # Optimize a common case
1001
 
            if specific_file_ids is not None and len(specific_file_ids) == 1:
1002
 
                file_id = list(specific_file_ids)[0]
1003
 
                if file_id in self:
1004
 
                    yield self.id2path(file_id), self[file_id]
1005
 
                return 
1006
 
            from_dir = self.root
1007
 
            if (specific_file_ids is None or 
1008
 
                self.root.file_id in specific_file_ids):
1009
 
                yield u'', self.root
1010
 
        elif isinstance(from_dir, basestring):
1011
 
            from_dir = self._byid[from_dir]
1012
 
 
1013
 
        if specific_file_ids is not None:
1014
 
            # TODO: jam 20070302 This could really be done as a loop rather
1015
 
            #       than a bunch of recursive calls.
1016
 
            parents = set()
1017
 
            byid = self._byid
1018
 
            def add_ancestors(file_id):
1019
 
                if file_id not in byid:
1020
 
                    return
1021
 
                parent_id = byid[file_id].parent_id
1022
 
                if parent_id is None:
1023
 
                    return
1024
 
                if parent_id not in parents:
1025
 
                    parents.add(parent_id)
1026
 
                    add_ancestors(parent_id)
1027
 
            for file_id in specific_file_ids:
1028
 
                add_ancestors(file_id)
1029
 
        else:
1030
 
            parents = None
1031
 
            
1032
 
        stack = [(u'', from_dir)]
1033
 
        while stack:
1034
 
            cur_relpath, cur_dir = stack.pop()
1035
 
 
1036
 
            child_dirs = []
1037
 
            for child_name, child_ie in sorted(cur_dir.children.iteritems()):
1038
 
 
1039
 
                child_relpath = cur_relpath + child_name
1040
 
 
1041
 
                if (specific_file_ids is None or 
1042
 
                    child_ie.file_id in specific_file_ids):
1043
 
                    yield child_relpath, child_ie
1044
 
 
1045
 
                if child_ie.kind == 'directory':
1046
 
                    if parents is None or child_ie.file_id in parents:
1047
 
                        child_dirs.append((child_relpath+'/', child_ie))
1048
 
            stack.extend(reversed(child_dirs))
1049
 
 
1050
 
    def make_entry(self, kind, name, parent_id, file_id=None):
1051
 
        """Simple thunk to bzrlib.inventory.make_entry."""
1052
 
        return make_entry(kind, name, parent_id, file_id)
1053
 
 
1054
918
    def entries(self):
1055
919
        """Return list of (path, ie) for all entries except the root.
1056
920
 
1061
925
            kids = dir_ie.children.items()
1062
926
            kids.sort()
1063
927
            for name, ie in kids:
1064
 
                child_path = osutils.pathjoin(dir_path, name)
 
928
                child_path = pathjoin(dir_path, name)
1065
929
                accum.append((child_path, ie))
1066
930
                if ie.kind == 'directory':
1067
931
                    descend(ie, child_path)
1069
933
        descend(self.root, u'')
1070
934
        return accum
1071
935
 
 
936
 
1072
937
    def directories(self):
1073
938
        """Return (path, entry) pairs for all directories, including the root.
1074
939
        """
1080
945
            kids.sort()
1081
946
 
1082
947
            for name, child_ie in kids:
1083
 
                child_path = osutils.pathjoin(parent_path, name)
 
948
                child_path = pathjoin(parent_path, name)
1084
949
                descend(child_ie, child_path)
1085
950
        descend(self.root, u'')
1086
951
        return accum
1087
952
        
 
953
 
 
954
 
1088
955
    def __contains__(self, file_id):
1089
956
        """True if this entry contains a file with given id.
1090
957
 
1091
958
        >>> inv = Inventory()
1092
959
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1093
 
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
 
960
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1094
961
        >>> '123' in inv
1095
962
        True
1096
963
        >>> '456' in inv
1097
964
        False
1098
965
        """
1099
 
        file_id = osutils.safe_file_id(file_id)
1100
 
        return (file_id in self._byid)
 
966
        return file_id in self._byid
 
967
 
1101
968
 
1102
969
    def __getitem__(self, file_id):
1103
970
        """Return the entry for given file_id.
1104
971
 
1105
972
        >>> inv = Inventory()
1106
973
        >>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
1107
 
        InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
 
974
        InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT')
1108
975
        >>> inv['123123'].name
1109
976
        'hello.c'
1110
977
        """
1111
 
        file_id = osutils.safe_file_id(file_id)
1112
978
        try:
1113
979
            return self._byid[file_id]
1114
980
        except KeyError:
1115
 
            # really we're passing an inventory, not a tree...
1116
 
            raise errors.NoSuchId(self, file_id)
 
981
            if file_id == None:
 
982
                raise BzrError("can't look up file_id None")
 
983
            else:
 
984
                raise BzrError("file_id {%s} not in inventory" % file_id)
 
985
 
1117
986
 
1118
987
    def get_file_kind(self, file_id):
1119
 
        file_id = osutils.safe_file_id(file_id)
1120
988
        return self._byid[file_id].kind
1121
989
 
1122
990
    def get_child(self, parent_id, filename):
1123
 
        parent_id = osutils.safe_file_id(parent_id)
1124
991
        return self[parent_id].children.get(filename)
1125
992
 
1126
 
    def _add_child(self, entry):
1127
 
        """Add an entry to the inventory, without adding it to its parent"""
1128
 
        if entry.file_id in self._byid:
1129
 
            raise BzrError("inventory already contains entry with id {%s}" %
1130
 
                           entry.file_id)
1131
 
        self._byid[entry.file_id] = entry
1132
 
        for child in getattr(entry, 'children', {}).itervalues():
1133
 
            self._add_child(child)
1134
 
        return entry
1135
993
 
1136
994
    def add(self, entry):
1137
995
        """Add entry to inventory.
1142
1000
        Returns the new entry object.
1143
1001
        """
1144
1002
        if entry.file_id in self._byid:
1145
 
            raise errors.DuplicateFileId(entry.file_id,
1146
 
                                         self._byid[entry.file_id])
1147
 
 
1148
 
        if entry.parent_id is None:
1149
 
            assert self.root is None and len(self._byid) == 0
1150
 
            self.root = entry
1151
 
        else:
1152
 
            try:
1153
 
                parent = self._byid[entry.parent_id]
1154
 
            except KeyError:
1155
 
                raise BzrError("parent_id {%s} not in inventory" %
1156
 
                               entry.parent_id)
1157
 
 
1158
 
            if entry.name in parent.children:
1159
 
                raise BzrError("%s is already versioned" %
1160
 
                        osutils.pathjoin(self.id2path(parent.file_id),
1161
 
                        entry.name).encode('utf-8'))
1162
 
            parent.children[entry.name] = entry
1163
 
        return self._add_child(entry)
 
1003
            raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
 
1004
 
 
1005
        if entry.parent_id == ROOT_ID or entry.parent_id is None:
 
1006
            entry.parent_id = self.root.file_id
 
1007
 
 
1008
        try:
 
1009
            parent = self._byid[entry.parent_id]
 
1010
        except KeyError:
 
1011
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
 
1012
 
 
1013
        if parent.children.has_key(entry.name):
 
1014
            raise BzrError("%s is already versioned" %
 
1015
                    pathjoin(self.id2path(parent.file_id), entry.name))
 
1016
 
 
1017
        self._byid[entry.file_id] = entry
 
1018
        parent.children[entry.name] = entry
 
1019
        return entry
 
1020
 
1164
1021
 
1165
1022
    def add_path(self, relpath, kind, file_id=None, parent_id=None):
1166
1023
        """Add entry from a path.
1169
1026
 
1170
1027
        Returns the new entry object."""
1171
1028
        
1172
 
        parts = osutils.splitpath(relpath)
 
1029
        parts = bzrlib.osutils.splitpath(relpath)
1173
1030
 
1174
1031
        if len(parts) == 0:
1175
1032
            if file_id is None:
1176
 
                file_id = generate_ids.gen_root_id()
1177
 
            else:
1178
 
                file_id = osutils.safe_file_id(file_id)
1179
 
            self.root = InventoryDirectory(file_id, '', None)
 
1033
                file_id = bzrlib.workingtree.gen_root_id()
 
1034
            self.root = RootEntry(file_id)
1180
1035
            self._byid = {self.root.file_id: self.root}
1181
 
            return self.root
 
1036
            return
1182
1037
        else:
1183
1038
            parent_path = parts[:-1]
1184
1039
            parent_id = self.path2id(parent_path)
1185
 
            if parent_id is None:
1186
 
                raise errors.NotVersionedError(path=parent_path)
 
1040
            if parent_id == None:
 
1041
                raise NotVersionedError(path=parent_path)
1187
1042
        ie = make_entry(kind, parts[-1], parent_id, file_id)
1188
1043
        return self.add(ie)
1189
1044
 
1192
1047
 
1193
1048
        >>> inv = Inventory()
1194
1049
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1195
 
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
 
1050
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1196
1051
        >>> '123' in inv
1197
1052
        True
1198
1053
        >>> del inv['123']
1199
1054
        >>> '123' in inv
1200
1055
        False
1201
1056
        """
1202
 
        file_id = osutils.safe_file_id(file_id)
1203
1057
        ie = self[file_id]
1204
1058
 
1205
1059
        assert ie.parent_id is None or \
1209
1063
        if ie.parent_id is not None:
1210
1064
            del self[ie.parent_id].children[ie.name]
1211
1065
 
 
1066
 
1212
1067
    def __eq__(self, other):
1213
1068
        """Compare two sets by comparing their contents.
1214
1069
 
1217
1072
        >>> i1 == i2
1218
1073
        True
1219
1074
        >>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
1220
 
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
 
1075
        InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1221
1076
        >>> i1 == i2
1222
1077
        False
1223
1078
        >>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
1224
 
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
 
1079
        InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1225
1080
        >>> i1 == i2
1226
1081
        True
1227
1082
        """
1228
1083
        if not isinstance(other, Inventory):
1229
1084
            return NotImplemented
1230
1085
 
 
1086
        if len(self._byid) != len(other._byid):
 
1087
            # shortcut: obviously not the same
 
1088
            return False
 
1089
 
1231
1090
        return self._byid == other._byid
1232
1091
 
 
1092
 
1233
1093
    def __ne__(self, other):
1234
1094
        return not self.__eq__(other)
1235
1095
 
 
1096
 
1236
1097
    def __hash__(self):
1237
1098
        raise ValueError('not hashable')
1238
1099
 
1239
1100
    def _iter_file_id_parents(self, file_id):
1240
1101
        """Yield the parents of file_id up to the root."""
1241
 
        file_id = osutils.safe_file_id(file_id)
1242
 
        while file_id is not None:
 
1102
        while file_id != None:
1243
1103
            try:
1244
1104
                ie = self._byid[file_id]
1245
1105
            except KeyError:
1246
 
                raise errors.NoSuchId(tree=None, file_id=file_id)
 
1106
                raise BzrError("file_id {%s} not found in inventory" % file_id)
1247
1107
            yield ie
1248
1108
            file_id = ie.parent_id
1249
1109
 
1255
1115
        is equal to the depth of the file in the tree, counting the
1256
1116
        root directory as depth 1.
1257
1117
        """
1258
 
        file_id = osutils.safe_file_id(file_id)
1259
1118
        p = []
1260
1119
        for parent in self._iter_file_id_parents(file_id):
1261
1120
            p.insert(0, parent.file_id)
1270
1129
        >>> print i.id2path('foo-id')
1271
1130
        src/foo.c
1272
1131
        """
1273
 
        file_id = osutils.safe_file_id(file_id)
1274
1132
        # get all names, skipping root
1275
1133
        return '/'.join(reversed(
1276
1134
            [parent.name for parent in 
1287
1145
 
1288
1146
        Returns None IFF the path is not found.
1289
1147
        """
1290
 
        if isinstance(name, basestring):
1291
 
            name = osutils.splitpath(name)
 
1148
        if isinstance(name, types.StringTypes):
 
1149
            name = splitpath(name)
1292
1150
 
1293
1151
        # mutter("lookup path %r" % name)
1294
1152
 
1295
1153
        parent = self.root
1296
 
        if parent is None:
1297
 
            return None
1298
1154
        for f in name:
1299
1155
            try:
1300
 
                children = getattr(parent, 'children', None)
1301
 
                if children is None:
1302
 
                    return None
1303
 
                cie = children[f]
 
1156
                cie = parent.children[f]
1304
1157
                assert cie.name == f
1305
1158
                assert cie.parent_id == parent.file_id
1306
1159
                parent = cie
1310
1163
 
1311
1164
        return parent.file_id
1312
1165
 
 
1166
 
1313
1167
    def has_filename(self, names):
1314
1168
        return bool(self.path2id(names))
1315
1169
 
 
1170
 
1316
1171
    def has_id(self, file_id):
1317
 
        file_id = osutils.safe_file_id(file_id)
1318
 
        return (file_id in self._byid)
 
1172
        return self._byid.has_key(file_id)
1319
1173
 
1320
 
    def remove_recursive_id(self, file_id):
1321
 
        """Remove file_id, and children, from the inventory.
1322
 
        
1323
 
        :param file_id: A file_id to remove.
1324
 
        """
1325
 
        file_id = osutils.safe_file_id(file_id)
1326
 
        to_find_delete = [self._byid[file_id]]
1327
 
        to_delete = []
1328
 
        while to_find_delete:
1329
 
            ie = to_find_delete.pop()
1330
 
            to_delete.append(ie.file_id)
1331
 
            if ie.kind == 'directory':
1332
 
                to_find_delete.extend(ie.children.values())
1333
 
        for file_id in reversed(to_delete):
1334
 
            ie = self[file_id]
1335
 
            del self._byid[file_id]
1336
 
        if ie.parent_id is not None:
1337
 
            del self[ie.parent_id].children[ie.name]
1338
 
        else:
1339
 
            self.root = None
1340
1174
 
1341
1175
    def rename(self, file_id, new_parent_id, new_name):
1342
1176
        """Move a file within the inventory.
1343
1177
 
1344
1178
        This can change either the name, or the parent, or both.
1345
1179
 
1346
 
        This does not move the working file.
1347
 
        """
1348
 
        file_id = osutils.safe_file_id(file_id)
 
1180
        This does not move the working file."""
1349
1181
        if not is_valid_name(new_name):
1350
1182
            raise BzrError("not an acceptable filename: %r" % new_name)
1351
1183
 
1369
1201
        file_ie.name = new_name
1370
1202
        file_ie.parent_id = new_parent_id
1371
1203
 
1372
 
    def is_root(self, file_id):
1373
 
        file_id = osutils.safe_file_id(file_id)
1374
 
        return self.root is not None and file_id == self.root.file_id
1375
 
 
1376
 
 
1377
 
entry_factory = {
1378
 
    'directory': InventoryDirectory,
1379
 
    'file': InventoryFile,
1380
 
    'symlink': InventoryLink,
1381
 
    'tree-reference': TreeReference
1382
 
}
1383
1204
 
1384
1205
def make_entry(kind, name, parent_id, file_id=None):
1385
1206
    """Create an inventory entry.
1390
1211
    :param file_id: the file_id to use. if None, one will be created.
1391
1212
    """
1392
1213
    if file_id is None:
1393
 
        file_id = generate_ids.gen_file_id(name)
 
1214
        file_id = bzrlib.workingtree.gen_file_id(name)
 
1215
    if kind == 'directory':
 
1216
        return InventoryDirectory(file_id, name, parent_id)
 
1217
    elif kind == 'file':
 
1218
        return InventoryFile(file_id, name, parent_id)
 
1219
    elif kind == 'symlink':
 
1220
        return InventoryLink(file_id, name, parent_id)
1394
1221
    else:
1395
 
        file_id = osutils.safe_file_id(file_id)
1396
 
 
1397
 
    #------- This has been copied to bzrlib.dirstate.DirState.add, please
1398
 
    # keep them synchronised.
1399
 
    # we dont import normalized_filename directly because we want to be
1400
 
    # able to change the implementation at runtime for tests.
1401
 
    norm_name, can_access = osutils.normalized_filename(name)
1402
 
    if norm_name != name:
1403
 
        if can_access:
1404
 
            name = norm_name
1405
 
        else:
1406
 
            # TODO: jam 20060701 This would probably be more useful
1407
 
            #       if the error was raised with the full path
1408
 
            raise errors.InvalidNormalization(name)
1409
 
 
1410
 
    try:
1411
 
        factory = entry_factory[kind]
1412
 
    except KeyError:
1413
1222
        raise BzrError("unknown kind %r" % kind)
1414
 
    return factory(file_id, name, parent_id)
 
1223
 
1415
1224
 
1416
1225
 
1417
1226
_NAME_RE = None
1418
1227
 
1419
1228
def is_valid_name(name):
1420
1229
    global _NAME_RE
1421
 
    if _NAME_RE is None:
 
1230
    if _NAME_RE == None:
1422
1231
        _NAME_RE = re.compile(r'^[^/\\]+$')
1423
1232
        
1424
1233
    return bool(_NAME_RE.match(name))