/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Alexander Belchenko
  • Date: 2007-10-26 21:49:15 UTC
  • mto: (2947.4.2 0.92)
  • mto: This revision was merged to the branch mainline in revision 2971.
  • Revision ID: bialix@ukr.net-20071026214915-5eacqh9k2ps6jagj
windows python-based installer: shortcut for uninstall action

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
27
27
# created, but it's not for now.
28
28
ROOT_ID = "TREE_ROOT"
29
29
 
30
 
 
31
 
import collections
32
 
import os.path
 
30
import os
33
31
import re
34
32
import sys
 
33
 
 
34
from bzrlib.lazy_import import lazy_import
 
35
lazy_import(globals(), """
 
36
import collections
35
37
import tarfile
36
 
import types
37
 
from warnings import warn
38
38
 
39
39
import bzrlib
40
 
from bzrlib import errors, osutils
41
 
from bzrlib.osutils import (pumpfile, quotefn, splitpath, joinpath,
42
 
                            pathjoin, sha_strings)
43
 
from bzrlib.errors import (NotVersionedError, InvalidEntryName,
44
 
                           BzrError, BzrCheckError, BinaryFile)
 
40
from bzrlib import (
 
41
    errors,
 
42
    generate_ids,
 
43
    osutils,
 
44
    symbol_versioning,
 
45
    workingtree,
 
46
    )
 
47
""")
 
48
 
 
49
from bzrlib.errors import (
 
50
    BzrCheckError,
 
51
    BzrError,
 
52
    )
 
53
from bzrlib.symbol_versioning import deprecated_method, zero_ninetyone
45
54
from bzrlib.trace import mutter
46
55
 
47
56
 
82
91
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
83
92
    >>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
84
93
    InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
85
 
    >>> shouldbe = {0: '', 1: 'src', 2: pathjoin('src','hello.c')}
 
94
    >>> shouldbe = {0: '', 1: 'src', 2: 'src/hello.c'}
86
95
    >>> for ix, j in enumerate(i.iter_entries()):
87
96
    ...   print (j[0] == shouldbe[ix], j[1])
88
97
    ... 
89
 
    (True, InventoryDirectory('TREE_ROOT', '', parent_id=None, revision=None))
 
98
    (True, InventoryDirectory('TREE_ROOT', u'', parent_id=None, revision=None))
