/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: Robert Collins
  • Date: 2007-03-08 04:06:06 UTC
  • mfrom: (2323.1.1 integration)
  • mto: This revision was merged to the branch mainline in revision 2442.
  • Revision ID: robertc@robertcollins.net-20070308040606-84gsniv56huiyjt4
Merge bzr.dev.

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
38
 
38
39
import bzrlib
39
 
from bzrlib.osutils import (pumpfile, quotefn, splitpath, joinpath,
40
 
                            pathjoin, sha_strings)
41
 
from bzrlib.errors import (NotVersionedError, InvalidEntryName,
42
 
                           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
    )
43
53
from bzrlib.trace import mutter
44
54
 
45
55
 
77
87
    >>> i.path2id('')
78
88
    'TREE_ROOT'
79
89
    >>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
80
 
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
 
90
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
81
91
    >>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
82
 
    InventoryFile('2323', 'hello.c', parent_id='123')
83
 
    >>> shouldbe = {0: 'src', 1: pathjoin('src','hello.c')}
 
92
    InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
 
93
    >>> shouldbe = {0: '', 1: 'src', 2: 'src/hello.c'}
84
94
    >>> for ix, j in enumerate(i.iter_entries()):
85
95
    ...   print (j[0] == shouldbe[ix], j[1])
86
96
    ... 
87
 
    (True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
88
 
    (True, InventoryFile('2323', 'hello.c', parent_id='123'))
89
 
    >>> i.add(InventoryFile('2323', 'bye.c', '123'))
90
 
    Traceback (most recent call last):
91
 
    ...
92
 
    BzrError: inventory already contains entry with id {2323}
 
97
    (True, InventoryDirectory('TREE_ROOT', u'', parent_id=None, revision=None))
 
98
    (True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
 
99
    (True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
93
100
    >>> i.add(InventoryFile('2324', 'bye.c', '123'))
94
 
    InventoryFile('2324', 'bye.c', parent_id='123')
 
101
    InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
95
102
    >>> i.add(InventoryDirectory('2325', 'wibble', '123'))
96
 
    InventoryDirectory('2325', 'wibble', parent_id='123')
 
103
    InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
97
104
    >>> i.path2id('src/wibble')
98
105
    '2325'
99
106
    >>> '2325' in i
100
107
    True
101
108
    >>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
102
 
    InventoryFile('2326', 'wibble.c', parent_id='2325')
 
109
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
103
110
    >>> i['2326']
104
 
    InventoryFile('2326', 'wibble.c', parent_id='2325')
 
111
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
105
112
    >>> for path, entry in i.iter_entries():
106
113
    ...     print path
107
114
    ...     assert i.path2id(path)
108
115
    ... 
 
116
    <BLANKLINE>
109
117
    src
110
118
    src/bye.c
111
119
    src/hello.c
123
131
    RENAMED = 'renamed'
124
132
    MODIFIED_AND_RENAMED = 'modified and renamed'
125
133
    
126
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
127
 
                 'text_id', 'parent_id', 'children', 'executable', 
128
 
                 'revision']
129
 
 
130
 
    def _add_text_to_weave(self, new_lines, parents, weave_store, transaction):
131
 
        versionedfile = weave_store.get_weave_or_empty(self.file_id,
132
 
                                                       transaction)
133
 
        versionedfile.add_lines(self.revision, parents, new_lines)
134
 
        versionedfile.clear_cache()
 
134
    __slots__ = []
135
135
 
136
136
    def detect_changes(self, old_entry):
137
137
        """Return a (text_modified, meta_modified) from this to old_entry.
166
166
                            versioned_file_store,
167
167
                            transaction,
168
168
                            entry_vf=None):
169
 
        """Return the revisions and entries that directly preceed this.
 
169
        """Return the revisions and entries that directly precede this.
170
170
 
171
171
        Returned as a map from revision to inventory entry.
172
172
 
250
250
 
251
251
    def get_tar_item(self, root, dp, now, tree):
252
252
        """Get a tarfile item and a file stream for its content."""
253
 
        item = tarfile.TarInfo(pathjoin(root, dp))
 
253
        item = tarfile.TarInfo(osutils.pathjoin(root, dp).encode('utf8'))
254
254
        # TODO: would be cool to actually set it to the timestamp of the
255
255
        # revision it was last changed
256
256
        item.mtime = now
285
285
        """
286
286
        assert isinstance(name, basestring), name
287
287
        if '/' in name or '\\' in name:
288
 
            raise InvalidEntryName(name=name)
 
288
            raise errors.InvalidEntryName(name=name)
289
289
        self.executable = False
290
290
        self.revision = None
291
291
        self.text_sha1 = None
292
292
        self.text_size = None
293
293
        self.file_id = file_id
 
294
        assert isinstance(file_id, (str, None.__class__)), \
 
295
            'bad type %r for %r' % (type(file_id), file_id)
294
296
        self.name = name
295
297
        self.text_id = text_id
296
298
        self.parent_id = parent_id
297
299
        self.symlink_target = None
 
300
        self.reference_revision = None
298
301
 
299
302
    def kind_character(self):
300
303
        """Return a short kind indicator useful for appending to names."""
301
304
        raise BzrError('unknown kind %r' % self.kind)
302
305
 
303
 
    known_kinds = ('file', 'directory', 'symlink', 'root_directory')
 
306
    known_kinds = ('file', 'directory', 'symlink')
304
307
 
305
308
    def _put_in_tar(self, item, tree):
306
309
        """populate item for stashing in a tar, and return the content stream.
315
318
        
316
319
        This is a template method - implement _put_on_disk in subclasses.
317
320
        """
318
 
        fullpath = pathjoin(dest, dp)
 
321
        fullpath = osutils.pathjoin(dest, dp)
319
322
        self._put_on_disk(fullpath, tree)
320
 
        mutter("  export {%s} kind %s to %s", self.file_id,
321
 
                self.kind, fullpath)
 
323
        # mutter("  export {%s} kind %s to %s", self.file_id,
 
324
        #         self.kind, fullpath)
322
325
 
323
326
    def _put_on_disk(self, fullpath, tree):
324
327
        """Put this entry onto disk at fullpath, from tree tree."""
325
328
        raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
326
329
 
327
330
    def sorted_children(self):
328
 
        l = self.children.items()
329
 
        l.sort()
330
 
        return l
 
331
        return sorted(self.children.items())
331
332
 
332
333
    @staticmethod
333
334
    def versionable_kind(kind):
334
 
        return kind in ('file', 'directory', 'symlink')
 
335
        return (kind in ('file', 'directory', 'symlink', 'tree-reference'))
335
336
 
336
337
    def check(self, checker, rev_id, inv, tree):
337
338
        """Check this inventory entry is intact.
347
348
        :param inv: Inventory from which the entry was loaded.
348
349
        :param tree: RevisionTree for this entry.
349
350
        """
350
 
        if self.parent_id != None:
 
351
        if self.parent_id is not None:
351
352
            if not inv.has_id(self.parent_id):
352
353
                raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
353
354
                        % (self.parent_id, rev_id))
382
383
            return 'added'
383
384
        elif new_entry is None:
384
385
            return 'removed'
 
386
        if old_entry.kind != new_entry.kind:
 
387
            return 'modified'
385
388
        text_modified, meta_modified = new_entry.detect_changes(old_entry)
386
389
        if text_modified or meta_modified:
387
390
            modified = True
403
406
        return 'unchanged'
404
407
 
405
408
    def __repr__(self):
406
 
        return ("%s(%r, %r, parent_id=%r)"
 
409
        return ("%s(%r, %r, parent_id=%r, revision=%r)"
407
410
                % (self.__class__.__name__,
408
411
                   self.file_id,
409
412
                   self.name,
410
 
                   self.parent_id))
 
413
                   self.parent_id,
 
414
                   self.revision))
