~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Michael Hudson
  • Date: 2008-07-17 00:43:32 UTC
  • mto: This revision was merged to the branch mainline in revision 3552.
  • Revision ID: michael.hudson@canonical.com-20080717004332-aqxut6zkoi0hiyl7
the two character fix

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006 Canonical Ltd
 
1
# Copyright (C) 2005, 2006, 2007 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
50
50
    BzrCheckError,
51
51
    BzrError,
52
52
    )
 
53
from bzrlib.symbol_versioning import deprecated_method
53
54
from bzrlib.trace import mutter
54
55
 
55
56
 
94
95
    >>> for ix, j in enumerate(i.iter_entries()):
95
96
    ...   print (j[0] == shouldbe[ix], j[1])
96
97
    ... 
97
 
    (True, InventoryDirectory('TREE_ROOT', '', parent_id=None, revision=None))
 
98
    (True, InventoryDirectory('TREE_ROOT', u'', parent_id=None, revision=None))
98
99
    (True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
99
100
    (True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
100
 
    >>> i.add(InventoryFile('2323', 'bye.c', '123'))
101
 
    Traceback (most recent call last):
102
 
    ...
103
 
    BzrError: inventory already contains entry with id {2323}
104
101
    >>> i.add(InventoryFile('2324', 'bye.c', '123'))
105
102
    InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
106
103
    >>> i.add(InventoryDirectory('2325', 'wibble', '123'))
115
112
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
116
113
    >>> for path, entry in i.iter_entries():
117
114
    ...     print path
118
 
    ...     assert i.path2id(path)
119
115
    ... 
120
116
    <BLANKLINE>
121
117
    src
145
141
        """
146
142
        return False, False
147
143
 
148
 
    def diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
149
 
             output_to, reverse=False):
150
 
        """Perform a diff from this to to_entry.
151
 
 
152
 
        text_diff will be used for textual difference calculation.
153
 
        This is a template method, override _diff in child classes.
154
 
        """
155
 
        self._read_tree_state(tree.id2path(self.file_id), tree)
156
 
        if to_entry:
157
 
            # cannot diff from one kind to another - you must do a removal
158
 
            # and an addif they do not match.
159
 
            assert self.kind == to_entry.kind
160
 
            to_entry._read_tree_state(to_tree.id2path(to_entry.file_id),
161
 
                                      to_tree)
162
 
        self._diff(text_diff, from_label, tree, to_label, to_entry, to_tree,
163
 
                   output_to, reverse)
164
 
 
165
144
    def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
166
145
             output_to, reverse=False):
167
146
        """Perform a diff between two entries of the same kind."""
168
 
 
169
 
    def find_previous_heads(self, previous_inventories,
170
 
                            versioned_file_store,
171
 
                            transaction,
172
 
                            entry_vf=None):
173
 
        """Return the revisions and entries that directly precede this.
174
 
 
175
 
        Returned as a map from revision to inventory entry.
176
 
 
177
 
        This is a map containing the file revisions in all parents
178
 
        for which the file exists, and its revision is not a parent of
179
 
        any other. If the file is new, the set will be empty.
180
 
 
181
 
        :param versioned_file_store: A store where ancestry data on this
182
 
                                     file id can be queried.
183
 
        :param transaction: The transaction that queries to the versioned 
184
 
                            file store should be completed under.
185
 
        :param entry_vf: The entry versioned file, if its already available.
 
147
    
 
148
    def parent_candidates(self, previous_inventories):
 
149
        """Find possible per-file graph parents.
 
150
 
 
151
        This is currently defined by:
 
152
         - Select the last changed revision in the parent inventory.
 
153
         - Do deal with a short lived bug in bzr 0.8's development two entries
 
154
           that have the same last changed but different 'x' bit settings are
 
155
           changed in-place.
186
156
        """
187
 
        def get_ancestors(weave, entry):
188
 
            return set(weave.get_ancestry(entry.revision))
189
157
        # revision:ie mapping for each ie found in previous_inventories.
190
158
        candidates = {}
191
 
        # revision:ie mapping with one revision for each head.
192
 
        heads = {}
193
 
        # revision: ancestor list for each head
194
 
        head_ancestors = {}
195
159
        # identify candidate head revision ids.
196
160
        for inv in previous_inventories:
197
161
            if self.file_id in inv:
198
162
                ie = inv[self.file_id]
199
 
                assert ie.file_id == self.file_id
200
163
                if ie.revision in candidates:
201
164
                    # same revision value in two different inventories:
202
165
                    # correct possible inconsistencies:
208
171
                            ie.executable = False
209
172
                    except AttributeError:
210
173
                        pass
211
 
                    # must now be the same.
212
 
                    assert candidates[ie.revision] == ie
213
174
                else:
214
175
                    # add this revision as a candidate.
215
176
                    candidates[ie.revision] = ie
216
 
 
217
 
        # common case optimisation
218
 
        if len(candidates) == 1:
219
 
            # if there is only one candidate revision found
220
 
            # then we can opening the versioned file to access ancestry:
221
 
            # there cannot be any ancestors to eliminate when there is 
222
 
            # only one revision available.
223
 
            heads[ie.revision] = ie
224
 
            return heads
225
 
 
226
 
        # eliminate ancestors amongst the available candidates:
227
 
        # heads are those that are not an ancestor of any other candidate
228
 
        # - this provides convergence at a per-file level.
229
 
        for ie in candidates.values():
230
 
            # may be an ancestor of a known head:
231
 
            already_present = 0 != len(
232
 
                [head for head in heads 
233
 
                 if ie.revision in head_ancestors[head]])
234
 
            if already_present:
235
 
                # an ancestor of an analyzed candidate.
236
 
                continue
237
 
            # not an ancestor of a known head:
238
 
            # load the versioned file for this file id if needed
239
 
            if entry_vf is None:
240
 
                entry_vf = versioned_file_store.get_weave_or_empty(
241
 
                    self.file_id, transaction)
242
 
            ancestors = get_ancestors(entry_vf, ie)
243
 
            # may knock something else out:
244
 
            check_heads = list(heads.keys())
245
 
            for head in check_heads:
246
 
                if head in ancestors:
247
 
                    # this previously discovered 'head' is not
248
 
                    # really a head - its an ancestor of the newly 
249
 
                    # found head,
250
 
                    heads.pop(head)
251
 
            head_ancestors[ie.revision] = ancestors
252
 
            heads[ie.revision] = ie
253
 
        return heads
 
177
        return candidates
254
178
 
255
179
    def get_tar_item(self, root, dp, now, tree):
256
180
        """Get a tarfile item and a file stream for its content."""
287
211
        Traceback (most recent call last):
288
212
        InvalidEntryName: Invalid entry name: src/hello.c
289
213
        """
290
 
        assert isinstance(name, basestring), name
291
214
        if '/' in name or '\\' in name:
292
215
            raise errors.InvalidEntryName(name=name)
293
216
        self.executable = False
299
222
        self.text_id = text_id
300
223
        self.parent_id = parent_id
301
224
        self.symlink_target = None
 
225
        self.reference_revision = None
302
226
 
303
227
    def kind_character(self):
304
228
        """Return a short kind indicator useful for appending to names."""
333
257
 
334
258
    @staticmethod
335
259
    def versionable_kind(kind):
336
 
        return (kind in ('file', 'directory', 'symlink'))
 
260
        return (kind in ('file', 'directory', 'symlink', 'tree-reference'))
337
261
 
338
262
    def check(self, checker, rev_id, inv, tree):
339
263
        """Check this inventory entry is intact.
414
338
                   self.parent_id,
415
339
                   self.revision))
