~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: 2006-10-02 00:43:10 UTC
  • mfrom: (2057.1.1 bzr.dev)
  • Revision ID: pqm@pqm.ubuntu.com-20061002004310-6e09ddd7fd28f71c
Merge in 0.11 NEWS entry.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# (C) 2005 Canonical Ltd
 
1
# Copyright (C) 2005, 2006 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
28
28
ROOT_ID = "TREE_ROOT"
29
29
 
30
30
 
 
31
import collections
31
32
import os.path
32
33
import re
33
34
import sys
34
35
import tarfile
35
36
import types
 
37
from warnings import warn
36
38
 
37
39
import bzrlib
 
40
from bzrlib import errors, osutils
38
41
from bzrlib.osutils import (pumpfile, quotefn, splitpath, joinpath,
39
 
                            appendpath, sha_strings)
 
42
                            pathjoin, sha_strings)
 
43
from bzrlib.errors import (NotVersionedError, InvalidEntryName,
 
44
                           BzrError, BzrCheckError, BinaryFile)
40
45
from bzrlib.trace import mutter
41
 
from bzrlib.errors import (NotVersionedError, InvalidEntryName,
42
 
                           BzrError, BzrCheckError)
43
46
 
44
47
 
45
48
class InventoryEntry(object):
76
79
    >>> i.path2id('')
77
80
    'TREE_ROOT'
78
81
    >>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
79
 
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
 
82
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
80
83
    >>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
81
 
    InventoryFile('2323', 'hello.c', parent_id='123')
82
 
    >>> for j in i.iter_entries():
83
 
    ...   print j
 
84
    InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
 
85
    >>> shouldbe = {0: '', 1: 'src', 2: pathjoin('src','hello.c')}
 
86
    >>> for ix, j in enumerate(i.iter_entries()):
 
87
    ...   print (j[0] == shouldbe[ix], j[1])
84
88
    ... 
85
 
    ('src', InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
86
 
    ('src/hello.c', InventoryFile('2323', 'hello.c', parent_id='123'))
 
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))
87
92
    >>> i.add(InventoryFile('2323', 'bye.c', '123'))
88
93
    Traceback (most recent call last):
89
94
    ...
90
95
    BzrError: inventory already contains entry with id {2323}
91
96
    >>> i.add(InventoryFile('2324', 'bye.c', '123'))
92
 
    InventoryFile('2324', 'bye.c', parent_id='123')
 
97
    InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
93
98
    >>> i.add(InventoryDirectory('2325', 'wibble', '123'))
94
 
    InventoryDirectory('2325', 'wibble', parent_id='123')
 
99
    InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
95
100
    >>> i.path2id('src/wibble')
96
101
    '2325'
97
102
    >>> '2325' in i
98
103
    True
99
104
    >>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
100
 
    InventoryFile('2326', 'wibble.c', parent_id='2325')
 
105
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
101
106
    >>> i['2326']
102
 
    InventoryFile('2326', 'wibble.c', parent_id='2325')
 
107
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
103
108
    >>> for path, entry in i.iter_entries():
104
 
    ...     print path.replace('\\\\', '/')     # for win32 os.sep
 
109
    ...     print path
105
110
    ...     assert i.path2id(path)
106
111
    ... 
 
112
    <BLANKLINE>
107
113
    src
108
114
    src/bye.c
109
115
    src/hello.c
110
116
    src/wibble
111
117
    src/wibble/wibble.c
112
 
    >>> i.id2path('2326').replace('\\\\', '/')
 
118
    >>> i.id2path('2326')
113
119
    'src/wibble/wibble.c'
114
120
    """
 
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'
115
129
    
116
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
117
 
                 'text_id', 'parent_id', 'children', 'executable', 
118
 
                 'revision']
119
 
 
120
 
    def _add_text_to_weave(self, new_lines, parents, weave_store, transaction):
121
 
        weave_store.add_text(self.file_id, self.revision, new_lines, parents,
122
 
                             transaction)
 
130
    __slots__ = []
123
131
 
124
132
    def detect_changes(self, old_entry):
125
133
        """Return a (text_modified, meta_modified) from this to old_entry.
150
158
             output_to, reverse=False):
151
159
        """Perform a diff between two entries of the same kind."""
152
160
 
