~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

Refactor status display code.

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
28
28
ROOT_ID = "TREE_ROOT"
29
29
 
30
30
 
31
 
import collections
32
31
import os.path
33
32
import re
34
33
import sys
35
34
import tarfile
36
35
import types
37
 
from warnings import warn
38
36
 
39
37
import bzrlib
40
 
from bzrlib import errors, osutils
41
38
from bzrlib.osutils import (pumpfile, quotefn, splitpath, joinpath,
42
 
                            pathjoin, sha_strings)
 
39
                            appendpath, sha_strings)
 
40
from bzrlib.trace import mutter
43
41
from bzrlib.errors import (NotVersionedError, InvalidEntryName,
44
 
                           BzrError, BzrCheckError, BinaryFile)
45
 
from bzrlib.trace import mutter
 
42
                           BzrError, BzrCheckError)
46
43
 
47
44
 
48
45
class InventoryEntry(object):
79
76
    >>> i.path2id('')
80
77
    'TREE_ROOT'
81
78
    >>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
82
 
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
 
79
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
83
80
    >>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
84
 
    InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
85
 
    >>> shouldbe = {0: '', 1: 'src', 2: pathjoin('src','hello.c')}
 
81
    InventoryFile('2323', 'hello.c', parent_id='123')
 
82
    >>> shouldbe = {0: 'src', 1: os.path.join('src','hello.c')}
86
83
    >>> for ix, j in enumerate(i.iter_entries()):
87
84
    ...   print (j[0] == shouldbe[ix], j[1])
88
85
    ... 
89
 
    (True, InventoryDirectory('TREE_ROOT', '', parent_id=None, revision=None))
