/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: Martin Pool
  • Date: 2006-10-06 02:04:17 UTC
  • mfrom: (1908.10.1 bench_usecases.merge2)
  • mto: This revision was merged to the branch mainline in revision 2068.
  • Revision ID: mbp@sourcefrog.net-20061006020417-4949ca86f4417a4d
merge additional fix from cfbolz

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# (C) 2005 Canonical Ltd
 
1
# Copyright (C) 2005, 2006 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
31
32
import os.path
32
33
import re
33
34
import sys
34
35
import tarfile
35
36
import types
 
37
from warnings import warn
36
38
 
37
39
import bzrlib
 
40
from bzrlib import errors, osutils
38
41
from bzrlib.osutils import (pumpfile, quotefn, splitpath, joinpath,
39
42
                            pathjoin, sha_strings)
 
43
from bzrlib.errors import (NotVersionedError, InvalidEntryName,
 
44
                           BzrError, BzrCheckError, BinaryFile)
40
45
from bzrlib.trace import mutter
41
 
from bzrlib.errors import (NotVersionedError, InvalidEntryName,
42
 
                           BzrError, BzrCheckError)
43
46
 
44
47
 
45
48
class InventoryEntry(object):
76
79
    >>> i.path2id('')
77
80
    'TREE_ROOT'
78
81
    >>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
79
 
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
 
82
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
80
83
    >>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
81
 
    InventoryFile('2323', 'hello.c', parent_id='123')
82
 
    >>> shouldbe = {0: 'src', 1: pathjoin('src','hello.c')}
 
84
    InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
 
85
    >>> shouldbe = {0: '', 1: 'src', 2: pathjoin('src','hello.c')}
83
86
    >>> for ix, j in enumerate(i.iter_entries()):
84
87
    ...   print (j[0] == shouldbe[ix], j[1])
85
88
    ... 
