~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Aaron Bentley
  • Date: 2006-04-07 22:46:52 UTC
  • mfrom: (1645 +trunk)
  • mto: This revision was merged to the branch mainline in revision 1727.
  • Revision ID: aaron.bentley@utoronto.ca-20060407224652-4925bc3735b926f8
Merged latest bzr.dev

Show diffs side-by-side

added added

removed removed

Lines of Context:
36
36
 
37
37
import bzrlib
38
38
from bzrlib.osutils import (pumpfile, quotefn, splitpath, joinpath,
39
 
                            appendpath, sha_strings)
 
39
                            pathjoin, sha_strings)
40
40
from bzrlib.trace import mutter
41
41
from bzrlib.errors import (NotVersionedError, InvalidEntryName,
42
42
                           BzrError, BzrCheckError)
79
79
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
80
80
    >>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
81
81
    InventoryFile('2323', 'hello.c', parent_id='123')
82
 
    >>> shouldbe = {0: 'src', 1: os.path.join('src','hello.c')}
 
82
    >>> shouldbe = {0: 'src', 1: pathjoin('src','hello.c')}
83
83
    >>> for ix, j in enumerate(i.iter_entries()):
84
84
    ...   print (j[0] == shouldbe[ix], j[1])
85
85
    ... 
102
102
    >>> i['2326']
103
103
    InventoryFile('2326', 'wibble.c', parent_id='2325')
104
104
    >>> for path, entry in i.iter_entries():
105
 
    ...     print path.replace('\\\\', '/')     # for win32 os.sep
 
105
    ...     print path
106
106
    ...     assert i.path2id(path)
107
107
    ... 
108
108
    src
110
110
    src/hello.c
111
111
    src/wibble
112
112
    src/wibble/wibble.c
113
 
    >>> i.id2path('2326').replace('\\\\', '/')
 
113
    >>> i.id2path('2326')
114
114
    'src/wibble/wibble.c'
115
115
    """
116
116
    
119
119
                 'revision']
120
120
 
121
121
    def _add_text_to_weave(self, new_lines, parents, weave_store, transaction):
122
 
        weave_store.add_text(self.file_id, self.revision, new_lines, parents,
123
 
                             transaction)
 
122
        versionedfile = weave_store.get_weave_or_empty(self.file_id,
 
123
                                                       transaction)
 
124
        versionedfile.add_lines(self.revision, parents, new_lines)
124
125
 
125
126
    def detect_changes(self, old_entry):
126
127
        """Return a (text_modified, meta_modified) from this to old_entry.
151
152
             output_to, reverse=False):
152
153
        """Perform a diff between two entries of the same kind."""
153
154
 
154
 
    def find_previous_heads(self, previous_inventories, entry_weave):
 
155
    def find_previous_heads(self, previous_inventories,
 
156
                            versioned_file_store,
 
157
                            transaction,
 
158
                            entry_vf=None):
155
159
        """Return the revisions and entries that directly preceed this.
156
160
 
157
161
        Returned as a map from revision to inventory entry.
159
163
        This is a map containing the file revisions in all parents
160
164
        for which the file exists, and its revision is not a parent of
161
165
        any other. If the file is new, the set will be empty.
 
166
 
 
167
        :param versioned_file_store: A store where ancestry data on this
 
168
                                     file id can be queried.
 
169
        :param transaction: The transaction that queries to the versioned 
 
170
                            file store should be completed under.
 
171
        :param entry_vf: The entry versioned file, if its already available.
162
172
        """
163
173
        def get_ancestors(weave, entry):
164
 
            return set(map(weave.idx_to_name,
165
 
                           weave.inclusions([weave.lookup(entry.revision)])))
 
174
            return set(weave.get_ancestry(entry.revision))
 
175
        # revision:ie mapping for each ie found in previous_inventories.
 
176
        candidates = {}
 
177
        # revision:ie mapping with one revision for each head.
166
178
        heads = {}
 
179
        # revision: ancestor list for each head
167
180
        head_ancestors = {}
 
181
        # identify candidate head revision ids.
168
182
        for inv in previous_inventories:
169
183
            if self.file_id in inv:
170
184
                ie = inv[self.file_id]
171
185
                assert ie.file_id == self.file_id
172
 
                if ie.revision in heads:
173
 
                    # fixup logic, there was a bug in revision updates.
174
 
                    # with x bit support.
 
186
                if ie.revision in candidates:
 
187
                    # same revision value in two different inventories:
 
188
                    # correct possible inconsistencies:
 
189
                    #     * there was a bug in revision updates with 'x' bit 
 
190
                    #       support.
175
191
                    try:
176
 
                        if heads[ie.revision].executable != ie.executable:
177
 
                            heads[ie.revision].executable = False
 
192
                        if candidates[ie.revision].executable != ie.executable:
 
193
                            candidates[ie.revision].executable = False
178
194
                            ie.executable = False
179
195
                    except AttributeError:
180
196
                        pass
181
 
                    assert heads[ie.revision] == ie
 
197
                    # must now be the same.
 
198
                    assert candidates[ie.revision] == ie
182
199
                else:
183
 
                    # may want to add it.
184
 
                    # may already be covered:
185
 
                    already_present = 0 != len(
186
 
                        [head for head in heads 
187
 
                         if ie.revision in head_ancestors[head]])
188
 
                    if already_present:
189
 
                        # an ancestor of a known head.
190
 
                        continue
191
 
                    # definately a head:
192
 
                    ancestors = get_ancestors(entry_weave, ie)
193
 
                    # may knock something else out:
194
 
                    check_heads = list(heads.keys())
195
 
                    for head in check_heads:
196
 
                        if head in ancestors:
197
 
                            # this head is not really a head
198
 
                            heads.pop(head)
199
 
                    head_ancestors[ie.revision] = ancestors
200
 
                    heads[ie.revision] = ie
 
200
                    # add this revision as a candidate.
 
201
                    candidates[ie.revision] = ie
 
202
 
 
203
        # common case optimisation
 
204
        if len(candidates) == 1:
 
205
            # if there is only one candidate revision found
 
206
            # then we can opening the versioned file to access ancestry:
 
207
            # there cannot be any ancestors to eliminate when there is 
 
208
            # only one revision available.
 
209
            heads[ie.revision] = ie
 
210
            return heads
 
211
 
 
212
        # eliminate ancestors amongst the available candidates:
 
213
        # heads are those that are not an ancestor of any other candidate
 
214
        # - this provides convergence at a per-file level.
 
215
        for ie in candidates.values():
 
216
            # may be an ancestor of a known head:
 
217
            already_present = 0 != len(
 
218
                [head for head in heads 
 
219
                 if ie.revision in head_ancestors[head]])
 
220
            if already_present:
 
221
                # an ancestor of an analyzed candidate.
 
222
                continue
 
223
            # not an ancestor of a known head:
 
224
            # load the versioned file for this file id if needed
 
225
            if entry_vf is None:
 
226
                entry_vf = versioned_file_store.get_weave_or_empty(
 
227
                    self.file_id, transaction)
 
228
            ancestors = get_ancestors(entry_vf, ie)
 
229
            # may knock something else out:
 
230
            check_heads = list(heads.keys())
 
231
            for head in check_heads:
 
232
                if head in ancestors:
 
233
                    # this previously discovered 'head' is not
 
234
                    # really a head - its an ancestor of the newly 
 
235
                    # found head,
 
236
                    heads.pop(head)
 
237
            head_ancestors[ie.revision] = ancestors
 
238
            heads[ie.revision] = ie
201
239
        return heads
202
240
 
203
241
    def get_tar_item(self, root, dp, now, tree):
204
242
        """Get a tarfile item and a file stream for its content."""
205
 
        item = tarfile.TarInfo(os.path.join(root, dp))
 
243
        item = tarfile.TarInfo(pathjoin(root, dp))
206
244
        # TODO: would be cool to actually set it to the timestamp of the
207
245
        # revision it was last changed
208
246
        item.mtime = now
267
305
        
268
306
        This is a template method - implement _put_on_disk in subclasses.
