~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

Update news and readme

- better explanation of dependencies

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006 Canonical Ltd
 
1
# (C) 2005 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
36
36
 
37
37
import bzrlib
38
38
from bzrlib.osutils import (pumpfile, quotefn, splitpath, joinpath,
39
 
                            pathjoin, sha_strings)
 
39
                            appendpath, sha_strings)
 
40
from bzrlib.trace import mutter
40
41
from bzrlib.errors import (NotVersionedError, InvalidEntryName,
41
 
                           BzrError, BzrCheckError, BinaryFile)
42
 
from bzrlib.trace import mutter
 
42
                           BzrError, BzrCheckError)
43
43
 
44
44
 
45
45
class InventoryEntry(object):
79
79
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
80
80
    >>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
81
81
    InventoryFile('2323', 'hello.c', parent_id='123')
82
 
    >>> shouldbe = {0: 'src', 1: pathjoin('src','hello.c')}
83
 
    >>> for ix, j in enumerate(i.iter_entries()):
84
 
    ...   print (j[0] == shouldbe[ix], j[1])
 
82
    >>> for j in i.iter_entries():
 
83
    ...   print j
85
84
    ... 
86
 
    (True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
87
 
    (True, InventoryFile('2323', 'hello.c', parent_id='123'))
 
85
    ('src', InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
 
86
    ('src/hello.c', InventoryFile('2323', 'hello.c', parent_id='123'))
88
87
    >>> i.add(InventoryFile('2323', 'bye.c', '123'))
89
88
    Traceback (most recent call last):
90
89
    ...
102
101
    >>> i['2326']
103
102
    InventoryFile('2326', 'wibble.c', parent_id='2325')
104
103
    >>> for path, entry in i.iter_entries():
105
 
    ...     print path
 
104
    ...     print path.replace('\\\\', '/')     # for win32 os.sep
106
105
    ...     assert i.path2id(path)
107
106
    ... 
108
107
    src
110
109
    src/hello.c
111
110
    src/wibble
112
111
    src/wibble/wibble.c
113
 
    >>> i.id2path('2326')
 
112
    >>> i.id2path('2326').replace('\\\\', '/')
114
113
    'src/wibble/wibble.c'
115
114
    """
116
 
 
117
 
    # Constants returned by describe_change()
118
 
    #
119
 
    # TODO: These should probably move to some kind of FileChangeDescription 
120
 
    # class; that's like what's inside a TreeDelta but we want to be able to 
121
 
    # generate them just for one file at a time.
122
 
    RENAMED = 'renamed'
123
 
    MODIFIED_AND_RENAMED = 'modified and renamed'
124
115
    
125
116
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
126
117
                 'text_id', 'parent_id', 'children', 'executable', 
127
118
                 'revision']
128
119
 
129
120
    def _add_text_to_weave(self, new_lines, parents, weave_store, transaction):
130
 
        versionedfile = weave_store.get_weave_or_empty(self.file_id,
131
 
                                                       transaction)
132
 
        versionedfile.add_lines(self.revision, parents, new_lines)
133
 
        versionedfile.clear_cache()
 
121
        weave_store.add_text(self.file_id, self.revision, new_lines, parents,
 
122
                             transaction)
134
123
 
135
124
    def detect_changes(self, old_entry):
136
125
        """Return a (text_modified, meta_modified) from this to old_entry.
161
150
             output_to, reverse=False):
162
151
        """Perform a diff between two entries of the same kind."""
163
152
 
164
 
    def find_previous_heads(self, previous_inventories,
165
 
                            versioned_file_store,
166
 
                            transaction,
167
 
                            entry_vf=None):
 
153
    def find_previous_heads(self, previous_inventories, entry_weave):
168
154
        """Return the revisions and entries that directly preceed this.
169
155
 
170
156
        Returned as a map from revision to inventory entry.
172
158
        This is a map containing the file revisions in all parents
173
159
        for which the file exists, and its revision is not a parent of
174
160
        any other. If the file is new, the set will be empty.
175
 
 
176
 
        :param versioned_file_store: A store where ancestry data on this
177
 
                                     file id can be queried.
178
 
        :param transaction: The transaction that queries to the versioned 
179
 
                            file store should be completed under.
180
 
        :param entry_vf: The entry versioned file, if its already available.
181
161
        """
182
162
        def get_ancestors(weave, entry):
183
 
            return set(weave.get_ancestry(entry.revision))
184
 
        # revision:ie mapping for each ie found in previous_inventories.
185
 
        candidates = {}
186
 
        # revision:ie mapping with one revision for each head.
 
163
            return set(map(weave.idx_to_name,
 
164
                           weave.inclusions([weave.lookup(entry.revision)])))
187
165
        heads = {}
188
 
        # revision: ancestor list for each head
189
166
        head_ancestors = {}
190
 
        # identify candidate head revision ids.
191
167
        for inv in previous_inventories:
192
168
            if self.file_id in inv:
193
169
                ie = inv[self.file_id]
194
170
                assert ie.file_id == self.file_id
195
 
                if ie.revision in candidates:
196
 
                    # same revision value in two different inventories:
197
 
                    # correct possible inconsistencies:
198
 
                    #     * there was a bug in revision updates with 'x' bit 
199
 
                    #       support.
 
171
                if ie.revision in heads:
 
172
                    # fixup logic, there was a bug in revision updates.
 
173
                    # with x bit support.
200
174
                    try:
201
 
                        if candidates[ie.revision].executable != ie.executable:
202
 
                            candidates[ie.revision].executable = False
 
175
                        if heads[ie.revision].executable != ie.executable:
 
176
                            heads[ie.revision].executable = False
203
177
                            ie.executable = False
204
178
                    except AttributeError:
205
179
                        pass
206
 
                    # must now be the same.
207
 
                    assert candidates[ie.revision] == ie
 
180
                    assert heads[ie.revision] == ie
208
181
                else:
209
 
                    # add this revision as a candidate.
210
 
                    candidates[ie.revision] = ie
211
 
 
212
 
        # common case optimisation
213
 
        if len(candidates) == 1:
214
 
            # if there is only one candidate revision found
215
 
            # then we can opening the versioned file to access ancestry:
216
 
            # there cannot be any ancestors to eliminate when there is 
217
 
            # only one revision available.
218
 
            heads[ie.revision] = ie
219
 
            return heads
220
 
 
221
 
        # eliminate ancestors amongst the available candidates:
222
 
        # heads are those that are not an ancestor of any other candidate
223
 
        # - this provides convergence at a per-file level.
224
 
        for ie in candidates.values():
225
 
            # may be an ancestor of a known head:
226
 
            already_present = 0 != len(
227
 
                [head for head in heads 
228
 
                 if ie.revision in head_ancestors[head]])
229
 
            if already_present:
230
 
                # an ancestor of an analyzed candidate.
231
 
                continue
232
 
            # not an ancestor of a known head:
233
 
            # load the versioned file for this file id if needed
234
 
            if entry_vf is None:
235
 
                entry_vf = versioned_file_store.get_weave_or_empty(
236
 
                    self.file_id, transaction)
237
 
            ancestors = get_ancestors(entry_vf, ie)
238
 
            # may knock something else out:
239
 
            check_heads = list(heads.keys())
240
 
            for head in check_heads:
241
 
                if head in ancestors:
242
 
                    # this previously discovered 'head' is not
243
 
                    # really a head - its an ancestor of the newly 
244
 
                    # found head,
245
 
                    heads.pop(head)
246
 
            head_ancestors[ie.revision] = ancestors
247
 
            heads[ie.revision] = ie
 
182
                    # may want to add it.
 
183
                    # may already be covered:
 
184
                    already_present = 0 != len(
 
185
                        [head for head in heads 
 
186
                         if ie.revision in head_ancestors[head]])
 
187
                    if already_present:
 
188
                        # an ancestor of a known head.
 
189
                        continue
 
190
                    # definately a head:
 
191
                    ancestors = get_ancestors(entry_weave, ie)
 
192
                    # may knock something else out:
 
193
                    check_heads = list(heads.keys())
 
194
                    for head in check_heads:
 
195
                        if head in ancestors:
 
196
                            # this head is not really a head
 
197
                            heads.pop(head)
 
198
                    head_ancestors[ie.revision] = ancestors
 
199
                    heads[ie.revision] = ie
248
200
        return heads
249
201
 
250
202
    def get_tar_item(self, root, dp, now, tree):
251
203
        """Get a tarfile item and a file stream for its content."""
252
 
        item = tarfile.TarInfo(pathjoin(root, dp))
 
204
        item = tarfile.TarInfo(os.path.join(root, dp))
253
205
        # TODO: would be cool to actually set it to the timestamp of the
254
206
        # revision it was last changed
255
207
        item.mtime = now
314
266
        
315
267
        This is a template method - implement _put_on_disk in subclasses.
316
268
        """
317
 
        fullpath = pathjoin(dest, dp)
 
269
        fullpath = appendpath(dest, dp)
318
270
        self._put_on_disk(fullpath, tree)
319
 
        mutter("  export {%s} kind %s to %s", self.file_id,
320
 
                self.kind, fullpath)
 
271
        mutter("  export {%s} kind %s to %s" % (self.file_id, self.kind, fullpath))
321
272
 
322
273
    def _put_on_disk(self, fullpath, tree):
323
274
        """Put this entry onto disk at fullpath, from tree tree."""
337
288
 
338
289
        This is a template method, override _check for kind specific
339
290
        tests.
340
 
 
341
 
        :param checker: Check object providing context for the checks; 
342
 
             can be used to find out what parts of the repository have already
343
 
             been checked.
344
 
        :param rev_id: Revision id from which this InventoryEntry was loaded.
345
 
             Not necessarily the last-changed revision for this file.
346
 
        :param inv: Inventory from which the entry was loaded.
347
 
        :param tree: RevisionTree for this entry.
348
291
        """
349
292
        if self.parent_id != None:
350
293
            if not inv.has_id(self.parent_id):
357
300
        raise BzrCheckError('unknown entry kind %r in revision {%s}' % 
358
301
                            (self.kind, rev_id))
359
302
 
 
303
 
360
304
    def copy(self):
361
305
        """Clone this inventory entry."""
362
306
        raise NotImplementedError
363
307
 
364
 
    @staticmethod
365
 
    def describe_change(old_entry, new_entry):
366
 
        """Describe the change between old_entry and this.
367
 
        
368
 
        This smells of being an InterInventoryEntry situation, but as its
369
 
        the first one, we're making it a static method for now.
370
 
 
371
 
        An entry with a different parent, or different name is considered 
372
 
        to be renamed. Reparenting is an internal detail.
373
 
        Note that renaming the parent does not trigger a rename for the
374
 
        child entry itself.
375
 
        """
376
 
        # TODO: Perhaps return an object rather than just a string
377
 
        if old_entry is new_entry:
378
 
            # also the case of both being None
379
 
            return 'unchanged'
380
 
        elif old_entry is None:
 
308
    def _get_snapshot_change(self, previous_entries):
 
309
        if len(previous_entries) > 1:
 
310
            return 'merged'
 
311
        elif len(previous_entries) == 0:
381
312
            return 'added'
382
 
        elif new_entry is None:
383
 
            return 'removed'
384
 
        text_modified, meta_modified = new_entry.detect_changes(old_entry)
385
 
        if text_modified or meta_modified:
386
 
            modified = True
387
 
        else:
388
 
            modified = False
389
 
        # TODO 20060511 (mbp, rbc) factor out 'detect_rename' here.
390
 
        if old_entry.parent_id != new_entry.parent_id:
391
 
            renamed = True
392
 
        elif old_entry.name != new_entry.name:
393
 
            renamed = True
394
 
        else:
395
 
            renamed = False
396
 
        if renamed and not modified:
397
 
            return InventoryEntry.RENAMED
398
 
        if modified and not renamed:
399
 
            return 'modified'
400
 
        if modified and renamed:
401
 
            return InventoryEntry.MODIFIED_AND_RENAMED
402
 
        return 'unchanged'
 
313
        else:
 
314
            return 'modified/renamed/reparented'
403
315
 
404
316
    def __repr__(self):
405
317
        return ("%s(%r, %r, parent_id=%r)"
424
336
                mutter("found unchanged entry")
425
337
                self.revision = parent_ie.revision
426
338
                return "unchanged"
427
 
        return self._snapshot_into_revision(revision, previous_entries, 
428
 
                                            work_tree, weave_store, transaction)
429
 
 
430
 
    def _snapshot_into_revision(self, revision, previous_entries, work_tree,
431
 
                                weave_store, transaction):
432
 
        """Record this revision unconditionally into a store.
433
 
 
434
 
        The entry's last-changed revision property (`revision`) is updated to 
435
 
        that of the new revision.
436
 
        
437
 
        :param revision: id of the new revision that is being recorded.
438
 
 
439
 
        :returns: String description of the commit (e.g. "merged", "modified"), etc.
440
 
        """
441
 
        mutter('new revision {%s} for {%s}', revision, self.file_id)
 
339
        return self.snapshot_revision(revision, previous_entries, 
 
340
                                      work_tree, weave_store, transaction)
 
341
 
 
342
    def snapshot_revision(self, revision, previous_entries, work_tree,
 
343
                          weave_store, transaction):
 
344
        """Record this revision unconditionally."""
 
345
        mutter('new revision for {%s}', self.file_id)
442
346
        self.revision = revision
 
347
        change = self._get_snapshot_change(previous_entries)
443
348
        self._snapshot_text(previous_entries, work_tree, weave_store,
444
349
                            transaction)
 
350
        return change
445
351
 
446
352
    def _snapshot_text(self, file_parents, work_tree, weave_store, transaction): 
447
353
        """Record the 'text' of this entry, whatever form that takes.
450
356
        """
451
357
        mutter('storing file {%s} in revision {%s}',
452
358
               self.file_id, self.revision)
453
 
        self._add_text_to_weave([], file_parents.keys(), weave_store, transaction)
 
359
        self._add_text_to_weave([], file_parents, weave_store, transaction)
454
360
 
455
361
    def __eq__(self, other):
456
362
        if not isinstance(other, InventoryEntry):
499
405
        # first requested, or preload them if they're already known
500
406
        pass            # nothing to do by default
501
407
 
502
 
    def _forget_tree_state(self):
503
 
        pass
504
 
 
505
408
 
506
409
class RootEntry(InventoryEntry):
507
410
 
513
416
        self.children = {}
514
417
        self.kind = 'root_directory'
515
418
        self.parent_id = None
516
 
        self.name = u''
 
419
        self.name = ''
517
420
 
518
421
    def __eq__(self, other):
519
422
        if not isinstance(other, RootEntry):
565
468
class InventoryFile(InventoryEntry):
566
469
    """A file in an inventory."""
567
470
 
568
 
    def _check(self, checker, tree_revision_id, tree):
 
471
    def _check(self, checker, rev_id, tree):
569
472
        """See InventoryEntry._check"""
570
 
        t = (self.file_id, self.revision)
 
473
        revision = self.revision
 
474
        t = (self.file_id, revision)
571
475
        if t in checker.checked_texts:
572
 
            prev_sha = checker.checked_texts[t]
 
476
            prev_sha = checker.checked_texts[t] 
573
477
            if prev_sha != self.text_sha1:
574
478
                raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
575
 
                                    (self.file_id, tree_revision_id))
 
479
                                    (self.file_id, rev_id))
