~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

Merge from bzr.dev.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# (C) 2005 Canonical Ltd
 
1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
27
27
# created, but it's not for now.
28
28
ROOT_ID = "TREE_ROOT"
29
29
 
30
 
 
31
 
import os.path
 
30
import os
32
31
import re
33
32
import sys
 
33
 
 
34
from bzrlib.lazy_import import lazy_import
 
35
lazy_import(globals(), """
 
36
import collections
34
37
import tarfile
35
 
import types
36
38
 
37
39
import bzrlib
38
 
from bzrlib.osutils import (pumpfile, quotefn, splitpath, joinpath,
39
 
                            pathjoin, sha_strings)
 
40
from bzrlib import (
 
41
    errors,
 
42
    generate_ids,
 
43
    osutils,
 
44
    symbol_versioning,
 
45
    workingtree,
 
46
    )
 
47
""")
 
48
 
 
49
from bzrlib.errors import (
 
50
    BzrCheckError,
 
51
    BzrError,
 
52
    )
 
53
from bzrlib.symbol_versioning import deprecated_method, zero_ninetyone
40
54
from bzrlib.trace import mutter
41
 
from bzrlib.errors import (NotVersionedError, InvalidEntryName,
42
 
                           BzrError, BzrCheckError)
43
55
 
44
56
 
45
57
class InventoryEntry(object):
76
88
    >>> i.path2id('')
77
89
    'TREE_ROOT'
78
90
    >>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
79
 
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
 
91
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
80
92
    >>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
81
 
    InventoryFile('2323', 'hello.c', parent_id='123')
82
 
    >>> shouldbe = {0: 'src', 1: pathjoin('src','hello.c')}
 
93
    InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
 
94
    >>> shouldbe = {0: '', 1: 'src', 2: 'src/hello.c'}
83
95
    >>> for ix, j in enumerate(i.iter_entries()):
84
96
    ...   print (j[0] == shouldbe[ix], j[1])
85
97
    ... 
