~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Aaron Bentley
  • Date: 2006-03-19 18:49:10 UTC
  • mto: This revision was merged to the branch mainline in revision 1644.
  • Revision ID: aaron.bentley@utoronto.ca-20060319184910-44558c38ac2077cf
Progress indicator for tree builts

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006 Canonical Ltd
 
1
# (C) 2005 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
37
37
import bzrlib
38
38
from bzrlib.osutils import (pumpfile, quotefn, splitpath, joinpath,
39
39
                            pathjoin, sha_strings)
 
40
from bzrlib.trace import mutter
40
41
from bzrlib.errors import (NotVersionedError, InvalidEntryName,
41
 
                           BzrError, BzrCheckError, BinaryFile)
42
 
from bzrlib.trace import mutter
 
42
                           BzrError, BzrCheckError)
43
43
 
44
44
 
45
45
class InventoryEntry(object):
119
119
                 'revision']
120
120
 
121
121
    def _add_text_to_weave(self, new_lines, parents, weave_store, transaction):
122
 
        versionedfile = weave_store.get_weave_or_empty(self.file_id,
123
 
                                                       transaction)
 
122
        versionedfile = weave_store.get_weave(self.file_id, transaction)
124
123
        versionedfile.add_lines(self.revision, parents, new_lines)
125
 
        versionedfile.clear_cache()
126
124
 
127
125
    def detect_changes(self, old_entry):
128
126
        """Return a (text_modified, meta_modified) from this to old_entry.
153
151
             output_to, reverse=False):
154
152
        """Perform a diff between two entries of the same kind."""
155
153
 
156
 
    def find_previous_heads(self, previous_inventories,
157
 
                            versioned_file_store,
158
 
                            transaction,
159
 
                            entry_vf=None):
 
154
    def find_previous_heads(self, previous_inventories, entry_weave):
160
155
        """Return the revisions and entries that directly preceed this.
161
156
 
162
157
        Returned as a map from revision to inventory entry.
164
159
        This is a map containing the file revisions in all parents
165
160
        for which the file exists, and its revision is not a parent of
166
161
        any other. If the file is new, the set will be empty.
167
 
 
168
 
        :param versioned_file_store: A store where ancestry data on this
169
 
                                     file id can be queried.
170
 
        :param transaction: The transaction that queries to the versioned 
171
 
                            file store should be completed under.
172
 
        :param entry_vf: The entry versioned file, if its already available.