411
415
 
412
416
    def snapshot(self, revision, path, previous_entries,
413
 
                 work_tree, weave_store, transaction):
 
417
                 work_tree, commit_builder):
414
418
        """Make a snapshot of this entry which may or may not have changed.
415
419
        
416
420
        This means that all its fields are populated, that it has its
417
421
        text stored in the text store or weave.
418
422
        """
419
 
        mutter('new parents of %s are %r', path, previous_entries)
 
423
        # mutter('new parents of %s are %r', path, previous_entries)
420
424
        self._read_tree_state(path, work_tree)
 
425
        # TODO: Where should we determine whether to reuse a
 
426
        # previous revision id or create a new revision? 20060606
421
427
        if len(previous_entries) == 1:
422
428
            # cannot be unchanged unless there is only one parent file rev.
423
429
            parent_ie = previous_entries.values()[0]
424
430
            if self._unchanged(parent_ie):
425
 
                mutter("found unchanged entry")
 
431
                # mutter("found unchanged entry")
426
432
                self.revision = parent_ie.revision
427
433
                return "unchanged"
428
434
        return self._snapshot_into_revision(revision, previous_entries, 
429
 
                                            work_tree, weave_store, transaction)
 
435
                                            work_tree, commit_builder)
430
436
 
431
437
    def _snapshot_into_revision(self, revision, previous_entries, work_tree,
432
 
                                weave_store, transaction):
 
438
                                commit_builder):
433
439
        """Record this revision unconditionally into a store.
434
440
 
435
441
        The entry's last-changed revision property (`revision`) is updated to 
439
445
 
440
446
        :returns: String description of the commit (e.g. "merged", "modified"), etc.
441
447
        """
442
 
        mutter('new revision {%s} for {%s}', revision, self.file_id)
 
448
        # mutter('new revision {%s} for {%s}', revision, self.file_id)
443
449
        self.revision = revision
444
 
        self._snapshot_text(previous_entries, work_tree, weave_store,
445
 
                            transaction)
 
450
        self._snapshot_text(previous_entries, work_tree, commit_builder)
446
451
 
447
 
    def _snapshot_text(self, file_parents, work_tree, weave_store, transaction): 
 
452
    def _snapshot_text(self, file_parents, work_tree, commit_builder): 