269
307
        """
270
 
        fullpath = appendpath(dest, dp)
 
308
        fullpath = pathjoin(dest, dp)
271
309
        self._put_on_disk(fullpath, tree)
272
310
        mutter("  export {%s} kind %s to %s", self.file_id,
273
311
                self.kind, fullpath)
290
328
 
291
329
        This is a template method, override _check for kind specific
292
330
        tests.
 
331
 
 
332
        :param checker: Check object providing context for the checks; 
 
333
             can be used to find out what parts of the repository have already
 
334
             been checked.
 
335
        :param rev_id: Revision id from which this InventoryEntry was loaded.
 
336
             Not necessarily the last-changed revision for this file.
 
337
        :param inv: Inventory from which the entry was loaded.
 
338
        :param tree: RevisionTree for this entry.
293
339
        """
294
340
        if self.parent_id != None:
295
341
            if not inv.has_id(self.parent_id):
358
404
        """
359
405
        mutter('storing file {%s} in revision {%s}',
360
406
               self.file_id, self.revision)
361
 
        self._add_text_to_weave([], file_parents, weave_store, transaction)
 
407
        self._add_text_to_weave([], file_parents.keys(), weave_store, transaction)
362
408
 
363
409
    def __eq__(self, other):
364
410
        if not isinstance(other, InventoryEntry):
407
453
        # first requested, or preload them if they're already known
408
454
        pass            # nothing to do by default
409
455
 
 
456
    def _forget_tree_state(self):
 
457
        pass
 
458
 
410
459
 
411
460
class RootEntry(InventoryEntry):
412
461
 
470
519
class InventoryFile(InventoryEntry):
471
520
    """A file in an inventory."""
472
521
 
473
 
    def _check(self, checker, rev_id, tree):
 
522
    def _check(self, checker, tree_revision_id, tree):
474
523
        """See InventoryEntry._check"""
475
 
        revision = self.revision
476
 
        t = (self.file_id, revision)
 
524
        t = (self.file_id, self.revision)
477
525
        if t in checker.checked_texts:
478
 
            prev_sha = checker.checked_texts[t] 
 
526
            prev_sha = checker.checked_texts[t]
479
527
            if prev_sha != self.text_sha1:
480
528
                raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
481
 
                                    (self.file_id, rev_id))
 
529
                                    (self.file_id, tree_revision_id))
482
530
            else:
483
531
                checker.repeated_text_cnt += 1
484
532
                return
485
 
        mutter('check version {%s} of {%s}', rev_id, self.file_id)
486
 
        file_lines = tree.get_file_lines(self.file_id)
487
 
        checker.checked_text_cnt += 1 
488
 
        if self.text_size != sum(map(len, file_lines)):
489
 
            raise BzrCheckError('text {%s} wrong size' % self.text_id)
490
 
        if self.text_sha1 != sha_strings(file_lines):
491
 
            raise BzrCheckError('text {%s} wrong sha1' % self.text_id)
 
533
 
 
534
        if self.file_id not in checker.checked_weaves:
 
535
            mutter('check weave {%s}', self.file_id)
 
536
            w = tree.get_weave(self.file_id)
 
537
            # Not passing a progress bar, because it creates a new
 
538
            # progress, which overwrites the current progress,
 
539
            # and doesn't look nice
 
540
            w.check()
 
541
            checker.checked_weaves[self.file_id] = True
 
542
        else:
 
543
            w = tree.get_weave(self.file_id)
 
544
 
 
545
        mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
 
546
        checker.checked_text_cnt += 1
 
547
        # We can't check the length, because Weave doesn't store that
 
548
        # information, and the whole point of looking at the weave's
 
549
        # sha1sum is that we don't have to extract the text.
 
550
        if self.text_sha1 != w.get_sha1(self.revision):
 
551
            raise BzrCheckError('text {%s} version {%s} wrong sha1' 
 
552
                                % (self.file_id, self.revision))
492
553
        checker.checked_texts[t] = self.text_sha1
493
554
 
494
555
    def copy(self):
557
618
        self.text_sha1 = work_tree.get_file_sha1(self.file_id)
558
619
        self.executable = work_tree.is_executable(self.file_id)
559
620
 
 
621
    def _forget_tree_state(self):
 
622
        self.text_sha1 = None
 
623
        self.executable = None
 
624
 
560
625
    def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
561
626
        """See InventoryEntry._snapshot_text."""
562
627
        mutter('storing file {%s} in revision {%s}',
567
632
            and self.text_sha1 == file_parents.values()[0].text_sha1
568
633
            and self.text_size == file_parents.values()[0].text_size):
569
634
            previous_ie = file_parents.values()[0]
570
 
            weave_store.add_identical_text(
571
 
                self.file_id, previous_ie.revision, 
572
 
                self.revision, file_parents, transaction)
 
635
            versionedfile = weave_store.get_weave(self.file_id, transaction)
 
636
            versionedfile.clone_text(self.revision, previous_ie.revision, file_parents.keys())
573
637
        else:
574
638
            new_lines = work_tree.get_file(self.file_id).readlines()
575
 
            self._add_text_to_weave(new_lines, file_parents, weave_store,
 
639
            self._add_text_to_weave(new_lines, file_parents.keys(), weave_store,
576
640
                                    transaction)
577
641
            self.text_sha1 = sha_strings(new_lines)
578
642
            self.text_size = sum(map(len, new_lines))
666
730
        """See InventoryEntry._read_tree_state."""
667
731
        self.symlink_target = work_tree.get_symlink_target(self.file_id)
668
732
 
 
733
    def _forget_tree_state(self):
 
734
        self.symlink_target = None
 
735
 
669
736
    def _unchanged(self, previous_ie):
670
737
        """See InventoryEntry._unchanged."""
671
738
        compatible = super(InventoryLink, self)._unchanged(previous_ie)
712
779
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
713
780
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
714
781
    """