173
162
        """
174
163
        def get_ancestors(weave, entry):
175
164
            return set(weave.get_ancestry(entry.revision))
176
 
        # revision:ie mapping for each ie found in previous_inventories.
177
 
        candidates = {}
178
 
        # revision:ie mapping with one revision for each head.
179
165
        heads = {}
180
 
        # revision: ancestor list for each head
181
166
        head_ancestors = {}
182
 
        # identify candidate head revision ids.
183
167
        for inv in previous_inventories:
184
168
            if self.file_id in inv:
185
169
                ie = inv[self.file_id]
186
170
                assert ie.file_id == self.file_id
187
 
                if ie.revision in candidates:
188
 
                    # same revision value in two different inventories:
189
 
                    # correct possible inconsistencies:
190
 
                    #     * there was a bug in revision updates with 'x' bit 
191
 
                    #       support.
 
171
                if ie.revision in heads:
 
172
                    # fixup logic, there was a bug in revision updates.
 
173
                    # with x bit support.
192
174
                    try:
193
 
                        if candidates[ie.revision].executable != ie.executable:
194
 
                            candidates[ie.revision].executable = False
 
175
                        if heads[ie.revision].executable != ie.executable:
 
176
                            heads[ie.revision].executable = False
195
177
                            ie.executable = False
196
178
                    except AttributeError:
197
179
                        pass
198
 
                    # must now be the same.
199
 
                    assert candidates[ie.revision] == ie
 
180
                    assert heads[ie.revision] == ie
200
181
                else:
201
 
                    # add this revision as a candidate.
202
 
                    candidates[ie.revision] = ie
203
 
 
204
 
        # common case optimisation
205
 
        if len(candidates) == 1:
206
 
            # if there is only one candidate revision found
207
 
            # then we can opening the versioned file to access ancestry:
208
 
            # there cannot be any ancestors to eliminate when there is 
209
 
            # only one revision available.
210
 
            heads[ie.revision] = ie
211
 
            return heads
212
 
 
213
 
        # eliminate ancestors amongst the available candidates:
214
 
        # heads are those that are not an ancestor of any other candidate
215
 
        # - this provides convergence at a per-file level.
216
 
        for ie in candidates.values():
217
 
            # may be an ancestor of a known head:
218
 
            already_present = 0 != len(
219
 
                [head for head in heads 
220
 
                 if ie.revision in head_ancestors[head]])
221
 
            if already_present:
222
 
                # an ancestor of an analyzed candidate.
223
 
                continue
224
 
            # not an ancestor of a known head:
225
 
            # load the versioned file for this file id if needed
226
 
            if entry_vf is None:
227
 
                entry_vf = versioned_file_store.get_weave_or_empty(
228
 
                    self.file_id, transaction)
229
 
            ancestors = get_ancestors(entry_vf, ie)
230
 
            # may knock something else out:
231
 
            check_heads = list(heads.keys())
232
 
            for head in check_heads:
233
 
                if head in ancestors:
234
 
                    # this previously discovered 'head' is not
235
 
                    # really a head - its an ancestor of the newly 
236
 
                    # found head,
237
 
                    heads.pop(head)
238
 
            head_ancestors[ie.revision] = ancestors
239
 
            heads[ie.revision] = ie
 
182
                    # may want to add it.
 
183
                    # may already be covered:
 
184
                    already_present = 0 != len(
 
185
                        [head for head in heads 
 
186
                         if ie.revision in head_ancestors[head]])
 
187
                    if already_present:
 
188
                        # an ancestor of a known head.
 
189
                        continue
 
190
                    # definately a head:
 
191
                    ancestors = get_ancestors(entry_weave, ie)
 
192
                    # may knock something else out:
 
193
                    check_heads = list(heads.keys())
 
194
                    for head in check_heads:
 
195
                        if head in ancestors:
 
196
                            # this head is not really a head
 
197
                            heads.pop(head)
 
198
                    head_ancestors[ie.revision] = ancestors
 
199
                    heads[ie.revision] = ie
240
200
        return heads
241
201
 
242
202
    def get_tar_item(self, root, dp, now, tree):
329
289
 
330
290
        This is a template method, override _check for kind specific
331
291
        tests.
332
 
 
333
 
        :param checker: Check object providing context for the checks; 
334
 
             can be used to find out what parts of the repository have already
335
 
             been checked.
336
 
        :param rev_id: Revision id from which this InventoryEntry was loaded.
337
 
             Not necessarily the last-changed revision for this file.
338
 
        :param inv: Inventory from which the entry was loaded.
339
 
        :param tree: RevisionTree for this entry.
340
292
        """
341
293
        if self.parent_id != None:
342
294
            if not inv.has_id(self.parent_id):
349
301
        raise BzrCheckError('unknown entry kind %r in revision {%s}' % 
350
302
                            (self.kind, rev_id))
351
303
 
 
304
 
352
305
    def copy(self):
353
306
        """Clone this inventory entry."""
354
307
        raise NotImplementedError
355
308
 
356
 
    def _describe_snapshot_change(self, previous_entries):
357
 
        """Describe how this entry will have changed in a new commit.
358
 
 
359
 
        :param previous_entries: Dictionary from revision_id to inventory entry.
360
 
 
361
 
        :returns: One-word description: "merged", "added", "renamed", "modified".
362
 
        """
363
 
        # XXX: This assumes that the file *has* changed -- it should probably
364
 
        # be fused with whatever does that detection.  Why not just a single
365
 
        # thing to compare the entries?
366
 
        #
367
 
        # TODO: Return some kind of object describing all the possible