448
453
        """Record the 'text' of this entry, whatever form that takes.
449
454
        
450
455
        This default implementation simply adds an empty text.
451
456
        """
452
 
        mutter('storing file {%s} in revision {%s}',
453
 
               self.file_id, self.revision)
454
 
        self._add_text_to_weave([], file_parents.keys(), weave_store, transaction)
 
457
        raise NotImplementedError(self._snapshot_text)
455
458
 
456
459
    def __eq__(self, other):
457
460
        if not isinstance(other, InventoryEntry):
467
470
                and (self.kind == other.kind)
468
471
                and (self.revision == other.revision)
469
472
                and (self.executable == other.executable)
 
473
                and (self.reference_revision == other.reference_revision)
470
474
                )
471
475
 
472
476
    def __ne__(self, other):
478
482
    def _unchanged(self, previous_ie):
479
483
        """Has this entry changed relative to previous_ie.
480
484
 
481
 
        This method should be overriden in child classes.
 
485
        This method should be overridden in child classes.
482
486
        """
483
487
        compatible = True
484
488
        # different inv parent
506
510
 
507
511
class RootEntry(InventoryEntry):
508
512
 
 
513
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
514
                 'text_id', 'parent_id', 'children', 'executable',
 
515
                 'revision', 'symlink_target', 'reference_revision']
 
516
 
509
517
    def _check(self, checker, rev_id, tree):
510
518
        """See InventoryEntry._check"""
511
519
 
512
520
    def __init__(self, file_id):
513
521
        self.file_id = file_id
514
522
        self.children = {}
515
 
        self.kind = 'root_directory'
 
523
        self.kind = 'directory'
516
524
        self.parent_id = None
517
525
        self.name = u''
 
526
        self.revision = None
 
527
        symbol_versioning.warn('RootEntry is deprecated as of bzr 0.10.'
 
528
                               '  Please use InventoryDirectory instead.',
 
529
                               DeprecationWarning, stacklevel=2)
518
530
 
519
531
    def __eq__(self, other):
520
532
        if not isinstance(other, RootEntry):
527
539
class InventoryDirectory(InventoryEntry):
528
540
    """A directory in an inventory."""
529
541
 
 
542
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
543
                 'text_id', 'parent_id', 'children', 'executable',
 
544
                 'revision', 'symlink_target', 'reference_revision']
 
545
 
530
546
    def _check(self, checker, rev_id, tree):
531
547
        """See InventoryEntry._check"""
532
 
        if self.text_sha1 != None or self.text_size != None or self.text_id != None:
 
548
        if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
533
549
            raise BzrCheckError('directory {%s} has text in revision {%s}'
534
550
                                % (self.file_id, rev_id))
535
551
 
562
578
        """See InventoryEntry._put_on_disk."""
563
579
        os.mkdir(fullpath)
564
580
 
 
581
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
 
582
        """See InventoryEntry._snapshot_text."""
 
583
        commit_builder.modified_directory(self.file_id, file_parents)
 
584
 
565
585
 
566
586
class InventoryFile(InventoryEntry):
567
587
    """A file in an inventory."""
568
588
 
 
589
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
590
                 'text_id', 'parent_id', 'children', 'executable',
 
591
                 'revision', 'symlink_target', 'reference_revision']
 
592
 
569
593
    def _check(self, checker, tree_revision_id, tree):
570
594
        """See InventoryEntry._check"""
571
595
        t = (self.file_id, self.revision)
610
634
 
611
635
    def detect_changes(self, old_entry):
612
636
        """See InventoryEntry.detect_changes."""
613
 
        assert self.text_sha1 != None
614
 
        assert old_entry.text_sha1 != None
 
637
        assert self.text_sha1 is not None
 
638
        assert old_entry.text_sha1 is not None
615
639
        text_modified = (self.text_sha1 != old_entry.text_sha1)
616
640
        meta_modified = (self.executable != old_entry.executable)
617
641
        return text_modified, meta_modified
631
655
            else:
632
656
                text_diff(to_label, to_text,
633
657
                          from_label, from_text, output_to)
634
 
        except BinaryFile:
 
658
        except errors.BinaryFile:
635
659
            if reverse:
636
660
                label_pair = (to_label, from_label)
637
661
            else:
663
687
 
664
688
    def _put_on_disk(self, fullpath, tree):
665
689
        """See InventoryEntry._put_on_disk."""
666
 
        pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
 
690
        osutils.pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
667
691
        if tree.is_executable(self.file_id):
668
692
            os.chmod(fullpath, 0755)
669
693
 
670
694
    def _read_tree_state(self, path, work_tree):
671
695
        """See InventoryEntry._read_tree_state."""
672
696
        self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
 
697
        # FIXME: 20050930 probe for the text size when getting sha1
 
698
        # in _read_tree_state
673
699
        self.executable = work_tree.is_executable(self.file_id, path=path)
674
700
 
 
701
    def __repr__(self):
 
702
        return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s)"
 