715
 
    def __init__(self, root_id=ROOT_ID):
 
782
    def __init__(self, root_id=ROOT_ID, revision_id=None):
716
783
        """Create or read an inventory.
717
784
 
718
785
        If a working directory is specified, the inventory is read
722
789
        The inventory is created with a default root directory, with
723
790
        an id of None.
724
791
        """
725
 
        # We are letting Branch.initialize() create a unique inventory
 
792
        # We are letting Branch.create() create a unique inventory
726
793
        # root id. Rather than generating a random one here.
727
794
        #if root_id is None:
728
795
        #    root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
729
796
        self.root = RootEntry(root_id)
 
797
        self.revision_id = revision_id
730
798
        self._byid = {self.root.file_id: self.root}
731
799
 
732
800
 
733
801
    def copy(self):
 
802
        # TODO: jam 20051218 Should copy also copy the revision_id?
734
803
        other = Inventory(self.root.file_id)
735
804
        # copy recursively so we know directories will be added before
736
805
        # their children.  There are more efficient ways than this...
764
833
            yield name, ie
765
834
            if ie.kind == 'directory':
766
835
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
767
 
                    yield os.path.join(name, cn), cie
 
836
                    yield pathjoin(name, cn), cie
768
837
 
769
838
 
770
839
    def entries(self):
777
846
            kids = dir_ie.children.items()
778
847
            kids.sort()
779
848
            for name, ie in kids:
780
 
                child_path = os.path.join(dir_path, name)
 
849
                child_path = pathjoin(dir_path, name)
781
850
                accum.append((child_path, ie))
782
851
                if ie.kind == 'directory':
783
852
                    descend(ie, child_path)
797
866
            kids.sort()
798
867
 
799
868
            for name, child_ie in kids:
800
 
                child_path = os.path.join(parent_path, name)
 
869
                child_path = pathjoin(parent_path, name)
801
870
                descend(child_ie, child_path)
802
871
        descend(self.root, u'')
803
872
        return accum
864
933
 
865
934
        if parent.children.has_key(entry.name):
866
935
            raise BzrError("%s is already versioned" %
867
 
                    appendpath(self.id2path(parent.file_id), entry.name))
 
936
                    pathjoin(self.id2path(parent.file_id), entry.name))
868
937
 
869
938
        self._byid[entry.file_id] = entry
870
939
        parent.children[entry.name] = entry
880
949
        from bzrlib.workingtree import gen_file_id