86
 
    (True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
87
 
    (True, InventoryFile('2323', 'hello.c', parent_id='123'))
88
 
    >>> i.add(InventoryFile('2323', 'bye.c', '123'))
89
 
    Traceback (most recent call last):
90
 
    ...
91
 
    BzrError: inventory already contains entry with id {2323}
 
98
    (True, InventoryDirectory('TREE_ROOT', u'', parent_id=None, revision=None))
 
99
    (True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
 
100
    (True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
92
101
    >>> i.add(InventoryFile('2324', 'bye.c', '123'))
93
 
    InventoryFile('2324', 'bye.c', parent_id='123')
 
102
    InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
94
103
    >>> i.add(InventoryDirectory('2325', 'wibble', '123'))
95
 
    InventoryDirectory('2325', 'wibble', parent_id='123')
 
104
    InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
96
105
    >>> i.path2id('src/wibble')
97
106
    '2325'
98
107
    >>> '2325' in i
99
108
    True
100
109
    >>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
101
 
    InventoryFile('2326', 'wibble.c', parent_id='2325')
 
110
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
102
111
    >>> i['2326']
103
 
    InventoryFile('2326', 'wibble.c', parent_id='2325')
 
112
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
104
113
    >>> for path, entry in i.iter_entries():
105
114
    ...     print path
106
115
    ...     assert i.path2id(path)
107
116
    ... 
 
117
    <BLANKLINE>
108
118
    src
109
119
    src/bye.c
110
120
    src/hello.c
113
123
    >>> i.id2path('2326')
114
124
    'src/wibble/wibble.c'
115
125
    """
 
126
 
 
127
    # Constants returned by describe_change()
 
128
    #
 
129
    # TODO: These should probably move to some kind of FileChangeDescription 
 
130
    # class; that's like what's inside a TreeDelta but we want to be able to 
 
131
    # generate them just for one file at a time.
 
132
    RENAMED = 'renamed'
 
133
    MODIFIED_AND_RENAMED = 'modified and renamed'
116
134
    
117
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
118
 
                 'text_id', 'parent_id', 'children', 'executable', 
119
 
                 'revision']
120
 
 
121
 
    def _add_text_to_weave(self, new_lines, parents, weave_store, transaction):
122
 
        weave_store.add_text(self.file_id, self.revision, new_lines, parents,
123
 
                             transaction)
 
135
    __slots__ = []
124
136
 
125
137
    def detect_changes(self, old_entry):
126
138
        """Return a (text_modified, meta_modified) from this to old_entry.
150
162
    def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
151
163
             output_to, reverse=False):
152
164
        """Perform a diff between two entries of the same kind."""
153
 
 
154
 
    def find_previous_heads(self, previous_inventories, entry_weave):
155
 
        """Return the revisions and entries that directly preceed this.
156
 
 
157
 
        Returned as a map from revision to inventory entry.
158
 
 
159
 
        This is a map containing the file revisions in all parents
160
 
        for which the file exists, and its revision is not a parent of
161
 
        any other. If the file is new, the set will be empty.
 
165
    
 
166
    def parent_candidates(self, previous_inventories):
 
167
        """Find possible per-file graph parents.
 
168
 
 
169
        This is currently defined by:
 
170
         - Select the last changed revision in the parent inventory.
 
171
         - Do deal with a short lived bug in bzr 0.8's development two entries
 
172
           that have the same last changed but different 'x' bit settings are
 
173
           changed in-place.
162
174
        """
163
 
        def get_ancestors(weave, entry):
164
 
            return set(map(weave.idx_to_name,
165
 
                           weave.inclusions([weave.lookup(entry.revision)])))
166
 
        heads = {}
167
 
        head_ancestors = {}
 
175
        # revision:ie mapping for each ie found in previous_inventories.
 
176
        candidates = {}
 
177
        # identify candidate head revision ids.
168
178
        for inv in previous_inventories:
169
179
            if self.file_id in inv:
170
180
                ie = inv[self.file_id]
171
181
                assert ie.file_id == self.file_id
172
 
                if ie.revision in heads:
173
 
                    # fixup logic, there was a bug in revision updates.
174
 
                    # with x bit support.
 
182
                if ie.revision in candidates:
 
183
                    # same revision value in two different inventories:
 
184
                    # correct possible inconsistencies:
 
185
                    #     * there was a bug in revision updates with 'x' bit 
 
186
                    #       support.
175
187
                    try:
176
 
                        if heads[ie.revision].executable != ie.executable:
177
 
                            heads[ie.revision].executable = False
 
188
                        if candidates[ie.revision].executable != ie.executable:
 
189
                            candidates[ie.revision].executable = False
178
190
                            ie.executable = False
179
191
                    except AttributeError:
180
192
                        pass
181
 
                    assert heads[ie.revision] == ie
 
193
                    # must now be the same.
 
194
                    assert candidates[ie.revision] == ie
182
195
                else:
183
 
                    # may want to add it.
184
 
                    # may already be covered:
185
 
                    already_present = 0 != len(
186
 
                        [head for head in heads 
187
 
                         if ie.revision in head_ancestors[head]])
188
 
                    if already_present:
189
 
                        # an ancestor of a known head.
190
 
                        continue
191
 
                    # definately a head:
192
 
                    ancestors = get_ancestors(entry_weave, ie)
193
 
                    # may knock something else out:
194
 
                    check_heads = list(heads.keys())
195
 
                    for head in check_heads:
196
 
                        if head in ancestors:
197
 
                            # this head is not really a head
198
 
                            heads.pop(head)
199
 
                    head_ancestors[ie.revision] = ancestors
200
 
                    heads[ie.revision] = ie
 
196
                    # add this revision as a candidate.
 
197
                    candidates[ie.revision] = ie
 
198
        return candidates
 
199
 
 
200
    @deprecated_method(zero_ninetyone)
 
201
    def find_previous_heads(self, previous_inventories,
 
202
                            versioned_file_store,
 
203
                            transaction,
 
204
                            entry_vf=None):
 
205
        """Return the revisions and entries that directly precede this.
 
206
 
 
207
        Returned as a map from revision to inventory entry.
 
208
 
 
209
        This is a map containing the file revisions in all parents
 
210
        for which the file exists, and its revision is not a parent of
 
211
        any other. If the file is new, the set will be empty.
 
212
 
 
213
        :param versioned_file_store: A store where ancestry data on this
 
214
                                     file id can be queried.
 
215
        :param transaction: The transaction that queries to the versioned 
 
216
                            file store should be completed under.
 
217
        :param entry_vf: The entry versioned file, if its already available.
 
218
        """
 
219
        candidates = self.parent_candidates(previous_inventories)
 
220
 
 
221
        # revision:ie mapping with one revision for each head.
 
222
        heads = {}
 
223
        # common case optimisation
 
224
        if len(candidates) == 1:
 
225
            # if there is only one candidate revision found
 
226
            # then we can avoid opening the versioned file to access ancestry:
 
227
            # there cannot be any ancestors to eliminate when there is 
 
228
            # only one revision available.
 
229
            return candidates
 
230
        
 
231
        # --- what follows is now encapsulated in repository.get_graph.heads(), 
 
232
        #     but that is not accessible from here as we have no repository
 
233
        #     pointer. Note that the repository.get_graph.heads() call can return
 
234
        #     different results *at the moment* because of the kind-changing check
 
235
        #     we have in parent_candidates().
 
236
 
 
237
        # eliminate ancestors amongst the available candidates:
 
238
        # heads are those that are not an ancestor of any other candidate
 
239
        # - this provides convergence at a per-file level.
 
240
        def get_ancestors(weave, entry):
 
241
            return set(weave.get_ancestry(entry.revision, topo_sorted=False))
 
242
        # revision: ancestor list for each head
 
243
        head_ancestors = {}
 
244
        for ie in candidates.values():
 
245
            # may be an ancestor of a known head:
 
246
            already_present = 0 != len(
 
247
                [head for head in heads 
 
248
                 if ie.revision in head_ancestors[head]])
 
249
            if already_present:
 
250
                # an ancestor of an analyzed candidate.
 
251
                continue
 
252
            # not an ancestor of a known head:
 
253
            # load the versioned file for this file id if needed
 
254
            if entry_vf is None:
 
255
                entry_vf = versioned_file_store.get_weave_or_empty(
 
256
                    self.file_id, transaction)
 
257
            ancestors = get_ancestors(entry_vf, ie)
 
258
            # may knock something else out:
 
259
            check_heads = list(heads.keys())
 
260
            for head in check_heads:
 
261
                if head in ancestors:
 
262
                    # this previously discovered 'head' is not
 
263
                    # really a head - its an ancestor of the newly 
 
264
                    # found head,
 
265
                    heads.pop(head)
 
266
            head_ancestors[ie.revision] = ancestors
 
267
            heads[ie.revision] = ie
201
268
        return heads
202
269
 
203
270
    def get_tar_item(self, root, dp, now, tree):
204
271
        """Get a tarfile item and a file stream for its content."""
205
 
        item = tarfile.TarInfo(pathjoin(root, dp))
 
272
        item = tarfile.TarInfo(osutils.pathjoin(root, dp).encode('utf8'))
206
273
        # TODO: would be cool to actually set it to the timestamp of the
207
274
        # revision it was last changed
208
275
        item.mtime = now
237
304
        """
238
305
        assert isinstance(name, basestring), name
239
306
        if '/' in name or '\\' in name:
240
 
            raise InvalidEntryName(name=name)
 
307
            raise errors.InvalidEntryName(name=name)
241
308
        self.executable = False
242
309
        self.revision = None
243
310
        self.text_sha1 = None
244
311
        self.text_size = None
245
312
        self.file_id = file_id
 
313
        assert isinstance(file_id, (str, None.__class__)), \
 
314
            'bad type %r for %r' % (type(file_id), file_id)
246
315
        self.name = name
247
316
        self.text_id = text_id
248
317
        self.parent_id = parent_id
249
318
        self.symlink_target = None
 
319
        self.reference_revision = None
250
320
 
251
321
    def kind_character(self):
252
322
        """Return a short kind indicator useful for appending to names."""
253
323
        raise BzrError('unknown kind %r' % self.kind)
254
324
 
255
 
    known_kinds = ('file', 'directory', 'symlink', 'root_directory')
 
325
    known_kinds = ('file', 'directory', 'symlink')
256
326
 
257
327
    def _put_in_tar(self, item, tree):
258
328
        """populate item for stashing in a tar, and return the content stream.
267
337
        
268
338
        This is a template method - implement _put_on_disk in subclasses.
269
339
        """
270
 
        fullpath = pathjoin(dest, dp)
 
340
        fullpath = osutils.pathjoin(dest, dp)
271
341
        self._put_on_disk(fullpath, tree)
272
 
        mutter("  export {%s} kind %s to %s", self.file_id,
273
 
                self.kind, fullpath)
 
342
        # mutter("  export {%s} kind %s to %s", self.file_id,
 
343
        #         self.kind, fullpath)
274
344
 
275
345
    def _put_on_disk(self, fullpath, tree):
276
346
        """Put this entry onto disk at fullpath, from tree tree."""
277
347
        raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
278
348
 
279
349
    def sorted_children(self):
280
 
        l = self.children.items()
281
 
        l.sort()
282
 
        return l
 
350
        return sorted(self.children.items())
283
351
 
284
352
    @staticmethod
285
353
    def versionable_kind(kind):
286
 
        return kind in ('file', 'directory', 'symlink')
 
354
        return (kind in ('file', 'directory', 'symlink', 'tree-reference'))
287
355
 
288
356
    def check(self, checker, rev_id, inv, tree):
289
357
        """Check this inventory entry is intact.
290
358
 
291
359
        This is a template method, override _check for kind specific
292
360
        tests.
 
361
 
 
362
        :param checker: Check object providing context for the checks; 
 
363
             can be used to find out what parts of the repository have already
 
364
             been checked.
 
365
        :param rev_id: Revision id from which this InventoryEntry was loaded.
 
366
             Not necessarily the last-changed revision for this file.
 
367
        :param inv: Inventory from which the entry was loaded.
 
368
        :param tree: RevisionTree for this entry.
293
369
        """
294
 
        if self.parent_id != None:
 
370
        if self.parent_id is not None:
295
371
            if not inv.has_id(self.parent_id):
296
372
                raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
297
373
                        % (self.parent_id, rev_id))
302
378
        raise BzrCheckError('unknown entry kind %r in revision {%s}' % 
303
379
                            (self.kind, rev_id))
304
380
 
305
 
 
306
381
    def copy(self):
307
382
        """Clone this inventory entry."""
308
383
        raise NotImplementedError
309
384
 
310
 
    def _get_snapshot_change(self, previous_entries):
311
 
        if len(previous_entries) > 1:
312
 
            return 'merged'
313
 
        elif len(previous_entries) == 0:
 
385
    @staticmethod
 
386
    def describe_change(old_entry, new_entry):
 
387
        """Describe the change between old_entry and this.
 
388
        
 
389
        This smells of being an InterInventoryEntry situation, but as its
 
390
        the first one, we're making it a static method for now.
 
391
 
 
392
        An entry with a different parent, or different name is considered 
 
393
        to be renamed. Reparenting is an internal detail.
 
394
        Note that renaming the parent does not trigger a rename for the
 
395
        child entry itself.
 
396
        """
 
397
        # TODO: Perhaps return an object rather than just a string
 
398
        if old_entry is new_entry:
 
399
            # also the case of both being None
 
400
            return 'unchanged'
 
401
        elif old_entry is None:
314
402
            return 'added'
315
 
        else:
316
 
            return 'modified/renamed/reparented'
 
403
        elif new_entry is None:
 
404
            return 'removed'
 
405
        if old_entry.kind != new_entry.kind:
 
406
            return 'modified'
 
407
        text_modified, meta_modified = new_entry.detect_changes(old_entry)
 
408
        if text_modified or meta_modified:
 
409
            modified = True
 
410
        else:
 
411
            modified = False
 
412
        # TODO 20060511 (mbp, rbc) factor out 'detect_rename' here.
 
413
        if old_entry.parent_id != new_entry.parent_id:
 
414
            renamed = True
 
415
        elif old_entry.name != new_entry.name:
 
416
            renamed = True
 
417
        else:
 
418
            renamed = False
 
419
        if renamed and not modified:
 
420
            return InventoryEntry.RENAMED
 
421
        if modified and not renamed:
 
422
            return 'modified'
 
423
        if modified and renamed:
 
424
            return InventoryEntry.MODIFIED_AND_RENAMED
 
425
        return 'unchanged'
317
426
 
318
427
    def __repr__(self):
319
 
        return ("%s(%r, %r, parent_id=%r)"
 
428
        return ("%s(%r, %r, parent_id=%r, revision=%r)"
320
429
                % (self.__class__.__name__,
321
430
                   self.file_id,
322
431
                   self.name,
323
 
                   self.parent_id))
324
 
 
325
 
    def snapshot(self, revision, path, previous_entries,
326
 
                 work_tree, weave_store, transaction):
327
 
        """Make a snapshot of this entry which may or may not have changed.
328
 
        
329
 
        This means that all its fields are populated, that it has its
330
 
        text stored in the text store or weave.
331
 
        """
332
 
        mutter('new parents of %s are %r', path, previous_entries)
333
 
        self._read_tree_state(path, work_tree)
334
 
        if len(previous_entries) == 1:
335
 
            # cannot be unchanged unless there is only one parent file rev.
336
 
            parent_ie = previous_entries.values()[0]
337
 
            if self._unchanged(parent_ie):
338
 
                mutter("found unchanged entry")
339
 
                self.revision = parent_ie.revision
340
 
                return "unchanged"
341
 
        return self.snapshot_revision(revision, previous_entries, 
342
 
                                      work_tree, weave_store, transaction)
343
 
 
344
 
    def snapshot_revision(self, revision, previous_entries, work_tree,
345
 
                          weave_store, transaction):
346
 
        """Record this revision unconditionally."""
347
 
        mutter('new revision for {%s}', self.file_id)
348
 
        self.revision = revision
349
 
        change = self._get_snapshot_change(previous_entries)
350
 
        self._snapshot_text(previous_entries, work_tree, weave_store,
351
 
                            transaction)
352
 
        return change
353
 
 
354
 
    def _snapshot_text(self, file_parents, work_tree, weave_store, transaction): 
355
 
        """Record the 'text' of this entry, whatever form that takes.
356
 
        
357
 
        This default implementation simply adds an empty text.
358
 
        """
359
 
        mutter('storing file {%s} in revision {%s}',
360
 
               self.file_id, self.revision)
361
 
        self._add_text_to_weave([], file_parents, weave_store, transaction)
 
432
                   self.parent_id,
 
433
                   self.revision))
