~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Jamie Wilkinson
  • Date: 2006-07-18 23:59:52 UTC
  • mfrom: (1868 +trunk)
  • mto: This revision was merged to the branch mainline in revision 1874.
  • Revision ID: jaq@spacepants.org-20060718235952-1e362401a7858958
merge from bzr.dev

Show diffs side-by-side

added added

removed removed

Lines of Context:
28
28
ROOT_ID = "TREE_ROOT"
29
29
 
30
30
 
 
31
import collections
31
32
import os.path
32
33
import re
33
34
import sys
35
36
import types
36
37
 
37
38
import bzrlib
 
39
from bzrlib import errors, osutils
38
40
from bzrlib.osutils import (pumpfile, quotefn, splitpath, joinpath,
39
41
                            pathjoin, sha_strings)
40
42
from bzrlib.errors import (NotVersionedError, InvalidEntryName,
76
78
    >>> i.path2id('')
77
79
    'TREE_ROOT'
78
80
    >>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
79
 
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
 
81
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
80
82
    >>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
81
 
    InventoryFile('2323', 'hello.c', parent_id='123')
 
83
    InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
82
84
    >>> shouldbe = {0: 'src', 1: pathjoin('src','hello.c')}
83
85
    >>> for ix, j in enumerate(i.iter_entries()):
84
86
    ...   print (j[0] == shouldbe[ix], j[1])
85
87
    ... 
86
 
    (True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
87
 
    (True, InventoryFile('2323', 'hello.c', parent_id='123'))
 
88
    (True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
 
89
    (True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
88
90
    >>> i.add(InventoryFile('2323', 'bye.c', '123'))
89
91
    Traceback (most recent call last):
90
92
    ...
91
93
    BzrError: inventory already contains entry with id {2323}
92
94
    >>> i.add(InventoryFile('2324', 'bye.c', '123'))
93
 
    InventoryFile('2324', 'bye.c', parent_id='123')
 
95
    InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
94
96
    >>> i.add(InventoryDirectory('2325', 'wibble', '123'))
95
 
    InventoryDirectory('2325', 'wibble', parent_id='123')
 
97
    InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
96
98
    >>> i.path2id('src/wibble')
97
99
    '2325'
98
100
    >>> '2325' in i
99
101
    True
100
102
    >>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
101
 
    InventoryFile('2326', 'wibble.c', parent_id='2325')
 
103
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
102
104
    >>> i['2326']
103
 
    InventoryFile('2326', 'wibble.c', parent_id='2325')
 
105
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
104
106
    >>> for path, entry in i.iter_entries():
105
107
    ...     print path
106
108
    ...     assert i.path2id(path)
122
124
    RENAMED = 'renamed'
123
125
    MODIFIED_AND_RENAMED = 'modified and renamed'
124
126
    
125
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
126
 
                 'text_id', 'parent_id', 'children', 'executable', 
127
 
                 'revision']
128
 
 
129
 
    def _add_text_to_weave(self, new_lines, parents, weave_store, transaction):
130
 
        versionedfile = weave_store.get_weave_or_empty(self.file_id,
131
 
                                                       transaction)
132
 
        versionedfile.add_lines(self.revision, parents, new_lines)
133
 
        versionedfile.clear_cache()
 
127
    __slots__ = []
134
128
 
135
129
    def detect_changes(self, old_entry):
136
130
        """Return a (text_modified, meta_modified) from this to old_entry.
165
159
                            versioned_file_store,
166
160
                            transaction,
167
161
                            entry_vf=None):
168
 
        """Return the revisions and entries that directly preceed this.
 
162
        """Return the revisions and entries that directly precede this.
169
163
 
170
164
        Returned as a map from revision to inventory entry.
171
165
 
324
318
        raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
325
319
 
326
320
    def sorted_children(self):
327
 
        l = self.children.items()
328
 
        l.sort()
329
 
        return l
 
321
        return sorted(self.children.items())
330
322
 
331
323
    @staticmethod
332
324
    def versionable_kind(kind):
346
338
        :param inv: Inventory from which the entry was loaded.
347
339
        :param tree: RevisionTree for this entry.
348
340
        """
349
 
        if self.parent_id != None:
 
341
        if self.parent_id is not None:
350
342
            if not inv.has_id(self.parent_id):
351
343
                raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
352
344
                        % (self.parent_id, rev_id))
402
394
        return 'unchanged'
403
395
 
404
396
    def __repr__(self):
405
 
        return ("%s(%r, %r, parent_id=%r)"
 
397
        return ("%s(%r, %r, parent_id=%r, revision=%r)"
406
398
                % (self.__class__.__name__,
407
399
                   self.file_id,
408
400
                   self.name,
409
 
                   self.parent_id))
 
401
                   self.parent_id,
 
402
                   self.revision))
410
403
 
411
404
    def snapshot(self, revision, path, previous_entries,
412
 
                 work_tree, weave_store, transaction):
 
405
                 work_tree, commit_builder):
