~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Jelmer Vernooij
  • Date: 2006-06-13 13:24:40 UTC
  • mfrom: (1767 +trunk)
  • mto: (1769.1.1 integration)
  • mto: This revision was merged to the branch mainline in revision 1770.
  • Revision ID: jelmer@samba.org-20060613132440-24e222a86f948f60
[merge] 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
126
127
                 'text_id', 'parent_id', 'children', 'executable', 
127
128
                 'revision']
128
129
 
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()
134
 
 
135
130
    def detect_changes(self, old_entry):
136
131
        """Return a (text_modified, meta_modified) from this to old_entry.
137
132
        
165
160
                            versioned_file_store,
166
161
                            transaction,
167
162
                            entry_vf=None):
168
 
        """Return the revisions and entries that directly preceed this.
 
163
        """Return the revisions and entries that directly precede this.
169
164
 
170
165
        Returned as a map from revision to inventory entry.
171
166
 
410
405
                   self.revision))
411
406
 
412
407
    def snapshot(self, revision, path, previous_entries,
413
 
                 work_tree, weave_store, transaction):
 
408
                 work_tree, commit_builder):
414
409
        """Make a snapshot of this entry which may or may not have changed.
415
410
        
416
411
        This means that all its fields are populated, that it has its
418
413
        """
419
414
        mutter('new parents of %s are %r', path, previous_entries)
420
415
        self._read_tree_state(path, work_tree)
 
416
        # TODO: Where should we determine whether to reuse a
 
417
        # previous revision id or create a new revision? 20060606
421
418
        if len(previous_entries) == 1:
422
419
            # cannot be unchanged unless there is only one parent file rev.
423
420
            parent_ie = previous_entries.values()[0]
426
423
                self.revision = parent_ie.revision
427
424
                return "unchanged"
428
425
        return self._snapshot_into_revision(revision, previous_entries, 
429
 
                                            work_tree, weave_store, transaction)
 
426
                                            work_tree, commit_builder)
430
427
 
431
428
    def _snapshot_into_revision(self, revision, previous_entries, work_tree,
432
 
                                weave_store, transaction):
 
429
                                commit_builder):
433
430
        """Record this revision unconditionally into a store.
434
431
 
435
432
        The entry's last-changed revision property (`revision`) is updated to 
441
438
        """
442
439
        mutter('new revision {%s} for {%s}', revision, self.file_id)
443
440
        self.revision = revision
444
 
        self._snapshot_text(previous_entries, work_tree, weave_store,
445
 
                            transaction)
 
441
        self._snapshot_text(previous_entries, work_tree, commit_builder)
446
442
 
447
 
    def _snapshot_text(self, file_parents, work_tree, weave_store, transaction): 
 
443
    def _snapshot_text(self, file_parents, work_tree, commit_builder): 
448
444
        """Record the 'text' of this entry, whatever form that takes.
449
445
        
450
446
        This default implementation simply adds an empty text.
451
447
        """
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)
 
448
        raise NotImplementedError(self._snapshot_text)
455
449
 
456
450
    def __eq__(self, other):
457
451
        if not isinstance(other, InventoryEntry):
478
472
    def _unchanged(self, previous_ie):
479
473
        """Has this entry changed relative to previous_ie.
480
474
 
481
 
        This method should be overriden in child classes.
 
475
        This method should be overridden in child classes.