362
434
 
363
435
    def __eq__(self, other):
364
436
        if not isinstance(other, InventoryEntry):
374
446
                and (self.kind == other.kind)
375
447
                and (self.revision == other.revision)
376
448
                and (self.executable == other.executable)
 
449
                and (self.reference_revision == other.reference_revision)
377
450
                )
378
451
 
379
452
    def __ne__(self, other):
385
458
    def _unchanged(self, previous_ie):
386
459
        """Has this entry changed relative to previous_ie.
387
460
 
388
 
        This method should be overriden in child classes.
 
461
        This method should be overridden in child classes.
389
462
        """
390
463
        compatible = True
391
464
        # different inv parent
394
467
        # renamed
395
468
        elif previous_ie.name != self.name:
396
469
            compatible = False
 
470
        elif previous_ie.kind != self.kind:
 
471
            compatible = False
397
472
        return compatible
398
473
 
399
474
    def _read_tree_state(self, path, work_tree):
407
482
        # first requested, or preload them if they're already known
408
483
        pass            # nothing to do by default
409
484
 
 
485
    def _forget_tree_state(self):
 
486
        pass
 
487
 
410
488
 
411
489
class RootEntry(InventoryEntry):
412
490
 
 
491
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
492
                 'text_id', 'parent_id', 'children', 'executable',
 
493
                 'revision', 'symlink_target', 'reference_revision']
 
494
 
413
495
    def _check(self, checker, rev_id, tree):
414
496
        """See InventoryEntry._check"""
415
497
 
416
498
    def __init__(self, file_id):
417
499
        self.file_id = file_id
418
500
        self.children = {}
419
 
        self.kind = 'root_directory'
 
501
        self.kind = 'directory'
420
502
        self.parent_id = None
421
503
        self.name = u''
 
504
        self.revision = None
 
505
        symbol_versioning.warn('RootEntry is deprecated as of bzr 0.10.'
 
506
                               '  Please use InventoryDirectory instead.',
 
507
                               DeprecationWarning, stacklevel=2)
422
508
 
423
509
    def __eq__(self, other):
424
510
        if not isinstance(other, RootEntry):
431
517
class InventoryDirectory(InventoryEntry):
432
518
    """A directory in an inventory."""
433
519
 
 
520
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
521
                 'text_id', 'parent_id', 'children', 'executable',
 
522
                 'revision', 'symlink_target', 'reference_revision']
 
523
 
434
524
    def _check(self, checker, rev_id, tree):
435
525
        """See InventoryEntry._check"""
436
 
        if self.text_sha1 != None or self.text_size != None or self.text_id != None:
 
526
        if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
437
527
            raise BzrCheckError('directory {%s} has text in revision {%s}'
438
528
                                % (self.file_id, rev_id))
439
529
 
470
560
class InventoryFile(InventoryEntry):
471
561
    """A file in an inventory."""
472
562
 
473
 
    def _check(self, checker, rev_id, tree):
 
563
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
564
                 'text_id', 'parent_id', 'children', 'executable',
 
565
                 'revision', 'symlink_target', 'reference_revision']
 
566
 
 
567
    def _check(self, checker, tree_revision_id, tree):
474
568
        """See InventoryEntry._check"""
475
 
        revision = self.revision
476
 
        t = (self.file_id, revision)
 
569
        t = (self.file_id, self.revision)
477
570
        if t in checker.checked_texts:
478
 
            prev_sha = checker.checked_texts[t] 
 
571
            prev_sha = checker.checked_texts[t]
479
572
            if prev_sha != self.text_sha1:
480
573
                raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