90
99
    (True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
91
100
    (True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
92
 
    >>> i.add(InventoryFile('2323', 'bye.c', '123'))
93
 
    Traceback (most recent call last):
94
 
    ...
95
 
    BzrError: inventory already contains entry with id {2323}
96
101
    >>> i.add(InventoryFile('2324', 'bye.c', '123'))
97
102
    InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
98
103
    >>> i.add(InventoryDirectory('2325', 'wibble', '123'))
157
162
    def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
158
163
             output_to, reverse=False):
159
164
        """Perform a diff between two entries of the same kind."""
160
 
 
161
 
    def find_previous_heads(self, previous_inventories,
162
 
                            versioned_file_store,
163
 
                            transaction,
164
 
                            entry_vf=None):
165
 
        """Return the revisions and entries that directly precede this.
166
 
 
167
 
        Returned as a map from revision to inventory entry.
168
 
 
169
 
        This is a map containing the file revisions in all parents
170
 
        for which the file exists, and its revision is not a parent of
171
 
        any other. If the file is new, the set will be empty.
172
 
 
173
 
        :param versioned_file_store: A store where ancestry data on this
174
 
                                     file id can be queried.
175
 
        :param transaction: The transaction that queries to the versioned 
176
 
                            file store should be completed under.
177
 
        :param entry_vf: The entry versioned file, if its already available.
 
165
    
 
166
    def parent_candidates(self, previous_inventories):
 
167
        """Find possible per-file graph parents.
 
168
 
 
169
        This is currently defined by:
 
170
         - Select the last changed revision in the parent inventory.
 
171
         - Do deal with a short lived bug in bzr 0.8's development two entries
 
172
           that have the same last changed but different 'x' bit settings are
 
173
           changed in-place.
178
174
        """
179
 
        def get_ancestors(weave, entry):
180
 
            return set(weave.get_ancestry(entry.revision))
181
175
        # revision:ie mapping for each ie found in previous_inventories.
182
176
        candidates = {}
183
 
        # revision:ie mapping with one revision for each head.
184
 
        heads = {}
185
 
        # revision: ancestor list for each head
186
 
        head_ancestors = {}
187
177
        # identify candidate head revision ids.
188
178
        for inv in previous_inventories:
189
179
            if self.file_id in inv:
205
195
                else:
206
196
                    # add this revision as a candidate.
207
197
                    candidates[ie.revision] = ie
208
 
 
 
198
        return candidates
 
199
 
 
200
    @deprecated_method(zero_ninetyone)
 
201
    def find_previous_heads(self, previous_inventories,
 
202
                            versioned_file_store,
 
203
                            transaction,
 
204
                            entry_vf=None):
 
205
        """Return the revisions and entries that directly precede this.
 
206
 
 
207
        Returned as a map from revision to inventory entry.
 
208
 
 
209
        This is a map containing the file revisions in all parents
 
210
        for which the file exists, and its revision is not a parent of
 
211
        any other. If the file is new, the set will be empty.
 
212
 
 
213
        :param versioned_file_store: A store where ancestry data on this
 
214
                                     file id can be queried.
 
215
        :param transaction: The transaction that queries to the versioned 
 
216
                            file store should be completed under.
 
217
        :param entry_vf: The entry versioned file, if its already available.
 
218
        """
 
219
        candidates = self.parent_candidates(previous_inventories)
 
220
 
 
221
        # revision:ie mapping with one revision for each head.
 
222
        heads = {}
209
223
        # common case optimisation
210
224
        if len(candidates) == 1:
211
225
            # if there is only one candidate revision found
212
 
            # then we can opening the versioned file to access ancestry:
 
226
            # then we can avoid opening the versioned file to access ancestry:
213
227
            # there cannot be any ancestors to eliminate when there is 
214
228
            # only one revision available.
215
 
            heads[ie.revision] = ie
216
 
            return heads
 
229
            return candidates
 
230
        
 
231
        # --- what follows is now encapsulated in repository.get_graph.heads(), 
 
232
        #     but that is not accessible from here as we have no repository
 
233
        #     pointer. Note that the repository.get_graph.heads() call can return
 
234
        #     different results *at the moment* because of the kind-changing check
 
235
        #     we have in parent_candidates().
217
236
 
218
237
        # eliminate ancestors amongst the available candidates:
219
238
        # heads are those that are not an ancestor of any other candidate
220
239
        # - this provides convergence at a per-file level.
 
240
        def get_ancestors(weave, entry):
 
241
            return set(weave.get_ancestry(entry.revision, topo_sorted=False))
 
242
        # revision: ancestor list for each head
 
243
        head_ancestors = {}
221
244
        for ie in candidates.values():
222
245
            # may be an ancestor of a known head:
223
246
            already_present = 0 != len(
246
269
 
247
270
    def get_tar_item(self, root, dp, now, tree):
248
271
        """Get a tarfile item and a file stream for its content."""
249
 
        item = tarfile.TarInfo(pathjoin(root, dp))
 
272
        item = tarfile.TarInfo(osutils.pathjoin(root, dp).encode('utf8'))
250
273
        # TODO: would be cool to actually set it to the timestamp of the
251
274
        # revision it was last changed
252
275
        item.mtime = now
281
304
        """
282
305
        assert isinstance(name, basestring), name
283
306
        if '/' in name or '\\' in name:
284
 
            raise InvalidEntryName(name=name)
 
307
            raise errors.InvalidEntryName(name=name)
285
308
        self.executable = False
286
309
        self.revision = None
287
310
        self.text_sha1 = None
288
311
        self.text_size = None
289
312
        self.file_id = file_id
 
313
        assert isinstance(file_id, (str, None.__class__)), \
 
314
            'bad type %r for %r' % (type(file_id), file_id)
290
315
        self.name = name
291
316
        self.text_id = text_id
292
317
        self.parent_id = parent_id
293
318
        self.symlink_target = None
 
319
        self.reference_revision = None
294
320
 
295
321
    def kind_character(self):
296
322
        """Return a short kind indicator useful for appending to names."""
311
337
        
312
338
        This is a template method - implement _put_on_disk in subclasses.
313
339
        """
314
 
        fullpath = pathjoin(dest, dp)
 
340
        fullpath = osutils.pathjoin(dest, dp)
315
341
        self._put_on_disk(fullpath, tree)
316
342
        # mutter("  export {%s} kind %s to %s", self.file_id,
317
343
        #         self.kind, fullpath)
325
351
 
326
352
    @staticmethod
327
353
    def versionable_kind(kind):
328
 
        return (kind in ('file', 'directory', 'symlink'))
 
354
        return (kind in ('file', 'directory', 'symlink', 'tree-reference'))
329
355
 
330
356
    def check(self, checker, rev_id, inv, tree):
331
357
        """Check this inventory entry is intact.
376
402
            return 'added'
377
403
        elif new_entry is None:
378
404
            return 'removed'
 
405
        if old_entry.kind != new_entry.kind:
 
406
            return 'modified'
379
407
        text_modified, meta_modified = new_entry.detect_changes(old_entry)
380
408
        if text_modified or meta_modified:
381
409
            modified = True
404
432
                   self.parent_id,
405
433
                   self.revision))
406
434
 
407
 
    def snapshot(self, revision, path, previous_entries,
408
 
                 work_tree, commit_builder):
409
 
        """Make a snapshot of this entry which may or may not have changed.
410
 
        
411
 
        This means that all its fields are populated, that it has its
412
 
        text stored in the text store or weave.
413
 
        """
