~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

Use numbered backup files

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006 Canonical Ltd
 
1
# (C) 2005 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
28
28
ROOT_ID = "TREE_ROOT"
29
29
 
30
30
 
31
 
import collections
32
31
import os.path
33
32
import re
34
33
import sys
36
35
import types
37
36
 
38
37
import bzrlib
39
 
from bzrlib import errors, osutils
40
38
from bzrlib.osutils import (pumpfile, quotefn, splitpath, joinpath,
41
39
                            pathjoin, sha_strings)
 
40
from bzrlib.trace import mutter
42
41
from bzrlib.errors import (NotVersionedError, InvalidEntryName,
43
 
                           BzrError, BzrCheckError, BinaryFile)
44
 
from bzrlib.trace import mutter
 
42
                           BzrError, BzrCheckError)
45
43
 
46
44
 
47
45
class InventoryEntry(object):
78
76
    >>> i.path2id('')
79
77
    'TREE_ROOT'
80
78
    >>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
81
 
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
 
79
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
82
80
    >>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
83
 
    InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
84
 
    >>> shouldbe = {0: '', 1: 'src', 2: pathjoin('src','hello.c')}
 
81
    InventoryFile('2323', 'hello.c', parent_id='123')
 
82
    >>> shouldbe = {0: 'src', 1: pathjoin('src','hello.c')}
85
83
    >>> for ix, j in enumerate(i.iter_entries()):
86
84
    ...   print (j[0] == shouldbe[ix], j[1])
87
85
    ... 
88
 
    (True, RootEntry('TREE_ROOT', u'', parent_id=None, revision=None))