416
340
 
417
 
    def snapshot(self, revision, path, previous_entries,
418
 
                 work_tree, commit_builder):
419
 
        """Make a snapshot of this entry which may or may not have changed.
420
 
        
421
 
        This means that all its fields are populated, that it has its
422
 
        text stored in the text store or weave.
423
 
        """
424
 
        # mutter('new parents of %s are %r', path, previous_entries)
425
 
        self._read_tree_state(path, work_tree)
426
 
        # TODO: Where should we determine whether to reuse a
427
 
        # previous revision id or create a new revision? 20060606
428
 
        if len(previous_entries) == 1:
429
 
            # cannot be unchanged unless there is only one parent file rev.
430
 
            parent_ie = previous_entries.values()[0]
431
 
            if self._unchanged(parent_ie):
432
 
                # mutter("found unchanged entry")
433
 
                self.revision = parent_ie.revision
434
 
                return "unchanged"
435
 
        return self._snapshot_into_revision(revision, previous_entries, 
436
 
                                            work_tree, commit_builder)
437
 
 
438
 
    def _snapshot_into_revision(self, revision, previous_entries, work_tree,
439
 
                                commit_builder):
440
 
        """Record this revision unconditionally into a store.
441
 
 
442
 
        The entry's last-changed revision property (`revision`) is updated to 
443
 
        that of the new revision.
444
 
        
445
 
        :param revision: id of the new revision that is being recorded.
446
 
 
447
 
        :returns: String description of the commit (e.g. "merged", "modified"), etc.
448
 
        """