153
 
    def find_previous_heads(self, previous_inventories, entry_weave):
154
 
        """Return the revisions and entries that directly preceed this.
 
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.
155
166
 
156
167
        Returned as a map from revision to inventory entry.
157
168
 
158
169
        This is a map containing the file revisions in all parents
159
170
        for which the file exists, and its revision is not a parent of
160
171
        any other. If the file is new, the set will be empty.
 
172
 
 
173
        :param versioned_file_store: A store where ancestry data on this
 
174
                                     file id can be queried.
 
175
        :param transaction: The transaction that queries to the versioned 
 
176
                            file store should be completed under.
 
177
        :param entry_vf: The entry versioned file, if its already available.
161
178
        """
162
179
        def get_ancestors(weave, entry):
163
 
            return set(map(weave.idx_to_name,
164
 
                           weave.inclusions([weave.lookup(entry.revision)])))
 
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.
165
184
        heads = {}
 
185
        # revision: ancestor list for each head
166
186
        head_ancestors = {}
 
187
        # identify candidate head revision ids.
167
188
        for inv in previous_inventories:
168
189
            if self.file_id in inv:
169
190
                ie = inv[self.file_id]
170
191
                assert ie.file_id == self.file_id
171
 
                if ie.revision in heads:
172
 
                    # fixup logic, there was a bug in revision updates.
173
 
                    # with x bit support.
 
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.
174
197
                    try:
175
 
                        if heads[ie.revision].executable != ie.executable:
176
 
                            heads[ie.revision].executable = False
 
198
                        if candidates[ie.revision].executable != ie.executable:
 
199
                            candidates[ie.revision].executable = False
177
200
                            ie.executable = False
178
201
                    except AttributeError:
179
202
                        pass
180
 
                    assert heads[ie.revision] == ie
 
203
                    # must now be the same.
 
204
                    assert candidates[ie.revision] == ie
181
205
                else:
182
 
                    # may want to add it.
183
 
                    # may already be covered:
184
 
                    already_present = 0 != len(
185
 
                        [head for head in heads 
186
 
                         if ie.revision in head_ancestors[head]])
187
 
                    if already_present:
188
 
                        # an ancestor of a known head.
189
 
                        continue
190
 
                    # definately a head:
191
 
                    ancestors = get_ancestors(entry_weave, ie)
192
 
                    # may knock something else out:
193
 
                    check_heads = list(heads.keys())
194
 
                    for head in check_heads:
195
 
                        if head in ancestors:
196
 
                            # this head is not really a head
197
 
                            heads.pop(head)
198
 
                    head_ancestors[ie.revision] = ancestors
199
 
                    heads[ie.revision] = ie
 
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
200
245
        return heads
201
246
 
202
247
    def get_tar_item(self, root, dp, now, tree):
203
248
        """Get a tarfile item and a file stream for its content."""
204
 
        item = tarfile.TarInfo(os.path.join(root, dp))
 
249
        item = tarfile.TarInfo(pathjoin(root, dp).encode('utf8'))
205
250
        # TODO: would be cool to actually set it to the timestamp of the
206
251
        # revision it was last changed
207
252
        item.mtime = now
251
296
        """Return a short kind indicator useful for appending to names."""
252
297
        raise BzrError('unknown kind %r' % self.kind)
253
298
 
254
 
    known_kinds = ('file', 'directory', 'symlink', 'root_directory')
 
299
    known_kinds = ('file', 'directory', 'symlink')
255
300
 
256
301
    def _put_in_tar(self, item, tree):
257
302
        """populate item for stashing in a tar, and return the content stream.
266
311
        
267
312
        This is a template method - implement _put_on_disk in subclasses.
268
313
        """
269
 
        fullpath = appendpath(dest, dp)
 
314
        fullpath = pathjoin(dest, dp)
270
315
        self._put_on_disk(fullpath, tree)
271
 
        mutter("  export {%s} kind %s to %s" % (self.file_id, self.kind, fullpath))
 
316
        # mutter("  export {%s} kind %s to %s", self.file_id,
 
317
        #         self.kind, fullpath)
272
318
 
273
319
    def _put_on_disk(self, fullpath, tree):
274
320
        """Put this entry onto disk at fullpath, from tree tree."""
275
321
        raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
276
322
 
277
323
    def sorted_children(self):