413
406
        """Make a snapshot of this entry which may or may not have changed.
414
407
        
415
408
        This means that all its fields are populated, that it has its
417
410
        """
418
411
        mutter('new parents of %s are %r', path, previous_entries)
419
412
        self._read_tree_state(path, work_tree)
 
413
        # TODO: Where should we determine whether to reuse a
 
414
        # previous revision id or create a new revision? 20060606
420
415
        if len(previous_entries) == 1:
421
416
            # cannot be unchanged unless there is only one parent file rev.
422
417
            parent_ie = previous_entries.values()[0]
425
420
                self.revision = parent_ie.revision
426
421
                return "unchanged"
427
422
        return self._snapshot_into_revision(revision, previous_entries, 
428
 
                                            work_tree, weave_store, transaction)
 
423
                                            work_tree, commit_builder)
429
424
 
430
425
    def _snapshot_into_revision(self, revision, previous_entries, work_tree,
431
 
                                weave_store, transaction):
 
426
                                commit_builder):
432
427
        """Record this revision unconditionally into a store.
433
428
 
434
429
        The entry's last-changed revision property (`revision`) is updated to 
440
435
        """
441
436
        mutter('new revision {%s} for {%s}', revision, self.file_id)
442
437
        self.revision = revision
443
 
        self._snapshot_text(previous_entries, work_tree, weave_store,
444
 
                            transaction)
 
438
        self._snapshot_text(previous_entries, work_tree, commit_builder)
445
439
 
446
 
    def _snapshot_text(self, file_parents, work_tree, weave_store, transaction): 
 
440
    def _snapshot_text(self, file_parents, work_tree, commit_builder): 
447
441
        """Record the 'text' of this entry, whatever form that takes.
448
442
        
449
443
        This default implementation simply adds an empty text.
450
444
        """
451
 
        mutter('storing file {%s} in revision {%s}',
452
 
               self.file_id, self.revision)
453
 
        self._add_text_to_weave([], file_parents.keys(), weave_store, transaction)
 
445
        raise NotImplementedError(self._snapshot_text)
454
446
 
455
447
    def __eq__(self, other):
456
448
        if not isinstance(other, InventoryEntry):
477
469
    def _unchanged(self, previous_ie):
478
470
        """Has this entry changed relative to previous_ie.
479
471
 
480
 
        This method should be overriden in child classes.
 
472
        This method should be overridden in child classes.
481
473
        """
482
474
        compatible = True
483
475
        # different inv parent
505
497
 
506
498
class RootEntry(InventoryEntry):
507
499
 
 
500
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
501
                 'text_id', 'parent_id', 'children', 'executable', 
 
502
                 'revision', 'symlink_target']
 
503
 
508
504
    def _check(self, checker, rev_id, tree):
509
505
        """See InventoryEntry._check"""
510
506
 
514
510
        self.kind = 'root_directory'
515
511
        self.parent_id = None
