~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-03-06 06:48:25 UTC
  • mfrom: (4070.8.6 debug-config)
  • Revision ID: pqm@pqm.ubuntu.com-20090306064825-kbpwggw21dygeix6
(mbp) debug_flags configuration option

Show diffs side-by-side

added added

removed removed

Lines of Context:
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
17
# FIXME: This refactoring of the workingtree code doesn't seem to keep
18
18
# the WorkingTree's copy of the inventory in sync with the branch.  The
27
27
# created, but it's not for now.
28
28
ROOT_ID = "TREE_ROOT"
29
29
 
30
 
from copy import deepcopy
 
30
import os
 
31
import re
 
32
import sys
31
33
 
32
34
from bzrlib.lazy_import import lazy_import
33
35
lazy_import(globals(), """
34
36
import collections
35
 
import os
36
 
import re
37
37
import tarfile
38
38
 
39
39
import bzrlib
40
40
from bzrlib import (
41
 
    chk_map,
42
41
    errors,
43
42
    generate_ids,
44
43
    osutils,
91
90
    >>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
92
91
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
93
92
    >>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
94
 
    InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None, revision=None)
 
93
    InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
95
94
    >>> shouldbe = {0: '', 1: 'src', 2: 'src/hello.c'}
96
95
    >>> for ix, j in enumerate(i.iter_entries()):
97
96
    ...   print (j[0] == shouldbe[ix], j[1])
98
97
    ...
99
98
    (True, InventoryDirectory('TREE_ROOT', u'', parent_id=None, revision=None))
100
99
    (True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
101
 
    (True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None, revision=None))
 
100
    (True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
102
101
    >>> i.add(InventoryFile('2324', 'bye.c', '123'))
103
 
    InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None, revision=None)
 
102
    InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
104
103
    >>> i.add(InventoryDirectory('2325', 'wibble', '123'))
105
104
    InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
106
105
    >>> i.path2id('src/wibble')
108
107
    >>> '2325' in i
109
108
    True
110
109
    >>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
111
 
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None, revision=None)
 
110
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
112
111
    >>> i['2326']
113
 
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None, revision=None)
 
112
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
114
113
    >>> for path, entry in i.iter_entries():
115
114
    ...     print path
116
115
    ...
490
489
                checker.repeated_text_cnt += 1
491
490
                return
492
491
 
 
492
        mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
493
493
        checker.checked_text_cnt += 1
494
494
        # We can't check the length, because Weave doesn't store that
495
495
        # information, and the whole point of looking at the weave's
567
567
        self.executable = work_tree.is_executable(self.file_id, path=path)
568
568
 
569
569
    def __repr__(self):
570
 
        return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s, revision=%s)"
 
570
        return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s)"
571
571
                % (self.__class__.__name__,
572
572
                   self.file_id,
573
573
                   self.name,
574
574
                   self.parent_id,
575
575
                   self.text_sha1,
576
 
                   self.text_size,
577
 
                   self.revision))
 
576
                   self.text_size))
578
577
 
579
578
    def _forget_tree_state(self):
580
579
        self.text_sha1 = None
713
712
        return compatible
714
713
 
715
714
 
716
 
class CommonInventory(object):
717
 
    """Basic inventory logic, defined in terms of primitives like has_id."""
718
 
 
719
 
    def __contains__(self, file_id):
720
 
        """True if this entry contains a file with given id.
721
 
 
722
 
        >>> inv = Inventory()
723
 
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
724
 
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
725
 
        >>> '123' in inv
726
 
        True
727
 
        >>> '456' in inv
728
 
        False
729
 
 
730
 
        Note that this method along with __iter__ are not encouraged for use as
731
 
        they are less clear than specific query methods - they may be rmeoved
732
 
        in the future.
733
 
        """
734
 
        return self.has_id(file_id)
735
 
 
736
 
    def id2path(self, file_id):
737
 
        """Return as a string the path to file_id.
738
 
 
739
 
        >>> i = Inventory()
740
 
        >>> e = i.add(InventoryDirectory('src-id', 'src', ROOT_ID))
741
 
        >>> e = i.add(InventoryFile('foo-id', 'foo.c', parent_id='src-id'))
742
 
        >>> print i.id2path('foo-id')
743
 
        src/foo.c
744
 
        """
745
 
        # get all names, skipping root
746
 
        return '/'.join(reversed(
747
 
            [parent.name for parent in
748
 
             self._iter_file_id_parents(file_id)][:-1]))
749
 
 
750
 
    def iter_entries(self, from_dir=None):
751
 
        """Return (path, entry) pairs, in order by name."""
752
 
        if from_dir is None:
753
 
            if self.root is None:
754
 
                return
755
 
            from_dir = self.root
756
 
            yield '', self.root
757
 
        elif isinstance(from_dir, basestring):
758
 
            from_dir = self[from_dir]
759
 
 
760
 
        # unrolling the recursive called changed the time from
761
 
        # 440ms/663ms (inline/total) to 116ms/116ms
762
 
        children = from_dir.children.items()
763
 
        children.sort()
764
 
        children = collections.deque(children)
765
 
        stack = [(u'', children)]
766
 
        while stack:
767
 
            from_dir_relpath, children = stack[-1]
768
 
 
769
 
            while children:
770
 
                name, ie = children.popleft()
771
 
 
772
 
                # we know that from_dir_relpath never ends in a slash
773
 
                # and 'f' doesn't begin with one, we can do a string op, rather
774
 
                # than the checks of pathjoin(), though this means that all paths
775
 
                # start with a slash
776
 
                path = from_dir_relpath + '/' + name
777
 
 
778
 
                yield path[1:], ie
779
 
 
780
 
                if ie.kind != 'directory':
781
 
                    continue
782
 
 
783
 
                # But do this child first
784
 
                new_children = ie.children.items()
785
 
                new_children.sort()
786
 
                new_children = collections.deque(new_children)
787
 
                stack.append((path, new_children))
788
 
                # Break out of inner loop, so that we start outer loop with child
789
 
                break
790
 
            else:
791
 
                # if we finished all children, pop it off the stack
792
 
                stack.pop()
793
 
 
794
 
    def iter_entries_by_dir(self, from_dir=None, specific_file_ids=None,
795
 
        yield_parents=False):
796
 
        """Iterate over the entries in a directory first order.
797
 
 
798
 
        This returns all entries for a directory before returning
799
 
        the entries for children of a directory. This is not
800
 
        lexicographically sorted order, and is a hybrid between
801
 
        depth-first and breadth-first.
802
 
 
803
 
        :param yield_parents: If True, yield the parents from the root leading
804
 
            down to specific_file_ids that have been requested. This has no
805
 
            impact if specific_file_ids is None.
806
 
        :return: This yields (path, entry) pairs
807
 
        """
808
 
        if specific_file_ids and not isinstance(specific_file_ids, set):
809
 
            specific_file_ids = set(specific_file_ids)
810
 
        # TODO? Perhaps this should return the from_dir so that the root is
811
 
        # yielded? or maybe an option?
812
 
        if from_dir is None:
813
 
            if self.root is None:
814
 
                return
815
 
            # Optimize a common case
816
 
            if (not yield_parents and specific_file_ids is not None and
817
 
                len(specific_file_ids) == 1):
818
 
                file_id = list(specific_file_ids)[0]
819
 
                if file_id in self:
820
 
                    yield self.id2path(file_id), self[file_id]
821
 
                return
822
 
            from_dir = self.root
823
 
            if (specific_file_ids is None or yield_parents or
824
 
                self.root.file_id in specific_file_ids):
825
 
                yield u'', self.root
826
 
        elif isinstance(from_dir, basestring):
827
 
            from_dir = self[from_dir]
828
 
 
829
 
        if specific_file_ids is not None:
830
 
            # TODO: jam 20070302 This could really be done as a loop rather
831
 
            #       than a bunch of recursive calls.
832
 
            parents = set()
833
 
            byid = self
834
 
            def add_ancestors(file_id):
835
 
                if file_id not in byid:
836
 
                    return
837
 
                parent_id = byid[file_id].parent_id
838
 
                if parent_id is None:
839
 
                    return
840
 
                if parent_id not in parents:
841
 
                    parents.add(parent_id)
842
 
                    add_ancestors(parent_id)
843
 
            for file_id in specific_file_ids:
844
 
                add_ancestors(file_id)
845
 
        else:
846
 
            parents = None
847
 
 
848
 
        stack = [(u'', from_dir)]
849
 
        while stack:
850
 
            cur_relpath, cur_dir = stack.pop()
851
 
 
852
 
            child_dirs = []
853
 
            for child_name, child_ie in sorted(cur_dir.children.iteritems()):
854
 
 
855
 
                child_relpath = cur_relpath + child_name
856
 
 
857
 
                if (specific_file_ids is None or
858
 
                    child_ie.file_id in specific_file_ids or
859
 
                    (yield_parents and child_ie.file_id in parents)):
860
 
                    yield child_relpath, child_ie
861
 
 
862
 
                if child_ie.kind == 'directory':
863
 
                    if parents is None or child_ie.file_id in parents:
864
 
                        child_dirs.append((child_relpath+'/', child_ie))
865
 
            stack.extend(reversed(child_dirs))
866
 
 
867
 
    def _make_delta(self, old):
868
 
        """Make an inventory delta from two inventories."""
869
 
        old_ids = set(old)
870
 
        new_ids = set(self)
871
 
        adds = new_ids - old_ids
872
 
        deletes = old_ids - new_ids
873
 
        common = old_ids.intersection(new_ids)
874
 
        delta = []
875
 
        for file_id in deletes:
876
 
            delta.append((old.id2path(file_id), None, file_id, None))
877
 
        for file_id in adds:
878
 
            delta.append((None, self.id2path(file_id), file_id, self[file_id]))
879
 
        for file_id in common:
880
 
            if old[file_id] != self[file_id]:
881
 
                delta.append((old.id2path(file_id), self.id2path(file_id),
882
 
                    file_id, self[file_id]))
883
 
        return delta
884
 
 
885
 
    def _get_mutable_inventory(self):
886
 
        """Returns a mutable copy of the object.
887
 
 
888
 
        Some inventories are immutable, yet working trees, for example, needs
889
 
        to mutate exisiting inventories instead of creating a new one.
890
 
        """
891
 
        raise NotImplementedError(self._get_mutable_inventory)
892
 
 
893
 
    def make_entry(self, kind, name, parent_id, file_id=None):
894
 
        """Simple thunk to bzrlib.inventory.make_entry."""
895
 
        return make_entry(kind, name, parent_id, file_id)
896
 
 
897
 
    def entries(self):
898
 
        """Return list of (path, ie) for all entries except the root.
899
 
 
900
 
        This may be faster than iter_entries.
901
 
        """
902
 
        accum = []
903
 
        def descend(dir_ie, dir_path):
904
 
            kids = dir_ie.children.items()
905
 
            kids.sort()
906
 
            for name, ie in kids:
907
 
                child_path = osutils.pathjoin(dir_path, name)
908
 
                accum.append((child_path, ie))
909
 
                if ie.kind == 'directory':
910
 
                    descend(ie, child_path)
911
 
 
912
 
        descend(self.root, u'')
913
 
        return accum
914
 
 
915
 
    def directories(self):
916
 
        """Return (path, entry) pairs for all directories, including the root.
917
 
        """
918
 
        accum = []
919
 
        def descend(parent_ie, parent_path):
920
 
            accum.append((parent_path, parent_ie))
921
 
 
922
 
            kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
923
 
            kids.sort()
924
 
 
925
 
            for name, child_ie in kids:
926
 
                child_path = osutils.pathjoin(parent_path, name)
927
 
                descend(child_ie, child_path)
928
 
        descend(self.root, u'')
929
 
        return accum
930
 
 
931
 
    def path2id(self, name):
932
 
        """Walk down through directories to return entry of last component.
933
 
 
934
 
        names may be either a list of path components, or a single
935
 
        string, in which case it is automatically split.
936
 
 
937
 
        This returns the entry of the last component in the path,
938
 
        which may be either a file or a directory.
939
 
 
940
 
        Returns None IFF the path is not found.
941
 
        """
942
 
        if isinstance(name, basestring):
943
 
            name = osutils.splitpath(name)
944
 
 
945
 
        # mutter("lookup path %r" % name)
946
 
 
947
 
        try:
948
 
            parent = self.root
949
 
        except errors.NoSuchId:
950
 
            # root doesn't exist yet so nothing else can
951
 
            return None
952
 
        if parent is None:
953
 
            return None
954
 
        for f in name:
955
 
            try:
956
 
                children = getattr(parent, 'children', None)
957
 
                if children is None:
958
 
                    return None
959
 
                cie = children[f]
960
 
                parent = cie
961
 
            except KeyError:
962
 
                # or raise an error?
963
 
                return None
964
 
 
965
 
        return parent.file_id
966
 
 
967
 
    def filter(self, specific_fileids):
968
 
        """Get an inventory view filtered against a set of file-ids.
969
 
 
970
 
        Children of directories and parents are included.
971
 
 
972
 
        The result may or may not reference the underlying inventory
973
 
        so it should be treated as immutable.
974
 
        """
975
 
        interesting_parents = set()
976
 
        for fileid in specific_fileids:
977
 
            try:
978
 
                interesting_parents.update(self.get_idpath(fileid))
979
 
            except errors.NoSuchId:
980
 
                # This fileid is not in the inventory - that's ok
981
 
                pass
982
 
        entries = self.iter_entries()
983
 
        if self.root is None:
984
 
            return Inventory(root_id=None)
985
 
        other = Inventory(entries.next()[1].file_id)
986
 
        other.root.revision = self.root.revision
987
 
        other.revision_id = self.revision_id
988
 
        directories_to_expand = set()
989
 
        for path, entry in entries:
990
 
            file_id = entry.file_id
991
 
            if (file_id in specific_fileids
992
 
                or entry.parent_id in directories_to_expand):
993
 
                if entry.kind == 'directory':
994
 
                    directories_to_expand.add(file_id)
995
 
            elif file_id not in interesting_parents:
996
 
                continue
997
 
            other.add(entry.copy())
998
 
        return other
999
 
 
1000
 
    def get_idpath(self, file_id):
1001
 
        """Return a list of file_ids for the path to an entry.
1002
 
 
1003
 
        The list contains one element for each directory followed by
1004
 
        the id of the file itself.  So the length of the returned list
1005
 
        is equal to the depth of the file in the tree, counting the
1006
 
        root directory as depth 1.
1007
 
        """
1008
 
        p = []
1009
 
        for parent in self._iter_file_id_parents(file_id):
1010
 
            p.insert(0, parent.file_id)
1011
 
        return p
1012
 
 
1013
 
 
1014
 
class Inventory(CommonInventory):
 
715
class Inventory(object):
1015
716
    """Inventory of versioned files in a tree.
1016
717
 
1017
718
    This describes which file_id is present at each point in the tree,
1030
731
 
1031
732
    >>> inv = Inventory()
1032
733
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
1033
 
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
 
734
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
1034
735
    >>> inv['123-123'].name
1035
736
    'hello.c'
1036
737
 
1050
751
    Traceback (most recent call last):
1051
752
    BzrError: parent_id {TREE_ROOT} not in inventory
1052
753
    >>> inv.add(InventoryFile('123-123', 'hello.c', 'TREE_ROOT-12345678-12345678'))
1053
 
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None, revision=None)
 