703
                % (self.__class__.__name__,
 
704
                   self.file_id,
 
705
                   self.name,
 
706
                   self.parent_id,
 
707
                   self.text_sha1,
 
708
                   self.text_size))
 
709
 
675
710
    def _forget_tree_state(self):
676
711
        self.text_sha1 = None
677
 
        self.executable = None
678
712
 
679
 
    def _snapshot_text(self, file_parents, work_tree, versionedfile_store, transaction):
 
713
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
680
714
        """See InventoryEntry._snapshot_text."""
681
 
        mutter('storing text of file {%s} in revision {%s} into %r',
682
 
               self.file_id, self.revision, versionedfile_store)
683
 
        # special case to avoid diffing on renames or 
684
 
        # reparenting
685
 
        if (len(file_parents) == 1
686
 
            and self.text_sha1 == file_parents.values()[0].text_sha1
687
 
            and self.text_size == file_parents.values()[0].text_size):
688
 
            previous_ie = file_parents.values()[0]
689
 
            versionedfile = versionedfile_store.get_weave(self.file_id, transaction)
690
 
            versionedfile.clone_text(self.revision, previous_ie.revision, file_parents.keys())
691
 
        else:
692
 
            new_lines = work_tree.get_file(self.file_id).readlines()
693
 
            self._add_text_to_weave(new_lines, file_parents.keys(), versionedfile_store,
694
 
                                    transaction)
695
 
            self.text_sha1 = sha_strings(new_lines)
696
 
            self.text_size = sum(map(len, new_lines))
697
 
 
 
715
        def get_content_byte_lines():
 
716
            return work_tree.get_file(self.file_id).readlines()
 
717
        self.text_sha1, self.text_size = commit_builder.modified_file_text(
 
718
            self.file_id, file_parents, get_content_byte_lines, self.text_sha1, self.text_size)
698
719
 
699
720
    def _unchanged(self, previous_ie):
700
721
        """See InventoryEntry._unchanged."""
713
734
class InventoryLink(InventoryEntry):
714
735
    """A file in an inventory."""
715
736
 
716
 
    __slots__ = ['symlink_target']
 
737
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
738
                 'text_id', 'parent_id', 'children', 'executable',
 
739
                 'revision', 'symlink_target', 'reference_revision']
717
740
 
718
741
    def _check(self, checker, rev_id, tree):
719
742
        """See InventoryEntry._check"""
720
 
        if self.text_sha1 != None or self.text_size != None or self.text_id != None:
 
743
        if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
721
744
            raise BzrCheckError('symlink {%s} has text in revision {%s}'
722
745
                    % (self.file_id, rev_id))
723
 
        if self.symlink_target == None:
 
746
        if self.symlink_target is None:
724
747
            raise BzrCheckError('symlink {%s} has no target in revision {%s}'
725
748
                    % (self.file_id, rev_id))
726
749
 
794
817
            compatible = False
795
818
        return compatible
796
819
 
 
820
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
 
821
        """See InventoryEntry._snapshot_text."""
 
822
        commit_builder.modified_link(
 
823
            self.file_id, file_parents, self.symlink_target)
 
824
 
 
825
 
 
826
class TreeReference(InventoryEntry):
 
827
    
 
828
    kind = 'tree-reference'
 
829
    
 
830
    def __init__(self, file_id, name, parent_id, revision=None,
 
831
                 reference_revision=None):
 
832
        InventoryEntry.__init__(self, file_id, name, parent_id)
 
833
        self.revision = revision
 
834
        self.reference_revision = reference_revision
 
835
 
 
836
    def copy(self):
 
837
        return TreeReference(self.file_id, self.name, self.parent_id,
 
838
                             self.revision, self.reference_revision)
 
839
 
 
840
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
 
841
        commit_builder.modified_reference(self.file_id, file_parents)
 
842
 
 
843
    def _read_tree_state(self, path, work_tree):
 
844
        """Populate fields in the inventory entry from the given tree.
 
845
        """
 
846
        self.reference_revision = work_tree.get_reference_revision(
 
847
            self.file_id, path)
 
848
 
 
849
    def _forget_tree_state(self):
 
850
        self.reference_revision = None 
 
851
 
797
852
 
798
853
class Inventory(object):
799
854
    """Inventory of versioned files in a tree.
814
869
 
815
870
    >>> inv = Inventory()
816
871
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
817
 
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT')
 
872
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
818
873
    >>> inv['123-123'].name
819
874
    'hello.c'
820
875
 
828
883
    May also look up by name:
829
884
 
830
885
    >>> [x[0] for x in inv.iter_entries()]
831
 
    [u'hello.c']
 
886
    ['', u'hello.c']
832
887
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
833
888
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
834
 
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
 
889
    Traceback (most recent call last):
 
890
    BzrError: parent_id {TREE_ROOT} not in inventory
 
891
    >>> inv.add(InventoryFile('123-123', 'hello.c', 'TREE_ROOT-12345678-12345678'))
 
892
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None)
835
893
    """
836
894
    def __init__(self, root_id=ROOT_ID, revision_id=None):
837
895
        """Create or read an inventory.
843
901
        The inventory is created with a default root directory, with
844
902
        an id of None.
845
903
        """