89
 
    (True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
90
 
    (True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
 
86
    (True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
 
87
    (True, InventoryFile('2323', 'hello.c', parent_id='123'))
91
88
    >>> i.add(InventoryFile('2323', 'bye.c', '123'))
92
89
    Traceback (most recent call last):
93
90
    ...
94
91
    BzrError: inventory already contains entry with id {2323}
95
92
    >>> i.add(InventoryFile('2324', 'bye.c', '123'))
96
 
    InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
 
93
    InventoryFile('2324', 'bye.c', parent_id='123')
97
94
    >>> i.add(InventoryDirectory('2325', 'wibble', '123'))
98
 
    InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
 
95
    InventoryDirectory('2325', 'wibble', parent_id='123')
99
96
    >>> i.path2id('src/wibble')
100
97
    '2325'
101
98
    >>> '2325' in i
102
99
    True
103
100
    >>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
104
 
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
 
101
    InventoryFile('2326', 'wibble.c', parent_id='2325')
105
102
    >>> i['2326']
106
 
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
 
103
    InventoryFile('2326', 'wibble.c', parent_id='2325')
107
104
    >>> for path, entry in i.iter_entries():
108
105
    ...     print path
109
106
    ...     assert i.path2id(path)
110
107
    ... 
111
 
    <BLANKLINE>
112
108
    src
113
109
    src/bye.c
114
110
    src/hello.c
117
113
    >>> i.id2path('2326')
118
114
    'src/wibble/wibble.c'
119
115
    """
120
 
 
121
 
    # Constants returned by describe_change()
122
 
    #
123
 
    # TODO: These should probably move to some kind of FileChangeDescription 
124
 
    # class; that's like what's inside a TreeDelta but we want to be able to 
125
 
    # generate them just for one file at a time.
126
 
    RENAMED = 'renamed'
127
 
    MODIFIED_AND_RENAMED = 'modified and renamed'
128
116
    
129
 
    __slots__ = []
 
117
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
118
                 'text_id', 'parent_id', 'children', 'executable', 
 
119
                 'revision']
 
120
 
 
121
    def _add_text_to_weave(self, new_lines, parents, weave_store, transaction):
 
122
        versionedfile = weave_store.get_weave_or_empty(self.file_id,
 
123
                                                       transaction)
 
124
        versionedfile.add_lines(self.revision, parents, new_lines)
130
125
 
131
126
    def detect_changes(self, old_entry):
132
127
        """Return a (text_modified, meta_modified) from this to old_entry.
161
156
                            versioned_file_store,
162
157
                            transaction,
163
158
                            entry_vf=None):
164
 
        """Return the revisions and entries that directly precede this.
 
159
        """Return the revisions and entries that directly preceed this.
165
160
 
166
161
        Returned as a map from revision to inventory entry.
167
162
 
312
307
        """
313
308
        fullpath = pathjoin(dest, dp)
314
309
        self._put_on_disk(fullpath, tree)
315
 
        # mutter("  export {%s} kind %s to %s", self.file_id,
316
 
        #         self.kind, fullpath)
 
310
        mutter("  export {%s} kind %s to %s", self.file_id,
 
311
                self.kind, fullpath)
317
312
 
318
313
    def _put_on_disk(self, fullpath, tree):
319
314
        """Put this entry onto disk at fullpath, from tree tree."""
320
315
        raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
321
316
 
322
317
    def sorted_children(self):
323
 
        return sorted(self.children.items())
 
318
        l = self.children.items()
 
319
        l.sort()
 
320
        return l
324
321
 
325
322
    @staticmethod
326
323
    def versionable_kind(kind):
340
337
        :param inv: Inventory from which the entry was loaded.
341
338
        :param tree: RevisionTree for this entry.
342
339
        """
343
 
        if self.parent_id is not None:
 
340
        if self.parent_id != None:
344
341
            if not inv.has_id(self.parent_id):
345
342
                raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
346
343
                        % (self.parent_id, rev_id))
351
348
        raise BzrCheckError('unknown entry kind %r in revision {%s}' % 
352
349
                            (self.kind, rev_id))
353
350
 
 
351
 
354
352
    def copy(self):
355
353
        """Clone this inventory entry."""
356
354
        raise NotImplementedError
357
355
 
358
 
    @staticmethod
359
 
    def describe_change(old_entry, new_entry):
360
 
        """Describe the change between old_entry and this.
361
 
        
362
 
        This smells of being an InterInventoryEntry situation, but as its
363
 
        the first one, we're making it a static method for now.
364
 
 
365
 
        An entry with a different parent, or different name is considered 
366
 
        to be renamed. Reparenting is an internal detail.
367
 
        Note that renaming the parent does not trigger a rename for the
368
 
        child entry itself.
369
 
        """
370
 
        # TODO: Perhaps return an object rather than just a string
371
 
        if old_entry is new_entry:
372
 
            # also the case of both being None
373
 
            return 'unchanged'
374
 
        elif old_entry is None:
 
356
    def _get_snapshot_change(self, previous_entries):
 
357
        if len(previous_entries) > 1:
 
358
            return 'merged'
 
359
        elif len(previous_entries) == 0:
375
360
            return 'added'
376
 
        elif new_entry is None:
377
 
            return 'removed'
378
 
        text_modified, meta_modified = new_entry.detect_changes(old_entry)
379
 
        if text_modified or meta_modified:
380
 
            modified = True
381
 
        else:
382
 
            modified = False
383
 
        # TODO 20060511 (mbp, rbc) factor out 'detect_rename' here.
384
 
        if old_entry.parent_id != new_entry.parent_id:
385
 
            renamed = True
386
 
        elif old_entry.name != new_entry.name:
387
 
            renamed = True
388
 
        else:
389
 
            renamed = False
390
 
        if renamed and not modified:
391
 
            return InventoryEntry.RENAMED
392
 
        if modified and not renamed:
393
 
            return 'modified'
394
 
        if modified and renamed:
395
 
            return InventoryEntry.MODIFIED_AND_RENAMED
396
 
        return 'unchanged'
 
361
        else:
 
362
            return 'modified/renamed/reparented'
397
363
 
398
364
    def __repr__(self):
399
 
        return ("%s(%r, %r, parent_id=%r, revision=%r)"
 
365
        return ("%s(%r, %r, parent_id=%r)"
400
366
                % (self.__class__.__name__,
401
367
                   self.file_id,
402
368
                   self.name,
403
 
                   self.parent_id,
404
 
                   self.revision))
 
369
                   self.parent_id))
405
370
 
406
371
    def snapshot(self, revision, path, previous_entries,
407
 
                 work_tree, commit_builder):
 
372
                 work_tree, weave_store, transaction):