449
 
        # mutter('new revision {%s} for {%s}', revision, self.file_id)
450
 
        self.revision = revision
451
 
        self._snapshot_text(previous_entries, work_tree, commit_builder)
452
 
 
453
 
    def _snapshot_text(self, file_parents, work_tree, commit_builder): 
454
 
        """Record the 'text' of this entry, whatever form that takes.
455
 
        
456
 
        This default implementation simply adds an empty text.
457
 
        """
458
 
        raise NotImplementedError(self._snapshot_text)
459
 
 
460
341
    def __eq__(self, other):
461
342
        if not isinstance(other, InventoryEntry):
462
343
            return NotImplemented
471
352
                and (self.kind == other.kind)
472
353
                and (self.revision == other.revision)
473
354
                and (self.executable == other.executable)
 
355
                and (self.reference_revision == other.reference_revision)
474
356
                )
475
357
 
476
358
    def __ne__(self, other):
491
373
        # renamed
492
374
        elif previous_ie.name != self.name:
493
375
            compatible = False
 
376
        elif previous_ie.kind != self.kind:
 
377
            compatible = False
494
378
        return compatible
495
379
 
496
380
    def _read_tree_state(self, path, work_tree):
511
395
class RootEntry(InventoryEntry):
512
396
 
513
397
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
514
 
                 'text_id', 'parent_id', 'children', 'executable', 
515
 
                 'revision', 'symlink_target']
 
398
                 'text_id', 'parent_id', 'children', 'executable',
 
399
                 'revision', 'symlink_target', 'reference_revision']
516
400
 
517
401
    def _check(self, checker, rev_id, tree):
518
402
        """See InventoryEntry._check"""
540
424
    """A directory in an inventory."""
541
425
 
542
426
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
543
 
                 'text_id', 'parent_id', 'children', 'executable', 
544
 
                 'revision', 'symlink_target']
 
427
                 'text_id', 'parent_id', 'children', 'executable',
 
428
                 'revision', 'symlink_target', 'reference_revision']
545
429
 
546
430
    def _check(self, checker, rev_id, tree):
547
431
        """See InventoryEntry._check"""
578
462
        """See InventoryEntry._put_on_disk."""
579
463
        os.mkdir(fullpath)
580
464
 
581
 
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
582
 
        """See InventoryEntry._snapshot_text."""
583
 
        commit_builder.modified_directory(self.file_id, file_parents)
584
 
 
585
465
 
586
466
class InventoryFile(InventoryEntry):
587
467
    """A file in an inventory."""
588
468
 
589
469
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
590
 
                 'text_id', 'parent_id', 'children', 'executable', 
591
 
                 'revision', 'symlink_target']
 
470
                 'text_id', 'parent_id', 'children', 'executable',
 
471
                 'revision', 'symlink_target', 'reference_revision']
592
472
 
593
473
    def _check(self, checker, tree_revision_id, tree):
594
474
        """See InventoryEntry._check"""
595
 
        t = (self.file_id, self.revision)
596
 
        if t in checker.checked_texts:
597
 
            prev_sha = checker.checked_texts[t]
 
475
        key = (self.file_id, self.revision)
 
476
        if key in checker.checked_texts:
 
477
            prev_sha = checker.checked_texts[key]
598
478
            if prev_sha != self.text_sha1:
599
 
                raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
600
 
                                    (self.file_id, tree_revision_id))
 
479
                raise BzrCheckError(
 
480
                    'mismatched sha1 on {%s} in {%s} (%s != %s) %r' %
 
481
                    (self.file_id, tree_revision_id, prev_sha, self.text_sha1,
 
482
                     t))
601
483
            else:
602
484
                checker.repeated_text_cnt += 1
603
485
                return
604
486
 
605
 
        if self.file_id not in checker.checked_weaves:
606
 
            mutter('check weave {%s}', self.file_id)
607
 
            w = tree.get_weave(self.file_id)
608
 
            # Not passing a progress bar, because it creates a new