481
 
                                    (self.file_id, rev_id))
 
574
                                    (self.file_id, tree_revision_id))
482
575
            else:
483
576
                checker.repeated_text_cnt += 1
484
577
                return
485
578
 
486
579
        if self.file_id not in checker.checked_weaves:
487
580
            mutter('check weave {%s}', self.file_id)
488
 
            w = tree.get_weave(self.file_id)
 
581
            w = tree._get_weave(self.file_id)
489
582
            # Not passing a progress bar, because it creates a new
490
583
            # progress, which overwrites the current progress,
491
584
            # and doesn't look nice
492
585
            w.check()
493
586
            checker.checked_weaves[self.file_id] = True
494
587
        else:
495
 
            w = tree.get_weave_prelude(self.file_id)
 
588
            w = tree._get_weave(self.file_id)
496
589
 
497
 
        mutter('check version {%s} of {%s}', rev_id, self.file_id)
498
 
        checker.checked_text_cnt += 1 
 
590
        mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
 
591
        checker.checked_text_cnt += 1
499
592
        # We can't check the length, because Weave doesn't store that
500
593
        # information, and the whole point of looking at the weave's
501
594
        # sha1sum is that we don't have to extract the text.
515
608
 
516
609
    def detect_changes(self, old_entry):
517
610
        """See InventoryEntry.detect_changes."""
518
 
        assert self.text_sha1 != None
519
 
        assert old_entry.text_sha1 != None
 
611
        assert self.text_sha1 is not None
 
612
        assert old_entry.text_sha1 is not None
520
613
        text_modified = (self.text_sha1 != old_entry.text_sha1)
521
614
        meta_modified = (self.executable != old_entry.executable)
522
615
        return text_modified, meta_modified
524
617
    def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
525
618
             output_to, reverse=False):
526
619
        """See InventoryEntry._diff."""
527
 
        from_text = tree.get_file(self.file_id).readlines()
528
 
        if to_entry:
529
 
            to_text = to_tree.get_file(to_entry.file_id).readlines()
530
 
        else:
531
 
            to_text = []
532
 
        if not reverse:
533
 
            text_diff(from_label, from_text,
534
 
                      to_label, to_text, output_to)
535
 
        else:
536
 
            text_diff(to_label, to_text,
537
 
                      from_label, from_text, output_to)
 
620
        try:
 
621
            from_text = tree.get_file(self.file_id).readlines()
 
622
            if to_entry:
 
623
                to_text = to_tree.get_file(to_entry.file_id).readlines()
 
624
            else:
 
625
                to_text = []
 
626
            if not reverse:
 
627
                text_diff(from_label, from_text,
 
628
                          to_label, to_text, output_to)
 
629
            else:
 
630
                text_diff(to_label, to_text,
 
631
                          from_label, from_text, output_to)
 
632
        except errors.BinaryFile:
 
633
            if reverse:
 
634
                label_pair = (to_label, from_label)
 
635
            else:
 
636
                label_pair = (from_label, to_label)
 
637
            print >> output_to, \
 
638
                  ("Binary files %s and %s differ" % label_pair).encode('utf8')
538
639
 
539
640
    def has_text(self):
540
641
        """See InventoryEntry.has_text."""
561
662
 
562
663
    def _put_on_disk(self, fullpath, tree):
563
664
        """See InventoryEntry._put_on_disk."""
564
 
        pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
 
665
        osutils.pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
565
666
        if tree.is_executable(self.file_id):
566
667
            os.chmod(fullpath, 0755)
567
668
 
568
669
    def _read_tree_state(self, path, work_tree):
569
670
        """See InventoryEntry._read_tree_state."""
570
 
        self.text_sha1 = work_tree.get_file_sha1(self.file_id)
571
 
        self.executable = work_tree.is_executable(self.file_id)
572
 
 
573
 
    def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
574
 
        """See InventoryEntry._snapshot_text."""
575
 
        mutter('storing file {%s} in revision {%s}',
576
 
               self.file_id, self.revision)
577
 
        # special case to avoid diffing on renames or 
578
 
        # reparenting
579
 
        if (len(file_parents) == 1
580
 
            and self.text_sha1 == file_parents.values()[0].text_sha1
581
 
            and self.text_size == file_parents.values()[0].text_size):
582
 
            previous_ie = file_parents.values()[0]
583
 
            weave_store.add_identical_text(
584
 
                self.file_id, previous_ie.revision, 
585
 
                self.revision, file_parents, transaction)
586
 
        else:
587
 
            new_lines = work_tree.get_file(self.file_id).readlines()
588
 
            self._add_text_to_weave(new_lines, file_parents, weave_store,
589
 
                                    transaction)
590
 
            self.text_sha1 = sha_strings(new_lines)
591
 
            self.text_size = sum(map(len, new_lines))
592
 
 
 
671
        self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
 
672
        # FIXME: 20050930 probe for the text size when getting sha1
 
673
        # in _read_tree_state
 
674
        self.executable = work_tree.is_executable(self.file_id, path=path)
 
675
 
 
676
    def __repr__(self):
 
677
        return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s)"
 
678
                % (self.__class__.__name__,
 
679
                   self.file_id,
 
680
                   self.name,
 
681
                   self.parent_id,
 
682
                   self.text_sha1,
 
683
                   self.text_size))
 
684
 
 
685
    def _forget_tree_state(self):
 
686
        self.text_sha1 = None
593
687
 
594
688
    def _unchanged(self, previous_ie):
595
689
        """See InventoryEntry._unchanged."""
608
702
class InventoryLink(InventoryEntry):
609
703
    """A file in an inventory."""
610
704
 
611
 
    __slots__ = ['symlink_target']
 
705
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
706
                 'text_id', 'parent_id', 'children', 'executable',
 
707
                 'revision', 'symlink_target', 'reference_revision']
612
708
 
613
709
    def _check(self, checker, rev_id, tree):
614
710
        """See InventoryEntry._check"""
615
 
        if self.text_sha1 != None or self.text_size != None or self.text_id != None:
 
711
        if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
616
712
            raise BzrCheckError('symlink {%s} has text in revision {%s}'
617
713
                    % (self.file_id, rev_id))
618
 
        if self.symlink_target == None:
 
714
        if self.symlink_target is None:
619
715
            raise BzrCheckError('symlink {%s} has no target in revision {%s}'
620
716
                    % (self.file_id, rev_id))
621
717
 
679
775
        """See InventoryEntry._read_tree_state."""
680
776
        self.symlink_target = work_tree.get_symlink_target(self.file_id)
681
777
 
 
778
    def _forget_tree_state(self):
 
779
        self.symlink_target = None
 
780
 
682
781
    def _unchanged(self, previous_ie):
683
782
        """See InventoryEntry._unchanged."""
684
783
        compatible = super(InventoryLink, self)._unchanged(previous_ie)
687
786
        return compatible
688
787
 
689
788
 
 
789
class TreeReference(InventoryEntry):
 
790
    
 
791
    kind = 'tree-reference'
 
792
    
 
793
    def __init__(self, file_id, name, parent_id, revision=None,
 
794
                 reference_revision=None):
 
795
        InventoryEntry.__init__(self, file_id, name, parent_id)
 
796
        self.revision = revision
 
797
        self.reference_revision = reference_revision
 
798
 
 
799
    def copy(self):
 
800
        return TreeReference(self.file_id, self.name, self.parent_id,
 
801
                             self.revision, self.reference_revision)
 
802
 
 
803
    def _read_tree_state(self, path, work_tree):
 
