~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2007-10-24 12:49:17 UTC
  • mfrom: (2935.1.1 ianc-integration)
  • Revision ID: pqm@pqm.ubuntu.com-20071024124917-xb75eckyxx6vkrlg
Makefile fixes - hooks.html generation & allow python to be overridden (Ian Clatworthy)

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}
898
965
    def iter_entries(self, from_dir=None):
899
966
        """Return (path, entry) pairs, in order by name."""
900
967
        if from_dir is None:
901
 
            assert self.root
 
968
            if self.root is None:
 
969
                return
902
970
            from_dir = self.root
903
971
            yield '', self.root
904
972
        elif isinstance(from_dir, basestring):
938
1006
                # if we finished all children, pop it off the stack
939
1007
                stack.pop()
940
1008
 
941
 
    def iter_entries_by_dir(self, from_dir=None):
 
1009
    def iter_entries_by_dir(self, from_dir=None, specific_file_ids=None,
 
1010
        yield_parents=False):
942
1011
        """Iterate over the entries in a directory first order.
943
1012
 
944
1013
        This returns all entries for a directory before returning
946
1015
        lexicographically sorted order, and is a hybrid between
947
1016
        depth-first and breadth-first.
948
1017
 
 
1018
        :param yield_parents: If True, yield the parents from the root leading
 
1019
            down to specific_file_ids that have been requested. This has no
 
1020
            impact if specific_file_ids is None.
949
1021
        :return: This yields (path, entry) pairs
950
1022
        """
 
1023
        if specific_file_ids and not isinstance(specific_file_ids, set):
 
1024
            specific_file_ids = set(specific_file_ids)
951
1025
        # TODO? Perhaps this should return the from_dir so that the root is
952
1026
        # yielded? or maybe an option?
953
1027
        if from_dir is None:
954
 
            assert self.root
 
1028
            if self.root is None:
 
1029
                return
 
1030
            # Optimize a common case
 
1031
            if (not yield_parents and specific_file_ids is not None and
 
1032
                len(specific_file_ids) == 1):
 
1033
                file_id = list(specific_file_ids)[0]
 
1034
                if file_id in self:
 
1035
                    yield self.id2path(file_id), self[file_id]
 
1036
                return 
955
1037
            from_dir = self.root
956
 
            yield '', self.root
 
1038
            if (specific_file_ids is None or yield_parents or
 
1039
                self.root.file_id in specific_file_ids):
 
1040
                yield u'', self.root
957
1041
        elif isinstance(from_dir, basestring):
958
1042
            from_dir = self._byid[from_dir]
 
1043
 
 
1044
        if specific_file_ids is not None:
 
1045
            # TODO: jam 20070302 This could really be done as a loop rather
 
1046
            #       than a bunch of recursive calls.
 
1047
            parents = set()
 
1048
            byid = self._byid
 
1049
            def add_ancestors(file_id):
 
1050
                if file_id not in byid:
 
1051
                    return
 
1052
                parent_id = byid[file_id].parent_id
 
1053
                if parent_id is None:
 
1054
                    return
 
1055
                if parent_id not in parents:
 
1056
                    parents.add(parent_id)
 
1057
                    add_ancestors(parent_id)
 
1058
            for file_id in specific_file_ids:
 
1059
                add_ancestors(file_id)
 
1060
        else:
 
1061
            parents = None
959
1062
            
960
1063
        stack = [(u'', from_dir)]
961
1064
        while stack:
966
1069
 
967
1070
                child_relpath = cur_relpath + child_name
968
1071
 
969
 
                yield child_relpath, child_ie
 
1072
                if (specific_file_ids is None or 
 
1073
                    child_ie.file_id in specific_file_ids or
 
1074
                    (yield_parents and child_ie.file_id in parents)):
 
1075
                    yield child_relpath, child_ie
970
1076
 
971
1077
                if child_ie.kind == 'directory':
972
 
                    child_dirs.append((child_relpath+'/', child_ie))
 
1078
                    if parents is None or child_ie.file_id in parents:
 
1079
                        child_dirs.append((child_relpath+'/', child_ie))
973
1080
            stack.extend(reversed(child_dirs))
974
1081
 
 
1082
    def make_entry(self, kind, name, parent_id, file_id=None):
 
1083
        """Simple thunk to bzrlib.inventory.make_entry."""
 
1084
        return make_entry(kind, name, parent_id, file_id)
 
1085
 
975
1086
    def entries(self):
