~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2007-11-09 19:50:36 UTC
  • mfrom: (2975.2.1 bug-161240)
  • Revision ID: pqm@pqm.ubuntu.com-20071109195036-5o5bwu0a01uniqwg
(robertc) Correct a missing import in the test support ftp server. (Robert Collins, #161240)

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
 
                raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
481
 
                                    (self.file_id, rev_id))
 
573
                raise BzrCheckError(
 
574
                    'mismatched sha1 on {%s} in {%s} (%s != %s) %r' %
 
575
                    (self.file_id, tree_revision_id, prev_sha, self.text_sha1,
 
576
                     t))
482
577
            else:
483
578
                checker.repeated_text_cnt += 1
484
579
                return
485
580
 
486
581
        if self.file_id not in checker.checked_weaves:
487
582
            mutter('check weave {%s}', self.file_id)
488
 
            w = tree.get_weave(self.file_id)
 
583
            w = tree._get_weave(self.file_id)
489
584
            # Not passing a progress bar, because it creates a new
490
585
            # progress, which overwrites the current progress,
491
586
            # and doesn't look nice
492
587
            w.check()
493
588
            checker.checked_weaves[self.file_id] = True
494
589
        else:
495
 
            w = tree.get_weave_prelude(self.file_id)
 
590
            w = tree._get_weave(self.file_id)
496
591
 
497
 
        mutter('check version {%s} of {%s}', rev_id, self.file_id)
498
 
        checker.checked_text_cnt += 1 
 
592
        mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
 
593
        checker.checked_text_cnt += 1
499
594
        # We can't check the length, because Weave doesn't store that
500
595
        # information, and the whole point of looking at the weave's
501
596
        # sha1sum is that we don't have to extract the text.
515
610
 
516
611
    def detect_changes(self, old_entry):
517
612
        """See InventoryEntry.detect_changes."""
518
 
        assert self.text_sha1 != None
519
 
        assert old_entry.text_sha1 != None
 
613
        assert self.text_sha1 is not None
 
614
        assert old_entry.text_sha1 is not None
520
615
        text_modified = (self.text_sha1 != old_entry.text_sha1)
521
616
        meta_modified = (self.executable != old_entry.executable)
522
617
        return text_modified, meta_modified
524
619
    def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
525
620
             output_to, reverse=False):
526
621
        """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)
 
622
        try:
 
623
            from_text = tree.get_file(self.file_id).readlines()
 
624
            if to_entry:
 
625
                to_text = to_tree.get_file(to_entry.file_id).readlines()
 
626
            else:
 
627
                to_text = []
 
628
            if not reverse:
 
629
                text_diff(from_label, from_text,
 
630
                          to_label, to_text, output_to)
 
631
            else:
 
632
                text_diff(to_label, to_text,
 
633
                          from_label, from_text, output_to)
 
634
        except errors.BinaryFile:
 
635
            if reverse:
 
636
                label_pair = (to_label, from_label)
 
637
            else:
 
638
                label_pair = (from_label, to_label)
 
639
            output_to.write(
 
640
                  ("Binary files %s and %s differ\n" % label_pair).encode('utf8'))
538
641
 
539
642
    def has_text(self):
540
643
        """See InventoryEntry.has_text."""
561
664
 
562
665
    def _put_on_disk(self, fullpath, tree):
563
666
        """See InventoryEntry._put_on_disk."""
564
 
        pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
 
667
        osutils.pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
565
668
        if tree.is_executable(self.file_id):
566
669
            os.chmod(fullpath, 0755)
567
670
 
568
671
    def _read_tree_state(self, path, work_tree):
569
672
        """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
 
 
 
673
        self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
 
674
        # FIXME: 20050930 probe for the text size when getting sha1
 
675
        # in _read_tree_state
 
676
        self.executable = work_tree.is_executable(self.file_id, path=path)
 
677
 
 
678
    def __repr__(self):
 
679
        return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s)"
 
680
                % (self.__class__.__name__,
 
681
                   self.file_id,
 
682
                   self.name,
 
683
                   self.parent_id,
 
684
                   self.text_sha1,
 
685
                   self.text_size))
 