609
 
            # progress, which overwrites the current progress,
610
 
            # and doesn't look nice
611
 
            w.check()
612
 
            checker.checked_weaves[self.file_id] = True
613
 
        else:
614
 
            w = tree.get_weave(self.file_id)
615
 
 
616
487
        mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
617
488
        checker.checked_text_cnt += 1
618
489
        # We can't check the length, because Weave doesn't store that
619
490
        # information, and the whole point of looking at the weave's
620
491
        # sha1sum is that we don't have to extract the text.
621
 
        if self.text_sha1 != w.get_sha1(self.revision):
622
 
            raise BzrCheckError('text {%s} version {%s} wrong sha1' 
623
 
                                % (self.file_id, self.revision))
624
 
        checker.checked_texts[t] = self.text_sha1
 
492
        if (self.text_sha1 != tree._repository.texts.get_sha1s([key])[key]):
 
493
            raise BzrCheckError('text {%s} version {%s} wrong sha1' % key)
 
494
        checker.checked_texts[key] = self.text_sha1
625
495
 
626
496
    def copy(self):
627
497
        other = InventoryFile(self.file_id, self.name, self.parent_id)
634
504
 
635
505
    def detect_changes(self, old_entry):
636
506
        """See InventoryEntry.detect_changes."""
637
 
        assert self.text_sha1 is not None
638
 
        assert old_entry.text_sha1 is not None
639
507
        text_modified = (self.text_sha1 != old_entry.text_sha1)
640
508
        meta_modified = (self.executable != old_entry.executable)
641
509
        return text_modified, meta_modified
643
511
    def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
644
512
             output_to, reverse=False):
645
513
        """See InventoryEntry._diff."""
646
 
        try:
647
 
            from_text = tree.get_file(self.file_id).readlines()
648
 
            if to_entry:
649
 
                to_text = to_tree.get_file(to_entry.file_id).readlines()
650
 
            else:
651
 
                to_text = []
652
 
            if not reverse:
653
 
                text_diff(from_label, from_text,
654
 
                          to_label, to_text, output_to)
655
 
            else:
656
 
                text_diff(to_label, to_text,
657
 
                          from_label, from_text, output_to)
658
 
        except errors.BinaryFile:
659
 
            if reverse:
660
 
                label_pair = (to_label, from_label)
661
 
            else:
662
 
                label_pair = (from_label, to_label)
663
 
            print >> output_to, "Binary files %s and %s differ" % label_pair
 
514
        from bzrlib.diff import DiffText
 
515
        from_file_id = self.file_id
 
516
        if to_entry:
 
517
            to_file_id = to_entry.file_id
 
518
        else:
 
519
            to_file_id = None
 
520
        if reverse:
 
521
            to_file_id, from_file_id = from_file_id, to_file_id
 
522
            tree, to_tree = to_tree, tree
 
523
            from_label, to_label = to_label, from_label
 
524
        differ = DiffText(tree, to_tree, output_to, 'utf-8', '', '',
 
525
                          text_diff)
 
526
        return differ.diff_text(from_file_id, to_file_id, from_label, to_label)
664
527
 
665
528
    def has_text(self):
666
529
        """See InventoryEntry.has_text."""
710
573
    def _forget_tree_state(self):
711
574
        self.text_sha1 = None
712
575
 
713
 
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
714
 
        """See InventoryEntry._snapshot_text."""
715
 
        def get_content_byte_lines():
716
 
            return work_tree.get_file(self.file_id).readlines()
717
 
        self.text_sha1, self.text_size = commit_builder.modified_file_text(
718
 
            self.file_id, file_parents, get_content_byte_lines, self.text_sha1, self.text_size)
719
 
 
720
576
    def _unchanged(self, previous_ie):
721
577
        """See InventoryEntry._unchanged."""
722
578
        compatible = super(InventoryFile, self)._unchanged(previous_ie)
735
591
    """A file in an inventory."""
736
592
 
737
593
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
738
 
                 'text_id', 'parent_id', 'children', 'executable', 
739
 
                 'revision', 'symlink_target']
 
594
                 'text_id', 'parent_id', 'children', 'executable',
 
595
                 'revision', 'symlink_target', 'reference_revision']
740
596
 
741
597
    def _check(self, checker, rev_id, tree):
742
598
        """See InventoryEntry._check"""
765
621
    def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
766
622
             output_to, reverse=False):