414
 
        # mutter('new parents of %s are %r', path, previous_entries)
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
418
 
        if len(previous_entries) == 1:
419
 
            # cannot be unchanged unless there is only one parent file rev.
420
 
            parent_ie = previous_entries.values()[0]
421
 
            if self._unchanged(parent_ie):
422
 
                # mutter("found unchanged entry")
423
 
                self.revision = parent_ie.revision
424
 
                return "unchanged"
425
 
        return self._snapshot_into_revision(revision, previous_entries, 
426
 
                                            work_tree, commit_builder)
427
 
 
428
 
    def _snapshot_into_revision(self, revision, previous_entries, work_tree,
429
 
                                commit_builder):
430
 
        """Record this revision unconditionally into a store.
431
 
 
432
 
        The entry's last-changed revision property (`revision`) is updated to 
433
 
        that of the new revision.
434
 
        
435
 
        :param revision: id of the new revision that is being recorded.
436
 
 
437
 
        :returns: String description of the commit (e.g. "merged", "modified"), etc.
438
 
        """
439
 
        # mutter('new revision {%s} for {%s}', revision, self.file_id)
440
 
        self.revision = revision
441
 
        self._snapshot_text(previous_entries, work_tree, commit_builder)
442
 
 
443
 
    def _snapshot_text(self, file_parents, work_tree, commit_builder): 
444
 
        """Record the 'text' of this entry, whatever form that takes.
445
 
        
446
 
        This default implementation simply adds an empty text.
447
 
        """
448
 
        raise NotImplementedError(self._snapshot_text)
449
 
 
450
435
    def __eq__(self, other):
451
436
        if not isinstance(other, InventoryEntry):
452
437
            return NotImplemented
461
446
                and (self.kind == other.kind)
462
447
                and (self.revision == other.revision)
463
448
                and (self.executable == other.executable)
 
449
                and (self.reference_revision == other.reference_revision)
464
450
                )
465
451
 
466
452
    def __ne__(self, other):
481
467
        # renamed
482
468
        elif previous_ie.name != self.name:
483
469
            compatible = False
 
470
        elif previous_ie.kind != self.kind:
 
471
            compatible = False
484
472
        return compatible
485
473
 
486
474
    def _read_tree_state(self, path, work_tree):
501
489
class RootEntry(InventoryEntry):
502
490
 
503
491
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
504
 
                 'text_id', 'parent_id', 'children', 'executable', 
505
 
                 'revision', 'symlink_target']
 
492
                 'text_id', 'parent_id', 'children', 'executable',
 
493
                 'revision', 'symlink_target', 'reference_revision']
506
494
 
507
495
    def _check(self, checker, rev_id, tree):
508
496
        """See InventoryEntry._check"""
514
502
        self.parent_id = None
515
503
        self.name = u''
516
504
        self.revision = None
517
 
        warn('RootEntry is deprecated as of bzr 0.10.  Please use '
518
 
             'InventoryDirectory instead.',
519
 
            DeprecationWarning, stacklevel=2)
 
505
        symbol_versioning.warn('RootEntry is deprecated as of bzr 0.10.'
 
506
                               '  Please use InventoryDirectory instead.',
 
507
                               DeprecationWarning, stacklevel=2)
520
508
 
521
509
    def __eq__(self, other):
522
510
        if not isinstance(other, RootEntry):
530
518
    """A directory in an inventory."""
531
519
 
532
520
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
533
 
                 'text_id', 'parent_id', 'children', 'executable', 
534
 
                 'revision', 'symlink_target']
 
521
                 'text_id', 'parent_id', 'children', 'executable',
 
522
                 'revision', 'symlink_target', 'reference_revision']
535
523
 
536
524
    def _check(self, checker, rev_id, tree):
537
525
        """See InventoryEntry._check"""
568
556
        """See InventoryEntry._put_on_disk."""
569
557
        os.mkdir(fullpath)
570
558
 
571
 
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
572
 
        """See InventoryEntry._snapshot_text."""
573
 
        commit_builder.modified_directory(self.file_id, file_parents)
574
 
 
575
559
 
576
560
class InventoryFile(InventoryEntry):
577
561
    """A file in an inventory."""
578
562
 
579
563
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
580
 
                 'text_id', 'parent_id', 'children', 'executable', 
581
 
                 'revision', 'symlink_target']
 
564
                 'text_id', 'parent_id', 'children', 'executable',
 
565
                 'revision', 'symlink_target', 'reference_revision']
582
566
 
583
567
    def _check(self, checker, tree_revision_id, tree):
584
568
        """See InventoryEntry._check"""
586
570
        if t in checker.checked_texts:
587
571
            prev_sha = checker.checked_texts[t]
588
572
            if prev_sha != self.text_sha1:
589
 
                raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
590
 
                                    (self.file_id, tree_revision_id))
 