754
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None)
1054
755
    """
1055
756
    def __init__(self, root_id=ROOT_ID, revision_id=None):
1056
757
        """Create or read an inventory.
1070
771
        self.revision_id = revision_id
1071
772
 
1072
773
    def __repr__(self):
1073
 
        # More than one page of ouput is not useful anymore to debug
1074
 
        max_len = 2048
1075
 
        closing = '...}'
1076
 
        contents = repr(self._byid)
1077
 
        if len(contents) > max_len:
1078
 
            contents = contents[:(max_len-len(closing))] + closing
1079
 
        return "<Inventory object at %x, contents=%r>" % (id(self), contents)
 
774
        return "<Inventory object at %x, contents=%r>" % (id(self), self._byid)
1080
775
 
1081
776
    def apply_delta(self, delta):
1082
777
        """Apply a delta to this inventory.
1111
806
            change regardless. E.g. in the recursive deletion of a directory -
1112
807
            the directory's children must be included in the delta, or the
1113
808
            final inventory will be invalid.
1114
 
 
1115
 
            Note that a file_id must only appear once within a given delta.
1116
 
            An AssertionError is raised otherwise.
1117
809
        """
1118
 
        # Check that the delta is legal. It would be nice if this could be
1119
 
        # done within the loops below but it's safer to validate the delta