846
 
        # We are letting Branch.create() create a unique inventory
847
 
        # root id. Rather than generating a random one here.
848
 
        #if root_id is None:
849
 
        #    root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
850
 
        self.root = RootEntry(root_id)
 
904
        if root_id is not None:
 
905
            assert root_id.__class__ == str
 
906
            self._set_root(InventoryDirectory(root_id, u'', None))
 
907
        else:
 
908
            self.root = None
 
909
            self._byid = {}
851
910
        self.revision_id = revision_id
 
911
 
 
912
    def _set_root(self, ie):
 
913
        self.root = ie
852
914
        self._byid = {self.root.file_id: self.root}
853
915
 
854
 
 
855
916
    def copy(self):
856
917
        # TODO: jam 20051218 Should copy also copy the revision_id?
857
 
        other = Inventory(self.root.file_id)
 
918
        entries = self.iter_entries()
 
919
        other = Inventory(entries.next()[1].file_id)
858
920
        # copy recursively so we know directories will be added before
859
921
        # their children.  There are more efficient ways than this...
860
 
        for path, entry in self.iter_entries():
861
 
            if entry == self.root:
862
 
                continue
 
922
        for path, entry in entries():
863
923
            other.add(entry.copy())
864
924
        return other
865
925
 
866
 
 
867
926
    def __iter__(self):
868
927
        return iter(self._byid)
869
928
 
870
 
 
871
929
    def __len__(self):
872
930
        """Returns number of entries."""
873
931
        return len(self._byid)
874
932
 
875
 
 
876
933
    def iter_entries(self, from_dir=None):
877
934
        """Return (path, entry) pairs, in order by name."""
878
 
        if from_dir == None:
879
 
            assert self.root
 
935
        if from_dir is None:
 
936
            if self.root is None:
 
937
                return
880
938
            from_dir = self.root
 
939
            yield '', self.root
881
940
        elif isinstance(from_dir, basestring):
882
941
            from_dir = self._byid[from_dir]
883
942
            
915
974
                # if we finished all children, pop it off the stack
916
975
                stack.pop()
917
976
 
 
977
    def iter_entries_by_dir(self, from_dir=None, specific_file_ids=None):
 
978
        """Iterate over the entries in a directory first order.
 
979
 
 
980
        This returns all entries for a directory before returning
 
981
        the entries for children of a directory. This is not
 
982
        lexicographically sorted order, and is a hybrid between
 
983
        depth-first and breadth-first.
 
984
 
 
985
        :return: This yields (path, entry) pairs
 
986
        """
 
987
        if specific_file_ids:
 
988
            safe = osutils.safe_file_id
 
989
            specific_file_ids = set(safe(fid) for fid in specific_file_ids)
 
990
        # TODO? Perhaps this should return the from_dir so that the root is
 
991
        # yielded? or maybe an option?
 
992
        if from_dir is None:
 
993
            if self.root is None:
 
994
                return
 
995
            # Optimize a common case
 
996
            if specific_file_ids is not None and len(specific_file_ids) == 1:
 
997
                file_id = list(specific_file_ids)[0]
 
998
                if file_id in self:
 
999
                    yield self.id2path(file_id), self[file_id]
 
1000
                return 
 
1001
            from_dir = self.root
 
1002
            if (specific_file_ids is None or 
 
1003
                self.root.file_id in specific_file_ids):
 
1004
                yield u'', self.root
 
1005
        elif isinstance(from_dir, basestring):
 
1006
            from_dir = self._byid[from_dir]
 
1007
 
 
1008
        if specific_file_ids is not None:
 
1009
            # TODO: jam 20070302 This could really be done as a loop rather
 
1010
            #       than a bunch of recursive calls.
 
1011
            parents = set()
 
1012
            byid = self._byid
 
1013
            def add_ancestors(file_id):
 
1014
                if file_id not in byid:
 
1015
                    return
 
1016
                parent_id = byid[file_id].parent_id
 
1017
                if parent_id is None:
 
1018
                    return
 
1019
                if parent_id not in parents:
 
1020
                    parents.add(parent_id)
 
1021
                    add_ancestors(parent_id)
 
1022
            for file_id in specific_file_ids:
 
1023
                add_ancestors(file_id)
 
1024
        else:
 
1025
            parents = None
 
1026
            
 
1027
        stack = [(u'', from_dir)]
 
1028
        while stack:
 
1029
            cur_relpath, cur_dir = stack.pop()
 
1030
 
 
1031
            child_dirs = []
 
1032
            for child_name, child_ie in sorted(cur_dir.children.iteritems()):
 
1033
 
 
1034
                child_relpath = cur_relpath + child_name
 
1035
 
 
1036
                if (specific_file_ids is None or 
 
1037
                    child_ie.file_id in specific_file_ids):
 
1038
                    yield child_relpath, child_ie
 
1039
 
 
1040
                if child_ie.kind == 'directory':
 
1041
                    if parents is None or child_ie.file_id in parents:
 
1042
                        child_dirs.append((child_relpath+'/', child_ie))
 