573
                raise BzrCheckError(
 
574
                    'mismatched sha1 on {%s} in {%s} (%s != %s) %r' %
 
575
                    (self.file_id, tree_revision_id, prev_sha, self.text_sha1,
 
576
                     t))
591
577
            else:
592
578
                checker.repeated_text_cnt += 1
593
579
                return
594
580
 
595
581
        if self.file_id not in checker.checked_weaves:
596
582
            mutter('check weave {%s}', self.file_id)
597
 
            w = tree.get_weave(self.file_id)
 
583
            w = tree._get_weave(self.file_id)
598
584
            # Not passing a progress bar, because it creates a new
599
585
            # progress, which overwrites the current progress,
600
586
            # and doesn't look nice
601
587
            w.check()
602
588
            checker.checked_weaves[self.file_id] = True
603
589
        else:
604
 
            w = tree.get_weave(self.file_id)
 
590
            w = tree._get_weave(self.file_id)
605
591
 
606
592
        mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
607
593
        checker.checked_text_cnt += 1
645
631
            else:
646
632
                text_diff(to_label, to_text,
647
633
                          from_label, from_text, output_to)
648
 
        except BinaryFile:
 
634
        except errors.BinaryFile:
649
635
            if reverse:
650
636
                label_pair = (to_label, from_label)
651
637
            else:
652
638
                label_pair = (from_label, to_label)
653
 
            print >> output_to, "Binary files %s and %s differ" % label_pair
 
639
            output_to.write(
 
640
                  ("Binary files %s and %s differ\n" % label_pair).encode('utf8'))
654
641
 
655
642
    def has_text(self):
656
643
        """See InventoryEntry.has_text."""
677
664
 
678
665
    def _put_on_disk(self, fullpath, tree):
679
666
        """See InventoryEntry._put_on_disk."""
680
 
        pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
 
667
        osutils.pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
681
668
        if tree.is_executable(self.file_id):
682
669
            os.chmod(fullpath, 0755)
683
670
 
700
687
    def _forget_tree_state(self):
701
688
        self.text_sha1 = None
702
689
 
703
 
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
704
 
        """See InventoryEntry._snapshot_text."""
705
 
        def get_content_byte_lines():
706
 
            return work_tree.get_file(self.file_id).readlines()
707
 
        self.text_sha1, self.text_size = commit_builder.modified_file_text(
708
 
            self.file_id, file_parents, get_content_byte_lines, self.text_sha1, self.text_size)
709
 
 
710
690
    def _unchanged(self, previous_ie):
711
691
        """See InventoryEntry._unchanged."""
712
692
        compatible = super(InventoryFile, self)._unchanged(previous_ie)
725
705
    """A file in an inventory."""
726
706
 
727
707
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
728
 
                 'text_id', 'parent_id', 'children', 'executable', 
729
 
                 'revision', 'symlink_target']
 
708
                 'text_id', 'parent_id', 'children', 'executable',
 
709
                 'revision', 'symlink_target', 'reference_revision']
730
710
 
731
711
    def _check(self, checker, rev_id, tree):
732
712
        """See InventoryEntry._check"""
762
742
                temp = from_text
763
743
                from_text = to_text
764
744
                to_text = temp
765
 
            print >>output_to, '=== target changed %r => %r' % (from_text, to_text)
 
745
            output_to.write('=== target changed %r => %r\n' % (from_text, to_text))
766
746
        else:
767
747
            if not reverse:
768
 
                print >>output_to, '=== target was %r' % self.symlink_target
 
748
                output_to.write('=== target was %r\n' % self.symlink_target)
769
749
            else:
770
 
                print >>output_to, '=== target is %r' % self.symlink_target
 
750
                output_to.write('=== target is %r\n' % self.symlink_target)
771
751
 
772
752
    def __init__(self, file_id, name, parent_id):
773
753
        super(InventoryLink, self).__init__(file_id, name, parent_id)
807
787
            compatible = False
808
788
        return compatible
809
789
 
810
 
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
811
 
        """See InventoryEntry._snapshot_text."""
812
 
        commit_builder.modified_link(
813
 
            self.file_id, file_parents, self.symlink_target)
 
790
 
 
791
class TreeReference(InventoryEntry):
 
792
    
 
793
    kind = 'tree-reference'
 
794
    
 
795
    def __init__(self, file_id, name, parent_id, revision=None,
 
796
                 reference_revision=None):
 
797
        InventoryEntry.__init__(self, file_id, name, parent_id)
 
798
        self.revision = revision
 
799
        self.reference_revision = reference_revision
 
800
 
 
801
    def copy(self):
 
802
        return TreeReference(self.file_id, self.name, self.parent_id,
 
803
                             self.revision, self.reference_revision)
 
804
 
 
805
    def _read_tree_state(self, path, work_tree):
 
806
        """Populate fields in the inventory entry from the given tree.
 
807
        """
 