1120
 
        # before starting to mutate the inventory.
1121
 
        unique_file_ids = set([f for _, _, f, _ in delta])
1122
 
        if len(unique_file_ids) != len(delta):
1123
 
            raise AssertionError("a file-id appears multiple times in %r"
1124
 
                    % (delta,))
1125
 
        del unique_file_ids
1126
 
 
1127
810
        children = {}
1128
811
        # Remove all affected items which were in the original inventory,
1129
812
        # starting with the longest paths, thus ensuring parents are examined
1152
835
                # Pop the child which to allow detection of children whose
1153
836
                # parents were deleted and which were not reattached to a new
1154
837
                # parent.
1155
 
                replacement = InventoryDirectory(new_entry.file_id,
1156
 
                    new_entry.name, new_entry.parent_id)
1157
 
                replacement.revision = new_entry.revision
1158
 
                replacement.children = children.pop(replacement.file_id, {})
1159
 
                new_entry = replacement
 
838
                new_entry.children = children.pop(new_entry.file_id, {})
1160
839
            self.add(new_entry)
1161
840
        if len(children):
1162
841
            # Get the parent id that was deleted
1181
860
            other.add(entry.copy())
1182
861
        return other
1183
862
 
1184
 
    def _get_mutable_inventory(self):
1185
 
        """Returns a mutable copy of the object.
1186
 
 
1187
 
        Some inventories are immutable, yet working trees, for example, needs
1188
 
        to mutate exisiting inventories instead of creating a new one.
1189
 
        """
1190
 
        return deepcopy(self)
1191
 
 
1192
 
    def _get_mutable_inventory(self):
1193
 
        """See CommonInventory._get_mutable_inventory."""
1194
 
        return deepcopy(self)
1195
 
 
1196
863
    def __iter__(self):
1197
 
        """Iterate over all file-ids."""
1198
864
        return iter(self._byid)
1199
865
 
1200
 
    def iter_just_entries(self):
1201
 
        """Iterate over all entries.
1202
 
        
1203
 
        Unlike iter_entries(), just the entries are returned (not (path, ie))
1204
 
        and the order of entries is undefined.
1205
 
 
1206
 
        XXX: We may not want to merge this into bzr.dev.
1207
 
        """
1208
 
        if self.root is None:
1209
 
            return
1210
 
        for _, ie in self._byid.iteritems():
1211
 
            yield ie
1212
 
 
1213
866
    def __len__(self):
1214
867
        """Returns number of entries."""
1215
868
        return len(self._byid)
1216
869
 
 
870
    def iter_entries(self, from_dir=None):
 
871
        """Return (path, entry) pairs, in order by name."""
 
872
        if from_dir is None:
 
873
            if self.root is None:
 
874
                return
 
875
            from_dir = self.root
 
876
            yield '', self.root
 
877
        elif isinstance(from_dir, basestring):
 
878
            from_dir = self._byid[from_dir]
 
879
 
 
880
        # unrolling the recursive called changed the time from
 
881
        # 440ms/663ms (inline/total) to 116ms/116ms
 
882
        children = from_dir.children.items()
 
883
        children.sort()
 
884
        children = collections.deque(children)
 
885
        stack = [(u'', children)]
 
886
        while stack:
 
887
            from_dir_relpath, children = stack[-1]
 
888
 
 
889
            while children:
 
890
                name, ie = children.popleft()
 
891
 
 
892
                # we know that from_dir_relpath never ends in a slash
 
893
                # and 'f' doesn't begin with one, we can do a string op, rather
 
894
                # than the checks of pathjoin(), though this means that all paths
 
895
                # start with a slash
 
896
                path = from_dir_relpath + '/' + name
 
897
 
 
898
                yield path[1:], ie
 
899
 
 
900
                if ie.kind != 'directory':
 
901
                    continue
 
902
 
 
903
                # But do this child first
 
904
                new_children = ie.children.items()
 
905
                new_children.sort()
 
906
                new_children = collections.deque(new_children)
 
907
                stack.append((path, new_children))
 
908
                # Break out of inner loop, so that we start outer loop with child
 
909
                break
 
910
            else:
 
911
                # if we finished all children, pop it off the stack
 
912
                stack.pop()
 
913
 
 
914
    def iter_entries_by_dir(self, from_dir=None, specific_file_ids=None,
 
915
        yield_parents=False):
 