804
        """Populate fields in the inventory entry from the given tree.
 
805
        """
 
806
        self.reference_revision = work_tree.get_reference_revision(
 
807
            self.file_id, path)
 
808
 
 
809
    def _forget_tree_state(self):
 
810
        self.reference_revision = None 
 
811
 
 
812
    def _unchanged(self, previous_ie):
 
813
        """See InventoryEntry._unchanged."""
 
814
        compatible = super(TreeReference, self)._unchanged(previous_ie)
 
815
        if self.reference_revision != previous_ie.reference_revision:
 
816
            compatible = False
 
817
        return compatible
 
818
 
 
819
 
690
820
class Inventory(object):
691
821
    """Inventory of versioned files in a tree.
692
822
 
706
836
 
707
837
    >>> inv = Inventory()
708
838
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
709
 
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT')
 
839
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
710
840
    >>> inv['123-123'].name
711
841
    'hello.c'
712
842
 
720
850
    May also look up by name:
721
851
 
722
852
    >>> [x[0] for x in inv.iter_entries()]
723
 
    ['hello.c']
 
853
    ['', u'hello.c']
724
854
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
725
855
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
726
 
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
 
856
    Traceback (most recent call last):
 
857
    BzrError: parent_id {TREE_ROOT} not in inventory
 
858
    >>> inv.add(InventoryFile('123-123', 'hello.c', 'TREE_ROOT-12345678-12345678'))
 
859
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None)
727
860
    """
728
 
    def __init__(self, root_id=ROOT_ID):
 
861
    def __init__(self, root_id=ROOT_ID, revision_id=None):
729
862
        """Create or read an inventory.
730
863
 
731
864
        If a working directory is specified, the inventory is read
735
868
        The inventory is created with a default root directory, with
736
869
        an id of None.
737
870
        """
738
 
        # We are letting Branch.initialize() create a unique inventory
739
 
        # root id. Rather than generating a random one here.
740
 
        #if root_id is None:
741
 
        #    root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
742
 
        self.root = RootEntry(root_id)
 
871
        if root_id is not None:
 
872
            assert root_id.__class__ == str
 
873
            self._set_root(InventoryDirectory(root_id, u'', None))
 
874
        else:
 
875
            self.root = None
 
876
            self._byid = {}
 
877
        self.revision_id = revision_id
 
878
 
 
879
    def __repr__(self):
 
880
        return "<Inventory object at %x, contents=%r>" % (id(self), self._byid)
 
881
 
 
882
    def apply_delta(self, delta):
 
883
        """Apply a delta to this inventory.
 
884
 
 
885
        :param delta: A list of changes to apply. After all the changes are
 
886
            applied the final inventory must be internally consistent, but it
 
887
            is ok to supply changes which, if only half-applied would have an
 
888
            invalid result - such as supplying two changes which rename two
 
889
            files, 'A' and 'B' with each other : [('A', 'B', 'A-id', a_entry),
 
890
            ('B', 'A', 'B-id', b_entry)].
 
891
 
 
892
            Each change is a tuple, of the form (old_path, new_path, file_id,
 
893
            new_entry).
 
894
            
 
895
            When new_path is None, the change indicates the removal of an entry
 
896
            from the inventory and new_entry will be ignored (using None is
 
897
            appropriate). If new_path is not None, then new_entry must be an
 
898
            InventoryEntry instance, which will be incorporated into the
 
899
            inventory (and replace any existing entry with the same file id).
 
900
            
 
901
            When old_path is None, the change indicates the addition of
 
902
            a new entry to the inventory.
 
903
            
 
904
            When neither new_path nor old_path are None, the change is a
 
905
            modification to an entry, such as a rename, reparent, kind change
 
906
            etc. 
 
907
 
 
908
            The children attribute of new_entry is ignored. This is because
 
909
            this method preserves children automatically across alterations to
 
910
            the parent of the children, and cases where the parent id of a
 
911
            child is changing require the child to be passed in as a separate
 
912
            change regardless. E.g. in the recursive deletion of a directory -
 
913
            the directory's children must be included in the delta, or the
 
914
            final inventory will be invalid.
 
915
        """
 
916
        children = {}
 
917
        # Remove all affected items which were in the original inventory,
 
918
        # starting with the longest paths, thus ensuring parents are examined
 
919
        # after their children, which means that everything we examine has no
 
920
        # modified children remaining by the time we examine it.
 
921
        for old_path, file_id in sorted(((op, f) for op, np, f, e in delta
 
922
                                        if op is not None), reverse=True):
 
923
            if file_id not in self:
 
924
                # adds come later
 
925
                continue
 
926
            # Preserve unaltered children of file_id for later reinsertion.
 
927
            children[file_id] = getattr(self[file_id], 'children', {})
 
928
            # Remove file_id and the unaltered children. If file_id is not
 
929
            # being deleted it will be reinserted back later.
 
930
            self.remove_recursive_id(file_id)
 
931
        # Insert all affected which should be in the new inventory, reattaching
 
932
        # their children if they had any. This is done from shortest path to
 
933
        # longest, ensuring that items which were modified and whose parents in
 
934
        # the resulting inventory were also modified, are inserted after their
 
935
        # parents.
 
936
        for new_path, new_entry in sorted((np, e) for op, np, f, e in
 
937
                                          delta if np is not None):
 
938
            if new_entry.kind == 'directory':
 
939
                new_entry.children = children.get(new_entry.file_id, {})
 
940
            self.add(new_entry)
 
941
 
 
942
    def _set_root(self, ie):
 
943
        self.root = ie
743
944
        self._byid = {self.root.file_id: self.root}
744
945
 
745
 
 
746
946
    def copy(self):
747
 
        other = Inventory(self.root.file_id)
 
947
        # TODO: jam 20051218 Should copy also copy the revision_id?
 
948
        entries = self.iter_entries()
 
949
        other = Inventory(entries.next()[1].file_id)
748
950
        # copy recursively so we know directories will be added before
749
951
        # their children.  There are more efficient ways than this...
750
 
        for path, entry in self.iter_entries():
751
 
            if entry == self.root:
752
 
                continue
 
952
        for path, entry in entries():
753
953
            other.add(entry.copy())
754
954
        return other
755
955
 
756
 
 
757
956
    def __iter__(self):
758
957
        return iter(self._byid)
759
958
 
760
 
 
761
959
    def __len__(self):
762
960
        """Returns number of entries."""
763
961
        return len(self._byid)
764
962
 
765
 
 
766
963
    def iter_entries(self, from_dir=None):
767
964
        """Return (path, entry) pairs, in order by name."""
768
 
        if from_dir == None:
769
 
            assert self.root
770
 
            from_dir = self.root
771
 
        elif isinstance(from_dir, basestring):
772
 
            from_dir = self._byid[from_dir]
773
 
            
774
 
        kids = from_dir.children.items()
775
 
        kids.sort()
776
 
        for name, ie in kids:
777
 
            yield name, ie
778
 
            if ie.kind == 'directory':
779
 
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
780
 
                    yield pathjoin(name, cn), cie
781
 
 
 
965
        if from_dir is None:
 
966
            if self.root is None:
 
967
                return
 
968
            from_dir = self.root
 
969
            yield '', self.root
 
970
        elif isinstance(from_dir, basestring):
 
971
            from_dir = self._byid[from_dir]
 
972
            
 
973
        # unrolling the recursive called changed the time from
 
974
        # 440ms/663ms (inline/total) to 116ms/116ms
 