278
 
        l = self.children.items()
279
 
        l.sort()
280
 
        return l
 
324
        return sorted(self.children.items())
281
325
 
282
326
    @staticmethod
283
327
    def versionable_kind(kind):
284
 
        return kind in ('file', 'directory', 'symlink')
 
328
        return (kind in ('file', 'directory', 'symlink'))
285
329
 
286
330
    def check(self, checker, rev_id, inv, tree):
287
331
        """Check this inventory entry is intact.
288
332
 
289
333
        This is a template method, override _check for kind specific
290
334
        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.
291
343
        """
292
 
        if self.parent_id != None:
 
344
        if self.parent_id is not None:
293
345
            if not inv.has_id(self.parent_id):
294
346
                raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
295
347
                        % (self.parent_id, rev_id))
300
352
        raise BzrCheckError('unknown entry kind %r in revision {%s}' % 
301
353
                            (self.kind, rev_id))
302
354
 
303
 
 
304
355
    def copy(self):
305
356
        """Clone this inventory entry."""
306
357
        raise NotImplementedError
307
358
 
308
 
    def _get_snapshot_change(self, previous_entries):
309
 
        if len(previous_entries) > 1:
310
 
            return 'merged'
311
 
        elif len(previous_entries) == 0:
 
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:
312
376
            return 'added'
313
 
        else:
314
 
            return 'modified/renamed/reparented'
 
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
398
 
316
399
    def __repr__(self):
317
 
        return ("%s(%r, %r, parent_id=%r)"
 
400
        return ("%s(%r, %r, parent_id=%r, revision=%r)"
318
401
                % (self.__class__.__name__,
319
402
                   self.file_id,
320
403
                   self.name,
321
 
                   self.parent_id))
 
404
                   self.parent_id,
 
405
                   self.revision))
322
406
 
323
407
    def snapshot(self, revision, path, previous_entries,
324
 
                 work_tree, weave_store, transaction):
 
408
                 work_tree, commit_builder):
325
409
        """Make a snapshot of this entry which may or may not have changed.
326
410
        
327
411
        This means that all its fields are populated, that it has its
328
412
        text stored in the text store or weave.
329
413
        """
330
 
        mutter('new parents of %s are %r', path, previous_entries)
 
414
        # mutter('new parents of %s are %r', path, previous_entries)
331
415
        self._read_tree_state(path, work_tree)
 
416
        # TODO: Where should we determine whether to reuse a
 
417
        # previous revision id or create a new revision? 20060606
332
418
        if len(previous_entries) == 1:
333
419
            # cannot be unchanged unless there is only one parent file rev.
334
420
            parent_ie = previous_entries.values()[0]
335
421
            if self._unchanged(parent_ie):
336
 
                mutter("found unchanged entry")
 
422
                # mutter("found unchanged entry")
337
423
                self.revision = parent_ie.revision
338
424
                return "unchanged"
339
 
        return self.snapshot_revision(revision, previous_entries, 
340
 
                                      work_tree, weave_store, transaction)
341
 
 
342
 
    def snapshot_revision(self, revision, previous_entries, work_tree,
343
 
                          weave_store, transaction):
344
 
        """Record this revision unconditionally."""
345
 
        mutter('new revision for {%s}', self.file_id)
 
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)
346
440
        self.revision = revision
347
 
        change = self._get_snapshot_change(previous_entries)
348
 
        self._snapshot_text(previous_entries, work_tree, weave_store,
349
 
                            transaction)
350
 
        return change
 
441
        self._snapshot_text(previous_entries, work_tree, commit_builder)
351
442
 
352
 
    def _snapshot_text(self, file_parents, work_tree, weave_store, transaction): 
 
443
    def _snapshot_text(self, file_parents, work_tree, commit_builder): 
353
444
        """Record the 'text' of this entry, whatever form that takes.
354
445
        
355
446
        This default implementation simply adds an empty text.
356
447
        """
357
 
        mutter('storing file {%s} in revision {%s}',
358
 
               self.file_id, self.revision)
359
 
        self._add_text_to_weave([], file_parents, weave_store, transaction)
 
448
        raise NotImplementedError(self._snapshot_text)
360
449
 
361
450
    def __eq__(self, other):
362
451
        if not isinstance(other, InventoryEntry):
