~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Robert Collins
  • Date: 2005-10-11 07:00:25 UTC
  • mto: This revision was merged to the branch mainline in revision 1443.
  • Revision ID: robertc@robertcollins.net-20051011070025-bac6b53cb6186dfd
create a config module - there is enough config logic to make this worthwhile, and start testing config processing.

Show diffs side-by-side

added added

removed removed

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