975
        children = from_dir.children.items()
 
976
        children.sort()
 
977
        children = collections.deque(children)
 
978
        stack = [(u'', children)]
 
979
        while stack:
 
980
            from_dir_relpath, children = stack[-1]
 
981
 
 
982
            while children:
 
983
                name, ie = children.popleft()
 
984
 
 
985
                # we know that from_dir_relpath never ends in a slash
 
986
                # and 'f' doesn't begin with one, we can do a string op, rather
 
987
                # than the checks of pathjoin(), though this means that all paths
 
988
                # start with a slash
 
989
                path = from_dir_relpath + '/' + name
 
990
 
 
991
                yield path[1:], ie
 
992
 
 
993
                if ie.kind != 'directory':
 
994
                    continue
 
995
 
 
996
                # But do this child first
 
997
                new_children = ie.children.items()
 
998
                new_children.sort()
 
999
                new_children = collections.deque(new_children)
 
1000
                stack.append((path, new_children))
 
1001
                # Break out of inner loop, so that we start outer loop with child
 
1002
                break
 
1003
            else:
 
1004
                # if we finished all children, pop it off the stack
 
1005
                stack.pop()
 
1006
 
 
1007
    def iter_entries_by_dir(self, from_dir=None, specific_file_ids=None,
 
1008
        yield_parents=False):
 
1009
        """Iterate over the entries in a directory first order.
 
1010
 
 
1011
        This returns all entries for a directory before returning
 
1012
        the entries for children of a directory. This is not
 
1013
        lexicographically sorted order, and is a hybrid between
 
1014
        depth-first and breadth-first.
 
1015
 
 
1016
        :param yield_parents: If True, yield the parents from the root leading
 
1017
            down to specific_file_ids that have been requested. This has no
 
1018
            impact if specific_file_ids is None.
 
1019
        :return: This yields (path, entry) pairs
 
1020
        """
 
1021
        if specific_file_ids:
 
1022
            safe = osutils.safe_file_id
 
1023
            specific_file_ids = set(safe(fid) for fid in specific_file_ids)
 
1024
        # TODO? Perhaps this should return the from_dir so that the root is
 
1025
        # yielded? or maybe an option?
 
1026
        if from_dir is None:
 
1027
            if self.root is None:
 
1028
                return
 
1029
            # Optimize a common case
 
1030
            if (not yield_parents and specific_file_ids is not None and
 
1031
                len(specific_file_ids) == 1):
 
1032
                file_id = list(specific_file_ids)[0]
 
1033
                if file_id in self:
 
1034
                    yield self.id2path(file_id), self[file_id]
 
1035
                return 
 
1036
            from_dir = self.root
 
1037
            if (specific_file_ids is None or yield_parents or
 
1038
                self.root.file_id in specific_file_ids):
 
1039
                yield u'', self.root
 
1040
        elif isinstance(from_dir, basestring):
 
1041
            from_dir = self._byid[from_dir]
 
1042
 
 
1043
        if specific_file_ids is not None:
 
1044
            # TODO: jam 20070302 This could really be done as a loop rather
 
1045
            #       than a bunch of recursive calls.
 
1046
            parents = set()
 
1047
            byid = self._byid
 
1048
            def add_ancestors(file_id):
 
1049
                if file_id not in byid:
 
1050
                    return
 
1051
                parent_id = byid[file_id].parent_id
 
1052
                if parent_id is None:
 
1053
                    return
 
1054
                if parent_id not in parents:
 
1055
                    parents.add(parent_id)
 
1056
                    add_ancestors(parent_id)
 
1057
            for file_id in specific_file_ids:
 
1058
                add_ancestors(file_id)
 
1059
        else:
 
1060
            parents = None
 
1061
            
 
1062
        stack = [(u'', from_dir)]
 
1063
        while stack:
 
1064
            cur_relpath, cur_dir = stack.pop()
 
1065
 
 
1066
            child_dirs = []
 
1067
            for child_name, child_ie in sorted(cur_dir.children.iteritems()):
 
1068
 
 
1069
                child_relpath = cur_relpath + child_name
 
1070
 
 
1071
                if (specific_file_ids is None or 
 
1072
                    child_ie.file_id in specific_file_ids or
 
1073
                    (yield_parents and child_ie.file_id in parents)):
 
1074
                    yield child_relpath, child_ie
 
1075
 
 
1076
                if child_ie.kind == 'directory':
 
1077
                    if parents is None or child_ie.file_id in parents:
 
1078
                        child_dirs.append((child_relpath+'/', child_ie))
 
1079
            stack.extend(reversed(child_dirs))
 
1080
 
 
1081
    def make_entry(self, kind, name, parent_id, file_id=None):
 
1082
        """Simple thunk to bzrlib.inventory.make_entry."""
 
1083
        return make_entry(kind, name, parent_id, file_id)
782
1084
 
783
1085
    def entries(self):
784
1086
        """Return list of (path, ie) for all entries except the root.
790
1092
            kids = dir_ie.children.items()
791
1093
            kids.sort()
792
1094
            for name, ie in kids:
793
 
                child_path = pathjoin(dir_path, name)
 
1095
                child_path = osutils.pathjoin(dir_path, name)
794
1096
                accum.append((child_path, ie))
795
1097
                if ie.kind == 'directory':
796
1098
                    descend(ie, child_path)
798
1100
        descend(self.root, u'')
799
1101
        return accum
800
1102
 
801
 
 
802
1103
    def directories(self):
803
1104
        """Return (path, entry) pairs for all directories, including the root.
804
1105
        """
810
1111
            kids.sort()
811
1112
 
812
1113
            for name, child_ie in kids:
813
 
                child_path = pathjoin(parent_path, name)
 
1114
                child_path = osutils.pathjoin(parent_path, name)
814
1115
                descend(child_ie, child_path)
815
1116
        descend(self.root, u'')
816
1117
        return accum
817
1118
        
818
 
 
819
 
 
820
1119
    def __contains__(self, file_id):
821
1120
        """True if this entry contains a file with given id.
822
1121
 
823
1122
        >>> inv = Inventory()
824
1123
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
825
 
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
 
1124
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
826
1125
        >>> '123' in inv
827
1126
        True
828
1127
        >>> '456' in inv
829
1128
        False
830
1129
        """
831
 
        return file_id in self._byid
832
 
 
 
1130
        file_id = osutils.safe_file_id(file_id)
 
1131
        return (file_id in self._byid)
833
1132
 
834
1133
    def __getitem__(self, file_id):
835
1134
        """Return the entry for given file_id.
836
1135
 
837
1136
        >>> inv = Inventory()
838
1137
        >>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
839
 
        InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT')
 
1138
        InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
840
1139
        >>> inv['123123'].name
841
1140
        'hello.c'
842
1141
        """
 
1142
        file_id = osutils.safe_file_id(file_id)
843
1143
        try:
844
1144
            return self._byid[file_id]
845
1145
        except KeyError:
846
 
            if file_id == None:
847
 
                raise BzrError("can't look up file_id None")
848
 
            else:
849
 
                raise BzrError("file_id {%s} not in inventory" % file_id)
850
 
 
 
1146
            # really we're passing an inventory, not a tree...
 
1147
            raise errors.NoSuchId(self, file_id)
851
1148
 
852
1149
    def get_file_kind(self, file_id):
 
1150
        file_id = osutils.safe_file_id(file_id)
853
1151
        return self._byid[file_id].kind
854
1152
 
855
1153
    def get_child(self, parent_id, filename):
 
1154
        parent_id = osutils.safe_file_id(parent_id)
856
1155
        return self[parent_id].children.get(filename)
857
1156
 
 
1157
    def _add_child(self, entry):
 
1158
        """Add an entry to the inventory, without adding it to its parent"""
 
1159
        if entry.file_id in self._byid:
 
1160
            raise BzrError("inventory already contains entry with id {%s}" %
 
1161
                           entry.file_id)
 
1162
        self._byid[entry.file_id] = entry
 
1163
        for child in getattr(entry, 'children', {}).itervalues():
 
1164
            self._add_child(child)
 
1165
        return entry
858
1166
 
859
1167
    def add(self, entry):
860
1168
        """Add entry to inventory.