767
623
        """See InventoryEntry._diff."""
768
 
        from_text = self.symlink_target
 
624
        from bzrlib.diff import DiffSymlink
 
625
        old_target = self.symlink_target
769
626
        if to_entry is not None:
770
 
            to_text = to_entry.symlink_target
771
 
            if reverse:
772
 
                temp = from_text
773
 
                from_text = to_text
774
 
                to_text = temp
775
 
            print >>output_to, '=== target changed %r => %r' % (from_text, to_text)
776
 
        else:
777
 
            if not reverse:
778
 
                print >>output_to, '=== target was %r' % self.symlink_target
779
 
            else:
780
 
                print >>output_to, '=== target is %r' % self.symlink_target
 
627
            new_target = to_entry.symlink_target
 
628
        else:
 
629
            new_target = None
 
630
        if not reverse:
 
631
            old_tree = tree
 
632
            new_tree = to_tree
 
633
        else:
 
634
            old_tree = to_tree
 
635
            new_tree = tree
 
636
            new_target, old_target = old_target, new_target
 
637
        differ = DiffSymlink(old_tree, new_tree, output_to)
 
638
        return differ.diff_symlink(old_target, new_target)
781
639
 
782
640
    def __init__(self, file_id, name, parent_id):
783
641
        super(InventoryLink, self).__init__(file_id, name, parent_id)
817
675
            compatible = False
818
676
        return compatible
819
677
 
820
 
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
821
 
        """See InventoryEntry._snapshot_text."""
822
 
        commit_builder.modified_link(
823
 
            self.file_id, file_parents, self.symlink_target)
 
678
 
 
679
class TreeReference(InventoryEntry):
 
680
    
 
681
    kind = 'tree-reference'
 
682
    
 
683
    def __init__(self, file_id, name, parent_id, revision=None,
 
684
                 reference_revision=None):
 
685
        InventoryEntry.__init__(self, file_id, name, parent_id)
 
686
        self.revision = revision
 
687
        self.reference_revision = reference_revision
 
688
 
 
689
    def copy(self):
 
690
        return TreeReference(self.file_id, self.name, self.parent_id,
 
691
                             self.revision, self.reference_revision)
 
692
 
 
693
    def _read_tree_state(self, path, work_tree):
 
694
        """Populate fields in the inventory entry from the given tree.
 
695
        """
 
696
        self.reference_revision = work_tree.get_reference_revision(
 
697
            self.file_id, path)
 
698
 
 
699
    def _forget_tree_state(self):
 
700
        self.reference_revision = None 
 
701
 
 
702
    def _unchanged(self, previous_ie):
 
703
        """See InventoryEntry._unchanged."""
 
704
        compatible = super(TreeReference, self)._unchanged(previous_ie)
 
705
        if self.reference_revision != previous_ie.reference_revision:
 
706
            compatible = False
 
707
        return compatible
824
708
 
825
709
 
826
710
class Inventory(object):
875
759
        an id of None.
876
760
        """
877
761
        if root_id is not None:
878
 
            self._set_root(InventoryDirectory(root_id, '', None))
 
762
            self._set_root(InventoryDirectory(root_id, u'', None))
879
763
        else:
880
764
            self.root = None
881
765
            self._byid = {}
882
766
        self.revision_id = revision_id
883
767
 
 
768
    def __repr__(self):
 
769
        return "<Inventory object at %x, contents=%r>" % (id(self), self._byid)
 
770
 
 
771
    def apply_delta(self, delta):
 
772
        """Apply a delta to this inventory.
 
773
 
 
774
        :param delta: A list of changes to apply. After all the changes are
 
775
            applied the final inventory must be internally consistent, but it
 
776
            is ok to supply changes which, if only half-applied would have an
 
777
            invalid result - such as supplying two changes which rename two
 
778
            files, 'A' and 'B' with each other : [('A', 'B', 'A-id', a_entry),
 
779
            ('B', 'A', 'B-id', b_entry)].
 
780
 
 
781
            Each change is a tuple, of the form (old_path, new_path, file_id,
 
782
            new_entry).
 
783
            
 
784
            When new_path is None, the change indicates the removal of an entry
 
785
            from the inventory and new_entry will be ignored (using None is
 
786
            appropriate). If new_path is not None, then new_entry must be an
 
787
            InventoryEntry instance, which will be incorporated into the
 
788
            inventory (and replace any existing entry with the same file id).
 
