~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2007-09-20 02:40:52 UTC
  • mfrom: (2835.1.1 ianc-integration)
  • Revision ID: pqm@pqm.ubuntu.com-20070920024052-y2l7r5o00zrpnr73
No longer propagate index differences automatically (Robert Collins)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006 Canonical Ltd
 
1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
27
27
# created, but it's not for now.
28
28
ROOT_ID = "TREE_ROOT"
29
29
 
30
 
 
31
 
import os.path
 
30
import os
32
31
import re
33
32
import sys
 
33
 
 
34
from bzrlib.lazy_import import lazy_import
 
35
lazy_import(globals(), """
 
36
import collections
34
37
import tarfile
35
 
import types
36
38
 
37
39
import bzrlib
38
 
from bzrlib.osutils import (pumpfile, quotefn, splitpath, joinpath,
39
 
                            pathjoin, sha_strings)
40
 
from bzrlib.errors import (NotVersionedError, InvalidEntryName,
41
 
                           BzrError, BzrCheckError, BinaryFile)
 
40
from bzrlib import (
 
41
    errors,
 
42
    generate_ids,
 
43
    osutils,
 
44
    symbol_versioning,
 
45
    workingtree,
 
46
    )
 
47
""")
 
48
 
 
49
from bzrlib.errors import (
 
50
    BzrCheckError,
 
51
    BzrError,
 
52
    )
 
53
from bzrlib.symbol_versioning import deprecated_method, zero_ninetyone
42
54
from bzrlib.trace import mutter
43
55
 
44
56
 
76
88
    >>> i.path2id('')
77
89
    'TREE_ROOT'
78
90
    >>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
79
 
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
 
91
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
80
92
    >>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
81
 
    InventoryFile('2323', 'hello.c', parent_id='123')
82
 
    >>> shouldbe = {0: 'src', 1: pathjoin('src','hello.c')}
 
93
    InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
 
94
    >>> shouldbe = {0: '', 1: 'src', 2: 'src/hello.c'}
83
95
    >>> for ix, j in enumerate(i.iter_entries()):
84
96
    ...   print (j[0] == shouldbe[ix], j[1])
85
97
    ... 