1043
            stack.extend(reversed(child_dirs))
 
1044
 
918
1045
    def entries(self):
919
1046
        """Return list of (path, ie) for all entries except the root.
920
1047
 
925
1052
            kids = dir_ie.children.items()
926
1053
            kids.sort()
927
1054
            for name, ie in kids:
928
 
                child_path = pathjoin(dir_path, name)
 
1055
                child_path = osutils.pathjoin(dir_path, name)
929
1056
                accum.append((child_path, ie))
930
1057
                if ie.kind == 'directory':
931
1058
                    descend(ie, child_path)
933
1060
        descend(self.root, u'')
934
1061
        return accum
935
1062
 
936
 
 
937
1063
    def directories(self):
938
1064
        """Return (path, entry) pairs for all directories, including the root.
939
1065
        """
945
1071
            kids.sort()
946
1072
 
947
1073
            for name, child_ie in kids:
948
 
                child_path = pathjoin(parent_path, name)
 
1074
                child_path = osutils.pathjoin(parent_path, name)
949
1075
                descend(child_ie, child_path)
950
1076
        descend(self.root, u'')
951
1077
        return accum
952
1078
        
953
 
 
954
 
 
955
1079
    def __contains__(self, file_id):
956
1080
        """True if this entry contains a file with given id.
957
1081
 
958
1082
        >>> inv = Inventory()
959
1083
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
960
 
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
 
1084
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
961
1085
        >>> '123' in inv
962
1086
        True
963
1087
        >>> '456' in inv
964
1088
        False
965
1089
        """
966
 
        return file_id in self._byid
967
 
 
 
1090
        file_id = osutils.safe_file_id(file_id)
 
1091
        return (file_id in self._byid)
968
1092
 
969
1093
    def __getitem__(self, file_id):
970
1094
        """Return the entry for given file_id.
971
1095
 
972
1096
        >>> inv = Inventory()
973
1097
        >>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
974
 
        InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT')
 
1098
        InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
975
1099
        >>> inv['123123'].name
976
1100
        'hello.c'
977
1101
        """
 
1102
        file_id = osutils.safe_file_id(file_id)
978
1103
        try:
979
1104
            return self._byid[file_id]
980
1105
        except KeyError:
981
 
            if file_id == None:
982
 
                raise BzrError("can't look up file_id None")
983
 
            else:
984
 
                raise BzrError("file_id {%s} not in inventory" % file_id)
985
 
 
 
1106
            # really we're passing an inventory, not a tree...
 
1107
            raise errors.NoSuchId(self, file_id)
986
1108
 
987
1109
    def get_file_kind(self, file_id):
 
1110
        file_id = osutils.safe_file_id(file_id)
988
1111
        return self._byid[file_id].kind
989
1112
 
990
1113
    def get_child(self, parent_id, filename):
 
1114
        parent_id = osutils.safe_file_id(parent_id)
991
1115
        return self[parent_id].children.get(filename)
992
1116
 
 
1117
    def _add_child(self, entry):
 
1118
        """Add an entry to the inventory, without adding it to its parent"""
 
1119
        if entry.file_id in self._byid:
 
1120
            raise BzrError("inventory already contains entry with id {%s}" %
 
1121
                           entry.file_id)
 
1122
        self._byid[entry.file_id] = entry
 
1123
        for child in getattr(entry, 'children', {}).itervalues():
 
1124
            self._add_child(child)
 
1125
        return entry
993
1126
 
994
1127
    def add(self, entry):
995
1128
        """Add entry to inventory.
1000
1133
        Returns the new entry object.
1001
1134
        """
1002
1135
        if entry.file_id in self._byid:
1003
 
            raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
1004
 
 
1005
 
        if entry.parent_id == ROOT_ID or entry.parent_id is None:
1006
 
            entry.parent_id = self.root.file_id
1007
 
 
1008
 
        try:
1009
 
            parent = self._byid[entry.parent_id]
1010
 
        except KeyError:
1011
 
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
1012
 
 
1013
 
        if parent.children.has_key(entry.name):
1014
 
            raise BzrError("%s is already versioned" %
1015
 
                    pathjoin(self.id2path(parent.file_id), entry.name))
1016
 
 
1017
 
        self._byid[entry.file_id] = entry
1018
 
        parent.children[entry.name] = entry
1019
 
        return entry
1020
 
 
 
1136
            raise errors.DuplicateFileId(entry.file_id,
 
1137
                                         self._byid[entry.file_id])
 
1138
 
 
1139
        if entry.parent_id is None:
 
1140
            assert self.root is None and len(self._byid) == 0
 
1141
            self.root = entry
 
1142
        else:
 
1143
            try:
 
1144
                parent = self._byid[entry.parent_id]
 
1145
            except KeyError:
 
1146
                raise BzrError("parent_id {%s} not in inventory" %
 
1147
                               entry.parent_id)
 
1148
 
 
1149
            if entry.name in parent.children:
 
1150
                raise BzrError("%s is already versioned" %
 
1151
                        osutils.pathjoin(self.id2path(parent.file_id),
 
1152
                        entry.name))
 
1153
            parent.children[entry.name] = entry
 
1154
        return self._add_child(entry)
1021
1155
 
1022
1156
    def add_path(self, relpath, kind, file_id=None, parent_id=None):
1023
1157
        """Add entry from a path.