408
373
        """Make a snapshot of this entry which may or may not have changed.
409
374
        
410
375
        This means that all its fields are populated, that it has its
411
376
        text stored in the text store or weave.
412
377
        """
413
 
        # mutter('new parents of %s are %r', path, previous_entries)
 
378
        mutter('new parents of %s are %r', path, previous_entries)
414
379
        self._read_tree_state(path, work_tree)
415
 
        # TODO: Where should we determine whether to reuse a
416
 
        # previous revision id or create a new revision? 20060606
417
380
        if len(previous_entries) == 1:
418
381
            # cannot be unchanged unless there is only one parent file rev.
419
382
            parent_ie = previous_entries.values()[0]
420
383
            if self._unchanged(parent_ie):
421
 
                # mutter("found unchanged entry")
 
384
                mutter("found unchanged entry")
422
385
                self.revision = parent_ie.revision
423
386
                return "unchanged"
424
 
        return self._snapshot_into_revision(revision, previous_entries, 
425
 
                                            work_tree, commit_builder)
426
 
 
427
 
    def _snapshot_into_revision(self, revision, previous_entries, work_tree,
428
 
                                commit_builder):
429
 
        """Record this revision unconditionally into a store.
430
 
 
431
 
        The entry's last-changed revision property (`revision`) is updated to 
432
 
        that of the new revision.
433
 
        
434
 
        :param revision: id of the new revision that is being recorded.
435
 
 
436
 
        :returns: String description of the commit (e.g. "merged", "modified"), etc.
437
 
        """
438
 
        # mutter('new revision {%s} for {%s}', revision, self.file_id)
 
387
        return self.snapshot_revision(revision, previous_entries, 
 
388
                                      work_tree, weave_store, transaction)
 
389
 
 
390
    def snapshot_revision(self, revision, previous_entries, work_tree,
 
391
                          weave_store, transaction):
 
392
        """Record this revision unconditionally."""
 
393
        mutter('new revision for {%s}', self.file_id)
439
394
        self.revision = revision
440
 
        self._snapshot_text(previous_entries, work_tree, commit_builder)
 
395
        change = self._get_snapshot_change(previous_entries)
 
396
        self._snapshot_text(previous_entries, work_tree, weave_store,
 
397
                            transaction)
 
398
        return change
441
399
 
442
 
    def _snapshot_text(self, file_parents, work_tree, commit_builder): 
 
400
    def _snapshot_text(self, file_parents, work_tree, weave_store, transaction): 
443
401
        """Record the 'text' of this entry, whatever form that takes.
444
402
        
445
403
        This default implementation simply adds an empty text.
446
404
        """
447
 
        raise NotImplementedError(self._snapshot_text)
 
405
        mutter('storing file {%s} in revision {%s}',
 
406
               self.file_id, self.revision)
 
407
        self._add_text_to_weave([], file_parents.keys(), weave_store, transaction)
448
408
 
449
409
    def __eq__(self, other):
450
410
        if not isinstance(other, InventoryEntry):
471
431
    def _unchanged(self, previous_ie):
472
432
        """Has this entry changed relative to previous_ie.
473
433
 
474
 
        This method should be overridden in child classes.
 
434
        This method should be overriden in child classes.
475
435
        """
476
436
        compatible = True
477
437
        # different inv parent
499
459
 
500
460
class RootEntry(InventoryEntry):
501
461
 
502
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
503
 
                 'text_id', 'parent_id', 'children', 'executable', 
504
 
                 'revision', 'symlink_target']
505
 
 
506
462
    def _check(self, checker, rev_id, tree):
507
463
        """See InventoryEntry._check"""
508
464
 
512
468
        self.kind = 'root_directory'
513
469
        self.parent_id = None
514
470
        self.name = u''
515
 
        self.revision = None
516
471
 
517
472
    def __eq__(self, other):
518
473
        if not isinstance(other, RootEntry):
525
480
class InventoryDirectory(InventoryEntry):
526
481
    """A directory in an inventory."""
527
482
 
528
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
529
 
                 'text_id', 'parent_id', 'children', 'executable', 
530
 
                 'revision', 'symlink_target']
531
 
 
532
483
    def _check(self, checker, rev_id, tree):