865
1173
        Returns the new entry object.
866
1174
        """
867
1175
        if entry.file_id in self._byid:
868
 
            raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
869
 
 
870
 
        if entry.parent_id == ROOT_ID or entry.parent_id is None:
871
 
            entry.parent_id = self.root.file_id
872
 
 
873
 
        try:
874
 
            parent = self._byid[entry.parent_id]
875
 
        except KeyError:
876
 
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
877
 
 
878
 
        if parent.children.has_key(entry.name):
879
 
            raise BzrError("%s is already versioned" %
880
 
                    pathjoin(self.id2path(parent.file_id), entry.name))
881
 
 
882
 
        self._byid[entry.file_id] = entry
883
 
        parent.children[entry.name] = entry
884
 
        return entry
885
 
 
886
 
 
887
 
    def add_path(self, relpath, kind, file_id=None):
 
1176
            raise errors.DuplicateFileId(entry.file_id,
 
1177
                                         self._byid[entry.file_id])
 
1178
 
 
1179
        if entry.parent_id is None:
 
1180
            assert self.root is None and len(self._byid) == 0
 
1181
            self.root = entry
 
1182
        else:
 
1183
            try:
 
1184
                parent = self._byid[entry.parent_id]
 
1185
            except KeyError:
 
1186
                raise BzrError("parent_id {%s} not in inventory" %
 
1187
                               entry.parent_id)
 
1188
 
 
1189
            if entry.name in parent.children:
 
1190
                raise BzrError("%s is already versioned" %
 
1191
                        osutils.pathjoin(self.id2path(parent.file_id),
 
1192
                        entry.name).encode('utf-8'))
 
1193
            parent.children[entry.name] = entry
 
1194
        return self._add_child(entry)
 
1195
 
 
1196
    def add_path(self, relpath, kind, file_id=None, parent_id=None):
888
1197
        """Add entry from a path.
889
1198
 
890
1199
        The immediate parent must already be versioned.
891
1200
 
892
1201
        Returns the new entry object."""
893
 
        from bzrlib.workingtree import gen_file_id
894
1202
        
895
 
        parts = bzrlib.osutils.splitpath(relpath)
 
1203
        parts = osutils.splitpath(relpath)
 
1204
 
896
1205
        if len(parts) == 0:
897
 
            raise BzrError("cannot re-add root of inventory")
898
 
 
899
 
        if file_id == None:
900
 
            file_id = gen_file_id(relpath)
901
 
 
902
 
        parent_path = parts[:-1]
903
 
        parent_id = self.path2id(parent_path)
904
 
        if parent_id == None:
905
 
            raise NotVersionedError(path=parent_path)
906
 
        if kind == 'directory':
907
 
            ie = InventoryDirectory(file_id, parts[-1], parent_id)
908
 
        elif kind == 'file':
909
 
            ie = InventoryFile(file_id, parts[-1], parent_id)
910
 
        elif kind == 'symlink':
911
 
            ie = InventoryLink(file_id, parts[-1], parent_id)
 
1206
            if file_id is None:
 
1207
                file_id = generate_ids.gen_root_id()
 
1208
            else:
 
1209
                file_id = osutils.safe_file_id(file_id)
 
1210
            self.root = InventoryDirectory(file_id, '', None)
 
1211
            self._byid = {self.root.file_id: self.root}
 
1212
            return self.root
912
1213
        else:
913
 
            raise BzrError("unknown kind %r" % kind)
 
1214
            parent_path = parts[:-1]
 
1215
            parent_id = self.path2id(parent_path)
 
1216
            if parent_id is None:
 
1217
                raise errors.NotVersionedError(path=parent_path)
 
1218
        ie = make_entry(kind, parts[-1], parent_id, file_id)
914
1219
        return self.add(ie)
915
1220
 
916
 
 
917
1221
    def __delitem__(self, file_id):
918
1222
        """Remove entry by id.
919
1223
 
920
1224
        >>> inv = Inventory()
921
1225
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
922
 
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
 
1226
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
923
1227
        >>> '123' in inv
924
1228
        True
925
1229
        >>> del inv['123']
926
1230
        >>> '123' in inv
927
1231
        False
928
1232
        """
 
1233
        file_id = osutils.safe_file_id(file_id)
929
1234
        ie = self[file_id]
930
1235
 
931
 
        assert self[ie.parent_id].children[ie.name] == ie
 
1236
        assert ie.parent_id is None or \
 
1237
            self[ie.parent_id].children[ie.name] == ie
932
1238
        
933
 
        # TODO: Test deleting all children; maybe hoist to a separate
934
 
        # deltree method?
935
 
        if ie.kind == 'directory':
936
 
            for cie in ie.children.values():
937
 
                del self[cie.file_id]
938
 
            del ie.children
939
 
 
940
1239
        del self._byid[file_id]
941
 
        del self[ie.parent_id].children[ie.name]
942
 
 
 
1240
        if ie.parent_id is not None:
 
1241
            del self[ie.parent_id].children[ie.name]
943
1242
 
944
1243
    def __eq__(self, other):
945
1244
        """Compare two sets by comparing their contents.
949
1248
        >>> i1 == i2
950
1249
        True
951
1250
        >>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
952
 
        InventoryFile('123', 'foo', parent_id='TREE_ROOT')
 
1251
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
953
1252
        >>> i1 == i2
954
1253
        False
955
1254
        >>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
956
 
        InventoryFile('123', 'foo', parent_id='TREE_ROOT')
 
1255
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
957
1256
        >>> i1 == i2
958
1257
        True
959
1258
        """
960
1259
        if not isinstance(other, Inventory):
961
1260
            return NotImplemented
962
1261
 
963
 
        if len(self._byid) != len(other._byid):
964
 
            # shortcut: obviously not the same
965
 
            return False
966
 
 
967
1262
        return self._byid == other._byid
968
1263
 
969
 
 
970
1264
    def __ne__(self, other):
971
1265
        return not self.__eq__(other)
972
1266
 
973
 
 
974
1267
    def __hash__(self):
975
1268
        raise ValueError('not hashable')
976
1269
 
 
1270
    def _iter_file_id_parents(self, file_id):
 
1271
        """Yield the parents of file_id up to the root."""
 
1272
        file_id = osutils.safe_file_id(file_id)
 
1273
        while file_id is not None:
 
1274
            try:
 
1275
                ie = self._byid[file_id]
 
1276
            except KeyError:
 
1277
                raise errors.NoSuchId(tree=None, file_id=file_id)
 
1278
            yield ie
 
1279
            file_id = ie.parent_id
977
1280
 
978
1281
    def get_idpath(self, file_id):
979
1282
        """Return a list of file_ids for the path to an entry.
983
1286
        is equal to the depth of the file in the tree, counting the
984
1287
        root directory as depth 1.