1026
1160
 
1027
1161
        Returns the new entry object."""
1028
1162
        
1029
 
        parts = bzrlib.osutils.splitpath(relpath)
 
1163
        parts = osutils.splitpath(relpath)
1030
1164
 
1031
1165
        if len(parts) == 0:
1032
1166
            if file_id is None:
1033
 
                file_id = bzrlib.workingtree.gen_root_id()
1034
 
            self.root = RootEntry(file_id)
 
1167
                file_id = generate_ids.gen_root_id()
 
1168
            else:
 
1169
                file_id = osutils.safe_file_id(file_id)
 
1170
            self.root = InventoryDirectory(file_id, '', None)
1035
1171
            self._byid = {self.root.file_id: self.root}
1036
 
            return
 
1172
            return self.root
1037
1173
        else:
1038
1174
            parent_path = parts[:-1]
1039
1175
            parent_id = self.path2id(parent_path)
1040
 
            if parent_id == None:
1041
 
                raise NotVersionedError(path=parent_path)
 
1176
            if parent_id is None:
 
1177
                raise errors.NotVersionedError(path=parent_path)
1042
1178
        ie = make_entry(kind, parts[-1], parent_id, file_id)
1043
1179
        return self.add(ie)
1044
1180
 
1047
1183
 
1048
1184
        >>> inv = Inventory()
1049
1185
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1050
 
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
 
1186
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
1051
1187
        >>> '123' in inv
1052
1188
        True
1053
1189
        >>> del inv['123']
1054
1190
        >>> '123' in inv
1055
1191
        False
1056
1192
        """
 
1193
        file_id = osutils.safe_file_id(file_id)
1057
1194
        ie = self[file_id]
1058
1195
 
1059
1196
        assert ie.parent_id is None or \
1063
1200
        if ie.parent_id is not None:
1064
1201
            del self[ie.parent_id].children[ie.name]
1065
1202
 
1066
 
 
1067
1203
    def __eq__(self, other):
1068
1204
        """Compare two sets by comparing their contents.
1069
1205
 
1072
1208
        >>> i1 == i2
1073
1209
        True
1074
1210
        >>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
1075
 
        InventoryFile('123', 'foo', parent_id='TREE_ROOT')
 
1211
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
1076
1212
        >>> i1 == i2
1077
1213
        False
1078
1214
        >>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
1079
 
        InventoryFile('123', 'foo', parent_id='TREE_ROOT')
 
1215
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
1080
1216
        >>> i1 == i2
1081
1217
        True
1082
1218
        """
1083
1219
        if not isinstance(other, Inventory):
1084
1220
            return NotImplemented
1085
1221
 
1086
 
        if len(self._byid) != len(other._byid):
1087
 
            # shortcut: obviously not the same
1088
 
            return False
1089
 
 
1090
1222
        return self._byid == other._byid
1091
1223
 
1092
 
 
1093
1224
    def __ne__(self, other):
1094
1225
        return not self.__eq__(other)
1095
1226
 
1096
 
 
1097
1227
    def __hash__(self):
1098
1228
        raise ValueError('not hashable')
1099
1229
 
1100
1230
    def _iter_file_id_parents(self, file_id):
1101
1231
        """Yield the parents of file_id up to the root."""
1102
 
        while file_id != None:
 
1232
        file_id = osutils.safe_file_id(file_id)
 
1233
        while file_id is not None:
1103
1234
            try:
1104
1235
                ie = self._byid[file_id]
1105
1236
            except KeyError:
1106
 
                raise BzrError("file_id {%s} not found in inventory" % file_id)
 
1237
                raise errors.NoSuchId(tree=None, file_id=file_id)
1107
1238
            yield ie
1108
1239
            file_id = ie.parent_id
1109
1240
 
1115
1246
        is equal to the depth of the file in the tree, counting the
1116
1247
        root directory as depth 1.
1117
1248
        """
 
1249
        file_id = osutils.safe_file_id(file_id)
1118
1250
        p = []
1119
1251
        for parent in self._iter_file_id_parents(file_id):
1120
1252
            p.insert(0, parent.file_id)
1129
1261
        >>> print i.id2path('foo-id')
1130
1262
        src/foo.c
1131
1263
        """
 
1264
        file_id = osutils.safe_file_id(file_id)
1132
1265
        # get all names, skipping root