86
 
    (True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
87
 
    (True, InventoryFile('2323', 'hello.c', parent_id='123'))
 
89
    (True, InventoryDirectory('TREE_ROOT', '', parent_id=None, revision=None))
 
90
    (True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
 
91
    (True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
88
92
    >>> i.add(InventoryFile('2323', 'bye.c', '123'))
89
93
    Traceback (most recent call last):
90
94
    ...
91
95
    BzrError: inventory already contains entry with id {2323}
92
96
    >>> i.add(InventoryFile('2324', 'bye.c', '123'))
93
 
    InventoryFile('2324', 'bye.c', parent_id='123')
 
97
    InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
94
98
    >>> i.add(InventoryDirectory('2325', 'wibble', '123'))
95
 
    InventoryDirectory('2325', 'wibble', parent_id='123')
 
99
    InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
96
100
    >>> i.path2id('src/wibble')
97
101
    '2325'
98
102
    >>> '2325' in i
99
103
    True
100
104
    >>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
101
 
    InventoryFile('2326', 'wibble.c', parent_id='2325')
 
105
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
102
106
    >>> i['2326']
103
 
    InventoryFile('2326', 'wibble.c', parent_id='2325')
 
107
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
104
108
    >>> for path, entry in i.iter_entries():
105
109
    ...     print path
106
110
    ...     assert i.path2id(path)
107
111
    ... 
 
112
    <BLANKLINE>
108
113
    src
109
114
    src/bye.c
110
115
    src/hello.c
113
118
    >>> i.id2path('2326')
114
119
    'src/wibble/wibble.c'
115
120
    """
 
121
 
 
122
    # Constants returned by describe_change()
 
123
    #
 
124
    # TODO: These should probably move to some kind of FileChangeDescription 
 
125
    # class; that's like what's inside a TreeDelta but we want to be able to 
 
126
    # generate them just for one file at a time.
 
127
    RENAMED = 'renamed'
 
128
    MODIFIED_AND_RENAMED = 'modified and renamed'
116
129
    
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
    __slots__ = []
125
131
 
126
132
    def detect_changes(self, old_entry):
127
133
        """Return a (text_modified, meta_modified) from this to old_entry.
156
162
                            versioned_file_store,
157
163
                            transaction,
158
164
                            entry_vf=None):
159
 
        """Return the revisions and entries that directly preceed this.
 
165
        """Return the revisions and entries that directly precede this.
160
166
 
161
167
        Returned as a map from revision to inventory entry.
162
168
 
240
246
 
241
247
    def get_tar_item(self, root, dp, now, tree):
242
248
        """Get a tarfile item and a file stream for its content."""
243
 
        item = tarfile.TarInfo(pathjoin(root, dp))
 
249
        item = tarfile.TarInfo(pathjoin(root, dp).encode('utf8'))
244
250
        # TODO: would be cool to actually set it to the timestamp of the
245
251
        # revision it was last changed
246
252
        item.mtime = now
290
296
        """Return a short kind indicator useful for appending to names."""
291
297
        raise BzrError('unknown kind %r' % self.kind)
292
298
 
293
 
    known_kinds = ('file', 'directory', 'symlink', 'root_directory')
 
299
    known_kinds = ('file', 'directory', 'symlink')
294
300
 
295
301
    def _put_in_tar(self, item, tree):
296
302
        """populate item for stashing in a tar, and return the content stream.
307
313
        """
308
314
        fullpath = pathjoin(dest, dp)
309
315
        self._put_on_disk(fullpath, tree)
310
 
        mutter("  export {%s} kind %s to %s", self.file_id,
311
 
                self.kind, fullpath)
 
316
        # mutter("  export {%s} kind %s to %s", self.file_id,
 
317
        #         self.kind, fullpath)
312
318
 
313
319
    def _put_on_disk(self, fullpath, tree):
314
320
        """Put this entry onto disk at fullpath, from tree tree."""
315
321
        raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
316
322
 
317
323
    def sorted_children(self):
318
 
        l = self.children.items()
319
 
        l.sort()
320
 
        return l
 
324
        return sorted(self.children.items())
321
325
 
322
326
    @staticmethod
323
327
    def versionable_kind(kind):
324
 
        return kind in ('file', 'directory', 'symlink')
 
328
        return (kind in ('file', 'directory', 'symlink'))
325
329
 
326
330
    def check(self, checker, rev_id, inv, tree):
327
331
        """Check this inventory entry is intact.
337
341
        :param inv: Inventory from which the entry was loaded.
338
342
        :param tree: RevisionTree for this entry.
339
343
        """
340
 
        if self.parent_id != None:
 
344
        if self.parent_id is not None:
341
345
            if not inv.has_id(self.parent_id):
342
346
                raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
343
347
                        % (self.parent_id, rev_id))
348
352
        raise BzrCheckError('unknown entry kind %r in revision {%s}' % 
349
353
                            (self.kind, rev_id))
350
354
 
351
 
 
352
355
    def copy(self):
353
356
        """Clone this inventory entry."""
354
357
        raise NotImplementedError
355
358
 
356
 
    def _get_snapshot_change(self, previous_entries):
357
 
        if len(previous_entries) > 1:
358
 
            return 'merged'
359
 
        elif len(previous_entries) == 0:
 
359
    @staticmethod
 
360
    def describe_change(old_entry, new_entry):
 
361
        """Describe the change between old_entry and this.
 
362
        
 
363
        This smells of being an InterInventoryEntry situation, but as its
 
364
        the first one, we're making it a static method for now.
 
365
 
 
366
        An entry with a different parent, or different name is considered 
 
367
        to be renamed. Reparenting is an internal detail.
 
368
        Note that renaming the parent does not trigger a rename for the
 
369
        child entry itself.
 
370
        """
 
371
        # TODO: Perhaps return an object rather than just a string
 
372
        if old_entry is new_entry:
 
373
            # also the case of both being None
 
374
            return 'unchanged'
 
375
        elif old_entry is None:
360
376
            return 'added'
361
 
        else:
362
 
            return 'modified/renamed/reparented'
 
377
        elif new_entry is None:
 
378
            return 'removed'
 
379
        text_modified, meta_modified = new_entry.detect_changes(old_entry)
 
380
        if text_modified or meta_modified:
 
381
            modified = True
 
382
        else:
 
383
            modified = False
 
384
        # TODO 20060511 (mbp, rbc) factor out 'detect_rename' here.
 
385
        if old_entry.parent_id != new_entry.parent_id:
 
386
            renamed = True
 
387
        elif old_entry.name != new_entry.name:
 
388
            renamed = True
 
389
        else:
 
390
            renamed = False
 
391
        if renamed and not modified:
 
392
            return InventoryEntry.RENAMED
 
393
        if modified and not renamed:
 
394
            return 'modified'
 
395
        if modified and renamed:
 
396
            return InventoryEntry.MODIFIED_AND_RENAMED
 
397
        return 'unchanged'
363
398
 
364
399
    def __repr__(self):
365
 
        return ("%s(%r, %r, parent_id=%r)"
 
400
        return ("%s(%r, %r, parent_id=%r, revision=%r)"
366
401
                % (self.__class__.__name__,
367
402
                   self.file_id,
368
403
                   self.name,
369
 
                   self.parent_id))
 
404
                   self.parent_id,
 
405
                   self.revision))
