~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

Merge bzr.dev

Show diffs side-by-side

added added

removed removed

Lines of Context:
77
77
    >>> i.path2id('')
78
78
    'TREE_ROOT'
79
79
    >>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
80
 
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
 
80
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
81
81
    >>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
82
 
    InventoryFile('2323', 'hello.c', parent_id='123')
 
82
    InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
83
83
    >>> shouldbe = {0: 'src', 1: pathjoin('src','hello.c')}
84
84
    >>> for ix, j in enumerate(i.iter_entries()):
85
85
    ...   print (j[0] == shouldbe[ix], j[1])
86
86
    ... 
87
 
    (True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
88
 
    (True, InventoryFile('2323', 'hello.c', parent_id='123'))
 
87
    (True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
 
88
    (True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
89
89
    >>> i.add(InventoryFile('2323', 'bye.c', '123'))
90
90
    Traceback (most recent call last):
91
91
    ...
92
92
    BzrError: inventory already contains entry with id {2323}
93
93
    >>> i.add(InventoryFile('2324', 'bye.c', '123'))
94
 
    InventoryFile('2324', 'bye.c', parent_id='123')
 
94
    InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
95
95
    >>> i.add(InventoryDirectory('2325', 'wibble', '123'))
96
 
    InventoryDirectory('2325', 'wibble', parent_id='123')
 
96
    InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
97
97
    >>> i.path2id('src/wibble')
98
98
    '2325'
99
99
    >>> '2325' in i
100
100
    True
101
101
    >>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
102
 
    InventoryFile('2326', 'wibble.c', parent_id='2325')
 
102
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
103
103
    >>> i['2326']
104
 
    InventoryFile('2326', 'wibble.c', parent_id='2325')
 
104
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
105
105
    >>> for path, entry in i.iter_entries():
106
106
    ...     print path
107
107
    ...     assert i.path2id(path)
123
123
    RENAMED = 'renamed'
124
124
    MODIFIED_AND_RENAMED = 'modified and renamed'
125
125
    
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()
 
126
    __slots__ = []
135
127
 
136
128
    def detect_changes(self, old_entry):
137
129
        """Return a (text_modified, meta_modified) from this to old_entry.
166
158
                            versioned_file_store,
167
159
                            transaction,
168
160
                            entry_vf=None):
169
 
        """Return the revisions and entries that directly preceed this.
 
161
        """Return the revisions and entries that directly precede this.
170
162
 
171
163
        Returned as a map from revision to inventory entry.
172
164
 
325
317
        raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
326
318
 
327
319
    def sorted_children(self):
328
 
        l = self.children.items()
329
 
        l.sort()
330
 
        return l
 
320
        return sorted(self.children.items())
331
321
 
332
322
    @staticmethod
333
323
    def versionable_kind(kind):
347
337
        :param inv: Inventory from which the entry was loaded.
348
338
        :param tree: RevisionTree for this entry.
349
339
        """
350
 
        if self.parent_id != None:
 
340
        if self.parent_id is not None:
351
341
            if not inv.has_id(self.parent_id):
352
342
                raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
353
343
                        % (self.parent_id, rev_id))
403
393
        return 'unchanged'
404
394
 
405
395
    def __repr__(self):
406
 
        return ("%s(%r, %r, parent_id=%r)"
 
396
        return ("%s(%r, %r, parent_id=%r, revision=%r)"
407
397
                % (self.__class__.__name__,
408
398
                   self.file_id,
409
399
                   self.name,
410
 
                   self.parent_id))
 
400
                   self.parent_id,
 
401
                   self.revision))
411
402
 
412
403
    def snapshot(self, revision, path, previous_entries,
413
 
                 work_tree, weave_store, transaction):
 
404
                 work_tree, commit_builder):
414
405
        """Make a snapshot of this entry which may or may not have changed.
415
406
        
416
407
        This means that all its fields are populated, that it has its
418
409
        """
419
410
        mutter('new parents of %s are %r', path, previous_entries)