789
            
 
790
            When old_path is None, the change indicates the addition of
 
791
            a new entry to the inventory.
 
792
            
 
793
            When neither new_path nor old_path are None, the change is a
 
794
            modification to an entry, such as a rename, reparent, kind change
 
795
            etc. 
 
796
 
 
797
            The children attribute of new_entry is ignored. This is because
 
798
            this method preserves children automatically across alterations to
 
799
            the parent of the children, and cases where the parent id of a
 
800
            child is changing require the child to be passed in as a separate
 
801
            change regardless. E.g. in the recursive deletion of a directory -
 
802
            the directory's children must be included in the delta, or the
 
803
            final inventory will be invalid.
 
804
        """
 
805
        children = {}
 
806
        # Remove all affected items which were in the original inventory,
 
807
        # starting with the longest paths, thus ensuring parents are examined
 
808
        # after their children, which means that everything we examine has no
 
809
        # modified children remaining by the time we examine it.
 
810
        for old_path, file_id in sorted(((op, f) for op, np, f, e in delta
 
811
                                        if op is not None), reverse=True):
 
812
            if file_id not in self:
 
813
                # adds come later
 
814
                continue
 
815
            # Preserve unaltered children of file_id for later reinsertion.
 
816
            children[file_id] = getattr(self[file_id], 'children', {})
 
817
            # Remove file_id and the unaltered children. If file_id is not
 
818
            # being deleted it will be reinserted back later.
 
819
            self.remove_recursive_id(file_id)
 
820
        # Insert all affected which should be in the new inventory, reattaching
 
821
        # their children if they had any. This is done from shortest path to
 
822
        # longest, ensuring that items which were modified and whose parents in
 
823
        # the resulting inventory were also modified, are inserted after their
 
824
        # parents.
 
825
        for new_path, new_entry in sorted((np, e) for op, np, f, e in
 
826
                                          delta if np is not None):
 
827
            if new_entry.kind == 'directory':
 
828
                new_entry.children = children.get(new_entry.file_id, {})
 
829
            self.add(new_entry)
 
830
 
884
831
    def _set_root(self, ie):
885
832
        self.root = ie
886
833
        self._byid = {self.root.file_id: self.root}
888
835
    def copy(self):
889
836
        # TODO: jam 20051218 Should copy also copy the revision_id?
890
837
        entries = self.iter_entries()
 
838
        if self.root is None:
 
839
            return Inventory(root_id=None)
891
840
        other = Inventory(entries.next()[1].file_id)
892
841
        # copy recursively so we know directories will be added before
893
842
        # their children.  There are more efficient ways than this...
894
 
        for path, entry in entries():
 
843
        for path, entry in entries:
895
844
            other.add(entry.copy())
896
845
        return other
897
846
 
946
895
                # if we finished all children, pop it off the stack
947
896
                stack.pop()
948
897
 
949
 
    def iter_entries_by_dir(self, from_dir=None, specific_file_ids=None):
 
898
    def iter_entries_by_dir(self, from_dir=None, specific_file_ids=None,
 
899
        yield_parents=False):
950
900
        """Iterate over the entries in a directory first order.
951
901
 
952
902
        This returns all entries for a directory before returning
954
904
        lexicographically sorted order, and is a hybrid between
955
905
        depth-first and breadth-first.
956
906
 
 
907
        :param yield_parents: If True, yield the parents from the root leading
 
908
            down to specific_file_ids that have been requested. This has no
 
909
            impact if specific_file_ids is None.
957
910
        :return: This yields (path, entry) pairs