686
 
 
687
    def _forget_tree_state(self):
 
688
        self.text_sha1 = None
593
689
 
594
690
    def _unchanged(self, previous_ie):
595
691
        """See InventoryEntry._unchanged."""
608
704
class InventoryLink(InventoryEntry):
609
705
    """A file in an inventory."""
610
706
 
611
 
    __slots__ = ['symlink_target']
 
707
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
708
                 'text_id', 'parent_id', 'children', 'executable',
 
709
                 'revision', 'symlink_target', 'reference_revision']
612
710
 
613
711
    def _check(self, checker, rev_id, tree):
614
712
        """See InventoryEntry._check"""
615
 
        if self.text_sha1 != None or self.text_size != None or self.text_id != None:
 
713
        if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
616
714
            raise BzrCheckError('symlink {%s} has text in revision {%s}'
617
715
                    % (self.file_id, rev_id))
618
 
        if self.symlink_target == None:
 
716
        if self.symlink_target is None:
619
717
            raise BzrCheckError('symlink {%s} has no target in revision {%s}'
620
718
                    % (self.file_id, rev_id))
621
719
 
644
742
                temp = from_text
645
743
                from_text = to_text
646
744
                to_text = temp
647
 
            print >>output_to, '=== target changed %r => %r' % (from_text, to_text)
 
745
            output_to.write('=== target changed %r => %r\n' % (from_text, to_text))
648
746
        else:
649
747
            if not reverse:
650
 
                print >>output_to, '=== target was %r' % self.symlink_target
 
748
                output_to.write('=== target was %r\n' % self.symlink_target)
651
749
            else:
652
 
                print >>output_to, '=== target is %r' % self.symlink_target
 
750
                output_to.write('=== target is %r\n' % self.symlink_target)
653
751
 
654
752
    def __init__(self, file_id, name, parent_id):
655
753
        super(InventoryLink, self).__init__(file_id, name, parent_id)
679
777
        """See InventoryEntry._read_tree_state."""
680
778
        self.symlink_target = work_tree.get_symlink_target(self.file_id)
681
779
 
 
780
    def _forget_tree_state(self):
 
781
        self.symlink_target = None
 
782
 
682
783
    def _unchanged(self, previous_ie):
683
784
        """See InventoryEntry._unchanged."""
684
785
        compatible = super(InventoryLink, self)._unchanged(previous_ie)
687
788
        return compatible
688
789
 
689
790
 
 
791
class TreeReference(InventoryEntry):
 
792
    
 
793
    kind = 'tree-reference'
 
794
    
 
795
    def __init__(self, file_id, name, parent_id, revision=None,
 
796
                 reference_revision=None):
 
797
        InventoryEntry.__init__(self, file_id, name, parent_id)
 
798
        self.revision = revision
 
799
        self.reference_revision = reference_revision
 
800
 
 
801
    def copy(self):
 
802
        return TreeReference(self.file_id, self.name, self.parent_id,
 
803
                             self.revision, self.reference_revision)
 
804
 
 
805
    def _read_tree_state(self, path, work_tree):
 
806
        """Populate fields in the inventory entry from the given tree.
 
807
        """
 
808
        self.reference_revision = work_tree.get_reference_revision(
 
809
            self.file_id, path)
 
810
 
 
811
    def _forget_tree_state(self):
 
812
        self.reference_revision = None 
 
813
 
 
814
    def _unchanged(self, previous_ie):
 
815
        """See InventoryEntry._unchanged."""
 
816
        compatible = super(TreeReference, self)._unchanged(previous_ie)
 
817
        if self.reference_revision != previous_ie.reference_revision:
 
818
            compatible = False
 
819
        return compatible
 
820
 
 
821
 
690
822
class Inventory(object):
691
823
    """Inventory of versioned files in a tree.
692
824
 
706
838
 
707
839
    >>> inv = Inventory()
708
840
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
709
 
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT')
 
841
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
710
842
    >>> inv['123-123'].name
711
843
    'hello.c'
712
844
 
720
852
    May also look up by name:
721
853
 
722
854
    >>> [x[0] for x in inv.iter_entries()]
723
 
    ['hello.c']
 
855
    ['', u'hello.c']
724
856
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
725
857
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
726
 
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
 
858
    Traceback (most recent call last):
 
859
    BzrError: parent_id {TREE_ROOT} not in inventory
 
860
    >>> inv.add(InventoryFile('123-123', 'hello.c', 'TREE_ROOT-12345678-12345678'))
 
861
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None)
727
862
    """