420
411
        self._read_tree_state(path, work_tree)
 
412
        # TODO: Where should we determine whether to reuse a
 
413
        # previous revision id or create a new revision? 20060606
421
414
        if len(previous_entries) == 1:
422
415
            # cannot be unchanged unless there is only one parent file rev.
423
416
            parent_ie = previous_entries.values()[0]
426
419
                self.revision = parent_ie.revision
427
420
                return "unchanged"
428
421
        return self._snapshot_into_revision(revision, previous_entries, 
429
 
                                            work_tree, weave_store, transaction)
 
422
                                            work_tree, commit_builder)
430
423
 
431
424
    def _snapshot_into_revision(self, revision, previous_entries, work_tree,
432
 
                                weave_store, transaction):
 
425
                                commit_builder):
433
426
        """Record this revision unconditionally into a store.
434
427
 
435
428
        The entry's last-changed revision property (`revision`) is updated to 
441
434
        """
442
435
        mutter('new revision {%s} for {%s}', revision, self.file_id)
443
436
        self.revision = revision
444
 
        self._snapshot_text(previous_entries, work_tree, weave_store,
445
 
                            transaction)
 
437
        self._snapshot_text(previous_entries, work_tree, commit_builder)
446
438
 
447
 
    def _snapshot_text(self, file_parents, work_tree, weave_store, transaction): 
 
439
    def _snapshot_text(self, file_parents, work_tree, commit_builder): 
448
440
        """Record the 'text' of this entry, whatever form that takes.
449
441
        
450
442
        This default implementation simply adds an empty text.
451
443
        """
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)
 
444
        raise NotImplementedError(self._snapshot_text)
455
445
 
456
446
    def __eq__(self, other):
457
447
        if not isinstance(other, InventoryEntry):
478
468
    def _unchanged(self, previous_ie):
479
469
        """Has this entry changed relative to previous_ie.
480
470
 
481
 
        This method should be overriden in child classes.
 
471
        This method should be overridden in child classes.
482
472
        """
483
473
        compatible = True
484
474
        # different inv parent
507
497
class InventoryDirectory(InventoryEntry):
508
498
    """A directory in an inventory."""
509
499
 
 
500
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
501
                 'text_id', 'parent_id', 'children', 'executable', 
 
502
                 'revision', 'symlink_target']
 
503
 
510
504
    def _check(self, checker, rev_id, tree):
511
505
        """See InventoryEntry._check"""
512
 
        if self.text_sha1 != None or self.text_size != None or self.text_id != None:
 
506
        if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
513
507
            raise BzrCheckError('directory {%s} has text in revision {%s}'
514
508
                                % (self.file_id, rev_id))
515
509
 
542
536
        """See InventoryEntry._put_on_disk."""
543
537
        os.mkdir(fullpath)
544
538
 
 
539
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
 
540
        """See InventoryEntry._snapshot_text."""
 
541
        commit_builder.modified_directory(self.file_id, file_parents)
 
542
 
545
543
 
546
544
class InventoryFile(InventoryEntry):
547
545
    """A file in an inventory."""
548
546
 
 
547
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
548
                 'text_id', 'parent_id', 'children', 'executable', 
 
549
                 'revision', 'symlink_target']
 
550
 
549
551
    def _check(self, checker, tree_revision_id, tree):
550
552
        """See InventoryEntry._check"""
551
553
        t = (self.file_id, self.revision)
590
592
 
591
593
    def detect_changes(self, old_entry):
592
594
        """See InventoryEntry.detect_changes."""
593
 
        assert self.text_sha1 != None
594
 
        assert old_entry.text_sha1 != None
 
595
        assert self.text_sha1 is not None
 
596
        assert old_entry.text_sha1 is not None
595
597
        text_modified = (self.text_sha1 != old_entry.text_sha1)
596
598
        meta_modified = (self.executable != old_entry.executable)
597
599
        return text_modified, meta_modified
650
652
    def _read_tree_state(self, path, work_tree):