958
911
        """
 
912
        if specific_file_ids and not isinstance(specific_file_ids, set):
 
913
            specific_file_ids = set(specific_file_ids)
959
914
        # TODO? Perhaps this should return the from_dir so that the root is
960
915
        # yielded? or maybe an option?
961
916
        if from_dir is None:
962
917
            if self.root is None:
963
918
                return
964
919
            # Optimize a common case
965
 
            if specific_file_ids is not None and len(specific_file_ids) == 1:
 
920
            if (not yield_parents and specific_file_ids is not None and
 
921
                len(specific_file_ids) == 1):
966
922
                file_id = list(specific_file_ids)[0]
967
923
                if file_id in self:
968
924
                    yield self.id2path(file_id), self[file_id]
969
925
                return 
970
926
            from_dir = self.root
971
 
            if (specific_file_ids is None or 
 
927
            if (specific_file_ids is None or yield_parents or
972
928
                self.root.file_id in specific_file_ids):
973
 
                yield '', self.root
 
929
                yield u'', self.root
974
930
        elif isinstance(from_dir, basestring):
975
931
            from_dir = self._byid[from_dir]
976
932
 
977
933
        if specific_file_ids is not None:
 
934
            # TODO: jam 20070302 This could really be done as a loop rather
 
935
            #       than a bunch of recursive calls.
978
936
            parents = set()
 
937
            byid = self._byid
979
938
            def add_ancestors(file_id):
980
 
                if file_id not in self:
 
939
                if file_id not in byid:
981
940
                    return
982
 
                parent_id = self[file_id].parent_id
 
941
                parent_id = byid[file_id].parent_id
983
942
                if parent_id is None:
984
943
                    return
985
944
                if parent_id not in parents:
1000
959
                child_relpath = cur_relpath + child_name
1001
960
 
1002
961
                if (specific_file_ids is None or 
1003
 
                    child_ie.file_id in specific_file_ids):
 
962
                    child_ie.file_id in specific_file_ids or
 
963
                    (yield_parents and child_ie.file_id in parents)):
1004
964
                    yield child_relpath, child_ie
1005
965
 
1006
966
                if child_ie.kind == 'directory':
1008
968
                        child_dirs.append((child_relpath+'/', child_ie))
1009
969
            stack.extend(reversed(child_dirs))
1010
970
 
 
971
    def make_entry(self, kind, name, parent_id, file_id=None):
 
972
        """Simple thunk to bzrlib.inventory.make_entry."""
 
973
        return make_entry(kind, name, parent_id, file_id)
 
974
 
1011
975
    def entries(self):
1012
976
        """Return list of (path, ie) for all entries except the root.
1013
977
 
1076
1040
    def get_child(self, parent_id, filename):
1077
1041
        return self[parent_id].children.get(filename)
1078
1042
 
 
1043
    def _add_child(self, entry):
 
1044
        """Add an entry to the inventory, without adding it to its parent"""
 
1045
        if entry.file_id in self._byid:
 
1046
            raise BzrError("inventory already contains entry with id {%s}" %
 
1047
                           entry.file_id)
 
1048
        self._byid[entry.file_id] = entry
 
1049
        for child in getattr(entry, 'children', {}).itervalues():
 
1050
            self._add_child(child)
 
1051
        return entry
 
1052
 
1079
1053
    def add(self, entry):
1080
1054
        """Add entry to inventory.
1081
1055
 
1085
1059
        Returns the new entry object.
1086
1060
        """
1087
1061
        if entry.file_id in self._byid:
1088
 
            raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
 
1062
            raise errors.DuplicateFileId(entry.file_id,
 
1063
                                         self._byid[entry.file_id])
1089
1064
 
1090
1065
        if entry.parent_id is None:
1091
 
            assert self.root is None and len(self._byid) == 0
1092
 
            self._set_root(entry)
1093
 
            return entry
1094
 
        try:
1095
 
            parent = self._byid[entry.parent_id]
1096
 
        except KeyError:
1097
 
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
1098
 
 
1099
 
        if entry.name in parent.children:
1100
 
            raise BzrError("%s is already versioned" %
1101
 
                    osutils.pathjoin(self.id2path(parent.file_id), entry.name))
1102
 
 
1103
 
        self._byid[entry.file_id] = entry
1104
 
        parent.children[entry.name] = entry
1105
 
        return entry
 
1066
            self.root = entry
 
1067
        else:
 
1068
            try:
 
1069
                parent = self._byid[entry.parent_id]
 
1070
            except KeyError:
 
1071
                raise BzrError("parent_id {%s} not in inventory" %
 
1072
                               entry.parent_id)
 
1073
 
 
1074
            if entry.name in parent.children:
 
1075
                raise BzrError("%s is already versioned" %
 
1076
                        osutils.pathjoin(self.id2path(parent.file_id),
 
1077
                        entry.name).encode('utf-8'))
 
1078
            parent.children[entry.name] = entry
 
1079
        return self._add_child(entry)
1106
1080
 
1107
1081
    def add_path(self, relpath, kind, file_id=None, parent_id=None):
1108
1082
        """Add entry from a path.
1140
1114
        False
1141
1115
        """
1142
1116
        ie = self[file_id]