576
480
            else:
577
481
                checker.repeated_text_cnt += 1
578
482
                return
579
 
 
580
 
        if self.file_id not in checker.checked_weaves:
581
 
            mutter('check weave {%s}', self.file_id)
582
 
            w = tree.get_weave(self.file_id)
583
 
            # Not passing a progress bar, because it creates a new
584
 
            # progress, which overwrites the current progress,
585
 
            # and doesn't look nice
586
 
            w.check()
587
 
            checker.checked_weaves[self.file_id] = True
588
 
        else:
589
 
            w = tree.get_weave(self.file_id)
590
 
 
591
 
        mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
592
 
        checker.checked_text_cnt += 1
593
 
        # We can't check the length, because Weave doesn't store that
594
 
        # information, and the whole point of looking at the weave's
595
 
        # sha1sum is that we don't have to extract the text.
596
 
        if self.text_sha1 != w.get_sha1(self.revision):
597
 
            raise BzrCheckError('text {%s} version {%s} wrong sha1' 
598
 
                                % (self.file_id, self.revision))
 
483
        mutter('check version {%s} of {%s}', rev_id, self.file_id)
 
484
        file_lines = tree.get_file_lines(self.file_id)
 
485
        checker.checked_text_cnt += 1 
 
486
        if self.text_size != sum(map(len, file_lines)):
 