482
476
        """
483
477
        compatible = True
484
478
        # different inv parent
563
557
        """See InventoryEntry._put_on_disk."""
564
558
        os.mkdir(fullpath)
565
559
 
 
560
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
 
561
        """See InventoryEntry._snapshot_text."""
 
562
        commit_builder.modified_directory(self.file_id, file_parents)
 
563
 
566
564
 
567
565
class InventoryFile(InventoryEntry):
568
566
    """A file in an inventory."""
670
668
 
671
669
    def _read_tree_state(self, path, work_tree):
672
670
        """See InventoryEntry._read_tree_state."""
673
 
        self.text_sha1 = work_tree.get_file_sha1(self.file_id)
674
 
        self.executable = work_tree.is_executable(self.file_id)
 
671
        self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
 
672
        # FIXME: 20050930 probe for the text size when getting sha1
 
673
        # in _read_tree_state
 
674
        self.executable = work_tree.is_executable(self.file_id, path=path)
675
675
 
676
676
    def __repr__(self):
677
677
        return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s)"
686
686
        self.text_sha1 = None
687
687
        self.executable = None
688
688
 
689
 
    def _snapshot_text(self, file_parents, work_tree, versionedfile_store, transaction):
 
689
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
690
690
        """See InventoryEntry._snapshot_text."""
691
 
        mutter('storing text of file {%s} in revision {%s} into %r',
692
 
               self.file_id, self.revision, versionedfile_store)
693
 
        # special case to avoid diffing on renames or 
694
 
        # reparenting
695
 
        if (len(file_parents) == 1
696
 
            and self.text_sha1 == file_parents.values()[0].text_sha1
697
 
            and self.text_size == file_parents.values()[0].text_size):
698
 
            previous_ie = file_parents.values()[0]
699
 
            versionedfile = versionedfile_store.get_weave(self.file_id, transaction)
700
 
            versionedfile.clone_text(self.revision, previous_ie.revision, file_parents.keys())
701
 
        else:
702
 
            new_lines = work_tree.get_file(self.file_id).readlines()
703
 
            self._add_text_to_weave(new_lines, file_parents.keys(), versionedfile_store,
704
 
                                    transaction)
705
 
            self.text_sha1 = sha_strings(new_lines)
706
 
            self.text_size = sum(map(len, new_lines))
707
 
 
 
691
        def get_content_byte_lines():
 
692
            return work_tree.get_file(self.file_id).readlines()
 
693
        self.text_sha1, self.text_size = commit_builder.modified_file_text(
 
694
            self.file_id, file_parents, get_content_byte_lines, self.text_sha1, self.text_size)
708
695
 
709
696
    def _unchanged(self, previous_ie):
710
697
        """See InventoryEntry._unchanged."""
730
717
        if self.text_sha1 != None or self.text_size != None or self.text_id != None:
731
718
            raise BzrCheckError('symlink {%s} has text in revision {%s}'
732
719
                    % (self.file_id, rev_id))
733
 
        if self.symlink_target == None:
 
720
        if self.symlink_target is None:
734
721
            raise BzrCheckError('symlink {%s} has no target in revision {%s}'
735
722
                    % (self.file_id, rev_id))
736
723
 
804
791
            compatible = False
805
792
        return compatible
806
793
 
 
794
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
 
795
        """See InventoryEntry._snapshot_text."""
 
796
        commit_builder.modified_link(
 
797
            self.file_id, file_parents, self.symlink_target)
 
798
 
807
799
 
808
800
class Inventory(object):
809
801
    """Inventory of versioned files in a tree.
838
830
    May also look up by name:
839
831
 
840
832
    >>> [x[0] for x in inv.iter_entries()]
841
 
    ['hello.c']
 
833
    [u'hello.c']
842
834
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
843
835
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
844
836
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
863
855
        self.revision_id = revision_id
864
856
        self._byid = {self.root.file_id: self.root}
865
857
 
866
 
 
867
858
    def copy(self):
868
859
        # TODO: jam 20051218 Should copy also copy the revision_id?
869
860
        other = Inventory(self.root.file_id)
875
866
            other.add(entry.copy())
876
867
        return other
877
868
 
878
 
 
879
869
    def __iter__(self):
880
870
        return iter(self._byid)
881
871
 
882
 
 
883
872
    def __len__(self):
884
873
        """Returns number of entries."""
885
874
        return len(self._byid)
886
875
 
887
 
 
888
876
    def iter_entries(self, from_dir=None):
889
877
        """Return (path, entry) pairs, in order by name."""
890
 
        if from_dir == None:
891
 
            assert self.root
892
 
            from_dir = self.root
893
 
        elif isinstance(from_dir, basestring):
894
 
            from_dir = self._byid[from_dir]
895
 
            
896
 
        kids = from_dir.children.items()
897
 
        kids.sort()
898
 
        for name, ie in kids:
899
 
            yield name, ie
900
 
            if ie.kind == 'directory':
901
 
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
902
 
                    yield pathjoin(name, cn), cie
903
 
 
 
878
        if from_dir is None:
 
879
            assert self.root
 
880
            from_dir = self.root
 
881
        elif isinstance(from_dir, basestring):
 
882
            from_dir = self._byid[from_dir]
 
883
            
 
884
        # unrolling the recursive called changed the time from
 
885
        # 440ms/663ms (inline/total) to 116ms/116ms
 
886
        children = from_dir.children.items()
 
887
        children.sort()
 
888
        children = collections.deque(children)
 
889
        stack = [(u'', children)]
 
890
        while stack:
 
891
            from_dir_relpath, children = stack[-1]
 
892
 
 
893
            while children:
 
894
                name, ie = children.popleft()
 
895
 
 
896
                # we know that from_dir_relpath never ends in a slash
 
897
                # and 'f' doesn't begin with one, we can do a string op, rather
 
898
                # than the checks of pathjoin(), though this means that all paths
 
899
                # start with a slash
 
900
                path = from_dir_relpath + '/' + name
 
901
 
 
902
                yield path[1:], ie
 
903
 
 
904
                if ie.kind != 'directory':
 
905
                    continue
 
906
 
 
907
                # But do this child first
 
908
                new_children = ie.children.items()
 
909
                new_children.sort()
 
910
                new_children = collections.deque(new_children)
 
911
                stack.append((path, new_children))
 
912
                # Break out of inner loop, so that we start outer loop with child
 
913
                break
 
914
            else:
 
915
                # if we finished all children, pop it off the stack
 
916
                stack.pop()
 