383
472
    def _unchanged(self, previous_ie):
384
473
        """Has this entry changed relative to previous_ie.
385
474
 
386
 
        This method should be overriden in child classes.
 
475
        This method should be overridden in child classes.
387
476
        """
388
477
        compatible = True
389
478
        # different inv parent
405
494
        # first requested, or preload them if they're already known
406
495
        pass            # nothing to do by default
407
496
 
 
497
    def _forget_tree_state(self):
 
498
        pass
 
499
 
408
500
 
409
501
class RootEntry(InventoryEntry):
410
502
 
 
503
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
504
                 'text_id', 'parent_id', 'children', 'executable', 
 
505
                 'revision', 'symlink_target']
 
506
 
411
507
    def _check(self, checker, rev_id, tree):
412
508
        """See InventoryEntry._check"""
413
509
 
414
510
    def __init__(self, file_id):
415
511
        self.file_id = file_id
416
512
        self.children = {}
417
 
        self.kind = 'root_directory'
 
513
        self.kind = 'directory'
418
514
        self.parent_id = None
419
 
        self.name = ''
 
515
        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)
420
520
 
421
521
    def __eq__(self, other):
422
522
        if not isinstance(other, RootEntry):
429
529
class InventoryDirectory(InventoryEntry):
430
530
    """A directory in an inventory."""
431
531
 
 
532
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
533
                 'text_id', 'parent_id', 'children', 'executable', 
 
534
                 'revision', 'symlink_target']
 
535
 
432
536
    def _check(self, checker, rev_id, tree):
433
537
        """See InventoryEntry._check"""
434
 
        if self.text_sha1 != None or self.text_size != None or self.text_id != None:
 
538
        if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
435
539
            raise BzrCheckError('directory {%s} has text in revision {%s}'
436
540
                                % (self.file_id, rev_id))
437
541
 
464
568
        """See InventoryEntry._put_on_disk."""
465
569
        os.mkdir(fullpath)
466
570
 
 
571
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
 
572
        """See InventoryEntry._snapshot_text."""
 
573
        commit_builder.modified_directory(self.file_id, file_parents)
 
574
 
467
575
 
468
576
class InventoryFile(InventoryEntry):
469
577
    """A file in an inventory."""
470
578
 
471
 
    def _check(self, checker, rev_id, tree):
 
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):
472
584
        """See InventoryEntry._check"""
473
 
        revision = self.revision
474
 
        t = (self.file_id, revision)
 
585
        t = (self.file_id, self.revision)
475
586
        if t in checker.checked_texts:
476
 
            prev_sha = checker.checked_texts[t] 
 
587
            prev_sha = checker.checked_texts[t]
477
588
            if prev_sha != self.text_sha1:
478
589
                raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
479
 
                                    (self.file_id, rev_id))
 
590
                                    (self.file_id, tree_revision_id))
480
591
            else:
481
592
                checker.repeated_text_cnt += 1
482
593
                return
483
 
        mutter('check version {%s} of {%s}', rev_id, self.file_id)
484
 
        file_lines = tree.get_file_lines(self.file_id)
485
 
        checker.checked_text_cnt += 1 
486
 
        if self.text_size != sum(map(len, file_lines)):
487
 
            raise BzrCheckError('text {%s} wrong size' % self.text_id)
488
 
        if self.text_sha1 != sha_strings(file_lines):
489
 
            raise BzrCheckError('text {%s} wrong sha1' % self.text_id)
 
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))
490
614
        checker.checked_texts[t] = self.text_sha1
491
615
 
492
616
    def copy(self):
500
624
 
501
625
    def detect_changes(self, old_entry):
502
626
        """See InventoryEntry.detect_changes."""
503
 
        assert self.text_sha1 != None
504
 
        assert old_entry.text_sha1 != None
 
627
        assert self.text_sha1 is not None
 
628
        assert old_entry.text_sha1 is not None
505
629
        text_modified = (self.text_sha1 != old_entry.text_sha1)
506
630
        meta_modified = (self.executable != old_entry.executable)
507
631
        return text_modified, meta_modified
509
633
    def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
510
634
             output_to, reverse=False):
511
635
        """See InventoryEntry._diff."""
512
 
        from_text = tree.get_file(self.file_id).readlines()
513
 
        if to_entry:
514
 
            to_text = to_tree.get_file(to_entry.file_id).readlines()
515
 
        else:
516
 
            to_text = []
517
 
        if not reverse:
518
 
            text_diff(from_label, from_text,
519
 
                      to_label, to_text, output_to)
520
 
        else:
521
 
            text_diff(to_label, to_text,
522
 
                      from_label, from_text, output_to)
 
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
523
654
 
524
655
    def has_text(self):
525
656
        """See InventoryEntry.has_text."""
552
683
 
553
684
    def _read_tree_state(self, path, work_tree):
554
685
        """See InventoryEntry._read_tree_state."""
555
 
        self.text_sha1 = work_tree.get_file_sha1(self.file_id)
556
 
        self.executable = work_tree.is_executable(self.file_id)
557
 
 
558
 
    def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
 
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):
559
704
        """See InventoryEntry._snapshot_text."""
560
 
        mutter('storing file {%s} in revision {%s}',
561
 
               self.file_id, self.revision)
562
 
        # special case to avoid diffing on renames or 
563
 
        # reparenting
564
 
        if (len(file_parents) == 1
565
 
            and self.text_sha1 == file_parents.values()[0].text_sha1
566
 
            and self.text_size == file_parents.values()[0].text_size):
567
 
            previous_ie = file_parents.values()[0]
568
 
            weave_store.add_identical_text(
569
 
                self.file_id, previous_ie.revision, 
570
 
                self.revision, file_parents, transaction)
571
 
        else:
572
 
            new_lines = work_tree.get_file(self.file_id).readlines()
573
 
            self._add_text_to_weave(new_lines, file_parents, weave_store,
574
 
                                    transaction)
575
 
            self.text_sha1 = sha_strings(new_lines)
576
 
            self.text_size = sum(map(len, new_lines))
577
 
 
 
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)
578
709
 
579
710
    def _unchanged(self, previous_ie):
580
711
        """See InventoryEntry._unchanged."""
593
724
class InventoryLink(InventoryEntry):
594
725
    """A file in an inventory."""
595
726
 
596
 
    __slots__ = ['symlink_target']
 
727
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
728
                 'text_id', 'parent_id', 'children', 'executable', 
 
729
                 'revision', 'symlink_target']
597
730
 
598
731
    def _check(self, checker, rev_id, tree):
599
732
        """See InventoryEntry._check"""
600
 
        if self.text_sha1 != None or self.text_size != None or self.text_id != None:
 
733
        if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
601
734
            raise BzrCheckError('symlink {%s} has text in revision {%s}'
602
735
                    % (self.file_id, rev_id))
603
 
        if self.symlink_target == None:
 
736
        if self.symlink_target is None:
604
737
            raise BzrCheckError('symlink {%s} has no target in revision {%s}'
605
738
                    % (self.file_id, rev_id))
606
739
 
646
779
 
647
780
    def _put_in_tar(self, item, tree):
648
781
        """See InventoryEntry._put_in_tar."""
649
 
        iterm.type = tarfile.SYMTYPE
 
782
        item.type = tarfile.SYMTYPE
650
783
        fileobj = None
651
784
        item.size = 0
652
785
        item.mode = 0755
664
797
        """See InventoryEntry._read_tree_state."""
665
798
        self.symlink_target = work_tree.get_symlink_target(self.file_id)
666
799
 
 
800
    def _forget_tree_state(self):
 
801
        self.symlink_target = None
 
802
 
667
803
    def _unchanged(self, previous_ie):
668
804
        """See InventoryEntry._unchanged."""
669
805
        compatible = super(InventoryLink, self)._unchanged(previous_ie)
671
807
            compatible = False
672
808
        return compatible
673
809
 
 
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
 
674
815
 
675
816
class Inventory(object):
676
817
    """Inventory of versioned files in a tree.
691
832
 
692
833
    >>> inv = Inventory()
693
834
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
694
 
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT')
 
835
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
695
836
    >>> inv['123-123'].name
696
837
    'hello.c'
697
838
 
705
846
    May also look up by name:
706
847
 
707
848
    >>> [x[0] for x in inv.iter_entries()]
708
 
    ['hello.c']
 
849
    ['', u'hello.c']
709
850
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
710
851
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
711
 
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
 
852
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None)
712
853
    """