516
512
        self.name = u''
 
513
        self.revision = None
517
514
 
518
515
    def __eq__(self, other):
519
516
        if not isinstance(other, RootEntry):
526
523
class InventoryDirectory(InventoryEntry):
527
524
    """A directory in an inventory."""
528
525
 
 
526
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
527
                 'text_id', 'parent_id', 'children', 'executable', 
 
528
                 'revision', 'symlink_target']
 
529
 
529
530
    def _check(self, checker, rev_id, tree):
530
531
        """See InventoryEntry._check"""
531
 
        if self.text_sha1 != None or self.text_size != None or self.text_id != None:
 
532
        if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
532
533
            raise BzrCheckError('directory {%s} has text in revision {%s}'
533
534
                                % (self.file_id, rev_id))
534
535
 
561
562
        """See InventoryEntry._put_on_disk."""
562
563
        os.mkdir(fullpath)
563
564
 
 
565
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
 
566
        """See InventoryEntry._snapshot_text."""
 
567
        commit_builder.modified_directory(self.file_id, file_parents)
 
568
 
564
569
 
565
570
class InventoryFile(InventoryEntry):
566
571
    """A file in an inventory."""
567
572
 
 
573
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
574
                 'text_id', 'parent_id', 'children', 'executable', 
 
575
                 'revision', 'symlink_target']
 
576
 
568
577
    def _check(self, checker, tree_revision_id, tree):
569
578
        """See InventoryEntry._check"""
570
579
        t = (self.file_id, self.revision)
609
618
 
610
619
    def detect_changes(self, old_entry):
611
620
        """See InventoryEntry.detect_changes."""
612
 
        assert self.text_sha1 != None
613
 
        assert old_entry.text_sha1 != None
 
621
        assert self.text_sha1 is not None
 
622
        assert old_entry.text_sha1 is not None
614
623
        text_modified = (self.text_sha1 != old_entry.text_sha1)
615
624
        meta_modified = (self.executable != old_entry.executable)
616
625
        return text_modified, meta_modified
668
677
 
669
678
    def _read_tree_state(self, path, work_tree):
670
679
        """See InventoryEntry._read_tree_state."""
671
 
        self.text_sha1 = work_tree.get_file_sha1(self.file_id)
672
 
        self.executable = work_tree.is_executable(self.file_id)
 
680
        self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
 
681
        # FIXME: 20050930 probe for the text size when getting sha1
 
682
        # in _read_tree_state
 
683
        self.executable = work_tree.is_executable(self.file_id, path=path)
 
684
 
 
685
    def __repr__(self):
 
686
        return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s)"
 
687
                % (self.__class__.__name__,
 
688
                   self.file_id,
 
689
                   self.name,
 
690
                   self.parent_id,
 
691
                   self.text_sha1,
 
692
                   self.text_size))
673
693
 
674
694
    def _forget_tree_state(self):
675
695
        self.text_sha1 = None
676
 
        self.executable = None
677
696
 
678
 
    def _snapshot_text(self, file_parents, work_tree, versionedfile_store, transaction):
 
697
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
679
698
        """See InventoryEntry._snapshot_text."""
680
 
        mutter('storing text of file {%s} in revision {%s} into %r',
681
 
               self.file_id, self.revision, versionedfile_store)
682
 
        # special case to avoid diffing on renames or 
683
 
        # reparenting
684
 
        if (len(file_parents) == 1
685
 
            and self.text_sha1 == file_parents.values()[0].text_sha1
686
 
            and self.text_size == file_parents.values()[0].text_size):
687
 
            previous_ie = file_parents.values()[0]
688
 
            versionedfile = versionedfile_store.get_weave(self.file_id, transaction)
689
 
            versionedfile.clone_text(self.revision, previous_ie.revision, file_parents.keys())
690
 
        else:
691
 
            new_lines = work_tree.get_file(self.file_id).readlines()