881
950
        
882
951
        parts = bzrlib.osutils.splitpath(relpath)
883
 
        if len(parts) == 0:
884
 
            raise BzrError("cannot re-add root of inventory")
885
952
 
886
953
        if file_id == None:
887
954
            file_id = gen_file_id(relpath)
888
955
 
889
 
        parent_path = parts[:-1]
890
 
        parent_id = self.path2id(parent_path)
891
 
        if parent_id == None:
892
 
            raise NotVersionedError(path=parent_path)
 
956
        if len(parts) == 0:
 
957
            self.root = RootEntry(file_id)
 
958
            self._byid = {self.root.file_id: self.root}
 
959
            return
 
960
        else:
 
961
            parent_path = parts[:-1]
 
962
            parent_id = self.path2id(parent_path)
 
963
            if parent_id == None:
 
964
                raise NotVersionedError(path=parent_path)
893
965
        if kind == 'directory':
894
966
            ie = InventoryDirectory(file_id, parts[-1], parent_id)
895
967
        elif kind == 'file':
915
987
        """
916
988
        ie = self[file_id]
917
989
 
918
 
        assert self[ie.parent_id].children[ie.name] == ie
 
990
        assert ie.parent_id is None or \
 
991
            self[ie.parent_id].children[ie.name] == ie
919
992
        
920
 
        # TODO: Test deleting all children; maybe hoist to a separate
921
 
        # deltree method?
922
 
        if ie.kind == 'directory':
923
 
            for cie in ie.children.values():
924
 
                del self[cie.file_id]
925
 
            del ie.children
926
 
 
927
993
        del self._byid[file_id]
928
 
        del self[ie.parent_id].children[ie.name]
 
994
        if ie.parent_id is not None:
 
995
            del self[ie.parent_id].children[ie.name]
929
996
 
930
997
 
931
998
    def __eq__(self, other):
961
1028
    def __hash__(self):
962
1029
        raise ValueError('not hashable')
963
1030
 
 
1031
    def _iter_file_id_parents(self, file_id):
 
1032
        """Yield the parents of file_id up to the root."""
 
1033
        while file_id != None:
 
1034
            try:
 
1035
                ie = self._byid[file_id]
 
1036
            except KeyError:
 
1037
                raise BzrError("file_id {%s} not found in inventory" % file_id)
 
1038
            yield ie
 
1039
            file_id = ie.parent_id
964
1040
 
965
1041
    def get_idpath(self, file_id):
966
1042
        """Return a list of file_ids for the path to an entry.
971
1047
        root directory as depth 1.
972
1048
        """
973
1049
        p = []
974
 
        while file_id != None:
975
 
            try:
976
 
                ie = self._byid[file_id]
977
 
            except KeyError:
978
 
                raise BzrError("file_id {%s} not found in inventory" % file_id)
979
 
            p.insert(0, ie.file_id)
980
 
            file_id = ie.parent_id
 
1050
        for parent in self._iter_file_id_parents(file_id):
 
1051
            p.insert(0, parent.file_id)
981
1052
        return p
982
1053
 
983
 
 
984
1054
    def id2path(self, file_id):
985
 
        """Return as a list the path to file_id.
 
1055
        """Return as a string the path to file_id.
986
1056
        
987
1057
        >>> i = Inventory()
988
1058
        >>> e = i.add(InventoryDirectory('src-id', 'src', ROOT_ID))
989
1059
        >>> e = i.add(InventoryFile('foo-id', 'foo.c', parent_id='src-id'))
990
 
        >>> print i.id2path('foo-id').replace(os.sep, '/')
 
1060
        >>> print i.id2path('foo-id')
991
1061
        src/foo.c
992
1062
        """
993
1063
        # get all names, skipping root
994
 
        p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
995
 
        return os.sep.join(p)
 
1064
        return '/'.join(reversed(
 
1065
            [parent.name for parent in 
 
1066
             self._iter_file_id_parents(file_id)][:-1]))
996
1067
            
997
 
 
998
 
 
999
1068
    def path2id(self, name):
1000
1069
        """Walk down through directories to return entry of last component.
1001
1070