~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

Late bind to PatienceSequenceMatcher to allow plugin to override.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# (C) 2005 Canonical Ltd
 
1
# Copyright (C) 2005, 2006 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
36
36
 
37
37
import bzrlib
38
38
from bzrlib.osutils import (pumpfile, quotefn, splitpath, joinpath,
39
 
                            appendpath, sha_strings)
 
39
                            pathjoin, sha_strings)
 
40
from bzrlib.errors import (NotVersionedError, InvalidEntryName,
 
41
                           BzrError, BzrCheckError, BinaryFile)
40
42
from bzrlib.trace import mutter
41
 
from bzrlib.errors import (NotVersionedError, InvalidEntryName,
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
 
    >>> for j in i.iter_entries():
83
 
    ...   print j
 
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])
84
85
    ... 
85
 
    ('src', InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
86
 
    ('src/hello.c', InventoryFile('2323', 'hello.c', parent_id='123'))
 
86
    (True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
 
87
    (True, InventoryFile('2323', 'hello.c', parent_id='123'))
87
88
    >>> i.add(InventoryFile('2323', 'bye.c', '123'))
88
89
    Traceback (most recent call last):
89
90
    ...
101
102
    >>> i['2326']
102
103
    InventoryFile('2326', 'wibble.c', parent_id='2325')
103
104
    >>> for path, entry in i.iter_entries():
104
 
    ...     print path.replace('\\\\', '/')     # for win32 os.sep
 
105
    ...     print path
105
106
    ...     assert i.path2id(path)
106
107
    ... 
107
108
    src
109
110
    src/hello.c
110
111
    src/wibble
111
112
    src/wibble/wibble.c
112
 
    >>> i.id2path('2326').replace('\\\\', '/')
 
113
    >>> i.id2path('2326')
113
114
    'src/wibble/wibble.c'
114
115
    """
 
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'
115
124
    
116
125
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
117
126
                 'text_id', 'parent_id', 'children', 'executable', 
118
127
                 'revision']
119
128
 
120
129
    def _add_text_to_weave(self, new_lines, parents, weave_store, transaction):
121
 
        weave_store.add_text(self.file_id, self.revision, new_lines, parents,
122
 
                             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()
123
134
 
124
135
    def detect_changes(self, old_entry):
125
136
        """Return a (text_modified, meta_modified) from this to old_entry.
150
161
             output_to, reverse=False):
151
162
        """Perform a diff between two entries of the same kind."""
152
163
 
153
 
    def find_previous_heads(self, previous_inventories, entry_weave):
 
164
    def find_previous_heads(self, previous_inventories,
 
165
                            versioned_file_store,
 
166
                            transaction,
 
167
                            entry_vf=None):
154
168
        """Return the revisions and entries that directly preceed this.
155
169
 
156
170
        Returned as a map from revision to inventory entry.
158
172
        This is a map containing the file revisions in all parents
159
173
        for which the file exists, and its revision is not a parent of
160
174
        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.
161
181
        """
162
182
        def get_ancestors(weave, entry):
163
 
            return set(map(weave.idx_to_name,
164
 
                           weave.inclusions([weave.lookup(entry.revision)])))
 
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.
165
187
        heads = {}
 
188
        # revision: ancestor list for each head
166
189
        head_ancestors = {}
 
190
        # identify candidate head revision ids.
167
191
        for inv in previous_inventories:
168
192
            if self.file_id in inv:
169
193
                ie = inv[self.file_id]
170
194
                assert ie.file_id == self.file_id
171
 
                if ie.revision in heads:
172
 
                    # fixup logic, there was a bug in revision updates.
173
 
                    # with x bit support.
 
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.
174
200
                    try:
175
 
                        if heads[ie.revision].executable != ie.executable:
176
 
                            heads[ie.revision].executable = False
 
201
                        if candidates[ie.revision].executable != ie.executable:
 
202
                            candidates[ie.revision].executable = False
177
203
                            ie.executable = False
178
204
                    except AttributeError:
179
205
                        pass
180
 
                    assert heads[ie.revision] == ie
 
206
                    # must now be the same.
 
207
                    assert candidates[ie.revision] == ie
181
208
                else:
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
 
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
200
248
        return heads
201
249
 
202
250
    def get_tar_item(self, root, dp, now, tree):
203
251
        """Get a tarfile item and a file stream for its content."""
204
 
        item = tarfile.TarInfo(os.path.join(root, dp))
 
252
        item = tarfile.TarInfo(pathjoin(root, dp))
205
253
        # TODO: would be cool to actually set it to the timestamp of the
206
254
        # revision it was last changed
207
255
        item.mtime = now
266
314
        
267
315
        This is a template method - implement _put_on_disk in subclasses.
268
316
        """
269
 
        fullpath = appendpath(dest, dp)
 
317
        fullpath = pathjoin(dest, dp)
270
318
        self._put_on_disk(fullpath, tree)
271
 
        mutter("  export {%s} kind %s to %s" % (self.file_id, self.kind, fullpath))
 
319
        mutter("  export {%s} kind %s to %s", self.file_id,
 
320
                self.kind, fullpath)
272
321
 
273
322
    def _put_on_disk(self, fullpath, tree):
274
323
        """Put this entry onto disk at fullpath, from tree tree."""
288
337
 
289
338
        This is a template method, override _check for kind specific
290
339
        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.
291
348
        """
292
349
        if self.parent_id != None:
293
350
            if not inv.has_id(self.parent_id):
300
357
        raise BzrCheckError('unknown entry kind %r in revision {%s}' % 
301
358
                            (self.kind, rev_id))
302
359
 
303
 
 
304
360
    def copy(self):
305
361
        """Clone this inventory entry."""
306
362
        raise NotImplementedError
307
363
 
308
 
    def _get_snapshot_change(self, previous_entries):
309
 
        if len(previous_entries) > 1:
310
 
            return 'merged'
311
 
        elif len(previous_entries) == 0:
 
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:
312
381
            return 'added'
313
 
        else:
314
 
            return 'modified/renamed/reparented'
 
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'
315
403
 
316
404
    def __repr__(self):
317
405
        return ("%s(%r, %r, parent_id=%r)"
336
424
                mutter("found unchanged entry")
337
425
                self.revision = parent_ie.revision
338
426
                return "unchanged"
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)
 
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)
346
442
        self.revision = revision