692
 
            self._add_text_to_weave(new_lines, file_parents.keys(), versionedfile_store,
693
 
                                    transaction)
694
 
            self.text_sha1 = sha_strings(new_lines)
695
 
            self.text_size = sum(map(len, new_lines))
696
 
 
 
699
        def get_content_byte_lines():
 
700
            return work_tree.get_file(self.file_id).readlines()
 
701
        self.text_sha1, self.text_size = commit_builder.modified_file_text(
 
702
            self.file_id, file_parents, get_content_byte_lines, self.text_sha1, self.text_size)
697
703
 
698
704
    def _unchanged(self, previous_ie):
699
705
        """See InventoryEntry._unchanged."""
712
718
class InventoryLink(InventoryEntry):
713
719
    """A file in an inventory."""
714
720
 
715
 
    __slots__ = ['symlink_target']
 
721
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
722
                 'text_id', 'parent_id', 'children', 'executable', 
 
723
                 'revision', 'symlink_target']
716
724
 
717
725
    def _check(self, checker, rev_id, tree):
718
726
        """See InventoryEntry._check"""
719
 
        if self.text_sha1 != None or self.text_size != None or self.text_id != None:
 
727
        if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
720
728
            raise BzrCheckError('symlink {%s} has text in revision {%s}'
721
729
                    % (self.file_id, rev_id))
722
 
        if self.symlink_target == None:
 
730
        if self.symlink_target is None:
723
731
            raise BzrCheckError('symlink {%s} has no target in revision {%s}'
724
732
                    % (self.file_id, rev_id))
725
733
 
793
801
            compatible = False
794
802
        return compatible
795
803
 
 
804
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
 
805
        """See InventoryEntry._snapshot_text."""
 
806
        commit_builder.modified_link(
 
807
            self.file_id, file_parents, self.symlink_target)
 
808
 
796
809
 
797
810
class Inventory(object):
798
811
    """Inventory of versioned files in a tree.
813
826
 
814
827
    >>> inv = Inventory()
815
828
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
816
 
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT')
 
829
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
817
830
    >>> inv['123-123'].name
818
831
    'hello.c'
819
832
 
827
840
    May also look up by name:
828
841
 
829
842
    >>> [x[0] for x in inv.iter_entries()]
830
 
    ['hello.c']
 
843
    [u'hello.c']
831
844
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
832
845
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
833
 
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
 
846
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None)
834
847
    """
835
848
    def __init__(self, root_id=ROOT_ID, revision_id=None):