916
        """Iterate over the entries in a directory first order.
 
917
 
 
918
        This returns all entries for a directory before returning
 
919
        the entries for children of a directory. This is not
 
920
        lexicographically sorted order, and is a hybrid between
 
921
        depth-first and breadth-first.
 
922
 
 
923
        :param yield_parents: If True, yield the parents from the root leading
 
924
            down to specific_file_ids that have been requested. This has no
 
925
            impact if specific_file_ids is None.
 
926
        :return: This yields (path, entry) pairs
 
927
        """
 
928
        if specific_file_ids and not isinstance(specific_file_ids, set):
 
929
            specific_file_ids = set(specific_file_ids)
 
930
        # TODO? Perhaps this should return the from_dir so that the root is
 
931
        # yielded? or maybe an option?
 
932
        if from_dir is None:
 
933
            if self.root is None:
 
934
                return
 
935
            # Optimize a common case
 
936
            if (not yield_parents and specific_file_ids is not None and
 
937
                len(specific_file_ids) == 1):
 
938
                file_id = list(specific_file_ids)[0]
 
939
                if file_id in self:
 
940
                    yield self.id2path(file_id), self[file_id]
 
941
                return
 
942
            from_dir = self.root
 
943
            if (specific_file_ids is None or yield_parents or
 
944
                self.root.file_id in specific_file_ids):
 
945
                yield u'', self.root
 
946
        elif isinstance(from_dir, basestring):
 
947
            from_dir = self._byid[from_dir]
 
948
 
 
949
        if specific_file_ids is not None:
 
950
            # TODO: jam 20070302 This could really be done as a loop rather
 
951
            #       than a bunch of recursive calls.
 
952
            parents = set()
 
953
            byid = self._byid
 
954
            def add_ancestors(file_id):
 
955
                if file_id not in byid:
 
956
                    return
 
957
                parent_id = byid[file_id].parent_id
 
958
                if parent_id is None:
 
959
                    return
 
960
                if parent_id not in parents:
 
961
                    parents.add(parent_id)
 
962
                    add_ancestors(parent_id)
 
963
            for file_id in specific_file_ids:
 
964
                add_ancestors(file_id)
 
965
        else:
 
966
            parents = None
 
967
 
 
968
        stack = [(u'', from_dir)]
 
969
        while stack:
 
970
            cur_relpath, cur_dir = stack.pop()
 
971
 
 
972
            child_dirs = []
 
973
            for child_name, child_ie in sorted(cur_dir.children.iteritems()):
 
974
 
 
975
                child_relpath = cur_relpath + child_name
 
976
 
 
977
                if (specific_file_ids is None or
 
978
                    child_ie.file_id in specific_file_ids or
 
979
                    (yield_parents and child_ie.file_id in parents)):
 
980
                    yield child_relpath, child_ie
 
981
 
 
982
                if child_ie.kind == 'directory':
 
983
                    if parents is None or child_ie.file_id in parents:
 
984
                        child_dirs.append((child_relpath+'/', child_ie))
 
985
            stack.extend(reversed(child_dirs))
 
986
 
 
987
    def make_entry(self, kind, name, parent_id, file_id=None):
 
988
        """Simple thunk to bzrlib.inventory.make_entry."""
 
989
        return make_entry(kind, name, parent_id, file_id)
 
990
 
 
991
    def entries(self):
 
992
        """Return list of (path, ie) for all entries except the root.
 
993
 
 
994
        This may be faster than iter_entries.
 
995
        """
 
996
        accum = []
 
997
        def descend(dir_ie, dir_path):
 
998
            kids = dir_ie.children.items()
 
999
            kids.sort()
 
1000
            for name, ie in kids:
 
1001
                child_path = osutils.pathjoin(dir_path, name)
 
1002
                accum.append((child_path, ie))
 
1003
                if ie.kind == 'directory':
 
1004
                    descend(ie, child_path)
 
1005
 
 
1006
        descend(self.root, u'')
 
1007
        return accum
 
1008
 
 
1009
    def directories(self):
 
1010
        """Return (path, entry) pairs for all directories, including the root.
 
1011
        """
 
1012
        accum = []
 
1013
        def descend(parent_ie, parent_path):
 
1014
            accum.append((parent_path, parent_ie))
 
1015
 
 
1016
            kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
 
1017
            kids.sort()
 
1018
 
 
1019
            for name, child_ie in kids:
 
1020
                child_path = osutils.pathjoin(parent_path, name)
 
1021
                descend(child_ie, child_path)
 
1022
        descend(self.root, u'')
 
1023
        return accum
 
1024
 
 
1025
    def __contains__(self, file_id):
 
1026
        """True if this entry contains a file with given id.
 
1027
 
 
1028
        >>> inv = Inventory()
 
1029
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
 
1030
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
 
1031
        >>> '123' in inv
 
1032
        True
 
1033
        >>> '456' in inv
 
1034
        False
 
1035
        """
 
1036
        return (file_id in self._byid)
 
1037
 
1217
1038
    def __getitem__(self, file_id):
1218
1039
        """Return the entry for given file_id.
1219
1040
 
1220
1041
        >>> inv = Inventory()
1221
1042
        >>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
1222
 
        InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
 
1043
        InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
1223
1044
        >>> inv['123123'].name
1224
1045
        'hello.c'
1225
1046
        """
1301
1122
 
1302
1123
        >>> inv = Inventory()
1303
1124
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1304
 
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
 
1125
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
1305
1126
        >>> '123' in inv
1306
1127
        True
1307
1128
        >>> del inv['123']
1321
1142
        >>> i1 == i2
1322
1143
        True
1323
1144
        >>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
1324
 
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
 
1145
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
1325
1146
        >>> i1 == i2
1326
1147
        False
1327
1148
        >>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
1328
 
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
 
1149
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
1329
1150
        >>> i1 == i2
1330
1151
        True
1331
1152
        """
1350
1171
            yield ie
1351
1172
            file_id = ie.parent_id
1352
1173
 
 
1174
    def get_idpath(self, file_id):
 
1175
        """Return a list of file_ids for the path to an entry.
 
1176
 
 
1177
        The list contains one element for each directory followed by
 
1178
        the id of the file itself.  So the length of the returned list
 
1179
        is equal to the depth of the file in the tree, counting the
 
1180
        root directory as depth 1.
 
1181
        """
 
1182
        p = []
 
1183
        for parent in self._iter_file_id_parents(file_id):
 
1184
            p.insert(0, parent.file_id)
 
1185
        return p
 
1186
 
 
1187
    def id2path(self, file_id):
 
1188
        """Return as a string the path to file_id.
 
1189
 
 
1190
        >>> i = Inventory()
 
1191
        >>> e = i.add(InventoryDirectory('src-id', 'src', ROOT_ID))
 
1192
        >>> e = i.add(InventoryFile('foo-id', 'foo.c', parent_id='src-id'))
 
1193
        >>> print i.id2path('foo-id')
 
1194
        src/foo.c
 
1195
        """
 
1196
        # get all names, skipping root
 
1197
        return '/'.join(reversed(
 
1198
            [parent.name for parent in
 
1199
             self._iter_file_id_parents(file_id)][:-1]))
 
1200
 
 
1201
    def path2id(self, name):
 
1202
        """Walk down through directories to return entry of last component.
 
1203
 
 
1204
        names may be either a list of path components, or a single
 
1205
        string, in which case it is automatically split.
 