808
        self.reference_revision = work_tree.get_reference_revision(
 
809
            self.file_id, path)
 
810
 
 
811
    def _forget_tree_state(self):
 
812
        self.reference_revision = None 
 
813
 
 
814
    def _unchanged(self, previous_ie):
 
815
        """See InventoryEntry._unchanged."""
 
816
        compatible = super(TreeReference, self)._unchanged(previous_ie)
 
817
        if self.reference_revision != previous_ie.reference_revision:
 
818
            compatible = False
 
819
        return compatible
814
820
 
815
821
 
816
822
class Inventory(object):
849
855
    ['', u'hello.c']
850
856
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
851
857
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
 
858
    Traceback (most recent call last):
 
859
    BzrError: parent_id {TREE_ROOT} not in inventory
 
860
    >>> inv.add(InventoryFile('123-123', 'hello.c', 'TREE_ROOT-12345678-12345678'))
852
861
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None)
853
862
    """
854
863
    def __init__(self, root_id=ROOT_ID, revision_id=None):
861
870
        The inventory is created with a default root directory, with
862
871
        an id of None.
863
872
        """
864
 
        # We are letting Branch.create() create a unique inventory
865
 
        # root id. Rather than generating a random one here.
866
 
        #if root_id is None:
867
 
        #    root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
868
873
        if root_id is not None:
869
 
            self._set_root(InventoryDirectory(root_id, '', None))
 
874
            assert root_id.__class__ == str
 
875
            self._set_root(InventoryDirectory(root_id, u'', None))
870
876
        else:
871
877
            self.root = None
872
878
            self._byid = {}
873
 
        # FIXME: this isn't ever used, changing it to self.revision may break
874
 
        # things. TODO make everything use self.revision_id
875
879
        self.revision_id = revision_id
876
880
 
 
881
    def __repr__(self):
 
882
        return "<Inventory object at %x, contents=%r>" % (id(self), self._byid)
 
883
 
 
884
    def apply_delta(self, delta):
 
885
        """Apply a delta to this inventory.
 
886
 
 
887
        :param delta: A list of changes to apply. After all the changes are
 
888
            applied the final inventory must be internally consistent, but it
 
889
            is ok to supply changes which, if only half-applied would have an
 
890
            invalid result - such as supplying two changes which rename two
 
891
            files, 'A' and 'B' with each other : [('A', 'B', 'A-id', a_entry),
 
892
            ('B', 'A', 'B-id', b_entry)].
 
893
 
 
894
            Each change is a tuple, of the form (old_path, new_path, file_id,
 
895
            new_entry).
 
896
            
 
897
            When new_path is None, the change indicates the removal of an entry
 
898
            from the inventory and new_entry will be ignored (using None is
 
899
            appropriate). If new_path is not None, then new_entry must be an
 
900
            InventoryEntry instance, which will be incorporated into the
 
901
            inventory (and replace any existing entry with the same file id).
 
902
            
 
903
            When old_path is None, the change indicates the addition of
 
904
            a new entry to the inventory.
 
905
            
 
906
            When neither new_path nor old_path are None, the change is a
 
907
            modification to an entry, such as a rename, reparent, kind change
 
908
            etc. 
 
909
 
 
910
            The children attribute of new_entry is ignored. This is because
 
911
            this method preserves children automatically across alterations to
 
912
            the parent of the children, and cases where the parent id of a
 
913
            child is changing require the child to be passed in as a separate
 
914
            change regardless. E.g. in the recursive deletion of a directory -
 
915
            the directory's children must be included in the delta, or the
 
916
            final inventory will be invalid.
 
917
        """
 
918
        children = {}
 
919
        # Remove all affected items which were in the original inventory,
 
920
        # starting with the longest paths, thus ensuring parents are examined
 
921
        # after their children, which means that everything we examine has no
 
922
        # modified children remaining by the time we examine it.
 
923
        for old_path, file_id in sorted(((op, f) for op, np, f, e in delta
 
924
                                        if op is not None), reverse=True):
 
925
            if file_id not in self:
 
926
                # adds come later
 
927
                continue
 
928
            # Preserve unaltered children of file_id for later reinsertion.
 
929
            children[file_id] = getattr(self[file_id], 'children', {})
 
930
            # Remove file_id and the unaltered children. If file_id is not
 
931
            # being deleted it will be reinserted back later.
 
932
            self.remove_recursive_id(file_id)
 
933
        # Insert all affected which should be in the new inventory, reattaching
 
934
        # their children if they had any. This is done from shortest path to
 
935
        # longest, ensuring that items which were modified and whose parents in
 
936
        # the resulting inventory were also modified, are inserted after their
 
937
        # parents.
 
938
        for new_path, new_entry in sorted((np, e) for op, np, f, e in
 
939
                                          delta if np is not None):
 
940
            if new_entry.kind == 'directory':
 
941
                new_entry.children = children.get(new_entry.file_id, {})
 