976
1087
        """Return list of (path, ie) for all entries except the root.
977
1088
 
982
1093
            kids = dir_ie.children.items()
983
1094
            kids.sort()
984
1095
            for name, ie in kids:
985
 
                child_path = pathjoin(dir_path, name)
 
1096
                child_path = osutils.pathjoin(dir_path, name)
986
1097
                accum.append((child_path, ie))
987
1098
                if ie.kind == 'directory':
988
1099
                    descend(ie, child_path)
1001
1112
            kids.sort()
1002
1113
 
1003
1114
            for name, child_ie in kids:
1004
 
                child_path = pathjoin(parent_path, name)
 
1115
                child_path = osutils.pathjoin(parent_path, name)
1005
1116
                descend(child_ie, child_path)
1006
1117
        descend(self.root, u'')
1007
1118
        return accum
1017
1128
        >>> '456' in inv
1018
1129
        False
1019
1130
        """
1020
 
        return file_id in self._byid
 
1131
        return (file_id in self._byid)
1021
1132
 
1022
1133
    def __getitem__(self, file_id):
1023
1134
        """Return the entry for given file_id.
1031
1142
        try:
1032
1143
            return self._byid[file_id]
1033
1144
        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)
 
1145
            # really we're passing an inventory, not a tree...
 
1146
            raise errors.NoSuchId(self, file_id)
1038
1147
 
1039
1148
    def get_file_kind(self, file_id):
1040
1149
        return self._byid[file_id].kind
1042
1151
    def get_child(self, parent_id, filename):
1043
1152
        return self[parent_id].children.get(filename)
1044
1153
 
 
1154
    def _add_child(self, entry):
 
1155
        """Add an entry to the inventory, without adding it to its parent"""
 
1156
        if entry.file_id in self._byid:
 
1157
            raise BzrError("inventory already contains entry with id {%s}" %
 
1158
                           entry.file_id)
 
1159
        self._byid[entry.file_id] = entry
 
1160
        for child in getattr(entry, 'children', {}).itervalues():
 
1161
            self._add_child(child)
 
1162
        return entry
 
1163
 
1045
1164
    def add(self, entry):
1046
1165
        """Add entry to inventory.
1047
1166
 
1051
1170
        Returns the new entry object.
1052
1171
        """
1053
1172
        if entry.file_id in self._byid:
1054
 
            raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
 
1173
            raise errors.DuplicateFileId(entry.file_id,
 
1174
                                         self._byid[entry.file_id])
1055
1175
 
1056
1176
        if entry.parent_id is None:
1057
1177
            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
 
1178
            self.root = entry
 
1179
        else:
 
1180
            try:
 
1181
                parent = self._byid[entry.parent_id]
 
1182
            except KeyError:
 
1183
                raise BzrError("parent_id {%s} not in inventory" %
 
1184
                               entry.parent_id)
 
1185
 
 
1186
            if entry.name in parent.children:
 
1187
                raise BzrError("%s is already versioned" %
 
1188
                        osutils.pathjoin(self.id2path(parent.file_id),
 
1189
                        entry.name).encode('utf-8'))
 
1190
            parent.children[entry.name] = entry
 
1191
        return self._add_child(entry)
1076
1192
 
1077
1193
    def add_path(self, relpath, kind, file_id=None, parent_id=None):
1078
1194
        """Add entry from a path.
1085
1201
 
1086
1202
        if len(parts) == 0:
1087
1203
            if file_id is None:
1088
 
                file_id = bzrlib.workingtree.gen_root_id()
 
1204
                file_id = generate_ids.gen_root_id()
1089
1205
            self.root = InventoryDirectory(file_id, '', None)
1090
1206
            self._byid = {self.root.file_id: self.root}
1091
 
            return
 
1207
            return self.root
1092
1208
        else:
1093
1209
            parent_path = parts[:-1]
1094
1210
            parent_id = self.path2id(parent_path)
1095
1211
            if parent_id is None:
1096
 
                raise NotVersionedError(path=parent_path)
 
1212
                raise errors.NotVersionedError(path=parent_path)
1097
1213
        ie = make_entry(kind, parts[-1], parent_id, file_id)
1098
1214
        return self.add(ie)
1099
1215
 
1151
1267
            try:
1152
1268
                ie = self._byid[file_id]
1153
1269
            except KeyError:
1154
 
                raise BzrError("file_id {%s} not found in inventory" % file_id)
 
1270
                raise errors.NoSuchId(tree=None, file_id=file_id)
1155
1271
            yield ie
1156
1272
            file_id = ie.parent_id
1157
1273
 
1193
1309
 
1194
1310
        Returns None IFF the path is not found.