1206
 
 
1207
        This returns the entry of the last component in the path,
 
1208
        which may be either a file or a directory.
 
1209
 
 
1210
        Returns None IFF the path is not found.
 
1211
        """
 
1212
        if isinstance(name, basestring):
 
1213
            name = osutils.splitpath(name)
 
1214
 
 
1215
        # mutter("lookup path %r" % name)
 
1216
 
 
1217
        parent = self.root
 
1218
        if parent is None:
 
1219
            return None
 
1220
        for f in name:
 
1221
            try:
 
1222
                children = getattr(parent, 'children', None)
 
1223
                if children is None:
 
1224
                    return None
 
1225
                cie = children[f]
 
1226
                parent = cie
 
1227
            except KeyError:
 
1228
                # or raise an error?
 
1229
                return None
 
1230
 
 
1231
        return parent.file_id
 
1232
 
1353
1233
    def has_filename(self, names):
1354
1234
        return bool(self.path2id(names))
1355
1235
 
1442
1322
        return self.root is not None and file_id == self.root.file_id
1443
1323
 
1444
1324
 
1445
 
class CHKInventory(CommonInventory):
1446
 
    """An inventory persisted in a CHK store.
1447
 
 
1448
 
    By design, a CHKInventory is immutable so many of the methods
1449
 
    supported by Inventory - add, rename, apply_delta, etc - are *not*
1450
 
    supported. To create a new CHKInventory, use create_by_apply_delta()
1451
 
    or from_inventory(), say.
1452
 
 
1453
 
    Internally, a CHKInventory has one or two CHKMaps:
1454
 
 
1455
 
    * id_to_entry - a map from (file_id,) => InventoryEntry as bytes
1456
 
    * parent_id_basename_to_file_id - a map from (parent_id, basename_utf8)
1457
 
        => file_id as bytes
1458
 
 
1459
 
    The second map is optional and not present in early CHkRepository's.
1460
 
 
1461
 
    No caching is performed: every method call or item access will perform
1462
 
    requests to the storage layer. As such, keep references to objects you
1463
 
    want to reuse.
1464
 
    """
1465
 
 
1466
 
    def __init__(self, search_key_name):
1467
 
        CommonInventory.__init__(self)
1468
 
        self._fileid_to_entry_cache = {}
1469
 
        self._path_to_fileid_cache = {}
1470
 
        self._search_key_name = search_key_name
1471
 
 
1472
 
    def _entry_to_bytes(self, entry):
1473
 
        """Serialise entry as a single bytestring.
1474
 
 
1475
 
        :param Entry: An inventory entry.
1476
 
        :return: A bytestring for the entry.
1477
 
 
1478
 
        The BNF:
1479
 
        ENTRY ::= FILE | DIR | SYMLINK | TREE
1480
 
        FILE ::= "file: " COMMON SEP SHA SEP SIZE SEP EXECUTABLE
1481
 
        DIR ::= "dir: " COMMON
1482
 
        SYMLINK ::= "symlink: " COMMON SEP TARGET_UTF8
1483
 
        TREE ::= "tree: " COMMON REFERENCE_REVISION
1484
 
        COMMON ::= FILE_ID SEP PARENT_ID SEP NAME_UTF8 SEP REVISION
1485
 
        SEP ::= "\n"
1486
 
        """
1487
 
        if entry.parent_id is not None:
1488
 
            parent_str = entry.parent_id
1489
 
        else:
1490
 
            parent_str = ''
1491
 
        name_str = entry.name.encode("utf8")
1492
 
        if entry.kind == 'file':
1493
 
            if entry.executable:
1494
 
                exec_str = "Y"
1495
 
            else:
1496
 
                exec_str = "N"
1497
 
            return "file: %s\n%s\n%s\n%s\n%s\n%d\n%s" % (
1498
 
                entry.file_id, parent_str, name_str, entry.revision,
1499
 
                entry.text_sha1, entry.text_size, exec_str)
1500
 
        elif entry.kind == 'directory':
1501
 
            return "dir: %s\n%s\n%s\n%s" % (
1502
 
                entry.file_id, parent_str, name_str, entry.revision)
1503
 
        elif entry.kind == 'symlink':
1504
 
            return "symlink: %s\n%s\n%s\n%s\n%s" % (
1505
 
                entry.file_id, parent_str, name_str, entry.revision,
1506
 
                entry.symlink_target.encode("utf8"))
1507
 
        elif entry.kind == 'tree-reference':
1508
 
            return "tree: %s\n%s\n%s\n%s\n%s" % (
1509
 
                entry.file_id, parent_str, name_str, entry.revision,
1510
 
                entry.reference_revision)
1511
 
        else:
1512
 
            raise ValueError("unknown kind %r" % entry.kind)
1513
 
 
1514
 
    @staticmethod
1515
 
    def _bytes_to_utf8name_key(bytes):
1516
 
        """Get the file_id, revision_id key out of bytes."""
1517
 
        # We don't normally care about name, except for times when we want
1518
 
        # to filter out empty names because of non rich-root...
1519
 
        sections = bytes.split('\n')
1520
 
        kind, file_id = sections[0].split(': ')
1521
 
        return (sections[2], file_id, sections[3])
1522
 
 
1523
 
    def _bytes_to_entry(self, bytes):
1524
 
        """Deserialise a serialised entry."""
1525
 
        sections = bytes.split('\n')
1526
 
        if sections[0].startswith("file: "):
1527
 
            result = InventoryFile(sections[0][6:],
1528
 
                sections[2].decode('utf8'),
1529
 
                sections[1])
1530
 
            result.text_sha1 = sections[4]
1531
 
            result.text_size = int(sections[5])
1532
 
            result.executable = sections[6] == "Y"
1533
 
        elif sections[0].startswith("dir: "):
1534
 
            result = CHKInventoryDirectory(sections[0][5:],
1535
 
                sections[2].decode('utf8'),
1536
 
                sections[1], self)
1537
 
        elif sections[0].startswith("symlink: "):
1538
 
            result = InventoryLink(sections[0][9:],
1539
 
                sections[2].decode('utf8'),
1540
 
                sections[1])
1541
 
            result.symlink_target = sections[4].decode('utf8')
1542
 
        elif sections[0].startswith("tree: "):
1543
 
            result = TreeReference(sections[0][6:],
1544
 
                sections[2].decode('utf8'),
1545
 
                sections[1])
1546
 
            result.reference_revision = sections[4]
1547
 
        else:
1548
 
            raise ValueError("Not a serialised entry %r" % bytes)
1549
 
        result.revision = sections[3]
1550
 
        if result.parent_id == '':
1551
 
            result.parent_id = None
1552
 
        self._fileid_to_entry_cache[result.file_id] = result
1553
 
        return result
1554
 
 
1555
 
    def _get_mutable_inventory(self):
1556
 
        """See CommonInventory._get_mutable_inventory."""
1557
 
        entries = self.iter_entries()
1558
 
        if self.root_id is not None:
1559
 
            entries.next()
1560
 
        inv = Inventory(self.root_id, self.revision_id)
1561
 
        for path, inv_entry in entries:
1562
 
            inv.add(inv_entry)
1563
 
        return inv
1564
 
 
1565
 
    def create_by_apply_delta(self, inventory_delta, new_revision_id,
1566
 
        propagate_caches=False):
1567
 
        """Create a new CHKInventory by applying inventory_delta to this one.