347
 
        change = self._get_snapshot_change(previous_entries)
348
443
        self._snapshot_text(previous_entries, work_tree, weave_store,
349
444
                            transaction)
350
 
        return change
351
445
 
352
446
    def _snapshot_text(self, file_parents, work_tree, weave_store, transaction): 
353
447
        """Record the 'text' of this entry, whatever form that takes.
356
450
        """
357
451
        mutter('storing file {%s} in revision {%s}',
358
452
               self.file_id, self.revision)
359
 
        self._add_text_to_weave([], file_parents, weave_store, transaction)
 
453
        self._add_text_to_weave([], file_parents.keys(), weave_store, transaction)
360
454
 
361
455
    def __eq__(self, other):
362
456
        if not isinstance(other, InventoryEntry):
405
499
        # first requested, or preload them if they're already known
406
500
        pass            # nothing to do by default
407
501
 
 
502
    def _forget_tree_state(self):
 
503
        pass
 
504
 
408
505
 
409
506
class RootEntry(InventoryEntry):
410
507
 
416
513
        self.children = {}
417
514
        self.kind = 'root_directory'
418
515
        self.parent_id = None
419
 
        self.name = ''
 
516
        self.name = u''
420
517
 
421
518
    def __eq__(self, other):
422
519
        if not isinstance(other, RootEntry):
468
565
class InventoryFile(InventoryEntry):
469
566
    """A file in an inventory."""
470
567
 
471
 
    def _check(self, checker, rev_id, tree):
 
568
    def _check(self, checker, tree_revision_id, tree):
472
569
        """See InventoryEntry._check"""
473
 
        revision = self.revision
474
 
        t = (self.file_id, revision)
 
570
        t = (self.file_id, self.revision)
475
571
        if t in checker.checked_texts:
476
 
            prev_sha = checker.checked_texts[t] 
 
572
            prev_sha = checker.checked_texts[t]
477
573
            if prev_sha != self.text_sha1:
478
574
                raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
479
 
                                    (self.file_id, rev_id))
 
575
                                    (self.file_id, tree_revision_id))
480
576
            else:
481
577
                checker.repeated_text_cnt += 1
482
578
                return
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)
 
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))
490
599
        checker.checked_texts[t] = self.text_sha1
491
600
 
492
601
    def copy(self):
509
618
    def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
510
619
             output_to, reverse=False):
511
620
        """See InventoryEntry._diff."""
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)
 
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
523
639
 
524
640
    def has_text(self):