985
1288
        """
 
1289
        file_id = osutils.safe_file_id(file_id)
986
1290
        p = []
987
 
        while file_id != None:
988
 
            try:
989
 
                ie = self._byid[file_id]
990
 
            except KeyError:
991
 
                raise BzrError("file_id {%s} not found in inventory" % file_id)
992
 
            p.insert(0, ie.file_id)
993
 
            file_id = ie.parent_id
 
1291
        for parent in self._iter_file_id_parents(file_id):
 
1292
            p.insert(0, parent.file_id)
994
1293
        return p
995
1294
 
996
 
 
997
1295
    def id2path(self, file_id):
998
 
        """Return as a list the path to file_id.
 
1296
        """Return as a string the path to file_id.
999
1297
        
1000
1298
        >>> i = Inventory()
1001
1299
        >>> e = i.add(InventoryDirectory('src-id', 'src', ROOT_ID))
1003
1301
        >>> print i.id2path('foo-id')
1004
1302
        src/foo.c
1005
1303
        """
 
1304
        file_id = osutils.safe_file_id(file_id)
1006
1305
        # get all names, skipping root
1007
 
        p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
1008
 
        if p:
1009
 
            return pathjoin(*p)
1010
 
        else:
1011
 
            return ''
 
1306
        return '/'.join(reversed(
 
1307
            [parent.name for parent in 
 
1308
             self._iter_file_id_parents(file_id)][:-1]))
1012
1309
            
1013
 
 
1014
 
 
1015
1310
    def path2id(self, name):
1016
1311
        """Walk down through directories to return entry of last component.
1017
1312
 
1021
1316
        This returns the entry of the last component in the path,
1022
1317
        which may be either a file or a directory.
1023
1318
 
1024
 
        Returns None iff the path is not found.
 
1319
        Returns None IFF the path is not found.
1025
1320
        """
1026
 
        if isinstance(name, types.StringTypes):
1027
 
            name = splitpath(name)
 
1321
        if isinstance(name, basestring):
 
1322
            name = osutils.splitpath(name)
1028
1323
 
1029
 
        mutter("lookup path %r" % name)
 
1324
        # mutter("lookup path %r" % name)
1030
1325
 
1031
1326
        parent = self.root
 
1327
        if parent is None:
 
1328
            return None
1032
1329
        for f in name:
1033
1330
            try:
1034
 
                cie = parent.children[f]
 
1331
                children = getattr(parent, 'children', None)
 
1332
                if children is None:
 
1333
                    return None
 
1334
                cie = children[f]
1035
1335
                assert cie.name == f
1036
1336
                assert cie.parent_id == parent.file_id
1037
1337
                parent = cie
1041
1341
 
1042
1342
        return parent.file_id
1043
1343
 
1044
 
 
1045
1344
    def has_filename(self, names):
1046
1345
        return bool(self.path2id(names))
1047
1346
 
1048
 
 
1049
1347
    def has_id(self, file_id):
1050
 
        return self._byid.has_key(file_id)
 
1348
        file_id = osutils.safe_file_id(file_id)
 
1349
        return (file_id in self._byid)
1051
1350
 
 
1351
    def remove_recursive_id(self, file_id):
 
1352
        """Remove file_id, and children, from the inventory.
 
1353
        
 
1354
        :param file_id: A file_id to remove.
 
1355
        """
 
1356
        file_id = osutils.safe_file_id(file_id)
 
1357
        to_find_delete = [self._byid[file_id]]
 
1358
        to_delete = []
 
1359
        while to_find_delete:
 
1360
            ie = to_find_delete.pop()
 
1361
            to_delete.append(ie.file_id)
 
1362
            if ie.kind == 'directory':
 
1363
                to_find_delete.extend(ie.children.values())
 
1364
        for file_id in reversed(to_delete):
 
1365
            ie = self[file_id]
 
1366
            del self._byid[file_id]
 
1367
        if ie.parent_id is not None:
 
1368
            del self[ie.parent_id].children[ie.name]
 
1369
        else:
 
1370
            self.root = None
1052
1371
 
1053
1372
    def rename(self, file_id, new_parent_id, new_name):
1054
1373
        """Move a file within the inventory.
1055
1374
 
1056
1375
        This can change either the name, or the parent, or both.
1057
1376
 
1058
 
        This does not move the working file."""
 
1377
        This does not move the working file.
 
1378
        """
 
1379
        file_id = osutils.safe_file_id(file_id)
 
1380
        new_name = ensure_normalized_name(new_name)
1059
1381
        if not is_valid_name(new_name):
1060
1382
            raise BzrError("not an acceptable filename: %r" % new_name)
1061
1383
 
1079
1401
        file_ie.name = new_name
1080
1402
        file_ie.parent_id = new_parent_id
1081
1403
 
1082
 
 
 
1404
    def is_root(self, file_id):
 
1405
        file_id = osutils.safe_file_id(file_id)
 
1406
        return self.root is not None and file_id == self.root.file_id
 
1407
 
 
1408
 
 
1409
entry_factory = {
 
1410
    'directory': InventoryDirectory,
 
1411
    'file': InventoryFile,
 
1412
    'symlink': InventoryLink,
 
1413
    'tree-reference': TreeReference
 
1414
}
 
1415
 
 
1416
def make_entry(kind, name, parent_id, file_id=None):
 
1417
    """Create an inventory entry.
 
1418
 
 
1419
    :param kind: the type of inventory entry to create.
 
1420
    :param name: the basename of the entry.
 
1421
    :param parent_id: the parent_id of the entry.
 
1422
    :param file_id: the file_id to use. if None, one will be created.
 
1423
    """
 
1424
    if file_id is None:
 
1425
        file_id = generate_ids.gen_file_id(name)
 
1426
    else:
 
1427
        file_id = osutils.safe_file_id(file_id)
 
1428
    name = ensure_normalized_name(name)
 
1429
    try:
 
1430
        factory = entry_factory[kind]
 
1431
    except KeyError:
 
1432
        raise BzrError("unknown kind %r" % kind)
 
1433
    return factory(file_id, name, parent_id)
 
1434
 
 
1435
 
 
1436
def ensure_normalized_name(name):
 
1437
    """Normalize name.
 
1438
 
 
1439
    :raises InvalidNormalization: When name is not normalized, and cannot be
 
1440
        accessed on this platform by the normalized path.
 
1441
    :return: The NFC/NFKC normalised version of name.
 
1442
    """
 
1443
    #------- This has been copied to bzrlib.dirstate.DirState.add, please
 
1444
    # keep them synchronised.
 
1445
    # we dont import normalized_filename directly because we want to be
 
1446
    # able to change the implementation at runtime for tests.
 
1447
    norm_name, can_access = osutils.normalized_filename(name)
 
1448
    if norm_name != name:
 
1449
        if can_access:
 
1450
            return norm_name
 
1451
        else:
 
1452
            # TODO: jam 20060701 This would probably be more useful
 
1453
            #       if the error was raised with the full path
 
1454
            raise errors.InvalidNormalization(name)
 
1455
    return name
1083
1456
 
1084
1457
 
1085
1458
_NAME_RE = None
1086
1459
 
1087
1460
def is_valid_name(name):
1088
1461
    global _NAME_RE
1089
 
    if _NAME_RE == None:
 
1462
    if _NAME_RE is None:
1090
1463
        _NAME_RE = re.compile(r'^[^/\\]+$')
1091
1464
        
1092
1465
    return bool(_NAME_RE.match(name))