1568
 
 
1569
 
        :param inventory_delta: The inventory delta to apply. See
1570
 
            Inventory.apply_delta for details.
1571
 
        :param new_revision_id: The revision id of the resulting CHKInventory.
1572
 
        :param propagate_caches: If True, the caches for this inventory are
1573
 
          copied to and updated for the result.
1574
 
        :return: The new CHKInventory.
1575
 
        """
1576
 
        result = CHKInventory(self._search_key_name)
1577
 
        if propagate_caches:
1578
 
            # Just propagate the path-to-fileid cache for now
1579
 
            result._path_to_fileid_cache = dict(self._path_to_fileid_cache.iteritems())
1580
 
        search_key_func = chk_map.search_key_registry.get(self._search_key_name)
1581
 
        self.id_to_entry._ensure_root()
1582
 
        maximum_size = self.id_to_entry._root_node.maximum_size
1583
 
        result.revision_id = new_revision_id
1584
 
        result.id_to_entry = chk_map.CHKMap(
1585
 
            self.id_to_entry._store,
1586
 
            self.id_to_entry.key(),
1587
 
            search_key_func=search_key_func)
1588
 
        result.id_to_entry._ensure_root()
1589
 
        result.id_to_entry._root_node.set_maximum_size(maximum_size)
1590
 
        parent_id_basename_delta = []
1591
 
        if self.parent_id_basename_to_file_id is not None:
1592
 
            result.parent_id_basename_to_file_id = chk_map.CHKMap(
1593
 
                self.parent_id_basename_to_file_id._store,
1594
 
                self.parent_id_basename_to_file_id.key(),
1595
 
                search_key_func=search_key_func)
1596
 
            result.parent_id_basename_to_file_id._ensure_root()
1597
 
            self.parent_id_basename_to_file_id._ensure_root()
1598
 
            result_p_id_root = result.parent_id_basename_to_file_id._root_node
1599
 
            p_id_root = self.parent_id_basename_to_file_id._root_node
1600
 
            result_p_id_root.set_maximum_size(p_id_root.maximum_size)
1601
 
            result_p_id_root._key_width = p_id_root._key_width
1602
 
        else:
1603
 
            result.parent_id_basename_to_file_id = None
1604
 
        result.root_id = self.root_id
1605
 
        id_to_entry_delta = []
1606
 
        for old_path, new_path, file_id, entry in inventory_delta:
1607
 
            # file id changes
1608
 
            if new_path == '':
1609
 
                result.root_id = file_id
1610
 
            if new_path is None:
1611
 
                # Make a delete:
1612
 
                new_key = None
1613
 
                new_value = None
1614
 
                # Update caches
1615
 
                if propagate_caches:
1616
 
                    try:
1617
 
                        del result._path_to_fileid_cache[old_path]
1618
 
                    except KeyError:
1619
 
                        pass
1620
 
            else:
1621
 
                new_key = (file_id,)
1622
 
                new_value = result._entry_to_bytes(entry)
1623
 
                # Update caches. It's worth doing this whether
1624
 
                # we're propagating the old caches or not.
1625
 
                result._path_to_fileid_cache[new_path] = file_id
1626
 
            if old_path is None:
1627
 
                old_key = None
1628
 
            else:
1629
 
                old_key = (file_id,)
1630
 
            id_to_entry_delta.append((old_key, new_key, new_value))
1631
 
            if result.parent_id_basename_to_file_id is not None:
1632
 
                # parent_id, basename changes
1633
 
                if old_path is None:
1634
 
                    old_key = None
1635
 
                else:
1636
 
                    old_entry = self[file_id]
1637
 
                    old_key = self._parent_id_basename_key(old_entry)
1638
 
                if new_path is None:
1639
 
                    new_key = None
1640
 
                    new_value = None
1641
 
                else:
1642
 
                    new_key = self._parent_id_basename_key(entry)
1643
 
                    new_value = file_id
1644
 
                if old_key != new_key:
1645
 
                    # If the two keys are the same, the value will be unchanged
1646
 
                    # as its always the file id.
1647
 
                    parent_id_basename_delta.append((old_key, new_key, new_value))
1648
 
        result.id_to_entry.apply_delta(id_to_entry_delta)
1649
 
        if parent_id_basename_delta:
1650
 
            result.parent_id_basename_to_file_id.apply_delta(parent_id_basename_delta)
1651
 
        return result
1652
 
 
1653
 
    @classmethod
1654
 
    def deserialise(klass, chk_store, bytes, expected_revision_id):
1655
 
        """Deserialise a CHKInventory.
1656
 
 
1657
 
        :param chk_store: A CHK capable VersionedFiles instance.
1658
 
        :param bytes: The serialised bytes.
1659
 
        :param expected_revision_id: The revision ID we think this inventory is
1660
 
            for.
1661
 
        :return: A CHKInventory
1662
 
        """
1663
 
        lines = bytes.split('\n')
1664
 
        if lines[-1] != '':
1665
 
            raise AssertionError('bytes to deserialize must end with an eol')
1666
 
        lines.pop()
1667
 
        if lines[0] != 'chkinventory:':
1668
 
            raise ValueError("not a serialised CHKInventory: %r" % bytes)
1669
 
        info = {}
1670
 
        allowed_keys = frozenset(['root_id', 'revision_id', 'search_key_name',
1671
 
                                  'parent_id_basename_to_file_id',
1672
 
                                  'id_to_entry'])
1673
 
        for line in lines[1:]:
1674
 
            key, value = line.split(': ', 1)
1675
 
            if key not in allowed_keys:
1676
 
                raise errors.BzrError('Unknown key in inventory: %r\n%r'
1677
 
                                      % (key, bytes))
1678
 
            if key in info:
1679
 
                raise errors.BzrError('Duplicate key in inventory: %r\n%r'
1680
 
                                      % (key, bytes))
1681
 
            info[key] = value
1682
 
        revision_id = info['revision_id']
1683
 
        root_id = info['root_id']
1684
 
        search_key_name = info.get('search_key_name', 'plain')
1685
 
        parent_id_basename_to_file_id = info.get(
1686
 
            'parent_id_basename_to_file_id', None)
1687
 
        id_to_entry = info['id_to_entry']
1688
 
 
1689
 
        result = CHKInventory(search_key_name)
1690
 
        result.revision_id = revision_id
1691
 
        result.root_id = root_id
1692
 
        search_key_func = chk_map.search_key_registry.get(
1693
 
                            result._search_key_name)
1694
 
        if parent_id_basename_to_file_id is not None:
1695
 
            result.parent_id_basename_to_file_id = chk_map.CHKMap(
1696
 
                chk_store, (parent_id_basename_to_file_id,),
1697
 
                search_key_func=search_key_func)
1698
 
        else:
1699
 
            result.parent_id_basename_to_file_id = None
1700
 
 
1701
 
        result.id_to_entry = chk_map.CHKMap(chk_store, (id_to_entry,),
1702
 
                                            search_key_func=search_key_func)
1703
 
        if (result.revision_id,) != expected_revision_id:
1704
 
            raise ValueError("Mismatched revision id and expected: %r, %r" %
1705
 
                (result.revision_id, expected_revision_id))
1706
 
        return result
1707
 
 
1708
 
    @classmethod
1709
 
    def from_inventory(klass, chk_store, inventory, maximum_size=0, search_key_name='plain'):
1710
 
        """Create a CHKInventory from an existing inventory.