525
641
        """See InventoryEntry.has_text."""
555
671
        self.text_sha1 = work_tree.get_file_sha1(self.file_id)
556
672
        self.executable = work_tree.is_executable(self.file_id)
557
673
 
558
 
    def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
 
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):
559
679
        """See InventoryEntry._snapshot_text."""
560
 
        mutter('storing file {%s} in revision {%s}',
561
 
               self.file_id, self.revision)
 
680
        mutter('storing text of file {%s} in revision {%s} into %r',
 
681
               self.file_id, self.revision, versionedfile_store)
562
682
        # special case to avoid diffing on renames or 
563
683
        # reparenting
564
684
        if (len(file_parents) == 1
565
685
            and self.text_sha1 == file_parents.values()[0].text_sha1
566
686
            and self.text_size == file_parents.values()[0].text_size):
567
687
            previous_ie = file_parents.values()[0]
568
 
            weave_store.add_identical_text(
569
 
                self.file_id, previous_ie.revision, 
570
 
                self.revision, file_parents, transaction)
 
688
            versionedfile = versionedfile_store.get_weave(self.file_id, transaction)
 
689
            versionedfile.clone_text(self.revision, previous_ie.revision, file_parents.keys())
571
690
        else:
572
691
            new_lines = work_tree.get_file(self.file_id).readlines()