728
 
    def __init__(self, root_id=ROOT_ID):
 
863
    def __init__(self, root_id=ROOT_ID, revision_id=None):
729
864
        """Create or read an inventory.
730
865
 
731
866
        If a working directory is specified, the inventory is read
735
870
        The inventory is created with a default root directory, with
736
871
        an id of None.
737
872
        """
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)
 
873
        if root_id is not None:
 
874
            assert root_id.__class__ == str
 
875
            self._set_root(InventoryDirectory(root_id, u'', None))
 
876
        else:
 
877
            self.root = None
 
878
            self._byid = {}
 
879
        self.revision_id = revision_id
 
880
 
 
881
    def __repr__(self):
 
882
        return "<Inventory object at %x, contents=%r>" % (id(self), self._byid)
 
883
 
 
884
    def apply_delta(self, delta):
 
885
        """Apply a delta to this inventory.
 
886
 
 
887
        :param delta: A list of changes to apply. After all the changes are
 
888
            applied the final inventory must be internally consistent, but it
 
889
            is ok to supply changes which, if only half-applied would have an
 
890
            invalid result - such as supplying two changes which rename two
 
891
            files, 'A' and 'B' with each other : [('A', 'B', 'A-id', a_entry),
 
892
            ('B', 'A', 'B-id', b_entry)].
 
893
 
 
894
            Each change is a tuple, of the form (old_path, new_path, file_id,
 
895
            new_entry).
 
896
            
 
897
            When new_path is None, the change indicates the removal of an entry
 
898
            from the inventory and new_entry will be ignored (using None is
 
899
            appropriate). If new_path is not None, then new_entry must be an
 
900
            InventoryEntry instance, which will be incorporated into the
 
901
            inventory (and replace any existing entry with the same file id).
 
902
            
 
903
            When old_path is None, the change indicates the addition of
 
904
            a new entry to the inventory.
 
905
            
 
906
            When neither new_path nor old_path are None, the change is a
 
907
            modification to an entry, such as a rename, reparent, kind change
 
908
            etc. 
 
909
 
 
910
            The children attribute of new_entry is ignored. This is because
 
911
            this method preserves children automatically across alterations to
 
912
            the parent of the children, and cases where the parent id of a
 
913
            child is changing require the child to be passed in as a separate
 
914
            change regardless. E.g. in the recursive deletion of a directory -
 
915
            the directory's children must be included in the delta, or the
 
916
            final inventory will be invalid.
 
917
        """
 
918
        children = {}
 
919
        # Remove all affected items which were in the original inventory,
 
920
        # starting with the longest paths, thus ensuring parents are examined
 
921
        # after their children, which means that everything we examine has no
 
922
        # modified children remaining by the time we examine it.
 
923
        for old_path, file_id in sorted(((op, f) for op, np, f, e in delta
 
924
                                        if op is not None), reverse=True):
 
925
            if file_id not in self:
 
926
                # adds come later
 
927
                continue
 
928
            # Preserve unaltered children of file_id for later reinsertion.
 
929
            children[file_id] = getattr(self[file_id], 'children', {})
 
930
            # Remove file_id and the unaltered children. If file_id is not
 
931
            # being deleted it will be reinserted back later.
 
932
            self.remove_recursive_id(file_id)
 
933
        # Insert all affected which should be in the new inventory, reattaching
 
934
        # their children if they had any. This is done from shortest path to
 
935
        # longest, ensuring that items which were modified and whose parents in
 
936
        # the resulting inventory were also modified, are inserted after their
 
937
        # parents.
 
938
        for new_path, new_entry in sorted((np, e) for op, np, f, e in
 
939
                                          delta if np is not None):
 
940
            if new_entry.kind == 'directory':
 
941
                new_entry.children = children.get(new_entry.file_id, {})
 