942
            self.add(new_entry)
 
943
 
877
944
    def _set_root(self, ie):
878
945
        self.root = ie
879
946
        self._byid = {self.root.file_id: self.root}
881
948
    def copy(self):
882
949
        # TODO: jam 20051218 Should copy also copy the revision_id?
883
950
        entries = self.iter_entries()
 
951
        if self.root is None:
 
952
            return Inventory(root_id=None)
884
953
        other = Inventory(entries.next()[1].file_id)
885
954
        # copy recursively so we know directories will be added before
886
955
        # their children.  There are more efficient ways than this...
887
 
        for path, entry in entries():
 
956
        for path, entry in entries:
888
957
            other.add(entry.copy())
889
958
        return other
890
959
 
898
967
    def iter_entries(self, from_dir=None):
899
968
        """Return (path, entry) pairs, in order by name."""
900
969
        if from_dir is None:
901
 
            assert self.root
 
970
            if self.root is None:
 
971
                return
902
972
            from_dir = self.root
903
973
            yield '', self.root
904
974
        elif isinstance(from_dir, basestring):
938
1008
                # if we finished all children, pop it off the stack
939
1009
                stack.pop()
940
1010
 
941
 
    def iter_entries_by_dir(self, from_dir=None):
 
1011
    def iter_entries_by_dir(self, from_dir=None, specific_file_ids=None,
 
1012
        yield_parents=False):
942
1013
        """Iterate over the entries in a directory first order.
943
1014
 
944
1015
        This returns all entries for a directory before returning
946
1017
        lexicographically sorted order, and is a hybrid between
947
1018
        depth-first and breadth-first.
948
1019
 
 
1020
        :param yield_parents: If True, yield the parents from the root leading
 
1021
            down to specific_file_ids that have been requested. This has no
 
1022
            impact if specific_file_ids is None.
949
1023
        :return: This yields (path, entry) pairs
950
1024
        """
 
1025
        if specific_file_ids and not isinstance(specific_file_ids, set):
 
1026
            specific_file_ids = set(specific_file_ids)
951
1027
        # TODO? Perhaps this should return the from_dir so that the root is
952
1028
        # yielded? or maybe an option?
953
1029
        if from_dir is None:
954
 
            assert self.root
 
1030
            if self.root is None:
 
1031
                return
 
1032
            # Optimize a common case
 
1033
            if (not yield_parents and specific_file_ids is not None and
 
1034
                len(specific_file_ids) == 1):
 
1035
                file_id = list(specific_file_ids)[0]
 
1036
                if file_id in self:
 
1037
                    yield self.id2path(file_id), self[file_id]
 
1038
                return 
955
1039
            from_dir = self.root
956
 
            yield '', self.root
 
1040
            if (specific_file_ids is None or yield_parents or
 
1041
                self.root.file_id in specific_file_ids):
 
1042
                yield u'', self.root
957
1043
        elif isinstance(from_dir, basestring):
958
1044
            from_dir = self._byid[from_dir]
 
1045
 
 
1046
        if specific_file_ids is not None:
 
1047
            # TODO: jam 20070302 This could really be done as a loop rather
 
1048
            #       than a bunch of recursive calls.
 
1049
            parents = set()
 
1050
            byid = self._byid
 
1051
            def add_ancestors(file_id):
 
1052
                if file_id not in byid:
 
1053
                    return
 
1054
                parent_id = byid[file_id].parent_id
 
1055
                if parent_id is None:
 
1056
                    return
 
1057
                if parent_id not in parents:
 
1058
                    parents.add(parent_id)
 
1059
                    add_ancestors(parent_id)
 
1060
            for file_id in specific_file_ids:
 
1061
                add_ancestors(file_id)
 
1062
        else:
 
1063
            parents = None
959
1064
            
960
1065
        stack = [(u'', from_dir)]
961
1066
        while stack:
966
1071
 
967
1072
                child_relpath = cur_relpath + child_name
968
1073
 
969
 
                yield child_relpath, child_ie
 
1074
                if (specific_file_ids is None or 
 
1075
                    child_ie.file_id in specific_file_ids or
 
1076
                    (yield_parents and child_ie.file_id in parents)):
 
1077
                    yield child_relpath, child_ie
970
1078
 
971
1079
                if child_ie.kind == 'directory':
972
 
                    child_dirs.append((child_relpath+'/', child_ie))
 
1080
                    if parents is None or child_ie.file_id in parents:
 
1081
                        child_dirs.append((child_relpath+'/', child_ie))
973
1082
            stack.extend(reversed(child_dirs))
974
1083
 
 
1084
    def make_entry(self, kind, name, parent_id, file_id=None):
 
1085
        """Simple thunk to bzrlib.inventory.make_entry."""
 
1086
        return make_entry(kind, name, parent_id, file_id)
 
1087
 
975
1088
    def entries(self):