487
            raise BzrCheckError('text {%s} wrong size' % self.text_id)
 
488
        if self.text_sha1 != sha_strings(file_lines):
 
489
            raise BzrCheckError('text {%s} wrong sha1' % self.text_id)
599
490
        checker.checked_texts[t] = self.text_sha1
600
491
 
601
492
    def copy(self):
618
509
    def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
619
510
             output_to, reverse=False):
620
511
        """See InventoryEntry._diff."""
621
 
        try:
622
 
            from_text = tree.get_file(self.file_id).readlines()
623
 
            if to_entry:
624
 
                to_text = to_tree.get_file(to_entry.file_id).readlines()
625
 
            else:
626
 
                to_text = []
627
 
            if not reverse:
628
 
                text_diff(from_label, from_text,
629
 
                          to_label, to_text, output_to)
630
 
            else:
631
 
                text_diff(to_label, to_text,
632
 
                          from_label, from_text, output_to)
633
 
        except BinaryFile:
634
 
            if reverse:
635
 
                label_pair = (to_label, from_label)
636
 
            else:
637
 
                label_pair = (from_label, to_label)
638
 
            print >> output_to, "Binary files %s and %s differ" % label_pair
 
512
        from_text = tree.get_file(self.file_id).readlines()
 
513
        if to_entry:
 