370
406
 
371
407
    def snapshot(self, revision, path, previous_entries,
372
 
                 work_tree, weave_store, transaction):
 
408
                 work_tree, commit_builder):
373
409
        """Make a snapshot of this entry which may or may not have changed.
374
410
        
375
411
        This means that all its fields are populated, that it has its
376
412
        text stored in the text store or weave.
377
413
        """
378
 
        mutter('new parents of %s are %r', path, previous_entries)
 
414
        # mutter('new parents of %s are %r', path, previous_entries)
379
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
380
418
        if len(previous_entries) == 1:
381
419
            # cannot be unchanged unless there is only one parent file rev.
382
420
            parent_ie = previous_entries.values()[0]
383
421
            if self._unchanged(parent_ie):
384
 
                mutter("found unchanged entry")
 
422
                # mutter("found unchanged entry")
385
423
                self.revision = parent_ie.revision
386
424
                return "unchanged"
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)
 
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)
394
440
        self.revision = revision
395
 
        change = self._get_snapshot_change(previous_entries)
396
 
        self._snapshot_text(previous_entries, work_tree, weave_store,
397
 
                            transaction)
398
 
        return change
 
441
        self._snapshot_text(previous_entries, work_tree, commit_builder)
399
442
 
400
 
    def _snapshot_text(self, file_parents, work_tree, weave_store, transaction): 
 
443
    def _snapshot_text(self, file_parents, work_tree, commit_builder): 
401
444
        """Record the 'text' of this entry, whatever form that takes.
402
445
        
403
446
        This default implementation simply adds an empty text.
404
447
        """
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
        raise NotImplementedError(self._snapshot_text)
408
449
 
409
450
    def __eq__(self, other):
410
451
        if not isinstance(other, InventoryEntry):
431
472
    def _unchanged(self, previous_ie):
432
473
        """Has this entry changed relative to previous_ie.
433
474
 
434
 
        This method should be overriden in child classes.
 
475
        This method should be overridden in child classes.
435
476
        """
436
477
        compatible = True
437
478
        # different inv parent
459
500
 
460
501
class RootEntry(InventoryEntry):
461
502
 
 
503
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
504
                 'text_id', 'parent_id', 'children', 'executable', 
 
505
                 'revision', 'symlink_target']
 
506
 
462
507
    def _check(self, checker, rev_id, tree):
463
508
        """See InventoryEntry._check"""
464
509
 
465
510
    def __init__(self, file_id):
466
511
        self.file_id = file_id
467
512
        self.children = {}
468
 
        self.kind = 'root_directory'
 
513
        self.kind = 'directory'