90
 
    (True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
91
 
    (True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
 
86
    (True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
 
87
    (True, InventoryFile('2323', 'hello.c', parent_id='123'))
92
88
    >>> i.add(InventoryFile('2323', 'bye.c', '123'))
93
89
    Traceback (most recent call last):
94
90
    ...
95
91
    BzrError: inventory already contains entry with id {2323}
96
92
    >>> i.add(InventoryFile('2324', 'bye.c', '123'))
97
 
    InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
 
93
    InventoryFile('2324', 'bye.c', parent_id='123')
98
94
    >>> i.add(InventoryDirectory('2325', 'wibble', '123'))
99
 
    InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
 
95
    InventoryDirectory('2325', 'wibble', parent_id='123')
100
96
    >>> i.path2id('src/wibble')
101
97
    '2325'
102
98
    >>> '2325' in i
103
99
    True
104
100
    >>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
105
 
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
 
101
    InventoryFile('2326', 'wibble.c', parent_id='2325')
106
102
    >>> i['2326']
107
 
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
 
103
    InventoryFile('2326', 'wibble.c', parent_id='2325')
108
104
    >>> for path, entry in i.iter_entries():
109
 
    ...     print path
 
105
    ...     print path.replace('\\\\', '/')     # for win32 os.sep
110
106
    ...     assert i.path2id(path)
111
107
    ... 
112
 
    <BLANKLINE>
113
108
    src
114
109
    src/bye.c
115
110
    src/hello.c
116
111
    src/wibble
117
112
    src/wibble/wibble.c
118
 
    >>> i.id2path('2326')
 
113
    >>> i.id2path('2326').replace('\\\\', '/')
119
114
    'src/wibble/wibble.c'
120
115
    """
121
 
 
122
 
    # Constants returned by describe_change()
123
 
    #
124
 
    # TODO: These should probably move to some kind of FileChangeDescription 
125
 
    # class; that's like what's inside a TreeDelta but we want to be able to 
126
 
    # generate them just for one file at a time.
127
 
    RENAMED = 'renamed'
128
 
    MODIFIED_AND_RENAMED = 'modified and renamed'
129
116
    
130
 
    __slots__ = []
 
117
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
118
                 'text_id', 'parent_id', 'children', 'executable', 
 
119
                 'revision']
 
120
 
 
121
    def _add_text_to_weave(self, new_lines, parents, weave_store, transaction):
 
122
        weave_store.add_text(self.file_id, self.revision, new_lines, parents,
 
123
                             transaction)
131
124
 
132
125
    def detect_changes(self, old_entry):
133
126
        """Return a (text_modified, meta_modified) from this to old_entry.
158
151
             output_to, reverse=False):
159
152
        """Perform a diff between two entries of the same kind."""
160
153
 
161
 
    def find_previous_heads(self, previous_inventories,
162
 
                            versioned_file_store,
163
 
                            transaction,
164
 
                            entry_vf=None):
165
 
        """Return the revisions and entries that directly precede this.
 
154
    def find_previous_heads(self, previous_inventories, entry_weave):
 
155
        """Return the revisions and entries that directly preceed this.
166
156
 
167
157
        Returned as a map from revision to inventory entry.
168
158
 
169
159
        This is a map containing the file revisions in all parents
170
160
        for which the file exists, and its revision is not a parent of
171
161
        any other. If the file is new, the set will be empty.
172
 
 
173
 
        :param versioned_file_store: A store where ancestry data on this
174
 
                                     file id can be queried.
175
 
        :param transaction: The transaction that queries to the versioned 
176
 
                            file store should be completed under.
177
 
        :param entry_vf: The entry versioned file, if its already available.
178
162
        """
179
163
        def get_ancestors(weave, entry):
180
 
            return set(weave.get_ancestry(entry.revision))
181
 
        # revision:ie mapping for each ie found in previous_inventories.
182
 
        candidates = {}
183
 
        # revision:ie mapping with one revision for each head.
 
164
            return set(map(weave.idx_to_name,
 
165
                           weave.inclusions([weave.lookup(entry.revision)])))
184
166
        heads = {}
185
 
        # revision: ancestor list for each head
186
167
        head_ancestors = {}
187
 
        # identify candidate head revision ids.
188
168
        for inv in previous_inventories:
189
169
            if self.file_id in inv:
190
170
                ie = inv[self.file_id]
191
171
                assert ie.file_id == self.file_id
192
 
                if ie.revision in candidates:
193
 
                    # same revision value in two different inventories:
194
 
                    # correct possible inconsistencies:
195
 
                    #     * there was a bug in revision updates with 'x' bit 
196
 
                    #       support.
 
172
                if ie.revision in heads:
 
173
                    # fixup logic, there was a bug in revision updates.
 
174
                    # with x bit support.
197
175
                    try:
198
 
                        if candidates[ie.revision].executable != ie.executable:
199
 
                            candidates[ie.revision].executable = False
 
176
                        if heads[ie.revision].executable != ie.executable:
 
177
                            heads[ie.revision].executable = False
200
178
                            ie.executable = False
201
179
                    except AttributeError:
202
180
                        pass
203
 
                    # must now be the same.
204
 
                    assert candidates[ie.revision] == ie
 
181
                    assert heads[ie.revision] == ie
205
182
                else:
206
 
                    # add this revision as a candidate.
207
 
                    candidates[ie.revision] = ie
208
 
 
209
 
        # common case optimisation
210
 
        if len(candidates) == 1:
211
 
            # if there is only one candidate revision found
212
 
            # then we can opening the versioned file to access ancestry:
213
 
            # there cannot be any ancestors to eliminate when there is 
214
 
            # only one revision available.
215
 
            heads[ie.revision] = ie
216
 
            return heads
217
 
 
218
 
        # eliminate ancestors amongst the available candidates:
219
 
        # heads are those that are not an ancestor of any other candidate
220
 
        # - this provides convergence at a per-file level.
221
 
        for ie in candidates.values():
222
 
            # may be an ancestor of a known head:
223
 
            already_present = 0 != len(
224
 
                [head for head in heads 
225
 
                 if ie.revision in head_ancestors[head]])
226
 
            if already_present:
227
 
                # an ancestor of an analyzed candidate.
228
 
                continue
229
 
            # not an ancestor of a known head:
230
 
            # load the versioned file for this file id if needed
231
 
            if entry_vf is None:
232
 
                entry_vf = versioned_file_store.get_weave_or_empty(
233
 
                    self.file_id, transaction)
234
 
            ancestors = get_ancestors(entry_vf, ie)
235
 
            # may knock something else out:
236
 
            check_heads = list(heads.keys())
237
 
            for head in check_heads:
238
 
                if head in ancestors:
239
 
                    # this previously discovered 'head' is not
240
 
                    # really a head - its an ancestor of the newly 
241
 
                    # found head,
242
 
                    heads.pop(head)
243
 
            head_ancestors[ie.revision] = ancestors
244
 
            heads[ie.revision] = ie
 
183
                    # may want to add it.
 
184
                    # may already be covered:
 
185
                    already_present = 0 != len(
 
186
                        [head for head in heads 
 
187
                         if ie.revision in head_ancestors[head]])
 
188
                    if already_present:
 
189
                        # an ancestor of a known head.
 
190
                        continue
 
191
                    # definately a head:
 
192
                    ancestors = get_ancestors(entry_weave, ie)
 
193
                    # may knock something else out:
 
194
                    check_heads = list(heads.keys())
 
195
                    for head in check_heads:
 
196
                        if head in ancestors:
 
197
                            # this head is not really a head
 
198
                            heads.pop(head)
 
199
                    head_ancestors[ie.revision] = ancestors
 
200
                    heads[ie.revision] = ie
245
201
        return heads
246
202
 
247
203
    def get_tar_item(self, root, dp, now, tree):
248
204
        """Get a tarfile item and a file stream for its content."""
249
 
        item = tarfile.TarInfo(pathjoin(root, dp).encode('utf8'))
 
205
        item = tarfile.TarInfo(os.path.join(root, dp))
250
206
        # TODO: would be cool to actually set it to the timestamp of the
251
207
        # revision it was last changed
252
208
        item.mtime = now
296
252
        """Return a short kind indicator useful for appending to names."""
297
253
        raise BzrError('unknown kind %r' % self.kind)
298
254
 
299
 
    known_kinds = ('file', 'directory', 'symlink')
 
255
    known_kinds = ('file', 'directory', 'symlink', 'root_directory')
300
256
 
301
257
    def _put_in_tar(self, item, tree):
302
258
        """populate item for stashing in a tar, and return the content stream.
311
267
        
312
268
        This is a template method - implement _put_on_disk in subclasses.
313
269
        """
314
 
        fullpath = pathjoin(dest, dp)
 
270
        fullpath = appendpath(dest, dp)
315
271
        self._put_on_disk(fullpath, tree)
316
 
        # mutter("  export {%s} kind %s to %s", self.file_id,
317
 
        #         self.kind, fullpath)
 
272
        mutter("  export {%s} kind %s to %s", self.file_id,
 
273
                self.kind, fullpath)
318
274
 
319
275
    def _put_on_disk(self, fullpath, tree):
320
276
        """Put this entry onto disk at fullpath, from tree tree."""
321
277
        raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
322
278
 
323
279
    def sorted_children(self):
324
 
        return sorted(self.children.items())
 
280
        l = self.children.items()
 
281
        l.sort()
 
282
        return l
325
283
 
326
284
    @staticmethod
327
285
    def versionable_kind(kind):
328
 
        return (kind in ('file', 'directory', 'symlink'))
 
286
        return kind in ('file', 'directory', 'symlink')
329
287
 
330
288
    def check(self, checker, rev_id, inv, tree):
331
289
        """Check this inventory entry is intact.
332
290
 
333
291
        This is a template method, override _check for kind specific
334
292
        tests.
335
 
 
336
 
        :param checker: Check object providing context for the checks; 
337
 
             can be used to find out what parts of the repository have already
338
 
             been checked.
339
 
        :param rev_id: Revision id from which this InventoryEntry was loaded.
340
 
             Not necessarily the last-changed revision for this file.
341
 
        :param inv: Inventory from which the entry was loaded.
342
 
        :param tree: RevisionTree for this entry.
343
293
        """
344
 
        if self.parent_id is not None:
 
294
        if self.parent_id != None:
345
295
            if not inv.has_id(self.parent_id):
346
296
                raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
347
297
                        % (self.parent_id, rev_id))
352
302
        raise BzrCheckError('unknown entry kind %r in revision {%s}' % 
353
303
                            (self.kind, rev_id))
354
304
 
 
305
 
355
306
    def copy(self):
356
307
        """Clone this inventory entry."""
357
308
        raise NotImplementedError
358
309
 
359
 
    @staticmethod
360
 
    def describe_change(old_entry, new_entry):
361
 
        """Describe the change between old_entry and this.
362
 
        
363
 
        This smells of being an InterInventoryEntry situation, but as its
364
 
        the first one, we're making it a static method for now.
365
 
 
366
 
        An entry with a different parent, or different name is considered 
367
 
        to be renamed. Reparenting is an internal detail.
368
 
        Note that renaming the parent does not trigger a rename for the
369
 
        child entry itself.
370
 
        """
371
 
        # TODO: Perhaps return an object rather than just a string
372
 
        if old_entry is new_entry:
373
 
            # also the case of both being None
374
 
            return 'unchanged'
375
 
        elif old_entry is None:
 
310
    def _get_snapshot_change(self, previous_entries):
 
311
        if len(previous_entries) > 1:
 
312
            return 'merged'
 
313
        elif len(previous_entries) == 0:
376
314
            return 'added'
377
 
        elif new_entry is None:
378
 
            return 'removed'
379
 
        text_modified, meta_modified = new_entry.detect_changes(old_entry)
380
 
        if text_modified or meta_modified:
381
 
            modified = True
382
 
        else:
383
 
            modified = False
384
 
        # TODO 20060511 (mbp, rbc) factor out 'detect_rename' here.
385
 
        if old_entry.parent_id != new_entry.parent_id:
386
 
            renamed = True
387
 
        elif old_entry.name != new_entry.name:
388
 
            renamed = True
389
 
        else:
390
 
            renamed = False
391
 
        if renamed and not modified:
392
 
            return InventoryEntry.RENAMED
393
 
        if modified and not renamed:
394
 
            return 'modified'
395
 
        if modified and renamed:
396
 
            return InventoryEntry.MODIFIED_AND_RENAMED
397
 
        return 'unchanged'
 
315
        else:
 
316
            return 'modified/renamed/reparented'
398
317
 
399
318
    def __repr__(self):
400
 
        return ("%s(%r, %r, parent_id=%r, revision=%r)"
 
319
        return ("%s(%r, %r, parent_id=%r)"
401
320
                % (self.__class__.__name__,
402
321
                   self.file_id,
403
322
                   self.name,
404
 
                   self.parent_id,
405
 
                   self.revision))
 
323
                   self.parent_id))
406
324
 
407
325
    def snapshot(self, revision, path, previous_entries,
408
 
                 work_tree, commit_builder):
 
326
                 work_tree, weave_store, transaction):
409
327
        """Make a snapshot of this entry which may or may not have changed.
410
328
        
411
329
        This means that all its fields are populated, that it has its
412
330
        text stored in the text store or weave.
413
331
        """
414
 
        # mutter('new parents of %s are %r', path, previous_entries)
 
332
        mutter('new parents of %s are %r', path, previous_entries)
415
333
        self._read_tree_state(path, work_tree)
416
 
        # TODO: Where should we determine whether to reuse a
417
 
        # previous revision id or create a new revision? 20060606
418
334
        if len(previous_entries) == 1:
419
335
            # cannot be unchanged unless there is only one parent file rev.
420
336
            parent_ie = previous_entries.values()[0]
421
337
            if self._unchanged(parent_ie):
422
 
                # mutter("found unchanged entry")
 
338
                mutter("found unchanged entry")
423
339
                self.revision = parent_ie.revision
424
340
                return "unchanged"
425
 
        return self._snapshot_into_revision(revision, previous_entries, 
426
 
                                            work_tree, commit_builder)
427
 
 
428
 
    def _snapshot_into_revision(self, revision, previous_entries, work_tree,
429
 
                                commit_builder):
430
 
        """Record this revision unconditionally into a store.
431
 
 
432
 
        The entry's last-changed revision property (`revision`) is updated to 
433
 
        that of the new revision.
434
 
        
435
 
        :param revision: id of the new revision that is being recorded.
436
 
 
437
 
        :returns: String description of the commit (e.g. "merged", "modified"), etc.
438
 
        """
439
 
        # mutter('new revision {%s} for {%s}', revision, self.file_id)
 
341
        return self.snapshot_revision(revision, previous_entries, 
 
342
                                      work_tree, weave_store, transaction)
 
343
 
 
344
    def snapshot_revision(self, revision, previous_entries, work_tree,
 
345
                          weave_store, transaction):
 
346
        """Record this revision unconditionally."""
 
347
        mutter('new revision for {%s}', self.file_id)
440
348
        self.revision = revision
441
 
        self._snapshot_text(previous_entries, work_tree, commit_builder)
 
349
        change = self._get_snapshot_change(previous_entries)
 
350
        self._snapshot_text(previous_entries, work_tree, weave_store,
 
351
                            transaction)
 
352
        return change
442
353
 
443
 
    def _snapshot_text(self, file_parents, work_tree, commit_builder): 
 
354
    def _snapshot_text(self, file_parents, work_tree, weave_store, transaction): 
444
355
        """Record the 'text' of this entry, whatever form that takes.
445
356
        
446
357
        This default implementation simply adds an empty text.
447
358
        """
448
 
        raise NotImplementedError(self._snapshot_text)
 
359
        mutter('storing file {%s} in revision {%s}',
 
360
               self.file_id, self.revision)
 
361
        self._add_text_to_weave([], file_parents, weave_store, transaction)
449
362
 
450
363
    def __eq__(self, other):
451
364
        if not isinstance(other, InventoryEntry):
472
385
    def _unchanged(self, previous_ie):
473
386
        """Has this entry changed relative to previous_ie.
474
387
 
475
 
        This method should be overridden in child classes.
 
388
        This method should be overriden in child classes.
476
389
        """
477
390
        compatible = True
478
391
        # different inv parent
494
407
        # first requested, or preload them if they're already known
495
408
        pass            # nothing to do by default
496
409
 
497
 
    def _forget_tree_state(self):
498
 
        pass
499
 
 
500
410
 
501
411
class RootEntry(InventoryEntry):
502
412
 
503
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
504
 
                 'text_id', 'parent_id', 'children', 'executable', 
505
 
                 'revision', 'symlink_target']
506
 
 
507
413
    def _check(self, checker, rev_id, tree):
508
414
        """See InventoryEntry._check"""
509
415
 
510
416
    def __init__(self, file_id):
511
417
        self.file_id = file_id
512
418
        self.children = {}
513
 
        self.kind = 'directory'
 
419
        self.kind = 'root_directory'
514
420
        self.parent_id = None
515
421
        self.name = u''
516
 
        self.revision = None
517
 
        warn('RootEntry is deprecated as of bzr 0.10.  Please use '
518
 
             'InventoryDirectory instead.',
519
 
            DeprecationWarning, stacklevel=2)
520
422
 
521
423
    def __eq__(self, other):
522
424
        if not isinstance(other, RootEntry):
529
431
class InventoryDirectory(InventoryEntry):
530
432
    """A directory in an inventory."""
531
433
 
532
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
533
 
                 'text_id', 'parent_id', 'children', 'executable', 
534
 
                 'revision', 'symlink_target']
535
 
 
536
434
    def _check(self, checker, rev_id, tree):
537
435
        """See InventoryEntry._check"""
538
 
        if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
 
436
        if self.text_sha1 != None or self.text_size != None or self.text_id != None:
539
437
            raise BzrCheckError('directory {%s} has text in revision {%s}'
540
438
                                % (self.file_id, rev_id))
541
439
 
568
466
        """See InventoryEntry._put_on_disk."""
569
467
        os.mkdir(fullpath)
570
468
 
571
 
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
572
 
        """See InventoryEntry._snapshot_text."""
573
 
        commit_builder.modified_directory(self.file_id, file_parents)
574
 
 
575
469
 
576
470
class InventoryFile(InventoryEntry):
577
471
    """A file in an inventory."""
578
472
 
579
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
580
 
                 'text_id', 'parent_id', 'children', 'executable', 
581
 
                 'revision', 'symlink_target']
582
 
 
583
 
    def _check(self, checker, tree_revision_id, tree):
 
473
    def _check(self, checker, rev_id, tree):
584
474
        """See InventoryEntry._check"""
585
 
        t = (self.file_id, self.revision)
 
475
        revision = self.revision
 
476
        t = (self.file_id, revision)
586
477
        if t in checker.checked_texts:
587
 
            prev_sha = checker.checked_texts[t]
 
478
            prev_sha = checker.checked_texts[t] 
588
479
            if prev_sha != self.text_sha1:
589
480
                raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
590
 
                                    (self.file_id, tree_revision_id))
 
481
                                    (self.file_id, rev_id))
591
482
            else:
592
483
                checker.repeated_text_cnt += 1
593
484
                return
594
 
 
595
 
        if self.file_id not in checker.checked_weaves:
596
 
            mutter('check weave {%s}', self.file_id)
597
 
            w = tree.get_weave(self.file_id)
598
 
            # Not passing a progress bar, because it creates a new
599
 
            # progress, which overwrites the current progress,
600
 
            # and doesn't look nice
601
 
            w.check()
602
 
            checker.checked_weaves[self.file_id] = True
603
 
        else:
604
 
            w = tree.get_weave(self.file_id)
605
 
 
606
 
        mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
607
 
        checker.checked_text_cnt += 1
608
 
        # We can't check the length, because Weave doesn't store that
609
 
        # information, and the whole point of looking at the weave's
610
 
        # sha1sum is that we don't have to extract the text.
611
 
        if self.text_sha1 != w.get_sha1(self.revision):
612
 
            raise BzrCheckError('text {%s} version {%s} wrong sha1' 
613
 
                                % (self.file_id, self.revision))
 
485
        mutter('check version {%s} of {%s}', rev_id, self.file_id)
 
486
        file_lines = tree.get_file_lines(self.file_id)
 
487
        checker.checked_text_cnt += 1 
 
488
        if self.text_size != sum(map(len, file_lines)):
 
489
            raise BzrCheckError('text {%s} wrong size' % self.text_id)
 
490
        if self.text_sha1 != sha_strings(file_lines):
 
491
            raise BzrCheckError('text {%s} wrong sha1' % self.text_id)
614
492
        checker.checked_texts[t] = self.text_sha1
615
493
 
616
494
    def copy(self):
624
502
 
625
503
    def detect_changes(self, old_entry):
626
504
        """See InventoryEntry.detect_changes."""
627
 
        assert self.text_sha1 is not None
628
 
        assert old_entry.text_sha1 is not None
 
505
        assert self.text_sha1 != None
 
506
        assert old_entry.text_sha1 != None
629
507
        text_modified = (self.text_sha1 != old_entry.text_sha1)
630
508
        meta_modified = (self.executable != old_entry.executable)
631
509
        return text_modified, meta_modified
633
511
    def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
634
512
             output_to, reverse=False):
635
513
        """See InventoryEntry._diff."""
636
 
        try:
637
 
            from_text = tree.get_file(self.file_id).readlines()
638
 
            if to_entry:
639
 
                to_text = to_tree.get_file(to_entry.file_id).readlines()
640
 
            else:
641
 
                to_text = []
642
 
            if not reverse:
643
 
                text_diff(from_label, from_text,
644
 
                          to_label, to_text, output_to)
645
 
            else:
646
 
                text_diff(to_label, to_text,
647
 
                          from_label, from_text, output_to)
648
 
        except BinaryFile:
649
 
            if reverse:
650
 
                label_pair = (to_label, from_label)
651
 
            else:
652
 
                label_pair = (from_label, to_label)
653
 
            print >> output_to, "Binary files %s and %s differ" % label_pair
 
514
        from_text = tree.get_file(self.file_id).readlines()
 
515
        if to_entry:
 
516
            to_text = to_tree.get_file(to_entry.file_id).readlines()
 
517
        else:
 
518
            to_text = []
 
519
        if not reverse:
 
520
            text_diff(from_label, from_text,
 
521
                      to_label, to_text, output_to)
 
522
        else:
 
523
            text_diff(to_label, to_text,
 
524
                      from_label, from_text, output_to)
654
525
 
655
526
    def has_text(self):
656
527
        """See InventoryEntry.has_text."""
683
554
 
684
555
    def _read_tree_state(self, path, work_tree):
685
556
        """See InventoryEntry._read_tree_state."""
686
 
        self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
687
 
        # FIXME: 20050930 probe for the text size when getting sha1
688
 
        # in _read_tree_state
689
 
        self.executable = work_tree.is_executable(self.file_id, path=path)
690
 
 
691
 
    def __repr__(self):
692
 
        return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s)"
693
 
                % (self.__class__.__name__,
694
 
                   self.file_id,
695
 
                   self.name,
696
 
                   self.parent_id,
697
 
                   self.text_sha1,
698
 
                   self.text_size))
699
 
 
700
 
    def _forget_tree_state(self):
701
 
        self.text_sha1 = None
702
 
 
703
 
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
 
557
        self.text_sha1 = work_tree.get_file_sha1(self.file_id)
 
558
        self.executable = work_tree.is_executable(self.file_id)
 
559
 
 
560
    def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
704
561
        """See InventoryEntry._snapshot_text."""
705
 
        def get_content_byte_lines():
706
 
            return work_tree.get_file(self.file_id).readlines()
707
 
        self.text_sha1, self.text_size = commit_builder.modified_file_text(
708
 
            self.file_id, file_parents, get_content_byte_lines, self.text_sha1, self.text_size)
 
562
        mutter('storing file {%s} in revision {%s}',
 
563
               self.file_id, self.revision)
 
564
        # special case to avoid diffing on renames or 
 
565
        # reparenting
 
566
        if (len(file_parents) == 1
 
567
            and self.text_sha1 == file_parents.values()[0].text_sha1
 
568
            and self.text_size == file_parents.values()[0].text_size):
 
569
            previous_ie = file_parents.values()[0]
 
570
            weave_store.add_identical_text(
 
571
                self.file_id, previous_ie.revision, 
 
572
                self.revision, file_parents, transaction)
 
573
        else:
 
574
            new_lines = work_tree.get_file(self.file_id).readlines()
 
575
            self._add_text_to_weave(new_lines, file_parents, weave_store,
 
576
                                    transaction)
 
577
            self.text_sha1 = sha_strings(new_lines)
 
578
            self.text_size = sum(map(len, new_lines))
 
579
 
709
580
 
710
581
    def _unchanged(self, previous_ie):
711
582
        """See InventoryEntry._unchanged."""
724
595
class InventoryLink(InventoryEntry):
725
596
    """A file in an inventory."""
726
597
 
727
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
728
 
                 'text_id', 'parent_id', 'children', 'executable', 
729
 
                 'revision', 'symlink_target']
 
598
    __slots__ = ['symlink_target']
730
599
 
731
600
    def _check(self, checker, rev_id, tree):
732
601
        """See InventoryEntry._check"""
733
 
        if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
 
602
        if self.text_sha1 != None or self.text_size != None or self.text_id != None:
734
603
            raise BzrCheckError('symlink {%s} has text in revision {%s}'
735
604
                    % (self.file_id, rev_id))
736
 
        if self.symlink_target is None:
 
605
        if self.symlink_target == None:
737
606
            raise BzrCheckError('symlink {%s} has no target in revision {%s}'
738
607
                    % (self.file_id, rev_id))
739
608
 
797
666
        """See InventoryEntry._read_tree_state."""
798
667
        self.symlink_target = work_tree.get_symlink_target(self.file_id)
799
668
 
800
 
    def _forget_tree_state(self):
801
 
        self.symlink_target = None
802
 
 
803
669
    def _unchanged(self, previous_ie):
804
670
        """See InventoryEntry._unchanged."""
805
671
        compatible = super(InventoryLink, self)._unchanged(previous_ie)
807
673
            compatible = False
808
674
        return compatible
809
675
 
810
 
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
811
 
        """See InventoryEntry._snapshot_text."""
812
 
        commit_builder.modified_link(
813
 
            self.file_id, file_parents, self.symlink_target)
814
 
 
815
676
 
816
677
class Inventory(object):
817
678
    """Inventory of versioned files in a tree.
832
693
 
833
694
    >>> inv = Inventory()
834
695
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
835
 
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
 
696
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT')
836
697
    >>> inv['123-123'].name
837
698
    'hello.c'
838
699
 
846
707
    May also look up by name:
847
708
 
848
709
    >>> [x[0] for x in inv.iter_entries()]
849
 
    ['', u'hello.c']
 
710
    ['hello.c']
850
711
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
851
712
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
852
 
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None)
 
713
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
853
714
    """
854
 
    def __init__(self, root_id=ROOT_ID, revision_id=None):
 
715
    def __init__(self, root_id=ROOT_ID):
855
716
        """Create or read an inventory.
856
717
 
857
718
        If a working directory is specified, the inventory is read
861
722
        The inventory is created with a default root directory, with
862
723
        an id of None.
863
724
        """
864
 
        # We are letting Branch.create() create a unique inventory
 
725
        # We are letting Branch.initialize() create a unique inventory
865
726
        # root id. Rather than generating a random one here.
866
727
        #if root_id is None:
867
728
        #    root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
868
 
        if root_id is not None:
869
 
            self._set_root(InventoryDirectory(root_id, '', None))
870
 
        else:
871
 
            self.root = None
872
 
            self._byid = {}
873
 
        # FIXME: this isn't ever used, changing it to self.revision may break
874
 
        # things. TODO make everything use self.revision_id
875
 
        self.revision_id = revision_id
876
 
 
877
 
    def _set_root(self, ie):
878
 
        self.root = ie
 
729
        self.root = RootEntry(root_id)
879
730
        self._byid = {self.root.file_id: self.root}
880
731
 
 
732
 
881
733
    def copy(self):
882
 
        # TODO: jam 20051218 Should copy also copy the revision_id?
883
 
        entries = self.iter_entries()
884
 
        other = Inventory(entries.next()[1].file_id)
 
734
        other = Inventory(self.root.file_id)
885
735
        # copy recursively so we know directories will be added before
886
736
        # their children.  There are more efficient ways than this...
887
 
        for path, entry in entries():
 
737
        for path, entry in self.iter_entries():
 
738
            if entry == self.root:
 
739
                continue
888
740
            other.add(entry.copy())
889
741
        return other
890
742
 
 
743
 
891
744
    def __iter__(self):
892
745
        return iter(self._byid)
893
746
 
 
747
 
894
748
    def __len__(self):
895
749
        """Returns number of entries."""
896
750
        return len(self._byid)
897
751
 
 
752
 
898
753
    def iter_entries(self, from_dir=None):
899
754
        """Return (path, entry) pairs, in order by name."""
900
 
        if from_dir is None:
901
 
            assert self.root
902
 
            from_dir = self.root
903
 
            yield '', self.root
904
 
        elif isinstance(from_dir, basestring):
905
 
            from_dir = self._byid[from_dir]
906
 
            
907
 
        # unrolling the recursive called changed the time from
908
 
        # 440ms/663ms (inline/total) to 116ms/116ms
909
 
        children = from_dir.children.items()
910
 
        children.sort()
911
 
        children = collections.deque(children)
912
 
        stack = [(u'', children)]
913
 
        while stack:
914
 
            from_dir_relpath, children = stack[-1]
915
 
 
916
 
            while children:
917
 
                name, ie = children.popleft()
918
 
 
919
 
                # we know that from_dir_relpath never ends in a slash
920
 
                # and 'f' doesn't begin with one, we can do a string op, rather
921
 
                # than the checks of pathjoin(), though this means that all paths
922
 
                # start with a slash
923
 
                path = from_dir_relpath + '/' + name
924
 
 
925
 
                yield path[1:], ie
926
 
 
927
 
                if ie.kind != 'directory':
928
 
                    continue
929
 
 
930
 
                # But do this child first
931
 
                new_children = ie.children.items()
932
 
                new_children.sort()
933
 
                new_children = collections.deque(new_children)
934
 
                stack.append((path, new_children))
935
 
                # Break out of inner loop, so that we start outer loop with child
936
 
                break
937
 
            else:
938
 
                # if we finished all children, pop it off the stack
939
 
                stack.pop()
940
 
 
941
 
    def iter_entries_by_dir(self, from_dir=None):
942
 
        """Iterate over the entries in a directory first order.
943
 
 
944
 
        This returns all entries for a directory before returning
945
 
        the entries for children of a directory. This is not
946
 
        lexicographically sorted order, and is a hybrid between
947
 
        depth-first and breadth-first.
948
 
 
949
 
        :return: This yields (path, entry) pairs
950
 
        """
951
 
        # TODO? Perhaps this should return the from_dir so that the root is
952
 
        # yielded? or maybe an option?
953
 
        if from_dir is None:
954
 
            assert self.root
955
 
            from_dir = self.root
956
 
            yield '', self.root
957
 
        elif isinstance(from_dir, basestring):
958
 
            from_dir = self._byid[from_dir]
959
 
            
960
 
        stack = [(u'', from_dir)]
961
 
        while stack:
962
 
            cur_relpath, cur_dir = stack.pop()
963
 
 
964
 
            child_dirs = []
965
 
            for child_name, child_ie in sorted(cur_dir.children.iteritems()):
966
 
 
967
 
                child_relpath = cur_relpath + child_name
968
 
 
969
 
                yield child_relpath, child_ie
970
 
 
971
 
                if child_ie.kind == 'directory':
972
 
                    child_dirs.append((child_relpath+'/', child_ie))
973
 
            stack.extend(reversed(child_dirs))
 
755
        if from_dir == None:
 
756
            assert self.root
 
757
            from_dir = self.root
 
758
        elif isinstance(from_dir, basestring):
 
759
            from_dir = self._byid[from_dir]
 
760
            
 
761
        kids = from_dir.children.items()
 
762
        kids.sort()
 
763
        for name, ie in kids:
 
764
            yield name, ie
 
765
            if ie.kind == 'directory':
 
766
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
 
767
                    yield os.path.join(name, cn), cie
 
768
 
974
769
 
975
770
    def entries(self):
976
771
        """Return list of (path, ie) for all entries except the root.
982
777
            kids = dir_ie.children.items()
983
778
            kids.sort()
984
779
            for name, ie in kids:
985
 
                child_path = pathjoin(dir_path, name)
 
780
                child_path = os.path.join(dir_path, name)
986
781
                accum.append((child_path, ie))
987
782
                if ie.kind == 'directory':
988
783
                    descend(ie, child_path)
990
785
        descend(self.root, u'')
991
786
        return accum
992
787
 
 
788
 
993
789
    def directories(self):
994
790
        """Return (path, entry) pairs for all directories, including the root.
995
791
        """
1001
797
            kids.sort()
1002
798
 
1003
799
            for name, child_ie in kids:
1004
 
                child_path = pathjoin(parent_path, name)
 
800
                child_path = os.path.join(parent_path, name)
1005
801
                descend(child_ie, child_path)
1006
802
        descend(self.root, u'')
1007
803
        return accum
1008
804
        
 
805
 
 
806
 
1009
807
    def __contains__(self, file_id):
1010
808
        """True if this entry contains a file with given id.
1011
809
 
1012
810
        >>> inv = Inventory()
1013
811
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1014
 
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
 
812
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1015
813
        >>> '123' in inv
1016
814
        True
1017
815
        >>> '456' in inv
1018
816
        False
1019
817
        """
1020
 
        return (file_id in self._byid)
 
818
        return file_id in self._byid
 
819
 
1021
820
 
1022
821
    def __getitem__(self, file_id):
1023
822
        """Return the entry for given file_id.
1024
823
 
1025
824
        >>> inv = Inventory()
1026
825
        >>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
1027
 
        InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
 
826
        InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT')
1028
827
        >>> inv['123123'].name
1029
828
        'hello.c'
1030
829
        """
1031
830
        try:
1032
831
            return self._byid[file_id]
1033
832
        except KeyError:
1034
 
            if file_id is None:
 
833
            if file_id == None:
1035
834
                raise BzrError("can't look up file_id None")
1036
835
            else:
1037
836
                raise BzrError("file_id {%s} not in inventory" % file_id)
1038
837
 
 
838
 
1039
839
    def get_file_kind(self, file_id):
1040
840
        return self._byid[file_id].kind
1041
841
 
1042
842
    def get_child(self, parent_id, filename):
1043
843
        return self[parent_id].children.get(filename)
1044
844
 
 
845
 
1045
846
    def add(self, entry):
1046
847
        """Add entry to inventory.
1047
848
 
1053
854
        if entry.file_id in self._byid:
1054
855
            raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
1055
856
 
1056
 
        if entry.parent_id is None:
1057
 
            assert self.root is None and len(self._byid) == 0
1058
 
            self._set_root(entry)
1059
 
            return entry
1060
 
        if entry.parent_id == ROOT_ID:
1061
 
            assert self.root is not None, self
 
857
        if entry.parent_id == ROOT_ID or entry.parent_id is None:
1062
858
            entry.parent_id = self.root.file_id
1063
859
 
1064
860
        try:
1066
862
        except KeyError:
1067
863
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
1068
864
 
1069
 
        if entry.name in parent.children:
 
865
        if parent.children.has_key(entry.name):
1070
866
            raise BzrError("%s is already versioned" %
1071
 
                    pathjoin(self.id2path(parent.file_id), entry.name))
 
867
                    appendpath(self.id2path(parent.file_id), entry.name))
1072
868
 
1073
869
        self._byid[entry.file_id] = entry
1074
870
        parent.children[entry.name] = entry
1075
871
        return entry
1076
872
 
1077
 
    def add_path(self, relpath, kind, file_id=None, parent_id=None):
 
873
 
 
874
    def add_path(self, relpath, kind, file_id=None):
1078
875
        """Add entry from a path.
1079
876
 
1080
877
        The immediate parent must already be versioned.
1081
878
 
1082
879
        Returns the new entry object."""
 
880
        from bzrlib.workingtree import gen_file_id
1083
881
        
1084
 
        parts = osutils.splitpath(relpath)
1085
 
 
 
882
        parts = bzrlib.osutils.splitpath(relpath)
1086
883
        if len(parts) == 0:
1087
 
            if file_id is None:
1088
 
                file_id = bzrlib.workingtree.gen_root_id()
1089
 
            self.root = InventoryDirectory(file_id, '', None)
1090
 
            self._byid = {self.root.file_id: self.root}
1091
 
            return
 
884
            raise BzrError("cannot re-add root of inventory")
 
885
 
 
886
        if file_id == None:
 
887
            file_id = gen_file_id(relpath)
 
888
 
 
889
        parent_path = parts[:-1]
 
890
        parent_id = self.path2id(parent_path)
 
891
        if parent_id == None:
 
892
            raise NotVersionedError(path=parent_path)
 
893
        if kind == 'directory':
 
894
            ie = InventoryDirectory(file_id, parts[-1], parent_id)
 
895
        elif kind == 'file':
 
896
            ie = InventoryFile(file_id, parts[-1], parent_id)
 
897
        elif kind == 'symlink':
 
898
            ie = InventoryLink(file_id, parts[-1], parent_id)
1092
899
        else:
1093
 
            parent_path = parts[:-1]
1094
 
            parent_id = self.path2id(parent_path)
1095
 
            if parent_id is None:
1096
 
                raise NotVersionedError(path=parent_path)
1097
 
        ie = make_entry(kind, parts[-1], parent_id, file_id)
 
900
            raise BzrError("unknown kind %r" % kind)
1098
901
        return self.add(ie)
1099
902
 
 
903
 
1100
904
    def __delitem__(self, file_id):
1101
905
        """Remove entry by id.
1102
906
 
1103
907
        >>> inv = Inventory()
1104
908
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1105
 
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
 
909
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1106
910
        >>> '123' in inv
1107
911
        True
1108
912
        >>> del inv['123']
1111
915
        """
1112
916
        ie = self[file_id]
1113
917
 
1114
 
        assert ie.parent_id is None or \
1115
 
            self[ie.parent_id].children[ie.name] == ie
 
918
        assert self[ie.parent_id].children[ie.name] == ie
1116
919
        
 
920
        # TODO: Test deleting all children; maybe hoist to a separate
 
921
        # deltree method?
 
922
        if ie.kind == 'directory':
 
923
            for cie in ie.children.values():
 
924
                del self[cie.file_id]
 
925
            del ie.children
 
926
 
1117
927
        del self._byid[file_id]
1118
 
        if ie.parent_id is not None:
1119
 
            del self[ie.parent_id].children[ie.name]
 
928
        del self[ie.parent_id].children[ie.name]
 
929
 
1120
930
 
1121
931
    def __eq__(self, other):
1122
932
        """Compare two sets by comparing their contents.
1126
936
        >>> i1 == i2
1127
937
        True
1128
938
        >>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
1129
 
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
 
939
        InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1130
940
        >>> i1 == i2
1131
941
        False
1132
942
        >>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
1133
 
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
 
943
        InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1134
944
        >>> i1 == i2
1135
945
        True
1136
946
        """
1137
947
        if not isinstance(other, Inventory):
1138
948
            return NotImplemented
1139
949
 
 
950
        if len(self._byid) != len(other._byid):
 
951
            # shortcut: obviously not the same
 
952
            return False
 
953
 
1140
954
        return self._byid == other._byid
1141
955
 
 
956
 
1142
957
    def __ne__(self, other):
1143
958
        return not self.__eq__(other)
1144
959
 
 
960
 
1145
961
    def __hash__(self):
1146
962
        raise ValueError('not hashable')
1147
963
 
1148
 
    def _iter_file_id_parents(self, file_id):
1149
 
        """Yield the parents of file_id up to the root."""
1150
 
        while file_id is not None:
1151
 
            try:
1152
 
                ie = self._byid[file_id]
1153
 
            except KeyError:
1154
 
                raise BzrError("file_id {%s} not found in inventory" % file_id)
1155
 
            yield ie
1156
 
            file_id = ie.parent_id
1157
964
 
1158
965
    def get_idpath(self, file_id):
1159
966
        """Return a list of file_ids for the path to an entry.
1164
971
        root directory as depth 1.
1165
972
        """
1166
973
        p = []
1167
 
        for parent in self._iter_file_id_parents(file_id):
1168
 
            p.insert(0, parent.file_id)
 
974
        while file_id != None:
 
975
            try:
 
976
                ie = self._byid[file_id]
 
977
            except KeyError:
 
978
                raise BzrError("file_id {%s} not found in inventory" % file_id)
 
979
            p.insert(0, ie.file_id)
 
980
            file_id = ie.parent_id
1169
981
        return p
1170
982
 
 
983
 
1171
984
    def id2path(self, file_id):
1172
 
        """Return as a string the path to file_id.
 
985
        """Return as a list the path to file_id.
1173
986
        
1174
987
        >>> i = Inventory()
1175
988
        >>> e = i.add(InventoryDirectory('src-id', 'src', ROOT_ID))
1176
989
        >>> e = i.add(InventoryFile('foo-id', 'foo.c', parent_id='src-id'))
1177
 
        >>> print i.id2path('foo-id')
 
990
        >>> print i.id2path('foo-id').replace(os.sep, '/')
1178
991
        src/foo.c
1179
992
        """
1180
993
        # get all names, skipping root
1181
 
        return '/'.join(reversed(
1182
 
            [parent.name for parent in 
1183
 
             self._iter_file_id_parents(file_id)][:-1]))
 
994
        p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
 
995
        return os.sep.join(p)
1184
996
            
 
997
 
 
998
 
1185
999
    def path2id(self, name):
1186
1000
        """Walk down through directories to return entry of last component.
1187
1001
 
1191
1005
        This returns the entry of the last component in the path,
1192
1006
        which may be either a file or a directory.
1193
1007
 
1194
 
        Returns None IFF the path is not found.
 
1008
        Returns None iff the path is not found.
1195
1009
        """
1196
1010
        if isinstance(name, types.StringTypes):
1197
1011
            name = splitpath(name)
1198
1012
 
1199
 
        # mutter("lookup path %r" % name)
 
1013
        mutter("lookup path %r" % name)
1200
1014
 
1201
1015
        parent = self.root
1202
1016
        for f in name:
1211
1025
 
1212
1026
        return parent.file_id
1213
1027
 
 
1028
 
1214
1029
    def has_filename(self, names):
1215
1030
        return bool(self.path2id(names))
1216
1031
 
 
1032
 
1217
1033
    def has_id(self, file_id):
1218
 
        return (file_id in self._byid)
 
1034
        return self._byid.has_key(file_id)
1219
1035
 
1220
 
    def remove_recursive_id(self, file_id):
1221
 
        """Remove file_id, and children, from the inventory.
1222
 
        
1223
 
        :param file_id: A file_id to remove.
1224
 
        """
1225
 
        to_find_delete = [self._byid[file_id]]
1226
 
        to_delete = []
1227
 
        while to_find_delete:
1228
 
            ie = to_find_delete.pop()
1229
 
            to_delete.append(ie.file_id)
1230
 
            if ie.kind == 'directory':
1231
 
                to_find_delete.extend(ie.children.values())
1232
 
        for file_id in reversed(to_delete):
1233
 
            ie = self[file_id]
1234
 
            del self._byid[file_id]
1235
 
            if ie.parent_id is not None:
1236
 
                del self[ie.parent_id].children[ie.name]
1237
1036
 
1238
1037
    def rename(self, file_id, new_parent_id, new_name):
1239
1038
        """Move a file within the inventory.
1265
1064
        file_ie.parent_id = new_parent_id
1266
1065
 
1267
1066
 
1268
 
def make_entry(kind, name, parent_id, file_id=None):
1269
 
    """Create an inventory entry.
1270
 
 
1271
 
    :param kind: the type of inventory entry to create.
1272
 
    :param name: the basename of the entry.
1273
 
    :param parent_id: the parent_id of the entry.
1274
 
    :param file_id: the file_id to use. if None, one will be created.
1275
 
    """
1276
 
    if file_id is None:
1277
 
        file_id = bzrlib.workingtree.gen_file_id(name)
1278
 
 
1279
 
    norm_name, can_access = osutils.normalized_filename(name)
1280
 
    if norm_name != name:
1281
 
        if can_access:
1282
 
            name = norm_name
1283
 
        else:
1284
 
            # TODO: jam 20060701 This would probably be more useful
1285
 
            #       if the error was raised with the full path
1286
 
            raise errors.InvalidNormalization(name)
1287
 
 
1288
 
    if kind == 'directory':
1289
 
        return InventoryDirectory(file_id, name, parent_id)
1290
 
    elif kind == 'file':
1291
 
        return InventoryFile(file_id, name, parent_id)
1292
 
    elif kind == 'symlink':
1293
 
        return InventoryLink(file_id, name, parent_id)
1294
 
    else:
1295
 
        raise BzrError("unknown kind %r" % kind)
1296
1067
 
1297
1068
 
1298
1069
_NAME_RE = None
1299
1070
 
1300
1071
def is_valid_name(name):
1301
1072
    global _NAME_RE
1302
 
    if _NAME_RE is None:
 
1073
    if _NAME_RE == None:
1303
1074
        _NAME_RE = re.compile(r'^[^/\\]+$')
1304
1075
        
1305
1076
    return bool(_NAME_RE.match(name))