917
 
 
918
    def iter_entries_by_dir(self, from_dir=None):
 
919
        """Iterate over the entries in a directory first order.
 
920
 
 
921
        This returns all entries for a directory before returning
 
922
        the entries for children of a directory. This is not
 
923
        lexicographically sorted order, and is a hybrid between
 
924
        depth-first and breadth-first.
 
925
 
 
926
        :return: This yields (path, entry) pairs
 
927
        """
 
928
        # TODO? Perhaps this should return the from_dir so that the root is
 
929
        # yielded? or maybe an option?
 
930
        if from_dir is None:
 
931
            assert self.root
 
932
            from_dir = self.root
 
933
        elif isinstance(from_dir, basestring):
 
934
            from_dir = self._byid[from_dir]
 
935
            
 
936
        stack = [(u'', from_dir)]
 
937
        while stack:
 
938
            cur_relpath, cur_dir = stack.pop()
 
939
 
 
940
            child_dirs = []
 
941
            for child_name, child_ie in sorted(cur_dir.children.iteritems()):
 
942
 
 
943
                child_relpath = cur_relpath + child_name
 
944
 
 
945
                yield child_relpath, child_ie
 
946
 
 
947
                if child_ie.kind == 'directory':
 
948
                    child_dirs.append((child_relpath+'/', child_ie))
 
949
            stack.extend(reversed(child_dirs))
904
950
 
905
951
    def entries(self):
906
952
        """Return list of (path, ie) for all entries except the root.
920
966
        descend(self.root, u'')
921
967
        return accum
922
968
 
923
 
 
924
969
    def directories(self):
925
970
        """Return (path, entry) pairs for all directories, including the root.
926
971
        """
937
982
        descend(self.root, u'')
938
983
        return accum
939
984
        
940
 
 
941
 
 
942
985
    def __contains__(self, file_id):
943
986
        """True if this entry contains a file with given id.
944
987
 
952
995
        """
953
996
        return file_id in self._byid
954
997
 
955
 
 
956
998
    def __getitem__(self, file_id):
957
999
        """Return the entry for given file_id.
958
1000
 
965
1007
        try:
966
1008
            return self._byid[file_id]
967
1009
        except KeyError:
968
 
            if file_id == None:
 
1010
            if file_id is None:
969
1011
                raise BzrError("can't look up file_id None")
970
1012
            else:
971
1013
                raise BzrError("file_id {%s} not in inventory" % file_id)
972
1014
 
973
 
 
974
1015
    def get_file_kind(self, file_id):
975
1016
        return self._byid[file_id].kind
976
1017
 
977
1018
    def get_child(self, parent_id, filename):
978
1019
        return self[parent_id].children.get(filename)
979
1020
 
980
 
 
981
1021
    def add(self, entry):
982
1022
        """Add entry to inventory.
983
1023
 
1005
1045
        parent.children[entry.name] = entry
1006
1046
        return entry
1007
1047
 
1008
 
 
1009
1048
    def add_path(self, relpath, kind, file_id=None, parent_id=None):
1010
1049
        """Add entry from a path.
1011
1050
 
1024
1063
        else:
1025
1064
            parent_path = parts[:-1]
1026
1065
            parent_id = self.path2id(parent_path)
1027
 
            if parent_id == None:
 
1066
            if parent_id is None:
1028
1067
                raise NotVersionedError(path=parent_path)
1029
1068
        ie = make_entry(kind, parts[-1], parent_id, file_id)
1030
1069
        return self.add(ie)
1050
1089
        if ie.parent_id is not None:
1051
1090
            del self[ie.parent_id].children[ie.name]
1052
1091
 
1053
 
 
1054
1092
    def __eq__(self, other):
1055
1093
        """Compare two sets by comparing their contents.
1056
1094
 
1077
1115
 
1078
1116
        return self._byid == other._byid
1079
1117
 
1080
 
 
1081
1118
    def __ne__(self, other):
1082
1119
        return not self.__eq__(other)
1083
1120
 
1084
 
 
1085
1121
    def __hash__(self):
1086
1122
        raise ValueError('not hashable')
1087
1123
 
1151
1187
 
1152
1188
        return parent.file_id
1153
1189
 
1154
 
 
1155
1190
    def has_filename(self, names):
1156
1191
        return bool(self.path2id(names))
1157
1192
 
1158
 
 
1159
1193
    def has_id(self, file_id):
1160
1194
        return self._byid.has_key(file_id)
1161
1195
 
1162
 
 
1163
1196
    def rename(self, file_id, new_parent_id, new_name):
1164
1197
        """Move a file within the inventory.
1165
1198
 
1215
1248
 
1216
1249
def is_valid_name(name):
1217
1250
    global _NAME_RE
1218
 
    if _NAME_RE == None:
 
1251
    if _NAME_RE is None:
1219
1252
        _NAME_RE = re.compile(r'^[^/\\]+$')
1220
1253
        
1221
1254
    return bool(_NAME_RE.match(name))