469
514
        self.parent_id = None
470
515
        self.name = u''
 
516
        self.revision = None
 
517
        warn('RootEntry is deprecated as of bzr 0.10.  Please use '
 
518
             'InventoryDirectory instead.',
 
519
            DeprecationWarning, stacklevel=2)
471
520
 
472
521
    def __eq__(self, other):
473
522
        if not isinstance(other, RootEntry):
480
529
class InventoryDirectory(InventoryEntry):
481
530
    """A directory in an inventory."""
482
531
 
 
532
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
533
                 'text_id', 'parent_id', 'children', 'executable', 
 
534
                 'revision', 'symlink_target']
 
535
 
483
536
    def _check(self, checker, rev_id, tree):
484
537
        """See InventoryEntry._check"""
485
 
        if self.text_sha1 != None or self.text_size != None or self.text_id != None:
 
538
        if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
486
539
            raise BzrCheckError('directory {%s} has text in revision {%s}'
487
540
                                % (self.file_id, rev_id))
488
541
 
515
568
        """See InventoryEntry._put_on_disk."""
516
569
        os.mkdir(fullpath)
517
570
 
 
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
 
518
575
 
519
576
class InventoryFile(InventoryEntry):
520
577
    """A file in an inventory."""
521
578
 
 
579
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
580
                 'text_id', 'parent_id', 'children', 'executable', 
 
581
                 'revision', 'symlink_target']
 
582
 
522
583
    def _check(self, checker, tree_revision_id, tree):
523
584
        """See InventoryEntry._check"""
524
585
        t = (self.file_id, self.revision)
563
624
 
564
625
    def detect_changes(self, old_entry):
565
626
        """See InventoryEntry.detect_changes."""
566
 
        assert self.text_sha1 != None
567
 
        assert old_entry.text_sha1 != None
 
627
        assert self.text_sha1 is not None
 
628
        assert old_entry.text_sha1 is not None
568
629
        text_modified = (self.text_sha1 != old_entry.text_sha1)
569
630
        meta_modified = (self.executable != old_entry.executable)
570
631
        return text_modified, meta_modified
572
633
    def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
573
634
             output_to, reverse=False):
574
635
        """See InventoryEntry._diff."""
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)
 
636
        try:
 
637
            from_text = tree.get_file(self.file_id).readlines()
 
638
            if to_entry:
 
639
                to_text = to_tree.get_file(to_entry.file_id).readlines()
 
640
            else:
 
641
                to_text = []
 
642
            if not reverse:
 
643
                text_diff(from_label, from_text,
 
644
                          to_label, to_text, output_to)
 
645
            else:
 
646
                text_diff(to_label, to_text,
 
647
                          from_label, from_text, output_to)
 
648
        except BinaryFile:
 
649
            if reverse:
 
650
                label_pair = (to_label, from_label)
 
651
            else:
 
652
                label_pair = (from_label, to_label)
 
653
            print >> output_to, "Binary files %s and %s differ" % label_pair
586
654
 
587
655
    def has_text(self):
588
656
        """See InventoryEntry.has_text."""
615
683
 
616
684
    def _read_tree_state(self, path, work_tree):
617
685
        """See InventoryEntry._read_tree_state."""
618
 
        self.text_sha1 = work_tree.get_file_sha1(self.file_id)
619
 
        self.executable = work_tree.is_executable(self.file_id)
 
686
        self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
 
687
        # FIXME: 20050930 probe for the text size when getting sha1
 
688
        # in _read_tree_state
 
689
        self.executable = work_tree.is_executable(self.file_id, path=path)
 
690
 
 
691
    def __repr__(self):
 
692
        return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s)"
 
693
                % (self.__class__.__name__,
 
694
                   self.file_id,
 
695
                   self.name,
 
696
                   self.parent_id,
 
697
                   self.text_sha1,
 
698
                   self.text_size))
620
699
 
621
700
    def _forget_tree_state(self):
622
701
        self.text_sha1 = None