942
            self.add(new_entry)
 
943
 
 
944
    def _set_root(self, ie):
 
945
        self.root = ie
743
946
        self._byid = {self.root.file_id: self.root}
744
947
 
745
 
 
746
948
    def copy(self):
747
 
        other = Inventory(self.root.file_id)
 
949
        # TODO: jam 20051218 Should copy also copy the revision_id?
 
950
        entries = self.iter_entries()
 
951
        if self.root is None:
 
952
            return Inventory(root_id=None)
 
953
        other = Inventory(entries.next()[1].file_id)
748
954
        # copy recursively so we know directories will be added before
749
955
        # their children.  There are more efficient ways than this...
750
 
        for path, entry in self.iter_entries():
751
 
            if entry == self.root:
752
 
                continue
 
956
        for path, entry in entries:
753
957
            other.add(entry.copy())
754
958
        return other
755
959
 
756
 
 
757
960
    def __iter__(self):
758
961
        return iter(self._byid)
759
962
 
760
 
 
761
963
    def __len__(self):
762
964
        """Returns number of entries."""
763
965
        return len(self._byid)
764
966
 
765
 
 
766
967
    def iter_entries(self, from_dir=None):
767
968
        """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
 
 
 
969
        if from_dir is None:
 
970
            if self.root is None:
 
971
                return
 
972
            from_dir = self.root
 
973
            yield '', self.root
 
974
        elif isinstance(from_dir, basestring):
 
975
            from_dir = self._byid[from_dir]
 
976
            
 
977
        # unrolling the recursive called changed the time from
 
978
        # 440ms/663ms (inline/total) to 116ms/116ms
 
979
        children = from_dir.children.items()
 
980
        children.sort()
 
981
        children = collections.deque(children)
 
982
        stack = [(u'', children)]
 
983
        while stack:
 
984
            from_dir_relpath, children = stack[-1]
 
985
 
 
986
            while children:
 
987
                name, ie = children.popleft()
 
988
 
 
989
                # we know that from_dir_relpath never ends in a slash
 
990
                # and 'f' doesn't begin with one, we can do a string op, rather
 
991
                # than the checks of pathjoin(), though this means that all paths
 
992
                # start with a slash
 
993
                path = from_dir_relpath + '/' + name
 
994
 
 
995
                yield path[1:], ie
 
996
 
 
997
                if ie.kind != 'directory':
 
998
                    continue
 
999
 
 
1000
                # But do this child first
 
1001
                new_children = ie.children.items()
 
1002
                new_children.sort()
 
1003
                new_children = collections.deque(new_children)
 
1004
                stack.append((path, new_children))
 
1005
                # Break out of inner loop, so that we start outer loop with child
 
1006
                break
 
1007
            else:
 
1008
                # if we finished all children, pop it off the stack
 
1009
                stack.pop()
 
1010
 
 
1011
    def iter_entries_by_dir(self, from_dir=None, specific_file_ids=None,
 
1012
        yield_parents=False):
 
1013
        """Iterate over the entries in a directory first order.
 
1014
 
 
1015
        This returns all entries for a directory before returning
 
1016
        the entries for children of a directory. This is not
 
1017
        lexicographically sorted order, and is a hybrid between
 
1018
        depth-first and breadth-first.
 
1019
 
 
1020
        :param yield_parents: If True, yield the parents from the root leading
 
1021
            down to specific_file_ids that have been requested. This has no
 
1022
            impact if specific_file_ids is None.
 
1023
        :return: This yields (path, entry) pairs
 