533
484
        """See InventoryEntry._check"""
534
 
        if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
 
485
        if self.text_sha1 != None or self.text_size != None or self.text_id != None:
535
486
            raise BzrCheckError('directory {%s} has text in revision {%s}'
536
487
                                % (self.file_id, rev_id))
537
488
 
564
515
        """See InventoryEntry._put_on_disk."""
565
516
        os.mkdir(fullpath)
566
517
 
567
 
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
568
 
        """See InventoryEntry._snapshot_text."""
569
 
        commit_builder.modified_directory(self.file_id, file_parents)
570
 
 
571
518
 
572
519
class InventoryFile(InventoryEntry):
573
520
    """A file in an inventory."""
574
521
 
575
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
576
 
                 'text_id', 'parent_id', 'children', 'executable', 
577
 
                 'revision', 'symlink_target']
578
 
 
579
522
    def _check(self, checker, tree_revision_id, tree):
580
523
        """See InventoryEntry._check"""
581
524
        t = (self.file_id, self.revision)
620
563
 
621
564
    def detect_changes(self, old_entry):
622
565
        """See InventoryEntry.detect_changes."""
623
 
        assert self.text_sha1 is not None
624
 
        assert old_entry.text_sha1 is not None
 
566
        assert self.text_sha1 != None
 
567
        assert old_entry.text_sha1 != None
625
568
        text_modified = (self.text_sha1 != old_entry.text_sha1)
626
569
        meta_modified = (self.executable != old_entry.executable)
627
570
        return text_modified, meta_modified
629
572
    def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
630
573
             output_to, reverse=False):
631
574
        """See InventoryEntry._diff."""
632
 
        try:
633
 
            from_text = tree.get_file(self.file_id).readlines()
634
 
            if to_entry:
635
 
                to_text = to_tree.get_file(to_entry.file_id).readlines()
636
 
            else:
637
 
                to_text = []
638
 
            if not reverse:
639
 
                text_diff(from_label, from_text,
640
 
                          to_label, to_text, output_to)
641
 
            else:
642
 
                text_diff(to_label, to_text,
643
 
                          from_label, from_text, output_to)
644
 
        except BinaryFile:
645
 
            if reverse:
646
 
                label_pair = (to_label, from_label)
647
 
            else:
648
 
                label_pair = (from_label, to_label)
649
 
            print >> output_to, "Binary files %s and %s differ" % label_pair
 
575
        from_text = tree.get_file(self.file_id).readlines()
 
576
        if to_entry:
 
577
            to_text = to_tree.get_file(to_entry.file_id).readlines()
 
578
        else:
 
579
            to_text = []
 
580
        if not reverse:
 
581
            text_diff(from_label, from_text,
 
582
                      to_label, to_text, output_to)
 
583
        else:
 
584
            text_diff(to_label, to_text,
 
585
                      from_label, from_text, output_to)
650
586
 
651
587
    def has_text(self):
652
588
        """See InventoryEntry.has_text."""
679
615
 
680
616
    def _read_tree_state(self, path, work_tree):
681
617
        """See InventoryEntry._read_tree_state."""
682
 
        self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
683
 
        # FIXME: 20050930 probe for the text size when getting sha1
684
 
        # in _read_tree_state
685
 
        self.executable = work_tree.is_executable(self.file_id, path=path)
686
 
 
687
 
    def __repr__(self):
688
 
        return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s)"
689
 
                % (self.__class__.__name__,
690
 
                   self.file_id,
691
 
                   self.name,
692
 
                   self.parent_id,
693
 
                   self.text_sha1,
694
 
                   self.text_size))
 
618
        self.text_sha1 = work_tree.get_file_sha1(self.file_id)
 
619
        self.executable = work_tree.is_executable(self.file_id)
695
620
 
696
621
    def _forget_tree_state(self):
697
622
        self.text_sha1 = None
 
623
        self.executable = None
698
624
 
699
 
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
 
625
    def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
700
626
        """See InventoryEntry._snapshot_text."""
701
 
        def get_content_byte_lines():
702
 
            return work_tree.get_file(self.file_id).readlines()