368
 
        # dimensions that can change, not just a string.  That can then give
369
 
        # both old and new names for renames, etc.
370
 
        #
 
309
    def _get_snapshot_change(self, previous_entries):
371
310
        if len(previous_entries) > 1:
372
311
            return 'merged'
373
312
        elif len(previous_entries) == 0:
374
313
            return 'added'
375
 
        the_parent, = previous_entries.values()
376
 
        if self.parent_id != the_parent.parent_id:
377
 
            # actually, moved to another directory
378
 
            return 'renamed'
379
 
        elif self.name != the_parent.name:
380
 
            return 'renamed'
381
 
        return 'modified'
 
314
        else:
 
315
            return 'modified/renamed/reparented'
382
316
 
383
317
    def __repr__(self):
384
318
        return ("%s(%r, %r, parent_id=%r)"
403
337
                mutter("found unchanged entry")
404
338
                self.revision = parent_ie.revision
405
339
                return "unchanged"
406
 
        return self._snapshot_into_revision(revision, previous_entries, 
407
 
                                            work_tree, weave_store, transaction)
408
 
 
409
 
    def _snapshot_into_revision(self, revision, previous_entries, work_tree,
410
 
                                weave_store, transaction):
411
 
        """Record this revision unconditionally into a store.
412
 
 
413
 
        The entry's last-changed revision property (`revision`) is updated to 
414
 
        that of the new revision.
415
 
        
416
 
        :param revision: id of the new revision that is being recorded.
417
 
 
418
 
        :returns: String description of the commit (e.g. "merged", "modified"), etc.
419
 
        """
420
 
        mutter('new revision {%s} for {%s}', revision, self.file_id)
 
340
        return self.snapshot_revision(revision, previous_entries, 
 
341
                                      work_tree, weave_store, transaction)
 
342
 
 
343
    def snapshot_revision(self, revision, previous_entries, work_tree,
 
344
                          weave_store, transaction):
 
345
        """Record this revision unconditionally."""
 
346
        mutter('new revision for {%s}', self.file_id)
421
347
        self.revision = revision
422
 
        change = self._describe_snapshot_change(previous_entries)
 
348
        change = self._get_snapshot_change(previous_entries)
423
349
        self._snapshot_text(previous_entries, work_tree, weave_store,
424
350
                            transaction)
425
351
        return change
546
472
class InventoryFile(InventoryEntry):
547
473
    """A file in an inventory."""
548
474
 
549
 
    def _check(self, checker, tree_revision_id, tree):
 
475
    def _check(self, checker, rev_id, tree):
550
476
        """See InventoryEntry._check"""
551
 
        t = (self.file_id, self.revision)
 
477
        revision = self.revision
 
478
        t = (self.file_id, revision)
552
479
        if t in checker.checked_texts:
553
 
            prev_sha = checker.checked_texts[t]
 
480
            prev_sha = checker.checked_texts[t] 
554
481
            if prev_sha != self.text_sha1:
555
482
                raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
556
 
                                    (self.file_id, tree_revision_id))
 
483
                                    (self.file_id, rev_id))
557
484
            else:
558
485
                checker.repeated_text_cnt += 1
559
486
                return
569
496
        else:
570
497
            w = tree.get_weave(self.file_id)
571
498
 
572
 
        mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
573
 
        checker.checked_text_cnt += 1
 
499
        mutter('check version {%s} of {%s}', rev_id, self.file_id)
 
500
        checker.checked_text_cnt += 1 
574
501
        # We can't check the length, because Weave doesn't store that
575
502
        # information, and the whole point of looking at the weave's
576
503
        # sha1sum is that we don't have to extract the text.
599
526
    def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
600
527
             output_to, reverse=False):
601
528
        """See InventoryEntry._diff."""
602
 
        try:
603
 
            from_text = tree.get_file(self.file_id).readlines()
604
 
            if to_entry:
605
 
                to_text = to_tree.get_file(to_entry.file_id).readlines()
606
 
            else:
607
 
                to_text = []