1024
        """
 
1025
        if specific_file_ids and not isinstance(specific_file_ids, set):
 
1026
            specific_file_ids = set(specific_file_ids)
 
1027
        # TODO? Perhaps this should return the from_dir so that the root is
 
1028
        # yielded? or maybe an option?
 
1029
        if from_dir is None:
 
1030
            if self.root is None:
 
1031
                return
 
1032
            # Optimize a common case
 
1033
            if (not yield_parents and specific_file_ids is not None and
 
1034
                len(specific_file_ids) == 1):
 
1035
                file_id = list(specific_file_ids)[0]
 
1036
                if file_id in self:
 
1037
                    yield self.id2path(file_id), self[file_id]
 
1038
                return 
 
1039
            from_dir = self.root
 
1040
            if (specific_file_ids is None or yield_parents or
 
1041
                self.root.file_id in specific_file_ids):
 
1042
                yield u'', self.root
 
1043
        elif isinstance(from_dir, basestring):
 
1044
            from_dir = self._byid[from_dir]
 
1045
 
 
1046
        if specific_file_ids is not None:
 
1047
            # TODO: jam 20070302 This could really be done as a loop rather
 
1048
            #       than a bunch of recursive calls.
 
1049
            parents = set()
 
1050
            byid = self._byid
 
1051
            def add_ancestors(file_id):
 
1052
                if file_id not in byid:
 
1053
                    return
 
1054
                parent_id = byid[file_id].parent_id
 
1055
                if parent_id is None:
 
1056
                    return
 
1057
                if parent_id not in parents:
 
1058
                    parents.add(parent_id)
 
1059
                    add_ancestors(parent_id)
 
1060
            for file_id in specific_file_ids:
 
1061
                add_ancestors(file_id)
 
1062
        else:
 
1063
            parents = None
 
1064
            
 
1065
        stack = [(u'', from_dir)]
 
1066
        while stack:
 
1067
            cur_relpath, cur_dir = stack.pop()
 
1068
 
 
1069
            child_dirs = []
 
1070
            for child_name, child_ie in sorted(cur_dir.children.iteritems()):
 
1071
 
 
1072
                child_relpath = cur_relpath + child_name
 
1073
 
 
1074
                if (specific_file_ids is None or 
 
1075
                    child_ie.file_id in specific_file_ids or
 
1076
                    (yield_parents and child_ie.file_id in parents)):
 
1077
                    yield child_relpath, child_ie
 
1078
 
 
1079
                if child_ie.kind == 'directory':
 
1080
                    if parents is None or child_ie.file_id in parents:
 
1081
                        child_dirs.append((child_relpath+'/', child_ie))
 
1082
            stack.extend(reversed(child_dirs))
 
1083
 
 
1084
    def make_entry(self, kind, name, parent_id, file_id=None):
 
1085
        """Simple thunk to bzrlib.inventory.make_entry."""
 
1086
        return make_entry(kind, name, parent_id, file_id)
782
1087
 
783
1088
    def entries(self):
784
1089
        """Return list of (path, ie) for all entries except the root.
790
1095
            kids = dir_ie.children.items()
791
1096
            kids.sort()
792
1097
            for name, ie in kids:
793
 
                child_path = pathjoin(dir_path, name)
 
1098
                child_path = osutils.pathjoin(dir_path, name)
794
1099
                accum.append((child_path, ie))
795
1100
                if ie.kind == 'directory':
796
1101
                    descend(ie, child_path)
798
1103
        descend(self.root, u'')
799
1104
        return accum
800
1105
 
801
 
 
802
1106
    def directories(self):
803
1107
        """Return (path, entry) pairs for all directories, including the root.
804
1108
        """
810
1114
            kids.sort()
811
1115
 
812
1116
            for name, child_ie in kids:
813
 
                child_path = pathjoin(parent_path, name)
 
1117
                child_path = osutils.pathjoin(parent_path, name)
814
1118
                descend(child_ie, child_path)
815
1119
        descend(self.root, u'')
816
1120
        return accum
817
1121
        
818
 
 
819
 
 
820
1122
    def __contains__(self, file_id):
821
1123
        """True if this entry contains a file with given id.
822
1124
 
823
1125
        >>> inv = Inventory()
824
1126
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
825
 
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
 
1127
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
826
1128
        >>> '123' in inv
827
1129
        True
828
1130
        >>> '456' in inv
829
1131
        False
830
1132
        """
831
 
        return file_id in self._byid
832
 
 
 
1133
        return (file_id in self._byid)
833
1134
 
834
1135
    def __getitem__(self, file_id):
835
1136
        """Return the entry for given file_id.
836
1137
 
837
1138
        >>> inv = Inventory()
838
1139
        >>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
839
 
        InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT')
 
1140
        InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
840
1141
        >>> inv['123123'].name
841
1142
        'hello.c'
842
1143
        """