713
 
    def __init__(self, root_id=ROOT_ID):
 
854
    def __init__(self, root_id=ROOT_ID, revision_id=None):
714
855
        """Create or read an inventory.
715
856
 
716
857
        If a working directory is specified, the inventory is read
720
861
        The inventory is created with a default root directory, with
721
862
        an id of None.
722
863
        """
723
 
        # We are letting Branch.initialize() create a unique inventory
 
864
        # We are letting Branch.create() create a unique inventory
724
865
        # root id. Rather than generating a random one here.
725
866
        #if root_id is None:
726
867
        #    root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
727
 
        self.root = RootEntry(root_id)
 
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
728
879
        self._byid = {self.root.file_id: self.root}
729
880
 
730
 
 
731
881
    def copy(self):
732
 
        other = Inventory(self.root.file_id)
 
882
        # TODO: jam 20051218 Should copy also copy the revision_id?
 
883
        entries = self.iter_entries()
 
884
        other = Inventory(entries.next()[1].file_id)
733
885
        # copy recursively so we know directories will be added before
734
886
        # their children.  There are more efficient ways than this...
735
 
        for path, entry in self.iter_entries():
736
 
            if entry == self.root:
737
 
                continue
 
887
        for path, entry in entries():
738
888
            other.add(entry.copy())
739
889
        return other
740
890
 
741
 
 
742
891
    def __iter__(self):
743
892
        return iter(self._byid)
744
893
 
745
 
 
746
894
    def __len__(self):
747
895
        """Returns number of entries."""
748
896
        return len(self._byid)
749
897
 
750
 
 
751
898
    def iter_entries(self, from_dir=None):
752
899
        """Return (path, entry) pairs, in order by name."""
753
 
        if from_dir == None:
754
 
            assert self.root
755
 
            from_dir = self.root
756
 
        elif isinstance(from_dir, basestring):
757
 
            from_dir = self._byid[from_dir]
758
 
            
759
 
        kids = from_dir.children.items()
760
 
        kids.sort()
761
 
        for name, ie in kids:
762
 
            yield name, ie
763
 
            if ie.kind == 'directory':
764
 
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
765
 
                    yield os.path.join(name, cn), cie
766
 
 
 
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))
767
974
 
768
975
    def entries(self):
769
976
        """Return list of (path, ie) for all entries except the root.
775
982
            kids = dir_ie.children.items()
776
983
            kids.sort()
777
984
            for name, ie in kids:
778
 
                child_path = os.path.join(dir_path, name)
 
985
                child_path = pathjoin(dir_path, name)
779
986
                accum.append((child_path, ie))
780
987
                if ie.kind == 'directory':
781
988
                    descend(ie, child_path)
782
989
 
783
 
        descend(self.root, '')
 
990
        descend(self.root, u'')
784
991
        return accum
785
992
 
786
 
 
787
993
    def directories(self):
788
994
        """Return (path, entry) pairs for all directories, including the root.
789
995
        """
795
1001
            kids.sort()
796
1002
 
797
1003
            for name, child_ie in kids:
798
 
                child_path = os.path.join(parent_path, name)
 
1004
                child_path = pathjoin(parent_path, name)
799
1005
                descend(child_ie, child_path)
800
 
        descend(self.root, '')
 
1006
        descend(self.root, u'')
801
1007
        return accum
802
1008
        
803
 
 
804
 
 
805
1009
    def __contains__(self, file_id):
806
1010
        """True if this entry contains a file with given id.
807
1011
 
808
1012
        >>> inv = Inventory()
809
1013
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
810
 
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
 
1014
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
811
1015
        >>> '123' in inv
812
1016
        True
813
1017
        >>> '456' in inv
814
1018
        False
815
1019
        """
816
 
        return file_id in self._byid
817
 
 
 
1020
        return (file_id in self._byid)
818
1021
 
819
1022
    def __getitem__(self, file_id):
820
1023
        """Return the entry for given file_id.
821
1024
 
822
1025
        >>> inv = Inventory()
823
1026
        >>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
824
 
        InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT')
 
1027
        InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
825
1028
        >>> inv['123123'].name
826
1029
        'hello.c'
