~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: John Arbash Meinel
  • Date: 2006-08-23 22:16:27 UTC
  • mto: This revision was merged to the branch mainline in revision 1955.
  • Revision ID: john@arbash-meinel.com-20060823221627-fc64105bb12ae770
Ghozzy: Fix Bzr's support of Active FTP (aftp://)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007 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
27
27
# created, but it's not for now.
28
28
ROOT_ID = "TREE_ROOT"
29
29
 
30
 
import os
 
30
 
 
31
import collections
 
32
import os.path
31
33
import re
32
34
import sys
33
 
 
34
 
from bzrlib.lazy_import import lazy_import
35
 
lazy_import(globals(), """
36
 
import collections
37
35
import tarfile
 
36
import types
 
37
from warnings import warn
38
38
 
39
39
import bzrlib
40
 
from bzrlib import (
41
 
    errors,
42
 
    generate_ids,
43
 
    osutils,
44
 
    symbol_versioning,
45
 
    workingtree,
46
 
    )
47
 
""")
48
 
 
49
 
from bzrlib.errors import (
50
 
    BzrCheckError,
51
 
    BzrError,
52
 
    )
53
 
from bzrlib.symbol_versioning import deprecated_method
 
40
from bzrlib import errors, osutils
 
41
from bzrlib.osutils import (pumpfile, quotefn, splitpath, joinpath,
 
42
                            pathjoin, sha_strings)
 
43
from bzrlib.errors import (NotVersionedError, InvalidEntryName,
 
44
                           BzrError, BzrCheckError, BinaryFile)
54
45
from bzrlib.trace import mutter
55
46
 
56
47
 
91
82
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
92
83
    >>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
93
84
    InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
94
 
    >>> shouldbe = {0: '', 1: 'src', 2: 'src/hello.c'}
 
85
    >>> shouldbe = {0: '', 1: 'src', 2: pathjoin('src','hello.c')}
95
86
    >>> for ix, j in enumerate(i.iter_entries()):
96
87
    ...   print (j[0] == shouldbe[ix], j[1])
97
88
    ... 
98
 
    (True, InventoryDirectory('TREE_ROOT', u'', parent_id=None, revision=None))
 
89
    (True, InventoryDirectory('TREE_ROOT', '', parent_id=None, revision=None))