623
 
        self.executable = None
624
702
 
625
 
    def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
 
703
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
626
704
        """See InventoryEntry._snapshot_text."""
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
        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)
644
709
 
645
710
    def _unchanged(self, previous_ie):
646
711
        """See InventoryEntry._unchanged."""
659
724
class InventoryLink(InventoryEntry):
660
725
    """A file in an inventory."""
661
726
 
662
 
    __slots__ = ['symlink_target']
 
727
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
728
                 'text_id', 'parent_id', 'children', 'executable', 
 
729
                 'revision', 'symlink_target']
663
730
 
664
731
    def _check(self, checker, rev_id, tree):
665
732
        """See InventoryEntry._check"""
666
 
        if self.text_sha1 != None or self.text_size != None or self.text_id != None:
 
733
        if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
667
734
            raise BzrCheckError('symlink {%s} has text in revision {%s}'
668
735
                    % (self.file_id, rev_id))
669
 
        if self.symlink_target == None:
 
736
        if self.symlink_target is None:
670
737
            raise BzrCheckError('symlink {%s} has no target in revision {%s}'
671
738
                    % (self.file_id, rev_id))
672
739
 
740
807
            compatible = False
741
808
        return compatible
742
809
 
 
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)
 
814
 
743
815
 
744
816
class Inventory(object):
745
817
    """Inventory of versioned files in a tree.
760
832
 
761
833
    >>> inv = Inventory()
762
834
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
763
 
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT')
 
835
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
764
836
    >>> inv['123-123'].name
765
837
    'hello.c'
766
838
 
774
846
    May also look up by name:
775
847
 
776
848
    >>> [x[0] for x in inv.iter_entries()]
777
 
    ['hello.c']
 
849
    ['', u'hello.c']
778
850
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
779
851
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
780
 
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
 
852
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None)
781
853
    """
782
854
    def __init__(self, root_id=ROOT_ID, revision_id=None):
783
855
        """Create or read an inventory.
793
865
        # root id. Rather than generating a random one here.
794
866
        #if root_id is None:
795
867
        #    root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
796
 
        self.root = RootEntry(root_id)
 
868
        if root_id is not None:
 
869
            self._set_root(InventoryDirectory(root_id, '', None))
 
870
        else:
 
871
            self.root = None
 
872
            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
797
875
        self.revision_id = revision_id
 
876
 
 
877
    def _set_root(self, ie):
 
878
        self.root = ie
798
879
        self._byid = {self.root.file_id: self.root}
799
880
 
800
 
 
801
881
    def copy(self):
802
882
        # TODO: jam 20051218 Should copy also copy the revision_id?
803
 
        other = Inventory(self.root.file_id)
 
883
        entries = self.iter_entries()
 
884
        other = Inventory(entries.next()[1].file_id)
804
885
        # copy recursively so we know directories will be added before
805
886
        # their children.  There are more efficient ways than this...
806
 
        for path, entry in self.iter_entries():
807
 
            if entry == self.root:
808
 
                continue
 
887
        for path, entry in entries():
809
888
            other.add(entry.copy())
810
889
        return other
811
890
 
812
 
 
813
891
    def __iter__(self):
814
892
        return iter(self._byid)
815
893
 
816
 
 
817
894
    def __len__(self):
818
895
        """Returns number of entries."""
819
896
        return len(self._byid)
820
897
 
821
 
 
822
898
    def iter_entries(self, from_dir=None):
823
899
        """Return (path, entry) pairs, in order by name."""
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
 
 
 
900
        if from_dir is None:
 
901
            assert self.root
 
902
            from_dir = self.root
 
903
            yield '', self.root
 
904
        elif isinstance(from_dir, basestring):
 
905
            from_dir = self._byid[from_dir]
 
906
            
 
907
        # unrolling the recursive called changed the time from
 
908
        # 440ms/663ms (inline/total) to 116ms/116ms
 
909
        children = from_dir.children.items()
 
910
        children.sort()
 
911
        children = collections.deque(children)
 
912
        stack = [(u'', children)]
 
913
        while stack:
 
914
            from_dir_relpath, children = stack[-1]
 
915
 
 
916
            while children:
 
917
                name, ie = children.popleft()
 
918
 
 
919
                # we know that from_dir_relpath never ends in a slash
 
920
                # and 'f' doesn't begin with one, we can do a string op, rather
 
921
                # than the checks of pathjoin(), though this means that all paths
 
922
                # start with a slash
 
923
                path = from_dir_relpath + '/' + name
 
924
 
 
925
                yield path[1:], ie
 
926
 
 
927
                if ie.kind != 'directory':
 
928
                    continue
 
929
 
 
930
                # But do this child first
 
931
                new_children = ie.children.items()
 
932
                new_children.sort()
 
933
                new_children = collections.deque(new_children)
 
934
                stack.append((path, new_children))
 
935
                # Break out of inner loop, so that we start outer loop with child
 
936
                break
 
937
            else:
 
938
                # if we finished all children, pop it off the stack
 
939
                stack.pop()
 
940
 
 
941
    def iter_entries_by_dir(self, from_dir=None):
 
942
        """Iterate over the entries in a directory first order.
 