703
 
        self.text_sha1, self.text_size = commit_builder.modified_file_text(
704
 
            self.file_id, file_parents, get_content_byte_lines, self.text_sha1, self.text_size)
 
627
        mutter('storing file {%s} in revision {%s}',
 
628
               self.file_id, self.revision)
 
629
        # special case to avoid diffing on renames or 
 
630
        # reparenting
 
631
        if (len(file_parents) == 1
 
632
            and self.text_sha1 == file_parents.values()[0].text_sha1
 
633
            and self.text_size == file_parents.values()[0].text_size):
 
634
            previous_ie = file_parents.values()[0]
 
635
            versionedfile = weave_store.get_weave(self.file_id, transaction)
 
636
            versionedfile.clone_text(self.revision, previous_ie.revision, file_parents.keys())
 
637
        else:
 
638
            new_lines = work_tree.get_file(self.file_id).readlines()
 
639
            self._add_text_to_weave(new_lines, file_parents.keys(), weave_store,
 
640
                                    transaction)
 
641
            self.text_sha1 = sha_strings(new_lines)
 
642
            self.text_size = sum(map(len, new_lines))
 
643
 
705
644
 
706
645
    def _unchanged(self, previous_ie):
707
646
        """See InventoryEntry._unchanged."""
720
659
class InventoryLink(InventoryEntry):
721
660
    """A file in an inventory."""
722
661
 
723
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
724
 
                 'text_id', 'parent_id', 'children', 'executable', 
725
 
                 'revision', 'symlink_target']
 
662
    __slots__ = ['symlink_target']
726
663
 
727
664
    def _check(self, checker, rev_id, tree):
728
665
        """See InventoryEntry._check"""
729
 
        if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
 
666
        if self.text_sha1 != None or self.text_size != None or self.text_id != None:
730
667
            raise BzrCheckError('symlink {%s} has text in revision {%s}'
731
668
                    % (self.file_id, rev_id))
732
 
        if self.symlink_target is None:
 
669
        if self.symlink_target == None:
733
670
            raise BzrCheckError('symlink {%s} has no target in revision {%s}'
734
671
                    % (self.file_id, rev_id))
735
672
 
803
740
            compatible = False
804
741
        return compatible
805
742
 
806
 
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
807
 
        """See InventoryEntry._snapshot_text."""
808
 
        commit_builder.modified_link(
809
 
            self.file_id, file_parents, self.symlink_target)
810
 
 
811
743
 
812
744
class Inventory(object):
813
745
    """Inventory of versioned files in a tree.
828
760
 
829
761
    >>> inv = Inventory()
830
762
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
831
 
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
 
763
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT')
832
764
    >>> inv['123-123'].name
833
765
    'hello.c'
834
766
 
842
774
    May also look up by name:
843
775
 
844
776
    >>> [x[0] for x in inv.iter_entries()]
845
 
    ['', u'hello.c']
 
777
    ['hello.c']
846
778
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
847
779
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
848
 
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None)
 
780
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
849
781
    """
850
782
    def __init__(self, root_id=ROOT_ID, revision_id=None):