976
1089
        """Return list of (path, ie) for all entries except the root.
977
1090
 
982
1095
            kids = dir_ie.children.items()
983
1096
            kids.sort()
984
1097
            for name, ie in kids:
985
 
                child_path = pathjoin(dir_path, name)
 
1098
                child_path = osutils.pathjoin(dir_path, name)
986
1099
                accum.append((child_path, ie))
987
1100
                if ie.kind == 'directory':
988
1101
                    descend(ie, child_path)
1001
1114
            kids.sort()
1002
1115
 
1003
1116
            for name, child_ie in kids:
1004
 
                child_path = pathjoin(parent_path, name)
 
1117
                child_path = osutils.pathjoin(parent_path, name)
1005
1118
                descend(child_ie, child_path)
1006
1119
        descend(self.root, u'')
1007
1120
        return accum
1031
1144
        try:
1032
1145
            return self._byid[file_id]
1033
1146
        except KeyError:
1034
 
            if file_id is None:
1035
 
                raise BzrError("can't look up file_id None")
1036
 
            else:
1037
 
                raise BzrError("file_id {%s} not in inventory" % file_id)
 
1147
            # really we're passing an inventory, not a tree...
 
1148
            raise errors.NoSuchId(self, file_id)
1038
1149
 
1039
1150
    def get_file_kind(self, file_id):
1040
1151
        return self._byid[file_id].kind
1042
1153
    def get_child(self, parent_id, filename):
1043
1154
        return self[parent_id].children.get(filename)
1044
1155
 
 
1156
    def _add_child(self, entry):
 
1157
        """Add an entry to the inventory, without adding it to its parent"""
 
1158
        if entry.file_id in self._byid:
 
1159
            raise BzrError("inventory already contains entry with id {%s}" %
 
1160
                           entry.file_id)
 
1161
        self._byid[entry.file_id] = entry
 
1162
        for child in getattr(entry, 'children', {}).itervalues():
 
1163
            self._add_child(child)
 
1164
        return entry
 
1165
 
1045
1166
    def add(self, entry):
1046
1167
        """Add entry to inventory.
1047
1168
 
1051
1172
        Returns the new entry object.
1052
1173
        """
1053
1174
        if entry.file_id in self._byid:
1054
 
            raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
 
1175
            raise errors.DuplicateFileId(entry.file_id,
 
1176
                                         self._byid[entry.file_id])
1055
1177
 
1056
1178
        if entry.parent_id is None:
1057
1179
            assert self.root is None and len(self._byid) == 0
1058
 
            self._set_root(entry)
1059
 
            return entry
1060
 
        if entry.parent_id == ROOT_ID:
1061
 
            assert self.root is not None, self
1062
 
            entry.parent_id = self.root.file_id
1063
 
 
1064
 
        try:
1065
 
            parent = self._byid[entry.parent_id]
1066
 
        except KeyError:
1067
 
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
1068
 
 
1069
 
        if entry.name in parent.children:
1070
 
            raise BzrError("%s is already versioned" %
1071
 
                    pathjoin(self.id2path(parent.file_id), entry.name))
1072
 
 
1073
 
        self._byid[entry.file_id] = entry
1074
 
        parent.children[entry.name] = entry
1075
 
        return entry
 
1180
            self.root = entry
 
1181
        else:
 
1182
            try:
 
1183
                parent = self._byid[entry.parent_id]
 
1184
            except KeyError:
 
1185
                raise BzrError("parent_id {%s} not in inventory" %
 
1186
                               entry.parent_id)
 
1187
 
 
1188
            if entry.name in parent.children:
 
1189
                raise BzrError("%s is already versioned" %
 
1190
                        osutils.pathjoin(self.id2path(parent.file_id),
 
1191
                        entry.name).encode('utf-8'))
 
1192
            parent.children[entry.name] = entry
 
1193
        return self._add_child(entry)
1076
1194
 
1077
1195
    def add_path(self, relpath, kind, file_id=None, parent_id=None):
1078
1196
        """Add entry from a path.
1085
1203
 
1086
1204
        if len(parts) == 0:
1087
1205
            if file_id is None:
1088
 
                file_id = bzrlib.workingtree.gen_root_id()
 
1206
                file_id = generate_ids.gen_root_id()
1089
1207
            self.root = InventoryDirectory(file_id, '', None)
1090
1208
            self._byid = {self.root.file_id: self.root}
1091
 
            return
 
1209
            return self.root
1092
1210
        else:
1093
1211
            parent_path = parts[:-1]
1094
1212
            parent_id = self.path2id(parent_path)
1095
1213
            if parent_id is None:
1096
 
                raise NotVersionedError(path=parent_path)
 
1214
                raise errors.NotVersionedError(path=parent_path)
1097
1215
        ie = make_entry(kind, parts[-1], parent_id, file_id)
1098
1216
        return self.add(ie)
1099
1217
 
1151
1269
            try:
1152
1270
                ie = self._byid[file_id]