1711
 
 
1712
 
        The content of inventory is copied into the chk_store, and a
1713
 
        CHKInventory referencing that is returned.
1714
 
 
1715
 
        :param chk_store: A CHK capable VersionedFiles instance.
1716
 
        :param inventory: The inventory to copy.
1717
 
        :param maximum_size: The CHKMap node size limit.
1718
 
        :param search_key_name: The identifier for the search key function
1719
 
        """
1720
 
        result = CHKInventory(search_key_name)
1721
 
        result.revision_id = inventory.revision_id
1722
 
        result.root_id = inventory.root.file_id
1723
 
        search_key_func = chk_map.search_key_registry.get(search_key_name)
1724
 
        result.id_to_entry = chk_map.CHKMap(chk_store, None, search_key_func)
1725
 
        result.id_to_entry._root_node.set_maximum_size(maximum_size)
1726
 
        file_id_delta = []
1727
 
        result.parent_id_basename_to_file_id = chk_map.CHKMap(chk_store,
1728
 
            None, search_key_func)
1729
 
        result.parent_id_basename_to_file_id._root_node.set_maximum_size(
1730
 
            maximum_size)
1731
 
        result.parent_id_basename_to_file_id._root_node._key_width = 2
1732
 
        parent_id_delta = []
1733
 
        for path, entry in inventory.iter_entries():
1734
 
            file_id_delta.append((None, (entry.file_id,),
1735
 
                result._entry_to_bytes(entry)))
1736
 
            parent_id_delta.append(
1737
 
                (None, result._parent_id_basename_key(entry),
1738
 
                 entry.file_id))
1739
 
        result.id_to_entry.apply_delta(file_id_delta)
1740
 
        result.parent_id_basename_to_file_id.apply_delta(parent_id_delta)
1741
 
        return result
1742
 
 
1743
 
    def _parent_id_basename_key(self, entry):
1744
 
        """Create a key for a entry in a parent_id_basename_to_file_id index."""
1745
 
        if entry.parent_id is not None:
1746
 
            parent_id = entry.parent_id
1747
 
        else:
1748
 
            parent_id = ''
1749
 
        return parent_id, entry.name.encode('utf8')
1750
 
 
1751
 
    def __getitem__(self, file_id):
1752
 
        """map a single file_id -> InventoryEntry."""
1753
 
        if file_id is None:
1754
 
            raise errors.NoSuchId(self, file_id)
1755
 
        result = self._fileid_to_entry_cache.get(file_id, None)
1756
 
        if result is not None:
1757
 
            return result
1758
 
        try:
1759
 
            return self._bytes_to_entry(
1760
 
                self.id_to_entry.iteritems([(file_id,)]).next()[1])
1761
 
        except StopIteration:
1762
 
            # really we're passing an inventory, not a tree...
1763
 
            raise errors.NoSuchId(self, file_id)
1764
 
 
1765
 
    def has_id(self, file_id):
1766
 
        # Perhaps have an explicit 'contains' method on CHKMap ?
1767
 
        if self._fileid_to_entry_cache.get(file_id, None) is not None:
1768
 
            return True
1769
 
        return len(list(self.id_to_entry.iteritems([(file_id,)]))) == 1
1770
 
 
1771
 
    def is_root(self, file_id):
1772
 
        return file_id == self.root_id
1773
 
 
1774
 
    def _iter_file_id_parents(self, file_id):
1775
 
        """Yield the parents of file_id up to the root."""
1776
 
        while file_id is not None:
1777
 
            try:
1778
 
                ie = self[file_id]
1779
 
            except KeyError:
1780
 
                raise errors.NoSuchId(tree=self, file_id=file_id)
1781
 
            yield ie
1782
 
            file_id = ie.parent_id
1783
 
 
1784
 
    def __iter__(self):
1785
 
        """Iterate over all file-ids."""
1786
 
        for key, _ in self.id_to_entry.iteritems():
1787
 
            yield key[-1]
1788
 
 
1789
 
    def iter_just_entries(self):
1790
 
        """Iterate over all entries.
1791
 
        
1792
 
        Unlike iter_entries(), just the entries are returned (not (path, ie))
1793
 
        and the order of entries is undefined.
1794
 
 
1795
 
        XXX: We may not want to merge this into bzr.dev.
1796
 
        """
1797
 
        for key, entry in self.id_to_entry.iteritems():
1798
 
            file_id = key[0]
1799
 
            ie = self._fileid_to_entry_cache.get(file_id, None)
1800
 
            if ie is None:
1801
 
                ie = self._bytes_to_entry(entry)
1802
 
                self._fileid_to_entry_cache[file_id] = ie
1803
 
            yield ie
1804
 
 
1805
 
    def iter_changes(self, basis):
1806
 
        """Generate a Tree.iter_changes change list between this and basis.
1807
 
 
1808
 
        :param basis: Another CHKInventory.
1809
 
        :return: An iterator over the changes between self and basis, as per
1810
 
            tree.iter_changes().