651
653
        """See InventoryEntry._read_tree_state."""
652
654
        self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
 
655
        # FIXME: 20050930 probe for the text size when getting sha1
 
656
        # in _read_tree_state
653
657
        self.executable = work_tree.is_executable(self.file_id, path=path)
654
658
 
 
659
    def __repr__(self):
 
660
        return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s)"
 
661
                % (self.__class__.__name__,
 
662
                   self.file_id,
 
663
                   self.name,
 
664
                   self.parent_id,
 
665
                   self.text_sha1,
 
666
                   self.text_size))
 
667
 
655
668
    def _forget_tree_state(self):
656
669
        self.text_sha1 = None
657
670
        self.executable = None
658
671
 
659
 
    def _snapshot_text(self, file_parents, work_tree, versionedfile_store, transaction):
 
672
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
660
673
        """See InventoryEntry._snapshot_text."""
661
 
        mutter('storing text of file {%s} in revision {%s} into %r',
662
 
               self.file_id, self.revision, versionedfile_store)
663
 
        # special case to avoid diffing on renames or 
664
 
        # reparenting
665
 
        if (len(file_parents) == 1
666
 
            and self.text_sha1 == file_parents.values()[0].text_sha1
667
 
            and self.text_size == file_parents.values()[0].text_size):
668
 
            previous_ie = file_parents.values()[0]
669
 
            versionedfile = versionedfile_store.get_weave(self.file_id, transaction)
670
 
            versionedfile.clone_text(self.revision, previous_ie.revision, file_parents.keys())
671
 
        else:
672
 
            new_lines = work_tree.get_file(self.file_id).readlines()
673
 
            self._add_text_to_weave(new_lines, file_parents.keys(), versionedfile_store,
674
 
                                    transaction)
675
 
            self.text_sha1 = sha_strings(new_lines)
676
 
            self.text_size = sum(map(len, new_lines))
677
 
 
 
674
        def get_content_byte_lines():
 
675
            return work_tree.get_file(self.file_id).readlines()
 
676
        self.text_sha1, self.text_size = commit_builder.modified_file_text(
 
677
            self.file_id, file_parents, get_content_byte_lines, self.text_sha1, self.text_size)
678
678
 
679
679
    def _unchanged(self, previous_ie):
680
680
        """See InventoryEntry._unchanged."""
693
693
class InventoryLink(InventoryEntry):
694
694
    """A file in an inventory."""
695
695
 
696
 
    __slots__ = ['symlink_target']
 
696
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
697
                 'text_id', 'parent_id', 'children', 'executable', 
 
698
                 'revision', 'symlink_target']
697
699
 
698
700
    def _check(self, checker, rev_id, tree):
699
701
        """See InventoryEntry._check"""
700
 
        if self.text_sha1 != None or self.text_size != None or self.text_id != None:
 
702
        if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
701
703
            raise BzrCheckError('symlink {%s} has text in revision {%s}'
702
704
                    % (self.file_id, rev_id))
703
 
        if self.symlink_target == None:
 
705
        if self.symlink_target is None:
704
706
            raise BzrCheckError('symlink {%s} has no target in revision {%s}'
705
707
                    % (self.file_id, rev_id))
706
708
 
774
776
            compatible = False
775
777
        return compatible
776
778
 
 
779
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
 
780
        """See InventoryEntry._snapshot_text."""
 
781
        commit_builder.modified_link(
 
782
            self.file_id, file_parents, self.symlink_target)
 
783
 
777
784
 
778
785
class Inventory(object):
779
786
    """Inventory of versioned files in a tree.
794
801
 
795
802
    >>> inv = Inventory()
796
803
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
797
 
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT')
 
804
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
798
805
    >>> inv['123-123'].name
799
806
    'hello.c'
800
807
 
811
818
    [u'hello.c']
812
819
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
813
820
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
814
 
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
 
821
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None)
815
822
    """
816
823
    def __init__(self, root_id=ROOT_ID, revision_id=None):