836
849
        """Create or read an inventory.
847
860
        #if root_id is None:
848
861
        #    root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
849
862
        self.root = RootEntry(root_id)
 
863
        # FIXME: this isn't ever used, changing it to self.revision may break
 
864
        # things. TODO make everything use self.revision_id
850
865
        self.revision_id = revision_id
851
866
        self._byid = {self.root.file_id: self.root}
852
867
 
853
 
 
854
868
    def copy(self):
855
869
        # TODO: jam 20051218 Should copy also copy the revision_id?
856
870
        other = Inventory(self.root.file_id)
862
876
            other.add(entry.copy())
863
877
        return other
864
878
 
865
 
 
866
879
    def __iter__(self):
867
880
        return iter(self._byid)
868
881
 
869
 
 
870
882
    def __len__(self):
871
883
        """Returns number of entries."""
872
884
        return len(self._byid)
873
885
 
874
 
 
875
886
    def iter_entries(self, from_dir=None):
876
887
        """Return (path, entry) pairs, in order by name."""
877
 
        if from_dir == None:
878
 
            assert self.root
879
 
            from_dir = self.root
880
 
        elif isinstance(from_dir, basestring):
881
 
            from_dir = self._byid[from_dir]
882
 
            
883
 
        kids = from_dir.children.items()
884
 
        kids.sort()
885
 
        for name, ie in kids:
886
 
            yield name, ie
887
 
            if ie.kind == 'directory':
888
 
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
889
 
                    yield pathjoin(name, cn), cie
890
 
 
 
888
        if from_dir is None:
 
889
            assert self.root
 
890
            from_dir = self.root
 
891
        elif isinstance(from_dir, basestring):
 
892
            from_dir = self._byid[from_dir]
 
893
            
 
894
        # unrolling the recursive called changed the time from
 
895
        # 440ms/663ms (inline/total) to 116ms/116ms
 
896
        children = from_dir.children.items()
 
897
        children.sort()
 
898
        children = collections.deque(children)
 
899
        stack = [(u'', children)]
 
900
        while stack:
 
901
            from_dir_relpath, children = stack[-1]
 
902
 
 
903
            while children:
 
904
                name, ie = children.popleft()
 
905
 
 
906
                # we know that from_dir_relpath never ends in a slash
 
907
                # and 'f' doesn't begin with one, we can do a string op, rather
 
908
                # than the checks of pathjoin(), though this means that all paths
 
909
                # start with a slash
 
910
                path = from_dir_relpath + '/' + name
 
911
 
 
912
                yield path[1:], ie
 
913
 
 
914
                if ie.kind != 'directory':
 
915
                    continue
 
916
 
 
917
                # But do this child first
 
918
                new_children = ie.children.items()
 
919
                new_children.sort()
 
920
                new_children = collections.deque(new_children)
 
921
                stack.append((path, new_children))
 
922
                # Break out of inner loop, so that we start outer loop with child
 
923
                break
 
924
            else:
 
925
                # if we finished all children, pop it off the stack
 
926
                stack.pop()
 
927
 
 
928
    def iter_entries_by_dir(self, from_dir=None):
 
929
        """Iterate over the entries in a directory first order.
 
930
 
 
931
        This returns all entries for a directory before returning
 
932
        the entries for children of a directory. This is not
 
933
        lexicographically sorted order, and is a hybrid between
 
934
        depth-first and breadth-first.
 
935
 
 
936
        :return: This yields (path, entry) pairs
 
937
        """
 
938
        # TODO? Perhaps this should return the from_dir so that the root is
 
939
        # yielded? or maybe an option?
 
940
        if from_dir is None:
 
941
            assert self.root
 
942
            from_dir = self.root
 
943
        elif isinstance(from_dir, basestring):
 
944
            from_dir = self._byid[from_dir]
 
945
            
 
946
        stack = [(u'', from_dir)]
 
947
        while stack:
 
948
            cur_relpath, cur_dir = stack.pop()
 
949
 
 
950
            child_dirs = []
 
951
            for child_name, child_ie in sorted(cur_dir.children.iteritems()):
 
952
 
 
953
                child_relpath = cur_relpath + child_name
 
954
 
 
955
                yield child_relpath, child_ie
 
956
 
 
957
                if child_ie.kind == 'directory':
 
958
                    child_dirs.append((child_relpath+'/', child_ie))
 
959
            stack.extend(reversed(child_dirs))
891
960
 
892
961
    def entries(self):
893
962
        """Return list of (path, ie) for all entries except the root.
907
976
        descend(self.root, u'')
908
977
        return accum
909
978
 
910
 
 
911
979
    def directories(self):
912
980
        """Return (path, entry) pairs for all directories, including the root.
913
981
        """
924
992
        descend(self.root, u'')
925
993
        return accum
926
994
        
927
 
 
928
 
 
929
995
    def __contains__(self, file_id):
930
996
        """True if this entry contains a file with given id.
931
997
 
932
998
        >>> inv = Inventory()
933
999
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
934
 
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
 
1000
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
935
1001
        >>> '123' in inv
936
1002
        True
937
1003
        >>> '456' in inv
939
1005
        """
940
1006
        return file_id in self._byid
941
1007
 
942
 
 
943
1008
    def __getitem__(self, file_id):
944
1009
        """Return the entry for given file_id.
945
1010
 
946
1011
        >>> inv = Inventory()
947
1012
        >>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
948
 
        InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT')
 
1013
        InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
949
1014
        >>> inv['123123'].name
950
1015
        'hello.c'
951
1016
        """