827
1030
        """
828
1031
        try:
829
1032
            return self._byid[file_id]
830
1033
        except KeyError:
831
 
            if file_id == None:
 
1034
            if file_id is None:
832
1035
                raise BzrError("can't look up file_id None")
833
1036
            else:
834
1037
                raise BzrError("file_id {%s} not in inventory" % file_id)
835
1038
 
836
 
 
837
1039
    def get_file_kind(self, file_id):
838
1040
        return self._byid[file_id].kind
839
1041
 
840
1042
    def get_child(self, parent_id, filename):
841
1043
        return self[parent_id].children.get(filename)
842
1044
 
843
 
 
844
1045
    def add(self, entry):
845
1046
        """Add entry to inventory.
846
1047
 
852
1053
        if entry.file_id in self._byid:
853
1054
            raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
854
1055
 
855
 
        if entry.parent_id == ROOT_ID or entry.parent_id is None:
 
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
856
1062
            entry.parent_id = self.root.file_id
857
1063
 
858
1064
        try:
860
1066
        except KeyError:
861
1067
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
862
1068
 
863
 
        if parent.children.has_key(entry.name):
 
1069
        if entry.name in parent.children:
864
1070
            raise BzrError("%s is already versioned" %
865
 
                    appendpath(self.id2path(parent.file_id), entry.name))
 
1071
                    pathjoin(self.id2path(parent.file_id), entry.name))
866
1072
 
867
1073
        self._byid[entry.file_id] = entry
868
1074
        parent.children[entry.name] = entry
869
1075
        return entry
870
1076
 
871
 
 
872
 
    def add_path(self, relpath, kind, file_id=None):
 
1077
    def add_path(self, relpath, kind, file_id=None, parent_id=None):
873
1078
        """Add entry from a path.
874
1079
 
875
1080
        The immediate parent must already be versioned.
876
1081
 
877
1082
        Returns the new entry object."""
878
 
        from bzrlib.branch import gen_file_id
879
1083
        
880
 
        parts = bzrlib.osutils.splitpath(relpath)
 
1084
        parts = osutils.splitpath(relpath)
 
1085
 
881
1086
        if len(parts) == 0:
882
 
            raise BzrError("cannot re-add root of inventory")
883
 
 
884
 
        if file_id == None:
885
 
            file_id = gen_file_id(relpath)
886
 
 
887
 
        parent_path = parts[:-1]
888
 
        parent_id = self.path2id(parent_path)
889
 
        if parent_id == None:
890
 
            raise NotVersionedError(path=parent_path)
891
 
        if kind == 'directory':
892
 
            ie = InventoryDirectory(file_id, parts[-1], parent_id)
893
 
        elif kind == 'file':
894
 
            ie = InventoryFile(file_id, parts[-1], parent_id)
895
 
        elif kind == 'symlink':
896
 
            ie = InventoryLink(file_id, parts[-1], parent_id)
 
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
897
1092
        else:
898
 
            raise BzrError("unknown kind %r" % kind)
 
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)
899
1098
        return self.add(ie)
900
1099
 
901
 
 
902
1100
    def __delitem__(self, file_id):
903
1101
        """Remove entry by id.
904
1102
 
905
1103
        >>> inv = Inventory()
906
1104
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
907
 
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
 
1105
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
908
1106
        >>> '123' in inv
909
1107
        True
910
1108
        >>> del inv['123']
913
1111
        """
914
1112
        ie = self[file_id]
915
1113
 
916
 
        assert self[ie.parent_id].children[ie.name] == ie
 
1114
        assert ie.parent_id is None or \
 
1115
            self[ie.parent_id].children[ie.name] == ie
917
1116
        
918
 
        # TODO: Test deleting all children; maybe hoist to a separate
919
 
        # deltree method?
920
 
        if ie.kind == 'directory':
921
 
            for cie in ie.children.values():
922
 
                del self[cie.file_id]
923
 
            del ie.children
924
 
 
925
1117
        del self._byid[file_id]
926
 
        del self[ie.parent_id].children[ie.name]
927
 
 
 
1118
        if ie.parent_id is not None:
 
1119
            del self[ie.parent_id].children[ie.name]
928
1120
 
929
1121
    def __eq__(self, other):