943
 
 
944
        This returns all entries for a directory before returning
 
945
        the entries for children of a directory. This is not
 
946
        lexicographically sorted order, and is a hybrid between
 
947
        depth-first and breadth-first.
 
948
 
 
949
        :return: This yields (path, entry) pairs
 
950
        """
 
951
        # TODO? Perhaps this should return the from_dir so that the root is
 
952
        # yielded? or maybe an option?
 
953
        if from_dir is None:
 
954
            assert self.root
 
955
            from_dir = self.root
 
956
            yield '', self.root
 
957
        elif isinstance(from_dir, basestring):
 
958
            from_dir = self._byid[from_dir]
 
959
            
 
960
        stack = [(u'', from_dir)]
 
961
        while stack:
 
962
            cur_relpath, cur_dir = stack.pop()
 
963
 
 
964
            child_dirs = []
 
965
            for child_name, child_ie in sorted(cur_dir.children.iteritems()):
 
966
 
 
967
                child_relpath = cur_relpath + child_name
 
968
 
 
969
                yield child_relpath, child_ie
 
970
 
 
971
                if child_ie.kind == 'directory':
 
972
                    child_dirs.append((child_relpath+'/', child_ie))
 
973
            stack.extend(reversed(child_dirs))
838
974
 
839
975
    def entries(self):
840
976
        """Return list of (path, ie) for all entries except the root.
854
990
        descend(self.root, u'')
855
991
        return accum
856
992
 
857
 
 
858
993
    def directories(self):
859
994
        """Return (path, entry) pairs for all directories, including the root.
860
995
        """
871
1006
        descend(self.root, u'')
872
1007
        return accum
873
1008
        
874
 
 
875
 
 
876
1009
    def __contains__(self, file_id):
877
1010
        """True if this entry contains a file with given id.
878
1011
 
879
1012
        >>> inv = Inventory()
880
1013
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
881
 
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
 
1014
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
882
1015
        >>> '123' in inv
883
1016
        True
884
1017
        >>> '456' in inv
885
1018
        False
886
1019
        """
887
 
        return file_id in self._byid
888
 
 
 
1020
        return (file_id in self._byid)
889
1021
 
890
1022
    def __getitem__(self, file_id):
891
1023
        """Return the entry for given file_id.
892
1024
 
893
1025
        >>> inv = Inventory()
894
1026
        >>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
895
 
        InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT')
 
1027
        InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
896
1028
        >>> inv['123123'].name
897
1029
        'hello.c'
898
1030
        """
899
1031
        try:
900
1032
            return self._byid[file_id]
901
1033
        except KeyError:
902
 
            if file_id == None:
 
1034
            if file_id is None:
903
1035
                raise BzrError("can't look up file_id None")
904
1036
            else:
905
1037
                raise BzrError("file_id {%s} not in inventory" % file_id)
906
1038
 
907
 
 
908
1039
    def get_file_kind(self, file_id):
909
1040
        return self._byid[file_id].kind
910
1041
 
911
1042
    def get_child(self, parent_id, filename):
912
1043
        return self[parent_id].children.get(filename)
913
1044
 
914
 
 
915
1045
    def add(self, entry):
916
1046
        """Add entry to inventory.
917
1047
 
923
1053
        if entry.file_id in self._byid:
924
1054
            raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
925
1055
 
926
 
        if entry.parent_id == ROOT_ID or entry.parent_id is None:
 
1056
        if entry.parent_id is None:
 
1057
            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
927
1062
            entry.parent_id = self.root.file_id
928
1063
 
929
1064
        try:
931
1066
        except KeyError:
932
1067
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
933
1068
 
934
 
        if parent.children.has_key(entry.name):
 
1069
        if entry.name in parent.children:
935
1070
            raise BzrError("%s is already versioned" %
936
1071
                    pathjoin(self.id2path(parent.file_id), entry.name))
937
1072
 
939
1074
        parent.children[entry.name] = entry
940
1075
        return entry
941
1076
 
942
 
 
943
 
    def add_path(self, relpath, kind, file_id=None):
 
1077
    def add_path(self, relpath, kind, file_id=None, parent_id=None):
944
1078
        """Add entry from a path.
945
1079
 
946
1080
        The immediate parent must already be versioned.
947
1081
 
948
1082
        Returns the new entry object."""
949
 
        from bzrlib.workingtree import gen_file_id
950
1083
        
951
 
        parts = bzrlib.osutils.splitpath(relpath)
952
 
 
953
 
        if file_id == None:
954
 
            file_id = gen_file_id(relpath)
 
1084
        parts = osutils.splitpath(relpath)
955
1085
 
956
1086
        if len(parts) == 0:
957
 
            self.root = RootEntry(file_id)
 
1087
            if file_id is None:
 
1088
                file_id = bzrlib.workingtree.gen_root_id()
 
1089
            self.root = InventoryDirectory(file_id, '', None)
958
1090
            self._byid = {self.root.file_id: self.root}
959
1091
            return
960
1092
        else:
961
1093
            parent_path = parts[:-1]
962
1094
            parent_id = self.path2id(parent_path)
963
 
            if parent_id == None:
 
1095
            if parent_id is None:
964
1096
                raise NotVersionedError(path=parent_path)
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)
 
1097
        ie = make_entry(kind, parts[-1], parent_id, file_id)
973
1098
        return self.add(ie)
974
1099
 
975
 
 
976
1100
    def __delitem__(self, file_id):
977
1101
        """Remove entry by id.
978
1102
 
979
1103
        >>> inv = Inventory()
980
1104
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
981
 
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
 
1105
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
982
1106
        >>> '123' in inv
983
1107
        True
984
1108
        >>> del inv['123']
994
1118
        if ie.parent_id is not None:
995
1119
            del self[ie.parent_id].children[ie.name]
996
1120
 
997
 
 
998
1121
    def __eq__(self, other):
999
1122
        """Compare two sets by comparing their contents.
1000
1123
 
1003
1126
        >>> i1 == i2
1004
1127
        True
1005
1128
        >>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
1006
 
        InventoryFile('123', 'foo', parent_id='TREE_ROOT')
 
1129
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
1007
1130
        >>> i1 == i2
1008
1131
        False
1009
1132
        >>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
1010
 
        InventoryFile('123', 'foo', parent_id='TREE_ROOT')
 
1133
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
1011
1134
        >>> i1 == i2
1012
1135
        True
1013
1136
        """
1014
1137
        if not isinstance(other, Inventory):
1015
1138
            return NotImplemented
1016
1139
 
1017
 
        if len(self._byid) != len(other._byid):
1018
 
            # shortcut: obviously not the same
1019
 
            return False