843
1144
        try:
844
1145
            return self._byid[file_id]
845
1146
        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
 
 
 
1147
            # really we're passing an inventory, not a tree...
 
1148
            raise errors.NoSuchId(self, file_id)
851
1149
 
852
1150
    def get_file_kind(self, file_id):
853
1151
        return self._byid[file_id].kind
855
1153
    def get_child(self, parent_id, filename):
856
1154
        return self[parent_id].children.get(filename)
857
1155
 
 
1156
    def _add_child(self, entry):
 
1157
        """Add an entry to the inventory, without adding it to its parent"""
 
1158
        if entry.file_id in self._byid:
 
1159
            raise BzrError("inventory already contains entry with id {%s}" %
 
1160
                           entry.file_id)
 
1161
        self._byid[entry.file_id] = entry
 
1162
        for child in getattr(entry, 'children', {}).itervalues():
 
1163
            self._add_child(child)
 
1164
        return entry
858
1165
 
859
1166
    def add(self, entry):
860
1167
        """Add entry to inventory.
865
1172
        Returns the new entry object.
866
1173
        """
867
1174
        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):
 
1175
            raise errors.DuplicateFileId(entry.file_id,
 
1176
                                         self._byid[entry.file_id])
 
1177
 
 
1178
        if entry.parent_id is None:
 
1179
            assert self.root is None and len(self._byid) == 0
 
1180
            self.root = entry
 
1181
        else:
 
1182
            try:
 
1183
                parent = self._byid[entry.parent_id]
 
1184
            except KeyError:
 
1185
                raise BzrError("parent_id {%s} not in inventory" %
 
1186
                               entry.parent_id)
 
1187
 
 
1188
            if entry.name in parent.children:
 
1189
                raise BzrError("%s is already versioned" %
 
1190
                        osutils.pathjoin(self.id2path(parent.file_id),
 
1191
                        entry.name).encode('utf-8'))
 
1192
            parent.children[entry.name] = entry
 
1193
        return self._add_child(entry)
 
1194
 
 
1195
    def add_path(self, relpath, kind, file_id=None, parent_id=None):
888
1196
        """Add entry from a path.
889
1197
 
890
1198
        The immediate parent must already be versioned.
891
1199
 
892
1200
        Returns the new entry object."""
893
 
        from bzrlib.workingtree import gen_file_id
894
1201
        
895
 
        parts = bzrlib.osutils.splitpath(relpath)
 
1202
        parts = osutils.splitpath(relpath)
 
1203
 
896
1204
        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)
 
1205
            if file_id is None:
 
1206
                file_id = generate_ids.gen_root_id()
 
1207
            self.root = InventoryDirectory(file_id, '', None)
 
1208
            self._byid = {self.root.file_id: self.root}
 
1209
            return self.root
912
1210
        else:
913
 
            raise BzrError("unknown kind %r" % kind)
 
1211
            parent_path = parts[:-1]
 
1212
            parent_id = self.path2id(parent_path)
 
1213
            if parent_id is None:
 
1214
                raise errors.NotVersionedError(path=parent_path)
 
1215
        ie = make_entry(kind, parts[-1], parent_id, file_id)
914
1216
        return self.add(ie)
915
1217
 
916
 
 
917
1218
    def __delitem__(self, file_id):
918
1219
        """Remove entry by id.
919
1220
 
920
1221
        >>> inv = Inventory()
921
1222
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
922
 
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
 
1223
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
923
1224
        >>> '123' in inv
924
1225
        True
925
1226
        >>> del inv['123']
928
1229
        """
929
1230
        ie = self[file_id]
930
1231
 
931
 
        assert self[ie.parent_id].children[ie.name] == ie
 
1232
        assert ie.parent_id is None or \
 
1233
            self[ie.parent_id].children[ie.name] == ie
932
1234
        
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
1235
        del self._byid[file_id]
941
 
        del self[ie.parent_id].children[ie.name]
942
 
 
 
1236
        if ie.parent_id is not None:
 
1237
            del self[ie.parent_id].children[ie.name]