1143
 
 
1144
 
        assert ie.parent_id is None or \
1145
 
            self[ie.parent_id].children[ie.name] == ie
1146
 
        
1147
1117
        del self._byid[file_id]
1148
1118
        if ie.parent_id is not None:
1149
1119
            del self[ie.parent_id].children[ie.name]
1181
1151
            try:
1182
1152
                ie = self._byid[file_id]
1183
1153
            except KeyError:
1184
 
                raise BzrError("file_id {%s} not found in inventory" % file_id)
 
1154
                raise errors.NoSuchId(tree=None, file_id=file_id)
1185
1155
            yield ie
1186
1156
            file_id = ie.parent_id
1187
1157
 
1237
1207
                if children is None:
1238
1208
                    return None
1239
1209
                cie = children[f]
1240
 
                assert cie.name == f
1241
 
                assert cie.parent_id == parent.file_id
1242
1210
                parent = cie
1243
1211
            except KeyError:
1244
1212
                # or raise an error?
1267
1235
        for file_id in reversed(to_delete):
1268
1236
            ie = self[file_id]
1269
1237
            del self._byid[file_id]
1270
 
            if ie.parent_id is not None:
1271
 
                del self[ie.parent_id].children[ie.name]
 
1238
        if ie.parent_id is not None:
 
1239
            del self[ie.parent_id].children[ie.name]
 
1240
        else:
 
1241
            self.root = None
1272
1242
 
1273
1243
    def rename(self, file_id, new_parent_id, new_name):
1274
1244
        """Move a file within the inventory.
1275
1245
 
1276
1246
        This can change either the name, or the parent, or both.
1277
1247
 
1278
 
        This does not move the working file."""
 
1248
        This does not move the working file.
 
1249
        """
 
1250
        new_name = ensure_normalized_name(new_name)
1279
1251
        if not is_valid_name(new_name):
1280
1252
            raise BzrError("not an acceptable filename: %r" % new_name)
1281
1253
 
1303
1275
        return self.root is not None and file_id == self.root.file_id
1304
1276
 
1305
1277
 
 
1278
entry_factory = {
 
1279
    'directory': InventoryDirectory,
 
1280
    'file': InventoryFile,
 
1281
    'symlink': InventoryLink,
 
1282
    'tree-reference': TreeReference
 
1283
}
 
1284
 
1306
1285
def make_entry(kind, name, parent_id, file_id=None):
1307
1286
    """Create an inventory entry.
1308
1287
 
1313
1292
    """
1314
1293
    if file_id is None:
1315
1294
        file_id = generate_ids.gen_file_id(name)
1316
 
 
 
1295
    name = ensure_normalized_name(name)
 
1296
    try:
 
1297
        factory = entry_factory[kind]
 
1298
    except KeyError:
 
1299
        raise BzrError("unknown kind %r" % kind)
 
1300
    return factory(file_id, name, parent_id)
 
1301
 
 
1302
 
 
1303
def ensure_normalized_name(name):
 
1304
    """Normalize name.
 
1305
 
 
1306
    :raises InvalidNormalization: When name is not normalized, and cannot be
 
1307
        accessed on this platform by the normalized path.
 
1308
    :return: The NFC normalised version of name.
 
1309
    """
 
1310
    #------- This has been copied to bzrlib.dirstate.DirState.add, please
 
1311
    # keep them synchronised.
 
1312
    # we dont import normalized_filename directly because we want to be
 
1313
    # able to change the implementation at runtime for tests.
1317
1314
    norm_name, can_access = osutils.normalized_filename(name)
1318
1315
    if norm_name != name:
1319
1316
        if can_access:
1320
 
            name = norm_name
 
1317
            return norm_name
1321
1318
        else:
1322
1319
            # TODO: jam 20060701 This would probably be more useful
1323
1320
            #       if the error was raised with the full path
1324
1321
            raise errors.InvalidNormalization(name)
1325
 
 
1326
 
    if kind == 'directory':
1327
 
        return InventoryDirectory(file_id, name, parent_id)
1328
 
    elif kind == 'file':
1329
 
        return InventoryFile(file_id, name, parent_id)
1330
 
    elif kind == 'symlink':
1331
 
        return InventoryLink(file_id, name, parent_id)
1332
 
    else:
1333
 
        raise BzrError("unknown kind %r" % kind)
 
1322
    return name
1334
1323
 
1335
1324
 
1336
1325
_NAME_RE = None