952
1017
        try:
953
1018
            return self._byid[file_id]
954
1019
        except KeyError:
955
 
            if file_id == None:
 
1020
            if file_id is None:
956
1021
                raise BzrError("can't look up file_id None")
957
1022
            else:
958
1023
                raise BzrError("file_id {%s} not in inventory" % file_id)
959
1024
 
960
 
 
961
1025
    def get_file_kind(self, file_id):
962
1026
        return self._byid[file_id].kind
963
1027
 
964
1028
    def get_child(self, parent_id, filename):
965
1029
        return self[parent_id].children.get(filename)
966
1030
 
967
 
 
968
1031
    def add(self, entry):
969
1032
        """Add entry to inventory.
970
1033
 
984
1047
        except KeyError:
985
1048
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
986
1049
 
987
 
        if parent.children.has_key(entry.name):
 
1050
        if entry.name in parent.children:
988
1051
            raise BzrError("%s is already versioned" %
989
1052
                    pathjoin(self.id2path(parent.file_id), entry.name))
990
1053
 
992
1055
        parent.children[entry.name] = entry
993
1056
        return entry
994
1057
 
995
 
 
996
 
    def add_path(self, relpath, kind, file_id=None):
 
1058
    def add_path(self, relpath, kind, file_id=None, parent_id=None):
997
1059
        """Add entry from a path.
998
1060
 
999
1061
        The immediate parent must already be versioned.
1000
1062
 
1001
1063
        Returns the new entry object."""
1002
1064
        
1003
 
        parts = bzrlib.osutils.splitpath(relpath)
 
1065
        parts = osutils.splitpath(relpath)
1004
1066
 
1005
1067
        if len(parts) == 0:
1006
1068
            if file_id is None:
1009
1071
            self._byid = {self.root.file_id: self.root}
1010
1072
            return
1011
1073
        else:
1012
 
            if file_id is None:
1013
 
                file_id = bzrlib.workingtree.gen_file_id(parts[-1])
1014
1074
            parent_path = parts[:-1]
1015
1075
            parent_id = self.path2id(parent_path)
1016
 
            if parent_id == None:
 
1076
            if parent_id is None:
1017
1077
                raise NotVersionedError(path=parent_path)
1018
 
        if kind == 'directory':
1019
 
            ie = InventoryDirectory(file_id, parts[-1], parent_id)
1020
 
        elif kind == 'file':
1021
 
            ie = InventoryFile(file_id, parts[-1], parent_id)
1022
 
        elif kind == 'symlink':
1023
 
            ie = InventoryLink(file_id, parts[-1], parent_id)
1024
 
        else:
1025
 
            raise BzrError("unknown kind %r" % kind)
 
1078
        ie = make_entry(kind, parts[-1], parent_id, file_id)
1026
1079
        return self.add(ie)
1027
1080
 
1028
 
 
1029
1081
    def __delitem__(self, file_id):
1030
1082
        """Remove entry by id.
1031
1083
 
1032
1084
        >>> inv = Inventory()
1033
1085
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1034
 
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
 
1086
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
1035
1087
        >>> '123' in inv
1036
1088
        True
1037
1089
        >>> del inv['123']
1047
1099
        if ie.parent_id is not None:
1048
1100
            del self[ie.parent_id].children[ie.name]
1049
1101
 
1050
 
 
1051
1102
    def __eq__(self, other):
1052
1103
        """Compare two sets by comparing their contents.
1053
1104
 
1056
1107
        >>> i1 == i2
1057
1108
        True
1058
1109
        >>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
1059
 
        InventoryFile('123', 'foo', parent_id='TREE_ROOT')
 
1110
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
1060
1111
        >>> i1 == i2
1061
1112
        False
1062
1113
        >>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
1063
 
        InventoryFile('123', 'foo', parent_id='TREE_ROOT')
 
1114
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
1064
1115
        >>> i1 == i2
1065
1116
        True
1066
1117
        """