1153
1271
            except KeyError:
1154
 
                raise BzrError("file_id {%s} not found in inventory" % file_id)
 
1272
                raise errors.NoSuchId(tree=None, file_id=file_id)
1155
1273
            yield ie
1156
1274
            file_id = ie.parent_id
1157
1275
 
1193
1311
 
1194
1312
        Returns None IFF the path is not found.
1195
1313
        """
1196
 
        if isinstance(name, types.StringTypes):
1197
 
            name = splitpath(name)
 
1314
        if isinstance(name, basestring):
 
1315
            name = osutils.splitpath(name)
1198
1316
 
1199
1317
        # mutter("lookup path %r" % name)
1200
1318
 
1201
1319
        parent = self.root
 
1320
        if parent is None:
 
1321
            return None
1202
1322
        for f in name:
1203
1323
            try:
1204
 
                cie = parent.children[f]
 
1324
                children = getattr(parent, 'children', None)
 
1325
                if children is None:
 
1326
                    return None
 
1327
                cie = children[f]
1205
1328
                assert cie.name == f
1206
1329
                assert cie.parent_id == parent.file_id
1207
1330
                parent = cie
1232
1355
        for file_id in reversed(to_delete):
1233
1356
            ie = self[file_id]
1234
1357
            del self._byid[file_id]
1235
 
            if ie.parent_id is not None:
1236
 
                del self[ie.parent_id].children[ie.name]
 
1358
        if ie.parent_id is not None:
 
1359
            del self[ie.parent_id].children[ie.name]
 
1360
        else:
 
1361
            self.root = None
1237
1362
 
1238
1363
    def rename(self, file_id, new_parent_id, new_name):
1239
1364
        """Move a file within the inventory.
1240
1365
 
1241
1366
        This can change either the name, or the parent, or both.
1242
1367
 
1243
 
        This does not move the working file."""
 
1368
        This does not move the working file.
 
1369
        """
 
1370
        new_name = ensure_normalized_name(new_name)
1244
1371
        if not is_valid_name(new_name):
1245
1372
            raise BzrError("not an acceptable filename: %r" % new_name)
1246
1373
 
1264
1391
        file_ie.name = new_name
1265
1392
        file_ie.parent_id = new_parent_id
1266
1393
 
 
1394
    def is_root(self, file_id):
 
1395
        return self.root is not None and file_id == self.root.file_id
 
1396
 
 
1397
 
 
1398
entry_factory = {
 
1399
    'directory': InventoryDirectory,
 
1400
    'file': InventoryFile,
 
1401
    'symlink': InventoryLink,
 
1402
    'tree-reference': TreeReference
 
1403
}
1267
1404
 
1268
1405
def make_entry(kind, name, parent_id, file_id=None):
1269
1406
    """Create an inventory entry.
1274
1411
    :param file_id: the file_id to use. if None, one will be created.
1275
1412
    """
1276
1413
    if file_id is None:
1277
 
        file_id = bzrlib.workingtree.gen_file_id(name)
1278
 
 
 
1414
        file_id = generate_ids.gen_file_id(name)
 
1415
    name = ensure_normalized_name(name)
 
1416
    try:
 
1417
        factory = entry_factory[kind]
 
1418
    except KeyError:
 
1419
        raise BzrError("unknown kind %r" % kind)
 
1420
    return factory(file_id, name, parent_id)
 
1421
 
 
1422
 
 
1423
def ensure_normalized_name(name):
 
1424
    """Normalize name.
 
1425
 
 
1426
    :raises InvalidNormalization: When name is not normalized, and cannot be
 
1427
        accessed on this platform by the normalized path.
 
1428
    :return: The NFC/NFKC normalised version of name.
 
1429
    """
 
1430
    #------- This has been copied to bzrlib.dirstate.DirState.add, please
 
1431
    # keep them synchronised.
 
1432
    # we dont import normalized_filename directly because we want to be
 
1433
    # able to change the implementation at runtime for tests.
1279
1434
    norm_name, can_access = osutils.normalized_filename(name)
1280
1435
    if norm_name != name:
1281
1436
        if can_access:
1282
 
            name = norm_name
 
1437
            return norm_name
1283
1438
        else:
1284
1439
            # TODO: jam 20060701 This would probably be more useful
1285
1440
            #       if the error was raised with the full path
1286
1441
            raise errors.InvalidNormalization(name)
1287
 
 
1288
 
    if kind == 'directory':
1289
 
        return InventoryDirectory(file_id, name, parent_id)
1290
 
    elif kind == 'file':
1291
 
        return InventoryFile(file_id, name, parent_id)
1292
 
    elif kind == 'symlink':
1293
 
        return InventoryLink(file_id, name, parent_id)
1294
 
    else:
1295
 
        raise BzrError("unknown kind %r" % kind)
 
1442
    return name
1296
1443
 
1297
1444
 
1298
1445
_NAME_RE = None