943
1238
 
944
1239
    def __eq__(self, other):
945
1240
        """Compare two sets by comparing their contents.
949
1244
        >>> i1 == i2
950
1245
        True
951
1246
        >>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
952
 
        InventoryFile('123', 'foo', parent_id='TREE_ROOT')
 
1247
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
953
1248
        >>> i1 == i2
954
1249
        False
955
1250
        >>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
956
 
        InventoryFile('123', 'foo', parent_id='TREE_ROOT')
 
1251
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
957
1252
        >>> i1 == i2
958
1253
        True
959
1254
        """
960
1255
        if not isinstance(other, Inventory):
961
1256
            return NotImplemented
962
1257
 
963
 
        if len(self._byid) != len(other._byid):
964
 
            # shortcut: obviously not the same
965
 
            return False
966
 
 
967
1258
        return self._byid == other._byid
968
1259
 
969
 
 
970
1260
    def __ne__(self, other):
971
1261
        return not self.__eq__(other)
972
1262
 
973
 
 
974
1263
    def __hash__(self):
975
1264
        raise ValueError('not hashable')
976
1265
 
 
1266
    def _iter_file_id_parents(self, file_id):
 
1267
        """Yield the parents of file_id up to the root."""
 
1268
        while file_id is not None:
 
1269
            try:
 
1270
                ie = self._byid[file_id]
 
1271
            except KeyError:
 
1272
                raise errors.NoSuchId(tree=None, file_id=file_id)
 
1273
            yield ie
 
1274
            file_id = ie.parent_id
977
1275
 
978
1276
    def get_idpath(self, file_id):
979
1277
        """Return a list of file_ids for the path to an entry.
984
1282
        root directory as depth 1.
985
1283
        """
986
1284
        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
 
1285
        for parent in self._iter_file_id_parents(file_id):
 
1286
            p.insert(0, parent.file_id)
994
1287
        return p
995
1288
 
996
 
 
997
1289
    def id2path(self, file_id):
998
 
        """Return as a list the path to file_id.
 
1290
        """Return as a string the path to file_id.
999
1291
        
1000
1292
        >>> i = Inventory()
1001
1293
        >>> e = i.add(InventoryDirectory('src-id', 'src', ROOT_ID))
1004
1296
        src/foo.c
1005
1297
        """
1006
1298
        # 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 ''
 
1299
        return '/'.join(reversed(
 
1300
            [parent.name for parent in 
 
1301
             self._iter_file_id_parents(file_id)][:-1]))
1012
1302
            
1013
 
 
1014
 
 
1015
1303
    def path2id(self, name):
1016
1304
        """Walk down through directories to return entry of last component.
1017
1305
 
1021
1309
        This returns the entry of the last component in the path,
1022
1310
        which may be either a file or a directory.
1023
1311
 
1024
 
        Returns None iff the path is not found.
 
1312
        Returns None IFF the path is not found.
1025
1313
        """
1026
 
        if isinstance(name, types.StringTypes):
1027
 
            name = splitpath(name)
 
1314
        if isinstance(name, basestring):
 
1315
            name = osutils.splitpath(name)
1028
1316
 
1029
 
        mutter("lookup path %r" % name)
 
1317
        # mutter("lookup path %r" % name)
1030
1318
 
1031
1319
        parent = self.root
 
1320
        if parent is None:
 
1321
            return None
1032
1322
        for f in name:
1033
1323
            try:
1034
 
                cie = parent.children[f]
 
1324
                children = getattr(parent, 'children', None)
 
1325
                if children is None:
 
1326
                    return None
 
1327
                cie = children[f]
1035
1328
                assert cie.name == f
1036
1329
                assert cie.parent_id == parent.file_id
1037
1330
                parent = cie
1041
1334
 
1042
1335
        return parent.file_id
1043
1336
 
1044
 
 
1045
1337
    def has_filename(self, names):
1046
1338
        return bool(self.path2id(names))
1047
1339
 
1048
 
 
1049
1340
    def has_id(self, file_id):
1050
 
        return self._byid.has_key(file_id)
 
1341
        return (file_id in self._byid)
1051
1342
 
 
1343
    def remove_recursive_id(self, file_id):
 
1344
        """Remove file_id, and children, from the inventory.
 