573
 
            self._add_text_to_weave(new_lines, file_parents, weave_store,
 
692
            self._add_text_to_weave(new_lines, file_parents.keys(), versionedfile_store,
574
693
                                    transaction)
575
694
            self.text_sha1 = sha_strings(new_lines)
576
695
            self.text_size = sum(map(len, new_lines))
646
765
 
647
766
    def _put_in_tar(self, item, tree):
648
767
        """See InventoryEntry._put_in_tar."""
649
 
        iterm.type = tarfile.SYMTYPE
 
768
        item.type = tarfile.SYMTYPE
650
769
        fileobj = None
651
770
        item.size = 0
652
771
        item.mode = 0755
664
783
        """See InventoryEntry._read_tree_state."""
665
784
        self.symlink_target = work_tree.get_symlink_target(self.file_id)
666
785
 
 
786
    def _forget_tree_state(self):
 
787
        self.symlink_target = None
 
788
 
667
789
    def _unchanged(self, previous_ie):
668
790
        """See InventoryEntry._unchanged."""
669
791
        compatible = super(InventoryLink, self)._unchanged(previous_ie)
710
832
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
711
833
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
712
834
    """
713
 
    def __init__(self, root_id=ROOT_ID):
 
835
    def __init__(self, root_id=ROOT_ID, revision_id=None):
714
836
        """Create or read an inventory.
715
837
 
716
838
        If a working directory is specified, the inventory is read
720
842
        The inventory is created with a default root directory, with
721
843
        an id of None.
722
844
        """
723
 
        # We are letting Branch.initialize() create a unique inventory
 
845
        # We are letting Branch.create() create a unique inventory
724
846
        # root id. Rather than generating a random one here.
725
847
        #if root_id is None:
726
848
        #    root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
727
849
        self.root = RootEntry(root_id)
 
850
        self.revision_id = revision_id
728
851
        self._byid = {self.root.file_id: self.root}
729
852
 
730
853
 
731
854
    def copy(self):
 
855
        # TODO: jam 20051218 Should copy also copy the revision_id?
732
856
        other = Inventory(self.root.file_id)
733
857
        # copy recursively so we know directories will be added before
734
858
        # their children.  There are more efficient ways than this...
762
886
            yield name, ie
763
887
            if ie.kind == 'directory':
764
888
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
765
 
                    yield os.path.join(name, cn), cie
 
889
                    yield pathjoin(name, cn), cie
766
890
 
767
891
 
768
892
    def entries(self):
775
899
            kids = dir_ie.children.items()
776
900
            kids.sort()
777
901
            for name, ie in kids:
778
 
                child_path = os.path.join(dir_path, name)
 
902
                child_path = pathjoin(dir_path, name)
779
903
                accum.append((child_path, ie))
780
904
                if ie.kind == 'directory':
781
905
                    descend(ie, child_path)
782
906
 
783
 
        descend(self.root, '')
 
907
        descend(self.root, u'')
784
908
        return accum
785
909
 
786
910
 
795
919
            kids.sort()
796
920
 
797
921
            for name, child_ie in kids:
798
 
                child_path = os.path.join(parent_path, name)
 
922
                child_path = pathjoin(parent_path, name)
799
923
                descend(child_ie, child_path)
800
 
        descend(self.root, '')
 
924
        descend(self.root, u'')
801
925
        return accum
802
926
        
803
927
 
862
986
 
863
987
        if parent.children.has_key(entry.name):
864
988
            raise BzrError("%s is already versioned" %
865
 
                    appendpath(self.id2path(parent.file_id), entry.name))
 
989
                    pathjoin(self.id2path(parent.file_id), entry.name))
866
990
 
867
991
        self._byid[entry.file_id] = entry
868
992
        parent.children[entry.name] = entry
869
993
        return entry
870
994
 
871
995
 
872
 
    def add_path(self, relpath, kind, file_id=None):
 
996
    def add_path(self, relpath, kind, file_id=None, parent_id=None):
873
997
        """Add entry from a path.
874
998
 
875
999
        The immediate parent must already be versioned.
876
1000
 
877
1001
        Returns the new entry object."""
878
 
        from bzrlib.branch import gen_file_id
879
1002
        
880
1003
        parts = bzrlib.osutils.splitpath(relpath)
 
1004
 
881
1005
        if len(parts) == 0:
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)
 
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
897
1011
        else:
898
 
            raise BzrError("unknown kind %r" % kind)
 
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)
899
1017
        return self.add(ie)
900
1018
 
901
 
 
902
1019
    def __delitem__(self, file_id):
903
1020
        """Remove entry by id.
904
1021
 
913
1030
        """
914
1031
        ie = self[file_id]
915
1032
 
916
 
        assert self[ie.parent_id].children[ie.name] == ie
 
1033
        assert ie.parent_id is None or \
 
1034
            self[ie.parent_id].children[ie.name] == ie
917
1035
        
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
 
 
925
1036
        del self._byid[file_id]
926
 
        del self[ie.parent_id].children[ie.name]
 
1037
        if ie.parent_id is not None:
 
1038
            del self[ie.parent_id].children[ie.name]
927
1039
 
928
1040
 
929
1041
    def __eq__(self, other):
959
1071
    def __hash__(self):
960
1072
        raise ValueError('not hashable')
961
1073
 
 
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
962
1083
 
963
1084
    def get_idpath(self, file_id):
964
1085
        """Return a list of file_ids for the path to an entry.
969
1090
        root directory as depth 1.
970
1091
        """
971
1092
        p = []
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
 
1093
        for parent in self._iter_file_id_parents(file_id):
 
1094
            p.insert(0, parent.file_id)
979
1095
        return p
980
1096
 
981
 
 
982
1097
    def id2path(self, file_id):
983
 
        """Return as a list the path to file_id.
 
1098
        """Return as a string the path to file_id.
984
1099
        
985
1100
        >>> i = Inventory()
986
1101
        >>> e = i.add(InventoryDirectory('src-id', 'src', ROOT_ID))
987
1102
        >>> e = i.add(InventoryFile('foo-id', 'foo.c', parent_id='src-id'))
988
 
        >>> print i.id2path('foo-id').replace(os.sep, '/')
 
1103
        >>> print i.id2path('foo-id')
989
1104
        src/foo.c
990
1105
        """
991
1106
        # get all names, skipping root
992
 
        p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
993
 
        return os.sep.join(p)
 
1107
        return '/'.join(reversed(
 
1108
            [parent.name for parent in 
 
1109
             self._iter_file_id_parents(file_id)][:-1]))
994
1110
            
995
 
 
996
 
 
997
1111
    def path2id(self, name):
998
1112
        """Walk down through directories to return entry of last component.
999
1113
 
1003
1117
        This returns the entry of the last component in the path,
1004
1118
        which may be either a file or a directory.
1005
1119
 
1006
 
        Returns None iff the path is not found.
 
1120
        Returns None IFF the path is not found.
1007
1121
        """
1008
1122
        if isinstance(name, types.StringTypes):
1009
1123
            name = splitpath(name)
1010
1124
 
1011
 
        mutter("lookup path %r" % name)
 
1125
        # mutter("lookup path %r" % name)
1012
1126
 
1013
1127
        parent = self.root
1014
1128
        for f in name:
1062
1176
        file_ie.parent_id = new_parent_id
1063
1177
 
1064
1178
 
 
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
 
1065
1198
 
1066
1199
 
1067
1200
_NAME_RE = None