1195
1311
        """
1196
 
        if isinstance(name, types.StringTypes):
1197
 
            name = splitpath(name)
 
1312
        if isinstance(name, basestring):
 
1313
            name = osutils.splitpath(name)
1198
1314
 
1199
1315
        # mutter("lookup path %r" % name)
1200
1316
 
1201
1317
        parent = self.root
 
1318
        if parent is None:
 
1319
            return None
1202
1320
        for f in name:
1203
1321
            try:
1204
 
                cie = parent.children[f]
 
1322
                children = getattr(parent, 'children', None)
 
1323
                if children is None:
 
1324
                    return None
 
1325
                cie = children[f]
1205
1326
                assert cie.name == f
1206
1327
                assert cie.parent_id == parent.file_id
1207
1328
                parent = cie
1215
1336
        return bool(self.path2id(names))
1216
1337
 
1217
1338
    def has_id(self, file_id):
1218
 
        return self._byid.has_key(file_id)
 
1339
        return (file_id in self._byid)
 
1340
 
 
1341
    def remove_recursive_id(self, file_id):
 
1342
        """Remove file_id, and children, from the inventory.
 
1343
        
 
1344
        :param file_id: A file_id to remove.
 
1345
        """
 
1346
        to_find_delete = [self._byid[file_id]]
 
1347
        to_delete = []
 
1348
        while to_find_delete:
 
1349
            ie = to_find_delete.pop()
 
1350
            to_delete.append(ie.file_id)
 
1351
            if ie.kind == 'directory':
 
1352
                to_find_delete.extend(ie.children.values())
 
1353
        for file_id in reversed(to_delete):
 
1354
            ie = self[file_id]
 
1355
            del self._byid[file_id]
 
1356
        if ie.parent_id is not None:
 
1357
            del self[ie.parent_id].children[ie.name]
 
1358
        else:
 
1359
            self.root = None
1219
1360
 
1220
1361
    def rename(self, file_id, new_parent_id, new_name):
1221
1362
        """Move a file within the inventory.
1222
1363
 
1223
1364
        This can change either the name, or the parent, or both.
1224
1365
 
1225
 
        This does not move the working file."""
 
1366
        This does not move the working file.
 
1367
        """
 
1368
        new_name = ensure_normalized_name(new_name)
1226
1369
        if not is_valid_name(new_name):
1227
1370
            raise BzrError("not an acceptable filename: %r" % new_name)
1228
1371
 
1246
1389
        file_ie.name = new_name
1247
1390
        file_ie.parent_id = new_parent_id
1248
1391
 
 
1392
    def is_root(self, file_id):
 
1393
        return self.root is not None and file_id == self.root.file_id
 
1394
 
 
1395
 
 
1396
entry_factory = {
 
1397
    'directory': InventoryDirectory,
 
1398
    'file': InventoryFile,
 
1399
    'symlink': InventoryLink,
 
1400
    'tree-reference': TreeReference
 
1401
}
1249
1402
 
1250
1403
def make_entry(kind, name, parent_id, file_id=None):
1251
1404
    """Create an inventory entry.
1256
1409
    :param file_id: the file_id to use. if None, one will be created.
1257
1410
    """
1258
1411
    if file_id is None:
1259
 
        file_id = bzrlib.workingtree.gen_file_id(name)
1260
 
 
 
1412
        file_id = generate_ids.gen_file_id(name)
 
1413
    name = ensure_normalized_name(name)
 
1414
    try:
 
1415
        factory = entry_factory[kind]
 
1416
    except KeyError:
 
1417
        raise BzrError("unknown kind %r" % kind)
 
1418
    return factory(file_id, name, parent_id)
 
1419
 
 
1420
 
 
1421
def ensure_normalized_name(name):
 
1422
    """Normalize name.
 
1423
 
 
1424
    :raises InvalidNormalization: When name is not normalized, and cannot be
 
1425
        accessed on this platform by the normalized path.
 
1426
    :return: The NFC/NFKC normalised version of name.
 
1427
    """
 
1428
    #------- This has been copied to bzrlib.dirstate.DirState.add, please
 
1429
    # keep them synchronised.
 
1430
    # we dont import normalized_filename directly because we want to be
 
1431
    # able to change the implementation at runtime for tests.
1261
1432
    norm_name, can_access = osutils.normalized_filename(name)
1262
1433
    if norm_name != name:
1263
1434
        if can_access:
1264
 
            name = norm_name
 
1435
            return norm_name
1265
1436
        else:
1266
1437
            # TODO: jam 20060701 This would probably be more useful
1267
1438
            #       if the error was raised with the full path
1268
1439
            raise errors.InvalidNormalization(name)
1269
 
 
1270
 
    if kind == 'directory':
1271
 
        return InventoryDirectory(file_id, name, parent_id)
1272
 
    elif kind == 'file':
1273
 
        return InventoryFile(file_id, name, parent_id)
1274
 
    elif kind == 'symlink':
1275
 
        return InventoryLink(file_id, name, parent_id)
1276
 
    else:
1277
 
        raise BzrError("unknown kind %r" % kind)
 
1440
    return name
1278
1441
 
1279
1442
 
1280
1443
_NAME_RE = None