851
783
        """Create or read an inventory.
862
794
        #if root_id is None:
863
795
        #    root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
864
796
        self.root = RootEntry(root_id)
865
 
        # FIXME: this isn't ever used, changing it to self.revision may break
866
 
        # things. TODO make everything use self.revision_id
867
797
        self.revision_id = revision_id
868
798
        self._byid = {self.root.file_id: self.root}
869
799
 
 
800
 
870
801
    def copy(self):
871
802
        # TODO: jam 20051218 Should copy also copy the revision_id?
872
 
        entries = self.iter_entries()
873
 
        other = Inventory(entries.next()[1].file_id)
 
803
        other = Inventory(self.root.file_id)
874
804
        # copy recursively so we know directories will be added before
875
805
        # their children.  There are more efficient ways than this...
876
 
        for path, entry in entries():
 
806
        for path, entry in self.iter_entries():
 
807
            if entry == self.root:
 
808
                continue
877
809
            other.add(entry.copy())
878
810
        return other
879
811
 
 
812
 
880
813
    def __iter__(self):
881
814
        return iter(self._byid)
882
815
 
 
816
 
883
817
    def __len__(self):
884
818
        """Returns number of entries."""
885
819
        return len(self._byid)
886
820
 
 
821
 
887
822
    def iter_entries(self, from_dir=None):
888
823
        """Return (path, entry) pairs, in order by name."""
889
 
        if from_dir is None:
890
 
            assert self.root
891
 
            from_dir = self.root
892
 
            yield '', self.root
893
 
        elif isinstance(from_dir, basestring):
894
 
            from_dir = self._byid[from_dir]
895
 
            
896
 
        # unrolling the recursive called changed the time from
897
 
        # 440ms/663ms (inline/total) to 116ms/116ms
898
 
        children = from_dir.children.items()
899
 
        children.sort()
900
 
        children = collections.deque(children)
901
 
        stack = [(u'', children)]
902
 
        while stack:
903
 
            from_dir_relpath, children = stack[-1]
904
 
 
905
 
            while children:
906
 
                name, ie = children.popleft()
907
 
 
908
 
                # we know that from_dir_relpath never ends in a slash
909
 
                # and 'f' doesn't begin with one, we can do a string op, rather
910
 
                # than the checks of pathjoin(), though this means that all paths
911
 
                # start with a slash
912
 
                path = from_dir_relpath + '/' + name
913
 
 
914
 
                yield path[1:], ie
915
 
 
916
 
                if ie.kind != 'directory':
917
 
                    continue
918
 
 
919
 
                # But do this child first
920
 
                new_children = ie.children.items()
921
 
                new_children.sort()
922
 
                new_children = collections.deque(new_children)
923
 
                stack.append((path, new_children))
924
 
                # Break out of inner loop, so that we start outer loop with child
925
 
                break
926
 
            else:
927
 
                # if we finished all children, pop it off the stack
928
 
                stack.pop()
929
 
 
930
 
    def iter_entries_by_dir(self, from_dir=None):
931
 
        """Iterate over the entries in a directory first order.
932
 
 
933
 
        This returns all entries for a directory before returning
934
 
        the entries for children of a directory. This is not
935
 
        lexicographically sorted order, and is a hybrid between
936
 
        depth-first and breadth-first.
937
 
 
938
 
        :return: This yields (path, entry) pairs
939
 
        """
940
 
        # TODO? Perhaps this should return the from_dir so that the root is
941
 
        # yielded? or maybe an option?
942
 
        if from_dir is None:
943
 
            assert self.root
944
 
            from_dir = self.root
945
 
            yield '', self.root
946
 
        elif isinstance(from_dir, basestring):
947
 
            from_dir = self._byid[from_dir]
948
 
            
949
 
        stack = [(u'', from_dir)]
950
 
        while stack:
951
 
            cur_relpath, cur_dir = stack.pop()
952
 
 
953
 
            child_dirs = []
954
 
            for child_name, child_ie in sorted(cur_dir.children.iteritems()):
955
 
 
956
 
                child_relpath = cur_relpath + child_name
957
 
 
958
 
                yield child_relpath, child_ie
959
 
 
960
 
                if child_ie.kind == 'directory':
961
 
                    child_dirs.append((child_relpath+'/', child_ie))
962
 
            stack.extend(reversed(child_dirs))
 
824
        if from_dir == None:
 
825
            assert self.root
 
826
            from_dir = self.root
 
827
        elif isinstance(from_dir, basestring):
 
828
            from_dir = self._byid[from_dir]
 
829
            
 
830
        kids = from_dir.children.items()
 
831
        kids.sort()
 
832
        for name, ie in kids:
 
833
            yield name, ie
 
834
            if ie.kind == 'directory':
 
835
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
 
836
                    yield pathjoin(name, cn), cie
 
837
 
963
838
 
964
839
    def entries(self):
965
840
        """Return list of (path, ie) for all entries except the root.
979
854
        descend(self.root, u'')
980
855
        return accum
981
856
 
 
857
 
982
858
    def directories(self):
983
859
        """Return (path, entry) pairs for all directories, including the root.
984
860
        """
995
871
        descend(self.root, u'')
996
872
        return accum
997
873
        
 
874
 
 
875
 
998
876
    def __contains__(self, file_id):