817
824
        """Create or read an inventory.
831
838
            self._byid = {}
832
839
        self.revision_id = revision_id
833
840
 
834
 
 
835
841
    def copy(self):
836
842
        # TODO: jam 20051218 Should copy also copy the revision_id?
837
843
        other = Inventory(self.root.file_id)
843
849
            other.add(entry.copy())
844
850
        return other
845
851
 
846
 
 
847
852
    def __iter__(self):
848
853
        return iter(self._byid)
849
854
 
850
 
 
851
855
    def __len__(self):
852
856
        """Returns number of entries."""
853
857
        return len(self._byid)
854
858
 
855
 
 
856
859
    def iter_entries(self, from_dir=None):
857
860
        """Return (path, entry) pairs, in order by name."""
858
 
        if from_dir == None:
 
861
        if from_dir is None:
859
862
            if self.root is None:
860
863
                return
861
864
            from_dir = self.root
896
899
                # if we finished all children, pop it off the stack
897
900
                stack.pop()
898
901
 
 
902
    def iter_entries_by_dir(self, from_dir=None):
 
903
        """Iterate over the entries in a directory first order.
 
904
 
 
905
        This returns all entries for a directory before returning
 
906
        the entries for children of a directory. This is not
 
907
        lexicographically sorted order, and is a hybrid between
 
908
        depth-first and breadth-first.
 
909
 
 
910
        :return: This yields (path, entry) pairs
 
911
        """
 
912
        # TODO? Perhaps this should return the from_dir so that the root is
 
913
        # yielded? or maybe an option?
 
914
        if from_dir is None:
 
915
            assert self.root
 
916
            from_dir = self.root
 
917
        elif isinstance(from_dir, basestring):
 
918
            from_dir = self._byid[from_dir]
 
919
            
 
920
        stack = [(u'', from_dir)]
 
921
        while stack:
 
922
            cur_relpath, cur_dir = stack.pop()
 
923
 
 
924
            child_dirs = []
 
925
            for child_name, child_ie in sorted(cur_dir.children.iteritems()):
 
926
 
 
927
                child_relpath = cur_relpath + child_name
 
928
 
 
929
                yield child_relpath, child_ie
 
930
 
 
931
                if child_ie.kind == 'directory':
 
932
                    child_dirs.append((child_relpath+'/', child_ie))
 
933
            stack.extend(reversed(child_dirs))
 
934
 
899
935
    def entries(self):
900
936
        """Return list of (path, ie) for all entries except the root.
901
937
 
914
950
        descend(self.root, u'')
915
951
        return accum
916
952
 
917
 
 
918
953
    def directories(self):
919
954
        """Return (path, entry) pairs for all directories, including the root.
920
955
        """
931
966
        descend(self.root, u'')
932
967
        return accum
933
968
        
934
 
 
935
 
 
936
969
    def __contains__(self, file_id):
937
970
        """True if this entry contains a file with given id.
938
971
 
939
972
        >>> inv = Inventory()
940
973
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
941
 
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
 
974
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
942
975
        >>> '123' in inv
943
976
        True
944
977
        >>> '456' in inv
946
979
        """
947
980
        return file_id in self._byid
948
981
 
949
 
 
950
982
    def __getitem__(self, file_id):
951
983
        """Return the entry for given file_id.
952
984
 
953
985
        >>> inv = Inventory()
954
986
        >>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
955
 
        InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT')
 
987
        InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
956
988
        >>> inv['123123'].name
957
989
        'hello.c'
958
990
        """
959
991
        try:
960
992
            return self._byid[file_id]
961
993
        except KeyError:
962
 
            if file_id == None:
 
994
            if file_id is None:
963
995
                raise BzrError("can't look up file_id None")
964
996
            else:
965
997
                raise BzrError("file_id {%s} not in inventory" % file_id)
966
998
 
967
 
 
968
999
    def get_file_kind(self, file_id):
969
1000
        return self._byid[file_id].kind
970
1001
 