1067
1118
        if not isinstance(other, Inventory):
1068
1119
            return NotImplemented
1069
1120
 
1070
 
        if len(self._byid) != len(other._byid):
1071
 
            # shortcut: obviously not the same
1072
 
            return False
1073
 
 
1074
1121
        return self._byid == other._byid
1075
1122
 
1076
 
 
1077
1123
    def __ne__(self, other):
1078
1124
        return not self.__eq__(other)
1079
1125
 
1080
 
 
1081
1126
    def __hash__(self):
1082
1127
        raise ValueError('not hashable')
1083
1128
 
1084
1129
    def _iter_file_id_parents(self, file_id):
1085
1130
        """Yield the parents of file_id up to the root."""
1086
 
        while file_id != None:
 
1131
        while file_id is not None:
1087
1132
            try:
1088
1133
                ie = self._byid[file_id]
1089
1134
            except KeyError:
1127
1172
        This returns the entry of the last component in the path,
1128
1173
        which may be either a file or a directory.
1129
1174
 
1130
 
        Returns None iff the path is not found.
 
1175
        Returns None IFF the path is not found.
1131
1176
        """
1132
1177
        if isinstance(name, types.StringTypes):
1133
1178
            name = splitpath(name)
1134
1179
 
1135
 
        mutter("lookup path %r" % name)
 
1180
        # mutter("lookup path %r" % name)
1136
1181
 
1137
1182
        parent = self.root
1138
1183
        for f in name:
1147
1192
 
1148
1193
        return parent.file_id
1149
1194
 
1150
 
 
1151
1195
    def has_filename(self, names):
1152
1196
        return bool(self.path2id(names))
1153
1197
 
1154
 
 
1155
1198
    def has_id(self, file_id):
1156
1199
        return self._byid.has_key(file_id)
1157
1200
 
1158
 
 
1159
1201
    def rename(self, file_id, new_parent_id, new_name):
1160
1202
        """Move a file within the inventory.
1161
1203
 
1186
1228
        file_ie.parent_id = new_parent_id
1187
1229
 
1188
1230
 
 
1231
def make_entry(kind, name, parent_id, file_id=None):
 
1232
    """Create an inventory entry.
 
1233
 
 
1234
    :param kind: the type of inventory entry to create.
 
1235
    :param name: the basename of the entry.
 
1236
    :param parent_id: the parent_id of the entry.
 
1237
    :param file_id: the file_id to use. if None, one will be created.
 
1238
    """
 
1239
    if file_id is None:
 
1240
        file_id = bzrlib.workingtree.gen_file_id(name)
 
1241
 
 
1242
    norm_name, can_access = osutils.normalized_filename(name)
 
1243
    if norm_name != name:
 
1244
        if can_access:
 
1245
            name = norm_name
 
1246
        else:
 
1247
            # TODO: jam 20060701 This would probably be more useful
 
1248
            #       if the error was raised with the full path
 
1249
            raise errors.InvalidNormalization(name)
 
1250
 
 
1251
    if kind == 'directory':
 
1252
        return InventoryDirectory(file_id, name, parent_id)
 
1253
    elif kind == 'file':
 
1254
        return InventoryFile(file_id, name, parent_id)
 
1255
    elif kind == 'symlink':
 
1256
        return InventoryLink(file_id, name, parent_id)
 
1257
    else:
 
1258
        raise BzrError("unknown kind %r" % kind)
1189
1259
 
1190
1260
 
1191
1261
_NAME_RE = None
1192
1262
 
1193
1263
def is_valid_name(name):
1194
1264
    global _NAME_RE
1195
 
    if _NAME_RE == None:
 
1265
    if _NAME_RE is None:
1196
1266
        _NAME_RE = re.compile(r'^[^/\\]+$')
1197
1267
        
1198
1268
    return bool(_NAME_RE.match(name))