1133
1266
        return '/'.join(reversed(
1134
1267
            [parent.name for parent in 
1145
1278
 
1146
1279
        Returns None IFF the path is not found.
1147
1280
        """
1148
 
        if isinstance(name, types.StringTypes):
1149
 
            name = splitpath(name)
 
1281
        if isinstance(name, basestring):
 
1282
            name = osutils.splitpath(name)
1150
1283
 
1151
1284
        # mutter("lookup path %r" % name)
1152
1285
 
1153
1286
        parent = self.root
 
1287
        if parent is None:
 
1288
            return None
1154
1289
        for f in name:
1155
1290
            try:
1156
 
                cie = parent.children[f]
 
1291
                children = getattr(parent, 'children', None)
 
1292
                if children is None:
 
1293
                    return None
 
1294
                cie = children[f]
1157
1295
                assert cie.name == f
1158
1296
                assert cie.parent_id == parent.file_id
1159
1297
                parent = cie
1163
1301
 
1164
1302
        return parent.file_id
1165
1303
 
1166
 
 
1167
1304
    def has_filename(self, names):
1168
1305
        return bool(self.path2id(names))
1169
1306
 
1170
 
 
1171
1307
    def has_id(self, file_id):
1172
 
        return self._byid.has_key(file_id)
 
1308
        file_id = osutils.safe_file_id(file_id)
 
1309
        return (file_id in self._byid)
1173
1310
 
 
1311
    def remove_recursive_id(self, file_id):
 
1312
        """Remove file_id, and children, from the inventory.
 
1313
        
 
1314
        :param file_id: A file_id to remove.
 
1315
        """
 
1316
        file_id = osutils.safe_file_id(file_id)
 
1317
        to_find_delete = [self._byid[file_id]]
 
1318
        to_delete = []
 
1319
        while to_find_delete:
 
1320
            ie = to_find_delete.pop()
 
1321
            to_delete.append(ie.file_id)
 
1322
            if ie.kind == 'directory':
 
1323
                to_find_delete.extend(ie.children.values())
 
1324
        for file_id in reversed(to_delete):
 
1325
            ie = self[file_id]
 
1326
            del self._byid[file_id]
 
1327
        if ie.parent_id is not None:
 
1328
            del self[ie.parent_id].children[ie.name]
1174
1329
 
1175
1330
    def rename(self, file_id, new_parent_id, new_name):
1176
1331
        """Move a file within the inventory.
1177
1332
 
1178
1333
        This can change either the name, or the parent, or both.
1179
1334
 
1180
 
        This does not move the working file."""
 
1335
        This does not move the working file.
 
1336
        """
 
1337
        file_id = osutils.safe_file_id(file_id)
1181
1338
        if not is_valid_name(new_name):
1182
1339
            raise BzrError("not an acceptable filename: %r" % new_name)
1183
1340
 
1201
1358
        file_ie.name = new_name
1202
1359
        file_ie.parent_id = new_parent_id
1203
1360
 
 
1361
    def is_root(self, file_id):
 
1362
        file_id = osutils.safe_file_id(file_id)
 
1363
        return self.root is not None and file_id == self.root.file_id
 
1364
 
 
1365
 
 
1366
entry_factory = {
 
1367
    'directory': InventoryDirectory,
 
1368
    'file': InventoryFile,
 
1369
    'symlink': InventoryLink,
 
1370
    'tree-reference': TreeReference
 
1371
}
1204
1372
 
1205
1373
def make_entry(kind, name, parent_id, file_id=None):
1206
1374
    """Create an inventory entry.
1211
1379
    :param file_id: the file_id to use. if None, one will be created.
1212
1380
    """
1213
1381
    if file_id is None:
1214
 
        file_id = bzrlib.workingtree.gen_file_id(name)
1215
 
    if kind == 'directory':
1216
 
        return InventoryDirectory(file_id, name, parent_id)
1217
 
    elif kind == 'file':
1218
 
        return InventoryFile(file_id, name, parent_id)
1219
 
    elif kind == 'symlink':
1220
 
        return InventoryLink(file_id, name, parent_id)
 
1382
        file_id = generate_ids.gen_file_id(name)
1221
1383
    else:
 
1384
        file_id = osutils.safe_file_id(file_id)
 
1385
 
 
1386
    #------- This has been copied to bzrlib.dirstate.DirState.add, please
 
1387
    # keep them synchronised.
 
1388
    # we dont import normalized_filename directly because we want to be
 
1389
    # able to change the implementation at runtime for tests.
 
1390
    norm_name, can_access = osutils.normalized_filename(name)
 
1391
    if norm_name != name:
 
1392
        if can_access:
 
1393
            name = norm_name
 
1394
        else:
 
1395
            # TODO: jam 20060701 This would probably be more useful
 
1396
            #       if the error was raised with the full path
 
1397
            raise errors.InvalidNormalization(name)
 
1398
 
 
1399
    try:
 
1400
        factory = entry_factory[kind]
 
1401
    except KeyError:
1222
1402
        raise BzrError("unknown kind %r" % kind)
1223
 
 
 
1403
    return factory(file_id, name, parent_id)
1224
1404
 
1225
1405
 
1226
1406
_NAME_RE = None
1227
1407
 
1228
1408
def is_valid_name(name):
1229
1409
    global _NAME_RE
1230
 
    if _NAME_RE == None:
 
1410
    if _NAME_RE is None:
1231
1411
        _NAME_RE = re.compile(r'^[^/\\]+$')
1232
1412
        
1233
1413
    return bool(_NAME_RE.match(name))