1345
        
 
1346
        :param file_id: A file_id to remove.
 
1347
        """
 
1348
        to_find_delete = [self._byid[file_id]]
 
1349
        to_delete = []
 
1350
        while to_find_delete:
 
1351
            ie = to_find_delete.pop()
 
1352
            to_delete.append(ie.file_id)
 
1353
            if ie.kind == 'directory':
 
1354
                to_find_delete.extend(ie.children.values())
 
1355
        for file_id in reversed(to_delete):
 
1356
            ie = self[file_id]
 
1357
            del self._byid[file_id]
 
1358
        if ie.parent_id is not None:
 
1359
            del self[ie.parent_id].children[ie.name]
 
1360
        else:
 
1361
            self.root = None
1052
1362
 
1053
1363
    def rename(self, file_id, new_parent_id, new_name):
1054
1364
        """Move a file within the inventory.
1055
1365
 
1056
1366
        This can change either the name, or the parent, or both.
1057
1367
 
1058
 
        This does not move the working file."""
 
1368
        This does not move the working file.
 
1369
        """
 
1370
        new_name = ensure_normalized_name(new_name)
1059
1371
        if not is_valid_name(new_name):
1060
1372
            raise BzrError("not an acceptable filename: %r" % new_name)
1061
1373
 
1079
1391
        file_ie.name = new_name
1080
1392
        file_ie.parent_id = new_parent_id
1081
1393
 
1082
 
 
 
1394
    def is_root(self, file_id):
 
1395
        return self.root is not None and file_id == self.root.file_id
 
1396
 
 
1397
 
 
1398
entry_factory = {
 
1399
    'directory': InventoryDirectory,
 
1400
    'file': InventoryFile,
 
1401
    'symlink': InventoryLink,
 
1402
    'tree-reference': TreeReference
 
1403
}
 
1404
 
 
1405
def make_entry(kind, name, parent_id, file_id=None):
 
1406
    """Create an inventory entry.
 
1407
 
 
1408
    :param kind: the type of inventory entry to create.
 
1409
    :param name: the basename of the entry.
 
1410
    :param parent_id: the parent_id of the entry.
 
1411
    :param file_id: the file_id to use. if None, one will be created.
 
1412
    """
 
1413
    if file_id is None:
 
1414
        file_id = generate_ids.gen_file_id(name)
 
1415
    name = ensure_normalized_name(name)
 
1416
    try:
 
1417
        factory = entry_factory[kind]
 
1418
    except KeyError:
 
1419
        raise BzrError("unknown kind %r" % kind)
 
1420
    return factory(file_id, name, parent_id)
 
1421
 
 
1422
 
 
1423
def ensure_normalized_name(name):
 
1424
    """Normalize name.
 
1425
 
 
1426
    :raises InvalidNormalization: When name is not normalized, and cannot be
 
1427
        accessed on this platform by the normalized path.
 
1428
    :return: The NFC/NFKC normalised version of name.
 
1429
    """
 
1430
    #------- This has been copied to bzrlib.dirstate.DirState.add, please
 
1431
    # keep them synchronised.
 
1432
    # we dont import normalized_filename directly because we want to be
 
1433
    # able to change the implementation at runtime for tests.
 
1434
    norm_name, can_access = osutils.normalized_filename(name)
 
1435
    if norm_name != name:
 
1436
        if can_access:
 
1437
            return norm_name
 
1438
        else:
 
1439
            # TODO: jam 20060701 This would probably be more useful
 
1440
            #       if the error was raised with the full path
 
1441
            raise errors.InvalidNormalization(name)
 
1442
    return name
1083
1443
 
1084
1444
 
1085
1445
_NAME_RE = None
1086
1446
 
1087
1447
def is_valid_name(name):
1088
1448
    global _NAME_RE
1089
 
    if _NAME_RE == None:
 
1449
    if _NAME_RE is None:
1090
1450
        _NAME_RE = re.compile(r'^[^/\\]+$')
1091
1451
        
1092
1452
    return bool(_NAME_RE.match(name))