999
877
        """True if this entry contains a file with given id.
1000
878
 
1001
879
        >>> inv = Inventory()
1002
880
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1003
 
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
 
881
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1004
882
        >>> '123' in inv
1005
883
        True
1006
884
        >>> '456' in inv
1008
886
        """
1009
887
        return file_id in self._byid
1010
888
 
 
889
 
1011
890
    def __getitem__(self, file_id):
1012
891
        """Return the entry for given file_id.
1013
892
 
1014
893
        >>> inv = Inventory()
1015
894
        >>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
1016
 
        InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
 
895
        InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT')
1017
896
        >>> inv['123123'].name
1018
897
        'hello.c'
1019
898
        """
1020
899
        try:
1021
900
            return self._byid[file_id]
1022
901
        except KeyError:
1023
 
            if file_id is None:
 
902
            if file_id == None:
1024
903
                raise BzrError("can't look up file_id None")
1025
904
            else:
1026
905
                raise BzrError("file_id {%s} not in inventory" % file_id)
1027
906
 
 
907
 
1028
908
    def get_file_kind(self, file_id):
1029
909
        return self._byid[file_id].kind
1030
910
 
1031
911
    def get_child(self, parent_id, filename):
1032
912
        return self[parent_id].children.get(filename)
1033
913
 
 
914
 
1034
915
    def add(self, entry):
1035
916
        """Add entry to inventory.
1036
917
 
1050
931
        except KeyError:
1051
932
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
1052
933
 
1053
 
        if entry.name in parent.children:
 
934
        if parent.children.has_key(entry.name):
1054
935
            raise BzrError("%s is already versioned" %
1055
936
                    pathjoin(self.id2path(parent.file_id), entry.name))
1056
937
 
1058
939
        parent.children[entry.name] = entry
1059
940
        return entry
1060
941
 
1061
 
    def add_path(self, relpath, kind, file_id=None, parent_id=None):
 
942
 
 
943
    def add_path(self, relpath, kind, file_id=None):
1062
944
        """Add entry from a path.
1063
945
 
1064
946
        The immediate parent must already be versioned.
1065
947
 
1066
948
        Returns the new entry object."""
 
949
        from bzrlib.workingtree import gen_file_id
1067
950
        
1068
 
        parts = osutils.splitpath(relpath)
 
951
        parts = bzrlib.osutils.splitpath(relpath)
 
952
 
 
953
        if file_id == None:
 
954
            file_id = gen_file_id(relpath)
1069
955
 
1070
956
        if len(parts) == 0:
1071
 
            if file_id is None:
1072
 
                file_id = bzrlib.workingtree.gen_root_id()
1073
957
            self.root = RootEntry(file_id)
1074
958
            self._byid = {self.root.file_id: self.root}
1075
959
            return
1076
960
        else:
1077
961
            parent_path = parts[:-1]
1078
962
            parent_id = self.path2id(parent_path)
1079
 
            if parent_id is None:
 
963
            if parent_id == None:
1080
964
                raise NotVersionedError(path=parent_path)
1081
 
        ie = make_entry(kind, parts[-1], parent_id, file_id)
 
965
        if kind == 'directory':
 
966
            ie = InventoryDirectory(file_id, parts[-1], parent_id)
 
967
        elif kind == 'file':
 
968
            ie = InventoryFile(file_id, parts[-1], parent_id)
 
969
        elif kind == 'symlink':
 
970
            ie = InventoryLink(file_id, parts[-1], parent_id)
 
971
        else:
 
972
            raise BzrError("unknown kind %r" % kind)
1082
973
        return self.add(ie)
1083
974
 
 
975
 
1084
976
    def __delitem__(self, file_id):
1085
977
        """Remove entry by id.
1086
978
 
1087
979
        >>> inv = Inventory()
1088
980
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1089
 
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
 
981
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1090
982
        >>> '123' in inv
1091
983
        True
1092
984
        >>> del inv['123']
1102
994
        if ie.parent_id is not None:
1103
995
            del self[ie.parent_id].children[ie.name]
1104
996
 
 
997
 
1105
998
    def __eq__(self, other):