99
90
    (True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
100
91
    (True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
 
92
    >>> i.add(InventoryFile('2323', 'bye.c', '123'))
 
93
    Traceback (most recent call last):
 
94
    ...
 
95
    BzrError: inventory already contains entry with id {2323}
101
96
    >>> i.add(InventoryFile('2324', 'bye.c', '123'))
102
97
    InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
103
98
    >>> i.add(InventoryDirectory('2325', 'wibble', '123'))
112
107
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
113
108
    >>> for path, entry in i.iter_entries():
114
109
    ...     print path
 
110
    ...     assert i.path2id(path)
115
111
    ... 
116
112
    <BLANKLINE>
117
113
    src
141
137
        """
142
138
        return False, False
143
139
 
 
140
    def diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
 
141
             output_to, reverse=False):
 
142
        """Perform a diff from this to to_entry.
 
143
 
 
144
        text_diff will be used for textual difference calculation.
 
145
        This is a template method, override _diff in child classes.
 
146
        """
 
147
        self._read_tree_state(tree.id2path(self.file_id), tree)
 
148
        if to_entry:
 
149
            # cannot diff from one kind to another - you must do a removal
 
150
            # and an addif they do not match.
 
151
            assert self.kind == to_entry.kind
 
152
            to_entry._read_tree_state(to_tree.id2path(to_entry.file_id),
 
153
                                      to_tree)
 
154
        self._diff(text_diff, from_label, tree, to_label, to_entry, to_tree,
 
155
                   output_to, reverse)
 
156
 
144
157
    def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
145
158
             output_to, reverse=False):
146
159
        """Perform a diff between two entries of the same kind."""
147
 
    
148
 
    def parent_candidates(self, previous_inventories):
149
 
        """Find possible per-file graph parents.
150
 
 
151
 
        This is currently defined by:
152
 
         - Select the last changed revision in the parent inventory.
153
 
         - Do deal with a short lived bug in bzr 0.8's development two entries
154
 
           that have the same last changed but different 'x' bit settings are
155
 
           changed in-place.
 
160
 
 
161
    def find_previous_heads(self, previous_inventories,
 
162
                            versioned_file_store,
 
163
                            transaction,
 
164
                            entry_vf=None):
 
165
        """Return the revisions and entries that directly precede this.
 
166
 
 
167
        Returned as a map from revision to inventory entry.
 
168
 
 
169
        This is a map containing the file revisions in all parents
 
170
        for which the file exists, and its revision is not a parent of
 
171
        any other. If the file is new, the set will be empty.
 
172
 
 
173
        :param versioned_file_store: A store where ancestry data on this
 
174
                                     file id can be queried.
 
175
        :param transaction: The transaction that queries to the versioned 
 
176
                            file store should be completed under.
 
177
        :param entry_vf: The entry versioned file, if its already available.
156
178
        """
 
179
        def get_ancestors(weave, entry):
 
180
            return set(weave.get_ancestry(entry.revision))
157
181
        # revision:ie mapping for each ie found in previous_inventories.
158
182
        candidates = {}
 
183
        # revision:ie mapping with one revision for each head.
 
184
        heads = {}
 
185
        # revision: ancestor list for each head
 
186
        head_ancestors = {}
159
187
        # identify candidate head revision ids.
160
188
        for inv in previous_inventories:
161
189
            if self.file_id in inv:
162
190
                ie = inv[self.file_id]
 
191
                assert ie.file_id == self.file_id
163
192
                if ie.revision in candidates:
164
193
                    # same revision value in two different inventories:
165
194
                    # correct possible inconsistencies:
171
200
                            ie.executable = False
172
201
                    except AttributeError:
173
202
                        pass
 
203
                    # must now be the same.
 
204
                    assert candidates[ie.revision] == ie
174
205
                else:
175
206
                    # add this revision as a candidate.
176
207
                    candidates[ie.revision] = ie
177
 
        return candidates
 
208
 
 
209
        # common case optimisation
 
210
        if len(candidates) == 1:
 
211
            # if there is only one candidate revision found
 
212
            # then we can opening the versioned file to access ancestry:
 
213
            # there cannot be any ancestors to eliminate when there is 
 
214
            # only one revision available.
 
215
            heads[ie.revision] = ie
 
216
            return heads
 
217
 
 
218
        # eliminate ancestors amongst the available candidates:
 
219
        # heads are those that are not an ancestor of any other candidate
 
220
        # - this provides convergence at a per-file level.
 
221
        for ie in candidates.values():
 
222
            # may be an ancestor of a known head:
 
223
            already_present = 0 != len(
 
224
                [head for head in heads 
 
225
                 if ie.revision in head_ancestors[head]])
 
226
            if already_present:
 
227
                # an ancestor of an analyzed candidate.
 
228
                continue
 
229
            # not an ancestor of a known head:
 
230
            # load the versioned file for this file id if needed
 
231
            if entry_vf is None:
 
232
                entry_vf = versioned_file_store.get_weave_or_empty(
 
233
                    self.file_id, transaction)
 
234
            ancestors = get_ancestors(entry_vf, ie)
 
235
            # may knock something else out:
 
236
            check_heads = list(heads.keys())
 
237
            for head in check_heads:
 
238
                if head in ancestors:
 
239
                    # this previously discovered 'head' is not
 
240
                    # really a head - its an ancestor of the newly 
 
241
                    # found head,
 
242
                    heads.pop(head)
 
243
            head_ancestors[ie.revision] = ancestors
 
244
            heads[ie.revision] = ie
 
245
        return heads
178
246
 
179
247
    def get_tar_item(self, root, dp, now, tree):
180
248
        """Get a tarfile item and a file stream for its content."""
181
 
        item = tarfile.TarInfo(osutils.pathjoin(root, dp).encode('utf8'))
 
249
        item = tarfile.TarInfo(pathjoin(root, dp))
182
250
        # TODO: would be cool to actually set it to the timestamp of the
183
251
        # revision it was last changed
184
252
        item.mtime = now
211
279
        Traceback (most recent call last):
212
280
        InvalidEntryName: Invalid entry name: src/hello.c
213
281
        """
 
282
        assert isinstance(name, basestring), name
214
283
        if '/' in name or '\\' in name:
215
 
            raise errors.InvalidEntryName(name=name)
 
284
            raise InvalidEntryName(name=name)
216
285
        self.executable = False
217
286
        self.revision = None
218
287
        self.text_sha1 = None
222
291
        self.text_id = text_id
223
292
        self.parent_id = parent_id
224
293
        self.symlink_target = None
225
 
        self.reference_revision = None
226
294
 
227
295
    def kind_character(self):
228
296
        """Return a short kind indicator useful for appending to names."""
243
311
        
244
312
        This is a template method - implement _put_on_disk in subclasses.
245
313
        """
246
 
        fullpath = osutils.pathjoin(dest, dp)
 
314
        fullpath = pathjoin(dest, dp)
247
315
        self._put_on_disk(fullpath, tree)
248
316
        # mutter("  export {%s} kind %s to %s", self.file_id,
249
317
        #         self.kind, fullpath)
257
325
 
258
326
    @staticmethod
259
327
    def versionable_kind(kind):
260
 
        return (kind in ('file', 'directory', 'symlink', 'tree-reference'))
 
328
        return kind in ('file', 'directory', 'symlink')
261
329
 
262
330
    def check(self, checker, rev_id, inv, tree):
263
331
        """Check this inventory entry is intact.
308
376
            return 'added'
309
377
        elif new_entry is None:
310
378
            return 'removed'
311
 
        if old_entry.kind != new_entry.kind:
312
 
            return 'modified'
313
379
        text_modified, meta_modified = new_entry.detect_changes(old_entry)
314
380
        if text_modified or meta_modified:
315
381
            modified = True
338
404
                   self.parent_id,
339
405
                   self.revision))
340
406
 
 
407
    def snapshot(self, revision, path, previous_entries,
 
408
                 work_tree, commit_builder):
 
409
        """Make a snapshot of this entry which may or may not have changed.
 
410
        
 
411
        This means that all its fields are populated, that it has its
 
412
        text stored in the text store or weave.
 
413
        """
 
414
        # mutter('new parents of %s are %r', path, previous_entries)
 
415
        self._read_tree_state(path, work_tree)
 
416
        # TODO: Where should we determine whether to reuse a
 
417
        # previous revision id or create a new revision? 20060606
 
418
        if len(previous_entries) == 1:
 
419
            # cannot be unchanged unless there is only one parent file rev.
 
420
            parent_ie = previous_entries.values()[0]
 
421
            if self._unchanged(parent_ie):
 
422
                # mutter("found unchanged entry")
 
423
                self.revision = parent_ie.revision
 
424
                return "unchanged"
 
425
        return self._snapshot_into_revision(revision, previous_entries, 
 
426
                                            work_tree, commit_builder)
 
427
 
 
428
    def _snapshot_into_revision(self, revision, previous_entries, work_tree,
 
429
                                commit_builder):
 
430
        """Record this revision unconditionally into a store.
 
431
 
 
432
        The entry's last-changed revision property (`revision`) is updated to 
 
433
        that of the new revision.
 
434
        
 
435
        :param revision: id of the new revision that is being recorded.
 
436
 
 
437
        :returns: String description of the commit (e.g. "merged", "modified"), etc.
 
438
        """
 
439
        # mutter('new revision {%s} for {%s}', revision, self.file_id)
 
440
        self.revision = revision
 
441
        self._snapshot_text(previous_entries, work_tree, commit_builder)
 
442
 
 
443
    def _snapshot_text(self, file_parents, work_tree, commit_builder): 
 
444
        """Record the 'text' of this entry, whatever form that takes.
 
445
        
 
446
        This default implementation simply adds an empty text.
 
447
        """
 
448
        raise NotImplementedError(self._snapshot_text)
 
449
 
341
450
    def __eq__(self, other):
342
451
        if not isinstance(other, InventoryEntry):
343
452
            return NotImplemented
352
461
                and (self.kind == other.kind)
353
462
                and (self.revision == other.revision)
354
463
                and (self.executable == other.executable)
355
 
                and (self.reference_revision == other.reference_revision)
356
464
                )
357
465
 
358
466
    def __ne__(self, other):
373
481
        # renamed
374
482
        elif previous_ie.name != self.name:
375
483
            compatible = False
376
 
        elif previous_ie.kind != self.kind:
377
 
            compatible = False
378
484
        return compatible
379
485
 
380
486
    def _read_tree_state(self, path, work_tree):
395
501
class RootEntry(InventoryEntry):
396
502
 
397
503
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
398
 
                 'text_id', 'parent_id', 'children', 'executable',
399
 
                 'revision', 'symlink_target', 'reference_revision']
 
504
                 'text_id', 'parent_id', 'children', 'executable', 
 
505
                 'revision', 'symlink_target']
400
506
 
401
507
    def _check(self, checker, rev_id, tree):
402
508
        """See InventoryEntry._check"""
408
514
        self.parent_id = None
409
515
        self.name = u''
410
516
        self.revision = None
411
 
        symbol_versioning.warn('RootEntry is deprecated as of bzr 0.10.'
412
 
                               '  Please use InventoryDirectory instead.',
413
 
                               DeprecationWarning, stacklevel=2)
 
517
        warn('RootEntry is deprecated as of bzr 0.10.  Please use '
 
518
             'InventoryDirectory instead.',
 
519
            DeprecationWarning, stacklevel=2)
414
520
 
415
521
    def __eq__(self, other):
416
522
        if not isinstance(other, RootEntry):
424
530
    """A directory in an inventory."""
425
531
 
426
532
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
427
 
                 'text_id', 'parent_id', 'children', 'executable',
428
 
                 'revision', 'symlink_target', 'reference_revision']
 
533
                 'text_id', 'parent_id', 'children', 'executable', 
 
534
                 'revision', 'symlink_target']
429
535
 
430
536
    def _check(self, checker, rev_id, tree):
431
537
        """See InventoryEntry._check"""
462
568
        """See InventoryEntry._put_on_disk."""
463
569
        os.mkdir(fullpath)
464
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
 
465
575
 
466
576
class InventoryFile(InventoryEntry):
467
577
    """A file in an inventory."""
468
578
 
469
579
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
470
 
                 'text_id', 'parent_id', 'children', 'executable',
471
 
                 'revision', 'symlink_target', 'reference_revision']
 
580
                 'text_id', 'parent_id', 'children', 'executable', 
 
581
                 'revision', 'symlink_target']
472
582
 
473
583
    def _check(self, checker, tree_revision_id, tree):
474
584
        """See InventoryEntry._check"""
476
586
        if t in checker.checked_texts:
477
587
            prev_sha = checker.checked_texts[t]
478
588
            if prev_sha != self.text_sha1:
479
 
                raise BzrCheckError(
480
 
                    'mismatched sha1 on {%s} in {%s} (%s != %s) %r' %
481
 
                    (self.file_id, tree_revision_id, prev_sha, self.text_sha1,
482
 
                     t))
 
589
                raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
 
590
                                    (self.file_id, tree_revision_id))
483
591
            else:
484
592
                checker.repeated_text_cnt += 1
485
593
                return
486
594
 
487
595
        if self.file_id not in checker.checked_weaves:
488
596
            mutter('check weave {%s}', self.file_id)
489
 
            w = tree._get_weave(self.file_id)
 
597
            w = tree.get_weave(self.file_id)
490
598
            # Not passing a progress bar, because it creates a new
491
599
            # progress, which overwrites the current progress,
492
600
            # and doesn't look nice
493
601
            w.check()
494
602
            checker.checked_weaves[self.file_id] = True
495
603
        else:
496
 
            w = tree._get_weave(self.file_id)
 
604
            w = tree.get_weave(self.file_id)
497
605
 
498
606
        mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
499
607
        checker.checked_text_cnt += 1
500
608
        # We can't check the length, because Weave doesn't store that
501
609
        # information, and the whole point of looking at the weave's
502
610
        # sha1sum is that we don't have to extract the text.
503
 
        if self.text_sha1 != w.get_sha1s([self.revision])[0]:
 
611
        if self.text_sha1 != w.get_sha1(self.revision):
504
612
            raise BzrCheckError('text {%s} version {%s} wrong sha1' 
505
613
                                % (self.file_id, self.revision))
506
614
        checker.checked_texts[t] = self.text_sha1
516
624
 
517
625
    def detect_changes(self, old_entry):
518
626
        """See InventoryEntry.detect_changes."""
 
627
        assert self.text_sha1 is not None
 
628
        assert old_entry.text_sha1 is not None
519
629
        text_modified = (self.text_sha1 != old_entry.text_sha1)
520
630
        meta_modified = (self.executable != old_entry.executable)
521
631
        return text_modified, meta_modified
523
633
    def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
524
634
             output_to, reverse=False):
525
635
        """See InventoryEntry._diff."""
526
 
        from bzrlib.diff import DiffText
527
 
        from_file_id = self.file_id
528
 
        if to_entry:
529
 
            to_file_id = to_entry.file_id
530
 
        else:
531
 
            to_file_id = None
532
 
        if reverse:
533
 
            to_file_id, from_file_id = from_file_id, to_file_id
534
 
            tree, to_tree = to_tree, tree
535
 
            from_label, to_label = to_label, from_label
536
 
        differ = DiffText(tree, to_tree, output_to, 'utf-8', '', '',
537
 
                          text_diff)
538
 
        return differ.diff_text(from_file_id, to_file_id, from_label, to_label)
 
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
539
654
 
540
655
    def has_text(self):
541
656
        """See InventoryEntry.has_text."""
562
677
 
563
678
    def _put_on_disk(self, fullpath, tree):
564
679
        """See InventoryEntry._put_on_disk."""
565
 
        osutils.pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
 
680
        pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
566
681
        if tree.is_executable(self.file_id):
567
682
            os.chmod(fullpath, 0755)
568
683
 
585
700
    def _forget_tree_state(self):
586
701
        self.text_sha1 = None
587
702
 
 
703
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
 
704
        """See InventoryEntry._snapshot_text."""
 
705
        def get_content_byte_lines():
 
706
            return work_tree.get_file(self.file_id).readlines()
 
707
        self.text_sha1, self.text_size = commit_builder.modified_file_text(
 
708
            self.file_id, file_parents, get_content_byte_lines, self.text_sha1, self.text_size)
 
709
 
588
710
    def _unchanged(self, previous_ie):
589
711
        """See InventoryEntry._unchanged."""
590
712
        compatible = super(InventoryFile, self)._unchanged(previous_ie)
603
725
    """A file in an inventory."""
604
726
 
605
727
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
606
 
                 'text_id', 'parent_id', 'children', 'executable',
607
 
                 'revision', 'symlink_target', 'reference_revision']
 
728
                 'text_id', 'parent_id', 'children', 'executable', 
 
729
                 'revision', 'symlink_target']
608
730
 
609
731
    def _check(self, checker, rev_id, tree):
610
732
        """See InventoryEntry._check"""
633
755
    def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
634
756
             output_to, reverse=False):
635
757
        """See InventoryEntry._diff."""
636
 
        from bzrlib.diff import DiffSymlink
637
 
        old_target = self.symlink_target
 
758
        from_text = self.symlink_target
638
759
        if to_entry is not None:
639
 
            new_target = to_entry.symlink_target
640
 
        else:
641
 
            new_target = None
642
 
        if not reverse:
643
 
            old_tree = tree
644
 
            new_tree = to_tree
645
 
        else:
646
 
            old_tree = to_tree
647
 
            new_tree = tree
648
 
            new_target, old_target = old_target, new_target
649
 
        differ = DiffSymlink(old_tree, new_tree, output_to)
650
 
        return differ.diff_symlink(old_target, new_target)
 
760
            to_text = to_entry.symlink_target
 
761
            if reverse:
 
762
                temp = from_text
 
763
                from_text = to_text
 
764
                to_text = temp
 
765
            print >>output_to, '=== target changed %r => %r' % (from_text, to_text)
 
766
        else:
 
767
            if not reverse:
 
768
                print >>output_to, '=== target was %r' % self.symlink_target
 
769
            else:
 
770
                print >>output_to, '=== target is %r' % self.symlink_target
651
771
 
652
772
    def __init__(self, file_id, name, parent_id):
653
773
        super(InventoryLink, self).__init__(file_id, name, parent_id)
687
807
            compatible = False
688
808
        return compatible
689
809
 
690
 
 
691
 
class TreeReference(InventoryEntry):
692
 
    
693
 
    kind = 'tree-reference'
694
 
    
695
 
    def __init__(self, file_id, name, parent_id, revision=None,
696
 
                 reference_revision=None):
697
 
        InventoryEntry.__init__(self, file_id, name, parent_id)
698
 
        self.revision = revision
699
 
        self.reference_revision = reference_revision
700
 
 
701
 
    def copy(self):
702
 
        return TreeReference(self.file_id, self.name, self.parent_id,
703
 
                             self.revision, self.reference_revision)
704
 
 
705
 
    def _read_tree_state(self, path, work_tree):
706
 
        """Populate fields in the inventory entry from the given tree.
707
 
        """
708
 
        self.reference_revision = work_tree.get_reference_revision(
709
 
            self.file_id, path)
710
 
 
711
 
    def _forget_tree_state(self):
712
 
        self.reference_revision = None 
713
 
 
714
 
    def _unchanged(self, previous_ie):
715
 
        """See InventoryEntry._unchanged."""
716
 
        compatible = super(TreeReference, self)._unchanged(previous_ie)
717
 
        if self.reference_revision != previous_ie.reference_revision:
718
 
            compatible = False
719
 
        return compatible
 
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)
720
814
 
721
815
 
722
816
class Inventory(object):
755
849
    ['', u'hello.c']
756
850
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
757
851
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
758
 
    Traceback (most recent call last):
759
 
    BzrError: parent_id {TREE_ROOT} not in inventory
760
 
    >>> inv.add(InventoryFile('123-123', 'hello.c', 'TREE_ROOT-12345678-12345678'))
761
852
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None)
762
853
    """
763
854
    def __init__(self, root_id=ROOT_ID, revision_id=None):
770
861
        The inventory is created with a default root directory, with
771
862
        an id of None.
772
863
        """
 
864
        # We are letting Branch.create() create a unique inventory
 
865
        # root id. Rather than generating a random one here.
 
866
        #if root_id is None:
 
867
        #    root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
773
868
        if root_id is not None:
774
 
            self._set_root(InventoryDirectory(root_id, u'', None))
 
869
            self._set_root(InventoryDirectory(root_id, '', None))
775
870
        else:
776
871
            self.root = None
777
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
778
875
        self.revision_id = revision_id
779
876
 
780
 
    def __repr__(self):
781
 
        return "<Inventory object at %x, contents=%r>" % (id(self), self._byid)
782
 
 
783
 
    def apply_delta(self, delta):
784
 
        """Apply a delta to this inventory.
785
 
 
786
 
        :param delta: A list of changes to apply. After all the changes are
787
 
            applied the final inventory must be internally consistent, but it
788
 
            is ok to supply changes which, if only half-applied would have an
789
 
            invalid result - such as supplying two changes which rename two
790
 
            files, 'A' and 'B' with each other : [('A', 'B', 'A-id', a_entry),
791
 
            ('B', 'A', 'B-id', b_entry)].
792
 
 
793
 
            Each change is a tuple, of the form (old_path, new_path, file_id,
794
 
            new_entry).
795
 
            
796
 
            When new_path is None, the change indicates the removal of an entry
797
 
            from the inventory and new_entry will be ignored (using None is
798
 
            appropriate). If new_path is not None, then new_entry must be an
799
 
            InventoryEntry instance, which will be incorporated into the
800
 
            inventory (and replace any existing entry with the same file id).
801
 
            
802
 
            When old_path is None, the change indicates the addition of
803
 
            a new entry to the inventory.
804
 
            
805
 
            When neither new_path nor old_path are None, the change is a
806
 
            modification to an entry, such as a rename, reparent, kind change
807
 
            etc. 
808
 
 
809
 
            The children attribute of new_entry is ignored. This is because
810
 
            this method preserves children automatically across alterations to
811
 
            the parent of the children, and cases where the parent id of a
812
 
            child is changing require the child to be passed in as a separate
813
 
            change regardless. E.g. in the recursive deletion of a directory -
814
 
            the directory's children must be included in the delta, or the
815
 
            final inventory will be invalid.
816
 
        """
817
 
        children = {}
818
 
        # Remove all affected items which were in the original inventory,
819
 
        # starting with the longest paths, thus ensuring parents are examined
820
 
        # after their children, which means that everything we examine has no
821
 
        # modified children remaining by the time we examine it.
822
 
        for old_path, file_id in sorted(((op, f) for op, np, f, e in delta
823
 
                                        if op is not None), reverse=True):
824
 
            if file_id not in self:
825
 
                # adds come later
826
 
                continue
827
 
            # Preserve unaltered children of file_id for later reinsertion.
828
 
            children[file_id] = getattr(self[file_id], 'children', {})
829
 
            # Remove file_id and the unaltered children. If file_id is not
830
 
            # being deleted it will be reinserted back later.
831
 
            self.remove_recursive_id(file_id)
832
 
        # Insert all affected which should be in the new inventory, reattaching
833
 
        # their children if they had any. This is done from shortest path to
834
 
        # longest, ensuring that items which were modified and whose parents in
835
 
        # the resulting inventory were also modified, are inserted after their
836
 
        # parents.
837
 
        for new_path, new_entry in sorted((np, e) for op, np, f, e in
838
 
                                          delta if np is not None):
839
 
            if new_entry.kind == 'directory':
840
 
                new_entry.children = children.get(new_entry.file_id, {})
841
 
            self.add(new_entry)
842
 
 
843
877
    def _set_root(self, ie):
844
878
        self.root = ie
845
879
        self._byid = {self.root.file_id: self.root}
847
881
    def copy(self):
848
882
        # TODO: jam 20051218 Should copy also copy the revision_id?
849
883
        entries = self.iter_entries()
850
 
        if self.root is None:
851
 
            return Inventory(root_id=None)
852
884
        other = Inventory(entries.next()[1].file_id)
853
885
        # copy recursively so we know directories will be added before
854
886
        # their children.  There are more efficient ways than this...
855
 
        for path, entry in entries:
 
887
        for path, entry in entries():
856
888
            other.add(entry.copy())
857
889
        return other
858
890
 
866
898
    def iter_entries(self, from_dir=None):
867
899
        """Return (path, entry) pairs, in order by name."""
868
900
        if from_dir is None:
869
 
            if self.root is None:
870
 
                return
 
901
            assert self.root
871
902
            from_dir = self.root
872
903
            yield '', self.root
873
904
        elif isinstance(from_dir, basestring):
907
938
                # if we finished all children, pop it off the stack
908
939
                stack.pop()
909
940
 
910
 
    def iter_entries_by_dir(self, from_dir=None, specific_file_ids=None,
911
 
        yield_parents=False):
 
941
    def iter_entries_by_dir(self, from_dir=None):
912
942
        """Iterate over the entries in a directory first order.
913
943
 
914
944
        This returns all entries for a directory before returning
916
946
        lexicographically sorted order, and is a hybrid between
917
947
        depth-first and breadth-first.
918
948
 
919
 
        :param yield_parents: If True, yield the parents from the root leading
920
 
            down to specific_file_ids that have been requested. This has no
921
 
            impact if specific_file_ids is None.
922
949
        :return: This yields (path, entry) pairs
923
950
        """
924
 
        if specific_file_ids and not isinstance(specific_file_ids, set):
925
 
            specific_file_ids = set(specific_file_ids)
926
951
        # TODO? Perhaps this should return the from_dir so that the root is
927
952
        # yielded? or maybe an option?
928
953
        if from_dir is None:
929
 
            if self.root is None:
930
 
                return
931
 
            # Optimize a common case
932
 
            if (not yield_parents and specific_file_ids is not None and
933
 
                len(specific_file_ids) == 1):
934
 
                file_id = list(specific_file_ids)[0]
935
 
                if file_id in self:
936
 
                    yield self.id2path(file_id), self[file_id]
937
 
                return 
 
954
            assert self.root
938
955
            from_dir = self.root
939
 
            if (specific_file_ids is None or yield_parents or
940
 
                self.root.file_id in specific_file_ids):
941
 
                yield u'', self.root
 
956
            yield '', self.root
942
957
        elif isinstance(from_dir, basestring):
943
958
            from_dir = self._byid[from_dir]
944
 
 
945
 
        if specific_file_ids is not None:
946
 
            # TODO: jam 20070302 This could really be done as a loop rather
947
 
            #       than a bunch of recursive calls.
948
 
            parents = set()
949
 
            byid = self._byid
950
 
            def add_ancestors(file_id):
951
 
                if file_id not in byid:
952
 
                    return
953
 
                parent_id = byid[file_id].parent_id
954
 
                if parent_id is None:
955
 
                    return
956
 
                if parent_id not in parents:
957
 
                    parents.add(parent_id)
958
 
                    add_ancestors(parent_id)
959
 
            for file_id in specific_file_ids:
960
 
                add_ancestors(file_id)
961
 
        else:
962
 
            parents = None
963
959
            
964
960
        stack = [(u'', from_dir)]
965
961
        while stack:
970
966
 
971
967
                child_relpath = cur_relpath + child_name
972
968
 
973
 
                if (specific_file_ids is None or 
974
 
                    child_ie.file_id in specific_file_ids or
975
 
                    (yield_parents and child_ie.file_id in parents)):
976
 
                    yield child_relpath, child_ie
 
969
                yield child_relpath, child_ie
977
970
 
978
971
                if child_ie.kind == 'directory':
979
 
                    if parents is None or child_ie.file_id in parents:
980
 
                        child_dirs.append((child_relpath+'/', child_ie))
 
972
                    child_dirs.append((child_relpath+'/', child_ie))
981
973
            stack.extend(reversed(child_dirs))
982
974
 
983
 
    def make_entry(self, kind, name, parent_id, file_id=None):
984
 
        """Simple thunk to bzrlib.inventory.make_entry."""
985
 
        return make_entry(kind, name, parent_id, file_id)
986
 
 
987
975
    def entries(self):
988
976
        """Return list of (path, ie) for all entries except the root.
989
977
 
994
982
            kids = dir_ie.children.items()
995
983
            kids.sort()
996
984
            for name, ie in kids:
997
 
                child_path = osutils.pathjoin(dir_path, name)
 
985
                child_path = pathjoin(dir_path, name)
998
986
                accum.append((child_path, ie))
999
987
                if ie.kind == 'directory':
1000
988
                    descend(ie, child_path)
1013
1001
            kids.sort()
1014
1002
 
1015
1003
            for name, child_ie in kids:
1016
 
                child_path = osutils.pathjoin(parent_path, name)
 
1004
                child_path = pathjoin(parent_path, name)
1017
1005
                descend(child_ie, child_path)
1018
1006
        descend(self.root, u'')
1019
1007
        return accum
1029
1017
        >>> '456' in inv
1030
1018
        False
1031
1019
        """
1032
 
        return (file_id in self._byid)
 
1020
        return file_id in self._byid
1033
1021
 
1034
1022
    def __getitem__(self, file_id):
1035
1023
        """Return the entry for given file_id.
1043
1031
        try:
1044
1032
            return self._byid[file_id]
1045
1033
        except KeyError:
1046
 
            # really we're passing an inventory, not a tree...
1047
 
            raise errors.NoSuchId(self, file_id)
 
1034
            if file_id is None:
 
1035
                raise BzrError("can't look up file_id None")
 
1036
            else:
 
1037
                raise BzrError("file_id {%s} not in inventory" % file_id)
1048
1038
 
1049
1039
    def get_file_kind(self, file_id):
1050
1040
        return self._byid[file_id].kind
1052
1042
    def get_child(self, parent_id, filename):
1053
1043
        return self[parent_id].children.get(filename)
1054
1044
 
1055
 
    def _add_child(self, entry):
1056
 
        """Add an entry to the inventory, without adding it to its parent"""
1057
 
        if entry.file_id in self._byid:
1058
 
            raise BzrError("inventory already contains entry with id {%s}" %
1059
 
                           entry.file_id)
1060
 
        self._byid[entry.file_id] = entry
1061
 
        for child in getattr(entry, 'children', {}).itervalues():
1062
 
            self._add_child(child)
1063
 
        return entry
1064
 
 
1065
1045
    def add(self, entry):
1066
1046
        """Add entry to inventory.
1067
1047
 
1071
1051
        Returns the new entry object.
1072
1052
        """
1073
1053
        if entry.file_id in self._byid:
1074
 
            raise errors.DuplicateFileId(entry.file_id,
1075
 
                                         self._byid[entry.file_id])
 
1054
            raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
1076
1055
 
1077
1056
        if entry.parent_id is None:
1078
 
            self.root = entry
1079
 
        else:
1080
 
            try:
1081
 
                parent = self._byid[entry.parent_id]
1082
 
            except KeyError:
1083
 
                raise BzrError("parent_id {%s} not in inventory" %
1084
 
                               entry.parent_id)
1085
 
 
1086
 
            if entry.name in parent.children:
1087
 
                raise BzrError("%s is already versioned" %
1088
 
                        osutils.pathjoin(self.id2path(parent.file_id),
1089
 
                        entry.name).encode('utf-8'))
1090
 
            parent.children[entry.name] = entry
1091
 
        return self._add_child(entry)
 
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
 
1062
            entry.parent_id = self.root.file_id
 
1063
 
 
1064
        try:
 
1065
            parent = self._byid[entry.parent_id]
 
1066
        except KeyError:
 
1067
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
 
1068
 
 
1069
        if entry.name in parent.children:
 
1070
            raise BzrError("%s is already versioned" %
 
1071
                    pathjoin(self.id2path(parent.file_id), entry.name))
 
1072
 
 
1073
        self._byid[entry.file_id] = entry
 
1074
        parent.children[entry.name] = entry
 
1075
        return entry
1092
1076
 
1093
1077
    def add_path(self, relpath, kind, file_id=None, parent_id=None):
1094
1078
        """Add entry from a path.
1101
1085
 
1102
1086
        if len(parts) == 0:
1103
1087
            if file_id is None:
1104
 
                file_id = generate_ids.gen_root_id()
 
1088
                file_id = bzrlib.workingtree.gen_root_id()
1105
1089
            self.root = InventoryDirectory(file_id, '', None)
1106
1090
            self._byid = {self.root.file_id: self.root}
1107
 
            return self.root
 
1091
            return
1108
1092
        else:
1109
1093
            parent_path = parts[:-1]
1110
1094
            parent_id = self.path2id(parent_path)
1111
1095
            if parent_id is None:
1112
 
                raise errors.NotVersionedError(path=parent_path)
 
1096
                raise NotVersionedError(path=parent_path)
1113
1097
        ie = make_entry(kind, parts[-1], parent_id, file_id)
1114
1098
        return self.add(ie)
1115
1099
 
1126
1110
        False
1127
1111
        """
1128
1112
        ie = self[file_id]
 
1113
 
 
1114
        assert ie.parent_id is None or \
 
1115
            self[ie.parent_id].children[ie.name] == ie
 
1116
        
1129
1117
        del self._byid[file_id]
1130
1118
        if ie.parent_id is not None:
1131
1119
            del self[ie.parent_id].children[ie.name]
1163
1151
            try:
1164
1152
                ie = self._byid[file_id]
1165
1153
            except KeyError:
1166
 
                raise errors.NoSuchId(tree=None, file_id=file_id)
 
1154
                raise BzrError("file_id {%s} not found in inventory" % file_id)
1167
1155
            yield ie
1168
1156
            file_id = ie.parent_id
1169
1157
 
1205
1193
 
1206
1194
        Returns None IFF the path is not found.
1207
1195
        """
1208
 
        if isinstance(name, basestring):
1209
 
            name = osutils.splitpath(name)
 
1196
        if isinstance(name, types.StringTypes):
 
1197
            name = splitpath(name)
1210
1198
 
1211
1199
        # mutter("lookup path %r" % name)
1212
1200
 
1213
1201
        parent = self.root
1214
 
        if parent is None:
1215
 
            return None
1216
1202
        for f in name:
1217
1203
            try:
1218
 
                children = getattr(parent, 'children', None)
1219
 
                if children is None:
1220
 
                    return None
1221
 
                cie = children[f]
 
1204
                cie = parent.children[f]
 
1205
                assert cie.name == f
 
1206
                assert cie.parent_id == parent.file_id
1222
1207
                parent = cie
1223
1208
            except KeyError:
1224
1209
                # or raise an error?
1230
1215
        return bool(self.path2id(names))
1231
1216
 
1232
1217
    def has_id(self, file_id):
1233
 
        return (file_id in self._byid)
1234
 
 
1235
 
    def remove_recursive_id(self, file_id):
1236
 
        """Remove file_id, and children, from the inventory.
1237
 
        
1238
 
        :param file_id: A file_id to remove.
1239
 
        """
1240
 
        to_find_delete = [self._byid[file_id]]
1241
 
        to_delete = []
1242
 
        while to_find_delete:
1243
 
            ie = to_find_delete.pop()
1244
 
            to_delete.append(ie.file_id)
1245
 
            if ie.kind == 'directory':
1246
 
                to_find_delete.extend(ie.children.values())
1247
 
        for file_id in reversed(to_delete):
1248
 
            ie = self[file_id]
1249
 
            del self._byid[file_id]
1250
 
        if ie.parent_id is not None:
1251
 
            del self[ie.parent_id].children[ie.name]
1252
 
        else:
1253
 
            self.root = None
 
1218
        return self._byid.has_key(file_id)
1254
1219
 
1255
1220
    def rename(self, file_id, new_parent_id, new_name):
1256
1221
        """Move a file within the inventory.
1257
1222
 
1258
1223
        This can change either the name, or the parent, or both.
1259
1224
 
1260
 
        This does not move the working file.
1261
 
        """
1262
 
        new_name = ensure_normalized_name(new_name)
 
1225
        This does not move the working file."""
1263
1226
        if not is_valid_name(new_name):
1264
1227
            raise BzrError("not an acceptable filename: %r" % new_name)
1265
1228
 
1283
1246
        file_ie.name = new_name
1284
1247
        file_ie.parent_id = new_parent_id
1285
1248
 
1286
 
    def is_root(self, file_id):
1287
 
        return self.root is not None and file_id == self.root.file_id
1288
 
 
1289
 
 
1290
 
entry_factory = {
1291
 
    'directory': InventoryDirectory,
1292
 
    'file': InventoryFile,
1293
 
    'symlink': InventoryLink,
1294
 
    'tree-reference': TreeReference
1295
 
}
1296
1249
 
1297
1250
def make_entry(kind, name, parent_id, file_id=None):
1298
1251
    """Create an inventory entry.
1303
1256
    :param file_id: the file_id to use. if None, one will be created.
1304
1257
    """
1305
1258
    if file_id is None:
1306
 
        file_id = generate_ids.gen_file_id(name)
1307
 
    name = ensure_normalized_name(name)
1308
 
    try:
1309
 
        factory = entry_factory[kind]
1310
 
    except KeyError:
1311
 
        raise BzrError("unknown kind %r" % kind)
1312
 
    return factory(file_id, name, parent_id)
1313
 
 
1314
 
 
1315
 
def ensure_normalized_name(name):
1316
 
    """Normalize name.
1317
 
 
1318
 
    :raises InvalidNormalization: When name is not normalized, and cannot be
1319
 
        accessed on this platform by the normalized path.
1320
 
    :return: The NFC normalised version of name.
1321
 
    """
1322
 
    #------- This has been copied to bzrlib.dirstate.DirState.add, please
1323
 
    # keep them synchronised.
1324
 
    # we dont import normalized_filename directly because we want to be
1325
 
    # able to change the implementation at runtime for tests.
 
1259
        file_id = bzrlib.workingtree.gen_file_id(name)
 
1260
 
1326
1261
    norm_name, can_access = osutils.normalized_filename(name)
1327
1262
    if norm_name != name:
1328
1263
        if can_access:
1329
 
            return norm_name
 
1264
            name = norm_name
1330
1265
        else:
1331
1266
            # TODO: jam 20060701 This would probably be more useful
1332
1267
            #       if the error was raised with the full path
1333
1268
            raise errors.InvalidNormalization(name)
1334
 
    return name
 
1269
 
 
1270
    if kind == 'directory':
 
1271
        return InventoryDirectory(file_id, name, parent_id)
 
1272
    elif kind == 'file':
 
1273
        return InventoryFile(file_id, name, parent_id)
 
1274
    elif kind == 'symlink':
 
1275
        return InventoryLink(file_id, name, parent_id)
 
1276
    else:
 
1277
        raise BzrError("unknown kind %r" % kind)
1335
1278
 
1336
1279
 
1337
1280
_NAME_RE = None