86
 
    (True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
87
 
    (True, InventoryFile('2323', 'hello.c', parent_id='123'))
88
 
    >>> i.add(InventoryFile('2323', 'bye.c', '123'))
89
 
    Traceback (most recent call last):
90
 
    ...
91
 
    BzrError: inventory already contains entry with id {2323}
 
98
    (True, InventoryDirectory('TREE_ROOT', u'', parent_id=None, revision=None))
 
99
    (True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
 
100
    (True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
92
101
    >>> i.add(InventoryFile('2324', 'bye.c', '123'))
93
 
    InventoryFile('2324', 'bye.c', parent_id='123')
 
102
    InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
94
103
    >>> i.add(InventoryDirectory('2325', 'wibble', '123'))
95
 
    InventoryDirectory('2325', 'wibble', parent_id='123')
 
104
    InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
96
105
    >>> i.path2id('src/wibble')
97
106
    '2325'
98
107
    >>> '2325' in i
99
108
    True
100
109
    >>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
101
 
    InventoryFile('2326', 'wibble.c', parent_id='2325')
 
110
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
102
111
    >>> i['2326']
103
 
    InventoryFile('2326', 'wibble.c', parent_id='2325')
 
112
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
104
113
    >>> for path, entry in i.iter_entries():
105
114
    ...     print path
106
115
    ...     assert i.path2id(path)
107
116
    ... 
 
117
    <BLANKLINE>
108
118
    src
109
119
    src/bye.c
110
120
    src/hello.c
122
132
    RENAMED = 'renamed'
123
133
    MODIFIED_AND_RENAMED = 'modified and renamed'
124
134
    
125
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
126
 
                 'text_id', 'parent_id', 'children', 'executable', 
127
 
                 'revision']
128
 
 
129
 
    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()
 
135
    __slots__ = []
134
136
 
135
137
    def detect_changes(self, old_entry):
136
138
        """Return a (text_modified, meta_modified) from this to old_entry.
160
162
    def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
161
163
             output_to, reverse=False):
162
164
        """Perform a diff between two entries of the same kind."""
163
 
 
164
 
    def find_previous_heads(self, previous_inventories,
165
 
                            versioned_file_store,
166
 
                            transaction,
167
 
                            entry_vf=None):
168
 
        """Return the revisions and entries that directly preceed this.
169
 
 
170
 
        Returned as a map from revision to inventory entry.
171
 
 
172
 
        This is a map containing the file revisions in all parents
173
 
        for which the file exists, and its revision is not a parent of
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.
 
165
    
 
166
    def parent_candidates(self, previous_inventories):
 
167
        """Find possible per-file graph parents.
 
168
 
 
169
        This is currently defined by:
 
170
         - Select the last changed revision in the parent inventory.
 
171
         - Do deal with a short lived bug in bzr 0.8's development two entries
 
172
           that have the same last changed but different 'x' bit settings are
 
173
           changed in-place.
181
174
        """
182
 
        def get_ancestors(weave, entry):
183
 
            return set(weave.get_ancestry(entry.revision))
184
175
        # revision:ie mapping for each ie found in previous_inventories.
185
176
        candidates = {}
186
 
        # revision:ie mapping with one revision for each head.
187
 
        heads = {}
188
 
        # revision: ancestor list for each head
189
 
        head_ancestors = {}
190
177
        # identify candidate head revision ids.
191
178
        for inv in previous_inventories:
192
179
            if self.file_id in inv:
208
195
                else:
209
196
                    # add this revision as a candidate.
210
197
                    candidates[ie.revision] = ie
211
 
 
 
198
        return candidates
 
199
 
 
200
    @deprecated_method(zero_ninetyone)
 
201
    def find_previous_heads(self, previous_inventories,
 
202
                            versioned_file_store,
 
203
                            transaction,
 
204
                            entry_vf=None):
 
205
        """Return the revisions and entries that directly precede this.
 
206
 
 
207
        Returned as a map from revision to inventory entry.
 
208
 
 
209
        This is a map containing the file revisions in all parents
 
210
        for which the file exists, and its revision is not a parent of
 
211
        any other. If the file is new, the set will be empty.
 
212
 
 
213
        :param versioned_file_store: A store where ancestry data on this
 
214
                                     file id can be queried.
 
215
        :param transaction: The transaction that queries to the versioned 
 
216
                            file store should be completed under.
 
217
        :param entry_vf: The entry versioned file, if its already available.
 
218
        """
 
219
        candidates = self.parent_candidates(previous_inventories)
 
220
 
 
221
        # revision:ie mapping with one revision for each head.
 
222
        heads = {}
212
223
        # common case optimisation
213
224
        if len(candidates) == 1:
214
225
            # if there is only one candidate revision found
215
 
            # then we can opening the versioned file to access ancestry:
 
226
            # then we can avoid opening the versioned file to access ancestry:
216
227
            # there cannot be any ancestors to eliminate when there is 
217
228
            # only one revision available.
218
 
            heads[ie.revision] = ie
219
 
            return heads
 
229
            return candidates
 
230
        
 
231
        # --- what follows is now encapsulated in repository.get_graph.heads(), 
 
232
        #     but that is not accessible from here as we have no repository
 
233
        #     pointer. Note that the repository.get_graph.heads() call can return
 
234
        #     different results *at the moment* because of the kind-changing check
 
235
        #     we have in parent_candidates().
220
236
 
221
237
        # eliminate ancestors amongst the available candidates:
222
238
        # heads are those that are not an ancestor of any other candidate
223
239
        # - this provides convergence at a per-file level.
 
240
        def get_ancestors(weave, entry):
 
241
            return set(weave.get_ancestry(entry.revision, topo_sorted=False))
 
242
        # revision: ancestor list for each head
 
243
        head_ancestors = {}
224
244
        for ie in candidates.values():
225
245
            # may be an ancestor of a known head:
226
246
            already_present = 0 != len(
249
269
 
250
270
    def get_tar_item(self, root, dp, now, tree):
251
271
        """Get a tarfile item and a file stream for its content."""
252
 
        item = tarfile.TarInfo(pathjoin(root, dp))
 
272
        item = tarfile.TarInfo(osutils.pathjoin(root, dp).encode('utf8'))
253
273
        # TODO: would be cool to actually set it to the timestamp of the
254
274
        # revision it was last changed
255
275
        item.mtime = now
284
304
        """
285
305
        assert isinstance(name, basestring), name
286
306
        if '/' in name or '\\' in name:
287
 
            raise InvalidEntryName(name=name)
 
307
            raise errors.InvalidEntryName(name=name)
288
308
        self.executable = False
289
309
        self.revision = None
290
310
        self.text_sha1 = None
291
311
        self.text_size = None
292
312
        self.file_id = file_id
 
313
        assert isinstance(file_id, (str, None.__class__)), \
 
314
            'bad type %r for %r' % (type(file_id), file_id)
293
315
        self.name = name
294
316
        self.text_id = text_id
295
317
        self.parent_id = parent_id
296
318
        self.symlink_target = None
 
319
        self.reference_revision = None
297
320
 
298
321
    def kind_character(self):
299
322
        """Return a short kind indicator useful for appending to names."""
300
323
        raise BzrError('unknown kind %r' % self.kind)
301
324
 
302
 
    known_kinds = ('file', 'directory', 'symlink', 'root_directory')
 
325
    known_kinds = ('file', 'directory', 'symlink')
303
326
 
304
327
    def _put_in_tar(self, item, tree):
305
328
        """populate item for stashing in a tar, and return the content stream.
314
337
        
315
338
        This is a template method - implement _put_on_disk in subclasses.
316
339
        """
317
 
        fullpath = pathjoin(dest, dp)
 
340
        fullpath = osutils.pathjoin(dest, dp)
318
341
        self._put_on_disk(fullpath, tree)
319
 
        mutter("  export {%s} kind %s to %s", self.file_id,
320
 
                self.kind, fullpath)
 
342
        # mutter("  export {%s} kind %s to %s", self.file_id,
 
343
        #         self.kind, fullpath)
321
344
 
322
345
    def _put_on_disk(self, fullpath, tree):
323
346
        """Put this entry onto disk at fullpath, from tree tree."""
324
347
        raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
325
348
 
326
349
    def sorted_children(self):
327
 
        l = self.children.items()
328
 
        l.sort()
329
 
        return l
 
350
        return sorted(self.children.items())
330
351
 
331
352
    @staticmethod
332
353
    def versionable_kind(kind):
333
 
        return kind in ('file', 'directory', 'symlink')
 
354
        return (kind in ('file', 'directory', 'symlink', 'tree-reference'))
334
355
 
335
356
    def check(self, checker, rev_id, inv, tree):
336
357
        """Check this inventory entry is intact.
346
367
        :param inv: Inventory from which the entry was loaded.
347
368
        :param tree: RevisionTree for this entry.
348
369
        """
349
 
        if self.parent_id != None:
 
370
        if self.parent_id is not None:
350
371
            if not inv.has_id(self.parent_id):
351
372
                raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
352
373
                        % (self.parent_id, rev_id))
381
402
            return 'added'
382
403
        elif new_entry is None:
383
404
            return 'removed'
 
405
        if old_entry.kind != new_entry.kind:
 
406
            return 'modified'
384
407
        text_modified, meta_modified = new_entry.detect_changes(old_entry)
385
408
        if text_modified or meta_modified:
386
409
            modified = True
402
425
        return 'unchanged'
403
426
 
404
427
    def __repr__(self):
405
 
        return ("%s(%r, %r, parent_id=%r)"
 
428
        return ("%s(%r, %r, parent_id=%r, revision=%r)"
406
429
                % (self.__class__.__name__,
407
430
                   self.file_id,
408
431
                   self.name,
409
 
                   self.parent_id))
 
432
                   self.parent_id,
 
433
                   self.revision))
410
434
 
411
435
    def snapshot(self, revision, path, previous_entries,
412
 
                 work_tree, weave_store, transaction):
 
436
                 work_tree, commit_builder):
413
437
        """Make a snapshot of this entry which may or may not have changed.
414
438
        
415
439
        This means that all its fields are populated, that it has its
416
440
        text stored in the text store or weave.
 
441
 
 
442
        :return: True if anything was recorded
417
443
        """
418
 
        mutter('new parents of %s are %r', path, previous_entries)
 
444
        # cannot be unchanged unless there is only one parent file rev.
419
445
        self._read_tree_state(path, work_tree)
420
446
        if len(previous_entries) == 1:
421
 
            # cannot be unchanged unless there is only one parent file rev.
422
447
            parent_ie = previous_entries.values()[0]
423
448
            if self._unchanged(parent_ie):
424
 
                mutter("found unchanged entry")
425
449
                self.revision = parent_ie.revision
426
 
                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)
 
450
                return False
442
451
        self.revision = revision
443
 
        self._snapshot_text(previous_entries, work_tree, weave_store,
444
 
                            transaction)
 
452
        return self._snapshot_text(previous_entries, work_tree, commit_builder)
445
453
 
446
 
    def _snapshot_text(self, file_parents, work_tree, weave_store, transaction): 
 
454
    def _snapshot_text(self, file_parents, work_tree, commit_builder): 
447
455
        """Record the 'text' of this entry, whatever form that takes.
448
 
        
449
 
        This default implementation simply adds an empty text.
 
456
 
 
457
        :return: True if anything was recorded
450
458
        """
451
 
        mutter('storing file {%s} in revision {%s}',
452
 
               self.file_id, self.revision)
453
 
        self._add_text_to_weave([], file_parents.keys(), weave_store, transaction)
 
459
        raise NotImplementedError(self._snapshot_text)
454
460
 
455
461
    def __eq__(self, other):
456
462
        if not isinstance(other, InventoryEntry):
466
472
                and (self.kind == other.kind)
467
473
                and (self.revision == other.revision)
468
474
                and (self.executable == other.executable)
 
475
                and (self.reference_revision == other.reference_revision)
469
476
                )
470
477
 
471
478
    def __ne__(self, other):
477
484
    def _unchanged(self, previous_ie):
478
485
        """Has this entry changed relative to previous_ie.
479
486
 
480
 
        This method should be overriden in child classes.
 
487
        This method should be overridden in child classes.
481
488
        """
482
489
        compatible = True
483
490
        # different inv parent
486
493
        # renamed
487
494
        elif previous_ie.name != self.name:
488
495
            compatible = False
 
496
        elif previous_ie.kind != self.kind:
 
497
            compatible = False
489
498
        return compatible
490
499
 
491
500
    def _read_tree_state(self, path, work_tree):
505
514
 
506
515
class RootEntry(InventoryEntry):
507
516
 
 
517
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
518
                 'text_id', 'parent_id', 'children', 'executable',
 
519
                 'revision', 'symlink_target', 'reference_revision']
 
520
 
508
521
    def _check(self, checker, rev_id, tree):
509
522
        """See InventoryEntry._check"""
510
523
 
511
524
    def __init__(self, file_id):
512
525
        self.file_id = file_id
513
526
        self.children = {}
514
 
        self.kind = 'root_directory'
 
527
        self.kind = 'directory'
515
528
        self.parent_id = None
516
529
        self.name = u''
 
530
        self.revision = None
 
531
        symbol_versioning.warn('RootEntry is deprecated as of bzr 0.10.'
 
532
                               '  Please use InventoryDirectory instead.',
 
533
                               DeprecationWarning, stacklevel=2)
517
534
 
518
535
    def __eq__(self, other):
519
536
        if not isinstance(other, RootEntry):
526
543
class InventoryDirectory(InventoryEntry):
527
544
    """A directory in an inventory."""
528
545
 
 
546
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
547
                 'text_id', 'parent_id', 'children', 'executable',
 
548
                 'revision', 'symlink_target', 'reference_revision']
 
549
 
529
550
    def _check(self, checker, rev_id, tree):
530
551
        """See InventoryEntry._check"""
531
 
        if self.text_sha1 != None or self.text_size != None or self.text_id != None:
 
552
        if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
532
553
            raise BzrCheckError('directory {%s} has text in revision {%s}'
533
554
                                % (self.file_id, rev_id))
534
555
 
561
582
        """See InventoryEntry._put_on_disk."""
562
583
        os.mkdir(fullpath)
563
584
 
 
585
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
 
586
        """See InventoryEntry._snapshot_text."""
 
587
        commit_builder.modified_directory(self.file_id, file_parents)
 
588
        return True
 
589
 
564
590
 
565
591
class InventoryFile(InventoryEntry):
566
592
    """A file in an inventory."""
567
593
 
 
594
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
595
                 'text_id', 'parent_id', 'children', 'executable',
 
596
                 'revision', 'symlink_target', 'reference_revision']
 
597
 
568
598
    def _check(self, checker, tree_revision_id, tree):
569
599
        """See InventoryEntry._check"""
570
600
        t = (self.file_id, self.revision)
579
609
 
580
610
        if self.file_id not in checker.checked_weaves:
581
611
            mutter('check weave {%s}', self.file_id)
582
 
            w = tree.get_weave(self.file_id)
 
612
            w = tree._get_weave(self.file_id)
583
613
            # Not passing a progress bar, because it creates a new
584
614
            # progress, which overwrites the current progress,
585
615
            # and doesn't look nice
586
616
            w.check()
587
617
            checker.checked_weaves[self.file_id] = True
588
618
        else:
589
 
            w = tree.get_weave(self.file_id)
 
619
            w = tree._get_weave(self.file_id)
590
620
 
591
621
        mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
592
622
        checker.checked_text_cnt += 1
609
639
 
610
640
    def detect_changes(self, old_entry):
611
641
        """See InventoryEntry.detect_changes."""
612
 
        assert self.text_sha1 != None
613
 
        assert old_entry.text_sha1 != None
 
642
        assert self.text_sha1 is not None
 
643
        assert old_entry.text_sha1 is not None
614
644
        text_modified = (self.text_sha1 != old_entry.text_sha1)
615
645
        meta_modified = (self.executable != old_entry.executable)
616
646
        return text_modified, meta_modified
630
660
            else:
631
661
                text_diff(to_label, to_text,
632
662
                          from_label, from_text, output_to)
633
 
        except BinaryFile:
 
663
        except errors.BinaryFile:
634
664
            if reverse:
635
665
                label_pair = (to_label, from_label)
636
666
            else:
637
667
                label_pair = (from_label, to_label)
638
 
            print >> output_to, "Binary files %s and %s differ" % label_pair
 
668
            print >> output_to, \
 
669
                  ("Binary files %s and %s differ" % label_pair).encode('utf8')
639
670
 
640
671
    def has_text(self):
641
672
        """See InventoryEntry.has_text."""
662
693
 
663
694
    def _put_on_disk(self, fullpath, tree):
664
695
        """See InventoryEntry._put_on_disk."""
665
 
        pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
 
696
        osutils.pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
666
697
        if tree.is_executable(self.file_id):
667
698
            os.chmod(fullpath, 0755)
668
699
 
669
700
    def _read_tree_state(self, path, work_tree):
670
701
        """See InventoryEntry._read_tree_state."""
671
 
        self.text_sha1 = work_tree.get_file_sha1(self.file_id)
672
 
        self.executable = work_tree.is_executable(self.file_id)
 
702
        self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
 
703
        # FIXME: 20050930 probe for the text size when getting sha1
 
704
        # in _read_tree_state
 
705
        self.executable = work_tree.is_executable(self.file_id, path=path)
 
706
 
 
707
    def __repr__(self):
 
708
        return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s)"
 
709
                % (self.__class__.__name__,
 
710
                   self.file_id,
 
711
                   self.name,
 
712
                   self.parent_id,
 
713
                   self.text_sha1,
 
714
                   self.text_size))
673
715
 
674
716
    def _forget_tree_state(self):
675
717
        self.text_sha1 = None
676
 
        self.executable = None
677
 
 
678
 
    def _snapshot_text(self, file_parents, work_tree, versionedfile_store, transaction):
679
 
        """See InventoryEntry._snapshot_text."""
680
 
        mutter('storing text of file {%s} in revision {%s} into %r',
681
 
               self.file_id, self.revision, versionedfile_store)
682
 
        # special case to avoid diffing on renames or 
683
 
        # reparenting
684
 
        if (len(file_parents) == 1
685
 
            and self.text_sha1 == file_parents.values()[0].text_sha1
686
 
            and self.text_size == file_parents.values()[0].text_size):
687
 
            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())
 
718
 
 
719
    def snapshot(self, revision, path, previous_entries,
 
720
                 work_tree, commit_builder):
 
721
        """See InventoryEntry.snapshot."""
 
722
        # Note: We use a custom implementation of this method for files
 
723
        # because it's a performance critical part of commit.
 
724
 
 
725
        # If this is the initial commit for this file, we know the sha is
 
726
        # coming later so skip calculating it now (in _read_tree_state())
 
727
        if len(previous_entries) == 0:
 
728
            self.executable = work_tree.is_executable(self.file_id, path=path)
690
729
        else:
691
 
            new_lines = work_tree.get_file(self.file_id).readlines()
692
 
            self._add_text_to_weave(new_lines, file_parents.keys(), versionedfile_store,
693
 
                                    transaction)
694
 
            self.text_sha1 = sha_strings(new_lines)
695
 
            self.text_size = sum(map(len, new_lines))
696
 
 
 
730
            self._read_tree_state(path, work_tree)
 
731
 
 
732
        # If nothing is changed from the sole parent, there's nothing to do
 
733
        if len(previous_entries) == 1:
 
734
            parent_ie = previous_entries.values()[0]
 
735
            if self._unchanged(parent_ie):
 
736
                self.revision = parent_ie.revision
 
737
                return False
 
738
 
 
739
        # Add the file to the repository
 
740
        self.revision = revision
 
741
        def get_content_byte_lines():
 
742
            return work_tree.get_file(self.file_id, path).readlines()
 
743
        self.text_sha1, self.text_size = commit_builder.modified_file_text(
 
744
            self.file_id, previous_entries, get_content_byte_lines,
 
745
            self.text_sha1, self.text_size)
 
746
        return True
697
747
 
698
748
    def _unchanged(self, previous_ie):
699
749
        """See InventoryEntry._unchanged."""
712
762
class InventoryLink(InventoryEntry):
713
763
    """A file in an inventory."""
714
764
 
715
 
    __slots__ = ['symlink_target']
 
765
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
766
                 'text_id', 'parent_id', 'children', 'executable',
 
767
                 'revision', 'symlink_target', 'reference_revision']
716
768
 
717
769
    def _check(self, checker, rev_id, tree):
718
770
        """See InventoryEntry._check"""
719
 
        if self.text_sha1 != None or self.text_size != None or self.text_id != None:
 
771
        if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
720
772
            raise BzrCheckError('symlink {%s} has text in revision {%s}'
721
773
                    % (self.file_id, rev_id))
722
 
        if self.symlink_target == None:
 
774
        if self.symlink_target is None:
723
775
            raise BzrCheckError('symlink {%s} has no target in revision {%s}'
724
776
                    % (self.file_id, rev_id))
725
777
 
793
845
            compatible = False
794
846
        return compatible
795
847
 
 
848
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
 
849
        """See InventoryEntry._snapshot_text."""
 
850
        commit_builder.modified_link(
 
851
            self.file_id, file_parents, self.symlink_target)
 
852
        return True
 
853
 
 
854
 
 
855
class TreeReference(InventoryEntry):
 
856
    
 
857
    kind = 'tree-reference'
 
858
    
 
859
    def __init__(self, file_id, name, parent_id, revision=None,
 
860
                 reference_revision=None):
 
861
        InventoryEntry.__init__(self, file_id, name, parent_id)
 
862
        self.revision = revision
 
863
        self.reference_revision = reference_revision
 
864
 
 
865
    def copy(self):
 
866
        return TreeReference(self.file_id, self.name, self.parent_id,
 
867
                             self.revision, self.reference_revision)
 
868
 
 
869
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
 
870
        commit_builder.modified_reference(self.file_id, file_parents)
 
871
        return True
 
872
 
 
873
    def _read_tree_state(self, path, work_tree):
 
874
        """Populate fields in the inventory entry from the given tree.
 
875
        """
 
876
        self.reference_revision = work_tree.get_reference_revision(
 
877
            self.file_id, path)
 
878
 
 
879
    def _forget_tree_state(self):
 
880
        self.reference_revision = None 
 
881
 
796
882
 
797
883
class Inventory(object):
798
884
    """Inventory of versioned files in a tree.
813
899
 
814
900
    >>> inv = Inventory()
815
901
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
816
 
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT')
 
902
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
817
903
    >>> inv['123-123'].name
818
904
    'hello.c'
819
905
 
827
913
    May also look up by name:
828
914
 
829
915
    >>> [x[0] for x in inv.iter_entries()]
830
 
    ['hello.c']
 
916
    ['', u'hello.c']
831
917
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
832
918
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
833
 
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
 
919
    Traceback (most recent call last):
 
920
    BzrError: parent_id {TREE_ROOT} not in inventory
 
921
    >>> inv.add(InventoryFile('123-123', 'hello.c', 'TREE_ROOT-12345678-12345678'))
 
922
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None)
834
923
    """
835
924
    def __init__(self, root_id=ROOT_ID, revision_id=None):
836
925
        """Create or read an inventory.
842
931
        The inventory is created with a default root directory, with
843
932
        an id of None.
844
933
        """
845
 
        # We are letting Branch.create() create a unique inventory
846
 
        # root id. Rather than generating a random one here.
847
 
        #if root_id is None:
848
 
        #    root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
849
 
        self.root = RootEntry(root_id)
 
934
        if root_id is not None:
 
935
            assert root_id.__class__ == str
 
936
            self._set_root(InventoryDirectory(root_id, u'', None))
 
937
        else:
 
938
            self.root = None
 
939
            self._byid = {}
850
940
        self.revision_id = revision_id
 
941
 
 
942
    def _set_root(self, ie):
 
943
        self.root = ie
851
944
        self._byid = {self.root.file_id: self.root}
852
945
 
853
 
 
854
946
    def copy(self):
855
947
        # TODO: jam 20051218 Should copy also copy the revision_id?
856
 
        other = Inventory(self.root.file_id)
 
948
        entries = self.iter_entries()
 
949
        other = Inventory(entries.next()[1].file_id)
857
950
        # copy recursively so we know directories will be added before
858
951
        # their children.  There are more efficient ways than this...
859
 
        for path, entry in self.iter_entries():
860
 
            if entry == self.root:
861
 
                continue
 
952
        for path, entry in entries():
862
953
            other.add(entry.copy())
863
954
        return other
864
955
 
865
 
 
866
956
    def __iter__(self):
867
957
        return iter(self._byid)
868
958
 
869
 
 
870
959
    def __len__(self):
871
960
        """Returns number of entries."""
872
961
        return len(self._byid)
873
962
 
874
 
 
875
963
    def iter_entries(self, from_dir=None):
876
964
        """Return (path, entry) pairs, in order by name."""
877
 
        if from_dir == None:
878
 
            assert self.root
879
 
            from_dir = self.root
880
 
        elif isinstance(from_dir, basestring):
881
 
            from_dir = self._byid[from_dir]
882
 
            
883
 
        kids = from_dir.children.items()
884
 
        kids.sort()
885
 
        for name, ie in kids:
886
 
            yield name, ie
887
 
            if ie.kind == 'directory':
888
 
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
889
 
                    yield pathjoin(name, cn), cie
890
 
 
 
965
        if from_dir is None:
 
966
            if self.root is None:
 
967
                return
 
968
            from_dir = self.root
 
969
            yield '', self.root
 
970
        elif isinstance(from_dir, basestring):
 
971
            from_dir = self._byid[from_dir]
 
972
            
 
973
        # unrolling the recursive called changed the time from
 
974
        # 440ms/663ms (inline/total) to 116ms/116ms
 
975
        children = from_dir.children.items()
 
976
        children.sort()
 
977
        children = collections.deque(children)
 
978
        stack = [(u'', children)]
 
979
        while stack:
 
980
            from_dir_relpath, children = stack[-1]
 
981
 
 
982
            while children:
 
983
                name, ie = children.popleft()
 
984
 
 
985
                # we know that from_dir_relpath never ends in a slash
 
986
                # and 'f' doesn't begin with one, we can do a string op, rather
 
987
                # than the checks of pathjoin(), though this means that all paths
 
988
                # start with a slash
 
989
                path = from_dir_relpath + '/' + name
 
990
 
 
991
                yield path[1:], ie
 
992
 
 
993
                if ie.kind != 'directory':
 
994
                    continue
 
995
 
 
996
                # But do this child first
 
997
                new_children = ie.children.items()
 
998
                new_children.sort()
 
999
                new_children = collections.deque(new_children)
 
1000
                stack.append((path, new_children))
 
1001
                # Break out of inner loop, so that we start outer loop with child
 
1002
                break
 
1003
            else:
 
1004
                # if we finished all children, pop it off the stack
 
1005
                stack.pop()
 
1006
 
 
1007
    def iter_entries_by_dir(self, from_dir=None, specific_file_ids=None):
 
1008
        """Iterate over the entries in a directory first order.
 
1009
 
 
1010
        This returns all entries for a directory before returning
 
1011
        the entries for children of a directory. This is not
 
1012
        lexicographically sorted order, and is a hybrid between
 
1013
        depth-first and breadth-first.
 
1014
 
 
1015
        :return: This yields (path, entry) pairs
 
1016
        """
 
1017
        if specific_file_ids:
 
1018
            safe = osutils.safe_file_id
 
1019
            specific_file_ids = set(safe(fid) for fid in specific_file_ids)
 
1020
        # TODO? Perhaps this should return the from_dir so that the root is
 
1021
        # yielded? or maybe an option?
 
1022
        if from_dir is None:
 
1023
            if self.root is None:
 
1024
                return
 
1025
            # Optimize a common case
 
1026
            if specific_file_ids is not None and len(specific_file_ids) == 1:
 
1027
                file_id = list(specific_file_ids)[0]
 
1028
                if file_id in self:
 
1029
                    yield self.id2path(file_id), self[file_id]
 
1030
                return 
 
1031
            from_dir = self.root
 
1032
            if (specific_file_ids is None or 
 
1033
                self.root.file_id in specific_file_ids):
 
1034
                yield u'', self.root
 
1035
        elif isinstance(from_dir, basestring):
 
1036
            from_dir = self._byid[from_dir]
 
1037
 
 
1038
        if specific_file_ids is not None:
 
1039
            # TODO: jam 20070302 This could really be done as a loop rather
 
1040
            #       than a bunch of recursive calls.
 
1041
            parents = set()
 
1042
            byid = self._byid
 
1043
            def add_ancestors(file_id):
 
1044
                if file_id not in byid:
 
1045
                    return
 
1046
                parent_id = byid[file_id].parent_id
 
1047
                if parent_id is None:
 
1048
                    return
 
1049
                if parent_id not in parents:
 
1050
                    parents.add(parent_id)
 
1051
                    add_ancestors(parent_id)
 
1052
            for file_id in specific_file_ids:
 
1053
                add_ancestors(file_id)
 
1054
        else:
 
1055
            parents = None
 
1056
            
 
1057
        stack = [(u'', from_dir)]
 
1058
        while stack:
 
1059
            cur_relpath, cur_dir = stack.pop()
 
1060
 
 
1061
            child_dirs = []
 
1062
            for child_name, child_ie in sorted(cur_dir.children.iteritems()):
 
1063
 
 
1064
                child_relpath = cur_relpath + child_name
 
1065
 
 
1066
                if (specific_file_ids is None or 
 
1067
                    child_ie.file_id in specific_file_ids):
 
1068
                    yield child_relpath, child_ie
 
1069
 
 
1070
                if child_ie.kind == 'directory':
 
1071
                    if parents is None or child_ie.file_id in parents:
 
1072
                        child_dirs.append((child_relpath+'/', child_ie))
 
1073
            stack.extend(reversed(child_dirs))
 
1074
 
 
1075
    def make_entry(self, kind, name, parent_id, file_id=None):
 
1076
        """Simple thunk to bzrlib.inventory.make_entry."""
 
1077
        return make_entry(kind, name, parent_id, file_id)
891
1078
 
892
1079
    def entries(self):
893
1080
        """Return list of (path, ie) for all entries except the root.
899
1086
            kids = dir_ie.children.items()
900
1087
            kids.sort()
901
1088
            for name, ie in kids:
902
 
                child_path = pathjoin(dir_path, name)
 
1089
                child_path = osutils.pathjoin(dir_path, name)
903
1090
                accum.append((child_path, ie))
904
1091
                if ie.kind == 'directory':
905
1092
                    descend(ie, child_path)
907
1094
        descend(self.root, u'')
908
1095
        return accum
909
1096
 
910
 
 
911
1097
    def directories(self):
912
1098
        """Return (path, entry) pairs for all directories, including the root.
913
1099
        """
919
1105
            kids.sort()
920
1106
 
921
1107
            for name, child_ie in kids:
922
 
                child_path = pathjoin(parent_path, name)
 
1108
                child_path = osutils.pathjoin(parent_path, name)
923
1109
                descend(child_ie, child_path)
924
1110
        descend(self.root, u'')
925
1111
        return accum
926
1112
        
927
 
 
928
 
 
929
1113
    def __contains__(self, file_id):
930
1114
        """True if this entry contains a file with given id.
931
1115
 
932
1116
        >>> inv = Inventory()
933
1117
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
934
 
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
 
1118
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
935
1119
        >>> '123' in inv
936
1120
        True
937
1121
        >>> '456' in inv
938
1122
        False
939
1123
        """
940
 
        return file_id in self._byid
941
 
 
 
1124
        file_id = osutils.safe_file_id(file_id)
 
1125
        return (file_id in self._byid)
942
1126
 
943
1127
    def __getitem__(self, file_id):
944
1128
        """Return the entry for given file_id.
945
1129
 
946
1130
        >>> inv = Inventory()
947
1131
        >>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
948
 
        InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT')
 
1132
        InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
949
1133
        >>> inv['123123'].name
950
1134
        'hello.c'
951
1135
        """
 
1136
        file_id = osutils.safe_file_id(file_id)
952
1137
        try:
953
1138
            return self._byid[file_id]
954
1139
        except KeyError:
955
 
            if file_id == None:
956
 
                raise BzrError("can't look up file_id None")
957
 
            else:
958
 
                raise BzrError("file_id {%s} not in inventory" % file_id)
959
 
 
 
1140
            # really we're passing an inventory, not a tree...
 
1141
            raise errors.NoSuchId(self, file_id)
960
1142
 
961
1143
    def get_file_kind(self, file_id):
 
1144
        file_id = osutils.safe_file_id(file_id)
962
1145
        return self._byid[file_id].kind
963
1146
 
964
1147
    def get_child(self, parent_id, filename):
 
1148
        parent_id = osutils.safe_file_id(parent_id)
965
1149
        return self[parent_id].children.get(filename)
966
1150
 
 
1151
    def _add_child(self, entry):
 
1152
        """Add an entry to the inventory, without adding it to its parent"""
 
1153
        if entry.file_id in self._byid:
 
1154
            raise BzrError("inventory already contains entry with id {%s}" %
 
1155
                           entry.file_id)
 
1156
        self._byid[entry.file_id] = entry
 
1157
        for child in getattr(entry, 'children', {}).itervalues():
 
1158
            self._add_child(child)
 
1159
        return entry
967
1160
 
968
1161
    def add(self, entry):
969
1162
        """Add entry to inventory.
974
1167
        Returns the new entry object.
975
1168
        """
976
1169
        if entry.file_id in self._byid:
977
 
            raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
978
 
 
979
 
        if entry.parent_id == ROOT_ID or entry.parent_id is None:
980
 
            entry.parent_id = self.root.file_id
981
 
 
982
 
        try:
983
 
            parent = self._byid[entry.parent_id]
984
 
        except KeyError:
985
 
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
986
 
 
987
 
        if parent.children.has_key(entry.name):
988
 
            raise BzrError("%s is already versioned" %
989
 
                    pathjoin(self.id2path(parent.file_id), entry.name))
990
 
 
991
 
        self._byid[entry.file_id] = entry
992
 
        parent.children[entry.name] = entry
993
 
        return entry
994
 
 
 
1170
            raise errors.DuplicateFileId(entry.file_id,
 
1171
                                         self._byid[entry.file_id])
 
1172
 
 
1173
        if entry.parent_id is None:
 
1174
            assert self.root is None and len(self._byid) == 0
 
1175
            self.root = entry
 
1176
        else:
 
1177
            try:
 
1178
                parent = self._byid[entry.parent_id]
 
1179
            except KeyError:
 
1180
                raise BzrError("parent_id {%s} not in inventory" %
 
1181
                               entry.parent_id)
 
1182
 
 
1183
            if entry.name in parent.children:
 
1184
                raise BzrError("%s is already versioned" %
 
1185
                        osutils.pathjoin(self.id2path(parent.file_id),
 
1186
                        entry.name).encode('utf-8'))
 
1187
            parent.children[entry.name] = entry
 
1188
        return self._add_child(entry)
995
1189
 
996
1190
    def add_path(self, relpath, kind, file_id=None, parent_id=None):
997
1191
        """Add entry from a path.
1000
1194
 
1001
1195
        Returns the new entry object."""
1002
1196
        
1003
 
        parts = bzrlib.osutils.splitpath(relpath)
 
1197
        parts = osutils.splitpath(relpath)
1004
1198
 
1005
1199
        if len(parts) == 0:
1006
1200
            if file_id is None:
1007
 
                file_id = bzrlib.workingtree.gen_root_id()
1008
 
            self.root = RootEntry(file_id)
 
1201
                file_id = generate_ids.gen_root_id()
 
1202
            else:
 
1203
                file_id = osutils.safe_file_id(file_id)
 
1204
            self.root = InventoryDirectory(file_id, '', None)
1009
1205
            self._byid = {self.root.file_id: self.root}
1010
 
            return
 
1206
            return self.root
1011
1207
        else:
1012
1208
            parent_path = parts[:-1]
1013
1209
            parent_id = self.path2id(parent_path)
1014
 
            if parent_id == None:
1015
 
                raise NotVersionedError(path=parent_path)
 
1210
            if parent_id is None:
 
1211
                raise errors.NotVersionedError(path=parent_path)
1016
1212
        ie = make_entry(kind, parts[-1], parent_id, file_id)
1017
1213
        return self.add(ie)
1018
1214
 
1021
1217
 
1022
1218
        >>> inv = Inventory()
1023
1219
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1024
 
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
 
1220
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
1025
1221
        >>> '123' in inv
1026
1222
        True
1027
1223
        >>> del inv['123']
1028
1224
        >>> '123' in inv
1029
1225
        False
1030
1226
        """
 
1227
        file_id = osutils.safe_file_id(file_id)
1031
1228
        ie = self[file_id]
1032
1229
 
1033
1230
        assert ie.parent_id is None or \
1037
1234
        if ie.parent_id is not None:
1038
1235
            del self[ie.parent_id].children[ie.name]
1039
1236
 
1040
 
 
1041
1237
    def __eq__(self, other):
1042
1238
        """Compare two sets by comparing their contents.
1043
1239
 
1046
1242
        >>> i1 == i2
1047
1243
        True
1048
1244
        >>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
1049
 
        InventoryFile('123', 'foo', parent_id='TREE_ROOT')
 
1245
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
1050
1246
        >>> i1 == i2
1051
1247
        False
1052
1248
        >>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
1053
 
        InventoryFile('123', 'foo', parent_id='TREE_ROOT')
 
1249
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
1054
1250
        >>> i1 == i2
1055
1251
        True
1056
1252
        """
1057
1253
        if not isinstance(other, Inventory):
1058
1254
            return NotImplemented
1059
1255
 
1060
 
        if len(self._byid) != len(other._byid):
1061
 
            # shortcut: obviously not the same
1062
 
            return False
1063
 
 
1064
1256
        return self._byid == other._byid
1065
1257
 
1066
 
 
1067
1258
    def __ne__(self, other):
1068
1259
        return not self.__eq__(other)
1069
1260
 
1070
 
 
1071
1261
    def __hash__(self):
1072
1262
        raise ValueError('not hashable')
1073
1263
 
1074
1264
    def _iter_file_id_parents(self, file_id):
1075
1265
        """Yield the parents of file_id up to the root."""
1076
 
        while file_id != None:
 
1266
        file_id = osutils.safe_file_id(file_id)
 
1267
        while file_id is not None:
1077
1268
            try:
1078
1269
                ie = self._byid[file_id]
1079
1270
            except KeyError:
1080
 
                raise BzrError("file_id {%s} not found in inventory" % file_id)
 
1271
                raise errors.NoSuchId(tree=None, file_id=file_id)
1081
1272
            yield ie
1082
1273
            file_id = ie.parent_id
1083
1274
 
1089
1280
        is equal to the depth of the file in the tree, counting the
1090
1281
        root directory as depth 1.
1091
1282
        """
 
1283
        file_id = osutils.safe_file_id(file_id)
1092
1284
        p = []
1093
1285
        for parent in self._iter_file_id_parents(file_id):
1094
1286
            p.insert(0, parent.file_id)
1103
1295
        >>> print i.id2path('foo-id')
1104
1296
        src/foo.c
1105
1297
        """
 
1298
        file_id = osutils.safe_file_id(file_id)
1106
1299
        # get all names, skipping root
1107
1300
        return '/'.join(reversed(
1108
1301
            [parent.name for parent in 
1119
1312
 
1120
1313
        Returns None IFF the path is not found.
1121
1314
        """
1122
 
        if isinstance(name, types.StringTypes):
1123
 
            name = splitpath(name)
 
1315
        if isinstance(name, basestring):
 
1316
            name = osutils.splitpath(name)
1124
1317
 
1125
1318
        # mutter("lookup path %r" % name)
1126
1319
 
1127
1320
        parent = self.root
 
1321
        if parent is None:
 
1322
            return None
1128
1323
        for f in name:
1129
1324
            try:
1130
 
                cie = parent.children[f]
 
1325
                children = getattr(parent, 'children', None)
 
1326
                if children is None:
 
1327
                    return None
 
1328
                cie = children[f]
1131
1329
                assert cie.name == f
1132
1330
                assert cie.parent_id == parent.file_id
1133
1331
                parent = cie
1137
1335
 
1138
1336
        return parent.file_id
1139
1337
 
1140
 
 
1141
1338
    def has_filename(self, names):
1142
1339
        return bool(self.path2id(names))
1143
1340
 
1144
 
 
1145
1341
    def has_id(self, file_id):
1146
 
        return self._byid.has_key(file_id)
 
1342
        file_id = osutils.safe_file_id(file_id)
 
1343
        return (file_id in self._byid)
1147
1344
 
 
1345
    def remove_recursive_id(self, file_id):
 
1346
        """Remove file_id, and children, from the inventory.
 
1347
        
 
1348
        :param file_id: A file_id to remove.
 
1349
        """
 
1350
        file_id = osutils.safe_file_id(file_id)
 
1351
        to_find_delete = [self._byid[file_id]]
 
1352
        to_delete = []
 
1353
        while to_find_delete:
 
1354
            ie = to_find_delete.pop()
 
1355
            to_delete.append(ie.file_id)
 
1356
            if ie.kind == 'directory':
 
1357
                to_find_delete.extend(ie.children.values())
 
1358
        for file_id in reversed(to_delete):
 
1359
            ie = self[file_id]
 
1360
            del self._byid[file_id]
 
1361
        if ie.parent_id is not None:
 
1362
            del self[ie.parent_id].children[ie.name]
 
1363
        else:
 
1364
            self.root = None
1148
1365
 
1149
1366
    def rename(self, file_id, new_parent_id, new_name):
1150
1367
        """Move a file within the inventory.
1151
1368
 
1152
1369
        This can change either the name, or the parent, or both.
1153
1370
 
1154
 
        This does not move the working file."""
 
1371
        This does not move the working file.
 
1372
        """
 
1373
        file_id = osutils.safe_file_id(file_id)
1155
1374
        if not is_valid_name(new_name):
1156
1375
            raise BzrError("not an acceptable filename: %r" % new_name)
1157
1376
 
1175
1394
        file_ie.name = new_name
1176
1395
        file_ie.parent_id = new_parent_id
1177
1396
 
 
1397
    def is_root(self, file_id):
 
1398
        file_id = osutils.safe_file_id(file_id)
 
1399
        return self.root is not None and file_id == self.root.file_id
 
1400
 
 
1401
 
 
1402
entry_factory = {
 
1403
    'directory': InventoryDirectory,
 
1404
    'file': InventoryFile,
 
1405
    'symlink': InventoryLink,
 
1406
    'tree-reference': TreeReference
 
1407
}
1178
1408
 
1179
1409
def make_entry(kind, name, parent_id, file_id=None):
1180
1410
    """Create an inventory entry.
1185
1415
    :param file_id: the file_id to use. if None, one will be created.
1186
1416
    """
1187
1417
    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)
 
1418
        file_id = generate_ids.gen_file_id(name)
1195
1419
    else:
 
1420
        file_id = osutils.safe_file_id(file_id)
 
1421
 
 
1422
    #------- This has been copied to bzrlib.dirstate.DirState.add, please
 
1423
    # keep them synchronised.
 
1424
    # we dont import normalized_filename directly because we want to be
 
1425
    # able to change the implementation at runtime for tests.
 
1426
    norm_name, can_access = osutils.normalized_filename(name)
 
1427
    if norm_name != name:
 
1428
        if can_access:
 
1429
            name = norm_name
 
1430
        else:
 
1431
            # TODO: jam 20060701 This would probably be more useful
 
1432
            #       if the error was raised with the full path
 
1433
            raise errors.InvalidNormalization(name)
 
1434
 
 
1435
    try:
 
1436
        factory = entry_factory[kind]
 
1437
    except KeyError:
1196
1438
        raise BzrError("unknown kind %r" % kind)
1197
 
 
 
1439
    return factory(file_id, name, parent_id)
1198
1440
 
1199
1441
 
1200
1442
_NAME_RE = None
1201
1443
 
1202
1444
def is_valid_name(name):
1203
1445
    global _NAME_RE
1204
 
    if _NAME_RE == None:
 
1446
    if _NAME_RE is None:
1205
1447
        _NAME_RE = re.compile(r'^[^/\\]+$')
1206
1448
        
1207
1449
    return bool(_NAME_RE.match(name))