971
1002
    def get_child(self, parent_id, filename):
972
1003
        return self[parent_id].children.get(filename)
973
1004
 
974
 
 
975
1005
    def add(self, entry):
976
1006
        """Add entry to inventory.
977
1007
 
991
1021
        except KeyError:
992
1022
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
993
1023
 
994
 
        if parent.children.has_key(entry.name):
 
1024
        if entry.name in parent.children:
995
1025
            raise BzrError("%s is already versioned" %
996
1026
                    pathjoin(self.id2path(parent.file_id), entry.name))
997
1027
 
999
1029
        parent.children[entry.name] = entry
1000
1030
        return entry
1001
1031
 
1002
 
 
1003
1032
    def add_path(self, relpath, kind, file_id=None, parent_id=None):
1004
1033
        """Add entry from a path.
1005
1034
 
1018
1047
        else:
1019
1048
            parent_path = parts[:-1]
1020
1049
            parent_id = self.path2id(parent_path)
1021
 
            if parent_id == None:
 
1050
            if parent_id is None:
1022
1051
                raise NotVersionedError(path=parent_path)
1023
1052
        ie = make_entry(kind, parts[-1], parent_id, file_id)
1024
1053
        return self.add(ie)
1028
1057
 
1029
1058
        >>> inv = Inventory()
1030
1059
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1031
 
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
 
1060
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
1032
1061
        >>> '123' in inv
1033
1062
        True
1034
1063
        >>> del inv['123']
1044
1073
        if ie.parent_id is not None:
1045
1074
            del self[ie.parent_id].children[ie.name]
1046
1075
 
1047
 
 
1048
1076
    def __eq__(self, other):
1049
1077
        """Compare two sets by comparing their contents.
1050
1078
 
1053
1081
        >>> i1 == i2
1054
1082
        True
1055
1083
        >>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
1056
 
        InventoryFile('123', 'foo', parent_id='TREE_ROOT')
 
1084
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
1057
1085
        >>> i1 == i2
1058
1086
        False
1059
1087
        >>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
1060
 
        InventoryFile('123', 'foo', parent_id='TREE_ROOT')
 
1088
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
1061
1089
        >>> i1 == i2
1062
1090
        True
1063
1091
        """
1064
1092
        if not isinstance(other, Inventory):
1065
1093
            return NotImplemented
1066
1094
 
1067
 
        if len(self._byid) != len(other._byid):
1068
 
            # shortcut: obviously not the same
1069
 
            return False
1070
 
 
1071
1095
        return self._byid == other._byid
1072
1096
 
1073
 
 
1074
1097
    def __ne__(self, other):
1075
1098
        return not self.__eq__(other)
1076
1099
 
1077
 
 
1078
1100
    def __hash__(self):
1079
1101
        raise ValueError('not hashable')
1080
1102
 
1081
1103
    def _iter_file_id_parents(self, file_id):
1082
1104
        """Yield the parents of file_id up to the root."""
1083
 
        while file_id != None:
 
1105
        while file_id is not None:
1084
1106
            try:
1085
1107
                ie = self._byid[file_id]
1086
1108
            except KeyError:
1146
1168
 
1147
1169
        return parent.file_id
1148
1170
 
1149
 
 
1150
1171
    def has_filename(self, names):
1151
1172
        return bool(self.path2id(names))
1152
1173
 
1153
 
 
1154
1174
    def has_id(self, file_id):
1155
1175
        return self._byid.has_key(file_id)
1156
1176
 
1157
 
 
1158
1177
    def rename(self, file_id, new_parent_id, new_name):
1159
1178
        """Move a file within the inventory.
1160
1179
 
1213
1232
 
1214
1233
def is_valid_name(name):
1215
1234
    global _NAME_RE
1216
 
    if _NAME_RE == None:
 
1235
    if _NAME_RE is None:
1217
1236
        _NAME_RE = re.compile(r'^[^/\\]+$')
1218
1237
        
1219
1238
    return bool(_NAME_RE.match(name))