608
 
            if not reverse:
609
 
                text_diff(from_label, from_text,
610
 
                          to_label, to_text, output_to)
611
 
            else:
612
 
                text_diff(to_label, to_text,
613
 
                          from_label, from_text, output_to)
614
 
        except BinaryFile:
615
 
            if reverse:
616
 
                label_pair = (to_label, from_label)
617
 
            else:
618
 
                label_pair = (from_label, to_label)
619
 
            print >> output_to, "Binary files %s and %s differ" % label_pair
 
529
        from_text = tree.get_file(self.file_id).readlines()
 
530
        if to_entry:
 
531
            to_text = to_tree.get_file(to_entry.file_id).readlines()
 
532
        else:
 
533
            to_text = []
 
534
        if not reverse:
 
535
            text_diff(from_label, from_text,
 
536
                      to_label, to_text, output_to)
 
537
        else:
 
538
            text_diff(to_label, to_text,
 
539
                      from_label, from_text, output_to)
620
540
 
621
541
    def has_text(self):
622
542
        """See InventoryEntry.has_text."""
813
733
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
814
734
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
815
735
    """
816
 
    def __init__(self, root_id=ROOT_ID, revision_id=None):
 
736
    def __init__(self, root_id=ROOT_ID):
817
737
        """Create or read an inventory.
818
738
 
819
739
        If a working directory is specified, the inventory is read
828
748
        #if root_id is None:
829
749
        #    root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
830
750
        self.root = RootEntry(root_id)
831
 
        self.revision_id = revision_id
832
751
        self._byid = {self.root.file_id: self.root}
833
752
 
834
753
 
835
754
    def copy(self):
836
 
        # TODO: jam 20051218 Should copy also copy the revision_id?
837
755
        other = Inventory(self.root.file_id)
838
756
        # copy recursively so we know directories will be added before
839
757
        # their children.  There are more efficient ways than this...
1062
980
    def __hash__(self):
1063
981
        raise ValueError('not hashable')
1064
982
 
1065
 
    def _iter_file_id_parents(self, file_id):
1066
 
        """Yield the parents of file_id up to the root."""
1067
 
        while file_id != None:
1068
 
            try:
1069
 
                ie = self._byid[file_id]
1070
 
            except KeyError:
1071
 
                raise BzrError("file_id {%s} not found in inventory" % file_id)
1072
 
            yield ie
1073
 
            file_id = ie.parent_id
1074
983
 
1075
984
    def get_idpath(self, file_id):
1076
985
        """Return a list of file_ids for the path to an entry.
1081
990
        root directory as depth 1.
1082
991
        """
1083
992
        p = []
1084
 
        for parent in self._iter_file_id_parents(file_id):
1085
 
            p.insert(0, parent.file_id)
 
993
        while file_id != None:
 
994
            try:
 
995
                ie = self._byid[file_id]
 
996
            except KeyError:
 
997
                raise BzrError("file_id {%s} not found in inventory" % file_id)
 
998
            p.insert(0, ie.file_id)
 
999
            file_id = ie.parent_id
1086
1000
        return p
1087
1001
 
 
1002
 
1088
1003
    def id2path(self, file_id):
1089
 
        """Return as a string the path to file_id.
 
1004
        """Return as a list the path to file_id.
1090
1005
        
1091
1006
        >>> i = Inventory()
1092
1007
        >>> e = i.add(InventoryDirectory('src-id', 'src', ROOT_ID))
1095
1010
        src/foo.c
1096
1011
        """
1097
1012
        # get all names, skipping root
1098
 
        return '/'.join(reversed(
1099
 
            [parent.name for parent in 
1100
 
             self._iter_file_id_parents(file_id)][:-1]))
 
1013
        p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
 
1014
        if p:
 
1015
            return pathjoin(*p)
 
1016
        else:
 
1017
            return ''
1101
1018
            
 
1019
 
 
1020
 
1102
1021
    def path2id(self, name):
1103
1022
        """Walk down through directories to return entry of last component.
1104
1023