1811
 
        """
1812
 
        # We want: (file_id, (path_in_source, path_in_target),
1813
 
        # changed_content, versioned, parent, name, kind,
1814
 
        # executable)
1815
 
        for key, basis_value, self_value in \
1816
 
            self.id_to_entry.iter_changes(basis.id_to_entry):
1817
 
            file_id = key[0]
1818
 
            if basis_value is not None:
1819
 
                basis_entry = basis._bytes_to_entry(basis_value)
1820
 
                path_in_source = basis.id2path(file_id)
1821
 
                basis_parent = basis_entry.parent_id
1822
 
                basis_name = basis_entry.name
1823
 
                basis_executable = basis_entry.executable
1824
 
            else:
1825
 
                path_in_source = None
1826
 
                basis_parent = None
1827
 
                basis_name = None
1828
 
                basis_executable = None
1829
 
            if self_value is not None:
1830
 
                self_entry = self._bytes_to_entry(self_value)
1831
 
                path_in_target = self.id2path(file_id)
1832
 
                self_parent = self_entry.parent_id
1833
 
                self_name = self_entry.name
1834
 
                self_executable = self_entry.executable
1835
 
            else:
1836
 
                path_in_target = None
1837
 
                self_parent = None
1838
 
                self_name = None
1839
 
                self_executable = None
1840
 
            if basis_value is None:
1841
 
                # add
1842
 
                kind = (None, self_entry.kind)
1843
 
                versioned = (False, True)
1844
 
            elif self_value is None:
1845
 
                # delete
1846
 
                kind = (basis_entry.kind, None)
1847
 
                versioned = (True, False)
1848
 
            else:
1849
 
                kind = (basis_entry.kind, self_entry.kind)
1850
 
                versioned = (True, True)
1851
 
            changed_content = False
1852
 
            if kind[0] != kind[1]:
1853
 
                changed_content = True
1854
 
            elif kind[0] == 'file':
1855
 
                if (self_entry.text_size != basis_entry.text_size or
1856
 
                    self_entry.text_sha1 != basis_entry.text_sha1):
1857
 
                    changed_content = True
1858
 
            elif kind[0] == 'symlink':
1859
 
                if self_entry.symlink_target != basis_entry.symlink_target:
1860
 
                    changed_content = True
1861
 
            elif kind[0] == 'tree-reference':
1862
 
                if (self_entry.reference_revision !=
1863
 
                    basis_entry.reference_revision):
1864
 
                    changed_content = True
1865
 
            parent = (basis_parent, self_parent)
1866
 
            name = (basis_name, self_name)
1867
 
            executable = (basis_executable, self_executable)
1868
 
            if (not changed_content
1869
 
                and parent[0] == parent[1]
1870
 
                and name[0] == name[1]
1871
 
                and executable[0] == executable[1]):
1872
 
                # Could happen when only the revision changed for a directory
1873
 
                # for instance.
1874
 
                continue
1875
 
            yield (file_id, (path_in_source, path_in_target), changed_content,
1876
 
                versioned, parent, name, kind, executable)
1877
 
 
1878
 
    def __len__(self):
1879
 
        """Return the number of entries in the inventory."""
1880
 
        return len(self.id_to_entry)
1881
 
 
1882
 
    def _make_delta(self, old):
1883
 
        """Make an inventory delta from two inventories."""
1884
 
        if type(old) != CHKInventory:
1885
 
            return CommonInventory._make_delta(self, old)
1886
 
        delta = []
1887
 
        for key, old_value, self_value in \
1888
 
            self.id_to_entry.iter_changes(old.id_to_entry):
1889
 
            file_id = key[0]
1890
 
            if old_value is not None:
1891
 
                old_path = old.id2path(file_id)
1892
 
            else:
1893
 
                old_path = None
1894
 
            if self_value is not None:
1895
 
                entry = self._bytes_to_entry(self_value)
1896
 
                self._fileid_to_entry_cache[file_id] = entry
1897
 
                new_path = self.id2path(file_id)
1898
 
            else:
1899
 
                entry = None
1900
 
                new_path = None
1901
 
            delta.append((old_path, new_path, file_id, entry))
1902
 
        return delta
1903
 
 
1904
 
    def path2id(self, name):
1905
 
        """See CommonInventory.path2id()."""
1906
 
        result = self._path_to_fileid_cache.get(name, None)
1907
 
        if result is None:
1908
 
            result = CommonInventory.path2id(self, name)
1909
 
            self._path_to_fileid_cache[name] = result
1910
 
        return result
1911
 
 
1912
 
    def to_lines(self):
1913
 
        """Serialise the inventory to lines."""
1914
 
        lines = ["chkinventory:\n"]
1915
 
        if self._search_key_name != 'plain':
1916
 
            # custom ordering grouping things that don't change together
1917
 
            lines.append('search_key_name: %s\n' % (self._search_key_name,))
1918
 
            lines.append("root_id: %s\n" % self.root_id)
1919
 
            lines.append('parent_id_basename_to_file_id: %s\n' %
1920
 
                self.parent_id_basename_to_file_id.key())
1921
 
            lines.append("revision_id: %s\n" % self.revision_id)
1922
 
            lines.append("id_to_entry: %s\n" % self.id_to_entry.key())
1923
 
        else:
1924
 
            lines.append("revision_id: %s\n" % self.revision_id)
1925
 
            lines.append("root_id: %s\n" % self.root_id)
1926
 
            if self.parent_id_basename_to_file_id is not None:
1927
 
                lines.append('parent_id_basename_to_file_id: %s\n' %
1928
 
                    self.parent_id_basename_to_file_id.key())
1929
 
            lines.append("id_to_entry: %s\n" % self.id_to_entry.key())
1930
 
        return lines
1931
 
 
1932
 
    @property
1933
 
    def root(self):
1934
 
        """Get the root entry."""
1935
 
        return self[self.root_id]
1936
 
 
1937
 
 
1938
 
class CHKInventoryDirectory(InventoryDirectory):
1939
 
    """A directory in an inventory."""
1940
 
 
1941
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
1942
 
                 'text_id', 'parent_id', '_children', 'executable',
1943
 
                 'revision', 'symlink_target', 'reference_revision',
1944
 
                 '_chk_inventory']
1945
 
 
1946
 
    def __init__(self, file_id, name, parent_id, chk_inventory):
1947
 
        # Don't call InventoryDirectory.__init__ - it isn't right for this
1948
 
        # class.
1949
 
        InventoryEntry.__init__(self, file_id, name, parent_id)
1950
 
        self._children = None
1951
 
        self.kind = 'directory'
1952
 
        self._chk_inventory = chk_inventory
1953
 
 
1954
 
    @property
1955
 
    def children(self):
1956
 
        """Access the list of children of this directory.
1957
 
 
1958
 
        With a parent_id_basename_to_file_id index, loads all the children,
1959
 
        without loads the entire index. Without is bad. A more sophisticated
1960
 
        proxy object might be nice, to allow partial loading of children as
1961
 
        well when specific names are accessed. (So path traversal can be
1962
 
        written in the obvious way but not examine siblings.).
1963
 
        """
1964
 
        if self._children is not None:
1965
 
            return self._children
1966
 
        # No longer supported
1967
 
        if self._chk_inventory.parent_id_basename_to_file_id is None:
1968
 
            raise AssertionError("Inventories without"
1969
 
                " parent_id_basename_to_file_id are no longer supported")
1970
 
        result = {}
1971
 
        # XXX: Todo - use proxy objects for the children rather than loading
1972
 
        # all when the attribute is referenced.
1973
 
        parent_id_index = self._chk_inventory.parent_id_basename_to_file_id
1974
 
        child_keys = set()
1975
 
        for (parent_id, name_utf8), file_id in parent_id_index.iteritems(
1976
 
            key_filter=[(self.file_id,)]):
1977
 
            child_keys.add((file_id,))
1978
 
        cached = set()
1979
 
        for file_id_key in child_keys:
1980
 
            entry = self._chk_inventory._fileid_to_entry_cache.get(
1981
 
                file_id_key[0], None)
1982
 
            if entry is not None:
1983
 
                result[entry.name] = entry
1984
 
                cached.add(file_id_key)
1985
 
        child_keys.difference_update(cached)
1986
 
        # populate; todo: do by name
1987
 
        id_to_entry = self._chk_inventory.id_to_entry
1988
 
        for file_id_key, bytes in id_to_entry.iteritems(child_keys):
1989
 
            entry = self._chk_inventory._bytes_to_entry(bytes)
1990
 
            result[entry.name] = entry
1991
 
            self._chk_inventory._fileid_to_entry_cache[file_id_key[0]] = entry
1992
 
        self._children = result
1993
 
        return result
1994
 
 
1995
1325
entry_factory = {
1996
1326
    'directory': InventoryDirectory,
1997
1327
    'file': InventoryFile,