1020
 
 
1021
1140
        return self._byid == other._byid
1022
1141
 
1023
 
 
1024
1142
    def __ne__(self, other):
1025
1143
        return not self.__eq__(other)
1026
1144
 
1027
 
 
1028
1145
    def __hash__(self):
1029
1146
        raise ValueError('not hashable')
1030
1147
 
1031
1148
    def _iter_file_id_parents(self, file_id):
1032
1149
        """Yield the parents of file_id up to the root."""
1033
 
        while file_id != None:
 
1150
        while file_id is not None:
1034
1151
            try:
1035
1152
                ie = self._byid[file_id]
1036
1153
            except KeyError:
1074
1191
        This returns the entry of the last component in the path,
1075
1192
        which may be either a file or a directory.
1076
1193
 
1077
 
        Returns None iff the path is not found.
 
1194
        Returns None IFF the path is not found.
1078
1195
        """
1079
1196
        if isinstance(name, types.StringTypes):
1080
1197
            name = splitpath(name)
1081
1198
 
1082
 
        mutter("lookup path %r" % name)
 
1199
        # mutter("lookup path %r" % name)
1083
1200
 
1084
1201
        parent = self.root
1085
1202
        for f in name:
1094
1211
 
1095
1212
        return parent.file_id
1096
1213
 
1097
 
 
1098
1214
    def has_filename(self, names):
1099
1215
        return bool(self.path2id(names))
1100
1216
 
1101
 
 
1102
1217
    def has_id(self, file_id):
1103
 
        return self._byid.has_key(file_id)
 
1218
        return (file_id in self._byid)
1104
1219
 
 
1220
    def remove_recursive_id(self, file_id):
 
1221
        """Remove file_id, and children, from the inventory.
 
1222
        
 
1223
        :param file_id: A file_id to remove.
 
1224
        """
 
1225
        to_find_delete = [self._byid[file_id]]
 
1226
        to_delete = []
 
1227
        while to_find_delete:
 
1228
            ie = to_find_delete.pop()
 
1229
            to_delete.append(ie.file_id)
 
1230
            if ie.kind == 'directory':
 
1231
                to_find_delete.extend(ie.children.values())
 
1232
        for file_id in reversed(to_delete):
 
1233
            ie = self[file_id]
 
1234
            del self._byid[file_id]
 
1235
            if ie.parent_id is not None:
 
1236
                del self[ie.parent_id].children[ie.name]
1105
1237
 
1106
1238
    def rename(self, file_id, new_parent_id, new_name):
1107
1239
        """Move a file within the inventory.
1133
1265
        file_ie.parent_id = new_parent_id
1134
1266
 
1135
1267
 
 
1268
def make_entry(kind, name, parent_id, file_id=None):
 
1269
    """Create an inventory entry.
 
1270
 
 
1271
    :param kind: the type of inventory entry to create.
 
1272
    :param name: the basename of the entry.
 
1273
    :param parent_id: the parent_id of the entry.
 
1274
    :param file_id: the file_id to use. if None, one will be created.
 
1275
    """
 
1276
    if file_id is None:
 
1277
        file_id = bzrlib.workingtree.gen_file_id(name)
 
1278
 
 
1279
    norm_name, can_access = osutils.normalized_filename(name)
 
1280
    if norm_name != name:
 
1281
        if can_access:
 
1282
            name = norm_name
 
1283
        else:
 
1284
            # TODO: jam 20060701 This would probably be more useful
 
1285
            #       if the error was raised with the full path
 
1286
            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)
1136
1296
 
1137
1297
 
1138
1298
_NAME_RE = None
1139
1299
 
1140
1300
def is_valid_name(name):
1141
1301
    global _NAME_RE
1142
 
    if _NAME_RE == None:
 
1302
    if _NAME_RE is None:
1143
1303
        _NAME_RE = re.compile(r'^[^/\\]+$')
1144
1304
        
1145
1305
    return bool(_NAME_RE.match(name))