514
            to_text = to_tree.get_file(to_entry.file_id).readlines()
 
515
        else:
 
516
            to_text = []
 
517
        if not reverse:
 
518
            text_diff(from_label, from_text,
 
519
                      to_label, to_text, output_to)
 
520
        else:
 
521
            text_diff(to_label, to_text,
 
522
                      from_label, from_text, output_to)
639
523
 
640
524
    def has_text(self):
641
525
        """See InventoryEntry.has_text."""
671
555
        self.text_sha1 = work_tree.get_file_sha1(self.file_id)
672
556
        self.executable = work_tree.is_executable(self.file_id)
673
557
 
674
 
    def _forget_tree_state(self):
675
 
        self.text_sha1 = None
676
 
        self.executable = None
677
 
 
678
 
    def _snapshot_text(self, file_parents, work_tree, versionedfile_store, transaction):
 
558
    def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
679
559
        """See InventoryEntry._snapshot_text."""
680
 
        mutter('storing text of file {%s} in revision {%s} into %r',
681
 
               self.file_id, self.revision, versionedfile_store)
 
560
        mutter('storing file {%s} in revision {%s}',
 
561
               self.file_id, self.revision)
682
562
        # special case to avoid diffing on renames or 
683
563
        # reparenting
684
564
        if (len(file_parents) == 1
685
565
            and self.text_sha1 == file_parents.values()[0].text_sha1
686
566
            and self.text_size == file_parents.values()[0].text_size):