1106
999
        """Compare two sets by comparing their contents.
1107
1000
 
1110
1003
        >>> i1 == i2
1111
1004
        True
1112
1005
        >>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
1113
 
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
 
1006
        InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1114
1007
        >>> i1 == i2
1115
1008
        False
1116
1009
        >>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
1117
 
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
 
1010
        InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1118
1011
        >>> i1 == i2
1119
1012
        True
1120
1013
        """
1121
1014
        if not isinstance(other, Inventory):
1122
1015
            return NotImplemented
1123
1016
 
 
1017
        if len(self._byid) != len(other._byid):
 
1018
            # shortcut: obviously not the same
 
1019
            return False
 
1020
 
1124
1021
        return self._byid == other._byid
1125
1022
 
 
1023
 
1126
1024
    def __ne__(self, other):
1127
1025
        return not self.__eq__(other)
1128
1026
 
 
1027
 
1129
1028
    def __hash__(self):
1130
1029
        raise ValueError('not hashable')
1131
1030
 
1132
1031
    def _iter_file_id_parents(self, file_id):
1133
1032
        """Yield the parents of file_id up to the root."""
1134
 
        while file_id is not None:
 
1033
        while file_id != None:
1135
1034
            try:
1136
1035
                ie = self._byid[file_id]
1137
1036
            except KeyError:
1175
1074
        This returns the entry of the last component in the path,
1176
1075
        which may be either a file or a directory.
1177
1076
 
1178
 
        Returns None IFF the path is not found.
 
1077
        Returns None iff the path is not found.
1179
1078
        """
1180
1079
        if isinstance(name, types.StringTypes):
1181
1080
            name = splitpath(name)
1182
1081
 
1183
 
        # mutter("lookup path %r" % name)
 
1082
        mutter("lookup path %r" % name)
1184
1083
 
1185
1084
        parent = self.root
1186
1085
        for f in name:
1195
1094
 
1196
1095
        return parent.file_id
1197
1096
 
 
1097
 
1198
1098
    def has_filename(self, names):
1199
1099
        return bool(self.path2id(names))
1200
1100
 
 
1101
 
1201
1102
    def has_id(self, file_id):
1202
1103
        return self._byid.has_key(file_id)
1203
1104
 
 
1105
 
1204
1106
    def rename(self, file_id, new_parent_id, new_name):
1205
1107
        """Move a file within the inventory.
1206
1108
 
1231
1133
        file_ie.parent_id = new_parent_id
1232
1134
 
1233
1135
 
1234
 
def make_entry(kind, name, parent_id, file_id=None):
1235
 
    """Create an inventory entry.
1236
 
 
1237
 
    :param kind: the type of inventory entry to create.
1238
 
    :param name: the basename of the entry.
1239
 
    :param parent_id: the parent_id of the entry.
1240
 
    :param file_id: the file_id to use. if None, one will be created.
1241
 
    """
1242
 
    if file_id is None:
1243
 
        file_id = bzrlib.workingtree.gen_file_id(name)
1244
 
 
1245
 
    norm_name, can_access = osutils.normalized_filename(name)
1246
 
    if norm_name != name:
1247
 
        if can_access:
1248
 
            name = norm_name
1249
 
        else:
1250
 
            # TODO: jam 20060701 This would probably be more useful
1251
 
            #       if the error was raised with the full path
1252
 
            raise errors.InvalidNormalization(name)
1253
 
 
1254
 
    if kind == 'directory':
1255
 
        return InventoryDirectory(file_id, name, parent_id)
1256
 
    elif kind == 'file':
1257
 
        return InventoryFile(file_id, name, parent_id)
1258
 
    elif kind == 'symlink':
1259
 
        return InventoryLink(file_id, name, parent_id)
1260
 
    else:
1261
 
        raise BzrError("unknown kind %r" % kind)
1262
1136
 
1263
1137
 
1264
1138
_NAME_RE = None
1265
1139
 
1266
1140
def is_valid_name(name):
1267
1141
    global _NAME_RE
1268
 
    if _NAME_RE is None:
 
1142
    if _NAME_RE == None:
1269
1143
        _NAME_RE = re.compile(r'^[^/\\]+$')
1270
1144
        
1271
1145
    return bool(_NAME_RE.match(name))