~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Robert Collins
  • Date: 2006-06-26 16:23:10 UTC
  • mfrom: (1780.2.1 misc-fixen)
  • mto: This revision was merged to the branch mainline in revision 1815.
  • Revision ID: robertc@robertcollins.net-20060626162310-98f5b55b8cc19d46
(robertc) Misc minor typos and the like.

Show diffs side-by-side

added added

removed removed

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