687
567
            previous_ie = file_parents.values()[0]
688
 
            versionedfile = versionedfile_store.get_weave(self.file_id, transaction)
689
 
            versionedfile.clone_text(self.revision, previous_ie.revision, file_parents.keys())
 
568
            weave_store.add_identical_text(
 
569
                self.file_id, previous_ie.revision, 
 
570
                self.revision, file_parents, transaction)
690
571
        else:
691
572
            new_lines = work_tree.get_file(self.file_id).readlines()
692
 
            self._add_text_to_weave(new_lines, file_parents.keys(), versionedfile_store,
 
573
            self._add_text_to_weave(new_lines, file_parents, weave_store,
693
574
                                    transaction)
694
575
            self.text_sha1 = sha_strings(new_lines)
695
576
            self.text_size = sum(map(len, new_lines))
765
646
 
766
647
    def _put_in_tar(self, item, tree):
767
648
        """See InventoryEntry._put_in_tar."""
768
 
        item.type = tarfile.SYMTYPE
 
649
        iterm.type = tarfile.SYMTYPE
769
650
        fileobj = None
770
651
        item.size = 0
771
652
        item.mode = 0755
783
664
        """See InventoryEntry._read_tree_state."""
784
665
        self.symlink_target = work_tree.get_symlink_target(self.file_id)
785
666
 
786
 
    def _forget_tree_state(self):
787
 
        self.symlink_target = None
788
 
 
789
667
    def _unchanged(self, previous_ie):
790
668
        """See InventoryEntry._unchanged."""
791
669
        compatible = super(InventoryLink, self)._unchanged(previous_ie)
832
710
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
833
711
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
834
712
    """
835
 
    def __init__(self, root_id=ROOT_ID, revision_id=None):
 
713
    def __init__(self, root_id=ROOT_ID):
836
714
        """Create or read an inventory.
837
715
 
838
716
        If a working directory is specified, the inventory is read
842
720
        The inventory is created with a default root directory, with
843
721
        an id of None.
844
722
        """
845
 
        # We are letting Branch.create() create a unique inventory
 
723
        # We are letting Branch.initialize() create a unique inventory
846
724
        # root id. Rather than generating a random one here.
847
725
        #if root_id is None:
848
726
        #    root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
849
727
        self.root = RootEntry(root_id)
850
 
        self.revision_id = revision_id
851
728
        self._byid = {self.root.file_id: self.root}
852
729
 
853
730
 
854
731
    def copy(self):
855
 
        # TODO: jam 20051218 Should copy also copy the revision_id?
856
732
        other = Inventory(self.root.file_id)
857
733
        # copy recursively so we know directories will be added before
858
734
        # their children.  There are more efficient ways than this...
886
762
            yield name, ie
887
763
            if ie.kind == 'directory':
888
764
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
889
 
                    yield pathjoin(name, cn), cie
 
765
                    yield os.path.join(name, cn), cie
890
766
 
891
767
 
892
768
    def entries(self):
899
775
            kids = dir_ie.children.items()
900
776
            kids.sort()
901
777
            for name, ie in kids:
902
 
                child_path = pathjoin(dir_path, name)
 
778
                child_path = os.path.join(dir_path, name)
903
779
                accum.append((child_path, ie))
904
780
                if ie.kind == 'directory':
905
781
                    descend(ie, child_path)
906
782
 
907
 
        descend(self.root, u'')
 
783
        descend(self.root, '')
908
784
        return accum
909
785
 
910
786
 
919
795
            kids.sort()
920
796
 
921
797
            for name, child_ie in kids:
922
 
                child_path = pathjoin(parent_path, name)
 
798
                child_path = os.path.join(parent_path, name)
923
799
                descend(child_ie, child_path)
924
 
        descend(self.root, u'')
 
800
        descend(self.root, '')
925
801
        return accum
926
802
        
927
803
 
986
862
 
987
863
        if parent.children.has_key(entry.name):
988
864
            raise BzrError("%s is already versioned" %
989
 
                    pathjoin(self.id2path(parent.file_id), entry.name))
 
865
                    appendpath(self.id2path(parent.file_id), entry.name))
990
866
 
991
867
        self._byid[entry.file_id] = entry
992
868
        parent.children[entry.name] = entry
993
869
        return entry
994
870
 
995
871
 
996
 
    def add_path(self, relpath, kind, file_id=None, parent_id=None):
 
872
    def add_path(self, relpath, kind, file_id=None):
997
873
        """Add entry from a path.
998
874
 
999
875
        The immediate parent must already be versioned.
1000
876
 
1001
877
        Returns the new entry object."""
 
878
        from bzrlib.branch import gen_file_id
1002
879
        
1003
880
        parts = bzrlib.osutils.splitpath(relpath)
1004
 
 
1005
881
        if len(parts) == 0:
1006
 
            if file_id is None:
1007
 
                file_id = bzrlib.workingtree.gen_root_id()
1008
 
            self.root = RootEntry(file_id)
1009
 
            self._byid = {self.root.file_id: self.root}
1010
 
            return
 
882
            raise BzrError("cannot re-add root of inventory")
 
883
 
 
884
        if file_id == None:
 
885
            file_id = gen_file_id(relpath)
 
886
 
 
887
        parent_path = parts[:-1]
 
888
        parent_id = self.path2id(parent_path)
 
889
        if parent_id == None:
 
890
            raise NotVersionedError(path=parent_path)
 
891
        if kind == 'directory':
 
892
            ie = InventoryDirectory(file_id, parts[-1], parent_id)
 
893
        elif kind == 'file':
 
894
            ie = InventoryFile(file_id, parts[-1], parent_id)
 
895
        elif kind == 'symlink':
 
896
            ie = InventoryLink(file_id, parts[-1], parent_id)
1011
897
        else:
1012
 
            parent_path = parts[:-1]
1013
 
            parent_id = self.path2id(parent_path)
1014
 
            if parent_id == None:
1015
 
                raise NotVersionedError(path=parent_path)
1016
 
        ie = make_entry(kind, parts[-1], parent_id, file_id)
 
898
            raise BzrError("unknown kind %r" % kind)
1017
899
        return self.add(ie)
1018
900
 
 
901
 
1019
902
    def __delitem__(self, file_id):
1020
903
        """Remove entry by id.
1021
904
 
1030
913
        """
1031
914
        ie = self[file_id]
1032
915
 
1033
 
        assert ie.parent_id is None or \
1034
 
            self[ie.parent_id].children[ie.name] == ie
 
916
        assert self[ie.parent_id].children[ie.name] == ie
1035
917
        
 
918
        # TODO: Test deleting all children; maybe hoist to a separate
 
919
        # deltree method?
 
920
        if ie.kind == 'directory':
 
921
            for cie in ie.children.values():
 
922
                del self[cie.file_id]
 
923
            del ie.children
 
924
 
1036
925
        del self._byid[file_id]
1037
 
        if ie.parent_id is not None:
1038
 
            del self[ie.parent_id].children[ie.name]
 
926
        del self[ie.parent_id].children[ie.name]
1039
927
 
1040
928
 
1041
929
    def __eq__(self, other):
1071
959
    def __hash__(self):
1072
960
        raise ValueError('not hashable')
1073
961
 
1074
 
    def _iter_file_id_parents(self, file_id):
1075
 
        """Yield the parents of file_id up to the root."""
1076
 
        while file_id != None:
1077
 
            try:
1078
 
                ie = self._byid[file_id]
1079
 
            except KeyError:
1080
 
                raise BzrError("file_id {%s} not found in inventory" % file_id)
1081
 
            yield ie
1082
 
            file_id = ie.parent_id
1083
962
 
1084
963
    def get_idpath(self, file_id):
1085
964
        """Return a list of file_ids for the path to an entry.
1090
969
        root directory as depth 1.
1091
970
        """
1092
971
        p = []
1093
 
        for parent in self._iter_file_id_parents(file_id):
1094
 
            p.insert(0, parent.file_id)
 
972
        while file_id != None:
 
973
            try:
 
974
                ie = self._byid[file_id]
 
975
            except KeyError:
 
976
                raise BzrError("file_id {%s} not found in inventory" % file_id)
 
977
            p.insert(0, ie.file_id)
 
978
            file_id = ie.parent_id
1095
979
        return p
1096
980
 
 
981
 
1097
982
    def id2path(self, file_id):
1098
 
        """Return as a string the path to file_id.
 
983
        """Return as a list the path to file_id.
1099
984
        
1100
985
        >>> i = Inventory()
1101
986
        >>> e = i.add(InventoryDirectory('src-id', 'src', ROOT_ID))
1102
987
        >>> e = i.add(InventoryFile('foo-id', 'foo.c', parent_id='src-id'))
1103
 
        >>> print i.id2path('foo-id')
 
988
        >>> print i.id2path('foo-id').replace(os.sep, '/')
1104
989
        src/foo.c
1105
990
        """
1106
991
        # get all names, skipping root
1107
 
        return '/'.join(reversed(
1108
 
            [parent.name for parent in 
1109
 
             self._iter_file_id_parents(file_id)][:-1]))
 
992
        p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
 
993
        return os.sep.join(p)
1110
994
            
 
995
 
 
996
 
1111
997
    def path2id(self, name):
1112
998
        """Walk down through directories to return entry of last component.
1113
999
 
1117
1003
        This returns the entry of the last component in the path,
1118
1004
        which may be either a file or a directory.
1119
1005
 
1120
 
        Returns None IFF the path is not found.
 
1006
        Returns None iff the path is not found.
1121
1007
        """
1122
1008
        if isinstance(name, types.StringTypes):
1123
1009
            name = splitpath(name)
1124
1010
 
1125
 
        # mutter("lookup path %r" % name)
 
1011
        mutter("lookup path %r" % name)
1126
1012
 
1127
1013
        parent = self.root
1128
1014
        for f in name:
1176
1062
        file_ie.parent_id = new_parent_id
1177
1063
 
1178
1064
 
1179
 
def make_entry(kind, name, parent_id, file_id=None):
1180
 
    """Create an inventory entry.
1181
 
 
1182
 
    :param kind: the type of inventory entry to create.
1183
 
    :param name: the basename of the entry.
1184
 
    :param parent_id: the parent_id of the entry.
1185
 
    :param file_id: the file_id to use. if None, one will be created.
1186
 
    """
1187
 
    if file_id is None:
1188
 
        file_id = bzrlib.workingtree.gen_file_id(name)
1189
 
    if kind == 'directory':
1190
 
        return InventoryDirectory(file_id, name, parent_id)
1191
 
    elif kind == 'file':
1192
 
        return InventoryFile(file_id, name, parent_id)
1193
 
    elif kind == 'symlink':
1194
 
        return InventoryLink(file_id, name, parent_id)
1195
 
    else:
1196
 
        raise BzrError("unknown kind %r" % kind)
1197
 
 
1198
1065
 
1199
1066
 
1200
1067
_NAME_RE = None