930
1122
        """Compare two sets by comparing their contents.
934
1126
        >>> i1 == i2
935
1127
        True
936
1128
        >>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
937
 
        InventoryFile('123', 'foo', parent_id='TREE_ROOT')
 
1129
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
938
1130
        >>> i1 == i2
939
1131
        False
940
1132
        >>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
941
 
        InventoryFile('123', 'foo', parent_id='TREE_ROOT')
 
1133
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
942
1134
        >>> i1 == i2
943
1135
        True
944
1136
        """
945
1137
        if not isinstance(other, Inventory):
946
1138
            return NotImplemented
947
1139
 
948
 
        if len(self._byid) != len(other._byid):
949
 
            # shortcut: obviously not the same
950
 
            return False
951
 
 
952
1140
        return self._byid == other._byid
953
1141
 
954
 
 
955
1142
    def __ne__(self, other):
956
1143
        return not self.__eq__(other)
957
1144
 
958
 
 
959
1145
    def __hash__(self):
960
1146
        raise ValueError('not hashable')
961
1147
 
 
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
962
1157
 
963
1158
    def get_idpath(self, file_id):
964
1159
        """Return a list of file_ids for the path to an entry.
969
1164
        root directory as depth 1.
970
1165
        """
971
1166
        p = []
972
 
        while file_id != None:
973
 
            try:
974
 
                ie = self._byid[file_id]
975
 
            except KeyError:
976
 
                raise BzrError("file_id {%s} not found in inventory" % file_id)
977
 
            p.insert(0, ie.file_id)
978
 
            file_id = ie.parent_id
 
1167
        for parent in self._iter_file_id_parents(file_id):
 
1168
            p.insert(0, parent.file_id)
979
1169
        return p
980
1170
 
981
 
 
982
1171
    def id2path(self, file_id):
983
 
        """Return as a list the path to file_id.
 
1172
        """Return as a string the path to file_id.
984
1173
        
985
1174
        >>> i = Inventory()
986
1175
        >>> e = i.add(InventoryDirectory('src-id', 'src', ROOT_ID))
987
1176
        >>> e = i.add(InventoryFile('foo-id', 'foo.c', parent_id='src-id'))
988
 
        >>> print i.id2path('foo-id').replace(os.sep, '/')
 
1177
        >>> print i.id2path('foo-id')
989
1178
        src/foo.c
990
1179
        """
991
1180
        # get all names, skipping root
992
 
        p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
993
 
        return os.sep.join(p)
 
1181
        return '/'.join(reversed(
 
1182
            [parent.name for parent in 
 
1183
             self._iter_file_id_parents(file_id)][:-1]))
994
1184
            
995
 
 
996
 
 
997
1185
    def path2id(self, name):
998
1186
        """Walk down through directories to return entry of last component.
999
1187
 
1003
1191
        This returns the entry of the last component in the path,
1004
1192
        which may be either a file or a directory.
1005
1193
 
1006
 
        Returns None iff the path is not found.
 
1194
        Returns None IFF the path is not found.
1007
1195
        """
1008
1196
        if isinstance(name, types.StringTypes):
1009
1197
            name = splitpath(name)
1010
1198
 
1011
 
        mutter("lookup path %r" % name)
 
1199
        # mutter("lookup path %r" % name)
1012
1200
 
1013
1201
        parent = self.root
1014
1202
        for f in name:
1023
1211
 
1024
1212
        return parent.file_id
1025
1213
 
1026
 
 
1027
1214
    def has_filename(self, names):
1028
1215
        return bool(self.path2id(names))
1029
1216
 
1030
 
 
1031
1217
    def has_id(self, file_id):
1032
 
        return self._byid.has_key(file_id)
 
1218
        return (file_id in self._byid)
1033
1219
 
 
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]
1034
1237
 
1035
1238
    def rename(self, file_id, new_parent_id, new_name):
1036
1239
        """Move a file within the inventory.
1062
1265
        file_ie.parent_id = new_parent_id
1063
1266
 
1064
1267
 
 
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)
1065
1296
 
1066
1297
 
1067
1298
_NAME_RE = None
1068
1299
 
1069
1300
def is_valid_name(name):
1070
1301
    global _NAME_RE
1071
 
    if _NAME_RE == None:
 
1302
    if _NAME_RE is None:
1072
1303
        _NAME_RE = re.compile(r'^[^/\\]+$')
1073
1304
        
1074
1305
    return bool(_NAME_RE.match(name))